Skip to content

Instantly share code, notes, and snippets.

@angelolloqui
Created August 18, 2016 08:50
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save angelolloqui/c45db7151bac31d404238dcf9899e612 to your computer and use it in GitHub Desktop.
Save angelolloqui/c45db7151bac31d404238dcf9899e612 to your computer and use it in GitHub Desktop.
SSL pinning validator with implementation for the Subject public key info (SPKI), based on the one at https://github.com/datatheorem/TrustKit
//
// SSLPinningValidator.swift
//
// Created by Angel Garcia on 17/08/16.
//
import Foundation
import CommonCrypto
protocol SSLPinningValidator {
func canHandleChallenge(challenge: NSURLAuthenticationChallenge) -> Bool
func isChallengeValid(challenge: NSURLAuthenticationChallenge) -> Bool
}
extension SSLPinningValidator {
func canHandleChallenge(challenge: NSURLAuthenticationChallenge) -> Bool {
return challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust
}
}
private let lockQueue = dispatch_queue_create("com.sslpinningvalidator", nil)
private let certificateCache = NSCache()
//Code for SPKI validation based on the one at https://github.com/datatheorem/TrustKit
class SSLPinningSPKIValidator: NSObject, SSLPinningValidator {
enum PublicKeyAlgorithm: String {
case Rsa2048
case Rsa4096
case EcDsaSecp256r1
var asn1HeaderBytes: [UInt8] {
switch self {
case .Rsa2048:
return [ 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00 ]
case .Rsa4096:
return [ 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00 ]
case .EcDsaSecp256r1:
return [ 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
0x42, 0x00 ]
}
}
}
typealias SPKI = (hostname: String, algorithm: PublicKeyAlgorithm, sha256: NSData)
let validSPKIs: [SPKI]
init(validSPKIs: [SPKI]) {
self.validSPKIs = validSPKIs
}
func isChallengeValid(challenge: NSURLAuthenticationChallenge) -> Bool {
let hostname = challenge.protectionSpace.host
let spkis = self.validSPKIs.filter({ hostname.containsString($0.hostname) })
let serverTrust = challenge.protectionSpace.serverTrust!
//Domain not pinned, then is valid
guard spkis.count > 0 else { return true }
// First re-check the certificate chain using the default SSL validation in case it was disabled
// This gives us revocation (only for EV certs I think?) and also ensures the certificate chain is sane
// And also gives us the exact path that successfully validated the chain
let sslPolicy = SecPolicyCreateSSL(true, hostname)
SecTrustSetPolicies(serverTrust, sslPolicy)
var trustResult: SecTrustResultType = 0
guard SecTrustEvaluate(serverTrust, &trustResult) == errSecSuccess else {
return false
}
guard trustResult == UInt32(kSecTrustResultUnspecified) || trustResult == UInt32(kSecTrustResultProceed) else { return false }
// Check each certificate in the server's certificate chain (the trust object); start with the CA all the way down to the leaf
let certificateChainLen = SecTrustGetCertificateCount(serverTrust)
for i in (0..<certificateChainLen).reverse() {
// Extract the certificate
if let certificate = SecTrustGetCertificateAtIndex(serverTrust, i) {
// For each spki key configuration, generate the subject public key info hash
for spki in spkis {
if let subjectPublicKeyInfoHash = sha256SubjectPublicKeyInfoFromCertificate(certificate, algorithm: spki.algorithm)
where subjectPublicKeyInfoHash == spki.sha256 {
return true
}
}
}
}
return false
}
func sha256SubjectPublicKeyInfoFromCertificate(certificate: SecCertificate, algorithm: PublicKeyAlgorithm) -> NSData? {
//Check the cache for already processed sha256
let cacheKey = cacheKeyForCertificate(certificate, algorithm: algorithm)
if let data = certificateCache.objectForKey(cacheKey) as? NSData {
return data
}
// First extract the public key bytes
guard let publicKeyData = extractPublicKeyDataFromCertificate(certificate) else { return nil }
// Generate a hash of the subject public key info
let subjectPublicKeyInfoHash = NSMutableData(length:Int(CC_SHA256_DIGEST_LENGTH))!
var shaCtx = CC_SHA256_CTX()
CC_SHA256_Init(&shaCtx)
// Add the missing ASN1 header for public keys to re-create the subject public key info
let header = algorithm.asn1HeaderBytes
CC_SHA256_Update(&shaCtx, header, CC_LONG(header.count))
// Add the public key
CC_SHA256_Update(&shaCtx, publicKeyData.bytes, CC_LONG(publicKeyData.length))
CC_SHA256_Final(UnsafeMutablePointer<UInt8>(subjectPublicKeyInfoHash.mutableBytes), &shaCtx)
//Save in the cache for later usage
certificateCache.setObject(subjectPublicKeyInfoHash, forKey: cacheKey)
return subjectPublicKeyInfoHash
}
func extractPublicKeyDataFromCertificate(certificate: SecCertificate) -> NSData? {
var tempTrust: SecTrust? = nil
let policy = SecPolicyCreateBasicX509()
// Get a public key reference from the certificate
SecTrustCreateWithCertificates(certificate, policy, &tempTrust)
SecTrustEvaluate(tempTrust!, nil)
let publicKey = SecTrustCopyPublicKey(tempTrust!)!
// Extract the actual bytes from the key reference using the Keychain
// Prepare the dictionary to add the key
let peerPublicKeyAdd: [NSString: AnyObject] = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: "SSLPinningSPKIValidator",
kSecValueRef: publicKey,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecReturnData: kCFBooleanTrue
]
// Prepare the dictionary to retrieve and delete the key
let publicKeyGet: [NSString: AnyObject] = [
kSecClass: kSecClassKey,
kSecAttrApplicationTag: "SSLPinningSPKIValidator",
kSecReturnData: kCFBooleanTrue
]
var publicKeyData: NSData? = nil
dispatch_sync(lockQueue) {
var data: AnyObject? = nil
SecItemAdd(peerPublicKeyAdd, &data)
SecItemDelete(publicKeyGet)
publicKeyData = data as? NSData
}
return publicKeyData
}
func cacheKeyForCertificate(certificate: SecCertificate, algorithm: PublicKeyAlgorithm) -> NSData {
let data = NSMutableData(data: SecCertificateCopyData(certificate))
data.appendData(algorithm.rawValue.dataUsingEncoding(NSUTF8StringEncoding)!)
return data
}
}
@angelolloqui
Copy link
Author

angelolloqui commented Oct 25, 2016

Example of usage:

Set the NSURLSession delegate to a MyClass instance, and add:

extension MyClass {
    var sslPinningValidator: SSLPinningValidator? {      
            //You can apply custom logic here to return different validators depending on business logic
            return SSLPinningSPKIValidator(validSPKIs: [
                (hostname: self.host, algorithm: SSLPinningSPKIValidator.PublicKeyAlgorithm.Rsa2048, sha256: NSData(hexString: "942a6916a6e4ae527711c5450247a2a74fb8e156a8254ca66e739a11493bb445"))
            ])        
    }
}
extension MyClass: NSURLSessionDelegate {
    func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
        guard let sslPinningValidator = sslPinningValidator where sslPinningValidator.canHandleChallenge(challenge) else {
            completionHandler(NSURLSessionAuthChallengeDisposition.PerformDefaultHandling, nil)
            return
        }

        if sslPinningValidator.isChallengeValid(challenge) {
            completionHandler(NSURLSessionAuthChallengeDisposition.PerformDefaultHandling, nil)
        } else {
            NSLog("Invalid SSL pinning!")
            completionHandler(NSURLSessionAuthChallengeDisposition.CancelAuthenticationChallenge, nil)
        }
    }
}

@tsafrir
Copy link

tsafrir commented Jun 18, 2018

I have updated your gist to Swift 4 here: https://gist.github.com/tsafrir/492b9b2cd993948118af6da41414e755
thanks for porting it to Swift!!

@sohailmansoori
Copy link

Hellow angelolloqui ,
i am very new to certificate pinning
could you please tell me what will come in "sha256" please tell me the steps to get that

var sslPinningValidator: SSLPinningValidator? {
//You can apply custom logic here to return different validators depending on business logic
return SSLPinningSPKIValidator(validSPKIs: [
(hostname: self.host, algorithm: SSLPinningSPKIValidator.PublicKeyAlgorithm.Rsa2048, sha256: NSData(hexString: "942a6916a6e4ae527711c5450247a2a74fb8e156a8254ca66e739a11493bb445"))
])
}

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