@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,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.eat
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title Entity Attestation Token (EAT)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* RFC 9711 Entity Attestation Token — a CWT (or JWT) profile that
|
|
9
|
+
* carries attestation claims describing the state of a device or
|
|
10
|
+
* software entity: a freshness nonce, a Universal Entity ID, OEM /
|
|
11
|
+
* hardware identifiers, debug status, software measurements, and
|
|
12
|
+
* nested submodule attestations. EAT is the token a Relying Party
|
|
13
|
+
* asks a device to produce to prove what it is and what state it is
|
|
14
|
+
* in. This module is the EAT profile over <code>b.cwt</code> — it
|
|
15
|
+
* maps the RFC 9711 claim names to their CWT claim-key integer
|
|
16
|
+
* labels and adds the attestation-specific verification.
|
|
17
|
+
*
|
|
18
|
+
* <code>b.eat.sign(claims, opts)</code> takes a friendly claims
|
|
19
|
+
* object (<code>nonce</code>, <code>ueid</code>, <code>oemid</code>,
|
|
20
|
+
* <code>dbgstat</code>, <code>eat_profile</code>,
|
|
21
|
+
* <code>measurements</code>, <code>submods</code>, … plus the
|
|
22
|
+
* standard CWT claims) and signs it as a CWT.
|
|
23
|
+
*
|
|
24
|
+
* <code>b.eat.verify(eat, opts)</code> verifies the CWT (signature +
|
|
25
|
+
* alg allowlist + time claims, via <code>b.cwt</code>) and then
|
|
26
|
+
* enforces the attestation contract:
|
|
27
|
+
*
|
|
28
|
+
* - <strong>Nonce binding</strong> — when the Relying Party supplied
|
|
29
|
+
* a fresh <code>expectedNonce</code>, the token's
|
|
30
|
+
* <code>eat_nonce</code> (claim 10) MUST match it (constant-time
|
|
31
|
+
* compare). This is the freshness / anti-replay defense: without
|
|
32
|
+
* it a captured attestation can be replayed indefinitely.
|
|
33
|
+
* - <strong>Debug status</strong> — <code>requireDebugDisabled</code>
|
|
34
|
+
* refuses a token whose <code>dbgstat</code> is
|
|
35
|
+
* <code>enabled</code> (0) or absent; only the disabled states
|
|
36
|
+
* (1–4) pass.
|
|
37
|
+
* - <strong>Profile</strong> — <code>expectedProfile</code> pins the
|
|
38
|
+
* <code>eat_profile</code> claim.
|
|
39
|
+
*
|
|
40
|
+
* Signing algorithms follow <code>b.cwt</code> / <code>b.cose</code>:
|
|
41
|
+
* ES256/384/512 + EdDSA (interoperable today) and ML-DSA-87.
|
|
42
|
+
*
|
|
43
|
+
* @card
|
|
44
|
+
* RFC 9711 Entity Attestation Token over b.cwt — sign / verify
|
|
45
|
+
* device + software attestation claims with verifier-nonce binding,
|
|
46
|
+
* debug-status policy, and profile pinning.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
var cwt = require("./cwt");
|
|
50
|
+
var bCrypto = require("./crypto");
|
|
51
|
+
var validateOpts = require("./validate-opts");
|
|
52
|
+
var { defineClass } = require("./framework-error");
|
|
53
|
+
|
|
54
|
+
var EatError = defineClass("EatError", { alwaysPermanent: true });
|
|
55
|
+
|
|
56
|
+
// RFC 9711 / IANA CWT Claims registry claim keys.
|
|
57
|
+
var EAT = { // allow:raw-byte-literal — RFC 9711 / IANA CWT claim-key labels, not byte sizes
|
|
58
|
+
nonce: 10, ueid: 256, sueids: 257, oemid: 258, hwmodel: 259, hwversion: 260, // allow:raw-byte-literal — CWT claim keys
|
|
59
|
+
uptime: 261, oemboot: 262, dbgstat: 263, location: 264, eat_profile: 265, // allow:raw-byte-literal — CWT claim keys
|
|
60
|
+
submods: 266, swname: 270, swversion: 271, manifests: 272, measurements: 273, // allow:raw-byte-literal — CWT claim keys
|
|
61
|
+
};
|
|
62
|
+
var EAT_BY_LABEL = {};
|
|
63
|
+
Object.keys(EAT).forEach(function (k) { EAT_BY_LABEL[EAT[k]] = k; });
|
|
64
|
+
|
|
65
|
+
// RFC 9711 §4.3.1 debug-status enumeration.
|
|
66
|
+
var DBGSTAT = {
|
|
67
|
+
"enabled": 0, "disabled": 1, "disabled-since-boot": 2,
|
|
68
|
+
"disabled-permanently": 3, "disabled-fully-and-permanently": 4,
|
|
69
|
+
};
|
|
70
|
+
var DBGSTAT_BY_VALUE = {};
|
|
71
|
+
Object.keys(DBGSTAT).forEach(function (k) { DBGSTAT_BY_VALUE[DBGSTAT[k]] = k; });
|
|
72
|
+
|
|
73
|
+
// Standard CWT claim labels → names (RFC 8392 §3.1.1) so EAT's
|
|
74
|
+
// friendly output names the standard claims alongside the EAT ones.
|
|
75
|
+
var STD_NAME = { 1: "iss", 2: "sub", 3: "aud", 4: "exp", 5: "nbf", 6: "iat", 7: "cti" };
|
|
76
|
+
|
|
77
|
+
function _toBuf(x) {
|
|
78
|
+
if (Buffer.isBuffer(x)) return x;
|
|
79
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
80
|
+
if (typeof x === "string") return Buffer.from(x, "utf8");
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _nonceMatches(claimValue, expected) {
|
|
85
|
+
var exp = _toBuf(expected);
|
|
86
|
+
if (!exp) return false;
|
|
87
|
+
// eat_nonce may be a single byte string or an array of them (one per
|
|
88
|
+
// verifier). Constant-time compare against each candidate.
|
|
89
|
+
var candidates = Array.isArray(claimValue) ? claimValue : [claimValue];
|
|
90
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
91
|
+
var c = _toBuf(candidates[i]);
|
|
92
|
+
if (c && c.length === exp.length && bCrypto.timingSafeEqual(c, exp)) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @primitive b.eat.sign
|
|
99
|
+
* @signature b.eat.sign(claims, opts)
|
|
100
|
+
* @since 0.12.35
|
|
101
|
+
* @status stable
|
|
102
|
+
* @related b.eat.verify, b.cwt.sign
|
|
103
|
+
*
|
|
104
|
+
* Sign EAT attestation claims into a CWT. EAT claim names map to their
|
|
105
|
+
* RFC 9711 integer labels; <code>dbgstat</code> accepts the enum name
|
|
106
|
+
* (<code>"disabled-since-boot"</code>) or its integer. Standard CWT
|
|
107
|
+
* claims (<code>iss</code> / <code>exp</code> / …) pass through to
|
|
108
|
+
* <code>b.cwt.sign</code>.
|
|
109
|
+
*
|
|
110
|
+
* @opts
|
|
111
|
+
* {
|
|
112
|
+
* alg: string, // COSE signing alg (ES256 / EdDSA / ML-DSA-87 / …)
|
|
113
|
+
* privateKey: object, // signing key
|
|
114
|
+
* kid?: string,
|
|
115
|
+
* tagged?: boolean, // CWT tag 61
|
|
116
|
+
* }
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* var eat = await b.eat.sign(
|
|
120
|
+
* { nonce: rpNonce, ueid: deviceUeid, oemid: oem, dbgstat: "disabled-permanently",
|
|
121
|
+
* eat_profile: "https://example.com/eat/profile-1", iat: Math.floor(Date.now()/1000) },
|
|
122
|
+
* { alg: "ES256", privateKey: deviceKey });
|
|
123
|
+
*/
|
|
124
|
+
async function sign(claims, opts) {
|
|
125
|
+
if (!claims || typeof claims !== "object" || Array.isArray(claims)) {
|
|
126
|
+
throw new EatError("eat/bad-claims", "eat.sign: claims must be a plain object");
|
|
127
|
+
}
|
|
128
|
+
validateOpts.requireObject(opts, "eat.sign", EatError);
|
|
129
|
+
// Translate EAT claim names to their integer labels into a Map (so
|
|
130
|
+
// the integer keys survive — a plain object would stringify them).
|
|
131
|
+
// Standard CWT names (iss/sub/aud/exp/nbf/iat/cti) + custom keys are
|
|
132
|
+
// left for b.cwt.sign to handle.
|
|
133
|
+
var mapped = new Map();
|
|
134
|
+
var keys = Object.keys(claims);
|
|
135
|
+
for (var i = 0; i < keys.length; i++) {
|
|
136
|
+
var name = keys[i];
|
|
137
|
+
var value = claims[name];
|
|
138
|
+
if (name === "dbgstat" && typeof value === "string") {
|
|
139
|
+
if (!Object.prototype.hasOwnProperty.call(DBGSTAT, value)) {
|
|
140
|
+
throw new EatError("eat/bad-dbgstat",
|
|
141
|
+
"eat.sign: dbgstat must be one of " + Object.keys(DBGSTAT).join(" / ") + " (or an integer 0-4)");
|
|
142
|
+
}
|
|
143
|
+
value = DBGSTAT[value];
|
|
144
|
+
}
|
|
145
|
+
mapped.set(Object.prototype.hasOwnProperty.call(EAT, name) ? EAT[name] : name, value);
|
|
146
|
+
}
|
|
147
|
+
return cwt.sign(mapped, opts);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @primitive b.eat.verify
|
|
152
|
+
* @signature b.eat.verify(eat, opts)
|
|
153
|
+
* @since 0.12.35
|
|
154
|
+
* @status stable
|
|
155
|
+
* @related b.eat.sign, b.cwt.verify
|
|
156
|
+
*
|
|
157
|
+
* Verify an EAT and return its attestation claims. Delegates the CWT
|
|
158
|
+
* signature + algorithm-allowlist + time-claim checks to
|
|
159
|
+
* <code>b.cwt.verify</code>, then enforces the attestation contract:
|
|
160
|
+
* the <code>eat_nonce</code> must match <code>expectedNonce</code>
|
|
161
|
+
* (when supplied — the freshness/anti-replay binding),
|
|
162
|
+
* <code>requireDebugDisabled</code> refuses a non-disabled
|
|
163
|
+
* <code>dbgstat</code>, and <code>expectedProfile</code> pins
|
|
164
|
+
* <code>eat_profile</code>.
|
|
165
|
+
*
|
|
166
|
+
* @opts
|
|
167
|
+
* {
|
|
168
|
+
* algorithms: string[], // required — accepted COSE algs
|
|
169
|
+
* publicKey?: object,
|
|
170
|
+
* keyResolver?: function,
|
|
171
|
+
* expectedNonce?: Buffer, // require eat_nonce to match (freshness)
|
|
172
|
+
* requireDebugDisabled?: boolean, // refuse dbgstat enabled / absent
|
|
173
|
+
* expectedProfile?: string, // pin eat_profile
|
|
174
|
+
* expectedIssuer?: string, // forwarded to b.cwt.verify
|
|
175
|
+
* expectedAudience?: string,
|
|
176
|
+
* clockSkewSec?: number,
|
|
177
|
+
* now?: number,
|
|
178
|
+
* externalAad?: Buffer,
|
|
179
|
+
* }
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* var att = await b.eat.verify(eat, { algorithms: ["ES256"], publicKey: devicePub, expectedNonce: rpNonce, requireDebugDisabled: true });
|
|
183
|
+
* // → { claims: { nonce, ueid, dbgstat: "disabled-permanently", ... }, raw: Map, alg }
|
|
184
|
+
*/
|
|
185
|
+
async function verify(eat, opts) {
|
|
186
|
+
validateOpts.requireObject(opts, "eat.verify", EatError);
|
|
187
|
+
var out = await cwt.verify(eat, {
|
|
188
|
+
algorithms: opts.algorithms, publicKey: opts.publicKey, keyResolver: opts.keyResolver,
|
|
189
|
+
expectedIssuer: opts.expectedIssuer, expectedAudience: opts.expectedAudience,
|
|
190
|
+
clockSkewSec: opts.clockSkewSec, now: opts.now, externalAad: opts.externalAad,
|
|
191
|
+
});
|
|
192
|
+
var raw = out.raw;
|
|
193
|
+
|
|
194
|
+
// Nonce binding — the freshness / anti-replay defense. When the RP
|
|
195
|
+
// supplied a nonce, the token MUST carry a matching eat_nonce.
|
|
196
|
+
if (opts.expectedNonce != null) {
|
|
197
|
+
if (!raw.has(EAT.nonce)) {
|
|
198
|
+
throw new EatError("eat/nonce-missing", "eat.verify: expectedNonce supplied but token has no eat_nonce claim");
|
|
199
|
+
}
|
|
200
|
+
if (!_nonceMatches(raw.get(EAT.nonce), opts.expectedNonce)) {
|
|
201
|
+
throw new EatError("eat/nonce-mismatch", "eat.verify: eat_nonce does not match expectedNonce (stale / replayed attestation)");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Debug status — refuse a token that can't prove debug is disabled.
|
|
206
|
+
if (opts.requireDebugDisabled === true) {
|
|
207
|
+
var ds = raw.get(EAT.dbgstat);
|
|
208
|
+
if (typeof ds !== "number" || ds < DBGSTAT.disabled) {
|
|
209
|
+
throw new EatError("eat/debug-not-disabled",
|
|
210
|
+
"eat.verify: requireDebugDisabled — dbgstat is " +
|
|
211
|
+
(ds === undefined ? "absent" : (DBGSTAT_BY_VALUE[ds] || ds)) + ", not a disabled state");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (opts.expectedProfile != null && raw.get(EAT.eat_profile) !== opts.expectedProfile) {
|
|
216
|
+
throw new EatError("eat/profile-mismatch", "eat.verify: eat_profile does not match expectedProfile");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build friendly claims: EAT + standard labels → names; decode the
|
|
220
|
+
// dbgstat enum to its name.
|
|
221
|
+
var claims = {};
|
|
222
|
+
raw.forEach(function (v, k) {
|
|
223
|
+
var name = Object.prototype.hasOwnProperty.call(EAT_BY_LABEL, k) ? EAT_BY_LABEL[k]
|
|
224
|
+
: (Object.prototype.hasOwnProperty.call(STD_NAME, k) ? STD_NAME[k] : k);
|
|
225
|
+
if (name === "dbgstat" && typeof v === "number" && Object.prototype.hasOwnProperty.call(DBGSTAT_BY_VALUE, v)) {
|
|
226
|
+
v = DBGSTAT_BY_VALUE[v];
|
|
227
|
+
}
|
|
228
|
+
claims[name] = v;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return { claims: claims, raw: raw, alg: out.alg, protectedHeaders: out.protectedHeaders };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
sign: sign,
|
|
236
|
+
verify: verify,
|
|
237
|
+
CLAIM_LABELS: EAT,
|
|
238
|
+
DBGSTAT: DBGSTAT,
|
|
239
|
+
EatError: EatError,
|
|
240
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.scitt
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title SCITT signed statements
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* A SCITT (Supply Chain Integrity, Transparency, and Trust) signed
|
|
9
|
+
* statement is a <code>b.cose</code> COSE_Sign1 that makes a signed,
|
|
10
|
+
* attributable claim <em>about an artifact</em> — a signed SBOM, a
|
|
11
|
+
* build attestation, a release approval. The artifact (or a hash /
|
|
12
|
+
* reference to it) is the payload; the issuer and the subject are
|
|
13
|
+
* carried in the integrity-protected <strong>CWT_Claims</strong>
|
|
14
|
+
* header (label 15, RFC 9597): <code>iss</code> (label 1) is who
|
|
15
|
+
* makes the statement, <code>sub</code> (label 2) is the artifact the
|
|
16
|
+
* statement is about. This module builds and verifies that envelope
|
|
17
|
+
* over <code>b.cose</code> + <code>b.cbor</code>.
|
|
18
|
+
*
|
|
19
|
+
* <code>b.scitt.signStatement(payload, opts)</code> produces the
|
|
20
|
+
* COSE_Sign1, placing <code>iss</code> / <code>sub</code> (plus any
|
|
21
|
+
* extra CWT claims) in the protected CWT_Claims header and declaring
|
|
22
|
+
* the payload media type as the COSE content type.
|
|
23
|
+
* <code>b.scitt.verifyStatement(statement, opts)</code> verifies the
|
|
24
|
+
* signature (delegating the mandatory algorithm allowlist to
|
|
25
|
+
* <code>b.cose.verify</code>), then enforces that a CWT_Claims header
|
|
26
|
+
* with both <code>iss</code> and <code>sub</code> is present —
|
|
27
|
+
* refusing a statement that omits the issuer/subject binding — and
|
|
28
|
+
* optionally checks them against expected values.
|
|
29
|
+
*
|
|
30
|
+
* The signing algorithms are exactly <code>b.cose</code>'s: the
|
|
31
|
+
* classical ES256/384/512 + EdDSA (final COSE ids, interoperable
|
|
32
|
+
* today) and ML-DSA-87 (PQC-forward, draft COSE id). Because the
|
|
33
|
+
* identity binding lives in the protected header it is covered by the
|
|
34
|
+
* signature and cannot be substituted without detection.
|
|
35
|
+
*
|
|
36
|
+
* <strong>Scope.</strong> This is the <em>issuer half</em> of SCITT —
|
|
37
|
+
* producing and verifying signed statements, which is buildable today
|
|
38
|
+
* on finalized RFCs (RFC 9052 COSE, RFC 9597 CWT_Claims header, RFC
|
|
39
|
+
* 8392 iss/sub). The <em>transparency receipt</em> (an inclusion proof
|
|
40
|
+
* from an append-only transparency service, COSE Receipts /
|
|
41
|
+
* draft-ietf-cose-merkle-tree-proofs) and the transparency-service
|
|
42
|
+
* registration protocol (draft-ietf-scitt-*) are deferred until those
|
|
43
|
+
* drafts publish — a signed statement produced here is the input a
|
|
44
|
+
* transparency service registers, and the receipt format is the part
|
|
45
|
+
* still in flux. Re-open on COSE-Receipts publication.
|
|
46
|
+
*
|
|
47
|
+
* @card
|
|
48
|
+
* SCITT signed statements (RFC 9052 COSE + RFC 9597 CWT_Claims) — a
|
|
49
|
+
* signed, issuer/subject-bound claim about an artifact (SBOM /
|
|
50
|
+
* attestation / approval). Composes b.cose; transparency receipts
|
|
51
|
+
* deferred to the COSE-Receipts draft.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
var cose = require("./cose");
|
|
55
|
+
var validateOpts = require("./validate-opts");
|
|
56
|
+
var { defineClass } = require("./framework-error");
|
|
57
|
+
|
|
58
|
+
var ScittError = defineClass("ScittError", { alwaysPermanent: true });
|
|
59
|
+
|
|
60
|
+
// CWT_Claims header label (RFC 9597) and the two SCITT-required claim
|
|
61
|
+
// labels inside it (RFC 8392 §3.1.1): iss = who states, sub = about what.
|
|
62
|
+
var HDR_CWT_CLAIMS = 15;
|
|
63
|
+
var CLAIM_ISS = 1;
|
|
64
|
+
var CLAIM_SUB = 2;
|
|
65
|
+
|
|
66
|
+
function _requireNonEmptyString(v, name) {
|
|
67
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
68
|
+
throw new ScittError("scitt/bad-" + name,
|
|
69
|
+
"scitt.signStatement: opts." + name + " is required and must be a non-empty string");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @primitive b.scitt.signStatement
|
|
75
|
+
* @signature b.scitt.signStatement(payload, opts)
|
|
76
|
+
* @since 0.12.37
|
|
77
|
+
* @status experimental
|
|
78
|
+
* @compliance soc2, cra
|
|
79
|
+
* @related b.scitt.verifyStatement, b.cose.sign
|
|
80
|
+
*
|
|
81
|
+
* Produce a SCITT signed statement: a COSE_Sign1 over
|
|
82
|
+
* <code>payload</code> (the artifact bytes, or a hash / reference to
|
|
83
|
+
* it) whose integrity-protected CWT_Claims header (label 15) binds the
|
|
84
|
+
* issuer (<code>iss</code>) and subject (<code>sub</code>). Declare the
|
|
85
|
+
* payload media type via <code>contentType</code> so a consumer knows
|
|
86
|
+
* how to interpret it.
|
|
87
|
+
*
|
|
88
|
+
* @opts
|
|
89
|
+
* {
|
|
90
|
+
* alg: string, // b.cose alg: "ES256" | … | "ML-DSA-87"
|
|
91
|
+
* privateKey: object, // matching KeyObject or PEM
|
|
92
|
+
* issuer: string, // → CWT_Claims iss (label 1) — who makes the statement
|
|
93
|
+
* subject: string, // → CWT_Claims sub (label 2) — the artifact the statement is about
|
|
94
|
+
* contentType?: number|string, // payload media type (e.g. "application/spdx+json")
|
|
95
|
+
* claims?: object, // extra CWT claims by integer label, merged into CWT_Claims
|
|
96
|
+
* kid?: string, // → unprotected header label 4
|
|
97
|
+
* externalAad?: Buffer, // bound into the signature
|
|
98
|
+
* }
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* var stmt = await b.scitt.signStatement(sbomBytes, {
|
|
102
|
+
* alg: "ES256", privateKey: issuerKey,
|
|
103
|
+
* issuer: "https://builder.example", subject: "pkg:npm/widget@1.2.3",
|
|
104
|
+
* contentType: "application/spdx+json",
|
|
105
|
+
* });
|
|
106
|
+
*/
|
|
107
|
+
async function signStatement(payload, opts) {
|
|
108
|
+
validateOpts.requireObject(opts, "scitt.signStatement", ScittError);
|
|
109
|
+
validateOpts(opts,
|
|
110
|
+
["alg", "privateKey", "issuer", "subject", "contentType", "claims", "kid", "externalAad"],
|
|
111
|
+
"scitt.signStatement");
|
|
112
|
+
_requireNonEmptyString(opts.issuer, "issuer");
|
|
113
|
+
_requireNonEmptyString(opts.subject, "subject");
|
|
114
|
+
|
|
115
|
+
var cwtClaims = new Map();
|
|
116
|
+
cwtClaims.set(CLAIM_ISS, opts.issuer);
|
|
117
|
+
cwtClaims.set(CLAIM_SUB, opts.subject);
|
|
118
|
+
// Extra CWT claims (e.g. iat = 6, a registration-policy claim) keyed
|
|
119
|
+
// by their integer label. iss / sub are managed via opts.issuer /
|
|
120
|
+
// opts.subject and cannot be overridden here.
|
|
121
|
+
if (opts.claims && typeof opts.claims === "object") {
|
|
122
|
+
var ck = opts.claims instanceof Map ? Array.from(opts.claims.keys()) : Object.keys(opts.claims);
|
|
123
|
+
for (var i = 0; i < ck.length; i++) {
|
|
124
|
+
var label = Number(ck[i]);
|
|
125
|
+
if (!Number.isInteger(label)) {
|
|
126
|
+
throw new ScittError("scitt/bad-claim-label",
|
|
127
|
+
"scitt.signStatement: claims keys must be integer CWT claim labels");
|
|
128
|
+
}
|
|
129
|
+
if (label === CLAIM_ISS || label === CLAIM_SUB) {
|
|
130
|
+
throw new ScittError("scitt/reserved-claim",
|
|
131
|
+
"scitt.signStatement: set iss / sub via opts.issuer / opts.subject, not opts.claims");
|
|
132
|
+
}
|
|
133
|
+
var val = opts.claims instanceof Map ? opts.claims.get(ck[i]) : opts.claims[ck[i]];
|
|
134
|
+
cwtClaims.set(label, val);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
var protectedHeaders = {};
|
|
139
|
+
protectedHeaders[HDR_CWT_CLAIMS] = cwtClaims;
|
|
140
|
+
|
|
141
|
+
return cose.sign(payload, {
|
|
142
|
+
alg: opts.alg,
|
|
143
|
+
privateKey: opts.privateKey,
|
|
144
|
+
kid: opts.kid,
|
|
145
|
+
contentType: opts.contentType,
|
|
146
|
+
externalAad: opts.externalAad,
|
|
147
|
+
protectedHeaders: protectedHeaders,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @primitive b.scitt.verifyStatement
|
|
153
|
+
* @signature b.scitt.verifyStatement(statement, opts)
|
|
154
|
+
* @since 0.12.37
|
|
155
|
+
* @status experimental
|
|
156
|
+
* @compliance soc2, cra
|
|
157
|
+
* @related b.scitt.signStatement, b.cose.verify
|
|
158
|
+
*
|
|
159
|
+
* Verify a SCITT signed statement and return its payload + identity
|
|
160
|
+
* binding. The COSE signature is checked through
|
|
161
|
+
* <code>b.cose.verify</code> (the algorithm allowlist is mandatory); a
|
|
162
|
+
* statement that does not carry a CWT_Claims header with both
|
|
163
|
+
* <code>iss</code> and <code>sub</code> is refused — that binding is
|
|
164
|
+
* what makes it a SCITT statement rather than a bare COSE_Sign1.
|
|
165
|
+
* <code>expectedIssuer</code> / <code>expectedSubject</code>, when
|
|
166
|
+
* given, must match.
|
|
167
|
+
*
|
|
168
|
+
* @opts
|
|
169
|
+
* {
|
|
170
|
+
* algorithms: string[], // required — accepted alg names (allowlist)
|
|
171
|
+
* publicKey?: object, // verification key (KeyObject / PEM)
|
|
172
|
+
* keyResolver?: function, // (protectedHeaders, unprotectedHeaders) → key
|
|
173
|
+
* expectedIssuer?: string, // require iss === this
|
|
174
|
+
* expectedSubject?: string, // require sub === this
|
|
175
|
+
* externalAad?: Buffer, // must match what was signed
|
|
176
|
+
* maxBytes?: number, // forwarded to b.cose.verify → b.cbor.decode
|
|
177
|
+
* maxDepth?: number,
|
|
178
|
+
* }
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* var out = await b.scitt.verifyStatement(stmt, {
|
|
182
|
+
* algorithms: ["ES256"], publicKey: issuerPub,
|
|
183
|
+
* expectedSubject: "pkg:npm/widget@1.2.3",
|
|
184
|
+
* });
|
|
185
|
+
* // → { payload: <Buffer>, issuer, subject, cwtClaims: Map, alg, protectedHeaders, unprotectedHeaders }
|
|
186
|
+
*/
|
|
187
|
+
async function verifyStatement(statement, opts) {
|
|
188
|
+
validateOpts.requireObject(opts, "scitt.verifyStatement", ScittError);
|
|
189
|
+
validateOpts(opts,
|
|
190
|
+
["algorithms", "publicKey", "keyResolver", "expectedIssuer", "expectedSubject",
|
|
191
|
+
"externalAad", "maxBytes", "maxDepth"],
|
|
192
|
+
"scitt.verifyStatement");
|
|
193
|
+
|
|
194
|
+
var out = await cose.verify(statement, {
|
|
195
|
+
algorithms: opts.algorithms,
|
|
196
|
+
publicKey: opts.publicKey,
|
|
197
|
+
keyResolver: opts.keyResolver,
|
|
198
|
+
externalAad: opts.externalAad,
|
|
199
|
+
maxBytes: opts.maxBytes,
|
|
200
|
+
maxDepth: opts.maxDepth,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
var cwtClaims = out.protectedHeaders.get(HDR_CWT_CLAIMS);
|
|
204
|
+
if (!(cwtClaims instanceof Map)) {
|
|
205
|
+
throw new ScittError("scitt/missing-cwt-claims",
|
|
206
|
+
"scitt.verifyStatement: no CWT_Claims header (label 15) — not a SCITT signed statement");
|
|
207
|
+
}
|
|
208
|
+
var issuer = cwtClaims.get(CLAIM_ISS);
|
|
209
|
+
var subject = cwtClaims.get(CLAIM_SUB);
|
|
210
|
+
if (issuer === undefined || issuer === null) {
|
|
211
|
+
throw new ScittError("scitt/missing-issuer",
|
|
212
|
+
"scitt.verifyStatement: CWT_Claims has no iss (label 1)");
|
|
213
|
+
}
|
|
214
|
+
if (subject === undefined || subject === null) {
|
|
215
|
+
throw new ScittError("scitt/missing-subject",
|
|
216
|
+
"scitt.verifyStatement: CWT_Claims has no sub (label 2)");
|
|
217
|
+
}
|
|
218
|
+
if (opts.expectedIssuer !== undefined && issuer !== opts.expectedIssuer) {
|
|
219
|
+
throw new ScittError("scitt/issuer-mismatch",
|
|
220
|
+
"scitt.verifyStatement: iss does not match expectedIssuer");
|
|
221
|
+
}
|
|
222
|
+
if (opts.expectedSubject !== undefined && subject !== opts.expectedSubject) {
|
|
223
|
+
throw new ScittError("scitt/subject-mismatch",
|
|
224
|
+
"scitt.verifyStatement: sub does not match expectedSubject");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
payload: out.payload,
|
|
229
|
+
issuer: issuer,
|
|
230
|
+
subject: subject,
|
|
231
|
+
cwtClaims: cwtClaims,
|
|
232
|
+
alg: out.alg,
|
|
233
|
+
protectedHeaders: out.protectedHeaders,
|
|
234
|
+
unprotectedHeaders: out.unprotectedHeaders,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
signStatement: signStatement,
|
|
240
|
+
verifyStatement: verifyStatement,
|
|
241
|
+
CWT_CLAIMS_LABEL: HDR_CWT_CLAIMS,
|
|
242
|
+
ScittError: ScittError,
|
|
243
|
+
};
|