NAME
Fugu::X509 - read an X.509 certificate as bytes, and drive openssl(1) over a certificate
SYNOPSIS
use Fugu::X509;
my $x509 = Fugu::X509->new;
die $x509->error unless $x509->is_available;
$x509->generate(
public => '/etc/keys/example.pem',
secret => '/etc/keys/example.key',
subject => '/C=SE/O=Example/CN=Example Signer',
days => 365,
) or die $x509->error;
$x509->sign(
public => '/etc/keys/example.pem',
secret => '/etc/keys/example.key',
file => '/var/www/htdocs/release.tgz',
signature => '/var/www/htdocs/release.p7s',
) or die $x509->error;
my $key = $x509->verify(
keys => ['/etc/keys/example.pem'],
file => '/var/www/htdocs/release.tgz',
signature => '/var/www/htdocs/release.p7s',
) or die $x509->error;
# The byte reader needs no command.
my $der = $x509->decode_pem($pem_text)
or die $x509->error;
my $fingerprint = $x509->fingerprint($der)
or die $x509->error;
my $certificate = $x509->parse($der)
or die $x509->error;
die 'the certificate expired'
if $certificate->{not_after} < time;
my $team = $certificate->{subject}{OU};
DESCRIPTION
Fugu::X509 follows Fugu::Signer over openssl(1). The parent holds the constructor and the command resolution, the three verbs, the run through Fugu::Process, the key walk of a verification, and the failure convention. This module holds the byte reader, the DER walk, and the arguments of each openssl(1) command line.
The byte reader decodes a PEM certificate block, it computes the SHA-256 fingerprint of the DER bytes, and it reads the subject, the issuer and the validity from the DER itself. It runs no command, and it uses core Digest::SHA and core MIME::Base64, so it adds no dependency. Each reader is a method of the object, and a caller that reads bytes alone needs no openssl(1).
The command part makes a self-signed certificate, it makes a detached CMS signature over a file, and it verifies one. The private key enters as a path and leaves as a file, so no key byte enters Perl.
The module holds no issuer by name. A code signing certificate of Apple Developer ID is one use, and the module reads every issuer the same way. A caller that pins an issuer reads the name that parse answers, and it decides by itself.
Every recoverable failure returns undef, and error holds the reason. No method answers the reason as a second return value. The module never logs. The caller decides what to report.
A reader never dies for bad input: a certificate comes from outside, so bad bytes are data and not a programming error. A method dies for a missing necessary argument alone, because that is a programming error.
The detached signature
sign makes a detached CMS signature: the signature holds the digest of the file and no byte of it. The caller keeps the file and the signature apart, and verify reads both again.
verify pins the one certificate of each step of the walk. A CMS signature carries the certificate of its signer, and the verifier ignores that copy. A signature of another certificate therefore fails with signer certificate not found, and a changed file fails with verification failure.
The verifier checks no chain and no revocation. It also reads no validity: a signature of an expired certificate verifies. The caller vouches for the certificate by other means, such as the fingerprint of a key directory, and it reads the validity with parse.
new
new(command => $command, timeout => $seconds) builds a generator, a signer and a verifier. The method resolves the command once, and it runs no process.
command names the openssl command, as a name or as an absolute path. Without the argument, the module walks $ENV{PATH} for openssl.
timeout is the bound of one run of the command, in seconds, and the default is OPENSSL_TIMEOUT. A signature reads the whole file, so this module raises the default of the parent.
new must not die for an absent command. It sets error instead, and is_available then returns 0.
is_available
is_available() returns 1 when the object resolved an executable command, and 0 otherwise. The method runs no process, and it never dies. The byte reader needs no command, so the answer describes the command part alone.
command
command() returns the resolved command, or undef. An operator who installed the wrong openssl needs this answer, and a caller can put it in a diagnostic.
error
error() returns the reason of the most recent failure. It returns undef after a success. Each method clears the reason before it starts.
The reason of a failed run names the act and then the diagnostic of openssl(1):
cannot sign /var/www/htdocs/release.tgz: unable to load certificate
openssl(1) writes one error record in each line of a stack, and a colon separates the fields. The module takes the reason field of the first record, because that record names the fault and each later one names what the fault broke. The generator writes a progress line of dots and a line of dashes first, and the module steps over each line that holds no letter. Each run takes LC_ALL=C, so the diagnostic arrives in English under any locale of the caller.
command_absent
command_absent() returns 1 when the most recent failure means that openssl(1) never ran. It covers a command that the search list did not resolve, and a command that failed to execve(2). It returns 0 otherwise.
An absent command is an install problem, and a failed signature is an integrity problem. The two answers let a caller tell them apart.
generate
generate(public => $path, secret => $path, subject => $text, days => $number) makes one self-signed certificate and its private key. It returns 1, or undef with the reason in error. Each argument is necessary, and the method dies without one.
openssl(1) writes the certificate and the private key at their paths, so no key byte enters Perl. The key is a KEY_TYPE key, and it carries no passphrase, so the caller owns the storage of the private half.
The parent refuses a path that exists, so one call never overwrites a key. It writes the private key in a private directory beside the destination, it sets the owner-only mode there, and one rename then moves the file into place.
subject takes the form of openssl(1), such as /C=SE/O=Example/CN=Example Signer. The subject reaches the command as one argument: a NUL byte would end that argument, and a line ending would open a second line, so the certificate would carry a subject that the caller never named. The method refuses each of them, and an empty subject, before the command runs.
days is the validity in days, and it must be a whole number of one day or more. openssl(1) writes notBefore at the current second, and notAfter that many days later.
The certificate is its own issuer, and it holds no chain. A test of a signature needs one certificate, and no other generator makes it. A production identity comes from an issuer, and the caller names its files instead.
sign
sign(public => $path, secret => $path, file => $path, signature => $path) makes a detached CMS signature over one file. It returns 1, or undef with the reason in error. Each argument is necessary, and the method dies without one.
Each argument is a path. A PEM private key names no certificate, so the signer takes public beside secret. openssl(1) reads the private key from the path, and it writes the signature file itself, so no key byte enters Perl and no key byte reaches a log. A second call over one signature path replaces the file, because a rotation signs one manifest again.
The parent refuses an input path that is no plain file, before the command runs, and the module refuses the certificate the same way.
The module reads no PKCS#12 file. The caller converts one with openssl pkcs12 before it names the certificate and the private key.
The signer names the digest, SIGNATURE_DIGEST, so an old default of the command never decides it.
verify
verify(keys => \@paths, file => $path, signature => $path) verifies one detached CMS signature against a certificate set, in trust order. Each argument is necessary, and the method dies without one. An empty keys list is a programming error, and the method dies for it too.
The method returns the certificate that verified the signature. It returns undef on every failure. The walk pins one certificate in one run, so a signature of another certificate fails.
The command writes the content of the signature to the null device, because the caller holds the file already. A file of 500 MB therefore never enters memory.
For a signature that no certificate verified, error names the file and then each certificate with its own reason.
decode_pem
decode_pem($text) returns the DER bytes of a PEM certificate block, or undef with the reason in error.
The method reads the two delimiter lines and it decodes the base64 body. A PEM block holds no checksum line, so the method holds the body to the base64 alphabet itself. MIME::Base64 skips a character that no alphabet holds, and a body with one bad character would otherwise decode to a shorter certificate.
The method takes one CERTIFICATE block. A private key block and a second block are each a failure, because a key directory publishes what this method accepts. A directory that holds a certificate and its private key in one file therefore fails, and the operator splits the two.
PEM is text, so the method reads either line ending. A block that travelled through email holds CRLF, and it decodes to the same bytes. The method also trims the space and the tab at the two ends of each body line, because a mailer can pad one. It never changes the string of the caller.
Each of these is a failure:
a missing delimiter line
a block of another type
a second block
a body line that is not base64
a base64 body that is not a whole number of groups
base64 padding before the end of the body
a text above
MAX_PEM_SIZE
fingerprint
fingerprint($der) returns the SHA-256 of the DER bytes, in upper-case hexadecimal with no separator, or undef with the reason in error.
A09E6F717684F203A8CBBE7AA663A933F3C405F306366917225226383664D780
openssl x509 -fingerprint -sha256 prints the same digest, with a colon between two bytes. A caller that holds the answer of this method compares two strings, and not two encodings.
The digest covers the bytes that decode_pem answered, and it reads no field of them. A caller that holds the DER of a certificate therefore names it with one string, and the name changes with each renewal of the certificate.
parse
parse($der) returns a hash reference with subject, issuer, not_before and not_after, or undef with the reason in error.
Each name is a hash of attribute type to value. The type is the short name of openssl(1) for the common types, such as CN, O, OU and C. Every other type arrives under its dotted object identifier, so the reader drops no attribute. Each value arrives as bytes, and a UTF8String holds UTF-8, so a caller that needs characters decodes them.
Two attributes of one type are a failure. A hash holds one value for each type, and a caller that pins a team identifier must never read one of two values.
Each time is seconds since the epoch. RFC 5280 holds a time to two forms: a UTCTime of the form YYMMDDHHMMSSZ, where a year of 50 or above names the last century, and a GeneralizedTime of the form YYYYMMDDHHMMSSZ. A certificate that expires in 2050 or later takes the second form. The method reads each field before it computes, so the 31st of February is a failure and never a date in March.
The walk takes the fields of RFC 5280 section 4.1 in order, and it stops after the subject. The method reads no extension, no public key and no signature. The key usage and the extended key usage stay with the issuer.
The reader takes the definite length forms of DER alone. An indefinite length, a multi-byte tag, a truncated element and bytes after the certificate are each a failure.
CONSTANTS
MAX_PEM_SIZE-
The size bound of a PEM text, 1 MiB. A certificate holds a few kilobytes. A caller that names a disk image by mistake gets a clean failure, not a decode of 500 MB.
PEM_TYPE-
The one PEM block type that
decode_pemtakes,CERTIFICATE. KEY_TYPE-
The key of a generated certificate,
rsa:2048. An RSA key of 2048 bits makes a CMS signature under openssl(1) and under LibreSSL. A newer key type needs a newer command. SIGNATURE_DIGEST-
The digest of a CMS signature,
sha256. OPENSSL_TIMEOUT-
The default time bound of one openssl(1) call, 300 seconds. A command that a caller named can be the wrong program, and a signature over a file of a few hundred megabytes reads the whole file.
MAX_LENGTH_BYTES-
The largest number of DER length bytes that the reader takes, 4.
RETURN VALUES
Each method returns its answer, or undef on a failure, and error holds the reason of that failure. No method answers the reason as a second return value.
Each reader rejects a string that holds a code point above 255. Digest::SHA dies on such a string, and a byte read takes the low byte of each character. A caller that holds text must encode it first.
CAVEATS
A fingerprint proves which bytes the certificate holds. It proves nothing about who made them, and nothing about the validity of the certificate. verify answers the second question, over the certificate set that the caller names.
The certificate of a code signing identity lives a few years, and the fingerprint of the leaf changes at each renewal. A key directory that pins a fingerprint therefore publishes the new one at each renewal. A caller that pins the subject instead reads CN and OU from parse, and those hold across a renewal.
The generator makes a self-signed certificate alone. It makes no certificate request, and it reads no PKCS#12 file. An issuer makes a chain, and this module never checks one.
SEE ALSO
openssl(1), Digest::SHA, MIME::Base64, Fugu::KeyDir, Fugu::OpenPGP, Fugu::Process, Fugu::Signer, Fugu::Signify
AUTHORS
Dick Olsson <hi@senzilla.io>