Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-flight/tests/tls_roundtrip.rs

//! Acceptance for the TLS-terminating transport path (design §f1, `tls` feature):
//! a `do_put` → `do_get` round-trip driven **over a self-signed TLS channel**
//! returns the batches byte-identical, and the same holds with bearer auth
//! layered under TLS.
//!
//! This whole file compiles only with `--features tls` (it exercises
//! `FlightServer::serve_with_tls[_and_auth]` + `FlightConn::connect_tls`, which
//! are `#[cfg(feature = "tls")]`). It is RED-when-broken: if the TLS serve/connect
//! seam drops or mangles a batch, or fails to establish the encrypted channel,
//! these assertions fail. The cert is generated in-process with `rcgen` — no
//! `openssl` subprocess (LAW #9, zero-shell).

#![cfg(feature = "tls")]

use std::net::{SocketAddr, TcpListener};
use std::sync::Arc;
use std::time::Duration;

use arrow_array::{Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
use nornir_flight::{FlightConn, FlightRoutes, FlightServer, MemFlightStore};
use tonic::transport::{Certificate, ClientTlsConfig, Identity, ServerTlsConfig};

/// Grab an OS-assigned free port (bind, read, drop — the server rebinds it).
fn free_addr() -> SocketAddr {
    TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap()
}

/// `n` distinct two-column batches (Int64 + nullable Utf8).
fn sample_batches(n: usize) -> Vec<RecordBatch> {
    let schema = Arc::new(Schema::new(vec![
        Field::new("id", DataType::Int64, false),
        Field::new("name", DataType::Utf8, true),
    ]));
    (0..n)
        .map(|i| {
            let base = i as i64 * 10;
            let ids = Int64Array::from(vec![base, base + 1, base + 2]);
            let names = StringArray::from(vec![Some(format!("r{i}-a")), None, Some(format!("r{i}-c"))]);
            RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(names)]).unwrap()
        })
        .collect()
}

/// A fresh self-signed cert/key for `localhost`, returned as (cert_pem, key_pem).
/// The one cert is BOTH the server identity AND (self-signed) the client's trust
/// root — a hermetic loopback PKI, generated purely in Rust.
fn self_signed_localhost() -> (String, String) {
    let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
    // rcgen 0.14 renamed `CertifiedKey::key_pair` to `signing_key` (same `KeyPair`).
    (ck.cert.pem(), ck.signing_key.serialize_pem())
}

/// Start a TLS Flight server (fresh `MemFlightStore`) on a free port with `token`
/// auth (empty ⇒ open) using `cert_pem`/`key_pem` as its identity, and return
/// (`https://localhost:port`, client-trust-config) once it accepts connects.
async fn start_tls_server(token: &str, cert_pem: &str, key_pem: &str) -> (String, ClientTlsConfig) {
    let addr = free_addr();
    let routes = FlightRoutes::shared(Arc::new(MemFlightStore::new()));
    let tok = Arc::new(token.to_string());
    let server_tls = ServerTlsConfig::new().identity(Identity::from_pem(cert_pem, key_pem));
    tokio::spawn(async move {
        let _ = FlightServer::serve_with_tls_and_auth(routes, addr, tok, server_tls).await;
    });
    for _ in 0..200 {
        if tokio::net::TcpStream::connect(addr).await.is_ok() {
            break;
        }
        tokio::time::sleep(Duration::from_millis(20)).await;
    }
    // The self-signed server cert IS the client's CA; SNI/verification host is
    // `localhost`, matching the cert's SAN.
    let client_tls = ClientTlsConfig::new()
        .ca_certificate(Certificate::from_pem(cert_pem))
        .domain_name("localhost");
    (format!("https://localhost:{}", addr.port()), client_tls)
}

#[tokio::test]
async fn tls_put_then_get_roundtrip_byte_identical() {
    let (cert, key) = self_signed_localhost();
    let (ep, client_tls) = start_tls_server("", &cert, &key).await;
    let mut c = FlightConn::connect_tls(ep, client_tls).await.unwrap();
    let batches = sample_batches(3);
    c.put("build/42", batches.clone()).await.unwrap();
    let got = c.get("build/42").await.unwrap();
    assert_eq!(
        got, batches,
        "do_get over TLS must return the do_put batches byte-identical"
    );
}

#[tokio::test]
async fn tls_exchange_roundtrip_byte_identical() {
    let (cert, key) = self_signed_localhost();
    let (ep, client_tls) = start_tls_server("", &cert, &key).await;
    let mut c = FlightConn::connect_tls(ep, client_tls).await.unwrap();
    let batches = sample_batches(4);
    let got = c.exchange("x/1", batches.clone()).await.unwrap();
    assert_eq!(got, batches, "do_exchange over TLS must echo the batches byte-identical");
}

#[tokio::test]
async fn tls_with_bearer_auth_roundtrip() {
    // TLS layered UNDER bearer auth: correct token + trusted cert round-trips.
    let token = "a-long-enough-secret-token-1234";
    let (cert, key) = self_signed_localhost();
    let (ep, client_tls) = start_tls_server(token, &cert, &key).await;
    let mut c = FlightConn::connect_tls(ep, client_tls)
        .await
        .unwrap()
        .with_bearer(token)
        .unwrap();
    let batches = sample_batches(2);
    c.put("k", batches.clone()).await.unwrap();
    assert_eq!(
        c.get("k").await.unwrap(),
        batches,
        "correct bearer over TLS must be accepted and round-trip"
    );
}

#[tokio::test]
async fn tls_missing_token_rejected() {
    // Even over a trusted TLS channel, a missing bearer is UNAUTHENTICATED — TLS
    // secures the pipe, auth guards the verb (RED if TLS silently drops auth).
    let token = "a-long-enough-secret-token-1234";
    let (cert, key) = self_signed_localhost();
    let (ep, client_tls) = start_tls_server(token, &cert, &key).await;
    let mut c = FlightConn::connect_tls(ep, client_tls).await.unwrap(); // no bearer
    let err = c
        .put("k", sample_batches(1))
        .await
        .expect_err("unauthenticated put over TLS MUST be rejected");
    let msg = err.to_string().to_lowercase();
    assert!(
        msg.contains("bearer") || msg.contains("unauthenticated"),
        "expected an auth rejection over TLS, got: {msg}"
    );
}