一篇文章搞懂密码学基础及SSLTLS协议

七月 18, 2025 [网络与安全] #obsidian #协议与标准 #安全 #密码学

一篇文章搞懂密码学基础及SSLTLS协议

SSL协议是现代网络通信中重要的一环,它提供了传输层上的数据安全。为了方便大家的理解,本文将先从加密学的基础知识入手,然后展开对SSL协议原理、流程以及一些重要的特性的详解,最后会扩展介绍一下国密SSL协议的差异、安全性以及TLS 1.3的关键新特性。

限于篇幅以及个人知识水平,本文不会涉及过于细节的内容。特别地,本文将不涉及算法的具体原理,也不涉及实际代码的实现。而是试图以图表等直观的方式来了解基本的原理以及流程。

密码学基础

古典的密码

密码学的历史可以追溯到很久以前,早在罗马共和国时期,据说凯撒就使用凯撒密码和他的将军进行通信。

凯撒密码

Caesar Cipher

凯撒密码就是一个简单地移位操作。凯撒密钥非常容易被破解,使用暴力破解的方式把密钥从0到25都尝试一遍就可以了。

简单替换密码

Simple Substitution Cipher

简单替换密码,其明文字母表和密文字母表之间是一种随机的映射关系。这种方式密钥空间为26! ~= 4 * 10^26,这已经无法使用暴力破解方式来找到正确的密钥。但是可以使用频率分析来破译。

Enigma密码机

二战时期德国使用的一系列转子机械加解密机器。尽管此机器的安全性较高,但盟军的密码学家们还是成功地破译了大量由这种机器加密的信息。

Enigma

主要弱点:

对称密码

块密码和流密码

上面几种密码其实都属于对称密码的范畴,对称加密算法可以分为块密码和流密码两种:

AES (Advanced Encryption Standard)

AES是目前最常用的对称算法之一。对称加密算法,顾名思义,就是加密和解密使用相同的密钥。发送方使用密钥K对明文P进行加密得到密文C,然后将密文C发送给接收方,接收方使用相同的密钥K对密文进行解密,得到明文P。

symmetric-cipher-flow

AES采用的是Rijndael算法,下图是Rijndael加密中一轮的操作:

RijndaelEncrypt

每一轮都会进行字节替换、行位移、列混合和轮密钥异或,使得输入的位得到充分的混淆。一个块的加密会经过很多轮的操作,最终得到密文。

因为这4轮操作都是可逆的,解密的时候就是一个相反的过程。

块密码的模式

模式

分组密码算法只能加密固定长度的分组,但是我们需要加密的明文长度可能会超过分组密码的分组长度,这时就需要对分组密码算法进行迭代,以便将一段很长的明文全部加密。而块与块之间进行迭代的方法就称为分组密码的模式(mode) 。

常用模式由:ECB、CBC、OFB、CFB、CTR等。这边限于篇幅,仅介绍ECB、CBC、CTR三种。

ECB模式

ECB是最简单的一种模式,每个块是独立进行加密的。将明文分组加密之后的结果直接成为密文分组。当最后一个明文分组的内容小于分组长度时,需要用一些特定的数据进行填充(padding)。

Ecb_encryption

Ecb_decryption

ECB模式有非常显著的缺点:同样的明文块会被加密成相同的密文块;因此,它不能很好的隐藏数据模式。在某些场合,这种方法不能提供严格的数据保密性。

比如下面的企鹅图,用ECB模式加密之后得到中间的图,还是能很明显地看出图片的轮廓,不能很好的保护数据的机密性。而用其他模式加密之后,就得到如第三个图所示的结果,已经看不出明显的特征。

原图 ECB模式其他模式

CBC模式

CBC模式中,每个明文块先与前一个密文块进行异或,再进行加密。所以每个明文块都依赖于前面的所有明文块。同时,为了保证每条消息的唯一性,在第一个块中需要使用初始化向量(IV)。

Cbc_encryption

Cbc_decryption

CBC是TLS1.2时代最为常用的工作模式。它没有ECB模式那个相同原文加密称相同密文的问题,但是也因此导致其并行计算能力不如ECB模式。

CTR模式

计数器模式其实是将块加密转换成了流加密,它对一个逐次累加的计数器进行加密,然后用加密的比特序列与明文进行XOR操作得到密文。

CTR_encryption

CTR_decryption

计数器的作用跟CBC模式中的IV类似,保证相同的明文会加密成不同的密文。相比CBC等模式,CTR模式还有如下优点:它非常适合并行计算、错误密文中的对应比特只会影响明文中的对应比特。

AEAD(Authenticated Encryption with Associated Data)

前面的那些模式都已经被发现存在不同程序的缺点或问题。在TLS1.3时代,只保留了AEAD类型的加密模式。AEAD在加密的同时增加了认证的功能,常用的有GCM、CCM、Ploy1305。

GCM(Galois/Counter Mode)

GCM中的G是指的GMAC(关于MAC我们稍后会讲到),C就是指的我们前面提到的CTR计数器模式。下图右上角的部分就是上面的Counter模式加密,剩余部分则是GMAC。最终的结果包含初始计数器值、加密密文和MAC值。

GCM-Galois_Counter_Mode_with_IV

公钥密码(非对称密码)

在使用对称加密时,一定会碰到密钥分发(密钥交换) 的问题。使用预先共享密钥具有局限性,需要一种安全的方式将密钥交给对方。于是就引出了公钥密码。

公钥密码算法(RSA、国密SM2)

最常用的公钥密钥算法要数大名鼎鼎的RSA了,国密中则有SM2算法。公钥密码有两个密钥,其中一个是公开密钥,公开密钥可以散布,另一个是私有密钥,需要自己严密保管。比如Bob要发消息给Alice,Bob用Alice的公钥对消息进行加密,然后发送给Alice,Alice则用自己的私钥进行解密。

