@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/CHANGELOG.md +51 -1
- package/README.md +4 -4
- package/lib/cms-encrypt.js +7 -11
- package/lib/constants.js +7 -4
- package/lib/crl-sign.js +152 -18
- package/lib/guard-bytes.js +6 -2
- package/lib/guard-name.js +15 -0
- package/lib/jose.js +39 -0
- package/lib/path-validate.js +113 -28
- package/lib/schema-pkix.js +62 -0
- package/lib/sign-scheme.js +45 -6
- package/lib/tsp-sign.js +40 -19
- package/lib/validator-cose.js +39 -0
- package/lib/webauthn-mds.js +216 -34
- package/lib/webauthn.js +370 -140
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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,
|
package/lib/path-validate.js
CHANGED
|
@@ -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,
|
|
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 (
|
|
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
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
|
1369
|
-
|
|
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
|
-
//
|
|
1450
|
-
//
|
|
1451
|
-
//
|
|
1452
|
-
|
|
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;
|
package/lib/schema-pkix.js
CHANGED
|
@@ -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,
|
package/lib/sign-scheme.js
CHANGED
|
@@ -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
|
-
//
|
|
71
|
-
//
|
|
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
|
|
76
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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 },
|
package/lib/validator-cose.js
CHANGED
|
@@ -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
|
}
|