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:

  1. 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.

  2. 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.

  3. 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) and state_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 RustCrypto hpke crate) 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, reusing canopy-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:

  1. 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.

  2. 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

  1. 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 map Partial/Unavailable/TimedOut to provisional or manual-review outcomes — never to silent false eligibility gates.

  2. 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_file only where the agreement authorizes whole-file receipt, else matched_only via 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.

  3. 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; unknown demands 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_program only.

  • 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 cacheable from 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-adapters feature, guard.rs, Noop/Scripted IEVS/SAVE/SOLQ adapters, AdapterSelection config, 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_file vs matched_only must 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.

Edit this page · default