--- id: attestation title: Where an agent was provisioned from is a claim somebody else signed status: open crates: [didbot-attest] dependsOn: [] exitCriterion: > An agent provisioned on a cloud instance carries an attestation whose evidence this project did not issue, and which a reader can check. --- # attestation Attestation answers one question once, at provisioning: where did this come from, and is that source admitted. It is an audit artifact, not a credential. The strongest claim available is about the node. Which session and which subagent are the harness's word, and nothing here pretends otherwise. - [ ] **A SPIFFE/SPIRE backend**, as a portable wrapper over the others. - [ ] **A hardware root of trust**, as a design stub. - [ ] **Say what each backend's evidence is worth to a reader who is not us.** Done for the AWS instance identity backend; the SPIFFE/SPIRE and hardware backends still need their own paragraph once they exist. - [ ] **Spent nonces across a restart.** `SeenNonces` is in memory, so a restart inside the window forgets what has been spent. Binding a claim to its request narrows what that is worth — a replayed claim can only re-run the provisioning it was signed for, which the account store refuses as a name already taken — but it does not close it for an account that has since been erased. Persisting them is durable state this crate has nowhere to put. - [ ] **An account that outlives its proof.** A claim answers once, at provisioning, and is spent when it is admitted. An account that renews — a CI identity, a machine — proves itself again on every run, which is a second question this interface does not currently distinguish from the first. See [account-types](account-types.md). ## Done - [x] What an admission established is recorded in the account's `bot.did.registration` record, as `admittedBy` and `assurance`. - [x] The backend interface. - [x] A node-credential backend, so a shared-secret holder cannot claim any allowlisted node. - [x] A seen-nonce store, so replay is prevented inside the window as well as bounded by it. - [x] An attestation claim is spent when it is admitted. - [x] **A cloud instance identity backend**, for AWS EC2. Verifies the detached PKCS#7/RSA-2048 signature over an instance identity document against this deployment's configured regional certificates — hand-rolled against `der`/`spki` rather than pulling the `cms` crate, which only exists as a pre-release and would have doubled the `der` stack already in the dependency graph. GCP and Azure are not implemented; the trait needed no change to make room for them. - [x] **Execution environment recorded at provisioning.** `Provenance` grew an optional `environment` field and an `ExecutionEnvironment` enum (laptop, container, cloud instance); the AWS backend is the first to set it, to `CloudInstance`. The node-credential backend leaves it unset rather than guess. - [x] **Watch for clock skew.** `AttestError::Stale` now carries the verifier's own clock alongside the claim's timestamp, and its message names clock skew explicitly rather than reading as a generic refusal. Applies to every backend, not just AWS's. - [x] **Retired the `agent.attestation` lexicon** — already done, in [account-types](account-types.md): it folded into `bot.did.registration`. Verified rather than redone. - [x] **The two missing properties of a detached-signature document — no audience, no expiry.** Bounded by the seen-nonce store, exactly as [account-types](account-types.md) says: the tuple spent is `instanceId` + `pendingTime`, both read out of the signed document. - [x] **A claim is bound to the request it was made for.** The node-credential backend's signed string covers a digest of the request's identifying fields — agent id, handle, parent — beside the node, the nonce and the timestamp, under a `v2` domain separator. A claim read off the path admits the provisioning it was made for and nothing else, and is refused for any other before its nonce is spent. A backend whose evidence is somebody else's document cannot do this: `AttestationBackend::attest_for` defaults to ignoring the request, and the AWS backend stays bounded by the seen-nonce store as [account-types](account-types.md) says. - [x] **The binding is an issuer and a stable subject.** The subject is `accountId` + `instanceId`, AWS's own identifiers, never a tag or a name. The claim's `node_id` is never trusted on its own — `attest_at` re-derives it from the verified document and refuses a mismatch.