DTLS(Datagram Transport Layer Security)是基于 UDP 场景下数据包可能丢失或重新排序的现实情况下,为 UDP 定制和改进的 TLS 协议。在 WebRTC 中使用 DTLS 的地方包括两部分:
- 协商和管理 SRTP 密钥;
- 为 DataChannel 提供加密通道;
通俗的讲就是:通过DTLS握手使双方都得到对端的证书、密钥、以及SRTP加密材料等信息。
从上图可以看出:在 WebRTC 中,媒体包通过 SRTP/SRTCP 进行传输,而数据包通过 SCTP 进行传输,他们都是基于 UDP 协议的。其中,SRTP 与 SCTP 的加密握手,由 DTLS 协议来完成。
DTLS 协议概念
DTLS 目前只有 1.0 和 1.2 版本,分别对应 TLS 1.1 和 TLS 1.2 版本,WebRTC 中主要使用的是 1.2 版本。TLS协议的目的是双方交换用于加密解密的证书和密钥,而 DTLS 比起 TLS,更好的支持了 UDP 协议在不确定网络中的特性。
其中,DTLS 和 TLS 的差别主要为以下几点:
传输方式:
- TLS 基于面向连接的传输,主要用于 TCP 协议;
- DTLS 基于面向数据包的传输,主要用于 UDP 协议,更适用于在不可靠的网络环境中,如实时音视频通信或物联网设备之间的通信;
不可靠网络环境:
- DTLS 被设计用于处理不可靠的网络环境,比如丢包、乱序等情况的处理,它引入了序列号(Sequence Number)和 消息验证码(MAC)等机制来确保数据的可靠性和完整性,适用于 UDP 等不可靠传输协议;
- DTLS 引入了握手重新交换(Handshake Retransmission)机制,以允许在 DTLS 握手包发生丢包后重新发送握手消息;
- DTLS 需要使用抖动缓冲区(Reordering Buffer)来重新组装乱序的数据报,确保握手和应用层数据的有序性;
消息披露保护:
- DTLS 在握手过程中,为了防止消息披露,会在握手的关键步骤使用特定的消息验证码(MAC)来保护敏感信息,以确保握手过程的安全性;
Record Layer(记录层)
记录层是在DTLS协议中负责传输和保护数据的核心组件,它负责将应用层数据分割成记录,并添加所需的保护和控制信息,然后将这些记录传输到对等通信方,抓包如下图所示:
记录层执行以下主要功能:
- 分割和重组数据:应用程序产生的数据被分割成小块,每块称为一个记录。这是因为UDP协议不像TCP协议那样提供数据流的完整性,记录层负责将这些记录重新组合成完整的应用层数据;
- 加密和解密:记录层使用协商好的加密算法对数据进行加密,以确保数据在传输过程中的机密性。接收方通过相同的算法进行解密,还原原始的应用层数据;
- 认证和完整性校验:记录层添加消息认证码(MAC)来验证数据的完整性,以防止数据被篡改。接收方会验证MAC以确保数据的完整性;
- 压缩和解压缩:记录层可以支持数据的压缩和解压缩,以减少数据传输的带宽消耗;
消息类型
// 更改密码规范
#define MBEDTLS_SSL_MSG_CHANGE_CIPHER_SPEC 20
// 告警
#define MBEDTLS_SSL_MSG_ALERT 21
// 握手
#define MBEDTLS_SSL_MSG_HANDSHAKE 22
// 自定义数据
#define MBEDTLS_SSL_MSG_APPLICATION_DATA 23
// 获取消息类型
static const char *msg_type(unsigned char *msg, size_t len)
{
switch (msg[0]) {
case MBEDTLS_SSL_MSG_CHANGE_CIPHER_SPEC: return "ChangeCipherSpec";
case MBEDTLS_SSL_MSG_ALERT: return "Alert";
case MBEDTLS_SSL_MSG_APPLICATION_DATA: return "ApplicationData";
case MBEDTLS_SSL_MSG_CID: return "CID";
case MBEDTLS_SSL_MSG_HANDSHAKE: break; /* See below */
default: return "Unknown";
}
if (len < 13 + 12) {
return "Invalid handshake";
}
if (msg[14] || msg[19] || msg[22]) {
return "Encrypted handshake";
}
switch (msg[13]) {
case MBEDTLS_SSL_HS_HELLO_REQUEST: return "HelloRequest";
case MBEDTLS_SSL_HS_CLIENT_HELLO: return "ClientHello";
case MBEDTLS_SSL_HS_SERVER_HELLO: return "ServerHello";
case MBEDTLS_SSL_HS_HELLO_VERIFY_REQUEST: return "HelloVerifyRequest";
case MBEDTLS_SSL_HS_NEW_SESSION_TICKET: return "NewSessionTicket";
case MBEDTLS_SSL_HS_CERTIFICATE: return "Certificate";
case MBEDTLS_SSL_HS_SERVER_KEY_EXCHANGE: return "ServerKeyExchange";
case MBEDTLS_SSL_HS_CERTIFICATE_REQUEST: return "CertificateRequest";
case MBEDTLS_SSL_HS_SERVER_HELLO_DONE: return "ServerHelloDone";
case MBEDTLS_SSL_HS_CERTIFICATE_VERIFY: return "CertificateVerify";
case MBEDTLS_SSL_HS_CLIENT_KEY_EXCHANGE: return "ClientKeyExchange";
case MBEDTLS_SSL_HS_FINISHED: return "Finished";
default: return "Unknown handshake";
}
}
消息头结构
// TLS头
typedef struct {
ContentType type;
ProtocolVersion version;
uint16 length;
opaque fragment[TLSPlaintext.length];
} TLSPlaintext;
// DTLS头
typedef struct {
ContentType type;
ProtocolVersion version;
uint16 epoch; // DTLS only, 在每次密码状态更改时递增的计数器值
uint48 sequence_number; // DTLS only, 记录序列号,用于检查后丢包和乱序
uint16 length;
opaque fragment[DTLSPlaintext.length];
} DTLSPlaintext;
R::Handshake(握手)
握手协议,使用非对称加密算法,完成 Record 协议使用的对称密钥的协商。下面,是所有握手消息的子类型:
#define MBEDTLS_SSL_HS_HELLO_REQUEST 0
#define MBEDTLS_SSL_HS_CLIENT_HELLO 1
#define MBEDTLS_SSL_HS_SERVER_HELLO 2
#define MBEDTLS_SSL_HS_HELLO_VERIFY_REQUEST 3
#define MBEDTLS_SSL_HS_NEW_SESSION_TICKET 4
#define MBEDTLS_SSL_HS_CERTIFICATE 11
#define MBEDTLS_SSL_HS_SERVER_KEY_EXCHANGE 12
#define MBEDTLS_SSL_HS_CERTIFICATE_REQUEST 13
#define MBEDTLS_SSL_HS_SERVER_HELLO_DONE 14
#define MBEDTLS_SSL_HS_CERTIFICATE_VERIFY 15
#define MBEDTLS_SSL_HS_CLIENT_KEY_EXCHANGE 16
#define MBEDTLS_SSL_HS_FINISHED 20
R::Change Cipher Spce(新加密)
Change Cipher Spec(更改密码规范)消息标志着从握手阶段到加密数据传输阶段的过渡。
在握手阶段,双方协商了“加密算法”和“密钥”等参数,但实际的数据传输仍然是明文的。当握手完成后,一方发送 Change Cipher Spec 消息,告知对方从此之后发送的数据将会使用新的加密参数进行加密和解密。
Change Cipher Spec消息本身是非常短小的,只包含一个字节的值,通常为 0x01。这个消息在协议中的作用是非常关键的,它通知了对方双方已经准备好使用新的加密规范,可以开始加密传输。
R::Alert(告警)
当发生错误时,或者是关闭DTLS连接时,都需要发送Alert消息。其实,Alert消息能包括的场景有很多:
#define MBEDTLS_SSL_ALERT_MSG_CLOSE_NOTIFY 0 /* 0x00 */
#define MBEDTLS_SSL_ALERT_MSG_UNEXPECTED_MESSAGE 10 /* 0x0A */
#define MBEDTLS_SSL_ALERT_MSG_BAD_RECORD_MAC 20 /* 0x14 */
#define MBEDTLS_SSL_ALERT_MSG_DECRYPTION_FAILED 21 /* 0x15 */
#define MBEDTLS_SSL_ALERT_MSG_RECORD_OVERFLOW 22 /* 0x16 */
#define MBEDTLS_SSL_ALERT_MSG_DECOMPRESSION_FAILURE 30 /* 0x1E */
#define MBEDTLS_SSL_ALERT_MSG_HANDSHAKE_FAILURE 40 /* 0x28 */
#define MBEDTLS_SSL_ALERT_MSG_NO_CERT 41 /* 0x29 */
#define MBEDTLS_SSL_ALERT_MSG_BAD_CERT 42 /* 0x2A */
#define MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_CERT 43 /* 0x2B */
#define MBEDTLS_SSL_ALERT_MSG_CERT_REVOKED 44 /* 0x2C */
#define MBEDTLS_SSL_ALERT_MSG_CERT_EXPIRED 45 /* 0x2D */
#define MBEDTLS_SSL_ALERT_MSG_CERT_UNKNOWN 46 /* 0x2E */
#define MBEDTLS_SSL_ALERT_MSG_ILLEGAL_PARAMETER 47 /* 0x2F */
#define MBEDTLS_SSL_ALERT_MSG_UNKNOWN_CA 48 /* 0x30 */
#define MBEDTLS_SSL_ALERT_MSG_ACCESS_DENIED 49 /* 0x31 */
#define MBEDTLS_SSL_ALERT_MSG_DECODE_ERROR 50 /* 0x32 */
#define MBEDTLS_SSL_ALERT_MSG_DECRYPT_ERROR 51 /* 0x33 */
#define MBEDTLS_SSL_ALERT_MSG_EXPORT_RESTRICTION 60 /* 0x3C */
#define MBEDTLS_SSL_ALERT_MSG_PROTOCOL_VERSION 70 /* 0x46 */
#define MBEDTLS_SSL_ALERT_MSG_INSUFFICIENT_SECURITY 71 /* 0x47 */
#define MBEDTLS_SSL_ALERT_MSG_INTERNAL_ERROR 80 /* 0x50 */
#define MBEDTLS_SSL_ALERT_MSG_INAPROPRIATE_FALLBACK 86 /* 0x56 */
#define MBEDTLS_SSL_ALERT_MSG_USER_CANCELED 90 /* 0x5A */
#define MBEDTLS_SSL_ALERT_MSG_NO_RENEGOTIATION 100 /* 0x64 */
#define MBEDTLS_SSL_ALERT_MSG_UNSUPPORTED_EXT 110 /* 0x6E */
#define MBEDTLS_SSL_ALERT_MSG_UNRECOGNIZED_NAME 112 /* 0x70 */
#define MBEDTLS_SSL_ALERT_MSG_UNKNOWN_PSK_IDENTITY 115 /* 0x73 */
#define MBEDTLS_SSL_ALERT_MSG_NO_APPLICATION_PROTOCOL 120 /* 0x78 */
CLOSE_NOTIFY(关闭通知)表示通信的一方希望关闭连接,这可以是正常的连接关闭,也可以是异常关闭,具体情况取决于Alert的上下文。
UNEXPECTION_FAILED(意外的消息)表示收到了不应该在当前状态下出现的消息,这可以指示协议错误或不一致,可能需要中断连接。
BAD_RECORD_MAC(错误的记录MAC)表示接收到的消息的身份验证失败,可能是由于消息被篡改或错误。
DECRYPTION_FAILED(解密失败)表示消息解密失败,可能是由于使用错误的密钥或其他解密问题。
RECORD_OVERFLOW(记录溢出)表示接收到的记录长度超过了允许的最大值,可能是攻击的迹象。
DECOMPRESSION_FAILURE(解压缩失败) 表示解压缩接收到的消息失败,可能是由于使用了错误的压缩算法或其他问题。
HANDSHAKE_FAILURE(握手失败)表示在握手过程中出现了错误,可能是由于加密套件不匹配、证书问题等。
ILLEGAL_PARAMETER(非法参数)表示接收到的消息中包含了不合法的参数,可能是攻击的迹象。
CERT_EXPIRED(证书过期)表示接收到的证书已经过期。
CERT_REVOKED(证书被吊销)表示接收到的证书已经被吊销。
CERT_UNKNOWN(未知证书)表示接收到的证书无法在验证的证书列表中找到。
DTLS 超时重传
DTLS 是基于 UDP 的,不可避免会出现丢包,需要重传。如果处理不当,会导致整个通信双方无法建立会话,通话失败。RFC6347@4.2.4 给出了超时和重传机制。
在处理重传时,以下几点需要注意:
为了解决丢包和重传问题,新增字段 Message Sequence:
在DTLS中,每个发送的消息都会被分配一个序列号(Sequence Number)。序列号是一个递增的整数,用于唯一标识每个消息。这个序列号不仅用于区分不同的消息,还用于处理消息的超时重传。
当发送方发送消息时,会记录消息的序列号,并且设置一个超时计时器。如果在超时时间内没有收到目标端的响应,就会认为消息可能丢失,需要进行重传。重传的原则是,重传相同序列号的消息,以确保目标端最终可以收到这条消息。
当服务端收到客户端的 Finished 消息后,发送 Finished 消息给客户端,并更新状态为“Handshake Complete”,开始发送 SRTP 数据。
假设此时发送给客户端的 Finished 消息出现丢包,这会导致客户端收到 SRTP 数据后立刻丢弃。同时,客户端再次发送 Finished 消息到服务端,服务端必须正确响应。否则,会导致 DTLS 协商完成的假象,通话失败。
TLS/DTLS 握手流程
红框内可省略。
从加密角度来看大体上分为三个过程:
- 明文通信过程
- 非对称加密通信过程
- 对称加密通信过程
明文通信过程
在通信两端首次向对方发送 Hello 消息时,由于双方都没有协商好要使用哪种加密方式,因此这个过程中的消息都是使用明文进行发送的。
- ==> Client Hello
- <== Server Hello、Certificate、( Server Key Exchange )、( Certificate Request )、Server Hello Done
非对称加密通信过程
由于非对称加密通信的性能较差,在实际的通信过程中其实使用的是对称加密通信,为了保证对称加密通信过程的安全性,也就是需要避免对称加密密钥被窃取,这个密钥在协商过程中使用非对称加密来进行加密。
- ==> ( Certificate )、Client Key Exchange、( Certificate Verify )、Change Cipher Spec、Finished
- <== ( Change Cipher Spec )、Finished
对称加密通信过程
通过上述握手过程协商出对称加密算法及使用的对称加密密钥之后,随后的通信过程,也就是实际的应用通信过程,都使用的是对称加密。
握手流程分解
下图是一个TLS握手,DTLS因为是基于UDP所以没有ACK。
==> Client Hello
/*
* ==> ClientHello
*/
客户端先发起 ClientHello,消息中告诉对方自己支持的 SSL/TLS 版本、加密套件、数据压缩算法等。消息中同时还会携带一个 Session ID,因为握手流程的开销比较大,使用 Session ID 可以在下一次与 TLS 握手的过程跳过后续繁琐的握手流程,重用之前的握手结果(如版本号、加密算法套件、master-key 等)并产生一个随机数 A,也告诉给对方。
1. TLS版本(Version)
如果服务器支持这个版本,它将在 ServerHello 中回复相同的版本号。如果服务器不支持这个版本,它可以选择回复一个支持的版本号,从而和客户端协商一个共同支持的 DTLS 版本。
2. 加密套件(Cipher Suites)
客户端先将自己所有支持的加密套件通过ClientHello发送给服务端。
3. 随机数(Random)
客户端在ClientHello消息中的"Random"字段会被填充为当前的格林尼治标准时间和随机生成的字节。服务器在ServerHello消息中也会包含一个类似的"Random"字段。
这两个随机数(客户端和服务器的)以及其他握手消息中的信息将被用于生成握手密钥、会话密钥等。由于这些随机数是每次握手都会变化的,因此攻击者很难预测生成的密钥。
4. 数据压缩算法(Compression Methods)
在TLS 1.2及之前的版本中,最常见的压缩算法是"null",表示不进行数据压缩。由于数据在TLS连接中已经进行了加密,再进行数据压缩可能会降低安全性,因此在实际应用中通常会选择不进行数据压缩。
5. 会话标识(Session ID)
如果客户端希望复用之前的会话,它可以在ClientHello中包含会话ID。如果服务器能够识别会话ID并确定其仍然有效,则可以跳过完整的握手过程,提供更快的连接建立。
<== Server Hello
/*
* <== ServerHello
* Certificate
* ( ServerKeyExchange )
* ( CertificateRequest )
* ServerHelloDone
*/
Server收到ClientHello之后,再向Client发起:ServerHello。消息中携带协商出来的 TLS/SSL 版本号、加密套件和数据压缩算法,如果服务端同意客户端重用上次的会话,就返回一个相同的 Session ID,否则就填入一个全新的 Session ID。
1. 加密套件(Cipher Suites)
然后客户端和服务器都会根据自己的加密套件列表和对方的选择,在共同支持的加密套件中进行协商。这个过程通常是选择两个列表的交集中的一个加密套件作为最终的选择。
需要注意的是,加密套件的选择不仅取决于客户端和服务器各自支持的加密套件,还取决于加密套件的安全性和性能。通常情况下,会优先选择安全性较高、性能较好的加密套件。
2. 压缩算法(Compression Method)
在 DTLS 的 ServerHello 消息中,压缩算法字段用于指定服务器选择的数据压缩算法。如果在 ServerHello 中看到压缩算法被设置为 null(零),意味着服务器在该握手过程中不会应用任何数据压缩算法。
- 会话标识(Session ID)
因为ClientHello中的SessionID为0,所以这里的SessionID是新生成的。
Certificate
该消息携带服务端数字证书(CA)以验证服务端身份,里面携带了服务端非对称加密所使用的公钥。这步虽然是可选的,但是一般来说客户端都会要求验证服务端的身份,在大多数情况下这步都会执行。
这里的证书也是经过加密的。
证书是用来干啥的?
- 身份验证(Authentication)
客户端会检查服务器证书链,以验证服务器的身份。
服务器的证书链中通常包含服务器证书以及一系列的中间证书和根证书。客户端会验证这些证书是否合法、有效,并且是否由受信任的证书颁发机构(CA)签发。这样可以确保客户端连接到了预期的、合法的服务器。
- 密钥交换(Key Exchange)
服务器证书链中的服务器证书包含服务器的公钥。客户端可以使用这个公钥来加密预主密钥(Pre-Master Secret),从而保证了密钥交换的安全性。服务器则可以使用自己的私钥解密预主密钥,并用预主密钥派生会话密钥,从而确保通信的机密性。
( Server Key Exchange )
如果是RSA算法,则不需要再包含该信息。因为RSA算法的公钥和证书复用了。如果是DH算法,那么就需要再生成一个服务端公钥,再将该公钥发送给客户端。
( Certificate Request )
在有些安全性要求高的场景,例如银行支付等,不仅需要验证服务端的身份,还需要验证客户端的身份,这时候服务端就会要求客户端提供客户端的身份证书。
这就是本消息的目的:请求客户端提供其证书,这样一来,服务端和客户端都将提供自己的证书给对端验证。
- 服务端支持的证书类型(Certificate type)
这是一个列表,包含服务器支持的证书类型,比如RSA、ECDSA等。客户端可以从中选择一个适合自己的证书类型来回应服务器的请求。
- 服务端支持的签名算法(Signature Algorithm)
包含服务器支持的签名算法。客户端可以从中选择一个适合自己的算法来签署自己的证书。
Server Hello Done
表明 Server Hello 结束。
==> ( Certificate )
根据服务端发来的 Certificate Request,通过 Certificate 返回过去,这个和之前服务端发给客户端 ServerHello 的那次是一样的。
/*
* ==> ( Certificate/Alert )
* ClientKeyExchange
* ( CertificateVerify )
* ChangeCipherSpec
* Finished
*/
Client Key Exchange
客户端在验证服务端的身份证书后,会取出其中的服务端公钥。然后本地产生一个随机数 C,作为 pre-master key,在本地使用之前的随机数 A、B 和这次生成的 C 共同生成对称加密密钥 master-key;使用服务端公钥对 pre-master key 加密后发送给服务端。
ClientKeyExchange的主要目的,是协商生成会话密钥,以便在后续的通信中进行加密和解密。
( Certificate Verify )
如果服务端要求客户端提供客户端证书,那么客户端在发送 Client Key Exchange 之后必须马上发送 Certificate Verify,其中的内容是客户端使用自己的私钥加密的一段数据,提供给服务端用客户端的公钥来进行解密验证。之所以需要这一步是为了确保客户端发送的证书确实是它自己的证书。
Change Cipher Spec
提示服务端,随后客户端将会使用 master key 来进行对称加密通信;
Finished
表明客户端侧 SSL/TLS 握手结束;
<== ( Change Cipher Spec )
提示客户端,随后服务端将会使用 master key 来进行对称加密通信;
/*
* <== ChangeCipherSpec
* Finished
*/
Finished
表明服务端侧 SSL/TLS 握手结束;