@parmana/verifier 1.6.2 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,16 @@ node bin/parmana-verify.mjs test/vectors/record-whole-record-v3.json test/vector
25
25
  ```
26
26
 
27
27
  ```json
28
- {"valid":true,"coverage":"full","errors":[],"checks":{"trustRecordHash":true,"signature":true,"hybridSignatures":true}}
28
+ {
29
+ "valid": true,
30
+ "coverage": "full",
31
+ "errors": [],
32
+ "checks": {
33
+ "trustRecordHash": true,
34
+ "signature": true,
35
+ "hybridSignatures": true
36
+ }
37
+ }
29
38
  ```
30
39
 
31
40
  The exit code is `0`. A record that has been changed is rejected:
@@ -36,6 +45,15 @@ node bin/parmana-verify.mjs test/vectors/record-tampered-amount.json test/vector
36
45
 
37
46
  The exit code is `1`, and the output lists the failed checks. The test vectors are labelled test material; see [test/vectors/README.md](test/vectors/README.md). For real records, pin the keys you trust (see [Pinning a key](#pinning-a-key)).
38
47
 
48
+ On Windows PowerShell, run the same two commands with Windows paths. Use Node 24 or later:
49
+
50
+ ```powershell
51
+ node bin\parmana-verify.mjs test\vectors\record-whole-record-v3.json test\vectors\trust-whole-record.json
52
+ node bin\parmana-verify.mjs test\vectors\record-tampered-amount.json test\vectors\trust.json
53
+ ```
54
+
55
+ The first prints the valid result above and exits with `0`. The second prints `"valid":false` and exits with `1`.
56
+
39
57
  ## Installation and usage
40
58
 
41
59
  The package is published on npm as [`@parmana/verifier`](https://www.npmjs.com/package/@parmana/verifier). Install it, or run it without installing:
@@ -104,6 +122,15 @@ A key is used only if all of these hold:
104
122
 
105
123
  A compromised key is rejected whatever the timestamp, so signatures it made stay void.
106
124
 
125
+ ### Records with no hybrid envelope
126
+
127
+ A record with no `signatures` array is a legacy-only record. The verifier refuses it by default. It is accepted only when:
128
+
129
+ - the trust file sets `hybridRequiredFrom` and the record was created before that instant; or
130
+ - the trust file sets `"allowLegacyOnly": true` and does not set `hybridRequiredFrom`.
131
+
132
+ Otherwise the result is invalid, and the error says which setting would accept it. A record created at or after `hybridRequiredFrom` always needs a valid hybrid envelope, whatever `allowLegacyOnly` says. See [docs/FORMAT.md](docs/FORMAT.md#trust-set-trustjson).
133
+
107
134
  ## Examples
108
135
 
109
136
  Run every example with:
@@ -141,7 +168,7 @@ The suite covers the legacy signature and hash, hybrid signatures, schema versio
141
168
  - 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.
142
169
  - For records with `schemaVersion` 2, or with no `schemaVersion`, only the fields listed in [docs/FORMAT.md](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.
143
170
  - For records with `schemaVersion` 3, every field except the signature envelope, `verifications` and `receipts` is protected. Check that `coverage` is `full` when that matters.
144
- - A record with `schemaVersion` 2 can be downgraded by someone who can write to storage, since `schemaVersion` is unsigned. Set `hybridRequiredFrom` in the trust file to reject downgraded records created after a cutover. Records before the cutover stay open to this.
171
+ - A record with `schemaVersion` 2 can be downgraded by someone who can write to storage, since `schemaVersion` is unsigned. Set `hybridRequiredFrom` in 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 sets `allowLegacyOnly` to `true` (see "Records with no hybrid envelope" above).
145
172
  - The validity window is checked against the record's `createdAt`, not against the signing time. A record that carries a false `createdAt` is checked against the window for that false time.
146
173
  - 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.
147
174
  - It does not verify the policy the record cites, the authorization decision logic, or the state of any connector or external system.
@@ -166,9 +193,12 @@ Every report is listed once, here. Start with the question you have.
166
193
 
167
194
  **Releases**
168
195
 
196
+ - **Published versions:** `@parmana/verifier` 1.6.2 on npm and `parmana-verifier` 1.6.2 on PyPI, from release `verifier-v1.6.2`.
169
197
  - **Changelog:** [CHANGELOG.md](CHANGELOG.md) lists each change to the record formats and the command-line interface.
198
+ - **Release process:** [RELEASING.md](RELEASING.md) describes how a release is built, checked and published.
170
199
 
171
200
  ---
201
+
172
202
  ## Read the Parmana blog
173
203
 
174
204
  The [Parmana blog](https://blog.parmanasystems.com) explains how the verifier fits into the wider product, in plain terms. Read the post for your role:
@@ -183,16 +213,20 @@ New posts are published regularly, so check the blog for updates.
183
213
 
184
214
  This is version 1.6.2, released with the SDK packages. Each change to the record formats or the command-line interface is listed in [CHANGELOG.md](CHANGELOG.md).
185
215
 
216
+ 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](RELEASING.md) for the steps.
217
+
218
+ What this version does not do is listed under [Limitations](#limitations).
219
+
186
220
  ## Repository layout
187
221
 
188
- | Path | Contents |
189
- | ------------ | ------------------------------------------------------------------------ |
190
- | `bin/` | The `parmana-verify` command-line entry point |
191
- | `src/` | The checks: canonical JSON, hashing, signatures, trust rules |
192
- | `test/` | The test suite, and `test/vectors/`, the labelled test records |
193
- | `examples/` | Nine runnable examples, run by `npm run examples` |
194
- | `docs/` | `FORMAT.md` (the input and output rules) and `SECURITY-REVIEW.md` |
195
- | `reports/` | Pointer to the reports index in this README (see Reports and evidence) |
222
+ | Path | Contents |
223
+ | ----------- | ---------------------------------------------------------------------- |
224
+ | `bin/` | The `parmana-verify` command-line entry point |
225
+ | `src/` | The checks: canonical JSON, hashing, signatures, trust rules |
226
+ | `test/` | The test suite, and `test/vectors/`, the labelled test records |
227
+ | `examples/` | Nine runnable examples, run by `npm run examples` |
228
+ | `docs/` | `FORMAT.md` (the input and output rules) and `SECURITY-REVIEW.md` |
229
+ | `reports/` | Pointer to the reports index in this README (see Reports and evidence) |
196
230
 
197
231
  ## Security
198
232
 
@@ -46,7 +46,7 @@ try {
46
46
  process.exit(2);
47
47
  }
48
48
 
49
- const result = verifyExecutionTrustRecord(proof, policy.keys, { hybridRequiredFrom: policy.hybridRequiredFrom ?? undefined });
49
+ const result = verifyExecutionTrustRecord(proof, policy.keys, { hybridRequiredFrom: policy.hybridRequiredFrom ?? undefined, allowLegacyOnly: policy.allowLegacyOnly });
50
50
 
51
51
  console.log(JSON.stringify(result));
52
52
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parmana/verifier",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Offline verifier for Parmana Execution Trust Records.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/trust.mjs CHANGED
@@ -47,6 +47,14 @@ export function parseTrustPolicy(document) {
47
47
  hybridRequiredFrom = document.hybridRequiredFrom;
48
48
  }
49
49
 
50
+ // Off by default: a record with no hybrid envelope is refused unless the
51
+ // trust file explicitly allows legacy-only records.
52
+ if (document.allowLegacyOnly !== undefined && typeof document.allowLegacyOnly !== "boolean") {
53
+ throw new Error('allowLegacyOnly must be true or false');
54
+ }
55
+
56
+ const allowLegacyOnly = document.allowLegacyOnly === true;
57
+
50
58
  const trustKeys = new Map();
51
59
 
52
60
  for (const [index, entry] of document.keys.entries()) {
@@ -100,5 +108,5 @@ export function parseTrustPolicy(document) {
100
108
  });
101
109
  }
102
110
 
103
- return { keys: trustKeys, hybridRequiredFrom };
111
+ return { keys: trustKeys, hybridRequiredFrom, allowLegacyOnly };
104
112
  }
package/src/verify.mjs CHANGED
@@ -116,16 +116,30 @@ export function verifyExecutionTrustRecord(record, trustKeys, options = {}) {
116
116
  checks.hybridSignatures = false;
117
117
  }
118
118
 
119
- const cutoverOk = !isAtOrAfterCutover(record.createdAt, options.hybridRequiredFrom) || checks.hybridSignatures === true;
120
-
121
- if (!cutoverOk) {
119
+ // A record with no hybrid envelope is accepted only when the caller allows
120
+ // it: explicitly (allowLegacyOnly), or because it predates a cutover the
121
+ // caller supplied. Otherwise it is refused, so a stripped envelope never
122
+ // passes by default.
123
+ const cutoverGiven = options.hybridRequiredFrom !== undefined && options.hybridRequiredFrom !== null;
124
+ const predatesCutover = cutoverGiven && !isAtOrAfterCutover(record.createdAt, options.hybridRequiredFrom);
125
+ // With a cutover, the cutover alone decides; allowLegacyOnly applies only when none is given.
126
+ const legacyOnlyAllowed = cutoverGiven ? predatesCutover : options.allowLegacyOnly === true;
127
+
128
+ // A record created at or after the cutover needs a valid envelope, whether it
129
+ // is missing or present but invalid.
130
+ const atOrAfterCutover = cutoverGiven && !predatesCutover;
131
+ if (atOrAfterCutover && checks.hybridSignatures !== true) {
122
132
  errors.push(`record created at or after hybridRequiredFrom (${options.hybridRequiredFrom}) has no valid hybrid envelope.`);
133
+ } else if (checks.hybridSignatures === undefined && !legacyOnlyAllowed) {
134
+ errors.push("record has no hybrid envelope: set hybridRequiredFrom or allowLegacyOnly to accept a legacy-only record.");
123
135
  }
124
136
 
137
+ const envelopeOk = checks.hybridSignatures === undefined ? legacyOnlyAllowed : checks.hybridSignatures;
138
+
125
139
  const coverage = schemaVersion === WHOLE_RECORD_SCHEMA_VERSION && checks.hybridSignatures === true ? "full" : "legacy";
126
140
 
127
141
  return {
128
- valid: hashValid && signatureValid && (checks.hybridSignatures ?? true) && cutoverOk,
142
+ valid: hashValid && signatureValid && envelopeOk,
129
143
  coverage,
130
144
  errors,
131
145
  checks,