Public_key_encryption

Diffie-Hellman密钥交换

另一种常用的公钥密码是DH类算法,可以用下图来形象地解释DH算法的原理。

Diffie-Hellman_Key_Exchange

首先双方协商一个相同的底色(算法参数),然后各自生成自己私有的颜色(相当于私钥),并通过混合得到对应的公有颜色(相当于公钥)。随后双方交换各自的公有颜色,并与自己的私钥颜色混合,最终协商出一个相同的颜色(即交换的密钥)。窃听者就算得到了双方交换的这些信息,也无法生成相同的密钥,求解离散对象问题的困难度保证了DH算法的安全性。

ECDH和ECDHE

ECDH是基于椭圆曲线的DH算法,原理上跟DH基本一样,主要是把有限域上的模幂运算替换成了椭圆曲线上的点乘运算。相比DH算法速度更快,可逆更难。

DH和ECDH都是使用的一个固定密钥,一旦密钥泄漏,以前所有的密文消息就都都破解了。ECDHE则提供了前向安全性,它每次使用一个临时的密钥,基于这个临时密钥进行密钥交换生成会话密钥。就算这个临时密钥泄漏了,也只影响本次SSL会话的消息。

单向散列函数(Hash)

前面的对称密钥和公钥密码解决了信息传输的机密性问题,使我们传输的信息不被窃听。但是还没有解决完整性的问题,消息有可能在中途被“篡改”。所以就轮到单向散列函数出场了。

单向散列函数可以根据输入的信息,计算出一个固定长度的散列值(摘要值),这个散列值可以作为消息的指纹用于检查消息的完整性。修改了原始消息的任意1个比特,最终生成的散列值可能就完全不同了。

Cryptographic_Hash_Function

理想的散列函数具有以下几个性质:

  1. 确定性:同样的消息总是产生同样的散列值
  2. 任何给定消息能够快速计算出散列值
  3. 单向性:无法通过散列值反算出消息
  4. 弱抗碰撞性:难以找到可以生成给定散列值的消息
  5. 强抗碰撞性:难以找到可以生成两条相同散列值的消息
  6. 消息的一个小的改动会导致Hash值的巨大变化

消息认证码(MAC)

单向散列函数虽然保证了消息的完整性,但是聪明的攻击者可以将消息连同其散列值一起篡改了,而接收方却无法进行识别。所以还需要对消息进行认证,传统的认证方法有手写签名、盖章、手印、身份证、口令(其实是个共享密钥)等。在密码领域则可以通过消息认证码的方法进行认证。消息认证码相比单向散列函数,多了一个共享密钥对消息进行认证。攻击者因为没有这个密钥,所以无法伪造出MAC值。

MAC-vs-HASH

HMAC-use-scene

MAC的问题

数字签名(RSA、ECDSA)

为了解决MAC的问题,又引入了数字签名。

数字签名同样属于公钥密码的范畴。跟之前不同的地方是,它使用私钥进行加密(该操作叫作签名),任何人都可以用公钥进行解密(该操作叫作验签)。因为公钥是公开的,所以解决了第三方证明的问题。又因为私钥只有本人具有,没有私钥的人事实上无法生成这段密文,所以也可以防止否认。所以MAC的三个问题都可以由数字签名解决。

Private_key_signing

通常配合散列函数使用,同时保证了完整性,也加快了速度。如下图所示,Alice发送消息给Bob。她先把自己的公钥发送给Bob,接着她对消息先计算一个散列值,然后用自己的私钥对这个散列值进行加密得到消息的签名值。然后她将初始的消息和签名一同发送给Bob。Bob收到之后,对消息进行同样的散列函数计算得到散列值,同时用Alice的公钥对签名数据进行解密,得到解密后散列值,然后在比较两个计算得到散列值是否一致,以验证签名的有效性。

DigitalSignature

数字证书、 CA

到目前为止,我们的公钥密码、数字签名解决了机密性、完整性、可以进行认证以及防止否认等。但其实这一切都是基于一个大前提:就是公钥是属于真正的发送者。如果公钥是伪造的、那么这一切就都失效了。前面提到的数字签名只能保证对方拥有这个公钥对应的私钥,但是没法认证公钥所有者本身的身份。

为了解决这个问题,数字证书应运而出。它的解决方式就是让一个可信的第三方来对公钥进行签名。这个可信的第三方,我们一般称之为Certificate authorities(CA) 。

我们来看看实际证书是什么样子的。其中Subject是证书拥有者的信息,Issuer就是其签发者的信息,Subject Public Key Info是实际的公钥信息,这里用的是RSA 2048位的密钥。证书最后就是CA用自己的私钥对这个证书的签名。任何拥有CA的证书(包含公钥)的人都可以对这个证书进行验证,从而验证公钥所有者的身份。Validity是这个证书的有效期。

