Skip to main content
Everything else in the protocol establishes that a brain is intact: anyone recomputes the hash structure offline and confirms it is exactly what its identifiers describe. What the hashes cannot say is who assembled it — internal consistency is cheap to manufacture, and a fabricated brain recomputes perfectly. Authenticity is the one assertion the hashes cannot make: a detached SSHSIG signature over the snapshot, judged against a key list that travels inside the brain itself. The two verifications stay separate, always:
There is deliberately no combined flag. “Intact, and signed by an authorized key” and “intact, provenance unknown” are different facts, and a consumer that collapses them cannot recover the difference afterwards. AuthenticationReport.state is a derived property — no construction can claim authorized while carrying a blocking finding.

Creating a governed brain

Authority starts at the genesis and nowhere else. Keys are ordinary SSH keys — the private half stays in your ssh-agent, a hardware token behind one, or a KMS that presents as one; the SDK never reads a private key and defines no format for one.
subject names who holds the key, as an actor identifier. It is the one thing that connects a signature to the actor a provenance record names — without it, provenance says a person and the signature says a fingerprint, and nothing asserts they are the same.
It is not a certificate. A subject is asserted by this brain’s governance and nowhere else: changing one changes the trust root, which is a revision, which needs a quorum. It is as trustworthy as that quorum and no more. Whether a key really belongs to a particular person in the world is still settled outside the protocol — what a subject adds is that once it is settled, the conclusion lives where a verifier reads it instead of in a maintainer’s memory.AgentSigner.suggested_subject offers the key’s ssh-agent comment when it already is an actor identifier, which it usually is. Offered, never adopted: the comment is a label the key’s own holder typed, so a quorum has to make the claim deliberately.
Omitting it costs nothing. An absent subject is left out of the canonical bytes entirely, so a trust root that names none has exactly the digest it had before this member existed, and every pin against it still holds. A genesis proves nothing by itself — any party can construct an indistinguishable one. What gives it meaning happens outside: consumers pin the trust root’s digest on first contact, and from that moment the derivable rules take over. A genesis is not validated, it is anchored.

Signing and verifying

Which actors a signature stands behind

A provenance record names who performed an operation. Until a key stands behind that name it is a declared identifier — whoever can write to a brain can write any name into its audit trail. Once the trust root names subjects, the two can be compared:
It never decides anything. An unvouched actor produces a non-blocking ATTRIBUTION_UNVERIFIED finding and leaves state at authorized. It has to: a snapshot legitimately names actors that never signed it — every merge does, because reconciliation brings another party’s records into a history your key signs. Refusing an unvouched actor would refuse reconciliation itself.
Only the records a snapshot introduces are compared, and only against keys whose signature actually verified. Assisting parties are never compared — nothing expects a model to hold a key, and counting its absence would bury the one comparison that means something. asserted and legacy are kept apart because the remedy differs: an unvouched actor needs a governance act, and a legacy identifier needs a rewrite nobody can perform on bytes already published. Three rules do most of the work:
  • Scopes are computed, never believed. What a snapshot required comes from its difference against its first parent — canonical gained blocks → ingest, canonical lost blocks → drop:canonical, a derived module changed → commit, the trust root changed → govern, a module’s signed tombstones list grew → redact. The scopes a signature claims aid diagnosis and decide nothing.
  • The trust root in force is the parent’s. A revision’s own signatures answer to the list it is replacing — which is why a key cannot admit itself (see below).
  • Missing evidence widens the requirement. A truncated chain or an unreadable composition makes the verdict insufficient_evidence, never a smaller demand: a verifier that quietly computed less from a truncated history would be exploitable by shipping a truncated history.

A stranger’s key: attributable, or an impersonation

The same snapshot, signed by the same key the trust root does not list, means two opposite things depending on the claim made for it — so the report says which question it answered:
  • Offered — someone hands you a history for review. The signature verifies, the author is cryptographically identified, and no authority attaches: the state is attributable, and the blocks are judged one at a time through validation, where a proposer’s identity earns them nothing anyway. This is how an open project hears from people it has never admitted.
  • As a head — a registry, a mirror, or the contributor’s own tag serves it as the brain’s current state. The same signature is now an unauthorized key, reported as one, and refused.
HEAD is the default because it is the safe answer: a caller who does not say is asking about a brain’s current state. plan_reconcile sets OFFERED for you and reports the result on the plan:
attributable is not a weaker authorized. require_authorized() still raises for it. It says the author is named and nothing is authorized — which is precisely what a maintainer needs to know before reading the contribution.

Admitting a key: the quorum rule

