Using TLS With Rust: Authentication
Learn how to implement the authentication portion of your network protocol in Rust.
Join the DZone community and get the full member experience.
Join For FreeAfter running into a few hurdles, I managed to get rust openssl bindings to work, which means that this is now the time to actually wire things properly in my network protocol. Let’s see how that works, shall we?
First, we have the OpenSSL setup:
struct Server {
tls_config: Arc<openssl::ssl::SslAcceptor>,
tcp_listener: TcpListener,
allowed_certs_thumbprints: HashSet<String>
}
impl Server {
fn new(cert_path: &str, key_path: &str, listen_uri: &str, allowed_certs_thumbprints: &[&str]) -> Result<Server, ConnectionError> {
let mut allowed = HashSet::new();
for thumbprint in allowed_certs_thumbprints {
allowed.insert(thumbprint.to_lowercase());
}
let mut sslb = openssl::ssl::SslAcceptor::mozilla_modern(openssl::ssl::SslMethod::tls())?;
sslb.set_private_key_file(key_path, openssl::ssl::SslFiletype::PEM)?;
sslb.set_certificate_chain_file(cert_path)?;
sslb.check_private_key()?;
// accept all certificates, we'll do our own validation on them
sslb.set_verify_callback(openssl::ssl::SslVerifyMode::PEER, |_, _| true);
let listener = TcpListener::bind(listen_uri)?;
Ok(Server { tls_config: Arc::new(sslb.build()), tcp_listener: listener, allowed_certs_thumbprints: allowed })
}
}
As you can see, this is pretty easy and there isn’t really anything there that is of actual interest. It does feel a whole lot easier than dealing with OpenSSL directly in C, though.
That said, when I started actually dealing with the client certificate, things got a lot more complicated. The first thing that I wanted to do is to do my authentication, which is defined as:
- Client present a client certificate (can be any client certificate).
- If a client doesn’t give a certificate, we accept the connection, send a message (using the encrypted tunnel), and abort.
- If the client provide an certificate, it must be one that was previously registered in the server. That is what allowed_certs_thumbprints is for. If it isn’t, we accept the connection, write an error, and abort.
- If the client certificate has expired or is not yet valid, accept, write error, and abort.
You get the gist. Here is what I had to do to implement the first part:
fn authenticate_certificate(stream: &mut openssl::ssl::SslStream<TcpStream>, server: &Server) -> Result<bool, ConnectionError> {
fn get_friendly_name(peer: &openssl::x509::X509) -> String {
peer.subject_name() // can't figure out how to get the real friendly name
.entries()
.last()
.map( |it| it.data()
.as_utf8()
.and_then(|s| Ok(s.to_string()))
.unwrap_or("".to_string())
)
.unwrap_or("<Unknown>".to_string())
}
match stream.ssl().peer_certificate() {
None => {
stream.write(b"ERR No certificate was provided\r\n")?;
return Ok(false);
}
Some(peer) => {
let thumbprint = hex::encode(peer.digest(openssl::hash::MessageDigest::sha1())?);
if server.allowed_certs_thumbprints.contains(&thumbprint) == false {
let msg = format!("ERR certificate ({}) thumbprint '{}' is unknown\r\n",
get_friendly_name(&peer),
thumbprint);
stream.write(msg.as_bytes())?;
return Ok(false);
}
}
};
return Ok(true);
}
fn handle_connection(socket: TcpStream, server: &Server) -> Result<(), ConnectionError> {
let acceptor = server.tls_config.clone();
let mut stream = acceptor.accept(socket)?;
if authenticate_certificate(&mut stream, server)? == false{
return Ok(());// error already sent to client
}
stream.write(b"OK\r\n")?;
// connection established, do the rest from here
}
Most of the code, actually, is about generating proper and clear error messages more than anything else. I’m not sure how to get the friendly name from the certificate, but this seems to be a good enough stand-in for now.
We validate that we have a certificate or send an error back. We validate that the certificate presented is known to us, or we send an error back.
The next part I wanted to implement was… really far too hard than it should be. I just wanted to verify that the certificate not before/not after dates are valid. And the problem is that the rust bindings for OpenSSL do not expose that information. Luckily, because it is using OpenSSL, I can just call to OpenSSL directly. That led me to some interesting search into how Rust calls out to C, how foreign types work, and a lot of “fun” like that. Given that I’m doing this to learn, I suppose that this is a good thing, though.
Here is what I ended up with (take a deep breath):
fn authenticate_certificate(stream: &mut openssl::ssl::SslStream<TcpStream>, server: &Server) -> Result<bool, ConnectionError> {
fn get_friendly_name(peer: &openssl::x509::X509) -> String {
// like before
}
extern "C" {
fn ASN1_TIME_diff(
pday: *mut std::os::raw::c_int,
psec: *mut std::os::raw::c_int,
from: *const openssl_sys::ASN1_TIME,
to: *const openssl_sys::ASN1_TIME) -> std::os::raw::c_int;
}
fn is_before(x: &openssl::asn1::Asn1TimeRef, y: &openssl::asn1::Asn1TimeRef) -> Result<bool, ConnectionError> {
unsafe {
let mut day : std::os::raw::c_int = 0;
let mut sec : std::os::raw::c_int = 0;
match ASN1_TIME_diff(&mut day, &mut sec, x.as_ptr(), y.as_ptr() ) {
0 => Err(ConnectionError::InvalidTimeFormat),
_ => Ok(day > 0 || sec > 0)
}
}
}
fn is_valid_time(peer: &openssl::x509::X509) -> Result<(), ConnectionError> {
let now = openssl::asn1::Asn1Time::days_from_now(0)?;
if is_before(&now, peer.not_before())? {
return Err(ConnectionError::ClientCertNotYetValid { date: peer.not_before().to_string() });
}
if is_before(peer.not_after(), &now)? {
return Err(ConnectionError::ClientCertExpired { date: peer.not_after().to_string() } );
}
Ok(())
}
match stream.ssl().peer_certificate() {
None => {
stream.write(b"ERR No certificate was provided\r\n")?;
return Ok(false);
}
Some(peer) => {
let thumbprint = hex::encode(peer.digest(openssl::hash::MessageDigest::sha1())?);
if server.allowed_certs_thumbprints.contains(&thumbprint) == false {
let msg = format!("ERR certificate ({}) thumbprint '{}' is unknown\r\n",
get_friendly_name(&peer),
thumbprint);
stream.write(msg.as_bytes())?;
return Ok(false);
}
if let Err(e) = is_valid_time(&peer) {
let msg = format!("ERR certificate ({}) thumbprint '{}' cannot be used: {}\r\n",
get_friendly_name(&peer),
thumbprint,
e);
stream.write(msg.as_bytes())?;
return Ok(false);
}
}
};
return Ok(true);
}
Notice that I’m doing all of this (defining external function, defining helper functions) inside the authenticate_certificate function. Coming up with that was harder than expected, but I really liked the fact that it was possible and that I can just shove this into a corner of my code and not have to make a Big Thing out of it.
And with that, I the authentication portion of my network protocol in Rust done.
The next stage is going to be implementing a server that can handle more than a single connection at a time
Published at DZone with permission of Oren Eini, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments