Status: draft for review. Audience: a bank's cryptography team, its data-protection officer, and its counsel. What this document is for: so those three people can decide whether Mirror is safe to join without taking a vendor's word for anything.
Everything below is implemented and test-enforced. Where something is not implemented, or is weaker than a reader might assume, it says so in the same sentence. There is no section of this document whose purpose is to sound impressive.
1. The problem, stated precisely#
The three legs of a trade live in three institutions. The exporter's bank sees the invoice. Customs sees the declared value. The importer's bank sees the payment. Trade-based money laundering is a mismatch between those legs — declare a 1,000-unit shipment at $300/unit when it is worth $100 and $200,000 has moved under cover of a legitimate-looking trade.
No party can see the mismatch, because no party holds more than one leg. Every incumbent AML system monitors one institution in isolation. The obvious fix — pool the data — is unlawful, commercially unacceptable, or both.
Mirror is the join, performed blind.
2. Roles and trust model#
| Role | Holds | Learns |
|---|---|---|
| Member (a bank, customs, a trade-data source) | raw values, the corridor key, the shared secret | its own data; whether its leg agreed |
| Operator | commitments only | nothing about any value — see §9 |
| Auditor / examiner | a signed record a member handed them | that the record is authentic and unaltered |
| Supervisor (regulator, read-only) | signed tree heads and proofs | that the log has not been rewritten — never a value |
The operator is not trusted, and does not need to be. It is structurally unable to read a value because a value is never in its input. That is a property of the wire format, not a policy, not a contractual promise, and not a configuration setting. §9 states exactly where the property ends.
2.1 Threat model — what must remain impossible#
Assume all of these adversaries at once, and the guarantee still holds. This table is the security product; if any row becomes false, that is a Sev-0, not a backlog item.
| Adversary | Goal | Why it fails |
|---|---|---|
| Curious, coerced, or rooted operator | Read a value, a name, a locator, a salt | Those objects were never in its input. Not encrypted — absent |
| Operator insider | Export "the good stuff" | There is no good stuff. The schema holds commitments, public keys, verdicts |
| Malicious member | Forge a clear, bury a divergence, learn a counterparty's invoice | Forging needs the other member's key or a hash collision; burial is visible in the append-only log; raw invoices never cross |
| Compromised bander (supply chain) | Exfiltrate raw values, or commit the wrong band | Deterministic commitment + member-side auditor replay: the member re-runs the pinned code on its own salt and must get the same commitment |
| Network observer | Traffic-analyse who reconciles what, when | ⚠️ NOT MITIGATED TODAY — see §2.4. shipment_ref travels in the clear so the operator can group a trade's legs. Cover traffic and opaque locators are specified, not default |
| Examiner or foreign state compelling Mirror | Compel the values | Mirror can produce only commitments, keys and verdicts. This must be true, not policy — see §2.2 |
| Rogue analyst inside a member | Use the bander as a dump valve | The bander logs who ran it, never what it saw |
| Rollback / replay | Re-use a cheap table over a price spike | The table is bound into the commitment (the corridor id is the hash of the edges) and versions are append-only |
The claim we make to a CISO is not "we encrypt well". It is: full root on Mirror production yields membership, corridor ids, opaque 256-bit blobs, signed verdicts, and the shipment references those verdicts are grouped by — nothing else. If a debug flag, a tracing span, or an error message ever violates that sentence, the architecture has failed, not the configuration.
That sentence used to end at "signed verdicts — nothing else", which was not true: shipment_ref is in the clear on the wire and in the store (§5.0). A security audit found the contradiction between that claim and §5.0 and was right to treat it as disqualifying — a reader who catches a spec contradicting itself has no reason to believe the rows they cannot check. The clause is now in the sentence, and the exposure it creates has its own section rather than a footnote.
2.2 Designed inability, and its limits#
The operator's inability is enforced in four places, and it is worth being exact about how strong each one is:
- The wire rejects raw data. Any payload carrying a value, unit count, band, corridor table, secret, salt, entity name, or counterparty name is refused at the door with
400 raw_leaked— on every route, and for the pKYC and flow surfaces with their own blocklists. Rejected, never stripped: silently discarding it would teach a caller that sending it is acceptable. - No filing decision can enter. A separate wall refuses any suspicious-activity field at any nesting depth (
400 sar_wall). See §10. - A build gate on the operator's own source. A linter fails the build if operator-side code so much as names a value-shaped field. Encryption-at-rest of data you should not have is how you accidentally have it; the control is that the code cannot name it.
- The commitment construction itself. A value is not encrypted on its way in — it is never sent. Only a salted hash of a band index travels.
The limit, stated plainly. Point 3 is a lint over source, which catches a named field, not obfuscation: it cannot stop leg["amo"+"unt"], a base64 blob, or data smuggled through a generic key. Its realistic threat is the well-meaning engineer adding a debug field, and against that it is effective. It is not a defence against a determined insider with commit access — nothing in this list is. What defends against that insider is point 4: even with commit access, the operator cannot recover a value it was never sent, because the salt lives with the members.
2.3 Cryptographic construction notes#
- Binding comes from the hash: after a member emits a commitment it cannot open it to a different band without a second preimage.
- Hiding from the operator lives entirely in the salt — 256 bits, derived per shipment via HMAC-SHA-256 from a secret the operator never holds. This is what makes the operator's brute force infeasible even though the band space is roughly 12–24 buckets.
- Not hiding from the counterparty. Stated in §9 and not treated as a defect.
- Domain separation. Distinct hash namespaces are tagged and length-prefixed so that fields cannot be spliced across them — a locator preimage is
<len>:<field>concatenated undermirror.locator.v1, and each banded dimension commits under its own tag. The core band commitment concatenates a fixed-width 64-hex salt with the band, which is unambiguous for the same reason. - Known gap, honestly. The verdict signature is ES256 (ECDSA P-256), which is not post-quantum. The commitments are hash-based and therefore already PQ-safe, but a verdict that must still verify in 2040 should carry a second, lattice-based signature. Mirror does not ship one today, and will not hand-roll one: it requires a vetted implementation, and shipping unreviewed post-quantum crypto would be worse than the gap it closes.
2.4 Residual exposure: what the wire still reveals#
This section exists because §2.1 used to claim "no shipment identifiers on the wire" while §5.0 said shipment_ref travels in the clear. Both could not be true. Rather than quietly delete the row, here is the exposure, stated at the same resolution as everything Mirror does mitigate.
What is exposed. Every leg submission carries shipment_ref in the clear, and the operator stores it. So the operator — and anyone who can observe its traffic or compel its logs — learns:
- that two named members reconciled the same trade reference, and when;
- how often a given pair of institutions reconciles, and the shape of that timing;
- the reference itself, which in documentary trade is frequently an LC number (§4) and is therefore a real-world identifier, not an opaque token.
What is still NOT exposed, even to a rooted operator: the value, the unit count, the band, the corridor's price table, the salt, and the counterparty's customer. Those remain absent, not encrypted. The exposure here is traffic analysis of correspondent pairs, not disclosure of trade terms.
Why it is like this. The operator has to group the legs of one trade to compare them, and grouping needs a join key. shipment_ref is the join key that requires no additional coordination between members, which is why the protocol shipped with it.
The specified mitigation, and its status. §4 defines a locator commitment: an opaque, length-prefixed value both members derive from the corridor secret. Grouping by that instead of by shipment_ref gives the operator a 256-bit blob with no external meaning, and padded fixed-interval batches remove the timing signal. That construction is specified but not the default, and until it is, this row of the threat table is false and is marked false.
Do not read the mitigation as shipped. A control that exists in a document and not on the wire is a plan, and this specification does not get to describe plans in the present tense.
3. The corridor#
A corridor is a party-agreed, versioned reference table of price bands for one product class.
Mirror invents no prices. Corridor edges are supplied by the members. There is no default price curve anywhere in the implementation, and shipping one would make Mirror a data vendor competing with the sources members already license.
A corridor is identified by its content, so two parties who used the same table get the same id without coordinating a label:
corridor_id = "c:" + sha256(sorted_edges.join(","))Corridors are append-only and versioned. Superseding a table creates a new version; the old one is retained forever, so a verdict signed last year can be re-checked against the exact table it was judged under. A commitment cites its table version, and mixing two versions can never reconcile — it is
undetermined.A corridor becomes usable only when a quorum of distinct members ratifies it. Quorum counts distinct members, never distinct signatures, and is never less than 2.
A table's outer bands are open. Band 0 runs from zero to the first edge and the top band from the last edge to infinity, so two legs that both land in either one agree at any ratio. A corridor MAY be ratified closed (
closed: true, §5): the table is then the half-open range(first edge, last edge], and a value outside it can never clear. Only on a closed table can a match ceiling (max_ratio, §7.1) be met, because only there is every clear's window bounded.The shape a new corridor must have. An operator admits a new
corridor.propose(and a ratification of one still pending) only if it is closed, carries amax_ratioabove 1 and at most 1.5, has at least two edges, and its widest band at tolerance 0 — with its ratifiededge_margin— fits under that ceiling. Anything else is refused at/registry/eventwithcorridor_not_closed,ceiling_required,ceiling_too_wide,corridor_no_interiororceiling_below_table; a citation corridor (§7.3), which cannot be closed yet, withcorridor_shape. The rule is applied at the door only: replayed history is never re-judged. The cap is a released constant (CORRIDOR_RATIO_CAPin the kernel), never an operator setting.
3.2 Federation — a verdict co-signed by independent operators#
A corridor MAY name a federation: { k, operators: [{ kid, org, jwk }] }, ratified with the table.
- Shape.
kis a whole number, at least 2 and at most the number of operators. The operators are 2 to 16. Akidis DERIVED from the operator's public key ("mir-" + sha256(canonical({crv, kty, x, y})).slice(0, 16)) and must equal the one given. Two operators may not share a key, and may not share an organisation name, compared case-insensitively with whitespace collapsed. A private key (d) is refused. The federation's id is"fd:" + sha256(canonical(federation)), with the operators sorted by kid. - Co-signing. A verdict is co-signed when k distinct listed operators each return a record that verifies under their own listed key, names their own key in the proof, and is for the requested shipment, and the identical canonical record is what they signed. One operator counts once, however many answers it gives. Two different records for the same shipment are DISSENT: nothing is co-signed, even where k operators agree. An operator that does not answer is not a vote against.
- Governance agreement. Operators that answer and show a different federation for the corridor, or no corridor, disagree and block the co-sign. At least k must have answered with the federation.
- Offline check. A co-signed verdict is verified from the artifact and the federation alone: each co-signature verifies over the record, each is a listed key, and at least k distinct keys signed.
- What is not changed. The record is an ordinary verdict, so every operator still learns only the commitments. The federation does not lift the independence gate: two parties alone are undetermined on every operator.
Co-signing is replication with quorum. It is not threshold ECDSA and not secure multi-party computation.
3.1 Ratio corridors — a table nobody negotiates#
A corridor MAY band the ratio of each price to a public reference instead of the price itself. The table is then dimensionless and published once for every corridor, and the only corridor-specific choice is which public figures make the reference — a citation, not a price.
- Published ratio tables (closed by construction, constants of the protocol):
ratio-5 = [0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15],ratio-10 = [0.85, 0.95, 1.05, 1.15],ratio-20 = [0.7, 0.9, 1.1, 1.3]. - The reference set (ratified with the corridor, public):
{ table, sources, quorum, max_spread, rule }.sourcesis 1–8 citations, each exactly{ source, instrument, publication, basis, currency, unit }— text only, no figure — all quoting the same currency, unit and basis, none listed twice.quorumis a whole number from 1 to the number of sources;max_spreada decimal in (0, 0.2];ruleone of the FX date rules (on_board_date,invoice_date,payment_value_date,fixed_date). Its id is"rs:" + sha256(canonical(set)), withmax_spreadas its shortest decimal string. - The reference R is derived party-side from the bank's own figures file
{ set_id, date, figures: [{ source, value, file_sha256 }] }: one figure per source, each a plain positive decimal with the SHA-256 of the publisher's file it came from. At leastquorumdistinct sources must report. With the counted figures sorted,hi ≤ lo · (1 + max_spread)must hold exactly, or there is no reference and nothing is banded. R is the median: the middle figure, or for an even count the exact mean of the middle two, written as its shortest decimal string. - The pin
{ set_id, date, rule, figures: [{ source, file_sha256 }] }(sorted by source; never a value) rides inside the committed pricing basis asref(§7.4), so two legs divided by different files readbasis_mismatchand can never clear. - The leg is an ordinary
commitLeg(§5) withunit_qty' = unit_qty × Rcomputed exactly, the ratio table ascorridor_edgesandclosed: true. A product that cannot be represented exactly makes the leg unbandable. Commitments are therefore byte-compatible with every implementation of §5. - Admission. A
corridor.proposecarryingreferencemust carry exactly that table's edges andclosed: true; otherwise the registry refuses it and the operator refuses it at/registry/eventwithcorridor_reference. The shape rule of §3 (a ceiling of at most 1.5) still applies. - Joining. One member authors a corridor (
corridor.propose); others join by accepting it (corridor.ratify), never by co-authoring it.
What it does not change. The operator still sees commitments only: it never learns R, a figure, a price or a band. What it adds is a new attack surface, stated plainly: an adversary may attack the reference instead of the trade. The quorum median and the spread refusal blunt that; they do not close it. Goods with no credible public reference — services, bespoke goods — have no ratio corridor and keep a negotiated closed table.
4. The locator — how two parties agree they mean the same shipment#
This is the part most easily got wrong, and the part a DPO will ask about first.
Two banks must agree they are discussing one shipment without either sending a customer-identifying reference to the operator. An invoice or bill-of-lading number is commercially sensitive and often identifying, so it never travels in the clear.
LP(fields) = concat over fields of <byte-length> ":" <field> (no separator)
inner = sha256(LP("mirror.locator.v1", corridor_key, locator_type, parts...))
commitment = "l:" + sha256(LP("mirror.locator.v1.outer", corridor_key, inner))
Length-prefixing is load-bearing, not decoration. A bare "|" join makes the parse ambiguous: type "A|B" with value "C" would hash identically to type "A" with value "B|C", letting a party walk a locator from one shipment onto another's mapping. Every field is length-prefixed so (type, parts) has exactly one parse.
The corridor key is printable ASCII with no spaces (0x21–0x7E), trimmed, 32 to 512 characters. A key outside that is refused, never truncated. The length is a floor, not an entropy measure.
Normalisation. Two families, and the difference is the point:
- Free-form references (
lc,bl, and the reference parts ofinv,wr,po): remove all whitespace, then upper-case. Punctuation is content —LC-2026/0412andLC20260412are different references, and Mirror does not decide otherwise. - Structured identifiers (
awb, the container number inctr, the IMO number inimo, and every UN/LOCODE): remove whitespace,-,/and., then upper-case. This is safe only because each standard defines a fixed alphanumeric form, so no two identifiers can differ by a separator alone. - Dates have whitespace removed but keep their hyphens: strict
YYYY-MM-DD, years 1900–2999, and they must be real calendar dates (2026-02-30is refused, not rolled into March).
A numeric input is accepted only when finite; a lost leading zero is not restored.
Recognised locator types, because real documentary trade is messy. parts are hashed in the order listed, after normalisation:
| Type | parts, in order | Validation |
|---|---|---|
lc | letter-of-credit number | 3–64 printable chars |
bl | bill-of-lading number | 3–64 printable chars |
inv | invoice no, invoice date, incoterms, HS4 | every part required; date real; incoterms [A-Z]{3}; HS4 exactly 4 digits (./- removed) |
wr | issuer, receipt no | both required; issuer 2–64, receipt 3–64 printable chars |
po | issuer, PO no, PO date | all required; issuer 2–64, PO no 3–64; date real |
awb | 11-digit air waybill number | exactly 11 digits; check digit valid (below) |
ctr | container no, port of loading, on-board date | full_container_load must be boolean true; container [A-Z]{3}U[0-9]{7} with a valid check digit; port a UN/LOCODE; date real |
imo | 7-digit IMO number, port of loading, load date | full_cargo must be boolean true; an optional leading IMO is removed; 7 digits with a valid check digit; port a UN/LOCODE; date real |
Composites (inv, wr, po, ctr, imo) are objects; a bare string for a composite type is refused. Every composite refuses if any part is missing — a partial composite collides across different shipments.
Check digits, exactly:
- ISO 6346 container number. Digits have their face value. Letters run from
A = 10upward, skipping every multiple of 11 (A=10, B=12 … K=21, L=23 … U=32, V=34 … Z=38). Take the first ten characters, multiply the value at positioni(0-based) by2^i, sum, takemod 11, thenmod 10. The result must equal the eleventh character.CSQU3054383is valid. - IMO number. Weight the first six digits
7, 6, 5, 4, 3, 2, sum, and take the last digit of the sum. It must equal the seventh digit.9074729is valid. - Air waybill. The digits in positions 4–10 (1-based), read as one number,
mod 7, must equal the eleventh digit.020-12345675is valid. - UN/LOCODE. Shape only:
[A-Z]{2}[A-Z2-9]{3}. Whether the code is on the published list is not checked.
The cargo keys are full-load only. ctr and imo exist so two lenders holding different documents for the same cargo — a bill of lading at one bank, a warehouse receipt at another, with no shared reference to alias — can still meet. A consolidated (LCL) container or a part cargo carries several consignments, so a container- or vessel-level key would join unrelated ones; those commit their own bill of lading instead. The full-load flag is refused on anything but boolean true — a string, 1 and an omitted flag are all refused. Container and IMO numbers are public and low-entropy, so a cargo key blinds the operator and outsiders, not anyone else who holds the key.
The hard case is handled explicitly. One side holds an LC number, the other a BL number, for the same shipment. A member-held alias table maps a locator to the equivalent locators that member knows about, so a party emits commitments for all of its aliases and matching is set intersection. The alias table lives inside the institution and never travels. Three conditions fail closed — the resolution is refused and counted, never guessed:
- an ambiguous alias: one locator mapped to two different shipments;
- a contaminated class: any locator in the shipment's class also claimed by another shipment;
- a physical contradiction: the same container or IMO number bound to two different (port, date) legs inside one shipment's class. One box or one full cargo loads once per leg. Reusing a box on a later voyage for a different shipment is not a contradiction — each leg is its own key.
Any unreadable row or entry in the table fails the whole resolution closed.
5. The commitment#
Everything the operator receives about a value is derived as follows, entirely inside the institution:
salt = HMAC-SHA256(shared_secret, "mirror:" + shipment_ref) (hex)
band = count of corridor edges strictly below (value / unit_qty)
commitment = sha256(salt + ":" + band) for each band in [band-t, band+t]
corridor_commitment = sha256(salt + "|c|" + corridor_id)
tis the tolerance, capped at 6. A party emits a commitment for its own band and for the neighbouring bands it would still accept as consistent.edge_margin(optional, the near-edge fix). A value a rounding error from a band edge is fragile: two honest banks can land in different bands and produce a falsedivergent. Wideningtfixes it but enlarges the in-band blind spot everywhere.edge_marginis the surgical alternative, agreed in the corridor table as a relative fractionm(e.g.0.005= 0.5%), out of range (m <= 0,m > 0.1, or non-numeric) meaning off — never a wide default. When on, the set of core bands is expanded before the tolerance window is applied:core = { band } if band < len(edges) and value/unit >= edges[band] * (1 - m): core += { band + 1 } # near the upper edge from below if band >= 1 and value/unit <= edges[band-1] * (1 + m): core += { band - 1 } # near the lower edge from above commitments = sorted, de-duplicated { sha256(salt + ":" + b) : b in [c-t, c+t] for c in core, b >= 0 }The
(1 - m)/(1 + m)comparisons are exact decimal (cross-multiplied as integers, never a float). A value in the middle of a band is untouched, so the blind spot is widened only at the edges where the wobble is real. Withedge_marginabsent or off the set is exactly[band-t, band+t], so no existing commitment changes. Two honest banks straddling one edge now share a commitment and reconcile; a genuine multi-band divergence still shares nothing and staysdivergent.edge_marginis ratified in the corridor table, not sent on the wire. It is a property of the agreed table (alongside the edges and the optionalmax_ratioceiling), so the operator reads it from its own ratified registry via the verdict'scorridor_ref— never from a leg. The leg is unchanged: a near-edge hit simply adds one neighbouring commitment, so a leg carries2t+1commitments in the middle of a band and2t+2next to an edge. The operator uses the ratifiedmto read the match window honestly: it knows a near-edge margin adds at most one band, so it subtracts that one band before inferring the tolerance (both counts read ast), and it widens the widest reported price ratio by exactly(1 + m) / (1 - m)— because a value can reach a band fromedges[k-1]·(1 - m)toedges[k]·(1 + m). Rounded up: this number states the corridor's blind spot on a signed record and must never understate it. Without a ratifiedmthe window is computed exactly as before.closed(optional, ratified with the table likeedge_margin). A boolean or absent; any other value is an ambiguous table and the leg is unbandable (no commitments). Whentrue, the open outer bands are not part of the table: after the near-edge expansion, bands0andlen(edges)are removed fromcore. If no core band is left — the value is at or below the first edge, or above the last, and not within the margin of the edge it is beyond — the leg commits out-of-range markers instead:commitments = [ sha256(salt + ":out:" + role + ":" + k) for k = 0 .. 2t ] # role lowercased, in k orderOtherwise it commits
corewidened bytexactly as above. So a value inside the table commits what it commits on the open table, byte for byte, except that a near-edge value next to the table's boundary no longer also commits the open band beyond it. A marker can match no band commitment and no other role's marker, so an out-of-range leg can only readdivergent— which routes it to review. Two legs agreeing far outside the agreed table (a 250/unit invoice and a 25,000/unit declaration both in the top band) cannot clear. A marker set has2t+1members, the count of a mid-band leg, so the operator's tolerance inference (and the commitment-count rule of the boundary register) is unchanged, and the operator cannot tell an out-of-range leg from any other divergence. Thecorridor_idis the edges' own: closure, like the margin, is a table property read from the ratified registry.The closed window. On a closed table every clear is between two interior bands at most
2tapart, soreaches_open_bandisfalse, the record carriesclosed: true, and the widest bounded ratio is the maximum, over interior bandsb, ofedges[min(b + 2t, len(edges) - 1)] / edges[b - 1](times(1 + m)/(1 - m)under a margin). It gates the ceiling, so it is computed exactly — integer arithmetic over the decimal mantissas — and rounded up to two decimals:110/100is1.1, never the1.11a float ceiling prints, and1.504is1.51, never1.5. A closed table with one edge has no interior band: nothing clears under it. An open table's window is unchanged.The
corridor_commitmentis salted, so the operator can tell that two legs were banded against the same agreed table without learning which table it is. The counterparty, who holds the salt and the table, can open it at audit time.The raw value, the unit count, and the band itself never leave the institution.
5.0 The shared secret — a security parameter, not a configuration detail#
shipment_ref travels to the operator in the clear: the operator has to group the legs of one trade, and it has nothing else to group them by. The band space is also small — a corridor table has a handful of edges, so band ranges over roughly 5 to 30 values.
Those two facts together mean an operator holding a single leg can run this, with no cryptanalysis at all:
for each candidate shared_secret S:
salt = HMAC-SHA256(S, "mirror:" + shipment_ref)
for band b in 0 .. B:
if sha256(salt + ":" + b) is among the leg's commitments: the band is b
Cost is (candidate secrets) × (B + 1) hashes. The band loop is a rounding error. The entire distance between "the operator cannot read a band" and "the operator reads every band before lunch" is the guess cost of shared_secret.
Therefore, normatively:
shared_secretMUST carry at least 128 bits of entropy. An implementation MUST refuse to band below that floor rather than warn. The reference bander refuses.shared_secretMUST be produced by a cryptographically secure random generator. It MUST NOT be derived from, or equal to, any business artifact — letter-of-credit number, UETR, invoice reference, booking reference, or any concatenation of them with a corridor name or year. Those pass every statistical entropy test and represent a search space of a few million to anyone who knows the format, which every counterparty and every operator does.- An implementation SHOULD generate the secret itself and record its provenance, because requirement 2 is not checkable by measurement. Measurement is the backstop; generation is the control.
- The secret MUST NOT reach the operator by any route. An operator holding a per-shipment secret does not need to search at all — it derives the salt and opens every commitment directly. There is deliberately no operator-side endpoint anywhere in this protocol that accepts, assesses, escrows, or recovers a shared secret.
5.0.1 Establishing the secret — the channel is part of the threat model#
The whole operator-blindness property rests on the two counterparties, and only them, holding the shared secret. If that secret is intercepted in transit, the scheme is defeated before a commitment is ever computed — so how the secret reaches the counterparty is in scope, not an implementation detail.
For v1, the establishment channel is out of band and pre-existing: the two institutions already run a trusted channel to agree a corridor at all (the same channel that ratifies the corridor table and exchanges member key fingerprints at onboarding), and the generated secret is delivered over that channel. The reference bander's new-secret command writes it to an owner-only file for exactly this hand-off; it is never printed to a shared terminal and never emailed in the clear.
A stated limitation, and the intended v2: the secret is currently a symmetric value delivered over a trusted side channel, not derived. A later version SHOULD establish it by authenticated key agreement between the institutions' already-registered long-term keys — an X25519 ECDH with the shared value run through HKDF-SHA256 and bound to the corridor and a per-shipment identifier — so that no secret value ever transits any channel and compromise of one shipment's material does not touch another's. That is deliberately not claimed as built: shipping an unaudited key-agreement path would be a worse trade than stating plainly that today the channel is trusted and out of band.
5.1 Two dimensions, not one#
Banding price-per-unit alone conflates two independent frauds. A party that inflates quantity and deflates unit price by matching factors produces a price-per-unit band that matches perfectly while the trade is a structure; and quantity short-shipment is invisible to a price band entirely.
So quantity and unit price are banded and compared separately, each under its own domain-separated reference ("mirror-dim:<dimension>:<locator>") so each dimension gets its own salt and a band-2 quantity commitment can never collide with a band-2 price commitment.
The combination rule that makes it worth building: a dimension that agrees never masks one that disagrees. Any diverging dimension makes the whole reading divergent. The reading also judges the union of the policy's dimensions and the dimensions actually present, so a party cannot hide a divergence by simply not banding that dimension.
6. Time — matching windows#
The legs of a trade are not same-day. Without a time model, "undetermined" is meaningless and a clear is indefensible, because nothing establishes that the legs belong to one trade.
A window is [anchor + min_days, anchor + max_days], closed at both ends, running forward. Party-supplied per corridor; the defaults below are conventions, not law:
| Role | Window | Measured from |
|---|---|---|
invoice | 0–0 days | (the conventional anchor) |
shipment | 0–30 days | invoice |
customs | 0–30 days | shipment |
payment | 0–90 days | invoice |
Everything ambiguous is undetermined, never a pass: a leg out of window, an undated leg, a stale corridor table, legs banded against different table versions, or a leg dated after the supplied clock.
One distinction matters operationally more than any other in this section: a window that is still open with a leg missing is pending — the trade is in flight. A window that has closed with a leg missing is a finding. Conflating them either floods the queue with in-flight trades or silently drops the structure.
Nonsense window policy falls to the stricter default, never the looser one, and every substitution is recorded on the face of the record.
7. The verdict — three states, and no fourth#
reconciled | divergent | undetermined
- There is no score. There is no
0.73 likely laundering. A probability would make Mirror an accusation engine, and would put a number in front of an examiner that no one can defend. clearedis true only forreconciled.- Fail-closed runs the opposite way to a permissions system. Here the dangerous act is clearing a trade that should have been looked at, so ambiguity must never clear. A missing leg, an unbandable value, an unreadable commitment, or legs banded against two different corridors all yield
undetermined. You reach a clear only on positive evidence that the legs agree.
Every verdict — including a clear — is signed and canonical. The clear is the product: an examiner asks "why did you not escalate this?" and the answer is a portable file they can check themselves.
7.1 What a clear asserts — exactly#
A reconciled verdict asserts that the parties' committed band labels agree. It does not assert that each label is the band of its submitter's true value: that link is checked at opening (§8), and, where a leg is bound to an origination document (§7.4), against the bank's own signed document. Every record MUST carry an asserts block stating this, with label_equals_value_band: "not_proven_here". A verifier MUST reject a record whose asserts block does not restate the record's own fields.
The governed match ceiling. A clear is only as strong as the window its legs agreed within. When the named corridor ratified a max_ratio, a comparison that agreed stays reconciled only if the match window is bounded (a closed table, §5) and its widest ratio is at most the ceiling. Otherwise — the window reaches an open band, exceeds the ceiling, or cannot be read at all (a corridor the operator holds no edges for) — the verdict is undetermined with undetermined_reason: "match_ceiling" and the ratified max_ratio on the record. A ceiling is never skipped into a clear. Like the independence gate (§7.2), the hold moves reconciled to undetermined only, so an offline verifier that recomputes the join and finds it reconciled MUST accept a record held for match_ceiling, and MUST NOT accept the reverse.
7.2 The independence gate#
A comparison that comes out reconciled stays reconciled only if at least one anchor agreed. An anchor is evidence from a party with no stake in the price:
- a leg signed by a witness key the members ratified by quorum under an independent class (customs, terminal, port, weighbridge, carrier, warehouse, inspectorate, surveyor, assayer, certifier), whose organisation is not a stakeholder's; or
- evidence such a key signed: a terminal's commitment-bulletin entry proven by inclusion (§7.5), or a signed measurement.
The anchor MUST be competent on an axis the reading judged, and that axis MUST have agreed. A class lives on the ratified key, never on what a leg says about itself. A relayed document (a stakeholder's segregated desk, class relay) anchors only on a corridor that ratified anchor.accept_relayed. Otherwise the verdict is undetermined, with undetermined_reason: "insufficient_independence". The gate moves reconciled to undetermined only. It never mints a clear, never softens a divergence and never produces a fourth state.
7.3 Nested resolution#
Legs MAY also commit at fine resolution under the same salt: sha256(salt ":fine:" band) against a fine table that contains every coarse edge. Coarse agreement with fine disagreement is divergent on axis resolution. Once any leg commits finely, every leg MUST, under one agreed (or ratified) fine window; otherwise the reading is undetermined. Every ratified table has a signed geometry certificate stating the widest mispricing that still reconciles, as a ratio (public) and an amount per unit (members). A corridor MAY be ratified as a citation of a published figure, with a closed field set and no price. Each bank derives the nested table party-side.
7.4 Label integrity#
A corridor MAY ratify its bander builds (a leg from any other build is undetermined) and MAY require every label to be bound to an origination document. That is a commitment to the document's figures, signed at booking by a key of class origin held by the bank's trade system and never by the bander. A leg MAY commit its pricing basis (currency, unit, Incoterm, place); a basis mismatch is undetermined. The leg signature MUST cover every block it carries: fine commitments, physical, dimensions, origin, basis, bander pin.
7.5 Physical witnesses, paths and services#
A terminal publishes an RFC 6962 commitment bulletin of H(locator, mass commitment, table commitment) entries, salted by a receipt secret that travels with the cargo's papers, and signs the head with its ratified witness key. The operator verifies inclusion and compares the terminal's mass commitment with each stakeholder's declared-mass set; it sees no kilogram and no container number. A path certificate counts one cargo's hops by opaque locator (states, declared free-zone hops, hops without an independent or customs-class anchor, and drift from mirror-chain), proves each hop into the verdict log, and is released to a member only when k institutions other than the requester stand behind it. Services use an obligation key derived from the signed statement of work, band hours and rate as two dimensions, and are witnessed only by a ratified certifier.
7.6 A cited benchmark#
A corridor MAY require a leg with the role benchmark: a public market figure (a settlement price, a published assessment) that one party cites, obtains itself and commits under the shipment's own salt and the corridor's table exactly as any leg, with unit quantity 1 and a tolerance equal to the market window the parties agreed (2w+1 commitments, w at most 6). It is compared pairwise with every other required role like any leg, so requiring it can only add a divergent pair; it can never remove one. Three rules keep it from changing what a clear asserts about the trade legs:
- The operator MUST NOT count a
benchmarkleg when it infers the tolerance for the match window (§5): the window describes the trade legs. A record whose required roles includebenchmarkstates the benchmark's own window in itsreason. - A
benchmarkleg takes no part in nested resolution (§7.3). It is compared at the corridor's resolution only, by the operator and by every verifier that recomputes the join. - It is outside a corridor's origination requirement (§7.4): it cites a public document, not a booking. A pricing basis it commits MUST match the trade legs', or the reading is
undetermined.
A benchmark is not a witness and never satisfies the independence gate (§7.2). Mirror supplies, fetches and chooses no figure: the citation and the figure stay with the party that committed the leg, and are checked at opening against the public document.
8. Signing and the Clear File#
ES256 (ECDSA P-256) over the record as canonical JSON: recursively sorted keys.
The signature covers canonical text, never an object. Handing an object to a signer stringifies it as
"[object Object]"— a constant — so every record would carry the same signature and any signature would verify. The implementation canonicalises internally so a caller cannot make that mistake.A record must contain no undefined-valued keys.
canonical()signs them asnullwhileJSON.stringifydrops them, so a genuine record would fail its own signature after one wire round trip. This is test-enforced on every emitted record.Key id, derived from the public half only, so signer and verifier reach the same id independently:
kid = "mir-" + sha256(canonical({crv, kty, x, y})).slice(0, 16)Verification selects the key by
proof.kid, neverkeys[0]— a JWKS carries several keys through rotation.
8.1 Input attestation — signing the legs, not only the verdict#
An operator signature proves one thing: that this operator computed this verdict. It says nothing about whether the inputs were real. Without input attestation, a compromised operator can copy the invoice bank's commitments into the customs slot, obtain a mathematically perfect agreement, and sign a clear that is cryptographically flawless and factually invented — and every offline verifier in the world would accept it.
So each member signs its own leg, with the same ratified member key the registry already publishes and member login already uses. No second key hierarchy and no second directory.
leg_envelope = { v: 1, type: "mirror.leg-commitment/v1",
shipment_ref, role, commitments (sorted), corridor_commitment, kid }
attest = { alg: "ES256", kid, sig = ES256(canonical(leg_envelope)) }
Every binding in that envelope is load-bearing: shipment_ref stops a leg being replayed onto another trade, role stops it being pasted into another slot, commitments stops any alteration, corridor_commitment stops re-judging under a different table, and kid puts the author inside the signed bytes. The type tag is domain separation — a leg attestation can never be replayed as a governance vote, a login assertion, or a cosignature.
The operator verifies each attestation against the active ratified member directory, re-checking membership on every request, and embeds the attestations in the verdict. A supervisor holding the verdict and the published keys then re-checks every input offline, trusting the operator for nothing.
Normatively:
- A leg whose signature does not verify, whose
kidis unknown, or whosekidis not an active ratified member MUST make the shipmentundetermined. Unverifiable is treated as bad, never as absent — otherwise corrupting a leg would be a way to delete it. - Equivocation — one member signing two different commitment sets for the same role on the same shipment — MUST make the shipment
undeterminedand MUST be attributed to that member. - A leg carrying no attestation MAY still reconcile, so a rail can migrate without breaking, but the verdict MUST state that its inputs were unauthenticated. An implementation MUST offer a strict mode in which unsigned legs are
undetermined, and the verdict MUST record which regime it was judged under. - Offline verification returns exactly three outcomes:
AUTHENTIC,TAMPERED, orUNAUTHENTICATED_INPUTS. "Probably fine" is not one of them.
What this does not fix: signatures defeat forgery, not censorship. An operator can still withhold a leg and report it missing, and no signature scheme can compel delivery. The member's own signed copy of that leg is the counter-evidence — the verdict says the leg was absent, the member holds a signed leg for that exact shipment and role, and both cannot be true. That is a dispute with proof rather than an argument, and §11's dispute mechanism is where it goes.
The Clear File is the artifact an examiner receives: a self-contained HTML file that re-verifies every signature in the browser, with the network unplugged, plus the canonical JSON for the archive. It has no external references of any kind, and is served under a Content-Security-Policy that forbids it from making any network request at all — so "it cannot call home" is enforced by the browser, not promised by us.
8.2 Canonical forms and derivations (normative)#
Everything an independent implementation must reproduce byte for byte. Each rule is pinned by named vectors in /conformance-vectors.json, and conformance/reference-impl.js reproduces them from this text without importing the kernels. Where this text and a vector disagree, that is a defect to report.
Text and canonical JSON
canon(v):- an array is
[+ its elements'canon, comma-joined, in order +]; - an object is
{+JSON(key):canon(value), comma-joined, keys sorted by UTF-16 code unit +}; - anything else is
JSON(v), whereundefinedbecomesnull. JSONis ECMAScriptJSON.stringify: shortest round-trip numbers, standard string escapes, no whitespace.
- an array is
- Signed string values are ASCII. A record carrying another character is a defect of the emitter, not a variant to accept.
lp(x)= the decimal length ofxin UTF-16 code units, then":", thenx. Every multi-part hash below length-prefixes its parts, so no two part-lists collide.H(x)= lowercase hex SHA-256 of the UTF-8 bytes ofx.
Exact decimal banding
- A decimal is its mantissa and base-10 scale, read from its shortest string. Exponent notation, non-finite and non-positive values do not band.
value / unit_qty > edgeiffv·10^(s_e+s_q) > e·q·10^(s_v), in integers.band= the number of ascending edges passed, stopping at the first that fails.- There is no floating-point division anywhere on the path.
Commitments
| Name | Formula |
|---|---|
| salt | HMAC-SHA256(secret, "mirror:" + shipment_ref), hex |
| band commitment | H(salt + ":" + band) for each band from max(0, band−t) to band+t, listed in ascending band order (plus the ratified near-edge extension when a corridor sets edge_margin) |
| out-of-range marker (§5, closed tables) | H(salt + ":out:" + role + ":" + k) for k = 0 … 2t, in k order, with role trimmed and lowercased — committed instead of bands when a closed table leaves no interior core band |
| corridor id | "c:" + H(edges sorted numerically, each as its shortest decimal string, comma-joined) |
| corridor commitment | H(salt + "|c|" + corridor_id) |
| fine commitment (§7.3) | H(salt + ":fine:" + band_fine) over the fine window, listed sorted as strings |
| fine table | H(salt + "|f|" + corridor_id(fine_edges)) |
| pricing basis (§7.4) | H(salt + "|b|" + canon({currency, unit, incoterm, place})); currency uppercase ISO 4217, unit lowercase, Incoterm uppercase from the 2020 set, place or null |
| origin (§7.4) | "o:" + H(lp(doc_salt) + lp("mirror:origin:v1") + lp(canon({value, unit_qty, currency, basis, terms_hash}))); value and unit_qty are decimal strings, missing optional fields are null |
Physical witness (§7.5)
cargo_key= the ISO 6346 container number (check digit verified) +"|"+ the UN/LOCODE +"|"+ the load dateYYYY-MM-DD.derive(label)=H(lp(receipt_secret) + lp("mirror:bulletin:" + label + ":v1") + lp(cargo_key)).- locator =
"cl:" + derive("locator"); salt ="ps:" + derive("salt"). - mass commitment =
H(lp(salt) + lp("mirror:bulletin:mass") + lp(band_kg)), whereband_kg=bandOf(kg, 1, kg_edges). A declared set covers the tolerance window and is listed sorted as strings. - quality commitment = the same with
"mirror:bulletin:quality". - table commitment =
H(lp(salt) + lp("mirror:bulletin:table") + lp(corridor_id(edges))).
Keys and signatures
kid = "mir-" + H(canon({crv, kty, x, y})).slice(0, 16).- An ES256 signature is
r‖s, base64url, with low-S (s ≤ n/2). A high-S twin is the same signing, never a second record.
Webhooks
- subscription id =
"wh_" + H("mirror:webhook-subscription:v1|" + canon({kid, url, events})).slice(0, 24). - delivery id =
"whd_" + H("mirror:webhook-delivery:v1|" + subscription_id + "|" + H(canon(judgement(record)))).slice(0, 32). judgement(record)= the record with every property namedsigorsignatureremoved, at any depth. A failover resubmission re-signs the same commitments, so it is the same judgement: one delivery, and one verdict in every count.
Field names at the wall
A key is compared after this normalisation:
- NFKC;
- invisible characters removed;
- listed confusables folded;
- camelCase split to snake_case;
- lowercased;
- runs of non-alphanumerics collapsed to one underscore;
- edge underscores trimmed.
Unit Price, unitPrice and unit_price are one field.
9. What each party can learn — stated exactly#
This section is the one a DPO should read twice. It is deliberately unflattering.
The operator learns: that two legs did or did not land in the same band; that they were banded against the same corridor (not which); opaque locator, entity and node commitments; counts; timestamps. It cannot invert a commitment: it lacks the salt, which is derived from a secret it never holds.
A counterparty learns more than the operator, and this is the real boundary. The two parties on a trade share the per-shipment secret. A price band is low-cardinality — typically 12–24 buckets per product — so a counterparty can brute-force the other's band by hashing every candidate. That is not a flaw to be patched; it follows from the threat model. Mirror's threat model is "do not pool raw trade data at a central operator, and do not expose it to unrelated members" — not "hide the two legs from each other." The two banks on a trade are, by construction, already jointly examining one shipment.
This is not zero-knowledge, not secure multi-party computation, and not homomorphic encryption. Anyone claiming those without building them would be lying. Where the counterparty-visibility gap actually matters to a member, Mirror ships real private set intersection (Diffie–Hellman PSI on P-256) as a separate, party-to-party step which closes exactly that brute-force — and which is honest-but-curious secure only, and is documented as such including the reflection attack it cannot detect.
Graph topology leaks. Flow edges and chain hops reveal structure to the operator even though the nodes are opaque. The mitigation is a k-anonymity release gate that withholds an edge until k distinct members have contributed to its window — which delays detection, a real cost, stated rather than hidden.
What Mirror can never do: compare either party to the physical world. Two colluding parties who agree a consistent lie reconcile perfectly. Mirror compares parties to each other.
10. Attribution, legal basis, and the SAR wall#
- A trigger may name the member that raised it, and that name counts only if a signature proves it. Otherwise any holder of the pilot token could post the same entity twice under two member names and manufacture "corroborated by two institutions" out of one bank's hunch. Unsigned triggers are still accepted and still score; they simply stay unattributed and can never become a second source.
- A cross-institution signal is shared under a named legal basis (FinCEN 314(b), UK ECCTA ss.188–189, EU AMLA, or explicit consent) or not at all. The basis is bound into the signature, so a member is cryptographically on record for the basis it asserted and cannot later disclaim it.
- The SAR wall. The rail is structurally incapable of holding a suspicious-activity filing decision. Any field naming one is refused at the door on every endpoint, at any nesting depth. Two reasons, both load-bearing: disclosing that a SAR was filed is a tipping-off offence in most jurisdictions; and if a Mirror record could hold a filing decision, the archive becomes a different legal object and members would rationally stop submitting the awkward trades. The joint-SAR conversation happens off-rail, referring to a Clear File id.
11. Supervision without disclosure#
Verdicts are appended to an RFC 6962-shaped transparency log. A supervisor holds signed tree heads and demands consistency proofs, which prove a newer head append-only-extends an older one: nothing inserted, reordered, or removed. It never sees a value.
If the operator ever signs two different roots at the same tree size, it has signed two contradictory histories — and because both are signed, that is non-repudiable proof of operator misbehaviour. This is why multiple independent supervisors comparing heads is what makes the guarantee real.
A supervisor shown nothing reports undetermined, never "consistent". A supervisor that says "all good" because it was shown no evidence is worse than no supervisor at all.
12. What this protocol does not do#
- It does not prove a trade is legitimate. A clear is a defensible record of why a trade was not escalated, never a guarantee of innocence.
- It does not prove a divergence is laundering. A divergence is a reason to look.
- It does not prove, at reconciliation, that a committed label is the band of its submitter's true value. That is checked at opening, against the bank's own origination document where one is bound (§7.4). A proof of it at reconciliation would need a zero-knowledge circuit, and none is part of this protocol.
- It does not prove that parties who all have a stake in the price told the truth. That is why a clear needs an independent anchor (§7.2), and why an anchor confirms only the axis it is competent on.
- It does not prove completeness of participation. A trade never submitted to the rail is invisible to it. The completeness digest proves a member disclosed the whole set it committed to — not that it committed everything it did.
- It does not verify that corridor tables are correct. Garbage corridors give garbage bands, and no part of the mechanism can tell a wrong price table from a right one.
- It does not measure detection rate or false-positive rate. No such measurement is performed here and none is implied.
13. Conformance#
An independent implementation earns a signed conformance certificate by reproducing generated test vectors byte-for-byte. There is no partial pass. The challenge is stateless — the seed re-derives the reference — so the operator keeps no memory of what it issued.
This exists so the commitment format can become something the industry emits even when it is not anyone's customer, and so a member can verify a vendor's claim to speak the protocol rather than trusting a logo.
Mirror is a working name, after central-bank mirror statistics — the practice of comparing what one country says it exported against what its partner says it imported.