NAME
App::FuguWeb::Keys - the key directory of a site
SYNOPSIS
use App::FuguWeb::Keys;
my $keys = App::FuguWeb::Keys->new(config => $config);
my @paths = $keys->paths;
my $generated = $keys->generated;
my @problems = $keys->problems;
DESCRIPTION
An organization publishes its public keys under one prefix, so a consumer install can fetch a key and verify a release with it. This class is the wiring of that directory. It reads the description blocks and calls the Fugu modules. It answers with paths, with bytes, and with the problems of a check.
Every generic part lives in Fugu. Fugu::KeyDir holds the name pattern, the publication order, and the text of the KEYS file and of security.txt. Fugu::OpenPGP decodes an armored key and computes a fingerprint and a Web Key Directory hash. Fugu::Signify parses the manifest. Nothing generic lives here.
The class runs no command. A site build neither signs nor verifies, so the manifest pair is a source file and not a generated one.
A description with no keys block has no key directory. A caller tests $config->keys_dir first, and this class dies without one.
THE DESCRIPTION
One keys block names the key directory. The name is one segment below the source directory. The block also names the organization word:
keys "keys" {
org = fugubsd
contact = mailto:security@fugubsd.org
expires = 2027-09-07T00:00:00Z
url = https://www.fugubsd.org/keys
}
key "fugubsd-1-release" {
status = current
since = 2026-09-07
}
The block name of a key block is the stem of the key file. The extension of the file on disk decides the type: .pub is a signify key, and .asc is an armored OpenPGP key. The status is current, next or retired.
App::FuguWeb::Config reads and validates both blocks. It resolves each block to its file, so this class repeats no name pattern.
THE PUBLISHED TREE
keys/<stem>.pub each key file, byte for byte
keys/<stem>.asc
keys/SHA256 the manifest pair, copied as it stands
keys/SHA256.sig
keys/KEYS every OpenPGP key, in the Apache form
keys/index.html the human page
.well-known/openpgpkey/hu/<hash> the Web Key Directory
.well-known/openpgpkey/policy
.well-known/security.txt RFC 9116
The two well-known paths sit at the site root, because a reader asks for the registered path and never for one below the key directory.
One Web Key Directory file holds every key of one address, in publication order. A rotation gives one address a current key and a next key, and a reader takes the whole file.
An address whose keys are all retired serves no file. gpg --locate-keys reads that file to encrypt a message, and a retired key is the one key that must not answer it.
METHODS
new
App::FuguWeb::Keys->new(config => $config)
config is an App::FuguWeb::Config and is required. The method dies when the description holds no keys block.
error
$keys->error
The reason of the most recent failure, or undef after a success.
paths
$keys->paths
Every path that the key directory adds to the output, relative to the output directory. The method reads no file: the description and the file names decide the list.
copies
$keys->copies
Every file that the build copies as it stands, as a list of hash references with from and to.
generated
$keys->generated
Every file that the build writes itself, as a hash reference of output path to bytes. The method returns undef on a failure, and error holds the reason.
key_set
$keys->key_set
The key set for Fugu::KeyDir: every key block of the description, with the armored body of each OpenPGP key read from its file.
problems
$keys->problems
What the key directory of the checkout is not true of, each one a sentence that names the file. An empty list means the directory is good.
The checks read the source directory and not the output. A stray file and a stale digest are faults of the checkout, so the answer must not depend on a build having run.
shaped
App::FuguWeb::Keys->shaped($config, $path)
Whether a path of the output is one that a build of the key directory writes. The set is bounded: a generated name of the key directory, a key file that Fugu::KeyDir parses, and the three well-known paths.
The prune and the clean read this one answer, so a build can never remove a file that the clean refuses. A stale key file needs it: the inventory names what the site holds today, so it cannot answer for a key that the description dropped.
A description with no keys block owns no path at all. A site of another maker holds .well-known/security.txt too, and a clean that took it would delete that site.
THE CHECKS
Every key file has a block, and every block has a file.
Every name matches the pattern of Fugu::KeyDir.
Every purpose holds exactly one
currentkey.SHA256names every key file, with the digest that the file has, and it names nothing else.The declared fingerprint of an OpenPGP key equals the computed one.
SHA256.sigtakes the shape of a signify signature. It holds two lines: anuntrusted comment:line, and 100 base64 characters. Those characters decode to 74 bytes, and the first two spellEd.
The class verifies no signature. That is the work of a consumer install: the site build cannot sign, so a site that verified its own manifest would prove nothing. It reads the shape of the signature because a file of another shape fails at every consumer install.
SEE ALSO
App::FuguWeb, App::FuguWeb::Config, App::FuguWeb::Check, App::FuguWeb::Site, Fugu::KeyDir, Fugu::OpenPGP, Fugu::Signify
AUTHOR
Dick Olsson <hi@senzilla.io>