[AVAILABLE], demonstrated below with the Parmana server confirmed stopped
throughout the verify step.
Goal
Prove the independent-verification claim literally: take a trust record as plain JSON, verify it with nothing but Parmana’s public key, while Parmana itself is not running.Prerequisites
- A trust record to verify, saved as JSON. Get one from Quickstart, or reuse
any
ExecutionTrustRecordyou already have on disk. - Parmana’s public key. Two ways to get it, covering both the local-dev case and the genuine
third party case (added 2026-09-11, see below):
- Already on this machine for local dev:
keys/default.public.pem. - A real third party (regulator, auditor, customer) who has never touched this
deployment’s filesystem fetches it over HTTP instead:
GET /keys/defaultorGET /.well-known/jwks.json(unauthenticated, no API key needed, see Authentication). This is what actually makes the claim below true for someone who isn’t the operator, not just “the server process happens to be stopped on the same box the key file already lives on.” You do not need the private key,default.private.pem, for any step in this guide.
- Already on this machine for local dev:
- Read Execution trust records first, this guide verifies exactly the hash and signature that page describes.
Steps
1. Get a trust record and save it, then stop the server
2. Verify it with VerificationCrypto, offline
VerificationCrypto (@parmana/crypto) takes a plain ExecutionTrustRecord object and a
FileKeyProvider reading only local PEM files, no network call, no database, no running
Runtime:
FileKeyProvider.getPublicKey() only checks for <keyId>.public.pem
(packages/crypto/src/providers/key/FileKeyProvider.ts), it never reads or checks
for the private key. Verifying a record you received from someone else never needs, and
never has access to, the key that signed it.
2b. Or: the standalone, zero-config offline verifier (2026-09-11)
VerificationCrypto above still resolves its key through FileKeyProvider, i.e.
PARMANA_KEY_DIR and a local file on disk. That is fine for local dev, but not what a genuine
third party (who fetched the key over HTTP per the prerequisites above, and has it as a
PEM string, not a file at a path this process’s env vars name) actually has. For that case,
use verifyExecutionTrustRecordOffline instead: no PARMANA_KEY_DIR, no env vars, no
FileKeyProvider at all, just the record and the key string, and it also checks a hybrid
signatures array (ML-DSA-65) if the record has one, which VerificationCrypto.verify()
above does too but this function reports the hybrid result explicitly:
scripts/verify-trust-record.ts) and, independently reimplemented
in Python (python/parmana/crypto/offline_verifier.py, Ed25519 only today), see the
troubleshoot note below.
Verify
Real output, server confirmed stopped (see step 1):true. Change one nested
field, executions[0].evidence.parameters.amount, in the saved JSON, and rerun the exact
same script against the tampered file:
verifySignature() also fails because it
verifies the signature against the current (now-mutated) content, not a cached original,
the mutation invalidates both checks independently, matching what the hash and signature
actually cover.
Troubleshoot
- A large record fails to verify in your own implementation. A record over 4096 bytes of
canonical content may have been signed by AWS KMS as a fixed 97 byte commitment, because KMS refuses
a longer raw Ed25519 message. Build the commitment as the bytes of
PARMANA-ED25519-LARGE-MESSAGE-V1, one NUL byte, then the SHA-512 digest of the canonical bytes, and verify the same signature over that. Try the raw signature first, and try the commitment form only when the record is over 4096 bytes, never for a smaller one.verifyExecutionTrustRecordOffline(TypeScript) andverify_execution_trust_record_offline(Python) both do this. Public key not found: default.VerificationCryptoresolves keys viaPARMANA_KEY_DIR(or./keysif unset), confirmkeys/default.public.pemexists at that path relative to where you run the script.- Verification fails on a record you’re sure is untampered. Confirm you copied the JSON exactly as returned, re-serializing through a tool that reorders keys or reformats timestamps can change byte-level content even though the JSON is semantically identical, canonical serialization is order-sensitive by design.
- You need this in Python, not TypeScript. The Python SDK’s
client.verification.verify()callsPOST /verifyover HTTP and requires a running server, so it is not offline. As of 2026-09-11,parmana.crypto.verify_execution_trust_record_offline(a separate, standalone module,python/parmana/crypto/offline_verifier.py) does the same offline check as the TypeScript example above, independently reimplemented, not a wrapper around the TypeScript code. Cross-language determinism is proven bypython/tests/test_offline_verifier.py, which signs a record with the real TypeScript signer and verifies it with only the Python module. Install it withpip install "parmana[verify]", since the module needs thecryptographypackage, which theverifyextra provides. On 1.4.0 and earlier, pass the record as the plain JSON the server returned, not a decoded model: a decoded record whosepreviousChainHashisnullfails to verify there (G-85, fixed in 1.5.0). Ed25519 only for now: thecryptographypackage version this SDK depends on has noml_dsamodule yet, so a hybrid-signed record’s ML-DSA-65 signature cannot be checked from Python until that lands upstream; use the TypeScript version for a complete hybrid check today.
Next
Detect tampering
The same tamper-then-verify pattern, applied to the authorization envelope
instead.
Execution trust records
Exactly what’s inside the hash this guide recomputed.