str0m 0.18.0

WebRTC library in Sans-IO style
Documentation
//! Tests for DTLS handshake edge cases and security.

use std::net::Ipv4Addr;
use std::time::{Duration, Instant};

use str0m::Rtc;
use str0m::RtcError;

mod common;
use common::{Peer, TestRtc, init_crypto_default, init_log, progress};

/// Test certificate fingerprint format and uniqueness.
#[test]
fn dtls_certificate_fingerprint_format() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    // Create two instances and verify they have different certificates
    let mut l = TestRtc::new(Peer::Left);
    let mut r = TestRtc::new(Peer::Right);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    // Get fingerprints
    let finger_l = l.direct_api().local_dtls_fingerprint().clone();
    let finger_r = r.direct_api().local_dtls_fingerprint().clone();

    // Fingerprints should be different for different instances
    assert_ne!(
        finger_l, finger_r,
        "Different Rtc instances should have different certificates"
    );

    // Fingerprint should be properly formatted (SHA-256 format)
    let finger_str = format!("{}", finger_l);
    assert!(
        finger_str.starts_with("sha-256 ") || finger_str.contains(':'),
        "Fingerprint should be in SHA-256 format: {}",
        finger_str
    );

    Ok(())
}

/// Test that new Rtc instances get new certificates.
#[test]
fn dtls_certificate_rotation() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    // Create first connection using L/R peers (respects L_CRYPTO/R_CRYPTO env vars)
    let mut l1 = TestRtc::new(Peer::Left);
    let mut r1 = TestRtc::new(Peer::Right);

    let finger1_l = l1.direct_api().local_dtls_fingerprint().clone();
    let finger1_r = r1.direct_api().local_dtls_fingerprint().clone();

    // Create second connection with new certificates
    let mut l2 = TestRtc::new(Peer::Left);
    let mut r2 = TestRtc::new(Peer::Right);

    let finger2_l = l2.direct_api().local_dtls_fingerprint().clone();
    let finger2_r = r2.direct_api().local_dtls_fingerprint().clone();

    // All fingerprints should be unique
    assert_ne!(finger1_l, finger1_r, "L1 and R1 should differ");
    assert_ne!(finger2_l, finger2_r, "L2 and R2 should differ");
    assert_ne!(finger1_l, finger2_l, "L1 and L2 should differ");
    assert_ne!(finger1_r, finger2_r, "R1 and R2 should differ");

    Ok(())
}

/// Test DTLS connection with ice-lite mode (passive role).
#[test]
fn dtls_with_ice_lite() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    let mut l = TestRtc::new(Peer::Left);

    // R is ice-lite (typically server-side), but still use Peer::Right crypto provider
    let mut rtc_r_builder = Rtc::builder().set_ice_lite(true);
    if let Some(crypto) = Peer::Right.crypto_provider() {
        rtc_r_builder = rtc_r_builder.set_crypto_provider(crypto);
    }
    let mut r = TestRtc::new_with_rtc(Peer::Right.span(), rtc_r_builder.build(Instant::now()));

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let (offer, pending) = l.span.in_scope(|| {
        let mut change = l.rtc.sdp_api();
        let _ = change.add_channel("test".into());
        change.apply().unwrap()
    });

    let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
    l.span
        .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        if l.duration() > Duration::from_secs(5) {
            panic!("Failed to connect with ice-lite");
        }
        progress(&mut l, &mut r)?;
    }

    Ok(())
}

/// Test pregenerated DTLS certificate.
#[test]
fn dtls_pregenerated_certificate() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    // Generate a certificate using Peer::Left's crypto provider if set
    let provider = Peer::Left
        .crypto_provider()
        .unwrap_or_else(|| std::sync::Arc::new(str0m::crypto::from_feature_flags()));
    let cert = provider.dtls_provider.generate_certificate().unwrap();

    // Use the pregenerated certificate with the same crypto provider
    let mut rtc_l_builder = Rtc::builder().set_dtls_cert(cert);
    if let Some(crypto) = Peer::Left.crypto_provider() {
        rtc_l_builder = rtc_l_builder.set_crypto_provider(crypto);
    }

    let mut l = TestRtc::new_with_rtc(Peer::Left.span(), rtc_l_builder.build(Instant::now()));
    let mut r = TestRtc::new(Peer::Right);

    l.add_host_candidate((Ipv4Addr::new(1, 1, 1, 1), 1000).into());
    r.add_host_candidate((Ipv4Addr::new(2, 2, 2, 2), 2000).into());

    let (offer, pending) = l.span.in_scope(|| {
        let mut change = l.rtc.sdp_api();
        let _ = change.add_channel("test".into());
        change.apply().unwrap()
    });

    let answer = r.span.in_scope(|| r.rtc.sdp_api().accept_offer(offer))?;
    l.span
        .in_scope(|| l.rtc.sdp_api().accept_answer(pending, answer))?;

    loop {
        if l.is_connected() && r.is_connected() {
            break;
        }
        if l.duration() > Duration::from_secs(5) {
            panic!("Failed to connect with pregenerated certificate");
        }
        progress(&mut l, &mut r)?;
    }

    Ok(())
}

/// Test that same pregenerated certificate produces same fingerprint.
#[test]
fn dtls_pregenerated_certificate_same_fingerprint() -> Result<(), RtcError> {
    init_log();
    init_crypto_default();

    // Generate a certificate using Peer::Left's crypto provider if set
    let provider = Peer::Left
        .crypto_provider()
        .unwrap_or_else(|| std::sync::Arc::new(str0m::crypto::from_feature_flags()));
    let cert = provider.dtls_provider.generate_certificate().unwrap();
    let cert_clone = cert.clone();

    // Create two instances with the same certificate
    let mut rtc1_builder = Rtc::builder().set_dtls_cert(cert);
    let mut rtc2_builder = Rtc::builder().set_dtls_cert(cert_clone);
    if let Some(crypto) = Peer::Left.crypto_provider() {
        rtc1_builder = rtc1_builder.set_crypto_provider(crypto.clone());
        rtc2_builder = rtc2_builder.set_crypto_provider(crypto);
    }

    let mut l1 = TestRtc::new_with_rtc(Peer::Left.span(), rtc1_builder.build(Instant::now()));
    let mut l2 = TestRtc::new_with_rtc(Peer::Left.span(), rtc2_builder.build(Instant::now()));

    let finger1 = l1.direct_api().local_dtls_fingerprint().clone();
    let finger2 = l2.direct_api().local_dtls_fingerprint().clone();

    // Same certificate should produce same fingerprint
    assert_eq!(
        finger1, finger2,
        "Same certificate should produce same fingerprint"
    );

    Ok(())
}