|
1 | 1 | import './additional-root-cas.js' |
2 | 2 | import { crypto } from '../crypto/index.ts' |
3 | | -import type { CertificatePublicKey, CipherSuite, Key, TLSProcessContext, X509Certificate } from '../types/index.ts' |
| 3 | +import type { CertificatePublicKey, CipherSuite, Key, Logger, TLSProcessContext, X509Certificate } from '../types/index.ts' |
4 | 4 | import { SUPPORTED_NAMED_CURVE_MAP, SUPPORTED_SIGNATURE_ALGS, SUPPORTED_SIGNATURE_ALGS_MAP } from './constants.ts' |
5 | 5 | import { getHash } from './decryption-utils.ts' |
6 | 6 | import { areUint8ArraysEqual, asciiToUint8Array, concatenateUint8Arrays } from './generics.ts' |
7 | 7 | import { MOZILLA_ROOT_CA_LIST } from './mozilla-root-cas.ts' |
8 | 8 | import { expectReadWithLength, packWithLength } from './packets.ts' |
9 | | -import { loadX509FromDer, loadX509FromPem } from './x509.ts' |
| 9 | +import { defaultFetchCertificateBytes, loadX509FromDer, loadX509FromPem } from './x509.ts' |
10 | 10 |
|
11 | 11 | type VerifySignatureOptions = { |
12 | 12 | signature: Uint8Array |
@@ -111,8 +111,7 @@ export async function verifyCertificateSignature({ |
111 | 111 | } |
112 | 112 |
|
113 | 113 | export async function getSignatureDataTls13( |
114 | | - hellos: Uint8Array[] | Uint8Array, |
115 | | - cipherSuite: CipherSuite |
| 114 | + hellos: Uint8Array[] | Uint8Array, cipherSuite: CipherSuite |
116 | 115 | ) { |
117 | 116 | const handshakeHash = await getHash(hellos, cipherSuite) |
118 | 117 | return concatenateUint8Arrays([ |
@@ -155,74 +154,81 @@ export async function getSignatureDataTls12( |
155 | 154 | export async function verifyCertificateChain( |
156 | 155 | chain: X509Certificate[], |
157 | 156 | host: string, |
| 157 | + logger: Logger, |
| 158 | + fetchCertificateBytes = defaultFetchCertificateBytes, |
158 | 159 | additionalRootCAs?: X509Certificate[] |
159 | 160 | ) { |
160 | 161 | const rootCAs = [ |
161 | 162 | ...loadRootCAs(), |
162 | 163 | ...(additionalRootCAs || []) |
163 | 164 | ] |
164 | 165 |
|
| 166 | + const leaf = chain[0] |
165 | 167 | const commonNames = [ |
166 | | - ...chain[0].getSubjectField('CN'), |
167 | | - ...chain[0].getAlternativeDNSNames() |
| 168 | + ...leaf.getSubjectField('CN'), |
| 169 | + ...leaf.getAlternativeDNSNames() |
168 | 170 | ] |
169 | 171 | if(!commonNames.some(cn => matchHostname(host, cn))) { |
170 | 172 | throw new Error(`Certificate is not for host ${host}`) |
171 | 173 | } |
172 | 174 |
|
| 175 | + chain = [...chain] // clone to allow appending fetched certs |
| 176 | + for(let i = 0; i < chain.length; i++) { |
| 177 | + const cert = chain[i] |
| 178 | + const cn = cert.getSubjectField('CN') |
| 179 | + if(!cert.isWithinValidity()) { |
| 180 | + throw new Error(`Certificate ${cn} (i: ${i}) is outside validity`) |
| 181 | + } |
173 | 182 |
|
174 | | - const tmpChain = [...chain] |
175 | | - let currentCert = tmpChain.shift()! |
176 | | - let rootIssuer: X509Certificate<any> | undefined |
177 | | - // look for issuers until we hit the end or find root CA that signed one of them |
178 | | - while(!rootIssuer) { |
179 | | - const cn = currentCert.getSubjectField('CN') |
180 | | - |
181 | | - if(!currentCert.isWithinValidity()) { |
182 | | - throw new Error(`Certificate ${cn} is not within validity period`) |
| 183 | + // look in our chain for issuer |
| 184 | + let issuer = findIssuer(chain.slice(i + 1), cert) |
| 185 | + // if not found, check in our root CAs |
| 186 | + if(!issuer) { |
| 187 | + issuer = findIssuer(rootCAs, cert) |
183 | 188 | } |
184 | 189 |
|
185 | | - rootIssuer = rootCAs.find(r => r.isIssuer(currentCert)) |
| 190 | + // if not found, we'll try fetching it via AIA extension |
| 191 | + if(!issuer) { |
| 192 | + const aiaExt = cert.getAIAExtension() |
| 193 | + if(!aiaExt) { |
| 194 | + throw new Error(`Missing issuer for certificate ${cn} (i: ${i})`) |
| 195 | + } |
186 | 196 |
|
187 | | - if(!rootIssuer) { |
188 | | - const issuer = findIssuer(tmpChain, currentCert) |
| 197 | + if(TLS_INTERMEDIATE_CA_CACHE?.[aiaExt]) { |
| 198 | + issuer = TLS_INTERMEDIATE_CA_CACHE[aiaExt] |
| 199 | + } else { |
| 200 | + logger.debug( |
| 201 | + { aiaExt, cn }, |
| 202 | + 'fetching issuer certificate via AIA extension' |
| 203 | + ) |
189 | 204 |
|
190 | | - //in case there are orphan certificates in chain |
191 | | - if(!issuer) { |
192 | | - break |
193 | | - } |
| 205 | + const bytes = await fetchCertificateBytes(aiaExt) |
| 206 | + issuer = await loadX509FromPem(bytes) |
| 207 | + // we'll need to verify this cert below too |
| 208 | + chain.push(issuer) |
194 | 209 |
|
195 | | - if(!(await issuer.cert.verifyIssued(currentCert))) { |
196 | | - throw new Error(`Certificate ${cn} issue verification failed`) |
| 210 | + TLS_INTERMEDIATE_CA_CACHE[aiaExt] = issuer |
197 | 211 | } |
198 | | - |
199 | | - currentCert = issuer.cert |
200 | | - //remove issuer cert from chain |
201 | | - tmpChain.splice(issuer.index, 1) |
202 | 212 | } |
203 | | - } |
204 | | - |
205 | | - if(!rootIssuer) { |
206 | | - throw new Error('Root CA not found. Could not verify certificate') |
207 | | - } |
208 | 213 |
|
209 | | - const verified = await rootIssuer.verifyIssued(currentCert) |
210 | | - if(!verified) { |
211 | | - throw new Error('Root CA did not issue certificate') |
212 | | - } |
| 214 | + if(!issuer.isWithinValidity()) { |
| 215 | + throw new Error(`Issuer Cert ${cn} is not within validity period`) |
| 216 | + } |
213 | 217 |
|
214 | | - if(!rootIssuer.isWithinValidity()) { |
215 | | - throw new Error('CA certificate is not within validity period') |
| 218 | + if(!(await issuer.verifyIssued(cert))) { |
| 219 | + const icn = issuer.getSubjectField('CN') |
| 220 | + throw new Error( |
| 221 | + `Verification of ${cn} failed by issuer ${icn} (i: ${i})` |
| 222 | + ) |
| 223 | + } |
216 | 224 | } |
| 225 | +} |
217 | 226 |
|
218 | | - function findIssuer(chain:X509Certificate[], cert: X509Certificate) { |
219 | | - for(const [i, element] of chain.entries()) { |
220 | | - if(element.isIssuer(cert)) { |
221 | | - return { cert: element, index: i } |
222 | | - } |
| 227 | +function findIssuer(chain: X509Certificate[], cert: X509Certificate) { |
| 228 | + for(const element of chain) { |
| 229 | + if(element.isIssuer(cert)) { |
| 230 | + return element |
223 | 231 | } |
224 | | - |
225 | | - return null |
226 | 232 | } |
227 | 233 | } |
228 | 234 |
|
|
0 commit comments