1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
//! [![crates.io version](https://img.shields.io/crates/v/rustls-pin.svg)](https://crates.io/crates/rustls-pin) //! [![license: Apache 2.0](https://gitlab.com/leonhard-llc/ops/-/raw/main/license-apache-2.0.svg)](https://gitlab.com/leonhard-llc/ops/-/raw/main/rustls-pin/LICENSE) //! [![unsafe forbidden](https://gitlab.com/leonhard-llc/ops/-/raw/main/unsafe-forbidden.svg)](https://github.com/rust-secure-code/safety-dance/) //! [![pipeline status](https://gitlab.com/leonhard-llc/ops/badges/main/pipeline.svg)](https://gitlab.com/leonhard-llc/ops/-/pipelines) //! //! # rustls-pin //! //! Server certificate pinning with `rustls`. //! //! ## Features //! - Make a TLS connection to a server //! - Check that the server is using an allowed certificate //! - `forbid(unsafe_code)` //! - 100% test coverage //! //! ## How to Update Pinned Certificates //! //! Before switching the server to a new certificate, you need to upgrade the //! clients to accept both the current certificate and the new one. //! //! If your users update their client software infrequently, you may need to //! wait a long time before switching to a new certificate. //! //! You can change certificates frequently by having multiple pending 'new' //! certificates. Example: //! - Server: cert1 //! - Client v1: cert1 //! - Client v2: cert1, cert2 //! - Client v3: cert1, cert2, cert3 //! - Server: cert2 //! - Client v4: cert2, cert3, cert4 //! - Server: cert3 //! - Client v5: cert3, cert4, cert5 //! - Server cert4 //! //! ## Example //! ``` //! # let listener = std::net::TcpListener::bind(&("127.0.0.1", 0)).unwrap(); //! # let addr = listener.local_addr().unwrap(); //! # std::thread::spawn(move || listener.accept().unwrap()); //! # let server_cert1 = rustls::Certificate(Vec::new()); //! # let server_cert2 = rustls::Certificate(Vec::new()); //! let mut stream = rustls_pin::connect_pinned( //! addr, //! vec![server_cert1, server_cert2], //! ).unwrap(); //! let mut response = String::new(); //! match std::io::Read::read_to_string( //! &mut stream, &mut response) { //! Ok(_) => {}, //! Err(e) if &e.to_string() == //! "invalid certificate: UnknownIssuer" //! => panic!("Update required."), //! Err(e) => { //! // panic!("{}", e) //! } //! } //! ``` //! //! When the client software reads/writes the stream and gets an //! `invalid certificate: UnknownIssuer` error, //! it can assume that it is outdated. //! It can tell the user to update. //! //! The rustls client terminates the TLS connection by sending the //! 'bad certificate' reason to the server. //! The server's stream read/write fails with: //! `"Custom { kind: InvalidData, error: AlertReceived(BadCertificate) }"`. //! //! ## Alternatives //! - [rustls#227 Implement support for certificate pinning](https://github.com/ctz/rustls/issues/227) //! //! ## Changelog //! - v0.1.2 //! - Add "How to Update Pinned Certificates" to docs. //! - Add error handling to example //! - v0.1.1 - Increase test coverage //! - v0.1.0 - Initial version //! //! ## Happy Contributors 🙂 //! Fixing bugs and adding features is easy and fast. //! Send us a pull request and we intend to: //! - Always respond within 24 hours //! - Provide clear & concrete feedback //! - Immediately make a new release for your accepted change #![forbid(unsafe_code)] use rustls::ClientSession; use std::net::{TcpStream, ToSocketAddrs}; use std::sync::Arc; /// A struct for TLS clients to verify the server's certificate. /// Implements certificate pinning. /// It accepts the server's certificate if it is identical to any of the certificates in the struct. /// /// The rustls library has an open issue to add something like this: /// "Implement support for certificate pinning" <https://github.com/ctz/rustls/issues/227> /// /// # Example /// /// ``` /// use std::net::TcpStream; /// use std::sync::Arc; /// # let listener = std::net::TcpListener::bind(&("127.0.0.1", 0)).unwrap(); /// # let addr = listener.local_addr().unwrap(); /// # std::thread::spawn(move || listener.accept().unwrap()); /// # let server_cert1 = rustls::Certificate(Vec::new()); /// # let server_cert2 = rustls::Certificate(Vec::new()); /// use rustls_pin::{ /// arbitrary_dns_name, /// PinnedServerCertVerifier /// }; /// let mut tcp_stream = /// TcpStream::connect(addr).unwrap(); /// let mut config = rustls::ClientConfig::new(); /// config.dangerous().set_certificate_verifier( /// Arc::new( /// PinnedServerCertVerifier::new(vec![ /// server_cert1, /// server_cert2 /// ]), /// ) /// ); /// let mut session = rustls::ClientSession::new( /// &Arc::new(config), /// arbitrary_dns_name().as_ref() /// ); /// let mut stream = rustls::Stream::new( /// &mut session, &mut tcp_stream); /// ``` pub struct PinnedServerCertVerifier<T> where T: AsRef<[rustls::Certificate]> + Send + Sync, { certs: T, } impl<T> PinnedServerCertVerifier<T> where T: AsRef<[rustls::Certificate]> + Send + Sync, { pub fn new(certs: T) -> Self { Self { certs } } } impl<T> rustls::ServerCertVerifier for PinnedServerCertVerifier<T> where T: AsRef<[rustls::Certificate]> + Send + Sync, { fn verify_server_cert( &self, _roots: &rustls::RootCertStore, presented_certs: &[rustls::Certificate], _dns_name: webpki::DNSNameRef, _ocsp_response: &[u8], ) -> Result<rustls::ServerCertVerified, rustls::TLSError> { // If the server sends several certificates (a certificate chain), we expect // the leaf certificate to be first. let presented_cert = &presented_certs[0]; for cert in self.certs.as_ref() { if presented_cert == cert { return Ok(rustls::ServerCertVerified::assertion()); } } Err(rustls::TLSError::WebPKIError(webpki::Error::UnknownIssuer)) } } /// An arbitrary `DNSName` struct, for passing to [`rustls::ClientSession::new`]. /// `PinnedServerCertVerifier` receives the value and ignores it. #[must_use] pub fn arbitrary_dns_name() -> webpki::DNSName { webpki::DNSNameRef::try_from_ascii_str("arbitrary1") .unwrap() .to_owned() } /// Make a TCP connection to `addr` and set up a TLS session. /// /// The first time you try to write or read the returned stream, /// `rustls` will do TLS negotiation. /// TLS negotiation fails if the server provides a leaf cert /// that is not in `certs`. /// /// Ignores hostnames in certificates. /// /// # Errors /// Returns an error if it fails to open the TCP connection. /// /// # Example /// See example in [`rustls_pin`](index.html) crate docs. pub fn connect_pinned( addr: impl ToSocketAddrs, certs: impl AsRef<[rustls::Certificate]> + Send + Sync + 'static, ) -> Result<rustls::StreamOwned<ClientSession, TcpStream>, std::io::Error> { let tcp_stream = std::net::TcpStream::connect(addr)?; let mut client_config = rustls::ClientConfig::new(); client_config .dangerous() .set_certificate_verifier(Arc::new(PinnedServerCertVerifier::new(certs))); let session = rustls::ClientSession::new(&Arc::new(client_config), arbitrary_dns_name().as_ref()); Ok(rustls::StreamOwned::new(session, tcp_stream)) }