Revert "Remove support for SCT stapling"

This reverts commit 4f0a7e0426.
This commit is contained in:
Joseph Birr-Pixton 2023-07-05 15:26:46 +01:00 committed by ctz
parent 0018e7586c
commit 777cc07a4b
25 changed files with 529 additions and 47 deletions

View File

@ -20,9 +20,6 @@ If you'd like to help out, please see [CONTRIBUTING.md](CONTRIBUTING.md).
## Release history
* Next release:
- *Breaking change*: remove support for SCT stapling. Ecosystem support for this is rare compared to
inclusion of SCTs in certificates.
* Release 0.21.2 (2023-06-14)
- Bump MSRV to 1.60 to track similar change in dependencies.
- Differentiate between unexpected and expected EOF in `Stream` and `OwnedStream`.
@ -112,6 +109,8 @@ obsolete cryptography.
* Extended master secret support ([RFC7627](https://tools.ietf.org/html/rfc7627)).
* Exporters ([RFC5705](https://tools.ietf.org/html/rfc5705)).
* OCSP stapling by servers.
* SCT stapling by servers.
* SCT verification by clients.
## Possible future features

View File

@ -40,11 +40,6 @@
"EmptyExtensions-ServerHello-TLS12": "",
"Server-JDK11*": "workarounds for oracle engineering quality",
"Client-RejectJDK11DowngradeRandom": "",
"SendUnsolicitedSCTOnCertificate-TLS13": "SCT stapling not supported",
"SignedCertificateTimestampListEmpty-Client-*": "",
"SignedCertificateTimestampListEmptySCT-Client-*": "",
"SendSCTListOnResume-TLS-TLS12": "",
"IgnoreExtensionsOnIntermediates-TLS13": "assumes SCT support",
"CBCRecordSplitting*": "insane ciphersuites",
"*CBCPadding*": "",
"RSAEphemeralKey": "",
@ -327,6 +322,7 @@
"NegotiatePSKResumption-TLS13": ":PEER_MISBEHAVIOUR:",
"PointFormat-Client-MissingUncompressed": ":PEER_MISBEHAVIOUR:",
"SendUnsolicitedOCSPOnCertificate-TLS13": ":PEER_MISBEHAVIOUR:",
"SendUnsolicitedSCTOnCertificate-TLS13": ":PEER_MISBEHAVIOUR:",
"UnsolicitedServerNameAck-TLS-TLS12": ":PEER_MISBEHAVIOUR:",
"UnsolicitedServerNameAck-TLS-TLS13": ":PEER_MISBEHAVIOUR:",
"TicketSessionIDLength-33-TLS-TLS12": ":BAD_HANDSHAKE_MSG:",
@ -336,6 +332,10 @@
"Ed25519DefaultDisable-NoAccept": ":PEER_MISBEHAVIOUR:",
"SendUnknownExtensionOnCertificate-TLS13": ":PEER_MISBEHAVIOUR:",
"SendDuplicateExtensionsOnCerts-TLS13": ":PEER_MISBEHAVIOUR:",
"SignedCertificateTimestampListEmpty-Client-TLS-TLS12": ":PEER_MISBEHAVIOUR:",
"SignedCertificateTimestampListEmpty-Client-TLS-TLS13": ":PEER_MISBEHAVIOUR:",
"SignedCertificateTimestampListEmptySCT-Client-TLS-TLS12": ":PEER_MISBEHAVIOUR:",
"SignedCertificateTimestampListEmptySCT-Client-TLS-TLS13": ":PEER_MISBEHAVIOUR:",
"EMS-Forbidden-TLS13": ":PEER_MISBEHAVIOUR:",
"Unclean-Shutdown": ":CLOSE_WITHOUT_CLOSE_NOTIFY:",
"SendExtensionOnClientCertificate-TLS13": ":PEER_MISBEHAVIOUR:",

View File

@ -344,6 +344,7 @@ mod danger {
_end_entity: &rustls::Certificate,
_intermediates: &[rustls::Certificate],
_server_name: &rustls::ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp: &[u8],
_now: std::time::SystemTime,
) -> Result<rustls::client::ServerCertVerified, rustls::Error> {

View File

@ -597,7 +597,7 @@ fn make_config(args: &Args) -> Arc<rustls::ServerConfig> {
.with_protocol_versions(&versions)
.expect("inconsistent cipher-suites/versions specified")
.with_client_cert_verifier(client_auth)
.with_single_cert_with_ocsp(certs, privkey, ocsp)
.with_single_cert_with_ocsp_and_sct(certs, privkey, ocsp, vec![])
.expect("bad certificates/private key");
config.key_log = Arc::new(rustls::KeyLogFile::new());

View File

@ -18,6 +18,7 @@ rustversion = { version = "1.0.6", optional = true }
[dependencies]
log = { version = "0.4.4", optional = true }
ring = "0.16.20"
sct = "0.7.0"
webpki = { package = "rustls-webpki", version = "0.100.0", features = ["alloc", "std"] }
[features]

View File

@ -50,6 +50,7 @@ struct Options {
check_close_notify: bool,
host_name: String,
use_sni: bool,
send_sct: bool,
key_file: String,
cert_file: String,
protocols: Vec<String>,
@ -59,6 +60,7 @@ struct Options {
min_version: Option<ProtocolVersion>,
max_version: Option<ProtocolVersion>,
server_ocsp_response: Vec<u8>,
server_sct_list: Vec<u8>,
use_signing_scheme: u16,
curves: Option<Vec<u16>>,
export_keying_material: usize,
@ -89,6 +91,7 @@ impl Options {
resume_with_tickets_disabled: false,
host_name: "example.com".to_string(),
use_sni: false,
send_sct: false,
queue_data: false,
queue_data_on_resume: false,
only_write_one_byte_after_handshake: false,
@ -106,6 +109,7 @@ impl Options {
min_version: None,
max_version: None,
server_ocsp_response: vec![],
server_sct_list: vec![],
use_signing_scheme: 0,
curves: None,
export_keying_material: 0,
@ -211,7 +215,9 @@ impl server::ClientCertVerifier for DummyClientAuth {
}
}
struct DummyServerAuth {}
struct DummyServerAuth {
send_sct: bool,
}
impl client::ServerCertVerifier for DummyServerAuth {
fn verify_server_cert(
@ -219,11 +225,16 @@ impl client::ServerCertVerifier for DummyServerAuth {
_end_entity: &Certificate,
_certs: &[Certificate],
_hostname: &ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp: &[u8],
_now: SystemTime,
) -> Result<client::ServerCertVerified, Error> {
Ok(client::ServerCertVerified::assertion())
}
fn request_scts(&self) -> bool {
self.send_sct
}
}
struct FixedSignatureSchemeSigningKey {
@ -407,7 +418,12 @@ fn make_server_cfg(opts: &Options) -> Arc<ServerConfig> {
.with_protocol_versions(&opts.supported_versions())
.unwrap()
.with_client_cert_verifier(client_auth)
.with_single_cert_with_ocsp(cert.clone(), key, opts.server_ocsp_response.clone())
.with_single_cert_with_ocsp_and_sct(
cert.clone(),
key,
opts.server_ocsp_response.clone(),
opts.server_sct_list.clone(),
)
.unwrap();
cfg.session_storage = ServerCacheWithResumptionDelay::new(opts.resumption_delay);
@ -522,7 +538,9 @@ fn make_client_cfg(opts: &Options) -> Arc<ClientConfig> {
.with_kx_groups(&kx_groups)
.with_protocol_versions(&opts.supported_versions())
.expect("inconsistent settings")
.with_custom_certificate_verifier(Arc::new(DummyServerAuth {}));
.with_custom_certificate_verifier(Arc::new(DummyServerAuth {
send_sct: opts.send_sct,
}));
let mut cfg = if !opts.cert_file.is_empty() && !opts.key_file.is_empty() {
let cert = load_cert(&opts.cert_file);
@ -973,7 +991,6 @@ fn main() {
"-on-resume-expect-no-offer-early-data" |
"-key-update" | //< we could implement an API for this
"-expect-tls13-downgrade" |
"-enable-signed-cert-timestamps" |
"-expect-session-id" => {
println!("not checking {}; NYI", arg);
}
@ -1003,6 +1020,16 @@ fn main() {
opts.server_ocsp_response = BASE64_STANDARD.decode(args.remove(0).as_bytes())
.expect("invalid base64");
}
"-signed-cert-timestamps" => {
opts.server_sct_list = BASE64_STANDARD.decode(args.remove(0).as_bytes())
.expect("invalid base64");
if opts.server_sct_list.len() == 2 &&
opts.server_sct_list[0] == 0x00 &&
opts.server_sct_list[1] == 0x00 {
quit(":INVALID_SCT_LIST:");
}
}
"-select-alpn" => {
opts.protocols.push(args.remove(0));
}
@ -1038,6 +1065,9 @@ fn main() {
"-use-null-client-ca-list" => {
opts.offer_no_client_cas = true;
}
"-enable-signed-cert-timestamps" => {
opts.send_sct = true;
}
"-enable-early-data" => {
opts.tickets = false;
opts.enable_early_data = true;
@ -1170,7 +1200,6 @@ fn main() {
"-wpa-202304" |
"-srtp-profiles" |
"-permute-extensions" |
"-signed-cert-timestamps" |
"-on-initial-expect-peer-cert-file" => {
println!("NYI option {:?}", arg);
process::exit(BOGO_NACK);

View File

@ -4,26 +4,27 @@ use crate::error::Error;
use crate::key_log::NoKeyLog;
use crate::kx::SupportedKxGroup;
use crate::suites::SupportedCipherSuite;
use crate::verify;
use crate::verify::{self, CertificateTransparencyPolicy};
use crate::{anchors, key, versions};
use super::client_conn::Resumption;
use std::marker::PhantomData;
use std::sync::Arc;
use std::time::SystemTime;
impl ConfigBuilder<ClientConfig, WantsVerifier> {
/// Choose how to verify server certificates.
pub fn with_root_certificates(
self,
root_store: anchors::RootCertStore,
) -> ConfigBuilder<ClientConfig, WantsClientCert> {
) -> ConfigBuilder<ClientConfig, WantsTransparencyPolicyOrClientCert> {
ConfigBuilder {
state: WantsClientCert {
state: WantsTransparencyPolicyOrClientCert {
cipher_suites: self.state.cipher_suites,
kx_groups: self.state.kx_groups,
versions: self.state.versions,
verifier: Arc::new(verify::WebPkiVerifier::new(root_store)),
root_store,
},
side: PhantomData,
}
@ -47,6 +48,90 @@ impl ConfigBuilder<ClientConfig, WantsVerifier> {
}
}
/// A config builder state where the caller needs to supply a certificate transparency policy or
/// client certificate resolver.
///
/// In this state, the caller can optionally enable certificate transparency, or ignore CT and
/// invoke one of the methods related to client certificates (as in the [`WantsClientCert`] state).
///
/// For more information, see the [`ConfigBuilder`] documentation.
#[derive(Clone, Debug)]
pub struct WantsTransparencyPolicyOrClientCert {
cipher_suites: Vec<SupportedCipherSuite>,
kx_groups: Vec<&'static SupportedKxGroup>,
versions: versions::EnabledVersions,
root_store: anchors::RootCertStore,
}
impl ConfigBuilder<ClientConfig, WantsTransparencyPolicyOrClientCert> {
/// Set Certificate Transparency logs to use for server certificate validation.
///
/// Because Certificate Transparency logs are sharded on a per-year basis and can be trusted or
/// distrusted relatively quickly, rustls stores a validation deadline. Server certificates will
/// be validated against the configured CT logs until the deadline expires. After the deadline,
/// certificates will no longer be validated, and a warning message will be logged. The deadline
/// may vary depending on how often you deploy builds with updated dependencies.
pub fn with_certificate_transparency_logs(
self,
logs: &'static [&'static sct::Log],
validation_deadline: SystemTime,
) -> ConfigBuilder<ClientConfig, WantsClientCert> {
self.with_logs(Some(CertificateTransparencyPolicy::new(
logs,
validation_deadline,
)))
}
/// Sets a single certificate chain and matching private key for use
/// in client authentication.
///
/// `cert_chain` is a vector of DER-encoded certificates.
/// `key_der` is a DER-encoded RSA, ECDSA, or Ed25519 private key.
///
/// This function fails if `key_der` is invalid.
pub fn with_single_cert(
self,
cert_chain: Vec<key::Certificate>,
key_der: key::PrivateKey,
) -> Result<ClientConfig, Error> {
self.with_logs(None)
.with_single_cert(cert_chain, key_der)
}
/// Do not support client auth.
pub fn with_no_client_auth(self) -> ClientConfig {
self.with_logs(None)
.with_client_cert_resolver(Arc::new(handy::FailResolveClientCert {}))
}
/// Sets a custom [`ResolvesClientCert`].
pub fn with_client_cert_resolver(
self,
client_auth_cert_resolver: Arc<dyn ResolvesClientCert>,
) -> ClientConfig {
self.with_logs(None)
.with_client_cert_resolver(client_auth_cert_resolver)
}
fn with_logs(
self,
ct_policy: Option<CertificateTransparencyPolicy>,
) -> ConfigBuilder<ClientConfig, WantsClientCert> {
ConfigBuilder {
state: WantsClientCert {
cipher_suites: self.state.cipher_suites,
kx_groups: self.state.kx_groups,
versions: self.state.versions,
verifier: Arc::new(verify::WebPkiVerifier::new(
self.state.root_store,
ct_policy,
)),
},
side: PhantomData,
}
}
}
/// A config builder state where the caller needs to supply whether and how to provide a client
/// certificate.
///

View File

@ -2,8 +2,8 @@ use super::ResolvesClientCert;
#[cfg(feature = "logging")]
use crate::log::{debug, trace};
use crate::msgs::enums::ExtensionType;
use crate::msgs::handshake::ServerExtension;
use crate::msgs::handshake::{CertificatePayload, DistinguishedName};
use crate::msgs::handshake::{Sct, ServerExtension};
use crate::{sign, SignatureScheme};
use std::sync::Arc;
@ -12,15 +12,29 @@ use std::sync::Arc;
pub(super) struct ServerCertDetails {
pub(super) cert_chain: CertificatePayload,
pub(super) ocsp_response: Vec<u8>,
pub(super) scts: Option<Vec<Sct>>,
}
impl ServerCertDetails {
pub(super) fn new(cert_chain: CertificatePayload, ocsp_response: Vec<u8>) -> Self {
pub(super) fn new(
cert_chain: CertificatePayload,
ocsp_response: Vec<u8>,
scts: Option<Vec<Sct>>,
) -> Self {
Self {
cert_chain,
ocsp_response,
scts,
}
}
pub(super) fn scts(&self) -> impl Iterator<Item = &[u8]> {
self.scts
.as_deref()
.unwrap_or(&[])
.iter()
.map(|payload| payload.as_ref())
}
}
pub(super) struct ClientHelloDetails {
@ -34,6 +48,11 @@ impl ClientHelloDetails {
}
}
pub(super) fn server_may_send_sct_list(&self) -> bool {
self.sent_extensions
.contains(&ExtensionType::SCT)
}
pub(super) fn server_sent_unsolicited_extensions(
&self,
received_exts: &[ServerExtension],

View File

@ -13,7 +13,7 @@ use crate::msgs::base::Payload;
use crate::msgs::enums::{Compression, ExtensionType};
use crate::msgs::enums::{ECPointFormat, PSKKeyExchangeMode};
use crate::msgs::handshake::ConvertProtocolNameList;
use crate::msgs::handshake::{CertificateStatusRequest, ClientSessionTicket};
use crate::msgs::handshake::{CertificateStatusRequest, ClientSessionTicket, Sct};
use crate::msgs::handshake::{ClientExtension, HasServerExtensions};
use crate::msgs::handshake::{ClientHelloPayload, HandshakeMessagePayload, HandshakePayload};
use crate::msgs::handshake::{HelloRetryRequest, KeyShareEntry};
@ -141,11 +141,13 @@ pub(super) fn start_handshake(
None => SessionId::random()?,
};
let may_send_sct_list = config.verifier.request_scts();
Ok(emit_client_hello_for_retry(
transcript_buffer,
None,
key_share,
extra_exts,
may_send_sct_list,
None,
ClientHelloInput {
config,
@ -192,6 +194,7 @@ fn emit_client_hello_for_retry(
retryreq: Option<&HelloRetryRequest>,
key_share: Option<kx::KeyExchange>,
extra_exts: Vec<ClientExtension>,
may_send_sct_list: bool,
suite: Option<SupportedCipherSuite>,
mut input: ClientHelloInput,
cx: &mut ClientContext<'_>,
@ -235,6 +238,10 @@ fn emit_client_hello_for_retry(
exts.push(ClientExtension::make_sni(sni_name));
}
if may_send_sct_list {
exts.push(ClientExtension::SignedCertificateTimestampRequest);
}
if let Some(key_share) = &key_share {
debug_assert!(support_tls13);
let key_share = KeyShareEntry::new(key_share.group(), key_share.pubkey.as_ref());
@ -472,6 +479,13 @@ pub(super) fn process_alpn_protocol(
Ok(())
}
pub(super) fn sct_list_is_invalid(scts: &[Sct]) -> bool {
scts.is_empty()
|| scts
.iter()
.any(|sct| sct.as_ref().is_empty())
}
impl State<ClientConnectionData> for ExpectServerHello {
fn handle(mut self: Box<Self>, cx: &mut ClientContext<'_>, m: Message) -> NextStateOrError {
let server_hello =
@ -786,6 +800,12 @@ impl ExpectServerHelloOrHelloRetryRequest {
cx.data.early_data.rejected();
}
let may_send_sct_list = self
.next
.input
.hello
.server_may_send_sct_list();
let key_share = match req_group {
Some(group) if group != offered_key_share.group() => {
let group = kx::KeyExchange::choose(group, &config.kx_groups).ok_or_else(|| {
@ -804,6 +824,7 @@ impl ExpectServerHelloOrHelloRetryRequest {
Some(hrr),
Some(key_share),
self.extra_exts,
may_send_sct_list,
Some(cs),
self.next.input,
cx,

View File

@ -12,7 +12,7 @@ use crate::msgs::base::{Payload, PayloadU8};
use crate::msgs::ccs::ChangeCipherSpecPayload;
use crate::msgs::codec::Codec;
use crate::msgs::handshake::{
CertificatePayload, HandshakeMessagePayload, HandshakePayload, NewSessionTicketPayload,
CertificatePayload, HandshakeMessagePayload, HandshakePayload, NewSessionTicketPayload, Sct,
ServerECDHParams, SessionId,
};
use crate::msgs::message::{Message, MessagePayload};
@ -102,6 +102,18 @@ mod server_hello {
debug!("Server may staple OCSP response");
}
// Save any sent SCTs for verification against the certificate.
let server_cert_sct_list = if let Some(sct_list) = server_hello.get_sct_list() {
debug!("Server sent {:?} SCTs", sct_list.len());
if hs::sct_list_is_invalid(sct_list) {
return Err(PeerMisbehaved::InvalidSctList.into());
}
Some(sct_list.to_owned())
} else {
None
};
// See if we're successfully resuming.
if let Some(ref resuming) = self.resuming_session {
if resuming.session_id == server_hello.session_id {
@ -175,6 +187,7 @@ mod server_hello {
suite,
may_send_cert_status,
must_issue_new_ticket,
server_cert_sct_list,
}))
}
}
@ -191,6 +204,7 @@ struct ExpectCertificate {
pub(super) suite: &'static Tls12CipherSuite,
may_send_cert_status: bool,
must_issue_new_ticket: bool,
server_cert_sct_list: Option<Vec<Sct>>,
}
impl State<ClientConnectionData> for ExpectCertificate {
@ -216,11 +230,13 @@ impl State<ClientConnectionData> for ExpectCertificate {
using_ems: self.using_ems,
transcript: self.transcript,
suite: self.suite,
server_cert_sct_list: self.server_cert_sct_list,
server_cert_chain,
must_issue_new_ticket: self.must_issue_new_ticket,
}))
} else {
let server_cert = ServerCertDetails::new(server_cert_chain, vec![]);
let server_cert =
ServerCertDetails::new(server_cert_chain, vec![], self.server_cert_sct_list);
Ok(Box::new(ExpectServerKx {
config: self.config,
@ -247,6 +263,7 @@ struct ExpectCertificateStatusOrServerKx {
using_ems: bool,
transcript: HandshakeHash,
suite: &'static Tls12CipherSuite,
server_cert_sct_list: Option<Vec<Sct>>,
server_cert_chain: CertificatePayload,
must_issue_new_ticket: bool,
}
@ -270,7 +287,11 @@ impl State<ClientConnectionData> for ExpectCertificateStatusOrServerKx {
using_ems: self.using_ems,
transcript: self.transcript,
suite: self.suite,
server_cert: ServerCertDetails::new(self.server_cert_chain, vec![]),
server_cert: ServerCertDetails::new(
self.server_cert_chain,
vec![],
self.server_cert_sct_list,
),
must_issue_new_ticket: self.must_issue_new_ticket,
})
.handle(cx, m),
@ -290,6 +311,7 @@ impl State<ClientConnectionData> for ExpectCertificateStatusOrServerKx {
using_ems: self.using_ems,
transcript: self.transcript,
suite: self.suite,
server_cert_sct_list: self.server_cert_sct_list,
server_cert_chain: self.server_cert_chain,
must_issue_new_ticket: self.must_issue_new_ticket,
})
@ -315,6 +337,7 @@ struct ExpectCertificateStatus {
using_ems: bool,
transcript: HandshakeHash,
suite: &'static Tls12CipherSuite,
server_cert_sct_list: Option<Vec<Sct>>,
server_cert_chain: CertificatePayload,
must_issue_new_ticket: bool,
}
@ -338,7 +361,11 @@ impl State<ClientConnectionData> for ExpectCertificateStatus {
&server_cert_ocsp_response
);
let server_cert = ServerCertDetails::new(self.server_cert_chain, server_cert_ocsp_response);
let server_cert = ServerCertDetails::new(
self.server_cert_chain,
server_cert_ocsp_response,
self.server_cert_sct_list,
);
Ok(Box::new(ExpectServerKx {
config: self.config,
@ -714,6 +741,7 @@ impl State<ClientConnectionData> for ExpectServerDone {
end_entity,
intermediates,
&st.server_name,
&mut st.server_cert.scts(),
&st.server_cert.ocsp_response,
now,
)

View File

@ -448,6 +448,7 @@ impl State<ClientConnectionData> for ExpectEncryptedExtensions {
suite: self.suite,
transcript: self.transcript,
key_schedule: self.key_schedule,
may_send_sct_list: self.hello.server_may_send_sct_list(),
}))
}
}
@ -460,6 +461,7 @@ struct ExpectCertificateOrCertReq {
suite: &'static Tls13CipherSuite,
transcript: HandshakeHash,
key_schedule: KeyScheduleHandshake,
may_send_sct_list: bool,
}
impl State<ClientConnectionData> for ExpectCertificateOrCertReq {
@ -479,6 +481,7 @@ impl State<ClientConnectionData> for ExpectCertificateOrCertReq {
suite: self.suite,
transcript: self.transcript,
key_schedule: self.key_schedule,
may_send_sct_list: self.may_send_sct_list,
client_auth: None,
})
.handle(cx, m),
@ -496,6 +499,7 @@ impl State<ClientConnectionData> for ExpectCertificateOrCertReq {
suite: self.suite,
transcript: self.transcript,
key_schedule: self.key_schedule,
may_send_sct_list: self.may_send_sct_list,
})
.handle(cx, m),
payload => Err(inappropriate_handshake_message(
@ -520,6 +524,7 @@ struct ExpectCertificateRequest {
suite: &'static Tls13CipherSuite,
transcript: HandshakeHash,
key_schedule: KeyScheduleHandshake,
may_send_sct_list: bool,
}
impl State<ClientConnectionData> for ExpectCertificateRequest {
@ -577,6 +582,7 @@ impl State<ClientConnectionData> for ExpectCertificateRequest {
suite: self.suite,
transcript: self.transcript,
key_schedule: self.key_schedule,
may_send_sct_list: self.may_send_sct_list,
client_auth: Some(client_auth),
}))
}
@ -589,6 +595,7 @@ struct ExpectCertificate {
suite: &'static Tls13CipherSuite,
transcript: HandshakeHash,
key_schedule: KeyScheduleHandshake,
may_send_sct_list: bool,
client_auth: Option<ClientAuthDetails>,
}
@ -618,8 +625,23 @@ impl State<ClientConnectionData> for ExpectCertificate {
));
}
let server_cert =
ServerCertDetails::new(cert_chain.convert(), cert_chain.get_end_entity_ocsp());
let server_cert = ServerCertDetails::new(
cert_chain.convert(),
cert_chain.get_end_entity_ocsp(),
cert_chain
.get_end_entity_scts()
.map(|scts| scts.to_vec()),
);
if let Some(sct_list) = server_cert.scts.as_ref() {
if hs::sct_list_is_invalid(sct_list) {
return Err(PeerMisbehaved::InvalidSctList.into());
}
if !self.may_send_sct_list {
return Err(PeerMisbehaved::UnsolicitedSctList.into());
}
}
Ok(Box::new(ExpectCertificateVerify {
config: self.config,
@ -670,6 +692,7 @@ impl State<ClientConnectionData> for ExpectCertificateVerify {
end_entity,
intermediates,
&self.server_name,
&mut self.server_cert.scts(),
&self.server_cert.ocsp_response,
now,
)

View File

@ -66,6 +66,9 @@ pub enum Error {
/// implementation.
InvalidCertificate(CertificateError),
/// The presented SCT(s) were invalid.
InvalidSct(sct::Error),
/// A catch-all error for unlikely errors.
General(String),
@ -182,6 +185,7 @@ pub enum PeerMisbehaved {
IncorrectBinder,
InvalidMaxEarlyDataSize,
InvalidKeyShare,
InvalidSctList,
KeyEpochWithPendingFragment,
KeyUpdateReceivedInQuicConnection,
MessageInterleavedWithHandshakeMessage,
@ -422,6 +426,7 @@ impl fmt::Display for Error {
Self::PeerSentOversizedRecord => write!(f, "peer sent excess record size"),
Self::HandshakeNotComplete => write!(f, "handshake not complete"),
Self::NoApplicationProtocol => write!(f, "peer doesn't support any known protocol"),
Self::InvalidSct(ref err) => write!(f, "invalid certificate timestamp: {:?}", err),
Self::FailedToGetCurrentTime => write!(f, "failed to get current time"),
Self::FailedToGetRandomBytes => write!(f, "failed to get random bytes"),
Self::BadMaxFragmentSize => {
@ -475,6 +480,7 @@ mod tests {
#[test]
fn smoke() {
use crate::enums::{AlertDescription, ContentType, HandshakeType};
use sct;
let all = vec![
Error::InappropriateMessage {
@ -492,6 +498,7 @@ mod tests {
super::PeerMisbehaved::UnsolicitedCertExtension.into(),
Error::AlertReceived(AlertDescription::ExportRestriction),
super::CertificateError::Expired.into(),
Error::InvalidSct(sct::Error::MalformedSct),
Error::General("undocumented error".to_string()),
Error::FailedToGetCurrentTime,
Error::FailedToGetRandomBytes,

View File

@ -25,6 +25,8 @@
//! * Extended master secret support ([RFC7627](https://tools.ietf.org/html/rfc7627)).
//! * Exporters ([RFC5705](https://tools.ietf.org/html/rfc5705)).
//! * OCSP stapling by servers.
//! * SCT stapling by servers.
//! * SCT verification by clients.
//!
//! ## Possible future features
//!
@ -415,7 +417,7 @@ pub mod client {
mod tls13;
pub use crate::dns_name::InvalidDnsNameError;
pub use builder::WantsClientCert;
pub use builder::{WantsClientCert, WantsTransparencyPolicyOrClientCert};
pub use client_conn::{
ClientConfig, ClientConnection, ClientConnectionData, ClientSessionStore,
ResolvesClientCert, Resumption, ServerName, Tls12Resumption, WriteEarlyData,
@ -424,8 +426,9 @@ pub mod client {
#[cfg(feature = "dangerous_configuration")]
pub use crate::verify::{
verify_server_cert_signed_by_trust_anchor, verify_server_name, HandshakeSignatureValid,
ServerCertVerified, ServerCertVerifier, WebPkiVerifier,
verify_server_cert_signed_by_trust_anchor, verify_server_name,
CertificateTransparencyPolicy, HandshakeSignatureValid, ServerCertVerified,
ServerCertVerifier, WebPkiVerifier,
};
#[cfg(feature = "dangerous_configuration")]
pub use client_conn::danger::DangerousClientConfig;

View File

@ -516,6 +516,15 @@ impl CertificateStatusRequest {
}
}
// ---
// SCTs
wrapped_payload!(Sct, PayloadU16,);
impl TlsListElement for Sct {
const SIZE_LEN: ListLength = ListLength::U16;
}
// ---
impl TlsListElement for PSKKeyExchangeMode {
@ -545,6 +554,7 @@ pub enum ClientExtension {
Cookie(PayloadU16),
ExtendedMasterSecretRequest,
CertificateStatusRequest(CertificateStatusRequest),
SignedCertificateTimestampRequest,
TransportParameters(Vec<u8>),
TransportParametersDraft(Vec<u8>),
EarlyData,
@ -567,6 +577,7 @@ impl ClientExtension {
Self::Cookie(_) => ExtensionType::Cookie,
Self::ExtendedMasterSecretRequest => ExtensionType::ExtendedMasterSecret,
Self::CertificateStatusRequest(_) => ExtensionType::StatusRequest,
Self::SignedCertificateTimestampRequest => ExtensionType::SCT,
Self::TransportParameters(_) => ExtensionType::TransportParameters,
Self::TransportParametersDraft(_) => ExtensionType::TransportParametersDraft,
Self::EarlyData => ExtensionType::EarlyData,
@ -587,6 +598,7 @@ impl Codec for ClientExtension {
Self::ServerName(ref r) => r.encode(&mut sub),
Self::SessionTicket(ClientSessionTicket::Request)
| Self::ExtendedMasterSecretRequest
| Self::SignedCertificateTimestampRequest
| Self::EarlyData => {}
Self::SessionTicket(ClientSessionTicket::Offer(ref r)) => r.encode(&mut sub),
Self::Protocols(ref r) => r.encode(&mut sub),
@ -637,6 +649,7 @@ impl Codec for ClientExtension {
let csr = CertificateStatusRequest::read(&mut sub)?;
Self::CertificateStatusRequest(csr)
}
ExtensionType::SCT if !sub.any_left() => Self::SignedCertificateTimestampRequest,
ExtensionType::TransportParameters => Self::TransportParameters(sub.rest().to_vec()),
ExtensionType::TransportParametersDraft => {
Self::TransportParametersDraft(sub.rest().to_vec())
@ -694,6 +707,7 @@ pub enum ServerExtension {
PresharedKey(u16),
ExtendedMasterSecretAck,
CertificateStatusAck,
SignedCertificateTimestamp(Vec<Sct>),
SupportedVersions(ProtocolVersion),
TransportParameters(Vec<u8>),
TransportParametersDraft(Vec<u8>),
@ -713,6 +727,7 @@ impl ServerExtension {
Self::PresharedKey(_) => ExtensionType::PreSharedKey,
Self::ExtendedMasterSecretAck => ExtensionType::ExtendedMasterSecret,
Self::CertificateStatusAck => ExtensionType::StatusRequest,
Self::SignedCertificateTimestamp(_) => ExtensionType::SCT,
Self::SupportedVersions(_) => ExtensionType::SupportedVersions,
Self::TransportParameters(_) => ExtensionType::TransportParameters,
Self::TransportParametersDraft(_) => ExtensionType::TransportParametersDraft,
@ -738,6 +753,7 @@ impl Codec for ServerExtension {
Self::Protocols(ref r) => r.encode(&mut sub),
Self::KeyShare(ref r) => r.encode(&mut sub),
Self::PresharedKey(r) => r.encode(&mut sub),
Self::SignedCertificateTimestamp(ref r) => r.encode(&mut sub),
Self::SupportedVersions(ref r) => r.encode(&mut sub),
Self::TransportParameters(ref r) | Self::TransportParametersDraft(ref r) => {
sub.extend_from_slice(r);
@ -764,6 +780,7 @@ impl Codec for ServerExtension {
ExtensionType::KeyShare => Self::KeyShare(KeyShareEntry::read(&mut sub)?),
ExtensionType::PreSharedKey => Self::PresharedKey(u16::read(&mut sub)?),
ExtensionType::ExtendedMasterSecret => Self::ExtendedMasterSecretAck,
ExtensionType::SCT => Self::SignedCertificateTimestamp(Vec::read(&mut sub)?),
ExtensionType::SupportedVersions => {
Self::SupportedVersions(ProtocolVersion::read(&mut sub)?)
}
@ -789,6 +806,11 @@ impl ServerExtension {
let empty = Vec::new();
Self::RenegotiationInfo(PayloadU8::new(empty))
}
pub fn make_sct(sctl: Vec<u8>) -> Self {
let scts = Vec::read_bytes(&sctl).expect("invalid SCT list");
Self::SignedCertificateTimestamp(scts)
}
}
#[derive(Debug)]
@ -1242,6 +1264,14 @@ impl ServerHelloPayload {
.is_some()
}
pub fn get_sct_list(&self) -> Option<&[Sct]> {
let ext = self.find_extension(ExtensionType::SCT)?;
match *ext {
ServerExtension::SignedCertificateTimestamp(ref sctl) => Some(sctl),
_ => None,
}
}
pub fn get_supported_versions(&self) -> Option<ProtocolVersion> {
let ext = self.find_extension(ExtensionType::SupportedVersions)?;
match *ext {
@ -1264,6 +1294,7 @@ impl TlsListElement for key::Certificate {
#[derive(Debug)]
pub enum CertificateExtension {
CertificateStatus(CertificateStatus),
SignedCertificateTimestamp(Vec<Sct>),
Unknown(UnknownExtension),
}
@ -1271,16 +1302,29 @@ impl CertificateExtension {
pub fn get_type(&self) -> ExtensionType {
match *self {
Self::CertificateStatus(_) => ExtensionType::StatusRequest,
Self::SignedCertificateTimestamp(_) => ExtensionType::SCT,
Self::Unknown(ref r) => r.typ,
}
}
pub fn make_sct(sct_list: Vec<u8>) -> Self {
let sctl = Vec::read_bytes(&sct_list).expect("invalid SCT list");
Self::SignedCertificateTimestamp(sctl)
}
pub fn get_cert_status(&self) -> Option<&Vec<u8>> {
match *self {
Self::CertificateStatus(ref cs) => Some(&cs.ocsp_response.0),
_ => None,
}
}
pub fn get_sct_list(&self) -> Option<&[Sct]> {
match *self {
Self::SignedCertificateTimestamp(ref sctl) => Some(sctl),
_ => None,
}
}
}
impl Codec for CertificateExtension {
@ -1290,6 +1334,7 @@ impl Codec for CertificateExtension {
let mut sub: Vec<u8> = Vec::new();
match *self {
Self::CertificateStatus(ref r) => r.encode(&mut sub),
Self::SignedCertificateTimestamp(ref r) => r.encode(&mut sub),
Self::Unknown(ref r) => r.encode(&mut sub),
}
@ -1307,6 +1352,7 @@ impl Codec for CertificateExtension {
let st = CertificateStatus::read(&mut sub)?;
Self::CertificateStatus(st)
}
ExtensionType::SCT => Self::SignedCertificateTimestamp(Vec::read(&mut sub)?),
_ => Self::Unknown(UnknownExtension::read(typ, &mut sub)),
};
@ -1363,9 +1409,9 @@ impl CertificateEntry {
}
pub fn has_unknown_extension(&self) -> bool {
self.exts
.iter()
.any(|ext| ext.get_type() != ExtensionType::StatusRequest)
self.exts.iter().any(|ext| {
ext.get_type() != ExtensionType::StatusRequest && ext.get_type() != ExtensionType::SCT
})
}
pub fn get_ocsp_response(&self) -> Option<&Vec<u8>> {
@ -1374,6 +1420,13 @@ impl CertificateEntry {
.find(|ext| ext.get_type() == ExtensionType::StatusRequest)
.and_then(CertificateExtension::get_cert_status)
}
pub fn get_scts(&self) -> Option<&[Sct]> {
self.exts
.iter()
.find(|ext| ext.get_type() == ExtensionType::SCT)
.and_then(CertificateExtension::get_sct_list)
}
}
impl TlsListElement for CertificateEntry {
@ -1446,6 +1499,12 @@ impl CertificatePayloadTLS13 {
.unwrap_or_default()
}
pub fn get_end_entity_scts(&self) -> Option<&[Sct]> {
self.entries
.first()
.and_then(CertificateEntry::get_scts)
}
pub fn convert(&self) -> CertificatePayload {
let mut ret = Vec::new();
for entry in &self.entries {

View File

@ -15,7 +15,7 @@ use crate::msgs::handshake::{
ECParameters, HandshakeMessagePayload, HandshakePayload, HasServerExtensions,
HelloRetryExtension, HelloRetryRequest, KeyShareEntry, NewSessionTicketExtension,
NewSessionTicketPayload, NewSessionTicketPayloadTLS13, PresharedKeyBinder,
PresharedKeyIdentity, PresharedKeyOffer, ProtocolName, Random, ServerECDHParams,
PresharedKeyIdentity, PresharedKeyOffer, ProtocolName, Random, Sct, ServerECDHParams,
ServerExtension, ServerHelloPayload, ServerKeyExchangePayload, SessionId, UnknownExtension,
};
use crate::verify::DigitallySignedStruct;
@ -131,7 +131,7 @@ fn refuses_server_ext_with_unparsed_bytes() {
#[test]
fn refuses_certificate_ext_with_unparsed_bytes() {
let bytes = [0x00u8, 0x05, 0x00, 0x03, 0x00, 0x00, 0x01];
let bytes = [0x00u8, 0x12, 0x00, 0x03, 0x00, 0x00, 0x01];
let mut rd = Reader::init(&bytes);
assert!(CertificateExtension::read(&mut rd).is_err());
}
@ -385,6 +385,7 @@ fn get_sample_clienthellopayload() -> ClientHelloPayload {
ClientExtension::Cookie(PayloadU16(vec![1, 2, 3])),
ClientExtension::ExtendedMasterSecretRequest,
ClientExtension::CertificateStatusRequest(CertificateStatusRequest::build_ocsp()),
ClientExtension::SignedCertificateTimestampRequest,
ClientExtension::TransportParameters(vec![1, 2, 3]),
ClientExtension::Unknown(UnknownExtension {
typ: ExtensionType::Unknown(12345),
@ -703,6 +704,11 @@ fn server_get_ecpoints_extension() {
});
}
#[test]
fn server_get_sct_list() {
test_server_extension_getter(ExtensionType::SCT, |shp| shp.get_sct_list().is_some());
}
#[test]
fn server_get_supported_versions() {
test_server_extension_getter(ExtensionType::SupportedVersions, |shp| {
@ -736,6 +742,11 @@ fn certentry_get_ocsp_response() {
});
}
#[test]
fn certentry_get_scts() {
test_cert_extension_getter(ExtensionType::SCT, |ce| ce.get_scts().is_some());
}
fn get_sample_serverhellopayload() -> ServerHelloPayload {
ServerHelloPayload {
legacy_version: ProtocolVersion::TLSv1_2,
@ -753,6 +764,7 @@ fn get_sample_serverhellopayload() -> ServerHelloPayload {
ServerExtension::PresharedKey(3),
ServerExtension::ExtendedMasterSecretAck,
ServerExtension::CertificateStatusAck,
ServerExtension::SignedCertificateTimestamp(vec![Sct::from(vec![0])]),
ServerExtension::SupportedVersions(ProtocolVersion::TLSv1_2),
ServerExtension::TransportParameters(vec![1, 2, 3]),
ServerExtension::Unknown(UnknownExtension {
@ -799,6 +811,7 @@ fn get_sample_certificatepayloadtls13() -> CertificatePayloadTLS13 {
CertificateExtension::CertificateStatus(CertificateStatus {
ocsp_response: PayloadU24(vec![1, 2, 3]),
}),
CertificateExtension::SignedCertificateTimestamp(vec![Sct::from(vec![0])]),
CertificateExtension::Unknown(UnknownExtension {
typ: ExtensionType::Unknown(12345),
payload: Payload(vec![1, 2, 3]),

View File

@ -77,15 +77,19 @@ impl ConfigBuilder<ServerConfig, WantsServerCert> {
/// `cert_chain` is a vector of DER-encoded certificates.
/// `key_der` is a DER-encoded RSA, ECDSA, or Ed25519 private key.
/// `ocsp` is a DER-encoded OCSP response. Ignored if zero length.
/// `scts` is an `SignedCertificateTimestampList` encoding (see RFC6962)
/// and is ignored if empty.
///
/// This function fails if `key_der` is invalid.
pub fn with_single_cert_with_ocsp(
pub fn with_single_cert_with_ocsp_and_sct(
self,
cert_chain: Vec<key::Certificate>,
key_der: key::PrivateKey,
ocsp: Vec<u8>,
scts: Vec<u8>,
) -> Result<ServerConfig, Error> {
let resolver = handy::AlwaysResolvesChain::new_with_extras(cert_chain, &key_der, ocsp)?;
let resolver =
handy::AlwaysResolvesChain::new_with_extras(cert_chain, &key_der, ocsp, scts)?;
Ok(self.with_cert_resolver(Arc::new(resolver)))
}

View File

@ -5,6 +5,7 @@ use crate::{key, sign};
pub(super) struct ActiveCertifiedKey<'a> {
key: &'a sign::CertifiedKey,
ocsp: Option<&'a [u8]>,
sct_list: Option<&'a [u8]>,
}
impl<'a> ActiveCertifiedKey<'a> {
@ -12,6 +13,7 @@ impl<'a> ActiveCertifiedKey<'a> {
ActiveCertifiedKey {
key,
ocsp: key.ocsp.as_deref(),
sct_list: key.sct_list.as_deref(),
}
}
@ -31,4 +33,9 @@ impl<'a> ActiveCertifiedKey<'a> {
pub(super) fn get_ocsp(&self) -> Option<&[u8]> {
self.ocsp
}
#[inline]
pub(super) fn get_sct_list(&self) -> Option<&[u8]> {
self.sct_list
}
}

View File

@ -112,6 +112,7 @@ impl AlwaysResolvesChain {
chain: Vec<key::Certificate>,
priv_key: &key::PrivateKey,
ocsp: Vec<u8>,
scts: Vec<u8>,
) -> Result<Self, Error> {
let mut r = Self::new(chain, priv_key)?;
@ -120,6 +121,9 @@ impl AlwaysResolvesChain {
if !ocsp.is_empty() {
cert.ocsp = Some(ocsp);
}
if !scts.is_empty() {
cert.sct_list = Some(scts);
}
}
Ok(r)

View File

@ -67,6 +67,7 @@ impl ExtensionProcessing {
config: &ServerConfig,
cx: &mut ServerContext<'_>,
ocsp_response: &mut Option<&[u8]>,
sct_list: &mut Option<&[u8]>,
hello: &ClientHelloPayload,
resumedata: Option<&persist::ServerSessionValue>,
extra_exts: Vec<ServerExtension>,
@ -155,6 +156,24 @@ impl ExtensionProcessing {
ocsp_response.take();
}
if !for_resume
&& hello
.find_extension(ExtensionType::SCT)
.is_some()
{
if !cx.common.is_tls13() {
// Take the SCT list, if any, so we don't send it later,
// and put it in the legacy extension.
if let Some(sct_list) = sct_list.take() {
self.exts
.push(ServerExtension::make_sct(sct_list.to_vec()));
}
}
} else {
// Throw away any SCT list so we don't send it later.
sct_list.take();
}
self.exts.extend(extra_exts);
Ok(())

View File

@ -193,7 +193,8 @@ mod client_hello {
debug_assert_eq!(ecpoint, ECPointFormat::Uncompressed);
let mut ocsp_response = server_key.get_ocsp();
let (mut ocsp_response, mut sct_list) =
(server_key.get_ocsp(), server_key.get_sct_list());
// If we're not offered a ticket or a potential session ID, allocate a session ID.
if !self.config.session_storage.can_cache() {
@ -210,6 +211,7 @@ mod client_hello {
self.suite,
self.using_ems,
&mut ocsp_response,
&mut sct_list,
client_hello,
None,
&self.randoms,
@ -281,6 +283,7 @@ mod client_hello {
self.suite,
self.using_ems,
&mut None,
&mut None,
client_hello,
Some(&resumedata),
&self.randoms,
@ -336,13 +339,22 @@ mod client_hello {
suite: &'static Tls12CipherSuite,
using_ems: bool,
ocsp_response: &mut Option<&[u8]>,
sct_list: &mut Option<&[u8]>,
hello: &ClientHelloPayload,
resumedata: Option<&persist::ServerSessionValue>,
randoms: &ConnectionRandoms,
extra_exts: Vec<ServerExtension>,
) -> Result<bool, Error> {
let mut ep = hs::ExtensionProcessing::new();
ep.process_common(config, cx, ocsp_response, hello, resumedata, extra_exts)?;
ep.process_common(
config,
cx,
ocsp_response,
sct_list,
hello,
resumedata,
extra_exts,
)?;
ep.process_tls12(config, hello, using_ems);
let sh = Message {

View File

@ -372,12 +372,14 @@ mod client_hello {
emit_fake_ccs(cx.common);
}
let mut ocsp_response = server_key.get_ocsp();
let (mut ocsp_response, mut sct_list) =
(server_key.get_ocsp(), server_key.get_sct_list());
let doing_early_data = emit_encrypted_extensions(
&mut self.transcript,
self.suite,
cx,
&mut ocsp_response,
&mut sct_list,
client_hello,
resumedata.as_ref(),
self.extra_exts,
@ -392,6 +394,7 @@ mod client_hello {
cx.common,
server_key.get_cert(),
ocsp_response,
sct_list,
);
emit_certificate_verify_tls13(
&mut self.transcript,
@ -663,13 +666,22 @@ mod client_hello {
suite: &'static Tls13CipherSuite,
cx: &mut ServerContext<'_>,
ocsp_response: &mut Option<&[u8]>,
sct_list: &mut Option<&[u8]>,
hello: &ClientHelloPayload,
resumedata: Option<&persist::ServerSessionValue>,
extra_exts: Vec<ServerExtension>,
config: &ServerConfig,
) -> Result<EarlyDataDecision, Error> {
let mut ep = hs::ExtensionProcessing::new();
ep.process_common(config, cx, ocsp_response, hello, resumedata, extra_exts)?;
ep.process_common(
config,
cx,
ocsp_response,
sct_list,
hello,
resumedata,
extra_exts,
)?;
let early_data = decide_if_early_data_allowed(cx, hello, resumedata, suite, config);
if early_data == EarlyDataDecision::Accepted {
@ -739,6 +751,7 @@ mod client_hello {
common: &mut CommonState,
cert_chain: &[Certificate],
ocsp_response: Option<&[u8]>,
sct_list: Option<&[u8]>,
) {
let mut cert_entries = vec![];
for cert in cert_chain {
@ -759,6 +772,13 @@ mod client_hello {
.exts
.push(CertificateExtension::CertificateStatus(cst));
}
// Likewise, SCT
if let Some(sct_list) = sct_list {
end_entity_cert
.exts
.push(CertificateExtension::make_sct(sct_list.to_owned()));
}
}
let cert_body = CertificatePayloadTLS13::new(cert_entries);

View File

@ -44,6 +44,11 @@ pub struct CertifiedKey {
/// An optional OCSP response from the certificate issuer,
/// attesting to its continued validity.
pub ocsp: Option<Vec<u8>>,
/// An optional collection of SCTs from CT logs, proving the
/// certificate is included on those logs. This must be
/// a `SignedCertificateTimestampList` encoding; see RFC6962.
pub sct_list: Option<Vec<u8>>,
}
impl CertifiedKey {
@ -56,6 +61,7 @@ impl CertifiedKey {
cert,
key,
ocsp: None,
sct_list: None,
}
}

View File

@ -6,7 +6,7 @@ use crate::enums::SignatureScheme;
use crate::error::{CertificateError, Error, InvalidMessage, PeerMisbehaved};
use crate::key::{Certificate, ParsedCertificate};
#[cfg(feature = "logging")]
use crate::log::trace;
use crate::log::{debug, trace, warn};
use crate::msgs::base::PayloadU16;
use crate::msgs::codec::{Codec, Reader};
use crate::msgs::handshake::DistinguishedName;
@ -108,12 +108,16 @@ pub trait ServerCertVerifier: Send + Sync {
/// the implementor to handle invalid data. It is recommended that the implementor returns
/// [`Error::InvalidCertificate(CertificateError::BadEncoding)`] when these cases are encountered.
///
/// `scts` contains the Signed Certificate Timestamps (SCTs) the server
/// sent with the end-entity certificate, if any.
///
/// [Certificate]: https://datatracker.ietf.org/doc/html/rfc8446#section-4.4.2
fn verify_server_cert(
&self,
end_entity: &Certificate,
intermediates: &[Certificate],
server_name: &ServerName,
scts: &mut dyn Iterator<Item = &[u8]>,
ocsp_response: &[u8],
now: SystemTime,
) -> Result<ServerCertVerified, Error>;
@ -181,6 +185,16 @@ pub trait ServerCertVerifier: Send + Sync {
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
WebPkiVerifier::verification_schemes()
}
/// Returns `true` if Rustls should ask the server to send SCTs.
///
/// Signed Certificate Timestamps (SCTs) are used for Certificate
/// Transparency validation.
///
/// The default implementation of this function returns true.
fn request_scts(&self) -> bool {
true
}
}
impl fmt::Debug for dyn ServerCertVerifier {
@ -376,6 +390,7 @@ impl ServerCertVerifier for WebPkiVerifier {
end_entity: &Certificate,
intermediates: &[Certificate],
server_name: &ServerName,
scts: &mut dyn Iterator<Item = &[u8]>,
ocsp_response: &[u8],
now: SystemTime,
) -> Result<ServerCertVerified, Error> {
@ -383,6 +398,10 @@ impl ServerCertVerifier for WebPkiVerifier {
verify_server_cert_signed_by_trust_anchor(&cert, &self.roots, intermediates, now)?;
if let Some(policy) = &self.ct_policy {
policy.verify(end_entity, now, scts)?;
}
if !ocsp_response.is_empty() {
trace!("Unvalidated OCSP response: {:?}", ocsp_response.to_vec());
}
@ -397,6 +416,7 @@ impl ServerCertVerifier for WebPkiVerifier {
#[cfg_attr(docsrs, doc(cfg(feature = "dangerous_configuration")))]
pub struct WebPkiVerifier {
roots: RootCertStore,
ct_policy: Option<CertificateTransparencyPolicy>,
}
#[allow(unreachable_pub)]
@ -404,8 +424,12 @@ impl WebPkiVerifier {
/// Constructs a new `WebPkiVerifier`.
///
/// `roots` is the set of trust anchors to trust for issuing server certs.
pub fn new(roots: RootCertStore) -> Self {
Self { roots }
///
/// `ct_logs` is the list of logs that are trusted for Certificate
/// Transparency. Currently CT log enforcement is opportunistic; see
/// <https://github.com/rustls/rustls/issues/479>.
pub fn new(roots: RootCertStore, ct_policy: Option<CertificateTransparencyPolicy>) -> Self {
Self { roots, ct_policy }
}
/// Returns the signature verification methods supported by
@ -425,6 +449,83 @@ impl WebPkiVerifier {
}
}
/// Policy for enforcing Certificate Transparency.
///
/// Because Certificate Transparency logs are sharded on a per-year basis and can be trusted or
/// distrusted relatively quickly, rustls stores a validation deadline. Server certificates will
/// be validated against the configured CT logs until the deadline expires. After the deadline,
/// certificates will no longer be validated, and a warning message will be logged. The deadline
/// may vary depending on how often you deploy builds with updated dependencies.
#[allow(unreachable_pub)]
#[cfg_attr(docsrs, doc(cfg(feature = "dangerous_configuration")))]
pub struct CertificateTransparencyPolicy {
logs: &'static [&'static sct::Log<'static>],
validation_deadline: SystemTime,
}
impl CertificateTransparencyPolicy {
/// Create a new policy.
#[allow(unreachable_pub)]
pub fn new(
logs: &'static [&'static sct::Log<'static>],
validation_deadline: SystemTime,
) -> Self {
Self {
logs,
validation_deadline,
}
}
fn verify(
&self,
cert: &Certificate,
now: SystemTime,
scts: &mut dyn Iterator<Item = &[u8]>,
) -> Result<(), Error> {
if self.logs.is_empty() {
return Ok(());
} else if self
.validation_deadline
.duration_since(now)
.is_err()
{
warn!("certificate transparency logs have expired, validation disabled");
return Ok(());
}
let now = unix_time_millis(now)?;
let mut last_sct_error = None;
for sct in scts {
#[cfg_attr(not(feature = "logging"), allow(unused_variables))]
match sct::verify_sct(&cert.0, sct, now, self.logs) {
Ok(index) => {
debug!(
"Valid SCT signed by {} on {}",
self.logs[index].operated_by, self.logs[index].description
);
return Ok(());
}
Err(e) => {
if e.should_be_fatal() {
return Err(Error::InvalidSct(e));
}
debug!("SCT ignored because {:?}", e);
last_sct_error = Some(e);
}
}
}
/* If we were supplied with some logs, and some SCTs,
* but couldn't verify any of them, fail the handshake. */
if let Some(last_sct_error) = last_sct_error {
warn!("No valid SCTs provided");
return Err(Error::InvalidSct(last_sct_error));
}
Ok(())
}
}
fn intermediate_chain(intermediates: &[Certificate]) -> Vec<&[u8]> {
intermediates
.iter()
@ -758,6 +859,16 @@ fn verify_tls13(
.map(|_| HandshakeSignatureValid::assertion())
}
fn unix_time_millis(now: SystemTime) -> Result<u64, Error> {
now.duration_since(std::time::UNIX_EPOCH)
.map(|dur| dur.as_secs())
.map_err(|_| Error::FailedToGetCurrentTime)
.and_then(|secs| {
secs.checked_mul(1000)
.ok_or(Error::FailedToGetCurrentTime)
})
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -213,7 +213,8 @@ impl Context {
}
fn bench(&self, count: usize) {
let verifier = verify::WebPkiVerifier::new(self.roots.clone());
let verifier = verify::WebPkiVerifier::new(self.roots.clone(), None);
const SCTS: &[&[u8]] = &[];
const OCSP_RESPONSE: &[u8] = &[];
let mut times = Vec::new();
@ -226,6 +227,7 @@ impl Context {
end_entity,
intermediates,
&server_name,
&mut SCTS.iter().copied(),
OCSP_RESPONSE,
self.now,
)

View File

@ -157,6 +157,7 @@ pub struct MockServerVerifier {
cert_rejection_error: Option<Error>,
tls12_signature_error: Option<Error>,
tls13_signature_error: Option<Error>,
wants_scts: bool,
signature_schemes: Vec<SignatureScheme>,
}
@ -166,12 +167,14 @@ impl ServerCertVerifier for MockServerVerifier {
end_entity: &rustls::Certificate,
intermediates: &[rustls::Certificate],
server_name: &rustls::ServerName,
scts: &mut dyn Iterator<Item = &[u8]>,
oscp_response: &[u8],
now: std::time::SystemTime,
) -> Result<ServerCertVerified, Error> {
let scts: Vec<Vec<u8>> = scts.map(|x| x.to_owned()).collect();
println!(
"verify_server_cert({:?}, {:?}, {:?}, {:?}, {:?})",
end_entity, intermediates, server_name, oscp_response, now
"verify_server_cert({:?}, {:?}, {:?}, {:?}, {:?}, {:?})",
end_entity, intermediates, server_name, scts, oscp_response, now
);
if let Some(error) = &self.cert_rejection_error {
Err(error.clone())
@ -217,6 +220,11 @@ impl ServerCertVerifier for MockServerVerifier {
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
self.signature_schemes.clone()
}
fn request_scts(&self) -> bool {
println!("request_scts? {:?}", self.wants_scts);
self.wants_scts
}
}
impl MockServerVerifier {
@ -262,6 +270,7 @@ impl Default for MockServerVerifier {
cert_rejection_error: None,
tls12_signature_error: None,
tls13_signature_error: None,
wants_scts: false,
signature_schemes: WebPkiVerifier::verification_schemes(),
}
}