identity_iota_core/document/
iota_document.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use core::fmt;
5use core::fmt::Debug;
6use core::fmt::Display;
7use identity_credential::credential::Jws;
8use identity_did::CoreDID;
9use identity_did::DIDUrl;
10use identity_document::verifiable::JwsVerificationOptions;
11use identity_verification::jose::jws::DecodedJws;
12use identity_verification::jose::jws::JwsVerifier;
13use product_common::network_name::NetworkName;
14use serde::Deserialize;
15use serde::Serialize;
16
17use identity_core::common::Object;
18use identity_core::common::OneOrSet;
19use identity_core::common::OrderedSet;
20use identity_core::common::Url;
21use identity_core::convert::FmtJson;
22use identity_document::document::CoreDocument;
23use identity_document::service::Service;
24use identity_document::utils::DIDUrlQuery;
25use identity_verification::MethodRelationship;
26use identity_verification::MethodScope;
27use identity_verification::VerificationMethod;
28
29use crate::error::Result;
30use crate::Error;
31use crate::IotaDID;
32use crate::IotaDocumentMetadata;
33use crate::StateMetadataDocument;
34use crate::StateMetadataEncoding;
35
36/// Struct used internally when deserializing [`IotaDocument`].
37#[derive(Debug, Deserialize)]
38struct ProvisionalIotaDocument {
39  #[serde(rename = "doc")]
40  document: CoreDocument,
41  #[serde(rename = "meta")]
42  metadata: IotaDocumentMetadata,
43}
44
45impl TryFrom<ProvisionalIotaDocument> for IotaDocument {
46  type Error = Error;
47  fn try_from(provisional: ProvisionalIotaDocument) -> std::result::Result<Self, Self::Error> {
48    let ProvisionalIotaDocument { document, metadata } = provisional;
49
50    IotaDID::check_validity(document.id()).map_err(|_| {
51      Error::SerializationError(
52        "deserializing iota document failed: id does not conform to the IOTA method specification",
53        None,
54      )
55    })?;
56
57    for controller_id in document
58      .controller()
59      .map(|controller_set| controller_set.iter())
60      .into_iter()
61      .flatten()
62    {
63      IotaDID::check_validity(controller_id).map_err(|_| {
64        Error::SerializationError(
65          "deserializing iota document failed: controller not conforming to the iota method specification detected",
66          None,
67        )
68      })?;
69    }
70
71    Ok(IotaDocument { document, metadata })
72  }
73}
74
75/// A DID Document adhering to the IOTA DID method specification.
76///
77/// This extends [`CoreDocument`].
78#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
79#[serde(try_from = "ProvisionalIotaDocument")]
80pub struct IotaDocument {
81  /// The DID document.
82  #[serde(rename = "doc")]
83  pub(crate) document: CoreDocument,
84  /// The metadata of an IOTA DID document.
85  #[serde(rename = "meta")]
86  pub metadata: IotaDocumentMetadata,
87}
88
89impl IotaDocument {
90  // ===========================================================================
91  // Constructors
92  // ===========================================================================
93
94  /// Constructs an empty DID Document with a [`IotaDID::placeholder`] identifier
95  /// for the given `network`.
96  // TODO: always take Option<NetworkName> or `new_with_options` for a particular network?
97  pub fn new(network: &NetworkName) -> Self {
98    Self::new_with_id(IotaDID::placeholder(network))
99  }
100
101  /// Constructs an empty DID Document with the given identifier.
102  pub fn new_with_id(id: IotaDID) -> Self {
103    // PANIC: constructing an empty DID Document is infallible, caught by tests otherwise.
104    let document: CoreDocument = CoreDocument::builder(Object::default())
105      .id(id.into())
106      .build()
107      .expect("empty IotaDocument constructor failed");
108    let metadata: IotaDocumentMetadata = IotaDocumentMetadata::new();
109    Self { document, metadata }
110  }
111
112  // ===========================================================================
113  // Properties
114  // ===========================================================================
115
116  /// Returns the DID document identifier.
117  pub fn id(&self) -> &IotaDID {
118    // CORRECTNESS: This cast is OK because the public API does not expose methods
119    // enabling unchecked mutation of the `id` field.
120    IotaDID::from_inner_ref_unchecked(self.document.id())
121  }
122
123  /// Returns an iterator yielding the DID controllers.
124  pub fn controller(&self) -> impl Iterator<Item = &IotaDID> + '_ {
125    let core_did_controller_iter = self
126      .document
127      .controller()
128      .map(|controllers| controllers.iter())
129      .into_iter()
130      .flatten();
131
132    // CORRECTNESS: These casts are OK because the public API only allows setting IotaDIDs.
133    core_did_controller_iter.map(IotaDID::from_inner_ref_unchecked)
134  }
135
136  /// Sets the value of the document controller.
137  ///
138  /// Note:
139  /// * Duplicates in `controller` will be ignored.
140  /// * Use an empty collection to clear all controllers.
141  pub fn set_controller<T>(&mut self, controller: T)
142  where
143    T: IntoIterator<Item = IotaDID>,
144  {
145    let controller_core_dids: Option<OneOrSet<CoreDID>> = {
146      let controller_set: OrderedSet<CoreDID> = controller.into_iter().map(CoreDID::from).collect();
147      if controller_set.is_empty() {
148        None
149      } else {
150        Some(OneOrSet::new_set(controller_set).expect("controller is checked to be not empty"))
151      }
152    };
153
154    *self.document.controller_mut() = controller_core_dids;
155  }
156
157  /// Returns a reference to the `alsoKnownAs` set.
158  pub fn also_known_as(&self) -> &OrderedSet<Url> {
159    self.document.also_known_as()
160  }
161
162  /// Returns a mutable reference to the `alsoKnownAs` set.
163  pub fn also_known_as_mut(&mut self) -> &mut OrderedSet<Url> {
164    self.document.also_known_as_mut()
165  }
166
167  /// Returns a reference to the underlying [`CoreDocument`].
168  pub fn core_document(&self) -> &CoreDocument {
169    &self.document
170  }
171
172  /// Returns a mutable reference to the underlying [`CoreDocument`].
173  ///
174  /// WARNING: Mutating the inner document directly bypasses checks and
175  /// may have undesired consequences.
176  pub(crate) fn core_document_mut(&mut self) -> &mut CoreDocument {
177    &mut self.document
178  }
179
180  /// Returns a reference to the custom DID Document properties.
181  pub fn properties(&self) -> &Object {
182    self.document.properties()
183  }
184
185  /// Returns a mutable reference to the custom DID Document properties.
186  ///
187  /// # Warning
188  ///
189  /// The properties returned are not checked against the standard fields in a [`CoreDocument`]. Incautious use can have
190  /// undesired consequences such as key collision when attempting to serialize the document or distinct resources (such
191  /// as services and methods) being identified by the same DID URL.  
192  pub fn properties_mut_unchecked(&mut self) -> &mut Object {
193    self.document.properties_mut_unchecked()
194  }
195
196  // ===========================================================================
197  // Services
198  // ===========================================================================
199
200  /// Return a set of all [`Service`]s in the document.
201  pub fn service(&self) -> &OrderedSet<Service> {
202    self.document.service()
203  }
204
205  /// Add a new [`Service`] to the document.
206  ///
207  /// # Errors
208  /// An error is returned if there already exists a service or (verification) method with
209  /// the same identifier in the document.  
210  pub fn insert_service(&mut self, service: Service) -> Result<()> {
211    self
212      .core_document_mut()
213      .insert_service(service)
214      .map_err(Error::InvalidDoc)
215  }
216
217  /// Remove and return the [`Service`] identified by the given [`DIDUrl`] from the document.
218  ///
219  /// `None` is returned if the service does not exist in the document.
220  pub fn remove_service(&mut self, did_url: &DIDUrl) -> Option<Service> {
221    self.core_document_mut().remove_service(did_url)
222  }
223
224  // ===========================================================================
225  // Verification Methods
226  // ===========================================================================
227
228  /// Returns a `Vec` of verification method references whose verification relationship matches `scope`.
229  ///
230  /// If `scope` is `None`, all **embedded** methods are returned.
231  pub fn methods(&self, scope: Option<MethodScope>) -> Vec<&VerificationMethod> {
232    self.document.methods(scope)
233  }
234
235  /// Adds a new [`VerificationMethod`] to the document in the given [`MethodScope`].
236  ///
237  /// # Errors
238  ///
239  /// Returns an error if a method with the same fragment already exists.
240  pub fn insert_method(&mut self, method: VerificationMethod, scope: MethodScope) -> Result<()> {
241    self
242      .core_document_mut()
243      .insert_method(method, scope)
244      .map_err(Error::InvalidDoc)
245  }
246
247  /// Removes and returns the [`VerificationMethod`] identified by `did_url` from the document.
248  ///
249  /// # Note
250  ///
251  /// All _references to the method_ found in the document will be removed.
252  /// This includes cases where the reference is to a method contained in another DID document.
253  pub fn remove_method(&mut self, did_url: &DIDUrl) -> Option<VerificationMethod> {
254    self.core_document_mut().remove_method(did_url)
255  }
256
257  /// Removes and returns the [`VerificationMethod`] from the document. The [`MethodScope`] under which the method was
258  /// found is appended to the second position of the returned tuple.
259  ///
260  /// # Note
261  ///
262  /// All _references to the method_ found in the document will be removed.
263  /// This includes cases where the reference is to a method contained in another DID document.
264  pub fn remove_method_and_scope(&mut self, did_url: &DIDUrl) -> Option<(VerificationMethod, MethodScope)> {
265    self.core_document_mut().remove_method_and_scope(did_url)
266  }
267
268  /// Attaches the relationship to the method resolved by `method_query`.
269  ///
270  /// # Errors
271  ///
272  /// Returns an error if the method does not exist or if it is embedded.
273  /// To convert an embedded method into a generic verification method, remove it first
274  /// and insert it with [`MethodScope::VerificationMethod`].
275  pub fn attach_method_relationship<'query, Q>(
276    &mut self,
277    method_query: Q,
278    relationship: MethodRelationship,
279  ) -> Result<bool>
280  where
281    Q: Into<DIDUrlQuery<'query>>,
282  {
283    self
284      .core_document_mut()
285      .attach_method_relationship(method_query, relationship)
286      .map_err(Error::InvalidDoc)
287  }
288
289  /// Detaches the `relationship` from the method identified by `did_url`.
290  /// Returns `true` if the relationship was found and removed, `false` otherwise.
291  ///
292  /// # Errors
293  ///
294  /// Returns an error if the method does not exist or is embedded.
295  /// To remove an embedded method, use [`Self::remove_method`].
296  ///
297  /// # Note
298  ///
299  /// If the method is referenced in the given scope, but the document does not contain the referenced verification
300  /// method, then the reference will persist in the document (i.e. it is not removed).
301  pub fn detach_method_relationship<'query, Q>(
302    &mut self,
303    method_query: Q,
304    relationship: MethodRelationship,
305  ) -> Result<bool>
306  where
307    Q: Into<DIDUrlQuery<'query>>,
308  {
309    self
310      .core_document_mut()
311      .detach_method_relationship(method_query, relationship)
312      .map_err(Error::InvalidDoc)
313  }
314
315  /// Returns the first [`VerificationMethod`] with an `id` property matching the
316  /// provided `method_query` and the verification relationship specified by `scope` if present.
317  ///
318  /// # Warning
319  ///
320  /// Incorrect use of this method can lead to distinct document resources being identified by the same DID URL.
321  pub fn resolve_method_mut<'query, Q>(
322    &mut self,
323    method_query: Q,
324    scope: Option<MethodScope>,
325  ) -> Option<&mut VerificationMethod>
326  where
327    Q: Into<DIDUrlQuery<'query>>,
328  {
329    self.document.resolve_method_mut(method_query, scope)
330  }
331
332  /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present.
333  // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains
334  // services whose ids are of the form <did different from this document's>#<fragment>.
335  pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&'me Service>
336  where
337    Q: Into<DIDUrlQuery<'query>>,
338  {
339    self.document.resolve_service(service_query)
340  }
341
342  /// Returns the first [`VerificationMethod`] with an `id` property matching the
343  /// provided `method_query` and the verification relationship specified by `scope` if present.
344  // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains methods
345  // whose ids are of the form <did different from this document's>#<fragment>.
346  pub fn resolve_method<'query, 'me, Q>(
347    &'me self,
348    method_query: Q,
349    scope: Option<MethodScope>,
350  ) -> Option<&'me VerificationMethod>
351  where
352    Q: Into<DIDUrlQuery<'query>>,
353  {
354    self.document.resolve_method(method_query, scope)
355  }
356
357  // ===========================================================================
358  // Signatures
359  // ===========================================================================
360
361  /// Decodes and verifies the provided JWS according to the passed [`JwsVerificationOptions`] and
362  /// [`JwsVerifier`].
363  ///
364  /// Regardless of which options are passed the following conditions must be met in order for a verification attempt to
365  /// take place.
366  /// - The JWS must be encoded according to the JWS compact serialization.
367  /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document.
368  pub fn verify_jws<'jws, T: JwsVerifier>(
369    &self,
370    jws: &'jws Jws,
371    detached_payload: Option<&'jws [u8]>,
372    signature_verifier: &T,
373    options: &JwsVerificationOptions,
374  ) -> Result<DecodedJws<'jws>> {
375    self
376      .core_document()
377      .verify_jws(jws.as_str(), detached_payload, signature_verifier, options)
378      .map_err(Error::JwsVerificationError)
379  }
380
381  // ===========================================================================
382  // Packing
383  // ===========================================================================
384
385  /// Serializes the document storing it in an identity.
386  /// with the default [`StateMetadataEncoding`].
387  pub fn pack(self) -> Result<Vec<u8>> {
388    self.pack_with_encoding(StateMetadataEncoding::default())
389  }
390
391  /// Serializes the document for storing it in an identity.
392  pub fn pack_with_encoding(self, encoding: StateMetadataEncoding) -> Result<Vec<u8>> {
393    StateMetadataDocument::from(self).pack(encoding)
394  }
395}
396
397#[cfg(feature = "iota-client")]
398mod client_document {
399  use identity_core::common::Timestamp;
400  use identity_did::DID;
401  use iota_interaction::rpc_types::IotaObjectData;
402
403  use crate::rebased::migration::unpack_identity_data;
404  use crate::rebased::migration::IdentityData;
405
406  use super::*;
407
408  impl IotaDocument {
409    // ===========================================================================
410    // Unpacking
411    // ===========================================================================
412
413    /// Deserializes the document from an `IotaObjectData` instance.
414    ///
415    /// If `allow_empty` is true, this will return an empty DID document marked as `deactivated`
416    /// if `state_metadata` is empty.
417    ///
418    /// NOTE: `did` is required since it is omitted from the serialized DID Document and
419    /// cannot be inferred from the state metadata. It also indicates the network, which is not
420    /// encoded in the object id alone.
421    pub fn unpack_from_iota_object_data(
422      did: &IotaDID,
423      data: &IotaObjectData,
424      allow_empty: bool,
425    ) -> Result<IotaDocument> {
426      let IdentityData {
427        multicontroller,
428        legacy_id,
429        created,
430        updated,
431        ..
432      } = unpack_identity_data(data.clone()).map_err(|_| {
433        Error::InvalidDoc(identity_document::Error::InvalidDocument(
434          "could not unpack identity data from IotaObjectData",
435          None,
436        ))
437      })?;
438      let did_network = did
439        .network_str()
440        .to_string()
441        .try_into()
442        .expect("did's network is a valid NetworkName");
443      let legacy_did = legacy_id.map(|id| IotaDID::new(&id.into_bytes(), &did_network));
444      let did_doc_bytes = multicontroller
445        .controlled_value()
446        .as_deref()
447        .ok_or_else(|| Error::DIDResolutionError("requested DID Document doesn't exist".to_string()))?;
448      let did_doc = Self::from_iota_document_data(did_doc_bytes, allow_empty, did, legacy_did, created, updated)?;
449
450      Ok(did_doc)
451    }
452
453    /// Parse given Bytes into a `IotaDocument`.
454    ///
455    /// Requires a valid document in `data` unless `allow_empty` is `true`, in which case
456    /// an empty, deactivated document is returned
457    ///
458    /// # Errors:
459    /// * document related parsing Errors from `StateMetadataDocument::unpack`
460    /// * possible parsing errors when trying to parse `created` and `updated` to a `Timestamp`
461    pub fn from_iota_document_data(
462      data: &[u8],
463      allow_empty: bool,
464      did: &IotaDID,
465      alternative_did: Option<IotaDID>,
466      created: Timestamp,
467      updated: Timestamp,
468    ) -> Result<Self> {
469      // check if DID has been deactivated
470      let mut did_doc = if data.is_empty() && allow_empty {
471        // DID has been deactivated by setting controlled value empty, therefore craft an empty document
472        let mut empty_document = Self::new_with_id(did.clone());
473        empty_document.metadata.deactivated = Some(true);
474        empty_document
475      } else {
476        // we have a value, therefore unpack it
477        StateMetadataDocument::unpack(data).and_then(|state_metadata_doc| state_metadata_doc.into_iota_document(did))?
478      };
479
480      // Set the `alsoKnownAs` property if a legacy DID is present.
481      if let Some(alternative_did) = alternative_did {
482        did_doc.also_known_as_mut().prepend(alternative_did.into_url().into());
483      }
484
485      // Overwrite `created` and `updated` with given timestamps
486      did_doc.metadata.created = Some(created);
487      did_doc.metadata.updated = Some(updated);
488
489      Ok(did_doc)
490    }
491  }
492}
493
494impl AsRef<CoreDocument> for IotaDocument {
495  fn as_ref(&self) -> &CoreDocument {
496    &self.document
497  }
498}
499
500#[cfg(feature = "revocation-bitmap")]
501mod iota_document_revocation {
502  use identity_credential::revocation::RevocationDocumentExt;
503  use identity_document::utils::DIDUrlQuery;
504
505  use crate::Error;
506  use crate::Result;
507
508  use super::IotaDocument;
509
510  impl IotaDocument {
511    /// If the document has a [`RevocationBitmap`](identity_credential::revocation::RevocationBitmap)
512    /// service identified by `service_query`, revoke all specified `indices`.
513    pub fn revoke_credentials<'query, 'me, Q>(&mut self, service_query: Q, indices: &[u32]) -> Result<()>
514    where
515      Q: Into<DIDUrlQuery<'query>>,
516    {
517      self
518        .core_document_mut()
519        .revoke_credentials(service_query, indices)
520        .map_err(Error::RevocationError)
521    }
522
523    /// If the document has a [`RevocationBitmap`](identity_credential::revocation::RevocationBitmap)
524    /// service with an id by `service_query`, unrevoke all specified `indices`.
525    pub fn unrevoke_credentials<'query, 'me, Q>(&'me mut self, service_query: Q, indices: &[u32]) -> Result<()>
526    where
527      Q: Into<DIDUrlQuery<'query>>,
528    {
529      self
530        .core_document_mut()
531        .unrevoke_credentials(service_query, indices)
532        .map_err(Error::RevocationError)
533    }
534  }
535}
536
537impl From<IotaDocument> for CoreDocument {
538  fn from(document: IotaDocument) -> Self {
539    document.document
540  }
541}
542
543impl From<CoreDocument> for IotaDocument {
544  fn from(value: CoreDocument) -> Self {
545    IotaDocument {
546      document: value,
547      metadata: IotaDocumentMetadata::default(),
548    }
549  }
550}
551
552impl TryFrom<(CoreDocument, IotaDocumentMetadata)> for IotaDocument {
553  type Error = Error;
554  /// Converts the tuple into an [`IotaDocument`] if the given [`CoreDocument`] has an identifier satisfying the
555  /// requirements of the IOTA UTXO method and the same holds for all of the [`CoreDocument's`](CoreDocument)
556  /// controllers.
557  ///
558  /// # Important
559  /// This does not check the relationship between the [`CoreDocument`] and the [`IotaDocumentMetadata`].
560  fn try_from(value: (CoreDocument, IotaDocumentMetadata)) -> std::result::Result<Self, Self::Error> {
561    ProvisionalIotaDocument {
562      document: value.0,
563      metadata: value.1,
564    }
565    .try_into()
566  }
567}
568
569impl Display for IotaDocument {
570  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
571    self.fmt_json(f)
572  }
573}
574
575#[cfg(test)]
576mod tests {
577  use identity_core::common::Timestamp;
578  use identity_core::convert::FromJson;
579  use identity_core::convert::ToJson;
580  use identity_did::DID;
581
582  use super::*;
583  use crate::test_utils::generate_method;
584
585  fn valid_did() -> IotaDID {
586    "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
587      .parse()
588      .unwrap()
589  }
590
591  fn generate_document(id: &IotaDID) -> IotaDocument {
592    let mut metadata: IotaDocumentMetadata = IotaDocumentMetadata::new();
593    metadata.created = Some(Timestamp::parse("2020-01-02T00:00:00Z").unwrap());
594    metadata.updated = Some(Timestamp::parse("2020-01-02T00:00:00Z").unwrap());
595
596    let document: CoreDocument = CoreDocument::builder(Object::default())
597      .id(id.clone().into())
598      .controller(id.clone().into())
599      .verification_method(generate_method(id, "#key-1"))
600      .verification_method(generate_method(id, "#key-2"))
601      .verification_method(generate_method(id, "#key-3"))
602      .authentication(generate_method(id, "#auth-key"))
603      .authentication(id.to_url().join("#key-3").unwrap())
604      .build()
605      .unwrap();
606
607    IotaDocument { document, metadata }
608  }
609
610  #[test]
611  fn test_new() {
612    // VALID new().
613    let network: NetworkName = NetworkName::try_from("test").unwrap();
614    let placeholder: IotaDID = IotaDID::placeholder(&network);
615    let doc1: IotaDocument = IotaDocument::new(&network);
616    assert_eq!(doc1.id().network_str(), network.as_ref());
617    assert_eq!(doc1.id().tag_str(), placeholder.tag_str());
618    assert_eq!(doc1.id(), &placeholder);
619    assert_eq!(doc1.methods(None).len(), 0);
620    assert!(doc1.service().is_empty());
621
622    // VALID new_with_id().
623    let did: IotaDID = valid_did();
624    let doc2: IotaDocument = IotaDocument::new_with_id(did.clone());
625    assert_eq!(doc2.id(), &did);
626    assert_eq!(doc2.methods(None).len(), 0);
627    assert!(doc2.service().is_empty());
628  }
629
630  #[test]
631  fn test_methods() {
632    let controller: IotaDID = valid_did();
633    let document: IotaDocument = generate_document(&controller);
634    let expected: [&'static str; 4] = ["key-1", "key-2", "key-3", "auth-key"];
635
636    let mut methods = document.methods(None).into_iter();
637    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[0]);
638    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[1]);
639    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[2]);
640    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[3]);
641    assert_eq!(methods.next(), None);
642  }
643
644  #[test]
645  fn test_services() {
646    // VALID: add one service.
647    let mut document: IotaDocument = IotaDocument::new_with_id(valid_did());
648    let url1: DIDUrl = document.id().to_url().join("#linked-domain").unwrap();
649    let service1: Service = Service::from_json(&format!(
650      r#"{{
651      "id":"{url1}",
652      "type": "LinkedDomains",
653      "serviceEndpoint": "https://bar.example.com"
654    }}"#
655    ))
656    .unwrap();
657    assert!(document.insert_service(service1.clone()).is_ok());
658    assert_eq!(1, document.service().len());
659    assert_eq!(document.resolve_service(&url1), Some(&service1));
660    assert_eq!(document.resolve_service("#linked-domain"), Some(&service1));
661    assert_eq!(document.resolve_service("linked-domain"), Some(&service1));
662    assert_eq!(document.resolve_service(""), None);
663    assert_eq!(document.resolve_service("#other"), None);
664
665    // VALID: add two services.
666    let url2: DIDUrl = document.id().to_url().join("#revocation").unwrap();
667    let service2: Service = Service::from_json(&format!(
668      r#"{{
669      "id":"{url2}",
670      "type": "RevocationBitmap2022",
671      "serviceEndpoint": "data:,blah"
672    }}"#
673    ))
674    .unwrap();
675    assert!(document.insert_service(service2.clone()).is_ok());
676    assert_eq!(2, document.service().len());
677    assert_eq!(document.resolve_service(&url2), Some(&service2));
678    assert_eq!(document.resolve_service("#revocation"), Some(&service2));
679    assert_eq!(document.resolve_service("revocation"), Some(&service2));
680    assert_eq!(document.resolve_service(""), None);
681    assert_eq!(document.resolve_service("#other"), None);
682
683    // INVALID: insert service with duplicate fragment fails.
684    let duplicate: Service = Service::from_json(&format!(
685      r#"{{
686      "id":"{url1}",
687      "type": "DuplicateService",
688      "serviceEndpoint": "data:,duplicate"
689    }}"#
690    ))
691    .unwrap();
692    assert!(document.insert_service(duplicate.clone()).is_err());
693    assert_eq!(2, document.service().len());
694    let resolved: &Service = document.resolve_service(&url1).unwrap();
695    assert_eq!(resolved, &service1);
696    assert_ne!(resolved, &duplicate);
697
698    // VALID: remove services.
699    assert_eq!(service1, document.remove_service(&url1).unwrap());
700    assert_eq!(1, document.service().len());
701    let last_service: &Service = document.resolve_service(&url2).unwrap();
702    assert_eq!(last_service, &service2);
703
704    assert_eq!(service2, document.remove_service(&url2).unwrap());
705    assert_eq!(0, document.service().len());
706  }
707
708  #[test]
709  fn test_document_equality() {
710    let mut original_doc: IotaDocument = IotaDocument::new_with_id(valid_did());
711    let method1: VerificationMethod = generate_method(original_doc.id(), "test-0");
712    original_doc
713      .insert_method(method1, MethodScope::capability_invocation())
714      .unwrap();
715
716    // Update the key material of the existing verification method #test-0.
717    let mut doc1 = original_doc.clone();
718    let method2: VerificationMethod = generate_method(original_doc.id(), "test-0");
719
720    doc1
721      .remove_method(&doc1.id().to_url().join("#test-0").unwrap())
722      .unwrap();
723    doc1
724      .insert_method(method2, MethodScope::capability_invocation())
725      .unwrap();
726
727    // Even though the method fragment is the same, the key material has been updated
728    // so the two documents are expected to not be equal.
729    assert_ne!(original_doc, doc1);
730
731    let mut doc2 = doc1.clone();
732    let method3: VerificationMethod = generate_method(original_doc.id(), "test-0");
733
734    let insertion_result = doc2.insert_method(method3, MethodScope::capability_invocation());
735
736    // Nothing was inserted, because a method with the same fragment already existed.
737    assert!(insertion_result.is_err());
738    assert_eq!(doc1, doc2);
739  }
740
741  #[test]
742  fn test_unpack_empty() {
743    // VALID: unpack empty, deactivated document.
744    let did: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
745      .parse()
746      .unwrap();
747    let document = IotaDocument::from_iota_document_data(
748      &[],
749      true,
750      &did,
751      None,
752      Timestamp::from_unix(12).unwrap(),
753      Timestamp::from_unix(34).unwrap(),
754    )
755    .unwrap();
756    assert_eq!(document.id(), &did);
757    assert_eq!(document.metadata.deactivated, Some(true));
758
759    // // Ensure no other fields are injected.
760    let json: String = format!(
761      r#"{{"doc":{{"id":"{did}"}},"meta":{{"created":"1970-01-01T00:00:12Z","updated":"1970-01-01T00:00:34Z","deactivated":true}}}}"#
762    );
763    assert_eq!(document.to_json().unwrap(), json);
764
765    // INVALID: reject empty document.
766    assert!(IotaDocument::from_iota_document_data(
767      &[],
768      false,
769      &did,
770      None,
771      Timestamp::from_unix(12).unwrap(),
772      Timestamp::from_unix(34).unwrap()
773    )
774    .is_err());
775
776    // Ensure re-packing keeps the controller as None
777    let packed: Vec<u8> = document.pack_with_encoding(StateMetadataEncoding::Json).unwrap();
778    let state_metadata_document: StateMetadataDocument = StateMetadataDocument::unpack(&packed).unwrap();
779    let unpacked_document: IotaDocument = state_metadata_document.into_iota_document(&did).unwrap();
780    assert!(unpacked_document.document.controller().is_none());
781  }
782
783  #[test]
784  fn test_json_roundtrip() {
785    let document: IotaDocument = generate_document(&valid_did());
786
787    let ser: String = document.to_json().unwrap();
788    let de: IotaDocument = IotaDocument::from_json(&ser).unwrap();
789    assert_eq!(document, de);
790  }
791
792  #[test]
793  fn test_json_fieldnames() {
794    // Changing the serialization is a breaking change!
795    let document: IotaDocument = IotaDocument::new_with_id(valid_did());
796    let serialization: String = document.to_json().unwrap();
797    assert_eq!(
798      serialization,
799      format!("{{\"doc\":{},\"meta\":{}}}", document.document, document.metadata)
800    );
801  }
802
803  #[test]
804  fn deserializing_id_from_other_method_fails() {
805    const JSON_DOC_INVALID_ID: &str = r#"
806    {
807      "doc": {
808        "id": "did:foo:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
809        "verificationMethod": [
810          {
811            "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
812            "controller": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
813            "type": "Ed25519VerificationKey2018",
814            "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
815          }
816        ]
817      },
818      "meta": {
819        "created": "2022-08-31T09:33:31Z",
820        "updated": "2022-08-31T09:33:31Z"
821      }
822    }"#;
823
824    let deserialization_result = IotaDocument::from_json(&JSON_DOC_INVALID_ID);
825
826    assert!(deserialization_result.is_err());
827
828    // Check that deserialization works after correcting the json document to have a valid IOTA DID as its identifier.
829    const JSON_DOC_CORRECT_ID: &str = r#"
830    {
831      "doc": {
832        "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
833        "verificationMethod": [
834          {
835            "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
836            "controller": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
837            "type": "Ed25519VerificationKey2018",
838            "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
839          }
840        ]
841      },
842      "meta": {
843        "created": "2022-08-31T09:33:31Z",
844        "updated": "2022-08-31T09:33:31Z"
845      }
846    }"#;
847
848    let corrected_deserialization_result = IotaDocument::from_json(&JSON_DOC_CORRECT_ID);
849    assert!(corrected_deserialization_result.is_ok());
850  }
851
852  #[test]
853  fn deserializing_controller_from_other_method_fails() {
854    const JSON_DOC_INVALID_CONTROLLER_ID: &str = r#"
855    {
856    "doc": {
857      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
858      "controller": "did:example:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
859      "verificationMethod": [
860        {
861          "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38#key-2",
862          "controller": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
863          "type": "Ed25519VerificationKey2018",
864          "publicKeyMultibase": "z7eTUXFdLCFg1LFVFhG8qUAM2aSjfTuPLB2x9XGXgQh6G"
865        }
866      ]
867    },
868    "meta": {
869      "created": "2023-01-25T15:48:09Z",
870      "updated": "2023-01-25T15:48:09Z"
871    }
872  }
873  "#;
874
875    let deserialization_result = IotaDocument::from_json(&JSON_DOC_INVALID_CONTROLLER_ID);
876    assert!(deserialization_result.is_err());
877
878    // Check that deserialization works after correcting the json document to have a valid IOTA DID as the controller.
879    const JSON_DOC_CORRECT_CONTROLLER_ID: &str = r#"
880  {
881  "doc": {
882    "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
883    "controller": "did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
884    "verificationMethod": [
885      {
886        "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38#key-2",
887        "controller": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
888        "type": "Ed25519VerificationKey2018",
889        "publicKeyMultibase": "z7eTUXFdLCFg1LFVFhG8qUAM2aSjfTuPLB2x9XGXgQh6G"
890      }
891    ]
892  },
893  "meta": {
894    "created": "2023-01-25T15:48:09Z",
895    "updated": "2023-01-25T15:48:09Z"
896  }
897}
898"#;
899    let corrected_deserialization_result = IotaDocument::from_json(JSON_DOC_CORRECT_CONTROLLER_ID);
900    assert!(corrected_deserialization_result.is_ok());
901  }
902
903  #[test]
904  fn controller_iterator_without_controller() {
905    const DOC_JSON: &str = r#"
906    {
907      "doc": {
908        "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
909      },
910      "meta": {
911        "created": "2022-08-31T09:33:31Z",
912        "updated": "2022-08-31T09:33:31Z"
913      }
914    }
915    "#;
916
917    let doc = IotaDocument::from_json(DOC_JSON).unwrap();
918    assert!(doc.controller().next().is_none());
919  }
920
921  #[test]
922  fn controller_iterator_with_controller() {
923    const DOC_JSON: &str = r#"
924  {
925    "doc": {
926      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
927      "controller": "did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343"
928    },
929    "meta": {
930      "created": "2023-01-25T15:48:09Z",
931      "updated": "2023-01-25T15:48:09Z"
932    }
933  }
934  "#;
935    let doc = IotaDocument::from_json(DOC_JSON).unwrap();
936    let expected_controller =
937      IotaDID::parse("did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343").unwrap();
938    let controllers: Vec<&IotaDID> = doc.controller().collect();
939    assert_eq!(&controllers, &[&expected_controller]);
940  }
941
942  #[test]
943  fn try_from_doc_metadata() {
944    const DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_ID: &str = r#"
945    {
946      "id": "did:foo:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
947      "verificationMethod": [
948        {
949          "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
950          "controller": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
951          "type": "Ed25519VerificationKey2018",
952          "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
953        }
954      ]
955    }
956    "#;
957
958    const DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_CONTROLLER: &str = r#"
959    {
960      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
961      "controller": "did:example:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
962      "verificationMethod": [
963        {
964          "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38#key-2",
965          "controller": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
966          "type": "Ed25519VerificationKey2018",
967          "publicKeyMultibase": "z7eTUXFdLCFg1LFVFhG8qUAM2aSjfTuPLB2x9XGXgQh6G"
968        }
969      ]
970    }
971    "#;
972
973    const METADATA_JSON: &str = r#"
974    {
975      "created": "2022-08-31T09:33:31Z",
976      "updated": "2022-08-31T09:33:31Z"
977    }
978    "#;
979
980    const DOCUMENT_WITH_IOTA_ID_AND_CONTROLLER_JSON: &str = r#"
981    {
982      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
983      "controller": "did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
984      "verificationMethod": [
985        {
986          "id": "did:foo:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
987          "controller": "did:bar:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
988          "type": "Ed25519VerificationKey2018",
989          "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
990        }
991      ]
992    }
993    "#;
994
995    let doc_not_iota_because_of_id: CoreDocument =
996      CoreDocument::from_json(DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_ID).unwrap();
997    let doc_not_iota_because_of_controller: CoreDocument =
998      CoreDocument::from_json(DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_CONTROLLER).unwrap();
999    let doc_with_iota_id_and_controller: CoreDocument =
1000      CoreDocument::from_json(DOCUMENT_WITH_IOTA_ID_AND_CONTROLLER_JSON).unwrap();
1001    let metadata: IotaDocumentMetadata = IotaDocumentMetadata::from_json(METADATA_JSON).unwrap();
1002
1003    assert!(IotaDocument::try_from((doc_not_iota_because_of_id, metadata.clone())).is_err());
1004
1005    assert!(IotaDocument::try_from((doc_not_iota_because_of_controller, metadata.clone())).is_err());
1006
1007    assert!(IotaDocument::try_from((doc_with_iota_id_and_controller, metadata)).is_ok());
1008  }
1009}