HTTPS 的 SSL/TLS 协议

HTTPS 的 SSL/TLS 协议 Recommended

12月 16, 2021
HTTP, Recommended, 计算机网络, 系统设计, 密码学, 浏览器

HTTPS 简介 #

通过上一篇的HTTPS 之密码学基础,现在可以正式进入 HTTPS 的介绍了。HTTPS 指的是超文本传输安全协议(HyperText Transfer Protocol Secure),顾名思义,比 HTTP 多了一个“安全”,而这里的安全指的就是 SSL/TLS 协议了,即 HTTPS 是由 HTTP + SSL/TLS 协议组成的。

HTTP 的问题 #

HTTP 怎么了,我们为什么需要 HTTPS?说白了 HTTP 就是不安全,从而引发了各种问题,这个原因是由 TCP/IP 协议的实现导致的。设想在没有HTTPS 之前,整个互联网的流量都建立在 TCP/IP 的明文传输之上,数据安全问题、个人隐私问题、弹窗广告问题可想而知。

使用 HTTPS 的好处 #

  1. 首先肯定是更加安全。
  2. 更好的 SEO 排名,Google 等搜索引擎厂商为了推广 HTTPS 的使用,为支持 HTTPS 的网站提高了权重。
  3. 提高品牌信任度,Chrome 等浏览器厂商为了推广 HTTPS,会在地址栏显示中区别对待,从而影响用户对品牌的印象。
  4. 可以通过进一步支持 HTTP/2 从而获得更好的性能,在各大浏览器厂商的实现中,使用 HTTP/2 必须基于 HTTPS。

SSL 和 TLS #

SSL(Secure Sockets Layer,安全套接字层)是 TLS(Transport Layer Security,传输层安全协议)的前身,TLS 的制定建立在 SSL 之上。习惯上我们仍然把 HTTPS 的安全协议称为 SSL/TLS 协议,但是现阶段我们主要使用的是 TLS 协议,本次研究对象也以 TLS v1.2 版本的实现为主、TLS v1.3 版本的实现为辅进行学习。

SSL/TLS 与 OpenSSL 的关系 #

SSL/TLS 是协议,规定了厂商使用 HTTPS 时的通信、加密规范,OpenSSL 是实现了协议里的功能的工具箱,提供了各种密码学安全工具的使用。

TLS v1.2 和 TLS v1.3 #

v1.3 相比 v1.2 也发生了很大的变化,性能也会更好,本篇暂不关注具体实现的区别,不过由于 v1.2 的广泛使用,相信两个主流版本会共存相当长的一段时间。

记录协议 #

The TLS record protocol takes messages to be transmitted, fragments the data into manageable blocks, protects the records, and transmits the result. Received data is verified, decrypted, reassembled, and then delivered to higher-level clients.

一般来说 TLS 以记录协议(Record Protocol)实现。记录层协议只关注数据传输和加密,所以有的地方也叫加密层协议,而将其他特性交给子协议,这也使得 TLS 可以扩展,伴随着记录协议可以加密数据,所有子协议的参数也都会得到保护,其核心的子协议有:

  • 握手协议(Handshake Protocol)
  • 密钥规格变更协议(Change Cipher Spec Protocol,v1.3 已删除)
  • 心跳协议(Heartbeat)
  • 应用数据协议(Application Data)
  • 警报协议(Alert Protocol)
enum {
    invalid(0),
    change_cipher_spec(20),
    alert(21),
    handshake(22),
    application_data(23),
    heartbeat(24),  /* RFC 6520 */
    (255)
} ContentType;

struct {
    ContentType type;
    ProtocolVersion legacy_record_version;
    uint16 length;
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

struct {
    ContentType type;
    ProtocolVersion version;
    uint16 length;
    opaque fragment[TLSCompressed.length];
} TLSCompressed;

struct {
    ContentType opaque_type = application_data; /* 23 */
    ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
    uint16 length;
    opaque encrypted_record[TLSCiphertext.length];
} TLSCiphertext;

TLS 记录层主要有四部分的逻辑处理:

