Andrew Phillips
Andrew Phillips

Reputation: 987

Getting client certificate to work for mutual authentication using Swift 3 and Alamofire 4

I am trying to figure out how to use Alamofire 4.0 with Swift 3.0 to send a p12 (i also have the PEM cert and key if need be) to a website for authentication. All the examples i have seen are for Swift 2.0 and not exactly what i'm looking for. In safari on my mac i can access the site by putting the p12 in the keychain and sending it when safari asks so i know that portion works. I don't know if anyone can help me with an example of how to do so in Alamofire 4.0 and Swift 3.0 in an application. The certificates are self signed as well.

Any thoughts or help? I am not just looking to pin the certificate as the client key and cert needs to be sent to the server for access...

Upvotes: 13

Views: 11589

Answers (2)

DoubleK
DoubleK

Reputation: 562

Here is my example that might help someone (Alamofire 4.0, Swift 3, xCode 8)

import Alamofire

class NetworkConnection {
    let developmentDomain = Config.developmentDomain // "api.myappdev.com"
    let productionDomain = Config.productionDomain // "api.myappprod.com"
    let certificateFilename = Config.certificateFilename // "godaddy"
    let certificateExtension = Config.certificateExtension // "der"
    let useSSL = true
    var manager: SessionManager!
    var serverTrustPolicies: [String : ServerTrustPolicy] = [String:ServerTrustPolicy]()
    static let sharedManager = NetworkConnection()


    init(){
        if useSSL {
            manager = initSafeManager()
        } else {
            manager = initUnsafeManager()
        }
    }

    //USED FOR SITES WITH CERTIFICATE, OTHERWISE .DisableEvaluation
    func initSafeManager() -> SessionManager {
        setServerTrustPolicies()

        manager = SessionManager(configuration: URLSessionConfiguration.default, delegate: SessionDelegate(), serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies))

        return manager
    }

    //USED FOR SITES WITHOUT CERTIFICATE, DOESN'T CHECK FOR CERTIFICATE
    func initUnsafeManager() -> SessionManager {
        manager = Alamofire.SessionManager.default

        manager.delegate.sessionDidReceiveChallenge = { session, challenge in
            var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
            var credential: URLCredential?

            if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
                disposition = URLSession.AuthChallengeDisposition.useCredential
                credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)     //URLCredential(forTrust: challenge.protectionSpace.serverTrust!)
            } else {
                if challenge.previousFailureCount > 0 {
                    disposition = .cancelAuthenticationChallenge
                } else {
                    credential = self.manager.session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)

                    if credential != nil {
                        disposition = .useCredential
                    }
                }
            }

            return (disposition, credential)
        }

        return manager
    }

    func setServerTrustPolicies() {
        let pathToCert = Bundle.main.path(forResource: certificateFilename, ofType: certificateExtension)
        let localCertificate:Data = try! Data(contentsOf: URL(fileURLWithPath: pathToCert!))

        let serverTrustPolicies: [String: ServerTrustPolicy] = [
            productionDomain: .pinCertificates(
                certificates: [SecCertificateCreateWithData(nil, localCertificate as CFData)!],
                validateCertificateChain: true,
                validateHost: true
            ),
            developmentDomain: .disableEvaluation
        ]

        self.serverTrustPolicies = serverTrustPolicies
    }

    static func addAuthorizationHeader (_ token: String, tokenType: String) -> [String : String] {
        let headers = [
            "Authorization": tokenType + " " + token
        ]

        return headers
    }

}

add following to your Info.plist

    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
        <key>NSExceptionDomains</key>
        <dict>
            <key>api.myappdev.com</key>
            <dict>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSIncludesSubdomains</key>
                <true/>
                <key>NSRequiresCertificateTransparency</key>
                <false/>
                <key>NSTemporaryExceptionMinimumTLSVersion</key>
                <string>TLSv1.2</string>
            </dict>
        </dict>
    </dict>

and here is an example of making an request

import Alamofire    

