@blamejs/pki 0.9.4 → 0.9.6
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 +60 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +19 -3
- package/lib/cmp-verify.js +25 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +10 -5
- package/lib/cms-encrypt.js +6 -1
- package/lib/cms-sign.js +16 -0
- package/lib/cms-verify.js +32 -14
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/csr-sign.js +6 -2
- package/lib/ct.js +16 -3
- package/lib/est.js +67 -31
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +224 -0
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +1 -1
- package/lib/http-transport.js +299 -36
- package/lib/jose.js +1 -1
- package/lib/lint.js +67 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +16 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +119 -18
- package/lib/pkcs12-build.js +6 -1
- package/lib/pki-build.js +38 -6
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +46 -11
- package/lib/scep.js +39 -14
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +32 -16
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +121 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +15 -2
- package/lib/webauthn-mds.js +29 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +26 -10
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
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,19 @@ 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
|
+
if (strippedAnchor && guard.distrust.has(distrusted, anchor.subjectPublicKeyInfo.bytes)) {
|
|
395
|
+
lastFault = _err(faultCode, "the " + subject + " is a trust anchor whose key is listed in opts.distrustedKeys");
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
return strippedAnchor;
|
|
399
|
+
}
|
|
385
400
|
var verdict = pathValidate.validate(path, {
|
|
386
401
|
time: at,
|
|
387
402
|
trustAnchors: { name: anchor.subject, publicKey: anchor.subjectPublicKeyInfo.bytes,
|
|
388
403
|
algorithm: anchor.subjectPublicKeyInfo.algorithm.oid,
|
|
389
404
|
parameters: anchor.subjectPublicKeyInfo.algorithm.parameters },
|
|
405
|
+
distrustedKeys: distrusted || null,
|
|
390
406
|
});
|
|
391
407
|
return intrinsic.chain(verdict, function (r) { return !!(r && r.valid); },
|
|
392
408
|
function (e) { lastFault = e; return false; });
|
|
@@ -443,6 +459,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
443
459
|
if (!_isPlainObject(payload)) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must be a JSON object");
|
|
444
460
|
if (typeof payload.legalHeader !== "string") throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a string legalHeader");
|
|
445
461
|
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");
|
|
462
|
+
// 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
463
|
if (opts.previousNo !== undefined && payload.no <= opts.previousNo) {
|
|
447
464
|
throw _err("webauthn/metadata-rollback", "the metadata BLOB no " + payload.no + " does not exceed the previously held " + opts.previousNo);
|
|
448
465
|
}
|
|
@@ -525,7 +542,9 @@ function _parsePayload(seg, at, opts) {
|
|
|
525
542
|
});
|
|
526
543
|
var result = { no: payload.no, legalHeader: payload.legalHeader, nextUpdate: payload.nextUpdate,
|
|
527
544
|
stale: stale, allowStale: opts.allowStale === true,
|
|
545
|
+
// 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
546
|
rollbackChecked: opts.previousNo !== undefined,
|
|
547
|
+
// 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
548
|
previousNo: opts.previousNo === undefined ? null : opts.previousNo,
|
|
530
549
|
entries: entries, byAaguid: byAaguid, byKeyIdentifier: byKeyIdentifier,
|
|
531
550
|
statusPolicy: opts.statusPolicy || "any", rejectUnknownStatus: opts.rejectUnknownStatus === true };
|
|
@@ -558,7 +577,8 @@ function metadataAnchors(entry, opts) {
|
|
|
558
577
|
if (typeof opts !== "object" || intrinsic.isArray(opts)) throw _err("webauthn/bad-input", "metadataAnchors opts must be an object");
|
|
559
578
|
guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, _err, "webauthn/bad-input", "metadataAnchors opts has an unknown key ");
|
|
560
579
|
opts = guard.identifier.ownOptions(opts);
|
|
561
|
-
if (opts.time
|
|
580
|
+
if (opts.time != null) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
581
|
+
// allow:caller-option-presence-tested-against-undefined -- a trust input: absence means no metadata anchor is applied, so a null must be refused
|
|
562
582
|
if (opts.metadata !== undefined) {
|
|
563
583
|
if (!isVerifiedResult(opts.metadata)) {
|
|
564
584
|
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 +591,8 @@ function metadataAnchors(entry, opts) {
|
|
|
571
591
|
* freshness and every status report's effective date are compared against, so a replacement
|
|
572
592
|
* answering with an earlier one accepts expired metadata and suppresses a report already in
|
|
573
593
|
* effect. */
|
|
574
|
-
var at = opts.time
|
|
594
|
+
var at = opts.time == null ? new intrinsic.Date() : opts.time;
|
|
595
|
+
// allow:caller-option-presence-tested-against-undefined -- a trust input: absence means no metadata anchor is applied, so a null must be refused
|
|
575
596
|
if (opts.metadata !== undefined) assertFresh(opts.metadata, at, "metadataAnchors");
|
|
576
597
|
if (statusDenied(entry, opts.metadata, opts.certificate, at)) {
|
|
577
598
|
throw _err("webauthn/metadata-status", "the metadata entry for this authenticator carries a disqualifying status report, so it registers no anchors to trust");
|
package/lib/webauthn.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
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 The `expected*` and `require*` options are SECURITY EXPECTATIONS, so presence
|
|
6
|
+
* is asked against `undefined` alone and a `null` is not read as an omission. A caller whose
|
|
7
|
+
* `expectedChallenge` arrives null, from an expired session or a missing field, must be
|
|
8
|
+
* refused rather than have the ceremony verified with the challenge comparison skipped:
|
|
9
|
+
* read as absence the check does not run, the verdict reports `checked.challenge` false, and
|
|
10
|
+
* the call still succeeds. `expectedTopOrigin: null` is a stated value on top of that, naming
|
|
11
|
+
* an unframed ceremony (WebAuthn sec. 5.8.1). */
|
|
4
12
|
/**
|
|
5
13
|
* @module pki.webauthn
|
|
6
14
|
* @nav Validation
|
|
@@ -413,7 +421,7 @@ function _aaguidIsSigned(fmt) { return _AAGUID_SIGNED_BY_FMT[fmt] !== false; }
|
|
|
413
421
|
var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
414
422
|
time: 1, metadata: 1, tpmPolicy: 1, safetyNetRoots: 1, verifySafetyNetJws: 1, requireCtsProfileMatch: 1,
|
|
415
423
|
expectedRpId: 1, requireUserPresence: 1, requireUserVerification: 1, allowedAlgorithms: 1,
|
|
416
|
-
rootCertificates: 1,
|
|
424
|
+
rootCertificates: 1, distrustedKeys: 1,
|
|
417
425
|
clientDataJSON: 1, expectedChallenge: 1, expectedOrigin: 1, expectedTopOrigin: 1,
|
|
418
426
|
});
|
|
419
427
|
|
|
@@ -484,12 +492,12 @@ function _applyCallerRoots(res, supplied, vopts, onlyPaths) {
|
|
|
484
492
|
throw _err("webauthn/anchor-not-applicable",
|
|
485
493
|
"opts.rootCertificates was supplied, but this attestation carries no trust path to anchor (format '" + res.fmt + "')");
|
|
486
494
|
}
|
|
487
|
-
var at = vopts.time
|
|
495
|
+
var at = vopts.time != null ? vopts.time : new intrinsic.Date();
|
|
488
496
|
var anchoredAll = intrinsic.reduce(paths, function (p, info) {
|
|
489
497
|
return intrinsic.chain(p, function () {
|
|
490
|
-
var pathAt = vopts.time
|
|
498
|
+
var pathAt = vopts.time != null ? vopts.time : (info.at || at);
|
|
491
499
|
return mds.chainToAnchor(_reverse(intrinsic.copyList(info.tp)), roots, pathAt,
|
|
492
|
-
"attestation trust path (against the roots supplied as opts.rootCertificates)");
|
|
500
|
+
"attestation trust path (against the roots supplied as opts.rootCertificates)", undefined, vopts.distrustedKeys);
|
|
493
501
|
});
|
|
494
502
|
}, _promiseResolve(_Promise, undefined));
|
|
495
503
|
return intrinsic.chain(anchoredAll, function () {
|
|
@@ -559,6 +567,7 @@ function _applyBindings(authData, coseKey, opts) {
|
|
|
559
567
|
"signature made by this credential would use SHA-1, so it is refused unless opts.allowedAlgorithms " +
|
|
560
568
|
"names -65535 explicitly");
|
|
561
569
|
}
|
|
570
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means every supported algorithm is accepted, so a null must be refused rather than drop the allowlist
|
|
562
571
|
if (opts.allowedAlgorithms !== undefined) {
|
|
563
572
|
/** @internal The entries are required to be the list's OWN. A hole answers from the prototype at
|
|
564
573
|
* that index, and the universal test skips it, so a sparse list with a number installed on
|
|
@@ -720,7 +729,7 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
|
|
|
720
729
|
if (!intrinsic.isArray(roots) || roots.length === 0) {
|
|
721
730
|
throw _err("webauthn/safetynet-no-root", "verifying an android-safetynet attestation requires opts.safetyNetRoots -- the Google root(s) to anchor the x5c chain to; this library bundles none (WebAuthn 8.5)");
|
|
722
731
|
}
|
|
723
|
-
if (opts.time
|
|
732
|
+
if (opts.time != null) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
724
733
|
|
|
725
734
|
_requireAttShape(att.attStmt, ["ver", "response"], ["ver", "response"]);
|
|
726
735
|
_attRead(att.attStmt, "ver", cbor.read.textString, "a text string");
|
|
@@ -783,10 +792,10 @@ var VERIFIERS = intrinsic.assign(intrinsic.create(null), {
|
|
|
783
792
|
var jwsTrusted = intrinsic.chain(_verifySig(-257, sigBytes, leaf.subjectPublicKeyInfo.bytes,
|
|
784
793
|
intrinsic.bufferFrom(segs[0] + "." + segs[1], "ascii"), _err), function (ok) {
|
|
785
794
|
if (!ok) throw _err("webauthn/verify-failed", "the android-safetynet JWS signature does not verify under the x5c leaf key");
|
|
786
|
-
chainAt = opts.time
|
|
795
|
+
chainAt = opts.time != null ? opts.time
|
|
787
796
|
: (typeof payload.timestampMs === "number" && intrinsic.isFinite(payload.timestampMs) && payload.timestampMs > 0
|
|
788
797
|
? new intrinsic.Date(payload.timestampMs) : undefined);
|
|
789
|
-
return _safetyNetChainTrusted(chain, roots, chainAt);
|
|
798
|
+
return _safetyNetChainTrusted(chain, roots, chainAt, opts.distrustedKeys);
|
|
790
799
|
});
|
|
791
800
|
return intrinsic.chain(jwsTrusted, function () {
|
|
792
801
|
var res = guard.verdict.of(_result("android-safetynet", "Basic", chain, att), { safetyNet: signals });
|
|
@@ -879,7 +888,7 @@ function _safetyNetHostnameOk(leaf) {
|
|
|
879
888
|
});
|
|
880
889
|
}
|
|
881
890
|
|
|
882
|
-
function _safetyNetChainTrusted(chain, roots, time) {
|
|
891
|
+
function _safetyNetChainTrusted(chain, roots, time, distrusted) {
|
|
883
892
|
var anchors;
|
|
884
893
|
try {
|
|
885
894
|
anchors = intrinsic.mapList(roots, function (root, i) {
|
|
@@ -892,7 +901,7 @@ function _safetyNetChainTrusted(chain, roots, time) {
|
|
|
892
901
|
});
|
|
893
902
|
} catch (e) { return _promiseReject(_Promise, e); }
|
|
894
903
|
return mds.chainToAnchor(chain, anchors, time === undefined ? new intrinsic.Date() : time,
|
|
895
|
-
"android-safetynet x5c certificate chain", "webauthn/safetynet-cert-untrusted");
|
|
904
|
+
"android-safetynet x5c certificate chain", "webauthn/safetynet-cert-untrusted", distrusted);
|
|
896
905
|
}
|
|
897
906
|
|
|
898
907
|
function _result(fmt, attestationType, chain, att) {
|
|
@@ -1015,6 +1024,9 @@ function _checkAndroidKeyDescription(cert, clientDataHash) {
|
|
|
1015
1024
|
* allowedAlgorithms -- the COSE algorithms this relying party accepts
|
|
1016
1025
|
* metadata -- a verifyMetadataBlob result; the model's own roots govern
|
|
1017
1026
|
* rootCertificates -- trust anchors you pin, for the models no catalogue lists
|
|
1027
|
+
* distrustedKeys -- SHA-256 digests of SubjectPublicKeyInfo DER (64 hex characters
|
|
1028
|
+
* or 32 bytes); a listed key on a trust path refuses that path on
|
|
1029
|
+
* the metadata, rootCertificates and safetyNetRoots routes
|
|
1018
1030
|
* time -- the instant certificate validity is judged at
|
|
1019
1031
|
* tpmPolicy -- required TPM key properties; refuses a non-TPM attestation
|
|
1020
1032
|
* safetyNetRoots / verifySafetyNetJws / requireCtsProfileMatch -- the
|
|
@@ -1045,11 +1057,16 @@ function verify(attestationObject, clientDataHash, opts) {
|
|
|
1045
1057
|
/** @internal A caller who passes no options gets a record with NO prototype. Defaulted to `{}` it
|
|
1046
1058
|
* inherits, so a property installed on `Object.prototype` reads as an option this call never set. */
|
|
1047
1059
|
opts = opts || intrinsic.create(null);
|
|
1060
|
+
var distrusted;
|
|
1048
1061
|
try {
|
|
1049
1062
|
if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
|
|
1050
1063
|
guard.identifier.assertKnownKeys(opts, _VERIFY_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
|
|
1051
1064
|
opts = guard.identifier.ownOptions(opts);
|
|
1052
|
-
if (opts.time
|
|
1065
|
+
if (opts.time != null) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
1066
|
+
/** @internal Typed here, before any chain is walked, because the walk turns a validation
|
|
1067
|
+
* rejection into an untrusted verdict, and a malformed list must be reported as the caller's
|
|
1068
|
+
* input fault on every route. */
|
|
1069
|
+
distrusted = guard.distrust.keys(opts.distrustedKeys, _err, "webauthn/bad-input", "opts.distrustedKeys");
|
|
1053
1070
|
/** @internal The walk comes from the capture. Each pass of it is a REFUSAL of a mistyped option,
|
|
1054
1071
|
* and a mistyped one that survives is read for truthiness later where the rule wants exactly
|
|
1055
1072
|
* `true`, so a walk that visits nothing admits `"true"` as the opt-in. */
|
|
@@ -1061,7 +1078,7 @@ function verify(attestationObject, clientDataHash, opts) {
|
|
|
1061
1078
|
var attBytes, cdh, clientData = null;
|
|
1062
1079
|
try {
|
|
1063
1080
|
attBytes = _bytesArg(attestationObject, "attestationObject");
|
|
1064
|
-
var haveJson = opts.clientDataJSON
|
|
1081
|
+
var haveJson = opts.clientDataJSON != null, haveHash = clientDataHash !== undefined;
|
|
1065
1082
|
if (haveJson === haveHash) {
|
|
1066
1083
|
throw _err("webauthn/bad-input", "verify takes exactly one of clientDataJSON or the clientDataHash argument");
|
|
1067
1084
|
}
|
|
@@ -1075,6 +1092,9 @@ function verify(attestationObject, clientDataHash, opts) {
|
|
|
1075
1092
|
});
|
|
1076
1093
|
cdh = _sha("sha256", cdjBytes);
|
|
1077
1094
|
} else {
|
|
1095
|
+
/** @internal `expectedTopOrigin: null` states that the ceremony must be unframed, so
|
|
1096
|
+
* it is an expectation the caller supplied and counts as supplied here too. */
|
|
1097
|
+
// allow:caller-option-presence-tested-against-undefined
|
|
1078
1098
|
if (opts.expectedChallenge !== undefined || opts.expectedOrigin !== undefined ||
|
|
1079
1099
|
opts.expectedTopOrigin !== undefined) {
|
|
1080
1100
|
throw _err("webauthn/bad-input",
|
|
@@ -1093,6 +1113,7 @@ function verify(attestationObject, clientDataHash, opts) {
|
|
|
1093
1113
|
}
|
|
1094
1114
|
var verifier = VERIFIERS[att.fmt];
|
|
1095
1115
|
if (!verifier) return _promiseReject(_Promise, _err("webauthn/unsupported-format", "attestation statement format '" + att.fmt + "' is not supported"));
|
|
1116
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no TPM policy is applied, so a null must be refused rather than drop it
|
|
1096
1117
|
if (opts.tpmPolicy !== undefined && !_formatCarries(att, "tpm")) {
|
|
1097
1118
|
return _promiseReject(_Promise, _err("webauthn/tpm-policy", "opts.tpmPolicy requires a TPM attestation, but this attestation is format '" + att.fmt + "', which carries no TPM public area"));
|
|
1098
1119
|
}
|
|
@@ -1105,6 +1126,7 @@ function verify(attestationObject, clientDataHash, opts) {
|
|
|
1105
1126
|
vopts = guard.identifier.ownOptions(opts);
|
|
1106
1127
|
vopts.rootCertificates = _snapshotRoots(opts.rootCertificates);
|
|
1107
1128
|
vopts.safetyNetRoots = _snapshotRoots(opts.safetyNetRoots);
|
|
1129
|
+
vopts.distrustedKeys = distrusted;
|
|
1108
1130
|
} catch (e) { return _promiseReject(_Promise, e); }
|
|
1109
1131
|
var ranVerifier = intrinsic.chain(_promiseResolve(_Promise, undefined),
|
|
1110
1132
|
function () { return verifier(att, clientDataHash, vopts); });
|
|
@@ -1112,7 +1134,9 @@ function verify(attestationObject, clientDataHash, opts) {
|
|
|
1112
1134
|
return guard.verdict.of(res, { clientData: clientData });
|
|
1113
1135
|
});
|
|
1114
1136
|
var anchoredRes = intrinsic.chain(withClientData, function (res) {
|
|
1137
|
+
// allow:caller-option-presence-tested-against-undefined -- a trust input: absence falls back to signature-only verification, so a null must be refused rather than skip anchoring
|
|
1115
1138
|
if (vopts.metadata === undefined) {
|
|
1139
|
+
// allow:caller-option-presence-tested-against-undefined -- a trust input: absence falls back to signature-only verification, so a null must be refused rather than skip anchoring
|
|
1116
1140
|
if (vopts.rootCertificates !== undefined) return _applyCallerRoots(res, vopts.rootCertificates, vopts);
|
|
1117
1141
|
var anchored = guard.verdict.of(res, { anchoredTo: _anchoredRoutes(res, null) });
|
|
1118
1142
|
var sn = _safetyNetAnchored(anchored);
|
|
@@ -1127,6 +1151,7 @@ function verify(attestationObject, clientDataHash, opts) {
|
|
|
1127
1151
|
function () { return _applyMetadata(res, att, vopts); });
|
|
1128
1152
|
return intrinsic.chain(applyMd,
|
|
1129
1153
|
function (out) { return guard.verdict.of(out, { anchoredTo: _anchoredRoutes(out, "metadata") }); }, function (e) {
|
|
1154
|
+
// allow:caller-option-presence-tested-against-undefined -- a trust input: absence falls back to signature-only verification, so a null must be refused rather than skip anchoring
|
|
1130
1155
|
if (!e || e.code !== "webauthn/metadata-not-found" || vopts.rootCertificates === undefined) throw e;
|
|
1131
1156
|
return intrinsic.chain(_promiseResolve(_Promise, _applyCallerRoots(res, vopts.rootCertificates, vopts, e.missedPaths)),
|
|
1132
1157
|
function (withRoots) {
|
|
@@ -1186,7 +1211,7 @@ function _applyMetadata(res, att, opts) {
|
|
|
1186
1211
|
guard.list.append(applied, { entry: entry, anchors: anchors, identifier: identifier });
|
|
1187
1212
|
return { anchors: anchors, identifier: identifier };
|
|
1188
1213
|
}
|
|
1189
|
-
var at = opts.time
|
|
1214
|
+
var at = opts.time != null ? opts.time : new intrinsic.Date();
|
|
1190
1215
|
mds.assertFresh(md, at, "the metadata supplied as opts.metadata");
|
|
1191
1216
|
var governed = [];
|
|
1192
1217
|
var missed = null;
|
|
@@ -1207,9 +1232,9 @@ function _applyMetadata(res, att, opts) {
|
|
|
1207
1232
|
var governedAnchored = intrinsic.reduce(governed, function (p, item) {
|
|
1208
1233
|
return intrinsic.chain(p, function () {
|
|
1209
1234
|
var g = item.g, info = item.info;
|
|
1210
|
-
var pathAt = opts.time
|
|
1235
|
+
var pathAt = opts.time != null ? opts.time : (info.at || at);
|
|
1211
1236
|
return mds.chainToAnchor(_reverse(intrinsic.copyList(info.tp)), g.anchors, pathAt,
|
|
1212
|
-
"attestation trust path for " + g.identifier + " (against the roots its metadata entry registers)");
|
|
1237
|
+
"attestation trust path for " + g.identifier + " (against the roots its metadata entry registers)", undefined, opts.distrustedKeys);
|
|
1213
1238
|
});
|
|
1214
1239
|
}, _promiseResolve(_Promise, undefined));
|
|
1215
1240
|
return intrinsic.chain(governedAnchored,
|
|
@@ -1272,6 +1297,9 @@ void constants;
|
|
|
1272
1297
|
* @opts
|
|
1273
1298
|
* - `rootCertificates` -- REQUIRED. The trust anchors the BLOB's own signing chain
|
|
1274
1299
|
* must reach. A certificate, PEM, or DER bytes.
|
|
1300
|
+
* - `distrustedKeys` -- SHA-256 digests of SubjectPublicKeyInfo DER (64 hex characters
|
|
1301
|
+
* or 32 bytes). A listed key anywhere on the BLOB's signing chain, the root
|
|
1302
|
+
* included, refuses the BLOB as `webauthn/metadata-untrusted`.
|
|
1275
1303
|
* - `time` -- the instant freshness is judged at. Defaults to now.
|
|
1276
1304
|
* - `previousNo` -- the sequence number of the BLOB you already hold. A BLOB whose
|
|
1277
1305
|
* `no` is not greater is refused as a rollback.
|
|
@@ -1507,6 +1535,10 @@ function parseClientData(bytes, opts) {
|
|
|
1507
1535
|
}
|
|
1508
1536
|
checked.origin = true;
|
|
1509
1537
|
}
|
|
1538
|
+
/** @internal `null` is a VALUE here, not absence: it states that the ceremony must be
|
|
1539
|
+
* unframed, which the branch below checks. So presence is asked the one way that keeps
|
|
1540
|
+
* it, and an option left out is the only form that skips the check. */
|
|
1541
|
+
// allow:caller-option-presence-tested-against-undefined -- see above
|
|
1510
1542
|
if (opts.expectedTopOrigin !== undefined) {
|
|
1511
1543
|
var wantTop = opts.expectedTopOrigin;
|
|
1512
1544
|
var framed = doc.crossOrigin === true;
|
package/lib/x509-sign.js
CHANGED
|
@@ -228,6 +228,10 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
228
228
|
return "unknown extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form for a custom extension";
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
/** @internal The criticality flags, typed at this door and read from the record below: the
|
|
232
|
+
* emitters took them off the caller's spec, where a value that was never a boolean selected a
|
|
233
|
+
* criticality nobody asked for. */
|
|
234
|
+
var crit = _b.criticality(extSpec);
|
|
231
235
|
var bc = extSpec.basicConstraints;
|
|
232
236
|
if (bc != null) _validateBcSpec(bc);
|
|
233
237
|
var caTrue = !!(bc && bc.cA === true);
|
|
@@ -257,14 +261,14 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
257
261
|
var akiF = _akiFields(akiVal, ctx);
|
|
258
262
|
_push(out,_ext(O("authorityKeyIdentifier"), false, _extAki(akiF[0], akiF[1], akiF[2])));
|
|
259
263
|
}
|
|
260
|
-
if (ku != null) _push(out,_ext(O("keyUsage"),
|
|
261
|
-
if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"),
|
|
264
|
+
if (ku != null) _push(out,_ext(O("keyUsage"), crit.keyUsageCritical, _extKeyUsage(ku)));
|
|
265
|
+
if (extSpec.extendedKeyUsage != null) _push(out,_ext(O("extKeyUsage"), crit.extendedKeyUsageCritical, _extExtKeyUsage(extSpec.extendedKeyUsage)));
|
|
262
266
|
if (bc != null) {
|
|
263
267
|
if (bc.cA === true && bc.critical === false) throw _err("x509/bad-input", "a CA certificate's basicConstraints MUST be critical (RFC 5280 sec. 4.2.1.9)");
|
|
264
268
|
_push(out,_ext(O("basicConstraints"), bc.critical !== false, _extBasicConstraints(bc)));
|
|
265
269
|
}
|
|
266
270
|
if (extSpec.subjectAltName != null) _push(out,_ext(O("subjectAltName"), ctx.subjectEmpty, _extSan(extSpec.subjectAltName)));
|
|
267
|
-
if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"),
|
|
271
|
+
if (extSpec.certificatePolicies != null) _push(out,_ext(O("certificatePolicies"), crit.certificatePoliciesCritical, _extCertPolicies(extSpec.certificatePolicies)));
|
|
268
272
|
/** @internal Sections 4.2.2.1 and 4.2.1.15 fix these non-critical; sec. 4.2.1.13 states a SHOULD
|
|
269
273
|
* and the toolkit issues the conforming form. */
|
|
270
274
|
if (extSpec.authorityInfoAccess != null) guard.list.append(out, _ext(O("authorityInfoAccess"), false, _extAia(extSpec.authorityInfoAccess)));
|
|
@@ -279,15 +283,15 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
279
283
|
* does not process the extension, so a critical one is a certificate this toolkit's own validator
|
|
280
284
|
* must reject and pki.lint.certificate grades `unknown-critical-extension`. The default is the
|
|
281
285
|
* usable form and the knob reaches the SHOULD-conforming one, as certificatePolicies does. */
|
|
282
|
-
if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"),
|
|
286
|
+
if (extSpec.policyMappings != null) guard.list.append(out, _ext(O("policyMappings"), crit.policyMappingsCritical, _extPolicyMappings(extSpec.policyMappings)));
|
|
283
287
|
if (extSpec.issuerAltName != null) guard.list.append(out, _ext(O("issuerAltName"), false, _encodeGeneralNames(_reqDenseArray(extSpec.issuerAltName, "issuerAltName"))));
|
|
284
288
|
/** @internal pki.path.validate does not process qcStatements, so a critical one is rejected as an
|
|
285
289
|
* unrecognized critical extension; the default is the form that validates and the knob reaches
|
|
286
290
|
* the other, as policyMappings and certificatePolicies do. */
|
|
287
|
-
if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"),
|
|
291
|
+
if (extSpec.qcStatements != null) guard.list.append(out, _ext(O("qcStatements"), crit.qcStatementsCritical, _extQcStatements(extSpec.qcStatements)));
|
|
288
292
|
/** @internal RFC 7633 sec. 4: SHOULD NOT be critical, and the clause names the case where critical
|
|
289
293
|
* is the desired behavior, so the knob exists and the default is the compatible form. */
|
|
290
|
-
if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"),
|
|
294
|
+
if (extSpec.tlsFeature != null) guard.list.append(out, _ext(O("tlsFeature"), crit.tlsFeatureCritical, _b.extTlsFeature(extSpec.tlsFeature)));
|
|
291
295
|
if (extSpec.nameConstraints != null) {
|
|
292
296
|
if (!caTrue) throw _err("x509/bad-input", "nameConstraints appears only in a CA certificate, so it requires basicConstraints cA=TRUE (RFC 5280 sec. 4.2.1.10)");
|
|
293
297
|
guard.list.append(out, _ext(O("nameConstraints"), true, _extNameConstraints(extSpec.nameConstraints)));
|
|
@@ -565,6 +569,14 @@ function _hasCriticalSan(extSpec) {
|
|
|
565
569
|
* (positive, <= 20 octets), the validity UTCTime/GeneralizedTime cutover, the DER DEFAULT omissions
|
|
566
570
|
* (v1 tag, `critical=FALSE`, `cA=FALSE`), and the CA cross-field rules (keyCertSign and
|
|
567
571
|
* pathLenConstraint require cA=TRUE) are all enforced; a violation throws a typed `CertificateError`.
|
|
572
|
+
*
|
|
573
|
+
* Criticality is stated per extension: `keyUsageCritical` (default true), and
|
|
574
|
+
* `extendedKeyUsageCritical`, `certificatePoliciesCritical`, `policyMappingsCritical`,
|
|
575
|
+
* `qcStatementsCritical` and `tlsFeatureCritical` (each default false). `basicConstraints` carries
|
|
576
|
+
* its own `critical` inside the value. Each is a boolean: a value other than `true`, `false`,
|
|
577
|
+
* `null` or an absent flag is refused `x509/bad-input`. The extensions RFC 5280 fixes one way
|
|
578
|
+
* (`nameConstraints` critical, `authorityInfoAccess` and `cRLDistributionPoints` non-critical) take
|
|
579
|
+
* no flag.
|
|
568
580
|
* Where the spec carries raw DER (a `Name` Buffer, a pre-encoded `Extension`, an issuer `publicKey`
|
|
569
581
|
* SPKI), a structural fault throws `CertificateError`, while a malformed leaf inside those bytes
|
|
570
582
|
* throws `Asn1Error`, the same two-error contract the parsers present.
|
|
@@ -959,14 +971,18 @@ function _extensionRow(name) {
|
|
|
959
971
|
if (oid.isDottedDecimal(name)) return intrinsic.hasOwn(EXTENSION_BY_OID, name) ? EXTENSION_BY_OID[name] : null;
|
|
960
972
|
throw _err("x509/bad-input", "unknown extension " + guard.text.showValue(name) + "; the names spec.extensions takes are accepted, and any other extension is named by its dotted OID with its extnValue pre-encoded");
|
|
961
973
|
}
|
|
974
|
+
/** @internal `stated` is the caller's option, carried here from the door that typed it,
|
|
975
|
+
* so absence has the same two forms there and here: a `null` states no criticality and
|
|
976
|
+
* takes the row's default, the way an omitted option does. Read as stated, it was
|
|
977
|
+
* returned AS the criticality and reached the emitter as a non-boolean. */
|
|
962
978
|
function _extensionCritical(row, opts) {
|
|
963
979
|
var rule = row.rule, stated = opts.critical;
|
|
964
980
|
if (rule.inValue) {
|
|
965
|
-
if (stated
|
|
981
|
+
if (stated != null) throw _err("x509/bad-input", row.name + " carries its criticality in the value ({ cA, pathLen, critical }), so opts.critical does not apply");
|
|
966
982
|
return null;
|
|
967
983
|
}
|
|
968
|
-
if (rule.knob) return stated
|
|
969
|
-
if (stated
|
|
984
|
+
if (rule.knob) return stated == null ? rule.dflt : stated;
|
|
985
|
+
if (stated != null && stated !== rule.fixed) {
|
|
970
986
|
throw _err("x509/bad-input", row.name + " is emitted " + (rule.fixed ? "critical" : "non-critical") + " (" + rule.cite + "), so opts.critical cannot say otherwise");
|
|
971
987
|
}
|
|
972
988
|
return rule.fixed;
|
|
@@ -1025,7 +1041,7 @@ function extension(name, value, opts) {
|
|
|
1025
1041
|
function _extension(name, value, opts) {
|
|
1026
1042
|
opts = guard.identifier.optionsObject(opts, _err, "x509/bad-input", "pki.x509.extension options");
|
|
1027
1043
|
guard.identifier.assertKnownKeys(opts, KNOWN_EXTENSION_OPTS, _err, "x509/bad-input", "pki.x509.extension has an unknown option ");
|
|
1028
|
-
if (opts.critical
|
|
1044
|
+
if (opts.critical != null && typeof opts.critical !== "boolean") throw _err("x509/bad-input", "opts.critical must be a boolean");
|
|
1029
1045
|
if (value === undefined || value === null) throw _err("x509/bad-input", "the extension value is required");
|
|
1030
1046
|
var row = _extensionRow(name);
|
|
1031
1047
|
if (row === null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "blamejs contributors",
|
|
@@ -87,6 +87,6 @@
|
|
|
87
87
|
"devDependencies": {
|
|
88
88
|
"c8": "12.0.0",
|
|
89
89
|
"esbuild": "0.28.2",
|
|
90
|
-
"eslint": "10.
|
|
90
|
+
"eslint": "10.12.0"
|
|
91
91
|
}
|
|
92
92
|
}
|
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:114691ca-67be-44e1-9f29-b49116312f56",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-21T08:20:31.429Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.9.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.9.6",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.9.
|
|
25
|
+
"version": "0.9.6",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.9.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.9.6",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.9.
|
|
57
|
+
"ref": "@blamejs/pki@0.9.6",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|