参考:
https://segmentfault.com/a/1190000009002353?sort=newest
https://zhuanlan.zhihu.com/p/353571366
https://juejin.cn/post/6863295544828444686
HTTPS=HTTP+TLS,其它的协议也类似,如FTPS=FTP+TLS
1) ClientHello
2)Server Hello
3)证书校验
Client端和Server端,最终都会用相同的算法将pre-master secret(预主密钥)转换成master secret(主密钥),通过主密钥可以生成session key。两者后续的通信交互数据,将通过session key进行加密。
参考:https://www.laoqingcai.com/tls1.2-premasterkey/
4)Client 握手结束通知
5)Server 握手结束通知
6)Client 开始HTTPS通讯
两者后续的通信交互数据,将通过session key进行加密。所以中间人即使截获数据,也无法解析。
证书=公钥+(公钥+元信息)的签名
其中的元信息包括:
签名 = 计算摘要 + 对摘要值私钥加密
CA:Certificate Authority,专门用自己的私钥 给别人进行签名的机构
注意,计算签名时,是对整个证书文件计算签名,也就是对【元信息+公钥】计算签名,而不只是对公钥计算签名。
(参考:https://blog.csdn.net/bluishglc/article/details/123617558)
关键过程:用信任CA库里CA证书(公钥),验证网站的证书文件里的签名
参考:
https://www.jianshu.com/p/46e48bc517d0
https://www.cnblogs.com/xiaxveliang/p/13183175.html
我们使用End-user Certificates来确保加密传输数据的公钥(public key)不被篡改,而又如何确保end-user certificates的合法性呢?
这个认证过程跟公钥的认证过程类似,首先获取颁布end-user certificates的CA的证书,然后验证end-user certificates的signature。一般来说,root CAs不会直接颁布end-user certificates的,而是授权给多个二级CA,而二级CA又可以授权给多个三级CA,这些中间的CA就是Intermediates CAs,它们才会颁布end-user certificates。
但是Intermediates Certificates的可靠性又如何保证呢?这就是涉及到证书链,Certificate Chain ,链式向上验证证书,直到Root Certificates,如下图:
中间CA的证书怎么获取?
以百度的TLS证书进行举例,百度服务器证书 签发者公钥(中间机构公钥)通过下图中的URI获取:
参考:
https://shunix.com/ssl-pinning/
https://zhuanlan.zhihu.com/p/58204817
默认情况下,只要网站证书的Root CA,属于系统信任的Root CA集合(例如,安卓中系统默认信任 /system/etc/security/ 中CA证书对应的CA)。
对于www.example.com,可能出现以下情况:
(1)情况A:
某个系统信任的Root CA,授权给了可靠的Intermediate CA 1,Intermediate CA 1给www.example.com颁发了一个合法的证书1;
同时该Root CA也授权给了不可靠的Intermediate CA 2(不可靠的原因可能是私钥被泄露),Intermediate CA 2给 www.example.com颁发了一个证书2。
这时候我们希望只信任证书1而不信任证书2,否则一些中间人拿到了证书2,就可以伪装成合法的www.example.com。
这通过修改信任CA集合是较难实现的,因为两个证书的根信任锚是相同的Root CA。当然,可以从信任集中删除Root CA,再添加Intermediate CA 1而不添加Intermediate CA 2。但这意味着我们需要移除Root CA。通常,一个Root CA会作为成千上万个证书的根信任锚,移除Root CA可能引发过大的影响。
(2)情况B:
系统的可信CA集合被篡改。例如,安卓系统在被Root的情况下,用户可以修改系统信任证书(方法例如:https://github.com/doug-leith/cydia)。
这种情况下,app可能需要只信任特定的某个(某些)证书。
原理:
可以采用证书固定。只有当网站的证书链中,至少有一个节点的证书全部内容/证书公钥,跟客户端预埋的证书的内容相匹配,我们的客户端才信任此证书链。
证书固定 与 限制可信CA 的关系
如果把某个Root CA的证书固定起来,那就相当于设置该Root CA为唯一可信的Root CA。
被固定的证书可以是(一般是)某个中间CA的证书。这样,不再是所有以trusted Root CA为根的证书链都仍旧可信了。只有子节点包含该中间CA的证书链才可信。
被固定的证书的Root CA可以不在系统trusted Root CA集合中。
具体实现技术上,SSL Pinning可以分为Certificate Pinning(证书固定)和Public Key Pinning(公钥固定)
把证书文件打包进安装包,将app设置为仅接受指定的内置证书,而不接受操作系统内置的CA根证书对应的任何证书。
提取证书中的公钥并内置到App中,通过与服务器对比公钥值,来验证连接的合法性。我们在申请证书时,公钥在证书的续期前后可以保持不变,所以可以解决证书有效期问题。
// kotlin语法
// 加载证书文件
val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt"))
// 使用CertificateFactory生成一个X509Certificate的实例
val ca: X509Certificate = caInput.use {
cf.generateCertificate(it) as X509Certificate
}
System.out.println("ca=" + ca.subjectDN)
// 创建一个KeyStore实例,并把前边的X509Certificate实例加进去,并起一个别名"ca"
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType).apply {
load(null, null)
setCertificateEntry("ca", ca)
}
// 创建一个TrustManagerFactory实例,并且使用前边的KeyStore实例进行初始化
val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
init(keyStore)
}
// 创建一个SSLContext实例,并且使用前面的TrustManagerFactory实例的trustManagers进行初始化
val context: SSLContext = SSLContext.getInstance("TLS").apply {
init(null, tmf.trustManagers, null)
}
// 创建HttpsURLConnection实例urlConnection
val url = URL("https://certs.cac.washington.edu/CAtest/")
val urlConnection = url.openConnection() as HttpsURLConnection
// 将SSLContext实例context的socketFactory属性,赋值给urlConnection
urlConnection.sslSocketFactory = context.socketFactory
val inputStream: InputStream = urlConnection.inputStream
copyInputStreamToOutputStream(inputStream, System.out)
需要在Manifest文件的android:networkSecurityConfig属性加上对应的配置内容,示例如下:
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.comdomain>
<trust-anchors>
<certificates src="@raw/my_ca"/>
trust-anchors>
domain-config>
<domain-config>
<domain includeSubdomains="true">example.comdomain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=pin>
<pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=pin>
pin-set>
domain-config>
network-security-config>
关于NSC的详细内容,可以参考论文:
[USENIX Sec’21] Why Eve and Mallory Still Love Android: Revisiting TLS (In)Security in Android Applications
或者直接参考Google的官网文档:
https://developer.android.com/training/articles/security-config
计划后续写一篇博客详细介绍Google Android的NSC。
参考:http://hanpfei.github.io/2018/03/20/android_cert_mgr_and_verify/
SSL Pinning机制中,客户端将特定域名的证书与特定的签发者绑定。即,对某个域名,客户端只承认特定CA为该域名签发的证书,而不承认其它 CA 为该域名签发的证书。
AOSP 源码库中,CA 根证书主要存放在 system/ca-certificates 目录下,而在 Android 系统中,则存放在 /system/etc/security/ 目录下:
cacerts_google 目录下的根证书,主要用于 system/update_engine、external/libbrillo 和 system/core/crash_reporter 等模块
cacerts 目录下的根证书则用于所有的应用。cacerts 目录下的根证书,即 Android 系统的根证书库,像下面这样:
它们都是 PEM 格式的 X.509 证书。
Android 系统通过 SystemCertificateSource、DirectoryCertificateSource 和 CertificateSource 等类管理系统根证书库。
CertificateSource定义了可以对根证书库执行的操作,主要是对根证书的获取和查找:
位于frameworks/base/core/java/android/security/net/config/CertificateSource.java
DirectoryCertificateSource 类提供证书的创建、获取和查找操作:
位于frameworks/base/core/java/android/security/net/config/DirectoryCertificateSource.java
获取根证书库的 getCertificates() 操作在第一次被调用时,遍历文件系统,并加载系统所有的根证书文件,并缓存起来,以备后面访问。
根证书的查找操作,主要依据证书文件的文件名进行,证书文件被要求以 [SubjectName 的哈希值].[Index] 的形式命名。
SystemCertificateSource 类定义了系统根证书库的路径,以及无效一个根证书的机制:
位于frameworks/base/core/java/android/security/net/config/SystemCertificateSource.java
Android 系统的根证书位于 /system/etc/security/cacerts/ 目录下。用户可以通过将特定根证书复制到用户配置目录的 cacerts-removed 目录下来无效一个根证书。
OpenSSLSocketImpl.startHandshake() 通过 NativeCrypto 类的 SSL_do_handshake() 方法执行握手操作:
(NativeCrypto 位于external/conscrypt/src/main/java/org/conscrypt/NativeCrypto.java)
SSL_do_handshake() 方法的第三参数是一个接口:SSLHandshakeCallbacks
SSLHandshakeCallbacks是NativeCrypto 类定义的接口,其中包含一组回调函数;这组回调函数,是SSL_do_handshake() 的参数,在SSL_do_handshake() 中被传入native层
SSLHandshakeCallbacks 中的方法之一是verifyCertificateChain():
/**
* A collection of callbacks from the native OpenSSL code that are
* related to the SSL handshake initiated by SSL_do_handshake.
*/
public interface SSLHandshakeCallbacks {
/**
* Verify that we trust the certificate chain is trusted.
*
* @param sslSessionNativePtr pointer to a reference of the SSL_SESSION
* @param certificateChainRefs chain of X.509 certificate references
* @param authMethod auth algorithm name
*
* @throws CertificateException if the certificate is untrusted
*/
public void verifyCertificateChain(long sslSessionNativePtr,
long[] certificateChainRefs,
String authMethod)
throws CertificateException;
verifyCertificateChain()的参数:
SSLHandshakeCallbacks中的回调方法的实现在 OpenSSLSocketImpl 。 OpenSSLSocketImpl中,verifyCertificateChain()的实现如下:
@SuppressWarnings("unused") // used by NativeCrypto.SSLHandshakeCallbacks
@Override
public void verifyCertificateChain(long sslSessionNativePtr, long[] certRefs, String authMethod)
throws CertificateException {
try {
X509TrustManager x509tm = sslParameters.getX509TrustManager();
if (x509tm == null) {
throw new CertificateException("No X.509 TrustManager");
}
if (certRefs == null || certRefs.length == 0) {
throw new SSLException("Peer sent no certificate");
}
OpenSSLX509Certificate[] peerCertChain = new OpenSSLX509Certificate[certRefs.length];
for (int i = 0; i < certRefs.length; i++) {
peerCertChain[i] = new OpenSSLX509Certificate(certRefs[i]);
}
// Used for verifyCertificateChain callback
handshakeSession = new OpenSSLSessionImpl(sslSessionNativePtr, null, peerCertChain,
getHostnameOrIP(), getPort(), null);
boolean client = sslParameters.getUseClientMode();
if (client) {
Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this);
if (sslParameters.isCTVerificationEnabled(getHostname())) {
byte[] tlsData = NativeCrypto.SSL_get_signed_cert_timestamp_list(
sslNativePointer);
byte[] ocspData = NativeCrypto.SSL_get_ocsp_response(sslNativePointer);
CTVerifier ctVerifier = sslParameters.getCTVerifier();
CTVerificationResult result =
ctVerifier.verifySignedCertificateTimestamps(peerCertChain, tlsData, ocspData);
if (result.getValidSCTs().size() == 0) {
throw new CertificateException("No valid SCT found");
}
}
} else {
String authType = peerCertChain[0].getPublicKey().getAlgorithm();
Platform.checkClientTrusted(x509tm, peerCertChain, authType, this);
}
} catch (CertificateException e) {
throw e;
} catch (Exception e) {
throw new CertificateException(e);
} finally {
// Clear this before notifying handshake completed listeners
handshakeSession = null;
}
}
这里面,verifyCertificateChain() 从 OpenSSLSocketImpl的 sslParameters 获得 X509TrustManager:
X509TrustManager x509tm = sslParameters.getX509TrustManager();
然后在 Platform.checkServerTrusted() 中执行服务端证书合法有效性的检查:
Platform.checkServerTrusted(x509tm, peerCertChain, authMethod, this);
Platform.checkServerTrusted在com.android.org.conscrypt.Platform类(external/conscrypt/src/compat/java/org/conscrypt/Platform.java):
public static void checkServerTrusted(X509TrustManager tm, X509Certificate[] chain,
String authType, OpenSSLSocketImpl socket) throws CertificateException {
if (!checkTrusted("checkServerTrusted", tm, chain, authType, Socket.class, socket)
&& !checkTrusted("checkServerTrusted", tm, chain, authType, String.class,
socket.getHandshakeSession().getPeerHost())) {
tm.checkServerTrusted(chain, authType);
}
}
可以看到,Platform.checkServerTrusted()会调用X509TrustManager.checkServerTrusted()来完成检查。
其中的X509TrustManager实例来源于OpenSSLSocketImpl 的sslParameters,如前文所述:
X509TrustManager x509tm = sslParameters.getX509TrustManager();
那OpenSSLSocketImpl 的 sslParameters 又来自于哪里呢?来源于构造函数,例如:
protected OpenSSLSocketImpl(SSLParametersImpl sslParameters) throws IOException {
this.socket = this;
this.peerHostname = null;
this.peerPort = -1;
this.autoClose = false;
this.sslParameters = sslParameters;
}
而OpenSSLSocketFactoryImpl类会实例化OpenSSLSocketImpl:
package org.conscrypt;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
public class OpenSSLSocketFactoryImpl extends javax.net.ssl.SSLSocketFactory {
private final SSLParametersImpl sslParameters;
private final IOException instantiationException;
…………
@Override
public Socket createSocket() throws IOException {
if (instantiationException != null) {
throw instantiationException;
}
return new OpenSSLSocketImpl((SSLParametersImpl) sslParameters.clone());
}
@Override
public Socket createSocket(String hostname, int port) throws IOException, UnknownHostException {
return new OpenSSLSocketImpl(hostname, port, (SSLParametersImpl) sslParameters.clone());
}
@Override
public Socket createSocket(String hostname, int port, InetAddress localHost, int localPort)
throws IOException, UnknownHostException {
return new OpenSSLSocketImpl(hostname,
port,
localHost,
localPort,
(SSLParametersImpl) sslParameters.clone());
}
@Override
public Socket createSocket(InetAddress address, int port) throws IOException {
return new OpenSSLSocketImpl(address, port, (SSLParametersImpl) sslParameters.clone());
}
@Override
public Socket createSocket(InetAddress address,
int port,
InetAddress localAddress,
int localPort)
throws IOException {
return new OpenSSLSocketImpl(address,
port,
localAddress,
localPort,
(SSLParametersImpl) sslParameters.clone());
}
}
后面的细节暂时略过不看。
总结:
OpenSSLSocketImpl.startHandshake() 和 NativeCrypto.SSL_do_handshake() 执行完整的 SSL/TLS 握手过程。
证书合法性验证是 SSL/TLS 握手的一个重要步骤。该过程通过 native层调用Java 层的回调方法 SSLHandshakeCallbacks.verifyCertificateChain() 来完成。
回调方法的实现在OpenSSLSocketImpl。
OpenSSLSocketImpl.verifyCertificateChain()调用Platform.checkServerTrusted(),调用RootTrustManager.checkServerTrusted() ,调用NetworkSecurityTrustManager.checkServerTrusted() ,将真正根据系统根证书库执行证书合法性验证的 TrustManagerImpl 和 SSL/TLS 握手过程结合起来。
OpenSSLSocketFactoryImpl 将 OpenSSLSocketImpl 和 SSLParametersImpl 粘起来。
SSLParametersImpl 将 OpenSSLSocketImpl 和 RootTrustManager 粘起来。
NetworkSecurityConfig 将 RootTrustManager 和 NetworkSecurityTrustManager
粘起来。NetworkSecurityConfig、NetworkSecurityTrustManager 和
TrustedCertificateStoreAdapter 将 TrustManagerImpl 和管理系统根证书库的
SystemCertificateSource 粘起来。
TrustManagerImpl 是证书合法性验证的核心,它会查找系统根证书库,并验证服务端证书的合法性做。
这个过程的调用栈如下:
com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted()
android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted()
android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted()
android.security.net.config.RootTrustManager.checkServerTrusted()
com.android.org.conscrypt.Platform.checkServerTrusted()
com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain()
com.android.org.conscrypt.NativeCrypto.SSL_do_handshake()
com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake()
com.android.okhttp.Connection.connectTls()
在实际的开发过程中,有时为了节省昂贵的购买证书的费用,而想要自己给自己的服务器的域名签发域名证书,这即是私有 CA 签名的证书。为了能够使用这种证书,需要在客户端预埋根证书,并对客户端证书合法性验证的过程进行干预,通过我们预埋的根证书为服务端的证书做合法性验证,而不依赖系统的根证书库。
要想定制 OpenSSLSocketImpl 的证书验证过程,必然要改变 SSLParametersImpl;要改变 OpenSSLSocketImpl 的 SSLParametersImpl,则必然需要修改 SSLSocketFactory。修改 SSLSocketFactory 常常是一个不错的方法。
两种实现手段:
(1)自己实现 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();
}
}
(2)仅修改 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;
}
服务端也可能校验客户端的证书(来确保客户端是合法的客户端),这种情况下需要把客户端预存的证书导入中间人抓包工具中。
可以参考:
https://www.anquanke.com/post/id/272672