> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify a record in your browser

> Paste a signed Execution Trust Record and a public key, and check the hash and the Ed25519 signature in your browser. Nothing is uploaded.

export const RecordVerifier = ({exampleRecord, examplePublicKey}) => {
  const verifyParmanaTrustRecord = async (record, publicKeyPem) => {
    const subtle = globalThis.crypto.subtle;
    const encoder = new TextEncoder();
    const errors = [];
    const notes = [];
    const normalize = value => {
      if (value === null || typeof value !== "object") return value;
      if (Array.isArray(value)) return value.map(normalize);
      return Object.keys(value).sort().reduce((out, key) => {
        Object.defineProperty(out, key, {
          value: normalize(value[key]),
          enumerable: true,
          writable: true,
          configurable: true
        });
        return out;
      }, {});
    };
    const canonicalBytes = value => encoder.encode(JSON.stringify(normalize(value)));
    const hex = buffer => Array.from(new Uint8Array(buffer), b => b.toString(16).padStart(2, "0")).join("");
    const fromBase64 = text => Uint8Array.from(atob(text), c => c.charCodeAt(0));
    if (typeof record !== "object" || record === null || Array.isArray(record)) {
      return {
        valid: false,
        hashValid: false,
        signatureValid: false,
        errors: ["The record is not a JSON object."],
        notes
      };
    }
    const view = {
      trustRecordId: record.trustRecordId,
      businessTransactionId: record.businessTransactionId,
      transaction: record.transaction,
      authorization: record.authorization,
      overrides: record.overrides,
      executions: record.executions,
      createdAt: record.createdAt
    };
    const bytes = canonicalBytes(view);
    const digest = hex(await subtle.digest("SHA-256", bytes));
    const hashValid = digest === record.trustRecordHash;
    if (!hashValid) {
      errors.push(`trustRecordHash does not match the content: computed ${digest}, record says ${String(record.trustRecordHash)}.`);
    }
    const signature = record.signature;
    let signatureValid = false;
    let signedForm;
    if (typeof signature !== "object" || signature === null || typeof signature.value !== "string") {
      errors.push("The record has no signature.");
    } else if (signature.algorithm !== "ed25519") {
      errors.push(`The signature algorithm is ${String(signature.algorithm)}; this page checks ed25519 only. Use @parmana/sign.`);
    } else {
      try {
        const der = fromBase64(String(publicKeyPem).replace(/-----(BEGIN|END) PUBLIC KEY-----/g, "").replace(/\s+/g, ""));
        const key = await subtle.importKey("spki", der, {
          name: "Ed25519"
        }, false, ["verify"]);
        const signatureBytes = fromBase64(signature.value);
        if (await subtle.verify({
          name: "Ed25519"
        }, key, signatureBytes, bytes)) {
          signatureValid = true;
          signedForm = "raw";
        } else if (bytes.length > 4096) {
          const prefix = encoder.encode("PARMANA-ED25519-LARGE-MESSAGE-V1\0");
          const sha512 = new Uint8Array(await subtle.digest("SHA-512", bytes));
          const commitment = new Uint8Array(prefix.length + sha512.length);
          commitment.set(prefix);
          commitment.set(sha512, prefix.length);
          if (await subtle.verify({
            name: "Ed25519"
          }, key, signatureBytes, commitment)) {
            signatureValid = true;
            signedForm = "commitment";
          }
        }
        if (!signatureValid) {
          errors.push(`The signature does not verify with this public key. The record names key "${String(signature.keyId)}"; check that the key you pasted is that key.`);
        }
      } catch (error) {
        const name = error && error.name;
        errors.push(name === "NotSupportedError" ? "This browser does not support Ed25519 in Web Crypto. Use a current Chrome, Edge, Firefox or Safari, or @parmana/sign." : `The public key or signature could not be read: ${error && error.message ? error.message : String(error)}`);
      }
    }
    if (Array.isArray(record.signatures) && record.signatures.length > 0) {
      notes.push("The record also carries a hybrid signatures array (for example ML-DSA-65). This page does not check it; @parmana/sign does.");
    }
    return {
      valid: hashValid && signatureValid,
      hashValid,
      signatureValid,
      keyId: signature && signature.keyId,
      signedForm,
      errors,
      notes
    };
  };
  const [recordText, setRecordText] = useState("");
  const [keyText, setKeyText] = useState("");
  const [result, setResult] = useState(null);
  const [busy, setBusy] = useState(false);
  const run = async (text, pem) => {
    setBusy(true);
    try {
      let record;
      try {
        record = JSON.parse(text);
      } catch (error) {
        setResult({
          valid: false,
          errors: [`The record is not valid JSON: ${error.message}`],
          notes: []
        });
        return;
      }
      if (!pem.trim()) {
        setResult({
          valid: false,
          errors: ["Paste the public key (PEM) that signed the record."],
          notes: []
        });
        return;
      }
      setResult(await verifyParmanaTrustRecord(record, pem));
    } catch (error) {
      setResult({
        valid: false,
        errors: [`Verification could not run: ${error && error.message ? error.message : String(error)}`],
        notes: []
      });
    } finally {
      setBusy(false);
    }
  };
  const loadExample = () => {
    const text = JSON.stringify(exampleRecord, null, 2);
    setRecordText(text);
    setKeyText(examplePublicKey);
    setResult(null);
  };
  const loadChangedExample = () => {
    const changed = JSON.parse(JSON.stringify(exampleRecord));
    changed.transaction.signals.amount = 100000;
    const text = JSON.stringify(changed, null, 2);
    setRecordText(text);
    setKeyText(examplePublicKey);
    setResult(null);
  };
  const fetchSandboxKey = async () => {
    try {
      const response = await fetch("https://parmana-sandbox.vercel.app/keys/default");
      const body = await response.json();
      setKeyText(body.pem);
    } catch (error) {
      setResult({
        valid: false,
        errors: [`Could not fetch the sandbox key: ${error.message}`],
        notes: []
      });
    }
  };
  const box = {
    width: "100%",
    fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
    fontSize: "12px",
    padding: "8px",
    borderRadius: "8px",
    border: "1px solid rgba(127, 127, 127, 0.4)",
    background: "transparent",
    color: "inherit"
  };
  const button = {
    padding: "6px 12px",
    marginRight: "8px",
    marginTop: "8px",
    borderRadius: "8px",
    border: "1px solid rgba(127, 127, 127, 0.4)",
    background: "transparent",
    color: "inherit",
    cursor: "pointer"
  };
  return <div>
      <label htmlFor="parmana-record">Execution Trust Record (JSON)</label>
      <textarea id="parmana-record" rows={12} style={box} value={recordText} onChange={event => setRecordText(event.target.value)} placeholder="{&quot;trustRecordId&quot;: &quot;...&quot;, &quot;trustRecordHash&quot;: &quot;...&quot;, &quot;signature&quot;: {...}}" />
      <label htmlFor="parmana-key">Public key (PEM)</label>
      <textarea id="parmana-key" rows={4} style={box} value={keyText} onChange={event => setKeyText(event.target.value)} placeholder="-----BEGIN PUBLIC KEY-----" />
      <div>
        <button type="button" style={{
    ...button,
    fontWeight: 600
  }} disabled={busy} onClick={() => run(recordText, keyText)}>
          Verify
        </button>
        <button type="button" style={button} onClick={loadExample}>
          Load the example
        </button>
        <button type="button" style={button} onClick={loadChangedExample}>
          Load the example, changed
        </button>
        <button type="button" style={button} onClick={fetchSandboxKey}>
          Use the sandbox key
        </button>
      </div>
      {result && <div role="status" style={{
    marginTop: "12px",
    padding: "12px",
    borderRadius: "8px",
    border: `1px solid ${result.valid ? "rgba(22, 163, 74, 0.6)" : "rgba(220, 38, 38, 0.6)"}`
  }}>
          <strong>
            {result.valid ? "Valid: the hash matches and the signature verifies." : "Not valid."}
          </strong>
          {result.hashValid !== undefined && <ul>
              <li>
                Hash:{" "}
                {result.hashValid ? "matches the content" : "does not match"}
              </li>
              <li>
                Signature:{" "}
                {result.signatureValid ? `verifies (key "${result.keyId}", ${result.signedForm} form)` : "does not verify"}
              </li>
            </ul>}
          {result.errors.length > 0 && <ul>
              {result.errors.map(error => <li key={error}>{error}</li>)}
            </ul>}
          {result.notes.length > 0 && <ul>
              {result.notes.map(note => <li key={note}>{note}</li>)}
            </ul>}
        </div>}
    </div>;
};

