@ziffer-io/verify 0.1.0
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/LICENSE +120 -0
- package/README.md +121 -0
- package/THIRD-PARTY-NOTICES +40 -0
- package/dist/canon.d.ts +61 -0
- package/dist/canon.d.ts.map +1 -0
- package/dist/canon.js +147 -0
- package/dist/canon.js.map +1 -0
- package/dist/clauses.d.ts +53 -0
- package/dist/clauses.d.ts.map +1 -0
- package/dist/clauses.js +53 -0
- package/dist/clauses.js.map +1 -0
- package/dist/ed25519.d.ts +51 -0
- package/dist/ed25519.d.ts.map +1 -0
- package/dist/ed25519.js +81 -0
- package/dist/ed25519.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/instant.d.ts +25 -0
- package/dist/instant.d.ts.map +1 -0
- package/dist/instant.js +104 -0
- package/dist/instant.js.map +1 -0
- package/dist/refusal.d.ts +16 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +20 -0
- package/dist/refusal.js.map +1 -0
- package/dist/suite.d.ts +56 -0
- package/dist/suite.d.ts.map +1 -0
- package/dist/suite.js +85 -0
- package/dist/suite.js.map +1 -0
- package/dist/testkeys.d.ts +20 -0
- package/dist/testkeys.d.ts.map +1 -0
- package/dist/testkeys.js +28 -0
- package/dist/testkeys.js.map +1 -0
- package/dist/verify.d.ts +123 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +338 -0
- package/dist/verify.js.map +1 -0
- package/package.json +57 -0
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The §9.3 stateless receipt gate -- the THIRD implementation, after the
|
|
3
|
+
* Python reference (`acp_executor.py`) and Rust (`crates/acp-decision`), both
|
|
4
|
+
* at the engine pin. Composed in the order `decide.rs` states, because on an
|
|
5
|
+
* input carrying more than one defect the ORDER decides which clause fires,
|
|
6
|
+
* and the clause is what a refusal tells an operator.
|
|
7
|
+
*
|
|
8
|
+
* # What is here, and what is NOT -- read this before trusting a green run
|
|
9
|
+
*
|
|
10
|
+
* This function covers exactly the checklist portion runnable with only
|
|
11
|
+
* receipt + proposal + identity (the ACP-197 runbook's stateless set):
|
|
12
|
+
*
|
|
13
|
+
* | step | rule | here |
|
|
14
|
+
* | --- | --- | --- |
|
|
15
|
+
* | -- | AB-0 receipt version, AB-5 signed-body exclusion, AB-6 byte cap | yes |
|
|
16
|
+
* | 1-2 | CR-1 / CR-4 / CR-3 conjunctive, then `decision` | yes |
|
|
17
|
+
* | 3 | `proposal_hash` recomputed from the caller's own bytes (B-1a) | yes |
|
|
18
|
+
* | 4 | policy basis (bundle hash, epoch) | NO -- needs the verifier's bundle |
|
|
19
|
+
* | 5 | temporal position (9.3-5) and the L-14 window ceiling | yes |
|
|
20
|
+
* | 6 | nonce WE-4 type and L-17 size | yes; the CL-2 single-use CLAIM is ledger state |
|
|
21
|
+
* | 7 | TR-8 / RV-3 recomputation | NO -- needs the signed policy |
|
|
22
|
+
* | 7b | the AT-* quorum, AB-1..AB-4 | NO -- needs the attester registry |
|
|
23
|
+
* | 8 | tenant scoping | NO -- needs the bundle's tenant |
|
|
24
|
+
* | 9-10 | capability recheck, delivery identity | NO -- absent in the engine too |
|
|
25
|
+
*
|
|
26
|
+
* An absent step is a DIVERGENCE, not an agreement: a receipt this function
|
|
27
|
+
* passes may still be refused by a full Executor at any absent step. A pass
|
|
28
|
+
* here means "the stateless set found nothing", never "the receipt may be
|
|
29
|
+
* consumed".
|
|
30
|
+
*
|
|
31
|
+
* # R / B / T classification (suite 12) for the inputs this module reads
|
|
32
|
+
*
|
|
33
|
+
* | input | class | why |
|
|
34
|
+
* | --- | :---: | --- |
|
|
35
|
+
* | `identity` (keys, floor) | R | the verifier's own configuration, never the message's |
|
|
36
|
+
* | signed bytes | R | canonicalised HERE from the receipt body (RES-9) |
|
|
37
|
+
* | `proposal_hash` | R | recomputed from `proposalBytes`, the caller's own copy; the receipt's field is compared, never used |
|
|
38
|
+
* | `alg`, `sig`, `decision`, temporal fields, `nonce`, `receipt_version` | B | inside (or selecting) the signed body; each is read only in a position where reading it can at most cause a refusal |
|
|
39
|
+
* | `nowUnixSeconds` | T | the caller's clock. Nothing here can check it; a stale or forward value silently voids the expiry and skew checks (the engine's residual 3, verbatim). Only L-14 survives such a caller, because it compares the receipt's two instants against each other |
|
|
40
|
+
*/
|
|
41
|
+
/** AB-0 (§8.6b): version 3, the digest-bound form, and NO other. A verifier
|
|
42
|
+
* accepting both 2 and 3 lets an attacker present the weaker one -- the CR-4
|
|
43
|
+
* downgrade shape with the format negotiated by the party under verification. */
|
|
44
|
+
export declare const RECEIPT_VERSION = 3;
|
|
45
|
+
/** AB-5: the signed body is the transport object MINUS these keys. ONE list --
|
|
46
|
+
* a second exclusion site would be a second definition of "the signed body". */
|
|
47
|
+
export declare const SIGNED_EXCLUDE: readonly string[];
|
|
48
|
+
/** AB-6: the KMS RAW cap; a bigger body could only come from a software key,
|
|
49
|
+
* so accepting one admits a custody downgrade silently. Counted in BYTES. */
|
|
50
|
+
export declare const RECEIPT_MAX_SIGNED_BYTES = 4096;
|
|
51
|
+
/** Step 5's tolerated clock skew, seconds -- the reference's `iat > now + 5`. */
|
|
52
|
+
export declare const CLOCK_SKEW_SECS = 5;
|
|
53
|
+
/** L-14's ceiling on a receipt's validity window, seconds. */
|
|
54
|
+
export declare const MAX_VALIDITY_WINDOW_SECS = 120;
|
|
55
|
+
/** FIPS 204 ML-DSA-65 public key length. */
|
|
56
|
+
export declare const MLDSA65_PK_LEN = 1952;
|
|
57
|
+
/** FIPS 204 ML-DSA-65 signature length. */
|
|
58
|
+
export declare const MLDSA65_SIG_LEN = 3309;
|
|
59
|
+
/** The one wire nonce size, 128-bit (`Nonce128`, cited by AT-1 and L-17). */
|
|
60
|
+
export declare const NONCE128_BYTES = 16;
|
|
61
|
+
/** `"b64:"` plus base64 of 16 bytes: 4 + 24. Counted, never decoded -- a rule
|
|
62
|
+
* whose behaviour on bad input is "throw" is not a refusal. */
|
|
63
|
+
export declare const NONCE128_LEN: number;
|
|
64
|
+
/**
|
|
65
|
+
* The verifier's own trust anchor: the receipt signing keys from the SIGNED
|
|
66
|
+
* bundle (`receipt_identity`, PB-12) and the bundle's suite floor
|
|
67
|
+
* (`manifest.min_suite`, CR-4).
|
|
68
|
+
*
|
|
69
|
+
* Both key halves, because hybrid composition is conjunctive (CR-3): a
|
|
70
|
+
* verifier holding only the classical key could not tell a stripped
|
|
71
|
+
* post-quantum leg from a suite that never had one. Every field is the
|
|
72
|
+
* VERIFIER's configuration -- reading any of it from the receipt would be
|
|
73
|
+
* RES-8, the five-times-recurred defect class this protocol exists to close.
|
|
74
|
+
*/
|
|
75
|
+
export interface TrustAnchor {
|
|
76
|
+
/** Ed25519 verification key, 32 raw bytes. */
|
|
77
|
+
readonly classical: Uint8Array;
|
|
78
|
+
/** ML-DSA-65 verification key, 1,952 raw bytes. */
|
|
79
|
+
readonly pq: Uint8Array;
|
|
80
|
+
/** CR-4 floor, by wire suite name, from the signed manifest. */
|
|
81
|
+
readonly minSuite: string;
|
|
82
|
+
}
|
|
83
|
+
/** What the stateless set concluded, when it concluded anything. */
|
|
84
|
+
export interface Verified {
|
|
85
|
+
/** This verifier's OWN canonical hash of the proposal -- the value the
|
|
86
|
+
* receipt's claim was compared against, never the claim itself (R). */
|
|
87
|
+
readonly proposalHash: string;
|
|
88
|
+
/** The receipt's `expires_at` as step 5 validated it, epoch seconds (B). */
|
|
89
|
+
readonly receiptExpiresAt: number;
|
|
90
|
+
}
|
|
91
|
+
/** Options for {@link verifyReceipt}. */
|
|
92
|
+
export interface VerifyOptions {
|
|
93
|
+
/**
|
|
94
|
+
* The verifier's clock, epoch seconds. Defaults to this process's
|
|
95
|
+
* `Date.now()`. A parameter for the reason `decide()` makes it one --
|
|
96
|
+
* a verifier taking its notion of the present from the party it verifies
|
|
97
|
+
* would be RES-8 with a clock -- and classified T either way (residual 3).
|
|
98
|
+
*/
|
|
99
|
+
readonly nowUnixSeconds?: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* WE-4: is this the ASCII string `b64:` followed by RFC 4648 §4 base64, WITH
|
|
103
|
+
* padding? Ported from `quorum.rs::is_we4_b64` -- the body is whole
|
|
104
|
+
* four-character groups, the alphabet is §4's (`+` and `/`, never §5's `-` and
|
|
105
|
+
* `_`), and `=` appears only as one or two characters at the very end.
|
|
106
|
+
* Rejected, never normalised: normalising would make this verifier accept two
|
|
107
|
+
* spellings of one nonce, which is how one nonce comes to hold two ledger
|
|
108
|
+
* slots (ACP-87).
|
|
109
|
+
*/
|
|
110
|
+
export declare function isWe4B64(s: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Run the stateless half of §9.3 in specification order over one receipt.
|
|
113
|
+
*
|
|
114
|
+
* @param receiptJson the receipt as parsed JSON (`unknown`: this module does
|
|
115
|
+
* its own narrowing and refuses by name, never casts).
|
|
116
|
+
* @param proposalBytes the Proposal THE CALLER received, as bytes. The hash is
|
|
117
|
+
* recomputed from these -- a transmitted identifier is a
|
|
118
|
+
* name for a binding, not evidence of one (RES-9/TR-10).
|
|
119
|
+
* @param identity the verifier's trust anchor (keys + floor), out-of-band.
|
|
120
|
+
* @throws Refusal with `clause` spelled exactly as the engine's constants.
|
|
121
|
+
*/
|
|
122
|
+
export declare function verifyReceipt(receiptJson: unknown, proposalBytes: Uint8Array, identity: TrustAnchor, opts?: VerifyOptions): Verified;
|
|
123
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAkCH;;iFAEiF;AACjF,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC;gFACgF;AAChF,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA4B,CAAC;AAEzE;6EAC6E;AAC7E,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C,iFAAiF;AACjF,eAAO,MAAM,eAAe,IAAI,CAAC;AAEjC,8DAA8D;AAC9D,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,4CAA4C;AAC5C,eAAO,MAAM,cAAc,OAAO,CAAC;AACnC,2CAA2C;AAC3C,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,6EAA6E;AAC7E,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC;+DAC+D;AAC/D,eAAO,MAAM,YAAY,QAAwC,CAAC;AAElE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,mDAAmD;IACnD,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;IACxB,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,oEAAoE;AACpE,MAAM,WAAW,QAAQ;IACvB;2EACuE;IACvE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,4EAA4E;IAC5E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,yCAAyC;AACzC,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAoCD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAc3C;AAyHD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,OAAO,EACpB,aAAa,EAAE,UAAU,EACzB,QAAQ,EAAE,WAAW,EACrB,IAAI,CAAC,EAAE,aAAa,GACnB,QAAQ,CA4HV"}
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The §9.3 stateless receipt gate -- the THIRD implementation, after the
|
|
3
|
+
* Python reference (`acp_executor.py`) and Rust (`crates/acp-decision`), both
|
|
4
|
+
* at the engine pin. Composed in the order `decide.rs` states, because on an
|
|
5
|
+
* input carrying more than one defect the ORDER decides which clause fires,
|
|
6
|
+
* and the clause is what a refusal tells an operator.
|
|
7
|
+
*
|
|
8
|
+
* # What is here, and what is NOT -- read this before trusting a green run
|
|
9
|
+
*
|
|
10
|
+
* This function covers exactly the checklist portion runnable with only
|
|
11
|
+
* receipt + proposal + identity (the ACP-197 runbook's stateless set):
|
|
12
|
+
*
|
|
13
|
+
* | step | rule | here |
|
|
14
|
+
* | --- | --- | --- |
|
|
15
|
+
* | -- | AB-0 receipt version, AB-5 signed-body exclusion, AB-6 byte cap | yes |
|
|
16
|
+
* | 1-2 | CR-1 / CR-4 / CR-3 conjunctive, then `decision` | yes |
|
|
17
|
+
* | 3 | `proposal_hash` recomputed from the caller's own bytes (B-1a) | yes |
|
|
18
|
+
* | 4 | policy basis (bundle hash, epoch) | NO -- needs the verifier's bundle |
|
|
19
|
+
* | 5 | temporal position (9.3-5) and the L-14 window ceiling | yes |
|
|
20
|
+
* | 6 | nonce WE-4 type and L-17 size | yes; the CL-2 single-use CLAIM is ledger state |
|
|
21
|
+
* | 7 | TR-8 / RV-3 recomputation | NO -- needs the signed policy |
|
|
22
|
+
* | 7b | the AT-* quorum, AB-1..AB-4 | NO -- needs the attester registry |
|
|
23
|
+
* | 8 | tenant scoping | NO -- needs the bundle's tenant |
|
|
24
|
+
* | 9-10 | capability recheck, delivery identity | NO -- absent in the engine too |
|
|
25
|
+
*
|
|
26
|
+
* An absent step is a DIVERGENCE, not an agreement: a receipt this function
|
|
27
|
+
* passes may still be refused by a full Executor at any absent step. A pass
|
|
28
|
+
* here means "the stateless set found nothing", never "the receipt may be
|
|
29
|
+
* consumed".
|
|
30
|
+
*
|
|
31
|
+
* # R / B / T classification (suite 12) for the inputs this module reads
|
|
32
|
+
*
|
|
33
|
+
* | input | class | why |
|
|
34
|
+
* | --- | :---: | --- |
|
|
35
|
+
* | `identity` (keys, floor) | R | the verifier's own configuration, never the message's |
|
|
36
|
+
* | signed bytes | R | canonicalised HERE from the receipt body (RES-9) |
|
|
37
|
+
* | `proposal_hash` | R | recomputed from `proposalBytes`, the caller's own copy; the receipt's field is compared, never used |
|
|
38
|
+
* | `alg`, `sig`, `decision`, temporal fields, `nonce`, `receipt_version` | B | inside (or selecting) the signed body; each is read only in a position where reading it can at most cause a refusal |
|
|
39
|
+
* | `nowUnixSeconds` | T | the caller's clock. Nothing here can check it; a stale or forward value silently voids the expiry and skew checks (the engine's residual 3, verbatim). Only L-14 survives such a caller, because it compares the receipt's two instants against each other |
|
|
40
|
+
*/
|
|
41
|
+
import { createHash } from 'node:crypto';
|
|
42
|
+
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
|
|
43
|
+
import { canon } from './canon.js';
|
|
44
|
+
import { CLAUSE_BODY_SIZE, CLAUSE_CANONICAL, CLAUSE_DECISION, CLAUSE_PROPOSAL_BINDING, CLAUSE_RECEIPT_NONCE_SIZE, CLAUSE_SIGNATURE, CLAUSE_SUITE_FLOOR, CLAUSE_TEMPORAL, CLAUSE_UNKNOWN_SUITE, CLAUSE_VALIDITY_WINDOW, CLAUSE_VERSION, CLAUSE_WIRE_TYPE, } from './clauses.js';
|
|
45
|
+
import { verifyEd25519Strict } from './ed25519.js';
|
|
46
|
+
import { parseInstant } from './instant.js';
|
|
47
|
+
import { Refusal } from './refusal.js';
|
|
48
|
+
import { parseSuite, satisfiesFloor, suitePrimitives, verifyHybrid, } from './suite.js';
|
|
49
|
+
/** AB-0 (§8.6b): version 3, the digest-bound form, and NO other. A verifier
|
|
50
|
+
* accepting both 2 and 3 lets an attacker present the weaker one -- the CR-4
|
|
51
|
+
* downgrade shape with the format negotiated by the party under verification. */
|
|
52
|
+
export const RECEIPT_VERSION = 3;
|
|
53
|
+
/** AB-5: the signed body is the transport object MINUS these keys. ONE list --
|
|
54
|
+
* a second exclusion site would be a second definition of "the signed body". */
|
|
55
|
+
export const SIGNED_EXCLUDE = ['sig', 'attestations'];
|
|
56
|
+
/** AB-6: the KMS RAW cap; a bigger body could only come from a software key,
|
|
57
|
+
* so accepting one admits a custody downgrade silently. Counted in BYTES. */
|
|
58
|
+
export const RECEIPT_MAX_SIGNED_BYTES = 4096;
|
|
59
|
+
/** Step 5's tolerated clock skew, seconds -- the reference's `iat > now + 5`. */
|
|
60
|
+
export const CLOCK_SKEW_SECS = 5;
|
|
61
|
+
/** L-14's ceiling on a receipt's validity window, seconds. */
|
|
62
|
+
export const MAX_VALIDITY_WINDOW_SECS = 120;
|
|
63
|
+
/** FIPS 204 ML-DSA-65 public key length. */
|
|
64
|
+
export const MLDSA65_PK_LEN = 1952;
|
|
65
|
+
/** FIPS 204 ML-DSA-65 signature length. */
|
|
66
|
+
export const MLDSA65_SIG_LEN = 3309;
|
|
67
|
+
/** The one wire nonce size, 128-bit (`Nonce128`, cited by AT-1 and L-17). */
|
|
68
|
+
export const NONCE128_BYTES = 16;
|
|
69
|
+
/** `"b64:"` plus base64 of 16 bytes: 4 + 24. Counted, never decoded -- a rule
|
|
70
|
+
* whose behaviour on bad input is "throw" is not a refusal. */
|
|
71
|
+
export const NONCE128_LEN = 4 + Math.ceil(NONCE128_BYTES / 3) * 4;
|
|
72
|
+
function isRecord(v) {
|
|
73
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
74
|
+
}
|
|
75
|
+
function strField(v, key) {
|
|
76
|
+
const raw = v[key];
|
|
77
|
+
return typeof raw === 'string' ? raw : null;
|
|
78
|
+
}
|
|
79
|
+
function hex(bytes) {
|
|
80
|
+
let out = '';
|
|
81
|
+
for (const b of bytes)
|
|
82
|
+
out += b.toString(16).padStart(2, '0');
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/** Decode a hex signature leg, refusing exactly as decide.rs's `unhex`. */
|
|
86
|
+
function unhex(s) {
|
|
87
|
+
if (s.length % 2 !== 0) {
|
|
88
|
+
throw new Refusal(CLAUSE_SIGNATURE, 'signature hex has odd length');
|
|
89
|
+
}
|
|
90
|
+
const out = new Uint8Array(s.length / 2);
|
|
91
|
+
for (let i = 0; i < s.length; i += 2) {
|
|
92
|
+
const byte = Number.parseInt(s.slice(i, i + 2), 16);
|
|
93
|
+
// parseInt would tolerate "0x", whitespace and a lone valid first digit;
|
|
94
|
+
// requiring both chars to be hex keeps this as strict as Rust's
|
|
95
|
+
// from_str_radix over a 2-char slice.
|
|
96
|
+
if (Number.isNaN(byte) || !/^[0-9a-fA-F]{2}$/.test(s.slice(i, i + 2))) {
|
|
97
|
+
throw new Refusal(CLAUSE_SIGNATURE, 'signature is not hex');
|
|
98
|
+
}
|
|
99
|
+
out[i / 2] = byte;
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* WE-4: is this the ASCII string `b64:` followed by RFC 4648 §4 base64, WITH
|
|
105
|
+
* padding? Ported from `quorum.rs::is_we4_b64` -- the body is whole
|
|
106
|
+
* four-character groups, the alphabet is §4's (`+` and `/`, never §5's `-` and
|
|
107
|
+
* `_`), and `=` appears only as one or two characters at the very end.
|
|
108
|
+
* Rejected, never normalised: normalising would make this verifier accept two
|
|
109
|
+
* spellings of one nonce, which is how one nonce comes to hold two ledger
|
|
110
|
+
* slots (ACP-87).
|
|
111
|
+
*/
|
|
112
|
+
export function isWe4B64(s) {
|
|
113
|
+
if (!s.startsWith('b64:'))
|
|
114
|
+
return false;
|
|
115
|
+
const body = s.slice(4);
|
|
116
|
+
if (body.length % 4 !== 0)
|
|
117
|
+
return false;
|
|
118
|
+
let pad = 0;
|
|
119
|
+
for (let i = body.length - 1; i >= 0 && body[i] === '='; i -= 1)
|
|
120
|
+
pad += 1;
|
|
121
|
+
if (pad > 2)
|
|
122
|
+
return false;
|
|
123
|
+
for (let i = 0; i < body.length - pad; i += 1) {
|
|
124
|
+
const c = body.charCodeAt(i);
|
|
125
|
+
const alnum = (c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
|
|
126
|
+
if (!alnum && c !== 0x2b /* + */ && c !== 0x2f /* / */)
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse a wire `sig` object into the parts the CR-3 combiner verifies --
|
|
133
|
+
* `decide.rs::parse_sig`, including its rule that the key set must be EXACTLY
|
|
134
|
+
* the suite's. Three attacks live in the difference: a scalar `sig` (format
|
|
135
|
+
* confusion is a downgrade in disguise), a missing primitive (the stripped
|
|
136
|
+
* leg), and an extra undeclared primitive (an accepted code path the attacker
|
|
137
|
+
* chose). An unknown primitive name is refused rather than dropped -- a
|
|
138
|
+
* dropped part would let a suite be satisfied by the parts that remain.
|
|
139
|
+
*/
|
|
140
|
+
function parseSig(sig, alg) {
|
|
141
|
+
// CR-1 first: an unparseable suite has no primitive set at all, so asking
|
|
142
|
+
// which primitives it requires would be a category error.
|
|
143
|
+
const suite = parseSuite(alg);
|
|
144
|
+
if (suite === null) {
|
|
145
|
+
throw new Refusal(CLAUSE_UNKNOWN_SUITE, 'unknown signature suite');
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(sig)) {
|
|
148
|
+
throw new Refusal(CLAUSE_SIGNATURE, 'receipt signature is not a per-primitive object');
|
|
149
|
+
}
|
|
150
|
+
const required = suitePrimitives(suite);
|
|
151
|
+
const keys = Object.keys(sig);
|
|
152
|
+
if (keys.length !== required.length || !required.every((p) => keys.includes(p))) {
|
|
153
|
+
throw new Refusal(CLAUSE_SIGNATURE, 'signature primitives are not exactly those the declared suite requires');
|
|
154
|
+
}
|
|
155
|
+
const parts = [];
|
|
156
|
+
for (const primitive of required) {
|
|
157
|
+
const value = sig[primitive];
|
|
158
|
+
if (typeof value !== 'string') {
|
|
159
|
+
throw new Refusal(CLAUSE_SIGNATURE, 'signature value is not a hex string');
|
|
160
|
+
}
|
|
161
|
+
parts.push({ primitive, bytes: unhex(value) });
|
|
162
|
+
}
|
|
163
|
+
return parts;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Verify one ML-DSA-65 signature. Never throws; length checks first, exactly
|
|
167
|
+
* as `primitives.rs` -- a malformed input is `invalid`, never `unsupported`:
|
|
168
|
+
* one is a statement about the signature, the other about this build, and
|
|
169
|
+
* reporting one as the other sends the investigation to the wrong place.
|
|
170
|
+
*/
|
|
171
|
+
function verifyMlDsa65(publicKey, message, signature) {
|
|
172
|
+
if (publicKey.length !== MLDSA65_PK_LEN || signature.length !== MLDSA65_SIG_LEN) {
|
|
173
|
+
return 'invalid';
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// Empty FIPS 204 context, matching both engine sides (MLDSA_CTX = &[]):
|
|
177
|
+
// a context the signer did not use is a different message.
|
|
178
|
+
return ml_dsa65.verify(signature, message, publicKey) ? 'valid' : 'invalid';
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return 'invalid';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The §9.3 gate steps 1-2 with CR-1/CR-4/CR-3 -- `receipt.rs::verify_receipt`.
|
|
186
|
+
*
|
|
187
|
+
* CR-4 runs BEFORE the signature: the floor exists to rule out PRIMITIVES,
|
|
188
|
+
* not forgeries, and `CR-4` says the deployment's policy was not met, which is
|
|
189
|
+
* a different fact from "this signature is bad". Step 2 (`decision`) is LAST
|
|
190
|
+
* among the cryptographic checks: a receipt whose signature does not verify is
|
|
191
|
+
* not evidence of anything, including of its own decision field.
|
|
192
|
+
*/
|
|
193
|
+
function verifyReceiptGate(alg, floor, identity, signedBytes, parts, decision) {
|
|
194
|
+
const suite = parseSuite(alg);
|
|
195
|
+
if (suite === null) {
|
|
196
|
+
throw new Refusal(CLAUSE_UNKNOWN_SUITE, 'unknown signature suite');
|
|
197
|
+
}
|
|
198
|
+
if (!satisfiesFloor(suite, floor)) {
|
|
199
|
+
throw new Refusal(CLAUSE_SUITE_FLOOR, 'signature suite does not contain every primitive of the bundle floor');
|
|
200
|
+
}
|
|
201
|
+
const verdicts = parts.map((part) => {
|
|
202
|
+
switch (part.primitive) {
|
|
203
|
+
case 'classical':
|
|
204
|
+
return [
|
|
205
|
+
part.primitive,
|
|
206
|
+
verifyEd25519Strict(identity.classical, signedBytes, part.bytes) ? 'valid' : 'invalid',
|
|
207
|
+
];
|
|
208
|
+
case 'pq':
|
|
209
|
+
return [part.primitive, verifyMlDsa65(identity.pq, signedBytes, part.bytes)];
|
|
210
|
+
case 'pq-slh':
|
|
211
|
+
// Declared, not implemented. Never a pass, and never silently dropped
|
|
212
|
+
// from the set either -- dropping it would let a suite naming it be
|
|
213
|
+
// satisfied by the primitives that remain.
|
|
214
|
+
return [part.primitive, 'unsupported'];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
const err = verifyHybrid(suite, verdicts);
|
|
218
|
+
if (err !== null) {
|
|
219
|
+
throw new Refusal(CLAUSE_SIGNATURE, err);
|
|
220
|
+
}
|
|
221
|
+
if (decision !== 'ALLOW') {
|
|
222
|
+
throw new Refusal(CLAUSE_DECISION, 'decision is not ALLOW');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Run the stateless half of §9.3 in specification order over one receipt.
|
|
227
|
+
*
|
|
228
|
+
* @param receiptJson the receipt as parsed JSON (`unknown`: this module does
|
|
229
|
+
* its own narrowing and refuses by name, never casts).
|
|
230
|
+
* @param proposalBytes the Proposal THE CALLER received, as bytes. The hash is
|
|
231
|
+
* recomputed from these -- a transmitted identifier is a
|
|
232
|
+
* name for a binding, not evidence of one (RES-9/TR-10).
|
|
233
|
+
* @param identity the verifier's trust anchor (keys + floor), out-of-band.
|
|
234
|
+
* @throws Refusal with `clause` spelled exactly as the engine's constants.
|
|
235
|
+
*/
|
|
236
|
+
export function verifyReceipt(receiptJson, proposalBytes, identity, opts) {
|
|
237
|
+
const floor = parseSuite(identity.minSuite);
|
|
238
|
+
if (floor === null) {
|
|
239
|
+
// The anchor is the verifier's own configuration; an unknown floor is a
|
|
240
|
+
// misconfiguration, refused under CR-1's name rather than defaulted --
|
|
241
|
+
// there is no default suite for the reason there is no default tenant.
|
|
242
|
+
throw new Refusal(CLAUSE_UNKNOWN_SUITE, 'trust anchor names an unknown suite floor');
|
|
243
|
+
}
|
|
244
|
+
const now = opts?.nowUnixSeconds ?? Date.now() / 1000;
|
|
245
|
+
// ------------------------------------------------------------- AB-0
|
|
246
|
+
// Version 3 ONLY, checked before the signature so a version-2 receipt is
|
|
247
|
+
// named as a version failure rather than a confusing signature mismatch.
|
|
248
|
+
// On a non-object input the version read yields undefined and AB-0 fires
|
|
249
|
+
// first, matching Rust's `.get()` on a non-object Value.
|
|
250
|
+
const version = isRecord(receiptJson) ? receiptJson['receipt_version'] : undefined;
|
|
251
|
+
if (version !== RECEIPT_VERSION) {
|
|
252
|
+
throw new Refusal(CLAUSE_VERSION, `receipt_version ${JSON.stringify(version)} is not ${RECEIPT_VERSION}`);
|
|
253
|
+
}
|
|
254
|
+
if (!isRecord(receiptJson)) {
|
|
255
|
+
// Unreachable after AB-0 (a non-object has no version), kept for shape
|
|
256
|
+
// parity with decide.rs, whose body build carries the same guard.
|
|
257
|
+
throw new Refusal(CLAUSE_SIGNATURE, 'receipt is not an object');
|
|
258
|
+
}
|
|
259
|
+
// ---------------------------------------------------------- steps 1-2
|
|
260
|
+
// AB-5: the body is the transport object minus SIGNED_EXCLUDE -- `sig`, and
|
|
261
|
+
// `attestations`, the beside-channel the receipt commits to by digest.
|
|
262
|
+
const body = {};
|
|
263
|
+
for (const [k, v] of Object.entries(receiptJson)) {
|
|
264
|
+
if (!SIGNED_EXCLUDE.includes(k))
|
|
265
|
+
body[k] = v;
|
|
266
|
+
}
|
|
267
|
+
const signedBytes = canon(body);
|
|
268
|
+
// AB-6 on the canonical bytes, before suite and signature: an oversized
|
|
269
|
+
// body is refused on its face.
|
|
270
|
+
if (signedBytes.length > RECEIPT_MAX_SIGNED_BYTES) {
|
|
271
|
+
throw new Refusal(CLAUSE_BODY_SIZE, `signed body is ${signedBytes.length} bytes, over the ${RECEIPT_MAX_SIGNED_BYTES}-byte custody limit`);
|
|
272
|
+
}
|
|
273
|
+
const alg = strField(receiptJson, 'alg');
|
|
274
|
+
if (alg === null) {
|
|
275
|
+
throw new Refusal(CLAUSE_UNKNOWN_SUITE, 'receipt declares no suite');
|
|
276
|
+
}
|
|
277
|
+
const parts = parseSig(receiptJson['sig'], alg);
|
|
278
|
+
verifyReceiptGate(alg, floor, identity, signedBytes, parts, strField(receiptJson, 'decision') ?? '');
|
|
279
|
+
// ------------------------------------------------------------ step 3
|
|
280
|
+
// B-1a: hash the Proposal WE received. The receipt's field is compared,
|
|
281
|
+
// never used. The bytes are parsed and re-canonicalised because the hash is
|
|
282
|
+
// defined over the canonical encoding (`h(canon(proposal))`), not over
|
|
283
|
+
// whatever spacing the transport used.
|
|
284
|
+
let proposalValue;
|
|
285
|
+
try {
|
|
286
|
+
proposalValue = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(proposalBytes));
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
throw new Refusal(CLAUSE_CANONICAL, 'proposal bytes are not UTF-8 JSON');
|
|
290
|
+
}
|
|
291
|
+
const proposalHash = `sha256:${hex(createHash('sha256').update(canon(proposalValue)).digest())}`;
|
|
292
|
+
if (strField(receiptJson, 'proposal_hash') !== proposalHash) {
|
|
293
|
+
throw new Refusal(CLAUSE_PROPOSAL_BINDING, 'receipt not bound to this proposal');
|
|
294
|
+
}
|
|
295
|
+
// Step 4 (policy basis: bundle hash + epoch) is NOT here -- it needs the
|
|
296
|
+
// verifier's own bundle, which this stateless API deliberately does not
|
|
297
|
+
// take. See the module doc's table; absence is disclosed, never approximated.
|
|
298
|
+
// ------------------------------------------------------------ step 5
|
|
299
|
+
// RFC 3339, PARSED, not a POSIX number (ACP-167): the one WE-5 parser in
|
|
300
|
+
// instant.ts, never a second grammar.
|
|
301
|
+
const iatRaw = strField(receiptJson, 'issued_at');
|
|
302
|
+
const expRaw = strField(receiptJson, 'expires_at');
|
|
303
|
+
if (iatRaw === null || expRaw === null) {
|
|
304
|
+
throw new Refusal(CLAUSE_TEMPORAL, 'missing temporal fields');
|
|
305
|
+
}
|
|
306
|
+
const iat = parseInstant(iatRaw);
|
|
307
|
+
const exp = parseInstant(expRaw);
|
|
308
|
+
if (iat === null || exp === null) {
|
|
309
|
+
throw new Refusal(CLAUSE_TEMPORAL, 'temporal field is not an RFC 3339 UTC instant');
|
|
310
|
+
}
|
|
311
|
+
if (now > exp) {
|
|
312
|
+
throw new Refusal(CLAUSE_TEMPORAL, 'receipt expired');
|
|
313
|
+
}
|
|
314
|
+
if (iat > now + CLOCK_SKEW_SECS) {
|
|
315
|
+
throw new Refusal(CLAUSE_TEMPORAL, 'issued in the future beyond skew');
|
|
316
|
+
}
|
|
317
|
+
// L-14 is a SEPARATE clause: a legal-today window 10x too long is Y2 (the
|
|
318
|
+
// widened theft interval), not "expired", and the clause must say so.
|
|
319
|
+
if (exp - iat > MAX_VALIDITY_WINDOW_SECS) {
|
|
320
|
+
throw new Refusal(CLAUSE_VALIDITY_WINDOW, `validity window ${exp - iat}s exceeds ${MAX_VALIDITY_WINDOW_SECS}s`);
|
|
321
|
+
}
|
|
322
|
+
// ------------------------------------------------------------ step 6
|
|
323
|
+
// The nonce's TYPE and SIZE, in the reference's order -- WE-4 then L-17 --
|
|
324
|
+
// so a value wrong in both ways stops at the same name in all three
|
|
325
|
+
// implementations. The CL-2 single-use CLAIM is ledger state and is not
|
|
326
|
+
// here; what is refused here is a value the ledger must never be handed.
|
|
327
|
+
const nonce = strField(receiptJson, 'nonce') ?? '';
|
|
328
|
+
if (!isWe4B64(nonce)) {
|
|
329
|
+
throw new Refusal(CLAUSE_WIRE_TYPE, `receipt nonce ${JSON.stringify(receiptJson['nonce'])} is not b64: + RFC 4648 sec 4 base64 with padding`);
|
|
330
|
+
}
|
|
331
|
+
if (nonce.length !== NONCE128_LEN) {
|
|
332
|
+
throw new Refusal(CLAUSE_RECEIPT_NONCE_SIZE, `receipt nonce is not ${NONCE128_BYTES * 8}-bit`);
|
|
333
|
+
}
|
|
334
|
+
// Steps 7/7b/8 (grading, quorum, tenant) need the signed policy, registry
|
|
335
|
+
// and bundle tenant -- outside the stateless set; see the module doc.
|
|
336
|
+
return { proposalHash, receiptExpiresAt: exp };
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ziffer-io/verify",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ZIFFER receipt verification: the stateless half of the ACP 9.3 checklist, refusals named by clause.",
|
|
5
|
+
"author": "code75 SASU",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
|
+
"comment-license": "The SDK is proprietary (code75 SASU). \"license\" is the SPDX escape hatch for exactly this case: there is no SPDX identifier for these terms, so the field points at the file that states them, and LICENSE ships in the tarball beside THIRD-PARTY-NOTICES. Do not put an OSI identifier here -- Apache-2.0 stood in these four files until ACP-214 and was wrong the whole time.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"comment-exports": "The `./dist/*` entry is not laziness and removing it breaks a caller. `exports` is a gate as well as a map: declaring only `.` makes every other path unreachable, and packages/mcp/src/tools.test.ts imports `@ziffer-io/verify/dist/testkeys.js` ON PURPOSE -- testkeys is deliberately absent from index.ts so it cannot be reached by accident, and the deep import is how the MCP package's tests get a signing key without a second definition of one identity. That import resolves today because there is no exports map; adding a `.`-only one would break it at publish time, which is the class of defect an exports map introduces rather than prevents. So the dist is declared addressable, and the reason is here rather than in a reviewer's memory.",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./dist/*": "./dist/*"
|
|
18
|
+
},
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"!dist/**/*.test.*",
|
|
23
|
+
"THIRD-PARTY-NOTICES"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/ziffer-hq/ziffer.git",
|
|
31
|
+
"directory": "packages/acp-verify"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://ziffer.io",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"comment-no-provenance": "There is deliberately no \"provenance\": true here, and it was removed rather than never added. npm generates a provenance attestation only from a recognised CI runner, and its documentation states plainly that provenance is NOT SUPPORTED for private repositories (docs.npmjs.com/trusted-publishers, read 2026-09-03) -- ziffer-hq/ziffer is private (`gh api` says so). Setting the flag does not degrade to a warning: npm attempts the attestation and the publish FAILS, so the field would have broken the operator's very first publish from a laptop and every CI publish after it. tools/release-npm.sh asserts the field stays absent, and that assertion is the thing to delete on the day this repository becomes public -- at which point trusted publishing generates provenance on its own, with no flag at all.",
|
|
38
|
+
"ziffer": {
|
|
39
|
+
"enginePin": "fed43d10b427e0a4435a8f04e474334d88a5aa6e"
|
|
40
|
+
},
|
|
41
|
+
"comment-enginePin": "The engine commit whose clause spellings, canon and mirrored corpus this implementation is bound to. A COPY of the rev in Cargo.toml, which is the one authority tools/guard.sh reads; tools/release-npm.sh refuses to release when the two differ, by name. tools/bump-pin.sh does not move this field -- see packages/types/package.json for why and what closing it costs.",
|
|
42
|
+
"comment-deps": "Exactly the two signature primitives, at the versions services/approval already pins -- the DR-2 permitted common set minus the wire types, which this package does not consume: it verifies raw parsed JSON, and typing the wire is the client's (@ziffer-io/client) business. Adding anything else needs written justification (ACP-197 runbook section 5).",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@noble/curves": "2.3.0",
|
|
45
|
+
"@noble/post-quantum": "0.7.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.15.0",
|
|
49
|
+
"typescript": "^5.9.2"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -b",
|
|
53
|
+
"typecheck": "tsc -b",
|
|
54
|
+
"pretest": "tsc -b && if ! find dist -name '*.test.js' -print -quit | grep -q .; then echo 'ACP-248: no compiled test file under dist/ -- node --test reports 0 tests and exits 0, so this package would go green having run nothing' >&2; exit 1; fi",
|
|
55
|
+
"test": "tsc -b && node --test \"dist/**/*.test.js\""
|
|
56
|
+
}
|
|
57
|
+
}
|