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.
Done #
What an admission established is recorded in the account's
bot.did.registration record, as admittedBy and assurance.
The backend interface.
A node-credential backend, so a shared-secret holder cannot claim any
allowlisted node.
A seen-nonce store, so replay is prevented inside the window as well as
bounded by it.
An attestation claim is spent when it is admitted.
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.
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.
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.
Retired the agent.attestation lexicon — already done, in
account-types : it folded into bot.did.registration.
Verified rather than redone.
The two missing properties of a detached-signature document — no
audience, no expiry. Bounded by the seen-nonce store, exactly as
account-types says: the tuple spent is
instanceId + pendingTime, both read out of the signed document.
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 says.
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.