@blamejs/pki 0.1.16 → 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,23 @@ 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
+
7
24
  ## v0.1.16 — 2026-07-06
8
25
 
9
26
  Certification path validation joins the toolkit — RFC 5280 section 6, as a pure re-entrant algorithm.
package/README.md CHANGED
@@ -199,10 +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 |
203
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` |
204
205
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
205
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `PathError`, 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 |
206
207
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
207
208
 
208
209
  ### CLI
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,
@@ -155,6 +155,11 @@ 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
+
158
163
  // PathError — a certification path that fails RFC 5280 §6 validation, or a
159
164
  // malformed input handed to the validator (an extension value that does not
160
165
  // decode, an empty path, an unsupported signature algorithm). Carries the
@@ -178,5 +183,6 @@ module.exports = {
178
183
  OcspError: OcspError,
179
184
  TspError: TspError,
180
185
  AttrCertError: AttrCertError,
186
+ CrmfError: CrmfError,
181
187
  PathError: PathError,
182
188
  };
package/lib/oid.js CHANGED
@@ -103,6 +103,14 @@ var FAMILIES = {
103
103
  ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
104
104
  ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
105
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
+
106
114
  // PKIX extended key purposes (id-kp, RFC 5280 §4.2.1.12). timeStamping is required
107
115
  // — critical and sole — on an RFC 3161 TSA signing certificate (§2.3).
108
116
  pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
@@ -126,7 +134,7 @@ var FAMILIES = {
126
134
  challengePassword: 7, extensionRequest: 14 } },
127
135
 
128
136
  // S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
129
- 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 } },
130
138
 
131
139
  // S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
132
140
  // signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;
package/lib/schema-all.js CHANGED
@@ -39,6 +39,7 @@ var pkcs8 = require("./schema-pkcs8");
39
39
  var cms = require("./schema-cms");
40
40
  var ocsp = require("./schema-ocsp");
41
41
  var tsp = require("./schema-tsp");
42
+ var crmf = require("./schema-crmf");
42
43
  var attrcert = require("./schema-attrcert");
43
44
  var frameworkError = require("./framework-error");
44
45
 
@@ -83,6 +84,19 @@ var FORMATS = [
83
84
  detect: tsp.matches,
84
85
  parse: function (input) { return tsp.parse(input); },
85
86
  },
87
+ {
88
+ // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (RFC 4211 §3) — a
89
+ // SEQUENCE whose first CertReqMsg's CertRequest leads with a universal INTEGER
90
+ // (certReqId) then a universal SEQUENCE (certTemplate). Leads with a SEQUENCE
91
+ // like the signed-envelope trio and the ocsp-request, but the INTEGER-then-
92
+ // SEQUENCE pair at that depth is disjoint from both. Registered AHEAD of
93
+ // ocsp-request: a single-message CertReqMessages is the only remaining overlap
94
+ // with the ocsp-request detector, and crmf's deeper probe is the more specific.
95
+ name: "crmf",
96
+ module: crmf,
97
+ detect: crmf.matches,
98
+ parse: function (input) { return crmf.parse(input); },
99
+ },
86
100
  {
87
101
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
88
102
  // OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
@@ -179,7 +193,7 @@ var FORMATS = [
179
193
  * The names of every registered format, in detection order.
180
194
  *
181
195
  * @example
182
- * pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
196
+ * pki.schema.all(); // → ["cms", "tsp", "crmf", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
183
197
  */
184
198
  function all() { return FORMATS.map(function (f) { return f.name; }); }
185
199
 
@@ -224,6 +238,7 @@ module.exports = {
224
238
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
225
239
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
226
240
  tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
241
+ crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode },
227
242
  attrcert: { parse: attrcert.parse, pemDecode: attrcert.pemDecode },
228
243
  all: all,
229
244
  parse: parse,
package/lib/schema-cms.js CHANGED
@@ -618,7 +618,7 @@ var CONTENT_INFO = schema.seq([
618
618
  * @primitive pki.schema.cms.parse
619
619
  * @signature pki.schema.cms.parse(input) -> signedData
620
620
  * @since 0.1.10
621
- * @status experimental
621
+ * @status stable
622
622
  * @spec RFC 5652
623
623
  * @related pki.schema.parse, pki.schema.x509.parse
624
624
  *
@@ -642,7 +642,7 @@ var parse = pkix.makeParser({ pemLabel: "CMS", PemError: PemError, ErrorClass: C
642
642
  * @primitive pki.schema.cms.pemDecode
643
643
  * @signature pki.schema.cms.pemDecode(text, label?) -> Buffer
644
644
  * @since 0.1.10
645
- * @status experimental
645
+ * @status stable
646
646
  * @spec RFC 7468, RFC 5652
647
647
  * @related pki.schema.cms.parse
648
648
  *
@@ -658,7 +658,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMS", Pe
658
658
  * @primitive pki.schema.cms.pemEncode
659
659
  * @signature pki.schema.cms.pemEncode(der, label?) -> string
660
660
  * @since 0.1.10
661
- * @status experimental
661
+ * @status stable
662
662
  * @spec RFC 7468
663
663
  * @related pki.schema.cms.pemDecode
664
664
  *
@@ -684,9 +684,18 @@ function matches(root) {
684
684
  return true;
685
685
  }
686
686
 
687
+ // Validate a bare EnvelopedData (RFC 5652 §6.1) — a universal SEQUENCE node —
688
+ // returning its structured value; throws a typed cms/* error on a malformed
689
+ // structure. Exposed for a composer that carries a bare EnvelopedData OUTSIDE a
690
+ // ContentInfo (an RFC 4211 CRMF encryptedKey [4] POPOPrivKey arm), which the
691
+ // content-type dispatch in `parse` cannot reach. The node MUST already be a
692
+ // universal SEQUENCE (an IMPLICIT [n] EnvelopedData is retagged by the caller).
693
+ function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).result; }
694
+
687
695
  module.exports = {
688
696
  parse: parse,
689
697
  pemDecode: pemDecode,
690
698
  pemEncode: pemEncode,
691
699
  matches: matches,
700
+ walkEnvelopedData: walkEnvelopedData,
692
701
  };
package/lib/schema-crl.js CHANGED
@@ -190,7 +190,7 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
190
190
  * @primitive pki.schema.crl.parse
191
191
  * @signature pki.schema.crl.parse(input) -> crl
192
192
  * @since 0.1.7
193
- * @status experimental
193
+ * @status stable
194
194
  * @spec RFC 5280
195
195
  * @related pki.schema.x509.parse, pki.schema.parse
196
196
  *
@@ -210,7 +210,7 @@ var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorCla
210
210
  * @primitive pki.schema.crl.pemDecode
211
211
  * @signature pki.schema.crl.pemDecode(text, label?) -> Buffer
212
212
  * @since 0.1.7
213
- * @status experimental
213
+ * @status stable
214
214
  * @spec RFC 7468, RFC 5280
215
215
  * @related pki.schema.crl.parse
216
216
  *
@@ -0,0 +1,487 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.crmf
6
+ * @nav Schema
7
+ * @title CRMF
8
+ * @order 180
9
+ * @slug crmf
10
+ *
11
+ * @intro
12
+ * Certificate Request Message Format handling per RFC 4211. `parse` decodes a
13
+ * `CertReqMessages` — the request body CMP and EST enrollment carry — into an
14
+ * array of messages, each with its `CertRequest` (certReqId, a `CertTemplate`
15
+ * of the requested certificate fields, and any registration controls), an
16
+ * optional proof-of-possession, and optional registration info.
17
+ *
18
+ * RFC 4211 Appendix B is an IMPLICIT TAGS module, so the whole `CertTemplate`
19
+ * body is one ascending run of IMPLICIT context tags `[0]`..`[9]`, every field
20
+ * OPTIONAL. Two fields break IMPLICIT because their base type is a CHOICE and
21
+ * X.680 forces EXPLICIT: `issuer [3]` / `subject [5]` are `Name`, dual-accepted
22
+ * here (the standards-compliant EXPLICIT encoding and the dominant IMPLICIT one
23
+ * real tooling emits); and the `OptionalValidity` times are EXPLICIT. version,
24
+ * when supplied, MUST be 2 (RFC 4211 §5); certReqId is an unbounded signed
25
+ * INTEGER (the RFC 9483 `-1` sentinel is legal). The `CertRequest` byte range
26
+ * the proof-of-possession signature covers, and each `poposkInput`, are surfaced
27
+ * RAW for a downstream verifier; registration controls / info values and the
28
+ * keyEncipherment / keyAgreement POP arms are surfaced RAW rather than recursed.
29
+ * DER-only, fail-closed.
30
+ *
31
+ * @card
32
+ * Parse DER / PEM RFC 4211 CertReqMessages into requested-certificate templates,
33
+ * proof-of-possession, and registration controls — dual-accepted names, raw
34
+ * verifier inputs, fail-closed.
35
+ */
36
+
37
+ var asn1 = require("./asn1-der");
38
+ var schema = require("./schema-engine");
39
+ var pkix = require("./schema-pkix");
40
+ var cms = require("./schema-cms");
41
+ var oid = require("./oid");
42
+ var frameworkError = require("./framework-error");
43
+
44
+ var CrmfError = frameworkError.CrmfError;
45
+ var PemError = frameworkError.PemError;
46
+
47
+ var NS = pkix.makeNS("crmf", CrmfError, oid);
48
+
49
+ var TAGS = asn1.TAGS;
50
+
51
+ // ---- dual-accept Name (issuer [3] / subject [5]) ---------------------
52
+ // RFC 4211 Appendix B is IMPLICIT TAGS, but Name is a CHOICE so X.680 §31.2.7
53
+ // forces EXPLICIT tagging — the standards-compliant wire is a context [tag]
54
+ // EXPLICIT wrapper around a universal RDNSequence. The dominant tooling
55
+ // (pyasn1-modules, BouncyCastle) instead encodes IMPLICIT (the [tag] REPLACES the
56
+ // RDNSequence 0x30, so the children ARE the RDN SETs). Both occur in real CMP /
57
+ // EST, so accept either, disambiguated by the first inner element's tag: a
58
+ // universal SET leads the IMPLICIT form; a single universal SEQUENCE is the
59
+ // EXPLICIT wrapper. Anything else fails closed.
60
+ function crmfName(tag) {
61
+ var NAME = pkix.name(NS); // EXPLICIT arm walks a universal RDNSequence
62
+ var INAME = pkix.name(NS, { implicitTag: tag }); // IMPLICIT arm: [tag] children ARE the RDN SETs
63
+ return schema.decode(function (n, ctx) {
64
+ if (n.tagClass !== "context" || n.tagNumber !== tag || !n.children) {
65
+ throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] must be a Name (RFC 4211 §5)");
66
+ }
67
+ if (n.children.length === 0) return schema.walk(INAME, n, ctx).result; // IMPLICIT empty RDNSequence
68
+ var c0 = n.children[0];
69
+ if (c0.tagClass === "universal" && c0.tagNumber === TAGS.SET) {
70
+ return schema.walk(INAME, n, ctx).result; // IMPLICIT: children ARE RDN SETs
71
+ }
72
+ if (n.children.length === 1 && c0.tagClass === "universal" && c0.tagNumber === TAGS.SEQUENCE) {
73
+ return schema.walk(NAME, c0, ctx).result; // EXPLICIT: [tag] wraps RDNSequence
74
+ }
75
+ throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] Name must be an IMPLICIT RDNSequence (SET-led) or an EXPLICIT-wrapped RDNSequence");
76
+ }, function (value) {
77
+ // encode the IMPLICIT form (the dominant wire encoding): the [tag] whose children
78
+ // ARE the RDN SETs, built from the decoded `rdns` via the shared Name schema.
79
+ return schema.encode(INAME, value.rdns, NS);
80
+ });
81
+ }
82
+
83
+ // ---- OptionalValidity (§5) -------------------------------------------
84
+ // validity [4] IMPLICIT SEQUENCE { notBefore [0] Time OPTIONAL, notAfter [1] Time
85
+ // OPTIONAL }. Time is a CHOICE of UTCTime / GeneralizedTime, so [0]/[1] are
86
+ // EXPLICIT (an IMPLICIT tag would erase the UTCTime-vs-GeneralizedTime
87
+ // discriminator). RFC 4211 §5 requires at least one of the two to be present.
88
+ var OPTIONAL_VALIDITY = schema.seq([
89
+ schema.trailing([
90
+ { tag: 0, name: "notBefore", schema: schema.time(NS), explicit: true, emptyCode: "crmf/bad-validity" },
91
+ { tag: 1, name: "notAfter", schema: schema.time(NS), explicit: true, emptyCode: "crmf/bad-validity" },
92
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "crmf/bad-validity", orderCode: "crmf/bad-validity" }),
93
+ ], {
94
+ assert: "implicit", implicitTag: 4, code: "crmf/bad-validity", what: "OptionalValidity",
95
+ build: function (m, ctx) {
96
+ var f = m.fields;
97
+ if (!f.notBefore.present && !f.notAfter.present) {
98
+ throw ctx.E("crmf/bad-validity", "OptionalValidity must contain notBefore or notAfter (RFC 4211 §5)");
99
+ }
100
+ return { notBefore: f.notBefore.present ? f.notBefore.value : null, notAfter: f.notAfter.present ? f.notAfter.value : null };
101
+ },
102
+ });
103
+
104
+ // ---- ProofOfPossession (§4) ------------------------------------------
105
+ // raVerified [0] NULL — the RA has already verified POP out of band.
106
+ var POPO_RAVERIFIED = schema.decode(function (n, ctx) {
107
+ try { asn1.read.nullImplicit(n, 0); }
108
+ catch (e) { throw ctx.E("crmf/bad-popo", "raVerified [0] must be an IMPLICIT NULL", e); }
109
+ return { type: "raVerified" };
110
+ });
111
+
112
+ // When poposkInput is present the POP signature is over the DER of the
113
+ // POPOSigningKeyInput VALUE (RFC 4211 §4.1), which is a SEQUENCE — but on the wire
114
+ // poposkInput is an IMPLICIT [0] field, so its bytes lead with the context tag
115
+ // (0xA0) rather than the SEQUENCE tag a verifier hashes. `asn1.sequenceTlv` recovers
116
+ // the SEQUENCE-tagged signed region from the field's content; `signedBytes` is that
117
+ // region and `bytes` keeps the raw wire [0] TLV, so a verifier can use either
118
+ // encoding its peer produced.
119
+
120
+ // POPOSigningKeyInput ::= SEQUENCE { authInfo CHOICE { sender [0] GeneralName,
121
+ // publicKeyMAC PKMACValue }, publicKey SubjectPublicKeyInfo } (§4.1). Structurally
122
+ // validated (not deferred raw), so an empty or malformed poposkInput fails closed
123
+ // rather than surfacing a signed region that was never a well-formed POP input.
124
+ // PKMACValue ::= SEQUENCE { algId AlgorithmIdentifier, value BIT STRING }.
125
+ var PKMAC_VALUE = schema.seq([
126
+ schema.field("algId", pkix.algorithmIdentifier(NS)),
127
+ schema.field("value", schema.bitString()),
128
+ ], { assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-popo", what: "PKMACValue" });
129
+
130
+ // authInfo CHOICE: sender [0] GeneralName is EXPLICIT (GeneralName is itself a
131
+ // CHOICE); publicKeyMAC is a bare PKMACValue SEQUENCE — disjoint by tag class.
132
+ var POPOSK_AUTH_INFO = schema.choice([
133
+ { when: { tagClass: "context", tagNumber: 0 }, schema: schema.explicit(0, pkix.generalName(NS, { code: "crmf/bad-popo" }), { code: "crmf/bad-popo" }) },
134
+ { when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE }, schema: PKMAC_VALUE },
135
+ ], { code: "crmf/bad-popo" });
136
+
137
+ var POPOSK_INPUT = schema.seq([
138
+ schema.field("authInfo", POPOSK_AUTH_INFO),
139
+ schema.field("publicKey", pkix.spki(NS)),
140
+ ], { assert: "implicit", implicitTag: 0, arity: { exact: 2 }, code: "crmf/bad-popo", what: "POPOSigningKeyInput" });
141
+
142
+ // POPOSigningKey ::= SEQUENCE { poposkInput [0] POPOSigningKeyInput OPTIONAL,
143
+ // algorithmIdentifier AlgorithmIdentifier, signature BIT STRING } — IMPLICIT [1].
144
+ var POPO_SIGNING_KEY = schema.seq([
145
+ schema.optional("poposkInput", schema.decode(function (n, ctx) {
146
+ // Validate the full POPOSigningKeyInput structure fail-closed, then surface the
147
+ // raw [0] node plus its decoded publicKey (for the §4.1 template-match check).
148
+ var m = schema.walk(POPOSK_INPUT, n, ctx);
149
+ return { node: n, publicKey: m.fields.publicKey.value.result.bytes };
150
+ }), { tag: 0 }),
151
+ schema.field("algorithmIdentifier", pkix.algorithmIdentifier(NS)),
152
+ schema.field("signature", schema.bitString()),
153
+ ], {
154
+ assert: "implicit", implicitTag: 1, code: "crmf/bad-popo", what: "POPOSigningKey",
155
+ build: function (m) {
156
+ var pin = m.fields.poposkInput;
157
+ return {
158
+ type: "signature",
159
+ poposkInput: pin.present ? { bytes: pin.node.bytes, signedBytes: asn1.sequenceTlv(pin.node), publicKey: pin.value.publicKey } : null,
160
+ algorithmIdentifier: m.fields.algorithmIdentifier.value.result,
161
+ signature: { unusedBits: m.fields.signature.value.unusedBits, bytes: m.fields.signature.value.bytes },
162
+ };
163
+ },
164
+ });
165
+
166
+ // keyEncipherment [2] / keyAgreement [3] POPOPrivKey (§4). POPOPrivKey is a
167
+ // 5-arm CHOICE { thisMessage [0], subsequentMessage [1], dhMAC [2], agreeMAC [3],
168
+ // encryptedKey [4] }, so X.680 §31.2.7 forces the outer [2]/[3] tag to be EXPLICIT
169
+ // (a CHOICE has no single tag to replace). Validate that shell — a constructed
170
+ // wrapper around exactly one inner context [0]..[4] alternative — before surfacing
171
+ // the arm RAW, so a primitive / empty / mis-tagged node is rejected rather than
172
+ // reported to a verifier as a well-formed POP. The inner alternative's own value
173
+ // (incl. the encryptedKey [4] EnvelopedData) stays deferred to the verify layer.
174
+ // POPOPrivKey ::= CHOICE { thisMessage [0] BIT STRING (deprecated), subsequentMessage
175
+ // [1] SubsequentMessage, dhMAC [2] BIT STRING (deprecated), agreeMAC [3] PKMACValue,
176
+ // encryptedKey [4] EnvelopedData } (RFC 4211 §4.2). The deprecated arms are still
177
+ // parsed. The inner VALUE stays deferred raw (the EnvelopedData / MAC decode is a
178
+ // verify-layer concern), but the alternative's shape is validated fail-closed.
179
+ var POPOPRIVKEY_METHODS = { 0: "thisMessage", 1: "subsequentMessage", 2: "dhMAC", 3: "agreeMAC", 4: "encryptedKey" };
180
+ function popoPrivKey(type) {
181
+ return schema.decode(function (n, ctx) {
182
+ if (!n.children || n.children.length !== 1) {
183
+ throw ctx.E("crmf/bad-popo", type + " [" + n.tagNumber + "] POPOPrivKey must be an EXPLICIT wrapper around one alternative (RFC 4211 §4)");
184
+ }
185
+ var inner = n.children[0];
186
+ if (inner.tagClass !== "context" || inner.tagNumber < 0 || inner.tagNumber > 4) {
187
+ throw ctx.E("crmf/bad-popo", type + " POPOPrivKey alternative must be a context [0]..[4] CHOICE element (RFC 4211 §4)");
188
+ }
189
+ // RFC 4211 §4.2/§4.3 — the MAC alternatives dhMAC [2] / agreeMAC [3] are the
190
+ // key-agreement "fourth method"; key-encipherment POP uses only the
191
+ // thisMessage / subsequentMessage / encryptedKey methods, so a MAC arm under
192
+ // keyEncipherment is non-conforming.
193
+ if (type === "keyEncipherment" && (inner.tagNumber === 2 || inner.tagNumber === 3)) {
194
+ throw ctx.E("crmf/bad-popo", "keyEncipherment POP cannot use the MAC alternative " + POPOPRIVKEY_METHODS[inner.tagNumber] + " (RFC 4211 §4.2)");
195
+ }
196
+ // FORM: thisMessage [0] / subsequentMessage [1] / dhMAC [2] are primitive (BIT
197
+ // STRING / INTEGER / BIT STRING); agreeMAC [3] PKMACValue and encryptedKey [4]
198
+ // EnvelopedData are constructed (SEQUENCE). A wrong-form node is malformed.
199
+ var mustBeConstructed = inner.tagNumber === 3 || inner.tagNumber === 4;
200
+ if (mustBeConstructed !== !!inner.children) {
201
+ throw ctx.E("crmf/bad-popo", type + " POPOPrivKey [" + inner.tagNumber + "] has the wrong primitive/constructed form (RFC 4211 §4)");
202
+ }
203
+ // Each alternative's PAYLOAD is decoded fail-closed, not just its form:
204
+ // thisMessage [0] / dhMAC [2] are BIT STRINGs; subsequentMessage [1] an INTEGER
205
+ // in {0,1}; agreeMAC [3] a PKMACValue SEQUENCE; encryptedKey [4] an EnvelopedData.
206
+ if (inner.tagNumber === 0 || inner.tagNumber === 2) {
207
+ try { asn1.read.bitStringImplicit(inner, inner.tagNumber); }
208
+ catch (e) { throw ctx.E("crmf/bad-popo", POPOPRIVKEY_METHODS[inner.tagNumber] + " [" + inner.tagNumber + "] must be a BIT STRING (RFC 4211 §4.2)", e); }
209
+ }
210
+ if (inner.tagNumber === 1) {
211
+ var v;
212
+ try { v = asn1.read.integerImplicit(inner, 1); }
213
+ catch (e) { throw ctx.E("crmf/bad-popo", "subsequentMessage must be an INTEGER", e); }
214
+ if (v !== 0n && v !== 1n) {
215
+ throw ctx.E("crmf/bad-popo", "SubsequentMessage must be encrCert(0) or challengeResp(1) (RFC 4211 §4.2)");
216
+ }
217
+ }
218
+ if (inner.tagNumber === 3) {
219
+ try { schema.walk(PKMAC_VALUE, asn1.decode(asn1.sequenceTlv(inner)), NS); }
220
+ catch (e) { throw ctx.E("crmf/bad-popo", "agreeMAC [3] must be a PKMACValue SEQUENCE { algId, BIT STRING } (RFC 4211 §4.2)", e); }
221
+ }
222
+ // encryptedKey [4] EnvelopedData — validate the CMS structure fail-closed by
223
+ // composing the shared EnvelopedData decoder (retag the IMPLICIT [4] to the
224
+ // universal SEQUENCE it decodes as). The EncKeyWithID identity check lives inside
225
+ // the ENCRYPTED content, so it stays a verify-layer concern.
226
+ if (inner.tagNumber === 4) {
227
+ var env;
228
+ try { env = cms.walkEnvelopedData(asn1.decode(asn1.sequenceTlv(inner))); }
229
+ catch (e) { throw ctx.E("crmf/bad-popo", "encryptedKey [4] must be a well-formed EnvelopedData (RFC 4211 §4.2, RFC 5652 §6.1)", e); }
230
+ // RFC 4211 §4.2 — the enveloped content type MUST be id-ct-encKeyWithID (the
231
+ // ContentType OID is in the clear even though the content itself is encrypted).
232
+ if (env.encryptedContentInfo.contentType !== oid.byName("encKeyWithID")) {
233
+ throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData content type MUST be id-ct-encKeyWithID (RFC 4211 §4.2)");
234
+ }
235
+ // The encrypted key material MUST be present — CMS allows a detached
236
+ // EnvelopedData (encryptedContent OPTIONAL), but a POP with no key to verify
237
+ // or archive is meaningless.
238
+ if (env.encryptedContentInfo.encryptedContent == null) {
239
+ throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData MUST carry the encrypted key in encryptedContent (RFC 4211 §4.2)");
240
+ }
241
+ }
242
+ return { type: type, method: POPOPRIVKEY_METHODS[inner.tagNumber], bytes: n.bytes };
243
+ });
244
+ }
245
+
246
+ // ProofOfPossession ::= CHOICE { raVerified [0] NULL, signature [1] POPOSigningKey,
247
+ // keyEncipherment [2] POPOPrivKey, keyAgreement [3] POPOPrivKey }. Each arm yields
248
+ // the final object so the CertReqMsg build reads one uniform popo shape.
249
+ var PROOF_OF_POSSESSION = schema.choice([
250
+ { when: { tagClass: "context", tagNumber: 0 }, schema: POPO_RAVERIFIED },
251
+ { when: { tagClass: "context", tagNumber: 1 }, schema: schema.decode(function (n, ctx) { return schema.walk(POPO_SIGNING_KEY, n, ctx).result; }) },
252
+ { when: { tagClass: "context", tagNumber: 2 }, schema: popoPrivKey("keyEncipherment") },
253
+ { when: { tagClass: "context", tagNumber: 3 }, schema: popoPrivKey("keyAgreement") },
254
+ ], { code: "crmf/bad-popo" });
255
+
256
+ // ---- Controls / regInfo (§5, §6, §7) ---------------------------------
257
+ // AttributeTypeAndValue ::= SEQUENCE { type OBJECT IDENTIFIER, value ANY DEFINED BY
258
+ // type }. The list is decoded; each value stays RAW (the per-OID value semantics
259
+ // are deferred). Shared shape for both controls and regInfo.
260
+ var CONTROL = schema.seq([
261
+ schema.field("type", schema.oidLeaf()),
262
+ schema.field("value", schema.any()),
263
+ ], {
264
+ assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-control", what: "AttributeTypeAndValue",
265
+ build: function (m, ctx) {
266
+ var t = m.fields.type.value;
267
+ return { type: t, name: ctx.oid.name(t) || null, value: m.fields.value.node.bytes };
268
+ },
269
+ });
270
+ function mapControls(m) { return m.items.map(function (it) { return it.value.result; }); }
271
+ // Controls ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§5).
272
+ var CONTROLS = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-controls", what: "Controls", build: mapControls });
273
+ // regInfo ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§7).
274
+ var REG_INFO = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-reg-info", what: "regInfo", build: mapControls });
275
+
276
+ // ---- CertTemplate (§5) — the IMPLICIT-TAGS core ----------------------
277
+ // CertTemplate ::= SEQUENCE { version [0], serialNumber [1], signingAlg [2],
278
+ // issuer [3] Name, validity [4] OptionalValidity, subject [5] Name, publicKey [6]
279
+ // SubjectPublicKeyInfo, issuerUID [7], subjectUID [8], extensions [9] } — every
280
+ // field IMPLICIT and OPTIONAL, so the whole body is one ascending [0..9] trailing
281
+ // run. issuer/subject Name and the [4] validity times are the CHOICE exceptions.
282
+ var CERT_TEMPLATE = schema.seq([
283
+ schema.trailing([
284
+ { tag: 0, name: "version", schema: schema.implicitInteger(0) },
285
+ { tag: 1, name: "serialNumber", schema: schema.implicitInteger(1) },
286
+ { tag: 2, name: "signingAlg", schema: pkix.algorithmIdentifier(NS, { implicitTag: 2 }) },
287
+ { tag: 3, name: "issuer", schema: crmfName(3) },
288
+ { tag: 4, name: "validity", schema: OPTIONAL_VALIDITY },
289
+ { tag: 5, name: "subject", schema: crmfName(5) },
290
+ { tag: 6, name: "publicKey", schema: pkix.spki(NS, { implicitTag: 6 }) },
291
+ { tag: 7, name: "issuerUID", schema: schema.implicitBitString(7) },
292
+ { tag: 8, name: "subjectUID", schema: schema.implicitBitString(8) },
293
+ { tag: 9, name: "extensions", schema: pkix.extensions(NS, { implicitTag: 9 }) },
294
+ ], { minTag: 0, maxTag: 9, unexpectedCode: "crmf/bad-cert-template", orderCode: "crmf/bad-cert-template" }),
295
+ ], {
296
+ assert: "sequence", code: "crmf/bad-cert-template", what: "CertTemplate",
297
+ build: function (m, ctx) {
298
+ var f = m.fields;
299
+ // RFC 4211 §5 — serialNumber, signingAlg, issuerUID, and subjectUID MUST be
300
+ // omitted from a CertTemplate: serialNumber and signingAlg are assigned by the
301
+ // CA, and the UID pair is deprecated. A requester must not dictate a
302
+ // CA-assigned value (a requester-chosen serialNumber is a real hazard), so a
303
+ // template that sets any of them is rejected fail-closed, not surfaced as
304
+ // acceptable. They stay in the field map (parsed, then rejected) so the
305
+ // diagnostic names the offending field rather than a bare tag number.
306
+ var caAssigned = ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
307
+ for (var i = 0; i < caAssigned.length; i++) {
308
+ if (f[caAssigned[i]].present) {
309
+ throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted — it is CA-assigned or deprecated (RFC 4211 §5)");
310
+ }
311
+ }
312
+ // version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5).
313
+ if (f.version.present && f.version.value !== 2n) {
314
+ throw ctx.E("crmf/bad-version", "CertTemplate version MUST be 2 (v3) if supplied (RFC 4211 §5)");
315
+ }
316
+ return {
317
+ version: f.version.present ? f.version.value : null,
318
+ issuer: f.issuer.present ? f.issuer.value : null,
319
+ validity: f.validity.present ? f.validity.value.result : null,
320
+ subject: f.subject.present ? f.subject.value : null,
321
+ publicKey: f.publicKey.present ? f.publicKey.value.result : null,
322
+ extensions: f.extensions.present ? f.extensions.value.result : null,
323
+ };
324
+ },
325
+ });
326
+
327
+ // ---- CertRequest / CertReqMsg / CertReqMessages (§3, §5) --------------
328
+ // CertRequest ::= SEQUENCE { certReqId INTEGER, certTemplate CertTemplate,
329
+ // controls Controls OPTIONAL }. certReqId is a SIGNED INTEGER with no value
330
+ // constraint — a negative value (the RFC 9483 -1 sentinel) is legal.
331
+ var CERT_REQUEST = schema.seq([
332
+ schema.field("certReqId", schema.integerLeaf()),
333
+ schema.field("certTemplate", CERT_TEMPLATE),
334
+ schema.optional("controls", CONTROLS, { whenUniversal: [TAGS.SEQUENCE] }),
335
+ ], {
336
+ assert: "sequence", code: "crmf/bad-cert-request", what: "CertRequest",
337
+ build: function (m) {
338
+ return {
339
+ certReqId: m.fields.certReqId.value,
340
+ certReqIdHex: m.fields.certReqId.node.content.toString("hex"),
341
+ certTemplate: m.fields.certTemplate.value.result,
342
+ controls: m.fields.controls.present ? m.fields.controls.value.result : null,
343
+ certReqBytes: m.node.bytes, // the exact CertRequest TLV the POP signature covers (§4.1)
344
+ };
345
+ },
346
+ });
347
+
348
+ // CertReqMsg ::= SEQUENCE { certReq CertRequest, popo ProofOfPossession OPTIONAL,
349
+ // regInfo SEQUENCE OF AttributeTypeAndValue OPTIONAL }. popo is a CHOICE of context
350
+ // [0]..[3]; regInfo is a bare universal SEQUENCE, so the two OPTIONAL positions are
351
+ // disambiguated by tag class.
352
+ var CERT_REQ_MSG = schema.seq([
353
+ schema.field("certReq", CERT_REQUEST),
354
+ schema.optional("popo", PROOF_OF_POSSESSION, { tags: [0, 1, 2, 3] }),
355
+ schema.optional("regInfo", REG_INFO, { whenUniversal: [TAGS.SEQUENCE] }),
356
+ ], {
357
+ assert: "sequence", code: "crmf/bad-cert-req-msg", what: "CertReqMsg",
358
+ build: function (m, ctx) {
359
+ var certReq = m.fields.certReq.value.result;
360
+ var popo = m.fields.popo.present ? m.fields.popo.value : null;
361
+ // A POP whose signature/MAC covers the certReq requires the CertTemplate to
362
+ // contain both subject and publicKey — otherwise there is no complete request
363
+ // to sign / MAC over, and a verifier would be bound to the wrong identity/key.
364
+ var complete = certReq.certTemplate.subject !== null && certReq.certTemplate.publicKey !== null;
365
+ // RFC 4211 §4.1 — for a signature POP, poposkInput's presence is fixed by the
366
+ // CertTemplate: it MUST be omitted when the template carries BOTH subject and
367
+ // publicKey (the signature is then over the DER of the CertRequest), and MUST be
368
+ // present otherwise.
369
+ if (popo && popo.type === "signature") {
370
+ if (complete && popo.poposkInput !== null) {
371
+ throw ctx.E("crmf/bad-popo", "poposkInput MUST be omitted when the CertTemplate contains both subject and publicKey (RFC 4211 §4.1)");
372
+ }
373
+ if (!complete && popo.poposkInput === null) {
374
+ throw ctx.E("crmf/bad-popo", "poposkInput MUST be present when the CertTemplate lacks subject or publicKey (RFC 4211 §4.1)");
375
+ }
376
+ // RFC 4211 §4.1 — the POPOSigningKeyInput publicKey MUST be exactly the template
377
+ // publicKey (both surfaced as canonical SubjectPublicKeyInfo DER), so a request
378
+ // cannot sign one key while asking for a certificate over a different key.
379
+ if (popo.poposkInput !== null && certReq.certTemplate.publicKey !== null &&
380
+ !popo.poposkInput.publicKey.equals(certReq.certTemplate.publicKey.bytes)) {
381
+ throw ctx.E("crmf/bad-popo", "POPOSigningKeyInput publicKey MUST equal the CertTemplate publicKey (RFC 4211 §4.1)");
382
+ }
383
+ }
384
+ // RFC 4211 §4.2/§4.3 — a MAC-based POPOPrivKey (dhMAC / agreeMAC) proves
385
+ // possession by MACing the certReq, which MUST include both subject and publicKey.
386
+ if (popo && (popo.type === "keyEncipherment" || popo.type === "keyAgreement") &&
387
+ (popo.method === "dhMAC" || popo.method === "agreeMAC") && !complete) {
388
+ throw ctx.E("crmf/bad-popo", "a MAC-based key-agreement/encipherment POP (dhMAC/agreeMAC) requires the CertTemplate to contain both subject and publicKey (RFC 4211 §4.2/§4.3)");
389
+ }
390
+ return {
391
+ certReq: certReq,
392
+ popo: popo,
393
+ regInfo: m.fields.regInfo.present ? m.fields.regInfo.value.result : null,
394
+ };
395
+ },
396
+ });
397
+
398
+ // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (§3) — an empty sequence
399
+ // is malformed.
400
+ var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
401
+ assert: "sequence", min: 1, code: "crmf/bad-cert-req-messages", what: "CertReqMessages",
402
+ build: function (m) { return { messages: m.items.map(function (it) { return it.value.result; }) }; },
403
+ });
404
+
405
+ // ---- parse -----------------------------------------------------------
406
+
407
+ /**
408
+ * @primitive pki.schema.crmf.parse
409
+ * @signature pki.schema.crmf.parse(input) -> certReqMessages
410
+ * @since 0.1.17
411
+ * @status experimental
412
+ * @spec RFC 4211
413
+ * @related pki.schema.parse, pki.schema.csr.parse
414
+ *
415
+ * Parse a DER `Buffer` or a PEM string/Buffer into a structured `CertReqMessages`:
416
+ * `{ messages: [ { certReq, popo, regInfo } ] }`. Each `certReq` is
417
+ * `{ certReqId, certReqIdHex, certTemplate, controls, certReqBytes }`, and
418
+ * `certTemplate` carries the requestable certificate fields (`version`, `issuer`,
419
+ * `validity`, `subject`, `publicKey`, `extensions` — each `null` when absent). The
420
+ * CA-assigned / deprecated fields RFC 4211 §5 requires a request to omit
421
+ * (`serialNumber`, `signingAlg`, `issuerUID`, `subjectUID`) are rejected, not
422
+ * surfaced. `popo` is
423
+ * `null`, `{ type: "raVerified" }`, `{ type: "signature", poposkInput,
424
+ * algorithmIdentifier, signature }`, or `{ type: "keyEncipherment" |
425
+ * "keyAgreement", method, bytes }` (where `method` is the POPOPrivKey alternative —
426
+ * `thisMessage` / `subsequentMessage` / `dhMAC` / `agreeMAC` / `encryptedKey` — each
427
+ * structurally validated, the `encryptedKey` EnvelopedData included). When present, `poposkInput` is `{ bytes, signedBytes }`
428
+ * — `bytes` is the raw wire `[0]` TLV and `signedBytes` is the `POPOSigningKeyInput`
429
+ * re-tagged to the `SEQUENCE` DER the signature actually covers (RFC 4211 §4.1).
430
+ * `certReqBytes` is the exact `CertRequest` byte range a proof-of-possession
431
+ * verifier hashes when `poposkInput` is absent.
432
+ *
433
+ * Throws `CrmfError` when the bytes are not a well-formed `CertReqMessages`, and
434
+ * `Asn1Error` when the underlying DER is malformed.
435
+ *
436
+ * @example
437
+ * var m = pki.schema.crmf.parse(der);
438
+ * m.messages[0].certReq.certTemplate.subject.dn; // "CN=req.example"
439
+ */
440
+ var parse = pkix.makeParser({
441
+ pemLabel: null, PemError: PemError, ErrorClass: CrmfError, prefix: "crmf",
442
+ what: "certificate request message", topSchema: CERT_REQ_MESSAGES, ns: NS,
443
+ });
444
+
445
+ /**
446
+ * @primitive pki.schema.crmf.pemDecode
447
+ * @signature pki.schema.crmf.pemDecode(text, label?) -> Buffer
448
+ * @since 0.1.17
449
+ * @status experimental
450
+ * @spec RFC 7468, RFC 4211
451
+ * @related pki.schema.crmf.parse
452
+ *
453
+ * Extract the DER bytes from a PEM block (RFC 4211 registers no RFC 7468 label, so
454
+ * the first block is taken unless `label` is given — CRMF rides inside CMP / EST as
455
+ * DER in practice). Throws `PemError` on a missing envelope or a non-base64 body.
456
+ *
457
+ * @example
458
+ * var der = pki.schema.crmf.pemDecode(pemText);
459
+ */
460
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
461
+
462
+ // matches(root): a CertReqMessages is a universal SEQUENCE whose first CertReqMsg
463
+ // is a SEQUENCE whose first CertRequest is a SEQUENCE leading with a universal
464
+ // INTEGER (certReqId) then a universal SEQUENCE (certTemplate). This 3-level probe
465
+ // is disjoint from every registered root PROVIDED crmf is checked before
466
+ // ocsp-request: an OCSPRequest's tbsRequest never leads with the INTEGER-then-
467
+ // SEQUENCE pair (its children[0] is a context [0] version or a requestList).
468
+ function matches(root) {
469
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children || root.children.length < 1) return false;
470
+ var msg = root.children[0];
471
+ if (!msg.children || msg.tagClass !== "universal" || msg.tagNumber !== TAGS.SEQUENCE || msg.children.length < 1) return false;
472
+ var certReq = msg.children[0];
473
+ if (!certReq.children || certReq.tagClass !== "universal" || certReq.tagNumber !== TAGS.SEQUENCE || certReq.children.length < 2) return false;
474
+ var id = certReq.children[0], tpl = certReq.children[1];
475
+ return id.tagClass === "universal" && id.tagNumber === TAGS.INTEGER &&
476
+ tpl.tagClass === "universal" && tpl.tagNumber === TAGS.SEQUENCE && !!tpl.children;
477
+ }
478
+
479
+ module.exports = {
480
+ parse: parse,
481
+ pemDecode: pemDecode,
482
+ matches: matches,
483
+ // The top schema, exposed (not on the curated pki.schema.crmf surface) so a
484
+ // round-trip test can prove schema.encode(schema, v) -> decode -> parse recovers
485
+ // v — the invariant that structurally validates the IMPLICIT/EXPLICIT tag handling.
486
+ certReqMessagesSchema: CERT_REQ_MESSAGES,
487
+ };
package/lib/schema-csr.js CHANGED
@@ -106,7 +106,7 @@ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO,
106
106
  * @primitive pki.schema.csr.parse
107
107
  * @signature pki.schema.csr.parse(input) -> csr
108
108
  * @since 0.1.8
109
- * @status experimental
109
+ * @status stable
110
110
  * @spec RFC 2986
111
111
  * @related pki.schema.x509.parse, pki.schema.parse
112
112
  *
@@ -129,7 +129,7 @@ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemErro
129
129
  * @primitive pki.schema.csr.pemDecode
130
130
  * @signature pki.schema.csr.pemDecode(text, label?) -> Buffer
131
131
  * @since 0.1.8
132
- * @status experimental
132
+ * @status stable
133
133
  * @spec RFC 7468, RFC 2986
134
134
  * @related pki.schema.csr.parse
135
135
  *
@@ -146,7 +146,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFIC
146
146
  * @primitive pki.schema.csr.pemEncode
147
147
  * @signature pki.schema.csr.pemEncode(der, label?) -> string
148
148
  * @since 0.1.8
149
- * @status experimental
149
+ * @status stable
150
150
  * @spec RFC 7468
151
151
  * @related pki.schema.csr.pemDecode
152
152
  *
@@ -93,39 +93,49 @@ function _assertArity(schema, kids, ctx) {
93
93
  // the OID / INTEGER / string reads). `decode(fn)` hands the whole node to fn,
94
94
  // which owns its try/catch and code mapping. `any()` yields the node itself.
95
95
 
96
- function oidLeaf() { return { kind: "leaf", read: asn1.read.oid }; }
97
- function integerLeaf() { return { kind: "leaf", read: asn1.read.integer }; }
98
- function boolean() { return { kind: "leaf", read: asn1.read.boolean }; }
99
- function octetString() { return { kind: "leaf", read: asn1.read.octetString }; }
100
- function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
96
+ // Leaves carry BOTH directions: `read(node) -> value` and `write(value) -> DER TLV`,
97
+ // so a single leaf definition drives decode (walk) and encode (build). A [tag]
98
+ // IMPLICIT leaf's write emits the context-tagged TLV directly, mirroring its read.
99
+ function _implicitIntContent(v) { return asn1.decode(asn1.build.integer(v)).content; }
100
+ function oidLeaf() { return { kind: "leaf", read: asn1.read.oid, write: function (v) { return asn1.build.oid(v); } }; }
101
+ function integerLeaf() { return { kind: "leaf", read: asn1.read.integer, write: function (v) { return asn1.build.integer(v); } }; }
102
+ function boolean() { return { kind: "leaf", read: asn1.read.boolean, write: function (v) { return asn1.build.boolean(v); } }; }
103
+ function octetString() { return { kind: "leaf", read: asn1.read.octetString, write: function (v) { return asn1.build.octetString(v); } }; }
104
+ function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; }, write: function (v) { return asn1.build.bitString(v.bytes, v.unusedBits); } }; }
101
105
  // A [tag] IMPLICIT BIT STRING leaf (context-class primitive) — the primitive-leaf
102
106
  // counterpart to implicitSetOf, for e.g. the PKCS#8 publicKey [1] (RFC 5958 §2).
103
- function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
107
+ function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; }, write: function (v) { return asn1.build.contextPrimitive(tag, Buffer.concat([Buffer.from([v.unusedBits]), v.bytes])); } }; }
104
108
  // A [tag] IMPLICIT OCTET STRING leaf (context-class primitive) — the sibling of
105
109
  // implicitBitString, for e.g. the CMS SignerIdentifier subjectKeyIdentifier [0]
106
110
  // (RFC 5652 §5.3). Yields the raw content bytes.
107
- function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); } }; }
111
+ function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, v); } }; }
108
112
  // A [tag] IMPLICIT NULL leaf (context-class primitive, empty content) — the sibling
109
113
  // of implicitBitString/implicitOctetString, for e.g. the OCSP CertStatus good [0] /
110
114
  // unknown [2] arms (RFC 6960 §4.2.1). Yields null; rejects a constructed or
111
115
  // non-empty [tag] node fail-closed.
112
- function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); } }; }
116
+ function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); }, write: function () { return asn1.build.contextPrimitive(tag, Buffer.alloc(0)); } }; }
113
117
  // A [tag] IMPLICIT INTEGER leaf (context-class primitive) — the integer sibling of
114
118
  // implicitBitString, for the RFC 3161 Accuracy millis [0] / micros [1] fields
115
119
  // (context-tagged primitive INTEGERs). Yields a BigInt; a constructed or wrong-tag
116
120
  // node fails asn1/* at the codec.
117
- function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); } }; }
121
+ function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, _implicitIntContent(v)); } }; }
118
122
  function any() { return { kind: "any" }; }
119
- function decode(fn) { return { kind: "decode", fn: fn }; }
123
+ // decode(fn, write?): a custom decode leaf; `write` is its optional paired encoder
124
+ // (value -> DER TLV) so the same leaf drives both directions.
125
+ function decode(fn, write) { return { kind: "decode", fn: fn, write: write }; }
120
126
 
121
127
  // time(ns): a UTCTime / GeneralizedTime value, asserting the tag before the
122
- // codec reads it (mirrors _parseValidityTime's x509/bad-time guard).
128
+ // codec reads it (mirrors _parseValidityTime's x509/bad-time guard). Encodes as
129
+ // UTCTime for years 1950..2049 and GeneralizedTime otherwise (RFC 5280 §4.1.2.5).
123
130
  function time(ns) {
124
131
  return decode(function (n, ctx) {
125
132
  if (n.tagClass !== "universal" || (n.tagNumber !== TAGS.UTC_TIME && n.tagNumber !== TAGS.GENERALIZED_TIME)) {
126
133
  _fail(ctx, ns.prefix + "/bad-time", "time must be UTCTime or GeneralizedTime");
127
134
  }
128
135
  return asn1.read.time(n);
136
+ }, function (date) {
137
+ var y = date.getUTCFullYear();
138
+ return (y >= 1950 && y < 2050) ? asn1.build.utcTime(date) : asn1.build.generalizedTime(date);
129
139
  });
130
140
  }
131
141
 
@@ -136,6 +146,10 @@ function optional(name, schema, opts) {
136
146
  opts = opts || {};
137
147
  // How the optional field is recognized at its position:
138
148
  // - default: a context [tag] (the certificate version [0] shape).
149
+ // - tags: a context tag in the set — an OPTIONAL field whose type is
150
+ // itself a CHOICE of several context alternatives, e.g. the CRMF CertReqMsg
151
+ // popo ProofOfPossession ([0]..[3]) sitting before the universal-SEQUENCE
152
+ // regInfo (RFC 4211 §3), where ANY of the CHOICE's tags marks it present.
139
153
  // - whenUniversal: the next element iff its UNIVERSAL tag is in the set —
140
154
  // the CRL TBSCertList shape (bare INTEGER version, Time nextUpdate,
141
155
  // SEQUENCE revokedCertificates), disambiguated by tag, not a context [n].
@@ -147,7 +161,9 @@ function optional(name, schema, opts) {
147
161
  ? function () { return true; }
148
162
  : opts.whenUniversal
149
163
  ? function (n) { return n.tagClass === "universal" && opts.whenUniversal.indexOf(n.tagNumber) !== -1; }
150
- : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
164
+ : opts.tags
165
+ ? function (n) { return n.tagClass === "context" && opts.tags.indexOf(n.tagNumber) !== -1; }
166
+ : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
151
167
  return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
152
168
  explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
153
169
  }
@@ -218,7 +234,7 @@ function implicitSeqOf(tag, item, opts) {
218
234
  * @primitive pki.schema.engine.walk
219
235
  * @signature pki.schema.engine.walk(schema, node, ctx) -> value
220
236
  * @since 0.1.7
221
- * @status experimental
237
+ * @status stable
222
238
  * @spec X.690, X.680
223
239
  * @related pki.asn1.decode, pki.schema.x509.parse
224
240
  *
@@ -389,6 +405,124 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
389
405
  }
390
406
  }
391
407
 
408
+ // ---- the encode engine (the inverse of walk) -------------------------
409
+ // encode(schema, value) is the constructor direction of the SAME schema walk
410
+ // decodes: it interprets the schema against a plain structural value and emits
411
+ // canonical DER. A single schema definition therefore drives both directions, so
412
+ // context-tag (EXPLICIT/IMPLICIT) handling cannot diverge between them — the class
413
+ // of bug where a decoder reads a [tag] IMPLICIT field but an ad-hoc encoder emits
414
+ // the wrong tag is structurally retired. Encode errors are authoring faults (a bad
415
+ // value / an un-encodable schema), thrown as plain Errors, not parse verdicts.
416
+
417
+ function _encFail(message) { throw new Error("schema.encode: " + message); }
418
+
419
+ /**
420
+ * @primitive pki.schema.engine.encode
421
+ * @signature pki.schema.engine.encode(schema, value, ctx) -> Buffer
422
+ * @since 0.1.17
423
+ * @status experimental
424
+ * @spec X.690, X.680
425
+ * @related pki.schema.engine.walk
426
+ *
427
+ * Encode a structural value to canonical DER by interpreting the SAME schema
428
+ * `walk` decodes — the constructor direction. `value` mirrors the schema: a `seq`
429
+ * takes `{ fieldName: value }`, a leaf its natural JS value (an OID string, a
430
+ * BigInt, a `{ unusedBits, bytes }` BIT STRING, a `Date`), a `repeat` an array, a
431
+ * `choice` `{ arm, value }`. EXPLICIT wrappers and IMPLICIT `[tag]` retagging are
432
+ * applied by the engine, so `walk(schema, decode(encode(schema, v)))` round-trips.
433
+ *
434
+ * @example
435
+ * var S = pki.schema.engine;
436
+ * var der = S.encode(S.seq([S.field("n", S.integerLeaf())]), { n: 42n });
437
+ */
438
+ function encode(schema, value, ctx) {
439
+ switch (schema.kind) {
440
+ case "leaf": return schema.write(value);
441
+ case "any": return Buffer.isBuffer(value) ? value : value.bytes;
442
+ case "decode":
443
+ if (typeof schema.write !== "function") _encFail("this decode leaf has no paired encoder (decode(fn, write))");
444
+ return schema.write(value, ctx);
445
+ case "seq": return _encodeSeq(schema, value, ctx);
446
+ case "explicit": return asn1.build.explicit(schema.tag, encode(schema.schema, value, ctx));
447
+ case "repeat": return _encodeRepeat(schema, value, ctx);
448
+ case "choice": return _encodeChoice(schema, value, ctx);
449
+ default: _encFail("cannot encode schema kind " + JSON.stringify(schema.kind));
450
+ }
451
+ return undefined;
452
+ }
453
+
454
+ function _wrapConstructed(schema, parts) {
455
+ if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
456
+ var mode = schema.assert || "sequence";
457
+ if (mode === "set") return asn1.build.set(parts);
458
+ // "constructed" is a DECODE-time leniency (accept any constructed shape); the
459
+ // canonical encoding is a universal SEQUENCE, same as "sequence".
460
+ if (mode === "sequence" || mode === "constructed") return asn1.build.sequence(parts);
461
+ _encFail("cannot encode a seq/repeat with assert " + JSON.stringify(mode));
462
+ return undefined;
463
+ }
464
+
465
+ function _present(v) { return v !== undefined && v !== null; }
466
+
467
+ function _encodeSeq(schema, value, ctx) {
468
+ value = value || {};
469
+ var parts = [];
470
+ for (var f = 0; f < schema.fields.length; f++) {
471
+ var fld = schema.fields[f];
472
+ if (fld.fkind === "required") {
473
+ parts.push(encode(fld.schema, value[fld.name], ctx));
474
+ } else if (fld.fkind === "optional") {
475
+ if (_present(value[fld.name])) {
476
+ var enc = encode(fld.schema, value[fld.name], ctx);
477
+ parts.push(fld.explicit ? asn1.build.explicit(fld.tag, enc) : enc);
478
+ }
479
+ } else if (fld.fkind === "trailing") {
480
+ // Emit present members in strictly-increasing tag order (the walk's rule).
481
+ var members = fld.members.slice().sort(function (a, b) { return a.tag - b.tag; });
482
+ for (var i = 0; i < members.length; i++) {
483
+ var mm = members[i];
484
+ if (_present(value[mm.name])) {
485
+ var menc = encode(mm.schema, value[mm.name], ctx);
486
+ parts.push(mm.explicit ? asn1.build.explicit(mm.tag, menc) : menc);
487
+ }
488
+ }
489
+ }
490
+ }
491
+ return _wrapConstructed(schema, parts);
492
+ }
493
+
494
+ function _encodeRepeat(schema, items, ctx) {
495
+ if (!Array.isArray(items)) _encFail("a repeat value must be an array");
496
+ // Enforce the SAME constraints walk enforces, so encode never emits DER its own
497
+ // decoder rejects (the walk(decode(encode)) invariant): the SIZE minimum, and
498
+ // semantic uniqueness (the key computed exactly as walk does, off the round-tripped
499
+ // item — a duplicate is an authoring fault, not a parse verdict).
500
+ if (schema.min != null && items.length < schema.min) {
501
+ _encFail("this repeat requires at least " + schema.min + " element(s) but got " + items.length);
502
+ }
503
+ var parts = items.map(function (it) { return encode(schema.item, it, ctx); });
504
+ if (schema.unique) {
505
+ var seen = new Set();
506
+ parts.forEach(function (tlv) {
507
+ var item = { node: asn1.decode(tlv) };
508
+ item.value = walk(schema.item, item.node, ctx);
509
+ var key = schema.unique(item);
510
+ if (seen.has(key)) _encFail("this repeat requires unique elements but got a duplicate key: " + key);
511
+ seen.add(key);
512
+ });
513
+ }
514
+ if (schema.derSetOrder) parts = parts.slice().sort(Buffer.compare);
515
+ if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
516
+ return schema.assert === "set" ? asn1.build.set(parts) : asn1.build.sequence(parts);
517
+ }
518
+
519
+ function _encodeChoice(schema, value, ctx) {
520
+ if (!value || typeof value.arm !== "number" || value.arm < 0 || value.arm >= schema.alts.length) {
521
+ _encFail("a choice value must be { arm: <index>, value: <arm value> }");
522
+ }
523
+ return encode(schema.alts[value.arm].schema, value.value, ctx);
524
+ }
525
+
392
526
  module.exports = {
393
527
  // structural
394
528
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
@@ -399,5 +533,5 @@ module.exports = {
399
533
  implicitNull: implicitNull, implicitInteger: implicitInteger,
400
534
  any: any, decode: decode, time: time,
401
535
  // engine
402
- walk: walk,
536
+ walk: walk, encode: encode,
403
537
  };
@@ -425,7 +425,7 @@ var OCSP_REQUEST = schema.seq([
425
425
  * @primitive pki.schema.ocsp.parseRequest
426
426
  * @signature pki.schema.ocsp.parseRequest(input) -> ocspRequest
427
427
  * @since 0.1.11
428
- * @status experimental
428
+ * @status stable
429
429
  * @spec RFC 6960
430
430
  * @related pki.schema.ocsp.parseResponse, pki.schema.parse
431
431
  *
@@ -447,7 +447,7 @@ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemErro
447
447
  * @primitive pki.schema.ocsp.parseResponse
448
448
  * @signature pki.schema.ocsp.parseResponse(input) -> ocspResponse
449
449
  * @since 0.1.11
450
- * @status experimental
450
+ * @status stable
451
451
  * @spec RFC 6960
452
452
  * @related pki.schema.ocsp.parseRequest, pki.schema.parse
453
453
  *
@@ -471,7 +471,7 @@ var parseResponse = pkix.makeParser({ pemLabel: "OCSP RESPONSE", PemError: PemEr
471
471
  * @primitive pki.schema.ocsp.pemDecode
472
472
  * @signature pki.schema.ocsp.pemDecode(text, label?) -> Buffer
473
473
  * @since 0.1.11
474
- * @status experimental
474
+ * @status stable
475
475
  * @spec RFC 7468, RFC 6960
476
476
  * @related pki.schema.ocsp.parseResponse
477
477
  *
@@ -102,7 +102,7 @@ var ENCRYPTED_PRIVATE_KEY_INFO = schema.seq([
102
102
  * @primitive pki.schema.pkcs8.parse
103
103
  * @signature pki.schema.pkcs8.parse(input) -> privateKey
104
104
  * @since 0.1.9
105
- * @status experimental
105
+ * @status stable
106
106
  * @spec RFC 5208, RFC 5958
107
107
  * @related pki.schema.parse, pki.schema.x509.parse
108
108
  *
@@ -124,7 +124,7 @@ var parse = pkix.makeParser({ pemLabel: "PRIVATE KEY", PemError: PemError, Error
124
124
  * @primitive pki.schema.pkcs8.parseEncrypted
125
125
  * @signature pki.schema.pkcs8.parseEncrypted(input) -> encrypted
126
126
  * @since 0.1.9
127
- * @status experimental
127
+ * @status stable
128
128
  * @spec RFC 5958, RFC 5208
129
129
  * @related pki.schema.pkcs8.parse
130
130
  *
@@ -143,7 +143,7 @@ var parseEncrypted = pkix.makeParser({ pemLabel: "ENCRYPTED PRIVATE KEY", PemErr
143
143
  * @primitive pki.schema.pkcs8.pemDecode
144
144
  * @signature pki.schema.pkcs8.pemDecode(text, label?) -> Buffer
145
145
  * @since 0.1.9
146
- * @status experimental
146
+ * @status stable
147
147
  * @spec RFC 7468, RFC 5958
148
148
  * @related pki.schema.pkcs8.parse
149
149
  *
@@ -160,7 +160,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "PRIVATE
160
160
  * @primitive pki.schema.pkcs8.pemEncode
161
161
  * @signature pki.schema.pkcs8.pemEncode(der, label?) -> string
162
162
  * @since 0.1.9
163
- * @status experimental
163
+ * @status stable
164
164
  * @spec RFC 7468
165
165
  * @related pki.schema.pkcs8.pemDecode
166
166
  *
@@ -166,6 +166,12 @@ function attrValueToString(ns) {
166
166
  }
167
167
  return "#" + node.bytes.toString("hex");
168
168
  }
169
+ }, function (value) {
170
+ // encode: a "#hex" escape (RFC 4514 §2.4) round-trips its raw DER verbatim; any
171
+ // other string encodes as UTF8String (the decode does not preserve the original
172
+ // string type, so this is the canonical re-encoding, not a byte-exact one).
173
+ if (typeof value === "string" && value.charAt(0) === "#") return Buffer.from(value.slice(1), "hex");
174
+ return asn1.build.utf8(value);
169
175
  });
170
176
  }
171
177
 
@@ -193,25 +199,34 @@ function relativeDistinguishedName(ns) {
193
199
  // RelativeDistinguishedName ::= SET SIZE (1..MAX) — an empty SET {} is malformed.
194
200
  return schema.setOf(attributeTypeAndValue(ns), { assert: "set", min: 1, code: ns.prefix + "/bad-rdn", what: "RelativeDistinguishedName" });
195
201
  }
196
- function name(ns) {
197
- return schema.seqOf(relativeDistinguishedName(ns), {
198
- assert: "sequence", code: ns.prefix + "/bad-name", what: "Name",
199
- build: function (m) {
200
- var rdns = [], parts = [];
201
- m.items.forEach(function (rdnItem) {
202
- var atvs = [], atvParts = [];
203
- rdnItem.value.items.forEach(function (atvItem) {
204
- var a = atvItem.value.result; // atvItem.value = the atv seq-match; .result = its build result
205
- atvs.push(a);
206
- var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
207
- atvParts.push(label + "=" + _escapeDnValue(a.value));
208
- });
209
- rdns.push(atvs);
210
- parts.push(atvParts.join("+"));
202
+ // opts.implicitTag (optional): read the Name as a [tag] IMPLICIT RDNSequence — the
203
+ // context tag REPLACES the universal SEQUENCE tag, so the node is a context-class
204
+ // constructed [tag] whose children ARE the RDN SETs (the CRMF CertTemplate issuer
205
+ // [3] / subject [5] IMPLICIT arm, RFC 4211 §5). With no opts the shape is a bare
206
+ // universal SEQUENCE OF, byte-identical to every existing caller.
207
+ function name(ns, opts) {
208
+ opts = opts || {};
209
+ function nameBuild(m) {
210
+ var rdns = [], parts = [];
211
+ m.items.forEach(function (rdnItem) {
212
+ var atvs = [], atvParts = [];
213
+ rdnItem.value.items.forEach(function (atvItem) {
214
+ var a = atvItem.value.result; // atvItem.value = the atv seq-match; .result = its build result
215
+ atvs.push(a);
216
+ var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
217
+ atvParts.push(label + "=" + _escapeDnValue(a.value));
211
218
  });
212
- return { rdns: rdns, dn: parts.join(", ") };
213
- },
214
- });
219
+ rdns.push(atvs);
220
+ parts.push(atvParts.join("+"));
221
+ });
222
+ return { rdns: rdns, dn: parts.join(", ") };
223
+ }
224
+ if (opts.implicitTag != null) {
225
+ return schema.implicitSeqOf(opts.implicitTag, relativeDistinguishedName(ns), {
226
+ code: ns.prefix + "/bad-name", what: "Name", build: nameBuild });
227
+ }
228
+ return schema.seqOf(relativeDistinguishedName(ns), {
229
+ assert: "sequence", code: ns.prefix + "/bad-name", what: "Name", build: nameBuild });
215
230
  }
216
231
 
217
232
  // GeneralName ::= CHOICE (RFC 5280 §4.2.1.6). A validate-and-surface-raw leaf for a
@@ -623,12 +638,20 @@ function extension(ns) {
623
638
  return { oid: extnID, name: ns.oid.name(extnID) || null, critical: critical, value: asn1.read.octetString(valueNode) };
624
639
  });
625
640
  }
626
- function extensions(ns) {
627
- return schema.seqOf(extension(ns), {
628
- assert: "sequence", min: 1, code: ns.prefix + "/bad-extensions", what: "Extensions",
629
- unique: function (item) { return item.value.oid; }, dupCode: ns.prefix + "/duplicate-extension",
641
+ // opts.implicitTag (optional): read Extensions as a [tag] IMPLICIT SEQUENCE OF
642
+ // Extension — the context tag REPLACES the universal SEQUENCE tag, for the CRMF
643
+ // CertTemplate extensions [9] (RFC 4211 §5). With no opts the shape is a bare
644
+ // universal SEQUENCE OF, byte-identical to every existing caller.
645
+ function extensions(ns, opts) {
646
+ opts = opts || {};
647
+ var EXT = extension(ns);
648
+ var extOpts = {
649
+ min: 1, code: ns.prefix + "/bad-extensions", what: "Extensions",
650
+ unique: function (it) { return it.value.oid; }, dupCode: ns.prefix + "/duplicate-extension",
630
651
  build: function (m) { return m.items.map(function (it) { return it.value; }); },
631
- });
652
+ };
653
+ if (opts.implicitTag != null) return schema.implicitSeqOf(opts.implicitTag, EXT, extOpts);
654
+ return schema.seqOf(EXT, Object.assign({ assert: "sequence" }, extOpts));
632
655
  }
633
656
 
634
657
  // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier,
@@ -636,17 +659,26 @@ function extensions(ns) {
636
659
  // universal SEQUENCE — a context-tagged or SET-tagged constructed node carrying
637
660
  // two well-formed children is NOT a SubjectPublicKeyInfo. Shared by the
638
661
  // certificate and CSR parsers.
639
- function spki(ns) {
662
+ // opts.implicitTag (optional): read the SubjectPublicKeyInfo as a [tag] IMPLICIT
663
+ // SEQUENCE (a context-class constructed node whose children are algorithm +
664
+ // subjectPublicKey), for the CRMF CertTemplate publicKey [6] (RFC 4211 §5). With no
665
+ // opts the shape is a universal SEQUENCE, byte-identical to every existing caller.
666
+ function spki(ns, opts) {
667
+ opts = opts || {};
640
668
  return schema.seq([
641
669
  schema.field("algorithm", algorithmIdentifier(ns)),
642
670
  schema.field("subjectPublicKey", schema.bitString()),
643
671
  ], {
644
- assert: "sequence", arity: { exact: 2 }, code: ns.prefix + "/bad-spki", what: "SubjectPublicKeyInfo",
672
+ assert: opts.implicitTag != null ? "implicit" : "sequence", implicitTag: opts.implicitTag,
673
+ arity: { exact: 2 }, code: ns.prefix + "/bad-spki", what: "SubjectPublicKeyInfo",
645
674
  build: function (m) {
646
675
  return {
647
676
  algorithm: m.fields.algorithm.value.result,
648
677
  publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
649
- bytes: m.node.bytes,
678
+ // `bytes` is the importable SubjectPublicKeyInfo DER. In IMPLICIT [tag]
679
+ // mode the wire node leads with the context tag, so recover the SEQUENCE
680
+ // encoding a consumer imports / hashes rather than the [tag] wire form.
681
+ bytes: opts.implicitTag != null ? asn1.sequenceTlv(m.node) : m.node.bytes,
650
682
  };
651
683
  },
652
684
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:69692b51-d096-4124-9a0c-886b9a5ad78e",
5
+ "serialNumber": "urn:uuid:b6db5d53-2a3e-46f9-9187-9c2f6fd9249b",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-06T19:48:47.311Z",
8
+ "timestamp": "2026-07-07T05:30:55.753Z",
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.1.16",
22
+ "bom-ref": "@blamejs/pki@0.1.17",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.16",
25
+ "version": "0.1.17",
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.1.16",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.17",
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.1.16",
57
+ "ref": "@blamejs/pki@0.1.17",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]