跳到主要内容

DTLS-SRTP工作原理

WebRTC 的媒体加密由两层协议协作完成:DTLS 负责密钥协商与身份认证,SRTP 负责实际的 RTP 包加密。两者通过 RFC 5764 定义的 use_srtp TLS 扩展绑定在一起,构成浏览器默认强制、无法关闭的端到点加密链路。

本篇只讨论"点到点"(peer-to-SFU 或 peer-to-peer)链路加密。SFU 持有 SRTP 密钥,要做真正的端到端加密见 端到端加密E2EE.md

1. 证书生成

1.1 为什么需要证书

DTLS 握手要求双方都出示 X.509 证书完成身份认证。但 WebRTC 场景里,对端身份由信令层 + SDP 指纹保证(见第 2 节),证书本身只是承载公钥的载体,所以浏览器一律使用自签名证书,CA 链没有意义。

1.2 算法选型:ECDSA P-256 vs RSA

维度ECDSA P-256RSA 2048
握手 CPU低(约 RSA 的 1/5)
证书体积小(约 250 字节)大(约 1200 字节)
握手 RTT 内的包大小不易触发 IP 分片容易触发分片
浏览器默认Chrome / Firefox / Safari 默认 ECDSA兼容性回落选项

结论:除非要兼容只支持 RSA 的旧设备,统一用 ECDSA P-256。Chrome 自 M52 起默认 ECDSA。

1.3 生命周期

  • 浏览器调 RTCPeerConnection 时若未传 certificates,会临时生成一张,仅当前 PC 生命周期内有效。
  • 显式生成的证书可通过 RTCPeerConnection.generateCertificate() 拿到,可缓存到 IndexedDB 复用(避免每次新建 PC 都重算 ECDSA 密钥对)。
  • 浏览器对自签证书有效期上限是 365 天(W3C 规范要求),超期生成会被拒绝。
  • 证书过期不影响正在进行的连接,只影响新建 PC 的握手。

1.4 代码:生成、缓存与复用

/**
* 生成或复用 DTLS 证书。
* 复用动机:ECDSA 密钥对生成约 5~20ms,移动端冷启动累积明显。
*/
async function getOrCreateCertificate() {
const cached = await idbGet('rtc-cert');
if (cached && cached.expires > Date.now() + 60_000) {
return cached.cert;
}

const cert = await RTCPeerConnection.generateCertificate({
name: 'ECDSA',
namedCurve: 'P-256',
// expires 单位毫秒,最大 365 天
expires: 7 * 24 * 60 * 60 * 1000,
});

await idbSet('rtc-cert', { cert, expires: cert.expires });
return cert;
}

const cert = await getOrCreateCertificate();
const pc = new RTCPeerConnection({
iceServers: [...],
certificates: [cert],
});

// 拿指纹用于自检(浏览器实际不暴露 fingerprint 字段,需要从 localDescription 里 parse)
console.log(cert.getFingerprints());
// [{ algorithm: 'sha-256', value: '8A:7F:...:E1' }]

什么时候不适用:iOS Safari 在跨 origin storage partition 下,IndexedDB 可能被清空,复用收益不稳定,可退化为每次现生成。

1.5 失败时的样子

  • 用 RSA 1024:现代浏览器握手直接拒绝(InvalidAccessError)。
  • 证书过期:createOffer() 不报错,但握手时对端发 bad_certificate alert,连接 iceConnectionState 卡在 checkingfailed
  • 时间不同步(设备时钟错乱超 365 天):自签证书 notBefore/notAfter 校验失败,表现同上。

2. 指纹交换

2.1 SDP 中的 a=fingerprint

DTLS 证书的指纹(SHA-256 摘要)通过 SDP 在信令层传递:

m=audio 9 UDP/TLS/RTP/SAVPF 111 103
c=IN IP4 0.0.0.0
a=ice-ufrag:F7gI
a=ice-pwd:x9cml/YzichV2+XlhiMu8g
a=fingerprint:sha-256 8A:7F:1C:D2:91:3B:5E:04:6A:E2:7C:43:9F:88:50:21:5C:6B:7A:E8:34:DD:9C:11:F0:42:B6:73:C5:99:E1:90
a=setup:actpass
a=mid:0
  • a=fingerprint证书公钥的哈希,不是证书本身。
  • 算法必须与证书签名算法匹配强度(SHA-256 是当前默认;SHA-1 仍被部分老网关使用,但 RFC 8122 已不推荐)。
  • a=setup:actpass / active / passive 决定 DTLS 角色,见第 3 节。

2.2 与 ICE 的绑定(防 MITM)

只有指纹不够,攻击者完全可以在信令链路里替换指纹 + 公钥完成 MITM。WebRTC 的防护是:

  1. 信令必须走 wss + 鉴权(业务责任,不在 WebRTC 规范内)。
  2. ICE 候选交换时 STUN 消息带 MESSAGE-INTEGRITY,用 ice-ufrag / ice-pwd 鉴权。ice-pwd 与 fingerprint 都在同一段 SDP 里,篡改任一项都会失败
  3. DTLS 握手完成后,比对对端证书指纹与 SDP 中 a=fingerprint,不一致直接 close。

什么时候不适用:纯 P2P 没有信令服务器(罕见),需要带外信道传指纹,否则无法防 MITM。

2.3 失败时的样子

  • 信令篡改指纹:DTLS 握手成功(攻击者出示自己的证书),但浏览器对比 SDP 指纹失败,connectionState 进入 failed,控制台报 DTLS fingerprint mismatch
  • SHA-1 指纹被对端拒绝:常见于和老旧 SBC/网关互通,需要双方协商升级。

3. DTLS 1.2 握手

3.1 为什么用 DTLS 而不是 TLS

WebRTC 媒体走 UDP,TLS 要求底层有序可靠,所以用 DTLS(RFC 6347)。DTLS 自带:

  • 显式序列号(应对乱序)。
  • 重传机制(Handshake 消息分片 + 计时重传)。
  • HelloVerifyRequest cookie(防 DoS 反射)。

3.2 角色:actpass / active / passive

  • a=setup:actpass:offer 方默认值,表示"我可以做主动也可以做被动"。
  • a=setup:active:answer 方主动发起 DTLS ClientHello(WebRTC 客户端默认值)。
  • a=setup:passive:被动等待 ClientHello。

WebRTC 客户端永远倾向于 active,SFU 通常 passive

3.3 完整握手流程

3.4 关键字段解释

阶段关键字段作用
ClientHellocipher_suitesextensions.use_srtp协商加密套件 + SRTP 套件
HelloVerifyRequestcookie服务端无状态验证客户端可达,防 IP 伪造反射 DoS
CertificateDER 编码 X.509出示自签证书
ClientKeyExchangeECDHE 公钥完成密钥协商
CertificateVerify签名证明私钥归属
Finished整段握手 HMAC确认握手未被篡改

无 cookie 的 DTLS 会成为放大攻击的反射器:攻击者伪造源 IP 发 ClientHello,服务端的 ServerHello + Certificate 体积放大数十倍打到受害者。HelloVerifyRequest 让服务端在确认客户端确实持有源 IP 之前不分配任何状态、不发大包

什么时候不适用:纯内网部署 + 受信网络可关闭 cookie 检查降低 1 个 RTT,但不推荐。

3.6 失败时的样子

  • 没有公共 ECDHE 曲线:handshake_failure alert。
  • 重传 4 次仍超时(默认 60s):DTLS abort,iceConnectionStatefailed
  • 客户端发了 ClientHello 但 ICE 没切到 nominated pair:包丢失,握手卡死。

4. DTLS 1.3 握手

4.1 为什么

DTLS 1.3(RFC 9147,2022 年定稿)把握手压缩到 1-RTT,密钥派生更安全(HKDF 替代 PRF),并去除了 1.2 里多个不安全的设计(RSA 密钥交换、CBC 套件等)。

4.2 1-RTT 握手

花括号 {} 表示已用 handshake traffic key 加密。

4.3 0-RTT 的风险

DTLS 1.3 支持 0-RTT early data(用上次会话的 PSK 立即发数据),WebRTC 不使用 0-RTT

  • 0-RTT 不防重放,攻击者可重放抓到的早期数据。
  • WebRTC 媒体首包延迟容忍度足够(ICE 本身就要 RTT),省那一个 RTT 没意义。

结论:实现层应主动禁用 0-RTT。

4.4 新密钥派生

DTLS 1.3 用 HKDF-Expand-Label 派生密钥,SRTP key 提取的 label 仍是 EXTRACTOR-dtls_srtp(RFC 5764 第 4.2 节,DTLS 1.3 沿用),但底层 PRF 改为 HKDF。实现层无感,只要 DTLS 库版本足够新。

4.5 失败时的样子

  • 老 SFU 不支持 DTLS 1.3:浏览器自动回落 1.2。
  • 中间盒(防火墙)做 DTLS 深检但只认 1.2 record layer:1.3 record header 加密后被误判丢弃,表现为握手卡死,抓包看到 ClientHello 但没有任何回包。

5. SRTP 套件

SRTP(RFC 3711、RFC 7714)定义了三类常见套件:

套件类别加密认证推荐度
SRTP_AES128_CM_HMAC_SHA1_80流式AES-128-CTRHMAC-SHA1 80bit兼容老网关时用
SRTP_AES128_CM_HMAC_SHA1_32流式AES-128-CTRHMAC-SHA1 32bit不要用,认证太短
SRTP_AEAD_AES_128_GCMAEADAES-128-GCM内建现代默认
SRTP_AEAD_AES_256_GCMAEADAES-256-GCM内建高安全需求

5.1 为什么 AEAD 优于 CM+HMAC

  • AEAD(GCM)一次操作完成加密 + 认证,CPU 友好(硬件加速广泛)。
  • 单次 nonce 推导避免 CM+HMAC 各算一次的复杂度。
  • HMAC-SHA1-80 截断会限制安全裕度。

5.2 浏览器现状

  • Chrome / Edge / Firefox / Safari 自 2019 年起默认协商 AEAD_AES_128_GCM
  • 回落到 AES_CM_128_HMAC_SHA1_80 仅当对端不支持 GCM(多见于老 SBC、老版本 freeswitch)。

什么时候不适用:要互通 RFC 3711 严格实现的老网关时,必须保留 SHA1-80 套件,但应监控握手结果,避免长期回落而不自知。

5.3 失败时的样子

  • 两端 use_srtp 扩展无交集:DTLS 握手成功,但 SRTP 包全部解不开,表现为媒体黑屏 / 静音,getStats 看到 inboundRtp.packetsReceived 增长但 framesDecoded 为 0。

6. 密钥派生

6.1 Extractor

DTLS 握手完成后,两端各自从 DTLS master secret 用 PRF/HKDF 派生 SRTP 密钥材料:

extractor_output = PRF(master_secret,
"EXTRACTOR-dtls_srtp",
client_random || server_random,
2 * (key_len + salt_len))

输出按顺序切成:

| client_write_SRTP_master_key |
| server_write_SRTP_master_key |
| client_write_SRTP_master_salt|
| server_write_SRTP_master_salt|
套件key_lensalt_len
AES_CM_128_HMAC_SHA1_801614
AEAD_AES_128_GCM1612
AEAD_AES_256_GCM3212

6.2 SRTP session key

master key/salt 再经过 KDF(RFC 3711 §4.3,AES-CM 模式的 PRF)派生出三类 session key:

用途label
加密 RTP payload0x00
RTP 认证 tag0x01
RTP salt0x02
加密 SRTCP0x03
SRTCP 认证 tag0x04
SRTCP salt0x05

session key 在到达 kdr (key derivation rate) 包数后会自动滚动,避免单 key 用太久。

6.3 失败时的样子

  • 两端 client_random / server_random 不一致(极少见,通常意味着 DTLS 状态机被破坏):session key 不同,SRTP 解包全失败。
  • 实现错把 client_writeserver_write 用:单向能通、反向全坏,常见于自研 SFU 的早期 bug。

7. SRTCP

7.1 为什么独立加密

SRTCP(RFC 3711 §3.4)承载 RTCP 控制报文(SR/RR/NACK/PLI/REMB/TWCC 等)。它有独立 session key、独立 index、独立认证 tag。原因:

  • RTCP 报文频率远低于 RTP(默认带宽 5%),共用 RTP 的滑动窗口会造成 index 难管理。
  • RTCP 必须强制认证(认证 tag 长度规范要求 80bit,不像 RTP 可选 32bit)。

7.2 索引与防重放

SRTCP 包头包含一个 31-bit SRTCP index(明文),加密标记位 E(1bit)共同构成 32-bit。接收端维护防重放窗口(默认 64 packets),收到的 index 必须:

  1. 大于窗口右边界 → 接收 + 滑动窗口;
  2. 在窗口内且未收过 → 接收 + 标记;
  3. 已收过或小于左边界 → 丢弃
SRTCP 包结构(payload 加密前)
+----+-+--------------+---------+
| V P RC PT length | SSRC... | ← RTCP 公共头(明文)
+--------------------+----------+
| 报告/控制内容 | ← 加密体
+--------------------+
| E | SRTCP index (31b) | ← 明文
| Authentication tag (10 字节) | ← HMAC-SHA1-80 / GCM tag
+-------------------------------+

7.3 失败时的样子

  • 重放窗口太小(实现错配置成 16):弱网乱序时合法包被误判为重放,NACK 丢失 → 卡顿。
  • 实现忘记加密 SRTCP(只加密 SRTP):抓包能看到明文 RTCP,泄露 SSRC 关系、网络估计带宽等信息。

8. 重协商与密钥更新

8.1 为什么要 rekey

  • 单 key 加密的数据量有理论上限(GCM 在同一 key 下安全 nonce 数 ~2^32,AES-CM 有 2^48 包上限)。
  • 长会议(小时级)通话累积包数接近上限时必须换 key。

8.2 DTLS rekey 方式

版本机制
DTLS 1.2触发新一轮握手(renegotiation),用旧 epoch 保护新握手消息
DTLS 1.3KeyUpdate 消息(RFC 9147 §4.5.3),无需重新握手

DTLS 1.3 的 KeyUpdate 更轻量:单方发送,对端确认后双方各自滚动 traffic secret。SRTP key 通过 extractor 重新派生。

8.3 浏览器是否会主动 rekey

Chrome / Firefox 在普通 1v1 通话中不主动 rekey,因为单次会话很难触达套件上限。SFU 在长会议(>4 小时)场景下应主动监控并触发。

8.4 失败时的样子

  • 一端 rekey 一端没切:旧 key 还在解码新包,packetsLost 飙升后通话断开。
  • 实现里 rekey 时未做"过渡窗口"(一段时间内同时尝试新旧 key):rekey 边界丢包率短暂飙升。

9. 与 ICE/STUN 的交互顺序

9.1 严格顺序

9.2 为什么 DTLS 必须在 ICE 之后

  • ICE 决定走哪条路径(host / srflx / relay)。在 nominated pair 之前发 DTLS 包属于"对不确定的目标握手",浪费且容易被 NAT 丢。
  • DTLS 握手包大(含证书),ICE 未通时大概率 MTU 受限被丢。
  • 实现上 ICE agent 选 pair 后才把 socket 绑给 DTLS 层。

9.3 ICE restart 是否影响 DTLS