class ActionUserUpdate {
        let url = "https://api.myappdev.com/v1/"
        let manager = NetworkConnection.sharedManager.manager

        func updateUser(_ token: String, tokenType: String, expiresIn: Int, params: [String : String]) {
            let headers = NetworkConnection.addAuthorizationHeader(token, tokenType: tokenType)
            manager?.request(url, method: .put, parameters: params, encoding: JSONEncoding.default, headers: headers).responseJSON { response in
                print(response.description)
                print(response.debugDescription)
                print(response.request)  // original URL request
                print(response.response) // URL response
                print(response.data)     // server data
                print(response.result)   // result of response serialization
            }
        }        
}

Upvotes: 6

Andrew Phillips
Andrew Phillips

Reputation: 987

I was able to get it to work. A few issues got into the way. First, you have to allow IOS to accept self signed certificates. This requires to set up AlamoFire serverTrustPolicy:

let serverTrustPolicies: [String: ServerTrustPolicy] = [
        "your-domain.com": .disableEvaluation
    ]

self.sessionManager = Alamofire.SessionManager(
        serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
    )

From there, you have to override the sessionDidRecieveChallenge to send the client certificate. Because i wanted to use a p12 file I modified some code I found elsewhere (sorry i don't have the source anymore) to make is Swift 3.0 to import the p12 using foundation classes:

import Foundation

public class PKCS12  {
    var label:String?
    var keyID:Data?
    var trust:SecTrust?
    var certChain:[SecTrust]?
    var identity:SecIdentity?

    let securityError:OSStatus

    public init(data:Data, password:String) {

        //self.securityError = errSecSuccess

        var items:CFArray?
        let certOptions:NSDictionary = [kSecImportExportPassphrase as NSString:password as NSString]

        // import certificate to read its entries
        self.securityError = SecPKCS12Import(data as NSData, certOptions, &items);

        if securityError == errSecSuccess {
            let certItems:Array = (items! as Array)
            let dict:Dictionary<String, AnyObject> = certItems.first! as! Dictionary<String, AnyObject>;

            self.label = dict[kSecImportItemLabel as String] as? String;
            self.keyID = dict[kSecImportItemKeyID as String] as? Data;
            self.trust = dict[kSecImportItemTrust as String] as! SecTrust?;
            self.certChain = dict[kSecImportItemCertChain as String] as? Array<SecTrust>;
            self.identity = dict[kSecImportItemIdentity as String] as! SecIdentity?;
        }


    }

    public convenience init(mainBundleResource:String, resourceType:String, password:String) {
        self.init(data: NSData(contentsOfFile: Bundle.main.path(forResource: mainBundleResource, ofType:resourceType)!)! as Data, password: password);
    }

    public func urlCredential()  -> URLCredential  {
        return URLCredential(
            identity: self.identity!,
            certificates: self.certChain!,
            persistence: URLCredential.Persistence.forSession);

    }



}

This will allow me to import the file, and send it back to the client.

let cert = PKCS12.init(mainBundleResource: "cert", resourceType: "p12", password: "password");

self.sessionManager.delegate.sessionDidReceiveChallenge = { session, challenge in
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
            return (URLSession.AuthChallengeDisposition.useCredential, self.cert.urlCredential());
        }
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            return (URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!));
        }
        return (URLSession.AuthChallengeDisposition.performDefaultHandling, Optional.none);
    }

Now you can use the sessionManager to create as many calls as you need to.

As a note, i've also added the following to the info.plist as recomended to get around the new security features in newer iOS features:

<key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
        <key>NSExceptionDomains</key>
        <dict>
            <key>your-domain.com</key>
            <dict>
                <key>NSIncludesSubdomains</key>
                <true/>
                <key>NSExceptionRequiresForwardSecrecy</key>
                <false/>
                <key>NSExceptionAllowsInsecureHTTPLoads</key>
                <true/>
            </dict>
        </dict>
    </dict>

I hope this helps!

Upvotes: 24

Related Questions