diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/rust/ohttp/src/lib.rs | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/ohttp/src/lib.rs')
-rw-r--r-- | third_party/rust/ohttp/src/lib.rs | 590 |
1 files changed, 590 insertions, 0 deletions
diff --git a/third_party/rust/ohttp/src/lib.rs b/third_party/rust/ohttp/src/lib.rs new file mode 100644 index 0000000000..7567cd2e97 --- /dev/null +++ b/third_party/rust/ohttp/src/lib.rs @@ -0,0 +1,590 @@ +#![deny(warnings, clippy::pedantic)] +#![allow(clippy::missing_errors_doc)] // I'm too lazy +#![cfg_attr( + not(all(feature = "client", feature = "server")), + allow(dead_code, unused_imports) +)] + +mod err; +pub mod hpke; +#[cfg(feature = "nss")] +mod nss; +#[cfg(feature = "rust-hpke")] +mod rand; +#[cfg(feature = "rust-hpke")] +mod rh; + +pub use err::Error; + +use crate::hpke::{Aead as AeadId, Kdf, Kem}; +use byteorder::{NetworkEndian, ReadBytesExt, WriteBytesExt}; +use err::Res; +use log::trace; +use std::cmp::max; +use std::convert::TryFrom; +use std::io::{BufReader, Read}; +use std::mem::size_of; + +#[cfg(feature = "nss")] +use nss::random; +#[cfg(feature = "nss")] +use nss::{ + aead::{Aead, Mode, NONCE_LEN}, + hkdf::{Hkdf, KeyMechanism}, + hpke::{generate_key_pair, Config as HpkeConfig, Exporter, HpkeR, HpkeS}, + PrivateKey, PublicKey, +}; + +#[cfg(feature = "rust-hpke")] +use crate::rand::random; +#[cfg(feature = "rust-hpke")] +use rh::{ + aead::{Aead, Mode, NONCE_LEN}, + hkdf::{Hkdf, KeyMechanism}, + hpke::{ + derive_key_pair, generate_key_pair, Config as HpkeConfig, Exporter, HpkeR, HpkeS, + PrivateKey, PublicKey, + }, +}; + +/// The request header is a `KeyId` and 2 each for KEM, KDF, and AEAD identifiers +const REQUEST_HEADER_LEN: usize = size_of::<KeyId>() + 6; +const INFO_REQUEST: &[u8] = b"message/bhttp request"; +/// The info used for HPKE export is `INFO_REQUEST`, a zero byte, and the header. +const INFO_LEN: usize = INFO_REQUEST.len() + 1 + REQUEST_HEADER_LEN; +const LABEL_RESPONSE: &[u8] = b"message/bhttp response"; +const INFO_KEY: &[u8] = b"key"; +const INFO_NONCE: &[u8] = b"nonce"; + +/// The type of a key identifier. +pub type KeyId = u8; + +pub fn init() { + #[cfg(feature = "nss")] + nss::init(); +} + +/// A tuple of KDF and AEAD identifiers. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SymmetricSuite { + kdf: Kdf, + aead: AeadId, +} + +impl SymmetricSuite { + #[must_use] + pub const fn new(kdf: Kdf, aead: AeadId) -> Self { + Self { kdf, aead } + } + + #[must_use] + pub fn kdf(self) -> Kdf { + self.kdf + } + + #[must_use] + pub fn aead(self) -> AeadId { + self.aead + } +} + +/// The key configuration of a server. This can be used by both client and server. +/// An important invariant of this structure is that it does not include +/// any combination of KEM, KDF, and AEAD that is not supported. +pub struct KeyConfig { + key_id: KeyId, + kem: Kem, + symmetric: Vec<SymmetricSuite>, + sk: Option<PrivateKey>, + pk: PublicKey, +} + +impl KeyConfig { + fn strip_unsupported(symmetric: &mut Vec<SymmetricSuite>, kem: Kem) { + symmetric.retain(|s| HpkeConfig::new(kem, s.kdf(), s.aead()).supported()); + } + + /// Construct a configuration for the server side. + /// # Panics + /// If the configurations don't include a supported configuration. + pub fn new(key_id: u8, kem: Kem, mut symmetric: Vec<SymmetricSuite>) -> Res<Self> { + Self::strip_unsupported(&mut symmetric, kem); + assert!(!symmetric.is_empty()); + let (sk, pk) = generate_key_pair(kem)?; + Ok(Self { + key_id, + kem, + symmetric, + sk: Some(sk), + pk, + }) + } + + /// Derive a configuration for the server side from input keying material, + /// using the `DeriveKeyPair` functionality of the HPKE KEM defined here: + /// <https://www.ietf.org/archive/id/draft-irtf-cfrg-hpke-12.html#section-4> + /// # Panics + /// If the configurations don't include a supported configuration. + #[allow(unused)] + pub fn derive( + key_id: u8, + kem: Kem, + mut symmetric: Vec<SymmetricSuite>, + ikm: &[u8], + ) -> Res<Self> { + #[cfg(feature = "rust-hpke")] + { + Self::strip_unsupported(&mut symmetric, kem); + assert!(!symmetric.is_empty()); + let (sk, pk) = derive_key_pair(kem, ikm)?; + Ok(Self { + key_id, + kem, + symmetric, + sk: Some(sk), + pk, + }) + } + #[cfg(not(feature = "rust-hpke"))] + { + Err(Error::Unsupported) + } + } + + /// Encode into a wire format. This shares a format with the core of ECH: + /// + /// ```tls-format + /// opaque HpkePublicKey[Npk]; + /// uint16 HpkeKemId; // Defined in I-D.irtf-cfrg-hpke + /// uint16 HpkeKdfId; // Defined in I-D.irtf-cfrg-hpke + /// uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke + /// + /// struct { + /// HpkeKdfId kdf_id; + /// HpkeAeadId aead_id; + /// } ECHCipherSuite; + /// + /// struct { + /// uint8 key_id; + /// HpkeKemId kem_id; + /// HpkePublicKey public_key; + /// ECHCipherSuite cipher_suites<4..2^16-4>; + /// } ECHKeyConfig; + /// ``` + /// # Panics + /// Not as a result of this function. + pub fn encode(&self) -> Res<Vec<u8>> { + let mut buf = Vec::new(); + buf.write_u8(self.key_id)?; + buf.write_u16::<NetworkEndian>(u16::from(self.kem))?; + let pk_buf = self.pk.key_data()?; + buf.extend_from_slice(&pk_buf); + buf.write_u16::<NetworkEndian>((self.symmetric.len() * 4).try_into()?)?; + for s in &self.symmetric { + buf.write_u16::<NetworkEndian>(u16::from(s.kdf()))?; + buf.write_u16::<NetworkEndian>(u16::from(s.aead()))?; + } + Ok(buf) + } + + /// Construct a configuration from the encoded server configuration. + /// The format of `encoded_config` is the output of `Self::encode`. + fn parse(encoded_config: &[u8]) -> Res<Self> { + let mut r = BufReader::new(encoded_config); + let key_id = r.read_u8()?; + let kem = Kem::try_from(r.read_u16::<NetworkEndian>()?)?; + + // Note that the KDF and AEAD doesn't matter here. + let kem_config = HpkeConfig::new(kem, Kdf::HkdfSha256, AeadId::Aes128Gcm); + if !kem_config.supported() { + return Err(Error::Unsupported); + } + let mut pk_buf = vec![0; kem_config.kem().n_pk()]; + r.read_exact(&mut pk_buf)?; + + let sym_len = r.read_u16::<NetworkEndian>()?; + let mut sym = vec![0; usize::from(sym_len)]; + r.read_exact(&mut sym)?; + if sym.is_empty() || (sym.len() % 4 != 0) { + return Err(Error::Format); + } + let sym_count = sym.len() / 4; + let mut sym_r = BufReader::new(&sym[..]); + let mut symmetric = Vec::with_capacity(sym_count); + for _ in 0..sym_count { + let kdf = Kdf::try_from(sym_r.read_u16::<NetworkEndian>()?)?; + let aead = AeadId::try_from(sym_r.read_u16::<NetworkEndian>()?)?; + symmetric.push(SymmetricSuite::new(kdf, aead)); + } + + // Check that there was nothing extra. + let mut tmp = [0; 1]; + if r.read(&mut tmp)? > 0 { + return Err(Error::Format); + } + + Self::strip_unsupported(&mut symmetric, kem); + let pk = HpkeR::decode_public_key(kem_config.kem(), &pk_buf)?; + + Ok(Self { + key_id, + kem, + symmetric, + sk: None, + pk, + }) + } + + fn select(&self, sym: SymmetricSuite) -> Res<HpkeConfig> { + if self.symmetric.contains(&sym) { + let config = HpkeConfig::new(self.kem, sym.kdf(), sym.aead()); + Ok(config) + } else { + Err(Error::Unsupported) + } + } +} + +/// Construct the info parameter we use to initialize an `HpkeS` instance. +fn build_info(key_id: KeyId, config: HpkeConfig) -> Res<Vec<u8>> { + let mut info = Vec::with_capacity(INFO_LEN); + info.extend_from_slice(INFO_REQUEST); + info.push(0); + info.write_u8(key_id)?; + info.write_u16::<NetworkEndian>(u16::from(config.kem()))?; + info.write_u16::<NetworkEndian>(u16::from(config.kdf()))?; + info.write_u16::<NetworkEndian>(u16::from(config.aead()))?; + trace!("HPKE info: {}", hex::encode(&info)); + Ok(info) +} + +/// This is the sort of information we expect to receive from the receiver. +/// This might not be necessary if we agree on a format. +#[cfg(feature = "client")] +pub struct ClientRequest { + hpke: HpkeS, + header: Vec<u8>, +} + +#[cfg(feature = "client")] +impl ClientRequest { + /// Reads an encoded configuration and constructs a single use client sender. + /// See `KeyConfig::encode` for the structure details. + #[allow(clippy::similar_names)] // for `sk_s` and `pk_s` + pub fn new(encoded_config: &[u8]) -> Res<Self> { + let mut config = KeyConfig::parse(encoded_config)?; + // TODO(mt) choose the best config, not just the first. + let selected = config.select(config.symmetric[0])?; + + // Build the info, which contains the message header. + let info = build_info(config.key_id, selected)?; + let hpke = HpkeS::new(selected, &mut config.pk, &info)?; + + let header = Vec::from(&info[INFO_REQUEST.len() + 1..]); + debug_assert_eq!(header.len(), REQUEST_HEADER_LEN); + Ok(Self { hpke, header }) + } + + /// Encapsulate a request. This consumes this object. + /// This produces a response handler and the bytes of an encapsulated request. + pub fn encapsulate(mut self, request: &[u8]) -> Res<(Vec<u8>, ClientResponse)> { + let extra = + self.hpke.config().kem().n_enc() + self.hpke.config().aead().n_t() + request.len(); + let expected_len = self.header.len() + extra; + + let mut enc_request = self.header; + enc_request.reserve_exact(extra); + + let enc = self.hpke.enc()?; + enc_request.extend_from_slice(&enc); + + let mut ct = self.hpke.seal(&[], request)?; + enc_request.append(&mut ct); + + debug_assert_eq!(expected_len, enc_request.len()); + Ok((enc_request, ClientResponse::new(self.hpke, enc))) + } +} + +/// A server can handle multiple requests. +/// It holds a single key pair and can generate a configuration. +/// (A more complex server would have multiple key pairs. This is simple.) +#[cfg(feature = "server")] +pub struct Server { + config: KeyConfig, +} + +#[cfg(feature = "server")] +impl Server { + /// Create a new server configuration. + /// # Panics + /// If the configuration doesn't include a private key. + pub fn new(config: KeyConfig) -> Res<Self> { + assert!(config.sk.is_some()); + Ok(Self { config }) + } + + /// Get the configuration that this server uses. + #[must_use] + pub fn config(&self) -> &KeyConfig { + &self.config + } + + /// Remove encapsulation on a message. + /// # Panics + /// Not as a consequence of this code, but Rust won't know that for sure. + #[allow(clippy::similar_names)] // for kem_id and key_id + pub fn decapsulate(&mut self, enc_request: &[u8]) -> Res<(Vec<u8>, ServerResponse)> { + if enc_request.len() < REQUEST_HEADER_LEN { + return Err(Error::Truncated); + } + let mut r = BufReader::new(enc_request); + let key_id = r.read_u8()?; + if key_id != self.config.key_id { + return Err(Error::KeyId); + } + let kem_id = Kem::try_from(r.read_u16::<NetworkEndian>()?)?; + if kem_id != self.config.kem { + return Err(Error::InvalidKem); + } + let kdf_id = Kdf::try_from(r.read_u16::<NetworkEndian>()?)?; + let aead_id = AeadId::try_from(r.read_u16::<NetworkEndian>()?)?; + let sym = SymmetricSuite::new(kdf_id, aead_id); + + let info = build_info( + key_id, + HpkeConfig::new(self.config.kem, sym.kdf(), sym.aead()), + )?; + + let cfg = self.config.select(sym)?; + let mut enc = vec![0; cfg.kem().n_enc()]; + r.read_exact(&mut enc)?; + let mut hpke = HpkeR::new( + cfg, + &self.config.pk, + self.config.sk.as_mut().unwrap(), + &enc, + &info, + )?; + + let mut ct = Vec::new(); + r.read_to_end(&mut ct)?; + + let request = hpke.open(&[], &ct)?; + Ok((request, ServerResponse::new(&hpke, enc)?)) + } +} + +fn entropy(config: HpkeConfig) -> usize { + max(config.aead().n_n(), config.aead().n_k()) +} + +fn make_aead( + mode: Mode, + cfg: HpkeConfig, + exp: &impl Exporter, + enc: Vec<u8>, + response_nonce: &[u8], +) -> Res<Aead> { + let secret = exp.export(LABEL_RESPONSE, entropy(cfg))?; + let mut salt = enc; + salt.extend_from_slice(response_nonce); + + let hkdf = Hkdf::new(cfg.kdf()); + let prk = hkdf.extract(&salt, &secret)?; + + let key = hkdf.expand_key(&prk, INFO_KEY, KeyMechanism::Aead(cfg.aead()))?; + let iv = hkdf.expand_data(&prk, INFO_NONCE, cfg.aead().n_n())?; + let nonce_base = <[u8; NONCE_LEN]>::try_from(iv).unwrap(); + + Aead::new(mode, cfg.aead(), &key, nonce_base) +} + +/// An object for encapsulating responses. +/// The only way to obtain one of these is through `Server::decapsulate()`. +#[cfg(feature = "server")] +pub struct ServerResponse { + response_nonce: Vec<u8>, + aead: Aead, +} + +#[cfg(feature = "server")] +impl ServerResponse { + fn new(hpke: &HpkeR, enc: Vec<u8>) -> Res<Self> { + let response_nonce = random(entropy(hpke.config())); + let aead = make_aead(Mode::Encrypt, hpke.config(), hpke, enc, &response_nonce)?; + Ok(Self { + response_nonce, + aead, + }) + } + + /// Consume this object by encapsulating a response. + pub fn encapsulate(mut self, response: &[u8]) -> Res<Vec<u8>> { + let mut enc_response = self.response_nonce; + let mut ct = self.aead.seal(&[], response)?; + enc_response.append(&mut ct); + Ok(enc_response) + } +} + +/// An object for decapsulating responses. +/// The only way to obtain one of these is through `ClientRequest::encapsulate()`. +#[cfg(feature = "client")] +pub struct ClientResponse { + hpke: HpkeS, + enc: Vec<u8>, +} + +#[cfg(feature = "client")] +impl ClientResponse { + /// Private method for constructing one of these. + /// Doesn't do anything because we don't have the nonce yet, so + /// the work that can be done is limited. + fn new(hpke: HpkeS, enc: Vec<u8>) -> Self { + Self { hpke, enc } + } + + /// Consume this object by decapsulating a response. + pub fn decapsulate(self, enc_response: &[u8]) -> Res<Vec<u8>> { + let (response_nonce, ct) = enc_response.split_at(entropy(self.hpke.config())); + let mut aead = make_aead( + Mode::Decrypt, + self.hpke.config(), + &self.hpke, + self.enc, + response_nonce, + )?; + aead.open(&[], 0, ct) // 0 is the sequence number + } +} + +#[cfg(all(test, feature = "client", feature = "server"))] +mod test { + use crate::hpke::{Aead, Kdf, Kem}; + use crate::{ClientRequest, KeyConfig, KeyId, Server, SymmetricSuite}; + use log::trace; + + const KEY_ID: KeyId = 1; + const KEM: Kem = Kem::X25519Sha256; + const SYMMETRIC: &[SymmetricSuite] = &[ + SymmetricSuite::new(Kdf::HkdfSha256, Aead::Aes128Gcm), + SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305), + ]; + + const REQUEST: &[u8] = &[ + 0x00, 0x03, 0x47, 0x45, 0x54, 0x05, 0x68, 0x74, 0x74, 0x70, 0x73, 0x0b, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x01, 0x2f, + ]; + const RESPONSE: &[u8] = &[0x01, 0x40, 0xc8]; + + fn init() { + crate::init(); + let _ = env_logger::try_init(); + } + + #[test] + fn request_response() { + init(); + + let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let mut server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + let client = ClientRequest::new(&encoded_config).unwrap(); + let (enc_request, client_response) = client.encapsulate(REQUEST).unwrap(); + trace!("Request: {}", hex::encode(REQUEST)); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + let (request, server_response) = server.decapsulate(&enc_request).unwrap(); + assert_eq!(&request[..], REQUEST); + + let enc_response = server_response.encapsulate(RESPONSE).unwrap(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + let response = client_response.decapsulate(&enc_response).unwrap(); + assert_eq!(&response[..], RESPONSE); + trace!("Response: {}", hex::encode(RESPONSE)); + } + + #[test] + fn two_requests() { + init(); + + let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let mut server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + + let client1 = ClientRequest::new(&encoded_config).unwrap(); + let (enc_request1, client_response1) = client1.encapsulate(REQUEST).unwrap(); + let client2 = ClientRequest::new(&encoded_config).unwrap(); + let (enc_request2, client_response2) = client2.encapsulate(REQUEST).unwrap(); + assert_ne!(enc_request1, enc_request2); + + let (request1, server_response1) = server.decapsulate(&enc_request1).unwrap(); + assert_eq!(&request1[..], REQUEST); + let (request2, server_response2) = server.decapsulate(&enc_request2).unwrap(); + assert_eq!(&request2[..], REQUEST); + + let enc_response1 = server_response1.encapsulate(RESPONSE).unwrap(); + let enc_response2 = server_response2.encapsulate(RESPONSE).unwrap(); + assert_ne!(enc_response1, enc_response2); + + let response1 = client_response1.decapsulate(&enc_response1).unwrap(); + assert_eq!(&response1[..], RESPONSE); + let response2 = client_response2.decapsulate(&enc_response2).unwrap(); + assert_eq!(&response2[..], RESPONSE); + } + + #[test] + fn response_truncated() { + init(); + + let server_config = KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap(); + let mut server = Server::new(server_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + trace!("Config: {}", hex::encode(&encoded_config)); + + let client = ClientRequest::new(&encoded_config).unwrap(); + let (enc_request, client_response) = client.encapsulate(REQUEST).unwrap(); + trace!("Request: {}", hex::encode(REQUEST)); + trace!("Encapsulated Request: {}", hex::encode(&enc_request)); + + let (request, server_response) = server.decapsulate(&enc_request).unwrap(); + assert_eq!(&request[..], REQUEST); + + let enc_response = server_response.encapsulate(RESPONSE).unwrap(); + trace!("Encapsulated Response: {}", hex::encode(&enc_response)); + + assert!(client_response.decapsulate(&enc_response[..16]).is_err()); + } + + #[cfg(feature = "rust-hpke")] + #[test] + fn derive_key_pair() { + const IKM: &[u8] = &[ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + ]; + const EXPECTED_CONFIG: &[u8] = &[ + 0x01, 0x00, 0x20, 0xfc, 0x01, 0x38, 0x93, 0x64, 0x10, 0x31, 0x1a, 0x0c, 0x64, 0x1a, + 0x5c, 0xa0, 0x86, 0x39, 0x1d, 0xe8, 0xe7, 0x03, 0x82, 0x33, 0x3f, 0x6d, 0x64, 0x49, + 0x25, 0x21, 0xad, 0x7d, 0xc7, 0x8a, 0x5d, 0x00, 0x08, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x03, + ]; + + init(); + + let config = KeyConfig::parse(EXPECTED_CONFIG).unwrap(); + + let new_config = KeyConfig::derive(KEY_ID, KEM, Vec::from(SYMMETRIC), IKM).unwrap(); + assert_eq!(config.key_id, new_config.key_id); + assert_eq!(config.kem, new_config.kem); + assert_eq!(config.symmetric, new_config.symmetric); + + let server = Server::new(new_config).unwrap(); + let encoded_config = server.config().encode().unwrap(); + assert_eq!(EXPECTED_CONFIG, encoded_config); + } +} |