No account · No internet · No trust required

Manual Proof Verification

A TrustBeat proof bundle is self-contained. Every cryptographic claim inside it can be checked with standard tools — openssl and Python 3 — without calling the TrustBeat API or trusting this website.

Before you start — export your proof bundle

All data used in the steps below comes from a single proof bundle JSON file. To get it, open the verify page, enter your anchor ID, and click Export proof bundle. Save the downloaded trustbeat-proof-*.json as proof.json before running any command below.

The bundle contains everything: the anchored hash, Merkle path, RFC 3161 token, archive stamps, and — for NIS2 or AI Act anchors — the full domain context (log metadata or AI decision metadata).

What's in a proof bundle

FieldPurpose
hashSHA-256 of your original document (hex)
merkle_rootRoot of the Merkle batch that was timestamped (hex)
proof_pathSibling hashes needed to re-derive the root from your leaf
leaf_indexPosition of your hash in the batch tree
tokenRFC 3161 TimeStampToken from an EU Trusted List QTSP (base64 DER)
tsa_serialSerial number of the TSA token — cross-check after decoding
anchored_atISO-8601 timestamp from the TSA
archive_stampsSubsequent re-stamps for long-term validity (may be empty)
log_contextNIS2 only: original log hash + metadata + combined hash
ai_contextAI Act only: input/output hashes + decision metadata + combined hash
Step 1

Verify your original data

Confirm what was anchored matches what you have. The method differs by anchor type.

Raw anchor — bundle_type: trustbeat.anchor.proof

SHA-256 your original file and compare to bundle["hash"]. Any difference means the document was modified after anchoring.

bash — Linux / macOS
sha256sum your-document.pdf
# macOS alternative:
shasum -a 256 your-document.pdf
powershell — Windows
Get-FileHash your-document.pdf -Algorithm SHA256

NIS2 log anchor — bundle_type: trustbeat.log.proof

The Merkle leaf is a combined hash, not the raw log hash. Compare bundle["log_context"]["log_hash"] against the SHA-256 of your original log file. bundle["hash"] holds the combined hash — verified in Step 5.

python3
import hashlib
print(hashlib.sha256(open("your-logfile.log","rb").read()).hexdigest())
# compare to bundle["log_context"]["log_hash"]

AI Act anchor — bundle_type: trustbeat.ai.proof

Two hashes are anchored: the model input and the model output/decision. Compare each against bundle["ai_context"]["input_hash"] and bundle["ai_context"]["output_hash"]. bundle["hash"] holds the combined hash — verified in Step 6.

python3
import hashlib, json
bundle = json.load(open("proof.json"))
ac = bundle["ai_context"]
print("input_hash :", hashlib.sha256(open("model-input.bin","rb").read()).hexdigest())
print("expected   :", ac["input_hash"])
print("output_hash:", hashlib.sha256(open("model-output.bin","rb").read()).hexdigest())
print("expected   :", ac["output_hash"])
All hash values you need are inside proof.json — nothing is fetched from the internet.
Step 2

Re-derive the Merkle root

Proves your hash was included in the batch that was timestamped.

Start with your hash as the current value. For each step in proof_path:

  • side = "left"current = SHA-256(sibling ‖ current)
  • side = "right"current = SHA-256(current ‖ sibling)

The final value must equal merkle_root.

python3
import hashlib, json, sys

bundle = json.load(open("proof.json"))

current = bytes.fromhex(bundle["hash"])
for step in bundle["proof_path"]:
    sibling = bytes.fromhex(step["sibling"])
    if step["side"] == "left":
        combined = sibling + current
    else:
        combined = current + sibling
    current = hashlib.sha256(combined).digest()

computed = current.hex()
expected = bundle["merkle_root"]

if computed == expected:
    print("✓ Merkle root OK —", computed)
else:
    print("✗ MISMATCH")
    print("  computed:", computed)
    print("  expected:", expected)
    sys.exit(1)
Step 3

Verify the RFC 3161 timestamp token

The token was issued by an EU Trusted List QTSP and covers the Merkle root.

The token field is a base64-encoded DER-encoded RFC 3161 TimeStampToken. It binds the merkle_root to a legally presumed timestamp under eIDAS Art. 42.

3a — Decode the token to a file

python3
import base64, json

bundle = json.load(open("proof.json"))
open("token.der", "wb").write(base64.b64decode(bundle["token"]))
print("token.der written")

3b — Inspect what the token contains

bash
openssl ts -reply -in token.der -text

Look for Message data — this is the SHA-256 of the merkle_root (TSAs hash the data before signing). Cross-check the Serial Number against bundle["tsa_serial"].

3c — Verify the token signature

Download the TSA certificate chain from the QTSP's trust repository (link in the proof bundle), then:

bash
# Derive the binary digest of the Merkle root that the TSA signed
python3 -c "
import hashlib, json
r = json.load(open('proof.json'))['merkle_root']
open('root.bin','wb').write(hashlib.sha256(bytes.fromhex(r)).digest())
"

# Verify the token signature against the SK TSA certificate chain
openssl ts -verify \
  -in token.der \
  -digest root.bin \
  -CAfile sk-tsa-chain.pem
A successful response prints: Verification: OK
Why do we hash the root again? The RFC 3161 protocol requires the client to send a hash of the data to be stamped. TrustBeat sends SHA-256(merkle_root_bytes) to the TSA. The token's MessageImprint contains exactly that value.
Step 4

Verify archive stamps (long-term validity)

Each archive stamp extends validity before the previous TSA certificate expires.

The archive_stamps array may be empty for recently anchored proofs. When present, each stamp is an independent RFC 3161 token that covers the covered_hash of the previous round — forming a chain that keeps the proof valid for 30+ years (CAdES-A pattern).

python3 — extract all archive tokens
import base64, json

bundle = json.load(open("proof.json"))
for i, stamp in enumerate(bundle.get("archive_stamps", [])):
    fname = f"archive_{i+1}.der"
    open(fname, "wb").write(base64.b64decode(stamp["token"]))
    print(f"Round {stamp['round']} → {fname}  (covers {stamp['covered_hash'][:16]}…)")

Verify each extracted archive_N.der the same way as Step 3, using the stamp's own covered_hash as the data digest.

Verification checklist

StepWhat you verifyTool
1file SHA-256 == bundle.hashsha256sum / shasum / PowerShell
2Merkle path re-derives merkle_rootPython 3 (stdlib only)
3atoken decodes to valid DERPython 3
3btoken covers merkle_rootopenssl ts -reply -text
3ctoken signature valid (SK TSA chain)openssl ts -verify
4archive stamps chain correctlyopenssl ts -verify (per stamp)
5*NIS2 combined hash formulaPython 3 — only for log proofs
6*AI Act combined hash formulaPython 3 — only for AI Act proofs

* Step 5 applies only to bundle_type = trustbeat.log.proof · Step 6 applies only to bundle_type = trustbeat.ai.proof

Powered by TrustBeat · eIDAS · RFC 3161 · Verify online · Inspect token

Verify a Qualified Timestamp Manually (openssl + Python) | TrustBeat