@blamejs/pki 0.9.5 → 0.9.7
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 +66 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/asn1-der.js +6 -5
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +26 -3
- package/lib/cmp-verify.js +24 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +3 -3
- package/lib/cms-verify.js +18 -8
- package/lib/compromised-keys.js +114 -0
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/crmf-sign.js +2 -0
- package/lib/csr-sign.js +5 -1
- package/lib/ct.js +16 -3
- package/lib/est.js +44 -29
- package/lib/framework-error.js +20 -7
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +344 -0
- package/lib/guard-json.js +1 -1
- package/lib/guard-verdict.js +16 -3
- package/lib/http-transport.js +412 -41
- package/lib/jose.js +1 -1
- package/lib/lint.js +49 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +8 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +131 -16
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +39 -9
- package/lib/scep.js +28 -13
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +18 -13
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +159 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +7 -1
- package/lib/webauthn-mds.js +30 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +16 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/trust.js
CHANGED
|
@@ -1035,6 +1035,7 @@ function anchor(entry, opts) {
|
|
|
1035
1035
|
!entry.name || !intrinsic.isArray(entry.name.rdns))) {
|
|
1036
1036
|
throw E("trust/bad-input", "anchor expects a trust-store entry ({ name, publicKey, algorithm, ... })");
|
|
1037
1037
|
}
|
|
1038
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no purpose restricts the bundle, so a null must be refused
|
|
1038
1039
|
if (opts.purpose !== undefined) {
|
|
1039
1040
|
if (_arrIndexOf(PURPOSES, opts.purpose) === -1) {
|
|
1040
1041
|
throw E("trust/bad-input", "anchor: opts.purpose must be one of " + _arrJoin(PURPOSES, " | "));
|
|
@@ -1226,7 +1227,7 @@ var _BUNDLE_PATHS = [
|
|
|
1226
1227
|
];
|
|
1227
1228
|
|
|
1228
1229
|
var _SYSTEM_ANCHOR_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
1229
|
-
path: 1, allowNodeBundle: 1, maxBytes: 1,
|
|
1230
|
+
path: 1, allowNodeBundle: 1, maxBytes: 1, distrustedKeys: 1,
|
|
1230
1231
|
});
|
|
1231
1232
|
|
|
1232
1233
|
/** @internal What an operator runs to produce a bundle this verb can read, per platform. Named in
|
|
@@ -1306,9 +1307,85 @@ function _readBundleText(chosen, maxBytes) {
|
|
|
1306
1307
|
return raw;
|
|
1307
1308
|
}
|
|
1308
1309
|
|
|
1310
|
+
/**
|
|
1311
|
+
* @primitive pki.trust.spkiDigest
|
|
1312
|
+
* @signature pki.trust.spkiDigest(cert) -> string
|
|
1313
|
+
* @since 0.9.6
|
|
1314
|
+
* @status stable
|
|
1315
|
+
* @spec RFC 5280
|
|
1316
|
+
* @related pki.path.validate, pki.trust.systemAnchors, pki.transport.https
|
|
1317
|
+
*
|
|
1318
|
+
* The SHA-256 digest of a certificate's DER SubjectPublicKeyInfo in canonical form, as 64 lower-case
|
|
1319
|
+
* hexadecimal characters. Canonical form is an uncompressed EC point with its named curve and an RSA
|
|
1320
|
+
* key with NULL parameters, which is how nearly every certificate encodes a key, so for those it is
|
|
1321
|
+
* the digest of the bytes the certificate carries and the SPKI SHA-256 root programs publish. This is
|
|
1322
|
+
* the entry `distrustedKeys` takes on `pki.path.validate`, `pki.path.build`,
|
|
1323
|
+
* `pki.trust.systemAnchors`, the TLS policy of `pki.transport.https`, and the verbs that validate a
|
|
1324
|
+
* chain on a caller's behalf. Every certificate that carries the same key has the same digest, however
|
|
1325
|
+
* it encodes the key, so one entry covers a CA certificate and each of its cross-signatures. `cert` is
|
|
1326
|
+
* DER bytes, a PEM string, or a `pki.schema.x509.parse` result. An identifier that restricts a key to
|
|
1327
|
+
* one use names the same key: id-ecDH and id-ecMQV read as ecPublicKey, and RSASSA-PSS and RSAES-OAEP
|
|
1328
|
+
* read as rsaEncryption. A key node does not read, such as a composite or stateful hash-based key, is
|
|
1329
|
+
* digested as the certificate encodes it, and a composite key is its own key, so listing one of its
|
|
1330
|
+
* component keys does not list the composite.
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
1334
|
+
* var der = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
1335
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
1336
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"] } },
|
|
1337
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
1338
|
+
* pki.trust.spkiDigest(der).length; // 64
|
|
1339
|
+
*/
|
|
1340
|
+
function spkiDigest(cert) {
|
|
1341
|
+
if (cert === undefined || cert === null) throw E("trust/bad-input", "spkiDigest takes a certificate (DER, PEM, or a parsed certificate)");
|
|
1342
|
+
var spki;
|
|
1343
|
+
if (typeof cert === "string") spki = x509.parse(x509.pemDecode(cert, "CERTIFICATE")).subjectPublicKeyInfo.bytes;
|
|
1344
|
+
else if (guard.bytes.isByteSource(cert)) spki = x509.parse(guard.bytes.snapshotSource(cert, E, "trust/bad-input", "the certificate")).subjectPublicKeyInfo.bytes;
|
|
1345
|
+
else if (typeof cert === "object" && cert.subjectPublicKeyInfo && guard.bytes.isByteSource(cert.subjectPublicKeyInfo.bytes)) {
|
|
1346
|
+
spki = guard.bytes.snapshotSource(cert.subjectPublicKeyInfo.bytes, E, "trust/bad-input", "the certificate's subjectPublicKeyInfo.bytes");
|
|
1347
|
+
} else {
|
|
1348
|
+
throw E("trust/bad-input", "spkiDigest takes a certificate (DER, PEM, or a parsed certificate)");
|
|
1349
|
+
}
|
|
1350
|
+
return guard.distrust.keyDigest(spki);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* @primitive pki.trust.knownCompromisedKeys
|
|
1355
|
+
* @signature pki.trust.knownCompromisedKeys() -> Array<{ spkiSha256, name, reason, sources }>
|
|
1356
|
+
* @since 0.9.7
|
|
1357
|
+
* @status stable
|
|
1358
|
+
* @spec CA/Browser Forum TLS BR
|
|
1359
|
+
* @related pki.trust.spkiDigest, pki.path.validate, pki.transport.https, pki.x509.sign
|
|
1360
|
+
*
|
|
1361
|
+
* The keys every verb in this toolkit refuses because their private key is public. Each record holds
|
|
1362
|
+
* `spkiSha256`, the `pki.trust.spkiDigest` value of the key; `name`, the subject common name of a
|
|
1363
|
+
* certificate that carries it, or the product's CA name for a key published without a certificate;
|
|
1364
|
+
* `reason`, how the private key became public; and `sources`, the URLs where that is recorded. Each
|
|
1365
|
+
* record is frozen, and the array is a new one on every call.
|
|
1366
|
+
*
|
|
1367
|
+
* The toolkit also refuses an elliptic curve key on P-256, P-384, P-521 or secp256k1 whose private key
|
|
1368
|
+
* is an integer below 256, or below 256 short of the group order, whether or not it is listed here.
|
|
1369
|
+
*
|
|
1370
|
+
* Path validation fails a chain whose trust anchor, intermediate or target carries such a key with
|
|
1371
|
+
* `path/compromised-key`. `pki.trust.systemAnchors` skips such a certificate with the reason
|
|
1372
|
+
* `trust/compromised-key`. `pki.transport.https` refuses a server whose certificate chain carries one
|
|
1373
|
+
* with `transport/server-key-compromised`, and a proxy with `transport/proxy-key-compromised`.
|
|
1374
|
+
* `pki.x509.sign`, `pki.csr.sign` and `pki.crmf.build` refuse to certify, sign with or request a
|
|
1375
|
+
* certificate for one, with `x509/compromised-key`, `csr/compromised-key` and `crmf/compromised-key`.
|
|
1376
|
+
* None of these refusals can be turned off.
|
|
1377
|
+
*
|
|
1378
|
+
* @example
|
|
1379
|
+
* var listed = pki.trust.knownCompromisedKeys();
|
|
1380
|
+
* listed[0].spkiSha256.length; // 64
|
|
1381
|
+
*/
|
|
1382
|
+
function knownCompromisedKeys() {
|
|
1383
|
+
return guard.distrust.knownCompromised();
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1309
1386
|
/**
|
|
1310
1387
|
* @primitive pki.trust.systemAnchors
|
|
1311
|
-
* @signature pki.trust.systemAnchors(opts?) -> { anchors, source, path, skipped }
|
|
1388
|
+
* @signature pki.trust.systemAnchors(opts?) -> { anchors, intermediates, source, path, skipped }
|
|
1312
1389
|
* @since 0.8.15
|
|
1313
1390
|
* @status stable
|
|
1314
1391
|
* @spec RFC 7468 (PEM), RFC 5280
|
|
@@ -1321,16 +1398,37 @@ function _readBundleText(chosen, maxBytes) {
|
|
|
1321
1398
|
* bundle carries that is not a certificate is listed in `skipped` with the label that named it,
|
|
1322
1399
|
* rather than dropped.
|
|
1323
1400
|
*
|
|
1401
|
+
* Only a self-issued certificate becomes an anchor. A certificate whose issuer name differs from its
|
|
1402
|
+
* subject name is listed in `skipped` with the reason `trust/not-self-issued` and returned, as DER,
|
|
1403
|
+
* in `intermediates`, which `pki.path.build` takes as `candidates`. A Windows store export includes
|
|
1404
|
+
* the Intermediate Certification Authorities store, which holds certificates the host has seen
|
|
1405
|
+
* rather than ones it trusts, and OpenSSL's default verification also requires a self-issued
|
|
1406
|
+
* certificate at the top of a chain. A certificate whose names cannot be compared, because an
|
|
1407
|
+
* attribute value carries a control byte, is listed in `skipped` with that reason and is returned in
|
|
1408
|
+
* neither list.
|
|
1409
|
+
*
|
|
1410
|
+
* A certificate whose key is listed in `opts.distrustedKeys` is neither an anchor nor an
|
|
1411
|
+
* intermediate; it is listed in `skipped` with the reason `trust/distrusted-key`. A certificate whose
|
|
1412
|
+
* private key is public (see `pki.trust.knownCompromisedKeys`) is skipped the same way with the reason
|
|
1413
|
+
* `trust/compromised-key`, whether or not a list is given. A bundle left with no anchor is refused
|
|
1414
|
+
* with `trust/no-anchors`.
|
|
1415
|
+
*
|
|
1324
1416
|
* A host whose roots are not in a readable PEM file is refused, and the refusal names the command
|
|
1325
1417
|
* that exports them. macOS keeps its roots in a binary keychain and Windows in a registry-backed
|
|
1326
1418
|
* store, and reading either means running another program, which this toolkit does not do. Falling
|
|
1327
1419
|
* back to Node's set instead would report anchors the host does not use, so that takes the
|
|
1328
|
-
* explicit `opts.allowNodeBundle`.
|
|
1420
|
+
* explicit `opts.allowNodeBundle`. Node's set is the Mozilla list as of the Node release that
|
|
1421
|
+
* compiled it, so a root that Mozilla has since removed stays in it until Node is upgraded;
|
|
1422
|
+
* `opts.distrustedKeys` removes one sooner.
|
|
1329
1423
|
*
|
|
1330
1424
|
* @opts
|
|
1331
1425
|
* - `path` -- read this file instead of searching the per-platform list.
|
|
1332
|
-
* - `allowNodeBundle` --
|
|
1426
|
+
* - `allowNodeBundle` -- `true` returns Node's compiled-in set when no bundle can be read.
|
|
1427
|
+
* It takes `true`, `false`, `null` or an absent option, and any other value is refused with
|
|
1428
|
+
* `trust/bad-input`.
|
|
1333
1429
|
* - `maxBytes` -- cap the bundle read (default `C.LIMITS.TRUST_MAX_BYTES`).
|
|
1430
|
+
* - `distrustedKeys` -- SHA-256 digests of SubjectPublicKeyInfo values (64 hex characters or 32
|
|
1431
|
+
* bytes each, see `pki.trust.spkiDigest`) whose certificates are left out.
|
|
1334
1432
|
*
|
|
1335
1433
|
* @example
|
|
1336
1434
|
* // Read the host's bundle where there is one, and say so when there is not. Check `source`
|
|
@@ -1347,9 +1445,12 @@ function systemAnchors(opts) {
|
|
|
1347
1445
|
"unknown systemAnchors option ");
|
|
1348
1446
|
var maxBytes = guard.limits.cap(opts.maxBytes, "systemAnchors: opts.maxBytes",
|
|
1349
1447
|
LIMITS.TRUST_MAX_BYTES, { E: E, code: "trust/bad-input", min: 1, max: LIMITS.TRUST_MAX_BYTES });
|
|
1448
|
+
var allowNodeBundle = guard.identifier.booleanOption(opts.allowNodeBundle, E, "trust/bad-input",
|
|
1449
|
+
"systemAnchors: opts.allowNodeBundle");
|
|
1450
|
+
var distrusted = guard.distrust.keys(opts.distrustedKeys, E, "trust/bad-input", "systemAnchors: opts.distrustedKeys");
|
|
1350
1451
|
|
|
1351
1452
|
var chosen = null;
|
|
1352
|
-
if (opts.path
|
|
1453
|
+
if (opts.path != null) {
|
|
1353
1454
|
if (typeof opts.path !== "string" || opts.path.length === 0) {
|
|
1354
1455
|
throw E("trust/bad-input", "systemAnchors: opts.path must be a non-empty string");
|
|
1355
1456
|
}
|
|
@@ -1364,7 +1465,7 @@ function systemAnchors(opts) {
|
|
|
1364
1465
|
* set a validation runs against for a different one. */
|
|
1365
1466
|
try { text = _readBundleText(chosen, maxBytes); }
|
|
1366
1467
|
catch (e) {
|
|
1367
|
-
if (
|
|
1468
|
+
if (!allowNodeBundle || !e || e.code !== "trust/no-readable-store") throw e;
|
|
1368
1469
|
text = null;
|
|
1369
1470
|
chosen = null;
|
|
1370
1471
|
}
|
|
@@ -1382,15 +1483,38 @@ function systemAnchors(opts) {
|
|
|
1382
1483
|
}
|
|
1383
1484
|
|
|
1384
1485
|
if (text === null) {
|
|
1385
|
-
if (
|
|
1486
|
+
if (!allowNodeBundle) {
|
|
1386
1487
|
throw E("trust/no-readable-store", "this host keeps no CA bundle this toolkit can read" +
|
|
1387
1488
|
(chosen === null ? "" : " at " + guard.text.showValue(chosen)) + "; " + _exportHint() +
|
|
1388
1489
|
", or pass opts.allowNodeBundle to use the Mozilla set compiled into Node, which is NOT " +
|
|
1389
1490
|
"what this host trusts");
|
|
1390
1491
|
}
|
|
1391
|
-
return _anchorsFromPems(nodeTls.rootCertificates, "node-bundled", null);
|
|
1492
|
+
return _anchorsFromPems(nodeTls.rootCertificates, "node-bundled", null, distrusted);
|
|
1493
|
+
}
|
|
1494
|
+
return _anchorsFromBundle(text, chosen, distrusted);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/** @internal Why a parsed bundle certificate is not an anchor, or null when it is one. A
|
|
1498
|
+
* distrusted key is reported first, so a listed intermediate is not offered as a candidate either.
|
|
1499
|
+
* The name comparison is the RFC 5280 sec. 7.1 one, and a name it cannot compare (a control byte
|
|
1500
|
+
* in an attribute value) is reported rather than thrown, so one malformed entry does not refuse the
|
|
1501
|
+
* whole bundle. */
|
|
1502
|
+
function _systemEntryRefusal(cert, distrusted) {
|
|
1503
|
+
var compromised = guard.distrust.compromised(cert.subjectPublicKeyInfo.bytes);
|
|
1504
|
+
if (compromised !== null) return { intermediate: false, reason: "trust/compromised-key: " + compromised };
|
|
1505
|
+
if (guard.distrust.has(distrusted, cert.subjectPublicKeyInfo.bytes)) {
|
|
1506
|
+
return { intermediate: false, reason: "trust/distrusted-key: the certificate's key is listed in opts.distrustedKeys" };
|
|
1507
|
+
}
|
|
1508
|
+
var selfIssued;
|
|
1509
|
+
try { selfIssued = guard.name.dnEqual(cert.subject.rdns, cert.issuer.rdns, E, "trust/bad-input", "the bundle certificate"); }
|
|
1510
|
+
catch (e) {
|
|
1511
|
+
return { intermediate: false, reason: (e && e.code ? e.code : "trust/bad-input") + ": " + guard.text.describeThrown(e) };
|
|
1392
1512
|
}
|
|
1393
|
-
|
|
1513
|
+
if (!selfIssued) {
|
|
1514
|
+
return { intermediate: true, reason: "trust/not-self-issued: the issuer name differs from the subject name, so the " +
|
|
1515
|
+
"certificate is not a root; it is returned in intermediates for chain building" };
|
|
1516
|
+
}
|
|
1517
|
+
return null;
|
|
1394
1518
|
}
|
|
1395
1519
|
|
|
1396
1520
|
/** @internal A line that reads as a BEGIN boundary but does not start one, because RFC 7468 sec. 2
|
|
@@ -1429,7 +1553,7 @@ function _reportUnusableBoundaries(bytes, rows, skipped) {
|
|
|
1429
1553
|
/** @internal Every certificate block becomes an anchor and every other block is reported, because
|
|
1430
1554
|
* a bundle an operator assembled by hand is exactly where a key or a CRL ends up, and a reader
|
|
1431
1555
|
* that drops one silently leaves them believing it was read. */
|
|
1432
|
-
function _anchorsFromBundle(text, chosen) {
|
|
1556
|
+
function _anchorsFromBundle(text, chosen, distrusted) {
|
|
1433
1557
|
var rows;
|
|
1434
1558
|
try { rows = pkix.pemDecodeBundle(text, {}, errors.PemError); }
|
|
1435
1559
|
catch (e) {
|
|
@@ -1440,7 +1564,7 @@ function _anchorsFromBundle(text, chosen) {
|
|
|
1440
1564
|
throw E("trust/bad-bundle", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1441
1565
|
" is not readable as PEM: " + guard.text.describeThrown(e), e);
|
|
1442
1566
|
}
|
|
1443
|
-
var anchors = [], skipped = [];
|
|
1567
|
+
var anchors = [], skipped = [], intermediates = [];
|
|
1444
1568
|
_reportUnusableBoundaries(text, rows, skipped);
|
|
1445
1569
|
for (var i = 0; i < rows.length; i++) {
|
|
1446
1570
|
var row = rows[i];
|
|
@@ -1479,40 +1603,53 @@ function _anchorsFromBundle(text, chosen) {
|
|
|
1479
1603
|
guard.list.append(skipped, { index: row.index, offset: row.offset, label: row.label, reason: why });
|
|
1480
1604
|
continue;
|
|
1481
1605
|
}
|
|
1606
|
+
var refusal = _systemEntryRefusal(cert, distrusted);
|
|
1607
|
+
if (refusal !== null) {
|
|
1608
|
+
guard.list.append(skipped, { index: row.index, offset: row.offset, label: row.label, reason: refusal.reason });
|
|
1609
|
+
if (refusal.intermediate) guard.list.append(intermediates, intrinsic.bufferFrom(row.der));
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1482
1612
|
guard.list.append(anchors, _mkAnchor(cert, {
|
|
1483
1613
|
distrustAfter: {}, purposes: null, label: null, mozillaCaPolicy: false,
|
|
1484
1614
|
}));
|
|
1485
1615
|
}
|
|
1486
1616
|
if (anchors.length === 0) {
|
|
1487
1617
|
throw E("trust/no-anchors", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1488
|
-
" carries no certificate, so it names no trust anchor");
|
|
1618
|
+
" carries no self-issued certificate this read admits, so it names no trust anchor");
|
|
1489
1619
|
}
|
|
1490
|
-
return _systemResult(anchors, "ca-bundle", chosen, skipped);
|
|
1620
|
+
return _systemResult(anchors, intermediates, "ca-bundle", chosen, skipped);
|
|
1491
1621
|
}
|
|
1492
1622
|
|
|
1493
|
-
function _anchorsFromPems(pems, source, chosen) {
|
|
1494
|
-
var anchors = [], skipped = [];
|
|
1623
|
+
function _anchorsFromPems(pems, source, chosen, distrusted) {
|
|
1624
|
+
var anchors = [], skipped = [], intermediates = [];
|
|
1495
1625
|
for (var i = 0; i < pems.length; i++) {
|
|
1496
|
-
var cert = null, why = null;
|
|
1497
|
-
try {
|
|
1626
|
+
var cert = null, why = null, der = null;
|
|
1627
|
+
try { der = x509.pemDecode(pems[i], "CERTIFICATE"); cert = x509.parse(der); }
|
|
1498
1628
|
catch (e) { why = (e && e.code ? e.code : "trust/not-a-certificate") + ": " + guard.text.describeThrown(e); }
|
|
1499
1629
|
if (cert === null) {
|
|
1500
1630
|
guard.list.append(skipped, { index: i, offset: null, label: "CERTIFICATE", reason: why });
|
|
1501
1631
|
continue;
|
|
1502
1632
|
}
|
|
1633
|
+
var refusal = _systemEntryRefusal(cert, distrusted);
|
|
1634
|
+
if (refusal !== null) {
|
|
1635
|
+
guard.list.append(skipped, { index: i, offset: null, label: "CERTIFICATE", reason: refusal.reason });
|
|
1636
|
+
if (refusal.intermediate) guard.list.append(intermediates, intrinsic.bufferFrom(der));
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1503
1639
|
guard.list.append(anchors, _mkAnchor(cert, {
|
|
1504
1640
|
distrustAfter: {}, purposes: null, label: null, mozillaCaPolicy: false,
|
|
1505
1641
|
}));
|
|
1506
1642
|
}
|
|
1507
|
-
if (anchors.length === 0) throw E("trust/no-anchors", "the " + source + " set carries no certificate");
|
|
1508
|
-
return _systemResult(anchors, source, chosen, skipped);
|
|
1643
|
+
if (anchors.length === 0) throw E("trust/no-anchors", "the " + source + " set carries no self-issued certificate this read admits");
|
|
1644
|
+
return _systemResult(anchors, intermediates, source, chosen, skipped);
|
|
1509
1645
|
}
|
|
1510
1646
|
|
|
1511
1647
|
/** @internal Defined rather than assigned, and a fresh list each call: the caller owns what it is
|
|
1512
1648
|
* handed, so emptying it must not change what the next call returns. */
|
|
1513
|
-
function _systemResult(anchors, source, chosen, skipped) {
|
|
1649
|
+
function _systemResult(anchors, intermediates, source, chosen, skipped) {
|
|
1514
1650
|
var out = intrinsic.create(null);
|
|
1515
1651
|
guard.verdict.set(out, "anchors", anchors);
|
|
1652
|
+
guard.verdict.set(out, "intermediates", intermediates);
|
|
1516
1653
|
guard.verdict.set(out, "source", source);
|
|
1517
1654
|
guard.verdict.set(out, "path", chosen);
|
|
1518
1655
|
guard.verdict.set(out, "skipped", skipped);
|
|
@@ -1528,4 +1665,6 @@ module.exports = intrinsic.freeze({
|
|
|
1528
1665
|
systemAnchors: systemAnchors,
|
|
1529
1666
|
discoversBundles: discoversBundles,
|
|
1530
1667
|
anchor: anchor,
|
|
1668
|
+
spkiDigest: spkiDigest,
|
|
1669
|
+
knownCompromisedKeys: knownCompromisedKeys,
|
|
1531
1670
|
});
|
package/lib/trustanchor-build.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
+
// codebase-patterns:allow-file caller-option-presence-tested-against-undefined
|
|
5
|
+
/** @internal Every member of a trust anchor RESTRICTS what it may certify: a policy set,
|
|
6
|
+
* policy flags, name constraints, a path length. A `null` here is a malformed spec rather
|
|
7
|
+
* than an option left out, because reading it as an omission would drop a restriction the
|
|
8
|
+
* caller named and build a WIDER anchor than they described without saying so. Presence is
|
|
9
|
+
* asked against `undefined` alone, and a null member is refused by the builder's own typed
|
|
10
|
+
* error, which the nested-record vector in schema-trustanchor.test.js pins. */
|
|
4
11
|
/**
|
|
5
12
|
* @module pki.trustanchor
|
|
6
13
|
* @nav Trust
|
|
@@ -98,7 +105,15 @@ function _certPathDer(spec) {
|
|
|
98
105
|
throw _err("trustanchor/bad-input", "certPath.taName must be a non-empty distinguished name (RFC 5914 sec. 2)");
|
|
99
106
|
}
|
|
100
107
|
var kids = [b.raw(nameDer)];
|
|
101
|
-
/** @internal
|
|
108
|
+
/** @internal Presence is asked as "not undefined" here, and deliberately not as "not
|
|
109
|
+
* absent", which is how a caller option is read everywhere else. These members RESTRICT
|
|
110
|
+
* what the anchor may certify: a policy set, policy flags, name constraints and a path
|
|
111
|
+
* length. Reading a `null` as an omission would drop the restriction the caller named and
|
|
112
|
+
* build a WIDER anchor than they described, without saying so. A member given as `null`
|
|
113
|
+
* is a malformed spec and is refused by the builder's own typed error, which the
|
|
114
|
+
* nested-record vector in schema-trustanchor.test.js pins.
|
|
115
|
+
*
|
|
116
|
+
* The RFC 5914 module is IMPLICIT TAGS, so a context tag REPLACES the member's own
|
|
102
117
|
* universal tag rather than wrapping it. `b.implicit` re-tags the value it is given; only the
|
|
103
118
|
* three members the module writes EXPLICIT beside get a wrapper. */
|
|
104
119
|
if (spec.certificate !== undefined) {
|
package/lib/tsp-sign.js
CHANGED
|
@@ -650,6 +650,9 @@ function _buildTsaChains(leaf, pool) {
|
|
|
650
650
|
* certificate when `certReq` was false) and an intermediate absent from the
|
|
651
651
|
* token can still verify and chain.
|
|
652
652
|
* @opts revocationChecker Passed through to `pki.path.validate`.
|
|
653
|
+
* @opts distrustedKeys SHA-256 digests of SubjectPublicKeyInfo DER (64 hexadecimal characters or
|
|
654
|
+
* 32 bytes each, see `pki.trust.spkiDigest`). A listed key on the TSA chain,
|
|
655
|
+
* the anchor included, fails the token with `tsp/untrusted-tsa`.
|
|
653
656
|
* @example
|
|
654
657
|
* var pair = await pki.key.generate("Ed25519");
|
|
655
658
|
* var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
|
|
@@ -664,7 +667,7 @@ function _buildTsaChains(leaf, pool) {
|
|
|
664
667
|
* res.valid; // boolean; pass opts.trustAnchors to also chain the TSA cert to a root
|
|
665
668
|
* res.genTime; // Date, read from the verified eContent
|
|
666
669
|
*/
|
|
667
|
-
var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, trustAnchors: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1, allowWeakDigests: 1 });
|
|
670
|
+
var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, trustAnchors: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1, allowWeakDigests: 1, distrustedKeys: 1 });
|
|
668
671
|
|
|
669
672
|
function _anyCritical(extensions) {
|
|
670
673
|
return guard.list.anyMatches(extensions, function (e) { return !!e && !!e.critical; });
|
|
@@ -684,6 +687,8 @@ async function verify(token, data, opts) {
|
|
|
684
687
|
if (opts.certs != null && (!Array.isArray(opts.certs) || !guard.list.allMatch(opts.certs, function (c) { return guard.bytes.isByteSource(c); }))) {
|
|
685
688
|
throw _err("tsp/bad-input", "pki.tsp.verify opts.certs must be an array of DER certificate Buffers");
|
|
686
689
|
}
|
|
690
|
+
/** @internal Typed here, ahead of the path step, whose throws all become `tsp/untrusted-tsa`. */
|
|
691
|
+
var distrustedKeys = guard.distrust.keys(opts.distrustedKeys, _err, "tsp/bad-input", "pki.tsp.verify opts.distrustedKeys");
|
|
687
692
|
var tokenDer = _tokenDer(token);
|
|
688
693
|
var parsed = schemaTsp.parseToken(tokenDer);
|
|
689
694
|
var tst = parsed.tstInfo;
|
|
@@ -732,6 +737,7 @@ async function verify(token, data, opts) {
|
|
|
732
737
|
async function validateAt(chain, when) {
|
|
733
738
|
var res = await pathValidate.validate(chain, {
|
|
734
739
|
time: when, trustAnchors: opts.trustAnchors, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
|
|
740
|
+
distrustedKeys: distrustedKeys,
|
|
735
741
|
});
|
|
736
742
|
if (_moreEstablished(res.revocationChecked, revocationChecked)) revocationChecked = res.revocationChecked;
|
|
737
743
|
if (anchorConstraints == null && res.anchorConstraints != null) anchorConstraints = res.anchorConstraints;
|
package/lib/webauthn-mds.js
CHANGED
|
@@ -122,7 +122,7 @@ var CERT_SCOPED_STATUS = intrinsic.freeze(intrinsic.assign(intrinsic.create(null
|
|
|
122
122
|
|
|
123
123
|
var _BLOB_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
124
124
|
rootCertificates: 1, time: 1, previousNo: 1, requireRollbackCheck: 1, allowStale: 1,
|
|
125
|
-
statusPolicy: 1, rejectUnknownStatus: 1,
|
|
125
|
+
statusPolicy: 1, rejectUnknownStatus: 1, distrustedKeys: 1,
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
function _isPlainObject(v) { return !!v && typeof v === "object" && !intrinsic.isArray(v); }
|
|
@@ -218,8 +218,9 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
218
218
|
if (!intrinsic.isArray(roots) || roots.length === 0) {
|
|
219
219
|
throw _err("webauthn/metadata-no-root", "verifying a metadata BLOB requires opts.rootCertificates -- the FIDO root(s) to anchor it to; this library bundles none");
|
|
220
220
|
}
|
|
221
|
-
if (o.time
|
|
222
|
-
var at = o.time
|
|
221
|
+
if (o.time != null) guard.time.assertValid(o.time, _err, "webauthn/bad-input", "opts.time");
|
|
222
|
+
var at = o.time == null ? new intrinsic.Date() : o.time;
|
|
223
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no rollback comparison is made, so a null must be refused rather than drop the check
|
|
223
224
|
if (o.previousNo !== undefined && (!intrinsic.isSafeInteger(o.previousNo) || o.previousNo < 0)) {
|
|
224
225
|
throw _err("webauthn/bad-input", "opts.previousNo must be a non-negative safe integer");
|
|
225
226
|
}
|
|
@@ -228,13 +229,18 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
228
229
|
throw _err("webauthn/bad-input", "opts." + k + " must be a boolean");
|
|
229
230
|
}
|
|
230
231
|
});
|
|
232
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means any status is accepted, so a null must be refused rather than widen the policy
|
|
231
233
|
if (o.statusPolicy !== undefined && typeof o.statusPolicy !== "function" &&
|
|
232
234
|
o.statusPolicy !== "any" && o.statusPolicy !== "latest-by-date") {
|
|
233
235
|
throw _err("webauthn/bad-input", "opts.statusPolicy must be \"any\", \"latest-by-date\", or a function");
|
|
234
236
|
}
|
|
237
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no rollback comparison is made, so a null must be refused rather than drop the check
|
|
235
238
|
if (o.requireRollbackCheck === true && o.previousNo === undefined) {
|
|
236
239
|
throw _err("webauthn/metadata-no-baseline", "opts.requireRollbackCheck was set without opts.previousNo, so there is no baseline to compare against");
|
|
237
240
|
}
|
|
241
|
+
/** @internal Typed here, before the chain walk, because the walk turns a validation rejection into
|
|
242
|
+
* an untrusted verdict, and a malformed list must be reported as the caller's input fault. */
|
|
243
|
+
var distrusted = guard.distrust.keys(o.distrustedKeys, _err, "webauthn/bad-input", "opts.distrustedKeys");
|
|
238
244
|
opts = o;
|
|
239
245
|
var anchors = intrinsic.mapList(roots, function (r, i) { return _asCert(r, "opts.rootCertificates[" + i + "]"); });
|
|
240
246
|
|
|
@@ -351,14 +357,17 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
351
357
|
}, function (e) { throw _err("webauthn/unsupported-algorithm", "the metadata BLOB x5c leaf key could not be imported for " + header.alg, e); });
|
|
352
358
|
var anchored = intrinsic.chain(verified, function (ok) {
|
|
353
359
|
if (!ok) throw _err("webauthn/verify-failed", "the metadata BLOB signature does not verify under its x5c leaf key");
|
|
354
|
-
return _chainToAnchor(chain, anchors, at);
|
|
360
|
+
return _chainToAnchor(chain, anchors, at, undefined, undefined, distrusted);
|
|
355
361
|
});
|
|
356
362
|
return intrinsic.chain(anchored, function () {
|
|
357
363
|
return _parsePayload(segs[1], at, opts);
|
|
358
364
|
});
|
|
359
365
|
}
|
|
360
366
|
|
|
361
|
-
|
|
367
|
+
/** @internal `distrusted` is a list `guard.distrust.keys` returned, or null. It reaches every
|
|
368
|
+
* validation below, and the self-presented anchor, which is trusted without one, is refused here
|
|
369
|
+
* when its key is listed. */
|
|
370
|
+
function _chainToAnchor(chain, anchors, at, what, code, distrusted) {
|
|
362
371
|
var subject = what || "metadata BLOB certificate chain";
|
|
363
372
|
var faultCode = code || "webauthn/metadata-untrusted";
|
|
364
373
|
var ordered = _reverse(intrinsic.copyList(chain));
|
|
@@ -381,12 +390,20 @@ function _chainToAnchor(chain, anchors, at, what, code) {
|
|
|
381
390
|
var path = intrinsic.copyList(ordered);
|
|
382
391
|
var strippedAnchor = false;
|
|
383
392
|
if (path.length && _isAnchorItself(path[0], anchor)) { path = intrinsic.copyList(path, 1); strippedAnchor = true; }
|
|
384
|
-
if (path.length === 0)
|
|
393
|
+
if (path.length === 0) {
|
|
394
|
+
var anchorRefusal = strippedAnchor ? guard.distrust.refusal(distrusted, anchor.subjectPublicKeyInfo.bytes, "opts.distrustedKeys") : null;
|
|
395
|
+
if (anchorRefusal !== null) {
|
|
396
|
+
lastFault = _err(faultCode, "the " + subject + " is a trust anchor whose key " + anchorRefusal);
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
return strippedAnchor;
|
|
400
|
+
}
|
|
385
401
|
var verdict = pathValidate.validate(path, {
|
|
386
402
|
time: at,
|
|
387
403
|
trustAnchors: { name: anchor.subject, publicKey: anchor.subjectPublicKeyInfo.bytes,
|
|
388
404
|
algorithm: anchor.subjectPublicKeyInfo.algorithm.oid,
|
|
389
405
|
parameters: anchor.subjectPublicKeyInfo.algorithm.parameters },
|
|
406
|
+
distrustedKeys: distrusted || null,
|
|
390
407
|
});
|
|
391
408
|
return intrinsic.chain(verdict, function (r) { return !!(r && r.valid); },
|
|
392
409
|
function (e) { lastFault = e; return false; });
|
|
@@ -443,6 +460,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
443
460
|
if (!_isPlainObject(payload)) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must be a JSON object");
|
|
444
461
|
if (typeof payload.legalHeader !== "string") throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a string legalHeader");
|
|
445
462
|
if (!intrinsic.isSafeInteger(payload.no) || payload.no < 0) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a non-negative integer no");
|
|
463
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no rollback comparison is made, so a null must be refused rather than drop the check
|
|
446
464
|
if (opts.previousNo !== undefined && payload.no <= opts.previousNo) {
|
|
447
465
|
throw _err("webauthn/metadata-rollback", "the metadata BLOB no " + payload.no + " does not exceed the previously held " + opts.previousNo);
|
|
448
466
|
}
|
|
@@ -525,7 +543,9 @@ function _parsePayload(seg, at, opts) {
|
|
|
525
543
|
});
|
|
526
544
|
var result = { no: payload.no, legalHeader: payload.legalHeader, nextUpdate: payload.nextUpdate,
|
|
527
545
|
stale: stale, allowStale: opts.allowStale === true,
|
|
546
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no rollback comparison is made, so a null must be refused rather than drop the check
|
|
528
547
|
rollbackChecked: opts.previousNo !== undefined,
|
|
548
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no rollback comparison is made, so a null must be refused rather than drop the check
|
|
529
549
|
previousNo: opts.previousNo === undefined ? null : opts.previousNo,
|
|
530
550
|
entries: entries, byAaguid: byAaguid, byKeyIdentifier: byKeyIdentifier,
|
|
531
551
|
statusPolicy: opts.statusPolicy || "any", rejectUnknownStatus: opts.rejectUnknownStatus === true };
|
|
@@ -558,7 +578,8 @@ function metadataAnchors(entry, opts) {
|
|
|
558
578
|
if (typeof opts !== "object" || intrinsic.isArray(opts)) throw _err("webauthn/bad-input", "metadataAnchors opts must be an object");
|
|
559
579
|
guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, _err, "webauthn/bad-input", "metadataAnchors opts has an unknown key ");
|
|
560
580
|
opts = guard.identifier.ownOptions(opts);
|
|
561
|
-
if (opts.time
|
|
581
|
+
if (opts.time != null) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
582
|
+
// allow:caller-option-presence-tested-against-undefined -- a trust input: absence means no metadata anchor is applied, so a null must be refused
|
|
562
583
|
if (opts.metadata !== undefined) {
|
|
563
584
|
if (!isVerifiedResult(opts.metadata)) {
|
|
564
585
|
throw _err("webauthn/bad-input", "metadataAnchors opts.metadata expects a verifyMetadataBlob result -- an object that merely resembles one has not been through the signature and chain checks");
|
|
@@ -571,7 +592,8 @@ function metadataAnchors(entry, opts) {
|
|
|
571
592
|
* freshness and every status report's effective date are compared against, so a replacement
|
|
572
593
|
* answering with an earlier one accepts expired metadata and suppresses a report already in
|
|
573
594
|
* effect. */
|
|
574
|
-
var at = opts.time
|
|
595
|
+
var at = opts.time == null ? new intrinsic.Date() : opts.time;
|
|
596
|
+
// allow:caller-option-presence-tested-against-undefined -- a trust input: absence means no metadata anchor is applied, so a null must be refused
|
|
575
597
|
if (opts.metadata !== undefined) assertFresh(opts.metadata, at, "metadataAnchors");
|
|
576
598
|
if (statusDenied(entry, opts.metadata, opts.certificate, at)) {
|
|
577
599
|
throw _err("webauthn/metadata-status", "the metadata entry for this authenticator carries a disqualifying status report, so it registers no anchors to trust");
|