Request and validate SCTs

This commit is contained in:
Joseph Birr-Pixton 2017-07-15 18:27:01 +01:00
parent df361f832b
commit f8dd80600a
8 changed files with 174 additions and 21 deletions

View File

@ -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]]

View File

@ -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 {

View File

@ -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(),

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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 {

View File

@ -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(())
}