I have a system that uses HTTPS client certificates to authenticate, but the certificates themselves are generated, according to the following process:
We have this system working on iOS, and I'm trying to port across to android, but encountering a lot of problems with Android's poorly documented and confusing security API's.
My code goes roughly like this:
Generating the certificate
keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
Date startDate = new Date();
Date endDate = new Date(startDate.getTime() + FORTY_YEARS_IN_MILLISECONDS);
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
.setAlias(alias)
.setKeySize(2048)
.setKeyType(KeyProperties.KEY_ALGORITHM_RSA)
.setSubject(new X500Principal("CN=" + alias))
.setSerialNumber(BigInteger.TEN)
.setStartDate(startDate)
.setEndDate(endDate)
.build();
KeyPairGenerator generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEYSTORE);
generator.initialize(spec);
KeyPair keyPair = generator.generateKeyPair(); // this will put a certificate and key pair in the keyStore.
dumpKeyStore(keyStore);
byte[] entireKey = keyPair.getPublic().getEncoded();
// chop off first 24 bytes; the java key pair generator puts an object ID of 1.2.840.113549.1.1.1 RSA (RSA_SIGN) before the key which gets mangled when the server signs and sends back the certificate
byte[] publicKeyBytes = Arrays.copyOfRange(entireKey, 24, entireKey.length);
dumpKeyStore
is a utility method which iterates the keystore, calls keyStore.getEntry
to get each entry and and just logs things.
At this point, it reports that there is a single entry with the given alias, and it is of type KeyStore.PrivateKeyEntry
. It has an associated certificate and public key which can be retrived from the PrivateKeyEntry
.
Sending to the server
publicKeyBytes
is sent to the server, which puts it as the public key for a new, signed x509 certificate, which is sent back in the response. I haven't put code in, it's just basic networking. The returned certificate loads and looks fine from what I can tell.
Saving and associating the certificate
I'm trying to put it into the keyStore with the same alias, so it (in theory) can be associated with the correct private key from earlier. My code thus far is like this:
KeyStore keyStore;
try {
keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
keyStore.load(null);
}catch (IOException | NoSuchAlgorithmException | CertificateException e) {
Log.wtf(TAG, e);
throw new FatalError(TAG, e);
}
CertificateFactory certificateFactory;
try {
certificateFactory = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
Log.wtf(TAG, e);
throw new FatalError(TAG, e);
}
Certificate cert = certificateFactory.generateCertificate(new ByteArrayInputStream(certificateFromServer));
// find the existing certificate, copy it's private key out, then replace the certificate with the one from the server but keeping the private key
try {
KeyStore.PrivateKeyEntry existingPrivateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
KeyStore.PrivateKeyEntry newEntry = new KeyStore.PrivateKeyEntry(existingPrivateKeyEntry.getPrivateKey(), new Certificate[]{ cert });
keyStore.setEntry(alias, newEntry, null);
} catch (Exception e) {
Log.wtf(TAG, e);
throw new FatalError(TAG, e);
}
dumpKeyStore(keyStore);
At this point, the final dumpKeyStore indicates that there is an entry with the correct alias, however it gets a "NoSuchAlgorithmException: Unknown key entry" exception thrown when it tries to call keyStore.getEntry
Is what I'm trying to do (replace a certificate but keep the private key) possible in android? If so, how might I do it? It seems like this isn't really working
Thanks
Orion
As it turns out, I was doing the wrong thing. You don't need to replace or modify the certificates in the KeyStore, you just need to use a custom KeyManager
when you initialize the SSLContext
used by HttpsURLConnection
and the KeyManager can pick whichever certificates or private keys you'd like.
This simplifies the KeyStore management greatly. My scenario is now
KeyPairGenerator
with an alias of X
setCertificateEntry
with an alias of X-Signed
When I establish the HttpsURLConnection
, it goes like this:
KeyStore androidKeyStore = KeyStore.getInstance(LocalKeyStore.ANDROID_KEYSTORE);
androidKeyStore.load(null);
X509Certificate signedClientCertificate = (X509Certificate)androidKeyStore.getCertificate("X-Signed");
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)androidKeyStore.getEntry("X", null);
X509ExtendedKeyManager keyManager = new X509ExtendedKeyManager() {
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return clientCertificateAlias;
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return null; // different if you're validating the server's cert
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return new X509Certificate[] { signedClientCertificate };
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return new String[]{ "X" };
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null; // different if you're validating server's cert
}
@Override
public PrivateKey getPrivateKey(String alias) {
if(alias != clientCertificateAlias) {
Log.e(TAG, String.format("X509ExtendedKeyManager is asking for privateKey with unknown alias %s. Expecting it to ask for %s", alias, clientCertificateAlias));
return null;
}
return privateKeyEntry.getPrivateKey();
}
};
X509TrustManager trustServerCertificates = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// do nothing, this method doesn't get called
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
// code to validate server's cert in here
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null; // any issuer
}
};
m_sslContext = SSLContext.getInstance("TLS");
m_sslContext.init(new KeyManager[]{ keyManager }, new TrustManager[] { trustServerCertificates }, null);
// later on
conn = (HttpURLConnection)url.openConnection();
SSLContext sslContext = m_sslContext;
if(conn instanceof HttpsURLConnection && sslContext != null) {
((HttpsURLConnection)conn).setSSLSocketFactory(sslContext.getSocketFactory());
}
This is working well for me, and I can keep using AndroidKeyStore
with it's per-app privacy and hardware-backed storage
I am having similar issues with the native AndroidKeyStore. I cannot update certificate once it has been created with generator.generateKeyPair()
It appears that the current OpenSSL implementation of the Android key store is not yet fully complete. I looked for working sample for a while but could not find anything that would let me update existing certificate belonging to the KeyStore.PrivateKeyEntry
thus I have filed bug for this issue here:
https://code.google.com/p/android/issues/detail?id=194955&q=keystore&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars
Meanwhile I would suggest to use "BKS" Bouncy Castle key store if you dont mind using third party cryptography provider.
Update: Custom KeyManager may be a better workaround for use in mutual SSL situation where you can force to use one keystore alias for private key and another keystore alias for the certificate chain. However the Android documentation states you should be able to replace it:
Generating a new PrivateKey requires that you also specify the initial X.509 attributes that the self-signed certificate will have. You can replace the certificate at a later time with a certificate signed by a Certificate Authority.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With