OXYGENHUB
◆ v0.1.0 spectra / tests / connect.rs
4.9 KiB · Rust · last changed Spectra stands alone: its own identities, signatures and CBOR; projects of any kind of work (#26, #27) 1 day ago source blame history raw
98a0fb9f Andy McCall ↶ 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.
eca5af92 Andy McCall ↶ 7
8
mod common;
9
98a0fb9f Andy McCall ↶ 10
use common::{hermetic, run, text, Serving};
eca5af92 Andy McCall ↶ 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
98a0fb9f Andy McCall ↶ 38
    // First contact, not at a terminal and not confirmed: nothing trusted.
eca5af92 Andy McCall ↶ 39
    let o = run(c, &["connect", &url]);
98a0fb9f Andy McCall ↶ 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));
eca5af92 Andy McCall ↶ 46
98a0fb9f Andy McCall ↶ 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 ...
eca5af92 Andy McCall ↶ 50
    let instance = KeyPair::instance(&s.join("data"), "earth").unwrap();
98a0fb9f Andy McCall ↶ 51
    let before = KeyPair::load_home(&c.join("owner")).unwrap();
52
    // ... and the server is trusted, pinned, and claimed as theirs.
53
    let o = run(c, &["connect", &url, "--expect", &instance.key.short(16)]);
eca5af92 Andy McCall ↶ 54
    assert!(o.status.success(), "{}", text(&o));
55
    assert!(text(&o).contains("Claimed"), "{}", text(&o));
98a0fb9f Andy McCall ↶ 56
    let current = KeyPair::load_home(&c.join("owner")).unwrap();
57
    assert_ne!(current.key, before.key);
58
    assert!(current.identity.delegates_to(&instance.key, 1));
59
    let cfg = std::fs::read_to_string(c.join("owner/client.toml")).unwrap();
eca5af92 Andy McCall ↶ 60
    assert!(cfg.contains(&instance.key.to_hex()) && cfg.contains("tls_spki"), "{cfg}");
61
    assert_eq!(instance_owner(port).await, current.key.to_hex(), "recorded at the owner's current identity");
62
63
    // Again: nothing to claim, still trusted.
64
    let o = run(c, &["connect", &url]);
65
    assert!(o.status.success(), "{}", text(&o));
66
    assert!(text(&o).contains("Connected"), "{}", text(&o));
67
    srv.stop();
68
69
    // A new certificate (the old one lost) is refused on the old pin ...
70
    std::fs::remove_file(s.join("data/keys/tls.crt")).unwrap();
71
    std::fs::remove_file(s.join("data/keys/tls.key")).unwrap();
72
    let (srv, port) = Serving::start_tls(s);
73
    let url = format!("https://127.0.0.1:{port}");
98a0fb9f Andy McCall ↶ 74
    let cfg = std::fs::read_to_string(c.join("owner/client.toml")).unwrap();
eca5af92 Andy McCall ↶ 75
    let old_pin: [u8; 32] =
76
        hex::decode(cfg.lines().find(|l| l.starts_with("tls_spki")).unwrap().split('"').nth(1).unwrap()).unwrap().try_into().unwrap();
77
    let pinned = Client::at(Endpoint::tls(&url, old_pin).unwrap(), None);
78
    assert!(pinned.send("GET", "/api/v1/instance", &[], vec![]).await.is_err());
79
    // ... until connect checks that the same instance key vouches for it.
80
    let o = run(c, &["connect", &url]);
81
    assert!(o.status.success(), "{}", text(&o));
98a0fb9f Andy McCall ↶ 82
    let cfg2 = std::fs::read_to_string(c.join("owner/client.toml")).unwrap();
eca5af92 Andy McCall ↶ 83
    assert_ne!(cfg, cfg2, "re-pinned");
84
    srv.stop();
85
86
    let o = hermetic(s).args(["admin", "verify"]).output().unwrap();
87
    assert!(o.status.success(), "{}", text(&o));
88
}
89
90
#[test]
91
fn someone_elses_server_is_refused() {
92
    let (sd, cd, md) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
93
    let (s, c, m) = (sd.path(), cd.path(), md.path());
94
    KeyPair::create_home(&c.join("owner"), "Andy McCall", "").unwrap();
95
    KeyPair::create_home(&m.join("owner"), "Mallory", "").unwrap();
96
    let (srv, port) = server_for(s, &m.join("owner"));
97
    let o = run(c, &["connect", &format!("https://127.0.0.1:{port}")]);
98
    assert!(!o.status.success());
99
    assert!(text(&o).contains("was set up for"), "{}", text(&o));
100
    srv.stop();
101
}