Import RSA keys to iPhone keychain?

2020-06-04 02:07发布

问题:

I have a couple of NSString objects that represent an RSA public-private keypair (not generated by SecKeyCreatePair, but by an external crypto library). How can I create SecKeyRef objects (which are required for the SecKeyDecrypt/Encrypt methods) from these NSString objects?

Do I need to import them into the Keychain first? If so, how?

Thanks!

回答1:

So in iOS, the keychain is sandboxed, AFAIK. This means that whatever you put into the keychain is only accessible by your app and your app alone unless you specify otherwise. You have to enable Keychain Sharing under Capabilities in the project settings.

Now that that's out of the way, you can certainly import the data. Since they're NSString objects, you'd first have to convert that to NSData objects to import them correctly. Most likely, they're encoded in Base64, so you'lL have to reverse that:

NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:base64String options:0];

Now that that's done, you can use this method to both save your key to the keychain and get the SecKeyRef:

/**
 * key: the data you're importing
 * keySize: the length of the key (512, 1024, 2048)
 * isPrivate: is this a private key or public key?
 */
- (SecKeyRef)saveKeyToKeychain:(NSData *)key keySize:(NSUInteger)keySize private:(BOOL)isPrivate {
    OSStatus sanityCheck = noErr;
    NSData *tag;
    id keyClass;

    if (isPrivate) {
        tag = privateTag;
        keyClass = (__bridge id) kSecAttrKeyClassPrivate;
    }
    else {
        tag = publicTag;
        keyClass = (__bridge id) kSecAttrKeyClassPublic;
    }

    NSDictionary *saveDict = @{
            (__bridge id) kSecClass : (__bridge id) kSecClassKey,
            (__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeRSA,
            (__bridge id) kSecAttrApplicationTag : tag,
            (__bridge id) kSecAttrKeyClass : keyClass,
            (__bridge id) kSecValueData : key,
            (__bridge id) kSecAttrKeySizeInBits : [NSNumber numberWithUnsignedInteger:keySize],
            (__bridge id) kSecAttrEffectiveKeySize : [NSNumber numberWithUnsignedInteger:keySize],
            (__bridge id) kSecAttrCanDerive : (__bridge id) kCFBooleanFalse,
            (__bridge id) kSecAttrCanEncrypt : (__bridge id) kCFBooleanTrue,
            (__bridge id) kSecAttrCanDecrypt : (__bridge id) kCFBooleanFalse,
            (__bridge id) kSecAttrCanVerify : (__bridge id) kCFBooleanTrue,
            (__bridge id) kSecAttrCanSign : (__bridge id) kCFBooleanFalse,
            (__bridge id) kSecAttrCanWrap : (__bridge id) kCFBooleanTrue,
            (__bridge id) kSecAttrCanUnwrap : (__bridge id) kCFBooleanFalse
    };

    SecKeyRef savedKeyRef = NULL;
    sanityCheck = SecItemAdd((__bridge CFDictionaryRef) saveDict, (CFTypeRef *)&savedKeyRef);
    if (sanityCheck != errSecSuccess) {
        LOGGING_FACILITY1(sanityCheck != noErr, @"Problem saving the key to keychain, OSStatus == %d.", sanityCheck);
    }

    return savedKeyRef;
}

Later, if you want to retrieve the SecKeyRef from the keychain, you can use this:

- (SecKeyRef)getKeyRef:(BOOL)isPrivate {
    OSStatus sanityCheck = noErr;
    NSData *tag;
    id keyClass;
    if (isPrivate) {
        if (privateKeyRef != NULL) {
            // already exists in memory, return
            return privateKeyRef;
        }
        tag = privateTag;
        keyClass = (__bridge id) kSecAttrKeyClassPrivate;
    }
    else {
        if (publicKeyRef != NULL) {
            // already exists in memory, return
            return publicKeyRef;
        }
        tag = publicTag;
        keyClass = (__bridge id) kSecAttrKeyClassPublic;
    }

    NSDictionary *queryDict = @{
            (__bridge id) kSecClass : (__bridge id) kSecClassKey,
            (__bridge id) kSecAttrKeyType : (__bridge id) kSecAttrKeyTypeRSA,
            (__bridge id) kSecAttrApplicationTag : tag,
            (__bridge id) kSecAttrKeyClass : keyClass,
            (__bridge id) kSecReturnRef : (__bridge id) kCFBooleanTrue
    };

    SecKeyRef keyReference = NULL;
    sanityCheck = SecItemCopyMatching((__bridge CFDictionaryRef) queryDict, (CFTypeRef *) &keyReference);
    if (sanityCheck != errSecSuccess) {
        NSLog(@"Error trying to retrieve key from server. isPrivate: %d. sanityCheck: %li", isPrivate, sanityCheck);
    }

    if (isPrivate) {
        privateKeyRef = keyReference;
    }
    else {
        publicKeyRef = keyReference;
    }
    return keyReference;
}


回答2:

EDIT: Using the below method we were able to import keys up to size 4096. Any RSA Keysize larger than this seems to be rejected by keychain. We get back a success status, but we don't get a reference to the key.

Just a quick note regarding importing RSA private/public keys. In my case, I needed to import a private key generated by OpenSSL.

This project does most of what I wanted as far as putting it into keychain. As you can see it just has a keydata where you shove the key data into it and keychain figures out the block size, etc from the key. Keychain supports an ASN.1 encoded key.

When you export a key to a file, it is most likely a PEM file. A PEM file is just a base64 encoded DER structure. The DER structure is a generalized structure, but in the case of OpenSSL, it's usually an ASN.1 encoded private or public key.

The ASN.1 structure is displayed pretty well here. PLEASE read and understand how to read the ASN.1 structure before you try and fiddle with this, or importing a key of another size will fail.

I apparently don't have enough "reputation" to post more than 2 links. So for the following example paste the base64 information (everything except the --- BEGIN * KEY --- and ---END * KEY --- at: lapo.it/asn1js.

If you look ay the iOS project I linked, you'll see they include sample keys. Paste the private key into the ASN.1 decoder. You'll notice you have a SEQUENCE tag followed by several INTEGER values.

Now paste in the public key. You'll notice that the public key has two pieces of information in common with the private key. The modulus and the exponent. In the private key this is the second and third INTEGER values. It also has some information at the top. It has 2 extra SEQUENCEs, an OBJECT ID, NULL, and BIT STRING tags.

You'll also notice in the project that he calls a special function to process that public key. What it does is strip all the header information until it gets to the innermost SEQUENCE tag. At that point he treats it exactly like the private key and can put it into keychain.

Why do this for one and not the other? Look at the header and footer text. The private key says --- BEGIN RSA PRIVATE KEY ---, the public key says --- BEGIN PUBLIC KEY ---. The object ID you will see in the public key is: 1.2.840.113549.1.1.1. This is an ID that is a static tag identifying the contained key as an RSA type key.

Since the private key has RSA in the preamble, it's assumed it's an RSA key, and that header ASN.1 information isn't needed to identify the key. The public key is just a generic key, so a header is required to identify what type of key it is.

Keychain will NOT import an RSA key with this ASN.1 header. You need to strip it all the way to the last SEQUENCE. At that point you can put it into keychain, and keychain was able to derive the block size and other key attributes.

So if BEGIN RSA PRIVATE KEY is there, you do not need to do the stripping. If it is -- BEGIN PRIVATE KEY ---, you will need to strip those initial headers before putting it into keychain.

In my case, I also needed the public key. We couldn't figure our a way to get it from keychain once we put the private key in successfully (We may have just missed something), so we actually created an ASN.1 public key from the private key and imported that into keycahin.

In the private key (After ASN.1 header stripping) you will have a SEQUENCE tag followed by 3 INTEGER tags (there are more INTEGERS after this, but the first 3 are all we care about).

The first one is a VERSION tag. The second one is the Modulus and the third is the public exponent.

Looking at the public key (after ASN.1 header stripping) You see SEQUENCE followed by 2 INTEGERS. You guessed it, this is the modulus and public exponent from the private key.

So all you need to do is:

  1. Grab the modulus and public exponent from the private key
  2. Create a SEQUENCE tag in a buffer and set its length to [modulus length] + [exponent length]. (When writing these bytes to the buffer, you will most likely need to reverse the endian of the bytes. I did at least.)
  3. Add the modulus data you grabbed from the private key
  4. Add the exponent data you grabbed from the private key

That's all you need to do to create a public key from your private key you imported. There doesn't seem to be much information out there for importing RSA keys you don't generate on the device, and I've heard that keys generated on the device do NOT contain these ASN.1 headers, but I've never tried. Our keys are quite large and take a device too long to generate. The only option I had ever found was to use OpenSSL, where you have to compile your own for iOS. I'd rather use the security framework where possible.

I'm still rather new to iOS development, and I'm sure someone knows a simple function that does all of this that I couldn't find, and I LOOKED. This seems to work fine until an easier API is available to process keys.

One final note: The private key included with the project had a tag of BIT STRING, but the one I import from an OpenSSL generated private key had the tag OCTET STRING.



回答3:

I dug this code up (BSD license) from the MYcrypto library. It seems to do what you want.

    SecKeyRef importKey(NSData *data, 
                    SecExternalItemType type,
                    SecKeychainRef keychain,
                    SecKeyImportExportParameters *params) {
    SecExternalFormat inputFormat = (type==kSecItemTypeSessionKey) ?kSecFormatRawKey :kSecFormatUnknown;
    CFArrayRef items = NULL;

    params->version = SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION;
    params->flags |= kSecKeyImportOnlyOne;
    params->keyAttributes |= CSSM_KEYATTR_EXTRACTABLE;
    if (keychain) {
        params->keyAttributes |= CSSM_KEYATTR_PERMANENT;
        if (type==kSecItemTypeSessionKey)
            params->keyUsage = CSSM_KEYUSE_ENCRYPT | CSSM_KEYUSE_DECRYPT;
        else if (type==kSecItemTypePublicKey)
            params->keyUsage = CSSM_KEYUSE_ENCRYPT | CSSM_KEYUSE_VERIFY | CSSM_KEYUSE_WRAP;
        else if (type==kSecItemTypePrivateKey)
            params->keyUsage = CSSM_KEYUSE_DECRYPT | CSSM_KEYUSE_SIGN;
    }
    if (!check(SecKeychainItemImport((CFDataRef)data, NULL, &inputFormat, &type,
                                     0, params, keychain, &items),
               @"SecKeychainItemImport"))
        return nil;
    if (!items || CFArrayGetCount(items) != 1)
        return nil;
    SecKeyRef key = (SecKeyRef)CFRetain(CFArrayGetValueAtIndex(items,0));
    CFRelease(items);
    return key; // caller must CFRelease
}


回答4:

The answer was to call SecItemAdd with the right set of flags. See: http://hg.mozilla.org/services/fx-home/file/tip/Sources/NetworkAndStorage/CryptoUtils.m#l931



回答5:

I'm not sure if the code in this Apple developer forum thread works, but it seems like a direct answer to your question.