From 23a6f192dd6e962194c88c38f85134f5eee9b6a1 Mon Sep 17 00:00:00 2001 From: Olivier Martin Date: Mon, 22 Apr 2024 21:37:13 +0000 Subject: [PATCH 1/4] CoreFoundation/URL.subproj: Add support to set CURL option from blob --- CoreFoundation/URL.subproj/CFURLSessionInterface.c | 8 ++++++++ CoreFoundation/URL.subproj/CFURLSessionInterface.h | 1 + 2 files changed, 9 insertions(+) diff --git a/CoreFoundation/URL.subproj/CFURLSessionInterface.c b/CoreFoundation/URL.subproj/CFURLSessionInterface.c index 1e6ec24a05..e53f2406a6 100644 --- a/CoreFoundation/URL.subproj/CFURLSessionInterface.c +++ b/CoreFoundation/URL.subproj/CFURLSessionInterface.c @@ -111,6 +111,14 @@ CFURLSessionEasyCode CFURLSession_easy_setopt_tc(CFURLSessionEasyHandle _Nonnull return MakeEasyCode(curl_easy_setopt(curl, option.value, a)); } +CFURLSessionEasyCode CFURLSession_easy_setopt_blob(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, void *_Nonnull data, size_t len) { + struct curl_blob stblob; + stblob.data = data; + stblob.len = len; + stblob.flags = CURL_BLOB_COPY; + return MakeEasyCode(curl_easy_setopt(curl, option.value, &stblob)); +} + CFURLSessionEasyCode CFURLSession_easy_getinfo_long(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionInfo info, long *_Nonnull a) { return MakeEasyCode(curl_easy_getinfo(curl, info.value, a)); } diff --git a/CoreFoundation/URL.subproj/CFURLSessionInterface.h b/CoreFoundation/URL.subproj/CFURLSessionInterface.h index 9856949a7f..3b711e34ef 100644 --- a/CoreFoundation/URL.subproj/CFURLSessionInterface.h +++ b/CoreFoundation/URL.subproj/CFURLSessionInterface.h @@ -626,6 +626,7 @@ typedef int (CFURLSessionSeekCallback)(void *_Nullable userp, long long offset, CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_seek(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, CFURLSessionSeekCallback * _Nullable a); typedef int (CFURLSessionTransferInfoCallback)(void *_Nullable userp, long long dltotal, long long dlnow, long long ultotal, long long ulnow); CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_tc(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, CFURLSessionTransferInfoCallback * _Nullable a); +CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_setopt_blob(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionOption option, void *_Nonnull data, size_t len); CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_getinfo_long(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionInfo info, long *_Nonnull a); CF_EXPORT CFURLSessionEasyCode CFURLSession_easy_getinfo_double(CFURLSessionEasyHandle _Nonnull curl, CFURLSessionInfo info, double *_Nonnull a); From 4845b8bbf84b42e86ab31497d3a6ab13fae1bcf1 Mon Sep 17 00:00:00 2001 From: Olivier Martin Date: Mon, 22 Apr 2024 21:38:32 +0000 Subject: [PATCH 2/4] CoreFoundation/URL.subproj: Added definitions for SSL key and certificate blob --- CoreFoundation/CMakeLists.txt | 4 ++++ CoreFoundation/URL.subproj/CFURLSessionInterface.c | 6 ++++++ CoreFoundation/URL.subproj/CFURLSessionInterface.h | 2 ++ Sources/FoundationNetworking/CMakeLists.txt | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/CoreFoundation/CMakeLists.txt b/CoreFoundation/CMakeLists.txt index f126c2983a..90835d02db 100644 --- a/CoreFoundation/CMakeLists.txt +++ b/CoreFoundation/CMakeLists.txt @@ -442,6 +442,10 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin) add_compile_definitions($<$:NS_CURL_MISSING_MAX_HOST_CONNECTIONS>) endif() + if((NS_CURL_ASSUME_FEATURES_MISSING) OR (CURL_VERSION_STRING VERSION_LESS "7.71.0")) + add_compile_definitions($<$:NS_CURL_MISSING_CURLINFO_SSLCERT_BLOB> $<$:NS_CURL_MISSING_CURLINFO_SSLKEY_BLOB>) + endif() + if((NS_CURL_ASSUME_FEATURES_MISSING) OR (CURL_VERSION_STRING VERSION_LESS "7.84.0")) add_compile_definitions($<$:NS_CURL_MISSING_CURLINFO_CAINFO>) endif() diff --git a/CoreFoundation/URL.subproj/CFURLSessionInterface.c b/CoreFoundation/URL.subproj/CFURLSessionInterface.c index e53f2406a6..453d069147 100644 --- a/CoreFoundation/URL.subproj/CFURLSessionInterface.c +++ b/CoreFoundation/URL.subproj/CFURLSessionInterface.c @@ -374,6 +374,9 @@ CFURLSessionOption const CFURLSessionOptionCOOKIE = { CURLOPT_COOKIE }; CFURLSessionOption const CFURLSessionOptionHTTPHEADER = { CURLOPT_HTTPHEADER }; CFURLSessionOption const CFURLSessionOptionHTTPPOST = { CURLOPT_HTTPPOST }; CFURLSessionOption const CFURLSessionOptionSSLCERT = { CURLOPT_SSLCERT }; +#if !NS_CURL_MISSING_CURLINFO_SSLCERT_BLOB +CFURLSessionOption const CFURLSessionOptionSSLCERT_BLOB = { CURLOPT_SSLCERT_BLOB }; +#endif CFURLSessionOption const CFURLSessionOptionKEYPASSWD = { CURLOPT_KEYPASSWD }; CFURLSessionOption const CFURLSessionOptionCRLF = { CURLOPT_CRLF }; CFURLSessionOption const CFURLSessionOptionQUOTE = { CURLOPT_QUOTE }; @@ -427,6 +430,9 @@ CFURLSessionOption const CFURLSessionOptionHTTP_VERSION = { CURLOPT_HTTP_VERSION CFURLSessionOption const CFURLSessionOptionFTP_USE_EPSV = { CURLOPT_FTP_USE_EPSV }; CFURLSessionOption const CFURLSessionOptionSSLCERTTYPE = { CURLOPT_SSLCERTTYPE }; CFURLSessionOption const CFURLSessionOptionSSLKEY = { CURLOPT_SSLKEY }; +#if !NS_CURL_MISSING_CURLINFO_SSLKEY_BLOB +CFURLSessionOption const CFURLSessionOptionSSLKEY_BLOB = { CURLOPT_SSLKEY_BLOB }; +#endif CFURLSessionOption const CFURLSessionOptionSSLKEYTYPE = { CURLOPT_SSLKEYTYPE }; CFURLSessionOption const CFURLSessionOptionSSLENGINE = { CURLOPT_SSLENGINE }; CFURLSessionOption const CFURLSessionOptionSSLENGINE_DEFAULT = { CURLOPT_SSLENGINE_DEFAULT }; diff --git a/CoreFoundation/URL.subproj/CFURLSessionInterface.h b/CoreFoundation/URL.subproj/CFURLSessionInterface.h index 3b711e34ef..5f7894e855 100644 --- a/CoreFoundation/URL.subproj/CFURLSessionInterface.h +++ b/CoreFoundation/URL.subproj/CFURLSessionInterface.h @@ -199,6 +199,7 @@ CF_EXPORT CFURLSessionOption const CFURLSessionOptionCOOKIE; // CURLOPT_COOKIE CF_EXPORT CFURLSessionOption const CFURLSessionOptionHTTPHEADER; // CURLOPT_HTTPHEADER CF_EXPORT CFURLSessionOption const CFURLSessionOptionHTTPPOST; // CURLOPT_HTTPPOST CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLCERT; // CURLOPT_SSLCERT +CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLCERT_BLOB; // CURLOPT_SSLCERT_BLOB CF_EXPORT CFURLSessionOption const CFURLSessionOptionKEYPASSWD; // CURLOPT_KEYPASSWD CF_EXPORT CFURLSessionOption const CFURLSessionOptionCRLF; // CURLOPT_CRLF CF_EXPORT CFURLSessionOption const CFURLSessionOptionQUOTE; // CURLOPT_QUOTE @@ -253,6 +254,7 @@ CF_EXPORT CFURLSessionOption const CFURLSessionOptionHTTP_VERSION; // CURLOPT_HT CF_EXPORT CFURLSessionOption const CFURLSessionOptionFTP_USE_EPSV; // CURLOPT_FTP_USE_EPSV CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLCERTTYPE; // CURLOPT_SSLCERTTYPE CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLKEY; // CURLOPT_SSLKEY +CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLKEY_BLOB; // CURLOPT_SSLKEY_BLOB CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLKEYTYPE; // CURLOPT_SSLKEYTYPE CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLENGINE; // CURLOPT_SSLENGINE CF_EXPORT CFURLSessionOption const CFURLSessionOptionSSLENGINE_DEFAULT; // CURLOPT_SSLENGINE_DEFAULT diff --git a/Sources/FoundationNetworking/CMakeLists.txt b/Sources/FoundationNetworking/CMakeLists.txt index ebb8dc405b..8ede2195fc 100644 --- a/Sources/FoundationNetworking/CMakeLists.txt +++ b/Sources/FoundationNetworking/CMakeLists.txt @@ -16,6 +16,10 @@ if(NOT CMAKE_SYSTEM_NAME STREQUAL Darwin) add_compile_definitions(NS_CURL_MISSING_MAX_HOST_CONNECTIONS) endif() + if((NS_CURL_ASSUME_FEATURES_MISSING) OR (CURL_VERSION_STRING VERSION_LESS "7.71.0")) + add_compile_definitions(NS_CURL_MISSING_CURLINFO_SSLCERT_BLOB NS_CURL_MISSING_CURLINFO_SSLKEY_BLOB) + endif() + if((NS_CURL_ASSUME_FEATURES_MISSING) OR (CURL_VERSION_STRING VERSION_LESS "7.84.0")) add_compile_definitions(NS_CURL_MISSING_CURLINFO_CAINFO) endif() From 3efad23b52078c97b2a32bb4a3404232cacd68db Mon Sep 17 00:00:00 2001 From: Olivier Martin Date: Mon, 22 Apr 2024 21:40:45 +0000 Subject: [PATCH 3/4] FoundationNetworking/URLCredential: Enable client key and certificate --- .../FoundationNetworking/URLCredential.swift | 77 +++++++++++++++---- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/Sources/FoundationNetworking/URLCredential.swift b/Sources/FoundationNetworking/URLCredential.swift index bdb844ba34..edef40b780 100644 --- a/Sources/FoundationNetworking/URLCredential.swift +++ b/Sources/FoundationNetworking/URLCredential.swift @@ -40,8 +40,12 @@ extension URLCredential { @discussion This class is an immutable object representing an authentication credential. The actual type of the credential is determined by the constructor called in the categories declared below. */ open class URLCredential : NSObject, NSSecureCoding, NSCopying { - private var _user : String - private var _password : String + private var _user : String? + private var _password : String? + // _privateClientKey contains the private client key in DER format + private var _privateClientKey: Data? + // _privateClientCertificate contains the private client certificate in DER format + private var _privateClientCertificate: Data? private var _persistence : Persistence /*! @@ -55,6 +59,25 @@ open class URLCredential : NSObject, NSSecureCoding, NSCopying { public init(user: String, password: String, persistence: Persistence) { _user = user _password = password + _privateClientKey = nil + _privateClientCertificate = nil + _persistence = persistence + super.init() + } + + /*! + @method initWithUser:password:persistence: + @abstract Initialize a URLCredential with a user and password + @param user the username + @param password the password + @param persistence enum that says to store per session, permanently or not at all + @result The initialized URLCredential + */ + public init(clientKey: Data, clientCertificate: Data, persistence: Persistence) { + _user = nil + _password = nil + _privateClientKey = clientKey + _privateClientCertificate = clientCertificate _persistence = persistence super.init() } @@ -76,24 +99,34 @@ open class URLCredential : NSObject, NSSecureCoding, NSCopying { func bridgeString(_ value: NSString) -> String? { return String._unconditionallyBridgeFromObjectiveC(value) } - - let encodedUser = aDecoder.decodeObject(forKey: "NS._user") as! NSString - self._user = bridgeString(encodedUser)! - - let encodedPassword = aDecoder.decodeObject(forKey: "NS._password") as! NSString - self._password = bridgeString(encodedPassword)! - - let encodedPersistence = aDecoder.decodeObject(forKey: "NS._persistence") as! NSNumber - self._persistence = Persistence(rawValue: encodedPersistence.uintValue)! + + if let encodedUser = aDecoder.decodeObject(forKey: "NS._user") as? NSString { + self._user = bridgeString(encodedUser)! + } + + if let encodedPassword = aDecoder.decodeObject(forKey: "NS._password") as? NSString { + self._password = bridgeString(encodedPassword)! + } + + if let encodedPersistence = aDecoder.decodeObject(forKey: "NS._persistence") as? NSNumber { + self._persistence = Persistence(rawValue: encodedPersistence.uintValue)! + } else { + self._persistence = Persistence.none + } } open func encode(with aCoder: NSCoder) { guard aCoder.allowsKeyedCoding else { preconditionFailure("Unkeyed coding is unsupported.") } - - aCoder.encode(self._user._bridgeToObjectiveC(), forKey: "NS._user") - aCoder.encode(self._password._bridgeToObjectiveC(), forKey: "NS._password") + + if let user = self._user { + aCoder.encode(user._bridgeToObjectiveC(), forKey: "NS._user") + } + if let password = self._password { + aCoder.encode(password._bridgeToObjectiveC(), forKey: "NS._password") + } + aCoder.encode(self._persistence.rawValue._bridgeToObjectiveC(), forKey: "NS._persistence") } @@ -141,6 +174,20 @@ open class URLCredential : NSObject, NSSecureCoding, NSCopying { */ open var password: String? { return _password } + /*! + @method privateClientKey + @abstract Get the private client key + @result The private key binary blob + */ + open var privateClientKey: Data? { return _privateClientKey } + + /*! + @method privateClientCertificate + @abstract Get the private client key + @result The private key binary blob + */ + open var privateClientCertificate: Data? { return _privateClientCertificate } + /*! @method hasPassword @abstract Find out if this credential has a password, without trying to get it @@ -152,6 +199,6 @@ open class URLCredential : NSObject, NSSecureCoding, NSCopying { */ open var hasPassword: Bool { // Currently no support for SecTrust/SecIdentity, always return true - return true + return _password != nil } } From 714171017e302cb6f6d0c3757460664419e8b39a Mon Sep 17 00:00:00 2001 From: Olivier Martin Date: Mon, 22 Apr 2024 21:52:44 +0000 Subject: [PATCH 4/4] FoundationNetworking/URLSessionConfiguration: Add URLCredential --- .../URLSession/Configuration.swift | 4 ++ .../URLSession/HTTP/HTTPURLProtocol.swift | 9 +++- .../URLSession/URLSessionConfiguration.swift | 14 ++++-- .../URLSession/libcurl/EasyHandle.swift | 49 ++++++++++++++++++- 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationNetworking/URLSession/Configuration.swift b/Sources/FoundationNetworking/URLSession/Configuration.swift index 7b3996e2e8..5260cbd147 100644 --- a/Sources/FoundationNetworking/URLSession/Configuration.swift +++ b/Sources/FoundationNetworking/URLSession/Configuration.swift @@ -78,6 +78,9 @@ internal extension URLSession { let shouldUseExtendedBackgroundIdleMode: Bool let protocolClasses: [AnyClass]? + + /// The credentials to use for connecting to servers + let clientCredential: URLCredential? } } internal extension URLSession._Configuration { @@ -100,6 +103,7 @@ internal extension URLSession._Configuration { urlCache = config.urlCache shouldUseExtendedBackgroundIdleMode = config.shouldUseExtendedBackgroundIdleMode protocolClasses = config.protocolClasses + clientCredential = config.clientCredential } } diff --git a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift index abf6623435..6dfc0a9ba5 100644 --- a/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift +++ b/Sources/FoundationNetworking/URLSession/HTTP/HTTPURLProtocol.swift @@ -328,7 +328,14 @@ internal class _HTTPURLProtocol: _NativeProtocol { } let session = task?.session as! URLSession let _config = session._configuration - easyHandle.set(sessionConfig: _config) + do { + try easyHandle.set(sessionConfig: _config) + } catch { + self.internalState = .transferFailed + let nsError = error as? NSError ?? NSError(domain: NSURLErrorDomain, code: NSURLErrorUserAuthenticationRequired) + failWith(error: nsError, request: request) + return + } easyHandle.setAllowedProtocolsToHTTPAndHTTPS() easyHandle.set(preferredReceiveBufferSize: Int.max) do { diff --git a/Sources/FoundationNetworking/URLSession/URLSessionConfiguration.swift b/Sources/FoundationNetworking/URLSession/URLSessionConfiguration.swift index 1282b70135..348847f1d3 100644 --- a/Sources/FoundationNetworking/URLSession/URLSessionConfiguration.swift +++ b/Sources/FoundationNetworking/URLSession/URLSessionConfiguration.swift @@ -75,9 +75,10 @@ open class URLSessionConfiguration : NSObject, NSCopying { urlCredentialStorage: .shared, urlCache: .shared, shouldUseExtendedBackgroundIdleMode: false, - protocolClasses: [_HTTPURLProtocol.self, _FTPURLProtocol.self, _WebSocketURLProtocol.self]) + protocolClasses: [_HTTPURLProtocol.self, _FTPURLProtocol.self, _WebSocketURLProtocol.self], + clientCredential: nil) } - + private init(identifier: String?, requestCachePolicy: URLRequest.CachePolicy, timeoutIntervalForRequest: TimeInterval, @@ -95,7 +96,8 @@ open class URLSessionConfiguration : NSObject, NSCopying { urlCredentialStorage: URLCredentialStorage?, urlCache: URLCache?, shouldUseExtendedBackgroundIdleMode: Bool, - protocolClasses: [AnyClass]?) + protocolClasses: [AnyClass]?, + clientCredential: URLCredential?) { self.identifier = identifier self.requestCachePolicy = requestCachePolicy @@ -115,6 +117,7 @@ open class URLSessionConfiguration : NSObject, NSCopying { self.urlCache = urlCache self.shouldUseExtendedBackgroundIdleMode = shouldUseExtendedBackgroundIdleMode self.protocolClasses = protocolClasses + self.clientCredential = clientCredential } open override func copy() -> Any { @@ -140,7 +143,8 @@ open class URLSessionConfiguration : NSObject, NSCopying { urlCredentialStorage: urlCredentialStorage, urlCache: urlCache, shouldUseExtendedBackgroundIdleMode: shouldUseExtendedBackgroundIdleMode, - protocolClasses: protocolClasses) + protocolClasses: protocolClasses, + clientCredential: clientCredential) } open class var `default`: URLSessionConfiguration { @@ -258,6 +262,8 @@ open class URLSessionConfiguration : NSObject, NSCopying { @available(*, unavailable, message: "Not available on non-Darwin platforms") open var multipathServiceType: URLSessionConfiguration.MultipathServiceType { NSUnsupported() } + /* Optional client credential to be used when connecting to servers */ + open var clientCredential: URLCredential? } @available(*, unavailable, message: "Not available on non-Darwin platforms") diff --git a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift index 45cd8cae7e..b805895b8c 100644 --- a/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift +++ b/Sources/FoundationNetworking/URLSession/libcurl/EasyHandle.swift @@ -178,8 +178,55 @@ extension _EasyHandle { } } - func set(sessionConfig config: URLSession._Configuration) { + func set(sessionConfig config: URLSession._Configuration) throws { _config = config + if let c = _config, let clientCredential = c.clientCredential { + // For TLS client certificate authentication + if var privateClientKey = clientCredential.privateClientKey, + var privateClientCertificate = clientCredential.privateClientCertificate { + // Key and certificate are expected to be in DER format + "DER".withCString { + let mutablePointer = UnsafeMutablePointer(mutating: $0) + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSSLKEYTYPE, mutablePointer).asError() + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionSSLCERTTYPE, mutablePointer).asError() + } + +#if !NS_CURL_MISSING_CURLINFO_SSLKEY_BLOB + privateClientKey.withUnsafeMutableBytes { + if let baseAddress = $0.baseAddress { + try! CFURLSession_easy_setopt_blob(rawHandle, CFURLSessionOptionSSLKEY_BLOB, + baseAddress, $0.count).asError() + } + } +#endif // !NS_CURL_MISSING_CURLINFO_SSLKEY_BLOB + +#if !NS_CURL_MISSING_CURLINFO_SSLCERT_BLOB + privateClientCertificate.withUnsafeMutableBytes { + if let baseAddress = $0.baseAddress { + try! CFURLSession_easy_setopt_blob(rawHandle, CFURLSessionOptionSSLCERT_BLOB, + baseAddress, $0.count).asError() + } + } +#endif // !NS_CURL_MISSING_CURLINFO_SSLCERT_BLOB + } else if let tlsAuthUsername = clientCredential.user, + let tlsAuthPassword = clientCredential.password { + "SRP".withCString { + let mutablePointer = UnsafeMutablePointer(mutating: $0) + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionTLSAUTH_TYPE, mutablePointer).asError() + } + tlsAuthUsername.withCString { + let mutablePointer = UnsafeMutablePointer(mutating: $0) + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionTLSAUTH_USERNAME, mutablePointer).asError() + } + tlsAuthPassword.withCString { + let mutablePointer = UnsafeMutablePointer(mutating: $0) + try! CFURLSession_easy_setopt_ptr(rawHandle, CFURLSessionOptionTLSAUTH_PASSWORD, mutablePointer).asError() + } + } else { + throw NSError(domain: NSURLErrorDomain, code: NSURLErrorUserAuthenticationRequired, + userInfo: [NSLocalizedDescriptionKey: "Client credentials from URLSessionConfiguration is incomplete."]) + } + } } /// Set the CA bundle path automatically if it isn't set