@blamejs/pki 0.8.9 → 0.8.11

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 CHANGED
@@ -4,6 +4,38 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.8.11 — 2026-09-23
8
+
9
+ What a certificate, CRL, request or attribute certificate asserts in its extensions is read in one call.
10
+
11
+ ### Added
12
+
13
+ - `pki.schema.x509.decodeExtensions(certOrBytes, opts?)` and `pki.schema.x509.decodeExtension(ext, opts?)` return a certificate's extensions as decoded records. Each row is `{ oid, name, critical, value, scope, index, containerIndex, state, decoded, code, profile }`. `state` is `"decoded"`, `"unrecognized"` for an identifier no decoder is registered for, or `"undecodable"` for a value that is not the structure its identifier names; `code` carries the typed error in that last arm. `value` is the raw extnValue octets in every arm, for a caller recomputing a hash or comparing an encoding. Read `state` rather than testing `decoded`, because a valid `ocspNoCheck` decodes to null.
14
+ - The same pair on `pki.schema.crl`, `pki.schema.csr`, `pki.schema.attrcert` and `pki.schema.crmf`. The CRL table spans the CRL's own extensions and every revoked entry's in one array, each row naming its scope and, for an entry, its position in `containerIndex`. `pki.schema.crl.decodeExtension` takes a required `opts.scope` of `"crl"`, `"crl-entry"` or `"ocsp-single"`, since RFC 6960 section 4.4.5 carries a CRL entry extension into an OCSP SingleResponse and the same identifier means different things at those three scopes.
15
+ - `pki.schema.csr.decodeExtensions` refuses a request carrying more than one `extensionRequest` attribute with `csr/ambiguous-extension-request`, because nothing states which of them a CA should honor. Pass `groups: true` to read each attribute separately, as `[{ attributeIndex, extensions }]`.
16
+ - Both verbs report what they find and refuse nothing on the strength of it. An extension against the criticality its RFC fixes still decodes, and the deviation is reported on `profile` with the clause that fixes it. An unrecognized critical extension comes back as a row, whatever `opts.strict` says. RFC 5280 section 4.2's rule about one belongs to the relying party, and the two facts that rule needs, `critical` and `state`, are on the row.
17
+ - The certificate extension table decodes the RFC 3779 address and AS number blocks (`ipAddrBlocks`, `autonomousSysIds`) and the RFC 8360 identifiers for the same syntax, plus the RFC 7633 TLS feature extension. `pki.inspect.certificate` renders all five. An AS number and a TLS feature are read as the `INTEGER` each type declares, so both come back as a `BigInt` and a value outside its registry's width is reported rather than refused.
18
+ - The CRL extension table decodes the issuing distribution point and the delta CRL indicator, along with the authority key identifier, issuer alternative name, freshest CRL and authority information access that RFC 5280 section 5.2 profiles, and the certificate issuer at entry scope.
19
+
20
+ ### Changed
21
+
22
+ - `pki.schema.crl.parse` puts the raw extnValue octets on every extension record as `valueBytes`, beside the `value` it already carried. The three extensions the CRL parse decodes in place, `cRLNumber`, `reasonCode` and `invalidityDate`, previously left a caller no way back to the bytes those values came from.
23
+ - Every signer that validates a pre-encoded extension now validates a pre-encoded `ipAddrBlocks`, `autonomousSysIds` or `tlsFeature` too, and refuses one whose value is not the structure its identifier names. A certificate carrying such a value can still be read: the refusal is on the writing side.
24
+ - `pki.attrcert.sign` and `pki.schema.attrcert.decodeExtensions` read the criticality RFC 5755 fixes from one table, so the signer's refusal names the section it refuses under where its message previously cited the RFC alone.
25
+
26
+ ## v0.8.10 — 2026-09-23
27
+
28
+ A certificate's names are read the same way wherever the toolkit reads them.
29
+
30
+ ### Fixed
31
+
32
+ - `pki.tsp.verify` reports `tsp/bad-eku` for an empty extendedKeyUsage. RFC 5280 section 4.2.1.12 has at least one KeyPurposeId, and an empty one decodes as a SEQUENCE, so a reader checking only the tag called it well-formed and reported it as naming the wrong purpose, `tsp/eku-not-exclusive`. Every other consumer of that extension already answered with the malformed-extension code.
33
+ - The subjectAltName a timestamp token names its TSA through is read under the same rules every other consumer reads a name under, so a name this toolkit refuses elsewhere cannot be a source of TSA identity. The match itself is still a comparison of the encoded GeneralName bytes, which is what naming a TSA means: the name in the token and the name in the certificate have to be the same name.
34
+
35
+ ### Security
36
+
37
+ - `pki.sigstore.verifyBundle` refuses a Fulcio certificate whose subjectAltName carries more than one name, whatever forms those names take. It counted only rfc822Name, dNSName, uniformResourceIdentifier and otherName, and dropped every other form before counting, so a certificate pairing a URI with an iPAddress or a directoryName was accepted as binding one identity. A caller identity policy matched the form the verifier reported and never saw the other, which is the outcome the one-identity rule exists to prevent. The rule now counts every name the extension carries. A certificate carrying a single name in a form with no identity string still reports `identity.san` as `null`, unchanged, so a policy gates on the null rather than on a name nothing read.
38
+
7
39
  ## v0.8.9 — 2026-09-23
8
40
 
9
41
  The 0.8.9 tarball is the one to install for everything 0.8.8 added.
