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

nordisk/monetize

crates/monetize-cli/tests/door_client.rs

//! ★ **The box's door through the one HTTPS client** (RA1-4): `monetize box certs` over the
//! real mutual TLS (nordisk-pairing's installer CA, the operator's certificate from the
//! material directory), against a door that answers CHUNKED as hyper does. The one client
//! reads both the door and a product's front with the same parser, so the door's chunked
//! answer is put back together (the door's own reader before RA1-4 kept the chunk sizes).

use std::io::{BufRead as _, BufReader, Write as _};
use std::net::TcpListener;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use nordisk_pairing::installer::{CA_CERT, INSTALLER_CERT, INSTALLER_KEY};
use nordisk_pairing::mtls::{self, InstallerCa};

/// What the door saw: each client's CN and request line.
type Seen = Arc<Mutex<Vec<(Option<String>, String)>>>;

/// The door: mTLS under `ca`, every client's CN and request line seen, the answer chunked.
fn door(ca: &InstallerCa, body: &'static str) -> (String, Seen) {
    let served = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
    let cfg = mtls::server_config(&ca.cert_pem, &served).unwrap();
    let l = TcpListener::bind("127.0.0.1:0").unwrap();
    let at = format!("127.0.0.1:{}", l.local_addr().unwrap().port());
    let seen: Seen = Arc::default();
    let s = seen.clone();
    std::thread::spawn(move || {
        for tcp in l.incoming().flatten() {
            let (cfg, s) = (cfg.clone(), s.clone());
            std::thread::spawn(move || {
                let conn = rustls::ServerConnection::new(cfg).unwrap();
                let mut tls = rustls::StreamOwned::new(conn, tcp);
                let mut line = String::new();
                {
                    let mut r = BufReader::new(&mut tls);
                    if r.read_line(&mut line).is_err() {
                        return;
                    }
                    loop {
                        let mut h = String::new();
                        if r.read_line(&mut h).is_err() || h.trim().is_empty() {
                            break;
                        }
                    }
                }
                s.lock().unwrap().push((mtls::peer_cn(&tls.conn), line.trim().to_string()));
                let (a, b) = body.split_at(body.len() / 2);
                let _ = write!(tls, "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ntransfer-encoding: chunked\r\n\r\n{:x}\r\n{a}\r\n{:x}\r\n{b}\r\n0\r\n\r\n", a.len(), b.len());
                tls.conn.send_close_notify();
                let _ = tls.flush();
            });
        }
    });
    (at, seen)
}

fn monetize(args: &[&str]) -> (bool, String, String) {
    let out = std::process::Command::new(env!("CARGO_BIN_EXE_monetize")).args(args).env_remove("MONETIZE_DOOR").env_remove("MONETIZE_MATERIAL").output().unwrap();
    (out.status.success(), String::from_utf8_lossy(&out.stdout).to_string(), String::from_utf8_lossy(&out.stderr).to_string())
}

#[test]
fn box_certs_reads_a_chunked_door_answer_over_the_operators_mutual_tls() {
    let ca = InstallerCa::mint("ra1-door", Duration::from_secs(3600)).unwrap();
    let me = ca.issue_installer(Duration::from_secs(3600)).unwrap();
    let material = tempfile::tempdir().unwrap();
    std::fs::write(material.path().join(CA_CERT), &ca.cert_pem).unwrap();
    std::fs::write(material.path().join(INSTALLER_CERT), &me.cert_pem).unwrap();
    std::fs::write(material.path().join(INSTALLER_KEY), &me.key_pem).unwrap();
    let report = r#"{"certificates":[{"host":"door.example","hosts":["door.example"],"lines":["valid until 2027-01-01","next attempt in 59 days"]}]}"#;
    let (at, seen) = door(&ca, report);
    let m = material.path().display().to_string();
    let (ok, out, err) = monetize(&["--door", &at, "--material", &m, "box", "certs"]);
    assert!(ok, "{out}{err}");
    assert_eq!(out, "door.example (door.example)\n  valid until 2027-01-01\n  next attempt in 59 days\n", "the chunked answer put back together");
    let seen = seen.lock().unwrap().clone();
    assert_eq!(seen, [(Some(mtls::INSTALLER_CN.to_string()), "GET /self/certs HTTP/1.1".to_string())], "the operator's certificate, one request");

    // A client certificate from another CA gets no answer, and the client says so (no hang).
    let other = InstallerCa::mint("ra1-other", Duration::from_secs(3600)).unwrap();
    let them = other.issue_installer(Duration::from_secs(3600)).unwrap();
    let wrong = tempfile::tempdir().unwrap();
    std::fs::write(wrong.path().join(CA_CERT), &ca.cert_pem).unwrap();
    std::fs::write(wrong.path().join(INSTALLER_CERT), &them.cert_pem).unwrap();
    std::fs::write(wrong.path().join(INSTALLER_KEY), &them.key_pem).unwrap();
    let (ok, out, err) = monetize(&["--door", &at, "--material", &wrong.path().display().to_string(), "box", "certs"]);
    assert!(!ok && err.contains(&at), "{out}{err}");
}