$ openssl x509 -in ~/sharefile/certs/rsa-user.pem -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            a2:95:90:e8:e0:f6:3a:e4
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=CN, ST=SH, L=Shanghai, O=Awesome Software, OU=SSL Group, CN=RSA_CA_Cert/emailAddress=ssl@ca.com
        Validity
            Not Before: Nov  5 01:47:05 2018 GMT
            Not After : Nov  2 01:47:05 2028 GMT
        Subject: C=CN, ST=SH, L=Shanghai, O=Awesome Software, OU=SSL Group, CN=RSA_USER/emailAddress=rsa@site.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:f4:a0:b6:57:e7:68:39:98:39:13:11:b9:61:ee:
                    1d:5c:60:c6:53:51:ec:0e:59:53:0e:12:75:1c:5b:
                    b1:b7:e4:dd:4d:c8:c1:b8:eb:07:32:12:a1:fc:50:
                    33:d9:63:4a:30:7f:b4:b5:e9:87:d9:71:33:65:ab:
                    56:54:ba:34:92:06:9a:af:ff:84:8e:a3:29:af:46:
                    61:3b:45:39:c5:a8:0a:f9:ae:fb:d4:2b:20:5a:9d:
                    ce:00:fe:c9:87:b2:f4:d2:f8:bf:b7:2e:7a:79:5e:
                    94:54:7d:f9:09:73:4a:ec:c7:22:01:79:4c:62:11:
                    55:a9:d3:3b:f8:ef:3e:1d:56:e3:2f:34:ef:c6:0f:
                    2c:94:40:01:a3:6f:1e:3b:61:bd:79:00:bf:26:f7:
                    d6:c3:a6:22:22:50:f0:6e:aa:03:1e:ea:d9:e6:ad:
                    4d:a9:60:4f:6d:81:80:f4:f9:a9:89:31:ab:f1:a3:
                    9b:1a:a2:57:ed:44:30:39:fc:3a:3a:c3:6e:c8:a6:
                    db:2e:14:c1:3b:6b:5b:ca:ab:b7:0d:fb:85:39:08:
                    bf:6b:41:c1:f6:42:b1:3f:9c:45:5c:4c:37:8e:7d:
                    c6:18:f8:9b:87:16:80:7f:25:34:8f:14:a9:02:2e:
                    7c:07:c9:8a:21:77:33:03:6a:9c:86:f1:73:9c:c8:
                    2d:f1
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Key Usage:
                Digital Signature, Non Repudiation, Key Encipherment
    Signature Algorithm: sha256WithRSAEncryption
         30:cf:b2:9f:50:ea:d7:0d:e2:87:50:e6:bd:d7:b0:17:12:31:
         b1:9f:59:16:50:60:bc:52:c0:46:7a:43:d0:34:43:48:d0:bd:
         e1:a0:dc:75:a2:60:a0:c9:8f:ed:d4:36:14:18:75:c0:ef:c3:
         92:fa:43:fa:34:5a:12:77:2f:03:00:eb:a6:db:d9:6b:50:ff:
         44:56:22:c6:51:73:73:9a:4b:fd:bb:53:ff:2b:7e:97:55:d3:
         4d:bb:bd:26:69:37:8d:71:30:41:bf:fd:48:40:fc:6f:cd:e2:
         b7:4a:90:6f:a2:11:85:a7:88:d3:61:d5:03:0a:50:98:cc:0e:
         aa:d5:83:38:b4:d5:f0:06:ff:a5:eb:d4:e6:54:14:e9:65:af:
         36:a5:e4:3e:8b:78:18:0b:d6:7c:cc:f1:a3:da:7a:03:fd:89:
         23:f0:e1:3e:af:7b:b1:7a:53:82:11:4a:5e:1d:84:b6:0b:cc:
         96:b4:3a:8a:43:cf:ff:b3:3a:be:47:e0:40:c0:48:15:b4:f3:
         2d:2b:73:b8:07:d2:21:83:3c:c4:4c:c2:31:17:4e:4c:15:da:
         66:fd:06:9a:b7:ed:b5:9e:71:a3:40:0b:39:12:3c:7b:cb:cb:
         a0:af:d0:c7:fe:59:41:35:04:7f:f3:f3:38:d0:d0:ac:7a:15:
         7e:fa:ee:fd

在实际使用中,通常会使用多级证书链,每一级证书由上一级CA签发,并由上一级CA的证书进行验签。最终的Root CA证书只能由它自己签发的,也即用自己的私钥对自己的公钥进行签名,否则就无限递归了。

CA-signing-of-digital-certificates

所以数字证书实际上是一种信任链的传递,将对众多个体的身份认证转移到对少数几个CA的身份认证,可以减少遭到中间人攻击的风险。从公钥密码和证书这些出发就引出了公钥基础设置(PKI) :这是为了能够更有效地运用公钥而制定的一系列规范和规格的总称。

混合密码系统

那么有了公钥密码,是不是就不需要对称密码了呢?公钥密码虽然解决了对称密码的密钥分发问题,但是在计算速度上却远低于对称密码,相差几个数量级。下面是用openssl speed测试RSA1024和AES128的结果。AES128(相同块大小)的速度大概是RSA1024签名的1200倍,是验签的70倍。

$ openssl speed rsa1024
                  sign    verify    sign/s verify/s
rsa 1024 bits 0.000103s 0.000006s   9732.8 162578.8

$ openssl speed -evp aes128
type             16 bytes     64 bytes    256 bytes   1024 bytes   8192 bytes  16384 bytes
aes-128-cbc    1075884.36k  1452740.12k  1510392.06k  1530545.83k  1537414.49k  1537114.11k

因为对称密码和非对称密码各有优缺点,所以在实际应用中通常将两者结合起来,使用非对称密码来完成密钥交换,然后生成会话密钥作为对称密码的密钥,使用对称密码对信息进行加解密。

随机数发生器

截止目前,我们已经解决了很多问题,包括机密性、完整性、认证性、防抵赖,但其实还有一个大问题我们没有解决:即我们的会话密钥如何生成?一个安全的算法其所有的安全性应该都是基于其密钥的安全性,如果我们的密钥可以被轻易破解或预测,那我们前面构建的一切就都土崩瓦解了。所以随机数发生器就在这里起到了至关重要的作用。

随机数同样在防重放攻击中具有重要的作用,所谓重放攻击,就是攻击者将窃听的数据保存下来,在后面再原样发送给接收者,以达到其特定的攻击目的。防重放的常用手段有使用序号、时间戳、随机数等。

这里留个问题给大家思考,因为机密性和完整性的保护,攻击者既不能解密消息,又不能对消息进行篡改,那么重放有什么用呢?

理想的随机数发生器具有以下特性:

密钥派生函数 key derivation function