package/README.md CHANGED
@@ -243,16 +243,16 @@ comment blocks, is at [pkijs.com](https://pkijs.com).
243
243
  | `pki.webcrypto` | A W3C `SubtleCrypto` engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2 and SHA, plus post-quantum ML-DSA-44/65/87, SLH-DSA, and ML-KEM-512/768/1024 key generation with certificate and 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. `encapsulateBits` and `decapsulateBits` are the ML-KEM key-establishment pair the CMS `KEMRecipientInfo` arm rides on, with the FIPS 203 §7.3 ciphertext-length check enforced here so a direct caller inherits it. Cross-implementation use is undefined in the specification, so a `CryptoKey` from a different WebCrypto implementation is refused here with a typed fault naming its origin; the `pki.*` verbs adopt such a key instead, whether it came from the platform or from a separately installed copy of this toolkit |
244
244
  | `pki.tls` | RFC 8879 certificate compression and the RFC 8446 §4.4.2 Certificate message it carries (RFC 8446 is obsoleted by RFC 9846), which is the handshake's largest payload. `decompressCertificate` decodes a `CompressedCertificate` and returns the algorithm, the declared uncompressed length, the recovered message raw, and its per-entry certificate DER. Decompression carries the two-sided bound §5 requires: capped at the message's own declared length so a bomb is refused mid-stream, then compared to that declaration exactly, which catches the under-length direction a cap alone cannot see. An algorithm outside the RFC 8879 registry, one the runtime cannot decompress, or one the receiver never advertised is refused before any decompressor is handed the bytes; an empty compressed body is a framing violation (`tls/bad-framing`); and trailing bytes, after the message or after the compressed frame, are refused (`tls/trailing-data`), so one chain has exactly one encoding. `compressCertificate` is the inverse and round-trips its own output before returning. `parseCertificateMessage` decodes the RFC 8446 §4.4.2 message on its own, surfacing each entry's certificate DER ready for `pki.schema.x509.parse` alongside its raw extensions; a separate extension negotiates `certificate_type`, so it is declared, never guessed. All three registered algorithms (zlib, brotli, zstd) are implemented, each offered only where the running Node decompresses it safely — one whose decompressor answers a truncated frame with a short result instead of a fault is dropped at startup rather than advertised with a truncation it cannot detect. Structure only: no handshake is spoken and no certificate verified — `decompressCertificate`, `compressCertificate`, `parseCertificateMessage` |
245
245
  | `pki.schema` | The schema family. `parse` detects which PKI format a DER or PEM input encodes and routes it to the right parser; `all` enumerates the registered formats; `pem.decodeBundle` reads every object in a multi-object PEM file (a `fullchain.pem`, a `ca-certificates.crt`, a key beside its certificate), carrying each object's label, DER and offset into the text, with explanatory text around the blocks read as text and the file bounded in both the number of objects and the bytes they decode to; `pem.encodeBundle` writes that list back; `pki.schema.pkcs1` reads and writes the RFC 8017 `RSA PRIVATE KEY` and `RSA PUBLIC KEY` encodings, with every component relation the specification states checked; `pki.schema.sec1` reads and writes the RFC 5915 `EC PRIVATE KEY` encoding, holding the scalar to its curve's width and the parameters field to a named curve; the engine and per-format members are grouped here |
246
- | `pki.schema.x509` | Certificates (RFC 5280) parsed into structured, validated fields. Each extension is surfaced by name with its `extnValue` bytes; the shared extension decoders that read those bytes are what `pki.lint.certificate`, `pki.inspect` and `pki.path.validate` consume, and they cover 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), the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, and the RFC 6960 OCSP no-check responder marker. Unknown statements stay opaque, fail-closed Fail-closed — `parse`, `pemDecode`, `pemEncode` |
246
+ | `pki.schema.x509` | Certificates (RFC 5280) parsed into structured, validated fields. Each extension is surfaced by name with its `extnValue` bytes; the shared extension decoders that read those bytes are what `pki.lint.certificate`, `pki.inspect` and `pki.path.validate` consume, and they cover 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), the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), subject information access, subject directory attributes, and the RFC 6960 OCSP no-check responder marker. Unknown statements stay opaque, fail-closed. `decodeExtensions` is the read door onto that table: it returns one row per extension carrying the decoded value, the raw `extnValue` octets, and a `state` of `decoded`, `unrecognized` or `undecodable`, so a reader can tell an extension nothing decodes from one whose value does not read. The RFC 3779 address and AS number blocks, their RFC 8360 identifiers, and the RFC 7633 TLS feature extension decode here too Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
247
247
  | `pki.schema.c509` | C509 certificates (draft-ietf-cose-cbor-encoded-cert), the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR. `encode` 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. A `parse` result re-emits its native array in 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, inhibitAnyPolicy, OCSP No Check, TLS Feature), the general-name-bearing extensions (subjectAltName, issuer alternative name, name constraints, CRL distribution points, freshest CRL, authority and subject information access, and the full authority key identifier) over one shared GeneralNames codec, certificate policies with their CPS-URI and UserNotice qualifiers, policy mappings and policy constraints, subject directory attributes, and the RFC 3779 resource-delegation extensions (IP address blocks and AS identifiers) with their RFC 8360 v2 twins, whose addresses ride either the delta-coded integer form or the byte-string form the specification mandates once an address exceeds eight octets. A value the compact form cannot carry exactly falls back to the byte-string form with its bytes intact; a certificate outside the invertible set throws a typed `C509Error`. Called explicitly, since it is CBOR rather than DER and so is not auto-routed |
248
- | `pki.schema.crl` | X.509 CRLs (RFC 5280 §5): revoked serials with real-`Date` revocation times, named and partly decoded extensions Fail-closed — `parse`, `pemDecode`, `pemEncode` |
249
- | `pki.schema.csr` | PKCS#10 certification requests (RFC 2986): subject DN, public key, requested attributes, signature Fail-closed — `parse`, `pemDecode`, `pemEncode` |
248
+ | `pki.schema.crl` | X.509 CRLs (RFC 5280 §5): revoked serials with real-`Date` revocation times, named and partly decoded extensions, each record carrying the raw `extnValue` octets as `valueBytes`. `decodeExtensions` returns the CRL's own extensions and every revoked entry's in one array, each row naming the scope it was read at, and covers the whole §5.2 and §5.3 profile including the issuing distribution point and the delta CRL indicator. `decodeExtension` takes the scope explicitly, since RFC 6960 §4.4.5 carries a CRL entry extension into an OCSP response Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
249
+ | `pki.schema.csr` | PKCS#10 certification requests (RFC 2986): subject DN, public key, requested attributes, signature. `decodeExtensions` is what a CA reads to see what a request asked for, returning the rows of its one `extensionRequest` attribute; a request carrying more than one is refused, and `groups: true` reads each separately Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
250
250
  | `pki.schema.pkcs8` | PKCS#8 private keys (RFC 5208 / 5958): algorithm, raw key bytes, attributes, optional public key. Encrypted keys are recognized but not decrypted. Fail-closed — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
251
251
  | `pki.schema.cms` | CMS (RFC 5652 / 5083 / 9629): SignedData (§5, signer infos plus raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds including RFC 5753 key agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface plus raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation). §11 attribute placement is enforced, countersignatures validate recursively, certificates and CRLs are validated against the closed CHOICE sets and kept raw, and every result is tagged `contentTypeName` Fail-closed — `parse`, `pemDecode`, `pemEncode` |
252
252
  | `pki.schema.ocsp` | OCSP requests and responses (RFC 6960): per-certificate status (good, revoked, unknown), responder identity, raw tbs bytes for external verification, certificates kept raw. A response's nonce, and a single response's archive cutoff (§4.4.4) and CRL reference (§4.4.2), are decoded onto their extension entries by name; a malformed one is refused. Non-basic response types are recognized but not decoded. Fail-closed — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
253
253
  | `pki.schema.tsp` | 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` |
254
- | `pki.schema.attrcert` | Attribute certificates (RFC 5755): holder and issuer identities as validated GeneralNames, the validity window as real `Date`s, and the privilege attributes and extensions decoded to structured values (role, clearance, service and access identity, group, charging identity; audit identity, target and proxy information, no-rev-avail, AA controls), with the raw signed region for a verifier. Unknown types stay opaque, and the obsolete v1 form is recognized and deferred Fail-closed — `parse`, `pemDecode`, `pemEncode` |
255
- | `pki.schema.crmf` | Certificate request messages (RFC 4211) — the CMP and EST enrollment body. The requested-certificate template (subject, public key, validity, extensions), proof of possession, and registration controls, with the raw `CertRequest` region surfaced for the caller to hash. Names are dual-accepted, IMPLICIT and EXPLICIT. This toolkit ships no proof-of-possession verifier yet, so confirming that a requester holds the key it asks to have certified remains the CA's own step Fail-closed — `parse`, `pemDecode`, `pemEncode` |
254
+ | `pki.schema.attrcert` | Attribute certificates (RFC 5755): holder and issuer identities as validated GeneralNames, the validity window as real `Date`s, and the privilege attributes and extensions decoded to structured values (role, clearance, service and access identity, group, charging identity; audit identity, target and proxy information, no-rev-avail, AA controls), with the raw signed region for a verifier. Unknown types stay opaque, and the obsolete v1 form is recognized and deferred. `decodeExtensions` returns the same row shape the other formats return, reporting the criticality RFC 5755 fixes for each extension it profiles Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
255
+ | `pki.schema.crmf` | Certificate request messages (RFC 4211) — the CMP and EST enrollment body. The requested-certificate template (subject, public key, validity, extensions), proof of possession, and registration controls, with the raw `CertRequest` region surfaced for the caller to hash. Names are dual-accepted, IMPLICIT and EXPLICIT. This toolkit ships no proof-of-possession verifier yet, so confirming that a requester holds the key it asks to have certified remains the CA's own step. `decodeExtensions` reads the extensions every message's template asks for, each row naming its message Fail-closed — `parse`, `pemDecode`, `pemEncode`, `decodeExtensions`, `decodeExtension` |
256
256
  | `pki.schema.pkcs12` | PKCS#12 (PFX) stores (RFC 7292) from DER, BER, or PEM: key bags via the PKCS#8 parser, shrouded keys with the algorithm surfaced and the ciphertext opaque, cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` and `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification. BER is accepted exactly where §4.1 requires it Fail-closed — `parse`, `pemDecode`, `pemEncode` |
257
257
  | `pki.schema.cmp` | CMP messages (RFC 9810): the header (version, sender and recipient including 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` and `bodyBytes` slices an external verifier reconstructs the protected part from. The CMP-before-OCSP dispatch order is enforced Fail-closed — `parse`, `pemDecode`, `pemEncode` |
258
258
  | `pki.schema.csrattrs` | EST CSR Attributes (`CsrAttrs`, RFC 8951 §3.5 / RFC 9908) — the `AttrOrOID` items a server sends to shape an enrollment: bare OIDs, attributes with raw values, and decoded views of the RFC 9908 meaningful types (extension requests, EC and RSA key-type conventions, the certification-request-info template). Unknown types are surfaced raw; structure and the RFC 9908 semantic MUSTs are fail-closed — `parse` |
