Authors note: This article was written in 2011; it is possible some of the information here is no longer accurate.
Apple have gone to pains to make cryptography in iOS (and MacOS in general) secure, building a layer between applications and the low-level stuff, like OpenSSL. The principle is to keep these functions in separate address space thus significantly reducing the surface area available for malicious code to find a weakness. In iOS this separation is enforced and, significantly, the documentation is sparse and terse. Public key use without also using certificates is mentioned but only in the context of using keys generated on the device. Posts on the Apple Developer forums indicate that using certificates is suggested because using public key pairs is “involved“. It turns out that the reason it’s involved is because of some odd implementation details and the aforementioned lack of documentation or useful examples.
I was developing a mechanism to verify some data that was generated outside the device with a public key. Using a simple key pair generated by OpenSSL at a command line it was very simple to create scripts in Perl and PHP to produce (and sign) and then decode (and validate) some data using this key pair. The functions to add a public or a private key to the keychain are there in iOS but they don’t work as expected.
Public key: It’s all in the format
There are many ways to transmit a key for use by another system but there are also some defacto standards. For an RSA public or private key, which has a pretty straightforward internal structure, that would be defined by PKCS#1.
There are two common forms to transmitting and using an RSA public or private key. ASN.1 DER – which is a non-displayable (i.e., not ASCII) format, or the same thing but base64 encoded and referred to as PEM – which is 7-bit ASCII and thus displayable. The base64 format is usually wrapped with something like -----BEGIN PUBLIC KEY-----
(which is what OpenSSL produces) or -----BEGIN RSA PUBLIC KEY-----
.
You can have OpenSSL generate a key pair very easily and you select the output form right on the command line:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
chrisy@baud:~/keytmp$ openssl genrsa -out testkeypair.pem 1024 Generating RSA private key, 1024 bit long modulus .......++++++ ...........++++++ e is 65537 (0x10001) chrisy@baud:~/keytmp$ more testkeypair.pem -----BEGIN RSA PRIVATE KEY----- MIICXgIBAAKBgQDBm8yuHmd0P6scl48DEi+xp47wXVZaKWRygGKtA2XkdRuCU99f 0Tq07Llcgf8XuR+Wnk+z2CdMMFMzOGhCePblVIAn33dcBVlDokpBF7AnTClsaLci xxZw1LIUiaPaBdN7oG8vt3G2caLHRrrkoEnccY+6GadfH7iuHdcVsz1mowIDAQAB AoGAWEt1TPMQuzNOFfwIfJ4OojaIOZZXi0bVSGLEnaKvFUFTCly1wjzpSRmsb0PZ 0jfa8BXCw4IQae6gAvv2kFoaPjAiohDRzsNL7r5VfWqYh2rvXM7FEa5Zl6EvhHm1 MdLVgqKW2gAN5N1dBqpRvzo0H8zEcbqH7a4gAyQivaxGXgECQQDz59utDOP1VS5L VVnr57M4x99/lrxHNuiTmKdwKtjhB2bZQy2R5SPC7xHF5lFfMOW35tg/6ZjCeEC/ KvPYZXNNAkEAyzV4KKcL4+7S7AZ7LcmraYY2UHFAyGkS/RBVLLaTcGIZOyrw9Pez M+S8kRERO7lblStcptCd4leTtPXY0X1prwJBAOiqk7bXZhmg4SGB0N6lzyRqHfzD GOXCLkilxYvNg8fd3LGCUNUsxVlt3wFufM8WgPxWHJGTT2KrffAelDAoTr0CQQCA 9DSFb8Ru596340EGBIWfmIkdMVGQHIXtTBERJ+eWmNo0HwL8Ibh6BPzY/kC2auFA X10Tiy22NidI3f6yqmiHAkEA1H/bkwBulMSoo1ylLCF1m482ucOY7wWnJ77ARc3X f5KJtsWSDfQiHP1UyJrnlZz+JLWH4fWuFm1ZPHZca38eBg== -----END RSA PRIVATE KEY----- chrisy@baud:~/keytmp$ openssl rsa -in testkeypair.pem -pubout -outform PEM -out testpublic.pem writing RSA key chrisy@baud:~/keytmp$ more testpublic.pem -----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBm8yuHmd0P6scl48DEi+xp47w XVZaKWRygGKtA2XkdRuCU99f0Tq07Llcgf8XuR+Wnk+z2CdMMFMzOGhCePblVIAn 33dcBVlDokpBF7AnTClsaLcixxZw1LIUiaPaBdN7oG8vt3G2caLHRrrkoEnccY+6 GadfH7iuHdcVsz1mowIDAQAB -----END PUBLIC KEY----- |
Sign here
So far so good. In PHP we can use the PEM encoded keys directly. For example, to acquire the signature of some data, one could do this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php function sign_data($data, $rsaprivkeyfile) { $rsakey = openssl_pkey_get_private($rsaprivkeyfile); if(!$rsakey) return false; $signed_data = false; if(!@openssl_sign($data, $signed_data, array($rsakey, ''))) return false; return "\n-----BEGIN DATA-----\n". chunk_split(base64_encode($license_data)). "-----END DATA-----\n". "-----BEGIN SIGNATURE-----\n". chunk_split(base64_encode($signed_data)). "-----END SIGNATURE-----"; } ?> |
The format being returned is not a standard, it’s something I made up, but it’s easily transmitted and base64 implementations are pretty universal.
How would we verify the integrity of this message? Here’s a crude example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?php function verify_message($message, $rsapubkeyfile) { $rsakey = openssl_pkey_get_public($rsapubkeyfile); if(!$rsakey) return false; // Extract the two sections $lines = explode("\n", $message); $a_dat = ''; $a_sig = ''; $f_dat = false; $f_sig = false; foreach($lines as $line) { $l = trim($line); if($l == '-----BEGIN DATA-----') { $f_dat = true; $f_sig = false; } else if($l == '-----END DATA-----') { $f_dat = false; $f_sig = false; } else if($l == '-----BEGIN SIGNATURE-----') { $f_dat = false; $f_sig = true; } else if($l == '-----END SIGNATURE-----') { $f_dat = false; $f_sig = false; } else if($f_dat) { $a_dat .= $line."\n"; } else if($f_sig) { $a_sig .= $line."\n"; } } if(!$a_dat || empty($a_dat)) return false; if(!$a_sig || empty($a_sig)) return false; $data = @base64_decode($a_dat); $sig = @base64_decode($a_sig); // Validate signature $result = @openssl_verify($data, $sig, $rsakey); if(!$result) return false; return true; } ?> |
With this little nugget of code it becomes trivial to find out if someone has been tampering with the data. This is useful, for example, for software licenses. Assuming the private key is kept secure you have a degree of trust that anything signed by it is also secure.
So what’s the problem with iOS?
iOS provides a security framework that should work with key pairs. However, if you try to use it for keys that were generated elsewhere then it simply doesn’t work. Worse, it gives only very limited indications why it doesn’t work.
iOS does not provide documented direct access to the OpenSSL API. If there is such an API there it wouldn’t matter – being undocumented means that it would face App Store rejection if their static analysis showed use of the undocumented library.
You could statically link an OpenSSL library that you compiled and it would work but in that situation it’s your app providing the cryptography code and thus you will need to jump some hurdles (you may need a CCATS) to get export clearance for your app (although it’s not that simple in general – read the Export Compliance FAQ in iTunes Connect). In my case, I’m using a signed software license to prevent piracy – digital rights management – and thus does not need US Government clearance – but only if I include only enough code to perform that function. If I link against the whole of OpenSSL then what my app is doing will be less clearly defined.
So the key to a simple life is to work out why the provided functions don’t work.
What’s in a key?
It comes down to format.
The first clue is that if you use iOS to generate a key pair then the resulting form is not ASCII. So that rules out PEM and means that if you have a PEM formatted key, you’ll also need a base64 decoder – which iOS does not provide. There’s plenty of them around. I used NSData+Base64.m
which if you Google for you will find many varied implementations and derivations of.
But that is not enough, even though an Apple employee confirms on the dev forums that iOS wants a PKCS#1 key and if you’re doing signature verification, that the signature is of the SHA1 hash of the data, as I would expect.
The next clue came when I noticed the keys produced by iOS don’t have the normal binary ASN.1 preamble to identify itself as a PKCS#1 key. This prompted some searching and I came across this post talking exactly about this issue. I have no idea why it did not come up in my earlier searching. In it Berin explains his similar discovery, but also that the payload still looks like a binary PKCS#1 key, just without the preamble. In his application he had to add the header so he could import the key elsewhere. What I wanted was the reverse.
And so I did the reverse. And it worked.
And then I came across this later post by Berin also doing what I wanted. Doh. Our implementations are not identical, but functionally similar.
Code or it didn’t happen
Assuming you have your OpenSSL generated RSA public key in an NSData object, this method will verify that it is in fact a PKCS#1 key and strip the header:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
- (NSData *)stripPublicKeyHeader:(NSData *)d_key { // Skip ASN.1 public key header if(d_key == nil) return nil; unsigned int len = [d_key length]; if(!len) return nil; unsigned char *c_key = (unsigned char *)[d_key bytes]; unsigned int idx = 0; if(c_key[idx++] != 0x30) return nil; if(c_key[idx] > 0x80) idx += c_key[idx] - 0x80 + 1; else idx++; // PKCS #1 rsaEncryption szOID_RSA_RSA static unsigned char seqiod[] = { 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00 }; if(memcmp(&c_key[idx], seqiod, 15)) return nil; idx += 15; if(c_key[idx++] != 0x03) return nil; if(c_key[idx] > 0x80) idx += c_key[idx] - 0x80 + 1; else idx++; if(c_key[idx++] != '\0') return nil; // Now make a new NSData from this buffer return [NSData dataWithBytes:&c_key[idx] length:len-idx]; } |
and the result can be fed into the iOS functions in the expected manner. For example, this code adds a key-chain reference to a public key to an array. If you use this, don’t forget to eventually CFRelease()
each SecKeyRef
you acquire, and don’t forget that tag
needs to be unique for every key you add.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
- (BOOL)addPublicKey:(NSString *)key withTag:(NSString *)tag { NSString *s_key = [NSString string]; NSArray *a_key = [key componentsSeparatedByString:@"\n"]; BOOL f_key = FALSE; for(NSString *a_line in a_key) { if([a_line isEqualToString:@"-----BEGIN PUBLIC KEY-----"]) { f_key = TRUE; } else if([a_line isEqualToString:@"-----END PUBLIC KEY-----"]) { f_key = FALSE; } else if(f_key) { s_key = [s_key stringByAppendingString:a_line]; } } if(s_key.length == 0) return FALSE; // This will be base64 encoded, decode it. NSData *d_key = [NSData dataFromBase64String:s_key]; d_key = [self stripPublicKeyHeader:d_key]; if(d_key == nil) return FALSE; NSData *d_tag = [NSData dataWithBytes:[tag UTF8String] length:[tag length]]; // Delete any old lingering key with the same tag NSMutableDictionary *publicKey = [[NSMutableDictionary alloc] init]; [publicKey setObject:(id)kSecClassKey forKey:(id)kSecClass]; [publicKey setObject:(id)kSecAttrKeyTypeRSA forKey:(id)kSecAttrKeyType]; [publicKey setObject:d_tag forKey:(id)kSecAttrApplicationTag]; SecItemDelete((CFDictionaryRef)publicKey); CFTypeRef persistKey = nil; // Add persistent version of the key to system keychain [publicKey setObject:d_key forKey:(id)kSecValueData]; [publicKey setObject:(id)kSecAttrKeyClassPublic forKey:(id)kSecAttrKeyClass]; [publicKey setObject:[NSNumber numberWithBool:YES] forKey:(id)kSecReturnPersistentRef]; OSStatus secStatus = SecItemAdd((CFDictionaryRef)publicKey, &persistKey); if(persistKey != nil) CFRelease(persistKey); if(secStatus != noErr && secStatus != errSecDuplicateItem) { [publicKey release]; return FALSE; } // Now fetch the SecKeyRef version of the key SecKeyRef keyRef = nil; [publicKey removeObjectForKey:(id)kSecValueData]; [publicKey removeObjectForKey:(id)kSecReturnPersistentRef]; [publicKey setObject:[NSNumber numberWithBool:YES] forKey:(id)kSecReturnRef]; [publicKey setObject:(id)kSecAttrKeyTypeRSA forKey:(id)kSecAttrKeyType]; secStatus = SecItemCopyMatching((CFDictionaryRef)publicKey, (CFTypeRef *)&keyRef); [publicKey release]; if(keyRef == nil) return FALSE; // Add to our pseudo keychain [keyRefs addObject:[NSValue valueWithBytes:&keyRef objCType:@encode(SecKeyRef)]]; return TRUE; } |
Lastly, to verify a message in the same way we did in PHP earlier, the code might look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
- (BOOL)verifyMessage:(NSString *)msg { // Search for the two sections: Data and a signature. NSString *s_data = [NSString string], *s_signature = [NSString string]; NSArray *a_key = [msg componentsSeparatedByString:@"\n"]; BOOL f_data = FALSE, f_signature = FALSE; for(NSString *a_line in a_key) { if([a_line isEqualToString:@"-----BEGIN DATA-----"]) { f_data = TRUE; f_signature = FALSE; } else if([a_line isEqualToString:@"-----END DATA-----"]) { f_data = FALSE; f_signature = FALSE; } else if([a_line isEqualToString:@"-----BEGIN SIGNATURE-----"]) { f_data = FALSE; f_signature = TRUE; } else if ([a_line isEqualToString:@"-----END SIGNATURE-----"]) { f_data = FALSE; f_signature = FALSE; } else if(f_data) { s_data = [s_data stringByAppendingString:a_line]; } else if(f_signature) { s_signature = [s_signature stringByAppendingString:a_line]; } } if(s_data.length == 0 || s_signature.length == 0) return FALSE; // These will be base64 encoded, decode them. NSData *d_data = [NSData dataFromBase64String:s_data]; if(d_data == nil) return FALSE; NSData *d_signature = [NSData dataFromBase64String:s_signature]; if(d_signature == nil) return FALSE; // Make SHA-1 hash of the data uint8_t h_data[CC_SHA1_DIGEST_LENGTH]; CC_SHA1(d_data.bytes, d_data.length, h_data); d_hash = [NSData dataWithBytes:h_data length:CC_SHA1_DIGEST_LENGTH]; // The signature is generated against the binary form of the data, validate it. BOOL valid = FALSE; for(NSValue *refVal in keyRefs) { SecKeyRef p_key = NULL; [refVal getValue:&p_key]; if(p_key == NULL) continue; OSStatus secStatus = SecKeyRawVerify(p_key, kSecPaddingPKCS1SHA1, d_hash.bytes, d_hash.length, d_signature.bytes, d_signature.length); if(secStatus == errSecSuccess) { valid = TRUE; break; } } return valid; } |
So, yes. It’s involved. But I think it’s worth it.
Also, for what it is worth, this does work in the Simulator, certainly at least with the current iOS SDK (4.2).