# ScoreCompute Receipt Standard v1

Open, dependency-free description of the signed calculation receipt issued by
ScoreCompute (`scorecompute.verify.receipt.v1`) and of its public keyset
(`scorecompute.verify.keys.v1`). This document describes the **implemented**
format; the reference verifiers provide executable conformance checks.

Status: v1, 2026-10-05. Version string changes only with an explicit, documented
revision. Nothing here is a scientific certification: a valid signature binds
this server to this calculation, nothing more.

## 1. Envelope

A receipt is a JSON object:

```json
{
  "payload": { "...": "parsed copy of payload_json" },
  "payload_json": "exact UTF-8 bytes that were hashed and signed",
  "payload_sha256": "64 lowercase hex characters",
  "signature": {
    "alg": "Ed25519",
    "key_id": "sc-verify-20261002",
    "public_key_url": "/.well-known/scorecompute-verify-key.json",
    "value_b64": "base64 Ed25519 signature (88 characters, padded)"
  },
  "signed_at": "2026-10-05T00:05:10.041Z"
}
```

Hard limits: receipt ≤ 64 KiB; canonical payload ≤ 128 KiB; canonicalizer
refuses more than 20 000 nodes or depth 32; non-finite numbers and the reserved
keys `__proto__`, `constructor`, `prototype` are rejected.

## 2. Payload fields

| Field | Meaning |
|---|---|
| `v` | Always `scorecompute.verify.receipt.v1`. |
| `tool` | Always `verify_claim` for wedge receipts. |
| `domain` | `numbers`, `ephemeris`, `shadow`, `statistics` or `route`. |
| `claim` | Structured claim actually evaluated (kind + parameters). |
| `request` | Normalized request the server executed. |
| `route` | `{ engine, native_route }` actually called. |
| `engine_response` | Native engine output when it fits the size budget (`engine_response_included: true`). |
| `engine_response_digest` | SHA-256 of the canonical engine response (top-level `elapsed_ms` removed) — always present. |
| `verdict` | Unified verdict: `compatible`, `contradicted`, `inconclusive` or `out_of_validation_scope`. |
| `validation_scope` | Engine-specific description of what was checked (tolerances, model, limitations). |
| `limits` | Explicit statements of what the receipt does **not** establish. |
| `server` | `{ name, release }` of the issuing service. |
| `signed_at` | ISO-8601 UTC; also copied on the envelope. |
| `signing` | `{ alg, key_id, public_key_url }` — must match `signature`. |

## 3. Canonicalization

`payload_json` must equal the canonical serialization of the parsed payload:

- objects: the service recursively inserts keys in ECMAScript UTF-16 lexical
  order, then uses `JSON.stringify`. Consequently array-index keys (`"0"`
  through `"4294967294"`, without leading zeroes) serialize first in numeric
  order; the remaining keys retain UTF-16 lexical order;
- arrays: order preserved;
- strings/booleans/null: ECMAScript JSON semantics, including escaped unpaired
  UTF-16 surrogates; numbers: finite IEEE-754 binary64 values and ECMAScript's
  shortest decimal representation (`-0` serializes as `0`);
- no whitespace; UTF-8; reserved keys rejected.

This implemented v1 profile is **not RFC 8785/JCS**: index-key ordering and
handling of unpaired surrogates differ. Integers outside binary64's exact
range are subject to binary64 rounding; encode exact large integers as strings
inside the scientific contract. A parsed `payload` copy may have a different
object insertion order, but its canonical representation must match exactly.

Verifiers must recompute the canonical form and compare byte-for-byte; a
receipt whose `payload_json` is not canonical is invalid even if the signature
would verify over the raw bytes.

## 4. Signing input

```
signing_input = ASCII( "scorecompute.verify.receipt.v1" + "\n" + payload_sha256 )
payload_sha256 = SHA-256( payload_json bytes )
signature = Ed25519_sign( signing_input )
```

The private key never leaves the server. The public key is distributed through
the keyset.

## 5. Keyset

```json
{
  "v": "scorecompute.verify.keys.v1",
  "current_key_id": "sc-verify-20261002",
  "keys": [
    {
      "key_id": "sc-verify-20261002",
      "alg": "Ed25519",
      "public_key_b64": "SPKI DER, base64",
      "encoding": "spki-der",
      "valid_from": "2026-10-01T22:25:42.836Z",
      "retired_at": null,
      "revoked": false
    }
  ]
}
```

Trust model: a verifier must obtain the keyset through a channel it trusts.
Fetching the URL named inside a receipt is trust-on-first-use, not a trust
anchor, and must be presented as such.

The reference programs are offline and never follow `public_key_url`. They
accept at most 33 uniquely identified keys, an existing `current_key_id`, an
explicit boolean `revoked`, and canonical UTC millisecond validity timestamps.
The reviewed profile uses four-digit years and rejects ambiguous date forms.
Retirement is exclusive: a receipt at `retired_at` is invalid. The signed time
is an issuer assertion, not an independently trusted timestamp.

