Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions ohttp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion ohttp/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion ohttp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
171 changes: 48 additions & 123 deletions ohttp/src/rh/aead.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -20,67 +24,29 @@ pub enum Mode {
Decrypt,
}

enum AeadEngine {
Aes128Gcm(Box<Aes128Gcm>),
Aes256Gcm(Box<Aes256Gcm>),
ChaCha20Poly1305(Box<ChaCha20Poly1305>),
}

// 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<Vec<u8>> {
let tag = match self {
Self::Aes128Gcm(e) => e.encrypt(Nonce::<Aes128Gcm>::from_slice(nonce), pt)?,
Self::Aes256Gcm(e) => e.encrypt(Nonce::<Aes256Gcm>::from_slice(nonce), pt)?,
Self::ChaCha20Poly1305(e) => {
e.encrypt(Nonce::<ChaCha20Poly1305>::from_slice(nonce), pt)?
}
};
Ok(tag)
}
fn decrypt(&mut self, nonce: &[u8], pt: Payload) -> Res<Vec<u8>> {
let tag = match self {
Self::Aes128Gcm(e) => e.decrypt(Nonce::<Aes128Gcm>::from_slice(nonce), pt)?,
Self::Aes256Gcm(e) => e.decrypt(Nonce::<Aes256Gcm>::from_slice(nonce), pt)?,
Self::ChaCha20Poly1305(e) => {
e.decrypt(Nonce::<ChaCha20Poly1305>::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<Self> {
let aead = match algorithm {
AeadId::Aes128Gcm => AeadEngine::Aes128Gcm(Box::new(Aes128Gcm::new(
Key::<Aes128Gcm>::from_slice(key.as_ref()),
))),
AeadId::Aes256Gcm => AeadEngine::Aes256Gcm(Box::new(Aes256Gcm::new(
Key::<Aes256Gcm>::from_slice(key.as_ref()),
))),
AeadId::ChaCha20Poly1305 => AeadEngine::ChaCha20Poly1305(Box::new(
ChaCha20Poly1305::new(Key::<ChaCha20Poly1305>::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,
})
Expand All @@ -92,27 +58,41 @@ impl Aead {
Ok(SymKey::from(k))
}

fn nonce(&self, seq: SequenceNumber) -> Vec<u8> {
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<Vec<u8>> {
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<Vec<u8>> {
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)
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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());
}
}
89 changes: 62 additions & 27 deletions ohttp/src/rh/hkdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Hash>(key: &[u8], data: &[u8]) -> Vec<u8>
where
<T as Hash>::Bytes: AsRef<[u8]>,
{
let mut engine = HmacEngine::<T>::new(key);
engine.input(data);
Hmac::<T>::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<T: Hash>(salt: &[u8], ikm: &[u8]) -> Vec<u8>
where
<T as Hash>::Bytes: AsRef<[u8]>,
{
hmac::<T>(salt, ikm)
}

/// RFC 5869 HKDF-Expand. Errors if `len` would need more than 255 blocks.
fn expand_with<T: Hash>(prk: &[u8], info: &[u8], len: usize) -> Res<Vec<u8>>
where
<T as Hash>::Bytes: AsRef<[u8]>,
{
let mut okm: Vec<u8> = Vec::with_capacity(len);
let mut block: Vec<u8> = Vec::new();
let mut counter: u8 = 1;
while okm.len() < len {
let mut engine = HmacEngine::<T>::new(prk);
engine.input(&block);
engine.input(info);
engine.input(&[counter]);
block = Hmac::<T>::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 {
Expand Down Expand Up @@ -48,17 +96,14 @@ impl Hkdf {

#[allow(clippy::unnecessary_wraps)]
pub fn extract(&self, salt: &[u8], ikm: &SymKey) -> Res<SymKey> {
let prk = match self {
Self::Sha256 => {
SymKey::from(HkdfImpl::<Sha256>::extract(Some(salt), &ikm.0).0.as_slice())
}
Self::Sha384 => {
SymKey::from(HkdfImpl::<Sha384>::extract(Some(salt), &ikm.0).0.as_slice())
let prk = SymKey::from(
match self {
Self::Sha256 => extract_with::<sha256::Hash>(salt, &ikm.0),
Self::Sha384 => extract_with::<sha384::Hash>(salt, &ikm.0),
Self::Sha512 => extract_with::<sha512::Hash>(salt, &ikm.0),
}
Self::Sha512 => {
SymKey::from(HkdfImpl::<Sha512>::extract(Some(salt), &ikm.0).0.as_slice())
}
};
.as_slice(),
);
trace!(
"HKDF extract: salt={} ikm={:?} prk={:?}",
hex::encode(salt),
Expand All @@ -80,21 +125,11 @@ impl Hkdf {
}

pub fn expand_data(&self, prk: &SymKey, info: &[u8], len: usize) -> Res<Vec<u8>> {
let mut okm = vec![0; len];
match self {
Self::Sha256 => {
let h = HkdfImpl::<Sha256>::from_prk(&prk.0).map_err(|_| Error::Internal)?;
h.expand(info, &mut okm).map_err(|_| Error::Internal)?;
}
Self::Sha384 => {
let h = HkdfImpl::<Sha384>::from_prk(&prk.0).map_err(|_| Error::Internal)?;
h.expand(info, &mut okm).map_err(|_| Error::Internal)?;
}
Self::Sha512 => {
let h = HkdfImpl::<Sha512>::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::<sha256::Hash>(&prk.0, info, len)?,
Self::Sha384 => expand_with::<sha384::Hash>(&prk.0, info, len)?,
Self::Sha512 => expand_with::<sha512::Hash>(&prk.0, info, len)?,
};
trace!(
"HKDF expand_data: prk={:?} info={} len={} okm={:?}",
prk,
Expand Down
Loading