package/lib/acme.js CHANGED
@@ -578,7 +578,11 @@ var OID_AKI = oid.byName("authorityKeyIdentifier");
578
578
  var OID_CN = oid.byName("commonName");
579
579
  var _extNs = pkix.makeNS("acme", AcmeError, oid);
580
580
  var _extDecoders = pkix.certExtensionDecoders(_extNs);
581
- var _extCtx = intrinsic.assign(intrinsic.create(null), { E: function (c, m, cause) { return new AcmeError(c, m, cause); }, oid: oid });
581
+ /** @internal The decoders take the value and nothing else: the error class and the OID registry
582
+ * they raise and resolve through come from the namespace the table was built with, above. Passing
583
+ * a second argument here suggested a per-call context that the table has no parameter for and
584
+ * silently dropped; where a decode genuinely needs resources, the way to supply them is to build
585
+ * the table with `schema.withResources`, as `schema-attrcert.js` does. */
582
586
 
583
587
  /**
584
588
  * @primitive pki.acme.tlsAlpn01Extension
@@ -657,7 +661,7 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
657
661
  if (!auth.equals(expected)) throw E("acme/bad-tlsalpn", "the acmeIdentifier digest does not match the key authorization");
658
662
  var sanExt = exts.filter(function (e) { return e.oid === OID_SAN; })[0];
659
663
  if (!sanExt) throw E("acme/bad-tlsalpn", "the validation certificate is missing the SubjectAltName");
660
- var san = _extDecoders.byOid[OID_SAN](sanExt.value, _extCtx);
664
+ var san = _extDecoders.byOid[OID_SAN](sanExt.value);
661
665
  if (!san.names || san.names.length !== 1) throw E("acme/bad-tlsalpn", "the SubjectAltName must carry EXACTLY ONE entry (RFC 8737 sec. 3)");
662
666
  var entry = san.names[0];
663
667
  if (!_isObject(identifier) || !_isString(identifier.type)) throw E("acme/bad-input", "an identifier { type, value } is required");
@@ -914,7 +918,7 @@ function _csrIdentifierSet(parsedCsr) {
914
918
  if (a.type !== oid.byName("extensionRequest") || !_isArrayI(a.extensions)) return;
915
919
  _forEach(a.extensions, function (e) {
916
920
  if (e.oid !== OID_SAN) return;
917
- var dec = _extDecoders.byOid[OID_SAN](e.value, _extCtx);
921
+ var dec = _extDecoders.byOid[OID_SAN](e.value);
918
922
  _forEach(dec.names || [], function (n) { _addSanName(set, n, "the finalize CSR"); });
919
923
  });
920
924
  });
@@ -981,7 +985,7 @@ function _certIdentifierSet(parsedCert) {
981
985
  var set = intrinsic.create(null);
982
986
  _forEach(parsedCert.extensions || [], function (e) {
983
987
  if (e.oid !== OID_SAN) return;
984
- var dec = _extDecoders.byOid[OID_SAN](e.value, _extCtx);
988
+ var dec = _extDecoders.byOid[OID_SAN](e.value);
985
989
  _forEach(dec.names || [], function (n) { _addSanName(set, n, "the downloaded certificate"); });
986
990
  });
987
991
  if (_objectKeys(set).length === 0) {
@@ -1249,7 +1253,7 @@ function ariCertId(certDer) {
1249
1253
  var cert = x509.parse(certDer);
1250
1254
  var akiExt = (cert.extensions || []).filter(function (e) { return e.oid === OID_AKI; })[0];
1251
1255
  if (!akiExt) throw E("acme/bad-certid", "the certificate has no authorityKeyIdentifier extension (RFC 9773 sec. 4.1)");
1252
- var aki = _extDecoders.byOid[OID_AKI](akiExt.value, _extCtx);
1256
+ var aki = _extDecoders.byOid[OID_AKI](akiExt.value);
1253
1257
  if (!aki || !Buffer.isBuffer(aki.keyIdentifier)) throw E("acme/bad-certid", "the authorityKeyIdentifier has no keyIdentifier field (RFC 9773 sec. 4.1)");
1254
1258
  var serialBytes = Buffer.from(cert.serialNumberHex, "hex");
1255
1259
  return jose.base64url.encode(aki.keyIdentifier) + "." + jose.base64url.encode(serialBytes);
@@ -69,18 +69,17 @@ var ATTR_OID_NAME = intrinsic.assign(intrinsic.create(null), {
69
69
  role: "role", clearance: "clearance", group: "group", chargingIdentity: "chargingIdentity",
70
70
  accessIdentity: "accessIdentity", authenticationInfo: "authenticationInfo",
71
71
  });
72
- var EXT_META = intrinsic.assign(intrinsic.create(null), {
73
- auditIdentity: { name: "acAuditIdentity", critical: true },
74
- targetInformation: { name: "targetInformation", critical: true },
75
- noRevAvail: { name: "noRevAvail", critical: false },
76
- aaControls: { name: "aaControls", critical: true },
77
- acProxying: { name: "acProxying", critical: true },
78
- authorityKeyIdentifier: { name: "authorityKeyIdentifier", critical: false },
79
- cRLDistributionPoints: { name: "cRLDistributionPoints", critical: false },
80
- authorityInfoAccess: { name: "authorityInfoAccess", critical: false },
81
- });
82
- var REQUIRED_CRITICALITY = intrinsic.create(null);
83
- Object.keys(EXT_META).forEach(function (k) { REQUIRED_CRITICALITY[O(EXT_META[k].name)] = EXT_META[k].critical; });
72
+ /** @internal The criticality each extension must carry is the schema module's table, so the shape
73
+ * this signer refuses to write and the deviation the extension door reports are one row set. What
74
+ * stays here is the friendly option name each extension is written under. */
75
+ var REQUIRED_CRITICALITY = attrcert.extensionCriticality(oid);
76
+ var EXT_META = intrinsic.create(null);
77
+ intrinsic.forEach([
78
+ ["auditIdentity", "acAuditIdentity"], ["targetInformation", "targetInformation"],
79
+ ["noRevAvail", "noRevAvail"], ["aaControls", "aaControls"], ["acProxying", "acProxying"],
80
+ ["authorityKeyIdentifier", "authorityKeyIdentifier"], ["cRLDistributionPoints", "cRLDistributionPoints"],
81
+ ["authorityInfoAccess", "authorityInfoAccess"],
82
+ ], function (r) { EXT_META[r[0]] = { name: r[1], critical: REQUIRED_CRITICALITY[O(r[1])].critical }; });
84
83
  var OID_AKI = O("authorityKeyIdentifier"), OID_OCSP = O("ocsp");
