【问题标题】:Android java updating certificate and private keys in Android KeyStoreAndroid java更新Android KeyStore中的证书和私钥
【发布时间】:2016-02-19 15:49:52
【问题描述】:

我有一个使用HTTPS客户端证书进行认证的系统,但是证书本身是按照如下流程生成的:

  1. 客户端设备生成证书(包括公钥和私钥)
  2. 客户端设备将公钥发送到服务器,服务器对公钥进行签名,并将其作为签名证书返回
  3. 客户端以安全方式存储证书,然后将其用作 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 中是否可行?如果是这样,我该怎么做?好像真的不行

谢谢

猎户座

【问题讨论】:

    标签: java android keystore


    【解决方案1】:

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

    这大大简化了 KeyStore 的管理。我的场景是现在

    1. 使用别名为XKeyPairGenerator 生成公钥/私钥对
    2. 将公钥发送到服务器,由该公钥生成新的签名证书,然后发回
    3. 使用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 对象泄漏到外部的唯一值是应该知道的模值。
      • 非常感谢您的验证!我想去测试它以确保它没有泄漏密钥,但我一直没有时间。
      猜你喜欢
      • 2020-10-23
      • 1970-01-01
      • 2016-08-15
      • 2021-05-21
      • 2020-01-25
      • 2020-01-17
      • 1970-01-01
      • 2019-04-20
      • 1970-01-01
      相关资源
      最近更新 更多