Fugu

Fugu::Signify(3p) Perl Library Manual Fugu::Signify(3p)

Fugu::Signify - make a signify(1) key pair, sign a file, verify a signature, and read and write a SHA256 manifest

    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;

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 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(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() 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() 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() 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() 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(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(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(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($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($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(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($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(\%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);

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

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.

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

Dick Olsson <hi@senzilla.io>

2026-10-16 OpenBSD