@simplewebauthn/server 14.0.1 → 14.0.2

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.
@@ -13,5 +13,5 @@ export declare class SimpleWebAuthnError extends Error {
13
13
  export declare class PQCNotSupportedError extends SimpleWebAuthnError {
14
14
  constructor(alg: COSEALG);
15
15
  }
16
- export type SimpleWebAuthnErrorCode = 'RUNTIME_NO_PQC_SUPPORT';
16
+ export type SimpleWebAuthnErrorCode = 'RUNTIME_NO_PQC_SUPPORT' | 'CERTIFICATE_PATH_VERIFICATION_FAILED';
17
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,IAAI,EAAE,uBAAuB,CAAC;gBAElB,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;QACpC,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,uBAAuB,CAAC;QAC9B,KAAK,CAAC,EAAE,KAAK,CAAC;KACf;CAKF;AAED,qBAAa,oBAAqB,SAAQ,mBAAmB;gBAC/C,GAAG,EAAE,OAAO;CAKzB;AAED,MAAM,MAAM,uBAAuB,GAAG,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,IAAI,EAAE,uBAAuB,CAAC;gBAElB,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;QACpC,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,uBAAuB,CAAC;QAC9B,KAAK,CAAC,EAAE,KAAK,CAAC;KACf;CAKF;AAED,qBAAa,oBAAqB,SAAQ,mBAAmB;gBAC/C,GAAG,EAAE,OAAO;CAKzB;AAED,MAAM,MAAM,uBAAuB,GAC/B,wBAAwB,GACxB,sCAAsC,CAAC"}
@@ -6,5 +6,5 @@ import { type X509Certificate } from '@peculiar/x509';
6
6
  *
7
7
  * CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
8
8
  */
9
- export declare function isCertRevoked(cert: X509Certificate): Promise<boolean>;
9
+ export declare function isCertRevoked(certificate: X509Certificate, issuer?: X509Certificate): Promise<boolean>;
10
10
  //# sourceMappingURL=isCertRevoked.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"isCertRevoked.d.ts","sourceRoot":"","sources":["../../src/helpers/isCertRevoked.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,gBAAgB,CAAC;AAexB;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAkG3E"}
1
+ {"version":3,"file":"isCertRevoked.d.ts","sourceRoot":"","sources":["../../src/helpers/isCertRevoked.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,gBAAgB,CAAC;AAexB;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,eAAe,EAC5B,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,OAAO,CAAC,CA0HlB"}
@@ -8,90 +8,110 @@ const cacheRevokedCerts = {};
8
8
  *
9
9
  * CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
10
10
  */
11
- export async function isCertRevoked(cert) {
12
- const { extensions } = cert;
11
+ export async function isCertRevoked(certificate, issuer) {
12
+ if (!issuer) {
13
+ // We don't have a public key to verify any CRL signature so don't bother continuing
14
+ return false;
15
+ }
16
+ const { extensions } = certificate;
13
17
  if (!extensions) {
14
18
  return false;
15
19
  }
16
- let extAuthorityKeyID;
17
- let extSubjectKeyID;
18
- let extCRLDistributionPoints;
19
- extensions.forEach((ext) => {
20
- if (ext instanceof AuthorityKeyIdentifierExtension) {
21
- extAuthorityKeyID = ext;
22
- }
23
- else if (ext instanceof SubjectKeyIdentifierExtension) {
24
- extSubjectKeyID = ext;
25
- }
26
- else if (ext instanceof CRLDistributionPointsExtension) {
27
- extCRLDistributionPoints = ext;
28
- }
20
+ const extCRLDistributionPoints = extensions.find((ext) => ext instanceof CRLDistributionPointsExtension);
21
+ // Get all CRL URLs from within the certificate
22
+ const crlURLs = [];
23
+ extCRLDistributionPoints?.distributionPoints?.forEach((dPoint) => {
24
+ dPoint.distributionPoint?.fullName?.forEach((fullName) => {
25
+ if (fullName.uniformResourceIdentifier) {
26
+ crlURLs.push(fullName.uniformResourceIdentifier);
27
+ }
28
+ });
29
29
  });
30
- // Check to see if we've got cached info for the cert's CA
31
- let keyIdentifier = undefined;
32
- if (extAuthorityKeyID && extAuthorityKeyID.keyId) {
33
- keyIdentifier = extAuthorityKeyID.keyId;
30
+ // If no URL(s) is provided then we have nothing to check
31
+ if (!(crlURLs.length > 0)) {
32
+ return false;
34
33
  }
35
- else if (extSubjectKeyID) {
36
- /**
37
- * We might be dealing with a self-signed root certificate. Check the
38
- * Subject key Identifier extension next.
39
- */
40
- keyIdentifier = extSubjectKeyID.keyId;
34
+ // Get the issuer's key identifier for part of the cache key and to check against the
35
+ // certificate's and CRL's AuthorityKeyIdentifier extension
36
+ const { keyIdentifier: issuerKeyIdentifier, fromExtension: issuerKeyIdentifierFromExtension, } = await getIssuerKeyIdentifier(issuer);
37
+ const certificateExtAuthorityKeyID = certificate.extensions.find((ext) => ext instanceof AuthorityKeyIdentifierExtension);
38
+ // When present, assert that certificates's AuthorityKeyIdentifier mirrors
39
+ // the issuer's SubjectKeyIdentifier
40
+ if (issuerKeyIdentifierFromExtension &&
41
+ certificateExtAuthorityKeyID?.keyId &&
42
+ certificateExtAuthorityKeyID.keyId !== issuerKeyIdentifier) {
43
+ throw new Error(`Certificate's AuthorityKeyIdentifier did not match issuer's SubjectKeyIdentifier "${issuerKeyIdentifier}"`);
41
44
  }
42
- if (keyIdentifier) {
43
- const cached = cacheRevokedCerts[keyIdentifier];
45
+ for (const crlURL of crlURLs) {
46
+ // Key off of issuer public key and CRL URL in case multiple CRLs are specified
47
+ const cacheKey = `${issuerKeyIdentifier}|${crlURL}`;
48
+ const cached = cacheRevokedCerts[cacheKey];
44
49
  if (cached) {
45
50
  const now = new Date();
46
51
  // If there's a nextUpdate then make sure we're before it
47
52
  if (!cached.nextUpdate || cached.nextUpdate > now) {
48
- return cached.revokedCerts.indexOf(cert.serialNumber) >= 0;
53
+ return cached.revokedCerts.indexOf(certificate.serialNumber) >= 0;
49
54
  }
50
55
  }
51
- }
52
- const crlURL = extCRLDistributionPoints?.distributionPoints?.[0].distributionPoint?.fullName?.[0]
53
- .uniformResourceIdentifier;
54
- // If no URL is provided then we have nothing to check
55
- if (!crlURL) {
56
- return false;
57
- }
58
- // Download and read the CRL
59
- let certListBytes;
60
- try {
61
- const respCRL = await fetch(crlURL);
62
- certListBytes = await respCRL.arrayBuffer();
63
- }
64
- catch (_err) {
65
- return false;
66
- }
67
- let data;
68
- try {
69
- data = new X509Crl(certListBytes);
70
- }
71
- catch (_err) {
72
- // Something was malformed with the CRL, so pass
73
- return false;
74
- }
75
- const newCached = {
76
- revokedCerts: [],
77
- nextUpdate: undefined,
78
- };
79
- // nextUpdate
80
- if (data.nextUpdate) {
81
- newCached.nextUpdate = data.nextUpdate;
82
- }
83
- // revokedCertificates
84
- const revokedCerts = data.entries;
85
- if (revokedCerts) {
86
- for (const cert of revokedCerts) {
87
- const revokedHex = cert.serialNumber;
88
- newCached.revokedCerts.push(revokedHex);
56
+ // Download and read the CRL
57
+ let crlBytes;
58
+ try {
59
+ const respCRL = await fetch(crlURL);
60
+ crlBytes = await respCRL.arrayBuffer();
61
+ }
62
+ catch (_err) {
63
+ // Some kind of network error occurred so try the next URL
64
+ continue;
65
+ }
66
+ let certCRL;
67
+ try {
68
+ certCRL = new X509Crl(crlBytes);
69
+ }
70
+ catch (_err) {
71
+ // Something was malformed with the CRL so try the next URL
72
+ continue;
73
+ }
74
+ const certCRLVerified = await certCRL.verify({ publicKey: issuer.publicKey });
75
+ if (!certCRLVerified) {
76
+ throw new Error(`CRL from ${crlURL} failed signature verification against issuer "${issuer.subject}"`);
89
77
  }
90
- // Cache the results
91
- if (keyIdentifier) {
92
- cacheRevokedCerts[keyIdentifier] = newCached;
78
+ // When present, assert that the CRL's AuthorityKeyIdentifier mirrors
79
+ // the issuer's SubjectKeyIdentifier
80
+ const crlAKI = certCRL.getExtension(AuthorityKeyIdentifierExtension);
81
+ if (issuerKeyIdentifierFromExtension &&
82
+ crlAKI?.keyId &&
83
+ crlAKI.keyId !== issuerKeyIdentifier) {
84
+ throw new Error(`CRL from ${crlURL} contained an AuthorityKeyIdentifier that did not match issuer's SubjectKeyIdentifier "${issuerKeyIdentifier}"`);
85
+ }
86
+ // Cache the CRL only if a nextUpdate has been provided
87
+ if (certCRL.nextUpdate) {
88
+ cacheRevokedCerts[cacheKey] = {
89
+ revokedCerts: certCRL.entries.map((entry) => entry.serialNumber),
90
+ nextUpdate: certCRL.nextUpdate,
91
+ };
92
+ }
93
+ // Finally, check entries for revocation status
94
+ const revoked = certCRL.findRevoked(certificate);
95
+ if (revoked) {
96
+ return true;
93
97
  }
94
- return newCached.revokedCerts.indexOf(cert.serialNumber) >= 0;
95
98
  }
96
99
  return false;
97
100
  }
101
+ /**
102
+ * Determine a unique key ID for the issuer certificate, based either on its
103
+ * SubjectKeyIdentifierExtension or by generating the same value using the issuer's public key
104
+ */
105
+ async function getIssuerKeyIdentifier(issuer) {
106
+ const issuerSKI = issuer.extensions?.find((ext) => ext instanceof SubjectKeyIdentifierExtension);
107
+ if (issuerSKI?.keyId) {
108
+ return { keyIdentifier: issuerSKI.keyId, fromExtension: true };
109
+ }
110
+ /**
111
+ * The issuer carries no SubjectKeyIdentifier extension, so derive one from its public key
112
+ * (RFC 5280, method 1). This is good enough to keep unrelated CAs from colliding when computing
113
+ * the revocation cache key, but it's not authoritative enough to reject a certificate over.
114
+ */
115
+ const computed = await SubjectKeyIdentifierExtension.create(issuer.publicKey);
116
+ return { keyIdentifier: computed.keyId, fromExtension: false };
117
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validateCertificatePath.d.ts","sourceRoot":"","sources":["../../src/helpers/validateCertificatePath.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAK1B;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,MAAM,EAAE,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,OAAO,CAAC,CAqIlB"}
1
+ {"version":3,"file":"validateCertificatePath.d.ts","sourceRoot":"","sources":["../../src/helpers/validateCertificatePath.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAM1B;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,MAAM,EAAE,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,OAAO,CAAC,CAiLlB"}
@@ -1,6 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { X509Certificate, X509ChainBuilder } from '@peculiar/x509';
3
3
  import { isCertRevoked } from './isCertRevoked.js';
4
+ import { SimpleWebAuthnError } from '../errors/index.js';
4
5
  /**
5
6
  * Traverse an array of PEM certificates and ensure they form a proper chain
6
7
  * @param x5cCertsPEM Typically the result of `x5c.map(convertASN1toPEM)`
@@ -11,48 +12,25 @@ export async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = [])
11
12
  // We have no trust anchors to chain back to, so skip path validation
12
13
  return true;
13
14
  }
14
- // Prepare to work with x5c certs
15
- const x5cCertsParsed = x5cCertsPEM.map((certPEM) => new X509Certificate(certPEM));
16
- // Check for any expired or temporally invalid certs in x5c
17
- for (let i = 0; i < x5cCertsParsed.length; i++) {
18
- const cert = x5cCertsParsed[i];
19
- const certPEM = x5cCertsPEM[i];
20
- try {
21
- await assertCertNotRevoked(cert);
22
- }
23
- catch (_err) {
24
- throw new Error(`Found revoked certificate in x5c:\n${certPEM}`);
25
- }
26
- try {
27
- assertCertIsWithinValidTimeWindow(cert.notBefore, cert.notAfter);
28
- }
29
- catch (_err) {
30
- throw new Error(`Found certificate out of validity period in x5c:\n${certPEM}`);
31
- }
32
- }
33
15
  // Prepare to work with trust anchor certs
34
16
  const trustAnchorsParsed = trustAnchorsPEM.map((certPEM) => {
35
17
  try {
36
18
  return new X509Certificate(certPEM);
37
19
  }
38
20
  catch (err) {
39
- const _err = err;
40
- throw new Error(`Could not parse trust anchor certificate:\n${certPEM}`, { cause: _err });
21
+ throw new SimpleWebAuthnError({
22
+ message: `Could not parse trust anchor certificate:\n${certPEM}`,
23
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
24
+ cause: err,
25
+ });
41
26
  }
42
27
  });
43
- // Filter out any expired or temporally invalid trust anchors certs
28
+ // Filter out any temporally invalid trust anchors certs
44
29
  const validTrustAnchors = [];
45
30
  for (let i = 0; i < trustAnchorsParsed.length; i++) {
46
31
  const cert = trustAnchorsParsed[i];
47
32
  try {
48
- await assertCertNotRevoked(cert);
49
- }
50
- catch (_err) {
51
- // Continue processing the other certs
52
- continue;
53
- }
54
- try {
55
- assertCertIsWithinValidTimeWindow(cert.notBefore, cert.notAfter);
33
+ assertCertIsWithinValidTimeWindow(cert);
56
34
  }
57
35
  catch (_err) {
58
36
  // Continue processing the other certs
@@ -61,22 +39,31 @@ export async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = [])
61
39
  validTrustAnchors.push(cert);
62
40
  }
63
41
  if (validTrustAnchors.length === 0) {
64
- throw new Error('No specified trust anchor was valid for verifying x5c');
42
+ throw new SimpleWebAuthnError({
43
+ message: 'No specified trust anchor was valid for verifying x5c',
44
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
45
+ });
46
+ }
47
+ // Prepare to work with x5c certs
48
+ const x5cCertsParsed = x5cCertsPEM.map((certPEM) => new X509Certificate(certPEM));
49
+ // Break apart x5c into leaf + intermediates
50
+ const x5cLeafCert = x5cCertsParsed[0];
51
+ let x5cIntermediates = [];
52
+ if (x5cCertsParsed.length > 1) {
53
+ x5cIntermediates = x5cCertsParsed.slice(1);
65
54
  }
66
55
  // Try to verify x5c with each valid trust anchor
67
56
  let invalidCertificateChain = true;
57
+ let validatedChain = undefined;
68
58
  for (const anchor of validTrustAnchors) {
69
59
  try {
70
60
  const x5cWithTrustAnchor = x5cCertsParsed.concat([anchor]);
71
61
  const numUniqueCerts = new Set(x5cWithTrustAnchor.map((cert) => cert.toString('pem'))).size;
72
62
  if (numUniqueCerts !== x5cWithTrustAnchor.length) {
73
- throw new Error('Invalid certificate path: found duplicate certificates');
74
- }
75
- // Break apart x5c to try and build a valid cert chain
76
- const x5cLeafCert = x5cCertsParsed[0];
77
- let x5cIntermediates = [];
78
- if (x5cCertsParsed.length > 1) {
79
- x5cIntermediates = x5cCertsParsed.slice(1);
63
+ throw new SimpleWebAuthnError({
64
+ message: 'Invalid certificate path: found duplicate certificates',
65
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
66
+ });
80
67
  }
81
68
  // Order of certs doesn't matter here but for readability
82
69
  const chainBuilder = new X509ChainBuilder({ certificates: [...x5cIntermediates, anchor] });
@@ -110,27 +97,87 @@ export async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = [])
110
97
  // Invalid cert chain, try the next trust anchor
111
98
  continue;
112
99
  }
113
- // If we successfully validated a path then there's no need to continue. Reset any existing
114
- // errors that were thrown by earlier trust anchors
100
+ if (certBeforeLastSignedByAnchor) {
101
+ /**
102
+ * Swap out the last cert in the chain with the anchor that we've already verified
103
+ * chains to the second-to-last certificate. This makes it easier to verify the chain
104
+ * as a standard certificate chain.
105
+ */
106
+ chain[chain.length - 1] = anchor;
107
+ }
108
+ // We successfully validated a chain so there's no need to continue
115
109
  invalidCertificateChain = false;
110
+ validatedChain = chain;
116
111
  break;
117
112
  }
118
113
  catch (err) {
119
- throw new Error('Unexpected error while validating certificate path', { cause: err });
114
+ throw new SimpleWebAuthnError({
115
+ message: 'Unexpected error while validating certificate path',
116
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
117
+ cause: err,
118
+ });
120
119
  }
121
120
  }
121
+ if (validatedChain) {
122
+ // Check for any temporally invalid or expired certs in the chain
123
+ for (let i = 0; i < validatedChain.length; i++) {
124
+ const cert = validatedChain[i];
125
+ try {
126
+ assertCertIsWithinValidTimeWindow(cert);
127
+ }
128
+ catch (_err) {
129
+ throw new SimpleWebAuthnError({
130
+ message: `Found certificate out of validity period:\n${cert.toString()}`,
131
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
132
+ });
133
+ }
134
+ /**
135
+ * Checking revocation is very expensive so do it only at the end when we're certain a cert
136
+ * is otherwise valid
137
+ */
138
+ let issuerCert = undefined;
139
+ if (i < validatedChain.length - 1) {
140
+ // Issuer cert is simply the next cert in the chain
141
+ issuerCert = validatedChain[i + 1];
142
+ }
143
+ else {
144
+ // `cert` is the anchor. Only try to verify its revocation status if it's a root certificate
145
+ if (await cert.isSelfSigned()) {
146
+ issuerCert = cert;
147
+ }
148
+ }
149
+ try {
150
+ await assertCertNotRevoked(cert, issuerCert);
151
+ }
152
+ catch (err) {
153
+ throw new SimpleWebAuthnError({
154
+ message: `The following certificate failed revocation status check\n${cert.toString()}`,
155
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
156
+ cause: err,
157
+ });
158
+ }
159
+ }
160
+ }
161
+ else {
162
+ invalidCertificateChain = true;
163
+ }
122
164
  // We tried multiple trust anchors and none of them worked
123
165
  if (invalidCertificateChain) {
124
- throw new InvalidCertificatePath();
166
+ throw new SimpleWebAuthnError({
167
+ message: 'x5c could not be chained to any specified trust anchor',
168
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
169
+ });
125
170
  }
126
171
  return true;
127
172
  }
128
173
  /**
129
174
  * Check if the certificate is revoked or not. If it is, raise an error
175
+ *
176
+ * @throws Error - Wrap this in a SimpleWebAuthnError so RPs can identify issues here
130
177
  */
131
- async function assertCertNotRevoked(certificate) {
178
+ async function assertCertNotRevoked(certificate, issuerCert) {
132
179
  // Check for certificate revocation
133
- const subjectCertRevoked = await isCertRevoked(certificate);
180
+ const subjectCertRevoked = await isCertRevoked(certificate, issuerCert);
134
181
  if (subjectCertRevoked) {
135
182
  throw new Error('Found revoked certificate in certificate path');
136
183
  }
@@ -138,16 +185,10 @@ async function assertCertNotRevoked(certificate) {
138
185
  /**
139
186
  * Require the cert to be within its notBefore and notAfter time window
140
187
  */
141
- function assertCertIsWithinValidTimeWindow(certNotBefore, certNotAfter) {
188
+ function assertCertIsWithinValidTimeWindow(certificate) {
189
+ const { notBefore: certNotBefore, notAfter: certNotAfter } = certificate;
142
190
  const now = new Date(Date.now());
143
191
  if (certNotBefore > now || certNotAfter < now) {
144
192
  throw new Error('Certificate is not yet valid or expired');
145
193
  }
146
194
  }
147
- class InvalidCertificatePath extends Error {
148
- constructor() {
149
- const message = 'x5c could not be chained to any specified trust anchor';
150
- super(message);
151
- this.name = 'InvalidX5CChain';
152
- }
153
- }
@@ -1 +1 @@
1
- {"version":3,"file":"verifyAttestationWithMetadata.d.ts","sourceRoot":"","sources":["../../src/metadata/verifyAttestationWithMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAIhE,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,OAAO,EAEZ,OAAO,EAER,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;GAGG;AACH,wBAAsB,6BAA6B,CAAC,EAClD,SAAS,EACT,mBAAmB,EACnB,GAAG,EACH,uBAAuB,GACxB,EAAE;IACD,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mBAAmB,EAAE,WAAW,CAAC;IACjC,GAAG,EAAE,WAAW,EAAE,GAAG,eAAe,EAAE,CAAC;IACvC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,OAAO,CAAC,CAoJnB;AAED,KAAK,QAAQ,GAAG;IACd,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE;KAAG,GAAG,IAAI,OAAO,GAAG,QAAQ;CAe9D,CAAC"}
1
+ {"version":3,"file":"verifyAttestationWithMetadata.d.ts","sourceRoot":"","sources":["../../src/metadata/verifyAttestationWithMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAIhE,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,OAAO,EAEZ,OAAO,EAER,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;GAGG;AACH,wBAAsB,6BAA6B,CAAC,EAClD,SAAS,EACT,mBAAmB,EACnB,GAAG,EACH,uBAAuB,GACxB,EAAE;IACD,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mBAAmB,EAAE,WAAW,CAAC;IACjC,GAAG,EAAE,WAAW,EAAE,GAAG,eAAe,EAAE,CAAC;IACvC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,OAAO,CAAC,CA6InB;AAED,KAAK,QAAQ,GAAG;IACd,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE;KAAG,GAAG,IAAI,OAAO,GAAG,QAAQ;CAe9D,CAAC"}
@@ -106,13 +106,7 @@ export async function verifyAttestationWithMetadata({ statement, credentialPubli
106
106
  authenticatorIsSelfReferencing = true;
107
107
  }
108
108
  if (!authenticatorIsSelfReferencing) {
109
- try {
110
- await validateCertificatePath(authenticatorCerts, statementRootCerts);
111
- }
112
- catch (err) {
113
- const _err = err;
114
- throw new Error(`Could not validate certificate path with any metadata root certificates: ${_err.message}`);
115
- }
109
+ await validateCertificatePath(authenticatorCerts, statementRootCerts);
116
110
  }
117
111
  return true;
118
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplewebauthn/server",
3
- "version": "14.0.1",
3
+ "version": "14.0.2",
4
4
  "description": "SimpleWebAuthn for Servers",
5
5
  "keywords": [
6
6
  "typescript",
@@ -13,5 +13,5 @@ export declare class SimpleWebAuthnError extends Error {
13
13
  export declare class PQCNotSupportedError extends SimpleWebAuthnError {
14
14
  constructor(alg: COSEALG);
15
15
  }
16
- export type SimpleWebAuthnErrorCode = 'RUNTIME_NO_PQC_SUPPORT';
16
+ export type SimpleWebAuthnErrorCode = 'RUNTIME_NO_PQC_SUPPORT' | 'CERTIFICATE_PATH_VERIFICATION_FAILED';
17
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,IAAI,EAAE,uBAAuB,CAAC;gBAElB,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;QACpC,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,uBAAuB,CAAC;QAC9B,KAAK,CAAC,EAAE,KAAK,CAAC;KACf;CAKF;AAED,qBAAa,oBAAqB,SAAQ,mBAAmB;gBAC/C,GAAG,EAAE,OAAO;CAKzB;AAED,MAAM,MAAM,uBAAuB,GAAG,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD;;GAEG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,IAAI,EAAE,uBAAuB,CAAC;gBAElB,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE;QACpC,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,uBAAuB,CAAC;QAC9B,KAAK,CAAC,EAAE,KAAK,CAAC;KACf;CAKF;AAED,qBAAa,oBAAqB,SAAQ,mBAAmB;gBAC/C,GAAG,EAAE,OAAO;CAKzB;AAED,MAAM,MAAM,uBAAuB,GAC/B,wBAAwB,GACxB,sCAAsC,CAAC"}
@@ -6,5 +6,5 @@ import { type X509Certificate } from '@peculiar/x509';
6
6
  *
7
7
  * CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
8
8
  */
9
- export declare function isCertRevoked(cert: X509Certificate): Promise<boolean>;
9
+ export declare function isCertRevoked(certificate: X509Certificate, issuer?: X509Certificate): Promise<boolean>;
10
10
  //# sourceMappingURL=isCertRevoked.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"isCertRevoked.d.ts","sourceRoot":"","sources":["../../src/helpers/isCertRevoked.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,gBAAgB,CAAC;AAexB;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAkG3E"}
1
+ {"version":3,"file":"isCertRevoked.d.ts","sourceRoot":"","sources":["../../src/helpers/isCertRevoked.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,gBAAgB,CAAC;AAexB;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,WAAW,EAAE,eAAe,EAC5B,MAAM,CAAC,EAAE,eAAe,GACvB,OAAO,CAAC,OAAO,CAAC,CA0HlB"}
@@ -11,90 +11,110 @@ const cacheRevokedCerts = {};
11
11
  *
12
12
  * CRL certificate structure referenced from https://tools.ietf.org/html/rfc5280#page-117
13
13
  */
14
- async function isCertRevoked(cert) {
15
- const { extensions } = cert;
14
+ async function isCertRevoked(certificate, issuer) {
15
+ if (!issuer) {
16
+ // We don't have a public key to verify any CRL signature so don't bother continuing
17
+ return false;
18
+ }
19
+ const { extensions } = certificate;
16
20
  if (!extensions) {
17
21
  return false;
18
22
  }
19
- let extAuthorityKeyID;
20
- let extSubjectKeyID;
21
- let extCRLDistributionPoints;
22
- extensions.forEach((ext) => {
23
- if (ext instanceof x509_1.AuthorityKeyIdentifierExtension) {
24
- extAuthorityKeyID = ext;
25
- }
26
- else if (ext instanceof x509_1.SubjectKeyIdentifierExtension) {
27
- extSubjectKeyID = ext;
28
- }
29
- else if (ext instanceof x509_1.CRLDistributionPointsExtension) {
30
- extCRLDistributionPoints = ext;
31
- }
23
+ const extCRLDistributionPoints = extensions.find((ext) => ext instanceof x509_1.CRLDistributionPointsExtension);
24
+ // Get all CRL URLs from within the certificate
25
+ const crlURLs = [];
26
+ extCRLDistributionPoints?.distributionPoints?.forEach((dPoint) => {
27
+ dPoint.distributionPoint?.fullName?.forEach((fullName) => {
28
+ if (fullName.uniformResourceIdentifier) {
29
+ crlURLs.push(fullName.uniformResourceIdentifier);
30
+ }
31
+ });
32
32
  });
33
- // Check to see if we've got cached info for the cert's CA
34
- let keyIdentifier = undefined;
35
- if (extAuthorityKeyID && extAuthorityKeyID.keyId) {
36
- keyIdentifier = extAuthorityKeyID.keyId;
33
+ // If no URL(s) is provided then we have nothing to check
34
+ if (!(crlURLs.length > 0)) {
35
+ return false;
37
36
  }
38
- else if (extSubjectKeyID) {
39
- /**
40
- * We might be dealing with a self-signed root certificate. Check the
41
- * Subject key Identifier extension next.
42
- */
43
- keyIdentifier = extSubjectKeyID.keyId;
37
+ // Get the issuer's key identifier for part of the cache key and to check against the
38
+ // certificate's and CRL's AuthorityKeyIdentifier extension
39
+ const { keyIdentifier: issuerKeyIdentifier, fromExtension: issuerKeyIdentifierFromExtension, } = await getIssuerKeyIdentifier(issuer);
40
+ const certificateExtAuthorityKeyID = certificate.extensions.find((ext) => ext instanceof x509_1.AuthorityKeyIdentifierExtension);
41
+ // When present, assert that certificates's AuthorityKeyIdentifier mirrors
42
+ // the issuer's SubjectKeyIdentifier
43
+ if (issuerKeyIdentifierFromExtension &&
44
+ certificateExtAuthorityKeyID?.keyId &&
45
+ certificateExtAuthorityKeyID.keyId !== issuerKeyIdentifier) {
46
+ throw new Error(`Certificate's AuthorityKeyIdentifier did not match issuer's SubjectKeyIdentifier "${issuerKeyIdentifier}"`);
44
47
  }
45
- if (keyIdentifier) {
46
- const cached = cacheRevokedCerts[keyIdentifier];
48
+ for (const crlURL of crlURLs) {
49
+ // Key off of issuer public key and CRL URL in case multiple CRLs are specified
50
+ const cacheKey = `${issuerKeyIdentifier}|${crlURL}`;
51
+ const cached = cacheRevokedCerts[cacheKey];
47
52
  if (cached) {
48
53
  const now = new Date();
49
54
  // If there's a nextUpdate then make sure we're before it
50
55
  if (!cached.nextUpdate || cached.nextUpdate > now) {
51
- return cached.revokedCerts.indexOf(cert.serialNumber) >= 0;
56
+ return cached.revokedCerts.indexOf(certificate.serialNumber) >= 0;
52
57
  }
53
58
  }
54
- }
55
- const crlURL = extCRLDistributionPoints?.distributionPoints?.[0].distributionPoint?.fullName?.[0]
56
- .uniformResourceIdentifier;
57
- // If no URL is provided then we have nothing to check
58
- if (!crlURL) {
59
- return false;
60
- }
61
- // Download and read the CRL
62
- let certListBytes;
63
- try {
64
- const respCRL = await (0, fetch_js_1.fetch)(crlURL);
65
- certListBytes = await respCRL.arrayBuffer();
66
- }
67
- catch (_err) {
68
- return false;
69
- }
70
- let data;
71
- try {
72
- data = new x509_1.X509Crl(certListBytes);
73
- }
74
- catch (_err) {
75
- // Something was malformed with the CRL, so pass
76
- return false;
77
- }
78
- const newCached = {
79
- revokedCerts: [],
80
- nextUpdate: undefined,
81
- };
82
- // nextUpdate
83
- if (data.nextUpdate) {
84
- newCached.nextUpdate = data.nextUpdate;
85
- }
86
- // revokedCertificates
87
- const revokedCerts = data.entries;
88
- if (revokedCerts) {
89
- for (const cert of revokedCerts) {
90
- const revokedHex = cert.serialNumber;
91
- newCached.revokedCerts.push(revokedHex);
59
+ // Download and read the CRL
60
+ let crlBytes;
61
+ try {
62
+ const respCRL = await (0, fetch_js_1.fetch)(crlURL);
63
+ crlBytes = await respCRL.arrayBuffer();
64
+ }
65
+ catch (_err) {
66
+ // Some kind of network error occurred so try the next URL
67
+ continue;
68
+ }
69
+ let certCRL;
70
+ try {
71
+ certCRL = new x509_1.X509Crl(crlBytes);
72
+ }
73
+ catch (_err) {
74
+ // Something was malformed with the CRL so try the next URL
75
+ continue;
76
+ }
77
+ const certCRLVerified = await certCRL.verify({ publicKey: issuer.publicKey });
78
+ if (!certCRLVerified) {
79
+ throw new Error(`CRL from ${crlURL} failed signature verification against issuer "${issuer.subject}"`);
92
80
  }
93
- // Cache the results
94
- if (keyIdentifier) {
95
- cacheRevokedCerts[keyIdentifier] = newCached;
81
+ // When present, assert that the CRL's AuthorityKeyIdentifier mirrors
82
+ // the issuer's SubjectKeyIdentifier
83
+ const crlAKI = certCRL.getExtension(x509_1.AuthorityKeyIdentifierExtension);
84
+ if (issuerKeyIdentifierFromExtension &&
85
+ crlAKI?.keyId &&
86
+ crlAKI.keyId !== issuerKeyIdentifier) {
87
+ throw new Error(`CRL from ${crlURL} contained an AuthorityKeyIdentifier that did not match issuer's SubjectKeyIdentifier "${issuerKeyIdentifier}"`);
88
+ }
89
+ // Cache the CRL only if a nextUpdate has been provided
90
+ if (certCRL.nextUpdate) {
91
+ cacheRevokedCerts[cacheKey] = {
92
+ revokedCerts: certCRL.entries.map((entry) => entry.serialNumber),
93
+ nextUpdate: certCRL.nextUpdate,
94
+ };
95
+ }
96
+ // Finally, check entries for revocation status
97
+ const revoked = certCRL.findRevoked(certificate);
98
+ if (revoked) {
99
+ return true;
96
100
  }
97
- return newCached.revokedCerts.indexOf(cert.serialNumber) >= 0;
98
101
  }
99
102
  return false;
100
103
  }
104
+ /**
105
+ * Determine a unique key ID for the issuer certificate, based either on its
106
+ * SubjectKeyIdentifierExtension or by generating the same value using the issuer's public key
107
+ */
108
+ async function getIssuerKeyIdentifier(issuer) {
109
+ const issuerSKI = issuer.extensions?.find((ext) => ext instanceof x509_1.SubjectKeyIdentifierExtension);
110
+ if (issuerSKI?.keyId) {
111
+ return { keyIdentifier: issuerSKI.keyId, fromExtension: true };
112
+ }
113
+ /**
114
+ * The issuer carries no SubjectKeyIdentifier extension, so derive one from its public key
115
+ * (RFC 5280, method 1). This is good enough to keep unrelated CAs from colliding when computing
116
+ * the revocation cache key, but it's not authoritative enough to reject a certificate over.
117
+ */
118
+ const computed = await x509_1.SubjectKeyIdentifierExtension.create(issuer.publicKey);
119
+ return { keyIdentifier: computed.keyId, fromExtension: false };
120
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validateCertificatePath.d.ts","sourceRoot":"","sources":["../../src/helpers/validateCertificatePath.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAK1B;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,MAAM,EAAE,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,OAAO,CAAC,CAqIlB"}
1
+ {"version":3,"file":"validateCertificatePath.d.ts","sourceRoot":"","sources":["../../src/helpers/validateCertificatePath.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAM1B;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,WAAW,EAAE,MAAM,EAAE,EACrB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,OAAO,CAAC,CAiLlB"}
@@ -4,6 +4,7 @@ exports.validateCertificatePath = validateCertificatePath;
4
4
  require("reflect-metadata");
5
5
  const x509_1 = require("@peculiar/x509");
6
6
  const isCertRevoked_js_1 = require("./isCertRevoked.js");
7
+ const index_js_1 = require("../errors/index.js");
7
8
  /**
8
9
  * Traverse an array of PEM certificates and ensure they form a proper chain
9
10
  * @param x5cCertsPEM Typically the result of `x5c.map(convertASN1toPEM)`
@@ -14,48 +15,25 @@ async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = []) {
14
15
  // We have no trust anchors to chain back to, so skip path validation
15
16
  return true;
16
17
  }
17
- // Prepare to work with x5c certs
18
- const x5cCertsParsed = x5cCertsPEM.map((certPEM) => new x509_1.X509Certificate(certPEM));
19
- // Check for any expired or temporally invalid certs in x5c
20
- for (let i = 0; i < x5cCertsParsed.length; i++) {
21
- const cert = x5cCertsParsed[i];
22
- const certPEM = x5cCertsPEM[i];
23
- try {
24
- await assertCertNotRevoked(cert);
25
- }
26
- catch (_err) {
27
- throw new Error(`Found revoked certificate in x5c:\n${certPEM}`);
28
- }
29
- try {
30
- assertCertIsWithinValidTimeWindow(cert.notBefore, cert.notAfter);
31
- }
32
- catch (_err) {
33
- throw new Error(`Found certificate out of validity period in x5c:\n${certPEM}`);
34
- }
35
- }
36
18
  // Prepare to work with trust anchor certs
37
19
  const trustAnchorsParsed = trustAnchorsPEM.map((certPEM) => {
38
20
  try {
39
21
  return new x509_1.X509Certificate(certPEM);
40
22
  }
41
23
  catch (err) {
42
- const _err = err;
43
- throw new Error(`Could not parse trust anchor certificate:\n${certPEM}`, { cause: _err });
24
+ throw new index_js_1.SimpleWebAuthnError({
25
+ message: `Could not parse trust anchor certificate:\n${certPEM}`,
26
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
27
+ cause: err,
28
+ });
44
29
  }
45
30
  });
46
- // Filter out any expired or temporally invalid trust anchors certs
31
+ // Filter out any temporally invalid trust anchors certs
47
32
  const validTrustAnchors = [];
48
33
  for (let i = 0; i < trustAnchorsParsed.length; i++) {
49
34
  const cert = trustAnchorsParsed[i];
50
35
  try {
51
- await assertCertNotRevoked(cert);
52
- }
53
- catch (_err) {
54
- // Continue processing the other certs
55
- continue;
56
- }
57
- try {
58
- assertCertIsWithinValidTimeWindow(cert.notBefore, cert.notAfter);
36
+ assertCertIsWithinValidTimeWindow(cert);
59
37
  }
60
38
  catch (_err) {
61
39
  // Continue processing the other certs
@@ -64,22 +42,31 @@ async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = []) {
64
42
  validTrustAnchors.push(cert);
65
43
  }
66
44
  if (validTrustAnchors.length === 0) {
67
- throw new Error('No specified trust anchor was valid for verifying x5c');
45
+ throw new index_js_1.SimpleWebAuthnError({
46
+ message: 'No specified trust anchor was valid for verifying x5c',
47
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
48
+ });
49
+ }
50
+ // Prepare to work with x5c certs
51
+ const x5cCertsParsed = x5cCertsPEM.map((certPEM) => new x509_1.X509Certificate(certPEM));
52
+ // Break apart x5c into leaf + intermediates
53
+ const x5cLeafCert = x5cCertsParsed[0];
54
+ let x5cIntermediates = [];
55
+ if (x5cCertsParsed.length > 1) {
56
+ x5cIntermediates = x5cCertsParsed.slice(1);
68
57
  }
69
58
  // Try to verify x5c with each valid trust anchor
70
59
  let invalidCertificateChain = true;
60
+ let validatedChain = undefined;
71
61
  for (const anchor of validTrustAnchors) {
72
62
  try {
73
63
  const x5cWithTrustAnchor = x5cCertsParsed.concat([anchor]);
74
64
  const numUniqueCerts = new Set(x5cWithTrustAnchor.map((cert) => cert.toString('pem'))).size;
75
65
  if (numUniqueCerts !== x5cWithTrustAnchor.length) {
76
- throw new Error('Invalid certificate path: found duplicate certificates');
77
- }
78
- // Break apart x5c to try and build a valid cert chain
79
- const x5cLeafCert = x5cCertsParsed[0];
80
- let x5cIntermediates = [];
81
- if (x5cCertsParsed.length > 1) {
82
- x5cIntermediates = x5cCertsParsed.slice(1);
66
+ throw new index_js_1.SimpleWebAuthnError({
67
+ message: 'Invalid certificate path: found duplicate certificates',
68
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
69
+ });
83
70
  }
84
71
  // Order of certs doesn't matter here but for readability
85
72
  const chainBuilder = new x509_1.X509ChainBuilder({ certificates: [...x5cIntermediates, anchor] });
@@ -113,27 +100,87 @@ async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = []) {
113
100
  // Invalid cert chain, try the next trust anchor
114
101
  continue;
115
102
  }
116
- // If we successfully validated a path then there's no need to continue. Reset any existing
117
- // errors that were thrown by earlier trust anchors
103
+ if (certBeforeLastSignedByAnchor) {
104
+ /**
105
+ * Swap out the last cert in the chain with the anchor that we've already verified
106
+ * chains to the second-to-last certificate. This makes it easier to verify the chain
107
+ * as a standard certificate chain.
108
+ */
109
+ chain[chain.length - 1] = anchor;
110
+ }
111
+ // We successfully validated a chain so there's no need to continue
118
112
  invalidCertificateChain = false;
113
+ validatedChain = chain;
119
114
  break;
120
115
  }
121
116
  catch (err) {
122
- throw new Error('Unexpected error while validating certificate path', { cause: err });
117
+ throw new index_js_1.SimpleWebAuthnError({
118
+ message: 'Unexpected error while validating certificate path',
119
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
120
+ cause: err,
121
+ });
123
122
  }
124
123
  }
124
+ if (validatedChain) {
125
+ // Check for any temporally invalid or expired certs in the chain
126
+ for (let i = 0; i < validatedChain.length; i++) {
127
+ const cert = validatedChain[i];
128
+ try {
129
+ assertCertIsWithinValidTimeWindow(cert);
130
+ }
131
+ catch (_err) {
132
+ throw new index_js_1.SimpleWebAuthnError({
133
+ message: `Found certificate out of validity period:\n${cert.toString()}`,
134
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
135
+ });
136
+ }
137
+ /**
138
+ * Checking revocation is very expensive so do it only at the end when we're certain a cert
139
+ * is otherwise valid
140
+ */
141
+ let issuerCert = undefined;
142
+ if (i < validatedChain.length - 1) {
143
+ // Issuer cert is simply the next cert in the chain
144
+ issuerCert = validatedChain[i + 1];
145
+ }
146
+ else {
147
+ // `cert` is the anchor. Only try to verify its revocation status if it's a root certificate
148
+ if (await cert.isSelfSigned()) {
149
+ issuerCert = cert;
150
+ }
151
+ }
152
+ try {
153
+ await assertCertNotRevoked(cert, issuerCert);
154
+ }
155
+ catch (err) {
156
+ throw new index_js_1.SimpleWebAuthnError({
157
+ message: `The following certificate failed revocation status check\n${cert.toString()}`,
158
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
159
+ cause: err,
160
+ });
161
+ }
162
+ }
163
+ }
164
+ else {
165
+ invalidCertificateChain = true;
166
+ }
125
167
  // We tried multiple trust anchors and none of them worked
126
168
  if (invalidCertificateChain) {
127
- throw new InvalidCertificatePath();
169
+ throw new index_js_1.SimpleWebAuthnError({
170
+ message: 'x5c could not be chained to any specified trust anchor',
171
+ code: 'CERTIFICATE_PATH_VERIFICATION_FAILED',
172
+ });
128
173
  }
129
174
  return true;
130
175
  }
131
176
  /**
132
177
  * Check if the certificate is revoked or not. If it is, raise an error
178
+ *
179
+ * @throws Error - Wrap this in a SimpleWebAuthnError so RPs can identify issues here
133
180
  */
134
- async function assertCertNotRevoked(certificate) {
181
+ async function assertCertNotRevoked(certificate, issuerCert) {
135
182
  // Check for certificate revocation
136
- const subjectCertRevoked = await (0, isCertRevoked_js_1.isCertRevoked)(certificate);
183
+ const subjectCertRevoked = await (0, isCertRevoked_js_1.isCertRevoked)(certificate, issuerCert);
137
184
  if (subjectCertRevoked) {
138
185
  throw new Error('Found revoked certificate in certificate path');
139
186
  }
@@ -141,16 +188,10 @@ async function assertCertNotRevoked(certificate) {
141
188
  /**
142
189
  * Require the cert to be within its notBefore and notAfter time window
143
190
  */
144
- function assertCertIsWithinValidTimeWindow(certNotBefore, certNotAfter) {
191
+ function assertCertIsWithinValidTimeWindow(certificate) {
192
+ const { notBefore: certNotBefore, notAfter: certNotAfter } = certificate;
145
193
  const now = new Date(Date.now());
146
194
  if (certNotBefore > now || certNotAfter < now) {
147
195
  throw new Error('Certificate is not yet valid or expired');
148
196
  }
149
197
  }
150
- class InvalidCertificatePath extends Error {
151
- constructor() {
152
- const message = 'x5c could not be chained to any specified trust anchor';
153
- super(message);
154
- this.name = 'InvalidX5CChain';
155
- }
156
- }
@@ -1 +1 @@
1
- {"version":3,"file":"verifyAttestationWithMetadata.d.ts","sourceRoot":"","sources":["../../src/metadata/verifyAttestationWithMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAIhE,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,OAAO,EAEZ,OAAO,EAER,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;GAGG;AACH,wBAAsB,6BAA6B,CAAC,EAClD,SAAS,EACT,mBAAmB,EACnB,GAAG,EACH,uBAAuB,GACxB,EAAE;IACD,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mBAAmB,EAAE,WAAW,CAAC;IACjC,GAAG,EAAE,WAAW,EAAE,GAAG,eAAe,EAAE,CAAC;IACvC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,OAAO,CAAC,CAoJnB;AAED,KAAK,QAAQ,GAAG;IACd,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE;KAAG,GAAG,IAAI,OAAO,GAAG,QAAQ;CAe9D,CAAC"}
1
+ {"version":3,"file":"verifyAttestationWithMetadata.d.ts","sourceRoot":"","sources":["../../src/metadata/verifyAttestationWithMetadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAIhE,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,OAAO,EAEZ,OAAO,EAER,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD;;;GAGG;AACH,wBAAsB,6BAA6B,CAAC,EAClD,SAAS,EACT,mBAAmB,EACnB,GAAG,EACH,uBAAuB,GACxB,EAAE;IACD,SAAS,EAAE,iBAAiB,CAAC;IAC7B,mBAAmB,EAAE,WAAW,CAAC;IACjC,GAAG,EAAE,WAAW,EAAE,GAAG,eAAe,EAAE,CAAC;IACvC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,OAAO,CAAC,CA6InB;AAED,KAAK,QAAQ,GAAG;IACd,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE;KAAG,GAAG,IAAI,OAAO,GAAG,QAAQ;CAe9D,CAAC"}
@@ -110,13 +110,7 @@ async function verifyAttestationWithMetadata({ statement, credentialPublicKey, x
110
110
  authenticatorIsSelfReferencing = true;
111
111
  }
112
112
  if (!authenticatorIsSelfReferencing) {
113
- try {
114
- await (0, validateCertificatePath_js_1.validateCertificatePath)(authenticatorCerts, statementRootCerts);
115
- }
116
- catch (err) {
117
- const _err = err;
118
- throw new Error(`Could not validate certificate path with any metadata root certificates: ${_err.message}`);
119
- }
113
+ await (0, validateCertificatePath_js_1.validateCertificatePath)(authenticatorCerts, statementRootCerts);
120
114
  }
121
115
  return true;
122
116
  }