WebRTC 传输安全机制第二话:深入显出 SRTP 协议
通过 后,RTC 通信的双方完成 和 的协商。接下来,我们继续分析在 WebRTC 中,如何使用交换的密钥,来对 RTP 和 RTCP 进行加密,实现数据的安全传输。同时,本文会对 libsrtp 使用中,遇到的问题的进行解答,例如,什么是 ROC,ROC 为什么是 32-bits?为什么会返回 error_code=9, error_code=10?交换的密钥有生命周期吗,如果有是多长时间呢?阅读本篇之前建议阅读,两者结合,效果更佳哦!
作者|进学
审校|泰一
要解决的问题
协议并没有对它的负载数据进行任何保护。因此,如果攻击者通过抓包工具,如 Wireshark,将音视频数据抓取到后,通过该工具就可以直接将音视频流播放出来,这是非常恐怖的事情。
在 WebRTC 中,为了防止这类事情发生,没有直接使用 协议,而是使用了 协议 ,即安全的 协议。WebRTC 使用了非常有名的 libsrtp 库将原来的 协议数据转换成 协议数据。
对 的负载 (payload) 进行加密,保证数据安全;
保证 包的完整性,同时防重放攻击。
SRTP/SRTCP 结构
SRTP 结构

从 SRTP 结构图中可以看到:
通常情况下只需要对 RTP 负载数据进行加密,如果需要对 RTP header extension 进行加密, 给出了详细方案,在 libsrtp 中也完成了实现。
SRTCP 结构

从 结构图中可以看到:
在初步认识了 和 的结构后,接下来介绍 和 如何得到了。
Key 管理
在 协议中,使用二元组 的方式来标识一个通信参与者的 会话,称为 。
在 SRTP 协议中使用三元组来标识一个 stream,一个 由多个 stream 组成。对每个 stream 的加解密相关参数的描述,称为 。
每个 stream 的 中 中的包含如下参数:
SSRC: Stream 使用的 SSRC。
Cipher Parameter:加解密使用的 key, salt,算法描述 (类型,参数等)。
Authentication Parameter: 完整性使用的 Key, salt,算法描述 (类型,参数等)。
Anti-Replay Data: 防止重放攻击缓存的数据信息,例如,ROC,最大序号等。
在 中,每个 Stream 都会使用到属于自己的,加解密 Key,Authentication Key。这些 Key 都是在同一个 Session 中使用到的,称为 。这些 是通过对 使用 KDF (Key Derivation Function) 导出的。
是用于导出 函数,KDF 默认使用是加解密函数。例如,在完成 DTLS 后,协商得到的 SRTP 加密算法的 Profile 为:
SRTP_AES128_CM_HMAC_SHA1_80
cipher: AES_128_CM
cipher_key_length: 128
cipher_salt_length: 112
maximum_lifetime: 2^31
auth_function: HMAC-SHA1
auth_key_length: 160
auth_tag_length: 80
对应的 为 。 的导出流程如下图所示:

的导出依赖于如下参数:
: 根据导出的 Key 的类型不同, 取值如下:
master_key: DTLS 完成后,协商得到的 Key。
master_salt: DTLS 完成后,协商得到的 Salt。
packet_index: RTP/RTCP 的包序号。SRTP 使用 48-bits 的隐式包需要,SRTCP 使用 31-bits 包序号。参考序号管理。
key_derivation_rate: key 导出速率,记为 kdr。默认取值为 0,执行 1 次 Key 导出。取值范围 。在 的情况下,在加密之前,执行一次 key 导出,后续在 时,执行 key 导出。
r = packet_index / kdr
key_id = label || r
x = key_id XOR master_salt
key = KDF(master_key, x)
'/':表示整除,B=0 时,C = A/B=0。||:表示连接的含义。A,B,C 使用网络字节序表示,C = A||B, 则 C 的高字节为 A,低字节位为 B。XOR:是异或运算,计算时按照低字节位对齐。
以下使用 ,举例说明 的导出过程,假设 DTLS 协商得到:
master_key: E1F97A0D3E018BE0D64FA32C06DE4139 // 128-bits
master_salt: 0EC675AD498AFEEBB6960B3AABE6 // 112-bits
导出加密 Key (cipher key):
packet_index/kdr: 000000000000
label: 00
master_salt: 0EC675AD498AFEEBB6960B3AABE6
-----------------------------------------------
xor: 0EC675AD498AFEEBB6960B3AABE6 (x, KDF input)
x*2^16: 0EC675AD498AFEEBB6960B3AABE60000 (AES-CM input)
cipher key: C61E7A93744F39EE10734AFE3FF7A087 (AES-CM output)
导出 SALT Key (cipher salt):
packet_index/kdr: 000000000000
label: 02
master_salt: 0EC675AD498AFEEBB6960B3AABE6
----------------------------------------------
xor: 0EC675AD498AFEE9B6960B3AABE6 (x, KDF input)
x*2^16: 0EC675AD498AFEE9B6960B3AABE60000 (AES-CM input)
30CBBC08863D8C85D49DB34A9AE17AC6 (AES-CM ouptut)
cipher salt: 30CBBC08863D8C85D49DB34A9AE1
导出校验 Key (auth key),需要 长度为 94 字节:
packet_index/kdr: 000000000000
label: 01
master salt: 0EC675AD498AFEEBB6960B3AABE6
-----------------------------------------------
xor: 0EC675AD498AFEEAB6960B3AABE6 (x, KDF input)
x*2^16: 0EC675AD498AFEEAB6960B3AABE60000 (AES-CM input)
auth key AES input blocks
CEBE321F6FF7716B6FD4AB49AF256A15 0EC675AD498AFEEAB6960B3AABE60000
6D38BAA48F0A0ACF3C34E2359E6CDBCE 0EC675AD498AFEEAB6960B3AABE60001
E049646C43D9327AD175578EF7227098 0EC675AD498AFEEAB6960B3AABE60002
6371C10C9A369AC2F94A8C5FBCDDDC25 0EC675AD498AFEEAB6960B3AABE60003
6D6E919A48B610EF17C2041E47403576 0EC675AD498AFEEAB6960B3AABE60004
6B68642C59BBFC2F34DB60DBDFB2 0EC675AD498AFEEAB6960B3AABE60005
至此,我们得到了 加密和认证需要的 :cipher key,auth key,salt key。
序列号管理
SRTP 序列号管理
在 包结构定义中使用 16-bit 来描述序列号。考虑到防重放攻击,消息完整性校验,加密数据,导出 的需要,在 协议中,SRTP 包的序列号,使用隐式方式来记录包序列号 ,使用 i 标识 。
对于发送端来说,i 的计算方式如下:
i = 2^16 * ROC + SEQ
其中,SEQ 是 RTP 包中描述的 16-bit 包序号。ROC (rollover couter) 是 RTP 包序号 (SEQ) 翻转计数,也就是每当 , ROC 计数加 1。ROC 初始值为 0。
对于接收端来说,考虑到丢包和乱序因素的影响,除了维护 ,还需要维护一个当前收到的最大包序号 s_l,当一个新的包到来时候,接收端需要估计出当前包所对应的实际 SRTP 包的序号。ROC 的初始值为 0,s_l 的初始值为收到第一个 SRTP 包的 SEQ。后续通过如下公式,估计接收到的 SRTP 序号 i:
i = 2^16 * v + SEQ
其中,可能的取值,ROC 是接收端本地维护的 ROC,SEQ 是收到 SRTP 的序号。v 分别取 ROC-1,ROC,ROC+1 计算出 i,与 进行比较,那个更接近,v 就取对应的值。完成 SRTP 解密和完整性校验后,更新 ROC 和 s_l,分如下 3 种情况:
更直观的代码描述:
if (s_l < 32768)
if (SEQ - s_l > 32768)
set v to (ROC-1) mod 2^32
else
set v to ROC
endif
else
if (s_l - 32768 > SEQ)
set v to (ROC+1) mod 2^32
else
set v to ROC
endif
endif
return SEQ + v*65536
SRTCP 序列号管理
中没有描述序号的字段, 的序号在 SRTCP 包,使用 中显示描述,详见 SRTCP 格式,也就是说在 SRTCP 的最大序列号为 2^31。
序列号与通信时长
可以看到 SRTP 的序列号最大值为 2^48, SRTCP 的序列号最大值为 2^16。在大多数应用中(假设每 128000 个 RTP 数据包至少有一个 RTCP 数据包),SRTCP 序号将首先达到上限。以 200 SRTCP 数据包 / 秒的速度, SRTCP 的 2^31 序列号空间足以确保大约 4 个月的通信。
防重放攻击
攻击者将截获的 SRTP/SRTCP 包保存下来,然后重新发送到网络中,实现了包的重放。SRTP 接收者通过维护一个重放列表 (ReplayList) 来防止这种攻击。理论上 Replay List 应该保存所有接收到并完成校验的包的序列号 index。在实际情况下 ReplayList 使用滑动窗口(sliding window)来实现防重放攻击。使用 来描述滑动窗口的大小。
SRTP 防重放攻击
在序列号管理部分,我们详述了接收者,根据接收到的 SRTP 包的 SEQ,ROC,s_l 估算出 SRTP 包的 的方法。同时,将接收者已经接收到 SRTP 包的最大序列号,记为 。计算差值 delta:
delta = packet_index - local_packet_index
分如下 3 种情况说明:
下图更加直观说明防重放攻击的三个区域:

