Skip to content

Verifying Evidence Offline

An evidence package is a ZIP file exported from the dashboard. It contains the consent records, the files needed to check them, and scripts that run the checks. Nothing on this page needs a network connection or a HashEntry server.

This page states what each check establishes. No single check makes a record “tamper-proof”; together they let a third party detect changes and tie a record to a point in time.


FileContent
consent_data.jsonThe exported records. Each record has a proof block with the hashed payload.
manifest.json, metadata.jsonPackage id, specification version, record count, package_hash.
checksums.sha256SHA-256 of every file in the package.
timestamps.tsv + *.tsrRFC 3161 timestamp tokens (tsa_consent_<id>.tsr, tsa_epoch_<id>.tsr, package.tsr) and what each one covers.
merkle_proofs.tsvMerkle inclusion proofs (for records timestamped in batches).
verify.sh, verify.ps1, README_*.txt, proof_verification.txtVerification scripts and instructions.

Packages come in two kinds: unsigned (checksums only) and signed (the package hash additionally carries an RFC 3161 timestamp, package.tsr).

Terminal window
# Linux / macOS
chmod +x verify.sh && ./verify.sh
Terminal window
# Windows
powershell -ExecutionPolicy Bypass -File .\verify.ps1

The scripts check file checksums, the manifest, timestamps and Merkle proofs. They do not recompute the proof hash, because that needs the data controller’s secret key (see below).


Terminal window
sha256sum -c checksums.sha256 # macOS: shasum -a 256 -c checksums.sha256
sha256sum consent_data.json # must equal "package_hash" in manifest.json

This shows that the files are the ones listed in the package. On its own it does not show who created the package or when — anyone can recompute checksums after changing a file. That is what the timestamp on a signed package is for.

Every record has a proof_hash. For current records (hash_version: 2):

proof_hash = HMAC-SHA256( key = tenant secret key, message = canonical_json(payload) ) → lowercase hex

The payload always has these eleven keys (a missing value is null):

v (= 2), site_id, document_hash, consent_type, decision, categories, timestamp,
pii_digest, signer_digest, custom_document_hash, signature_hash
  • timestamp is UTC with second precision, YYYY-MM-DDTHH:MM:SS+00:00, and equals the record’s creation time.
  • Personal data does not enter the hash directly. It enters through salted digests: pii_digest = SHA-256( pii_salt || canonical_json({ip_address, user_agent, visitor_id}) ), and signer_digest = SHA-256( pii_salt || canonical_json(signer evidence fields) ) (or null when there is no signature or identity statement). When a record is anonymised, the personal fields and the salt are deleted but the digests stay, so the proof hash can still be recomputed while the digests can no longer be linked back to an IP or e-mail address.

Canonical JSON (HashEntry Canonical JSON v2 — this is not RFC 8785):

  1. UTF-8, no whitespace between tokens.
  2. Object keys sorted by UTF-8 byte order, at every level.
  3. Array order is kept.
  4. An empty object and an empty array are both written as [].
  5. In strings only ", \ and control characters U+0000–U+001F are escaped; / and non-ASCII characters are not.
  6. Numbers are integers only.
  7. true, false, null in lower case.

Who can check it. The proof hash is a keyed hash. Recomputing it needs the tenant’s secret key, so it can be checked by the data controller (who holds the key) or by an expert the controller gives the key to. Without the key, a third party cannot recompute it — and does not need to: the timestamp and Merkle checks below work on the proof_hash value itself and show that this value existed at the stated time.

Terminal window
# With the key: recompute one record's proof hash (canonical payload in payload.json, no trailing newline)
openssl dgst -sha256 -hmac "$TENANT_SECRET_KEY" payload.json

Legacy records (hash_version: 1) use SHA-256( json(payload with sorted top-level keys) || key ) and contain personal data in the payload directly.

timestamps.tsv has one row per token:

<file.tsr> <kind> <stamped data> <provider> <level at issuance>

The stamped data is a lowercase hex string: a record’s proof_hash, a Merkle root, or the package_hash. The timestamp authority signed the SHA-256 of the ASCII characters of that string, without a trailing newline:

Terminal window
printf '%s' "<stamped data>" > data.txt
openssl ts -verify -data data.txt -in <file.tsr> -CAfile <tsa_root.pem>
  • The TSA’s root certificate is not in the package. Obtain it from the timestamp authority itself; a root shipped by the party whose data you are checking would prove nothing.
  • Tokens of some authorities fail the ESSCertID check of openssl ts -verify. Verify signature and chain in two steps instead:
Terminal window
openssl ts -reply -in <file.tsr> -token_out -out token.der
openssl cms -verify -inform DER -in token.der -CAfile <tsa_root.pem> \
-purpose timestampsign -binary -out tstinfo.der
openssl ts -reply -in <file.tsr> -text # compare "Message data" with SHA-256 of the stamped data

A row whose level is simulated is a test timestamp: it has no .tsr file and no evidential value.

A valid token shows that the stamped value existed no later than the time in the token, according to that timestamp authority. Whether a given authority’s timestamps carry a legal presumption (for example qualified timestamps under eIDAS, or timestamps of an authorised provider under Turkish law) depends on the authority and the jurisdiction.

When records are timestamped in batches, one token covers the Merkle root of a batch and each record gets an inclusion proof. merkle_proofs.tsv:

<consent_id> <leaf> <index> <leaf_count> <root> <epoch.tsr> <path>

The tree is HashEntry’s own SHA-256 binary Merkle scheme. It is not the RFC 6962 (Certificate Transparency) tree, and RFC 6962 tools will not verify it:

  • leaf = the record’s proof_hash (64 lowercase hex characters), with no additional hashing;
  • node = SHA-256 over the 128 ASCII characters hex(left) || hex(right) — the hex strings, not the raw bytes;
  • a layer with an odd number of nodes duplicates its last node;
  • there is no leaf/node domain separation prefix.

To verify a path, start with cur = leaf and apply each step:

L:<h> → cur = sha256(<h> || cur)
R:<h> → cur = sha256(cur || <h>)
Terminal window
# one step, e.g. "R:<h>"
cur=$(printf '%s%s' "$cur" "$h" | sha256sum | cut -d' ' -f1)

The result must equal <root>. Also check that 0 <= index < leaf_count and that the number of steps equals the tree depth for leaf_count — the duplication rule does not bind the tree size into the root, so these values are part of the proof. Finally verify <epoch.tsr> against <root> as in section 3: that is what proves when the root, and therefore the record’s proof hash, existed.


CheckEstablishes
ChecksumsThe package files are internally consistent.
Proof hash (with the key)The record’s content matches the hash computed when it was written.
RFC 3161 tokenThe stamped value existed no later than the token’s time, per that authority.
Merkle proof + epoch tokenThe record’s proof hash was part of a batch whose root existed at that time.

What they do not establish: who the person was (see identity_assurance in Document Approval & Signature), or that the record was created correctly in the first place. Records without a timestamp token rely on the proof hash alone, and therefore on the secrecy of the key.