javaiosobjective-cjava-securitycommoncrypto

Public key generated on iOS device invalid on Java server


I have an issue with a SecKeyRef generated on iOS device - when trying to use it on Java server, the exception is thrown:

InvalidKeyException: EC domain parameters must be encoded in the algorithm identifier

here's the code snippet from the server code:

String key = ...
byte[] byteKey =  Base64.decode(key.getBytes(StandardCharsets.UTF_8));
X509EncodedKeySpec X509publicKey = new X509EncodedKeySpec(byteKey);
KeyFactory kf = KeyFactory.getInstance("EC");
return kf.generatePublic(X509publicKey);

The exception is thrown by kf.generatePublic(X509publicKey);

The key is created on iOS, using SecKeyGeneratePair

[keyPairAttr setObject:(__bridge id)kSecAttrKeyTypeEC forKey:(__bridge id)kSecAttrKeyType];
[keyPairAttr setObject:[NSNumber numberWithUnsignedInteger:256] forKey:(__bridge id)kSecAttrKeySizeInBits];

// Set the private key dictionary
[privateKeyAttr setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecAttrIsPermanent];
[privateKeyAttr setObject:self.privateTag forKey:(__bridge id)kSecAttrApplicationTag];

// Set the public key dictionary
[publicKeyAttr setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecAttrIsPermanent];
[publicKeyAttr setObject:self.publicTag forKey:(__bridge id)kSecAttrApplicationTag];

// Set attributes to top level dictionary
[keyPairAttr setObject:privateKeyAttr forKey:(__bridge id)kSecPrivateKeyAttrs];
[keyPairAttr setObject:publicKeyAttr forKey:(__bridge id)kSecPublicKeyAttrs];

// Generate key pair
OSStatus sanityCheck = SecKeyGeneratePair((__bridge CFDictionaryRef)keyPairAttr, &publicKeyRef, &privateKeyRef);

The key pair is created successfully. I then extract key's bit data using following code

CFDataRef publicKeyBitsRef = NULL;
NSMutableDictionary *queryPublicKey = [NSMutableDictionary dictionary];

// Set the public key query dictionary.
[queryPublicKey setObject:(__bridge id)kSecClassKey forKey:(__bridge id)kSecClass];
[queryPublicKey setObject:self.publicTag forKey:(__bridge id)kSecAttrApplicationTag];
[queryPublicKey setObject:(__bridge id)kSecAttrKeyTypeEC forKey:(__bridge id)kSecAttrKeyType];

[queryPublicKey setObject:[NSNumber numberWithBool:YES] forKey:(__bridge id)kSecReturnData];

// Get the key bits.
OSStatus sanityCheck = SecItemCopyMatching((__bridge CFDictionaryRef)queryPublicKey, (CFTypeRef *)&publicKeyBitsRef);

Then, I export the key using CryptoExportImportManager

NSData *publicKeyIDERData = [manager exportPublicKeyToDER:keyBits keyType:(__bridge NSString*)kSecAttrKeyTypeEC keySize:256];
NSString *derKeyString = [publicKeyIDERData base64EncodedStringWithOptions:0];

According to this answer, the DER header contains info about key type and parameters and for secp256r1 key it's equivalent of following data

[
    0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 
    0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a,
    0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 
    0x42, 0x00
]

Which is indeed added to key header on export.

derKeyString is then sent to backend and processed using Java code mentioned above. However, the exception is thrown.

The same backend processes also the key created on Android device using following code

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
        keyPairGenerator.initialize(new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_SIGN)
                .setDigests(KeyProperties.DIGEST_SHA256)
                .setAlgorithmParameterSpec(
                new ECGenParameterSpec("secp256r1"))
                .setUserAuthenticationRequired(true).build());
        keyPairGenerator.generateKeyPair();

The Android key works just fine.

What am I doing wrong? Have I forgotten about something while creating keys with SecKeyGeneratePair or exporting the public key?


Solution

  • I solved the problem, although I'm not sure why.

    What I did is I ditched CryptoExportImportManager library and I'm manually creating key data, like so:

    unsigned char _encodedECOID[] = {
        0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86,
        0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a,
        0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
        0x42, 0x00
    };
    
    NSMutableData *data = [NSMutableData new];
    [data appendBytes:_encodedECOID length:sizeof(_encodedECOID)];
    [data appendData:keyBits]; // keyBits is od NSData type
    

    Now the Java server properly creates public key from my string (base64 encoded from data).

    However, having looked at source code of CryptoExportImportManager, the way it creates the encoded string from my key bits looks like that (in Swift):

    let curveOIDHeader: [UInt8] = [0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 
                                   0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A,
                                   0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 
                                   0x42, 0x00]
    let curveOIDHeaderLen: Int = 26
    
    var data = Data(bytes: curveOIDHeader, count: curveOIDHeaderLen)
    data.append(rawPublicKeyBytes)
    

    It basically does exactly the same thing. So where's the difference?

    Now the only thing that comes to mind is the difference in how the header is stored - in my case it's an array of unsigned char, in library's case it's an array of UInt8.

    According to this answer, C types unsigned char and uint8_t are not equivalent, they are only guaranteed to have the same length, but can differ in i.e. byte ordering.

    Though that question had nothing to do with Swift's UInt8 (but was tagged C, of which Objective-C is superset), the documentation of Swift's UInt8 type says nothing about it's relation to unsigned char type, that would be the only reasonable explanation I can see.