Seal Verification Specification
Normative and third-party-implementable: this defines how to verify a QRS reproducibility seal without any QRS code or account. The reference implementation is the MIT-licensed qrs-replay CLI. The signing key is published, unauthenticated, at /trust/seal-key/v1.pem.
Rendered verbatim from spec/seal-verification/v1.md (Spec version seal-verify/1).
Verify a seal right now — drop or paste a seal envelope and check its signature and lineage chain in your browser (Steps 2–3), no account needed.
Open Seal Verifier →# QRS Seal Verification Specification — Version 1
- **Status:** Draft (in-repo). Normative.
- **Spec version:** `seal-verify/1` (this document).
- **Covers seal schemas:** `qrs.seal/v1`, `qrs.seal/v2` (per-run); `qrs.meta_seal/v1` (release bundle).
- **Audience:** anyone building an independent verifier — auditors, regulators, reinsurers, customers.
This document defines, normatively and completely, how to verify a QRS
reproducibility seal **without any QRS code or account**. A third party can
implement a conforming verifier from this document alone. The reference
implementation is the MIT-licensed `qrs-replay` CLI.
The keywords **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are
to be interpreted as described in RFC 2119.
---
## 1. Scope and terminology
A **seal** is a JSON document that binds a completed QRS engine run — its engine
version, pinned dependencies, inputs, configuration (including all RNG seeds),
execution environment, outputs, and a hash-chained lineage head — and is signed
with the QRS signing key. Verifying a seal answers two questions:
- **Authenticity** — was this exact payload signed by the QRS key? (Step 2.)
- **Reproducibility** — do the bound engine version, dependencies, inputs, and
seeds deterministically reproduce the recorded outputs? (Steps 4–5.)
A seal does **not** assert that the model is *correct* (see §11).
Definitions:
- **Payload** — the signed JSON object (the `payload` field of the envelope).
- **Envelope** — the object returned by `GET /runs/{run_id}/seal`: the `payload`
plus the detached `signature` and convenience copies of a few indexed fields.
- **Canonical bytes** — the payload rendered by the canonicalization in §4.
- **Conforming verifier** — software that implements Steps 1–3 (at minimum) and
emits the result contract in §9.
---
## 2. The seal envelope
`GET /runs/{run_id}/seal` (tenant-scoped) returns:
| Field | Type | Notes |
|-------|------|-------|
| `run_id` | string | Run identifier. |
| `seal_status` | string | `SEALED` \| `PENDING_KMS` \| `FAILED`. Only `SEALED` rows carry a signature. |
| `schema_version` | string | `qrs.seal/v1` or `qrs.seal/v2`. |
| `payload` | object | The signed payload (§3). |
| `signature` | string \| null | base64url(DER ECDSA signature). `null` until signed. |
| `signing_key_id` | string | Key identifier (e.g. `alias/qrs-seal-v1`). |
| `sealed_at` | string \| null | ISO-8601 timestamp. |
| `lineage_chain_head` | string | Convenience copy of `payload.outputs.lineage_chain_head` (DB-indexed). |
| `final_losses_sha256` | string | Convenience copy of `payload.outputs.final_losses_sha256`. |
For **offline** verification the envelope is saved to a file and distributed
independently of any QRS account. A conforming verifier MUST accept a saved
envelope and MUST NOT require API credentials to verify it.
The convenience fields `lineage_chain_head` and `final_losses_sha256` are
**outside** the signature. They exist only so Step 3 can detect after-the-fact
edits to the stored payload (see §6). The authoritative values are always the
ones inside `payload`.
---
## 3. What the payload binds
The payload is a nested object. The exact tree (schema `qrs.seal/v2`; `v1` is
identical minus `inputs.calibration_provenance`):
```
{
"schema_version": "qrs.seal/v2",
"run": { "run_id", "tenant_id", "peril", "submitted_at", "completed_at" },
"engine_version": { "git_sha", "git_tag_or_branch", "docker_image_digest" },
"wheel_lock": { "requirements_sha256", "entries": [ {name, version, sha256}, ... ] },
"inputs": {
"portfolio_sha256": "sha256:...",
"calibration_sha256": "sha256:...",
"calibration_provenance": { ... }, // v2+ only; see §10
"config": { ... } // all RNG seeds + sampler settings
},
"execution": {
"backend_class": "SIMULATOR_CPU" | ...,
"ran_on_real_quantum_hardware": false,
"hardware_fingerprint": { cpu_model, cpu_arch, blas_impl, blas_threads, ... }
},
"outputs": {
"final_losses_sha256": "sha256:...", // bit-identical hash of the loss array
"final_losses_length": <int>,
"var_995": <float>, "tvar_995": <float>,
"lineage_chain_head": "<hash>", // see §7
"lineage_entry_count": <int>
},
"seal_metadata": {
"sealed_at": "ISO-8601",
"seal_format_version": "qrs.seal/v2",
"signing_key_id": "alias/qrs-seal-v1",
"signature_algorithm": "ECDSA_SHA_256",
"public_key_url": "https://trust.qrsrisk.com/seal-key-v1.pem"
}
}
```
Field meanings a verifier relies on:
- **`engine_version.git_sha` / `docker_image_digest`** — the exact engine code
and container image to reproduce the run (Step 4).
- **`wheel_lock`** — hash-pinned Python dependencies. `entries[]` each carry
`name`, `version`, `sha256`. A full replay MUST confirm the running image's
installed wheels match (Step 4).
- **`inputs.portfolio_sha256` / `calibration_sha256`** — SHA-256 of the
portfolio bytes and of the calibration (`theta`) output, as `sha256:<hex>`.
- **`inputs.config`** — the full run configuration, including every RNG seed
(`numpy`, `python_random`, `qiskit_seed_simulator`, `qiskit_seed_transpiler`)
and sampler settings (`shots`, `sampler_mode`, `use_qae`, `use_vqe`,
`n_qubits`). Determinism depends on these being reused exactly.
- **`outputs.final_losses_sha256`** — the integrity anchor for the result. See
§3.1 for how it is computed.
### 3.1 Output hash (`final_losses_sha256`)
The final loss distribution is a numeric array. Its hash is:
```
sha256( ascontiguous(losses).tobytes() ) → "sha256:" + hex
```
The array is normalized to a C-contiguous layout before hashing, and the
element **dtype is part of the hash** (the byte serialization is
dtype-dependent). A reproducing verifier MUST therefore produce an array of the
same dtype and shape; an `int32` array and an equal-valued `float32` array hash
differently — correctly, because they are different numbers.
---
## 4. Canonicalization (normative)
Both signing and verification operate on the **canonical bytes** of the payload:
```
canonical_bytes = json.dumps(
payload,
sort_keys=True, # recursively sort object keys
separators=(",", ":"), # no insignificant whitespace
ensure_ascii=False, # emit UTF-8, do not \u-escape
).encode("utf-8")
```
A conforming verifier MUST reproduce these bytes exactly: keys sorted
lexicographically at every level, item/key separators `","` and `":"` with no
spaces, and UTF-8 output without ASCII escaping. Number formatting MUST match a
standard JSON encoder (the seal contains no values that require special number
formatting beyond standard JSON). Any deviation changes the bytes and therefore
the signature check.
> **Note.** This compact form is used for the **seal payload** and the
> **meta-seal** (§8). The **lineage entry hash** (§7) uses a *different* JSON
> form (default separators); do not confuse the two.
---
## 5. Signature scheme
- **Algorithm:** ECDSA on curve **NIST P-256** (secp256r1) with **SHA-256**.
Identified in the payload as `seal_metadata.signature_algorithm = "ECDSA_SHA_256"`.
- **What is signed:** the SHA-256 digest of the canonical bytes (§4). Signing
the canonical bytes with `ECDSA(SHA256)` and signing their pre-computed SHA-256
digest are equivalent and produce interchangeable signatures.
- **Signature encoding:** ASN.1 **DER**, transported in the envelope as
**base64url** (`signature`).
- **Public key:** a NIST P-256 public key in **PEM** (`SubjectPublicKeyInfo`).
QRS publishes the current key, unauthenticated, at the URL in
`seal_metadata.public_key_url` (the reference deployment serves it at
`GET /trust/seal-key/v1.pem`, `Content-Type: application/x-pem-file`, cacheable
for 1 hour). Key rotation is expressed by a versioned URL/key id; a verifier
MUST use the key identified by the seal it is checking.
**Verification (Step 2)** — a conforming verifier MUST:
1. Read `signature` from the envelope; if absent/`null`, fail with
`INVALID_SIGNATURE` (an unsigned `PENDING_KMS`/`FAILED` seal is not verifiable).
2. base64url-decode the signature to DER bytes.
3. Obtain the public key PEM — either fetched from `public_key_url`, or supplied
out-of-band (offline / air-gapped). Loading a non-EC or malformed key MUST
fail with `INVALID_SIGNATURE`.
4. Recompute `canonical_bytes` (§4) and verify the DER signature over them with
ECDSA-P256/SHA-256. A failed check MUST fail with `INVALID_SIGNATURE`
("payload tampered or wrong key").
A verifier that cannot retrieve the public key (network/5xx) and was not given
one offline MUST fail with `KMS_PUBLIC_KEY_UNREACHABLE` and SHOULD advise
supplying the key file for offline use.
---
## 6. Verification algorithm (steps 1–5)
| Step | Name | Needs | Proves |
|------|------|-------|--------|
| 1 | Obtain seal | saved file *or* API fetch | you have an envelope to check |
| 2 | Verify signature | public key (online or offline) | **authenticity** — QRS signed this exact payload |
| 3 | Verify lineage chain head | envelope only | the stored payload was not edited post-signing (§6.1) |
| 4 | Re-pin environment | Docker + `wheel_lock` | the same engine + deps are used to reproduce |
| 5 | Re-run & compare output | Docker, portfolio bytes | **reproducibility** — recomputed `final_losses_sha256` matches |
**Steps 1–3 are crypto-only**, finish in milliseconds, require no Docker, and
can run **fully offline / air-gapped** (saved envelope + saved public key). This
is the assurance most auditors need: the result is authentic and its inputs are
fixed and self-describing.
**Steps 4–5 are full reproduction**: pull the pinned image by digest, confirm
the installed wheels match `wheel_lock`, re-run the engine on the same portfolio
bytes with the sealed config/seeds, and confirm the recomputed
`final_losses_sha256` equals the sealed value. These require the portfolio bytes
and a container runtime and are opt-in.
A conforming verifier MUST implement Steps 1–3. It MAY implement Steps 4–5; if
it does, it MUST use the digest-pinned image and the sealed config without
modification.
### 6.1 Lineage chain-head binding (Step 3)
The envelope carries a top-level `lineage_chain_head` (a DB-indexed copy) and
the signed payload carries `payload.outputs.lineage_chain_head`. Step 3 confirms
the two are equal. Because the payload copy is inside the signature, a mismatch
means the stored payload was altered after signing without re-signing (which
would also fail Step 2); surfacing it separately as `LINEAGE_HEAD_MISMATCH`
gives an actionable diagnostic.
---
## 7. Lineage hash chain
Each run maintains an append-only, SHA-256-linked ledger of analytical steps.
`outputs.lineage_chain_head` is the hash of the **last entry recorded before the
seal**, binding the entire prior chain into the signature.
- **Genesis hash:** `SHA256(b"QRS_GENESIS_BLOCK")`, hex. The first entry's
`previous_hash` MUST equal this.
- **Entry hash:** for an entry with fields `entry_id` (int), `timestamp` (str),
`step_name` (str), `action` (str), `data_hash` (str), `previous_hash` (str),
and `details` (object):
```
entry_hash = sha256(
json.dumps(
{entry_id, timestamp, step_name, action, data_hash, previous_hash, details},
sort_keys=True, default=str # NOTE: default separators (", ", ": ")
).encode("utf-8")
).hexdigest()
```
This JSON form uses **default separators (with spaces)** and `default=str` for
non-JSON-native values — deliberately different from the compact seal
canonicalization in §4. A lineage verifier MUST reproduce this exact form.
- **Linkage:** each entry's `previous_hash` MUST equal the prior entry's
`entry_hash`; `entry_id` is the zero-based index.
A full lineage re-verification (recomputing every `entry_hash` and checking
linkage from genesis) operates on the exported ledger entries, which are
available separately from the seal. The offline seal verifier (Steps 1–3)
checks only that the signed payload commits the chain **head** (§6.1); it does
not need the full entry list.
---
## 8. Meta-seal (release bundle), `qrs.meta_seal/v1`
A meta-seal binds a set of per-run seals to a specific engine release (e.g. a
K=30 validation bundle). Shape:
```
{
"schema_version": "qrs.meta_seal/v1",
"model_version": { "git_sha", "git_tag", "released_at" },
"certified_runs": [ { "run_id", "run_label", "seal_signature", "lineage_chain_head", "final_losses_sha256" }, ... ],
"rollup_hash": "sha256:...",
"seal_metadata": { "sealed_at", "seal_format_version", "signing_key_id", "signature_algorithm", "public_key_url" }
}
```
Verification:
1. **Rollup** — sort `certified_runs` by `run_label` (lexicographic by Unicode
code point, case-sensitive, ascending), canonicalize the sorted list with the
§4 compact form, and confirm
`rollup_hash == "sha256:" + SHA256(that).hexdigest()`. Mismatch ⇒
`META_SEAL_ROLLUP_MISMATCH`.
2. **Outer signature** — verify the meta-seal payload signature exactly as a
per-run seal (§5).
3. **Constituents** — for each certified run, fetch/obtain its per-run seal and
confirm its signature matches `seal_signature` (and re-verify it per §5).
> **Obtaining the constituents.** Step 3 needs each certified run's per-run
> seal. Online, these come from `GET /runs/{run_id}/seal` (one per run, QRS API
> access required). For air-gapped verification, export the per-run seal
> envelopes to a local directory — one `<run_id>.json` each — and verify from
> those files with no network: the reference `qrs-replay` CLI does this via
> `verify-meta-seal --seals-dir <dir> --public-key <pem>`. This obtaining step
> is the only part of meta-seal verification that is not self-contained in the
> bundle; the verification semantics above are unchanged.
---
## 9. Conformance — result contract
A conforming verifier MUST communicate the outcome via process exit code, and
SHOULD emit the corresponding line. These codes are stable across this spec
version.
| Exit | Code / line | Meaning |
|------|-------------|---------|
| 0 | `SEAL_VERIFIED run=<id> chain_head=<…> output_hash=<…>` | Steps 1–3 passed (and 4–5 if requested → also `FULL_REPLAY_VERIFIED`). |
| 10 | `SEAL_NOT_FOUND` | Seal could not be obtained (missing API seal, or a missing/unreadable/not-an-envelope `--seal` file). |
| 11 | `INVALID_SIGNATURE` | Signature missing, malformed, key invalid, or does not verify (tamper / wrong key). |
| 12 | `LINEAGE_HEAD_MISMATCH` | Envelope vs. payload `lineage_chain_head` differ (§6.1). |
| 13 | `WHEEL_LOCK_MISMATCH` | (full replay) installed wheels ≠ `wheel_lock`. |
| 14 | `PORTFOLIO_HASH_MISMATCH` | (full replay) portfolio bytes ≠ `inputs.portfolio_sha256`. |
| 15 | `OUTPUT_HASH_MISMATCH` | (full replay) recomputed output ≠ `outputs.final_losses_sha256`. |
| 16 | `META_SEAL_ROLLUP_MISMATCH` | Meta-seal rollup hash mismatch (§8). |
| 20 | `KMS_PUBLIC_KEY_UNREACHABLE` | Public key could not be fetched and none supplied offline. |
| 30 | `DOCKER_PULL_FAILED` | (full replay) image pull failed. |
Failure lines are written as `{CODE}: {diagnostic}`. Verifiers MAY add codes for
additional checks but MUST NOT repurpose the codes above.
---
## 10. Versioning and compatibility
- This document is **Seal Verification Specification v1** and is independent of
the seal `schema_version`. It covers `qrs.seal/v1`, `qrs.seal/v2`, and
`qrs.meta_seal/v1`.
- **Signature verification is schema-agnostic**: it operates on the canonical
bytes (§4), so widening the payload does not break older verifiers as long as
the canonicalization is unchanged.
- **v1 → v2** is purely additive: v2 adds `inputs.calibration_provenance`
(content-hash + source of the validated calibration dataset behind `theta`, or
an honest absence marker). No existing field changed; v1 seals remain valid
and verify unchanged.
- A verifier encountering a payload **without** `inputs.calibration_provenance`
MUST treat it as `{ "available": false }` (a v1 seal), not as an error.
- A verifier MUST NOT reject a payload solely because it carries unknown
additional fields (forward-compatibility), provided the signature verifies.
---
## 11. Security considerations and non-goals
What a passing verification **proves**: the payload is authentic (signed by the
QRS key) and integral (unmodified), and — with Steps 4–5 — that the bound
engine/deps/inputs/seeds reproduce the recorded outputs bit-for-bit.
What it **does not** prove (non-goals):
- **Model correctness/accuracy.** The seal attests integrity and
reproducibility, not that the loss numbers are *right*. Accuracy is addressed
separately (benchmark-lab / model cards).
- **Cross-hardware bit-determinism.** Reproduction targets the sealed
`hardware_fingerprint` / BLAS settings. Different hardware may not reproduce
byte-identical floating-point output; the fingerprint records the conditions.
- **Trusted timestamping.** `sealed_at` is asserted by the signer, not an
RFC-3161 timestamp authority.
- **Post-quantum resistance.** ECDSA-P256 is not post-quantum; out of scope for
v1.
Operational notes: the public-key endpoint is public and cacheable; verifiers
SHOULD cache the PEM and MAY pin it out-of-band for air-gapped use. Tenant
scoping on `GET /runs/{id}/seal` returns 404 for unknown/other-tenant runs to
avoid leaking existence.
---
## 12. Reference test vector — RESERVED
This section will be populated with a published sealed run and its expected
verification result (envelope, public key, and expected `SEAL_VERIFIED` output),
so implementers can validate a new verifier end-to-end.
When published, the vector(s) live in [`vectors/`](./vectors/) alongside this
document, in the layout described by [`vectors/README.md`](./vectors/README.md):
one subdirectory per vector holding `seal.json`, `seal-key.pem`, and an
`expected.json` exit code. A conforming verifier's test suite consumes whatever
vectors are present there, so the first production-signed run drops in as the
reference vector with no change to this specification.
It is intentionally empty today: by policy, **no placeholder or synthetic
vector is published**. The reference vector will be added from a real
production-signed run once production signing is active. Until then, implementers
can self-test against locally generated P-256 keys and the algorithm above.