@blamejs/pki 0.8.55 → 0.8.57
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 +94 -0
- package/README.md +1 -1
- package/lib/acme.js +2 -2
- package/lib/asn1-der.js +17 -5
- package/lib/attrcert-verify.js +1 -1
- package/lib/cmc-verify.js +1 -1
- package/lib/cmp-session.js +2 -2
- package/lib/cmp-verify.js +2 -2
- package/lib/cms-decrypt.js +4 -1
- package/lib/cms-encrypt.js +4 -1
- package/lib/cms-sign.js +26 -12
- package/lib/cms-verify.js +1 -1
- package/lib/composite-kem.js +30 -12
- package/lib/composite-sig.js +42 -23
- package/lib/constants.js +7 -2
- package/lib/crl-verify.js +1 -1
- package/lib/crmf-sign.js +72 -43
- package/lib/crmf-verify.js +1 -1
- package/lib/csr-verify.js +1 -1
- package/lib/ct.js +2 -2
- package/lib/edwards-point.js +1 -1
- package/lib/est.js +3 -3
- package/lib/guard-async.js +22 -1
- package/lib/guard-bytes.js +6 -3
- package/lib/guard-identifier.js +1 -1
- package/lib/guard-intrinsic.js +474 -0
- package/lib/guard-parsed.js +23 -7
- package/lib/http-digest.js +146 -62
- package/lib/http-transport.js +82 -17
- package/lib/identity-match.js +2 -2
- package/lib/inspect.js +1 -1
- package/lib/lint.js +51 -3
- package/lib/merkle.js +2 -2
- package/lib/ocsp-request-verify.js +1 -1
- package/lib/ocsp.js +53 -22
- package/lib/oid.js +7 -0
- package/lib/path-validate.js +169 -76
- package/lib/pbes2.js +14 -7
- package/lib/schema-attrcert.js +2 -2
- package/lib/schema-cmc.js +2 -2
- package/lib/schema-cmp.js +2 -2
- package/lib/schema-cms.js +115 -10
- package/lib/schema-crmf.js +2 -2
- package/lib/schema-csr.js +2 -2
- package/lib/schema-ocsp.js +41 -6
- package/lib/schema-pkix.js +3 -3
- package/lib/schema-x509.js +2 -2
- package/lib/sigstore.js +1 -1
- package/lib/tlog.js +7 -4
- package/lib/trust.js +187 -104
- package/lib/tsp-sign.js +1 -1
- package/lib/webauthn-mds.js +257 -121
- package/lib/webauthn.js +327 -164
- package/lib/webcrypto.js +10 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/webauthn-mds.js
CHANGED
|
@@ -8,6 +8,7 @@ var frameworkError = require("./framework-error");
|
|
|
8
8
|
var x509 = require("./schema-x509");
|
|
9
9
|
var guard = require("./guard-all");
|
|
10
10
|
var intrinsic = require("./guard-intrinsic");
|
|
11
|
+
var _nodeCrypto = require("crypto");
|
|
11
12
|
/** @internal The seed of the fold below is a RESOLVED promise carrying `false`, the verdict for a chain
|
|
12
13
|
* that matched no anchor, and `Promise.resolve` builds from its RECEIVER. Read from the global binding,
|
|
13
14
|
* that seed is constructed by whatever the binding holds, so a replacement settling truthy starts the
|
|
@@ -18,6 +19,19 @@ var _hasOwn = intrinsic.hasOwn;
|
|
|
18
19
|
var _reverse = intrinsic.reverse;
|
|
19
20
|
var _arrSlice = intrinsic.arraySlice;
|
|
20
21
|
var _charAt = intrinsic.uncurry(String.prototype.charAt);
|
|
22
|
+
/** @internal The string operations an identifier is normalized and sliced with. A metadata lookup key
|
|
23
|
+
* is built out of these, and the entry the key finds supplies both the status reports that refuse a
|
|
24
|
+
* revoked authenticator and the attestation roots a chain is anchored to, so an operation answering
|
|
25
|
+
* with another identifier selects another entry's status and another entry's roots. */
|
|
26
|
+
var _strToLowerCase = intrinsic.toLowerCase;
|
|
27
|
+
var _strSlice = intrinsic.uncurry(String.prototype.slice);
|
|
28
|
+
var _strSplit = intrinsic.uncurry(String.prototype.split);
|
|
29
|
+
/** @internal The hash constructor and the methods of the object it returns. What a digest here
|
|
30
|
+
* produces is a metadata lookup key and the identity two certificates are told apart by, so a
|
|
31
|
+
* replaced constructor, update or digest decides which entry governs an attestation. */
|
|
32
|
+
var _createHash = _nodeCrypto.createHash;
|
|
33
|
+
var _hashUpdate = intrinsic.uncurry(_nodeCrypto.createHash("sha256").update);
|
|
34
|
+
var _hashDigest = intrinsic.uncurry(_nodeCrypto.createHash("sha256").digest);
|
|
21
35
|
var jose = require("./jose");
|
|
22
36
|
var rfc3339 = require("./rfc3339");
|
|
23
37
|
var constants = require("./constants");
|
|
@@ -25,7 +39,6 @@ var pathValidate = require("./path-validate");
|
|
|
25
39
|
var signScheme = require("./sign-scheme");
|
|
26
40
|
var edwardsPoint = require("./edwards-point");
|
|
27
41
|
var webcrypto = require("./webcrypto");
|
|
28
|
-
var nodeCrypto = require("crypto");
|
|
29
42
|
|
|
30
43
|
var oid = require("./oid");
|
|
31
44
|
var pkix = require("./schema-pkix");
|
|
@@ -50,9 +63,13 @@ var C = constants.LIMITS;
|
|
|
50
63
|
* and carries genuine doubles: the published BLOB has members whose values are not integers, among them
|
|
51
64
|
* the biometric `selfAttestedFRR` and `selfAttestedFAR` rates and `iAPARThreshold`. A document-wide rule
|
|
52
65
|
* refuses the real catalogue. */
|
|
53
|
-
var EXACT_INTEGER_BLOB_MEMBERS = intrinsic.assign(intrinsic.create(null), { no: 1 });
|
|
66
|
+
var EXACT_INTEGER_BLOB_MEMBERS = intrinsic.freeze(intrinsic.assign(intrinsic.create(null), { no: 1 }));
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
/** @internal Sealed all the way down. Each row says which signature schemes a leaf key of that type
|
|
69
|
+
* may be used with, and the refusal reads the scheme straight out of the row, so a row that still
|
|
70
|
+
* inherits admits a scheme the key type does not support: `RSASSA-PKCS1-v1_5` installed on
|
|
71
|
+
* `Object.prototype` is read as present on the Ed25519 row. */
|
|
72
|
+
var LEAF_SCHEMES_BY_SPKI_ALG = _sealTable(intrinsic.assign(intrinsic.create(null), {
|
|
56
73
|
ecPublicKey: { ECDSA: 1 },
|
|
57
74
|
rsaEncryption: { "RSASSA-PKCS1-v1_5": 1, "RSA-PSS": 1 },
|
|
58
75
|
rsassaPss: { "RSA-PSS": 1 },
|
|
@@ -61,13 +78,13 @@ var LEAF_SCHEMES_BY_SPKI_ALG = Object.assign(Object.create(null), {
|
|
|
61
78
|
"id-ml-dsa-44": { "ML-DSA-44": 1 },
|
|
62
79
|
"id-ml-dsa-65": { "ML-DSA-65": 1 },
|
|
63
80
|
"id-ml-dsa-87": { "ML-DSA-87": 1 },
|
|
64
|
-
});
|
|
81
|
+
}));
|
|
65
82
|
|
|
66
83
|
function _schemeE(kind, message, cause) { return new WebauthnError("webauthn/" + kind, message, cause); }
|
|
67
84
|
|
|
68
85
|
function _deriveBlobAlgs() {
|
|
69
|
-
var out =
|
|
70
|
-
jose.sigAlgs()
|
|
86
|
+
var out = intrinsic.create(null);
|
|
87
|
+
intrinsic.forEach(jose.sigAlgs(), function (row) {
|
|
71
88
|
if (row.kty === "EC") {
|
|
72
89
|
out[row.alg] = { scheme: "ECDSA", hash: row.hash,
|
|
73
90
|
imp: { name: "ECDSA", namedCurve: row.crv }, ver: { name: "ECDSA", hash: row.hash } };
|
|
@@ -85,38 +102,65 @@ function _deriveBlobAlgs() {
|
|
|
85
102
|
});
|
|
86
103
|
return out;
|
|
87
104
|
}
|
|
88
|
-
|
|
105
|
+
/** @internal Sealed all the way down, because this table is EXPORTED and its NESTED rows carry the
|
|
106
|
+
* import and verify parameters a BLOB signature is checked under. A caller holding the object could
|
|
107
|
+
* redefine `RS256`'s hash, and the leaf key would then be imported for a weaker digest than the
|
|
108
|
+
* header declares. Freezing alone is not enough: a row written as an object literal still INHERITS,
|
|
109
|
+
* so `fromLeaf` installed on `Object.prototype` is read off every frozen row and sends the strongest
|
|
110
|
+
* algorithm down the take-it-from-the-leaf branch, which is why the prototype goes too. */
|
|
111
|
+
var BLOB_ALGS = _sealTable(_deriveBlobAlgs());
|
|
89
112
|
|
|
90
113
|
function _blobAlgParams(algRow, leafAlgName) {
|
|
91
114
|
if (!algRow.fromLeaf) return algRow;
|
|
92
|
-
return { scheme: algRow.scheme, hash: null, imp: { name: leafAlgName }, ver: { name: leafAlgName } };
|
|
115
|
+
return _sealTable({ scheme: algRow.scheme, hash: null, imp: { name: leafAlgName }, ver: { name: leafAlgName } });
|
|
93
116
|
}
|
|
94
117
|
|
|
95
|
-
|
|
118
|
+
/** @internal Frozen, because this table is EXPORTED and membership in it is what makes a status report
|
|
119
|
+
* disqualifying. A caller holding the object could redefine `REVOKED` so the lookup stops recognizing
|
|
120
|
+
* revocation, and `statusDenied` then permits a revoked authenticator with every operation around the
|
|
121
|
+
* lookup still captured. */
|
|
122
|
+
var DISQUALIFYING = intrinsic.freeze(intrinsic.assign(intrinsic.create(null), {
|
|
96
123
|
REVOKED: 1, ATTESTATION_KEY_COMPROMISE: 1, USER_KEY_REMOTE_COMPROMISE: 1,
|
|
97
124
|
USER_KEY_PHYSICAL_COMPROMISE: 1, USER_VERIFICATION_BYPASS: 1,
|
|
98
|
-
});
|
|
125
|
+
}));
|
|
99
126
|
|
|
100
|
-
var CERT_SCOPED_STATUS =
|
|
127
|
+
var CERT_SCOPED_STATUS = intrinsic.freeze(intrinsic.assign(intrinsic.create(null), { ATTESTATION_KEY_COMPROMISE: 1 }));
|
|
101
128
|
|
|
102
|
-
var _BLOB_OPTS =
|
|
129
|
+
var _BLOB_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
103
130
|
rootCertificates: 1, time: 1, previousNo: 1, requireRollbackCheck: 1, allowStale: 1,
|
|
104
131
|
statusPolicy: 1, rejectUnknownStatus: 1,
|
|
105
132
|
});
|
|
106
133
|
|
|
107
|
-
function _isPlainObject(v) { return !!v && typeof v === "object" && !
|
|
134
|
+
function _isPlainObject(v) { return !!v && typeof v === "object" && !intrinsic.isArray(v); }
|
|
108
135
|
|
|
136
|
+
/** @internal Both markers are read through captures. Membership here is what says an object is a
|
|
137
|
+
* verified BLOB result rather than a raw one a caller assembled, so a replacement answering `true`
|
|
138
|
+
* admits unverified metadata wherever `opts.metadata` is accepted; the second binds an entry to the
|
|
139
|
+
* result it was read out of, and one answering with the caller's own object passes an entry off as
|
|
140
|
+
* belonging to metadata that never carried it. */
|
|
109
141
|
var _verifiedResults = new WeakSet();
|
|
110
|
-
function isVerifiedResult(v) { return _isPlainObject(v) &&
|
|
142
|
+
function isVerifiedResult(v) { return _isPlainObject(v) && intrinsic.weakSetHas(_verifiedResults, v); }
|
|
111
143
|
|
|
112
144
|
var _entryOrigin = new WeakMap();
|
|
113
|
-
function _isEntryOf(entry, metadata) { return
|
|
145
|
+
function _isEntryOf(entry, metadata) { return intrinsic.weakGet(_entryOrigin, entry) === metadata; }
|
|
146
|
+
|
|
147
|
+
/** @internal A decision table and every row in it, with no prototype and nothing writable. Each row is
|
|
148
|
+
* looked up by a name taken from the input, so an inherited property answers for a row the table does
|
|
149
|
+
* not hold and a writable one answers differently than the table says. Detaching the prototype has to
|
|
150
|
+
* come first, since a frozen object's prototype can no longer be set. Arrays keep theirs: a row list
|
|
151
|
+
* is still indexed and walked. */
|
|
152
|
+
function _sealTable(v) {
|
|
153
|
+
if (!v || typeof v !== "object" || intrinsic.isView(v)) return v;
|
|
154
|
+
if (!intrinsic.isArray(v)) intrinsic.setPrototypeOf(v, null);
|
|
155
|
+
intrinsic.forEach(intrinsic.getOwnPropertyNames(v), function (k) { _sealTable(v[k]); });
|
|
156
|
+
return intrinsic.freeze(v);
|
|
157
|
+
}
|
|
114
158
|
|
|
115
159
|
function _deepFreeze(v, depth) {
|
|
116
|
-
if (!v || typeof v !== "object" ||
|
|
160
|
+
if (!v || typeof v !== "object" || intrinsic.isFrozen(v) || intrinsic.isView(v)) return v;
|
|
117
161
|
if (depth > C.JSON_MAX_DEPTH) return v;
|
|
118
|
-
|
|
119
|
-
|
|
162
|
+
intrinsic.freeze(v);
|
|
163
|
+
intrinsic.forEach(intrinsic.keys(v), function (k) { _deepFreeze(v[k], depth + 1); });
|
|
120
164
|
return v;
|
|
121
165
|
}
|
|
122
166
|
function _isAaguid(s) {
|
|
@@ -132,9 +176,9 @@ function _isAaguid(s) {
|
|
|
132
176
|
return true;
|
|
133
177
|
}
|
|
134
178
|
var ZERO_AAGUID = "00000000-0000-0000-0000-000000000000";
|
|
135
|
-
var UNDERSTOOD_HEADER =
|
|
179
|
+
var UNDERSTOOD_HEADER = intrinsic.freeze(intrinsic.assign(intrinsic.create(null), {
|
|
136
180
|
alg: 1, typ: 1, cty: 1, crit: 1, jku: 1, jwk: 1, kid: 1, x5u: 1, x5c: 1, x5t: 1, "x5t#S256": 1,
|
|
137
|
-
});
|
|
181
|
+
}));
|
|
138
182
|
|
|
139
183
|
function _assertLeafSigns(leaf) {
|
|
140
184
|
var ku = pkix.keyUsageOf(_KU_NS, leaf, _err, "webauthn/bad-att-cert", "metadata BLOB x5c leaf");
|
|
@@ -143,9 +187,12 @@ function _assertLeafSigns(leaf) {
|
|
|
143
187
|
}
|
|
144
188
|
}
|
|
145
189
|
|
|
190
|
+
/** @internal The key comparison comes from the capture. Answering `true` makes a certificate that
|
|
191
|
+
* merely carries the anchor's subject count as the anchor itself, and a single-certificate chain is
|
|
192
|
+
* then reported trusted with no path validation run over it. */
|
|
146
193
|
function _isAnchorItself(cert, anchor) {
|
|
147
194
|
return guard.name.dnEqual(cert.subject.rdns, anchor.subject.rdns, _err, "webauthn/bad-att-cert", "the anchor subject") &&
|
|
148
|
-
cert.subjectPublicKeyInfo.bytes
|
|
195
|
+
intrinsic.bufferEquals(cert.subjectPublicKeyInfo.bytes, anchor.subjectPublicKeyInfo.bytes);
|
|
149
196
|
}
|
|
150
197
|
|
|
151
198
|
function _asCert(v, label) {
|
|
@@ -160,7 +207,10 @@ function verifyMetadataBlob(blob, opts) {
|
|
|
160
207
|
}
|
|
161
208
|
|
|
162
209
|
function _verifyMetadataBlob(blob, opts) {
|
|
163
|
-
|
|
210
|
+
/** @internal A caller who passes no options gets a record with NO prototype. Defaulted to `{}` it
|
|
211
|
+
* inherits, and the snapshot below materializes every declared option name, so `allowStale`
|
|
212
|
+
* installed on `Object.prototype` reached the freshness check as an opt-in the caller never made. */
|
|
213
|
+
opts = opts || intrinsic.create(null);
|
|
164
214
|
if (!_isPlainObject(opts)) throw _err("webauthn/bad-input", "opts must be an object");
|
|
165
215
|
guard.identifier.assertKnownKeys(opts, _BLOB_OPTS, _err, "webauthn/bad-input", "opts has an unknown key ");
|
|
166
216
|
/** @internal Every option is read here, once, into a plain snapshot, and validation, the checks and
|
|
@@ -171,15 +221,15 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
171
221
|
var o = guard.identifier.snapshotOptions(opts, _BLOB_OPTS);
|
|
172
222
|
|
|
173
223
|
var roots = o.rootCertificates;
|
|
174
|
-
if (!
|
|
224
|
+
if (!intrinsic.isArray(roots) || roots.length === 0) {
|
|
175
225
|
throw _err("webauthn/metadata-no-root", "verifying a metadata BLOB requires opts.rootCertificates -- the FIDO root(s) to anchor it to; this library bundles none");
|
|
176
226
|
}
|
|
177
227
|
if (o.time !== undefined) guard.time.assertValid(o.time, _err, "webauthn/bad-input", "opts.time");
|
|
178
|
-
var at = o.time === undefined ? new Date() : o.time;
|
|
179
|
-
if (o.previousNo !== undefined && (!
|
|
228
|
+
var at = o.time === undefined ? new intrinsic.Date() : o.time;
|
|
229
|
+
if (o.previousNo !== undefined && (!intrinsic.isSafeInteger(o.previousNo) || o.previousNo < 0)) {
|
|
180
230
|
throw _err("webauthn/bad-input", "opts.previousNo must be a non-negative safe integer");
|
|
181
231
|
}
|
|
182
|
-
["requireRollbackCheck", "allowStale", "rejectUnknownStatus"]
|
|
232
|
+
intrinsic.forEach(["requireRollbackCheck", "allowStale", "rejectUnknownStatus"], function (k) {
|
|
183
233
|
if (o[k] !== undefined && typeof o[k] !== "boolean") {
|
|
184
234
|
throw _err("webauthn/bad-input", "opts." + k + " must be a boolean");
|
|
185
235
|
}
|
|
@@ -192,40 +242,43 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
192
242
|
throw _err("webauthn/metadata-no-baseline", "opts.requireRollbackCheck was set without opts.previousNo, so there is no baseline to compare against");
|
|
193
243
|
}
|
|
194
244
|
opts = o;
|
|
195
|
-
var anchors =
|
|
245
|
+
var anchors = intrinsic.mapList(roots, function (r, i) { return _asCert(r, "opts.rootCertificates[" + i + "]"); });
|
|
196
246
|
|
|
197
247
|
var raw = guard.bytes.isByteSource(blob)
|
|
198
248
|
? guard.bytes.source(blob, WebauthnError, "webauthn/bad-input", "the metadata BLOB") : null;
|
|
199
|
-
var declaredLength = raw ? raw.length : (typeof blob === "string" ?
|
|
249
|
+
var declaredLength = raw ? raw.length : (typeof blob === "string" ? intrinsic.byteLength(blob, "utf8") : null);
|
|
200
250
|
if (declaredLength !== null && declaredLength > C.MDS_BLOB_MAX_BYTES) {
|
|
201
251
|
throw _err("webauthn/too-large", "the metadata BLOB is " + declaredLength + " bytes, above the " + C.MDS_BLOB_MAX_BYTES + "-byte ceiling");
|
|
202
252
|
}
|
|
203
253
|
if (!raw && typeof blob !== "string") {
|
|
204
254
|
throw _err("webauthn/bad-input", "the metadata BLOB must be bytes or a string");
|
|
205
255
|
}
|
|
206
|
-
var bytes = raw ||
|
|
256
|
+
var bytes = raw || intrinsic.bufferFrom(blob, "utf8");
|
|
207
257
|
if (bytes.length > C.MDS_BLOB_MAX_BYTES) {
|
|
208
258
|
throw _err("webauthn/too-large", "the metadata BLOB is " + bytes.length + " bytes, above the " + C.MDS_BLOB_MAX_BYTES + "-byte ceiling");
|
|
209
259
|
}
|
|
210
|
-
|
|
260
|
+
/** @internal The decode and the split that divides the JWS are captured. They decide which bytes are
|
|
261
|
+
* the signed input and which are the signature, so a replacement chooses what the verification
|
|
262
|
+
* below covers. */
|
|
263
|
+
var segs = intrinsic.splitChar(intrinsic.bufToString(bytes, "utf8"), 46);
|
|
211
264
|
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)");
|
|
212
|
-
if (segs[0].length >
|
|
265
|
+
if (segs[0].length > intrinsic.ceil(C.MDS_BLOB_HEADER_MAX_BYTES / 3) * 4) {
|
|
213
266
|
throw _err("webauthn/too-large", "the metadata BLOB protected header is above the " + C.MDS_BLOB_HEADER_MAX_BYTES + "-byte ceiling");
|
|
214
267
|
}
|
|
215
|
-
if (segs[2].length >
|
|
268
|
+
if (segs[2].length > intrinsic.ceil(C.MDS_BLOB_SIG_MAX_BYTES / 3) * 4) {
|
|
216
269
|
throw _err("webauthn/too-large", "the metadata BLOB signature is above the " + C.MDS_BLOB_SIG_MAX_BYTES + "-byte ceiling");
|
|
217
270
|
}
|
|
218
271
|
var header, sig;
|
|
219
272
|
try {
|
|
220
273
|
/** @internal The header carries `alg`, `typ` and the `x5c` chain, all strings, and no number this
|
|
221
274
|
* module reads as an integer, so there is nothing here for the rule to apply to. */
|
|
222
|
-
header = guard.json.parse(
|
|
275
|
+
header = guard.json.parse(intrinsic.bufferFrom(jose.base64url.decode(segs[0])), _err, {
|
|
223
276
|
maxBytes: C.MDS_BLOB_HEADER_MAX_BYTES, maxDepth: C.JSON_MAX_DEPTH,
|
|
224
277
|
tooLarge: "webauthn/too-large", badJson: "webauthn/bad-metadata-blob",
|
|
225
278
|
tooDeep: "webauthn/bad-metadata-blob", duplicateMember: "webauthn/bad-metadata-blob",
|
|
226
279
|
badInput: "webauthn/bad-metadata-blob", label: "the metadata BLOB header",
|
|
227
280
|
});
|
|
228
|
-
sig =
|
|
281
|
+
sig = intrinsic.bufferFrom(jose.base64url.decode(segs[2]));
|
|
229
282
|
} catch (e) {
|
|
230
283
|
if (e instanceof WebauthnError) throw e;
|
|
231
284
|
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header or signature is not decodable", e);
|
|
@@ -236,30 +289,32 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
236
289
|
}
|
|
237
290
|
if (_hasOwn(header, "crit")) {
|
|
238
291
|
var crit = header.crit;
|
|
239
|
-
if (!
|
|
292
|
+
if (!intrinsic.isArray(crit) || crit.length === 0) {
|
|
240
293
|
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header crit must be a non-empty array (RFC 7515 sec. 4.1.11)");
|
|
241
294
|
}
|
|
242
|
-
var critSeen =
|
|
295
|
+
var critSeen = intrinsic.create(null);
|
|
243
296
|
for (var ci = 0; ci < crit.length; ci++) {
|
|
244
297
|
var critName = crit[ci];
|
|
245
298
|
if (typeof critName !== "string") throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header crit entries must be strings");
|
|
246
|
-
if (critSeen[critName]) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header crit repeats " +
|
|
299
|
+
if (critSeen[critName]) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header crit repeats " + intrinsic.stringify(critName));
|
|
247
300
|
critSeen[critName] = 1;
|
|
248
301
|
if (UNDERSTOOD_HEADER[critName]) {
|
|
249
|
-
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header crit must not name the standard header parameter " +
|
|
302
|
+
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header crit must not name the standard header parameter " + intrinsic.stringify(critName) + " (RFC 7515 sec. 4.1.11)");
|
|
250
303
|
}
|
|
251
|
-
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header names critical header parameter " +
|
|
304
|
+
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header names critical header parameter " + intrinsic.stringify(critName) + ", which this reader does not process");
|
|
252
305
|
}
|
|
253
306
|
}
|
|
254
307
|
var algRow = typeof header.alg === "string" ? BLOB_ALGS[header.alg] : undefined;
|
|
255
|
-
if (!algRow) throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " +
|
|
256
|
-
if (!
|
|
308
|
+
if (!algRow) throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + intrinsic.stringify(header.alg) + " is not a supported JWS signature algorithm");
|
|
309
|
+
if (!intrinsic.isArray(header.x5c) || header.x5c.length === 0) {
|
|
257
310
|
throw _err("webauthn/bad-metadata-blob", "the metadata BLOB header carries no x5c certificate chain (RFC 7515 sec. 4.1.6)");
|
|
258
311
|
}
|
|
259
312
|
if (header.x5c.length > C.WEBAUTHN_X5C_MAX_CERTS) {
|
|
260
313
|
throw _err("webauthn/too-large", "the metadata BLOB x5c carries " + header.x5c.length + " certificates, above the " + C.WEBAUTHN_X5C_MAX_CERTS + " this library will parse");
|
|
261
314
|
}
|
|
262
|
-
|
|
315
|
+
/** @internal The walk over the x5c entries is captured. It produces the chain the BLOB signature is
|
|
316
|
+
* verified under and anchored with, so a walk answering with certificates of its own replaces both. */
|
|
317
|
+
var chain = intrinsic.mapList(header.x5c, function (entry, i) {
|
|
263
318
|
if (typeof entry !== "string") throw _err("webauthn/bad-metadata-blob", "the metadata BLOB x5c entry " + i + " is not a string");
|
|
264
319
|
var der;
|
|
265
320
|
try { der = guard.encoding.base64(entry, C.MDS_BLOB_MAX_BYTES, _err, "webauthn/bad-metadata-blob", "a metadata BLOB x5c entry"); }
|
|
@@ -272,7 +327,7 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
272
327
|
var leafAlg = (leaf.subjectPublicKeyInfo.algorithm || {}).name;
|
|
273
328
|
var leafSchemes = typeof leafAlg === "string" ? LEAF_SCHEMES_BY_SPKI_ALG[leafAlg] : undefined;
|
|
274
329
|
if (!leafSchemes || !leafSchemes[algRow.scheme]) {
|
|
275
|
-
throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + header.alg + " does not match the x5c leaf key type " +
|
|
330
|
+
throw _err("webauthn/unsupported-algorithm", "the metadata BLOB alg " + header.alg + " does not match the x5c leaf key type " + intrinsic.stringify(leafAlg));
|
|
276
331
|
}
|
|
277
332
|
if (leafAlg === "rsassaPss") {
|
|
278
333
|
var pinnedHash = signScheme.pssSpkiPinnedHash(leaf, _schemeE);
|
|
@@ -286,41 +341,64 @@ function _verifyMetadataBlob(blob, opts) {
|
|
|
286
341
|
WebauthnError, "webauthn/bad-att-cert");
|
|
287
342
|
}
|
|
288
343
|
var params = _blobAlgParams(algRow, leafAlg);
|
|
289
|
-
var signingInput =
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
.
|
|
300
|
-
|
|
344
|
+
var signingInput = intrinsic.bufferFrom(segs[0] + "." + segs[1], "ascii");
|
|
345
|
+
/** @internal Each step of this chain is continued through the captured `then`, because each one's
|
|
346
|
+
* resolved value decides the next: a replacement settling the import step hands a key nothing
|
|
347
|
+
* imported to the verify, settling the verify step reports a signature that was never checked as
|
|
348
|
+
* one that verified, and settling the anchoring step brands the catalogue verified without the
|
|
349
|
+
* chain having reached a root. The catalogue's status entries and attestation roots govern
|
|
350
|
+
* WebAuthn verification once it is branded. */
|
|
351
|
+
var imported = _subtleImportKey(webcrypto.webcrypto.subtle, "spki", leaf.subjectPublicKeyInfo.bytes, params.imp, false, ["verify"]);
|
|
352
|
+
var verified = intrinsic.chain(imported, function (key) {
|
|
353
|
+
var checked = _subtleVerify(webcrypto.webcrypto.subtle, params.ver, key, sig, signingInput);
|
|
354
|
+
return intrinsic.chain(checked, undefined, function (e) {
|
|
355
|
+
throw _err("webauthn/verify-error", "the metadata BLOB signature could not be evaluated under its x5c leaf key", e);
|
|
301
356
|
});
|
|
357
|
+
}, function (e) { throw _err("webauthn/unsupported-algorithm", "the metadata BLOB x5c leaf key could not be imported for " + header.alg, e); });
|
|
358
|
+
var anchored = intrinsic.chain(verified, function (ok) {
|
|
359
|
+
if (!ok) throw _err("webauthn/verify-failed", "the metadata BLOB signature does not verify under its x5c leaf key");
|
|
360
|
+
return _chainToAnchor(chain, anchors, at);
|
|
361
|
+
});
|
|
362
|
+
return intrinsic.chain(anchored, function () {
|
|
363
|
+
return _parsePayload(segs[1], at, opts);
|
|
364
|
+
});
|
|
302
365
|
}
|
|
303
366
|
|
|
304
367
|
function _chainToAnchor(chain, anchors, at, what, code) {
|
|
305
368
|
var subject = what || "metadata BLOB certificate chain";
|
|
306
369
|
var faultCode = code || "webauthn/metadata-untrusted";
|
|
307
|
-
var ordered = _reverse(
|
|
370
|
+
var ordered = _reverse(intrinsic.copyList(chain));
|
|
308
371
|
var lastFault = null;
|
|
309
|
-
|
|
310
|
-
|
|
372
|
+
/** @internal The fold over the anchors is captured. It is what walks each candidate anchor until
|
|
373
|
+
* one validates, so a replacement handing back a settled `true` without visiting an anchor reports
|
|
374
|
+
* the chain trusted against a root nothing compared it with. */
|
|
375
|
+
var walked = intrinsic.reduce(anchors, function (p, anchor) {
|
|
376
|
+
/** @internal The continuation is taken through the captured `then`. Read off the promise at the
|
|
377
|
+
* call, a replacement settles `true` without running this callback at all, which feeds `true`
|
|
378
|
+
* into the `false` seed, skips the chain validation below, and reports a chain unrelated to
|
|
379
|
+
* every supplied root as trusted. */
|
|
380
|
+
return intrinsic.chain(p, function (done) {
|
|
311
381
|
if (done) return true;
|
|
312
|
-
|
|
382
|
+
/** @internal Both copies are built without a construction protocol. The first is the chain this
|
|
383
|
+
* anchor is tried against, and the second is what is left once a self-presented anchor is
|
|
384
|
+
* stripped, so a copy answering with fewer elements leaves nothing to validate and the empty
|
|
385
|
+
* path reports trusted on the strength of the strip alone. A captured `slice` is not enough,
|
|
386
|
+
* since it runs ArraySpeciesCreate and hands its elements to whatever the species built. */
|
|
387
|
+
var path = intrinsic.copyList(ordered);
|
|
313
388
|
var strippedAnchor = false;
|
|
314
|
-
if (path.length && _isAnchorItself(path[0], anchor)) { path =
|
|
389
|
+
if (path.length && _isAnchorItself(path[0], anchor)) { path = intrinsic.copyList(path, 1); strippedAnchor = true; }
|
|
315
390
|
if (path.length === 0) return strippedAnchor;
|
|
316
|
-
|
|
391
|
+
var verdict = pathValidate.validate(path, {
|
|
317
392
|
time: at,
|
|
318
393
|
trustAnchors: { name: anchor.subject, publicKey: anchor.subjectPublicKeyInfo.bytes,
|
|
319
394
|
algorithm: anchor.subjectPublicKeyInfo.algorithm.oid,
|
|
320
395
|
parameters: anchor.subjectPublicKeyInfo.algorithm.parameters },
|
|
321
|
-
})
|
|
396
|
+
});
|
|
397
|
+
return intrinsic.chain(verdict, function (r) { return !!(r && r.valid); },
|
|
398
|
+
function (e) { lastFault = e; return false; });
|
|
322
399
|
});
|
|
323
|
-
}, _promiseResolve(_Promise, false))
|
|
400
|
+
}, _promiseResolve(_Promise, false));
|
|
401
|
+
return intrinsic.chain(walked, function (trusted) {
|
|
324
402
|
if (!trusted) {
|
|
325
403
|
throw _err(faultCode, "the " + subject + " does not validate to any of the roots it must reach", lastFault);
|
|
326
404
|
}
|
|
@@ -337,7 +415,7 @@ function assertFresh(metadata, at, label) {
|
|
|
337
415
|
if (!metadata || metadata.allowStale === true || typeof metadata.nextUpdate !== "string") return;
|
|
338
416
|
var atMs = guard.time.isDate(at) ? guard.time.instantOf(at) : NaN;
|
|
339
417
|
var limit = _staleAfter(metadata.nextUpdate);
|
|
340
|
-
if (!
|
|
418
|
+
if (!intrinsic.numberIsFinite(atMs) || !intrinsic.numberIsFinite(limit)) return;
|
|
341
419
|
if (atMs >= limit) {
|
|
342
420
|
throw _err("webauthn/metadata-stale", (label || "the metadata") + " expired after " + metadata.nextUpdate +
|
|
343
421
|
"; re-verify a current BLOB, or pass opts.allowStale when verifying it");
|
|
@@ -357,7 +435,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
357
435
|
* `selfAttestedFRR` and `selfAttestedFAR`, the biometric false-reject and false-accept rates, and
|
|
358
436
|
* `iAPARThreshold`. Measured on the live BLOB from mds3.fidoalliance.org rather than read off the
|
|
359
437
|
* specification, because the catalogue is what has to be ingestible. */
|
|
360
|
-
payload = guard.json.parse(
|
|
438
|
+
payload = guard.json.parse(intrinsic.bufferFrom(jose.base64url.decode(seg)), _err, {
|
|
361
439
|
maxBytes: C.MDS_BLOB_MAX_BYTES, maxDepth: C.JSON_MAX_DEPTH,
|
|
362
440
|
exactIntegerMembers: EXACT_INTEGER_BLOB_MEMBERS,
|
|
363
441
|
tooLarge: "webauthn/too-large", badJson: "webauthn/bad-metadata-blob",
|
|
@@ -370,36 +448,40 @@ function _parsePayload(seg, at, opts) {
|
|
|
370
448
|
}
|
|
371
449
|
if (!_isPlainObject(payload)) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must be a JSON object");
|
|
372
450
|
if (typeof payload.legalHeader !== "string") throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a string legalHeader");
|
|
373
|
-
if (!
|
|
451
|
+
if (!intrinsic.isSafeInteger(payload.no) || payload.no < 0) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry a non-negative integer no");
|
|
374
452
|
if (opts.previousNo !== undefined && payload.no <= opts.previousNo) {
|
|
375
453
|
throw _err("webauthn/metadata-rollback", "the metadata BLOB no " + payload.no + " does not exceed the previously held " + opts.previousNo);
|
|
376
454
|
}
|
|
377
455
|
var staleAfter = _staleAfter(payload.nextUpdate);
|
|
378
456
|
var atMs = guard.time.instantOf(at);
|
|
379
|
-
if (!
|
|
457
|
+
if (!intrinsic.numberIsFinite(atMs) || !intrinsic.numberIsFinite(staleAfter)) {
|
|
380
458
|
throw _err("webauthn/bad-input", "the metadata freshness comparison has no usable instant");
|
|
381
459
|
}
|
|
382
460
|
var stale = atMs >= staleAfter;
|
|
383
461
|
if (stale && opts.allowStale !== true) {
|
|
384
462
|
throw _err("webauthn/metadata-stale", "the metadata BLOB expired after " + payload.nextUpdate + "; pass opts.allowStale to accept it anyway");
|
|
385
463
|
}
|
|
386
|
-
if (!
|
|
464
|
+
if (!intrinsic.isArray(payload.entries)) throw _err("webauthn/bad-metadata-blob", "the metadata BLOB payload must carry an entries array");
|
|
387
465
|
if (payload.entries.length > C.MDS_MAX_ENTRIES) {
|
|
388
466
|
throw _err("webauthn/too-large", "the metadata BLOB declares " + payload.entries.length + " entries, above the " + C.MDS_MAX_ENTRIES + " ceiling");
|
|
389
467
|
}
|
|
390
468
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
469
|
+
/** @internal The two lookup indexes and the walk that fills them are captured. An entry is found
|
|
470
|
+
* through these, and the entry supplies both the status reports that refuse a revoked authenticator
|
|
471
|
+
* and the attestation roots a chain is anchored to, so an index handed back already holding a key,
|
|
472
|
+
* or a walk that drops an entry, decides which entry an attestation is governed by. */
|
|
473
|
+
var byAaguid = intrinsic.create(null);
|
|
474
|
+
var byKeyIdentifier = intrinsic.create(null);
|
|
475
|
+
var entries = intrinsic.mapList(payload.entries, function (e, i) {
|
|
394
476
|
if (!_isPlainObject(e)) throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " is not an object");
|
|
395
|
-
if (!
|
|
477
|
+
if (!intrinsic.isArray(e.statusReports) || e.statusReports.length === 0) {
|
|
396
478
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " must carry a non-empty statusReports array");
|
|
397
479
|
}
|
|
398
480
|
if (e.statusReports.length > C.MDS_MAX_STATUS_REPORTS_PER_ENTRY) {
|
|
399
481
|
throw _err("webauthn/too-large", "metadata entry " + i + " declares " + e.statusReports.length +
|
|
400
482
|
" status reports, above the " + C.MDS_MAX_STATUS_REPORTS_PER_ENTRY + " ceiling");
|
|
401
483
|
}
|
|
402
|
-
e.statusReports
|
|
484
|
+
intrinsic.forEach(e.statusReports, function (r, ri) {
|
|
403
485
|
if (!_isPlainObject(r)) throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " status report " + ri + " is not an object");
|
|
404
486
|
if (typeof r.status !== "string" || !r.status) {
|
|
405
487
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " status report " + ri + " has no status (MDS v3.0 sec. 3.1.3 requires one)");
|
|
@@ -407,31 +489,32 @@ function _parsePayload(seg, at, opts) {
|
|
|
407
489
|
});
|
|
408
490
|
var aaguid = null;
|
|
409
491
|
if (e.aaguid !== undefined) {
|
|
410
|
-
if (typeof e.aaguid !== "string" || !_isAaguid(e.aaguid
|
|
492
|
+
if (typeof e.aaguid !== "string" || !_isAaguid(_strToLowerCase(e.aaguid))) {
|
|
411
493
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " has a malformed aaguid");
|
|
412
494
|
}
|
|
413
|
-
aaguid = e.aaguid
|
|
495
|
+
aaguid = _strToLowerCase(e.aaguid);
|
|
414
496
|
}
|
|
415
497
|
var st = e.metadataStatement;
|
|
416
498
|
var keyIds = [];
|
|
417
|
-
var seenKeyId =
|
|
418
|
-
|
|
419
|
-
.
|
|
499
|
+
var seenKeyId = intrinsic.create(null);
|
|
500
|
+
intrinsic.forEach(
|
|
501
|
+
[[e.attestationCertificateKeyIdentifiers, "entry"], [st && st.attestationCertificateKeyIdentifiers, "metadataStatement"]],
|
|
502
|
+
function (pair) {
|
|
420
503
|
var list = pair[0];
|
|
421
504
|
if (list === undefined) return;
|
|
422
|
-
if (!
|
|
505
|
+
if (!intrinsic.isArray(list)) {
|
|
423
506
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " " + pair[1] + " attestationCertificateKeyIdentifiers is not an array");
|
|
424
507
|
}
|
|
425
508
|
if (list.length > C.MDS_MAX_KEY_IDS_PER_ENTRY) {
|
|
426
509
|
throw _err("webauthn/too-large", "metadata entry " + i + " declares " + list.length +
|
|
427
510
|
" attestation certificate key identifiers, above the " + C.MDS_MAX_KEY_IDS_PER_ENTRY + " ceiling");
|
|
428
511
|
}
|
|
429
|
-
|
|
512
|
+
intrinsic.forEach(list, function (k) {
|
|
430
513
|
if (typeof k !== "string" || k.length !== 40 || !pkix.allCharsIn(k, _KEYID_HEX_CI)) {
|
|
431
514
|
throw _err("webauthn/bad-metadata-blob", "metadata entry " + i + " has a malformed attestation certificate key identifier");
|
|
432
515
|
}
|
|
433
|
-
var lower = k
|
|
434
|
-
if (!seenKeyId[lower]) { seenKeyId[lower] = 1;
|
|
516
|
+
var lower = _strToLowerCase(k);
|
|
517
|
+
if (!seenKeyId[lower]) { seenKeyId[lower] = 1; guard.list.append(keyIds, lower); }
|
|
435
518
|
});
|
|
436
519
|
});
|
|
437
520
|
var out = { index: i, aaguid: aaguid, keyIdentifiers: keyIds, statusReports: e.statusReports,
|
|
@@ -440,7 +523,7 @@ function _parsePayload(seg, at, opts) {
|
|
|
440
523
|
if (byAaguid[aaguid]) throw _err("webauthn/duplicate-metadata-entry", "two metadata entries claim aaguid " + aaguid);
|
|
441
524
|
byAaguid[aaguid] = out;
|
|
442
525
|
}
|
|
443
|
-
|
|
526
|
+
intrinsic.forEach(keyIds, function (k) {
|
|
444
527
|
if (byKeyIdentifier[k]) throw _err("webauthn/duplicate-metadata-entry", "two metadata entries claim attestation certificate key identifier " + k);
|
|
445
528
|
byKeyIdentifier[k] = out;
|
|
446
529
|
});
|
|
@@ -453,15 +536,18 @@ function _parsePayload(seg, at, opts) {
|
|
|
453
536
|
entries: entries, byAaguid: byAaguid, byKeyIdentifier: byKeyIdentifier,
|
|
454
537
|
statusPolicy: opts.statusPolicy || "any", rejectUnknownStatus: opts.rejectUnknownStatus === true };
|
|
455
538
|
_deepFreeze(result, 0);
|
|
456
|
-
|
|
457
|
-
|
|
539
|
+
intrinsic.weakSetAdd(_verifiedResults, result);
|
|
540
|
+
intrinsic.forEach(entries, function (e) { intrinsic.weakSet(_entryOrigin, e, result); });
|
|
458
541
|
return result;
|
|
459
542
|
}
|
|
460
543
|
|
|
461
544
|
function metadataFor(metadata, identifier) {
|
|
462
545
|
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");
|
|
463
546
|
if (typeof identifier !== "string") return null;
|
|
464
|
-
|
|
547
|
+
/** @internal The fold that builds the lookup key is captured. The entry this key finds governs the
|
|
548
|
+
* attestation, supplying its status reports and its attestation roots, so a fold answering with
|
|
549
|
+
* another identifier hands the attestation another authenticator's entry. */
|
|
550
|
+
var key = _strToLowerCase(identifier);
|
|
465
551
|
if (_isAaguid(key)) {
|
|
466
552
|
if (key === ZERO_AAGUID) return null;
|
|
467
553
|
return metadata.byAaguid[key] || null;
|
|
@@ -470,12 +556,12 @@ function metadataFor(metadata, identifier) {
|
|
|
470
556
|
return null;
|
|
471
557
|
}
|
|
472
558
|
|
|
473
|
-
var _ANCHOR_OPTS =
|
|
559
|
+
var _ANCHOR_OPTS = intrinsic.assign(intrinsic.create(null), { metadata: 1, time: 1, certificate: 1 });
|
|
474
560
|
|
|
475
561
|
function metadataAnchors(entry, opts) {
|
|
476
562
|
if (!entry || typeof entry !== "object") throw _err("webauthn/bad-input", "metadataAnchors expects a metadata entry");
|
|
477
|
-
opts = opts ||
|
|
478
|
-
if (typeof opts !== "object" ||
|
|
563
|
+
opts = opts || intrinsic.create(null);
|
|
564
|
+
if (typeof opts !== "object" || intrinsic.isArray(opts)) throw _err("webauthn/bad-input", "metadataAnchors opts must be an object");
|
|
479
565
|
guard.identifier.assertKnownKeys(opts, _ANCHOR_OPTS, _err, "webauthn/bad-input", "metadataAnchors opts has an unknown key ");
|
|
480
566
|
opts = guard.identifier.ownOptions(opts);
|
|
481
567
|
if (opts.time !== undefined) guard.time.assertValid(opts.time, _err, "webauthn/bad-input", "opts.time");
|
|
@@ -487,17 +573,24 @@ function metadataAnchors(entry, opts) {
|
|
|
487
573
|
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");
|
|
488
574
|
}
|
|
489
575
|
}
|
|
490
|
-
|
|
576
|
+
/** @internal The default instant is built with the captured constructor. It is the instant metadata
|
|
577
|
+
* freshness and every status report's effective date are compared against, so a replacement
|
|
578
|
+
* answering with an earlier one accepts expired metadata and suppresses a report already in
|
|
579
|
+
* effect. */
|
|
580
|
+
var at = opts.time === undefined ? new intrinsic.Date() : opts.time;
|
|
491
581
|
if (opts.metadata !== undefined) assertFresh(opts.metadata, at, "metadataAnchors");
|
|
492
582
|
if (statusDenied(entry, opts.metadata, opts.certificate, at)) {
|
|
493
583
|
throw _err("webauthn/metadata-status", "the metadata entry for this authenticator carries a disqualifying status report, so it registers no anchors to trust");
|
|
494
584
|
}
|
|
495
585
|
var st = entry.metadataStatement;
|
|
496
|
-
var list = st &&
|
|
586
|
+
var list = st && intrinsic.isArray(st.attestationRootCertificates) ? st.attestationRootCertificates : [];
|
|
497
587
|
if (list.length > C.MDS_MAX_ANCHORS_PER_ENTRY) {
|
|
498
588
|
throw _err("webauthn/too-large", "metadata entry " + entry.index + " declares " + list.length + " attestation roots, above the " + C.MDS_MAX_ANCHORS_PER_ENTRY + " ceiling");
|
|
499
589
|
}
|
|
500
|
-
|
|
590
|
+
/** @internal The walk that parses the registered roots is captured. These are the anchors an
|
|
591
|
+
* attestation path is validated to, so a walk answering with a certificate of its own supplies the
|
|
592
|
+
* anchor rather than reading it out of the entry. */
|
|
593
|
+
return intrinsic.mapList(list, function (b64, i) {
|
|
501
594
|
var der;
|
|
502
595
|
try { der = guard.encoding.base64(b64, C.MDS_BLOB_MAX_BYTES, _err, "webauthn/bad-metadata-entry", "an attestation root certificate"); }
|
|
503
596
|
catch (e) { throw _err("webauthn/bad-metadata-entry", "metadata entry " + entry.index + " attestation root " + i + " is not canonical base64", e); }
|
|
@@ -523,63 +616,106 @@ function _reportInForceAt(report, atMs) {
|
|
|
523
616
|
return guard.time.instantOf(d) <= atMs;
|
|
524
617
|
}
|
|
525
618
|
|
|
619
|
+
/** @internal The one refusal that stops a revoked or compromised authenticator being trusted, written
|
|
620
|
+
* without any array operation that consults a replaceable protocol. A captured `filter` and `map`
|
|
621
|
+
* still build their result through ArraySpeciesCreate, which reads a constructor off the receiver, so
|
|
622
|
+
* a species replacement returning an ordinary object collects the matches as indexed properties while
|
|
623
|
+
* its `length` stays 0 and the membership test at the end visits nothing. `concat` reads
|
|
624
|
+
* `Symbol.isConcatSpreadable` the same way, and a `false` there leaves the two selections nested
|
|
625
|
+
* inside the result where no report has a `status` to recognize. Both end in a permit. Every
|
|
626
|
+
* selection here is therefore an index loop appending into an array literal, which creates through
|
|
627
|
+
* no protocol, and each report field is read as an OWN property so a caller-supplied record cannot
|
|
628
|
+
* inherit `effectiveDate` and have an undated revocation read as one that is not yet in force. */
|
|
526
629
|
function statusDenied(entry, metadata, leaf, at) {
|
|
527
630
|
var policy = (metadata && metadata.statusPolicy) || "any";
|
|
528
631
|
var reports = entry.statusReports || [];
|
|
529
632
|
if (typeof policy === "function") return policy(reports) === true;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
633
|
+
function dateOf(r) {
|
|
634
|
+
if (!r || !_hasOwn(r, "effectiveDate")) return null;
|
|
635
|
+
var d = r.effectiveDate;
|
|
636
|
+
return (typeof d === "string" && rfc3339.isValidDate(d)) ? d : null;
|
|
637
|
+
}
|
|
638
|
+
function statusOf(r) {
|
|
639
|
+
if (!r || !_hasOwn(r, "status")) return null;
|
|
640
|
+
return typeof r.status === "string" ? r.status : null;
|
|
641
|
+
}
|
|
642
|
+
var atMs = (guard.time.isDate(at) && intrinsic.numberIsFinite(guard.time.instantOf(at))) ? guard.time.instantOf(at) : null;
|
|
643
|
+
var i;
|
|
644
|
+
var inForce = [];
|
|
645
|
+
for (i = 0; i < reports.length; i++) {
|
|
646
|
+
var r0 = reports[i];
|
|
647
|
+
var s0 = statusOf(r0);
|
|
648
|
+
if (s0 !== null && CERT_SCOPED_STATUS[s0] && _reportNamesOtherCert(r0, leaf)) continue;
|
|
649
|
+
var d0 = dateOf(r0);
|
|
650
|
+
if (atMs !== null && d0 !== null && !_reportInForceAt(r0, atMs)) continue;
|
|
651
|
+
guard.list.append(inForce, r0);
|
|
652
|
+
}
|
|
653
|
+
var considered = inForce;
|
|
539
654
|
if (policy === "latest-by-date") {
|
|
540
|
-
var
|
|
541
|
-
|
|
542
|
-
var
|
|
543
|
-
|
|
544
|
-
|
|
655
|
+
var newest = null;
|
|
656
|
+
for (i = 0; i < inForce.length; i++) {
|
|
657
|
+
var dN = dateOf(inForce[i]);
|
|
658
|
+
if (dN !== null && (newest === null || dN >= newest)) newest = dN;
|
|
659
|
+
}
|
|
660
|
+
if (newest !== null) {
|
|
661
|
+
considered = [];
|
|
662
|
+
for (i = 0; i < inForce.length; i++) {
|
|
663
|
+
var dK = dateOf(inForce[i]);
|
|
664
|
+
if (dK === null || dK === newest) guard.list.append(considered, inForce[i]);
|
|
665
|
+
}
|
|
545
666
|
}
|
|
546
667
|
}
|
|
547
668
|
var rejectUnknown = !!(metadata && metadata.rejectUnknownStatus);
|
|
548
|
-
|
|
549
|
-
var s =
|
|
550
|
-
if (s === null)
|
|
669
|
+
for (i = 0; i < considered.length; i++) {
|
|
670
|
+
var s = statusOf(considered[i]);
|
|
671
|
+
if (s === null) continue;
|
|
551
672
|
if (DISQUALIFYING[s]) return true;
|
|
552
|
-
|
|
553
|
-
}
|
|
673
|
+
if (rejectUnknown && !_KNOWN_STATUS[s]) return true;
|
|
674
|
+
}
|
|
675
|
+
return false;
|
|
554
676
|
}
|
|
555
677
|
|
|
556
|
-
|
|
678
|
+
/** @internal Frozen. Under `rejectUnknownStatus` a status NOT in this table is disqualifying, so a
|
|
679
|
+
* caller able to add an entry makes an unrecognized status read as a known-good one. */
|
|
680
|
+
var _KNOWN_STATUS = intrinsic.freeze(intrinsic.assign(intrinsic.create(null), {
|
|
557
681
|
NOT_FIDO_CERTIFIED: 1, SELF_ASSERTION_SUBMITTED: 1, FIDO_CERTIFIED: 1, FIDO_CERTIFIED_L1: 1,
|
|
558
682
|
FIDO_CERTIFIED_L1plus: 1, FIDO_CERTIFIED_L2: 1, FIDO_CERTIFIED_L2plus: 1, FIDO_CERTIFIED_L3: 1,
|
|
559
683
|
FIDO_CERTIFIED_L3plus: 1, UPDATE_AVAILABLE: 1,
|
|
560
|
-
});
|
|
684
|
+
}));
|
|
561
685
|
|
|
686
|
+
/** @internal The rendering of an AAGUID is captured end to end. The string it returns is the metadata
|
|
687
|
+
* lookup key, so an encoding or a slice answering with another authenticator's AAGUID selects that
|
|
688
|
+
* authenticator's entry, with its status reports and its attestation roots. */
|
|
562
689
|
function aaguidToString(buf) {
|
|
563
|
-
if (!
|
|
564
|
-
var h =
|
|
565
|
-
return h
|
|
690
|
+
if (!intrinsic.isBuffer(buf) || buf.length !== 16) return null;
|
|
691
|
+
var h = intrinsic.bufToString(buf, "hex");
|
|
692
|
+
return _strSlice(h, 0, 8) + "-" + _strSlice(h, 8, 12) + "-" + _strSlice(h, 12, 16) + "-" +
|
|
693
|
+
_strSlice(h, 16, 20) + "-" + _strSlice(h, 20);
|
|
566
694
|
}
|
|
567
695
|
|
|
696
|
+
/** @internal The digest, its update and its encoding all come from captures. What this returns is both
|
|
697
|
+
* a metadata lookup key and the identity two certificates are told apart by where a status report
|
|
698
|
+
* names one, so an operation answering with another certificate's identifier selects another entry
|
|
699
|
+
* and makes a certificate-scoped compromise report apply to the wrong leaf. */
|
|
568
700
|
function certKeyIdentifier(cert) {
|
|
569
701
|
var pk = cert && cert.subjectPublicKeyInfo && cert.subjectPublicKeyInfo.publicKey;
|
|
570
|
-
if (!pk || !
|
|
702
|
+
if (!pk || !intrinsic.isBuffer(pk.bytes)) {
|
|
571
703
|
throw _err("webauthn/bad-input", "certKeyIdentifier expects a parsed certificate carrying a subject public key");
|
|
572
704
|
}
|
|
573
|
-
return
|
|
705
|
+
return _hashDigest(_hashUpdate(_createHash("sha1"), pk.bytes), "hex");
|
|
574
706
|
}
|
|
575
707
|
|
|
576
708
|
function metadataForKeyIdentifier(metadata, keyId) {
|
|
577
709
|
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");
|
|
578
710
|
if (typeof keyId !== "string") return null;
|
|
579
|
-
return metadata.byKeyIdentifier[keyId
|
|
711
|
+
return metadata.byKeyIdentifier[_strToLowerCase(keyId)] || null;
|
|
580
712
|
}
|
|
581
713
|
|
|
582
|
-
|
|
714
|
+
/** @internal Frozen, because a consumer reads these off this object at the call. `metadataFor` is what
|
|
715
|
+
* finds the entry a status refusal is read out of, and `chainToAnchor` IS the chain validation, so a
|
|
716
|
+
* writable export lets a replacement answer `null` for a disqualified entry or settle without walking
|
|
717
|
+
* a path, and the consumer's own captures decide nothing after that. */
|
|
718
|
+
module.exports = intrinsic.freeze({
|
|
583
719
|
verifyMetadataBlob: verifyMetadataBlob,
|
|
584
720
|
metadataFor: metadataFor,
|
|
585
721
|
metadataForKeyIdentifier: metadataForKeyIdentifier,
|
|
@@ -594,4 +730,4 @@ module.exports = {
|
|
|
594
730
|
DISQUALIFYING: DISQUALIFYING,
|
|
595
731
|
// @internal
|
|
596
732
|
BLOB_ALGS: BLOB_ALGS,
|
|
597
|
-
};
|
|
733
|
+
});
|