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 的管理。我的场景是现在

    使用别名为XKeyPairGenerator 生成公钥/私钥对 将公钥发送到服务器,由该公钥生成新的签名证书,然后发回 使用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 API key 怎么获取

如何在基于 Android 的 Java 中使用 SECRET KEY 制作 HMAC SHA256? [复制]

android studio DES加密编译的报错提示:java.security.InvalidKeyException: Wrong key size

登录 API Android API KEY 认证

android arcgis加载天地图作为底图不显示问题记录