dimpl 0.7.4

DTLS 1.2/1.3 implementation (Sans‑IO, Sync)
Documentation
#![cfg(any(feature = "aws-lc-rs", feature = "rust-crypto"))]

#[path = "dtls13/common.rs"]
mod common;

#[path = "ossl/mod.rs"]
mod ossl_helper;

#[cfg(not(windows))]
#[path = "wolfssl/mod.rs"]
mod wolfssl_helper;

use std::sync::Arc;
use std::time::Instant;

use dimpl::{Dtls, DtlsCertificate};

use crate::common::{drain_outputs, dtls13_config};
use crate::ossl_helper::{DtlsCertOptions, OsslDtlsCert};

fn certificate() -> DtlsCertificate {
    let cert = OsslDtlsCert::new(DtlsCertOptions::default());
    DtlsCertificate {
        certificate: cert.x509.to_der().expect("certificate DER"),
        private_key: cert.pkey.private_key_to_pkcs8().expect("private key DER"),
    }
}

fn cookie_extensions_start(body: &[u8], msg_type: u8) -> Option<usize> {
    let mut pos = 0;
    match msg_type {
        0x01 => {
            pos += 2 + 32;
            let sid_len = *body.get(pos)? as usize;
            pos += 1 + sid_len;
            let cookie_len = *body.get(pos)? as usize;
            pos += 1 + cookie_len;
            let suites_len = u16::from_be_bytes([*body.get(pos)?, *body.get(pos + 1)?]) as usize;
            pos += 2 + suites_len;
            let compression_len = *body.get(pos)? as usize;
            pos += 1 + compression_len;
        }
        0x02 => {
            pos += 2 + 32;
            let sid_len = *body.get(pos)? as usize;
            pos += 1 + sid_len + 2 + 1;
        }
        _ => return None,
    }

    Some(pos)
}

fn shrink_dtls13_cookie_extension_inner_len(packet: &mut [u8]) -> bool {
    const RECORD_HEADER_LEN: usize = 13;
    const HANDSHAKE_HEADER_LEN: usize = 12;
    const COOKIE_EXTENSION: u16 = 0x002C;

    if packet.len() < RECORD_HEADER_LEN + HANDSHAKE_HEADER_LEN || packet[0] != 22 {
        return false;
    }

    let handshake = &mut packet[RECORD_HEADER_LEN..];
    let msg_type = handshake[0];
    let body_len =
        ((handshake[1] as usize) << 16) | ((handshake[2] as usize) << 8) | handshake[3] as usize;
    if handshake.len() < HANDSHAKE_HEADER_LEN + body_len {
        return false;
    }

    let body = &mut handshake[HANDSHAKE_HEADER_LEN..HANDSHAKE_HEADER_LEN + body_len];
    let mut pos = match cookie_extensions_start(body, msg_type) {
        Some(pos) => pos,
        None => return false,
    };
    if body.len() < pos + 2 {
        return false;
    }

    let extensions_len = u16::from_be_bytes([body[pos], body[pos + 1]]) as usize;
    pos += 2;
    let extensions_end = pos + extensions_len;
    if body.len() < extensions_end {
        return false;
    }

    while pos + 4 <= extensions_end {
        let extension_type = u16::from_be_bytes([body[pos], body[pos + 1]]);
        let extension_len = u16::from_be_bytes([body[pos + 2], body[pos + 3]]) as usize;
        let extension_body = pos + 4;
        let next = extension_body + extension_len;
        if next > extensions_end {
            return false;
        }

        if extension_type == COOKIE_EXTENSION && extension_len > 2 {
            let cookie_len = u16::from_be_bytes([body[extension_body], body[extension_body + 1]]);
            if cookie_len == 0 {
                return false;
            }
            body[extension_body..extension_body + 2]
                .copy_from_slice(&(cookie_len - 1).to_be_bytes());
            return true;
        }

        pos = next;
    }

    false
}

#[test]
fn dtls13_client_rejects_hrr_cookie_extension_trailing_bytes() {
    let _ = env_logger::try_init();

    let client_cert = certificate();
    let server_cert = certificate();
    let config = dtls13_config();
    let now = Instant::now();

    let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
    client.set_active(true);

    let mut server = Dtls::new_13(config, server_cert, now);
    server.set_active(false);

    client.handle_timeout(now).expect("client timeout");
    let client_out = drain_outputs(&mut client);
    assert!(!client_out.packets.is_empty(), "client should send CH1");
    for packet in &client_out.packets {
        server.handle_packet(packet).expect("server receives CH1");
    }

    server.handle_timeout(now).expect("server timeout");
    let server_out = drain_outputs(&mut server);
    let mut hrr = server_out
        .packets
        .into_iter()
        .next()
        .expect("server should emit HRR");
    assert!(
        shrink_dtls13_cookie_extension_inner_len(&mut hrr),
        "fixture should contain a Cookie extension"
    );

    client
        .handle_packet(&hrr)
        .expect("malformed HRR Cookie extension should be discarded");

    client
        .handle_timeout(now)
        .expect("client timeout after error");
    let client_out = drain_outputs(&mut client);
    assert!(
        client_out.packets.is_empty(),
        "client must not send CH2 after malformed HRR Cookie"
    );
}

#[test]
fn dtls13_server_rejects_clienthello_cookie_extension_trailing_bytes() {
    let _ = env_logger::try_init();

    let client_cert = certificate();
    let server_cert = certificate();
    let config = dtls13_config();
    let now = Instant::now();

    let mut client = Dtls::new_13(Arc::clone(&config), client_cert, now);
    client.set_active(true);

    let mut server = Dtls::new_13(config, server_cert, now);
    server.set_active(false);

    client.handle_timeout(now).expect("client timeout");
    let client_out = drain_outputs(&mut client);
    for packet in &client_out.packets {
        server.handle_packet(packet).expect("server receives CH1");
    }

    server.handle_timeout(now).expect("server timeout");
    let server_out = drain_outputs(&mut server);
    let hrr = server_out
        .packets
        .first()
        .expect("server should emit HRR")
        .clone();
    client
        .handle_packet(&hrr)
        .expect("client receives valid HRR");

    client
        .handle_timeout(now)
        .expect("client timeout after HRR");
    let client_out = drain_outputs(&mut client);
    let mut ch2 = client_out
        .packets
        .into_iter()
        .next()
        .expect("client should emit CH2 with cookie");
    assert!(
        shrink_dtls13_cookie_extension_inner_len(&mut ch2),
        "fixture should contain a Cookie extension"
    );

    server
        .handle_packet(&ch2)
        .expect("malformed ClientHello Cookie extension should be discarded");
}

#[test]
fn dtls13_cookie_only_retry_preserves_key_share() {
    const COOKIE: u16 = 44;
    const KEY_SHARE: u16 = 51;

    let config = dtls13_config();
    let now = Instant::now();
    let mut client = Dtls::new_13(Arc::clone(&config), certificate(), now);
    client.set_active(true);
    let mut server = Dtls::new_13(config, certificate(), now);
    server.set_active(false);
    server.handle_timeout(now).expect("start server");
    assert!(drain_outputs(&mut server).packets.is_empty());

    client.handle_timeout(now).expect("send CH1");
    let ch1 = drain_outputs(&mut client).packets;
    assert_eq!(ch1.len(), 1);
    let key_share = hello_extension(&ch1[0], 1, KEY_SHARE).expect("CH1 key_share");

    server.handle_packet(&ch1[0]).expect("receive CH1");
    let hrr = drain_outputs(&mut server).packets;
    assert_eq!(hrr.len(), 1);
    let cookie = hello_extension(&hrr[0], 2, COOKIE).expect("HRR cookie");
    assert!(
        hello_extension(&hrr[0], 2, KEY_SHARE).is_none(),
        "cookie-only HRR"
    );

    client.handle_packet(&hrr[0]).expect("receive HRR");
    let ch2 = drain_outputs(&mut client).packets;
    assert_eq!(ch2.len(), 1);
    assert_eq!(hello_extension(&ch2[0], 1, COOKIE), Some(cookie));
    assert_eq!(hello_extension(&ch2[0], 1, KEY_SHARE), Some(key_share));
}

