iOS: Creating a server socket for mutual SSL authentication using GCDAsyncSocket - ios

I'm pulling my hair out on this one and would greatly appreciate some assistance. Unfortunately my experience with SSL is quite limited so I don't know where I'm going wrong.
I need to develop an iOS app that acts as an server with mutual SSL authentication with the client. I'm using the GCDAsyncSocket library and I've managed to get the server part working fine (without SSL), however, I'm stuck getting the SSL working. I've had a look at this post, however, it doesn't explain the steps I need to perform to achieve what I want.
In terms of the certificate setup, I have the following p12 certificates:
Self-signed Server Root CA certificate
Server Certificate issued by the CA above (ServerCert).
Self-signed Client Root CA certificate
Client Certificate issued by the CA above (ClientCert).
I then installed both Root CA certificates on the iOS device and bundled the serverCert certificate in my app. When a new socket is accepted, I extract the certificate info from the p12 file (according to the apple certificate programming guide), and setup the SSL settings as follows prior to starting the SSL session:
-(NSDictionary*) loadSSLSettings
{
// Configure SSL/TLS settings
NSMutableDictionary *settings = [NSMutableDictionary dictionaryWithCapacity:4];
// Configure this connection as the server
[settings setObject:[NSNumber numberWithBool:YES]
forKey:(NSString *)kCFStreamSSLIsServer];
CFArrayRef certsArray = [self loadCertificates];
[settings setObject:(id)CFBridgingRelease(certsArray) forKey:(NSString *)kCFStreamSSLCertificates];
[settings setObject:NSStreamSocketSecurityLevelNegotiatedSSL forKey:(NSString *)kCFStreamSSLLevel];
[settings setObject:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamSSLAllowsAnyRoot];
return settings;
}
-(CFArrayRef) loadCertificates
{
// Load Certificate
NSString *path = [[NSBundle mainBundle] pathForResource:#"ServerCert" ofType:#"p12"];
NSData *p12data = [NSData dataWithContentsOfFile:path];
CFDataRef inP12data = (__bridge CFDataRef)p12data;
SecIdentityRef myIdentity;
SecTrustRef myTrust;
extractIdentityAndTrust(inP12data, &myIdentity, &myTrust);
SecCertificateRef myCertificate;
SecIdentityCopyCertificate(myIdentity, &myCertificate);
const void *certs[] = { myCertificate };
CFArrayRef certsArray = CFArrayCreate(NULL, certs, 1, NULL);
return certsArray;
}
This code seems load the certificate fine, but then when I make a client connection (this is without even trying mutual SSL) using openSSL, such as:
openssl s_client -connect 192.168.2.8:1700 -state -debug
I get the following output:
> SSL_connect:before/connect initialization
> SSL_connect:SSLv2/v3 write client hello A
> SSL_connect:error in SSLv2/v3 read server hello A
And the iOS log produces this from the GCDAsyncSocket library:
Error in CFStreamSetProperty, code: 8
All I can tell is that code 8 is 'Other'.
I'm at a loss at to what is going wrong...perhaps I'm doing something fundamentally wrong, and if so I'd greatly appreciate somebody pointing it out :)
Also, once I get past this step, how do I go about validating the client certificate when it sends one through?
If I've left out any vital information, please let me know and I'll happily add it.
Thanks!

Related

iOS Swift Pin PEM Certificate to Run REST POST Request

I've been trying to use Alamofire to pin a PEM certificate using these methods:
SSL pinning in iOS - Swift edition
Adding certificate pinning to your iOS app with Alamofire
I've also tried using AFNetworking:
NSData *certificateData = [NSData dataWithContentsOfFile:certificatePath];
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef) certificateData);
NSArray *arr = [[NSArray alloc] initWithObjects:(__bridge_transfer NSData *)SecCertificateCopyData(cert), nil];
[securityPolicy setPinnedCertificates:arr];
Most of the time I will get -999 and -1012 errors. When running a curl command, it works as it should. However, it does not seem to work when converting the PEM certificate to a DER format.

How to create .cer file for AFNetworking to be used in iOS app bundle

I keep seeing disjointed accounts all over the web of what the .cer file is and how to generate it so that the app can properly communicate with the server via https.
To quote this source, one person says:
1) You now need .cer files in your bundle for all certificates in the chain. So, at least, two certificate files must be present (your certificate and the root CA). In our case, we needed an intermediate as well.
2) These certificates must be in a particular order. Specifically, the certificate that certifies the host that you are communicating with must be first in the list of pinned certificates/keys. It doesn't look like the order of the rest of them matter. This is because AFSecurityPolicy uses the first object in the array to validate the host name.
3) The host validation fails for wildcard certificates if you are communicating with the root domain. For example, consider a certificate pointed at *.foo.com. If you are communicating with foo.com, the host check will fail because the host component counts will not match. I understand the need for this early optimization, but it fails before it even reaches the logic that specifically handles wildcard domains.
Official documentation backs that up here.
I suppose that in order to generate this, one must cat the contents of their whole cert chain into a .cer file in a specific order. I remember seeing this posted somewhere, but can't seem to find it.
The question is, what is the undebatable method of creating a .cer file for AFNetworking?
UPDATE:
After more research, it seems to me that you simply take the .crt file and perform this on it:
openssl x509 -in www.mydomain.com.crt -out www.mydomain.com.cer -outform der
However, even after doing this and attaching the .cer to my app bundle, I get an error here:
+ (NSArray *)pinnedPublicKeys {
static NSArray *_pinnedPublicKeys = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *pinnedCertificates = [self pinnedCertificates];
NSMutableArray *publicKeys = [NSMutableArray arrayWithCapacity:[pinnedCertificates count]];
for (NSData *data in pinnedCertificates) {
SecCertificateRef allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data);
NSParameterAssert(allowedCertificate);
The error comes in at the last line. I suppose that in the preceding line where it attempts to assign an allowedCertificate, SecCertificateCreateWithData has the following description:
/*!
#function SecCertificateCreateWithData
#abstract Create a certificate given it's DER representation as a CFData.
#param allocator CFAllocator to allocate the certificate with.
#param certificate DER encoded X.509 certificate.
#result Return NULL if the passed-in data is not a valid DER-encoded
X.509 certificate, return a SecCertificateRef otherwise.
*/
Clearly it is returning a NULL, but I don't know why. My specific certificate is in the correct format. However, I did notice that there were other 'certificates' in the pinnedCertificates array, but I have no idea where it is getting them. Nor can I seem to find them or print them out. As far as I know, I only have the one in the app bundle, but it seems to show more than that.
The error generated from the assert on the last line is:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: allowedCertificate'
Assuming you are using AFNetworking 2.5 the steps described below:
Install valid certificate on your server
Create .cer file openssl x509 -in www_yourdomain_com.crt -out www_yourdomain_com.cer -outform der
Add .cer file to app project. IMPORTANT: Never add private key to your project!!!
Setup AFNetworking secure policy
- (AFSecurityPolicy *)securityPolicy {
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
[securityPolicy setAllowInvalidCertificates:NO];
[securityPolicy setValidatesDomainName:YES];
return securityPolicy;
}
Set policy on network manager when making network call
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager setSecurityPolicy:[self securityPolicy]];
There's no need to manually load .cer file, if you're using policyWithPinningMode:pinningMode method to create security policy AFNetworking automatically pins them.

Overriding TLS server validation hostname doesn't seem to be working

