Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/CLAIMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1682,6 +1682,8 @@ Independently checkable at the time of this writing: the repository carries an O

**Update (2026-09-11):** independently re-verified via `gh api repos/pavancharak/parmana-sign` as part of that day's PQC production-readiness audit (`docs/VERIFICATION-GAPS.md` gap 51) -- still real, public, Apache-2.0, actively pushed to. Its own README confirms this section's description exactly (canonical serialization, `Dilithium3SignatureProvider`, no key management, no policy logic). The hybrid-envelope gap above still holds: a third party wanting to fully verify a hybrid-signed Execution Trust Record today should use this repository's own `packages/crypto/src/OfflineVerifier.ts` (or its Python counterpart, `python/parmana/crypto/offline_verifier.py`), not `@parmana/sign`, until that external package is separately updated to recognize the `signatures` array -- work this repository's own build cannot perform.

**Update (2026-10-03):** `@parmana/sign` 0.2.0 is published on npm (`npm view @parmana/sign version` returns `0.2.0`; tarball shasum `84f2cdb4…` matches the tarball attached to the signed `v0.2.0` GitHub release, which carries SLSA provenance and a Sigstore bundle; this npm publish has no npm provenance attestation). It now ships `Ed25519SignatureProvider` (including the large-message KMS commitment in `SignatureCommitment.ts`), this repository's Execution Trust Record and Execution Intent canonical field mappings, and `verifyExecutionTrustRecordOffline` / `verifyExecutionIntentOffline`, which also check the hybrid `signatures`/`schemaVersion` envelope. Its compatibility tests (`parmana-compat.test.ts` in that repository) verify fixtures signed by this repository's own `packages/crypto` code (legacy, hybrid, large KMS-committed, large raw, and an Execution Intent), regenerated by that repository's fixture script from a checkout of this one. The hybrid-envelope gap described above is closed as of that version. Same evidentiary footing as the rest of this section: an external repository, not proven by this repository's `npm test`.

Evidence

