自签名证书是无需别的证书为其签名来证明其合法性的证书,根证书都是自签名证书。私有 CA 签名证书则是指,为域名证书签名的 CA,其合法有效性没有得到广泛的认可,该 CA 的根证书没有被内置到系统中。
在实际的开发过程中,有时为了节省昂贵的购买证书的费用,而想要自己给自己的服务器的域名签发域名证书,这即是私有 CA 签名的证书。为了能够使用这种证书,需要在客户端预埋根证书,并对客户端证书合法性验证的过程进行干预,通过我们预埋的根证书为服务端的证书做合法性验证,而不依赖系统的根证书库。
自定义 javax.net.ssl.SSLSocket
的代价太高,通常不会通过自定义 javax.net.ssl.SSLSocket
来修改服务端证书的合法性验证过程。以此为基础,从上面的分析中不难看出,要想定制 OpenSSLSocketImpl
的证书验证过程,则必然要改变 SSLParametersImpl
,要改变 OpenSSLSocketImpl
的 SSLParametersImpl
,则必然需要修改 SSLSocketFactory
。修改 SSLSocketFactory
常常是一个不错的方法。
在 Java 中,SSLContext
正是被设计用于这一目的。创建定制了 SSLParametersImpl
,即定制了 TrustManager
的 SSLSocketFactory
的方法如下:
TrustManager[] trustManagers = new TrustManager[] { new HelloX509TrustManager() };;
SSLContext context = null;
try {
context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, new SecureRandom());
} catch (NoSuchAlgorithmException e) {
Log.i(TAG,"NoSuchAlgorithmException INFO:"+e.getMessage());
} catch (KeyManagementException e) {
Log.i(TAG, "KeyManagementException INFO:" + e.getMessage());
}
HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory());
SSLContext
的相关方法实现(位于libcore/ojluni/src/main/java/javax/net/ssl/SSLContext.java
)如下:
private final SSLContextSpi contextSpi;
. . . . . .
public static SSLContext getInstance(String protocol)
throws NoSuchAlgorithmException {
GetInstance.Instance instance = GetInstance.getInstance
("SSLContext", SSLContextSpi.class, protocol);
return new SSLContext((SSLContextSpi)instance.impl, instance.provider,
protocol);
}
. . . . . .
public final void init(KeyManager[] km, TrustManager[] tm,
SecureRandom random)
throws KeyManagementException {
contextSpi.engineInit(km, tm, random);
}
/**
* Returns a <code>SocketFactory</code> object for this
* context.
*
* @return the <code>SocketFactory</code> object
* @throws IllegalStateException if the SSLContextImpl requires
* initialization and the <code>init()</code> has not been called
*/
public final SSLSocketFactory getSocketFactory() {
return contextSpi.engineGetSocketFactory();
}
其中 SSLContextSpi
为 OpenSSLContextImpl
,该类的实现(位于external/conscrypt/src/main/java/org/conscrypt/OpenSSLContextImpl.java
)如下:
package org.conscrypt;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.SecureRandom;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContextSpi;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
/**
* OpenSSL-backed SSLContext service provider interface.
*/
public class OpenSSLContextImpl extends SSLContextSpi {
/**
* The default SSLContextImpl for use with
* SSLContext.getInstance("Default"). Protected by the
* DefaultSSLContextImpl.class monitor.
*/
private static DefaultSSLContextImpl DEFAULT_SSL_CONTEXT_IMPL;
/** TLS algorithm to initialize all sockets. */
private final String[] algorithms;
/** Client session cache. */
private final ClientSessionContext clientSessionContext;
/** Server session cache. */
private final ServerSessionContext serverSessionContext;
protected SSLParametersImpl sslParameters;
/** Allows outside callers to get the preferred SSLContext. */
public static OpenSSLContextImpl getPreferred() {
return new TLSv12();
}
protected OpenSSLContextImpl(String[] algorithms) {
this.algorithms = algorithms;
clientSessionContext = new ClientSessionContext();
serverSessionContext = new ServerSessionContext();
}
/**
* Constuctor for the DefaultSSLContextImpl.
*
* @param dummy is null, used to distinguish this case from the public
* OpenSSLContextImpl() constructor.
*/
protected OpenSSLContextImpl() throws GeneralSecurityException, IOException {
synchronized (DefaultSSLContextImpl.class) {
this.algorithms = null;
if (DEFAULT_SSL_CONTEXT_IMPL == null) {
clientSessionContext = new ClientSessionContext();
serverSessionContext = new ServerSessionContext();
DEFAULT_SSL_CONTEXT_IMPL = (DefaultSSLContextImpl) this;
} else {
clientSessionContext = DEFAULT_SSL_CONTEXT_IMPL.engineGetClientSessionContext();
serverSessionContext = DEFAULT_SSL_CONTEXT_IMPL.engineGetServerSessionContext();
}
sslParameters = new SSLParametersImpl(DEFAULT_SSL_CONTEXT_IMPL.getKeyManagers(),
DEFAULT_SSL_CONTEXT_IMPL.getTrustManagers(), null, clientSessionContext,
serverSessionContext, algorithms);
}
}
/**
* Initializes this {@code SSLContext} instance. All of the arguments are
* optional, and the security providers will be searched for the required
* implementations of the needed algorithms.
*
* @param kms the key sources or {@code null}
* @param tms the trust decision sources or {@code null}
* @param sr the randomness source or {@code null}
* @throws KeyManagementException if initializing this instance fails
*/
@Override
public void engineInit(KeyManager[] kms, TrustManager[] tms, SecureRandom sr)
throws KeyManagementException {
sslParameters = new SSLParametersImpl(kms, tms, sr, clientSessionContext,
serverSessionContext, algorithms);
}
@Override
public SSLSocketFactory engineGetSocketFactory() {
if (sslParameters == null) {
throw new IllegalStateException("SSLContext is not initialized.");
}
return Platform.wrapSocketFactoryIfNeeded(new OpenSSLSocketFactoryImpl(sslParameters));
}
如我们前面讨论,验证服务端证书合法性是 PKI 体系中,保障系统安全极为关键的环节。如果不验证服务端证书的合法性,则即使部署了 HTTPS,HTTPS 也将形同虚设,毫无价值。因而在我们自己实现的 X509TrustManager
中,加载预埋的根证书,并据此验证服务端证书的合法性必不可少,这一检查在 checkServerTrusted()
中完成。然而为了使我们实现的 X509TrustManager
功能更完备,在根据我们预埋的根证书验证失败后,我们再使用系统默认的 X509TrustManager
做验证,像下面这样:
private final class HelloX509TrustManager implements X509TrustManager {
private X509TrustManager mSystemDefaultTrustManager;
private X509Certificate mCertificate;
private HelloX509TrustManager() {
mCertificate = loadRootCertificate();
mSystemDefaultTrustManager = systemDefaultTrustManager();
}
private X509Certificate loadRootCertificate() {
String certName = "netease.crt";
X509Certificate certificate = null;
InputStream certInput = null;
try {
certInput = new BufferedInputStream(MainActivity.this.getAssets().open(certName));
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
certificate = (X509Certificate) certificateFactory.generateCertPath(certInput).getCertificates().get(0);
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} finally {
if (certInput != null) {
try {
certInput.close();
} catch (IOException e) {
}
}
}
return certificate;
}
private X509TrustManager systemDefaultTrustManager() {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
mSystemDefaultTrustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
for (X509Certificate certificate : chain) {
try {
certificate.verify(mCertificate.getPublicKey());
return;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
}
}
mSystemDefaultTrustManager.checkServerTrusted(chain, authType);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return mSystemDefaultTrustManager.getAcceptedIssuers();
}
}
此外,也可以不自己实现 X509TrustManager
,而仅仅修改 X509TrustManager
所用的根证书库,就像下面这样:
private TrustManager[] createX509TrustManager() {
CertificateFactory cf = null;
InputStream in = null;
TrustManager[] trustManagers = null
try {
cf = CertificateFactory.getInstance("X.509");
in = getAssets().open("ca.crt");
Certificate ca = cf.generateCertificate(in);
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(null, null);
keystore.setCertificateEntry("ca", ca);
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keystore);
trustManagers = tmf.getTrustManagers();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return trustManagers;
}
自己实现 X509TrustManager
接口和通过 TrustManagerFactory
,仅定制 KeyStore
这两种创建 X509TrustManager
对象的方式,当然是后一种方式更好一些了。如我们前面看到的,系统的 X509TrustManager
实现 RootTrustManager
集成自 X509ExtendedTrustManager
,而不是直接实现的 X509TrustManager
接口 。JCA 的接口层也在随着新的安全协议和 SSL 库的发展在不断扩展,在具体的 Java 加密服务实现中,可能会实现并依赖这些扩展的功能,如上面看到的 X509TrustManager
,而且加密服务的实现中常常通过反射,来动态依赖一些扩展的接口。因而,自己实现 X509TrustManager
接口时,以及其它加密相关的接口时,如 SSLSocket
等,可能会破坏一些功能。
很多时候可以看到,为了使用私有 CA 签名的证书,而定制域名匹配验证的逻辑,即自己实现 HostnameVerifier
。不过通常情况下,网络库都会按照规范对域名与证书的匹配性做严格的检查,因而不是那么地有必要,除非域名证书有什么不那么规范的地方。
关于证书钉扎,在使用私有 CA 签名的证书时,通常似乎也没有那么必要。
参考文章:
Android实现https网络通信之添加指定信任证书/信任所有证书
Java https请求 HttpsURLConnection
Done。
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者韩鹏飞授权发布。