@blamejs/pki 0.2.31 → 0.2.33
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 +20 -0
- package/README.md +2 -2
- package/lib/inspect.js +15 -0
- package/lib/lint.js +27 -18
- package/lib/oid.js +7 -0
- package/lib/path-validate.js +15 -0
- package/lib/schema-attrcert.js +251 -2
- package/lib/schema-pkix.js +75 -13
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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.2.33 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
Attribute certificates now decode their RFC 5755 attribute values and attribute-certificate extensions, not just the certificate structure.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.attrcert.parse decodes the RFC 5755 section 4.4 attribute values -- role (RoleSyntax), clearance (Clearance, including the classification bit list and security categories), authenticationInfo and accessIdentity (SvceAuthInfo), and group and chargingIdentity (IetfAttrSyntax) -- and the section 4.3 attribute-certificate extensions -- auditIdentity, targetInformation and proxyInfo (Targets, with targetCert reusing the IssuerSerial decoder), noRevAvail, and aaControls. Each is surfaced additively (a decoded field alongside the raw value) through the parse consumer path; an unrecognized attribute type or extension id is preserved opaque, and a malformed recognized value fails closed with a typed error. Every GeneralName inside these structures is validated through the shared name decoder.
|
|
14
|
+
|
|
15
|
+
## v0.2.32 — 2026-07-16
|
|
16
|
+
|
|
17
|
+
X.509 certificates now decode the Microsoft Active Directory Certificate Services enrollment extensions, and pki.lint's critical-extension check is aligned with certification-path validation.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- pki.schema.x509.parse decodes the Microsoft Active Directory Certificate Services enrollment extensions ([MS-WCCE] / [MS-CRTD]): the v2 certificate template (szOID-CERTIFICATE_TEMPLATE -- template OID plus major/minor version), the legacy v1 template name (szOID-ENROLL_CERTTYPE_EXTENSION), the CA version (szOID-CERTSRV_CA_VERSION, surfaced as the raw value and as the CA key index / certificate index split), the previous-CA-certificate hash (szOID-CERTSRV_PREVIOUS_CERT_HASH), and the application policies (szOID-APPLICATION_CERT_POLICIES, decoded as RFC 5280 certificate policies). Each is rendered by pki.inspect and fails closed with a typed error on a malformed shape. The version fields accept the full 32-bit range Active Directory uses, not a signed subset.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- pki.lint's unrecognized-critical-extension check now mirrors certification-path validation: it reports a critical extension whose semantics the path validator does not process -- an authority/subject key identifier, a freshest-CRL pointer, an SCT list, a qualified-certificate statement, or a Microsoft enterprise-CA extension -- rather than treating any extension it can merely decode as processed. precertificatePoison, which RFC 6962 requires to be critical, is not reported. A conforming relying party must reject a critical extension it cannot process, so a certificate whose critical enterprise or qualified-certificate constraints this toolkit does not enforce is now surfaced by the linter.
|
|
26
|
+
|
|
7
27
|
## v0.2.31 — 2026-07-16
|
|
8
28
|
|
|
9
29
|
X.509 certificates now decode the RFC 3739 / ETSI EN 319 412-5 qualified-certificate qcStatements extension, and the sigstore verifier gains a low-order EdDSA public-key gate.
|
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ is callable today; nothing below is a stub.
|
|
|
203
203
|
| `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
|
|
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
|
-
| `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), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
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
207
|
| `pki.schema.c509` | Parse 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) |
|
|
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` |
|
|
@@ -211,7 +211,7 @@ is callable today; nothing below is a stub.
|
|
|
211
211
|
| `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 / 5083 / 9629 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface + raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation); §11 attribute placement enforced, countersignatures validated recursively, certificates / CRLs validated against the closed CHOICE sets and kept raw, every result tagged `contentTypeName`, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
212
212
|
| `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
|
|
213
213
|
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp requests, responses, and tokens — the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseRequest`, `parseResponse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
|
|
214
|
-
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group /
|
|
214
|
+
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes and extensions **decoded to structured values** (role, clearance, service/access identity, group, charging identity; audit identity, target/proxy information, no-rev-avail, AA controls — unknown types preserved opaque), with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
215
215
|
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
216
216
|
| `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
217
217
|
| `pki.schema.cmp` | Parse DER / PEM RFC 9810 Certificate Management Protocol messages (PKIMessage) — the header (version, sender / recipient incl. the anonymous NULL-DN, nonces, transaction id, general info), the 27-arm body (certificate requests via the CRMF parser, an encrypted certificate's EnvelopedData via CMS, response / revocation / confirmation / error / support / polling arms structural, the rest raw), and the exact `headerBytes` / `bodyBytes` slices an external verifier reconstructs the protected part from; the CMP-before-OCSP dispatch order is enforced, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
package/lib/inspect.js
CHANGED
|
@@ -335,6 +335,19 @@ var EXT_RENDERERS = {
|
|
|
335
335
|
return label;
|
|
336
336
|
}).join("\n");
|
|
337
337
|
},
|
|
338
|
+
msCertificateTemplate: function (decoded, inner) {
|
|
339
|
+
var v = decoded.templateMajorVersion === null ? "" : " v" + decoded.templateMajorVersion + "." + (decoded.templateMinorVersion === null ? 0 : decoded.templateMinorVersion);
|
|
340
|
+
return inner + "Template: " + (decoded.name || decoded.templateID) + v;
|
|
341
|
+
},
|
|
342
|
+
msEnrollCertType: function (decoded, inner) {
|
|
343
|
+
return inner + "Cert Type: " + _clean(String(decoded));
|
|
344
|
+
},
|
|
345
|
+
msCaVersion: function (decoded, inner) {
|
|
346
|
+
return inner + "CA Version: V" + decoded.caKeyIndex + "." + decoded.certIndex;
|
|
347
|
+
},
|
|
348
|
+
msPreviousCertHash: function (decoded, inner) {
|
|
349
|
+
return inner + _hexColon(Buffer.isBuffer(decoded) ? decoded : Buffer.alloc(0), { upper: true });
|
|
350
|
+
},
|
|
338
351
|
subjectAltName: _renderAltName,
|
|
339
352
|
issuerAltName: _renderAltName,
|
|
340
353
|
certificatePolicies: function (decoded, inner) {
|
|
@@ -422,6 +435,8 @@ var EXT_RENDERERS = {
|
|
|
422
435
|
return akiLines.length ? akiLines.join("\n") : inner + "keyid:(none)";
|
|
423
436
|
},
|
|
424
437
|
};
|
|
438
|
+
// [MS-WCCE] szOID-APPLICATION_CERT_POLICIES decodes as certificatePolicies, so it renders identically.
|
|
439
|
+
EXT_RENDERERS.msApplicationPolicies = EXT_RENDERERS.certificatePolicies;
|
|
425
440
|
|
|
426
441
|
// Coverage residual -- the EXT_RENDERERS entry fallbacks are unreachable because each shared
|
|
427
442
|
// decoder (and asn1.read.oid) already narrows the shape before the renderer runs:
|
package/lib/lint.js
CHANGED
|
@@ -29,6 +29,7 @@ var guard = require("./guard-all");
|
|
|
29
29
|
var oid = require("./oid");
|
|
30
30
|
var x509 = require("./schema-x509");
|
|
31
31
|
var pkix = require("./schema-pkix");
|
|
32
|
+
var path = require("./path-validate");
|
|
32
33
|
var C = require("./constants");
|
|
33
34
|
var ipUtils = require("./ip-utils");
|
|
34
35
|
|
|
@@ -237,24 +238,32 @@ function _subjectCNs(cert) {
|
|
|
237
238
|
return out;
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
// An extension is
|
|
241
|
-
// it
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
241
|
+
// An extension's CRITICALITY is honoured (RFC 5280 4.2: a consumer MUST reject a critical extension
|
|
242
|
+
// it cannot PROCESS) only when this toolkit actually processes its semantics. That authority is
|
|
243
|
+
// path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set), NOT the decoder
|
|
244
|
+
// table: certExtensionDecoders also DECODES many extensions purely for display (qcStatements, the MS
|
|
245
|
+
// enterprise-CA extensions, the SCT list, the *KeyIdentifiers, freshestCRL, issuerAltName) whose
|
|
246
|
+
// critical semantics are not enforced -- path-validate rejects a critical instance of any of those
|
|
247
|
+
// as unrecognized-critical, so the linter flags them too (structural decodability is not validation
|
|
248
|
+
// processing). The one decode-only extension that is LEGITIMATELY critical is precertificatePoison
|
|
249
|
+
// (RFC 6962 sec. 3.1 REQUIRES it critical), so it is allow-listed. Everything else -- a processed
|
|
250
|
+
// extension, or an unregistered OID (algorithm / EKU-purpose OID the name registry resolves but that
|
|
251
|
+
// is no extension) -- falls out correctly: processed -> recognized, unregistered -> unknown. Driving
|
|
252
|
+
// this off the validator's set (not the decoder table) means a newly-decoded extension is
|
|
253
|
+
// flagged-when-critical automatically until its semantics are actually processed.
|
|
254
|
+
var _CRITICAL_LEGIT = {};
|
|
255
|
+
_CRITICAL_LEGIT[oid.byName("precertificatePoison")] = true;
|
|
256
|
+
// Mirrors path-validate's unrecognizedCriticalExtension. An extension not in PROCESSED_EXTENSIONS is
|
|
257
|
+
// unknown everywhere (precertificatePoison is the one legitimately-critical exception). An extension
|
|
258
|
+
// that IS processed for an intermediate but is unprocessed on the TARGET (policyMappings) is also
|
|
259
|
+
// flagged: path-validate decides target status purely by path position (i === n), so ANY certificate
|
|
260
|
+
// -- a CA included -- can be validated as the final/target certificate, where a critical instance is
|
|
261
|
+
// rejected. A linter has no path context and cannot rule that out, so it flags these regardless of
|
|
262
|
+
// cA-ness (policyMappings is SHOULD-be-non-critical everywhere, so this never mis-flags conformant input).
|
|
263
|
+
function _isUnknownExtension(extOid) {
|
|
264
|
+
if (path.PROCESSED_EXTENSIONS[extOid] !== true) return _CRITICAL_LEGIT[extOid] !== true;
|
|
265
|
+
return path.TARGET_UNPROCESSED_IF_CRITICAL[extOid] === true;
|
|
266
|
+
}
|
|
258
267
|
|
|
259
268
|
// RSA-modulus / EC-curve helpers for the weak-key lint. Each returns null when the key
|
|
260
269
|
// material cannot be read as expected (a malformed RSA modulus, or EC parameters that are
|
package/lib/oid.js
CHANGED
|
@@ -95,6 +95,13 @@ var FAMILIES = {
|
|
|
95
95
|
// ETSI EN 319 412-5 QcType member OIDs (the certificate-purpose types) on the id-etsi-qct arc.
|
|
96
96
|
etsiQcType: { base: [0, 4, 0, 1862, 1, 6], of: { qctEsign: 1, qctEseal: 2, qctWeb: 3 } },
|
|
97
97
|
|
|
98
|
+
// [MS-WCCE] AD CS enrollment extensions on the id-microsoft-21 arc (szOID-CERTSRV / szOID-CERTIFICATE_TEMPLATE / szOID-APPLICATION_CERT_POLICIES).
|
|
99
|
+
msEnrollment: { base: [1, 3, 6, 1, 4, 1, 311, 21], of: {
|
|
100
|
+
msCaVersion: 1, msPreviousCertHash: 2, msCertificateTemplate: 7, msApplicationPolicies: 10 } },
|
|
101
|
+
|
|
102
|
+
// [MS-CRTD] legacy v1 enroll cert-type name on the id-microsoft-20 arc (szOID-ENROLL_CERTTYPE_EXTENSION).
|
|
103
|
+
msEnrollmentLegacy: { base: [1, 3, 6, 1, 4, 1, 311, 20], of: { msEnrollCertType: 2 } },
|
|
104
|
+
|
|
98
105
|
// id-aca -- RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
|
|
99
106
|
// authenticationInfo sec. 4.4.1 .. group sec. 4.4.4, plus encAttrs sec. 7.1 (the encrypted-
|
|
100
107
|
// attribute wrapper, syntax ContentInfo). { id-aca 5 } is reserved.
|
package/lib/path-validate.js
CHANGED
|
@@ -101,6 +101,11 @@ var PROCESSED_EXTENSIONS = {};
|
|
|
101
101
|
OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
|
|
102
102
|
OID.extKeyUsage, OID.cRLDistributionPoints].
|
|
103
103
|
forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
|
|
104
|
+
// Frozen after seeding: this exact object is both consulted by the critical-extension check here and
|
|
105
|
+
// exported (for pki.lint to stay consistent). A caller must not be able to add an OID -- doing so
|
|
106
|
+
// would make an attacker's critical, decoder-less extension pass as "processed" and skip both the
|
|
107
|
+
// unrecognized-critical check and structural validation. Freezing makes any such write a no-op.
|
|
108
|
+
Object.freeze(PROCESSED_EXTENSIONS);
|
|
104
109
|
|
|
105
110
|
// ---- signature verify bridge (NEW 6) ---------------------------------------
|
|
106
111
|
|
|
@@ -1061,6 +1066,7 @@ function applyPolicyMappings(state, mappings, i) {
|
|
|
1061
1066
|
// a terminal CA cert is conforming and is NOT treated as unprocessed here.)
|
|
1062
1067
|
var TARGET_UNPROCESSED_IF_CRITICAL = {};
|
|
1063
1068
|
TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
|
|
1069
|
+
Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
|
|
1064
1070
|
|
|
1065
1071
|
function unrecognizedCriticalExtension(cert, isTarget) {
|
|
1066
1072
|
for (var i = 0; i < cert.extensions.length; i++) {
|
|
@@ -2023,4 +2029,13 @@ module.exports = {
|
|
|
2023
2029
|
crlChecker: crlChecker,
|
|
2024
2030
|
ocspChecker: ocspChecker,
|
|
2025
2031
|
verifyOcspResponse: verifyOcspResponse,
|
|
2032
|
+
// The set of extension OIDs whose CRITICAL semantics this validator processes (RFC 5280 sec. 6.1).
|
|
2033
|
+
// Exposed so a linter can distinguish "processed" from "merely decoded" and stay consistent with
|
|
2034
|
+
// the path-validation verdict on a critical extension -- a decoder in certExtensionDecoders is NOT
|
|
2035
|
+
// by itself proof the criticality is honoured. Both sets are frozen so a caller cannot mutate them.
|
|
2036
|
+
PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
|
|
2037
|
+
// Extensions that ARE processed for an intermediate CA but are unprocessed on the target/leaf, so a
|
|
2038
|
+
// critical instance on the target fails closed (RFC 5280 sec. 6.1.5(f)) -- policyMappings is
|
|
2039
|
+
// prepare-next-only and SHOULD-be-non-critical. A linter mirrors this for a leaf certificate.
|
|
2040
|
+
TARGET_UNPROCESSED_IF_CRITICAL: TARGET_UNPROCESSED_IF_CRITICAL,
|
|
2026
2041
|
};
|
package/lib/schema-attrcert.js
CHANGED
|
@@ -281,7 +281,24 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
|
281
281
|
// kept stable for consumers keying off `form`.
|
|
282
282
|
var issuer = { form: "v2Form", v2Form: acinfo.fields.issuer.value.result, v1Form: null };
|
|
283
283
|
|
|
284
|
+
// RFC 5755 sec. 4.4 -- decode each attribute's value(s) additively: a recognized attribute type
|
|
285
|
+
// gains `decoded` (parallel to `values`); an unrecognized type falls back to { opaque, bytes } so
|
|
286
|
+
// the parse never fails on an unknown attribute, and a recognized-but-malformed value fails closed.
|
|
287
|
+
var attributes = acinfo.fields.attributes.value.result;
|
|
288
|
+
attributes.forEach(function (a) {
|
|
289
|
+
var dec = _ATTR_VALUE_DECODERS[a.type];
|
|
290
|
+
a.decoded = a.values.map(function (v) { return dec ? dec(v) : { opaque: true, bytes: v }; });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// RFC 5755 sec. 4.3 -- decode each AC extension additively (extensions[i].decoded), same opaque
|
|
294
|
+
// fallback + fail-closed-on-malformed contract as the attributes above.
|
|
284
295
|
var extField = acinfo.fields.extensions;
|
|
296
|
+
var extensions = (extField && extField.present) ? extField.value.result : [];
|
|
297
|
+
extensions.forEach(function (x) {
|
|
298
|
+
var dec = _AC_EXT_DECODERS[x.oid];
|
|
299
|
+
x.decoded = dec ? dec(x.value) : { opaque: true, bytes: x.value };
|
|
300
|
+
});
|
|
301
|
+
|
|
285
302
|
return {
|
|
286
303
|
version: acinfo.fields.version.value, // 2
|
|
287
304
|
holder: acinfo.fields.holder.value.result,
|
|
@@ -291,9 +308,9 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
|
291
308
|
serialNumber: acinfo.fields.serialNumber.value,
|
|
292
309
|
serialNumberHex: sc.toString("hex"),
|
|
293
310
|
validity: acinfo.fields.attrCertValidityPeriod.value.result,
|
|
294
|
-
attributes:
|
|
311
|
+
attributes: attributes,
|
|
295
312
|
issuerUniqueID: acinfo.fields.issuerUniqueID.present ? acinfo.fields.issuerUniqueID.value : null,
|
|
296
|
-
extensions:
|
|
313
|
+
extensions: extensions,
|
|
297
314
|
tbsBytes: e.tbsBytes,
|
|
298
315
|
signatureValue: e.signatureValue,
|
|
299
316
|
};
|
|
@@ -417,6 +434,238 @@ function matchesV1(root) {
|
|
|
417
434
|
return schema.isUniversal(k[1], TAGS.SEQUENCE);
|
|
418
435
|
}
|
|
419
436
|
|
|
437
|
+
// ---- RFC 5755 sec. 4.4 attribute-value decoders ----------------------
|
|
438
|
+
// A per-attribute-type decoder table (mirroring pkix.certExtensionDecoders): each takes the raw
|
|
439
|
+
// AttributeValue TLV Buffer and returns a structured value, throwing a typed attrcert/bad-* error on
|
|
440
|
+
// any non-conformant shape. Composes pkix's shared imperative helpers + generalName/generalNames.
|
|
441
|
+
// A single GeneralName decodes as the walk OBJECT itself (gn.tagNumber / gn.value); generalNames
|
|
442
|
+
// (plural) returns { names:[...] } on .result.
|
|
443
|
+
var _AV = pkix._decodeHelpers(NS);
|
|
444
|
+
var _decodeTop = _AV.decodeTop, _seqChildren = _AV.seqChildren, _readInt = _AV.readInt, _Oav = _AV.O;
|
|
445
|
+
var _GN = pkix.generalName(NS, { decodeValue: true, code: "attrcert/bad-general-name" });
|
|
446
|
+
var _GNS_0 = pkix.generalNames(NS, { implicitTag: 0, decodeValue: true, code: "attrcert/bad-general-names" });
|
|
447
|
+
function _isCtx(node, n) { return node && node.tagClass === "context" && node.tagNumber === n; }
|
|
448
|
+
function _isU(node, tag) { return node && node.tagClass === "universal" && node.tagNumber === tag; }
|
|
449
|
+
function _oneGN(node, code) {
|
|
450
|
+
if (!_isCtx(node, 0) && !_isCtx(node, 1) && !_isCtx(node, 2) && !_isCtx(node, 3) &&
|
|
451
|
+
!_isCtx(node, 4) && !_isCtx(node, 5) && !_isCtx(node, 6) && !_isCtx(node, 7) && !_isCtx(node, 8)) {
|
|
452
|
+
throw NS.E(code, "a GeneralName must be a context-tagged [0]..[8] CHOICE alternative");
|
|
453
|
+
}
|
|
454
|
+
var gn = schema.walk(_GN, node, NS);
|
|
455
|
+
return { tagNumber: gn.tagNumber, value: gn.value };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// SvceAuthInfo ::= SEQUENCE { service GeneralName, ident GeneralName, authInfo OCTET STRING OPTIONAL }
|
|
459
|
+
// -- authenticationInfo (sec. 4.4.1) allows authInfo; accessIdentity (sec. 4.4.2) MUST omit it.
|
|
460
|
+
function _svceAuthInfo(authInfoAllowed, code) {
|
|
461
|
+
return function (buf) {
|
|
462
|
+
var kids = _seqChildren(buf, code, "SvceAuthInfo");
|
|
463
|
+
if (kids.length < 2 || kids.length > 3) throw NS.E(code, "SvceAuthInfo must be a SEQUENCE { service, ident, authInfo? }");
|
|
464
|
+
var service = _oneGN(kids[0], code);
|
|
465
|
+
var ident = _oneGN(kids[1], code);
|
|
466
|
+
var authInfo = null;
|
|
467
|
+
if (kids.length === 3) {
|
|
468
|
+
if (!authInfoAllowed) throw NS.E(code, "this attribute must not carry authInfo (RFC 5755 sec. 4.4.2)");
|
|
469
|
+
if (!_isU(kids[2], TAGS.OCTET_STRING)) throw NS.E(code, "SvceAuthInfo authInfo must be an OCTET STRING");
|
|
470
|
+
authInfo = Buffer.concat([asn1.read.octetString(kids[2])]);
|
|
471
|
+
}
|
|
472
|
+
return { service: service, ident: ident, authInfo: authInfo };
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// IetfAttrSyntax ::= SEQUENCE { policyAuthority [0] IMPLICIT GeneralNames OPTIONAL,
|
|
477
|
+
// values SEQUENCE OF CHOICE { octets OCTET STRING, oid OBJECT IDENTIFIER, string UTF8String } }
|
|
478
|
+
function _ietfAttrSyntax(buf) {
|
|
479
|
+
var C = "attrcert/bad-ietf-attr";
|
|
480
|
+
var kids = _seqChildren(buf, C, "IetfAttrSyntax");
|
|
481
|
+
if (!kids.length || kids.length > 2) throw NS.E(C, "IetfAttrSyntax must be a SEQUENCE { policyAuthority [0]?, values }");
|
|
482
|
+
var policyAuthority = null, i = 0;
|
|
483
|
+
if (_isCtx(kids[0], 0)) { policyAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
|
|
484
|
+
var valuesNode = kids[i];
|
|
485
|
+
if (!valuesNode || !_isU(valuesNode, TAGS.SEQUENCE) || !valuesNode.children || !valuesNode.children.length) {
|
|
486
|
+
throw NS.E(C, "IetfAttrSyntax values must be a non-empty SEQUENCE OF value");
|
|
487
|
+
}
|
|
488
|
+
if (i + 1 !== kids.length) throw NS.E(C, "IetfAttrSyntax has unexpected trailing fields (a [0] must precede values)");
|
|
489
|
+
var kinds = {};
|
|
490
|
+
var values = valuesNode.children.map(function (v) {
|
|
491
|
+
if (_isU(v, TAGS.OCTET_STRING)) { kinds.octets = true; return { kind: "octets", value: Buffer.concat([asn1.read.octetString(v)]) }; }
|
|
492
|
+
if (_isU(v, TAGS.OBJECT_IDENTIFIER)) { kinds.oid = true; return { kind: "oid", value: asn1.read.oid(v) }; }
|
|
493
|
+
if (_isU(v, TAGS.UTF8_STRING)) { kinds.string = true; return { kind: "string", value: asn1.read.string(v) }; }
|
|
494
|
+
throw NS.E(C, "IetfAttrSyntax value must be an OCTET STRING, OBJECT IDENTIFIER, or UTF8String");
|
|
495
|
+
});
|
|
496
|
+
return { policyAuthority: policyAuthority, values: values, homogeneous: Object.keys(kinds).length === 1 };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// RoleSyntax ::= SEQUENCE { roleAuthority [0] IMPLICIT GeneralNames OPTIONAL, roleName [1] EXPLICIT GeneralName }
|
|
500
|
+
function _role(buf) {
|
|
501
|
+
var C = "attrcert/bad-role";
|
|
502
|
+
var kids = _seqChildren(buf, C, "RoleSyntax");
|
|
503
|
+
if (!kids.length || kids.length > 2) throw NS.E(C, "RoleSyntax must be a SEQUENCE { roleAuthority [0]?, roleName [1] }");
|
|
504
|
+
var roleAuthority = null, i = 0;
|
|
505
|
+
if (_isCtx(kids[0], 0)) { roleAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
|
|
506
|
+
var rn = kids[i];
|
|
507
|
+
if (!_isCtx(rn, 1) || !rn.children || rn.children.length !== 1) throw NS.E(C, "RoleSyntax roleName must be an EXPLICIT [1] wrapping exactly one GeneralName");
|
|
508
|
+
if (i + 1 !== kids.length) throw NS.E(C, "RoleSyntax has unexpected trailing fields");
|
|
509
|
+
return { roleAuthority: roleAuthority, roleName: _oneGN(rn.children[0], C) };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Clearance ::= SEQUENCE { policyId OBJECT IDENTIFIER, classList ClassList DEFAULT {unclassified},
|
|
513
|
+
// securityCategories SET OF SecurityCategory OPTIONAL }.
|
|
514
|
+
// ClassList ::= BIT STRING { unmarked(0), unclassified(1), restricted(2), confidential(3), secret(4), topSecret(5) }.
|
|
515
|
+
var _CLASSLIST_NAMES = ["unmarked", "unclassified", "restricted", "confidential", "secret", "topSecret"];
|
|
516
|
+
function _decodeClassList(node, C) {
|
|
517
|
+
var bs;
|
|
518
|
+
try { bs = asn1.read.bitString(node); } catch (e) { throw NS.E(C, "Clearance classList must be a BIT STRING", e); }
|
|
519
|
+
schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw NS.E(C, m); });
|
|
520
|
+
var names = [], flags = {}, reserved = [];
|
|
521
|
+
var total = bs.bytes.length * 8 - bs.unusedBits;
|
|
522
|
+
for (var bit = 0; bit < total; bit++) {
|
|
523
|
+
if ((bs.bytes[bit >> 3] & (0x80 >> (bit & 7))) === 0) continue;
|
|
524
|
+
if (bit < _CLASSLIST_NAMES.length) { names.push(_CLASSLIST_NAMES[bit]); flags[_CLASSLIST_NAMES[bit]] = true; }
|
|
525
|
+
else reserved.push(bit);
|
|
526
|
+
}
|
|
527
|
+
return { names: names, flags: flags, reservedBits: reserved };
|
|
528
|
+
}
|
|
529
|
+
// SecurityCategory ::= SEQUENCE { type [0] IMPLICIT OBJECT IDENTIFIER, value [1] EXPLICIT ANY }.
|
|
530
|
+
function _securityCategory(node, C) {
|
|
531
|
+
if (!_isU(node, TAGS.SEQUENCE) || !node.children || node.children.length !== 2) throw NS.E(C, "a SecurityCategory must be a SEQUENCE of a type and a value");
|
|
532
|
+
var typeNode = node.children[0], valueNode = node.children[1];
|
|
533
|
+
if (!_isCtx(typeNode, 0) || typeNode.children) throw NS.E(C, "SecurityCategory type must be a primitive [0] IMPLICIT OBJECT IDENTIFIER");
|
|
534
|
+
var type; try { type = asn1.decodeOidContent(typeNode.content); } catch (e) { throw NS.E(C, "SecurityCategory type is not a valid OBJECT IDENTIFIER", e); }
|
|
535
|
+
if (!_isCtx(valueNode, 1) || !valueNode.children || valueNode.children.length !== 1) throw NS.E(C, "SecurityCategory value must be an EXPLICIT [1] wrapping one element");
|
|
536
|
+
return { type: type, valueBytes: Buffer.concat([valueNode.children[0].bytes]) };
|
|
537
|
+
}
|
|
538
|
+
function _clearance(buf) {
|
|
539
|
+
var C = "attrcert/bad-clearance";
|
|
540
|
+
var kids = _seqChildren(buf, C, "Clearance");
|
|
541
|
+
if (!kids.length || kids.length > 3) throw NS.E(C, "Clearance must be a SEQUENCE { policyId, classList?, securityCategories? }");
|
|
542
|
+
if (!_isU(kids[0], TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER");
|
|
543
|
+
var policyId; try { policyId = asn1.read.oid(kids[0]); } catch (e) { throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER", e); }
|
|
544
|
+
var classList = { names: ["unclassified"], flags: { unclassified: true }, reservedBits: [] }, i = 1; // DEFAULT {unclassified}
|
|
545
|
+
if (kids[i] && _isU(kids[i], TAGS.BIT_STRING)) {
|
|
546
|
+
classList = _decodeClassList(kids[i], C);
|
|
547
|
+
if (classList.names.length === 1 && classList.flags.unclassified && !classList.reservedBits.length) {
|
|
548
|
+
throw NS.E(C, "Clearance classList equal to the DEFAULT {unclassified} must be omitted (non-canonical DER)");
|
|
549
|
+
}
|
|
550
|
+
i++;
|
|
551
|
+
}
|
|
552
|
+
var securityCategories = null;
|
|
553
|
+
if (kids[i]) {
|
|
554
|
+
var setNode = kids[i];
|
|
555
|
+
if (!_isU(setNode, TAGS.SET) || !setNode.children || !setNode.children.length) throw NS.E(C, "Clearance securityCategories must be a non-empty SET OF SecurityCategory");
|
|
556
|
+
securityCategories = setNode.children.map(function (categoryNode) { return _securityCategory(categoryNode, C); });
|
|
557
|
+
i++;
|
|
558
|
+
}
|
|
559
|
+
if (i !== kids.length) throw NS.E(C, "Clearance has fields out of order or unexpected trailing fields");
|
|
560
|
+
return { policyId: policyId, classList: classList, securityCategories: securityCategories };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
var _ATTR_VALUE_DECODERS = {};
|
|
564
|
+
_ATTR_VALUE_DECODERS[_Oav("role")] = _role;
|
|
565
|
+
_ATTR_VALUE_DECODERS[_Oav("clearance")] = _clearance;
|
|
566
|
+
// RFC 5755 sec. 4.4.6 requires decoding the legacy RFC 3281 id-at-clearance too (the X.501
|
|
567
|
+
// selected-attribute-types arc, an alias of "clearance" -- built from arcs, never a dotted literal).
|
|
568
|
+
_ATTR_VALUE_DECODERS[oid.fromArcs([2, 5, 1, 5, 55])] = _clearance;
|
|
569
|
+
_ATTR_VALUE_DECODERS[_Oav("authenticationInfo")] = _svceAuthInfo(true, "attrcert/bad-svce-auth-info");
|
|
570
|
+
_ATTR_VALUE_DECODERS[_Oav("accessIdentity")] = _svceAuthInfo(false, "attrcert/bad-access-identity");
|
|
571
|
+
_ATTR_VALUE_DECODERS[_Oav("chargingIdentity")] = _ietfAttrSyntax;
|
|
572
|
+
_ATTR_VALUE_DECODERS[_Oav("group")] = _ietfAttrSyntax;
|
|
573
|
+
|
|
574
|
+
// ---- RFC 5755 sec. 4.3 AC-extension decoders -------------------------
|
|
575
|
+
// A per-extension-OID decoder table for the AC-specific extensions (kept in the AC domain, not the
|
|
576
|
+
// shared cert-extension table, because Targets/TargetCert reuse the local IssuerSerial). Each takes
|
|
577
|
+
// the raw extnValue content Buffer and returns a structured value, fail-closed with a typed error.
|
|
578
|
+
|
|
579
|
+
// AuditIdentity ::= OCTET STRING (sec. 4.3.1) -- an opaque audit tag, unconstrained (no SIZE), so an
|
|
580
|
+
// empty OCTET STRING (04 00) is valid; only the OCTET STRING type is enforced.
|
|
581
|
+
function _acAuditIdentity(buf) {
|
|
582
|
+
var C = "attrcert/bad-audit-identity";
|
|
583
|
+
var n = _decodeTop(buf, C, "AuditIdentity");
|
|
584
|
+
if (!_isU(n, TAGS.OCTET_STRING)) throw NS.E(C, "AuditIdentity must be an OCTET STRING");
|
|
585
|
+
return Buffer.concat([asn1.read.octetString(n)]);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// NoRevAvail syntax is NULL (sec. 4.3.6: '0500'H is the DER encoding) -- the extnValue OCTET STRING
|
|
589
|
+
// contains a DER NULL. An empty or any-other-shape value is malformed and fails closed.
|
|
590
|
+
function _noRevAvail(buf) {
|
|
591
|
+
var C = "attrcert/bad-no-rev-avail";
|
|
592
|
+
var n = _decodeTop(buf, C, "NoRevAvail");
|
|
593
|
+
try { asn1.read.nullValue(n); } catch (e) { throw NS.E(C, "NoRevAvail must be a DER NULL (RFC 5755 sec. 4.3.6)", e); }
|
|
594
|
+
return { noRevAvail: true };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Target ::= CHOICE { targetName [0] GeneralName, targetGroup [1] GeneralName, targetCert [2] TargetCert }.
|
|
598
|
+
// [0]/[1] wrap a GeneralName CHOICE (EXPLICIT); [2] is an IMPLICIT TargetCert SEQUENCE.
|
|
599
|
+
function _acTargetCert(node, C) {
|
|
600
|
+
if (!node.children || !node.children.length) throw NS.E(C, "a targetCert [2] must be a non-empty TargetCert SEQUENCE");
|
|
601
|
+
var out = { kind: "targetCert", targetCertificate: null, targetName: null, certDigestInfo: null }, i = 0;
|
|
602
|
+
out.targetCertificate = schema.walk(ISSUER_SERIAL, node.children[i++], NS).result;
|
|
603
|
+
if (node.children[i] && node.children[i].tagClass === "context") out.targetName = _oneGN(node.children[i++], C);
|
|
604
|
+
if (node.children[i] && _isU(node.children[i], TAGS.SEQUENCE)) out.certDigestInfo = schema.walk(OBJECT_DIGEST_INFO, node.children[i++], NS).result;
|
|
605
|
+
if (i !== node.children.length) throw NS.E(C, "a targetCert has unexpected trailing fields");
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
function _acTarget(node, C, allowTargetCert) {
|
|
609
|
+
if (_isCtx(node, 0) || _isCtx(node, 1)) {
|
|
610
|
+
if (!node.children || node.children.length !== 1) throw NS.E(C, "a Target name must be an EXPLICIT [0]/[1] wrapping one GeneralName");
|
|
611
|
+
return { kind: node.tagNumber === 0 ? "targetName" : "targetGroup", name: _oneGN(node.children[0], C) };
|
|
612
|
+
}
|
|
613
|
+
if (_isCtx(node, 2)) {
|
|
614
|
+
if (!allowTargetCert) throw NS.E(C, "the targetCert [2] CHOICE MUST NOT be used in proxying information (RFC 5755 sec. 7.4)");
|
|
615
|
+
return _acTargetCert(node, C);
|
|
616
|
+
}
|
|
617
|
+
throw NS.E(C, "a Target must be [0] targetName, [1] targetGroup, or [2] targetCert");
|
|
618
|
+
}
|
|
619
|
+
// Targets ::= SEQUENCE OF Target -- decode a Targets SEQUENCE node into an array of Target.
|
|
620
|
+
function _acDecodeTargets(node, C, allowTargetCert) {
|
|
621
|
+
if (!_isU(node, TAGS.SEQUENCE) || !node.children || !node.children.length) throw NS.E(C, "Targets must be a non-empty SEQUENCE OF Target");
|
|
622
|
+
return node.children.map(function (t) { return _acTarget(t, C, allowTargetCert); });
|
|
623
|
+
}
|
|
624
|
+
// Both targetInformation (sec. 4.3.2) and ProxyInfo (sec. 7.4) are SEQUENCE OF Targets -- an outer
|
|
625
|
+
// SEQUENCE whose every element is itself a Targets (SEQUENCE OF Target). A conforming issuer emits one
|
|
626
|
+
// Targets, but users MUST accept several. proxying additionally forbids the targetCert [2] alternative.
|
|
627
|
+
function _seqOfTargets(buf, C, allowTargetCert) {
|
|
628
|
+
var kids = _seqChildren(buf, C, "SEQUENCE OF Targets");
|
|
629
|
+
if (!kids.length) throw NS.E(C, "a SEQUENCE OF Targets must be non-empty");
|
|
630
|
+
return kids.map(function (targetsNode) { return _acDecodeTargets(targetsNode, C, allowTargetCert); });
|
|
631
|
+
}
|
|
632
|
+
function _targetInformation(buf) { return _seqOfTargets(buf, "attrcert/bad-targets", true); }
|
|
633
|
+
function _acProxying(buf) { return _seqOfTargets(buf, "attrcert/bad-proxy-info", false); }
|
|
634
|
+
|
|
635
|
+
// AAControls ::= SEQUENCE { pathLenConstraint INTEGER (0..MAX) OPTIONAL, permittedAttrs [0] AttrSpec
|
|
636
|
+
// OPTIONAL, excludedAttrs [1] AttrSpec OPTIONAL, permitUnSpecified BOOLEAN DEFAULT TRUE }.
|
|
637
|
+
// AttrSpec ::= SEQUENCE OF OBJECT IDENTIFIER (the [0]/[1] IMPLICIT tag replaces the SEQUENCE tag).
|
|
638
|
+
function _acAttrSpec(node, C) {
|
|
639
|
+
if (!node.children) throw NS.E(C, "an AttrSpec must be a SEQUENCE OF OBJECT IDENTIFIER");
|
|
640
|
+
return node.children.map(function (o) {
|
|
641
|
+
if (!_isU(o, TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER");
|
|
642
|
+
try { return asn1.read.oid(o); } catch (e) { throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER", e); }
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function _aaControls(buf) {
|
|
646
|
+
var C = "attrcert/bad-aa-controls";
|
|
647
|
+
var kids = _seqChildren(buf, C, "AAControls");
|
|
648
|
+
if (kids.length > 4) throw NS.E(C, "AAControls has too many fields");
|
|
649
|
+
var out = { pathLenConstraint: null, permittedAttrs: null, excludedAttrs: null, permitUnSpecified: true }, i = 0;
|
|
650
|
+
if (kids[i] && _isU(kids[i], TAGS.INTEGER)) { out.pathLenConstraint = guard.range.uint31(_readInt(kids[i], C, "pathLenConstraint"), NS.E, C, "AAControls pathLenConstraint"); i++; }
|
|
651
|
+
if (kids[i] && _isCtx(kids[i], 0)) { out.permittedAttrs = _acAttrSpec(kids[i], C); i++; }
|
|
652
|
+
if (kids[i] && _isCtx(kids[i], 1)) { out.excludedAttrs = _acAttrSpec(kids[i], C); i++; }
|
|
653
|
+
if (kids[i] && _isU(kids[i], TAGS.BOOLEAN)) {
|
|
654
|
+
var v = asn1.read.boolean(kids[i]);
|
|
655
|
+
if (v === true) throw NS.E(C, "AAControls permitUnSpecified TRUE equals the DEFAULT and must be omitted (non-canonical DER)");
|
|
656
|
+
out.permitUnSpecified = v; i++;
|
|
657
|
+
}
|
|
658
|
+
if (i !== kids.length) throw NS.E(C, "AAControls has fields out of order or unexpected trailing fields");
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
var _AC_EXT_DECODERS = {};
|
|
663
|
+
_AC_EXT_DECODERS[_Oav("acAuditIdentity")] = _acAuditIdentity;
|
|
664
|
+
_AC_EXT_DECODERS[_Oav("targetInformation")] = _targetInformation;
|
|
665
|
+
_AC_EXT_DECODERS[_Oav("noRevAvail")] = _noRevAvail;
|
|
666
|
+
_AC_EXT_DECODERS[_Oav("aaControls")] = _aaControls;
|
|
667
|
+
_AC_EXT_DECODERS[_Oav("acProxying")] = _acProxying;
|
|
668
|
+
|
|
420
669
|
module.exports = {
|
|
421
670
|
parse: parse,
|
|
422
671
|
parseV1: parseV1,
|
package/lib/schema-pkix.js
CHANGED
|
@@ -598,13 +598,14 @@ function assertPolicyQualifiers(qNode, fail) {
|
|
|
598
598
|
});
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
// Shared imperative decode helpers for the certExtensionDecoders + attrValueDecoders factories. Both
|
|
602
|
+
// take the caller's ns, so the helpers close over ns.E / ns.oid; extracted once so the two factories
|
|
603
|
+
// share one copy (a per-OID value-decoder body is the identical idiom to a per-OID extension body).
|
|
604
|
+
function _decodeHelpers(ns) {
|
|
604
605
|
function decodeTop(buf, code, what) {
|
|
605
606
|
var n;
|
|
606
607
|
try { n = asn1.decode(buf); }
|
|
607
|
-
catch (e) { throw ns.E(code, "malformed " + what + "
|
|
608
|
+
catch (e) { throw ns.E(code, "malformed " + what + " value: " + ((e && e.message) || String(e)), e); }
|
|
608
609
|
return n;
|
|
609
610
|
}
|
|
610
611
|
function seqChildren(buf, code, what) {
|
|
@@ -618,6 +619,19 @@ function certExtensionDecoders(ns) {
|
|
|
618
619
|
try { return asn1.read.integer(node); }
|
|
619
620
|
catch (e) { throw ns.E(code, what + " must be an INTEGER", e); }
|
|
620
621
|
}
|
|
622
|
+
// Resolve a registered OID name -> dotted string, throwing at module load on a typo (a decoder keyed
|
|
623
|
+
// by an unresolved name would silently never match, treating a real extension/attribute as opaque).
|
|
624
|
+
function O(nm) {
|
|
625
|
+
var d = ns.oid.byName(nm);
|
|
626
|
+
if (typeof d !== "string") throw new TypeError("pki.schema.pkix: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
627
|
+
return d;
|
|
628
|
+
}
|
|
629
|
+
return { decodeTop: decodeTop, seqChildren: seqChildren, readInt: readInt, O: O };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function certExtensionDecoders(ns) {
|
|
633
|
+
var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
|
|
634
|
+
var _h = _decodeHelpers(ns), decodeTop = _h.decodeTop, seqChildren = _h.seqChildren, readInt = _h.readInt, O = _h.O;
|
|
621
635
|
|
|
622
636
|
// basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLen INTEGER (0..MAX) OPTIONAL }
|
|
623
637
|
function basicConstraints(buf) {
|
|
@@ -910,15 +924,6 @@ function certExtensionDecoders(ns) {
|
|
|
910
924
|
});
|
|
911
925
|
}
|
|
912
926
|
|
|
913
|
-
// byName returns undefined for an unregistered name; keying a decoder row
|
|
914
|
-
// under "undefined" would silently drop that extension from dispatch (the
|
|
915
|
-
// consumer would treat the extension as structurally unvalidated / absent),
|
|
916
|
-
// so a typo'd key-list entry must fail here, at module load.
|
|
917
|
-
function O(nm) {
|
|
918
|
-
var d = ns.oid.byName(nm);
|
|
919
|
-
if (typeof d !== "string") throw new TypeError("certExtensionDecoders: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
920
|
-
return d;
|
|
921
|
-
}
|
|
922
927
|
// qcStatements ::= SEQUENCE OF QCStatement (RFC 3739 sec. 3.2.6). QCStatement ::= SEQUENCE {
|
|
923
928
|
// statementId OBJECT IDENTIFIER, statementInfo ANY DEFINED BY statementId OPTIONAL }. Known statementIds
|
|
924
929
|
// (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4 catalog) decode their statementInfo; an unknown statementId
|
|
@@ -1031,6 +1036,57 @@ function certExtensionDecoders(ns) {
|
|
|
1031
1036
|
return out;
|
|
1032
1037
|
}
|
|
1033
1038
|
|
|
1039
|
+
// [MS-WCCE] szOID-CERTIFICATE_TEMPLATE -- CertificateTemplateOID ::= SEQUENCE {
|
|
1040
|
+
// templateID OID, templateMajorVersion INTEGER OPTIONAL, templateMinorVersion INTEGER OPTIONAL }.
|
|
1041
|
+
// The two versions are DWORDs (0..2^32-1), NOT uint31 -- a CA key/template revision with bit 31
|
|
1042
|
+
// set is a legal DWORD, so bound to the whole safe-integer range, never guard.range.uint31.
|
|
1043
|
+
function msCertificateTemplate(buf) {
|
|
1044
|
+
var C = ns.prefix + "/bad-ms-certificate-template";
|
|
1045
|
+
var kids = seqChildren(buf, C, "CertificateTemplate");
|
|
1046
|
+
if (!kids.length || kids.length > 3) throw ns.E(C, "CertificateTemplateOID must be a SEQUENCE of a templateID and up to two version INTEGERs");
|
|
1047
|
+
if (kids[0].tagClass !== "universal" || kids[0].tagNumber !== _T.OBJECT_IDENTIFIER) throw ns.E(C, "CertificateTemplateOID templateID must be an OBJECT IDENTIFIER");
|
|
1048
|
+
var id;
|
|
1049
|
+
try { id = asn1.read.oid(kids[0]); } catch (e) { throw ns.E(C, "CertificateTemplateOID templateID must be an OBJECT IDENTIFIER", e); }
|
|
1050
|
+
function ver(i, what) {
|
|
1051
|
+
if (!kids[i]) return null;
|
|
1052
|
+
if (kids[i].tagClass !== "universal" || kids[i].tagNumber !== _T.INTEGER) throw ns.E(C, what + " must be an INTEGER");
|
|
1053
|
+
return guard.range.int(readInt(kids[i], C, what), 0n, 4294967295n, ns.E, C, what);
|
|
1054
|
+
}
|
|
1055
|
+
return { templateID: id, name: ns.oid.name(id) || null, templateMajorVersion: ver(1, "templateMajorVersion"), templateMinorVersion: ver(2, "templateMinorVersion") };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// [MS-CRTD] szOID-ENROLL_CERTTYPE_EXTENSION -- the legacy v1 template name as a BARE BMPString.
|
|
1059
|
+
// asn1.read.string accepts any universal string tag, so the explicit BMP_STRING assert is the guard.
|
|
1060
|
+
function msEnrollCertType(buf) {
|
|
1061
|
+
var C = ns.prefix + "/bad-ms-enroll-cert-type";
|
|
1062
|
+
var n = decodeTop(buf, C, "EnrollCertType");
|
|
1063
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.BMP_STRING) throw ns.E(C, "the enroll cert-type name must be a BMPString");
|
|
1064
|
+
try { return asn1.read.string(n); } catch (e) { throw ns.E(C, "the enroll cert-type name must be a well-formed BMPString", e); }
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// [MS-WCCE] szOID-CERTSRV_CA_VERSION -- INTEGER (0..2^32-1) = (caKeyIndex << 16) | certIndex.
|
|
1068
|
+
function msCaVersion(buf) {
|
|
1069
|
+
var C = ns.prefix + "/bad-ms-ca-version";
|
|
1070
|
+
var n = decodeTop(buf, C, "CACertVersion");
|
|
1071
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "CACertVersion must be an INTEGER");
|
|
1072
|
+
var v = guard.range.int(readInt(n, C, "CACertVersion"), 0n, 4294967295n, ns.E, C, "CACertVersion (DWORD)");
|
|
1073
|
+
return { caVersion: v, caKeyIndex: v >>> 16, certIndex: v & 0xffff };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// [MS-WCCE] szOID-CERTSRV_PREVIOUS_CERT_HASH -- the SHA-1 thumbprint of the prior CA certificate.
|
|
1077
|
+
// AD CS stamps the Windows certificate THUMBPRINT here, which Windows computes with SHA-1 regardless
|
|
1078
|
+
// of the certificate's signature algorithm, so the OCTET STRING is exactly 20 octets. An empty or
|
|
1079
|
+
// other-length value is malformed renewal metadata and fails closed.
|
|
1080
|
+
function msPreviousCertHash(buf) {
|
|
1081
|
+
var C = ns.prefix + "/bad-ms-previous-cert-hash";
|
|
1082
|
+
var n = decodeTop(buf, C, "CAPrevCertHash");
|
|
1083
|
+
var hash;
|
|
1084
|
+
try { hash = Buffer.concat([asn1.read.octetString(n)]); }
|
|
1085
|
+
catch (e) { throw ns.E(C, "CAPrevCertHash must be an OCTET STRING", e); }
|
|
1086
|
+
if (hash.length !== 20) throw ns.E(C, "CAPrevCertHash must be a 20-octet SHA-1 certificate thumbprint");
|
|
1087
|
+
return hash;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1034
1090
|
var byOid = {};
|
|
1035
1091
|
byOid[O("qcStatements")] = qcStatements;
|
|
1036
1092
|
byOid[O("basicConstraints")] = basicConstraints;
|
|
@@ -1049,6 +1105,11 @@ function certExtensionDecoders(ns) {
|
|
|
1049
1105
|
byOid[O("precertificatePoison")] = precertPoison;
|
|
1050
1106
|
byOid[O("cRLDistributionPoints")] = crlDistributionPoints;
|
|
1051
1107
|
byOid[O("freshestCRL")] = crlDistributionPoints;
|
|
1108
|
+
byOid[O("msCertificateTemplate")] = msCertificateTemplate;
|
|
1109
|
+
byOid[O("msEnrollCertType")] = msEnrollCertType;
|
|
1110
|
+
byOid[O("msCaVersion")] = msCaVersion;
|
|
1111
|
+
byOid[O("msPreviousCertHash")] = msPreviousCertHash;
|
|
1112
|
+
byOid[O("msApplicationPolicies")] = certificatePolicies; // [MS-WCCE] = RFC 5280 certificatePolicies, reused not re-implemented
|
|
1052
1113
|
return { byOid: byOid };
|
|
1053
1114
|
}
|
|
1054
1115
|
|
|
@@ -1263,6 +1324,7 @@ module.exports = {
|
|
|
1263
1324
|
rawNonEmptySequence: rawNonEmptySequence,
|
|
1264
1325
|
CRL_REASON_NAMES: CRL_REASON_NAMES,
|
|
1265
1326
|
certExtensionDecoders: certExtensionDecoders,
|
|
1327
|
+
_decodeHelpers: _decodeHelpers,
|
|
1266
1328
|
attribute: attribute,
|
|
1267
1329
|
extension: extension,
|
|
1268
1330
|
extensions: extensions,
|
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:81b0b6a6-dee2-4e3f-b995-abbe40a0f560",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-17T00:29:40.078Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.33",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.33",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.33",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.33",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|