@simplewebauthn/server 13.2.2 → 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.
@@ -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 `startAssertion()`
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 `startAssertion()`
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
@@ -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"}
@@ -13,4 +13,5 @@ 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';
@@ -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":"AAGA,OAAO,KAAK,EAIV,iBAAiB,EAClB,MAAM,yBAAyB,CAAC;AAKjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyBrD;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAIvD,UAAU,eAAe;IACvB;;;;;;;;;;;;OAYG;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;IA+DV,YAAY,CAChB,MAAM,EAAE,MAAM,GAAG,WAAW,GAC3B,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IA6DzC;;OAEG;YACW,YAAY;IAqE1B;;OAEG;IACH,OAAO,CAAC,eAAe;IAgCvB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAWjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,eAA2C,CAAC"}
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 { SettingsService } from './settingsService.js';
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
- import { parseJWT } from '../metadata/parseJWT.js';
9
- import { verifyJWT } from '../metadata/verifyJWT.js';
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
- // If metadata statements are provided, load them into the cache first
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
- // If MDS servers are provided, then process them and add their statements to the cache
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
- await this.downloadBlob({
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(mds) {
155
- const { url, no } = mds;
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
- // Parse the JWT
160
- const parsedJWT = parseJWT(data);
161
- const header = parsedJWT[0];
162
- const payload = parsedJWT[1];
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. "${payload.no}" is not greater than previous ${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
- // Remember info about the server so we can refresh later
197
- const [year, month, day] = payload.nextUpdate.split('-');
198
- this.mdsCache[url] = {
199
- ...mds,
200
- // Store the payload `no` to make sure we're getting the next BLOB in the sequence
201
- no: payload.no,
202
- // Convert the nextUpdate property into a Date so we can determine when to re-download
203
- nextUpdate: new Date(parseInt(year, 10),
204
- // Months need to be zero-indexed
205
- parseInt(month, 10) - 1, parseInt(day, 10)),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplewebauthn/server",
3
- "version": "13.2.2",
3
+ "version": "13.3.0",
4
4
  "description": "SimpleWebAuthn for Servers",
5
5
  "keywords": [
6
6
  "typescript",
@@ -51,12 +51,12 @@
51
51
  "dependencies": {
52
52
  "@hexagon/base64": "^1.1.27",
53
53
  "@levischuck/tiny-cbor": "^0.2.2",
54
- "@peculiar/asn1-android": "^2.3.10",
55
- "@peculiar/asn1-ecc": "^2.3.8",
56
- "@peculiar/asn1-rsa": "^2.3.8",
57
- "@peculiar/asn1-schema": "^2.3.8",
58
- "@peculiar/asn1-x509": "^2.3.8",
59
- "@peculiar/x509": "^1.13.0"
54
+ "@peculiar/asn1-android": "^2.6.0",
55
+ "@peculiar/asn1-ecc": "^2.6.1",
56
+ "@peculiar/asn1-rsa": "^2.6.1",
57
+ "@peculiar/asn1-schema": "^2.6.0",
58
+ "@peculiar/asn1-x509": "^2.6.1",
59
+ "@peculiar/x509": "^1.14.3"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^20.9.0"
@@ -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 `startAssertion()`
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 `startAssertion()`
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"}
@@ -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":"AAGA,OAAO,KAAK,EAIV,iBAAiB,EAClB,MAAM,yBAAyB,CAAC;AAKjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyBrD;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,QAAQ,CAAC;AAIvD,UAAU,eAAe;IACvB;;;;;;;;;;;;OAYG;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;IA+DV,YAAY,CAChB,MAAM,EAAE,MAAM,GAAG,WAAW,GAC3B,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IA6DzC;;OAEG;YACW,YAAY;IAqE1B;;OAEG;IACH,OAAO,CAAC,eAAe;IAgCvB;;OAEG;IACH,OAAO,CAAC,QAAQ;CAWjB;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe,EAAE,eAA2C,CAAC"}
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 settingsService_js_1 = require("./settingsService.js");
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
- const parseJWT_js_1 = require("../metadata/parseJWT.js");
12
- const verifyJWT_js_1 = require("../metadata/verifyJWT.js");
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
- // If metadata statements are provided, load them into the cache first
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
- // If MDS servers are provided, then process them and add their statements to the cache
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
- await this.downloadBlob({
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(mds) {
158
- const { url, no } = mds;
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
- // Parse the JWT
163
- const parsedJWT = (0, parseJWT_js_1.parseJWT)(data);
164
- const header = parsedJWT[0];
165
- const payload = parsedJWT[1];
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. "${payload.no}" is not greater than previous ${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
- // Remember info about the server so we can refresh later
200
- const [year, month, day] = payload.nextUpdate.split('-');
201
- this.mdsCache[url] = {
202
- ...mds,
203
- // Store the payload `no` to make sure we're getting the next BLOB in the sequence
204
- no: payload.no,
205
- // Convert the nextUpdate property into a Date so we can determine when to re-download
206
- nextUpdate: new Date(parseInt(year, 10),
207
- // Months need to be zero-indexed
208
- parseInt(month, 10) - 1, parseInt(day, 10)),
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