use std::io::Write;
use boring::hpke::HpkeKey;
use boring::pkcs12::Pkcs12;
use boring::pkey::{PKey, Private};
use boring::ssl::{
AlpnError, CertificateCompressionAlgorithm, CertificateCompressor, SslAcceptor, SslConnector, SslContextBuilder, SslEchKeys,
SslMethod, SslOptions, SslVerifyMode,
};
use boring::stack::Stack;
use boring::x509::store::X509StoreBuilder;
use boring::x509::X509;
use foreign_types::{ForeignType, ForeignTypeRef};
use crate::errors::Error;
use crate::models::{ALPN, Message, Version};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TLSVersion(pub u16);
impl TLSVersion {
pub const V1_3: Self = Self(0x0304);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TLSCipher(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TLSGroup(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Security {
pub secure: bool,
pub early_data: bool,
pub tls: bool,
pub tls_version: Option<TLSVersion>,
pub tls_group: Option<TLSGroup>,
pub tls_cipher: Option<TLSCipher>,
pub quic: bool,
pub quic_version: Option<u32>,
}
impl Security {
pub fn of(ssl: &boring::ssl::SslRef) -> Self {
let version = unsafe { boring_sys::SSL_version(ssl.as_ptr()) };
let group = unsafe { boring_sys::SSL_get_curve_id(ssl.as_ptr()) };
Self {
secure: true,
early_data: unsafe { boring_sys::SSL_early_data_accepted(ssl.as_ptr()) } != 0,
tls: true,
tls_version: u16::try_from(version).ok().filter(|version| *version != 0).map(TLSVersion),
tls_group: (group != 0).then_some(TLSGroup(group)),
tls_cipher: ssl.current_cipher().map(|cipher| TLSCipher(cipher.protocol_id())),
..Self::default()
}
}
pub fn quic(version: Option<u32>) -> Self {
Self {
secure: true,
tls: true,
tls_version: Some(TLSVersion::V1_3),
quic: true,
quic_version: version,
..Self::default()
}
}
pub fn apply(&self, message: &mut Message) {
message.security = *self;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
DER,
PEM,
}
impl Format {
pub const SEQUENCE: u8 = 0x30;
pub fn of(raw: &[u8]) -> Self {
match raw.iter().find(|byte| !byte.is_ascii_whitespace()) {
Some(&Self::SEQUENCE) => Self::DER,
_ => Self::PEM,
}
}
pub fn certificates(raw: &[u8]) -> Result<Vec<X509>, Error> {
match Self::of(raw) {
Self::DER => Ok(vec![X509::from_der(raw).map_err(Error::tls)?]),
Self::PEM => {
let parsed = X509::stack_from_pem(raw).map_err(Error::tls)?;
if parsed.is_empty() {
return Err(Error::TLS("PEM data carries no certificate".into()));
}
Ok(parsed)
}
}
}
pub fn certificate_list(blobs: &[Vec<u8>]) -> Result<Vec<X509>, Error> {
let mut list = Vec::with_capacity(blobs.len());
for blob in blobs {
list.extend(Self::certificates(blob)?);
}
Ok(list)
}
pub fn private_key(raw: &[u8]) -> Result<PKey<Private>, Error> {
match Self::of(raw) {
Self::DER => PKey::private_key_from_der(raw).map_err(Error::tls),
Self::PEM => PKey::private_key_from_pem(raw).map_err(Error::tls),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TLSConfig {
pub ciphers: Option<String>,
pub groups: Option<String>,
pub signature_algorithms: Option<String>,
pub prefer_server_ciphers: bool,
pub session_tickets: bool,
pub early_data: bool,
pub certificate_compression: bool,
}
impl Default for TLSConfig {
fn default() -> Self {
Self {
ciphers: None,
groups: None,
signature_algorithms: None,
prefer_server_ciphers: false,
session_tickets: true,
early_data: false,
certificate_compression: false,
}
}
}
impl TLSConfig {
pub fn install(&self, builder: &mut SslContextBuilder) -> Result<(), Error> {
if let Some(ciphers) = &self.ciphers {
builder.set_cipher_list(ciphers).map_err(Error::tls)?;
}
if let Some(groups) = &self.groups {
builder.set_curves_list(groups).map_err(Error::tls)?;
}
if let Some(algorithms) = &self.signature_algorithms {
builder.set_sigalgs_list(algorithms).map_err(Error::tls)?;
}
if self.prefer_server_ciphers {
builder.set_options(SslOptions::CIPHER_SERVER_PREFERENCE);
}
if !self.session_tickets {
builder.set_options(SslOptions::NO_TICKET);
}
unsafe { boring_sys::SSL_CTX_set_early_data_enabled(builder.as_ptr(), i32::from(self.early_data)) };
if self.certificate_compression {
builder.add_certificate_compression_algorithm(ZlibCertificateCompressor).map_err(Error::tls)?;
}
Ok(())
}
pub fn install_roots(roots: &[Vec<u8>], builder: &mut SslContextBuilder) -> Result<(), Error> {
if roots.is_empty() {
builder.set_default_verify_paths().map_err(Error::tls)?;
return Ok(());
}
let mut store = X509StoreBuilder::new().map_err(Error::tls)?;
for root in Format::certificate_list(roots)? {
store.add_cert(root).map_err(Error::tls)?;
}
builder.set_verify_cert_store(store.build()).map_err(Error::tls)?;
Ok(())
}
pub fn client(&self, roots: &[Vec<u8>], versions: &[Version]) -> Result<SslConnector, Error> {
let mut builder = SslConnector::builder(SslMethod::tls()).map_err(Error::tls)?;
builder.set_alpn_protos(&ALPN::wire(versions)).map_err(Error::tls)?;
Self::install_roots(roots, &mut builder)?;
self.install(&mut builder)?;
Ok(builder.build())
}
pub fn server(&self, identity: &Identity, versions: &[Version], ech: Option<&ECHKeys>) -> Result<SslAcceptor, Error> {
let mut builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).map_err(Error::tls)?;
identity.install(&mut builder)?;
self.install(&mut builder)?;
let offered = ALPN::list(versions);
builder.set_alpn_select_callback(move |_ssl, client| ALPN::select(&offered, client).ok_or(AlpnError::NOACK));
if let Some(ech) = ech {
ech.install(&builder)?;
}
Ok(builder.build())
}
pub fn quic_client(&self, roots: &[Vec<u8>]) -> Result<SslContextBuilder, Error> {
let mut builder = SslContextBuilder::new(SslMethod::tls()).map_err(Error::tls)?;
Self::install_roots(roots, &mut builder)?;
self.install(&mut builder)?;
builder.set_verify(SslVerifyMode::PEER);
Ok(builder)
}
pub fn quic_server(&self, identity: &Identity, ech: Option<&ECHKeys>) -> Result<SslContextBuilder, Error> {
let mut builder = SslContextBuilder::new(SslMethod::tls()).map_err(Error::tls)?;
identity.install(&mut builder)?;
self.install(&mut builder)?;
builder.set_verify(SslVerifyMode::NONE);
if let Some(ech) = ech {
ech.install(&builder)?;
}
Ok(builder)
}
}
pub struct ZlibCertificateCompressor;
impl CertificateCompressor for ZlibCertificateCompressor {
const ALGORITHM: CertificateCompressionAlgorithm = CertificateCompressionAlgorithm::ZLIB;
const CAN_COMPRESS: bool = true;
const CAN_DECOMPRESS: bool = true;
fn compress<W: Write>(&self, input: &[u8], output: &mut W) -> std::io::Result<()> {
let mut encoder = flate2::write::ZlibEncoder::new(output, flate2::Compression::default());
encoder.write_all(input)?;
encoder.finish()?;
Ok(())
}
fn decompress<W: Write>(&self, input: &[u8], output: &mut W) -> std::io::Result<()> {
let mut decoder = flate2::write::ZlibDecoder::new(output);
decoder.write_all(input)?;
decoder.finish()?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct Identity {
pub certificates: Vec<Vec<u8>>,
pub key: Vec<u8>,
}
impl Identity {
pub fn new(certificates: Vec<Vec<u8>>, key: Vec<u8>) -> Self {
Self { certificates, key }
}
pub fn from_pkcs12(raw: &[u8], passphrase: &str) -> Result<Self, Error> {
let archive = Pkcs12::from_der(raw).map_err(Error::tls)?;
let passphrase = std::ffi::CString::new(passphrase).map_err(Error::tls)?;
let mut key = std::ptr::null_mut();
let mut leaf = std::ptr::null_mut();
let mut rest = std::ptr::null_mut();
let opened = unsafe { boring_sys::PKCS12_parse(archive.as_ptr(), passphrase.as_ptr(), &mut key, &mut leaf, &mut rest) };
let key = (!key.is_null()).then(|| unsafe { PKey::<Private>::from_ptr(key) });
let leaf = (!leaf.is_null()).then(|| unsafe { X509::from_ptr(leaf) });
let rest = (!rest.is_null()).then(|| unsafe { Stack::<X509>::from_ptr(rest) });
if opened != 1 {
return Err(Error::tls(boring::error::ErrorStack::get()));
}
let (Some(key), Some(leaf)) = (key, leaf) else {
return Err(Error::TLS("the PKCS#12 archive carries no certificate for its key".into()));
};
let mut certificates = vec![leaf.to_der().map_err(Error::tls)?];
for extra in rest.into_iter().flatten() {
certificates.push(extra.to_der().map_err(Error::tls)?);
}
Ok(Self { certificates, key: key.private_key_to_der_pkcs8().map_err(Error::tls)? })
}
pub fn chain(&self) -> Result<Vec<X509>, Error> {
Format::certificate_list(&self.certificates)
}
pub fn private_key(&self) -> Result<PKey<Private>, Error> {
Format::private_key(&self.key)
}
pub fn install(&self, builder: &mut SslContextBuilder) -> Result<(), Error> {
let mut chain = self.chain()?.into_iter();
let leaf = chain.next().ok_or_else(|| Error::TLS("identity has no certificate".into()))?;
builder.set_certificate(&leaf).map_err(Error::tls)?;
for extra in chain {
builder.add_extra_chain_cert(extra).map_err(Error::tls)?;
}
let key = self.private_key()?;
builder.set_private_key(&key).map_err(Error::tls)?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ECHConfig {
pub version: u16,
pub public_name: String,
pub maximum_name_length: u8,
}
impl ECHConfig {
pub const VERSION: u16 = 0xfe0d;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ECHConfigList {
pub configs: Vec<ECHConfig>,
}
impl ECHConfigList {
pub fn parse(raw: &[u8]) -> Result<Self, Error> {
if raw.len() < 2 {
return Err(Error::TLS("ECHConfigList is too short".into()));
}
let declared = u16::from_be_bytes([raw[0], raw[1]]) as usize;
let body = &raw[2..];
if declared != body.len() {
return Err(Error::TLS("ECHConfigList length does not match its contents".into()));
}
let mut configs = Vec::new();
let mut offset = 0;
while offset + 4 <= body.len() {
let version = u16::from_be_bytes([body[offset], body[offset + 1]]);
let size = u16::from_be_bytes([body[offset + 2], body[offset + 3]]) as usize;
offset += 4;
let contents = body
.get(offset..offset + size)
.ok_or_else(|| Error::TLS("an ECHConfig runs past the list".into()))?;
if version == ECHConfig::VERSION {
configs.push(Self::contents(contents)?);
}
offset += size;
}
if configs.is_empty() {
return Err(Error::TLS("ECHConfigList carries no supported ECHConfig".into()));
}
Ok(Self { configs })
}
pub fn contents(raw: &[u8]) -> Result<ECHConfig, Error> {
if raw.len() < 5 {
return Err(Error::TLS("ECHConfig is too short".into()));
}
let key_length = u16::from_be_bytes([raw[3], raw[4]]) as usize;
let mut offset = 5 + key_length;
let cipher_length = raw
.get(offset..offset + 2)
.map(|slice| u16::from_be_bytes([slice[0], slice[1]]) as usize)
.ok_or_else(|| Error::TLS("ECHConfig ends inside its cipher suites".into()))?;
offset += 2 + cipher_length;
let maximum_name_length = *raw.get(offset).ok_or_else(|| Error::TLS("ECHConfig has no maximum name length".into()))?;
offset += 1;
let name_length = *raw.get(offset).ok_or_else(|| Error::TLS("ECHConfig has no public name".into()))? as usize;
offset += 1;
let name = raw
.get(offset..offset + name_length)
.ok_or_else(|| Error::TLS("ECHConfig public name runs past the config".into()))?;
Ok(ECHConfig {
version: ECHConfig::VERSION,
public_name: String::from_utf8_lossy(name).into_owned(),
maximum_name_length,
})
}
}
#[derive(Debug, Clone)]
pub struct ECHKeys {
pub config: Vec<u8>,
pub private_key: Vec<u8>,
}
impl ECHKeys {
pub const KEM_X25519_HKDF_SHA256: u16 = 0x0020;
pub const KDF_HKDF_SHA256: u16 = 0x0001;
pub const AEAD_AES_128_GCM: u16 = 0x0001;
pub const MAXIMUM_NAME_LENGTH: u8 = 64;
pub const MAXIMUM_PUBLIC_NAME: usize = u8::MAX as usize;
pub fn generate(public_name: &str, config_id: u8) -> Result<Self, Error> {
let mut public = [0u8; 32];
let mut private = [0u8; 32];
unsafe { boring_sys::X25519_keypair(public.as_mut_ptr(), private.as_mut_ptr()) };
Ok(Self { config: Self::encode(public_name, config_id, &public)?, private_key: private.to_vec() })
}
pub fn encode(public_name: &str, config_id: u8, public_key: &[u8]) -> Result<Vec<u8>, Error> {
if public_name.len() > Self::MAXIMUM_PUBLIC_NAME {
return Err(Error::TLS(format!("an ECH public name of {} octets does not fit its length prefix", public_name.len())));
}
let Ok(key_length) = u16::try_from(public_key.len()) else {
return Err(Error::TLS(format!("an ECH public key of {} octets does not fit its length prefix", public_key.len())));
};
let mut contents = Vec::new();
contents.push(config_id);
contents.extend_from_slice(&Self::KEM_X25519_HKDF_SHA256.to_be_bytes());
contents.extend_from_slice(&key_length.to_be_bytes());
contents.extend_from_slice(public_key);
contents.extend_from_slice(&4u16.to_be_bytes());
contents.extend_from_slice(&Self::KDF_HKDF_SHA256.to_be_bytes());
contents.extend_from_slice(&Self::AEAD_AES_128_GCM.to_be_bytes());
contents.push(Self::MAXIMUM_NAME_LENGTH);
contents.push(public_name.len() as u8);
contents.extend_from_slice(public_name.as_bytes());
contents.extend_from_slice(&0u16.to_be_bytes());
let mut config = ECHConfig::VERSION.to_be_bytes().to_vec();
config.extend_from_slice(&(contents.len() as u16).to_be_bytes());
config.extend_from_slice(&contents);
Ok(config)
}
pub fn config_list(&self) -> Vec<u8> {
let mut list = (self.config.len() as u16).to_be_bytes().to_vec();
list.extend_from_slice(&self.config);
list
}
pub fn install(&self, builder: &SslContextBuilder) -> Result<(), Error> {
let key = HpkeKey::dhkem_p256_sha256(&self.private_key).map_err(Error::tls)?;
let mut keys = SslEchKeys::builder().map_err(Error::tls)?;
keys.add_key(true, &self.config, key).map_err(Error::tls)?;
builder.set_ech_keys(&keys.build()).map_err(Error::tls)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ECHStatus {
pub accepted: bool,
}
impl ECHStatus {
pub fn of(ssl: &boring::ssl::SslRef) -> Self {
Self { accepted: ssl.ech_accepted() }
}
pub fn succeeded(&self) -> bool {
self.accepted
}
}