@blamejs/pki 0.1.15 → 0.1.17

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,35 @@ 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.1.17 — 2026-07-07
8
+
9
+ An RFC 4211 certificate-request-message parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.crmf.parse(input) — RFC 4211 CertReqMessages parsing. It decodes a DER Buffer or PEM into { messages: [ { certReq, popo, regInfo } ] }, where each certReq is { certReqId, certReqIdHex, certTemplate, controls, certReqBytes } and certTemplate carries the requestable fields version, issuer, validity, subject, publicKey, and extensions (each null when absent). The fields RFC 4211 §5 requires a request to omit — serialNumber and signingAlg (assigned by the CA) and issuerUID and subjectUID (deprecated) — are rejected, not returned, so a requester cannot dictate a CA-assigned value. issuer and subject Names are accepted in both the EXPLICIT and the IMPLICIT wire encodings; the OptionalValidity times are EXPLICIT UTCTime or GeneralizedTime; a supplied CertTemplate version must be 2; certReqId is an unbounded signed integer. popo is null, a raVerified marker, a decoded signature proof (with its poposkInput and signature surfaced raw), or a raw key-encipherment / key-agreement arm; for a signature proof, poposkInput's presence is checked against the template per §4.1. certReqBytes is the exact CertRequest byte range a proof-of-possession verifier hashes. Malformed input fails closed with a typed crmf/* or asn1/* code.
14
+ - pki.schema.crmf.pemDecode(text, label?) — extract the DER bytes from a PEM block (the first block unless a label is given).
15
+ - The OID registry gains the RFC 4211 registration-control (id-regCtrl) and registration-info (id-regInfo) identifiers on the id-pkip arc, so a parsed control or info entry resolves to its name (oldCertID, pkiArchiveOptions, utf8Pairs, and the rest).
16
+ - The error taxonomy gains CrmfError, carrying a stable crmf/* code.
17
+ - pki.schema.engine.encode(schema, value) — the constructor direction of the schema engine. A single declarative schema now drives both decode (walk, bytes to value) and encode (canonical DER, value to bytes): every leaf carries a read and a write, and EXPLICIT wrapping and IMPLICIT context-tag retagging are applied in one place, so an encoder can no longer emit a different tag than the decoder reads. A round-trip test proves walk(decode(encode(value))) recovers the value across universal, IMPLICIT, EXPLICIT, and SET-OF-ordered shapes, and the CRMF request format is proven to round-trip end to end.
18
+
19
+ ### Changed
20
+
21
+ - pki.schema.ocsp request and response parsing (parseRequest, parseResponse, pemDecode) is now stable.
22
+ - An experimental primitive is surfaced for a graduation review once it has been experimental for a fixed number of releases; the review is recorded as a graduation to stable or a dated keep-experimental decision, so the experimental-to-stable-to-deprecated transition is driven on a schedule (see LTS-CALENDAR.md).
23
+
24
+ ## v0.1.16 — 2026-07-06
25
+
26
+ Certification path validation joins the toolkit — RFC 5280 section 6, as a pure re-entrant algorithm.
27
+
28
+ ### Added
29
+
30
+ - pki.path.validate(path, opts) — RFC 5280 section 6 certification-path validation. It validates an ordered array of pki.schema.x509.parse certificates (or DER/PEM it parses) against a trust anchor, running the section 6.1 state machine: section 6.1.3(a)(1) signature chaining, the always-on section 6.1.3(a)(2) validity window with the check date an explicit input, section 6.1.3(a)(4) name chaining, section 6.1.3(b,c)/6.1.4(g) name constraints (directoryName, dNSName, rfc822Name including an emailAddress carried in the subject DN, uniformResourceIdentifier, and iPAddress with the address-and-mask subtree form), section 6.1.4(k) basic constraints as the single authoritative CA gate, section 6.1.4(l,m) path length, section 6.1.4(n) keyUsage keyCertSign, and the section 6.1.3(d)/6.1.4(a,b,i,j)/6.1.5 certificate-policy tree with its explicit-policy, policy-mapping, and inhibit-any-policy counters. It returns { valid, path, results, workingPublicKey, workingPublicKeyAlgorithm, workingPublicKeyParameters, validPolicyTree } where results[i].checks carries a stable path/* reason code per check. Validation is pure and re-entrant. An unrecognized critical extension, an undetermined revocation status, or a structural fault fails the path with a typed code.
31
+ - pki.path.crlChecker(crls) — a CRL-backed revocation checker for the validate revocationChecker option, composing pki.schema.crl.parse. For each certificate it consults every CRL issued by the certificate's issuer (so a clean CRL cannot shadow a revoking one), verifies each CRL signature over its tbsBytes, honors the issuing-distribution-point scope and reason coverage, checks thisUpdate/nextUpdate currency, and requires the CRL signer to assert keyUsage cRLSign; a certificate listed in any authoritative in-scope CRL is revoked, and an issuer with no authoritative in-scope CRL yields an undetermined status, which the validator fails closed unless softFail is set. An OCSP checker satisfies the same interface.
32
+ - pki.schema.pkix gains the ns-parameterized RFC 5280 section 4.2.1 extension-value decoders (pkix.certExtensionDecoders) — basicConstraints, keyUsage, nameConstraints, certificatePolicies, policyMappings, policyConstraints, inhibitAnyPolicy, subjectAltName / issuerAltName, extKeyUsage, and authorityKeyIdentifier / subjectKeyIdentifier — each turning a raw extension value into a validated structure or a typed error, fail-closed. The shared GeneralName validator gains a decoded-value mode (surfacing the IA5 text, the IP octets, or the directoryName as a structured name alongside the raw bytes) and an address-and-mask subtree-base mode for name-constraint bases; both are opt-in, so the existing callers are byte-identical.
33
+ - The OID registry gains the RFC 5280 policy and wildcard extension identifiers used by path validation: policyMappings, policyConstraints, and inhibitAnyPolicy, plus the anyPolicy and anyExtendedKeyUsage special-OID leaves.
34
+ - The error taxonomy gains PathError, carrying the per-check reason in its stable path/* code.
35
+
7
36
  ## v0.1.15 — 2026-07-06
8
37
 
9
38
  CMS EnvelopedData and EncryptedData join the parser, and every documentation example is now executed as a test.
package/README.md CHANGED
@@ -199,9 +199,11 @@ is callable today; nothing below is a stub.
199
199
  | `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` |
200
200
  | `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — 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`, `parseTstInfo`, `parseToken`, `pemDecode` |
201
201
  | `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 / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode` |
202
+ | `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` |
202
203
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
204
+ | `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
203
205
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
204
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError`, each carrying a stable `code` in `domain/reason` form |
206
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `PathError`, each carrying a stable `code` in `domain/reason` form |
205
207
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
206
208
 
207
209
  ### CLI
package/index.js CHANGED
@@ -29,6 +29,7 @@ var asn1 = require("./lib/asn1-der");
29
29
  var oid = require("./lib/oid");
30
30
  var webcrypto = require("./lib/webcrypto");
31
31
  var schema = require("./lib/schema-all");
32
+ var path = require("./lib/path-validate");
32
33
 
33
34
  module.exports = {
34
35
  version: constants.version,
@@ -42,6 +43,9 @@ module.exports = {
42
43
  // `schema` is the family: the L2 structure-schema engine (schema.engine) and
43
44
  // the per-format parsers (schema.x509, …) with detect-and-route schema.parse.
44
45
  schema: schema,
46
+ // `path` is RFC 5280 §6 certification-path validation — pki.path.validate
47
+ // runs the §6.1 state machine over an already-parsed path + a trust anchor.
48
+ path: path,
45
49
  // A ready W3C Crypto instance (globalThis.crypto shape) with the classes for
46
50
  // constructing more attached under the same namespace (pki.webcrypto.CryptoKey,
47
51
  // .SubtleCrypto, .Crypto, .WebCryptoError). PQC-first, classical-capable, zero-dep.
package/lib/asn1-der.js CHANGED
@@ -627,6 +627,18 @@ function encodeLength(n) {
627
627
  return Buffer.concat([Buffer.from([0x80 | bytes.length]), Buffer.from(bytes)]);
628
628
  }
629
629
 
630
+ // The DER of a universal SEQUENCE recovered from a decoded IMPLICIT [n] SEQUENCE
631
+ // `node`, whose context tag replaced the universal 0x30 on the wire (e.g. an RFC
632
+ // 4211 CertTemplate publicKey [6] SubjectPublicKeyInfo, or a POPOSigningKey
633
+ // poposkInput [0]): the field's value is imported / signed as a standalone SEQUENCE,
634
+ // not as the context-tagged wire form. A constructed node exposes its content as
635
+ // `children` (not `content`), so the content is those children's DER concatenated;
636
+ // the identifier becomes the SEQUENCE tag and the length is canonical DER.
637
+ function sequenceTlv(node) {
638
+ var content = node.content != null ? node.content : Buffer.concat((node.children || []).map(function (c) { return c.bytes; }));
639
+ return Buffer.concat([Buffer.from([TAGS.SEQUENCE | CONSTRUCTED_BIT]), encodeLength(content.length), content]);
640
+ }
641
+
630
642
  function encodeIdentifier(classBits, constructed, tagNumber) {
631
643
  var lead = classBits | (constructed ? CONSTRUCTED_BIT : 0);
632
644
  if (tagNumber < 0x1f) return Buffer.from([lead | tagNumber]);
@@ -848,6 +860,7 @@ module.exports = {
848
860
  encode: encodeTLV,
849
861
  encodeTLV: encodeTLV,
850
862
  encodeLength: encodeLength,
863
+ sequenceTlv: sequenceTlv,
851
864
  encodeIdentifier: encodeIdentifier,
852
865
  decodeOidContent: decodeOidContent,
853
866
  encodeOidContent: encodeOidContent,
package/lib/constants.js CHANGED
@@ -117,6 +117,10 @@ var LIMITS = {
117
117
  PEM_MAX_BYTES: BYTES.mib(16),
118
118
  DER_MAX_INTEGER_BYTES: BYTES.kib(16),
119
119
  OID_MAX_SUBIDENTIFIER_BYTES: 32,
120
+ // Certification-path length ceiling: bounds the per-cert asymmetric verify
121
+ // work on an untrusted certificate bundle (a real chain is well under this;
122
+ // the operator may override via opts.maxPathCerts).
123
+ PATH_MAX_CERTS: 100,
120
124
  };
121
125
 
122
126
  // Single-sourced from the package manifest so the reported version can
@@ -155,6 +155,18 @@ var TspError = defineClass("TspError", { withCause: true });
155
155
  // underlying leaf fault as `.cause`.
156
156
  var AttrCertError = defineClass("AttrCertError", { withCause: true });
157
157
 
158
+ // CrmfError — a byte sequence that is not a well-formed RFC 4211 CertReqMessages
159
+ // / CertReqMsg / CertRequest / CertTemplate. Carries the underlying leaf fault as
160
+ // `.cause`.
161
+ var CrmfError = defineClass("CrmfError", { withCause: true });
162
+
163
+ // PathError — a certification path that fails RFC 5280 §6 validation, or a
164
+ // malformed input handed to the validator (an extension value that does not
165
+ // decode, an empty path, an unsupported signature algorithm). Carries the
166
+ // per-check reason in `.code` (`path/*`) and the underlying leaf fault as
167
+ // `.cause`.
168
+ var PathError = defineClass("PathError", { withCause: true });
169
+
158
170
  module.exports = {
159
171
  PkiError: PkiError,
160
172
  defineClass: defineClass,
@@ -171,4 +183,6 @@ module.exports = {
171
183
  OcspError: OcspError,
172
184
  TspError: TspError,
173
185
  AttrCertError: AttrCertError,
186
+ CrmfError: CrmfError,
187
+ PathError: PathError,
174
188
  };
package/lib/oid.js CHANGED
@@ -61,8 +61,12 @@ var FAMILIES = {
61
61
  cRLNumber: 20, reasonCode: 21, invalidityDate: 24, deltaCRLIndicator: 27,
62
62
  issuingDistributionPoint: 28, certificateIssuer: 29,
63
63
  nameConstraints: 30, cRLDistributionPoints: 31,
64
- certificatePolicies: 32, authorityKeyIdentifier: 35, extKeyUsage: 37,
65
- freshestCRL: 46,
64
+ certificatePolicies: 32, policyMappings: 33, authorityKeyIdentifier: 35,
65
+ policyConstraints: 36, extKeyUsage: 37, freshestCRL: 46,
66
+ inhibitAnyPolicy: 54,
67
+ // The two special-OID leaves under extension arcs (RFC 5280 §4.2.1.4 /
68
+ // §4.2.1.12): the certificate-policies wildcard and the EKU wildcard.
69
+ anyPolicy: [32, 0], anyExtendedKeyUsage: [37, 0],
66
70
  // RFC 5755 attribute-certificate extensions (§4.3.2, §4.3.6).
67
71
  targetInformation: 55, noRevAvail: 56 } },
68
72
 
@@ -99,6 +103,14 @@ var FAMILIES = {
99
103
  ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
100
104
  ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
101
105
 
106
+ // CRMF (RFC 4211) registration controls (§6) and registration info (§7) on the
107
+ // id-pkip arc (id-pkix 5). id-regCtrl (id-pkip 1) names the control types a
108
+ // CertRequest carries; id-regInfo (id-pkip 2) names the registration-info types.
109
+ // The parser surfaces each control/info value RAW keyed by these names.
110
+ regCtrl: { base: [1, 3, 6, 1, 5, 5, 7, 5, 1], of: {
111
+ regToken: 1, authenticator: 2, pkiPublicationInfo: 3, pkiArchiveOptions: 4, oldCertID: 5, protocolEncrKey: 6 } },
112
+ regInfo: { base: [1, 3, 6, 1, 5, 5, 7, 5, 2], of: { utf8Pairs: 1, certReq: 2 } },
113
+
102
114
  // PKIX extended key purposes (id-kp, RFC 5280 §4.2.1.12). timeStamping is required
103
115
  // — critical and sole — on an RFC 3161 TSA signing certificate (§2.3).
104
116
  pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
@@ -106,7 +118,8 @@ var FAMILIES = {
106
118
 
107
119
  // PKCS#1 RSA public-key + RSASSA signature algorithms.
108
120
  rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
109
- rsaEncryption: 1, rsassaPss: 10, sha256WithRSAEncryption: 11,
121
+ rsaEncryption: 1, rsaesOaep: 7, mgf1: 8, rsassaPss: 10,
122
+ sha256WithRSAEncryption: 11,
110
123
  sha384WithRSAEncryption: 12, sha512WithRSAEncryption: 13 } },
111
124
 
112
125
  // PKCS#7 / CMS content types (RFC 5652 §4, RFC 2315). id-signedData is the one
@@ -121,7 +134,7 @@ var FAMILIES = {
121
134
  challengePassword: 7, extensionRequest: 14 } },
122
135
 
123
136
  // S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
124
- smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4 } },
137
+ smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4, encKeyWithID: 21 } },
125
138
 
126
139
  // S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
127
140
  // signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;