Florete

0015: Anchor Signer Trust in the Root CA

Status

Accepted

Obsoletes ADR-0011: Bootstrap and Rotate Signer Trust. Supersedes the trust-block section of ADR-0010: Compiled Artifact Object Model; the rest of that record stands. Uses the signing-identity kinds of ADR-0005 and the envelope of ADR-0012 unchanged. Arose from the C0 Enrollment design.

Context

ADR-0011 made artifact-signer trust a pinned set carried in configuration. A node received an initial authorized_mgmt_signers snapshot at enrollment, persisted it, and thereafter verified agent.json against the persisted set — adopting an updated list only when the artifact carrying it was signed by a key already in that set. A key was always introduced by an already-trusted key; nothing bootstrapped itself.

That model is coherent and has two defects that only appear together with decisions taken elsewhere.

The chain has no anchor, so a leaked signer key is unrecoverable. The set held bare public keys, and the validator's requirement that a signer's certificate be CA-signed runs on the operator's machine and reaches no node. So an attacker holding the current signer's private key can publish a set naming their own key and retiring the operator's; the operator's offline root CA cannot overrule it, because a successor is admitted by the chain and the attacker owns the chain. One leaked key becomes permanent loss of the management plane, recoverable only by physically re-enrolling every node.

The chain requires seeing every step, and the coordinator serves only the latest tree. A node absent across a rotation returns to an artifact signed by a signer it has never heard of, rejects it against its pinned set, and can never advance — nothing older is fetchable. An overlap window does not fix this; it only sets a deadline, and the deadline is bounded below by the longest period a node may be unable to reach the coordinator. For a device out of contact for a week, an overlap of days is a trap.

Neither defect is in ADR-0011 alone. Latest-only publication and chain-of-custody trust are each sound; their conjunction makes absence fatal.

Decision

A signer's certificate travels with what it signed, and is validated against the trust anchors the verifier already holds.

  • The envelope carries the signer's certificate. signature becomes { cert, value }. key_id is removed: the certificate carries the SPIFFE ID in its SAN, and a second copy would be a second source for one fact. alg is removed on the same argument: the certificate's SubjectPublicKeyInfo names the key type, the key type fixes the verification scheme, and a signed document must not carry a field selecting how its own signature is checked.
  • The verification scheme is derived from the trusted certificate, never read out of the document. A compiled-in table maps key type to scheme; C0's table has one row, Ed25519 → PureEdDSA per RFC 8410, whose OID names the key and the signature together. value is the raw signature, base64-encoded.
  • Verification is: extract the certificate, validate it against the anchors, check that its kind matches the plane, and for ctrl plane — check that signer is in authorized_ctrl_signers, then check the signature. In that order. The kind check reads the SAN's first path segment and requires management-plane for a mgmt artifact, control-plane for a ctrl one, and management-plane for a bundle manifest; a certificate of any other kind is refused before its signature is looked at. It is not optional and it is not defence in depth. The X.509 extension policy cannot stand in for it: signing kinds carry keyUsage: digitalSignature and no extKeyUsage, TLS kinds carry that same bit plus an extKeyUsage, so there is no extension a verifier can require that a TLS leaf lacks — the SAN's kind is the only thing separating a signer from any other principal the root ever vouched for. Without it, user/alice's leaf, whose private key sits in a vertex on Alice's laptop, verifies a rete-wide agent.json.
  • Nothing beyond those four steps is consulted for mgmt — in particular, no list: authorized_mgmt_signers is removed. No artifact carries a mgmt-signer set and no node persists one. A rete with more than one management-plane identity therefore cannot narrow which of them may sign what; the kind check admits all of them equally. This is a gap C0 accepts rather than a problem it solves — see the Rationale below.
  • authorized_ctrl_signers stays, and becomes a list of SPIFFE IDs rather than of keys. A ctrl artifact is accepted when its carried certificate validates against the anchors and the identity that certificate names is on the list. Rotating a ctrl key touches nothing; revoking a ctrl signer is the list edit.
  • A bundle's manifest carries its signature in the same shape, a signature block over the canonical manifest minus itself — the routine an envelope uses, applied to a different document. There is no detached signature file: the signer's certificate lives in the manifest, so a verifier must parse before it can verify either way, and the advantage a detached form would have had disappears with it.
  • The trust anchors are the node's identity/anchors.pem, delivered at the first enrollment and immutable afterwards. One file holding one or more concatenated PEM certificates: the plural shape is what an eventual rotation needs, so it is reserved here, but C0 builds neither an add path nor a remove path and a re-enrollment delivering a different set is refused. See ID-C0-18.
  • Rotation of a signer is: mint a fresh certificate from the root CA, replace the operator's pair, sign with it. There is no add-switch-retire sequence, no overlap window and no list edit. A node accepts the successor on the anchor's word.
  • The agent still persists version high-water-marks. Replay defence is unchanged and independent of signer trust.
  • Enrollment remains the out-of-band trust anchor. A bundle's integrity is an assumption the protocol does not provide, as for every PKI's root delivery. What a bundle now establishes is the anchors, not a signer set.
  • A re-enrollment is verified, and then compared: the bundle's manifest carries its signer's certificate, validated against the anchors the node already holds; the anchor set and trust domain it delivers must then equal the ones the scope is pinned to. Verifying alone would be insufficient — it proves who signed the manifest, not what the manifest carries — and without the comparison the mgmt signer could hand a node a new root of trust. A first enrollment verifies no signature, having nothing to verify against, and says so.

