crates/installer/plugin-door-wire/tests/register_over_mtls.rs
//! **The whole client, against a door over real mutual TLS.**
//!
//! A fake plugin door (the installer CA, a server certificate named
//! `bootstrap.installer`, the client's CN read off the handshake) that keeps
//! what it was sent, the way vm1's does: `GET /register/<id>` answers what it
//! holds, `POST` is taken only from the box it names as primary. What is
//! proven is the rule every product server lives by — **register once, again
//! only when something changed, and never from the wrong box** — through the
//! same `register_if_changed` gunnar-server and holger-server call.
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use nordisk_pairing::mtls::{self, InstallerCa, Issued};
use plugin_door_wire::{digest, register_if_changed, Done, Held, Me, Plugin, Registered, SelfRegister};
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
const DAY: Duration = Duration::from_secs(24 * 3600);
#[derive(Default)]
struct Door {
held: Option<Held>,
posts: usize,
}
fn boxed(ca: &InstallerCa, name: &str) -> Issued {
let (csr, key) = mtls::box_request().expect("csr");
Issued { cert_pem: ca.sign_box(&csr, name, DAY).expect("sign"), key_pem: key }
}
/// One request per connection, like the real door (`Connection: close`).
async fn serve(ca: InstallerCa, door_cert: Issued, primary: &'static str, state: Arc<Mutex<Door>>) -> SocketAddr {
let cfg = mtls::server_config(&ca.cert_pem, &door_cert).expect("server config");
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind");
let at = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let Ok((tcp, _)) = listener.accept().await else { return };
let acceptor = tokio_rustls::TlsAcceptor::from(cfg.clone());
let state = state.clone();
tokio::spawn(async move {
let Ok(mut tls) = acceptor.accept(tcp).await else { return };
let cn = mtls::peer_cn(tls.get_ref().1).unwrap_or_default();
let mut buf = Vec::new();
let mut chunk = [0u8; 65536];
// Read the head, then Content-Length bytes of body.
let (head, body) = loop {
let n = tls.read(&mut chunk).await.unwrap_or(0);
if n == 0 {
return;
}
buf.extend_from_slice(&chunk[..n]);
if let Some(i) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
let head = String::from_utf8_lossy(&buf[..i]).to_string();
let len: usize = head
.lines()
.find_map(|l| l.strip_prefix("Content-Length: "))
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0);
while buf.len() < i + 4 + len {
let n = tls.read(&mut chunk).await.unwrap_or(0);
if n == 0 {
break;
}
buf.extend_from_slice(&chunk[..n]);
}
break (head, buf[i + 4..].to_vec());
}
};
let mut words = head.split_whitespace();
let (method, path) = (words.next().unwrap_or(""), words.next().unwrap_or(""));
let id = path.strip_prefix("/register/").unwrap_or("").to_string();
let (status, json) = {
let mut d = state.lock().unwrap();
match method {
_ if cn != primary && cn != mtls::INSTALLER_CN => (403, serde_json::to_string(&Registered::refused("self-register-not-primary", format!("`{cn}` is not the primary"))).unwrap()),
"GET" => (200, serde_json::to_string(&d.held.clone().unwrap_or(Held { id: id.clone(), ..Held::default() })).unwrap()),
"POST" => {
let req: SelfRegister = serde_json::from_slice(&body).expect("a registration");
use base64::Engine as _;
let module = base64::engine::general_purpose::STANDARD.decode(&req.module).unwrap();
let outcome = if d.held.is_some() { "REPLACED" } else { "ADDED" };
d.posts += 1;
d.held = Some(Held { id, digest: Some(digest(&module)), ticket_key: req.ticket_key, registrar: Some(cn.clone()), monetize_pubkey: Some("ab".repeat(32)) });
(200, serde_json::to_string(&Registered { outcome: outcome.into(), line: format!("{outcome} by {cn}"), monetize_pubkey: Some("ab".repeat(32)) }).unwrap())
}
_ => (405, "{}".into()),
}
};
let answer = format!("HTTP/1.1 {status} X\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{json}", json.len());
let _ = tls.write_all(answer.as_bytes()).await;
let _ = tls.shutdown().await;
});
}
});
at
}
#[tokio::test(flavor = "current_thread")]
async fn once_then_nothing_then_replaced_and_never_from_the_twin() {
let ca = InstallerCa::mint("door.test", DAY).expect("ca");
let state = Arc::new(Mutex::new(Door::default()));
let door = serve(ca.clone(), boxed(&ca, "vm1"), "vm2", state.clone()).await;
let primary = boxed(&ca, "vm2");
let me = Me { name: "vm2", identity: &primary, ca_pem: &ca.cert_pem, ticket_key: None };
let v1 = b"\0asm\x01\0\0\0 version one".to_vec();
let v2 = b"\0asm\x01\0\0\0 version two".to_vec();
// First time: sent, ADDED, and the front's monetize key comes back.
let done = register_if_changed(door, &me, "holger", Plugin { module: &v1, signature: b"sig" }).await.expect("first");
assert!(matches!(&done, Done::Sent { status: 200, answer } if answer.outcome == "ADDED"), "{done:?}");
assert_eq!(done.monetize_pubkey(), Some("ab".repeat(32).as_str()));
// Every later boot at the same version: nothing is sent.
let done = register_if_changed(door, &me, "holger", Plugin { module: &v1, signature: b"sig" }).await.expect("again");
assert!(matches!(done, Done::Unchanged(_)), "{done:?}");
assert_eq!(state.lock().unwrap().posts, 1, "the same module from the same box is never sent twice");
// A new version: REPLACED.
let done = register_if_changed(door, &me, "holger", Plugin { module: &v2, signature: b"sig" }).await.expect("new version");
assert!(matches!(&done, Done::Sent { answer, .. } if answer.outcome == "REPLACED"), "{done:?}");
assert_eq!(state.lock().unwrap().held.as_ref().and_then(|h| h.digest.clone()), Some(digest(&v2)));
// The twin, with a certificate from the same CA: refused, nothing written.
let twin = boxed(&ca, "vm3");
let as_twin = Me { name: "vm3", identity: &twin, ca_pem: &ca.cert_pem, ticket_key: None };
let err = register_if_changed(door, &as_twin, "holger", Plugin { module: &v1, signature: b"sig" }).await.expect_err("the twin");
assert!(err.contains("403"), "{err}");
assert_eq!(state.lock().unwrap().posts, 2);
// A certificate from ANOTHER CA does not get through the handshake at all.
let stranger_ca = InstallerCa::mint("elsewhere", DAY).unwrap();
let stranger = boxed(&stranger_ca, "vm2");
let as_stranger = Me { name: "vm2", identity: &stranger, ca_pem: &ca.cert_pem, ticket_key: None };
assert!(register_if_changed(door, &as_stranger, "holger", Plugin { module: &v1, signature: b"sig" }).await.is_err());
assert_eq!(state.lock().unwrap().posts, 2);
}
/// A door that serves the CA the client pins but does NOT trust the client's
/// certificate (another CA): under TLS 1.3 the client's handshake completes and
/// the refusal arrives as an alert on its FIRST READ. MEASURED on the o-ops and
/// release-3 mocks 2026-09-29: main's self-registration said only
/// `no HTTP answer: ""` — the read's error was thrown away, so the real reason
/// (the alert) was never said.
#[tokio::test]
async fn a_door_that_refuses_the_client_certificate_is_named_not_an_empty_answer() {
let ca = InstallerCa::mint("door.test", DAY).expect("ca");
let other = InstallerCa::mint("other.test", DAY).expect("other ca");
let door_cert = boxed(&ca, "vm1");
// The door presents `ca`'s certificate, and takes clients of `other` only.
let cfg = mtls::server_config(&other.cert_pem, &door_cert).expect("server config");
let cfg = {
let mut c = (*cfg).clone();
c.cert_resolver = mtls::server_config(&ca.cert_pem, &door_cert).expect("door cert").cert_resolver.clone();
Arc::new(c)
};
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind");
let door = listener.local_addr().unwrap();
tokio::spawn(async move {
while let Ok((tcp, _)) = listener.accept().await {
let acceptor = tokio_rustls::TlsAcceptor::from(cfg.clone());
tokio::spawn(async move {
let _ = acceptor.accept(tcp).await;
});
}
});
let primary = boxed(&ca, "vm2");
let me = Me { name: "vm2", identity: &primary, ca_pem: &ca.cert_pem, ticket_key: None };
let v1 = b"\0asm\x01\0\0\0 version one".to_vec();
let e = register_if_changed(door, &me, "gunnar", Plugin { module: &v1, signature: b"sig" }).await.expect_err("a refused certificate is no answer");
assert!(e.contains("no HTTP answer"), "{e}");
assert!(!e.ends_with("no HTTP answer: \"\""), "the read's own error must be said, not an empty answer: {e}");
assert!(e.to_ascii_lowercase().contains("alert") || e.to_ascii_lowercase().contains("certificate"), "the TLS alert names the refused certificate: {e}");
}