Deep dive
How it works
In: an SRTP packet (RTP header, encrypted payload, tag) and the stream's context · Out: the plain RTP packet, or a drop · State: per SSRC: session keys k_e, k_a, k_s, ROC (32 bit), s_l (last sequence number), replay window (at least 64 packets) · Cost: one AES-CTR pass over the payload plus one HMAC (or one GCM pass); index and replay check O(1)
Receive path, RFC 3711 §3.3:
- Find the context by SSRC and destination.
- Estimate the index (Appendix A, signed arithmetic): if
s_l < 32768, thenv = ROC-1whenSEQ - s_l > 32768, elseROC; otherwisev = ROC+1whens_l - 32768 > SEQ, elseROC.i = SEQ + v * 65536. - Session keys, derived once per master key (rate 0, §4.3):
x = (label || index DIV rate) XOR master_salt; key= AES-CM(master_key, IV = x * 2^16)truncated. Labels 0x00 k_e, 0x01 k_a, 0x02 k_s (SRTCP 0x03-0x05). - Replay check against the window; drop if seen or too old, before any crypto.
- Verify the tag over
M = header || ciphertext || ROC(HMAC-SHA1 truncated to the left-most 80 or 32 bits); drop on mismatch. - Decrypt:
IV = (k_s * 2^16) XOR (SSRC * 2^64) XOR (i * 2^16); keystream block j isAES(k_e, IV + j), XORed into the payload. - Commit only now:
v = ROC+1setss_l = SEQ,ROC = v;v = ROCraisess_lifSEQis larger;v = ROC-1changes nothing. Markiin the window. - Strip the tag (and optional MKI).
The sender: ROC starts at 0, increments when SEQ wraps; encrypt, then tag.
SRTCP carries its index explicitly: a 31-bit counter plus a 1-bit E flag (payload encrypted or not) after the compound packet; the mandatory tag covers both. Same master key, other labels, no estimation.
AES-GCM profile (RFC 7714): 12-byte IV = 2 zero bytes, SSRC (4), ROC (4), SEQ (2), XOR 12-byte salt; the RTP header is associated data; a full 16-byte tag, no separate HMAC (profiles SRTP_AEAD_AES_128_GCM, SRTP_AEAD_AES_256_GCM).
Keys: DTLS-SRTP (RFC 5764) runs a DTLS handshake on the media path and exports 2 * (key + salt) bytes under the label EXTRACTOR-dtls_srtp: client key, server key, client salt, server salt; one context per direction. SDES (RFC 4568) puts the key inside the SDP, so it is only as private as the signalling path; RFC 8827 says WebRTC must not offer or select it and must offer DTLS-SRTP on every media channel.
Through an SFU: SRTP is hop-by-hop; the SFU decrypts and re-encrypts. SFrame (RFC 9605) encrypts whole frames end to end, independent of RTP.
Code
Std only. AES is not implemented here (use a vetted library); the block cipher is a trait and XorStandIn is a stand-in that only tests the plumbing, never a cipher.
/// One SRTP stream (SSRC): RFC 3711 s_l/ROC plus a 64-packet replay window.
#[derive(Default)]
pub struct Rx { started: bool, roc: u32, s_l: u16, top: u64, mask: u64 }
impl Rx {
/// Index estimate (RFC 3711 App. A) + replay check; `commit` only after the tag verified.
pub fn check(&self, seq: u16) -> Option<(u32, u64)> {
if !self.started { return Some((self.roc, (self.roc as u64) << 16 | seq as u64)); }
let (s, q) = (self.s_l as i32, seq as i32);
let v = if s < 32768 { if q - s > 32768 { self.roc.wrapping_sub(1) } else { self.roc } }
else if s - 32768 > q { self.roc.wrapping_add(1) } else { self.roc };
if self.roc == 0 && v == u32::MAX { return None; } // would precede the session start
let i = (v as u64) << 16 | seq as u64;
if i > self.top { return Some((v, i)); }
let d = self.top - i;
if d >= 64 || self.mask >> d & 1 == 1 { None } else { Some((v, i)) } // too old / replay
}
/// After the tag verified: update s_l, ROC (RFC 3711 s3.3.1) and the window.
pub fn commit(&mut self, v: u32, i: u64, seq: u16) {
if !self.started { self.started = true; self.s_l = seq; self.top = i; self.mask = 1; return; }
if v == self.roc.wrapping_add(1) { self.s_l = seq; self.roc = v; }
else if v == self.roc && seq > self.s_l { self.s_l = seq; } // v == ROC-1: no update
if i > self.top {
let sh = i - self.top;
self.mask = if sh >= 64 { 1 } else { self.mask << sh | 1 };
self.top = i;
} else { self.mask |= 1 << (self.top - i); }
}
}
/// IV = (k_s * 2^16) XOR (SSRC * 2^64) XOR (i * 2^16), RFC 3711 s4.1.1; k_s = 112-bit session salt.
pub fn iv(salt: &[u8; 14], ssrc: u32, index: u64) -> [u8; 16] {
let mut s = [0u8; 16];
s[..14].copy_from_slice(salt);
(u128::from_be_bytes(s) ^ (ssrc as u128) << 64 ^ (index as u128) << 16).to_be_bytes()
}
pub trait BlockCipher { fn encrypt(&self, block: [u8; 16]) -> [u8; 16]; }
/// Segmented integer counter mode: keystream block j = E(k, IV + j mod 2^128).
pub fn ctr_xor<C: BlockCipher>(c: &C, iv: [u8; 16], data: &mut [u8]) {
let mut ctr = u128::from_be_bytes(iv);
for chunk in data.chunks_mut(16) {
let ks = c.encrypt(ctr.to_be_bytes());
chunk.iter_mut().zip(ks).for_each(|(b, k)| *b ^= k);
ctr = ctr.wrapping_add(1);
}
}
/// STAND-IN, NOT A CIPHER: XOR with a fixed block, only to test the plumbing without AES.
pub struct XorStandIn(pub [u8; 16]);
impl BlockCipher for XorStandIn {
fn encrypt(&self, mut b: [u8; 16]) -> [u8; 16] { b.iter_mut().zip(self.0).for_each(|(x, k)| *x ^= k); b }
}
#[cfg(test)]
mod tests {
use super::*;
fn feed(r: &mut Rx, seqs: &[u16]) -> Vec<bool> {
seqs.iter().map(|&s| r.check(s).map(|(v, i)| r.commit(v, i, s)).is_some()).collect()
}
#[test]
fn wrap_late_replay_window() {
let mut r = Rx::default();
assert_eq!(feed(&mut r, &[65533, 65534, 0, 1]), [true; 4]);
assert_eq!(r.check(2), Some((1, 65538))); // ROC went 0 -> 1 at 65535 -> 0
assert_eq!(r.check(65535), Some((0, 65535))); // late packet from before the wrap: old ROC
r.commit(0, 65535, 65535);
assert_eq!(r.check(65535), None); // replay
let mut r = Rx::default();
feed(&mut r, &[100, 200]);
assert!(r.check(137).is_some() && r.check(136).is_none()); // 200-136 = 64: too old
}
#[test]
fn iv_and_counter_mode() {
let salt = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; // hand-computed XOR, ROC 1, SEQ 2
assert_eq!(iv(&salt, 0xDEADBEEF, 0x1_0002),
[1, 2, 3, 4, 0xDB, 0xAB, 0xB9, 0xE7, 9, 10, 11, 0x0D, 0x0D, 0x0C, 0, 0]);
// RFC 3711 B.2: salt F0..FD, ROC 0, SEQ 0, SSRC 0 gives first counter F0F1..FD0000
let b2 = [0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD];
let ivb = iv(&b2, 0, 0);
assert!(ivb[..14] == b2 && ivb[14..] == [0, 0]);
let mut z = [0u8; 32];
ctr_xor(&XorStandIn([0; 16]), ivb, &mut z); // key 0: keystream = counter blocks
assert!(z[..16] == ivb && z[30..] == [0, 1]); // second counter ends ...0001
}
}
Complexity and constants
- Index estimate, replay check and commit: O(1), a handful of compares and one shift. Measured on this Mac (Apple M4 Pro), release build, single thread, packets 1 in 8 reordered: about 394 M checks/s (2.5 ns each, three runs 394.1-394.7 M/s). Cheap and branch-predictable; real traffic adds context-lookup cache misses.
- State per stream in the code: ROC 4 B + s_l 2 B + top 8 B + window 8 B, plus session keys (16 B k_e, 20 B k_a, 14 B k_s).
- Cost drivers: the AES pass and the HMAC, linear in packet size.
- Limits per master key: 2^48 SRTP packets (RFC 3711 §3.3.1); 2^31 SRTCP packets (RFC 7714 profiles).
Efficiency
- Cost: per packet: one AES-CTR pass over the payload, one HMAC-SHA1 over header plus payload (or one GCM pass), index and replay check 2.5 ns (measured on this Mac, Apple M4 Pro). Bound: compute per payload byte; it matters most for an SFU that re-encrypts every forwarded copy. Added bytes: 10 / 4 / 16.
- Levers:
- Replay check before crypto — no HMAC or AES spent on duplicates (RFC 3711 §3.3) — always.
- AES-GCM profile instead of CM + HMAC — one pass, no auth key (RFC 7714) — when both ends support it.
- -32 tag — 6 bytes less per packet (RFC 5764 profiles) — only where one forged packet is tolerable (RFC 3711 §7.5); RTCP keeps 80 bits.
- Keys derived once per master key (rate 0, RFC 3711 §4.3.1) — no per-packet derivation — default.
- SFrame — end-to-end without SRTP double encryption, which RFC 9605 §1 calls inefficient — group calls via an SFU.
- Naive → standard → state of the art:
- Keys derived per packet, software AES, separate HMAC · derivation dominates.
- Keys derived once, AES-CM + HMAC-SHA1-80, hardware AES · two passes, +10 B.
- AES-GCM (RFC 7714), hardware AES and carry-less multiply, SFrame for end-to-end · one pass, +16 B.
Implementation notes
- Check replay and tag before decrypting, commit state only after the tag verified; RFC 3711 §3.3.1 warns the ROC cannot be made robust without authentication.
- Window as a u64 bitmask relative to the highest authenticated index; a shift by 64 or more must reset to one bit (
<< 64panics in debug Rust). - Compare tags in constant time. One context per SSRC and direction.
- At most 2^16 AES blocks per packet (IV reserves 16 counter bits): always true for UDP.
Pitfalls
- Updating ROC/s_l from unauthenticated packets: one forged packet desynchronises the stream.
- Reusing (key, SSRC, index) is a two-time pad (RFC 3711 §9.1): two senders with one SSRC and key, or a restart that resets SEQ and ROC.
- ROC 0 with
v = ROC-1wraps to an index near 2^48: reject it (done in the code). - Resetting the ROC on re-keying: RFC 3711 forbids it.
- -32 tags on stateful codecs: one forged packet can corrupt later decoding (RFC 3711 §7.5).
Variants and alternatives
In production
- WebRTC must secure all media with SRTP keyed by DTLS-SRTP (RFC 8827 §6.5); SIP stacks negotiate it through SDP offer/answer.
- The key derivation is the RFC 3711 AES-CM PRF, not HKDF; the handshake underneath is DTLS, a TLS relative.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| libsrtp | C | cisco/libsrtp | apt install libsrtp2-dev | — | — | — | — | reference C library; pluggable crypto backend; no Homebrew formula, build with CMake |
| webrtc-srtp | Rust | webrtc-rs/webrtc (srtp/) | cargo add webrtc-srtp | — | — | — | — | pure Rust SRTP/SRTCP |
| pion/srtp | Go | pion/srtp | go get github.com/pion/srtp/v3 | — | — | — | — | Go SRTP used by Pion WebRTC |
| str0m | Rust | algesten/str0m | cargo add str0m | — | — | — | — | sans-IO WebRTC library incl. SRTP; crypto backend selectable |
Sources
- Baugher, McGrew, Naslund, Carrara, Norrman, The Secure Real-time Transport Protocol (SRTP), RFC 3711, 2004. rfc
- McGrew, Igoe, AES-GCM Authenticated Encryption in the Secure Real-time Transport Protocol (SRTP), RFC 7714, 2015. rfc
- McGrew, Rescorla, Datagram Transport Layer Security (DTLS) Extension to Establish Keys for the Secure Real-time Transport Protocol (SRTP), RFC 5764, 2010. rfc
- Andreasen, Baugher, Wing, Session Description Protocol (SDP) Security Descriptions for Media Streams, RFC 4568, 2006. rfc
- Rescorla, WebRTC Security Architecture, RFC 8827, 2021. rfc
- Omara, Uberti, Murillo, Barnes (ed.), Fablet, Secure Frame (SFrame): Lightweight Authenticated Encryption for Real-Time Media, RFC 9605, 2024. rfc