NAME
Fugu::Signify - make a signify(1) key pair, sign a file, verify a signature, and read and write a SHA256 manifest
SYNOPSIS
use Fugu::Signify;
my $sig = Fugu::Signify->new;
die $sig->error unless $sig->is_available;
my $key = $sig->verify(
keys => [
'/etc/signify/openbsd-78-base.pub',
'/etc/signify/openbsd-79-base.pub',
],
file => '/var/cache/SHA256',
signature => '/var/cache/SHA256.sig',
) or die $sig->error;
$key = $sig->verify_manifest(
keys => ['/etc/signify/openbsd-78-base.pub'],
manifest => '/var/cache/SHA256',
signature => '/var/cache/SHA256.sig',
files => { 'miniroot78.img' => $tmp_path },
) or die $sig->error;
# One object serves the generator and the signer too.
$sig->generate(
comment => 'example release 01',
public => '/etc/signify/example-01.pub',
secret => '/etc/signify/example-01.sec',
) or die $sig->error;
$sig->sign(
secret => '/etc/signify/example-01.sec',
file => '/var/cache/SHA256',
signature => '/var/cache/SHA256.sig',
) or die $sig->error;
DESCRIPTION
Fugu::Signify follows Fugu::Signer over signify(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 signify(1) file formats, the manifest methods, and the two engines.
The module verifies a file against a signify(1) public key and a signature file. It also verifies each named file of a signed SHA256 manifest against its digest. It reads and writes the manifest form itself, so a producer and a checker share one implementation. It computes each manifest digest with core Digest::SHA, and the digest streams from a file handle, so a large file never enters memory whole.
The module also makes a key pair and signs a file. Fugu::Ed25519 holds no private key operation, so generate and sign run signify(1) under either engine. Each one names a key file as a path, so no key byte enters Perl. The module holds no private key of its own.
The object holds no key set. One object serves the generator, the signer, the verifier and the manifest readers, and each verification names the keys of the call.
The two engines
The module verifies with two engines, and engine names the one to take. The engine selects the verifier alone.
The perl engine parses the signify(1) file formats itself and checks the signature with Fugu::Ed25519. It needs core Perl alone, so a host verifies a release with no signify(1) installed. It is the default.
The signify engine runs signify(1) through Fugu::Process, with an argument list and never a shell. A caller that names a command asks for the command, so that call takes this engine by default.
Both engines answer the same on the same input, and both write the same error shape. verify_manifest calls verify, so both engines serve it.
generate and sign run signify(1) under either engine, because Perl holds no private key operation.
new resolves the command under either engine, so command answers the resolved path, or undef. Under the perl engine an absent command is no failure of the object: is_available returns 1, and error holds no reason. The first generate or sign of such an object then reports the absent command itself.
Every recoverable failure returns undef, and error holds the reason. An absent signify(1) is a clean failure, and not a die. The module never logs. The caller decides what to report.
new
new(engine => $engine, command => $command, timeout => $seconds) builds a generator, a signer and a verifier. The method resolves the command once, and it runs no process.
engine is perl or signify. The default is perl, except that a caller who names a command gets signify. new dies for any other value, because it is a programming error.
command names the signify command, as a name or as an absolute path. Without the argument, the module walks $ENV{PATH} over the search list signify-openbsd, then signify.
timeout is the bound of one run of the command, in seconds, and the default is 30.
new must not die for an absent command. Under the signify engine it sets error instead, and is_available then returns 0.
is_available
is_available() returns 1 when the object can verify. The perl engine always can, so it returns 1 with an empty $ENV{PATH}. The signify engine returns 1 when new resolved an executable command, and 0 otherwise. The method runs no process, and it never dies.
The answer describes verification alone. generate and sign need the command under either engine, and each one reports its own failure.
command
command() returns the resolved command path, or undef, under either engine. An operator who installed the wrong signify 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.
For a signature that no key verified, the string names the file. It then names each key with the reason of that key. The signify engine writes the first line of the signify(1) diagnostic, or the reason that the command gave no line. The perl engine writes the parser reason, the reason that the module could not read the file, or the reason that the check against that key failed. The signify engine writes this:
/var/cache/SHA256: no key verified the signature:
/etc/signify/openbsd-78-base.pub: signature verification failed;
/etc/signify/openbsd-79-base.pub: can't open /etc/signify/openbsd-79-base.pub
A caller thus tells a wrong key from an absent key file.
command_absent
command_absent() returns 1 when the most recent failure means that signify(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. A verification under the perl engine runs no command, so it always returns 0. generate and sign run the command under either engine, so each one can report 1.
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(comment => $text, public => $path, secret => $path) makes a key pair with no passphrase. Each argument is necessary, and the method dies without one. It returns 1, or undef with the reason in error.
signify(1) writes each half itself, so no key byte enters Perl, and no key byte reaches a log. It writes comment in the untrusted comment line of each half, and it appends public key or secret key to that text.
comment must hold no newline. The text reaches the first line of each half, and one line holds one field. A newline would write a third line into the key file, and the parsers of this module refuse such a file. The method refuses the comment before the command runs, so the call writes no key.
signify(1) holds the two paths to one naming scheme: the stem of public and the stem of secret must agree, as in example-01.pub and example-01.sec. It refuses another pair of names. The parent writes the private half in a directory of its own, and the name of that file does not change there.
The parent refuses a path that exists, so one call never overwrites a key. It writes the private half in a private directory beside the destination, it sets the owner-only mode there, and one rename then moves the file into place. A failed run therefore leaves no half behind.
sign
sign(secret => $path, file => $path, signature => $path) signs one file with a private half. Each argument is necessary, and the method dies without one. It returns 1, or undef with the reason in error.
signify(1) reads the private half from the path and writes the signature file itself, so no key byte enters Perl. 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.
verify
verify(keys => \@paths, file => $path, signature => $path) verifies one file against the key 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 public key file that verified the signature. It returns undef on every failure. The method fails closed: an absent command under the signify engine, an absent file, an absent signature file, and a signature that no key verified are each a failure.
The perl engine reads the signature file under a bound of 4 KiB and parses it. It then reads and parses the key file under the same bound. A key whose number differs from the signature gives the reason checked against wrong key, and the walk continues. A key whose number matches runs Fugu::Ed25519 over the file, and a failed check gives the reason signature verification failed.
parse_public_key
parse_public_key($bytes) parses a signify(1) public key file. It returns a hash reference with comment, keynum and key, or undef with the reason in error.
A public key file holds two lines. The first line starts with untrusted comment: , and comment is the text after that header. The second line is 56 base64 characters that decode to 42 bytes: the two letters Ed, the 8-byte keynum, and the 32-byte key.
The comment line carries no trust. No signature covers it, and any producer writes any text after the header.
parse_signature
parse_signature($bytes) parses a signify(1) signature file. It returns a hash reference with comment, keynum and signature, or undef with the reason in error.
A signature file holds the same two lines. Its body is 100 base64 characters that decode to 74 bytes: the two letters Ed, the 8-byte keynum, and the 64-byte signature. The signature covers the bytes of the signed file.
The keynum binds a signature to a key. Both parsers report a body of another length, a body with another prefix, and a file with one line only.
verify_manifest
verify_manifest(keys => \@paths, manifest => $path, signature => $path, files => \%map) verifies a signed SHA256 manifest, and then verifies the digest of each named file.
manifest is the path of the signed SHA256 file, and signature is the path of its signature. files is a hash reference: each key is a name in the manifest, and each value is the local path to digest. Each argument is necessary, and files must not be empty. The module must never choose which file to check, so an empty files is a programming error, and the method dies.
The method verifies the manifest signature first, through verify, so it dies for an empty keys list as well. No file is digested before the manifest verifies. It then refuses a manifest above MAX_MANIFEST_SIZE, which is 1 MiB. A name that the manifest does not hold, a local file that does not open, and a digest mismatch are each a failure. A mismatch names the file, the expected digest and the computed digest.
The method returns the public key file that verified the manifest, or undef on every failure.
The manifest name and the local path can differ. A caller therefore verifies a file before it moves the file into place:
my $key = $sig->verify_manifest(
keys => [$current_key],
manifest => "$cache/SHA256",
signature => "$cache/SHA256.sig",
files => { 'miniroot78.img' => $tmp_path },
);
parse_manifest
parse_manifest($bytes) is the public form of the parser that verify_manifest uses. It returns a hash reference of manifest key to lower-case digest.
Two callers read a manifest without a signature at that moment. A rotation writes a manifest, and it must read the file that it wrote. A site check compares a manifest against the files beside it, and a site build cannot sign. A private parser would make each one write the line form again.
The method verifies nothing. A caller that needs the signature calls verify_manifest, which verifies the signature before it digests one file.
An undef input, an empty manifest, a line that the method cannot parse, a digest that is not 64 hexadecimal characters, and a duplicate key are each a failure.
write_manifest
write_manifest(\%digests) returns the text of a SHA256 manifest. Each line holds SHA256 (key) = digest.
The keys sort in ascending order, so two runs of a rotation write one byte sequence. A diff of two manifests then shows the change only. The method lowercases each digest.
The key of a line is a file name, a file path, or a download URL, whichever the producer writes. The method therefore rejects only a key that another reader cannot carry.
parse_manifest takes the text up to the last parenthesis. It therefore reads a key with a parenthesis back without a change. A stricter reader does not.
A parenthesis ends the key in a reader that stops at the first one. Whitespace breaks a reader that splits a line on space. A manifest travels to sha256(1) and to fugubench deps, so the writer holds a key to the strict form.
The whitespace test names the ASCII whitespace alone. A file name that holds a byte above 127 therefore passes.
Each of these is a failure: an empty hash, an empty key, a key with a parenthesis, a key with whitespace, and a digest that is not 64 hexadecimal characters.
The method dies when the argument is not a hash reference.
Neither method needs signify(1). An object whose command did not resolve still parses and still writes:
my $manifest = Fugu::Signify->new;
my $text = $manifest->write_manifest(\%digests);
RETURN VALUES
generate() and sign() return 1, or undef. verify() and verify_manifest() return the public key file that verified the signature, or undef. parse_public_key(), parse_signature() and parse_manifest() each return a hash reference, and write_manifest() returns text; each one returns undef on a failure. error() returns the reason of the most recent failure, or undef.
CAVEATS
The command name differs by platform. OpenBSD base holds signify. The Debian and Ubuntu package signify-openbsd installs the command as signify-openbsd. The Homebrew package signify-osx installs the command as signify.
A caller under taint mode must pass command as an absolute path. $ENV{PATH} is tainted, so a path from the search list cannot reach execve(2) under perl -T.
A caller under pledge(2) needs rpath because the module reads files. A call that runs signify(1) also needs proc exec: every verification under the signify engine, and generate and sign under either engine. A program that verifies under the perl engine alone takes rpath alone, and it therefore pledges less. The pledge belongs to the program, not to a library method.
SEE ALSO
signify(1), sha256(1), Digest::SHA, Fugu::Ed25519, Fugu::File, Fugu::KeyDir, Fugu::Process, Fugu::Signer
AUTHORS
Dick Olsson <hi@senzilla.io>