Я хотел бы использовать аппаратные ключи для клиентского взаимного TLS на моем Android. Ключ должен быть разблокирован с помощью биометрии.
Я нашел, как генерировать аппаратные пары ключей на Android:
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
keyGenerator.initialize(
new KeyGenParameterSpec.Builder(myAlias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.setUserAuthenticationRequired(true)
.build());
keyGenerator.generateKeyPair();
и как разблокировать закрытый ключ с аппаратной поддержкой с помощью отпечатка пальца:
FingerprintManager fingerprintManager = (FingerprintManager) this.getSystemService(Context.FINGERPRINT_SERVICE);
PrivateKey key = (PrivateKey) keyStore.getKey(myAlias, null);
Cipher cipher = Cipher.getInstance(cipherAlgorithm, "AndroidKeyStore");
cipher.init(Cipher.DECRYPT_MODE, key);
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, authenticationCallback, null);
Я также могу настроить свой HttpClient для использования клиентских сертификатов:
// I have loaded the PrivateKey privateKey and Certificate certificate from PEM files
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(null);
final char pseudoSecretPassword[] = ("##" + System.currentTimeMillis()).toCharArray();
keyStore.setKeyEntry(
PKIModule.DEFAULT_KEYSTORE_ALIAS,
privateKey,
pseudoSecretPassword,
new Certificate[] {certificate}
);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
kmf.init(keyStore, pseudoSecretPassword);
KeyManager[] keyManagers = kmf.getKeyManagers();
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(keyManagers, trustManagers, new SecureRandom());
OkHttpClient newClient = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory())
.build();
Однако я не нашел способа напрямую разблокировать закрытый ключ с аппаратной поддержкой для использования в KeyManager, используемом SSLContext, потому что механизм разблокировки работает с криптообъектами, а не с закрытыми ключами.
Как сделать так, чтобы разблокировка биометрического ключа и клиентские сертификаты TLS работали вместе на Android?
Следуя пунктам @pedrofb, я обновил свой код для генерации пары ключей с помощью KeyProperties.PURPOSE_SIGN
и KeyProperties.DIGEST_NONE
. Я подписал пару ключей клиента с помощью CA, который был импортирован в хранилище доверенных сертификатов сервера. А также создание клиента KeyManager на основе AndroidKeyStore:
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
factory.init(keyStore, null);
KeyManager[] keyManagers = factory.getKeyManagers();
sslContext.init(keyManagers, trustManagers, new SecureRandom());
Однако это не с
W/CryptoUpcalls: Preferred provider doesn't support key:
W/System.err: java.security.InvalidKeyException: Keystore operation failed
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1256)
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1281)
at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
at android.security.keystore.AndroidKeyStoreSignatureSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreSignatureSpiBase.java:219)
at android.security.keystore.AndroidKeyStoreSignatureSpiBase.engineInitSign(AndroidKeyStoreSignatureSpiBase.java:99)
at android.security.keystore.AndroidKeyStoreSignatureSpiBase.engineInitSign(AndroidKeyStoreSignatureSpiBase.java:77)
at java.security.Signature$Delegate.init(Signature.java:1357)
at java.security.Signature$Delegate.chooseProvider(Signature.java:1310)
at java.security.Signature$Delegate.engineInitSign(Signature.java:1385)
at java.security.Signature.initSign(Signature.java:679)
at com.android.org.conscrypt.CryptoUpcalls.rawSignDigestWithPrivateKey(CryptoUpcalls.java:88)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.NativeSsl.doHandshake(NativeSsl.java:383)
at com.android.org.conscrypt.ConscryptFileDescriptorSocket.startHandshake(ConscryptFileDescriptorSocket.java:231)
at okhttp3.internal.connection.RealConnection.connectTls(RealConnection.java:336)
at okhttp3.internal.connection.RealConnection.establishProtocol(RealConnection.java:300)
at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:185)
at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.java:224)
at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.java:107)
at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.java:87)
at okhttp3.internal.connection.Transmitter.newExchange(Transmitter.java:169)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:41)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.java:94)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.java:93)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.java:88)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:142)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:117)
at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.java:221)
at okhttp3.RealCall.execute(RealCall.java:81)
at com.jemmic.secuchat.biometriccrypto.MainActivity.testMutualTLS(MainActivity.java:402)
at com.jemmic.secuchat.biometriccrypto.MainActivity.access$300(MainActivity.java:87)
at com.jemmic.secuchat.biometriccrypto.MainActivity$TestMutualTlsTask.doInBackground(MainActivity.java:315)
at com.jemmic.secuchat.biometriccrypto.MainActivity$TestMutualTlsTask.doInBackground(MainActivity.java:311)
at android.os.AsyncTask$2.call(AsyncTask.java:333)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at android.os.AsyncTask$SerialExecutor$1.run(AsyncTask.java:245)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)
W/System.err: Caused by: android.security.KeyStoreException: Incompatible padding mode
at android.security.KeyStore.getKeyStoreException(KeyStore.java:1159)
... 43 more
W/CryptoUpcalls: Could not find provider for algorithm: NONEwithRSA
Некоторые соображения:
Ключ не имеет аппаратного обеспечения, если устройство не имеет аппаратной поддержки. Вы можете проверить, хранится ли ключ на безопасном оборудовании, с помощью KeyInfo.isInsideSecurityHardware()
.
TLS требует цифровой подписи, но ваш ключ создан для целей шифрования. Вам необходимо изменить KeyProperties.PURPOSE_ENCRYPT
с помощью KeyProperties.PURPOSE_SIGN
FingerprintManager инкапсулирует использование объекта Signature
но не расширяет java.security.KeyStore, требуемый по умолчанию KeyManager
TLS требует непосредственного управления закрытым ключом, поскольку протокол TLS выполняет подпись части общих данных во время рукопожатия с помощью определенного алгоритма. Чтобы использовать FingerprintManager, базовый поставщик криптографии должен поддерживать его напрямую.
Я считаю, что вы можете получить тот же результат, выполнив это:
1- Разблокировать нужный ключ по отпечатку пальца
2- Предоставьте AndroidKeyStore для KeyManagerFactory
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null,null);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509");
kmf.init(keyStore, pseudoSecretPassword);
Это решение, я думаю, могло бы работать, но вам нужно будет связать сертификат с закрытым ключом, который соответствует списку принятых CA, отправленных сервером, поэтому вам придется выдать сертификат с использованием открытого ключа и сохранить его в Android хранилище ключей, связанное с закрытым ключом.
Вы не упомянули, собираетесь ли вы использовать решение этого стиля, которое довольно сложно
Если вы не собираетесь использовать сертификаты, вы можете написать свой собственный KeyManager
для получения правильного PrivateKey
во время рукопожатия TLS. Посмотрите на мой ответ здесь, он очень похож на ваш вариант использования, но с использованием AndroidKeyChain вместо AndroidKeyStore
Запрос с автоматическим или пользовательским выбором соответствующего сертификата клиента
KeyInfo.isInsideSecurityHardware()
о необходимости проверкиKeyInfo.isInsideSecurityHardware()
и необходимостиKeyProperties.PURPOSE_ENCRYPT
иKeyProperties.PURPOSE_SIGN
. Кроме.setDigests(KeyProperties.DIGEST_NONE, ...)
может потребоваться.setDigests(KeyProperties.DIGEST_NONE, ...)
( github.com/google/conscrypt/issues/497 ).