@usehasp/verify
v1.0.2
Published
Verify Hasp signed audit exports. Offline, no telemetry, standard primitives only.
Downloads
578
Maintainers
Readme
For auditors, compliance reviewers, and customers who want to verify Hasp audit exports for themselves.
Hasp signs every audit-log entry with an Ed25519 key and chains them with SHA-256, then anchors the chain head with an RFC 3161 timestamp from an independent TSA. This tool re-runs those four checks on an export so you don't have to trust Hasp's word for it. The same checks can be run by hand from the manual recipe — this tool is a convenience, not a trust anchor.
- Offline. No service calls. One optional network fetch (TSA CA cert) you can disable or replace with a local file.
- Auditable. Under 400 LOC of plain JS over Node
crypto+openssl ts. Read it in one sitting. - Reproducible. npm provenance + Sigstore attestation on every release. Same input → same verdict, forever.
- Quickstart
- Install
- Requirements
- Usage
- What it checks
- Programmatic use
- Versioning & support
- Trust & provenance
- FAQ
- Security
- Contributing
- License
Quickstart
# Grab a sample export and verify it. Requires Node 20+ and openssl.
curl -O https://usehasp.com/trust/audit-export-sample.json
npx @usehasp/verify audit-export-sample.jsonExpected output:
✓ schema valid
✓ chain intact (4 / 4 entries)
✓ signatures verified (4 / 4)
✓ TSA anchor valid
— https://freetsa.org/tsr
VERIFIED.If any check fails, the tool exits non-zero and prints which one and why. For example, a tampered chain looks like:
✓ schema valid
✗ chain broken at seq=2: computed <a>, declared <b>
FAILED.Exit code is 1. The failing check is named with the entry seq; checks that passed are still shown so you can see how far verification got.
This tool is a convenience layer over the manual recipe published at the Hasp Trust Center. If the tool ever disagrees with the manual recipe, the manual recipe wins — open an issue.
Install
No install needed if you have Node 20+ and npx:
npx @usehasp/verify export.jsonOr install globally:
npm install -g @usehasp/verify
hasp-verify export.jsonRequirements
- Node.js 20+ (uses built-in
node:cryptoEd25519, no native deps). - OpenSSL 1.1+ on
PATH(used only foropenssl ts -verify— RFC 3161 TSA anchor check). Skip with--skip-tsaif you don't have it. - Network to fetch the TSA CA certificate (URL is in the export). Skip with
--skip-tsaor pass--ca-file <path>for fully offline operation.
macOS LibreSSL caveat. macOS ships LibreSSL by default and
openssl ts -verifybehaves differently from OpenSSL. Install OpenSSL 3 via Homebrew (brew install openssl@3) and put it first onPATH, or pass--skip-tsa.
macOS:
brew install node openssl@3Debian / Ubuntu:
sudo apt-get install nodejs opensslUsage
hasp-verify <export.json | -> [options]
Options:
--json Emit machine-readable JSON instead of a human report.
--skip-tsa Skip the RFC 3161 TSA anchor check (offline mode).
--ca-file <p> Read TSA CA cert from a local PEM file (no network).
--verbose Print extra detail (success: summary; failure: full check JSON).
-h, --help Show help.
-v, --version Show version.Read from stdin by passing - as the file argument:
cat export.json | hasp-verify - --skip-tsaVerify the TSA anchor fully offline with a previously archived CA cert (see docs/AIR-GAPPED.md):
hasp-verify export.json --ca-file ./freetsa-cacert.pemExit codes:
| Code | Meaning | |------|---------| | 0 | VERIFIED — all checks passed | | 1 | FAILED — at least one check failed | | 2 | USAGE error — bad arguments or unreadable file |
What it checks
Algorithms are pinned: Ed25519 for signatures, SHA-256 for the chain, RFC 3161 for the timestamp anchor. The tool rejects exports that declare anything else.
Four checks, in this order. Any failure stops the run and reports the failing check.
- Schema —
schema_versionis1.0, all required fields present,entries[*].seqis 1-indexed and contiguous,chain_head_hashis 64-hex, TSA URLs arehttps:. → proves the export has the shape downstream checks expect; malformed input fails loudly instead of being silently coerced. - Hash chain — for every entry,
sha256(prev_hash_hex || canonical_json(entry_without_hash_and_signature))matches the declaredentry.hash. The final entry's hash matchesverification.chain_head_hash. → proves no entry was added, removed, or mutated after signing — any tampering breaks the chain. - Signatures — every entry's
ed25519:<base64>signature verifies againstverification.public_key_pemover the same canonical payload. → proves each entry was signed by the holder of the published key, not anyone else. - TSA anchor — for each anchor in
tsa_anchor_chain: decode TSR, fetch CA cert fromtsa_cacert_url(or read from--ca-file), runopenssl ts -verify -in <tsr> -CAfile <cacert> -data <chain_head_hash_ascii>, requireVerification: OK. → proves the chain head existed at the timestamped instant; an attacker who later compromises the signing key cannot retroactively forge older entries without also compromising the TSA.
Field-by-field schema reference: docs/SCHEMA.md. Machine-readable JSON Schema: schema/v1.0.json.
Programmatic use
import { verifyExport } from "@usehasp/verify";
import { readFile } from "node:fs/promises";
const data = JSON.parse(await readFile("export.json", "utf8"));
const result = await verifyExport(data, { skipTsa: false });
if (!result.ok) {
for (const [name, check] of Object.entries(result.checks)) {
if (check.ran && !check.ok) console.error(`${name}: ${check.error}`);
}
process.exit(1);
}Versioning & support
- SemVer. Tool follows Semantic Versioning. Breaking CLI flag changes, breaking programmatic API changes, and dropped Node majors bump major.
- Node support. Active Node LTS lines (currently 20, 22). When a Node major reaches end-of-life, it is dropped in the next minor.
- Schema compatibility. Each tool minor pins to one export
schema_version. The tool will refuse exports with an unrecognisedschema_versionand tell you which tool version to install.
| Export schema_version | Tool version | Status |
|-------------------------|--------------|----------|
| 1.0 | 1.x | current |
Trust & provenance
Verify the tarball you installed came from this repo and matches the published build:
# Cryptographic provenance attached by GitHub Actions OIDC + Sigstore.
npm audit signatures
# Optional: verify Sigstore attestation against the exact build workflow.
gh attestation verify $(npm pack @usehasp/verify | tail -1) --repo UseHasp/verifyBoth commands must succeed. If they don't, do not run the binary.
Full trust posture: docs/TRUST.md. Authoritative explanation of how exports are signed, anchored, and verified: Hasp Trust Center.
FAQ
Native binaries break the reproducibility claim. The published npm tarball is the exact JS source you can read in src/ — no build step on the auditor's machine, no opaque blob. If we shipped a binary, you would have to trust our build pipeline; with source-only Node, you trust yours.
Auditors already trust OpenSSL. Bundling our own TSR parser would mean asking auditors to trust 500 lines of crypto we wrote ourselves. openssl ts -verify is the same primitive the manual recipe at https://usehasp.com/trust/verify uses — the tool stays equivalent to the recipe.
Yes. Pass --skip-tsa to skip the only network fetch (the TSA CA cert). The schema, chain, and signature checks all run locally with no network. The TSA anchor check is the only one that needs the cert — if you have the cert on disk, you can run openssl ts -verify manually against the embedded TSR.
It doesn't, by design. The tool is a convenience layer over the same primitives the manual recipe uses (openssl ts -verify, Ed25519, SHA-256, canonical JSON). Same inputs → same verdict. If they ever disagree, the manual recipe wins and the tool has a bug.
No. The tool touches the network for exactly one thing: fetching the TSA CA certificate at the URL embedded in the export. That fetch is capped at 15 s and 1 MB. Pass --skip-tsa to disable it entirely, or --ca-file <path> to read the cert from disk. No analytics, no error reporting, no update checks.
Yes, if you archive the TSA CA certificate alongside the export. The certificate is fetched from tsa_cacert_url at verify time; if that URL eventually 404s, the TSR bytes in the export are still valid but unverifiable without the cert. Recommended archival bundle:
export.json- the CA cert PEM downloaded at export time (from
tsa_cacert_url) - a pinned copy of
@usehasp/verifyat the version that producedVERIFIED
Then run hasp-verify export.json --ca-file ./tsa-cacert.pem to verify against the local cert with no network access. --skip-tsa lets you re-run the schema, chain, and signature checks indefinitely even without the cert. Full cookbook: docs/AIR-GAPPED.md.
Security
Report vulnerabilities to [email protected]. See SECURITY.md.
Contributing
See CONTRIBUTING.md for build, test, coverage, and release workflow.
License
MIT. See LICENSE.
