WultraSSLPinning
is a library implementing dynamic SSL pinning, written in Swift.
The SSL pinning (or public key, or certificate pinning) is a technique mitigating Man-in-the-middle attacks against the secure HTTP communication. The typical iOS solution is to bundle the hash of the certificate, or the exact data of the certificate to the application and validate the incoming challenge in the URLSessionDelegate
. This in general works well, but it has, unfortunately, one major drawback of the certificate's expiration date. The certificate expiration forces you to update your application regularly before the certificate expires, but still, some percentage of the users don't update their apps automatically. So, the users on the older version, will not be able to contact the application servers.
The solution to this problem is the dynamic SSL pinning, where the list of certificate fingerprints are securely downloaded from the remote server. The WultraSSLPinning
library does precisely this:
Before you start using the library, you should also check our other related projects:
The Swift Package Manager is a tool for automating the distribution of Swift code and is integrated into the swift
compiler.
Once you have your Swift package set up, adding this lbirary as a dependency is as easy as adding it to the dependencies
value of your Package.swift
.
dependencies: [
.package(url: "https://github.com/wultra/ssl-pinning-ios.git", .upToNextMajor(from: "1.5.0"))
]
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
$ gem install cocoapods
To integrate framework into your Xcode project using CocoaPods, specify it in your Podfile
:
platform :ios, '11.0'
target '<Your Target App>' do
pod 'WultraSSLPinning/PowerAuthIntegration'
end
The current version of the library depends on PowerAuth2 framework, version 0.19.1
and greater.
Note that Carthage integration is experimental. We don't provide support for this type of installation.
Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. You can install Carthage with Homebrew using the following command:
$ brew update
$ brew install carthage
To integrate the library into your Xcode project using Carthage, specify it in your Cartfile
:
github "wultra/WultraSSLPinning"
Run carthage update
to build the framework and drag the built WultraSSLPinning.framework
into your Xcode project.
The library provides the following core types:
CertStore
- the main class which provides all tasks for dynamic pinningCertStoreConfiguration
- the configuration structure for CertStore
classThe next chapters of this document will explain how to configure and use CertStore
for the SSL pinning purposes.
Following code will configure CertStore
object with basic configuration, with using PowerAuth2
as cryptographic provider & secure storage provider:
import WultraSSLPinning
let configuration = CertStoreConfiguration(
serviceUrl: URL(string: "https://...")!,
publicKey: "BMne....kdh2ak=",
useChallenge: true
)
let certStore = CertStore.powerAuthCertStore(configuration: configuration)
We'll use certStore
variable in the rest of the documentation as a reference to already configured CertStore
instance.
The configuration has the following properties:
serviceUrl
- parameter defining URL with a remote list of certificates. It is recommended that serviceUrl
points to a different domain than you're going to protect with pinning. See the FAQ section for more details.publicKey
- contains the public key counterpart to the private key, used for data signing. The Base64 formatted string is expected.useChallenge
- parameter that defines whether remote server requires challenge request header:
true
in case you're connecting to Mobile Utility Server or similar service.false
in case the remote server provides a static data, generated by SSL Pinning Tool.expectedCommonNames
- an optional array of strings, defining which domains you expect in certificate validation.identifier
- optional string identifier for scenarios, where multiple CertStore
instances are used in the applicationfallbackCertificatesData
- optional hardcoded data for fallback fingerprints. See the next chapter of this document for details.periodicUpdateInterval
- defines how often will CertStore
update the fingerprints silently at the background. The default value is 1 week.expirationUpdateTreshold
- defines time window before the next certificate will expire. In this time window CertStore
will try to update the list of fingerprints more often than usual. Default value is 2 weeks before the next expiration.sslValidationStrategy
- defines the validation strategy for HTTPS connections initiated from the library itself. The .default
value performs standard certificate chain validation provided by the operating system. Be aware that altering this option may put your application at risk. You should not ship your application to production with SSL validation turned off.The CertStoreConfiguration
may contain an optional data with predefined certificates fingerprints. This technique can speed up the first application's startup when the database of fingerprints is empty. You still need to update your application, once the fallback fingerprints expire.
To configure the property, you need to provide JSON data with fallback fingerprints. The JSON should contain the same data as are usually received from the server, except that "signature" property is not validated (but must be provided in JSON). For example:
{
"fingerprints":[
{
"name": "github.com",
"fingerprint": "MRFQDEpmASza4zPsP8ocnd5FyVREDn7kE3Fr/zZjwHQ=",
"expires": 1591185600,
"signature": ""
}
]
}
""".data(using: .ascii)
let configuration = CertStoreConfiguration(
serviceUrl: URL(string: "https://...")!,
publicKey: "BMne....kdh2ak=",
fallbackCertificatesData: fallbackData!
)
let certStore = CertStore.powerAuthCertStore(configuration: configuration)
Note that if you provide the wrong JSON data, then the fatal error is thrown.
The library doesn't provide singleton for CertStore
, but you can make it on your own. For example:
extension CertStore {
static var shared: CertStore {
let config = CertStoreConfiguration(
serviceUrl: URL(string: "https://...")!,
publicKey: "BMne....kdh2ak="
)
return .powerAuthCertStore(configuration: config)
}
}
To update list of fingerprints from the remote server, use the following code:
certStore.update { (result, error) in
if result == .ok {
// everything's OK,
// No action is required, or silent update was started
} else if result == .storeIsEmpty {
// Update succeeded, but it looks like the remote list contains
// already expired fingerprints. The certStore will probably not be able
// to validate the fingerprints.
} else {
// Other error. See `CertStore.UpdateResult` for details.
// The "error" variable is set in case of a network error.
}
}
You have to typically call the update on your application's startup, before you initiate the secure HTTP request to the server, which certificate's expected to be validated with the pinning. The update function works in two basic modes:
CertStore
performs the update on the background. The purpose of the silent update is to do not block your app's startup, but still keep that the list of fingerprints is up to date. The periodicity of the updates are determined automatically by the CertStore
, but don't worry, we don't want to eat your users' data plan :)You can optionally provide the completion dispatch queue for scheduling the completion block. This may be useful for situations when you're calling update from other than "main" thread (for example, from your own networking code). The default queue for the completion is .main
.
The CertStore
provides several methods for certificate fingerprint validation. You can choose the one which suits best for your scenario:
// [ 1 ] If you already have the common name (e.g. domain) and certificate fingerprint
let commonName = "yourdomain.com"
let fingerprint = Data(...)
let validationResult = certStore.validate(commonName: commonName, fingerprint: fingerprint)
// [ 2 ] If you already have the common name and the certificate data (in DER format)
let commonName = "yourdomain.com"
let certData = Data(...)
let validationResult = certStore.validate(commonName: commonName, certificateData: certData)
// [ 3 ] You want to validate URLAuthenticationChallenge
let validationResult = certStore.validate(challenge: challenge)
Each validate
methods returns CertStore.ValidationResult
enumeration with following options:
trusted
- the server certificate is trusted. You can continue with the communication
The right response on this situation is to continue with the ongoing TLS handshake (e.g. report .performDefaultHandling to the completion callback)
untrusted
- the server certificate is not trusted. You should cancel the ongoing challenge.
The untrusted result means that CertStore
has some fingerprints stored in its
database, but none matches the value you requested for validation. The right
response on this situation is always to cancel the ongoing TLS handshake (e.g. report
.cancelAuthenticationChallenge
to the completion callback)
empty
- the fingerprints database is empty, or there's no fingerprint for the validated common name.
The "empty" validation result typically means that the CertStore
should update
the list of certificates immediately. Before you do this, you should check whether
the requested common name is what's you're expecting. To simplify this step, you can set
the list of expected common names in the CertStoreConfiguration
and treat all others as untrusted.
For all situations, the right response on this situation is always to cancel the ongoing TLS handshake (e.g. report .cancelAuthenticationChallenge to the completion callback)
The full challenge handling in your app may look like this:
class YourUrlSessionDelegate: NSObject, URLSessionDelegate {
let certStore: CertStore
init(certStore: CertStore) {
self.certStore = certStore
}
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
switch certStore.validate(challenge: challenge) {
case .trusted:
// Accept challenge with a default handling
completionHandler(.performDefaultHandling, nil)
case .untrusted, .empty:
/// Reject challenge
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
The WultraSSLPinning/PowerAuthIntegration
cocoapod sub-spec provides a several additional classes which enhances the PowerAuth SDK functionality. The most important one is the PowerAuthSslPinningValidationStrategy
class, which implements SSL pinning with using fingerprints, stored in the CertStore
. You can simply instantiate this object from the existing CertStore
and set it to the PowerAuthClientConfiguration
. Then the class will provide SSL pinning for all communication initiated from the PowerAuth SDK.
For example, this is how the configuration sequence may look like if you want to use both, PowerAuthSDK
and CertStore
, as singletons:
import WultraSSLPinning
import PowerAuth2
extension CertStore {
/// Singleton for `CertStore`
static var shared: CertStore {
let config = CertStoreConfiguration(
serviceUrl: URL(string: "https://...")!,
publicKey: "BASE64...KEY"
)
return .powerAuthCertStore(configuration: config)
}
}
extension PowerAuthSDK {
/// Singleton for `PowerAuthSDK`
static var shared: PowerAuthSDK {
// Configure your PA...
let config = PowerAuthConfiguration()
config.baseEndpointUrl = ...
// Configure the keychain
let keychain = PowerAuthKeychainConfiguration()
keychain.identifier = ...
// Configure PowerAuthClient and assign validation strategy...
let client = PowerAuthClientConfiguration()
client.sslValidationStrategy = CertStore.shared.powerAuthSslValidationStrategy()
// And construct the SDK instance
guard let powerAuth = PowerAuthSDK(configuration: config, keychainConfiguration: keychain, clientConfiguration: client)
else { fatalError() }
return powerAuth
}
}
serviceUrl
?iOS is using TLS cache for all secure connections to the remote servers. The cache keeps already established connection alive for a while, to speed up the next HTTPS request (see Apple's Technical Q&A for more information). Unfortunately, you don't have the direct control on that cache, so you cannot close already established connection. That unfortunately, opens a small door for the attacker. Imagine this scenario:
Well, not everything's lost. If you're using URLSession
(probably yes), then you can re-create a new URLSession
, because it has its own TLS cache. But all this is not well documented, so that's why we recommend putting the list of fingerprints on the different domain, to avoid this kind of conflicts in the TLS cache at all.
Yes, you can change how much information is printed to the debug console:
WultraDebug.verboseLevel = .all
The library requires several cryptographic primitives, which are typically not available in iOS (like ECDSA). The PowerAuth2
already provides this functions, and most of our clients are already using PowerAuth2 framework in their applications. So, for our purposes, it makes sense to glue both libraries together.
But not everything is lost. The core of the library is using CryptoProvider
protocol and therefore is implementation independent. We'll provide the standalone version of the pinning library later.
All sources are licensed using Apache 2.0 license. You can use them with no restriction. If you are using this library, please let us know. We will be happy to share and promote your project.
If you need any assistance, do not hesitate to drop us a line at [email protected] or our official gitter.im/wultra channel.
If you believe you have identified a security vulnerability with WultraSSLPinning, you should report it as soon as possible via email to [email protected]. Please do not post it to a public issue tracker.
link |
Stars: 56 |
Last commit: 2 weeks ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics