From 62b0c7625a3dc973d017a2b2f077565a1de71647 Mon Sep 17 00:00:00 2001 From: Tom Fay Date: Thu, 7 Nov 2024 06:50:45 +0000 Subject: [PATCH] initial implemention of openssl crypto provider --- .gitignore | 2 + Cargo.toml | 30 ++++ README.md | 6 + src/cipher_suites.rs | 164 ++++++++++++++++++ src/ecdh.rs | 149 +++++++++++++++++ src/hash.rs | 89 ++++++++++ src/hmac.rs | 72 ++++++++ src/lib.rs | 331 +++++++++++++++++++++++++++++++++++++ src/signer.rs | 144 ++++++++++++++++ src/tls12.rs | 337 +++++++++++++++++++++++++++++++++++++ src/tls13.rs | 163 ++++++++++++++++++ src/verify.rs | 201 ++++++++++++++++++++++ tests/certs/RootCA.crt | 20 +++ tests/certs/RootCA.key | 28 ++++ tests/certs/RootCA.pem | 20 +++ tests/certs/localhost.crt | 21 +++ tests/certs/localhost.key | 28 ++++ tests/certs/localhost.pem | 21 +++ tests/it.rs | 340 ++++++++++++++++++++++++++++++++++++++ 19 files changed, 2166 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/cipher_suites.rs create mode 100644 src/ecdh.rs create mode 100644 src/hash.rs create mode 100644 src/hmac.rs create mode 100644 src/lib.rs create mode 100644 src/signer.rs create mode 100644 src/tls12.rs create mode 100644 src/tls13.rs create mode 100644 src/verify.rs create mode 100644 tests/certs/RootCA.crt create mode 100644 tests/certs/RootCA.key create mode 100644 tests/certs/RootCA.pem create mode 100644 tests/certs/localhost.crt create mode 100644 tests/certs/localhost.key create mode 100644 tests/certs/localhost.pem create mode 100644 tests/it.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f4ab365 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "rustls-openssl" +authors = ["Tom Fay "] +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Rustls crypto provider for OpenSSL" +homepage = "https://github.com/tofay/rustls-openssl" +repository = "https://github.com/tofay/rustls-openssl" +readme = "README.md" + +[dependencies] +openssl = { version = "0.10.68" } +rustls = { version = "0.23.0", features = [ + "tls12", + "std", +], default-features = false } +rustls-webpki = "0.102.2" + +[features] +default = [] +x25519 = [] +chacha = [] + +[dev-dependencies] +lazy_static = "1.4.0" +once_cell = "1.8.0" +rstest = "0.23.0" +rustls-pemfile = "2" +webpki-roots = "0.26" diff --git a/README.md b/README.md new file mode 100644 index 0000000..f4765e5 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# rustls-openssl +An experimental [rustls Crypto Provider](https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html) that uses OpenSSL for cryptographic operations. + +## Usage +The main entry points are the `rustls_openssl::default_provider` and `rustls_openssl::custom_provider` functions. +See the [rustls documentation]((https://docs.rs/rustls/latest/rustls/crypto/struct.CryptoProvider.html)) for how to use them. diff --git a/src/cipher_suites.rs b/src/cipher_suites.rs new file mode 100644 index 0000000..37110f7 --- /dev/null +++ b/src/cipher_suites.rs @@ -0,0 +1,164 @@ +use crate::hash::HashAlgorithm; +use crate::hmac::{HmacSha256, HmacSha384}; +use crate::tls12::{AesGcm, Tls12Gcm}; +use rustls::crypto::{CipherSuiteCommon, KeyExchangeAlgorithm}; +use rustls::{ + CipherSuite, SignatureScheme, SupportedCipherSuite, Tls12CipherSuite, Tls13CipherSuite, +}; + +use rustls::crypto::tls12::PrfUsingHmac; +use rustls::crypto::tls13::HkdfUsingHmac; + +use crate::tls13::AeadAlgorithm; + +/// The TLS1.3 ciphersuite TLS_CHACHA20_POLY1305_SHA256 +#[cfg(feature = "chacha")] +pub static TLS13_CHACHA20_POLY1305_SHA256: SupportedCipherSuite = + SupportedCipherSuite::Tls13(&Tls13CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + hash_provider: &HashAlgorithm::SHA256, + confidentiality_limit: u64::MAX, + }, + hkdf_provider: &HkdfUsingHmac(&HmacSha256), + aead_alg: &AeadAlgorithm::ChaCha20Poly1305, + quic: None, + }); + +/// The TLS1.3 ciphersuite TLS_AES_256_GCM_SHA384 +pub static TLS13_AES_256_GCM_SHA384: SupportedCipherSuite = + SupportedCipherSuite::Tls13(&Tls13CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS13_AES_256_GCM_SHA384, + hash_provider: &HashAlgorithm::SHA384, + confidentiality_limit: 1 << 23, + }, + hkdf_provider: &HkdfUsingHmac(&HmacSha384), + aead_alg: &AeadAlgorithm::Aes256Gcm, + quic: None, + }); + +/// The TLS1.3 ciphersuite TLS_AES_128_GCM_SHA256 +pub static TLS13_AES_128_GCM_SHA256: SupportedCipherSuite = + SupportedCipherSuite::Tls13(&Tls13CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS13_AES_128_GCM_SHA256, + hash_provider: &HashAlgorithm::SHA256, + confidentiality_limit: 1 << 23, + }, + hkdf_provider: &HkdfUsingHmac(&HmacSha256), + aead_alg: &AeadAlgorithm::Aes128Gcm, + quic: None, + }); + +/// TLS 1.2 + +// /// The TLS1.2 ciphersuite TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256. +// #[cfg(feature = "chacha")] +// pub static TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: SupportedCipherSuite = +// SupportedCipherSuite::Tls12(&Tls12CipherSuite { +// common: CipherSuiteCommon { +// suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, +// hash_provider: &HashAlgorithm::SHA256, +// confidentiality_limit: u64::MAX, +// }, +// kx: KeyExchangeAlgorithm::ECDHE, +// sign: TLS12_ECDSA_SCHEMES, +// aead_alg: &crate::tls12::Tls12ChaCha, +// prf_provider: &PrfUsingHmac(&HmacSha256), +// }); + +/// The TLS1.2 ciphersuite TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 +#[cfg(feature = "chacha")] +pub static TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: SupportedCipherSuite = + SupportedCipherSuite::Tls12(&Tls12CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + hash_provider: &HashAlgorithm::SHA256, + confidentiality_limit: u64::MAX, + }, + kx: KeyExchangeAlgorithm::ECDHE, + sign: TLS12_RSA_SCHEMES, + aead_alg: &crate::tls12::Tls12ChaCha, + prf_provider: &PrfUsingHmac(&HmacSha256), + }); + +/// The TLS1.2 ciphersuite TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +pub static TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: SupportedCipherSuite = + SupportedCipherSuite::Tls12(&Tls12CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + hash_provider: &HashAlgorithm::SHA256, + confidentiality_limit: 1 << 23, + }, + kx: KeyExchangeAlgorithm::ECDHE, + sign: TLS12_RSA_SCHEMES, + aead_alg: &Tls12Gcm { + algo_type: AesGcm::Aes128Gcm, + }, + prf_provider: &PrfUsingHmac(&HmacSha256), + }); + +/// The TLS1.2 ciphersuite TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +pub static TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: SupportedCipherSuite = + SupportedCipherSuite::Tls12(&Tls12CipherSuite { + common: CipherSuiteCommon { + suite: CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + hash_provider: &HashAlgorithm::SHA384, + confidentiality_limit: 1 << 23, + }, + kx: KeyExchangeAlgorithm::ECDHE, + sign: TLS12_RSA_SCHEMES, + aead_alg: &Tls12Gcm { + algo_type: AesGcm::Aes256Gcm, + }, + prf_provider: &PrfUsingHmac(&HmacSha384), + }); + +// /// The TLS1.2 ciphersuite TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +// pub static TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: SupportedCipherSuite = +// SupportedCipherSuite::Tls12(&Tls12CipherSuite { +// common: CipherSuiteCommon { +// suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, +// hash_provider: &HashAlgorithm::SHA256, +// confidentiality_limit: 1 << 23, +// }, +// kx: KeyExchangeAlgorithm::ECDHE, +// sign: TLS12_ECDSA_SCHEMES, +// aead_alg: &Tls12Gcm { +// algo_type: AesGcm::Aes128Gcm, +// }, +// prf_provider: &PrfUsingHmac(&HmacSha256), +// }); + +// /// The TLS1.2 ciphersuite TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +// pub static TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: SupportedCipherSuite = +// SupportedCipherSuite::Tls12(&Tls12CipherSuite { +// common: CipherSuiteCommon { +// suite: CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, +// hash_provider: &HashAlgorithm::SHA384, +// confidentiality_limit: 1 << 23, +// }, +// kx: KeyExchangeAlgorithm::ECDHE, +// sign: TLS12_ECDSA_SCHEMES, +// aead_alg: &Tls12Gcm { +// algo_type: AesGcm::Aes256Gcm, +// }, +// prf_provider: &PrfUsingHmac(&HmacSha384), +// }); + +// static TLS12_ECDSA_SCHEMES: &[SignatureScheme] = &[ +// SignatureScheme::ECDSA_NISTP521_SHA512, +// SignatureScheme::ECDSA_NISTP384_SHA384, +// SignatureScheme::ECDSA_NISTP256_SHA256, +// ]; + +/// RSA schemes in descending order of preference +pub(crate) static TLS12_RSA_SCHEMES: &[SignatureScheme] = &[ + SignatureScheme::RSA_PSS_SHA512, + SignatureScheme::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA256, + SignatureScheme::RSA_PKCS1_SHA512, + SignatureScheme::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA256, +]; diff --git a/src/ecdh.rs b/src/ecdh.rs new file mode 100644 index 0000000..375d66e --- /dev/null +++ b/src/ecdh.rs @@ -0,0 +1,149 @@ +use openssl::bn::BigNumContext; +use openssl::derive::Deriver; +use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm}; +use openssl::nid::Nid; +#[cfg(feature = "x25519")] +use openssl::pkey::Id; +use openssl::pkey::{PKey, Private, Public}; +use rustls::crypto::{ActiveKeyExchange, SharedSecret, SupportedKxGroup}; +use rustls::{Error, NamedGroup}; + +/// Supported KeyExchange groups. +/// ```ignore +/// SECP384R1 +/// SECP256R1 +/// X25519 // Enabled with the `x25519` feature +/// ``` +pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ + SECP256R1, + SECP384R1, + #[cfg(feature = "x25519")] + X25519, +]; + +/// KXGroup's that use openssl::ec module with Nid's for key exchange. +#[derive(Debug)] +struct EcKxGroup { + name: NamedGroup, + nid: Nid, +} + +struct EcKeyExchange { + priv_key: EcKey, + name: NamedGroup, + group: EcGroup, + pub_key: Vec, +} + +#[cfg(feature = "x25519")] +/// KXGroup for X25519 +#[derive(Debug)] +struct X25519KxGroup {} + +#[cfg(feature = "x25519")] +#[derive(Debug)] +struct X25519KeyExchange { + private_key: PKey, + public_key: Vec, +} + +#[cfg(feature = "x25519")] +pub const X25519: &dyn SupportedKxGroup = &X25519KxGroup {}; + +pub const SECP256R1: &dyn SupportedKxGroup = &EcKxGroup { + name: NamedGroup::secp256r1, + nid: Nid::X9_62_PRIME256V1, +}; + +pub const SECP384R1: &dyn SupportedKxGroup = &EcKxGroup { + name: NamedGroup::secp384r1, + nid: Nid::SECP384R1, +}; + +impl SupportedKxGroup for EcKxGroup { + fn start(&self) -> Result, Error> { + let group = EcGroup::from_curve_name(self.nid).unwrap(); + let priv_key = EcKey::generate(&group).unwrap(); + let mut ctx = BigNumContext::new().unwrap(); + let pub_key = priv_key + .public_key() + .to_bytes(&group, PointConversionForm::UNCOMPRESSED, &mut ctx) + .unwrap(); + + Ok(Box::new(EcKeyExchange { + priv_key, + name: self.name, + group, + pub_key, + })) + } + + fn name(&self) -> NamedGroup { + self.name + } +} + +impl EcKeyExchange { + fn load_peer_key(&self, peer_pub_key: &[u8]) -> Result, Error> { + let mut ctx = BigNumContext::new().unwrap(); + let point = EcPoint::from_bytes(&self.group, peer_pub_key, &mut ctx).unwrap(); + let peer_key = EcKey::from_public_key(&self.group, &point).unwrap(); + peer_key.check_key().unwrap(); + let peer_key: PKey<_> = peer_key.try_into().unwrap(); + Ok(peer_key) + } +} + +impl ActiveKeyExchange for EcKeyExchange { + fn complete(self: Box, peer_pub_key: &[u8]) -> Result { + let peer_key = self.load_peer_key(peer_pub_key).unwrap(); + let key: PKey<_> = self.priv_key.try_into().unwrap(); + let mut deriver = Deriver::new(&key).unwrap(); + deriver.set_peer(&peer_key).unwrap(); + let secret = deriver.derive_to_vec().unwrap(); + Ok(SharedSecret::from(secret.as_slice())) + } + + fn pub_key(&self) -> &[u8] { + &self.pub_key + } + + fn group(&self) -> NamedGroup { + self.name + } +} + +#[cfg(feature = "x25519")] +impl SupportedKxGroup for X25519KxGroup { + fn start(&self) -> Result, Error> { + let private_key = PKey::generate_x25519().unwrap(); + let public_key = private_key.raw_public_key().unwrap(); + Ok(Box::new(X25519KeyExchange { + private_key, + public_key, + })) + } + + fn name(&self) -> NamedGroup { + NamedGroup::X25519 + } +} + +#[cfg(feature = "x25519")] +impl ActiveKeyExchange for X25519KeyExchange { + fn complete(self: Box, peer_pub_key: &[u8]) -> Result { + let peer_public_key = PKey::public_key_from_raw_bytes(peer_pub_key, Id::X25519).unwrap(); + let mut deriver = Deriver::new(&self.private_key).unwrap(); + deriver.set_peer(&peer_public_key).unwrap(); + let secret = deriver.derive_to_vec().unwrap(); + Ok(SharedSecret::from(secret.as_slice())) + } + + fn pub_key(&self) -> &[u8] { + &self.public_key + } + + fn group(&self) -> NamedGroup { + NamedGroup::X25519 + } +} diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..7c8005b --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,89 @@ +//! Provide Rustls `Hash` implementation using OpenSSL `MessageDigest`. +use openssl::{ + hash::MessageDigest, + sha::{self, sha256, sha384}, +}; +use rustls::crypto::hash::{Context, Hash, Output}; + +pub(crate) enum HashAlgorithm { + SHA256, + SHA384, +} + +impl HashAlgorithm { + pub fn message_digest(&self) -> MessageDigest { + match &self { + HashAlgorithm::SHA256 => MessageDigest::sha256(), + HashAlgorithm::SHA384 => MessageDigest::sha384(), + } + } +} + +impl Hash for HashAlgorithm { + fn start(&self) -> Box { + match &self { + HashAlgorithm::SHA256 => Box::new(Sha256Context(sha::Sha256::new())), + HashAlgorithm::SHA384 => Box::new(Sha384Context(sha::Sha384::new())), + } + } + + fn hash(&self, data: &[u8]) -> Output { + match &self { + HashAlgorithm::SHA256 => Output::new(&sha256(data)[..]), + HashAlgorithm::SHA384 => Output::new(&sha384(data)[..]), + } + } + + fn output_len(&self) -> usize { + self.message_digest().size() + } + + fn algorithm(&self) -> rustls::crypto::hash::HashAlgorithm { + match &self { + HashAlgorithm::SHA256 => rustls::crypto::hash::HashAlgorithm::SHA256, + HashAlgorithm::SHA384 => rustls::crypto::hash::HashAlgorithm::SHA384, + } + } +} + +struct Sha256Context(sha::Sha256); +struct Sha384Context(sha::Sha384); + +impl Context for Sha256Context { + fn fork_finish(&self) -> Output { + let new_context = self.0.clone(); + + Output::new(&new_context.finish()[..]) + } + + fn fork(&self) -> Box { + Box::new(Sha256Context(self.0.clone())) + } + + fn finish(self: Box) -> Output { + Output::new(&self.0.finish()[..]) + } + + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } +} + +impl Context for Sha384Context { + fn fork_finish(&self) -> Output { + let new_context = self.0.clone(); + Output::new(&new_context.finish()[..]) + } + + fn fork(&self) -> Box { + Box::new(Sha384Context(self.0.clone())) + } + + fn finish(self: Box) -> Output { + Output::new(&self.0.finish()[..]) + } + + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } +} diff --git a/src/hmac.rs b/src/hmac.rs new file mode 100644 index 0000000..30f8bb3 --- /dev/null +++ b/src/hmac.rs @@ -0,0 +1,72 @@ +use openssl::{ + hash::MessageDigest, + pkey::{PKey, Private}, + sign::Signer, +}; +use rustls::crypto::hmac::{Hmac, Key, Tag}; + +pub struct HmacSha256; +pub struct HmacSha256Key(PKey); + +pub struct HmacSha384; +pub struct HmacSha384Key(PKey); + +impl Hmac for HmacSha256 { + fn with_key(&self, key: &[u8]) -> Box { + Box::new(HmacSha256Key(PKey::hmac(key).unwrap())) + } + + fn hash_output_len(&self) -> usize { + 32 + } +} + +impl Key for HmacSha256Key { + fn sign(&self, data: &[&[u8]]) -> Tag { + self.sign_concat(&[], data, &[]) + } + + fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> Tag { + let mut signer = Signer::new(MessageDigest::sha256(), &self.0).unwrap(); + signer.update(first).unwrap(); + for d in middle { + signer.update(d).unwrap(); + } + signer.update(last).unwrap(); + Tag::new(&signer.sign_to_vec().unwrap()) + } + + fn tag_len(&self) -> usize { + 32 + } +} + +impl Hmac for HmacSha384 { + fn with_key(&self, key: &[u8]) -> Box { + Box::new(HmacSha384Key(PKey::hmac(key).unwrap())) + } + + fn hash_output_len(&self) -> usize { + 48 + } +} + +impl Key for HmacSha384Key { + fn sign(&self, data: &[&[u8]]) -> Tag { + self.sign_concat(&[], data, &[]) + } + + fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> Tag { + let mut signer = Signer::new(MessageDigest::sha384(), &self.0).unwrap(); + signer.update(first).unwrap(); + for d in middle { + signer.update(d).unwrap(); + } + signer.update(last).unwrap(); + Tag::new(&signer.sign_to_vec().unwrap()) + } + + fn tag_len(&self) -> usize { + 48 + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b4a9f2f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,331 @@ +//! # OpenSSL Provider for Rustls +//! +//! A Rustls crypto provider that uses `OpenSSL` for crypto. +//! +//! ## Limitations +//! +//! - TLS 1.2: No ECDSA support. +//! - QUIC Protocol: Not supported. +//! +//! ## Supported Ciphers +//! +//! Supported cipher suites are listed below, ordered by preference. IE: The default configuration prioritizes `TLS13_AES_256_GCM_SHA384` over `TLS13_AES_128_GCM_SHA256`. +//! +//! ### TLS 1.3 +//! +//! ```ignore +//! TLS13_AES_256_GCM_SHA384 +//! TLS13_AES_128_GCM_SHA256 +//! TLS13_CHACHA20_POLY1305_SHA256 // Requires the `chacha` feature +//! ``` +//! +//! ### TLS 1.2 +//! +//! ```ignore +// //! TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +// //! TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +// //! TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 // Requires the `chacha` feature +//! TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +//! TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +//! TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 // Requires the `chacha` feature +//! ``` +//! ## Supported Key Exchanges +//! +//! Key exchanges are listed below, ordered by preference. IE: `SECP384R1` is preferred over `SECP256R1`. +//! +//! ```ignore +//! SECP384R1 +//! SECP256R1 +//! X25519 // Requires the `x25519` feature +//! ``` +//! +//! ## Usage +//! +//! Add `rustls-openssl` to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! rustls = { version = "0.23.0", features = ["tls12", "std"], default-features = false } +//! rustls_openssl = "0.1.0" +//! ``` +//! +//! ### Default Configuration +//! +//! Use `default_provider()` for a `ClientConfig` that utilizes the default cipher suites and key exchange groups listed above: +//! +//! ```rust +//! use rustls::{ClientConfig, RootCertStore}; +//! use rustls_openssl::default_provider; +//! use std::sync::Arc; +//! use webpki_roots; +//! +//! let mut root_store = RootCertStore { +//! roots: webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(), +//! }; +//! +//! let mut config = +//! ClientConfig::builder_with_provider(Arc::new(default_provider())) +//! .with_safe_default_protocol_versions() +//! .unwrap() +//! .with_root_certificates(root_store) +//! .with_no_client_auth(); +//! ``` +//! +//! ### Custom Configuration +//! +//! To modify or change the order of negotiated cipher suites for `ClientConfig`, use `custom_provider()`. +//! +//! ```rust +//! use rustls::{ClientConfig, RootCertStore}; +//! use rustls_openssl::{custom_provider, TLS13_AES_128_GCM_SHA256, SECP256R1}; +//! use std::sync::Arc; +//! use webpki_roots; +//! +//! let mut root_store = RootCertStore { +//! roots: webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(), +//! }; +//! +//! // Set custom config of cipher suites that have been imported from rustls_openssl. +//! let cipher_suites = vec![TLS13_AES_128_GCM_SHA256]; +//! let kx_group = vec![SECP256R1]; +//! +//! let mut config = +//! ClientConfig::builder_with_provider(Arc::new(custom_provider( +//! Some(cipher_suites), Some(kx_group)))) +//! .with_safe_default_protocol_versions() +//! .unwrap() +//! .with_root_certificates(root_store) +//! .with_no_client_auth(); +//! ``` +//! +//! # Features +//! The following non-default features are available: +//! - `chacha`: Enables ChaCha20-Poly1305 cipher suites for TLS 1.2 and TLS 1.3. +//! - `x25519`: Enables X25519 key exchange group. + +use openssl::rand::rand_bytes; +use rustls::crypto::{ + CryptoProvider, GetRandomFailed, SecureRandom, SupportedKxGroup, WebPkiSupportedAlgorithms, +}; +use rustls::{SignatureScheme, SupportedCipherSuite}; + +mod cipher_suites; +mod ecdh; +mod hash; +mod hmac; +mod signer; +mod tls12; +mod tls13; +mod verify; + +/// Exporting default cipher suites for TLS 1.3 +pub use cipher_suites::{TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384}; + +/// Exporting default cipher suites for TLS 1.2 +pub use cipher_suites::{ + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, +}; + +/// Exporting ChaCha suites for TLS 1.2 and TLS 1.3 +#[cfg(feature = "chacha")] +pub use cipher_suites::{ + TLS13_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, +}; + +/// Exporting default key exchange groups +pub use ecdh::{SECP256R1, SECP384R1}; + +/// Exporting X25519 key exchange group +#[cfg(feature = "x25519")] +pub use ecdh::X25519; + +/// `default_provider` returns a `CryptoProvider` using default and cipher suites. +/// For cipher suites see[`DEFAULT_CIPHER_SUITES`]. +/// +/// Sample usage: +/// ```rust +/// use rustls::{ClientConfig, RootCertStore}; +/// use rustls_openssl::default_provider; +/// use std::sync::Arc; +/// use webpki_roots; +/// +/// let mut root_store = RootCertStore { +/// roots: webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(), +/// }; +/// +/// let mut config = +/// ClientConfig::builder_with_provider(Arc::new(default_provider())) +/// .with_safe_default_protocol_versions() +/// .unwrap() +/// .with_root_certificates(root_store) +/// .with_no_client_auth(); +/// +/// ``` +pub fn default_provider() -> CryptoProvider { + CryptoProvider { + cipher_suites: DEFAULT_CIPHER_SUITES.to_vec(), + kx_groups: ecdh::ALL_KX_GROUPS.to_vec(), + signature_verification_algorithms: SUPPORTED_SIG_ALGS, + secure_random: &Rng, + key_provider: &signer::Provider, + } +} + +/// Create a `CryptoProvider` with specific cipher suites and key exchange groups during setup. +/// +/// `provided_cipher_suites` takes in an optional `Vec<>` of `SupportedCipherSuites` +/// The supplied arguments for `provided_cipher_suite` will be used when when negotiating the TLS cipher suite; +/// and should be placed in preference order, where the first element has highest priority. +/// If `None` or an empty `Vec<>` is provided the [`DEFAULT_CIPHER_SUITES`] will be used instead. +/// +/// `provided_kx_group` takes in an optional `Vec<>` of `SupportedKxGroup` +/// The supplied arguments for `provided_kx_group` will be used when when negotiating the TLS key exchange; +/// and should be placed in preference order, where the first element has highest priority. +/// If `None` or an empty `Vec<>` is provided the default will be used instead. +/// +/// Sample usage: +/// ```rust +/// use rustls::{ClientConfig, RootCertStore}; +/// use rustls_openssl::{custom_provider, TLS13_AES_128_GCM_SHA256, SECP256R1}; +/// use std::sync::Arc; +/// use webpki_roots; +/// +/// let mut root_store = RootCertStore { +/// roots: webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(), +/// }; +/// +/// // Set custom config of cipher suites that have been imported from rustls_openssl. +/// let cipher_suites = vec![TLS13_AES_128_GCM_SHA256]; +/// let kx_group = vec![SECP256R1]; +/// +/// let mut config = +/// ClientConfig::builder_with_provider(Arc::new(custom_provider( +/// Some(cipher_suites), Some(kx_group)))) +/// .with_safe_default_protocol_versions() +/// .unwrap() +/// .with_root_certificates(root_store) +/// .with_no_client_auth(); +/// +/// +/// ``` +pub fn custom_provider( + provided_cipher_suites: Option>, + provided_kx_group: Option>, +) -> CryptoProvider { + let cipher_suites = match provided_cipher_suites { + Some(suites) if !suites.is_empty() => suites, + _ => DEFAULT_CIPHER_SUITES.to_vec(), + }; + + let kx_group = match provided_kx_group { + Some(groups) if !groups.is_empty() => groups, + _ => ecdh::ALL_KX_GROUPS.to_vec(), + }; + + CryptoProvider { + cipher_suites, + kx_groups: kx_group, + signature_verification_algorithms: SUPPORTED_SIG_ALGS, + secure_random: &Rng, + key_provider: &signer::Provider, + } +} + +/// List of supported cipher suites in a preference order. +/// The first element has highest priority when negotiating cipher suites. +/// ```ignore +/// // TLS 1.3 suites +/// TLS13_AES_256_GCM_SHA384 +/// TLS13_AES_128_GCM_SHA256 +/// TLS13_CHACHA20_POLY1305_SHA256 // Enabled with the `chacha` feature +/// // TLS 1.2 suites +/// // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +/// // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +/// // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 // Enabled with the `chacha` feature +/// TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +/// TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +/// TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 // Enabled with the `chacha` feature +/// ``` +pub static DEFAULT_CIPHER_SUITES: &[SupportedCipherSuite] = ALL_CIPHER_SUITES; + +static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ + // TLS 1.3 suites + TLS13_AES_256_GCM_SHA384, + TLS13_AES_128_GCM_SHA256, + #[cfg(feature = "chacha")] + TLS13_CHACHA20_POLY1305_SHA256, + // TLS 1.2 suites + // TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + // TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + // #[cfg(feature = "chacha")] + // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + #[cfg(feature = "chacha")] + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, +]; + +static SUPPORTED_SIG_ALGS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { + all: &[ + // webpki_algs::ECDSA_P256_SHA256, + // webpki_algs::ECDSA_P256_SHA384, + // webpki_algs::ECDSA_P384_SHA256, + // webpki_algs::ECDSA_P384_SHA384, + verify::RSA_PSS_2048_8192_SHA256_LEGACY_KEY, + verify::RSA_PSS_2048_8192_SHA384_LEGACY_KEY, + verify::RSA_PSS_2048_8192_SHA512_LEGACY_KEY, + verify::RSA_PKCS1_2048_8192_SHA256, + verify::RSA_PKCS1_2048_8192_SHA384, + verify::RSA_PKCS1_2048_8192_SHA512, + verify::RSA_PKCS1_3072_8192_SHA384, + ], + mapping: &[ + // Note: for TLS1.2 the curve is not fixed by SignatureScheme. For TLS1.3 it is. + // ( + // SignatureScheme::ECDSA_NISTP384_SHA384, + // &[ + // webpki_algs::ECDSA_P384_SHA384, + // webpki_algs::ECDSA_P256_SHA384, + // ], + // ), + // ( + // SignatureScheme::ECDSA_NISTP256_SHA256, + // &[ + // webpki_algs::ECDSA_P256_SHA256, + // webpki_algs::ECDSA_P384_SHA256, + // ], + // ), + ( + SignatureScheme::RSA_PSS_SHA512, + &[verify::RSA_PSS_2048_8192_SHA512_LEGACY_KEY], + ), + ( + SignatureScheme::RSA_PSS_SHA384, + &[verify::RSA_PSS_2048_8192_SHA384_LEGACY_KEY], + ), + ( + SignatureScheme::RSA_PSS_SHA256, + &[verify::RSA_PSS_2048_8192_SHA256_LEGACY_KEY], + ), + ( + SignatureScheme::RSA_PKCS1_SHA512, + &[verify::RSA_PKCS1_2048_8192_SHA512], + ), + ( + SignatureScheme::RSA_PKCS1_SHA384, + &[verify::RSA_PKCS1_2048_8192_SHA384], + ), + ( + SignatureScheme::RSA_PKCS1_SHA256, + &[verify::RSA_PKCS1_2048_8192_SHA256], + ), + ], +}; +#[derive(Debug)] +struct Rng; + +impl SecureRandom for Rng { + fn fill(&self, buf: &mut [u8]) -> Result<(), GetRandomFailed> { + rand_bytes(buf).map_err(|_| GetRandomFailed) + } +} diff --git a/src/signer.rs b/src/signer.rs new file mode 100644 index 0000000..dc0e681 --- /dev/null +++ b/src/signer.rs @@ -0,0 +1,144 @@ +use openssl::hash::MessageDigest; +use openssl::pkey::{Id, Private}; +use openssl::rsa::Padding; +use rustls::crypto::KeyProvider; +use rustls::pki_types::PrivateKeyDer; +use rustls::sign::SigningKey; +use rustls::{Error, SignatureAlgorithm, SignatureScheme}; +use std::sync::Arc; + +use crate::cipher_suites::TLS12_RSA_SCHEMES; + +#[derive(Debug)] +pub(crate) struct Provider; + +#[derive(Debug)] +struct PKey(Arc>); + +fn rsa_padding(scheme: &SignatureScheme) -> Option { + match scheme { + SignatureScheme::RSA_PKCS1_SHA1 + | SignatureScheme::RSA_PKCS1_SHA256 + | SignatureScheme::RSA_PKCS1_SHA384 + | SignatureScheme::RSA_PKCS1_SHA512 => Some(Padding::PKCS1), + SignatureScheme::RSA_PSS_SHA256 + | SignatureScheme::RSA_PSS_SHA384 + | SignatureScheme::RSA_PSS_SHA512 => Some(Padding::PKCS1_PSS), + _ => None, + } +} + +fn message_digest(scheme: &SignatureScheme) -> Option { + match scheme { + SignatureScheme::RSA_PKCS1_SHA1 => Some(MessageDigest::sha1()), + SignatureScheme::RSA_PKCS1_SHA256 => Some(MessageDigest::sha256()), + SignatureScheme::RSA_PKCS1_SHA384 => Some(MessageDigest::sha384()), + SignatureScheme::RSA_PKCS1_SHA512 => Some(MessageDigest::sha512()), + SignatureScheme::RSA_PSS_SHA256 => Some(MessageDigest::sha256()), + SignatureScheme::RSA_PSS_SHA384 => Some(MessageDigest::sha384()), + SignatureScheme::RSA_PSS_SHA512 => Some(MessageDigest::sha512()), + SignatureScheme::ECDSA_NISTP256_SHA256 => Some(MessageDigest::sha256()), + SignatureScheme::ECDSA_NISTP384_SHA384 => Some(MessageDigest::sha384()), + SignatureScheme::ECDSA_NISTP521_SHA512 => Some(MessageDigest::sha512()), + _ => None, + } +} + +impl PKey { + fn signer(&self, scheme: &SignatureScheme) -> Signer { + Signer { + key: Arc::clone(&self.0), + scheme: *scheme, + } + } +} + +impl KeyProvider for Provider { + fn load_private_key( + &self, + key_der: PrivateKeyDer<'static>, + ) -> Result, Error> { + Ok(Arc::new(PKey(Arc::new( + openssl::pkey::PKey::private_key_from_der(key_der.secret_der()).unwrap(), + )))) + } +} + +impl SigningKey for PKey { + fn choose_scheme(&self, offered: &[SignatureScheme]) -> Option> { + match self.algorithm() { + SignatureAlgorithm::RSA => TLS12_RSA_SCHEMES + .iter() + .find(|scheme| offered.contains(scheme)) + .map(|scheme| Box::new(self.signer(scheme)) as Box), + + SignatureAlgorithm::ED25519 => { + if offered.contains(&SignatureScheme::ED25519) { + Some(Box::new(Signer { + key: Arc::clone(&self.0), + scheme: SignatureScheme::ED25519, + })) + } else { + None + } + } + SignatureAlgorithm::ED448 => { + if offered.contains(&SignatureScheme::ED448) { + Some(Box::new(Signer { + key: Arc::clone(&self.0), + scheme: SignatureScheme::ED448, + })) + } else { + None + } + } + // SignatureAlgorithm::ECDSA => ALL_ECDSA_SCHEMES + // .iter() + // .find(|scheme| offered.contains(scheme)) + // .map(|scheme| { + // Box::new(self.digest_signer(scheme)) as Box + // }), + _ => None, + } + } + + fn algorithm(&self) -> SignatureAlgorithm { + match self.0.id() { + Id::RSA => SignatureAlgorithm::RSA, + Id::EC => SignatureAlgorithm::ECDSA, + Id::ED448 => SignatureAlgorithm::ED448, + Id::ED25519 => SignatureAlgorithm::ED25519, + _ => SignatureAlgorithm::Unknown(self.0.id().as_raw().try_into().unwrap_or_default()), + } + } +} + +#[derive(Debug)] +struct Signer { + key: Arc>, + scheme: SignatureScheme, +} + +impl rustls::sign::Signer for Signer { + fn sign(&self, message: &[u8]) -> Result, Error> { + if let Some(message_digest) = message_digest(&self.scheme) { + openssl::sign::Signer::new(message_digest, &self.key) + .and_then(|mut signer| { + if let Some(padding) = rsa_padding(&self.scheme) { + signer.set_rsa_padding(padding)?; + } + signer.update(message)?; + signer.sign_to_vec() + }) + .map_err(|e| Error::General(format!("OpenSSL error: {}", e))) + } else { + openssl::sign::Signer::new_without_digest(&self.key) + .and_then(|mut signer| signer.sign_oneshot_to_vec(message)) + .map_err(|e| Error::General(format!("OpenSSL error: {}", e))) + } + } + + fn scheme(&self) -> SignatureScheme { + self.scheme + } +} diff --git a/src/tls12.rs b/src/tls12.rs new file mode 100644 index 0000000..43fe534 --- /dev/null +++ b/src/tls12.rs @@ -0,0 +1,337 @@ +use openssl::cipher::{Cipher, CipherRef}; +use openssl::cipher_ctx::CipherCtx; +use rustls::crypto::cipher::{ + make_tls12_aad, AeadKey, InboundOpaqueMessage, InboundPlainMessage, Iv, KeyBlockShape, + MessageDecrypter, MessageEncrypter, Nonce, OutboundOpaqueMessage, OutboundPlainMessage, + PrefixedPayload, Tls12AeadAlgorithm, UnsupportedOperationError, +}; +use rustls::{ConnectionTrafficSecrets, Error}; + +const GCM_FULL_NONCE_LENGTH: usize = 12; +const GCM_EXPLICIT_NONCE_LENGTH: usize = 8; +const GCM_IMPLICIT_NONCE_LENGTH: usize = 4; +const GCM_TAG_LENGTH: usize = 16; + +#[cfg(feature = "chacha")] +const CHACHA_TAG_LENGTH: usize = 16; +#[cfg(feature = "chacha")] +const CHAHCA_NONCE_LENGTH: usize = 12; +#[cfg(feature = "chacha")] +const CHACHA_KEY_LENGTH: usize = 32; + +#[cfg(feature = "chacha")] +pub(crate) struct Tls12ChaCha; + +#[cfg(feature = "chacha")] +pub(crate) struct Tls12ChaCha20Poly1305 { + key: [u8; CHACHA_KEY_LENGTH], + iv: Iv, +} + +#[cfg(feature = "chacha")] +impl Tls12AeadAlgorithm for Tls12ChaCha { + fn encrypter(&self, key: AeadKey, iv: &[u8], _: &[u8]) -> Box { + // The caller ensures that the key is the correct length. + let mut chacha_key = [0u8; CHACHA_KEY_LENGTH]; + chacha_key.copy_from_slice(key.as_ref()); + + Box::new(Tls12ChaCha20Poly1305 { + key: chacha_key, + iv: Iv::copy(iv), + }) + } + + fn decrypter(&self, key: AeadKey, iv: &[u8]) -> Box { + // The caller ensures that the key is the correct length. + let mut chacha_key = [0u8; CHACHA_KEY_LENGTH]; + chacha_key.copy_from_slice(key.as_ref()); + + Box::new(Tls12ChaCha20Poly1305 { + key: chacha_key, + iv: Iv::copy(iv), + }) + } + + fn key_block_shape(&self) -> KeyBlockShape { + KeyBlockShape { + enc_key_len: CHACHA_KEY_LENGTH, + fixed_iv_len: CHAHCA_NONCE_LENGTH, + explicit_nonce_len: 0, + } + } + + fn extract_keys( + &self, + key: AeadKey, + iv: &[u8], + _explicit: &[u8], + ) -> Result { + Ok(ConnectionTrafficSecrets::Chacha20Poly1305 { + key, + iv: Iv::new(iv[..].try_into().unwrap()), + }) + } +} + +#[cfg(feature = "chacha")] +impl MessageEncrypter for Tls12ChaCha20Poly1305 { + fn encrypt( + &mut self, + msg: OutboundPlainMessage, + seq: u64, + ) -> Result { + let total_len = self.encrypted_payload_len(msg.payload.len()); + let mut payload = PrefixedPayload::with_capacity(total_len); + + let mut tag = [0u8; CHACHA_TAG_LENGTH]; + let cipher = Cipher::chacha20_poly1305(); + let nonce = Nonce::new(&self.iv, seq); + let aad = make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len()); + + payload.extend_from_chunks(&msg.payload); + payload.extend_from_slice(&vec![0u8; cipher.block_size()]); + + CipherCtx::new() + .and_then(|mut ctx| { + ctx.encrypt_init(Some(cipher), Some(self.key.as_ref()), Some(&nonce.0))?; + ctx.cipher_update(&aad, None)?; + let count = ctx.cipher_update_inplace(payload.as_mut(), msg.payload.len())?; + let rest = ctx.cipher_final(&mut payload.as_mut()[count..])?; + payload.truncate(count + rest); + ctx.tag(&mut tag)?; + payload.extend_from_slice(&tag); + Ok(OutboundOpaqueMessage::new(msg.typ, msg.version, payload)) + }) + .map_err(|e| rustls::Error::General(format!("OpenSSL error: {}", e))) + } + + fn encrypted_payload_len(&self, payload_len: usize) -> usize { + payload_len + CHACHA_TAG_LENGTH + } +} + +#[cfg(feature = "chacha")] +impl MessageDecrypter for Tls12ChaCha20Poly1305 { + fn decrypt<'a>( + &mut self, + mut msg: InboundOpaqueMessage<'a>, + seq: u64, + ) -> Result, Error> { + let payload = &mut msg.payload; + let payload_len = payload.len(); + if payload_len < CHACHA_TAG_LENGTH { + return Err(Error::DecryptError); + } + let message_len = payload_len - CHACHA_TAG_LENGTH; + + let nonce = Nonce::new(&self.iv, seq); + let aad = make_tls12_aad(seq, msg.typ, msg.version, message_len); + let mut tag = [0u8; CHACHA_TAG_LENGTH]; + tag.copy_from_slice(&payload[message_len..]); + + CipherCtx::new() + .and_then(|mut ctx| { + ctx.decrypt_init( + Some(Cipher::chacha20_poly1305()), + Some(self.key.as_ref()), + Some(&nonce.0), + )?; + ctx.cipher_update(&aad, None).unwrap(); + let count = ctx.cipher_update_inplace(payload, message_len).unwrap(); + ctx.set_tag(&tag).unwrap(); + let rest = ctx.cipher_final(&mut payload[count..]).unwrap(); + payload.truncate(count + rest); + Ok(()) + }) + .map_err(|e| rustls::Error::General(format!("OpenSSL error: {}", e)))?; + Ok(msg.into_plain_message()) + } +} + +#[derive(Debug, Clone)] +pub(crate) enum AesGcm { + Aes128Gcm, + Aes256Gcm, +} + +impl AesGcm { + fn key_size(&self) -> usize { + match self { + AesGcm::Aes128Gcm => 16, + AesGcm::Aes256Gcm => 32, + } + } + + fn openssl_cipher(&self) -> &'static CipherRef { + match self { + AesGcm::Aes128Gcm => Cipher::aes_128_gcm(), + AesGcm::Aes256Gcm => Cipher::aes_256_gcm(), + } + } +} + +pub(crate) struct Tls12Gcm { + pub(crate) algo_type: AesGcm, +} + +pub(crate) struct Gcm12Decrypt { + algo_type: AesGcm, + key: AeadKey, + iv: [u8; GCM_IMPLICIT_NONCE_LENGTH], +} + +pub(crate) struct Gcm12Encrypt { + algo_type: AesGcm, + key: AeadKey, + full_iv: [u8; GCM_FULL_NONCE_LENGTH], +} + +impl Tls12AeadAlgorithm for Tls12Gcm { + fn encrypter(&self, key: AeadKey, iv: &[u8], extra: &[u8]) -> Box { + let mut full_iv = [0u8; GCM_FULL_NONCE_LENGTH]; + full_iv[..GCM_IMPLICIT_NONCE_LENGTH].copy_from_slice(iv); + full_iv[GCM_IMPLICIT_NONCE_LENGTH..].copy_from_slice(extra); + + Box::new(Gcm12Encrypt { + algo_type: self.algo_type.clone(), + key, + full_iv, + }) + } + + fn decrypter(&self, key: AeadKey, iv: &[u8]) -> Box { + let mut implicit_iv = [0u8; GCM_IMPLICIT_NONCE_LENGTH]; + implicit_iv.copy_from_slice(iv); + + Box::new(Gcm12Decrypt { + algo_type: self.algo_type.clone(), + key, + iv: implicit_iv, + }) + } + + fn key_block_shape(&self) -> KeyBlockShape { + KeyBlockShape { + enc_key_len: self.algo_type.key_size(), + fixed_iv_len: GCM_IMPLICIT_NONCE_LENGTH, + explicit_nonce_len: GCM_EXPLICIT_NONCE_LENGTH, + } + } + + fn extract_keys( + &self, + key: AeadKey, + iv: &[u8], + explicit: &[u8], + ) -> Result { + let mut gcm_iv = [0; GCM_FULL_NONCE_LENGTH]; + gcm_iv[..GCM_IMPLICIT_NONCE_LENGTH].copy_from_slice(iv); + gcm_iv[GCM_IMPLICIT_NONCE_LENGTH..].copy_from_slice(explicit); + + match self.algo_type.key_size() { + 16 => Ok(ConnectionTrafficSecrets::Aes128Gcm { + key, + iv: Iv::new(gcm_iv), + }), + 32 => Ok(ConnectionTrafficSecrets::Aes256Gcm { + key, + iv: Iv::new(gcm_iv), + }), + _ => Err(UnsupportedOperationError), + } + } +} + +impl MessageEncrypter for Gcm12Encrypt { + fn encrypt( + &mut self, + msg: OutboundPlainMessage, + seq: u64, + ) -> Result { + let total_len = self.encrypted_payload_len(msg.payload.len()); + let mut payload = PrefixedPayload::with_capacity(total_len); + let cipher = self.algo_type.openssl_cipher(); + + let nonce = Nonce::new(&Iv::copy(&self.full_iv), seq); + payload.extend_from_slice(&nonce.0[GCM_IMPLICIT_NONCE_LENGTH..]); + payload.extend_from_chunks(&msg.payload); + payload.extend_from_slice(&vec![0u8; cipher.block_size()]); + + let mut tag = [0u8; GCM_TAG_LENGTH]; + let aad = make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len()); + + CipherCtx::new() + .and_then(|mut ctx| { + ctx.encrypt_init(Some(cipher), Some(self.key.as_ref()), Some(&nonce.0))?; + ctx.cipher_update(&aad, None).unwrap(); + let count = ctx.cipher_update_inplace( + &mut payload.as_mut()[GCM_EXPLICIT_NONCE_LENGTH..], + msg.payload.len(), + )?; + let rest = ctx.cipher_final(&mut payload.as_mut()[count..])?; + payload.truncate(GCM_EXPLICIT_NONCE_LENGTH + count + rest); + ctx.tag(&mut tag)?; + payload.extend_from_slice(&tag); + + Ok(OutboundOpaqueMessage::new(msg.typ, msg.version, payload)) + }) + .map_err(|e| rustls::Error::General(format!("OpenSSL error: {}", e))) + } + + fn encrypted_payload_len(&self, payload_len: usize) -> usize { + payload_len + GCM_EXPLICIT_NONCE_LENGTH + GCM_TAG_LENGTH + } +} + +impl MessageDecrypter for Gcm12Decrypt { + fn decrypt<'a>( + &mut self, + mut msg: InboundOpaqueMessage<'a>, + seq: u64, + ) -> Result, Error> { + let payload = &mut msg.payload; + let payload_len = payload.len(); + if payload_len < GCM_TAG_LENGTH + GCM_EXPLICIT_NONCE_LENGTH { + return Err(Error::DecryptError); + } + + let cipher = self.algo_type.openssl_cipher(); + let mut nonce = [0u8; GCM_FULL_NONCE_LENGTH]; + nonce[..GCM_IMPLICIT_NONCE_LENGTH].copy_from_slice(&self.iv); + nonce[GCM_IMPLICIT_NONCE_LENGTH..].copy_from_slice(&payload[..GCM_EXPLICIT_NONCE_LENGTH]); + + let mut tag = [0u8; GCM_TAG_LENGTH]; + tag.copy_from_slice(&payload[payload_len - GCM_TAG_LENGTH..]); + let aad = make_tls12_aad( + seq, + msg.typ, + msg.version, + payload_len - GCM_TAG_LENGTH - GCM_EXPLICIT_NONCE_LENGTH, + ); + + // This is more complicated that the others as we have `GCM_EXPLICIT_NONCE_LENGTH` + // bytes at the front that we don't need to decrypt, and nor do we want to include + // in the plaintext. + CipherCtx::new() + .and_then(|mut ctx| { + ctx.decrypt_init(Some(cipher), Some(self.key.as_ref()), Some(&nonce))?; + ctx.cipher_update(&aad, None)?; + let count = ctx.cipher_update_inplace( + &mut payload[GCM_EXPLICIT_NONCE_LENGTH..], + payload_len - GCM_TAG_LENGTH - GCM_EXPLICIT_NONCE_LENGTH, + )?; + ctx.set_tag(&tag)?; + let rest = ctx.cipher_final(&mut payload[GCM_EXPLICIT_NONCE_LENGTH + count..])?; + // copy the decrypted bytes to the front of the buffer + payload.copy_within( + GCM_EXPLICIT_NONCE_LENGTH..(GCM_EXPLICIT_NONCE_LENGTH + count + rest), + 0, + ); + // and remove all but the decrypted bytes + payload.truncate(count + rest); + Ok(()) + }) + .map_err(|e| rustls::Error::General(format!("OpenSSL error: {}", e)))?; + Ok(msg.into_plain_message()) + } +} diff --git a/src/tls13.rs b/src/tls13.rs new file mode 100644 index 0000000..d1f839d --- /dev/null +++ b/src/tls13.rs @@ -0,0 +1,163 @@ +use openssl::cipher::CipherRef; +use openssl::cipher_ctx::CipherCtx; +use rustls::crypto::cipher::{ + make_tls13_aad, AeadKey, InboundOpaqueMessage, InboundPlainMessage, Iv, MessageDecrypter, + MessageEncrypter, Nonce, OutboundOpaqueMessage, OutboundPlainMessage, PrefixedPayload, + Tls13AeadAlgorithm, UnsupportedOperationError, +}; +use rustls::ConnectionTrafficSecrets; + +pub(crate) struct AeadMessageCrypter { + pub algo: AeadAlgorithm, + pub key: AeadKey, + pub iv: Iv, +} + +#[derive(Clone)] +pub(crate) enum AeadAlgorithm { + Aes128Gcm, + Aes256Gcm, + #[cfg(feature = "chacha")] + ChaCha20Poly1305, +} + +impl AeadAlgorithm { + fn openssl_cipher(&self) -> &'static CipherRef { + match self { + AeadAlgorithm::Aes128Gcm => openssl::cipher::Cipher::aes_128_gcm(), + AeadAlgorithm::Aes256Gcm => openssl::cipher::Cipher::aes_256_gcm(), + #[cfg(feature = "chacha")] + AeadAlgorithm::ChaCha20Poly1305 => openssl::cipher::Cipher::chacha20_poly1305(), + } + } + + fn tag_len(&self) -> usize { + match self { + AeadAlgorithm::Aes128Gcm | AeadAlgorithm::Aes256Gcm => 16, + #[cfg(feature = "chacha")] + AeadAlgorithm::ChaCha20Poly1305 => 16, + } + } +} + +impl Tls13AeadAlgorithm for AeadAlgorithm { + fn encrypter(&self, key: AeadKey, iv: Iv) -> Box { + Box::new(AeadMessageCrypter { + algo: self.clone(), + key, + iv, + }) + } + + fn decrypter(&self, key: AeadKey, iv: Iv) -> Box { + Box::new(AeadMessageCrypter { + algo: self.clone(), + key, + iv, + }) + } + + fn key_len(&self) -> usize { + self.openssl_cipher().key_length() + } + + fn extract_keys( + &self, + key: AeadKey, + iv: Iv, + ) -> Result { + Ok(match self { + AeadAlgorithm::Aes128Gcm => ConnectionTrafficSecrets::Aes128Gcm { key, iv }, + AeadAlgorithm::Aes256Gcm => ConnectionTrafficSecrets::Aes256Gcm { key, iv }, + #[cfg(feature = "chacha")] + AeadAlgorithm::ChaCha20Poly1305 => { + ConnectionTrafficSecrets::Chacha20Poly1305 { key, iv } + } + }) + } +} + +impl MessageEncrypter for AeadMessageCrypter { + fn encrypt( + &mut self, + msg: OutboundPlainMessage, + seq: u64, + ) -> Result { + let total_len = self.encrypted_payload_len(msg.payload.len()); + let mut payload = PrefixedPayload::with_capacity(total_len); + let nonce = Nonce::new(&self.iv, seq); + let aad = make_tls13_aad(total_len); + payload.extend_from_chunks(&msg.payload); + payload.extend_from_slice(&msg.typ.to_array()); + + // https://docs.rs/openssl/latest/openssl/cipher_ctx/struct.CipherCtxRef.html#method.cipher_update_inplace + // below requires the buffer to be block_size larger than the actual data + payload.extend_from_slice(&vec![0u8; self.algo.openssl_cipher().block_size()]); + let mut tag = vec![0u8; self.algo.tag_len()]; + + CipherCtx::new() + .and_then(|mut ctx| { + ctx.encrypt_init( + Some(self.algo.openssl_cipher()), + Some(self.key.as_ref()), + Some(&nonce.0), + )?; + // Providing no output buffer implies input is AAD. + ctx.cipher_update(&aad, None)?; + let count = ctx.cipher_update_inplace(payload.as_mut(), msg.payload.len() + 1)?; + let rest = ctx.cipher_final(&mut payload.as_mut()[count..])?; + payload.truncate(count + rest); + ctx.tag(&mut tag)?; + payload.extend_from_slice(&tag); + Ok(OutboundOpaqueMessage::new( + rustls::ContentType::ApplicationData, + // Note: all TLS 1.3 application data records use TLSv1_2 (0x0303) as the legacy record + // protocol version, see https://www.rfc-editor.org/rfc/rfc8446#section-5.1 + rustls::ProtocolVersion::TLSv1_2, + payload, + )) + }) + .map_err(|e| rustls::Error::General(format!("OpeenSSL error: {}", e))) + } + + fn encrypted_payload_len(&self, payload_len: usize) -> usize { + payload_len + 1 + self.algo.tag_len() + } +} + +impl MessageDecrypter for AeadMessageCrypter { + fn decrypt<'a>( + &mut self, + mut msg: InboundOpaqueMessage<'a>, + seq: u64, + ) -> Result, rustls::Error> { + let payload = &mut msg.payload; + if payload.len() < self.algo.tag_len() { + return Err(rustls::Error::DecryptError); + } + + let message_len = payload.len() - self.algo.tag_len(); + let nonce = Nonce::new(&self.iv, seq); + let aad = make_tls13_aad(payload.len()); + let mut tag = vec![0u8; self.algo.tag_len()]; + tag.copy_from_slice(&payload[message_len..]); + + CipherCtx::new() + .and_then(|mut ctx| { + ctx.decrypt_init( + Some(self.algo.openssl_cipher()), + Some(self.key.as_ref()), + Some(&nonce.0), + ) + .unwrap(); + ctx.cipher_update(&aad, None)?; + let count = ctx.cipher_update_inplace(payload, message_len)?; + ctx.set_tag(&tag)?; + let rest = ctx.cipher_final(&mut payload[count..]).unwrap(); + payload.truncate(count + rest); + Ok(()) + }) + .map_err(|e| rustls::Error::General(format!("OpenSSL error: {}", e)))?; + msg.into_tls13_unpadded_message() + } +} diff --git a/src/verify.rs b/src/verify.rs new file mode 100644 index 0000000..2e1cda1 --- /dev/null +++ b/src/verify.rs @@ -0,0 +1,201 @@ +use core::fmt; +use std::ops::RangeInclusive; + +use openssl::{ + hash::MessageDigest, + pkey::{PKey, Public}, + rsa::{Padding, Rsa}, + sign::{RsaPssSaltlen, Verifier}, +}; +use rustls::pki_types::{AlgorithmIdentifier, InvalidSignature, SignatureVerificationAlgorithm}; +use webpki::alg_id; + +/// RSA PKCS#1 1.5 signatures using SHA-256 for keys of 2048-8192 bits. +pub static RSA_PKCS1_2048_8192_SHA256: &dyn SignatureVerificationAlgorithm = &MyAlgorithm { + public_key_alg_id: alg_id::RSA_ENCRYPTION, + signature_alg_id: alg_id::RSA_PKCS1_SHA256, + range: 2048..=8192, +}; + +/// RSA PKCS#1 1.5 signatures using SHA-384 for keys of 2048-8192 bits. +pub static RSA_PKCS1_2048_8192_SHA384: &dyn SignatureVerificationAlgorithm = &MyAlgorithm { + public_key_alg_id: alg_id::RSA_ENCRYPTION, + signature_alg_id: alg_id::RSA_PKCS1_SHA384, + range: 2048..=8192, +}; + +/// RSA PKCS#1 1.5 signatures using SHA-512 for keys of 2048-8192 bits. +pub static RSA_PKCS1_2048_8192_SHA512: &dyn SignatureVerificationAlgorithm = &MyAlgorithm { + public_key_alg_id: alg_id::RSA_ENCRYPTION, + signature_alg_id: alg_id::RSA_PKCS1_SHA512, + range: 2048..=8192, +}; + +/// RSA PKCS#1 1.5 signatures using SHA-384 for keys of 3072-8192 bits. +pub static RSA_PKCS1_3072_8192_SHA384: &dyn SignatureVerificationAlgorithm = &MyAlgorithm { + public_key_alg_id: alg_id::RSA_ENCRYPTION, + signature_alg_id: alg_id::RSA_PKCS1_SHA384, + range: 3072..=8192, +}; + +/// RSA PSS signatures using SHA-256 for keys of 2048-8192 bits and of +/// type rsaEncryption; see [RFC 4055 Section 1.2]. +/// +/// [RFC 4055 Section 1.2]: https://tools.ietf.org/html/rfc4055#section-1.2 +pub static RSA_PSS_2048_8192_SHA256_LEGACY_KEY: &dyn SignatureVerificationAlgorithm = + &MyAlgorithm { + public_key_alg_id: alg_id::RSA_ENCRYPTION, + signature_alg_id: alg_id::RSA_PSS_SHA256, + range: 2048..=8192, + }; + +/// RSA PSS signatures using SHA-384 for keys of 2048-8192 bits and of +/// type rsaEncryption; see [RFC 4055 Section 1.2]. +/// +/// [RFC 4055 Section 1.2]: https://tools.ietf.org/html/rfc4055#section-1.2 +pub static RSA_PSS_2048_8192_SHA384_LEGACY_KEY: &dyn SignatureVerificationAlgorithm = + &MyAlgorithm { + public_key_alg_id: alg_id::RSA_ENCRYPTION, + signature_alg_id: alg_id::RSA_PSS_SHA384, + range: 2048..=8192, + }; + +/// RSA PSS signatures using SHA-512 for keys of 2048-8192 bits and of +/// type rsaEncryption; see [RFC 4055 Section 1.2]. +/// +/// [RFC 4055 Section 1.2]: https://tools.ietf.org/html/rfc4055#section-1.2 +pub static RSA_PSS_2048_8192_SHA512_LEGACY_KEY: &dyn SignatureVerificationAlgorithm = + &MyAlgorithm { + public_key_alg_id: alg_id::RSA_ENCRYPTION, + signature_alg_id: alg_id::RSA_PSS_SHA512, + range: 2048..=8192, + }; + +struct MyAlgorithm { + public_key_alg_id: AlgorithmIdentifier, + signature_alg_id: AlgorithmIdentifier, + range: RangeInclusive, //verification_alg: &'static dyn signature::VerificationAlgorithm, +} + +impl fmt::Debug for MyAlgorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MyAlgorithm") + .field("public_key_alg_id", &self.public_key_alg_id) + .field("signature_alg_id", &self.signature_alg_id) + .finish() + } +} + +impl MyAlgorithm { + fn public_key(&self, public_key: &[u8]) -> Result, InvalidSignature> { + match self.public_key_alg_id { + alg_id::RSA_ENCRYPTION => Rsa::public_key_from_der_pkcs1(public_key) + .and_then(|rsa| rsa.try_into()) + .map_err(|_| InvalidSignature), + _ => Err(InvalidSignature), + } + } + + fn message_digest(&self) -> Result { + match self.signature_alg_id { + alg_id::RSA_PKCS1_SHA256 => Ok(MessageDigest::sha256()), + alg_id::RSA_PKCS1_SHA384 => Ok(MessageDigest::sha384()), + alg_id::RSA_PKCS1_SHA512 => Ok(MessageDigest::sha512()), + alg_id::RSA_PSS_SHA256 => Ok(MessageDigest::sha256()), + alg_id::RSA_PSS_SHA384 => Ok(MessageDigest::sha384()), + alg_id::RSA_PSS_SHA512 => Ok(MessageDigest::sha512()), + _ => Err(InvalidSignature), + } + } + + fn mgf1(&self) -> Option { + match self.signature_alg_id { + alg_id::RSA_PSS_SHA256 => Some(MessageDigest::sha256()), + alg_id::RSA_PSS_SHA384 => Some(MessageDigest::sha384()), + alg_id::RSA_PSS_SHA512 => Some(MessageDigest::sha512()), + _ => None, + } + } + + fn pss_salt_len(&self) -> Option { + match self.signature_alg_id { + alg_id::RSA_PSS_SHA256 => Some(RsaPssSaltlen::DIGEST_LENGTH), + alg_id::RSA_PSS_SHA384 => Some(RsaPssSaltlen::DIGEST_LENGTH), + alg_id::RSA_PSS_SHA512 => Some(RsaPssSaltlen::DIGEST_LENGTH), + _ => None, + } + } + + fn rsa_padding(&self) -> Option { + match self.signature_alg_id { + alg_id::RSA_PSS_SHA512 | alg_id::RSA_PSS_SHA384 | alg_id::RSA_PSS_SHA256 => { + Some(Padding::PKCS1_PSS) + } + alg_id::RSA_PKCS1_SHA512 | alg_id::RSA_PKCS1_SHA384 | alg_id::RSA_PKCS1_SHA256 => { + Some(Padding::PKCS1) + } + _ => None, + } + } +} + +impl SignatureVerificationAlgorithm for MyAlgorithm { + fn public_key_alg_id(&self) -> AlgorithmIdentifier { + self.public_key_alg_id + } + + fn signature_alg_id(&self) -> AlgorithmIdentifier { + self.signature_alg_id + } + + fn verify_signature( + &self, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result<(), InvalidSignature> { + if matches!( + self.public_key_alg_id, + alg_id::ECDSA_P256 | alg_id::ECDSA_P384 | alg_id::ECDSA_P521 + ) { + // Restrict the allowed encodings of EC public keys. + // + // "The first octet of the OCTET STRING indicates whether the key is + // compressed or uncompressed. The uncompressed form is indicated + // by 0x04 and the compressed form is indicated by either 0x02 or + // 0x03 (see 2.3.3 in [SEC1]). The public key MUST be rejected if + // any other value is included in the first octet." + // -- + match public_key.first() { + Some(0x04) | Some(0x02) | Some(0x03) => {} + _ => { + return Err(InvalidSignature); + } + }; + } + let pkey = self.public_key(public_key)?; + + // Check the length is in the range. + if !self.range.contains(&pkey.bits()) { + return Err(InvalidSignature); + } + + let message_digest = self.message_digest()?; + Verifier::new(message_digest, &pkey) + .and_then(|mut verifier| { + if let Some(padding) = self.rsa_padding() { + verifier.set_rsa_padding(padding)?; + } + if let Some(mgf1_md) = self.mgf1() { + verifier.set_rsa_mgf1_md(mgf1_md)?; + } + if let Some(salt_len) = self.pss_salt_len() { + verifier.set_rsa_pss_saltlen(salt_len)?; + } + verifier.update(message)?; + verifier.verify(signature)?; + Ok(()) + }) + .map_err(|e| InvalidSignature) + } +} diff --git a/tests/certs/RootCA.crt b/tests/certs/RootCA.crt new file mode 100644 index 0000000..a003014 --- /dev/null +++ b/tests/certs/RootCA.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUTANmSWnfVeu3082UBM4SZV8e9jAwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +NDAyMjExOTM4MjNaFw0yNjEyMTExOTM4MjNaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCnH5dA166tnxD0tJWiHphWsFK/oc73hHmYwOOxRa+HaR/0QBy7753jFw/o +JueMVfTrNtHoaLUaVwX9W3ATnbpv27JvTNSmoJx2h+XRn7Z7p+yokRj9YjnEu+Qo +PvusGkYj8fh3y5SE6JfEhQa/tOD9Ex2fl+6KS2Va6L5uY8IfbU9/h/SX7AIMKCwr +FlGvPmiavlj/WFSgao6qY4kB4dNjUHmgKMk/hFtPfKu5oEZVBt383cUxWI/CkwnK +WnaFC89vV3UBPTjrixIwNcUpMsksl93ky3SBSeIQSUL19hvs1yJnB8koOpdSmT4O +wqIai3d3YQyRzuRo6rvsbQ/Kag9LAgMBAAGjUzBRMB0GA1UdDgQWBBS1AhH/ACQN +g49LL4LnWAYXsyE4zDAfBgNVHSMEGDAWgBS1AhH/ACQNg49LL4LnWAYXsyE4zDAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCboiZMR8uQuDBMNGw+ +br2N06tHJWXdc80dNoFyHFbgvxYD/thCIG+eOotkFZbM7wds/dGhwSCLmPX6PIVh +//ZVZdD/Dk8B0J0jfbeuRM/kAQAvX+6aZCP3EQ2A6zYmquWSWHLazyWD3aY3sGPX +4vfggNVTCnROS1Rk0i0IzU08z4fDZDfOjDW06x6pUek+7sUR1oVMFMd2Q6CxWCp9 +15J63r0JKmZDG8SOG1iIGTPj4GopDuNnQKA8qJGWc6WwPvJTinCPWMBJ2s73KOwY +hKJQGZ22zULoReHXFY+Dt05vN9iig95OpHHNw+f3jPgwKlPBHhInpjEPNvKFJZ6K +F2kb +-----END CERTIFICATE----- diff --git a/tests/certs/RootCA.key b/tests/certs/RootCA.key new file mode 100644 index 0000000..56af845 --- /dev/null +++ b/tests/certs/RootCA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCnH5dA166tnxD0 +tJWiHphWsFK/oc73hHmYwOOxRa+HaR/0QBy7753jFw/oJueMVfTrNtHoaLUaVwX9 +W3ATnbpv27JvTNSmoJx2h+XRn7Z7p+yokRj9YjnEu+QoPvusGkYj8fh3y5SE6JfE +hQa/tOD9Ex2fl+6KS2Va6L5uY8IfbU9/h/SX7AIMKCwrFlGvPmiavlj/WFSgao6q +Y4kB4dNjUHmgKMk/hFtPfKu5oEZVBt383cUxWI/CkwnKWnaFC89vV3UBPTjrixIw +NcUpMsksl93ky3SBSeIQSUL19hvs1yJnB8koOpdSmT4OwqIai3d3YQyRzuRo6rvs +bQ/Kag9LAgMBAAECggEBAJ0FyY9bFvx6X+wLYCwaoveQY6850MQu7DDhyw1cdDe+ +Rg+vzU+nK6mamY9+PkBU4vG9aCv9dWtyKGaL6xoDMJC400ZP4d1NOrUDqqLydPpq +JKmc6uXnzG9UOmK2CrEBXrWXO+USmlDmWPKEKnsk79/YfhTdI3s8q9Zmp8YAZPww +qwev2QjHc09EfUXr4F3GxwPA/SRDgtjo0kjdGb66B3CmtS1YrABsTyefFFHEsO5w +B39mW/q8q45N+6zAz5gqOpZFESTtr3IAf+eHecyGfSOtuIh2ssRiNF4Cj8Y40eM0 +kFcWVX7YNbveGNouJU8/lktBQmzOKhN4mswnROC8oEECgYEA25KLBivO6jK7NzDh +5LkI6CiENJzMs5Hdyv+cMTwBDJ9VftvTgHz5jc/Fu+0/UfxkdZRebFuE36DgfoDu +4g0RBVeDkfRuAHkrw9/VwksUHUSSG4WU6xzUZZqR/x3oGqG8VWmD+iCt0AaNjfqx +BWBNqEY3MT+9ejys+W9D8URfkY0CgYEAwtl8YSpsd5N7NNv3iMKPaoR8l2AMWRB/ +Nt8qUBt5DAjNbDtx8s/xz0/T9MAijEv94XFR93rCVWMDW2Gl3bxaoenMNNPOowui +HeNApEstXYmWFsJljW/yNPvbU+e3shXUPf3aXVwKA/qkqbD+Bjz3CvsvUipjAWXe +gtficCSVcjcCgYAKNj6RAuiUq9dZMcTPxmtLoNbFO6WplFckYc752ziRRbfMNp0X +lLhmiAtCOj5/qaVicowRrg/39pt6RrTVfpYUEYXk++FB1GDcs0RVzPgahF3nOcc7 +SBP4xb+UheeNlYgU0Nt6fpqW2jcrK0WgYmI6OUnH2JcPYFMLJsmaJvvq4QKBgHy5 +uOt9u4bjigdxEsehOyqE+jfvzJeqfrRCMBStMVPpwo0YlD1IrNH2mIfgAX1rG22X +G0/ebc04nyp8nC8O5bklLolWV7x4suKM2JESakyoyMFy2Iyr7w/JdEEGX8kIPh8c +gw4l32dipsrUuBaIKd8GoOjopw17Bu8cgB8m298LAoGBAKMZrL7GZMM4uInZeCT9 +YYiF3g2WhsGnjB5kbIhy20CPCUtOEMviaziMnvlgQhwUz6ez4dmx05p6zYEMv5EX +57vCTTs31uokmGy+YX61J9DVt2eYjd2qcVKLPBEkgTLBJ8LZK3+yK1HBsVWRs2Cd +2bkIRFDri2sd0pFkAGNyGGRu +-----END PRIVATE KEY----- diff --git a/tests/certs/RootCA.pem b/tests/certs/RootCA.pem new file mode 100644 index 0000000..a003014 --- /dev/null +++ b/tests/certs/RootCA.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUTANmSWnfVeu3082UBM4SZV8e9jAwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +NDAyMjExOTM4MjNaFw0yNjEyMTExOTM4MjNaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCnH5dA166tnxD0tJWiHphWsFK/oc73hHmYwOOxRa+HaR/0QBy7753jFw/o +JueMVfTrNtHoaLUaVwX9W3ATnbpv27JvTNSmoJx2h+XRn7Z7p+yokRj9YjnEu+Qo +PvusGkYj8fh3y5SE6JfEhQa/tOD9Ex2fl+6KS2Va6L5uY8IfbU9/h/SX7AIMKCwr +FlGvPmiavlj/WFSgao6qY4kB4dNjUHmgKMk/hFtPfKu5oEZVBt383cUxWI/CkwnK +WnaFC89vV3UBPTjrixIwNcUpMsksl93ky3SBSeIQSUL19hvs1yJnB8koOpdSmT4O +wqIai3d3YQyRzuRo6rvsbQ/Kag9LAgMBAAGjUzBRMB0GA1UdDgQWBBS1AhH/ACQN +g49LL4LnWAYXsyE4zDAfBgNVHSMEGDAWgBS1AhH/ACQNg49LL4LnWAYXsyE4zDAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCboiZMR8uQuDBMNGw+ +br2N06tHJWXdc80dNoFyHFbgvxYD/thCIG+eOotkFZbM7wds/dGhwSCLmPX6PIVh +//ZVZdD/Dk8B0J0jfbeuRM/kAQAvX+6aZCP3EQ2A6zYmquWSWHLazyWD3aY3sGPX +4vfggNVTCnROS1Rk0i0IzU08z4fDZDfOjDW06x6pUek+7sUR1oVMFMd2Q6CxWCp9 +15J63r0JKmZDG8SOG1iIGTPj4GopDuNnQKA8qJGWc6WwPvJTinCPWMBJ2s73KOwY +hKJQGZ22zULoReHXFY+Dt05vN9iig95OpHHNw+f3jPgwKlPBHhInpjEPNvKFJZ6K +F2kb +-----END CERTIFICATE----- diff --git a/tests/certs/localhost.crt b/tests/certs/localhost.crt new file mode 100644 index 0000000..2eff631 --- /dev/null +++ b/tests/certs/localhost.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgIUVuQWWEJldt7G1ixpk1UwDM2xMw4wDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +NDAyMjExOTM5NDNaFw0yNjEyMTExOTM5NDNaMG0xCzAJBgNVBAYTAlVTMRIwEAYD +VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt +cGxlLUNlcnRpZmljYXRlczEYMBYGA1UEAwwPbG9jYWxob3N0LmxvY2FsMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwnqNmj1kU7mF2CHdk2J+aB+IXu6 +RettCfthQvER1AnehHUnLaMk5ufzuPoc7/7ZW03TdptKBgB4isT8RXwM9usxlvoI +qFYldGD0rlQFfaVfdzFmGlwgloyS8tnQGzr5w6oUDKDClNPAxGIa5RzWiz5nT/zR +7F8+eiELdvrlLB8fTA5N7A3xCI9yywdYDgLlFsREUd6DpLrfdoCGah8xsFl8aU7E +uL/QuROywz/aShGm9WG4cO7biac59/yMB9+ccchtI+PgScdzz/WsfiqaMZNL+XQ5 +iFg9KDJdvB5ZyXtEfQB0f6aX9bbDZ4qlMElCH1tYOyGy7tuy9Cl0AGEfRwIDAQAB +o1EwTzAfBgNVHSMEGDAWgBS1AhH/ACQNg49LL4LnWAYXsyE4zDAJBgNVHRMEAjAA +MAsGA1UdDwQEAwIE8DAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQEL +BQADggEBAFWcr8VYNr+SUQUcAdgrZ5uBjdS9v0oE3co6zuClWhv9P7pKT+QYEe4/ +0DzNWz4BisT4VHUqyV4QAXqIY6qT0cmlHMwFBTcMNTagZ6sAGr0ULcLNAkwTjUkm +xTQecGPE06ynZGxThY8Bd6jQGfrn8Bkpzv5DdM47SQxG5e2k6IeQ1GW7wE6BpjUv +eRGUDAI7UD1R3T3zDDtWFOCz8jFodqC5E6Cy/Np7p2J+5Rhbxl+0ytMtegQY/Mfv +Uvltab20jnZiozF4y27l18iG4SP7h61SJoF02Jc4VVZMAnoqm25ddR2VaoSXjW6i +Tq/gk2MYM/5Ym1iSykAevHovwMMwzB0= +-----END CERTIFICATE----- diff --git a/tests/certs/localhost.key b/tests/certs/localhost.key new file mode 100644 index 0000000..64e2409 --- /dev/null +++ b/tests/certs/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvCeo2aPWRTuYX +YId2TYn5oH4he7pF620J+2FC8RHUCd6EdSctoyTm5/O4+hzv/tlbTdN2m0oGAHiK +xPxFfAz26zGW+gioViV0YPSuVAV9pV93MWYaXCCWjJLy2dAbOvnDqhQMoMKU08DE +YhrlHNaLPmdP/NHsXz56IQt2+uUsHx9MDk3sDfEIj3LLB1gOAuUWxERR3oOkut92 +gIZqHzGwWXxpTsS4v9C5E7LDP9pKEab1Ybhw7tuJpzn3/IwH35xxyG0j4+BJx3PP +9ax+Kpoxk0v5dDmIWD0oMl28HlnJe0R9AHR/ppf1tsNniqUwSUIfW1g7IbLu27L0 +KXQAYR9HAgMBAAECggEADbm2TuvuDaXlLwIXvTQZVKS8Hz4HfkIWu8ileM1Ue48G +jtZs6ww28ZBQsTmTmVIfkyRIJ63HoS7aRO9rZLt0fMw2iEM0+JZAu556sUzPXWnh +UYRjIEAHIicFwttHkUsPmMM2bUMR3v+3xu52c27Od/69tSz6/RD+4i7DKmJEJDBv +O0SJVVxdoam15TTTW1CqnQeomg+C5dNhFAg1uYu+REiddFRjLRJR4xG4FPsqdA1z +1rzGYZlU7I5bW3B8gNpwkRBx4f03NxyqDu3aPK8CBi/NMJumTpq0E1kSW7vODesZ +/5fb8mWUVKQJwFmPWUNQs/Go5D0YOVYlu0YgVU5VqQKBgQDnuekUU10iadZKurkU +4NumGaHULVc0v9Iy6nhIQ8Y5uF0OMOWYIJ9kG1pAgzJ10bHJvhUvqi4fwxdRvo48 +qM8AT4RiXTa0RtZv3sMHcAn1ZHVzYGhwArN+UmMBuPvbj1HJHHYjUETBeHweHRVa +/B/l1IjwoU4lYvTgNSEtmmuMfQKBgQDBX9f7SGdUhYyxjsAcnMmyYDQKiKa+XRlZ +Is7q673ahs/b6oIBuvj5Bo4ZfpuX0ZkbbPgXemaKw1wnTOqaBv6bEAhqf8jb5HXp +6LvTzg2UQL8HdBdZOd/K1ozFbkdqqeO+sUw4dsp2lYU/zE+LhS1/MyRCWB7zMp1K +7d2aXXUaEwKBgQDBhZWOEADb2J/KUR54vUEy+n0YAbWuq/QT6ZUCZPeLBNlSHKvh +3HzA0ccR0X+2vaVI4qI26F0U0Y0MC6QmLKSTkdTxgP9Kl05GpzchYwQuF/Ouo3kU +8myMtqlQqvhLaOnYlxhibYq+OK0PSSKolZ7eBh1HOK9Wscnn5PcMasYe0QKBgB0j +JvUrFL7MnMWIX/Qvv8iL7GuF6bIXbyFaOFl3ihTqaVmWvV4rYSaM0U6QIDvBDlPu +mHdZLyhLhZA6a8MnuKd+w/XgKVDQ3N+Q/PROQQeMtfwWhwofyVPT/kQleMder/1k +07pSU/GIWBqj23yHZbKb7yO8CXXVs5O9wb1nxaRXAoGANjF/qdvNyEZ6RVu1Y1B9 +hT1zV9qUDfb+hdSEaUplijucrIISWNeOXyhLEPcQE/i+2gHYQShHBxN720n0HGlc +L7sT9H4ewsJcLrH00VGZb1LZx497Aiwa86KEiLolNpPEOQ38gSLXxcq4wmLsS2bB +XY84nhn6D6sPQDYhnKR7s0k= +-----END PRIVATE KEY----- diff --git a/tests/certs/localhost.pem b/tests/certs/localhost.pem new file mode 100644 index 0000000..2eff631 --- /dev/null +++ b/tests/certs/localhost.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgIUVuQWWEJldt7G1ixpk1UwDM2xMw4wDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +NDAyMjExOTM5NDNaFw0yNjEyMTExOTM5NDNaMG0xCzAJBgNVBAYTAlVTMRIwEAYD +VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt +cGxlLUNlcnRpZmljYXRlczEYMBYGA1UEAwwPbG9jYWxob3N0LmxvY2FsMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwnqNmj1kU7mF2CHdk2J+aB+IXu6 +RettCfthQvER1AnehHUnLaMk5ufzuPoc7/7ZW03TdptKBgB4isT8RXwM9usxlvoI +qFYldGD0rlQFfaVfdzFmGlwgloyS8tnQGzr5w6oUDKDClNPAxGIa5RzWiz5nT/zR +7F8+eiELdvrlLB8fTA5N7A3xCI9yywdYDgLlFsREUd6DpLrfdoCGah8xsFl8aU7E +uL/QuROywz/aShGm9WG4cO7biac59/yMB9+ccchtI+PgScdzz/WsfiqaMZNL+XQ5 +iFg9KDJdvB5ZyXtEfQB0f6aX9bbDZ4qlMElCH1tYOyGy7tuy9Cl0AGEfRwIDAQAB +o1EwTzAfBgNVHSMEGDAWgBS1AhH/ACQNg49LL4LnWAYXsyE4zDAJBgNVHRMEAjAA +MAsGA1UdDwQEAwIE8DAUBgNVHREEDTALgglsb2NhbGhvc3QwDQYJKoZIhvcNAQEL +BQADggEBAFWcr8VYNr+SUQUcAdgrZ5uBjdS9v0oE3co6zuClWhv9P7pKT+QYEe4/ +0DzNWz4BisT4VHUqyV4QAXqIY6qT0cmlHMwFBTcMNTagZ6sAGr0ULcLNAkwTjUkm +xTQecGPE06ynZGxThY8Bd6jQGfrn8Bkpzv5DdM47SQxG5e2k6IeQ1GW7wE6BpjUv +eRGUDAI7UD1R3T3zDDtWFOCz8jFodqC5E6Cy/Np7p2J+5Rhbxl+0ytMtegQY/Mfv +Uvltab20jnZiozF4y27l18iG4SP7h61SJoF02Jc4VVZMAnoqm25ddR2VaoSXjW6i +Tq/gk2MYM/5Ym1iSykAevHovwMMwzB0= +-----END CERTIFICATE----- diff --git a/tests/it.rs b/tests/it.rs new file mode 100644 index 0000000..6a3fef6 --- /dev/null +++ b/tests/it.rs @@ -0,0 +1,340 @@ +/// Integration tests, based on rustls-symcrypt integration tests +use std::env; +use std::fs::File; +use std::io::{BufReader, Read, Write}; +use std::net::TcpStream; +use std::path::PathBuf; +use std::process::{Child, Command}; +use std::sync::{Arc, Mutex}; +use std::thread; + +use rstest::rstest; +use rustls::crypto::SupportedKxGroup; +use rustls::{CipherSuite, SupportedCipherSuite}; +use rustls_openssl::{ + custom_provider, default_provider, SECP256R1, SECP384R1, TLS13_AES_128_GCM_SHA256, + TLS13_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, +}; +#[cfg(feature = "chacha")] +use rustls_openssl::{TLS13_CHACHA20_POLY1305_SHA256, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}; + +static TEST_CERT_PATH: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("tests"); + path.push("certs"); + path +}); + +// Note: must run with feature flags enabled Ie: +// cargo test --features x25519,chacha + +// Make test function that accepts an array for both + +// Test assumes user has openssl on the machine and is in the PATH. +fn start_openssl_server() -> Child { + // Spawn openssl server + // openssl s_server -accept 4443 -cert localhost.crt -key localhost.key + + let cert_path = TEST_CERT_PATH + .join("localhost.pem") + .into_os_string() + .into_string() + .unwrap(); + let key_path = TEST_CERT_PATH + .join("localhost.key") + .into_os_string() + .into_string() + .unwrap(); + + Command::new("openssl") + .arg("s_server") + .arg("-accept") + .arg("4443") + .arg("-cert") + .arg(cert_path) + .arg("-key") + .arg(key_path) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + .expect("Failed to start OpenSSL server.") +} + +fn test_with_config( + suite: SupportedCipherSuite, + group: &'static dyn SupportedKxGroup, +) -> CipherSuite { + let cipher_suites = vec![suite]; + let kx_group = vec![group]; + + // Add default webpki roots to the root store + let mut root_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }; + + let cert_path = TEST_CERT_PATH + .join("RootCA.pem") + .into_os_string() + .into_string() + .unwrap(); + + let certs = rustls_pemfile::certs(&mut BufReader::new(&mut File::open(cert_path).unwrap())) + .collect::, _>>() + .unwrap(); + + root_store.add_parsable_certificates(certs); + + let config = rustls::ClientConfig::builder_with_provider(Arc::new(custom_provider( + Some(cipher_suites), + Some(kx_group), + ))) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let server_name = "localhost".try_into().unwrap(); + + let mut sock = TcpStream::connect("localhost:4443").unwrap(); + + let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); + let mut tls = rustls::Stream::new(&mut conn, &mut sock); + tls.write_all( + concat!( + "GET / HTTP/1.1\r\n", + "Host: localhost\r\n", + "Connection: close\r\n", + "Accept-Encoding: identity\r\n", + "\r\n" + ) + .as_bytes(), + ) + .unwrap(); + + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + let mut exit_buffer: [u8; 1] = [0]; // Size 1 because "Q" is a single byte command + exit_buffer[0] = b'Q'; // Assign the ASCII value of "Q" to the buffer + + // Write the "Q" command to the TLS connection stream + tls.write_all(&exit_buffer).unwrap(); + ciphersuite.suite() +} + +fn test_with_custom_config_to_internet( + suite: SupportedCipherSuite, + group: &'static dyn SupportedKxGroup, +) -> CipherSuite { + let cipher_suites = vec![suite]; + let kx_group = vec![group]; + + // Add default webpki roots to the root store + let root_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }; + + let config = rustls::ClientConfig::builder_with_provider(Arc::new(custom_provider( + Some(cipher_suites), + Some(kx_group), + ))) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let server_name = "index.crates.io".try_into().unwrap(); + let mut sock = TcpStream::connect("index.crates.io:443").unwrap(); + + let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); + let mut tls = rustls::Stream::new(&mut conn, &mut sock); + + tls.write_all( + concat!( + "GET /config.json HTTP/1.1\r\n", + "Host: index.crates.io\r\n", + "Connection: close\r\n", + "Accept-Encoding: identity\r\n", + "\r\n" + ) + .as_bytes(), + ) + .unwrap(); + + let mut buf = Vec::new(); + tls.read_to_end(&mut buf).unwrap(); + assert!(String::from_utf8_lossy(&buf).contains("https://static.crates.io/crates")); + + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + let mut exit_buffer: [u8; 1] = [0]; // Size 1 because "Q" is a single byte command + exit_buffer[0] = b'Q'; // Assign the ASCII value of "Q" to the buffer + + // Write the "Q" command to the TLS connection stream + tls.write_all(&exit_buffer).unwrap(); + ciphersuite.suite() +} + +#[rstest] +#[case( + TLS13_AES_128_GCM_SHA256, + SECP384R1, + CipherSuite::TLS13_AES_128_GCM_SHA256 +)] +#[case( + TLS13_AES_256_GCM_SHA384, + SECP256R1, + CipherSuite::TLS13_AES_256_GCM_SHA384 +)] +#[cfg_attr( + feature = "chacha", + case( + TLS13_CHACHA20_POLY1305_SHA256, + SECP256R1, + CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 + ) +)] +#[case( + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + SECP256R1, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +)] +#[case( + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + SECP256R1, + CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +)] +#[cfg_attr( + feature = "x25519", + case( + TLS13_AES_256_GCM_SHA384, + rustls_openssl::X25519, + CipherSuite::TLS13_AES_256_GCM_SHA384 + ) +)] +#[case( + TLS13_AES_256_GCM_SHA384, + SECP384R1, + CipherSuite::TLS13_AES_256_GCM_SHA384 +)] +// #[case( +// TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, +// SECP384R1, +// CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 +// )] +fn test_tls( + #[case] suite: SupportedCipherSuite, + #[case] group: &'static dyn SupportedKxGroup, + #[case] expected: CipherSuite, +) { + let server_thread = { + let openssl_server = Arc::new(Mutex::new(start_openssl_server())); + thread::spawn(move || { + openssl_server + .lock() + .unwrap() + .wait() + .expect("OpenSSL server crashed unexpectedly"); + }) + }; + + // Wait for the server to start + thread::sleep(std::time::Duration::from_secs(1)); + + let actual_suite = test_with_config(suite, group); + assert_eq!(actual_suite, expected); + drop(server_thread); +} + +#[rstest] +#[cfg_attr( + feature = "chacha", + case( + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + SECP384R1, + CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + ) +)] +#[case( + TLS13_AES_256_GCM_SHA384, + SECP384R1, + CipherSuite::TLS13_AES_256_GCM_SHA384 +)] +fn test_to_internet( + #[case] suite: SupportedCipherSuite, + #[case] group: &'static dyn SupportedKxGroup, + #[case] expected: CipherSuite, +) { + let actual = test_with_custom_config_to_internet(suite, group); + assert_eq!(actual, expected); +} + +#[test] +fn test_default_client() { + // Spawn a concurrent thread that starts the server + let server_thread = { + let openssl_server = Arc::new(Mutex::new(start_openssl_server())); + thread::spawn(move || { + openssl_server + .lock() + .unwrap() + .wait() + .expect("OpenSSL server crashed unexpectedly"); + }) + }; + + // Wait for the server to start + thread::sleep(std::time::Duration::from_secs(1)); + + // Add default webpki roots to the root store + let mut root_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), + }; + + let cert_path = TEST_CERT_PATH + .join("RootCA.pem") + .into_os_string() + .into_string() + .unwrap(); + + let certs = rustls_pemfile::certs(&mut BufReader::new(&mut File::open(cert_path).unwrap())) + .collect::, _>>() + .unwrap(); + + root_store.add_parsable_certificates(certs); + + let config = rustls::ClientConfig::builder_with_provider(Arc::new(default_provider())) + .with_safe_default_protocol_versions() + .unwrap() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let server_name = "localhost".try_into().unwrap(); + + let mut sock = TcpStream::connect("localhost:4443").unwrap(); + + let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); + let mut tls = rustls::Stream::new(&mut conn, &mut sock); + tls.write_all( + concat!( + "GET / HTTP/1.1\r\n", + "Host: localhost\r\n", + "Connection: close\r\n", + "Accept-Encoding: identity\r\n", + "\r\n" + ) + .as_bytes(), + ) + .unwrap(); + + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + + let mut exit_buffer: [u8; 1] = [0]; // Size 1 because "Q" is a single byte command + exit_buffer[0] = b'Q'; // Assign the ASCII value of "Q" to the buffer + + // Write the "Q" command to the TLS connection stream + tls.write_all(&exit_buffer).unwrap(); + + assert_eq!(ciphersuite.suite(), CipherSuite::TLS13_AES_256_GCM_SHA384); + drop(server_thread); +}