@blamejs/pki 0.9.0 → 0.9.3

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/cms-sign.js CHANGED
@@ -205,6 +205,19 @@ function _assertAlgorithmProtectionPairs(pairs, digestAlgId, armAlgId, armTag) {
205
205
  function _resolveSignerContext(signer, opts) {
206
206
  var so = signer || intrinsic.create(null);
207
207
  var keyOnly = so.cert == null && so.spki != null;
208
+ /** @internal A descriptor names the digest and the padding the signature is made under, so a field
209
+ * this verb does not read is a request that was never carried out: measured, a `digestAlgoritm`
210
+ * typo signed under sha256 while the caller believed they had asked for sha384, and the same
211
+ * misspelling one argument along in `opts` was already a refusal. The two key sets below have
212
+ * described this shape since they were written, and `pkcs12-build` already holds the same
213
+ * descriptors to them; this is the module that owns them doing the same. */
214
+ guard.identifier.assertKnownDescriptorKeys(so, keyOnly ? KNOWN_SIGNER_KEY_ONLY_KEYS : KNOWN_SIGNER_CERT_KEYS,
215
+ _err, "cms/bad-input", function (k) {
216
+ return "unknown signer field " + guard.text.showValue(k) + " for the " + (keyOnly
217
+ ? "key-only form; it takes { key, spki, keyIdentifier } plus the signature parameters"
218
+ : "certificate form; it takes { key, cert } plus the signature parameters") +
219
+ " (pss, digestAlgorithm, combinedRsaSig)";
220
+ });
208
221
  var soKey = so.key;
209
222
  var soSpki = keyOnly ? guard.bytes.snapshotSource(so.spki, CmsError, "cms/bad-input", "a key-only signer's spki") : null;
210
223
  var certDer = keyOnly ? null : _normCertDer(so.cert);
@@ -534,6 +547,14 @@ function _resolveSignerIndices(spec, n) {
534
547
 
535
548
  function _buildCountersignature(targetSigOctets, countersigner, opts) {
536
549
  var so = countersigner || intrinsic.create(null);
550
+ /** @internal A countersignature is a SignerInfo (RFC 5652 sec. 11.4) and its descriptor names the
551
+ * same signature parameters, so a field this reader does not read is the same dropped request it
552
+ * is at `sign`. This reader is its own, so the gate at the other one did not cover it. A
553
+ * countersignature is always the certificate form, which is the `cert` read below. */
554
+ guard.identifier.assertKnownDescriptorKeys(so, KNOWN_SIGNER_CERT_KEYS, _err, "cms/bad-input", function (k) {
555
+ return "unknown countersigner field " + guard.text.showValue(k) +
556
+ "; it takes { key, cert } plus the signature parameters (pss, digestAlgorithm, combinedRsaSig)";
557
+ });
537
558
  var certDer = _normCertDer(so.cert);
538
559
  var cert = x509.parse(certDer);
539
560
  var scheme = signScheme.resolveSignScheme(cert, so, opts.signedAttributes === false, _signE);
package/lib/cms-verify.js CHANGED
@@ -1170,10 +1170,12 @@ function _snapshotTimestampTrust(opts, allowWeakDigests) {
1170
1170
  };
1171
1171
  }
1172
1172
 
1173
- /** @internal A signature over content is authorized by digitalSignature, contentCommitment or
1174
- * nonRepudiation alike; the shared helper carries the rest of the rule. */
1173
+ /** @internal The decoder emits the RFC 5280 sec. 4.2.1.3 bit name `nonRepudiation`, never the X.509
1174
+ * spelling `contentCommitment`, so naming the latter here asked for a name nothing defines.
1175
+ * A signature over content is authorized by digitalSignature or nonRepudiation alike; the shared
1176
+ * helper carries the rest of the rule. */
1175
1177
  var _permitsSigning = pkix.keyUsagePermits(pkix.makeNS("cms", CmsError, oid),
1176
- ["digitalSignature", "contentCommitment", "nonRepudiation"]);
1178
+ ["digitalSignature", "nonRepudiation"]);
1177
1179
 
1178
1180
  function _keyUsagePermitsSigning(parsedCerts, der) {
1179
1181
  var entry = guard.list.firstMatch(parsedCerts, function (c) { return intrinsic.bufferEquals(c.der, der); });
package/lib/constants.js CHANGED
@@ -189,6 +189,9 @@ var LIMITS = Object.assign(Object.create(null), {
189
189
  PBKDF2_MAX_SALT: 1024,
190
190
  MIME_MAX_BYTES: BYTES.mib(16),
191
191
  HEADER_LINE_MAX_OCTETS: 998,
192
+ MIME_MAX_PARTS: 1024,
193
+ MIME_MAX_HEADERS: 1024,
194
+ MIME_MAX_PARAMS: 256,
192
195
  COMPRESS_MAX_BYTES: BYTES.mib(16),
193
196
  TLS_CERT_MSG_MAX_BYTES: BYTES.mib(16) - 1,
194
197
  TLS_CERT_MAX_ENTRIES: 100,
package/lib/crl-sign.js CHANGED
@@ -682,7 +682,7 @@ function _verify(crl, issuer) {
682
682
  var parsed = _coerceCrl(crl);
683
683
  var resolved = _resolveIssuer(issuer);
684
684
  var issuerMaySign = resolved.cert ? _issuerMaySign(parsed, resolved.cert) : true;
685
- return crlVerify.verifyCrlSignature(parsed, resolved.spki).then(function (sigOk) {
685
+ return intrinsic.chain(crlVerify.verifyCrlSignature(parsed, resolved.spki), function (sigOk) {
686
686
  var signatureValid = sigOk === true;
687
687
  var valid = issuerMaySign && signatureValid;
688
688
  return guard.verdict.of(
package/lib/crmf-sign.js CHANGED
@@ -1255,7 +1255,7 @@ function _build(spec, key, opts) {
1255
1255
  * `Buffer`; a malformed template throws a typed `CrmfError`.
1256
1256
  *
1257
1257
  * @example
1258
- * var tpl = pki.crmf.buildCertTemplate({ serialNumber: 42n, issuer: "CN=CA" });
1258
+ * var tpl = pki.crmf.buildCertTemplate({ serialNumber: 42n, issuer: "CA" });
1259
1259
  * pki.asn1.decode(tpl).tagNumber === pki.asn1.TAGS.SEQUENCE; // the CertTemplate SEQUENCE
1260
1260
  */
1261
1261
  function buildCertTemplate(template) { return _encodeCertTemplate(template, { revocation: true }).der; }
package/lib/csr-sign.js CHANGED
@@ -338,8 +338,10 @@ function _coerceCsr(request) {
338
338
  * var req = await pki.csr.sign({ subject: "device-42", subjectPublicKey: spki }, { key: pkcs8 });
339
339
  * var r = await pki.csr.verify(req);
340
340
  * // Issue from r.subject / r.subjectPublicKeyInfo / r.attributes, which are the verified fields.
341
+ * // `r.subject.bytes` is the request's own Name DER, so the issued subject is the requested one
342
+ * // byte for byte. `r.subject.dn` is its text form, which a builder reads as a common name.
341
343
  * var issued = r.verified
342
- * ? await pki.x509.sign({ subject: r.subject.dn, subjectPublicKey: r.subjectPublicKeyInfo.bytes,
344
+ * ? await pki.x509.sign({ subject: r.subject.bytes, subjectPublicKey: r.subjectPublicKeyInfo.bytes,
343
345
  * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z") },
344
346
  * { key: pkcs8 })
345
347
  * : null;
@@ -347,7 +349,7 @@ function _coerceCsr(request) {
347
349
  function verify(request) { return guard.async.deferred(function () { return _verify(request); }); }
348
350
  function _verify(request) {
349
351
  var parsed = _coerceCsr(request);
350
- return _promiseResolve(_Promise, csrVerify.verifyCsrSignature(parsed)).then(function (ok) {
352
+ return intrinsic.chain(_promiseResolve(_Promise, csrVerify.verifyCsrSignature(parsed)), function (ok) {
351
353
  return guard.verdict.of({
352
354
  valid: ok === true,
353
355
  verified: ok === true,
package/lib/est.js CHANGED
@@ -281,8 +281,14 @@ function _matchDelimAt(text, ls, boundary) {
281
281
  if (_charCodeAt(text, q) === 0x0d && q + 1 < n && _charCodeAt(text, q + 1) === 0x0a) return { end: q + 2, close: close };
282
282
  return null;
283
283
  }
284
+ /** @internal The delimiter count is bounded before the list is built, since one mark is appended per
285
+ * delimiter found anywhere in the body and the scan does not stop at the terminal one. The bound is
286
+ * the part cap plus that terminal delimiter, and it is the same `C.LIMITS` row the MIME reader
287
+ * counts parts against, so the two doors hold a multipart body to one number. */
284
288
  function _findMultipartMarks(text, boundary) {
285
289
  var marks = [], n = text.length, searchStart = 0, p;
290
+ var counted = guard.limits.counter(constants.LIMITS.MIME_MAX_PARTS, E, "est/bad-multipart",
291
+ "the multipart body part");
286
292
  while (searchStart <= n) {
287
293
  var found = null, foundAt = -1;
288
294
  for (p = searchStart; p <= n; p++) {
@@ -293,7 +299,15 @@ function _findMultipartMarks(text, boundary) {
293
299
  if (lsN !== -1) { var r1 = _matchDelimAt(text, lsN, boundary); if (r1) { found = r1; foundAt = p; break; } }
294
300
  }
295
301
  if (!found) break;
302
+ /** @internal The closing delimiter opens no part, so it is not charged and the cap counts the
303
+ * parts an operator is told it counts. */
304
+ if (!found.close) counted.tick();
296
305
  guard.list.append(marks, { at: foundAt, bodyStart: found.end, close: found.close });
306
+ /** @internal RFC 2046 sec. 5.1.1 discards everything after the closing delimiter, and the caller
307
+ * reads no mark past the first one that closes, so the scan stops there. Running on charged the
308
+ * epilogue against the part budget: a body at the cap was refused for text it never reads, while
309
+ * the same body without that epilogue was accepted. */
310
+ if (found.close) break;
297
311
  searchStart = found.end === foundAt ? found.end + 1 : found.end;
298
312
  }
299
313
  return marks;
@@ -314,18 +328,29 @@ function _stripOneTrailingEol(seg, end) {
314
328
  }
315
329
  return end;
316
330
  }
317
- function _unfoldHeaders(raw) {
318
- var out = "", last = 0, i = 0, n = raw.length;
331
+ /** @internal Every line break in the part's header area is charged as the scan passes it, before the
332
+ * unfolded text is split, so the refusal costs the cap rather than the input. Counting the split
333
+ * lines afterwards instead read 2,000,000 one-line headers in 425 ms and 172 MiB of resident memory
334
+ * before refusing, and never reached two on 1,500,000 continuation lines, which fold into one line. */
335
+ function _unfoldHeaders(raw, counted) {
336
+ var out = "", last = 0, i = 0, n = raw.length, afterLastBreak = 0;
319
337
  while (i < n) {
320
338
  var nlLen = 0;
321
339
  if (_charCodeAt(raw, i) === 0x0d && i + 1 < n && _charCodeAt(raw, i + 1) === 0x0a) nlLen = 2;
322
340
  else if (_charCodeAt(raw, i) === 0x0a) nlLen = 1;
323
- if (nlLen > 0) {
324
- var after = i + nlLen;
325
- if (after < n && (_charCodeAt(raw, after) === 0x20 || _charCodeAt(raw, after) === 0x09)) { out += _strSlice(raw, last, i); last = after; i = after; continue; }
326
- }
327
- i += 1;
341
+ if (nlLen === 0) { i += 1; continue; }
342
+ /** @internal The scan steps PAST the whole break rather than one byte into it, so a CRLF is one
343
+ * line and not two: stepping one byte left the LF to be read as a second break on the next pass,
344
+ * which charged every CRLF twice and halved the cap a part was actually held to. */
345
+ counted.tick();
346
+ var after = i + nlLen;
347
+ afterLastBreak = after;
348
+ if (after < n && (_charCodeAt(raw, after) === 0x20 || _charCodeAt(raw, after) === 0x09)) { out += _strSlice(raw, last, i); last = after; i = after; continue; }
349
+ i = after;
328
350
  }
351
+ /** @internal The run after the last break is a line as well, and a header area that ends at a break
352
+ * has no run after it, so the charge is the LINE count the cap is stated in and not one less. */
353
+ if (afterLastBreak < n) counted.tick();
329
354
  return out + _strSlice(raw, last, n);
330
355
  }
331
356
  function _splitCrlf(s) {
@@ -359,7 +384,9 @@ function splitMultipartMixed(body, contentType) {
359
384
  var rawHeaders = _strSlice(seg, 0, sep);
360
385
  var partBody = _strSlice(seg, _stripTwoLeadingEol(seg, sep), _stripOneTrailingEol(seg, seg.length));
361
386
  var headers = intrinsic.create(null);
362
- var lines = _splitCrlf(_unfoldHeaders(rawHeaders));
387
+ var countedHeaders = guard.limits.counter(constants.LIMITS.MIME_MAX_HEADERS, E, "est/bad-multipart",
388
+ "the multipart part header line");
389
+ var lines = _splitCrlf(_unfoldHeaders(rawHeaders, countedHeaders));
363
390
  for (var li = 0; li < lines.length; li++) {
364
391
  var line = lines[li];
365
392
  /** @internal A part's header name and value are cut, trimmed and folded through the captured
@@ -367,7 +394,20 @@ function splitMultipartMixed(body, contentType) {
367
394
  * its body goes to: measured, a part stating `application/pkcs8` was read as
368
395
  * `application/pkcs7-mime` with a `smime-type` of `certs-only`. */
369
396
  var col = _estIndexOf(line, ":");
370
- if (col > 0) headers[_toLowerCase(_estTrim(_strSlice(line, 0, col)))] = _estTrim(_strSlice(line, col + 1));
397
+ if (col <= 0) continue;
398
+ var fname = _toLowerCase(_estTrim(_strSlice(line, 0, col)));
399
+ /** @internal A repeat is refused rather than overwritten. Storing each value in turn left the
400
+ * LAST line deciding the field, and the field this decides is the part's own content type,
401
+ * which selects the reader its body goes to: measured, the same two Content-Type lines swapped
402
+ * reported `application/pkcs7-mime; smime-type=certs-only` one way and `application/pkcs8` the
403
+ * other, which is which part is the private key and which is the certificate. RFC 5322 sec. 3.6
404
+ * gives a singleton field one occurrence, and taking the first is as conforming as taking the
405
+ * last, so there is no value here to resolve to. */
406
+ if (intrinsic.hasOwn(headers, fname)) {
407
+ throw E("est/bad-multipart", "a multipart part declares the " + guard.text.showValue(fname) +
408
+ " header field more than once, so the value it resolves to is ambiguous (RFC 5322 sec. 3.6)");
409
+ }
410
+ headers[fname] = _estTrim(_strSlice(line, col + 1));
371
411
  }
372
412
  var partCt = headers["content-type"] || "";
373
413
  if (_ciStartsWith(partCt, "MULTIPART/")) throw E("est/bad-multipart", "a nested multipart part is not permitted");
@@ -471,36 +511,58 @@ function _recipientMatches(recipientInfos, opts) {
471
511
  * replacement decides which segments a parameter is then looked for in, and one of those parameters
472
512
  * is the multipart boundary a response body is divided on: measured, a `boundary=wire` header
473
513
  * yielded `forged`. */
514
+ /** @internal The first segment is the media type rather than a parameter, so it is not charged, and
515
+ * the charge comes BEFORE the segment is stored, so a refusal costs the cap and not the input: with
516
+ * no cap here a 3.8 MiB Content-Type carrying 1,000,000 parameters was accepted in 194 ms and 100 MiB
517
+ * of resident memory, and a part's media type splits the same value a second time. The bound is the
518
+ * `C.LIMITS` row the MIME reader counts parameters against, so both doors hold one media type to one
519
+ * number. */
520
+ function _pushCtSegment(segs, seg, counted) {
521
+ if (segs.length > 0) counted.tick();
522
+ intrinsic.append(segs, seg);
523
+ }
524
+
474
525
  function _splitContentTypeParams(s) {
475
526
  var segs = [], cur = "", inQuote = false;
527
+ var counted = guard.limits.counter(constants.LIMITS.MIME_MAX_PARAMS, E, "est/bad-multipart",
528
+ "the Content-Type parameter");
476
529
  for (var i = 0; i < s.length; i++) {
477
530
  var ch = _estCharAt(s, i);
478
531
  if (ch === '"') { inQuote = !inQuote; cur += ch; }
479
- else if (ch === ";" && !inQuote) { intrinsic.append(segs, cur); cur = ""; }
532
+ else if (ch === ";" && !inQuote) { _pushCtSegment(segs, cur, counted); cur = ""; }
480
533
  else cur += ch;
481
534
  }
482
- intrinsic.append(segs, cur);
535
+ _pushCtSegment(segs, cur, counted);
483
536
  return segs;
484
537
  }
485
538
 
486
539
  function _ctParam(contentType, name) {
487
540
  var segs = _splitContentTypeParams(_stringOf(contentType || ""));
488
- var value = null, count = 0;
541
+ var value = null, plain = 0, starred = 0;
489
542
  /** @internal Each parameter is read through the captured operations. The name decides which
490
543
  * parameter this is, and the value it returns is the one a caller acts on: for the RFC 7030
491
544
  * multipart boundary, it is where a response body is split. */
492
545
  for (var i = 1; i < segs.length; i++) {
493
546
  var seg = segs[i];
494
547
  var eq = _estIndexOf(seg, "=");
495
- if (eq === -1) continue;
496
- if (_toLowerCase(_estTrim(_strSlice(seg, 0, eq))) !== name) continue;
497
- count++;
498
- if (count > 1) continue;
548
+ /** @internal An occurrence counts whether or not it carries a value, which is why a bare
549
+ * attribute is counted: skipping it let `boundary; boundary="b"` through while
550
+ * `boundary="a"; boundary="b"` was refused. The plain and the starred RFC 2231 spellings are
551
+ * counted apart, because what has to be refused is two readings carrying DIFFERENT values.
552
+ * `boundary*=us-ascii''DECOY; boundary=REAL` is read here as REAL and by an RFC 2231 reader as
553
+ * DECOY, while continuation segments alone leave this reader no boundary at all, which the
554
+ * caller already refuses (RFC 2231 sec. 3, sec. 4, sec. 7). */
555
+ var attr = _toLowerCase(_estTrim(eq === -1 ? seg : _strSlice(seg, 0, eq)));
556
+ var star = _estIndexOf(attr, "*");
557
+ if ((star > 0 ? _strSlice(attr, 0, star) : attr) !== name) continue;
558
+ if (star > 0) { starred++; continue; }
559
+ plain++;
560
+ if (eq === -1 || plain > 1) continue;
499
561
  var val = _estTrim(_strSlice(seg, eq + 1));
500
562
  if (val.length >= 2 && _estCharAt(val, 0) === '"' && _estCharAt(val, val.length - 1) === '"') val = _strSlice(val, 1, -1);
501
563
  value = val;
502
564
  }
503
- return { value: value, duplicated: count > 1 };
565
+ return { value: value, duplicated: plain > 1 || (plain > 0 && starred > 0) };
504
566
  }
505
567
 
506
568
  var _MT_FIRST = pkix.charTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
@@ -1451,7 +1513,7 @@ function _fullcmcResult(res, opts, wanted, sent) {
1451
1513
  bound: { transactionId: false, senderNonce: false, dataReturn: false, bodyPartIDs: false },
1452
1514
  boundToRequest: false });
1453
1515
  }
1454
- return cmcVerify.verify(der, sent).then(function (verdict) {
1516
+ return intrinsic.chain(cmcVerify.verify(der, sent), function (verdict) {
1455
1517
  if (verdict.outcome !== "issued") return verdict;
1456
1518
  if (!wanted.length) { verdict.issuedCertificates = []; return verdict; }
1457
1519
  var issuedCerts = _correlateIssued(verdict.certificates, wanted, "CMC-response");
@@ -1462,12 +1524,12 @@ function _fullcmcResult(res, opts, wanted, sent) {
1462
1524
  }
1463
1525
 
1464
1526
  function _tryDecodeCmcFault(res, sent) {
1465
- return _promiseResolve(_Promise, undefined).then(function () {
1527
+ return intrinsic.chain(intrinsic.chain(_promiseResolve(_Promise, undefined), function () {
1466
1528
  var pt = _partMediaType(_ciHeader(res.headers, "content-type"));
1467
1529
  if (pt.media !== "application/pkcs7-mime") return null;
1468
1530
  if (pt.ambiguous) return null;
1469
1531
  if (String(pt.smimeType || "").toLowerCase() !== "cmc-response") return null;
1470
- return cmcVerify.verify(transferDecode(res.body), sent).then(function (verdict) {
1532
+ return intrinsic.chain(cmcVerify.verify(transferDecode(res.body), sent), function (verdict) {
1471
1533
  if (verdict.outcome !== "rejected") return null;
1472
1534
  var e = E("est/cmc-failed",
1473
1535
  "the EST server rejected the Full PKI Request: " + verdict.outcome +
@@ -1476,7 +1538,7 @@ function _tryDecodeCmcFault(res, sent) {
1476
1538
  e.httpStatus = res.status;
1477
1539
  return e;
1478
1540
  });
1479
- }).then(null, function () { return null; });
1541
+ }), null, function () { return null; });
1480
1542
  }
1481
1543
 
1482
1544
  function _isTypedError(e) {
@@ -1777,6 +1839,16 @@ function _ciHeader(headers, name) {
1777
1839
  "the response carries more than one " + lname + " header field, so what the body is cannot be told " +
1778
1840
  "from it (RFC 9110 sec. 5.1: field names are case-insensitive)");
1779
1841
  }
1842
+ /** @internal One key can still hold SEVERAL values, which is how `node:http` reports a field that
1843
+ * arrived more than once, and these three fields each carry one value. Counting keys alone left a
1844
+ * two-element array to be read as one string: measured, the two orders of the same pair gave
1845
+ * `est/bad-content-type` one way and an accepted classification the other, because the joined text
1846
+ * parsed as a media type in one order and not in the other. */
1847
+ if (intrinsic.isArray(found) && found.length > 1) {
1848
+ throw E("est/bad-content-type",
1849
+ "the response carries " + found.length + " values for " + lname + ", so what the body is cannot be " +
1850
+ "told from it (RFC 9110 sec. 5.1)");
1851
+ }
1780
1852
  return n === 0 ? null : found;
1781
1853
  }
1782
1854
 
@@ -31,6 +31,7 @@ var _mapSet = intrinsic.mapSet;
31
31
  var _mapHas = intrinsic.mapHas;
32
32
  var _Set = intrinsic.Set;
33
33
  var _Map = intrinsic.Map;
34
+ var _ObjectProto = intrinsic.ObjectProto;
34
35
  var _charCodeAt = intrinsic.uncurry(String.prototype.charCodeAt);
35
36
  var _strSplit = intrinsic.uncurry(String.prototype.split);
36
37
  var _fnToString = intrinsic.uncurry(Function.prototype.toString);
@@ -92,6 +93,54 @@ function assertKnownKeys(obj, known, E, code, message) {
92
93
  });
93
94
  }
94
95
 
96
+ /**
97
+ * @internal
98
+ * Whether `k` is `v`'s function machinery, owned or reached from its prototype chain. A check that
99
+ * walks the chain asks this; a reader that copies one LEVEL at a time asks `isOwnMachineryName`.
100
+ *
101
+ * Which of the two a given name is depends on the function and the engine. A STRICT function, which
102
+ * is every function in an ES module or a `"use strict"` file, owns neither `arguments` nor `caller`,
103
+ * and `Function.prototype` carries both as the restricted accessors, so a chain walk finds them
104
+ * there. On the Node 24.21.0 floor a non-strict function owns both outright. Measured before this
105
+ * covered the inherited side: a plain strict callable descriptor was refused for `arguments` by
106
+ * `pki.cms.sign` and `pki.cms.countersign`, while the minted floor shape passed, which is the shape
107
+ * the vectors were written on.
108
+ */
109
+ // @enforced-by behavioral -- the vectors that drive a PLAIN callable and a floor-shaped one through
110
+ // @enforced-by behavioral -- each door are the guard; the name set has no rename-proof code shape
111
+ function isMachineryName(v, k) {
112
+ if (v === null || v === undefined) return false;
113
+ if (isOwnMachineryName(v, k)) return true;
114
+ if (typeof v !== "function" || !_CALLABLE_MACHINERY.has(k)) return false;
115
+ /** @internal Not owned, so `readableNames` reached it through the prototype chain. A caller's own
116
+ * property of that name is theirs and stays refused by name, which `isOwnMachineryName` above
117
+ * decides; making one takes `defineProperty`, since assigning `fn.arguments` reaches the
118
+ * restricted setter and throws. */
119
+ return _getOwnPropertyDescriptor(v, k) === undefined;
120
+ }
121
+
122
+ /**
123
+ * @internal
124
+ * `assertKnownKeys` for a bag a verb documents as acceptably CALLABLE, where the names a function
125
+ * carries by construction are not fields the caller named. The exemption belongs at the door that
126
+ * refuses, not only at a reader that copies: `pki.smime.sign` snapshots a descriptor before handing
127
+ * it on, so excluding the machinery there left a caller going straight to `pki.cms.sign` with the
128
+ * same callable refused for `length`.
129
+ *
130
+ * It is not `assertKnownKeys`'s own rule, because a bare function handed in where an OPTIONS BAG
131
+ * belongs is refused for exactly those names, which is a contract of its own.
132
+ */
133
+ // @enforced-by behavioral -- the vectors that drive a callable descriptor through each door are the
134
+ // @enforced-by behavioral -- guard; the exemption has no rename-proof code shape
135
+ function assertKnownDescriptorKeys(obj, known, E, code, message) {
136
+ _refuseUnenumerable(obj, E, code, "the options");
137
+ var describe = typeof message === "function" ? message : function (k) { return message + _stringify(k); };
138
+ _forEach(_readableNames(obj), function (k) {
139
+ if (_hasOwn(known, k) || isMachineryName(obj, k)) return;
140
+ throw E(code, describe(typeof k === "symbol" ? _String(k) : k));
141
+ });
142
+ }
143
+
95
144
  // @enforced-by behavioral -- reading a property twice has no rename-proof code shape; the RED
96
145
  // @enforced-by behavioral -- vectors that drive an accessor-backed option are the guard
97
146
  function snapshotOptions(obj, known) {
@@ -320,6 +369,64 @@ function _kindMembers(v) {
320
369
  return [];
321
370
  }
322
371
 
372
+ /** @internal The names a callable carries before a caller assigns a field, asked of the engine
373
+ * rather than listed here: a function's own names, plus `Function.prototype`'s, which is where the
374
+ * restricted `arguments` and `caller` accessors live. Which of them a given function makes OWN
375
+ * depends on the engine. Measured: on Node 24.21.0, the version `engines.node` floors at, every
376
+ * non-strict function carries own `arguments` and `caller`; on 26.9.0 none does, for a declaration
377
+ * and for `new Function` alike. A list written against one of those refuses a legitimate callable
378
+ * descriptor on the other, and a caller's non-strict CommonJS module is where the shape comes
379
+ * from. */
380
+ var _CALLABLE_MACHINERY = (function () {
381
+ var names = _create(null), symbols = new _Set();
382
+ /** @internal One pristine probe per callable KIND, because the kinds do not share a prototype: an
383
+ * async function, a generator and an async generator each sit on their own intrinsic prototype
384
+ * carrying `Symbol.toStringTag`, which an ordinary function's chain never reaches. Measured
385
+ * before the probes were per-kind: `pki.cms.sign` refused all three for
386
+ * `Symbol(Symbol.toStringTag)` while an ordinary function passed. Symbol keys are collected too,
387
+ * since that is the name that was missing. */
388
+ /** @internal Every kind, not the subset that happens to suffice: the four intrinsic prototypes
389
+ * carry the SAME well-known `Symbol.toStringTag` under different values, so dropping the async
390
+ * generator still passes today and would not if a kind ever carried a key of its own. */
391
+ var probes = [function () {}, function* () {}, async function () {}, async function* () {}, class {}];
392
+ _forEach(probes, function (p) {
393
+ for (var o = p; o !== null && o !== _ObjectProto; o = _getPrototypeOf(o)) {
394
+ _forEach(_ownKeys(o), function (k) {
395
+ if (typeof k === "symbol") _setAdd(symbols, k); else names[k] = true;
396
+ });
397
+ }
398
+ });
399
+ return {
400
+ has: function (k) { return typeof k === "symbol" ? _setHas(symbols, k) : names[k] === true; },
401
+ };
402
+ })();
403
+
404
+ /**
405
+ * @internal
406
+ * Whether `k` is part of `v`'s own machinery rather than a field a caller named: a RegExp's
407
+ * `lastIndex`, the non-thenable mask's `then`, and a callable's own function machinery, which is
408
+ * non-enumerable where a field a caller assigned is enumerable.
409
+ *
410
+ * A reader that COPIES a descriptor's own keys asks this, so it copies what the caller wrote and
411
+ * nothing else. The name checks do not: a bare function handed in where an options bag belongs is
412
+ * refused for the names it carries, which is a contract of its own, so the callable rule lives here
413
+ * rather than in `_isOwnStructuralName`.
414
+ */
415
+ // @enforced-by behavioral -- a per-kind name set has no rename-proof code shape; the vectors
416
+ // @enforced-by behavioral -- that drive a callable descriptor through a copying door are the guard
417
+ function isOwnMachineryName(v, k) {
418
+ if (v === null || v === undefined) return false;
419
+ var d = _getOwnPropertyDescriptor(v, k);
420
+ /** @internal A DATA property, because that is what the machinery is: measured on both Node
421
+ * 24.21.0 and 26.9.0, every one of these names a function owns is a non-enumerable data
422
+ * property. An accessor a caller installed under one of those names is therefore theirs, and it
423
+ * reaches the unknown-field refusal by its name rather than being exempted and ignored. Asking
424
+ * the descriptor rather than reading the property leaves the getter uninvoked either way. */
425
+ if (typeof v === "function" && !!d && d.enumerable === false && _hasOwn(d, "value") &&
426
+ _CALLABLE_MACHINERY.has(k)) return true;
427
+ return _isOwnStructuralName(v, k, d, false);
428
+ }
429
+
323
430
  function _isOwnStructuralName(v, k, d, inherited) {
324
431
  if (util.types.isRegExp(v) && k === "lastIndex") return true;
325
432
  /** @internal The non-thenable mask `guard.verdict.shield` leaves on a value the toolkit hands back:
@@ -680,6 +787,9 @@ module.exports = intrinsic.freeze({
680
787
  snapshotOptions: snapshotOptions,
681
788
  snapshotPresentOptions: snapshotPresentOptions,
682
789
  optionNames: optionNames,
790
+ isOwnMachineryName: isOwnMachineryName,
791
+ isMachineryName: isMachineryName,
792
+ assertKnownDescriptorKeys: assertKnownDescriptorKeys,
683
793
  ownOptions: ownOptions,
684
794
  pickSettings: pickSettings,
685
795
  readableNames: readableNames,
package/lib/guard-name.js CHANGED
@@ -65,7 +65,51 @@ function assertPrintableIa5(buf, E, code, label) {
65
65
  }
66
66
 
67
67
  function _isSpace(c) { return c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d; }
68
- function _canonAttrValue(v, E, code, label) {
68
+ /** @internal The attribute types whose EQUALITY rule is not case-ignoring, resolved once to their
69
+ * object identifiers. An equality rule belongs to the attribute, and folding case on one whose rule
70
+ * is case-exact makes two distinct names compare as one name: measured through `pki.x509.parseDn`,
71
+ * `challengePassword=Secret` and `challengePassword=secret` encode differently and compared EQUAL,
72
+ * as did `localKeyId=AB` against `ab` and a `role` URI against its lowercase form.
73
+ *
74
+ * Only the exceptions are listed. caseIgnoreMatch under the RFC 4518 preparation is the correct
75
+ * rule for every attribute RFC 5280 sec. 4.1.2.4 requires support for, so a conforming PKIX name
76
+ * compares as it did and an attribute the registry does not name keeps that default rather than
77
+ * being given a rule nothing states.
78
+ *
79
+ * The identifiers are resolved rather than written, since a dotted-decimal literal is never the
80
+ * identity a module carries. */
81
+ /** @internal RFC 2985 sec. 5.4.1 gives challengePassword caseExactMatch, sec. 5.5.2 gives localKeyId
82
+ * octetStringMatch, and RFC 5755 sec. 4.4.5 makes a role a URI. */
83
+ var _CASE_EXACT_NAMES = ["challengePassword", "localKeyId", "role"];
84
+ var _caseExactOids = null;
85
+ function _isCaseExactType(type) {
86
+ if (typeof type !== "string") return false;
87
+ if (_caseExactOids === null) {
88
+ var oid = require("./oid"); // allow:inline-require -- circular load: oid -> guard-all -> this module
89
+ _caseExactOids = _create(null);
90
+ for (var i = 0; i < _CASE_EXACT_NAMES.length; i++) {
91
+ var o = oid.byName(_CASE_EXACT_NAMES[i]);
92
+ if (typeof o === "string") _caseExactOids[o] = true;
93
+ }
94
+ }
95
+ return _caseExactOids[type] === true;
96
+ }
97
+
98
+ /** @internal The registry's own reading of a dotted-decimal object identifier, taken from there
99
+ * rather than re-derived: `numericoid` is RFC 4512's production and one definition of it decides
100
+ * both what the registry resolves and what opens a distinguished name. */
101
+ var _isDottedOid = null;
102
+ function _opensAsNumericOid(run) {
103
+ if (_isDottedOid === null) {
104
+ var oid = require("./oid"); // allow:inline-require -- circular load: oid -> guard-all -> this module
105
+ _isDottedOid = oid.isDottedDecimal;
106
+ }
107
+ return _isDottedOid(run) === true;
108
+ }
109
+
110
+ /** @internal The RFC 4518 insignificant-space preparation, which every equality rule here applies.
111
+ * The case fold is the caller's to add, because that step is the one the attribute's rule decides. */
112
+ function _prepAttrValue(v, E, code, label) {
69
113
  if (typeof v !== "string") return v;
70
114
  assertNoControlBytes(v, E, code, label);
71
115
  var out = "", lastWasSpace = true;
@@ -75,8 +119,9 @@ function _canonAttrValue(v, E, code, label) {
75
119
  lastWasSpace = false;
76
120
  out += _charAt(v, i);
77
121
  }
78
- return _toLowerCase(out);
122
+ return out;
79
123
  }
124
+
80
125
  /** @internal Both lengths are NARROWED where they are taken, and every test and walk below asks the
81
126
  * local. `Array.isArray` is true for a Proxy whose target is an array and a `get` trap answers `length`
82
127
  * afresh on each read, so the equality test and the walk that follows it could see two different counts:
@@ -114,14 +159,25 @@ function rdnEqual(a, b, E, code, label) {
114
159
  * comparison's one unacceptable answer, since it is what chains a certificate to its issuer. */
115
160
  var ea = a[i];
116
161
  var typeA = ea == null ? undefined : ea.type;
117
- var valueA = _canonAttrValue(ea == null ? undefined : ea.value, E, code, label);
162
+ /** @internal Prepared once without the fold, and folded once, so the attribute's own rule picks
163
+ * between them without reading the element again. */
164
+ var prepA = _prepAttrValue(ea == null ? undefined : ea.value, E, code, label);
165
+ var foldedA = typeof prepA === "string" ? _toLowerCase(prepA) : prepA;
166
+ var exactA = _isCaseExactType(typeA);
118
167
  var found = false;
119
168
  for (var j = 0; j < n.b; j++) {
120
169
  if (used[j]) continue;
121
170
  var eb = b[j];
122
171
  var typeB = eb == null ? undefined : eb.type;
123
172
  if (typeA !== typeB) continue;
124
- if (valueA === _canonAttrValue(eb == null ? undefined : eb.value, E, code, label)) {
173
+ var prepB = _prepAttrValue(eb == null ? undefined : eb.value, E, code, label);
174
+ /** @internal Either side naming a case-exact attribute decides it. The types already matched,
175
+ * so the two can disagree only through an object answering differently per read, and the
176
+ * exact comparison is the one that cannot make two distinct names equal. */
177
+ var same = exactA || _isCaseExactType(typeB)
178
+ ? prepA === prepB
179
+ : foldedA === (typeof prepB === "string" ? _toLowerCase(prepB) : prepB);
180
+ if (same) {
125
181
  used[j] = true; found = true; break;
126
182
  }
127
183
  }
@@ -213,6 +269,36 @@ function isAttributeDescriptor(name) {
213
269
  return true;
214
270
  }
215
271
 
272
+ /**
273
+ * @internal
274
+ * Whether a string opens with the prefix RFC 4514 sec. 3 requires of a distinguished name: an
275
+ * attribute type, a descriptor or a dotted-decimal OID, immediately followed by `=`. It answers one
276
+ * question, whether a caller who passed a bare string meant a distinguished name rather than the
277
+ * single value the shorthand reads it as, and it is deliberately narrower than parsing: a value that
278
+ * CONTAINS an `=` further along, `Example CA (v=2)`, opens with no attribute type and is not
279
+ * a distinguished name.
280
+ *
281
+ * `attributeType` is `descr / numericoid`, and a `numericoid` carries at least one dot, so a numeric
282
+ * run without one is not an attribute type: `1=1` is a value no reader reads as a distinguished
283
+ * name, and refusing it would name a remedy that does not apply. The leading whitespace sec. 3
284
+ * admits nowhere is the same case from the other side: ` CN=Example` opens with no attribute type
285
+ * and is the value it says it is.
286
+ */
287
+ // @enforced-by behavioral -- an opening-prefix test has no rename-proof shape distinct from
288
+ function opensAsDnString(str) {
289
+ if (typeof str !== "string" || str.length === 0) return false;
290
+ var i = 0, n = str.length, first = _charCodeAt(str, 0);
291
+ if (_isDigit(first)) {
292
+ while (i < n && (_isDigit(_charCodeAt(str, i)) || _charCodeAt(str, i) === 0x2e)) i++;
293
+ if (!_opensAsNumericOid(_strSlice(str, 0, i))) return false;
294
+ } else if (_isAttrTypeStart(first)) {
295
+ while (i < n && _isAttrTypeChar(_charCodeAt(str, i))) i++;
296
+ } else {
297
+ return false;
298
+ }
299
+ return i > 0 && i < n && _charCodeAt(str, i) === 0x3d;
300
+ }
301
+
216
302
  // @enforced-by behavioral -- the inverse of escapeDnValue has no rename-proof shape distinct
217
303
  function parseDnString(str, E, code, label) {
218
304
  if (typeof str !== "string") throw E(code, label + " must be a string");
@@ -1058,6 +1144,7 @@ module.exports = intrinsic.freeze({
1058
1144
  assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5,
1059
1145
  dnEqual: dnEqual, rdnEqual: rdnEqual, emailEqual: emailEqual, dpnCorresponds: dpnCorresponds,
1060
1146
  escapeControlBytes: escapeControlBytes, escapeDnValue: escapeDnValue, parseDnString: parseDnString,
1147
+ opensAsDnString: opensAsDnString,
1061
1148
  isAttributeDescriptor: isAttributeDescriptor,
1062
1149
  lowerAscii: lowerAscii, uriSchemeIs: uriSchemeIs,
1063
1150
  stripTrailingDot: stripTrailingDot, isFqdnHost: isFqdnHost, splitMailbox: splitMailbox,
package/lib/inspect.js CHANGED
@@ -1531,8 +1531,8 @@ function crmfReport(input) {
1531
1531
  * var key = await pki.key.export(pair.privateKey);
1532
1532
  * var cert = await pki.x509.sign({ subject: "cmp.example", subjectPublicKey: await pki.key.export(pair.publicKey),
1533
1533
  * notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
1534
- * var der = await pki.cmp.build({ header: { sender: { directoryName: "CN=cmp.example" },
1535
- * recipient: { directoryName: "CN=A CA" } },
1534
+ * var der = await pki.cmp.build({ header: { sender: { directoryName: "cmp.example" },
1535
+ * recipient: { directoryName: "A CA" } },
1536
1536
  * body: { ir: { certTemplate: { subject: [{ commonName: "cmp.example" }], publicKey: await pki.key.export(pair.publicKey) } } } },
1537
1537
  * { cert: cert, key: key });
1538
1538
  * pki.inspect.cmp(der).split("\n")[0]; // "CMP Message:"
@@ -1937,10 +1937,10 @@ function tspReport(input) {
1937
1937
  *
1938
1938
  * @example
1939
1939
  * var pair = await pki.key.generate("Ed25519");
1940
- * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "CN=Alice" } },
1940
+ * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "Alice" } },
1941
1941
  * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
1942
1942
  * attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
1943
- * { name: "CN=An AA", publicKey: await pki.key.export(pair.publicKey), key: await pki.key.export(pair.privateKey) });
1943
+ * { name: "An AA", publicKey: await pki.key.export(pair.publicKey), key: await pki.key.export(pair.privateKey) });
1944
1944
  * pki.inspect.attrcert(der).split("\n")[0]; // "Attribute Certificate:"
1945
1945
  */
1946
1946
  function attrCertReport(input) {
package/lib/lint.js CHANGED
@@ -7556,10 +7556,10 @@ function _ingestAttrCert(input) {
7556
7556
  * @example
7557
7557
  * var pair = await pki.key.generate("Ed25519");
7558
7558
  * var key = await pki.key.export(pair.privateKey);
7559
- * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "CN=Alice" } },
7559
+ * var der = await pki.attrcert.sign({ holder: { entityName: { directoryName: "Alice" } },
7560
7560
  * notBeforeTime: new Date("2026-01-01T00:00:00Z"), notAfterTime: new Date("2027-01-01T00:00:00Z"),
7561
7561
  * attributes: { role: { roleName: { uniformResourceIdentifier: "urn:role:admin" } } } },
7562
- * { name: "CN=Example AA", publicKey: await pki.key.export(pair.publicKey), key: key });
7562
+ * { name: "Example AA", publicKey: await pki.key.export(pair.publicKey), key: key });
7563
7563
  * pki.lint.attrcert(der).findings.length; // -> 0 for a conforming attribute certificate
7564
7564
  */
7565
7565
  function attrcert(input, opts) {