@blamejs/pki 0.3.29 → 0.3.32
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 +25 -0
- package/README.md +1 -1
- package/lib/oid.js +28 -4
- package/lib/schema-c509.js +665 -33
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@ 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.3.32 — 2026-08-05
|
|
8
|
+
|
|
9
|
+
pki.schema.c509 encodes and decodes the compact general-name extension values -- a C509 certificate's subjectAltName, issuer alternative name, name constraints, CRL distribution points, authority/subject information access, and the authority key identifier issuer form now ride their specific draft-20 CBOR shape and interoperate with a conformant C509 implementation, not only this decoder.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.c509 encodes and decodes the compact value forms for the general-name-bearing extensions defined by draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3: subjectAltName and issuerAltName (a flat array of (general-name type, value) pairs, or the bare dNSName as CBOR text for a single host name), nameConstraints (the permitted and excluded GeneralSubtrees, with the RFC 9549 sec. 2.2 address-plus-prefix-length form for an iPAddress constraint), cRLDistributionPoints and freshestCRL (each distribution point's URI full name with optional reasons and CRL issuer, or a single bare URI as CBOR text), authorityInfoAccess and subjectInfoAccess (each access description as a sec. 8.11 registry integer or unwrapped OID plus its URI), and the authorityKeyIdentifier key-identifier / authority-cert-issuer / serial form. One shared GeneralNames codec drives them all, covering every general-name form the sec. 8.13 registry defines -- rfc822Name, dNSName, directoryName, URI, iPAddress, registeredID, and otherName including the hardware-module-name (RFC 4108), SMTP UTF-8 mailbox (RFC 9598), and MAC-address specials. Each form inverts to the DER extnValue byte-for-byte in both directions, so a certificate carrying these extensions is the specific compact shape a conformant C509 implementation reads rather than an opaque DER byte string. The encoder is guarded: it emits the compact form only when it decodes back to the exact DER value, so a general name outside the registry (an X.400 address, an EDI party name), a non-canonical value, or a value the compact form cannot hold falls the whole extension back to the unwrapped-OID byte-string form instead of encoding a partial or lossy value; a malformed compact value fails closed with a typed C509Error.
|
|
14
|
+
- The OID registry gains the id-on other-name type identifiers (hardware module name, SMTP UTF-8 mailbox, MAC address), the id-ad access-description methods (time stamping, CA repository, RPKI manifest / signed object / notify), and subject information access, resolvable through pki.oid.byName / pki.oid.name.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- A C509 certificate carrying a general-name-bearing extension (subjectAltName, issuerAltName, nameConstraints, cRLDistributionPoints, freshestCRL, authorityInfoAccess, subjectInfoAccess, or the authorityKeyIdentifier issuer form) 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. A native C509 that carried a subjectAltName as a raw byte string under its extension integer, which is not a value form the draft defines, is now rejected.
|
|
19
|
+
|
|
20
|
+
## v0.3.31 — 2026-08-03
|
|
21
|
+
|
|
22
|
+
pki.schema.c509 encodes and decodes the compact per-extension value forms -- a C509 certificate's keyUsage, basicConstraints, extended key usage, key identifiers, and other scalar extensions now ride in their specific draft-20 CBOR shape and interoperate with a conformant C509 implementation, not only this decoder.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- pki.schema.c509 encodes and decodes the compact per-extension value forms defined by draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3 for the common scalar extensions: subjectKeyIdentifier (the bare key id), keyUsage (a network-byte-order integer), basicConstraints (an integer -- -2 for cA=false, -1 for cA=true with no path length, N for the path length), authorityKeyIdentifier (the bare key id, keyId-only form), extendedKeyUsage (an array of registry integers or unwrapped OIDs, the array omitted for a single purpose), inhibitAnyPolicy (an integer), OCSP No Check (CBOR null), and TLS Feature (an array of integers). Each form inverts to the DER extnValue byte-for-byte in both directions, so a C509 certificate carrying these extensions is the specific compact shape a conformant C509 implementation expects (draft-20 sec. 3.7 requires the specific form where one is defined) rather than an opaque DER byte string. The encoder is guarded: it emits the compact form only when it decodes back to the exact DER value, so a non-canonical or unrepresentable extension value falls back to the unwrapped-OID byte-string form instead of encoding lossily; a malformed compact value fails closed with a typed C509Error. The extended-key-usage integer shorthands cover the full draft-20 sec. 8.12 registry -- the RFC 5280 purposes (serverAuth, clientAuth, codeSigning, emailProtection, timeStamping, ocspSigning, anyExtendedKeyUsage) plus the SSH (RFC 6187), Kerberos PKINIT (RFC 4556), CMC (RFC 6402), and Wi-SUN purposes; any purpose outside the registry encodes as an unwrapped OID.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- pki.schema.c509's RDN-attribute (sec. 8.6) and extension (sec. 8.8) integer registries now match draft-ietf-cose-cbor-encoded-cert-20: authorityKeyIdentifier is extension integer 7, extendedKeyUsage 8, inhibitAnyPolicy 30, OCSP No Check 36, and TLS Feature 38; and the RDN attributes localityName, stateOrProvinceName, and streetAddress are integers 5, 6, and 7. A certificate carrying any of these attributes or extensions now encodes to -- and decodes from -- the draft-20 integers and compact value shapes, so it interoperates with a conformant C509 implementation; re-encode any C509 produced by an earlier release, whose integers and per-extension value shapes differ.
|
|
31
|
+
|
|
7
32
|
## v0.3.29 — 2026-08-02
|
|
8
33
|
|
|
9
34
|
The pki.acme client rounds out its RFC 8555 / RFC 9773 surface -- pre-authorize an identifier with client.newAuthz, choose among alternate issuance chains in client.downloadCertificate, and schedule renewal from the CA's ARI window with client.renewalWindow.
|
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 and the
|
|
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) and 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; 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/oid.js
CHANGED
|
@@ -81,6 +81,10 @@ var FAMILIES = {
|
|
|
81
81
|
pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
|
|
82
82
|
authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10,
|
|
83
83
|
acmeIdentifier: 31,
|
|
84
|
+
// id-pe-subjectInfoAccess (RFC 5280 sec. 4.2.2.2) -- the SIA extension companion to the AIA.
|
|
85
|
+
subjectInfoAccess: 11,
|
|
86
|
+
// id-pe-tlsfeature (RFC 7633) -- the TLS Feature (formerly "must-staple") extension.
|
|
87
|
+
tlsFeature: 24,
|
|
84
88
|
// id-pe-qcStatements (RFC 3739 sec. 3.2.6) -- the qualified-certificate-statements extension.
|
|
85
89
|
qcStatements: 3 } },
|
|
86
90
|
|
|
@@ -117,8 +121,18 @@ var FAMILIES = {
|
|
|
117
121
|
|
|
118
122
|
// PKIX Access Descriptor methods (id-ad, RFC 5280 sec. 4.2.2.1/sec. 4.2.2.2). id-ad-ocsp
|
|
119
123
|
// is the arc the OCSP responder OIDs hang under; id-ad-caIssuers names the AIA
|
|
120
|
-
// CA-issuers access method.
|
|
121
|
-
|
|
124
|
+
// CA-issuers access method. The remaining id-ad methods (timeStamping RFC 3161, caRepository
|
|
125
|
+
// + rpkiManifest/signedObject/rpkiNotify RFC 6487/8182) complete the C509 Information Access
|
|
126
|
+
// registry (draft-ietf-cose-cbor-encoded-cert sec. 8.11); they carry the id-ad- prefix so the
|
|
127
|
+
// name->OID reverse stays unambiguous next to the id-kp "timeStamping" purpose (a distinct arc).
|
|
128
|
+
adAccess: { base: [1, 3, 6, 1, 5, 5, 7, 48], of: { ocsp: 1, caIssuers: 2,
|
|
129
|
+
"id-ad-timeStamping": 3, "id-ad-caRepository": 5, "id-ad-rpkiManifest": 10,
|
|
130
|
+
"id-ad-signedObject": 11, "id-ad-rpkiNotify": 13 } },
|
|
131
|
+
|
|
132
|
+
// id-on -- RFC 5280 sec. 4.2.1.6 otherName type-ids the C509 sec. 8.13 registry gives their own
|
|
133
|
+
// negative ints: id-on-hardwareModuleName (RFC 4108), id-on-SmtpUTF8Mailbox (RFC 9598),
|
|
134
|
+
// id-on-MACAddress (I-D.ietf-lamps-macaddress-on) on the id-pkix 8 arc.
|
|
135
|
+
pkixOn: { base: [1, 3, 6, 1, 5, 5, 7, 8], of: { hardwareModuleName: 4, smtpUtf8Mailbox: 9, macAddress: 12 } },
|
|
122
136
|
|
|
123
137
|
// OCSP (RFC 6960) on the id-pkix-ocsp arc (= id-ad-ocsp). id-pkix-ocsp-basic is
|
|
124
138
|
// the ResponseBytes.responseType this build decodes; id-pkix-ocsp-nonce (sec. 4.4.1)
|
|
@@ -138,9 +152,19 @@ var FAMILIES = {
|
|
|
138
152
|
regInfo: { base: [1, 3, 6, 1, 5, 5, 7, 5, 2], of: { utf8Pairs: 1, certReq: 2 } },
|
|
139
153
|
|
|
140
154
|
// PKIX extended key purposes (id-kp, RFC 5280 sec. 4.2.1.12). timeStamping is required
|
|
141
|
-
// -- critical and sole -- on an RFC 3161 TSA signing certificate (sec. 2.3).
|
|
155
|
+
// -- critical and sole -- on an RFC 3161 TSA signing certificate (sec. 2.3). The SSH
|
|
156
|
+
// (RFC 6187), CMC (RFC 6402), and Bundle Security (RFC 9174) purposes complete the C509
|
|
157
|
+
// Extended Key Usages registry (draft-ietf-cose-cbor-encoded-cert sec. 8.12).
|
|
142
158
|
pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
|
|
143
|
-
serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9
|
|
159
|
+
serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9,
|
|
160
|
+
secureShellClient: 21, secureShellServer: 22, cmcCA: 27, cmcRA: 28, cmcArchive: 29, cmKGA: 32, bundleSecurity: 35 } },
|
|
161
|
+
|
|
162
|
+
// Kerberos PKINIT extended key purposes (id-pkinit, RFC 4556 sec. 3.2.3 / 3.2.4): the client
|
|
163
|
+
// and KDC authentication EKUs (C509 Extended Key Usages registry integers 10 / 11).
|
|
164
|
+
pkinitKp: { base: [1, 3, 6, 1, 5, 2, 3], of: { pkinitClientAuth: 4, pkinitKdc: 5 } },
|
|
165
|
+
|
|
166
|
+
// Wi-SUN FAN device extended key purpose (C509 Extended Key Usages registry integer 20).
|
|
167
|
+
wisun: { base: [1, 3, 6, 1, 4, 1, 45605], of: { fanDevice: 1 } },
|
|
144
168
|
|
|
145
169
|
// Google Certificate Transparency (RFC 6962) on the 1.3.6.1.4.1.11129.2.4 arc:
|
|
146
170
|
// the SCT-list X.509 extension (sec. 3.3), the precertificate poison (sec. 3.1), the
|
package/lib/schema-c509.js
CHANGED
|
@@ -19,6 +19,8 @@ var asn1 = require("./asn1-der");
|
|
|
19
19
|
var oid = require("./oid");
|
|
20
20
|
var x509 = require("./schema-x509");
|
|
21
21
|
var pkix = require("./schema-pkix");
|
|
22
|
+
var schema = require("./schema-engine");
|
|
23
|
+
var guard = require("./guard-all");
|
|
22
24
|
var constants = require("./constants");
|
|
23
25
|
var frameworkError = require("./framework-error");
|
|
24
26
|
var validator = require("./validator-all");
|
|
@@ -55,27 +57,89 @@ var PK_ALG_BY_INT = {
|
|
|
55
57
|
// EC curve -> field size in bytes (the SEC1 coordinate width), for point-length validation.
|
|
56
58
|
var EC_FIELD_BYTES = { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 };
|
|
57
59
|
|
|
58
|
-
// sec. 8.6 attribute types (abs(int) -> name; the sign selects the X.509 string type).
|
|
60
|
+
// sec. 8.6 RDN attribute types (abs(int) -> name; the sign selects the X.509 string type). emailAddress
|
|
61
|
+
// (int 0) is an IA5String value not covered by the DirectoryString SpecialText codec below, so it is
|
|
62
|
+
// declined; a DN carrying it encodes via the ~oid attribute form, not this alias table.
|
|
59
63
|
var ATTR_BY_INT = {
|
|
60
64
|
1: _name("commonName"),
|
|
61
65
|
2: _name("surname"),
|
|
62
66
|
3: _name("serialNumber"),
|
|
63
67
|
4: _name("countryName"),
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
5: _name("localityName"),
|
|
69
|
+
6: _name("stateOrProvinceName"),
|
|
70
|
+
7: _name("streetAddress"),
|
|
66
71
|
8: _name("organizationName"),
|
|
67
72
|
9: _name("organizationalUnitName"),
|
|
68
73
|
10: _name("title"),
|
|
69
74
|
};
|
|
70
|
-
// sec. 8.8 extension types (abs(int) -> name; the sign selects criticality).
|
|
75
|
+
// sec. 8.8 extension types (abs(int) -> name; the sign selects criticality). The general-name-bearing
|
|
76
|
+
// extensions (subjectAltName / issuerAltName / nameConstraints / cRLDistributionPoints / freshestCRL /
|
|
77
|
+
// authorityInfoAccess / subjectInfoAccess) ride the shared sec. 8.13 GeneralNames value codec below; each
|
|
78
|
+
// carries a specific CBOR value form and falls back to ~oid + byte-string only when a member is outside
|
|
79
|
+
// the sec. 8.13 registry (an x400Address / ediPartyName general name, a non-URI access location, ...).
|
|
71
80
|
var EXT_BY_INT = {
|
|
72
81
|
1: _name("subjectKeyIdentifier"),
|
|
73
82
|
2: _name("keyUsage"),
|
|
74
83
|
3: _name("subjectAltName"),
|
|
75
84
|
4: _name("basicConstraints"),
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
5: _name("cRLDistributionPoints"),
|
|
86
|
+
7: _name("authorityKeyIdentifier"),
|
|
87
|
+
8: _name("extKeyUsage"),
|
|
88
|
+
9: _name("authorityInfoAccess"),
|
|
89
|
+
25: _name("issuerAltName"),
|
|
90
|
+
26: _name("nameConstraints"),
|
|
91
|
+
29: _name("freshestCRL"),
|
|
92
|
+
30: _name("inhibitAnyPolicy"),
|
|
93
|
+
31: _name("subjectInfoAccess"),
|
|
94
|
+
36: _name("ocspNoCheck"),
|
|
95
|
+
38: _name("tlsFeature"),
|
|
78
96
|
};
|
|
97
|
+
// The extensions whose compact CBOR value form (sec. 3.3) is invertible to/from the DER extnValue.
|
|
98
|
+
// A registered extension NOT in this set carries its value as the ~oid + byte-string form (sec. 3.7).
|
|
99
|
+
var EXT_COMPACT = {
|
|
100
|
+
subjectKeyIdentifier: 1, keyUsage: 1, basicConstraints: 1, authorityKeyIdentifier: 1,
|
|
101
|
+
extKeyUsage: 1, inhibitAnyPolicy: 1, ocspNoCheck: 1, tlsFeature: 1,
|
|
102
|
+
subjectAltName: 1, issuerAltName: 1, nameConstraints: 1, cRLDistributionPoints: 1,
|
|
103
|
+
freshestCRL: 1, authorityInfoAccess: 1, subjectInfoAccess: 1,
|
|
104
|
+
};
|
|
105
|
+
// sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
|
|
106
|
+
// outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
|
|
107
|
+
var EKU_BY_INT = {
|
|
108
|
+
0: _name("anyExtendedKeyUsage"), 1: _name("serverAuth"), 2: _name("clientAuth"),
|
|
109
|
+
3: _name("codeSigning"), 4: _name("emailProtection"), 8: _name("timeStamping"), 9: _name("ocspSigning"),
|
|
110
|
+
10: _name("pkinitClientAuth"), 11: _name("pkinitKdc"), 12: _name("secureShellClient"), 13: _name("secureShellServer"),
|
|
111
|
+
14: _name("bundleSecurity"), 15: _name("cmcCA"), 16: _name("cmcRA"), 17: _name("cmcArchive"), 18: _name("cmKGA"),
|
|
112
|
+
20: _name("fanDevice"),
|
|
113
|
+
};
|
|
114
|
+
var EKU_TO_INT = {}; // dotted OID -> C509 int (the encode inverse; keyed on the resolved OID)
|
|
115
|
+
Object.keys(EKU_BY_INT).forEach(function (k) { EKU_TO_INT[oid.byName(EKU_BY_INT[k])] = Number(k); });
|
|
116
|
+
|
|
117
|
+
// The shared pkix namespace + GeneralName leaves the general-name value codec composes for the DER ->
|
|
118
|
+
// CBOR direction (the CVE-2009-2408 IA5 control-byte guard, the otherName/directoryName shape, the
|
|
119
|
+
// iPAddress length and RFC 9549 subtree-base length are all enforced there -- never re-validated by hand).
|
|
120
|
+
var NS = pkix.makeNS("c509", C509Error, oid);
|
|
121
|
+
var GN_LEAF = pkix.generalName(NS, { decodeValue: true }); // SAN-form GeneralName leaf
|
|
122
|
+
var GN_LEAF_SUBTREE = pkix.generalName(NS, { decodeValue: true, subtreeBase: true }); // name-constraints subtree base
|
|
123
|
+
|
|
124
|
+
// sec. 8.13 C509 General Names registry: for the standard forms the C509 int == the RFC 5280 sec. 4.2.1.6
|
|
125
|
+
// GeneralName CHOICE context tag (1 rfc822Name, 2 dNSName, 4 directoryName, 6 URI, 7 iPAddress, 8
|
|
126
|
+
// registeredID); the _generalNameToDer / _generalNameFromDer switches carry that mapping directly.
|
|
127
|
+
// x400Address [3] and ediPartyName [5] have no registry row -- a GeneralNames carrying either is not
|
|
128
|
+
// compact-encodable (the enclosing extension falls back to ~oid + byte-string).
|
|
129
|
+
// The id-on otherName specials get their own negative C509 ints (sec. 8.13 / sec. 3.3): int -> the id-on
|
|
130
|
+
// type-id OID name; the derived inverse keys on the resolved OID so a decode selects the negative int.
|
|
131
|
+
var ON_NAME_BY_INT = { "-1": _name("hardwareModuleName"), "-2": _name("smtpUtf8Mailbox"), "-3": _name("macAddress") };
|
|
132
|
+
var ON_INT_BY_OID = {};
|
|
133
|
+
Object.keys(ON_NAME_BY_INT).forEach(function (k) { ON_INT_BY_OID[oid.byName(ON_NAME_BY_INT[k])] = Number(k); });
|
|
134
|
+
// sec. 8.11 C509 Information Access registry (int -> registered access-method OID name; the id-ad methods
|
|
135
|
+
// keep the id-ad- prefix, see oid.js). A method OID outside the table encodes as ~oid; a C509 method int
|
|
136
|
+
// outside it fails closed. IA_TO_INT is the derived encode inverse, keyed on the resolved OID.
|
|
137
|
+
var IA_BY_INT = {
|
|
138
|
+
1: _name("ocsp"), 2: _name("caIssuers"), 3: _name("id-ad-timeStamping"), 5: _name("id-ad-caRepository"),
|
|
139
|
+
10: _name("id-ad-rpkiManifest"), 11: _name("id-ad-signedObject"), 13: _name("id-ad-rpkiNotify"),
|
|
140
|
+
};
|
|
141
|
+
var IA_TO_INT = {};
|
|
142
|
+
Object.keys(IA_BY_INT).forEach(function (k) { IA_TO_INT[oid.byName(IA_BY_INT[k])] = Number(k); });
|
|
79
143
|
|
|
80
144
|
// ---- field readers (the unwrapped ~biguint / ~time / ~oid contracts; draft-20 sec. 3.1) ----
|
|
81
145
|
|
|
@@ -94,7 +158,7 @@ function _biguint(node, code, label) {
|
|
|
94
158
|
// MUST reject. Bound to the Date window; the CBOR simple null (permitted only for notAfter) -> null.
|
|
95
159
|
function _time(node, allowNull, label) {
|
|
96
160
|
if (allowNull && node.majorType === 7 && node.ai === 22) return null; // CBOR simple null (0xF6)
|
|
97
|
-
if (
|
|
161
|
+
if (node.majorType !== 0) throw _err("c509/bad-validity", label + " must be an unwrapped CBOR epoch integer (~time)");
|
|
98
162
|
var secs = node.argument;
|
|
99
163
|
if (secs > MAX_EPOCH_SECONDS) throw _err("c509/bad-validity", label + " is outside the representable Date range");
|
|
100
164
|
return new Date(C.TIME.seconds(Number(secs)));
|
|
@@ -167,6 +231,12 @@ function _name509(node, isSubject) {
|
|
|
167
231
|
// attribute type with no value -- reject rather than silently drop the trailing element.
|
|
168
232
|
if (kids.length % 2 !== 0) throw _err("c509/bad-name", "a C509 Name array must be attribute-type/value pairs (dangling attribute type)");
|
|
169
233
|
for (var i = 0; i + 1 < kids.length; i += 2) {
|
|
234
|
+
// The attributeType slot must be a CBOR integer (major type 0/1). Guard the major type BEFORE the read
|
|
235
|
+
// so a non-integer type slot fails in this module's own domain (c509/bad-name), matching the value slot,
|
|
236
|
+
// rather than leaking cbor.read.int's cbor/unexpected-major fault onto the parse surface. Reached with
|
|
237
|
+
// attacker-controlled bytes via the sec. 8.13 directoryName general name (subjectAltName, authorityKey-
|
|
238
|
+
// Identifier issuer, cRLDistributionPoints cRLIssuer, nameConstraints subtree base) and the top-level Name.
|
|
239
|
+
if (kids[i].majorType !== 0 && kids[i].majorType !== 1) throw _err("c509/bad-name", "a C509 Name attribute type must be a CBOR integer");
|
|
170
240
|
var ti = Number(cbor.read.int(kids[i]));
|
|
171
241
|
var tname = ATTR_BY_INT[Math.abs(ti)];
|
|
172
242
|
if (tname === undefined) throw _err("c509/bad-name", "attribute type integer " + ti + " has no C509 registry row");
|
|
@@ -179,6 +249,559 @@ function _name509(node, isSubject) {
|
|
|
179
249
|
}
|
|
180
250
|
function _shortName(n) { return n === "commonName" ? "CN" : n === "countryName" ? "C" : n === "organizationName" ? "O" : n === "organizationalUnitName" ? "OU" : n === "localityName" ? "L" : n === "stateOrProvinceName" ? "ST" : n; }
|
|
181
251
|
|
|
252
|
+
// ---- compact per-extension value codec (draft-20 sec. 3.3) -------------------
|
|
253
|
+
// Each registered extension in EXT_COMPACT has a specific CBOR encoding of its value that is invertible
|
|
254
|
+
// to/from the DER extnValue inner content (the bytes _reconExtensions wraps in an OCTET STRING). Native
|
|
255
|
+
// C509 MUST use the specific form (sec. 3.7); the decode side reconstructs the DER, the encode side emits
|
|
256
|
+
// the compact form only when it round-trips byte-for-byte (else the ~oid + byte-string fallback).
|
|
257
|
+
|
|
258
|
+
// A CBOR unsigned integer node -> its BigInt value (major type 0 guarantees non-negative). Arbitrary
|
|
259
|
+
// precision: a DER INTEGER holds any width, so no cap -- the round-trip guard verifies the reconstruction.
|
|
260
|
+
function _cborUint(node, label) {
|
|
261
|
+
if (node.majorType !== 0) throw _err("c509/bad-extensions", "a " + label + " value must be a CBOR unsigned integer");
|
|
262
|
+
return cbor.read.int(node);
|
|
263
|
+
}
|
|
264
|
+
// A CBOR integer node (either sign) -> its BigInt value (basicConstraints uses -2/-1/pathLen).
|
|
265
|
+
function _cborIntVal(node, label) {
|
|
266
|
+
if (node.majorType !== 0 && node.majorType !== 1) throw _err("c509/bad-extensions", "a " + label + " value must be a CBOR integer");
|
|
267
|
+
return cbor.read.int(node);
|
|
268
|
+
}
|
|
269
|
+
// A C509 KeyPurposeId (int registry alias OR unwrapped ~oid) -> the dotted extended-key-usage OID.
|
|
270
|
+
function _ekuPurposeOid(node) {
|
|
271
|
+
if (node.majorType === 0 || node.majorType === 1) {
|
|
272
|
+
var i = Number(cbor.read.int(node));
|
|
273
|
+
var nm = EKU_BY_INT[i];
|
|
274
|
+
if (nm === undefined) throw _err("c509/bad-extensions", "an extKeyUsage int " + i + " has no C509 registry row");
|
|
275
|
+
return oid.byName(nm);
|
|
276
|
+
}
|
|
277
|
+
// A ~oid KeyPurposeId routes through _oidName, which validates the BER OID content and remaps an oid/*
|
|
278
|
+
// fault to c509/bad-extensions (fault preserved as .cause) -- a non-byte-string node fails closed there too.
|
|
279
|
+
return _oidName(node, "c509/bad-extensions", "an extKeyUsage KeyPurposeId").oid;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---- the shared C509 GeneralNames value codec (draft-20 sec. 3.3 / sec. 8.13) ------------------
|
|
283
|
+
// One int <-> DER-context-tag pair (_generalNameToDer / _generalNameFromDer) drives every general-name-
|
|
284
|
+
// bearing extension (subjectAltName, nameConstraints, authorityKeyIdentifier, cRLDistributionPoints,
|
|
285
|
+
// authorityInfoAccess, ...), so a tag/int mapping bug cannot diverge across them. The DER -> CBOR side
|
|
286
|
+
// composes the shared pkix.generalName leaf; the CBOR -> DER side builds directly with asn1.build.*, the
|
|
287
|
+
// exact inverse. Every encode stays behind the extension-level byte-exact round-trip guard
|
|
288
|
+
// (_tryCompactExtValue), so a member the codec cannot invert byte-for-byte falls the WHOLE extension back
|
|
289
|
+
// to the conformant ~oid + byte-string form (sec. 3.7) -- never a partial GeneralNames.
|
|
290
|
+
|
|
291
|
+
// The unwrapped ~oid CBOR form (a bare byte string of the BER OID content octets; RFC 9090).
|
|
292
|
+
function _oidCbor(dotted) { return cbor.build.byteString(asn1.encodeOidContent(dotted)); }
|
|
293
|
+
// The CBOR simple value null (0xF6): the omitted permitted / excluded / reasons / cRLIssuer field.
|
|
294
|
+
function _isCborNull(node) { return node.majorType === 7 && node.ai === 22; }
|
|
295
|
+
|
|
296
|
+
// A CBOR text node -> the IA5String content octets of an IMPLICIT [n] GeneralName (rfc822Name/dNSName/URI).
|
|
297
|
+
// IA5String is 7-bit ASCII, so a code point > 0x7f cannot be one; a control byte (C0/DEL) is an injection
|
|
298
|
+
// vector (CVE-2009-2408) the shared name guard rejects -- matching the pkix.generalName decode side, so a
|
|
299
|
+
// value it rejects also fails the reconstruction direction (no round-trip guard runs on decode).
|
|
300
|
+
function _ia5Bytes(node, tag) {
|
|
301
|
+
if (node.majorType !== 3) throw _err("c509/bad-extensions", "a GeneralName [" + tag + "] value must be a CBOR text string (IA5String)");
|
|
302
|
+
var s = cbor.read.textString(node);
|
|
303
|
+
// An empty IA5 GeneralName is rejected by the shared pkix.generalName leaf (a non-empty rfc822Name /
|
|
304
|
+
// dNSName / URI), so reject it on the reconstruction side too rather than emit an empty [n] IA5String.
|
|
305
|
+
if (s.length === 0) throw _err("c509/bad-extensions", "a GeneralName [" + tag + "] IA5String must be non-empty");
|
|
306
|
+
for (var i = 0; i < s.length; i++) { if (s.charCodeAt(i) > 0x7f) throw _err("c509/bad-extensions", "a GeneralName [" + tag + "] IA5String has a non-ASCII code point"); }
|
|
307
|
+
var buf = Buffer.from(s, "latin1");
|
|
308
|
+
guard.name.assertPrintableIa5(buf, _err, "c509/bad-extensions", "GeneralName [" + tag + "]");
|
|
309
|
+
return buf;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// A named-bit-list value (bit i -> named bit i, MSB-first; the 9-bit keyUsage / ReasonFlags range) -> the
|
|
313
|
+
// DER BIT STRING content { unusedBits, bytes }, or null when the value is outside the 9 defined bits.
|
|
314
|
+
function _namedBitsToContent(value) {
|
|
315
|
+
if (!Number.isInteger(value) || value <= 0 || value > 0x1ff) return null;
|
|
316
|
+
var hi = 0; for (var t = value; t; t >>= 1) hi++; hi -= 1; // highest set bit index
|
|
317
|
+
var buf = Buffer.alloc((hi >> 3) + 1);
|
|
318
|
+
for (var bit = 0; bit <= hi; bit++) { if (value & (1 << bit)) buf[bit >> 3] |= 0x80 >> (bit & 7); }
|
|
319
|
+
return { unusedBits: 7 - (hi & 7), bytes: buf };
|
|
320
|
+
}
|
|
321
|
+
// The inverse: a DER BIT STRING content -> the named-bit value (bit i -> 2^i), or null when out of range.
|
|
322
|
+
function _namedBitsFromContent(bytes, unusedBits) {
|
|
323
|
+
var total = bytes.length * 8 - unusedBits, value = 0;
|
|
324
|
+
for (var bit = 0; bit < total && bit < 31; bit++) { if (bytes[bit >> 3] & (0x80 >> (bit & 7))) value |= (1 << bit); }
|
|
325
|
+
return value > 0 && value <= 0x1ff ? value : null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// One C509 (int, value) general name -> one DER GeneralName TLV. `ipMode` (name-constraints subtree base)
|
|
329
|
+
// only changes the iPAddress arm: SAN is a bare 4/16-octet address, a subtree base is the RFC 9549 form.
|
|
330
|
+
function _generalNameToDer(intVal, valueNode, ipMode) {
|
|
331
|
+
if (intVal === 1 || intVal === 2 || intVal === 6) return b.contextPrimitive(intVal, _ia5Bytes(valueNode, intVal)); // IMPLICIT IA5String
|
|
332
|
+
if (intVal === 4) return b.explicit(4, _reconName(_name509(valueNode, true))); // directoryName [4] EXPLICIT Name
|
|
333
|
+
if (intVal === 7) return b.contextPrimitive(7, ipMode ? _ncIpToDer(valueNode) : _sanIpBytes(valueNode)); // iPAddress [7] IMPLICIT OCTET STRING
|
|
334
|
+
if (intVal === 8) return b.contextPrimitive(8, asn1.encodeOidContent(_oidName(valueNode, "c509/bad-extensions", "a registeredID [8]").oid)); // registeredID [8] IMPLICIT OID
|
|
335
|
+
if (intVal === 0 || intVal === -1 || intVal === -2 || intVal === -3) return _otherNameToDer(valueNode, intVal); // otherName [0]
|
|
336
|
+
throw _err("c509/bad-extensions", "GeneralName int " + intVal + " has no C509 sec. 8.13 registry row");
|
|
337
|
+
}
|
|
338
|
+
// The DER GeneralName leaf result { tagNumber, value, bytes } -> a [intCbor, valueCbor] pair, or null when
|
|
339
|
+
// the name is not compact-representable (x400Address [3] / ediPartyName [5], a directoryName the compact
|
|
340
|
+
// Name codec cannot hold, a non-canonical name-constraints IP mask).
|
|
341
|
+
function _generalNameFromDer(gn, ipMode) {
|
|
342
|
+
var t = gn.tagNumber;
|
|
343
|
+
if (t === 1 || t === 2 || t === 6) return [cbor.build.int(BigInt(t)), cbor.build.textString(gn.value)];
|
|
344
|
+
if (t === 4) { var nm = _dirNameToCbor(gn.bytes); return nm == null ? null : [cbor.build.int(4n), nm]; }
|
|
345
|
+
if (t === 7) {
|
|
346
|
+
if (!ipMode) return [cbor.build.int(7n), cbor.build.byteString(gn.value)];
|
|
347
|
+
var ip = _ncIpFromDer(gn.value); return ip == null ? null : [cbor.build.int(7n), cbor.build.byteString(ip)];
|
|
348
|
+
}
|
|
349
|
+
if (t === 8) return [cbor.build.int(8n), _oidCbor(gn.value)];
|
|
350
|
+
if (t === 0) return _otherNameFromDer(gn.value);
|
|
351
|
+
return null; // tags 3 / 5 (x400Address / ediPartyName) -- no sec. 8.13 registry row
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// A flat CBOR array [int, value, int, value, ...] -> the list of DER GeneralName TLVs (the caller wraps
|
|
355
|
+
// them: a universal SEQUENCE for SAN/IAN, an implicit [n] for AKI issuer / DP cRLIssuer). These are always
|
|
356
|
+
// SAN-form (the RFC 9549 subtree-base iPAddress form is name-constraints-only and routes through
|
|
357
|
+
// _subtreesToDer / _subtreesFromDer, which call the singular codec with ipMode directly).
|
|
358
|
+
function _generalNamesToDer(node) {
|
|
359
|
+
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a GeneralNames value must be a CBOR array");
|
|
360
|
+
var kids = node.children;
|
|
361
|
+
if (kids.length === 0 || kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a GeneralNames array must be non-empty (int, value) pairs (sec. 3.3)");
|
|
362
|
+
var out = [];
|
|
363
|
+
for (var i = 0; i + 1 < kids.length; i += 2) out.push(_generalNameToDer(Number(_cborIntVal(kids[i], "a GeneralName type")), kids[i + 1], false));
|
|
364
|
+
return out;
|
|
365
|
+
}
|
|
366
|
+
// The inverse: a list of DER GeneralName nodes -> the flat CBOR items, or null if ANY member is not
|
|
367
|
+
// compact-representable (the whole enclosing extension then falls back -- never a partial GeneralNames).
|
|
368
|
+
function _generalNamesFromDer(gnNodes) {
|
|
369
|
+
var out = [];
|
|
370
|
+
for (var i = 0; i < gnNodes.length; i++) {
|
|
371
|
+
var pair = _generalNameFromDer(schema.walk(GN_LEAF, gnNodes[i], NS), false);
|
|
372
|
+
if (pair == null) return null;
|
|
373
|
+
out.push(pair[0]); out.push(pair[1]);
|
|
374
|
+
}
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// otherName [0] IMPLICIT SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY } -- the context tag
|
|
379
|
+
// REPLACES the SEQUENCE tag (RFC 5280 IMPLICIT tagging), so the [0] node holds type-id then value directly
|
|
380
|
+
// (no inner universal SEQUENCE). The generic form (C509 int 0) carries [ ~oid(type-id), bytes(the [0]
|
|
381
|
+
// EXPLICIT inner value's raw TLV) ]; the id-on specials (-1/-2/-3) carry the RFC 4108 / RFC 9598 /
|
|
382
|
+
// lamps-macaddress value shapes and reconstruct the full OtherName.
|
|
383
|
+
function _otherNameToDer(node, intVal) {
|
|
384
|
+
var typeId, inner;
|
|
385
|
+
if (intVal === 0) {
|
|
386
|
+
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");
|
|
387
|
+
typeId = _oidName(node.children[0], "c509/bad-extensions", "an otherName type-id").oid;
|
|
388
|
+
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)");
|
|
389
|
+
inner = node.children[1].content;
|
|
390
|
+
try { asn1.decode(inner); } catch (e) { throw _err("c509/bad-extensions", "an otherName value is not a single well-formed DER element", e); }
|
|
391
|
+
} else if (intVal === -1) { // id-on-hardwareModuleName: [ ~oid(hwType), bytes(hwSerialNum) ] -> HardwareModuleName
|
|
392
|
+
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");
|
|
393
|
+
if (node.children[1].majorType !== 2) throw _err("c509/bad-extensions", "a hardwareModuleName hwSerialNum must be a CBOR byte string");
|
|
394
|
+
typeId = oid.byName("hardwareModuleName");
|
|
395
|
+
inner = b.sequence([b.oid(_oidName(node.children[0], "c509/bad-extensions", "a hardwareModuleName hwType").oid), b.octetString(node.children[1].content)]);
|
|
396
|
+
} else if (intVal === -2) { // id-on-SmtpUTF8Mailbox: text -> UTF8String (RFC 9598 SIZE (1..MAX))
|
|
397
|
+
if (node.majorType !== 3) throw _err("c509/bad-extensions", "an id-on-SmtpUTF8Mailbox value must be a CBOR text string");
|
|
398
|
+
var smtp = cbor.read.textString(node);
|
|
399
|
+
if (smtp.length === 0) throw _err("c509/bad-extensions", "an id-on-SmtpUTF8Mailbox value must be non-empty (RFC 9598 SIZE (1..MAX))");
|
|
400
|
+
typeId = oid.byName("smtpUtf8Mailbox");
|
|
401
|
+
inner = b.utf8(smtp);
|
|
402
|
+
} else { // intVal === -3, id-on-MACAddress: bytes (6 = EUI-48, 8 = EUI-64) -> OCTET STRING
|
|
403
|
+
if (node.majorType !== 2) throw _err("c509/bad-extensions", "an id-on-MACAddress value must be a CBOR byte string");
|
|
404
|
+
if (node.content.length !== 6 && node.content.length !== 8) throw _err("c509/bad-extensions", "an id-on-MACAddress value must be 6 (EUI-48) or 8 (EUI-64) octets");
|
|
405
|
+
typeId = oid.byName("macAddress");
|
|
406
|
+
inner = b.octetString(node.content);
|
|
407
|
+
}
|
|
408
|
+
return b.contextConstructed(0, Buffer.concat([b.oid(typeId), b.explicit(0, inner)])); // [0] IMPLICIT SEQUENCE
|
|
409
|
+
}
|
|
410
|
+
// The inverse: the pkix.generalName otherName value { typeId, valueBytes } -> a [intCbor, valueCbor] pair,
|
|
411
|
+
// or null when an id-on special's inner value is not its RFC-defined shape (the extension then falls back).
|
|
412
|
+
function _otherNameFromDer(v) {
|
|
413
|
+
var onInt = ON_INT_BY_OID[v.typeId];
|
|
414
|
+
if (onInt === undefined) return [cbor.build.int(0n), cbor.build.array([_oidCbor(v.typeId), cbor.build.byteString(v.valueBytes)])];
|
|
415
|
+
var inner;
|
|
416
|
+
try { inner = asn1.decode(v.valueBytes); } catch (_e) { return null; }
|
|
417
|
+
if (onInt === -1) { // HardwareModuleName ::= SEQUENCE { hwType OID, hwSerialNum OCTET STRING } (RFC 4108)
|
|
418
|
+
if (inner.tagClass !== "universal" || inner.tagNumber !== asn1.TAGS.SEQUENCE || !inner.children || inner.children.length !== 2) return null;
|
|
419
|
+
var hwType, hwSerial;
|
|
420
|
+
try { hwType = asn1.read.oid(inner.children[0]); hwSerial = asn1.read.octetString(inner.children[1]); } catch (_e2) { return null; }
|
|
421
|
+
return [cbor.build.int(-1n), cbor.build.array([_oidCbor(hwType), cbor.build.byteString(hwSerial)])];
|
|
422
|
+
}
|
|
423
|
+
if (onInt === -2) { // SmtpUTF8Mailbox ::= UTF8String (RFC 9598)
|
|
424
|
+
if (inner.tagClass !== "universal" || inner.tagNumber !== asn1.TAGS.UTF8_STRING) return null;
|
|
425
|
+
var txt; try { txt = asn1.read.string(inner); } catch (_e3) { return null; }
|
|
426
|
+
return [cbor.build.int(-2n), cbor.build.textString(txt)];
|
|
427
|
+
}
|
|
428
|
+
if (inner.tagClass !== "universal" || inner.tagNumber !== asn1.TAGS.OCTET_STRING) return null; // onInt === -3: MACAddress ::= OCTET STRING
|
|
429
|
+
var mac; try { mac = asn1.read.octetString(inner); } catch (_e4) { return null; }
|
|
430
|
+
// nosemgrep: pki-non-constant-time-secret-compare -- mac is a PUBLIC hardware MAC address (an EUI-48/64
|
|
431
|
+
// identifier, RFC 9542 / lamps-macaddress), not a MAC / HMAC / auth tag / secret; this is a length check.
|
|
432
|
+
if (mac.length !== 6 && mac.length !== 8) return null;
|
|
433
|
+
return [cbor.build.int(-3n), cbor.build.byteString(mac)];
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// directoryName [4] EXPLICIT Name (a CHOICE, hence EXPLICIT) <-> the C509 compact Name. Reuses the file's
|
|
437
|
+
// Name codecs; a Name the compact form cannot hold (a multi-value RDN, an attribute outside sec. 8.6)
|
|
438
|
+
// returns null so the whole extension falls back to ~oid + byte-string.
|
|
439
|
+
function _dirNameToCbor(gnBytes) {
|
|
440
|
+
var node = asn1.decode(gnBytes); // the [4]-tagged EXPLICIT wrapper
|
|
441
|
+
if (!node.children || node.children.length !== 1) return null;
|
|
442
|
+
var name;
|
|
443
|
+
try { name = _c509NameFromDer(node.children[0].bytes); } catch (_e) { return null; }
|
|
444
|
+
try { return _encName(name, true); } catch (_e2) { return null; }
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// iPAddress [7] (SAN form): a bare 4-octet (IPv4) or 16-octet (IPv6) address, CBOR bytes <-> DER content.
|
|
448
|
+
function _sanIpBytes(node) {
|
|
449
|
+
if (node.majorType !== 2) throw _err("c509/bad-extensions", "an iPAddress value must be a CBOR byte string");
|
|
450
|
+
if (node.content.length !== 4 && node.content.length !== 16) throw _err("c509/bad-extensions", "an iPAddress must be 4 (IPv4) or 16 (IPv6) octets");
|
|
451
|
+
return node.content;
|
|
452
|
+
}
|
|
453
|
+
// iPAddress [7] (name-constraints subtree base): RFC 9549 sec. 2.2. CBOR is address || prefix-length (5 or
|
|
454
|
+
// 17 octets, the last octet the prefix bit count); DER is address || mask (8 or 32 octets). CBOR -> DER
|
|
455
|
+
// expands the prefix length to a contiguous mask.
|
|
456
|
+
function _ncIpToDer(node) {
|
|
457
|
+
if (node.majorType !== 2) throw _err("c509/bad-extensions", "a name-constraints iPAddress value must be a CBOR byte string");
|
|
458
|
+
var buf = node.content, addrLen;
|
|
459
|
+
if (buf.length === 5) addrLen = 4; else if (buf.length === 17) addrLen = 16;
|
|
460
|
+
else throw _err("c509/bad-extensions", "a name-constraints iPAddress must be 5 (IPv4) or 17 (IPv6) octets (address + prefix length; RFC 9549 sec. 2.2)");
|
|
461
|
+
var prefixLen = buf[addrLen], maxBits = addrLen * 8;
|
|
462
|
+
if (prefixLen > maxBits) throw _err("c509/bad-extensions", "a name-constraints iPAddress prefix length " + prefixLen + " exceeds " + maxBits);
|
|
463
|
+
var mask = Buffer.alloc(addrLen);
|
|
464
|
+
for (var bit = 0; bit < prefixLen; bit++) mask[bit >> 3] |= 0x80 >> (bit & 7);
|
|
465
|
+
return Buffer.concat([buf.subarray(0, addrLen), mask]);
|
|
466
|
+
}
|
|
467
|
+
// DER -> CBOR: address || mask (8/32) -> address || prefix-length (5/17). The mask MUST be a canonical
|
|
468
|
+
// contiguous prefix mask (N leading 1-bits then all 0-bits); a non-prefix mask (host bits set,
|
|
469
|
+
// non-contiguous) is not representable in the prefix form -> null (the extension falls back, losing no bits).
|
|
470
|
+
function _ncIpFromDer(buf) {
|
|
471
|
+
var addrLen = buf.length >> 1, mask = buf.subarray(addrLen), prefixLen = 0, seenZero = false;
|
|
472
|
+
for (var i = 0; i < mask.length; i++) {
|
|
473
|
+
for (var bit = 0; bit < 8; bit++) {
|
|
474
|
+
if (mask[i] & (0x80 >> bit)) { if (seenZero) return null; prefixLen++; }
|
|
475
|
+
else seenZero = true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return Buffer.concat([buf.subarray(0, addrLen), Buffer.from([prefixLen])]);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// GeneralSubtrees = [ + GeneralName ] (the flat int/value array) <-> the concatenated GeneralSubtree
|
|
482
|
+
// SEQUENCEs (RFC 5280 sec. 4.2.1.10: SEQUENCE { base, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }); the
|
|
483
|
+
// C509 profile omits minimum/maximum, so each GeneralSubtree is base-only.
|
|
484
|
+
function _subtreesToDer(node) {
|
|
485
|
+
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "a GeneralSubtrees value must be a CBOR array");
|
|
486
|
+
var kids = node.children;
|
|
487
|
+
if (kids.length === 0 || kids.length % 2 !== 0) throw _err("c509/bad-extensions", "a GeneralSubtrees array must be non-empty (int, value) pairs");
|
|
488
|
+
var out = [];
|
|
489
|
+
for (var i = 0; i + 1 < kids.length; i += 2) out.push(b.sequence([_generalNameToDer(Number(_cborIntVal(kids[i], "a GeneralSubtree base type")), kids[i + 1], true)]));
|
|
490
|
+
return Buffer.concat(out);
|
|
491
|
+
}
|
|
492
|
+
function _subtreesFromDer(subtreeNodes) {
|
|
493
|
+
if (!subtreeNodes.length) return null;
|
|
494
|
+
var out = [];
|
|
495
|
+
for (var i = 0; i < subtreeNodes.length; i++) {
|
|
496
|
+
var st = subtreeNodes[i];
|
|
497
|
+
if (st.tagClass !== "universal" || st.tagNumber !== asn1.TAGS.SEQUENCE || !st.children || st.children.length !== 1) return null; // base only (minimum/maximum omitted)
|
|
498
|
+
var pair = _generalNameFromDer(schema.walk(GN_LEAF_SUBTREE, st.children[0], NS), true);
|
|
499
|
+
if (pair == null) return null;
|
|
500
|
+
out.push(pair[0]); out.push(pair[1]);
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// A CBOR accessMethod (int, sec. 8.11 registry, or ~oid) -> the dotted access-method OID.
|
|
506
|
+
function _accessMethodOid(node) {
|
|
507
|
+
if (node.majorType === 0 || node.majorType === 1) {
|
|
508
|
+
var i = Number(cbor.read.int(node)), nm = IA_BY_INT[i];
|
|
509
|
+
if (nm === undefined) throw _err("c509/bad-extensions", "an accessMethod int " + i + " has no C509 sec. 8.11 registry row");
|
|
510
|
+
return oid.byName(nm);
|
|
511
|
+
}
|
|
512
|
+
return _oidName(node, "c509/bad-extensions", "an accessMethod").oid;
|
|
513
|
+
}
|
|
514
|
+
// A reasons uint (bit i -> named ReasonFlags bit i) -> the [1] IMPLICIT ReasonFlags BIT STRING TLV.
|
|
515
|
+
function _reasonsBitsToDer(value) {
|
|
516
|
+
// ReasonFlags bit 0 is reserved "unused" (RFC 5280 sec. 4.2.1.13; the reason bits are 1..8), so a value
|
|
517
|
+
// that sets it is not a valid ReasonFlags even though the shared 9-bit range check would accept it. Bit 0
|
|
518
|
+
// is a legitimate keyUsage bit (digitalSignature), so this reason-specific reject lives here, not in the
|
|
519
|
+
// shared named-bit helper -- a malformed native C509 reasons value fails closed rather than reconstructing
|
|
520
|
+
// a semantically invalid X.509 ReasonFlags.
|
|
521
|
+
if ((value & 1) !== 0) throw _err("c509/bad-extensions", "a cRLDistributionPoints reasons value must not set the reserved unused ReasonFlags bit 0 (RFC 5280 sec. 4.2.1.13)");
|
|
522
|
+
var c = _namedBitsToContent(value);
|
|
523
|
+
if (c == null) throw _err("c509/bad-extensions", "a cRLDistributionPoints reasons value must be within the 9 defined ReasonFlags bits");
|
|
524
|
+
return b.contextPrimitive(1, Buffer.concat([Buffer.from([c.unusedBits]), c.bytes]));
|
|
525
|
+
}
|
|
526
|
+
// A CBOR ~biguint serial node -> the DER INTEGER content octets (minimal two's-complement of the
|
|
527
|
+
// non-negative serial): the bare magnitude, prefixed with a 0x00 sign octet when its top bit is set.
|
|
528
|
+
function _serialIntContent(node) {
|
|
529
|
+
var mag = _minBytes(_biguint(node, "c509/bad-extensions", "an authorityKeyIdentifier serial"));
|
|
530
|
+
if (mag.length === 0) return Buffer.from([0x00]); // serial 0
|
|
531
|
+
return (mag[0] & 0x80) ? Buffer.concat([Buffer.from([0x00]), mag]) : mag;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// One CBOR DistributionPointName [ fullName, reasons, cRLIssuer ] -> one DER DistributionPoint SEQUENCE.
|
|
535
|
+
// distributionPoint [0] is EXPLICIT (it wraps the DistributionPointName CHOICE); fullName [0], reasons [1],
|
|
536
|
+
// cRLIssuer [2] are IMPLICIT (RFC 5280 sec. 4.2.1.13) -- mixing these is the classic byte-exactness trap.
|
|
537
|
+
function _dpToDer(dpNode) {
|
|
538
|
+
if (dpNode.majorType !== 4 || !dpNode.children || dpNode.children.length !== 3) throw _err("c509/bad-extensions", "a DistributionPoint must be a CBOR [ fullName, reasons, cRLIssuer ] array");
|
|
539
|
+
var fullName = dpNode.children[0], reasons = dpNode.children[1], crlIssuer = dpNode.children[2];
|
|
540
|
+
var uris;
|
|
541
|
+
if (fullName.majorType === 3) uris = [b.contextPrimitive(6, _ia5Bytes(fullName, 6))];
|
|
542
|
+
else if (fullName.majorType === 4 && fullName.children) {
|
|
543
|
+
if (fullName.children.length < 2) throw _err("c509/bad-extensions", "a DistributionPoint fullName array must hold 2 or more URIs (a single URI is a bare text; sec. 3.3)");
|
|
544
|
+
uris = fullName.children.map(function (u) { return b.contextPrimitive(6, _ia5Bytes(u, 6)); });
|
|
545
|
+
} else throw _err("c509/bad-extensions", "a DistributionPoint fullName must be a URI text or an array of URIs");
|
|
546
|
+
var fields = [b.explicit(0, b.contextConstructed(0, Buffer.concat(uris)))]; // distributionPoint [0] EXPLICIT { fullName [0] IMPLICIT GeneralNames }
|
|
547
|
+
if (!_isCborNull(reasons)) fields.push(_reasonsBitsToDer(Number(_cborUint(reasons, "cRLDistributionPoints reasons"))));
|
|
548
|
+
if (!_isCborNull(crlIssuer)) fields.push(b.contextConstructed(2, b.explicit(4, _reconName(_name509(crlIssuer, true))))); // cRLIssuer [2] { [4] directoryName }
|
|
549
|
+
return b.sequence(fields);
|
|
550
|
+
}
|
|
551
|
+
// One DER DistributionPoint SEQUENCE -> the pieces for the CBOR DistributionPointName, or null when the DP
|
|
552
|
+
// is not compact-representable (no fullName, a non-URI fullName member, a nameRelativeToCRLIssuer, or a
|
|
553
|
+
// cRLIssuer that is not a single directoryName). oneUri is set when fullName is exactly one URI, so the
|
|
554
|
+
// whole-extension bare-text shortcut can fire.
|
|
555
|
+
function _dpFromDer(dp) {
|
|
556
|
+
if (dp.tagClass !== "universal" || dp.tagNumber !== asn1.TAGS.SEQUENCE || !dp.children) return null;
|
|
557
|
+
var fullNameUris = null, reasonsVal = null, crlIssuerCbor = null, lastTag = -1;
|
|
558
|
+
for (var i = 0; i < dp.children.length; i++) {
|
|
559
|
+
var f = dp.children[i];
|
|
560
|
+
if (f.tagClass !== "context" || f.tagNumber <= lastTag) return null; // DER: unique + ascending
|
|
561
|
+
lastTag = f.tagNumber;
|
|
562
|
+
if (f.tagNumber === 0) {
|
|
563
|
+
if (!f.children || f.children.length !== 1) return null;
|
|
564
|
+
var dpn = f.children[0];
|
|
565
|
+
if (dpn.tagClass !== "context" || dpn.tagNumber !== 0 || !dpn.children || !dpn.children.length) return null; // fullName [0] only
|
|
566
|
+
fullNameUris = [];
|
|
567
|
+
for (var j = 0; j < dpn.children.length; j++) {
|
|
568
|
+
var mm = dpn.children[j];
|
|
569
|
+
if (mm.tagClass !== "context" || mm.tagNumber !== 6) return null; // non-URI fullName member
|
|
570
|
+
fullNameUris.push(schema.walk(GN_LEAF, mm, NS).value);
|
|
571
|
+
}
|
|
572
|
+
} else if (f.tagNumber === 1) {
|
|
573
|
+
var bs;
|
|
574
|
+
try { bs = asn1.read.bitStringImplicit(f, 1); } catch (_e) { return null; }
|
|
575
|
+
reasonsVal = _namedBitsFromContent(bs.bytes, bs.unusedBits);
|
|
576
|
+
if (reasonsVal == null) return null;
|
|
577
|
+
} else if (f.tagNumber === 2) {
|
|
578
|
+
if (!f.children || f.children.length !== 1) return null;
|
|
579
|
+
var ci = f.children[0];
|
|
580
|
+
if (ci.tagClass !== "context" || ci.tagNumber !== 4 || !ci.children || ci.children.length !== 1) return null; // one directoryName
|
|
581
|
+
try { crlIssuerCbor = _encName(_c509NameFromDer(ci.children[0].bytes), true); } catch (_e2) { return null; }
|
|
582
|
+
} else return null;
|
|
583
|
+
}
|
|
584
|
+
if (fullNameUris == null) return null; // C509 requires the fullName distributionPoint
|
|
585
|
+
var oneUri = fullNameUris.length === 1 ? fullNameUris[0] : null;
|
|
586
|
+
var fnCbor = oneUri != null ? cbor.build.textString(oneUri) : cbor.build.array(fullNameUris.map(function (u) { return cbor.build.textString(u); }));
|
|
587
|
+
return {
|
|
588
|
+
triple: cbor.build.array([fnCbor, reasonsVal == null ? cbor.build.nullValue() : cbor.build.uint(BigInt(reasonsVal)), crlIssuerCbor == null ? cbor.build.nullValue() : crlIssuerCbor]),
|
|
589
|
+
oneUri: oneUri, noReasons: reasonsVal == null, noIssuer: crlIssuerCbor == null,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Decode a compact extension value (a decoded CBOR node) to the DER extnValue inner content. Fails closed
|
|
594
|
+
// (c509/bad-extensions) on a CBOR shape the named extension does not define.
|
|
595
|
+
function _extValueToDer(name, node) {
|
|
596
|
+
switch (name) {
|
|
597
|
+
case "subjectKeyIdentifier": // KeyIdentifier = bytes -> OCTET STRING(keyid)
|
|
598
|
+
if (node.majorType !== 2) throw _err("c509/bad-extensions", "a subjectKeyIdentifier value must be a CBOR byte string");
|
|
599
|
+
return b.octetString(node.content);
|
|
600
|
+
case "keyUsage": // uint -> KeyUsage BIT STRING (a value past the 9 named bits fails closed)
|
|
601
|
+
return _reconKeyUsageBits(Number(_cborUint(node, "keyUsage")));
|
|
602
|
+
case "basicConstraints": { // int -> SEQUENCE { cA?, pathLen? }
|
|
603
|
+
var iv = _cborIntVal(node, "basicConstraints");
|
|
604
|
+
if (iv === -2n) return b.sequence([]); // cA false (omitted)
|
|
605
|
+
if (iv === -1n) return b.sequence([b.boolean(true)]); // cA true, no pathLen
|
|
606
|
+
if (iv >= 0n) return b.sequence([b.boolean(true), b.integer(iv)]); // cA true, pathLen
|
|
607
|
+
throw _err("c509/bad-extensions", "a basicConstraints int " + iv + " is outside the -2/-1/pathLen range");
|
|
608
|
+
}
|
|
609
|
+
case "authorityKeyIdentifier": // keyId-only bytes, or [ keyId, authorityCertIssuer, serial ] (sec. 3.3)
|
|
610
|
+
if (node.majorType === 2) return b.sequence([b.contextPrimitive(0, node.content)]);
|
|
611
|
+
if (node.majorType === 4 && node.children && node.children.length === 3) {
|
|
612
|
+
if (node.children[0].majorType !== 2) throw _err("c509/bad-extensions", "an authorityKeyIdentifier keyIdentifier must be a CBOR byte string");
|
|
613
|
+
return b.sequence([
|
|
614
|
+
b.contextPrimitive(0, node.children[0].content), // keyIdentifier [0] IMPLICIT OCTET STRING
|
|
615
|
+
b.contextConstructed(1, Buffer.concat(_generalNamesToDer(node.children[1]))), // authorityCertIssuer [1] IMPLICIT GeneralNames
|
|
616
|
+
b.contextPrimitive(2, _serialIntContent(node.children[2])), // authorityCertSerialNumber [2] IMPLICIT INTEGER
|
|
617
|
+
]);
|
|
618
|
+
}
|
|
619
|
+
throw _err("c509/bad-extensions", "an authorityKeyIdentifier value must be a keyId byte string or the [ keyId, authorityCertIssuer, serial ] array (sec. 3.3)");
|
|
620
|
+
case "extKeyUsage": { // [2* KeyPurposeId] / KeyPurposeId -> SEQUENCE OF OID (draft-20 sec. 3.3)
|
|
621
|
+
var items;
|
|
622
|
+
if (node.majorType === 4) {
|
|
623
|
+
items = node.children || [];
|
|
624
|
+
if (items.length < 2) throw _err("c509/bad-extensions", "an extKeyUsage array must hold 2 or more KeyPurposeIds; a single purpose omits the array (draft-20 sec. 3.3)");
|
|
625
|
+
} else {
|
|
626
|
+
items = [node];
|
|
627
|
+
}
|
|
628
|
+
return b.sequence(items.map(function (it) { return b.oid(_ekuPurposeOid(it)); }));
|
|
629
|
+
}
|
|
630
|
+
case "inhibitAnyPolicy": // uint -> INTEGER SkipCerts
|
|
631
|
+
return b.integer(_cborUint(node, "inhibitAnyPolicy"));
|
|
632
|
+
case "ocspNoCheck": // null -> NULL
|
|
633
|
+
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");
|
|
634
|
+
return b.nullValue();
|
|
635
|
+
case "tlsFeature": { // [uint ...] -> SEQUENCE OF INTEGER
|
|
636
|
+
if (node.majorType !== 4) throw _err("c509/bad-extensions", "a tlsFeature value must be a CBOR array");
|
|
637
|
+
return b.sequence((node.children || []).map(function (f) { return b.integer(_cborUint(f, "tlsFeature feature")); }));
|
|
638
|
+
}
|
|
639
|
+
case "subjectAltName":
|
|
640
|
+
case "issuerAltName": // SubjectAltName = GeneralNames / text (exactly one dNSName -> bare text)
|
|
641
|
+
if (node.majorType === 3) return b.sequence([b.contextPrimitive(2, _ia5Bytes(node, 2))]);
|
|
642
|
+
return b.sequence(_generalNamesToDer(node));
|
|
643
|
+
case "nameConstraints": { // [ permittedSubtrees / null, excludedSubtrees / null ]
|
|
644
|
+
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)");
|
|
645
|
+
var ncFields = [];
|
|
646
|
+
if (!_isCborNull(node.children[0])) ncFields.push(b.contextConstructed(0, _subtreesToDer(node.children[0]))); // permittedSubtrees [0]
|
|
647
|
+
if (!_isCborNull(node.children[1])) ncFields.push(b.contextConstructed(1, _subtreesToDer(node.children[1]))); // excludedSubtrees [1]
|
|
648
|
+
if (ncFields.length === 0) throw _err("c509/bad-extensions", "nameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280 sec. 4.2.1.10)");
|
|
649
|
+
return b.sequence(ncFields);
|
|
650
|
+
}
|
|
651
|
+
case "authorityInfoAccess":
|
|
652
|
+
case "subjectInfoAccess": { // [ accessMethod, uri, ... ] -> SEQUENCE OF AccessDescription { OID, [6] URI }
|
|
653
|
+
if (node.majorType !== 4 || !node.children) throw _err("c509/bad-extensions", "an " + name + " value must be a CBOR array");
|
|
654
|
+
var aiaKids = node.children;
|
|
655
|
+
if (aiaKids.length === 0 || aiaKids.length % 2 !== 0) throw _err("c509/bad-extensions", "an " + name + " array must be non-empty (accessMethod, uri) pairs (sec. 3.3)");
|
|
656
|
+
var descs = [];
|
|
657
|
+
for (var ai = 0; ai + 1 < aiaKids.length; ai += 2) descs.push(b.sequence([b.oid(_accessMethodOid(aiaKids[ai])), b.contextPrimitive(6, _ia5Bytes(aiaKids[ai + 1], 6))]));
|
|
658
|
+
return b.sequence(descs);
|
|
659
|
+
}
|
|
660
|
+
case "cRLDistributionPoints":
|
|
661
|
+
case "freshestCRL": // [ + DistributionPointName ] / text (one DP, one-URI fullName -> bare text)
|
|
662
|
+
if (node.majorType === 3) return b.sequence([b.sequence([b.explicit(0, b.contextConstructed(0, b.contextPrimitive(6, _ia5Bytes(node, 6))))])]);
|
|
663
|
+
if (node.majorType !== 4 || !node.children || node.children.length < 1) throw _err("c509/bad-extensions", "a " + name + " value must be a CBOR array of DistributionPoints or a bare URI text (sec. 3.3)");
|
|
664
|
+
return b.sequence(node.children.map(function (dp) { return _dpToDer(dp); }));
|
|
665
|
+
default:
|
|
666
|
+
throw _err("c509/bad-extensions", "extension " + name + " has no compact value decoder");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Encode a DER extnValue inner content to its compact CBOR value bytes, or null when the DER is not the
|
|
671
|
+
// canonical form the compact encoding covers (the caller then emits the ~oid + byte-string fallback).
|
|
672
|
+
function _extValueFromDer(name, der) {
|
|
673
|
+
var node;
|
|
674
|
+
// A malformed DER extnValue (an ill-formed OID/INTEGER child, a truncated SEQUENCE) is simply not
|
|
675
|
+
// compact-encodable; any decode/read fault surfaces as null so the caller emits the ~oid byte-string form.
|
|
676
|
+
// Wrong-type children throw from asn1.read.* / cbor.build.uint (a negative value) and surface as the null
|
|
677
|
+
// fallback; the encode-side round-trip guard is the final net, so only the structural dispatch is inline.
|
|
678
|
+
try {
|
|
679
|
+
node = asn1.decode(der);
|
|
680
|
+
switch (name) {
|
|
681
|
+
case "subjectKeyIdentifier": // OCTET STRING(keyid) -> the bare key id
|
|
682
|
+
return cbor.build.byteString(asn1.read.octetString(node));
|
|
683
|
+
case "keyUsage": { // BIT STRING -> uint
|
|
684
|
+
var bits = _keyUsageBitsFromDer(der);
|
|
685
|
+
return bits == null ? null : cbor.build.uint(BigInt(bits));
|
|
686
|
+
}
|
|
687
|
+
case "basicConstraints": { // SEQUENCE { cA?, pathLen? } -> int
|
|
688
|
+
var kids = node.children || [];
|
|
689
|
+
if (kids.length === 0) return cbor.build.int(-2n); // cA absent (a non-SEQUENCE falls back via the guard)
|
|
690
|
+
if (asn1.read.boolean(kids[0]) !== true) return null; // explicit cA=false is non-canonical -> fall back
|
|
691
|
+
if (kids.length === 1) return cbor.build.int(-1n);
|
|
692
|
+
if (kids.length !== 2) return null;
|
|
693
|
+
return cbor.build.uint(asn1.read.integer(kids[1])); // pathLen (>= 0; a negative INTEGER throws -> fall back)
|
|
694
|
+
}
|
|
695
|
+
case "authorityKeyIdentifier": { // SEQUENCE { [0] keyId } or the 3-tuple form
|
|
696
|
+
var akids = node.children || [];
|
|
697
|
+
if (akids.length === 1 && akids[0].tagClass === "context" && akids[0].tagNumber === 0) {
|
|
698
|
+
return cbor.build.byteString(asn1.read.octetStringImplicit(akids[0], 0)); // keyId-only
|
|
699
|
+
}
|
|
700
|
+
// the [ keyIdentifier [0], authorityCertIssuer [1], authorityCertSerialNumber [2] ] all-three form.
|
|
701
|
+
if (akids.length === 3 && akids[0].tagClass === "context" && akids[0].tagNumber === 0 &&
|
|
702
|
+
akids[1].tagClass === "context" && akids[1].tagNumber === 1 && akids[1].children &&
|
|
703
|
+
akids[2].tagClass === "context" && akids[2].tagNumber === 2) {
|
|
704
|
+
var akiIssuer = _generalNamesFromDer(akids[1].children);
|
|
705
|
+
if (akiIssuer == null) return null;
|
|
706
|
+
var akiSerial = asn1.read.integerImplicit(akids[2], 2);
|
|
707
|
+
if (akiSerial < 0n) return null; // C509 authorityCertSerialNumber is a non-negative ~biguint
|
|
708
|
+
return cbor.build.array([cbor.build.byteString(asn1.read.octetStringImplicit(akids[0], 0)), cbor.build.array(akiIssuer), cbor.build.byteString(_minBytes(akiSerial))]);
|
|
709
|
+
}
|
|
710
|
+
return null; // keyId+serial-without-issuer / issuer-only / other combinations fall back to ~oid
|
|
711
|
+
}
|
|
712
|
+
case "extKeyUsage": { // SEQUENCE OF OID -> [int/~oid ...] / single
|
|
713
|
+
var purposes = node.children || [];
|
|
714
|
+
if (!purposes.length) return null;
|
|
715
|
+
var out = purposes.map(function (p) {
|
|
716
|
+
var dotted = asn1.read.oid(p), pint = EKU_TO_INT[dotted];
|
|
717
|
+
return pint !== undefined ? cbor.build.int(BigInt(pint)) : cbor.build.byteString(asn1.encodeOidContent(dotted));
|
|
718
|
+
});
|
|
719
|
+
return out.length === 1 ? out[0] : cbor.build.array(out);
|
|
720
|
+
}
|
|
721
|
+
case "inhibitAnyPolicy": // INTEGER -> uint
|
|
722
|
+
return cbor.build.uint(asn1.read.integer(node));
|
|
723
|
+
case "ocspNoCheck": // NULL -> null
|
|
724
|
+
asn1.read.nullValue(node);
|
|
725
|
+
return cbor.build.nullValue();
|
|
726
|
+
case "tlsFeature": // SEQUENCE OF INTEGER -> [uint ...]
|
|
727
|
+
return cbor.build.array((node.children || []).map(function (f) { return cbor.build.uint(asn1.read.integer(f)); }));
|
|
728
|
+
case "subjectAltName":
|
|
729
|
+
case "issuerAltName": { // SEQUENCE OF GeneralName -> flat array / dNSName text
|
|
730
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
|
|
731
|
+
var sanMembers = node.children;
|
|
732
|
+
if (sanMembers.length === 1 && sanMembers[0].tagClass === "context" && sanMembers[0].tagNumber === 2) {
|
|
733
|
+
return cbor.build.textString(schema.walk(GN_LEAF, sanMembers[0], NS).value); // exactly one dNSName -> bare text
|
|
734
|
+
}
|
|
735
|
+
var sanItems = _generalNamesFromDer(sanMembers);
|
|
736
|
+
return sanItems == null ? null : cbor.build.array(sanItems);
|
|
737
|
+
}
|
|
738
|
+
case "nameConstraints": { // SEQUENCE { [0] permitted?, [1] excluded? } -> [perm, excl]
|
|
739
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children || node.children.length < 1) return null;
|
|
740
|
+
var permCbor = cbor.build.nullValue(), exclCbor = cbor.build.nullValue(), sawNc = false, ncLast = -1;
|
|
741
|
+
for (var nci = 0; nci < node.children.length; nci++) {
|
|
742
|
+
var ncf = node.children[nci];
|
|
743
|
+
if (ncf.tagClass !== "context" || ncf.tagNumber <= ncLast) return null; // DER: unique + ascending
|
|
744
|
+
ncLast = ncf.tagNumber;
|
|
745
|
+
var subItems = _subtreesFromDer(ncf.children || []);
|
|
746
|
+
if (subItems == null) return null;
|
|
747
|
+
if (ncf.tagNumber === 0) { permCbor = cbor.build.array(subItems); sawNc = true; }
|
|
748
|
+
else if (ncf.tagNumber === 1) { exclCbor = cbor.build.array(subItems); sawNc = true; }
|
|
749
|
+
else return null;
|
|
750
|
+
}
|
|
751
|
+
return sawNc ? cbor.build.array([permCbor, exclCbor]) : null;
|
|
752
|
+
}
|
|
753
|
+
case "authorityInfoAccess":
|
|
754
|
+
case "subjectInfoAccess": { // SEQUENCE OF AccessDescription -> [method, uri, ...]
|
|
755
|
+
var aiaDescs = node.children || [];
|
|
756
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || aiaDescs.length < 1) return null;
|
|
757
|
+
var aiaOut = [];
|
|
758
|
+
for (var adi = 0; adi < aiaDescs.length; adi++) {
|
|
759
|
+
var d = aiaDescs[adi];
|
|
760
|
+
if (d.tagClass !== "universal" || d.tagNumber !== asn1.TAGS.SEQUENCE || !d.children || d.children.length !== 2) return null;
|
|
761
|
+
var aiaLoc = d.children[1];
|
|
762
|
+
if (aiaLoc.tagClass !== "context" || aiaLoc.tagNumber !== 6) return null; // the compact form is URI-only
|
|
763
|
+
var methodDotted = asn1.read.oid(d.children[0]), mint = IA_TO_INT[methodDotted];
|
|
764
|
+
aiaOut.push(mint !== undefined ? cbor.build.int(BigInt(mint)) : _oidCbor(methodDotted));
|
|
765
|
+
aiaOut.push(cbor.build.textString(schema.walk(GN_LEAF, aiaLoc, NS).value));
|
|
766
|
+
}
|
|
767
|
+
return cbor.build.array(aiaOut);
|
|
768
|
+
}
|
|
769
|
+
case "cRLDistributionPoints":
|
|
770
|
+
case "freshestCRL": { // SEQUENCE OF DistributionPoint -> array / bare URI text
|
|
771
|
+
var dps = node.children || [];
|
|
772
|
+
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || dps.length < 1) return null;
|
|
773
|
+
var dpResults = [];
|
|
774
|
+
for (var dpi = 0; dpi < dps.length; dpi++) { var dpr = _dpFromDer(dps[dpi]); if (dpr == null) return null; dpResults.push(dpr); }
|
|
775
|
+
if (dpResults.length === 1 && dpResults[0].oneUri != null && dpResults[0].noReasons && dpResults[0].noIssuer) return cbor.build.textString(dpResults[0].oneUri);
|
|
776
|
+
return cbor.build.array(dpResults.map(function (r) { return r.triple; }));
|
|
777
|
+
}
|
|
778
|
+
default:
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
} catch (_e) {
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Encode-side guard: emit the compact value only when it decodes back to the EXACT DER extnValue, so a
|
|
787
|
+
// non-canonical or unrepresentable value can never produce a lossy compact form (it falls back to ~oid).
|
|
788
|
+
// A value _extValueFromDer produced but _extValueToDer cannot reconstruct -- an empty GeneralNames the DER
|
|
789
|
+
// builder rejects (an AKI authorityCertIssuer [1] with no members), or any future codec asymmetry -- is
|
|
790
|
+
// NOT compact-representable: the round-trip reconstruction throwing is treated identically to a byte
|
|
791
|
+
// mismatch, so encode() falls back to the ~oid form rather than raising on a certificate x509.parse
|
|
792
|
+
// accepts. The final encode() self-verify still gates byte-exactness end to end.
|
|
793
|
+
function _tryCompactExtValue(name, der) {
|
|
794
|
+
var compact = _extValueFromDer(name, der);
|
|
795
|
+
if (compact == null) return null;
|
|
796
|
+
var recon;
|
|
797
|
+
try {
|
|
798
|
+
recon = _extValueToDer(name, cbor.decode(compact));
|
|
799
|
+
} catch (_e) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
return recon.equals(der) ? compact : null;
|
|
803
|
+
}
|
|
804
|
+
|
|
182
805
|
// extensions (sec. 3.1.10/sec. 3.3/sec. 8.8): [ * Extension ] | a single keyUsage int-shortcut.
|
|
183
806
|
function _extensions(node) {
|
|
184
807
|
// The keyUsage int-shortcut (sec. 3.1.10): a bare int -> one keyUsage extension, criticality from the
|
|
@@ -201,9 +824,18 @@ function _extensions(node) {
|
|
|
201
824
|
name = EXT_BY_INT[Math.abs(ei)];
|
|
202
825
|
if (name === undefined) throw _err("c509/bad-extensions", "extension type integer " + ei + " has no C509 registry row");
|
|
203
826
|
extOid = oid.byName(name); critical = ei < 0;
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
827
|
+
// A registered extension with a compact value form (sec. 3.3) decodes its specific CBOR value to the
|
|
828
|
+
// DER extnValue inner content. A registered extension WITHOUT one (subjectAltName, until the general-
|
|
829
|
+
// name value codec lands) can only reconstruct a byte-string value (the raw DER extnValue); a
|
|
830
|
+
// non-byte-string value there is an unsupported compact form and MUST fail closed -- a text/array value
|
|
831
|
+
// is NOT raw DER, and copying its bytes would reconstruct a structurally invalid extension.
|
|
832
|
+
if (EXT_COMPACT[name]) {
|
|
833
|
+
valContent = _extValueToDer(name, valNode);
|
|
834
|
+
} else if (valNode.majorType === 2) {
|
|
835
|
+
valContent = valNode.content;
|
|
836
|
+
} else {
|
|
837
|
+
throw _err("c509/bad-extensions", "extension " + name + " has no compact value codec; its int-form value must be a byte string (draft-20 sec. 3.3)");
|
|
838
|
+
}
|
|
207
839
|
} else {
|
|
208
840
|
var r = _oidName(idNode, "c509/bad-extensions", "an extension id");
|
|
209
841
|
name = r.name; extOid = r.oid;
|
|
@@ -297,18 +929,13 @@ function _reconSpki(spkAlg, keyBytes, rsaKey) {
|
|
|
297
929
|
}
|
|
298
930
|
|
|
299
931
|
// The keyUsage int value -> the DER KeyUsage BIT STRING (RFC 5280 sec. 4.2.1.3): bit i of the value ->
|
|
300
|
-
// BIT STRING named bit i (bit 0 = digitalSignature = the MSB of the first content octet).
|
|
932
|
+
// BIT STRING named bit i (bit 0 = digitalSignature = the MSB of the first content octet). Composes the
|
|
933
|
+
// shared named-bit content builder (also used by the cRLDistributionPoints reasons transform); a
|
|
934
|
+
// non-positive, non-integer, or > 0x1FF value is not a valid KeyUsage and fails closed.
|
|
301
935
|
function _reconKeyUsageBits(value) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (!Number.isInteger(value) || value <= 0 || value > 0x1ff) throw _err("c509/non-invertible", "a keyUsage value must be a positive integer within the 9 defined bits");
|
|
306
|
-
var hi = 0; for (var t = value; t; t >>= 1) hi++; // number of significant bits
|
|
307
|
-
hi -= 1; // highest set bit index
|
|
308
|
-
var nbytes = (hi >> 3) + 1;
|
|
309
|
-
var buf = Buffer.alloc(nbytes);
|
|
310
|
-
for (var bit = 0; bit <= hi; bit++) { if (value & (1 << bit)) buf[bit >> 3] |= 0x80 >> (bit & 7); }
|
|
311
|
-
return b.bitString(buf, 7 - (hi & 7));
|
|
936
|
+
var c = _namedBitsToContent(value);
|
|
937
|
+
if (c == null) throw _err("c509/non-invertible", "a keyUsage value must be a positive integer within the 9 defined bits");
|
|
938
|
+
return b.bitString(c.bytes, c.unusedBits);
|
|
312
939
|
}
|
|
313
940
|
|
|
314
941
|
// extensions -> the [3] EXPLICIT SEQUENCE OF Extension DER.
|
|
@@ -486,12 +1113,16 @@ function matches(node) {
|
|
|
486
1113
|
// FLAGSHIP type-3 forward transform (parse(encode(der)).reconstructedDer == der, so the original signature
|
|
487
1114
|
// verifies); a c509.parse result -> re-emit its native array. Signing-free (mirrors ct.encodeSctList).
|
|
488
1115
|
|
|
489
|
-
// The registry INVERSE tables
|
|
490
|
-
//
|
|
1116
|
+
// The registry INVERSE tables (name -> canonical int). The RDN-attribute and extension inverses are
|
|
1117
|
+
// DERIVED from the *_BY_INT decode tables so the two directions cannot drift apart (a hand-kept inverse
|
|
1118
|
+
// once carried a stale pre-draft-20 numbering); the extension inverse keeps only the compact-encodable
|
|
1119
|
+
// rows, so a registered extension without a value codec (subjectAltName) routes to the ~oid form.
|
|
491
1120
|
var SIG_ALG_TO_INT = { ecdsaWithSHA256: 0, ecdsaWithSHA384: 1, ecdsaWithSHA512: 2 };
|
|
492
1121
|
var PK_ALG_TO_INT = { rsaEncryption: 0, "ecPublicKey|prime256v1": 1, "ecPublicKey|secp384r1": 2, "ecPublicKey|secp521r1": 3 };
|
|
493
|
-
var ATTR_TO_INT = {
|
|
494
|
-
|
|
1122
|
+
var ATTR_TO_INT = {};
|
|
1123
|
+
Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_TO_INT[ATTR_BY_INT[k]] = Number(k); });
|
|
1124
|
+
var EXT_TO_INT = {};
|
|
1125
|
+
Object.keys(EXT_BY_INT).forEach(function (k) { if (EXT_COMPACT[EXT_BY_INT[k]]) EXT_TO_INT[EXT_BY_INT[k]] = Number(k); });
|
|
495
1126
|
|
|
496
1127
|
// A non-negative BigInt -> its minimal big-endian ~biguint bytes (the leading 0x00 sign octet omitted).
|
|
497
1128
|
function _minBytes(n) {
|
|
@@ -548,13 +1179,15 @@ function _encExtensions(exts) {
|
|
|
548
1179
|
var items = [];
|
|
549
1180
|
exts.forEach(function (ext) {
|
|
550
1181
|
var ei = EXT_TO_INT[ext.name];
|
|
551
|
-
|
|
1182
|
+
// A registered extension with a compact value form emits int extID + the specific CBOR value, but
|
|
1183
|
+
// only when that value inverts to the EXACT DER extnValue (the round-trip guard); otherwise it falls
|
|
1184
|
+
// through to the conformant ~oid + byte-string form (sec. 3.7) -- never a lossy compact encoding.
|
|
1185
|
+
var compact = (ei !== undefined && Buffer.isBuffer(ext.value)) ? _tryCompactExtValue(ext.name, ext.value) : null;
|
|
1186
|
+
if (compact != null) {
|
|
552
1187
|
items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
|
|
553
|
-
|
|
554
|
-
if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + ext.name + " has no byte-string value to encode");
|
|
555
|
-
items.push(cbor.build.byteString(ext.value));
|
|
1188
|
+
items.push(compact);
|
|
556
1189
|
} else {
|
|
557
|
-
items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid))); // ~oid extension id
|
|
1190
|
+
items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid || oid.byName(ext.name)))); // ~oid extension id
|
|
558
1191
|
if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
|
|
559
1192
|
var bs = cbor.build.byteString(ext.value);
|
|
560
1193
|
items.push(ext.critical ? cbor.build.array([bs]) : bs); // critical ~oid value wraps in a 1-element array
|
|
@@ -677,6 +1310,7 @@ function _c509NameFromDer(nameBytes) {
|
|
|
677
1310
|
return { rdns: rdns };
|
|
678
1311
|
}
|
|
679
1312
|
// A keyUsage extnValue (a DER KeyUsage BIT STRING) -> the C509 integer whose bit i is the named bit i.
|
|
1313
|
+
// Composes the shared named-bit reader (also used by the cRLDistributionPoints reasons transform).
|
|
680
1314
|
function _keyUsageBitsFromDer(extnValue) {
|
|
681
1315
|
var bs;
|
|
682
1316
|
try {
|
|
@@ -684,9 +1318,7 @@ function _keyUsageBitsFromDer(extnValue) {
|
|
|
684
1318
|
} catch (_e) {
|
|
685
1319
|
return null; // a malformed keyUsage BIT STRING cannot take the int shortcut -> encode it as a raw extension
|
|
686
1320
|
}
|
|
687
|
-
|
|
688
|
-
for (var bit = 0; bit < total && bit < 31; bit++) { if (bs.bytes[bit >> 3] & (0x80 >> (bit & 7))) value |= (1 << bit); }
|
|
689
|
-
return value > 0 && value <= 0x1ff ? value : null;
|
|
1321
|
+
return _namedBitsFromContent(bs.bytes, bs.unusedBits);
|
|
690
1322
|
}
|
|
691
1323
|
var _NO_EXPIRY = Date.UTC(9999, 11, 31, 23, 59, 59);
|
|
692
1324
|
// The C509 (OID/node) curve names <-> the WebCrypto namedCurve the P1363 converter expects, and the
|
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:0d0e1498-d791-4209-890f-c1d837673a21",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-05T23:30:49.498Z",
|
|
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.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.32",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.32",
|
|
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.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.32",
|
|
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.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.32",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|