Skip to content

Commit

Permalink
encrypted client hello (ECH) config support
Browse files Browse the repository at this point in the history
This commit updates pemfile to support reading PEM encoded encrypted
client hello (ECH) items from PEM files. We identify these based on the
"ECHCONFIG" header value proposed in draft-farrell-tls-pemesni[0].

For convenience, a `server_ech_configs` fn is added that expects
the exact format described in draft-farrell-tls-pemesni for servers;
a PEM encoded PKCS#8 encoded private key followed by the PEM encoded
TLS encoded ECH config list. Both items are mandatory and an error
is raised if missing.

In some cases (e.g. testing client support) it may be sufficient to only
read an iterator of the ECH config list items from a PEM file, skipping
private keys or other items. For this the `ech_configs` fn
is added, matching similar helpers for other item types.

[0]: https://github.com/sftcd/pemesni/blob/44bcf7259f204a60421ea05be02a1e2859cadaa9/draft-farrell-tls-pemesni.txt
  • Loading branch information
cpu committed May 30, 2024
1 parent c6aafcc commit 193dde5
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 13 deletions.
68 changes: 64 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,16 @@ mod tests;

/// --- Main crate APIs:
mod pemfile;

#[cfg(feature = "std")]
pub use pemfile::{read_all, read_one};
pub use pemfile::{read_one_from_slice, Error, Item};
#[cfg(feature = "std")]
use pki_types::PrivateKeyDer;
#[cfg(feature = "std")]
use pki_types::{
CertificateDer, CertificateRevocationListDer, CertificateSigningRequestDer, PrivatePkcs1KeyDer,
PrivatePkcs8KeyDer, PrivateSec1KeyDer,
CertificateDer, CertificateRevocationListDer, CertificateSigningRequestDer, EchConfigListBytes,
PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer,
};

#[cfg(feature = "std")]
Expand Down Expand Up @@ -104,7 +105,9 @@ pub fn private_key(rd: &mut dyn io::BufRead) -> Result<Option<PrivateKeyDer<'sta
Item::Pkcs1Key(key) => return Ok(Some(key.into())),
Item::Pkcs8Key(key) => return Ok(Some(key.into())),
Item::Sec1Key(key) => return Ok(Some(key.into())),
Item::X509Certificate(_) | Item::Crl(_) | Item::Csr(_) => continue,
Item::X509Certificate(_) | Item::Crl(_) | Item::Csr(_) | Item::EchConfigs(_) => {
continue
}
}
}

Expand All @@ -126,7 +129,8 @@ pub fn csr(
| Item::Pkcs8Key(_)
| Item::Sec1Key(_)
| Item::X509Certificate(_)
| Item::Crl(_) => continue,
| Item::Crl(_)
| Item::EchConfigs(_) => continue,
}
}

Expand Down Expand Up @@ -192,3 +196,59 @@ pub fn ec_private_keys(
_ => None,
})
}

/// Return a PKCS#8 private key and Encrypted Client Hello (ECH) config list from `rd`.
///
/// Both are mandatory and must be present in the input. The file should begin with the PEM
/// encoded PKCS#8 private key, followed by the PEM encoded ECH config list.
///
/// See [draft-farrell-tls-pemesni.txt] and [draft-ietf-tls-esni §4][draft-ietf-tls-esni]
/// for more information.
///
/// [draft-farrell-tls-pemesni.txt]: https://github.com/sftcd/pemesni/blob/44bcf7259f204a60421ea05be02a1e2859cadaa9/draft-farrell-tls-pemesni.txt
/// [draft-ietf-tls-esni]: https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#section-4
#[cfg(feature = "std")]
pub fn server_ech_configs(
rd: &mut dyn io::BufRead,
) -> Result<(PrivatePkcs8KeyDer<'static>, EchConfigListBytes<'static>), io::Error> {
// draft-farrell-tls-pemesni specifies the PEM format for a server's ECH config as the PEM
// delimited base64 encoding of a PKCS#8 private key, and then subsequently the PEM
// delimited base64 encoding of a TLS encoded ECH config. Both are mandatory.

let Ok(Some(Item::Pkcs8Key(private_key))) = read_one(rd) else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Missing mandatory PKCS#8 private key",
));
};

let Ok(Some(Item::EchConfigs(ech_configs))) = read_one(rd) else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Missing mandatory ECH config",
));
};

Ok((private_key, ech_configs))
}

/// Return an iterator over Encrypted Client Hello (ECH) configs from `rd`.
///
/// Each ECH config is expected to be a PEM-delimited ("-----BEGIN ECH CONFIG-----") BASE64
/// encoding of a TLS encoded ECHConfigList structure, as described in
/// [draft-ietf-tls-esni §4][draft-ietf-tls-esni].
///
/// For server configurations that require both a private key and a config, prefer
/// [server_ech_config].
///
/// [draft-ietf-tls-esni]: https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#section-4
#[cfg(feature = "std")]
pub fn ech_configs(
rd: &mut dyn io::BufRead,
) -> impl Iterator<Item = Result<EchConfigListBytes<'static>, io::Error>> + '_ {
iter::from_fn(move || read_one(rd).transpose()).filter_map(|item| match item {
Ok(Item::EchConfigs(ech_configs)) => Some(Ok(ech_configs)),
Err(err) => Some(Err(err)),
_ => None,
})
}
29 changes: 20 additions & 9 deletions src/pemfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use core::ops::ControlFlow;
use std::io::{self, ErrorKind};

use pki_types::{
CertificateDer, CertificateRevocationListDer, CertificateSigningRequestDer, PrivatePkcs1KeyDer,
PrivatePkcs8KeyDer, PrivateSec1KeyDer,
CertificateDer, CertificateRevocationListDer, CertificateSigningRequestDer, EchConfigListBytes,
PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer,
};

/// The contents of a single recognised block in a PEM file.
Expand Down Expand Up @@ -46,6 +46,15 @@ pub enum Item {
///
/// Appears as "CERTIFICATE REQUEST" in PEM files.
Csr(CertificateSigningRequestDer<'static>),

/// Encrypted client hello (ECH) configs; as specified in
/// [draft-ietf-tls-esni-18 §4][draft-ietf-tls-esni-18].
///
/// PEM encoding specified in [draft-farrell-tls-pemesni.txt].
///
/// [draft-ietf-tls-esni-18]: <https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#section-4>
/// [draft-farrell-tls-pemesni.txt]: <https://github.com/sftcd/pemesni/blob/44bcf7259f204a60421ea05be02a1e2859cadaa9/draft-farrell-tls-pemesni.txt>
EchConfigs(EchConfigListBytes<'static>),
}

/// Errors that may arise when parsing the contents of a PEM file
Expand Down Expand Up @@ -190,17 +199,18 @@ fn read_one_impl(

if let Some((section_type, end_marker)) = section.as_ref() {
if line.starts_with(end_marker) {
let der = base64::ENGINE
let raw = base64::ENGINE
.decode(&b64buf)
.map_err(|err| Error::Base64Decode(format!("{err:?}")))?;

let item = match section_type.as_slice() {
b"CERTIFICATE" => Some(Item::X509Certificate(der.into())),
b"RSA PRIVATE KEY" => Some(Item::Pkcs1Key(der.into())),
b"PRIVATE KEY" => Some(Item::Pkcs8Key(der.into())),
b"EC PRIVATE KEY" => Some(Item::Sec1Key(der.into())),
b"X509 CRL" => Some(Item::Crl(der.into())),
b"CERTIFICATE REQUEST" => Some(Item::Csr(der.into())),
b"CERTIFICATE" => Some(Item::X509Certificate(raw.into())),
b"RSA PRIVATE KEY" => Some(Item::Pkcs1Key(raw.into())),
b"PRIVATE KEY" => Some(Item::Pkcs8Key(raw.into())),
b"EC PRIVATE KEY" => Some(Item::Sec1Key(raw.into())),
b"X509 CRL" => Some(Item::Crl(raw.into())),
b"CERTIFICATE REQUEST" => Some(Item::Csr(raw.into())),
b"ECHCONFIG" => Some(Item::EchConfigs(raw.into())),
_ => {
*section = None;
b64buf.clear();
Expand Down Expand Up @@ -303,6 +313,7 @@ mod base64 {
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
}

use self::base64::Engine;

#[cfg(test)]
Expand Down
7 changes: 7 additions & 0 deletions tests/data/server_ech_config.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VuBCIEICjd4yGRdsoP9gU7YT7My8DHx1Tjme8GYDXrOMCi8v1V
-----END PRIVATE KEY-----
-----BEGIN ECHCONFIG-----
AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEA
AQALZXhhbXBsZS5jb20AAA==
-----END ECHCONFIG-----
19 changes: 19 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,22 @@ fn whitespace_prefix() {
assert_eq!(items.len(), 1);
assert!(matches!(items[0], rustls_pemfile::Item::X509Certificate(_)));
}

#[test]
fn server_ech_configs() {
let (_, _) = rustls_pemfile::server_ech_configs(&mut BufReader::new(
&include_bytes!("data/server_ech_config.pem")[..],
))
.unwrap();
}

#[test]
fn ech_configs() {
let items = rustls_pemfile::ech_configs(&mut BufReader::new(
&include_bytes!("data/server_ech_config.pem")[..],
))
.collect::<Result<Vec<_>, _>>()
.unwrap();

assert_eq!(items.len(), 1);
}

0 comments on commit 193dde5

Please # to comment.