gemini_adk_fluent_rs/telephony/
srtp.rs

1//! SRTP (RFC 3711) for SIP media, keyed by SDES (RFC 4568).
2//!
3//! *(feature `sip`)* A SIP trunk or softphone that offers `RTP/SAVP` with an
4//! `a=crypto` line sends its media encrypted and authenticated. This module
5//! is the packet layer: [`SrtpSession::protect`] turns an RTP packet into an
6//! SRTP packet, and [`SrtpSession::unprotect`] checks and decrypts one.
7//! [`CryptoAttribute`] parses and prints the SDP line that carries the keys.
8//!
9//! Supported: the `AES_CM_128_HMAC_SHA1_80` and `AES_CM_128_HMAC_SHA1_32`
10//! suites (the ones every SDES endpoint implements), a key derivation rate
11//! of zero, and no MKI. SRTCP is not implemented, since the agent does not
12//! send RTCP; an offer is answered for RTP only.
13//!
14//! SDES sends the keys in the SDP body, so they are only as secret as the
15//! signalling. Use SIP over TLS when the keys must not be readable on the
16//! network path.
17
18use std::collections::HashMap;
19use std::fmt;
20
21use aes::Aes128;
22use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
23use base64::Engine as _;
24use hmac::{Hmac, Mac};
25use sha1::Sha1;
26
27type HmacSha1 = Hmac<Sha1>;
28
29const MASTER_KEY_LEN: usize = 16;
30const MASTER_SALT_LEN: usize = 14;
31const AUTH_KEY_LEN: usize = 20;
32/// Packets this far behind the newest one are rejected as possible replays.
33const REPLAY_WINDOW: u64 = 64;
34
35/// An SRTP crypto suite.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[non_exhaustive]
38pub enum SrtpSuite {
39    /// AES-128 counter mode, HMAC-SHA1 with an 80-bit tag.
40    AesCm128HmacSha1_80,
41    /// AES-128 counter mode, HMAC-SHA1 with a 32-bit tag.
42    AesCm128HmacSha1_32,
43}
44
45impl SrtpSuite {
46    /// The suite's SDP name.
47    pub fn name(self) -> &'static str {
48        match self {
49            Self::AesCm128HmacSha1_80 => "AES_CM_128_HMAC_SHA1_80",
50            Self::AesCm128HmacSha1_32 => "AES_CM_128_HMAC_SHA1_32",
51        }
52    }
53
54    /// The suite named `name` in SDP, when supported.
55    pub fn from_name(name: &str) -> Option<Self> {
56        match name {
57            "AES_CM_128_HMAC_SHA1_80" => Some(Self::AesCm128HmacSha1_80),
58            "AES_CM_128_HMAC_SHA1_32" => Some(Self::AesCm128HmacSha1_32),
59            _ => None,
60        }
61    }
62
63    /// Bytes of authentication tag appended to each packet.
64    pub fn tag_len(self) -> usize {
65        match self {
66            Self::AesCm128HmacSha1_80 => 10,
67            Self::AesCm128HmacSha1_32 => 4,
68        }
69    }
70}
71
72/// A master key and salt, from which the session keys are derived.
73#[derive(Clone, PartialEq, Eq)]
74pub struct MasterKey {
75    key: [u8; MASTER_KEY_LEN],
76    salt: [u8; MASTER_SALT_LEN],
77}
78
79impl MasterKey {
80    /// A key and salt from their bytes.
81    pub fn new(key: [u8; MASTER_KEY_LEN], salt: [u8; MASTER_SALT_LEN]) -> Self {
82        Self { key, salt }
83    }
84
85    /// A fresh random key and salt from the operating system's generator.
86    pub fn generate() -> std::io::Result<Self> {
87        let mut buf = [std::mem::MaybeUninit::<u8>::uninit(); MASTER_KEY_LEN + MASTER_SALT_LEN];
88        let bytes = getrandom::getrandom_uninit(&mut buf).map_err(std::io::Error::other)?;
89        Ok(Self::from_bytes(bytes).expect("the length is right"))
90    }
91
92    fn from_bytes(bytes: &[u8]) -> Option<Self> {
93        if bytes.len() != MASTER_KEY_LEN + MASTER_SALT_LEN {
94            return None;
95        }
96        Some(Self {
97            key: bytes[..MASTER_KEY_LEN].try_into().ok()?,
98            salt: bytes[MASTER_KEY_LEN..].try_into().ok()?,
99        })
100    }
101
102    /// The SDES `inline:` key parameter: base64 of key then salt.
103    pub fn to_inline(&self) -> String {
104        let mut bytes = Vec::with_capacity(MASTER_KEY_LEN + MASTER_SALT_LEN);
105        bytes.extend_from_slice(&self.key);
106        bytes.extend_from_slice(&self.salt);
107        base64::engine::general_purpose::STANDARD.encode(bytes)
108    }
109
110    /// Parse an SDES `inline:` key parameter (with or without the prefix).
111    /// A lifetime (`|2^31`) is accepted and ignored; an MKI (`|1:4`) is not
112    /// supported and fails the parse.
113    pub fn from_inline(inline: &str) -> Option<Self> {
114        let inline = inline.strip_prefix("inline:").unwrap_or(inline);
115        let mut parts = inline.split('|');
116        let key = parts.next()?;
117        for part in parts {
118            if part.contains(':') {
119                return None; // an MKI
120            }
121        }
122        let bytes = base64::engine::general_purpose::STANDARD.decode(key).ok()?;
123        Self::from_bytes(&bytes)
124    }
125}
126
127impl fmt::Debug for MasterKey {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.write_str("MasterKey([redacted])")
130    }
131}
132
133/// An SDES `a=crypto` attribute (RFC 4568).
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct CryptoAttribute {
136    /// The attribute's tag, echoed in the answer.
137    pub tag: u32,
138    /// The crypto suite.
139    pub suite: SrtpSuite,
140    /// The sender's master key.
141    pub key: MasterKey,
142}
143
144impl CryptoAttribute {
145    /// Parse the value of an `a=crypto:` line, e.g.
146    /// `1 AES_CM_128_HMAC_SHA1_80 inline:WVNfX19zZW...`. `None` for an
147    /// unsupported suite, key form or session parameter.
148    pub fn parse(value: &str) -> Option<Self> {
149        let value = value.strip_prefix("a=crypto:").unwrap_or(value);
150        let mut parts = value.split_whitespace();
151        let tag = parts.next()?.parse().ok()?;
152        let suite = SrtpSuite::from_name(parts.next()?)?;
153        let key_params = parts.next()?;
154        // One key only: several `;`-joined keys need an MKI to tell apart.
155        if key_params.contains(';') {
156            return None;
157        }
158        let key = MasterKey::from_inline(key_params)?;
159        // Session parameters (UNENCRYPTED_SRTP, KDR=.., ...) change the
160        // transform; none is supported.
161        if parts.next().is_some() {
162            return None;
163        }
164        Some(Self { tag, suite, key })
165    }
166
167    /// The attribute's value, for an `a=crypto:` line.
168    pub fn to_value(&self) -> String {
169        format!(
170            "{} {} inline:{}",
171            self.tag,
172            self.suite.name(),
173            self.key.to_inline()
174        )
175    }
176}
177
178/// Why a packet failed [`SrtpSession::unprotect`].
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180#[non_exhaustive]
181pub enum SrtpError {
182    /// Too short, or not an RTP version 2 header.
183    Malformed,
184    /// The authentication tag does not match: altered, or keyed differently.
185    AuthenticationFailed,
186    /// This packet was already received, or is too old to tell.
187    Replayed,
188}
189
190impl fmt::Display for SrtpError {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.write_str(match self {
193            Self::Malformed => "malformed SRTP packet",
194            Self::AuthenticationFailed => "SRTP authentication failed",
195            Self::Replayed => "replayed SRTP packet",
196        })
197    }
198}
199
200impl std::error::Error for SrtpError {}
201
202/// Keys derived from a master key (RFC 3711 §4.3, key derivation rate 0).
203struct SessionKeys {
204    cipher_key: [u8; MASTER_KEY_LEN],
205    salt: [u8; MASTER_SALT_LEN],
206    auth_key: [u8; AUTH_KEY_LEN],
207}
208
209impl SessionKeys {
210    fn derive(master: &MasterKey) -> Self {
211        Self {
212            cipher_key: derive(master, 0x00),
213            auth_key: derive(master, 0x01),
214            salt: derive(master, 0x02),
215        }
216    }
217}
218
219/// The AES-CM PRF over `x = label XOR master salt`, index 0: the first `N`
220/// bytes of the keystream whose block `i` is `E(key, x * 2^16 + i)`.
221fn derive<const N: usize>(master: &MasterKey, label: u8) -> [u8; N] {
222    let cipher = Aes128::new(&master.key.into());
223    let blocks: Vec<[u8; 16]> = (0..N.div_ceil(16))
224        .map(|i| {
225            let counter: [u8; 16] = std::array::from_fn(|j| match j {
226                7 => master.salt[j] ^ label,
227                j if j < MASTER_SALT_LEN => master.salt[j],
228                14 => (i >> 8) as u8,
229                _ => i as u8,
230            });
231            let mut block = GenericArray::from(counter);
232            cipher.encrypt_block(&mut block);
233            block.into()
234        })
235        .collect();
236    std::array::from_fn(|k| blocks[k / 16][k % 16])
237}
238
239/// Receive-side state of one SSRC.
240#[derive(Default)]
241struct Inbound {
242    roc: u32,
243    highest_seq: u16,
244    /// Newest accepted packet index, and which of the 64 before it arrived.
245    newest: u64,
246    seen: u64,
247}
248
249/// Send-side state of one SSRC.
250#[derive(Default)]
251struct Outbound {
252    roc: u32,
253    last_seq: Option<u16>,
254}
255
256/// One direction of an SRTP stream: protect what you send with your key,
257/// unprotect what you receive with the peer's.
258pub struct SrtpSession {
259    suite: SrtpSuite,
260    keys: SessionKeys,
261    cipher: Aes128,
262    outbound: HashMap<u32, Outbound>,
263    inbound: HashMap<u32, Inbound>,
264}
265
266impl fmt::Debug for SrtpSession {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        f.debug_struct("SrtpSession")
269            .field("suite", &self.suite)
270            .finish_non_exhaustive()
271    }
272}
273
274impl SrtpSession {
275    /// A session keyed by `key`.
276    pub fn new(suite: SrtpSuite, key: &MasterKey) -> Self {
277        let keys = SessionKeys::derive(key);
278        let cipher = Aes128::new(&keys.cipher_key.into());
279        Self {
280            suite,
281            keys,
282            cipher,
283            outbound: HashMap::new(),
284            inbound: HashMap::new(),
285        }
286    }
287
288    /// Encrypt and authenticate an RTP packet. `None` when `rtp` is not an
289    /// RTP packet.
290    pub fn protect(&mut self, rtp: &[u8]) -> Option<Vec<u8>> {
291        let header = header_len(rtp)?;
292        let (seq, ssrc) = seq_ssrc(rtp);
293        let state = self.outbound.entry(ssrc).or_default();
294        if let Some(last) = state.last_seq
295            && seq < last
296            && last - seq > 0x8000
297        {
298            state.roc = state.roc.wrapping_add(1);
299        }
300        state.last_seq = Some(seq);
301        let roc = state.roc;
302        let index = (u64::from(roc) << 16) | u64::from(seq);
303
304        let mut packet = rtp.to_vec();
305        self.keystream(ssrc, index, &mut packet[header..]);
306        let tag = self.tag(&packet, roc);
307        packet.extend_from_slice(&tag[..self.suite.tag_len()]);
308        Some(packet)
309    }
310
311    /// Authenticate and decrypt an SRTP packet back to RTP.
312    pub fn unprotect(&mut self, srtp: &[u8]) -> Result<Vec<u8>, SrtpError> {
313        let tag_len = self.suite.tag_len();
314        if srtp.len() < tag_len {
315            return Err(SrtpError::Malformed);
316        }
317        let (authenticated, tag) = srtp.split_at(srtp.len() - tag_len);
318        let header = header_len(authenticated).ok_or(SrtpError::Malformed)?;
319        let (seq, ssrc) = seq_ssrc(authenticated);
320
321        let (roc, index) = match self.inbound.get(&ssrc) {
322            Some(state) => {
323                let roc = estimate_roc(state.roc, state.highest_seq, seq);
324                let index = (u64::from(roc) << 16) | u64::from(seq);
325                if is_replay(state, index) {
326                    return Err(SrtpError::Replayed);
327                }
328                (roc, index)
329            }
330            None => (0, u64::from(seq)),
331        };
332
333        let mut mac =
334            <HmacSha1 as Mac>::new_from_slice(&self.keys.auth_key).expect("any key length");
335        mac.update(authenticated);
336        mac.update(&roc.to_be_bytes());
337        mac.verify_truncated_left(tag)
338            .map_err(|_| SrtpError::AuthenticationFailed)?;
339
340        let mut packet = authenticated.to_vec();
341        self.keystream(ssrc, index, &mut packet[header..]);
342        self.accept(ssrc, roc, seq, index);
343        Ok(packet)
344    }
345
346    /// Record an authenticated packet: advance the rollover counter and the
347    /// replay window.
348    fn accept(&mut self, ssrc: u32, roc: u32, seq: u16, index: u64) {
349        let state = self.inbound.entry(ssrc).or_insert_with(|| Inbound {
350            roc,
351            highest_seq: seq,
352            newest: index,
353            seen: 0,
354        });
355        if index > state.newest {
356            let shift = index - state.newest;
357            state.seen = if shift >= REPLAY_WINDOW {
358                0
359            } else {
360                state.seen << shift
361            };
362            state.newest = index;
363            state.roc = roc;
364            state.highest_seq = seq;
365        }
366        let behind = state.newest - index;
367        state.seen |= 1 << behind;
368    }
369
370    /// XOR the AES-CM keystream for packet `index` of `ssrc` into `data`.
371    fn keystream(&self, ssrc: u32, index: u64, data: &mut [u8]) {
372        let mut counter = [0u8; 16];
373        counter[..MASTER_SALT_LEN].copy_from_slice(&self.keys.salt);
374        for (i, b) in ssrc.to_be_bytes().iter().enumerate() {
375            counter[4 + i] ^= b;
376        }
377        for (i, b) in index.to_be_bytes()[2..].iter().enumerate() {
378            counter[8 + i] ^= b;
379        }
380        for chunk in data.chunks_mut(16) {
381            let mut block = GenericArray::from(counter);
382            self.cipher.encrypt_block(&mut block);
383            for (d, k) in chunk.iter_mut().zip(block.iter()) {
384                *d ^= k;
385            }
386            let next = u16::from_be_bytes([counter[14], counter[15]]).wrapping_add(1);
387            counter[14..].copy_from_slice(&next.to_be_bytes());
388        }
389    }
390
391    fn tag(&self, authenticated: &[u8], roc: u32) -> [u8; 20] {
392        let mut mac =
393            <HmacSha1 as Mac>::new_from_slice(&self.keys.auth_key).expect("any key length");
394        mac.update(authenticated);
395        mac.update(&roc.to_be_bytes());
396        mac.finalize().into_bytes().into()
397    }
398}
399
400/// The RTP header's length, including CSRCs and an extension.
401fn header_len(packet: &[u8]) -> Option<usize> {
402    if packet.len() < 12 || packet[0] >> 6 != 2 {
403        return None;
404    }
405    let mut len = 12 + usize::from(packet[0] & 0x0F) * 4;
406    if packet[0] & 0x10 != 0 {
407        let words = packet.get(len + 2..len + 4)?;
408        len += 4 + usize::from(u16::from_be_bytes([words[0], words[1]])) * 4;
409    }
410    (len <= packet.len()).then_some(len)
411}
412
413fn seq_ssrc(packet: &[u8]) -> (u16, u32) {
414    (
415        u16::from_be_bytes([packet[2], packet[3]]),
416        u32::from_be_bytes([packet[8], packet[9], packet[10], packet[11]]),
417    )
418}
419
420/// The rollover counter a packet with `seq` most likely belongs to
421/// (RFC 3711 §3.3.1).
422fn estimate_roc(roc: u32, highest: u16, seq: u16) -> u32 {
423    if highest < 0x8000 {
424        if seq > highest && seq - highest > 0x8000 {
425            roc.wrapping_sub(1)
426        } else {
427            roc
428        }
429    } else if highest - 0x8000 > seq {
430        roc.wrapping_add(1)
431    } else {
432        roc
433    }
434}
435
436fn is_replay(state: &Inbound, index: u64) -> bool {
437    if index > state.newest {
438        return false;
439    }
440    let behind = state.newest - index;
441    behind >= REPLAY_WINDOW || state.seen & (1 << behind) != 0
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    fn hex(s: &str) -> Vec<u8> {
449        let s: String = s.split_whitespace().collect();
450        (0..s.len())
451            .step_by(2)
452            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
453            .collect()
454    }
455
456    fn rfc_master() -> MasterKey {
457        let bytes = hex("E1F97A0D3E018BE0D64FA32C06DE4139 0EC675AD498AFEEBB6960B3AABE6");
458        MasterKey::from_bytes(&bytes).unwrap()
459    }
460
461    /// RFC 3711 Appendix B.3.
462    #[test]
463    fn key_derivation_matches_rfc_3711() {
464        let keys = SessionKeys::derive(&rfc_master());
465        assert_eq!(
466            keys.cipher_key.to_vec(),
467            hex("C61E7A93744F39EE10734AFE3FF7A087")
468        );
469        assert_eq!(keys.salt.to_vec(), hex("30CBBC08863D8C85D49DB34A9AE1"));
470        assert_eq!(
471            keys.auth_key.to_vec(),
472            hex("CEBE321F6FF7716B6FD4AB49AF256A156D38BAA4")
473        );
474    }
475
476    /// RFC 3711 Appendix B.2: the AES-CM keystream for SSRC 0, index 0.
477    #[test]
478    fn keystream_matches_rfc_3711() {
479        let key: [u8; 16] = hex("2B7E151628AED2A6ABF7158809CF4F3C").try_into().unwrap();
480        let salt: [u8; 14] = hex("F0F1F2F3F4F5F6F7F8F9FAFBFCFD").try_into().unwrap();
481        let session = SrtpSession {
482            suite: SrtpSuite::AesCm128HmacSha1_80,
483            keys: SessionKeys {
484                cipher_key: key,
485                salt,
486                auth_key: [0; AUTH_KEY_LEN],
487            },
488            cipher: Aes128::new(&key.into()),
489            outbound: HashMap::new(),
490            inbound: HashMap::new(),
491        };
492        let mut stream = [0u8; 48];
493        session.keystream(0, 0, &mut stream);
494        assert_eq!(
495            stream.to_vec(),
496            hex("E03EAD0935C95E80E166B16DD92B4EB4 \
497                 D23513162B02D0F72A43A2FE4A5F97AB \
498                 41E95B3BB0A2E8DD477901E4FCA894C0")
499        );
500    }
501
502    /// The reference packet of libsrtp's test driver: SSRC 0xCAFEBABE,
503    /// sequence 0x1234, sixteen bytes of 0xAB.
504    #[test]
505    fn protects_the_reference_packet() {
506        let plain = hex("800f1234 decafbad cafebabe abababab abababab abababab abababab");
507        let expected = hex(
508            "800f1234 decafbad cafebabe 4e55dc4c e79978d8 8ca4d215 949d2402 \
509             b78d6acc 99ea179b 8dbb",
510        );
511        let mut sender = SrtpSession::new(SrtpSuite::AesCm128HmacSha1_80, &rfc_master());
512        assert_eq!(sender.protect(&plain).unwrap(), expected);
513
514        let mut receiver = SrtpSession::new(SrtpSuite::AesCm128HmacSha1_80, &rfc_master());
515        assert_eq!(receiver.unprotect(&expected).unwrap(), plain);
516    }
517
518    fn rtp(seq: u16, payload: &[u8]) -> Vec<u8> {
519        let mut packet = vec![0x80, 0x00];
520        packet.extend_from_slice(&seq.to_be_bytes());
521        packet.extend_from_slice(&[0, 0, 0, 160]);
522        packet.extend_from_slice(&0x1234_5678u32.to_be_bytes());
523        packet.extend_from_slice(payload);
524        packet
525    }
526
527    #[test]
528    fn a_tampered_or_replayed_packet_is_refused() {
529        let key = MasterKey::generate().unwrap();
530        for suite in [
531            SrtpSuite::AesCm128HmacSha1_80,
532            SrtpSuite::AesCm128HmacSha1_32,
533        ] {
534            let mut tx = SrtpSession::new(suite, &key);
535            let mut rx = SrtpSession::new(suite, &key);
536            let packet = tx.protect(&rtp(7, b"hello")).unwrap();
537            assert_eq!(packet.len(), 12 + 5 + suite.tag_len());
538            assert_ne!(&packet[12..17], b"hello", "the payload is encrypted");
539
540            let mut tampered = packet.clone();
541            tampered[13] ^= 1;
542            assert_eq!(
543                rx.unprotect(&tampered),
544                Err(SrtpError::AuthenticationFailed)
545            );
546
547            assert_eq!(rx.unprotect(&packet).unwrap(), rtp(7, b"hello"));
548            assert_eq!(rx.unprotect(&packet), Err(SrtpError::Replayed));
549
550            let other =
551                SrtpSession::new(suite, &MasterKey::generate().unwrap()).protect(&rtp(8, b"x"));
552            assert_eq!(
553                rx.unprotect(&other.unwrap()),
554                Err(SrtpError::AuthenticationFailed),
555                "a packet under another key"
556            );
557        }
558    }
559
560    #[test]
561    fn the_rollover_counter_follows_a_sequence_wrap() {
562        let key = MasterKey::generate().unwrap();
563        let suite = SrtpSuite::AesCm128HmacSha1_80;
564        let mut tx = SrtpSession::new(suite, &key);
565        let mut rx = SrtpSession::new(suite, &key);
566        let mut seq: u16 = 65_530;
567        for n in 0..20u8 {
568            let packet = tx.protect(&rtp(seq, &[n; 4])).unwrap();
569            assert_eq!(
570                rx.unprotect(&packet).unwrap(),
571                rtp(seq, &[n; 4]),
572                "seq {seq}"
573            );
574            seq = seq.wrapping_add(1);
575        }
576        assert_eq!(tx.outbound[&0x1234_5678].roc, 1);
577        assert_eq!(rx.inbound[&0x1234_5678].roc, 1);
578
579        // A packet from before the wrap that arrives after it still
580        // authenticates, under the previous rollover count.
581        let mut tx = SrtpSession::new(suite, &key);
582        let mut rx = SrtpSession::new(suite, &key);
583        let early = tx.protect(&rtp(65_534, b"a")).unwrap();
584        let late = tx.protect(&rtp(65_535, b"b")).unwrap();
585        let wrapped = tx.protect(&rtp(0, b"c")).unwrap();
586        assert!(rx.unprotect(&early).is_ok());
587        assert!(rx.unprotect(&wrapped).is_ok());
588        assert_eq!(rx.unprotect(&late).unwrap(), rtp(65_535, b"b"));
589        assert_eq!(rx.unprotect(&late), Err(SrtpError::Replayed));
590    }
591
592    #[test]
593    fn a_crypto_line_round_trips() {
594        let line = "1 AES_CM_128_HMAC_SHA1_80 inline:4fl6DT4Bi+DWT6MsBt5BOQ7Gda1Jiv7rtpYLOqvm|2^20";
595        let attribute = CryptoAttribute::parse(line).unwrap();
596        assert_eq!(attribute.tag, 1);
597        assert_eq!(attribute.suite, SrtpSuite::AesCm128HmacSha1_80);
598        assert_eq!(attribute.key, rfc_master());
599        assert_eq!(
600            CryptoAttribute::parse(&attribute.to_value()).unwrap(),
601            attribute
602        );
603        assert!(
604            !format!("{attribute:?}").contains("4fl6DT4B"),
605            "keys never print"
606        );
607
608        assert_eq!(
609            CryptoAttribute::parse(
610                "1 F8_128_HMAC_SHA1_80 inline:4fl6DT4Bi+DWT6MsBt5BOQ7Gda1Jiv7rtpYLOqvm"
611            ),
612            None
613        );
614        assert_eq!(
615            CryptoAttribute::parse(
616                "1 AES_CM_128_HMAC_SHA1_80 inline:4fl6DT4Bi+DWT6MsBt5BOQ7Gda1Jiv7rtpYLOqvm|2^20|1:4"
617            ),
618            None,
619            "an MKI"
620        );
621        assert_eq!(
622            CryptoAttribute::parse(
623                "1 AES_CM_128_HMAC_SHA1_80 inline:4fl6DT4Bi+DWT6MsBt5BOQ7Gda1Jiv7rtpYLOqvm KDR=1"
624            ),
625            None,
626            "a session parameter"
627        );
628    }
629}