WebRTC 强制要求所有媒体流加密。DTLS-SRTP 是唯一合规方案:用 DTLS 握手协商密钥,用 SRTP 加密 RTP 媒体包。
一、为什么 WebRTC 必须加密?
RFC 8827 明确规定:所有 WebRTC 媒体流必须使用 SRTP 加密,且密钥必须通过 DTLS 协商(不允许静态预共享密钥)。
这意味着:
- 没有 DTLS 握手 = 没有媒体流
PeerConnectionState.CONNECTED才是真正的连接完成标志(不是 ICE CONNECTED)- 代码里
"DTLS connected"才触发通话建立
java
// CallActivity.java
// ICE 连通不代表通话建立
// 真正的里程碑是 DTLS 握手成功:
if (newState == PeerConnectionState.CONNECTED) {
logAndToast("DTLS connected, delay=" + delta + "ms"); // line 924
}
二、完整握手时序
scss
发起方 (Initiator / DTLS Client) 接收方 (Responder / DTLS Server)
| |
[ICE 连通,UDP 通道就绪] [ICE 连通,UDP 通道就绪]
| |
|--- DTLS ClientHello ------------------>|
| (支持的密码套件, 随机数) |
| |
|<-- DTLS ServerHello -------------------|
| (选定密码套件, 服务端证书, 随机数) |
| |
[验证证书指纹 vs SDP a=fingerprint] [验证证书指纹 vs SDP a=fingerprint]
| |
|--- DTLS ClientKeyExchange ------------>|
|--- DTLS CertificateVerify ------------->|
|--- DTLS ChangeCipherSpec ------------->|
|--- DTLS Finished --------------------->|
| |
|<-- DTLS ChangeCipherSpec --------------|
|<-- DTLS Finished ----------------------|
| |
[双方从 DTLS master secret 导出 SRTP 密钥]
| |
|=== SRTP 加密的 RTP/RTCP 媒体流 =======>|
三、证书与指纹:防中间人攻击
DTLS 证书类型
WebRTC 引擎在 PeerConnectionFactory 初始化时自动生成自签名证书,本项目使用 ECDSA(比 RSA 快 10 倍握手):
java
// PeerConnectionClient.createPeerConnectionInternal()
rtcConfig.keyType = PeerConnection.KeyType.ECDSA; // line 606
ECDSA 密钥更短(256 bit vs 2048 bit),握手计算量更低,移动端友好。
指纹验证流程
- 本端在创建
PeerConnection时自动生成证书,提取 SHA-256 指纹 - 指纹写入 SDP,通过信令发给对端:
ini
// SDP 中的指纹字段
a=fingerprint:sha-256 4A:6F:2B:...(64个十六进制字节)
a=setup:actpass
- DTLS 握手时,收到对端真实证书后,计算其 SHA-256 指纹,与 SDP 里的
a=fingerprint比对 - 不一致 = 中间人攻击,立即断开
这就是为什么信令通道只传 SDP(不传密钥),但仍然安全:攻击者就算劫持信令,也无法伪造合法证书通过指纹校验。
四、SDP 中的 DTLS 相关字段
ini
// 传输协议声明(RTP over DTLS over SRTP)
m=audio 9 UDP/TLS/RTP/SAVPF 111 103
// 本端证书指纹(SHA-256)
a=fingerprint:sha-256 4A:6F:2B:C1:...
// DTLS 角色协商
a=setup:actpass ← 发起方:我可以当 client 或 server,你决定
a=setup:active ← 接收方:我当 client(主动发 ClientHello)
a=setup:passive ← 被动方:我当 server(等待 ClientHello)
a=setup 角色协商规则
| 本端 setup | 对端 setup | 结果 |
|---|---|---|
actpass |
active |
本端 = DTLS Server,对端 = DTLS Client |
actpass |
passive |
本端 = DTLS Client,对端 = DTLS Server |
active |
passive |
本端 = DTLS Client,对端 = DTLS Server |
发起方(Offer)通常设置 actpass,接收方(Answer)选择 active,所以接收方发起 DTLS ClientHello。
五、SRTP 密钥导出
DTLS 握手成功后,双方从 DTLS 的 master secret 通过 DTLS-SRTP Key Material Exporter(RFC 5764)导出 SRTP 密钥材料:
python
SRTP_KEY_MATERIAL = PRF(master_secret, "EXTRACTOR-dtls_srtp", client_random + server_random, 60 bytes)
拆分为:
├── client_write_SRTP_master_key (16 bytes) ← 发起方加密用
├── server_write_SRTP_master_key (16 bytes) ← 接收方加密用
├── client_write_SRTP_master_salt (14 bytes)
└── server_write_SRTP_master_salt (14 bytes)
每个方向的 RTP 加密独立使用各自的密钥,无需在网络上传输任何密钥(密钥从 master secret 本地计算)。
六、SRTP 包结构:RTP vs SRTP
普通 RTP 包(明文,WebRTC 不允许)
java
| RTP Header (12 bytes) | RTP Payload (明文音视频数据) |
SRTP 包(加密后)
java
| RTP Header (12 bytes,不加密) | 加密的 Payload | SRTP Auth Tag (10 bytes) |
- Header 不加密:路由节点需要读取 SSRC、Seq、Timestamp 做 QoS
- Payload 加密:AES-128-CM(Counter Mode)加密音视频内容
- Auth Tag:HMAC-SHA1 认证,防止篡改(即使攻击者无法解密,也无法伪造合法包)
七、代码中的关键约束
强制启用 DTLS-SRTP
java
// PeerConnectionClient.java line 112
private static final String DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT = "DtlsSrtpKeyAgreement";
// MediaConstraints 中添加(旧版 API,现代 WebRTC 已默认启用)
// keyAgreement = true 表示强制使用 DTLS-SRTP 密钥协商
Loopback 模式特例(禁用加密)
单设备自测时,loopback 模式会关闭加密,避免自握手的复杂性:
java
// PeerConnectionClient.createPeerConnectionInternal() line 444-445
if (peerConnectionParameters.loopback) {
options.disableEncryption = true;
options.networkIgnoreMask = 0; // CallActivity line 376
}
生产环境绝不能设置
disableEncryption = true。
DTLS 连接状态监听
java
// PCObserver.onConnectionChange() PeerConnectionClient.java line 1248-1258
@Override
public void onConnectionChange(PeerConnectionState newState) {
executor.execute(() -> {
if (newState == PeerConnectionState.CONNECTED) {
events.onConnected(); // → CallActivity: "DTLS connected"
} else if (newState == PeerConnectionState.DISCONNECTED) {
events.onDisconnected(); // → CallActivity: "DTLS disconnected"
} else if (newState == PeerConnectionState.FAILED) {
reportError("DTLS connection failed."); // ICE 通但 DTLS 失败
}
});
}
八、完整时间线(从信令到媒体流)
ini
t=0 用户点击"连接"
t=1 HTTP 获取房间参数(STUN/TURN 服务器列表)
t=2 WebSocket 建立,注册 clientId
t=3 Offer SDP 生成(含 a=fingerprint, a=setup:actpass)
t=4 Offer 通过信令发给对端
t=5 对端生成 Answer SDP(含自己的 a=fingerprint, a=setup:active)
t=6 ICE Gathering:采集 host/srflx/relay candidates
t=7 ICE candidates 通过信令交换
t=8 ICE Connectivity Check:STUN ping/pong
t=9 IceConnectionState → CONNECTED(UDP 通道打通)
t=10 DTLS ClientHello(接收方发起,因为 a=setup:active)
t=11 证书交换 + 指纹验证
t=12 DTLS Finished(双向)
t=13 SRTP 密钥从 master secret 导出
t=14 PeerConnectionState → CONNECTED
logAndToast("DTLS connected, delay=" + delta + "ms")
t=15 SRTP 加密的 RTP 包开始流动(媒体传输开始)
九、常见问题
ICE CONNECTED 了但 DTLS FAILED?
原因:
- 两端 SDP 里的
a=fingerprint不一致(信令篡改?证书重新生成?) - 网络防火墙拦截了 DTLS 握手包(UDP 443/5349 端口被封)
- 两端时钟偏差过大导致证书有效期校验失败(DTLS 证书通常有效期很短)
代码中触发路径:PeerConnectionState.FAILED → reportError("DTLS connection failed.") → CallActivity 显示错误并挂断。
为什么不用 TLS 而用 DTLS?
RTP 基于 UDP,UDP 无序、不可靠,TLS 要求底层可靠传输(TCP)。DTLS(Datagram TLS)是 TLS 的 UDP 版本,增加了:
- 消息重传机制(握手包可能丢失)
- 消息序号(防止重排)
- 握手分片(避免超过 MTU)
十、一句话总结
| 组件 | 作用 |
|---|---|
| DTLS | 在 UDP 上进行 TLS 握手,完成双向身份认证和密钥协商 |
a=fingerprint |
SDP 中携带证书指纹,防止中间人替换证书 |
| SRTP 密钥导出 | 从 DTLS master secret 本地派生,密钥从不在网络传输 |
| SRTP | 对 RTP Payload 加密(AES-128-CM)+ 认证(HMAC-SHA1) |
PeerConnectionState.CONNECTED |
DTLS 握手完成的信号,媒体流才真正开始 |