SRTP-WINDOW-SIZE 的取值,最小是 64。应用可以根据需要设置成较大的值,libsrtp 会向上取整为 32 的整数倍。例如,在 WebRTC 中 。使用者可以根据需要进行调整,但要达到防重放攻击的目的。
SRTCP 防重放攻击
在 SRTCP 中,packet index 显式给出。在 libsrtp 中,SRTCP 的防重放攻击的窗口大小为 128。使用 记录防重放攻击的起始序列号。SRTCP 防重放攻击的检查步骤如下:
加密和校验算法
在 SRTP 中,使用了 CTR(Counter mode)模式的 AES 加密算法,CTR 模式通过递增一个加密计数器以产生连续的密钥流,计数器可以是任意保证长时间不产生重复输出的密钥。根据计数方式的不同,分为以下两种类型:
: ICM 模式(Integer Counter Mode,整数计数模式),使用整数计数运算。
: GCM 模式(Galois Counter Mode,基于伽罗瓦域计数模式),计数运算定义在伽罗瓦域。
在 SRTP 中,使用 完成加密算法,同时使用 完成 计算,对数据进行完整性校验,加密和 计算需要分两步完成。 基于 AEAD(Authenticated-Encryption with Associated-Data,关联数据的认证加密)的思想,在对数据进行加密的同时计算 值,实现了一个步骤,完成加密和校验信息的计算。下面分别对这个 和 的用法进行介绍。
AEC—ICM

上图描述了 的加密和解密过程,图中的 是通过 KDF 导出的 。加密和加密都是通过对 Counter 进行加密,与明文 P 异或运算得到加密数据 C,反之,与密文 C 异或运算得到明文数据 P。考虑到安全性,Counter 生成依赖于 , 包的索引(packet index)和包的 SSRC。Counter 是 128-bits 的计数,生成方式如下定义:
one byte
<-->
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|00|00|00|00| SSRC | packet index | b_c |---+
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |
|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ v
| salt (k_s) |00|00|->(+)
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |
|
v
+-------------+
encryption key (k_e) -> | AES encrypt |
+-------------+
|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ |
| keystream block |<--+
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
其中, 是 Counter 的计数,初始 的取值为 0,对应 Counter 0, 每加密 128-bits 数据, 增加 1,作为下一个 Counter。根据一个 RTP 包的 index,SSRC 计算出来的 Counter,组成了 keystream,每个 Counter 是一个 keystream block。通过使用 算法,对 的负载进行加密得到了 部分。
HMAC—SHA1
散列消息认证码(Hash-based message authentication code,缩写为 HMAC),是一种通过特别计算方式之后产生的消息认证码(MAC),使用密码散列函数,同时结合一个加密密钥,它可以用来保证数据的完整性,同时可以用来作某个消息的身份验证。HMAC 通过一个标准算法,在计算哈希的过程中,把 key 混入计算过程中。HMAC 的加密实现如下:
HMAC(K,M) = H ( (K XOR opad ) + H( (K XOR ipad ) + M ) )
H:hash 算法,比如,MD5,SHA-1,SHA-256。
B:块字节的长度,块是 hash 操作的基本单位。这里 B=64。
L:hash 算法计算出来的字节长度。(L=16 for MD5, L=20 for SHA-1)。
K:共享密钥,K 的长度可以是任意的,但是为了安全考虑,还是推荐 K 的长度 > B。当 K 长度大于 时候,会先在 K 上面执行 hash 算法,将得到的 L 长度结果作为新的共享密钥。如果 K 的长度 <, 那么会在 K 后面填充 0x00 一直到等于长度 B。
M:要认证的内容。
opad:外部填充常量,是 0x5C 重复 B 次。
ipad:内部填充常量,是 0x36 重复 B 次。
XOR:异或运算。
+:代表 "连接" 运算。
计算步骤如下:
和 计算 ,使用的 对应 Key 管理部分描述的 和 ,使用的 Hash 算法为 , 的长度为 80-bits。
在计算 的,要认证的内容 M 为:
M = Authenticated Portion + ROC
其中, 代表 "连接" 运算, 在 的结构图中给出。
在计算 时,要认证的内容 M 为:
M=Authenticated Portion
其中, 在 的结构图中给出。
通过使用 算法,计算得到 的 部分。
AES—GCM
使用计数器模式来加密数据,该操作可以有效地流水线化,GCM 身份验证使用的操作特别适合于硬件中的有效实现。在 详述了 GCM 的理论知识, 详述了硬件实现。
在 加密中的应用,在 进行了详细描述。Key 管理和序列号管理与本文中描述的相同,需要注意的是:
libsrtp 的使用
重要的结构 ,用来初始化加解密参数,在 中使用这个结构。以下参数需要关注:
是一个新生代实时通信服务器,对 libsrtp 感兴趣的同学,可以快速在本机搭起调试环境,进行相关测试,更加深入理解相关的算法。
总结
本文通过对 相关原理的深入详细解读,对 libsrtp 使用遇到的问题进行解答,希望能够给实时音视频通信的相关领域的同学以帮助。