open-standards/joule-code/joule-code-rs/crates/joule-moq-remote/tests/tls.rs
Transaction Science 91271ef917 joule-moq-remote: a sealing key a session, sealed inputs opened before they are injected; TLS to the room service and the relay
The host makes an X25519 sealing key for each session and states it in its screen statement; the
gate decides every input by all nine rules of remote-desktop §5.3 (wai-rs's decide_opened),
opening a sealed input with the session's opening key, and the host injects what it seals. The
opening key stays in the session unless --opening-key writes it (mode 0600, never over a file)
for a verifier. The headless viewer seals its key, text and clipboard actions to the screen's
sealing key, keeps the records it receives, and follows another principal's events when told.

The agent reaches the room service over https:// and the relay over wss://: rustls with ring,
the system's roots and any certificate a PEM file names (--ca), every chain and name verified.
tls::front is a TLS front for a plain backend. Lock files gain tokio-rustls and the new edges
alone.

tests/tls.rs: the service and the relay through a TLS front, trusted by a PEM file; refused under
the system's roots alone, and for a certificate that names another host. tests/loopback.rs:
alice's typing sealed, injected as typed, absent from what bob receives; her text in the clear
refused input_unsealed; the session verified at the opened level.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 10:43:13 -04:00

118 lines
5.7 KiB
Rust

//! The host agent's TLS clients: the room service over `https://` and the relay over `wss://`,
//! through a TLS front on loopback whose certificate is made here.
//!
//! - Trusting that certificate (given as a PEM file, as `--ca` gives it), the service answers over
//! `https://` and the relay's session opens over `wss://`.
//! - Trusting only the system's roots, which never signed it, each refuses the server: the
//! certificate's chain is verified.
//! - A certificate for another name than the host the URL names is refused even when its chain is
//! trusted: the name is verified too.
use joule_moq_relay::cert::RelayIdentity;
use joule_moq_relay::client::ClientOptions;
use joule_moq_relay::{RelayConfig, RelayServer};
use joule_moq_remote::service::RoomService;
use joule_moq_remote::tls::{self, Trust};
use joule_moq_remote::wire;
use std::net::SocketAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
/// A plain HTTP backend that answers every request with a small JSON body.
async fn backend() -> SocketAddr {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let at = l.local_addr().unwrap();
tokio::spawn(async move {
loop {
let Ok((mut s, _)) = l.accept().await else { return };
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
let mut got = Vec::new();
while !got.windows(4).any(|w| w == b"\r\n\r\n") {
match s.read(&mut buf).await {
Ok(0) | Err(_) => return,
Ok(n) => got.extend_from_slice(&buf[..n]),
}
}
let host = String::from_utf8_lossy(&got).lines().find_map(|l| l.strip_prefix("Host: ").map(str::to_string)).unwrap_or_default();
let body = format!("{{\"over\":\"tls\",\"host\":\"{host}\"}}");
let _ = s.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await;
let _ = s.shutdown().await;
});
}
});
at
}
fn identity(names: &[&str]) -> RelayIdentity {
RelayIdentity::dev_self_signed(&names.iter().map(|n| n.to_string()).collect::<Vec<_>>()).expect("a certificate")
}
/// The certificate's chain as a PEM file, as `--ca` names one.
fn pem_file(id: &RelayIdentity, name: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!("wai-remote-tls-{}-{name}.pem", std::process::id()));
std::fs::write(&p, id.chain.iter().map(tls::pem_of).collect::<String>()).unwrap();
p
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_room_service_over_https_is_verified() {
let id = identity(&["127.0.0.1"]);
let front = tls::front(id.chain.clone(), id.key.clone_key(), backend().await).await.unwrap();
let ca = pem_file(&id, "service");
let trusting = Trust::system_and_pem(&ca).expect("the PEM file reads");
let _ = std::fs::remove_file(&ca);
let url = format!("https://{front}");
let svc = RoomService::new(&url).unwrap().trusting(trusting);
let a = svc.call("GET", "/room", None, &[]).await.expect("the service answers over TLS");
assert_eq!(a.status, 200);
assert_eq!(a.body["over"], "tls");
assert_eq!(a.body["host"], front.to_string(), "the request's Host is the client's, through the front");
// The system's roots never signed this certificate: the service is refused.
let refused = RoomService::new(&url).unwrap().call("GET", "/room", None, &[]).await;
let e = refused.expect_err("an untrusted certificate is refused");
assert!(e.contains("certificate"), "{e}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_certificate_for_another_name_is_refused() {
let id = identity(&["relay.invalid"]);
let front = tls::front(id.chain.clone(), id.key.clone_key(), backend().await).await.unwrap();
let svc = RoomService::new(&format!("https://{front}")).unwrap().trusting(Trust::only(id.chain.clone()));
let e = svc.call("GET", "/room", None, &[]).await.expect_err("the certificate names another host");
assert!(e.contains("certificate"), "{e}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_relay_over_wss_is_verified() {
let mut c = RelayConfig::loopback();
c.quic_listen = None;
let server = RelayServer::start(c).await.expect("the relay starts");
let plain = server.websocket_addr.expect("a WebSocket listener");
let path = server.relay().config().path.clone();
let id = identity(&["127.0.0.1"]);
let front = tls::front(id.chain.clone(), id.key.clone_key(), plain).await.unwrap();
let url = format!("wss://{front}{path}");
let client = tokio::time::timeout(Duration::from_secs(20), wire::connect(&url, ClientOptions::default(), &Trust::only(id.chain.clone())))
.await
.expect("in time")
.expect("a session over wss:// with its certificate trusted");
client.close(0, "done");
let e = match wire::connect(&url, ClientOptions::default(), &Trust::system()).await {
Ok(_) => panic!("a session with a certificate the system's roots never signed"),
Err(e) => e,
};
assert!(e.contains("certificate"), "{e}");
let other = identity(&["relay.invalid"]);
let front2 = tls::front(other.chain.clone(), other.key.clone_key(), plain).await.unwrap();
let e = match wire::connect(&format!("wss://{front2}{path}"), ClientOptions::default(), &Trust::only(other.chain.clone())).await {
Ok(_) => panic!("a session with a certificate for another name"),
Err(e) => e,
};
assert!(e.contains("certificate"), "{e}");
server.shutdown().await;
}