@blamejs/pki 0.4.5 → 0.4.6
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 +16 -0
- package/lib/jose.js +4 -3
- package/lib/schema-c509.js +93 -5
- package/lib/sign-scheme.js +3 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.4.6 — 2026-08-08
|
|
8
|
+
|
|
9
|
+
A C509 certificate now has one encoding where the specification defines one -- nine alternative spellings that rebuilt a byte-identical X.509 certificate, so that a single signature covered all of them, are refused, and the encoder emits the spelling it accepts.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- An attribute value now carries the one spelling the specification assigns it. A text value of even length drawn only from the characters 0-9 and a-f is a byte string; a value in EUI-64 form is a tagged MAC address, 48-bit when it matches the FF-FE marker pattern and 64-bit otherwise; anything else is text. Each alternative spelling rebuilt the identical certificate, so the one signature over it covered them all. An empty value spelled as an empty byte string is refused for the same reason -- it renders as the empty text, which already has a spelling.
|
|
14
|
+
- A name holding a single common name is the bare value, not an array of one pair; an extensions field holding only a key usage is the single integer, not an array of two; and an alternative name holding exactly one DNS name is the bare text, not an array. Each of these compact forms is the encoding the specification defines for that case, and the long form of the same value is now refused rather than accepted alongside it. The long form remains the encoding for every case that is not the single one -- a name with two attributes, a key usage beside another extension, an alternative name with two entries.
|
|
15
|
+
- A certificate whose issuer is identical to its subject encodes that issuer as the CBOR simple value null, which the specification requires and which this toolkit previously wrote out in full. Both directions changed: the encoder emits the null, and a certificate that spells the issuer out instead is refused. The comparison is made on the certificate's own bytes rather than on a normalized name, because the reconstruction rebuilds a null issuer from the subject -- two names that merely compare equal would rebuild different bytes and break the signature over them.
|
|
16
|
+
- Algorithm parameters must be a complete element. An empty byte string is none, and it rebuilt the same algorithm identifier as the form that omits parameters entirely, giving one algorithm two encodings.
|
|
17
|
+
- The encoder walks the same rules it enforces on the way in, so a certificate it emits is one it reads back. Previously it wrote an even-length-hex attribute value as text -- a spelling its own parser now refuses -- and wrote a self-signed certificate's issuer out in full.
|
|
18
|
+
|
|
19
|
+
### Known limitations
|
|
20
|
+
|
|
21
|
+
- One redundancy remains because the specification permits it: a registered algorithm may be encoded either as its registry integer or as its object identifier, and both are accepted, so a certificate using one is byte-different from the same certificate using the other. Identify a certificate by the X.509 bytes it reconstructs rather than by its C509 bytes.
|
|
22
|
+
|
|
7
23
|
## v0.4.5 — 2026-08-08
|
|
8
24
|
|
|
9
25
|
A private key created outside this toolkit's own WebCrypto now signs and exports across the toolkit -- a key from the platform's WebCrypto, or from a separately-installed copy of this toolkit, previously reached the crypto library as a key it could not read and failed with a type error instead of a reason.
|
package/lib/jose.js
CHANGED
|
@@ -437,9 +437,10 @@ async function sign(opts) {
|
|
|
437
437
|
var payloadB64 = payload.length === 0 ? "" : b64uEncode(payload);
|
|
438
438
|
var signingInput = Buffer.from(protectedB64 + "." + payloadB64, "ascii");
|
|
439
439
|
var jwk = header.jwk || opts.jwk || {};
|
|
440
|
-
// opts.key is documented as a CryptoKey, so
|
|
441
|
-
//
|
|
442
|
-
//
|
|
440
|
+
// opts.key is documented as a CryptoKey, so one created outside this engine signs here too: it
|
|
441
|
+
// carries none of the material this engine signs with, and is re-imported through it rather than
|
|
442
|
+
// reaching subtle.sign as a key it cannot read. A key whose material this process cannot reach at
|
|
443
|
+
// all -- non-extractable, or held behind another implementation's interface -- is refused there.
|
|
443
444
|
var signKey = await wcEngine.adoptKey(opts.key, null, ["sign"], E, "jose/bad-input");
|
|
444
445
|
var sigBuf = Buffer.from(await webcrypto.subtle.sign(_cryptoAlg(checked.algRow, jwk, signKey), signKey, signingInput));
|
|
445
446
|
// Pin the produced signature length the same way verify does. A key whose curve /
|
package/lib/schema-c509.js
CHANGED
|
@@ -225,6 +225,11 @@ function _algorithm(node, byInt, code, label) {
|
|
|
225
225
|
// The [~oid, params] form carries the DER parameters as a CBOR byte string; a non-byte-string here
|
|
226
226
|
// is malformed and cannot be reconstructed (b.raw would append garbage) -- fail closed.
|
|
227
227
|
if (node.children[1].majorType !== 2) throw _err(code, label + " algorithm parameters must be a CBOR byte string");
|
|
228
|
+
// The parameters carry ONE complete DER element spliced into the reconstruction. An empty byte
|
|
229
|
+
// string is no element at all: it rebuilds the same AlgorithmIdentifier as the bare ~oid form,
|
|
230
|
+
// which is how the CDDL already spells "no parameters" -- so accepting it would give one
|
|
231
|
+
// algorithm two encodings, and one X.509 signature would cover both.
|
|
232
|
+
if (node.children[1].content.length === 0) throw _err(code, label + " algorithm parameters must not be an empty byte string; omit them with the bare ~oid form (draft sec. 3.1.3)");
|
|
228
233
|
return { name: a.name, oid: a.oid, parameters: node.children[1].content };
|
|
229
234
|
}
|
|
230
235
|
throw _err(code, label + " is not a C509 AlgorithmIdentifier (int / ~oid / [~oid, params])");
|
|
@@ -248,6 +253,40 @@ function _specialText(node) {
|
|
|
248
253
|
throw _err("c509/bad-name", "an attribute value is not a C509 SpecialText (text / bytes / tag-48)");
|
|
249
254
|
}
|
|
250
255
|
|
|
256
|
+
// draft sec. 3.1.4 fixes WHICH of the three SpecialText spellings a value takes -- the choice belongs to
|
|
257
|
+
// the specification, not the sender. A text string of an even length >= 2 drawn only from '0'-'9'/'a'-'f'
|
|
258
|
+
// is encoded as a byte string; a text string of the form "HH-HH-HH-HH-HH-HH-HH-HH" is encoded as a tag-48
|
|
259
|
+
// MAC address, 48-bit when it matches "HH-HH-HH-FF-FE-HH-HH-HH" and 64-bit otherwise; anything else is a
|
|
260
|
+
// text string. Accepting a second spelling of one value would give the reconstructed certificate more than
|
|
261
|
+
// one C509 encoding, so a single X.509 signature would cover them all and two distinct byte strings would
|
|
262
|
+
// name one certificate. NOT enforced for a natively signed certificate: sec. 3.1.4 says bytes and tag 48
|
|
263
|
+
// there "do not correspond to any predefined text string encoding and may also be used for other attribute
|
|
264
|
+
// types", so no canonical text spelling exists to hold one to.
|
|
265
|
+
var _HEX_OPTIMIZED = /^(?:[0-9a-f]{2})+$/;
|
|
266
|
+
var _EUI64_TEXT = /^(?:[0-9A-F]{2}-){7}[0-9A-F]{2}$/;
|
|
267
|
+
function _assertCanonicalSpecialText(node, isNative, E) {
|
|
268
|
+
if (isNative) return;
|
|
269
|
+
if (node.majorType === 3) {
|
|
270
|
+
var t = cbor.read.textString(node);
|
|
271
|
+
if (t.length >= 2 && _HEX_OPTIMIZED.test(t)) {
|
|
272
|
+
throw _err(E, "a text attribute value of even-length hex characters must be encoded as a CBOR byte string (draft sec. 3.1.4)");
|
|
273
|
+
}
|
|
274
|
+
if (_EUI64_TEXT.test(t)) {
|
|
275
|
+
throw _err(E, "a text attribute value in EUI-64 form must be encoded as a CBOR tag-48 MAC address (draft sec. 3.1.4)");
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (node.majorType === 2) {
|
|
280
|
+
// An empty byte string renders as the empty text, whose canonical spelling is a text string.
|
|
281
|
+
if (node.content.length === 0) throw _err(E, "an empty attribute value must be encoded as a CBOR text string (draft sec. 3.1.4)");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
var eui = node.children[0].content;
|
|
285
|
+
if (eui.length === 8 && eui[3] === 0xff && eui[4] === 0xfe) {
|
|
286
|
+
throw _err(E, "an EUI-64 of the form HH-HH-HH-FF-FE-HH-HH-HH must be encoded as a 48-bit MAC address (draft sec. 3.1.4)");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
251
290
|
// A single Name (sec. 3.1.4/sec. 3.1.6): the CBOR simple null (issuer only) | a bare SpecialText single
|
|
252
291
|
// commonName | an array of RDNAttributes. Surfaces { dn, rdns, eui64? } shape-compatible with x509.
|
|
253
292
|
// An rdn's (attribute type, sign) pair DECLARES an X.509 string type, so its text must be valid for that type --
|
|
@@ -269,6 +308,7 @@ function _name509(node, isSubject, isNative) {
|
|
|
269
308
|
// A bare SpecialText (not an array) is a single commonName attribute (attributeType == +1).
|
|
270
309
|
if (node.majorType === 3 || node.majorType === 2 || node.majorType === 6) {
|
|
271
310
|
var sv = _specialText(node);
|
|
311
|
+
_assertCanonicalSpecialText(node, isNative, "c509/bad-name");
|
|
272
312
|
// The bare form is a single commonName -- hold its value to the SAME rules the array form applies, so a
|
|
273
313
|
// natively-signed certificate (which never reconstructs) cannot carry a value the reconstruction would refuse.
|
|
274
314
|
// A tag-48 MAC renders to a fixed 17-character EUI-64 string that satisfies every value rule by
|
|
@@ -307,6 +347,7 @@ function _name509(node, isSubject, isNative) {
|
|
|
307
347
|
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
308
348
|
if (tname === undefined) throw _err("c509/bad-name", "attribute type integer " + ti + " has no C509 registry row");
|
|
309
349
|
var v = _specialText(kids[i + 1]);
|
|
350
|
+
_assertCanonicalSpecialText(kids[i + 1], isNative, "c509/bad-name");
|
|
310
351
|
var vv = v.text !== undefined ? v.text : (v.hex !== undefined ? v.hex : _macToEui64String(v.eui64));
|
|
311
352
|
// The RENDERED text is what carries into the rdn -- deliberately NOT the raw eui64, because the EUI shortcut
|
|
312
353
|
// in _reconAttrValue is unconditional and would take precedence over the attribute's declared string type,
|
|
@@ -317,6 +358,13 @@ function _name509(node, isSubject, isNative) {
|
|
|
317
358
|
rdns.push(rdn);
|
|
318
359
|
parts.push(_shortName(tname) + "=" + guard.name.escapeDnValue(vv));
|
|
319
360
|
}
|
|
361
|
+
// sec. 3.1.4: "If Name contains a single 'common name' attribute with attributeType = +1, it is for
|
|
362
|
+
// compactness encoded as just the SpecialText containing the single attribute value." The array
|
|
363
|
+
// spelling of that one case is therefore a second encoding of a name that already has a canonical
|
|
364
|
+
// one. Only +1 has a bare form -- a negative (printableString) commonName keeps the array.
|
|
365
|
+
if (!isNative && rdns.length === 1 && rdns[0].type === "commonName" && !rdns[0].printable) {
|
|
366
|
+
throw _err("c509/bad-name", "a Name holding a single +1 commonName must be encoded as a bare SpecialText, not an array (draft sec. 3.1.4)");
|
|
367
|
+
}
|
|
320
368
|
return { rdns: rdns, dn: parts.join(",") };
|
|
321
369
|
}
|
|
322
370
|
// The rendered dn is an RFC 4514 string, so every VALUE is escaped through the shared guard the rest of the
|
|
@@ -1109,6 +1157,14 @@ function _extValueToDer(name, node, isNative) {
|
|
|
1109
1157
|
case "subjectAltName":
|
|
1110
1158
|
case "issuerAltName": // SubjectAltName = GeneralNames / text (exactly one dNSName -> bare text)
|
|
1111
1159
|
if (node.majorType === 3) return b.sequence([b.contextPrimitive(2, _ia5Bytes(node, 2))]);
|
|
1160
|
+
// sec. 3.3: "If subjectAltName contains exactly one dNSName, the array and the int are omitted and
|
|
1161
|
+
// extensionValue is the dNSName encoded as a CBOR text string." The array spelling of that one case
|
|
1162
|
+
// is a second encoding of the same extension value.
|
|
1163
|
+
if (!isNative && node.majorType === 4 && node.children && node.children.length === 2 &&
|
|
1164
|
+
node.children[0].majorType === 0 && Number(cbor.read.int(node.children[0])) === 2 &&
|
|
1165
|
+
node.children[1].majorType === 3) {
|
|
1166
|
+
throw _err("c509/bad-extensions", "a " + name + " holding exactly one dNSName must be encoded as a bare CBOR text string, not an array (draft sec. 3.3)");
|
|
1167
|
+
}
|
|
1112
1168
|
return b.sequence(_generalNamesToDer(node, isNative));
|
|
1113
1169
|
case "nameConstraints": { // [ permittedSubtrees / null, excludedSubtrees / null ]
|
|
1114
1170
|
if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "a nameConstraints value must be a 2-element CBOR array [ permitted, excluded ] (sec. 3.3)");
|
|
@@ -1747,6 +1803,14 @@ function _extensions(node, isNative) {
|
|
|
1747
1803
|
// Each extension is an (extensionID, extensionValue) pair; an odd-length array is a dangling
|
|
1748
1804
|
// extension identifier with no value -- reject rather than silently drop the trailing element.
|
|
1749
1805
|
if (kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a C509 extensions array must be id/value pairs (dangling extension identifier)");
|
|
1806
|
+
// sec. 3.1.10: "If the CBOR array contains exactly two ints and the absolute value of the first int is
|
|
1807
|
+
// 2 (corresponding to keyUsage), the CBOR array is omitted and the 'extensions' field is encoded as a
|
|
1808
|
+
// single CBOR int." The array spelling of that one case is a second encoding of the same extensions.
|
|
1809
|
+
if (!isNative && kids.length === 2 &&
|
|
1810
|
+
(kids[0].majorType === 0 || kids[0].majorType === 1) && (kids[1].majorType === 0 || kids[1].majorType === 1) &&
|
|
1811
|
+
Math.abs(Number(cbor.read.int(kids[0]))) === 2) {
|
|
1812
|
+
throw _err("c509/bad-extensions", "an extensions field holding only keyUsage must be encoded as a single CBOR int, not an array (draft sec. 3.1.10)");
|
|
1813
|
+
}
|
|
1750
1814
|
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
1751
1815
|
var idNode = kids[i], valNode = kids[i + 1];
|
|
1752
1816
|
var name, extOid, critical, valContent;
|
|
@@ -1921,13 +1985,21 @@ function _reconAlgId(alg) {
|
|
|
1921
1985
|
// The full type-3 -> DER Certificate reconstruction, byte-for-byte.
|
|
1922
1986
|
function _reconstructDer(r, sigNode) {
|
|
1923
1987
|
var sigAlgSeq = _reconAlgId(r.signatureAlgorithm);
|
|
1988
|
+
var subjectName = _reconName(r.subject);
|
|
1989
|
+
var spelledIssuer = r.issuer && r.issuer.rdns ? _reconName(r.issuer) : null;
|
|
1990
|
+
// sec. 3.1.4: an issuer identical to the subject MUST be the CBOR simple value null. Spelling it
|
|
1991
|
+
// out instead rebuilds the same DER, so one certificate would have two C509 encodings and the one
|
|
1992
|
+
// X.509 signature over that DER would cover both.
|
|
1993
|
+
if (spelledIssuer !== null && spelledIssuer.equals(subjectName)) {
|
|
1994
|
+
throw _err("c509/bad-name", "an issuer identical to the subject must be encoded as the CBOR simple value null (draft sec. 3.1.4)");
|
|
1995
|
+
}
|
|
1924
1996
|
var tbsFields = [
|
|
1925
1997
|
b.explicit(0, b.integer(2n)), // version v3 (type-3 is X.509 v3)
|
|
1926
1998
|
b.integer(r.serialNumber),
|
|
1927
1999
|
sigAlgSeq,
|
|
1928
|
-
|
|
2000
|
+
spelledIssuer === null ? subjectName : spelledIssuer, // null issuer -> issuer == subject
|
|
1929
2001
|
b.sequence([_reconTime(r.validity.notBefore), _reconTime(r.validity.notAfter)]),
|
|
1930
|
-
|
|
2002
|
+
subjectName,
|
|
1931
2003
|
_reconSpki(r.subjectPublicKeyAlgorithm, r.subjectPublicKey, r.rsaPublicKey),
|
|
1932
2004
|
];
|
|
1933
2005
|
// RFC 5280 sec. 4.1: the [3] extensions field is OPTIONAL and, when present, SHALL contain at least one
|
|
@@ -2088,6 +2160,10 @@ function _minBytes(n) {
|
|
|
2088
2160
|
return Buffer.from(hex, "hex");
|
|
2089
2161
|
}
|
|
2090
2162
|
// A C509 AlgorithmIdentifier -> int (registry) | ~oid (bare bytes) | [~oid, params]. `key` selects the row.
|
|
2163
|
+
// The bare-~oid arm is unreachable from either call site as the type-3 encoder stands, and is kept as
|
|
2164
|
+
// the honest fallback rather than an assumption: the signature slot refuses every non-ECDSA algorithm
|
|
2165
|
+
// before this runs, and every curve the subjectPublicKey slot accepts (prime256v1 / secp384r1 /
|
|
2166
|
+
// secp521r1) carries an ecPublicKey registry row. Adding a curve without its row would reach it.
|
|
2091
2167
|
function _encAlgorithm(alg, toInt, key) {
|
|
2092
2168
|
var i = toInt[key];
|
|
2093
2169
|
if (i !== undefined && !(alg.parameters && alg.parameters.length)) return cbor.build.int(BigInt(i));
|
|
@@ -2095,10 +2171,16 @@ function _encAlgorithm(alg, toInt, key) {
|
|
|
2095
2171
|
if (alg.parameters && alg.parameters.length) return cbor.build.array([oidBytes, cbor.build.byteString(alg.parameters)]);
|
|
2096
2172
|
return oidBytes;
|
|
2097
2173
|
}
|
|
2098
|
-
// A SpecialText attribute value -> CBOR
|
|
2174
|
+
// A SpecialText attribute value -> CBOR, walking the sec. 3.1.4 cascade: an EUI-64 is a tag-48 MAC
|
|
2175
|
+
// address, a text string of even length >= 2 drawn only from '0'-'9'/'a'-'f' is a byte string, and
|
|
2176
|
+
// anything else is a text string. The decoder holds an incoming certificate to exactly this cascade,
|
|
2177
|
+
// so the encoder walks it too -- a spelling chosen here that the decoder refuses would make this
|
|
2178
|
+
// encoder emit certificates its own parser rejects.
|
|
2099
2179
|
function _encSpecialText(rdn) {
|
|
2100
2180
|
if (rdn.eui64) return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
|
|
2101
|
-
|
|
2181
|
+
var v = String(rdn.value);
|
|
2182
|
+
if (v.length >= 2 && _HEX_OPTIMIZED.test(v)) return cbor.build.byteString(Buffer.from(v, "hex"));
|
|
2183
|
+
return cbor.build.textString(v);
|
|
2102
2184
|
}
|
|
2103
2185
|
// A Name -> CBOR: null (issuer only) | a bare SpecialText single utf8 commonName | an array of RDN pairs.
|
|
2104
2186
|
function _encName(name, isSubject) {
|
|
@@ -2375,7 +2457,13 @@ _derToType3 = function (input, opts) {
|
|
|
2375
2457
|
certificateType: 3,
|
|
2376
2458
|
serialNumber: c.serialNumber, // no serialNumberHex -> the encoder uses the minimal ~biguint magnitude
|
|
2377
2459
|
signatureAlgorithm: { name: c.signatureAlgorithm.name, oid: c.signatureAlgorithm.oid },
|
|
2378
|
-
|
|
2460
|
+
// sec. 3.1.4: "If the 'issuer' field is identical to the 'subject' field, e.g., in case of
|
|
2461
|
+
// self-signed certificates, then the 'issuer' field MUST be encoded as the CBOR simple value
|
|
2462
|
+
// null." Compared on the RAW DER bytes, not on a canonicalized name: the reconstruction rebuilds
|
|
2463
|
+
// a null issuer FROM the subject, so two names that merely compare equal (a PrintableString
|
|
2464
|
+
// against a UTF8String of the same characters) would rebuild different bytes and break the
|
|
2465
|
+
// signature that covers them. Byte-identical is exactly the condition under which that is safe.
|
|
2466
|
+
issuer: c.issuer.bytes.equals(c.subject.bytes) ? null : _c509NameFromDer(c.issuer.bytes),
|
|
2379
2467
|
validity: { notBefore: c.validity.notBefore, notAfter: c.validity.notAfter.getTime() === _NO_EXPIRY ? null : c.validity.notAfter },
|
|
2380
2468
|
subject: _c509NameFromDer(c.subject.bytes),
|
|
2381
2469
|
subjectPublicKeyAlgorithm: { name: "ecPublicKey", oid: c.subjectPublicKeyInfo.algorithm.oid, curve: curve },
|
package/lib/sign-scheme.js
CHANGED
|
@@ -170,9 +170,9 @@ function _normCompositeKeys(key, comp, E) {
|
|
|
170
170
|
function _importKey(key, imp, E) {
|
|
171
171
|
if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
|
|
172
172
|
_assertKeyMatchesScheme(key, imp, E);
|
|
173
|
-
// `key` is documented as taking a CryptoKey, so
|
|
174
|
-
//
|
|
175
|
-
//
|
|
173
|
+
// `key` is documented as taking a CryptoKey, so one created outside this engine signs here too --
|
|
174
|
+
// it is re-imported through this engine, because it carries none of the key material this engine
|
|
175
|
+
// signs with. One whose material this process cannot reach is refused with that as the reason.
|
|
176
176
|
return webcrypto.adoptKey(key, imp, ["sign"], E, "bad-input");
|
|
177
177
|
}
|
|
178
178
|
var der;
|
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:1feef474-ddbd-4d2d-ae82-845a967ba69b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-08T20:12:36.384Z",
|
|
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.4.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.4.6",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.4.
|
|
25
|
+
"version": "0.4.6",
|
|
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.4.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.4.6",
|
|
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.4.
|
|
57
|
+
"ref": "@blamejs/pki@0.4.6",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|