@simplewebauthn/server 13.2.3 → 13.3.0
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/esm/authentication/verifyAuthenticationResponse.d.ts +1 -1
- package/esm/authentication/verifyAuthenticationResponse.js +1 -1
- package/esm/helpers/index.d.ts +1 -0
- package/esm/helpers/index.d.ts.map +1 -1
- package/esm/helpers/index.js +1 -0
- package/esm/metadata/verifyMDSBlob.d.ts +18 -0
- package/esm/metadata/verifyMDSBlob.d.ts.map +1 -0
- package/esm/metadata/verifyMDSBlob.js +59 -0
- package/esm/services/metadataService.d.ts +6 -1
- package/esm/services/metadataService.d.ts.map +1 -1
- package/esm/services/metadataService.js +60 -51
- package/package.json +1 -1
- package/script/authentication/verifyAuthenticationResponse.d.ts +1 -1
- package/script/authentication/verifyAuthenticationResponse.js +1 -1
- package/script/helpers/index.d.ts +1 -0
- package/script/helpers/index.d.ts.map +1 -1
- package/script/helpers/index.js +1 -0
- package/script/metadata/verifyMDSBlob.d.ts +18 -0
- package/script/metadata/verifyMDSBlob.d.ts.map +1 -0
- package/script/metadata/verifyMDSBlob.js +62 -0
- package/script/services/metadataService.d.ts +6 -1
- package/script/services/metadataService.d.ts.map +1 -1
- package/script/services/metadataService.js +60 -51
|
@@ -9,7 +9,7 @@ export type VerifyAuthenticationResponseOpts = Parameters<typeof verifyAuthentic
|
|
|
9
9
|
*
|
|
10
10
|
* **Options:**
|
|
11
11
|
*
|
|
12
|
-
* @param response - Response returned by **@simplewebauthn/browser**'s `
|
|
12
|
+
* @param response - Response returned by **@simplewebauthn/browser**'s `startAuthentication()`
|
|
13
13
|
* @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateAuthenticationOptions()`
|
|
14
14
|
* @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on
|
|
15
15
|
* @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options
|
|
@@ -10,7 +10,7 @@ import { isoBase64URL, isoUint8Array } from '../helpers/iso/index.js';
|
|
|
10
10
|
*
|
|
11
11
|
* **Options:**
|
|
12
12
|
*
|
|
13
|
-
* @param response - Response returned by **@simplewebauthn/browser**'s `
|
|
13
|
+
* @param response - Response returned by **@simplewebauthn/browser**'s `startAuthentication()`
|
|
14
14
|
* @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateAuthenticationOptions()`
|
|
15
15
|
* @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on
|
|
16
16
|
* @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options
|
package/esm/helpers/index.d.ts
CHANGED
|
@@ -13,5 +13,6 @@ export * from './toHash.js';
|
|
|
13
13
|
export * from './validateCertificatePath.js';
|
|
14
14
|
export * from './verifySignature.js';
|
|
15
15
|
export * from './iso/index.js';
|
|
16
|
+
export * from '../metadata/verifyMDSBlob.js';
|
|
16
17
|
export * as cose from './cose.js';
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,aAAa,CAAC;AAC5B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,aAAa,CAAC;AAC5B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC;AAC7C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC"}
|
package/esm/helpers/index.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MDSJWTPayload, MetadataStatement } from './mdsTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Perform authenticity and integrity verification of a
|
|
4
|
+
* [FIDO Metadata Service (MDS)](https://fidoalliance.org/metadata/)-compatible blob, and then
|
|
5
|
+
* extract the FIDO2 metadata statements included within. This method will make network requests
|
|
6
|
+
* for things like CRL checks.
|
|
7
|
+
*
|
|
8
|
+
* @param blob - A JWT downloaded from an MDS server (e.g. https://mds3.fidoalliance.org)
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyMDSBlob(blob: string): Promise<{
|
|
11
|
+
/** MetadataStatement entries within the verified blob */
|
|
12
|
+
statements: MetadataStatement[];
|
|
13
|
+
/** A JS `Date` instance of the verified blob's `payload.nextUpdate` string */
|
|
14
|
+
parsedNextUpdate: Date;
|
|
15
|
+
/** The verified blob's `payload` value */
|
|
16
|
+
payload: MDSJWTPayload;
|
|
17
|
+
}>;
|
|
18
|
+
//# sourceMappingURL=verifyMDSBlob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyMDSBlob.d.ts","sourceRoot":"","sources":["../../src/metadata/verifyMDSBlob.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,aAAa,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAQpF;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACzD,yDAAyD;IACzD,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,8EAA8E;IAC9E,gBAAgB,EAAE,IAAI,CAAC;IACvB,0CAA0C;IAC1C,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC,CAuDD"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { parseJWT } from './parseJWT.js';
|
|
2
|
+
import { verifyJWT } from './verifyJWT.js';
|
|
3
|
+
import { validateCertificatePath } from '../helpers/validateCertificatePath.js';
|
|
4
|
+
import { convertCertBufferToPEM } from '../helpers/convertCertBufferToPEM.js';
|
|
5
|
+
import { convertPEMToBytes } from '../helpers/convertPEMToBytes.js';
|
|
6
|
+
import { SettingsService } from '../services/settingsService.js';
|
|
7
|
+
/**
|
|
8
|
+
* Perform authenticity and integrity verification of a
|
|
9
|
+
* [FIDO Metadata Service (MDS)](https://fidoalliance.org/metadata/)-compatible blob, and then
|
|
10
|
+
* extract the FIDO2 metadata statements included within. This method will make network requests
|
|
11
|
+
* for things like CRL checks.
|
|
12
|
+
*
|
|
13
|
+
* @param blob - A JWT downloaded from an MDS server (e.g. https://mds3.fidoalliance.org)
|
|
14
|
+
*/
|
|
15
|
+
export async function verifyMDSBlob(blob) {
|
|
16
|
+
// Parse the JWT
|
|
17
|
+
const parsedJWT = parseJWT(blob);
|
|
18
|
+
const header = parsedJWT[0];
|
|
19
|
+
const payload = parsedJWT[1];
|
|
20
|
+
const headerCertsPEM = header.x5c.map(convertCertBufferToPEM);
|
|
21
|
+
try {
|
|
22
|
+
// Validate the certificate chain
|
|
23
|
+
const rootCerts = SettingsService.getRootCertificates({
|
|
24
|
+
identifier: 'mds',
|
|
25
|
+
});
|
|
26
|
+
await validateCertificatePath(headerCertsPEM, rootCerts);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const _error = error;
|
|
30
|
+
// From FIDO MDS docs: "ignore the file if the chain cannot be verified or if one of the
|
|
31
|
+
// chain certificates is revoked"
|
|
32
|
+
throw new Error('BLOB certificate path could not be validated', { cause: _error });
|
|
33
|
+
}
|
|
34
|
+
// Verify the BLOB JWT signature
|
|
35
|
+
const leafCert = headerCertsPEM[0];
|
|
36
|
+
const verified = await verifyJWT(blob, convertPEMToBytes(leafCert));
|
|
37
|
+
if (!verified) {
|
|
38
|
+
// From FIDO MDS docs: "The FIDO Server SHOULD ignore the file if the signature is invalid."
|
|
39
|
+
throw new Error('BLOB signature could not be verified');
|
|
40
|
+
}
|
|
41
|
+
// Cache statements for FIDO2 devices
|
|
42
|
+
const statements = [];
|
|
43
|
+
for (const entry of payload.entries) {
|
|
44
|
+
// Only cache entries with an `aaguid`
|
|
45
|
+
if (entry.aaguid && entry.metadataStatement) {
|
|
46
|
+
statements.push(entry.metadataStatement);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Convert the nextUpdate property into a Date so we can determine when to re-download
|
|
50
|
+
const [year, month, day] = payload.nextUpdate.split('-');
|
|
51
|
+
const parsedNextUpdate = new Date(parseInt(year, 10),
|
|
52
|
+
// Months need to be zero-indexed
|
|
53
|
+
parseInt(month, 10) - 1, parseInt(day, 10));
|
|
54
|
+
return {
|
|
55
|
+
statements,
|
|
56
|
+
parsedNextUpdate,
|
|
57
|
+
payload,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -13,7 +13,8 @@ interface MetadataService {
|
|
|
13
13
|
*
|
|
14
14
|
* @param opts.mdsServers An array of URLs to FIDO Alliance Metadata Service
|
|
15
15
|
* (version 3.0)-compatible servers. Defaults to the official FIDO MDS server
|
|
16
|
-
* @param opts.statements An array of local metadata statements
|
|
16
|
+
* @param opts.statements An array of local metadata statements. Statements will be loaded but
|
|
17
|
+
* not refreshed
|
|
17
18
|
* @param opts.verificationMode How MetadataService will handle unregistered AAGUIDs. Defaults to
|
|
18
19
|
* `"strict"` which throws errors during registration response verification when an
|
|
19
20
|
* unregistered AAGUID is encountered. Set to `"permissive"` to allow registration by
|
|
@@ -53,6 +54,10 @@ export declare class BaseMetadataService implements MetadataService {
|
|
|
53
54
|
* Download and process the latest BLOB from MDS
|
|
54
55
|
*/
|
|
55
56
|
private downloadBlob;
|
|
57
|
+
/**
|
|
58
|
+
* Verify and process the MDS metadata blob
|
|
59
|
+
*/
|
|
60
|
+
private verifyBlob;
|
|
56
61
|
/**
|
|
57
62
|
* A helper method to pause execution until the service is ready
|
|
58
63
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadataService.d.ts","sourceRoot":"","sources":["../../src/services/metadataService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metadataService.d.ts","sourceRoot":"","sources":["../../src/services/metadataService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA4B,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAI3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmCrD;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAIvD,UAAU,eAAe;IACvB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,IAAI,CAAC,EAAE;QAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;QACjC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;KACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;CACnF;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,eAAe;IACzD,OAAO,CAAC,QAAQ,CAAoC;IACpD,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,KAAK,CAAyC;IACtD,OAAO,CAAC,gBAAgB,CAA8B;IAEhD,UAAU,CACd,IAAI,GAAE;QACJ,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;QACjC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;KAChC,GACL,OAAO,CAAC,IAAI,CAAC;IA4EV,YAAY,CAChB,MAAM,EAAE,MAAM,GAAG,WAAW,GAC3B,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IA8DzC;;OAEG;YACW,YAAY;IAU1B;;OAEG;YACW,UAAU;IA8CxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAgCvB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAWjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,eAA2C,CAAC"}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { validateCertificatePath } from '../helpers/validateCertificatePath.js';
|
|
2
|
-
import { convertCertBufferToPEM } from '../helpers/convertCertBufferToPEM.js';
|
|
3
1
|
import { convertAAGUIDToString } from '../helpers/convertAAGUIDToString.js';
|
|
4
|
-
import {
|
|
2
|
+
import { verifyMDSBlob } from '../metadata/verifyMDSBlob.js';
|
|
5
3
|
import { getLogger } from '../helpers/logging.js';
|
|
6
|
-
import { convertPEMToBytes } from '../helpers/convertPEMToBytes.js';
|
|
7
4
|
import { fetch } from '../helpers/fetch.js';
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
/**
|
|
6
|
+
* An instance of `CachedMDS` that will not trigger attempts to refresh the associated entry's blob
|
|
7
|
+
*/
|
|
8
|
+
const NonRefreshingMDS = {
|
|
9
|
+
url: '',
|
|
10
|
+
no: 0,
|
|
11
|
+
nextUpdate: new Date(0),
|
|
12
|
+
};
|
|
10
13
|
const defaultURLMDS = 'https://mds.fidoalliance.org/'; // v3
|
|
11
14
|
var SERVICE_STATE;
|
|
12
15
|
(function (SERVICE_STATE) {
|
|
@@ -49,9 +52,14 @@ export class BaseMetadataService {
|
|
|
49
52
|
});
|
|
50
53
|
}
|
|
51
54
|
async initialize(opts = {}) {
|
|
55
|
+
// Reset statement cache
|
|
56
|
+
this.statementCache = {};
|
|
52
57
|
const { mdsServers = [defaultURLMDS], statements, verificationMode } = opts;
|
|
53
58
|
this.setState(SERVICE_STATE.REFRESHING);
|
|
54
|
-
|
|
59
|
+
/**
|
|
60
|
+
* If metadata statements are provided, load them into the cache first. These statements will
|
|
61
|
+
* not be refreshed when a stale one is detected.
|
|
62
|
+
*/
|
|
55
63
|
if (statements?.length) {
|
|
56
64
|
let statementsAdded = 0;
|
|
57
65
|
statements.forEach((statement) => {
|
|
@@ -63,25 +71,31 @@ export class BaseMetadataService {
|
|
|
63
71
|
statusReports: [],
|
|
64
72
|
timeOfLastStatusChange: '1970-01-01',
|
|
65
73
|
},
|
|
66
|
-
url:
|
|
74
|
+
url: NonRefreshingMDS.url,
|
|
67
75
|
};
|
|
68
76
|
statementsAdded += 1;
|
|
69
77
|
}
|
|
70
78
|
});
|
|
71
79
|
log(`Cached ${statementsAdded} local statements`);
|
|
72
80
|
}
|
|
73
|
-
|
|
81
|
+
/**
|
|
82
|
+
* If MDS servers are provided, then download blobs from them, verify them, and then add their
|
|
83
|
+
* entries to the cache. Blobs loaded in this way will be refreshed when a stale entry within is
|
|
84
|
+
* detected.
|
|
85
|
+
*/
|
|
74
86
|
if (mdsServers?.length) {
|
|
75
87
|
// Get a current count so we know how many new statements we've added from MDS servers
|
|
76
88
|
const currentCacheCount = Object.keys(this.statementCache).length;
|
|
77
89
|
let numServers = mdsServers.length;
|
|
78
90
|
for (const url of mdsServers) {
|
|
79
91
|
try {
|
|
80
|
-
|
|
92
|
+
const cachedMDS = {
|
|
81
93
|
url,
|
|
82
94
|
no: 0,
|
|
83
95
|
nextUpdate: new Date(0),
|
|
84
|
-
}
|
|
96
|
+
};
|
|
97
|
+
const blob = await this.downloadBlob(cachedMDS);
|
|
98
|
+
await this.verifyBlob(blob, cachedMDS);
|
|
85
99
|
}
|
|
86
100
|
catch (err) {
|
|
87
101
|
// Notify of the error and move on
|
|
@@ -128,7 +142,8 @@ export class BaseMetadataService {
|
|
|
128
142
|
if (now > mds.nextUpdate) {
|
|
129
143
|
try {
|
|
130
144
|
this.setState(SERVICE_STATE.REFRESHING);
|
|
131
|
-
await this.downloadBlob(mds);
|
|
145
|
+
const blob = await this.downloadBlob(mds);
|
|
146
|
+
await this.verifyBlob(blob, mds);
|
|
132
147
|
}
|
|
133
148
|
finally {
|
|
134
149
|
this.setState(SERVICE_STATE.READY);
|
|
@@ -151,40 +166,23 @@ export class BaseMetadataService {
|
|
|
151
166
|
/**
|
|
152
167
|
* Download and process the latest BLOB from MDS
|
|
153
168
|
*/
|
|
154
|
-
async downloadBlob(
|
|
155
|
-
const { url
|
|
169
|
+
async downloadBlob(cachedMDS) {
|
|
170
|
+
const { url } = cachedMDS;
|
|
156
171
|
// Get latest "BLOB" (FIDO's terminology, not mine)
|
|
157
172
|
const resp = await fetch(url);
|
|
158
173
|
const data = await resp.text();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Verify and process the MDS metadata blob
|
|
178
|
+
*/
|
|
179
|
+
async verifyBlob(blob, cachedMDS) {
|
|
180
|
+
const { url, no } = cachedMDS;
|
|
181
|
+
const { payload, parsedNextUpdate } = await verifyMDSBlob(blob);
|
|
163
182
|
if (payload.no <= no) {
|
|
164
183
|
// From FIDO MDS docs: "also ignore the file if its number (no) is less or equal to the
|
|
165
184
|
// number of the last BLOB cached locally."
|
|
166
|
-
throw new Error(`Latest BLOB no.
|
|
167
|
-
}
|
|
168
|
-
const headerCertsPEM = header.x5c.map(convertCertBufferToPEM);
|
|
169
|
-
try {
|
|
170
|
-
// Validate the certificate chain
|
|
171
|
-
const rootCerts = SettingsService.getRootCertificates({
|
|
172
|
-
identifier: 'mds',
|
|
173
|
-
});
|
|
174
|
-
await validateCertificatePath(headerCertsPEM, rootCerts);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
const _error = error;
|
|
178
|
-
// From FIDO MDS docs: "ignore the file if the chain cannot be verified or if one of the
|
|
179
|
-
// chain certificates is revoked"
|
|
180
|
-
throw new Error('BLOB certificate path could not be validated', { cause: _error });
|
|
181
|
-
}
|
|
182
|
-
// Verify the BLOB JWT signature
|
|
183
|
-
const leafCert = headerCertsPEM[0];
|
|
184
|
-
const verified = await verifyJWT(data, convertPEMToBytes(leafCert));
|
|
185
|
-
if (!verified) {
|
|
186
|
-
// From FIDO MDS docs: "The FIDO Server SHOULD ignore the file if the signature is invalid."
|
|
187
|
-
throw new Error('BLOB signature could not be verified');
|
|
185
|
+
throw new Error(`Latest BLOB no. ${payload.no} is not greater than previous no. ${no}`);
|
|
188
186
|
}
|
|
189
187
|
// Cache statements for FIDO2 devices
|
|
190
188
|
for (const entry of payload.entries) {
|
|
@@ -193,17 +191,28 @@ export class BaseMetadataService {
|
|
|
193
191
|
this.statementCache[entry.aaguid] = { entry, url };
|
|
194
192
|
}
|
|
195
193
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
194
|
+
if (url) {
|
|
195
|
+
// Remember info about the server so we can refresh later
|
|
196
|
+
this.mdsCache[url] = {
|
|
197
|
+
...cachedMDS,
|
|
198
|
+
// Store the payload `no` to make sure we're getting the next BLOB in the sequence
|
|
199
|
+
no: payload.no,
|
|
200
|
+
// Remember when we need to refresh this blob
|
|
201
|
+
nextUpdate: parsedNextUpdate,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
/**
|
|
206
|
+
* This blob will not be refreshed, but we should still alert if the blob's `nextUpdate` is
|
|
207
|
+
* in the past
|
|
208
|
+
*/
|
|
209
|
+
if (parsedNextUpdate < new Date()) {
|
|
210
|
+
// TODO (Feb 2026): It'd be more actionable for devs if a specific error was raised here,
|
|
211
|
+
// then this message was logged higher up when it can include the array index of the stale
|
|
212
|
+
// blob.
|
|
213
|
+
log(`⚠️ This MDS blob (serial: ${payload.no}) contains stale data as of ${parsedNextUpdate.toISOString()}. Please consider re-initializing MetadataService with a newer MDS blob.`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
207
216
|
}
|
|
208
217
|
/**
|
|
209
218
|
* A helper method to pause execution until the service is ready
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ export type VerifyAuthenticationResponseOpts = Parameters<typeof verifyAuthentic
|
|
|
9
9
|
*
|
|
10
10
|
* **Options:**
|
|
11
11
|
*
|
|
12
|
-
* @param response - Response returned by **@simplewebauthn/browser**'s `
|
|
12
|
+
* @param response - Response returned by **@simplewebauthn/browser**'s `startAuthentication()`
|
|
13
13
|
* @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateAuthenticationOptions()`
|
|
14
14
|
* @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on
|
|
15
15
|
* @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options
|
|
@@ -13,7 +13,7 @@ const index_js_1 = require("../helpers/iso/index.js");
|
|
|
13
13
|
*
|
|
14
14
|
* **Options:**
|
|
15
15
|
*
|
|
16
|
-
* @param response - Response returned by **@simplewebauthn/browser**'s `
|
|
16
|
+
* @param response - Response returned by **@simplewebauthn/browser**'s `startAuthentication()`
|
|
17
17
|
* @param expectedChallenge - The base64url-encoded `options.challenge` returned by `generateAuthenticationOptions()`
|
|
18
18
|
* @param expectedOrigin - Website URL (or array of URLs) that the registration should have occurred on
|
|
19
19
|
* @param expectedRPID - RP ID (or array of IDs) that was specified in the registration options
|
|
@@ -13,5 +13,6 @@ export * from './toHash.js';
|
|
|
13
13
|
export * from './validateCertificatePath.js';
|
|
14
14
|
export * from './verifySignature.js';
|
|
15
15
|
export * from './iso/index.js';
|
|
16
|
+
export * from '../metadata/verifyMDSBlob.js';
|
|
16
17
|
export * as cose from './cose.js';
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,aAAa,CAAC;AAC5B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.ts"],"names":[],"mappings":"AAAA,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,aAAa,CAAC;AAC5B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,8BAA8B,CAAC;AAC7C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC"}
|
package/script/helpers/index.js
CHANGED
|
@@ -42,4 +42,5 @@ __exportStar(require("./toHash.js"), exports);
|
|
|
42
42
|
__exportStar(require("./validateCertificatePath.js"), exports);
|
|
43
43
|
__exportStar(require("./verifySignature.js"), exports);
|
|
44
44
|
__exportStar(require("./iso/index.js"), exports);
|
|
45
|
+
__exportStar(require("../metadata/verifyMDSBlob.js"), exports);
|
|
45
46
|
exports.cose = __importStar(require("./cose.js"));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MDSJWTPayload, MetadataStatement } from './mdsTypes.js';
|
|
2
|
+
/**
|
|
3
|
+
* Perform authenticity and integrity verification of a
|
|
4
|
+
* [FIDO Metadata Service (MDS)](https://fidoalliance.org/metadata/)-compatible blob, and then
|
|
5
|
+
* extract the FIDO2 metadata statements included within. This method will make network requests
|
|
6
|
+
* for things like CRL checks.
|
|
7
|
+
*
|
|
8
|
+
* @param blob - A JWT downloaded from an MDS server (e.g. https://mds3.fidoalliance.org)
|
|
9
|
+
*/
|
|
10
|
+
export declare function verifyMDSBlob(blob: string): Promise<{
|
|
11
|
+
/** MetadataStatement entries within the verified blob */
|
|
12
|
+
statements: MetadataStatement[];
|
|
13
|
+
/** A JS `Date` instance of the verified blob's `payload.nextUpdate` string */
|
|
14
|
+
parsedNextUpdate: Date;
|
|
15
|
+
/** The verified blob's `payload` value */
|
|
16
|
+
payload: MDSJWTPayload;
|
|
17
|
+
}>;
|
|
18
|
+
//# sourceMappingURL=verifyMDSBlob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verifyMDSBlob.d.ts","sourceRoot":"","sources":["../../src/metadata/verifyMDSBlob.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,aAAa,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAQpF;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACzD,yDAAyD;IACzD,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,8EAA8E;IAC9E,gBAAgB,EAAE,IAAI,CAAC;IACvB,0CAA0C;IAC1C,OAAO,EAAE,aAAa,CAAC;CACxB,CAAC,CAuDD"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyMDSBlob = verifyMDSBlob;
|
|
4
|
+
const parseJWT_js_1 = require("./parseJWT.js");
|
|
5
|
+
const verifyJWT_js_1 = require("./verifyJWT.js");
|
|
6
|
+
const validateCertificatePath_js_1 = require("../helpers/validateCertificatePath.js");
|
|
7
|
+
const convertCertBufferToPEM_js_1 = require("../helpers/convertCertBufferToPEM.js");
|
|
8
|
+
const convertPEMToBytes_js_1 = require("../helpers/convertPEMToBytes.js");
|
|
9
|
+
const settingsService_js_1 = require("../services/settingsService.js");
|
|
10
|
+
/**
|
|
11
|
+
* Perform authenticity and integrity verification of a
|
|
12
|
+
* [FIDO Metadata Service (MDS)](https://fidoalliance.org/metadata/)-compatible blob, and then
|
|
13
|
+
* extract the FIDO2 metadata statements included within. This method will make network requests
|
|
14
|
+
* for things like CRL checks.
|
|
15
|
+
*
|
|
16
|
+
* @param blob - A JWT downloaded from an MDS server (e.g. https://mds3.fidoalliance.org)
|
|
17
|
+
*/
|
|
18
|
+
async function verifyMDSBlob(blob) {
|
|
19
|
+
// Parse the JWT
|
|
20
|
+
const parsedJWT = (0, parseJWT_js_1.parseJWT)(blob);
|
|
21
|
+
const header = parsedJWT[0];
|
|
22
|
+
const payload = parsedJWT[1];
|
|
23
|
+
const headerCertsPEM = header.x5c.map(convertCertBufferToPEM_js_1.convertCertBufferToPEM);
|
|
24
|
+
try {
|
|
25
|
+
// Validate the certificate chain
|
|
26
|
+
const rootCerts = settingsService_js_1.SettingsService.getRootCertificates({
|
|
27
|
+
identifier: 'mds',
|
|
28
|
+
});
|
|
29
|
+
await (0, validateCertificatePath_js_1.validateCertificatePath)(headerCertsPEM, rootCerts);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const _error = error;
|
|
33
|
+
// From FIDO MDS docs: "ignore the file if the chain cannot be verified or if one of the
|
|
34
|
+
// chain certificates is revoked"
|
|
35
|
+
throw new Error('BLOB certificate path could not be validated', { cause: _error });
|
|
36
|
+
}
|
|
37
|
+
// Verify the BLOB JWT signature
|
|
38
|
+
const leafCert = headerCertsPEM[0];
|
|
39
|
+
const verified = await (0, verifyJWT_js_1.verifyJWT)(blob, (0, convertPEMToBytes_js_1.convertPEMToBytes)(leafCert));
|
|
40
|
+
if (!verified) {
|
|
41
|
+
// From FIDO MDS docs: "The FIDO Server SHOULD ignore the file if the signature is invalid."
|
|
42
|
+
throw new Error('BLOB signature could not be verified');
|
|
43
|
+
}
|
|
44
|
+
// Cache statements for FIDO2 devices
|
|
45
|
+
const statements = [];
|
|
46
|
+
for (const entry of payload.entries) {
|
|
47
|
+
// Only cache entries with an `aaguid`
|
|
48
|
+
if (entry.aaguid && entry.metadataStatement) {
|
|
49
|
+
statements.push(entry.metadataStatement);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Convert the nextUpdate property into a Date so we can determine when to re-download
|
|
53
|
+
const [year, month, day] = payload.nextUpdate.split('-');
|
|
54
|
+
const parsedNextUpdate = new Date(parseInt(year, 10),
|
|
55
|
+
// Months need to be zero-indexed
|
|
56
|
+
parseInt(month, 10) - 1, parseInt(day, 10));
|
|
57
|
+
return {
|
|
58
|
+
statements,
|
|
59
|
+
parsedNextUpdate,
|
|
60
|
+
payload,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -13,7 +13,8 @@ interface MetadataService {
|
|
|
13
13
|
*
|
|
14
14
|
* @param opts.mdsServers An array of URLs to FIDO Alliance Metadata Service
|
|
15
15
|
* (version 3.0)-compatible servers. Defaults to the official FIDO MDS server
|
|
16
|
-
* @param opts.statements An array of local metadata statements
|
|
16
|
+
* @param opts.statements An array of local metadata statements. Statements will be loaded but
|
|
17
|
+
* not refreshed
|
|
17
18
|
* @param opts.verificationMode How MetadataService will handle unregistered AAGUIDs. Defaults to
|
|
18
19
|
* `"strict"` which throws errors during registration response verification when an
|
|
19
20
|
* unregistered AAGUID is encountered. Set to `"permissive"` to allow registration by
|
|
@@ -53,6 +54,10 @@ export declare class BaseMetadataService implements MetadataService {
|
|
|
53
54
|
* Download and process the latest BLOB from MDS
|
|
54
55
|
*/
|
|
55
56
|
private downloadBlob;
|
|
57
|
+
/**
|
|
58
|
+
* Verify and process the MDS metadata blob
|
|
59
|
+
*/
|
|
60
|
+
private verifyBlob;
|
|
56
61
|
/**
|
|
57
62
|
* A helper method to pause execution until the service is ready
|
|
58
63
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadataService.d.ts","sourceRoot":"","sources":["../../src/services/metadataService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"metadataService.d.ts","sourceRoot":"","sources":["../../src/services/metadataService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA4B,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAI3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAmCrD;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAIvD,UAAU,eAAe;IACvB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,IAAI,CAAC,EAAE;QAChB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;QACjC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;KACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;CACnF;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,YAAW,eAAe;IACzD,OAAO,CAAC,QAAQ,CAAoC;IACpD,OAAO,CAAC,cAAc,CAA6C;IACnE,OAAO,CAAC,KAAK,CAAyC;IACtD,OAAO,CAAC,gBAAgB,CAA8B;IAEhD,UAAU,CACd,IAAI,GAAE;QACJ,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;QACtB,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAC;QACjC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;KAChC,GACL,OAAO,CAAC,IAAI,CAAC;IA4EV,YAAY,CAChB,MAAM,EAAE,MAAM,GAAG,WAAW,GAC3B,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IA8DzC;;OAEG;YACW,YAAY;IAU1B;;OAEG;YACW,UAAU;IA8CxB;;OAEG;IACH,OAAO,CAAC,eAAe;IAgCvB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAWjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,eAA2C,CAAC"}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MetadataService = exports.BaseMetadataService = void 0;
|
|
4
|
-
const validateCertificatePath_js_1 = require("../helpers/validateCertificatePath.js");
|
|
5
|
-
const convertCertBufferToPEM_js_1 = require("../helpers/convertCertBufferToPEM.js");
|
|
6
4
|
const convertAAGUIDToString_js_1 = require("../helpers/convertAAGUIDToString.js");
|
|
7
|
-
const
|
|
5
|
+
const verifyMDSBlob_js_1 = require("../metadata/verifyMDSBlob.js");
|
|
8
6
|
const logging_js_1 = require("../helpers/logging.js");
|
|
9
|
-
const convertPEMToBytes_js_1 = require("../helpers/convertPEMToBytes.js");
|
|
10
7
|
const fetch_js_1 = require("../helpers/fetch.js");
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* An instance of `CachedMDS` that will not trigger attempts to refresh the associated entry's blob
|
|
10
|
+
*/
|
|
11
|
+
const NonRefreshingMDS = {
|
|
12
|
+
url: '',
|
|
13
|
+
no: 0,
|
|
14
|
+
nextUpdate: new Date(0),
|
|
15
|
+
};
|
|
13
16
|
const defaultURLMDS = 'https://mds.fidoalliance.org/'; // v3
|
|
14
17
|
var SERVICE_STATE;
|
|
15
18
|
(function (SERVICE_STATE) {
|
|
@@ -52,9 +55,14 @@ class BaseMetadataService {
|
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
async initialize(opts = {}) {
|
|
58
|
+
// Reset statement cache
|
|
59
|
+
this.statementCache = {};
|
|
55
60
|
const { mdsServers = [defaultURLMDS], statements, verificationMode } = opts;
|
|
56
61
|
this.setState(SERVICE_STATE.REFRESHING);
|
|
57
|
-
|
|
62
|
+
/**
|
|
63
|
+
* If metadata statements are provided, load them into the cache first. These statements will
|
|
64
|
+
* not be refreshed when a stale one is detected.
|
|
65
|
+
*/
|
|
58
66
|
if (statements?.length) {
|
|
59
67
|
let statementsAdded = 0;
|
|
60
68
|
statements.forEach((statement) => {
|
|
@@ -66,25 +74,31 @@ class BaseMetadataService {
|
|
|
66
74
|
statusReports: [],
|
|
67
75
|
timeOfLastStatusChange: '1970-01-01',
|
|
68
76
|
},
|
|
69
|
-
url:
|
|
77
|
+
url: NonRefreshingMDS.url,
|
|
70
78
|
};
|
|
71
79
|
statementsAdded += 1;
|
|
72
80
|
}
|
|
73
81
|
});
|
|
74
82
|
log(`Cached ${statementsAdded} local statements`);
|
|
75
83
|
}
|
|
76
|
-
|
|
84
|
+
/**
|
|
85
|
+
* If MDS servers are provided, then download blobs from them, verify them, and then add their
|
|
86
|
+
* entries to the cache. Blobs loaded in this way will be refreshed when a stale entry within is
|
|
87
|
+
* detected.
|
|
88
|
+
*/
|
|
77
89
|
if (mdsServers?.length) {
|
|
78
90
|
// Get a current count so we know how many new statements we've added from MDS servers
|
|
79
91
|
const currentCacheCount = Object.keys(this.statementCache).length;
|
|
80
92
|
let numServers = mdsServers.length;
|
|
81
93
|
for (const url of mdsServers) {
|
|
82
94
|
try {
|
|
83
|
-
|
|
95
|
+
const cachedMDS = {
|
|
84
96
|
url,
|
|
85
97
|
no: 0,
|
|
86
98
|
nextUpdate: new Date(0),
|
|
87
|
-
}
|
|
99
|
+
};
|
|
100
|
+
const blob = await this.downloadBlob(cachedMDS);
|
|
101
|
+
await this.verifyBlob(blob, cachedMDS);
|
|
88
102
|
}
|
|
89
103
|
catch (err) {
|
|
90
104
|
// Notify of the error and move on
|
|
@@ -131,7 +145,8 @@ class BaseMetadataService {
|
|
|
131
145
|
if (now > mds.nextUpdate) {
|
|
132
146
|
try {
|
|
133
147
|
this.setState(SERVICE_STATE.REFRESHING);
|
|
134
|
-
await this.downloadBlob(mds);
|
|
148
|
+
const blob = await this.downloadBlob(mds);
|
|
149
|
+
await this.verifyBlob(blob, mds);
|
|
135
150
|
}
|
|
136
151
|
finally {
|
|
137
152
|
this.setState(SERVICE_STATE.READY);
|
|
@@ -154,40 +169,23 @@ class BaseMetadataService {
|
|
|
154
169
|
/**
|
|
155
170
|
* Download and process the latest BLOB from MDS
|
|
156
171
|
*/
|
|
157
|
-
async downloadBlob(
|
|
158
|
-
const { url
|
|
172
|
+
async downloadBlob(cachedMDS) {
|
|
173
|
+
const { url } = cachedMDS;
|
|
159
174
|
// Get latest "BLOB" (FIDO's terminology, not mine)
|
|
160
175
|
const resp = await (0, fetch_js_1.fetch)(url);
|
|
161
176
|
const data = await resp.text();
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Verify and process the MDS metadata blob
|
|
181
|
+
*/
|
|
182
|
+
async verifyBlob(blob, cachedMDS) {
|
|
183
|
+
const { url, no } = cachedMDS;
|
|
184
|
+
const { payload, parsedNextUpdate } = await (0, verifyMDSBlob_js_1.verifyMDSBlob)(blob);
|
|
166
185
|
if (payload.no <= no) {
|
|
167
186
|
// From FIDO MDS docs: "also ignore the file if its number (no) is less or equal to the
|
|
168
187
|
// number of the last BLOB cached locally."
|
|
169
|
-
throw new Error(`Latest BLOB no.
|
|
170
|
-
}
|
|
171
|
-
const headerCertsPEM = header.x5c.map(convertCertBufferToPEM_js_1.convertCertBufferToPEM);
|
|
172
|
-
try {
|
|
173
|
-
// Validate the certificate chain
|
|
174
|
-
const rootCerts = settingsService_js_1.SettingsService.getRootCertificates({
|
|
175
|
-
identifier: 'mds',
|
|
176
|
-
});
|
|
177
|
-
await (0, validateCertificatePath_js_1.validateCertificatePath)(headerCertsPEM, rootCerts);
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
const _error = error;
|
|
181
|
-
// From FIDO MDS docs: "ignore the file if the chain cannot be verified or if one of the
|
|
182
|
-
// chain certificates is revoked"
|
|
183
|
-
throw new Error('BLOB certificate path could not be validated', { cause: _error });
|
|
184
|
-
}
|
|
185
|
-
// Verify the BLOB JWT signature
|
|
186
|
-
const leafCert = headerCertsPEM[0];
|
|
187
|
-
const verified = await (0, verifyJWT_js_1.verifyJWT)(data, (0, convertPEMToBytes_js_1.convertPEMToBytes)(leafCert));
|
|
188
|
-
if (!verified) {
|
|
189
|
-
// From FIDO MDS docs: "The FIDO Server SHOULD ignore the file if the signature is invalid."
|
|
190
|
-
throw new Error('BLOB signature could not be verified');
|
|
188
|
+
throw new Error(`Latest BLOB no. ${payload.no} is not greater than previous no. ${no}`);
|
|
191
189
|
}
|
|
192
190
|
// Cache statements for FIDO2 devices
|
|
193
191
|
for (const entry of payload.entries) {
|
|
@@ -196,17 +194,28 @@ class BaseMetadataService {
|
|
|
196
194
|
this.statementCache[entry.aaguid] = { entry, url };
|
|
197
195
|
}
|
|
198
196
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
197
|
+
if (url) {
|
|
198
|
+
// Remember info about the server so we can refresh later
|
|
199
|
+
this.mdsCache[url] = {
|
|
200
|
+
...cachedMDS,
|
|
201
|
+
// Store the payload `no` to make sure we're getting the next BLOB in the sequence
|
|
202
|
+
no: payload.no,
|
|
203
|
+
// Remember when we need to refresh this blob
|
|
204
|
+
nextUpdate: parsedNextUpdate,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
/**
|
|
209
|
+
* This blob will not be refreshed, but we should still alert if the blob's `nextUpdate` is
|
|
210
|
+
* in the past
|
|
211
|
+
*/
|
|
212
|
+
if (parsedNextUpdate < new Date()) {
|
|
213
|
+
// TODO (Feb 2026): It'd be more actionable for devs if a specific error was raised here,
|
|
214
|
+
// then this message was logged higher up when it can include the array index of the stale
|
|
215
|
+
// blob.
|
|
216
|
+
log(`⚠️ This MDS blob (serial: ${payload.no}) contains stale data as of ${parsedNextUpdate.toISOString()}. Please consider re-initializing MetadataService with a newer MDS blob.`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
210
219
|
}
|
|
211
220
|
/**
|
|
212
221
|
* A helper method to pause execution until the service is ready
|