OXYGENHUB
# 01320eefd505 spectra / tests / connect.rs
4.8 KiB · Rust · last changed spectra connect: trust a server through the owner's delegation, pin it, claim it (#21) 1 day ago source blame history raw
1
//! `spectra connect` (spec §8.6, spectra#21): a workstation trusts a server
2
//! because the owner delegates to its instance key and that key vouches for
3
//! its certificate; it claims a server set up with `init --owner`, even
4
//! though delegating changed the owner's KeyId; a new certificate is
5
//! refused until connect runs again; someone else's server is refused.
6
7
mod common;
8
9
use common::{delegate, hermetic, run, text, Serving};
10
use spectra_core::keys::KeyPair;
11
use spectra_mcp::client::{Client, Endpoint};
12
use spectra_mcp::tls::Pinned;
13
use std::path::Path;
14
15
/// A server in `s` set up for the owner whose home is `owner_home`.
16
fn server_for(s: &Path, owner_home: &Path) -> (Serving, u16) {
17
    let owner = KeyPair::load_home(owner_home).unwrap();
18
    let o = run(s, &["init", "--owner", &owner.key.to_hex(), "--name", "earth"]);
19
    assert!(o.status.success(), "{}", text(&o));
20
    Serving::start_tls(s)
21
}
22
23
async fn instance_owner(port: u16) -> serde_json::Value {
24
    let p = Pinned::new(None);
25
    let c = Client::at(Endpoint::Tls { addr: format!("127.0.0.1:{port}"), config: p.client_config().unwrap() }, None);
26
    c.send("GET", "/api/v1/instance", &[], vec![]).await.unwrap().json().unwrap()["owner"].clone()
27
}
28
29
#[tokio::test]
30
async fn the_owner_connects_delegates_and_claims() {
31
    let (sd, cd) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
32
    let (s, c) = (sd.path(), cd.path());
33
    KeyPair::create_home(&c.join("owner"), "Andy McCall", "").unwrap();
34
    let (srv, port) = server_for(s, &c.join("owner"));
35
    let url = format!("https://127.0.0.1:{port}");
36
37
    // First contact: not yet delegated, so not trusted; told what to run.
38
    let o = run(c, &["connect", &url]);
39
    assert_eq!(o.status.code(), Some(1), "{}", text(&o));
40
    assert!(text(&o).contains("oxy identity delegate add"), "{}", text(&o));
41
    let saved = c.join(".config/spectra/earth-instance.identity");
42
    assert!(saved.exists(), "the instance's public identity is saved to delegate to");
43
    assert!(!c.join(".config/spectra/client.toml").exists(), "nothing trusted yet");
44
45
    // The owner delegates (which gives their identity a new KeyId) ...
46
    let instance = KeyPair::instance(&s.join("data"), "earth").unwrap();
47
    assert_eq!(oxy_core::identity::Identity::decode(&std::fs::read(&saved).unwrap()).unwrap().hash(), instance.key);
48
    delegate(&c.join("owner"), &instance);
49
    let current = KeyPair::load_home(&c.join("owner")).unwrap();
50
51
    // ... and connects again: trusted, pinned, and claimed as theirs.
52
    let o = run(c, &["connect", &url]);
53
    assert!(o.status.success(), "{}", text(&o));
54
    assert!(text(&o).contains("Claimed"), "{}", text(&o));
55
    let cfg = std::fs::read_to_string(c.join(".config/spectra/client.toml")).unwrap();
56
    assert!(cfg.contains(&instance.key.to_hex()) && cfg.contains("tls_spki"), "{cfg}");
57
    assert_eq!(instance_owner(port).await, current.key.to_hex(), "recorded at the owner's current identity");
58
59
    // Again: nothing to claim, still trusted.
60
    let o = run(c, &["connect", &url]);
61
    assert!(o.status.success(), "{}", text(&o));
62
    assert!(text(&o).contains("Connected"), "{}", text(&o));
63
    srv.stop();
64
65
    // A new certificate (the old one lost) is refused on the old pin ...
66
    std::fs::remove_file(s.join("data/keys/tls.crt")).unwrap();
67
    std::fs::remove_file(s.join("data/keys/tls.key")).unwrap();
68
    let (srv, port) = Serving::start_tls(s);
69
    let url = format!("https://127.0.0.1:{port}");
70
    let cfg = std::fs::read_to_string(c.join(".config/spectra/client.toml")).unwrap();
71
    let old_pin: [u8; 32] =
72
        hex::decode(cfg.lines().find(|l| l.starts_with("tls_spki")).unwrap().split('"').nth(1).unwrap()).unwrap().try_into().unwrap();
73
    let pinned = Client::at(Endpoint::tls(&url, old_pin).unwrap(), None);
74
    assert!(pinned.send("GET", "/api/v1/instance", &[], vec![]).await.is_err());
75
    // ... until connect checks that the same instance key vouches for it.
76
    let o = run(c, &["connect", &url]);
77
    assert!(o.status.success(), "{}", text(&o));
78
    let cfg2 = std::fs::read_to_string(c.join(".config/spectra/client.toml")).unwrap();
79
    assert_ne!(cfg, cfg2, "re-pinned");
80
    srv.stop();
81
82
    let o = hermetic(s).args(["admin", "verify"]).output().unwrap();
83
    assert!(o.status.success(), "{}", text(&o));
84
}
85
86
#[test]
87
fn someone_elses_server_is_refused() {
88
    let (sd, cd, md) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
89
    let (s, c, m) = (sd.path(), cd.path(), md.path());
90
    KeyPair::create_home(&c.join("owner"), "Andy McCall", "").unwrap();
91
    KeyPair::create_home(&m.join("owner"), "Mallory", "").unwrap();
92
    let (srv, port) = server_for(s, &m.join("owner"));
93
    let o = run(c, &["connect", &format!("https://127.0.0.1:{port}")]);
94
    assert!(!o.status.success());
95
    assert!(text(&o).contains("was set up for"), "{}", text(&o));
96
    srv.stop();
97
}