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?

Upvotes: 2

Views: 275

Answers (1)

Finn Bear
Finn Bear

Reputation: 1970

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:

  • You can and should pass the server IP instead of "N/A" for server name.
  • The default certificate verifier requires the IP to be among the subject names.
[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.

Upvotes: 2

Related Questions