Rationale

This is the standard flow. CMS SignedData, JWS x5c and every code-signing scheme carry the signer's certificate with the signed message and validate it to a trust anchor. ADR-0011 built a bespoke pinning layer above that flow; both of its defects are costs of the layer rather than of the problem.

Self-carriage is not the circularity ADR-0011 forbade. That prohibition was against an artifact authorising its own signer. Here an artifact merely presents one, and the authority to accept it is external and predates the artifact — the anchors arrived at enrollment. The distinction is the whole of why this is safe.

Once the anchor is the root CA, the pinned set has almost no work left. It could restrict which CA-vouched signers may sign, and it could retire one. The first is a no-op while a rete has one signer, and the kind already separates management-plane from control-plane. The second reaches only nodes that have already received the list carrying the retirement — so as a revocation mechanism it protects exactly the nodes that did not need protecting, while the node you could not reach is the one it fails.

What replaces the mgmt list is nothing, and C0 does not pretend otherwise. Once the kind check is in place the remaining question is an ACL over mgmt signers — which of a rete's management-plane identities may sign, and how one is withdrawn — and C0 answers it with the kind alone because every shape available is wrong in the same way. A list riding a mgmt artifact is verified against the list it updates, which is the self-reference this record exists to remove. A dedicated signer-authorisation artifact signed by the root directly looks like the escape and is not one: it moves the deadlock rather than closing it (the artifact carrying the withdrawal reaches only nodes that can still fetch), it requires the root key on every authorisation change and so undoes the cold-root posture, and it makes the root sign something other than a certificate — the root is an issuer, not a principal, and giving it an envelope-signing role is the merge ID-C0-19 keeps apart. The honest C0 position is that a rete has one mgmt signer, the kind check is what binds it, and an ACL waits for a milestone that has a second signer to narrow and a working revocation story to hang it on. Reserve a property, not a field.

Both defects close, and the absence case closes without a mechanism. An attacker with a signer key can sign artifacts and cannot mint a signer; the operator mints from the cold root, signs with it, and every node accepts — including nodes that were switched off, because acceptance rests on an anchor rather than on a list an absent node never received.

A signing certificate cannot introduce a successor, which is what makes the root the only admitting authority. The management-plane and control-plane kinds carry keyUsage: digitalSignature with no extKeyUsage and no CA basic constraints, so a signing key cannot issue a certificate without becoming an intermediate — and an intermediate able to mint its own successor would reintroduce exactly the lockout above.

A plane may constrain the plane below it, never itself. That is the general form of the asymmetry, and it is why one list survives.

The two planes are not symmetric. A mgmt list gated its own delivery channel: the artifact carrying the update was itself verified against the list being updated, which is what made a node's absence fatal. The ctrl list has no such self-reference — it is delivered by mgmt, which is anchored independently, so a node absent across a ctrl rotation returns, verifies the current mgmt set against the anchors, and receives the new list in the ordinary way. Nothing deadlocks.

