From 332cba100306166ff605a16969d9c4efa1771113 Mon Sep 17 00:00:00 2001 From: Gabriel Reimers Date: Fri, 24 Feb 2017 11:47:28 +0100 Subject: [PATCH 1/3] - get public key data from SecKey without KeyChain on iOS 10+ - adds domains to public key hash debug logging --- .../CertificatePinner.swift | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/CertificatePinningTest/CertificatePinner.swift b/CertificatePinningTest/CertificatePinner.swift index 7f1a8f8..ed2f35e 100644 --- a/CertificatePinningTest/CertificatePinner.swift +++ b/CertificatePinningTest/CertificatePinner.swift @@ -12,33 +12,27 @@ import Security /* In order to use this, you need to pull this file in, as well as CertificatePinner_swiftBridge.h - If you already have a Swift bridge, you can just include this in it: #import - + This pulls in the SHA256 functions. - + Portions of this - usually Objective C versons - taken from AlamoFire and AFNetworking - */ class CertificatePinner { /** - The base URL to check against when validateCertificateTrustChain is called. - */ open var expectedBaseUrl : String? /** - Debug mode prints out the hashes on validation. Useful for finding out what the server is presenting so you have something to pin to - */ open var debugMode: Bool = false @@ -47,21 +41,17 @@ class CertificatePinner { init() { - } - init(_ expectedUrl : String) { + init(_ expectedUrl : String) { expectedBaseUrl = expectedUrl } /** - Add a hash to validate against. - - - Parameter hash: the hash string (eg "+abCS2zjVyISeEE90Fq1eC1ihAtQoh6q3mMUjlLGXfw=") to match - Use debugMode to find the hashes + - Parameter hash: the hash string (eg "+abCS2zjVyISeEE90Fq1eC1ihAtQoh6q3mMUjlLGXfw=") to match */ open func addCertificateHash(_ hash : String) { localHashList.append(hash) @@ -69,13 +59,10 @@ class CertificatePinner { /** - Validates the certificate trust chain - we are expecing a certificate from google.com, did we get one? - Parameter trust: The trust provided by NSUrlSession and NSUrlConnection - - Returns: true if the chain is valid. - */ open func validateCertificateTrustChain(_ trust: SecTrust) -> Bool { @@ -95,9 +82,7 @@ class CertificatePinner { if SecTrustEvaluate(trust, &result) == errSecSuccess { return (result == SecTrustResultType.unspecified || result == SecTrustResultType.proceed) } - return false - } /** @@ -116,10 +101,8 @@ class CertificatePinner { */ open func validateTrustPublicKeys(_ trust: SecTrust) -> Bool { - let trustPublicKeys = getPublicKeysFromTrust(trust) - //do we have anything to compare to? if trustPublicKeys.count == 0 { return false @@ -130,13 +113,6 @@ class CertificatePinner { return true } - if debugMode { - print("hash order is usually most specific to least, so the first one is your domain, the last is the root CA") - for trustKey in trustPublicKeys { - print("hash: \(trustKey)") - } - } - for trustKey in trustPublicKeys { for localKey in localHashList { if (localKey == trustKey) { @@ -144,9 +120,7 @@ class CertificatePinner { } } } - return false - } /** @@ -156,7 +130,10 @@ class CertificatePinner { //https://github.com/Alamofire/Alamofire/blob/master/Source/ServerTrustPolicy.swift#L274 var res : [String] = [] - + if debugMode { + print("hash order is usually most specific to least, so the first one is your domain, the last is the root CA") + } + for index in 0..? = nil + let keyData = SecKeyCopyExternalRepresentation(publicKeyRef, &error) as? Data + + if let error = error?.takeRetainedValue() as? Error { + print("publicKeyRefToData > \(error.localizedDescription)") + } + return keyData + } + + // on iOS < 10 we need to go via KeyChain to get the key data let putKeyParams : NSMutableDictionary = [ kSecClass as String : kSecClassKey, kSecAttrApplicationTag as String : keychainTag, @@ -244,5 +237,4 @@ class CertificatePinner { } - } From fe1301ae470e573801c03730e4837694ddb9cca3 Mon Sep 17 00:00:00 2001 From: Gabriel Reimers Date: Fri, 24 Feb 2017 15:27:21 +0100 Subject: [PATCH 2/3] - added hashForDERCertificate function to calculate hashes offline from DER files - added google_co_nz sample DER file - added GUI button to calculate DER hash - added more debugging prints to better see which hash belongs to which certificate --- .../project.pbxproj | 4 ++ .../Base.lproj/Main.storyboard | 27 +++++-- .../CertificatePinner.swift | 66 ++++++++++++++++-- CertificatePinningTest/ViewController.swift | 30 +++++++- CertificatePinningTest/google_co_nz.der | Bin 0 -> 1170 bytes 5 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 CertificatePinningTest/google_co_nz.der diff --git a/CertificatePinningTest.xcodeproj/project.pbxproj b/CertificatePinningTest.xcodeproj/project.pbxproj index da765d5..8c43de5 100644 --- a/CertificatePinningTest.xcodeproj/project.pbxproj +++ b/CertificatePinningTest.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0E99BB8F1E605D4000D5D9AE /* google_co_nz.der in Resources */ = {isa = PBXBuildFile; fileRef = 0E99BB8E1E605D4000D5D9AE /* google_co_nz.der */; }; 1AA083F3585BFCD761DB621C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AA08F761811FE8A00D7461C /* AppDelegate.swift */; }; 1AA08439AA67FA53500386CE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1AA080394497B76A2CBB10F5 /* LaunchScreen.storyboard */; }; 1AA0858A0E88237F2D023ED6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1AA0829C71FC696C5999FE6B /* Assets.xcassets */; }; @@ -16,6 +17,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 0E99BB8E1E605D4000D5D9AE /* google_co_nz.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = google_co_nz.der; sourceTree = ""; }; 1AA080648CCAF28748471A07 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 1AA080BBC6E3EC2D53231846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 1AA08283197B9FC4137BEE55 /* CertificatePinningTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CertificatePinningTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -64,6 +66,7 @@ 1AA0836BD32E37949482A37B /* Main.storyboard */, 1AA086524429E8CCC7BBF2AA /* ViewController.swift */, 1AA086FD50B456A773CB0655 /* CertificatePinner.swift */, + 0E99BB8E1E605D4000D5D9AE /* google_co_nz.der */, 1AA08A525F53A849E0444162 /* CertificatePinner_swiftBridge.h */, ); path = CertificatePinningTest; @@ -128,6 +131,7 @@ files = ( 1AA0858A0E88237F2D023ED6 /* Assets.xcassets in Resources */, 1AA08439AA67FA53500386CE /* LaunchScreen.storyboard in Resources */, + 0E99BB8F1E605D4000D5D9AE /* google_co_nz.der in Resources */, 1AA0875D3CD7CBDE9237ED76 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CertificatePinningTest/Base.lproj/Main.storyboard b/CertificatePinningTest/Base.lproj/Main.storyboard index d6964aa..56d6cd8 100644 --- a/CertificatePinningTest/Base.lproj/Main.storyboard +++ b/CertificatePinningTest/Base.lproj/Main.storyboard @@ -1,8 +1,12 @@ - - + + + + + - + + @@ -14,29 +18,38 @@ - + + - + + + diff --git a/CertificatePinningTest/CertificatePinner.swift b/CertificatePinningTest/CertificatePinner.swift index ed2f35e..af1a09b 100644 --- a/CertificatePinningTest/CertificatePinner.swift +++ b/CertificatePinningTest/CertificatePinner.swift @@ -73,17 +73,34 @@ class CertificatePinner { } let policy = SecPolicyCreateSSL(true, baseUrl as CFString) - SecTrustSetPolicies(trust, policy) //https://github.com/Alamofire/Alamofire/blob/master/Source/ServerTrustPolicy.swift#L238 var result = SecTrustResultType.invalid - if SecTrustEvaluate(trust, &result) == errSecSuccess { return (result == SecTrustResultType.unspecified || result == SecTrustResultType.proceed) } return false } + + /** + Calculate a hash for a given certificate in DER format + + - Parameter derData: data of a DER encoded certificate (file) + - Returns: the SHA256 hash of the certificates public key, `nil` on error + + This is useful to get the hash of a certificate before it is deployed. + Tip: You can export DER certificates from the certificate details in Firefox + + */ + open func hashForDERCertificate(derData: Data) -> String? { + + if let certificate = SecCertificateCreateWithData(nil, derData as CFData), + let key = publicKeyForCertificate(certificate) { + return publicKeyRefToHash(key) + } + return nil + } /** Validate the trust's provided public keys against any provided hash values @@ -139,12 +156,15 @@ class CertificatePinner { certificate = SecTrustGetCertificateAtIndex(trust, index), let publicKey = publicKeyForCertificate(certificate) { + if debugMode { + let summary = SecCertificateCopySubjectSummary(certificate) as? String ?? "" + print("\nCertificate: \(summary)") + } let publicKeyHash = publicKeyRefToHash(publicKey) res.append(publicKeyHash) if debugMode { - let summary = SecCertificateCopySubjectSummary(certificate) as? String ?? "" - print("Certificate: \(summary)\nHash: \(publicKeyHash)") + print("Hash SHA256: \(publicKeyHash)") } } } @@ -156,6 +176,7 @@ class CertificatePinner { */ fileprivate func publicKeyForCertificate(_ certificate: SecCertificate) -> SecKey? { //https://github.com/Alamofire/Alamofire/blob/master/Source/ServerTrustPolicy.swift#L289 + var publicKey: SecKey? let policy = SecPolicyCreateBasicX509() @@ -177,11 +198,16 @@ class CertificatePinner { if let keyData = publicKeyRefToData(publicKeyRef) { + if debugMode { + let hex = (keyData as NSData).hexString ?? "" + print("Public Key \(keyData.count) bytes:\n\(hex)") + } + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) CC_SHA256((keyData as NSData).bytes, CC_LONG(keyData.count), &hash) let res = Data(bytes: UnsafePointer(hash), count: Int(CC_SHA256_DIGEST_LENGTH)) - - return res.base64EncodedString(options: NSData.Base64EncodingOptions.init(rawValue: 0)) + + return res.base64EncodedString() } return "" @@ -200,13 +226,14 @@ class CertificatePinner { var delResult : OSStatus = noErr // on iOS 10+ we can directly get the key data - if #available(iOS 10, *) { + if #available(iOS 10.0, macOS 10.12, *) { var error:Unmanaged? = nil let keyData = SecKeyCopyExternalRepresentation(publicKeyRef, &error) as? Data if let error = error?.takeRetainedValue() as? Error { print("publicKeyRefToData > \(error.localizedDescription)") } + return keyData } @@ -238,3 +265,28 @@ class CertificatePinner { } + + + +extension NSData { + + var hexString: String? { + let buf = bytes.assumingMemoryBound(to: UInt8.self) + let charA = UInt8(UnicodeScalar("a").value) + let char0 = UInt8(UnicodeScalar("0").value) + + func itoh(_ value: UInt8) -> UInt8 { + return (value > 9) ? (charA + value - 10) : (char0 + value) + } + + let hexLen = length * 2 + let ptr = UnsafeMutablePointer.allocate(capacity: hexLen) + + for i in 0 ..< length { + ptr[i*2] = itoh((buf[i] >> 4) & 0xF) + ptr[i*2+1] = itoh(buf[i] & 0xF) + } + + return String(bytesNoCopy: ptr, length: hexLen, encoding: .utf8, freeWhenDone: true)?.uppercased() + } +} diff --git a/CertificatePinningTest/ViewController.swift b/CertificatePinningTest/ViewController.swift index 542e903..c80a0ef 100644 --- a/CertificatePinningTest/ViewController.swift +++ b/CertificatePinningTest/ViewController.swift @@ -16,6 +16,7 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. + } @@ -83,8 +84,33 @@ class ViewController: UIViewController { task.resume() } - - + + + @IBAction func getHashOfCertificateFile(_ sender: UIButton) { + + /* + CertificatePinner can calculate the hash for DER coded certificate + files on your disk / in your project. + + To do so, read in the DER file as Data and hand it to the + hashForDERCertificate function. + + This is very handy when you have not deployed your certificate yet. + */ + + let pinner = CertificatePinner() + + //read in sample DER certificate + let derURL = Bundle.main.url(forResource: "google_co_nz", withExtension: "der")! + let derData = try! Data(contentsOf: derURL) + if let hash = pinner.hashForDERCertificate(derData: derData) { + print("SHA256 hash for \(derURL):\n\(hash)\n\n") + } else { + print("Could not calculate hash for \(derURL)\n\n") + } + } + + } diff --git a/CertificatePinningTest/google_co_nz.der b/CertificatePinningTest/google_co_nz.der new file mode 100644 index 0000000000000000000000000000000000000000..290b688d32f74c19f449f80986e83680cd9d877b GIT binary patch literal 1170 zcmXqLV(BwzVlG?2%*4pV#3AU#<|dFmwcUW1jZ>@5qwPB{BO^B}gMp_Zw*e;`b0`a& zFjHu-p|F7WO}a!$<2Ov^9I%S<#BGY|n; z#m>X)n_rq&l9-vN5SE!*4tEz1vb)3$M8WRj;nUJf2Rm0UIbScY%Ako+3E7{FtPIRe zjQk8haW1ANMn;C|=GQtBH7#wmPt>Na@c5@-(%)=To!5W1eP?I3tMA3^9Eqe0MQn@t z4PGy7UD4F`_1L_B|D;aL%j!JD)ZzbQgX}!HH%jkI|I6>v4(91=RQRp6W6nv1{WakW z-rZ{d;4~*^^`VO`u@_DZ#%Q9C`{O0Ddx)8?+2t$CVi}1tUg13mAc-t`qZ$D zidE_L2P`F?NA;Z*|Kzhh!pc^}?EB=|t3MYVVcuLR^kL4|jBhu?Kjv9qP>p}2w#ZmH z_~xpg2YsgeNP9i!of^Rf3#uybszlJ2(7B zU&M<8Id)1Xe;qI|f1Wux@XMmJGntqf85kEgF-8JIB-B6_7>Kf}ETEw0&}L&~Wo2h( zgtM3o)IrjMED8p4O=#iV#Dfy*88A(Zj4Uz0_|}H;4Vc>GGfGMdtn~E@vNI7;m8F92xmt0hWq?i$|I5{UXHLs-D5Lrila&ZB~#e6JcEF$fT^VA;APyUp4L;HUli>&6I zUl;Zm@PJ$_%*gnkg~@=yKpwgifYt1Py{JZ zV39JA0P5rdh5!rWwMr&dMgtBuw)93GVD2$604b4Y(KJvuP+g$BKoRal9RA5cPJX~_ z226g84DtuNUf%SoRxaw)U42D&x>&)jWirQ4oOijoK-sBRYuni?rE%%%Z$F6^v~_Ko z(jc_XaVAsMap@0V*;~~*rpDjsGcyW0XzTW5OU}31avB!q52AMSH@q{?yWIBmnf#@S zn{y_8-}^XVdc@YXTo)v=kF=TX`1nV7an>Qbwuv*W?K=Ymf6R=#%n<1IoNxV4yN71} zjxpiY6*CInewEU=6}xnMa*s)JNzSir=GDxmE;$u{6UG0y{3~k9w)|4VaC^VV+TdIB z*KVHMm%dMo*>Cw%hcAnn=Gx7UJYLYgb!9{H^O_j}8y0qLG%UX+613U6By!^gzS55_ T$8FcPbhfECndM8aaQOxRpPio( literal 0 HcmV?d00001 From 18ed99985fd21169293f35c6c8e2e82abbe54630 Mon Sep 17 00:00:00 2001 From: Gabriel Reimers Date: Fri, 24 Feb 2017 15:44:06 +0100 Subject: [PATCH 3/3] added instructions to hash local certificate file to readme --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 1d9312b..f9604db 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,26 @@ You then implement `NSURLConnectionDelegate`, and override `willSendRequestForAu This implementation errs on the side of "reject if anything looks wrong". + +## Local Certificate File +It might well be, that you have a certificate file that is not deployed online. You can calculate the public key hash of a certificate file using the `hashForDERCertificate(: Data)` function: + +```swift +let pinner = CertificatePinner() + +//read in sample DER certificate +let derURL = Bundle.main.url(forResource: "google_co_nz", withExtension: "der")! +let derData = try! Data(contentsOf: derURL) +if let hash = pinner.hashForDERCertificate(derData: derData) { + print("SHA256 hash for \(derURL):\n\(hash)\n\n") +} else { + print("Could not calculate hash for \(derURL)\n\n") +} +``` + +Tip: You can easily get the DER file of a website's certificate by showing the certificate details in Firefox. There will be a export button to save the certificate in CER or DER format. + + # Thanks Big props to the [AlamoFire](https://github.com/Alamofire/Alamofire) and [AFNetworking](https://github.com/AFNetworking/AFNetworking) teams, where the bulk of the code came from - this is mostly an extraction and replementation in Swift of their code, with a bit of opinion added for good measure.