=head1 NAME

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

=head1 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;

=head1 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
L<Fugu::Process>, with an argument list and never a shell. It computes
each manifest digest with core L<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 C<undef>, and C<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.

=head2 new

C<new(keys =E<gt> \@paths, command =E<gt> $command)> builds a
verifier. The method resolves the command once, and it runs no
process.

C<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.
C<new> dies when C<keys> is absent, when it is not an array
reference, and when it is empty. Each one is a programming error.

C<command> names the signify command, as a name or as an absolute
path. Without the argument, the module walks C<$ENV{PATH}> over the
search list C<signify-openbsd>, then C<signify>.

C<new> must not die for an absent command. It sets C<error> instead,
and C<is_available> then returns 0.

=head2 is_available

C<is_available()> returns 1 when the object resolved an executable
command. It returns 0 otherwise. The method runs no process, and it
never dies.

=head2 command

C<command()> returns the resolved command, or C<undef>. An operator
who installed the wrong C<signify> needs this answer, and a caller
can put it in a diagnostic.

=head2 error

C<error()> returns the reason of the most recent failure. It returns
C<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.

=head2 command_absent

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

=head2 verify

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

The method returns the public key file that verified the signature.
It returns C<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.

=head2 verify_manifest

C<verify_manifest(manifest =E<gt> $path, signature =E<gt> $path,
files =E<gt> \%map)> verifies a signed SHA256 manifest, and then
verifies the digest of each named file.

C<manifest> is the path of the signed SHA256 file. The argument is
necessary. C<signature> defaults to C<"$manifest.sig">. C<files> is a
hash reference: each key is a name in the manifest, and each value is
the local path to digest. C<files> is necessary and must not be
empty. The module must never choose which file to check, so an empty
C<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
C<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
C<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 },
    );

=head1 RETURN VALUES

C<verify()> and C<verify_manifest()> return the public key file that
verified the signature, or C<undef>. C<error()> returns the reason of
the most recent failure, or C<undef>.

=head1 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 C<signify>.
The Debian and Ubuntu package C<signify-openbsd> installs the command
as C<signify-openbsd>. The Homebrew package C<signify-osx> installs
the command as C<signify>.

A caller under taint mode must pass C<command> as an absolute path.
C<$ENV{PATH}> is tainted, so a path from the search list cannot reach
execve(2) under C<perl -T>.

A caller under pledge(2) needs two promises: C<rpath> because the
module reads files, and C<proc exec> because the module runs a
command. The pledge belongs to the program, not to a library method.

=head1 SEE ALSO

signify(1), sha256(1), L<Digest::SHA>, L<Fugu::File>, L<Fugu::Process>

=head1 AUTHORS

Dick Olsson E<lt>hi@senzilla.ioE<gt>

=cut
