自动或用户选择适当客户端证书的请求

9
我正在开发一个混合式 Cordova 应用程序,可能会连接到不同的服务器。其中一些服务器需要客户端证书。
在 Android 手机上,对应的根证书和客户端证书已安装。
在 Chrome 浏览器中,我会得到以下对话框,选择相应的客户端证书进行 Web 连接。

Choose certificate on Chrome

使用cordova插件cordova-client-cert-authentication,WebView中的Http(s)请求会弹出相同的对话框。
我的问题是如何在本机Android平台上实现Http(s)请求的自动证书选择,而无需显式声明相应的客户端证书。或者是否有类似于Chrome上实现的用户证书选择的东西?
这是当前的实现,它会抛出握手异常:
try {
    URL url = new URL( versionUrl );
    HttpsURLConnection urlConnection = ( HttpsURLConnection ) url.openConnection();

    urlConnection.setConnectTimeout( 10000 );

    InputStream in = urlConnection.getInputStream();
}
catch(Exception e)
{
    //javax.net.ssl.SSLHandshakeException: Handshake failed
}

1
您想使用先前在Android KeyChain(系统密钥存储)中安装的证书,还是直接提供证书给 HttpsURLConnection - pedrofb
我想使用之前在KeyChain中安装的证书,它是使用"VPN和应用程序"凭据使用安装的。 - kerosene
2个回答

6
您可以使用之前在Android KeyChain(系统密钥库)中安装的证书,扩展X509ExtendedKeyManager来配置由URLConnection使用的SSLContext
证书由您需要的别名引用。要提示用户选择类似于Chrome的对话框,请使用:
KeyChain.choosePrivateKeyAlias(this, this, // Callback
            new String[] {"RSA", "DSA"}, // Any key types.
            null, // Any issuers.
            null, // Any host
            -1, // Any port
            DEFAULT_ALIAS);

这是配置SSL连接使用自定义KeyManager的代码。它使用默认的TrustManager和HostnameVerifier。如果服务器使用Android默认的信任库中不存在的自签名证书,您需要对它们进行配置(不建议信任所有证书)。
//Configure trustManager if needed
TrustManager[] trustManagers = null;

//Configure keyManager to select the private key and the certificate chain from KeyChain
KeyManager keyManager = KeyChainKeyManager.fromAlias(
            context, mClientCertAlias);

//Configure SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(new KeyManager[] {keyManager}, trustManagers, null);


//Perform the connection
URL url = new URL( versionUrl );
HttpsURLConnection urlConnection = ( HttpsURLConnection ) url.openConnection();
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
//urlConnection.setHostnameVerifier(hostnameVerifier);  //Configure hostnameVerifier if needed
urlConnection.setConnectTimeout( 10000 );
InputStream in = urlConnection.getInputStream();

最后在这里,您可以找到来自这里这里的完整自定义X509ExtendedKeyManager实现,它负责选择客户端证书。我已经提取了所需的代码。

public static class KeyChainKeyManager extends X509ExtendedKeyManager {
    private final String mClientAlias;
    private final X509Certificate[] mCertificateChain;
    private final PrivateKey mPrivateKey;

        /**
         * Builds an instance of a KeyChainKeyManager using the given certificate alias.
         * If for any reason retrieval of the credentials from the system {@link android.security.KeyChain} fails,
         * a {@code null} value will be returned.
         */
        public static KeyChainKeyManager fromAlias(Context context, String alias)
                throws CertificateException {
            X509Certificate[] certificateChain;
            try {
                certificateChain = KeyChain.getCertificateChain(context, alias);
            } catch (KeyChainException e) {
                throw new CertificateException(e);
            } catch (InterruptedException e) {
                throw new CertificateException(e);
            }

            PrivateKey privateKey;
            try {
                privateKey = KeyChain.getPrivateKey(context, alias);
            } catch (KeyChainException e) {
                throw new CertificateException(e);
            } catch (InterruptedException e) {
                throw new CertificateException(e);
            }

            if (certificateChain == null || privateKey == null) {
                throw new CertificateException("Can't access certificate from keystore");
            }

            return new KeyChainKeyManager(alias, certificateChain, privateKey);
        }

        private KeyChainKeyManager(
                String clientAlias, X509Certificate[] certificateChain, PrivateKey privateKey) {
            mClientAlias = clientAlias;
            mCertificateChain = certificateChain;
            mPrivateKey = privateKey;
        }


        @Override
        public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) {
            return mClientAlias;
        }

        @Override
        public X509Certificate[] getCertificateChain(String alias) {
            return mCertificateChain;
        }

        @Override
        public PrivateKey getPrivateKey(String alias) {
            return mPrivateKey;
        }

         @Override
        public final String chooseServerAlias( String keyType, Principal[] issuers, Socket socket) {
            // not a client SSLSocket callback
            throw new UnsupportedOperationException();
        }

        @Override
        public final String[] getClientAliases(String keyType, Principal[] issuers) {
            // not a client SSLSocket callback
            throw new UnsupportedOperationException();
        }

        @Override
        public final String[] getServerAliases(String keyType, Principal[] issuers) {
            // not a client SSLSocket callback
            throw new UnsupportedOperationException();
        }
    }
}

我没有测试它。请报告任何错误!


请查看此链接 https://stackoverflow.com/questions/51652992/ssl-hand-shaking-getting-failed-on-enterprise-android-but-working-good-with-ordi - user2028

-2
如果您的URL仍处于开发阶段(非生产版本),则可以跳过安装SSL / NON-SSL证书以访问URL。
以下是如何跳过SSL验证: 在访问URL之前,在activity onCreate()或需要的位置调用。
public static void skipSSLValidation() {
        try {
            TrustManager[] trustAllCerts = new TrustManager[]{
                    new X509TrustManager() {
                        public X509Certificate[] getAcceptedIssuers() {
                    /* Create a new array with room for an additional trusted certificate. */
                            return new X509Certificate[0];
                        }

                        @Override
                        public void checkClientTrusted(X509Certificate[] certs, String authType) {
                        }

                        @Override
                        public void checkServerTrusted(X509Certificate[] certs, String authType) {
                        }
                    }
            };

            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
            HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }
            });
        } catch (Exception e) {
            // pass
        }
    }

注意:如果您的 HTTPS URL 有效,您不需要使用服务器生成的证书。您应该仅在测试/开发中使用此方法。对于发布/生产,您不必使用此方法。

1
此解决方案无效。跳过服务器证书验证意味着客户端将信任任何证书,但服务器仍需要客户端提供证书进行身份验证。 - pedrofb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接