2015-11-18 1 views
3

У меня есть система, которая использует HTTPS клиентские сертификаты для аутентификации, но сертификаты сами генерируются, в соответствии со следующим способом:сертификат Android обновления Java и закрытые ключи в Android KeyStore

  1. Клиентское устройство генерирует сертификат (включая открытый и закрытый ключ)
  2. Клиентское устройство отправляет открытый ключ на сервер, который подписывает открытый ключ и возвращает его в качестве подписанного сертификата
  3. Клиент хранит сертификат в безопасном режиме, а затем использует его как сертификат клиента HTTPS

У нас есть эта система, работающая на iOS, и я пытаюсь перейти на Android, но столкнулся с множеством проблем с плохо документированными и запутанными API безопасности Android.

Мой код идет примерно так:

Генерация сертификата

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 является метод утилита, которая перебирает хранилища ключей, вызывает keyStore.getEntry, чтобы получить каждую запись и и просто регистрирует вещи. На этом этапе сообщается, что имеется одна запись с заданным псевдонимом, и она имеет тип KeyStore.PrivateKeyEntry. Он имеет связанный сертификат и открытый ключ, который можно извлечь из PrivateKeyEntry.

Отправка на сервер

publicKeyBytes отправляется на сервер, который ставит его в качестве открытого ключа для новой, подписанный сертификат x509, который отправляется обратно в ответ. Я не вставлял код, это просто базовая сеть. Возвращаемый сертификат загружается и отлично выглядит из того, что я могу сказать.

Сохранение и ассоциирования сертификат

Я пытаюсь поставить его в хранилище ключей с тем же псевдонимом, так что (в теории) может быть связано с правильным закрытым ключом из ранее. Мой код до сих пор, как это:

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); 

На данный момент, окончательное dumpKeyStore указывает, что есть запись с правильным псевдонимом, однако он получает «NoSuchAlgorithmException: Неизвестный ввод ключа» исключение генерируется, когда он пытается звоните keyStore.getEntry

Является ли то, что я пытаюсь сделать (заменить сертификат, но сохранить закрытый ключ) в андроиде? Если да, то как я могу это сделать? Похоже, что это на самом деле не работает

Благодаря

Orion

ответ

1

Я аналогичные проблемы с родной AndroidKeyStore. Я не могу обновить сертификат после его создания с помощью generator.generateKeyPair()

Похоже, что текущая реализация OpenSSL хранилища ключей Android еще не полностью завершена.Я искал работающий образец на некоторое время, но не смогли найти ничего, что позволит мне обновить существующий сертификат, принадлежащий к KeyStore.PrivateKeyEntry, таким образом, я подал ошибку в этом вопросе здесь: https://code.google.com/p/android/issues/detail?id=194955&q=keystore&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars

Тем временем я бы предложил использовать «БКС» Bouncy Замок ключа магазин, если вы не возражаете, используя сторонний поставщик криптографии.

Обновление: Пользовательское KeyManager может быть лучше обходным путем для использования в обоюдной SSL ситуации, когда вы можете заставить использовать один хранилищу псевдоним секретного ключа и другое хранилище ключей псевдонима для цепочки сертификатов. Однако Android documentation утверждает, вы должны быть в состоянии его заменить:

Создание нового PrivateKey требует, чтобы вы также указать начальный X.509 атрибуты, которые самостоятельно подписанный сертификат будет. Вы можете заменить сертификат в более позднее время сертификатом, подписанным центром сертификации .

3

Как оказалось, я поступал неправильно. Вам не нужно заменять или изменять сертификаты в KeyStore, вам просто нужно использовать пользовательский KeyManager, когда вы инициализируете SSLContext, используемый HttpsURLConnection, и KeyManager может выбрать любые сертификаты или секретные ключи, которые вы хотите.

Это значительно упрощает управление KeyStore. Мой сценарий теперь

  1. Сформировать открытые/закрытые пары ключей, используя KeyPairGenerator с псевдонимом X
  2. Отправить публичный ключ сервера, который генерирует новый подписанный сертификат от этого открытого ключа, а и отправляет его обратно
  3. Put этот сертификат в хранилище ключей с помощью setCertificateEntry с псевдонимом X-Signed

Когда я устанавливаю HttpsURLConnection, это выглядит следующим образом:

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()); 
} 

Это хорошо работает для меня, и я могу продолжать использовать AndroidKeyStore с его в приложение конфиденциальности и поддерживаться аппаратным обеспечением хранения