Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use installed Android cacerts for URLSession #5163

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
58 changes: 58 additions & 0 deletions Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this file have to be cleaned up at some point?

Copy link
Author

@marcprux marcprux Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I'm not sure exactly when libcurl will read this file, and so we don't know when we can delete it. In the case of an Android app, NSTemporaryDirectory() will be the app's caches folder, and so will eventually be cleaned up, but still leaving stay files around like this is not good form.

I think the best solution would be rather than using CURLOPT_CAINFO (as exposed through CFURLSessionOptionCAINFO), we would instead use CURLOPT_CAINFO_BLOB, which would mean we wouldn't need to create any temporary file at all. But not only does this not currently have any equivalent CFURLSessionOption* property, it would also require bringing in the curl_blob struct from libcurl, which seemed a bit much just for this Android feature. OTOH, I don't see any other way to avoid the issue of leaving behind old aggregate certificate files.

If you think it is worth investigating, I can look into it…

return
}

guard let fs = FileHandle(forWritingAtPath: aggregateCertPath) else {
return
}

// write a header
fs.write("""
## Bundle of CA Root Certificates
## Auto-generated on \(Date())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to give this a specific format instead of just relying on the debug description.

## 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aggregateCertPath should probably be a URL and you can call withUnsafeFileSystemRepresentation here

// 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

Expand Down