File
Blob: firmware/vendor/str0m/tests/dtls-security.rs
| 1 | //! Tests for DTLS handshake edge cases and security. |
| 2 | |
| 3 | use std::net::Ipv4Addr; |
| 4 | use std::time::{Duration, Instant}; |
| 5 | |
| 6 | use str0m::Rtc; |
| 7 | use str0m::RtcError; |
| 8 | |
| 9 | mod common; |
| 10 | use common::{Peer, TestRtc, init_crypto_default, init_log, progress}; |
| 11 | |
| 12 | /// Test certificate fingerprint format and uniqueness. |
| 13 | #[test] |
| 14 | fn dtls_certificate_fingerprint_format() -> Result<(), RtcError> { |
| 15 | init_log(); |
| 16 | init_crypto_default(); |
| 17 | |
| 18 | // Create two instances and verify they have different certificates |
| 19 | let mut l = TestRtc::new(Peer::Left); |
| 20 | let mut r = TestRtc::new(Peer::Right); |
| 21 | |
| 22 | l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); |
| 23 | r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); |
| 24 | |
| 25 | // Get fingerprints |
| 26 | let finger_l = l.direct_api().local_dtls_fingerprint().clone(); |
| 27 | let finger_r = r.direct_api().local_dtls_fingerprint().clone(); |
| 28 | |
| 29 | // Fingerprints should be different for different instances |
| 30 | assert_ne!( |
| 31 | finger_l, finger_r, |
| 32 | "Different Rtc instances should have different certificates" |
| 33 | ); |
| 34 | |
| 35 | // Fingerprint should be properly formatted (SHA-256 format) |
| 36 | let finger_str = format!("{}", finger_l); |
| 37 | assert!( |
| 38 | finger_str.starts_with("sha-256 ") || finger_str.contains(':'), |
| 39 | "Fingerprint should be in SHA-256 format: {}", |
| 40 | finger_str |
| 41 | ); |
| 42 | |
| 43 | Ok(()) |
| 44 | } |
| 45 | |
| 46 | /// Test that new Rtc instances get new certificates. |
| 47 | #[test] |
| 48 | fn dtls_certificate_rotation() -> Result<(), RtcError> { |
| 49 | init_log(); |
| 50 | init_crypto_default(); |
| 51 | |
| 52 | // Create first connection using L/R peers (respects L_CRYPTO/R_CRYPTO env vars) |
| 53 | let mut l1 = TestRtc::new(Peer::Left); |
| 54 | let mut r1 = TestRtc::new(Peer::Right); |
| 55 | |
| 56 | let finger1_l = l1.direct_api().local_dtls_fingerprint().clone(); |
| 57 | let finger1_r = r1.direct_api().local_dtls_fingerprint().clone(); |
| 58 | |
| 59 | // Create second connection with new certificates |
| 60 | let mut l2 = TestRtc::new(Peer::Left); |
| 61 | let mut r2 = TestRtc::new(Peer::Right); |
| 62 | |
| 63 | let finger2_l = l2.direct_api().local_dtls_fingerprint().clone(); |
| 64 | let finger2_r = r2.direct_api().local_dtls_fingerprint().clone(); |
| 65 | |
| 66 | // All fingerprints should be unique |
| 67 | assert_ne!(finger1_l, finger1_r, "L1 and R1 should differ"); |
| 68 | assert_ne!(finger2_l, finger2_r, "L2 and R2 should differ"); |
| 69 | assert_ne!(finger1_l, finger2_l, "L1 and L2 should differ"); |
| 70 | assert_ne!(finger1_r, finger2_r, "R1 and R2 should differ"); |
| 71 | |
| 72 | Ok(()) |
| 73 | } |
| 74 | |
| 75 | /// Test DTLS connection with ice-lite mode (passive role). |
| 76 | #[test] |
| 77 | fn dtls_with_ice_lite() -> Result<(), RtcError> { |
| 78 | init_log(); |
| 79 | init_crypto_default(); |
| 80 | |
| 81 | let mut l = TestRtc::new(Peer::Left); |
| 82 | |
| 83 | // R is ice-lite (typically server-side), but still use Peer::Right crypto provider |
| 84 | let mut rtc_r_builder = Rtc::builder().set_ice_lite(true); |
| 85 | if let Some(crypto) = Peer::Right.crypto_provider() { |
| 86 | rtc_r_builder = rtc_r_builder.set_crypto_provider(crypto); |
| 87 | } |
| 88 | let mut r = TestRtc::new_with_rtc(Peer::Right.span(), rtc_r_builder.build(Instant::now())); |
| 89 | |
| 90 | l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); |
| 91 | r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); |
| 92 | |
| 93 | let (offer, pending) = l.span.in_scope(|| { |
| 94 | let mut change = l.rtc.sdp_api(); |
| 95 | let _ = change.add_channel("test".into()); |
| 96 | change.apply().unwrap() |
| 97 | }); |
| 98 | |
| 99 | let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; |
| 100 | l.span |
| 101 | .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; |
| 102 | |
| 103 | loop { |
| 104 | if l.is_connected() && r.is_connected() { |
| 105 | break; |
| 106 | } |
| 107 | if l.duration() > Duration::from_secs(5) { |
| 108 | panic!("Failed to connect with ice-lite"); |
| 109 | } |
| 110 | progress(&mut l, &mut r)?; |
| 111 | } |
| 112 | |
| 113 | Ok(()) |
| 114 | } |
| 115 | |
| 116 | /// Test pregenerated DTLS certificate. |
| 117 | #[test] |
| 118 | fn dtls_pregenerated_certificate() -> Result<(), RtcError> { |
| 119 | init_log(); |
| 120 | init_crypto_default(); |
| 121 | |
| 122 | // Generate a certificate using Peer::Left's crypto provider if set |
| 123 | let provider = Peer::Left |
| 124 | .crypto_provider() |
| 125 | .unwrap_or_else(|| std::sync::Arc::new(str0m::crypto::from_feature_flags())); |
| 126 | let cert = provider.dtls_provider.generate_certificate().unwrap(); |
| 127 | |
| 128 | // Use the pregenerated certificate with the same crypto provider |
| 129 | let mut rtc_l_builder = Rtc::builder().set_dtls_cert(cert); |
| 130 | if let Some(crypto) = Peer::Left.crypto_provider() { |
| 131 | rtc_l_builder = rtc_l_builder.set_crypto_provider(crypto); |
| 132 | } |
| 133 | |
| 134 | let mut l = TestRtc::new_with_rtc(Peer::Left.span(), rtc_l_builder.build(Instant::now())); |
| 135 | let mut r = TestRtc::new(Peer::Right); |
| 136 | |
| 137 | l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into()); |
| 138 | r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into()); |
| 139 | |
| 140 | let (offer, pending) = l.span.in_scope(|| { |
| 141 | let mut change = l.rtc.sdp_api(); |
| 142 | let _ = change.add_channel("test".into()); |
| 143 | change.apply().unwrap() |
| 144 | }); |
| 145 | |
| 146 | let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?; |
| 147 | l.span |
| 148 | .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?; |
| 149 | |
| 150 | loop { |
| 151 | if l.is_connected() && r.is_connected() { |
| 152 | break; |
| 153 | } |
| 154 | if l.duration() > Duration::from_secs(5) { |
| 155 | panic!("Failed to connect with pregenerated certificate"); |
| 156 | } |
| 157 | progress(&mut l, &mut r)?; |
| 158 | } |
| 159 | |
| 160 | Ok(()) |
| 161 | } |
| 162 | |
| 163 | /// Test that same pregenerated certificate produces same fingerprint. |
| 164 | #[test] |
| 165 | fn dtls_pregenerated_certificate_same_fingerprint() -> Result<(), RtcError> { |
| 166 | init_log(); |
| 167 | init_crypto_default(); |
| 168 | |
| 169 | // Generate a certificate using Peer::Left's crypto provider if set |
| 170 | let provider = Peer::Left |
| 171 | .crypto_provider() |
| 172 | .unwrap_or_else(|| std::sync::Arc::new(str0m::crypto::from_feature_flags())); |
| 173 | let cert = provider.dtls_provider.generate_certificate().unwrap(); |
| 174 | let cert_clone = cert.clone(); |
| 175 | |
| 176 | // Create two instances with the same certificate |
| 177 | let mut rtc1_builder = Rtc::builder().set_dtls_cert(cert); |
| 178 | let mut rtc2_builder = Rtc::builder().set_dtls_cert(cert_clone); |
| 179 | if let Some(crypto) = Peer::Left.crypto_provider() { |
| 180 | rtc1_builder = rtc1_builder.set_crypto_provider(crypto.clone()); |
| 181 | rtc2_builder = rtc2_builder.set_crypto_provider(crypto); |
| 182 | } |
| 183 | |
| 184 | let mut l1 = TestRtc::new_with_rtc(Peer::Left.span(), rtc1_builder.build(Instant::now())); |
| 185 | let mut l2 = TestRtc::new_with_rtc(Peer::Left.span(), rtc2_builder.build(Instant::now())); |
| 186 | |
| 187 | let finger1 = l1.direct_api().local_dtls_fingerprint().clone(); |
| 188 | let finger2 = l2.direct_api().local_dtls_fingerprint().clone(); |
| 189 | |
| 190 | // Same certificate should produce same fingerprint |
| 191 | assert_eq!( |
| 192 | finger1, finger2, |
| 193 | "Same certificate should produce same fingerprint" |
| 194 | ); |
| 195 | |
| 196 | Ok(()) |
| 197 | } |