Android java更新Android KeyStore中的证书和私钥
Posted
技术标签:
【中文标题】Android java更新Android KeyStore中的证书和私钥【英文标题】:Android java updating certificate and private keys in Android KeyStore 【发布时间】:2016-02-19 15:49:52 【问题描述】:我有一个使用HTTPS客户端证书进行认证的系统,但是证书本身是按照如下流程生成的:
-
客户端设备生成证书(包括公钥和私钥)
客户端设备将公钥发送到服务器,服务器对公钥进行签名,并将其作为签名证书返回
客户端以安全方式存储证书,然后将其用作 HTTPS 客户端证书
我们有这个系统在 ios 上运行,我正在尝试移植到 android,但遇到了很多关于 Android 记录不充分且令人困惑的安全 API 的问题。
我的代码大致是这样的:
生成证书
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 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 表明存在具有正确别名的条目,但是在尝试调用 keyStore.getEntry
时会抛出“NoSuchAlgorithmException: Unknown key entry”异常
我正在尝试做的事情(替换证书但保留私钥)在 android 中是否可行?如果是这样,我该怎么做?好像真的不行
谢谢
猎户座
【问题讨论】:
【参考方案1】:事实证明,我做错了事。您不需要替换或修改 KeyStore 中的证书,您只需要在初始化 HttpsURLConnection
使用的 SSLContext
时使用自定义的 KeyManager
,并且 KeyManager 可以选择您想要的任何证书或私钥喜欢。
这大大简化了 KeyStore 的管理。我的场景是现在
-
使用别名为
X
的KeyPairGenerator
生成公钥/私钥对
将公钥发送到服务器,由该公钥生成新的签名证书,然后发回
使用setCertificateEntry
将此签名证书放入KeyStore,别名为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
以及它的每个应用程序隐私和硬件支持的存储
【讨论】:
这是一个很好的解决方案。但是,我认为如果您可以替换与 X 关联的证书会更好。我已经提交了一个错误来修复它。 clientCertificateAlias 的值是多少? 应该是“X”【参考方案2】:我在使用原生 AndroidKeyStore 时遇到了类似的问题。使用generator.generateKeyPair()
创建证书后,我无法更新证书
Android 密钥库的当前 OpenSSL 实现似乎尚未完全完成。我寻找了一段时间的工作样本,但找不到任何可以让我更新属于KeyStore.PrivateKeyEntry
的现有证书的东西,因此我在这里为这个问题提交了错误:
https://code.google.com/p/android/issues/detail?id=194955&q=keystore&colspec=ID%20Type%20Status%20Owner%20Summary%20Stars
同时,如果您不介意使用第三方加密提供商,我建议您使用“BKS”Bouncy Castle 密钥库。
更新: 自定义 KeyManager 可能是在相互 SSL 情况下使用的更好解决方法,在这种情况下,您可以强制将一个密钥库别名用于私钥,将另一个密钥库别名用于证书链。但是Android documentation 声明您应该可以替换它:
生成新的 PrivateKey 要求您还指定初始 自签名证书将具有的 X.509 属性。你可以 稍后将证书替换为由 a 签名的证书 证书颁发机构。
【讨论】:
【参考方案3】:此评论不完全准确,可以忽略
由于null
作为第三个参数(protoParam)传递给KeyStore.setEntry
,第二篇文章中提供的解决方案似乎并没有像预期的Hardware-backed Keystore 那样安全地保护密钥:
KeyStore.PrivateKeyEntry newEntry = new KeyStore.PrivateKeyEntry(existingPrivateKeyEntry.getPrivateKey(), new Certificate[] cert );
keyStore.setEntry(alias, newEntry, null);
因此,密钥的原始材料可以访问,如代码本身所示:
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)androidKeyStore.getEntry("X", null);
我认为正确的解决方案必须利用 KeyGenParameterSpec.Builder 安全存储密钥并仅对其执行加密操作。如果有人提供在 TLS 相互身份验证中使用受 AndroidKeyStore 保护的客户端证书的示例代码,我将不胜感激。
【讨论】:
根据我上面的回答,您使用keyStore.setEntry(alias, newEntry, null);
引用的代码最终没有工作,而是我在密钥库中创建了密钥,永远不要复制/移动/设置它,并使用自定义 KeyManager 选择正确的客户端证书。据我所知,这个答案确实使用受 AndroidKeyStore 保护的客户端证书,而且它肯定会进行 TLS 相互身份验证
感谢您的澄清。我还使用您的代码实现了相互身份验证,但在您的示例中,密钥从硬件支持的存储泄漏到 android 框架/java 应用程序。也可以使用每个应用程序私有的 Android 内部存储来代替密钥库,因为该示例不利于 AndroidKeyStore 提供的安全保护。 Keystore 的一个主要目的是在不访问原始私钥材料的情况下执行加密操作(签名、解密)。您可以与我们分享代码吗?
我做了一些测试,我认为你是对的。从 privKey 对象泄漏到外部的唯一值是应该知道的模值。
非常感谢您的验证!我想去测试它以确保它没有泄漏密钥,但我一直没有时间。以上是关于Android java更新Android KeyStore中的证书和私钥的主要内容,如果未能解决你的问题,请参考以下文章
java Obtener el Hash Key Android App facebook
如何在基于 Android 的 Java 中使用 SECRET KEY 制作 HMAC SHA256? [复制]
android studio DES加密编译的报错提示:java.security.InvalidKeyException: Wrong key size