Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CertificatePinningTest.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand All @@ -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 = "<group>"; };
1AA080648CCAF28748471A07 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
1AA080BBC6E3EC2D53231846 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
1AA08283197B9FC4137BEE55 /* CertificatePinningTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CertificatePinningTest.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -64,6 +66,7 @@
1AA0836BD32E37949482A37B /* Main.storyboard */,
1AA086524429E8CCC7BBF2AA /* ViewController.swift */,
1AA086FD50B456A773CB0655 /* CertificatePinner.swift */,
0E99BB8E1E605D4000D5D9AE /* google_co_nz.der */,
1AA08A525F53A849E0444162 /* CertificatePinner_swiftBridge.h */,
);
path = CertificatePinningTest;
Expand Down Expand Up @@ -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;
Expand Down
27 changes: 20 additions & 7 deletions CertificatePinningTest/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9532" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9530"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
Expand All @@ -14,29 +18,38 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="E4t-re-mIY">
<rect key="frame" x="220.5" y="245" width="159" height="30"/>
<rect key="frame" x="108" y="278.5" width="159" height="30"/>
<state key="normal" title="Use NSURLConnection"/>
<connections>
<action selector="nsUrlConnectionTapped:" destination="BYZ-38-t0r" eventType="touchUpInside" id="nQf-sx-Azs"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="F9Z-Dl-hhc">
<rect key="frame" x="233" y="295" width="134" height="30"/>
<rect key="frame" x="120.5" y="328.5" width="134" height="30"/>
<state key="normal" title="Use NSURLSession"/>
<connections>
<action selector="nsUrlSessionTapped:" destination="BYZ-38-t0r" eventType="touchUpInside" id="iwi-xW-2fe"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="l4V-Ec-SyL">
<rect key="frame" x="117" y="378.5" width="140" height="30"/>
<state key="normal" title="Get Hash of DER file"/>
<connections>
<action selector="getHashOfCertificateFile:" destination="BYZ-38-t0r" eventType="touchUpInside" id="uWh-Kb-29F"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="F9Z-Dl-hhc" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="5xY-p0-7k2"/>
<constraint firstItem="E4t-re-mIY" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="88r-bP-yEw"/>
<constraint firstItem="E4t-re-mIY" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" constant="-40" id="Flh-h2-kCh"/>
<constraint firstItem="l4V-Ec-SyL" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="H4H-Wg-I6g"/>
<constraint firstItem="l4V-Ec-SyL" firstAttribute="top" secondItem="F9Z-Dl-hhc" secondAttribute="bottom" constant="20" id="JXs-zu-hJS"/>
<constraint firstItem="F9Z-Dl-hhc" firstAttribute="top" secondItem="E4t-re-mIY" secondAttribute="bottom" constant="20" id="qBL-E2-bVC"/>
</constraints>
</view>
Expand Down
116 changes: 80 additions & 36 deletions CertificatePinningTest/CertificatePinner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CommonCrypto/CommonHMAC.h>

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

Expand All @@ -47,35 +41,28 @@ 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)
}


/**

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 {

Expand All @@ -86,18 +73,33 @@ 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
}

/**
Expand All @@ -116,10 +118,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
Expand All @@ -130,23 +130,14 @@ 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) {
return true
}
}
}

return false

}

/**
Expand All @@ -156,17 +147,27 @@ 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..<SecTrustGetCertificateCount(trust) {
if let
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 {
print("Hash SHA256: \(publicKeyHash)")
}
}
}

return res
}

Expand All @@ -175,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()
Expand All @@ -196,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<UInt8>(hash), count: Int(CC_SHA256_DIGEST_LENGTH))

return res.base64EncodedString(options: NSData.Base64EncodingOptions.init(rawValue: 0))
return res.base64EncodedString()
}

return ""
Expand All @@ -217,7 +224,20 @@ class CertificatePinner {
var publicKeyData : AnyObject?
var putResult : OSStatus = noErr
var delResult : OSStatus = noErr

// on iOS 10+ we can directly get the key data
if #available(iOS 10.0, macOS 10.12, *) {
var error:Unmanaged<CFError>? = 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,
Expand All @@ -244,5 +264,29 @@ 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<UInt8>.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()
}
}
30 changes: 28 additions & 2 deletions CertificatePinningTest/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.

}


Expand Down Expand Up @@ -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")
}
}



}

Expand Down
Binary file added CertificatePinningTest/google_co_nz.der
Binary file not shown.
Loading