@blamejs/pki 0.1.17 → 0.1.19

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,928 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.cmp
6
+ * @nav Schema
7
+ * @title CMP
8
+ * @order 200
9
+ * @slug cmp
10
+ *
11
+ * @intro
12
+ * Certificate Management Protocol handling per RFC 9810 (which obsoletes
13
+ * RFC 4210 and RFC 9480). `parse` decodes a `PKIMessage` — the protected
14
+ * transport envelope CMP enrollment, revocation, confirmation, and support
15
+ * exchanges ride — into its header (version, sender / recipient
16
+ * GeneralNames including the NULL-DN anonymous form, nonces, transaction
17
+ * id, free text, general info), its body, its protection bits, and its
18
+ * extra certificates.
19
+ *
20
+ * The body is a 27-arm tagged CHOICE: certificate-request arms (`ir`,
21
+ * `cr`, `kur`, `krr`, `ccr`) are RFC 4211 CertReqMessages decoded by the
22
+ * CRMF parser; response, revocation, confirmation, error, general-message,
23
+ * and polling arms decode structurally (an encrypted certificate's
24
+ * EnvelopedData decodes via the CMS parser); every other defined arm is
25
+ * recognized and surfaced raw — `nested` messages are never auto-recursed.
26
+ * Protection is surfaced, not verified: the exact `headerBytes` and
27
+ * `bodyBytes` wire slices are exposed so an external verifier reconstructs
28
+ * the virtual ProtectedPart `SEQUENCE { header, body }` and checks the MAC
29
+ * or signature itself. DER-only, fail-closed.
30
+ *
31
+ * @card
32
+ * Parse DER / PEM RFC 9810 CMP PKIMessages — header, 27-arm body (requests
33
+ * via CRMF, encrypted certs via CMS, the rest structural or raw),
34
+ * protection inputs surfaced byte-exact for external verification,
35
+ * fail-closed.
36
+ */
37
+
38
+ var asn1 = require("./asn1-der.js");
39
+ var oid = require("./oid.js");
40
+ var schema = require("./schema-engine.js");
41
+ var pkix = require("./schema-pkix.js");
42
+ var cms = require("./schema-cms.js");
43
+ var crmf = require("./schema-crmf.js");
44
+ var frameworkError = require("./framework-error.js");
45
+
46
+ var CmpError = frameworkError.CmpError;
47
+ var PemError = frameworkError.PemError;
48
+ var NS = pkix.makeNS("cmp", CmpError, oid);
49
+ var TAGS = asn1.TAGS;
50
+
51
+ var OID_IMPLICIT_CONFIRM = oid.byName("implicitConfirm");
52
+ var OID_CONFIRM_WAIT_TIME = oid.byName("confirmWaitTime");
53
+ var OID_CERT_PROFILE = oid.byName("certProfile");
54
+ var OID_PKI_ARCHIVE_OPTIONS = oid.byName("pkiArchiveOptions");
55
+
56
+ // ---- leaves -----------------------------------------------------------
57
+
58
+ // CMP times are GeneralizedTime only (RFC 9810 §5.1.1).
59
+ var GENERALIZED_TIME = schema.decode(function (n, ctx) {
60
+ if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
61
+ throw ctx.E("cmp/bad-time", "CMP times must be GeneralizedTime (RFC 9810 §5.1.1)");
62
+ }
63
+ return asn1.read.time(n);
64
+ });
65
+
66
+ // A PKIFreeText element MUST be a UTF8String (§5.1.1).
67
+ var UTF8_TEXT = schema.decode(function (n, ctx) {
68
+ if (n.tagClass !== "universal" || n.tagNumber !== TAGS.UTF8_STRING) {
69
+ throw ctx.E("cmp/bad-freetext", "PKIFreeText elements must be UTF8String (RFC 9810 §5.1.1)");
70
+ }
71
+ return asn1.read.string(n);
72
+ });
73
+
74
+ // PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String.
75
+ var PKI_FREE_TEXT = schema.seqOf(UTF8_TEXT, {
76
+ min: 1, code: "cmp/bad-freetext", what: "PKIFreeText",
77
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
78
+ });
79
+
80
+ // PKIStatus ::= INTEGER {0..6} (§5.2.3) — a value whitelist, surfaced named.
81
+ var PKI_STATUS_NAMES = ["accepted", "grantedWithMods", "rejection", "waiting",
82
+ "revocationWarning", "revocationNotification", "keyUpdateWarning"];
83
+ var PKI_STATUS = schema.decode(function (n, ctx) {
84
+ var v = asn1.read.integer(n);
85
+ if (v < 0n || v > 6n) throw ctx.E("cmp/bad-status", "undefined PKIStatus " + v.toString() + " (RFC 9810 §5.2.3)");
86
+ var code = Number(v);
87
+ return { code: code, name: PKI_STATUS_NAMES[code] };
88
+ });
89
+
90
+ // PKIFailureInfo ::= BIT STRING named bits 0..26 (§5.2.3). X.690 §11.2.2:
91
+ // a named-bit BIT STRING drops trailing zero bits, so the final carried bit
92
+ // must be 1. Bits past 26 are extensible and surface as numeric indexes.
93
+ var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "badCertId",
94
+ "badDataFormat", "wrongAuthority", "incorrectData", "missingTimeStamp", "badPOP",
95
+ "certRevoked", "certConfirmed", "wrongIntegrity", "badRecipientNonce", "timeNotAvailable",
96
+ "unacceptedPolicy", "unacceptedExtension", "addInfoNotAvailable", "badSenderNonce",
97
+ "badCertTemplate", "signerNotTrusted", "transactionIdInUse", "unsupportedVersion",
98
+ "notAuthorized", "systemUnavail", "systemFailure", "duplicateCertReq"];
99
+ var PKI_FAILURE_INFO = schema.decode(function (n, ctx) {
100
+ var bs = asn1.read.bitString(n);
101
+ var total = bs.bytes.length * 8 - bs.unusedBits;
102
+ schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw ctx.E("cmp/bad-fail-info", m); });
103
+ var bits = [];
104
+ for (var i = 0; i < total; i++) {
105
+ if ((bs.bytes[i >> 3] >> (7 - (i % 8))) & 1) bits.push(i < FAIL_INFO_NAMES.length ? FAIL_INFO_NAMES[i] : i);
106
+ }
107
+ return { bits: bits, raw: bs };
108
+ });
109
+
110
+ // PollRep checkAfter is a delay in seconds — negative poisons scheduling, and
111
+ // the value surfaces as an exact number or not at all.
112
+ var CHECK_AFTER = schema.decode(function (n, ctx) {
113
+ var v = asn1.read.integer(n);
114
+ if (v < 0n || v > 2147483647n) throw ctx.E("cmp/bad-poll-rep", "checkAfter must be a non-negative delay in seconds (RFC 9810 §5.3.22)");
115
+ return Number(v);
116
+ });
117
+
118
+ // A raw certificate / CRL / publication-info element — surfaced byte-exact for
119
+ // downstream parsing, but structurally a universal SEQUENCE: a CMPCertificate
120
+ // is a Certificate, and CertificateList / PKIPublicationInfo are SEQUENCEs, so
121
+ // a primitive (an INTEGER) or wrong-tag element is not a valid structure and
122
+ // rejects rather than handing arbitrary bytes to certificate processing. The
123
+ // `code` names the containing structure (a shared leaf cannot know its context).
124
+ function rawSequence(code) {
125
+ return schema.decode(function (n, ctx) {
126
+ // A Certificate, CertificateList, and PKIPublicationInfo are each a
127
+ // NON-EMPTY universal SEQUENCE — an empty SEQUENCE (0x30 0x00) has the
128
+ // right tag but is none of them, so require at least one child rather than
129
+ // surface a degenerate structure as certificate/CRL bytes.
130
+ if (!(n.tagClass === "universal" && n.tagNumber === TAGS.SEQUENCE && n.children && n.children.length >= 1)) {
131
+ throw ctx.E(code, "expected a non-empty universal SEQUENCE (Certificate / CertificateList / PKIPublicationInfo)");
132
+ }
133
+ return n.bytes;
134
+ });
135
+ }
136
+
137
+ var GENERAL_NAME = pkix.generalName(NS, { decodeValue: true });
138
+ var ALG_ID = pkix.algorithmIdentifier(NS);
139
+ var EXTENSIONS = pkix.extensions(NS);
140
+
141
+ // ---- header ------------------------------------------------------------
142
+
143
+ // InfoTypeAndValue ::= SEQUENCE { infoType OID, infoValue ANY OPTIONAL }
144
+ // (§5.1.1, §5.3.19). Recognized id-it types with a fixed value syntax are
145
+ // validated (the CMS signed-attr value-syntax posture); all others surface
146
+ // raw with the registry name resolving.
147
+ var INFO_TYPE_AND_VALUE = schema.seq([
148
+ schema.field("infoType", schema.oidLeaf()),
149
+ schema.optional("infoValue", schema.any(), { whenAny: true }),
150
+ ], {
151
+ assert: "sequence", code: "cmp/bad-info-type-and-value", what: "InfoTypeAndValue",
152
+ build: function (m, ctx) {
153
+ var t = m.fields.infoType.value;
154
+ var valueNode = m.fields.infoValue.present ? m.fields.infoValue.value : null;
155
+ // A recognized id-it type with a FIXED value syntax (RFC 9810 §5.1.1) MUST
156
+ // carry its infoValue — implicitConfirm a NULL, confirmWaitTime a
157
+ // GeneralizedTime, certProfile a SEQUENCE OF UTF8String. A missing value is
158
+ // accepted only for the open / query id-it types (a bare OID in a genm body),
159
+ // never for a type whose value syntax the RFC defines.
160
+ if ((t === OID_IMPLICIT_CONFIRM || t === OID_CONFIRM_WAIT_TIME || t === OID_CERT_PROFILE) && valueNode === null) {
161
+ throw ctx.E("cmp/bad-info-value", "a recognized fixed-syntax id-it (implicitConfirm / confirmWaitTime / certProfile) must carry its infoValue (RFC 9810 §5.1.1)");
162
+ }
163
+ if (valueNode !== null) {
164
+ // A recognized id-it value is validated by CONTENT, not just tag: run the
165
+ // strict typed reader so a well-tagged but malformed payload (a non-empty
166
+ // NULL, a GeneralizedTime carrying garbage, an invalid-UTF-8 string)
167
+ // rejects rather than being surfaced as valid CMP.
168
+ if (t === OID_IMPLICIT_CONFIRM) {
169
+ if (!(valueNode.tagClass === "universal" && valueNode.tagNumber === TAGS.NULL)) {
170
+ throw ctx.E("cmp/bad-info-value", "an implicitConfirm value must be NULL (RFC 9810 §5.1.1.1)");
171
+ }
172
+ try { asn1.read.nullValue(valueNode); }
173
+ catch (e) { throw ctx.E("cmp/bad-info-value", "an implicitConfirm value must be an empty NULL (RFC 9810 §5.1.1.1)", e); }
174
+ }
175
+ if (t === OID_CONFIRM_WAIT_TIME) {
176
+ if (!(valueNode.tagClass === "universal" && valueNode.tagNumber === TAGS.GENERALIZED_TIME)) {
177
+ throw ctx.E("cmp/bad-info-value", "a confirmWaitTime value must be a GeneralizedTime (RFC 9810 §5.1.1.2)");
178
+ }
179
+ try { asn1.read.time(valueNode); }
180
+ catch (e) { throw ctx.E("cmp/bad-info-value", "a confirmWaitTime GeneralizedTime is malformed (RFC 9810 §5.1.1.2)", e); }
181
+ }
182
+ if (t === OID_CERT_PROFILE) {
183
+ if (!(valueNode.tagClass === "universal" && valueNode.tagNumber === TAGS.SEQUENCE &&
184
+ valueNode.children && valueNode.children.length >= 1)) {
185
+ throw ctx.E("cmp/bad-info-value", "a certProfile value must be a non-empty SEQUENCE OF UTF8String (RFC 9810 §5.1.1.4)");
186
+ }
187
+ for (var i = 0; i < valueNode.children.length; i++) {
188
+ var el = valueNode.children[i];
189
+ if (!(el.tagClass === "universal" && el.tagNumber === TAGS.UTF8_STRING)) {
190
+ throw ctx.E("cmp/bad-info-value", "a certProfile element must be a UTF8String (RFC 9810 §5.1.1.4)");
191
+ }
192
+ try { asn1.read.string(el); }
193
+ catch (e) { throw ctx.E("cmp/bad-info-value", "a certProfile element must be a valid UTF8String (RFC 9810 §5.1.1.4)", e); }
194
+ }
195
+ }
196
+ }
197
+ return { type: t, name: ctx.oid.name(t) || null, value: valueNode === null ? null : valueNode.bytes };
198
+ },
199
+ });
200
+
201
+ var GENERAL_INFO = schema.seqOf(INFO_TYPE_AND_VALUE, {
202
+ min: 1, code: "cmp/bad-general-info", what: "generalInfo",
203
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
204
+ });
205
+
206
+ // PKIHeader ::= SEQUENCE { pvno, sender, recipient, then EXPLICIT [0]..[8]
207
+ // optionals, ascending, at-most-once } (§5.1.1).
208
+ var PKI_HEADER = schema.seq([
209
+ schema.field("pvno", pkix.versionReader(NS, { "1": 1, "2": 2, "3": 3 })),
210
+ schema.field("sender", GENERAL_NAME),
211
+ schema.field("recipient", GENERAL_NAME),
212
+ schema.trailing([
213
+ { tag: 0, name: "messageTime", schema: GENERALIZED_TIME, explicit: true, emptyCode: "cmp/bad-header" },
214
+ { tag: 1, name: "protectionAlg", schema: ALG_ID, explicit: true, emptyCode: "cmp/bad-header" },
215
+ { tag: 2, name: "senderKID", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
216
+ { tag: 3, name: "recipKID", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
217
+ { tag: 4, name: "transactionID", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
218
+ { tag: 5, name: "senderNonce", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
219
+ { tag: 6, name: "recipNonce", schema: schema.octetString(), explicit: true, emptyCode: "cmp/bad-header" },
220
+ { tag: 7, name: "freeText", schema: PKI_FREE_TEXT, explicit: true, emptyCode: "cmp/bad-header" },
221
+ { tag: 8, name: "generalInfo", schema: GENERAL_INFO, explicit: true, emptyCode: "cmp/bad-header" },
222
+ ], { minTag: 0, maxTag: 8, unexpectedCode: "cmp/bad-header", orderCode: "cmp/bad-header" }),
223
+ ], {
224
+ assert: "sequence", code: "cmp/bad-header", what: "PKIHeader",
225
+ build: function (m) {
226
+ var f = m.fields;
227
+ return {
228
+ pvno: f.pvno.value,
229
+ sender: f.sender.value,
230
+ recipient: f.recipient.value,
231
+ messageTime: f.messageTime.present ? f.messageTime.value : null,
232
+ protectionAlg: f.protectionAlg.present ? f.protectionAlg.value.result : null,
233
+ senderKID: f.senderKID.present ? f.senderKID.value : null,
234
+ recipKID: f.recipKID.present ? f.recipKID.value : null,
235
+ transactionID: f.transactionID.present ? f.transactionID.value : null,
236
+ senderNonce: f.senderNonce.present ? f.senderNonce.value : null,
237
+ recipNonce: f.recipNonce.present ? f.recipNonce.value : null,
238
+ freeText: f.freeText.present ? f.freeText.value.result : null,
239
+ generalInfo: f.generalInfo.present ? f.generalInfo.value.result : null,
240
+ };
241
+ },
242
+ });
243
+
244
+ // ---- decoded body arms ---------------------------------------------------
245
+
246
+ // PKIStatusInfo ::= SEQUENCE { status, statusString OPTIONAL, failInfo OPTIONAL } (§5.2.3).
247
+ var PKI_STATUS_INFO = schema.seq([
248
+ schema.field("status", PKI_STATUS),
249
+ schema.optional("statusString", PKI_FREE_TEXT, { whenUniversal: [TAGS.SEQUENCE] }),
250
+ schema.optional("failInfo", PKI_FAILURE_INFO, { whenUniversal: [TAGS.BIT_STRING] }),
251
+ ], {
252
+ assert: "sequence", code: "cmp/bad-status-info", what: "PKIStatusInfo",
253
+ build: function (m) {
254
+ return {
255
+ status: m.fields.status.value,
256
+ statusString: m.fields.statusString.present ? m.fields.statusString.value.result : null,
257
+ failInfo: m.fields.failInfo.present ? m.fields.failInfo.value : null,
258
+ };
259
+ },
260
+ });
261
+
262
+ // ErrorMsgContent ::= SEQUENCE { pKIStatusInfo, errorCode INTEGER OPTIONAL,
263
+ // errorDetails PKIFreeText OPTIONAL } (§5.3.21).
264
+ var ERROR_MSG_CONTENT = schema.seq([
265
+ schema.field("pKIStatusInfo", PKI_STATUS_INFO),
266
+ schema.optional("errorCode", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
267
+ schema.optional("errorDetails", PKI_FREE_TEXT, { whenUniversal: [TAGS.SEQUENCE] }),
268
+ ], {
269
+ assert: "sequence", code: "cmp/bad-error", what: "ErrorMsgContent",
270
+ build: function (m) {
271
+ return {
272
+ pKIStatusInfo: m.fields.pKIStatusInfo.value.result,
273
+ errorCode: m.fields.errorCode.present ? m.fields.errorCode.value : null,
274
+ errorDetails: m.fields.errorDetails.present ? m.fields.errorDetails.value.result : null,
275
+ };
276
+ },
277
+ });
278
+
279
+ // CertStatus ::= SEQUENCE { certHash, certReqId, statusInfo OPTIONAL,
280
+ // hashAlg [0] OPTIONAL } (§5.3.18). certReqId accepts the -1 sentinel.
281
+ var CERT_STATUS = schema.seq([
282
+ schema.field("certHash", schema.octetString()),
283
+ schema.field("certReqId", schema.integerLeaf()),
284
+ schema.optional("statusInfo", PKI_STATUS_INFO, { whenUniversal: [TAGS.SEQUENCE] }),
285
+ schema.trailing([
286
+ { tag: 0, name: "hashAlg", schema: ALG_ID, explicit: true, emptyCode: "cmp/bad-cert-status" },
287
+ ], { minTag: 0, maxTag: 0, unexpectedCode: "cmp/bad-cert-status", orderCode: "cmp/bad-cert-status" }),
288
+ ], {
289
+ assert: "sequence", code: "cmp/bad-cert-status", what: "CertStatus",
290
+ build: function (m) {
291
+ return {
292
+ certHash: m.fields.certHash.value,
293
+ certReqId: m.fields.certReqId.value,
294
+ statusInfo: m.fields.statusInfo.present ? m.fields.statusInfo.value.result : null,
295
+ hashAlg: m.fields.hashAlg.present ? m.fields.hashAlg.value.result : null,
296
+ };
297
+ },
298
+ });
299
+
300
+ // CertConfirmContent ::= SEQUENCE OF CertStatus — an EMPTY sequence is legal
301
+ // (reject-all, §5.3.18), so no SIZE floor.
302
+ var CERT_CONFIRM_CONTENT = schema.seqOf(CERT_STATUS, {
303
+ code: "cmp/bad-cert-status", what: "CertConfirmContent",
304
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
305
+ });
306
+
307
+ // GenMsgContent / GenRepContent ::= SEQUENCE OF InfoTypeAndValue — no SIZE
308
+ // bound, empty legal (§5.3.19/.20).
309
+ var GEN_MSG_CONTENT = schema.seqOf(INFO_TYPE_AND_VALUE, {
310
+ code: "cmp/bad-info-type-and-value", what: "GenMsgContent",
311
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
312
+ });
313
+
314
+ // RevDetails ::= SEQUENCE { certDetails CertTemplate, crlEntryDetails
315
+ // Extensions OPTIONAL } (§5.3.9) — the CertTemplate interior (an IMPLICIT
316
+ // TAGS module) is owned by the CRMF parser and walked NS-bound there.
317
+ var CERT_TEMPLATE_LEAF = schema.decode(function (n) { return crmf.walkCertTemplate(n); });
318
+ var REV_DETAILS = schema.seq([
319
+ schema.field("certDetails", CERT_TEMPLATE_LEAF),
320
+ schema.optional("crlEntryDetails", EXTENSIONS, { whenUniversal: [TAGS.SEQUENCE] }),
321
+ ], {
322
+ assert: "sequence", code: "cmp/bad-rev-req", what: "RevDetails",
323
+ build: function (m) {
324
+ return {
325
+ certDetails: m.fields.certDetails.value,
326
+ crlEntryDetails: m.fields.crlEntryDetails.present ? m.fields.crlEntryDetails.value.result : null,
327
+ };
328
+ },
329
+ });
330
+ var REV_REQ_CONTENT = schema.seqOf(REV_DETAILS, {
331
+ code: "cmp/bad-rev-req", what: "RevReqContent",
332
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
333
+ });
334
+
335
+ // CertId ::= SEQUENCE { issuer GeneralName, serialNumber INTEGER } (RFC 4211).
336
+ var CERT_ID = schema.seq([
337
+ schema.field("issuer", GENERAL_NAME),
338
+ schema.field("serialNumber", schema.integerLeaf()),
339
+ ], {
340
+ assert: "sequence", code: "cmp/bad-rev-rep", what: "CertId",
341
+ build: function (m) {
342
+ return { issuer: m.fields.issuer.value, serialNumber: m.fields.serialNumber.value };
343
+ },
344
+ });
345
+
346
+ // RevRepContent ::= SEQUENCE { status SIZE(1..MAX), revCerts [0] OPTIONAL,
347
+ // crls [1] OPTIONAL } (§5.3.10).
348
+ var REV_REP_CONTENT = schema.seq([
349
+ schema.field("status", schema.seqOf(PKI_STATUS_INFO, {
350
+ min: 1, code: "cmp/bad-rev-rep", what: "RevRep status",
351
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
352
+ })),
353
+ schema.trailing([
354
+ { tag: 0, name: "revCerts", schema: schema.seqOf(CERT_ID, {
355
+ min: 1, code: "cmp/bad-rev-rep", what: "revCerts",
356
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
357
+ }), explicit: true, emptyCode: "cmp/bad-rev-rep" },
358
+ { tag: 1, name: "crls", schema: schema.seqOf(rawSequence("cmp/bad-rev-rep"), {
359
+ min: 1, code: "cmp/bad-rev-rep", what: "crls",
360
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
361
+ }), explicit: true, emptyCode: "cmp/bad-rev-rep" },
362
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "cmp/bad-rev-rep", orderCode: "cmp/bad-rev-rep" }),
363
+ ], {
364
+ assert: "sequence", code: "cmp/bad-rev-rep", what: "RevRepContent",
365
+ build: function (m) {
366
+ return {
367
+ status: m.fields.status.value.result,
368
+ revCerts: m.fields.revCerts.present ? m.fields.revCerts.value.result : null,
369
+ crls: m.fields.crls.present ? m.fields.crls.value.result : null,
370
+ };
371
+ },
372
+ });
373
+
374
+ // EncryptedKey ::= CHOICE { encryptedValue EncryptedValue (deprecated),
375
+ // envelopedData [0] EnvelopedData } (§5.2.2). EncryptedKey is imported from
376
+ // the RFC 4211 CRMF module, which is IMPLICIT TAGS, so `envelopedData [0]` is
377
+ // an IMPLICIT context tag REPLACING the EnvelopedData SEQUENCE tag — the [0]
378
+ // node's children ARE the EnvelopedData fields, no inner wrapper. Retag it to
379
+ // the universal SEQUENCE the CMS walker expects, exactly as the sibling CRMF
380
+ // `encryptedKey [4] EnvelopedData` POP arm does (schema-crmf.js). The
381
+ // deprecated encryptedValue arm surfaces RAW — never field-walked, so nothing
382
+ // dereferences its optional interior (absent algorithm parameters crash
383
+ // consumers that walk it blindly).
384
+ var ENCRYPTED_KEY = schema.choice([
385
+ { when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE },
386
+ schema: schema.decode(function (n) { return { encryptedValue: n.bytes }; }) },
387
+ { when: { tagClass: "context", tagNumber: 0 },
388
+ schema: schema.decode(function (n, ctx) {
389
+ var env;
390
+ // Wrap every EnvelopedData failure — a non-decodable retarget OR a CmsError
391
+ // from the composed walker — as the CMP domain error (with the original as
392
+ // the cause), so cmp.parse never escapes with a cms/* error for a malformed
393
+ // PKIMessage (the same wrapping the sibling CRMF encryptedKey POP arm does).
394
+ try { env = cms.walkEnvelopedData(asn1.decode(asn1.sequenceTlv(n))); }
395
+ catch (e) {
396
+ throw ctx.E("cmp/bad-cert-response", "envelopedData [0] must be a well-formed IMPLICIT EnvelopedData (RFC 9810 §5.2.2)", e);
397
+ }
398
+ // RFC 9810 §5.2.2 — a CMP EncryptedKey EnvelopedData is addressed to
399
+ // exactly one recipient (the end entity for a delivered cert / key). CMS
400
+ // permits RecipientInfos SET SIZE 1..MAX, so the single-recipient bound is
401
+ // CMP's: more than one recipient is a malformed EncryptedKey.
402
+ if (env.recipientInfos.length !== 1) {
403
+ throw ctx.E("cmp/bad-cert-response", "an EncryptedKey EnvelopedData must contain exactly one RecipientInfo (RFC 9810 §5.2.2)");
404
+ }
405
+ // The ciphertext IS the encrypted certificate / private key here, so an
406
+ // EnvelopedData with no ciphertext to decrypt — absent (detached, CMS
407
+ // permits it) OR present-but-empty — must reject (RFC 9810 §5.2.2/§5.3.4;
408
+ // the same rule the CRMF encryptedKey POP arm enforces).
409
+ var ct = env.encryptedContentInfo.encryptedContent;
410
+ if (ct === null || ct.length === 0) {
411
+ throw ctx.E("cmp/bad-cert-response", "an EncryptedKey envelopedData must carry non-empty attached ciphertext — its encryptedContent is the certificate/key payload (RFC 9810 §5.2.2)");
412
+ }
413
+ return { envelopedData: env };
414
+ }) },
415
+ ], { code: "cmp/bad-cert-response", what: "EncryptedKey" });
416
+
417
+ // CertOrEncCert ::= CHOICE { certificate [0], encryptedCert [1] } (§5.3.4).
418
+ var CERT_OR_ENC_CERT = schema.choice([
419
+ { when: { tagClass: "context", tagNumber: 0 },
420
+ schema: schema.explicit(0, schema.decode(function (n, ctx) {
421
+ if (!(n.tagClass === "universal" && n.tagNumber === TAGS.SEQUENCE && n.children && n.children.length >= 1)) {
422
+ throw ctx.E("cmp/bad-cert-response", "a certificate [0] must be a CMPCertificate (a non-empty universal SEQUENCE)");
423
+ }
424
+ return { certificate: n.bytes };
425
+ }), { code: "cmp/bad-cert-response" }) },
426
+ { when: { tagClass: "context", tagNumber: 1 },
427
+ schema: schema.explicit(1, ENCRYPTED_KEY, { code: "cmp/bad-cert-response" }) },
428
+ ], { code: "cmp/bad-cert-response", what: "CertOrEncCert" });
429
+
430
+ // CertifiedKeyPair ::= SEQUENCE { certOrEncCert, privateKey [0] OPTIONAL,
431
+ // publicationInfo [1] OPTIONAL } (§5.3.4).
432
+ var CERTIFIED_KEY_PAIR = schema.seq([
433
+ schema.field("certOrEncCert", CERT_OR_ENC_CERT),
434
+ schema.trailing([
435
+ { tag: 0, name: "privateKey", schema: ENCRYPTED_KEY, explicit: true, emptyCode: "cmp/bad-cert-response" },
436
+ { tag: 1, name: "publicationInfo", schema: rawSequence("cmp/bad-cert-response"), explicit: true, emptyCode: "cmp/bad-cert-response" },
437
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "cmp/bad-cert-response", orderCode: "cmp/bad-cert-response" }),
438
+ ], {
439
+ assert: "sequence", code: "cmp/bad-cert-response", what: "CertifiedKeyPair",
440
+ build: function (m) {
441
+ var out = m.fields.certOrEncCert.value.certificate !== undefined
442
+ ? { certificate: m.fields.certOrEncCert.value.certificate }
443
+ : { encryptedCert: m.fields.certOrEncCert.value };
444
+ out.privateKey = m.fields.privateKey.present ? m.fields.privateKey.value : null;
445
+ out.publicationInfo = m.fields.publicationInfo.present ? m.fields.publicationInfo.value : null;
446
+ return out;
447
+ },
448
+ });
449
+
450
+ // CertResponse ::= SEQUENCE { certReqId INTEGER (the -1 sentinel is legal),
451
+ // status PKIStatusInfo, certifiedKeyPair OPTIONAL, rspInfo OPTIONAL } (§5.3.4).
452
+ var CERT_RESPONSE = schema.seq([
453
+ schema.field("certReqId", schema.integerLeaf()),
454
+ schema.field("status", PKI_STATUS_INFO),
455
+ schema.optional("certifiedKeyPair", CERTIFIED_KEY_PAIR, { whenUniversal: [TAGS.SEQUENCE] }),
456
+ schema.optional("rspInfo", schema.octetString(), { whenUniversal: [TAGS.OCTET_STRING] }),
457
+ ], {
458
+ assert: "sequence", code: "cmp/bad-cert-response", what: "CertResponse",
459
+ build: function (m, ctx) {
460
+ var status = m.fields.status.value.result;
461
+ var certifiedKeyPair = m.fields.certifiedKeyPair.present ? m.fields.certifiedKeyPair.value.result : null;
462
+ // RFC 9810 §5.3.4 — only one of the failInfo (in PKIStatusInfo) and
463
+ // certifiedKeyPair fields can be present in a CertResponse: a rejection
464
+ // carries no certificate. Accepting both lets a caller keying off
465
+ // certifiedKeyPair process a certificate from a failed response.
466
+ if (status.failInfo !== null && certifiedKeyPair !== null) {
467
+ throw ctx.E("cmp/bad-cert-response",
468
+ "a CertResponse must not carry both failInfo and certifiedKeyPair (RFC 9810 §5.3.4)");
469
+ }
470
+ // A certifiedKeyPair is present ONLY when the status grants the certificate:
471
+ // accepted (0) or grantedWithMods (1). Any other status — rejection, waiting,
472
+ // the revocation / keyUpdate warnings — denies or defers the request, so a
473
+ // certificate under it is a malformed response even when no explicit failInfo
474
+ // bit is set (a rejection is commonly signalled by status alone). Keying the
475
+ // rule off failInfo presence alone would let a bare-rejection status ship a
476
+ // certificate (RFC 9810 §5.3.4).
477
+ if (certifiedKeyPair !== null && status.status.code !== 0 && status.status.code !== 1) {
478
+ throw ctx.E("cmp/bad-cert-response",
479
+ "a CertResponse certifiedKeyPair is allowed only under a granting status (accepted or grantedWithMods) (RFC 9810 §5.3.4)");
480
+ }
481
+ return {
482
+ certReqId: m.fields.certReqId.value,
483
+ status: status,
484
+ certifiedKeyPair: certifiedKeyPair,
485
+ rspInfo: m.fields.rspInfo.present ? m.fields.rspInfo.value : null,
486
+ };
487
+ },
488
+ });
489
+
490
+ // CertRepMessage ::= SEQUENCE { caPubs [1] OPTIONAL, response SEQUENCE OF
491
+ // CertResponse } (§5.3.4) — the optional precedes the required field (the
492
+ // x509-version shape). caPubs surface raw: the parser confers NO trust.
493
+ var CERT_REP_MESSAGE = schema.seq([
494
+ schema.optional("caPubs", schema.seqOf(rawSequence("cmp/bad-cert-rep"), {
495
+ min: 1, code: "cmp/bad-cert-rep", what: "caPubs",
496
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
497
+ }), { tag: 1, explicit: true, emptyCode: "cmp/bad-cert-rep" }),
498
+ schema.field("response", schema.seqOf(CERT_RESPONSE, {
499
+ code: "cmp/bad-cert-rep", what: "response",
500
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
501
+ })),
502
+ ], {
503
+ assert: "sequence", code: "cmp/bad-cert-rep", what: "CertRepMessage",
504
+ build: function (m) {
505
+ return {
506
+ caPubs: m.fields.caPubs.present ? m.fields.caPubs.value.result : null,
507
+ response: m.fields.response.value.result,
508
+ };
509
+ },
510
+ });
511
+
512
+ // KeyRecRepContent ::= SEQUENCE { status PKIStatusInfo, newSigCert [0]
513
+ // Certificate OPTIONAL, caCerts [1] SEQUENCE OF Certificate OPTIONAL,
514
+ // keyPairHist [2] SEQUENCE OF CertifiedKeyPair OPTIONAL } (§5.3.8). The
515
+ // keyPairHist CertifiedKeyPairs can carry the same EnvelopedData form as the
516
+ // enrollment responses, so krp is decoded (not surfaced raw) — the operator sees
517
+ // the status / key history and the cross-field EnvelopedData version check runs.
518
+ var KEY_REC_REP_CONTENT = schema.seq([
519
+ schema.field("status", PKI_STATUS_INFO),
520
+ schema.trailing([
521
+ { tag: 0, name: "newSigCert", schema: rawSequence("cmp/bad-key-rec-rep"), explicit: true, emptyCode: "cmp/bad-key-rec-rep" },
522
+ { tag: 1, name: "caCerts", schema: schema.seqOf(rawSequence("cmp/bad-key-rec-rep"), {
523
+ min: 1, code: "cmp/bad-key-rec-rep", what: "caCerts",
524
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
525
+ }), explicit: true, emptyCode: "cmp/bad-key-rec-rep" },
526
+ { tag: 2, name: "keyPairHist", schema: schema.seqOf(CERTIFIED_KEY_PAIR, {
527
+ min: 1, code: "cmp/bad-key-rec-rep", what: "keyPairHist",
528
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
529
+ }), explicit: true, emptyCode: "cmp/bad-key-rec-rep" },
530
+ ], { minTag: 0, maxTag: 2, unexpectedCode: "cmp/bad-key-rec-rep", orderCode: "cmp/bad-key-rec-rep" }),
531
+ ], {
532
+ assert: "sequence", code: "cmp/bad-key-rec-rep", what: "KeyRecRepContent",
533
+ build: function (m) {
534
+ return {
535
+ status: m.fields.status.value.result,
536
+ newSigCert: m.fields.newSigCert.present ? m.fields.newSigCert.value : null,
537
+ caCerts: m.fields.caCerts.present ? m.fields.caCerts.value.result : null,
538
+ keyPairHist: m.fields.keyPairHist.present ? m.fields.keyPairHist.value.result : null,
539
+ };
540
+ },
541
+ });
542
+
543
+ // PollReqContent / PollRepContent (§5.3.22) — certReqId may be -1.
544
+ var POLL_REQ_ENTRY = schema.seq([
545
+ schema.field("certReqId", schema.integerLeaf()),
546
+ ], {
547
+ assert: "sequence", code: "cmp/bad-poll-req", what: "PollReq entry",
548
+ build: function (m) { return { certReqId: m.fields.certReqId.value }; },
549
+ });
550
+ var POLL_REQ_CONTENT = schema.seqOf(POLL_REQ_ENTRY, {
551
+ code: "cmp/bad-poll-req", what: "PollReqContent",
552
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
553
+ });
554
+ var POLL_REP_ENTRY = schema.seq([
555
+ schema.field("certReqId", schema.integerLeaf()),
556
+ schema.field("checkAfter", CHECK_AFTER),
557
+ schema.optional("reason", PKI_FREE_TEXT, { whenUniversal: [TAGS.SEQUENCE] }),
558
+ ], {
559
+ assert: "sequence", code: "cmp/bad-poll-rep", what: "PollRep entry",
560
+ build: function (m) {
561
+ return {
562
+ certReqId: m.fields.certReqId.value,
563
+ checkAfter: m.fields.checkAfter.value,
564
+ reason: m.fields.reason.present ? m.fields.reason.value.result : null,
565
+ };
566
+ },
567
+ });
568
+ var POLL_REP_CONTENT = schema.seqOf(POLL_REP_ENTRY, {
569
+ code: "cmp/bad-poll-rep", what: "PollRepContent",
570
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
571
+ });
572
+
573
+ // ---- the 27-arm body dispatch ----------------------------------------------
574
+
575
+ // Tag -> arm name, verbatim from the PKIXCMP-2023 module (§5.1.2). Every
576
+ // defined arm is at minimum recognized; a subset decodes structurally.
577
+ var BODY_ARMS = ["ir", "ip", "cr", "cp", "p10cr", "popdecc", "popdecr", "kur", "kup",
578
+ "krr", "krp", "rr", "rp", "ccr", "ccp", "ckuann", "cann", "rann", "crlann",
579
+ "pkiconf", "nested", "genm", "genp", "error", "certConf", "pollReq", "pollRep"];
580
+ // Arms carrying RFC 4211 CertReqMessages — walked by the CRMF parser.
581
+ var CRMF_ARMS = { 0: true, 2: true, 7: true, 9: true, 13: true };
582
+ // Arms with a structural schema here.
583
+ var DECODED_ARMS = {
584
+ 1: CERT_REP_MESSAGE, 3: CERT_REP_MESSAGE, 8: CERT_REP_MESSAGE, 14: CERT_REP_MESSAGE,
585
+ 10: KEY_REC_REP_CONTENT,
586
+ 11: REV_REQ_CONTENT, 12: REV_REP_CONTENT,
587
+ 21: GEN_MSG_CONTENT, 22: GEN_MSG_CONTENT,
588
+ 23: ERROR_MSG_CONTENT, 24: CERT_CONFIRM_CONTENT,
589
+ 25: POLL_REQ_CONTENT, 26: POLL_REP_CONTENT,
590
+ };
591
+
592
+ // The open-CHOICE dispatch: tag -> arm name; walk where a schema exists;
593
+ // surface every other defined arm raw as { arm, tag, bytes } — `decoded` is
594
+ // ABSENT (not null) on raw arms, so its presence discriminates decoded-empty
595
+ // (pkiconf decodes to null) from recognized-undecoded. `nested` [20] is never
596
+ // auto-recursed: a self-nesting tower amplifies per-level walk products, so
597
+ // the operator re-feeds its bytes to parse explicitly.
598
+ var BODY = schema.decode(function (n, ctx) {
599
+ if (n.tagClass !== "context" || !n.children) {
600
+ throw ctx.E("cmp/bad-body", "PKIBody must be a constructed context-tagged CHOICE arm (RFC 9810 §5.1.2)");
601
+ }
602
+ if (n.tagNumber < 0 || n.tagNumber > 26) {
603
+ throw ctx.E("cmp/bad-body", "PKIBody tag [" + n.tagNumber + "] is not a defined arm (RFC 9810 §5.1.2)");
604
+ }
605
+ if (n.children.length !== 1) {
606
+ throw ctx.E("cmp/bad-body", "a PKIBody arm wraps exactly one value (X.690 §8.14)");
607
+ }
608
+ var inner = n.children[0];
609
+ var out = { arm: BODY_ARMS[n.tagNumber], tag: n.tagNumber, bytes: inner.bytes };
610
+ if (CRMF_ARMS[n.tagNumber]) {
611
+ // ccr [13] cross-certification: RFC 9810 Appendix D.6 profiles its template
612
+ // with signingAlg present and allows exactly one CertReqMsg (multiple
613
+ // cross-certificates go in separate PKIMessages).
614
+ var isCcr = n.tagNumber === 13;
615
+ out.decoded = crmf.walkCertReqMessages(inner, { allowSigningAlg: isCcr });
616
+ if (isCcr && out.decoded.messages.length !== 1) {
617
+ throw ctx.E("cmp/bad-body", "a cross-certification request (ccr) must contain exactly one CertReqMsg (RFC 9810 Appendix D.6)");
618
+ }
619
+ } else if (DECODED_ARMS[n.tagNumber]) {
620
+ var match = schema.walk(DECODED_ARMS[n.tagNumber], inner, ctx);
621
+ out.decoded = match.result;
622
+ } else if (n.tagNumber === 19) {
623
+ // PKIConfirmContent ::= NULL (§5.3.16).
624
+ if (!(inner.tagClass === "universal" && inner.tagNumber === TAGS.NULL)) {
625
+ throw ctx.E("cmp/bad-pkiconf", "PKIConfirmContent must be NULL (RFC 9810 §5.3.16)");
626
+ }
627
+ // A NULL is well-formed only with empty content (X.690 §8.8.2); the tag check
628
+ // alone would surface a non-empty NULL as a valid confirmation.
629
+ try { asn1.read.nullValue(inner); }
630
+ catch (e) { throw ctx.E("cmp/bad-pkiconf", "PKIConfirmContent must be an empty NULL (RFC 9810 §5.3.16)", e); }
631
+ out.decoded = null;
632
+ }
633
+ return out;
634
+ });
635
+
636
+ // ---- the message envelope -----------------------------------------------------
637
+
638
+ // PKIProtection ::= BIT STRING — raw bits for the external verifier.
639
+ var PROTECTION = schema.decode(function (n, ctx) {
640
+ var bs;
641
+ try { bs = asn1.read.bitString(n); }
642
+ catch (e) { throw ctx.E("cmp/bad-protection", "PKIProtection must be a BIT STRING (RFC 9810 §5.1.3)", e); }
643
+ return { unusedBits: bs.unusedBits, bytes: bs.bytes };
644
+ });
645
+
646
+ // PKIMessage ::= SEQUENCE { header, body, protection [0] OPTIONAL,
647
+ // extraCerts [1] OPTIONAL } (§5.1). The build runs the two cross-checks that
648
+ // span structures: protection presence <=> protectionAlg presence (§5.1.1,
649
+ // both directions), and a certConf hashAlg requires pvno cmp2021(3)
650
+ // (§5.3.18). headerBytes / bodyBytes are the exact wire slices protection is
651
+ // computed over (as DER-SEQUENCE(headerBytes || bodyBytes)) — surfaced raw,
652
+ // never re-encoded.
653
+ var PKI_MESSAGE = schema.seq([
654
+ schema.field("header", PKI_HEADER),
655
+ schema.field("body", BODY),
656
+ schema.trailing([
657
+ { tag: 0, name: "protection", schema: PROTECTION, explicit: true, emptyCode: "cmp/bad-protection" },
658
+ { tag: 1, name: "extraCerts", schema: schema.seqOf(rawSequence("cmp/bad-extra-certs"), {
659
+ min: 1, code: "cmp/bad-extra-certs", what: "extraCerts",
660
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
661
+ }), explicit: true, emptyCode: "cmp/bad-extra-certs" },
662
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "cmp/not-a-pki-message", orderCode: "cmp/not-a-pki-message" }),
663
+ ], {
664
+ assert: "sequence", code: "cmp/not-a-pki-message", what: "PKIMessage",
665
+ build: function (m, ctx) {
666
+ var header = m.fields.header.value.result;
667
+ var body = m.fields.body.value;
668
+ var protection = m.fields.protection.present ? m.fields.protection.value : null;
669
+ var extraCerts = m.fields.extraCerts.present ? m.fields.extraCerts.value.result : null;
670
+ if ((protection !== null) !== (header.protectionAlg !== null)) {
671
+ throw ctx.E("cmp/protection-alg-mismatch",
672
+ "protection bits and the header protectionAlg must be present together or absent together (RFC 9810 §5.1.1)");
673
+ }
674
+ if (body.arm === "certConf") {
675
+ for (var i = 0; i < body.decoded.length; i++) {
676
+ if (body.decoded[i].hashAlg !== null && header.pvno !== 3) {
677
+ throw ctx.E("cmp/bad-cert-status", "a certConf hashAlg requires CMP version cmp2021(3) (RFC 9810 §5.3.18)");
678
+ }
679
+ }
680
+ }
681
+ // RFC 9810 §5.2.2 / §7 — EnvelopedData is cmp2021(3) syntax: a response
682
+ // carrying it (an encryptedCert or a privateKey in the envelopedData form)
683
+ // under pvno < 3 is a version mismatch, the same version gate the certConf
684
+ // hashAlg rule applies. The deprecated EncryptedValue form is the pre-2021
685
+ // encoding and stays legal at pvno 2. CertifiedKeyPairs arrive both in the
686
+ // CertRepMessage `response` array (ip/cp/kup/ccp) and in the krp keyPairHist.
687
+ var ckps = [];
688
+ if (body.decoded && body.decoded.response) {
689
+ for (var r = 0; r < body.decoded.response.length; r++) {
690
+ if (body.decoded.response[r].certifiedKeyPair) ckps.push(body.decoded.response[r].certifiedKeyPair);
691
+ }
692
+ }
693
+ if (body.decoded && body.decoded.keyPairHist) {
694
+ for (var kh = 0; kh < body.decoded.keyPairHist.length; kh++) ckps.push(body.decoded.keyPairHist[kh]);
695
+ }
696
+ for (var ck = 0; ck < ckps.length; ck++) {
697
+ var usesEnveloped =
698
+ (ckps[ck].encryptedCert && ckps[ck].encryptedCert.envelopedData !== undefined) ||
699
+ (ckps[ck].privateKey && ckps[ck].privateKey.envelopedData !== undefined);
700
+ if (usesEnveloped && header.pvno !== 3) {
701
+ throw ctx.E("cmp/bad-version", "a response carrying EnvelopedData (encryptedCert or privateKey) requires CMP version cmp2021(3) (RFC 9810 §5.2.2, §7)");
702
+ }
703
+ }
704
+ // RFC 9810 §5.3.12 — a cross-certification response (ccp) reuses the
705
+ // CertRepMessage syntax "with the restriction that no encrypted private key
706
+ // can be sent": cross-certification certifies an existing CA's public key,
707
+ // so there is no key generation and CertifiedKeyPair.privateKey has no
708
+ // meaning. Reject a ccp that carries one rather than surface the key
709
+ // material; the field stays legal in the enrollment responses (ip/cp/kup).
710
+ if (body.arm === "ccp" && body.decoded && body.decoded.response) {
711
+ // RFC 9810 Appendix D.6 — a one-way cross-certification response carries
712
+ // exactly one CertResponse (multiple cross-certificates go in separate
713
+ // PKIMessages), the mirror of the ccr one-CertReqMsg rule.
714
+ if (body.decoded.response.length !== 1) {
715
+ throw ctx.E("cmp/bad-body", "a cross-certification response (ccp) must contain exactly one CertResponse (RFC 9810 Appendix D.6)");
716
+ }
717
+ for (var c = 0; c < body.decoded.response.length; c++) {
718
+ var ckpCcp = body.decoded.response[c].certifiedKeyPair;
719
+ if (ckpCcp && ckpCcp.privateKey !== null) {
720
+ throw ctx.E("cmp/bad-cert-response", "a cross-certification response (ccp) must not carry an encrypted private key (RFC 9810 §5.3.12)");
721
+ }
722
+ }
723
+ }
724
+ // RFC 9810 §5.3.11 — a cross-certification request (ccr) MUST NOT send the
725
+ // private key to the responding CA (the requesting CA generates and holds
726
+ // it). The private-key-carrying POP choices — encryptedKey (cmp2021) and the
727
+ // deprecated thisMessage (cmp2000) — are therefore forbidden in a ccr; the
728
+ // MAC (dhMAC / agreeMAC) and indirect (subsequentMessage) methods carry no
729
+ // key. Checked before the version gate so a ccr always gets the ccr verdict.
730
+ if (body.arm === "ccr" && body.decoded && body.decoded.messages) {
731
+ for (var cc = 0; cc < body.decoded.messages.length; cc++) {
732
+ var ccrMsg = body.decoded.messages[cc];
733
+ // Via the POP: the encryptedKey / thisMessage POPOPrivKey choices carry
734
+ // the private key.
735
+ var ccrPopo = ccrMsg.popo;
736
+ if (ccrPopo && (ccrPopo.method === "encryptedKey" || ccrPopo.method === "thisMessage")) {
737
+ throw ctx.E("cmp/bad-body", "a cross-certification request (ccr) must not send the private key — the " + ccrPopo.method + " POP choice is forbidden (RFC 9810 §5.3.11)");
738
+ }
739
+ // Via a control: a pkiArchiveOptions control with the encryptedPrivKey [0]
740
+ // arm is the other private-key transport (§5.2.8.3.1), likewise forbidden
741
+ // in a ccr. The control value is the raw PKIArchiveOptions CHOICE; the
742
+ // encryptedPrivKey arm is context tag [0].
743
+ var controls = ccrMsg.certReq.controls;
744
+ if (controls) {
745
+ for (var cn = 0; cn < controls.length; cn++) {
746
+ if (controls[cn].type !== OID_PKI_ARCHIVE_OPTIONS) continue;
747
+ var arch;
748
+ try { arch = asn1.decode(controls[cn].value); }
749
+ catch (e) { throw ctx.E("cmp/bad-body", "a ccr pkiArchiveOptions control did not decode (RFC 9810 §5.3.11)", e); }
750
+ if (arch.tagClass === "context" && arch.tagNumber === 0) {
751
+ throw ctx.E("cmp/bad-body", "a cross-certification request (ccr) must not send the private key — a pkiArchiveOptions encryptedPrivKey control is forbidden (RFC 9810 §5.3.11, §5.2.8.3.1)");
752
+ }
753
+ }
754
+ }
755
+ }
756
+ }
757
+ // RFC 9810 §5.2.8.3 — the agreeMAC and encryptedKey POPOPrivKey choices are
758
+ // cmp2021(3) syntax; a request (ir/cr/kur/krr/ccr) whose proof-of-possession
759
+ // uses either under pvno < 3 is a version mismatch, the same gate the
760
+ // response EnvelopedData rule applies. The CRMF walker surfaces the chosen
761
+ // POPOPrivKey method on each message's popo.
762
+ if (body.decoded && body.decoded.messages) {
763
+ for (var q = 0; q < body.decoded.messages.length; q++) {
764
+ var popo = body.decoded.messages[q].popo;
765
+ if (popo && (popo.method === "agreeMAC" || popo.method === "encryptedKey") && header.pvno !== 3) {
766
+ throw ctx.E("cmp/bad-version", "a request POP using the agreeMAC or encryptedKey choice requires CMP version cmp2021(3) (RFC 9810 §5.2.8.3)");
767
+ }
768
+ // §5.2.8.3.1/§7 — the same key transport via a pkiArchiveOptions control
769
+ // whose encryptedPrivKey [0] EXPLICIT wraps the envelopedData-form
770
+ // EncryptedKey is cmp2021 syntax too (a ccr rejects the control outright
771
+ // above; the other request arms are version-gated here).
772
+ var archCtrls = body.decoded.messages[q].certReq.controls;
773
+ if (archCtrls && header.pvno !== 3) {
774
+ for (var ac = 0; ac < archCtrls.length; ac++) {
775
+ if (archCtrls[ac].type !== OID_PKI_ARCHIVE_OPTIONS) continue;
776
+ var opt = asn1.decode(archCtrls[ac].value);
777
+ if (opt.tagClass === "context" && opt.tagNumber === 0 && opt.children && opt.children.length >= 1 &&
778
+ opt.children[0].tagClass === "context" && opt.children[0].tagNumber === 0) {
779
+ throw ctx.E("cmp/bad-version", "a pkiArchiveOptions encryptedPrivKey using the EnvelopedData form requires CMP version cmp2021(3) (RFC 9810 §5.2.8.3.1, §7)");
780
+ }
781
+ }
782
+ }
783
+ }
784
+ }
785
+ // RFC 9810 §5.1.1.4 — the certProfile name count is bounded by the message
786
+ // body: a p10cr carries at most one; an ir/cr/kur no more names than its
787
+ // CertReqMsg count, a genm no more than its GenMsgContent InfoTypeAndValue
788
+ // count (the names map positionally to the body entries). An overlong list
789
+ // makes that mapping ambiguous, so reject it.
790
+ if (header.generalInfo) {
791
+ for (var gi = 0; gi < header.generalInfo.length; gi++) {
792
+ if (header.generalInfo[gi].type !== OID_CERT_PROFILE) continue;
793
+ var profileCount = asn1.decode(header.generalInfo[gi].value).children.length;
794
+ var bodyCount = null;
795
+ if (body.arm === "p10cr") bodyCount = 1;
796
+ else if (body.decoded && body.decoded.messages) bodyCount = body.decoded.messages.length;
797
+ else if (body.arm === "genm" && Array.isArray(body.decoded)) bodyCount = body.decoded.length;
798
+ if (bodyCount !== null && profileCount > bodyCount) {
799
+ throw ctx.E("cmp/bad-info-value", "a certProfile carries " + profileCount + " profile name(s), more than the " + bodyCount + " the " + body.arm + " body permits (RFC 9810 §5.1.1.4)");
800
+ }
801
+ }
802
+ }
803
+ // RFC 9810 §5.3.13/§7 — the cAKeyUpdAnnV3 [0] RootCaKeyUpdateContent CA key
804
+ // update form is cmp2021(3) syntax; a ckuann using it under pvno < 3 is a
805
+ // version mismatch. ckuann surfaces raw, so inspect its top-level tag: the
806
+ // deprecated cAKeyUpdAnnV2 is a universal SEQUENCE, the v3 form is [0].
807
+ if (body.arm === "ckuann" && header.pvno !== 3) {
808
+ var caKeyUpd = asn1.decode(body.bytes);
809
+ if (caKeyUpd.tagClass === "context" && caKeyUpd.tagNumber === 0) {
810
+ throw ctx.E("cmp/bad-version", "a ckuann using the cAKeyUpdAnnV3 [0] RootCaKeyUpdateContent form requires CMP version cmp2021(3) (RFC 9810 §5.3.13, §7)");
811
+ }
812
+ }
813
+ return {
814
+ header: header,
815
+ headerBytes: m.fields.header.value.node.bytes,
816
+ body: body,
817
+ bodyBytes: m.fields.body.node.bytes,
818
+ protection: protection,
819
+ extraCerts: extraCerts,
820
+ };
821
+ },
822
+ });
823
+
824
+ /**
825
+ * @primitive pki.schema.cmp.parse
826
+ * @signature pki.schema.cmp.parse(input) -> pkiMessage
827
+ * @since 0.1.19
828
+ * @status experimental
829
+ * @spec RFC 9810, RFC 9483, RFC 9481
830
+ * @related pki.schema.parse, pki.schema.crmf.parse, pki.schema.cms.parse
831
+ *
832
+ * Parse a DER `Buffer` or a PEM string into a structured PKIMessage:
833
+ * `{ header, headerBytes, body, bodyBytes, protection, extraCerts }`.
834
+ * `header` carries `pvno` (1..3), `sender` / `recipient` (validated
835
+ * GeneralNames — the anonymous NULL-DN form included), and the optional
836
+ * `messageTime`, `protectionAlg`, `senderKID` / `recipKID`,
837
+ * `transactionID`, `senderNonce` / `recipNonce`, `freeText`, and
838
+ * `generalInfo` (recognized id-it types value-checked: implicitConfirm is
839
+ * NULL, confirmWaitTime a GeneralizedTime, certProfile a sequence of
840
+ * UTF8Strings). `body` is `{ arm, tag, bytes, decoded? }`: request arms
841
+ * (`ir` / `cr` / `kur` / `krr` / `ccr`) decode via the CRMF parser;
842
+ * `ip` / `cp` / `kup` / `ccp` decode to `{ caPubs, response }` (an encrypted
843
+ * certificate's EnvelopedData decodes via the CMS parser; the deprecated
844
+ * EncryptedValue arm and `caPubs` surface raw — the parser confers no
845
+ * trust); `krp` decodes to `{ status, newSigCert, caCerts, keyPairHist }`;
846
+ * `rr` / `rp`, `genm` / `genp`, `error`, `certConf` (an empty
847
+ * confirmation is the legal reject-all), and `pollReq` / `pollRep` decode
848
+ * structurally; `pkiconf` decodes to `null`; every other defined arm —
849
+ * `p10cr` (feed `body.bytes` to `pki.schema.csr.parse`), the
850
+ * challenge-response and announcement arms, and `nested` (never
851
+ * auto-recursed) — surfaces raw with `decoded` absent. `certReqId` values
852
+ * are BigInt and accept the protocol's -1 sentinel.
853
+ *
854
+ * Protection is surfaced, not verified: `protection` carries the raw BIT
855
+ * STRING, and the MAC or signature is computed over the DER of the virtual
856
+ * `ProtectedPart ::= SEQUENCE { header, body }` — reconstruct it as a DER
857
+ * SEQUENCE wrapping exactly `headerBytes || bodyBytes`. `extraCerts` are
858
+ * raw DER certificates and are NOT covered by protection.
859
+ *
860
+ * Throws `CmpError` when the bytes are not a well-formed PKIMessage, and
861
+ * `Asn1Error` when the underlying DER is malformed.
862
+ *
863
+ * @example
864
+ * var m = pki.schema.cmp.parse(der);
865
+ * m.body.arm; // "ir", "ip", "error", ...
866
+ * m.header.transactionID; // raw Buffer or null
867
+ */
868
+ var parse = pkix.makeParser({
869
+ pemLabel: "CMP", PemError: PemError, ErrorClass: CmpError,
870
+ prefix: "cmp", what: "PKIMessage", topSchema: PKI_MESSAGE, ns: NS,
871
+ });
872
+
873
+ /**
874
+ * @primitive pki.schema.cmp.pemDecode
875
+ * @signature pki.schema.cmp.pemDecode(text, label?) -> Buffer
876
+ * @since 0.1.19
877
+ * @status experimental
878
+ * @spec RFC 7468, RFC 9810
879
+ * @related pki.schema.cmp.parse
880
+ *
881
+ * Extract the DER bytes from a PEM block (default label `CMP`). CMP is
882
+ * wire-DER over HTTP (RFC 9811) — the PEM path is a convenience for
883
+ * messages that transit text channels.
884
+ *
885
+ * @example
886
+ * var der = pki.schema.cmp.pemDecode(pemText);
887
+ */
888
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CMP", PemError); }
889
+
890
+ /**
891
+ * @primitive pki.schema.cmp.pemEncode
892
+ * @signature pki.schema.cmp.pemEncode(der, label?) -> string
893
+ * @since 0.1.19
894
+ * @status experimental
895
+ * @spec RFC 7468
896
+ * @related pki.schema.cmp.pemDecode
897
+ *
898
+ * Wrap DER bytes in a PEM envelope (default label `CMP`).
899
+ *
900
+ * @example
901
+ * var pem = pki.schema.cmp.pemEncode(der);
902
+ */
903
+ function pemEncode(der, label) { return pkix.pemEncode(der, label || "CMP", PemError); }
904
+
905
+ // A PKIMessage root is a SEQUENCE of 2-4 whose first child (PKIHeader) is a
906
+ // SEQUENCE of >= 3 leading with a bare INTEGER (pvno) and whose second child
907
+ // (PKIBody) is context-class constructed [0..26]. The one overlap in the
908
+ // registry is one-directional: a 2-child PKIMessage with body ir [0] also
909
+ // satisfies the shallow ocsp-request probe, so this detector registers AHEAD
910
+ // of ocsp-request — while every real OCSPRequest fails here (its tbsRequest
911
+ // never leads with a bare INTEGER).
912
+ function matches(root) {
913
+ var k = pkix.rootSequenceChildren(root, 2, 4);
914
+ if (!k) return false;
915
+ var header = k[0];
916
+ if (!(schema.isUniversal(header, TAGS.SEQUENCE) && header.children && header.children.length >= 3)) return false;
917
+ var pvno = header.children[0];
918
+ if (!schema.isUniversal(pvno, TAGS.INTEGER)) return false;
919
+ var body = k[1];
920
+ return schema.isContextInRange(body, 0, 26) && !!body.children;
921
+ }
922
+
923
+ module.exports = {
924
+ parse: parse,
925
+ pemDecode: pemDecode,
926
+ pemEncode: pemEncode,
927
+ matches: matches,
928
+ };