Skip to content

Commit

Permalink
[#783] Encrypt retry token
Browse files Browse the repository at this point in the history
  • Loading branch information
kansi committed Oct 5, 2020
1 parent a68e18c commit 01134c5
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 52 deletions.
27 changes: 13 additions & 14 deletions quinn-proto/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::crypto::types::{Certificate, CertificateChain, PrivateKey};
use crate::{
cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator},
congestion,
crypto::{self, ClientConfig as _, HmacKey as _, ServerConfig as _},
crypto::{self, ClientConfig as _, HandshakeTokenKey as _, HmacKey as _, ServerConfig as _},
VarInt,
};

Expand Down Expand Up @@ -413,8 +413,9 @@ where
/// Must be set to use TLS 1.3 only.
pub crypto: S::ServerConfig,

/// Private key used to authenticate data included in handshake tokens.
pub(crate) token_key: Arc<S::HmacKey>,
/// Used to generate one-time AEAD keys to protect handshake tokens
pub(crate) token_key: Arc<S::HandshakeTokenKey>,

/// Whether to require clients to prove ownership of an address before committing resources.
///
/// Introduces an additional round-trip to the handshake to make denial of service attacks more difficult.
Expand All @@ -438,13 +439,13 @@ impl<S> ServerConfig<S>
where
S: crypto::Session,
{
/// Create a default config with a particular `token_key`
pub fn new(token_key: S::HmacKey) -> Self {
/// Create a default config with a particular `master_key`
pub fn new(prk: S::HandshakeTokenKey) -> Self {
Self {
transport: Arc::new(TransportConfig::default()),
crypto: S::ServerConfig::new(),

token_key: Arc::new(token_key),
token_key: Arc::new(prk),
use_stateless_retry: false,
retry_token_lifetime: 15_000_000,

Expand All @@ -455,8 +456,8 @@ where
}

/// Private key used to authenticate data included in handshake tokens.
pub fn token_key(&mut self, value: &[u8]) -> Result<&mut Self, ConfigError> {
self.token_key = Arc::new(S::HmacKey::new(value)?);
pub fn token_key(&mut self, master_key: &[u8]) -> Result<&mut Self, ConfigError> {
self.token_key = Arc::new(S::HandshakeTokenKey::from_secret(&master_key));
Ok(self)
}

Expand Down Expand Up @@ -529,12 +530,10 @@ where
fn default() -> Self {
let rng = &mut rand::thread_rng();

let mut token_key = vec![0; S::HmacKey::KEY_LEN];
rng.fill_bytes(&mut token_key);
Self::new(
S::HmacKey::new(&token_key)
.expect("HMAC key rejected random bytes; use ServerConfig::new instead"),
)
let mut master_key = [0u8; 64];
rng.fill_bytes(&mut master_key);

Self::new(S::HandshakeTokenKey::from_secret(&master_key))
}
}

Expand Down
25 changes: 25 additions & 0 deletions quinn-proto/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ pub trait Session: Send + Sized {
type ClientConfig: ClientConfig<Self>;
/// Type used to sign various values
type HmacKey: HmacKey;
/// Key used to generate one-time-use handshake token keys
type HandshakeTokenKey: HandshakeTokenKey;
/// Type of keys used to protect packet headers
type HeaderKey: HeaderKey;
/// Type used to represent packet protection keys
Expand Down Expand Up @@ -210,3 +212,26 @@ pub trait HmacKey: Send + Sized + Sync {
/// This error occurs if the requested output length is too large.
#[derive(Debug, PartialEq, Eq)]
pub struct ExportKeyingMaterialError;

/// A pseudo random key for HKDF
pub trait HandshakeTokenKey: Send + Sized + Sync {
/// AEAD key type
type AeadKey: AeadKey;

/// Derive AEAD using hkdf
fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Self::AeadKey;
/// Method to build pseudo random key from existing bytes
fn from_secret(secret: &[u8]) -> Self;
}

/// A key for sealing data with AEAD-based algorithms
pub trait AeadKey {
/// Length of AEAD Key
const KEY_LEN: usize;

// fn from_hkdf(master_key: &impl PseudoRandomKey, random_bytes: &[u8]) -> Self;
/// Method for sealing message `data`
fn seal(&self, data: &mut Vec<u8>, additional_data: &[u8]) -> Result<(), ()>;
/// Method for opening a sealed message `data`
fn open<'a>(&self, data: &'a mut [u8], additional_data: &[u8]) -> Result<&'a mut [u8], ()>;
}
38 changes: 37 additions & 1 deletion quinn-proto/src/crypto/ring.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ring::{aead, hmac};
use ring::{aead, hkdf, hmac};

use crate::{
config::ConfigError,
Expand Down Expand Up @@ -70,3 +70,39 @@ impl crypto::HmacKey for hmac::Key {
hmac::verify(self, data, signature).map_err(|_| ())
}
}

impl crypto::HandshakeTokenKey for hkdf::Prk {
type AeadKey = ring::aead::LessSafeKey;

fn aead_from_hkdf(&self, random_bytes: &[u8]) -> Self::AeadKey {
let mut key_buffer = [0u8; 32];
let info = [random_bytes];
let okm = self.expand(&info, hkdf::HKDF_SHA256).unwrap();

okm.fill(&mut key_buffer).unwrap();

let key = aead::UnboundKey::new(&aead::AES_256_GCM, &key_buffer).unwrap();
Self::AeadKey::new(key)
}

fn from_secret(bytes: &[u8]) -> Self {
hkdf::Salt::new(hkdf::HKDF_SHA256, &[]).extract(bytes)
}
}

impl crypto::AeadKey for aead::LessSafeKey {
const KEY_LEN: usize = 32;

fn seal(&self, data: &mut Vec<u8>, additional_data: &[u8]) -> Result<(), ()> {
let aad = ring::aead::Aad::from(additional_data);
let zero_nonce = ring::aead::Nonce::assume_unique_for_key([0u8; 12]);
self.seal_in_place_append_tag(zero_nonce, aad, data)
.map_err(|_| ())
}

fn open<'a>(&self, data: &'a mut [u8], additional_data: &[u8]) -> Result<&'a mut [u8], ()> {
let aad = ring::aead::Aad::from(additional_data);
let zero_nonce = ring::aead::Nonce::assume_unique_for_key([0u8; 12]);
self.open_in_place(zero_nonce, aad, data).map_err(|_| ())
}
}
3 changes: 2 additions & 1 deletion quinn-proto/src/crypto/rustls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
};

use bytes::BytesMut;
use ring::{aead, aead::quic::HeaderProtectionKey, hmac};
use ring::{aead, aead::quic::HeaderProtectionKey, hkdf, hmac};
pub use rustls::TLSError;
use rustls::{
self,
Expand Down Expand Up @@ -50,6 +50,7 @@ impl crypto::Session for TlsSession {
type Identity = CertificateChain;
type ClientConfig = Arc<rustls::ClientConfig>;
type HmacKey = hmac::Key;
type HandshakeTokenKey = hkdf::Prk;
type PacketKey = PacketKey;
type HeaderKey = HeaderProtectionKey;
type ServerConfig = Arc<rustls::ServerConfig>;
Expand Down
7 changes: 6 additions & 1 deletion quinn-proto/src/endpoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,16 +548,21 @@ where
let (retry_src_cid, orig_dst_cid) = if server_config.use_stateless_retry {
if token.is_empty() {
// First Initial
let mut random_bytes = vec![0u8; RetryToken::RANDOM_BYTES_LEN];
self.rng.fill_bytes(&mut random_bytes);

let token = RetryToken {
orig_dst_cid: dst_cid,
issued: SystemTime::now(),
random_bytes: &random_bytes,
}
.encode(&*server_config.token_key, &remote, &temp_loc_cid);
let mut buf = Vec::new();

let header = Header::Retry {
src_cid: temp_loc_cid,
dst_cid: src_cid,
};
let mut buf = Vec::new();
let encode = header.encode(&mut buf);
buf.put_slice(&token);
buf.extend_from_slice(&S::retry_tag(&dst_cid, &buf));
Expand Down
141 changes: 106 additions & 35 deletions quinn-proto/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bytes::BufMut;

use crate::{
coding::{BufExt, BufMutExt},
crypto::HmacKey,
crypto::{AeadKey, HandshakeTokenKey, HmacKey},
shared::ConnectionId,
RESET_TOKEN_SIZE,
};
Expand All @@ -19,20 +19,27 @@ use crate::{
// - AEAD nonce is always set to 0
// in other words, for each ticket, use different key derived from random using HKDF

pub struct RetryToken {
pub struct RetryToken<'a> {
/// The destination connection ID set in the very first packet from the client
pub orig_dst_cid: ConnectionId,
/// The time at which this token was issued
pub issued: SystemTime,
/// Random bytes for deriving AEAD key
pub random_bytes: &'a [u8],
}

impl RetryToken {
impl<'a> RetryToken<'a> {
const MAX_ADDITIONAL_DATA_SIZE: usize = 39; // max(ipv4, ipv6) + port + retry_src_cid
pub const RANDOM_BYTES_LEN: usize = 32;

pub fn encode(
&self,
key: &impl HmacKey,
key: &impl HandshakeTokenKey,
address: &SocketAddr,
retry_src_cid: &ConnectionId,
) -> Vec<u8> {
let aead_key = key.aead_from_hkdf(self.random_bytes);

let mut buf = Vec::new();

self.orig_dst_cid.encode_long(&mut buf);
Expand All @@ -43,46 +50,59 @@ impl RetryToken {
.unwrap_or(0),
);

let signature_pos = buf.len();
let mut additional_data = [0u8; Self::MAX_ADDITIONAL_DATA_SIZE];
let mut cursor = &mut additional_data[..];
match address.ip() {
IpAddr::V4(x) => buf.put_slice(&x.octets()),
IpAddr::V6(x) => buf.put_slice(&x.octets()),
IpAddr::V4(x) => cursor.put_slice(&x.octets()),
IpAddr::V6(x) => cursor.put_slice(&x.octets()),
}
buf.write(address.port());
retry_src_cid.encode_long(&mut buf);

let signature = key.sign(&buf);
// No reason to actually encode the IP in the token, since we always have the remote addr for an incoming packet.
buf.truncate(signature_pos);
buf.extend_from_slice(signature.as_ref());
buf
cursor.write(address.port());
retry_src_cid.encode_long(&mut cursor);

let size = Self::MAX_ADDITIONAL_DATA_SIZE - cursor.len();
aead_key.seal(&mut buf, &additional_data[..size]).unwrap();

let mut token = Vec::new();
token.put_slice(self.random_bytes);
token.put_slice(&buf);
token
}

pub fn from_bytes(
key: &impl HmacKey,
key: &impl HandshakeTokenKey,
address: &SocketAddr,
retry_src_cid: &ConnectionId,
data: &[u8],
raw_token_bytes: &'a [u8],
) -> Result<Self, ()> {
let mut reader = io::Cursor::new(data);
if raw_token_bytes.len() < Self::RANDOM_BYTES_LEN {
// Invalid length
return Err(());
}

let orig_dst_cid = ConnectionId::decode_long(&mut reader).ok_or(())?;
let issued = UNIX_EPOCH + Duration::new(reader.get::<u64>().map_err(|_| ())?, 0);
let random_bytes = &raw_token_bytes[..Self::RANDOM_BYTES_LEN];
let aead_key = key.aead_from_hkdf(random_bytes);
let mut sealed_token = raw_token_bytes[Self::RANDOM_BYTES_LEN..].to_vec();

let signature_start = reader.position() as usize;
let mut buf = Vec::new();
buf.put_slice(&data[0..signature_start]);
let mut additional_data = [0u8; Self::MAX_ADDITIONAL_DATA_SIZE];
let mut cursor = &mut additional_data[..];
match address.ip() {
IpAddr::V4(x) => buf.put_slice(&x.octets()),
IpAddr::V6(x) => buf.put_slice(&x.octets()),
IpAddr::V4(x) => cursor.put_slice(&x.octets()),
IpAddr::V6(x) => cursor.put_slice(&x.octets()),
}
buf.write(address.port());
retry_src_cid.encode_long(&mut buf);
cursor.write(address.port());
retry_src_cid.encode_long(&mut cursor);

let size = Self::MAX_ADDITIONAL_DATA_SIZE - cursor.len();
let data = aead_key.open(&mut sealed_token, &additional_data[..size])?;
let mut reader = io::Cursor::new(data);

let orig_dst_cid = ConnectionId::decode_long(&mut reader).ok_or(())?;
let issued = UNIX_EPOCH + Duration::new(reader.get::<u64>().map_err(|_| ())?, 0);

key.verify(&buf, &data[signature_start..]).map_err(|_| ())?;
Ok(Self {
orig_dst_cid,
issued,
random_bytes,
})
}
}
Expand Down Expand Up @@ -141,29 +161,80 @@ mod test {
fn token_sanity() {
use super::*;
use crate::cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator};
use crate::MAX_CID_SIZE;
use crate::{crypto, MAX_CID_SIZE};

use rand::RngCore;
use ring::hmac;
use std::{
net::Ipv6Addr,
time::{Duration, UNIX_EPOCH},
};

let mut key = [0; 64];
rand::thread_rng().fill_bytes(&mut key);
let key = <hmac::Key as HmacKey>::new(&key).unwrap();
let rng = &mut rand::thread_rng();

let mut master_key = [0; 64];
rng.fill_bytes(&mut master_key);

let mut random_bytes = [0; 32];
rng.fill_bytes(&mut random_bytes);

let mut master_key = vec![0u8; 64];
rng.fill_bytes(&mut master_key);

let prk: ring::hkdf::Prk = crypto::HandshakeTokenKey::from_secret(&master_key);

let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433);
let (retry_src_cid, _) = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid();
let token = RetryToken {
orig_dst_cid: RandomConnectionIdGenerator::new(MAX_CID_SIZE)
.generate_cid()
.0,
issued: UNIX_EPOCH + Duration::new(42, 0), // Fractional seconds would be lost
random_bytes: &random_bytes,
};
let encoded = token.encode(&key, &addr, &retry_src_cid);
let decoded = RetryToken::from_bytes(&key, &addr, &retry_src_cid, &encoded)
let encoded = token.encode(&prk, &addr, &retry_src_cid);

let decoded = RetryToken::from_bytes(&prk, &addr, &retry_src_cid, &encoded)
.expect("token didn't validate");
assert_eq!(token.orig_dst_cid, decoded.orig_dst_cid);
assert_eq!(token.issued, decoded.issued);
}

#[cfg(feature = "ring")]
#[test]
fn invalid_token_returns_err() {
use super::*;
use crate::cid_generator::{ConnectionIdGenerator, RandomConnectionIdGenerator};
use crate::{crypto, MAX_CID_SIZE};
use rand::RngCore;
use std::net::Ipv6Addr;

let rng = &mut rand::thread_rng();

let mut master_key = [0; 64];
rng.fill_bytes(&mut master_key);

let mut random_bytes = [0; 32];
rng.fill_bytes(&mut random_bytes);

let prk: ring::hkdf::Prk = crypto::HandshakeTokenKey::from_secret(&master_key);

let addr = SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 4433);
let (retry_src_cid, _) = RandomConnectionIdGenerator::new(MAX_CID_SIZE).generate_cid();

let mut invalid_token = Vec::new();
invalid_token.put_slice(&random_bytes);

let mut random_data = [0; 32];
rand::thread_rng().fill_bytes(&mut random_data);
invalid_token.put_slice(&random_data);

// Assert: garbage sealed data with valid random bytes returns err
assert!(RetryToken::from_bytes(&prk, &addr, &retry_src_cid, &invalid_token).is_err());

let invalid_token = [0; 31];
rand::thread_rng().fill_bytes(&mut random_bytes);

// Assert: completely invalid retry token returns error
assert!(RetryToken::from_bytes(&prk, &addr, &retry_src_cid, &invalid_token).is_err());
}
}

0 comments on commit 01134c5

Please sign in to comment.