Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,137 changes: 1,137 additions & 0 deletions conformance/corpus/cases.json

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions conformance/corpusgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ func seeds() map[string]proto.Message {
// items field is now repeated.min_items=1 (single-offer mode removed).
"Offer": offer(),
"TransactionRequest": &rampv1.TransactionRequest{IdempotencyKey: "idem-tx", Items: []*rampv1.TransactionItem{{Offer: offer()}}},
"AgentRequestAcceptancePayload": &rampv1.AgentRequestAcceptancePayload{
Items: []*rampv1.AgentRequestAcceptanceItem{{
OfferSig: "offer-signature",
Exchange: "exchange.example",
}},
RequesterId: "agent-seed", IdempotencyKey: "idem-tx",
},
// ramp.admin.v1 payloads embedded (required) in the setter request/response
// envelopes. RequiredFields MUST be exactly ["x"]: the repeated.unique
// duplicate_item edge appends the auto-filled good item (stringSamples[0]=="x")
Expand Down
40 changes: 30 additions & 10 deletions docs/design-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ agent's identity (`agent_identity_hash`) and needs no return-path relay.

## Canonical signing: JCS over proto-JSON, not deterministic protobuf

The two signed RAMP payloads that cover a protobuf message — `Offer.signature` and
the agent's `AgentAcceptance.signature` — originally covered deterministic protobuf
*binary*: marshal the message with deterministic field order, sign those bytes. We
Three signed RAMP payloads cover a protobuf message: `Offer.signature`, the
agent's `AgentAcceptance.signature`, and the agent's
`AgentRequestAcceptance.signature` — the request-set proof, added later and born
directly onto the settled form. The first two originally covered deterministic
protobuf *binary*: marshal the message with deterministic field order, sign
those bytes. We
reversed that and moved both onto RFC 8785 JCS over canonical proto-JSON,
`JCS(protojson(msg with the signature fields cleared))`, under one pinned
proto-JSON option set (snake_case field names, enums as name strings, unpopulated
Expand Down Expand Up @@ -78,15 +81,18 @@ One clause of that pinned option set carries more weight than it looks.
with an empty value — so the canonical bytes for an empty string field are never the
bytes for a populated one. Go inherits that from `protojson` for free. A port that
assembles the JSON object by hand does not, and has to enumerate the omission for
every field or it signs bytes Go never produces. The acceptance payload is the one
place the Python and TS ports hand-build the object — Go renders the
`AgentAcceptancePayload` message through the same protojson canonicalizer as the
offer — and it is exactly where the divergence
every field or it signs bytes Go never produces. The acceptance payloads are the
places the Python and TS ports hand-build the object — both
`AgentAcceptancePayload` and `AgentRequestAcceptancePayload`, the latter down to
each entry of its nested `items` list — while Go renders both messages through
the same protojson canonicalizer as the offer. The original acceptance payload
is exactly where the divergence
appeared: Python and TS dropped an empty `requester_domain` but emitted an empty
`requester_id`, which is wire-valid because `Requester.id` carries no `min_len`. Such
a mismatch fails closed, but it falsifies the byte-equivalence the canonical-bytes
accessors promise, so the shared corpus now carries a vector that holds every port to
the omission.
the omission, and the request-acceptance vectors pin the same invariant for the
second hand-built payload.

The two reversals left a consequence worth naming, because it is the reason the
canonical bytes are a first-class SDK export rather than an internal detail. A
Expand All @@ -95,8 +101,10 @@ re-derives the bytes at verification time has silently pinned an *already-signed
payload to whatever canonicalization the SDK implements *later* — the failure would
surface as "the signature does not verify", indistinguishable from "it was never
signed". So all three SDKs expose the exact signed bytes as a public accessor
(`CanonicalOfferBytes` / `CanonicalAcceptanceBytes` in Go, `canonical_offer_payload`
/ `jcs_acceptance_payload` in Python, `canonicalOfferPayload` / `acceptancePayload`
(`CanonicalOfferBytes` / `CanonicalAcceptanceBytes` /
`CanonicalRequestAcceptanceBytes` in Go, `canonical_offer_payload` /
`jcs_acceptance_payload` / `jcs_request_acceptance_payload` in Python,
`canonicalOfferPayload` / `acceptancePayload` / `requestAcceptancePayload`
in TS). A party keeping evidence stores those bytes and re-verifies against them
verbatim, rather than trusting a future canonicalizer to reproduce the past.

Expand Down Expand Up @@ -433,6 +441,18 @@ top-level-versus-items mismatch to police. That last exemption is why
string: an empty value is unroutable, and the swap-protection its signature is
supposed to provide is vacuous when the signed bytes carry no recipient at all.

`AgentRequestAcceptanceItem.exchange` does not reopen that exemption. It is a
signed **projection index**, not an audience field: the binding audience
statement for a transaction stays `Offer.exchange` inside each Exchange-signed
offer, and the item's copy exists so a recipient of a projected subrequest can
derive which signed references must appear in its own projection without
holding the offers addressed to other Exchanges. The
top-level-versus-items-mismatch objection that killed a top-level audience
field does not apply, because the projection check itself polices the copy:
every forwarded offer must name the verifying Exchange, and every reference
whose `exchange` names it must be present, in order, so a disagreement between
the two spellings of the recipient is a refusal, not a latent inconsistency.

## The audience match is exact; the endpoint rule is not

Two host comparisons sit a few sections apart in this document and answer
Expand Down
10 changes: 9 additions & 1 deletion docs/sdk-parity-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

Go is the oracle (`sdk/go/{helpers,resolvers,core,connect,connectserver}`); Python and TS mirror it. This document is **generated** from the same two artifacts CI already enforces against the code, so it cannot drift from the real surface — a mismatch fails the API-surface gate or the corpus-completeness gate before it can reach this file.

**At a glance:** 130 symbols at cross-language parity · 16 documented divergences · 179 Go-idiomatic exclusions · 34 conformance corpora, each tri-replayed.
**At a glance:** 133 symbols at cross-language parity · 16 documented divergences · 183 Go-idiomatic exclusions · 35 conformance corpora, each tri-replayed.

Layering (L1 pure trust core vs L2 I/O resolvers), the SSRF transport-wiring invariant, and naming conventions are recorded in [`design-history.md`](./design-history.md).

Expand All @@ -32,6 +32,7 @@ Legend: a name = the public face in that language · `—` = intentionally none
| `BareDomainPattern` | `BARE_DOMAIN_PATTERN` | `bareDomainPattern` |
| `CanonicalAcceptanceBytes` | `jcs_acceptance_payload` | `acceptancePayload` |
| `CanonicalOfferBytes` | `canonical_offer_payload` | `canonicalOfferPayload` |
| `CanonicalRequestAcceptanceBytes` | `jcs_request_acceptance_payload` | `requestAcceptancePayload` |
| `CanonicalRestrictionToken` | `canonical_restriction_token` | `canonicalRestrictionToken` |
| `CanonicalizeMoney` | `canonicalize_money` | `canonicalizeMoney` |
| `CatalogRejectionDetail` | `catalog_rejection_detail` | `catalogRejectionDetail` |
Expand Down Expand Up @@ -93,6 +94,7 @@ Legend: a name = the public face in that language · `—` = intentionally none
| `SignOffer` | `sign_offer_jcs` | `signOffer` |
| `SignOfferAcceptance` | `sign_offer_acceptance_jcs` | `signOfferAcceptance` |
| `SignRequest` | `sign_request` | `signRequest` |
| `SignRequestAcceptance` | `sign_request_acceptance_jcs` | `signRequestAcceptance` |
| `SignURLEd25519` | `sign_ed25519_signed_url` | `signEd25519SignedUrl` |
| `SignatureAgentHeader` | `SignatureAgentHeader` | `SignatureAgentHeader` |
| `StaticKeyResolver` | `StaticKeyResolver` | `StaticKeyResolver` |
Expand All @@ -106,6 +108,7 @@ Legend: a name = the public face in that language · `—` = intentionally none
| `VerifyMultisigRequest` | `verify_multisig_request_server` | `verifyMultisigRequestServer` |
| `VerifyOfferAcceptance` | `verify_offer_acceptance_jcs` | `verifyOfferAcceptance` |
| `VerifyRequest` | `verify_request` | `verifyRequestServer` |
| `VerifyRequestAcceptance` | `verify_request_acceptance_jcs` | `verifyRequestAcceptance` |
| `VerifyURLEd25519` | `verify_ed25519_signed_url` | `verifyEd25519SignedUrl` |

### resolvers — L2 I/O (key/endpoint resolution, active-key, SSRF-guarded fetch)
Expand Down Expand Up @@ -319,6 +322,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c
| `helpers.ErrOfferExpired` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. |
| `helpers.ErrOfferSignatureInvalid` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. |
| `helpers.ErrProofOfPossessionMismatch` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. |
| `helpers.ErrRequestAcceptanceSignatureInvalid` | Go errors.Is sentinel; py/ts return false for a request-acceptance mismatch rather than exporting a sentinel. |
| `helpers.ErrSignatureLifetimeTooLong` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. |
| `helpers.ErrSignatureVerify` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. |
| `helpers.ErrTooManyHops` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. |
Expand All @@ -343,6 +347,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c
| `helpers.RegistrationDataTooManyMembers` | Member of the mapped helpers.RegistrationDataVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. |
| `helpers.RegistrationDataUncanonicalizable` | Member of the mapped helpers.RegistrationDataVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. |
| `helpers.RegistrationSchemaCompileTimeout` | Go-only wall-clock backstop on compilation, not part of the accepted/refused contract and deliberately absent from the ports: Go's runtime preempts, while a CPU-bound spin holds CPython's interpreter and blocks Node's event loop, so a timer there cannot interrupt the work it names. What bounds all three identically is static — the size, depth and evaluation caps and the pattern alphabet — and no admitted schema should ever reach this timeout. |
| `helpers.RequestAcceptancePayload` | Go typed-protobuf builder for the request-acceptance payload; Python and TypeScript build their language-native payload objects inside the mapped canonicalizer/client. |
| `helpers.RetrievalAuthFailureReasonFromToken` | Go lookup from the delivery edge's refusal token to the typed enum; py/ts branch on the token string directly. |
| `helpers.SchemaAccepted` | Member of the mapped helpers.SchemaVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. |
| `helpers.SchemaCompileTimeout` | Member of the mapped helpers.SchemaVerdict vocabulary. Python and TypeScript spell it as a literal; the shared registration-schema corpus pins the token. |
Expand All @@ -361,6 +366,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c
| `helpers.SharedValidator` | Go protovalidate validator singleton; TS/Python ship no protovalidate face. |
| `helpers.SignOfferAcceptanceWith` | Go Signer-custody variant of SignOfferAcceptance so the SDK never holds the key; py/ts pass key material directly to their single acceptance signer. |
| `helpers.SignOptions` | Go options struct for SignRequest; py/ts pass options via kwargs/options objects. |
| `helpers.SignRequestAcceptanceWith` | Go Signer-custody variant of SignRequestAcceptance; Python custody is a SigningTransport method and TypeScript passes its CryptoKey directly. |
| `helpers.SignatureAgentFromContext` | Go context.Context accessor; py/ts thread signature-agent state explicitly. |
| `helpers.SignedURL` | Go signed-URL result value type; py/ts return language-native result objects. |
| `helpers.Signer` | Go Signer interface; py/ts inject a sign function (TS Ed25519SignFn; Python a signing callable) rather than a named Signer type — divergent handle shape, not a missing operation. |
Expand All @@ -372,6 +378,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c
| `helpers.VerifyOffer` | Go low-level offer-signature verify; py/ts route offer verification through the Verifier face (core.Verifier). |
| `helpers.VerifyOptions` | Go options struct for verify; py/ts pass options via kwargs/options objects. |
| `helpers.VerifyPresentedOffer` | Go low-level presented-offer freshness verify; py/ts route offer verification through the Verifier face. |
| `helpers.VerifyRequestAcceptanceProjection` | Go Exchange-server projection gate; Python and TypeScript currently ship agent clients, while canonical sign/verify remains at parity and shared vectors pin the payload. |
| `helpers.VerifyRequestResolved` | Go resolver-injected VerifyRequest overload; py/ts expose a single verify entry point. |
| `helpers.WithSignatureAgent` | Go functional-option builder; py/ts pass options via kwargs/options objects. |
| `resolvers.ActiveKeyScanOptions` | Go scan-options struct; py/ts pass scan options inline. |
Expand Down Expand Up @@ -408,6 +415,7 @@ Go emits each `*-vectors.json` oracle; Python and TS replay it. The completeness
| `helpers/testdata/offer-verify-vectors.json` | ✅ | ✅ | ✅ |
| `helpers/testdata/pop-vectors.json` | ✅ | ✅ | ✅ |
| `helpers/testdata/registration-schema-vectors.json` | ✅ | ✅ | ✅ |
| `helpers/testdata/request-acceptance-vectors.json` | ✅ | ✅ | ✅ |
| `helpers/testdata/scopes-vectors.json` | ✅ | ✅ | ✅ |
| `helpers/testdata/sign-request-vectors.json` | ✅ | ✅ | ✅ |
| `helpers/testdata/signedurl-vectors.json` | ✅ | ✅ | ✅ |
Expand Down
Binary file modified gen/descriptor.binpb
Binary file not shown.
Loading
Loading