//! The instance's word for its TLS certificate (spec ยง8.6): the //! `instance.tls` event the daemon signs with the instance key, and the //! check a client makes of it. use anyhow::{bail, Context, Result}; use serde_json::{json, Value as Json}; use spectra_core::event::Event; use spectra_core::identity::Identity; use spectra_core::keys::KeyPair; use spectra_core::ledger::Ledger; use spectra_core::proof::Proof; /// The latest `instance.tls` event, if any. pub async fn latest_tls(ledger: &Ledger) -> Result> { let mut last = None; let mut from = 1; loop { let rows = ledger.events_of_kinds(from, &["instance.tls"], 1000).await?; match rows.last() { Some(e) => { from = e.seq + 1; last = Some(e.clone()); } None => return Ok(last), } } } /// Record the certificate's pin, signed by the instance key, unless the /// latest record already names it. pub async fn record_tls(ledger: &Ledger, instance: &KeyPair, spki: &[u8; 32], names: &[String]) -> Result { let hex_spki = hex::encode(spki); if let Some(e) = latest_tls(ledger).await? { if e.json().get("spki_sha256").and_then(Json::as_str) == Some(hex_spki.as_str()) { return Ok(false); } } let payload = json!({ "spki_sha256": hex_spki, "names": names }); ledger.append_one(spectra_core::principals::signed_event(instance, "system", "instance.tls", payload)).await?; Ok(true) } /// What `GET /api/v1/instance` told a client, checked. #[derive(Clone, Debug)] pub struct Checked { pub name: String, pub instance: Identity, pub owner: Option, /// The certificate the instance key vouches for. pub spki_sha256: [u8; 32], } /// Check `/api/v1/instance`'s answer: the identity is whole, and the /// instance key signed an `instance.tls` naming the certificate `seen`. pub fn check(answer: &Json, seen: &[u8; 32]) -> Result { let ip = answer.get("instance").context("no instance in the answer")?; let bytes = ip.get("identity").and_then(Json::as_str).and_then(|h| hex::decode(h).ok()).context("no instance identity")?; let instance = Identity::decode(&bytes).map_err(|e| anyhow::anyhow!("malformed instance identity: {e}"))?; let tls = answer.get("tls").filter(|t| !t.is_null()).context("the server has not signed a TLS certificate")?; let payload = tls.get("payload").context("no TLS payload")?; let proof = tls.get("proof").and_then(Json::as_str).and_then(|h| hex::decode(h).ok()).context("no TLS proof")?; let proof = Proof::decode(&proof).map_err(|e| anyhow::anyhow!(e))?; if proof.key() != instance.hash().0 || proof.method != "cli" { bail!("the certificate is not vouched for by the instance key"); } proof .verify(&instance, Some(&spectra_core::event::canonical_json(payload))) .map_err(|e| anyhow::anyhow!("the instance's signature on its certificate: {e}"))?; let named = payload.get("spki_sha256").and_then(Json::as_str).context("no pin in the TLS record")?; if named != hex::encode(seen) { bail!("the server presented a certificate its instance key did not sign"); } Ok(Checked { name: answer.get("name").and_then(Json::as_str).unwrap_or("").to_string(), instance, owner: answer.get("owner").and_then(Json::as_str).map(String::from), spki_sha256: *seen, }) }