The problem with “trust our logs”
Most audit trails are rows in a vendor’s database. If the operator, an attacker or a bug can change a row, the row proves only what the database says today. A signature changes the question. Instead of “do you trust the database”, it becomes “do you trust this public key belongs to the deployment you are auditing”. That is a much smaller thing to trust, and it is something the auditor can check herself.What a Parmana record carries
Every action that goes through Parmana leaves an Execution Trust Record. Two parts of it matter here.- A hash. The signed fields (the transaction, the authorization, the executions and so on) are serialized as canonical JSON with keys sorted, and hashed with SHA-256. The result is stored as
trustRecordHash. - A signature. An Ed25519 signature over the same canonical bytes, with the id of the key that made it.
Three ways to check, none of them need my server
In the browser. The docs site has a page called “Verify a record in your browser”. You paste a record and a public key, click Verify, and it recomputes the hash and checks the Ed25519 signature with the browser’s own Web Crypto API. Nothing you paste is uploaded. There is an example record on the page, and a second copy with the amount changed from 100 to 100000, so you can watch the hash stop matching. The page’s code is in the repository, and a test runs that same code against records signed by Parmana’s signer and checks it agrees with the offline verifier. From the command line.scripts/verify-trust-record.ts takes a record and one or more public key files and prints valid, hashValid, which algorithms were checked, and any errors. It makes no network call and needs no database.
From code, in TypeScript or Python. verifyExecutionTrustRecordOffline takes the record and a public key as a string. No environment variables, no key directory, no running server. The Python SDK has its own version, written separately rather than wrapping the TypeScript code, and a test signs a record with the real TypeScript signer and verifies it with only the Python module.
The public key itself comes from the deployment: GET /keys/default or the JWKS endpoint, both unauthenticated. The private key is never needed for any of this.
I tried to break it
A verifier that always says “valid” is easy to build. So the docs show the negative case. In the “Verify independently” guide, I saved a record, stopped the server completely, confirmed nothing was listening, and verified it: hash matches, signature valid. Then I changed one nested field, the amount inside an execution, and ran the same script on the edited file. Hash mismatch, signature invalid, fully verified false. The same pattern runs in CI on every change for the authorization envelope that releases an action: a changed parameter, a forged signature and a reused nonce are each generated, mutated and re-checked, and each one is rejected. The verifier reports each check separately, so you can tell “forged” from “expired”.The offline check
For a self-hosted deployment there is one command that runs the whole flow on a Docker network with no route to the internet, then verifies the result. It ends with “14 of 14 checks passed”. The last two checks are the ones this post is about: the Trust Record verifies with only the two public keys, and a copy with the amount changed fails. The check leaves the record and the public keys on disk, so anyone can verify it again on another machine.One wrinkle: big records and AWS KMS
AWS KMS refuses to sign a raw Ed25519 message over 4096 bytes, and a full Trust Record is often bigger. So under KMS a large record is signed as a fixed 97-byte commitment (a prefix, a NUL byte, and the SHA-512 of the content), still an ordinary Ed25519 signature. A verifier accepts that form only for records over 4096 bytes. All three verifiers above handle it.What it does not do
- It does not tell you who holds the key. A valid signature proves the record was signed by that key. Get the key from the deployment you trust, not from whoever sent you the record.
- It does not prove nothing bypassed Parmana. A signature shows what Parmana recorded and that it was not altered. An agent with its own credentials to a system can act without leaving any record at all.
- The browser page checks Ed25519 only. A record can also carry a post-quantum ML-DSA-65 signature. Browsers have no ML-DSA support yet, so the page says so and skips it. The Python verifier is Ed25519 only for the same reason (its crypto library lacks ML-DSA). Use the TypeScript verifier for a full hybrid check.
- The browser page checks Trust Records only. Execution Intents and Refusal Records are checked other ways.
- There is no in-browser check on the Playground itself yet. The Playground shows the offline check in each SDK, and you paste its record into the separate verify page.
- Re-serializing breaks it. If a tool reorders keys or reformats timestamps, a genuine record can fail. Keep the JSON exactly as returned.