//! Setting a server up without the owner's secret key (spec ยง8.6, //! spectra#22): `spectra init --owner KEYID`, nothing served but who it //! is until the named owner claims it, and nobody else may. mod common; use common::{hermetic, run, text, Serving}; use spectra_core::daemon::WireEvent; use spectra_core::keys::KeyPair; use spectra_core::principals::{identity_payload, signed_event}; use spectra_mcp::client::{Client, Endpoint}; use spectra_mcp::tls::Pinned; fn claim_body(pair: &KeyPair) -> Vec { let e = signed_event(pair, "human", "identity.owner", identity_payload(&pair.identity)); serde_json::to_vec(&vec![WireEvent::from_new(&e)]).unwrap() } #[tokio::test] async fn the_named_owner_claims_a_server_and_nobody_else_can() { let dir = tempfile::tempdir().unwrap(); let d = dir.path(); // The owner's key is on another machine: here there is only its KeyId. let owner = KeyPair::generate("Andy McCall", ""); let o = run(d, &["init", "--owner", &owner.key.to_hex(), "--name", "earth"]); assert!(o.status.success(), "{}", text(&o)); assert!(text(&o).contains("not yet claimed"), "{}", text(&o)); assert!(!d.join("owner").exists(), "no owner home was read or made"); let (srv, port) = Serving::start_tls(d); let probe = Pinned::new(None); let anon = Client::at(Endpoint::Tls { addr: format!("127.0.0.1:{port}"), config: probe.client_config().unwrap() }, None); let info = anon.send("GET", "/api/v1/instance", &[], vec![]).await.unwrap().json().unwrap(); assert_eq!(info["name"], "earth"); assert!(info["owner"].is_null(), "unclaimed"); assert_eq!(info["named_owner"], owner.key.to_hex(), "says whose it waits for"); let pin = probe.seen().unwrap().1; let url = format!("https://127.0.0.1:{port}"); // Before the claim, even the owner's signed requests are refused, and say why. let as_owner = Client::at(Endpoint::tls(&url, pin).unwrap(), Some(owner.clone())); let r = as_owner.send("GET", "/api/v1/agents", &[], vec![]).await.unwrap(); assert_eq!(r.status, 401); assert!(String::from_utf8_lossy(&r.body).contains("no owner yet"), "{}", String::from_utf8_lossy(&r.body)); // Someone else cannot claim it. let mallory = KeyPair::generate("Mallory", ""); let r = anon.send("POST", "/api/v1/claim", &[], claim_body(&mallory)).await.unwrap(); assert_eq!(r.status, 403); assert!(String::from_utf8_lossy(&r.body).contains("another owner")); // Nor can a claim naming the owner but signed by another key. let mut forged = signed_event(&mallory, "human", "identity.owner", identity_payload(&owner.identity)); forged.payload = identity_payload(&owner.identity); let r = anon.send("POST", "/api/v1/claim", &[], serde_json::to_vec(&vec![WireEvent::from_new(&forged)]).unwrap()).await.unwrap(); assert_eq!(r.status, 403); // The owner can, once. let r = anon.send("POST", "/api/v1/claim", &[], claim_body(&owner)).await.unwrap(); assert_eq!(r.status, 200, "{}", String::from_utf8_lossy(&r.body)); let r = as_owner.send("GET", "/api/v1/agents", &[], vec![]).await.unwrap(); assert_eq!(r.status, 200, "{}", String::from_utf8_lossy(&r.body)); let r = anon.send("POST", "/api/v1/claim", &[], claim_body(&owner)).await.unwrap(); assert_eq!(r.status, 403); assert!(String::from_utf8_lossy(&r.body).contains("already has an owner")); let info = anon.send("GET", "/api/v1/instance", &[], vec![]).await.unwrap().json().unwrap(); assert_eq!(info["owner"], owner.key.to_hex()); srv.stop(); // An owner recorded after the instance verifies, and rebuilds. for args in [["admin", "verify"], ["admin", "rebuild"], ["admin", "check"]] { let o = hermetic(d).args(args).output().unwrap(); assert!(o.status.success(), "{args:?}: {}", text(&o)); } } #[test] fn init_refuses_what_is_not_a_key_id() { let dir = tempfile::tempdir().unwrap(); let o = run(dir.path(), &["init", "--owner", "andy"]); assert!(!o.status.success()); assert!(text(&o).contains("not a KeyId"), "{}", text(&o)); }