@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.
- package/esm/errors/index.d.ts +1 -1
- package/esm/errors/index.d.ts.map +1 -1
- package/esm/helpers/isCertRevoked.d.ts +1 -1
- package/esm/helpers/isCertRevoked.d.ts.map +1 -1
- package/esm/helpers/isCertRevoked.js +90 -70
- package/esm/helpers/validateCertificatePath.d.ts.map +1 -1
- package/esm/helpers/validateCertificatePath.js +93 -52
- package/esm/metadata/verifyAttestationWithMetadata.d.ts.map +1 -1
- package/esm/metadata/verifyAttestationWithMetadata.js +1 -7
- package/package.json +1 -1
- package/script/errors/index.d.ts +1 -1
- package/script/errors/index.d.ts.map +1 -1
- package/script/helpers/isCertRevoked.d.ts +1 -1
- package/script/helpers/isCertRevoked.d.ts.map +1 -1
- package/script/helpers/isCertRevoked.js +90 -70
- package/script/helpers/validateCertificatePath.d.ts.map +1 -1
- package/script/helpers/validateCertificatePath.js +93 -52
- package/script/metadata/verifyAttestationWithMetadata.d.ts.map +1 -1
- package/script/metadata/verifyAttestationWithMetadata.js +1 -7
package/esm/errors/index.d.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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,
|
|
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(
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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(
|
|
53
|
+
return cached.revokedCerts.indexOf(certificate.serialNumber) >= 0;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
package/script/errors/index.d.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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,
|
|
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(
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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(
|
|
56
|
+
return cached.revokedCerts.indexOf(certificate.serialNumber) >= 0;
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
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;
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
}
|