paritytech / x509-signature

Low-level X.509 verification
Apache License 2.0
20 stars 9 forks source link

Add example code #4

Open mleonhard opened 4 years ago

mleonhard commented 4 years ago

How about adding some example code?

mleonhard commented 4 years ago

Here's an example of using x509-signature in a TLS client that does certificate pinning.

# Cargo.toml
[package]
edition = "2018"
name = "example"
version = "0.1.0"

[dependencies]
log = "0.4"
pem = "0.8"
rustls = {version = "0.18", features = ["dangerous_configuration"]}
tokio = {version = "0.2", features = ["full"]}
tokio-rustls = "0.14"
webpki = "0.21"
x509-signature = {version = "0.5", features = ["rustls", "webpki"]}
// src/main.rs
// This program shows how to use TLS with certificate pinning.

use log::trace;
use std::println;
use std::sync::Arc;

/// AcceptSpecificCertsVerifier implements certificate pinning.
///
/// It accepts a pre-configured set of certificates.
/// It ignores subject alternate names and all other certificate 
/// properties like expiration date and certificate type.
///
/// It accepts only X.509 v3 certificates because of
/// "QUESTION Can we verify version 1 x509 cert and integrating with `Rustls`"
/// https://github.com/paritytech/x509-signature/issues/2
/// Therefore, it cannot be used to work around:
/// - "Handshake fails with BadDER" https://github.com/ctz/rustls/issues/127
/// - "V1 certificates are rejected with `BadDER` instead of `UnsupportedCertVersion`"
///    https://github.com/briansmith/webpki/issues/53
/// - "Certificates missing X509v3 Extensions field and/or subjectAltName
///    are rejected with unclear BadDER error."
///    https://github.com/briansmith/webpki/issues/90
///
/// The rustls library has an open issue to add something like this:
/// "Implement support for certificate pinning" https://github.com/ctz/rustls/issues/227
struct AcceptSpecificCertsVerifier {
    certs: Vec<rustls::Certificate>,
}

impl rustls::ServerCertVerifier for AcceptSpecificCertsVerifier {
    fn verify_server_cert(
        &self,
        _roots: &rustls::RootCertStore,
        presented_certs: &[rustls::Certificate],
        _dns_name: webpki::DNSNameRef,
        _ocsp_response: &[u8],
    ) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
        let presented_cert = &presented_certs[0];
        for cert in &self.certs {
            if presented_cert == cert {
                return Ok(rustls::ServerCertVerified::assertion());
            }
        }
        return Err(rustls::TLSError::WebPKIError(webpki::Error::UnknownIssuer));
    }

    fn verify_tls12_signature(
        &self,
        message: &[u8],
        cert: &rustls::Certificate,
        dss: &rustls::internal::msgs::handshake::DigitallySignedStruct,
    ) -> Result<rustls::HandshakeSignatureValid, rustls::TLSError> {
        let x509_cert = x509_signature::parse_certificate(&cert.0).map_err(
            |x509_err: x509_signature::Error| {
                trace!("error parsing certificate: {}", x509_err);
                rustls::TLSError::WebPKIError(x509_err)
            },
        )?;
        x509_cert
            .check_tls12_signature(dss.scheme, message, &dss.sig.0)
            .map_err(|x509_err: x509_signature::Error| {
                trace!("error checking signature: {}", x509_err);
                rustls::TLSError::WebPKIError(x509_err)
            })?;
        Ok(rustls::HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &rustls::Certificate,
        dss: &rustls::internal::msgs::handshake::DigitallySignedStruct,
    ) -> Result<rustls::HandshakeSignatureValid, rustls::TLSError> {
        let x509_cert = x509_signature::parse_certificate(&cert.0).map_err(
            |x509_err: x509_signature::Error| {
                trace!("error parsing certificate: {}", x509_err);
                rustls::TLSError::WebPKIError(x509_err)
            },
        )?;
        x509_cert
            .check_tls13_signature(dss.scheme, message, &dss.sig.0)
            .map_err(|x509_err: x509_signature::Error| {
                trace!("error checking signature: {}", x509_err);
                rustls::TLSError::WebPKIError(x509_err)
            })?;
        Ok(rustls::HandshakeSignatureValid::assertion())
    }
}

fn arbitrary_dns_name() -> webpki::DNSName {
    webpki::DNSNameRef::try_from_ascii_str("arbitrary1")
        .unwrap()
        .to_owned()
}

async fn async_main() -> () {
    let cert_pem_bytes = tokio::fs::read("localhost.cert").await.unwrap();
    let cert_pem = pem::parse(cert_pem_bytes).unwrap();
    assert_eq!(cert_pem.tag, "CERTIFICATE");
    let cert = rustls::Certificate(cert_pem.contents);

    let key_pem_bytes = tokio::fs::read("localhost.key").await.unwrap();
    let key_pem = pem::parse(key_pem_bytes).unwrap();
    let key = rustls::PrivateKey(key_pem.contents);
    assert_eq!(key_pem.tag, "PRIVATE KEY");

    let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 1690));
    let mut listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
    println!(
        "INFO server listening on {}",
        listener.local_addr().unwrap()
    );

    let mut server_config = rustls::ServerConfig::new(rustls::NoClientAuth::new());
    server_config
        .set_single_cert(vec![cert.clone()], key)
        .unwrap();
    let server_config_arc = Arc::new(server_config);

    tokio::spawn(async move {
        let tls_acceptor = tokio_rustls::TlsAcceptor::from(server_config_arc);
        loop {
            let (tcp_stream, _addr) = listener.accept().await.unwrap();
            let mut tls_stream = tls_acceptor.accept(tcp_stream).await.unwrap();
            use tokio::io::AsyncWriteExt;
            if let Err(e) = tls_stream.write_all(b"response").await {
                println!("WARN server write error: {:?}", e);
                return;
            }
        }
    });

    let mut client_config = rustls::ClientConfig::new();
    client_config
        .dangerous()
        .set_certificate_verifier(Arc::new(AcceptSpecificCertsVerifier { certs: vec![cert] }));
    let tls_connector = tokio_rustls::TlsConnector::from(Arc::new(client_config));
    let tcp_stream = tokio::net::TcpStream::connect("127.0.0.1:1690")
        .await
        .unwrap();
    let mut tls_stream = tls_connector
        .connect(arbitrary_dns_name().as_ref(), tcp_stream)
        .await
        .unwrap();
    use tokio::io::AsyncReadExt;
    let mut buf = String::new();
    if let Err(e) = tls_stream.read_to_string(&mut buf).await {
        println!("WARN client read error: {:?}", e);
        return;
    }
    println!("INFO client read {:?}", buf);
}

pub fn main() {
    let mut runtime = tokio::runtime::Builder::new()
        .threaded_scheduler()
        .enable_all()
        .build()
        .unwrap();
    runtime.block_on(async_main());
    runtime.shutdown_background();
}
# openssl.cfg
[req]
distinguished_name=dn
x509_extensions=ext
[ dn ]
CN=localhost
[ ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = 127.0.0.1
IP.2 = ::1
$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -out localhost.cert -keyout localhost.key -subj '/CN=localhost' -config openssl.cfg
Generating a 2048 bit RSA private key
.......................................+++
............+++
writing new private key to 'localhost.key'
-----
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/example`
INFO server listening on 127.0.0.1:1690
INFO client read "response"