export const exampleRecord = {
  trustRecordId: "cross-language-fixture-txn",
  businessTransactionId: "cross-language-fixture-txn",
  transaction: {
    businessTransactionId: "cross-language-fixture-txn",
    status: "RECEIVED",
    createdAt: "2026-01-01T00:00:00.000Z",
    signals: {
      amount: 100,
      vendorId: "vendor-café"
    }
  },
  overrides: [],
  executions: [],
  verifications: [],
  receipts: [],
  createdAt: "2026-01-01T00:00:00.000Z",
  updatedAt: "2026-01-01T00:00:00.000Z",
  trustRecordHash: "dfc3d4d686244a2f79b120eac15886c7472d55a1708d4c71da898a79c9f9238d",
  signature: {
    algorithm: "ed25519",
    keyId: "cross-language-fixture",
    value: "S0dkK028gp/fH9LJjV9Aq8Ap4BR8w22hZHRECQSbOu3FOyGYljbjCBEjKpRkXjmq1LgJQJxRgcZJeYPy6VZTDw==",
    signedAt: "2026-10-05T09:25:09.491Z"
  }
};

export const examplePublicKey = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA8DDY1SMMZC/HT+ec/tMAdxofJVCNTj/FFOVgqQKsUdY=\n-----END PUBLIC KEY-----\n";

