Using an RSA public key generated by OpenSSL in iOS

Apple have gone to pains to make crypto 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/private 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/private 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. 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.

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
17
18
19
20
<? 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<? 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 but if you try to use it for keys you generated elsewhere then it doesn’t work and gives only very limited indications why it doesn’t work.

iOS does not provide documented direct access to the OpenSSL API. If it’s 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
32
33
34
35
- (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 keychain 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
63
64
65
66
67
68
69
70
71
72
- (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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
- (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).

Related Reading

Tags: Apple, Cryptography, iOS, Objective C, OpenSSL, PHP, Public Key, RSA
Posted in: iOS
Both comments and pings are currently closed.

Comments are closed.