How are CoreFoundation and Security types bridged into Swift?

(Jon Shier) #1

CoreFoundation's CFType and its subtypes(?) have always been mysterious in many ways. However, when they're bridged into Swift, the mystery deepens. Swift's generated API for many of the types show no protocol conformances (and the documentation doesn't either), yet you can sometimes use methods typically reserved for conforming types in Swift, like ==.

For example, here's the evolution of Alamofire's public key pinning check from Swift 3, 4, and 5.

Swift < 3:

outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
    for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
        if serverPublicKey.isEqual(pinnedPublicKey) {
            serverTrustIsValid = true
            break outerLoop
        }
    }
}

Cast to AnyObject was necessary to get isEqual.

Swift 4(ish):

let pinnedKeysInServerKeys: Bool = {
    for serverPublicKey in trust.af.publicKeys as [AnyHashable] {
        for pinnedPublicKey in keys as [AnyHashable] {
            if serverPublicKey == pinnedPublicKey {
                return true
            }
        }
    }
    return false
}()

After discussion in this thread, we were able to transition from AnyObject and isEqual to AnyHashable and ==. I understand why this worked, since AnyHashable is itself also Equatable. I just have no idea where it got that implementation from for SecKey.

Swift 5:

let pinnedKeysInServerKeys: Bool = {
    for serverPublicKey in trust.af.publicKeys {
        for pinnedPublicKey in keys {
            if serverPublicKey == pinnedPublicKey {
                return true
            }
        }
    }
    return false
}()

Now, even without the AnyHashable cast, the same algorithm works. But how?! SecKey does not conform to Equatable in any way I can see, and this algorithm didn't work in earlier versions of Swift without the cast.

Investigating in the debugger I can see that, somehow, the equal keys end up with the same memory address, despite one being loaded from a certificate from disk and the other received over the network. Is this some sort of internal implementation detail for SecKey? Is this always guaranteed? Could the various overlays be updated to indicate Equatable conformance? Even more, would it be possible to make these values Hashable?

1 Like
(Quinn “The Eskimo!”) #2

Investigating in the debugger I can see that, somehow, the equal keys
end up with the same memory address, despite one being loaded from a
certificate from disk and the other received over the network. Is this
some sort of internal implementation detail for SecKey?

No.

Is this always guaranteed?

No.

Consider the following test code:

func test() {
    func loadCert(named name: String) -> SecCertificate {
        let certURL = Bundle.main.url(forResource: name, withExtension: "cer")!
        let certData = try! Data(contentsOf: certURL)
        return SecCertificateCreateWithData(nil, certData as NSData)!
    }
    let cert1 = loadCert(named: "Frankie")
    let cert2 = loadCert(named: "Frankie")
    let pubKey1 = SecCertificateCopyPublicKey(cert1)!
    let pubKey2 = SecCertificateCopyPublicKey(cert2)!
    print(ObjectIdentifier(pubKey1))
    print(ObjectIdentifier(pubKey2))
}

Testing in an app running on iOS 12.2 with Frankie.cer included in the main bundle, this prints:

ObjectIdentifier(0x000060000163d1e0)
ObjectIdentifier(0x000060000163d240)

As you can see, the two keys have different addresses.

IMO the canonically correct way to test CF types for equality is CFEqual.

print(CFEqual(pubKey1, pubKey2))    // A

The other mechanisms you suggested also seem to work:

let hash1 = pubKey1 as AnyHashable
let hash2 = pubKey2 as AnyHashable
print(hash1 == hash2)               // B
print(pubKey1 == pubKey2)           // C

If you set a breakpoint on SecKeyEqual [1] and then look at the backtrace, you’ll see the following:

// case A

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x00000001025fb49c Security`SecKeyEqual
    frame #1: 0x0000000100e15fe1 xxsi`MainViewController.test(self=0x00007faa75908b40) at MainViewController.swift:26:15

