@blamejs/pki 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/README.md +49 -27
- package/index.js +10 -0
- package/lib/framework-error.js +13 -0
- package/lib/hpke.js +487 -0
- package/lib/oid.js +14 -0
- package/lib/sigstore.js +613 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/sigstore.js
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.sigstore
|
|
6
|
+
* @nav Supply chain
|
|
7
|
+
* @title Sigstore
|
|
8
|
+
* @intro Zero-dependency verifier for a Sigstore bundle -- the exact artifact
|
|
9
|
+
* `npm publish --provenance` produces and the npm registry serves at its
|
|
10
|
+
* attestations API. A bundle is a keyless (Fulcio) signature over a DSSE-wrapped
|
|
11
|
+
* in-toto SLSA provenance attestation, with a Rekor transparency-log inclusion
|
|
12
|
+
* proof. `verifyBundle` composes five fail-closed legs against caller-supplied
|
|
13
|
+
* trust material (the Fulcio CA roots + Rekor log keys, never trusted from the
|
|
14
|
+
* bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key;
|
|
15
|
+
* the Fulcio certificate chain, validated as of the Rekor log time (the cert is
|
|
16
|
+
* ephemeral, ~10 minutes); the Rekor inclusion proof folded to a Rekor-signed
|
|
17
|
+
* tree root; the log entry binding to this exact signature; and the in-toto
|
|
18
|
+
* subject digest the caller confirms against the published artifact. Verify-only
|
|
19
|
+
* and offline -- every input is in the bundle or a caller argument. Reuses the
|
|
20
|
+
* shipped X.509 parser, RFC 5280 path validator, RFC 9162 Merkle verifier, and
|
|
21
|
+
* native crypto engine; the net-new codecs are the DSSE PAE byte-builder and a
|
|
22
|
+
* fail-closed JSON bundle reader.
|
|
23
|
+
* @spec DSSE, Sigstore bundle v0.3, RFC 9162, SLSA provenance v1
|
|
24
|
+
* @card Verify an npm --provenance Sigstore bundle offline (DSSE + Fulcio + Rekor + SLSA).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
var nodeCrypto = require("crypto");
|
|
28
|
+
var frameworkError = require("./framework-error");
|
|
29
|
+
var constants = require("./constants");
|
|
30
|
+
var asn1 = require("./asn1-der");
|
|
31
|
+
var guard = require("./guard-all");
|
|
32
|
+
var x509 = require("./schema-x509");
|
|
33
|
+
var pathValidate = require("./path-validate");
|
|
34
|
+
var merkle = require("./merkle");
|
|
35
|
+
var oid = require("./oid");
|
|
36
|
+
|
|
37
|
+
var C = constants;
|
|
38
|
+
var SigstoreError = frameworkError.SigstoreError;
|
|
39
|
+
function _err(code, message, cause) { return new SigstoreError(code, message, cause); }
|
|
40
|
+
|
|
41
|
+
var JSON_MAX = C.LIMITS.JSON_MAX_BYTES;
|
|
42
|
+
var SP = Buffer.from(" ", "ascii");
|
|
43
|
+
var DSSEV1 = Buffer.from("DSSEv1", "ascii");
|
|
44
|
+
// The checkpoint signature-line marker is EM DASH (U+2014) + space; built at
|
|
45
|
+
// runtime so the source file stays pure ASCII.
|
|
46
|
+
var NOTE_SIG = new RegExp("^" + String.fromCharCode(0x2014) + " (\\S+) (\\S+)$");
|
|
47
|
+
|
|
48
|
+
// Parse untrusted JSON through the bounded, duplicate-member-rejecting guard.
|
|
49
|
+
function _jsonParse(input, code, label) {
|
|
50
|
+
return guard.json.parse(input, SigstoreError, {
|
|
51
|
+
maxBytes: JSON_MAX, maxDepth: 64,
|
|
52
|
+
badJson: code, tooDeep: code, duplicateMember: code, tooLarge: code, badInput: code, label: label,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- DSSE PAE (secure-systems-lab/dsse protocol.md) --------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @primitive pki.sigstore.pae
|
|
60
|
+
* @signature pki.sigstore.pae(payloadType, payloadBytes) -> Buffer
|
|
61
|
+
* @since 0.2.3
|
|
62
|
+
* @status experimental
|
|
63
|
+
* @spec DSSE
|
|
64
|
+
* @related pki.sigstore.verifyBundle
|
|
65
|
+
*
|
|
66
|
+
* The DSSE Pre-Authentication Encoding: `"DSSEv1" SP LEN(type) SP type SP
|
|
67
|
+
* LEN(body) SP body`, where `LEN` is the ASCII-decimal byte length (no leading
|
|
68
|
+
* zeros) and `type` is the UTF-8 `payloadType`. This is the exact preimage a DSSE
|
|
69
|
+
* signature covers; `LEN` is over the decoded body byte length, never the base64
|
|
70
|
+
* length -- any deviation is a signature-verify bypass.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* var b = pki.sigstore.pae("application/vnd.in-toto+json", Buffer.from("{}"));
|
|
74
|
+
* b.slice(0, 6).toString(); // "DSSEv1"
|
|
75
|
+
*/
|
|
76
|
+
function pae(payloadType, payloadBytes) {
|
|
77
|
+
if (typeof payloadType !== "string") throw new TypeError("pae: payloadType must be a string");
|
|
78
|
+
var type = Buffer.from(payloadType, "utf8");
|
|
79
|
+
var body = Buffer.isBuffer(payloadBytes) ? payloadBytes : Buffer.from(payloadBytes || []);
|
|
80
|
+
return Buffer.concat([
|
|
81
|
+
DSSEV1, SP,
|
|
82
|
+
Buffer.from(String(type.length), "ascii"), SP, type, SP,
|
|
83
|
+
Buffer.from(String(body.length), "ascii"), SP, body,
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- strict base64 decode (fail-closed, canonical) ---------------------------
|
|
88
|
+
|
|
89
|
+
// Sigstore fields are standard (padded) base64; DSSE permits URL-safe. Decode
|
|
90
|
+
// either, but reject a non-canonical re-encoding (an encoding-malleability guard).
|
|
91
|
+
function _b64(s, label) {
|
|
92
|
+
if (typeof s !== "string") throw _err("sigstore/bad-bundle", label + " must be a base64 string");
|
|
93
|
+
var urlSafe = /[-_]/.test(s);
|
|
94
|
+
var enc = urlSafe ? "base64url" : "base64";
|
|
95
|
+
var buf = Buffer.from(s, enc);
|
|
96
|
+
if (buf.toString(enc).replace(/=+$/, "") !== s.replace(/=+$/, "")) {
|
|
97
|
+
throw _err("sigstore/bad-bundle", label + " is not canonical base64");
|
|
98
|
+
}
|
|
99
|
+
return buf;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- JSON bundle reader (fail-closed) ----------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @primitive pki.sigstore.parseBundle
|
|
106
|
+
* @signature pki.sigstore.parseBundle(input) -> bundle
|
|
107
|
+
* @since 0.2.3
|
|
108
|
+
* @status experimental
|
|
109
|
+
* @spec Sigstore bundle v0.3
|
|
110
|
+
* @related pki.sigstore.verifyBundle
|
|
111
|
+
*
|
|
112
|
+
* Decode + structurally validate a Sigstore bundle (a JSON object, string, or
|
|
113
|
+
* Buffer) fail-closed: a non-object, malformed JSON, an oversize input, an
|
|
114
|
+
* unknown `mediaType`, or a missing required member throws a typed
|
|
115
|
+
* `sigstore/bad-bundle` / `sigstore/bad-bundle-version`. Returns the validated
|
|
116
|
+
* bundle object (structure only -- no cryptographic verification).
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* var b = pki.sigstore.parseBundle(bundle);
|
|
120
|
+
* b.mediaType; // "application/vnd.dev.sigstore.bundle.v0.3+json"
|
|
121
|
+
*/
|
|
122
|
+
function parseBundle(input) {
|
|
123
|
+
var obj;
|
|
124
|
+
if (Buffer.isBuffer(input) || typeof input === "string") {
|
|
125
|
+
obj = _jsonParse(input, "sigstore/bad-bundle", "bundle");
|
|
126
|
+
} else if (input && typeof input === "object") {
|
|
127
|
+
obj = input;
|
|
128
|
+
} else {
|
|
129
|
+
throw _err("sigstore/bad-bundle", "bundle must be a JSON object, string, or Buffer");
|
|
130
|
+
}
|
|
131
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) throw _err("sigstore/bad-bundle", "bundle must be a JSON object");
|
|
132
|
+
// Accept only the bundle versions this verifier actually handles (v0.1-v0.3);
|
|
133
|
+
// a newer version is recognized-and-deferred, never accepted by an over-broad
|
|
134
|
+
// match (the inline `.v0.N+json` form and the `+json;version=0.N` form).
|
|
135
|
+
var mt = obj.mediaType;
|
|
136
|
+
if (typeof mt !== "string" || !/^application\/vnd\.dev\.sigstore\.bundle(\.v0\.[123]\+json|\+json;version=0\.[123])$/.test(mt)) {
|
|
137
|
+
throw _err("sigstore/bad-bundle-version", "unsupported or unknown bundle media type: " + (mt === undefined ? "(none)" : mt));
|
|
138
|
+
}
|
|
139
|
+
if (!obj.verificationMaterial || typeof obj.verificationMaterial !== "object") {
|
|
140
|
+
throw _err("sigstore/bad-bundle", "bundle is missing verificationMaterial");
|
|
141
|
+
}
|
|
142
|
+
if (obj.messageSignature && !obj.dsseEnvelope) {
|
|
143
|
+
throw _err("sigstore/unsupported-content", "a message_signature bundle is not supported (only dsse_envelope)");
|
|
144
|
+
}
|
|
145
|
+
if (!obj.dsseEnvelope || typeof obj.dsseEnvelope !== "object") {
|
|
146
|
+
throw _err("sigstore/bad-bundle", "bundle is missing a dsseEnvelope");
|
|
147
|
+
}
|
|
148
|
+
var d = obj.dsseEnvelope;
|
|
149
|
+
if (typeof d.payload !== "string" || typeof d.payloadType !== "string" || !Array.isArray(d.signatures) || !d.signatures.length ||
|
|
150
|
+
!d.signatures[0] || typeof d.signatures[0] !== "object" || typeof d.signatures[0].sig !== "string") {
|
|
151
|
+
throw _err("sigstore/bad-dsse", "the DSSE envelope is missing a required field (payload / payloadType / signatures[].sig)");
|
|
152
|
+
}
|
|
153
|
+
return obj;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- leaf-key + signature primitives (node:crypto, sync) ---------------------
|
|
157
|
+
|
|
158
|
+
// A bundle X509Certificate element is `{ rawBytes: <base64 DER> }`; a null /
|
|
159
|
+
// non-object / non-string element fails closed, never a raw property deref.
|
|
160
|
+
function _certBytes(c, label) {
|
|
161
|
+
if (!c || typeof c !== "object" || typeof c.rawBytes !== "string") throw _err("sigstore/bad-bundle", label + " is not a { rawBytes } certificate");
|
|
162
|
+
return _b64(c.rawBytes, label);
|
|
163
|
+
}
|
|
164
|
+
function _leafCertDer(vm) {
|
|
165
|
+
if (vm.certificate) return _certBytes(vm.certificate, "verificationMaterial.certificate");
|
|
166
|
+
if (vm.x509CertificateChain && Array.isArray(vm.x509CertificateChain.certificates) && vm.x509CertificateChain.certificates.length) {
|
|
167
|
+
return _certBytes(vm.x509CertificateChain.certificates[0], "verificationMaterial.x509CertificateChain[0]");
|
|
168
|
+
}
|
|
169
|
+
throw _err("sigstore/bad-bundle", "bundle has no Fulcio certificate (public_key bundles are not supported)");
|
|
170
|
+
}
|
|
171
|
+
function _chainDers(vm) {
|
|
172
|
+
if (vm.x509CertificateChain && Array.isArray(vm.x509CertificateChain.certificates)) {
|
|
173
|
+
return vm.x509CertificateChain.certificates.map(function (c, i) { return _certBytes(c, "x509CertificateChain[" + i + "]"); });
|
|
174
|
+
}
|
|
175
|
+
return [_leafCertDer(vm)];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Verify a DER ECDSA / Ed25519 signature over `data` under a node public key.
|
|
179
|
+
function _rawVerify(keyObj, data, derSig) {
|
|
180
|
+
var t = keyObj.asymmetricKeyType;
|
|
181
|
+
if (t === "ed25519" || t === "ed448") return nodeCrypto.verify(null, data, keyObj, derSig);
|
|
182
|
+
var crv = keyObj.asymmetricKeyDetails && keyObj.asymmetricKeyDetails.namedCurve;
|
|
183
|
+
var hash = crv === "secp384r1" ? "sha384" : (crv === "secp521r1" ? "sha512" : "sha256");
|
|
184
|
+
return nodeCrypto.verify(hash, data, { key: keyObj, dsaEncoding: "der" }, derSig);
|
|
185
|
+
}
|
|
186
|
+
function _pubFromSpki(spkiDer, label) {
|
|
187
|
+
try { return nodeCrypto.createPublicKey({ key: spkiDer, format: "der", type: "spki" }); }
|
|
188
|
+
catch (e) { throw _err("sigstore/bad-key", "invalid " + label + " public key", e); }
|
|
189
|
+
}
|
|
190
|
+
// Parse a certificate, re-typing a malformed-DER fault to a sigstore/* error so
|
|
191
|
+
// the caller never sees a raw certificate/* / asn1/* leak from this boundary.
|
|
192
|
+
function _parseCert(der, label) {
|
|
193
|
+
try { return x509.parse(der); }
|
|
194
|
+
catch (e) { throw _err("sigstore/bad-certificate", "invalid " + label, e); }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- Rekor leg (RFC 9162 inclusion + Rekor-signed root + entry binding) -------
|
|
198
|
+
|
|
199
|
+
function _sha256(buf) { return nodeCrypto.createHash("sha256").update(buf).digest(); }
|
|
200
|
+
|
|
201
|
+
// Select the caller Rekor key whose logId.keyId matches the entry's logId (full
|
|
202
|
+
// 32-byte id) or a 4-byte checkpoint keyhint. Never trust a key from the bundle.
|
|
203
|
+
// A key carrying a validFor window is used only if `timeMs` (the entry's log
|
|
204
|
+
// time) falls inside it -- a rotated-out key must not verify a later entry.
|
|
205
|
+
// A validFor bound is an epoch-ms number, a Date, or an ISO-8601 string (the form
|
|
206
|
+
// a Sigstore trusted_root carries). Returns null for absent/unparseable.
|
|
207
|
+
function _toMs(x) {
|
|
208
|
+
if (x == null) return null;
|
|
209
|
+
if (typeof x === "number") return Number.isFinite(x) ? x : null;
|
|
210
|
+
if (x instanceof Date) { var d = x.getTime(); return isNaN(d) ? null : d; }
|
|
211
|
+
if (typeof x === "string") { var t = Date.parse(x); return isNaN(t) ? null : t; }
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
function _inWindow(timeMs, vf) {
|
|
215
|
+
if (!vf) return true;
|
|
216
|
+
// A present-but-unparseable bound fails closed (the key/CA is not used) rather
|
|
217
|
+
// than silently disabling the window it configures.
|
|
218
|
+
if (vf.start != null) { var s = _toMs(vf.start); if (s === null || !(timeMs >= s)) return false; }
|
|
219
|
+
if (vf.end != null) { var e = _toMs(vf.end); if (e === null || !(timeMs <= e)) return false; }
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
function _rekorKey(rekorKeys, keyIdBuf, hint, timeMs) {
|
|
223
|
+
for (var i = 0; i < rekorKeys.length; i++) {
|
|
224
|
+
var k = rekorKeys[i];
|
|
225
|
+
var match = (keyIdBuf && k.keyId && k.keyId.equals(keyIdBuf)) ||
|
|
226
|
+
(hint && k.keyId && k.keyId.length >= 4 && k.keyId.subarray(0, 4).equals(hint));
|
|
227
|
+
if (match && _inWindow(timeMs, k.validFor)) return k;
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Verify the checkpoint (C2SP signed-note): ECDSA-P256-SHA256 over the note body
|
|
233
|
+
// (bytes up to and including the newline before the blank-line separator).
|
|
234
|
+
function _verifyCheckpoint(envelope, ip, rekorKeys, timeMs) {
|
|
235
|
+
var buf = Buffer.from(envelope, "utf8");
|
|
236
|
+
var sep = buf.indexOf("\n\n");
|
|
237
|
+
if (sep < 0) throw _err("sigstore/bad-checkpoint", "the checkpoint has no note/signature separator");
|
|
238
|
+
var body = buf.subarray(0, sep + 1);
|
|
239
|
+
var lines = body.toString("utf8").split("\n");
|
|
240
|
+
// The note body's third line is the base64 tree root; it MUST equal the proof root.
|
|
241
|
+
if (_b64(lines[2], "checkpoint root").toString("hex") !== _b64(ip.rootHash, "inclusionProof.rootHash").toString("hex")) {
|
|
242
|
+
throw _err("sigstore/inclusion-proof-mismatch", "the checkpoint root does not match the inclusion-proof root");
|
|
243
|
+
}
|
|
244
|
+
var sigBlock = buf.subarray(sep + 2).toString("utf8").split("\n");
|
|
245
|
+
for (var i = 0; i < sigBlock.length; i++) {
|
|
246
|
+
var m = sigBlock[i].match(NOTE_SIG);
|
|
247
|
+
if (!m) continue;
|
|
248
|
+
var blob = _b64(m[2], "checkpoint signature");
|
|
249
|
+
if (blob.length < 5) continue;
|
|
250
|
+
var hint = blob.subarray(0, 4), derSig = blob.subarray(4);
|
|
251
|
+
var k = _rekorKey(rekorKeys, null, hint, timeMs);
|
|
252
|
+
if (!k) continue;
|
|
253
|
+
if (_rawVerify(_pubFromSpki(k.spki, "Rekor log"), body, derSig)) return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Verify the SET (inclusionPromise.signedEntryTimestamp): ECDSA over the RFC 8785
|
|
259
|
+
// canonical JSON { body, integratedTime, logID, logIndex } (keys sorted).
|
|
260
|
+
function _verifySet(te, rekorKeys, timeMs) {
|
|
261
|
+
var promise = te.inclusionPromise;
|
|
262
|
+
if (!promise || typeof promise.signedEntryTimestamp !== "string") return false;
|
|
263
|
+
var keyId = te.logId && te.logId.keyId ? _b64(te.logId.keyId, "logId.keyId") : null;
|
|
264
|
+
var k = _rekorKey(rekorKeys, keyId, null, timeMs);
|
|
265
|
+
if (!k) return false;
|
|
266
|
+
var canonical = JSON.stringify({
|
|
267
|
+
body: te.canonicalizedBody,
|
|
268
|
+
integratedTime: Number(te.integratedTime),
|
|
269
|
+
logID: keyId.toString("hex"),
|
|
270
|
+
logIndex: Number(te.logIndex),
|
|
271
|
+
});
|
|
272
|
+
var sig = _b64(promise.signedEntryTimestamp, "signedEntryTimestamp");
|
|
273
|
+
return _rawVerify(_pubFromSpki(k.spki, "Rekor log"), Buffer.from(canonical, "utf8"), sig);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Bind the tlog entry to THIS bundle's signature: the dsse-kind body's embedded
|
|
277
|
+
// signature + payload hash must be the envelope's (RFC 9162 proves the body is
|
|
278
|
+
// logged; this proves the body is OUR envelope, not some other logged entry).
|
|
279
|
+
function _bindEntry(te, envelope, leafDer) {
|
|
280
|
+
var body = _jsonParse(_b64(te.canonicalizedBody, "canonicalizedBody"), "sigstore/bad-tlog-entry", "Rekor entry body");
|
|
281
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw _err("sigstore/bad-tlog-entry", "the Rekor entry body is not a JSON object");
|
|
282
|
+
var spec = body.spec;
|
|
283
|
+
if (body.kind !== "dsse" || !spec || typeof spec !== "object" || !Array.isArray(spec.signatures) || !spec.signatures.length ||
|
|
284
|
+
!spec.signatures[0] || typeof spec.signatures[0] !== "object" || typeof spec.signatures[0].signature !== "string") {
|
|
285
|
+
throw _err("sigstore/unsupported-content", "unsupported or malformed Rekor entry: " + body.kind);
|
|
286
|
+
}
|
|
287
|
+
var envSig = _b64(envelope.signatures[0].sig, "dsseEnvelope.signatures[0].sig");
|
|
288
|
+
var bodySig = _b64(spec.signatures[0].signature, "Rekor entry signature");
|
|
289
|
+
if (!envSig.equals(bodySig)) throw _err("sigstore/entry-mismatch", "the Rekor entry signature does not match the bundle signature");
|
|
290
|
+
var payload = _b64(envelope.payload, "dsseEnvelope.payload");
|
|
291
|
+
// payloadHash is a required binding field: bind it to the envelope payload
|
|
292
|
+
// rather than skipping the check when it is absent.
|
|
293
|
+
if (!spec.payloadHash || typeof spec.payloadHash.value !== "string") throw _err("sigstore/bad-tlog-entry", "the Rekor dsse entry is missing its payloadHash");
|
|
294
|
+
if (spec.payloadHash.value !== _sha256(payload).toString("hex")) {
|
|
295
|
+
throw _err("sigstore/entry-mismatch", "the Rekor entry payloadHash does not match the bundle payload");
|
|
296
|
+
}
|
|
297
|
+
// The dsse entry's verifier certificate is REQUIRED and MUST be the bundle's
|
|
298
|
+
// leaf cert -- otherwise a valid inclusion proof for an entry signed by a
|
|
299
|
+
// DIFFERENT Fulcio cert could be paired with this envelope (the signature match
|
|
300
|
+
// alone leaves cert substitution).
|
|
301
|
+
var vf = spec.signatures[0].verifier;
|
|
302
|
+
if (typeof vf !== "string") throw _err("sigstore/bad-tlog-entry", "the Rekor dsse entry is missing its verifier certificate");
|
|
303
|
+
var vDer;
|
|
304
|
+
try { vDer = x509.pemDecode(_b64(vf, "Rekor entry verifier").toString("utf8"), "CERTIFICATE"); }
|
|
305
|
+
catch (e) { throw _err("sigstore/bad-tlog-entry", "the Rekor entry verifier is not a valid certificate", e); }
|
|
306
|
+
if (!vDer.equals(leafDer)) throw _err("sigstore/entry-mismatch", "the Rekor entry verifier certificate does not match the bundle leaf certificate");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function _verifyRekor(te, envelope, rekorKeys, leafDer) {
|
|
310
|
+
var ip = te.inclusionProof;
|
|
311
|
+
if (!ip || typeof ip.rootHash !== "string" || !Array.isArray(ip.hashes)) {
|
|
312
|
+
throw _err("sigstore/bad-inclusion-proof", "the transparency-log entry has no inclusion proof");
|
|
313
|
+
}
|
|
314
|
+
// The entry's CLAIMED log time selects which caller Rekor key is in its validity
|
|
315
|
+
// window; the SET (required below) re-confirms it by signing it.
|
|
316
|
+
var claimedSec = Number(te.integratedTime);
|
|
317
|
+
var claimedMs = Number.isFinite(claimedSec) ? C.TIME.seconds(claimedSec) : NaN;
|
|
318
|
+
// Bind the entry to this signature FIRST (a proof for another entry is not evidence).
|
|
319
|
+
_bindEntry(te, envelope, leafDer);
|
|
320
|
+
// Fold the audit path to the tree root (RFC 9162), reusing pki.merkle.
|
|
321
|
+
var included;
|
|
322
|
+
try {
|
|
323
|
+
included = merkle.verifyInclusion({
|
|
324
|
+
leafIndex: BigInt(ip.logIndex),
|
|
325
|
+
treeSize: BigInt(ip.treeSize),
|
|
326
|
+
leafHash: merkle.leafHash(_b64(te.canonicalizedBody, "canonicalizedBody")),
|
|
327
|
+
proof: ip.hashes.map(function (h, i) { return _b64(h, "inclusionProof.hashes[" + i + "]"); }),
|
|
328
|
+
rootHash: _b64(ip.rootHash, "inclusionProof.rootHash"),
|
|
329
|
+
});
|
|
330
|
+
} catch (e) {
|
|
331
|
+
throw _err("sigstore/bad-inclusion-proof", "the inclusion proof is malformed: " + e.message, e);
|
|
332
|
+
}
|
|
333
|
+
if (!included) throw _err("sigstore/inclusion-proof-mismatch", "the inclusion proof does not reconstruct the tree root");
|
|
334
|
+
// The inclusion proof above only reconstructs the attacker-supplied
|
|
335
|
+
// inclusionProof.rootHash; that root is trust ONLY once the Rekor-signed
|
|
336
|
+
// checkpoint (which signs the tree root) verifies over it -- so the checkpoint
|
|
337
|
+
// is REQUIRED, or the inclusion check is theater against an unsigned root.
|
|
338
|
+
var checkpointOk = ip.checkpoint && typeof ip.checkpoint.envelope === "string" && _verifyCheckpoint(ip.checkpoint.envelope, ip, rekorKeys, claimedMs);
|
|
339
|
+
if (!checkpointOk) throw _err("sigstore/unsigned-root", "the Rekor checkpoint (the signed tree root) did not verify under the caller Rekor key -- the inclusion-proof root is not attested");
|
|
340
|
+
// The integratedTime dates the ephemeral (~10-min) Fulcio cert, so it too must
|
|
341
|
+
// be Rekor-attested: ONLY the SET signs it (the checkpoint signs the root, not
|
|
342
|
+
// the time). Without a verified SET the time is attacker-controlled and could
|
|
343
|
+
// date an expired cert into validity -- require it (the RFC 3161 timestamp
|
|
344
|
+
// source is the deferred alternative).
|
|
345
|
+
var setOk = _verifySet(te, rekorKeys, claimedMs);
|
|
346
|
+
if (!setOk) throw _err("sigstore/unattested-time", "the Rekor SET (the signed source of integratedTime) did not verify -- the log time is not attested and cannot date the Fulcio certificate");
|
|
347
|
+
var t = Number(te.integratedTime);
|
|
348
|
+
if (!Number.isFinite(t) || t < 0) throw _err("sigstore/bad-tlog-entry", "the transparency-log entry has a malformed integratedTime");
|
|
349
|
+
return t;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ---- Fulcio chain leg (RFC 5280 path validation, as-of the log time) ----------
|
|
353
|
+
|
|
354
|
+
function _dn(cert, which) { return cert[which] && cert[which].dn; }
|
|
355
|
+
|
|
356
|
+
// Normalize a caller Fulcio root: a raw DER Buffer, or { der|rawBytes, validFor }
|
|
357
|
+
// (the shape carried by a Sigstore trusted_root certificateAuthorities entry).
|
|
358
|
+
function _normRoots(fulcioRoots) {
|
|
359
|
+
return fulcioRoots.map(function (r, i) {
|
|
360
|
+
var der = Buffer.isBuffer(r) ? r : (r && (r.der || r.rawBytes));
|
|
361
|
+
if (!Buffer.isBuffer(der)) throw _err("sigstore/bad-input", "fulcioRoots[" + i + "] must be a DER Buffer or { der, validFor }");
|
|
362
|
+
return { cert: _parseCert(der, "Fulcio CA root [" + i + "]"), validFor: (r && r.validFor) || null };
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Build the ordered path body [intermediates..., leaf] from the leaf up to (not
|
|
367
|
+
// including) a cert issued by `anchor`, drawing intermediates from `intBySubject`
|
|
368
|
+
// (caller certs AND bundle links -- all get cryptographically re-checked by
|
|
369
|
+
// path.validate). `anchor` itself is never placed in the body. Returns null if no
|
|
370
|
+
// such path exists. The terminal trust anchor is always a caller cert; a bundle
|
|
371
|
+
// cert can appear only here, as a non-terminal path step.
|
|
372
|
+
function _pathToAnchor(leaf, anchor, intBySubject) {
|
|
373
|
+
var anchorSubj = _dn(anchor, "subject");
|
|
374
|
+
var path = [leaf], cur = leaf, seen = {}, guardN = 0;
|
|
375
|
+
while (guardN++ < 16) {
|
|
376
|
+
var issuerDn = _dn(cur, "issuer");
|
|
377
|
+
if (issuerDn === anchorSubj) return path;
|
|
378
|
+
var cands = intBySubject[issuerDn] || [];
|
|
379
|
+
var next = null;
|
|
380
|
+
for (var j = 0; j < cands.length; j++) {
|
|
381
|
+
if (cands[j] === anchor || seen[_dn(cands[j], "subject")]) continue;
|
|
382
|
+
next = cands[j]; break;
|
|
383
|
+
}
|
|
384
|
+
if (!next) return null;
|
|
385
|
+
seen[_dn(next, "subject")] = 1;
|
|
386
|
+
path.unshift(next);
|
|
387
|
+
cur = next;
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
|
|
393
|
+
var roots = _normRoots(fulcioRoots);
|
|
394
|
+
var bundleLinks = chainDers.slice(1).map(function (d, i) { return _parseCert(d, "bundle intermediate [" + i + "]"); });
|
|
395
|
+
// Path-body intermediates: caller certs (trusted) + bundle intermediates (path
|
|
396
|
+
// steps only). The terminal anchor is chosen ONLY from the caller roots below.
|
|
397
|
+
var intBySubject = {};
|
|
398
|
+
function addInt(c) { (intBySubject[_dn(c, "subject")] = intBySubject[_dn(c, "subject")] || []).push(c); }
|
|
399
|
+
roots.forEach(function (r) { addInt(r.cert); });
|
|
400
|
+
bundleLinks.forEach(addInt);
|
|
401
|
+
// Try each caller anchor within its validity window (the trusted_root carries
|
|
402
|
+
// several CA rotations, some sharing a subject DN); accept the first that both
|
|
403
|
+
// yields a path and cryptographically validates as of the log time.
|
|
404
|
+
var candidates = roots.filter(function (r) { return _inWindow(timeMs, r.validFor); });
|
|
405
|
+
var lastErr = _err("sigstore/chain-incomplete", "no caller-supplied Fulcio anchor (within its validity window) issues the chain");
|
|
406
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
407
|
+
var anchor = candidates[i].cert;
|
|
408
|
+
var path = _pathToAnchor(leaf, anchor, intBySubject);
|
|
409
|
+
if (!path) continue;
|
|
410
|
+
var spki = anchor.subjectPublicKeyInfo;
|
|
411
|
+
try {
|
|
412
|
+
var res = await pathValidate.validate(path, {
|
|
413
|
+
time: new Date(timeMs),
|
|
414
|
+
historicalMode: true,
|
|
415
|
+
trustAnchor: { name: anchor.subject, publicKey: spki.bytes, algorithm: spki.algorithm.oid, parameters: spki.algorithm.parameters },
|
|
416
|
+
requiredEku: ["codeSigning"],
|
|
417
|
+
});
|
|
418
|
+
if (res.valid) return;
|
|
419
|
+
lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain is not valid as of the log time");
|
|
420
|
+
} catch (e) {
|
|
421
|
+
lastErr = _err("sigstore/chain-invalid", "the Fulcio certificate chain failed validation: " + e.message, e);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
throw lastErr;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ---- Fulcio identity leg -----------------------------------------------------
|
|
428
|
+
|
|
429
|
+
// Decode the identity the Fulcio cert commits to: the SAN plus the Fulcio
|
|
430
|
+
// extension arc (Issuer .1.8, SourceRepositoryURI .1.12, etc). `.1.8`+ values are
|
|
431
|
+
// DER UTF8String; the legacy `.1.1`-`.1.6` are raw strings (never DER).
|
|
432
|
+
// Derive the OIDs from the registry (never a dotted-decimal literal in source):
|
|
433
|
+
// the SAN, and the Fulcio arc base (the parent of the `.7` otherName member).
|
|
434
|
+
var SAN_OID = oid.byName("subjectAltName");
|
|
435
|
+
var FULCIO_PREFIX = oid.byName("otherName").split(".").slice(0, -1).join(".") + ".";
|
|
436
|
+
function _identity(leaf) {
|
|
437
|
+
var out = { san: null, extensions: {} };
|
|
438
|
+
// The identity is security-relevant: an undecodable SAN / Fulcio member is a
|
|
439
|
+
// malformed certificate and fails closed with a named reason (never a silent
|
|
440
|
+
// drop that would hide an identity claim a caller policy relies on).
|
|
441
|
+
try {
|
|
442
|
+
(leaf.extensions || []).forEach(function (ext) {
|
|
443
|
+
if (ext.oid === SAN_OID) { out.san = _sanValue(ext); return; }
|
|
444
|
+
if (ext.oid.indexOf(FULCIO_PREFIX) === 0) {
|
|
445
|
+
out.extensions[ext.name || ext.oid] = _fulcioExtValue(ext, Number(ext.oid.slice(FULCIO_PREFIX.length)));
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
} catch (e) {
|
|
449
|
+
if (e instanceof SigstoreError) throw e;
|
|
450
|
+
throw _err("sigstore/bad-certificate", "the Fulcio certificate identity could not be decoded", e);
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
// The SAN GeneralNames the OIDC identity is carried in: a context-tagged
|
|
455
|
+
// [1] rfc822Name / [2] dNSName / [6] uniformResourceIdentifier. Return the URI
|
|
456
|
+
// (a machine identity) preferentially, else the first name, with its type.
|
|
457
|
+
var GN_TYPE = { 1: "rfc822Name", 2: "dNSName", 6: "uri" };
|
|
458
|
+
function _sanValue(ext) {
|
|
459
|
+
var seq = asn1.decode(ext.value);
|
|
460
|
+
var names = [];
|
|
461
|
+
for (var i = 0; i < (seq.children || []).length; i++) {
|
|
462
|
+
var n = seq.children[i];
|
|
463
|
+
if (n.tagClass !== "context") continue;
|
|
464
|
+
var val = null;
|
|
465
|
+
if (!n.constructed && GN_TYPE[n.tagNumber]) {
|
|
466
|
+
// A primitive IMPLICIT IA5String name: rfc822Name / dNSName / URI.
|
|
467
|
+
val = { type: GN_TYPE[n.tagNumber], value: asn1.read.octetStringImplicit(n, n.tagNumber).toString("utf8") };
|
|
468
|
+
} else if (n.constructed && n.tagNumber === 0 && (n.children || []).length >= 2) {
|
|
469
|
+
// otherName ::= SEQUENCE { type-id OID, [0] EXPLICIT value } -- Fulcio carries
|
|
470
|
+
// machine identities here (type-id .1.7, value a UTF8String).
|
|
471
|
+
var inner = (n.children[1].children || [])[0] || n.children[1];
|
|
472
|
+
var v;
|
|
473
|
+
try { v = asn1.read.string(inner); } catch (_e) { v = Buffer.from(inner.content || []).toString("utf8"); }
|
|
474
|
+
val = { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
|
|
475
|
+
}
|
|
476
|
+
if (val) names.push(val);
|
|
477
|
+
}
|
|
478
|
+
// A Fulcio certificate binds exactly one identity; more than one SAN lets a
|
|
479
|
+
// mis-issued cert pair the expected identity with a smuggled extra one, so a
|
|
480
|
+
// caller policy that matches the wrong entry cannot be relied on -- reject.
|
|
481
|
+
if (names.length > 1) throw _err("sigstore/bad-certificate", "the Fulcio certificate carries multiple SAN identities");
|
|
482
|
+
return names[0] || null;
|
|
483
|
+
}
|
|
484
|
+
function _fulcioExtValue(ext, leafArc) {
|
|
485
|
+
if (!Buffer.isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
|
|
486
|
+
// The raw-vs-DER split by member (Fulcio oid-info): .1-.6 legacy are raw UTF-8
|
|
487
|
+
// strings; .8+ are DER UTF8String -- the arc is open-ended past .22. An
|
|
488
|
+
// undecodable member throws (caught by _identity) rather than dropping silently.
|
|
489
|
+
if (leafArc >= 1 && leafArc <= 6) return ext.value.toString("utf8");
|
|
490
|
+
return asn1.read.string(asn1.decode(ext.value));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function _checkIdentity(id, policy) {
|
|
494
|
+
if (!policy) return;
|
|
495
|
+
var sanValue = id.san && id.san.value;
|
|
496
|
+
if (policy.san && sanValue !== policy.san) throw _err("sigstore/identity-mismatch", "the certificate SAN " + JSON.stringify(sanValue) + " does not match the expected identity");
|
|
497
|
+
// The OIDC issuer is carried by the current Issuer V2 (.1.8) or, on older certs,
|
|
498
|
+
// only by the deprecated raw-string issuer (.1.1); match against either.
|
|
499
|
+
if (policy.issuer && policy.issuer !== id.extensions.issuer && policy.issuer !== id.extensions.issuerLegacy) throw _err("sigstore/identity-mismatch", "the certificate OIDC issuer does not match the expected issuer");
|
|
500
|
+
if (policy.sourceRepositoryURI && id.extensions.sourceRepositoryURI !== policy.sourceRepositoryURI) throw _err("sigstore/identity-mismatch", "the certificate source-repository URI does not match");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ---- in-toto Statement leg ---------------------------------------------------
|
|
504
|
+
|
|
505
|
+
function _statement(payload, payloadType, expectedPredicate) {
|
|
506
|
+
if (payloadType !== "application/vnd.in-toto+json") {
|
|
507
|
+
throw _err("sigstore/bad-statement", "unsupported DSSE payloadType: " + payloadType);
|
|
508
|
+
}
|
|
509
|
+
var st = _jsonParse(payload, "sigstore/bad-statement", "in-toto statement");
|
|
510
|
+
if (!st || typeof st !== "object" || st._type !== "https://in-toto.io/Statement/v1") throw _err("sigstore/bad-statement", "the payload is not an in-toto Statement v1");
|
|
511
|
+
if (!Array.isArray(st.subject) || !st.subject.length) throw _err("sigstore/bad-statement", "the in-toto statement has no subject");
|
|
512
|
+
// The verdict carries predicateType for the caller's own gate; when the caller
|
|
513
|
+
// pins one (opts.predicateType), a mismatch fails closed so a non-SLSA
|
|
514
|
+
// attestation (e.g. an SBOM) is not mistaken for the expected provenance.
|
|
515
|
+
if (expectedPredicate && st.predicateType !== expectedPredicate) {
|
|
516
|
+
throw _err("sigstore/predicate-mismatch", "the statement predicateType " + JSON.stringify(st.predicateType) + " does not match the expected " + JSON.stringify(expectedPredicate));
|
|
517
|
+
}
|
|
518
|
+
return st;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ---- orchestrator ------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* @primitive pki.sigstore.verifyBundle
|
|
525
|
+
* @signature pki.sigstore.verifyBundle(bundle, opts) -> Promise<result>
|
|
526
|
+
* @since 0.2.3
|
|
527
|
+
* @status experimental
|
|
528
|
+
* @spec DSSE, Sigstore bundle v0.3, RFC 9162, SLSA provenance v1
|
|
529
|
+
* @related pki.sigstore.parseBundle, pki.sigstore.pae
|
|
530
|
+
*
|
|
531
|
+
* Verify a Sigstore bundle (an npm `--provenance` artifact) offline against
|
|
532
|
+
* caller-supplied trust material, composing five fail-closed legs: the DSSE
|
|
533
|
+
* signature over its PAE under the Fulcio leaf key; the Fulcio chain validated as
|
|
534
|
+
* of the Rekor log time; the Rekor inclusion proof folded to a Rekor-signed root;
|
|
535
|
+
* the log entry bound to this exact signature; and the in-toto SLSA statement.
|
|
536
|
+
* Any leg failing throws a typed `sigstore/*` error. On success returns
|
|
537
|
+
* `{ verified: true, payload, statement, subjects, predicateType, predicate,
|
|
538
|
+
* identity, integratedTime }` -- `payload` is the RAW verified envelope bytes
|
|
539
|
+
* (never a re-serialization), and the caller confirms a `subjects[].digest`
|
|
540
|
+
* matches the published artifact.
|
|
541
|
+
*
|
|
542
|
+
* @opts
|
|
543
|
+
* fulcioRoots: Array, // the Fulcio CA anchors: a DER Buffer or { der, validFor } each
|
|
544
|
+
* rekorKeys: Array, // [{ keyId, spki, validFor? }] the Rekor log public keys
|
|
545
|
+
* identity: object, // optional policy: { san, issuer, sourceRepositoryURI }
|
|
546
|
+
* predicateType: string, // optional: require this in-toto predicateType (e.g. the SLSA URI)
|
|
547
|
+
* time: Date, // optional check-date override (default: the Rekor integratedTime)
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* var out = await pki.sigstore.verifyBundle(bundle, sigstoreTrust);
|
|
551
|
+
* out.verified; // true
|
|
552
|
+
* out.subjects[0].digest; // { sha512: "..." } -- confirm against your tarball
|
|
553
|
+
*/
|
|
554
|
+
async function verifyBundle(bundle, opts) {
|
|
555
|
+
if (bundle === null || typeof bundle !== "object" && typeof bundle !== "string" && !Buffer.isBuffer(bundle)) {
|
|
556
|
+
throw new TypeError("verifyBundle: bundle must be an object, JSON string, or Buffer");
|
|
557
|
+
}
|
|
558
|
+
opts = opts || {};
|
|
559
|
+
var b = parseBundle(bundle);
|
|
560
|
+
var vm = b.verificationMaterial;
|
|
561
|
+
var env = b.dsseEnvelope;
|
|
562
|
+
var rekorKeys = opts.rekorKeys || [];
|
|
563
|
+
var fulcioRoots = opts.fulcioRoots || [];
|
|
564
|
+
|
|
565
|
+
// Leg 1 -- DSSE signature over PAE under the Fulcio leaf public key.
|
|
566
|
+
var leafDer = _leafCertDer(vm);
|
|
567
|
+
var leaf = _parseCert(leafDer, "Fulcio leaf certificate");
|
|
568
|
+
var payload = _b64(env.payload, "dsseEnvelope.payload");
|
|
569
|
+
var preimage = pae(env.payloadType, payload);
|
|
570
|
+
var derSig = _b64(env.signatures[0].sig, "dsseEnvelope.signatures[0].sig");
|
|
571
|
+
var leafKey = _pubFromSpki(leaf.subjectPublicKeyInfo.bytes, "Fulcio leaf");
|
|
572
|
+
if (!_rawVerify(leafKey, preimage, derSig)) throw _err("sigstore/dsse-verify-failed", "the DSSE signature does not verify under the Fulcio leaf key");
|
|
573
|
+
|
|
574
|
+
// Leg 2 -- Rekor inclusion + Rekor-signed root + entry binding (yields the
|
|
575
|
+
// trusted time). Try every transparency-log entry; accept the first that fully
|
|
576
|
+
// verifies (and binds this signature), rejecting only if none do.
|
|
577
|
+
var tlogs = Array.isArray(vm.tlogEntries) ? vm.tlogEntries : [];
|
|
578
|
+
if (!tlogs.length) throw _err("sigstore/bad-bundle", "a keyless bundle requires at least one transparency-log entry");
|
|
579
|
+
var integratedTime = null, lastErr = null;
|
|
580
|
+
for (var ti = 0; ti < tlogs.length; ti++) {
|
|
581
|
+
if (!tlogs[ti] || typeof tlogs[ti] !== "object") { lastErr = _err("sigstore/bad-bundle", "a transparency-log entry is not an object"); continue; }
|
|
582
|
+
try { integratedTime = _verifyRekor(tlogs[ti], env, rekorKeys, leafDer); break; }
|
|
583
|
+
catch (e) { lastErr = e; }
|
|
584
|
+
}
|
|
585
|
+
if (integratedTime === null) throw lastErr;
|
|
586
|
+
|
|
587
|
+
// Leg 3 -- Fulcio chain, validated as of the (trusted) log time; then identity.
|
|
588
|
+
var checkTime = (opts.time instanceof Date) ? opts.time.getTime() : C.TIME.seconds(integratedTime);
|
|
589
|
+
await _verifyChain(leaf, _chainDers(vm), fulcioRoots, checkTime);
|
|
590
|
+
var identity = _identity(leaf);
|
|
591
|
+
_checkIdentity(identity, opts.identity);
|
|
592
|
+
|
|
593
|
+
// Leg 4 -- the in-toto SLSA statement + subject binding (with an optional
|
|
594
|
+
// caller-pinned predicateType).
|
|
595
|
+
var st = _statement(payload, env.payloadType, opts.predicateType);
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
verified: true,
|
|
599
|
+
payload: payload,
|
|
600
|
+
statement: st,
|
|
601
|
+
subjects: st.subject,
|
|
602
|
+
predicateType: st.predicateType,
|
|
603
|
+
predicate: st.predicate,
|
|
604
|
+
identity: identity,
|
|
605
|
+
integratedTime: integratedTime,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
module.exports = {
|
|
610
|
+
pae: pae,
|
|
611
|
+
parseBundle: parseBundle,
|
|
612
|
+
verifyBundle: verifyBundle,
|
|
613
|
+
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:ac694fe2-c3bb-413d-a4dc-e5ebffa0a8e3",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-12T08:08:32.351Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.3",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.3",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.3",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.3",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|