NAME

Fugu::Signify - verify a signify(1) signature and 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. 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 },
);

RETURN VALUES

verify() and verify_manifest() return the public key file that verified the signature, or undef. 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::Process

AUTHORS

Dick Olsson <hi@senzilla.io>