安卓KeyStore中的Android java更新证书和私钥

时间:2015-11-18 03:02:30

标签: java android keystore

我有一个使用HTTPS客户端证书进行身份验证的系统,但根据以下过程生成证书本身:

  1. 客户端设备生成证书(包括公钥和私钥)
  2. 客户端设备将公钥发送到服务器,该服务器签署公钥,并将其作为签名证书返回
  3. 客户端以安全的方式存储证书,然后将其用作HTTPS客户端证书
  4. 我们有这个系统在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?如果是这样,我该怎么做?这似乎并没有真正起作用

    由于

    猎户

3 个答案:

答案 0 :(得分:5)

事实证明,我做错了。您无需替换或修改KeyStore中的证书,只需在初始化KeyManager使用的SSLContext时使用自定义HttpsURLConnection,并且KeyManager可以选择任何证书或者您想要的私钥。

这极大地简化了KeyStore管理。我的方案现在是

  1. 使用别名为KeyPairGenerator
  2. X生成公钥/私钥对
  3. 将公钥发送到服务器,该服务器从该公钥生成新的签名证书,然后将其发回给
  4. 使用别名为setCertificateEntry
  5. 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每个应用程序的隐私和硬件支持的存储空间

答案 1 :(得分:2)

我遇到了与原生AndroidKeyStore类似的问题。使用generator.generateKeyPair()

创建证书后,我无法更新证书

似乎Android密钥库的当前OpenSSL实现尚未完全完成。我找了一段时间的工作样本,但找不到任何可以让我更新属于KeyStore.PrivateKeyEntry的现有证书的东西,因此我在这里提出了这个问题的bug: 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签名的证书替换证书   证书颁发机构。

答案 2 :(得分:0)

此评论不够准确,可以忽略

第二篇文章中提供的解决方案似乎无法像Hardware-backed Keystore那样安全地保护密钥,因为null作为第三个参数(protoParam)传递给了{{ 1}}:

KeyStore.setEntry

因此,可以如代码本身所示访问密钥的原材料:

    KeyStore.PrivateKeyEntry newEntry = new KeyStore.PrivateKeyEntry(existingPrivateKeyEntry.getPrivateKey(), new Certificate[]{ cert });
    keyStore.setEntry(alias, newEntry, null);

我认为正确的解决方案必须利用 KeyGenParameterSpec.Builder以安全地存储密钥,并且仅对其执行加密操作。如果有人提供的示例代码在TLS双向身份验证中使用受AndroidKeyStore保护的客户端证书,我将不胜感激。