diff --git a/ohttp/Cargo.toml b/ohttp/Cargo.toml index 0765f7d..e016a3b 100644 --- a/ohttp/Cargo.toml +++ b/ohttp/Cargo.toml @@ -17,16 +17,14 @@ external-sqlite = [] gecko = ["nss", "mozbuild"] nss = ["bindgen", "regex-mess"] regex-mess = ["regex", "regex-automata", "regex-syntax"] -rust-hpke = ["rand", "aead", "aes-gcm", "chacha20poly1305", "hkdf", "sha2", "bitcoin-hpke"] +rust-hpke = ["rand", "chacha20-poly1305", "bitcoin_hashes", "bitcoin-hpke"] server = [] [dependencies] -aead = {version = "0.4", optional = true, features = ["std"]} -aes-gcm = {version = "0.9", optional = true} byteorder = "1.4" -chacha20poly1305 = {version = "0.8", optional = true} +chacha20-poly1305 = {version = "0.2", optional = true, default-features = false} hex = "0.4" -hkdf = {version = "0.11", optional = true} +bitcoin_hashes = {version = "0.14", optional = true, default-features = false} bitcoin-hpke = {version = "0.20.0", optional = true, default-features = false, features = ["std", "secp"]} lazy_static = "1.4" log = {version = "0.4", default-features = false} @@ -36,7 +34,6 @@ rand = {version = "0.8", optional = true} regex = {version = "~1.9", optional = true} regex-automata = {version = "~0.3", optional = true} regex-syntax = {version = "~0.7", optional = true} -sha2 = {version = "0.9", optional = true} thiserror = "1" [build-dependencies] diff --git a/ohttp/src/err.rs b/ohttp/src/err.rs index bf65d87..5d652a7 100644 --- a/ohttp/src/err.rs +++ b/ohttp/src/err.rs @@ -4,7 +4,7 @@ use thiserror::Error; pub enum Error { #[cfg(feature = "rust-hpke")] #[error("a problem occurred with the AEAD")] - Aead(#[from] aead::Error), + Aead, #[cfg(feature = "nss")] #[error("a problem occurred during cryptographic processing: {0}")] Crypto(#[from] crate::nss::Error), diff --git a/ohttp/src/lib.rs b/ohttp/src/lib.rs index 1e8b0e8..2da6fe5 100644 --- a/ohttp/src/lib.rs +++ b/ohttp/src/lib.rs @@ -395,7 +395,7 @@ mod test { match res.unwrap_err() { Error::Truncated => {} #[cfg(feature = "rust-hpke")] - Error::Aead(_) => {} + Error::Aead => {} #[cfg(feature = "nss")] Error::Crypto(_) => {} Error::Io(e) => assert_eq!(e.kind(), ErrorKind::UnexpectedEof), diff --git a/ohttp/src/rh/aead.rs b/ohttp/src/rh/aead.rs index b89c96e..ecf712b 100644 --- a/ohttp/src/rh/aead.rs +++ b/ohttp/src/rh/aead.rs @@ -1,16 +1,20 @@ #![allow(dead_code)] // TODO: remove -use super::SymKey; -use crate::{err::Res, hpke::Aead as AeadId}; -use aead::{AeadMut, Key, NewAead, Nonce, Payload}; -use aes_gcm::{Aes128Gcm, Aes256Gcm}; -use chacha20poly1305::ChaCha20Poly1305; use std::convert::TryFrom; +use chacha20_poly1305::{ChaCha20Poly1305, Key, Nonce}; + +use super::SymKey; +use crate::{ + err::{Error, Res}, + hpke::Aead as AeadId, +}; + /// All the nonces are the same length. Exploit that. pub const NONCE_LEN: usize = 12; const COUNTER_LEN: usize = 8; const TAG_LEN: usize = 16; +const KEY_LEN: usize = 32; type SequenceNumber = u64; @@ -20,67 +24,29 @@ pub enum Mode { Decrypt, } -enum AeadEngine { - Aes128Gcm(Box), - Aes256Gcm(Box), - ChaCha20Poly1305(Box), -} - -// Dispatch functions; this just shows how janky that this sort of abstraction can be. -// If this grows too much, this is fairly clearly responsive to using a macro. -impl AeadEngine { - fn encrypt(&mut self, nonce: &[u8], pt: Payload) -> Res> { - let tag = match self { - Self::Aes128Gcm(e) => e.encrypt(Nonce::::from_slice(nonce), pt)?, - Self::Aes256Gcm(e) => e.encrypt(Nonce::::from_slice(nonce), pt)?, - Self::ChaCha20Poly1305(e) => { - e.encrypt(Nonce::::from_slice(nonce), pt)? - } - }; - Ok(tag) - } - fn decrypt(&mut self, nonce: &[u8], pt: Payload) -> Res> { - let tag = match self { - Self::Aes128Gcm(e) => e.decrypt(Nonce::::from_slice(nonce), pt)?, - Self::Aes256Gcm(e) => e.decrypt(Nonce::::from_slice(nonce), pt)?, - Self::ChaCha20Poly1305(e) => { - e.decrypt(Nonce::::from_slice(nonce), pt)? - } - }; - Ok(tag) - } -} - -/// A switch-hitting AEAD that uses a selected primitive. +/// ChaCha20-Poly1305, the only AEAD this crate implements. `bitcoin-hpke` dropped +/// the AES-GCM schemes in 0.13.0, so the GCM suites were never usable end to end. pub struct Aead { mode: Mode, - engine: AeadEngine, + key: [u8; KEY_LEN], nonce_base: [u8; NONCE_LEN], seq: SequenceNumber, } impl Aead { - #[allow(clippy::unnecessary_wraps)] pub fn new( mode: Mode, algorithm: AeadId, key: &SymKey, nonce_base: [u8; NONCE_LEN], ) -> Res { - let aead = match algorithm { - AeadId::Aes128Gcm => AeadEngine::Aes128Gcm(Box::new(Aes128Gcm::new( - Key::::from_slice(key.as_ref()), - ))), - AeadId::Aes256Gcm => AeadEngine::Aes256Gcm(Box::new(Aes256Gcm::new( - Key::::from_slice(key.as_ref()), - ))), - AeadId::ChaCha20Poly1305 => AeadEngine::ChaCha20Poly1305(Box::new( - ChaCha20Poly1305::new(Key::::from_slice(key.as_ref())), - )), - }; + if algorithm != AeadId::ChaCha20Poly1305 { + return Err(Error::Unsupported); + } + let key = <[u8; KEY_LEN]>::try_from(key.as_ref()).map_err(|_| Error::Unsupported)?; Ok(Self { mode, - engine: aead, + key, nonce_base, seq: 0, }) @@ -92,27 +58,41 @@ impl Aead { Ok(SymKey::from(k)) } - fn nonce(&self, seq: SequenceNumber) -> Vec { - let mut nonce = Vec::from(self.nonce_base); + fn nonce(&self, seq: SequenceNumber) -> [u8; NONCE_LEN] { + let mut nonce = self.nonce_base; for (i, n) in nonce.iter_mut().rev().take(COUNTER_LEN).enumerate() { *n ^= u8::try_from((seq >> (8 * i)) & 0xff).unwrap(); } nonce } + fn cipher(&self, nonce: [u8; NONCE_LEN]) -> ChaCha20Poly1305 { + ChaCha20Poly1305::new(Key::new(self.key), Nonce::new(nonce)) + } + + #[allow(clippy::unnecessary_wraps)] // Res is part of the interface shared with the NSS backend pub fn seal(&mut self, aad: &[u8], pt: &[u8]) -> Res> { assert_eq!(self.mode, Mode::Encrypt); - // A copy for the nonce generator to write into. But we don't use the value. let nonce = self.nonce(self.seq); self.seq += 1; - let ct = self.engine.encrypt(&nonce, Payload { msg: pt, aad })?; + let mut ct = pt.to_vec(); + let tag = self.cipher(nonce).encrypt(&mut ct, Some(aad)); + ct.extend_from_slice(&tag); Ok(ct) } pub fn open(&mut self, aad: &[u8], seq: SequenceNumber, ct: &[u8]) -> Res> { assert_eq!(self.mode, Mode::Decrypt); + if ct.len() < TAG_LEN { + return Err(Error::Truncated); + } + let (body, tag) = ct.split_at(ct.len() - TAG_LEN); + let tag = <[u8; TAG_LEN]>::try_from(tag).map_err(|_| Error::Truncated)?; let nonce = self.nonce(seq); - let pt = self.engine.decrypt(&nonce, Payload { msg: ct, aad })?; + let mut pt = body.to_vec(); + self.cipher(nonce) + .decrypt(&mut pt, tag, Some(aad)) + .map_err(|_| Error::Aead)?; Ok(pt) } } @@ -161,73 +141,8 @@ mod test { assert_eq!(&plaintext[..], pt); } - /// This tests the AEAD in QUIC in combination with the HKDF code. - /// This is an AEAD-only example. - #[test] - fn quic_retry() { - const KEY: &[u8] = &[ - 0xbe, 0x0c, 0x69, 0x0b, 0x9f, 0x66, 0x57, 0x5a, 0x1d, 0x76, 0x6b, 0x54, 0xe3, 0x68, - 0xc8, 0x4e, - ]; - const NONCE: &[u8; NONCE_LEN] = &[ - 0x46, 0x15, 0x99, 0xd3, 0x5d, 0x63, 0x2b, 0xf2, 0x23, 0x98, 0x25, 0xbb, - ]; - const AAD: &[u8] = &[ - 0x08, 0x83, 0x94, 0xc8, 0xf0, 0x3e, 0x51, 0x57, 0x08, 0xff, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x08, 0xf0, 0x67, 0xa5, 0x50, 0x2a, 0x42, 0x62, 0xb5, 0x74, 0x6f, 0x6b, 0x65, - 0x6e, - ]; - const CT: &[u8] = &[ - 0x04, 0xa2, 0x65, 0xba, 0x2e, 0xff, 0x4d, 0x82, 0x90, 0x58, 0xfb, 0x3f, 0x0f, 0x24, - 0x96, 0xba, - ]; - check0(AeadId::Aes128Gcm, KEY, NONCE, AAD, &[], CT); - } - - #[test] - fn quic_server_initial() { - const ALG: AeadId = AeadId::Aes128Gcm; - const KEY: &[u8] = &[ - 0xcf, 0x3a, 0x53, 0x31, 0x65, 0x3c, 0x36, 0x4c, 0x88, 0xf0, 0xf3, 0x79, 0xb6, 0x06, - 0x7e, 0x37, - ]; - const NONCE_BASE: &[u8; NONCE_LEN] = &[ - 0x0a, 0xc1, 0x49, 0x3c, 0xa1, 0x90, 0x58, 0x53, 0xb0, 0xbb, 0xa0, 0x3e, - ]; - // Note that this integrates the sequence number of 1 from the example, - // otherwise we can't use a sequence number of 0 to encrypt. - const NONCE: &[u8; NONCE_LEN] = &[ - 0x0a, 0xc1, 0x49, 0x3c, 0xa1, 0x90, 0x58, 0x53, 0xb0, 0xbb, 0xa0, 0x3f, - ]; - const AAD: &[u8] = &[ - 0xc1, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0xf0, 0x67, 0xa5, 0x50, 0x2a, 0x42, 0x62, - 0xb5, 0x00, 0x40, 0x75, 0x00, 0x01, - ]; - const PT: &[u8] = &[ - 0x02, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x40, 0x5a, 0x02, 0x00, 0x00, 0x56, 0x03, - 0x03, 0xee, 0xfc, 0xe7, 0xf7, 0xb3, 0x7b, 0xa1, 0xd1, 0x63, 0x2e, 0x96, 0x67, 0x78, - 0x25, 0xdd, 0xf7, 0x39, 0x88, 0xcf, 0xc7, 0x98, 0x25, 0xdf, 0x56, 0x6d, 0xc5, 0x43, - 0x0b, 0x9a, 0x04, 0x5a, 0x12, 0x00, 0x13, 0x01, 0x00, 0x00, 0x2e, 0x00, 0x33, 0x00, - 0x24, 0x00, 0x1d, 0x00, 0x20, 0x9d, 0x3c, 0x94, 0x0d, 0x89, 0x69, 0x0b, 0x84, 0xd0, - 0x8a, 0x60, 0x99, 0x3c, 0x14, 0x4e, 0xca, 0x68, 0x4d, 0x10, 0x81, 0x28, 0x7c, 0x83, - 0x4d, 0x53, 0x11, 0xbc, 0xf3, 0x2b, 0xb9, 0xda, 0x1a, 0x00, 0x2b, 0x00, 0x02, 0x03, - 0x04, - ]; - const CT: &[u8] = &[ - 0x5a, 0x48, 0x2c, 0xd0, 0x99, 0x1c, 0xd2, 0x5b, 0x0a, 0xac, 0x40, 0x6a, 0x58, 0x16, - 0xb6, 0x39, 0x41, 0x00, 0xf3, 0x7a, 0x1c, 0x69, 0x79, 0x75, 0x54, 0x78, 0x0b, 0xb3, - 0x8c, 0xc5, 0xa9, 0x9f, 0x5e, 0xde, 0x4c, 0xf7, 0x3c, 0x3e, 0xc2, 0x49, 0x3a, 0x18, - 0x39, 0xb3, 0xdb, 0xcb, 0xa3, 0xf6, 0xea, 0x46, 0xc5, 0xb7, 0x68, 0x4d, 0xf3, 0x54, - 0x8e, 0x7d, 0xde, 0xb9, 0xc3, 0xbf, 0x9c, 0x73, 0xcc, 0x3f, 0x3b, 0xde, 0xd7, 0x4b, - 0x56, 0x2b, 0xfb, 0x19, 0xfb, 0x84, 0x02, 0x2f, 0x8e, 0xf4, 0xcd, 0xd9, 0x37, 0x95, - 0xd7, 0x7d, 0x06, 0xed, 0xbb, 0x7a, 0xaf, 0x2f, 0x58, 0x89, 0x18, 0x50, 0xab, 0xbd, - 0xca, 0x3d, 0x20, 0x39, 0x8c, 0x27, 0x64, 0x56, 0xcb, 0xc4, 0x21, 0x58, 0x40, 0x7d, - 0xd0, 0x74, 0xee, - ]; - check0(ALG, KEY, NONCE, AAD, PT, CT); - decrypt(ALG, KEY, NONCE_BASE, 1, AAD, PT, CT); - } - + /// The QUIC ChaCha20-Poly1305 sample (RFC 9001 A.5). Pins the AEAD to a published + /// vector so a cipher swap cannot silently change the wire format. #[test] fn quic_chacha() { const ALG: AeadId = AeadId::ChaCha20Poly1305; @@ -254,4 +169,14 @@ mod test { // Now use the real nonce and sequence number from the example. decrypt(ALG, KEY, NONCE_BASE, 654_360_564, AAD, PT, CT); } + + /// The AES-GCM suites were never implementable here, so construction must fail + /// cleanly rather than half-work. + #[test] + fn aes_gcm_unsupported() { + init(); + let k = Aead::import_key(AeadId::ChaCha20Poly1305, &[0; 32]).unwrap(); + assert!(Aead::new(Mode::Encrypt, AeadId::Aes128Gcm, &k, [0; NONCE_LEN]).is_err()); + assert!(Aead::new(Mode::Encrypt, AeadId::Aes256Gcm, &k, [0; NONCE_LEN]).is_err()); + } } diff --git a/ohttp/src/rh/hkdf.rs b/ohttp/src/rh/hkdf.rs index aeb3a8d..7537633 100644 --- a/ohttp/src/rh/hkdf.rs +++ b/ohttp/src/rh/hkdf.rs @@ -5,9 +5,57 @@ use crate::{ err::{Error, Res}, hpke::{Aead, Kdf}, }; -use hkdf::Hkdf as HkdfImpl; +use bitcoin_hashes::{Hash, HashEngine, Hmac, HmacEngine, sha256, sha384, sha512}; use log::trace; -use sha2::{Sha256, Sha384, Sha512}; + +/// One HMAC-Hash invocation over `data` keyed with `key`. +fn hmac(key: &[u8], data: &[u8]) -> Vec +where + ::Bytes: AsRef<[u8]>, +{ + let mut engine = HmacEngine::::new(key); + engine.input(data); + Hmac::::from_engine(engine) + .as_byte_array() + .as_ref() + .to_vec() +} + +/// RFC 5869 HKDF-Extract. An empty salt and a HashLen-of-zeros salt are equivalent +/// here, since HMAC zero-pads the key to the block size either way. +fn extract_with(salt: &[u8], ikm: &[u8]) -> Vec +where + ::Bytes: AsRef<[u8]>, +{ + hmac::(salt, ikm) +} + +/// RFC 5869 HKDF-Expand. Errors if `len` would need more than 255 blocks. +fn expand_with(prk: &[u8], info: &[u8], len: usize) -> Res> +where + ::Bytes: AsRef<[u8]>, +{ + let mut okm: Vec = Vec::with_capacity(len); + let mut block: Vec = Vec::new(); + let mut counter: u8 = 1; + while okm.len() < len { + let mut engine = HmacEngine::::new(prk); + engine.input(&block); + engine.input(info); + engine.input(&[counter]); + block = Hmac::::from_engine(engine) + .as_byte_array() + .as_ref() + .to_vec(); + okm.extend_from_slice(&block); + if okm.len() >= len { + break; + } + counter = counter.checked_add(1).ok_or(Error::Internal)?; + } + okm.truncate(len); + Ok(okm) +} #[derive(Clone, Copy)] pub enum KeyMechanism { @@ -48,17 +96,14 @@ impl Hkdf { #[allow(clippy::unnecessary_wraps)] pub fn extract(&self, salt: &[u8], ikm: &SymKey) -> Res { - let prk = match self { - Self::Sha256 => { - SymKey::from(HkdfImpl::::extract(Some(salt), &ikm.0).0.as_slice()) - } - Self::Sha384 => { - SymKey::from(HkdfImpl::::extract(Some(salt), &ikm.0).0.as_slice()) + let prk = SymKey::from( + match self { + Self::Sha256 => extract_with::(salt, &ikm.0), + Self::Sha384 => extract_with::(salt, &ikm.0), + Self::Sha512 => extract_with::(salt, &ikm.0), } - Self::Sha512 => { - SymKey::from(HkdfImpl::::extract(Some(salt), &ikm.0).0.as_slice()) - } - }; + .as_slice(), + ); trace!( "HKDF extract: salt={} ikm={:?} prk={:?}", hex::encode(salt), @@ -80,21 +125,11 @@ impl Hkdf { } pub fn expand_data(&self, prk: &SymKey, info: &[u8], len: usize) -> Res> { - let mut okm = vec![0; len]; - match self { - Self::Sha256 => { - let h = HkdfImpl::::from_prk(&prk.0).map_err(|_| Error::Internal)?; - h.expand(info, &mut okm).map_err(|_| Error::Internal)?; - } - Self::Sha384 => { - let h = HkdfImpl::::from_prk(&prk.0).map_err(|_| Error::Internal)?; - h.expand(info, &mut okm).map_err(|_| Error::Internal)?; - } - Self::Sha512 => { - let h = HkdfImpl::::from_prk(&prk.0).map_err(|_| Error::Internal)?; - h.expand(info, &mut okm).map_err(|_| Error::Internal)?; - } - } + let okm = match self { + Self::Sha256 => expand_with::(&prk.0, info, len)?, + Self::Sha384 => expand_with::(&prk.0, info, len)?, + Self::Sha512 => expand_with::(&prk.0, info, len)?, + }; trace!( "HKDF expand_data: prk={:?} info={} len={} okm={:?}", prk,