mirror of https://github.com/ctz/rustls
Request and validate SCTs
This commit is contained in:
parent
df361f832b
commit
f8dd80600a
|
@ -16,6 +16,7 @@ base64 = "0.6"
|
|||
log = { version = "0.3.6", optional = true }
|
||||
ring = { version = "0.11", features = ["rsa_signing"] }
|
||||
webpki = "0.14"
|
||||
sct = "0.1.2"
|
||||
|
||||
[features]
|
||||
default = ["logging"]
|
||||
|
@ -29,6 +30,7 @@ mio = "0.6"
|
|||
docopt = "0.7"
|
||||
rustc-serialize = "0.3"
|
||||
webpki-roots = "0.11"
|
||||
ct-logs = "0.1"
|
||||
regex = "0.2"
|
||||
|
||||
[[example]]
|
||||
|
|
|
@ -19,6 +19,7 @@ use docopt::Docopt;
|
|||
|
||||
extern crate rustls;
|
||||
extern crate webpki_roots;
|
||||
extern crate ct_logs;
|
||||
|
||||
use rustls::Session;
|
||||
|
||||
|
@ -437,6 +438,7 @@ fn make_config(args: &Args) -> Arc<rustls::ClientConfig> {
|
|||
.unwrap();
|
||||
} else {
|
||||
config.root_store.add_trust_anchors(&webpki_roots::ROOTS);
|
||||
config.ct_logs = Some(&ct_logs::LOGS);
|
||||
}
|
||||
|
||||
if args.flag_no_tickets {
|
||||
|
|
|
@ -3,6 +3,7 @@ use msgs::enums::{AlertDescription, HandshakeType, ExtensionType};
|
|||
use session::{Session, SessionSecrets, SessionRandoms, SessionCommon};
|
||||
use suites::{SupportedCipherSuite, ALL_CIPHERSUITES};
|
||||
use msgs::handshake::{CertificatePayload, DigitallySignedStruct, SessionID};
|
||||
use msgs::handshake::SCTList;
|
||||
use msgs::enums::SignatureScheme;
|
||||
use msgs::enums::{ContentType, ProtocolVersion};
|
||||
use msgs::message::Message;
|
||||
|
@ -21,6 +22,8 @@ use std::sync::{Arc, Mutex};
|
|||
use std::io;
|
||||
use std::fmt;
|
||||
|
||||
use sct;
|
||||
|
||||
/// A trait for the ability to store client session data.
|
||||
/// The keys and values are opaque.
|
||||
///
|
||||
|
@ -199,6 +202,11 @@ pub struct ClientConfig {
|
|||
/// is all supported versions.
|
||||
pub versions: Vec<ProtocolVersion>,
|
||||
|
||||
/// Collection of certificate transparency logs.
|
||||
/// If this collection is empty, then certificate transparency
|
||||
/// checking is disabled.
|
||||
pub ct_logs: Option<&'static [&'static sct::Log<'static>]>,
|
||||
|
||||
/// How to verify the server certificate chain.
|
||||
verifier: Arc<verify::ServerCertVerifier>,
|
||||
}
|
||||
|
@ -217,6 +225,7 @@ impl ClientConfig {
|
|||
client_auth_cert_resolver: Arc::new(FailResolveClientCert {}),
|
||||
enable_tickets: true,
|
||||
versions: vec![ProtocolVersion::TLSv1_3, ProtocolVersion::TLSv1_2],
|
||||
ct_logs: None,
|
||||
verifier: Arc::new(verify::WebPKIVerifier {})
|
||||
}
|
||||
}
|
||||
|
@ -303,6 +312,7 @@ pub mod danger {
|
|||
pub struct ClientHandshakeData {
|
||||
pub server_cert_chain: CertificatePayload,
|
||||
pub server_cert_ocsp_response: Vec<u8>,
|
||||
pub server_cert_scts: Option<SCTList>,
|
||||
pub dns_name: String,
|
||||
pub session_id: SessionID,
|
||||
pub sent_extensions: Vec<ExtensionType>,
|
||||
|
@ -328,6 +338,7 @@ impl ClientHandshakeData {
|
|||
ClientHandshakeData {
|
||||
server_cert_chain: Vec::new(),
|
||||
server_cert_ocsp_response: Vec::new(),
|
||||
server_cert_scts: None,
|
||||
dns_name: host_name.to_string(),
|
||||
session_id: SessionID::empty(),
|
||||
sent_extensions: Vec::new(),
|
||||
|
|
|
@ -582,6 +582,12 @@ fn handle_server_hello(sess: &mut ClientSessionImpl, m: Message) -> StateResult
|
|||
sess.handshake_data.may_send_cert_status = true;
|
||||
}
|
||||
|
||||
// Save any sent SCTs for verification against the certificate.
|
||||
if let Some(sct_list) = server_hello.get_sct_list() {
|
||||
info!("Server sent {:?} SCTs", sct_list.len());
|
||||
sess.handshake_data.server_cert_scts = Some(sct_list.clone());
|
||||
}
|
||||
|
||||
// See if we're successfully resuming.
|
||||
let mut abbreviated_handshake = false;
|
||||
if let Some(ref resuming) = sess.handshake_data.resuming_session {
|
||||
|
@ -771,6 +777,8 @@ fn handle_certificate_tls13(sess: &mut ClientSessionImpl, m: Message) -> StateRe
|
|||
return Err(TLSError::PeerMisbehavedError("bad cert chain extensions".to_string()));
|
||||
}
|
||||
|
||||
sess.handshake_data.server_cert_ocsp_response = cert_chain.get_end_entity_ocsp();
|
||||
sess.handshake_data.server_cert_scts = cert_chain.get_end_entity_scts();
|
||||
sess.handshake_data.server_cert_chain = cert_chain.convert();
|
||||
Ok(&EXPECT_TLS13_CERTIFICATE_VERIFY)
|
||||
}
|
||||
|
@ -904,6 +912,16 @@ fn handle_certificate_verify(sess: &mut ClientSessionImpl,
|
|||
&handshake_hash,
|
||||
b"TLS 1.3, server CertificateVerify\x00")?;
|
||||
|
||||
// 3. Verify any included SCTs.
|
||||
match (sess.handshake_data.server_cert_scts.as_ref(), sess.config.ct_logs) {
|
||||
(Some(ref scts), Some(logs)) => {
|
||||
verify::verify_scts(&sess.handshake_data.server_cert_chain[0],
|
||||
&scts,
|
||||
&logs)?;
|
||||
}
|
||||
(_, _) => {}
|
||||
}
|
||||
|
||||
sess.handshake_data.transcript.add_message(&m);
|
||||
|
||||
Ok(&EXPECT_TLS13_FINISHED)
|
||||
|
@ -1122,15 +1140,16 @@ fn handle_server_hello_done(sess: &mut ClientSessionImpl,
|
|||
info!("Server DNS name is {:?}", sess.handshake_data.dns_name);
|
||||
|
||||
// 1. Verify the cert chain.
|
||||
// 2. Verify that the top certificate signed their kx.
|
||||
// 3. If doing client auth, send our Certificate.
|
||||
// 4. Complete the key exchange:
|
||||
// 2. Verify any SCTs provided with the certificate.
|
||||
// 3. Verify that the top certificate signed their kx.
|
||||
// 4. If doing client auth, send our Certificate.
|
||||
// 5. Complete the key exchange:
|
||||
// a) generate our kx pair
|
||||
// b) emit a ClientKeyExchange containing it
|
||||
// c) if doing client auth, emit a CertificateVerify
|
||||
// d) emit a CCS
|
||||
// e) derive the shared keys, and start encryption
|
||||
// 5. emit a Finished, our first encrypted message under the new keys.
|
||||
// 6. emit a Finished, our first encrypted message under the new keys.
|
||||
|
||||
// 1.
|
||||
if sess.handshake_data.server_cert_chain.is_empty() {
|
||||
|
@ -1142,7 +1161,17 @@ fn handle_server_hello_done(sess: &mut ClientSessionImpl,
|
|||
&sess.handshake_data.dns_name,
|
||||
&sess.handshake_data.server_cert_ocsp_response)?;
|
||||
|
||||
// 2.
|
||||
// 2. Verify any included SCTs.
|
||||
match (sess.handshake_data.server_cert_scts.as_ref(), sess.config.ct_logs) {
|
||||
(Some(ref scts), Some(logs)) => {
|
||||
verify::verify_scts(&sess.handshake_data.server_cert_chain[0],
|
||||
&scts,
|
||||
&logs)?;
|
||||
}
|
||||
(_, _) => {}
|
||||
}
|
||||
|
||||
// 3.
|
||||
// Build up the contents of the signed message.
|
||||
// It's ClientHello.random || ServerHello.random || ServerKeyExchange.params
|
||||
{
|
||||
|
@ -1166,30 +1195,30 @@ fn handle_server_hello_done(sess: &mut ClientSessionImpl,
|
|||
sig)?;
|
||||
}
|
||||
|
||||
// 3.
|
||||
// 4.
|
||||
if sess.handshake_data.doing_client_auth {
|
||||
emit_certificate(sess);
|
||||
}
|
||||
|
||||
// 4a.
|
||||
// 5a.
|
||||
let kxd = sess.common.get_suite()
|
||||
.do_client_kx(&sess.handshake_data.server_kx_params)
|
||||
.ok_or_else(|| TLSError::PeerMisbehavedError("key exchange failed".to_string()))?;
|
||||
|
||||
// 4b.
|
||||
// 5b.
|
||||
emit_clientkx(sess, &kxd);
|
||||
// nb. EMS handshake hash only runs up to ClientKeyExchange.
|
||||
let handshake_hash = sess.handshake_data.transcript.get_current_hash();
|
||||
|
||||
// 4c.
|
||||
// 5c.
|
||||
if sess.handshake_data.doing_client_auth {
|
||||
emit_certverify(sess)?;
|
||||
}
|
||||
|
||||
// 4d.
|
||||
// 5d.
|
||||
emit_ccs(sess);
|
||||
|
||||
// 4e. Now commit secrets.
|
||||
// 5e. Now commit secrets.
|
||||
let hashalg = sess.common.get_suite().get_hash();
|
||||
if sess.handshake_data.using_ems {
|
||||
sess.secrets = Some(SessionSecrets::new_ems(&sess.handshake_data.randoms,
|
||||
|
@ -1203,7 +1232,7 @@ fn handle_server_hello_done(sess: &mut ClientSessionImpl,
|
|||
}
|
||||
sess.start_encryption_tls12();
|
||||
|
||||
// 5.
|
||||
// 6.
|
||||
emit_finished(sess);
|
||||
|
||||
if sess.handshake_data.must_issue_new_ticket {
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::fmt;
|
|||
use std::error::Error;
|
||||
use msgs::enums::{ContentType, HandshakeType, AlertDescription};
|
||||
use webpki;
|
||||
use sct;
|
||||
|
||||
/// rustls reports protocol errors using this type.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
|
@ -54,6 +55,9 @@ pub enum TLSError {
|
|||
/// The presented certificate chain is invalid.
|
||||
WebPKIError(webpki::Error),
|
||||
|
||||
/// The presented SCT(s) were invalid.
|
||||
InvalidSCT(sct::Error),
|
||||
|
||||
/// A catch-all error for unlikely errors.
|
||||
General(String),
|
||||
}
|
||||
|
@ -112,6 +116,7 @@ impl Error for TLSError {
|
|||
TLSError::PeerMisbehavedError(_) => "peer misbehaved",
|
||||
TLSError::AlertReceived(_) => "received fatal alert",
|
||||
TLSError::WebPKIError(_) => "invalid certificate",
|
||||
TLSError::InvalidSCT(_) => "invalid certificate timestamp",
|
||||
TLSError::General(_) => "unexpected error", // (please file a bug)
|
||||
}
|
||||
}
|
||||
|
@ -125,6 +130,7 @@ mod tests {
|
|||
use std::error::Error;
|
||||
use msgs::enums::{ContentType, HandshakeType, AlertDescription};
|
||||
use webpki;
|
||||
use sct;
|
||||
|
||||
let all = vec![TLSError::InappropriateMessage {
|
||||
expect_types: vec![ContentType::Alert],
|
||||
|
@ -142,6 +148,7 @@ mod tests {
|
|||
TLSError::PeerMisbehavedError("inconsistent something".to_string()),
|
||||
TLSError::AlertReceived(AlertDescription::ExportRestriction),
|
||||
TLSError::WebPKIError(webpki::Error::ExtensionValueInvalid),
|
||||
TLSError::InvalidSCT(sct::Error::MalformedSCT),
|
||||
TLSError::General("undocumented error".to_string())];
|
||||
|
||||
for err in all {
|
||||
|
|
|
@ -197,6 +197,9 @@ extern crate time;
|
|||
// untrusted for feeding ring and webpki.
|
||||
extern crate untrusted;
|
||||
|
||||
// sct for validation of stapled certificate transparency SCTs.
|
||||
extern crate sct;
|
||||
|
||||
// rust-base64 for pemfile module.
|
||||
extern crate base64;
|
||||
|
||||
|
|
|
@ -46,6 +46,9 @@ macro_rules! declare_u16_vec(
|
|||
}
|
||||
);
|
||||
|
||||
declare_u16_vec!(VecU16OfPayloadU8, PayloadU8);
|
||||
declare_u16_vec!(VecU16OfPayloadU16, PayloadU16);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Random([u8; 32]);
|
||||
|
||||
|
@ -360,7 +363,6 @@ impl ConvertServerNameList for ServerNameRequest {
|
|||
}
|
||||
|
||||
pub type ProtocolNameList = VecU16OfPayloadU8;
|
||||
declare_u16_vec!(VecU16OfPayloadU8, PayloadU8);
|
||||
|
||||
pub trait ConvertProtocolNameList {
|
||||
fn from_strings(names: &[String]) -> Self;
|
||||
|
@ -562,6 +564,11 @@ impl CertificateStatusRequest {
|
|||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// SCTs
|
||||
|
||||
pub type SCTList = VecU16OfPayloadU16;
|
||||
|
||||
// ---
|
||||
|
||||
declare_u8_vec!(PSKKeyExchangeModes, PSKKeyExchangeMode);
|
||||
|
@ -834,7 +841,7 @@ pub enum ServerExtension {
|
|||
PresharedKey(u16),
|
||||
ExtendedMasterSecretAck,
|
||||
CertificateStatusAck,
|
||||
SignedCertificateTimestamp(Payload),
|
||||
SignedCertificateTimestamp(SCTList),
|
||||
Unknown(UnknownExtension),
|
||||
}
|
||||
|
||||
|
@ -910,7 +917,8 @@ impl Codec for ServerExtension {
|
|||
}
|
||||
ExtensionType::ExtendedMasterSecret => ServerExtension::ExtendedMasterSecretAck,
|
||||
ExtensionType::SCT => {
|
||||
ServerExtension::SignedCertificateTimestamp(try_ret!(Payload::read(&mut sub)))
|
||||
let scts = try_ret!(SCTList::read(&mut sub));
|
||||
ServerExtension::SignedCertificateTimestamp(scts)
|
||||
}
|
||||
_ => ServerExtension::Unknown(try_ret!(UnknownExtension::read(typ, &mut sub))),
|
||||
})
|
||||
|
@ -928,7 +936,9 @@ impl ServerExtension {
|
|||
}
|
||||
|
||||
pub fn make_sct(sctl: Vec<u8>) -> ServerExtension {
|
||||
ServerExtension::SignedCertificateTimestamp(Payload::new(sctl))
|
||||
let scts = SCTList::read_bytes(&sctl)
|
||||
.expect("invalid SCT list");
|
||||
ServerExtension::SignedCertificateTimestamp(scts)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1333,6 +1343,14 @@ impl ServerHelloPayload {
|
|||
self.find_extension(ExtensionType::ExtendedMasterSecret)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn get_sct_list(&self) -> Option<&SCTList> {
|
||||
let ext = try_ret!(self.find_extension(ExtensionType::SCT));
|
||||
match *ext {
|
||||
ServerExtension::SignedCertificateTimestamp(ref sctl) => Some(sctl),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type CertificatePayload = Vec<key::Certificate>;
|
||||
|
@ -1355,7 +1373,7 @@ impl Codec for CertificatePayload {
|
|||
#[derive(Debug)]
|
||||
pub enum CertificateExtension {
|
||||
CertificateStatus(CertificateStatus),
|
||||
SignedCertificateTimestamp(Payload),
|
||||
SignedCertificateTimestamp(SCTList),
|
||||
Unknown(UnknownExtension),
|
||||
}
|
||||
|
||||
|
@ -1369,7 +1387,23 @@ impl CertificateExtension {
|
|||
}
|
||||
|
||||
pub fn make_sct(sct_list: Vec<u8>) -> CertificateExtension {
|
||||
CertificateExtension::SignedCertificateTimestamp(Payload::new(sct_list))
|
||||
let sctl = SCTList::read_bytes(&sct_list)
|
||||
.expect("invalid SCT list");
|
||||
CertificateExtension::SignedCertificateTimestamp(sctl)
|
||||
}
|
||||
|
||||
pub fn get_cert_status(&self) -> Option<&Vec<u8>> {
|
||||
match self {
|
||||
&CertificateExtension::CertificateStatus(ref cs) => Some(&cs.ocsp_response.0),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_sct_list(&self) -> Option<&SCTList> {
|
||||
match self {
|
||||
&CertificateExtension::SignedCertificateTimestamp(ref sctl) => Some(sctl),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1399,7 +1433,7 @@ impl Codec for CertificateExtension {
|
|||
CertificateExtension::CertificateStatus(st)
|
||||
}
|
||||
ExtensionType::SCT => {
|
||||
let scts = try_ret!(Payload::read(&mut sub));
|
||||
let scts = try_ret!(SCTList::read(&mut sub));
|
||||
CertificateExtension::SignedCertificateTimestamp(scts)
|
||||
}
|
||||
_ => CertificateExtension::Unknown(try_ret!(UnknownExtension::read(typ, &mut sub))),
|
||||
|
@ -1456,9 +1490,24 @@ impl CertificateEntry {
|
|||
self.exts
|
||||
.iter()
|
||||
.any(|ext| {
|
||||
ext.get_type() != ExtensionType::StatusRequest
|
||||
ext.get_type() != ExtensionType::StatusRequest &&
|
||||
ext.get_type() != ExtensionType::SCT
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_ocsp_response(&self) -> Option<&Vec<u8>> {
|
||||
self.exts
|
||||
.iter()
|
||||
.find(|ext| ext.get_type() == ExtensionType::StatusRequest)
|
||||
.and_then(|ext| ext.get_cert_status())
|
||||
}
|
||||
|
||||
pub fn get_scts(&self) -> Option<&SCTList> {
|
||||
self.exts
|
||||
.iter()
|
||||
.find(|ext| ext.get_type() == ExtensionType::SCT)
|
||||
.and_then(|ext| ext.get_sct_list())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -1519,6 +1568,19 @@ impl CertificatePayloadTLS13 {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn get_end_entity_ocsp(&self) -> Vec<u8> {
|
||||
self.list.first()
|
||||
.and_then(|ent| ent.get_ocsp_response())
|
||||
.map(|ocsp| ocsp.clone())
|
||||
.unwrap_or_else(|| Vec::new())
|
||||
}
|
||||
|
||||
pub fn get_end_entity_scts(&self) -> Option<SCTList> {
|
||||
self.list.first()
|
||||
.and_then(|ent| ent.get_scts())
|
||||
.map(|scts| scts.clone())
|
||||
}
|
||||
|
||||
pub fn convert(&self) -> CertificatePayload {
|
||||
let mut ret = Vec::new();
|
||||
for entry in &self.list {
|
||||
|
@ -1781,7 +1843,7 @@ impl HasServerExtensions for EncryptedExtensions {
|
|||
// -- CertificateRequest and sundries --
|
||||
declare_u8_vec!(ClientCertificateTypes, ClientCertificateType);
|
||||
pub type DistinguishedName = PayloadU16;
|
||||
declare_u16_vec!(DistinguishedNames, DistinguishedName);
|
||||
pub type DistinguishedNames = VecU16OfPayloadU16;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CertificateRequestPayload {
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use webpki;
|
||||
use time;
|
||||
use untrusted;
|
||||
use sct;
|
||||
|
||||
use key::Certificate;
|
||||
use msgs::handshake::DigitallySignedStruct;
|
||||
use msgs::handshake::SCTList;
|
||||
use msgs::enums::SignatureScheme;
|
||||
use error::TLSError;
|
||||
use anchors::RootCertStore;
|
||||
|
@ -217,3 +219,38 @@ pub fn verify_tls13(cert: &Certificate,
|
|||
untrusted::Input::from(&dss.sig.0))
|
||||
.map_err(TLSError::WebPKIError)
|
||||
}
|
||||
|
||||
pub fn verify_scts(cert: &Certificate,
|
||||
scts: &SCTList,
|
||||
logs: &[&sct::Log]) -> Result<(), TLSError> {
|
||||
let mut valid_scts = 0;
|
||||
let total_scts = scts.len();
|
||||
let now = (time::get_time().sec * 1000) as u64;
|
||||
let mut last_sct_error = None;
|
||||
|
||||
for sct in scts {
|
||||
match sct::verify_sct(&cert.0, &sct.0, now, logs) {
|
||||
Ok(index) => {
|
||||
info!("Valid SCT signed by {} on {}",
|
||||
logs[index].operated_by, logs[index].description);
|
||||
valid_scts += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
if e.should_be_fatal() {
|
||||
return Err(TLSError::InvalidSCT(e));
|
||||
}
|
||||
info!("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 !logs.is_empty() && total_scts != 0 && valid_scts == 0 {
|
||||
warn!("No valid SCTs provided");
|
||||
return Err(TLSError::InvalidSCT(last_sct_error.unwrap()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue