Skip to main content

iop_morpheus_proto/data/
diddoc.rs

1use super::*;
2
3use crate::data::auth::Authentication;
4use crate::data::{
5    did::Did,
6    validation::{ValidationIssueSeverity as Severity, ValidationResult},
7};
8
9#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
10pub enum Right {
11    #[serde(rename = "update")]
12    Update,
13    #[serde(rename = "impersonate")]
14    Impersonation,
15}
16
17impl Display for Right {
18    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
19        let value =
20            serde_json::to_value(self).expect("Implementation error: Right is not serializable");
21        match value {
22            serde_json::Value::String(s) => write!(f, "{}", s),
23            _ => panic!("Implementation error: unexpected Right serialization"),
24        }
25    }
26}
27
28impl FromStr for Right {
29    type Err = anyhow::Error;
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        Ok(serde_json::from_value(serde_json::Value::String(s.to_owned()))?)
32    }
33}
34
35impl Right {
36    pub fn map_all<T>(f: impl Fn(&Right) -> T) -> HashMap<Right, T> {
37        vec![Self::Update, Self::Impersonation].drain(..).map(|r| (r, f(&r))).collect()
38    }
39}
40
41pub fn is_in_opt_range(
42    height: BlockHeight, from_inc: Option<BlockHeight>, until_exc: Option<BlockHeight>,
43) -> bool {
44    if let Some(from) = from_inc {
45        if height < from {
46            return false;
47        }
48    }
49    if let Some(until) = until_exc {
50        if until <= height {
51            return false;
52        }
53    }
54    true
55}
56
57pub fn is_between(height: BlockHeight, after: BlockHeight, until_exc: BlockHeight) -> bool {
58    is_in_opt_range(height, Some(after + 1), Some(until_exc))
59}
60
61#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
62pub struct KeyState {
63    #[serde(rename = "auth")]
64    pub authentication: Authentication,
65    #[serde(rename = "validFromHeight")]
66    pub valid_from_block: Option<BlockHeight>, // TODO should be timestamp on the long term
67    #[serde(rename = "validUntilHeight")]
68    pub valid_until_block: Option<BlockHeight>, // TODO should be timestamp on the long term
69}
70
71#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72pub struct KeyDataDerived {
73    pub index: usize,
74    pub valid: bool,
75}
76
77#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
78pub struct KeyData {
79    #[serde(flatten)]
80    pub state: KeyState,
81    #[serde(flatten)]
82    pub derived: KeyDataDerived,
83}
84
85impl KeyData {
86    fn from_auth(authentication: Authentication) -> Self {
87        let state = KeyState { authentication, valid_from_block: None, valid_until_block: None };
88        let derived = KeyDataDerived { index: 0, valid: true };
89        Self { state, derived }
90    }
91
92    fn is_valid_at(&self, height: BlockHeight) -> bool {
93        is_in_opt_range(height, self.state.valid_from_block, self.state.valid_until_block)
94    }
95}
96
97#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
98pub struct KeyRightHistoryItem {
99    pub height: Option<BlockHeight>,
100    pub valid: bool,
101}
102
103#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
104pub struct KeyRightState {
105    pub history: Vec<KeyRightHistoryItem>,
106}
107
108#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
109pub struct KeyRightDerived {
110    #[serde(rename = "keyLink")]
111    pub key_link: String, // TODO should be more strictly typed
112    pub valid: bool,
113}
114
115#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
116pub struct KeyRightHistory {
117    #[serde(flatten)]
118    pub state: KeyRightState,
119    #[serde(flatten)]
120    pub derived: KeyRightDerived,
121}
122
123impl KeyRightHistory {
124    fn ensure_valid_history(&self) -> Result<()> {
125        let heights: Vec<_> =
126            self.state.history.iter().map(|item| item.height.unwrap_or_default()).collect();
127        let mut sorted = heights.clone();
128        sorted.sort_unstable(); // equal u32 instances do not differ, so unstable is fine
129        ensure!(heights == sorted, "Height of key history items must be strictly increasing");
130        Ok(())
131    }
132
133    fn is_true_at(&self, height: BlockHeight) -> Result<bool> {
134        // All such checks should be done instead when constructing/parsing the whole DidDocument
135        self.ensure_valid_history()?;
136
137        let last_state_before_height =
138            self.state.history.iter().rev().find(|item| item.height.unwrap_or_default() <= height);
139        let valid = match last_state_before_height {
140            None => false,
141            Some(item) => item.valid,
142        };
143        Ok(valid)
144    }
145}
146
147#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
148pub enum ServiceType {
149    // TODO
150}
151
152#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, PartialOrd, Serialize)]
153pub struct Service {
154    #[serde(rename = "type")]
155    pub type_: ServiceType,
156    pub name: String,
157    pub service_endpoint: String, // TODO should we use multiaddr::Multiaddr here and thus add CID-dependency?
158}
159
160#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
161pub struct DidDocument {
162    #[serde(with = "serde_str")]
163    pub did: Did,
164    pub keys: Vec<KeyData>,
165    #[serde(skip_serializing_if = "HashMap::is_empty", default)]
166    pub rights: HashMap<Right, Vec<KeyRightHistory>>,
167    #[serde(skip_serializing_if = "Vec::is_empty", default)]
168    pub services: Vec<Service>,
169    #[serde(rename = "tombstonedAtHeight")]
170    pub tombstoned_at_height: Option<BlockHeight>,
171    pub tombstoned: bool,
172    #[serde(rename = "queriedAtHeight")]
173    pub queried_at_height: BlockHeight,
174}
175
176impl DidDocument {
177    pub fn implicit(did: &Did) -> Self {
178        let default_key = KeyData::from_auth(Authentication::KeyId(did.default_key_id()));
179        Self {
180            did: did.to_owned(),
181            keys: vec![default_key],
182            rights: Default::default(),
183            services: Default::default(),
184            tombstoned_at_height: Default::default(),
185            tombstoned: Default::default(),
186            queried_at_height: Default::default(),
187        }
188    }
189
190    fn key(&self, key_link: &str) -> Result<KeyData> {
191        ensure!(key_link.starts_with('#'), "Key links for remote DIDs are not supported yet");
192        let idx_str: String = key_link.chars().skip(1).collect();
193        let idx: usize = idx_str.parse()?;
194        let key =
195            self.keys.get(idx).ok_or_else(|| anyhow!("No key found for link {}", key_link))?;
196        Ok(key.to_owned())
197    }
198
199    fn ensure_known_height(&self, height: BlockHeight) -> Result<()> {
200        if self.queried_at_height < height {
201            bail!("Queried future height {}, present is {}", height, self.queried_at_height);
202        }
203        Ok(())
204    }
205
206    pub fn has_right_at(
207        &self, auth: &Authentication, right: Right, height: BlockHeight,
208    ) -> Result<bool> {
209        self.ensure_known_height(height)?;
210
211        if let Some(tombstoned_at_height) = self.tombstoned_at_height {
212            if tombstoned_at_height <= height {
213                return Ok(false);
214            }
215        }
216        let keys_with_right = match self.rights.get(&right) {
217            Some(key) => key,
218            None => return Ok(false),
219        };
220
221        for key_right in keys_with_right.iter() {
222            let key = self.key(&key_right.derived.key_link)?;
223            if !key.is_valid_at(height) {
224                continue;
225            }
226            if key.state.authentication != *auth {
227                continue;
228            }
229
230            return key_right.is_true_at(height);
231        }
232
233        Ok(false)
234    }
235
236    pub fn is_tombstoned_at(&self, height: BlockHeight) -> Result<bool> {
237        self.ensure_known_height(height)?;
238
239        if let Some(tombstone_height) = self.tombstoned_at_height {
240            return Ok(tombstone_height <= height);
241        }
242
243        Ok(false)
244    }
245
246    // TODO reconsider and thoroughly check if until should be inclusive or exclusive and if implementation matches
247    pub fn validate_right(
248        &self, auth: &Authentication, right: Right, from: BlockHeight, until: BlockHeight,
249    ) -> Result<ValidationResult> {
250        ensure!(1 <= from, "Range must not predate genesis block");
251        ensure!(from < until, "Invalid block range {}-{}", from, until);
252        self.ensure_known_height(until)?;
253
254        let mut result: ValidationResult = Default::default();
255
256        if self.is_tombstoned_at(from)? {
257            result.add_issue(Severity::Error, "DID was tombstoned before given period");
258        }
259        if let Some(tombstone_height) = self.tombstoned_at_height {
260            if is_between(tombstone_height, from, until) {
261                result.add_issue(Severity::Warning, "DID was tombstoned during given period");
262            }
263        }
264
265        let keys_with_right = match self.rights.get(&right) {
266            Some(entries) => entries,
267            None => {
268                result
269                    .add_issue(Severity::Error, "Right was never granted to given authentication");
270                return Ok(result);
271            }
272        };
273
274        let key_history_opt = keys_with_right.iter().find_map(|right_entry| {
275            let key_data = match self.key(&right_entry.derived.key_link) {
276                Ok(key_entry) => key_entry,
277                Err(e) => {
278                    // TODO ideally detected earlier during parsing and should never happen here
279                    result.add_issue(Severity::Error, &e.to_string());
280                    return None;
281                }
282            };
283            if key_data.state.authentication != *auth {
284                return None;
285            }
286            Some((key_data, right_entry))
287        });
288
289        let (key_data, key_right) = match key_history_opt {
290            Some(key_history) => key_history,
291            None => {
292                result.add_issue(Severity::Error, "No matching authentication found in DID");
293                return Ok(result);
294            }
295        };
296
297        if let Some(key_valid_from) = key_data.state.valid_from_block {
298            if until < key_valid_from {
299                result.add_issue(Severity::Error, "Key was enabled only after given period");
300            }
301            if is_between(key_valid_from, from, until) {
302                result.add_issue(Severity::Warning, "Key was enabled during given period");
303            }
304        }
305
306        if let Some(key_valid_until) = key_data.state.valid_until_block {
307            if key_valid_until < from {
308                result.add_issue(Severity::Error, "Key expired before given period");
309            }
310            if is_between(key_valid_until, from, until) {
311                result.add_issue(Severity::Warning, "Key expired during given period");
312            }
313        }
314
315        let history = &key_right.state.history;
316        ensure!(! history.is_empty(), "Implementation error: key related to rights were already filtered, right must be present here");
317
318        let mut right_changes_in_range =
319            history.iter().filter(|item| is_between(item.height.unwrap_or_default(), from, until));
320
321        if !key_right.is_true_at(from)? {
322            if right_changes_in_range.next().is_none() {
323                result.add_issue(Severity::Error, "Required right was never granted for key");
324            } else {
325                result.add_issue(Severity::Warning, "Required right changed during given period");
326            }
327        }
328
329        Ok(result)
330    }
331}
332
333#[cfg(test)]
334mod test {
335    use super::*;
336    use crate::data::validation::ValidationStatus;
337
338    #[test]
339    fn pretty_json() -> Result<()> {
340        test_parsed_did_document(
341            r##"{
342            "did": "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr",
343            "keys": [
344              {
345                "index": 0,
346                "auth": "iezbeWGSY2dqcUBqT8K7R14xr",
347                "valid": true
348              },
349              {
350                "index": 1,
351                "auth": "iez25N5WZ1Q6TQpgpyYgiu9gTX",
352                "valid": true,
353                "validFromHeight": 120
354              }
355            ],
356            "rights": {
357              "impersonate": [
358                {
359                  "keyLink": "#0",
360                  "history": [
361                    {
362                      "height": null,
363                      "valid": true
364                    }
365                  ],
366                  "valid": true
367                },
368                {
369                  "keyLink": "#1",
370                  "history": [
371                    {
372                      "height": null,
373                      "valid": false
374                    },
375                    {
376                      "height": 126,
377                      "valid": true
378                    }
379                  ],
380                  "valid": true
381                }
382              ],
383              "update": [
384                {
385                  "keyLink": "#0",
386                  "history": [
387                    {
388                      "height": null,
389                      "valid": true
390                    }
391                  ],
392                  "valid": true
393                },
394                {
395                  "keyLink": "#1",
396                  "history": [
397                    {
398                      "height": null,
399                      "valid": false
400                    }
401                  ],
402                  "valid": false
403                }
404              ]
405            },
406            "tombstonedAtHeight": null,
407            "tombstoned": false,
408            "queriedAtHeight": 126
409          }"##,
410        )
411    }
412
413    #[test]
414    fn terse_json() -> Result<()> {
415        test_parsed_did_document(
416            r##"{"did":"did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr","keys":[{"index":0,"auth":"iezbeWGSY2dqcUBqT8K7R14xr","validFromHeight":null,"validUntilHeight":null,"valid":true},{"index":1,"auth":"iez25N5WZ1Q6TQpgpyYgiu9gTX","validFromHeight":120,"validUntilHeight":null,"valid":true}],"rights":{"impersonate":[{"keyLink":"#0","history":[{"height":null,"valid":true}],"valid":true},{"keyLink":"#1","history":[{"height":null,"valid":false},{"height":126,"valid":true}],"valid":true}],"update":[{"keyLink":"#0","history":[{"height":null,"valid":true}],"valid":true},{"keyLink":"#1","history":[{"height":null,"valid":false}],"valid":false}]},"tombstoned":false,"tombstonedAtHeight":null,"queriedAtHeight":126}"##,
417        )
418    }
419
420    fn test_parsed_did_document(s: &str) -> Result<()> {
421        let doc: DidDocument = serde_json::from_str(s)?;
422
423        assert_eq!(doc.did, "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr".parse()?);
424        assert_eq!(doc.tombstoned_at_height, None);
425        assert_eq!(doc.queried_at_height, 126);
426        assert_eq!(doc.tombstoned, false);
427
428        let first_key = &doc.keys[0].state.authentication;
429        let second_key = &doc.keys[1].state.authentication;
430
431        assert!(doc.has_right_at(first_key, Right::Impersonation, 1)?);
432        assert!(doc.has_right_at(first_key, Right::Impersonation, 2)?);
433        assert!(doc.has_right_at(first_key, Right::Impersonation, 125)?);
434        assert!(doc.has_right_at(first_key, Right::Impersonation, 126)?);
435        assert!(doc.has_right_at(first_key, Right::Impersonation, 127).is_err());
436
437        assert!(!doc.has_right_at(second_key, Right::Impersonation, 1)?);
438        assert!(!doc.has_right_at(second_key, Right::Impersonation, 2)?);
439        assert!(!doc.has_right_at(second_key, Right::Impersonation, 125)?);
440        assert!(doc.has_right_at(second_key, Right::Impersonation, 126)?);
441        assert!(doc.has_right_at(second_key, Right::Impersonation, 127).is_err());
442
443        assert!(doc.has_right_at(first_key, Right::Update, 1)?);
444        assert!(doc.has_right_at(first_key, Right::Update, 2)?);
445        assert!(doc.has_right_at(first_key, Right::Update, 125)?);
446        assert!(doc.has_right_at(first_key, Right::Update, 126)?);
447        assert!(doc.has_right_at(first_key, Right::Update, 127).is_err());
448
449        assert!(!doc.has_right_at(second_key, Right::Update, 1)?);
450        assert!(!doc.has_right_at(second_key, Right::Update, 2)?);
451        assert!(!doc.has_right_at(second_key, Right::Update, 125)?);
452        assert!(!doc.has_right_at(second_key, Right::Update, 126)?);
453        assert!(doc.has_right_at(second_key, Right::Update, 127).is_err());
454
455        Ok(())
456    }
457
458    #[test]
459    #[allow(clippy::cognitive_complexity)]
460    fn has_right_between() -> Result<()> {
461        let did_doc_str = r##"{
462            "did": "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr",
463            "keys": [
464              {
465                "index": 0,
466                "auth": "iezbeWGSY2dqcUBqT8K7R14xr",
467                "valid": true
468              },
469              {
470                "index": 1,
471                "auth": "iez25N5WZ1Q6TQpgpyYgiu9gTX",
472                "valid": true,
473                "validFromHeight": 10,
474                "validUntilHeight": 90
475              }
476            ],
477            "rights": {
478              "impersonate": [
479                {
480                  "keyLink": "#0",
481                  "history": [
482                    { "height": null, "valid": true }
483                  ],
484                  "valid": true
485                },
486                {
487                  "keyLink": "#1",
488                  "history": [
489                    { "height": null, "valid": false },
490                    { "height": 20, "valid": true },
491                    { "height": 80, "valid": false }
492                  ],
493                  "valid": false
494                }
495              ],
496              "update": [
497                {
498                  "keyLink": "#0",
499                  "history": [
500                    { "height": null, "valid": true }
501                  ],
502                  "valid": true
503                },
504                {
505                  "keyLink": "#1",
506                  "history": [
507                    { "height": null, "valid": false },
508                    { "height": 90, "valid": true }
509                  ],
510                  "valid": true
511                }
512              ]
513            },
514            "tombstonedAtHeight": 100,
515            "tombstoned": true,
516            "queriedAtHeight": 200
517          }"##;
518
519        let doc: DidDocument = serde_json::from_str(did_doc_str)?;
520
521        assert_eq!(doc.did, "did:morpheus:ezbeWGSY2dqcUBqT8K7R14xr".parse()?);
522        assert_eq!(doc.tombstoned_at_height, Some(100));
523        assert_eq!(doc.queried_at_height, 200);
524        assert_eq!(doc.tombstoned, true);
525
526        let first_key = &doc.keys[0].state.authentication;
527        let second_key = &doc.keys[1].state.authentication;
528        assert_eq!(*first_key, Authentication::KeyId("iezbeWGSY2dqcUBqT8K7R14xr".parse()?));
529        assert_eq!(*second_key, Authentication::KeyId("iez25N5WZ1Q6TQpgpyYgiu9gTX".parse()?));
530
531        use Right::*;
532        use ValidationStatus::*;
533
534        assert_eq!(doc.validate_right(first_key, Impersonation, 1, 100)?.status(), Valid);
535        assert_eq!(doc.validate_right(first_key, Update, 1, 100)?.status(), Valid);
536        assert_eq!(doc.validate_right(first_key, Impersonation, 10, 90)?.status(), Valid);
537        assert_eq!(doc.validate_right(first_key, Update, 10, 90)?.status(), Valid);
538        assert_eq!(doc.validate_right(first_key, Impersonation, 101, 200)?.status(), Invalid);
539        assert_eq!(doc.validate_right(first_key, Update, 101, 200)?.status(), Invalid);
540        assert_eq!(doc.validate_right(first_key, Impersonation, 1, 200)?.status(), MaybeValid);
541        assert_eq!(doc.validate_right(first_key, Update, 1, 200)?.status(), MaybeValid);
542        assert_eq!(doc.validate_right(first_key, Impersonation, 50, 150)?.status(), MaybeValid);
543        assert_eq!(doc.validate_right(first_key, Update, 50, 150)?.status(), MaybeValid);
544
545        assert_eq!(doc.validate_right(second_key, Impersonation, 20, 80)?.status(), Valid);
546        assert_eq!(doc.validate_right(second_key, Impersonation, 30, 70)?.status(), Valid);
547        assert_eq!(doc.validate_right(second_key, Impersonation, 1, 80)?.status(), MaybeValid);
548        assert_eq!(doc.validate_right(second_key, Impersonation, 20, 200)?.status(), MaybeValid);
549        assert_eq!(doc.validate_right(second_key, Impersonation, 1, 20)?.status(), Invalid);
550        assert_eq!(doc.validate_right(second_key, Impersonation, 80, 200)?.status(), Invalid);
551
552        assert_eq!(doc.validate_right(second_key, Update, 90, 100)?.status(), Valid);
553        assert_eq!(doc.validate_right(second_key, Update, 1, 90)?.status(), Invalid);
554        assert_eq!(doc.validate_right(second_key, Update, 100, 200)?.status(), Invalid);
555        assert_eq!(doc.validate_right(second_key, Update, 80, 110)?.status(), MaybeValid);
556
557        Ok(())
558    }
559}