KDF可用于将密钥材料扩展为更长的密钥或获取所需格式的密钥。TLS 1.2中是用的PRF算法,TLS 1.3中则是用的HKDF算法。

SSL/TLS协议详解

什么是SSL/TLS协议

好了,有了前面的密码学基础,我们就可以正式进入TLS协议的介绍。前面介绍的基本都是独立的算法或者是几个算法结合起来的组件,而SSL/TLS协议则是基于这些底层的算法原语和组件,最终拼装而成的一个成品的密码学协议。

SSL全称是Secure Sockets Layer,安全套接字层,它是由网景公司(Netscape)设计的主要用于Web的安全传输协议,目的是为网络通信提供机密性、认证性及数据完整性保障。如今,SSL已经成为互联网保密通信的工业标准。

SSL最初的几个版本(SSL 1.0、SSL2.0、SSL 3.0)由网景公司设计和维护,从3.1版本开始,SSL协议由因特网工程任务小组(IETF)正式接管,并更名为TLS(Transport Layer Security),发展至今已有TLS 1.0、TLS1.1、TLS1.2这几个版本。目前主流的还是TLS1.2,不过TLS1.3即将是大势所趋。

ProtocolPublishedStatus
SSL 1.0UnpublishedUnpublished
SSL 2.01995Deprecated in 2011 (RFC6176)
SSL 3.01996Deprecated in 2015 (RFC7568)
TLS 1.01999Deprecated in 2020 (RFC8996)[8][9][10]
TLS 1.12006Deprecated in 2020 (RFC8996)[8][9][10]
TLS 1.22008
TLS 1.32018

SSL/TLS协议能够提供的安全目标主要包括如下几个:

协议分层

相信大家对TCP/IP 5层模型已经非常熟悉了,TLS协议就如其名字所说的(Transport Layer Security),用于保障传输层的安全。它位于传输层之上,应用层之下。

TLS-layered-structure

SSL/TLS协议有一个高度模块化的架构,其内部又分为很多子协议:Handshake协议、Alert协议、ChangeCipherSpec协议、Application协议。它们底层都是基于Record协议,记录层协议负责识别不同的上层消息类型以及消息的分段加密认证等。

SSL/TLS协议被设计为一个两阶段协议,分为握手阶段和应用阶段。

握手阶段:也称协商阶段,在这一阶段主要目标就是进行我们前面已经提到过的协商安全参数和算法套件、身份认证(基于数字证书)以及密钥交换生成后续加密通信所使用的密钥。

应用阶段:双方使用握手阶段协商好的密钥进行安全通信。

SSL record

SSL记录层包的格式,跟其下的IP或TCP层类似,所有在SSL会话上交换的数据都按如下格式封装成帧。记录层协议负责识别不同的消息类型,也负责对消息的分段、压缩、消息认证和完整性保护、加密等工作。

TLS-record-structure

一个典型的记录层工作流(分组密码算法)如下:

当加密的数据到了接收端,对方则进行相反的操作:解密数据、验证MAC、重组数据并交给应用层。

所有这些工作都由SSL层完成了,对于上层应用是完全透明的。

对于握手阶段的消息,payload是明文,当然就没有MAC和Padding。其他消息的payload都是密文。

对于流加密算法,没有后面的padding。对于块加密算法的记录,根据使用的算法在payload之前有可选的IV字段。

对于AEAD算法,因为认证已经包含在算法里了,所以没有后面的MAC和Padding字段。payload之前有一个外部nonce字段。

算法套件CipherSuites

在深入握手流程之前,我们先来了解一下算法套件的概念。前面在密码学基础部分我们已经了解到了各种算法,包括认证算法、密钥交换算法、对称密码算法、完整性认证的算法。

TLS 1.2-

一个算法套件是一个SSL连接中用到的这些算法类型的组合,包含如下几个部分:

常见的算法套件类型比如TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384、TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384、ECC_SM2_WITH_SM4_SM3(国密)、ECDHE_SM2_WITH_SM4_SM3(国密)。

CipherSuite1_2

以ECDHE-ECDSA-AES128-GCM-SHA256为例,前面的ECDHE表示密钥交换算法,ECDSA表示身份认证算法,AES128-GCM表示对称加密算法,其中128表示块长度,GCM是其模式,SHA256表示哈希算法。对于AEAD因为消息认证和加密已经合并到一起了,最后的SHA256只表示密钥派生函数的算法,而对于传统的数据加密和认证分开的算法套件,它还表示MAC的算法。

可以通过如下命令查看每种算法套件的详细情况:openssl ciphers -V | column -t | less。

TLS 1.3

对于TLSv1.3,因为已经将密钥交换和身份认证算法从算法套件中独立出去,算法套件只表示加密算法和密钥派生函数。之所以要独立出去,是因为支持的算法越来越多了,导致相乘之后算法套件数量庞大。

CipherSuite1_3

可以用如下命令查看当前支持哪些TLS 1.3算法套件:openssl ciphers -V | column -t | grep 'TLSv1.3'。

SSL handshake

终于进入核心的握手协议部分了。如前所述,SSL握手主要干以下几件事情:首先得商量双方用什么算法套件,然后根据需要认证对方的身份(基于数字证书),最后基于选择的算法套件进行密钥交换生成后续加密通信所使用的密钥。(本节描述的是TLS 1.2及以前版本的情况)

下图是一个完整SSL握手的建立流程:

TLS-full-handshake-flow

首先是TCP的3次握手建立TCP连接,然后客户端发起SSL握手。一次完整的SSL握手包含两次交互,第一次主要是完成算法套件的选择。第二次交互主要是完成身份认证以及密钥交换。这些都协商完成之后,SSL的安全通道就建立完成了。后续的应用数据就会在安全通道上进行加密传输。

密钥交换流程-RSA

基于RSA的密钥交换流程如下:

SSL-keyexchange-RSA

我们来模拟下协商过程:

注:上面的流程是单向认证(服务端没有验证客户端的身份),如果服务端也需要验客户端的身份,会在第一次交互中发送Certificate Request消息,客户端相应的在第二次交互中发送自己的Certificate以及CertificateVerify消息给服务端。

密钥交换流程-DH

基于DH的密钥交换流程如下:

SSL-keyexchange-DH

我们同样来模拟下协商过程:

密钥生成

通过第一次ClientHello和ServerHello的交互、以及密钥交换的过程,客户端和服务端双方,都得到了client随机数、server随机数以及预主密钥。接下来就可以通过这些来计算主密钥了。

master_secret = PRF(pre_master_secret, "master secret",
                    ClientHello.random + ServerHello.random)
                    [0..47];

得到主密钥之后,再将其扩展为一个安全字节序列。

key_block = PRF(SecurityParameters.master_secret,
                     "key expansion",
                     SecurityParameters.server_random +
                     SecurityParameters.client_random);

然后分别切分为MAC密钥、对称加密的key和IV。

client_write_MAC_key[SecurityParameters.mac_key_length]
server_write_MAC_key[SecurityParameters.mac_key_length]
client_write_key[SecurityParameters.enc_key_length]
server_write_key[SecurityParameters.enc_key_length]
client_write_IV[SecurityParameters.fixed_iv_length]
server_write_IV[SecurityParameters.fixed_iv_length]

画成图的话大概是下面这个样子。

TLS1.2_KeyCalculation

Session重用

SSL握手额外引入了两次交互以及CPU密集型的算法运算。每次连接都进行SSL握手时非常耗费性能的,有没有什么办法进行性能优化呢?显然提升硬件性能、软件性能都是有效的方法。其实SSL从协议层面也考虑了这个问题,它提供了“会话重用”的特性。双方在建立前一次SSL连接之后,可以将SSL会话保存下来。下一次想要在建立SSL连接的时候,可以直接恢复之前的SSL会话,从而简化SSL握手流程。Session重用的时候只需要进行一次SSL握手交互,而且不需要再进行身份认证和密钥交换,从而大幅减小了整个流程的延迟和计算开销。

事实上,如果浏览器要对同个站点发起多个连接,它通常就会等第一个SSL握手完成后再发起其他的连接,这样其他的连接就可以复用之前的那个Session。

TLS-half-handshake-flow

Session重用有两种机制,分别为Session IDs和Session tickets。

Session ID

我们来看下使用Session ID时的Session重用流程。从前面密钥交换的流程图中可以看到,服务端在Server Hello消息中会发送本次会话的Session ID给客户端。完成握手之后,服务端会保存本次的Session。

session_resumption_with_session_id

后续客户端可以通过恢复这个Session来建立SSL连接。具体做法就是在ClientHello消息中带上要恢复的Session ID,然后服务端会根据Session ID去查找对应的会话,如果一切都OK的话,就会根据保存的会话恢复出会话密钥等信息。如果服务端不支持会话重用、或者没找到Session ID、或者会话已经过期了,那么就退化到完整的SSL握手。

Session Ticket

Session ID机制它要求服务端保存每个客户端的session cache,这会在服务端造成几个问题:内存的额外开销、要求Session的保存和淘汰策略、然后对有多个服务器的站点,如何高性能地共享session cache提出了挑战。

session_resumption_with_session_ticket

Session Ticket机制的提出就是为了解决Session ID的问题。Session Ticket不需要服务端保存每个客户端的会话。取而代之,如果客户端宣称它支持session ticket,服务端会发送给客户端一个New Session Ticket消息,其中包含了会话相关的加密数据,加密的密钥只有服务端知道。

客户端会保存这个session ticket,当需要会话恢复的时候,它会在ClientHello消息的SessionTicket扩展中带上这个session ticket。服务端在收到之后解密出其中的session数据从而恢复上次的会话。

所以Session Ticket机制对于服务端来说是一种无状态(stateless)的重用,不需要服务端保存session cache,当然也就没有多服务器同步的问题。

证书的吊销(黑名单)

我们在介绍证书的时候已经看到了证书有一个有效期,在有效期外是无法验证通过的。但是如果我们想在有效期内就让证书失效该怎么办?比如公司员工离职了、或者私钥泄漏等,总不可能把发出去的证书收回来吧。

所以就引入了证书的吊销,CA可以吊销某张证书,在验证证书有效性的时候进行额外的黑名单验证。黑名单的验证有如下几种机制:

Certificate Revocation List (CRL)

CRL,即证书吊销列表。CA机构维护一个被吊销证书的列表,里面是被吊销证书的信息。验证证书方需要下载这个列表,进行黑名单的验证。

CRL的缺陷也很明显:证书验证方必须下载这个列表,且下载的列表跟实际CA机构的列表之间可能不同步。如果一个证书实际已经被吊销,但是并没有在本地列表中,就可能形成安全隐患。

Online Certificate Status Protocol (OCSP)

OCSP,即在线证书状态协议。它通过在线请求的方式来进行黑名单验证,不需要下载整个 list,只需要将该证书的序列号发送给 CA 进行验证。部署 OCSP 对 CA也引入了一定的要求,CA 需要搭建的一个高性能的服务器来提供验证服务。万一服务器挂掉了,那所有的黑名单验证就会当成是通过,有一定的安全隐患。

OCSP Stapling

OCSP Stapling是OCSP标准的一个扩展,主要目标就是提升性能和安全性。证书拥有者自己定期向OCSP服务器发送请求。获取OCSP响应,这个响应是基于时间戳的,而且由CA直接签名。

OCSP Stapling提升整体的性能,一方面证书验证方不需要再直接请求CA的服务器去查询状态,另一方面CA的OCSP服务器的压力也减小了。

