iOS TLS/SSL Pinning using NSRequiresCertificateTransparency key in Info.plist - ios

I want to secure my app against man-in-the-middle (mitm) attacks using SSL Pinning.
By default it is possible to use a proxy like Charles or mitmproxy to intercept traffic, and decrypt it using a self-signed certificate.
After extensive research, I found several options:
Adding NSPinnedDomains > MY_DOMAIN > NSPinnedLeafIdentities to Info.plist
Apple Documentation: Identity Pinning
Guardsquare: Leveraging Info.plist Based Certificate Pinning
Pros: Simple
Cons: App becomes unusable once Certificate/Private Key is renewed (typically after a few months)
Adding NSPinnedDomains > MY_DOMAIN > NSPinnedCAIdentities to Info.plist
Apple Documentation: same as above
Pros: Simple. No failure on Leaf Certificate renewal because Root CAs are pinned instead (expiration dates decades out)
Cons: Seems redundant as most root CAs are already included in the OS
Checking certificates in code URLSessionDelegate > SecTrustEvaluateWithError (or Alamofire wrapper)
Ray Wenderlich: Preventing Man-in-the-Middle Attacks in iOS with SSL Pinning
Apple Documentation: Handling an Authentication Challenge
Medium article: Everything you need to know about SSL Pinning
Medium article: Securing iOS Applications with SSL Pinning
Pros: More flexibility. Potentially more secure. Recommended by Apple (see Apple-link above).
Cons: A more laborious version of (1) or (2). Same Cons as (1) and (2) regarding leaf expirations / root CA redundancies. More complicated.
Adding NSExceptionDomains > MY_DOMAIN > NSRequiresCertificateTransparency to Info.plist
Apple documentation: Section Info.plist keys 'Certificate Transparency'
Pros: Very simple. No redundant CA integration.
Cons: Documentation is unclear whether this should be used for ssl pinning
After evaluation I came to the following conclusion:
Not suitable for a production app because of certificate expiration
Probably best balance between simplicity, security and sustainability – but I don't like the duplication of adding root CAs the system already knows
Too complicated, too risky, any implementation error may lock the app
My preferred way. Simple. Works in my tests but – unclear documentation.
I am tempted to use option (4) but I am not sure if this is really meant for ssl pinning.
In the documentation it says:
Certificate Transparency (CT) is a protocol that ATS can use to identify mistakenly or maliciously issued X.509 certificates. Set the value for the NSRequiresCertificateTransparency key to YES to require that for a given domain, server certificates are supported by valid, signed CT timestamps from at least two CT logs trusted by Apple. For more information about Certificate Transparency, see RFC6962.
and in the linked RFC6962:
This document describes an experimental protocol for publicly logging the existence of Transport Layer Security (TLS) certificates [...]
The terms "experimental protocol" and "publicly logging" raise flags for me and although flipping the feature on in the Info.plist seems to solve SSL pinning I am not sure if I should use it.
I am by no means a security expert and I need a dead simple solution that gives me decent protection while protecting me from choking my own app through possible expired / changed certificates.
My question:
Should I use NSRequiresCertificateTransparency for ssl pinning and preventing mitm-attacks on my app?
And if not:
What should I use instead?
PS:
This same question was essentially already asked in this thread:
https://developer.apple.com/forums/thread/675791
However the answer was vague about NSRequiresCertificateTransparency (4. in my list above):
Right, Certificate Transparency is great tool for verifying that a provided leaf does contain a set of SCTs (Signed Certificate Timestamp) either embedded in the certificate (RFC 6962), through a TLS extension (which can be seen in a Packet Trace), or by checking the OCSP logs of the certificate. When you make a trust decision in your app, I would recommend taking a look at is property via the SecPolicyRef object.
Additional side note:
My expectation from Apple as a security-aware company would have been, that pinning to root CAs was enabled by default, and that I would have to add exceptions manually, e.g. allow proxying with Charles on debug builds.
I hear Android does it that way.

I'm using SecTrustEvaluateWithError to evaluate the certificate. In case if certificate expired or any another case where evaluate return an error, I getting new one from the server. Certificate is stored and received witch keychain. One of the problem I faced with this solution was updating existing certificate at keychain because in apple documentation way to do it, is by using kSecValueRef but that one returns error whenever you try to update it. Instead cert is saved with kSecValueData.
So solution nr 3 (kind of) is used here, but in my case there is a socket connection instead.
First I connect to the socket with settings using CocoaAsyncSocket library
GCDAsyncSocketManuallyEvaluateTrust: NSNumber(value: true),
kCFStreamSSLPeerName as String: NSString("name")
next I use delegate to receive trust object
public func socket(_ sock: GCDAsyncSocket, didReceive trust: SecTrust, completionHandler: #escaping (Bool) -> Void)
next evaluate with existing one (from keychain), or update cert and repeat evaluation
if let cert = CertificateManager.shared.getServerCertificate() {
SecTrustSetAnchorCertificates(trust, [cert] as NSArray)
SecTrustSetAnchorCertificatesOnly(trust, true)
var error: CFError?
let evaluationSucceeded = SecTrustEvaluateWithError(trust, &error)
guard evaluationSucceeded else {
CertificateManager.shared.updateCertificate()
return
}
completionHandler(evaluationSucceeded)
} else {
CertificateManager.shared.updateCertificate()
}
Method for getting certificate is just a regular URLSession dataTask on domain that has certificate with URLSessionDelegate I get a URLAuthenticationChallenge from that object you can retrieve certificate and save it to keychain.
There is info from apple documentation how to store certificate
Best if you read thru it, but how I mentioned above I faced problems with that solution with updating existing one so there there are methods that I use for saving and retrieving certificate
add as data:
public func saveServerCertificate(_ certificate: SecCertificate, completion: #escaping () -> Void) throws {
let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: attribute]
let status = SecItemCopyMatching(query as CFDictionary, nil)
switch status {
case errSecItemNotFound:
let certData = SecCertificateCopyData(certificate) as Data
let saveQuery: [String: Any] = [kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: attribute,
kSecValueData as String: certData]
let addStatus = SecItemAdd(saveQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
completion()
case errSecSuccess:
let certData = SecCertificateCopyData(certificate) as Data
let attributes: [String: Any] = [kSecValueData as String: certData]
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard updateStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
completion()
default:
throw KeychainError.unhandledError(status: status)
}
}
get and create certificate with data:
public func getServerCertificate(completion: #escaping (SecCertificate) -> Void) throws {
let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: attribute,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
switch status {
case errSecItemNotFound:
throw KeychainError.noCertificate
case errSecSuccess:
guard let existingItem = item as? [String : Any],
let certData = existingItem[kSecValueData as String] as? Data
else {
throw KeychainError.unexpectedCertificateData
}
if let certificate = SecCertificateCreateWithData(nil, certData as CFData) {
completion(certificate)
} else {
throw KeychainError.unexpectedCertificateData
}
default:
throw KeychainError.unhandledError(status: status)
}
}

Additional resource for you is OWASP. It is good to follow their recommendations for all of your platforms.
https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning
As for your points:
The public key does not always change when the certificates change (leaf certs are usualy rotated yearly?). This is one of the advantages of pinning against the public key.
'Cons: Seems redundant as most root CAs are already included in the OS'
As you said here, pinning against the root cert is pointless, as the certificate is likely already trusted by the OS.
However, from the doc you linked to: 'A pinned CA public key must appear in a certificate chain either in an intermediate or root certificate. Pinned keys are always associated with a domain name, and the app will refuse to connect to that domain unless the pinning requirement is met.'
You would be pinning against the intermediate certificate here. For peace of mind you can do a test to print out the public key of your root certs + intermediate certs and prove they don't match.
'Too complicated, too risky, any implementation error may lock the app.' Apple provides an implementation in their tech note, and you can test all the codepaths yourself manually. Also, as owasp recommends, you can look into trust kit. I seems it has an implementation of this, and again here you have the option to just pin against the intermediate certificate (NOT ROOT) vs leaf node certs. Intermediate certificates typically last for 5-10 years.
I would hold off on this as personally I am not sure, I think this might just be an additional check you might want to use in addition to cert pinning. There also does not seem to be mention of this in the owasp document.
Personally, if I was writing a new app, I would go with option 1. Since android N, the OS provides a similar approach, which means you can also stay in sync with your android counterparts and they will also only have to update when you do and vice versa. https://developer.android.com/training/articles/security-config.html#CertificatePinning
I am not a security expert, but I am giving my thoughts based on my experience working for large coorperations that have their applications penetration tested. If you are really working on an app that requires high security, you really should have a penetration tester test your application. If you are in a big company, you may have a cyber team that can help.

No, you can’t do ssl certificate pining by using NSRequiresCertificateTransparency, it’s uses for client side TLS. If you want to implement pinning ,you can use server certificate pining to prevent MITM attacks.
Certificate pining
The difference is bellow
1) Client-side certificate transparency
For iOS apps, turning on client-side certificate transparency check is rather simple – you do nothing! Certificate transparency is enforced by default on devices running iOS 12.1.1 and higher. For devices running earlier versions of the iOS, you will need to set the NSRequiresCertificateTransparency option to YES in your Info.plist file.
2) Server-side certificate transparency
Certificate transparency has two aspects:
Pin the certificate: You can download the server’s certificate and bundle it into your app. At runtime, the app compares the server’s certificate to the one you’ve embedded.
Pin the public key: You can retrieve the certificate’s public key and include it in your code as a string. At runtime, the app compares the certificate’s public key to the one hard-coded in your code.
An SSL certificate with an SCT is definitely required. Make sure your server certificate is one with a valid SCT. Almost every CA these days issue certificates with SCTs

Related

How to protect SecIdentity with biometrics/kSecAttrAccessControl?

Here's the repo where this can be easily reproduced: https://github.com/haemi/SecIdentityBiometrics
What I try to achieve is generating a RSA 2048bit key pair, where the public key is sent to the server. The server sends a x.509 certificate back.
With my private key and the certificate, I need to create a SecIdentity (used for URLAuthChallenge). This works when I do not use biometrics to protect the private key.
But as soon as I add biometrics, I'm unable to fetch the SecIdentity. I tried with two variants:
enum Variant {
case writeIdentityWithBiometrics
case writePrivateKeyWithBiometrics
}
writeIdentityWithBiometrics
Here I add kSecAttrAccessControl to the private key directly and try to read the SecIdentity in the end.
writePrivateKeyWithBiometrics
Here I first add the keypair, then the certificate to the keychain - everything without biometrics. Then I read the SecIdentity and try to add it with kSecAttrAccessControl.

How to verify X.509 certificate was signed by another certificate?

The story: I call a request where I am getting a JWS token which I parse with the JOSESwift library. In the response I have a x5u parameter, which is a URL pointing to a certificate, which was used to sign the payload. I download the certificate and using JOSESwift I verify that. With some simplification I am doing the following:
let serverCert = // Getting the downloaded certificate as a SecCertificate object
var publicKey: SecKey!
publicKey = SecCertificateCopyKey(serverCert)
let rsaVerifier = RSAVerifier(algorithm: .ES256, publicKey: publicKey)
if let headerVerifier = Verifier(verifyingAlgorithm: .RS256, publicKey: rsaVerifier.publicKey) {
do {
_ = try jws.validate(using: headerVerifier)
print("Verifying success")
} catch {
print("Verifying failed with error:", error)
}
}
Question1: This is working nice so far. Now I want to verify that the downloaded certificate was indeed signed by a specific certificate I store locally in my application. And that's where I am stuck, that I am unable to find out how it could be done.
Question2: A requirement for the functionality is that the locally stored certificate can be self-signed, but also it can be some other certificate from a chain. So basically I should trust the locally stored certificate regardless it is self-signed or not. Is it doable, or when verifying we should always know about the root certificate ?
[rootCert] <--signed by-- [localCert] <--signed by-- [receivedCert] // We have only the local and receivedCert
My thoughts/ What I tried: My first thought was to use a SecTrust object, with setting the locally stored certificate as the trust anchor, and use the SecTrustEvaluateWithError(_:_:) to check if it's is a correct chain(what I conclude eventually will check also if the received certificate was signed by the one I store locally). I have seen working this in SSLPinning implementations, but somehow it is returning me false, even though I double checked that the certificates are correct. Here is what I am doing:
let serverCert = // Getting my server cert as a SecCertificate object
let localCert = // Getting my server cert as a SecCertificate object
let policy = SecPolicyCreateBasicX509()
var optionalTrust: SecTrust?
let status = SecTrustCreateWithCertificates([serverCert] as AnyObject,
policy,
&optionalTrust)
guard status == errSecSuccess else { return }
let trust = optionalTrust! // Safe to force unwrap now
SecTrustSetAnchorCertificates(trust, [localCert] as CFArray)
var error: CFError?
print("Veryfing result: ", SecTrustEvaluateWithError(trust, &error))
print(error)
Is it a good approach to verify that the received certification was signed by the local one ? If yes than what I am doing wrong that I am getting false ? If the approach is bad, what else can I try here ?
I found a similar question, but it is in Java(but maybe can give a hint what I am trying to accomplish): Verify x509 signature Java. However it also mentions the root certificate, so it again rises me the Question2 if it's doable without the root/self-signed certificate.
Update
As suggested in the comments, I am printing the error I am getting from SecTrustEvaluateWithError:
Error Domain=NSOSStatusErrorDomain Code=-25318 "“AuthInfoKey:18”
certificate is not trusted"
UserInfo={NSLocalizedDescription=“AuthInfoKey:18” certificate is not
trusted, NSUnderlyingError=0x280efb9c0 {Error
Domain=NSOSStatusErrorDomain Code=-25318 "Certificate 0
“AuthInfoKey:18” has errors: Unable to build chain to root (possible
missing intermediate);" UserInfo={NSLocalizedDescription=Certificate 0
“AuthInfoKey:18” has errors: Unable to build chain to root (possible
missing intermediate);}}}
The issues seems reasonable as I do not have access to the root certificate as mentioned above, I want to verify an intermediate certificate.
So again my question is: it possible to check the serverCert(an intermediate certificate) was signed by my local certificate(an other intermediate certificate) with SecTrustEvaluateWithError ? For me it seems it is working only if one has also the root certificate.

Alamofire 5 AF.upload() fails to send an image to a server with an invalid certificate

I am sending an image, using the following call:
AF.upload(...)
to a "Development server" that has an invalid certificate, and consequently, I get the following error:
NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be ...
I have already solved this problem for "AF.request(...)" calls; that is, I can perform "AF.request(...)" calls to a server with an invalid certificate using the following code:
#if DEBUGDEV
//To enable connections with wrong certificate
private let session: Session = {
let evaluators: [String: ServerTrustEvaluating] = [
"api.my.server.dev.api.group.com": DisabledEvaluator()
]
let manager = ServerTrustManager(allHostsMustBeEvaluated: false,
evaluators: evaluators)
let configuration = URLSessionConfiguration.af.default
return Session(configuration: configuration,
serverTrustManager: manager,
eventMonitors: [ AlamofireLogger() ])
}()
#else
private let session: Session = Session.default
#endif
And then calling:
let request = self.session.request(urlConvertible)
In addition I have modified the Info.plist file to contain "NSAppTransportSecurity -> NSExceptionDomains -> "api.my.server.dev.api.group.com" -> "NSThirdPartyExceptionAllowsInsecureHTTPLoads = false" and other entries that can be found in other StackOverFlow" posts.
However; the previous code works for "AF.request(...)", but not for "AF.upload(...)", so I would need a way to make "AF.upload(..)" work properly for servers with and invalid certificate.
Is it a way to "insert" the "Session" inside the "AF.upload(...)" call ? or
Is another way so that "AF.upload(...)" can connect to servers with an invalid certificate?
After reviewing and updating my question, I have realised that the answer is easy; that is, in the same way I do not call
AF.request(...)
but
self.session.request(...)
I can do the same with upload; that is, to call:
self.session.upload(...)
instead of calling:
AF.upload(...)

Sandbox apple pay testing handshake failure

I am having trouble validating the merchant in my apple pay sandbox environment. Taken from https://developer.apple.com/reference/applepayjs/applepaysession#2166532, once my server then calls the Start Session endpoint at the provided URL, I get a 500 error.
I've dug around and this 500 error is happening somewhere in the network layer. As listed on the apple page (https://developer.apple.com/reference/applepayjs/), I need the following requirements met:
All pages that include Apple Pay must be served over HTTPS. done, server has ssl/https sitewide
To enable merchant validation, your server must allow access over HTTPS (TCP over port 443) to the Apple Pay IP addresses provided in Listing 1 below. done, server is open to all ips on port 443
Your server must support the Transport Layer Security (TLS) 1.2 protocol and one of the cipher suites listed in Table 1. server does support tls 1.2, since I send requests on tls 1.2 to apple pay's development server (below)
I've been using Wireshark to check what's going on, and I seem to be failing once the server is in the ChangeCipherSpec phase, after the server sends back the cipher spec to the client. (Reference for ssl procedure: https://support.f5.com/csp/article/K15292). As you can see from my image, I'm communicating to the apple pay sandbox server, passing in the same supported tls protocol and cipher suite that the error would suggest -> Handshake Failure (40), so something else is going on and I don't know where to look
If you look at the ServerHello message, you can see the server found and accepted the cipher suite that matches the client, which also matches one of the required ciphers that apple pay supports
I can add other details as necessary
The issue was that our server did not have TLS 1.2 enabled by default. Enabling TLS 1.2 and disabling TLS 1.0 fixed the issue - Win 2008
edit
There are a few things that needed to happen. Our server was on .net 4.5, which does not use tls 1.2 by default (apple requires tls 1.2 to be used). So, we upgraded our solution to .net 4.6, and also forced tls 1.2 for our request. Additionally, we have to include the merchant id certificate in our request to apple (which isn't mentioned in the docs very well).
You can find the github repo of the source I used here (https://github.com/justeat/ApplePayJSSample), but here is my code that I needed to put in my solution to make things work (I also had to export my merchant certificate from my mac's keychain that gave me a .p12 file. I imported this .p12 file into my server's computer certificate store)
[System.Web.Http.HttpPost]
public async Task<ContentResult> GetApplePaySession([FromBody] string url)
{
// http://stackoverflow.com/a/36912392/1837080
System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
// Load the merchant certificate for two-way TLS authentication with the Apple Pay server.
var certificate = LoadMerchantCertificate();
// Get the merchant identifier from the certificate to send in the validation payload.
var merchantIdentifier = GetMerchantIdentifier(certificate);
// Create the JSON payload to POST to the Apple Pay merchant validation URL.
var payload = new ApplePayRequest()
{
merchantIdentifier = merchantIdentifier,
domainName = System.Web.HttpContext.Current.Request.Url.Host,
displayName = "[display name from apple developer portal]"
};
JObject merchantSession;
// Create an HTTP client with the merchant certificate
// for two-way TLS authentication over HTTPS.
using (var httpClient = CreateHttpClient(certificate))
{
var jsonPayload = JsonConvert.SerializeObject(payload);
using (var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"))
{
// POST the data to create a valid Apple Pay merchant session.
using (var response = await httpClient.PostAsync(url, content))
{
response.EnsureSuccessStatusCode();
// Read the opaque merchant session JSON from the response body.
var merchantSessionJson = await response.Content.ReadAsStringAsync();
merchantSession = JObject.Parse(merchantSessionJson);
}
}
}
// Return the merchant session as JSON.
return Content(merchantSession.ToString(), "application/json");
}
#region Apple Pay helper methods
private X509Certificate2 LoadMerchantCertificate()
{
X509Certificate2 certificate;
// Load the certificate from the current user's certificate store. This
// is useful if you do not want to publish the merchant certificate with
// your application, but it is also required to be able to use an X.509
// certificate with a private key if the user profile is not available,
// such as when using IIS hosting in an environment such as Microsoft Azure.
using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
store.Open(OpenFlags.ReadOnly);
// when using thumbprint from mmc, look at:
// http://stackoverflow.com/a/14852713
// there is a hidden character that you must delete
var certificates = store.Certificates.Find(
X509FindType.FindByThumbprint,
"[thumbprint]",
validOnly: false);
if (certificates.Count < 1)
{
throw new InvalidOperationException(
// ReSharper disable once UseStringInterpolation
string.Format(
"Could not find Apple Pay merchant certificate with thumbprint '{0}' from store '{1}' in location '{2}'.",
"‎[thumpprint]", store.Name, store.Location));
}
certificate = certificates[0];
}
return certificate;
}
private string GetMerchantIdentifier(X509Certificate2 certificate)
{
// This OID returns the ASN.1 encoded merchant identifier
var extension = certificate.Extensions["1.2.840.113635.100.6.32"];
// Convert the raw ASN.1 data to a string containing the ID
return extension == null ? string.Empty : Encoding.ASCII.GetString(extension.RawData).Substring(2);
}
private HttpClient CreateHttpClient(X509Certificate2 certificate)
{
var handler = new WebRequestHandler();
handler.ClientCertificates.Add(certificate);
return new HttpClient(handler, disposeHandler: true);
}
#endregion
I just recently went through this. For me I had to merge the PEM and KEY files into a PFX. Then I was able to run the start session call from ubuntu 16.04 using .net core 2.1
private HttpClient CreateHttpClient()
{
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2(#"/path/yourcombinedpfx.pfx"));
return new HttpClient(handler); ;
}

How to implement server-side sever name indication using Security.framework under OS X/iOS

I am trying to implement server name indication on the server side of a OS X application: The server should pick a certificate based on the peer host name provided by the client.
Does anybody know whether this can be achieved using the Security.framework?
SSLSetCertificate takes only one leaf certificate and I cannot find any callback for providing a certificate based on a host name.
In openSSL for example, there is the SSL_CTX_set_tlsext_servername_callback for this purpose.
Any help is greatly appreciated.
There is a new feature in OS X 10.11 (El Capitan) which makes this possible. Sadly, there is currently zero documentation on this feature, but I nevertheless found out how it works:
You have to enable the new option kSSLSessionOptionBreakOnClientHello on your SSL context with:
SSLSetSessionOption(context, kSSLSessionOptionBreakOnClientHello, YES);
This causes the handshake to break after is has received the hostname from the client and it returns the status errSSLClientHelloReceived. You can then get the hostname, use it to look up the appropriate certificate and apply the certificate to the context. Then you can continue the handshake.
OSStatus status = SSLHandshake(context);
// ...
if(status == errSSLClientHelloReceived)
{
size_t hostnameLength;
SSLGetPeerDomainNameLength(context, &hostnameLength);
char hostname[hostnameLength];
SSLGetPeerDomainName(context, hostname, &hostnameLength);
SecIdentityRef cert = ... ; // Look up certificate using hostname
SSLSetCertificate(context, (__bridge CFArrayRef)#[(__bridge id) cert]);
// Repeat from start by calling SSLHandshake
}

Resources