I'm trying to connect my app to a development server that has a server certificate with the wrong hostname, but it is signed by a trusted anchor certificate. When I evaluate the server trust object, it fails as expected. I am trying to change the trust evaluation policy for the server's hostname, but it doesn't seem to help.
// In -connection:willSendRequestForAuthenticationChallenge:
// NSURLAuthenticationMethodServerTrust
SecTrustRef trust = [challenge.protectionSpace serverTrust];
SecTrustResultType trustResult;
SecTrustEvaluate(trust, &trustResult);
// trustResult == kSecTrustResultRecoverableTrustFailure
SecPolicyRef policyOverride = SecPolicyCreateSSL(true, (CFStringRef)#"devhost");
CFArrayRef policies = (CFArrayRef)#[policyOverride];
SecTrustSetPolicies(trust, policies);
CFRelease(policyOverride);
SecTrustEvaluate(trust, &trustResult);
// trustResult == kSecTrustResultRecoverableTrustFailure
As far as I understand, the second time I call SecTrustEvaluate(), it should be returning kSecTrustResultUnspecified. I have connected to the dev server using "devhost" when I initialized the NSURLConnection, and challenge.protectionSpace.host == #"devhost" as well. What am I doing wrong here?
I was using the wrong host name in the call to SecPolicyCreateSSL.
When using SecPolicyCreateSSL to override hostname validation, the hostname argument should be one that matches the certificate you're validating. Then the validation pretends that the host you're communicating with has the newly specified hostname.
In my case, the server has a certificate for "*.mydomain.tld", so I call
SecPolicyCreateSSL(true, (CFStringRef)#"devhost.mydomain.tld");
and then the certificate chain can be successfully validated.

iOS connect with self-signed TLS/SSL certificate not working

I would like to encrypt and secure a network connection agains MITM attacks for an iOS application. Since the application will only connect to one server, there is no need to have the certificate signed by a CA like VeriSign. I want to self-sign the certificate and distribute it with the application.
I tried this but end up with kSecTrustResultRecoverableTrustFailure and can not figure out where I went wrong. Can someone look over it and identify the problem or point me into a direction on how to debug this? Is it a problem because I use/test on localhost?
I think it is a problem in the creation of the certificate or setup of the server but I don't know what it is. I tested it with openssl s_client and it seems to work but iOS does not accept it (see below). I could accept kSecTrustResultRecoverableTrustFailure as success but would rather avoid it.
Creation of certificate
My openssl.cnf. The last line specifies subjectAltName and should be the only important one.
[ req ]
default_bits = 2048 # Size of keys
default_keyfile = key.pem # name of generated keys
default_md = sha256 # message digest algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
# Variable name Prompt string
# #---------------------- ----------------------------------
0.organizationName = Organization Name (company)
organizationalUnitName = Organizational Unit Name (department, division)
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
# Default values for the above, for consistency and less typing.
# Variable name Value
#------------------------------ ------------------------------
0.organizationName_default = The Sample Company
localityName_default = Metropolis
stateOrProvinceName_default = New York
countryName_default = US
[ server ]
basicConstraints = critical,CA:FALSE
keyUsage = digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
nsCertType = server
subjectAltName = IP:127.0.0.1,DNS:localhost
This is how I create the certificate. I use sha256 since md5 seems not supported. Afterwards I transform the certificate to DER format which iOS needs.
macbook:~/Documents/app/https-test/cert$ openssl req -x509 -sha256 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 356 -nodes -config openssl.cnf
Generating a 2048 bit RSA private key
..+++
............................................................+++
writing new private key to 'key.pem'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:DE
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:com
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:
Email Address []:
macbook:~/Documents/app/https-test/cert$ ls
cert.der cert.pem key.pem openssl.cnf
macbook:~/Documents/app/https-test/cert$ openssl x509 -in cert.pem -outform der -out cert.der
Server
The server is a node.js server that accepts https requests.
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('../cert/key.pem'),
cert: fs.readFileSync('../cert/cert.pem')
};
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("test return\n");
}).listen(8443);
I tested this server with the following output:
macbook:~/Documents/app/https-test/server$ openssl s_client -showcerts -host localhost -port 8443 -CAfile ../cert/cert.pem
CONNECTED(00000003)
depth=0 /O=The Sample Company/L=Metropolis/ST=New York/C=US
verify return:1
---
Certificate chain
0 s:/O=The Sample Company/L=Metropolis/ST=New York/C=US
i:/O=The Sample Company/L=Metropolis/ST=New York/C=US
-----BEGIN CERTIFICATE-----
MIIDIDCCAggCCQClnXQ2tGOF1jANBgkqhkiG9w0BAQsFADBSMRswGQYDVQQKExJU
aGUgU2FtcGxlIENvbXBhbnkxEzARBgNVBAcTCk1ldHJvcG9saXMxETAPBgNVBAgT
CE5ldyBZb3JrMQswCQYDVQQGEwJVUzAeFw0xNDAyMjQwMDEwMTJaFw0xNTAyMTUw
MDEwMTJaMFIxGzAZBgNVBAoTElRoZSBTYW1wbGUgQ29tcGFueTETMBEGA1UEBxMK
TWV0cm9wb2xpczERMA8GA1UECBMITmV3IFlvcmsxCzAJBgNVBAYTAlVTMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzudBEZmW78S9EnxQzObf778qIBRf
/pUPVKSC31/8iVLM3w71GtHxI39Gt+WwAhMRRKmO+EhsFWDmQZfg3GNsws4Uj/uO
8I6Xp9rF7IWBIAZ37X2nUPD/qEU4+SFmiNi8POaXPt+5mQQLYFfun5YzpZPoiPpi
wuIkcgY8mOJlNv7Hr4AnyMtMMnscZis+ELVky5Q/mDsiamHzPPGjjKYKDMfwYj8S
yAz0GLKrcHBBm4Re++mefJU0sdapAYEliAJdTs0aBA5lxcRBzkKlFwxgsQrhtwL9
xBY+RC/PbnVWRF/YVrd7o6JvXmWOPFDlbL99v9tGGjoUyFDeLoIMaqaGmwIDAQAB
MA0GCSqGSIb3DQEBCwUAA4IBAQAdnvu4W6GoWkAALpvpEgXBMKq2sApLHib+i8Be
+LrAS/zA1GxlMqswUBUvtGuQq88oGWC/eU3n3PvRE2tuIARg4ZSGo2/KdYfvOFYy
O7hnwdlAYirdj3XKUnomj0sVgeAjJV4xSha7aOzs9mNyLquJvewBEAvQdJnPRYfS
LwSUq5kbbiHyFWHmJnTUfLpfKj0w+LNO4Jrb0GdFs7ZWq3R0Mscig668Htue4xST
jWEh0f/ZcWLK+UVvTvpMb9DTM8oOV94EHt+slaIMEzD2hWjtLcwGfUzX5qYU450v
Kt1b40tBHRHi8ytstg4qdLlwf0NpXejcLQiW1CgNZoEIBtP+
-----END CERTIFICATE-----
---
Server certificate
subject=/O=The Sample Company/L=Metropolis/ST=New York/C=US
issuer=/O=The Sample Company/L=Metropolis/ST=New York/C=US
---
No client certificate CA names sent
---
SSL handshake has read 983 bytes and written 468 bytes
---
New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
Protocol : TLSv1
Cipher : AES256-SHA
Session-ID: C8901BBE04CB24444E0DDEA60EB7A72A64822E652973AD1D16E27D1E1F29F828
Session-ID-ctx:
Master-Key: D143A0F58C848B0E1BCA7BDF22EEBC326F811961CC10FF3A653715A8D8F96F5825AFC6D200F334D2E1581BFECA940111
Key-Arg : None
Start Time: 1393256956
Timeout : 300 (sec)
Verify return code: 0 (ok)
---
^C
iOS app
The app uses this NSURLConnectionDelegate https://gist.github.com/dhoerl/2051599
The iOS code you linked to expects the server's certificate to be in the device's trusted root certificate store, or at least signed by a trusted root certificate authority. The error you are getting suggests that this is not the case.
That error means the certificate is not trusted. By definition, a self-signed certificate is not trusted because it isn't signed by a trusted root certificate authority (so there's no way to verify that the signer of the certificate is who they say they are).
If you just want the benefits of SSL encryption without the protection from MITM attacks, you can bypass the server check by doing something like the following in the NSURLConnection delegate's didReceiveAuthenticationChallenge method:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
Note that this will not prevent MITM attacks, since you are now allowing a connection to any SSL host, but if you truly want that kind of protection you shouldn't use a self-signed certificate. If you just want the encryption offered by SSL, a self-signed certificate is fine.
That said, you can do server authentication if you are bundling the server certificate in with your application - this is known as certificate pinning. You would need to add code in the didReceiveAuthenticationChallenge method above to compare the server's certificate with the one that's embedded in your application and have it trust ONLY that specific certificate and no other. This of course means that if the certificate on your server ever expires or changes, your clients will no longer be able to connect (until you rebuild and redistribute your application with the new certificate). It also means that if your server's private key is ever stolen or compromised, you won't be able to revoke it and issue a new one, and the Bad Guys will be able to impersonate your server to any clients that try to connect using the compromised key. Using a certificate issued by a trusted root CA avoids both problems, and is still the recommended way to go if you truly need server authentication. That way you can revoke the certificate if you ever need to, issue a new one, and everything will still work.
#1703536 's solution almost did it, I only had to implement another URLConnection's delegate message :
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
Concerns about security doing that are well explained in the other answer ;-)

SSL Pinning on iOS

To improve my app's security and protect the user from MITM attacks I'm trying to do SSL pinning with my self-signed certificate following the content of this post.
So I'm using the following code to compare the certificate that I get from the server with the one that bundled in the app.
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, 0);
NSData *remoteCertificateData = CFBridgingRelease(SecCertificateCopyData(certificate));
NSLog(#"Remote Certificate Data Length: %d",[remoteCertificateData length]);
NSString *cerPath = [[NSBundle mainBundle] pathForResource:#"apache" ofType:#"crt"];
NSData *localCertData = [NSData dataWithContentsOfFile:cerPath];
NSLog(#"Local Certificate Data Length: %d",[localCertData length]);
if ([remoteCertificateData isEqualToData:localCertData]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
}
else {
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
}
The only things that are different between my code and the one in the blog post I linked are the name and the extension (.cer to .crt) for the resource representing my certificate and the two NSLogs I added that will come handy later to show what the problem is.
In fact when this code is executed I get this output:
2013-05-22 16:08:53.331 HTTPS Test[5379:c07] Remote Certificate Data Length: 880
2013-05-22 16:09:01.346 HTTPS Test[5379:c07] Local Certificate Data Length: 1249
Obviously the comparison between the Local and the Remote certificates fails because the length of the data is different and so it also fails the pinning.
Why does this happen and how could I solve this problem?
I had the same issue. The problem is probably because you have not converted your .crt file to the correct format. iOS & OSX are looking for your certificate to be in .der format. You need to use openssl to convert it. Here is a very helpful article on this topic. My public certificate came from an Apache server (I am assuming that yours did as well). After looking over openssl documentation I was able to figure out how to get this to work.
1) Open Terminal and change directory to the location of your .crt.
2) Execute this command:
openssl x509 -in your_cert.crt -outform der -out your_output_name.der
This will create an output file named 'your_output_file.der'. You must import this into your xCode project and reference it in the
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
method of your NSURLConnectionDelegate implementation.
I hope this helps!

Resources