Canonical Ed25519 SPKI DER and padded base64 are required. The corrected
reference verifiers reject noncanonical curve points and require a nonidentity
prime-order issuer key; the signature point must also be in that subgroup.
These checks reject weak-key forgeries if a malformed key is accidentally put
in the caller's trusted keyset. They do not let the receipt choose its own trust
anchor or imply that the service's real key was compromised.

## 6. Verification algorithm (normative)

1. Size-check the receipt (≤ 64 KiB) and parse it.
2. `payload_json` parses; `parsed.v == scorecompute.verify.receipt.v1`;
   `parsed.tool == verify_claim`; `parsed.signed_at == envelope.signed_at`.
3. `canonicalJson(parsed) == payload_json` (byte equality).
4. `SHA-256(payload_json) == payload_sha256`.
5. `parsed.signing` matches `signature.{alg,key_id,public_key_url}`.
6. The key exists in the trusted keyset; `revoked == false`;
   `valid_from <= signed_at`; if `retired_at` is set, `signed_at < retired_at`.
7. Ed25519 verify over the signing input (§4) with the SPKI DER public key.
8. If `engine_response_included` is true, recompute
   `engine_response_digest` from the included response after removing only its
   top-level `elapsed_ms`. Reject mismatches or incomplete response metadata.
   If false, the response must be null: the digest is signed but cannot be
   recomputed without the omitted result.
9. Only then display `verdict`, `validation_scope` and `limits` — never strip
   the limits.

Any failure: the receipt is invalid; never "probably valid".

## 7. Reference verifiers and vectors

- `node verify-receipt.mjs <receipt.json | link.json | -> <trusted-keyset.json> [--json]`
  — dependency-free, offline; exit 0 valid, 1 malformed, 2 verification failed.
- `python3 verify-receipt.py <receipt.json | link.json> <trusted-keyset.json>`
  — **independent implementation** (standard library only; Ed25519
  reimplemented from RFC 8032, no shared code with the Node verifier or the
  service); exit codes identical.
- `vectors/` — 7 historical conformance cases: valid receipt, valid link,
  tampered digest, non-canonical payload, tampered signature, unknown key,
  retired key. These are frozen provenance fixtures; do not regenerate them
  when adding tests. Their minimal signed payloads omit the native response
  metadata and demonstrate signature verification only. `generate-vectors.mjs`
  is their original generator (its test keys existed only in memory).

Evidence, 2026-10-05: all 7 vectors behave as declared with **both**
implementations; the Python verifier passes the two RFC 8032 §7.1 Ed25519
vectors and verifies a **fresh production receipt** (`2 + 2` against `4`,
key `sc-verify-20261002`) with the same result as the Node verifier. The
independent implementation also caught a real canonicalization edge the
spec-test vectors did not exercise: ECMAScript number formatting (`0.0` → `0`,
`1e-09` → `1e-9`, decimal expansion below 1e21) differs from plain Python
`json.dumps`. That initial correction was incomplete: the later review below
adds counterexamples and a bounded cross-runtime corpus. The original seven
fixtures remain unchanged; their success alone never established complete
canonicalization or cryptographic correctness.

### 5 October review correction

The reference scripts now reject weak/noncanonical keys, malformed key validity
metadata, noncanonical base64 and inconsistent included-response digests.
The Python implementation now enforces byte/node/depth limits, handles UTF-16
ordering and string escaping, distinguishes booleans from numeric payload
copies, and selects shortest binary64 decimal strings using exact rational
distances. Both accept reordered but equivalent parsed payload objects. The
CLI limits stdin as well as files, and refuses invalid UTF-8.

The output field `engine_response_digest` distinguishes `verified` (internal
digest consistency), `not_included` (result omitted) and `not_present` (legacy
minimal signature fixture). None is a scientific verdict. Current production
receipts carry all three response fields; the legacy vectors are retained as
signature-only examples, not examples of a complete calculation record.

Run the offline regressions:

```sh
node docs/receipt-standard/tests/test-verifiers.mjs
python3 -B docs/receipt-standard/tests/test-independent.py
```

They include the frozen vectors, a previously captured real production receipt,
new independently signed test receipts, malformed/forged cases, RFC 8032
signature anchors and RFC 8785 Appendix B number anchors, plus a deterministic
binary64 corpus. Tests use no secret production key and make no network calls.
The Python program is a readable reference implementation, not a hardened
general-purpose cryptographic library. This scoped review and the tests are
not an independent external cryptographic audit or a proof over all inputs.

Primary algorithm references: [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032)
and [RFC 8785 Appendix B](https://www.rfc-editor.org/rfc/rfc8785#appendix-B)
(number test vectors only; the receipt profile is not JCS).

## 8. What a valid receipt does not establish

- Physical truth, correct source data, units or claimant intent.
- Image authenticity, fraud, or the absence of measurement bias.
- An independent external audit or a scientific certification.
- The identity of the client or of the model that produced the claim.

A signature proves **origin and integrity** of one bounded calculation.

## 9. Adoption

Other issuers may implement the same envelope to make their receipts
verifiable by the same tools. Rules: keep `v` and `tool` meaning intact, publish
your keyset in this format, never truncate limits, and version explicitly.
Shareable receipt links are described in [LINK.md](LINK.md).
