Trust CenterVerification

Verify on your
auditor's machine.

A complete verification procedure that runs anywhere. Use a one-line tool, the hand recipe below, or both — the math is the same. No HASP software required in either path.

What verification actually proves

Every audit entry is hash-chained to the entry before it and Ed25519-signed by a per-tenant key. The chain head is periodically countersigned by an independent Time Stamping Authority — a third party with no business relationship to HASP.

That arrangement is load-bearing. Tampering with any past entry breaks the chain mathematically. Forging an entry requires the tenant private key. Rewriting the chain head requires forging the timestamp authority's signature too. None of those failure modes are something HASP can talk its way out of — and you don't have to take our word for any of it, because the verification math runs on your machine, against standard primitives, using a sample we publish in the open.

  • Hash chain: SHA-256, the same function that secures Git and Bitcoin.
  • Signatures: Ed25519 (RFC 8032), per-tenant keys published at a well-known URL.
  • Timestamp anchor: RFC 3161, signed by an external TSA whose certificate you can fetch independently.
Fastest path

One command, green or red.

For auditors who just want pass/fail, an open-source verifier is available as a standalone CLI. It performs all four checks below and prints a signed report. Don't trust the tool? Skip to the manual recipe — same math, same answer, no HASP code involved.

bash
npx @usehasp/verify export.json

Open source. ~300 lines. MIT licensed.
github.com/UseHasp/verify

Expected output
✓ embedded key matches published key
✓ chain intact (4 / 4 entries)
✓ signatures verified (4 / 4)
✓ TSA anchor valid
  — signed by freetsa.org at 2026-04-25T15:00:00Z
  — covers chain head 719bbcca…2771

VERIFIED.
Manual recipe

Verify by hand, no HASP code anywhere

Python, openssl, jq, and curl — that's it. Each command below has been executed end-to-end against the sample export and produces the "expected output" shown. If your run produces something different, that is a finding.

  1. 01

    Get the export and the published key

    From the platform: Admin → Audit → Exports → Download. Or use the sample export linked at the top of this page to follow along. The export embeds the Ed25519 public key used to sign each entry. The same key is published independently at the well-known URL below — confirm the two match before proceeding. If they don't, stop: the export is not authentic.

    bash
    curl -sLo export.json https://usehasp.com/trust/audit-export-sample.json
    curl -sLo published-keys.json https://usehasp.com/.well-known/audit-keys.json
    
    diff <(jq -r '.verification.public_key_pem' export.json) \
         <(jq -r '.keys[0].public_key_pem' published-keys.json) \
      && echo "key match: OK"
    Expected output
    
                        key match: OK
                      
  2. 02

    Install verifier dependencies

    The recipe uses standard tooling: jq for JSON parsing, openssl for hash computation and RFC 3161 timestamp verification, and the Python cryptography package for Ed25519 signatures. No HASP software is required at any step.

    bash
    # macOS
    brew install jq openssl@3
    python3 -m pip install --user cryptography
    
    # Debian / Ubuntu
    sudo apt-get install jq openssl python3-pip
    python3 -m pip install --user cryptography
  3. 03

    Verify the hash chain

    Walk the chain top-to-bottom. For each entry, compute SHA-256 of (prev_hash || canonical_entry_payload) and confirm it matches the entry's published hash. If any entry was modified, the chain breaks at that point and every entry after it.

    python
    import json, hashlib
    
    with open("export.json") as f:
        data = json.load(f)
    
    prev = "0" * 64
    for e in data["entries"]:
        payload = json.dumps(
            {k: v for k, v in e.items() if k not in ("hash", "signature")},
            sort_keys=True, separators=(",", ":"),
        )
        h = hashlib.sha256((prev + payload).encode()).hexdigest()
        assert h == e["hash"], f"chain broken at seq={e['seq']}"
        prev = e["hash"]
    
    assert prev == data["verification"]["chain_head_hash"], "chain head mismatch"
    print(f"chain intact, {len(data['entries'])} entries verified")
    Expected output
    
                        chain intact, 4 entries verified
                      
  4. 04

    Verify each entry's signature

    Each entry carries an Ed25519 signature over the canonical entry payload. Verify with the public key from step 1. A mismatch means the entry was forged or modified after signing — the chain check alone can't catch a coordinated rewrite, but a rewrite would also have to re-sign every affected entry with the tenant's private key, which HASP doesn't have access to in plaintext.

    python
    import json, base64
    from cryptography.hazmat.primitives.serialization import load_pem_public_key
    
    with open("export.json") as f:
        data = json.load(f)
    
    pub = load_pem_public_key(data["verification"]["public_key_pem"].encode())
    ok = 0
    for e in data["entries"]:
        payload = json.dumps(
            {k: v for k, v in e.items() if k not in ("hash", "signature")},
            sort_keys=True, separators=(",", ":"),
        ).encode()
        sig = base64.b64decode(e["signature"].split(":", 1)[1])
        pub.verify(sig, payload)
        ok += 1
    
    print(f"signatures verified: {ok}")
    Expected output
    
                        signatures verified: 4
                      
  5. 05

    Verify the independent timestamp anchor

    The chain head is countersigned by an external Time Stamping Authority (RFC 3161). That TSA is operated by a third party with no business relationship to HASP. Verify the TSA's signature using its own published certificate — this proves the timestamps don't depend on a HASP server clock and that the chain existed in this exact state at the time the TSA signed it.

    bash
    # Extract the TSA reply and the chain head it covers
    jq -r '.verification.tsa_anchor_chain[0].tsa_tsr_base64' export.json \
      | base64 -d > tsa.tsr
    jq -rj '.verification.chain_head_hash' export.json > chainhead.txt
    
    # Fetch the TSA's CA certificate (URL is published in the export)
    curl -sSLo tsa-cert.pem \
      "$(jq -r '.verification.tsa_anchor_chain[0].tsa_cacert_url' export.json)"
    
    # Verify the TSA signature covers the chain head
    openssl ts -verify -in tsa.tsr -CAfile tsa-cert.pem -data chainhead.txt
    Expected output
    
                        Verification: OK
                      
  6. 06

    What “verified” means

    If steps 1 and 3–5 all succeed: the embedded key matches the independently published key (no key swap), the chain is intact end-to-end (no entry modified or removed), every entry was signed by the published tenant key (no forgery), and the chain head carries a third-party timestamp signature (the state isn't a HASP clock claim). Any failure at any step is a finding — reproducible on your machine, signed by parties that aren't us.

Stuck? Email compliance.

If a step fails on a real customer export — not the marketing-site sample — that's a finding we want to hear about immediately. The compliance contact below routes to a real human promptly; security incidents route through a separate, faster channel.