@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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict Ed25519 -- the small-order rule WRITTEN OUT (ACP-106), the third
|
|
3
|
+
* spelling of the one rule the engine keeps in two places: Rust calls
|
|
4
|
+
* `ed25519-dalek`'s `verify_strict`; the Python reference writes the guard out
|
|
5
|
+
* in `acp_crypto.verify_prim` because `cryptography` has no strict mode. The
|
|
6
|
+
* three MUST stay one rule -- a guard on one side only converts a closed
|
|
7
|
+
* defect into a divergence -- and `fixtures/refused_ed25519.json` (mirrored
|
|
8
|
+
* byte-identically from the engine) is the executable statement of it: every
|
|
9
|
+
* vector there is accepted by RFC 8032 as OpenSSL implements it and must be
|
|
10
|
+
* refused here.
|
|
11
|
+
*
|
|
12
|
+
* WHY. Under a small-order public key A the verification equation
|
|
13
|
+
* `[s]B == R + [k]A` loses its A term -- for the identity point `[k]A` is the
|
|
14
|
+
* identity whatever k is -- so what remains, `[s]B == R`, does not mention the
|
|
15
|
+
* message at all, and ONE signature verifies for EVERY message under that key.
|
|
16
|
+
* A small-order R is the same fail-open reached from the signature side: an
|
|
17
|
+
* honest signer can emit r=0 and a non-strict verifier accepts it over a
|
|
18
|
+
* message it never committed to. Both halves are refused, as in `verify_prim`.
|
|
19
|
+
*
|
|
20
|
+
* @noble/curves' `verify` with `{ zip215: false }` gives RFC 8032 cofactored
|
|
21
|
+
* verification with canonical encodings enforced, but -- like OpenSSL -- it
|
|
22
|
+
* does NOT refuse small-order points: those are perfectly canonical. So the
|
|
23
|
+
* guard is written out here, before the equation, exactly as in Python.
|
|
24
|
+
*/
|
|
25
|
+
/** RFC 8032 raw public key length. */
|
|
26
|
+
export declare const ED25519_PK_LEN = 32;
|
|
27
|
+
/** RFC 8032 signature length. */
|
|
28
|
+
export declare const ED25519_SIG_LEN = 64;
|
|
29
|
+
/**
|
|
30
|
+
* Whether an encoded point has order dividing 8 -- dalek's `is_weak`, the
|
|
31
|
+
* predicate `verify_strict` applies before it checks the equation. Computed on
|
|
32
|
+
* the curve (`[8]P == identity` via the library's own point arithmetic), never
|
|
33
|
+
* from a table of the eight encodings: a hardcoded blacklist is a second
|
|
34
|
+
* definition of "small order" that nothing can check against the curve.
|
|
35
|
+
*
|
|
36
|
+
* Bytes that do not decode as a point are `false`, not `true`: "not a key" is
|
|
37
|
+
* refused by the verifier itself a line later, and a guard that answered
|
|
38
|
+
* "weak" about bytes that are not a point would make its own failure mode
|
|
39
|
+
* unreadable (the engine states this rule on both sides).
|
|
40
|
+
*/
|
|
41
|
+
export declare function ed25519IsSmallOrder(encoded: Uint8Array): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Verify one Ed25519 signature under the strict rule.
|
|
44
|
+
*
|
|
45
|
+
* Never throws: a malformed signature is a verification FAILURE, not an
|
|
46
|
+
* exception for a caller further up to catch -- a throw would travel a
|
|
47
|
+
* different path from a `false`, and the two must be indistinguishable to the
|
|
48
|
+
* fail-closed contract (`verify_prim`'s own words).
|
|
49
|
+
*/
|
|
50
|
+
export declare function verifyEd25519Strict(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
|
|
51
|
+
//# sourceMappingURL=ed25519.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ed25519.d.ts","sourceRoot":"","sources":["../src/ed25519.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,sCAAsC;AACtC,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,iCAAiC;AACjC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAOhE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,UAAU,EACrB,OAAO,EAAE,UAAU,EACnB,SAAS,EAAE,UAAU,GACpB,OAAO,CAmBT"}
|
package/dist/ed25519.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict Ed25519 -- the small-order rule WRITTEN OUT (ACP-106), the third
|
|
3
|
+
* spelling of the one rule the engine keeps in two places: Rust calls
|
|
4
|
+
* `ed25519-dalek`'s `verify_strict`; the Python reference writes the guard out
|
|
5
|
+
* in `acp_crypto.verify_prim` because `cryptography` has no strict mode. The
|
|
6
|
+
* three MUST stay one rule -- a guard on one side only converts a closed
|
|
7
|
+
* defect into a divergence -- and `fixtures/refused_ed25519.json` (mirrored
|
|
8
|
+
* byte-identically from the engine) is the executable statement of it: every
|
|
9
|
+
* vector there is accepted by RFC 8032 as OpenSSL implements it and must be
|
|
10
|
+
* refused here.
|
|
11
|
+
*
|
|
12
|
+
* WHY. Under a small-order public key A the verification equation
|
|
13
|
+
* `[s]B == R + [k]A` loses its A term -- for the identity point `[k]A` is the
|
|
14
|
+
* identity whatever k is -- so what remains, `[s]B == R`, does not mention the
|
|
15
|
+
* message at all, and ONE signature verifies for EVERY message under that key.
|
|
16
|
+
* A small-order R is the same fail-open reached from the signature side: an
|
|
17
|
+
* honest signer can emit r=0 and a non-strict verifier accepts it over a
|
|
18
|
+
* message it never committed to. Both halves are refused, as in `verify_prim`.
|
|
19
|
+
*
|
|
20
|
+
* @noble/curves' `verify` with `{ zip215: false }` gives RFC 8032 cofactored
|
|
21
|
+
* verification with canonical encodings enforced, but -- like OpenSSL -- it
|
|
22
|
+
* does NOT refuse small-order points: those are perfectly canonical. So the
|
|
23
|
+
* guard is written out here, before the equation, exactly as in Python.
|
|
24
|
+
*/
|
|
25
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
26
|
+
/** RFC 8032 raw public key length. */
|
|
27
|
+
export const ED25519_PK_LEN = 32;
|
|
28
|
+
/** RFC 8032 signature length. */
|
|
29
|
+
export const ED25519_SIG_LEN = 64;
|
|
30
|
+
/**
|
|
31
|
+
* Whether an encoded point has order dividing 8 -- dalek's `is_weak`, the
|
|
32
|
+
* predicate `verify_strict` applies before it checks the equation. Computed on
|
|
33
|
+
* the curve (`[8]P == identity` via the library's own point arithmetic), never
|
|
34
|
+
* from a table of the eight encodings: a hardcoded blacklist is a second
|
|
35
|
+
* definition of "small order" that nothing can check against the curve.
|
|
36
|
+
*
|
|
37
|
+
* Bytes that do not decode as a point are `false`, not `true`: "not a key" is
|
|
38
|
+
* refused by the verifier itself a line later, and a guard that answered
|
|
39
|
+
* "weak" about bytes that are not a point would make its own failure mode
|
|
40
|
+
* unreadable (the engine states this rule on both sides).
|
|
41
|
+
*/
|
|
42
|
+
export function ed25519IsSmallOrder(encoded) {
|
|
43
|
+
if (encoded.length !== ED25519_PK_LEN)
|
|
44
|
+
return false;
|
|
45
|
+
try {
|
|
46
|
+
return ed25519.Point.fromBytes(encoded).isSmallOrder();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Verify one Ed25519 signature under the strict rule.
|
|
54
|
+
*
|
|
55
|
+
* Never throws: a malformed signature is a verification FAILURE, not an
|
|
56
|
+
* exception for a caller further up to catch -- a throw would travel a
|
|
57
|
+
* different path from a `false`, and the two must be indistinguishable to the
|
|
58
|
+
* fail-closed contract (`verify_prim`'s own words).
|
|
59
|
+
*/
|
|
60
|
+
export function verifyEd25519Strict(publicKey, message, signature) {
|
|
61
|
+
if (publicKey.length !== ED25519_PK_LEN || signature.length !== ED25519_SIG_LEN) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// ACP-106, both halves, BEFORE the equation: a small-order A makes one
|
|
65
|
+
// signature verify for every message; a small-order R (the first 32 bytes
|
|
66
|
+
// of the signature) lets an r=0 signature verify over a message the signer
|
|
67
|
+
// never committed to.
|
|
68
|
+
if (ed25519IsSmallOrder(publicKey) || ed25519IsSmallOrder(signature.subarray(0, 32))) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// zip215: false -- canonical point and scalar encodings enforced, matching
|
|
73
|
+
// dalek's strict decoding; the small-order refusals above are the part no
|
|
74
|
+
// library default provides.
|
|
75
|
+
return ed25519.verify(signature, message, publicKey, { zip215: false });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=ed25519.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ed25519.js","sourceRoot":"","sources":["../src/ed25519.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAEnD,sCAAsC;AACtC,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AACjC,iCAAiC;AACjC,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAmB;IACrD,IAAI,OAAO,CAAC,MAAM,KAAK,cAAc;QAAE,OAAO,KAAK,CAAC;IACpD,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAqB,EACrB,OAAmB,EACnB,SAAqB;IAErB,IAAI,SAAS,CAAC,MAAM,KAAK,cAAc,IAAI,SAAS,CAAC,MAAM,KAAK,eAAe,EAAE,CAAC;QAChF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,uEAAuE;IACvE,0EAA0E;IAC1E,2EAA2E;IAC3E,sBAAsB;IACtB,IAAI,mBAAmB,CAAC,SAAS,CAAC,IAAI,mBAAmB,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;QACrF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,2EAA2E;QAC3E,0EAA0E;QAC1E,4BAA4B;QAC5B,OAAO,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ziffer-io/verify -- the third receipt-verifier implementation (ACP-197 §5).
|
|
3
|
+
*
|
|
4
|
+
* `verifyReceipt` is the API; everything else is exported so the pieces are
|
|
5
|
+
* individually testable and so `@ziffer-io/client` can re-export ONE verify (the
|
|
6
|
+
* client adds no verification logic of its own -- one home per rule).
|
|
7
|
+
*/
|
|
8
|
+
export { canon, compareCodePoints, type Json } from './canon.js';
|
|
9
|
+
export { 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';
|
|
10
|
+
export { ED25519_PK_LEN, ED25519_SIG_LEN, ed25519IsSmallOrder, verifyEd25519Strict, } from './ed25519.js';
|
|
11
|
+
export { parseInstant } from './instant.js';
|
|
12
|
+
export { Refusal } from './refusal.js';
|
|
13
|
+
export { parseSuite, satisfiesFloor, suitePrimitives, verifyHybrid, type HybridError, type Primitive, type PrimitiveVerdict, type Suite, } from './suite.js';
|
|
14
|
+
export { CLOCK_SKEW_SECS, MAX_VALIDITY_WINDOW_SECS, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, NONCE128_BYTES, NONCE128_LEN, RECEIPT_MAX_SIGNED_BYTES, RECEIPT_VERSION, SIGNED_EXCLUDE, isWe4B64, verifyReceipt, type TrustAnchor, type Verified, type VerifyOptions, } from './verify.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAC;AACjE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,UAAU,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,KAAK,GACX,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,cAAc,EACd,eAAe,EACf,cAAc,EACd,YAAY,EACZ,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,QAAQ,EACR,aAAa,EACb,KAAK,WAAW,EAChB,KAAK,QAAQ,EACb,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ziffer-io/verify -- the third receipt-verifier implementation (ACP-197 §5).
|
|
3
|
+
*
|
|
4
|
+
* `verifyReceipt` is the API; everything else is exported so the pieces are
|
|
5
|
+
* individually testable and so `@ziffer-io/client` can re-export ONE verify (the
|
|
6
|
+
* client adds no verification logic of its own -- one home per rule).
|
|
7
|
+
*/
|
|
8
|
+
export { canon, compareCodePoints } from './canon.js';
|
|
9
|
+
export { 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';
|
|
10
|
+
export { ED25519_PK_LEN, ED25519_SIG_LEN, ed25519IsSmallOrder, verifyEd25519Strict, } from './ed25519.js';
|
|
11
|
+
export { parseInstant } from './instant.js';
|
|
12
|
+
export { Refusal } from './refusal.js';
|
|
13
|
+
export { parseSuite, satisfiesFloor, suitePrimitives, verifyHybrid, } from './suite.js';
|
|
14
|
+
export { CLOCK_SKEW_SECS, MAX_VALIDITY_WINDOW_SECS, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, NONCE128_BYTES, NONCE128_LEN, RECEIPT_MAX_SIGNED_BYTES, RECEIPT_VERSION, SIGNED_EXCLUDE, isWe4B64, verifyReceipt, } from './verify.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAa,MAAM,YAAY,CAAC;AACjE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,uBAAuB,EACvB,yBAAyB,EACzB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,sBAAsB,EACtB,cAAc,EACd,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EACL,UAAU,EACV,cAAc,EACd,eAAe,EACf,YAAY,GAKb,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,cAAc,EACd,eAAe,EACf,cAAc,EACd,YAAY,EACZ,wBAAwB,EACxB,eAAe,EACf,cAAc,EACd,QAAQ,EACR,aAAa,GAId,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RFC 3339 UTC instant, ported from the engine's ONE parser
|
|
3
|
+
* (`crates/acp-core/src/instant.rs::Timestamp`, WE-5's grammar).
|
|
4
|
+
*
|
|
5
|
+
* Strict, not permissive: exactly `YYYY-MM-DDTHH:MM:SSZ` and nothing else.
|
|
6
|
+
* Offsets, fractional seconds and lowercase `z` are refused rather than
|
|
7
|
+
* normalised -- a permissive decoder silently folding two spellings into one
|
|
8
|
+
* value is the encoding-split defect, and here it would decide when a receipt
|
|
9
|
+
* stops being valid. The day is checked against the ACTUAL month length
|
|
10
|
+
* (2026-02-31 used to parse in the engine and rolled forward to 2026-03-03:
|
|
11
|
+
* two spellings of one instant) and leap seconds are refused because the
|
|
12
|
+
* reference `datetime` refuses them.
|
|
13
|
+
*
|
|
14
|
+
* `fixtures/instant-type-vectors.json` is the engine's shared corpus for this
|
|
15
|
+
* grammar -- the file instant.rs, the Python reference and the schema pattern
|
|
16
|
+
* all answer -- mirrored byte-identically; the test here makes this file its
|
|
17
|
+
* FOURTH consumer, so a spelling that tells any two implementations apart
|
|
18
|
+
* turns something red.
|
|
19
|
+
*
|
|
20
|
+
* This module is a parser only. The engine also renders; a verifier writes no
|
|
21
|
+
* instants, and an unused renderer would be untested surface.
|
|
22
|
+
*/
|
|
23
|
+
/** Seconds since the Unix epoch for a WE-5 instant, or null when refused. */
|
|
24
|
+
export declare function parseInstant(s: string): number | null;
|
|
25
|
+
//# sourceMappingURL=instant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instant.d.ts","sourceRoot":"","sources":["../src/instant.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmCrD"}
|
package/dist/instant.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RFC 3339 UTC instant, ported from the engine's ONE parser
|
|
3
|
+
* (`crates/acp-core/src/instant.rs::Timestamp`, WE-5's grammar).
|
|
4
|
+
*
|
|
5
|
+
* Strict, not permissive: exactly `YYYY-MM-DDTHH:MM:SSZ` and nothing else.
|
|
6
|
+
* Offsets, fractional seconds and lowercase `z` are refused rather than
|
|
7
|
+
* normalised -- a permissive decoder silently folding two spellings into one
|
|
8
|
+
* value is the encoding-split defect, and here it would decide when a receipt
|
|
9
|
+
* stops being valid. The day is checked against the ACTUAL month length
|
|
10
|
+
* (2026-02-31 used to parse in the engine and rolled forward to 2026-03-03:
|
|
11
|
+
* two spellings of one instant) and leap seconds are refused because the
|
|
12
|
+
* reference `datetime` refuses them.
|
|
13
|
+
*
|
|
14
|
+
* `fixtures/instant-type-vectors.json` is the engine's shared corpus for this
|
|
15
|
+
* grammar -- the file instant.rs, the Python reference and the schema pattern
|
|
16
|
+
* all answer -- mirrored byte-identically; the test here makes this file its
|
|
17
|
+
* FOURTH consumer, so a spelling that tells any two implementations apart
|
|
18
|
+
* turns something red.
|
|
19
|
+
*
|
|
20
|
+
* This module is a parser only. The engine also renders; a verifier writes no
|
|
21
|
+
* instants, and an unused renderer would be untested surface.
|
|
22
|
+
*/
|
|
23
|
+
/** Seconds since the Unix epoch for a WE-5 instant, or null when refused. */
|
|
24
|
+
export function parseInstant(s) {
|
|
25
|
+
if (s.length !== 20)
|
|
26
|
+
return null;
|
|
27
|
+
if (s.charCodeAt(4) !== 0x2d || // -
|
|
28
|
+
s.charCodeAt(7) !== 0x2d || // -
|
|
29
|
+
s.charCodeAt(10) !== 0x54 || // T
|
|
30
|
+
s.charCodeAt(13) !== 0x3a || // :
|
|
31
|
+
s.charCodeAt(16) !== 0x3a || // :
|
|
32
|
+
s.charCodeAt(19) !== 0x5a // Z
|
|
33
|
+
) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const n = (from, to) => {
|
|
37
|
+
let v = 0;
|
|
38
|
+
for (let i = from; i < to; i += 1) {
|
|
39
|
+
const c = s.charCodeAt(i);
|
|
40
|
+
if (c < 0x30 || c > 0x39)
|
|
41
|
+
return null;
|
|
42
|
+
v = v * 10 + (c - 0x30);
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
};
|
|
46
|
+
const y = n(0, 4);
|
|
47
|
+
const mo = n(5, 7);
|
|
48
|
+
const d = n(8, 10);
|
|
49
|
+
const h = n(11, 13);
|
|
50
|
+
const mi = n(14, 16);
|
|
51
|
+
const sec = n(17, 19);
|
|
52
|
+
if (y === null || mo === null || d === null || h === null || mi === null || sec === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
// sec stops at 59: no leap second, matching the reference's datetime.
|
|
56
|
+
if (mo < 1 || mo > 12 || d < 1 || d > daysInMonth(y, mo) || h > 23 || mi > 59 || sec > 59) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return daysFromCivil(y, mo, d) * 86_400 + h * 3600 + mi * 60 + sec;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* How many days a month actually has. The century rule is the part that gets
|
|
63
|
+
* written wrong: 2000 IS a leap year (divisible by 400), 2100 is not
|
|
64
|
+
* (divisible by 100 but not 400). Both are in the mirrored corpus.
|
|
65
|
+
*/
|
|
66
|
+
function daysInMonth(y, m) {
|
|
67
|
+
switch (m) {
|
|
68
|
+
case 1:
|
|
69
|
+
case 3:
|
|
70
|
+
case 5:
|
|
71
|
+
case 7:
|
|
72
|
+
case 8:
|
|
73
|
+
case 10:
|
|
74
|
+
case 12:
|
|
75
|
+
return 31;
|
|
76
|
+
case 4:
|
|
77
|
+
case 6:
|
|
78
|
+
case 9:
|
|
79
|
+
case 11:
|
|
80
|
+
return 30;
|
|
81
|
+
case 2:
|
|
82
|
+
return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0 ? 29 : 28;
|
|
83
|
+
default:
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Days since 1970-01-01 for a proleptic Gregorian date -- Howard Hinnant's
|
|
89
|
+
* `days_from_civil`, the same algorithm instant.rs uses, so the two cannot
|
|
90
|
+
* disagree on exactly the dates the /100 and /400 rules cover.
|
|
91
|
+
*
|
|
92
|
+
* `Math.trunc` where Rust integer division truncates: `/` alone would carry
|
|
93
|
+
* fractions into every later term.
|
|
94
|
+
*/
|
|
95
|
+
function daysFromCivil(yIn, m, d) {
|
|
96
|
+
const y = m <= 2 ? yIn - 1 : yIn;
|
|
97
|
+
const era = Math.trunc((y >= 0 ? y : y - 399) / 400);
|
|
98
|
+
const yoe = y - era * 400;
|
|
99
|
+
const mp = (m + 9) % 12;
|
|
100
|
+
const doy = Math.trunc((153 * mp + 2) / 5) + d - 1;
|
|
101
|
+
const doe = yoe * 365 + Math.trunc(yoe / 4) - Math.trunc(yoe / 100) + doy;
|
|
102
|
+
return era * 146_097 + doe - 719_468;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=instant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instant.js","sourceRoot":"","sources":["../src/instant.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named refusal, mirroring the engine's `acp_decision::Refusal` and the
|
|
3
|
+
* reference's `FailClosed(clause, msg)`.
|
|
4
|
+
*
|
|
5
|
+
* Thrown, not returned: the Python SDK's `verify()` raises `RefusedError(name)`
|
|
6
|
+
* and this package's `verifyReceipt` is its TS sibling -- a caller that wants a
|
|
7
|
+
* verdict catches and reads `clause`. The CLAUSE is the machine-readable half
|
|
8
|
+
* (the value the cross-implementation comparison is on); `message` is for the
|
|
9
|
+
* operator and is never part of the contract.
|
|
10
|
+
*/
|
|
11
|
+
export declare class Refusal extends Error {
|
|
12
|
+
/** The clause id, spelled exactly as the engine's Rust constants spell it. */
|
|
13
|
+
readonly clause: string;
|
|
14
|
+
constructor(clause: string, message: string);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=refusal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal.d.ts","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,qBAAa,OAAQ,SAAQ,KAAK;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAK5C"}
|
package/dist/refusal.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A named refusal, mirroring the engine's `acp_decision::Refusal` and the
|
|
3
|
+
* reference's `FailClosed(clause, msg)`.
|
|
4
|
+
*
|
|
5
|
+
* Thrown, not returned: the Python SDK's `verify()` raises `RefusedError(name)`
|
|
6
|
+
* and this package's `verifyReceipt` is its TS sibling -- a caller that wants a
|
|
7
|
+
* verdict catches and reads `clause`. The CLAUSE is the machine-readable half
|
|
8
|
+
* (the value the cross-implementation comparison is on); `message` is for the
|
|
9
|
+
* operator and is never part of the contract.
|
|
10
|
+
*/
|
|
11
|
+
export class Refusal extends Error {
|
|
12
|
+
/** The clause id, spelled exactly as the engine's Rust constants spell it. */
|
|
13
|
+
clause;
|
|
14
|
+
constructor(clause, message) {
|
|
15
|
+
super(`${clause}: ${message}`);
|
|
16
|
+
this.name = 'Refusal';
|
|
17
|
+
this.clause = clause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=refusal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refusal.js","sourceRoot":"","sources":["../src/refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,OAAO,OAAQ,SAAQ,KAAK;IAChC,8EAA8E;IACrE,MAAM,CAAS;IAExB,YAAY,MAAc,EAAE,OAAe;QACzC,KAAK,CAAC,GAAG,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC;QAC/B,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF"}
|
package/dist/suite.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signature suite table (CR-1, CR-4) and the conjunctive combiner (CR-3),
|
|
3
|
+
* mirroring `crates/acp-crypto/src/suite.rs` and `lib.rs::verify_hybrid` at
|
|
4
|
+
* the pin. Third writing of one table; the mirrored vectors and the shared
|
|
5
|
+
* clause names are what keep the three honest.
|
|
6
|
+
*
|
|
7
|
+
* `pq-slh` (SLH-DSA, FIPS 205) is DECLARED AND NOT IMPLEMENTED, with its own
|
|
8
|
+
* primitive name. Aliasing it to `pq` would mean a receipt claiming suite
|
|
9
|
+
* `slhdsa128s` was verified against an ML-DSA key -- the label naming one
|
|
10
|
+
* algorithm and the bytes another. It fails closed as `unsupported`.
|
|
11
|
+
*/
|
|
12
|
+
/** One cryptographic primitive within a suite, by its wire name (CR-2 keys). */
|
|
13
|
+
export type Primitive = 'classical' | 'pq' | 'pq-slh';
|
|
14
|
+
/** A named signature suite, by its wire name. */
|
|
15
|
+
export type Suite = 'ed25519' | 'hybrid-ed25519-mldsa65' | 'slhdsa128s';
|
|
16
|
+
/**
|
|
17
|
+
* Parse a wire suite name. Unknown names are null and MUST be refused by the
|
|
18
|
+
* caller, never defaulted to a known suite (CR-1).
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseSuite(name: string): Suite | null;
|
|
21
|
+
/** The primitives a parsed suite requires, in the table's order. */
|
|
22
|
+
export declare function suitePrimitives(suite: Suite): readonly Primitive[];
|
|
23
|
+
/**
|
|
24
|
+
* CR-4: whether `suite` satisfies a signed floor.
|
|
25
|
+
*
|
|
26
|
+
* CONTAINMENT, NOT RANK -- the engine's published correction. Under a rank
|
|
27
|
+
* table `hybrid-ed25519-mldsa65` outranked `slhdsa128s` while containing none
|
|
28
|
+
* of its primitives: a deployment whose floor said "hash-based post-quantum,
|
|
29
|
+
* no lattice assumption" was served a lattice signature and told its floor was
|
|
30
|
+
* met. The floor is satisfied iff every primitive it names is present.
|
|
31
|
+
*/
|
|
32
|
+
export declare function satisfiesFloor(suite: Suite, floor: Suite): boolean;
|
|
33
|
+
/** Outcome of verifying one primitive within a declared suite. */
|
|
34
|
+
export type PrimitiveVerdict = 'valid' | 'invalid' | 'unsupported';
|
|
35
|
+
/** Why the conjunctive combiner refused; the Rust `HybridError` variants. */
|
|
36
|
+
export type HybridError = 'PrimitiveInvalid' | 'PrimitiveUnsupported' | 'SuiteMismatch';
|
|
37
|
+
/**
|
|
38
|
+
* CR-3 -- verification is conjunctive over the DECLARED suite: the primitives
|
|
39
|
+
* presented must be exactly those the suite requires, and every one must
|
|
40
|
+
* verify. An `any`-shaped check lets an attacker strip the post-quantum leg,
|
|
41
|
+
* present a genuine classical one, and be accepted -- the downgrade the hybrid
|
|
42
|
+
* suite exists to prevent.
|
|
43
|
+
*
|
|
44
|
+
* The suite is a PARAMETER, exactly as in Rust: a caller that decides how many
|
|
45
|
+
* primitives a hybrid signature needs can be persuaded to decide "one".
|
|
46
|
+
* Completeness compares as multisets, so a primitive presented twice cannot
|
|
47
|
+
* stand in for one not presented at all. `unsupported` is reported before
|
|
48
|
+
* `invalid` -- the first is a statement about this BUILD, the second about the
|
|
49
|
+
* signature, and reporting one as the other sends the investigation to the
|
|
50
|
+
* wrong place.
|
|
51
|
+
*
|
|
52
|
+
* Returns null on success, the error name on refusal (the caller wraps it in
|
|
53
|
+
* a 9.3-1 Refusal; this layer is protocol logic, not the checklist).
|
|
54
|
+
*/
|
|
55
|
+
export declare function verifyHybrid(suite: Suite, verdicts: readonly (readonly [Primitive, PrimitiveVerdict])[]): HybridError | null;
|
|
56
|
+
//# sourceMappingURL=suite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suite.d.ts","sourceRoot":"","sources":["../src/suite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,IAAI,GAAG,QAAQ,CAAC;AAEtD,iDAAiD;AACjD,MAAM,MAAM,KAAK,GAAG,SAAS,GAAG,wBAAwB,GAAG,YAAY,CAAC;AAaxE;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAWrD;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,SAAS,EAAE,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAElE;AAED,kEAAkE;AAClE,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;AAEnE,6EAA6E;AAC7E,MAAM,MAAM,WAAW,GAAG,kBAAkB,GAAG,sBAAsB,GAAG,eAAe,CAAC;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,SAAS,CAAC,SAAS,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,EAAE,GAC5D,WAAW,GAAG,IAAI,CAYpB"}
|
package/dist/suite.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signature suite table (CR-1, CR-4) and the conjunctive combiner (CR-3),
|
|
3
|
+
* mirroring `crates/acp-crypto/src/suite.rs` and `lib.rs::verify_hybrid` at
|
|
4
|
+
* the pin. Third writing of one table; the mirrored vectors and the shared
|
|
5
|
+
* clause names are what keep the three honest.
|
|
6
|
+
*
|
|
7
|
+
* `pq-slh` (SLH-DSA, FIPS 205) is DECLARED AND NOT IMPLEMENTED, with its own
|
|
8
|
+
* primitive name. Aliasing it to `pq` would mean a receipt claiming suite
|
|
9
|
+
* `slhdsa128s` was verified against an ML-DSA key -- the label naming one
|
|
10
|
+
* algorithm and the bytes another. It fails closed as `unsupported`.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Every primitive a suite requires. Verification is conjunctive over ALL of
|
|
14
|
+
* them (CR-3). Mirrors the Rust `Suite::primitives` and the reference's
|
|
15
|
+
* `SUITES` table.
|
|
16
|
+
*/
|
|
17
|
+
const SUITES = {
|
|
18
|
+
ed25519: ['classical'],
|
|
19
|
+
'hybrid-ed25519-mldsa65': ['classical', 'pq'],
|
|
20
|
+
slhdsa128s: ['pq-slh'],
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Parse a wire suite name. Unknown names are null and MUST be refused by the
|
|
24
|
+
* caller, never defaulted to a known suite (CR-1).
|
|
25
|
+
*/
|
|
26
|
+
export function parseSuite(name) {
|
|
27
|
+
// A switch, so the narrowing is the compiler's and not a cast's (the
|
|
28
|
+
// project rules forbid `as`, and here the cast would be load-bearing).
|
|
29
|
+
switch (name) {
|
|
30
|
+
case 'ed25519':
|
|
31
|
+
case 'hybrid-ed25519-mldsa65':
|
|
32
|
+
case 'slhdsa128s':
|
|
33
|
+
return name;
|
|
34
|
+
default:
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The primitives a parsed suite requires, in the table's order. */
|
|
39
|
+
export function suitePrimitives(suite) {
|
|
40
|
+
return SUITES[suite];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* CR-4: whether `suite` satisfies a signed floor.
|
|
44
|
+
*
|
|
45
|
+
* CONTAINMENT, NOT RANK -- the engine's published correction. Under a rank
|
|
46
|
+
* table `hybrid-ed25519-mldsa65` outranked `slhdsa128s` while containing none
|
|
47
|
+
* of its primitives: a deployment whose floor said "hash-based post-quantum,
|
|
48
|
+
* no lattice assumption" was served a lattice signature and told its floor was
|
|
49
|
+
* met. The floor is satisfied iff every primitive it names is present.
|
|
50
|
+
*/
|
|
51
|
+
export function satisfiesFloor(suite, floor) {
|
|
52
|
+
return SUITES[floor].every((needed) => SUITES[suite].includes(needed));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* CR-3 -- verification is conjunctive over the DECLARED suite: the primitives
|
|
56
|
+
* presented must be exactly those the suite requires, and every one must
|
|
57
|
+
* verify. An `any`-shaped check lets an attacker strip the post-quantum leg,
|
|
58
|
+
* present a genuine classical one, and be accepted -- the downgrade the hybrid
|
|
59
|
+
* suite exists to prevent.
|
|
60
|
+
*
|
|
61
|
+
* The suite is a PARAMETER, exactly as in Rust: a caller that decides how many
|
|
62
|
+
* primitives a hybrid signature needs can be persuaded to decide "one".
|
|
63
|
+
* Completeness compares as multisets, so a primitive presented twice cannot
|
|
64
|
+
* stand in for one not presented at all. `unsupported` is reported before
|
|
65
|
+
* `invalid` -- the first is a statement about this BUILD, the second about the
|
|
66
|
+
* signature, and reporting one as the other sends the investigation to the
|
|
67
|
+
* wrong place.
|
|
68
|
+
*
|
|
69
|
+
* Returns null on success, the error name on refusal (the caller wraps it in
|
|
70
|
+
* a 9.3-1 Refusal; this layer is protocol logic, not the checklist).
|
|
71
|
+
*/
|
|
72
|
+
export function verifyHybrid(suite, verdicts) {
|
|
73
|
+
const presented = verdicts.map(([p]) => p).sort();
|
|
74
|
+
const required = [...SUITES[suite]].sort();
|
|
75
|
+
if (presented.length !== required.length ||
|
|
76
|
+
!presented.every((p, i) => p === required[i])) {
|
|
77
|
+
return 'SuiteMismatch';
|
|
78
|
+
}
|
|
79
|
+
if (verdicts.some(([, v]) => v === 'unsupported'))
|
|
80
|
+
return 'PrimitiveUnsupported';
|
|
81
|
+
if (verdicts.some(([, v]) => v === 'invalid'))
|
|
82
|
+
return 'PrimitiveInvalid';
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=suite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suite.js","sourceRoot":"","sources":["../src/suite.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH;;;;GAIG;AACH,MAAM,MAAM,GAAkD;IAC5D,OAAO,EAAE,CAAC,WAAW,CAAC;IACtB,wBAAwB,EAAE,CAAC,WAAW,EAAE,IAAI,CAAC;IAC7C,UAAU,EAAE,CAAC,QAAQ,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,qEAAqE;IACrE,uEAAuE;IACvE,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,SAAS,CAAC;QACf,KAAK,wBAAwB,CAAC;QAC9B,KAAK,YAAY;YACf,OAAO,IAAI,CAAC;QACd;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,eAAe,CAAC,KAAY;IAC1C,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,KAAY,EAAE,KAAY;IACvD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AACzE,CAAC;AAQD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAY,EACZ,QAA6D;IAE7D,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAClD,MAAM,QAAQ,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3C,IACE,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QACpC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAC7C,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,aAAa,CAAC;QAAE,OAAO,sBAAsB,CAAC;IACjF,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC;QAAE,OAAO,kBAAkB,CAAC;IACzE,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST KEY MATERIAL ONLY -- the reference's `HybridKey` seed derivation
|
|
3
|
+
* (`acp_crypto.py`), mirrored so fixtures signed here are signed under
|
|
4
|
+
* identities the ENGINE derives from the same seeds: ed secret =
|
|
5
|
+
* sha256(seed || "ed"), ML-DSA seed = sha256(seed || "mldsa"), both legs from
|
|
6
|
+
* one seed (an unseeded ML-DSA keygen gave each process a different key for
|
|
7
|
+
* one identity once -- the engine's own recorded defect).
|
|
8
|
+
*
|
|
9
|
+
* A seed derived from a string anyone can read is a key anyone can hold. This
|
|
10
|
+
* module is imported by tests only and is deliberately NOT exported from
|
|
11
|
+
* index.ts; a deployment loads keys from a KMS, never from here.
|
|
12
|
+
*/
|
|
13
|
+
export interface TestHybridKey {
|
|
14
|
+
readonly edSecret: Uint8Array;
|
|
15
|
+
readonly classical: Uint8Array;
|
|
16
|
+
readonly pq: Uint8Array;
|
|
17
|
+
readonly pqSecret: Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
export declare function hybridKeyFromSeed(seedText: string): TestHybridKey;
|
|
20
|
+
//# sourceMappingURL=testkeys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testkeys.d.ts","sourceRoot":"","sources":["../src/testkeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;CAC/B;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,CAejE"}
|
package/dist/testkeys.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST KEY MATERIAL ONLY -- the reference's `HybridKey` seed derivation
|
|
3
|
+
* (`acp_crypto.py`), mirrored so fixtures signed here are signed under
|
|
4
|
+
* identities the ENGINE derives from the same seeds: ed secret =
|
|
5
|
+
* sha256(seed || "ed"), ML-DSA seed = sha256(seed || "mldsa"), both legs from
|
|
6
|
+
* one seed (an unseeded ML-DSA keygen gave each process a different key for
|
|
7
|
+
* one identity once -- the engine's own recorded defect).
|
|
8
|
+
*
|
|
9
|
+
* A seed derived from a string anyone can read is a key anyone can hold. This
|
|
10
|
+
* module is imported by tests only and is deliberately NOT exported from
|
|
11
|
+
* index.ts; a deployment loads keys from a KMS, never from here.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
15
|
+
import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
|
|
16
|
+
export function hybridKeyFromSeed(seedText) {
|
|
17
|
+
const seed = new TextEncoder().encode(seedText);
|
|
18
|
+
const edSecret = new Uint8Array(createHash('sha256').update(seed).update(new TextEncoder().encode('ed')).digest());
|
|
19
|
+
const mlSeed = new Uint8Array(createHash('sha256').update(seed).update(new TextEncoder().encode('mldsa')).digest());
|
|
20
|
+
const kp = ml_dsa65.keygen(mlSeed);
|
|
21
|
+
return {
|
|
22
|
+
edSecret,
|
|
23
|
+
classical: Uint8Array.from(ed25519.getPublicKey(edSecret)),
|
|
24
|
+
pq: Uint8Array.from(kp.publicKey),
|
|
25
|
+
pqSecret: Uint8Array.from(kp.secretKey),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=testkeys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testkeys.js","sourceRoot":"","sources":["../src/testkeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AASzD,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,QAAQ,GAAG,IAAI,UAAU,CAC7B,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAClF,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,UAAU,CAC3B,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CACrF,CAAC;IACF,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACnC,OAAO;QACL,QAAQ;QACR,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC1D,EAAE,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC;QACjC,QAAQ,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,CAAC;KACxC,CAAC;AACJ,CAAC"}
|