  • 数据分块
  • 压缩
  • 加密和完整性保护
  • 添加消息头

数据分块 #

子协议的数据进入 TLS 记录层协议后,首先需要将消息拆分成块(Fragment),TLSPlaintext 是 TLS 记录层协议分块后的数据结构。

压缩 #

由于存在一些安全问题,在TLS/SSL协议中,一般不启用压缩算法。TLS 记录层协议处理 TLSPlaintext,将其转换为 TLSCompressed,如果不压缩,可以认为两者是一致的。

加密和完整性校验 #

TLSPlaintext 通过加密得到 TLSCiphertext,常用的加密方法有分组加密、流密码、AEAD 模式三种。一般是通过在加密前加上 MAC 值,保证消息的完整性。

分组加密 #

分组加密主要指的就是 AES_256_CBC_SHA256 算法。

流密码 #

流密码模式也需要 HMAC 生成消息摘要。

AEAD #

AEAD 加密相比前两种加密方式,使用更加简单,安全性更高。因为它不需要使用者考虑 HMAC 算法,并且也不需要初始化向量与填充 padding。

添加消息头 #

每条 TLS 的记录都以一个标头和数据组成,标头包含记录内容的类型(ContentType)、协议版本(ProtocolVersion)和长度。

tls

经过加密以后,得到了 TLSCiphertext 数据结构,再添加上消息头后,统一传给 TCP 或 UDP 层。

握手协议 #

The handshake protocol is used to negotiate the security parameters of a connection. Handshake messages are supplied to the TLS record layer, where they are encapsulated within one or more TLSPlaintext or TLSCiphertext structures which are processed and transmitted as specified by the current active connection state.

握手协议用于协商连接的安全参数,握手消息被提供给 TLS 记录层,它们被封装在一个或多个 TLSPlaintext 或 TLSCiphertext 结构中被按照当前活动连接状态指定的方式处理和传输。

enum {
    hello_request(0), client_hello(1), server_hello(2),
    certificate(11), server_key_exchange (12),
    certificate_request(13), server_hello_done(14),
    certificate_verify(15), client_key_exchange(16),
    finished(20), (255)
} HandshakeType;

struct {
    HandshakeType msg_type;    /* handshake type */
    uint24 length;             /* bytes in message */
    select (HandshakeType) {
        case hello_request:       HelloRequest;
        case client_hello:        ClientHello;
        case server_hello:        ServerHello;
        case certificate:         Certificate;
        case server_key_exchange: ServerKeyExchange;
        case certificate_request: CertificateRequest;
        case server_hello_done:   ServerHelloDone;
        case certificate_verify:  CertificateVerify;
        case client_key_exchange: ClientKeyExchange;
        case finished:            Finished;
    } body;
} Handshake;

握手协议是 TLS 协议的核心部分,在这个过程中,通信双方通过“握手”的形式,协商连接的密钥并完成身份验证。握手的类型根据场景也分几种:

  1. 完整的握手,对服务器进行身份验证。
  2. 对服务端和客户端都进行身份验证的握手。
  3. 恢复之前会话采用的简短握手。

完整的握手 #

      Client                                               Server

      ClientHello                  -------->
                                                      ServerHello
                                                     Certificate*
                                               ServerKeyExchange*
                                   <--------      ServerHelloDone
      ClientKeyExchange
      [ChangeCipherSpec]
      Finished                     -------->
                                               [ChangeCipherSpec]
                                   <--------             Finished
      Application Data             <------->     Application Data

ClientHello #

struct {
    ProtocolVersion client_version;
    Random random;
    SessionID session_id;
    CipherSuite cipher_suites<2..2^16-2>;
    CompressionMethod compression_methods<1..2^8-1>;
    select (extensions_present) {
        case false:
            struct {};
        case true:
            Extension extensions<0..2^16-1>;
    };
} ClientHello;

Client 发送这条消息给服务端希望建立 TLS 连接,消息体一般包括:

  1. Version:客服端支持的最佳协议版本。
  2. Random:随机数。
  3. Session ID:第一次连接时,会话 ID 是空的,表示客户端并不希望恢复某个已存在的会话。
  4. Cipher Suites:按照优先级顺序排列的客户端支持的密码套件列表。
  5. Extensions:客户端传递的拓展信息列表。

ServerHello #

struct {
    ProtocolVersion server_version;
    Random random;
    SessionID session_id;
    CipherSuite cipher_suite;
    CompressionMethod compression_method;
    select (extensions_present) {
        case false:
            struct {};
        case true:
            Extension extensions<0..2^16-1>;
    };
} ServerHello;

服务器返回给客户端的消息,消息结构与 ClientHello 类似,只是每个字段都只有一个选项,意味着服务器的选择结果。

Certificate #

发送服务器 X.509 标准的证书链,一个目的是可以让客户端验证身份,另一个目的是通过证书的公钥可以让客户端协商出预备主密钥。

ServerKeyExchange #

服务端为密钥协商发送的数据。

ServerHelloDone #

表示服务器已将所有信息发送完毕。

ClientKeyExchange #

表示客户端为密钥协商提供的数据。

ChangeCipherSpec #

表示发送端已经生成加密密钥,并且切换到了加密模式,客户端和服务器在条件成熟时都会发送这个消息。

Finished #

意味着握手已经完成,消息内容是加密的。

enum {
    hello_request(0), client_hello(1), server_hello(2),
    certificate(11), server_key_exchange (12),
    certificate_request(13), server_hello_done(14),
    certificate_verify(15), client_key_exchange(16),
    finished(20), (255)
} HandshakeType;

struct {
    HandshakeType msg_type;    /* handshake type */
    uint24 length;             /* bytes in message */
    select (HandshakeType) {
        case hello_request:       HelloRequest;
        case client_hello:        ClientHello;
        case server_hello:        ServerHello;
        case certificate:         Certificate;
        case server_key_exchange: ServerKeyExchange;
        case certificate_request: CertificateRequest;
        case server_hello_done:   ServerHelloDone;
        case certificate_verify:  CertificateVerify;
        case client_key_exchange: ClientKeyExchange;
        case finished:            Finished;
    } body;
} Handshake;

互相验证的握手 #

      Client                                               Server

      ClientHello                  -------->
                                                      ServerHello
                                                     Certificate*
                                               ServerKeyExchange*
                                              CertificateRequest*
                                   <--------      ServerHelloDone
      Certificate*
      ClientKeyExchange
      CertificateVerify*
      [ChangeCipherSpec]
      Finished                     -------->
                                               [ChangeCipherSpec]
                                   <--------             Finished
      Application Data             <------->     Application Data

和上个流程相比,多了两个类型的消息。

CertificateRequest #

服务器请求对客户端进行身份验证,并发送其接受的证书公钥和签名算法。

CertificateVerify #

消息中包含之前所有握手消息的签名,可以证明客户端拥有的私钥和之前发送的客户端证书中的公钥相对应。

会话恢复的握手 #

为了避免每次都需要完整的握手,TLS 也支持了会话恢复机制,以减少完整握手的网络资源和时间的消耗。

      Client                                                Server

      ClientHello                   -------->
                                                       ServerHello
                                                [ChangeCipherSpec]
                                    <--------             Finished
      [ChangeCipherSpec]
      Finished                      -------->
      Application Data              <------->     Application Data

Session ID #

在一次完整协商的连接断开时,客户端和服务器都会将会话的 Session ID 保存一段时间。但是这样 Session ID 有两个很明显的缺点:

  1. Session ID 缓存在服务器会占用内存空间。
  2. 如果是分布式服务器架构,Session ID 的机制就失效了。

于是又出现了会话票证(Session Ticket)机制来替代 Session ID 机制。

Session Ticket #

SessionTicket 的原理很简单,服务器将会话信息加密后以票证(ticket)的方式发送给客户端,服务器本身不存储会话信息。客户端接收到票据后将其存储到内存中,如果想恢复会话,则下一次连接的是将票据发送给服务器端,服务器端解密后,如果确认无误则表示可以进行会话恢复,完成了一次简短的握手。

密钥协商 #

有了上面的握手流程,可以知道握手最重要的目的就是进行密钥协商,常见的密钥协商算法有:RSA、DHE_RSA、ECDHE_RSA 和 ECDHE_ECDSA。

RSA #

上篇已经说过 RSA 公开密钥严格上不算是密钥协商算法,因为这种只在客户端生成预备主密钥的机制太简单还是存在安全问题,攻击者不需要实时进行攻击,只要记录流量,等待有朝一日拿到密钥就可以解密之前所有的数据,这四种算法中,除了 RSA 算法,其他都不受这个问题影响,即向前保密性。

向前保密性 #

用于加密预备主密钥的服务器公钥(证书公钥),一般多年不变,任何能够接触到对应私钥的人都可以恢复预备主密钥,并构建相同的主密钥,从而危害到会话安全性,即不具备向前保密性。

预备主密钥、主密钥和会话密钥 #

密钥协商场景里,协商的结果是预备主密钥(pre master secret),用这个值进而可以得到主密钥(master secret)的值,然后再通过主密钥得到会话密钥(密钥块,key_block)从而进行 TLS 通信。

struct {
    ConnectionEnd          entity;
    PRFAlgorithm           prf_algorithm;
    BulkCipherAlgorithm    bulk_cipher_algorithm;
    CipherType             cipher_type;
    uint8                  enc_key_length;
    uint8                  block_length;
    uint8                  fixed_iv_length;
    uint8                  record_iv_length;
    MACAlgorithm           mac_algorithm;
    uint8                  mac_length;
    uint8                  mac_key_length;
    CompressionMethod      compression_algorithm;
    opaque                 master_secret[48];
    opaque                 client_random[32];
    opaque                 server_random[32];
} SecurityParameters;

主密钥是由预备主密钥、ClientHello.random 和 ServerHello.random 通过 PRF(伪随机函数)函数生成的,主密钥长度是 48 字节。一旦主密钥确定,预备主密钥就会从客户端和服务端删除,避免攻击者获取预备主密钥,从而以相同的算法生成主密钥和会话密钥。

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

会话密钥是由主密钥、SecurityParameters.server_random 和 SecurityParameters.client_random 数通过 PRF 函数来生成,会话密钥里面包含对称加密密钥、消息认证和 CBC 模式的初始化向量,对于非 CBC 模式的加密算法来说,就没有用到这个初始化向量。

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

Session ID 缓存和 Session Ticket 里面保存的也是主密钥,而不是会话密钥,这样每次会话复用的时候再用双方的随机数和主密钥导出会话密钥,从而实现每次加密通信的会话密钥不一样,即使一个会话的主密钥泄露了或者被破解了也不会影响到另一个会话。

应用数据协议 #

应用数据协议就是 TLS/SSL 记录层协议的上层协议,包括 HTTP、FTP、SMTP 等应用层协议。TLS/SSL 协议能够无缝地处理应用层数据,TLS 记录层协议密码学保护的主要信息就是应用层协议数据,其他子协议不进行加密保护。

警报协议 #

警报协议以一种简单的机制告知对端通信出现异常情况,通常会伴随着关闭连接(close_notify)的描述。

enum { warning(1), fatal(2), (255) } AlertLevel;

enum {
    close_notify(0),
    unexpected_message(10),
    bad_record_mac(20),
    decryption_failed_RESERVED(21),
    record_overflow(22),
    decompression_failure(30),
    handshake_failure(40),
    no_certificate_RESERVED(41),
    bad_certificate(42),
    unsupported_certificate(43),
    certificate_revoked(44),
    certificate_expired(45),
    certificate_unknown(46),
    illegal_parameter(47),
    unknown_ca(48),
    access_denied(49),
    decode_error(50),
    decrypt_error(51),
    export_restriction_RESERVED(60),
    protocol_version(70),
    insufficient_security(71),
    internal_error(80),
    user_canceled(90),
    no_renegotiation(100),
    unsupported_extension(110),           /* new */
    (255)
} AlertDescription;

struct {
    AlertLevel level;
    AlertDescription description;
} Alert;

心跳协议 #

心跳(Heartbeat)协议主要用于连接保活(尤其是基于 UDP 的 DLTS)和发现 LTS 和 ULTS 的路径最大传输单元(PMTU),著名的心跳出血(Heartbleed)攻击也是利用了 OpenSSL 在此协议上的一个实现上的漏洞,导致敏感数据从内存溢出。

Todo #

现在其实还不算完,显然 HTTPS 的东西确实太多了,本篇并没有深入到每一步的细节上深究,只是达到了我目前暂时的目的:了解 HTTPS 相关概念,未来需要做的事情还有很多:

  • 有了现在的概念,需要看看实际的抓包是怎样的?
  • 交互细节,细化到每一步消息字段的变化。
  • OpenSSL 相关实战。
  • 部署一个 HTTPS 网站。

参考 #

图解密码技术
加密相关的理论基础,属于必读了。

深入浅出 HTTPS
这本书循序渐进讲解了 HTTPS,从理论到实践再到理论再到实践,可能因为 HTTPS 本身就比较复杂,所以书中的结构内容也比较混乱,但是我需要一些细节的东西最后都找到了,可以多读几遍。

HTTPS 权威指南
这本书大量介绍了历史上针对 HTTPS 的攻击手段,非常有意思,推荐阅读。

RFC 文档,这里有最权威的握手流程图和数据结构体。

本文共 4281 字,上次修改于 Jul 9, 2024,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» HTTPS 的密码学基础

» 跨域相关问题

» 浏览器中的 HTTP 缓存使用策略

» 了解下 Protobuf 相关概念

» 说说实际工作中 GraphQL 的使用体验