@blamejs/pki 0.6.13 → 0.6.15
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 +22 -1
- package/README.md +1 -1
- package/index.js +1 -1
- package/lib/acme.js +59 -815
- package/lib/asn1-der.js +7 -245
- package/lib/attrcert-sign.js +12 -260
- package/lib/attrcert-verify.js +1 -7
- package/lib/byte-reader.js +0 -8
- package/lib/byte-writer.js +0 -11
- package/lib/cbor-det.js +0 -54
- package/lib/cmc-build.js +15 -357
- package/lib/cmc-verify.js +6 -239
- package/lib/cmp-build.js +41 -317
- package/lib/cmp-session.js +73 -760
- package/lib/cmp-verify.js +32 -488
- package/lib/cms-compress.js +3 -32
- package/lib/cms-decrypt.js +29 -294
- package/lib/cms-encrypt.js +14 -234
- package/lib/cms-sign.js +29 -324
- package/lib/cms-verify.js +9 -500
- package/lib/composite-kem.js +2 -102
- package/lib/composite-sig.js +1 -77
- package/lib/constants.js +0 -249
- package/lib/crl-sign.js +9 -237
- package/lib/crl-verify.js +1 -10
- package/lib/crmf-sign.js +19 -198
- package/lib/crmf-verify.js +1 -12
- package/lib/csr-sign.js +3 -34
- package/lib/csr-verify.js +1 -12
- package/lib/ct.js +28 -253
- package/lib/edwards-point.js +10 -44
- package/lib/est.js +28 -764
- package/lib/framework-error.js +4 -182
- package/lib/guard-all.js +1 -58
- package/lib/guard-async.js +1 -36
- package/lib/guard-bytes.js +6 -650
- package/lib/guard-compress.js +5 -102
- package/lib/guard-crypto.js +1 -40
- package/lib/guard-der.js +2 -93
- package/lib/guard-encoding.js +2 -60
- package/lib/guard-header.js +1 -36
- package/lib/guard-identifier.js +7 -531
- package/lib/guard-intrinsic.js +1 -150
- package/lib/guard-json.js +10 -73
- package/lib/guard-limits.js +1 -77
- package/lib/guard-list.js +1 -49
- package/lib/guard-name.js +3 -209
- package/lib/guard-parsed.js +7 -335
- package/lib/guard-range.js +1 -71
- package/lib/guard-secret.js +1 -102
- package/lib/guard-text.js +1 -64
- package/lib/guard-time.js +1 -97
- package/lib/hpke.js +3 -105
- package/lib/http-digest.js +14 -142
- package/lib/http-retry-after.js +1 -47
- package/lib/http-transport.js +21 -131
- package/lib/inspect.js +30 -284
- package/lib/ip-utils.js +6 -47
- package/lib/jose.js +7 -142
- package/lib/key.js +2 -67
- package/lib/lint.js +9 -194
- package/lib/merkle.js +1 -27
- package/lib/mime.js +9 -82
- package/lib/ocsp-request-verify.js +1 -14
- package/lib/ocsp-verify.js +1 -82
- package/lib/ocsp.js +11 -169
- package/lib/oid.js +5 -337
- package/lib/path-validate.js +103 -1492
- package/lib/pbes2.js +2 -89
- package/lib/pkcs12-build.js +31 -379
- package/lib/pki-build.js +64 -290
- package/lib/rc2.js +2 -18
- package/lib/rfc3339.js +4 -27
- package/lib/scep.js +59 -233
- package/lib/schema-all.js +0 -122
- package/lib/schema-attrcert.js +4 -146
- package/lib/schema-c509.js +153 -802
- package/lib/schema-cmc.js +5 -220
- package/lib/schema-cmp.js +2 -223
- package/lib/schema-cms.js +13 -427
- package/lib/schema-crl.js +5 -68
- package/lib/schema-crmf.js +6 -167
- package/lib/schema-csr.js +1 -54
- package/lib/schema-csrattrs.js +2 -87
- package/lib/schema-engine.js +0 -152
- package/lib/schema-ocsp.js +0 -117
- package/lib/schema-pkcs12.js +2 -93
- package/lib/schema-pkcs8.js +3 -35
- package/lib/schema-pkix.js +24 -655
- package/lib/schema-smime.js +0 -54
- package/lib/schema-tsp.js +3 -99
- package/lib/schema-x509.js +4 -63
- package/lib/shbs.js +11 -95
- package/lib/sign-scheme.js +2 -97
- package/lib/sigstore.js +9 -169
- package/lib/sleep.js +2 -8
- package/lib/smime.js +15 -408
- package/lib/tls-cert-compress.js +0 -62
- package/lib/trust.js +19 -185
- package/lib/tsp-sign.js +16 -270
- package/lib/validator-all.js +1 -30
- package/lib/validator-attcert.js +2 -46
- package/lib/validator-cose.js +2 -153
- package/lib/validator-keydesc.js +1 -37
- package/lib/validator-sig.js +1 -46
- package/lib/validator-tls.js +1 -88
- package/lib/validator-tpm.js +19 -122
- package/lib/verify-seam.js +1 -24
- package/lib/webauthn-mds.js +3 -400
- package/lib/webauthn.js +12 -833
- package/lib/webcrypto.js +5 -485
- package/lib/x509-sign.js +4 -108
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/webauthn-mds.js
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
-
//
|
|
5
|
-
// @internal -- the pki.webauthn metadata implementation. The operator-facing @module pki.webauthn
|
|
4
|
+
// @internal
|
|
6
5
|
// and the @primitive blocks for verifyMetadataBlob / metadataFor / metadataAnchors live in
|
|
7
|
-
// lib/webauthn.js, which re-exports these, so the namespace has one documented home.
|
|
8
|
-
//
|
|
9
|
-
// webauthn-mds -- the FIDO Metadata Service (MDS v3) reader. The BLOB is a JWS whose payload
|
|
10
|
-
// lists every registered authenticator model, keyed by AAGUID, with the certificates its
|
|
11
|
-
// attestations chain to and the status reports that say whether it is still trusted.
|
|
12
|
-
//
|
|
13
|
-
// The ordering is the load-bearing part: the signature and its certificate chain are established
|
|
14
|
-
// against a CALLER-supplied FIDO root before a single byte of the payload is read, so a hostile
|
|
15
|
-
// BLOB never reaches the JSON reader, the entry walk, or any per-entry certificate decode. No
|
|
16
|
-
// root is bundled and there is no trust-on-first-use: an operator supplies the anchor, exactly
|
|
17
|
-
// as pki.trust does for a root store. Retrieval is out of scope: the BLOB is caller-supplied
|
|
18
|
-
// bytes, so this module never touches a socket.
|
|
19
|
-
//
|
|
20
|
-
// FIDO Metadata Service v3.0 sec. 3.1 / sec. 3.2, RFC 7515 (JWS).
|
|
21
6
|
|
|
22
7
|
var frameworkError = require("./framework-error");
|
|
23
8
|
var x509 = require("./schema-x509");
|
|
24
9
|
var guard = require("./guard-all");
|
|
25
10
|
var intrinsic = require("./guard-intrinsic");
|
|
26
|
-
// Own-key membership through an operation taken at load. Written out, the question reads two
|
|
27
|
-
// replaceable properties to answer one thing, and here it decides which status rows an entry carries.
|
|
28
11
|
var _hasOwn = intrinsic.hasOwn;
|
|
29
12
|
var _charAt = intrinsic.uncurry(String.prototype.charAt);
|
|
30
13
|
var jose = require("./jose");
|
|
@@ -41,23 +24,10 @@ var pkix = require("./schema-pkix");
|
|
|
41
24
|
var WebauthnError = frameworkError.WebauthnError;
|
|
42
25
|
var _KU_NS = pkix.makeNS("webauthn", WebauthnError, oid);
|
|
43
26
|
function _err(code, message, cause) { return new WebauthnError(code, message, cause); }
|
|
44
|
-
// A 40-character certificate-key-identifier: SHA-1 hex. One accepts either case (/^[0-9a-fA-F]{40}$/), the
|
|
45
|
-
// lookup form is lower-case only (/^[0-9a-f]{40}$/).
|
|
46
27
|
var _KEYID_HEX_CI = pkix.charTable("0123456789abcdefABCDEF");
|
|
47
28
|
var _KEYID_HEX_LOWER = pkix.charTable("0123456789abcdef");
|
|
48
29
|
var C = constants.LIMITS;
|
|
49
30
|
|
|
50
|
-
// Which signature schemes each certificate key type may perform, keyed by the SPKI algorithm the
|
|
51
|
-
// x5c leaf carries. Taking the scheme from the token and the key from the chain without checking
|
|
52
|
-
// they agree is the JWS algorithm-confusion class, and the relation is not one-to-one: a
|
|
53
|
-
// PKCS#1-encoded RSA key can do either RSA scheme, while an id-RSASSA-PSS key is restricted BY ITS
|
|
54
|
-
// CERTIFICATE to RSASSA-PSS and must not verify an RS* signature (RFC 4055 sec. 1.2 / 3.1).
|
|
55
|
-
//
|
|
56
|
-
// The Edwards and ML-DSA rows exist because an X.509 SubjectPublicKeyInfo carries those keys
|
|
57
|
-
// perfectly well -- RFC 8410 for Ed25519 / Ed448, RFC 9881 for ML-DSA -- so a JWS algorithm over
|
|
58
|
-
// one is a conformant BLOB signature, not a theoretical one. `EdDSA` names a scheme without fixing
|
|
59
|
-
// a curve (RFC 8037), so for that scheme the certificate decides which of the two it is; every
|
|
60
|
-
// other row's algorithm fixes its own.
|
|
61
31
|
var LEAF_SCHEMES_BY_SPKI_ALG = Object.assign(Object.create(null), {
|
|
62
32
|
ecPublicKey: { ECDSA: 1 },
|
|
63
33
|
rsaEncryption: { "RSASSA-PKCS1-v1_5": 1, "RSA-PSS": 1 },
|
|
@@ -69,32 +39,8 @@ var LEAF_SCHEMES_BY_SPKI_ALG = Object.assign(Object.create(null), {
|
|
|
69
39
|
"id-ml-dsa-87": { "ML-DSA-87": 1 },
|
|
70
40
|
});
|
|
71
41
|
|
|
72
|
-
// The sign-scheme resolver's faults arrive as a bare kind; they keep webauthn/* codes here so a
|
|
73
|
-
// BLOB whose leaf pins a hash this toolkit cannot read fails as a webauthn verdict, not a foreign one.
|
|
74
42
|
function _schemeE(kind, message, cause) { return new WebauthnError("webauthn/" + kind, message, cause); }
|
|
75
43
|
|
|
76
|
-
// The JWS algorithms a metadata BLOB may be signed with, each with the key family it requires and
|
|
77
|
-
// the WebCrypto parameters that import an x5c leaf under it and verify with it.
|
|
78
|
-
//
|
|
79
|
-
// DERIVED, not declared. A second algorithm table maintained beside pki.jose's is how PS256 came
|
|
80
|
-
// to verify as an ACME signature and be refused as a metadata BLOB signature: both tables were
|
|
81
|
-
// correct on the day they were written and only one of them was extended. Deriving means the
|
|
82
|
-
// question "which algorithms does this toolkit accept for a JWS?" has exactly one answer, and a
|
|
83
|
-
// row added there reaches here without anybody remembering to copy it.
|
|
84
|
-
//
|
|
85
|
-
// The derivation is total over the registry: every algorithm pki.jose verifies gets a row, and an
|
|
86
|
-
// X.509 SubjectPublicKeyInfo can carry a key of every type in it. EC becomes ECDSA over the curve
|
|
87
|
-
// the alg fixes; RSA with a salt length becomes RSASSA-PSS (RFC 7518 sec. 3.5) and without one
|
|
88
|
-
// RSASSA-PKCS1-v1_5; OKP becomes EdDSA, whose curve the certificate supplies because the algorithm
|
|
89
|
-
// does not name one; AKP becomes the ML-DSA parameter set the algorithm itself fixes.
|
|
90
|
-
//
|
|
91
|
-
// A row's import and verify parameters may therefore depend on the LEAF as well as the alg, which
|
|
92
|
-
// is why they are resolved per-verification and never frozen into the row: EdDSA over an Ed25519
|
|
93
|
-
// certificate and EdDSA over an Ed448 certificate are the same JWS algorithm and two different
|
|
94
|
-
// WebCrypto algorithms.
|
|
95
|
-
//
|
|
96
|
-
// Null-prototype: `alg` is attacker-supplied, and an inherited Object member would otherwise
|
|
97
|
-
// resolve to a truthy non-row and read as a recognized algorithm.
|
|
98
44
|
function _deriveBlobAlgs() {
|
|
99
45
|
var out = Object.create(null);
|
|
100
46
|
jose.sigAlgs().forEach(function (row) {
|
|
@@ -108,7 +54,6 @@ function _deriveBlobAlgs() {
|
|
|
108
54
|
out[row.alg] = { scheme: "RSASSA-PKCS1-v1_5", hash: row.hash,
|
|
109
55
|
imp: { name: "RSASSA-PKCS1-v1_5", hash: row.hash }, ver: { name: "RSASSA-PKCS1-v1_5" } };
|
|
110
56
|
} else if (row.kty === "OKP") {
|
|
111
|
-
// `fromLeaf` marks a row whose WebCrypto algorithm is the certificate's own key algorithm.
|
|
112
57
|
out[row.alg] = { scheme: "EdDSA", hash: null, fromLeaf: true };
|
|
113
58
|
} else if (row.kty === "AKP") {
|
|
114
59
|
out[row.alg] = { scheme: row.alg, hash: null, imp: { name: row.alg }, ver: { name: row.alg } };
|
|
@@ -118,27 +63,16 @@ function _deriveBlobAlgs() {
|
|
|
118
63
|
}
|
|
119
64
|
var BLOB_ALGS = _deriveBlobAlgs();
|
|
120
65
|
|
|
121
|
-
// The WebCrypto import and verify parameters for one verification. Fixed by the algorithm for every
|
|
122
|
-
// scheme that names its own curve or parameter set; taken from the certificate for EdDSA, which
|
|
123
|
-
// does not. The leaf has already been checked to permit this row's scheme, so its algorithm name is
|
|
124
|
-
// one of the two the scheme covers.
|
|
125
66
|
function _blobAlgParams(algRow, leafAlgName) {
|
|
126
67
|
if (!algRow.fromLeaf) return algRow;
|
|
127
68
|
return { scheme: algRow.scheme, hash: null, imp: { name: leafAlgName }, ver: { name: leafAlgName } };
|
|
128
69
|
}
|
|
129
70
|
|
|
130
|
-
// The status values that deny trust (MDS v3.0 sec. 3.1.4). An unknown status is IGNORED for the
|
|
131
|
-
// gate and surfaced raw -- the specification requires that a verifier not fail on a status it does
|
|
132
|
-
// not recognize -- unless the caller opts into refusing them.
|
|
133
71
|
var DISQUALIFYING = Object.assign(Object.create(null), {
|
|
134
72
|
REVOKED: 1, ATTESTATION_KEY_COMPROMISE: 1, USER_KEY_REMOTE_COMPROMISE: 1,
|
|
135
73
|
USER_KEY_PHYSICAL_COMPROMISE: 1, USER_VERIFICATION_BYPASS: 1,
|
|
136
74
|
});
|
|
137
75
|
|
|
138
|
-
// The statuses whose optional `certificate` field NARROWS the report to the certificate it names
|
|
139
|
-
// (MDS v3.0 sec. 3.1.3). Only a compromised attestation key is about one certificate; every other
|
|
140
|
-
// disqualifying status is about the model, and a certificate attached to one of those is a
|
|
141
|
-
// nonconforming field, not a narrower scope.
|
|
142
76
|
var CERT_SCOPED_STATUS = Object.assign(Object.create(null), { ATTESTATION_KEY_COMPROMISE: 1 });
|
|
143
77
|
|
|
144
78
|
var _BLOB_OPTS = Object.assign(Object.create(null), {
|
|
@@ -148,32 +82,12 @@ var _BLOB_OPTS = Object.assign(Object.create(null), {
|
|
|
148
82
|
|
|
149
83
|
function _isPlainObject(v) { return !!v && typeof v === "object" && !Array.isArray(v); }
|
|
150
84
|
|
|
151
|
-
// The results this module actually produced. Membership is the PROVENANCE check: a metadata object
|
|
152
|
-
// is only allowed to decide trust if verifyMetadataBlob made it. Recognizing it by shape instead
|
|
153
|
-
// would accept anything carrying the right property names, including a catalogue deserialized
|
|
154
|
-
// from a cache, where nothing establishes that the signature and chain were ever checked, and an
|
|
155
|
-
// attacker who can write that cache chooses which roots an authenticator is allowed to chain to.
|
|
156
|
-
// A WeakSet keys on object IDENTITY, which no serialization survives, so a round-tripped catalogue
|
|
157
|
-
// is refused and the caller re-verifies -- which the freshness rule wants of them anyway. It also
|
|
158
|
-
// holds no strong reference, so a discarded result is still collectable.
|
|
159
85
|
var _verifiedResults = new WeakSet();
|
|
160
86
|
function isVerifiedResult(v) { return _isPlainObject(v) && _verifiedResults.has(v); }
|
|
161
87
|
|
|
162
|
-
// Which catalogue each entry came OUT of. Knowing that a supplied catalogue is verified says
|
|
163
|
-
// nothing about whether the entry being judged belongs to it, and a process may hold several: pair
|
|
164
|
-
// an entry from catalogue A with catalogue B and B's statusPolicy and freshness decide about A's
|
|
165
|
-
// status reports. B reading `latest-by-date` can then hand back anchors that A, reading `any`,
|
|
166
|
-
// records as revoked -- and a stale A can be judged current because B is. Keyed on object identity,
|
|
167
|
-
// for the same reason the provenance set is, and holding no strong reference to either side.
|
|
168
88
|
var _entryOrigin = new WeakMap();
|
|
169
89
|
function _isEntryOf(entry, metadata) { return _entryOrigin.get(entry) === metadata; }
|
|
170
90
|
|
|
171
|
-
// Provenance alone is not enough: the verified catalogue is handed to the caller, and anything
|
|
172
|
-
// holding a reference could rewrite `allowStale`, a status report, or an entry's registered roots
|
|
173
|
-
// and the object would still pass the identity check. Freezing it means the catalogue that decides
|
|
174
|
-
// a later verification is the one the signature covered, field for field. Depth-bounded and
|
|
175
|
-
// cycle-safe via the frozen check; typed arrays are skipped because Object.freeze rejects a view
|
|
176
|
-
// with elements, and the payload is JSON-derived so it holds none anyway.
|
|
177
91
|
function _deepFreeze(v, depth) {
|
|
178
92
|
if (!v || typeof v !== "object" || Object.isFrozen(v) || ArrayBuffer.isView(v)) return v;
|
|
179
93
|
if (depth > C.JSON_MAX_DEPTH) return v;
|
|
@@ -181,8 +95,6 @@ function _deepFreeze(v, depth) {
|
|
|
181
95
|
Object.keys(v).forEach(function (k) { _deepFreeze(v[k], depth + 1); });
|
|
182
96
|
return v;
|
|
183
97
|
}
|
|
184
|
-
// The AAGUID canonical form (lowercase hex 8-4-4-4-12 with dashes), matching
|
|
185
|
-
// /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, decided by character (no regex).
|
|
186
98
|
function _isAaguid(s) {
|
|
187
99
|
if (s.length !== 36) return false;
|
|
188
100
|
for (var i = 0; i < 36; i++) {
|
|
@@ -195,21 +107,11 @@ function _isAaguid(s) {
|
|
|
195
107
|
}
|
|
196
108
|
return true;
|
|
197
109
|
}
|
|
198
|
-
// "This authenticator declares no model identity": the AAGUID a U2F authenticator carries, since
|
|
199
|
-
// U2F has no such concept. It is a sentinel, never a lookup key, and lives in one place so the
|
|
200
|
-
// lookup and the dispatch that decides which key space applies cannot disagree about it.
|
|
201
110
|
var ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
|
202
|
-
// The JWS header parameters this reader understands (RFC 7515 sec. 4.1). Used only to tell a
|
|
203
|
-
// standard name from an extension name when checking `crit`.
|
|
204
111
|
var UNDERSTOOD_HEADER = Object.assign(Object.create(null), {
|
|
205
112
|
alg: 1, typ: 1, cty: 1, crit: 1, jku: 1, jwk: 1, kid: 1, x5u: 1, x5c: 1, x5t: 1, "x5t#S256": 1,
|
|
206
113
|
});
|
|
207
114
|
|
|
208
|
-
// The BLOB's signing certificate must be allowed to sign. RFC 5280 sec. 4.2.1.3 numbers
|
|
209
|
-
// digitalSignature bit 0 of the keyUsage BIT STRING; an absent extension places no restriction.
|
|
210
|
-
// Through the shared reader, which applies the NamedBitList rules a local bit test does not: DER
|
|
211
|
-
// drops trailing zero bits (X.690 sec. 11.2.2) and sec. 4.2.1.3 requires at least one bit set, so
|
|
212
|
-
// reading the bits here would let a certificate the rest of the toolkit calls malformed sign a BLOB.
|
|
213
115
|
function _assertLeafSigns(leaf) {
|
|
214
116
|
var ku = pkix.keyUsageOf(_KU_NS, leaf, _err, "webauthn/bad-att-cert", "metadata BLOB x5c leaf");
|
|
215
117
|
if (ku && ku.digitalSignature !== true) {
|
|
@@ -217,43 +119,18 @@ function _assertLeafSigns(leaf) {
|
|
|
217
119
|
}
|
|
218
120
|
}
|
|
219
121
|
|
|
220
|
-
// Is this certificate the anchor itself? RFC 5280 sec. 6.1.1 defines a trust anchor as a name and a
|
|
221
|
-
// public key, so that pair (not the issuer field, and not the certificate's bytes) is what
|
|
222
|
-
// decides. The DN comparison is the canonical one, so two spellings of one name still match.
|
|
223
122
|
function _isAnchorItself(cert, anchor) {
|
|
224
123
|
return guard.name.dnEqual(cert.subject.rdns, anchor.subject.rdns, _err, "webauthn/bad-att-cert", "the anchor subject") &&
|
|
225
124
|
cert.subjectPublicKeyInfo.bytes.equals(anchor.subjectPublicKeyInfo.bytes);
|
|
226
125
|
}
|
|
227
126
|
|
|
228
|
-
// A parsed anchor certificate, whichever shape the caller supplied it in.
|
|
229
|
-
//
|
|
230
|
-
// An already-parsed certificate is taken as-is, never re-encoded and re-parsed, but the
|
|
231
|
-
// recognition test names every field the anchor is later read for: the subject name, the public
|
|
232
|
-
// key bytes, and the signature algorithm OID. Recognizing an object on a looser test lets something
|
|
233
|
-
// that is merely certificate-SHAPED through: a parsed CSR satisfies "has a subject and an SPKI", and
|
|
234
|
-
// a hand-built object literal satisfies it too and then raises a raw TypeError from deep inside the
|
|
235
|
-
// path validator, which is an untyped throw escaping a public verb.
|
|
236
127
|
function _asCert(v, label) {
|
|
237
|
-
// Through the shared certificate door: these become the anchors a metadata BLOB's signer chain is
|
|
238
|
-
// judged against, and an anchor's identity is its subject and its key. A caller-assembled object
|
|
239
|
-
// could carry a real root's subject beside a substituted key, every field well-formed, nothing
|
|
240
|
-
// for a shape test to catch -- so the object is re-derived from the bytes its parser read instead.
|
|
241
|
-
// Bytes in any form (a Buffer, a typed-array view, a DataView, an ArrayBuffer) are parsed here;
|
|
242
|
-
// refusing one of those would make the accepted set depend on how the caller received the file.
|
|
243
128
|
return guard.parsed.acceptDerived(v, "certificate", function (bytes) {
|
|
244
129
|
try { return x509.parse(bytes); }
|
|
245
130
|
catch (e) { throw _err("webauthn/bad-input", label + " is not a decodable certificate", e); }
|
|
246
131
|
}, _err, "webauthn/bad-input", label);
|
|
247
132
|
}
|
|
248
133
|
|
|
249
|
-
// Verify a FIDO Metadata Service BLOB and return its entries indexed for lookup. `blob` is the
|
|
250
|
-
// BLOB as caller-supplied bytes or a string; this module never fetches it. The signature is
|
|
251
|
-
// checked under the certificate in the BLOB's own header, that chain is validated to one of
|
|
252
|
-
// `opts.rootCertificates`, and only then is the payload parsed: a BLOB that does not verify never
|
|
253
|
-
// reaches the JSON reader. `no` must exceed a supplied `previousNo` (the rollback check) and
|
|
254
|
-
// `nextUpdate` must not have passed (the freshness check), both fail-closed. No FIDO root ships
|
|
255
|
-
// with this toolkit: which metadata authority to trust is the operator's choice, and a verifier
|
|
256
|
-
// that bundled its own would be deciding trust on the caller's behalf.
|
|
257
134
|
function verifyMetadataBlob(blob, opts) {
|
|
258
135
|
return guard.async.deferred(function () { return _verifyMetadataBlob(blob, opts); });
|
|
259
136
|
}
|
|
@@ -271,10 +148,6 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
271
148
|
if (opts.previousNo !== undefined && (!Number.isSafeInteger(opts.previousNo) || opts.previousNo < 0)) {
|
|
272
149
|
throw _err("webauthn/bad-input", "opts.previousNo must be a non-negative safe integer");
|
|
273
150
|
}
|
|
274
|
-
// The boolean options are checked for TYPE, not merely compared against true. A caller who wrote
|
|
275
|
-
// `rejectUnknownStatus: "true"` from a config file is asking for the stricter behavior; comparing
|
|
276
|
-
// against `true` silently records the policy as off and the authenticator carrying an unrecognized
|
|
277
|
-
// status is then accepted -- the fail-open the option exists to prevent.
|
|
278
151
|
["requireRollbackCheck", "allowStale", "rejectUnknownStatus"].forEach(function (k) {
|
|
279
152
|
if (opts[k] !== undefined && typeof opts[k] !== "boolean") {
|
|
280
153
|
throw _err("webauthn/bad-input", "opts." + k + " must be a boolean");
|
|
@@ -289,25 +162,8 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
289
162
|
}
|
|
290
163
|
var anchors = roots.map(function (r, i) { return _asCert(r, "opts.rootCertificates[" + i + "]"); });
|
|
291
164
|
|
|
292
|
-
// Cap before the copy, not after it. Measuring the converted buffer would mean an oversized input
|
|
293
|
-
// is fully materialized in order to discover it should have been refused, which is the allocation the
|
|
294
|
-
// ceiling exists to prevent, performed on the way to reporting that it was too large. A string's
|
|
295
|
-
// length is its character count and a BLOB is ASCII base64url + dots, so it bounds the byte count.
|
|
296
|
-
// A byte input is re-viewed through the shared guard, which is where the detached-backing-buffer
|
|
297
|
-
// case is handled once and never re-derived here.
|
|
298
|
-
// Every byte form, not only the two a Buffer-shaped API thinks of. A BLOB is retrieved over the
|
|
299
|
-
// network, and the ordinary way to hold a fetched body is an ArrayBuffer, so the form an
|
|
300
|
-
// operator most naturally arrives with was the one form this refused. `byteLength` is declared by
|
|
301
|
-
// an ArrayBuffer and by every view over one, so the ceiling below still bites before any copy.
|
|
302
165
|
var raw = guard.bytes.isByteSource(blob)
|
|
303
166
|
? guard.bytes.source(blob, WebauthnError, "webauthn/bad-input", "the metadata BLOB") : null;
|
|
304
|
-
// A string's `.length` counts UTF-16 code units, not the UTF-8 bytes the conversion produces, so
|
|
305
|
-
// measuring it would let a string of multi-byte characters sit under the ceiling and then expand
|
|
306
|
-
// several-fold past it during the copy -- the allocation the ceiling exists to prevent.
|
|
307
|
-
// Buffer.byteLength computes the encoded size without producing the buffer, so the bound still
|
|
308
|
-
// bites before anything is materialized. (A conforming BLOB is base64url and dots, hence ASCII,
|
|
309
|
-
// where the two counts agree; a non-ASCII one is not a JWS at all and is refused on size or on
|
|
310
|
-
// shape immediately after.)
|
|
311
167
|
var declaredLength = raw ? raw.length : (typeof blob === "string" ? Buffer.byteLength(blob, "utf8") : null);
|
|
312
168
|
if (declaredLength !== null && declaredLength > C.MDS_BLOB_MAX_BYTES) {
|
|
313
169
|
throw _err("webauthn/too-large", "the metadata BLOB is " + declaredLength + " bytes, above the " + C.MDS_BLOB_MAX_BYTES + "-byte ceiling");
|
|
@@ -321,35 +177,17 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
321
177
|
}
|
|
322
178
|
var segs = bytes.toString("utf8").split(".");
|
|
323
179
|
if (segs.length !== 3) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB is not a three-part JWS compact serialization (RFC 7515 sec. 3.1)");
|
|
324
|
-
// The header's ceiling has to bite on the ENCODED segment, before it is decoded. Checking it
|
|
325
|
-
// afterwards means the decode has already allocated the very buffer the ceiling exists to bound,
|
|
326
|
-
// and does so for input nothing has authenticated yet. base64url expands 4 characters to 3 bytes,
|
|
327
|
-
// so the encoded length bounds the decoded one.
|
|
328
180
|
if (segs[0].length > Math.ceil(C.MDS_BLOB_HEADER_MAX_BYTES / 3) * 4) {
|
|
329
181
|
throw _err("webauthn/too-large", "the metadata BLOB protected header is above the " + C.MDS_BLOB_HEADER_MAX_BYTES + "-byte ceiling");
|
|
330
182
|
}
|
|
331
|
-
// The signature is bounded on the same principle and for the same reason: it is read before
|
|
332
|
-
// anything is authenticated, and every algorithm here has a tightly bounded signature size, so a
|
|
333
|
-
// segment consuming the whole envelope allowance is not a signature; it is a request to
|
|
334
|
-
// allocate megabytes and hand them to the verifier without possessing any key.
|
|
335
183
|
if (segs[2].length > Math.ceil(C.MDS_BLOB_SIG_MAX_BYTES / 3) * 4) {
|
|
336
184
|
throw _err("webauthn/too-large", "the metadata BLOB signature is above the " + C.MDS_BLOB_SIG_MAX_BYTES + "-byte ceiling");
|
|
337
185
|
}
|
|
338
186
|
var header, sig;
|
|
339
187
|
try {
|
|
340
|
-
// The header goes through the same bounded reader as the payload: a duplicate `alg` or `x5c`
|
|
341
|
-
// must not be resolvable to whichever copy a permissive parser happens to keep.
|
|
342
188
|
header = guard.json.parse(Buffer.from(jose.base64url.decode(segs[0])), _err, {
|
|
343
|
-
// The header is read before anything is authenticated, so its ceiling is the header's own,
|
|
344
|
-
// not the whole BLOB's: a JWS protected header is a few hundred bytes, and giving this reader
|
|
345
|
-
// the 32 MiB envelope cap would let unauthenticated bytes buy an unbounded multiple of that
|
|
346
|
-
// in heap ahead of the signature check. The payload's reader keeps the envelope cap, which is
|
|
347
|
-
// sound because nothing reaches it until the signature and chain hold.
|
|
348
189
|
maxBytes: C.MDS_BLOB_HEADER_MAX_BYTES, maxDepth: C.JSON_MAX_DEPTH,
|
|
349
190
|
tooLarge: "webauthn/too-large", badJson: "webauthn/bad-metadata-blob",
|
|
350
|
-
// Every code the guard can raise is named. An omitted one falls back to the framework default
|
|
351
|
-
// and the module's own defenses -- duplicate-member smuggling, the depth cap -- surface under
|
|
352
|
-
// a generic code no webauthn/* consumer can switch on.
|
|
353
191
|
tooDeep: "webauthn/bad-metadata-blob", duplicateMember: "webauthn/bad-metadata-blob",
|
|
354
192
|
badInput: "webauthn/bad-metadata-blob", label: "the metadata BLOB header",
|
|
355
193
|
});
|
|
@@ -359,17 +197,9 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
359
197
|
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header or signature is not decodable", e);
|
|
360
198
|
}
|
|
361
199
|
if (!_isPlainObject(header)) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header must be a JSON object");
|
|
362
|
-
// An x5u names a certificate chain to be FETCHED. This verifier performs no network retrieval, so
|
|
363
|
-
// it cannot establish that chain and must not pretend the BLOB is anchored.
|
|
364
200
|
if (header.x5u !== undefined) {
|
|
365
201
|
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header carries x5u, which names a chain to fetch; supply a BLOB with an inline x5c instead");
|
|
366
202
|
}
|
|
367
|
-
// RFC 7515 sec. 4.1.11: `crit` lists header parameters the recipient MUST understand and process,
|
|
368
|
-
// and a JWS naming one this implementation does not is invalid. Ignoring it is the same fault as
|
|
369
|
-
// ignoring x5u, one parameter over: the producer said "refuse this unless you handle it" and a
|
|
370
|
-
// reader that skips the list accepts a token on terms it never met. Nothing here processes an
|
|
371
|
-
// extension parameter, so any name is unprocessed, including a standard name, which sec. 4.1.11
|
|
372
|
-
// forbids listing at all.
|
|
373
203
|
if (_hasOwn(header, "crit")) {
|
|
374
204
|
var crit = header.crit;
|
|
375
205
|
if (!Array.isArray(crit) || crit.length === 0) {
|
|
@@ -404,21 +234,12 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
404
234
|
catch (e) { throw _err("webauthn/bad-att-cert", "the metadata BLOB x5c entry " + i + " is not a decodable certificate", e); }
|
|
405
235
|
});
|
|
406
236
|
var leaf = chain[0];
|
|
407
|
-
// RFC 5280 sec. 4.2.1.3: a certificate that carries a keyUsage extension may only be used for the
|
|
408
|
-
// purposes it asserts. Verifying the BLOB signature with a leaf whose keyUsage omits
|
|
409
|
-
// digitalSignature grants metadata-signing authority to a certificate that explicitly excludes it
|
|
410
|
-
// -- and the path validation that follows checks the CHAIN, not the target's application-level
|
|
411
|
-
// usage, so nothing else catches it. An absent keyUsage places no restriction, per that section.
|
|
412
237
|
_assertLeafSigns(leaf);
|
|
413
|
-
// The alg names a signature scheme; the leaf key must be permitted to perform it.
|
|
414
238
|
var leafAlg = (leaf.subjectPublicKeyInfo.algorithm || {}).name;
|
|
415
239
|
var leafSchemes = typeof leafAlg === "string" ? LEAF_SCHEMES_BY_SPKI_ALG[leafAlg] : undefined;
|
|
416
240
|
if (!leafSchemes || !leafSchemes[algRow.scheme]) {
|
|
417
241
|
throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + header.alg + " does not match the x5c leaf key type " + JSON.stringify(leafAlg));
|
|
418
242
|
}
|
|
419
|
-
// An id-RSASSA-PSS certificate MAY narrow the key further, to one hash (RFC 4055 sec. 1.2). The
|
|
420
|
-
// restriction is the certificate's, so it outranks the alg: a key issued for SHA-256 must not
|
|
421
|
-
// verify a PS512 signature just because the header asked for one.
|
|
422
243
|
if (leafAlg === "rsassaPss") {
|
|
423
244
|
var pinnedHash = signScheme.pssSpkiPinnedHash(leaf, _schemeE);
|
|
424
245
|
if (pinnedHash && pinnedHash !== algRow.hash) {
|
|
@@ -426,14 +247,6 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
426
247
|
}
|
|
427
248
|
}
|
|
428
249
|
|
|
429
|
-
// An Edwards leaf key is validated on-curve and full-order before it is imported. The identity
|
|
430
|
-
// point and the other low-order points are accepted by the platform and verify a trivial
|
|
431
|
-
// signature over ANY message, so a leaf carrying one authenticates whatever payload it is shown.
|
|
432
|
-
// Chaining to the caller's FIDO root is not protection here: the point is malformed, not
|
|
433
|
-
// unissued, and the certificate that carries it can be perfectly well signed. This is the same
|
|
434
|
-
// gate every other Edwards key in the toolkit passes (the attestation path, CMS, the path
|
|
435
|
-
// validator, sigstore, JOSE), and a new verification route skipping it is how one key type comes
|
|
436
|
-
// to be checked everywhere except the newest door.
|
|
437
250
|
if (leafAlg === "Ed25519" || leafAlg === "Ed448") {
|
|
438
251
|
edwardsPoint.validateSpki(leaf.subjectPublicKeyInfo.bytes, leafAlg === "Ed25519" ? 6 : 7,
|
|
439
252
|
WebauthnError, "webauthn/bad-att-cert");
|
|
@@ -442,11 +255,6 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
442
255
|
var signingInput = Buffer.from(segs[0] + "." + segs[1], "ascii");
|
|
443
256
|
return webcrypto.webcrypto.subtle.importKey("spki", leaf.subjectPublicKeyInfo.bytes, params.imp, false, ["verify"])
|
|
444
257
|
.then(function (key) {
|
|
445
|
-
// The VERIFY is wrapped as well as the import. A rejection handler attached to the import
|
|
446
|
-
// alone leaves anything the verify itself rejects with, such as a certificate whose RSASSA-PSS
|
|
447
|
-
// parameters demand a longer salt than the algorithm supplies imports cleanly and then fails
|
|
448
|
-
// inside OpenSSL -- to escape as a raw platform Error, out of a verb whose whole contract is
|
|
449
|
-
// that every failure is a typed webauthn/* verdict.
|
|
450
258
|
return webcrypto.webcrypto.subtle.verify(params.ver, key, sig, signingInput)
|
|
451
259
|
.catch(function (e) { throw _err("webauthn/verify-error", "the metadata BLOB signature could not be evaluated under its x5c leaf key", e); });
|
|
452
260
|
}, function (e) { throw _err("webauthn/unsupported-algorithm", "the metadata BLOB x5c leaf key could not be imported for " + header.alg, e); })
|
|
@@ -455,57 +263,24 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
455
263
|
return _chainToAnchor(chain, anchors, at);
|
|
456
264
|
})
|
|
457
265
|
.then(function () {
|
|
458
|
-
// Only now is the payload read. Everything above establishes that these bytes came from the
|
|
459
|
-
// holder of a key the caller anchored; parsing before that would expose the JSON reader, the
|
|
460
|
-
// entry walk and every per-entry decode to bytes nobody vouched for.
|
|
461
266
|
return _parsePayload(segs[1], at, opts);
|
|
462
267
|
});
|
|
463
268
|
}
|
|
464
269
|
|
|
465
|
-
// The x5c chain must validate to one of the caller's anchors. Every anchor is tried because an
|
|
466
|
-
// operator may hold several across a rotation; the last path verdict is threaded as the cause so a
|
|
467
|
-
// caller can see why it did not chain, and not merely that it did not.
|
|
468
|
-
//
|
|
469
|
-
// `what` and `code` name the caller's subject and its own refusal code, so every chain in the
|
|
470
|
-
// namespace -- the BLOB's, an attestation's, an android-safetynet service chain -- reaches its
|
|
471
|
-
// anchors through this same walk while keeping the verdict its own callers already handle. A second copy
|
|
472
|
-
// of the walk is not merely duplication: the anchor-stripping rule is subtle (a terminal
|
|
473
|
-
// certificate that IS the anchor is identified by subject name AND public key, which is what
|
|
474
|
-
// recognizes a cross-signed root), and a copy that got it slightly differently would refuse a valid
|
|
475
|
-
// chain in one place and accept a self-validated one in another.
|
|
476
270
|
function _chainToAnchor(chain, anchors, at, what, code) {
|
|
477
271
|
var subject = what || "metadata BLOB certificate chain";
|
|
478
272
|
var faultCode = code || "webauthn/metadata-untrusted";
|
|
479
|
-
var ordered = chain.slice().reverse();
|
|
273
|
+
var ordered = chain.slice().reverse();
|
|
480
274
|
var lastFault = null;
|
|
481
275
|
return anchors.reduce(function (p, anchor) {
|
|
482
276
|
return p.then(function (done) {
|
|
483
277
|
if (done) return true;
|
|
484
|
-
// A terminal certificate that is the anchor is the anchor, not a path element, so validating it
|
|
485
|
-
// against itself would be a different assertion from the one being made. What identifies it is
|
|
486
|
-
// the trust-anchor identity the validator itself uses: the SUBJECT NAME and the PUBLIC KEY. A
|
|
487
|
-
// self-issued test instead of a key comparison misses the cross-signed form of the same root,
|
|
488
|
-
// which carries that identity but was signed by a cross-signing CA, so it would be left in
|
|
489
|
-
// the path and then fail to verify under the anchor that never issued it, refusing an
|
|
490
|
-
// otherwise valid chain. Matching on name AND key cannot be looser: two certificates naming
|
|
491
|
-
// one subject with one key ARE the same entity for anchoring, whoever signed them.
|
|
492
278
|
var path = ordered.slice();
|
|
493
279
|
var strippedAnchor = false;
|
|
494
280
|
if (path.length && _isAnchorItself(path[0], anchor)) { path = path.slice(1); strippedAnchor = true; }
|
|
495
|
-
// Nothing left to validate. The two ways of getting here are not the same, and the difference
|
|
496
|
-
// is the whole verdict. If the anchor itself was the entire chain, then the signature was
|
|
497
|
-
// verified under the anchor's own key -- the identity was established by name AND key, so
|
|
498
|
-
// there is nothing further to chain and it is trusted. If the path was empty for any other
|
|
499
|
-
// reason, a validator has been handed nothing and nothing has been proved, so it refuses and
|
|
500
|
-
// the walk moves on to the next anchor.
|
|
501
281
|
if (path.length === 0) return strippedAnchor;
|
|
502
282
|
return pathValidate.validate(path, {
|
|
503
283
|
time: at,
|
|
504
|
-
// The anchor tuple names the anchor's own key: its SubjectPublicKeyInfo algorithm and that
|
|
505
|
-
// algorithm's parameters -- not the algorithm its issuer used to sign it. The validator
|
|
506
|
-
// carries these forward as the working public key, and a certificate below the anchor may
|
|
507
|
-
// inherit its key parameters from them (the DSA-style parameter-inheritance case), so
|
|
508
|
-
// supplying the signature OID leaves such a key unreconstructable and refuses a valid chain.
|
|
509
284
|
trustAnchors: { name: anchor.subject, publicKey: anchor.subjectPublicKeyInfo.bytes,
|
|
510
285
|
algorithm: anchor.subjectPublicKeyInfo.algorithm.oid,
|
|
511
286
|
parameters: anchor.subjectPublicKeyInfo.algorithm.parameters },
|
|
@@ -518,21 +293,12 @@ function _chainToAnchor(chain, anchors, at, what, code) {
|
|
|
518
293
|
});
|
|
519
294
|
}
|
|
520
295
|
|
|
521
|
-
// The instant a catalogue stops being current. `nextUpdate` is a DATE, so the BLOB is current
|
|
522
|
-
// through the end of that UTC day. One home, because the rule is applied twice (once when the
|
|
523
|
-
// BLOB is verified, and again whenever a verified result is used) and two copies would drift.
|
|
524
296
|
function _staleAfter(nextUpdate) {
|
|
525
297
|
var d = rfc3339.parseDate(nextUpdate, function (c, m) { return _err("webauthn/bad-metadata-blob", m); },
|
|
526
298
|
"webauthn/bad-metadata-blob", "the metadata BLOB nextUpdate");
|
|
527
299
|
return guard.time.instantOf(d) + constants.TIME.days(1);
|
|
528
300
|
}
|
|
529
301
|
|
|
530
|
-
// A verified result is a plain object the caller may hold for as long as it likes, so its freshness
|
|
531
|
-
// has to be re-established every time it decides something, not only when it was parsed. A
|
|
532
|
-
// catalogue fetched before its nextUpdate and reused a month later would otherwise keep authorizing
|
|
533
|
-
// an authenticator whose status reports have since revoked it, which is precisely what nextUpdate
|
|
534
|
-
// exists to prevent. The caller's original allowStale decision is carried on the result and honored
|
|
535
|
-
// here, so opting out stays opted out and never silently reappears at the point of use.
|
|
536
302
|
function assertFresh(metadata, at, label) {
|
|
537
303
|
if (!metadata || metadata.allowStale === true || typeof metadata.nextUpdate !== "string") return;
|
|
538
304
|
var atMs = guard.time.isDate(at) ? guard.time.instantOf(at) : NaN;
|
|
@@ -547,9 +313,6 @@ function assertFresh(metadata, at, label) {
|
|
|
547
313
|
function _parsePayload(seg, at, opts) {
|
|
548
314
|
var payload;
|
|
549
315
|
try {
|
|
550
|
-
// The shared bounded JSON reader: byte cap before the UTF-8 decode, depth cap, and a duplicate
|
|
551
|
-
// member rejected at any depth -- so a payload cannot smuggle a second `no` or `entries` past
|
|
552
|
-
// whichever one JSON.parse would have kept.
|
|
553
316
|
payload = guard.json.parse(Buffer.from(jose.base64url.decode(seg)), _err, {
|
|
554
317
|
maxBytes: C.MDS_BLOB_MAX_BYTES, maxDepth: C.JSON_MAX_DEPTH,
|
|
555
318
|
tooLarge: "webauthn/too-large", badJson: "webauthn/bad-metadata-blob",
|
|
@@ -563,22 +326,11 @@ function _parsePayload(seg, at, opts) {
|
|
|
563
326
|
if (!_isPlainObject(payload)) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must be a JSON object");
|
|
564
327
|
if (typeof payload.legalHeader !== "string") throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a string legalHeader");
|
|
565
328
|
if (!Number.isSafeInteger(payload.no) || payload.no < 0) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a non-negative integer no");
|
|
566
|
-
// Rollback: a BLOB older than the one already held would reinstate authenticators whose trust was
|
|
567
|
-
// withdrawn since, so it is refused, not merely reported.
|
|
568
329
|
if (opts.previousNo !== undefined && payload.no <= opts.previousNo) {
|
|
569
330
|
throw _err("webauthn/metadata-rollback", "the metadata BLOB no " + payload.no + " does not exceed the previously held " + opts.previousNo);
|
|
570
331
|
}
|
|
571
332
|
var staleAfter = _staleAfter(payload.nextUpdate);
|
|
572
333
|
var atMs = guard.time.instantOf(at);
|
|
573
|
-
// An unusable comparison instant must not read as "fresh": without this, a NaN date makes every
|
|
574
|
-
// comparison false and the staleness check silently passes.
|
|
575
|
-
//
|
|
576
|
-
// Unreachable as the code stands, and kept deliberately. Both operands are already gated by
|
|
577
|
-
// throwing guards -- `at` by guard.time.assertValid at the entry, `staleAfter` by
|
|
578
|
-
// rfc3339.parseDate above, which refuses a date that does not exist and never rolls it over,
|
|
579
|
-
// so no caller input reaches here non-finite. It stays because the cost is one comparison and the
|
|
580
|
-
// failure it catches is silent: if either gate is ever relaxed or moved, this is what keeps a NaN
|
|
581
|
-
// from being read as fresh instead of stale.
|
|
582
334
|
if (!isFinite(atMs) || !isFinite(staleAfter)) {
|
|
583
335
|
throw _err("webauthn/bad-input", "the metadata freshness comparison has no usable instant");
|
|
584
336
|
}
|
|
@@ -598,18 +350,10 @@ function _parsePayload(seg, at, opts) {
|
|
|
598
350
|
if (!Array.isArray(e.statusReports) || e.statusReports.length === 0) {
|
|
599
351
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " must carry a non-empty statusReports array");
|
|
600
352
|
}
|
|
601
|
-
// Bounded like every other repeated per-entry structure. The status gate walks this array on
|
|
602
|
-
// each verify, so an unbounded one turns a single entry into per-verification work with no
|
|
603
|
-
// ceiling -- the entry count and anchor count are capped for exactly that reason, and leaving
|
|
604
|
-
// one of the three uncapped is the same rule applied to part of the structure.
|
|
605
353
|
if (e.statusReports.length > C.MDS_MAX_STATUS_REPORTS_PER_ENTRY) {
|
|
606
354
|
throw _err("webauthn/too-large", "metadata entry " + i + " declares " + e.statusReports.length +
|
|
607
355
|
" status reports, above the " + C.MDS_MAX_STATUS_REPORTS_PER_ENTRY + " ceiling");
|
|
608
356
|
}
|
|
609
|
-
// sec. 3.1.3 makes `status` REQUIRED on every report. A report that is not an object, or that
|
|
610
|
-
// omits it, is refused here and never skipped by the status gate: the gate reads a missing
|
|
611
|
-
// status as "nothing disqualifying", so a malformed report would be silently read as a clean
|
|
612
|
-
// bill of health for the authenticator whose status it was supposed to carry.
|
|
613
357
|
e.statusReports.forEach(function (r, ri) {
|
|
614
358
|
if (!_isPlainObject(r)) throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " status report " + ri + " is not an object");
|
|
615
359
|
if (typeof r.status !== "string" || !r.status) {
|
|
@@ -623,15 +367,6 @@ function _parsePayload(seg, at, opts) {
|
|
|
623
367
|
}
|
|
624
368
|
aaguid = e.aaguid.toLowerCase();
|
|
625
369
|
}
|
|
626
|
-
// A U2F authenticator carries no AAGUID, so the catalogue keys it by the key identifiers of its
|
|
627
|
-
// attestation certificates instead. Without this index such an authenticator matches nothing,
|
|
628
|
-
// and a caller who enabled metadata enforcement would have every U2F registration refused as
|
|
629
|
-
// unlisted -- fail-closed, but on an authenticator the catalogue does in fact describe.
|
|
630
|
-
// sec. 3.1.1 puts attestationCertificateKeyIdentifiers on the ENTRY, as a sibling of
|
|
631
|
-
// metadataStatement -- that is the field the lookup is keyed by, and reading it only from the
|
|
632
|
-
// statement means real U2F entries never populate the index and every catalogued U2F
|
|
633
|
-
// authenticator is refused as unlisted. sec. 3.2 defines the same field on the statement as
|
|
634
|
-
// well, and live entries populate both, so both are read and the union is indexed.
|
|
635
370
|
var st = e.metadataStatement;
|
|
636
371
|
var keyIds = [];
|
|
637
372
|
var seenKeyId = Object.create(null);
|
|
@@ -647,26 +382,16 @@ function _parsePayload(seg, at, opts) {
|
|
|
647
382
|
" attestation certificate key identifiers, above the " + C.MDS_MAX_KEY_IDS_PER_ENTRY + " ceiling");
|
|
648
383
|
}
|
|
649
384
|
list.forEach(function (k) {
|
|
650
|
-
// A SHA-1 key identifier is 40 hex digits (RFC 5280 sec. 4.2.1.2 method 1). Anything else
|
|
651
|
-
// cannot be what a certificate hashes to, so accepting it would add a key that never
|
|
652
|
-
// matches -- an entry that silently cannot be found. sec. 3.1.1 requires lower case; the
|
|
653
|
-
// value is canonicalized, not refused, so a catalogue that differs only in letter
|
|
654
|
-
// case still resolves instead of denying service for every authenticator it lists.
|
|
655
385
|
if (typeof k !== "string" || k.length !== 40 || !pkix.allCharsIn(k, _KEYID_HEX_CI)) {
|
|
656
386
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " has a malformed attestation certificate key identifier");
|
|
657
387
|
}
|
|
658
388
|
var lower = k.toLowerCase();
|
|
659
|
-
// The entry and its statement legitimately repeat an identifier; that is one entry naming
|
|
660
|
-
// itself twice, not two entries claiming one authenticator, so it is deduplicated here
|
|
661
|
-
// and never reaches the cross-entry duplicate check below.
|
|
662
389
|
if (!seenKeyId[lower]) { seenKeyId[lower] = 1; keyIds.push(lower); }
|
|
663
390
|
});
|
|
664
391
|
});
|
|
665
392
|
var out = { index: i, aaguid: aaguid, keyIdentifiers: keyIds, statusReports: e.statusReports,
|
|
666
393
|
metadataStatement: st || null, timeOfLastStatusChange: e.timeOfLastStatusChange || null };
|
|
667
394
|
if (aaguid) {
|
|
668
|
-
// A duplicate identifier is refused, never resolved by position: two entries claiming
|
|
669
|
-
// one authenticator give the lookup a choice the specification does not define.
|
|
670
395
|
if (byAaguid[aaguid]) throw _err("webauthn/duplicate-metadata-entry", "two metadata entries claim aaguid " + aaguid);
|
|
671
396
|
byAaguid[aaguid] = out;
|
|
672
397
|
}
|
|
@@ -676,36 +401,18 @@ function _parsePayload(seg, at, opts) {
|
|
|
676
401
|
});
|
|
677
402
|
return out;
|
|
678
403
|
});
|
|
679
|
-
// The rollback rule leaves a trace, exactly as the freshness rule does either side of it. It runs
|
|
680
|
-
// only when a caller supplies the sequence number it already holds, so a result that does not say
|
|
681
|
-
// whether it ran cannot be told apart from one where the check was skipped, and the whole point
|
|
682
|
-
// of the rule is that a caller can show its catalogue never went backwards. `previousNo` is the
|
|
683
|
-
// baseline it was compared against, so the claim is auditable, not merely asserted.
|
|
684
404
|
var result = { no: payload.no, legalHeader: payload.legalHeader, nextUpdate: payload.nextUpdate,
|
|
685
405
|
stale: stale, allowStale: opts.allowStale === true,
|
|
686
406
|
rollbackChecked: opts.previousNo !== undefined,
|
|
687
407
|
previousNo: opts.previousNo === undefined ? null : opts.previousNo,
|
|
688
408
|
entries: entries, byAaguid: byAaguid, byKeyIdentifier: byKeyIdentifier,
|
|
689
409
|
statusPolicy: opts.statusPolicy || "any", rejectUnknownStatus: opts.rejectUnknownStatus === true };
|
|
690
|
-
// Frozen first, then recorded as verified: the mark means "this exact catalogue passed the
|
|
691
|
-
// signature, chain, rollback and freshness gates", and that claim only holds if the object cannot
|
|
692
|
-
// be edited afterwards. This is the only place a catalogue can have been through those gates.
|
|
693
410
|
_deepFreeze(result, 0);
|
|
694
411
|
_verifiedResults.add(result);
|
|
695
|
-
// Recorded after the freeze, alongside the provenance mark and for the same reason: an entry may
|
|
696
|
-
// only be judged against the catalogue it was read out of.
|
|
697
412
|
entries.forEach(function (e) { _entryOrigin.set(e, result); });
|
|
698
413
|
return result;
|
|
699
414
|
}
|
|
700
415
|
|
|
701
|
-
// The verified metadata entry for an AAGUID, or `null` when the BLOB lists none. `metadata` is a
|
|
702
|
-
// `verifyMetadataBlob` result -- never raw bytes, so a lookup cannot be answered from an
|
|
703
|
-
// unverified BLOB. The all-zero AAGUID means "no model identity" and never matches.
|
|
704
|
-
// It also accepts the identifier a U2F authenticator is keyed by instead: the key identifier of
|
|
705
|
-
// its attestation certificate -- so one verb covers both of the catalogue's key spaces. The two are
|
|
706
|
-
// disjoint by shape (a dashed 36-character UUID against 40 hex digits), so the form is DISPATCHED
|
|
707
|
-
// on, never guessed at: anything matching neither is a miss, not a lookup in whichever table
|
|
708
|
-
// happens to answer.
|
|
709
416
|
function metadataFor(metadata, identifier) {
|
|
710
417
|
if (!isVerifiedResult(metadata)) throw _err("webauthn/bad-input", "metadataFor expects a verifyMetadataBlob result -- an object that merely resembles one, such as a catalogue restored from a cache, has not been through the signature and chain checks");
|
|
711
418
|
if (typeof identifier !== "string") return null;
|
|
@@ -718,50 +425,23 @@ function metadataFor(metadata, identifier) {
|
|
|
718
425
|
return null;
|
|
719
426
|
}
|
|
720
427
|
|
|
721
|
-
// The options metadataAnchors recognizes. Null-prototype for the same reason every other
|
|
722
|
-
// caller-keyed table in this namespace is: a supplied key must not resolve to an inherited member.
|
|
723
428
|
var _ANCHOR_OPTS = Object.assign(Object.create(null), { metadata: 1, time: 1, certificate: 1 });
|
|
724
429
|
|
|
725
|
-
// The attestation root certificates an entry's authenticator chains to, decoded on demand. Decoding
|
|
726
|
-
// is deliberately per entry, and not for the whole BLOB: a handful of the certificates in the
|
|
727
|
-
// live metadata do not parse under a strict decoder, and decoding everything up front would let one
|
|
728
|
-
// vendor's malformed root refuse the entire BLOB for every authenticator in it.
|
|
729
430
|
function metadataAnchors(entry, opts) {
|
|
730
431
|
if (!entry || typeof entry !== "object") throw _err("webauthn/bad-input", "metadataAnchors expects a metadata entry");
|
|
731
432
|
opts = opts || {};
|
|
732
433
|
if (typeof opts !== "object" || Array.isArray(opts)) throw _err("webauthn/bad-input", "metadataAnchors opts must be an object");
|
|
733
434
|
guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, _err, "webauthn/bad-input", "metadataAnchors opts has an unknown key ");
|
|
734
|
-
// One read of the caller's object: the status gate below decides on these values, and a value
|
|
735
|
-
// read twice is a value that can differ between the check and the use.
|
|
736
435
|
opts = Object.assign({}, opts);
|
|
737
436
|
if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
738
|
-
// Provenance, the same rule metadataFor applies: only a catalogue this module actually verified
|
|
739
|
-
// may govern which anchors are handed out. Accepting any object carrying a `statusPolicy` would
|
|
740
|
-
// let a caller supply a predicate that denies nothing and defeat the gate on the next line --
|
|
741
|
-
// and the gate's whole purpose is that a disqualified model registers no anchors.
|
|
742
437
|
if (opts.metadata !== undefined) {
|
|
743
438
|
if (!isVerifiedResult(opts.metadata)) {
|
|
744
439
|
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");
|
|
745
440
|
}
|
|
746
|
-
// ...and the entry must be one of THAT catalogue's. Verified provenance says the supplied
|
|
747
|
-
// catalogue is real; it does not say this entry came from it, and a process holding two would
|
|
748
|
-
// otherwise judge one catalogue's status reports under the other's policy and freshness.
|
|
749
441
|
if (!_isEntryOf(entry, opts.metadata)) {
|
|
750
442
|
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");
|
|
751
443
|
}
|
|
752
444
|
}
|
|
753
|
-
// A model whose status reports deny trust has no anchors to offer. This is the ROUTE an operator
|
|
754
|
-
// follows to anchor an attestation themselves -- metadataFor, then these anchors, then
|
|
755
|
-
// pki.path.validate -- and nothing along it consulted the status reports, so a REVOKED
|
|
756
|
-
// authenticator's registered roots were handed over and the path validated against them. The
|
|
757
|
-
// catalogue's whole purpose is to say which models are still trusted; handing out the roots of
|
|
758
|
-
// one it has disqualified is answering a different question than the caller asked.
|
|
759
|
-
//
|
|
760
|
-
// Judged with the same inputs the attestation path uses, so the two readings cannot diverge: the
|
|
761
|
-
// caller's own statusPolicy when the verified result is supplied, the instant being judged, and
|
|
762
|
-
// the attestation certificate actually presented (so a report naming a single certificate is
|
|
763
|
-
// judged against that one, so the entry does not deny every device it covers). With none of them,
|
|
764
|
-
// the strictest reading applies -- any disqualifying report, judged now.
|
|
765
445
|
var at = opts.time === undefined ? new Date() : opts.time;
|
|
766
446
|
if (opts.metadata !== undefined) assertFresh(opts.metadata, at, "metadataAnchors");
|
|
767
447
|
if (statusDenied(entry, opts.metadata, opts.certificate, at)) {
|
|
@@ -781,20 +461,6 @@ function metadataAnchors(entry, opts) {
|
|
|
781
461
|
});
|
|
782
462
|
}
|
|
783
463
|
|
|
784
|
-
// Does this entry's status deny trust? Default: ANY disqualifying report denies, wherever it sits
|
|
785
|
-
// in the array. The array is not stated to be chronological, `effectiveDate` is optional, and in
|
|
786
|
-
// the live metadata a number of entries are not in date order, and one of them flipping its verdict
|
|
787
|
-
// between "last element" and "newest by date", in the direction that matters. A caller who wants
|
|
788
|
-
// the by-date reading asks for it.
|
|
789
|
-
// A status report may name the single certificate it concerns (MDS v3.0 sec. 3.1.3 `certificate`),
|
|
790
|
-
// and a key-compromise report that does so is about that one attestation key, not about every
|
|
791
|
-
// authenticator the entry covers. A whole batch is often listed under one entry, so ignoring the
|
|
792
|
-
// scoping would refuse registrations from devices whose key was never compromised. A report that
|
|
793
|
-
// names nothing applies to the entry as a whole, and a report whose named certificate cannot be
|
|
794
|
-
// read applies too: an unreadable scope is not a narrower scope.
|
|
795
|
-
// Phrased as "does this report demonstrably name a DIFFERENT certificate", so the only way to
|
|
796
|
-
// escape a disqualifying report is to PROVE it is about someone else. Every uncertain path returns
|
|
797
|
-
// false and the report keeps applying: a scope that cannot be read is not a narrower scope.
|
|
798
464
|
function _reportNamesOtherCert(report, leaf) {
|
|
799
465
|
if (typeof report.certificate !== "string" || !report.certificate) return false;
|
|
800
466
|
if (!leaf) return false;
|
|
@@ -805,13 +471,9 @@ function _reportNamesOtherCert(report, leaf) {
|
|
|
805
471
|
catch (_e) { return false; }
|
|
806
472
|
}
|
|
807
473
|
|
|
808
|
-
// Has this report taken effect by `atMs`?
|
|
809
474
|
function _reportInForceAt(report, atMs) {
|
|
810
475
|
var d = rfc3339.parseDate(report.effectiveDate, function (c, m) { return _err("webauthn/bad-metadata-blob", m); },
|
|
811
476
|
"webauthn/bad-metadata-blob", "a status report effectiveDate");
|
|
812
|
-
// Only ever called with a report whose effectiveDate rfc3339.isValidDate already accepted, and
|
|
813
|
-
// parseDate throws and never returns an Invalid Date, so d is finite; atMs was isFinite-
|
|
814
|
-
// checked by the caller before this runs.
|
|
815
477
|
// allow:nan-date-comparison-unguarded -- both operands are source-validated, as described above.
|
|
816
478
|
return guard.time.instantOf(d) <= atMs;
|
|
817
479
|
}
|
|
@@ -819,25 +481,10 @@ function _reportInForceAt(report, atMs) {
|
|
|
819
481
|
function statusDenied(entry, metadata, leaf, at) {
|
|
820
482
|
var policy = (metadata && metadata.statusPolicy) || "any";
|
|
821
483
|
var reports = entry.statusReports || [];
|
|
822
|
-
// A caller's own predicate receives the entry's reports AS GIVEN, which is what the documented
|
|
823
|
-
// contract promises. Handing it a pre-filtered array would silently defeat the very policies it
|
|
824
|
-
// exists for -- an entry-wide rule such as "deny whenever any attestation key is compromised"
|
|
825
|
-
// would be evaluated against an array with exactly those reports removed.
|
|
826
484
|
if (typeof policy === "function") return policy(reports) === true;
|
|
827
|
-
// For the built-in policies, reports that demonstrably concern a DIFFERENT certificate are removed
|
|
828
|
-
// first, before recency is considered. Scope has to be settled before recency, or under
|
|
829
|
-
// latest-by-date such a report could be selected as the newest, displace an older model-wide
|
|
830
|
-
// revocation, and then be discarded as inapplicable, clearing the entry using a report that was
|
|
831
|
-
// never about this authenticator at all.
|
|
832
485
|
reports = reports.filter(function (r) {
|
|
833
486
|
return !(r && typeof r.status === "string" && CERT_SCOPED_STATUS[r.status] && _reportNamesOtherCert(r, leaf));
|
|
834
487
|
});
|
|
835
|
-
// A report dated after the instant being judged has not taken effect, whatever the policy. This
|
|
836
|
-
// belongs here, not inside one policy's branch: under the default reading, a scheduled
|
|
837
|
-
// revocation would otherwise deny every registration from the moment it is published instead of
|
|
838
|
-
// from the date it names, and a deliberately historical verification would see reports filed
|
|
839
|
-
// after the time it asks about. When every dated report is still in the future, the answer is
|
|
840
|
-
// that none of them is in force, not that all of them are.
|
|
841
488
|
var isDated = function (r) { return r && typeof r.effectiveDate === "string" && rfc3339.isValidDate(r.effectiveDate); };
|
|
842
489
|
var atMs = (guard.time.isDate(at) && isFinite(guard.time.instantOf(at))) ? guard.time.instantOf(at) : null;
|
|
843
490
|
if (atMs !== null) {
|
|
@@ -847,17 +494,7 @@ function statusDenied(entry, metadata, leaf, at) {
|
|
|
847
494
|
if (policy === "latest-by-date") {
|
|
848
495
|
var dated = reports.filter(isDated);
|
|
849
496
|
if (dated.length) {
|
|
850
|
-
// Every report on the newest date, not the first one found there. Reducing to a single report
|
|
851
|
-
// makes a tie resolve by array position: a same-day clean report and a same-day REVOKED would
|
|
852
|
-
// deny or not depending purely on which the catalogue happened to list first, and reversing
|
|
853
|
-
// the array would flip the verdict. Reports that are equally recent are equally current, so a
|
|
854
|
-
// disqualifying one among them cannot be discarded.
|
|
855
497
|
var newest = dated.reduce(function (a, b) { return a.effectiveDate >= b.effectiveDate ? a : b; }).effectiveDate;
|
|
856
|
-
// effectiveDate is OPTIONAL (sec. 3.1.3), and a report without one cannot be shown to be
|
|
857
|
-
// older than anything, so it is kept and never dropped. Discarding it would let an entry
|
|
858
|
-
// clear an undated REVOKED simply by adding a dated clean report, which is the fail-open this
|
|
859
|
-
// policy is most likely to be reached for. Where the ordering cannot be established, the
|
|
860
|
-
// report still counts.
|
|
861
498
|
considered = dated.filter(function (r) { return r.effectiveDate === newest; })
|
|
862
499
|
.concat(reports.filter(function (r) { return !isDated(r); }));
|
|
863
500
|
}
|
|
@@ -866,62 +503,31 @@ function statusDenied(entry, metadata, leaf, at) {
|
|
|
866
503
|
return considered.some(function (r) {
|
|
867
504
|
var s = r && typeof r.status === "string" ? r.status : null;
|
|
868
505
|
if (s === null) return false;
|
|
869
|
-
// Scope was settled above, so anything still here applies to this authenticator.
|
|
870
506
|
if (DISQUALIFYING[s]) return true;
|
|
871
|
-
// An unrecognized status is IGNORED for the gate unless the caller opted in: the specification
|
|
872
|
-
// requires a verifier not to fail on a status value it does not know.
|
|
873
507
|
return rejectUnknown && !_KNOWN_STATUS[s];
|
|
874
508
|
});
|
|
875
509
|
}
|
|
876
510
|
|
|
877
|
-
// Status values this library recognizes as non-disqualifying, so `rejectUnknownStatus` can tell an
|
|
878
|
-
// unknown value from a known-good one.
|
|
879
511
|
var _KNOWN_STATUS = Object.assign(Object.create(null), {
|
|
880
512
|
NOT_FIDO_CERTIFIED: 1, SELF_ASSERTION_SUBMITTED: 1, FIDO_CERTIFIED: 1, FIDO_CERTIFIED_L1: 1,
|
|
881
513
|
FIDO_CERTIFIED_L1plus: 1, FIDO_CERTIFIED_L2: 1, FIDO_CERTIFIED_L2plus: 1, FIDO_CERTIFIED_L3: 1,
|
|
882
514
|
FIDO_CERTIFIED_L3plus: 1, UPDATE_AVAILABLE: 1,
|
|
883
515
|
});
|
|
884
516
|
|
|
885
|
-
// A 16-byte AAGUID from authenticatorData -> the dashed lower-case form the metadata is keyed by.
|
|
886
517
|
function aaguidToString(buf) {
|
|
887
518
|
if (!Buffer.isBuffer(buf) || buf.length !== 16) return null;
|
|
888
519
|
var h = buf.toString("hex");
|
|
889
520
|
return h.slice(0, 8) + "-" + h.slice(8, 12) + "-" + h.slice(12, 16) + "-" + h.slice(16, 20) + "-" + h.slice(20);
|
|
890
521
|
}
|
|
891
522
|
|
|
892
|
-
// The key identifier of a certificate's subject public key, which is how the metadata keys a U2F
|
|
893
|
-
// authenticator that carries no AAGUID (`attestationCertificateKeyIdentifiers`).
|
|
894
|
-
//
|
|
895
|
-
// RFC 5280 sec. 4.2.1.2 method 1 exactly: the SHA-1 of the BIT STRING **contents** of the
|
|
896
|
-
// subjectPublicKey -- not of the whole SubjectPublicKeyInfo, which is a different digest that would
|
|
897
|
-
// match nothing in the catalogue and silently turn every U2F lookup into a miss.
|
|
898
|
-
//
|
|
899
|
-
// This is the same value pki-build's spkiKeyId derives for the subjectKeyIdentifier extension, from
|
|
900
|
-
// the DER, and not from a parsed certificate. Two derivations of one definition can drift, and a
|
|
901
|
-
// drift here is silent -- every U2F lookup simply stops matching -- so a vector pins the value
|
|
902
|
-
// against a real certificate's own subjectKeyIdentifier extension, which is the independent oracle
|
|
903
|
-
// for method 1 and fails the moment either derivation changes.
|
|
904
|
-
//
|
|
905
|
-
// The algorithm is not a choice: SHA-1 is what the standard names, and this is an identifier, not
|
|
906
|
-
// a signature or an integrity check, and the identity it labels is re-established by the certificate
|
|
907
|
-
// chain validation that follows. Choosing a stronger hash would produce a value the catalogue does
|
|
908
|
-
// not contain.
|
|
909
523
|
function certKeyIdentifier(cert) {
|
|
910
524
|
var pk = cert && cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.publicKey;
|
|
911
525
|
if (!pk || !Buffer.isBuffer(pk.bytes)) {
|
|
912
526
|
throw _err("webauthn/bad-input", "certKeyIdentifier expects a parsed certificate carrying a subject public key");
|
|
913
527
|
}
|
|
914
|
-
// Collision resistance is not the property relied on here: a second key hashing to the same
|
|
915
|
-
// identifier would resolve to the same catalogue entry, and its certificate would then still have
|
|
916
|
-
// to validate to the roots that same entry registers, which is the check that actually grants trust.
|
|
917
|
-
// nosemgrep: pki-weak-hash-md5-sha1
|
|
918
528
|
return nodeCrypto.createHash("sha1").update(pk.bytes).digest("hex");
|
|
919
529
|
}
|
|
920
530
|
|
|
921
|
-
// The verified metadata entry registering an attestation-certificate key identifier, or null. This
|
|
922
|
-
// is the lookup for an authenticator with no AAGUID (the U2F case) and it takes a
|
|
923
|
-
// verifyMetadataBlob result for the same reason metadataFor does: an unverified catalogue must not
|
|
924
|
-
// be able to answer which roots an authenticator is allowed to chain to.
|
|
925
531
|
function metadataForKeyIdentifier(metadata, keyId) {
|
|
926
532
|
if (!isVerifiedResult(metadata)) throw _err("webauthn/bad-input", "metadataForKeyIdentifier expects a verifyMetadataBlob result -- an object that merely resembles one has not been through the signature and chain checks");
|
|
927
533
|
if (typeof keyId !== "string") return null;
|
|
@@ -941,9 +547,6 @@ module.exports = {
|
|
|
941
547
|
ZERO_AAGUID: ZERO_AAGUID,
|
|
942
548
|
certKeyIdentifier: certKeyIdentifier,
|
|
943
549
|
DISQUALIFYING: DISQUALIFYING,
|
|
944
|
-
// @internal
|
|
945
|
-
// derivation is TOTAL over pki.jose's registry: every algorithm the toolkit verifies has a row
|
|
946
|
-
// here, carrying the WebCrypto parameters its key type needs, or saying that the certificate
|
|
947
|
-
// supplies them where the algorithm alone does not fix a curve.
|
|
550
|
+
// @internal
|
|
948
551
|
BLOB_ALGS: BLOB_ALGS,
|
|
949
552
|
};
|