crates/installer/plugin-door-wire/tests/attach_enrolment.rs
//! f-attach-enrol (money 2026-09-28): an estate on another cloud enrols with
//! the CENTRAL monetize once — a one-shot token, the owner's SSHSIG, its own
//! key's CSR signed under the attach CA — and every way to get it wrong is
//! refused by name.
use std::time::Duration;
use nordisk_pairing::mtls::{box_request, InstallerCa};
use plugin_door_wire::attach::{enrol, EnrolReply, EnrolRequest, Refusal, Tokens, NAMESPACE, VERSION};
const NOW: i64 = 1_790_600_000_000;
fn owner(seed: u8) -> ssh_key::PrivateKey {
ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::from_seed(&[seed; 32]))
}
fn request(token: &str, estate: &str, key: &ssh_key::PrivateKey, at: i64) -> EnrolRequest {
let (csr, _key) = box_request().unwrap();
EnrolRequest {
v: VERSION,
token: token.into(),
estate: estate.into(),
product: "gunnar".into(),
box_name: "main".into(),
csr_pem: csr,
endpoint: "gunnar.acme.example:443".into(),
owner_key: key.public_key().to_openssh().unwrap(),
estate_ca_pem: "-----BEGIN CERTIFICATE-----\nESTATE\n-----END CERTIFICATE-----\n".into(),
nonce: "n0nce-0123456789abcdef".into(),
at_unix_ms: at,
owner_sig: String::new(),
}
.sign(key)
.unwrap()
}
fn tokens() -> (tempfile::TempDir, Tokens) {
let dir = tempfile::tempdir().unwrap();
let t = Tokens::at(dir.path().join("attach-tokens.json"));
(dir, t)
}
fn reply(ca: &InstallerCa) -> impl FnOnce(String) -> EnrolReply + '_ {
move |cert| EnrolReply {
cert_pem: cert,
attach_ca_pem: ca.cert_pem.clone(),
monetize_identity: "monetize-central".into(),
monetize_identity_key: owner(9).public_key().to_openssh().unwrap(),
monetize_pubkey: "00".repeat(32),
plugin_door: "monetize.ignalina.dk:8475".into(),
product: "gunnar".into(),
estate_bearer: String::new(),
}
}
#[test]
fn an_estate_enrols_once_with_a_token_and_its_owners_signature_and_gets_a_certificate_under_the_attach_ca() {
let (_d, t) = tokens();
let ca = InstallerCa::mint("monetize attach", Duration::from_secs(3600)).unwrap();
let token = t.mint("acme", "gunnar", NOW, 3_600_000, [7u8; 32]).unwrap();
assert!(token.starts_with("att1."));
assert!(!std::fs::read_to_string(_d.path().join("attach-tokens.json")).unwrap().contains(token.rsplit('.').next().unwrap()), "the store holds no usable token");
let key = owner(1);
let req = request(&token, "acme", &key, NOW);
let out = enrol(&req, &t, NOW + 1_000, |csr, cn| ca.sign_attached(csr, cn, "gunnar.acme.example", Duration::from_secs(3600)).map_err(|e| e.to_string()), reply(&ca)).unwrap();
assert!(out.cert_pem.contains("BEGIN CERTIFICATE"));
assert_eq!(nordisk_pairing::mtls::cert_cn(&out.cert_pem).unwrap(), "acme/main", "CN names the estate and the box");
// The same token again: used.
let again = enrol(&req, &t, NOW + 2_000, |_, _| Ok(String::new()), reply(&ca)).unwrap_err();
assert_eq!(again, Refusal::TokenUsed);
}
#[test]
fn every_way_to_get_an_enrolment_wrong_is_refused_by_name_and_redeems_nothing() {
let (_d, t) = tokens();
let ca = InstallerCa::mint("monetize attach", Duration::from_secs(3600)).unwrap();
let token = t.mint("acme", "gunnar", NOW, 3_600_000, [9u8; 32]).unwrap();
let key = owner(2);
let sign = |csr: &str, cn: &str| ca.sign_attached(csr, cn, "gunnar.acme.example", Duration::from_secs(3600)).map_err(|e| e.to_string());
// Another key signed it than the one it names.
let mut forged = request(&token, "acme", &key, NOW);
forged.owner_key = owner(3).public_key().to_openssh().unwrap();
assert!(matches!(enrol(&forged, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)));
// A field changed after signing — the endpoint, or the CA the central is
// to trust the estate's control plane by.
let mut edited = request(&token, "acme", &key, NOW);
edited.endpoint = "evil.example:443".into();
assert!(matches!(enrol(&edited, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)));
let mut swapped_ca = request(&token, "acme", &key, NOW);
swapped_ca.estate_ca_pem = "-----BEGIN CERTIFICATE-----\nEVIL\n-----END CERTIFICATE-----\n".into();
assert!(matches!(enrol(&swapped_ca, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)), "the estate's CA is signed too");
// A signature made under another namespace (a git commit signature) never verifies here.
let mut other_ns = request(&token, "acme", &key, NOW);
other_ns.owner_sig = key.sign("git", ssh_key::HashAlg::Sha512, &other_ns.signed_bytes()).unwrap().to_pem(ssh_key::LineEnding::LF).unwrap();
assert!(matches!(enrol(&other_ns, &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Signature(_)));
assert_ne!(NAMESPACE, "git");
// A stale request.
assert!(matches!(enrol(&request(&token, "acme", &key, NOW - 3_600_000), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::Clock { .. }));
// A token minted for another estate.
assert!(matches!(enrol(&request(&token, "other", &key, NOW), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::TokenOtherEstate { .. }));
// An unknown token, and an expired one.
assert_eq!(enrol(&request("att1.0000.ffff", "acme", &key, NOW), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::TokenUnknown);
let late = t.mint("acme", "gunnar", NOW - 10_000, 1_000, [5u8; 32]).unwrap();
assert_eq!(enrol(&request(&late, "acme", &key, NOW), &t, NOW, sign, reply(&ca)).unwrap_err(), Refusal::TokenExpired);
// A bad shape.
let mut shape = request(&token, "acme", &key, NOW);
shape.box_name = "Main Box".into();
assert!(matches!(shape.verify(NOW).unwrap_err(), Refusal::Shape(_)));
// None of those spent the good token.
assert!(enrol(&request(&token, "acme", &key, NOW), &t, NOW, sign, reply(&ca)).is_ok(), "the token was still good after every refusal");
// Every refusal says its clause.
assert!(Refusal::TokenUsed.to_string().starts_with("REFUSED [attach-token-used]"));
}