Skip to content

Commit 33d3f63

Browse files
committed
Better OCSP error handling and cache OCSP results
1 parent e5104b8 commit 33d3f63

File tree

1 file changed

+99
-21
lines changed

1 file changed

+99
-21
lines changed

Sources/PackageCollectionsSigning/Certificate/CertificatePolicy.swift

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -179,17 +179,33 @@ extension CertificatePolicy {
179179

180180
if certChain.count >= 1, let httpClient = httpClient {
181181
// Whether cert chain can be trusted depends on OCSP result
182-
self.BoringSSL_OCSP_isGood(certificate: certChain[0], issuer: certChain[1], httpClient: httpClient, callbackQueue: callbackQueue, callback: callback)
182+
ocspClient.checkStatus(certificate: certChain[0], issuer: certChain[1], httpClient: httpClient,
183+
diagnosticsEngine: diagnosticsEngine, callbackQueue: callbackQueue, callback: callback)
183184
} else {
184185
wrappedCallback(.success(()))
185186
}
186187
}
188+
#endif
189+
}
190+
191+
#if !os(macOS)
192+
private let ocspClient = BoringSSLOCSPClient()
193+
194+
private struct BoringSSLOCSPClient {
195+
private let resultCache = ThreadSafeKeyValueStore<CacheKey, CacheValue>()
196+
197+
private let cacheTTL: DispatchTimeInterval
187198

188-
private func BoringSSL_OCSP_isGood(certificate: Certificate,
189-
issuer: Certificate,
190-
httpClient: HTTPClient,
191-
callbackQueue: DispatchQueue,
192-
callback: @escaping (Result<Void, Error>) -> Void) {
199+
init(cacheTTL: DispatchTimeInterval = .seconds(300)) {
200+
self.cacheTTL = cacheTTL
201+
}
202+
203+
func checkStatus(certificate: Certificate,
204+
issuer: Certificate,
205+
httpClient: HTTPClient,
206+
diagnosticsEngine: DiagnosticsEngine,
207+
callbackQueue: DispatchQueue,
208+
callback: @escaping (Result<Void, Error>) -> Void) {
193209
let wrappedCallback: (Result<Void, Error>) -> Void = { result in callbackQueue.async { callback(result) } }
194210

195211
let ocspURLs = CCryptoBoringSSL_X509_get1_ocsp(certificate.underlying)
@@ -224,16 +240,27 @@ extension CertificatePolicy {
224240

225241
let requestData = Data(UnsafeBufferPointer(start: out.pointee, count: count))
226242

227-
let results = ThreadSafeArrayStore<Bool>()
243+
let results = ThreadSafeArrayStore<Result<Bool, Error>>()
228244
let group = DispatchGroup()
229245

230246
// Query each OCSP responder and record result
231247
for index in 0 ..< ocspURLCount {
232248
guard let urlStr = CCryptoBoringSSL_sk_OPENSSL_STRING_value(ocspURLs, numericCast(index)),
233249
let url = String(validatingUTF8: urlStr).flatMap({ URL(string: $0) }) else {
250+
results.append(.failure(OCSPError.badURL))
234251
continue
235252
}
236253

254+
let cacheKey = CacheKey(url: url, request: requestData)
255+
if let cachedResult = self.resultCache[cacheKey] {
256+
if cachedResult.timestamp + self.cacheTTL > DispatchTime.now() {
257+
results.append(.success(cachedResult.isCertGood))
258+
continue
259+
} else {
260+
_ = self.resultCache.removeValue(forKey: cacheKey)
261+
}
262+
}
263+
237264
var headers = HTTPClientHeaders()
238265
headers.add(name: "Content-Type", value: "application/ocsp-request")
239266
url.host.map { headers.add(name: "Host", value: "\($0)") }
@@ -246,16 +273,20 @@ extension CertificatePolicy {
246273
defer { group.leave() }
247274

248275
switch result {
249-
case .failure:
250-
return
276+
case .failure(let error):
277+
results.append(.failure(error))
251278
case .success(let response):
252-
guard let responseData = response.body else { return }
279+
guard let responseData = response.body else {
280+
results.append(.failure(OCSPError.emptyResponseBody))
281+
return
282+
}
253283

254284
let bytes = responseData.copyBytes()
255285
// Convert response to bio then OCSP response
256286
guard let bio = CCryptoBoringSSL_BIO_new(CCryptoBoringSSL_BIO_s_mem()),
257287
CCryptoBoringSSL_BIO_write(bio, bytes, numericCast(bytes.count)) > 0,
258288
let response = d2i_OCSP_RESPONSE_bio(bio, nil) else {
289+
results.append(.failure(OCSPError.responseConversionFailure))
259290
return
260291
}
261292
defer { CCryptoBoringSSL_BIO_free(bio) }
@@ -266,6 +297,7 @@ extension CertificatePolicy {
266297
CCryptoBoringSSL_OBJ_obj2nid(response.pointee.responseBytes.pointee.responseType) == NID_id_pkix_OCSP_basic,
267298
let basicResp = OCSP_response_get1_basic(response),
268299
let basicRespData = basicResp.pointee.tbsResponseData?.pointee else {
300+
results.append(.failure(OCSPError.badResponse))
269301
return
270302
}
271303
defer { OCSP_BASICRESP_free(basicResp) }
@@ -274,11 +306,14 @@ extension CertificatePolicy {
274306
for i in 0 ..< sk_OCSP_SINGLERESP_num(basicRespData.responses) {
275307
guard let singleResp = sk_OCSP_SINGLERESP_value(basicRespData.responses, numericCast(i)),
276308
let certStatus = singleResp.pointee.certStatus else {
277-
continue
309+
results.append(.failure(OCSPError.badResponse))
310+
return
278311
}
279312

280313
// Is the certificate in good status?
281-
results.append(certStatus.pointee.type == V_OCSP_CERTSTATUS_GOOD)
314+
let isCertGood = certStatus.pointee.type == V_OCSP_CERTSTATUS_GOOD
315+
results.append(.success(isCertGood))
316+
self.resultCache[cacheKey] = CacheValue(isCertGood: isCertGood, timestamp: DispatchTime.now())
282317
break
283318
}
284319
}
@@ -287,19 +322,59 @@ extension CertificatePolicy {
287322

288323
group.notify(queue: callbackQueue) {
289324
// If there's no result then something must have gone wrong
290-
guard !results.isEmpty else {
325+
guard !results.isEmpty, results.compactMap({ $0.failure }).isEmpty else {
326+
diagnosticsEngine.emit(error: "OCSP failed. All results: \(results)")
291327
return wrappedCallback(.failure(CertificatePolicyError.ocspFailure))
292328
}
293329
// Is there response "bad status" response?
294-
guard results.get().first(where: { !$0 }) == nil else {
330+
guard results.compactMap({ $0.success }).first(where: { !$0 }) == nil else {
295331
return wrappedCallback(.failure(CertificatePolicyError.invalidCertChain))
296332
}
297333
wrappedCallback(.success(()))
298334
}
299335
}
300-
#endif
336+
337+
private struct CacheKey: Hashable {
338+
let url: URL
339+
let request: Data
340+
}
341+
342+
private struct CacheValue {
343+
let isCertGood: Bool
344+
let timestamp: DispatchTime
345+
}
346+
}
347+
348+
private extension Result {
349+
var failure: Failure? {
350+
switch self {
351+
case .failure(let failure):
352+
return failure
353+
case .success:
354+
return nil
355+
}
356+
}
357+
358+
var success: Success? {
359+
switch self {
360+
case .failure:
361+
return nil
362+
case .success(let value):
363+
return value
364+
}
365+
}
301366
}
302367

368+
private extension HTTPClient {
369+
static func makeDefault(callbackQueue: DispatchQueue) -> HTTPClient {
370+
var httpClientConfig = HTTPClientConfiguration()
371+
httpClientConfig.callbackQueue = callbackQueue
372+
httpClientConfig.requestTimeout = .seconds(1)
373+
return HTTPClient(configuration: httpClientConfig)
374+
}
375+
}
376+
#endif
377+
303378
// MARK: - Supporting methods and types
304379

305380
extension CertificatePolicy {
@@ -411,6 +486,13 @@ enum CertificatePolicyError: Error, Equatable {
411486
case ocspFailure
412487
}
413488

489+
private enum OCSPError: Error {
490+
case badURL
491+
case emptyResponseBody
492+
case responseConversionFailure
493+
case badResponse
494+
}
495+
414496
// MARK: - Certificate policies
415497

416498
/// Default policy for validating certificates used to sign package collections.
@@ -459,9 +541,7 @@ struct DefaultCertificatePolicy: CertificatePolicy {
459541
self.diagnosticsEngine = diagnosticsEngine
460542

461543
#if !os(macOS)
462-
var httpClientConfig = HTTPClientConfiguration()
463-
httpClientConfig.callbackQueue = callbackQueue
464-
self.httpClient = HTTPClient(configuration: httpClientConfig)
544+
self.httpClient = HTTPClient.makeDefault(callbackQueue: callbackQueue)
465545
#endif
466546
}
467547

@@ -547,9 +627,7 @@ struct AppleDeveloperCertificatePolicy: CertificatePolicy {
547627
self.diagnosticsEngine = diagnosticsEngine
548628

549629
#if !os(macOS)
550-
var httpClientConfig = HTTPClientConfiguration()
551-
httpClientConfig.callbackQueue = callbackQueue
552-
self.httpClient = HTTPClient(configuration: httpClientConfig)
630+
self.httpClient = HTTPClient.makeDefault(callbackQueue: callbackQueue)
553631
#endif
554632
}
555633

0 commit comments

Comments
 (0)