1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
// `ASN1HEX` exists in the lib but not in its typings
// @ts-ignore 2305
import { KJUR, X509, ASN1HEX, zulutodate } from 'jsrsasign';
import { isCertRevoked } from './isCertRevoked';
const { crypto } = KJUR;
/**
* Traverse an array of PEM certificates and ensure they form a proper chain
* @param certificates Typically the result of `x5c.map(convertASN1toPEM)`
* @param rootCertificates Possible root certificates to complete the path
*/
export async function validateCertificatePath(
certificates: string[],
rootCertificates: string[] = [],
): Promise<boolean> {
if (rootCertificates.length === 0) {
// We have no root certs with which to create a full path, so skip path validation
// TODO: Is this going to be acceptable default behavior??
return true;
}
let invalidSubjectAndIssuerError = false;
let certificateNotYetValidOrExpiredErrorMessage = undefined;
for (const rootCert of rootCertificates) {
try {
const certsWithRoot = certificates.concat([rootCert]);
await _validatePath(certsWithRoot);
// If we successfully validated a path then there's no need to continue. Reset any existing
// errors that were thrown by earlier root certificates
invalidSubjectAndIssuerError = false;
certificateNotYetValidOrExpiredErrorMessage = undefined;
break;
} catch (err) {
if (err instanceof InvalidSubjectAndIssuer) {
invalidSubjectAndIssuerError = true;
} else if (err instanceof CertificateNotYetValidOrExpired) {
certificateNotYetValidOrExpiredErrorMessage = err.message;
} else {
throw err;
}
}
}
// We tried multiple root certs and none of them worked
if (invalidSubjectAndIssuerError) {
throw new InvalidSubjectAndIssuer();
} else if (certificateNotYetValidOrExpiredErrorMessage) {
throw new CertificateNotYetValidOrExpired(certificateNotYetValidOrExpiredErrorMessage);
}
return true;
}
async function _validatePath(certificates: string[]): Promise<boolean> {
if (new Set(certificates).size !== certificates.length) {
throw new Error('Invalid certificate path: found duplicate certificates');
}
// From leaf to root, make sure each cert is issued by the next certificate in the chain
for (let i = 0; i < certificates.length; i += 1) {
const subjectPem = certificates[i];
const subjectCert = new X509();
subjectCert.readCertPEM(subjectPem);
const isLeafCert = i === 0;
const isRootCert = i + 1 >= certificates.length;
let issuerPem = '';
if (isRootCert) {
issuerPem = subjectPem;
} else {
issuerPem = certificates[i + 1];
}
const issuerCert = new X509();
issuerCert.readCertPEM(issuerPem);
// Check for certificate revocation
const subjectCertRevoked = await isCertRevoked(subjectCert);
if (subjectCertRevoked) {
throw new Error(`Found revoked certificate in certificate path`);
}
// Check that intermediate certificate is within its valid time window
const notBefore = zulutodate(issuerCert.getNotBefore());
const notAfter = zulutodate(issuerCert.getNotAfter());
const now = new Date(Date.now());
if (notBefore > now || notAfter < now) {
if (isLeafCert) {
throw new CertificateNotYetValidOrExpired(
`Leaf certificate is not yet valid or expired: ${issuerPem}`,
);
} else if (isRootCert) {
throw new CertificateNotYetValidOrExpired(
`Root certificate is not yet valid or expired: ${issuerPem}`,
);
} else {
throw new CertificateNotYetValidOrExpired(
`Intermediate certificate is not yet valid or expired: ${issuerPem}`,
);
}
}
if (subjectCert.getIssuerString() !== issuerCert.getSubjectString()) {
throw new InvalidSubjectAndIssuer();
}
const subjectCertStruct = ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]);
const alg = subjectCert.getSignatureAlgorithmField();
const signatureHex = subjectCert.getSignatureValueHex();
const Signature = new crypto.Signature({ alg });
Signature.init(issuerPem);
Signature.updateHex(subjectCertStruct);
if (!Signature.verify(signatureHex)) {
throw new Error('Invalid certificate path: invalid signature');
}
}
return true;
}
// Custom errors to help pass on certain errors
class InvalidSubjectAndIssuer extends Error {
constructor() {
const message = 'Subject issuer did not match issuer subject';
super(message);
this.name = 'InvalidSubjectAndIssuer';
}
}
class CertificateNotYetValidOrExpired extends Error {
constructor(message: string) {
super(message);
this.name = 'CertificateNotYetValidOrExpired';
}
}
|