85
84
 
86
85
  /** @internal RFC 5755 sets rules on the issuer that its parser reads past and its verifier does
@@ -442,9 +441,9 @@ function _buildExtensions(extSpec, ctx) {
442
441
  var extnId = asn1.read.oid(n.children[0]);
443
442
  if (seenA[extnId]) throw _err("attrcert/bad-input", "duplicate extension " + extnId + " (RFC 5755 sec. 4.2.9)");
444
443
  seenA[extnId] = true;
445
- var required = REQUIRED_CRITICALITY[extnId];
446
- if (required != null && (n.children.length === 3) !== required) {
447
- throw _err("attrcert/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension must be marked " + (required ? "critical" : "non-critical") + " (RFC 5755)");
444
+ var fixed = intrinsic.hasOwn(REQUIRED_CRITICALITY, extnId) ? REQUIRED_CRITICALITY[extnId] : null;
445
+ if (fixed !== null && (n.children.length === 3) !== fixed.critical) {
446
+ throw _err("attrcert/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension must be marked " + (fixed.critical ? "critical" : "non-critical") + " (" + fixed.citation + ")");
448
447
  }
449
448
  var extnValue = asn1.read.octetString(n.children[n.children.length - 1]);
450
449
  var decoded;
package/lib/constants.js CHANGED
@@ -244,6 +244,11 @@ var NAMES = Object.assign(Object.create(null), {
244
244
  "8": "removeFromCRL", "9": "privilegeWithdrawn", "10": "aACompromise",
245
245
  },
246
246
  OCSP_STATUS: { "0": "successful", "1": "malformedRequest", "2": "internalError", "3": "tryLater", "5": "sigRequired", "6": "unauthorized" },
247
+ /** @internal The TLS ExtensionType values RFC 7633 sec. 4 names for the TLS feature extension. A
248
+ * value outside this table is printed as its number. */
249
+ TLS_FEATURE: { "5": "status_request", "17": "status_request_v2" },
250
+ /** @internal The IANA Address Family Numbers RFC 3779 sec. 2.2.3.3 names. */
251
+ ADDRESS_FAMILY: { "1": "IPv4", "2": "IPv6" },
247
252
  OBJECT_DIGEST_TYPE: { "0": "publicKey", "1": "publicKeyCert", "2": "otherObjectTypes" },
248
253
  ALGORITHM: {
249
254
  rsaEncryption: "rsaEncryption", rsassaPss: "rsassaPss", rsaesOaep: "rsaesOaep", ecPublicKey: "id-ecPublicKey",
@@ -60,7 +60,7 @@ function _isExtensionEntry(e) {
60
60
 
61
61
  function _isCrlExtensionEntry(e) {
62
62
  return !!e && typeof e.oid === "string" && _isOptName(e.name) &&
63
- typeof e.critical === "boolean" && e.value !== undefined;
63
+ typeof e.critical === "boolean" && e.value !== undefined && _isBuffer(e.valueBytes);
64
64
  }
65
65
 
66
66
  // @enforced-by guard-shape-reinlined -- shares the `accept` shape below: any door
package/lib/inspect.js CHANGED
@@ -134,6 +134,31 @@ function _keyBlock(spki, pad) {
134
134
  }
135
135
 
136
136
 
137
+ var TLS_FEATURE_NAMES = NAMES.TLS_FEATURE;
138
+ var AFI_NAMES = NAMES.ADDRESS_FAMILY;
139
+ var ipUtils = require("./ip-utils");
140
+ /** @internal An RFC 3779 IPAddress is a BIT STRING carrying only the leading bits of the address.
141
+ * What the bits it does not carry stand for depends on where the address sits: RFC 3779 sec. 2.1.2
142
+ * makes them zero in a range's `min` and ONE in its `max`, so a max written out zero-filled names
143
+ * a different address from the one the range ends at. `fillOnes` selects the rule, and it applies
144
+ * to the unused bits inside the last present octet as well as to the octets left off entirely. */
145
+ var AFI_OCTETS = Object.assign(Object.create(null), { 1: 4, 2: 16 });
146
+ function _ipAddressText(afi, addr, fillOnes) {
147
+ var width = AFI_OCTETS[afi];
148
+ if (!width || addr.bytes.length > width) return addr.bytes.toString("hex");
149
+ var padded = Buffer.alloc(width, fillOnes ? 0xff : 0x00);
150
+ addr.bytes.copy(padded, 0, 0, addr.bytes.length);
151
+ if (addr.unusedBits > 0 && addr.bytes.length > 0) {
152
+ var last = addr.bytes.length - 1;
153
+ var keep = (0xff << addr.unusedBits) & 0xff;
154
+ padded[last] = fillOnes ? (padded[last] | (~keep & 0xff)) : (padded[last] & keep);
155
+ }
156
+ return ipUtils.textFromOctets(padded) || padded.toString("hex");
157
+ }
158
+ /** @internal A prefix names a block, so it is written as its address and its length in bits. A
159
+ * range endpoint names one address and carries no length. */
160
+ function _ipPrefix(afi, addr) { return _ipAddressText(afi, addr, false) + "/" + addr.prefixLength; }
161
+
137
162
  var EXT_LABEL = NAMES.EXTENSION;
138
163
  var KU_LABEL = NAMES.KEY_USAGE;
139
164
  var EKU_LABEL = NAMES.EXT_KEY_USAGE;
@@ -388,8 +413,42 @@ var EXT_RENDERERS = Object.assign(Object.create(null), {
388
413
  }
389
414
  return akiLines.length ? akiLines.join("\n") : inner + "keyid:(none)";
390
415
  },
416
+ /** @internal RFC 7633 sec. 6 carries TLS ExtensionType values, so each is named from the registry
417
+ * where the toolkit knows it and printed as its number where it does not. */
418
+ tlsFeature: function (decoded, inner) {
419
+ return (decoded.features || []).map(function (f) {
420
+ var text = String(f);
421
+ var name = TLS_FEATURE_NAMES[text];
422
+ return inner + (name ? name + " (" + text + ")" : text);
423
+ }).join("\n") || inner + "(none)";
424
+ },
425
+ ipAddrBlocks: function (decoded, inner) {
426
+ return (decoded.families || []).map(function (f) {
427
+ var head = inner + (AFI_NAMES[f.afi] || "AFI " + f.afi) + (f.safi === null ? "" : " SAFI " + f.safi) + ":";
428
+ if (f.inherit) return head + " inherit";
429
+ var rows = (f.addressesOrRanges || []).map(function (r) {
430
+ if (r.kind === "addressPrefix") return inner + " " + _ipPrefix(f.afi, r.addressPrefix);
431
+ return inner + " " + _ipAddressText(f.afi, r.min, false) + " to " + _ipAddressText(f.afi, r.max, true);
432
+ });
433
+ return head + (rows.length ? "\n" + rows.join("\n") : " (none)");
434
+ }).join("\n") || inner + "(none)";
435
+ },
436
+ autonomousSysIds: function (decoded, inner) {
437
+ var lines = [];
438
+ [["asnum", decoded.asnum], ["rdi", decoded.rdi]].forEach(function (pair) {
439
+ if (pair[1] === null) return;
440
+ if (pair[1].inherit) { lines.push(inner + pair[0] + ": inherit"); return; }
441
+ lines.push(inner + pair[0] + ":");
442
+ (pair[1].asIdsOrRanges || []).forEach(function (r) {
443
+ lines.push(inner + " " + (r.kind === "id" ? String(r.id) : String(r.min) + " to " + String(r.max)));
444
+ });
445
+ });
446
+ return lines.length ? lines.join("\n") : inner + "(none)";
447
+ },
391
448
  });
392
449
  EXT_RENDERERS.msApplicationPolicies = EXT_RENDERERS.certificatePolicies;
450
+ EXT_RENDERERS.ipAddrBlocksV2 = EXT_RENDERERS.ipAddrBlocks;
451
+ EXT_RENDERERS.autonomousSysIdsV2 = EXT_RENDERERS.autonomousSysIds;
393
452
 
394
453
 
395
454
  function _renderExtValue(ext, decoded, inner) {
package/lib/lint.js CHANGED
@@ -1416,10 +1416,10 @@ var OID_REASON_CODE = oid.byName("reasonCode");
1416
1416
  * references itself and refuses a malformed one, so an entry for any of those would be dead. */
1417
1417
  var OCSP_SINGLE_VALUE_READER = Object.create(null);
1418
1418
  OCSP_SINGLE_VALUE_READER[OID_REASON_CODE] = function (bytes) {
1419
- crlSchema.decodeExtension({ oid: OID_REASON_CODE, name: "reasonCode", critical: false, value: bytes });
1419
+ crlSchema.decodeExt({ oid: OID_REASON_CODE, name: "reasonCode", critical: false, value: bytes });
1420
1420
  };
1421
1421
  OCSP_SINGLE_VALUE_READER[oid.byName("invalidityDate")] = function (bytes) {
1422
- crlSchema.decodeExtension({ oid: oid.byName("invalidityDate"), name: "invalidityDate", critical: false, value: bytes });
1422
+ crlSchema.decodeExt({ oid: oid.byName("invalidityDate"), name: "invalidityDate", critical: false, value: bytes });
1423
1423
  };
1424
1424
  OCSP_SINGLE_VALUE_READER[oid.byName("certificateIssuer")] = CRL_ENTRY_VALUE_READER[oid.byName("certificateIssuer")];
1425
1425
  var OCSP_RESPONSE_VALUE_READER = Object.create(null);
@@ -1444,7 +1444,7 @@ function _singleReasonCode(sr) {
1444
1444
  var e = _extIn(_ocspSingleExts(sr), OID_REASON_CODE);
1445
1445
  if (!e || !Buffer.isBuffer(e.value)) return null;
1446
1446
  // allow:swallow-unverified a value the CRL reader refuses carries no reason; the value-syntax row reports it
1447
- try { return crlSchema.decodeExtension(e).value; }
1447
+ try { return crlSchema.decodeExt(e).value; }
1448
1448
  catch (_e) { return null; }
1449
1449
  }
1450
1450
  /** @internal RFC 6960 sec. 2.2 gives a "revoked" answer for a non-issued certificate a fixed shape:
package/lib/schema-all.js CHANGED
@@ -345,10 +345,13 @@ function _detectOrNull(der, label) {
345
345
 
346
346
  module.exports = {
347
347
  engine: engine,
348
- x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
348
+ x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode,
349
+ decodeExtensions: x509.decodeExtensions, decodeExtension: x509.decodeExtension },
349
350
  c509: { parse: c509.parse, encode: c509.encode },
350
- crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode },
351
- csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
351
+ crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode,
352
+ decodeExtensions: crl.decodeExtensions, decodeExtension: crl.decodeExtension },
353
+ csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode,
354
+ decodeExtensions: csr.decodeExtensions, decodeExtension: csr.decodeExtension },
352
355
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
353
356
  pkcs1: {
354
357
  parse: pkcs1.parse, parsePublic: pkcs1.parsePublic,
@@ -361,10 +364,12 @@ module.exports = {
361
364
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
362
365
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode, pemEncode: ocsp.pemEncode },
363
366
  tsp: { parse: tsp.parse, parseResponse: tsp.parseResponse, parseRequest: tsp.parseRequest, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
364
- crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode },
367
+ crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode,
368
+ decodeExtensions: crmf.decodeExtensions, decodeExtension: crmf.decodeExtension },
365
369
  cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
366
370
  csrattrs: { parse: csrattrs.parse },
367
- attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode },
371
+ attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode, pemEncode: attrcert.pemEncode,
372
+ decodeExtensions: attrcert.decodeExtensions, decodeExtension: attrcert.decodeExtension },
368
373
  smime: { parseSigningCertificate: smime.parseSigningCertificate, parseSigningCertificateV2: smime.parseSigningCertificateV2, parseSmimeCapabilities: smime.parseSmimeCapabilities, decodeAttribute: smime.decodeAttribute },
369
374
  cmc: { parse: cmc.parse, parsePkiData: cmc.parsePkiData, parsePkiResponse: cmc.parsePkiResponse },
370
375
  pem: {
@@ -590,19 +590,9 @@ _AC_EXT_DECODERS[_Oav("acProxying")] = _acProxying;
590
590
  /** @internal The three RFC 5280 extensions RFC 5755 sec. 4.3.3 to 4.3.5 profile for an AC are read
591
591
  * through the certificate decoders, so a relying party running the sec. 6 "pointer in AC" scheme
592
592
  * reads the distribution point and the OCSP location off the parse rather than off raw bytes. */
593
- var _CERT_EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
594
- /** @internal The certificate decoders close over the namespace they were built with, so a parse
595
- * that named caps gets its own table and every decode below these three reads the caller's
596
- * figure. A caller outside a parse passes no context and reuses the module's table. */
597
- function _certExtDecoder(name) {
598
- var key = _Oav(name);
599
- return function (buf, ctx) {
600
- return (ctx ? pkix.certExtensionDecoders(schema.withResources(NS, ctx)).byOid : _CERT_EXT_DECODERS)[key](buf);
601
- };
602
- }
603
- _AC_EXT_DECODERS[_Oav("authorityKeyIdentifier")] = _certExtDecoder("authorityKeyIdentifier");
604
- _AC_EXT_DECODERS[_Oav("authorityInfoAccess")] = _certExtDecoder("authorityInfoAccess");
605
- _AC_EXT_DECODERS[_Oav("cRLDistributionPoints")] = _certExtDecoder("cRLDistributionPoints");
593
+ _AC_EXT_DECODERS[_Oav("authorityKeyIdentifier")] = pkix.scopedCertDecoder(NS, "authorityKeyIdentifier");
594
+ _AC_EXT_DECODERS[_Oav("authorityInfoAccess")] = pkix.scopedCertDecoder(NS, "authorityInfoAccess");
595
+ _AC_EXT_DECODERS[_Oav("cRLDistributionPoints")] = pkix.scopedCertDecoder(NS, "cRLDistributionPoints");
606
596
 
607
597
  // @internal
608
598
  function validateAttributeValue(type, valueDer) {
@@ -614,6 +604,108 @@ function validateExtensionValue(extnOid, extnValueDer) {
614
604
  return dec ? dec(extnValueDer) : null;
615
605
  }
616
606
 
607
+ /** @internal The criticality RFC 5755 fixes for each extension it profiles for an attribute
608
+ * certificate. The AC signer refuses to write an extension against this table and the extension
609
+ * door reports one that arrived against it, from this one row set. */
610
+ var AC_FIXED_CRITICALITY_ROWS = [
611
+ ["acAuditIdentity", true, "RFC 5755 sec. 4.3.1"], ["targetInformation", true, "RFC 5755 sec. 4.3.2"],
612
+ ["authorityKeyIdentifier", false, "RFC 5755 sec. 4.3.3"], ["authorityInfoAccess", false, "RFC 5755 sec. 4.3.4"],
613
+ ["cRLDistributionPoints", false, "RFC 5755 sec. 4.3.5"], ["noRevAvail", false, "RFC 5755 sec. 4.3.6"],
614
+ ["acProxying", true, "RFC 5755 sec. 7.3"], ["aaControls", true, "RFC 5755 sec. 7.4"],
615
+ ];
616
+ function extensionCriticality(oidRegistry) {
617
+ var table = intrinsic.create(null);
618
+ intrinsic.forEach(AC_FIXED_CRITICALITY_ROWS, function (r) {
619
+ table[oidRegistry.byName(r[0])] = { critical: r[1], citation: r[2] };
620
+ });
621
+ return table;
622
+ }
623
+
624
+ /** @internal The attribute-certificate scope. The table is this module's own, passed to the shared
625
+ * door rather than reached for from it, because a registry in `schema-pkix.js` that required this
626
+ * module would invert the dependency. */
627
+ var AC_SCOPES = intrinsic.assign(intrinsic.create(null), {
628
+ "attribute-certificate": {
629
+ /** @internal Every decoder in this table takes the walk context and threads it into its own
630
+ * decode, so the one table serves every call and the caller's caps still reach the value. */
631
+ decoders: function () { return _AC_EXT_DECODERS; },
632
+ criticality: extensionCriticality(oid),
633
+ },
634
+ });
635
+
636
+ var DOOR = pkix.extensionDoor({
637
+ ns: NS, ErrorClass: AttrCertError, key: "attrcert", kind: "attributeCertificate",
638
+ what: "the attribute certificate", parse: parse, scopes: AC_SCOPES,
639
+ defaultScope: "attribute-certificate",
640
+ segments: function (ac) { return [{ list: ac.extensions || [], scope: "attribute-certificate" }]; },
641
+ });
642
+
643
+ /**
644
+ * @primitive pki.schema.attrcert.decodeExtensions
645
+ * @signature pki.schema.attrcert.decodeExtensions(acOrBytes, opts?) -> rows
646
+ * @since 0.8.11
647
+ * @status stable
648
+ * @spec RFC 5755 sec. 4.2.9, RFC 5755 sec. 4.3
649
+ * @related pki.schema.attrcert.parse, pki.schema.x509.decodeExtensions
650
+ *
651
+ * Read an attribute certificate's extensions as decoded records. The argument is the object `parse`
652
+ * returned or the bytes `parse` takes.
653
+ *
654
+ * Each row is `{ oid, name, critical, value, scope, index, containerIndex, state, decoded, code,
655
+ * profile }`, with `scope` `"attribute-certificate"` and `containerIndex` `null`. `state` is the
656
+ * discriminator, and `profile` reports the criticality RFC 5755 fixes for the extension.
657
+ *
658
+ * RFC 5755 sec. 5 item 7 has a verifier reject an AC carrying a critical extension it does not
659
+ * recognize. That is the caller's rule to apply, and the two facts it needs, `critical` and
660
+ * `state`, are on the row.
661
+ *
662
+ * @opts
663
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
664
+ * undecodable value. It does not turn an unrecognized OID into a throw.
665
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
666
+ * @example
667
+ * var pair = await pki.key.generate("Ed25519");
668
+ * var key = await pki.key.export(pair.privateKey);
669
+ * var aaCert = await pki.x509.sign({ subject: "Example AA", subjectPublicKey: await pki.key.export(pair.publicKey),
670
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
671
+ * var acDer = await pki.attrcert.sign({ holder: { entityName: [{ directoryName: "CN=Holder" }] },
672
+ * attributes: { role: { roleName: "https://example.test/role/admin" } }, serialNumber: 1n,
673
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
674
+ * extensions: { noRevAvail: true } }, { cert: aaCert, key: key });
675
+ * var rows = pki.schema.attrcert.decodeExtensions(acDer);
676
+ * rows.filter(function (r) { return r.critical && r.state === "unrecognized"; }).length; // -> 0
677
+ */
678
+ var decodeExtensions = DOOR.decodeExtensions;
679
+
680
+ /**
681
+ * @primitive pki.schema.attrcert.decodeExtension
682
+ * @signature pki.schema.attrcert.decodeExtension(ext, opts?) -> row
683
+ * @since 0.8.11
684
+ * @status stable
685
+ * @spec RFC 5755 sec. 4.3
686
+ * @related pki.schema.attrcert.decodeExtensions
687
+ *
688
+ * Read one attribute-certificate extension record as a decoded row, at scope
689
+ * `"attribute-certificate"`. The argument is `{ oid, name, critical, value }`.
690
+ *
691
+ * @opts
692
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
693
+ * undecodable value.
694
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
695
+ * @example
696
+ * var pair = await pki.key.generate("Ed25519");
697
+ * var key = await pki.key.export(pair.privateKey);
698
+ * var aaCert = await pki.x509.sign({ subject: "Example AA", subjectPublicKey: await pki.key.export(pair.publicKey),
699
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
700
+ * var acDer = await pki.attrcert.sign({ holder: { entityName: [{ directoryName: "CN=Holder" }] },
701
+ * attributes: { role: { roleName: "https://example.test/role/admin" } }, serialNumber: 1n,
702
+ * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
703
+ * extensions: { noRevAvail: true } }, { cert: aaCert, key: key });
704
+ * var ac = pki.schema.attrcert.parse(acDer);
705
+ * pki.schema.attrcert.decodeExtension(ac.extensions[0]).profile.citation; // -> "RFC 5755 sec. 4.3.6"
706
+ */
707
+ var decodeExtension = DOOR.decodeExtension;
708
+
617
709
  module.exports = {
618
710
  parse: parse,
619
711
  parseV1: parseV1,
@@ -621,6 +713,9 @@ module.exports = {
621
713
  pemEncode: pemEncode,
622
714
  matches: matches,
623
715
  matchesV1: matchesV1,
716
+ decodeExtensions: decodeExtensions,
717
+ decodeExtension: decodeExtension,
718
+ extensionCriticality: extensionCriticality,
624
719
  readGeneralName: readGeneralName,
625
720
  validateAttributeValue: validateAttributeValue,
626
721
  validateExtensionValue: validateExtensionValue,