Changing the trust root is a trust-root revision: a snapshot that changes the key list and nothing else, covered by at least govern_quorum signatures from distinct keys holding govern in the revision before the change. A single owner admits a second key in one call:
With a quorum of two or more, the signers may not share a machine. The revision document is built once — it carries created_at, so two constructions would sign different bytes — and the exact bytes travel by any channel; nothing in them is secret, and each party inspects what it signs:
countersign refuses mechanically what a reviewer would refuse by reading: a parent it cannot see, content smuggled into a governance act, a trust root that does not advance, an admission claim the observable chain refutes. A failed quorum advances nothing. Keep a margin. A quorum equal to the number of govern holders is legal and permanent: lose one key and neither the remaining holders nor the attacker can assemble the signatures to record a compromise or admit a replacement, while a stolen key keeps signing within its scopes. There is no recovery path inside the protocol — re-anchoring would be exactly the self-assertion the quorum rule exists to forbid — so init and rotate warn when you enter that state, and the report names it:
Why a key cannot admit itself. An attacker installs the public brain, writes their key into the list — possibly byte-identical to a legitimate admission — and signs with it. Integrity passes; the signature verifies. What fails is the transition: the trust root in force at their snapshot is the one its parent names, where their key is absent, and the revision carries none of the govern signatures the previous list demands. This fails with no pin at all, and the rejection propagates: every descendant of the forged revision stands on authority that was never granted.

Retirement and revocation, without clocks

The two look similar and behave oppositely. Both are trust-root revisions under the same quorum:
  • Retired (retired_from, a revision number): everything the key signed while authorized stays valid. An ordinary departure is harmless, and a verifier reports retired_key distinctly — collapsing it into “unauthorized” would invalidate history for an administrative reason.
  • Compromised (compromised_from, a snapshot digest): every signature at and after that position is withdrawn, even though it was signed while the key was listed. The only construct in the protocol that invalidates a previously valid signature, reported in the report’s separate withdrawn list.
No clock is consulted anywhere. A timestamp is written by whoever holds the pen — a stolen key backdates freely — so validity is positional: snapshots are chained by digest, a position cannot be forged without changing every descendant, and the question is always was this key authorized, with this scope, at this position in the chain.

The pin: the one thing from outside

Trust cannot be manufactured from inside a system. The pin reduces the exposure to a single decision:
A pinned consumer refuses any brain whose trust root neither matches the pin nor descends from it through revisions that each satisfied the quorum rule — an approved rotation is the mechanism working, not a mismatch. On pull, the manifest’s trust-root annotation is compared before any module layer is transferred; when it differs, only the small documents move until the custody walk decides. A pin is for one brain, and a brain is its genesis. Tags are re-assignable and the trust root rotates, so neither identifies a brain; the genesis digest never changes. pin() records it, and the verifier checks it first — otherwise an anchor taken for one brain would be evaluated against another’s chain, and match or mismatch would both be answers to the wrong question.
The same brain under a new repository reference still matches. A history pruned below its genesis cannot answer the question, and that is reported as undecidable rather than as a mismatch — refusing it would punish pruning, which the protocol permits.

Publication: signatures accumulate around the artifact

A signature is never a layer of the brain manifest — countersigning would change the brain’s digest, and a brain must not change identity because someone agreed with it. Each record is the single layer of its own manifest, whose subject names the brain: in a registry, that is an OCI referrer; in the local layout, one more index.json entry, which is exactly what an export carries. push publishes them, pull and fetch collect them, and a transport that never learned about referrers still moves the brain — its consumers simply see it unsigned, and the push says so. Install-time tolerances are a VerificationPolicy, not a hierarchy of flags:
What is never configurable is the reporting: no policy can present an unverified brain as verified.

The evidence carries its authorship

Every query result reports both verifications, separately:

Without the extra

The Ed25519 mathematics lives in the optional extra — pip install 'pyboltzmann[authenticity]'. It buys exactly one operation. Everything structural works on a plain install: parsing, fingerprints, the trust root, quorum arithmetic, and the one rejection the paper requires of every reader — a record whose named fingerprint disagrees with the key inside its own signature blob. What a bare install cannot do is reach authorized: an unchecked signature reports unverifiable, because “could not check” is a different fact from “failed”, and a missing dependency must never read as a forgery — or as a pass.
Signing through an AgentSigner works without the extra: the mathematics runs inside the agent, which is the point of never holding the key.

Verification security floor

Boltzmann signatures pin SSHSIG’s message hash to SHA-512. A generic SSHSIG document using SHA-256 is valid SSH syntax, but it is not a valid Boltzmann signature. Verification also requires canonical Ed25519 point encodings, S below the group order, the strict cofactorless equation, and a public key outside the small-order subgroup. ssh-dss is always refused; RSA keys below 3072 bits are refused before the SDK reports whether that algorithm is implemented.

Golden vectors

Two published files pin this behaviour for any implementation, in any language: sshsig.json carries the wire format — including the signed-data blob, which is what tells a framing bug from a signing bug — and signatures.json carries whole chains, published test key pairs, and the verdict a verifier MUST reach for each of the paper’s worked cases. AuthenticityConformance in the conformance suite replays them against any store.