blob: ae8a2fde023e3803c319d646fde024a78a03aedd (
plain)
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
|
/* 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 default 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;
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
invalidSubjectAndIssuerError = false;
break;
} catch (err) {
if (err instanceof InvalidSubjectAndIssuer) {
invalidSubjectAndIssuerError = true;
} else {
throw err;
}
}
}
// We tried multiple root certs and none of them worked
if (invalidSubjectAndIssuerError) {
throw new InvalidSubjectAndIssuer();
}
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);
let issuerPem = '';
if (i + 1 >= certificates.length) {
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();
if (notBefore > now || notAfter < now) {
throw new Error('Intermediate certificate is not yet valid or expired');
}
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';
}
}
|