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.