OXYGENHUB
◆ v0.1.0 spectra / crates / spectra-mcp / src / instance.rs
3.4 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
b576d05c Andy McCall ↶ 1
//! The instance's word for its TLS certificate (spec §8.6): the
2
//! `instance.tls` event the daemon signs with the instance key, and the
3
//! check a client makes of it.
4
5
use anyhow::{bail, Context, Result};
6
use serde_json::{json, Value as Json};
7
use spectra_core::event::Event;
98a0fb9f Andy McCall ↶ 8
use spectra_core::identity::Identity;
b576d05c Andy McCall ↶ 9
use spectra_core::keys::KeyPair;
10
use spectra_core::ledger::Ledger;
11
use spectra_core::proof::Proof;
12
13
/// The latest `instance.tls` event, if any.
14
pub async fn latest_tls(ledger: &Ledger) -> Result<Option<Event>> {
15
    let mut last = None;
16
    let mut from = 1;
17
    loop {
18
        let rows = ledger.events_of_kinds(from, &["instance.tls"], 1000).await?;
19
        match rows.last() {
20
            Some(e) => {
21
                from = e.seq + 1;
22
                last = Some(e.clone());
23
            }
24
            None => return Ok(last),
25
        }
26
    }
27
}
28
29
/// Record the certificate's pin, signed by the instance key, unless the
30
/// latest record already names it.
31
pub async fn record_tls(ledger: &Ledger, instance: &KeyPair, spki: &[u8; 32], names: &[String]) -> Result<bool> {
32
    let hex_spki = hex::encode(spki);
33
    if let Some(e) = latest_tls(ledger).await? {
34
        if e.json().get("spki_sha256").and_then(Json::as_str) == Some(hex_spki.as_str()) {
35
            return Ok(false);
36
        }
37
    }
38
    let payload = json!({ "spki_sha256": hex_spki, "names": names });
39
    ledger.append_one(spectra_core::principals::signed_event(instance, "system", "instance.tls", payload)).await?;
40
    Ok(true)
41
}
42
43
/// What `GET /api/v1/instance` told a client, checked.
44
#[derive(Clone, Debug)]
45
pub struct Checked {
46
    pub name: String,
47
    pub instance: Identity,
48
    pub owner: Option<String>,
49
    /// The certificate the instance key vouches for.
50
    pub spki_sha256: [u8; 32],
51
}
52
53
/// Check `/api/v1/instance`'s answer: the identity is whole, and the
54
/// instance key signed an `instance.tls` naming the certificate `seen`.
55
pub fn check(answer: &Json, seen: &[u8; 32]) -> Result<Checked> {
56
    let ip = answer.get("instance").context("no instance in the answer")?;
57
    let bytes = ip.get("identity").and_then(Json::as_str).and_then(|h| hex::decode(h).ok()).context("no instance identity")?;
58
    let instance = Identity::decode(&bytes).map_err(|e| anyhow::anyhow!("malformed instance identity: {e}"))?;
59
    let tls = answer.get("tls").filter(|t| !t.is_null()).context("the server has not signed a TLS certificate")?;
60
    let payload = tls.get("payload").context("no TLS payload")?;
61
    let proof = tls.get("proof").and_then(Json::as_str).and_then(|h| hex::decode(h).ok()).context("no TLS proof")?;
62
    let proof = Proof::decode(&proof).map_err(|e| anyhow::anyhow!(e))?;
63
    if proof.key() != instance.hash().0 || proof.method != "cli" {
64
        bail!("the certificate is not vouched for by the instance key");
65
    }
66
    proof
67
        .verify(&instance, Some(&spectra_core::event::canonical_json(payload)))
68
        .map_err(|e| anyhow::anyhow!("the instance's signature on its certificate: {e}"))?;
69
    let named = payload.get("spki_sha256").and_then(Json::as_str).context("no pin in the TLS record")?;
70
    if named != hex::encode(seen) {
71
        bail!("the server presented a certificate its instance key did not sign");
72
    }
73
    Ok(Checked {
74
        name: answer.get("name").and_then(Json::as_str).unwrap_or("").to_string(),
75
        instance,
76
        owner: answer.get("owner").and_then(Json::as_str).map(String::from),
77
        spki_sha256: *seen,
78
    })
79
}