2

I've got a private key in addition to a public key to pin certificate.

How would authenticate with the to the server?

$ file *
foo.der:         data
foo.private.der: data

derived with openssl from

foo.key: PEM RSA private key
foo.pem: PEM certificate

what i need is an alamofire equivalent of this:

curl --key foo.key --cert foo.pem --location --request GET 'https://somhostofmine/v1/welcome/'

$ curl --key ./client_key.pem --cert ./client.pem --location --request GET 'https://someurl' "Hello wold!"

works

$ curl --cert ./client.pem --location --request GET 'https://someurl/v1/welcome/' curl: (58) unable to set private key file: './client.pem' type PEM

does not

So suggestions to use certificate itself without private key do not work.

Are we dealing with certificate pinning here or given the presence of the private key this is something else going here???

Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66
  • Is your private key like this generate with SHA? 4PhpWPCTGkqmmjRFussirzvNSi2LjL7WWhUSAVFIXDc= – Anis Mansuri Sep 14 '20 at 07:37
  • It's a pem. Which I can convert into cer or der I suppose to get ios securite a chance to load it, – Anton Tropashko Sep 14 '20 at 07:44
  • @AntonTropashko you are interested on how to do the certificate pinning from the iOS side? Are you using Alamofire? – gcharita Sep 14 '20 at 08:14
  • Yes, I'm using alamofire5. I probably need to pin cerificate but do not understand what the PRIVATE key is provided for. – Anton Tropashko Sep 14 '20 at 08:24
  • @AntonTropashko the private key is only for the server. In your client you only need the certificate. (public key) – gcharita Sep 14 '20 at 08:37
  • With almofire you can compare public key or certificate it self. Alamofire provides default methods to initialise session. – Anis Mansuri Sep 14 '20 at 08:43
  • No, the private key is needed for request to work. See the question amended with working curl with a private key and the defunct one without private key. – Anton Tropashko Sep 14 '20 at 09:00
  • @AntonTropashko as far as I can tell you cannot pin the servers private key. (see [What Is Pinning?](https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning) section) – gcharita Sep 14 '20 at 09:07
  • Quite possibly.That's why I refrased the question and included the curl that needs to be matched with alamofire machinery. – Anton Tropashko Sep 14 '20 at 09:27

2 Answers2

3

You can create a ServerTrustManager passing as parameter your evaluators per host name. For public key pinning use PublicKeysTrustEvaluator like this:

let evaluators: [String : ServerTrustEvaluating] = [
    "your.host.com": PublicKeysTrustEvaluator(performDefaultValidation: false, validateHost: false)
]
let serverTrustManager = ServerTrustManager(evaluators: evaluators)

For certificate pinning use PinnedCertificatesTrustEvaluator like this:

let evaluators: [String : ServerTrustEvaluating] = [
    "your.host.com": PinnedCertificatesTrustEvaluator(
        acceptSelfSignedCertificates: true,
        performDefaultValidation: false,
        validateHost: false
    )
]
let serverTrustManager = ServerTrustManager(evaluators: evaluators)

Both methods require your certificate to be included in your bundle as .cer or .der file.

After creating your ServerTrustManager pass it to a Session instance and use this for your requests:

let session = Session(serverTrustManager: serverTrustManager)

If you want to have a more complex logic on how you verify your server or you have to use wildcards in your domain you have to subclass ServerTrustManager and override serverTrustEvaluator(forHost:) function:

class MyServerTrustManager: ServerTrustManager {
    init() {
        super.init(evaluators: [:])
    }
    
