@blamejs/pki 0.1.16 → 0.1.18

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.
@@ -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.embeddedDer(PKMAC_VALUE, asn1.sequenceTlv(inner), NS, { code: "crmf/bad-popo", what: "agreeMAC [3] PKMACValue" }); }
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
  *