- `github.com/pavancharak/parmana-sign` (external repository; README, badge row, `LICENSE`)
Expand All @@ -1702,6 +1704,8 @@ The secondary (ML-DSA-65) key lives at a distinct keyId, `default-secondary` (`D

**Required caveat, load-bearing:** `@parmana/sign` (3.12), the public SDK used for independent third-party verification, does not yet recognize the `signatures`/`schemaVersion` envelope shape. Today, a third party verifying a hybrid-signed record through `@parmana/sign` checks the legacy `signature` field only — that check is genuinely correct, not a false pass, since the legacy field remains a real, valid Ed25519 signature over the record. But it is a partial verification: it does not check the ML-DSA-65 signature, and therefore does not confirm the full hybrid guarantee this capability is designed to eventually provide. Updating `@parmana/sign` to recognize the new envelope shape is untracked, separate follow-on work, not part of this capability and not a dependency of it (see 3.12).

**Update (2026-10-03):** the caveat above is resolved for `@parmana/sign` 0.2.0 and later (3.12): `verifyExecutionTrustRecordOffline` checks every entry in the `signatures` array as well as the legacy field, and reports `hybridSignaturesValid` separately, so a third party can confirm the full hybrid guarantee through the public package. Earlier versions of `@parmana/sign` still check the legacy field only.

Evidence

- `packages/crypto/src/HybridSignatureProvider.ts`
Expand Down Expand Up @@ -2057,8 +2061,6 @@ The following claims are planned but are intentionally withheld until supported

- [FUTURE] `CRYPTO_MODE=hybrid` running anywhere in staging or production: the capability described in 3.13 is built and tested but not wired into any deployed environment. `PRIMARY_SIGNATURE_PROVIDER=ed25519` alone remains the configuration everywhere this codebase currently runs, including `parmana-api-live.fly.dev` (3.9).

- [FUTURE] `@parmana/sign` recognizing the hybrid `signatures`/`schemaVersion` envelope shape (3.13's own caveat): third-party verification of a hybrid-signed record via the public SDK today checks only the legacy Ed25519 `signature` field, a genuine but partial verification. No work on `@parmana/sign` itself is in scope of this repository.

These claims will be promoted to the Supported Technical Claims section only after the required evidence is complete.

---
Expand Down
11 changes: 11 additions & 0 deletions docs/VERIFICATION-GAPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,13 @@ Full verification: `npx tsc -b` clean; `npx eslint . --ext .ts` clean (repo-wide

**Not fixed, by design, per this remediation's own explicit scope:** a Go reference verifier (the original remediation plan's TASK 1 as drafted) was not built -- this repository has no Go anywhere, and introducing an entire second language toolchain for one CLI was judged a real scope increase beyond "fix gaps, no redesign," confirmed with the user before starting. Python was used instead, since it already exists as a first-class SDK language here. Whether to also update the external `@parmana/sign` package (a separate, real, published, admin-accessible repository) was raised but deliberately left as a distinct decision, not folded into this remediation.

**Update (2026-10-03):** the external-package half of this gap is now done. `@parmana/sign`
0.2.0 (published on npm) ships the Execution Trust Record field mapping, the hybrid
`signatures`/`schemaVersion` envelope and `verifyExecutionTrustRecordOffline`, tested
against fixtures signed by this repository's `packages/crypto` code. The canonical
serializer parity fix for literal `"__proto__"` keys landed here in
pavancharak/AgentLabsBuildathon#128.

---

## Gaps closed in the 2026-09-14 execution-audit-trail hardening session
Expand Down Expand Up @@ -2416,6 +2423,10 @@ remains opt-in, not the production default (`parmana-api-live.fly.dev` still run
claim: it does not say hybrid signing runs in staging or production anywhere, because it
doesn't yet.

**Update (2026-10-03):** `@parmana/sign` 0.2.0 recognizes the `signatures` envelope and
checks every entry, so the "Required caveat" in 3.13 no longer applies to that version and
later (see `docs/CLAIMS.md` 3.12 and 3.13 updates of the same date).

**Decision still required for the remaining surfaces, see below** (D-2's Option A/B choice
was written before this partial closure and should be re-read as applying only to the
signing paths listed as unwired above).
Expand Down
17 changes: 17 additions & 0 deletions docs/site/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,25 @@ product version). The SDKs are versioned independently on their own registries,
and per-version notes. This page covers changes that affect what you can build against, not
every commit, run `git log` in the repository for the complete record.

## 2026-10-03

**Added: the Verification SDK (`@parmana/sign` 0.2.0)**

- [`@parmana/sign`](/sdks/parmana-sign/overview) 0.2.0 is on npm. It verifies Execution Trust
Records and Execution Intents offline, with only the record and the public keys, including
hybrid (Ed25519 and ML-DSA-65) records and large records signed through AWS KMS.
- New **Verification SDK** tab: overview and quick start, public keys, a reference page for
each verifier and for the signing primitives, and every error message with what to do.

## 2026-10-02

**Fixed: canonical JSON keeps `"__proto__"` keys**

- The server's canonical serializer and the TypeScript SDK's `canonicalSerialize` silently
dropped an object key named `"__proto__"`, so its content was not covered by the hash or
signature. Both now keep it, matching the Python SDK and `@parmana/sign`. Output for data
without that key is unchanged, so existing signatures stay valid.

**Sandbox: visitor data deleted, retention paused**

- Every request and record visitors had sent to the public sandbox was deleted around 03:30 UTC, around the
Expand Down
21 changes: 21 additions & 0 deletions docs/site/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,27 @@
}
]
},
{
"tab": "Verification SDK",
"groups": [
{
"group": "Get started",
"pages": [
"sdks/parmana-sign/overview",
"sdks/parmana-sign/public-keys"
]
},
{
"group": "API reference",
"pages": [
"sdks/parmana-sign/verify-trust-record",
"sdks/parmana-sign/verify-execution-intent",
"sdks/parmana-sign/signing-primitives",
"sdks/parmana-sign/results-and-errors"
]
}
]
},
{
"tab": "Connector SDKs",
"groups": [
Expand Down
9 changes: 5 additions & 4 deletions docs/site/handbook/18-independent-verification.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ algorithm used isn't itself recorded on the artifact separately from the hash va
The comment also names a real relationship worth knowing: this module is the reference a
separately published, independently maintained package,
`@parmana/sign` (`github.com/pavancharak/parmana-sign`, see `docs/CLAIMS.md` §3.12), is built
on, that external package ships the lower-level primitives (canonical serialization, sign/verify)
this module also uses, but does not yet know the Execution-Trust-Record-specific canonical field
mapping or the hybrid envelope shape. Syncing that into the external package is separate work,
not something this repository's own build performs.
on. Since version 0.2.0 that package also ships this module's Execution Trust Record and
Execution Intent field mappings, the hybrid envelope, and the Ed25519 large-message
commitment, as `verifyExecutionTrustRecordOffline` and `verifyExecutionIntentOffline`. Its
compatibility tests run against records signed by this repository's own code, so a third
party can verify Parmana records with `npm install @parmana/sign` alone.

### Large records signed under AWS KMS

Expand Down
14 changes: 6 additions & 8 deletions docs/site/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8187,14 +8187,12 @@ algorithm at once, with verification requiring both signatures independently, fa
This is a built, tested capability (`CRYPTO_MODE=hybrid` is real, validated
configuration), but it is opt-in and not running anywhere today: every deployed
environment signs Ed25519 alone.
**Caveat:** `@parmana/sign`, the public verification SDK, does not yet recognize the
hybrid envelope shape; a third party verifying a hybrid-signed record through it today
checks the legacy Ed25519 signature only, a genuine but partial verification, not the
full hybrid guarantee. This repository's own `packages/crypto/src/OfflineVerifier.ts`
(TypeScript) and `python/parmana/crypto/offline_verifier.py` (Python, Ed25519 only)
do recognize the hybrid envelope and check both signatures independently, offline,
with zero network access. Use these for a full hybrid check today, not `@parmana/sign`,
until that separate, external package is updated.
**Third-party verification:** `@parmana/sign` 0.2.0 and later, the public verification
SDK on npm, recognizes the hybrid envelope: `verifyExecutionTrustRecordOffline` checks
the record hash, the legacy Ed25519 signature, and every entry in `signatures`, offline,
with only the record and the public keys. This repository's own
`packages/crypto/src/OfflineVerifier.ts` (TypeScript) does the same, and
`python/parmana/crypto/offline_verifier.py` (Python) checks Ed25519 only.

**Hybrid-signature downgrade resistance, opt-in.** A genuinely hybrid-signed record's
`signatures` array (the ML-DSA-65 half) can be stripped entirely with no cryptographic
Expand Down
4 changes: 4 additions & 0 deletions docs/site/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ This file is generated from the documentation navigation. Do not edit it by hand

* [Python SDK for AI Agents](https://docs.parmanasystems.com/guides/python-sdk-ai-agents): Integrate Parmana authorization into LangChain, CrewAI, FastAPI, and async agent code. Policy decides, not the agent.
* [Python SDK Production Guide](https://docs.parmanasystems.com/guides/python-sdk-production): Deploy the Python SDK to production: real error handling, audit logging, and a deployment checklist.
## Verification SDK: Get started

* [Verification SDK](https://docs.parmanasystems.com/sdks/parmana-sign/overview): Verify Parmana records yourself, offline, with @parmana/sign.
* [Public keys](https://docs.parmanasystems.com/sdks/parmana-sign/public-keys): Where verification keys come from, and how to pass them in.
## Connector SDKs: Overview

* [Integrations](https://docs.parmanasystems.com/integrations/overview): The Connector SDK foundation is real. Four enterprise-named connectors exist as explicit, self-documented mocks; no real enterprise integration is built on it yet.
Expand Down
112 changes: 112 additions & 0 deletions docs/site/sdks/parmana-sign/overview.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: "Verification SDK"
description: "Verify Parmana records yourself, offline, with @parmana/sign."
---

<Info>
**[AVAILABLE]**, **published**: [`@parmana/sign` on
npm](https://www.npmjs.com/package/@parmana/sign), version 0.2.0, Apache
License 2.0. Source:
[github.com/pavancharak/parmana-sign](https://github.com/pavancharak/parmana-sign).
</Info>

`@parmana/sign` checks a signed Parmana record using only the record and Parmana's public
keys. It makes no network calls, reads no files or environment variables, and needs no
Parmana account. An auditor, a regulator or a counterparty can run it without trusting
Parmana's servers or database.

It checks three things, and reports each one separately:

1. The record's hash matches its content.
2. The record's Ed25519 signature is valid for the named key.
3. If the record is hybrid signed, every entry in its `signatures` array (Ed25519 and
ML-DSA-65) is valid.

Changing any signed field, even one number, fails the hash and every signature.

## Install

```bash
npm install @parmana/sign
```

Requires Node.js 24.6.0 or later. ML-DSA-65 support in `node:crypto` starts at 24.6.0.

## Quick start

<Steps>
<Step title="Get the record">
Fetch it from the API, or use one exported from Parmana as JSON.

```bash
curl -s -H "Authorization: Bearer $PARMANA_API_KEY" \
https://parmana-sandbox.vercel.app/trust-records/$BUSINESS_TRANSACTION_ID > record.json
```

</Step>
<Step title="Get the public key">
The record names its key in `signature.keyId`. Public keys need no API key.

```bash
curl -s https://parmana-sandbox.vercel.app/keys/default > key.json
```

See [Public keys](/sdks/parmana-sign/public-keys) for hybrid records and caching.

</Step>
<Step title="Verify">
```ts verify.mjs
import { readFileSync } from "node:fs";
import { verifyExecutionTrustRecordOffline } from "@parmana/sign";

const record = JSON.parse(readFileSync("record.json", "utf8"));
const key = JSON.parse(readFileSync("key.json", "utf8"));

const result = await verifyExecutionTrustRecordOffline(record, {
[key.keyId]: key.pem,
});

console.log(result.valid ? "valid" : result.errors);
```

```bash
node verify.mjs
```

</Step>
</Steps>

Records from the sandbox only verify against the sandbox's key, and production records only
against production's key.

## What you can verify

| Record | Function | Reference |
| ---------------------------- | ---------------------------------------- | ------------------------------------------------------------------------ |
| Execution Trust Record | `verifyExecutionTrustRecordOffline` | [Verify a Trust Record](/sdks/parmana-sign/verify-trust-record) |
| Execution Intent | `verifyExecutionIntentOffline` | [Verify an Execution Intent](/sdks/parmana-sign/verify-execution-intent) |
| Any object you sign yourself | `SignatureVerifier`, signature providers | [Signing primitives](/sdks/parmana-sign/signing-primitives) |

## How it relates to the other SDKs

The [TypeScript SDK](/sdks/typescript) (`@parmana/sdk`) calls the Parmana API and also
includes offline verifiers. `@parmana/sign` is the standalone verifier: no API client,
no dependency on any other Parmana package, and an open source license. Use it when the
party verifying a record should not depend on Parmana's own client code.

Both use the same field mappings as the server's signer. `@parmana/sign` is tested against
records signed by the server's own signing code, including hybrid records and large records
signed through AWS KMS.

## Versioning and support

- Follows [Semantic Versioning](https://semver.org). Below 1.0.0, a minor release may
include breaking changes, always listed in the
[changelog](https://github.com/pavancharak/parmana-sign/blob/main/CHANGELOG.md).
- Supported Node.js versions: 24.6.0 and later, on Linux, Windows and macOS.
- Releases carry SLSA provenance and a Sigstore signature on the
[GitHub release](https://github.com/pavancharak/parmana-sign/releases). See
[RELEASING.md](https://github.com/pavancharak/parmana-sign/blob/main/RELEASING.md) to check them.
- Report bugs in [GitHub issues](https://github.com/pavancharak/parmana-sign/issues), and
security issues as described in its
[SECURITY.md](https://github.com/pavancharak/parmana-sign/blob/main/SECURITY.md).
73 changes: 73 additions & 0 deletions docs/site/sdks/parmana-sign/public-keys.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: "Public keys"
description: "Where verification keys come from, and how to pass them in."
---

Every signature names the key that made it, in its `keyId`. Verification needs the public
half of each named key. Parmana serves them without authentication.

## Fetch a key

```bash
curl -s https://parmana-sandbox.vercel.app/keys/default
```

```json Response
{
"keyId": "default",
"algorithm": "ed25519",
"use": "sig",
"pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAQKyaz9ifANjaew96i34spqhM31mBatJ6TTHaMY7mclI=\n-----END PUBLIC KEY-----\n",
"jwk": {
"crv": "Ed25519",
"x": "QKyaz9ifANjaew96i34spqhM31mBatJ6TTHaMY7mclI"
}
}
```

Pass the `pem` field to the verifiers, keyed by `keyId`:

```ts
const key = await fetch("https://parmana-sandbox.vercel.app/keys/default").then(
(r) => r.json(),
);

const publicKeys = { [key.keyId]: key.pem };
```

`GET /.well-known/jwks.json` lists every key the server holds, as JWKs.

## Which keys a record needs

Collect every `keyId` the record names:

```ts
function keyIdsOf(record) {
const ids = new Set([record.signature.keyId]);
for (const entry of record.signatures ?? []) ids.add(entry.keyId);
return [...ids];
}

const publicKeys = {};
for (const keyId of keyIdsOf(record)) {
const key = await fetch(
`https://parmana-sandbox.vercel.app/keys/${keyId}`,
).then((r) => r.json());
publicKeys[keyId] = key.pem;
}
```

A hybrid record typically names `default` (Ed25519) and `default-secondary` (ML-DSA-65).

## Keys in production use

- **Fetch once, then pin.** Store the keys you trust and pass them in from storage. Fetching
a key from the same server at verification time only proves the record matches that
server's current key.
- **Rotated keys stay valid.** Rotation signs new records under a new `keyId`. The server
keeps serving the old key while it holds it, and older records still name the old
`keyId`, so they remain verifiable. Keep every key you have pinned.
- **Sandbox and production are separate.** Each has its own keys; a record verifies only
against the environment that signed it.
- **A `KeyObject` works too.** `publicKeys` values may be `node:crypto` `KeyObject`s instead
of PEM strings.
Loading
Loading