Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get a non-nil serverTrust value when creating a URLAuthenticationChallenge programmatically?

Tags:

ios

swift

iphone

I have public key pinning code in my Swift iOS application that works great.

I'm now working on building unit tests that will test the public key pinning code without the need for network connectivity / real live server. I've got this almost working, but I can't figure out how to create a URLAuthenticationChallenge programmatically that has a non-nil serverTrust? All the Apple documentation states that this should be non-nil if your authentication method is NSURLAuthenticationMethodServerTrust. I'm using the p12 and cer files generated from my local machine to build the URLCredential in the example below. No matter what I do, challenge.protectionSpace.serverTrust always comes back nil.

let protectionSpace = URLProtectionSpace(host: "mockSession",
                                                 port: 0,
                                                 protocol: "https",
                                                 realm: nil,
                      authenticationMethod: NSURLAuthenticationMethodServerTrust)

var urlCredential:URLCredential?
if let p12Data = try? Data(contentsOf: URL(fileURLWithPath: Bundle.init(for: type(of: self)).path(forResource: "cm7justindomnit", ofType: "p12") ?? "")),
    let cerData = try? Data(contentsOf: URL(fileURLWithPath: Bundle.init(for: type(of: self)).path(forResource: "cm7justindomnit", ofType: "cer") ?? "")){
    let options: NSDictionary = [kSecImportExportPassphrase:"password"]
    var items: CFArray?
    let _ = SecPKCS12Import(p12Data as CFData, options, &items)
    if let items = items {
        let objectsData = Data.init(from: CFArrayGetValueAtIndex(items, 0))
        let objects = objectsData.toArray(type: CFDictionary.self).first
        let secIdentityData = Data.init(from: CFDictionaryGetValue(objects, Unmanaged.passUnretained(kSecImportItemIdentity).toOpaque()))
        if let secIdentity = secIdentityData.toArray(type: SecIdentity.self).first {
            if let secCertifiate = SecCertificateCreateWithData(kCFAllocatorDefault, cerData as CFData) {
                urlCredential = URLCredential(identity: secIdentity, certificates: [secCertifiate], persistence: .forSession)
            }
        }
    }
}

let challenge = URLAuthenticationChallenge(protectionSpace: protectionSpace,
                                                   proposedCredential: urlCredential,
                                                   previousFailureCount: 0,
                                                   failureResponse: nil,
                                                   error: nil,
                                                   sender: self)

I have a Data extension to handle the UnsafeBufferPointers.

extension Data {

    init<T>(from value: T) {
        var value = value
        self.init(buffer: UnsafeBufferPointer(start: &value, count: 1))
    }

    func to<T>(type: T.Type) -> T {
        return self.withUnsafeBytes { $0.pointee }
    }

    func toArray<T>(type: T.Type) -> [T] {
        return self.withUnsafeBytes {
            [T](UnsafeBufferPointer(start: $0, count: self.count/MemoryLayout<T>.stride))
        }
    }
}
like image 733
Justin Domnitz Avatar asked Jul 12 '17 19:07

Justin Domnitz


2 Answers

I realise that this is an older question but I ran into the same issue today while writing tests for my SSL pinning code. I solved it by subclassing URLProtectionSpace

private class TestURLProtectionSpace: URLProtectionSpace {
    var internalServerTrust: SecTrust?
    override var serverTrust: SecTrust? {
        return internalServerTrust
    }
}

let protectionSpace = TestURLProtectionSpace(
        host: "myhost.com",
        port: 443,
        protocol: "https",
        realm: nil,
        authenticationMethod: NSURLAuthenticationMethodServerTrust
)
protectionSpace.internalServerTrust = serverTrust
like image 74
Peter Avatar answered Nov 12 '22 04:11

Peter


You can always move didReceive challenge-method away from the class you are testing and link it with some protocol communication. Then just call its methods when it is needed. For example:

protocol CheckerProtocol {
    // or event simplier
    func isOkCertificate(_ certificate: ) -> Bool
}

If you need to test some logic with certificate just pass it to your checker dependency directly:

if let serverTrust = challenge.protectionSpace.serverTrust {
    var secresult = SecTrustResultType(kSecTrustResultInvalid)
    let status = SecTrustEvaluate(serverTrust, &secresult)

    if status == errSecSuccess {
        if let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {
            if self.checker.isOkCertificate(certificate) { ... }
        }
    }
}

Move checking code to

class Checker: CheckerProtocol { ... }

Meenwhile in your test target:

let certificate = // init right/wrong one
let checker = Checker()
XCTAssert[True|False](checker.isOkCertificate(certificate))
like image 1
Daniyar Avatar answered Nov 12 '22 03:11

Daniyar