feat(ca2a): mutual attestation design, and the challenge primitive - #89
Conversation
…t give The primitives are already symmetric: either peer can bind a channel key into a report and either can appraise one, which is why attestation.md says the design composes into mutual attestation and why claim C6 validates both directions. The reference transport is what is one-directional. The callee today verifies the caller's delegation chain, which is authorization: it proves the caller holds a credential whose scope covers the request. It says nothing about what the caller is running, so a callee cannot tell an enclave from a laptop. The change is a callee-issued challenge and a caller offer bound to it. Three things fall out and each is a requirement rather than a consequence. The challenge must come from the callee, because a caller that picks its own nonce proves only that it can produce a report and not that it produced one for this exchange. The callee must appraise before it acts rather than before it responds, because the payload is sealed to the callee's key and readable on arrival, so appraising afterwards means an unattested caller has already had its work done. And the caller's key must be used or it is ceremony: sealing the response to it makes the callee's answer readable only by the enclave it appraised. Two open decisions, both with real costs. A challenge store gives exactly-once within one process and makes the server stateful; a stateless HMAC challenge works across instances and cannot be single-use, only bounded by a window. The document ends with what this still would not give: it is not simultaneous, there is an instant where the caller has committed and the callee has not verified; it does not make either peer trustworthy, only established; and it does not make the recorded cross-operator hardware run mutual, it makes a mutual run possible. Proposal only. No code written.
…guarantee stated Step one of the mutual-attestation design: the callee issues the nonce that the caller's attestation report must bind. A caller that picks its own nonce proves it can produce a report, not that it produced one for this exchange. Stateless, per the decision recorded in the design. A challenge is v1.<expiry>.<random>.<mac>, the MAC covering the first three parts under a per-process secret, so any instance verifies what any other issued and nothing has to be remembered. The cost is real and is written down rather than implied: single-use is unachievable without state, so the property is at-most-once-per-window and the window is the TTL. A deployment that needs exactly-once wants the challenge store this was chosen over. Two details that are decisions rather than style. The MAC is verified before the expiry is read, because acting on a timestamp pulled out of an unauthenticated string means trusting an attacker's arithmetic, and answering "expired" to a forgery tells the sender their forgery was well-formed. And the secret is per-process and never persisted, so restarting the callee invalidates outstanding challenges: a restarted enclave is a different enclave, and a challenge it never issued should not verify. 16 tests, including that extending a captured challenge by editing its timestamp is caught by the MAC, and that a forgery reports as forged rather than expired.
|
Updated: the design decisions are recorded and the challenge primitive is in. Transport wiring follows in a second PR so the security-critical piece gets reviewed on its own. Decisions taken, all three now in the document:
Two details in
|
The argument for sealing the response to the caller's attested key was that an appraised key which is never used is ceremony. Wrong on both halves. There is no confidential response to seal. serialize_peer_result never echoes the opened payload, by design; the response carries the provenance record, which exists to be chained and handed to a verifier. Sealing it would produce a record only one enclave can read, encrypting the one artifact built to be shareable. And the key was never ceremonial. It is the vehicle: binding it into a report under the callee's challenge is what makes the caller's measurement live rather than replayed. The callee learns what the caller is running, which is the whole property. The key does its job at appraisal time. Withdrawn before implementation rather than after, which is the cheap moment. The other two decisions stand: stateless HMAC challenge, and record the outcome with the requirement configurable.
Design only, no code. cA2A is the one asset with no funded competitor, so it is worth getting the protocol change right rather than fast.
What is actually one-directional
Not the primitives. Either peer can bind a channel key into a report and either can appraise one — that is why
attestation.mdsays the design composes into mutual attestation, and why claim C6 drives both directions.The reference transport is. The callee verifies the caller's delegation chain, which is authorization: it proves the caller holds a credential whose scope covers the request. It says nothing about what the caller is running. A callee cannot tell an enclave from a laptop.
The change
A callee-issued challenge, and a caller offer bound to it. Three properties fall out, each a requirement rather than a consequence:
The decision I did not make
A challenge is worth nothing unless it is single-use and expiring, and the server keeps no state today.
What it still would not give
Not simultaneous — there is an instant where the caller has committed a sealed payload and the callee has not yet verified it. Not trustworthiness — it establishes what each side is running, and the delegation chain remains what says it is allowed. And it does not make the recorded cross-operator hardware run mutual; that run had the caller appraise a real TDX quote and the callee appraise nothing. This makes a mutual run possible.
🤖 Generated with Claude Code