@blamejs/pki 0.3.33 → 0.4.1
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 +32 -1
- package/README.md +1 -1
- package/lib/asn1-der.js +13 -1
- package/lib/attrcert-sign.js +1 -1
- package/lib/crl-sign.js +3 -3
- package/lib/key.js +6 -6
- package/lib/oid.js +1 -0
- package/lib/pkcs12-build.js +3 -3
- package/lib/schema-c509.js +344 -21
- package/package.json +3 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,38 @@ 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.
|
|
7
|
+
## v0.4.1 — 2026-08-08
|
|
8
|
+
|
|
9
|
+
pki.schema.c509 encodes and decodes the compact subjectDirectoryAttributes value form -- a C509 certificate's subject directory attributes ride their draft-20 registry integers (or unwrapped OIDs) with their directory-string values, interoperating with a conformant C509 implementation rather than only this decoder.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.c509 encodes and decodes the compact value form for the subjectDirectoryAttributes extension (draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3): a flat array of (attribute type, attribute values) pairs where each type is a sec. 8.6 registry integer (the sign selecting the directory-string type) or an unwrapped OID, and each values slot is a non-empty array holding the attribute's SET of one or more values -- the string values for a registry-integer type, or the raw DER attribute values for an unwrapped-OID type. Both directions invert to the DER extnValue byte-for-byte, so a certificate carrying the extension is the specific compact shape a conformant C509 implementation reads rather than an opaque DER byte string. The encoder is guarded per attribute and per extension: an attribute whose value is not a directory string, or whose value SET mixes string types, uses the unwrapped-OID form for that attribute (keeping the rest of the extension compact); a value the compact form cannot represent falls the whole extension back to the unwrapped-OID byte-string form instead of encoding lossily; and a malformed compact value fails closed with a typed C509Error.
|
|
14
|
+
- The OID registry gains the subjectDirectoryAttributes certificate-extension identifier (2.5.29.9), resolvable through pki.oid.byName / pki.oid.name.
|
|
15
|
+
- The ASN.1 codec reads NumericString (pki.asn1.TAGS.NUMERIC_STRING), the X.520 syntax of the x121Address and internationalISDNNumber directory attributes. It is validated strictly like every other string type: a value outside the digits-and-space set the type permits is rejected as malformed rather than decoded.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- A C509 certificate carrying subjectDirectoryAttributes now encodes to -- and decodes from -- its compact CBOR value shape rather than the unwrapped-OID byte-string form an earlier release emitted; both reconstruct the same DER, but the CBOR bytes differ, so re-encode any C509 produced by an earlier release.
|
|
20
|
+
|
|
21
|
+
## v0.4.0 — 2026-08-06
|
|
22
|
+
|
|
23
|
+
CRL issuance and verification, PKCS#12 build and open, attribute-certificate issuance, and the key-material lifecycle graduate to stable, and pki.schema.c509 adds the compact policyMappings and policyConstraints value forms -- a C509 certificate's policy mappings and policy constraints now ride their specific draft-20 CBOR shape and interoperate with a conformant C509 implementation rather than only this decoder.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- pki.schema.c509 encodes and decodes the compact value forms for the policyMappings and policyConstraints extensions (draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3): a policy mapping's issuerDomainPolicy and subjectDomainPolicy each ride the sec. 8.9 registry-integer / unwrapped-OID policy space the certificatePolicies extension uses (a policy mapping to or from the special anyPolicy is preserved, matching what a certificate's own decoder accepts), and a policy constraints value rides the fixed two-element [requireExplicitPolicy, inhibitPolicyMapping] array, each field a non-negative skip count or absent. Both directions invert to the DER extnValue byte-for-byte, so a certificate carrying either extension is the specific compact shape a conformant C509 implementation reads rather than an opaque DER byte string. The encoder is guarded: a value the compact form cannot hold falls the whole extension back to the unwrapped-OID byte-string form instead of encoding lossily; a malformed compact value -- an empty or both-absent policy constraints, an odd-length or empty policy-mappings array, a policy mapping member that is not a two-policy pair, or an unregistered policy integer -- fails closed with a typed C509Error.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- pki.crl.sign / verify / isRevoked (RFC 5280 sec. 5), pki.pkcs12.build / open / verifyMac (RFC 7292 / RFC 9579), pki.attrcert.sign (RFC 5755), and the key-material lifecycle pki.key.encrypt / decrypt / export / import / generate / publicFromPrivate (PKCS#8 / RFC 5958, RFC 8018) graduate from experimental to stable. Their governing standards are settled and each is proven against an independent implementation in the integration harness (OpenSSL), or for the attribute-certificate format through the toolkit's own conformance-vector round-trip plus coverage-guided fuzzing. They are now covered by the stability contract: a breaking change to any of them ships only after a prior deprecation cycle, never silently in a minor (the published LTS support window itself takes effect at v1.0).
|
|
32
|
+
- A C509 certificate carrying policyMappings or policyConstraints now encodes to -- and decodes from -- its compact CBOR value shape rather than the unwrapped-OID byte-string form an earlier release emitted; both reconstruct the same DER, but the CBOR bytes differ, so re-encode any C509 produced by an earlier release.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- The C509 encoder now bounds the basicConstraints path length and the inhibitAnyPolicy and policyConstraints skip counts to the same non-negative 31-bit range the toolkit's own certificate decoders enforce. A native C509 carrying one of these counts past that range now fails closed with a typed C509Error rather than reconstructing a DER an X.509 decoder -- this toolkit's included -- would then reject.
|
|
37
|
+
|
|
38
|
+
## v0.3.33 — 2026-08-05
|
|
8
39
|
|
|
9
40
|
pki.schema.c509 encodes and decodes the compact certificatePolicies value form -- a C509 certificate's policy identifiers ride their draft-20 registry integers (or unwrapped OIDs) and their CPS-URI and UserNotice qualifiers ride the specific compact CBOR shape, interoperating with a conformant C509 implementation rather than only this decoder.
|
|
10
41
|
|
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ is callable today; nothing below is a stub.
|
|
|
204
204
|
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
|
|
205
205
|
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
206
206
|
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
207
|
-
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec,
|
|
207
|
+
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms — the scalar extensions (keyUsage, basicConstraints, extended key usage, subject key identifier, and more), the general-name-bearing extensions (subjectAltName, issuer alt name, name constraints, CRL distribution points, authority/subject information access, and the full authority key identifier) over one shared GeneralNames value codec, certificate policies (registry-integer or OID policy identifiers with their CPS-URI and UserNotice qualifiers), and policy mappings and policy constraints, and subject directory attributes; a certificate outside the invertible set throws a typed `C509Error` |
|
|
208
208
|
| `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
209
209
|
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
210
210
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
package/lib/asn1-der.js
CHANGED
|
@@ -66,6 +66,7 @@ var UNIVERSAL_TYPES = {
|
|
|
66
66
|
UTF8_STRING: { tag: 0x0c, form: "primitive" },
|
|
67
67
|
SEQUENCE: { tag: 0x10, form: "constructed" },
|
|
68
68
|
SET: { tag: 0x11, form: "constructed" },
|
|
69
|
+
NUMERIC_STRING: { tag: 0x12, form: "primitive" },
|
|
69
70
|
PRINTABLE_STRING: { tag: 0x13, form: "primitive" },
|
|
70
71
|
TELETEX_STRING: { tag: 0x14, form: "primitive" },
|
|
71
72
|
IA5_STRING: { tag: 0x16, form: "primitive" },
|
|
@@ -95,7 +96,7 @@ var CONSTRUCTED_BIT = 0x20;
|
|
|
95
96
|
// constructed-capable set is DERIVED from the registry above and keyed by tag
|
|
96
97
|
// for an O(1) decode-time form check. It is a WHITELIST: a universal tag
|
|
97
98
|
// outside it -- a registered primitive type and an unregistered one
|
|
98
|
-
// (
|
|
99
|
+
// (GeneralString, ObjectDescriptor, REAL, ...) alike -- has no
|
|
99
100
|
// DER constructed form, so the default is reject.
|
|
100
101
|
var CONSTRUCTED_ONLY_UNIVERSAL_TAGS = Object.create(null);
|
|
101
102
|
Object.keys(UNIVERSAL_TYPES).forEach(function (k) {
|
|
@@ -622,6 +623,15 @@ function _decodePrintable(buf) {
|
|
|
622
623
|
return s;
|
|
623
624
|
}
|
|
624
625
|
|
|
626
|
+
// NumericString (X.680 sec. 41, Table 9) permits ONLY the digits and SPACE. It carries the X.520 syntax of
|
|
627
|
+
// the x121Address and internationalISDNNumber directory attributes, so a value of this type must be readable
|
|
628
|
+
// -- and strictly, since anything outside that alphabet is not a valid encoding of the type.
|
|
629
|
+
function _decodeNumeric(buf) {
|
|
630
|
+
var s = buf.toString("latin1");
|
|
631
|
+
if (!NUMERIC_RE.test(s)) throw new Asn1Error("asn1/bad-numeric-string", "NumericString has characters outside the digits-and-space set");
|
|
632
|
+
return s;
|
|
633
|
+
}
|
|
634
|
+
|
|
625
635
|
// UTF8String must be well-formed UTF-8; a lenient decoder substitutes U+FFFD
|
|
626
636
|
// for invalid sequences, silently mangling hostile input into valid text.
|
|
627
637
|
function _decodeUtf8Strict(buf) {
|
|
@@ -637,6 +647,7 @@ function readString(node) {
|
|
|
637
647
|
_expectPrimitive(node, "readString");
|
|
638
648
|
switch (node.tagNumber) {
|
|
639
649
|
case TAGS.UTF8_STRING: return _decodeUtf8Strict(node.content);
|
|
650
|
+
case TAGS.NUMERIC_STRING: return _decodeNumeric(node.content);
|
|
640
651
|
case TAGS.PRINTABLE_STRING: return _decodePrintable(node.content);
|
|
641
652
|
case TAGS.IA5_STRING: return _decodeIa5(node.content);
|
|
642
653
|
case TAGS.TELETEX_STRING: return _decodeText(node.content, "latin1");
|
|
@@ -870,6 +881,7 @@ function encodeOidContent(dotted) {
|
|
|
870
881
|
}
|
|
871
882
|
|
|
872
883
|
var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
|
|
884
|
+
var NUMERIC_RE = /^[0-9 ]*$/;
|
|
873
885
|
|
|
874
886
|
function _fmtTwo(n) { return (n < 10 ? "0" : "") + n; }
|
|
875
887
|
|
package/lib/attrcert-sign.js
CHANGED
|
@@ -408,7 +408,7 @@ function _buildExtensions(extSpec, aaSpki) {
|
|
|
408
408
|
* @primitive pki.attrcert.sign
|
|
409
409
|
* @signature pki.attrcert.sign(spec, issuer, opts?) -> Promise<Buffer|string>
|
|
410
410
|
* @since 0.3.2
|
|
411
|
-
* @status
|
|
411
|
+
* @status stable
|
|
412
412
|
* @spec RFC 5755
|
|
413
413
|
* @defends forged-attribute-certificate (CWE-347)
|
|
414
414
|
* @related pki.schema.attrcert.parse, pki.x509.sign
|
package/lib/crl-sign.js
CHANGED
|
@@ -454,7 +454,7 @@ function _sign(spec, issuer, opts) {
|
|
|
454
454
|
* @primitive pki.crl.sign
|
|
455
455
|
* @signature pki.crl.sign(spec, issuer, opts?) -> Promise<Buffer|string>
|
|
456
456
|
* @since 0.3.9
|
|
457
|
-
* @status
|
|
457
|
+
* @status stable
|
|
458
458
|
* @spec RFC 5280 sec. 5, RFC 9882, RFC 9814
|
|
459
459
|
* @defends crl-forgery (CWE-347)
|
|
460
460
|
* @related pki.schema.crl.parse, pki.crl.verify, pki.path.crlChecker
|
|
@@ -510,7 +510,7 @@ function _resolveIssuerSpki(issuer) {
|
|
|
510
510
|
* @primitive pki.crl.verify
|
|
511
511
|
* @signature pki.crl.verify(crl, issuer) -> Promise<boolean>
|
|
512
512
|
* @since 0.3.9
|
|
513
|
-
* @status
|
|
513
|
+
* @status stable
|
|
514
514
|
* @spec RFC 5280 sec. 5.1.1.3, RFC 9814
|
|
515
515
|
* @defends crl-signature-bypass (CWE-347)
|
|
516
516
|
* @related pki.crl.sign, pki.path.crlChecker, pki.schema.crl.parse
|
|
@@ -549,7 +549,7 @@ function _serialHexOf(serial) {
|
|
|
549
549
|
* @primitive pki.crl.isRevoked
|
|
550
550
|
* @signature pki.crl.isRevoked(crl, serialNumber) -> entry | null
|
|
551
551
|
* @since 0.3.9
|
|
552
|
-
* @status
|
|
552
|
+
* @status stable
|
|
553
553
|
* @spec RFC 5280 sec. 5.1.2.6
|
|
554
554
|
* @related pki.crl.verify, pki.schema.crl.parse
|
|
555
555
|
*
|
package/lib/key.js
CHANGED
|
@@ -81,7 +81,7 @@ async function _toPrivateKeyDer(input) {
|
|
|
81
81
|
* @primitive pki.key.encrypt
|
|
82
82
|
* @signature pki.key.encrypt(privateKey, password, opts?) -> Promise<Buffer|string>
|
|
83
83
|
* @since 0.3.10
|
|
84
|
-
* @status
|
|
84
|
+
* @status stable
|
|
85
85
|
* @spec RFC 5958 sec. 3, RFC 8018
|
|
86
86
|
* @related pki.key.decrypt, pki.schema.pkcs8.parseEncrypted, pki.cms.encrypt
|
|
87
87
|
*
|
|
@@ -132,7 +132,7 @@ async function encrypt(privateKey, password, opts) {
|
|
|
132
132
|
* @primitive pki.key.decrypt
|
|
133
133
|
* @signature pki.key.decrypt(encrypted, password, opts?) -> Promise<Buffer|string>
|
|
134
134
|
* @since 0.3.10
|
|
135
|
-
* @status
|
|
135
|
+
* @status stable
|
|
136
136
|
* @spec RFC 5958 sec. 3, RFC 8018 sec. 6.2, RFC 8018 sec. 8
|
|
137
137
|
* @defends pbes2-padding-oracle (CWE-208), pbkdf2-work-dos (CWE-400)
|
|
138
138
|
* @related pki.key.encrypt, pki.schema.pkcs8.parse
|
|
@@ -187,7 +187,7 @@ function _decryptPbes2(encAlg, ciphertext, password, opts) {
|
|
|
187
187
|
* @primitive pki.key.export
|
|
188
188
|
* @signature pki.key.export(key, opts?) -> Promise<Buffer|string>
|
|
189
189
|
* @since 0.3.10
|
|
190
|
-
* @status
|
|
190
|
+
* @status stable
|
|
191
191
|
* @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
|
|
192
192
|
* @related pki.key.import, pki.key.publicFromPrivate, pki.schema.pkcs8.parse
|
|
193
193
|
*
|
|
@@ -222,7 +222,7 @@ async function export_(key, opts) {
|
|
|
222
222
|
* @primitive pki.key.import
|
|
223
223
|
* @signature pki.key.import(input, opts?) -> Promise<CryptoKey>
|
|
224
224
|
* @since 0.3.10
|
|
225
|
-
* @status
|
|
225
|
+
* @status stable
|
|
226
226
|
* @spec RFC 5958, RFC 5280 sec. 4.1.2.7, RFC 8018
|
|
227
227
|
* @related pki.key.export, pki.key.decrypt
|
|
228
228
|
*
|
|
@@ -265,7 +265,7 @@ async function import_(input, opts) {
|
|
|
265
265
|
* @primitive pki.key.generate
|
|
266
266
|
* @signature pki.key.generate(algorithm, opts?) -> Promise<{ privateKey, publicKey }>
|
|
267
267
|
* @since 0.3.10
|
|
268
|
-
* @status
|
|
268
|
+
* @status stable
|
|
269
269
|
* @spec W3C WebCrypto, FIPS 203, FIPS 204
|
|
270
270
|
* @related pki.key.export, pki.key.publicFromPrivate
|
|
271
271
|
*
|
|
@@ -296,7 +296,7 @@ async function generate(algorithm, opts) {
|
|
|
296
296
|
* @primitive pki.key.publicFromPrivate
|
|
297
297
|
* @signature pki.key.publicFromPrivate(privateKey, opts?) -> Promise<Buffer|string>
|
|
298
298
|
* @since 0.3.10
|
|
299
|
-
* @status
|
|
299
|
+
* @status stable
|
|
300
300
|
* @spec RFC 5280 sec. 4.1.2.7, RFC 8410 sec. 3
|
|
301
301
|
* @related pki.key.export, pki.key.import
|
|
302
302
|
*
|
package/lib/oid.js
CHANGED
|
@@ -60,6 +60,7 @@ var FAMILIES = {
|
|
|
60
60
|
|
|
61
61
|
// RFC 5280 certificate extensions.
|
|
62
62
|
certExtension: { base: [2, 5, 29], of: {
|
|
63
|
+
subjectDirectoryAttributes: 9, // id-ce 9 (RFC 5280 sec. 4.2.1.8) -> 2.5.29.9
|
|
63
64
|
subjectKeyIdentifier: 14, keyUsage: 15, subjectAltName: 17, issuerAltName: 18,
|
|
64
65
|
basicConstraints: 19,
|
|
65
66
|
// CRL + CRL-entry extensions (RFC 5280 sec. 5.2, sec. 5.3)
|
package/lib/pkcs12-build.js
CHANGED
|
@@ -403,7 +403,7 @@ function _normalizeSpec(spec, opts) {
|
|
|
403
403
|
* @primitive pki.pkcs12.build
|
|
404
404
|
* @signature pki.pkcs12.build(spec, opts?) -> Promise<Buffer|string>
|
|
405
405
|
* @since 0.3.11
|
|
406
|
-
* @status
|
|
406
|
+
* @status stable
|
|
407
407
|
* @spec RFC 7292, RFC 9579, RFC 8018
|
|
408
408
|
* @related pki.schema.pkcs12.parse, pki.pkcs12.verifyMac
|
|
409
409
|
*
|
|
@@ -468,7 +468,7 @@ async function build(spec, opts) {
|
|
|
468
468
|
* @primitive pki.pkcs12.verifyMac
|
|
469
469
|
* @signature pki.pkcs12.verifyMac(pfx, password, opts?) -> Promise<boolean>
|
|
470
470
|
* @since 0.3.11
|
|
471
|
-
* @status
|
|
471
|
+
* @status stable
|
|
472
472
|
* @spec RFC 7292 sec. 5.1, RFC 9579
|
|
473
473
|
* @defends pkcs12-mac-forgery (CWE-347)
|
|
474
474
|
* @related pki.pkcs12.build, pki.schema.pkcs12.parse
|
|
@@ -533,7 +533,7 @@ function _capWork(iterations, salt, opts, keyLength, hardCap) {
|
|
|
533
533
|
* @primitive pki.pkcs12.open
|
|
534
534
|
* @signature pki.pkcs12.open(pfx, password, opts?) -> Promise<OpenResult>
|
|
535
535
|
* @since 0.3.12
|
|
536
|
-
* @status
|
|
536
|
+
* @status stable
|
|
537
537
|
* @spec RFC 7292 sec. 5.1, RFC 9579, RFC 8018
|
|
538
538
|
* @defends pkcs12-unauthenticated-decrypt (CWE-347), pbes2-padding-oracle (CWE-208)
|
|
539
539
|
* @related pki.pkcs12.build, pki.pkcs12.verifyMac, pki.schema.pkcs12.parse
|
package/lib/schema-c509.js
CHANGED
|
@@ -57,10 +57,12 @@ var PK_ALG_BY_INT = {
|
|
|
57
57
|
// EC curve -> field size in bytes (the SEC1 coordinate width), for point-length validation.
|
|
58
58
|
var EC_FIELD_BYTES = { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 };
|
|
59
59
|
|
|
60
|
-
// sec. 8.6 RDN attribute types (abs(int) -> name; the sign selects the X.509 string type
|
|
61
|
-
//
|
|
62
|
-
//
|
|
60
|
+
// sec. 8.6 RDN attribute types (abs(int) -> name; the sign selects the X.509 string type: positive utf8String,
|
|
61
|
+
// negative printableString). An attribute whose value is ALWAYS an IA5String is instead "unambiguously
|
|
62
|
+
// represented using a non-negative int" (draft sec. 3.1.4) -- emailAddress is int 0, so its value reconstructs
|
|
63
|
+
// as an IA5String and its sign carries no string-type meaning.
|
|
63
64
|
var ATTR_BY_INT = {
|
|
65
|
+
0: _name("emailAddress"),
|
|
64
66
|
1: _name("commonName"),
|
|
65
67
|
2: _name("surname"),
|
|
66
68
|
3: _name("serialNumber"),
|
|
@@ -87,8 +89,11 @@ var EXT_BY_INT = {
|
|
|
87
89
|
7: _name("authorityKeyIdentifier"),
|
|
88
90
|
8: _name("extKeyUsage"),
|
|
89
91
|
9: _name("authorityInfoAccess"),
|
|
92
|
+
24: _name("subjectDirectoryAttributes"),
|
|
90
93
|
25: _name("issuerAltName"),
|
|
91
94
|
26: _name("nameConstraints"),
|
|
95
|
+
27: _name("policyMappings"),
|
|
96
|
+
28: _name("policyConstraints"),
|
|
92
97
|
29: _name("freshestCRL"),
|
|
93
98
|
30: _name("inhibitAnyPolicy"),
|
|
94
99
|
31: _name("subjectInfoAccess"),
|
|
@@ -102,6 +107,7 @@ var EXT_COMPACT = {
|
|
|
102
107
|
extKeyUsage: 1, inhibitAnyPolicy: 1, ocspNoCheck: 1, tlsFeature: 1,
|
|
103
108
|
subjectAltName: 1, issuerAltName: 1, nameConstraints: 1, cRLDistributionPoints: 1,
|
|
104
109
|
freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1, certificatePolicies: 1,
|
|
110
|
+
policyMappings: 1, policyConstraints: 1, subjectDirectoryAttributes: 1,
|
|
105
111
|
};
|
|
106
112
|
// sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
|
|
107
113
|
// outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
|
|
@@ -237,14 +243,36 @@ function _specialText(node) {
|
|
|
237
243
|
|
|
238
244
|
// A single Name (sec. 3.1.4/sec. 3.1.6): the CBOR simple null (issuer only) | a bare SpecialText single
|
|
239
245
|
// commonName | an array of RDNAttributes. Surfaces { dn, rdns, eui64? } shape-compatible with x509.
|
|
246
|
+
// An rdn's (attribute type, sign) pair DECLARES an X.509 string type, so its text must be valid for that type --
|
|
247
|
+
// a printableString sign over a non-PrintableString character, a non-ASCII emailAddress (IA5String-only), or a
|
|
248
|
+
// value outside the type's SIZE. Assert it by running the SAME builder the reconstruction uses, so a Name this
|
|
249
|
+
// parse accepts is always one it can rebuild, and a natively-signed certificate (which never reconstructs) is
|
|
250
|
+
// held to the identical rule. Applied to BOTH Name forms -- the array form and the bare single-commonName form.
|
|
251
|
+
// The builder's asn1/* fault is re-thrown in this module's domain rather than leaking onto the parse surface.
|
|
252
|
+
function _assertAttrValue(rdn) {
|
|
253
|
+
try { _reconAttrValue(rdn); }
|
|
254
|
+
catch (e) {
|
|
255
|
+
if (e instanceof C509Error) throw e;
|
|
256
|
+
throw _err("c509/bad-name", "the " + rdn.type + " value is not valid for the string type its attribute integer declares", e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
240
260
|
function _name509(node, isSubject) {
|
|
241
261
|
if (!isSubject && node.majorType === 7 && node.ai === 22) return null; // issuer == subject (self-signed)
|
|
242
262
|
// A bare SpecialText (not an array) is a single commonName attribute (attributeType == +1).
|
|
243
263
|
if (node.majorType === 3 || node.majorType === 2 || node.majorType === 6) {
|
|
244
264
|
var sv = _specialText(node);
|
|
245
|
-
|
|
265
|
+
// The bare form is a single commonName -- hold its value to the SAME rules the array form applies, so a
|
|
266
|
+
// natively-signed certificate (which never reconstructs) cannot carry a value the reconstruction would refuse.
|
|
267
|
+
if (sv.eui64) {
|
|
268
|
+
var euiRdn = { type: "commonName", eui64: sv.eui64 };
|
|
269
|
+
_assertAttrValue(euiRdn);
|
|
270
|
+
return { rdns: [euiRdn], eui64: sv.eui64, dn: "CN=" + _macToEui64String(sv.eui64) };
|
|
271
|
+
}
|
|
246
272
|
var val = sv.text !== undefined ? sv.text : sv.hex;
|
|
247
|
-
|
|
273
|
+
var bareRdn = { type: "commonName", value: val };
|
|
274
|
+
_assertAttrValue(bareRdn);
|
|
275
|
+
return { rdns: [bareRdn], dn: "CN=" + val };
|
|
248
276
|
}
|
|
249
277
|
if (node.majorType !== 4) throw _err("c509/bad-name", "a C509 Name must be null, a SpecialText, or an array of RDN attributes");
|
|
250
278
|
var rdns = [];
|
|
@@ -265,7 +293,13 @@ function _name509(node, isSubject) {
|
|
|
265
293
|
if (tname === undefined) throw _err("c509/bad-name", "attribute type integer " + ti + " has no C509 registry row");
|
|
266
294
|
var v = _specialText(kids[i + 1]);
|
|
267
295
|
var vv = v.text !== undefined ? v.text : (v.hex !== undefined ? v.hex : _macToEui64String(v.eui64));
|
|
268
|
-
|
|
296
|
+
// The RENDERED text is what carries into the rdn -- deliberately NOT the raw eui64, because the EUI shortcut
|
|
297
|
+
// in _reconAttrValue is unconditional and would take precedence over the attribute's declared string type,
|
|
298
|
+
// rebuilding a tag-48 emailAddress or countryName as a UTF8String. Only the bare-SpecialText form above is
|
|
299
|
+
// always a commonName and may carry it; here the value is held to whatever type the attribute integer declares.
|
|
300
|
+
var rdn = { type: tname, value: vv, printable: ti < 0 };
|
|
301
|
+
_assertAttrValue(rdn);
|
|
302
|
+
rdns.push(rdn);
|
|
269
303
|
parts.push(_shortName(tname) + "=" + vv);
|
|
270
304
|
}
|
|
271
305
|
return { rdns: rdns, dn: parts.join(",") };
|
|
@@ -289,6 +323,14 @@ function _cborIntVal(node, label) {
|
|
|
289
323
|
if (node.majorType !== 0 && node.majorType !== 1) throw _err("c509/bad-extensions", "a " + label + " value must be a CBOR integer");
|
|
290
324
|
return cbor.read.int(node);
|
|
291
325
|
}
|
|
326
|
+
// A non-negative reconstruct-side count (a policyConstraints / inhibitAnyPolicy SkipCerts, a basicConstraints
|
|
327
|
+
// pathLenConstraint) narrowed to [0, 2^31-1] via the SAME guard.range.uint31 the toolkit's own DER decoders
|
|
328
|
+
// (schema-pkix) bound these INTEGER (0..MAX) counts through. A native CBOR value past 2^31-1 fails closed here
|
|
329
|
+
// rather than reconstructing a DER the toolkit's own decoder would then reject -- the reconstruct path must
|
|
330
|
+
// produce nothing that decoder rejects. Returns a Number; b.integer narrows it to the minimal DER INTEGER.
|
|
331
|
+
function _boundCount(value, label) {
|
|
332
|
+
return guard.range.uint31(value, _err, "c509/bad-extensions", label + " (0..2^31-1)");
|
|
333
|
+
}
|
|
292
334
|
// A C509 KeyPurposeId (int registry alias OR unwrapped ~oid) -> the dotted extended-key-usage OID.
|
|
293
335
|
function _ekuPurposeOid(node) {
|
|
294
336
|
if (node.majorType === 0 || node.majorType === 1) {
|
|
@@ -409,8 +451,8 @@ function _otherNameToDer(node, intVal) {
|
|
|
409
451
|
if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "a generic otherName value must be a CBOR [ ~oid, bytes ] pair");
|
|
410
452
|
typeId = _oidName(node.children[0], "c509/bad-extensions", "an otherName type-id").oid;
|
|
411
453
|
if (node.children[1].majorType !== 2) throw _err("c509/bad-extensions", "an otherName value must be a CBOR byte string (the [0] EXPLICIT inner TLV)");
|
|
412
|
-
|
|
413
|
-
|
|
454
|
+
// The [0] EXPLICIT inner TLV is an ANY spliced verbatim -- strict-validate it, not just its framing.
|
|
455
|
+
inner = _requireStrictDerTlv(node.children[1].content, "c509/bad-extensions", "an otherName value");
|
|
414
456
|
} else if (intVal === -1) { // id-on-hardwareModuleName: [ ~oid(hwType), bytes(hwSerialNum) ] -> HardwareModuleName
|
|
415
457
|
if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "an id-on-hardwareModuleName value must be a CBOR [ ~oid, bytes ] pair");
|
|
416
458
|
if (node.children[1].majorType !== 2) throw _err("c509/bad-extensions", "a hardwareModuleName hwSerialNum must be a CBOR byte string");
|
|
@@ -686,7 +728,7 @@ function _extValueToDer(name, node) {
|
|
|
686
728
|
var iv = _cborIntVal(node, "basicConstraints");
|
|
687
729
|
if (iv === -2n) return b.sequence([]); // cA false (omitted)
|
|
688
730
|
if (iv === -1n) return b.sequence([b.boolean(true)]); // cA true, no pathLen
|
|
689
|
-
if (iv >= 0n) return b.sequence([b.boolean(true), b.integer(iv)]);
|
|
731
|
+
if (iv >= 0n) return b.sequence([b.boolean(true), b.integer(_boundCount(iv, "a basicConstraints pathLenConstraint"))]); // cA true, pathLen (0..2^31-1, as the DER decoder bounds it)
|
|
690
732
|
throw _err("c509/bad-extensions", "a basicConstraints int " + iv + " is outside the -2/-1/pathLen range");
|
|
691
733
|
}
|
|
692
734
|
case "authorityKeyIdentifier": // keyId-only bytes, or [ keyId, authorityCertIssuer, serial ] (sec. 3.3)
|
|
@@ -710,8 +752,8 @@ function _extValueToDer(name, node) {
|
|
|
710
752
|
}
|
|
711
753
|
return b.sequence(items.map(function (it) { return b.oid(_ekuPurposeOid(it)); }));
|
|
712
754
|
}
|
|
713
|
-
case "inhibitAnyPolicy": // uint -> INTEGER SkipCerts
|
|
714
|
-
return b.integer(_cborUint(node, "inhibitAnyPolicy"));
|
|
755
|
+
case "inhibitAnyPolicy": // uint -> INTEGER SkipCerts (0..2^31-1, as the DER decoder bounds it)
|
|
756
|
+
return b.integer(_boundCount(_cborUint(node, "inhibitAnyPolicy"), "an inhibitAnyPolicy SkipCerts"));
|
|
715
757
|
case "ocspNoCheck": // null -> NULL
|
|
716
758
|
if (node.majorType !== 7 || !(Buffer.isBuffer(node.bytes) && node.bytes.length === 1 && node.bytes[0] === 0xf6)) throw _err("c509/bad-extensions", "an ocspNoCheck value must be the CBOR simple value null");
|
|
717
759
|
return b.nullValue();
|
|
@@ -770,6 +812,29 @@ function _extValueToDer(name, node) {
|
|
|
770
812
|
}
|
|
771
813
|
return b.sequence(polInfos);
|
|
772
814
|
}
|
|
815
|
+
case "policyMappings": { // [ idp, sdp, idp, sdp, ... ] -> SEQUENCE OF SEQUENCE { OID, OID } (sec. 3.3)
|
|
816
|
+
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a policyMappings value must be a CBOR array");
|
|
817
|
+
var pmKids = node.children;
|
|
818
|
+
if (pmKids.length === 0 || pmKids.length % 2 !== 0) throw _err("c509/bad-extensions", "a policyMappings array must be non-empty (issuerDomainPolicy, subjectDomainPolicy) pairs (sec. 3.3)");
|
|
819
|
+
var maps = [];
|
|
820
|
+
// each member is a CertPolicyId -- the same sec. 8.9 int/~oid policy space as certificatePolicies (an int
|
|
821
|
+
// outside sec. 8.9 fails closed in _policyIdToDerOid). anyPolicy is accepted: RFC 5280 sec. 4.2.1.5's
|
|
822
|
+
// "MUST NOT map to/from anyPolicy" is a generation rule the toolkit's own DER decoder does not reject.
|
|
823
|
+
for (var mi = 0; mi + 1 < pmKids.length; mi += 2) maps.push(b.sequence([b.oid(_policyIdToDerOid(pmKids[mi])), b.oid(_policyIdToDerOid(pmKids[mi + 1]))]));
|
|
824
|
+
return b.sequence(maps);
|
|
825
|
+
}
|
|
826
|
+
case "policyConstraints": { // [ requireExplicitPolicy: uint/null, inhibitPolicyMapping: uint/null ] -> SEQUENCE { [0]?, [1]? }
|
|
827
|
+
if (node.majorType !== 4 || !node.children || node.children.length !== 2) throw _err("c509/bad-extensions", "a policyConstraints value must be a 2-element CBOR array [ requireExplicitPolicy, inhibitPolicyMapping ] (sec. 3.3)");
|
|
828
|
+
var pcFields = [], repN = node.children[0], ipmN = node.children[1];
|
|
829
|
+
if (!_isCborNull(repN)) pcFields.push(b.implicit(0, b.integer(_boundCount(_cborUint(repN, "a policyConstraints requireExplicitPolicy"), "a policyConstraints requireExplicitPolicy")))); // requireExplicitPolicy [0] IMPLICIT SkipCerts
|
|
830
|
+
if (!_isCborNull(ipmN)) pcFields.push(b.implicit(1, b.integer(_boundCount(_cborUint(ipmN, "a policyConstraints inhibitPolicyMapping"), "a policyConstraints inhibitPolicyMapping")))); // inhibitPolicyMapping [1] IMPLICIT SkipCerts
|
|
831
|
+
// both-null is the empty-SEQUENCE case RFC 5280 sec. 4.2.1.11 forbids ("either ... MUST be present"); the
|
|
832
|
+
// toolkit's own DER decoder rejects the empty PolicyConstraints, so the reconstruct fails closed too.
|
|
833
|
+
if (pcFields.length === 0) throw _err("c509/bad-extensions", "policyConstraints must contain requireExplicitPolicy or inhibitPolicyMapping (RFC 5280 sec. 4.2.1.11)");
|
|
834
|
+
return b.sequence(pcFields); // positional slot -> tag makes [0] < [1] unique + ascending by construction
|
|
835
|
+
}
|
|
836
|
+
case "subjectDirectoryAttributes": // [ type, values, type, values, ... ] -> SEQUENCE OF Attribute (sec. 3.3)
|
|
837
|
+
return _sdaToDer(node);
|
|
773
838
|
default:
|
|
774
839
|
throw _err("c509/bad-extensions", "extension " + name + " has no compact value decoder");
|
|
775
840
|
}
|
|
@@ -906,6 +971,34 @@ function _extValueFromDer(name, der) {
|
|
|
906
971
|
}
|
|
907
972
|
return cbor.build.array(cpOut);
|
|
908
973
|
}
|
|
974
|
+
case "policyMappings": { // SEQUENCE OF SEQUENCE { OID, OID } -> [ idp, sdp, ... ]
|
|
975
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
|
|
976
|
+
var pmOut = [];
|
|
977
|
+
for (var pm = 0; pm < node.children.length; pm++) {
|
|
978
|
+
var mp = node.children[pm];
|
|
979
|
+
if (mp.tagClass !== "universal" || mp.tagNumber !== asn1.TAGS.SEQUENCE || !mp.children || mp.children.length !== 2) return null;
|
|
980
|
+
pmOut.push(_policyIdFromDer(asn1.read.oid(mp.children[0])));
|
|
981
|
+
pmOut.push(_policyIdFromDer(asn1.read.oid(mp.children[1])));
|
|
982
|
+
}
|
|
983
|
+
return cbor.build.array(pmOut);
|
|
984
|
+
}
|
|
985
|
+
case "policyConstraints": { // SEQUENCE { [0] rep?, [1] ipm? } -> [ rep/null, ipm/null ]
|
|
986
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1 || node.children.length > 2) return null;
|
|
987
|
+
var repC = cbor.build.nullValue(), ipmC = cbor.build.nullValue(), pcLast = -1;
|
|
988
|
+
for (var pci = 0; pci < node.children.length; pci++) {
|
|
989
|
+
var f = node.children[pci];
|
|
990
|
+
if (f.tagClass !== "context" || f.tagNumber <= pcLast) return null; // DER: unique + ascending [0] < [1]
|
|
991
|
+
pcLast = f.tagNumber;
|
|
992
|
+
var sv = asn1.read.integerImplicit(f, f.tagNumber);
|
|
993
|
+
if (sv < 0n) return null; // SkipCerts (0..MAX) -- a negative is non-compact
|
|
994
|
+
if (f.tagNumber === 0) repC = cbor.build.uint(sv);
|
|
995
|
+
else if (f.tagNumber === 1) ipmC = cbor.build.uint(sv);
|
|
996
|
+
else return null;
|
|
997
|
+
}
|
|
998
|
+
return cbor.build.array([repC, ipmC]);
|
|
999
|
+
}
|
|
1000
|
+
case "subjectDirectoryAttributes": // SEQUENCE OF Attribute -> [ type, values, ... ]
|
|
1001
|
+
return _sdaFromDer(node);
|
|
909
1002
|
default:
|
|
910
1003
|
return null;
|
|
911
1004
|
}
|
|
@@ -914,6 +1007,219 @@ function _extValueFromDer(name, der) {
|
|
|
914
1007
|
}
|
|
915
1008
|
}
|
|
916
1009
|
|
|
1010
|
+
// ---- subjectDirectoryAttributes compact value codec (draft-20 sec. 3.3 / 8.6 / 8.8, RFC 5280 sec. 4.2.1.8) ----
|
|
1011
|
+
// SubjectDirectoryAttributes ::= SEQUENCE SIZE(1..MAX) OF Attribute ::= SEQUENCE { type, values SET OF value }.
|
|
1012
|
+
// The compact form is a flat CBOR array [ type1, values1, ... ] where each values slot is ITSELF a non-empty
|
|
1013
|
+
// CBOR array -- an Attribute's SET may hold more than one value, unlike a Name's single-value RDN. type is a
|
|
1014
|
+
// sec. 8.6 registry int (the sign selects the string type) with text values, or an unwrapped ~oid with raw DER
|
|
1015
|
+
// value bytes (an unregistered type stays compact via its own ~oid form, never dropping the whole extension).
|
|
1016
|
+
// The extension-level _tryCompactExtValue round-trip guard is the byte-exactness net.
|
|
1017
|
+
|
|
1018
|
+
// A ~oid-form attributeValue must be exactly ONE non-empty, well-formed DER AttributeValue TLV. An empty byte
|
|
1019
|
+
// string would build a SET OF SIZE 0 (RFC 5280 sec. 4.2.1.8 requires at least one value); a byte string carrying
|
|
1020
|
+
// more than one TLV would fan one declared value into several SET members (draft-20 sec. 3.3: one `bytes` is one
|
|
1021
|
+
// AttributeValue). There is no downstream DER decoder to catch either (SDA ships c509-only), so the reconstruct
|
|
1022
|
+
// arm self-enforces the single-TLV shape rather than splice a degenerate or invalid Attribute.
|
|
1023
|
+
// ---- the shared strict-DER gate for a raw-spliced ANY value --------------------------------------
|
|
1024
|
+
// Several reconstruct sites splice CALLER/ATTACKER-supplied bytes into the rebuilt certificate verbatim (an
|
|
1025
|
+
// AlgorithmIdentifier's `parameters`, a generic otherName's `value [0] EXPLICIT ANY`, a subjectDirectoryAttributes
|
|
1026
|
+
// `~oid`-form AttributeValue). asn1.decode validates TLV FRAMING ONLY -- it rejects truncation, trailing bytes and
|
|
1027
|
+
// non-minimal lengths, and enforces the DER primitive/constructed rules, but it does NOT check per-type CONTENT:
|
|
1028
|
+
// it frames an empty INTEGER, the reserved end-of-contents tag 0, and an out-of-alphabet string quite happily.
|
|
1029
|
+
// Splicing those produces a certificate an independent decoder refuses to load, and one this toolkit's OWN readers
|
|
1030
|
+
// reject -- so every such site routes through the one gate below instead of a bare asn1.decode.
|
|
1031
|
+
|
|
1032
|
+
// Every universal PRIMITIVE tag whose content this toolkit can strictly validate, mapped to its reader. A
|
|
1033
|
+
// universal primitive OUTSIDE this map has no strict content validator here, so it is REJECTED rather than
|
|
1034
|
+
// spliced unchecked. The documented residual: a value whose type this toolkit cannot yet strictly validate is
|
|
1035
|
+
// not compact-representable -- NumericString / VideotexString / GraphicString / GeneralString (no content reader),
|
|
1036
|
+
// and a fractional-seconds GeneralizedTime (the X.690 sec. 11.7 relaxation is deliberately scoped to the codec
|
|
1037
|
+
// and RFC 3161 timestamping, and must not creep into a third consumer). On the ENCODE path such a value simply
|
|
1038
|
+
// degrades to the byte-exact ~oid + byte-string form, losing nothing; on the DECODE path it is a fail-closed
|
|
1039
|
+
// verdict rather than a guess.
|
|
1040
|
+
var _ANY_VALUE_READERS = (function () {
|
|
1041
|
+
var m = {}, R = asn1.read, T = asn1.TAGS;
|
|
1042
|
+
m[T.BOOLEAN] = R.boolean; m[T.INTEGER] = R.integer; m[T.ENUMERATED] = R.enumerated;
|
|
1043
|
+
m[T.BIT_STRING] = R.bitString; m[T.OCTET_STRING] = R.octetString; m[T.NULL] = R.nullValue;
|
|
1044
|
+
m[T.OBJECT_IDENTIFIER] = R.oid; m[T.UTC_TIME] = R.time; m[T.GENERALIZED_TIME] = R.time;
|
|
1045
|
+
[T.UTF8_STRING, T.PRINTABLE_STRING, T.NUMERIC_STRING, T.IA5_STRING, T.TELETEX_STRING, T.VISIBLE_STRING, T.BMP_STRING, T.UNIVERSAL_STRING].forEach(function (t) { m[t] = R.string; });
|
|
1046
|
+
return m;
|
|
1047
|
+
})();
|
|
1048
|
+
|
|
1049
|
+
// A universal SET's required member order depends on a type the ANY does not carry: X.690 sec. 11.6 orders a
|
|
1050
|
+
// SET OF by the members' full encodings, while a structured SET is ordered by TAG (X.680 sec. 8.6) -- and the two
|
|
1051
|
+
// differ whenever the constructed bit does (a SEQUENCE member, tag 16, sorts BEFORE a PrintableString, tag 19, by
|
|
1052
|
+
// tag but AFTER it by octets). A structured SET cannot repeat a tag, so a repeated tag proves SET OF and the
|
|
1053
|
+
// octet rule binds; with all-distinct tags either reading is possible, so accept a value that satisfies EITHER
|
|
1054
|
+
// (rejecting only what is non-canonical under BOTH readings -- sound in both directions, never a guess).
|
|
1055
|
+
var _TAG_CLASS_RANK = { universal: 0, application: 1, context: 2, private: 3 };
|
|
1056
|
+
function _setOrderOk(kids) {
|
|
1057
|
+
var i, dup = false, seen = {};
|
|
1058
|
+
for (i = 0; i < kids.length; i++) {
|
|
1059
|
+
var key = kids[i].tagClass + ":" + kids[i].tagNumber;
|
|
1060
|
+
if (seen[key]) { dup = true; break; }
|
|
1061
|
+
seen[key] = true;
|
|
1062
|
+
}
|
|
1063
|
+
var octetAsc = true, tagAsc = true;
|
|
1064
|
+
for (i = 1; i < kids.length; i++) {
|
|
1065
|
+
if (Buffer.compare(kids[i - 1].bytes, kids[i].bytes) > 0) octetAsc = false;
|
|
1066
|
+
// Tag order ranks by CLASS first (universal < application < context < private, X.680 sec. 8.6), then by tag
|
|
1067
|
+
// number -- compare the class's NUMBER, never its name (the names do not sort in class order).
|
|
1068
|
+
var pc = _TAG_CLASS_RANK[kids[i - 1].tagClass], cc = _TAG_CLASS_RANK[kids[i].tagClass];
|
|
1069
|
+
if (pc !== cc ? pc > cc : kids[i - 1].tagNumber > kids[i].tagNumber) tagAsc = false;
|
|
1070
|
+
}
|
|
1071
|
+
return dup ? octetAsc : (octetAsc || tagAsc);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Strict-validate a decoded DER element at ANY depth, in the caller's error domain. Rejects the reserved EOC tag
|
|
1075
|
+
// 0; runs a universal primitive through its strict content reader (or rejects a type with none); recurses into a
|
|
1076
|
+
// constructed element's children; and holds a universal SET to a canonical order. Only SEQUENCE and SET are
|
|
1077
|
+
// accepted as universal CONSTRUCTED types -- an EXTERNAL / EMBEDDED PDV / CHARACTER STRING has mandatory
|
|
1078
|
+
// components this gate cannot verify (the degenerate empty form is not a valid encoding of any of them), so it is
|
|
1079
|
+
// refused for the same reason an unvalidatable primitive is. A NON-universal element (a legitimately context- or
|
|
1080
|
+
// application-tagged ANY) passes on its framing, but its constructed children are still walked.
|
|
1081
|
+
function _strictDerElement(node, code, label) {
|
|
1082
|
+
if (node.tagClass === "universal" && node.tagNumber === 0) throw _err(code, label + " must not use the reserved end-of-contents encoding (tag 0)");
|
|
1083
|
+
if (node.constructed) {
|
|
1084
|
+
if (node.tagClass === "universal" && node.tagNumber !== asn1.TAGS.SEQUENCE && node.tagNumber !== asn1.TAGS.SET) {
|
|
1085
|
+
throw _err(code, label + " of universal constructed type " + node.tagNumber + " has no strict DER structure validator here");
|
|
1086
|
+
}
|
|
1087
|
+
var kids = node.children; // asn1.decode always sets a (possibly empty) children array on a constructed node
|
|
1088
|
+
for (var i = 0; i < kids.length; i++) _strictDerElement(kids[i], code, label);
|
|
1089
|
+
if (node.tagClass === "universal" && node.tagNumber === asn1.TAGS.SET && !_setOrderOk(kids)) {
|
|
1090
|
+
throw _err(code, label + " has a SET whose members are in no canonical DER order (X.690 sec. 11.6 / X.680 sec. 8.6)");
|
|
1091
|
+
}
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (node.tagClass === "universal") {
|
|
1095
|
+
// Validate-or-reject: a universal primitive with no strict content reader is NOT accepted on framing alone
|
|
1096
|
+
// (asn1.decode frames a malformed NumericString "12 01 40" happily), so the map is exhaustive by refusal.
|
|
1097
|
+
var reader = _ANY_VALUE_READERS[node.tagNumber];
|
|
1098
|
+
if (!reader) throw _err(code, label + " of universal type " + node.tagNumber + " has no strict DER content validator here");
|
|
1099
|
+
try { reader(node); } catch (e) { throw _err(code, label + " is not a valid DER element for its type", e); }
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// Raw ANY bytes about to be spliced verbatim must be exactly ONE non-empty, well-formed AND strictly-valid DER
|
|
1103
|
+
// element: framing + no-trailing-data via asn1.decode, then content / structure / SET order via
|
|
1104
|
+
// _strictDerElement, both reported in the CALLER's error domain. Returns the bytes so a call site can wrap a
|
|
1105
|
+
// splice inline. Used by every reconstruct site that emits caller-supplied ANY bytes verbatim.
|
|
1106
|
+
function _requireStrictDerTlv(content, code, label) {
|
|
1107
|
+
if (content.length === 0) throw _err(code, label + " must be a non-empty DER element");
|
|
1108
|
+
var node;
|
|
1109
|
+
try { node = asn1.decode(content); } catch (e) { throw _err(code, label + " must be exactly one well-formed DER element (no trailing data)", e); }
|
|
1110
|
+
_strictDerElement(node, code, label);
|
|
1111
|
+
return content;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// asn1.build.set SORTS its members, so handing it a non-canonically-ordered values list would SILENTLY rewrite
|
|
1115
|
+
// the declared order -- many distinct C509 encodings would reconstruct one and the same certificate. Require the
|
|
1116
|
+
// declared order to already BE canonical, so the compact form maps one-to-one onto the DER it rebuilds.
|
|
1117
|
+
function _derSetInDeclaredOrder(vals) {
|
|
1118
|
+
for (var i = 1; i < vals.length; i++) {
|
|
1119
|
+
if (Buffer.compare(vals[i - 1], vals[i]) > 0) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attributeValue list must be in DER ascending order (X.690 sec. 11.6)");
|
|
1120
|
+
}
|
|
1121
|
+
return b.set(vals);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// [ type1, values1, ... ] -> the DER SubjectDirectoryAttributes. A malformed native value fails closed
|
|
1125
|
+
// (c509/bad-extensions); on the encode path the same throw is a round-trip mismatch -> whole-ext ~oid fallback.
|
|
1126
|
+
function _sdaToDer(node) {
|
|
1127
|
+
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes value must be a CBOR array");
|
|
1128
|
+
var kids = node.children;
|
|
1129
|
+
if (kids.length === 0 || kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes array must be non-empty (attributeType, attributeValue) pairs (sec. 3.3)");
|
|
1130
|
+
var attrs = [];
|
|
1131
|
+
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
1132
|
+
var typeNode = kids[i], valuesNode = kids[i + 1];
|
|
1133
|
+
if (valuesNode.majorType !== 4 || !valuesNode.children || valuesNode.children.length < 1) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attributeValue must be a non-empty CBOR array (SET OF, SIZE 1..MAX, RFC 5280 sec. 4.2.1.8)");
|
|
1134
|
+
var vals = [];
|
|
1135
|
+
if (typeNode.majorType === 0 || typeNode.majorType === 1) { // int form: a sec. 8.6 registry alias, text values
|
|
1136
|
+
var ti = Number(cbor.read.int(typeNode));
|
|
1137
|
+
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
1138
|
+
if (tname === undefined) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attribute type int " + ti + " has no C509 sec. 8.6 registry row");
|
|
1139
|
+
// countryName / serialNumber are PrintableString-restricted -- their int MUST carry the negative
|
|
1140
|
+
// (printableString) sign. A non-negative sign is ambiguous/nonconformant: fail closed rather than
|
|
1141
|
+
// silently coerce the value to PrintableString (the ~oid form is the escape hatch for a genuine odd value).
|
|
1142
|
+
if ((tname === "countryName" || tname === "serialNumber") && ti >= 0) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes " + tname + " must carry the negative (printableString) sign (it is PrintableString-restricted)");
|
|
1143
|
+
for (var vi = 0; vi < valuesNode.children.length; vi++) {
|
|
1144
|
+
var vn = valuesNode.children[vi];
|
|
1145
|
+
if (vn.majorType !== 3) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes int-form attribute value must be a CBOR text string (a non-string value requires the ~oid form)");
|
|
1146
|
+
// _reconAttrValue -> b.printable / b.utf8 throws only an Asn1Error for a value outside its string type's
|
|
1147
|
+
// alphabet (a printableString sign over non-PrintableString characters); remap it to this module's
|
|
1148
|
+
// domain so attacker-controlled native input fails closed as c509/bad-extensions, not a leaked asn1/*.
|
|
1149
|
+
try { vals.push(_reconAttrValue({ type: tname, value: cbor.read.textString(vn), printable: ti < 0 })); }
|
|
1150
|
+
catch (e) { throw _err("c509/bad-extensions", "a subjectDirectoryAttributes " + tname + " value is not valid for its string type", e); }
|
|
1151
|
+
}
|
|
1152
|
+
attrs.push(b.sequence([b.oid(oid.byName(tname)), _derSetInDeclaredOrder(vals)]));
|
|
1153
|
+
} else if (typeNode.majorType === 2) { // ~oid form: raw AttributeValue TLVs
|
|
1154
|
+
var dotted = _oidName(typeNode, "c509/bad-extensions", "a subjectDirectoryAttributes attribute type").oid;
|
|
1155
|
+
for (var vj = 0; vj < valuesNode.children.length; vj++) {
|
|
1156
|
+
var vb = valuesNode.children[vj];
|
|
1157
|
+
if (vb.majorType !== 2) throw _err("c509/bad-extensions", "a ~oid-form subjectDirectoryAttributes value must be a CBOR byte string (a raw DER AttributeValue)");
|
|
1158
|
+
vals.push(b.raw(_requireStrictDerTlv(vb.content, "c509/bad-extensions", "a ~oid-form subjectDirectoryAttributes AttributeValue")));
|
|
1159
|
+
}
|
|
1160
|
+
attrs.push(b.sequence([b.oid(dotted), _derSetInDeclaredOrder(vals)]));
|
|
1161
|
+
} else {
|
|
1162
|
+
throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attribute type must be a CBOR integer (sec. 8.6) or a ~oid");
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return b.sequence(attrs);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// One DER Attribute's SET members -> the int-form { sign, values:[CBOR text] }, or null when the int form is
|
|
1169
|
+
// not confidently byte-exact (an unregistered type, a non-utf8/printable value, or a SET mixing utf8String and
|
|
1170
|
+
// printableString -- one sign cannot express it). A null routes that ONE attribute to the always-byte-exact
|
|
1171
|
+
// ~oid + raw-bytes form (a still-compact sec. 3.3 SDA form); the extension round-trip guard is the ultimate net.
|
|
1172
|
+
function _sdaTryIntForm(tint, name, valueNodes) {
|
|
1173
|
+
if (tint === undefined) return null;
|
|
1174
|
+
var i;
|
|
1175
|
+
// An IA5String-only attribute (emailAddress) rides its non-negative int with IA5String values -- its type,
|
|
1176
|
+
// not a sign, fixes the string type, so it is matched separately from the utf8/printable sign convention.
|
|
1177
|
+
if (name === "emailAddress") {
|
|
1178
|
+
var mails = [];
|
|
1179
|
+
for (i = 0; i < valueNodes.length; i++) {
|
|
1180
|
+
if (valueNodes[i].tagClass !== "universal" || valueNodes[i].tagNumber !== asn1.TAGS.IA5_STRING) return null;
|
|
1181
|
+
mails.push(cbor.build.textString(asn1.read.string(valueNodes[i])));
|
|
1182
|
+
}
|
|
1183
|
+
return { sign: 1, values: mails };
|
|
1184
|
+
}
|
|
1185
|
+
var sign = 0, out = []; // sign: -1 printableString, +1 utf8String
|
|
1186
|
+
for (i = 0; i < valueNodes.length; i++) {
|
|
1187
|
+
var vn = valueNodes[i], s;
|
|
1188
|
+
if (vn.tagClass === "universal" && vn.tagNumber === asn1.TAGS.UTF8_STRING) s = 1;
|
|
1189
|
+
else if (vn.tagClass === "universal" && vn.tagNumber === asn1.TAGS.PRINTABLE_STRING) s = -1;
|
|
1190
|
+
else return null; // any other value tag -> the ~oid form preserves it exactly
|
|
1191
|
+
if (sign === 0) sign = s;
|
|
1192
|
+
else if (sign !== s) return null; // a mixed utf8/printable SET -- the headline trap
|
|
1193
|
+
out.push(cbor.build.textString(asn1.read.string(vn)));
|
|
1194
|
+
}
|
|
1195
|
+
return { sign: sign, values: out };
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// The DER SubjectDirectoryAttributes -> the flat compact CBOR array, or null (whole-ext ~oid fallback). Per
|
|
1199
|
+
// attribute: the int form when the type is sec. 8.6-registered AND every value is a single-signed utf8/
|
|
1200
|
+
// printable string, else the ~oid + raw-bytes form (always byte-exact).
|
|
1201
|
+
function _sdaFromDer(node) {
|
|
1202
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
|
|
1203
|
+
var out = [];
|
|
1204
|
+
for (var ai = 0; ai < node.children.length; ai++) {
|
|
1205
|
+
var attr = node.children[ai];
|
|
1206
|
+
if (attr.tagClass !== "universal" || attr.tagNumber !== asn1.TAGS.SEQUENCE || !attr.children || attr.children.length !== 2) return null;
|
|
1207
|
+
var setNode = attr.children[1];
|
|
1208
|
+
if (setNode.tagClass !== "universal" || setNode.tagNumber !== asn1.TAGS.SET || !setNode.children || setNode.children.length < 1) return null; // SET OF, non-empty
|
|
1209
|
+
var dotted = asn1.read.oid(attr.children[0]);
|
|
1210
|
+
var nm = oid.name(dotted), tint = nm != null ? ATTR_TO_INT[nm] : undefined;
|
|
1211
|
+
var intForm = _sdaTryIntForm(tint, nm, setNode.children);
|
|
1212
|
+
if (intForm != null) {
|
|
1213
|
+
out.push(cbor.build.int(BigInt(intForm.sign < 0 ? -tint : tint)));
|
|
1214
|
+
out.push(cbor.build.array(intForm.values));
|
|
1215
|
+
} else {
|
|
1216
|
+
out.push(_oidCbor(dotted));
|
|
1217
|
+
out.push(cbor.build.array(setNode.children.map(function (v) { return cbor.build.byteString(v.bytes); })));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
return cbor.build.array(out);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
917
1223
|
// Encode-side guard: emit the compact value only when it decodes back to the EXACT DER extnValue, so a
|
|
918
1224
|
// non-canonical or unrepresentable value can never produce a lossy compact form (it falls back to ~oid).
|
|
919
1225
|
// A value _extValueFromDer produced but _extValueToDer cannot reconstruct -- an empty GeneralNames the DER
|
|
@@ -1005,9 +1311,20 @@ function _macToEui64String(buf) {
|
|
|
1005
1311
|
// One RDN attribute value -> its DER string. The C509 sign convention: a positive attribute int ->
|
|
1006
1312
|
// utf8String, a negative -> printableString; countryName / serialNumber are PrintableString-restricted.
|
|
1007
1313
|
function _reconAttrValue(rdn) {
|
|
1008
|
-
if (rdn.eui64) return b.utf8(_macToEui64String(rdn.eui64));
|
|
1009
|
-
|
|
1010
|
-
|
|
1314
|
+
if (rdn.eui64) return b.utf8(_macToEui64String(rdn.eui64)); // the bare-form commonName MAC (a fixed 17 chars)
|
|
1315
|
+
var s = String(rdn.value);
|
|
1316
|
+
// The attribute's registered ASN.1 type carries a SIZE constraint as well as an alphabet, and a value outside
|
|
1317
|
+
// it is not a valid encoding of that type: every DirectoryString-valued attribute is SIZE (1..MAX) and
|
|
1318
|
+
// emailAddress is IA5String SIZE (1..MAX) (RFC 5280 App. A.1), so an empty value is never valid; countryName
|
|
1319
|
+
// "SHALL have length 2" (draft sec. 3.1.4, X.520 SIZE (2)). Enforced HERE, the single place the value is built
|
|
1320
|
+
// for its declared type, so the decode, reconstruct and subject-directory-attributes paths cannot disagree.
|
|
1321
|
+
if (s.length === 0) throw _err("c509/bad-name", "a " + rdn.type + " value must be non-empty (SIZE (1..MAX))");
|
|
1322
|
+
if (rdn.type === "countryName" && s.length !== 2) throw _err("c509/bad-name", "a countryName value must have length 2 (draft sec. 3.1.4)");
|
|
1323
|
+
// emailAddress is an IA5String-only attribute (draft sec. 3.1.4): its type, not the int's sign, fixes the
|
|
1324
|
+
// string type, so it reconstructs as an IA5String whichever way the non-negative int was written.
|
|
1325
|
+
if (rdn.type === "emailAddress") return b.ia5(s);
|
|
1326
|
+
if (rdn.type === "countryName" || rdn.type === "serialNumber") return b.printable(s);
|
|
1327
|
+
return rdn.printable ? b.printable(s) : b.utf8(s);
|
|
1011
1328
|
}
|
|
1012
1329
|
|
|
1013
1330
|
// A Name -> the DER RDNSequence (SEQUENCE OF SET OF SEQUENCE{ type, value }); one attribute per RDN.
|
|
@@ -1091,13 +1408,11 @@ function _reconExtensions(exts) {
|
|
|
1091
1408
|
function _reconAlgId(alg) {
|
|
1092
1409
|
var fields = [b.oid(alg.oid)];
|
|
1093
1410
|
if (alg.parameters && alg.parameters.length) {
|
|
1094
|
-
// AlgorithmIdentifier.parameters is ANY --
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
catch (e) { throw _err("c509/non-invertible", "algorithm parameters are not a single well-formed DER element", e); }
|
|
1100
|
-
fields.push(b.raw(alg.parameters));
|
|
1411
|
+
// AlgorithmIdentifier.parameters is ANY, spliced verbatim -- so it gets the SAME strict gate as every other
|
|
1412
|
+
// raw-ANY splice, not just a framing check. Framing alone admits an empty INTEGER / reserved tag 0 / a
|
|
1413
|
+
// non-minimal INTEGER, each of which reconstructs an AlgorithmIdentifier that an independent X.509 decoder
|
|
1414
|
+
// refuses to load and that this toolkit's own readers reject.
|
|
1415
|
+
fields.push(b.raw(_requireStrictDerTlv(alg.parameters, "c509/non-invertible", "algorithm parameters")));
|
|
1101
1416
|
}
|
|
1102
1417
|
return b.sequence(fields);
|
|
1103
1418
|
}
|
|
@@ -1168,6 +1483,9 @@ function parse(input) {
|
|
|
1168
1483
|
var f = root.children;
|
|
1169
1484
|
if (f.length !== 11) throw _err("c509/bad-tbs", "a C509 certificate must be an array of exactly 11 elements, got " + f.length);
|
|
1170
1485
|
|
|
1486
|
+
// The type slot must be a CBOR integer -- guard the major type here so a text/array/byte-string field 0 fails
|
|
1487
|
+
// in this module's domain rather than leaking cbor.read.int's cbor/unexpected-major fault to the caller.
|
|
1488
|
+
if (f[0].majorType !== 0 && f[0].majorType !== 1) throw _err("c509/bad-certificate-type", "c509CertificateType must be a CBOR integer");
|
|
1171
1489
|
var type = Number(cbor.read.int(f[0]));
|
|
1172
1490
|
if (type !== 2 && type !== 3) throw _err("c509/bad-certificate-type", "c509CertificateType must be 2 (native) or 3 (re-encoded), got " + type);
|
|
1173
1491
|
|
|
@@ -1434,6 +1752,11 @@ function _c509NameFromDer(nameBytes) {
|
|
|
1434
1752
|
if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
|
|
1435
1753
|
var valNode = attr.children[1];
|
|
1436
1754
|
var value = asn1.read.string(valNode);
|
|
1755
|
+
// The IA5String type belongs ONLY to an IA5-only attribute (emailAddress, draft sec. 3.1.4), whose value
|
|
1756
|
+
// reconstructs from its type rather than the int's sign. Refuse either mismatch here with a precise verdict
|
|
1757
|
+
// instead of emitting an int form whose reconstruction would differ from the source bytes.
|
|
1758
|
+
var isIa5 = valNode.tagClass === "universal" && valNode.tagNumber === asn1.TAGS.IA5_STRING;
|
|
1759
|
+
if ((attrName === "emailAddress") !== isIa5) throw _err("c509/non-invertible", "attribute " + attrName + " carries a " + (isIa5 ? "IA5String" : "non-IA5String") + " value the C509 sec. 8.6 int form cannot represent");
|
|
1437
1760
|
var eui = attrName === "commonName" ? _euiFromCn(value) : null;
|
|
1438
1761
|
if (eui) rdns.push({ type: attrName, value: value, eui64: eui }); // tag-48 MAC commonName
|
|
1439
1762
|
else rdns.push({ type: attrName, value: value, printable: valNode.tagNumber === asn1.TAGS.PRINTABLE_STRING });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"coverage": "c8 --include=lib/** --include=index.js --reporter=text-summary --reporter=lcov node test/smoke.js",
|
|
69
69
|
"check:swallows": "node scripts/check-swallow-coverage.js",
|
|
70
70
|
"coverage:gated": "npm run coverage && npm run check:swallows",
|
|
71
|
+
"coverage:unified": "node scripts/coverage-unified.js",
|
|
72
|
+
"coverage:unified:gated": "npm run coverage:unified && npm run check:swallows",
|
|
71
73
|
"prepack": "node scripts/check-pack-against-gitignore.js",
|
|
72
74
|
"check:vendor-currency": "node scripts/check-vendor-currency.js"
|
|
73
75
|
},
|
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:62c970a5-3536-480a-8269-a0c5d85b82eb",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-08T02:38:25.668Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.4.1",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.4.1",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.4.1",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.4.1",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|