Skip to content

Commit 1f3f797

Browse files
committed
Better OCSP error handling and limit request rate in tests
1 parent 1dca48a commit 1f3f797

File tree

4 files changed

+91
-15
lines changed

4 files changed

+91
-15
lines changed

Sources/PackageCollectionsSigning/Certificate/CertificatePolicy.swift

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,8 @@ 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+
self.BoringSSL_OCSP_isGood(certificate: certChain[0], issuer: certChain[1], httpClient: httpClient,
183+
diagnosticsEngine: diagnosticsEngine, callbackQueue: callbackQueue, callback: callback)
183184
} else {
184185
wrappedCallback(.success(()))
185186
}
@@ -188,6 +189,7 @@ extension CertificatePolicy {
188189
private func BoringSSL_OCSP_isGood(certificate: Certificate,
189190
issuer: Certificate,
190191
httpClient: HTTPClient,
192+
diagnosticsEngine: DiagnosticsEngine,
191193
callbackQueue: DispatchQueue,
192194
callback: @escaping (Result<Void, Error>) -> Void) {
193195
let wrappedCallback: (Result<Void, Error>) -> Void = { result in callbackQueue.async { callback(result) } }
@@ -224,13 +226,14 @@ extension CertificatePolicy {
224226

225227
let requestData = Data(UnsafeBufferPointer(start: out.pointee, count: count))
226228

227-
let results = ThreadSafeArrayStore<Bool>()
229+
let results = ThreadSafeArrayStore<Result<Bool, Error>>()
228230
let group = DispatchGroup()
229231

230232
// Query each OCSP responder and record result
231233
for index in 0 ..< ocspURLCount {
232234
guard let urlStr = CCryptoBoringSSL_sk_OPENSSL_STRING_value(ocspURLs, numericCast(index)),
233235
let url = String(validatingUTF8: urlStr).flatMap({ URL(string: $0) }) else {
236+
results.append(.failure(OCSPError.badURL))
234237
continue
235238
}
236239

@@ -246,16 +249,20 @@ extension CertificatePolicy {
246249
defer { group.leave() }
247250

248251
switch result {
249-
case .failure:
250-
return
252+
case .failure(let error):
253+
results.append(.failure(error))
251254
case .success(let response):
252-
guard let responseData = response.body else { return }
255+
guard let responseData = response.body else {
256+
results.append(.failure(OCSPError.emptyResponseBody))
257+
return
258+
}
253259

254260
let bytes = responseData.copyBytes()
255261
// Convert response to bio then OCSP response
256262
guard let bio = CCryptoBoringSSL_BIO_new(CCryptoBoringSSL_BIO_s_mem()),
257263
CCryptoBoringSSL_BIO_write(bio, bytes, numericCast(bytes.count)) > 0,
258264
let response = d2i_OCSP_RESPONSE_bio(bio, nil) else {
265+
results.append(.failure(OCSPError.responseConversionFailure))
259266
return
260267
}
261268
defer { CCryptoBoringSSL_BIO_free(bio) }
@@ -266,6 +273,7 @@ extension CertificatePolicy {
266273
CCryptoBoringSSL_OBJ_obj2nid(response.pointee.responseBytes.pointee.responseType) == NID_id_pkix_OCSP_basic,
267274
let basicResp = OCSP_response_get1_basic(response),
268275
let basicRespData = basicResp.pointee.tbsResponseData?.pointee else {
276+
results.append(.failure(OCSPError.badResponse))
269277
return
270278
}
271279
defer { OCSP_BASICRESP_free(basicResp) }
@@ -274,11 +282,12 @@ extension CertificatePolicy {
274282
for i in 0 ..< sk_OCSP_SINGLERESP_num(basicRespData.responses) {
275283
guard let singleResp = sk_OCSP_SINGLERESP_value(basicRespData.responses, numericCast(i)),
276284
let certStatus = singleResp.pointee.certStatus else {
277-
continue
285+
results.append(.failure(OCSPError.badResponse))
286+
return
278287
}
279288

280289
// Is the certificate in good status?
281-
results.append(certStatus.pointee.type == V_OCSP_CERTSTATUS_GOOD)
290+
results.append(.success(certStatus.pointee.type == V_OCSP_CERTSTATUS_GOOD))
282291
break
283292
}
284293
}
@@ -287,11 +296,12 @@ extension CertificatePolicy {
287296

288297
group.notify(queue: callbackQueue) {
289298
// If there's no result then something must have gone wrong
290-
guard !results.isEmpty else {
299+
guard !results.isEmpty, results.compactMap({ $0.failure }).isEmpty else {
300+
diagnosticsEngine.emit(error: "OCSP failed. All results: \(results)")
291301
return wrappedCallback(.failure(CertificatePolicyError.ocspFailure))
292302
}
293303
// Is there response "bad status" response?
294-
guard results.get().first(where: { !$0 }) == nil else {
304+
guard results.compactMap({ $0.success }).first(where: { !$0 }) == nil else {
295305
return wrappedCallback(.failure(CertificatePolicyError.invalidCertChain))
296306
}
297307
wrappedCallback(.success(()))
@@ -411,6 +421,13 @@ enum CertificatePolicyError: Error, Equatable {
411421
case ocspFailure
412422
}
413423

424+
private enum OCSPError: Error {
425+
case badURL
426+
case emptyResponseBody
427+
case responseConversionFailure
428+
case badResponse
429+
}
430+
414431
// MARK: - Certificate policies
415432

416433
/// Default policy for validating certificates used to sign package collections.
@@ -459,9 +476,7 @@ struct DefaultCertificatePolicy: CertificatePolicy {
459476
self.diagnosticsEngine = diagnosticsEngine
460477

461478
#if !os(macOS)
462-
var httpClientConfig = HTTPClientConfiguration()
463-
httpClientConfig.callbackQueue = callbackQueue
464-
self.httpClient = HTTPClient(configuration: httpClientConfig)
479+
self.httpClient = HTTPClient.makeDefault(callbackQueue: callbackQueue)
465480
#endif
466481
}
467482

@@ -547,9 +562,7 @@ struct AppleDeveloperCertificatePolicy: CertificatePolicy {
547562
self.diagnosticsEngine = diagnosticsEngine
548563

549564
#if !os(macOS)
550-
var httpClientConfig = HTTPClientConfiguration()
551-
httpClientConfig.callbackQueue = callbackQueue
552-
self.httpClient = HTTPClient(configuration: httpClientConfig)
565+
self.httpClient = HTTPClient.makeDefault(callbackQueue: callbackQueue)
553566
#endif
554567
}
555568

@@ -611,3 +624,34 @@ public enum CertificatePolicyKey: Equatable, Hashable {
611624
public static let `default` = CertificatePolicyKey.default(subjectUserID: nil)
612625
public static let appleDistribution = CertificatePolicyKey.appleDistribution(subjectUserID: nil)
613626
}
627+
628+
// MARK: - Utilities
629+
630+
private extension Result {
631+
var failure: Failure? {
632+
switch self {
633+
case .failure(let failure):
634+
return failure
635+
case .success:
636+
return nil
637+
}
638+
}
639+
640+
var success: Success? {
641+
switch self {
642+
case .failure:
643+
return nil
644+
case .success(let value):
645+
return value
646+
}
647+
}
648+
}
649+
650+
private extension HTTPClient {
651+
static func makeDefault(callbackQueue: DispatchQueue) -> HTTPClient {
652+
var httpClientConfig = HTTPClientConfiguration()
653+
httpClientConfig.callbackQueue = callbackQueue
654+
httpClientConfig.requestTimeout = .seconds(1)
655+
return HTTPClient(configuration: httpClientConfig)
656+
}
657+
}

Tests/PackageCollectionsSigningTests/CertificatePolicyTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,20 +197,26 @@ class CertificatePolicyTests: XCTestCase {
197197
try withTemporaryDirectory { tmp in
198198
try localFileSystem.copy(from: rootCAPath, to: tmp.appending(components: "AppleIncRoot.cer"))
199199

200+
waitBetweenOCSPRequests()
201+
200202
// Specify `trustedRootCertsDir`
201203
do {
202204
let policy = DefaultCertificatePolicy(trustedRootCertsDir: tmp.asURL, additionalTrustedRootCerts: nil,
203205
callbackQueue: callbackQueue, diagnosticsEngine: diagnosticsEngine)
204206
XCTAssertNoThrow(try tsc_await { callback in policy.validate(certChain: certChain, callback: callback) })
205207
}
206208

209+
waitBetweenOCSPRequests()
210+
207211
// Another way is to pass in `additionalTrustedRootCerts`
208212
do {
209213
let policy = DefaultCertificatePolicy(trustedRootCertsDir: nil, additionalTrustedRootCerts: [rootCA],
210214
callbackQueue: callbackQueue, diagnosticsEngine: diagnosticsEngine)
211215
XCTAssertNoThrow(try tsc_await { callback in policy.validate(certChain: certChain, callback: callback) })
212216
}
213217

218+
waitBetweenOCSPRequests()
219+
214220
// What if the same cert is in both `trustedRootCertsDir` and `additionalTrustedRootCerts`?
215221
do {
216222
let policy = DefaultCertificatePolicy(trustedRootCertsDir: tmp.asURL, additionalTrustedRootCerts: [rootCA],
@@ -262,20 +268,26 @@ class CertificatePolicyTests: XCTestCase {
262268
try withTemporaryDirectory { tmp in
263269
try localFileSystem.copy(from: rootCAPath, to: tmp.appending(components: "AppleIncRoot.cer"))
264270

271+
waitBetweenOCSPRequests()
272+
265273
// Specify `trustedRootCertsDir`
266274
do {
267275
let policy = AppleDeveloperCertificatePolicy(trustedRootCertsDir: tmp.asURL, additionalTrustedRootCerts: nil,
268276
callbackQueue: callbackQueue, diagnosticsEngine: diagnosticsEngine)
269277
XCTAssertNoThrow(try tsc_await { callback in policy.validate(certChain: certChain, callback: callback) })
270278
}
271279

280+
waitBetweenOCSPRequests()
281+
272282
// Another way is to pass in `additionalTrustedRootCerts`
273283
do {
274284
let policy = AppleDeveloperCertificatePolicy(trustedRootCertsDir: nil, additionalTrustedRootCerts: [rootCA],
275285
callbackQueue: callbackQueue, diagnosticsEngine: diagnosticsEngine)
276286
XCTAssertNoThrow(try tsc_await { callback in policy.validate(certChain: certChain, callback: callback) })
277287
}
278288

289+
waitBetweenOCSPRequests()
290+
279291
// What if the same cert is in both `trustedRootCertsDir` and `additionalTrustedRootCerts`?
280292
do {
281293
let policy = AppleDeveloperCertificatePolicy(trustedRootCertsDir: tmp.asURL, additionalTrustedRootCerts: [rootCA],

Tests/PackageCollectionsSigningTests/PackageCollectionSigningTest.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,8 @@ class PackageCollectionSigningTests: XCTestCase {
252252
try withTemporaryDirectory { tmp in
253253
try localFileSystem.copy(from: rootCAPath, to: tmp.appending(components: "AppleIncRoot.cer"))
254254

255+
waitBetweenOCSPRequests()
256+
255257
// Specify `trustedRootCertsDir`
256258
do {
257259
let signing = PackageCollectionSigning(trustedRootCertsDir: tmp.asURL, callbackQueue: callbackQueue, diagnosticsEngine: diagnosticsEngine)
@@ -266,6 +268,8 @@ class PackageCollectionSigningTests: XCTestCase {
266268
})
267269
}
268270

271+
waitBetweenOCSPRequests()
272+
269273
// Another way is to pass in `additionalTrustedRootCerts`
270274
do {
271275
let signing = PackageCollectionSigning(additionalTrustedRootCerts: [rootCAData.base64EncodedString()],
@@ -344,6 +348,8 @@ class PackageCollectionSigningTests: XCTestCase {
344348
try withTemporaryDirectory { tmp in
345349
try localFileSystem.copy(from: rootCAPath, to: tmp.appending(components: "AppleIncRoot.cer"))
346350

351+
waitBetweenOCSPRequests()
352+
347353
// Specify `trustedRootCertsDir`
348354
do {
349355
let signing = PackageCollectionSigning(trustedRootCertsDir: tmp.asURL, callbackQueue: callbackQueue, diagnosticsEngine: diagnosticsEngine)
@@ -358,6 +364,8 @@ class PackageCollectionSigningTests: XCTestCase {
358364
})
359365
}
360366

367+
waitBetweenOCSPRequests()
368+
361369
// Another way is to pass in `additionalTrustedRootCerts`
362370
do {
363371
let signing = PackageCollectionSigning(additionalTrustedRootCerts: [rootCAData.base64EncodedString()],

Tests/PackageCollectionsSigningTests/Utilities.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,16 @@ extension String {
174174

175175
extension XCTestCase {
176176
func skipIfUnsupportedPlatform() throws {}
177+
178+
func waitBetweenOCSPRequests() {
179+
// Use this for "real" tests only (i.e., gated by ENABLE_REAL_CERT_TEST)!
180+
//
181+
// The OCSP implementation using PackageCollectionsSigningLibc does not cache
182+
// OCSP responses so we'd hit the responder on every query. It seems like a
183+
// responder may decide not to respond at all if we make too many requests too fast,
184+
// causing tests to fail due to HTTP timed out error.
185+
//
186+
// The Security framework does caching already so we shouldn't sleep at all.
187+
Thread.sleep(forTimeInterval: 2)
188+
}
177189
}

0 commit comments

Comments
 (0)