2

Update (tl;dr)

  • Swift (iOS) produces PKCS#1-formatted keys
  • Java 17 produces keys which look alike but are slightly different using the SubjectPublicKeyInfo (or SPKI) specification, following RFC 5280

Objective

Creating equally formatted public keys in PKCS#1-format

General approach

I am generating 4096 RSA public keys, in both ...

  • Java 17
  • Swift in iOS emulator (iPhone 14)

Samples of the subsequent generated and keys can be found in the Appendix.

Sample Code: Generating a KeyPair (Swift - iOS)

// Keypair attributes
let tag = "my_personal_keystore_keypair_alias".data(using: .utf8)!
let attributes: [String: Any] =
    [kSecAttrKeyType as String:            kSecAttrKeyTypeRSA,
     kSecAttrKeySizeInBits as String:      "4096",
     kSecPrivateKeyAttrs as String:
        [kSecAttrIsPermanent as String:    true,
         kSecAttrApplicationTag as String: tag]
]
var error: Unmanaged<CFError>?

// Generate PrivateKey / Keypair
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
    throw error!.takeRetainedValue() as Error
}

// Extract PublicKey
guard let publicKey = SecKeyCopyPublicKey(privateKey),
      let publicKeyOptional = SecKeyCopyExternalRepresentation(publicKey, nil) else {
    return "PublicKey could not be accessed."
}
let publicKeyData = publicKeyOptional as Data
let base64PublicKey = publicKeyData.base64EncodedString()
print("PublicKey, base64:   ", base64PublicKey)

Sample Code: Generating a KeyPair (Java 17)

private static String getPublicKey() {
    KeyPairGenerator generator;
    try {
        generator = KeyPairGenerator.getInstance("RSA");
    } catch (NoSuchAlgorithmException e) {
        throw new EncryptionException(e);
    }
    generator.initialize(4096);
    KeyPair pair = generator.generateKeyPair();
    return Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
}

Aim - Parsing the keys in Java 17

Contrary to the assumption that the keys are formatted equally, the Java-side code is only capable to read the keys generated by Java itself.

Solution for keys generated by Java 17

Parsing the key(s) generated by Java 17:

public Optional<PublicKey> validateKeyFromJava(String publicKey) {
    try {
        byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey);
        KeyFactory keyFactory = KeyFactory.getInstance(EncryptionProperties.ALGORITHM);
        EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBytes);
        return Optional.ofNullable(keyFactory.generatePublic(publicKeySpec));
    } catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException | NullPointerException e) {
        return Optional.empty();
    }
}

Problem: Parsing the keys generated via Swift (iOS)

java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: algid parse error, not a sequence at java.base/sun.security.rsa.RSAKeyFactory.engineGeneratePublic(RSAKeyFactory.java:241) at java.base/java.security.KeyFactory.generatePublic(KeyFactory.java:351)

NOT WANTED - Workaround for keys generated via Swift (iOS)

Using an external dependency ..

import org.bouncycastle.asn1.ASN1Sequence;

.. it is possible to parse the iOS keys as well. The key can thereafter be used to properly encrypt 'payload' ...

Parsing the key(s) generated via Swift (iOS):

public Optional<PublicKey> validateKeyFromIOS (String publicKeyANS1) {
    try {
        byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyANS1);

        ASN1Sequence sequence = ASN1Sequence.getInstance(publicKeyBytes);
        ASN1Integer modulus = ASN1Integer.getInstance(sequence.getObjectAt(0));
        ASN1Integer exponent = ASN1Integer.getInstance(sequence.getObjectAt(1));
        RSAPublicKeySpec keySpec = new RSAPublicKeySpec(modulus.getPositiveValue(),
                exponent.getPositiveValue());

        KeyFactory factory = KeyFactory.getInstance(EncryptionProperties.ALGORITHM);

        return Optional.ofNullable(factory.generatePublic(keySpec));
    } catch (NoSuchAlgorithmException e) {
        String errorInfo = "iOS-PublicKey - Erwarteter Algorithmus: RSA, Retrieved: " +
                e.getMessage();
        sendToErrorQueue(e, errorInfo);
        return Optional.empty();
    } catch (InvalidKeySpecException | IllegalArgumentException | NullPointerException e) {
        sendToErrorQueue(e, "Der iOS-PublicKey entspricht nicht der erwarteten Specification.");
        return Optional.empty();
    }
}

... Still it is a different way to read the keys. Therefore the keys have to be somehow different.

Wrapping up

Both systems produce keys which are PKCS#1-formatted. This can be examined (see Appendix) The keys are somewhat different.

Questions to be answered:

  1. What is the difference?
  2. Major - is it possible to generate public keys in Swift (iOS) alike those in Java 17?
  3. Minor - is it possible for Java 17 to read the keys generated by Swift (iOS) without the BouncyCastle dependency?
  • Aim: Reduction of switches in the code for different input paths.

Appendix

All keys may be analyzed:

Multiple keys are given to prevent 'lucky shots' .. ;-)

Sample keys - Swift iOS

First:

MIICCgKCAgEAl0HYn9IdRmexAR9dSh3SxmFazWF0RV2TKmtRxGySvCA+aHZ/Dp+8tC0YDWpQzP+tZdlDWNOBj/D7i1z4b+UZk8F9ZEoi+w61kYQz+KjMkRuYrPBFi4A/gsS2qGeFJ4OvLQzGObZ0C+5/E+wunUWyQCVjgjTHKH8yqfVXeWYFKmaSSZblSZJAObE8Ao5J38DMINF1oWG+GdZt8RTLf35ULLEa8zKvFXrfBkDjiQH90WKPwDxWwvFInqZhjpsFxLytiKW+iBLqa4+0P7uwXVodFXa38RW9dk9mEsLvj6764BKtPAaWym9XZfEZNBu0jwlfO07k0augynGps6bsSIyYglDImlTRagdA/e+1AMBlSxEFVO4NpCjEpMT7rNX/LYPeEaJ8fTC3axOM1yEntcwUfSSCDulVPGlWlFbPxt29y1RrGtMmwfbTlbOQf2okMf0mGx9Ytd9HWadIdWh4FqDVWA8MF82PP1YsOW5TA0hkprK1qJwfkaLRKnmpfL5a7gWn4HWzpdDqrezhbc/zTCeHbX5c61mgWsAIWnB3fyXsJHpR62+QtBEznwhDxafVCQyUmsDqbncBxilEnFIn08G+6ox2PQC8GhNdpFVGsSoqpDfctsm51/tUYLpyeIBdzw/3nMLL1J5jL3yWr7ZKgnzNwjpC8l6c6gpsF3NRodeVhp8CAwEAAQ==

Second:

MIICCgKCAgEAlSbN7rlX6EMCDGuxQm9G6qqXIWshDvI5VaN4QPNCFcFzPjI/FyaHqi43+mxTyKGycT6TNqvanSu67yoIA80tSlFH2LndWv6sPQeHKxNwwFKU98Y/uL3M6hKIVXDwa6kZGM58dfY9qMWEOPXB4AwZtVaxJ4ePHFAR0kEOAHLCPpQxFkNlkg9FlCwlBzZbhHkI5uhCmlGEiLpLaJNJzjn7yo33m65189J6tIh3vopZ2ahFHb/vo6SVSbtS7Y0YlinJwH3MCGLm5+pKBpE64t5lgEhDPYSF7pKpLhEVqC5ETaya3mYTkXPSCiY/9Mo5zddniUGrxAJaY940jY3QNYeFmWPEEdhhIW2tUbuiM3IlHAqk4Du5p/2o1QcSVZdBsHStSGdcWqs00Z+ooxXwrOpMOUnZeb8Jt5dGDyHwjOmPf0N8dDQwz+5pgOoTkxd/z5cTK9tMv/nC+Mbbt8LRsct16M3LYrRAYnA8unX1ZD3eXTJPOSB/fM9Hmf1ObxQro9tatC5RaJjS0Xg5aepUsey62BW9lj4kHCkRPD4w1OmjDWGUCOSEhwy0KwqFyRBppYUMndEbJb3GKEjpekcQYwjERPtH/vEVxaN5gGYaCORancUaNH+d03zAmi0G830QXQvyohyKUkGw5qljEBH5R5Wqu7YLTlUes0rQEx8DpLVclaMCAwEAAQ==

Sample keys - Java 17

First:

MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqwMRLS3hhNuVBQTqRT4714fbl4h3PtrSZQ8Ry1qMCefFOLzmLmLD/VdnFoHj3QiqlZMlajvVuJonprhOy911aNW2Qs2VZ+zoMkLRjah9gqg00dLVtdWa2FXjAX6bh/rSR6jmF1hzfLPs/CqOKyDpX003AUF/WvgHkuAizQjZeskEDKawbExs6RSSFHNycrGwB9hBhAwGvyZpC2+fqPN/93W2NZ8xyb5H6CmM7pCpG+sTcsEEYOZjjq2HV8vYdM9KHi7O+nmY0MJxjLJ5JOv5oLqPXFt4Epezz4v2C3oE3Leu4gIy1r1D2xHuxu0pyQ9F2nI7a+6mGZbRAYDA8Yq2oidqBDC9pKogoHcMYhXj/G0WNEhE9ODuHg+GkiiGc3zysuKwfKu1Z5ZmUtlI1kVFqUEEi81smylTXtkU/oEf7P0OX94XcNTq1N3iO+nuFayAhINlvLUXwyO4JQUsmCyu0bH17XuNUfbnSCmOfGhFSjpyU0PthW9Qtf9yY9MmyZz9c9T2kfxoUxBnNRhVUZ+Kg8oA6kkNJp/ac+AdZzkPZFev91ZKYCbyI+rcakJ4KBQ5YoprDtpHmAQZ5E3/rWcq1Tis0yiomE2+natrzaO4aQbrljOHEgB6arbMkeu7/sZHEvYfK7ilH7tsZfDdU2DVOV0yxNReYwOHAFgFMTNQvBsCAwEAAQ==

