@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.
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * @intro
12
12
  * Certificate Request Message Format handling per RFC 4211. `parse` decodes a
13
- * `CertReqMessages` the request body CMP and EST enrollment carry into an
13
+ * `CertReqMessages` -- the request body CMP and EST enrollment carry -- into an
14
14
  * array of messages, each with its `CertRequest` (certReqId, a `CertTemplate`
15
15
  * of the requested certificate fields, and any registration controls), an
16
16
  * optional proof-of-possession, and optional registration info.
@@ -21,7 +21,7 @@
21
21
  * X.680 forces EXPLICIT: `issuer [3]` / `subject [5]` are `Name`, dual-accepted
22
22
  * here (the standards-compliant EXPLICIT encoding and the dominant IMPLICIT one
23
23
  * real tooling emits); and the `OptionalValidity` times are EXPLICIT. version,
24
- * when supplied, MUST be 2 (RFC 4211 §5); certReqId is an unbounded signed
24
+ * when supplied, MUST be 2 (RFC 4211 sec. 5); certReqId is an unbounded signed
25
25
  * INTEGER (the RFC 9483 `-1` sentinel is legal). The `CertRequest` byte range
26
26
  * the proof-of-possession signature covers, and each `poposkInput`, are surfaced
27
27
  * RAW for a downstream verifier; registration controls / info values and the
@@ -30,7 +30,7 @@
30
30
  *
31
31
  * @card
32
32
  * Parse DER / PEM RFC 4211 CertReqMessages into requested-certificate templates,
33
- * proof-of-possession, and registration controls dual-accepted names, raw
33
+ * proof-of-possession, and registration controls -- dual-accepted names, raw
34
34
  * verifier inputs, fail-closed.
35
35
  */
36
36
 
@@ -49,8 +49,8 @@ var NS = pkix.makeNS("crmf", CrmfError, oid);
49
49
  var TAGS = asn1.TAGS;
50
50
 
51
51
  // ---- dual-accept Name (issuer [3] / subject [5]) ---------------------
52
- // RFC 4211 Appendix B is IMPLICIT TAGS, but Name is a CHOICE so X.680 §31.2.7
53
- // forces EXPLICIT tagging the standards-compliant wire is a context [tag]
52
+ // RFC 4211 Appendix B is IMPLICIT TAGS, but Name is a CHOICE so X.680 sec. 31.2.7
53
+ // forces EXPLICIT tagging -- the standards-compliant wire is a context [tag]
54
54
  // EXPLICIT wrapper around a universal RDNSequence. The dominant tooling
55
55
  // (pyasn1-modules, BouncyCastle) instead encodes IMPLICIT (the [tag] REPLACES the
56
56
  // RDNSequence 0x30, so the children ARE the RDN SETs). Both occur in real CMP /
@@ -62,7 +62,7 @@ function crmfName(tag) {
62
62
  var INAME = pkix.name(NS, { implicitTag: tag }); // IMPLICIT arm: [tag] children ARE the RDN SETs
63
63
  return schema.decode(function (n, ctx) {
64
64
  if (n.tagClass !== "context" || n.tagNumber !== tag || !n.children) {
65
- throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] must be a Name (RFC 4211 §5)");
65
+ throw ctx.E("crmf/bad-name", "issuer/subject [" + tag + "] must be a Name (RFC 4211 sec. 5)");
66
66
  }
67
67
  if (n.children.length === 0) return schema.walk(INAME, n, ctx).result; // IMPLICIT empty RDNSequence
68
68
  var c0 = n.children[0];
@@ -80,11 +80,11 @@ function crmfName(tag) {
80
80
  });
81
81
  }
82
82
 
83
- // ---- OptionalValidity (§5) -------------------------------------------
83
+ // ---- OptionalValidity (sec. 5) -------------------------------------------
84
84
  // validity [4] IMPLICIT SEQUENCE { notBefore [0] Time OPTIONAL, notAfter [1] Time
85
85
  // OPTIONAL }. Time is a CHOICE of UTCTime / GeneralizedTime, so [0]/[1] are
86
86
  // EXPLICIT (an IMPLICIT tag would erase the UTCTime-vs-GeneralizedTime
87
- // discriminator). RFC 4211 §5 requires at least one of the two to be present.
87
+ // discriminator). RFC 4211 sec. 5 requires at least one of the two to be present.
88
88
  var OPTIONAL_VALIDITY = schema.seq([
89
89
  schema.trailing([
90
90
  { tag: 0, name: "notBefore", schema: schema.time(NS), explicit: true, emptyCode: "crmf/bad-validity" },
@@ -95,14 +95,14 @@ var OPTIONAL_VALIDITY = schema.seq([
95
95
  build: function (m, ctx) {
96
96
  var f = m.fields;
97
97
  if (!f.notBefore.present && !f.notAfter.present) {
98
- throw ctx.E("crmf/bad-validity", "OptionalValidity must contain notBefore or notAfter (RFC 4211 §5)");
98
+ throw ctx.E("crmf/bad-validity", "OptionalValidity must contain notBefore or notAfter (RFC 4211 sec. 5)");
99
99
  }
100
100
  return { notBefore: f.notBefore.present ? f.notBefore.value : null, notAfter: f.notAfter.present ? f.notAfter.value : null };
101
101
  },
102
102
  });
103
103
 
104
- // ---- ProofOfPossession (§4) ------------------------------------------
105
- // raVerified [0] NULL the RA has already verified POP out of band.
104
+ // ---- ProofOfPossession (sec. 4) ------------------------------------------
105
+ // raVerified [0] NULL -- the RA has already verified POP out of band.
106
106
  var POPO_RAVERIFIED = schema.decode(function (n, ctx) {
107
107
  try { asn1.read.nullImplicit(n, 0); }
108
108
  catch (e) { throw ctx.E("crmf/bad-popo", "raVerified [0] must be an IMPLICIT NULL", e); }
@@ -110,7 +110,7 @@ var POPO_RAVERIFIED = schema.decode(function (n, ctx) {
110
110
  });
111
111
 
112
112
  // When poposkInput is present the POP signature is over the DER of the
113
- // POPOSigningKeyInput VALUE (RFC 4211 §4.1), which is a SEQUENCE but on the wire
113
+ // POPOSigningKeyInput VALUE (RFC 4211 sec. 4.1), which is a SEQUENCE -- but on the wire
114
114
  // poposkInput is an IMPLICIT [0] field, so its bytes lead with the context tag
115
115
  // (0xA0) rather than the SEQUENCE tag a verifier hashes. `asn1.sequenceTlv` recovers
116
116
  // the SEQUENCE-tagged signed region from the field's content; `signedBytes` is that
@@ -118,17 +118,31 @@ var POPO_RAVERIFIED = schema.decode(function (n, ctx) {
118
118
  // encoding its peer produced.
119
119
 
120
120
  // POPOSigningKeyInput ::= SEQUENCE { authInfo CHOICE { sender [0] GeneralName,
121
- // publicKeyMAC PKMACValue }, publicKey SubjectPublicKeyInfo } (§4.1). Structurally
121
+ // publicKeyMAC PKMACValue }, publicKey SubjectPublicKeyInfo } (sec. 4.1). Structurally
122
122
  // validated (not deferred raw), so an empty or malformed poposkInput fails closed
123
123
  // rather than surfacing a signed region that was never a well-formed POP input.
124
- // PKMACValue ::= SEQUENCE { algId AlgorithmIdentifier, value BIT STRING }.
124
+ // PKMACValue ::= SEQUENCE { algId AlgorithmIdentifier, value BIT STRING }. The
125
+ // MAC value is octet-string algorithm output handed to an external verifier;
126
+ // with no in-tree POP verify layer, octet alignment is enforced at parse.
125
127
  var PKMAC_VALUE = schema.seq([
126
128
  schema.field("algId", pkix.algorithmIdentifier(NS)),
127
129
  schema.field("value", schema.bitString()),
128
- ], { assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-popo", what: "PKMACValue" });
130
+ ], {
131
+ assert: "sequence", arity: { exact: 2 }, code: "crmf/bad-popo", what: "PKMACValue",
132
+ build: function (m, ctx) {
133
+ // The parsed BIT STRING structure { unusedBits, bytes } -- a structural
134
+ // octet-alignment check on the unused-bit COUNT, not a comparison of the
135
+ // MAC secret (there is no in-tree POP verify), so no timing oracle exists.
136
+ var macBitString = m.fields.value.value;
137
+ if (macBitString.unusedBits !== 0) {
138
+ throw ctx.E("crmf/bad-popo", "a PKMACValue MAC BIT STRING must be octet-aligned (0 unused bits)");
139
+ }
140
+ return { algId: m.fields.algId.value.result, value: { unusedBits: macBitString.unusedBits, bytes: macBitString.bytes } };
141
+ },
142
+ });
129
143
 
130
144
  // authInfo CHOICE: sender [0] GeneralName is EXPLICIT (GeneralName is itself a
131
- // CHOICE); publicKeyMAC is a bare PKMACValue SEQUENCE disjoint by tag class.
145
+ // CHOICE); publicKeyMAC is a bare PKMACValue SEQUENCE -- disjoint by tag class.
132
146
  var POPOSK_AUTH_INFO = schema.choice([
133
147
  { when: { tagClass: "context", tagNumber: 0 }, schema: schema.explicit(0, pkix.generalName(NS, { code: "crmf/bad-popo" }), { code: "crmf/bad-popo" }) },
134
148
  { when: { tagClass: "universal", tagNumber: TAGS.SEQUENCE }, schema: PKMAC_VALUE },
@@ -140,11 +154,11 @@ var POPOSK_INPUT = schema.seq([
140
154
  ], { assert: "implicit", implicitTag: 0, arity: { exact: 2 }, code: "crmf/bad-popo", what: "POPOSigningKeyInput" });
141
155
 
142
156
  // POPOSigningKey ::= SEQUENCE { poposkInput [0] POPOSigningKeyInput OPTIONAL,
143
- // algorithmIdentifier AlgorithmIdentifier, signature BIT STRING } IMPLICIT [1].
157
+ // algorithmIdentifier AlgorithmIdentifier, signature BIT STRING } -- IMPLICIT [1].
144
158
  var POPO_SIGNING_KEY = schema.seq([
145
159
  schema.optional("poposkInput", schema.decode(function (n, ctx) {
146
160
  // Validate the full POPOSigningKeyInput structure fail-closed, then surface the
147
- // raw [0] node plus its decoded publicKey (for the §4.1 template-match check).
161
+ // raw [0] node plus its decoded publicKey (for the sec. 4.1 template-match check).
148
162
  var m = schema.walk(POPOSK_INPUT, n, ctx);
149
163
  return { node: n, publicKey: m.fields.publicKey.value.result.bytes };
150
164
  }), { tag: 0 }),
@@ -152,91 +166,99 @@ var POPO_SIGNING_KEY = schema.seq([
152
166
  schema.field("signature", schema.bitString()),
153
167
  ], {
154
168
  assert: "implicit", implicitTag: 1, code: "crmf/bad-popo", what: "POPOSigningKey",
155
- build: function (m) {
169
+ build: function (m, ctx) {
156
170
  var pin = m.fields.poposkInput;
171
+ var sig = m.fields.signature.value;
172
+ // The POP signature is octet-string algorithm output an external verifier
173
+ // consumes; with no in-tree POP verify layer, octet alignment is enforced
174
+ // at parse (RFC 4211 sec. 4.1).
175
+ if (sig.unusedBits !== 0) {
176
+ throw ctx.E("crmf/bad-popo", "a POPOSigningKey signature BIT STRING must be octet-aligned (0 unused bits)");
177
+ }
157
178
  return {
158
179
  type: "signature",
159
180
  poposkInput: pin.present ? { bytes: pin.node.bytes, signedBytes: asn1.sequenceTlv(pin.node), publicKey: pin.value.publicKey } : null,
160
181
  algorithmIdentifier: m.fields.algorithmIdentifier.value.result,
161
- signature: { unusedBits: m.fields.signature.value.unusedBits, bytes: m.fields.signature.value.bytes },
182
+ signature: { unusedBits: sig.unusedBits, bytes: sig.bytes },
162
183
  };
163
184
  },
164
185
  });
165
186
 
166
- // keyEncipherment [2] / keyAgreement [3] POPOPrivKey (§4). POPOPrivKey is a
187
+ // keyEncipherment [2] / keyAgreement [3] POPOPrivKey (sec. 4). POPOPrivKey is a
167
188
  // 5-arm CHOICE { thisMessage [0], subsequentMessage [1], dhMAC [2], agreeMAC [3],
168
- // encryptedKey [4] }, so X.680 §31.2.7 forces the outer [2]/[3] tag to be EXPLICIT
169
- // (a CHOICE has no single tag to replace). Validate that shell a constructed
170
- // wrapper around exactly one inner context [0]..[4] alternative before surfacing
189
+ // encryptedKey [4] }, so X.680 sec. 31.2.7 forces the outer [2]/[3] tag to be EXPLICIT
190
+ // (a CHOICE has no single tag to replace). Validate that shell -- a constructed
191
+ // wrapper around exactly one inner context [0]..[4] alternative -- before surfacing
171
192
  // the arm RAW, so a primitive / empty / mis-tagged node is rejected rather than
172
193
  // reported to a verifier as a well-formed POP. The inner alternative's own value
173
194
  // (incl. the encryptedKey [4] EnvelopedData) stays deferred to the verify layer.
174
195
  // POPOPrivKey ::= CHOICE { thisMessage [0] BIT STRING (deprecated), subsequentMessage
175
196
  // [1] SubsequentMessage, dhMAC [2] BIT STRING (deprecated), agreeMAC [3] PKMACValue,
176
- // encryptedKey [4] EnvelopedData } (RFC 4211 §4.2). The deprecated arms are still
197
+ // encryptedKey [4] EnvelopedData } (RFC 4211 sec. 4.2). The deprecated arms are still
177
198
  // parsed. The inner VALUE stays deferred raw (the EnvelopedData / MAC decode is a
178
199
  // verify-layer concern), but the alternative's shape is validated fail-closed.
179
200
  var POPOPRIVKEY_METHODS = { 0: "thisMessage", 1: "subsequentMessage", 2: "dhMAC", 3: "agreeMAC", 4: "encryptedKey" };
180
201
  function popoPrivKey(type) {
181
202
  return schema.decode(function (n, ctx) {
182
203
  if (!n.children || n.children.length !== 1) {
183
- throw ctx.E("crmf/bad-popo", type + " [" + n.tagNumber + "] POPOPrivKey must be an EXPLICIT wrapper around one alternative (RFC 4211 §4)");
204
+ throw ctx.E("crmf/bad-popo", type + " [" + n.tagNumber + "] POPOPrivKey must be an EXPLICIT wrapper around one alternative (RFC 4211 sec. 4)");
184
205
  }
185
206
  var inner = n.children[0];
186
207
  if (inner.tagClass !== "context" || inner.tagNumber < 0 || inner.tagNumber > 4) {
187
- throw ctx.E("crmf/bad-popo", type + " POPOPrivKey alternative must be a context [0]..[4] CHOICE element (RFC 4211 §4)");
208
+ throw ctx.E("crmf/bad-popo", type + " POPOPrivKey alternative must be a context [0]..[4] CHOICE element (RFC 4211 sec. 4)");
188
209
  }
189
- // RFC 4211 §4.24.3 the MAC alternatives dhMAC [2] / agreeMAC [3] are the
210
+ // RFC 4211 sec. 4.2/sec. 4.3 -- the MAC alternatives dhMAC [2] / agreeMAC [3] are the
190
211
  // key-agreement "fourth method"; key-encipherment POP uses only the
191
212
  // thisMessage / subsequentMessage / encryptedKey methods, so a MAC arm under
192
213
  // keyEncipherment is non-conforming.
193
214
  if (type === "keyEncipherment" && (inner.tagNumber === 2 || inner.tagNumber === 3)) {
194
- throw ctx.E("crmf/bad-popo", "keyEncipherment POP cannot use the MAC alternative " + POPOPRIVKEY_METHODS[inner.tagNumber] + " (RFC 4211 §4.2)");
215
+ throw ctx.E("crmf/bad-popo", "keyEncipherment POP cannot use the MAC alternative " + POPOPRIVKEY_METHODS[inner.tagNumber] + " (RFC 4211 sec. 4.2)");
195
216
  }
196
217
  // FORM: thisMessage [0] / subsequentMessage [1] / dhMAC [2] are primitive (BIT
197
218
  // STRING / INTEGER / BIT STRING); agreeMAC [3] PKMACValue and encryptedKey [4]
198
219
  // EnvelopedData are constructed (SEQUENCE). A wrong-form node is malformed.
199
220
  var mustBeConstructed = inner.tagNumber === 3 || inner.tagNumber === 4;
200
221
  if (mustBeConstructed !== !!inner.children) {
201
- throw ctx.E("crmf/bad-popo", type + " POPOPrivKey [" + inner.tagNumber + "] has the wrong primitive/constructed form (RFC 4211 §4)");
222
+ throw ctx.E("crmf/bad-popo", type + " POPOPrivKey [" + inner.tagNumber + "] has the wrong primitive/constructed form (RFC 4211 sec. 4)");
202
223
  }
203
224
  // Each alternative's PAYLOAD is decoded fail-closed, not just its form:
204
225
  // thisMessage [0] / dhMAC [2] are BIT STRINGs; subsequentMessage [1] an INTEGER
205
226
  // in {0,1}; agreeMAC [3] a PKMACValue SEQUENCE; encryptedKey [4] an EnvelopedData.
206
227
  if (inner.tagNumber === 0 || inner.tagNumber === 2) {
207
228
  try { asn1.read.bitStringImplicit(inner, inner.tagNumber); }
208
- catch (e) { throw ctx.E("crmf/bad-popo", POPOPRIVKEY_METHODS[inner.tagNumber] + " [" + inner.tagNumber + "] must be a BIT STRING (RFC 4211 §4.2)", e); }
229
+ catch (e) { throw ctx.E("crmf/bad-popo", POPOPRIVKEY_METHODS[inner.tagNumber] + " [" + inner.tagNumber + "] must be a BIT STRING (RFC 4211 sec. 4.2)", e); }
209
230
  }
210
231
  if (inner.tagNumber === 1) {
211
232
  var v;
212
233
  try { v = asn1.read.integerImplicit(inner, 1); }
213
234
  catch (e) { throw ctx.E("crmf/bad-popo", "subsequentMessage must be an INTEGER", e); }
214
235
  if (v !== 0n && v !== 1n) {
215
- throw ctx.E("crmf/bad-popo", "SubsequentMessage must be encrCert(0) or challengeResp(1) (RFC 4211 §4.2)");
236
+ throw ctx.E("crmf/bad-popo", "SubsequentMessage must be encrCert(0) or challengeResp(1) (RFC 4211 sec. 4.2)");
216
237
  }
217
238
  }
218
239
  if (inner.tagNumber === 3) {
219
240
  try { schema.embeddedDer(PKMAC_VALUE, asn1.sequenceTlv(inner), NS, { code: "crmf/bad-popo", what: "agreeMAC [3] PKMACValue" }); }
220
- catch (e) { throw ctx.E("crmf/bad-popo", "agreeMAC [3] must be a PKMACValue SEQUENCE { algId, BIT STRING } (RFC 4211 §4.2)", e); }
241
+ catch (e) { throw ctx.E("crmf/bad-popo", "agreeMAC [3] must be a PKMACValue SEQUENCE { algId, BIT STRING } (RFC 4211 sec. 4.2)", e); }
221
242
  }
222
- // encryptedKey [4] EnvelopedData validate the CMS structure fail-closed by
243
+ // encryptedKey [4] EnvelopedData -- validate the CMS structure fail-closed by
223
244
  // composing the shared EnvelopedData decoder (retag the IMPLICIT [4] to the
224
245
  // universal SEQUENCE it decodes as). The EncKeyWithID identity check lives inside
225
246
  // the ENCRYPTED content, so it stays a verify-layer concern.
226
247
  if (inner.tagNumber === 4) {
227
248
  var env;
228
249
  try { env = cms.walkEnvelopedData(asn1.decode(asn1.sequenceTlv(inner))); }
229
- catch (e) { throw ctx.E("crmf/bad-popo", "encryptedKey [4] must be a well-formed EnvelopedData (RFC 4211 §4.2, RFC 5652 §6.1)", e); }
230
- // RFC 4211 §4.2 the enveloped content type MUST be id-ct-encKeyWithID (the
250
+ catch (e) { throw ctx.E("crmf/bad-popo", "encryptedKey [4] must be a well-formed EnvelopedData (RFC 4211 sec. 4.2, RFC 5652 sec. 6.1)", e); }
251
+ // RFC 4211 sec. 4.2 -- the enveloped content type MUST be id-ct-encKeyWithID (the
231
252
  // ContentType OID is in the clear even though the content itself is encrypted).
232
253
  if (env.encryptedContentInfo.contentType !== oid.byName("encKeyWithID")) {
233
- throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData content type MUST be id-ct-encKeyWithID (RFC 4211 §4.2)");
254
+ throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData content type MUST be id-ct-encKeyWithID (RFC 4211 sec. 4.2)");
234
255
  }
235
- // The encrypted key material MUST be present CMS allows a detached
256
+ // The encrypted key material MUST be present -- CMS allows a detached
236
257
  // EnvelopedData (encryptedContent OPTIONAL), but a POP with no key to verify
237
- // or archive is meaningless.
238
- if (env.encryptedContentInfo.encryptedContent == null) {
239
- throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData MUST carry the encrypted key in encryptedContent (RFC 4211 §4.2)");
258
+ // or archive is meaningless. A zero-length attached ciphertext is the same
259
+ // fault as a detached one (the rule the CMP encryptedKey arm also holds).
260
+ if (env.encryptedContentInfo.encryptedContent == null || env.encryptedContentInfo.encryptedContent.length === 0) {
261
+ throw ctx.E("crmf/bad-popo", "encryptedKey [4] EnvelopedData MUST carry a non-empty encrypted key in encryptedContent (RFC 4211 sec. 4.2)");
240
262
  }
241
263
  }
242
264
  return { type: type, method: POPOPRIVKEY_METHODS[inner.tagNumber], bytes: n.bytes };
@@ -253,7 +275,7 @@ var PROOF_OF_POSSESSION = schema.choice([
253
275
  { when: { tagClass: "context", tagNumber: 3 }, schema: popoPrivKey("keyAgreement") },
254
276
  ], { code: "crmf/bad-popo" });
255
277
 
256
- // ---- Controls / regInfo (§5, §6, §7) ---------------------------------
278
+ // ---- Controls / regInfo (sec. 5, sec. 6, sec. 7) ---------------------------------
257
279
  // AttributeTypeAndValue ::= SEQUENCE { type OBJECT IDENTIFIER, value ANY DEFINED BY
258
280
  // type }. The list is decoded; each value stays RAW (the per-OID value semantics
259
281
  // are deferred). Shared shape for both controls and regInfo.
@@ -268,15 +290,15 @@ var CONTROL = schema.seq([
268
290
  },
269
291
  });
270
292
  function mapControls(m) { return m.items.map(function (it) { return it.value.result; }); }
271
- // Controls ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§5).
293
+ // Controls ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (sec. 5).
272
294
  var CONTROLS = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-controls", what: "Controls", build: mapControls });
273
- // regInfo ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (§7).
295
+ // regInfo ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue (sec. 7).
274
296
  var REG_INFO = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-reg-info", what: "regInfo", build: mapControls });
275
297
 
276
- // ---- CertTemplate (§5) the IMPLICIT-TAGS core ----------------------
298
+ // ---- CertTemplate (sec. 5) -- the IMPLICIT-TAGS core ----------------------
277
299
  // CertTemplate ::= SEQUENCE { version [0], serialNumber [1], signingAlg [2],
278
300
  // issuer [3] Name, validity [4] OptionalValidity, subject [5] Name, publicKey [6]
279
- // SubjectPublicKeyInfo, issuerUID [7], subjectUID [8], extensions [9] } every
301
+ // SubjectPublicKeyInfo, issuerUID [7], subjectUID [8], extensions [9] } -- every
280
302
  // field IMPLICIT and OPTIONAL, so the whole body is one ascending [0..9] trailing
281
303
  // run. issuer/subject Name and the [4] validity times are the CHOICE exceptions.
282
304
  var CERT_TEMPLATE = schema.seq([
@@ -296,15 +318,20 @@ var CERT_TEMPLATE = schema.seq([
296
318
  assert: "sequence", code: "crmf/bad-cert-template", what: "CertTemplate",
297
319
  build: function (m, ctx) {
298
320
  var f = m.fields;
299
- // version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5). This
321
+ // version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 sec. 5). This
300
322
  // is a structural CertTemplate rule (it holds wherever the template appears);
301
323
  // the CA-assigned-field omission rule is REQUEST-context-specific and lives in
302
324
  // CERT_REQUEST.build, because the same CertTemplate structure identifies an
303
- // existing certificate serialNumber and issuer present inside a CMP
304
- // RevDetails (RFC 9810 §5.3.9). Every field is surfaced so both the request
325
+ // existing certificate -- serialNumber and issuer present -- inside a CMP
326
+ // RevDetails (RFC 9810 sec. 5.3.9). Every field is surfaced so both the request
305
327
  // omission check and the revocation consumer read them off one result.
306
- if (f.version.present && f.version.value !== 2n) {
307
- throw ctx.E("crmf/bad-version", "CertTemplate version MUST be 2 (v3) if supplied (RFC 4211 §5)");
328
+ // The ONE profile override: RFC 9810 Appendix D.6 profiles the CMP
329
+ // cross-certification (ccr) template as "version v1 or v3", so the caller
330
+ // opts in to v1 (0) via ctx.allowV1Version -- the same base-section-vs-
331
+ // profile-appendix override mechanism as the signingAlg relaxation below.
332
+ if (f.version.present && f.version.value !== 2n &&
333
+ !(ctx.allowV1Version && f.version.value === 0n)) {
334
+ throw ctx.E("crmf/bad-version", "CertTemplate version MUST be 2 (v3) if supplied (RFC 4211 sec. 5)");
308
335
  }
309
336
  return {
310
337
  version: f.version.present ? f.version.value : null,
@@ -321,10 +348,10 @@ var CERT_TEMPLATE = schema.seq([
321
348
  },
322
349
  });
323
350
 
324
- // ---- CertRequest / CertReqMsg / CertReqMessages (§3, §5) --------------
351
+ // ---- CertRequest / CertReqMsg / CertReqMessages (sec. 3, sec. 5) --------------
325
352
  // CertRequest ::= SEQUENCE { certReqId INTEGER, certTemplate CertTemplate,
326
353
  // controls Controls OPTIONAL }. certReqId is a SIGNED INTEGER with no value
327
- // constraint a negative value (the RFC 9483 -1 sentinel) is legal.
354
+ // constraint -- a negative value (the RFC 9483 -1 sentinel) is legal.
328
355
  var CERT_REQUEST = schema.seq([
329
356
  schema.field("certReqId", schema.integerLeaf()),
330
357
  schema.field("certTemplate", CERT_TEMPLATE),
@@ -333,7 +360,7 @@ var CERT_REQUEST = schema.seq([
333
360
  assert: "sequence", code: "crmf/bad-cert-request", what: "CertRequest",
334
361
  build: function (m, ctx) {
335
362
  var tpl = m.fields.certTemplate.value.result;
336
- // RFC 4211 §5 serialNumber, signingAlg, issuerUID, and subjectUID MUST be
363
+ // RFC 4211 sec. 5 -- serialNumber, signingAlg, issuerUID, and subjectUID MUST be
337
364
  // omitted from a REQUEST's CertTemplate: serialNumber and signingAlg are
338
365
  // assigned by the CA, and the UID pair is deprecated. A requester must not
339
366
  // dictate a CA-assigned value (a requester-chosen serialNumber is a real
@@ -350,7 +377,7 @@ var CERT_REQUEST = schema.seq([
350
377
  : ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
351
378
  for (var i = 0; i < caAssigned.length; i++) {
352
379
  if (tpl[caAssigned[i]] !== null) {
353
- throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted it is CA-assigned or deprecated (RFC 4211 §5)");
380
+ throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted -- it is CA-assigned or deprecated (RFC 4211 sec. 5)");
354
381
  }
355
382
  }
356
383
  return {
@@ -358,7 +385,7 @@ var CERT_REQUEST = schema.seq([
358
385
  certReqIdHex: m.fields.certReqId.node.content.toString("hex"),
359
386
  certTemplate: tpl,
360
387
  controls: m.fields.controls.present ? m.fields.controls.value.result : null,
361
- certReqBytes: m.node.bytes, // the exact CertRequest TLV the POP signature covers (§4.1)
388
+ certReqBytes: m.node.bytes, // the exact CertRequest TLV the POP signature covers (sec. 4.1)
362
389
  };
363
390
  },
364
391
  });
@@ -377,33 +404,33 @@ var CERT_REQ_MSG = schema.seq([
377
404
  var certReq = m.fields.certReq.value.result;
378
405
  var popo = m.fields.popo.present ? m.fields.popo.value : null;
379
406
  // A POP whose signature/MAC covers the certReq requires the CertTemplate to
380
- // contain both subject and publicKey otherwise there is no complete request
407
+ // contain both subject and publicKey -- otherwise there is no complete request
381
408
  // to sign / MAC over, and a verifier would be bound to the wrong identity/key.
382
409
  var complete = certReq.certTemplate.subject !== null && certReq.certTemplate.publicKey !== null;
383
- // RFC 4211 §4.1 for a signature POP, poposkInput's presence is fixed by the
410
+ // RFC 4211 sec. 4.1 -- for a signature POP, poposkInput's presence is fixed by the
384
411
  // CertTemplate: it MUST be omitted when the template carries BOTH subject and
385
412
  // publicKey (the signature is then over the DER of the CertRequest), and MUST be
386
413
  // present otherwise.
387
414
  if (popo && popo.type === "signature") {
388
415
  if (complete && popo.poposkInput !== null) {
389
- throw ctx.E("crmf/bad-popo", "poposkInput MUST be omitted when the CertTemplate contains both subject and publicKey (RFC 4211 §4.1)");
416
+ throw ctx.E("crmf/bad-popo", "poposkInput MUST be omitted when the CertTemplate contains both subject and publicKey (RFC 4211 sec. 4.1)");
390
417
  }
391
418
  if (!complete && popo.poposkInput === null) {
392
- throw ctx.E("crmf/bad-popo", "poposkInput MUST be present when the CertTemplate lacks subject or publicKey (RFC 4211 §4.1)");
419
+ throw ctx.E("crmf/bad-popo", "poposkInput MUST be present when the CertTemplate lacks subject or publicKey (RFC 4211 sec. 4.1)");
393
420
  }
394
- // RFC 4211 §4.1 the POPOSigningKeyInput publicKey MUST be exactly the template
421
+ // RFC 4211 sec. 4.1 -- the POPOSigningKeyInput publicKey MUST be exactly the template
395
422
  // publicKey (both surfaced as canonical SubjectPublicKeyInfo DER), so a request
396
423
  // cannot sign one key while asking for a certificate over a different key.
397
424
  if (popo.poposkInput !== null && certReq.certTemplate.publicKey !== null &&
398
425
  !popo.poposkInput.publicKey.equals(certReq.certTemplate.publicKey.bytes)) {
399
- throw ctx.E("crmf/bad-popo", "POPOSigningKeyInput publicKey MUST equal the CertTemplate publicKey (RFC 4211 §4.1)");
426
+ throw ctx.E("crmf/bad-popo", "POPOSigningKeyInput publicKey MUST equal the CertTemplate publicKey (RFC 4211 sec. 4.1)");
400
427
  }
401
428
  }
402
- // RFC 4211 §4.24.3 a MAC-based POPOPrivKey (dhMAC / agreeMAC) proves
429
+ // RFC 4211 sec. 4.2/sec. 4.3 -- a MAC-based POPOPrivKey (dhMAC / agreeMAC) proves
403
430
  // possession by MACing the certReq, which MUST include both subject and publicKey.
404
431
  if (popo && (popo.type === "keyEncipherment" || popo.type === "keyAgreement") &&
405
432
  (popo.method === "dhMAC" || popo.method === "agreeMAC") && !complete) {
406
- throw ctx.E("crmf/bad-popo", "a MAC-based key-agreement/encipherment POP (dhMAC/agreeMAC) requires the CertTemplate to contain both subject and publicKey (RFC 4211 §4.24.3)");
433
+ throw ctx.E("crmf/bad-popo", "a MAC-based key-agreement/encipherment POP (dhMAC/agreeMAC) requires the CertTemplate to contain both subject and publicKey (RFC 4211 sec. 4.2/sec. 4.3)");
407
434
  }
408
435
  return {
409
436
  certReq: certReq,
@@ -413,8 +440,10 @@ var CERT_REQ_MSG = schema.seq([
413
440
  },
414
441
  });
415
442
 
416
- // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (§3) an empty sequence
417
- // is malformed.
443
+ // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg (sec. 3) -- an empty sequence
444
+ // is malformed. The top-level reject code predates the
445
+ // `<prefix>/not-a-<structure>` convention and stays frozen on the public
446
+ // error-code surface.
418
447
  var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
419
448
  assert: "sequence", min: 1, code: "crmf/bad-cert-req-messages", what: "CertReqMessages",
420
449
  build: function (m) { return { messages: m.items.map(function (it) { return it.value.result; }) }; },
@@ -434,17 +463,20 @@ var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
434
463
  * `{ messages: [ { certReq, popo, regInfo } ] }`. Each `certReq` is
435
464
  * `{ certReqId, certReqIdHex, certTemplate, controls, certReqBytes }`, and
436
465
  * `certTemplate` carries the requestable certificate fields (`version`, `issuer`,
437
- * `validity`, `subject`, `publicKey`, `extensions` each `null` when absent). The
438
- * CA-assigned / deprecated fields RFC 4211 §5 requires a request to omit
466
+ * `validity`, `subject`, `publicKey`, `extensions` -- each `null` when absent). The
467
+ * CA-assigned / deprecated fields RFC 4211 sec. 5 requires a request to omit
439
468
  * (`serialNumber`, `signingAlg`, `issuerUID`, `subjectUID`) are rejected, not
440
469
  * surfaced. `popo` is
441
470
  * `null`, `{ type: "raVerified" }`, `{ type: "signature", poposkInput,
442
471
  * algorithmIdentifier, signature }`, or `{ type: "keyEncipherment" |
443
- * "keyAgreement", method, bytes }` (where `method` is the POPOPrivKey alternative
444
- * `thisMessage` / `subsequentMessage` / `dhMAC` / `agreeMAC` / `encryptedKey` each
445
- * structurally validated, the `encryptedKey` EnvelopedData included). When present, `poposkInput` is `{ bytes, signedBytes }`
446
- * `bytes` is the raw wire `[0]` TLV and `signedBytes` is the `POPOSigningKeyInput`
447
- * re-tagged to the `SEQUENCE` DER the signature actually covers (RFC 4211 §4.1).
472
+ * "keyAgreement", method, bytes }` (where `method` is the POPOPrivKey alternative --
473
+ * `thisMessage` / `subsequentMessage` / `dhMAC` / `agreeMAC` / `encryptedKey` -- each
474
+ * structurally validated, the `encryptedKey` EnvelopedData included). When present, `poposkInput` is
475
+ * `{ bytes, signedBytes, publicKey }` -- `bytes` is the raw wire `[0]` TLV,
476
+ * `signedBytes` is the `POPOSigningKeyInput` re-tagged to the `SEQUENCE` DER the
477
+ * signature actually covers, and `publicKey` is the canonical
478
+ * `SubjectPublicKeyInfo` DER the RFC 4211 sec. 4.1 template-match check compares
479
+ * against the `CertTemplate` publicKey (a POP verifier imports it directly).
448
480
  * `certReqBytes` is the exact `CertRequest` byte range a proof-of-possession
449
481
  * verifier hashes when `poposkInput` is absent.
450
482
  *
@@ -469,7 +501,7 @@ var parse = pkix.makeParser({
469
501
  * @related pki.schema.crmf.parse
470
502
  *
471
503
  * Extract the DER bytes from a PEM block (RFC 4211 registers no RFC 7468 label, so
472
- * the first block is taken unless `label` is given CRMF rides inside CMP / EST as
504
+ * the first block is taken unless `label` is given -- CRMF rides inside CMP / EST as
473
505
  * DER in practice). Throws `PemError` on a missing envelope or a non-base64 body.
474
506
  *
475
507
  * @example
@@ -477,6 +509,23 @@ var parse = pkix.makeParser({
477
509
  */
478
510
  function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
479
511
 
512
+ /**
513
+ * @primitive pki.schema.crmf.pemEncode
514
+ * @signature pki.schema.crmf.pemEncode(der, label) -> string
515
+ * @since 0.1.23
516
+ * @status experimental
517
+ * @spec RFC 7468, RFC 4211
518
+ * @related pki.schema.crmf.pemDecode
519
+ *
520
+ * Wrap DER bytes in a PEM envelope. RFC 4211 registers no RFC 7468 label, so
521
+ * `label` is REQUIRED -- the operator names the envelope explicitly (mirroring
522
+ * `pemDecode`, which accepts any label). Omitting it throws `pem/bad-label`.
523
+ *
524
+ * @example
525
+ * var pem = pki.schema.crmf.pemEncode(der, "CERT REQUEST MESSAGES");
526
+ */
527
+ function pemEncode(der, label) { return pkix.pemEncode(der, label, PemError); }
528
+
480
529
  // matches(root): a CertReqMessages is a universal SEQUENCE whose first CertReqMsg
481
530
  // is a SEQUENCE whose first CertRequest is a SEQUENCE leading with a universal
482
531
  // INTEGER (certReqId) then a universal SEQUENCE (certTemplate). This 3-level probe
@@ -495,15 +544,22 @@ function matches(root) {
495
544
  schema.isUniversal(tpl, TAGS.SEQUENCE) && !!tpl.children;
496
545
  }
497
546
 
498
- // Validate a bare, already-decoded CertReqMessages / CertTemplate node for a
547
+ // Validate a bare, already-decoded CertReqMessages / CertTemplate node -- for a
499
548
  // composer that carries these structures inside its own envelope (an RFC 9810
500
549
  // CMP PKIBody's ir/cr/kur/krr/ccr arms; a RevDetails.certDetails). NS-bound to
501
550
  // crmf so an interior failure throws CrmfError with a crmf/* code, never the
502
551
  // composer's class wearing a foreign code. Same contract as the CMS walkers.
503
- // opts.allowSigningAlg relaxes the request-template signingAlg-omission rule for
504
- // the CMP cross-certification (ccr) arm only (RFC 9810 Appendix D.6).
552
+ // The opts carry the RFC 9810 Appendix D.6 ccr-profile overrides of RFC 4211 sec. 5,
553
+ // applied by the CMP composer to the cross-certification arm only:
554
+ // opts.allowSigningAlg relaxes the request-template signingAlg-omission rule and
555
+ // opts.allowV1Version accepts a CertTemplate version of v1 ("version v1 or v3").
505
556
  function walkCertReqMessages(node, opts) {
506
- var ctx = (opts && opts.allowSigningAlg) ? Object.assign({}, NS, { allowSigningAlg: true }) : NS;
557
+ var ctx = NS;
558
+ if (opts && (opts.allowSigningAlg || opts.allowV1Version)) {
559
+ ctx = Object.assign({}, NS);
560
+ if (opts.allowSigningAlg) ctx.allowSigningAlg = true;
561
+ if (opts.allowV1Version) ctx.allowV1Version = true;
562
+ }
507
563
  return schema.walk(CERT_REQ_MESSAGES, node, ctx).result;
508
564
  }
509
565
  function walkCertTemplate(node) { return schema.walk(CERT_TEMPLATE, node, NS).result; }
@@ -511,11 +567,12 @@ function walkCertTemplate(node) { return schema.walk(CERT_TEMPLATE, node, NS).re
511
567
  module.exports = {
512
568
  parse: parse,
513
569
  pemDecode: pemDecode,
570
+ pemEncode: pemEncode,
514
571
  matches: matches,
515
572
  walkCertReqMessages: walkCertReqMessages,
516
573
  walkCertTemplate: walkCertTemplate,
517
574
  // The top schema, exposed (not on the curated pki.schema.crmf surface) so a
518
575
  // round-trip test can prove schema.encode(schema, v) -> decode -> parse recovers
519
- // v the invariant that structurally validates the IMPLICIT/EXPLICIT tag handling.
576
+ // v -- the invariant that structurally validates the IMPLICIT/EXPLICIT tag handling.
520
577
  certReqMessagesSchema: CERT_REQ_MESSAGES,
521
578
  };