And mgmt is not therefore unconstrained: the trust anchors are its ACL, arriving out of band at enrollment and delivered by no plane at all. The hierarchy is anchors → mgmt → ctrl, each level bounded by the one above it and none by itself.

The second asymmetry is who holds the key. A mgmt signer's key sits beside the root key on the operator's own machine, so signer-only compromise is largely hypothetical and revocation earns little. A ctrl signer is a running service — hot by design, and hosted rather than held from B1 onward — so its compromise is exactly the realistic case, and an authority delegated by mgmt is precisely how the operator withdraws it without touching the root. The list is doing real work there and none here.

Why no alg, when every comparable format has one. JWS and COSE carry an algorithm field because their verifiers do not control issuance and must accept keys from parties they have never met. A rete is the opposite: rete mints every key in it, rete.yaml fixes the algorithm set, and a verifier reaches a signature only after deciding the certificate carrying it chains to the anchors. In that setting the field adds no fact and subtracts a property — an untrusted document selecting the routine that checks its own signature is the shape behind the JWS algorithm-confusion class, and removing the field closes it structurally rather than by validating the field carefully. It is also what the digest rule already does: ENRL-C0-16 fixes SHA-256 with no algorithm field and reserves a second algorithm as a schema change, and the signature had no reason to answer differently.

The narrowness matters and is worth stating, because "a key does not determine its scheme" is true in general and false here. id-Ed25519 names the key and the signature together, parameters absent, one routine; Ed448 and the ML-DSA and SLH-DSA parameter sets are self-determining the same way. ECDSA underdetermines only its hash, which every modern profile pins by curve; unrestricted rsaEncryption is the one genuinely ambiguous case, and even it has id-RSASSA-PSS to pin itself. So adding a key type means fixing its scheme with it, which is a rete-wide policy act, not a per-envelope one.

Should a key type ever arrive with two admissible schemes, the field returns as an ordinary ADR-0012 minor bump — producers stamp the lowest minor their content uses, so envelopes not using it keep verifying on unupgraded consumers, and one that does use it is refused by a consumer that could not have verified that scheme anyway. It returns as a check, not a selector: required to agree with what the key type implies, and a disagreement is a refusal. That is a last resort and may never arrive.

Alternatives considered.

  • Keep the pinned set and require an overlap window sized against the worst expected absence. Rejected: the window is unbounded in the deployments that most need it, and the failure is a bricked node needing an operator visit.
  • Keep the set but let a CA-vouched signer verify even when absent from it, making the set a retirement list. Rejected as an intermediate step that keeps a field earning nothing: with acceptance already resting on the anchor, the residual list is a revocation mechanism, and revocation is deferred milestone-wide.
  • Have the outgoing signer sign the incoming certificate. Rejected: it makes the signer an intermediate, per the kind policy above.
  • Keep an empty trust block reserved for a future narrowing ACL over mgmt signers. Rejected: neither C0 nor C1 has a second mgmt signer, so its shape would be guesswork carried by every artifact. Reserve a property, not a field. The ctrl list is not this: it exists, has a caller, and is delegated rather than self-anchored.
  • Remove the ctrl list too, for symmetry. Rejected on both asymmetries above; symmetry between a self-anchored authority and a delegated one is not a property worth buying.

Consequences

A leaked mgmt-signer key cannot be blocked, and a lost one is a different event. A lost key is recovered from the root — mint a fresh certificate, replace the pair, sign with it — and every node accepts on the anchor's word, including one that was switched off; no node is visited. A leaked key has no remedy: it signs valid artifacts until its certificate expires, there is no list to edit, and re-enrollment cannot displace it because the anchors are pinned and go on vouching for it. Its bound is the signer certificate's validity or a new rete. This is accepted for C0 on two grounds: revocation lists are already deferred milestone-wide, so no new gap is introduced; and the root key and the signer key live in the same directory on the operator's machine, so a leak reaching one has reached the other, and a leaked root is unrecoverable by construction. Revisit when the two stop sharing a location — an intermediate, a CA service, or a hardware-held root — because that is when signer-only compromise becomes a real scenario and a revocation mechanism starts earning its cost.