fn hello_extension(packet: &[u8], message_type: u8, extension_type: u16) -> Option<&[u8]> {
    assert_eq!(packet[0], 22);
    assert_eq!(&packet[3..5], &[0, 0]);
    assert_eq!(packet[13], message_type);
    assert_eq!(&packet[19..22], &[0, 0, 0]);
    assert_eq!(&packet[14..17], &packet[22..25]);
    let body = &packet[25..];
    let start = cookie_extensions_start(body, message_type).expect("hello extensions");
    let length = u16::from_be_bytes([body[start], body[start + 1]]) as usize;
    let mut extensions = &body[start + 2..start + 2 + length];
    while !extensions.is_empty() {
        let kind = u16::from_be_bytes([extensions[0], extensions[1]]);
        let length = u16::from_be_bytes([extensions[2], extensions[3]]) as usize;
        if kind == extension_type {
            return Some(&extensions[4..4 + length]);
        }
        extensions = &extensions[4 + length..];
    }
    None
}

#[test]
#[cfg(not(windows))]
fn dtls13_wolfssl_cookie_only_retry_preserves_key_share() {
    use std::collections::VecDeque;
    use std::time::Duration;

    use crate::wolfssl_helper::WolfDtlsCert;

    const COOKIE: u16 = 44;
    const KEY_SHARE: u16 = 51;

    let server_cert = certificate();
    let wolf_cert = WolfDtlsCert::new(server_cert.certificate, server_cert.private_key);
    let mut server = wolf_cert.new_dtls13_impl(true).expect("wolfSSL server");
    let mut events = VecDeque::new();
    let mut now = Instant::now();
    let mut client = Dtls::new_13(dtls13_config(), certificate(), now);
    client.set_active(true);

    client.handle_timeout(now).expect("send CH1");
    let ch1 = drain_outputs(&mut client).packets;
    assert_eq!(ch1.len(), 1);
    let key_share = hello_extension(&ch1[0], 1, KEY_SHARE).expect("CH1 key_share");
    server
        .handle_receive(&ch1[0], &mut events)
        .expect("receive CH1");

    let hrr = server.poll_datagram().expect("wolfSSL HRR");
    let cookie = hello_extension(&hrr, 2, COOKIE).expect("HRR cookie");
    assert!(
        hello_extension(&hrr, 2, KEY_SHARE).is_none(),
        "cookie-only HRR"
    );
    client.handle_packet(&hrr).expect("receive HRR");
    let ch2 = drain_outputs(&mut client).packets;
    assert_eq!(ch2.len(), 1);
    assert_eq!(hello_extension(&ch2[0], 1, COOKIE), Some(cookie));
    assert_eq!(hello_extension(&ch2[0], 1, KEY_SHARE), Some(key_share));
    server
        .handle_receive(&ch2[0], &mut events)
        .expect("receive CH2");

    let mut connected = false;
    for _ in 0..50 {
        while let Some(packet) = server.poll_datagram() {
            client
                .handle_packet(&packet)
                .expect("receive wolfSSL flight");
            let out = drain_outputs(&mut client);
            connected |= out.connected;
            for packet in out.packets {
                server
                    .handle_receive(&packet, &mut events)
                    .expect("receive client flight");
            }
        }
        if connected && server.is_connected() {
            break;
        }
        now += Duration::from_millis(10);
        client.handle_timeout(now).expect("client timeout");
        let out = drain_outputs(&mut client);
        connected |= out.connected;
        for packet in out.packets {
            server
                .handle_receive(&packet, &mut events)
                .expect("receive client retry");
        }
    }
    assert!(connected, "dimpl client connected");
    assert!(server.is_connected(), "wolfSSL server connected");

    let payload = b"cookie retry interop";
    server.write(payload).expect("wolfSSL write");
    let mut received = Vec::new();
    while let Some(packet) = server.poll_datagram() {
        client
            .handle_packet(&packet)
            .expect("receive encrypted data");
        let out = drain_outputs(&mut client);
        for data in out.app_data {
            received.extend_from_slice(&data);
        }
        for packet in out.packets {
            server
                .handle_receive(&packet, &mut events)
                .expect("receive client ACK");
        }
    }
    assert_eq!(received, payload);
}