【网络安全】https与证书原理 您所在的位置:网站首页 网络安全https 【网络安全】https与证书原理

【网络安全】https与证书原理

2024-07-09 18:17| 来源: 网络整理| 查看: 265

SSL Pinning 1 HTTPS协议流程

参考: 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

Client 首先发送本地的 TLS 版本、支持的加密算法套件,并且生成一个随机数 R1 。

2)Server Hello

Server 端确认 TLS 版本号。从 Client 端支持的加密套件中选取一个,并生成一个随机数 R2 一起发送给 Client。Server 向 Client 发送自己的CA证书(包含公钥、证书签名)。

3)证书校验

Client 判断证书签名与CA证书是否合法有效Client 生成随机数pre-master secret,并使用Server发过来的公钥对pre-master secret进行加密,将加密后的pre-master secret送给Server。这一步结束后,Client 与 Server 就都有 R1、R2、pre-master secret 了,两端便可以使用这 3 个随机数独立生成 对称会话密钥了,避免了对称密钥的传输,同时可以 根据会话密钥生成 6 个密钥(P1~P6) 用作后续身份验证

Client端和Server端,最终都会用相同的算法将pre-master secret(预主密钥)转换成master secret(主密钥),通过主密钥可以生成session key。两者后续的通信交互数据,将通过session key进行加密。 在这里插入图片描述 参考:https://www.laoqingcai.com/tls1.2-premasterkey/

4)Client 握手结束通知

Client 使用 P1 将之前的握手信息的 hash 值加密并发送给 ServerClient 发送握手结束消息

5)Server 握手结束通知

Server 计算之前的握手信息的 hash 值,并与 P1 解密客户端发送的握手信息的 hash 对比校验验证通过后,使用 P2 将之前的握手信息的 hash 值加密并发送给 Client

6)Client 开始HTTPS通讯

Client 计算之前的握手信息的 hash 值,并与 P2 解密 Server 发送的握手信息的 hash 对比校验验证通过后,开始发起 HTTPS 请求。

两者后续的通信交互数据,将通过session key进行加密。所以中间人即使截获数据,也无法解析。

2 证书相关 证书文件

证书=公钥+(公钥+元信息)的签名

图片名称

其中的元信息包括:

Subject(主体信息): Common Name(CN)通用名称SANOrganizationOrganization Unit(OU)CountryStateCityAddressPostal code Issuer(签发者信息): Common Name(CN)通用名称OrganizationOrganization Unit(OU)CountryStateCityAddressPostal code Validity(有效期): Not Before(签发日期)Not After(过期时间) Signature AlgorithmSerial NumberVersionExtensions(扩展信息):只在证书版本2、3中才有

因此,证书的结构大致如下: 在这里插入图片描述

CA

签名 = 计算摘要 + 对摘要值私钥加密 CA:Certificate Authority,专门用自己的私钥 给别人进行签名的机构

签发证书的过程

注意,计算签名时,是对整个证书文件计算签名,也就是对【元信息+公钥】计算签名,而不只是对公钥计算签名。 在这里插入图片描述 (参考:https://blog.csdn.net/bluishglc/article/details/123617558)

证书的验证过程

关键过程:用信任CA库里CA证书(公钥),验证网站的证书文件里的签名

在TLS握手的过程中,客户端得到了网站的证书客户端打开证书,查看是哪个CA签名的这个证书在自己信任的CA库中,找相应CA的证书(包含CA的公钥),用CA证书里面的公钥解密网站证书上的签名,取出网站证书的摘要,然后用同样的算法(比如sha256)算出网站证书的摘要,如果摘要和签名中的摘要对的上,说明这个证书是合法的,且没被人篡改过读出里面的CN,对于网站的证书,里面一般包含的是域名检查里面的域名和自己访问网站的域名对不对的上,对的上,就说明这个证书确实是颁发给这个网站的到此为止检查通过 证书链的验证

参考: 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获取: 在这里插入图片描述

3 SSL Pinning

参考: https://shunix.com/ssl-pinning/ https://zhuanlan.zhihu.com/p/58204817

3.1 原理

默认情况下,只要网站证书的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集合中。

3.2 实现方案

具体实现技术上,SSL Pinning可以分为Certificate Pinning(证书固定)和Public Key Pinning(公钥固定)

3.2.1 证书固定

把证书文件打包进安装包,将app设置为仅接受指定的内置证书,而不接受操作系统内置的CA根证书对应的任何证书。

3.2.2 公钥固定

提取证书中的公钥并内置到App中,通过与服务器对比公钥值,来验证连接的合法性。我们在申请证书时,公钥在证书的续期前后可以保持不变,所以可以解决证书有效期问题。

3.3 实例 3.3.1 证书固定实例:基于TrustManagerFactory // 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) 3.3.2 证书固定实例:基于NSC配置文件

需要在Manifest文件的android:networkSecurityConfig属性加上对应的配置内容,示例如下:

example.com example.com 7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y= fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=

关于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。

4 安卓中的SSL Pinning

参考:http://hanpfei.github.io/2018/03/20/android_cert_mgr_and_verify/

SSL Pinning机制中,客户端将特定域名的证书与特定的签发者绑定。即,对某个域名,客户端只承认特定CA为该域名签发的证书,而不承认其它 CA 为该域名签发的证书。

4.1 Android 的根证书管理

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 目录下来无效一个根证书。

4.2 证书链合法性验证

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()的参数:

指向一个sslSession的指针X.509 证书链认证算法名称

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 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()

4.3 自定义证书(SSL Pinning)

在实际的开发过程中,有时为了节省昂贵的购买证书的费用,而想要自己给自己的服务器的域名签发域名证书,这即是私有 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; } 4.4 双向认证

服务端也可能校验客户端的证书(来确保客户端是合法的客户端),这种情况下需要把客户端预存的证书导入中间人抓包工具中。

可以参考: https://www.anquanke.com/post/id/272672



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有