Seal Record Format · version 1 · in use
A seal is three things: a JSON object, a detached signature over its exact bytes, and the artifacts that object names. This page defines all three. It is written so that a second implementation can produce byte-identical records from the same inputs, and so that anyone can check an existing one without running our software. Where the prose and the issued record disagree, the record is right.
Everything else rests on this. A record is hashed and signed as bytes, so two implementations that disagree about whitespace produce different signatures over identical data. The serialisation is fixed:
UTF-8, no byte-order mark
keys sorted lexicographically, at every level of nesting
two-space indent, one key per line
non-ASCII emitted literally — never \uXXXX escaped
exactly one trailing newline (0x0A)
In the reference implementation that is a single expression — json.dumps(obj, sort_keys=True, indent=2, ensure_ascii=False) + "\n", encoded UTF-8. Reproducing it in another language means reproducing those five properties, not that expression.
Sorting is why the fields on disk are alphabetical and bear no relation to the order they are written in code. Do not reorder a record to make it read better. That is a different document with a different hash and no valid signature.
Every field below is required in v1. Shown here in reading order with hashes cut short; on disk the keys are sorted and every hash is the full 64 hex characters.
{
"schema": "evalseal/seal/v1" ← format identifier
"id": "D291-7CAF-C9B1" ← derived, never chosen
"grade": "A" ← A | B | C — who executed the run
"grade_meaning": "independently executed — the subject had no access to the run"
"caveat": "…" ← plain-language limits, never empty
"subject": { file, kind, sha256 } ← the artifact under test
"dataset": { file, name, version, tasks, sha256 }
"grading": "A task passes only if every case matches exactly…"
"conditions": { deterministic, note } ← anything that could make a re-run differ
"result": { passed: 8, total: 12, rate: "66.7%", failed: [ … ] }
"traces": { file, records, bytes, sha256 }
"environment": { python, platform, harness: { file, sha256 } }
"files": { "<path>": "<sha256>", … }
"sealed_at": "2026-07-28T09:14:02Z" ← RFC 3339, UTC, second precision
"signature": { scheme, namespace, identity, file, key }
}
The number is derived from the content. It is not assigned, not sequential, and cannot be chosen.
digest = sha256( traces_sha256_hex + sealed_at )
id = first 12 hex of digest, uppercased, grouped 4-4-4
sha256("52b14610…b29684" + "2026-07-28T09:14:02Z")
→ d2917cafc9b1… → D291-7CAF-C9B1
SSHSIG — the same detached-signature format used for signed git commits — over the exact record bytes. The verifying side needs ssh-keygen and nothing else.
# issue
ssh-keygen -Y sign -f <private key> -n evalseal record.json → record.json.sig
# check
ssh-keygen -Y verify -f allowed_signers -I seal@evalseal.com \
-n evalseal -s record.sig < record.json
evalseal. A signature made under any other namespace will not verify as a seal, so a key used elsewhere — for git, for SSH — cannot have one of its signatures replayed into this format.record.json and nothing else. Every other artifact is covered transitively, through its hash in files. Break either link and the check fails at a known point./keys/allowed_signers, in SSHSIG allowed-signers format. It is served from the domain it vouches for, which is a real limitation and is stated on the seal itself. Pin the fingerprint and watch for it moving.Verification does not establish that an evaluation was competent, or that its questions were fair. It establishes that these bytes are the bytes that were signed. That is the whole of what a signature is for.
A hash over an archive is worthless unless the archive is byte-reproducible. Ordinary tar and gzip are not — they record a clock, a username and whatever order the filesystem handed over. Every one of those is pinned:
gzip mtime = 0 (no timestamp in the header)
tar entries sorted by name
path prefix traces/
mtime 0 · mode 0644 · uid 0 · gid 0 · uname "" · gname ""
entries one per graded task, canonical JSON, same five rules as the record
Each of these lines exists because its default value differs between two machines that ran the same evaluation. Left alone they change the hash without changing the data, which is the one thing a content hash must never do.
The grade field states who executed the run. It says nothing about the score, and a high score cannot raise it.
Absences in a format are decisions. These four are deliberate, and the last two are unfinished.
The schema field carries the version, and a record is always read under the version it names.
evalseal/seal/v2.A format is only a format if a second implementation can produce the same bytes. Everything above is written to make that possible — including the parts that make it inconvenient.