diff --git a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift index e9b90a23f1..5f4812661a 100644 --- a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift +++ b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift @@ -220,6 +220,64 @@ extension _EasyHandle { try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionCAINFO, caInfo).asError() } return + } else { + // When no certificate file has been specified, assemble all the certificate files + // from the Android certificate store and writes them to a single `cacerts.pem` file + + // See https://github.com/apple/swift-nio-ssl/blob/main/Sources/NIOSSL/AndroidCABundle.swift + let certsFolders = [ + "/apex/com.android.conscrypt/cacerts", // >= Android14 + "/system/etc/security/cacerts" // < Android14 + ] + + let aggregateCertPath = NSTemporaryDirectory() + "/cacerts-\(UUID().uuidString).pem" + + if FileManager.default.createFile(atPath: aggregateCertPath, contents: nil) == false { + return + } + + guard let fs = FileHandle(forWritingAtPath: aggregateCertPath) else { + return + } + + // write a header + fs.write(""" + ## Bundle of CA Root Certificates + ## Auto-generated on \(Date()) + ## by aggregating certificates from: \(certsFolders) + + """.data(using: .utf8)!) + + // Go through each folder and load each certificate file (ending with ".0"), + // and append them together into a single aggreagate file that curl can load. + // The .0 files will contain some extra metadata, but libcurl only cares about the + // -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- sections, + // so we can naïvely concatenate them all and libcurl will understand the bundle. + for certsFolder in certsFolders { + let certsFolderURL = URL(fileURLWithPath: certsFolder) + if (try? certsFolderURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) != true { continue } + let certURLs = try! FileManager.default.contentsOfDirectory(at: certsFolderURL, includingPropertiesForKeys: [.isRegularFileKey, .isReadableKey]) + for certURL in certURLs { + // certificate files have names like "53a1b57a.0" + if certURL.pathExtension != "0" { continue } + do { + try? fs.write(contentsOf: Data(contentsOf: certURL)) + } catch { + // ignore individual errors and soldier on… + continue + } + } + } + + try! fs.close() + + aggregateCertPath.withCString { pathPtr in + // note that it would be nice to use CFURLSessionOptionCAPATH instead + // (see https://curl.se/libcurl/c/CURLOPT_CAPATH.html) + // but it requires `c_rehash` to be run on the folder, which Android doesn't do + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionCAINFO, UnsafeMutablePointer(mutating: pathPtr)).asError() + } + return } #endif