ADR-045: Blind-Broker Exchange Partner-Evidence Architecture
On this page
Status: Accepted (2026-08-22)
Issue: #1527 (inventory + architecture); epic &79 (Gateway-derived partner mocks)
Amends: ADR-001 (canopy-exchange joins the shared-infrastructure inventory as a sealed-transit processor), ADR-004 (transit-processor capability + isolation-map/matrix reconciliation), ADR-005 (exchange becomes a required peer in every program profile; degraded behavior redefined), ADR-012 (adapter-selection config retired), ADR-017 (adds the recipient private-key lifecycle — key types, rotation overlap — that ADR-017 explicitly scoped out), ADR-034 (its flagged orchestrator-resident SOLQ tension is resolved program-side)
Compatible with (not amended): ADR-036 (its shared-per-fact-DEK model is untouched; see Rationale)
Affirms: ADR-002 (the orchestrator loses its last restricted-source touch)
Relates to: ADR-019 (service identity — the caller-binding mechanism), ADR-028 (determination input snapshots — how fetched evidence stays replayable), ADR-041 (fail-closed default + accountable override — the posture every guard here follows), the implementing plan, the Gateway interface catalog
Context
canopy’s external-partner surfaces are stubs and fabricated-data adapters:
canopy-verification’s Noop/Scripted IEVS/SAVE/SOLQ adapters behind a
noop-adapters feature, canopy-enrollment’s NoopEbtAdapter, and
canopy-exchange as a 56-line skeleton with an empty trait. Real connectivity
is partner-blocked, but the shape of every interface Georgia Gateway speaks
is now catalogued from two fused evidence bases (the source-derived
interface catalog and the 7.4 INT
design-document review — 51 packages, 168 documented operations, field-level
layouts and code tables).
Three architecture problems needed one answer:
-
Multi-jurisdiction variance. Beyond the federally fixed formats (SSA SVES/SOLQ/SDX/BENDEX, IRS BEER, FNS eDRS, NDNH, PARIS, SAVE, FFM ATX), every state’s interface estate differs — a state may run three income-verification sources where Georgia runs one. The shapes canopy’s program services consume must be jurisdiction-independent; the variance must live at the wire edge.
-
Legal scoping is mutable; architecture should not be. Statutes, waivers, and data-exchange agreements change which programs may see which source. If acquisition code lives inside a program service, every legal change is a code migration. Acquisition topology must be decoupled from entitlement.
-
Data residency is load-bearing. ADR-001/ADR-004 isolate restricted data by database precisely so that Pub 1075 / CMA / IEVS audit scope stays bounded. A central partner layer must not become a commingled plaintext store that drags the whole system into every audit boundary.
Decision
canopy-exchange becomes the real, single partner-integration service —
a blind broker — structured as three layers.
The three layers
-
L1 — canonical contracts (
crates/canopy-contracts-exchange): jurisdiction-independent evidence and command shapes per information kind. Evidence kinds:income,identity(SSN verification + death),incarceration,immigration_status,assets,enrollment_elsewhere,disqualification. Command kinds:benefit_issuance,account_transfer,referral,federal_report. Reserved kinds states need that Georgia’s estate lacks:lottery_winnings_match(the federally mandated SNAP S-LEB match) andstate_tax_income. Program services consume ONLY these shapes. -
L2 — aggregation and routing (in canopy-exchange): for evidence kinds, a grant-checked live-query facade that fans out to the enabled sources and merges in flight; for command kinds, routing to exactly one connector per (kind, jurisdiction, direction, operation). A source registry declares per-source capabilities (live | batch | workflow | command; multi-kind), enablement, and driver (calendar cron | event-trigger set | audited manual trigger).
-
L3 — wire connectors (in canopy-exchange): per-partner dialects — parsers, message builders, auth shapes. Developed and tested against the devstack partner simulator (
tools/canopy-partner-sim, its own Dockerfile build target, never in production images, refuses to serve outside development mode, watermarked responses). Real-connector enablement is compile-gated on the real-connectivity hardening feature set (mTLS, endpoint allowlists, PGP-at-rest); enabling one without it is a boot error.
The blind-broker rule
Exchange’s database stores no plaintext programmatic data. It holds operational metadata only — source registry state, scheduler state, run/batch logs, per-recipient delivery receipts, batch-file tracking (ids, checksums, control totals) — plus ciphertext it cannot decrypt.
Two at-rest exposures exist outside the database and are bounded deliberately, not hidden:
-
The inbound transfer volume (batch relay only): a partner file is plaintext on disk from arrival until parse+seal+enqueue completes, then it is deleted. Encrypted volume, tight retention, PGP-at-rest where the partner supports it. This is the one plaintext at-rest window.
-
The quarantine store (parse failures only): sealed to a dedicated forensic keypair whose private half is held by the deployment’s security function, never deployed to exchange — exchange can write quarantine entries and never read them, so the blind property holds on this path too.
-
Every durable or returned evidence object is a signed, recipient-sealed envelope: HPKE base mode (RFC 9180,
DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + AES-256-GCM, single-shot, the pinned RustCryptohpkecrate) sealing the payload to the recipient service’s public key, with an ECDSA P-256 detached JWS signature by exchange’s signing key over the RFC 8785-canonicalized inner envelope — the estate’s existing signature primitive, reusingcanopy-signing(alg: ES256, ADR-002’s determination-signing construction) rather than introducing a second algorithm. Recipients verify after decrypting; a sealed-but-unsigned envelope is rejected and audited — a database writer can seal, but cannot sign, so forged evidence cannot be injected at rest. -
The authenticated header carries: protocol version, jurisdiction, recipient service, benefit program(s), purpose, source id, classification + legal authority, evidence kind(s), schema + connector versions, a UUIDv7 delivery/query id (the replay key), record reference, recipient key id, issued/expiry times, payload digest.
-
Exchange handles plaintext in memory during parse/merge/seal — that is unavoidable wherever connectors live, and it is why the ADR-004 matrix authorizes exchange as a transit processor (a bounded, enforceable capability — see Compliance gate below), not a custodian. The crypto narrows at-rest exposure; it does not eliminate processing exposure, and this ADR does not claim otherwise.
Key and grant model
Two independent registries; key possession never participates in authorization decisions:
-
Grant registry (legal): grants keyed by source × jurisdiction × recipient service × benefit program × purpose × authority. Each connector compiles its legal maxima — classification, authority, and the maximum program set. For SOLQ/BINDEX the compiled maximum is programs {snap, tanf, medicaid, chip} — sourced from ADR-004’s §1137 CMA source table, which names those four programs — on the services the reconciled tenancy matrix authorizes (today
[class.ssa_solq_bindex]lists canopy-tanf, canopy-snap, canopy-medicaid, canopy-verification; canopy-verification’s entry lapses with the rip-out, and the reconciliation below settles the rest). Deployment configuration may disable sources or narrow programs; it can never broaden them — a config exceeding the compiled maximum is a boot error. Narrowing takes effect immediately against live queries, cache hits, and new envelopes, independent of key state. -
Key registry (crypto): append-only, audited. A service enrolls its X25519 public key by proving possession (signing a challenge with the candidate key) under its ADR-019 service identity; key id = fingerprint(jurisdiction, service, public key); duplicate key material across recipients is rejected; rotation = new key id + a bounded decryption window (runbook in security-operations). Private keys are non-Debug, non-Clone, zeroizing newtypes — a new discipline this ADR adds; the delivery of the secret follows ADR-017 (SOPS-encrypted at rest → env var →
EnvSecretProvider), while the in-memory key types and the multi-key rotation overlap are exactly what ADR-017 scoped out and this ADR now supplies. Private keys never appear in exchange config, crash output, or logs.
The three functions
-
Live query facade. Callers authenticate with ADR-019 service identity; exchange maps caller → program/purpose server-side (never caller-asserted), checks grants, fans out, merges in memory, and returns one signed sealed envelope containing the complete
EvidenceResult— per-source outcomes (Matched(n),NoMatch,NotAuthorized,NotRequested,Unavailable{retryable},TimedOut) and an overall completeness verdict travel inside the sealed payload. Consumers mapPartial/Unavailable/TimedOutto provisional or manual-review outcomes — never to silent false eligibility gates. -
Batch relay. Inbound partner files land on the transfer volume — an explicitly scoped at-rest transit boundary (encrypted volume, tight retention; real partners add PGP-at-rest where supported). Exchange stream-parses, seals each normalized record once per entitled recipient (per the compiled per-source delivery scope:
full_fileonly where the agreement authorizes whole-file receipt, elsematched_onlyvia match tokens), enqueues the envelopes, and deletes the file (parse failures quarantine sealed to the forensic keypair whose private half the deployment’s security function holds — never exchange — with audited opening). The ciphertext queue is the redelivery source: envelopes push to the recipient’s ingest endpoint; the recipient verifies, decrypts, matches, persists, audits, and acknowledges atomically; receipts are recipient-signed over delivery id
envelope hash; uniqueness key = (source, file checksum, record ref, recipient); expiry is per-source policy and always alerted. Person matching is program-local: canopy-persons provides an audited identity projection (full SSN/name/DOB for authorized evidence purposes) and a match-token facility (keyed HMAC over normalized identity); exchange computes tokens transiently at parse and places them inside sealed payloads, so programs match against their own caseloads without raw-SSN fan-out, and uncertain matches never drive adverse action. -
Command routing (origin-durable). The origin program service owns the durable command record and resubmits attempts; exchange transforms in memory per attempt. Command envelopes carry a UUIDv7 command id, idempotency key, actor, and the state machine submitted → accepted|rejected|unknown → confirmed;
unknowndemands reconciliation, never silent resolution. Outbound wire files are partner-encrypted when real and regenerable from the origin outbox in sim. Stateful partner workflows (SAVE steps, eDRS mutations) keep their continuation state in the requesting program service; tokens pass through sealed. The EBT rail follows this model: enrollment keeps the issuance ledger (settlement, expungement); the rail — account provisioning, issuance, drawdown-derived reconciliation, address sync — is a routed command/evidence kind whose vendor variance (thick-vendor API vs Georgia’s file-based EBTAS) is pure L3.
Response cache (a purpose-limitation carve-out, not a plaintext one)
Metered sources (per-transaction-billed vendors, quota-bound agreements) justify response reuse as a generic, per-source, opt-in mechanism:
-
Cache rows are signed sealed envelopes of the complete merged result for an exact query fingerprint; a hit is a ciphertext pass-through — byte-shape-identical to a miss on the API. Scope is
per_programonly. -
Fingerprints are keyed HMACs (exchange-local key) over jurisdiction, recipient service, program, purpose, kinds, source set, connector
policy versions, identity material, history window, and as-of — no plaintext PII in key columns. TTL sweep plus same-source invalidation on batch-reported changes; errors and no-hits are never cached. -
A connector compiles
cacheablefrom its legal frame. Classified sources additionally require an attestation block naming the deployment’s data-exchange agreement authorizing reuse — fail-closed default, loud audited override (ADR-041 pattern). Encryption is not a substitute for purpose limitation.
Events, audit, transport
-
Bus events are operational only (
exchange.run.completed— run id, source id, record counts). Batch delivery is push, so no evidence-availability events exist; nothing person-identifying rides the bus. -
Fail-closed audit through the canopy-security pipeline for: query and deny, parse, seal, cache fill/hit, push, receipt, expiry, key enrollment/revocation, grant change, quarantine access. Program services audit decrypt/match/persist; a nonmatch decrypt is still an access.
-
Transport: internal HTTP with service identity matches the existing estate; sealed responses add end-to-end payload encryption above it. The full transport-hardening set (mTLS, allowlists, PGP-at-rest, SSH/key rotation) is the real-connectivity gate that real restricted connectors are compile-gated on.
Compliance gate (transit capability)
compliance/data-tenancy-authorisation.toml and cargo xtask compliance
audit-data-tenancy gain a transit capability mode before any exchange
rows are added: protected field patterns may appear in exchange code
(parsing), never in exchange migrations/columns; queue and cache schemas
are type-enforced to opaque envelope blobs. Exchange is never added to the
blanket authorization lists. The ADR-004 isolation map is reconciled with
the matrix in the same change; three discrepancies exist today for
SOLQ/BINDEX and each is settled explicitly: the matrix authorizes
canopy-medicaid where the map does not list it; the matrix authorizes
canopy-verification where the map gives verification only SAVE + Death
Master File (this entry lapses with the verification rip-out); and
CHIP appears in ADR-004’s source table but in neither the map’s
service rows nor the matrix.
What this replaces
-
canopy-verification’s
noop-adaptersfeature,guard.rs, Noop/Scripted IEVS/SAVE/SOLQ adapters,AdapterSelectionconfig, and the internal ievs/save/ssa routes — deleted once the corresponding connectors and consumer cutovers land. Verification survives as the human workflow service (verifications, responses, discrepancies). -
canopy-eligibility’s pre-dispatch SOLQ fetch and
ApplicationContext.ssa_solq(the ADR-034-flagged ADR-004 tension, and its person-id-as-synthetic-SSN hack) — deleted; canopy-medicaid (the current consumer) fetches SSA benefit evidence from exchange inside its own determination boundary and snapshots it per ADR-028. -
canopy-enrollment’s
NoopEbtAdapter— replaced by the exchange EBT rail; the issuance ledger stays in enrollment.
Rationale
-
Acquisition topology decoupled from law. A statute change, waiver, or new agreement is: an ADR + tenancy-matrix amendment + a grant change + (for delivery) a key-list change. No connector moves between services.
-
The blind property makes residency additive, not subtractive. A full exchange database dump — WAL, replicas, backups included — yields ciphertext payloads plus their cleartext authenticated headers: the header is AAD, so a dump discloses a per-row metadata trail (which program asked which source about which partner record, and when). That trail is the residual disclosure this design accepts; payload contents and identity material are not in it, and compromising one program’s private key exposes only that program’s copies. This extends the ADR-001/ADR-004 isolation philosophy to the transit layer rather than weakening it. Relation to ADR-036: that ADR’s realized model is one shared per-fact DEK whose ciphertext is copied verbatim across the persons/security boundary — the antipattern it names is a second copy of the value key obtained by re-sealing plaintext. Per-recipient envelopes here neither share a private key nor re-seal plaintext outside the sealing service, so the constructions are compatible; ADR-045 does not disturb ADR-036’s DEK model.
-
Sign-then-seal closes the at-rest forgery hole that recipient encryption alone leaves open (anyone with a public key can seal).
-
Origin-durable commands keep money-movement custody with the ledger owner and keep exchange stateless per attempt — no exchange-decryptable command store exists to violate the blind rule.
-
Gateway as the "average state" template is evidence-based: its estate covers the entire IEVS-mandated set plus PARIS-VA, NVRA, direct certification, newborn, and dual-participation surfaces; the reserved kinds cover the known gaps (S-LEB, state tax income).
Consequences
-
Two new registries (grants, keys) and an envelope protocol to operate; the security-operations page gains key-lifecycle runbooks.
-
Program services take on ingest endpoints (via a shared recipient-ingest kit) and provenance columns; snap’s determination-time evidence becomes live {GDOL, SOLQ-under-SNAP-purpose} plus locally stored batch-fed {SDX, BENDEX}.
-
The devstack gains one simulator container; every parser/builder is proptest-mandatory; golden fixtures are source-derived bytes committed independently of the code under test.
-
Enabling any additional same-kind source beyond the status-quo set is blocked on the dedup/conflict-policy child — the merged view stays per-source-separated until then.
-
Delivery scope
full_filevsmatched_onlymust be justified per source from its agreement facts — recorded in the registry entry. -
Rolling deploys of wire-shape changes follow ADR-016 expand/contract; the deploy order for each cutover is exchange + keys first, recipient enrollment, then the caller flip.