NAME

Fugu::Signify - verify a signify(1) signature, and read and write a SHA256 manifest

SYNOPSIS

use Fugu::Signify;

my $sig = Fugu::Signify->new(
    keys => [
        '/etc/signify/openbsd-78-base.pub',
        '/etc/signify/openbsd-79-base.pub',
    ],
);

die $sig->error unless $sig->is_available;

my $key = $sig->verify('/var/cache/SHA256', '/var/cache/SHA256.sig')
    or die $sig->error;

$key = $sig->verify_manifest(
    manifest => '/var/cache/SHA256',
    files    => { 'miniroot78.img' => $tmp_path },
) or die $sig->error;

DESCRIPTION

Fugu::Signify 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. The module runs signify(1) through Fugu::Process, with an argument list and never a shell. 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 object holds a small key set, in trust order. A caller puts the current key first and the next key second. The verification methods return the key that matched, so a caller learns that a release moved to the next key.

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(keys => \@paths, command => $command) builds a verifier. The method resolves the command once, and it runs no process.

keys is an array reference of public key file paths. The argument is necessary and must not be empty. The order is the trust order. new dies when keys is absent, when it is not an array reference, and when it is empty. Each one 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.

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. It returns 0 otherwise. The method runs no process, and it never dies.

command

command() returns the resolved command, or undef. 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 verification 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 first line of its signify diagnostic:

/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.

An absent command is an install problem, and a failed signature is an integrity problem. The two answers let a caller tell them apart.

verify

verify($file, $sigfile) verifies one file against the key set, in order. $sigfile defaults to "$file.sig", which is the default of signify(1) itself.

The method returns the public key file that verified the signature. It returns undef on every failure. The method fails closed: an absent command, an absent file, an absent signature file, and a signature that no key verified are each a failure.

verify_manifest

verify_manifest(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. The argument is necessary. signature defaults to "$manifest.sig". files is a hash reference: each key is a name in the manifest, and each value is the local path to digest. files is necessary and 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. 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(
    manifest => "$cache/SHA256",
    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 scripts/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.

new still needs a non-empty keys, because the verification methods need a key set. A caller that only parses or writes names any path, and it never reads error from the construction:

my $manifest = Fugu::Signify->new(keys => ['/nonexistent']);
my $text     = $manifest->write_manifest(\%digests);

RETURN VALUES

verify() and verify_manifest() return the public key file that verified the signature, or undef. parse_manifest() returns 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 module cannot sign, and it takes no secret key path. A signature is a human act, without exception.

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 two promises: rpath because the module reads files, and proc exec because the module runs a command. The pledge belongs to the program, not to a library method.

SEE ALSO

signify(1), sha256(1), Digest::SHA, Fugu::File, Fugu::KeyDir, Fugu::Process

AUTHORS

Dick Olsson <hi@senzilla.io>