OXYGENHUB
# c7161ec1a607 spectra / tests / connect.rs
5.3 KiB · Rust · last changed spectra status, probe and bridge: what the VS Code extension drives (#31) 1 day ago source blame history raw
1
//! `spectra connect` (spec §8.6): a workstation trusts a server once the
2
//! owner confirms its instance key and delegates to it, and that key
3
//! vouches for its certificate; it claims a server set up with `init
4
//! --owner`, even though delegating changed the owner's KeyId; a new
5
//! certificate is refused until connect runs again; someone else's server
6
//! is refused.
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
/// A server in `s` set up for the owner whose home is `owner_home`.
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
    // First contact, not at a terminal and not confirmed: nothing trusted.
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
    // A key that does not match what `init` printed is refused.
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
    // The owner checks the instance key against what `init` printed (here,
48
    // by --expect) and connects: their identity now delegates to it, under
49
    // a new KeyId ...
50
    let instance = KeyPair::instance(&s.join("data"), "earth").unwrap();
51
    let before = KeyPair::load_home(&c.join("owner")).unwrap();
52
    // `probe` says what the server is, without trusting it.
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
    // ... and the server is trusted, pinned, and claimed as theirs.
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
    // Again: nothing to claim, still trusted.
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
    // A new certificate (the old one lost) is refused on the old pin ...
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
    // ... until connect checks that the same instance key vouches for it.
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
}