    override func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
        guard host.hasSuffix(".host.com") else {
            return try super.serverTrustEvaluator(forHost: host)
        }
        return PublicKeysTrustEvaluator(performDefaultValidation: false, validateHost: false)
    }
}
gcharita
  • 7,729
  • 3
  • 20
  • 37
  • why is validateHost: false ? – Anton Tropashko Sep 14 '20 at 08:37
  • `validateHost` as the documentation states `Determines whether or not the evaluator should validate the host, in addition to performing the default evaluation, even if performDefaultValidation is false. true by default.` In general you need to mess with these parameters only when you have to do with a self signed ssl certificate. – gcharita Sep 14 '20 at 08:42
  • It's best to keep the default values for these as long as you can. – gcharita Sep 14 '20 at 08:46
  • But where do I plug the public and private key into? – Anton Tropashko Sep 14 '20 at 08:47
  • If you jump into `PublicKeysTrustEvaluator` and `PinnedCertificatesTrustEvaluator` initializers you will see that both have default values for the their first parameters. Those are conveniently placed implementations that retrieve any certificate you included in you main bundle. – gcharita Sep 14 '20 at 08:51
  • @AntonTropashko have you tried this to see if it works? – gcharita Sep 14 '20 at 09:06
  • Yes. It does not: Server trust evaluation failed due to reason: Certificate pinning failed for host suchandsuch – Anton Tropashko Sep 14 '20 at 09:24
  • This is not surprising: see the updated question what happens when curl does not have key parameter fed into it, just the certificate. – Anton Tropashko Sep 14 '20 at 09:25
  • @AntonTropashko I am sorry that I insist but I used this kind of pinning a million times and works as expected. Could you please try to call `Bundle.main.af.certificates` directly and see if returns any certificate. If an empty array is returned your certificate is not in the correct format or it is not included in your target. – gcharita Sep 14 '20 at 09:29
  • Yes. Of course it returns the certificate. But as I said the curl works ONLY if you feed it both the certificate and the private key. Bottom line is curl works and it can be done. With alamofire or without. Yes, I used ceritificate and public keys pinning without private key in older versions of alamofire and it works as expected. But this is something else. – Anton Tropashko Sep 14 '20 at 09:33
  • (lldb) p certificates.count (Int) $R0 = 1 – Anton Tropashko Sep 14 '20 at 09:40
  • I owe you an upvote nevertheless: at least I tried porting my pinning code to alamofire5. That it did not work is another matter worth 300 reputation. – Anton Tropashko Sep 14 '20 at 09:44
  • @AntonTropashko thank you. What is the error that Alamofire is returning to you? The error code should be helpful. – gcharita Sep 14 '20 at 09:57
  • Alamofire.AFError.serverTrustEvaluationFailed( – Anton Tropashko Sep 14 '20 at 10:04
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/221444/discussion-between-anton-tropashko-and-gcharita). – Anton Tropashko Sep 14 '20 at 10:41
2
  1. packed private and public key into p12 container with openssl
  2. this was key Alamofire without evaluation and with sending client certificate

created PKCS512() instance and created URLCredential using that (code in the link above) in alamofire all you need from the link above is the PKCS12 class and URLCredential extension

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

    public init(PKCS12Data:NSData,password:String)
    {
        let importPasswordOption:NSDictionary = [kSecImportExportPassphrase as NSString:password]
        var items : CFArray?
        let secError:OSStatus = SecPKCS12Import(PKCS12Data, importPasswordOption, &items)

        guard secError == errSecSuccess else {
            if secError == errSecAuthFailed {
                NSLog("ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?")
            }
            fatalError("SecPKCS12Import returned an error trying to import PKCS12 data")
        }

        guard let theItemsCFArray = items else { fatalError()  }
        let theItemsNSArray:NSArray = theItemsCFArray as NSArray
        guard let dictArray = theItemsNSArray as? [[String:AnyObject]] else { fatalError() }

        func f<T>(key:CFString) -> T? {
            for d in dictArray {
                if let v = d[key as String] as? T {
                    return v
                }
            }
            return nil
        }

        self.label = f(key: kSecImportItemLabel)
        self.keyID = f(key: kSecImportItemKeyID)
        self.trust = f(key: kSecImportItemTrust)
        self.certChain = f(key: kSecImportItemCertChain)
        self.identity =  f(key: kSecImportItemIdentity)
    }
}

extension URLCredential {
    public convenience init?(PKCS12 thePKCS12:PKCS12) {
        if let identity = thePKCS12.identity {
            self.init(
                identity: identity,
                certificates: thePKCS12.certChain,
                persistence: URLCredential.Persistence.forSession)
        }
        else { return nil }
    }
}
  1. fed that credential to authenticate(with: urlCredential) per Missing sessionDidReceiveChallenge in Alamofire 5 delegate

and that was it.

Hardcoding password into p12 constructor was super ugly. Eww What's key I guess is base64 baking in p12 into code rather than having it as a file prone to ipa patcher attach.

Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66
  • I was also digging into this, after my answer. So this is not pinning the servers certificate, it's about authenticating client to the server, using a key pair. I didn't have the chance to use this kind of authentication in the past. – gcharita Sep 14 '20 at 12:24
  • 2
    Well, here's you benefit in all this then. Something new. – Anton Tropashko Sep 14 '20 at 12:30