ICE restart 只换 ice-ufrag / ice-pwd不影响已建立的 DTLS 会话。SRTP key 不变,媒体不中断。这也是 ICE restart 能做无缝网络切换的前提。

9.4 失败时的样子

  • 实现把 DTLS ClientHello 在 ICE 之前发:被 NAT 丢,握手永远超时。
  • ICE 还在 checking,应用层尝试发 RTP:浏览器内部 SRTP 还没准备好,包直接丢,无任何报错。

10. Wireshark 抓包字段

抓包时关注以下字段(Wireshark filter:dtls || stun || srtp):

[ STUN Binding Request ]
Message Type : 0x0001
USERNAME : ufragB:ufragA
MESSAGE-INTEGRITY: <HMAC-SHA1 of ice-pwd>
PRIORITY : 0x6E7F1EFF
USE-CANDIDATE : (present in nominated check)
FINGERPRINT : 0x...

[ DTLS 1.2 ClientHello ]
Version : DTLS 1.2 (0xFEFD)
Random : 32 bytes
Session ID : 0
Cookie : 0 (first hello) / N bytes (after HelloVerifyRequest)
Cipher Suites : TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, ...
Extension: use_srtp
Protection Profile: SRTP_AEAD_AES_128_GCM (0x0007)
Protection Profile: SRTP_AES128_CM_HMAC_SHA1_80 (0x0001)
Extension: supported_groups
Named Group: secp256r1

[ DTLS Certificate ]
Certificate Length: 280
Signature Algorithm: ecdsa-with-SHA256
Subject : CN=WebRTC
Issuer : CN=WebRTC ← 自签 = 同一个
Subject Public Key: id-ecPublicKey, secp256r1

[ SRTP ]
RTP Payload Type : 111 (opus)
Sequence : 13422
Timestamp : 384720
SSRC : 0x1A2B3C4D
SRTP Encrypted Payload: <bytes>
SRTP Auth Tag : 16 bytes (GCM tag)

Wireshark 解 SRTP 需要导入 SRTP master key(从 Chrome chrome://webrtc-internalsgetStats 或专门的 SSLKEYLOGFILE 导出)。生产环境禁止导出 key 用于调试。

11. 反模式

反模式后果应该怎么做
用 365 天上限的过期证书新建 PC 直接失败缓存证书时校验 expires,提前 1 天刷新
不校验 SDP a=fingerprintMITM 可解密全部媒体浏览器默认会校验,自研 SFU 也必须做
信令走 ws://指纹被替换 → MITM强制 wss:// + 服务端鉴权
SRTP_AES128_CM_HMAC_SHA1_3232bit 认证 tag 易被伪造至少用 SHA1-80,新部署直接上 GCM
把 DTLS 握手放在 ICE 之前握手包被丢,连接超时DTLS 必须等 ICE nominated pair
启用 DTLS 1.3 0-RTTearly data 可被重放WebRTC 场景一律禁用 0-RTT
长会议不监控 rekey触达套件包数上限崩溃SFU 监控每 SSRC 包数,主动触发 rekey
用 RSA 1024 / SHA-1 指纹现代浏览器直接拒绝ECDSA P-256 + SHA-256
自研 SFU 共用 client/server key单向通另一向坏严格按 RFC 3711 §4.3 区分方向
调试时打开 SSLKEYLOGFILE 上线生产媒体可被解密仅本地调试环境启用

12. 自检清单

  • 客户端用 ECDSA P-256 自签证书,缓存且不超过 30 天复用
  • SDP 中存在 a=fingerprint:sha-256 ...,且与对端实际证书匹配
  • ICE nominated pair 选定后才看到 DTLS ClientHello
  • DTLS 协商出 SRTP_AEAD_AES_128_GCM 或更强
  • 信令链路 wss:// + JWT 鉴权
  • SFU 长会议主动 rekey 策略已上线
  • DTLS 1.3 部署时 0-RTT 已禁用
  • 抓包工具与 SSLKEYLOGFILE 仅限本地调试

13. 权威资料