Parmana makes sure AI agents take only the actions your business has approved, and keeps signed proof of what they did. This verifier checks that proof using only a record file and the keys you trust. It needs no network access, no account and no dependencies.
Parmana signs an Execution Trust Record for every action an agent takes through it. The record says what was authorized, what ran, and what the outcome was. This verifier checks that a record has not been changed and that it was signed by keys you chose to trust. It does not contact Parmana's servers, so you do not have to trust them to check a record.
The records come from Parmana, which signs them. The verifier works without the rest of Parmana: it needs only a record and a trust file.
Test results, the security review scope and the changelog are indexed under Reports and evidence.
- Auditors and reviewers who need to confirm a record without relying on the system that produced it.
- Operators who want to check records in their own environment, with keys they pinned themselves.
- Developers who want a small, dependency-free reference implementation of the record checks.
Requires Node.js 24 or later. Ed25519 works on any Node 24 build; post-quantum ML-DSA-65 signatures need OpenSSL 3.5 or later. From a clone of this repository:
node bin/parmana-verify.mjs test/vectors/record-whole-record-v3.json test/vectors/trust-whole-record.json{
"valid": true,
"coverage": "full",
"errors": [],
"checks": {
"trustRecordHash": true,
"signature": true,
"hybridSignatures": true
},
"correspondence": {
"status": "not_checked",
"executions": [
{ "executionId": "ex_1", "result": { "status": "not_checked", "reason": "no authorization is embedded in the trust record" } }
]
}
}The exit code is 0. The correspondence field is a separate report, described in Execution correspondence. It does not change valid or the exit code. This sample record is a trimmed test vector that does not embed its authorization, so there is nothing to compare and its correspondence is reported as not_checked, not as a mismatch. A complete record from Parmana embeds the authorization and is checked. A record that has been changed is rejected:
node bin/parmana-verify.mjs test/vectors/record-tampered-amount.json test/vectors/trust.jsonThe exit code is 1, and the output lists the failed checks. The test vectors are labelled test material; see test/vectors/README.md. For real records, pin the keys you trust (see Pinning a key).
On Windows PowerShell, run the same two commands with Windows paths. Use Node 24 or later:
node bin\parmana-verify.mjs test\vectors\record-whole-record-v3.json test\vectors\trust-whole-record.json
node bin\parmana-verify.mjs test\vectors\record-tampered-amount.json test\vectors\trust.jsonThe first prints the valid result above and exits with 0. The second prints "valid":false and exits with 1.
The package is published on npm as @parmana/verifier. Install it, or run it without installing:
npm install -g @parmana/verifier
parmana-verify <proof.json> <trust.json>
parmana-verify key-fingerprint <public-key.pem>npx @parmana/verifier <proof.json> <trust.json>From a clone of this repository, run the same commands with node bin/parmana-verify.mjs in place of parmana-verify.
There is also a Python version in python/. It checks the same records and gives the same results, including ML-DSA-65 (hybrid) signatures when cryptography 48.0.0 or later is installed.
pip install parmana-verifier
parmana-verify <proof.json> <trust.json>-
proof.jsonis the Execution Trust Record. -
trust.jsonis the trust set. It lists each key you accept:{ "keys": [ { "keyId": "default", "algorithm": "ed25519", "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n", "fingerprint": "sha256:<64 hex digits>", "validFrom": "2026-01-01T00:00:00.000Z", "validUntil": null, "compromised": false } ] } -
key-fingerprintprints thesha256:value to pin for a PEM key. To pin a key safely, follow the steps in Pinning a key.
The verify command prints one line of JSON and exits with 0 (valid), 1 (invalid, with reasons), or 2 (usage, missing file, or invalid JSON). The full input and output rules are in docs/FORMAT.md.
On Windows PowerShell, use the same commands with Windows paths, for example node bin\parmana-verify.mjs proof.json trust.json.
The output also carries a correspondence report: whether what was dispatched is what was authorized. It is checked from the record alone, the same way as the signatures, and it uses the same rules as Parmana's own offline check.
For each execution, the record's authorization (its nonce, business transaction hash, policy content hash and authority grant) is compared with the execution's evidence.correspondence. Every field must be present and must match, and the dispatched content hash must equal the authorized one. The statuses are:
passed: every field is present and matches the authorization.failed: a field is missing or different. This is a mismatch that was observed.not_checked: the execution has no correspondence fields, no evidence, or the record embeds no authorization to compare with. Nothing was compared, so this is not a mismatch. It is never reported as passed.
The report is passed only when every execution is checked and corresponds. The verifier reports it and does not decide what it means for your system: a failed or not_checked report is for you to act on. It is meaningful only for a record whose signatures also verify, so read it together with valid.
A signature proves only that a key made it. Before you trust a record, decide which keys you trust, and check that each key is the one its owner published.
- Get the operator's public key PEM and the
sha256:fingerprint they published. Get the fingerprint from a source other than the one that gave you the key, such as a signed message or a call to a known number. - Run
node bin/parmana-verify.mjs key-fingerprint public-key.pemon the PEM you received. - Compare the two fingerprints character by character. If they differ, do not use the key.
- Only then add the key to your trust file with that fingerprint.
A key is used only if all of these hold:
- its PEM hashes to the pinned fingerprint (SHA-256 of the SPKI DER encoding);
- the algorithm named by the signature matches the algorithm in the trust file;
- it is not marked
compromised; - the record's
createdAtis at or aftervalidFromand beforevalidUntil(if set).
A compromised key is rejected whatever the timestamp, so signatures it made stay void.
A record with no signatures array is a legacy-only record. The verifier refuses it by default. It is accepted only when:
- the trust file sets
hybridRequiredFromand the record was created before that instant; or - the trust file sets
"allowLegacyOnly": trueand does not sethybridRequiredFrom.
Otherwise the result is invalid, and the error says which setting would accept it. A record with no schemaVersion, or with schemaVersion 2, is refused unless the trust file sets "allowLegacyV2": true, because it signs only seven top-level fields. A record created at or after hybridRequiredFrom always needs a valid hybrid envelope, whatever allowLegacyOnly says. See docs/FORMAT.md.
Run every example with:
npm run examplesThe examples are in examples/, and each one prints what it checks:
01-verify-a-proof.mjs: verify a record and explain each check.02-tamper-detection.mjs: change a signed field, then show that recomputing the hash does not help an attacker.03-pinned-keys-and-windows.mjs: how pinned fingerprints, validity windows and compromised keys decide what is accepted.04-hybrid-envelope.mjs: the Ed25519 and ML-DSA-65 entries of a hybrid record (needs Node 24).05-build-and-verify-a-record.mjs: build, hash, sign, pin and verify a record from scratch, with a runtime test key.06-command-line.mjs: the command-line exit codes, and fingerprinting a key for pinning.07-whole-record-coverage.mjs: what coveragefullandlegacymean, and that a changed covered field is refused.08-hybrid-cutover.mjs: thehybridRequiredFromcutover, and what it adds to a record created after it.09-unsupported-schema-version.mjs: a schema version other than 2 or 3 is refused, not guessed at.
Each example exits 0 when it behaves as described, so the set runs in CI.
npm testRun the tests on Node 24. On Node 22 or any runtime without ML-DSA-65, the hybrid tests are skipped. Test keys are generated at run time, labelled "TEST KEY - NOT FOR PRODUCTION", and never written to disk.
The suite covers the legacy signature and hash, hybrid signatures, schema version 3 coverage, the trust rules, the test vectors, and the command-line exit codes. Each area has its own file under test/.
Records from parmana-core are tested in test/parmana-core-records.test.mjs and python/tests/test_parmana_core_records.py. They use test/vectors/parmana-core-generated/cases.json: 22 cases over records exported at parmana-core 7e9a54fb, with tampered copies and trust-file edits. Both verifiers must agree on every case. The quick-start record in python/README.md is tested in test/quickstart.test.mjs and python/tests/test_quickstart.py.
- The verifier can only check that a record was signed by the key you pinned. It cannot prove that the key belongs to Parmana. That depends on how you obtained and compared the fingerprint.
- For records with
schemaVersion2, or with noschemaVersion, only the fields listed in docs/FORMAT.md are protected. Any other field can be changed without the verifier noticing. Read decisions only from covered fields. See "Fields not covered by any signature" in that document. - For records with
schemaVersion3, every field except the signature envelope,verificationsandreceiptsis protected. coverageisfullonly when the hybrid signatures also verify. It does not report the schema version. A schema version 3 record that fails the hybrid check reportslegacy, and an Ed25519-only failure can reportfullalongsidevalid: false. Readvalidfor the verdict.checks.signatureis the legacy top-level signature, which covers only the seven legacy fields. A change toupdatedAtin a schema version 3 record is caught by the hybrid signatures alone, sochecks.signaturestaystrue(pinned bytest/parmana-core-records.test.mjs).- A record with
schemaVersion2 can be downgraded by someone who can write to storage, sinceschemaVersionis unsigned. SethybridRequiredFromin the trust file to reject downgraded records created after a cutover. Records before the cutover stay open to this. With no cutover set, a record with no hybrid envelope is refused unless the trust file setsallowLegacyOnlytotrue(see "Records with no hybrid envelope" above). - The validity window is checked against the record's
createdAt, not against the signing time. A record that carries a falsecreatedAtis checked against the window for that false time. - Keys and fingerprints must be kept current. The verifier does not know about rotations or revocations that you have not written into the trust file.
- It does not verify the policy the record cites, the authorization decision logic, or the state of any connector or external system.
- It does not confirm that the action described by the record was executed.
- The records in
test/vectors/were produced by the Parmana signing code and are test material only. They show that the checks agree with that code; they are not an independent specification.
Every report is listed once, here. Start with the question you have.
Test results
- Live CI runs: the CI workflow runs the tests and examples on every push. The Python workflow runs the Python package tests.
- Test suite: what it covers, and how to run it on Node 24, is under Testing.
- Test vectors: labelled test records and keys, in test/vectors/. They are for tests only, not for production use.
- Examples: nine runnable examples that check their own behaviour, run by
npm run examples(Examples).
Security
- Security review scope: docs/SECURITY-REVIEW.md is written for an external reviewer. It covers what the verifier claims, what to examine, and its known limits.
- Security policy: SECURITY.md explains how to report a vulnerability privately.
Releases
- Published versions:
@parmana/verifier2.0.3 on npm andparmana-verifier2.0.3 on PyPI, published 2026-11-15 from commit4068278. - Changelog: CHANGELOG.md lists each change to the record formats and the command-line interface.
- Release process: RELEASING.md describes how a release is built, checked and published.
The Parmana blog explains how the verifier fits into the wider product, in plain terms. Read the post for your role:
- Auditors and risk teams: Check a record offline, without trusting us. How to verify a signed record with only a file and a trust file, and what the check does not cover.
- Engineers: Run the checks in an afternoon. Build, test and verify a record from source.
- Anyone who wants the honest picture: Lessons from the limits we still have. What we got wrong, and what remains open.
New posts are published regularly, so check the blog for updates.
This is version 2.0.3, published 2026-11-15 on npm and PyPI, released with the 2.0.3 SDK packages. Each change to the record formats or the command-line interface is listed in CHANGELOG.md.
On 2026-10-29, on Node 24.22.1 and Windows PowerShell, the two quick-start commands above returned the output shown, with exit codes 0 and 1. The release process builds the packages, records their hashes, and attaches build provenance. See RELEASING.md for the steps.
What this version does not do is listed under Limitations.
| Path | Contents |
|---|---|
bin/ |
The parmana-verify command-line entry point |
src/ |
The checks: canonical JSON, hashing, signatures, trust rules |
test/ |
The test suite, and test/vectors/, the labelled test records |
examples/ |
Nine runnable examples, run by npm run examples |
docs/ |
FORMAT.md (the input and output rules) and SECURITY-REVIEW.md |
reports/ |
Pointer to the reports index in this README (see Reports and evidence) |
Do not open a public issue for a security problem. Report it through GitHub's private vulnerability reporting: open the Security tab and choose Report a vulnerability. See SECURITY.md. The scope for an external review is in docs/SECURITY-REVIEW.md.
Read CONTRIBUTING.md before you open a pull request. Run npm test on Node 24 first. Every commit must carry a Signed-off-by line (git commit -s), certifying the Developer Certificate of Origin. Everyone taking part follows the Code of Conduct.
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE.