@blamejs/pki 0.5.2 → 0.5.4

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/jose.js CHANGED
@@ -566,8 +566,47 @@ async function thumbprint(jwk) {
566
566
  return b64uEncode(digest);
567
567
  }
568
568
 
569
+ /**
570
+ * @primitive pki.jose.sigAlgs
571
+ * @signature pki.jose.sigAlgs() -> Array<{alg,kty,crv,hash,saltLength}>
572
+ * @since 0.5.3
573
+ * @status stable
574
+ * @spec RFC 7518 sec. 3, RFC 8037, RFC 9964
575
+ * @related pki.jose.verify
576
+ *
577
+ * The JWS signature algorithms this toolkit verifies, one row per `alg`, each
578
+ * naming the JWK key type it requires (`kty`, plus the exact `crv` where the
579
+ * curve is fixed), the hash, and the RSASSA-PSS salt length where the algorithm
580
+ * is PSS. Rows describe key material in RFC 7517 / 7518 vocabulary only, so a
581
+ * caller can decide whether a key it holds can sign or verify a given `alg`
582
+ * without a table of its own.
583
+ *
584
+ * MAC algorithms are deliberately absent: `HS*` is not a signature algorithm and
585
+ * listing it beside `RS256` is how the HMAC key-confusion class starts. `none`
586
+ * does not exist here at all.
587
+ *
588
+ * Each call returns a fresh array of fresh rows -- the registry that drives
589
+ * verification is never handed out, so nothing a caller does to the result can
590
+ * widen what a signature check accepts.
591
+ *
592
+ * @example
593
+ * var pss = pki.jose.sigAlgs().filter(function (r) { return r.saltLength; });
594
+ * pss.map(function (r) { return r.alg; }); // -> ["PS256", "PS384", "PS512"]
595
+ */
596
+ function sigAlgs() {
597
+ return Object.keys(SIG_ALGS).map(function (alg) {
598
+ var row = SIG_ALGS[alg];
599
+ var out = { alg: alg, kty: row.kty };
600
+ if (row.crv) out.crv = row.crv;
601
+ if (row.hash) out.hash = row.hash;
602
+ if (row.saltLength) out.saltLength = row.saltLength;
603
+ return out;
604
+ });
605
+ }
606
+
569
607
  module.exports = {
570
608
  base64url: { encode: b64uEncode, decode: b64uDecode },
609
+ sigAlgs: sigAlgs,
571
610
  parseJson: parseJson,
572
611
  verify: verify,
573
612
  sign: sign,
@@ -1127,13 +1127,35 @@ function validateCriticalExtensionStructure(cert) {
1127
1127
  * The value-carrying options (`time`, `maxPathCerts`, `maxPolicyNodes`, the
1128
1128
  * subtree seeds, `userInitialPolicySet`, `requiredEku`) are validated at the
1129
1129
  * entry point -- a mis-shaped value throws `path/bad-input` rather than
1130
- * silently not applying. Returns `{ valid, path,
1131
- * results, workingPublicKey, workingPublicKeyAlgorithm,
1130
+ * silently not applying. Returns `{ valid, revocationChecked, anchorConstraints,
1131
+ * path, results, workingPublicKey, workingPublicKeyAlgorithm,
1132
1132
  * workingPublicKeyParameters, validPolicyTree }` where `results[i].checks`
1133
1133
  * carries a per-check reason code (`path/*`) for every step. Pure and
1134
1134
  * re-entrant -- no input object is mutated. An empty path or a missing anchor
1135
1135
  * throws a typed `PathError`.
1136
1136
  *
1137
+ * `valid` alone cannot say whether revocation was ever established, so
1138
+ * `revocationChecked` answers separately, taking the WEAKEST outcome on the
1139
+ * path: `false` when no `revocationChecker` was supplied, `"determined"` when
1140
+ * every certificate got an explicit good or revoked answer, `"waived"` when
1141
+ * `softFail` turned an undetermined one into a pass, and `"undetermined"` when
1142
+ * one could not be answered at all and the path fails for it. The
1143
+ * per-certificate `revocation` check carries the `status` it was decided on and
1144
+ * marks a waiver, so "checked, good" is distinguishable from "could not check,
1145
+ * and you waived it" -- which a stored verdict is re-read to settle. A checker
1146
+ * that THROWS is a fault in the checker rather than a status it reported, so it
1147
+ * fails the path as `path/revocation-checker-error` carrying the fault whatever
1148
+ * `softFail` says -- `softFail` opts into an undetermined ANSWER, and the
1149
+ * built-in checkers report one as `{ status: "unknown" }` rather than throwing.
1150
+ *
1151
+ * `anchorConstraints` reports what the anchor's own trust metadata decided:
1152
+ * the `checkedPurpose` it was judged under, and whether the `distrustAfter`
1153
+ * date and the `purposes` delegator map each applied. That metadata is keyed BY
1154
+ * key purpose, so an anchor carrying it while `opts.checkPurpose` is absent is a
1155
+ * configuration fault (`path/bad-input`) rather than a constraint that silently
1156
+ * does nothing -- a root distrusted years ago must not quietly validate a
1157
+ * current leaf.
1158
+ *
1137
1159
  * @example
1138
1160
  * var pair = await pki.key.generate("Ed25519");
1139
1161
  * var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
@@ -1204,6 +1226,22 @@ async function validate(path, opts) {
1204
1226
  // then fails the path closed instead of silently skipping the step.
1205
1227
  var requireRevocation = opts.requireRevocation === true;
1206
1228
  var failed = false;
1229
+ // Whether the revocation rule RAN, and whether any certificate's answer had to be waived. The
1230
+ // verdict reports the two separately because they are different claims: a caller re-reading a
1231
+ // stored result needs to tell "every certificate was determined not revoked" from "the step was
1232
+ // skipped" from "the step could not conclude and you had asked for that to pass".
1233
+ var revocationRan = false, revocationWaived = false, revocationUndetermined = false;
1234
+ // Which of the anchor's purpose-scoped constraints actually decided anything, reported so a
1235
+ // verdict can be re-read to tell an anchor that was judged from one that carried nothing to judge.
1236
+ var anchorDistrustApplied = false, anchorPurposeApplied = false;
1237
+ // A trust anchor carrying purpose-scoped metadata, validated with no purpose to select by, is a
1238
+ // configuration fault. The constraint is KEYED by purpose -- there is no way to apply
1239
+ // `distrustAfter.serverAuth` without being told the validation is about serverAuth -- so without
1240
+ // one the caller's stated intent would be discarded silently, and a root distrusted years ago
1241
+ // would validate a current leaf. An anchor carrying no such metadata is unaffected.
1242
+ if (!checkPurpose && opts.trustAnchor && _hasPurposeScopedMetadata(opts.trustAnchor)) {
1243
+ throw E("path/bad-input", "validate: the trust anchor carries purpose-scoped metadata (distrustAfter / purposes), which is keyed by key purpose -- supply opts.checkPurpose to say which purpose this validation is for, or the constraint cannot be applied");
1244
+ }
1207
1245
 
1208
1246
  for (var idx = 0; idx < n; idx++) {
1209
1247
  var i = idx + 1;
@@ -1274,16 +1312,48 @@ async function validate(path, opts) {
1274
1312
  // 6.1.3(a)(3) revocation.
1275
1313
  if (revocationChecker) {
1276
1314
  var issuerCert = idx > 0 ? certs[idx - 1] : null; // the anchor issues cert[1]
1277
- var rv;
1315
+ var rv, rvError = null;
1316
+ // A checker that THROWS -- or whose promise rejects -- is not a checker reporting "unknown".
1317
+ // Laundering the two together made a broken checker indistinguishable from a working one that
1318
+ // could not reach the responder, and under softFail both became a pass. The fault is carried
1319
+ // onto the check so an operator can tell their own bug from a network condition, and it fails
1320
+ // the path whatever softFail says (see the branch below).
1278
1321
  try { rv = await revocationChecker.check(cert, { workingIssuerName: state.workingIssuerName, workingPublicKey: state.workingPublicKey, workingPublicKeyAlgorithm: state.workingPublicKeyAlgorithm, issuerCert: issuerCert }, { time: opts.time, historicalMode: opts.historicalMode === true }); }
1279
- catch (_e) { rv = { status: "unknown" }; }
1322
+ catch (e) { rv = { status: "error" }; rvError = e; }
1280
1323
  // ONLY an explicit "good" is a determined non-revocation; "revoked" fails;
1281
1324
  // every other value ("unknown", an OCSP tryLater/unauthorized, a typo, a
1282
1325
  // missing status) is undetermined and fails closed unless softFail.
1283
- if (rv && rv.status === "good") { checks.push({ name: "revocation", ok: true }); }
1284
- else if (rv && rv.status === "revoked") { checks.push({ name: "revocation", ok: false, code: "path/revoked" }); failed = true; }
1285
- else if (softFail) { checks.push({ name: "revocation", ok: true }); }
1286
- else { checks.push({ name: "revocation", ok: false, code: "path/revocation-undetermined" }); failed = true; }
1326
+ //
1327
+ // The two `ok: true` outcomes are NOT the same claim and no longer the same object. "checked,
1328
+ // and it said good" and "could not check, and you waived it" read identically from a bare
1329
+ // boolean, which is the whole reason a stored verdict cannot be re-read to answer whether
1330
+ // revocation was ever established. Each entry now names the status it was decided on, and a
1331
+ // waiver marks itself.
1332
+ var rvStatus = (rv && typeof rv.status === "string") ? rv.status : "unknown";
1333
+ if (rv && rv.status === "good") { checks.push({ name: "revocation", ok: true, status: "good" }); }
1334
+ else if (rv && rv.status === "revoked") { checks.push({ name: "revocation", ok: false, status: "revoked", code: "path/revoked" }); failed = true; }
1335
+ else if (rvError) {
1336
+ // A checker that threw is a FAULT in the checker, not a status it could not reach, and
1337
+ // softFail is the caller opting into an undetermined ANSWER. The built-in CRL and OCSP
1338
+ // checkers return `{status:"unknown"}` for every unreachable or unverifiable condition and
1339
+ // never throw, so a throw here is the caller's own bug -- waiving it would pass the
1340
+ // certificate with no revocation result at all, which is the outcome softFail is asked for
1341
+ // and this is not.
1342
+ checks.push({ name: "revocation", ok: false, status: "error", code: "path/revocation-checker-error", error: rvError });
1343
+ revocationUndetermined = true;
1344
+ failed = true;
1345
+ }
1346
+ else if (softFail) {
1347
+ // No `error` slot here or below: a fault took the branch above, so anything reaching these
1348
+ // two is a status the checker actually reported.
1349
+ checks.push({ name: "revocation", ok: true, status: rvStatus, waived: true });
1350
+ revocationWaived = true;
1351
+ } else {
1352
+ checks.push({ name: "revocation", ok: false, status: rvStatus, code: "path/revocation-undetermined" });
1353
+ revocationUndetermined = true;
1354
+ failed = true;
1355
+ }
1356
+ revocationRan = true;
1287
1357
  } else if (requireRevocation) {
1288
1358
  // No checker was supplied but the caller demands a revocation determination:
1289
1359
  // the step cannot be performed, so fail closed (never silently skip).
@@ -1357,6 +1427,7 @@ async function validate(path, opts) {
1357
1427
  // before the comparison; an absent (undefined/null) date is no restriction.
1358
1428
  var distrustDate = assertAnchorConstraints(ta, checkPurpose);
1359
1429
  if (distrustDate != null) {
1430
+ anchorDistrustApplied = true;
1360
1431
  // STRICTLY > : a leaf whose notBefore == the distrust date stays trusted
1361
1432
  // (Mozilla certverifier isDistrustedCertificateChain: endEntityNotBefore
1362
1433
  // <= distrustAfterTime -> not distrusted; the end-of-day ...235959Z
@@ -1365,8 +1436,11 @@ async function validate(path, opts) {
1365
1436
  checks.push({ name: "distrustAfter", ok: false, code: "path/distrusted-after" }); failed = true;
1366
1437
  }
1367
1438
  }
1368
- if (checkPurpose && ta.purposes && ta.purposes[checkPurpose] !== true) {
1369
- checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1439
+ if (checkPurpose && ta.purposes) {
1440
+ anchorPurposeApplied = true;
1441
+ if (ta.purposes[checkPurpose] !== true) {
1442
+ checks.push({ name: "purposeTrust", ok: false, code: "path/purpose-not-trusted" }); failed = true;
1443
+ }
1370
1444
  }
1371
1445
  updateWorkingKey(state, cert); // 6.1.5(c),(d) -- key AND algorithm AND parameters
1372
1446
  }
@@ -1404,6 +1478,20 @@ async function validate(path, opts) {
1404
1478
 
1405
1479
  return {
1406
1480
  valid: !failed,
1481
+ // What the revocation rule and the anchor's own trust metadata actually decided. `valid` alone
1482
+ // cannot answer either, and both are questions a stored verdict is re-read to settle: was this
1483
+ // certificate ever established as un-revoked, and was the anchor's distrust date consulted.
1484
+ // The WEAKEST outcome on the path, not the fact that a checker ran: a certificate nobody could
1485
+ // answer for leaves revocation unestablished however many others answered, and deriving the word
1486
+ // from "a checker ran" put that run on the same value as one that established every answer.
1487
+ revocationChecked: !revocationRan ? false
1488
+ : revocationUndetermined ? "undetermined"
1489
+ : revocationWaived ? "waived" : "determined",
1490
+ anchorConstraints: {
1491
+ checkedPurpose: checkPurpose || null,
1492
+ distrustAfterApplied: anchorDistrustApplied,
1493
+ purposeTrustApplied: anchorPurposeApplied,
1494
+ },
1407
1495
  path: certs,
1408
1496
  results: state.results,
1409
1497
  workingPublicKey: state.workingPublicKey,
@@ -1446,24 +1534,10 @@ var OID_AUTHORITY_KEY_ID = oid.byName("authorityKeyIdentifier");
1446
1534
  var OID_CRL_NUMBER = oid.byName("cRLNumber");
1447
1535
  var OID_FRESHEST_CRL = oid.byName("freshestCRL");
1448
1536
 
1449
- // IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0] OPTIONAL,
1450
- // onlyContainsUserCerts [1] DEFAULT FALSE, onlyContainsCACerts [2] DEFAULT FALSE,
1451
- // onlySomeReasons [3] ReasonFlags OPTIONAL, indirectCRL [4] DEFAULT FALSE,
1452
- // onlyContainsAttributeCerts [5] DEFAULT FALSE } (RFC 5280 sec. 5.2.5). Declared
1453
- // through the engine so the trailing-field grammar (strictly-ascending tags, each
1454
- // at most once) and the DER BOOLEAN value rules are the shared enforcement, not a
1455
- // hand-walk; a present DEFAULT-FALSE flag encoding FALSE is the omitted default
1456
- // (X.690 sec. 11.5) and rejects at the leaf-value level below.
1457
- var IDP_SCHEMA = schema.seq([
1458
- schema.trailing([
1459
- { tag: 0, name: "distributionPoint", schema: schema.any() },
1460
- { tag: 1, name: "onlyContainsUserCerts", schema: schema.implicitBoolean(1) },
1461
- { tag: 2, name: "onlyContainsCACerts", schema: schema.implicitBoolean(2) },
1462
- { tag: 3, name: "onlySomeReasons", schema: schema.implicitBitString(3) },
1463
- { tag: 4, name: "indirectCRL", schema: schema.implicitBoolean(4) },
1464
- { tag: 5, name: "onlyContainsAttributeCerts", schema: schema.implicitBoolean(5) },
1465
- ], { minTag: 0, maxTag: 5, unexpectedCode: "path/bad-idp", orderCode: "path/bad-idp" }),
1466
- ], { assert: "sequence", code: "path/bad-idp", what: "IssuingDistributionPoint" });
1537
+ // The RFC 5280 sec. 5.2.5 IssuingDistributionPoint grammar, shared with the CRL
1538
+ // verbs (pkix.issuingDistributionPoint) so the scope this validator reads and the
1539
+ // scope pki.crl.isRevoked refuses to answer past are read by the same rules.
1540
+ var IDP_SCHEMA = pkix.issuingDistributionPoint("path/bad-idp");
1467
1541
 
1468
1542
  // RFC 5280 sec. 6.3.2(a): the legal members of reasons_mask are exactly the eight
1469
1543
  // named ReasonFlags bits, 1..8 (`unused` bit 0 is not a reason, and `unspecified`
@@ -2437,6 +2511,17 @@ function coerceCert(input) {
2437
2511
  // exposed for the same reason: a caller that may never reach the walk -- pki.cms.verify when no
2438
2512
  // signer verified -- has to be able to reject a malformed anchor at ITS entry point, through this
2439
2513
  // same definition, so configuration validity never depends on the message.
2514
+ // Does this anchor carry trust metadata that only a named key purpose can unlock? A non-empty
2515
+ // `distrustAfter` or `purposes` map is such metadata -- both are indexed BY purpose, so both are
2516
+ // inert without one. An EMPTY map states no constraint and is not a reason to refuse.
2517
+ function _hasPurposeScopedMetadata(ta) {
2518
+ if (!ta || typeof ta !== "object") return false;
2519
+ return ["distrustAfter", "purposes"].some(function (k) {
2520
+ var m = ta[k];
2521
+ return !!m && typeof m === "object" && Object.keys(m).length > 0;
2522
+ });
2523
+ }
2524
+
2440
2525
  function assertAnchorConstraints(ta, checkPurpose) {
2441
2526
  var d = (checkPurpose && ta && ta.distrustAfter) ? ta.distrustAfter[checkPurpose] : null;
2442
2527
  if (d == null) return null;
@@ -669,6 +669,66 @@ function distributionPointName(ns, node, code) {
669
669
  throw ns.E(code, "DistributionPointName must be fullName [0] or nameRelativeToCRLIssuer [1] (RFC 5280 sec. 4.2.1.13)");
670
670
  }
671
671
 
672
+ // A certificate's keyUsage as the named booleans the shared sec. 4.2.1 decoder produces, or null
673
+ // when the certificate carries no keyUsage extension -- which places no restriction (sec. 4.2.1.3),
674
+ // a distinct answer from "carries one that permits nothing".
675
+ //
676
+ // Declared here because "may this certificate do X" is asked at five boundaries and the answer has
677
+ // to be the same at all of them. keyUsage is a NamedBitList: DER drops its trailing zero bits
678
+ // (X.690 sec. 11.2.2) and sec. 4.2.1.3 requires at least one bit set. A boundary that reads the
679
+ // bits with a plain BIT STRING read applies neither, so the same certificate is authorized there
680
+ // and rejected as malformed by the issuing side and the path validator -- one extension with two
681
+ // readings, and the permissive one deciding.
682
+ //
683
+ // `E(code, message, cause)` is the caller's typed error factory, so each boundary keeps its own
684
+ // domain while the RULE stays single-homed.
685
+ var _KU_DECODER = new WeakMap(); // one decoder table per namespace, built on first use
686
+ function keyUsageOf(ns, cert, E, code, label) {
687
+ var exts = (cert && cert.extensions) || [];
688
+ var want = ns.oid.byName("keyUsage");
689
+ for (var i = 0; i < exts.length; i++) {
690
+ if (exts[i].oid !== want || exts[i].value == null) continue;
691
+ var dec = _KU_DECODER.get(ns);
692
+ if (!dec) { dec = certExtensionDecoders(ns).byOid[want]; _KU_DECODER.set(ns, dec); }
693
+ try { return dec(exts[i].value); }
694
+ catch (e) { throw E(code, "the " + label + " keyUsage extension is malformed", e); }
695
+ }
696
+ return null;
697
+ }
698
+
699
+ // IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0] OPTIONAL,
700
+ // onlyContainsUserCerts [1] DEFAULT FALSE, onlyContainsCACerts [2] DEFAULT FALSE,
701
+ // onlySomeReasons [3] ReasonFlags OPTIONAL, indirectCRL [4] DEFAULT FALSE,
702
+ // onlyContainsAttributeCerts [5] DEFAULT FALSE } (RFC 5280 sec. 5.2.5).
703
+ //
704
+ // Declared here rather than in a consumer because the extension answers a
705
+ // question -- which certificates does this CRL speak for -- that both the path
706
+ // validator and the standalone CRL verbs act on, and a scope flag read by a hand
707
+ // walk over the raw children is read by the reader's own rules instead of the
708
+ // encoding's. Each flag is an IMPLICIT BOOLEAN, so `implicitBoolean` is what
709
+ // holds it to one content octet of 0x00 or 0xFF (X.690 sec. 11.1); a hand walk
710
+ // testing a content byte reads an EMPTY [4] as absent and a multi-octet one by
711
+ // whichever byte it happens to index, and "absent" is the reading that grants
712
+ // permission to answer. `trailing` supplies the rest of the grammar: strictly
713
+ // ascending tags, each field at most once, nothing outside 0..5.
714
+ //
715
+ // A present DEFAULT-FALSE flag encoding FALSE is well-formed at the leaf but is
716
+ // still an encoding DER forbids (X.690 sec. 11.5 omits the default), so the
717
+ // caller decides what that means for it -- reported as a value of `false`
718
+ // against a `present` of true, never silently normalized away.
719
+ function issuingDistributionPoint(code) {
720
+ return schema.seq([
721
+ schema.trailing([
722
+ { tag: 0, name: "distributionPoint", schema: schema.any() },
723
+ { tag: 1, name: "onlyContainsUserCerts", schema: schema.implicitBoolean(1) },
724
+ { tag: 2, name: "onlyContainsCACerts", schema: schema.implicitBoolean(2) },
725
+ { tag: 3, name: "onlySomeReasons", schema: schema.implicitBitString(3) },
726
+ { tag: 4, name: "indirectCRL", schema: schema.implicitBoolean(4) },
727
+ { tag: 5, name: "onlyContainsAttributeCerts", schema: schema.implicitBoolean(5) },
728
+ ], { minTag: 0, maxTag: 5, unexpectedCode: code, orderCode: code }),
729
+ ], { assert: "sequence", code: code, what: "IssuingDistributionPoint" });
730
+ }
731
+
672
732
  // certExtensionDecoders(ns) -- the ns-parameterized RFC 5280 sec. 4.2.1 extension
673
733
  // VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
674
734
  // value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
@@ -1587,6 +1647,8 @@ module.exports = {
1587
1647
  generalName: generalName,
1588
1648
  generalNames: generalNames,
1589
1649
  distributionPointName: distributionPointName,
1650
+ issuingDistributionPoint: issuingDistributionPoint,
1651
+ keyUsageOf: keyUsageOf,
1590
1652
  generalizedTime: generalizedTime,
1591
1653
  utf8Text: utf8Text,
1592
1654
  rawNonEmptySequence: rawNonEmptySequence,
@@ -67,23 +67,61 @@ function _pssAlgId(digestName) {
67
67
  var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(BigInt(PSS_SALT[HASH[digestName]])))]);
68
68
  return b.sequence([b.oid(O("rsassaPss")), params]);
69
69
  }
70
- // An id-RSASSA-PSS SPKI MAY pin its permitted hash in the params (RFC 4055 sec. 1.2 / 3.1). Read
71
- // it so signing honors the restriction; absent params or an unrecognized hash returns null.
70
+ // The hash an id-RSASSA-PSS SPKI restricts its key to, or null when it restricts none.
71
+ //
72
+ // RFC 4055 sec. 3.1 draws the line at whether the parameters are THERE: "if present, the parameters
73
+ // field MUST contain RSASSA-PSS-params", and "if RSASSA-PSS-params is present, the certificate user
74
+ // MUST perform those operations using the one-way hash function ... identified in the ...
75
+ // parameters". Absent parameters therefore restrict nothing, and null says so.
76
+ //
77
+ // PRESENT parameters are a restriction even where they look empty. `hashAlgorithm` is
78
+ // `[0] HashAlgorithm DEFAULT sha1Identifier`, so a params SEQUENCE that omits it names SHA-1 --
79
+ // it does not decline to name anything. Reading the omission as "no restriction" is the fail-open
80
+ // that matters here: it turns a key its own certificate confines to SHA-1 into one that will verify
81
+ // a SHA-512 signature. And parameters that are present but unreadable are a restriction this code
82
+ // cannot honor, which is not the same as no restriction either, so they are refused.
72
83
  function _pssHashFromSpki(cert, E) {
73
84
  var params = cert.subjectPublicKeyInfo.algorithm.parameters;
74
85
  if (params == null) return null;
75
- var node = asn1.decode(params);
76
- if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) return null;
86
+ var node;
87
+ try { node = asn1.decode(params); }
88
+ catch (e) { throw E("unsupported-algorithm", "the id-RSASSA-PSS key parameters are not decodable, so the restriction they carry cannot be honored", e); }
89
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) {
90
+ throw E("unsupported-algorithm", "the id-RSASSA-PSS key parameters are not an RSASSA-PSS-params SEQUENCE (RFC 4055 sec. 3.1)");
91
+ }
77
92
  var hashField = node.children.filter(function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
78
- if (!hashField || !hashField.children || !hashField.children[0] || !hashField.children[0].children) return null;
93
+ // DEFAULT sha1Identifier -- an omitted hashAlgorithm names SHA-1, which this toolkit does not
94
+ // sign or verify with, so it is reported as the pin it is and refused by the caller's own table.
95
+ if (!hashField) return "sha1";
96
+ if (!hashField.children || !hashField.children[0] || !hashField.children[0].children) {
97
+ throw E("unsupported-algorithm", "the id-RSASSA-PSS key parameters carry a malformed hashAlgorithm");
98
+ }
79
99
  var oidNode = hashField.children[0].children[0];
80
- if (!oidNode || oidNode.tagClass !== "universal" || oidNode.tagNumber !== asn1.TAGS.OBJECT_IDENTIFIER) return null;
100
+ if (!oidNode || oidNode.tagClass !== "universal" || oidNode.tagNumber !== asn1.TAGS.OBJECT_IDENTIFIER) {
101
+ throw E("unsupported-algorithm", "the id-RSASSA-PSS key parameters hashAlgorithm is not an OBJECT IDENTIFIER");
102
+ }
81
103
  var pinnedOid = asn1.read.oid(oidNode);
82
104
  var name = HASH_NAME_BY_OID[pinnedOid];
83
105
  if (!name) throw E("unsupported-algorithm", "the id-RSASSA-PSS signer key pins an unsupported hash algorithm (" + pinnedOid + ")");
84
106
  return name;
85
107
  }
86
108
 
109
+ // @internal -- the WebCrypto hash name an id-RSASSA-PSS SPKI pins, or null when it pins none.
110
+ // Verifiers need the same restriction the signer above honors: a key whose certificate says
111
+ // SHA-256 must not be handed a SHA-512 signature to check, and reading the pin in two places is
112
+ // how the two directions come to disagree. Throws through the caller's E on a hash this toolkit
113
+ // does not implement, so an unreadable restriction is never treated as no restriction.
114
+ function pssSpkiPinnedHash(cert, E) {
115
+ var d = _pssHashFromSpki(cert, E);
116
+ if (!d) return null;
117
+ // A pin this toolkit has no WebCrypto hash for -- SHA-1, which RSASSA-PSS-params names by DEFAULT
118
+ // -- is still a pin. Returning undefined here would hand the caller a falsy value it reads as
119
+ // "unrestricted", which is the same fail-open the DEFAULT reading above exists to close, one
120
+ // layer up.
121
+ if (!HASH[d]) throw E("unsupported-algorithm", "the id-RSASSA-PSS key is restricted to " + d + ", which this toolkit does not verify with");
122
+ return HASH[d];
123
+ }
124
+
87
125
  // resolveSignScheme(cert, so, noSignedAttrs, E) -> the signature scheme from the signer cert's
88
126
  // public-key algorithm + per-signer opts (so.digestAlgorithm / so.pss / so.combinedRsaSig -- the
89
127
  // last folds the digest into a combined RSA signature OID for a caller with no digestAlgorithm
@@ -205,6 +243,7 @@ function signOverTbs(scheme, key, signedBytes, E) {
205
243
  // no drift between the two.
206
244
  module.exports = {
207
245
  resolveSignScheme: resolveSignScheme,
246
+ pssSpkiPinnedHash: pssSpkiPinnedHash,
208
247
  signOverTbs: signOverTbs,
209
248
  MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST,
210
249
  SLHDSA_BY_OID: SLHDSA_BY_OID,
package/lib/tsp-sign.js CHANGED
@@ -23,11 +23,12 @@ var pathValidate = require("./path-validate");
23
23
  var pkiX509 = require("./schema-x509");
24
24
  var smime = require("./schema-smime");
25
25
  var schemaTsp = require("./schema-tsp");
26
- var schema = require("./schema-engine");
27
26
  var guard = require("./guard-all");
28
27
  var frameworkError = require("./framework-error");
29
28
 
29
+ var pkix = require("./schema-pkix");
30
30
  var TspError = frameworkError.TspError;
31
+ var _NS = pkix.makeNS("tsp", TspError, oid);
31
32
  var b = asn1.build;
32
33
  function _err(code, message, cause) { return new TspError(code, message, cause); }
33
34
  function O(name) { return oid.byName(name); }
@@ -484,19 +485,12 @@ function _checkTsaCertUsage(tsaCertDer) {
484
485
  // A keyUsage that forbids signing cannot mint a token (RFC 5280 sec. 4.2.1.3); an absent keyUsage
485
486
  // is unrestricted. Require digitalSignature (bit 0) or nonRepudiation/contentCommitment (bit 1) --
486
487
  // the signing bits, the TSA analogue of the OCSP-responder keyUsage gate.
487
- var kuExts = (cert.extensions || []).filter(function (e) { return e.oid === O("keyUsage"); });
488
- if (kuExts.length) {
489
- var ku;
490
- try {
491
- ku = asn1.read.bitString(asn1.decode(kuExts[0].value));
492
- // KeyUsage is a NamedBitList: enforce the X.690 sec. 11.2.2 minimal-DER rule (no trailing zero
493
- // bits) that the shared certExtensionDecoders keyUsage decoder applies, so a non-minimal
494
- // encoding other paths reject cannot slip a "permits signing" verdict through here.
495
- schema.assertMinimalNamedBits(ku.unusedBits, ku.bytes, function (m) { throw _err("tsp/bad-key-usage", m); });
496
- } catch (_e) { return "tsp/bad-key-usage"; }
497
- var byte0 = ku.bytes.length ? ku.bytes[0] : 0;
498
- if (!((byte0 >> 7) & 1) && !((byte0 >> 6) & 1)) return "tsp/bad-key-usage"; // no digitalSignature / nonRepudiation
499
- }
488
+ // Through the shared reader, which applies the NamedBitList rules (X.690 sec. 11.2.2 minimal
489
+ // encoding, sec. 4.2.1.3 at least one bit set) this boundary was applying only half of.
490
+ var ku;
491
+ try { ku = pkix.keyUsageOf(_NS, cert, _err, "tsp/bad-key-usage", "TSA certificate"); }
492
+ catch (_e) { return "tsp/bad-key-usage"; }
493
+ if (ku && !ku.digitalSignature && !ku.nonRepudiation) return "tsp/bad-key-usage";
500
494
  return true;
501
495
  }
502
496
 
@@ -570,7 +564,7 @@ function _buildTsaChains(leaf, pool) {
570
564
  * PEM (never a parsed object -- every checked field is read from the CMS-verified eContent, so a
571
565
  * mutated parsed structure cannot desynchronize the checks from the signed bytes). `data` is the
572
566
  * original bytes (hashed under the token's messageImprint algorithm) or a precomputed
573
- * `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, genTime, accuracy, serialNumber,
567
+ * `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, trusted, genTime, accuracy, serialNumber,
574
568
  * serialNumberHex, policy, nonce, tsa, tstInfo, signer, code?, reason? }`. `valid` is true only
575
569
  * when the CMS signature, the imprint match, the eContentType, the ESSCertID(V2) binding, the
576
570
  * RFC 3161 sec. 2.3 critical single-`timeStamping` extendedKeyUsage rule, the requested nonce (when
@@ -578,6 +572,13 @@ function _buildTsaChains(leaf, pool) {
578
572
  * validation all pass. A conformance / trust failure of a well-formed token is a
579
573
  * `{ valid:false, code }` verdict; malformed or config input throws a typed `TspError`.
580
574
  *
575
+ * `trusted` is the second claim and is kept apart from the first. `valid` says the token's
576
+ * signature and structural bindings hold; `trusted` says the timestamp authority chained to an
577
+ * anchor this caller named. Without `trustAnchor` there is nothing to chain to and `trusted` is
578
+ * `false` -- a definite answer rather than a missing one, on the refusal branch as well as the
579
+ * accepting one. A timestamp is archived precisely to be re-read years later, and one boolean
580
+ * cannot answer both questions then.
581
+ *
581
582
  * @opts trustAnchor Anchor `{ name, publicKey, algorithm }` -- runs `pki.path.validate` on the
582
583
  * TSA certificate chain ordered from the token's embedded certificates
583
584
  * (validity at genTime, requiredEku timeStamping, revocation), so a TSA under
@@ -625,8 +626,12 @@ async function verify(token, data, opts) {
625
626
  // structure and decodes the TSTInfo FROM the raw eContent; a structural defect throws.
626
627
  var parsed = schemaTsp.parseToken(tokenDer);
627
628
  var tst = parsed.tstInfo;
629
+ // `trusted: false` on every refusal too, not only on the accepting return. A caller reading
630
+ // `res.trusted` must get an answer on both branches -- an undefined on the failure path is the
631
+ // same "cannot tell what was checked" the field was added to remove, and `!res.trusted` reading
632
+ // true by accident is not the same as its reading true because nothing anchored the TSA.
628
633
  function fail(code, reason) {
629
- return { valid: false, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null };
634
+ return { valid: false, trusted: false, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null };
630
635
  }
631
636
  // M12 -- the CMS signature over the exact RFC 5652 sec. 5.4 preimage (message-digest bound to the
632
637
  // authenticated eContent). cms.verify re-parses the same bytes; a failure is a fail-closed verdict.
@@ -673,6 +678,14 @@ async function verify(token, data, opts) {
673
678
  // key-param inheritance, requiredEku, optional revocation), only when a trustAnchor is supplied.
674
679
  // The path is ordered from the token's embedded certificates (leaf + any intermediates), so a TSA
675
680
  // issued under an intermediate CA -- not just directly under the anchor -- validates.
681
+ // `trusted` is the SECOND claim, kept apart from `valid`. `valid` says the token's signature and
682
+ // its structural bindings hold; whether the timestamp authority is one this caller accepts is
683
+ // answered only by the chain below, and only when an anchor was supplied. Collapsing the two into
684
+ // one boolean meant an archived verdict could not be re-read to tell whether the TSA was ever
685
+ // trusted -- which is exactly what a timestamp is archived to answer. Without an anchor there is
686
+ // nothing to chain to and `trusted` is false: a definite answer rather than a missing one, the
687
+ // same shape pki.cms.verify and pki.cmp.verify return.
688
+ var trusted = false;
676
689
  if (opts.trustAnchor) {
677
690
  var pathRes = null;
678
691
  // tst.genTime floors to millisecond precision. When genTime carries sub-millisecond digits the true
@@ -693,22 +706,30 @@ async function verify(token, data, opts) {
693
706
  // path.validate validates a FIXED path, so backtracking over same-subject issuer candidates
694
707
  // happens here -- accept the TSA certificate if ANY enumerated chain validates to the anchor at
695
708
  // both window endpoints.
709
+ //
710
+ // checkPurpose names timeStamping alongside requiredEku, and the pairing is the point. The EKU
711
+ // constrains the TSA CERTIFICATE; checkPurpose selects the ANCHOR's own trust metadata, which
712
+ // pki.path consults only when a purpose is named -- so asking one without the other checks one
713
+ // end of the chain and not the other, and a root explicitly distrusted for timestamping would
714
+ // still answer trusted. The purpose is not a caller choice here: this verb validates timestamp
715
+ // tokens and nothing else, so there is exactly one purpose its anchors can be judged under.
696
716
  var chains = _buildTsaChains(pkiX509.parse(tsaCertDer), pool);
697
717
  for (var ci = 0; ci < chains.length && !(pathRes && pathRes.valid); ci++) {
698
718
  pathRes = await pathValidate.validate(chains[ci], {
699
- time: floorT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], revocationChecker: opts.revocationChecker,
719
+ time: floorT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
700
720
  });
701
721
  if (pathRes.valid && ceilT !== floorT) {
702
722
  pathRes = await pathValidate.validate(chains[ci], {
703
- time: ceilT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], revocationChecker: opts.revocationChecker,
723
+ time: ceilT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
704
724
  });
705
725
  }
706
726
  }
707
727
  } catch (e) { return fail("tsp/untrusted-tsa", (e && e.message) || String(e)); }
708
728
  if (!pathRes || !pathRes.valid) return fail("tsp/untrusted-tsa", "the TSA certificate did not validate to the trust anchor at genTime");
729
+ trusted = true;
709
730
  }
710
731
  return {
711
- valid: true, genTime: tst.genTime, accuracy: tst.accuracy,
732
+ valid: true, trusted: trusted, genTime: tst.genTime, accuracy: tst.accuracy,
712
733
  serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex,
713
734
  policy: tst.policy, policyName: tst.policyName, nonce: tst.nonce, tsa: tst.tsa,
714
735
  tstInfo: tst, signer: { cert: tsaCertDer, sid: signer.sid },
@@ -57,6 +57,17 @@ var OKP_CRV = { 6: { oid: "Ed25519", len: 32 }, 7: { oid: "Ed448", len: 57 } };
57
57
  // -8 (EdDSA) is Ed25519 ONLY, and the RFC 9864 fully-specified ids (-9 ESP256, -51 ESP384,
58
58
  // -52 ESP512, -19 Ed25519, -53 Ed448) each pin key type + curve. A verifier accepts the
59
59
  // fully-specified ids even though WebAuthn recommends against them for credential creation.
60
+ // The RSA credential-key bounds. 2048 bits is the floor every current FIDO authenticator and
61
+ // NIST SP 800-57 agree on; nothing in the field emits less, so the floor refuses forgeable keys
62
+ // without refusing real ones. The exponent bound is a work bound, not a security one.
63
+ var RSA_MIN_MODULUS_BITS = 2048;
64
+ var RSA_MAX_EXPONENT_BYTES = 8;
65
+ // The modulus BIT length. A byte count is not one: minimally encoded, a 256-byte modulus whose
66
+ // leading byte is 0x01 is 2041 bits, and would clear a floor expressed in bytes while sitting below
67
+ // the floor that floor exists to state. The leading byte is non-zero by the minimal-encoding check
68
+ // above, so its position fixes the total.
69
+ function _modulusBits(n) { return (n.length - 1) * 8 + (32 - Math.clz32(n[0])); }
70
+
60
71
  var ALG_PROFILE = {
61
72
  "-7": { kty: 2, crv: 1 }, "-35": { kty: 2, crv: 2 }, "-36": { kty: 2, crv: 3 },
62
73
  "-9": { kty: 2, crv: 1 }, "-51": { kty: 2, crv: 2 }, "-52": { kty: 2, crv: 3 },
@@ -111,6 +122,34 @@ function credentialKey(node, E, code, unsupportedCode) {
111
122
  } else if (kty === 3n) {
112
123
  key.n = ib(-1); key.e = ib(-2);
113
124
  if (!key.n || !key.n.length || !key.e || !key.e.length) throw bad("an RSA COSE_Key must carry n (-1) and e (-2)");
125
+ // The MATERIAL, not merely its presence -- the same standard the other two key types are held
126
+ // to, where EC2 pins x/y to the curve's field size and has the point validated on the curve,
127
+ // and OKP pins x to an exact length. Checking only presence let a 1-byte modulus and an
128
+ // exponent of 1 through as conformant credential public keys, and both reach the WebCrypto
129
+ // import, so they reach real signature verification. e = 1 makes RSA the identity function:
130
+ // the "signature" is the message, and it verifies under any modulus.
131
+ // BOTH values first, before either is judged. RFC 8230 sec. 4 encodes n and e as unsigned
132
+ // big-endian integers with no leading zero, and every check below reads a byte LENGTH as though
133
+ // it were a magnitude: the modulus floor, the exponent bound, and the exponent's value. A
134
+ // padded encoding decouples the two, so `00 01` would be read as a two-byte exponent and skip
135
+ // the value check that refuses 1 -- the degenerate key the whole check exists to catch. (An EC2
136
+ // coordinate is the opposite case, fixed-width and zero-padded by definition, which is why this
137
+ // rule is stated for the RSA parameters and not for x/y.)
138
+ if (key.n[0] === 0) throw bad("an RSA COSE_Key modulus (-1) must be minimally encoded, with no leading zero byte (RFC 8230 sec. 4)");
139
+ if (key.e[0] === 0) throw bad("an RSA COSE_Key exponent (-2) must be minimally encoded, with no leading zero byte (RFC 8230 sec. 4)");
140
+ var modulusBits = _modulusBits(key.n);
141
+ if (modulusBits < RSA_MIN_MODULUS_BITS) {
142
+ throw bad("an RSA COSE_Key modulus (-1) is " + modulusBits + " bits, below the " +
143
+ RSA_MIN_MODULUS_BITS + "-bit minimum");
144
+ }
145
+ // e must be odd and greater than 1: RSA needs gcd(e, phi(n)) = 1, so an even exponent is not a
146
+ // valid RSA public exponent at all, and 1 is the degenerate case above. Bounded on the way in
147
+ // so a caller cannot hand over a megabyte of exponent for the modular exponentiation to chew.
148
+ if (key.e.length > RSA_MAX_EXPONENT_BYTES) throw bad("an RSA COSE_Key exponent (-2) is longer than " + RSA_MAX_EXPONENT_BYTES + " bytes");
149
+ if ((key.e[key.e.length - 1] & 1) === 0) throw bad("an RSA COSE_Key exponent (-2) must be odd");
150
+ // Minimal encoding above makes a one-byte e the ONLY way to express a value this small, so the
151
+ // comparison is on the value and not on where it happens to sit.
152
+ if (key.e.length === 1 && key.e[0] <= 1) throw bad("an RSA COSE_Key exponent (-2) must be greater than 1 -- e = 1 makes RSA the identity function");
114
153
  } else {
115
154
  throw bad("unsupported COSE_Key kty " + Number(kty));
116
155
  }