File: ech.rs

package info (click to toggle)
rust-rustls 0.23.26%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 13,816 kB
  • sloc: sh: 199; python: 181; makefile: 23
file content (125 lines) | stat: -rw-r--r-- 4,898 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use base64::prelude::{BASE64_STANDARD, Engine};
use pki_types::DnsName;
use rustls::internal::msgs::codec::{Codec, Reader};
use rustls::internal::msgs::enums::{EchVersion, HpkeAead, HpkeKdf, HpkeKem};
use rustls::internal::msgs::handshake::{
    EchConfigContents, EchConfigPayload, HpkeKeyConfig, HpkeSymmetricCipherSuite,
};

#[test]
fn test_decode_config_list() {
    fn assert_config(contents: &EchConfigContents, public_name: impl AsRef<[u8]>, max_len: u8) {
        assert_eq!(contents.maximum_name_length, max_len);
        assert_eq!(
            contents.public_name,
            DnsName::try_from(public_name.as_ref()).unwrap()
        );
        assert!(contents.extensions.is_empty());
    }

    fn assert_key_config(
        config: &HpkeKeyConfig,
        id: u8,
        kem_id: HpkeKem,
        cipher_suites: Vec<HpkeSymmetricCipherSuite>,
    ) {
        assert_eq!(config.config_id, id);
        assert_eq!(config.kem_id, kem_id);
        assert_eq!(config.symmetric_cipher_suites, cipher_suites);
    }

    let config_list = get_ech_config(BASE64_ECHCONFIG_LIST_LOCALHOST);
    assert_eq!(config_list.len(), 1);
    let EchConfigPayload::V18(contents) = &config_list[0] else {
        panic!("unexpected ECH config version: {:?}", config_list[0]);
    };
    assert_config(contents, "localhost", 128);
    assert_key_config(
        &contents.key_config,
        0,
        HpkeKem::DHKEM_X25519_HKDF_SHA256,
        vec![
            HpkeSymmetricCipherSuite {
                kdf_id: HpkeKdf::HKDF_SHA256,
                aead_id: HpkeAead::AES_128_GCM,
            },
            HpkeSymmetricCipherSuite {
                kdf_id: HpkeKdf::HKDF_SHA256,
                aead_id: HpkeAead::CHACHA20_POLY_1305,
            },
        ],
    );

    let config_list = get_ech_config(BASE64_ECHCONFIG_LIST_CF);
    assert_eq!(config_list.len(), 2);
    let EchConfigPayload::V18(contents_a) = &config_list[0] else {
        panic!("unexpected ECH config version: {:?}", config_list[0]);
    };
    assert_config(contents_a, "cloudflare-esni.com", 37);
    assert_key_config(
        &contents_a.key_config,
        195,
        HpkeKem::DHKEM_X25519_HKDF_SHA256,
        vec![HpkeSymmetricCipherSuite {
            kdf_id: HpkeKdf::HKDF_SHA256,
            aead_id: HpkeAead::AES_128_GCM,
        }],
    );
    let EchConfigPayload::V18(contents_b) = &config_list[1] else {
        panic!("unexpected ECH config version: {:?}", config_list[1]);
    };
    assert_config(contents_b, "cloudflare-esni.com", 42);
    assert_key_config(
        &contents_b.key_config,
        3,
        HpkeKem::DHKEM_P256_HKDF_SHA256,
        vec![HpkeSymmetricCipherSuite {
            kdf_id: HpkeKdf::HKDF_SHA256,
            aead_id: HpkeAead::AES_128_GCM,
        }],
    );

    let config_list = get_ech_config(BASE64_ECHCONFIG_LIST_WITH_UNSUPPORTED);
    assert_eq!(config_list.len(), 4);
    // The first config should be unsupported.
    assert!(matches!(
        config_list[0],
        EchConfigPayload::Unknown {
            version: EchVersion::Unknown(0xBADD),
            ..
        }
    ));
    // The other configs should be recognized.
    for config in config_list.iter().skip(1) {
        assert!(matches!(config, EchConfigPayload::V18(_)));
    }
}

#[test]
fn test_echconfig_serialization() {
    fn assert_round_trip_eq(data: &str) {
        let configs = get_ech_config(data);
        let mut output = Vec::new();
        configs.encode(&mut output);
        assert_eq!(data, BASE64_STANDARD.encode(&output));
    }

    assert_round_trip_eq(BASE64_ECHCONFIG_LIST_LOCALHOST);
    assert_round_trip_eq(BASE64_ECHCONFIG_LIST_CF);
    assert_round_trip_eq(BASE64_ECHCONFIG_LIST_WITH_UNSUPPORTED);
}

fn get_ech_config(s: &str) -> Vec<EchConfigPayload> {
    let bytes = BASE64_STANDARD.decode(s).unwrap();
    Vec::<_>::read(&mut Reader::init(&bytes)).unwrap()
}

// One EchConfig, with server-name "localhost".
const BASE64_ECHCONFIG_LIST_LOCALHOST: &str =
    "AED+DQA8AAAgACAxoIJyV36iDlfFRmqE+ho2PxXE0EISPfUUJYKCy6T8VwAIAAEAAQABAAOACWxvY2FsaG9zdAAA";

// Two EchConfigs, both with server-name "cloudflare-esni.com".
const BASE64_ECHCONFIG_LIST_CF: &str = "AK3+DQBCwwAgACAJ9T5U4FeM6631r2bvAuGtmEd8zQaoTkFAtArTcMl/XQAEAAEAASUTY2xvdWRmbGFyZS1lc25pLmNvbQAA/g0AYwMAEABBBGGbUlGLuGRorUeFwmrgHImkrh9uxoPrnFKpS5bQvnc5grfMS3PvymQ2FYL02WQi1ZzZJg5OsYYdzlaGYnEoJNsABAABAAEqE2Nsb3VkZmxhcmUtZXNuaS5jb20AAA==";

// Three EchConfigs, the first one with an unsupported version.
const BASE64_ECHCONFIG_LIST_WITH_UNSUPPORTED: &str = "AQW63QAFBQQDAgH+DQBmAAAQAEEE5itp4r9ln5e+Lx4NlIpM1Zdrt6keDUb73ampHp3culoB59aXqAoY+cPEox5W4nyDSNsWGhz1HX7xlC1Lz3IiwQAMAAEAAQABAAIAAQADQA5wdWJsaWMuZXhhbXBsZQAA/g0APQAAIAAgfWYWFXMCFK7ucFMzZvNqYJ6tZcDCCOYjIjRqtbzY3hwABBERIiJADnB1YmxpYy5leGFtcGxlAAD+DQBNAAAgACCFvWoDJ3wlQntS4mngx3qOtSS6HrPS8TJmLUsKxstzVwAMAAEAAQABAAIAAQADQA5wdWJsaWMuZXhhbXBsZQAIqqoABHRlc3Q=";