@blamejs/pki 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -1
- package/README.md +1 -1
- package/lib/constants.js +7 -4
- package/lib/guard-bytes.js +6 -2
- package/lib/guard-name.js +15 -0
- package/lib/jose.js +39 -0
- package/lib/sign-scheme.js +45 -6
- package/lib/validator-cose.js +39 -0
- package/lib/webauthn-mds.js +207 -23
- package/lib/webauthn.js +370 -140
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/webauthn-mds.js
CHANGED
|
@@ -27,6 +27,8 @@ var jose = require("./jose");
|
|
|
27
27
|
var rfc3339 = require("./rfc3339");
|
|
28
28
|
var constants = require("./constants");
|
|
29
29
|
var pathValidate = require("./path-validate");
|
|
30
|
+
var signScheme = require("./sign-scheme");
|
|
31
|
+
var edwardsPoint = require("./edwards-point");
|
|
30
32
|
var webcrypto = require("./webcrypto");
|
|
31
33
|
var nodeCrypto = require("crypto");
|
|
32
34
|
|
|
@@ -34,17 +36,85 @@ var WebauthnError = frameworkError.WebauthnError;
|
|
|
34
36
|
function _err(code, message, cause) { return new WebauthnError(code, message, cause); }
|
|
35
37
|
var C = constants.LIMITS;
|
|
36
38
|
|
|
37
|
-
//
|
|
39
|
+
// Which signature schemes each certificate key type may perform, keyed by the SPKI algorithm the
|
|
40
|
+
// x5c leaf carries. Taking the scheme from the token and the key from the chain without checking
|
|
41
|
+
// they agree is the JWS algorithm-confusion class, and the relation is not one-to-one: a
|
|
42
|
+
// PKCS#1-encoded RSA key can do either RSA scheme, while an id-RSASSA-PSS key is restricted BY ITS
|
|
43
|
+
// CERTIFICATE to RSASSA-PSS and must not verify an RS* signature (RFC 4055 sec. 1.2 / 3.1).
|
|
44
|
+
//
|
|
45
|
+
// The Edwards and ML-DSA rows exist because an X.509 SubjectPublicKeyInfo carries those keys
|
|
46
|
+
// perfectly well -- RFC 8410 for Ed25519 / Ed448, RFC 9881 for ML-DSA -- so a JWS algorithm over
|
|
47
|
+
// one is a conformant BLOB signature, not a theoretical one. `EdDSA` names a scheme without fixing
|
|
48
|
+
// a curve (RFC 8037), so for that scheme the certificate decides which of the two it is; every
|
|
49
|
+
// other row's algorithm fixes its own.
|
|
50
|
+
var LEAF_SCHEMES_BY_SPKI_ALG = Object.assign(Object.create(null), {
|
|
51
|
+
ecPublicKey: { ECDSA: 1 },
|
|
52
|
+
rsaEncryption: { "RSASSA-PKCS1-v1_5": 1, "RSA-PSS": 1 },
|
|
53
|
+
rsassaPss: { "RSA-PSS": 1 },
|
|
54
|
+
Ed25519: { EdDSA: 1 },
|
|
55
|
+
Ed448: { EdDSA: 1 },
|
|
56
|
+
"id-ml-dsa-44": { "ML-DSA-44": 1 },
|
|
57
|
+
"id-ml-dsa-65": { "ML-DSA-65": 1 },
|
|
58
|
+
"id-ml-dsa-87": { "ML-DSA-87": 1 },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// The sign-scheme resolver's faults arrive as a bare kind; they keep webauthn/* codes here so a
|
|
62
|
+
// BLOB whose leaf pins a hash this toolkit cannot read fails as a webauthn verdict, not a foreign one.
|
|
63
|
+
function _schemeE(kind, message, cause) { return new WebauthnError("webauthn/" + kind, message, cause); }
|
|
64
|
+
|
|
65
|
+
// The JWS algorithms a metadata BLOB may be signed with, each with the key family it requires and
|
|
66
|
+
// the WebCrypto parameters that import an x5c leaf under it and verify with it.
|
|
67
|
+
//
|
|
68
|
+
// DERIVED, not declared. A second algorithm table maintained beside pki.jose's is how PS256 came
|
|
69
|
+
// to verify as an ACME signature and be refused as a metadata BLOB signature: both tables were
|
|
70
|
+
// correct on the day they were written and only one of them was extended. Deriving means the
|
|
71
|
+
// question "which algorithms does this toolkit accept for a JWS?" has exactly one answer, and a
|
|
72
|
+
// row added there reaches here without anybody remembering to copy it.
|
|
73
|
+
//
|
|
74
|
+
// The derivation is TOTAL over the registry -- every algorithm pki.jose verifies gets a row, and an
|
|
75
|
+
// X.509 SubjectPublicKeyInfo can carry a key of every type in it. EC becomes ECDSA over the curve
|
|
76
|
+
// the alg fixes; RSA with a salt length becomes RSASSA-PSS (RFC 7518 sec. 3.5) and without one
|
|
77
|
+
// RSASSA-PKCS1-v1_5; OKP becomes EdDSA, whose curve the certificate supplies because the algorithm
|
|
78
|
+
// does not name one; AKP becomes the ML-DSA parameter set the algorithm itself fixes.
|
|
79
|
+
//
|
|
80
|
+
// A row's import and verify parameters may therefore depend on the LEAF as well as the alg, which
|
|
81
|
+
// is why they are resolved per-verification rather than frozen into the row: EdDSA over an Ed25519
|
|
82
|
+
// certificate and EdDSA over an Ed448 certificate are the same JWS algorithm and two different
|
|
83
|
+
// WebCrypto algorithms.
|
|
84
|
+
//
|
|
38
85
|
// Null-prototype: `alg` is attacker-supplied, and an inherited Object member would otherwise
|
|
39
86
|
// resolve to a truthy non-row and read as a recognised algorithm.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
87
|
+
function _deriveBlobAlgs() {
|
|
88
|
+
var out = Object.create(null);
|
|
89
|
+
jose.sigAlgs().forEach(function (row) {
|
|
90
|
+
if (row.kty === "EC") {
|
|
91
|
+
out[row.alg] = { scheme: "ECDSA", hash: row.hash,
|
|
92
|
+
imp: { name: "ECDSA", namedCurve: row.crv }, ver: { name: "ECDSA", hash: row.hash } };
|
|
93
|
+
} else if (row.kty === "RSA" && row.saltLength) {
|
|
94
|
+
out[row.alg] = { scheme: "RSA-PSS", hash: row.hash,
|
|
95
|
+
imp: { name: "RSA-PSS", hash: row.hash }, ver: { name: "RSA-PSS", saltLength: row.saltLength } };
|
|
96
|
+
} else if (row.kty === "RSA") {
|
|
97
|
+
out[row.alg] = { scheme: "RSASSA-PKCS1-v1_5", hash: row.hash,
|
|
98
|
+
imp: { name: "RSASSA-PKCS1-v1_5", hash: row.hash }, ver: { name: "RSASSA-PKCS1-v1_5" } };
|
|
99
|
+
} else if (row.kty === "OKP") {
|
|
100
|
+
// `fromLeaf` marks a row whose WebCrypto algorithm is the certificate's own key algorithm.
|
|
101
|
+
out[row.alg] = { scheme: "EdDSA", hash: null, fromLeaf: true };
|
|
102
|
+
} else if (row.kty === "AKP") {
|
|
103
|
+
out[row.alg] = { scheme: row.alg, hash: null, imp: { name: row.alg }, ver: { name: row.alg } };
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
var BLOB_ALGS = _deriveBlobAlgs();
|
|
109
|
+
|
|
110
|
+
// The WebCrypto import and verify parameters for one verification. Fixed by the algorithm for every
|
|
111
|
+
// scheme that names its own curve or parameter set; taken from the certificate for EdDSA, which
|
|
112
|
+
// does not. The leaf has already been checked to permit this row's scheme, so its algorithm name is
|
|
113
|
+
// one of the two the scheme covers.
|
|
114
|
+
function _blobAlgParams(algRow, leafAlgName) {
|
|
115
|
+
if (!algRow.fromLeaf) return algRow;
|
|
116
|
+
return { scheme: algRow.scheme, hash: null, imp: { name: leafAlgName }, ver: { name: leafAlgName } };
|
|
117
|
+
}
|
|
48
118
|
|
|
49
119
|
// The status values that deny trust (MDS v3.0 sec. 3.1.4). An unknown status is IGNORED for the
|
|
50
120
|
// gate and surfaced raw -- the specification requires that a verifier not fail on a status it does
|
|
@@ -78,6 +148,15 @@ function _isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArr
|
|
|
78
148
|
var _verifiedResults = new WeakSet();
|
|
79
149
|
function isVerifiedResult(v) { return _isPlainObject(v) && _verifiedResults.has(v); }
|
|
80
150
|
|
|
151
|
+
// Which catalogue each entry came OUT of. Knowing that a supplied catalogue is verified says
|
|
152
|
+
// nothing about whether the entry being judged belongs to it, and a process may hold several: pair
|
|
153
|
+
// an entry from catalogue A with catalogue B and B's statusPolicy and freshness decide about A's
|
|
154
|
+
// status reports. B reading `latest-by-date` can then hand back anchors that A, reading `any`,
|
|
155
|
+
// records as revoked -- and a stale A can be judged current because B is. Keyed on object identity,
|
|
156
|
+
// for the same reason the provenance set is, and holding no strong reference to either side.
|
|
157
|
+
var _entryOrigin = new WeakMap();
|
|
158
|
+
function _isEntryOf(entry, metadata) { return _entryOrigin.get(entry) === metadata; }
|
|
159
|
+
|
|
81
160
|
// Provenance alone is not enough: the verified catalogue is handed to the caller, and anything
|
|
82
161
|
// holding a reference could rewrite `allowStale`, a status report, or an entry's registered roots
|
|
83
162
|
// and the object would still pass the identity check. Freezing it means the catalogue that decides
|
|
@@ -122,7 +201,7 @@ function _assertLeafSigns(leaf) {
|
|
|
122
201
|
// public key, so that pair -- not the issuer field, and not the certificate's bytes -- is what
|
|
123
202
|
// decides. The DN comparison is the canonical one, so two spellings of one name still match.
|
|
124
203
|
function _isAnchorItself(cert, anchor) {
|
|
125
|
-
return guard.name.dnEqual(cert.subject, anchor.subject) &&
|
|
204
|
+
return guard.name.dnEqual(cert.subject.rdns, anchor.subject.rdns, _err, "webauthn/bad-att-cert", "the anchor subject") &&
|
|
126
205
|
cert.subjectPublicKeyInfo.bytes.equals(anchor.subjectPublicKeyInfo.bytes);
|
|
127
206
|
}
|
|
128
207
|
|
|
@@ -135,6 +214,13 @@ function _isAnchorItself(cert, anchor) {
|
|
|
135
214
|
// a hand-built object literal satisfies it too and then raises a raw TypeError from deep inside the
|
|
136
215
|
// path validator, which is an untyped throw escaping a public verb.
|
|
137
216
|
function _asCert(v, label) {
|
|
217
|
+
// A certificate arriving as BYTES arrives in whichever byte form the caller holds -- the same set
|
|
218
|
+
// every other byte argument in this namespace takes. A DataView or an ArrayBuffer over identical
|
|
219
|
+
// DER is the identical certificate, and refusing one of them makes the accepted set depend on how
|
|
220
|
+
// the caller happened to receive the file rather than on what it contains.
|
|
221
|
+
if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer) {
|
|
222
|
+
v = guard.bytes.source(v, WebauthnError, "webauthn/bad-input", label);
|
|
223
|
+
}
|
|
138
224
|
if (v && typeof v === "object" && !Buffer.isBuffer(v) && !(v instanceof Uint8Array) &&
|
|
139
225
|
v.subject && v.subjectPublicKeyInfo && Buffer.isBuffer(v.subjectPublicKeyInfo.bytes) &&
|
|
140
226
|
v.signatureAlgorithm && typeof v.signatureAlgorithm.oid === "string" &&
|
|
@@ -197,8 +283,12 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
197
283
|
// length is its character count and a BLOB is ASCII base64url + dots, so it bounds the byte count.
|
|
198
284
|
// A byte input is re-viewed through the shared guard, which is where the detached-backing-buffer
|
|
199
285
|
// case is handled once, rather than re-derived here.
|
|
200
|
-
|
|
201
|
-
|
|
286
|
+
// Every byte form, not only the two a Buffer-shaped API thinks of. A BLOB is retrieved over the
|
|
287
|
+
// network, and the ordinary way to hold a fetched body is an ArrayBuffer -- so the form an
|
|
288
|
+
// operator most naturally arrives with was the one form this refused. `byteLength` is declared by
|
|
289
|
+
// an ArrayBuffer and by every view over one, so the ceiling below still bites before any copy.
|
|
290
|
+
var raw = (ArrayBuffer.isView(blob) || blob instanceof ArrayBuffer)
|
|
291
|
+
? guard.bytes.source(blob, WebauthnError, "webauthn/bad-input", "the metadata BLOB") : null;
|
|
202
292
|
// A string's `.length` counts UTF-16 code units, not the UTF-8 bytes the conversion produces, so
|
|
203
293
|
// measuring it would let a string of multi-byte characters sit under the ceiling and then expand
|
|
204
294
|
// several-fold past it during the copy -- the allocation the ceiling exists to prevent.
|
|
@@ -308,19 +398,46 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
308
398
|
// -- and the path validation that follows checks the CHAIN, not the target's application-level
|
|
309
399
|
// usage, so nothing else catches it. An absent keyUsage places no restriction, per that section.
|
|
310
400
|
_assertLeafSigns(leaf);
|
|
311
|
-
// The alg names a signature scheme; the leaf key must be
|
|
312
|
-
// the scheme from the token and the key from the chain without checking they agree is the JWS
|
|
313
|
-
// algorithm-confusion class.
|
|
401
|
+
// The alg names a signature scheme; the leaf key must be permitted to perform it.
|
|
314
402
|
var leafAlg = (leaf.subjectPublicKeyInfo.algorithm || {}).name;
|
|
315
|
-
var
|
|
316
|
-
if (
|
|
403
|
+
var leafSchemes = typeof leafAlg === "string" ? LEAF_SCHEMES_BY_SPKI_ALG[leafAlg] : undefined;
|
|
404
|
+
if (!leafSchemes || !leafSchemes[algRow.scheme]) {
|
|
317
405
|
throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + header.alg + " does not match the x5c leaf key type " + JSON.stringify(leafAlg));
|
|
318
406
|
}
|
|
407
|
+
// An id-RSASSA-PSS certificate MAY narrow the key further, to one hash (RFC 4055 sec. 1.2). The
|
|
408
|
+
// restriction is the certificate's, so it outranks the alg: a key issued for SHA-256 must not
|
|
409
|
+
// verify a PS512 signature just because the header asked for one.
|
|
410
|
+
if (leafAlg === "rsassaPss") {
|
|
411
|
+
var pinnedHash = signScheme.pssSpkiPinnedHash(leaf, _schemeE);
|
|
412
|
+
if (pinnedHash && pinnedHash !== algRow.hash) {
|
|
413
|
+
throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + header.alg + " uses " + algRow.hash + ", but the x5c leaf key is restricted to " + pinnedHash);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
319
416
|
|
|
417
|
+
// An Edwards leaf key is validated on-curve and full-order BEFORE it is imported. The identity
|
|
418
|
+
// point and the other low-order points are accepted by the platform and verify a trivial
|
|
419
|
+
// signature over ANY message, so a leaf carrying one authenticates whatever payload it is shown.
|
|
420
|
+
// Chaining to the caller's FIDO root is not protection here: the point is malformed, not
|
|
421
|
+
// unissued, and the certificate that carries it can be perfectly well signed. This is the same
|
|
422
|
+
// gate every other Edwards key in the toolkit passes -- the attestation path, CMS, the path
|
|
423
|
+
// validator, sigstore, JOSE -- and a new verification route skipping it is how one key type comes
|
|
424
|
+
// to be checked everywhere except the newest door.
|
|
425
|
+
if (leafAlg === "Ed25519" || leafAlg === "Ed448") {
|
|
426
|
+
edwardsPoint.validateSpki(leaf.subjectPublicKeyInfo.bytes, leafAlg === "Ed25519" ? 6 : 7,
|
|
427
|
+
WebauthnError, "webauthn/bad-att-cert");
|
|
428
|
+
}
|
|
429
|
+
var params = _blobAlgParams(algRow, leafAlg);
|
|
320
430
|
var signingInput = Buffer.from(segs[0] + "." + segs[1], "ascii");
|
|
321
|
-
return webcrypto.webcrypto.subtle.importKey("spki", leaf.subjectPublicKeyInfo.bytes,
|
|
322
|
-
.then(function (key) {
|
|
323
|
-
|
|
431
|
+
return webcrypto.webcrypto.subtle.importKey("spki", leaf.subjectPublicKeyInfo.bytes, params.imp, false, ["verify"])
|
|
432
|
+
.then(function (key) {
|
|
433
|
+
// The VERIFY is wrapped as well as the import. A rejection handler attached to the import
|
|
434
|
+
// alone leaves anything the verify itself rejects with -- a certificate whose RSASSA-PSS
|
|
435
|
+
// parameters demand a longer salt than the algorithm supplies imports cleanly and then fails
|
|
436
|
+
// inside OpenSSL -- to escape as a raw platform Error, out of a verb whose whole contract is
|
|
437
|
+
// that every failure is a typed webauthn/* verdict.
|
|
438
|
+
return webcrypto.webcrypto.subtle.verify(params.ver, key, sig, signingInput)
|
|
439
|
+
.catch(function (e) { throw _err("webauthn/verify-error", "the metadata BLOB signature could not be evaluated under its x5c leaf key", e); });
|
|
440
|
+
}, function (e) { throw _err("webauthn/unsupported-algorithm", "the metadata BLOB x5c leaf key could not be imported for " + header.alg, e); })
|
|
324
441
|
.then(function (ok) {
|
|
325
442
|
if (!ok) throw _err("webauthn/verify-failed", "the metadata BLOB signature does not verify under its x5c leaf key");
|
|
326
443
|
return _chainToAnchor(chain, anchors, at);
|
|
@@ -336,8 +453,17 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
336
453
|
// The x5c chain must validate to one of the caller's anchors. Every anchor is tried because an
|
|
337
454
|
// operator may hold several across a rotation; the last path verdict is threaded as the cause so a
|
|
338
455
|
// caller can see WHY it did not chain rather than only that it did not.
|
|
339
|
-
|
|
456
|
+
//
|
|
457
|
+
// `what` and `code` name the caller's subject and its own refusal code, so every chain in the
|
|
458
|
+
// namespace -- the BLOB's, an attestation's, an android-safetynet service chain -- reaches its
|
|
459
|
+
// anchors through THIS walk while keeping the verdict its own callers already handle. A second copy
|
|
460
|
+
// of the walk is not merely duplication: the anchor-stripping rule is subtle (a terminal
|
|
461
|
+
// certificate that IS the anchor is identified by subject name AND public key, which is what
|
|
462
|
+
// recognises a cross-signed root), and a copy that got it slightly differently would refuse a valid
|
|
463
|
+
// chain in one place and accept a self-validated one in another.
|
|
464
|
+
function _chainToAnchor(chain, anchors, at, what, code) {
|
|
340
465
|
var subject = what || "metadata BLOB certificate chain";
|
|
466
|
+
var faultCode = code || "webauthn/metadata-untrusted";
|
|
341
467
|
var ordered = chain.slice().reverse(); // path.validate takes anchor-adjacent first
|
|
342
468
|
var lastFault = null;
|
|
343
469
|
return anchors.reduce(function (p, anchor) {
|
|
@@ -375,7 +501,7 @@ function _chainToAnchor(chain, anchors, at, what) {
|
|
|
375
501
|
});
|
|
376
502
|
}, Promise.resolve(false)).then(function (trusted) {
|
|
377
503
|
if (!trusted) {
|
|
378
|
-
throw _err(
|
|
504
|
+
throw _err(faultCode, "the " + subject + " does not validate to any of the roots it must reach", lastFault);
|
|
379
505
|
}
|
|
380
506
|
});
|
|
381
507
|
}
|
|
@@ -538,8 +664,15 @@ function _parsePayload(seg, at, opts) {
|
|
|
538
664
|
});
|
|
539
665
|
return out;
|
|
540
666
|
});
|
|
667
|
+
// The rollback rule leaves a trace, exactly as the freshness rule does either side of it. It runs
|
|
668
|
+
// only when a caller supplies the sequence number it already holds, so a result that does not say
|
|
669
|
+
// whether it ran cannot be told apart from one where the check was skipped -- and the whole point
|
|
670
|
+
// of the rule is that a caller can show its catalogue never went backwards. `previousNo` is the
|
|
671
|
+
// baseline it was compared against, so the claim is auditable rather than merely asserted.
|
|
541
672
|
var result = { no: payload.no, legalHeader: payload.legalHeader, nextUpdate: payload.nextUpdate,
|
|
542
673
|
stale: stale, allowStale: opts.allowStale === true,
|
|
674
|
+
rollbackChecked: opts.previousNo !== undefined,
|
|
675
|
+
previousNo: opts.previousNo === undefined ? null : opts.previousNo,
|
|
543
676
|
entries: entries, byAaguid: byAaguid, byKeyIdentifier: byKeyIdentifier,
|
|
544
677
|
statusPolicy: opts.statusPolicy || "any", rejectUnknownStatus: opts.rejectUnknownStatus === true };
|
|
545
678
|
// Frozen FIRST, then recorded as verified: the mark means "this exact catalogue passed the
|
|
@@ -547,6 +680,9 @@ function _parsePayload(seg, at, opts) {
|
|
|
547
680
|
// be edited afterwards. This is the only place a catalogue can have been through those gates.
|
|
548
681
|
_deepFreeze(result, 0);
|
|
549
682
|
_verifiedResults.add(result);
|
|
683
|
+
// Recorded AFTER the freeze, alongside the provenance mark and for the same reason: an entry may
|
|
684
|
+
// only be judged against the catalogue it was read out of.
|
|
685
|
+
entries.forEach(function (e) { _entryOrigin.set(e, result); });
|
|
550
686
|
return result;
|
|
551
687
|
}
|
|
552
688
|
|
|
@@ -570,12 +706,55 @@ function metadataFor(metadata, identifier) {
|
|
|
570
706
|
return null;
|
|
571
707
|
}
|
|
572
708
|
|
|
709
|
+
// The options metadataAnchors recognises. Null-prototype for the same reason every other
|
|
710
|
+
// caller-keyed table in this namespace is: a supplied key must not resolve to an inherited member.
|
|
711
|
+
var _ANCHOR_OPTS = Object.assign(Object.create(null), { metadata: 1, time: 1, certificate: 1 });
|
|
712
|
+
|
|
573
713
|
// The attestation root certificates an entry's authenticator chains to, decoded on demand. Decoding
|
|
574
714
|
// is deliberately per entry rather than for the whole BLOB: a handful of the certificates in the
|
|
575
715
|
// live metadata do not parse under a strict decoder, and decoding everything up front would let one
|
|
576
716
|
// vendor's malformed root refuse the entire BLOB for every authenticator in it.
|
|
577
|
-
function metadataAnchors(entry) {
|
|
717
|
+
function metadataAnchors(entry, opts) {
|
|
578
718
|
if (!entry || typeof entry !== "object") throw _err("webauthn/bad-input", "metadataAnchors expects a metadata entry");
|
|
719
|
+
opts = opts || {};
|
|
720
|
+
if (typeof opts !== "object" || Array.isArray(opts)) throw _err("webauthn/bad-input", "metadataAnchors opts must be an object");
|
|
721
|
+
guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, _err, "webauthn/bad-input", "metadataAnchors opts has an unknown key ");
|
|
722
|
+
// ONE read of the caller's object: the status gate below decides on these values, and a value
|
|
723
|
+
// read twice is a value that can differ between the check and the use.
|
|
724
|
+
opts = Object.assign({}, opts);
|
|
725
|
+
if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
726
|
+
// Provenance, the same rule metadataFor applies: only a catalogue this module actually verified
|
|
727
|
+
// may govern which anchors are handed out. Accepting any object carrying a `statusPolicy` would
|
|
728
|
+
// let a caller supply a predicate that denies nothing and defeat the gate on the next line --
|
|
729
|
+
// and the gate's whole purpose is that a disqualified model registers no anchors.
|
|
730
|
+
if (opts.metadata !== undefined) {
|
|
731
|
+
if (!isVerifiedResult(opts.metadata)) {
|
|
732
|
+
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");
|
|
733
|
+
}
|
|
734
|
+
// ...and the entry must be one of THAT catalogue's. Verified provenance says the supplied
|
|
735
|
+
// catalogue is real; it does not say this entry came from it, and a process holding two would
|
|
736
|
+
// otherwise judge one catalogue's status reports under the other's policy and freshness.
|
|
737
|
+
if (!_isEntryOf(entry, opts.metadata)) {
|
|
738
|
+
throw _err("webauthn/bad-input", "metadataAnchors was given an entry from a different catalogue than opts.metadata, so the status reports would be judged under a policy and freshness that are not theirs");
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// A model whose status reports deny trust has no anchors to offer. This is the ROUTE an operator
|
|
742
|
+
// follows to anchor an attestation themselves -- metadataFor, then these anchors, then
|
|
743
|
+
// pki.path.validate -- and nothing along it consulted the status reports, so a REVOKED
|
|
744
|
+
// authenticator's registered roots were handed over and the path validated against them. The
|
|
745
|
+
// catalogue's whole purpose is to say which models are still trusted; handing out the roots of
|
|
746
|
+
// one it has disqualified is answering a different question than the caller asked.
|
|
747
|
+
//
|
|
748
|
+
// Judged with the same inputs the attestation path uses, so the two readings cannot diverge: the
|
|
749
|
+
// caller's own statusPolicy when the verified result is supplied, the instant being judged, and
|
|
750
|
+
// the attestation certificate actually presented (so a report naming a single certificate is
|
|
751
|
+
// judged against that one rather than denying every device the entry covers). With none of them,
|
|
752
|
+
// the strictest reading applies -- any disqualifying report, judged now.
|
|
753
|
+
var at = opts.time === undefined ? new Date() : opts.time;
|
|
754
|
+
if (opts.metadata !== undefined) assertFresh(opts.metadata, at, "metadataAnchors");
|
|
755
|
+
if (statusDenied(entry, opts.metadata, opts.certificate, at)) {
|
|
756
|
+
throw _err("webauthn/metadata-status", "the metadata entry for this authenticator carries a disqualifying status report, so it registers no anchors to trust");
|
|
757
|
+
}
|
|
579
758
|
var st = entry.metadataStatement;
|
|
580
759
|
var list = st && Array.isArray(st.attestationRootCertificates) ? st.attestationRootCertificates : [];
|
|
581
760
|
if (list.length > C.MDS_MAX_ANCHORS_PER_ENTRY) {
|
|
@@ -750,4 +929,9 @@ module.exports = {
|
|
|
750
929
|
ZERO_AAGUID: ZERO_AAGUID,
|
|
751
930
|
certKeyIdentifier: certKeyIdentifier,
|
|
752
931
|
DISQUALIFYING: DISQUALIFYING,
|
|
932
|
+
// @internal -- the derived JWS algorithm table, exposed so a conformance vector can assert the
|
|
933
|
+
// derivation is TOTAL over pki.jose's registry: every algorithm the toolkit verifies has a row
|
|
934
|
+
// here, carrying the WebCrypto parameters its key type needs, or saying that the certificate
|
|
935
|
+
// supplies them where the algorithm alone does not fix a curve.
|
|
936
|
+
BLOB_ALGS: BLOB_ALGS,
|
|
753
937
|
};
|