pki.sgit.ai / enrolment

Enrolment: start with a keypair, and nothing else

The registry is where keys end up. This is how one arrives — without the agent first holding authority broader than the identity being created. The constraint on the initial state is the design: everything else follows from what the agent is allowed to start with.

Status. The transport is shipped and account-less. The canonical enrolment object, the trusted processor and the registry are proposed. This page keeps that boundary explicit throughout, because a page that blurs it produces a confidently wrong implementation.

The initial state is the design

   UNKNOWN AGENT
     computation
     randomness
     its own newly generated private key
     ------------------------------------
     and nothing else

Explicitly not required at the outset: a repository credential, repository read or write authority, a project token, the certificate authority's private key, the vault key, or an administrator identity. The reason to be strict is not purity — every item on that list grants authority broader than the identity being created, which is the bootstrap trap in one sentence.

Possession of this private key lets the agent act as itself, rather than as the project or as the human operator. That is the difference between an identity and a borrowed credential.

The enrolment request

The agent constructs a canonical request and signs it with the key being enrolled. At minimum it carries:

FieldPurpose
Type and versionSo the format can change
ProjectWhich project is being asked
Subject public key and fingerprintWhat is being enrolled
Requested identityAn optional label and agent type
Requested mandateAn optional role and scope
Created at, and a nonceFreshness and replay resistance
Proof of possessionA signature over the canonical request

A signature over an enrolment request proves the submitter controls the corresponding private key. It does not prove that the project should trust the agent. Trust is a policy decision made afterwards.

Conflating those two is the most likely way to misread this whole design, which is why the sentence is on the page rather than in a footnote.

Two details that are easy to leave out and expensive to omit. The nonce is not optional — replay protection needs a nonce or sequence number inside the signed payload. And the canonical form must be specified precisely, because a signature over an ambiguously serialised object is a signature over whatever the verifier happens to reconstruct.

The append lane is the narrow door

The request is delivered through a project append lane: a managed transport outside the commit history, with a small operation set — configure, write, list, fetch, mark processed, purge.

The capability granted to the agent is to add an object to an inbox. Not to read that inbox, not to alter what is there, not to reach anything else in the project. That is the smallest useful grant, and it is why the bootstrap does not require borrowing a larger identity.

Shipped

Posting to a lane requires an append token in the request body and no access token. The response is a blind acknowledgement: the sender learns that the write succeeded, and nothing else. So the write-only property is enforced in the response itself rather than only in the permission model — a sender cannot use the write path to probe what is already in the lane.

Worth being explicit about the consequence: a compromised or hostile agent can add junk to an enrolment inbox. It cannot read other requests, cannot see who else has enrolled, and cannot reach the registry or the certificate authority. The blast radius of the ingress is a queue that needs draining — the correct shape for a public door, and the reason spam and denial-of-service controls are a named open question rather than an afterthought.

The trusted side

The processing side holds everything that matters and is deliberately not reachable from the ingress:

ComponentHoldsDoes
Enrolment inboxSubmitted requestsReceives — nothing else
Trusted processorRead access to the inboxVerifies signatures, applies policy
Certificate authorityThe issuing keySigns certificates
RegistryIssued identities and mandatesPublishes what has been recognised

The untrusted side never holds more than its own key, and the trusted side never exposes the issuing key to anything reachable from outside.

That is the referee-and-player separation applied to enrolment: enforcement belongs at a layer that cannot be bypassed by the thing being enforced.

What arrives: two statements, not one

What the registry records is not a single blob. Identity and mandate are separate signed statements from the start, because they are issued, revoked and reasoned about independently. An enrolment request may ask for both; the issuer may grant either, both, or neither.

Addressing, and the boundary

A lane is addressed by the hash of the recipient's public key, so an agent's own key is its address and no directory lookup is required to find where to send. An agent that has generated a keypair already knows its own lane address without asking anybody — which is precisely the property the bootstrap trap requires.

The derivation is proposed, not shipped. No shipped command emits append_token = H(recipient public key); today the token is agreed out of band. Do not code against the derivation. The full shipped-versus-proposed split.

Why this is a smaller project than it looks

The architecture's original milestones assume building a lane. Most of that is configuring one. What remains is the client sealing layer, the canonical enrolment object, the trusted processor and the registry — a materially smaller project than a from-scratch transport, and the reason the build order puts a private registry next rather than someday.

Open questions