crates/monetize-front/tests/attach.rs
//! ★ f-attach-enrol (money 2026-09-28): the CENTRAL monetize's side of an
//! estate attaching from another cloud — the operator mints a one-shot token
//! at the door (operator only), the estate POSTs /attach/enrol with its CSR
//! and its owner's SSHSIG, and gets a certificate under the central's attach
//! CA, the attach CA, the central's identity name and fact key. The token is
//! spent; a second enrolment is refused.
//!
//! G7 (5.4, pull): the record is where monetize-server's guardian takes it
//! (`<data>/joined/<estate>.json`): the estate is DECLARED under its product
//! and its box pins its key on its first call. Monetize never dials it.
use std::time::Duration;
use axum::body::Body;
use axum::http::Request;
use monetize_front::attach::{mint_token, routes, Central};
use plugin_door_wire::attach::{EnrolReply, EnrolRequest, VERSION};
use tower::ServiceExt as _;
fn owner() -> ssh_key::PrivateKey {
ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::from_seed(&[4u8; 32]))
}
async fn post(app: axum::Router, body: String) -> (u16, String) {
let res = app.oneshot(Request::post("/attach/enrol").header("content-type", "application/json").body(Body::from(body)).unwrap()).await.unwrap();
let st = res.status().as_u16();
let bytes = axum::body::to_bytes(res.into_body(), 1 << 20).await.unwrap();
(st, String::from_utf8_lossy(&bytes).to_string())
}
#[tokio::test]
async fn an_estate_on_another_cloud_enrols_once_and_the_guardian_takes_it_to_be_called() {
let dir = tempfile::tempdir().unwrap();
let central = Central { root: dir.path().to_path_buf(), plugin_door: "monetize.ignalina.dk:443".into() };
let token = mint_token(¢ral, "acme", "gunnar", Duration::from_secs(3600)).unwrap();
let (csr, _key) = nordisk_pairing::mtls::box_request().unwrap();
let key = owner();
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64;
let req = EnrolRequest {
v: VERSION,
token,
estate: "acme".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: "0123456789abcdef-n".into(),
at_unix_ms: now,
owner_sig: String::new(),
}
.sign(&key)
.unwrap();
let body = serde_json::to_string(&req).unwrap();
let (st, text) = post(routes(central.clone()), body.clone()).await;
assert_eq!(st, 200, "{text}");
let reply: EnrolReply = serde_json::from_str(&text).unwrap();
assert_eq!(nordisk_pairing::mtls::cert_cn(&reply.cert_pem).unwrap(), "acme/main");
assert_eq!((reply.plugin_door.as_str(), reply.product.as_str()), ("monetize.ignalina.dk:443", "gunnar"));
assert_eq!(reply.monetize_pubkey.len(), 64, "the central's fact key, hex");
// G8: the key the estate is handed is its PRODUCT's, not the central's root —
// a fact for another product does not verify on this estate.
let root = monetize_front::intake::signer(dir.path()).unwrap();
let hex = |k: [u8; 32]| k.iter().map(|b| format!("{b:02x}")).collect::<String>();
assert_eq!(reply.monetize_pubkey, hex(monetize::Keyring::derive(&root, "gunnar").public_key()), "the gunnar key");
assert_ne!(reply.monetize_pubkey, hex(root.public_key()), "the root signs nothing an estate trusts");
// ST-15 (no legacy): the dial-in model's SSH identity is gone — none is minted, none sent.
assert_eq!((reply.monetize_identity.as_str(), reply.monetize_identity_key.as_str()), ("", ""), "no SSH identity under pull");
assert!(!dir.path().join("monetize/attach/identity-ssh").exists(), "no SSH identity is minted");
let rec = monetize_front::attach::enrolled(¢ral, "acme").unwrap().expect("the enrolment is recorded");
assert_eq!((rec.product.as_str(), rec.endpoint.as_str()), ("gunnar", "gunnar.acme.example:443"));
assert!(rec.estate_ca_pem.contains("ESTATE"));
// G7: the guardian takes the record — the estate is declared under its product and the
// box that enrolled may pin its key on its first call.
let state = monetize_server::state::AppState::in_memory(monetize_server::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()));
let lines = monetize_server::fleet::take_joined(&state, &dir.path().join(monetize_front::intake::DATA_REL).join(monetize_server::fleet::JOINED_REL));
let g = state.fleet.guardian().unwrap();
assert_eq!(g.store.product_of("acme").as_deref(), Some("gunnar"), "{lines:?}");
assert!(g.store.pin_window("acme", "main") > state.now(), "the box may pin: {lines:?}");
let ca_now = std::fs::read_to_string(dir.path().join(monetize_front::attach::ATTACH_CA_REL)).unwrap();
assert_eq!(reply.attach_ca_pem, ca_now);
// The token is spent.
let (st, text) = post(routes(central.clone()), body).await;
assert_eq!(st, 403, "{text}");
assert!(text.contains("attach-token-used"), "{text}");
// Garbage is a 400, by name.
let (st, text) = post(routes(central), "{".into()).await;
assert_eq!(st, 400, "{text}");
}
/// The front records where monetize-server's guardian takes them.
#[test]
fn the_front_records_an_enrolment_where_the_server_takes_it() {
assert_eq!(monetize_front::attach::ESTATES_REL, format!("{}/{}", monetize_front::intake::DATA_REL, monetize_server::fleet::JOINED_REL));
}
async fn post_to(app: axum::Router, path: &str, body: String) -> (u16, String) {
let res = app.oneshot(Request::post(path).header("content-type", "application/json").body(Body::from(body)).unwrap()).await.unwrap();
let st = res.status().as_u16();
let bytes = axum::body::to_bytes(res.into_body(), 1 << 20).await.unwrap();
(st, String::from_utf8_lossy(&bytes).to_string())
}
fn now_ms() -> i64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as i64
}
fn installed(roles: &[&str], at: i64, key: &ssh_key::PrivateKey) -> String {
let n = plugin_door_wire::attach::InstalledNotice { v: VERSION, estate: "acme".into(), product: "gunnar".into(), roles: roles.iter().map(|r| r.to_string()).collect(), nonce: "0123456789abcdef-i".into(), at_unix_ms: at, ticket_key: String::new(), fleet_keys: Default::default(), owner_sig: String::new() };
serde_json::to_string(&n.sign(key).unwrap()).unwrap()
}
fn installed_keyed(roles: &[&str], at: i64, ticket_key: &str, key: &ssh_key::PrivateKey) -> String {
let n = plugin_door_wire::attach::InstalledNotice { v: VERSION, estate: "acme".into(), product: "gunnar".into(), roles: roles.iter().map(|r| r.to_string()).collect(), nonce: format!("0123456789abcdef-{at}"), at_unix_ms: at, ticket_key: ticket_key.into(), fleet_keys: Default::default(), owner_sig: String::new() };
serde_json::to_string(&n.sign(key).unwrap()).unwrap()
}
/// ★ **The site's ticket key comes from the estate itself** (STINKY ST-3, 2026-10-11: both
/// sites' `appliance_key` were unset on the ONE box, so every actor ticket was refused). The
/// enrolled owner's install notice carries its elected primary's `ticket_pubkey`; the central
/// records it with the estate, a later notice with none keeps it, and the guardian hands it to
/// the registry as that site's appliance key — the key a ticket from that site is checked
/// under. An operator's `[estates.<label>] appliance_key` stays a PIN: when it is set and the
/// estate delivers another key, the pin wins and the delivered one is refused by name.
#[tokio::test]
async fn an_attached_estates_owner_delivers_its_sites_ticket_key() {
let dir = tempfile::tempdir().unwrap();
let central = Central { root: dir.path().to_path_buf(), plugin_door: "monetize.ignalina.dk:443".into() };
let key = owner();
let token = mint_token(¢ral, "acme", "gunnar", Duration::from_secs(3600)).unwrap();
let (csr, _) = nordisk_pairing::mtls::box_request().unwrap();
let req = EnrolRequest { v: VERSION, token, estate: "acme".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: String::new(), nonce: "0123456789abcdef-n".into(), at_unix_ms: now_ms(), owner_sig: String::new() }.sign(&key).unwrap();
let (st, text) = post_to(routes(central.clone()), "/attach/enrol", serde_json::to_string(&req).unwrap()).await;
assert_eq!(st, 200, "{text}");
let path = plugin_door_wire::attach::INSTALLED_PATH;
let real = |seed: u8| monetize::Signer::from_bytes(&[seed; 32]).public_key().iter().map(|b| format!("{b:02x}")).collect::<String>();
let ticket = real(3);
let at = now_ms();
let (st, text) = post_to(routes(central.clone()), path, installed_keyed(&["main", "twin"], at, &ticket, &key)).await;
assert_eq!(st, 200, "{text}");
assert_eq!(monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().appliance_key, ticket, "recorded with the estate");
assert!(monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().installed_with_ticket_key, "PIN-2: the keyed notice is said to be one (the guardian re-opens no window for it)");
let next = monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().installed_unix_ms + 1;
let (st, text) = post_to(routes(central.clone()), path, installed_keyed(&["main", "twin"], next, "", &key)).await;
assert_eq!(st, 200, "{text}");
assert_eq!(monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().appliance_key, ticket, "a notice with no key keeps the one delivered");
assert!(!monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().installed_with_ticket_key, "PIN-2: a notice with no key lays boxes");
let joined = dir.path().join(monetize_front::intake::DATA_REL).join(monetize_server::fleet::JOINED_REL);
let site = |pin: &str| monetize_server::config::EstateConfig { product: "gunnar".into(), vendor: "mockpay".into(), appliance_key: pin.into(), ..Default::default() };
let state = monetize_server::state::AppState::in_memory(monetize_server::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()).estate("acme", site("")));
assert_eq!(state.registry.appliance_key_of("acme"), None, "nothing before the guardian takes the record");
let lines = monetize_server::fleet::take_joined(&state, &joined);
assert_eq!(state.registry.appliance_key_of("acme").as_deref(), Some(ticket.as_str()), "the site's key, delivered by the estate: {lines:?}");
assert!(lines.join("\n").contains("appliance key"), "said once: {lines:?}");
let pin = real(4);
let pinned = monetize_server::state::AppState::in_memory(monetize_server::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()).estate("acme", site(&pin)));
let lines = monetize_server::fleet::take_joined(&pinned, &joined);
assert_eq!(pinned.registry.appliance_key_of("acme").as_deref(), Some(pin.as_str()), "the operator's pin wins");
assert!(lines.join("\n").contains("REFUSED") && lines.join("\n").contains("pin"), "the delivered key is refused by name: {lines:?}");
assert!(!dir.path().join(monetize_front::attach::ESTATES_REL).join("acme.json.refused").exists(), "the estate's join stands; only its key is refused");
let undeclared = monetize_server::state::AppState::in_memory(monetize_server::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()));
let lines = monetize_server::fleet::take_joined(&undeclared, &joined);
assert_eq!(undeclared.registry.appliance_key_of("acme"), None, "a site not declared here takes no key: {lines:?}");
}
/// ★ **An attached estate's boxes are pinned from its INSTALL, every role** (i54a run13: test2,
/// enrolled at the ONE box, was laid for 15 minutes; its main's first report came after the
/// ten-minute window counted from the ENROLMENT had closed, and its twin never had one — both
/// "not signed by a box this monetize knows"). After the install the enrolled owner says which
/// boxes will call (`POST /attach/installed`, signed with the ENROLLED key); the guardian opens
/// each role's window from then, as for a sold estate's install PASS. A stranger's notice, one
/// for an estate that never enrolled, and a replay are refused by name and open nothing.
#[tokio::test]
async fn an_attached_estates_owner_opens_its_boxes_pin_windows_after_the_install() {
let dir = tempfile::tempdir().unwrap();
let central = Central { root: dir.path().to_path_buf(), plugin_door: "monetize.ignalina.dk:443".into() };
let key = owner();
let path = plugin_door_wire::attach::INSTALLED_PATH;
let (st, text) = post_to(routes(central.clone()), path, installed(&["main", "twin"], now_ms(), &key)).await;
assert_eq!(st, 403, "an estate that never enrolled: {text}");
assert!(text.contains("attach-not-enrolled"), "{text}");
let token = mint_token(¢ral, "acme", "gunnar", Duration::from_secs(3600)).unwrap();
let (csr, _) = nordisk_pairing::mtls::box_request().unwrap();
let req = EnrolRequest { v: VERSION, token, estate: "acme".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: String::new(), nonce: "0123456789abcdef-n".into(), at_unix_ms: now_ms(), owner_sig: String::new() }.sign(&key).unwrap();
let (st, text) = post_to(routes(central.clone()), "/attach/enrol", serde_json::to_string(&req).unwrap()).await;
assert_eq!(st, 200, "{text}");
let stranger = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::from_seed(&[9u8; 32]));
let (st, text) = post_to(routes(central.clone()), path, installed(&["main", "twin"], now_ms(), &stranger)).await;
assert_eq!(st, 403, "{text}");
assert!(text.contains("attach-signature"), "only the enrolled owner: {text}");
let at = now_ms();
let (st, text) = post_to(routes(central.clone()), path, installed(&["main", "twin"], at, &key)).await;
assert_eq!(st, 200, "{text}");
let rec = monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap();
assert_eq!(rec.roles, ["main", "twin"]);
assert!(rec.installed_unix_ms >= at);
let (st, text) = post_to(routes(central.clone()), path, installed(&["main", "twin"], at, &key)).await;
assert_eq!(st, 403, "the same notice again: {text}");
assert!(text.contains("attach-replayed"), "{text}");
// The guardian takes it: every named role may pin, the window counted from the notice.
let state = monetize_server::state::AppState::in_memory(monetize_server::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()));
let lines = monetize_server::fleet::take_joined(&state, &dir.path().join(monetize_front::intake::DATA_REL).join(monetize_server::fleet::JOINED_REL));
let g = state.fleet.guardian().unwrap();
for role in ["main", "twin"] {
assert_eq!(g.store.pin_window("acme", role), rec.installed_unix_ms as u64 + monetize_server::fleet::ATTACHED_INSTALL_WINDOW_MS, "{role}: the whole install, not the 10-minute window: {lines:?}");
}
}
/// ★ **An enrolment hands the estate its OWN bearer** (owner 2026-10-11: gunnar.rs and
/// test2.gunnar.rs on ONE box, each its own Stripe account): shown once in the reply, kept
/// only as its digest; the guardian takes it, and monetize-server names the estate a call
/// with it comes from.
#[tokio::test]
async fn an_enrolment_hands_the_estate_its_own_bearer_kept_only_as_a_digest() {
let dir = tempfile::tempdir().unwrap();
let central = Central { root: dir.path().to_path_buf(), plugin_door: "monetize.ignalina.dk:443".into() };
let key = owner();
let token = mint_token(¢ral, "acme", "gunnar", Duration::from_secs(3600)).unwrap();
let (csr, _) = nordisk_pairing::mtls::box_request().unwrap();
let req = EnrolRequest { v: VERSION, token, estate: "acme".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: String::new(), nonce: "0123456789abcdef-n".into(), at_unix_ms: now_ms(), owner_sig: String::new() }.sign(&key).unwrap();
let (st, text) = post_to(routes(central.clone()), "/attach/enrol", serde_json::to_string(&req).unwrap()).await;
assert_eq!(st, 200, "{text}");
let reply: EnrolReply = serde_json::from_str(&text).unwrap();
assert_eq!(reply.estate_bearer.len(), 64, "32 random bytes, hex: {}", reply.estate_bearer);
let rec = monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap();
assert_eq!(rec.bearer_sha256, monetize_server::fleet::bearer_digest(&reply.estate_bearer));
let kept = std::fs::read_to_string(dir.path().join(monetize_front::attach::ESTATES_REL).join("acme.json")).unwrap();
assert!(!kept.contains(&reply.estate_bearer), "the bearer itself is never kept");
let state = monetize_server::state::AppState::in_memory(monetize_server::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()));
let _ = monetize_server::fleet::take_joined(&state, &dir.path().join(monetize_front::intake::DATA_REL).join(monetize_server::fleet::JOINED_REL));
assert_eq!(state.fleet.estate_of_bearer(&reply.estate_bearer).as_deref(), Some("acme"));
assert_eq!(state.fleet.estate_of_bearer("someone-else"), None);
}
fn enrol_req(central: &Central, key: &ssh_key::PrivateKey, nonce: &str) -> String {
let token = mint_token(central, "acme", "gunnar", Duration::from_secs(3600)).unwrap();
let (csr, _) = nordisk_pairing::mtls::box_request().unwrap();
let req = EnrolRequest { v: VERSION, token, estate: "acme".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: String::new(), nonce: nonce.into(), at_unix_ms: now_ms(), owner_sig: String::new() }.sign(key).unwrap();
serde_json::to_string(&req).unwrap()
}
/// ★ **STINKY ST-13: the estate bearer has a lifecycle.** A re-enrolment (a new one-shot
/// token) mints a new bearer and the OLD one is refused on the guardian's next take — no
/// restart; the record carries the bearer's expiry; the operator's revoke takes it away.
#[tokio::test]
async fn a_re_enrolment_replaces_the_estate_bearer_and_a_revoke_takes_it_away() {
let dir = tempfile::tempdir().unwrap();
let central = Central { root: dir.path().to_path_buf(), plugin_door: "monetize.ignalina.dk:443".into() };
let key = owner();
let joined = dir.path().join(monetize_front::intake::DATA_REL).join(monetize_server::fleet::JOINED_REL);
let state = monetize_server::state::AppState::in_memory(monetize_server::registry::Registry::new().product(monetize::testing::MockProduct::gunnar()));
let (st, text) = post_to(routes(central.clone()), "/attach/enrol", enrol_req(¢ral, &key, "0123456789abcdef-1")).await;
assert_eq!(st, 200, "{text}");
let first: EnrolReply = serde_json::from_str(&text).unwrap();
let rec = monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap();
assert!(rec.bearer_until_unix_ms > now_ms() as u64 && rec.bearer_until_unix_ms <= now_ms() as u64 + monetize_server::fleet::BEARER_LIFE_MS, "the bearer expires: {}", rec.bearer_until_unix_ms);
let _ = monetize_server::fleet::take_joined(&state, &joined);
assert_eq!(state.fleet.estate_of_bearer(&first.estate_bearer).as_deref(), Some("acme"));
let (st, text) = post_to(routes(central.clone()), "/attach/enrol", enrol_req(¢ral, &key, "0123456789abcdef-2")).await;
assert_eq!(st, 200, "{text}");
let second: EnrolReply = serde_json::from_str(&text).unwrap();
let _ = monetize_server::fleet::take_joined(&state, &joined);
assert_eq!(state.fleet.estate_of_bearer(&first.estate_bearer), None, "the old bearer is refused after the re-enrolment");
assert_eq!(state.fleet.estate_of_bearer(&second.estate_bearer).as_deref(), Some("acme"));
let said = monetize_front::attach::revoke(¢ral, "acme").unwrap();
assert!(said.contains("acme"), "{said}");
let rec = monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap();
assert!(rec.bearer_sha256.is_empty() && rec.bearer_revoked_unix_ms > 0, "{rec:?}");
let _ = monetize_server::fleet::take_joined(&state, &joined);
assert_eq!(state.fleet.estate_of_bearer(&second.estate_bearer), None, "a revoked bearer opens nothing");
assert!(state.fleet.bearer_of("acme").is_some_and(|b| b.revoked_unix_ms > 0));
assert!(monetize_front::attach::revoke(¢ral, "nobody").is_err(), "an estate that never enrolled is refused by name");
}
/// ★ STINKY ST-14 (2026-10-12): **the owner's notice names each box's own fleet key, and the
/// central records exactly those** with the estate — the guardian pins them, no first caller
/// inside a window takes a role. A later notice without keys (an install laying NEW boxes)
/// clears them, so its boxes pin inside the window it opens as before.
#[tokio::test]
async fn an_attached_estates_owner_names_each_boxs_fleet_key() {
let dir = tempfile::tempdir().unwrap();
let central = Central { root: dir.path().to_path_buf(), plugin_door: "monetize.ignalina.dk:443".into() };
let key = owner();
let token = mint_token(¢ral, "acme", "gunnar", Duration::from_secs(3600)).unwrap();
let (csr, _) = nordisk_pairing::mtls::box_request().unwrap();
let req = EnrolRequest { v: VERSION, token, estate: "acme".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: String::new(), nonce: "0123456789abcdef-n".into(), at_unix_ms: now_ms(), owner_sig: String::new() }.sign(&key).unwrap();
let (st, text) = post_to(routes(central.clone()), "/attach/enrol", serde_json::to_string(&req).unwrap()).await;
assert_eq!(st, 200, "{text}");
let path = plugin_door_wire::attach::INSTALLED_PATH;
let keys: std::collections::BTreeMap<String, String> = [("main".to_string(), "aa".repeat(32)), ("twin".to_string(), "bb".repeat(32))].into();
let at = now_ms();
let n = plugin_door_wire::attach::InstalledNotice { v: VERSION, estate: "acme".into(), product: "gunnar".into(), roles: vec!["main".into(), "twin".into()], nonce: format!("0123456789abcdef-{at}"), at_unix_ms: at, ticket_key: String::new(), fleet_keys: keys.clone(), owner_sig: String::new() }.sign(&key).unwrap();
let (st, text) = post_to(routes(central.clone()), path, serde_json::to_string(&n).unwrap()).await;
assert_eq!(st, 200, "{text}");
assert_eq!(monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().fleet_keys, keys, "recorded as the owner named them");
let next = monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().installed_unix_ms + 1;
let (st, text) = post_to(routes(central.clone()), path, installed(&["main", "twin"], next, &key)).await;
assert_eq!(st, 200, "{text}");
assert!(monetize_front::attach::enrolled(¢ral, "acme").unwrap().unwrap().fleet_keys.is_empty(), "an unkeyed notice lays new boxes: no stale key is kept");
}