This commit is contained in:
nora 2024-07-20 13:59:05 +02:00
parent d4b0e9809f
commit b930bc0c5d
4 changed files with 199 additions and 182 deletions

70
src/crypto/aead.rs Normal file
View file

@ -0,0 +1,70 @@
use ring::aead::Nonce;
use crate::proto;
use super::{keys, TlsCiphertext, TlsInnerPlaintext};
pub struct AeadKey {
key: ring::aead::LessSafeKey,
}
impl AeadKey {
fn new(algorithm: proto::CipherSuite, key_bytes: &[u8]) -> Self {
Self {
key: ring::aead::LessSafeKey::new(
ring::aead::UnboundKey::new(proto_algo_to_ring(algorithm), key_bytes)
.expect("invalid key"),
),
}
}
}
fn proto_algo_to_ring(algo: proto::CipherSuite) -> &'static ring::aead::Algorithm {
match algo {
proto::CipherSuite::TLS_AES_128_GCM_SHA256 => &ring::aead::AES_128_GCM,
proto::CipherSuite::TLS_AES_256_GCM_SHA384 => &ring::aead::AES_256_GCM,
proto::CipherSuite::TLS_CHACHA20_POLY1305_SHA256 => &ring::aead::CHACHA20_POLY1305,
proto::CipherSuite::TLS_AES_128_CCM_SHA256 => todo!("TLS_AES_128_CCM_SHA256"),
proto::CipherSuite::TLS_AES_128_CCM_8_SHA256 => todo!("TLS_AES_128_CCM_8_SHA256"),
}
}
fn encrypt(key: AeadKey, message: &[u8], seq: u8, nonce: Nonce) -> Vec<u8> {
let total_len = message.len() + key.key.algorithm().tag_len();
let mut ciphertext_payload = Vec::with_capacity(total_len);
ciphertext_payload.extend_from_slice(message);
// FIXME: fill out the AAD properly
let aad = ring::aead::Aad::from([0; 5]);
key.key
.seal_in_place_append_tag(nonce, aad, &mut ciphertext_payload)
.unwrap();
ciphertext_payload
}
fn decrypt(key: AeadKey, msg: &mut [u8], nonce: ring::aead::Nonce) {
// FIXME: fill out the AAD properly
let aad = ring::aead::Aad::from([0; 5]);
key.key.open_in_place(nonce, aad, msg);
}
impl TlsCiphertext {
pub fn decrypt(mut self, secret: &[u8], nonce: Nonce) -> TlsInnerPlaintext {
let key = keys::hkdf_expand_label::<sha2::Sha256>(secret, b"key", &[]);
let iv = keys::hkdf_expand_label::<sha2::Sha256>(secret, b"iv", &[]);
let key = AeadKey::new(proto::CipherSuite::TLS_AES_128_GCM_SHA256, secret);
decrypt(key, &mut self.encrypted_record, nonce);
TlsInnerPlaintext {
content: self.encrypted_record,
content_type: 0,
padding_len: 0,
}
}
}

View file

@ -1,69 +1,16 @@
//! Module for encrypting `TLSPlaintext` records. use sha2::{digest::OutputSizeUser, Digest};
use crate::proto::{
self,
ser_de::{proto_enum, proto_struct, Value},
Handshake,
};
use hkdf::{hmac::Hmac, HmacImpl};
use ring::aead::{self, Nonce};
pub use seq::{SeqId, SeqIdGen};
use sha2::{
digest::{
core_api::{self, CoreProxy},
generic_array::ArrayLength,
typenum::Unsigned,
OutputSizeUser,
},
Digest,
};
use x25519_dalek::SharedSecret; use x25519_dalek::SharedSecret;
pub struct TlsCiphertext { use crate::proto::{self, ser_de::Value};
/// The encrypted [`TlsInnerPlaintext`] record.
pub encrypted_record: Vec<u8>,
}
impl From<Vec<u8>> for TlsCiphertext {
fn from(value: Vec<u8>) -> Self {
TlsCiphertext {
encrypted_record: value,
}
}
}
trait TlsHasher: OutputSizeUser { use super::{CryptoProvider, TlsHasher};
const ZEROED: &'static [u8];
fn expand(ikm: &[u8], label: &[u8], okm: &mut [u8]) -> Result<(), hkdf::InvalidLength>;
fn extract(salt: &[u8], ikm: &[u8]) -> Vec<u8>;
}
macro_rules! impl_hkdf_hasher {
() => {
const ZEROED: &'static [u8] =
&[0; <<Self as OutputSizeUser>::OutputSize as Unsigned>::USIZE];
fn expand(ikm: &[u8], label: &[u8], okm: &mut [u8]) -> Result<(), hkdf::InvalidLength> {
hkdf::Hkdf::<Self>::new(None, ikm).expand(&label, okm)
}
fn extract(salt: &[u8], ikm: &[u8]) -> Vec<u8> {
hkdf::Hkdf::<Self>::extract(Some(&[]), &[])
.0
.as_slice()
.to_vec()
}
};
}
impl TlsHasher for sha2::Sha256 {
impl_hkdf_hasher!();
}
impl TlsHasher for sha2::Sha384 {
impl_hkdf_hasher!();
}
// Key Schedule // Key Schedule
// https://datatracker.ietf.org/doc/html/rfc8446#section-7.1 // https://datatracker.ietf.org/doc/html/rfc8446#section-7.1
// The Hash function used by Transcript-Hash and HKDF is the cipher suite hash algorithm // The Hash function used by Transcript-Hash and HKDF is the cipher suite hash algorithm
fn hkdf_expand_label<H: TlsHasher>(secret: &[u8], label: &[u8], context: &[u8]) -> Vec<u8> { pub(super) fn hkdf_expand_label<H: TlsHasher>(secret: &[u8], label: &[u8], context: &[u8]) -> Vec<u8> {
proto_struct! { proto::ser_de::proto_struct! {
#[derive(Debug)] #[derive(Debug)]
pub struct HkdfLabel { pub struct HkdfLabel {
pub length: u16, pub length: u16,
@ -93,25 +40,10 @@ fn hkdf_expand_label<H: TlsHasher>(secret: &[u8], label: &[u8], context: &[u8])
/// Messages is the concatenation of the indicated handshake messages, /// Messages is the concatenation of the indicated handshake messages,
/// including the handshake message type and length fields, but not /// including the handshake message type and length fields, but not
/// including record layer headers. /// including record layer headers.
fn derive_secret<H: TlsHasher>(secret: &[u8], label: &[u8], messages_hash: &[u8]) -> Vec<u8> { pub(super) fn derive_secret<H: TlsHasher>(secret: &[u8], label: &[u8], messages_hash: &[u8]) -> Vec<u8> {
hkdf_expand_label::<H>(secret, label, messages_hash) hkdf_expand_label::<H>(secret, label, messages_hash)
} }
pub struct CryptoProvider {
zeroed_of_hash_size: &'static [u8],
hkdf_extract: fn(salt: &[u8], ikm: &[u8]) -> Vec<u8>,
derive_secret: fn(secret: &[u8], label: &[u8], messages_hash: &[u8]) -> Vec<u8>,
}
impl CryptoProvider {
fn new<H: TlsHasher>() -> Self {
CryptoProvider {
zeroed_of_hash_size: H::ZEROED,
hkdf_extract: H::extract,
derive_secret: derive_secret::<H>,
}
}
}
pub struct TranscriptHash { pub struct TranscriptHash {
state: sha2::Sha256, state: sha2::Sha256,
} }
@ -121,9 +53,9 @@ impl TranscriptHash {
state: sha2::Sha256::new(), state: sha2::Sha256::new(),
} }
} }
pub fn handshake(&mut self, handshake: &Handshake) { pub fn handshake(&mut self, handshake: &proto::Handshake) {
let mut buf = Vec::new(); let mut buf = Vec::new();
handshake.write(&mut buf); proto::ser_de::Value::write(handshake, &mut buf).unwrap();
self.state.update(&buf); self.state.update(&buf);
} }
pub fn get_current(&self) -> Vec<u8> { pub fn get_current(&self) -> Vec<u8> {
@ -256,104 +188,3 @@ impl KeysAfterServerHello {
todo!() todo!()
} }
} }
pub struct AeadKey {
key: aead::LessSafeKey,
}
impl AeadKey {
fn new(algorithm: proto::CipherSuite, key_bytes: &[u8]) -> Self {
Self {
key: aead::LessSafeKey::new(
aead::UnboundKey::new(proto_algo_to_ring(algorithm), key_bytes)
.expect("invalid key"),
),
}
}
}
mod seq {
use std::cell::Cell;
use ring::aead::{self, Nonce};
/// The sequence ID generator.
/// There is a separate one maintained for reading and writing.
pub struct SeqIdGen {
next: Cell<u64>,
}
impl SeqIdGen {
pub fn new() -> SeqIdGen {
SeqIdGen { next: Cell::new(0) }
}
pub fn next(&self) -> SeqId {
let next = self.next.get();
self.next.set(next.checked_add(1).unwrap());
SeqId(next)
}
}
// Don't implement `Clone` to ensure every seq id is only used once.
pub struct SeqId(u64);
impl SeqId {
pub fn to_nonce(self) -> Nonce {
let mut nonce = [0; aead::NONCE_LEN];
nonce[4..].copy_from_slice(&self.0.to_be_bytes());
Nonce::assume_unique_for_key(nonce)
}
}
}
fn proto_algo_to_ring(algo: proto::CipherSuite) -> &'static aead::Algorithm {
match algo {
proto::CipherSuite::TLS_AES_128_GCM_SHA256 => &aead::AES_128_GCM,
proto::CipherSuite::TLS_AES_256_GCM_SHA384 => &aead::AES_256_GCM,
proto::CipherSuite::TLS_CHACHA20_POLY1305_SHA256 => &aead::CHACHA20_POLY1305,
proto::CipherSuite::TLS_AES_128_CCM_SHA256 => todo!("TLS_AES_128_CCM_SHA256"),
proto::CipherSuite::TLS_AES_128_CCM_8_SHA256 => todo!("TLS_AES_128_CCM_8_SHA256"),
}
}
fn encrypt(key: AeadKey, message: &[u8], seq: u8, nonce: Nonce) -> Vec<u8> {
let total_len = message.len() + key.key.algorithm().tag_len();
let mut ciphertext_payload = Vec::with_capacity(total_len);
ciphertext_payload.extend_from_slice(message);
// FIXME: fill out the AAD properly
let aad = aead::Aad::from([0; 5]);
key.key
.seal_in_place_append_tag(nonce, aad, &mut ciphertext_payload)
.unwrap();
ciphertext_payload
}
fn decrypt(key: AeadKey, msg: &mut [u8], nonce: Nonce) {
// FIXME: fill out the AAD properly
let aad = aead::Aad::from([0; 5]);
key.key.open_in_place(nonce, aad, msg);
}
pub struct TlsInnerPlaintext {
/// The `TLSPlaintext.fragment`` value
pub content: Vec<u8>,
/// The `TLSPlaintext.type` value
pub content_type: u8,
pub padding_len: u16,
}
impl TlsCiphertext {
pub fn decrypt(mut self, secret: &[u8], nonce: Nonce) -> TlsInnerPlaintext {
let key = hkdf_expand_label::<sha2::Sha256>(secret, b"key", &[]);
let iv = hkdf_expand_label::<sha2::Sha256>(secret, b"iv", &[]);
let key = AeadKey::new(proto::CipherSuite::TLS_AES_128_GCM_SHA256, secret);
decrypt(key, &mut self.encrypted_record, nonce);
TlsInnerPlaintext {
content: self.encrypted_record,
content_type: 0,
padding_len: 0,
}
}
}

116
src/crypto/mod.rs Normal file
View file

@ -0,0 +1,116 @@
//! Cryptographic operations.
pub mod aead;
pub mod keys;
use crate::proto::{
self,
ser_de::{proto_enum, proto_struct, Value},
Handshake,
};
use hkdf::{hmac::Hmac, HmacImpl};
use ring::aead::Nonce;
pub use seq::{SeqId, SeqIdGen};
use sha2::{
digest::{
core_api::{self, CoreProxy},
generic_array::ArrayLength,
typenum::Unsigned,
OutputSizeUser,
},
Digest,
};
use x25519_dalek::SharedSecret;
pub struct TlsCiphertext {
/// The encrypted [`TlsInnerPlaintext`] record.
pub encrypted_record: Vec<u8>,
}
impl From<Vec<u8>> for TlsCiphertext {
fn from(value: Vec<u8>) -> Self {
TlsCiphertext {
encrypted_record: value,
}
}
}
trait TlsHasher: OutputSizeUser {
const ZEROED: &'static [u8];
fn expand(ikm: &[u8], label: &[u8], okm: &mut [u8]) -> Result<(), hkdf::InvalidLength>;
fn extract(salt: &[u8], ikm: &[u8]) -> Vec<u8>;
}
macro_rules! impl_hkdf_hasher {
() => {
const ZEROED: &'static [u8] =
&[0; <<Self as OutputSizeUser>::OutputSize as Unsigned>::USIZE];
fn expand(ikm: &[u8], label: &[u8], okm: &mut [u8]) -> Result<(), hkdf::InvalidLength> {
hkdf::Hkdf::<Self>::new(None, ikm).expand(&label, okm)
}
fn extract(salt: &[u8], ikm: &[u8]) -> Vec<u8> {
hkdf::Hkdf::<Self>::extract(Some(&[]), &[])
.0
.as_slice()
.to_vec()
}
};
}
impl TlsHasher for sha2::Sha256 {
impl_hkdf_hasher!();
}
impl TlsHasher for sha2::Sha384 {
impl_hkdf_hasher!();
}
pub struct CryptoProvider {
zeroed_of_hash_size: &'static [u8],
hkdf_extract: fn(salt: &[u8], ikm: &[u8]) -> Vec<u8>,
derive_secret: fn(secret: &[u8], label: &[u8], messages_hash: &[u8]) -> Vec<u8>,
}
impl CryptoProvider {
fn new<H: TlsHasher>() -> Self {
CryptoProvider {
zeroed_of_hash_size: H::ZEROED,
hkdf_extract: H::extract,
derive_secret: keys::derive_secret::<H>,
}
}
}
mod seq {
use std::cell::Cell;
use ring::aead::{self, Nonce};
/// The sequence ID generator.
/// There is a separate one maintained for reading and writing.
pub struct SeqIdGen {
next: Cell<u64>,
}
impl SeqIdGen {
pub fn new() -> SeqIdGen {
SeqIdGen { next: Cell::new(0) }
}
pub fn next(&self) -> SeqId {
let next = self.next.get();
self.next.set(next.checked_add(1).unwrap());
SeqId(next)
}
}
// Don't implement `Clone` to ensure every seq id is only used once.
pub struct SeqId(u64);
impl SeqId {
pub fn to_nonce(self) -> Nonce {
let mut nonce = [0; aead::NONCE_LEN];
nonce[4..].copy_from_slice(&self.0.to_be_bytes());
Nonce::assume_unique_for_key(nonce)
}
}
}
pub struct TlsInnerPlaintext {
/// The `TLSPlaintext.fragment`` value
pub content: Vec<u8>,
/// The `TLSPlaintext.type` value
pub content_type: u8,
pub padding_len: u16,
}

View file

@ -1,6 +1,6 @@
#![allow(unused)] #![allow(unused)]
mod crypt; mod crypto;
pub mod proto; pub mod proto;
use std::{ use std::{
@ -9,7 +9,7 @@ use std::{
io::{self, Read, Write}, io::{self, Read, Write},
}; };
use crypt::{KeysAfterServerHello, TranscriptHash}; use crypto::keys::{KeysAfterServerHello, TranscriptHash};
use proto::{ser_de::Value, CipherSuite}; use proto::{ser_de::Value, CipherSuite};
use crate::proto::TLSPlaintext; use crate::proto::TLSPlaintext;
@ -38,7 +38,7 @@ struct ClientSetupConnection<W> {
mod stream_state { mod stream_state {
use std::io::{Read, Write}; use std::io::{Read, Write};
use crate::crypt::{SeqId, SeqIdGen}; use crate::crypto::{SeqId, SeqIdGen};
use crate::proto::{self, TLSPlaintext}; use crate::proto::{self, TLSPlaintext};
use crate::Result; use crate::Result;
@ -317,7 +317,7 @@ impl<W: Read + Write> ClientSetupConnection<W> {
dh_shared_secret.as_bytes() dh_shared_secret.as_bytes()
); );
let keys = crypt::KeysAfterServerHello::compute( let keys = KeysAfterServerHello::compute(
dh_shared_secret, dh_shared_secret,
*cipher_suite, *cipher_suite,
&transcript.borrow(), &transcript.borrow(),
@ -336,7 +336,7 @@ impl<W: Read + Write> ClientSetupConnection<W> {
return unexpected_message!("expected ApplicationData, got {frame:?}"); return unexpected_message!("expected ApplicationData, got {frame:?}");
}; };
// Encrypted with server_handshake_traffic_secret // Encrypted with server_handshake_traffic_secret
crypt::TlsCiphertext::from(data).decrypt( crypto::TlsCiphertext::from(data).decrypt(
&keys &keys
.borrow() .borrow()
.as_ref() .as_ref()