Summary
The fix for agent-receipts/ar#719 made hash-linkage verification recompute from the verbatim receipt_json wire bytes (receipt.HashRawReceipt), so a valid chain carrying forward-compat fields a newer SDK wrote no longer reports a false break.
The signature path in the same function has the identical latent divergence and was not fixed: internal/verify/chain.go still calls receipt.Verify(cr.Receipt, publicKeyPEM) on the parsed struct.
// internal/verify/chain.go (~L82)
if publicKeyPEM != "" {
ok, err := receipt.Verify(r, publicKeyPEM) // r = cr.Receipt (parsed struct)
...
}
receipt.Verify rebuilds an UnsignedAgentReceipt from the Go struct and canonicalizes that. Any field the current struct doesn't know about is dropped on json.Unmarshal before verification.
Impact
For a receipt where a newer SDK added a field nested inside credentialSubject and signed over it (Sign canonicalizes the whole UnsignedAgentReceipt, including credentialSubject):
- Hash linkage → valid (the #719 fix hashes raw bytes, preserving the field) ✅
signature_valid → false (the dashboard's older struct drops the nested field, canonicalizes different bytes, Ed25519 verify fails) ❌
So GET /api/chains/{id}/verify?public_key=... would report the chain's hash links intact but every signature invalid — a contradictory, misleading result for a genuinely valid signed chain.
Note: top-level forward-compat fields are not affected, because Sign/Verify only cover the fixed UnsignedAgentReceipt shape (Context, ID, Type, Version, Issuer, IssuanceDate, CredentialSubject) — a top-level extra field is signed by neither side. The divergence is specific to fields nested within the signed payload (e.g. credentialSubject.*).
Scope / reachability
- Reachable only via the
?public_key= API parameter; the dashboard UI does not currently pass a key (the "Verify signatures" button does hash/sequence checks only — see the relabel note in #719).
- Pre-existing; not a regression from the #719 fix.
Suggested fix
There is no raw-bytes signature verifier in the SDK today (receipt package exposes only Verify(AgentReceipt, string); no VerifyRaw([]byte, ...)). A proper fix needs the SDK to expose a verify that canonicalizes the verbatim wire bytes (minus proof), analogous to HashRawReceipt vs HashReceipt. Then VerifyChainLinks can verify signatures against cr.Raw the same way it now hashes against cr.Raw.
Until then, options:
- File/track the SDK
VerifyRaw work and switch the dashboard once available.
- Or document the limitation alongside the existing #719 docs disclaimer.
References
Summary
The fix for agent-receipts/ar#719 made hash-linkage verification recompute from the verbatim
receipt_jsonwire bytes (receipt.HashRawReceipt), so a valid chain carrying forward-compat fields a newer SDK wrote no longer reports a false break.The signature path in the same function has the identical latent divergence and was not fixed:
internal/verify/chain.gostill callsreceipt.Verify(cr.Receipt, publicKeyPEM)on the parsed struct.receipt.Verifyrebuilds anUnsignedAgentReceiptfrom the Go struct and canonicalizes that. Any field the current struct doesn't know about is dropped onjson.Unmarshalbefore verification.Impact
For a receipt where a newer SDK added a field nested inside
credentialSubjectand signed over it (Signcanonicalizes the wholeUnsignedAgentReceipt, includingcredentialSubject):signature_valid→ false (the dashboard's older struct drops the nested field, canonicalizes different bytes, Ed25519 verify fails) ❌So
GET /api/chains/{id}/verify?public_key=...would report the chain's hash links intact but every signature invalid — a contradictory, misleading result for a genuinely valid signed chain.Note: top-level forward-compat fields are not affected, because
Sign/Verifyonly cover the fixedUnsignedAgentReceiptshape (Context, ID, Type, Version, Issuer, IssuanceDate, CredentialSubject) — a top-level extra field is signed by neither side. The divergence is specific to fields nested within the signed payload (e.g.credentialSubject.*).Scope / reachability
?public_key=API parameter; the dashboard UI does not currently pass a key (the "Verify signatures" button does hash/sequence checks only — see the relabel note in #719).Suggested fix
There is no raw-bytes signature verifier in the SDK today (
receiptpackage exposes onlyVerify(AgentReceipt, string); noVerifyRaw([]byte, ...)). A proper fix needs the SDK to expose a verify that canonicalizes the verbatim wire bytes (minusproof), analogous toHashRawReceiptvsHashReceipt. ThenVerifyChainLinkscan verify signatures againstcr.Rawthe same way it now hashes againstcr.Raw.Until then, options:
VerifyRawwork and switch the dashboard once available.References
internal/verify/chain.goVerifyChainLinks, branchclaude/eloquent-knuth-5tuvX.