| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
mod common; |
| 9 |
|
| 10 |
use common::{hermetic, run, text, Serving}; |
| 11 |
use spectra_core::keys::KeyPair; |
| 12 |
use spectra_mcp::client::{Client, Endpoint}; |
| 13 |
use spectra_mcp::tls::Pinned; |
| 14 |
use std::path::Path; |
| 15 |
|
| 16 |
|
| 17 |
fn server_for(s: &Path, owner_home: &Path) -> (Serving, u16) { |
| 18 |
let owner = KeyPair::load_home(owner_home).unwrap(); |
| 19 |
let o = run(s, &["init", "--owner", &owner.key.to_hex(), "--name", "earth"]); |
| 20 |
assert!(o.status.success(), "{}", text(&o)); |
| 21 |
Serving::start_tls(s) |
| 22 |
} |
| 23 |
|
| 24 |
async fn instance_owner(port: u16) -> serde_json::Value { |
| 25 |
let p = Pinned::new(None); |
| 26 |
let c = Client::at(Endpoint::Tls { addr: format!("127.0.0.1:{port}"), config: p.client_config().unwrap() }, None); |
| 27 |
c.send("GET", "/api/v1/instance", &[], vec![]).await.unwrap().json().unwrap()["owner"].clone() |
| 28 |
} |
| 29 |
|
| 30 |
#[tokio::test] |
| 31 |
async fn the_owner_connects_delegates_and_claims() { |
| 32 |
let (sd, cd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 33 |
let (s, c) = (sd.path(), cd.path()); |
| 34 |
KeyPair::create_home(&c.join("owner"), "Andy McCall", "").unwrap(); |
| 35 |
let (srv, port) = server_for(s, &c.join("owner")); |
| 36 |
let url = format!("https://127.0.0.1:{port}"); |
| 37 |
|
| 38 |
|
| 39 |
let o = run(c, &["connect", &url]); |
| 40 |
assert!(!o.status.success(), "{}", text(&o)); |
| 41 |
assert!(text(&o).contains("--yes"), "{}", text(&o)); |
| 42 |
assert!(!c.join("owner/client.toml").exists(), "nothing trusted yet"); |
| 43 |
|
| 44 |
let o = run(c, &["connect", &url, "--expect", "00ff"]); |
| 45 |
assert!(!o.status.success() && text(&o).contains("does not start with"), "{}", text(&o)); |
| 46 |
|
| 47 |
|
| 48 |
|
| 49 |
|
| 50 |
let instance = KeyPair::instance(&s.join("data"), "earth").unwrap(); |
| 51 |
let before = KeyPair::load_home(&c.join("owner")).unwrap(); |
| 52 |
|
| 53 |
let p: serde_json::Value = serde_json::from_slice(&run(c, &["probe", &url, "--json"]).stdout).unwrap(); |
| 54 |
assert_eq!(p["instance"], instance.key.to_hex()); |
| 55 |
assert_eq!(p["waiting_for"], before.key.to_hex()); |
| 56 |
assert_eq!(p["yours"], true, "{p}"); |
| 57 |
assert!(!c.join("owner/client.toml").exists(), "probing trusts nothing"); |
| 58 |
|
| 59 |
let o = run(c, &["connect", &url, "--expect", &instance.key.short(16)]); |
| 60 |
assert!(o.status.success(), "{}", text(&o)); |
| 61 |
assert!(text(&o).contains("Claimed"), "{}", text(&o)); |
| 62 |
let current = KeyPair::load_home(&c.join("owner")).unwrap(); |
| 63 |
assert_ne!(current.key, before.key); |
| 64 |
assert!(current.identity.delegates_to(&instance.key, 1)); |
| 65 |
let cfg = std::fs::read_to_string(c.join("owner/client.toml")).unwrap(); |
| 66 |
assert!(cfg.contains(&instance.key.to_hex()) && cfg.contains("tls_spki"), "{cfg}"); |
| 67 |
assert_eq!(instance_owner(port).await, current.key.to_hex(), "recorded at the owner's current identity"); |
| 68 |
|
| 69 |
|
| 70 |
let o = run(c, &["connect", &url]); |
| 71 |
assert!(o.status.success(), "{}", text(&o)); |
| 72 |
assert!(text(&o).contains("Connected"), "{}", text(&o)); |
| 73 |
srv.stop(); |
| 74 |
|
| 75 |
|
| 76 |
std::fs::remove_file(s.join("data/keys/tls.crt")).unwrap(); |
| 77 |
std::fs::remove_file(s.join("data/keys/tls.key")).unwrap(); |
| 78 |
let (srv, port) = Serving::start_tls(s); |
| 79 |
let url = format!("https://127.0.0.1:{port}"); |
| 80 |
let cfg = std::fs::read_to_string(c.join("owner/client.toml")).unwrap(); |
| 81 |
let old_pin: [u8; 32] = |
| 82 |
hex::decode(cfg.lines().find(|l| l.starts_with("tls_spki")).unwrap().split('"').nth(1).unwrap()).unwrap().try_into().unwrap(); |
| 83 |
let pinned = Client::at(Endpoint::tls(&url, old_pin).unwrap(), None); |
| 84 |
assert!(pinned.send("GET", "/api/v1/instance", &[], vec![]).await.is_err()); |
| 85 |
|
| 86 |
let o = run(c, &["connect", &url]); |
| 87 |
assert!(o.status.success(), "{}", text(&o)); |
| 88 |
let cfg2 = std::fs::read_to_string(c.join("owner/client.toml")).unwrap(); |
| 89 |
assert_ne!(cfg, cfg2, "re-pinned"); |
| 90 |
srv.stop(); |
| 91 |
|
| 92 |
let o = hermetic(s).args(["admin", "verify"]).output().unwrap(); |
| 93 |
assert!(o.status.success(), "{}", text(&o)); |
| 94 |
} |
| 95 |
|
| 96 |
#[test] |
| 97 |
fn someone_elses_server_is_refused() { |
| 98 |
let (sd, cd, md) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap()); |
| 99 |
let (s, c, m) = (sd.path(), cd.path(), md.path()); |
| 100 |
KeyPair::create_home(&c.join("owner"), "Andy McCall", "").unwrap(); |
| 101 |
KeyPair::create_home(&m.join("owner"), "Mallory", "").unwrap(); |
| 102 |
let (srv, port) = server_for(s, &m.join("owner")); |
| 103 |
let o = run(c, &["connect", &format!("https://127.0.0.1:{port}")]); |
| 104 |
assert!(!o.status.success()); |
| 105 |
assert!(text(&o).contains("was set up for"), "{}", text(&o)); |
| 106 |
srv.stop(); |
| 107 |
} |