@blamejs/blamejs-shop 0.0.127 → 0.0.128
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/CHANGELOG.md +2 -0
- package/README.md +2 -0
- package/lib/storefront.js +128 -0
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +12 -0
- package/lib/vendor/blamejs/README.md +6 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +378 -2
- package/lib/vendor/blamejs/index.js +5 -0
- package/lib/vendor/blamejs/lib/cose.js +212 -4
- package/lib/vendor/blamejs/lib/cwt.js +244 -0
- package/lib/vendor/blamejs/lib/eat.js +240 -0
- package/lib/vendor/blamejs/lib/scitt.js +243 -0
- package/lib/vendor/blamejs/lib/tsa.js +688 -0
- package/lib/vendor/blamejs/lib/vc.js +328 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.34.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.35.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.36.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.37.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.12.38.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.39.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +9 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +84 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/cwt.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/eat.test.js +111 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scitt.test.js +158 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tsa.test.js +373 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +188 -0
- package/package.json +1 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.cwt (RFC 8392 CBOR Web Token) = COSE_Sign1 over a CBOR
|
|
4
|
+
* claims map. Composes b.cose (signature + alg allowlist) + b.cbor.
|
|
5
|
+
* Covers round-trip + standard-claim mapping + exp/nbf/iss/aud
|
|
6
|
+
* enforcement + tag-61 + tamper (delegated to cose).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
var b = require("../../index");
|
|
10
|
+
var helpers = require("../helpers");
|
|
11
|
+
var check = helpers.check;
|
|
12
|
+
var nodeCrypto = require("node:crypto");
|
|
13
|
+
|
|
14
|
+
var EC = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
15
|
+
function _now() { return Math.floor(Date.now() / 1000); }
|
|
16
|
+
|
|
17
|
+
function testSurface() {
|
|
18
|
+
check("b.cwt.sign exposed", typeof b.cwt.sign === "function");
|
|
19
|
+
check("b.cwt.verify exposed", typeof b.cwt.verify === "function");
|
|
20
|
+
check("b.cwt.CLAIM_LABELS maps standard claims", b.cwt.CLAIM_LABELS.iss === 1 && b.cwt.CLAIM_LABELS.exp === 4 && b.cwt.CLAIM_LABELS.cti === 7);
|
|
21
|
+
check("b.cwt.CwtError exposed", typeof b.cwt.CwtError === "function");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function testRoundTrip() {
|
|
25
|
+
var now = _now();
|
|
26
|
+
var cwt = await b.cwt.sign(
|
|
27
|
+
{ iss: "issuer.example", sub: "dev-42", aud: "svc", exp: now + 3600, nbf: now - 10, iat: now, scope: "telemetry" },
|
|
28
|
+
{ alg: "ES256", privateKey: EC.privateKey, kid: "k1" });
|
|
29
|
+
check("sign: produces a COSE_Sign1 (tag 18 → 0xd2)", cwt[0] === 0xd2);
|
|
30
|
+
var v = await b.cwt.verify(cwt, { algorithms: ["ES256"], publicKey: EC.publicKey });
|
|
31
|
+
check("verify: standard claims mapped back to names", v.claims.iss === "issuer.example" && v.claims.sub === "dev-42" && v.claims.exp === now + 3600);
|
|
32
|
+
check("verify: custom claim preserved", v.claims.scope === "telemetry");
|
|
33
|
+
check("verify: raw Map uses integer labels", v.raw.get(1) === "issuer.example" && v.raw.get(4) === now + 3600);
|
|
34
|
+
check("verify: alg surfaced", v.alg === "ES256");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function testTimeClaims() {
|
|
38
|
+
var now = _now();
|
|
39
|
+
var expired = await b.cwt.sign({ exp: now - 3600 }, { alg: "ES256", privateKey: EC.privateKey });
|
|
40
|
+
var e1 = null;
|
|
41
|
+
try { await b.cwt.verify(expired, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { e1 = e; }
|
|
42
|
+
check("verify: expired token refused", e1 && e1.code === "cwt/expired");
|
|
43
|
+
|
|
44
|
+
var future = await b.cwt.sign({ nbf: now + 3600 }, { alg: "ES256", privateKey: EC.privateKey });
|
|
45
|
+
var e2 = null;
|
|
46
|
+
try { await b.cwt.verify(future, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { e2 = e; }
|
|
47
|
+
check("verify: not-yet-valid (nbf) token refused", e2 && e2.code === "cwt/not-yet-valid");
|
|
48
|
+
|
|
49
|
+
// Clock skew tolerance: exp 30s in the past passes with 60s skew.
|
|
50
|
+
var justExpired = await b.cwt.sign({ exp: now - 30 }, { alg: "ES256", privateKey: EC.privateKey });
|
|
51
|
+
var skewOk = await b.cwt.verify(justExpired, { algorithms: ["ES256"], publicKey: EC.publicKey, clockSkewSec: 60 });
|
|
52
|
+
check("verify: clock skew tolerates marginally-expired exp", skewOk.claims.exp === now - 30);
|
|
53
|
+
|
|
54
|
+
// now override (testing): a token valid in the future verifies if now is advanced.
|
|
55
|
+
var fut = await b.cwt.sign({ nbf: now + 100, exp: now + 200 }, { alg: "ES256", privateKey: EC.privateKey });
|
|
56
|
+
var adv = await b.cwt.verify(fut, { algorithms: ["ES256"], publicKey: EC.publicKey, now: (now + 150) * 1000 });
|
|
57
|
+
check("verify: now override advances the clock", adv.claims.nbf === now + 100);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function testIssuerAudience() {
|
|
61
|
+
var cwt = await b.cwt.sign({ iss: "iss-A", aud: ["svc-1", "svc-2"] }, { alg: "ES256", privateKey: EC.privateKey });
|
|
62
|
+
var ok = await b.cwt.verify(cwt, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedIssuer: "iss-A", expectedAudience: "svc-2" });
|
|
63
|
+
check("verify: matching iss + aud (array) accepted", ok.claims.iss === "iss-A");
|
|
64
|
+
var e1 = null;
|
|
65
|
+
try { await b.cwt.verify(cwt, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedIssuer: "iss-B" }); } catch (e) { e1 = e; }
|
|
66
|
+
check("verify: issuer mismatch refused", e1 && e1.code === "cwt/issuer-mismatch");
|
|
67
|
+
var e2 = null;
|
|
68
|
+
try { await b.cwt.verify(cwt, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedAudience: "svc-3" }); } catch (e) { e2 = e; }
|
|
69
|
+
check("verify: audience not in aud array refused", e2 && e2.code === "cwt/audience-mismatch");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function testTaggedAndTamper() {
|
|
73
|
+
var tagged = await b.cwt.sign({ iss: "x" }, { alg: "ES256", privateKey: EC.privateKey, tagged: true });
|
|
74
|
+
check("sign: tagged wraps in CWT tag 61 (0xd8 0x3d)", tagged[0] === 0xd8 && tagged[1] === 0x3d);
|
|
75
|
+
var v = await b.cwt.verify(tagged, { algorithms: ["ES256"], publicKey: EC.publicKey });
|
|
76
|
+
check("verify: accepts tag-61-wrapped CWT", v.claims.iss === "x");
|
|
77
|
+
|
|
78
|
+
var cwt = await b.cwt.sign({ iss: "y" }, { alg: "ES256", privateKey: EC.privateKey });
|
|
79
|
+
var t = Buffer.from(cwt); t[t.length - 1] ^= 0xff;
|
|
80
|
+
var tampered = null;
|
|
81
|
+
try { await b.cwt.verify(t, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { tampered = e; }
|
|
82
|
+
check("verify: tampered token refused by the COSE signature check", tampered && tampered.code === "cose/bad-signature");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function testRobustTagAndMalformedClaims() {
|
|
86
|
+
// Codex P1 on PR #185 — a non-minimal CBOR tag-61 encoding
|
|
87
|
+
// (0xd9 0x00 0x3d, the 2-byte-argument form) must still be unwrapped.
|
|
88
|
+
var bare = await b.cwt.sign({ iss: "x" }, { alg: "ES256", privateKey: EC.privateKey });
|
|
89
|
+
var nonMinimal = Buffer.concat([Buffer.from([0xd9, 0x00, 0x3d]), bare]);
|
|
90
|
+
var v = await b.cwt.verify(nonMinimal, { algorithms: ["ES256"], publicKey: EC.publicKey });
|
|
91
|
+
check("verify: non-minimal tag-61 encoding accepted (interop)", v.claims.iss === "x");
|
|
92
|
+
|
|
93
|
+
// Codex P2 on PR #185 — a present-but-non-numeric exp must be
|
|
94
|
+
// refused, not silently skipped (it would otherwise never expire).
|
|
95
|
+
var badExp = b.cbor.encode(new Map([[1, "iss-A"], [4, "whenever"]])); // label 4 = exp, string value
|
|
96
|
+
var badToken = await b.cose.sign(badExp, { alg: "ES256", privateKey: EC.privateKey });
|
|
97
|
+
var e1 = null;
|
|
98
|
+
try { await b.cwt.verify(badToken, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { e1 = e; }
|
|
99
|
+
check("verify: malformed (non-numeric) exp refused", e1 && e1.code === "cwt/malformed-claim");
|
|
100
|
+
|
|
101
|
+
var badNbf = b.cbor.encode(new Map([[5, "soon"]])); // label 5 = nbf
|
|
102
|
+
var badNbfToken = await b.cose.sign(badNbf, { alg: "ES256", privateKey: EC.privateKey });
|
|
103
|
+
var e2 = null;
|
|
104
|
+
try { await b.cwt.verify(badNbfToken, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { e2 = e; }
|
|
105
|
+
check("verify: malformed (non-numeric) nbf refused", e2 && e2.code === "cwt/malformed-claim");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function testValidation() {
|
|
109
|
+
var bad = null;
|
|
110
|
+
try { await b.cwt.sign({ exp: "not-a-number" }, { alg: "ES256", privateKey: EC.privateKey }); } catch (e) { bad = e; }
|
|
111
|
+
check("sign: non-integer NumericDate refused", bad && bad.code === "cwt/bad-numeric-date");
|
|
112
|
+
var noAlg = null;
|
|
113
|
+
try { await b.cwt.verify(await b.cwt.sign({ iss: "z" }, { alg: "ES256", privateKey: EC.privateKey }), { publicKey: EC.publicKey }); } catch (e) { noAlg = e; }
|
|
114
|
+
check("verify: missing algorithms refused (delegated to cose)", noAlg !== null);
|
|
115
|
+
var badInput = null;
|
|
116
|
+
try { await b.cwt.verify("not-a-buffer", { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { badInput = e; }
|
|
117
|
+
check("verify: non-buffer input refused", badInput && badInput.code === "cwt/bad-input");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function run() {
|
|
121
|
+
testSurface();
|
|
122
|
+
await testRoundTrip();
|
|
123
|
+
await testTimeClaims();
|
|
124
|
+
await testIssuerAudience();
|
|
125
|
+
await testTaggedAndTamper();
|
|
126
|
+
await testRobustTagAndMalformedClaims();
|
|
127
|
+
await testValidation();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { run: run };
|
|
131
|
+
|
|
132
|
+
if (require.main === module) {
|
|
133
|
+
run().then(
|
|
134
|
+
function () { console.log("[cwt] OK — " + helpers.getChecks() + " checks passed"); },
|
|
135
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.eat (RFC 9711 Entity Attestation Token) over b.cwt.
|
|
4
|
+
* Covers EAT claim-label mapping, the verifier-nonce freshness
|
|
5
|
+
* binding, debug-status policy, profile pinning, and that the
|
|
6
|
+
* signature / time checks delegate to b.cwt / b.cose.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
var b = require("../../index");
|
|
10
|
+
var helpers = require("../helpers");
|
|
11
|
+
var check = helpers.check;
|
|
12
|
+
var nodeCrypto = require("node:crypto");
|
|
13
|
+
|
|
14
|
+
var EC = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
15
|
+
function _now() { return Math.floor(Date.now() / 1000); }
|
|
16
|
+
|
|
17
|
+
function testSurface() {
|
|
18
|
+
check("b.eat.sign exposed", typeof b.eat.sign === "function");
|
|
19
|
+
check("b.eat.verify exposed", typeof b.eat.verify === "function");
|
|
20
|
+
check("b.eat.CLAIM_LABELS has RFC 9711 keys", b.eat.CLAIM_LABELS.nonce === 10 && b.eat.CLAIM_LABELS.dbgstat === 263 && b.eat.CLAIM_LABELS.eat_profile === 265);
|
|
21
|
+
check("b.eat.DBGSTAT enum", b.eat.DBGSTAT["disabled-permanently"] === 3 && b.eat.DBGSTAT.enabled === 0);
|
|
22
|
+
check("b.eat.EatError exposed", typeof b.eat.EatError === "function");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function testRoundTripAndLabels() {
|
|
26
|
+
var nonce = b.crypto.generateBytes(16);
|
|
27
|
+
var eat = await b.eat.sign(
|
|
28
|
+
{ nonce: nonce, ueid: Buffer.from([1, 2, 3, 4]), oemid: Buffer.from("acme"), dbgstat: "disabled-permanently",
|
|
29
|
+
eat_profile: "https://example.com/eat/p1", iss: "device-ca", iat: _now() },
|
|
30
|
+
{ alg: "ES256", privateKey: EC.privateKey });
|
|
31
|
+
var v = await b.eat.verify(eat, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedNonce: nonce });
|
|
32
|
+
check("verify: friendly EAT claim names returned", Buffer.isBuffer(v.claims.ueid) && v.claims.eat_profile === "https://example.com/eat/p1");
|
|
33
|
+
check("verify: dbgstat decoded to enum name", v.claims.dbgstat === "disabled-permanently");
|
|
34
|
+
check("verify: standard claim (iss) still named", v.claims.iss === "device-ca");
|
|
35
|
+
check("verify: raw Map uses RFC 9711 integer labels", v.raw.get(10) !== undefined && v.raw.get(263) === 3 && v.raw.get(265) === "https://example.com/eat/p1");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function testNonceBinding() {
|
|
39
|
+
var nonce = b.crypto.generateBytes(16);
|
|
40
|
+
var eat = await b.eat.sign({ nonce: nonce, ueid: Buffer.from([9]) }, { alg: "ES256", privateKey: EC.privateKey });
|
|
41
|
+
var ok = await b.eat.verify(eat, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedNonce: nonce });
|
|
42
|
+
check("nonce: matching expectedNonce accepted", Buffer.isBuffer(ok.claims.nonce));
|
|
43
|
+
var mismatch = null;
|
|
44
|
+
try { await b.eat.verify(eat, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedNonce: b.crypto.generateBytes(16) }); } catch (e) { mismatch = e; }
|
|
45
|
+
check("nonce: mismatch refused (replay/freshness defense)", mismatch && mismatch.code === "eat/nonce-mismatch");
|
|
46
|
+
// Token with no nonce but RP expects one → refused.
|
|
47
|
+
var noNonce = await b.eat.sign({ ueid: Buffer.from([1]) }, { alg: "ES256", privateKey: EC.privateKey });
|
|
48
|
+
var missing = null;
|
|
49
|
+
try { await b.eat.verify(noNonce, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedNonce: nonce }); } catch (e) { missing = e; }
|
|
50
|
+
check("nonce: expectedNonce but no eat_nonce claim refused", missing && missing.code === "eat/nonce-missing");
|
|
51
|
+
// Array-of-nonces (multi-verifier) membership.
|
|
52
|
+
var multi = await b.eat.sign({ nonce: [b.crypto.generateBytes(16), nonce] }, { alg: "ES256", privateKey: EC.privateKey });
|
|
53
|
+
var m = await b.eat.verify(multi, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedNonce: nonce });
|
|
54
|
+
check("nonce: array form matches when one entry equals expectedNonce", Array.isArray(m.claims.nonce));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function testDebugStatusAndProfile() {
|
|
58
|
+
var enabled = await b.eat.sign({ dbgstat: "enabled" }, { alg: "ES256", privateKey: EC.privateKey });
|
|
59
|
+
var e1 = null;
|
|
60
|
+
try { await b.eat.verify(enabled, { algorithms: ["ES256"], publicKey: EC.publicKey, requireDebugDisabled: true }); } catch (e) { e1 = e; }
|
|
61
|
+
check("dbgstat: enabled refused under requireDebugDisabled", e1 && e1.code === "eat/debug-not-disabled");
|
|
62
|
+
|
|
63
|
+
var absent = await b.eat.sign({ ueid: Buffer.from([1]) }, { alg: "ES256", privateKey: EC.privateKey });
|
|
64
|
+
var e2 = null;
|
|
65
|
+
try { await b.eat.verify(absent, { algorithms: ["ES256"], publicKey: EC.publicKey, requireDebugDisabled: true }); } catch (e) { e2 = e; }
|
|
66
|
+
check("dbgstat: absent refused under requireDebugDisabled", e2 && e2.code === "eat/debug-not-disabled");
|
|
67
|
+
|
|
68
|
+
var disabled = await b.eat.sign({ dbgstat: "disabled-fully-and-permanently" }, { alg: "ES256", privateKey: EC.privateKey });
|
|
69
|
+
var ok = await b.eat.verify(disabled, { algorithms: ["ES256"], publicKey: EC.publicKey, requireDebugDisabled: true });
|
|
70
|
+
check("dbgstat: a disabled state passes", ok.claims.dbgstat === "disabled-fully-and-permanently");
|
|
71
|
+
|
|
72
|
+
var prof = await b.eat.sign({ eat_profile: "p-A" }, { alg: "ES256", privateKey: EC.privateKey });
|
|
73
|
+
var e3 = null;
|
|
74
|
+
try { await b.eat.verify(prof, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedProfile: "p-B" }); } catch (e) { e3 = e; }
|
|
75
|
+
check("profile: expectedProfile mismatch refused", e3 && e3.code === "eat/profile-mismatch");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function testValidationAndTamper() {
|
|
79
|
+
var bad = null;
|
|
80
|
+
try { await b.eat.sign({ dbgstat: "bogus" }, { alg: "ES256", privateKey: EC.privateKey }); } catch (e) { bad = e; }
|
|
81
|
+
check("sign: bad dbgstat enum refused", bad && bad.code === "eat/bad-dbgstat");
|
|
82
|
+
|
|
83
|
+
var eat = await b.eat.sign({ nonce: b.crypto.generateBytes(16) }, { alg: "ES256", privateKey: EC.privateKey });
|
|
84
|
+
var t = Buffer.from(eat); t[t.length - 1] ^= 0xff;
|
|
85
|
+
var tampered = null;
|
|
86
|
+
try { await b.eat.verify(t, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { tampered = e; }
|
|
87
|
+
check("verify: tampered token refused by COSE signature check", tampered && tampered.code === "cose/bad-signature");
|
|
88
|
+
|
|
89
|
+
// exp from the CWT layer still enforced through EAT.
|
|
90
|
+
var expired = await b.eat.sign({ exp: _now() - 3600, ueid: Buffer.from([1]) }, { alg: "ES256", privateKey: EC.privateKey });
|
|
91
|
+
var exp = null;
|
|
92
|
+
try { await b.eat.verify(expired, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { exp = e; }
|
|
93
|
+
check("verify: expired EAT refused (delegated to cwt)", exp && exp.code === "cwt/expired");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function run() {
|
|
97
|
+
testSurface();
|
|
98
|
+
await testRoundTripAndLabels();
|
|
99
|
+
await testNonceBinding();
|
|
100
|
+
await testDebugStatusAndProfile();
|
|
101
|
+
await testValidationAndTamper();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { run: run };
|
|
105
|
+
|
|
106
|
+
if (require.main === module) {
|
|
107
|
+
run().then(
|
|
108
|
+
function () { console.log("[eat] OK — " + helpers.getChecks() + " checks passed"); },
|
|
109
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Layer 0 — b.scitt (SCITT signed statements) over b.cose.
|
|
4
|
+
* Covers the signed-statement round-trip, the integrity-protected
|
|
5
|
+
* CWT_Claims (iss/sub) binding, refusal of a bare COSE_Sign1 that
|
|
6
|
+
* carries no CWT_Claims, expected-issuer/subject enforcement, the
|
|
7
|
+
* reserved-claim guard, content-type media-type strings, and that the
|
|
8
|
+
* signature checks delegate to b.cose. Exercises the classical (ES256 /
|
|
9
|
+
* EdDSA) algorithms and the ML-DSA-87 PQC-forward path.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
var b = require("../../index");
|
|
13
|
+
var helpers = require("../helpers");
|
|
14
|
+
var check = helpers.check;
|
|
15
|
+
var nodeCrypto = require("node:crypto");
|
|
16
|
+
|
|
17
|
+
var EC = nodeCrypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
18
|
+
var ED = nodeCrypto.generateKeyPairSync("ed25519");
|
|
19
|
+
var SBOM = Buffer.from('{"spdxVersion":"SPDX-2.3","name":"widget"}', "utf8");
|
|
20
|
+
|
|
21
|
+
function testSurface() {
|
|
22
|
+
check("b.scitt.signStatement exposed", typeof b.scitt.signStatement === "function");
|
|
23
|
+
check("b.scitt.verifyStatement exposed", typeof b.scitt.verifyStatement === "function");
|
|
24
|
+
check("b.scitt.CWT_CLAIMS_LABEL is RFC 9597 label 15", b.scitt.CWT_CLAIMS_LABEL === 15);
|
|
25
|
+
check("b.scitt.ScittError exposed", typeof b.scitt.ScittError === "function");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function testRoundTrip() {
|
|
29
|
+
var stmt = await b.scitt.signStatement(SBOM, {
|
|
30
|
+
alg: "ES256", privateKey: EC.privateKey,
|
|
31
|
+
issuer: "https://builder.example", subject: "pkg:npm/widget@1.2.3",
|
|
32
|
+
contentType: "application/spdx+json", claims: { 6: 1700000000 },
|
|
33
|
+
});
|
|
34
|
+
check("signed statement is bytes", Buffer.isBuffer(stmt));
|
|
35
|
+
|
|
36
|
+
var out = await b.scitt.verifyStatement(stmt, {
|
|
37
|
+
algorithms: ["ES256"], publicKey: EC.publicKey,
|
|
38
|
+
expectedIssuer: "https://builder.example", expectedSubject: "pkg:npm/widget@1.2.3",
|
|
39
|
+
});
|
|
40
|
+
check("verify: payload round-trips", out.payload.equals(SBOM));
|
|
41
|
+
check("verify: issuer extracted from CWT_Claims", out.issuer === "https://builder.example");
|
|
42
|
+
check("verify: subject extracted from CWT_Claims", out.subject === "pkg:npm/widget@1.2.3");
|
|
43
|
+
check("verify: extra CWT claim (iat) preserved", out.cwtClaims.get(6) === 1700000000);
|
|
44
|
+
check("verify: alg reported", out.alg === "ES256");
|
|
45
|
+
// Content type is a media-type STRING in the protected header (label 3).
|
|
46
|
+
check("verify: content-type media-type string in protected header", out.protectedHeaders.get(3) === "application/spdx+json");
|
|
47
|
+
// CWT_Claims live in the INTEGRITY-PROTECTED header (label 15), not unprotected.
|
|
48
|
+
check("verify: CWT_Claims is in the protected header", out.protectedHeaders.get(15) instanceof Map);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function testIdentityBindingRefusals() {
|
|
52
|
+
var stmt = await b.scitt.signStatement(SBOM, {
|
|
53
|
+
alg: "ES256", privateKey: EC.privateKey, issuer: "iss-a", subject: "sub-a",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
var e1 = null;
|
|
57
|
+
try {
|
|
58
|
+
await b.scitt.verifyStatement(stmt, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedIssuer: "iss-b" });
|
|
59
|
+
} catch (e) { e1 = e; }
|
|
60
|
+
check("verify: issuer mismatch refused", e1 && e1.code === "scitt/issuer-mismatch");
|
|
61
|
+
|
|
62
|
+
var e2 = null;
|
|
63
|
+
try {
|
|
64
|
+
await b.scitt.verifyStatement(stmt, { algorithms: ["ES256"], publicKey: EC.publicKey, expectedSubject: "sub-b" });
|
|
65
|
+
} catch (e) { e2 = e; }
|
|
66
|
+
check("verify: subject mismatch refused", e2 && e2.code === "scitt/subject-mismatch");
|
|
67
|
+
|
|
68
|
+
// A bare COSE_Sign1 (no CWT_Claims header) is NOT a SCITT statement.
|
|
69
|
+
var bare = await b.cose.sign(SBOM, { alg: "ES256", privateKey: EC.privateKey });
|
|
70
|
+
var e3 = null;
|
|
71
|
+
try { await b.scitt.verifyStatement(bare, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { e3 = e; }
|
|
72
|
+
check("verify: bare COSE_Sign1 (no CWT_Claims) refused", e3 && e3.code === "scitt/missing-cwt-claims");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function testSignRefusals() {
|
|
76
|
+
var refusals = [
|
|
77
|
+
["missing issuer", { alg: "ES256", privateKey: EC.privateKey, subject: "s" }, "scitt/bad-issuer"],
|
|
78
|
+
["empty issuer", { alg: "ES256", privateKey: EC.privateKey, issuer: "", subject: "s" }, "scitt/bad-issuer"],
|
|
79
|
+
["missing subject", { alg: "ES256", privateKey: EC.privateKey, issuer: "i" }, "scitt/bad-subject"],
|
|
80
|
+
["iss override via claims", { alg: "ES256", privateKey: EC.privateKey, issuer: "i", subject: "s", claims: { 1: "evil" } }, "scitt/reserved-claim"],
|
|
81
|
+
["sub override via claims", { alg: "ES256", privateKey: EC.privateKey, issuer: "i", subject: "s", claims: { 2: "evil" } }, "scitt/reserved-claim"],
|
|
82
|
+
["non-integer claim label", { alg: "ES256", privateKey: EC.privateKey, issuer: "i", subject: "s", claims: { 1.5: "x" } }, "scitt/bad-claim-label"],
|
|
83
|
+
];
|
|
84
|
+
for (var i = 0; i < refusals.length; i++) {
|
|
85
|
+
var err = null;
|
|
86
|
+
try { await b.scitt.signStatement(SBOM, refusals[i][1]); } catch (e) { err = e; }
|
|
87
|
+
check("signStatement refuses: " + refusals[i][0], err && err.code === refusals[i][2]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function testTamperAndDelegation() {
|
|
92
|
+
var stmt = await b.scitt.signStatement(SBOM, {
|
|
93
|
+
alg: "ES256", privateKey: EC.privateKey, issuer: "i", subject: "s",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Tampering the bytes fails the underlying COSE signature check.
|
|
97
|
+
var bad = Buffer.from(stmt); bad[bad.length - 1] ^= 0xff;
|
|
98
|
+
var t = null;
|
|
99
|
+
try { await b.scitt.verifyStatement(bad, { algorithms: ["ES256"], publicKey: EC.publicKey }); } catch (e) { t = e; }
|
|
100
|
+
check("verify: tampered statement refused by COSE signature", t && t.code === "cose/bad-signature");
|
|
101
|
+
|
|
102
|
+
// The algorithm allowlist is mandatory and delegated to b.cose.verify.
|
|
103
|
+
var a = null;
|
|
104
|
+
try { await b.scitt.verifyStatement(stmt, { algorithms: ["EdDSA"], publicKey: EC.publicKey }); } catch (e) { a = e; }
|
|
105
|
+
check("verify: alg outside allowlist refused (delegated to cose)", a && a.code === "cose/alg-not-allowed");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function testEdDsaAndPqc() {
|
|
109
|
+
// EdDSA (final COSE id, interoperable today).
|
|
110
|
+
var s1 = await b.scitt.signStatement(SBOM, { alg: "EdDSA", privateKey: ED.privateKey, issuer: "i", subject: "s" });
|
|
111
|
+
var o1 = await b.scitt.verifyStatement(s1, { algorithms: ["EdDSA"], publicKey: ED.publicKey });
|
|
112
|
+
check("EdDSA: round-trips with iss/sub", o1.issuer === "i" && o1.subject === "s");
|
|
113
|
+
|
|
114
|
+
// ML-DSA-87 (PQC-forward) — skip gracefully if the runtime lacks it.
|
|
115
|
+
var mldsa = null;
|
|
116
|
+
try { mldsa = nodeCrypto.generateKeyPairSync("ml-dsa-87"); } catch (_e) { mldsa = null; }
|
|
117
|
+
if (mldsa) {
|
|
118
|
+
var s2 = await b.scitt.signStatement(SBOM, { alg: "ML-DSA-87", privateKey: mldsa.privateKey, issuer: "pqc-iss", subject: "pqc-sub" });
|
|
119
|
+
var o2 = await b.scitt.verifyStatement(s2, { algorithms: ["ML-DSA-87"], publicKey: mldsa.publicKey });
|
|
120
|
+
check("ML-DSA-87: round-trips with iss/sub", o2.issuer === "pqc-iss" && o2.subject === "pqc-sub");
|
|
121
|
+
} else {
|
|
122
|
+
check("ML-DSA-87: runtime lacks ml-dsa-87 — classical path covers the contract", true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function testExternalAadBinding() {
|
|
127
|
+
var aad = Buffer.from("registration-context", "utf8");
|
|
128
|
+
var stmt = await b.scitt.signStatement(SBOM, {
|
|
129
|
+
alg: "ES256", privateKey: EC.privateKey, issuer: "i", subject: "s", externalAad: aad,
|
|
130
|
+
});
|
|
131
|
+
var ok = await b.scitt.verifyStatement(stmt, { algorithms: ["ES256"], publicKey: EC.publicKey, externalAad: aad });
|
|
132
|
+
check("externalAad: matching context verifies", ok.subject === "s");
|
|
133
|
+
|
|
134
|
+
var mism = null;
|
|
135
|
+
try {
|
|
136
|
+
await b.scitt.verifyStatement(stmt, { algorithms: ["ES256"], publicKey: EC.publicKey, externalAad: Buffer.from("other") });
|
|
137
|
+
} catch (e) { mism = e; }
|
|
138
|
+
check("externalAad: mismatched context refused", mism && mism.code === "cose/bad-signature");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function run() {
|
|
142
|
+
testSurface();
|
|
143
|
+
await testRoundTrip();
|
|
144
|
+
await testIdentityBindingRefusals();
|
|
145
|
+
await testSignRefusals();
|
|
146
|
+
await testTamperAndDelegation();
|
|
147
|
+
await testEdDsaAndPqc();
|
|
148
|
+
await testExternalAadBinding();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { run: run };
|
|
152
|
+
|
|
153
|
+
if (require.main === module) {
|
|
154
|
+
run().then(
|
|
155
|
+
function () { console.log("[scitt] OK — " + helpers.getChecks() + " checks passed"); },
|
|
156
|
+
function (e) { console.error("FAIL:", e && e.stack || e); process.exit(1); }
|
|
157
|
+
);
|
|
158
|
+
}
|