← Back to the verifier

Verifying a BLACK BOX report independently

This page exists so a court, an advocate, or an opposing expert can verify a BLACK BOX report without BLACK BOX, without an account, and without the verifier page ever having existed. The verifier is a convenience. This specification is the trust.

0. What the signature claims — and what it does not

The signature attests that:

The signature does not claim:

1. The document

A report is a single self-contained HTML file with three parts:

PartMarkerSigned
Visible evidence text<pre id="blackbox-evidence-text">…</pre>yes (as renderedHash)
Machine-readable payload<script type="application/json" id="blackbox-attestation">…</script>yes (as evidenceHash)
Survivor statement<div id="blackbox-statement"><pre>…</pre></div>no — deliberately

The payload is a JSON object:

{
  "evidence":    { … the evidence zone … },
  "attestation": { … the signed object … },
  "signature":   "base64 ECDSA P-256 signature (raw r‖s)",
  "publicKey":   "base64 SPKI public key"
}

Inside the <script> block every < is escaped as the six characters < so the JSON cannot terminate the element; un-escape before parsing. In the visible blocks, &, < and > are HTML-escaped as &amp;, &lt;, &gt; — reverse in that order, with &amp; last.

2. Canonical JSON

Both hashes are taken over a canonical serialization — a small, deliberately boring subset of RFC 8785 (JCS), short enough to re-implement in any language:

  1. Object keys sorted ascending by Unicode code point of the raw key string.
  2. Members whose value is undefined are omitted; null is kept.
  3. Array order is preserved — order carries meaning (chunk sequence, notification order).
  4. No insignificant whitespace: no space after : or ,, no newlines.
  5. Strings, numbers and booleans use standard JSON encoding (RFC 8259).
  6. Non-finite numbers are invalid and never appear.

3. The verification procedure

Step 1 — extract

Take the text between the attestation markers, replace < with <, and JSON.parse it. Take the text between the evidence markers and HTML-unescape it. If either marker is absent the file is not a BLACK BOX report — a different outcome from TAMPERED.

Step 2 — check the key is the published one

This step is not optional. A document carries its own public key, so a forger can sign an altered report with a key they control and publish that key alongside it. Such a document is perfectly self-consistent and proves nothing. Always compare payload.publicKey against the published BLACK BOX key below.

Published key (SPKI, base64):

MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXqmajlR2iQOxIrPfF4AE8kTQdfofQIlLWaZn39L2mxyq2mwbcCb32QvqrNzQsTVHnBR1l3j9+0c/7nmbydR+GQ==

Also downloadable as PEM or JWK.

Step 3 — check the evidence data

sha256_hex( utf8( canonicalize( payload.evidence ) ) )  ==  attestation.evidenceHash

Step 4 — check the visible text

Collapse every run of whitespace in the evidence text to a single space, trim both ends, then:

sha256_hex( utf8( normalized ) )  ==  attestation.renderedHash

Whitespace is normalized so that reflowing, re-saving or printing a document is not mistaken for tampering. Any change that survives normalization is a real change to what the document asserts.

Step 5 — check the text matches the data

The visible text is a deterministic rendering of payload.evidence. Re-render it, normalize as in step 4, and confirm it equals the document's own evidence text. This catches a document that displays one thing while carrying another. If you are not re-implementing the renderer, rely on steps 3 and 4 together — and read the visible evidence text, since that is what a reader sees.

Step 6 — check the signature

ECDSA_P256_SHA256_verify(
  key       = payload.publicKey        (SPKI, base64)
  message   = utf8( canonicalize( payload.attestation ) )
  signature = payload.signature        (base64, raw r‖s)
)

The signed message is the canonical attestation — not the document bytes, and not the JSON as it happens to appear in the file.

Interop note — signature encoding. WebCrypto produces and consumes the raw r‖s form (64 bytes for P-256). OpenSSL expects DER. Convert before using openssl pkeyutl. This is the one real gotcha in the format.
# raw r||s (64 bytes) -> DER, then verify
python3 - <<'PY'
import base64
sig = base64.b64decode(open('sig.b64').read().strip())
r, s = int.from_bytes(sig[:32],'big'), int.from_bytes(sig[32:],'big')
def enc(x):
    b = x.to_bytes((x.bit_length()+8)//8 or 1, 'big')
    return b'\x02' + bytes([len(b)]) + b
body = enc(r) + enc(s)
open('sig.der','wb').write(b'\x30' + bytes([len(body)]) + body)
PY

printf '%s' "$CANONICAL_ATTESTATION" > msg.bin
openssl dgst -sha256 -verify blackbox-report-public-key.pem -signature sig.der msg.bin

Verdicts

ConditionVerdict
Markers absent / payload unparseableNot a BLACK BOX report
Steps 2–6 all passCERTIFIED — evidence verified, unaltered since generation
Any of steps 2–6 failsTAMPERED — evidence altered; not certified
Your runtime cannot perform the checkCould not verify — never report this as certified

That last row matters. A verifier that cannot distinguish "unaltered" from "I could not check" is worse than no verifier, because it prints a reassuring answer on faith.

4. The statement is not signed, and that is deliberate

The statement zone is excluded from every hash above. Two consequences, both intended:

Editing the statement must never be reported as tampering. A verifier that flags an edited statement is incorrect.

5. The attestation object

{
  "format":          "blackbox-certified-report/v1",
  "alg":             "ECDSA-P256-SHA256",
  "eventId":         "…",
  "evidenceHash":    "sha256 hex — computed on the survivor's device",
  "renderedHash":    "sha256 hex — computed on the survivor's device",
  "commitmentsHash": "sha256 hex — recomputed SERVER-side from its own records",
  "chainHead":       "integrity-chain head at signing, or null",
  "chainSeq":        41,
  "signedAt":        "UTC ISO-8601"
}

commitmentsHash is the canonical hash of the ordered commitment list, derived by the server from its own records — not from anything the device supplied. That is what makes the certification non-circular: without it, the signature would attest only that the device said what the device said.

6. Zero knowledge

BLACK BOX signs two SHA-256 hashes and an event id. It does not receive the evidence, the recording, or the statement, and holds no key that can decrypt a recording — those are decrypted on the survivor's own device with her own key. A BLACK BOX signature is therefore a signature over a fingerprint the signer cannot invert.