macoscertificatekeychainsha1cfdata

Get a macOS Keychain certificate's SHA1 hash in Swift


I've retrieved the set of certificates in my keychain using this code:

    let query: [String: Any] = [
        kSecClass as String: kSecClassCertificate,
        kSecMatchLimit as String: kSecMatchLimitAll,
        kSecReturnAttributes as String: false,
        kSecReturnData as String: true
    ]

    var result: CFTypeRef?
    
    var results : Set<CertsResult> = []


    let status = SecItemCopyMatching(query as CFDictionary, &result)
    //[Check status]

    guard let certificateData = result as? [CFData] else {
        //[Handle]
    }

From here, I loop through certificateData and gather information about the certificates, but I need to get the SHA1 hash of the certificates as well. I've gathered from researching that I need to use import CommonCrypto and CC_SHA1, but what I've read doesn't use a CFData.

Is there a good way to get from this point to its SHA1?


Solution

  • You can achieve it by performing the hash yourself. The fingerprints are not part of the certificate itself. More info about that over here.

    import CryptoKit
    
    let certificate = ...
    
    let der = SecCertificateCopyData(certificate) as Data
    let sha1 = Insecure.SHA1.hash(data: der)
    let sha256 = SHA256.hash(data: der)
    

    This can be created in an extension too. I've used CommonCrypto in the extension.

    import CommonCrypto
    
    extension SecCertificate {
        var sha1: Data {
            var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH))
            let der = SecCertificateCopyData(self) as Data
            _ = CC_SHA1(Array(der), CC_LONG(der.count), &digest)
            return Data(digest)
        }
    
        var sha256: Data {
            var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
            let der = SecCertificateCopyData(self) as Data
            _ = CC_SHA256(Array(der), CC_LONG(der.count), &digest)
            return Data(digest)
        }
    }
    

    I'd like to mention that SHA-1 hashes of certificates are deprecated since like 2017 and websites and tech giants are starting to drop support for them.

    Playground example

    import CryptoKit
    import Foundation
    
    class CertificateStuff: NSObject, URLSessionDelegate {
        func urlSession(
            _ session: URLSession,
            didReceive challenge: URLAuthenticationChallenge,
            completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
        ) {
            guard let serverTrust = challenge.protectionSpace.serverTrust else {
                completionHandler(.rejectProtectionSpace, nil)
                return
            }
    
            for index in 0 ..< SecTrustGetCertificateCount(serverTrust) {
                let certificate = SecTrustGetCertificateAtIndex(serverTrust, index)!
    
                let der = SecCertificateCopyData(certificate)
                let sha1 = Insecure.SHA1.hash(data: der as Data)
                let sha256 = SHA256.hash(data: der as Data)
    
                print(certificate)
                print(sha1)
                print(sha256)
                print()
            }
            completionHandler(.performDefaultHandling, nil)
        }
    
        func request(_ done: @escaping (Result<Data, Error>) -> Void) {
            let url = URL(string: "https://security.stackexchange.com/questions/14330/what-is-the-actual-value-of-a-certificate-fingerprint")!
            let request = URLRequest(url: url)
            URLSession(configuration: .default, delegate: self, delegateQueue: nil).dataTask(with: request) { (d, r, e) in
                if let e = e {
                    print(e)
                    return
                }
                print(d!)
            }.resume()
        }
    }
    
    CertificateStuff().request { result in print(result) }