Skip to content

Instantly share code, notes, and snippets.

@davidmtamas
Created April 2, 2020 07:19
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save davidmtamas/00206ef0bcf746a6911d72e5bb93d3ed to your computer and use it in GitHub Desktop.
Save davidmtamas/00206ef0bcf746a6911d72e5bb93d3ed to your computer and use it in GitHub Desktop.
final class PublicKeyPinner {
/// Stored public key hashes
private let hashes: [String]
public init(hashes: [String]) {
self.hashes = hashes
}
/// ASN1 header for our public key to re-create the subject public key info
private let rsa2048Asn1Header: [UInt8] = [
0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
]
/// Validates an object used to evaluate trust's certificate by comparing their's public key hashes to the known, trused key hashes stored in the app
/// Configuration.
/// - Parameter serverTrust: The object used to evaluate trust.
public func validate(serverTrust: SecTrust, domain: String?) -> Bool {
// Set SSL policies for domain name check, if needed
if let domain = domain {
let policies = NSMutableArray()
policies.add(SecPolicyCreateSSL(true, domain as CFString))
SecTrustSetPolicies(serverTrust, policies)
}
// Check if the trust is valid
var secresult = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &secresult)
guard status == errSecSuccess else { return false }
// For each certificate in the valid trust:
for index in 0..<SecTrustGetCertificateCount(serverTrust) {
// Get the public key data for the certificate at the current index of the loop.
guard let certificate = SecTrustGetCertificateAtIndex(serverTrust, index),
let publicKey = SecCertificateCopyPublicKey(certificate),
let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) else {
return false
}
// Hash the key, and check it's validity.
let keyHash = hash(data: (publicKeyData as NSData) as Data)
if hashes.contains(keyHash) {
// Success! This is our server!
return true
}
}
// If none of the calculated hashes match any of our stored hashes, the connection we tried to establish is untrusted.
return false
}
/// Creates a hash from the received data using the `sha256` algorithm.
/// `Returns` the `base64` encoded representation of the hash.
///
/// To replicate the output of the `openssl dgst -sha256` command, an array of specific bytes need to be appended to
/// the beginning of the data to be hashed.
/// - Parameter data: The data to be hashed.
private func hash(data: Data) -> String {
// Add the missing ASN1 header for public keys to re-create the subject public key info
var keyWithHeader = Data(rsa2048Asn1Header)
keyWithHeader.append(data)
// Using CryptoKit
if #available(iOS 13, *) {
return SHA256.hash(data: keyWithHeader).description
} else {
// Using CommonCrypto's CC_SHA256 method
// var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
// _ = keyWithHeader.withUnsafeBytes {
// CC_SHA256($0.baseAddress!, CC_LONG(keyWithHeader.count), &hash)
// }
// return Data(hash).base64EncodedString()
// Using CryptoSwift's Data.sha256() method
return keyWithHeader.sha256().base64EncodedString()
}
}
}
@mesheilah
Copy link

mesheilah commented Dec 9, 2020

line 66 must be return Data(SHA256.hash(data: keyWithHeader)).base64EncodedString() as mentioned in the article, if you use it as is ( return SHA256.hash(data: keyWithHeader).description ) it won't match with a public key generated using openssl in terminal like the following command:

openssl s_client -servername www.google.com -connect www.google.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

@zhouhao27
Copy link

How to call the validate? Here is my code:

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
    
    var success: Bool = false
    if let serverTrust = challenge.protectionSpace.serverTrust {
      
      if self.pinner.validate(serverTrust: serverTrust, domain: self.domain) {
        success = true
        completionHandler(.useCredential, URLCredential(trust:serverTrust))
      }

It's working if I validate through Info.plist. But it failed when I use your publicKey validate method. Don't know why?

@zhouhao27
Copy link

I managed to solve the issue. My implementation is here.

@ShoaibPathan
Copy link

ShoaibPathan commented Nov 22, 2023

@zhouhao27 The implementation is it working solution?. Even i struggling to find optimum solution.Kindly let me know if its working solution?

@zhouhao27
Copy link

zhouhao27 commented Nov 23, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment