Skip to content

Commit

Permalink
Add TLS Certificate Compression (RFC8879)
Browse files Browse the repository at this point in the history
  • Loading branch information
sharksforarms committed Apr 16, 2021
1 parent 9fe0521 commit 310088d
Show file tree
Hide file tree
Showing 15 changed files with 484 additions and 25 deletions.
7 changes: 5 additions & 2 deletions bogo/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
"EchoTLS13CompatibilitySessionID": "",
"ClientOCSPCallback*": "ocsp not supported yet",
"ServerOCSPCallback*": "",
"CertCompression*": "not implemented",
"DuplicateCertCompressionExt*": "",
"ECH-*": "",
"ALPS-*": "",
"ExtraClientEncryptedExtension-TLS-TLS13": "uses ALPS",
Expand Down Expand Up @@ -135,6 +133,11 @@
":ENCRYPTED_LENGTH_TOO_LONG:": ":GARBAGE:"
},
"TestErrorMap": {
"CertCompressionTooLargeClient-TLS13": ":CERT_DECOMPRESSION_FAILED:",
"DuplicateCertCompressionExt-TLS12": ":PEER_MISBEHAVIOUR:",
"DuplicateCertCompressionExt2-TLS12": ":PEER_MISBEHAVIOUR:",
"DuplicateCertCompressionExt-TLS13": ":PEER_MISBEHAVIOUR:",
"DuplicateCertCompressionExt2-TLS13": ":PEER_MISBEHAVIOUR:",
"EmptyCertificateList": ":NO_CERTS:",
"SendInvalidRecordType": ":GARBAGE:",
"NoSharedCipher": ":HANDSHAKE_FAILURE:",
Expand Down
71 changes: 71 additions & 0 deletions rustls/examples/internal/bogo_shim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ use rustls::quic;
use rustls::quic::ClientQuicExt;
use rustls::quic::ServerQuicExt;
use rustls::ClientHello;
use rustls::{
CertificateCompress, CertificateCompression, CertificateCompressionAlgorithm,
CertificateDecompress,
};
use std::env;
use std::fs;
use std::io;
Expand Down Expand Up @@ -77,6 +81,7 @@ struct Options {
expect_accept_early_data: bool,
expect_reject_early_data: bool,
expect_version: u16,
install_cert_compression_algs: bool,
}

impl Options {
Expand Down Expand Up @@ -124,6 +129,7 @@ impl Options {
expect_accept_early_data: false,
expect_reject_early_data: false,
expect_version: 0,
install_cert_compression_algs: false,
}
}

Expand Down Expand Up @@ -161,6 +167,58 @@ fn load_key(filename: &str) -> rustls::PrivateKey {
rustls::PrivateKey(keys[0].clone())
}

fn get_certificate_compression_algorithms() -> Vec<Arc<CertificateCompression>> {
let mut cc_algs = Vec::new();

// Add `expanding` algorithm
// https://github.com/google/boringssl/blob/a2278d4d2cabe73f6663e3299ea7808edfa306b9/ssl/test/runner/runner.go#L15930
//
// expanding_prefix is just some arbitrary byte string. This has to match the value in the shim.
let expanding_prefix = [1, 2, 3, 4].as_ref();
cc_algs.push(Arc::new(CertificateCompression {
alg: CertificateCompressionAlgorithm::Unknown(0xff02),
compress: CertificateCompress::new(Box::new(move |writer: Vec<u8>, input: &[u8]| {
let mut w = writer;
w.extend_from_slice(expanding_prefix);
w.extend_from_slice(input);

Ok(w)
})),
decompress: CertificateDecompress::new(Box::new(move |_writer: Vec<u8>, input: &[u8]| {
if !input.starts_with(expanding_prefix) {
panic!("cannot decompress certificate message {:x?}", input);
}

Ok(input[expanding_prefix.len()..].to_vec())
})),
}));

// Add `shrinking` algorithm
// https://github.com/google/boringssl/blob/a2278d4d2cabe73f6663e3299ea7808edfa306b9/ssl/test/runner/runner.go#L15912
//
// shrinking_prefix is the first two bytes of a Certificate message
let shrinking_prefix = [0, 0].as_ref();
cc_algs.push(Arc::new(CertificateCompression {
alg: CertificateCompressionAlgorithm::Unknown(0xff01),
compress: CertificateCompress::new(Box::new(move |_writer: Vec<u8>, input: &[u8]| {
if !input.starts_with(&shrinking_prefix) {
panic!("cannot compress certificate message {:x?}", input);
}

Ok(input[shrinking_prefix.len()..].to_vec())
})),
decompress: CertificateDecompress::new(Box::new(move |writer: Vec<u8>, input: &[u8]| {
let mut w = writer;
w.extend_from_slice(shrinking_prefix);
w.extend_from_slice(input);

Ok(w)
})),
}));

cc_algs
}

fn split_protocols(protos: &str) -> Vec<String> {
let mut ret = Vec::new();

Expand Down Expand Up @@ -394,6 +452,10 @@ fn make_server_cfg(opts: &Options) -> Arc<rustls::ServerConfig> {
.collect();
}

if opts.install_cert_compression_algs {
cfg.certificate_compression_algorithms = get_certificate_compression_algorithms();
}

Arc::new(cfg)
}

Expand Down Expand Up @@ -481,6 +543,10 @@ fn make_client_cfg(opts: &Options) -> Arc<rustls::ClientConfig> {
.collect();
}

if opts.install_cert_compression_algs {
cfg.certificate_compression_algorithms = get_certificate_compression_algorithms();
}

Arc::new(cfg)
}

Expand Down Expand Up @@ -537,6 +603,8 @@ fn handle_err(err: rustls::Error) -> ! {
quit(":WRONG_SIGNATURE_TYPE:")
}
Error::PeerSentOversizedRecord => quit(":DATA_LENGTH_TOO_LONG:"),
TLSError::FailedCertificateDecompression => quit(":CERT_DECOMPRESSION_FAILED:"),
TLSError::UnknownCertCompressionAlg => quit(":UNKNOWN_CERT_COMPRESSION_ALG:"),
_ => {
println_err!("unhandled error: {:?}", err);
quit(":FIXME:")
Expand Down Expand Up @@ -981,6 +1049,9 @@ fn main() {
opts.curves = Some(vec![ curve ]);
}
}
"-install-cert-compression-algs" => {
opts.install_cert_compression_algs = true;
}

// defaults:
"-enable-all-curves" |
Expand Down
1 change: 1 addition & 0 deletions rustls/src/client/common.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::kx;
#[cfg(feature = "logging")]
use crate::log::trace;
use crate::msgs::enums::CertificateCompressionAlgorithm;
use crate::msgs::enums::ExtensionType;
use crate::msgs::enums::NamedGroup;
use crate::msgs::handshake::CertificatePayload;
Expand Down
15 changes: 15 additions & 0 deletions rustls/src/client/hs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,21 @@ fn emit_client_hello_for_retry(
exts.push(ClientExtension::PresharedKeyModes(psk_modes));
}

if support_tls13
&& !sess
.config
.certificate_compression_algorithms
.is_empty()
{
exts.push(ClientExtension::CompressCertificate(
sess.config
.certificate_compression_algorithms
.iter()
.map(|v| v.alg)
.collect(),
))
}

if !conn.config.alpn_protocols.is_empty() {
exts.push(ClientExtension::Protocols(ProtocolNameList::from_slices(
&conn
Expand Down
5 changes: 5 additions & 0 deletions rustls/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ pub struct ClientConfig {
/// How to decide what client auth certificate/keys to use.
pub client_auth_cert_resolver: Arc<dyn ResolvesClientCert>,

/// Certificate compression algorithms.
/// If None, certificate compression won't be enabled
pub certificate_compression_algorithms: Vec<Arc<CertificateCompression>>,

/// Whether to support RFC5077 tickets. You must provide a working
/// `session_persistence` member for this to have any meaningful
/// effect.
Expand Down Expand Up @@ -189,6 +193,7 @@ impl ClientConfig {
session_persistence: handy::ClientSessionMemoryCache::new(32),
mtu: None,
client_auth_cert_resolver: Arc::new(handy::FailResolveClientCert {}),
certificate_compression_algorithms: Vec::new(),
enable_tickets: true,
versions: vec![ProtocolVersion::TLSv1_3, ProtocolVersion::TLSv1_2],
enable_sni: true,
Expand Down
89 changes: 78 additions & 11 deletions rustls/src/client/tls13.rs
Original file line number Diff line number Diff line change
Expand Up @@ -524,11 +524,48 @@ impl hs::State for ExpectCertificate {
conn: &mut ClientConnection,
m: Message,
) -> hs::NextStateOrError {
let cert_chain = require_handshake_msg!(
m,
HandshakeType::Certificate,
HandshakePayload::CertificateTLS13
)?;
let decompressed_cert_chain = if m.is_handshake_type(HandshakeType::CompressedCertificate) {
let compressed_cert_chain = require_handshake_msg!(
m,
HandshakeType::CompressedCertificate,
HandshakePayload::CompressedCertificate
)?;

self.handshake
.certificate_compression_algorithm = Some(compressed_cert_chain.algorithm);

let cert_chain = if let Some(decompress) = &sess
.config
.certificate_compression_algorithms
.iter()
.find(|cc| cc.alg == compressed_cert_chain.algorithm)
.map(|v| &v.decompress)
{
compressed_cert_chain.decompress(decompress)
} else {
Err(TLSError::UnknownCertCompressionAlg)
};

if cert_chain.is_err() {
sess.common
.send_fatal_alert(AlertDescription::BadCertificate);
}

cert_chain?
} else {
None
};

let cert_chain = if let Some(cert_chain) = decompressed_cert_chain.as_ref() {
cert_chain
} else {
require_handshake_msg!(
m,
HandshakeType::Certificate,
HandshakePayload::CertificateTLS13
)?
};

self.transcript.add_message(&m);

// This is only non-empty for client auth.
Expand Down Expand Up @@ -598,10 +635,13 @@ impl hs::State for ExpectCertificateOrCertReq {
&[ContentType::Handshake],
&[
HandshakeType::Certificate,
HandshakeType::CompressedCertificate,
HandshakeType::CertificateRequest,
],
)?;
if m.is_handshake_type(HandshakeType::Certificate) {
if m.is_handshake_type(HandshakeType::Certificate)
|| m.is_handshake_type(HandshakeType::CompressedCertificate)
{
Box::new(ExpectCertificate {
dns_name: self.dns_name,
randoms: self.randoms,
Expand Down Expand Up @@ -820,7 +860,7 @@ fn emit_certificate_tls13(
transcript: &mut HandshakeHash,
client_auth: &mut ClientAuthDetails,
conn: &mut ClientConnection,
) {
) -> Result<(), TLSError> {
let context = client_auth
.auth_context
.take()
Expand All @@ -839,16 +879,43 @@ fn emit_certificate_tls13(
}
}


let payload = if let Some(certificate_compression_algorithm) =
handshake.certificate_compression_algorithm
{
let compressed_cert_payload = if let Some(compressor) = &sess
.config
.certificate_compression_algorithms
.iter()
.find(|cc| cc.alg == certificate_compression_algorithm)
.map(|v| &v.compress)
{
cert_payload.compress_with(certificate_compression_algorithm, compressor)
} else {
Err(TLSError::UnknownCertCompressionAlg)
}?;

MessagePayload::Handshake(HandshakeMessagePayload {
typ: HandshakeType::CompressedCertificate,
payload: HandshakePayload::CompressedCertificate(compressed_cert_payload),
})
} else {
MessagePayload::Handshake(HandshakeMessagePayload {
typ: HandshakeType::Certificate,
payload: HandshakePayload::CertificateTLS13(cert_payload),
})
};

let m = Message {
typ: ContentType::Handshake,
version: ProtocolVersion::TLSv1_3,
payload: MessagePayload::Handshake(HandshakeMessagePayload {
typ: HandshakeType::Certificate,
payload: HandshakePayload::CertificateTLS13(cert_payload),
}),
payload,
};

transcript.add_message(&m);
conn.common.send_msg(m, true);

Ok(())
}

fn emit_certverify_tls13(
Expand Down
55 changes: 55 additions & 0 deletions rustls/src/compression.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! Compression related operations

use crate::msgs::enums::CertificateCompressionAlgorithm;
use std::io::Result;

/// A certificate compression algorithm described
/// as a pair of compression and decompression
/// functions used to compress and decompress a certificate
pub struct CertificateCompression {
/// compression algorithm
pub alg: CertificateCompressionAlgorithm,

/// compression function
pub compress: CertificateCompress,

/// decompression function
pub decompress: CertificateDecompress,
}

type CompressionFn = Box<dyn (Fn(Vec<u8>, &[u8]) -> Result<Vec<u8>>) + Send + Sync>;

/// A compression function which returns the compressed representation
/// of the input
pub struct CertificateCompress(CompressionFn);

impl CertificateCompress {
/// Create a new CertificateCompress
pub fn new(compression_fn: CompressionFn) -> Self {
Self(compression_fn)
}

/// Call the compression function
pub(crate) fn compress(&self, writer: Vec<u8>, input: &[u8]) -> Result<Vec<u8>> {
self.0(writer, input)
}
}

type DecompressionFn = CompressionFn;

/// A decompression function which returns the decompressed representation
/// of the input. The input to this function is a pre-allocated vector
/// of the expected length of the uncompressed input
pub struct CertificateDecompress(DecompressionFn);

impl CertificateDecompress {
/// Create a new CertificateDecompress
pub fn new(decompression_fn: DecompressionFn) -> Self {
Self(decompression_fn)
}

/// Call the decompression function
pub(crate) fn decompress(&self, writer: Vec<u8>, input: &[u8]) -> Result<Vec<u8>> {
self.0(writer, input)
}
}
Loading

0 comments on commit 310088d

Please sign in to comment.