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
145
146
147
148
149
|
import { AsnSerializer } from '../deps.ts';
import { isCertRevoked } from './isCertRevoked.ts';
import { verifySignature } from './verifySignature.ts';
import { mapX509SignatureAlgToCOSEAlg } from './mapX509SignatureAlgToCOSEAlg.ts';
import { getCertificateInfo } from './getCertificateInfo.ts';
import { convertPEMToBytes } from './convertPEMToBytes.ts';
/**
* 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 isLeafCert = i === 0;
const isRootCert = i + 1 >= certificates.length;
let issuerPem = '';
if (isRootCert) {
issuerPem = subjectPem;
} else {
issuerPem = certificates[i + 1];
}
const subjectInfo = getCertificateInfo(convertPEMToBytes(subjectPem));
const issuerInfo = getCertificateInfo(convertPEMToBytes(issuerPem));
const x509Subject = subjectInfo.parsedCertificate;
// Check for certificate revocation
const subjectCertRevoked = await isCertRevoked(x509Subject);
if (subjectCertRevoked) {
throw new Error(`Found revoked certificate in certificate path`);
}
// Check that intermediate certificate is within its valid time window
const { notBefore, notAfter } = issuerInfo;
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 (subjectInfo.issuer.combined !== issuerInfo.subject.combined) {
throw new InvalidSubjectAndIssuer();
}
// Verify the subject certificate's signature with the issuer cert's public key
const data = AsnSerializer.serialize(x509Subject.tbsCertificate);
const signature = x509Subject.signatureValue;
const signatureAlgorithm = mapX509SignatureAlgToCOSEAlg(
x509Subject.signatureAlgorithm.algorithm,
);
const issuerCertBytes = convertPEMToBytes(issuerPem);
const verified = await verifySignature({
data: new Uint8Array(data),
signature: new Uint8Array(signature),
x509Certificate: issuerCertBytes,
hashAlgorithm: signatureAlgorithm,
});
if (!verified) {
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';
}
}
|