Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android java updating certificate and private keys in Android KeyStore

I have a system that uses HTTPS client certificates to authenticate, but the certificates themselves are generated, according to the following process:

  1. Client device generates a certificate (including public and private key)
  2. Client device sends the public key to the server, which signs the public key, and returns it as a signed certificate
  3. Client stores the certificate in a secure fashion, and then later on uses it as the HTTPS client certificate

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

like image 760
Orion Edwards Avatar asked Nov 18 '15 03:11

Orion Edwards


2 Answers

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

  1. Generate a public/private key pair using KeyPairGenerator with an alias of X
  2. Send the public key to the server which generates a new signed certificate from that public key, and and sends it back
  3. Put this signed certificate in the KeyStore using 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

like image 141
Orion Edwards Avatar answered Oct 20 '22 13:10

Orion Edwards


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.

like image 40
pkzip Avatar answered Oct 20 '22 12:10

pkzip