The directory, ritueel.yaml, root profiles and key files
Not built yet. This is the layout the M1 code writes (issue #8, issue #9); the rules it follows are in ADR 0004.
ritueel keeps everything it knows in one directory of plain files that the holder keeps in version control and backs up. This directory is the ledger that Learn advises on: where to keep it, and how to back it up and restore it. It holds public material only: never a private key, a PIN, a passphrase or a revocation signature. Losing it loses the registry, the attestations and the OpenPGP creation times, so it is part of every key’s identity (ADR 0003). A software root’s private keys live in key files outside it.
The layout
Section titled “The layout”| Path | Written by | Holds |
|---|---|---|
ritueel.yaml |
The operator | The name, the release address, the policy and the intermediates (below). |
cards.yaml |
ritueel | The registry of known cards, from M2. Absent while every root is in software. |
roots/<label>/root.json |
ritueel | The facts of one root (below). |
roots/<label>/x509.pem |
ritueel | The root’s current certificate. |
roots/<label>/openpgp.pgp |
ritueel | The primary’s current public certificate, binary. |
roots/<label>/attestation/ |
ritueel | For a root on a card or in KMS: the attestation or the key’s description and policy, as read at init. |
roots/<label>/ssh-break-glass.pub |
ritueel | From M3: the root’s break-glass SSH CA key, one OpenSSH line (the SSH CA key list). |
requests/<stem>.* |
The operator | Pending requests: the files of the hand-off contract, one stem each. |
answers/<stem>.* |
ritueel | The answers: <stem>.result.json, and <stem>.cert.pem or <stem>.bound.pgp. |
plans/, attempts/, records/, transcripts/ |
ritueel | The ceremony files, one of each kind for each ceremony. |
products/<sequence>/ |
ritueel | The trust products of one trust sequence: root-list.json, root-list.jws and ca-bundle.pem (the root list); from M2 crl-<label>.pem for each active root (the CRLs); from M3 ssh-ca-keys.json, ssh-ca-keys.jws and ssh-user-ca-keys.pub (the SSH CA key list), keyring/ (the keyring package) and revocations/. |
- ritueel never writes
ritueel.yamlorrequests/, and the operator does not edit the other paths. - A file that a ceremony replaces, such as
x509.pem, keeps its history inproducts/and in version control. Nothing underproducts/,plans/,attempts/,records/ortranscripts/is ever overwritten. - ritueel refuses a request whose stem already has an answer.
- The directory is found by
--dir DIR, or is the current directory when it holdsritueel.yaml(Commands).
ritueel.yaml
Section titled “ritueel.yaml”The operator writes it before init. An unknown key is an error, as in the contract.
version: 1name: Example # in the roots' subjects and user IDsrelease_address: release@example.compolicy: stores: [piv, openpgp-card, aws-kms] min_firmware: 5.7.0 pin_policy: [once, always] require_name_constraints: true validity_months: 13intermediates: - name: example-ca type: x509-intermediate permitted_uri_hosts: [example.com] - name: example-release type: openpgp-subkey kms_read_roles: "111122223333": arn:aws:iam::111122223333:role/example-certifier-read - name: example-ssh type: ssh-ca-key use: userkeyring: license: CC0-1.0| Key | Meaning |
|---|---|
version |
1. |
name |
1 to 64 characters: letters, digits, spaces and hyphens. It names the roots and their primaries (below). |
release_address |
The address in every primary’s user ID: the project’s release identity, never a person’s (ADR 0006). |
policy |
The rules every request is checked against: stores, min_firmware, pin_policy, require_name_constraints and validity_months, with the meanings of the reference policy. All five are required. |
intermediates |
The intermediates ritueel may certify, one entry each. name is the NAME of the commands: lower-case letters, digits and hyphens, 1 to 63 characters, unique. type is x509-intermediate, openpgp-subkey or ssh-ca-key. |
intermediates[].permitted_uri_hosts |
For x509-intermediate: the hosts a request’s name constraints must stay inside. Required when require_name_constraints is true. Only URI hosts are expressible in version 1. Planned: permitted and excluded DNS subtrees and excluded IP ranges, for an intermediate that issues for DNS names and for per-environment limits; the schema will grow when a first installation needs it. |
intermediates[].use |
For ssh-ca-key: user or host, required. A request for another use is refused with policy; the key is listed with this use (the contract). |
keyring |
Optional. license is the SPDX identifier written into the .rpm, required to build one: the operator’s choice, which ritueel does not default (the keyring package). |
intermediates[].kms_read_roles |
For a requester’s KMS key in another account: the read-only role to assume, by account (the contract). The role is never taken from a request. |
A test vector’s configuration in testdata/hand-off/v1/manifest.json is the policy block together with the one intermediate entry its requests are for: the permitted_uri_hosts and kms_read_roles there are that entry’s.
The schema, in JSON Schema (draft 2020-12), which the code ships and checks the file against:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "additionalProperties": false, "required": ["version", "name", "release_address", "policy"], "properties": { "version": { "const": 1 }, "name": { "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9 -]{0,63}$" }, "release_address": { "type": "string", "format": "email" }, "policy": { "type": "object", "additionalProperties": false, "required": ["stores", "min_firmware", "pin_policy", "require_name_constraints", "validity_months"], "properties": { "stores": { "type": "array", "uniqueItems": true, "minItems": 1, "items": { "enum": ["piv", "openpgp-card", "aws-kms", "software"] } }, "min_firmware": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, "pin_policy": { "type": "array", "uniqueItems": true, "minItems": 1, "items": { "enum": ["never", "once", "always"] } }, "require_name_constraints": { "type": "boolean" }, "validity_months": { "type": "integer", "minimum": 1 } } }, "keyring": { "type": "object", "additionalProperties": false, "properties": { "license": { "type": "string", "minLength": 1 } } }, "intermediates": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["name", "type"], "properties": { "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,62}$" }, "type": { "enum": ["x509-intermediate", "openpgp-subkey", "ssh-ca-key"] }, "use": { "enum": ["user", "host"] }, "permitted_uri_hosts": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, "kms_read_roles": { "type": "object", "propertyNames": { "pattern": "^[0-9]{12}$" }, "additionalProperties": { "type": "string", "pattern": "^arn:aws[a-z-]*:iam::[0-9]{12}:role/" } } } } } }}Three rules the schema cannot state: intermediate names are unique, an x509-intermediate has permitted_uri_hosts when require_name_constraints is true, and an ssh-ca-key has use and no other key of the entry. A name that begins with break-glass- is refused, because the SSH CA key list uses those for the break-glass keys. ritueel checks all of them when it reads the file.
root.json
Section titled “root.json”roots/<label>/root.json is one canonical JSON object (the common rules), written at init or root add and not changed after, except revoked_at.
| Member | Meaning |
|---|---|
version |
1. |
label |
The root’s label: one upper-case letter, A and B for the two roots init makes, then the next free letter for each root root add makes, whatever its key store. It never names the store and is never reused. |
store |
software, piv or aws-kms. |
test |
True for a root made with --test-root. |
created_at |
When the root was made: the time of its ceremony. |
x509_spki_sha256 |
The pin of the X.509 root key. |
openpgp_fingerprint |
The primary’s fingerprint. |
openpgp_created |
The OpenPGP creation time of the primary, which fixes its fingerprint and is on no card (ADR 0003). |
revocation_sha256 |
The SHA-256 of the primary’s revocation signature, which is stored elsewhere. |
revoked_at |
Present once the root is revoked. |
The root certificate
Section titled “The root certificate”Every root’s X.509 certificate has this profile. A relying party can check every row.
| Field | Value |
|---|---|
| Version, serial | 3. A positive number of at most 20 bytes from a cryptographic random source, with at least 64 random bits. |
| Signature | ecdsa-with-SHA384, self-signed with the root’s own P-384 key. |
| Issuer, subject | The same name: one common name, <name> Root <label>, from ritueel.yaml, for example CN=Example Root A. A test root’s name ends in (TEST ONLY): CN=Example Root A (TEST ONLY). |
| Subject public key | The root’s X.509 key: ECDSA on P-384. |
| Validity | notBefore is the ceremony time; notAfter is 36 calendar months later (same day and time of the month, or the last day of the month if it has none), both UTC. |
| Basic constraints | Critical. cA true, no path length. |
| Key usage | Critical. keyCertSign and cRLSign. |
| Subject key identifier | Not critical. The leftmost 160 bits of the SHA-256 of the subject public key bits (RFC 7093, section 2, method 1), as in the intermediate’s profile. |
| Name constraints | Absent: constraints belong to the intermediates. |
| Anything else | Absent, the authority key identifier included (RFC 5280, section 4.2.1.1, allows it in a self-signed certificate). |
Each use re-issues it. Every ceremony that signs with a root makes a new certificate over the same key and the same subject, with a new serial, starting at the ceremony time (ADR 0002). A root that is used every year or two therefore never comes close to its end, and pins on its key never change.
The OpenPGP primary
Section titled “The OpenPGP primary”Every root’s OpenPGP primary has this profile, as ADR 0006 decides.
| Part | Value |
|---|---|
| Key | Version 4, ECDSA on P-384 (algorithm 19). The creation time is openpgp_created. |
| User ID | One: <name> Release Root <label> <release_address>, for example Example Release Root A <release@example.com>. A test root’s adds (TEST ONLY) before the address. No person’s name. |
| Self-signature | A positive certification (type 0x13) of the user ID, SHA-384. Hashed: signature creation time (the ceremony time), key flags 0x01 (certify only), key expiration time, marked critical, so that the key expires 36 calendar months after the ceremony, and the issuer fingerprint. Unhashed: the issuer key ID (the packet rule). |
| Subkeys | None of its own. It binds the signing subkeys of intermediates through the hand-off. |
| Revocation | A key revocation signature (type 0x20), made when the key is born and stored apart from it. |
Each use extends it, with a new self-signature of the same shape at the ceremony time.
The test roots of the hand-off vectors follow both profiles, with the name Example Test.
The software root’s key files
Section titled “The software root’s key files”A software root (the software store) serves tests, drills and development, and never anchors a signed release (ADR 0006). Its private keys are in files in a key directory, given by --key-dir DIR, which ritueel refuses when it is inside the directory. Every command that writes them, and every record of a ceremony that uses them, says that a software root can be copied.
| File | Content |
|---|---|
<label>.x509.key.age |
The X.509 root key: a PKCS#8 PrivateKeyInfo (RFC 5958), PEM-armoured as PRIVATE KEY, encrypted to a passphrase with age. |
<label>.openpgp.key.age |
The primary: a transferable secret key, binary (RFC 9580, section 10.2), version 4, whose secret key material is not protected inside (S2K usage 0) because age encrypts the whole file. |
<label>.revocation.pgp.age |
The primary’s revocation signature, encrypted the same way. Its SHA-256 before encryption is revocation_sha256. |
- Encryption. The age format (
age-encryption.org/v1) with a passphrase recipient, which derives the key with scrypt. One passphrase for the files of a root, read from the terminal without echo, or from--passphrase-fd N. It is never a flag, an environment variable or a line of output. --test-rootwrites the same keys with no passphrase:<label>.x509.key.pemwith the lineTEST ONLY: a test root with no passphrase; anyone with this file can sign as this root.before the armour, and<label>.openpgp.key.ascand<label>.revocation.asc, ASCII-armoured with the headerComment: TEST ONLY. The root’s names carry(TEST ONLY)too, and products refuse to mix test and real roots.- The holder keeps the revocation file apart from the key files, as for a card (ADR 0002).
Follow-ups
Section titled “Follow-ups”cards.yamland theattestation/files are specified with thepivandaws-kmsstores (issue #11, issue #12).- The CRLs, the SSH CA key list and the keyring files under
products/<sequence>/come with their builders (issue #30, issue #55, issue #15).
Standards
Section titled “Standards”| Standard | Used for |
|---|---|
| RFC 5280 | The root certificate profile. |
| RFC 7093 | The subject key identifier. |
| RFC 9580 | The primary, its self-signature and revocation, transferable secret keys. |
| RFC 5958, RFC 7468 | PKCS#8 private keys and their PEM armour. |
| age v1 | Passphrase encryption of the software root’s key files (scrypt). |
| JSON Schema 2020-12 | The schema of ritueel.yaml. |