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 unpacked = unpack_identity_data(did, data).map_err(|_| {
427        Error::InvalidDoc(identity_document::Error::InvalidDocument(
428          "could not unpack identity data from IotaObjectData",
429          None,
430        ))
431      })?;
432      let IdentityData {
433        multicontroller,
434        legacy_id,
435        created,
436        updated,
437        ..
438      } = match unpacked {
439        Some(data) => data,
440        None => {
441          return Err(Error::InvalidDoc(identity_document::Error::InvalidDocument(
442            "given IotaObjectData did not contain a document",
443            None,
444          )));
445        }
446      };
447      let did_network = did
448        .network_str()
449        .to_string()
450        .try_into()
451        .expect("did's network is a valid NetworkName");
452      let legacy_did = legacy_id.map(|id| IotaDID::new(&id.into_bytes(), &did_network));
453      let did_doc_bytes = multicontroller
454        .controlled_value()
455        .as_deref()
456        .ok_or_else(|| Error::DIDResolutionError("requested DID Document doesn't exist".to_string()))?;
457      let did_doc = Self::from_iota_document_data(did_doc_bytes, allow_empty, did, legacy_did, created, updated)?;
458
459      Ok(did_doc)
460    }
461
462    /// Parse given Bytes into a `IotaDocument`.
463    ///
464    /// Requires a valid document in `data` unless `allow_empty` is `true`, in which case
465    /// an empty, deactivated document is returned
466    ///
467    /// # Errors:
468    /// * document related parsing Errors from `StateMetadataDocument::unpack`
469    /// * possible parsing errors when trying to parse `created` and `updated` to a `Timestamp`
470    pub fn from_iota_document_data(
471      data: &[u8],
472      allow_empty: bool,
473      did: &IotaDID,
474      alternative_did: Option<IotaDID>,
475      created: Timestamp,
476      updated: Timestamp,
477    ) -> Result<Self> {
478      // check if DID has been deactivated
479      let mut did_doc = if data.is_empty() && allow_empty {
480        // DID has been deactivated by setting controlled value empty, therefore craft an empty document
481        let mut empty_document = Self::new_with_id(did.clone());
482        empty_document.metadata.deactivated = Some(true);
483        empty_document
484      } else {
485        // we have a value, therefore unpack it
486        StateMetadataDocument::unpack(data).and_then(|state_metadata_doc| state_metadata_doc.into_iota_document(did))?
487      };
488
489      // Set the `alsoKnownAs` property if a legacy DID is present.
490      if let Some(alternative_did) = alternative_did {
491        did_doc.also_known_as_mut().prepend(alternative_did.into_url().into());
492      }
493
494      // Overwrite `created` and `updated` with given timestamps
495      did_doc.metadata.created = Some(created);
496      did_doc.metadata.updated = Some(updated);
497
498      Ok(did_doc)
499    }
500  }
501}
502
503impl AsRef<CoreDocument> for IotaDocument {
504  fn as_ref(&self) -> &CoreDocument {
505    &self.document
506  }
507}
508
509#[cfg(feature = "revocation-bitmap")]
510mod iota_document_revocation {
511  use identity_credential::revocation::RevocationDocumentExt;
512  use identity_document::utils::DIDUrlQuery;
513
514  use crate::Error;
515  use crate::Result;
516
517  use super::IotaDocument;
518
519  impl IotaDocument {
520    /// If the document has a [`RevocationBitmap`](identity_credential::revocation::RevocationBitmap)
521    /// service identified by `service_query`, revoke all specified `indices`.
522    pub fn revoke_credentials<'query, 'me, Q>(&mut self, service_query: Q, indices: &[u32]) -> Result<()>
523    where
524      Q: Into<DIDUrlQuery<'query>>,
525    {
526      self
527        .core_document_mut()
528        .revoke_credentials(service_query, indices)
529        .map_err(Error::RevocationError)
530    }
531
532    /// If the document has a [`RevocationBitmap`](identity_credential::revocation::RevocationBitmap)
533    /// service with an id by `service_query`, unrevoke all specified `indices`.
534    pub fn unrevoke_credentials<'query, 'me, Q>(&'me mut self, service_query: Q, indices: &[u32]) -> Result<()>
535    where
536      Q: Into<DIDUrlQuery<'query>>,
537    {
538      self
539        .core_document_mut()
540        .unrevoke_credentials(service_query, indices)
541        .map_err(Error::RevocationError)
542    }
543  }
544}
545
546impl From<IotaDocument> for CoreDocument {
547  fn from(document: IotaDocument) -> Self {
548    document.document
549  }
550}
551
552impl From<CoreDocument> for IotaDocument {
553  fn from(value: CoreDocument) -> Self {
554    IotaDocument {
555      document: value,
556      metadata: IotaDocumentMetadata::default(),
557    }
558  }
559}
560
561impl TryFrom<(CoreDocument, IotaDocumentMetadata)> for IotaDocument {
562  type Error = Error;
563  /// Converts the tuple into an [`IotaDocument`] if the given [`CoreDocument`] has an identifier satisfying the
564  /// requirements of the IOTA UTXO method and the same holds for all of the [`CoreDocument's`](CoreDocument)
565  /// controllers.
566  ///
567  /// # Important
568  /// This does not check the relationship between the [`CoreDocument`] and the [`IotaDocumentMetadata`].
569  fn try_from(value: (CoreDocument, IotaDocumentMetadata)) -> std::result::Result<Self, Self::Error> {
570    ProvisionalIotaDocument {
571      document: value.0,
572      metadata: value.1,
573    }
574    .try_into()
575  }
576}
577
578impl Display for IotaDocument {
579  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580    self.fmt_json(f)
581  }
582}
583
584#[cfg(test)]
585mod tests {
586  use identity_core::common::Timestamp;
587  use identity_core::convert::FromJson;
588  use identity_core::convert::ToJson;
589  use identity_did::DID;
590
591  use super::*;
592  use crate::test_utils::generate_method;
593
594  fn valid_did() -> IotaDID {
595    "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
596      .parse()
597      .unwrap()
598  }
599
600  fn generate_document(id: &IotaDID) -> IotaDocument {
601    let mut metadata: IotaDocumentMetadata = IotaDocumentMetadata::new();
602    metadata.created = Some(Timestamp::parse("2020-01-02T00:00:00Z").unwrap());
603    metadata.updated = Some(Timestamp::parse("2020-01-02T00:00:00Z").unwrap());
604
605    let document: CoreDocument = CoreDocument::builder(Object::default())
606      .id(id.clone().into())
607      .controller(id.clone().into())
608      .verification_method(generate_method(id, "#key-1"))
609      .verification_method(generate_method(id, "#key-2"))
610      .verification_method(generate_method(id, "#key-3"))
611      .authentication(generate_method(id, "#auth-key"))
612      .authentication(id.to_url().join("#key-3").unwrap())
613      .build()
614      .unwrap();
615
616    IotaDocument { document, metadata }
617  }
618
619  #[test]
620  fn test_new() {
621    // VALID new().
622    let network: NetworkName = NetworkName::try_from("test").unwrap();
623    let placeholder: IotaDID = IotaDID::placeholder(&network);
624    let doc1: IotaDocument = IotaDocument::new(&network);
625    assert_eq!(doc1.id().network_str(), network.as_ref());
626    assert_eq!(doc1.id().tag_str(), placeholder.tag_str());
627    assert_eq!(doc1.id(), &placeholder);
628    assert_eq!(doc1.methods(None).len(), 0);
629    assert!(doc1.service().is_empty());
630
631    // VALID new_with_id().
632    let did: IotaDID = valid_did();
633    let doc2: IotaDocument = IotaDocument::new_with_id(did.clone());
634    assert_eq!(doc2.id(), &did);
635    assert_eq!(doc2.methods(None).len(), 0);
636    assert!(doc2.service().is_empty());
637  }
638
639  #[test]
640  fn test_methods() {
641    let controller: IotaDID = valid_did();
642    let document: IotaDocument = generate_document(&controller);
643    let expected: [&'static str; 4] = ["key-1", "key-2", "key-3", "auth-key"];
644
645    let mut methods = document.methods(None).into_iter();
646    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[0]);
647    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[1]);
648    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[2]);
649    assert_eq!(methods.next().unwrap().id().fragment().unwrap(), expected[3]);
650    assert_eq!(methods.next(), None);
651  }
652
653  #[test]
654  fn test_services() {
655    // VALID: add one service.
656    let mut document: IotaDocument = IotaDocument::new_with_id(valid_did());
657    let url1: DIDUrl = document.id().to_url().join("#linked-domain").unwrap();
658    let service1: Service = Service::from_json(&format!(
659      r#"{{
660      "id":"{url1}",
661      "type": "LinkedDomains",
662      "serviceEndpoint": "https://bar.example.com"
663    }}"#
664    ))
665    .unwrap();
666    assert!(document.insert_service(service1.clone()).is_ok());
667    assert_eq!(1, document.service().len());
668    assert_eq!(document.resolve_service(&url1), Some(&service1));
669    assert_eq!(document.resolve_service("#linked-domain"), Some(&service1));
670    assert_eq!(document.resolve_service("linked-domain"), Some(&service1));
671    assert_eq!(document.resolve_service(""), None);
672    assert_eq!(document.resolve_service("#other"), None);
673
674    // VALID: add two services.
675    let url2: DIDUrl = document.id().to_url().join("#revocation").unwrap();
676    let service2: Service = Service::from_json(&format!(
677      r#"{{
678      "id":"{url2}",
679      "type": "RevocationBitmap2022",
680      "serviceEndpoint": "data:,blah"
681    }}"#
682    ))
683    .unwrap();
684    assert!(document.insert_service(service2.clone()).is_ok());
685    assert_eq!(2, document.service().len());
686    assert_eq!(document.resolve_service(&url2), Some(&service2));
687    assert_eq!(document.resolve_service("#revocation"), Some(&service2));
688    assert_eq!(document.resolve_service("revocation"), Some(&service2));
689    assert_eq!(document.resolve_service(""), None);
690    assert_eq!(document.resolve_service("#other"), None);
691
692    // INVALID: insert service with duplicate fragment fails.
693    let duplicate: Service = Service::from_json(&format!(
694      r#"{{
695      "id":"{url1}",
696      "type": "DuplicateService",
697      "serviceEndpoint": "data:,duplicate"
698    }}"#
699    ))
700    .unwrap();
701    assert!(document.insert_service(duplicate.clone()).is_err());
702    assert_eq!(2, document.service().len());
703    let resolved: &Service = document.resolve_service(&url1).unwrap();
704    assert_eq!(resolved, &service1);
705    assert_ne!(resolved, &duplicate);
706
707    // VALID: remove services.
708    assert_eq!(service1, document.remove_service(&url1).unwrap());
709    assert_eq!(1, document.service().len());
710    let last_service: &Service = document.resolve_service(&url2).unwrap();
711    assert_eq!(last_service, &service2);
712
713    assert_eq!(service2, document.remove_service(&url2).unwrap());
714    assert_eq!(0, document.service().len());
715  }
716
717  #[test]
718  fn test_document_equality() {
719    let mut original_doc: IotaDocument = IotaDocument::new_with_id(valid_did());
720    let method1: VerificationMethod = generate_method(original_doc.id(), "test-0");
721    original_doc
722      .insert_method(method1, MethodScope::capability_invocation())
723      .unwrap();
724
725    // Update the key material of the existing verification method #test-0.
726    let mut doc1 = original_doc.clone();
727    let method2: VerificationMethod = generate_method(original_doc.id(), "test-0");
728
729    doc1
730      .remove_method(&doc1.id().to_url().join("#test-0").unwrap())
731      .unwrap();
732    doc1
733      .insert_method(method2, MethodScope::capability_invocation())
734      .unwrap();
735
736    // Even though the method fragment is the same, the key material has been updated
737    // so the two documents are expected to not be equal.
738    assert_ne!(original_doc, doc1);
739
740    let mut doc2 = doc1.clone();
741    let method3: VerificationMethod = generate_method(original_doc.id(), "test-0");
742
743    let insertion_result = doc2.insert_method(method3, MethodScope::capability_invocation());
744
745    // Nothing was inserted, because a method with the same fragment already existed.
746    assert!(insertion_result.is_err());
747    assert_eq!(doc1, doc2);
748  }
749
750  #[test]
751  fn test_unpack_empty() {
752    // VALID: unpack empty, deactivated document.
753    let did: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
754      .parse()
755      .unwrap();
756    let document = IotaDocument::from_iota_document_data(
757      &[],
758      true,
759      &did,
760      None,
761      Timestamp::from_unix(12).unwrap(),
762      Timestamp::from_unix(34).unwrap(),
763    )
764    .unwrap();
765    assert_eq!(document.id(), &did);
766    assert_eq!(document.metadata.deactivated, Some(true));
767
768    // // Ensure no other fields are injected.
769    let json: String = format!(
770      r#"{{"doc":{{"id":"{did}"}},"meta":{{"created":"1970-01-01T00:00:12Z","updated":"1970-01-01T00:00:34Z","deactivated":true}}}}"#
771    );
772    assert_eq!(document.to_json().unwrap(), json);
773
774    // INVALID: reject empty document.
775    assert!(IotaDocument::from_iota_document_data(
776      &[],
777      false,
778      &did,
779      None,
780      Timestamp::from_unix(12).unwrap(),
781      Timestamp::from_unix(34).unwrap()
782    )
783    .is_err());
784
785    // Ensure re-packing keeps the controller as None
786    let packed: Vec<u8> = document.pack_with_encoding(StateMetadataEncoding::Json).unwrap();
787    let state_metadata_document: StateMetadataDocument = StateMetadataDocument::unpack(&packed).unwrap();
788    let unpacked_document: IotaDocument = state_metadata_document.into_iota_document(&did).unwrap();
789    assert!(unpacked_document.document.controller().is_none());
790  }
791
792  #[test]
793  fn test_json_roundtrip() {
794    let document: IotaDocument = generate_document(&valid_did());
795
796    let ser: String = document.to_json().unwrap();
797    let de: IotaDocument = IotaDocument::from_json(&ser).unwrap();
798    assert_eq!(document, de);
799  }
800
801  #[test]
802  fn test_json_fieldnames() {
803    // Changing the serialization is a breaking change!
804    let document: IotaDocument = IotaDocument::new_with_id(valid_did());
805    let serialization: String = document.to_json().unwrap();
806    assert_eq!(
807      serialization,
808      format!("{{\"doc\":{},\"meta\":{}}}", document.document, document.metadata)
809    );
810  }
811
812  #[test]
813  fn deserializing_id_from_other_method_fails() {
814    const JSON_DOC_INVALID_ID: &str = r#"
815    {
816      "doc": {
817        "id": "did:foo:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
818        "verificationMethod": [
819          {
820            "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
821            "controller": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
822            "type": "Ed25519VerificationKey2018",
823            "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
824          }
825        ]
826      },
827      "meta": {
828        "created": "2022-08-31T09:33:31Z",
829        "updated": "2022-08-31T09:33:31Z"
830      }
831    }"#;
832
833    let deserialization_result = IotaDocument::from_json(&JSON_DOC_INVALID_ID);
834
835    assert!(deserialization_result.is_err());
836
837    // Check that deserialization works after correcting the json document to have a valid IOTA DID as its identifier.
838    const JSON_DOC_CORRECT_ID: &str = r#"
839    {
840      "doc": {
841        "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
842        "verificationMethod": [
843          {
844            "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
845            "controller": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
846            "type": "Ed25519VerificationKey2018",
847            "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
848          }
849        ]
850      },
851      "meta": {
852        "created": "2022-08-31T09:33:31Z",
853        "updated": "2022-08-31T09:33:31Z"
854      }
855    }"#;
856
857    let corrected_deserialization_result = IotaDocument::from_json(&JSON_DOC_CORRECT_ID);
858    assert!(corrected_deserialization_result.is_ok());
859  }
860
861  #[test]
862  fn deserializing_controller_from_other_method_fails() {
863    const JSON_DOC_INVALID_CONTROLLER_ID: &str = r#"
864    {
865    "doc": {
866      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
867      "controller": "did:example:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
868      "verificationMethod": [
869        {
870          "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38#key-2",
871          "controller": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
872          "type": "Ed25519VerificationKey2018",
873          "publicKeyMultibase": "z7eTUXFdLCFg1LFVFhG8qUAM2aSjfTuPLB2x9XGXgQh6G"
874        }
875      ]
876    },
877    "meta": {
878      "created": "2023-01-25T15:48:09Z",
879      "updated": "2023-01-25T15:48:09Z"
880    }
881  }
882  "#;
883
884    let deserialization_result = IotaDocument::from_json(&JSON_DOC_INVALID_CONTROLLER_ID);
885    assert!(deserialization_result.is_err());
886
887    // Check that deserialization works after correcting the json document to have a valid IOTA DID as the controller.
888    const JSON_DOC_CORRECT_CONTROLLER_ID: &str = r#"
889  {
890  "doc": {
891    "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
892    "controller": "did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
893    "verificationMethod": [
894      {
895        "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38#key-2",
896        "controller": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
897        "type": "Ed25519VerificationKey2018",
898        "publicKeyMultibase": "z7eTUXFdLCFg1LFVFhG8qUAM2aSjfTuPLB2x9XGXgQh6G"
899      }
900    ]
901  },
902  "meta": {
903    "created": "2023-01-25T15:48:09Z",
904    "updated": "2023-01-25T15:48:09Z"
905  }
906}
907"#;
908    let corrected_deserialization_result = IotaDocument::from_json(JSON_DOC_CORRECT_CONTROLLER_ID);
909    assert!(corrected_deserialization_result.is_ok());
910  }
911
912  #[test]
913  fn controller_iterator_without_controller() {
914    const DOC_JSON: &str = r#"
915    {
916      "doc": {
917        "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
918      },
919      "meta": {
920        "created": "2022-08-31T09:33:31Z",
921        "updated": "2022-08-31T09:33:31Z"
922      }
923    }
924    "#;
925
926    let doc = IotaDocument::from_json(DOC_JSON).unwrap();
927    assert!(doc.controller().next().is_none());
928  }
929
930  #[test]
931  fn controller_iterator_with_controller() {
932    const DOC_JSON: &str = r#"
933  {
934    "doc": {
935      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
936      "controller": "did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343"
937    },
938    "meta": {
939      "created": "2023-01-25T15:48:09Z",
940      "updated": "2023-01-25T15:48:09Z"
941    }
942  }
943  "#;
944    let doc = IotaDocument::from_json(DOC_JSON).unwrap();
945    let expected_controller =
946      IotaDID::parse("did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343").unwrap();
947    let controllers: Vec<&IotaDID> = doc.controller().collect();
948    assert_eq!(&controllers, &[&expected_controller]);
949  }
950
951  #[test]
952  fn try_from_doc_metadata() {
953    const DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_ID: &str = r#"
954    {
955      "id": "did:foo:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
956      "verificationMethod": [
957        {
958          "id": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
959          "controller": "did:iota:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
960          "type": "Ed25519VerificationKey2018",
961          "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
962        }
963      ]
964    }
965    "#;
966
967    const DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_CONTROLLER: &str = r#"
968    {
969      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
970      "controller": "did:example:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
971      "verificationMethod": [
972        {
973          "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38#key-2",
974          "controller": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
975          "type": "Ed25519VerificationKey2018",
976          "publicKeyMultibase": "z7eTUXFdLCFg1LFVFhG8qUAM2aSjfTuPLB2x9XGXgQh6G"
977        }
978      ]
979    }
980    "#;
981
982    const METADATA_JSON: &str = r#"
983    {
984      "created": "2022-08-31T09:33:31Z",
985      "updated": "2022-08-31T09:33:31Z"
986    }
987    "#;
988
989    const DOCUMENT_WITH_IOTA_ID_AND_CONTROLLER_JSON: &str = r#"
990    {
991      "id": "did:iota:rms:0x7591a0bc872e3a4ab66228d65773961a7a95d2299ec8464331c80fcd86b35f38",
992      "controller": "did:iota:rms:0xfbaaa919b51112d51a8f18b1500d98f0b2e91d793bc5b27fd5ab04cb1b806343",
993      "verificationMethod": [
994        {
995          "id": "did:foo:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#issuerKey",
996          "controller": "did:bar:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
997          "type": "Ed25519VerificationKey2018",
998          "publicKeyMultibase": "zFVen3X669xLzsi6N2V91DoiyzHzg1uAgqiT8jZ9nS96Z"
999        }
1000      ]
1001    }
1002    "#;
1003
1004    let doc_not_iota_because_of_id: CoreDocument =
1005      CoreDocument::from_json(DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_ID).unwrap();
1006    let doc_not_iota_because_of_controller: CoreDocument =
1007      CoreDocument::from_json(DOC_JSON_NOT_IOTA_DOCUMENT_BECAUSE_OF_CONTROLLER).unwrap();
1008    let doc_with_iota_id_and_controller: CoreDocument =
1009      CoreDocument::from_json(DOCUMENT_WITH_IOTA_ID_AND_CONTROLLER_JSON).unwrap();
1010    let metadata: IotaDocumentMetadata = IotaDocumentMetadata::from_json(METADATA_JSON).unwrap();
1011
1012    assert!(IotaDocument::try_from((doc_not_iota_because_of_id, metadata.clone())).is_err());
1013
1014    assert!(IotaDocument::try_from((doc_not_iota_because_of_controller, metadata.clone())).is_err());
1015
1016    assert!(IotaDocument::try_from((doc_with_iota_id_and_controller, metadata)).is_ok());
1017  }
1018}