diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da5aac6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/target +/.vscode +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4565544 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +members = [ + # the main library and tests + "boring-rustls-provider", + # tests and example code + "examples", +] +default-members = [ + "examples", + "boring-rustls-provider", +] + +resolver = "2" + +[workspace.dependencies] +rustls = { version = "=0.22.0-alpha.4", default-features = false } diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..8b34586 --- /dev/null +++ b/Readme.md @@ -0,0 +1,55 @@ +# boring-rustls-provider + +This is supposed to be the start to a [boringssl](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. + +## Status +This is just a dump of me figuring out how to interface with boring and rustls. +It works to establish a connection and exchange data but I haven't written real tests yet, nor did I cleanup the code or made the effort to make it look nice. +There is probably some code in here that should rather live in the `boring` crate. + +Further, the rustls crypto provider API is still not stable it seems. This works currently with `rustls = 0.22.0-alpha.4`. + +### Supported ciphers +Currently, supports only TLS 1.3: +``` +AES_128_GCM_SHA256 +AES_256_GCM_SHA256 +CHACHA20_POLY1305_SHA256 +``` + +### Key Exchange Algorithms + +`ECDHE` with curves: +``` +X25519 +X448 +secp256r1 +secp384r1 +secp521r1 +``` + + +`FFDHE` with: +``` +ffdhe2048 +``` + +### Signature Generation / Verification + +``` +RSA_PKCS1_SHA256, +RSA_PKCS1_SHA384, +RSA_PKCS1_SHA512, +RSA_PSS_SHA256, +RSA_PSS_SHA384 +RSA_PSS_SHA512 +ECDSA_NISTP256_SHA256 +ECDSA_NISTP384_SHA384 +ECDSA_NISTP521_SHA512 +ED25519 +ED448 +``` + + +## License +MIT diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml new file mode 100644 index 0000000..0bc5487 --- /dev/null +++ b/boring-rustls-provider/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "boring-rustls-provider" +version = "0.0.1" +authors = ["Jan RĂ¼th "] +edition = "2021" +license = "MIT" +description = "Boringssl rustls provider" +publish = false + +[features] +default = [] +# Use a FIPS-validated version of boringssl. +#fips = ["boring/fips", "boring-sys/fips"] +logging = ["log"] + + +[dependencies] +aead = {version = "0.5", default_features = false, features = ["alloc"] } +boring = { version = "4.0", default-features = false } +boring-sys = { version = "4.0", default-features = false } +foreign-types = "0.5" +lazy_static = "1.4" +log = { version = "0.4.4", optional = true } +once_cell = "1" +rustls = { workspace = true } +rustls-pki-types = "0.2" +spki = "0.7" +webpki = { package = "rustls-webpki", version = "0.102.0-alpha.1", default-features = false, features = ["alloc", "std"] } + +[dev-dependencies] +hex-literal = "0.4" + + + diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs new file mode 100644 index 0000000..6605e23 --- /dev/null +++ b/boring-rustls-provider/src/aead.rs @@ -0,0 +1,177 @@ +use std::marker::PhantomData; + +use aead::{AeadCore, AeadInPlace, Nonce, Tag}; +use boring::error::ErrorStack; +use rustls::crypto::cipher::{self, make_tls13_aad, Iv}; +use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion}; + +use crate::helper::error_stack_to_aead_error; + +use self::aead2::Algorithm; + +pub(crate) mod aead2; +pub(crate) mod aes; +pub(crate) mod chacha20; + +pub(crate) trait BoringCipher { + fn new() -> Algorithm; + fn key_size() -> usize; + fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets; +} + +pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {} + +pub(crate) struct BoringAeadCrypter { + crypter: aead2::Crypter, + iv: Iv, + phantom: PhantomData, +} + +unsafe impl Sync for BoringAeadCrypter {} +unsafe impl Send for BoringAeadCrypter {} + +impl AeadCore for BoringAeadCrypter { + // inherit all properties from the Algorithm + + type NonceSize = T::NonceSize; + + type TagSize = T::TagSize; + + type CiphertextOverhead = T::CiphertextOverhead; +} + +impl BoringAeadCrypter { + pub fn new(iv: Iv, key: &[u8]) -> Result { + let cipher = ::new(); + + assert_eq!( + cipher.nonce_len(), + rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() + ); + + let crypter = BoringAeadCrypter { + crypter: aead2::Crypter::new(cipher, key)?, + iv, + phantom: PhantomData, + }; + Ok(crypter) + } +} + +impl aead::AeadInPlace for BoringAeadCrypter { + fn encrypt_in_place_detached( + &self, + nonce: &Nonce, + associated_data: &[u8], + buffer: &mut [u8], + ) -> aead::Result> { + let mut tag = Tag::::default(); + self.crypter + .seal_in_place(&nonce, &associated_data, buffer, &mut tag) + .map_err(|e| error_stack_to_aead_error("seal_in_place", e))?; + + Ok(tag) + } + + fn decrypt_in_place_detached( + &self, + nonce: &Nonce, + associated_data: &[u8], + buffer: &mut [u8], + tag: &Tag, + ) -> aead::Result<()> { + self.crypter + .open_in_place(&nonce, &associated_data, buffer, tag) + .map_err(|e| error_stack_to_aead_error("open_in_place", e))?; + Ok(()) + } +} + +impl cipher::MessageEncrypter for BoringAeadCrypter +where + T: BoringAead, +{ + fn encrypt( + &self, + msg: cipher::BorrowedPlainMessage, + seq: u64, + ) -> Result { + let total_len = msg.payload.len() + 1 + self.crypter.max_overhead(); + + let mut payload = Vec::with_capacity(total_len); + payload.extend_from_slice(msg.payload); + payload.push(msg.typ.get_u8()); + + let nonce = cipher::Nonce::new(&self.iv, seq); + + let aad = cipher::make_tls13_aad(total_len); + self.encrypt_in_place(Nonce::::from_slice(&nonce.0), &aad, &mut payload) + .map_err(|_| rustls::Error::EncryptError) + .map(|_| { + cipher::OpaqueMessage::new( + ContentType::ApplicationData, + ProtocolVersion::TLSv1_2, + payload, + ) + }) + } +} + +impl cipher::MessageDecrypter for BoringAeadCrypter +where + T: BoringAead, +{ + fn decrypt( + &self, + mut m: cipher::OpaqueMessage, + seq: u64, + ) -> Result { + let payload = m.payload_mut(); + + // construct nonce + let nonce = cipher::Nonce::new(&self.iv, seq); + + // construct the aad + let aad = make_tls13_aad(payload.len()); + + // decrypt on clone to ensure this can be done in parallel + self.decrypt_in_place(Nonce::::from_slice(&nonce.0), &aad, payload) + .map_err(|_| rustls::Error::DecryptError) + .and_then(|_| m.into_tls13_unpadded_message()) + } +} + +pub(crate) struct Aead(PhantomData); + +unsafe impl Sync for Aead {} +unsafe impl Send for Aead {} + +impl Aead { + pub const DEFAULT: Self = Self(PhantomData); +} + +impl cipher::Tls13AeadAlgorithm for Aead { + fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box { + Box::new( + BoringAeadCrypter::::new(iv, key.as_ref()).expect("failed to create AEAD crypter"), + ) + } + + fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box { + Box::new( + BoringAeadCrypter::::new(iv, key.as_ref()).expect("failed to create AEAD crypter"), + ) + } + + fn key_len(&self) -> usize { + ::key_size() + } + + fn extract_keys( + &self, + key: cipher::AeadKey, + iv: cipher::Iv, + ) -> Result { + Ok(::extract_keys(key, iv)) + } +} diff --git a/boring-rustls-provider/src/aead/aead2.rs b/boring-rustls-provider/src/aead/aead2.rs new file mode 100644 index 0000000..6ad4594 --- /dev/null +++ b/boring-rustls-provider/src/aead/aead2.rs @@ -0,0 +1,187 @@ +use std::ptr; + +use boring::error::ErrorStack; + +use crate::helper::{cvt, cvt_p}; + +pub struct Algorithm(*const boring_sys::EVP_AEAD); + +impl Algorithm { + /// AES-128 in Galois Counter Mode. + /// + /// Note: AES-GCM should only be used with 12-byte (96-bit) nonces. Although it is specified to take a variable-length nonce, nonces with other lengths are effectively randomized, which means one must consider collisions. Unless implementing an existing protocol which has already specified incorrect parameters, only use 12-byte nonces. + pub fn aes_128_gcm() -> Self { + Self(unsafe { boring_sys::EVP_aead_aes_128_gcm() }) + } + + /// AES-256 in Galois Counter Mode. + /// + /// Note: AES-GCM should only be used with 12-byte (96-bit) nonces. Although it is specified to take a variable-length nonce, nonces with other lengths are effectively randomized, which means one must consider collisions. Unless implementing an existing protocol which has already specified incorrect parameters, only use 12-byte nonces. + pub fn aes_256_gcm() -> Self { + Self(unsafe { boring_sys::EVP_aead_aes_256_gcm() }) + } + + /// ChaCha20 and Poly1305 as described in RFC 8439. + pub fn chacha20_poly1305() -> Self { + Self(unsafe { boring_sys::EVP_aead_chacha20_poly1305() }) + } + + /// ChaCha20-Poly1305 with an extended nonce that makes random generation of nonces safe. + #[allow(unused)] + pub fn xchacha20_poly1305() -> Self { + Self(unsafe { boring_sys::EVP_aead_xchacha20_poly1305() }) + } + + /// Returns the length, in bytes, of the keys used by aead + pub fn key_length(&self) -> usize { + unsafe { boring_sys::EVP_AEAD_key_length(self.0) } + } + + /// Returns the maximum number of additional bytes added by the act of sealing data with aead. + pub fn max_overhead(&self) -> usize { + unsafe { boring_sys::EVP_AEAD_max_overhead(self.0) } + } + + /// Returns the maximum tag length when using aead. + #[allow(unused)] + pub fn max_tag_len(&self) -> usize { + unsafe { boring_sys::EVP_AEAD_max_tag_len(self.0) } + } + + /// Returns the length, in bytes, of the per-message nonce for aead. + pub fn nonce_len(&self) -> usize { + unsafe { boring_sys::EVP_AEAD_nonce_length(self.0) } + } +} + +pub struct Crypter { + ctx: *mut boring_sys::EVP_AEAD_CTX, + max_overhead: usize, + nonce_len: usize, +} + +unsafe impl Send for Crypter {} +unsafe impl Sync for Crypter {} + +impl Crypter { + pub fn new(aead_alg: Algorithm, key: &[u8]) -> Result { + assert_eq!(aead_alg.key_length(), key.len()); + boring_sys::init(); + + let this = unsafe { + Self { + ctx: cvt_p(boring_sys::EVP_AEAD_CTX_new( + aead_alg.0, + key.as_ptr(), + key.len(), + boring_sys::EVP_AEAD_DEFAULT_TAG_LENGTH as usize, + ))?, + max_overhead: aead_alg.max_overhead(), + nonce_len: aead_alg.nonce_len(), + } + }; + + Ok(this) + } + + pub fn max_overhead(&self) -> usize { + self.max_overhead + } + + /// Encrypts and authenticates buffer and authenticates associated_data. + /// It writes the ciphertext to buffer and the authentication tag to tag. + /// On success, it returns the actual length of the tag + pub fn seal_in_place( + &self, + nonce: &[u8], + associated_data: &[u8], + buffer: &mut [u8], + tag: &mut [u8], + ) -> Result { + assert!(tag.len() >= self.max_overhead); + assert_eq!(nonce.len(), self.nonce_len); + + let mut tag_len = tag.len(); + unsafe { + cvt(boring_sys::EVP_AEAD_CTX_seal_scatter( + self.ctx, + buffer.as_mut_ptr(), + tag.as_mut_ptr(), + &mut tag_len, + tag.len(), + nonce.as_ptr(), + nonce.len(), + buffer.as_ptr(), + buffer.len(), + ptr::null_mut(), + 0, + associated_data.as_ptr(), + associated_data.len(), + ))?; + } + Ok(tag_len) + } + + pub fn open_in_place( + &self, + nonce: &[u8], + associated_data: &[u8], + buffer: &mut [u8], + tag: &[u8], + ) -> Result<(), ErrorStack> { + assert_eq!(nonce.len(), self.nonce_len); + + unsafe { + cvt(boring_sys::EVP_AEAD_CTX_open_gather( + self.ctx, + buffer.as_mut_ptr(), + nonce.as_ptr(), + nonce.len(), + buffer.as_ptr(), + buffer.len(), + tag.as_ptr(), + tag.len(), + associated_data.as_ptr(), + associated_data.len(), + ))?; + } + Ok(()) + } +} + +impl Drop for Crypter { + fn drop(&mut self) { + unsafe { + boring_sys::EVP_AEAD_CTX_free(self.ctx); + } + } +} + +#[cfg(test)] +mod tests { + use super::Crypter; + + #[test] + fn in_out() { + let key = Crypter::new(super::Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); + let nonce = [0u8; 12]; + let associated_data = "this is signed".as_bytes(); + let mut buffer = Vec::with_capacity(26); + buffer.push('A' as u8); + buffer.push('B' as u8); + buffer.push('C' as u8); + buffer.push('D' as u8); + buffer.push('E' as u8); + + let mut tag = [0u8; 16]; + key.seal_in_place(&nonce, &associated_data, buffer.as_mut_slice(), &mut tag) + .unwrap(); + + println!("Encrypted: {:02X?}, Tag: {:02X?}", buffer, tag); + + key.open_in_place(&nonce, &associated_data, buffer.as_mut_slice(), &tag[..]) + .unwrap(); + + println!("Plaintext: {}", String::from_utf8(buffer).unwrap()); + } +} diff --git a/boring-rustls-provider/src/aead/aes.rs b/boring-rustls-provider/src/aead/aes.rs new file mode 100644 index 0000000..ea3acf4 --- /dev/null +++ b/boring-rustls-provider/src/aead/aes.rs @@ -0,0 +1,90 @@ +use aead::consts::{U12, U16}; +use rustls::{crypto::cipher, ConnectionTrafficSecrets}; + +use super::{aead2::Algorithm, BoringAead, BoringCipher}; + +pub struct Aes128 {} + +impl BoringAead for Aes128 {} +unsafe impl Send for Aes128 {} +unsafe impl Sync for Aes128 {} + +impl BoringCipher for Aes128 { + fn new() -> Algorithm { + Algorithm::aes_128_gcm() + } + + fn key_size() -> usize { + 16 + } + + fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets { + ConnectionTrafficSecrets::Aes128Gcm { key, iv } + } +} + +impl aead::AeadCore for Aes128 { + type NonceSize = U12; + type TagSize = U16; + type CiphertextOverhead = U16; +} + +pub struct Aes256 {} + +impl BoringAead for Aes256 {} +unsafe impl Send for Aes256 {} +unsafe impl Sync for Aes256 {} + +impl BoringCipher for Aes256 { + fn new() -> Algorithm { + Algorithm::aes_256_gcm() + } + + fn key_size() -> usize { + 32 + } + + fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets { + ConnectionTrafficSecrets::Aes256Gcm { key, iv } + } +} + +impl aead::AeadCore for Aes256 { + type NonceSize = U12; + type TagSize = U16; + type CiphertextOverhead = U16; +} + +#[cfg(test)] +mod tests { + use aead::{generic_array::GenericArray, AeadCore, Nonce, Tag}; + + use crate::aead::{ + aes::{Aes128, Aes256}, + BoringCipher, + }; + + #[test] + fn ensure_aes128_aead_core() { + let alg = Aes128::new(); + let nonce = Nonce::::default(); + assert_eq!(nonce.len(), alg.nonce_len()); + let tag = Tag::::default(); + assert_eq!(alg.max_tag_len(), tag.len()); + + let overhead = GenericArray::::CiphertextOverhead>::default(); + assert_eq!(alg.max_overhead(), overhead.len()); + } + + #[test] + fn ensure_aes256_aead_core() { + let alg = Aes256::new(); + let nonce = Nonce::::default(); + assert_eq!(nonce.len(), alg.nonce_len()); + let tag = Tag::::default(); + assert_eq!(alg.max_tag_len(), tag.len()); + + let overhead = GenericArray::::CiphertextOverhead>::default(); + assert_eq!(alg.max_overhead(), overhead.len()); + } +} diff --git a/boring-rustls-provider/src/aead/chacha20.rs b/boring-rustls-provider/src/aead/chacha20.rs new file mode 100644 index 0000000..efc3cd7 --- /dev/null +++ b/boring-rustls-provider/src/aead/chacha20.rs @@ -0,0 +1,56 @@ +use aead::{ + consts::{U12, U16}, + AeadCore, +}; +use rustls::{crypto::cipher, ConnectionTrafficSecrets}; + +use super::{aead2::Algorithm, BoringAead, BoringCipher}; + +pub struct ChaCha20Poly1305 {} + +impl BoringAead for ChaCha20Poly1305 {} +unsafe impl Send for ChaCha20Poly1305 {} +unsafe impl Sync for ChaCha20Poly1305 {} + +impl BoringCipher for ChaCha20Poly1305 { + fn new() -> Algorithm { + Algorithm::chacha20_poly1305() + } + + fn key_size() -> usize { + 32 + } + + fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets { + ConnectionTrafficSecrets::Chacha20Poly1305 { key, iv } + } +} + +impl AeadCore for ChaCha20Poly1305 { + type NonceSize = U12; + + type TagSize = U16; + + type CiphertextOverhead = U16; +} + +#[cfg(test)] +mod tests { + use aead::{generic_array::GenericArray, AeadCore, Nonce, Tag}; + + use super::ChaCha20Poly1305; + use crate::aead::BoringCipher; + + #[test] + fn ensure_aead_core() { + let alg = ChaCha20Poly1305::new(); + let nonce = Nonce::::default(); + assert_eq!(nonce.len(), alg.nonce_len()); + let tag = Tag::::default(); + assert_eq!(alg.max_tag_len(), tag.len()); + + let overhead = + GenericArray::::CiphertextOverhead>::default(); + assert_eq!(alg.max_overhead(), overhead.len()); + } +} diff --git a/boring-rustls-provider/src/hash.rs b/boring-rustls-provider/src/hash.rs new file mode 100644 index 0000000..fefaab0 --- /dev/null +++ b/boring-rustls-provider/src/hash.rs @@ -0,0 +1,70 @@ +use rustls::crypto::hash; + +pub const SHA256: &dyn hash::Hash = &Hash(boring::nid::Nid::SHA256); + +pub struct Hash(pub boring::nid::Nid); + +impl hash::Hash for Hash { + fn start(&self) -> Box { + let digest = boring::hash::MessageDigest::from_nid(self.0).unwrap(); + let hasher = boring::hash::Hasher::new(digest).unwrap(); + Box::new(HasherContext(hasher)) + } + + fn hash(&self, data: &[u8]) -> hash::Output { + let mut hasher = self.start(); + hasher.update(data); + hasher.finish() + } + + fn algorithm(&self) -> hash::HashAlgorithm { + match self.0 { + boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256, + _ => unimplemented!(), + } + } + + fn output_len(&self) -> usize { + boring::hash::MessageDigest::from_nid(self.0) + .unwrap() + .size() + } +} + +struct HasherContext(boring::hash::Hasher); + +impl hash::Context for HasherContext { + fn fork_finish(&self) -> hash::Output { + let mut cloned = self.0.clone(); + + hash::Output::new(&cloned.finish().unwrap()[..]) + } + + fn fork(&self) -> Box { + Box::new(HasherContext(self.0.clone())) + } + + fn finish(mut self: Box) -> hash::Output { + hash::Output::new(&self.0.finish().unwrap()[..]) + } + + fn update(&mut self, data: &[u8]) { + self.0.update(data).unwrap(); + } +} + +#[cfg(test)] +mod tests { + use super::SHA256; + use hex_literal::hex; + + #[test] + fn test_sha256() { + let hash = SHA256.hash("test".as_bytes()); + + assert_eq!( + hash.as_ref(), + hex!("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08") + ); + } +} diff --git a/boring-rustls-provider/src/helper.rs b/boring-rustls-provider/src/helper.rs new file mode 100644 index 0000000..0fbf942 --- /dev/null +++ b/boring-rustls-provider/src/helper.rs @@ -0,0 +1,42 @@ +use std::os::raw::c_int; + +use boring::error::ErrorStack; +#[cfg(feature = "log")] +use log::trace; + +/// Check the value returned from a BoringSSL ffi call +/// that returns a pointer. +/// +/// If the pointer is null, this method returns the BoringSSL +/// ErrorStack as Err, the pointer otherwise. +pub(crate) fn cvt_p(r: *mut T) -> Result<*mut T, ErrorStack> { + if r.is_null() { + Err(ErrorStack::get()) + } else { + Ok(r) + } +} + +/// Check the value returned from a BoringSSL ffi call that +/// returns a integer. +/// +/// Returns the BoringSSL Errorstack when the result is <= 0. +/// And forwards the return code otherwise +pub(crate) fn cvt(r: c_int) -> Result { + if r <= 0 { + Err(ErrorStack::get()) + } else { + Ok(r) + } +} + +#[cfg(feature = "log")] +pub(crate) fn error_stack_to_aead_error(func: &'static str, e: ErrorStack) -> aead::Error { + trace!("failed {}, error: {}", func, e); + aead::Error +} + +#[cfg(not(feature = "log"))] +pub(crate) fn error_stack_to_aead_error(_: &'static str, _: ErrorStack) -> aead::Error { + aead::Error +} diff --git a/boring-rustls-provider/src/hkdf.rs b/boring-rustls-provider/src/hkdf.rs new file mode 100644 index 0000000..3654438 --- /dev/null +++ b/boring-rustls-provider/src/hkdf.rs @@ -0,0 +1,184 @@ +use std::marker::PhantomData; + +use rustls::crypto::tls13::{self, Hkdf as RustlsHkdf}; + +use crate::helper::{cvt, cvt_p}; + +pub trait BoringHash: Send + Sync { + fn new() -> boring::hash::MessageDigest; +} + +pub struct Sha256(); +impl BoringHash for Sha256 { + fn new() -> boring::hash::MessageDigest { + boring::hash::MessageDigest::sha256() + } +} + +pub struct Hkdf(PhantomData); + +impl Hkdf { + pub const DEFAULT: Self = Self(PhantomData); +} + +impl RustlsHkdf for Hkdf { + /// `HKDF-Extract(salt, 0_HashLen)` + /// + /// `0_HashLen` is a string of `HashLen` zero bytes. + /// + /// A `salt` of `None` should be treated as a sequence of `HashLen` zero bytes. + fn extract_from_zero_ikm( + &self, + salt: Option<&[u8]>, + ) -> Box { + let hash_size = T::new().size(); + + let secret = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let secret_len = hash_size; + + self.extract_from_secret(salt, &secret[..secret_len]) + } + + /// `HKDF-Extract(salt, secret)` + /// + /// A `salt` of `None` should be treated as a sequence of `HashLen` zero bytes. + fn extract_from_secret( + &self, + salt: Option<&[u8]>, + secret: &[u8], + ) -> Box { + let digest = T::new(); + let hash_size = digest.size(); + + let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let mut prk_len = 0; + + // if salt isn't set we usen these bytes here as salt + let salt_bytes = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + + let salt = if let Some(salt) = salt { + salt + } else { + &salt_bytes[..hash_size] + }; + + unsafe { + cvt(boring_sys::HKDF_extract( + prk.as_mut_ptr(), + &mut prk_len, + digest.as_ptr(), + secret.as_ptr(), + secret.len(), + salt.as_ptr(), + salt.len(), + )) + .expect("HKDF_extract failed"); + } + Box::new(HkdfExpander { + prk, + prk_len, + digest, + }) + } + + fn expander_for_okm( + &self, + okm: &rustls::crypto::tls13::OkmBlock, + ) -> Box { + let okm = okm.as_ref(); + let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let prk_len = okm.len(); + + prk[..prk_len].copy_from_slice(okm); + + Box::new(HkdfExpander { + prk, + prk_len, + digest: T::new(), + }) + } + + fn hmac_sign( + &self, + key: &rustls::crypto::tls13::OkmBlock, + message: &[u8], + ) -> rustls::crypto::hmac::Tag { + let digest = T::new(); + let mut hash = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let mut hash_len = 0u32; + unsafe { + cvt_p(boring_sys::HMAC( + digest.as_ptr(), + key.as_ref().as_ptr() as _, + key.as_ref().len(), + message.as_ptr(), + message.len(), + hash.as_mut_ptr(), + &mut hash_len, + )) + .expect("HMAC failed"); + } + rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize]) + } +} + +pub struct HkdfExpander { + prk: [u8; boring_sys::EVP_MAX_MD_SIZE as usize], + prk_len: usize, + digest: boring::hash::MessageDigest, +} + +impl tls13::HkdfExpander for HkdfExpander { + /// `HKDF-Expand(PRK, info, L)` into a slice. + /// + /// Where: + /// + /// - `PRK` is the implicit key material represented by this instance. + /// - `L` is `output.len()`. + /// - `info` is a slice of byte slices, which should be processed sequentially + /// (or concatenated if that is not possible). + /// + /// Returns `Err(OutputLengthError)` if `L` is larger than `255 * HashLen`. + /// Otherwise, writes to `output`. + fn expand_slice( + &self, + info: &[&[u8]], + output: &mut [u8], + ) -> Result<(), tls13::OutputLengthError> { + let info_concat = info.concat(); + unsafe { + boring_sys::HKDF_expand( + output.as_mut_ptr(), + output.len(), + self.digest.as_ptr(), + self.prk.as_ptr(), + self.prk_len, + info_concat.as_ptr(), + info_concat.len(), + ); + }; + Ok(()) + } + + /// `HKDF-Expand(PRK, info, L=HashLen)` returned as a value. + /// + /// - `PRK` is the implicit key material represented by this instance. + /// - `L := HashLen`. + /// - `info` is a slice of byte slices, which should be processed sequentially + /// (or concatenated if that is not possible). + /// + /// This is infallible, because by definition `OkmBlock` is always exactly + /// `HashLen` bytes long. + fn expand_block(&self, info: &[&[u8]]) -> tls13::OkmBlock { + let mut output = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; + let output_len = self.hash_len(); + + self.expand_slice(info, &mut output[..output_len]).unwrap(); + + tls13::OkmBlock::new(&output[..output_len]) + } + + fn hash_len(&self) -> usize { + self.digest.size() + } +} diff --git a/boring-rustls-provider/src/hmac.rs b/boring-rustls-provider/src/hmac.rs new file mode 100644 index 0000000..2ac8652 --- /dev/null +++ b/boring-rustls-provider/src/hmac.rs @@ -0,0 +1,144 @@ +use std::{os::raw::c_void, ptr}; + +use boring::hash::MessageDigest; +use rustls::crypto; + +use crate::helper::{cvt, cvt_p}; + +#[allow(unused)] +pub const SHA256: &dyn crypto::hmac::Hmac = &BoringHmac(boring::nid::Nid::SHA256); + +pub struct BoringHmac(pub boring::nid::Nid); + +impl crypto::hmac::Hmac for BoringHmac { + fn with_key(&self, key: &[u8]) -> Box { + Box::new(unsafe { + let ctx = cvt_p(boring_sys::HMAC_CTX_new()).unwrap(); + + let md = boring::hash::MessageDigest::from_nid(self.0).unwrap(); + + BoringHmacKey { + ctx, + md, + key: key.to_vec(), + } + }) + } + + fn hash_output_len(&self) -> usize { + boring::hash::MessageDigest::from_nid(self.0) + .unwrap() + .size() + } +} + +struct BoringHmacKey { + ctx: *mut boring_sys::HMAC_CTX, + md: MessageDigest, + key: Vec, +} + +impl Clone for BoringHmacKey { + fn clone(&self) -> Self { + let ctx = unsafe { + let ctx = cvt_p(boring_sys::HMAC_CTX_new()).unwrap(); + + cvt(boring_sys::HMAC_CTX_copy(ctx, self.ctx)).unwrap(); + ctx + }; + Self { + ctx, + md: self.md.clone(), + key: self.key.clone(), + } + } +} + +unsafe impl Sync for BoringHmacKey {} +unsafe impl Send for BoringHmacKey {} + +impl crypto::hmac::Key for BoringHmacKey { + fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> crypto::hmac::Tag { + let mut out = [0u8; 32]; + + crypto::hmac::Tag::new(unsafe { + // initialize a new hmac + cvt(boring_sys::HMAC_Init_ex( + self.ctx, + self.key.as_ptr() as *const c_void, + self.key.len(), + self.md.as_ptr(), + ptr::null_mut(), + )) + .unwrap(); + + cvt(boring_sys::HMAC_Update( + self.ctx, + first.as_ptr(), + first.len(), + )) + .unwrap(); + + for m in middle { + cvt(boring_sys::HMAC_Update(self.ctx, m.as_ptr(), m.len())).unwrap(); + } + + cvt(boring_sys::HMAC_Update(self.ctx, last.as_ptr(), last.len())).unwrap(); + + let mut out_len = 0; + cvt(boring_sys::HMAC_Final( + self.ctx, + out.as_mut_ptr(), + &mut out_len, + )) + .unwrap(); + + &out[..out_len as usize] + }) + } + + fn tag_len(&self) -> usize { + self.md.size() + } +} + +impl Drop for BoringHmacKey { + fn drop(&mut self) { + unsafe { + boring_sys::HMAC_CTX_free(self.ctx); + } + } +} + +#[cfg(test)] +mod tests { + use super::SHA256; + use hex_literal::hex; + + #[test] + fn test_sha256_hmac() { + let hasher = SHA256.with_key("Very Secret".as_bytes()); + + let _tag = hasher.sign_concat( + "yay".as_bytes(), + &["this".as_bytes(), "works".as_bytes()], + "well".as_bytes(), + ); + + let tag = hasher.sign_concat( + &[], + &[ + "yay".as_bytes(), + "this".as_bytes(), + "works".as_bytes(), + "well".as_bytes(), + ], + &[], + ); + + assert_eq!( + tag.as_ref(), + hex!("11fa4a6ee97bebfad9e1087145c556fec9a786cad0659aa10702d21bd2968305") + ); + } +} diff --git a/boring-rustls-provider/src/kx.rs b/boring-rustls-provider/src/kx.rs new file mode 100644 index 0000000..14ead07 --- /dev/null +++ b/boring-rustls-provider/src/kx.rs @@ -0,0 +1,139 @@ +use rustls::crypto::{self, ActiveKeyExchange}; + +mod dh; +mod evp; + +#[derive(Debug)] +enum DhKeyType { + EC(i32), + ED(i32), + FFDHE2048, +} + +#[derive(Debug)] +pub struct X25519; + +impl crypto::SupportedKxGroup for X25519 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new( + evp::BoringEvpKey::generate_x25519().map_err(|_| crypto::GetRandomFailed)?, + )) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::X25519 + } +} + +#[derive(Debug)] +pub struct X448; + +impl crypto::SupportedKxGroup for X448 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new( + evp::BoringEvpKey::generate_x448().map_err(|_| crypto::GetRandomFailed)?, + )) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::X448 + } +} + +#[derive(Debug)] +pub struct Secp256r1; + +impl crypto::SupportedKxGroup for Secp256r1 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new( + evp::BoringEvpKey::generate_secp256r1().map_err(|_| crypto::GetRandomFailed)?, + )) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::secp256r1 + } +} + +#[derive(Debug)] +pub struct Secp384r1; + +impl crypto::SupportedKxGroup for Secp384r1 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new( + evp::BoringEvpKey::generate_secp384r1().map_err(|_| crypto::GetRandomFailed)?, + )) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::secp384r1 + } +} + +#[derive(Debug)] +pub struct Secp521r1; + +impl crypto::SupportedKxGroup for Secp521r1 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new( + evp::BoringEvpKey::generate_secp521r1().map_err(|_| crypto::GetRandomFailed)?, + )) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::secp521r1 + } +} + +#[derive(Debug)] +pub struct FfDHe2048; + +impl crypto::SupportedKxGroup for FfDHe2048 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new( + dh::BoringDhKey::generate_ffdhe_2048().map_err(|_| crypto::GetRandomFailed)?, + )) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::FFDHE2048 + } +} + +#[cfg(test)] +mod tests { + use rustls::crypto::ActiveKeyExchange; + + #[test] + fn test_derive_ed() { + let alice = super::evp::BoringEvpKey::generate_x25519().unwrap(); + let bob = super::evp::BoringEvpKey::generate_x25519().unwrap(); + + let shared_secret1 = alice.diffie_hellman(&bob.pub_key()).unwrap(); + let shared_secret2 = bob.diffie_hellman(&alice.pub_key()).unwrap(); + + assert_eq!(shared_secret1, shared_secret2) + } + + #[test] + fn test_derive_ec() { + let alice = super::evp::BoringEvpKey::generate_secp256r1().unwrap(); + let bob = super::evp::BoringEvpKey::generate_secp256r1().unwrap(); + + let shared_secret1 = alice.diffie_hellman(&bob.pub_key()).unwrap(); + let shared_secret2 = bob.diffie_hellman(&alice.pub_key()).unwrap(); + + assert_eq!(shared_secret1, shared_secret2) + } + + #[test] + fn test_derive_dh() { + let alice = super::dh::BoringDhKey::generate_ffdhe_2048().unwrap(); + let bob = super::dh::BoringDhKey::generate_ffdhe_2048().unwrap(); + + let shared_secret1 = alice.diffie_hellman(&bob.pub_key()).unwrap(); + let shared_secret2 = bob.diffie_hellman(&alice.pub_key()).unwrap(); + + assert_eq!(shared_secret1, shared_secret2) + } +} diff --git a/boring-rustls-provider/src/kx/dh.rs b/boring-rustls-provider/src/kx/dh.rs new file mode 100644 index 0000000..24b7353 --- /dev/null +++ b/boring-rustls-provider/src/kx/dh.rs @@ -0,0 +1,98 @@ +use boring::error::ErrorStack; +use foreign_types::ForeignType; +use rustls::crypto; + +use crate::helper::{cvt, cvt_p}; + +use super::DhKeyType; + +pub struct BoringDhKey { + dh: boring::dh::Dh, + pub_bytes: Vec, + key_type: DhKeyType, +} + +impl BoringDhKey { + pub fn generate_ffdhe_2048() -> Result { + let mut me = Self { + dh: unsafe { boring::dh::Dh::from_ptr(cvt_p(boring_sys::DH_get_rfc7919_2048())?) }, + pub_bytes: Vec::new(), + key_type: DhKeyType::FFDHE2048, + }; + + me.pub_bytes = unsafe { + // generate a new key pair + cvt(boring_sys::DH_generate_key(me.dh.as_ptr()))?; + + // get a reference to the pub key + let pubkey = boring_sys::DH_get0_pub_key(me.dh.as_ptr()); + + // figure out how many bytes we need, round up to the next full byte + let size = (boring_sys::BN_num_bits(pubkey) as usize + 7) / 8; + + // alloc a vector with enough capacity + let mut v = Vec::with_capacity(size); + + // convert to binary representation + let after_size = boring_sys::BN_bn2bin(pubkey, v.as_mut_ptr()); + // size should be what we calculated before + assert_eq!(size, after_size); + + // ensure those bytes are accessible in the vec + v.set_len(size); + v + }; + + Ok(me) + } + + pub fn diffie_hellman(&self, raw_public_key: &[u8]) -> Result, ErrorStack> { + let peer = boring::bn::BigNum::from_slice(raw_public_key).unwrap(); + let secret_len = unsafe { cvt(boring_sys::DH_size(self.dh.as_ptr()))? } as usize; + let mut secret = vec![0u8; secret_len]; + let secret_len = unsafe { + cvt(boring_sys::DH_compute_key_padded( + secret.as_mut_ptr(), + peer.as_ptr(), + self.dh.as_ptr(), + ))? + } as usize; + secret.truncate(secret_len); + Ok(secret) + } + + #[allow(unused)] + fn pub_key(&self) -> &[u8] { + self.pub_bytes.as_ref() + } +} + +impl crypto::ActiveKeyExchange for BoringDhKey { + fn complete( + self: Box, + peer_pub_key: &[u8], + ) -> Result { + let expected_len = self.pub_bytes.len(); + + if peer_pub_key.len() != expected_len { + return Err(rustls::Error::from(rustls::PeerMisbehaved::InvalidKeyShare)); + } + + Ok(crypto::SharedSecret::from( + self.diffie_hellman(peer_pub_key) + .map_err(|x| rustls::Error::General(x.to_string()))? + .as_ref(), + )) + } + + fn pub_key(&self) -> &[u8] { + self.pub_bytes.as_ref() + } + + fn group(&self) -> rustls::NamedGroup { + match self.key_type { + DhKeyType::FFDHE2048 => rustls::NamedGroup::FFDHE2048, + _ => unimplemented!(), + } + } +} diff --git a/boring-rustls-provider/src/kx/evp.rs b/boring-rustls-provider/src/kx/evp.rs new file mode 100644 index 0000000..8319fde --- /dev/null +++ b/boring-rustls-provider/src/kx/evp.rs @@ -0,0 +1,315 @@ +use std::{ + mem::MaybeUninit, + ops::{Deref, DerefMut}, + ptr::{self, NonNull}, +}; + +use boring::error::ErrorStack; +use foreign_types::{ForeignType, ForeignTypeRef, Opaque}; +use rustls::crypto; +use spki::der::Decode; + +use crate::helper::{cvt, cvt_p}; + +use super::DhKeyType; + +pub struct BoringEvpPkeyCtxRef(Opaque); + +unsafe impl ForeignTypeRef for BoringEvpPkeyCtxRef { + type CType = boring_sys::EVP_PKEY_CTX; +} + +unsafe impl Sync for BoringEvpPkeyCtxRef {} +unsafe impl Send for BoringEvpPkeyCtxRef {} + +unsafe impl Sync for BoringEvpPkeyCtx {} +unsafe impl Send for BoringEvpPkeyCtx {} + +pub struct BoringEvpPkeyCtx(NonNull); +unsafe impl ForeignType for BoringEvpPkeyCtx { + type CType = boring_sys::EVP_PKEY_CTX; + + type Ref = BoringEvpPkeyCtxRef; + + unsafe fn from_ptr(ptr: *mut Self::CType) -> Self { + Self(NonNull::new_unchecked(ptr)) + } + + fn as_ptr(&self) -> *mut Self::CType { + self.0.as_ptr() + } +} +impl Drop for BoringEvpPkeyCtx { + fn drop(&mut self) { + unsafe { + boring_sys::EVP_PKEY_CTX_free(self.0.as_ptr()); + } + } +} + +impl core::fmt::Debug for BoringEvpPkeyCtx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("BoringEvpPkeyCtx").field(&self.0).finish() + } +} + +impl Deref for BoringEvpPkeyCtx { + type Target = BoringEvpPkeyCtxRef; + + fn deref(&self) -> &BoringEvpPkeyCtxRef { + unsafe { BoringEvpPkeyCtxRef::from_ptr(self.as_ptr()) } + } +} + +impl DerefMut for BoringEvpPkeyCtx { + fn deref_mut(&mut self) -> &mut BoringEvpPkeyCtxRef { + unsafe { BoringEvpPkeyCtxRef::from_ptr_mut(self.as_ptr()) } + } +} + +#[derive(Debug)] +pub struct BoringEvpKey { + /// the private key context for deriving shared secrets + dctx: BoringEvpPkeyCtx, + + key_type: DhKeyType, + + pub_bytes: Vec, +} + +unsafe impl Sync for BoringEvpKey {} +unsafe impl Send for BoringEvpKey {} + +impl BoringEvpKey { + pub fn generate_x25519() -> Result { + unsafe { + Self::generate_with_ctx( + DhKeyType::ED(boring_sys::NID_X25519), + Self::generate_ctx_from_nid(boring_sys::NID_X25519)?, + ) + } + } + + pub fn generate_x448() -> Result { + unsafe { + Self::generate_with_ctx( + DhKeyType::ED(boring_sys::NID_X448), + Self::generate_ctx_from_nid(boring_sys::NID_X448)?, + ) + } + } + + pub fn generate_secp256r1() -> Result { + unsafe { + let pctx = Self::generate_ctx_with_ec_curve(boring_sys::NID_X9_62_prime256v1)?; + Self::generate_with_ctx(DhKeyType::EC(boring_sys::NID_X9_62_prime256v1), pctx) + } + } + pub fn generate_secp384r1() -> Result { + unsafe { + let pctx = Self::generate_ctx_with_ec_curve(boring_sys::NID_secp384r1)?; + Self::generate_with_ctx(DhKeyType::EC(boring_sys::NID_secp384r1), pctx) + } + } + pub fn generate_secp521r1() -> Result { + unsafe { + let pctx = Self::generate_ctx_with_ec_curve(boring_sys::NID_secp521r1)?; + Self::generate_with_ctx(DhKeyType::EC(boring_sys::NID_secp521r1), pctx) + } + } + + unsafe fn generate_ctx_with_ec_curve(curve_nid: i32) -> Result { + boring_sys::init(); + + let pctx = BoringEvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id( + boring_sys::EVP_PKEY_EC, + ptr::null_mut(), + ))?); + + // The following function is for generating parameters + cvt(boring_sys::EVP_PKEY_paramgen_init(pctx.as_ptr()))?; + + // Set the curve + cvt(boring_sys::EVP_PKEY_CTX_set_ec_paramgen_curve_nid( + pctx.as_ptr(), + curve_nid, + ))?; + + // Used a named curve which has max compatiblity according man page + cvt(boring_sys::EVP_PKEY_CTX_set_ec_param_enc( + pctx.as_ptr(), + boring_sys::OPENSSL_EC_NAMED_CURVE, + ))?; + + // generate parameters + let mut pkey = MaybeUninit::<*mut boring_sys::EVP_PKEY>::new(ptr::null_mut()).assume_init(); + cvt(boring_sys::EVP_PKEY_paramgen(pctx.as_ptr(), &mut pkey))?; + let pkey: boring::pkey::PKey = boring::pkey::PKey::from_ptr(pkey); + + Ok(BoringEvpPkeyCtx::from_ptr( + // ctx will take ownership of pkey + cvt_p(boring_sys::EVP_PKEY_CTX_new(pkey.as_ptr(), ptr::null_mut())).expect("failed"), + )) + } + + fn generate_ctx_from_nid(nid: i32) -> Result { + boring_sys::init(); + Ok(unsafe { + BoringEvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id( + nid, + ptr::null_mut(), + ))?) + }) + } + + unsafe fn generate_with_ctx( + key_type: DhKeyType, + pctx: BoringEvpPkeyCtx, + ) -> Result { + let mut pkey = MaybeUninit::<*mut boring_sys::EVP_PKEY>::new(ptr::null_mut()).assume_init(); + + cvt(boring_sys::EVP_PKEY_keygen_init(pctx.as_ptr()))?; + + cvt(boring_sys::EVP_PKEY_keygen(pctx.as_ptr(), &mut pkey))?; + let pkey: boring::pkey::PKey = boring::pkey::PKey::from_ptr(pkey); + + let dctx = BoringEvpPkeyCtx::from_ptr( + // dctx will take ownership of pkey, we can safely drop it + cvt_p(boring_sys::EVP_PKEY_CTX_new(pkey.as_ptr(), ptr::null_mut()))?, + ); + + let pub_bytes = Self::raw_public_key(pkey.as_ref())?; + + Ok(Self { + dctx, + key_type, + pub_bytes, + }) + } + + fn raw_public_key( + pkey: &boring::pkey::PKeyRef, + ) -> Result, ErrorStack> { + let key_len = unsafe { + // figure out how many bytes we need for the key + cvt(boring_sys::i2d_PUBKEY(pkey.as_ptr(), ptr::null_mut()))? as usize + }; + let mut spki = vec![0u8; key_len]; + unsafe { + // write the key to spki + cvt(boring_sys::i2d_PUBKEY( + pkey.as_ptr(), + &mut spki.as_mut_ptr(), + ))?; + } + // parse the key + let key = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref()).unwrap(); + + // return the raw public key as a new vec + Ok(Vec::from(key.subject_public_key.as_bytes().unwrap())) + } + + pub fn diffie_hellman(&self, raw_public_key: &[u8]) -> Result, ErrorStack> { + match self.key_type { + DhKeyType::EC(nid) => self.diffie_hellman_ec(nid, raw_public_key), + DhKeyType::ED(nid) => self.diffie_hellman_ed(nid, raw_public_key), + _ => unimplemented!(), + } + } + + fn diffie_hellman_ec(&self, nid: i32, raw_public_key: &[u8]) -> Result, ErrorStack> { + // this is only the key data not the algo identifier etc + let group = boring::ec::EcGroup::from_curve_name(boring::nid::Nid::from_raw(nid))?; + let mut bn_ctx = boring::bn::BigNumContext::new()?; + let point = + crate::verify::ec::ec_point(group.as_ref(), &mut bn_ctx, raw_public_key).unwrap(); + + let peerkey = crate::verify::ec::ec_public_key(group.as_ref(), point.as_ref()).unwrap(); + + self.diffie_hellman_common(peerkey.as_ptr()) + } + + fn diffie_hellman_ed(&self, nid: i32, raw_public_key: &[u8]) -> Result, ErrorStack> { + let peerkey: boring::pkey::PKey = unsafe { + boring::pkey::PKey::from_ptr(cvt_p(boring_sys::EVP_PKEY_new_raw_public_key( + nid, + ptr::null_mut(), + raw_public_key.as_ptr(), + raw_public_key.len(), + ))?) + }; + + self.diffie_hellman_common(peerkey.as_ptr()) + } + + fn diffie_hellman_common( + &self, + peerkey: *mut boring_sys::EVP_PKEY, + ) -> Result, ErrorStack> { + unsafe { + // Initialize + cvt(boring_sys::EVP_PKEY_derive_init(self.dctx.as_ptr()))?; + + // Provide the peer public key + cvt(boring_sys::EVP_PKEY_derive_set_peer( + self.dctx.as_ptr(), + peerkey, + ))?; + } + + // Determine buffer length for shared secret + let mut secret_len = unsafe { + let mut secret_len = 0; + cvt(boring_sys::EVP_PKEY_derive( + self.dctx.as_ptr(), + ptr::null_mut(), + &mut secret_len, + ))?; + secret_len + }; + + let mut secret = vec![0u8; secret_len]; + unsafe { + cvt(boring_sys::EVP_PKEY_derive( + self.dctx.as_ptr(), + secret.as_mut_ptr(), + &mut secret_len, + ))?; + } + Ok(secret) + } +} + +impl crypto::ActiveKeyExchange for BoringEvpKey { + fn complete( + self: Box, + peer_pub_key: &[u8], + ) -> Result { + let expected_len = self.pub_bytes.len(); + + if peer_pub_key.len() != expected_len { + return Err(rustls::Error::from(rustls::PeerMisbehaved::InvalidKeyShare)); + } + + Ok(crypto::SharedSecret::from( + self.diffie_hellman(peer_pub_key) + .map_err(|x| rustls::Error::General(x.to_string()))? + .as_ref(), + )) + } + + fn pub_key(&self) -> &[u8] { + self.pub_bytes.as_ref() + } + + fn group(&self) -> rustls::NamedGroup { + match self.key_type { + DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519, + DhKeyType::ED(boring_sys::NID_X448) => rustls::NamedGroup::X448, + DhKeyType::EC(boring_sys::NID_X9_62_prime256v1) => rustls::NamedGroup::secp256r1, + DhKeyType::EC(boring_sys::NID_secp384r1) => rustls::NamedGroup::secp384r1, + DhKeyType::EC(boring_sys::NID_secp521r1) => rustls::NamedGroup::secp521r1, + _ => unimplemented!(), + } + } +} diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs new file mode 100644 index 0000000..b3c0e27 --- /dev/null +++ b/boring-rustls-provider/src/lib.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use rustls::{ + crypto::{CryptoProvider, GetRandomFailed, SupportedKxGroup}, + SupportedCipherSuite, +}; +use rustls_pki_types::PrivateKeyDer; + +mod aead; +mod hash; +mod helper; +mod hkdf; +mod hmac; +mod kx; +mod sign; +mod tls13; +mod verify; + +pub static PROVIDER: &'static dyn CryptoProvider = &Provider; + +#[derive(Debug)] +struct Provider; + +impl CryptoProvider for Provider { + fn fill_random(&self, bytes: &mut [u8]) -> Result<(), GetRandomFailed> { + boring::rand::rand_bytes(bytes).map_err(|_| GetRandomFailed) + } + + fn default_cipher_suites(&self) -> &'static [SupportedCipherSuite] { + if boring::fips::enabled() { + ALL_FIPS_SUITES + } else { + ALL_CIPHER_SUITES + } + } + + fn default_kx_groups(&self) -> &'static [&'static dyn SupportedKxGroup] { + if boring::fips::enabled() { + ALL_FIPS_KX_GROUPS + } else { + ALL_KX_GROUPS + } + } + + fn load_private_key( + &self, + key_der: PrivateKeyDer<'static>, + ) -> Result, rustls::Error> { + sign::BoringPrivateKey::try_from(key_der) + .map(|x| Arc::new(x) as _) + .map_err(|_| rustls::Error::General("invalid private key".into())) + } + + fn signature_verification_algorithms(&self) -> rustls::WebPkiSupportedAlgorithms { + verify::ALL_ALGORITHMS + } +} + +static ALL_FIPS_SUITES: &[SupportedCipherSuite] = &[ + SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), + SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA256), +]; + +static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ + SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), + SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA256), + SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), +]; + +pub const ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[]; +pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ + &kx::X25519 as _, + &kx::X448 as _, + &kx::Secp256r1 as _, + &kx::Secp384r1 as _, + &kx::Secp521r1 as _, + &kx::FfDHe2048 as _, +]; diff --git a/boring-rustls-provider/src/sign.rs b/boring-rustls-provider/src/sign.rs new file mode 100644 index 0000000..2f57ebc --- /dev/null +++ b/boring-rustls-provider/src/sign.rs @@ -0,0 +1,194 @@ +use std::sync::Arc; + +use boring::{hash::MessageDigest, pkey::Id, rsa::Padding, sign::RsaPssSaltlen}; +use rustls::{ + sign::{Signer, SigningKey}, + SignatureScheme, +}; +use rustls_pki_types::PrivateKeyDer; + +const ALL_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, +]; + +const ALL_EC_SCHEMES: &[SignatureScheme] = &[ + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512, +]; + +#[derive(Debug)] +pub struct BoringPrivateKey( + Arc>, + rustls::SignatureAlgorithm, +); + +impl TryFrom> for BoringPrivateKey { + type Error = rustls::Error; + + fn try_from(value: PrivateKeyDer<'static>) -> Result { + let pkey = match value { + PrivateKeyDer::Pkcs8(der) => { + boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()).map_err(|_| ()) + } + PrivateKeyDer::Pkcs1(der) => { + boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()).map_err(|_| ()) + } + _ => Err(()), + } + .map_err(|_| rustls::Error::General("failed loading private key".into()))?; + + let sig = match pkey.id() { + Id::RSA => rustls::SignatureAlgorithm::RSA, + Id::EC => rustls::SignatureAlgorithm::ECDSA, + Id::ED25519 => rustls::SignatureAlgorithm::ED25519, + Id::ED448 => rustls::SignatureAlgorithm::ED448, + _ => return Err(rustls::Error::General("unsupported key format".into())), + }; + Ok(Self(Arc::new(pkey), sig)) + } +} + +fn rsa_signer_from_params( + key: &boring::pkey::PKeyRef, + digest: MessageDigest, + padding: Padding, +) -> boring::sign::Signer { + let mut signer = boring::sign::Signer::new(digest.clone(), key).expect("failed getting signer"); + signer + .set_rsa_padding(padding) + .expect("failed setting padding"); + if padding == Padding::PKCS1_PSS { + signer + .set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) + .expect("failed setting rsa_pss salt lengths"); + signer + .set_rsa_mgf1_md(digest) + .expect("failed setting mgf1 digest"); + } + + signer +} + +fn ec_signer_from_params( + key: &boring::pkey::PKeyRef, + digest: MessageDigest, +) -> boring::sign::Signer { + let signer = boring::sign::Signer::new(digest, key).expect("failed getting signer"); + signer +} + +impl BoringPrivateKey {} + +impl SigningKey for BoringPrivateKey { + fn choose_scheme( + &self, + offered: &[rustls::SignatureScheme], + ) -> Option> { + match self.1 { + rustls::SignatureAlgorithm::RSA => ALL_RSA_SCHEMES + .iter() + .find(|scheme| offered.contains(scheme)) + .map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), + rustls::SignatureAlgorithm::ECDSA => ALL_EC_SCHEMES + .iter() + .find(|scheme| offered.contains(scheme)) + .map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), + rustls::SignatureAlgorithm::ED25519 + if offered.contains(&rustls::SignatureScheme::ED25519) => + { + Some(Box::new(BoringSigner( + self.0.clone(), + rustls::SignatureScheme::ED25519, + ))) + } + rustls::SignatureAlgorithm::ED448 + if offered.contains(&rustls::SignatureScheme::ED448) => + { + Some(Box::new(BoringSigner( + self.0.clone(), + rustls::SignatureScheme::ED448, + ))) + } + _ => None, + } + } + + fn algorithm(&self) -> rustls::SignatureAlgorithm { + self.1 + } +} + +#[derive(Debug)] +pub struct BoringSigner( + Arc>, + rustls::SignatureScheme, +); + +impl BoringSigner { + fn get_signer(&self) -> boring::sign::Signer { + match self.1 { + SignatureScheme::RSA_PKCS1_SHA256 => { + rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1) + } + SignatureScheme::RSA_PKCS1_SHA384 => { + rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha384(), Padding::PKCS1) + } + SignatureScheme::RSA_PKCS1_SHA512 => { + rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1) + } + + SignatureScheme::RSA_PSS_SHA256 => { + rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1_PSS) + } + SignatureScheme::RSA_PSS_SHA384 => { + rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha384(), Padding::PKCS1_PSS) + } + SignatureScheme::RSA_PSS_SHA512 => { + rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1_PSS) + } + + SignatureScheme::ECDSA_NISTP256_SHA256 => { + ec_signer_from_params(self.0.as_ref(), MessageDigest::sha256()) + } + SignatureScheme::ECDSA_NISTP384_SHA384 => { + ec_signer_from_params(self.0.as_ref(), MessageDigest::sha384()) + } + SignatureScheme::ECDSA_NISTP521_SHA512 => { + ec_signer_from_params(self.0.as_ref(), MessageDigest::sha512()) + } + + SignatureScheme::ED25519 => boring::sign::Signer::new_without_digest(self.0.as_ref()) + .expect("failed getting signer"), + SignatureScheme::ED448 => boring::sign::Signer::new_without_digest(self.0.as_ref()) + .expect("failed getting signer"), + + _ => unimplemented!(), + } + } +} + +impl Signer for BoringSigner { + fn sign(&self, message: &[u8]) -> Result, rustls::Error> { + let signer = self.get_signer(); + let mut msg_with_sig = + Vec::::with_capacity(message.len() + boring_sys::EVP_MAX_MD_SIZE as usize); + msg_with_sig.extend_from_slice(message); + msg_with_sig.extend_from_slice(&[0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); + + let toatl_len = signer + .sign(&mut msg_with_sig[..]) + .map_err(|_| rustls::Error::General("failed signing".into()))?; + msg_with_sig.truncate(toatl_len); + Ok(msg_with_sig) + } + + fn scheme(&self) -> rustls::SignatureScheme { + self.1 + } +} diff --git a/boring-rustls-provider/src/tls13.rs b/boring-rustls-provider/src/tls13.rs new file mode 100644 index 0000000..0d106b0 --- /dev/null +++ b/boring-rustls-provider/src/tls13.rs @@ -0,0 +1,31 @@ +use rustls::Tls13CipherSuite; + +use crate::{aead, hash, hkdf}; + +pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { + common: rustls::CipherSuiteCommon { + suite: rustls::CipherSuite::TLS13_AES_128_GCM_SHA256, + hash_provider: hash::SHA256, + }, + hkdf_provider: &hkdf::Hkdf::::DEFAULT, + aead_alg: &aead::Aead::::DEFAULT, +}; + +pub static AES_256_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { + common: rustls::CipherSuiteCommon { + suite: rustls::CipherSuite::TLS13_AES_128_GCM_SHA256, + hash_provider: hash::SHA256, + }, + hkdf_provider: &hkdf::Hkdf::::DEFAULT, + aead_alg: &aead::Aead::::DEFAULT, +}; + +pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite { + common: rustls::CipherSuiteCommon { + suite: rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, + hash_provider: hash::SHA256, + }, + + hkdf_provider: &hkdf::Hkdf::::DEFAULT, + aead_alg: &aead::Aead::::DEFAULT, +}; diff --git a/boring-rustls-provider/src/verify.rs b/boring-rustls-provider/src/verify.rs new file mode 100644 index 0000000..872d0f6 --- /dev/null +++ b/boring-rustls-provider/src/verify.rs @@ -0,0 +1,61 @@ +use rustls::{SignatureScheme, WebPkiSupportedAlgorithms}; + +pub(crate) mod ec; +mod ed; +pub(crate) mod rsa; + +pub static ALL_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { + all: &[ + &rsa::BoringRsaVerifier::RSA_PKCS1_SHA256, + &rsa::BoringRsaVerifier::RSA_PKCS1_SHA384, + &rsa::BoringRsaVerifier::RSA_PKCS1_SHA512, + &rsa::BoringRsaVerifier::RSA_PSS_SHA256, + &rsa::BoringRsaVerifier::RSA_PSS_SHA384, + &rsa::BoringRsaVerifier::RSA_PSS_SHA512, + &ec::BoringEcVerifier::ECDSA_NISTP256_SHA256, + &ec::BoringEcVerifier::ECDSA_NISTP384_SHA384, + &ec::BoringEcVerifier::ECDSA_NISTP521_SHA512, + &ed::BoringEdVerifier::ED25519, + &ed::BoringEdVerifier::ED448, + ], + mapping: &[ + ( + SignatureScheme::RSA_PKCS1_SHA256, + &[&rsa::BoringRsaVerifier::RSA_PKCS1_SHA256], + ), + ( + SignatureScheme::RSA_PKCS1_SHA384, + &[&rsa::BoringRsaVerifier::RSA_PKCS1_SHA384], + ), + ( + SignatureScheme::RSA_PKCS1_SHA512, + &[&rsa::BoringRsaVerifier::RSA_PKCS1_SHA512], + ), + ( + SignatureScheme::RSA_PSS_SHA256, + &[&rsa::BoringRsaVerifier::RSA_PSS_SHA256], + ), + ( + SignatureScheme::RSA_PSS_SHA384, + &[&rsa::BoringRsaVerifier::RSA_PSS_SHA384], + ), + ( + SignatureScheme::RSA_PSS_SHA512, + &[&rsa::BoringRsaVerifier::RSA_PSS_SHA512], + ), + ( + SignatureScheme::ECDSA_NISTP256_SHA256, + &[&ec::BoringEcVerifier::ECDSA_NISTP256_SHA256], + ), + ( + SignatureScheme::ECDSA_NISTP384_SHA384, + &[&ec::BoringEcVerifier::ECDSA_NISTP384_SHA384], + ), + ( + SignatureScheme::ECDSA_NISTP521_SHA512, + &[&ec::BoringEcVerifier::ECDSA_NISTP521_SHA512], + ), + (SignatureScheme::ED25519, &[&ed::BoringEdVerifier::ED25519]), + (SignatureScheme::ED448, &[&ed::BoringEdVerifier::ED448]), + ], +}; diff --git a/boring-rustls-provider/src/verify/ec.rs b/boring-rustls-provider/src/verify/ec.rs new file mode 100644 index 0000000..d42df1f --- /dev/null +++ b/boring-rustls-provider/src/verify/ec.rs @@ -0,0 +1,115 @@ +use boring::hash::MessageDigest; +use rustls::SignatureScheme; +use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; + +pub struct BoringEcVerifier(SignatureScheme); + +impl BoringEcVerifier { + pub const ECDSA_NISTP256_SHA256: Self = Self(SignatureScheme::ECDSA_NISTP256_SHA256); + pub const ECDSA_NISTP384_SHA384: Self = Self(SignatureScheme::ECDSA_NISTP384_SHA384); + pub const ECDSA_NISTP521_SHA512: Self = Self(SignatureScheme::ECDSA_NISTP521_SHA512); +} + +impl SignatureVerificationAlgorithm for BoringEcVerifier { + fn verify_signature( + &self, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result<(), rustls_pki_types::InvalidSignature> { + let (group, mut bn_ctx) = setup_ec_key(self.0); + let ec_point = ec_point(group.as_ref(), bn_ctx.as_mut(), public_key)?; + let public_key = ec_public_key(group.as_ref(), ec_point.as_ref())?; + let mut verifier = match self.0 { + SignatureScheme::ECDSA_NISTP256_SHA256 => { + ec_verifier_from_params(public_key.as_ref(), MessageDigest::sha256()) + } + SignatureScheme::ECDSA_NISTP384_SHA384 => { + ec_verifier_from_params(public_key.as_ref(), MessageDigest::sha384()) + } + SignatureScheme::ECDSA_NISTP521_SHA512 => { + ec_verifier_from_params(public_key.as_ref(), MessageDigest::sha512()) + } + + _ => unimplemented!(), + }; + verifier.verify_oneshot(signature, message).map_or_else( + |_| Err(InvalidSignature), + |res| if res { Ok(()) } else { Err(InvalidSignature) }, + ) + } + + fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { + match self.0 { + SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_P256, + SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_P384, + SignatureScheme::ECDSA_NISTP521_SHA512 => { + // See rfc5480 appendix-A (secp521r1): 1.3.132.0.35 + rustls_pki_types::AlgorithmIdentifier::from_slice(&[ + 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, + 0x04, 0x00, 0x23, + ]) + } + _ => unimplemented!(), + } + } + + fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { + match self.0 { + SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512 => { + // See rfc5480 appendix-A (ecdsa-with-SHA512): 1.2.840.10045.4.3.4 + rustls_pki_types::AlgorithmIdentifier::from_slice(&[ + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04, + ]) + } + _ => unimplemented!(), + } + } +} + +fn ec_verifier_from_params( + key: &boring::pkey::PKeyRef, + digest: MessageDigest, +) -> boring::sign::Verifier { + let verifier = + boring::sign::Verifier::new(digest.clone(), key).expect("failed getting verifier"); + + verifier +} + +fn group_for_scheme(scheme: SignatureScheme) -> boring::ec::EcGroup { + let nid = match scheme { + SignatureScheme::ECDSA_NISTP256_SHA256 => boring::nid::Nid::X9_62_PRIME256V1, + SignatureScheme::ECDSA_NISTP384_SHA384 => boring::nid::Nid::SECP384R1, + SignatureScheme::ECDSA_NISTP521_SHA512 => boring::nid::Nid::SECP521R1, + _ => unimplemented!(), + }; + boring::ec::EcGroup::from_curve_name(nid).expect("failed getting verify curve") +} + +fn setup_ec_key(scheme: SignatureScheme) -> (boring::ec::EcGroup, boring::bn::BigNumContext) { + ( + group_for_scheme(scheme), + boring::bn::BigNumContext::new().unwrap(), + ) +} + +pub(crate) fn ec_point( + group: &boring::ec::EcGroupRef, + bignum_ctx: &mut boring::bn::BigNumContextRef, + spki_spk: &[u8], +) -> Result { + boring::ec::EcPoint::from_bytes(group, spki_spk, bignum_ctx).map_err(|_| InvalidSignature) +} + +pub(crate) fn ec_public_key( + group: &boring::ec::EcGroupRef, + ec_point: &boring::ec::EcPointRef, +) -> Result, InvalidSignature> { + boring::pkey::PKey::from_ec_key( + boring::ec::EcKey::from_public_key(group, ec_point).map_err(|_| InvalidSignature)?, + ) + .map_err(|_| InvalidSignature) +} diff --git a/boring-rustls-provider/src/verify/ed.rs b/boring-rustls-provider/src/verify/ed.rs new file mode 100644 index 0000000..8d0daf3 --- /dev/null +++ b/boring-rustls-provider/src/verify/ed.rs @@ -0,0 +1,78 @@ +use std::ptr; + +use foreign_types::ForeignType; +use rustls::SignatureScheme; +use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; + +use crate::helper::cvt_p; + +pub struct BoringEdVerifier(SignatureScheme); + +impl BoringEdVerifier { + pub const ED25519: Self = Self(SignatureScheme::ED25519); + pub const ED448: Self = Self(SignatureScheme::ED448); +} + +impl SignatureVerificationAlgorithm for BoringEdVerifier { + fn verify_signature( + &self, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result<(), rustls_pki_types::InvalidSignature> { + let public_key = ed_public_key(public_key, self.0)?; + let mut verifier = ed_verifier_from_params(public_key.as_ref()); + + verifier.verify_oneshot(signature, message).map_or_else( + |_| Err(InvalidSignature), + |res| if res { Ok(()) } else { Err(InvalidSignature) }, + ) + } + + fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { + // for ed those are the same + self.signature_alg_id() + } + + fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { + match self.0 { + SignatureScheme::ED25519 => webpki::alg_id::ED25519, + SignatureScheme::ED448 => { + // rfc8410#section-3: 1.3.101.113: -> DER: 06 03 2B 65 71 + rustls_pki_types::AlgorithmIdentifier::from_slice(&[0x06, 0x03, 0x2B, 0x65, 0x71]) + } + _ => unimplemented!(), + } + } +} + +fn ed_verifier_from_params( + key: &boring::pkey::PKeyRef, +) -> boring::sign::Verifier { + let verifier = + boring::sign::Verifier::new_without_digest(key).expect("failed getting verifier"); + + verifier +} + +fn ed_public_key( + spki_spk: &[u8], + scheme: SignatureScheme, +) -> Result, InvalidSignature> { + let typ = match scheme { + SignatureScheme::ED25519 => boring_sys::EVP_PKEY_ED25519, + SignatureScheme::ED448 => boring_sys::EVP_PKEY_ED448, + _ => unimplemented!(), + }; + Ok(unsafe { + let pkey = cvt_p(boring_sys::EVP_PKEY_new_raw_public_key( + typ, + ptr::null_mut(), + spki_spk.as_ptr(), + spki_spk.len(), + )) + .map_err(|_| InvalidSignature)?; + + boring::pkey::PKey::from_ptr(pkey) + }) +} diff --git a/boring-rustls-provider/src/verify/rsa.rs b/boring-rustls-provider/src/verify/rsa.rs new file mode 100644 index 0000000..87445c2 --- /dev/null +++ b/boring-rustls-provider/src/verify/rsa.rs @@ -0,0 +1,123 @@ +use boring::{hash::MessageDigest, rsa::Padding, sign::RsaPssSaltlen}; +use rustls::SignatureScheme; +use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; +use spki::der::Reader; + +pub struct BoringRsaVerifier(SignatureScheme); + +impl BoringRsaVerifier { + pub const RSA_PKCS1_SHA256: Self = Self(SignatureScheme::RSA_PKCS1_SHA256); + pub const RSA_PKCS1_SHA384: Self = Self(SignatureScheme::RSA_PKCS1_SHA384); + pub const RSA_PKCS1_SHA512: Self = Self(SignatureScheme::RSA_PKCS1_SHA512); + pub const RSA_PSS_SHA256: Self = Self(SignatureScheme::RSA_PSS_SHA256); + pub const RSA_PSS_SHA384: Self = Self(SignatureScheme::RSA_PSS_SHA384); + pub const RSA_PSS_SHA512: Self = Self(SignatureScheme::RSA_PSS_SHA512); +} + +impl SignatureVerificationAlgorithm for BoringRsaVerifier { + fn verify_signature( + &self, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result<(), rustls_pki_types::InvalidSignature> { + let public_key = decode_spki_spk(public_key)?; + let mut verifier = match self.0 { + SignatureScheme::RSA_PKCS1_SHA256 => rsa_verifier_from_params( + public_key.as_ref(), + MessageDigest::sha256(), + Padding::PKCS1, + ), + SignatureScheme::RSA_PKCS1_SHA384 => rsa_verifier_from_params( + public_key.as_ref(), + MessageDigest::sha384(), + Padding::PKCS1, + ), + SignatureScheme::RSA_PKCS1_SHA512 => rsa_verifier_from_params( + public_key.as_ref(), + MessageDigest::sha512(), + Padding::PKCS1, + ), + + SignatureScheme::RSA_PSS_SHA256 => rsa_verifier_from_params( + public_key.as_ref(), + MessageDigest::sha256(), + Padding::PKCS1_PSS, + ), + SignatureScheme::RSA_PSS_SHA384 => rsa_verifier_from_params( + public_key.as_ref(), + MessageDigest::sha384(), + Padding::PKCS1_PSS, + ), + SignatureScheme::RSA_PSS_SHA512 => rsa_verifier_from_params( + public_key.as_ref(), + MessageDigest::sha512(), + Padding::PKCS1_PSS, + ), + + _ => unimplemented!(), + }; + verifier.verify_oneshot(signature, message).map_or_else( + |_| Err(InvalidSignature), + |res| if res { Ok(()) } else { Err(InvalidSignature) }, + ) + } + + fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { + webpki::alg_id::RSA_ENCRYPTION + } + + fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { + match self.0 { + SignatureScheme::RSA_PKCS1_SHA256 => webpki::alg_id::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384 => webpki::alg_id::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512 => webpki::alg_id::RSA_PKCS1_SHA512, + + SignatureScheme::RSA_PSS_SHA256 => webpki::alg_id::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384 => webpki::alg_id::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512 => webpki::alg_id::RSA_PSS_SHA512, + + _ => unimplemented!(), + } + } +} + +fn rsa_verifier_from_params( + key: &boring::pkey::PKeyRef, + digest: MessageDigest, + padding: Padding, +) -> boring::sign::Verifier { + let mut verifier = + boring::sign::Verifier::new(digest.clone(), key).expect("failed getting verifier"); + verifier + .set_rsa_padding(padding) + .expect("failed setting padding"); + if padding == Padding::PKCS1_PSS { + verifier + .set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) + .expect("failed setting rsa_pss salt lengths"); + verifier + .set_rsa_mgf1_md(digest) + .expect("failed setting mgf1 digest"); + } + + verifier +} + +pub(crate) fn decode_spki_spk( + spki_spk: &[u8], +) -> Result, InvalidSignature> { + // public_key: unfortunately this is not a whole SPKI, but just the key material. + // decode the two integers manually. + + let mut reader = spki::der::SliceReader::new(spki_spk).map_err(|_| InvalidSignature)?; + let ne: [spki::der::asn1::UintRef; 2] = reader.decode().map_err(|_| InvalidSignature)?; + + let n = boring::bn::BigNum::from_slice(ne[0].as_bytes()).map_err(|_| InvalidSignature)?; + let e = boring::bn::BigNum::from_slice(ne[1].as_bytes()).map_err(|_| InvalidSignature)?; + + boring::pkey::PKey::from_rsa( + boring::rsa::Rsa::from_public_components(n, e).map_err(|_| InvalidSignature)?, + ) + .map_err(|_| InvalidSignature) +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 0000000..09f7247 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "boring-rustls-provider-examples" +version = "0.0.1" +edition = "2021" +license = "MIT" +description = "Boring Rustls provider example code and tests." +publish = false + +[dependencies] +docopt = "~1.1" +env_logger = "0.10" +log = { version = "0.4.4" } +mio = { version = "0.8", features = ["net", "os-poll"] } +pki-types = { package = "rustls-pki-types", version = "0.2" } +rcgen = { version = "0.11.3", features = ["pem"], default-features = false } +rustls = { workspace = true, features = [ "logging" ]} +boring-rustls-provider = { path = "../boring-rustls-provider", features = ["logging"] } +rustls-pemfile = "=2.0.0-alpha.1" +serde = "1.0" +serde_derive = "1.0" +webpki-roots = "=0.26.0-alpha.1" \ No newline at end of file diff --git a/examples/src/bin/client.rs b/examples/src/bin/client.rs new file mode 100644 index 0000000..55a4cc0 --- /dev/null +++ b/examples/src/bin/client.rs @@ -0,0 +1,43 @@ +use std::io::{stdout, Read, Write}; +use std::net::TcpStream; +use std::sync::Arc; + +use boring_rustls_provider::PROVIDER; + +fn main() { + env_logger::init(); + + let mut root_store = rustls::RootCertStore::empty(); + root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + + let config = rustls::ClientConfig::builder_with_provider(PROVIDER) + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let server_name = "www.rust-lang.org".try_into().unwrap(); + let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); + let mut sock = TcpStream::connect("www.rust-lang.org:443").unwrap(); + let mut tls = rustls::Stream::new(&mut conn, &mut sock); + tls.write_all( + concat!( + "GET / HTTP/1.1\r\n", + "Host: www.rust-lang.org\r\n", + "Connection: close\r\n", + "Accept-Encoding: identity\r\n", + "\r\n" + ) + .as_bytes(), + ) + .unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); + writeln!( + &mut std::io::stderr(), + "Current ciphersuite: {:?}", + ciphersuite.suite() + ) + .unwrap(); + let mut plaintext = Vec::new(); + tls.read_to_end(&mut plaintext).unwrap(); + stdout().write_all(&plaintext).unwrap(); +}