A signed Execution Trust Record proves two things to anyone holding the public key: its
content has not changed since it was signed, and it was signed by the holder of that key. This
page checks both, in your browser, with the Web Crypto API. Nothing you paste leaves your
machine. The only network call is **Use the sandbox key**, which fetches the sandbox's public
key.

<RecordVerifier exampleRecord={exampleRecord} examplePublicKey={examplePublicKey} />

## Try it

1. **Load the example**, then **Verify**. The result is valid.
2. **Load the example, changed**: the same record with the amount changed from 100 to 100000.
   **Verify** reports that the hash no longer matches. Changing any signed field has the
   same effect.
3. Check a record of your own. Run the [Playground](/playground) to get a real signed record
   from the public sandbox, paste it, click **Use the sandbox key**, then **Verify**.

The example was signed by Parmana's own signer with a throwaway key made for this page, by
`scripts/generate-offline-verifier-fixture.ts`. The same script produces the fixture that the
Python SDK's offline verifier is tested against.

## What is checked

| Check | How |
| - | - |
| The hash | The signed fields (`trustRecordId`, `businessTransactionId`, `transaction`, `authorization`, `overrides`, `executions`, `createdAt`) are serialized as canonical JSON, with object keys sorted, and hashed with SHA-256. The result must equal `trustRecordHash`. |
| The signature | The `signature` field must be an Ed25519 signature over the same canonical bytes, by the key you paste. A record over 4096 bytes may be signed as a commitment (a fixed prefix and the SHA-512 of the content), which AWS KMS requires; both forms are accepted, as in `@parmana/sign`. |

The code is
[docs/site/snippets/record-verifier.jsx](https://github.com/pavancharak/parmana/blob/main/docs/site/snippets/record-verifier.jsx).
A test in the repository runs that exact code against records signed by Parmana's signer,
including a large record signed as a commitment, and checks that it agrees with Parmana's
offline verifier and rejects changed records.

## What this page does not check

* **Who holds the key.** A valid signature proves the record was signed by that key. Get the
  key from the deployment you trust, for example `GET /keys/default`, not from the person who
  sent you the record.
* **Hybrid signatures.** A record can also carry an ML-DSA-65 signature in a `signatures`
  array. Browsers have no ML-DSA support yet, so this page checks only the Ed25519 signature
  and says so.
* **Other record types.** Execution Intents are signed too; verify them with
  [`@parmana/sign`](/sdks/parmana-sign/overview). Refusal Records are checked with
  `POST /refusal/verify` ([Refusal Records](/concepts/refusal-records)).
* **That the action was authorized.** A signature shows what Parmana recorded and that it was
  not altered. It does not show that nothing bypassed Parmana. See the
  [Audit guide](/evaluation/audit-guide).

For scripted or bulk checks, use [`@parmana/sign`](/sdks/parmana-sign/overview) or
[Verify independently](/guides/verify-independently).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.