Server Name Indication(SNI)

当一个站点上部署了多个Server时(相当于一个IP映射了多个域名),不同的Server可能需要使用不同的证书。问题是如何在SSL握手阶段知道是访问那个host(还没到HTTP阶段,无法用请求头里的HOST字段),从而决定使用对应的证书呢?

SNI就是为了解决这个问题,具体做法是在ClientHello扩展中带上SNI,服务端就能从中得知需要访问哪个host,从而选择相应的证书。

GMSSL协议差异

GMSSL修改自TLS1.1,总体上与TLS协议的差异不大。详见《GMT 0024-2014 SSL VPN技术规范》。

协议号

TLSv1.0, TSLv1.1, TLSv1.2、TLSv1.3的协议号分别为0x0301、0x0302、0x0303、0x0304。

而国密的版本号是0x0101。

算法套件

新增了国密的SM1/2/3/4等算法,定义了多个算法套件,其中比较常用的如ECC_SM4_SM3和ECDHE_SM4_SM3。ECDHE_SM4_SM3要求必须双向认证。

其中ECC_SM4_SM3的密钥交换过程类似RSA的密钥交换过程,由客户端用服务端的公钥对预主密钥进行加密后发送给服务端。ECDHE_SM4_SM3的密钥交换过程与普通TLS的ECDHE密钥交换类似,预主密钥由客户端和服务端双方推导得出。ECC_SM4_SM3和ECDHE_SM4_SM3的身份认证是都是通过SM2的签名/验签来完成。

双证书体系

证书消息

国密SSL采用双证书体系:一个签名证书、一个加密证书。其中签名证书用于身份认证、加密证书用于密钥交换。发送Certificate消息时需要同时发送两个证书,格式与标准TLS报文格式一样,第一个证书是签名证书、第二个证书是加密证书。

ECC_SM4_SM3密钥交换

因为采用了双证书体系,在SSL状态机上略微有点不同。ECC_SM4_SM3的密钥交换过程如下:服务端发送Certificate消息之后,还要发送一个ServerKeyExchange消息(这跟RSA密钥交换有所不同),ServerKeyExchange中包含了一个签名值,签名由服务端签名证书对应的私钥(签名私钥)进行计算,签名的内容包括了ClientHello和ServerHello中的随机数以及加密证书。

客户端验证证书和签名之后,使用服务端加密证书加密预主密钥,发送给服务端,服务端则由自己的加密私钥进行解密得到预主密钥。

ECDHE_SM4_SM3密钥交换

ECDHE_SM4_SM3的密钥交换过程如下:服务端发送Certificate消息之后,同样发送一个ServerKeyExchange消息,ServerKeyExchange中包含了一个签名值,签名由服务端签名证书对应的私钥(签名私钥)进行计算。签名的内容跟ECC_SM4_SM3时有所不同,包括了ClientHello和ServerHello中的随机数以及服务端ECDH参数(曲线、公钥)。国密ECDHE的密钥推导计算方式跟TLS也有所不同,TLS只需要对方的临时公钥和自己的临时私钥参与计算,而国密需要对方的临时公钥和固定公钥(即加密证书中的公钥)及自己的临时私钥和固定私钥(即加密私钥)参与计算。所以国密ECDHE必须是双向认证,因为服务端在进行密钥推导的时候也需要用到客户端的加密证书。

客户端验证证书和签名之后,发送自己的证书给服务端,接着根据ServerKeyExchange中ECDH参数信息生成自己的临时密钥,然后同自己的加密密钥、服务端临时公钥和加密公钥一起进行密钥推导得到预主密钥。并将自己的临时密钥参数通过ClientKeyExchange消息发送给服务端,服务端同样使用自己的临时密钥、加密密钥、客户端的临时公钥和加密公钥一起进行密钥推导得到预主密钥。

因为是双向认证,客户端在发送ClientKeyExchange消息之后,还需要发送一个CertificateVerify消息,签名的内容为从ClientHello消息开始到目前为止的所有已经交换的握手消息。

安全性

常见攻击

有些是针对协议设计上的漏洞,有些则是针对实现的bug,经常结合降级攻击手段一起使用。因为篇幅原因,这里仅挑选两个有代表性的重协商攻击和Heartbleed着重介绍一下,对于其余攻击感兴趣的可以参考rfc7457-已知攻击总结

中间人在不需要劫持、解密SSL/TLS连接的情况下,成功地将自己伪造的数据插入到用户真正数据之前。中间人如果了解APP协议(如HTTPS)的话,则会精心构造不完整的数据,让服务器的APP程序认为发生粘包,将数据暂缓不处理,继续等待后续的数据上来。例如攻击者先发送了如下的半拉子请求

GET /bank/sendmoney.asp?acct=attacker&amount=1000000 HTTP/1.1
X-Ignore-This:

后面当客户端发送过来真正的请求

GET /ebanking HTTP/1.1
Cookie: validcookie

APP程序将请求拼接,真正的请求头被屏蔽了,但是却保留了用户的Cookie信息,从而利用用户的Cookie去访问网站内容。服务端会以为前面发送的请求是真正的客户端发送的。

GET /bank/sendmoney.asp?acct=attacker&amount=1000000 HTTP/1.1
X-Ignore-This: GET /ebanking HTTP/1.1
Cookie: validcookie

这个漏洞成因在于,客户端认为的首次协商却被服务器认为是重协商,以及首次协商和重协商之间缺少关联性。解决办法就是禁用重协商或者使用安全重协商。安全重协商会增加一个安全重协商标志,以及确认首次协商和重协商的关联性校验,从而确保中间人的攻击行为可以被识别并拒绝,保证重协商安全。

我们来看看安全重协商是如何保证安全的,对于前面的ClientHello2里面不携带安全重协商表示的情况:

renegotiation-attack-fix1

对于前面的ClientHello2里面携带安全重协商表示的情况:

renegotiation-attack-fix2

无论哪种情况都能保证中间人无机可乘。而这个安全重协商的标识就是提供了一个新的扩展性renegotiation_info。因为SSLv3/TLS 1.0不支持扩展,所以提供了另一种方法,即在算法套件列表中加上TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0xFF),这个不是一个真正的算法套件,只是起标识作用。

安全重协商的流程如下:

 1. 在连接建立第一次SSL握手期间,双方通过`renegotiation_info`扩展或`SCSV`套件通知对方自己支持安全重协商
 2. 然后在握手结束之后,client和server都分别记录`Finish`消息之中的`client_verify_data`和`server_verify_data`
 3. 重协商时client在`ClientHello`中包含`client_verify_data`,server在`ServerHello`中包含`client_verify_data`和`server_verify_data`。对于受害者,如果协商中不会携带这些数据则连接无法建立。而Finished消息由于是加密的,攻击者无法得到client_verify_data和server_verify_data的值。

这是OpenSSL库一个实现上的bug,而不是TLS协议本身的bug。这个bug是由于TLS heartbeat扩展的实现没有进行正确的输入验证(缺少边界检查)导致的。bug的命名也从heartbeat而来。由于没有进行边界检查,导致可以读取的数据超出允许的范围。

📄 [图片文字版] Heartbleed 原理图(正常 vs 恶意心跳):正常用法客户端请求回传 4 字母词 bird,服务器只回显 bird;恶意用法攻击者声称要 500 字母的词、实际只发 bird,服务器按声称长度回显,把相邻内存一并带出——响应里除 bird 外还有 Server master key is 31431498531054、User Carol wants to change password 等敏感内容,心跳长度字段未校验导致越界读取泄漏服务器内存。

这个bug当前造成了非常广泛的影响,有调查显示在这个漏洞公布后几年,仍有许多网站暴露在此攻击之下。这个bug告诫我们就算协议是安全的,实现仍然可能引入安全问题。安全性就像一个木桶,整体的安全性取决于最短的那块木板。

这两个攻击都是基于压缩算法,通过改变请求正文,对比被压缩后的密文长度,可以破解出某些信息。

CRIME通过在受害者的浏览器中运行JavaScript代码并同时监听HTTPS传输数据,能够解密会话Cookie,主要针对TLS压缩。

Javascript代码尝试一位一位的暴力破解Cookie的值。中间人组件能够观察到每次破解请求和响应的密文,寻找不同,一旦发现了一个,他会和执行破解的Javascript通信并继续破解下一位。

BREACH攻击是CRIME攻击的升级版,攻击方法和CRIME相同,不同的是BREACH利用的不是SSL/TLS压缩,而是HTTP压缩。所以要抵御BREACH攻击必须禁用HTTP压缩。

TLS 在 1.1版本之前,下一个记录的IV是直接使用的前一个记录的密文。BEAST攻击就是利用了这一点,攻击者控制受害者发送大量请求,利用可预测性IV猜测关键信息。解决方法就是部署TLS 1.1或者更高级的版本。

基于RC4算法的安全性,RC4目前已经不安全,应该禁用。

是SSL 3.0设计上的漏洞,使用了非确定性的CBC-padding,使中间人攻击者更容易通过padding-oracle攻击获取明文数据。

欺骗服务器使用低版本的不安全的TLS协议,常和其他攻击手段结合使用。删除后向兼容性通常是防止降低攻击的唯一方法。

前向安全性

如果没有前向安全性,一旦私钥泄漏,不仅将来的会话会受影响,过去的会话也都会受影响。一个耐心的黑客,可以先把以前截获的数据先保存下来,一旦私钥泄露或被破解,就可以破解之前的所有密文。这就是所谓的今日截获,明日破解。

TLS的实现之一就是通过使用临时的DH密钥交换来生成会话密钥,一次一密保证了即使黑客花大力气破解了这一次的会话密钥,也只是这次通信被攻击,之前的历史消息不会受到影响。这我们已经在第一部分中讲到了。

但即使是使用了临时DH密钥交换,服务端的session管理机制也会影响到前向安全性。前面Session重用的部分我们讲到了session ticket,它的保护完全是依赖于对称加密,所以长时间有效的session ticket密钥会阻止前向安全性的实现。

在实践中,应该优先使用临时DH密钥交换类算法套件,session的有效期不宜设置过长,session ticket的key也应该经常更换。

TLS 1.3新特性

TLS 1.3相比TLS 1.2改动巨大,它的主要改进目标是最大兼容、强化安全、提升性能。

下面列举了相比TLS 1.3的主要差异:

这里介绍几个关键的特性

密钥交换模式

TLS 1.3提出了3种密钥交换模式:

1-RTT握手

如前所述,TLS 1.2的完整握手有2个RTT,第一个RTT是ClientHello/ServerHello,第二个RTT是ServerKeyExchange/ClientKeyExchange。之所以需要两个RTT是因为TLS 1.2支持多种密钥交换算法及各种不同的参数,这些都依赖第一个RTT去协商出来。TLS 1.3直接大刀阔斧,砍掉了各种自定义的group、curve,砍掉了RSA密钥交换,只剩下为数不多的几个密钥交换算法,实际应用中大部分使用ECDH P-256或X25519。所以干脆让客户端缓存服务器上一次用的是啥密钥交换算法,把KeyExchange直接合入第一个RTT。如果服务器发现客户端发上来的算法不对,那么再告诉它正确的,让客户端重试就好了。(这就引入了HelloRetryRequest消息)。这样基本没有副作用,就可以降到1-RTT了。

TLS 1.3的完整握手流程如下:

       Client                                           Server

