Skip to content

Chain verify: signature check false-negatives on forward-compat receipts (same class as #719, for signatures) #73

Description

@ojongerius

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_validfalse (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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions