# Open Evidence Rail (OER) — Specification v1.0.0-draft

**Status:** draft, implemented against a live CapchaCloud reference deployment (waves 1–4 of 8; see
"Implementation status" at the end of this document for exactly what is built and tested versus
planned).

**License:** this specification is released under CC0 1.0 (public domain dedication). Any party may
implement it — a canonicalizer, a format registry, a seal endpoint, an offline verifier — without
CapchaCloud's involvement, permission, or a business relationship. The reference implementation
(this repository, `trust-engine/src/oer-*.ts`, `trust-engine/sdk/oer*.js`) is provided as a working
example, not as the only legitimate implementation.

## 1. What this is, and what it is not

The Open Evidence Rail is a **format-agnostic sealing and verification protocol**. A participant
registers the shape of their own evidence (a JSON Schema plus a canonicalization rule), seals
payloads against that registered format, and receives a receipt that:

- **proves** the exact payload bytes, once canonicalized under the format's declared rule, were
  hashed and included in a tamper-evident, independently-anchored append-only chain at a specific
  time;
- **does not prove** that the payload's real-world content is true, that any action described by
  the payload actually occurred, that any party consented to anything, or that the payload complies
  with any law or regulation. A seal is evidence of *what was submitted and when*, not evidence of
  *what it means*.

Every receipt/bundle produced under this spec MUST carry a `does_not_prove` field stating this
limitation in the context of that specific record. Implementations that omit it are non-conformant.

## 2. Canonicalization

Three canonicalization modes are defined. A registered format declares exactly one.

### 2.1 `jcs` (default)

RFC 8785 JSON Canonicalization Scheme, **with two documented CapchaCloud-specific extensions layered
on top of the base spec**, both applied for the same reason: two payloads a reasonable person would
call "the same" must hash identically, and a payload whose meaning is genuinely ambiguous must never
be silently guessed at.

1. **Unicode normalization.** Every JSON string (both object keys and string values) is normalized
   to NFC (Unicode Normalization Form C) before serialization. Plain RFC 8785 does not require this
   — it serializes exactly what a JSON parser handed it — but two payloads differing only in
   Unicode composition form (e.g. precomposed `é` U+00E9 vs. `e` + combining acute accent U+0065
   U+0301) are the same text to any reasonable definition, and this extension makes them hash the
   same.
2. **Fail-closed number handling.** A JSON number literal is REJECTED — not canonicalized — if
   converting it to an IEEE-754 double and back via the standard ECMAScript `Number::toString`
   algorithm does not reproduce the *exact* original decimal value. This catches large integers
   (e.g. external IDs beyond 2^53) and over-precise decimals that would otherwise be silently
   rounded. Implementations MUST perform this exact-value round-trip check, not merely check
   `Number.isSafeInteger` (which under-rejects fractional values and over-rejects some safe large
   integers expressed with trailing zeros).

Additional fail-closed rules (both inherited from strict JSON semantics and CapchaCloud-specific):

- **Duplicate object keys are rejected**, compared after NFC normalization (so `"café"` and
  `"café"` as two keys of the same object is a duplicate-key rejection, not two distinct
  keys).
- **Unpaired UTF-16 surrogates are rejected.** A string containing a lone high or low surrogate
  cannot be losslessly UTF-8 encoded (naive `TextEncoder` implementations silently substitute
  U+FFFD, which is exactly the kind of silent, information-destroying behavior this spec forbids).
- **`null` and an absent key are distinct.** `{"a":null}` and `{}` must not canonicalize to the same
  hash.
- **Arrays are order-significant.** Element order is never reordered or treated as insignificant.
- Canonical output has **zero insignificant whitespace** between tokens.
- Object keys are sorted by simple UTF-16 code-unit comparison (the same rule ECMAScript's default
  string `<` uses — this is what RFC 8785 itself specifies).

An implementation MUST reject, with a precise machine-readable reason and (where applicable) an
RFC 6901 JSON Pointer to the offending location, rather than "best-effort" hashing a payload that
fails any of the above. The canonical set of rejection reasons the reference implementation uses:
`empty_input`, `invalid_utf8`, `malformed_json`, `trailing_content`, `duplicate_object_key`,
`number_precision_loss`, `number_out_of_range`, `lone_utf16_surrogate`, `max_depth_exceeded`,
`max_size_exceeded`. Third-party implementations are not required to use these exact string names,
but MUST distinguish these failure classes from each other and from success.

### 2.2 `raw-bytes`

The payload is hashed exactly as given — no parsing, no interpretation, no normalization. For
formats that are not JSON, or that intentionally want byte-exact framing (e.g. an existing binary or
XML-based standard). Whitespace is significant; there is no equivalence class beyond byte identity.

### 2.3 `cbor-deterministic`

RFC 8949 §4.2 "core deterministic encoding" applied to the same JSON value model `jcs` uses, with
one documented, deliberate divergence from RFC 8949's *preferred* serialization: non-integer numbers
are always encoded as 8-byte IEEE-754 doubles (major type 7, additional info 27) rather than the
theoretical minimum width (half/single/double chosen per value). This is still fully deterministic —
a given value always encodes identically — it is simply not bit-width-minimal. Implementations MAY
choose to implement true width-minimization; a bundle sealed under either choice remains internally
self-consistent as long as one canonicalizer instance is used consistently for a given format
version (mixing the two within one deployment would break determinism and MUST NOT be done).

Whole integers that fit in a 64-bit signed/unsigned range are encoded exactly via CBOR's native
integer major types (0 and 1) regardless of float64 representability — this is the practical
advantage of choosing `cbor-deterministic` over `jcs` for a format whose payloads carry large
integer identifiers. Map keys are ordered by bytewise lexicographic comparison of their own encoded
bytes, per RFC 8949 §4.2.1. The same duplicate-key / lone-surrogate rejection rules from §2.1 apply
(they are properties of the JSON *input*, not of the target encoding).

## 3. Format registry

A **format** is `(format_id, version)` plus:

| Field | Type | Notes |
|---|---|---|
| `format_id` | string | lowercase kebab-case, e.g. `gordian-envelope`. First-come, permanent, owned by whichever tenant registers its first version. |
| `version` | string | strict semver (`MAJOR.MINOR.PATCH[-prerelease]`). |
| `owner_tenant_id` | string | the registrant of the format_id's first version. Only this tenant may publish subsequent versions. |
| `human_name`, `description`, `homepage` | string | descriptive only. |
| `json_schema` | object | a JSON Schema (see §3.1 for the exact supported subset) payloads sealed under this format@version must satisfy. |
| `canonicalization_mode` | enum | one of §2's three modes. |
| `field_map` | object | maps a fixed set of rail concepts (`subject`, `parties`, `jurisdiction`, `amount`, `action_type`, `timestamp`) to RFC 6901 JSON Pointers into the payload. All optional; a format may map none, some, or all. |
| `content_hash` | string | SHA-256 hex of the JCS canonicalization (§2.1) of the object `{format_id, version, owner_tenant_id, human_name, description, homepage, json_schema, canonicalization_mode, field_map}`. |

**Immutability:** once `(format_id, version)` is registered, it is never updated or deleted — not
even by its owner. Publishing a new version is a new, independent row; it never mutates an existing
one. A receipt citing `format_id@version` plus `content_hash` lets any third party prove — by
re-fetching the registration and recomputing its content hash — that the format's rules were exactly
what they were at seal time, forever.

**Ownership:** `format_id` is first-come and permanent. A non-owner attempting to publish a new
version under an existing `format_id` MUST be rejected (recommended: HTTP 403). Sealing a payload
through an already-registered format does **not** require ownership — any party may seal against any
registered format, which is what makes this a shared rail rather than N private silos.

### 3.1 JSON Schema subset

Implementations are not required to support the full JSON Schema 2020-12 specification (few
dependency-free implementations do). The reference implementation supports: `type`, `required`,
`properties`, `additionalProperties`, `items` (single-schema form only — tuple-form `items` as an
array is NOT supported), `enum`, `const`, `minimum`/`maximum`/`exclusiveMinimum`/`exclusiveMaximum`,
`minLength`/`maxLength`/`pattern`, `minItems`/`maxItems`/`uniqueItems`, `minProperties`/
`maxProperties`, `allOf`/`anyOf`/`oneOf`/`not`, and `format` (advisory only, not a hard rejection
condition for non-conformant implementations). Metadata keywords (`$schema`, `$id`, `title`,
`description`, `examples`, `default`, `$comment`) are always accepted and ignored for validation
purposes.

**Registration MUST be rejected if the schema uses any validation keyword outside this list**
(`$ref`, `patternProperties`, tuple-form `items`, `if`/`then`/`else`, `dependentSchemas`,
`unevaluatedProperties`, etc.) — silently ignoring an unsupported keyword would under-validate every
payload sealed against that format, which is worse than refusing the registration outright.

## 4. Sealing

`POST /api/v1/rail/seal?format_id=<id>&format_version=<semver, optional — omit for latest>`
(tenant-authenticated). The **request body is the payload's raw bytes/text, exactly as the format
expects** — not a JSON envelope wrapping a `payload` field. This is deliberate: canonicalization's
duplicate-key and number-precision checks (§2.1) require the payload's ORIGINAL text; nesting it
inside a larger JSON document that a server-side `JSON.parse` has already touched would destroy that
information before the canonicalizer ever saw it.

Processing order: (1) canonicalize the raw body per the format's declared mode — reject per §2 on
any ambiguity; (2) for `jcs`/`cbor-deterministic` modes, validate the resulting value against the
format's `json_schema` — reject with the failing JSON Pointer(s) on any violation; (3) project the
`field_map` pointers onto the payload; (4) seal the canonical hash into an append-only,
tamper-evident, independently-witnessed chain (implementation detail — the reference implementation
reuses its existing evidence chain rather than building a parallel one; see §6). A successful seal
returns a receipt citing `seal_id`, `format.{id, version, content_hash}`,
`canonicalization.{mode, payload_hash}`, `projected_fields`, and a `chain_hash`.

## 5. Evidence bundle & offline verification

`GET /api/v1/rail/seal/{seal_id}/bundle` (public, no authentication) returns a **signed,
independently-verifiable bundle**:

```json
{
  "bundle_schema_version": "capcha-evidence-bundle-1.0.0",
  "kind": "oer_seal",
  "id": "<seal_id>",
  "factset": { "...": "the signed statement of facts, including chain_inclusion" },
  "factset_sha256": "<sha256 hex of JSON.stringify(factset) exactly as returned>",
  "signature": "<Ed25519 signature (hex) over the raw bytes of the hex-decoded factset_sha256, or null if unsigned>",
  "signer_id": "<string, or null>",
  "signed": true,
  "pubkey_url": "<URL serving the operator's current Ed25519 public key(s)>",
  "how_to_verify": "<human-readable instructions>"
}
```

**Offline verification, minimum bar (any implementation MUST support this much):**

1. Recompute SHA-256 of `JSON.stringify(factset)` exactly as returned in the `factset` field. It
   MUST equal `factset_sha256`.
2. If `signed:true`, fetch (or otherwise obtain) the operator's public key from `pubkey_url`, and
   verify the Ed25519 signature (hex-decoded) against the raw bytes of the hex-decoded
   `factset_sha256`.
3. If `signed:false`, the bundle's hash integrity is still meaningful (step 1) but there is no
   cryptographic binding to a specific operator key — report this honestly, never treat an unsigned
   bundle as equivalently strong to a signed one.

**Full verification** (recommended, not required for a minimal implementation) additionally walks
`factset.chain_inclusion`: recomputes the Merkle path from the record's leaf hash to the cited
`merkle_root`, verifies the block header's own signature, and checks any external anchor witnesses
(RFC 3161 timestamp tokens, OpenTimestamps/Bitcoin commitments) cited in the block. The reference
implementation's full verifier is `scripts/verify-evidence-bundle.mjs` / `public/verify-capchachain.mjs`
in this repository — deliberately generic over `kind`, so a new format's bundles verify unmodified as
long as they match the shape above (proven in this repo by
`test/oer-seal-bundle-verifier.spec.ts`, which runs the actual unmodified verifier against a
`kind:"oer_seal"` bundle as a real subprocess).

## 6. Cross-format references

Any receipt may reference any other receipt by id, across formats and tenants, with no
coordination or grant required — up to 10 references per seal, submitted as
`?references=id1,id2` on `POST /rail/seal`. The reference id list is written into the sealing
tenant's own envelope (so it is content-hashed and tamper-evident, the same as every other
envelope field). `GET .../verify` and `.../bundle` walk the reference graph (bounded to 3 hops and
25 total nodes across the whole walk, cycle-safe) and report, per referenced seal, ONLY its format
identity (`format_id`/`version`/`content_hash`), a re-derived vault `integrity` status
(`"ok"`/`"tampered"`/`"missing"` — recomputed by re-fetching the referenced seal's content-addressed
vault object and re-hashing it, not trusted from the stored value), and its chain-inclusion status.
`projected_fields` and anything payload-derived is NEVER included for a referenced seal — only the
citing seal's own record carries its own payload projection.

