1use 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct Key {
13 pub id: String,
15 pub public: [u8; 32],
18 pub valid_from: String,
20 pub valid_to: Option<String>,
22}
23
24impl Key {
25 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
33pub struct KeyDoc {
34 pub host: String,
36 pub keys: Vec<Key>,
38}
39
40impl KeyDoc {
41 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 pub fn key(&self, id: &str) -> Option<&Key> {
49 self.keys.iter().find(|k| k.id == id)
50 }
51
52 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
115pub 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#[derive(Clone, Debug, Default)]
135pub struct Keys {
136 hosts: BTreeMap<String, core::result::Result<KeyDoc, Error>>,
137}
138
139impl Keys {
140 pub fn new() -> Keys {
142 Keys::default()
143 }
144
145 pub fn insert(&mut self, doc: KeyDoc) {
147 self.hosts.insert(doc.host.clone(), Ok(doc));
148 }
149
150 pub fn insert_result(&mut self, host: &str, result: Result<KeyDoc>) {
153 self.hosts.insert(host.into(), result);
154 }
155
156 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 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 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}