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 current key.

  • SHA256 names 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.sig takes the shape of a signify signature. It holds two lines: an untrusted comment: line, and 100 base64 characters. Those characters decode to 74 bytes, and the first two spell Ed.

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>