Key  ^ ClientHello
Exch | + key_share*
     | + signature_algorithms*
     | + psk_key_exchange_modes*
     v + pre_shared_key*       -------->
                                                  ServerHello  ^ Key
                                                 + key_share*  | Exch
                                            + pre_shared_key*  v
                                        {EncryptedExtensions}  ^  Server
                                        {CertificateRequest*}  v  Params
                                               {Certificate*}  ^
                                         {CertificateVerify*}  | Auth
                                                   {Finished}  v
                               <--------  [Application Data*]
     ^ {Certificate*}
Auth | {CertificateVerify*}
     v {Finished}              -------->
       [Application Data]      <------->  [Application Data]

              +  Indicates noteworthy extensions sent in the
                 previously noted message.

              *  Indicates optional or situation-dependent
                 messages/extensions that are not always sent.

              {} Indicates messages protected using keys
                 derived from a [sender]_handshake_traffic_secret.

              [] Indicates messages protected using keys
                 derived from [sender]_application_traffic_secret_N.

               Figure 1: Message Flow for Full TLS Handshake

握手过程可以分成三个阶段:

重用和PSK

TLS的PSK可以直接在带外建立,也可以通过前一个连接的会话建立。一旦一个握手完成了之后,服务端就会给客户端发送一个PSK id,对应初始握手推导出的密钥。(对应TLS 1.2及之前版本的Session ID和Session Tickets,这两个机制在TLS 1.3中都弃用了)。

PSK可以单独使用,或者跟(EC)DHE密钥交换结合使用以提供前向安全性。

重用和PSK的握手流程如下:

       Client                                               Server

Initial Handshake:
       ClientHello
       + key_share               -------->
                                                       ServerHello
                                                       + key_share
                                             {EncryptedExtensions}
                                             {CertificateRequest*}
                                                    {Certificate*}
                                              {CertificateVerify*}
                                                        {Finished}
                                 <--------     [Application Data*]
       {Certificate*}
       {CertificateVerify*}
       {Finished}                -------->
                                 <--------      [NewSessionTicket]
       [Application Data]        <------->      [Application Data]

Subsequent Handshake:
       ClientHello
       + key_share*
       + pre_shared_key          -------->
                                                       ServerHello
                                                  + pre_shared_key
                                                      + key_share*
                                             {EncryptedExtensions}
                                                        {Finished}
                                 <--------     [Application Data*]
       {Finished}                -------->
       [Application Data]        <------->      [Application Data]

            Figure 3: Message Flow for Resumption and PSK

这种情况下服务端的身份通过PSK认证,所以服务端不发送Certficate和CertificateVerify消息。当客户端通过PSK提议重用的时候,也应该提供key_share扩展,以便服务端在拒绝重用的时候回退到全握手。

有副作用的0-RTT握手

当客户端和服务端共享一个PSK的时候(无论是通过外部获得还是通过前面的握手获得),TLS 1.3允许客户端在第一个flight上就发送数据(early data)。客户端使用PSK来认证服务端及加密early data。

0-RTT的握手流程如下,与PSK重用的1-RTT握手相比,增加early_data扩展以及第一个flight上的0-RTT应用数据。当接收到服务端的Finished消息之后,会发送一个EndOfEarlyData消息指示后面加密密钥的更换。

Client                                               Server

ClientHello
+ early_data
+ key_share*
+ psk_key_exchange_modes
+ pre_shared_key
(Application Data*)     -------->
                                                ServerHello
                                           + pre_shared_key
                                               + key_share*
                                      {EncryptedExtensions}
                                              + early_data*
                                                 {Finished}
                        <--------       [Application Data*]
(EndOfEarlyData)
{Finished}              -------->
[Application Data]      <------->        [Application Data]

      +  Indicates noteworthy extensions sent in the
         previously noted message.

      *  Indicates optional or situation-dependent
         messages/extensions that are not always sent.

      () Indicates messages protected using keys
         derived from a client_early_traffic_secret.

      {} Indicates messages protected using keys
         derived from a [sender]_handshake_traffic_secret.

      [] Indicates messages protected using keys
         derived from [sender]_application_traffic_secret_N.

      Figure 4: Message Flow for a 0-RTT Handshake

0-RTT的数据安全性较弱:

密钥派生过程

密钥派生过程用到了HKDF-Extract和HKDF-Expand函数,以及如下的函数

HKDF-Expand-Label(Secret, Label, Context, Length) =
      HKDF-Expand(Secret, HkdfLabel, Length)

其中HkdfLabel表示

struct {
  uint16 length = Length;
  opaque label<7..255> = "tls13 " + Label;
  opaque context<0..255> = Context;
} HkdfLabel;

Derive-Secret(Secret, Label, Messages) = HKDF-Expand-Label(
  Secret, Label, Transcript-Hash(Messages), Hash.length)

不管是哪种密钥交换模式都给走完下面的整个流程,当没有对应的输入密钥材料(IKM),对应的位置用Hash长度的0值字符串代替。例如没有PSK的话,Early Secret就是HKDF-Extract(0, 0)。

TLS1_3-Key-Schedule

其中exporter_secret是导出密钥,用于用户自定义的其他用途。resumption_master_secret用于生成ticket。client_early_traffic_secret用于推导0-RTT的early-data密钥,*_handshake_traffic_secret用于推到握手消息的加密密钥,*_application_traffic_secret_N用于推导应用消息的加密密钥。

常见实现

OpenSSL:非常流行的开源实现、代码量最大、写得最烂?

LibreSSL:也是OpenSSL的一个fork,OpenBSD项目

BoringSSL:是OpenSSL的一个fork分支,主要用于Google的Chrome/Chromium、Android以及其他应用

JSSE(Java Secure Socket Extension):Java实现

NSS:最初由网景开发的库,现在主要被浏览器和客户端软件使用,比如Firefox使用的就是NSS库(Mozilla开发)。

go.crypto:Go语言的实现

参考资料