1. DTLS与SRTP协议安全通信的核心逻辑
在实时音视频、物联网设备通信这些对延迟和丢包极其敏感的场景里,传统的TLS协议有时会显得力不从心。它的握手过程相对繁重,并且严格依赖按序到达的记录,一旦出现网络抖动或丢包,重传机制就会引入难以接受的延迟。这时,DTLS和SRTP就成为了更合适的选择。DTLS可以看作是TLS的“UDP版本”,它继承了TLS的安全特性,但运行在无连接的UDP之上,天然容忍乱序和丢包。SRTP则是专门为实时传输协议设计的,它在RTP协议之上直接提供加密和认证,开销极小。
这两者看似应用场景不同,但在底层安全机制上却共享着相似的工程哲学:如何在不可靠的传输层上,高效、可靠地实现数据的机密性、完整性和抗重放攻击。其核心流程都围绕着“封装”与“解封装”展开。简单来说,发送端(封装)的任务是给原始数据(Payload)穿上一件“安全外套”,这件外套包括加密层和完整性校验层;接收端(解封装)则需要验证这件外套是否完好无损,并安全地脱下它,还原出原始数据。
这个过程中,最关键的“裁缝”就是加密算法和模式。从早期的分组密码模式(如CBC)到流密码模式(如AES-CTR),再到如今主流的认证加密(AEAD,如AES-GCM/AES-CCM),技术的演进反映了对安全与效率平衡点的持续探索。理解这些模式在DTLS和SRTP中如何被具体调用、参数如何构建,是进行底层安全引擎开发或深度协议调试的基石。本文将深入这两个协议的加解密流程,结合工程实践,拆解从传统CBC到现代AEAD的每一步实现细节。
2. DTLS记录层封装与解封装全流程解析
DTLS的记录层协议是其安全传输的骨架,它定义了应用数据如何被分片、压缩(DTLS通常不压缩)、加密和认证,然后添加记录头,最终通过网络发送。与TLS最大的区别在于记录头中包含了显式的Epoch和序列号,这是支持乱序接收和抗重放的关键。
2.1 DTLS记录封装的核心步骤
封装过程,即发送端构建一个安全数据包的过程。无论使用何种加密套件,其宏观逻辑是一致的:认证部分数据头、加密载荷、计算并附加完整性校验值。我们结合NXP SEC引擎的处理流程,来具体看三种不同加密模式下的差异。
2.1.1 使用分组密码的封装流程
当使用类似AES-CBC-HMAC-SHA256这样的传统分组密码套件时,流程最为经典,也最能体现DTLS与TLS的差异。
第一步:准备与预处理。安全引擎(如SEC)收到待处理的明文载荷(Payload)和协议数据块。PDB中包含了本次加密所需的所有上下文信息:密钥、盐、序列号、Epoch、版本号等。引擎首先会检查PDB中的选项位,特别是IE和WB位,以决定如何构造初始化向量。根据RFC和工程实践,对于DTLS 1.2,正确的设置是WB=0(不使用上一块的密文)和IE=1(IV显式包含在记录中)。这意味着IV会作为一个独立的字段,放置在加密数据之前。
第二步:构建认证数据。这是DTLS与TLS在认证顺序上的关键区别。为了兼容TLS的认证计算模型,DTLS在计算HMAC时,认证的数据顺序并非网络传输的顺序。引擎会从PDB中提取Epoch、Sequence Number、Type、Version,并按照Epoch || Seq Num || Type || Version的顺序送入哈希引擎(Class 2 CHA)进行计算。这个拼接后的数据,连同后续的“长度(Pre-ICV)”字段(即完整记录长度减去ICV、填充和填充长度字节后的值),共同构成了HMAC的输入消息。这个“重排序”步骤至关重要,确保了与TLS实现的一致性。
第三步:加密载荷。在认证数据构建的同时或之后,加密流程并行进行。引擎将明文载荷、完整性校验值、填充字节和填充长度字节作为一个整体,使用指定的加密算法(如AES)和模式(CBC),以及构造好的IV进行加密。这里有一个工程细节:填充方案遵循PKCS#7,以确保数据长度符合分组密码的块大小要求。加密后的密文块被推送到输出帧缓冲区。
第四步:组装输出帧。最后,引擎按照网络传输的顺序组装最终的DTLS记录。顺序为:Type->Version->Epoch->Sequence Number->Length (Full Rec)->显式IV->加密后的数据。其中,Length (Full Rec)包含了记录头、显式IV、加密数据(含ICV和填充)的总长度。至此,一个完整的DTLS记录封装完成,可以发送。
注意:序列号管理。在封装过程中,每处理一个记录,序列号必须递增并更新回PDB内存中。这是一个必须由驱动或硬件严格保证的原子操作,任何序列号的重用或回滚都会导致严重的安全问题,例如可能让攻击者实施重放攻击。
2.1.2 使用流密码的封装流程
当使用AES-CTR-HMAC这类流密码套件时,流程有所简化,因为CTR模式不需要填充,且加密解密是对称操作。
核心差异:认证数据的构成。与分组密码类似,认证数据同样由重排序的头部(Epoch || Seq Num || Type || Version)和调整后的长度字段(Length (Pre-ICV))构成。这里的Length (Pre-ICV)是完整记录长度减去ICV长度。
计数器IV的构建。流密码加密需要一个初始计数器值。引擎从PDB中提取当前的Sequence Number和Write_IV(一个由密钥派生出的值),将它们写入Class 1上下文中,形成加密的初始计数器。加密时,AES-CTR模式会基于此计数器生成一个密钥流,与明文(Payload和ICV)进行异或操作,直接产生密文。因此,输出帧的组装顺序为:Type->Version->Epoch->Seq Num->Length (Full Rec)->加密后的Payload+ICV。
流密码的优势与注意点。AES-CTR模式的优势在于并行性好、无需填充,适合高速数据流。但需要注意的是,绝对禁止重复使用相同的密钥和计数器值,否则会完全破坏机密性。DTLS通过显式且递增的序列号参与IV/计数器构造,有效避免了这一问题。
2.1.3 使用AEAD密码的封装流程
AES-GCM和AES-CCM是现代DTLS 1.2推荐的AEAD密码套件,它们将加密和认证在一个操作内完成,效率更高,且通常更抗某些类型的攻击。
AAD的构建。AEAD操作除了处理明文,还需要输入“附加认证数据”。对于DTLS,AAD的构建方式与HMAC认证数据的构建思路一致:同样是重排序的头部(Epoch || Seq Num || Type || Version)加上Length (Pre-ICV)。这个Length (Pre-ICV)在这里表示不包含ICV(对于GCM还不包含Nonce_Explicit)的记录长度。AAD本身不加密,但参与认证标签的计算,确保其完整性。
Nonce/IV的生成。这是AEAD的关键。以AES-GCM为例,它需要一个12字节的IV。DTLS 1.2的构造方式与TLS 1.2相同:通常由一个固定的“盐”和一个每记录唯一的“nonce_explicit”组合而成。在SEC引擎的流程中,nonce_explicit是一个随机数,会被包含在最终的输出帧中(位于Epoch和Seq Num之后,加密数据之前)。接收方需要用它和共享的盐来重建相同的IV。
输出帧结构。对于AES-GCM,输出顺序为:Type->Version->Epoch->Seq Num->Length (Full Rec)->Nonce_Explicit->加密且认证后的密文->ICV。注意,Length (Full Rec)包含了所有字段的总长。AES-CCM的流程类似,但Nonce的构造和内部格式(B0, CTR0)有所不同,且nonce_explicit不显式传输,而是由双方根据共享信息和序列号等推导得出。
2.2 DTLS记录解封装的核心步骤
解封装是封装的逆过程,但增加了完整性验证和抗重放检查,更为复杂。
2.2.1 使用分组密码的解封装流程
接收端收到一个DTLS记录后,需要安全地还原出明文。
第一步:解析与预解密。引擎首先从输入帧中提取记录头信息。对于CBC模式,它有一个巧妙的“预解密”步骤来获取填充长度:由于CBC的特性,引擎可以快速跳转到消息的倒数第二个密文块,用它作为IV来解密最后一个块,从而得到最后一个字节——即填充长度字节。这避免了先解密整个消息再解析填充的低效操作。
第二步:认证与解密并行/串行。提取出头部的Type,Version,Epoch,Seq Num,Length后,引擎开始并行或流水线式处理:
- 认证计算:同样,将
Epoch || Seq Num || Type || Version以及计算得到的Length (Pre-ICV)送入哈希引擎。同时,解密后的明文载荷也被送入哈希引擎。 - 解密操作:根据IV(显式或隐式)和密钥,对密文(包括加密的载荷、ICV和填充)进行解密。
- 抗重放检查:如果启用,引擎会利用PDB中维护的状态(如一个滑动窗口),检查当前收到的序列号是否在可接受的范围内,以拒绝重放或过迟的数据包。这是一个重要的安全特性,必须在完整性验证前或同时进行,但最终生效应在完整性验证通过后。
第三步:完整性验证与输出。哈希引擎计算出一个ICV,与从密文中解密得到的ICV进行比较。如果不匹配,整个记录被丢弃,并返回认证错误。只有验证通过,解密后的明文载荷才会被输出到应用层。根据PDB的outFMT选项,输出可以仅是净荷,也可以是包含记录头的完整解密记录。
2.2.2 使用流密码与AEAD密码的解封装流程
流密码的解封装与封装对称,同样需要构建计数器IV,然后对密文(Payload+ICV)进行解密(同样是异或操作),再进行HMAC验证。
AEAD的解封装则是其加密的逆过程。引擎首先从输入帧中提取必要的字段(对于GCM是nonce_explicit),重建Nonce/IV。然后将接收到的记录头(按AAD要求重排序)作为AAD输入,将加密部分作为消息数据输入,将接收到的ICV标签传入。AEAD算法内部会执行解密和认证验证。如果认证失败,解密出的数据将被视为无效而丢弃。
实操心得:状态管理是关键。在实现DTLS解封装,尤其是硬件加速时,最棘手的部分往往是状态管理。PDB中的序列号、Epoch、抗重放窗口等状态必须在处理每个包后准确无误地更新并持久化。在多核或异步处理场景下,需要谨慎设计锁或原子操作机制,确保状态的一致性。一个常见的坑是,在完整性校验失败后,错误地更新了抗重放状态,这可能导致后续合法的包被误判为重放包而拒绝。
3. SRTP数据包处理的工程实现细节
SRTP为RTP媒体流提供安全保障,其设计目标是在极低的开销下提供加密、认证和抗重放保护。它与DTLS记录层有相似之处,但结构更紧凑,且专门针对RTP头部的特性进行了优化。
3.1 SRTP加密核心:计数器IV与Nonce的构建
SRTP的加密核心在于为每个RTP包生成一个唯一的计数器或Nonce,这是保证流密码安全性的生命线。
AES-CTR模式的计数器IV构建:SRTP使用AES-CTR模式。其128位的计数器IV由三部分异或而成:14字节的Salt Key(来自主密钥的派生值)、4字节的SSRC(同步源标识符)、2字节的序列号(Seq Num)以及4字节的滚动计数器(ROC)。具体公式可视为:IV = Salt Key XOR (SSRC || ROC || Seq Num || 0x0000),其中填充了2字节的零以对齐长度。这里的ROC是一个每当16位序列号回绕(从65535到0)时就加1的32位计数器,它与序列号共同构成了一个扩展的、不会重复的48位包索引,确保了IV的全局唯一性。
AEAD模式(GCM/CCM)的Nonce构建:对于AES-GCM和AES-CCM,需要构建一个12字节的Nonce。构建方式与CTR IV类似:Nonce = Salt Key XOR (0x0000 || ROC || Seq Num || SSRC)。注意这里SSRC和Seq Num的位置与CTR IV有所不同,且填充了2字节的零。这个12字节的Nonce直接作为GCM的IV,或用于构造CCM的B0和CTR0初始块。
重要:密钥派生与Salt管理。Salt Key并非直接使用的加密密钥,而是与主密钥一起通过密钥派生函数生成会话加密密钥和Salt。在实际工程中,必须严格遵循RFC 3711或更新的RFC 7714(对于GCM)中定义的密钥派生流程。Salt的泄露会危及所有使用该主密钥的会话安全。因此,Salt应作为密钥材料的一部分安全存储和传输。
3.2 SRTP封装与解封装流程
SRTP的封装解封装流程比DTLS记录层更为直白,因为它直接操作RTP包结构。
封装流程:
- 构建IV/Nonce:根据上述方法,使用PDB中的Salt、ROC以及输入帧RTP头中的SSRC和Seq Num,计算得到计数器IV(CTR模式)或Nonce(AEAD模式)。
- 处理头部与载荷:SRTP头部(包括RTP固定头、可能的CSRC列表和扩展头)被认证但不加密。对于HMAC-SHA-1套件,头部被送入哈希引擎;对于AEAD套件,头部作为AAD处理。
- 加密/认证加密:RTP载荷、可能的尾部填充及填充长度字节被加密(CTR模式)或进行认证加密(AEAD模式)。对于CTR模式,加密后的数据再被送入哈希引擎计算ICV。
- 组装与输出:输出帧按顺序包含:SRTP头部、加密后的载荷、可选的MKI、ICV。ROC仅用于IV生成和内部状态更新,不出现在输出帧中。处理完成后,如果序列号发生回绕,必须更新PDB中的ROC值。
解封装流程:
- 提取与构建:从输入帧中提取SRTP头部、加密载荷、ICV和可选的MKI。同样使用头部中的SSRC、Seq Num和PDB中的ROC、Salt构建IV/Nonce。
- 验证与解密:
- CTR+HMAC:将SRTP头部送入哈希引擎,解密载荷,将解密后的载荷送入哈希引擎,最后将ROC送入哈希引擎。计算ICV并与接收的ICV比较。
- AEAD:将SRTP头部作为AAD,加密部分作为消息数据,接收的ICV作为标签,传入AEAD算法进行认证解密。
- 抗重放检查:SRTP同样支持抗重放检查,通常使用一个滑动窗口(如64或128个包)。检查基于扩展的序列号(ROC, Seq Num)。工程上需要注意,抗重放状态的更新应在ICV验证成功之后进行,否则攻击者可能通过发送伪造的包来扰乱接收方的状态。
- 输出:验证通过后,输出原始的RTP包(头部+解密后的载荷)。
3.3 PDB配置与常见错误处理
无论是DTLS还是SRTP,协议数据块(PDB)的配置都是驱动开发中的核心。它相当于给硬件引擎下发的一份“工作指令单”。
关键字段解析:
- 算法与模式选择:通过
PROTINFO字段指定是DTLS还是SRTP,以及具体的加密套件(如AES128-CBC-SHA, AES128-GCM等)。 - 序列号与ROC管理:
Seq Num和ROC字段必须由软件在每次处理包后正确递增和回写。对于SRTP,ROC在序列号回绕时递增。 - Salt/Key:提供加密所需的盐值和密钥。注意密钥可能需要以“拆分密钥”格式加载以供HMAC使用。
- 选项字节:控制关键行为,如DTLS中IV的生成方式(
IE,WB位)、SRTP中是否包含MKI、是否启用抗重放及其窗口大小(ARS位)。 - 输出格式:控制输出帧包含哪些内容(如是否包含记录头)。
常见错误与排查:
- ICV校验���败:这是最常见的问题。首先检查双方使用的密钥、Salt是否一致。其次,检查认证数据的构造顺序,特别是DTLS中
Epoch和Seq Num是否被正确置于Type和Version之前进行计算。对于SRTP,确认ROC是否正确参与IV/Nonce计算和HMAC计算。 - 解密失败或输出乱码:检查IV/Nonce的构造是否正确。确认分组密码的填充方案是否一致(通常为PKCS#7)。检查计数器模式下的计数器是否从未重复。
- 抗重放误报:检查接收窗口大小设置是否合理。确认在ICV验证失败后没有错误地更新了抗重放状态。检查序列号同步机制,在会话恢复或密钥更新后,序列号应重置。
- 性能瓶颈:确保PDB、密钥描述符等数据结构在内存中对齐到硬件要求(通常是32字节或64字节边界),避免不必要的内存拷贝。利用硬件的并行处理能力,例如在支持的情况下让认证和加解密操作流水线进行。
4. 从CBC到AEAD:模式选择与工程实践考量
在嵌入式安全引擎中实现这些协议,不仅仅是理解流程,更重要的是做出正确的工程选择。
CBC模式:作为传统模式,其最大问题是需要填充,这会导致数据膨胀,并且对填充错误的处理可能成为侧信道攻击的突破口(如Padding Oracle攻击)。在DTLS中,由于记录本身可能丢失,攻击者更容易利用这一点。在现代工程实践中,应尽量避免在新的DTLS实现中使用CBC模式套件。
CTR模式:解决了填充问题,并行性好,效率高。但它需要额外的HMAC操作来实现认证,这意味着数据需要被处理两次(加密和认证),并且存在“先解密后认证”可能带来的理论风险(虽然在实际的DTLS/SRTP实现中,由于结构设计,该风险已 mitigated)。它仍然是SRTP RFC 3711的标准选项之一。
AEAD模式:以AES-GCM为代表,是当前的最佳实践。它将加密和认证融合为一个原子操作,效率通常更高,并且从设计上避免了先解密后认证的分离问题。GCM模式还需要特别注意IV的唯一性,DTLS/SRTP通过结合盐、序列号、ROC等确保了这一点。对于新的DTLS 1.2和SRTP实现,应优先选择AES-GCM套件。
工程落地建议:
- 硬件加速利用:像NXP SEC这样的硬件引擎,能极大卸载CPU的加解密负担。关键是将协议流程正确映射到硬件的描述符链(Descriptor)上,配置好PDB和上下文。
- 状态管理外置:对于复杂的多会话场景,考虑将序列号、ROC、抗重放窗口等状态维护在软件层,硬件只负责单次计算的加速。这提供了更大的灵活性,但需要确保软件状态与硬件操作间的同步。
- 测试与验证:构建全面的测试向量,包括边界情况(如序列号回绕、填充长度极值、无效ICV等)。使用Wireshark等工具捕获数据包,与成熟的软件实现(如OpenSSL的DTLS, libSRTP)进行交叉验证,是调试协议实现的不二法门。
- 关注RFC更新:安全协议在不断演进。例如,SRTP现在更推荐使用AES-GCM(RFC 7714),而非旧的AES-CM+HMAC-SHA1。及时跟进RFC,淘汰不安全的算法套件。
理解DTLS和SRTP的加解密流程,从宏观的协议交互到微观的比特位操作,是构建安全、高效实时通信系统的基石。尤其是在资源受限的嵌入式环境中,合理利用硬件加速,并做出正确的算法和模式选择,直接决定了产品的安全水位和性能表现。