credibil_dwn/interfaces/
protocols.rs

1//! # Protocols Configure
2//!
3//! The protocols configure endpoint handles `ProtocolsConfigure` messages —
4//! requests to write to [`Configure`] records to the DWN's [`MessageStore`].
5
6use std::collections::BTreeMap;
7#[cfg(feature = "server")]
8use std::collections::HashMap;
9
10use credibil_infosec::jose::jwk::PublicKeyJwk;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use super::{Cursor, Descriptor};
15use crate::authorization::Authorization;
16use crate::hd_key::{self, DerivationPath, DerivationScheme, DerivedPrivateJwk, PrivateKeyJwk};
17use crate::utils::cid;
18use crate::{Result, bad};
19
20/// Default protocol for managing web node permission grants.
21pub const PROTOCOL_URI: &str = "https://credibil.website/dwn/permissions";
22
23/// The protocol path of the `request` record.
24pub const REQUEST_PATH: &str = "request";
25
26/// The protocol path of the `grant` record.
27pub const GRANT_PATH: &str = "grant";
28
29///The protocol path of the `revocation` record.
30pub const REVOCATION_PATH: &str = "grant/revocation";
31
32/// The [`Configure`] message expected by the handler.
33#[derive(Clone, Debug, Default, Deserialize, Serialize)]
34pub struct Configure {
35    /// The Configure descriptor.
36    pub descriptor: ConfigureDescriptor,
37
38    /// The message authorization.
39    pub authorization: Authorization,
40
41    /// Flattened fields as key/value pairs to use for indexing stored records.
42    #[serde(skip)]
43    #[cfg(feature = "server")]
44    pub(crate) indexes: HashMap<String, String>,
45}
46
47impl Configure {
48    /// Compute the content identifier (CID) for the `Configure` message.
49    ///
50    /// # Errors
51    ///
52    /// This method will fail if the message cannot be serialized to CBOR.
53    pub fn cid(&self) -> Result<String> {
54        cid::from_value(self)
55    }
56}
57
58/// The [`Configure`] message descriptor.
59#[derive(Clone, Debug, Default, Deserialize, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct ConfigureDescriptor {
62    /// The base descriptor
63    #[serde(flatten)]
64    pub base: Descriptor,
65
66    /// The protocol definition.
67    pub definition: Definition,
68}
69
70/// Protocol definition.
71#[derive(Clone, Debug, Default, Deserialize, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct Definition {
74    /// Protocol URI.
75    pub protocol: String,
76
77    /// Specifies whether the `Definition` can be returned by unauthorized
78    /// `ProtocolsQuery`.
79    pub published: bool,
80
81    /// Protocol types.
82    pub types: BTreeMap<String, ProtocolType>,
83
84    /// Protocol rules.
85    pub structure: BTreeMap<String, RuleSet>,
86}
87
88impl Definition {
89    /// Returns a new [`Definition`]
90    #[must_use]
91    pub fn new(protocol: impl Into<String>) -> Self {
92        Self {
93            protocol: protocol.into(),
94            ..Self::default()
95        }
96    }
97
98    /// Whether the definition should be published.
99    #[must_use]
100    pub const fn published(mut self, published: bool) -> Self {
101        self.published = published;
102        self
103    }
104
105    /// Add a protocol type.
106    #[must_use]
107    pub fn add_type(mut self, name: impl Into<String>, type_: ProtocolType) -> Self {
108        self.types.insert(name.into(), type_);
109        self
110    }
111
112    /// Add a rule.
113    #[must_use]
114    pub fn add_rule(mut self, name: impl Into<String>, rule_set: RuleSet) -> Self {
115        self.structure.insert(name.into(), rule_set);
116        self
117    }
118
119    /// Derives public encryption key and adds it to the `$encryption` property
120    /// for each protocol path segment.
121    ///
122    /// # Errors
123    ///
124    /// This method will fail when an error occurs deriving the public key.
125    pub fn with_encryption(
126        mut self, root_key_id: &str, private_key_jwk: PrivateKeyJwk,
127    ) -> Result<Self> {
128        let root_key = DerivedPrivateJwk {
129            root_key_id: root_key_id.to_string(),
130            derivation_scheme: DerivationScheme::ProtocolPath,
131            derivation_path: None,
132            derived_private_key: private_key_jwk,
133        };
134
135        // create protocol-derived jwk
136        let path = vec![DerivationScheme::ProtocolPath.to_string(), self.protocol.clone()];
137        let derived_jwk = hd_key::derive_jwk(root_key, &DerivationPath::Relative(&path))?;
138
139        // recursively add `encryption` property to each rule set
140        add_encryption(&mut self.structure, &derived_jwk)?;
141
142        Ok(self)
143    }
144}
145
146fn add_encryption(
147    structure: &mut BTreeMap<String, RuleSet>, parent_key: &DerivedPrivateJwk,
148) -> Result<()> {
149    for (key, rule_set) in structure {
150        let derived_jwk =
151            hd_key::derive_jwk(parent_key.clone(), &DerivationPath::Relative(&[key.clone()]))?;
152        let public_key_jwk = derived_jwk.derived_private_key.public_key.clone();
153        rule_set.encryption = Some(PathEncryption {
154            root_key_id: parent_key.root_key_id.clone(),
155            public_key_jwk,
156        });
157
158        // recurse into nested rules sets
159        add_encryption(&mut rule_set.structure, &derived_jwk)?;
160    }
161    Ok(())
162}
163
164/// Protocol type
165#[derive(Clone, Debug, Default, Deserialize, Serialize)]
166#[serde(rename_all = "camelCase")]
167pub struct ProtocolType {
168    /// The protocol schema.
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub schema: Option<String>,
171
172    /// Data formats supported by the protocol.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub data_formats: Option<Vec<String>>,
175}
176
177/// Protocol rule set.
178#[derive(Clone, Debug, Default, Deserialize, Serialize)]
179pub struct RuleSet {
180    /// Encryption setting for objects that are in this protocol path.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    #[serde(rename = "$encryption")]
183    pub encryption: Option<PathEncryption>,
184
185    /// The protocol action rules.
186    #[serde(skip_serializing_if = "Option::is_none")]
187    #[serde(rename = "$actions")]
188    pub actions: Option<Vec<ActionRule>>,
189
190    /// Storable is a role record.
191    #[serde(skip_serializing_if = "Option::is_none")]
192    #[serde(rename = "$role")]
193    pub role: Option<bool>,
194
195    /// If $size is set, the record size in bytes must be within the limits.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    #[serde(rename = "$size")]
198    pub size: Option<Size>,
199
200    /// Tags for this protocol path.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    #[serde(rename = "$tags")]
203    pub tags: Option<Tags>,
204
205    /// JSON Schema verifies that properties other than properties prefixed
206    /// with $ will actually have type `ProtocolRuleSet`
207    #[serde(flatten)]
208    pub structure: BTreeMap<String, RuleSet>,
209}
210
211/// Config for protocol-path encryption scheme.
212#[derive(Clone, Debug, Default, Deserialize, Serialize)]
213#[serde(rename_all = "camelCase")]
214pub struct PathEncryption {
215    /// The ID of the root key that derives the public key at this protocol path for encrypting the symmetric key used for data encryption.
216    pub root_key_id: String,
217
218    /// Public key for encrypting the symmetric key used for data encryption.
219    pub public_key_jwk: PublicKeyJwk,
220}
221
222/// Rules are used to define which actors can access records for a given
223/// protocol path. Rules take three forms, e.g.:
224///
225/// 1. Anyone can create:
226/// ```json
227///   {
228///     who: 'anyone',
229///     can: ['create']
230///   }
231/// ```
232///
233/// 2. Author of `protocol_path` can create; OR Recipient of `protocol_path`
234///    can write:
235/// ```json
236///   {
237///     who: 'recipient'
238///     of: 'requestForQuote',
239///     can: ['create']
240///   }
241/// ```
242///
243/// 3. Role can create:
244/// ```json
245///   {
246///     role: 'friend',
247///     can: ['create']
248///   }
249/// ```
250#[derive(Clone, Debug, Default, Deserialize, Serialize)]
251#[serde(rename_all = "camelCase")]
252pub struct ActionRule {
253    /// If `who` === 'anyone', then `of` must be omitted. Otherwise `of` must be present.
254    /// Mutually exclusive with `role`
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub who: Option<Actor>,
257
258    /// The protocol path of a role record type marked with $role: true.
259    /// Mutually exclusive with `who`
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub role: Option<String>,
262
263    /// Protocol path.
264    /// Must be present if `who` === 'author' or 'recipient'
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub of: Option<String>,
267
268    /// Array of actions that the actor/role can perform.
269    /// N.B. 'query' and 'subscribe' are only supported for `role` rules.
270    pub can: Vec<Action>,
271}
272
273/// Actor types.
274#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
275#[serde(rename_all = "camelCase")]
276pub enum Actor {
277    /// Anyone can perform the action.
278    #[default]
279    Anyone,
280
281    /// Author of the ??.
282    Author,
283
284    /// Recipient of the ??.
285    Recipient,
286}
287
288/// Rule actions.
289#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
290#[serde(rename_all = "camelCase")]
291pub enum Action {
292    /// Create
293    Create,
294
295    /// Delete
296    Delete,
297
298    /// Prune
299    Prune,
300
301    /// Query
302    Query,
303
304    /// Subscribe
305    Subscribe,
306
307    /// Read
308    #[default]
309    Read,
310
311    /// Update
312    Update,
313
314    /// Co-delete
315    #[serde(rename = "co-delete")]
316    CoDelete,
317
318    /// Co-prune
319    #[serde(rename = "co-prune")]
320    CoPrune,
321
322    /// Co-update
323    #[serde(rename = "co-update")]
324    CoUpdate,
325}
326
327/// Data size range.
328#[derive(Clone, Debug, Default, Deserialize, Serialize)]
329pub struct Size {
330    /// The range's minimum value.
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub min: Option<usize>,
333
334    /// The range's maximum value.
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub max: Option<usize>,
337}
338
339/// Protocol tags
340#[derive(Clone, Debug, Default, Deserialize, Serialize)]
341#[serde(rename_all = "camelCase")]
342pub struct Tags {
343    /// Tags required for this protocol path.
344    #[serde(skip_serializing_if = "Option::is_none")]
345    #[serde(rename = "$requiredTags")]
346    pub required: Option<Vec<String>>,
347
348    /// Allow tags other than those explicitly listed.
349    #[serde(skip_serializing_if = "Option::is_none")]
350    #[serde(rename = "$allowUndefinedTags")]
351    pub allow_undefined: Option<bool>,
352
353    /// Tag properties
354    #[serde(flatten)]
355    pub undefined: BTreeMap<String, Value>,
356}
357
358/// [`ConfigureReply`] is returned by the handler in the [`Reply`] `body` field.
359#[derive(Clone, Debug, Default, Deserialize, Serialize)]
360pub struct ConfigureReply {
361    /// The [`Configure`] entry.
362    #[serde(flatten)]
363    pub message: Configure,
364}
365
366/// Access level for query.
367#[derive(PartialEq, Eq, PartialOrd)]
368pub enum Access {
369    /// Query published records only
370    Published,
371
372    /// Query published and unpublished records
373    Unpublished,
374}
375
376/// The [`Query`] message expected by the handler.
377#[derive(Clone, Debug, Default, Deserialize, Serialize)]
378pub struct Query {
379    /// The Query descriptor.
380    pub descriptor: QueryDescriptor,
381
382    /// The message authorization.
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub authorization: Option<Authorization>,
385}
386
387/// The [`Query`] message descriptor.
388#[derive(Clone, Debug, Default, Deserialize, Serialize)]
389#[serde(rename_all = "camelCase")]
390pub struct QueryDescriptor {
391    /// The base descriptor
392    #[serde(flatten)]
393    pub base: Descriptor,
394
395    /// Filter Records for query.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub filter: Option<ProtocolsFilter>,
398}
399
400/// [`QueryReply`] is returned by the handler in the [`Reply`] `body` field.
401#[derive(Clone, Debug, Default, Deserialize, Serialize)]
402pub struct QueryReply {
403    /// [`Configure`] entries matching the query.
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub entries: Option<Vec<Configure>>,
406
407    /// Pagination cursor.
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub cursor: Option<Cursor>,
410}
411
412/// The Protocols filter is used when querying for protocols.
413#[derive(Clone, Debug, Default, Deserialize, Serialize)]
414#[serde(rename_all = "camelCase")]
415pub struct ProtocolsFilter {
416    /// Protocol matching the specified protocol.
417    pub protocol: String,
418}
419
420/// Verify the structure (rule sets) of the protocol definition.
421pub(crate) fn validate_structure(definition: &Definition) -> Result<()> {
422    let keys = definition.types.keys().collect::<Vec<&String>>();
423
424    // parse rule set for roles
425    let roles = role_paths("", &definition.structure, &[])?;
426
427    // validate rule set hierarchy
428    for rule_set in definition.structure.values() {
429        validate_rule_set(rule_set, "", &keys, &roles)?;
430    }
431
432    Ok(())
433}
434
435// Validates a rule set structure, recursively validating nested rule sets.
436fn validate_rule_set(
437    rule_set: &RuleSet, protocol_path: &str, types: &Vec<&String>, roles: &Vec<String>,
438) -> Result<()> {
439    // validate size rule
440    if let Some(size) = &rule_set.size {
441        if size.max.is_some() && size.min > size.max {
442            return Err(bad!("invalid size range"));
443        }
444    }
445
446    // validate tags schemas
447    if let Some(tags) = &rule_set.tags {
448        for tag in tags.undefined.keys() {
449            let schema = serde_json::from_str(tag)?;
450            jsonschema::validator_for(&schema)
451                .map_err(|e| bad!("tag schema validation error: {e}"))?;
452        }
453    }
454
455    // validate action rules
456    let empty = Vec::new();
457    let mut action_iter = rule_set.actions.as_ref().unwrap_or(&empty).iter();
458
459    while let Some(action) = action_iter.next() {
460        // validate action's `role` property, if exists.
461        if let Some(role) = &action.role {
462            // role must contain valid protocol paths to a role record
463            if !roles.contains(role) {
464                return Err(bad!("missing role {role} in action"));
465            }
466
467            // if ANY `can` actions are read-like ('read', 'query', 'subscribe')
468            // then ALL read-like actions must be present
469            let mut read_actions = vec![Action::Read, Action::Query, Action::Subscribe];
470            read_actions.retain(|ra| action.can.contains(ra));
471
472            // intersection of `read_actions` and `can`: it should be empty or 3
473            if !read_actions.is_empty() && read_actions.len() != 3 {
474                return Err(bad!("role {role} is missing read-like actions"));
475            }
476        }
477
478        // when `who` is `anyone`, `of` cannot be set
479        if action.who.as_ref().is_some_and(|w| w == &Actor::Anyone) && action.of.is_some() {
480            return Err(bad!("`of` must not be set when `who` is \"anyone\""));
481        }
482
483        // When `who` is "recipient" and `of` is unset, `can` must only contain
484        // `co-update`, `co-delete`, and `co-prune`.
485        //
486        // Any other action is disallowed because:
487        //   - `read` - recipients are always allowed to read
488        //   - `write` - unset `of` implies the recipient of this record, but there
489        //      is no 'recipient' until the record has been created.
490        //   - `query` - query is authorized using roles, not recipients.
491        if action.who.as_ref().is_some_and(|w| w == &Actor::Recipient) && action.of.is_none() {
492            let allowed = [Action::CoUpdate, Action::CoDelete, Action::CoPrune];
493            if !allowed.iter().any(|ra| action.can.contains(ra)) {
494                return Err(bad!(
495                    "recipient action must contain only co-update, co-delete, and co-prune",
496                ));
497            }
498        }
499
500        // when `who` is set to "author" then `of` must be set
501        if action.who.as_ref().is_some_and(|w| w == &Actor::Author) && action.of.is_none() {
502            return Err(bad!("`of` must be set when `who` is set to 'author'"));
503        }
504
505        // when `can` contains `update` or `delete`, it must also contain `create`
506        if action.can.contains(&Action::Update) && !action.can.contains(&Action::Create) {
507            return Err(bad!("action rule {action:?} contains 'update' but no 'create'"));
508        }
509        if action.can.contains(&Action::Delete) && !action.can.contains(&Action::Create) {
510            return Err(bad!("action rule {action:?} contains 'delete' but no 'create'"));
511        }
512
513        // ensure no duplicate actors or roles in the remaining action rules
514        // ie. no two action rules can have the same combination of `who` + `of` or `role`.
515        for other in action_iter.clone() {
516            if action.who.is_some() {
517                if action.who == other.who && action.of == other.of {
518                    return Err(bad!("an actor may only have one rule within a rule set"));
519                }
520            } else if action.role == other.role {
521                return Err(bad!(
522                    "more than one action rule per role {:?} not allowed within a rule set: {action:?}",
523                    action.role
524                ));
525            }
526        }
527    }
528
529    // verify nested rule sets
530    for (set_name, rule_set) in &rule_set.structure {
531        if !types.contains(&set_name) {
532            return Err(bad!("rule set {set_name} is not declared as an allowed type"));
533        }
534        let protocol_path = if protocol_path.is_empty() {
535            set_name
536        } else {
537            &format!("{protocol_path}/{set_name}")
538        };
539        validate_rule_set(rule_set, protocol_path, types, roles)?;
540    }
541
542    Ok(())
543}
544
545// Parses the given rule set hierarchy to get all the role protocol paths.
546fn role_paths(
547    protocol_path: &str, structure: &BTreeMap<String, RuleSet>, roles: &[String],
548) -> Result<Vec<String>> {
549    // restrict to max depth of 10 levels
550    if protocol_path.split('/').count() > 10 {
551        return Err(bad!("Storable nesting depth exceeded 10 levels."));
552    }
553
554    let mut roles = roles.to_owned();
555
556    // only check for roles in nested rule sets
557    for (rule_name, rule_set) in structure {
558        let protocol_path = if protocol_path.is_empty() {
559            rule_name
560        } else {
561            &format!("{protocol_path}/{rule_name}")
562        };
563
564        if rule_set.role.is_some() {
565            roles.push(protocol_path.to_string());
566        } else {
567            roles = role_paths(protocol_path, &rule_set.structure, &roles)?;
568        }
569    }
570
571    Ok(roles)
572}