NAME
App::FuguWeb::Rotate - write the key directory of a site
SYNOPSIS
use App::FuguWeb::Rotate;
my $rotate = App::FuguWeb::Rotate->new(
config => $config,
org => 'fugubsd',
url => 'https://www.fugubsd.org/keys',
);
# The first root mint. No root stands, so the new key signs
# its own manifest and each key in force binds to it.
my $facts = $rotate->mint(
purpose => 'root',
secret => '/tmp/root-1.sec',
bind => { 'fugubsd-1-release' => '/tmp/release-1.sec' },
) or die $rotate->error;
# A subordinate mint. The current root signs the manifest,
# and the new key binds to that root.
$facts = $rotate->mint(
purpose => 'release',
secret => '/tmp/release-2.sec',
signer => '/tmp/root-1.sec',
) or die $rotate->error;
# An OpenPGP mint. gpg(1) makes the key and the subkey, and
# the email is the one user id.
$facts = $rotate->mint(
purpose => 'contact',
type => 'openpgp',
email => 'security@fugubsd.org',
expires => '2028-01-01',
secret => '/tmp/contact-1.sec',
signer => '/tmp/root-1.sec',
) or die $rotate->error;
# An import. An issuer makes a certificate, so no verb mints
# one, and the private key of the publisher signs its binding.
$facts = $rotate->import_key(
purpose => 'sign',
type => 'x509',
file => '/tmp/sign-1.pem',
secret => '/tmp/sign-1.key',
signer => '/tmp/root-1.sec',
) or die $rotate->error;
# A subordinate promote. The retiring key signs the key that
# takes its place.
$facts = $rotate->promote(
purpose => 'release',
signer => '/tmp/root-1.sec',
retiring => '/tmp/release-1.sec',
) or die $rotate->error;
DESCRIPTION
App::FuguWeb::Keys reads the key directory, and this class writes it. One class holds every step, so the reader and the writer share one implementation of the name pattern, the status vocabulary and the manifest.
One signify key of the directory is the root of trust, and its purpose word is root. The current root signs the manifest, and every other key of the directory binds to it with a signature of its own. A consumer that pins the root can therefore trust every key beside it.
A consumer verifies a release against the copy of the public key that it holds. A release that a new key signs therefore fails in each consumer that holds the old copy. One rotation runs in two steps, and the trust order carries the gap.
mint makes the next key of a purpose, and import_key publishes a key that another tool made. promote makes that key current and retires the old one. The first key of a purpose is current at once.
A mint makes a signify pair or an OpenPGP key. An OpenPGP key is one Ed25519 primary key with one Curve25519 encryption subkey, and the email of the caller is its one user id.
An issuer makes an X.509 certificate, so no verb mints one. The import publishes it, and the unencrypted PEM private key of the publisher signs its binding. A renewal is a rotation: the new certificate enters as the next key, and a promote retires the old one.
This class signs, and a site build does not. It runs no command of its own: Fugu::Signify holds every call of signify(1), Fugu::OpenPGP holds every call of gpg(1), and the class of each other key type holds every call of its own command.
THE BINDINGS
A binding is a detached signature by one key over the public key file of another key. Its file name is <target file>.<signer stem>.<ext>, and Fugu::KeyDir holds that name and the retention rule.
The type of the signer key selects the signer and the extension. A signify key signs with signify(1), an OpenPGP key signs with gpg(1), and a certificate signs with openssl(1). A PEM private key names no certificate, so the signer of that type reads the published certificate of the key beside the private half.
Each current and next key of a subordinate purpose holds one binding over the public key file of the current root. That signature proves that the holder of the root also holds the subordinate key.
A promote writes a chain binding: the retiring key signs the public key file of the key that takes its place. A consumer that trusts the old key therefore reaches the new one. The chain binding stays published, as the retired key does, and the promote removes the root binding of the retired key.
A first root mint and a root promote write the binding of each key in force again, because the root that those keys attest changes. The step takes the private half of each such key in bind, and it refuses before it writes when one is absent. A root promote also removes each binding of the retired root, less a chain binding.
THE TRUST RULES
Each rule below answers a way to publish a key directory that no consumer can use.
- The description holds the status
-
The status of a key comes from its
keyblock, and no step assumes one. A key file with no block, and a block with no file, each fail the step. A step that read the first key of a purpose would take a retired key for the current one. - The current root signs
-
Every step but two takes the private half of the current root as
signer. A first root mint takes none, because no root stands and the new key signs its own manifest. A root promote takes none, because the key that it makes current signs. - The signature verifies before it lands
-
Each step stages the manifest, each binding and each signature in a temporary directory. It verifies each signature against the published public key of the signer, and only a verified file reaches the key directory.
- One mint waits for the promote
-
A mint refuses a purpose that holds a
nextkey already, and it refuses before it generates a pair. A caller holds one place for the private half of a mint, so a second key would lose the private half of the first. - A step lands whole, or not at all
-
Every byte of a step is ready before the first write. Each file lands through a temporary file in the same directory, so a reader sees the old bytes or the new ones. A write that fails, and a check that fails after the writes, both put every file back.
A binding that a step retires goes after every write. A run that stops between the two therefore leaves a file that the manifest does not name, and never a manifest that names a file which is gone.
The private half of a new key lands last, after the check passes. A step that fails therefore leaves no key that the site does not publish.
A process that dies inside a step leaves the checkout dirty. A caller runs a step and commits after it, so it commits nothing then.
- One directory holds one root
-
One manifest covers the whole directory, and the current root signs it. A step of another purpose fails when the directory holds no current root. A directory whose keys hold no root is the state of a site before its first root mint, and that one step takes it.
- The reader decides
-
Each step ends with App::FuguWeb::Keys. A directory that the reader rejects is a directory that
fuguweb checkrejects, so the step fails and the caller commits nothing.
METHODS
- App::FuguWeb::Rotate->new(config => $config, ...)
-
The rotation over one key directory of one description.
configis an App::FuguWeb::Config.dirnames the directory that each step writes. A description with onekeysblock needs none, and a description with several refuses a step without it. Each directory holds its own root of trust, per D-02, so a step of one directory reads no key of another.A directory that publishes its first key holds no
keysblock, because such a block cannot load until akeyblock stands beside it. The caller then namesbootstrapandorg, and it can namedirandurl. One commit carries thekeysblock, the firstkeyblock and the key itself. A second key directory of a site arrives the same way, and it takesdir.bootstrapis the intent to make that directory. A step without it refuses adirthat nokeysblock names, because a mistyped word would otherwise publish a second root of trust in silence.mintandimport_keymake a directory, andpromotemakes none. - $rotate->mint(purpose => $word, ...)
-
Generate the next key of the purpose. The public half goes into the key directory, and the private half goes to
secretwith no group mode and no other mode.typenames the key type. The default issignify, andopenpgpis the other type that a mint makes. A mint of another type fails before it generates one byte.signeris the private half of the current root key.bindis a hash reference of key stem to path, and a first root mint takes one entry for each key in force.An OpenPGP mint needs
email, which becomes the one user id of the key, and it takes an optionalexpires.expiresis a date of the formYYYY-MM-DD, and the key expires at the start of that date in UTC. A mint with noexpiresmakes a key with no expiry. A mint of another type takes neither option.The
keyblock of an OpenPGP key takes theemailand thefingerprintof the new key. The fingerprint comes from the key that the step generated, and never from an argument.secretmust name no file that stands, and it must not namesigner. The private half of a key is the one thing that a rotation cannot make again, and a caller holds each one in one place.The method answers a hash reference with
name,stem,serial,status,digestandurl, or undef with a reason inerror.urlstands when the description names a published prefix. - $rotate->import_key(purpose => $word, file => $path, ...)
-
Publish a key that another tool made, under the next name of the purpose.
fileis the public key file, andsecretis the private half of that key, which writes the binding of the new key. The caller holds that half already, so the step writes no key file of its own.typenames the key type, and the step reads every type that a key directory holds:signify,openpgpandx509. Anx509fileholds one PEM certificate, and a file that holds a private key or a second block fails the step.secretis then an unencrypted PEM private key: the verbs take no passphrase, so a caller that holds a PKCS#12 file converts it first.The
keyblock of an OpenPGP key and of a certificate takes thefingerprintof the key that the step published. An import takes noemail, because it generates no user id.The step takes every other option of a mint of its purpose, and it answers the same facts.
- $rotate->promote(purpose => $word, ...)
-
Make the
nextkey of the purpose current, and retire the key that was current with anuntildate.retiringis the private half of the key that retires, and it signs the chain binding. A promote of a subordinate purpose takessigner, the private half of the current root, which signs the manifest. A root promote takessecret, the private half of the key that becomes the root, which signs the manifest, and onebindentry for each key in force.The retired key keeps its file and its published URL, so a release that it signed still verifies.
The method answers a hash reference with
name,stem,serial,statusandretired, or undef with a reason inerror. - $rotate->config
-
The description. A step loads it again after it writes, so a caller that holds this object reads the current state.
- $rotate->error
-
The reason of the most recent failure, or undef.
- $rotate->tool_missing
-
True when the failure was an absent command of a key type. A caller answers a missing tool with its own exit code, so a script tells it from a step that failed.
WHAT THIS CLASS DOES NOT DO
It writes no secret store and it opens no pull request. It reaches no network and it holds no token. A caller stores the private key and declares the public one.
SEE ALSO
App::FuguWeb::Keys, Fugu::KeyDir, Fugu::Signify, Fugu::OpenPGP, Fugu::X509, App::FuguWeb::Config, signify(1), gpg(1)
AUTHORS
Dick Olsson <hi@senzilla.io>