13

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

luk2302
  • 55,258
  • 23
  • 97
  • 137
Andrew Phillips
  • 937
  • 1
  • 7
  • 19

2 Answers2

24

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!

Andrew Phillips
  • 937
  • 1
  • 7
  • 19
  • I have implemented the same way but while execting with my api, its not triggering sessionDidReceiveChallenge method of sessionManager.delegate ...Please help – samridhgupta Jan 05 '18 at 10:32
  • sam, guessing you either didn't set the delegate, or you're setting it on an object that is no longer used/shadowed by another – Stephen J Mar 05 '18 at 15:19
  • This may be a dumb question, but where is the p12 actually being imported? Is "cert" the path to the file? @StephenJ – SuperHanz98 Jul 26 '19 at 13:10
  • @Cutter that's a choice, and up to you. The general idea is, you want to get it into your app however you want, you'll see I used a convoluted method in my answer history... but when you have it, you store it in the keychain. Then access it via keychain. Apple docs also dont tell you how to store certs in the keychain, so you'll have to infer how to. But I'll give you a shortcut for when you do that, just remember the keychain only stores certain types, cert being the only undocumented – Stephen J Jul 26 '19 at 14:58
  • To more directly answer your "cert" question, I don't know. That's part of Andrew's library he said he doesn't have. It looks like he dragged the p12 into the project and accessed it that way. I recommend keychain because bundle isn't encrypted – Stephen J Jul 26 '19 at 15:00
  • 1
    @StephenJ For the time being I'm just adding it to the bundle to get it working. I'm getting 'unexpectedly found nil unwrapping an optional value' when it gets to using the convenience init. Any idea why that might be? – SuperHanz98 Jul 29 '19 at 10:17
  • My bad... I had an extra char in my string. This works perfectly now. Thanks OP & Andrew & Stephen – SuperHanz98 Jul 29 '19 at 10:47
  • Is there any way I could convert this to use PEM and Key files instead of P12s? – SuperHanz98 Aug 12 '19 at 12:01
  • can somehow implement the same above functionality using alamofire 5.0 please thank – Ravi Apr 30 '20 at 10:02
  • Hi @Andrew Phillips Would you help to share the code for client certificate(.p12) to work for mutual authentication using Swift 4.2 and Alamofire 5 – Chathura Palihakkara Aug 04 '21 at 07:20
  • There is an issue, instead of `var certChain:[SecTrust]?`, it's `var certChain:[SecCertificate]?`. Also, you don't need ATS exception unless your server requires them. – kean Aug 31 '21 at 18:32
6

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
            }
        }        
}
DoubleK
  • 542
  • 3
  • 16
  • 2
    This looks exactly like _not_ what I was looking for since it does not provide mutual authentication if I am not mistaken, it just validates a server based on a bundled certificate. That sounds like certificate pinning. – luk2302 Nov 07 '16 at 16:09
  • 2
    nervermind, you are not the first one to confuse those two. My first two hours of googling only yielded result similiar to your code ;) – luk2302 Nov 07 '16 at 16:14