This is the ONLY visibility policy shipped in this reference implementation: reference existence
plus that public projection is always disclosed to anyone who can already call the public
verify/bundle endpoints. A per-tenant opt-out ("nobody may cite my receipts") is **not
implemented** — same class of disclosed gap as `evaluate: true` in §4.

## 7. Conformance levels

- **L1** — seals a payload through a registered format and the resulting bundle verifies offline per
  §5's minimum bar.
- **L2** — L1, plus: canonicalization is proven deterministic over a randomized/curated equivalence
  corpus (shuffled key order, varied whitespace, value-preserving number reformatting, NFC/NFD
  Unicode pairs all hash identically; structurally distinct payloads never collide).
- **L3** — L2, plus: cross-format references (§6) verify correctly. Not attainable until §6 is
  implemented by the party being conformance-tested.

A conformance suite that cannot fail against a deliberately broken implementation is worthless. The
reference conformance suite (`sdk/oer-conformance.js`) is required to — and, in this repository's own
test suite, demonstrably does — FAIL against adapters with non-deterministic canonicalization, silent
payload mutation, and incorrect format content-hash reporting.

## 8. Implementation status (this repository, as of this revision)

Built and tested at the code+test level (see the repository's CURRENT-TASK.md for the exact probe
evidence; treat anything here as UNPROVEN in production until a live post-deploy probe confirms
it — code+tests passing is not the same claim as a production probe):

- §2 canonicalization (all three modes) — Wave 1.
- §3 format registry, including the immutability/ownership/JSON-Schema-subset rules — Wave 2.
- §4 sealing endpoint, reusing the existing evidence chain — Wave 3.
- §5's minimum offline-verification bar, both as a portable SDK method and proven compatible with
  the existing full verifier script — Wave 3/4.
- The SDK (`sdk/oer.js`) and conformance suite (`sdk/oer-conformance.js`), including the required
  "must fail against a broken adapter" proof — Wave 4.
- §6 cross-format references, including the bounded/cycle-safe graph walk and the vault-integrity
  re-derivation — Wave 5.
- All three Wave 6 reference adapters: `sdk/adapters/plain-json.js` (zero-friction on-ramp),
  `sdk/adapters/gordian-envelope.js` (aligned with Wolf McNally / Blockchain Commons's
  `draft-mcnally-envelope` at the subject/assertion semantic-model level — see that file's header
  for the honestly-documented CBOR/digest-tree/elision divergences), `sdk/adapters/iso20022.js`
  (re-expresses this repo's existing exact-bytes ISO 20022 sealing philosophy as a registered
  raw-bytes format).
- §7 conformance levels L1/L2/L3 all reachable (L3 is opt-in via
  `runConformance(adapter, { crossFormatReferences: true })` — an adapter that never claimed
  reference support is neither silently passed nor silently failed for it).
- The public surfaces: `/rail` (landing), `/rail/formats` (live directory), `/rail/setup`
  (self-serve invite-code claim -> tenant -> format -> seal -> verify, zero human contact), and the
  SDK served publicly at `/sdk/oer.js` + `/sdk/adapters/*.js` — Wave 7.
- Wave 8 guardrails: a per-tenant format-registration rate limit (previously unlimited), a
  documented (and now-verified) real payload cap of 256 KiB — not the previously-documented 1 MiB —
  for every canonicalization mode, a per-code seal rate limit for invite-claimed tenants sized
  below the default tenant rate limit, and a documented takedown path for a format registered in
  bad faith (registrations stay immutable; a takedown blocks new seals and is surfaced publicly,
  never retroactively invalidating already-sealed evidence).

**Not built / disclosed gaps, still current:** `evaluate: true` (oracle-gated sealing) returns an
honest "not implemented" response rather than a fabricated verdict. A per-tenant reference
visibility opt-out (§6) is not implemented. Wave 8's cost-per-1,000-seals and
canonicalization-added-latency figures are estimates (operation-count-based and a local Node
micro-benchmark respectively), not live production measurements — no production deploy/measurement
was performed by the pass that built Waves 5/6-remainder/7/8.
