@blamejs/pki 0.8.10 → 0.8.11

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-crl.js CHANGED
@@ -36,11 +36,6 @@ var PemError = frameworkError.PemError;
36
36
  var TAGS = asn1.TAGS;
37
37
 
38
38
 
39
- var CRL_REASONS = pkix.CRL_REASON_NAMES;
40
-
41
- var OID_CRL_NUMBER = oid.byName("cRLNumber");
42
- var OID_REASON_CODE = oid.byName("reasonCode");
43
- var OID_INVALIDITY_DATE = oid.byName("invalidityDate");
44
39
 
45
40
  var NS = pkix.makeNS("crl", CrlError, oid);
46
41
 
@@ -51,29 +46,23 @@ var TIME = pkix.time(NS);
51
46
 
52
47
  var CRL_VERSION = pkix.versionReader(NS, { "1": 2 });
53
48
 
49
+ /** @internal The decoders are the shared scope registry's, so which extensions a CRL profiles at
50
+ * which scope is stated once rather than in an if/else chain here. What stays here is this
51
+ * module's own wrapping: every fault becomes `crl/bad-extension-value` naming the extension, and
52
+ * an OID no scope profiles keeps its raw value. The signature and the `undefined`-means-both
53
+ * reading are unchanged, because `lib/lint.js` calls this with one argument. */
54
+ var CRL_EXT_DECODERS = pkix.crlExtensionDecoders(NS);
55
+
54
56
  function decodeExt(ext, scope, ctx) {
55
57
  var value = ext.value;
56
- var atCrl = scope !== "entry";
57
- var atEntry = scope !== "crl";
58
- try {
59
- if (ext.oid === OID_CRL_NUMBER && atCrl) {
60
- value = asn1.read.integer(pkix.decodeNested(ext.value, ctx, "a cRLNumber extension value"));
61
- if (value < 0n) throw new Error("cRLNumber must be non-negative (INTEGER 0..MAX)");
62
- } else if (ext.oid === OID_REASON_CODE && atEntry) {
63
- var reason = asn1.read.enumerated(pkix.decodeNested(ext.value, ctx, "a reasonCode extension value"));
64
- if (!_hasOwn(CRL_REASONS, reason.toString())) throw new Error("undefined CRLReason " + reason.toString());
65
- value = Number(reason);
66
- } else if (ext.oid === OID_INVALIDITY_DATE && atEntry) {
67
- var n = pkix.decodeNested(ext.value, ctx, "an invalidityDate extension value");
68
- if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
69
- throw new Error("invalidityDate must be a GeneralizedTime");
70
- }
71
- value = asn1.read.time(n);
58
+ var decoder = CRL_EXT_DECODERS.at(ext.oid, scope);
59
+ if (decoder !== null) {
60
+ try { value = decoder(ext.value, ctx); }
61
+ catch (e) {
62
+ throw NS.E("crl/bad-extension-value", "malformed " + (ext.name || ext.oid) + " extension value: " + ((e && e.message) || String(e)), e);
72
63
  }
73
- } catch (e) {
74
- throw NS.E("crl/bad-extension-value", "malformed " + (ext.name || ext.oid) + " extension value: " + ((e && e.message) || String(e)), e);
75
64
  }
76
- return { oid: ext.oid, name: ext.name, critical: ext.critical, value: value };
65
+ return { oid: ext.oid, name: ext.name, critical: ext.critical, value: value, valueBytes: ext.value };
77
66
  }
78
67
 
79
68
  var REVOKED_ENTRY = schema.seq([
@@ -169,6 +158,10 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
169
158
  * validated on the way in; a malformed CertificateList / TBSCertList throws a
170
159
  * typed `CrlError` (`crl/*`) and a leaf-level codec fault surfaces as `asn1/*`.
171
160
  *
161
+ * Each extension record is `{ oid, name, critical, value, valueBytes }`. `valueBytes` is the raw
162
+ * `extnValue` octets and `value` is the decoded form for the three extensions this parse decodes in
163
+ * place, `cRLNumber`, `reasonCode` and `invalidityDate`, and the same octets for every other.
164
+ *
172
165
  * @example
173
166
  * var pair = await pki.key.generate("Ed25519");
174
167
  * var key = await pki.key.export(pair.privateKey);
@@ -245,19 +238,11 @@ function matches(root) {
245
238
  return schema.isUniversalOneOf(pos, [TAGS.UTC_TIME, TAGS.GENERALIZED_TIME]);
246
239
  }
247
240
 
248
- /** @internal The criticality RFC 5280 sec. 5.3 fixes for each CRL entry extension, keyed by dotted
249
- * OID through the registry the caller passes: the CRL signer, the OCSP signer (sec. 4.4.5 carries
250
- * the same extensions into a SingleResponse) and the linter all read this one table. */
251
- var ENTRY_EXTENSION_CRITICALITY_ROWS = [
252
- ["reasonCode", false, "RFC 5280 sec. 5.3.1"], ["invalidityDate", false, "RFC 5280 sec. 5.3.2"],
253
- ["certificateIssuer", true, "RFC 5280 sec. 5.3.3"],
254
- ];
241
+ /** @internal The criticality RFC 5280 sec. 5.3 fixes for each CRL entry extension, from the shared
242
+ * scope registry: the CRL signer, the OCSP signer (sec. 4.4.5 carries the same extensions into a
243
+ * SingleResponse) and the linter all read this one table. */
255
244
  function entryExtensionCriticality(oidRegistry) {
256
- var table = intrinsic.create(null);
257
- intrinsic.forEach(ENTRY_EXTENSION_CRITICALITY_ROWS, function (r) {
258
- table[oidRegistry.byName(r[0])] = { critical: r[1], citation: r[2] };
259
- });
260
- return table;
245
+ return pkix.crlEntryFixedCriticality(oidRegistry);
261
246
  }
262
247
  /** @internal Reads a CRL entry extension's value the way this parser reads it, for a consumer that
263
248
  * meets one outside a CRL (an OCSP SingleResponse, RFC 6960 sec. 4.4.5): reasonCode and
@@ -272,19 +257,132 @@ function readEntryExtension(ext, ctx) {
272
257
  catch (e) {
273
258
  throw NS.E("crl/bad-extension-value", "malformed certificateIssuer extension value: " + ((e && e.message) || intrinsic.String(e)), e);
274
259
  }
275
- return { oid: ext.oid, name: ext.name, critical: ext.critical, value: names };
260
+ return { oid: ext.oid, name: ext.name, critical: ext.critical, value: names, valueBytes: ext.value };
261
+ }
262
+
263
+ /** @internal The three scopes a CRL reader meets, as the shared door reads them. `ocsp-single`
264
+ * carries the entry tables under its own key because RFC 6960 sec. 4.4.5 puts a CRL entry
265
+ * extension in an OCSP SingleResponse, where the row should say where it was read rather than
266
+ * claim it came off a CRL entry. */
267
+ function crlDoorTable(arm) {
268
+ return function (ctx) { return pkix.crlExtensionDecoders(ctx).doorScope[arm]; };
276
269
  }
270
+ var CRL_SCOPES = intrinsic.assign(intrinsic.create(null), {
271
+ "crl": { decoders: crlDoorTable("crl"), criticality: pkix.crlFixedCriticality(oid) },
272
+ "crl-entry": { decoders: crlDoorTable("entry"), criticality: pkix.crlEntryFixedCriticality(oid) },
273
+ "ocsp-single": { decoders: crlDoorTable("entry"), criticality: pkix.crlEntryFixedCriticality(oid) },
274
+ });
275
+
276
+ /** @internal The parse decodes cRLNumber, reasonCode and invalidityDate in place, so its records
277
+ * carry the decoded value. The door reads the raw octets the record kept beside it, and decodes
278
+ * them itself under this call's caps and this call's scope. */
279
+ function rawRecords(list) {
280
+ return intrinsic.map(list || [], function (e) {
281
+ return { oid: e.oid, name: e.name, critical: e.critical, value: e.valueBytes };
282
+ });
283
+ }
284
+
285
+ var DOOR = pkix.extensionDoor({
286
+ ns: NS, ErrorClass: CrlError, key: "crl", kind: "crl", what: "the CRL",
287
+ parse: parse, scopes: CRL_SCOPES,
288
+ segments: function (crl) {
289
+ var out = [{ list: rawRecords(crl.crlExtensions), scope: "crl" }];
290
+ var revoked = crl.revokedCertificates || [];
291
+ for (var i = 0; i < revoked.length; i++) {
292
+ intrinsic.push(out, { list: rawRecords(revoked[i].crlEntryExtensions), scope: "crl-entry", containerIndex: i });
293
+ }
294
+ return out;
295
+ },
296
+ });
297
+
298
+ /**
299
+ * @primitive pki.schema.crl.decodeExtensions
300
+ * @signature pki.schema.crl.decodeExtensions(crlOrBytes, opts?) -> rows
301
+ * @since 0.8.11
302
+ * @status stable
303
+ * @spec RFC 5280 sec. 5.2, RFC 5280 sec. 5.3
304
+ * @related pki.schema.crl.parse, pki.schema.x509.decodeExtensions, pki.lint.crl
305
+ *
306
+ * Read a CRL's extensions as decoded records: the CRL's own extensions first, then each revoked
307
+ * entry's. The argument is the object `parse` returned or the bytes `parse` takes.
308
+ *
309
+ * One array spans both scopes rather than two, because RFC 5280 sec. 5.3 makes an unreadable
310
+ * critical ENTRY extension a verdict about the whole CRL, and a caller handed two lists can read
311
+ * one and not the other. Each row names where it was read: `scope` is `"crl"` or `"crl-entry"`, and
312
+ * `containerIndex` is the position of the revoked entry an entry row came from, or `null` for the
313
+ * CRL's own.
314
+ *
315
+ * Each row is `{ oid, name, critical, value, scope, index, containerIndex, state, decoded, code,
316
+ * profile }`. `state` is the discriminator: `"decoded"`, `"unrecognized"` for an OID no decoder is
317
+ * registered for AT THAT SCOPE, and `"undecodable"`. A `cRLNumber` on a revoked entry and a
318
+ * `reasonCode` on the CRL are each unrecognized rather than undecodable, because neither scope
319
+ * profiles the other's extension.
320
+ *
321
+ * @opts
322
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
323
+ * undecodable value. It does not turn an unrecognized OID into a throw.
324
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call. Each may only
325
+ * tighten the built-in ceiling, and each bounds the decoding this call performs.
326
+ * @example
327
+ * var pair = await pki.key.generate("Ed25519");
328
+ * var key = await pki.key.export(pair.privateKey);
329
+ * var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
330
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
331
+ * extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
332
+ * var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"), crlNumber: 7n,
333
+ * revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }] },
334
+ * { cert: caCert, key: key });
335
+ * var rows = pki.schema.crl.decodeExtensions(crlDer);
336
+ * rows.filter(function (r) { return r.scope === "crl-entry" && r.name === "reasonCode"; })[0].decoded; // -> 1
337
+ */
338
+ var decodeExtensions = DOOR.decodeExtensions;
339
+
340
+ /**
341
+ * @primitive pki.schema.crl.decodeExtension
342
+ * @signature pki.schema.crl.decodeExtension(ext, opts) -> row
343
+ * @since 0.8.11
344
+ * @status stable
345
+ * @spec RFC 5280 sec. 5.2, RFC 5280 sec. 5.3, RFC 6960 sec. 4.4.5
346
+ * @related pki.schema.crl.decodeExtensions
347
+ *
348
+ * Read one CRL extension record as a decoded row. `opts.scope` is required and says which table to
349
+ * read it against, because the same identifier means different things at the three scopes a CRL
350
+ * reader meets. The argument is `{ oid, name, critical, value }`, with `value` the raw `extnValue`
351
+ * octets, which a `parse` result carries as `valueBytes`.
352
+ *
353
+ * @opts
354
+ * - `scope` (string, required) -- `"crl"` for a CRL's own extension, `"crl-entry"` for a revoked
355
+ * entry's, `"ocsp-single"` for one carried in an OCSP `SingleResponse` (RFC 6960 sec. 4.4.5).
356
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
357
+ * undecodable value.
358
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
359
+ * @example
360
+ * var pair = await pki.key.generate("Ed25519");
361
+ * var key = await pki.key.export(pair.privateKey);
362
+ * var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
363
+ * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
364
+ * extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"] } }, { key: key });
365
+ * var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"), crlNumber: 7n,
366
+ * revoked: [{ serialNumber: 0x0a3fn, revocationDate: new Date("2026-01-15T00:00:00Z"), reason: "keyCompromise" }] },
367
+ * { cert: caCert, key: key });
368
+ * var e = pki.schema.crl.parse(crlDer).revokedCertificates[0].crlEntryExtensions[0];
369
+ * pki.schema.crl.decodeExtension({ oid: e.oid, name: e.name, critical: e.critical, value: e.valueBytes },
370
+ * { scope: "crl-entry" }).decoded; // -> 1
371
+ */
372
+ var decodeExtension = DOOR.decodeExtension;
277
373
 
278
374
  module.exports = {
279
375
  parse: parse,
280
376
  pemDecode: pemDecode,
281
377
  pemEncode: pemEncode,
282
378
  matches: matches,
379
+ decodeExtensions: decodeExtensions,
380
+ decodeExtension: decodeExtension,
283
381
  /** @internal The reader this parser applies to a CRL entry extension (reasonCode, invalidityDate),
284
382
  * for a consumer that meets the same extension elsewhere: RFC 6960 sec. 4.4.5 carries every CRL
285
383
  * entry extension into an OCSP SingleResponse, and the linter reads it with this rather than a
286
384
  * second one. Not on the curated `pki.schema.crl` surface. */
287
- decodeExtension: decodeExt,
385
+ decodeExt: decodeExt,
288
386
  entryExtensionCriticality: entryExtensionCriticality,
289
387
  readEntryExtension: readEntryExtension,
290
388
  };
@@ -40,6 +40,7 @@ var pkix = require("./schema-pkix");
40
40
  var cms = require("./schema-cms");
41
41
  var oid = require("./oid");
42
42
  var frameworkError = require("./framework-error");
43
+ var intrinsic = require("./guard-intrinsic");
43
44
 
44
45
  var CrmfError = frameworkError.CrmfError;
45
46
  var PemError = frameworkError.PemError;
@@ -140,7 +141,7 @@ var POPO_SIGNING_KEY = schema.seq([
140
141
  },
141
142
  });
142
143
 
143
- var POPOPRIVKEY_METHODS = Object.assign(Object.create(null), { 0: "thisMessage", 1: "subsequentMessage", 2: "dhMAC", 3: "agreeMAC", 4: "encryptedKey" });
144
+ var POPOPRIVKEY_METHODS = intrinsic.assign(intrinsic.create(null), { 0: "thisMessage", 1: "subsequentMessage", 2: "dhMAC", 3: "agreeMAC", 4: "encryptedKey" });
144
145
  function popoPrivKey(type) {
145
146
  return schema.decode(function (n, ctx) {
146
147
  if (!n.children || n.children.length !== 1) {
@@ -203,7 +204,7 @@ var CONTROL = schema.seq([
203
204
  return { type: t, name: ctx.oid.name(t) || null, value: m.fields.value.node.bytes };
204
205
  },
205
206
  });
206
- function mapControls(m) { return m.items.map(function (it) { return it.value.result; }); }
207
+ function mapControls(m) { return intrinsic.map(m.items, function (it) { return it.value.result; }); }
207
208
  var CONTROLS = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-controls", what: "Controls", build: mapControls });
208
209
  var REG_INFO = schema.seqOf(CONTROL, { assert: "sequence", min: 1, code: "crmf/bad-reg-info", what: "regInfo", build: mapControls });
209
210
 
@@ -261,7 +262,7 @@ var CERT_REQUEST = schema.seq([
261
262
  }
262
263
  return {
263
264
  certReqId: m.fields.certReqId.value,
264
- certReqIdHex: m.fields.certReqId.node.content.toString("hex"),
265
+ certReqIdHex: intrinsic.bufToString(m.fields.certReqId.node.content, "hex"),
265
266
  certTemplate: tpl,
266
267
  controls: m.fields.controls.present ? m.fields.controls.value.result : null,
267
268
  certReqBytes: m.node.bytes,
@@ -287,7 +288,7 @@ var CERT_REQ_MSG = schema.seq([
287
288
  throw ctx.E("crmf/bad-popo", "poposkInput MUST be present when the CertTemplate lacks subject or publicKey (RFC 4211 sec. 4.1)");
288
289
  }
289
290
  if (popo.poposkInput !== null && certReq.certTemplate.publicKey !== null &&
290
- !popo.poposkInput.publicKey.equals(certReq.certTemplate.publicKey.bytes)) {
291
+ !intrinsic.bufferEquals(popo.poposkInput.publicKey, certReq.certTemplate.publicKey.bytes)) {
291
292
  throw ctx.E("crmf/bad-popo", "POPOSigningKeyInput publicKey MUST equal the CertTemplate publicKey (RFC 4211 sec. 4.1)");
292
293
  }
293
294
  }
@@ -305,7 +306,7 @@ var CERT_REQ_MSG = schema.seq([
305
306
 
306
307
  var CERT_REQ_MESSAGES = schema.seqOf(CERT_REQ_MSG, {
307
308
  assert: "sequence", min: 1, code: "crmf/bad-cert-req-messages", what: "CertReqMessages",
308
- build: function (m) { return { messages: m.items.map(function (it) { return it.value.result; }) }; },
309
+ build: function (m) { return { messages: intrinsic.map(m.items, function (it) { return it.value.result; }) }; },
309
310
  });
310
311
 
311
312
 
@@ -422,7 +423,7 @@ function matches(root) {
422
423
  function walkCertReqMessages(node, opts, outer) {
423
424
  var ctx = schema.withResources(NS, outer);
424
425
  if (opts && (opts.allowSigningAlg || opts.allowV1Version)) {
425
- ctx = Object.assign({}, ctx);
426
+ ctx = intrinsic.assign({}, ctx);
426
427
  if (opts.allowSigningAlg) ctx.allowSigningAlg = true;
427
428
  if (opts.allowV1Version) ctx.allowV1Version = true;
428
429
  }
@@ -432,11 +433,95 @@ function walkCertTemplate(node, outer) {
432
433
  return schema.walk(CERT_TEMPLATE, node, schema.withResources(NS, outer)).result;
433
434
  }
434
435
 
436
+ /** @internal A CertTemplate asks for certificate extensions, so the certificate table reads them.
437
+ * As with a PKCS#10 request, criticality is part of what is asked for and RFC 4211 sec. 5 leaves
438
+ * the decision to the CA; the criticality RFC 5280 fixes is reported all the same. */
439
+ var CRMF_SCOPES = intrinsic.assign(intrinsic.create(null), {
440
+ "crmf-template": {
441
+ decoders: function (ctx) { return pkix.certExtensionDecoders(ctx).byOid; },
442
+ criticality: pkix.certFixedCriticality(oid),
443
+ },
444
+ });
445
+
446
+ var DOOR = pkix.extensionDoor({
447
+ ns: NS, ErrorClass: CrmfError, key: "crmf", kind: "crmf", what: "the certificate request messages",
448
+ parse: parse, scopes: CRMF_SCOPES, defaultScope: "crmf-template",
449
+ segments: function (parsed) {
450
+ var messages = parsed.messages || [];
451
+ var out = [];
452
+ for (var i = 0; i < messages.length; i++) {
453
+ var template = messages[i].certReq && messages[i].certReq.certTemplate;
454
+ intrinsic.push(out, { list: (template && template.extensions) || [], scope: "crmf-template", containerIndex: i });
455
+ }
456
+ return out;
457
+ },
458
+ });
459
+
460
+ /**
461
+ * @primitive pki.schema.crmf.decodeExtensions
462
+ * @signature pki.schema.crmf.decodeExtensions(messagesOrBytes, opts?) -> rows
463
+ * @since 0.8.11
464
+ * @status stable
465
+ * @spec RFC 4211 sec. 5, RFC 5280 sec. 4.2
466
+ * @related pki.schema.crmf.parse, pki.schema.csr.decodeExtensions
467
+ *
468
+ * Read the extensions the certificate templates in a `CertReqMessages` ask for, as decoded records.
469
+ * The argument is the object `parse` returned or the bytes `parse` takes, and the rows of every
470
+ * message are returned in one array, each naming the message it came from in `containerIndex`.
471
+ *
472
+ * Each row is `{ oid, name, critical, value, scope, index, containerIndex, state, decoded, code,
473
+ * profile }`, with `scope` `"crmf-template"`. Criticality here is part of the request: RFC 4211
474
+ * sec. 5 leaves which requested extensions to honor to the CA, and `profile` reports what RFC 5280
475
+ * would fix for the certificate the request would produce.
476
+ *
477
+ * @opts
478
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
479
+ * undecodable value. It does not turn an unrecognized OID into a throw.
480
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
481
+ * @example
482
+ * var pair = await pki.key.generate("Ed25519");
483
+ * var der = await pki.crmf.build(
484
+ * { certReqId: 0, certTemplate: { subject: "req.example", publicKey: await pki.key.export(pair.publicKey),
485
+ * extensions: { subjectAltName: [{ dNSName: "req.example" }] } } },
486
+ * { key: await pki.key.export(pair.privateKey) });
487
+ * var rows = pki.schema.crmf.decodeExtensions(der);
488
+ * rows[0].scope + " " + rows[0].name; // -> "crmf-template subjectAltName"
489
+ */
490
+ var decodeExtensions = DOOR.decodeExtensions;
491
+
492
+ /**
493
+ * @primitive pki.schema.crmf.decodeExtension
494
+ * @signature pki.schema.crmf.decodeExtension(ext, opts?) -> row
495
+ * @since 0.8.11
496
+ * @status stable
497
+ * @spec RFC 4211 sec. 5
498
+ * @related pki.schema.crmf.decodeExtensions
499
+ *
500
+ * Read one requested extension record as a decoded row, at scope `"crmf-template"`. The argument is
501
+ * one element of a `certTemplate`'s `extensions`, as `{ oid, name, critical, value }`.
502
+ *
503
+ * @opts
504
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
505
+ * undecodable value.
506
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
507
+ * @example
508
+ * var pair = await pki.key.generate("Ed25519");
509
+ * var der = await pki.crmf.build(
510
+ * { certReqId: 0, certTemplate: { subject: "req.example", publicKey: await pki.key.export(pair.publicKey),
511
+ * extensions: { subjectAltName: [{ dNSName: "req.example" }] } } },
512
+ * { key: await pki.key.export(pair.privateKey) });
513
+ * var msgs = pki.schema.crmf.parse(der);
514
+ * pki.schema.crmf.decodeExtension(msgs.messages[0].certReq.certTemplate.extensions[0]).name; // -> "subjectAltName"
515
+ */
516
+ var decodeExtension = DOOR.decodeExtension;
517
+
435
518
  module.exports = {
436
519
  parse: parse,
437
520
  pemDecode: pemDecode,
438
521
  pemEncode: pemEncode,
439
522
  matches: matches,
523
+ decodeExtensions: decodeExtensions,
524
+ decodeExtension: decodeExtension,
440
525
  walkCertReqMessages: walkCertReqMessages,
441
526
  walkCertTemplate: walkCertTemplate,
442
527
  certReqMessagesSchema: CERT_REQ_MESSAGES,
package/lib/schema-csr.js CHANGED
@@ -34,6 +34,7 @@ var schema = require("./schema-engine");
34
34
  var pkix = require("./schema-pkix");
35
35
  var oid = require("./oid");
36
36
  var frameworkError = require("./framework-error");
37
+ var intrinsic = require("./guard-intrinsic");
37
38
 
38
39
  var CsrError = frameworkError.CsrError;
39
40
  var PemError = frameworkError.PemError;
@@ -52,16 +53,16 @@ var DIRECTORY_STRING_TAGS = [
52
53
  asn1.TAGS.UTF8_STRING, asn1.TAGS.PRINTABLE_STRING, asn1.TAGS.TELETEX_STRING,
53
54
  asn1.TAGS.UNIVERSAL_STRING, asn1.TAGS.BMP_STRING,
54
55
  ];
55
- var RECOGNIZED_ATTRIBUTE_VALUE = Object.create(null);
56
+ var RECOGNIZED_ATTRIBUTE_VALUE = intrinsic.create(null);
56
57
  RECOGNIZED_ATTRIBUTE_VALUE[oid.byName("extensionRequest")] = function (node, ctx) {
57
58
  try { return { extensions: schema.walk(EXTENSIONS, node, ctx).result }; }
58
59
  catch (e) {
59
60
  throw ctx.E("csr/bad-attribute-value",
60
- "extensionRequest value must be a well-formed Extensions SEQUENCE (RFC 2985 sec. 5.4.2): " + ((e && e.message) || String(e)), e);
61
+ "extensionRequest value must be a well-formed Extensions SEQUENCE (RFC 2985 sec. 5.4.2): " + ((e && e.message) || intrinsic.String(e)), e);
61
62
  }
62
63
  };
63
64
  RECOGNIZED_ATTRIBUTE_VALUE[oid.byName("challengePassword")] = function (node, ctx) {
64
- if (node.tagClass !== "universal" || DIRECTORY_STRING_TAGS.indexOf(node.tagNumber) === -1) {
65
+ if (node.tagClass !== "universal" || intrinsic.indexOf(DIRECTORY_STRING_TAGS, node.tagNumber) === -1) {
65
66
  throw ctx.E("csr/bad-attribute-value", "challengePassword must be a DirectoryString (RFC 2985 sec. 5.4.1)");
66
67
  }
67
68
  var s;
@@ -80,7 +81,7 @@ var CERTIFICATION_REQUEST_INFO = schema.seq([
80
81
  ], {
81
82
  assert: "sequence", code: "csr/bad-cri", what: "certificationRequestInfo",
82
83
  build: function (m, ctx) {
83
- var attributes = m.fields.attributes.value.items.map(function (it) {
84
+ var attributes = intrinsic.map(m.fields.attributes.value.items, function (it) {
84
85
  var a = it.value.result;
85
86
  var checkValue = RECOGNIZED_ATTRIBUTE_VALUE[a.type];
86
87
  if (checkValue) {
@@ -89,7 +90,7 @@ var CERTIFICATION_REQUEST_INFO = schema.seq([
89
90
  throw ctx.E("csr/bad-attribute-value", (a.name || a.type) + " is a SINGLE VALUE attribute (RFC 2985 sec. 5.4)");
90
91
  }
91
92
  var enriched = checkValue(valueItems[0].node, ctx);
92
- if (enriched) { Object.keys(enriched).forEach(function (k) { a[k] = enriched[k]; }); }
93
+ if (enriched) { intrinsic.forEach(intrinsic.keys(enriched), function (k) { a[k] = enriched[k]; }); }
93
94
  }
94
95
  return a;
95
96
  });
@@ -205,10 +206,120 @@ function matches(root) {
205
206
  schema.isContext(k[3], 0);
206
207
  }
207
208
 
209
+ /** @internal A request asks for certificate extensions, so the certificate table reads them. What
210
+ * the scope changes is the verdict a caller draws: RFC 2985 sec. 5.4.2 makes criticality part of
211
+ * the REQUEST, and which requested extensions to honor, critical or not, is the CA's decision. The
212
+ * criticality profile is reported all the same, because a CA honoring a request against it would
213
+ * issue a non-conforming certificate. */
214
+ var CSR_SCOPES = intrinsic.assign(intrinsic.create(null), {
215
+ "csr-requested": {
216
+ decoders: function (ctx) { return pkix.certExtensionDecoders(ctx).byOid; },
217
+ criticality: pkix.certFixedCriticality(oid),
218
+ },
219
+ });
220
+ var OID_EXTENSION_REQUEST = oid.byName("extensionRequest");
221
+
222
+ function _err(code, message, cause) { return new CsrError(code, message, cause); }
223
+
224
+ var DOOR = pkix.extensionDoor({
225
+ ns: NS, ErrorClass: CsrError, key: "csr", kind: "csr", what: "the certification request",
226
+ parse: parse, scopes: CSR_SCOPES, defaultScope: "csr-requested", groups: true,
227
+ segments: function (csr, read) {
228
+ var attrs = csr.attributes || [];
229
+ var out = [];
230
+ for (var i = 0; i < attrs.length; i++) {
231
+ if (attrs[i].type !== OID_EXTENSION_REQUEST) continue;
232
+ intrinsic.push(out, { list: attrs[i].extensions || [], scope: "csr-requested", containerIndex: i });
233
+ }
234
+ if (!read.groups && out.length > 1) {
235
+ throw _err("csr/ambiguous-extension-request", "this request carries " + out.length +
236
+ " extensionRequest attributes, and which of them a CA should honor is not stated anywhere; " +
237
+ "pass { groups: true } to read each one separately");
238
+ }
239
+ return out;
240
+ },
241
+ group: function (rows, segments) {
242
+ var out = [], byIndex = intrinsic.create(null);
243
+ for (var s = 0; s < segments.length; s++) {
244
+ var g = { attributeIndex: segments[s].containerIndex, extensions: [] };
245
+ byIndex[segments[s].containerIndex] = g;
246
+ intrinsic.push(out, g);
247
+ }
248
+ for (var r = 0; r < rows.length; r++) intrinsic.push(byIndex[rows[r].containerIndex].extensions, rows[r]);
249
+ return out;
250
+ },
251
+ });
252
+
253
+ /**
254
+ * @primitive pki.schema.csr.decodeExtensions
255
+ * @signature pki.schema.csr.decodeExtensions(csrOrBytes, opts?) -> rows
256
+ * @since 0.8.11
257
+ * @status stable
258
+ * @spec RFC 2985 sec. 5.4.2, RFC 5280 sec. 4.2
259
+ * @related pki.schema.csr.parse, pki.schema.x509.decodeExtensions
260
+ *
261
+ * Read the extensions a certification request asks for, as decoded records. This is the verb a CA
262
+ * calls to see what a request wants before deciding what to issue. The argument is the object
263
+ * `parse` returned or the bytes `parse` takes.
264
+ *
265
+ * The rows come from the request's one `extensionRequest` attribute. A request carrying more than
266
+ * one is refused with `csr/ambiguous-extension-request`, because nothing says which of them a CA
267
+ * should honor; pass `groups: true` to read each attribute separately instead.
268
+ *
269
+ * Each row is `{ oid, name, critical, value, scope, index, containerIndex, state, decoded, code,
270
+ * profile }`, with `scope` `"csr-requested"` and `containerIndex` the position of the attribute the
271
+ * row came from. Criticality here is part of the request, not a verdict: RFC 2985 sec. 5.4.2 leaves
272
+ * which requested extensions to honor to the CA, and `profile` reports what RFC 5280 would fix for
273
+ * the certificate that request would produce.
274
+ *
275
+ * @opts
276
+ * - `groups` (boolean) -- return `[{ attributeIndex, extensions }]`, one entry per
277
+ * `extensionRequest` attribute, instead of one flat table. This is how a non-conforming request
278
+ * carrying several is read.
279
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
280
+ * undecodable value. It does not turn an unrecognized OID into a throw.
281
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
282
+ * @example
283
+ * var pair = await pki.key.generate("Ed25519");
284
+ * var csrDer = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey),
285
+ * extensionRequest: { subjectAltName: [{ dNSName: "req.example" }] } },
286
+ * { key: await pki.key.export(pair.privateKey) });
287
+ * var rows = pki.schema.csr.decodeExtensions(csrDer);
288
+ * rows.filter(function (r) { return r.name === "subjectAltName"; })[0].state; // -> "decoded"
289
+ */
290
+ var decodeExtensions = DOOR.decodeExtensions;
291
+
292
+ /**
293
+ * @primitive pki.schema.csr.decodeExtension
294
+ * @signature pki.schema.csr.decodeExtension(ext, opts?) -> row
295
+ * @since 0.8.11
296
+ * @status stable
297
+ * @spec RFC 2985 sec. 5.4.2
298
+ * @related pki.schema.csr.decodeExtensions
299
+ *
300
+ * Read one requested extension record as a decoded row, at scope `"csr-requested"`. The argument is
301
+ * one element of an `extensionRequest` attribute's `extensions`, as `{ oid, name, critical, value }`.
302
+ *
303
+ * @opts
304
+ * - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
305
+ * undecodable value.
306
+ * - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call.
307
+ * @example
308
+ * var pair = await pki.key.generate("Ed25519");
309
+ * var csrDer = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey),
310
+ * extensionRequest: { subjectAltName: [{ dNSName: "req.example" }] } },
311
+ * { key: await pki.key.export(pair.privateKey) });
312
+ * var csr = pki.schema.csr.parse(csrDer);
313
+ * pki.schema.csr.decodeExtension(csr.attributes[0].extensions[0]).state; // -> "decoded"
314
+ */
315
+ var decodeExtension = DOOR.decodeExtension;
316
+
208
317
  module.exports = {
209
318
  parse: parse,
210
319
  pemDecode: pemDecode,
211
320
  pemEncode: pemEncode,
212
321
  matches: matches,
322
+ decodeExtensions: decodeExtensions,
323
+ decodeExtension: decodeExtension,
213
324
  certificationRequestSchema: CERTIFICATION_REQUEST,
214
325
  };