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