1#[cfg(any(
2 feature = "compression-gzip",
3 feature = "compression-brotli",
4 feature = "compression-deflate",
5 feature = "compression-zstd"
6))]
7use leyline_bssl::ssl::{CertificateCompressionAlgorithm, CertificateCompressor};
8use leyline_bssl::ssl::{SslContextBuilder, SslVerifyMode};
9
10use crate::profile::{BrowserProfile, TlsProfile};
11
12use crate::tls::error::TlsError;
13use crate::tls::keylog::install_from_env;
14use crate::tls::trust::{TlsTrustConfig, wire_configured_trust};
15
16mod phase;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19#[non_exhaustive]
20pub enum TlsMinVersion {
21 Tls10,
22 Tls12,
23 Tls13,
24}
25
26#[cfg(any(leyline_unstable_bssl, feature = "bench-internals"))]
27pub(crate) fn build_ssl_context(
28 profile: &BrowserProfile,
29 min_version: TlsMinVersion,
30) -> Result<SslContextBuilder, TlsError> {
31 let mut builder = SslContextBuilder::new(leyline_bssl::ssl::SslMethod::tls())
32 .map_err(TlsError::from_stack)?;
33 apply_profile_with_trust(
34 &mut builder,
35 profile,
36 min_version,
37 &TlsTrustConfig::default(),
38 )?;
39 Ok(builder)
40}
41
42pub(crate) fn apply_profile_with_trust(
43 builder: &mut SslContextBuilder,
44 profile: &BrowserProfile,
45 min_version: TlsMinVersion,
46 trust: &TlsTrustConfig,
47) -> Result<(), TlsError> {
48 apply_tls_with_trust(builder, &profile.tls, min_version, trust)
49}
50
51pub(crate) fn apply_tls_with_trust(
52 builder: &mut SslContextBuilder,
53 tls: &TlsProfile,
54 min_version: TlsMinVersion,
55 trust: &TlsTrustConfig,
56) -> Result<(), TlsError> {
57 phase::ciphers(builder, tls)?;
58 phase::curves(builder, tls)?;
59 phase::sigalgs(builder, tls)?;
60 phase::extensions(builder, tls)?;
61 phase::versions(builder, tls, min_version, trust.min_tls_version)?;
62
63 wire_configured_trust(builder, trust)?;
64 builder.set_verify(SslVerifyMode::PEER);
65
66 install_from_env(builder);
67
68 Ok(())
69}
70
71fn tls13_cipher_ids(ciphers: &[String]) -> Result<Vec<u16>, TlsError> {
72 ciphers
73 .iter()
74 .filter_map(|cipher| match crate::iana::cipher_id(cipher) {
75 Some(id) if crate::iana::is_tls13_cipher(id) => Some(Ok(id)),
76 _ if cipher.starts_with("TLS_AES_") || cipher.starts_with("TLS_CHACHA20_") => {
77 Some(Err(TlsError::Profile(format!(
78 "unknown TLS 1.3 cipher: {cipher}"
79 ))))
80 }
81 _ => None,
82 })
83 .collect()
84}
85
86fn profile_min_version(declared: &Option<String>) -> Result<Option<TlsMinVersion>, TlsError> {
87 match declared.as_deref() {
88 None => Ok(None),
89 Some("1.0") => Ok(Some(TlsMinVersion::Tls10)),
90 Some("1.2") => Ok(Some(TlsMinVersion::Tls12)),
91 Some("1.3") => Ok(Some(TlsMinVersion::Tls13)),
92 Some(other) => Err(TlsError::Profile(format!(
93 "unsupported min_tls_version {other:?}; expected \"1.0\", \"1.2\", or \"1.3\""
94 ))),
95 }
96}
97
98#[cfg(not(all(
99 feature = "compression-brotli",
100 feature = "compression-zstd",
101 any(feature = "compression-gzip", feature = "compression-deflate")
102)))]
103fn missing(algo: &str, feature: &str) -> TlsError {
104 TlsError::Profile(format!(
105 "profile requests {algo} certificate decompression but the `{feature}` feature is not compiled in"
106 ))
107}
108
109#[cfg(any(
110 feature = "compression-gzip",
111 feature = "compression-brotli",
112 feature = "compression-deflate",
113 feature = "compression-zstd"
114))]
115const MAX_CERT_DECOMPRESSED_BYTES: usize = 1024 * 1024;
116
117#[cfg(any(
118 feature = "compression-gzip",
119 feature = "compression-brotli",
120 feature = "compression-deflate",
121 feature = "compression-zstd"
122))]
123fn read_limited_cert<R: std::io::Read>(mut decoder: R) -> std::io::Result<Vec<u8>> {
124 let mut out = Vec::new();
125 let mut buf = [0u8; 8192];
126 loop {
127 let n = decoder.read(&mut buf)?;
128 if n == 0 {
129 return Ok(out);
130 }
131 if out.len() + n > MAX_CERT_DECOMPRESSED_BYTES {
132 return Err(std::io::Error::new(
133 std::io::ErrorKind::InvalidData,
134 "decompressed certificate exceeds limit",
135 ));
136 }
137 out.extend_from_slice(&buf[..n]);
138 }
139}
140
141#[cfg(feature = "compression-brotli")]
142#[derive(Debug)]
143struct BrotliDecompressor;
144
145#[cfg(feature = "compression-brotli")]
146impl CertificateCompressor for BrotliDecompressor {
147 const ALGORITHM: CertificateCompressionAlgorithm = CertificateCompressionAlgorithm::BROTLI;
148 const CAN_COMPRESS: bool = false;
149 const CAN_DECOMPRESS: bool = true;
150
151 fn decompress<W>(&self, input: &[u8], output: &mut W) -> std::io::Result<()>
152 where
153 W: std::io::Write,
154 {
155 let decoded = read_limited_cert(brotli::Decompressor::new(input, 4096))?;
156 output.write_all(&decoded)
157 }
158}
159
160#[cfg(any(feature = "compression-gzip", feature = "compression-deflate"))]
161#[derive(Debug)]
162struct ZlibDecompressor;
163
164#[cfg(any(feature = "compression-gzip", feature = "compression-deflate"))]
165impl CertificateCompressor for ZlibDecompressor {
166 const ALGORITHM: CertificateCompressionAlgorithm = CertificateCompressionAlgorithm::ZLIB;
167 const CAN_COMPRESS: bool = false;
168 const CAN_DECOMPRESS: bool = true;
169
170 fn decompress<W>(&self, input: &[u8], output: &mut W) -> std::io::Result<()>
171 where
172 W: std::io::Write,
173 {
174 let decoded = read_limited_cert(flate2::read::ZlibDecoder::new(input))?;
175 output.write_all(&decoded)
176 }
177}
178
179#[cfg(feature = "compression-zstd")]
180#[derive(Debug)]
181struct ZstdDecompressor;
182
183#[cfg(feature = "compression-zstd")]
184impl CertificateCompressor for ZstdDecompressor {
185 const ALGORITHM: CertificateCompressionAlgorithm = CertificateCompressionAlgorithm::ZSTD;
186 const CAN_COMPRESS: bool = false;
187 const CAN_DECOMPRESS: bool = true;
188
189 fn decompress<W>(&self, input: &[u8], output: &mut W) -> std::io::Result<()>
190 where
191 W: std::io::Write,
192 {
193 let decoder = zstd::stream::read::Decoder::new(input)?;
194 let decoded = read_limited_cert(decoder)?;
195 output.write_all(&decoded)
196 }
197}
198
199#[cfg(test)]
200mod tests;