sslnetwork-programmingrustp2ptls1.3

How to make a TLS connection between two peers using Rust tokio_native_tls


I am working in a file transfer system that has p2p connectivity library using Rust. It works using just tcp but now I want to improve it using TLS but I do not understand 2 things. First is how can 2 peers share the Certificate Authorities CA so they can verify that the connection is trustworthy.

Secondly, I am trying to use the tokio_native_tls which wraps the native_tls library and adds async. I am following the examples in the native_tls docs. However they use connector with a domain and as the domain they use Google.

This is the example they provide.

use native_tls::TlsConnector;
use std::io::{Read, Write};
use std::net::TcpStream;

let connector = TlsConnector::new().unwrap();

let stream = TcpStream::connect("google.com:443").unwrap();
let mut stream = connector.connect("google.com", stream).unwrap();

stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
let mut res = vec![];
stream.read_to_end(&mut res).unwrap();
println!("{}", String::from_utf8_lossy(&res));

In the peer to peer connection I describe there is only 2 peers trying to transfer some files. Since they don't have a domain only IP it would not work. I am pretty lost. How can I approach this project?


Solution

  • native-tls

    The docs for the synchronous connect function say that:

    The domain is ignored if both SNI and hostname verification are disabled.

    Taking that into account, switching to the async API's, and generating a self-signed certificate, we arrive at the following working code:

    [package]
    name = "tls"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    rcgen = "0.13.1"
    tokio = { version = "1.41.1", features = ["full"] }
    tokio-native-tls = "0.3.1"
    
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::{TcpListener, TcpStream};
    use tokio_native_tls::native_tls::Identity;
    use tokio_native_tls::{native_tls, TlsAcceptor, TlsConnector};
    
    #[tokio::main]
    async fn main() {
        let cert = rcgen::generate_simple_self_signed([]).unwrap();
        let listener = TcpListener::bind("127.0.0.1:8001").await.unwrap();
    
        let trusted = native_tls::Certificate::from_pem(cert.cert.pem().as_bytes()).unwrap();
        tokio::spawn(async move {
            let connector = TlsConnector::from(
                native_tls::TlsConnector::builder()
                    .disable_built_in_roots(true)
                    .add_root_certificate(trusted)
                    .danger_accept_invalid_hostnames(true)
                    .use_sni(false)
                    .build()
                    .unwrap(),
            );
            let tcp = TcpStream::connect("0.0.0.0:8001").await.unwrap();
            let mut tls = connector.connect("N/A", tcp).await.unwrap();
            tls.write_all(b"hello!").await.unwrap();
            tls.shutdown().await.unwrap();
        });
    
        let acceptor = TlsAcceptor::from(
            native_tls::TlsAcceptor::new(
                Identity::from_pkcs8(
                    cert.cert.pem().as_bytes(),
                    cert.key_pair.serialize_pem().as_bytes(),
                )
                .unwrap(),
            )
            .unwrap(),
        );
    
        let (tcp, _) = listener.accept().await.unwrap();
        let mut tls = acceptor.accept(tcp).await.unwrap();
        let mut buf = String::new();
        tls.read_to_string(&mut buf).await.unwrap();
        println!("read: {buf}");
    }
    

    rustls

    Here is similar code using rustls. Differences include:

    [package]
    name = "tls"
    version = "0.1.0"
    edition = "2021"
    
    [dependencies]
    rcgen = "0.13.1"
    tokio = { version = "1.41.1", features = ["full"] }
    tokio-rustls = "0.26.0"
    
    use std::net::Ipv4Addr;
    use std::sync::Arc;
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::{TcpListener, TcpStream};
    use tokio_rustls::rustls::pki_types::pem::PemObject;
    use tokio_rustls::rustls::pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, ServerName};
    use tokio_rustls::rustls::{ClientConfig, RootCertStore};
    use tokio_rustls::{rustls, TlsAcceptor, TlsConnector};
    
    #[tokio::main]
    async fn main() {
        let cert = rcgen::generate_simple_self_signed(["127.0.0.1".to_owned()]).unwrap();
        let listener = TcpListener::bind("127.0.0.1:8001").await.unwrap();
    
        let mut trusted = RootCertStore::empty();
        trusted.add(cert.cert.der().clone()).unwrap();
        tokio::spawn(async move {
            let connector: TlsConnector = TlsConnector::from(Arc::new(
                ClientConfig::builder()
                    .with_root_certificates(trusted)
                    .with_no_client_auth(),
            ));
            let tcp = TcpStream::connect("127.0.0.1:8001").await.unwrap();
            let mut tls = connector
                .connect(
                    ServerName::IpAddress(Ipv4Addr::new(127, 0, 0, 1).into()),
                    tcp,
                )
                .await
                .unwrap();
            tls.write_all(b"hello!").await.unwrap();
            tls.shutdown().await.unwrap();
        });
    
        let acceptor = TlsAcceptor::from(Arc::new(
            rustls::ServerConfig::builder()
                .with_no_client_auth()
                .with_single_cert(
                    vec![cert.cert.der().clone()],
                    PrivateKeyDer::Pkcs8(
                        PrivatePkcs8KeyDer::from_pem_slice(cert.key_pair.serialize_pem().as_bytes())
                            .unwrap(),
                    ),
                )
                .unwrap(),
        ));
    
        let (tcp, _) = listener.accept().await.unwrap();
        let mut tls = acceptor.accept(tcp).await.unwrap();
        let mut buf = String::new();
        tls.read_to_string(&mut buf).await.unwrap();
        println!("read: {buf}");
    }
    

    If you wanted to use a CA-signed certificate, you would add the CA certificate as a trusted root instead.