@blamejs/pki 0.1.22 → 0.1.24

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/lib/schema-cmp.js CHANGED
@@ -10,9 +10,9 @@
10
10
  *
11
11
  * @intro
12
12
  * Certificate Management Protocol handling per RFC 9810 (which obsoletes
13
- * RFC 4210 and RFC 9480). `parse` decodes a `PKIMessage` the protected
13
+ * RFC 4210 and RFC 9480). `parse` decodes a `PKIMessage` -- the protected
14
14
  * transport envelope CMP enrollment, revocation, confirmation, and support
15
- * exchanges ride into its header (version, sender / recipient
15
+ * exchanges ride -- into its header (version, sender / recipient
16
16
  * GeneralNames including the NULL-DN anonymous form, nonces, transaction
17
17
  * id, free text, general info), its body, its protection bits, and its
18
18
  * extra certificates.
@@ -22,14 +22,14 @@
22
22
  * CRMF parser; response, revocation, confirmation, error, general-message,
23
23
  * and polling arms decode structurally (an encrypted certificate's
24
24
  * EnvelopedData decodes via the CMS parser); every other defined arm is
25
- * recognized and surfaced raw `nested` messages are never auto-recursed.
25
+ * recognized and surfaced raw -- `nested` messages are never auto-recursed.
26
26
  * Protection is surfaced, not verified: the exact `headerBytes` and
27
27
  * `bodyBytes` wire slices are exposed so an external verifier reconstructs
28
28
  * the virtual ProtectedPart `SEQUENCE { header, body }` and checks the MAC
29
29
  * or signature itself. DER-only, fail-closed.
30
30
  *
31
31
  * @card
32
- * Parse DER / PEM RFC 9810 CMP PKIMessages header, 27-arm body (requests
32
+ * Parse DER / PEM RFC 9810 CMP PKIMessages -- header, 27-arm body (requests
33
33
  * via CRMF, encrypted certs via CMS, the rest structural or raw),
34
34
  * protection inputs surfaced byte-exact for external verification,
35
35
  * fail-closed.
@@ -55,21 +55,10 @@ var OID_PKI_ARCHIVE_OPTIONS = oid.byName("pkiArchiveOptions");
55
55
 
56
56
  // ---- leaves -----------------------------------------------------------
57
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
- });
58
+ // CMP times are GeneralizedTime only; PKIFreeText elements are UTF8String
59
+ // (RFC 9810 sec. 5.1.1). Both ride the shared pkix leaf factories.
60
+ var GENERALIZED_TIME = pkix.generalizedTime(NS, { code: "cmp/bad-time", message: "CMP times must be GeneralizedTime (RFC 9810 sec. 5.1.1)" });
61
+ var UTF8_TEXT = pkix.utf8Text(NS, { code: "cmp/bad-freetext", message: "PKIFreeText elements must be UTF8String (RFC 9810 sec. 5.1.1)" });
73
62
 
74
63
  // PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String.
75
64
  var PKI_FREE_TEXT = schema.seqOf(UTF8_TEXT, {
@@ -77,17 +66,17 @@ var PKI_FREE_TEXT = schema.seqOf(UTF8_TEXT, {
77
66
  build: function (m) { return m.items.map(function (it) { return it.value; }); },
78
67
  });
79
68
 
80
- // PKIStatus ::= INTEGER {0..6} (§5.2.3) a value whitelist, surfaced named.
69
+ // PKIStatus ::= INTEGER {0..6} (sec. 5.2.3) -- a value whitelist, surfaced named.
81
70
  var PKI_STATUS_NAMES = ["accepted", "grantedWithMods", "rejection", "waiting",
82
71
  "revocationWarning", "revocationNotification", "keyUpdateWarning"];
83
72
  var PKI_STATUS = schema.decode(function (n, ctx) {
84
73
  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)");
74
+ if (v < 0n || v > 6n) throw ctx.E("cmp/bad-status", "undefined PKIStatus " + v.toString() + " (RFC 9810 sec. 5.2.3)");
86
75
  var code = Number(v);
87
76
  return { code: code, name: PKI_STATUS_NAMES[code] };
88
77
  });
89
78
 
90
- // PKIFailureInfo ::= BIT STRING named bits 0..26 (§5.2.3). X.690 §11.2.2:
79
+ // PKIFailureInfo ::= BIT STRING named bits 0..26 (sec. 5.2.3). X.690 sec. 11.2.2:
91
80
  // a named-bit BIT STRING drops trailing zero bits, so the final carried bit
92
81
  // must be 1. Bits past 26 are extensible and surface as numeric indexes.
93
82
  var FAIL_INFO_NAMES = ["badAlg", "badMessageCheck", "badRequest", "badTime", "badCertId",
@@ -107,31 +96,22 @@ var PKI_FAILURE_INFO = schema.decode(function (n, ctx) {
107
96
  return { bits: bits, raw: bs };
108
97
  });
109
98
 
110
- // PollRep checkAfter is a delay in seconds negative poisons scheduling, and
99
+ // PollRep checkAfter is a delay in seconds -- negative poisons scheduling, and
111
100
  // the value surfaces as an exact number or not at all.
112
101
  var CHECK_AFTER = schema.decode(function (n, ctx) {
113
102
  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)");
103
+ if (v < 0n || v > 2147483647n) throw ctx.E("cmp/bad-poll-rep", "checkAfter must be a non-negative delay in seconds (RFC 9810 sec. 5.3.22)");
115
104
  return Number(v);
116
105
  });
117
106
 
118
- // A raw certificate / CRL / publication-info element surfaced byte-exact for
107
+ // A raw certificate / CRL / publication-info element -- surfaced byte-exact for
119
108
  // downstream parsing, but structurally a universal SEQUENCE: a CMPCertificate
120
109
  // is a Certificate, and CertificateList / PKIPublicationInfo are SEQUENCEs, so
121
110
  // a primitive (an INTEGER) or wrong-tag element is not a valid structure and
122
111
  // rejects rather than handing arbitrary bytes to certificate processing. The
123
112
  // `code` names the containing structure (a shared leaf cannot know its context).
124
113
  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
- });
114
+ return pkix.rawNonEmptySequence(NS, { code: code, message: "expected a non-empty universal SEQUENCE (Certificate / CertificateList / PKIPublicationInfo)" });
135
115
  }
136
116
 
137
117
  var GENERAL_NAME = pkix.generalName(NS, { decodeValue: true });
@@ -141,7 +121,7 @@ var EXTENSIONS = pkix.extensions(NS);
141
121
  // ---- header ------------------------------------------------------------
142
122
 
143
123
  // InfoTypeAndValue ::= SEQUENCE { infoType OID, infoValue ANY OPTIONAL }
144
- // (§5.1.1, §5.3.19). Recognized id-it types with a fixed value syntax are
124
+ // (sec. 5.1.1, sec. 5.3.19). Recognized id-it types with a fixed value syntax are
145
125
  // validated (the CMS signed-attr value-syntax posture); all others surface
146
126
  // raw with the registry name resolving.
147
127
  var INFO_TYPE_AND_VALUE = schema.seq([
@@ -152,13 +132,13 @@ var INFO_TYPE_AND_VALUE = schema.seq([
152
132
  build: function (m, ctx) {
153
133
  var t = m.fields.infoType.value;
154
134
  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
135
+ // A recognized id-it type with a FIXED value syntax (RFC 9810 sec. 5.1.1) MUST
136
+ // carry its infoValue -- implicitConfirm a NULL, confirmWaitTime a
157
137
  // GeneralizedTime, certProfile a SEQUENCE OF UTF8String. A missing value is
158
138
  // accepted only for the open / query id-it types (a bare OID in a genm body),
159
139
  // never for a type whose value syntax the RFC defines.
160
140
  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)");
141
+ throw ctx.E("cmp/bad-info-value", "a recognized fixed-syntax id-it (implicitConfirm / confirmWaitTime / certProfile) must carry its infoValue (RFC 9810 sec. 5.1.1)");
162
142
  }
163
143
  if (valueNode !== null) {
164
144
  // A recognized id-it value is validated by CONTENT, not just tag: run the
@@ -167,30 +147,30 @@ var INFO_TYPE_AND_VALUE = schema.seq([
167
147
  // rejects rather than being surfaced as valid CMP.
168
148
  if (t === OID_IMPLICIT_CONFIRM) {
169
149
  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)");
150
+ throw ctx.E("cmp/bad-info-value", "an implicitConfirm value must be NULL (RFC 9810 sec. 5.1.1.1)");
171
151
  }
172
152
  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); }
153
+ catch (e) { throw ctx.E("cmp/bad-info-value", "an implicitConfirm value must be an empty NULL (RFC 9810 sec. 5.1.1.1)", e); }
174
154
  }
175
155
  if (t === OID_CONFIRM_WAIT_TIME) {
176
156
  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)");
157
+ throw ctx.E("cmp/bad-info-value", "a confirmWaitTime value must be a GeneralizedTime (RFC 9810 sec. 5.1.1.2)");
178
158
  }
179
159
  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); }
160
+ catch (e) { throw ctx.E("cmp/bad-info-value", "a confirmWaitTime GeneralizedTime is malformed (RFC 9810 sec. 5.1.1.2)", e); }
181
161
  }
182
162
  if (t === OID_CERT_PROFILE) {
183
163
  if (!(valueNode.tagClass === "universal" && valueNode.tagNumber === TAGS.SEQUENCE &&
184
164
  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)");
165
+ throw ctx.E("cmp/bad-info-value", "a certProfile value must be a non-empty SEQUENCE OF UTF8String (RFC 9810 sec. 5.1.1.4)");
186
166
  }
187
167
  for (var i = 0; i < valueNode.children.length; i++) {
188
168
  var el = valueNode.children[i];
189
169
  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)");
170
+ throw ctx.E("cmp/bad-info-value", "a certProfile element must be a UTF8String (RFC 9810 sec. 5.1.1.4)");
191
171
  }
192
172
  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); }
173
+ catch (e) { throw ctx.E("cmp/bad-info-value", "a certProfile element must be a valid UTF8String (RFC 9810 sec. 5.1.1.4)", e); }
194
174
  }
195
175
  }
196
176
  }
@@ -204,7 +184,7 @@ var GENERAL_INFO = schema.seqOf(INFO_TYPE_AND_VALUE, {
204
184
  });
205
185
 
206
186
  // PKIHeader ::= SEQUENCE { pvno, sender, recipient, then EXPLICIT [0]..[8]
207
- // optionals, ascending, at-most-once } (§5.1.1).
187
+ // optionals, ascending, at-most-once } (sec. 5.1.1).
208
188
  var PKI_HEADER = schema.seq([
209
189
  schema.field("pvno", pkix.versionReader(NS, { "1": 1, "2": 2, "3": 3 })),
210
190
  schema.field("sender", GENERAL_NAME),
@@ -243,7 +223,7 @@ var PKI_HEADER = schema.seq([
243
223
 
244
224
  // ---- decoded body arms ---------------------------------------------------
245
225
 
246
- // PKIStatusInfo ::= SEQUENCE { status, statusString OPTIONAL, failInfo OPTIONAL } (§5.2.3).
226
+ // PKIStatusInfo ::= SEQUENCE { status, statusString OPTIONAL, failInfo OPTIONAL } (sec. 5.2.3).
247
227
  var PKI_STATUS_INFO = schema.seq([
248
228
  schema.field("status", PKI_STATUS),
249
229
  schema.optional("statusString", PKI_FREE_TEXT, { whenUniversal: [TAGS.SEQUENCE] }),
@@ -260,7 +240,7 @@ var PKI_STATUS_INFO = schema.seq([
260
240
  });
261
241
 
262
242
  // ErrorMsgContent ::= SEQUENCE { pKIStatusInfo, errorCode INTEGER OPTIONAL,
263
- // errorDetails PKIFreeText OPTIONAL } (§5.3.21).
243
+ // errorDetails PKIFreeText OPTIONAL } (sec. 5.3.21).
264
244
  var ERROR_MSG_CONTENT = schema.seq([
265
245
  schema.field("pKIStatusInfo", PKI_STATUS_INFO),
266
246
  schema.optional("errorCode", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
@@ -277,7 +257,7 @@ var ERROR_MSG_CONTENT = schema.seq([
277
257
  });
278
258
 
279
259
  // CertStatus ::= SEQUENCE { certHash, certReqId, statusInfo OPTIONAL,
280
- // hashAlg [0] OPTIONAL } (§5.3.18). certReqId accepts the -1 sentinel.
260
+ // hashAlg [0] OPTIONAL } (sec. 5.3.18). certReqId accepts the -1 sentinel.
281
261
  var CERT_STATUS = schema.seq([
282
262
  schema.field("certHash", schema.octetString()),
283
263
  schema.field("certReqId", schema.integerLeaf()),
@@ -297,22 +277,22 @@ var CERT_STATUS = schema.seq([
297
277
  },
298
278
  });
299
279
 
300
- // CertConfirmContent ::= SEQUENCE OF CertStatus an EMPTY sequence is legal
301
- // (reject-all, §5.3.18), so no SIZE floor.
280
+ // CertConfirmContent ::= SEQUENCE OF CertStatus -- an EMPTY sequence is legal
281
+ // (reject-all, sec. 5.3.18), so no SIZE floor.
302
282
  var CERT_CONFIRM_CONTENT = schema.seqOf(CERT_STATUS, {
303
283
  code: "cmp/bad-cert-status", what: "CertConfirmContent",
304
284
  build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
305
285
  });
306
286
 
307
- // GenMsgContent / GenRepContent ::= SEQUENCE OF InfoTypeAndValue no SIZE
308
- // bound, empty legal (§5.3.19/.20).
287
+ // GenMsgContent / GenRepContent ::= SEQUENCE OF InfoTypeAndValue -- no SIZE
288
+ // bound, empty legal (sec. 5.3.19/.20).
309
289
  var GEN_MSG_CONTENT = schema.seqOf(INFO_TYPE_AND_VALUE, {
310
290
  code: "cmp/bad-info-type-and-value", what: "GenMsgContent",
311
291
  build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
312
292
  });
313
293
 
314
294
  // RevDetails ::= SEQUENCE { certDetails CertTemplate, crlEntryDetails
315
- // Extensions OPTIONAL } (§5.3.9) the CertTemplate interior (an IMPLICIT
295
+ // Extensions OPTIONAL } (sec. 5.3.9) -- the CertTemplate interior (an IMPLICIT
316
296
  // TAGS module) is owned by the CRMF parser and walked NS-bound there.
317
297
  var CERT_TEMPLATE_LEAF = schema.decode(function (n) { return crmf.walkCertTemplate(n); });
318
298
  var REV_DETAILS = schema.seq([
@@ -344,7 +324,7 @@ var CERT_ID = schema.seq([
344
324
  });
345
325
 
346
326
  // RevRepContent ::= SEQUENCE { status SIZE(1..MAX), revCerts [0] OPTIONAL,
347
- // crls [1] OPTIONAL } (§5.3.10).
327
+ // crls [1] OPTIONAL } (sec. 5.3.10).
348
328
  var REV_REP_CONTENT = schema.seq([
349
329
  schema.field("status", schema.seqOf(PKI_STATUS_INFO, {
350
330
  min: 1, code: "cmp/bad-rev-rep", what: "RevRep status",
@@ -372,49 +352,59 @@ var REV_REP_CONTENT = schema.seq([
372
352
  });
373
353
 
374
354
  // EncryptedKey ::= CHOICE { encryptedValue EncryptedValue (deprecated),
375
- // envelopedData [0] EnvelopedData } (§5.2.2). EncryptedKey is imported from
355
+ // envelopedData [0] EnvelopedData } (sec. 5.2.2). EncryptedKey is imported from
376
356
  // 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]
357
+ // an IMPLICIT context tag REPLACING the EnvelopedData SEQUENCE tag -- the [0]
378
358
  // node's children ARE the EnvelopedData fields, no inner wrapper. Retag it to
379
359
  // the universal SEQUENCE the CMS walker expects, exactly as the sibling CRMF
380
360
  // `encryptedKey [4] EnvelopedData` POP arm does (schema-crmf.js). The
381
- // deprecated encryptedValue arm surfaces RAW never field-walked, so nothing
361
+ // deprecated encryptedValue arm surfaces RAW -- never field-walked, so nothing
382
362
  // dereferences its optional interior (absent algorithm parameters crash
383
363
  // consumers that walk it blindly).
384
364
  var ENCRYPTED_KEY = schema.choice([
385
365
  { when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE },
386
- schema: schema.decode(function (n) { return { encryptedValue: n.bytes }; }) },
366
+ schema: schema.decode(function (n, ctx) {
367
+ // An RFC 4211 sec. 2 EncryptedValue REQUIRES encValue (its final BIT STRING
368
+ // field), so an EMPTY SEQUENCE has the right tag but is not any valid
369
+ // EncryptedValue -- require at least one child rather than surface a
370
+ // degenerate structure as encrypted certificate/key bytes (the same
371
+ // non-empty rule every raw-SEQUENCE surface in this module applies).
372
+ if (!(n.children && n.children.length >= 1)) {
373
+ throw ctx.E("cmp/bad-cert-response", "an EncryptedValue must be a non-empty SEQUENCE (RFC 4211 sec. 2)");
374
+ }
375
+ return { encryptedValue: n.bytes };
376
+ }) },
387
377
  { when: { tagClass: "context", tagNumber: 0 },
388
378
  schema: schema.decode(function (n, ctx) {
389
379
  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
380
+ // Wrap every EnvelopedData failure -- a non-decodable retarget OR a CmsError
381
+ // from the composed walker -- as the CMP domain error (with the original as
392
382
  // the cause), so cmp.parse never escapes with a cms/* error for a malformed
393
383
  // PKIMessage (the same wrapping the sibling CRMF encryptedKey POP arm does).
394
384
  try { env = cms.walkEnvelopedData(asn1.decode(asn1.sequenceTlv(n))); }
395
385
  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);
386
+ throw ctx.E("cmp/bad-cert-response", "envelopedData [0] must be a well-formed IMPLICIT EnvelopedData (RFC 9810 sec. 5.2.2)", e);
397
387
  }
398
- // RFC 9810 §5.2.2 a CMP EncryptedKey EnvelopedData is addressed to
388
+ // RFC 9810 sec. 5.2.2 -- a CMP EncryptedKey EnvelopedData is addressed to
399
389
  // exactly one recipient (the end entity for a delivered cert / key). CMS
400
390
  // permits RecipientInfos SET SIZE 1..MAX, so the single-recipient bound is
401
391
  // CMP's: more than one recipient is a malformed EncryptedKey.
402
392
  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)");
393
+ throw ctx.E("cmp/bad-cert-response", "an EncryptedKey EnvelopedData must contain exactly one RecipientInfo (RFC 9810 sec. 5.2.2)");
404
394
  }
405
395
  // 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.25.3.4;
396
+ // EnvelopedData with no ciphertext to decrypt -- absent (detached, CMS
397
+ // permits it) OR present-but-empty -- must reject (RFC 9810 sec. 5.2.2/sec. 5.3.4;
408
398
  // the same rule the CRMF encryptedKey POP arm enforces).
409
399
  var ct = env.encryptedContentInfo.encryptedContent;
410
400
  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)");
401
+ 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 sec. 5.2.2)");
412
402
  }
413
403
  return { envelopedData: env };
414
404
  }) },
415
405
  ], { code: "cmp/bad-cert-response", what: "EncryptedKey" });
416
406
 
417
- // CertOrEncCert ::= CHOICE { certificate [0], encryptedCert [1] } (§5.3.4).
407
+ // CertOrEncCert ::= CHOICE { certificate [0], encryptedCert [1] } (sec. 5.3.4).
418
408
  var CERT_OR_ENC_CERT = schema.choice([
419
409
  { when: { tagClass: "context", tagNumber: 0 },
420
410
  schema: schema.explicit(0, schema.decode(function (n, ctx) {
@@ -428,7 +418,7 @@ var CERT_OR_ENC_CERT = schema.choice([
428
418
  ], { code: "cmp/bad-cert-response", what: "CertOrEncCert" });
429
419
 
430
420
  // CertifiedKeyPair ::= SEQUENCE { certOrEncCert, privateKey [0] OPTIONAL,
431
- // publicationInfo [1] OPTIONAL } (§5.3.4).
421
+ // publicationInfo [1] OPTIONAL } (sec. 5.3.4).
432
422
  var CERTIFIED_KEY_PAIR = schema.seq([
433
423
  schema.field("certOrEncCert", CERT_OR_ENC_CERT),
434
424
  schema.trailing([
@@ -448,7 +438,7 @@ var CERTIFIED_KEY_PAIR = schema.seq([
448
438
  });
449
439
 
450
440
  // CertResponse ::= SEQUENCE { certReqId INTEGER (the -1 sentinel is legal),
451
- // status PKIStatusInfo, certifiedKeyPair OPTIONAL, rspInfo OPTIONAL } (§5.3.4).
441
+ // status PKIStatusInfo, certifiedKeyPair OPTIONAL, rspInfo OPTIONAL } (sec. 5.3.4).
452
442
  var CERT_RESPONSE = schema.seq([
453
443
  schema.field("certReqId", schema.integerLeaf()),
454
444
  schema.field("status", PKI_STATUS_INFO),
@@ -459,24 +449,24 @@ var CERT_RESPONSE = schema.seq([
459
449
  build: function (m, ctx) {
460
450
  var status = m.fields.status.value.result;
461
451
  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
452
+ // RFC 9810 sec. 5.3.4 -- only one of the failInfo (in PKIStatusInfo) and
463
453
  // certifiedKeyPair fields can be present in a CertResponse: a rejection
464
454
  // carries no certificate. Accepting both lets a caller keying off
465
455
  // certifiedKeyPair process a certificate from a failed response.
466
456
  if (status.failInfo !== null && certifiedKeyPair !== null) {
467
457
  throw ctx.E("cmp/bad-cert-response",
468
- "a CertResponse must not carry both failInfo and certifiedKeyPair (RFC 9810 §5.3.4)");
458
+ "a CertResponse must not carry both failInfo and certifiedKeyPair (RFC 9810 sec. 5.3.4)");
469
459
  }
470
460
  // 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
461
+ // accepted (0) or grantedWithMods (1). Any other status -- rejection, waiting,
462
+ // the revocation / keyUpdate warnings -- denies or defers the request, so a
473
463
  // certificate under it is a malformed response even when no explicit failInfo
474
464
  // bit is set (a rejection is commonly signalled by status alone). Keying the
475
465
  // rule off failInfo presence alone would let a bare-rejection status ship a
476
- // certificate (RFC 9810 §5.3.4).
466
+ // certificate (RFC 9810 sec. 5.3.4).
477
467
  if (certifiedKeyPair !== null && status.status.code !== 0 && status.status.code !== 1) {
478
468
  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)");
469
+ "a CertResponse certifiedKeyPair is allowed only under a granting status (accepted or grantedWithMods) (RFC 9810 sec. 5.3.4)");
480
470
  }
481
471
  return {
482
472
  certReqId: m.fields.certReqId.value,
@@ -488,7 +478,7 @@ var CERT_RESPONSE = schema.seq([
488
478
  });
489
479
 
490
480
  // CertRepMessage ::= SEQUENCE { caPubs [1] OPTIONAL, response SEQUENCE OF
491
- // CertResponse } (§5.3.4) the optional precedes the required field (the
481
+ // CertResponse } (sec. 5.3.4) -- the optional precedes the required field (the
492
482
  // x509-version shape). caPubs surface raw: the parser confers NO trust.
493
483
  var CERT_REP_MESSAGE = schema.seq([
494
484
  schema.optional("caPubs", schema.seqOf(rawSequence("cmp/bad-cert-rep"), {
@@ -511,9 +501,9 @@ var CERT_REP_MESSAGE = schema.seq([
511
501
 
512
502
  // KeyRecRepContent ::= SEQUENCE { status PKIStatusInfo, newSigCert [0]
513
503
  // Certificate OPTIONAL, caCerts [1] SEQUENCE OF Certificate OPTIONAL,
514
- // keyPairHist [2] SEQUENCE OF CertifiedKeyPair OPTIONAL } (§5.3.8). The
504
+ // keyPairHist [2] SEQUENCE OF CertifiedKeyPair OPTIONAL } (sec. 5.3.8). The
515
505
  // keyPairHist CertifiedKeyPairs can carry the same EnvelopedData form as the
516
- // enrollment responses, so krp is decoded (not surfaced raw) the operator sees
506
+ // enrollment responses, so krp is decoded (not surfaced raw) -- the operator sees
517
507
  // the status / key history and the cross-field EnvelopedData version check runs.
518
508
  var KEY_REC_REP_CONTENT = schema.seq([
519
509
  schema.field("status", PKI_STATUS_INFO),
@@ -540,7 +530,7 @@ var KEY_REC_REP_CONTENT = schema.seq([
540
530
  },
541
531
  });
542
532
 
543
- // PollReqContent / PollRepContent (§5.3.22) certReqId may be -1.
533
+ // PollReqContent / PollRepContent (sec. 5.3.22) -- certReqId may be -1.
544
534
  var POLL_REQ_ENTRY = schema.seq([
545
535
  schema.field("certReqId", schema.integerLeaf()),
546
536
  ], {
@@ -572,12 +562,12 @@ var POLL_REP_CONTENT = schema.seqOf(POLL_REP_ENTRY, {
572
562
 
573
563
  // ---- the 27-arm body dispatch ----------------------------------------------
574
564
 
575
- // Tag -> arm name, verbatim from the PKIXCMP-2023 module (§5.1.2). Every
565
+ // Tag -> arm name, verbatim from the PKIXCMP-2023 module (sec. 5.1.2). Every
576
566
  // defined arm is at minimum recognized; a subset decodes structurally.
577
567
  var BODY_ARMS = ["ir", "ip", "cr", "cp", "p10cr", "popdecc", "popdecr", "kur", "kup",
578
568
  "krr", "krp", "rr", "rp", "ccr", "ccp", "ckuann", "cann", "rann", "crlann",
579
569
  "pkiconf", "nested", "genm", "genp", "error", "certConf", "pollReq", "pollRep"];
580
- // Arms carrying RFC 4211 CertReqMessages walked by the CRMF parser.
570
+ // Arms carrying RFC 4211 CertReqMessages -- walked by the CRMF parser.
581
571
  var CRMF_ARMS = { 0: true, 2: true, 7: true, 9: true, 13: true };
582
572
  // Arms with a structural schema here.
583
573
  var DECODED_ARMS = {
@@ -590,29 +580,30 @@ var DECODED_ARMS = {
590
580
  };
591
581
 
592
582
  // 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
583
+ // surface every other defined arm raw as { arm, tag, bytes } -- `decoded` is
594
584
  // ABSENT (not null) on raw arms, so its presence discriminates decoded-empty
595
585
  // (pkiconf decodes to null) from recognized-undecoded. `nested` [20] is never
596
586
  // auto-recursed: a self-nesting tower amplifies per-level walk products, so
597
587
  // the operator re-feeds its bytes to parse explicitly.
598
588
  var BODY = schema.decode(function (n, ctx) {
599
589
  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)");
590
+ throw ctx.E("cmp/bad-body", "PKIBody must be a constructed context-tagged CHOICE arm (RFC 9810 sec. 5.1.2)");
601
591
  }
602
592
  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)");
593
+ throw ctx.E("cmp/bad-body", "PKIBody tag [" + n.tagNumber + "] is not a defined arm (RFC 9810 sec. 5.1.2)");
604
594
  }
605
595
  if (n.children.length !== 1) {
606
- throw ctx.E("cmp/bad-body", "a PKIBody arm wraps exactly one value (X.690 §8.14)");
596
+ throw ctx.E("cmp/bad-body", "a PKIBody arm wraps exactly one value (X.690 sec. 8.14)");
607
597
  }
608
598
  var inner = n.children[0];
609
599
  var out = { arm: BODY_ARMS[n.tagNumber], tag: n.tagNumber, bytes: inner.bytes };
610
600
  if (CRMF_ARMS[n.tagNumber]) {
611
601
  // ccr [13] cross-certification: RFC 9810 Appendix D.6 profiles its template
612
- // with signingAlg present and allows exactly one CertReqMsg (multiple
602
+ // with signingAlg present and version "v1 or v3" (both overriding RFC 4211
603
+ // sec. 5 for this one arm), and allows exactly one CertReqMsg (multiple
613
604
  // cross-certificates go in separate PKIMessages).
614
605
  var isCcr = n.tagNumber === 13;
615
- out.decoded = crmf.walkCertReqMessages(inner, { allowSigningAlg: isCcr });
606
+ out.decoded = crmf.walkCertReqMessages(inner, { allowSigningAlg: isCcr, allowV1Version: isCcr });
616
607
  if (isCcr && out.decoded.messages.length !== 1) {
617
608
  throw ctx.E("cmp/bad-body", "a cross-certification request (ccr) must contain exactly one CertReqMsg (RFC 9810 Appendix D.6)");
618
609
  }
@@ -620,14 +611,14 @@ var BODY = schema.decode(function (n, ctx) {
620
611
  var match = schema.walk(DECODED_ARMS[n.tagNumber], inner, ctx);
621
612
  out.decoded = match.result;
622
613
  } else if (n.tagNumber === 19) {
623
- // PKIConfirmContent ::= NULL (§5.3.16).
614
+ // PKIConfirmContent ::= NULL (sec. 5.3.16).
624
615
  if (!(inner.tagClass === "universal" && inner.tagNumber === TAGS.NULL)) {
625
- throw ctx.E("cmp/bad-pkiconf", "PKIConfirmContent must be NULL (RFC 9810 §5.3.16)");
616
+ throw ctx.E("cmp/bad-pkiconf", "PKIConfirmContent must be NULL (RFC 9810 sec. 5.3.16)");
626
617
  }
627
- // A NULL is well-formed only with empty content (X.690 §8.8.2); the tag check
618
+ // A NULL is well-formed only with empty content (X.690 sec. 8.8.2); the tag check
628
619
  // alone would surface a non-empty NULL as a valid confirmation.
629
620
  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); }
621
+ catch (e) { throw ctx.E("cmp/bad-pkiconf", "PKIConfirmContent must be an empty NULL (RFC 9810 sec. 5.3.16)", e); }
631
622
  out.decoded = null;
632
623
  }
633
624
  return out;
@@ -635,20 +626,26 @@ var BODY = schema.decode(function (n, ctx) {
635
626
 
636
627
  // ---- the message envelope -----------------------------------------------------
637
628
 
638
- // PKIProtection ::= BIT STRING raw bits for the external verifier.
629
+ // PKIProtection ::= BIT STRING -- raw bits for the external verifier. Every
630
+ // RFC 9481 MSG_SIG_ALG / MSG_MAC_ALG output is an octet string, so the BIT
631
+ // STRING must be octet-aligned; with no in-tree CMP verify layer this is
632
+ // enforced at parse (the posture the OCSP signature shares), fail-closed.
639
633
  var PROTECTION = schema.decode(function (n, ctx) {
640
634
  var bs;
641
635
  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); }
636
+ catch (e) { throw ctx.E("cmp/bad-protection", "PKIProtection must be a BIT STRING (RFC 9810 sec. 5.1.3)", e); }
637
+ if (bs.unusedBits !== 0) {
638
+ throw ctx.E("cmp/bad-protection", "PKIProtection must be octet-aligned (0 unused bits) -- every RFC 9481 protection algorithm emits whole octets");
639
+ }
643
640
  return { unusedBits: bs.unusedBits, bytes: bs.bytes };
644
641
  });
645
642
 
646
643
  // 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,
644
+ // extraCerts [1] OPTIONAL } (sec. 5.1). The build runs the two cross-checks that
645
+ // span structures: protection presence <=> protectionAlg presence (sec. 5.1.1,
649
646
  // 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,
647
+ // (sec. 5.3.18). headerBytes / bodyBytes are the exact wire slices protection is
648
+ // computed over (as DER-SEQUENCE(headerBytes || bodyBytes)) -- surfaced raw,
652
649
  // never re-encoded.
653
650
  var PKI_MESSAGE = schema.seq([
654
651
  schema.field("header", PKI_HEADER),
@@ -669,16 +666,16 @@ var PKI_MESSAGE = schema.seq([
669
666
  var extraCerts = m.fields.extraCerts.present ? m.fields.extraCerts.value.result : null;
670
667
  if ((protection !== null) !== (header.protectionAlg !== null)) {
671
668
  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)");
669
+ "protection bits and the header protectionAlg must be present together or absent together (RFC 9810 sec. 5.1.1)");
673
670
  }
674
671
  if (body.arm === "certConf") {
675
672
  for (var i = 0; i < body.decoded.length; i++) {
676
673
  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)");
674
+ throw ctx.E("cmp/bad-cert-status", "a certConf hashAlg requires CMP version cmp2021(3) (RFC 9810 sec. 5.3.18)");
678
675
  }
679
676
  }
680
677
  }
681
- // RFC 9810 §5.2.2 / §7 EnvelopedData is cmp2021(3) syntax: a response
678
+ // RFC 9810 sec. 5.2.2 / sec. 7 -- EnvelopedData is cmp2021(3) syntax: a response
682
679
  // carrying it (an encryptedCert or a privateKey in the envelopedData form)
683
680
  // under pvno < 3 is a version mismatch, the same version gate the certConf
684
681
  // hashAlg rule applies. The deprecated EncryptedValue form is the pre-2021
@@ -698,17 +695,17 @@ var PKI_MESSAGE = schema.seq([
698
695
  (ckps[ck].encryptedCert && ckps[ck].encryptedCert.envelopedData !== undefined) ||
699
696
  (ckps[ck].privateKey && ckps[ck].privateKey.envelopedData !== undefined);
700
697
  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)");
698
+ throw ctx.E("cmp/bad-version", "a response carrying EnvelopedData (encryptedCert or privateKey) requires CMP version cmp2021(3) (RFC 9810 sec. 5.2.2, sec. 7)");
702
699
  }
703
700
  }
704
- // RFC 9810 §5.3.12 a cross-certification response (ccp) reuses the
701
+ // RFC 9810 sec. 5.3.12 -- a cross-certification response (ccp) reuses the
705
702
  // CertRepMessage syntax "with the restriction that no encrypted private key
706
703
  // can be sent": cross-certification certifies an existing CA's public key,
707
704
  // so there is no key generation and CertifiedKeyPair.privateKey has no
708
705
  // meaning. Reject a ccp that carries one rather than surface the key
709
706
  // material; the field stays legal in the enrollment responses (ip/cp/kup).
710
707
  if (body.arm === "ccp" && body.decoded && body.decoded.response) {
711
- // RFC 9810 Appendix D.6 a one-way cross-certification response carries
708
+ // RFC 9810 Appendix D.6 -- a one-way cross-certification response carries
712
709
  // exactly one CertResponse (multiple cross-certificates go in separate
713
710
  // PKIMessages), the mirror of the ccr one-CertReqMsg rule.
714
711
  if (body.decoded.response.length !== 1) {
@@ -717,14 +714,14 @@ var PKI_MESSAGE = schema.seq([
717
714
  for (var c = 0; c < body.decoded.response.length; c++) {
718
715
  var ckpCcp = body.decoded.response[c].certifiedKeyPair;
719
716
  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)");
717
+ throw ctx.E("cmp/bad-cert-response", "a cross-certification response (ccp) must not carry an encrypted private key (RFC 9810 sec. 5.3.12)");
721
718
  }
722
719
  }
723
720
  }
724
- // RFC 9810 §5.3.11 a cross-certification request (ccr) MUST NOT send the
721
+ // RFC 9810 sec. 5.3.11 -- a cross-certification request (ccr) MUST NOT send the
725
722
  // 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
723
+ // it). The private-key-carrying POP choices -- encryptedKey (cmp2021) and the
724
+ // deprecated thisMessage (cmp2000) -- are therefore forbidden in a ccr; the
728
725
  // MAC (dhMAC / agreeMAC) and indirect (subsequentMessage) methods carry no
729
726
  // key. Checked before the version gate so a ccr always gets the ccr verdict.
730
727
  if (body.arm === "ccr" && body.decoded && body.decoded.messages) {
@@ -734,10 +731,10 @@ var PKI_MESSAGE = schema.seq([
734
731
  // the private key.
735
732
  var ccrPopo = ccrMsg.popo;
736
733
  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)");
734
+ 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 sec. 5.3.11)");
738
735
  }
739
736
  // 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
737
+ // arm is the other private-key transport (sec. 5.2.8.3.1), likewise forbidden
741
738
  // in a ccr. The control value is the raw PKIArchiveOptions CHOICE; the
742
739
  // encryptedPrivKey arm is context tag [0].
743
740
  var controls = ccrMsg.certReq.controls;
@@ -746,15 +743,15 @@ var PKI_MESSAGE = schema.seq([
746
743
  if (controls[cn].type !== OID_PKI_ARCHIVE_OPTIONS) continue;
747
744
  var arch;
748
745
  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); }
746
+ catch (e) { throw ctx.E("cmp/bad-body", "a ccr pkiArchiveOptions control did not decode (RFC 9810 sec. 5.3.11)", e); }
750
747
  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)");
748
+ 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 sec. 5.3.11, sec. 5.2.8.3.1)");
752
749
  }
753
750
  }
754
751
  }
755
752
  }
756
753
  }
757
- // RFC 9810 §5.2.8.3 the agreeMAC and encryptedKey POPOPrivKey choices are
754
+ // RFC 9810 sec. 5.2.8.3 -- the agreeMAC and encryptedKey POPOPrivKey choices are
758
755
  // cmp2021(3) syntax; a request (ir/cr/kur/krr/ccr) whose proof-of-possession
759
756
  // uses either under pvno < 3 is a version mismatch, the same gate the
760
757
  // response EnvelopedData rule applies. The CRMF walker surfaces the chosen
@@ -763,9 +760,9 @@ var PKI_MESSAGE = schema.seq([
763
760
  for (var q = 0; q < body.decoded.messages.length; q++) {
764
761
  var popo = body.decoded.messages[q].popo;
765
762
  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)");
763
+ throw ctx.E("cmp/bad-version", "a request POP using the agreeMAC or encryptedKey choice requires CMP version cmp2021(3) (RFC 9810 sec. 5.2.8.3)");
767
764
  }
768
- // §5.2.8.3.17 the same key transport via a pkiArchiveOptions control
765
+ // sec. 5.2.8.3.1/sec. 7 -- the same key transport via a pkiArchiveOptions control
769
766
  // whose encryptedPrivKey [0] EXPLICIT wraps the envelopedData-form
770
767
  // EncryptedKey is cmp2021 syntax too (a ccr rejects the control outright
771
768
  // above; the other request arms are version-gated here).
@@ -776,13 +773,13 @@ var PKI_MESSAGE = schema.seq([
776
773
  var opt = asn1.decode(archCtrls[ac].value);
777
774
  if (opt.tagClass === "context" && opt.tagNumber === 0 && opt.children && opt.children.length >= 1 &&
778
775
  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)");
776
+ throw ctx.E("cmp/bad-version", "a pkiArchiveOptions encryptedPrivKey using the EnvelopedData form requires CMP version cmp2021(3) (RFC 9810 sec. 5.2.8.3.1, sec. 7)");
780
777
  }
781
778
  }
782
779
  }
783
780
  }
784
781
  }
785
- // RFC 9810 §5.1.1.4 the certProfile name count is bounded by the message
782
+ // RFC 9810 sec. 5.1.1.4 -- the certProfile name count is bounded by the message
786
783
  // body: a p10cr carries at most one; an ir/cr/kur no more names than its
787
784
  // CertReqMsg count, a genm no more than its GenMsgContent InfoTypeAndValue
788
785
  // count (the names map positionally to the body entries). An overlong list
@@ -796,18 +793,37 @@ var PKI_MESSAGE = schema.seq([
796
793
  else if (body.decoded && body.decoded.messages) bodyCount = body.decoded.messages.length;
797
794
  else if (body.arm === "genm" && Array.isArray(body.decoded)) bodyCount = body.decoded.length;
798
795
  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)");
796
+ 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 sec. 5.1.1.4)");
800
797
  }
801
798
  }
802
799
  }
803
- // RFC 9810 §5.3.137 the cAKeyUpdAnnV3 [0] RootCaKeyUpdateContent CA key
800
+ // RFC 9810 sec. 5.3.13/sec. 7 -- the cAKeyUpdAnnV3 [0] RootCaKeyUpdateContent CA key
804
801
  // update form is cmp2021(3) syntax; a ckuann using it under pvno < 3 is a
805
802
  // version mismatch. ckuann surfaces raw, so inspect its top-level tag: the
806
803
  // deprecated cAKeyUpdAnnV2 is a universal SEQUENCE, the v3 form is [0].
807
804
  if (body.arm === "ckuann" && header.pvno !== 3) {
808
805
  var caKeyUpd = asn1.decode(body.bytes);
809
806
  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)");
807
+ throw ctx.E("cmp/bad-version", "a ckuann using the cAKeyUpdAnnV3 [0] RootCaKeyUpdateContent form requires CMP version cmp2021(3) (RFC 9810 sec. 5.3.13, sec. 7)");
808
+ }
809
+ }
810
+ // RFC 9810 sec. 5.2.8.3.2/sec. 7 -- Challenge.encryptedRand [0] EnvelopedData is
811
+ // cmp2021(3) syntax (the replacement for the deprecated challenge field).
812
+ // popdecc surfaces raw, so inspect the already-decoded Challenges the same
813
+ // way the ckuann gate inspects its top-level tag: a Challenge carrying a
814
+ // context [0] (its only context-tagged field) under pvno < 3 is a version
815
+ // mismatch; a non-Challenges shape stays on the documented raw posture.
816
+ if (body.arm === "popdecc" && header.pvno !== 3) {
817
+ var challenges = asn1.decode(body.bytes);
818
+ if (challenges.tagClass === "universal" && challenges.tagNumber === TAGS.SEQUENCE && challenges.children) {
819
+ for (var chi = 0; chi < challenges.children.length; chi++) {
820
+ var chKids = challenges.children[chi].children;
821
+ for (var chf = 0; chKids && chf < chKids.length; chf++) {
822
+ if (chKids[chf].tagClass === "context" && chKids[chf].tagNumber === 0) {
823
+ throw ctx.E("cmp/bad-version", "a popdecc Challenge carrying encryptedRand [0] EnvelopedData requires CMP version cmp2021(3) (RFC 9810 sec. 5.2.8.3.2, sec. 7)");
824
+ }
825
+ }
826
+ }
811
827
  }
812
828
  }
813
829
  return {
@@ -832,7 +848,7 @@ var PKI_MESSAGE = schema.seq([
832
848
  * Parse a DER `Buffer` or a PEM string into a structured PKIMessage:
833
849
  * `{ header, headerBytes, body, bodyBytes, protection, extraCerts }`.
834
850
  * `header` carries `pvno` (1..3), `sender` / `recipient` (validated
835
- * GeneralNames the anonymous NULL-DN form included), and the optional
851
+ * GeneralNames -- the anonymous NULL-DN form included), and the optional
836
852
  * `messageTime`, `protectionAlg`, `senderKID` / `recipKID`,
837
853
  * `transactionID`, `senderNonce` / `recipNonce`, `freeText`, and
838
854
  * `generalInfo` (recognized id-it types value-checked: implicitConfirm is
@@ -841,24 +857,29 @@ var PKI_MESSAGE = schema.seq([
841
857
  * (`ir` / `cr` / `kur` / `krr` / `ccr`) decode via the CRMF parser;
842
858
  * `ip` / `cp` / `kup` / `ccp` decode to `{ caPubs, response }` (an encrypted
843
859
  * certificate's EnvelopedData decodes via the CMS parser; the deprecated
844
- * EncryptedValue arm and `caPubs` surface raw the parser confers no
860
+ * EncryptedValue arm and `caPubs` surface raw -- the parser confers no
845
861
  * trust); `krp` decodes to `{ status, newSigCert, caCerts, keyPairHist }`;
846
862
  * `rr` / `rp`, `genm` / `genp`, `error`, `certConf` (an empty
847
863
  * confirmation is the legal reject-all), and `pollReq` / `pollRep` decode
848
- * structurally; `pkiconf` decodes to `null`; every other defined arm
864
+ * structurally; `pkiconf` decodes to `null`; every other defined arm --
849
865
  * `p10cr` (feed `body.bytes` to `pki.schema.csr.parse`), the
850
866
  * challenge-response and announcement arms, and `nested` (never
851
- * auto-recursed) surfaces raw with `decoded` absent. `certReqId` values
867
+ * auto-recursed) -- surfaces raw with `decoded` absent. `certReqId` values
852
868
  * are BigInt and accept the protocol's -1 sentinel.
853
869
  *
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
870
+ * Protection is surfaced, not verified: `protection` carries the raw,
871
+ * octet-aligned BIT STRING (unused bits are rejected at parse -- every
872
+ * RFC 9481 protection algorithm emits whole octets), and the MAC or
873
+ * signature is computed over the DER of the virtual
874
+ * `ProtectedPart ::= SEQUENCE { header, body }` -- reconstruct it as a DER
857
875
  * SEQUENCE wrapping exactly `headerBytes || bodyBytes`. `extraCerts` are
858
876
  * raw DER certificates and are NOT covered by protection.
859
877
  *
860
878
  * Throws `CmpError` when the bytes are not a well-formed PKIMessage, and
861
- * `Asn1Error` when the underlying DER is malformed.
879
+ * `Asn1Error` when the underlying DER is malformed. The request-arm interiors
880
+ * (`ir` / `cr` / `kur` / `krr` / `ccr`, and an `rr` entry's `certDetails`) are
881
+ * walked by the CRMF parser, so a malformed interior throws `CrmfError` with a
882
+ * `crmf/*` code -- all `PkiError` subclasses.
862
883
  *
863
884
  * @example
864
885
  * var m = pki.schema.cmp.parse(der);
@@ -879,7 +900,7 @@ var parse = pkix.makeParser({
879
900
  * @related pki.schema.cmp.parse
880
901
  *
881
902
  * 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
903
+ * wire-DER over HTTP (RFC 9811) -- the PEM path is a convenience for
883
904
  * messages that transit text channels.
884
905
  *
885
906
  * @example
@@ -907,7 +928,7 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label || "CMP", PemE
907
928
  // (PKIBody) is context-class constructed [0..26]. The one overlap in the
908
929
  // registry is one-directional: a 2-child PKIMessage with body ir [0] also
909
930
  // satisfies the shallow ocsp-request probe, so this detector registers AHEAD
910
- // of ocsp-request while every real OCSPRequest fails here (its tbsRequest
931
+ // of ocsp-request -- while every real OCSPRequest fails here (its tbsRequest
911
932
  // never leads with a bare INTEGER).
912
933
  function matches(root) {
913
934
  var k = pkix.rootSequenceChildren(root, 2, 4);