Every artifact grows by a certificate. A few hundred bytes per envelope, against a handful of artifacts per node. What it buys is that an envelope is verifiable alone — which the agent needs, since it verifies each artifact independently; which the coordinator needs, since it re-serves each byte-for-byte; and which ctrl artifacts need structurally, being independent streams with no set root to hang a certificate from.

The trust anchor becomes plural, which makes its rotation expressible rather than built — and C0 goes further, forbidding the change outright. identity/anchors.pem holds a set, so old and new could be trusted at once; the anchors arrive at the first enrollment and are pinned there. C0 rotates the root CA not at all, and the pin is what keeps that a property rather than a convention: the only channel that could have moved an anchor is a re-enrollment manifest, signed by the mgmt signer, and a signer able to rewrite a node's anchors is a signer able to take the node — an escalation outliving its own certificate's expiry, which is the permanent management-plane loss this record exists to remove. The deferral is deliberate rather than an omission — see ID-C0-18 for the three cases and why they do not share a mechanism. The recursion stops there: nothing anchors the root CA, and an attacker holding its key already mints any principal at will. That remains the irreducible root ADR-0011 named, and this record does not move it.

Vocabulary. A root CA is an issuing authority; a trust anchor is what a verifier validates against. They usually coincide and are not the same thing — a deployment can anchor on an intermediate, and an intermediate is a CA that is not an anchor. This record uses both and never a bare "CA".

Ctrl revocation improves rather than survives. Listing identities rather than keys means a ctrl signer's certificate can be replaced without an artifact changing anywhere, while removing its authority stays a single mgmt-signed edit — so rotation and revocation stop being the same operation, which they were when the list held keys.

Validation is bounded by position, not by a timestamp. Verification happens at admission — a set fetched from the coordinator, and an enrollment-delivered set entering through incoming/ — and there it evaluates the signer certificate's validity window against the node's current time. It deliberately does not evaluate that window against the artifact's own generated_at: generated_at sits inside the payload the signer controls, so validating a certificate against it would let the holder of an expired key choose its own validity and make expiry unenforceable against exactly the party expiry exists to stop.

An already-admitted set is not re-authorised at startup. It crossed the trust boundary once and was validated then; it is now local filesystem state, and a signature protects the distribution path rather than local-filesystem integrity — the rule this record's threat model already applies to the vertex, which does not re-verify agent-staged files. So the agent's startup pass checks integrity, not authority: signature and set coherence, with the signer certificate's validity window not evaluated. Without that split, a rete would acquire a hard deadline at its mgmt signer's notAfter — every node that restarted afterwards without having synced a re-signed set would fail to spawn any workload, including the vertex that is its only path to a coordinator. mTLS is unaffected and stays strict: a presented certificate is admission, so a principal whose leaf has expired does lose connectivity until it is re-keyed, which is the bound ID-C0-22 intends.

The host clock therefore sits in the trust path at admission, and C0 says what that costs. A clock running behind — the ordinary embedded case, a device with no battery-backed RTC starting from an old built-in time — accepts material that has in fact expired; it does not brick, and the failure is permissive rather than fatal. A clock running ahead starts refusing valid material early, which day-scale validity absorbs and short-lived credentials would not. Neither is C0's problem to solve: C0 and C1 nodes are desktops and servers whose clocks are maintained, and a node without a usable clock is out of scope for a pilot rather than a case being handled badly. The narrowing is recorded because it is cheap whenever it is wanted — every node holds material it has already verified, so now is at least the newest instant I have authenticated (its certificates' notBefore, the generated_at of its newest verified artifact) is a monotonic floor available before any connection. It is deferred to B2's network clock, which is where embedded nodes and battery-less RTCs actually arrive and where the floor is already part of the design.

What is unchanged. The envelope's other fields, its schema_version gating, version monotonicity and the agent's high-water-marks, the signing-identity kinds and their X.509 extension policy, the delegation of ctrl authority by mgmt, and the rule that the agent is the sole verifier.

On this page