DEV Community

Imran Siddique
Imran Siddique

Posted on Originally published at Medium on

What your agent can prove after the second hop

A2A solved the part everyone hits first. Agents can find each other, describe what they do, and hand each other work without a bespoke integration per pair. The extension mechanism is the piece I keep coming back to, because it made the protocol extensible without making it heavy, and the team deliberately left trust decisions to the deployment instead of baking one model into the wire format. That restraint is why anyone can profile it, including us.

So agent A delegates to agent B, and that mostly works. Then B delegates to C, and the question changes. Not “is this call authorized” but what can C check on its own, without asking anybody, including us.

cA2A is our profile on A2A for calls that cross a trust boundary. Here is what it can answer today, and how I know.

The chain is a portable object

A delegation chain is a root-to-leaf list of signed credentials. Each hop carries an issuer, a subject, a scope, a depth, a pointer to its parent, and an Ed25519 signature over the canonical body. Canonical means RFC 8785, so the bytes one implementation signs are the bytes another verifies. That is what makes the chain checkable by someone who did not build our runtime.

When C receives a task that came through B, it checks nine things and raises on the first that fails, each with its own error code. The module docstring groups them as five invariants; nine is the count of distinct checks, and it is the more useful number if you are the one reading the failure. Signatures against their issuers. The root having no parent and depth zero. Each parent_id matching the previous credential_id, and each issuer matching the previous subject. Depth incrementing and staying under the limit. No credential_id repeating. Each validity window containing the evaluation time.

Two of those nine carry most of the weight.

Scope attenuation. Every hop’s scope must be a provable subset of its parent’s. That is the confused-deputy defense: without it, B accepts a narrow task from A and then acts with authority A never granted.

Root pinning. A valid signature tells you who issued a chain, not whether you should care. A chain minted by an attacker is perfectly self-consistent: every signature verifies, every scope narrows correctly. C supplies its own set of trusted root issuers and fails closed when the root is not in it. Structural verification is spell-checking; the pinned anchor is what makes it authorization.

Offline audit gets the same verifier with — at-time, so recorded evidence is checked at the moment the action was decided rather than the moment somebody is looking. A window that lapsed since then says nothing about validity when it mattered.

What that looks like end to end

There is a runnable example in the repo, examples/cross-operator-delegation. A parent agent in domain A delegates a scoped task to a child in domain B, with independent keys on each side. It runs offline in a few seconds and checks twelve things:

The child is delegated {task:read, task:write}. Its local Cedar policy permits {task:read, task:audit}. The effective scope is the intersection, {task:read}, so the child allows task:read and denies task:write even though task:write was legitimately delegated. Delegated authority is a ceiling, not a grant.

The task is sealed to the child’s attested channel key. A silently swapped binary on the child is rejected, because its measurement no longer matches. Every hop emits a provenance record, the records form a hash-linked DAG, and reparenting a record breaks it. Then the CLI re-verifies the committed artifacts from scratch: ca2a verify-chain and ca2a verify-dag, both against an explicitly pinned root.

I fixed that example this week, which is worth admitting because of what it says about the invariant. When we made root pinning mandatory, the change updated the two CLI calls in that demo and missed the three in-process ones. The demo failed with UntrustedDelegationRoot, which reads like the example is wrong about delegation when it was simply not passing the root it already had in hand. It is 12/12 now.

Attestation, on real silicon, this week

The part I most wanted to stop hedging about. Verifying somebody else’s captured quote proves your verifier parses. It says nothing about whether your code can produce that evidence on a real machine.

So this week both collectors ran on genuine confidential VMs.

On a GCP C3 instance, Intel TDX, the guest identified itself and our provider produced an 8,000-byte DCAP v4 quote whose report data matched the binding we asked for. Our verifier then appraised it to the Intel SGX Root CA, giving a non-zero 48-byte MRTD.

On a GCP N2D pinned to AMD Milan, our provider produced a 1,184-byte SEV-SNP report, with the certificate table arriving alongside it: VCEK, SEV-Milan, and the self-signed ARK-Milan. Our verifier appraised it to that root and failed closed on a single flipped bit in the signed body.

Both quotes produced by our own code, both appraised by our own code, to the vendor roots. The captures are not committed, because a SEV-SNP report’s 64-byte CHIP_ID is a per-CPU hardware identifier.

The thing nobody was checking

A SEV-SNP report says four things well: it is authentic, it chains to AMD, its signature verifies, and it describes this measurement. Those are the right four checks and they are not in dispute.

None of them ask what kind of machine it came from.

The report carries that separately, in a field at offset 0x40. Whether SMT is enabled. Whether ECC is on. Whether the firmware completed its boot-time DRAM alias check, which is AMD’s mitigation for BadRAM. Our verifier read past that field and did not look.

It looks now. You pass a policy: which fields must be true, which must be false. The direction lives in the argument name rather than the field name, which sounds fussy until you see the alternative. The reference verifier keeps one struct of booleans documented as “the maximum of acceptable PLATFORM_INFO data” and then enforces four of its seven fields as minimums, so setting a field to true does not permit that condition, it demands it. With require and forbid as separate arguments you cannot make that mistake.

Then we pointed it at the real GCP host, and it reported 0x25:

The DRAM alias check is complete, so BadRAM is mitigated and require_platform={“alias_check_complete”} passes on GCP. ECC is on. And SMT is enabled, so forbid_platform={“smt_enabled”} rejects that host.

That is not a defect in GCP and it is not a defect in us. It is the point. Before this week, that report verified exactly as cleanly as one from a machine with SMT off and the alias check never run. If SMT matters to your threat model, you now have a way to say so and a way to find out. If it does not, nothing changes and the appraisal stays off, because it is opt in and appraises nothing by default.

What is still open, honestly

Software mode is still the default, where a peer key is accepted at assurance=”none”. The hardware path is validated, not automatic. Mutual attestation is implemented but off by default and not yet simultaneous: the caller commits a sealed payload before the callee has appraised it. And the sealed channel is per hop by construction, so a payload is plaintext inside every intermediate that acts on it. Authority composes across hops. Confidentiality terminates at each one.

What I would do on Monday

Pin your root issuers and fail closed, because signature validity is not authorization and the gap between them is where confused-deputy bugs live. Evaluate credential windows at decision time when you audit. And if you run confidential workloads anywhere, go and read PLATFORM_INFO on one of your own hosts.

I would genuinely like to know what your cloud says. Ours said SMT was on, and we had been verifying those reports for weeks without noticing.

Top comments (0)