// case B

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x00000001025fb49c Security`SecKeyEqual
    frame #1: 0x0000000102dfdc29 libswiftCoreFoundation.dylib`static (extension in CoreFoundation):CoreFoundation._CFObject.== infix(A, A) -> Swift.Bool + 9
    frame #2: 0x0000000100e17c5d xxsi`protocol witness for static Equatable.== infix(_:_:) in conformance SecKeyRef at <compiler-generated>:0
    frame #3: 0x00000001028341e5 libswiftCore.dylib`Swift._ConcreteHashableBox._isEqual(to: Swift._AnyHashableBox) -> Swift.Optional<Swift.Bool> + 261
    frame #4: 0x00000001028343f9 libswiftCore.dylib`protocol witness for Swift._AnyHashableBox._isEqual(to: Swift._AnyHashableBox) -> Swift.Optional<Swift.Bool> in conformance Swift._ConcreteHashableBox<A> : Swift._AnyHashableBox in Swift + 9
    frame #5: 0x00000001029abe90 libswiftCore.dylib`function signature specialization <Arg[2] = Dead> of static Swift.AnyHashable.== infix(Swift.AnyHashable, Swift.AnyHashable) -> Swift.Bool + 192
    frame #6: 0x00000001028347c9 libswiftCore.dylib`static Swift.AnyHashable.== infix(Swift.AnyHashable, Swift.AnyHashable) -> Swift.Bool + 9
  * frame #7: 0x0000000100e16181 xxsi`MainViewController.test(self=0x00007faa75908b40) at MainViewController.swift:29:21

// case C

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x00000001025fb49c Security`SecKeyEqual
    frame #1: 0x0000000102dfdc29 libswiftCoreFoundation.dylib`static (extension in CoreFoundation):CoreFoundation._CFObject.== infix(A, A) -> Swift.Bool + 9
    frame #2: 0x0000000100e16244 xxsi`MainViewController.test(self=0x00007faa75908b40) at MainViewController.swift:30:23

Backtrace A is easy to understand. test calls CFEqual which dispatches to SecKeyEqual, with the tail call eliminated so that CFEqual does not appear in the backtrace.

Backtrace C is also pretty straightforward: It seems that the Swift overlay for CF has an implementation of == for CF objects (frame 1).

Backtrace B… sheesh… that’s complex, and I don’t pretend to understanding it all, but I’ll note that frame 2 looks like a call through Equatable and frames 1 through 0 look just like backtrace C.


Despite the results above, I’m still going to keep using CFEqual. It avoids any ambiguity and it works well in both Swift and [Objective-]C[++].


Even more, would it be possible to make these values Hashable?

CF objects support hashing via CFHash. I do not know how SecKey implements this, but I can’t see a custom implementation which means it probably inherits the base implementation from CF, that is, it hashes the pointer. Indeed, this code:

print(CFHash(pubKey1))
print(CFHash(pubKey2))

prints this result:

105553136120864
105553136120896

indicating that it probably won’t be useful to you.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] CF type equality is dispatched through a faux v-table but, by convention, you’ll find that the private equality function for CF type X is named CFXEqual.

3 Likes
(Jordan Rose) #3

The conformances for CF types live on a hidden protocol called _CFObject. It's meant to be an implementation detail (as in, please don't write things that are generic over _CFObject, since we provide no guarantees as to what that might mean), but you can rely on it using CFHash and CFEqual.

5 Likes
(Jon Shier) #4

@eskimo How were you able to set the breakpoint on SecKeyEqual?

@jrose Interesting. So the conformance is official, just implemented in a way that's not visible to the documentation? Would it be appropriate to indicate in the documentation that these types conform to the various protocols?

(Jordan Rose) #5

Yes, it would! Can you file a Radar?

1 Like
(Jon Shier) #6

Sure, filed 49926431 about the documentation issue.

Filed 49926559 about the uselessness of SecKey's Hashable implementation.

2 Likes
(Quinn “The Eskimo!”) #7

How were you able to set the breakpoint on SecKeyEqual?

For a persistent breakpoint:

  1. Choose View > Navigators > Show Breakpoint Navigator.

  2. From the + menu at the bottom right, choose Symbolic Breakpoint.

  3. Enter SecKeyEqual in the Symbol field.

For a temporary breakpoint, use the command line:

(lldb) br set -n SecKeyEqual

Filed 49926431 about the documentation issue.

Filed 49926559 about the uselessness of SecKey's Hashable implementation.

Thanks!

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like