Skip to main content

datumesh_core/
keys.rs

1//! Key documents (9.2), their declarations (`spec-0.4.3`), key resolution results (9.3) and the
2//! key set `verify` takes (9.4).
3
4use std::collections::BTreeMap;
5
6use crate::error::{err, index, member, Error, ErrorCode, Result};
7use crate::json::{self, Value};
8use crate::lexical::{self, limits};
9use crate::licence::{declarations_member, Declarations};
10use crate::schema;
11
12/// One published key.
13#[derive(Clone, Debug, PartialEq, Eq, Hash)]
14pub struct Key {
15    /// The key id: first 16 hex digits of SHA-256 of `public`.
16    pub id: String,
17    /// The raw Ed25519 public key. Not checked to be a valid point (9.2); signatures made
18    /// with an invalid one fail in step 6.
19    pub public: [u8; 32],
20    /// Start of the validity window (inclusive).
21    pub valid_from: String,
22    /// End of the validity window (exclusive); `None` is open-ended.
23    pub valid_to: Option<String>,
24}
25
26impl Key {
27    /// Whether the key may verify a receipt issued at `issued` (9.5).
28    pub fn usable_at(&self, issued: &str) -> bool {
29        self.valid_from.as_str() <= issued && self.valid_to.as_deref().is_none_or(|to| issued < to)
30    }
31}
32
33/// A host's key document (9.2), checked.
34#[derive(Clone, Debug, PartialEq, Eq, Hash)]
35pub struct KeyDoc {
36    /// The host the document was fetched from.
37    pub host: String,
38    /// The keys, in document order.
39    pub keys: Vec<Key>,
40}
41
42impl KeyDoc {
43    /// Parses and checks a key document fetched from `host`. Every failure is `E_KEY_DOC`.
44    /// Since 0.3.7 (`spec-0.4.3`) the document may carry `declarations`, which are checked like
45    /// the rest and dropped here: [`KeyDocument::from_json`] keeps them.
46    pub fn from_json(host: &str, body: &[u8]) -> Result<KeyDoc> {
47        KeyDocument::from_json(host, body).map(|d| d.keys)
48    }
49
50    /// The key with this id.
51    pub fn key(&self, id: &str) -> Option<&Key> {
52        self.keys.iter().find(|k| k.id == id)
53    }
54
55    /// The key ids, in document order.
56    pub fn key_ids(&self) -> Vec<&str> {
57        self.keys.iter().map(|k| k.id.as_str()).collect()
58    }
59}
60
61/// A host's key document (9.2) with its `declarations` (`spec-0.4.3`), checked: the keys as a
62/// [`KeyDoc`], and the declarations the publisher made for its own functions, which licence
63/// inheritance (8.5, [`crate::inherit_licence`]) and the entitlement check (10.3,
64/// [`crate::check_entitlement`]) read. Verification (10.1) reads the keys only: a document with
65/// declarations verifies every receipt exactly as the same document without them.
66#[derive(Clone, Debug, PartialEq, Eq, Hash)]
67#[non_exhaustive]
68pub struct KeyDocument {
69    /// The host and its keys.
70    pub keys: KeyDoc,
71    /// The declarations; `None` when the document has none (an empty `declarations` is not a
72    /// valid document, so `Some` is never empty).
73    pub declarations: Option<Declarations>,
74}
75
76impl KeyDocument {
77    /// Parses and checks a key document fetched from `host`, `declarations` included (9.2).
78    /// Every failure is `E_KEY_DOC`: anything malformed in the declarations rejects the whole
79    /// document, as a malformed key does.
80    pub fn from_json(host: &str, body: &[u8]) -> Result<KeyDocument> {
81        let v = json::parse(body).map_err(|e| e.recode(ErrorCode::KeyDoc))?;
82        key_doc(host, &v)
83    }
84
85    /// A key document from its parts, for a publisher writing its own: `declarations`, when
86    /// given, must be non-empty and name only functions under `keys.host` (`E_KEY_DOC`).
87    pub fn new(keys: KeyDoc, declarations: Option<Declarations>) -> Result<KeyDocument> {
88        if let Some(d) = &declarations {
89            d.check_host(&keys.host, "$.declarations")?;
90        }
91        Ok(KeyDocument { keys, declarations })
92    }
93
94    /// The host the document was fetched from.
95    pub fn host(&self) -> &str {
96        &self.keys.host
97    }
98}
99
100impl From<KeyDoc> for KeyDocument {
101    fn from(keys: KeyDoc) -> KeyDocument {
102        KeyDocument { keys, declarations: None }
103    }
104}
105
106pub(crate) fn key_doc(host: &str, v: &Value) -> Result<KeyDocument> {
107    let kd = ErrorCode::KeyDoc;
108    let m = schema::object_or(v, "$", kd, "a key document")?;
109    schema::exact(m, &["host", "keys", "proto"], &["declarations"], "$", kd)?;
110    let doc_host = schema::string_or(m, "host", "$", kd)?;
111    let proto = schema::string_or(m, "proto", "$", kd)?;
112    if proto != "datumesh/1" {
113        return err(kd, "$.proto", "proto must be datumesh/1");
114    }
115    if lexical::check_dns_name(host, "$").is_err() || doc_host != host {
116        return err(kd, "$.host", format!("the document names {doc_host:?}, fetched from {host:?}"));
117    }
118    let items = match schema::get(m, "keys") {
119        Value::Array(a) if !a.is_empty() && a.len() <= limits::KEYS => a,
120        _ => return err(kd, "$.keys", "keys must be an array of 1 to 64 keys"),
121    };
122    let mut keys: Vec<Key> = Vec::with_capacity(items.len());
123    for (i, item) in items.iter().enumerate() {
124        let p = index("$.keys", i);
125        let km = schema::object_or(item, &p, kd, "a key")?;
126        schema::exact(km, &["alg", "id", "public", "valid_from"], &["valid_to"], &p, kd)?;
127        let alg = schema::string_or(km, "alg", &p, kd)?;
128        let id = schema::string_or(km, "id", &p, kd)?;
129        let public = schema::string_or(km, "public", &p, kd)?;
130        let from = schema::string_or(km, "valid_from", &p, kd)?;
131        let to = match schema::opt(km, "valid_to") {
132            None => None,
133            Some(_) => Some(schema::string_or(km, "valid_to", &p, kd)?),
134        };
135        if alg != "ed25519" {
136            return err(kd, &member(&p, "alg"), "alg must be ed25519");
137        }
138        let Some(raw) = lexical::decode_hex_lower::<32>(public) else {
139            return err(kd, &member(&p, "public"), "public must be 64 lowercase hex digits");
140        };
141        if lexical::check_hex(id, 16, "").is_err() || crate::crypto::key_id(&raw) != id {
142            return err(kd, &member(&p, "id"), "id is not the key id of public");
143        }
144        if !lexical::is_instant(from) {
145            return err(kd, &member(&p, "valid_from"), "valid_from must be an instant");
146        }
147        if let Some(t) = to {
148            if !lexical::is_instant(t) {
149                return err(kd, &member(&p, "valid_to"), "valid_to must be an instant");
150            }
151            if t <= from {
152                return err(kd, &member(&p, "valid_to"), "valid_to must be after valid_from");
153            }
154        }
155        if keys.iter().any(|k| k.id == id) {
156            return err(kd, &p, format!("key {id} appears twice"));
157        }
158        keys.push(Key { id: id.into(), public: raw, valid_from: from.into(), valid_to: to.map(String::from) });
159    }
160    // spec-0.4.3: declarations, after the keys.
161    let declarations = match schema::opt(m, "declarations") {
162        None => None,
163        Some(d) => Some(declarations_member(d, host)?),
164    };
165    Ok(KeyDocument { keys: KeyDoc { host: host.into(), keys }, declarations })
166}
167
168/// Resolves a host's keys from the HTTPS response to `GET https://<host>/.well-known/datumesh.json`
169/// (9.3). `body` is `None` when a 200 response carried no document.
170///
171/// 200 with a valid document gives its keys; 200 with an invalid or missing document is
172/// `E_KEY_DOC`; any other status, a redirect included, is `E_KEY_UNAVAILABLE`. Network, DNS,
173/// TLS and timeout failures are also `E_KEY_UNAVAILABLE`: record them with
174/// [`Keys::insert_unavailable`].
175pub fn keys_from_response(host: &str, status: u16, body: Option<&[u8]>) -> Result<KeyDoc> {
176    key_document_from_response(host, status, body).map(|d| d.keys)
177}
178
179/// [`keys_from_response`], keeping the document's `declarations` (`spec-0.4.3`): the
180/// [`KeyDocument`] of a 200 response with a valid document, `E_KEY_DOC` for a 200 with an invalid
181/// or missing one, `E_KEY_UNAVAILABLE` for any other status.
182pub fn key_document_from_response(host: &str, status: u16, body: Option<&[u8]>) -> Result<KeyDocument> {
183    if status != 200 {
184        return err(ErrorCode::KeyUnavailable, "$", format!("key document request for {host} returned HTTP {status}"));
185    }
186    match body {
187        Some(b) => KeyDocument::from_json(host, b),
188        None => err(ErrorCode::KeyDoc, "$", "a 200 response without a document"),
189    }
190}
191
192/// The keys a verifier has resolved: per host, a key document (with its declarations, since
193/// 0.3.7) or the failure resolving it. A host that was never resolved behaves as
194/// `E_KEY_UNAVAILABLE` (SPEC 11).
195#[derive(Clone, Debug, Default)]
196pub struct Keys {
197    hosts: BTreeMap<String, core::result::Result<KeyDocument, Error>>,
198}
199
200impl Keys {
201    /// An empty key set.
202    pub fn new() -> Keys {
203        Keys::default()
204    }
205
206    /// Adds a checked key document for its host (with no declarations).
207    pub fn insert(&mut self, doc: KeyDoc) {
208        self.hosts.insert(doc.host.clone(), Ok(doc.into()));
209    }
210
211    /// Adds a checked key document, with its declarations, for its host (`spec-0.4.3`).
212    pub fn insert_document(&mut self, doc: KeyDocument) {
213        self.hosts.insert(doc.keys.host.clone(), Ok(doc));
214    }
215
216    /// Records the outcome of resolving a host: a document, or the error (`E_KEY_DOC`,
217    /// `E_KEY_UNAVAILABLE`) that resolving it gave.
218    pub fn insert_result(&mut self, host: &str, result: Result<KeyDoc>) {
219        self.hosts.insert(host.into(), result.map(KeyDocument::from));
220    }
221
222    /// Records the response to a key document request (see [`key_document_from_response`]):
223    /// the document's declarations are kept ([`Keys::get_document`]).
224    pub fn insert_response(&mut self, host: &str, status: u16, body: Option<&[u8]>) {
225        self.hosts.insert(host.into(), key_document_from_response(host, status, body));
226    }
227
228    /// Records that a host's keys could not be retrieved (network, DNS, TLS, timeout).
229    pub fn insert_unavailable(&mut self, host: &str) {
230        let e = Error::new(ErrorCode::KeyUnavailable, "$", format!("key document for {host} could not be retrieved"));
231        self.hosts.insert(host.into(), Err(e));
232    }
233
234    /// The key document for `host`, or why there is none.
235    pub fn get(&self, host: &str) -> Result<&KeyDoc> {
236        self.get_document(host).map(|d| &d.keys)
237    }
238
239    /// The key document for `host` with its declarations (`spec-0.4.3`), or why there is none:
240    /// the K of the entitlement check ([`crate::check_entitlement`]).
241    pub fn get_document(&self, host: &str) -> Result<&KeyDocument> {
242        match self.hosts.get(host) {
243            Some(Ok(doc)) => Ok(doc),
244            Some(Err(e)) => Err(e.clone()),
245            None => err(ErrorCode::KeyUnavailable, "$", format!("no key document for {host}")),
246        }
247    }
248}