Second:

MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAs9ywiZnmv9Ewf+dIsmwV8ZlZKQNg4lbVLpNptb3pftnoukvBjak1rDqb6GcxXtM+UuBgr4Wigi0TI+vItzWv6QlBB8HShA4WImKrOCD3+U1fFdDpwCxQO+oHqTqeNKhRcKcM0e0C/c88hJo8l9G2OCjIANW3O+pGeMirB/XjPdcQ+d37TFGpRs83wPR8gUFQsH8qQMkS7uOtTCZHfcK6UzbvJB3Gf/llcCSfPYKUMY9SXIc5K9hpI0E1v5EdEUvdbhiilZI0esntr5BzeO5AxH/m9fFPH9ekZZyc9nXsHY7tPOJMD+wsTad4D9NBGi9x+lzxZRdxyDLote4ix+aHSqMgcdtOyJ69WFym2Ty1VfCgrmG8fhC/PZBEzk6obK5uJD4mO3owrKgYsmJgZHO3CbVkmv04+EEPpQLippQu8uCl+2KO480EvjSco5ryVH3XEsAGokBLCsZviBtntY/Ia0OLXPmy7WWXigxsVQkgPZ1SyZa3FUp/Zuwtoo0BWDI/9JdUZPfKhbQkcxtnKbCe8QXf51YUgIfKXPTBlmk/Zj931EzXdQCZFMvgK+I5p79DqCsD9yKbiUnfK9tXJ4SzMfuiLd4pqB0UIMVMQEj5SZPYn7eHRZyje4v96FDwtj56NQ/kZPYXKqLoOv5nvMALZngzJ6aFaGjkWZX9RNQLfA8CAwEAAQ==

d.braun1991
  • 151
  • 1
  • 13
  • 3
    Java does **not** encode public keys using the PKCS1 format, instead it uses an arguably more common (and better) standard called SubjectPublicKeyInfo (or SPKI) format, specified in [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.7). This standard is supported by many software packages, not just Java. – President James K. Polk Dec 18 '22 at 21:53
  • Thanks a lot @PresidentJamesK.Polk .. :) ... This little comment untied a huge knot I was carrying around .. :D – d.braun1991 Dec 19 '22 at 09:28

1 Answers1

0

Why ... .. .

By today I have at least a guess WHY the keys differ. If I had to categorize those two types ..

  • Public-Key Cryptography Standards (PKCS)
  • Simple public key infrastructure (SPKI)

.. it would look like follows:

ASN.1 object
├── Human readable & File-oriented
│   ├── base64
│   │   └── PEM
│   └── binary
│       ├── BER
│       └── DER
└── Compile time-oriented
    └── SPKI

The ASN.1-object is the upper level container (Definition).

PEM, DER and BER are file-oriented representations of the key (Source).

PKCS#1 in PEM

Theory:
-----Header----
$ Base65 encoded Key (Here: PKCS#1
-----Footer-----

Praxis:
-----BEGIN RSA PRIVATE KEY-----
MIHgMIGaAgEBMA0GCSqG...
-----END RSA PRIVATE KEY-----

Header and Footer contain meta-information about the delivered key, here: RSA PRIVATE KEY This makes the key more likely to be human-readable - at least regarding the key administration 'by hand'.

SPKI

I assume that this meta-information is encoded within the SPKI-format. Therefore there is no further meta-information needed to parse the key. On the other hand this information can not be obtained easily without further knowledge. The human-readability is largely limited.

Hence:

  • Choose the key-format regarding your purpose. ** For machine-to-machine communication SPKI might be the better choice. ** For key administration which include human interaction, PEM seems a good choice.

Afterall, in terms of security the two formats do not differ.

How ... .. .

More specifically, the HOW can be answered ...

... the subsequent analysis stems from Java 17. The information provided by Java is seen here as the 'ground truth'. ... ;-)

Analyze keys generated by Java itself

// Static inputs
String algorithm = "RSA";
int keySize = 4096;

// Key generation
KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm);
generator.initialize(keySize);
KeyPair pair = generator.generateKeyPair();

// Analysis
System.out.println("----- PublicKey information -----");
System.out.println("PublicKey algorithm: " + pair.getPublic().getAlgorithm());
System.out.println("PublicKey format: " + pair.getPublic().getFormat());

System.out.println("----- PrivateKey information -----");
System.out.println("PrivateKey algorithm: " + pair.getPrivate().getAlgorithm());
System.out.println("PrivateKey format: " + pair.getPrivate().getFormat());

Output:

----- PublicKey information -----
PublicKey algorithm: RSA
PublicKey format: X.509
----- PrivateKey information -----
PrivateKey algorithm: RSA
PrivateKey format: PKCS#8

Conclusion

Java

  • Java generates its PublicKeys in the x.509-format.
  • PrivateKeys are generated in the PKCS#8-format.

Swift

  • Swift generates its PublicKeys in the PKCS#1-format.

These findings match the following answer in the apple-developer forum:

d.braun1991
  • 151
  • 1
  • 13