Skip to main content

datumesh_core/
keys.rs

1//! Key documents (9.2), key resolution results (9.3) and the key set `verify` takes (9.4).
2
3use std::collections::BTreeMap;
4
5use crate::error::{err, index, member, Error, ErrorCode, Result};
6use crate::json::{self, Value};
7use crate::lexical::{self, limits};
8use crate::schema;
9
10/// One published key.
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct Key {
13    /// The key id: first 16 hex digits of SHA-256 of `public`.
14    pub id: String,
15    /// The raw Ed25519 public key. Not checked to be a valid point (9.2); signatures made
16    /// with an invalid one fail in step 6.
17    pub public: [u8; 32],
18    /// Start of the validity window (inclusive).
19    pub valid_from: String,
20    /// End of the validity window (exclusive); `None` is open-ended.
21    pub valid_to: Option<String>,
22}
23
24impl Key {
25    /// Whether the key may verify a receipt issued at `issued` (9.5).
26    pub fn usable_at(&self, issued: &str) -> bool {
27        self.valid_from.as_str() <= issued && self.valid_to.as_deref().is_none_or(|to| issued < to)
28    }
29}
30
31/// A host's key document (9.2), checked.
32#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub struct KeyDoc {
34    /// The host the document was fetched from.
35    pub host: String,
36    /// The keys, in document order.
37    pub keys: Vec<Key>,
38}
39
40impl KeyDoc {
41    /// Parses and checks a key document fetched from `host`. Every failure is `E_KEY_DOC`.
42    pub fn from_json(host: &str, body: &[u8]) -> Result<KeyDoc> {
43        let v = json::parse(body).map_err(|e| e.recode(ErrorCode::KeyDoc))?;
44        key_doc(host, &v)
45    }
46
47    /// The key with this id.
48    pub fn key(&self, id: &str) -> Option<&Key> {
49        self.keys.iter().find(|k| k.id == id)
50    }
51
52    /// The key ids, in document order.
53    pub fn key_ids(&self) -> Vec<&str> {
54        self.keys.iter().map(|k| k.id.as_str()).collect()
55    }
56}
57
58fn key_doc(host: &str, v: &Value) -> Result<KeyDoc> {
59    let kd = ErrorCode::KeyDoc;
60    let m = schema::object_or(v, "$", kd, "a key document")?;
61    schema::exact(m, &["host", "keys", "proto"], &[], "$", kd)?;
62    let doc_host = schema::string_or(m, "host", "$", kd)?;
63    let proto = schema::string_or(m, "proto", "$", kd)?;
64    if proto != "datumesh/1" {
65        return err(kd, "$.proto", "proto must be datumesh/1");
66    }
67    if lexical::check_dns_name(host, "$").is_err() || doc_host != host {
68        return err(kd, "$.host", format!("the document names {doc_host:?}, fetched from {host:?}"));
69    }
70    let items = match schema::get(m, "keys") {
71        Value::Array(a) if !a.is_empty() && a.len() <= limits::KEYS => a,
72        _ => return err(kd, "$.keys", "keys must be an array of 1 to 64 keys"),
73    };
74    let mut keys: Vec<Key> = Vec::with_capacity(items.len());
75    for (i, item) in items.iter().enumerate() {
76        let p = index("$.keys", i);
77        let km = schema::object_or(item, &p, kd, "a key")?;
78        schema::exact(km, &["alg", "id", "public", "valid_from"], &["valid_to"], &p, kd)?;
79        let alg = schema::string_or(km, "alg", &p, kd)?;
80        let id = schema::string_or(km, "id", &p, kd)?;
81        let public = schema::string_or(km, "public", &p, kd)?;
82        let from = schema::string_or(km, "valid_from", &p, kd)?;
83        let to = match schema::opt(km, "valid_to") {
84            None => None,
85            Some(_) => Some(schema::string_or(km, "valid_to", &p, kd)?),
86        };
87        if alg != "ed25519" {
88            return err(kd, &member(&p, "alg"), "alg must be ed25519");
89        }
90        let Some(raw) = lexical::decode_hex_lower::<32>(public) else {
91            return err(kd, &member(&p, "public"), "public must be 64 lowercase hex digits");
92        };
93        if lexical::check_hex(id, 16, "").is_err() || crate::crypto::key_id(&raw) != id {
94            return err(kd, &member(&p, "id"), "id is not the key id of public");
95        }
96        if !lexical::is_instant(from) {
97            return err(kd, &member(&p, "valid_from"), "valid_from must be an instant");
98        }
99        if let Some(t) = to {
100            if !lexical::is_instant(t) {
101                return err(kd, &member(&p, "valid_to"), "valid_to must be an instant");
102            }
103            if t <= from {
104                return err(kd, &member(&p, "valid_to"), "valid_to must be after valid_from");
105            }
106        }
107        if keys.iter().any(|k| k.id == id) {
108            return err(kd, &p, format!("key {id} appears twice"));
109        }
110        keys.push(Key { id: id.into(), public: raw, valid_from: from.into(), valid_to: to.map(String::from) });
111    }
112    Ok(KeyDoc { host: host.into(), keys })
113}
114
115/// Resolves a host's keys from the HTTPS response to `GET https://<host>/.well-known/datumesh.json`
116/// (9.3). `body` is `None` when a 200 response carried no document.
117///
118/// 200 with a valid document gives its keys; 200 with an invalid or missing document is
119/// `E_KEY_DOC`; any other status, a redirect included, is `E_KEY_UNAVAILABLE`. Network, DNS,
120/// TLS and timeout failures are also `E_KEY_UNAVAILABLE`: record them with
121/// [`Keys::insert_unavailable`].
122pub fn keys_from_response(host: &str, status: u16, body: Option<&[u8]>) -> Result<KeyDoc> {
123    if status != 200 {
124        return err(ErrorCode::KeyUnavailable, "$", format!("key document request for {host} returned HTTP {status}"));
125    }
126    match body {
127        Some(b) => KeyDoc::from_json(host, b),
128        None => err(ErrorCode::KeyDoc, "$", "a 200 response without a document"),
129    }
130}
131
132/// The keys a verifier has resolved: per host, a key document or the failure resolving it.
133/// A host that was never resolved behaves as `E_KEY_UNAVAILABLE` (SPEC 11).
134#[derive(Clone, Debug, Default)]
135pub struct Keys {
136    hosts: BTreeMap<String, core::result::Result<KeyDoc, Error>>,
137}
138
139impl Keys {
140    /// An empty key set.
141    pub fn new() -> Keys {
142        Keys::default()
143    }
144
145    /// Adds a checked key document for its host.
146    pub fn insert(&mut self, doc: KeyDoc) {
147        self.hosts.insert(doc.host.clone(), Ok(doc));
148    }
149
150    /// Records the outcome of resolving a host: a document, or the error (`E_KEY_DOC`,
151    /// `E_KEY_UNAVAILABLE`) that resolving it gave.
152    pub fn insert_result(&mut self, host: &str, result: Result<KeyDoc>) {
153        self.hosts.insert(host.into(), result);
154    }
155
156    /// Records the response to a key document request (see [`keys_from_response`]).
157    pub fn insert_response(&mut self, host: &str, status: u16, body: Option<&[u8]>) {
158        self.insert_result(host, keys_from_response(host, status, body));
159    }
160
161    /// Records that a host's keys could not be retrieved (network, DNS, TLS, timeout).
162    pub fn insert_unavailable(&mut self, host: &str) {
163        let e = Error::new(ErrorCode::KeyUnavailable, "$", format!("key document for {host} could not be retrieved"));
164        self.hosts.insert(host.into(), Err(e));
165    }
166
167    /// The key document for `host`, or why there is none.
168    pub fn get(&self, host: &str) -> Result<&KeyDoc> {
169        match self.hosts.get(host) {
170            Some(Ok(doc)) => Ok(doc),
171            Some(Err(e)) => Err(e.clone()),
172            None => err(ErrorCode::KeyUnavailable, "$", format!("no key document for {host}")),
173        }
174    }
175}