-
Notifications
You must be signed in to change notification settings - Fork 1.2k
For https: to work on Android #1103
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
Conversation
The change is acceptable to me. However, though the change is additive in nature, we seem to have introduced a new "non-debug" environment variable here. So, this can be considered to be changing the external behaviour of Foundation on Android. @parkera @phausler Is there a general guidance around introducing new env variables? Where do we document these? |
// For SSL to work you need "cacert.pem" to be accessable | ||
// at the path pointed to by the URLSessionCAInfo env var. | ||
// Downloadable here: https://curl.haxx.se/ca/cacert.pem | ||
if let caInfo = getenv("URLSessionCAInfo") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ProcessInfo.processInfo.environment
is another way (Swifty) to access env vars, but I am not sure if it offers benefits in this context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
umm isn't this a security hole? If someone sets URLSessionCAInfo then they can circumvent the root certificate authorities? Thusly making all traffic vulnerable to being spoofed for certificates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without providing a URLSessionCAInfo file SSL doesn’t work at all using libcurl on Android. It doesn’t seem to have an idea about where the android root CA certs are stored and how to use them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there another way we can obtain the path other than env? My worry is that is a global state of the operating system that is publicly writable; so it could easily be hijacked by a malicious actor and circumvent root certificates.
I would presume that most android projects would be an APK that would load the libraries; can we add a JNI call or something like that so that once libFoundation.so is loaded the call is made to assign the appropriate path (as per the trust of the application).
Alternatively if there was some way we could grab that from the OS that would be preferable. Since I would assume that the cacert.pem would be stored as read only unless you are root.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, It does bear all the hallmarks of an attack vector of some sort. Would a static public setter (Android only) on URLSession be OK? Any preference for the name? Grabbing it from the OS would be a bit of a rewrite somewhere in libcurl which I don’t fancy taking on. Android does not store Root CA’s in .pem format as far as I can see: https://stackoverflow.com/questions/4461360/how-to-install-trusted-ca-certificate-on-android-device
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New version updated with setter i.e. URLSession.setCAInfoFile( "/mnt/sdcard/cacert.pem" )
@swift-ci please test |
definitely need this one for my swiftjava android project :) |
#if os(Android) | ||
extension URLSession { | ||
|
||
public static func setCAInfoFile( _ _CAInfoFile: String ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems much less vulnerable to me; thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree
I am going to trust the android side works as expected (since we don't have a CI hook for that) @swift-ci please test and merge |
#if os(Android) | ||
// See https://curl.haxx.se/docs/sslcerts.html | ||
// For SSL to work you need "cacert.pem" to be accessable | ||
// at the path pointed to by the URLSessionCAInfo env var. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment needs to be updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks again @xwu, anything else? Naming conventions etc? I’ll raise another PR.
#if os(Android)
// See https://curl.haxx.se/docs/sslcerts.html
// For SSL to work you need a "cacert.pem" to be accessible
// at the path set by URLSession.setCAInfoFile( <string> ).
// Downloadable here: https://curl.haxx.se/docs/caextract.html
if let caInfo = _EasyHandle._CAInfoFile {
if String(cString: caInfo) == "UNSAFE_SSL_NOVERIFY" {
try! CFURLSession_easy_setopt_int(rawHandle, CFURLSessionOptionSSL_VERIFYPEER, 0).asError()
}
else {
try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionCAINFO, caInfo).asError()
}
}
#endif
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you ask... :P
- These should probably be named
_CAInfoFilePath
andsetCAInfoFilePath
, as it's not the file but the file path (versus, say, the file URL). - Style nit: unsure why you're indenting this block between
#if
and#endif
but not any of the others you added. - Style nit: why have a blank line here and before the closing brace?
- Nit: if you're going to log instead of print from
printLibcurlDebug
, shouldn't it belogLibcurlDebug
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Roger, not much I can do about the last one. There is no stdout on Android. Can you check johnno1962b@d3805cb please and I’ll raise a new PR tomorrow. Thanks again! I updated the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on URLSession.setCAInfo(filePath: path)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to follow the naming conventions put forth for Foundation API;
- if you have a setter you should have a getter (unless it is absolutely justifiable that it can only be set)
- it should be a URL not a path
- don't use abbreviations like CA, instead it should be probably named
URLSession.defaultCertificateAuthorityFile
or something along those lines so that it does what it says on the tin. - it should probably validate that the url is a valid file path url, and perhaps it should be nullable?
- it should probably also be atomic (using some sort of lock to ensure thread safe access)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johnno1962 👍 , but for the full monty Philippe certainly lists some important considerations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
URLSession.sslCertificateAuthorityFile as a URL static var with validation enough? I’m not sure I can stretch to atomicity. I’m thinking I preferred the old API tho. I like URLSession .setCertificateAuthorityInfo(filePath: “UNSAFE_SSL_NOVERIFY”) from a health warning point of view.
#if os(Android)
extension URLSession {
public static func setCertificateAuthorityInfo(filePath: String) {
free(_EasyHandle._CAInfoFilePath)
filePath.withCString {
_EasyHandle._CAInfoFilePath = strdup($0)
}
}
public static var sslCertificateAuthorityFile: URL? {
set(value) {
if value == nil {
free(_EasyHandle._CAInfoFilePath)
_EasyHandle._CAInfoFilePath = strdup("UNSAFE_SSL_NOVERIFY")
return
}
guard value?.isFileURL == true else { return }
value!.path.withCString {
free(_EasyHandle._CAInfoFilePath)
_EasyHandle._CAInfoFilePath = strdup($0)
}
}
get {
return URL(fileURLWithPath: String(cString: _EasyHandle._CAInfoFilePath!))
}
}
}
#endif
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation should check that it's a file URL that actually exists. When that fails, the setter should do something other than return silently. There should be one API, not two, so sslCertificateAuthorityFile should be the only way to set the underlying stored property. And the underlying property should probably be renamed to match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, a version with no more dummy values left over from being an env var..
#if os(Android)
extension URLSession {
/// See https://curl.haxx.se/docs/sslcerts.html
/// For SSL to work you need a "cacert.pem" to be accessible at the
/// by setting URLSession.sslCertificateAuthorityFile to a file URL.
/// Downloadable here: https://curl.haxx.se/docs/caextract.html
/// Alternatively you can bypass SSL peer verification altogether:
/// URLSession.unsafeBypassSSLPeerVerify = true (not advised)
fileprivate static var _sslCertificateAuthorityFile: UnsafeMutablePointer<Int8>?
public static var unsafeBypassSSLPeerVerify = false
public static var sslCertificateAuthorityFile: URL {
set(value) {
unsafeBypassSSLPeerVerify = false
free(_sslCertificateAuthorityFile)
guard value.isFileURL && FileManager.default.fileExists(atPath: value.path) else {
NSLog("*** sslCertificateAuthorityFile not FileURL or does not exist ***")
_sslCertificateAuthorityFile = nil
return
}
value.path.withCString {
_sslCertificateAuthorityFile = strdup($0)
}
}
get {
if let value = _sslCertificateAuthorityFile {
return URL(fileURLWithPath: String(cString: value))
}
else {
return URL(fileURLWithPath: "NOVALUE")
}
}
}
}
#endif
@@ -168,6 +171,20 @@ extension _EasyHandle { | |||
let protocols = (CFURLSessionProtocolHTTP | CFURLSessionProtocolHTTPS) | |||
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionPROTOCOLS, protocols).asError() | |||
try! CFURLSession_easy_setopt_long(rawHandle, CFURLSessionOptionREDIR_PROTOCOLS, protocols).asError() | |||
#if os(Android) | |||
// See https://curl.haxx.se/docs/sslcerts.html | |||
// For SSL to work you need "cacert.pem" to be accessable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "accessible"
If there are no further comments I’m ready to raise a PR based on https://github.com/johnno1962b/swift-corelibs-foundation/commits/master including a few other minor patches for compilation on Android |
Hi, just wondering whether you would consider merging this change to allow https: fetches using NSURLSession to work on Android. It turns out there is a root certificate authorities file required by libcurl for SSL to work.
This PR gives the user the opportunity to provide one and specify it’s location in the environment variable “URLSessionCAInfo” for it to be configured correctly. As providing an absolute path to anything is spectacularly difficult in Android or if the user’s servers are self-signed it also gives the user the option to specify the value “UNSAFE_SSL_NOVERIFY” and SSL hostname verification will not be performed at all.
The advantage of an environment variable over increasing the API surface is that the value can come all the way from the Java side of the application where it is most likely to know the path.
How does this sound?