From d51910fe3bd63146e2e22a4b0f783cf838e760ce Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:57:56 +0100 Subject: [PATCH] Allow setting additional controllers for `IotaDocument`. (#1314) --- bindings/wasm/docs/api-reference.md | 111 +++++++------- bindings/wasm/src/iota/iota_document.rs | 20 +++ .../src/document/iota_document.rs | 135 ++++++++++++++++-- .../src/state_metadata/document.rs | 29 +++- 4 files changed, 235 insertions(+), 60 deletions(-) diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index e9e3a65a85..e83bbc81b6 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -187,10 +187,6 @@ working with storage backed DID documents.

## Members
-
StateMetadataEncoding
-
-
MethodRelationship
-
CredentialStatus
SubjectHolderRelationship
@@ -207,6 +203,9 @@ This variant is the default.

Any

The holder is not required to have any kind of relationship to any credential subject.

+
StatusPurpose
+

Purpose of a StatusList2021.

+
FailFast

Declares when validation should return if an error occurs.

@@ -216,9 +215,6 @@ This variant is the default.

FirstError

Return after the first error occurs.

-
StatusPurpose
-

Purpose of a StatusList2021.

-
StatusCheck

Controls validation behaviour when checking whether or not a credential has been revoked by its credentialStatus.

@@ -236,16 +232,17 @@ This variant is the default.

SkipAll

Skip all status checks.

+
StateMetadataEncoding
+
+
MethodRelationship
+
## Functions
-
encodeB64(data)string
-

Encode the given bytes in url-safe base64.

-
-
decodeB64(data)Uint8Array
-

Decode the given url-safe base64-encoded slice into its raw bytes.

+
start()
+

Initializes the console error panic hook for better error messages

verifyEd25519(alg, signingInput, decodedSignature, publicKey)

Verify a JWS signature secured with the EdDSA algorithm and curve Ed25519.

@@ -255,8 +252,11 @@ This variant is the default.

This function does not check whether alg = EdDSA in the protected header. Callers are expected to assert this prior to calling the function.

-
start()
-

Initializes the console error panic hook for better error messages

+
encodeB64(data)string
+

Encode the given bytes in url-safe base64.

+
+
decodeB64(data)Uint8Array
+

Decode the given url-safe base64-encoded slice into its raw bytes.

@@ -1967,6 +1967,7 @@ if the object is being concurrently modified. * _instance_ * [.id()](#IotaDocument+id) ⇒ [IotaDID](#IotaDID) * [.controller()](#IotaDocument+controller) ⇒ [Array.<IotaDID>](#IotaDID) + * [.setController(controllers)](#IotaDocument+setController) * [.alsoKnownAs()](#IotaDocument+alsoKnownAs) ⇒ Array.<string> * [.setAlsoKnownAs(urls)](#IotaDocument+setAlsoKnownAs) * [.properties()](#IotaDocument+properties) ⇒ Map.<string, any> @@ -2039,6 +2040,20 @@ NOTE: controllers are determined by the `state_controller` unlock condition of t during resolution and are omitted when publishing. **Kind**: instance method of [IotaDocument](#IotaDocument) + + +### iotaDocument.setController(controllers) +Sets the controllers of the document. + +Note: Duplicates will be ignored. +Use `null` to remove all controllers. + +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| controllers | [CoreDID](#CoreDID) \| [Array.<CoreDID>](#CoreDID) \| null | + ### iotaDocument.alsoKnownAs() ⇒ Array.<string> @@ -6104,14 +6119,6 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | - - -## StateMetadataEncoding -**Kind**: global variable - - -## MethodRelationship -**Kind**: global variable ## CredentialStatus @@ -6142,6 +6149,12 @@ The holder must match the subject only for credentials where the [`nonTransferab ## Any The holder is not required to have any kind of relationship to any credential subject. +**Kind**: global variable + + +## StatusPurpose +Purpose of a [StatusList2021](#StatusList2021). + **Kind**: global variable @@ -6160,12 +6173,6 @@ Return all errors that occur during validation. ## FirstError Return after the first error occurs. -**Kind**: global variable - - -## StatusPurpose -Purpose of a [StatusList2021](#StatusList2021). - **Kind**: global variable @@ -6198,28 +6205,20 @@ Validate the status if supported, skip any unsupported Skip all status checks. **Kind**: global variable - - -## encodeB64(data) ⇒ string -Encode the given bytes in url-safe base64. - -**Kind**: global function + -| Param | Type | -| --- | --- | -| data | Uint8Array | +## StateMetadataEncoding +**Kind**: global variable + - +## MethodRelationship +**Kind**: global variable + -## decodeB64(data) ⇒ Uint8Array -Decode the given url-safe base64-encoded slice into its raw bytes. +## start() +Initializes the console error panic hook for better error messages **Kind**: global function - -| Param | Type | -| --- | --- | -| data | Uint8Array | - ## verifyEd25519(alg, signingInput, decodedSignature, publicKey) @@ -6242,9 +6241,25 @@ prior to calling the function. | decodedSignature | Uint8Array | | publicKey | [Jwk](#Jwk) | - + -## start() -Initializes the console error panic hook for better error messages +## encodeB64(data) ⇒ string +Encode the given bytes in url-safe base64. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| data | Uint8Array | + + + +## decodeB64(data) ⇒ Uint8Array +Decode the given url-safe base64-encoded slice into its raw bytes. **Kind**: global function + +| Param | Type | +| --- | --- | +| data | Uint8Array | + diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 8f8cbe6823..8d004422ad 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -5,12 +5,14 @@ use std::rc::Rc; use identity_iota::core::Object; use identity_iota::core::OneOrMany; + use identity_iota::core::OrderedSet; use identity_iota::core::Timestamp; use identity_iota::core::Url; use identity_iota::credential::Credential; use identity_iota::credential::JwtPresentationOptions; use identity_iota::credential::Presentation; + use identity_iota::did::DIDUrl; use identity_iota::iota::block::output::dto::AliasOutputDto; use identity_iota::iota::block::output::AliasOutput; @@ -48,6 +50,7 @@ use crate::credential::WasmJws; use crate::credential::WasmJwt; use crate::credential::WasmPresentation; use crate::did::CoreDocumentLock; + use crate::did::PromiseJws; use crate::did::PromiseJwt; use crate::did::WasmCoreDocument; @@ -156,6 +159,20 @@ impl WasmIotaDocument { ) } + /// Sets the controllers of the document. + /// + /// Note: Duplicates will be ignored. + /// Use `null` to remove all controllers. + #[wasm_bindgen(js_name = setController)] + pub fn set_controller(&mut self, controller: &OptionArrayIotaDID) -> Result<()> { + let controller: Option> = controller.into_serde().wasm_result()?; + match controller { + Some(controller) => self.0.try_write()?.set_controller(controller), + None => self.0.try_write()?.set_controller([]), + }; + Ok(()) + } + /// Returns a copy of the document's `alsoKnownAs` set. #[wasm_bindgen(js_name = alsoKnownAs)] pub fn also_known_as(&self) -> Result { @@ -845,6 +862,9 @@ impl From for WasmIotaDocument { #[wasm_bindgen] extern "C" { + #[wasm_bindgen(typescript_type = "IotaDID[] | null")] + pub type OptionArrayIotaDID; + #[wasm_bindgen(typescript_type = "IotaDID[]")] pub type ArrayIotaDID; diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 89abf06cf5..7ae60381d7 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -5,7 +5,6 @@ use core::fmt; use core::fmt::Debug; use core::fmt::Display; use identity_credential::credential::Jws; -#[cfg(feature = "client")] use identity_did::CoreDID; use identity_did::DIDUrl; use identity_document::verifiable::JwsVerificationOptions; @@ -15,7 +14,6 @@ use serde::Deserialize; use serde::Serialize; use identity_core::common::Object; -#[cfg(feature = "client")] use identity_core::common::OneOrSet; use identity_core::common::OrderedSet; use identity_core::common::Url; @@ -123,9 +121,6 @@ impl IotaDocument { } /// Returns an iterator yielding the DID controllers. - /// - /// NOTE: controllers are determined by the `state_controller` unlock condition of the output - /// during resolution and are omitted when publishing. pub fn controller(&self) -> impl Iterator + '_ { let core_did_controller_iter = self .document @@ -134,11 +129,31 @@ impl IotaDocument { .into_iter() .flatten(); - // CORRECTNESS: These casts are OK because the public API does not expose methods - // enabling unchecked mutation of the controllers. + // CORRECTNESS: These casts are OK because the public API only allows setting IotaDIDs. core_did_controller_iter.map(IotaDID::from_inner_ref_unchecked) } + /// Sets the value of the document controller. + /// + /// Note: + /// * Duplicates in `controller` will be ignored. + /// * Use an empty collection to clear all controllers. + pub fn set_controller(&mut self, controller: T) + where + T: IntoIterator, + { + let controller_core_dids: Option> = { + let controller_set: OrderedSet = controller.into_iter().map(CoreDID::from).collect(); + if controller_set.is_empty() { + None + } else { + Some(OneOrSet::new_set(controller_set).expect("controller is checked to be not empty")) + } + }; + + *self.document.controller_mut() = controller_core_dids; + } + /// Returns a reference to the `alsoKnownAs` set. pub fn also_known_as(&self) -> &OrderedSet { self.document.also_known_as() @@ -442,7 +457,14 @@ mod client_document { _ => None, }; - *self.core_document_mut().controller_mut() = controller_did.map(CoreDID::from).map(OneOrSet::new_one); + if let Some(controller_did) = controller_did { + match self.core_document_mut().controller_mut() { + Some(controllers) => { + controllers.append(CoreDID::from(controller_did)); + } + None => *self.core_document_mut().controller_mut() = Some(OneOrSet::new_one(CoreDID::from(controller_did))), + } + } Ok(()) } @@ -731,6 +753,98 @@ mod tests { assert_eq!(doc1, doc2); } + #[test] + fn test_unpack_no_external_controller() { + let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .parse() + .unwrap(); + let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + .parse() + .unwrap(); + + let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); + original_doc.set_controller([]); + + let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) + .with_state_metadata(original_doc.pack().unwrap()) + .add_unlock_condition(UnlockCondition::StateControllerAddress( + StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), + )) + .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( + Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), + ))) + .finish() + .unwrap(); + + let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); + let controllers: Vec = document.controller().cloned().collect::>(); + assert_eq!(controllers.first().unwrap(), &alias_controller); + assert_eq!(controllers.len(), 1); + } + + #[test] + fn test_unpack_with_duplicate_controller() { + let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .parse() + .unwrap(); + let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + .parse() + .unwrap(); + + let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); + original_doc.set_controller([alias_controller.clone()]); + + let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) + .with_state_metadata(original_doc.pack().unwrap()) + .add_unlock_condition(UnlockCondition::StateControllerAddress( + StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), + )) + .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( + Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), + ))) + .finish() + .unwrap(); + + let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); + let controllers: Vec = document.controller().cloned().collect::>(); + assert_eq!(controllers.first().unwrap(), &alias_controller); + assert_eq!(controllers.len(), 1); + } + + #[test] + fn test_unpack_with_external_controller() { + let document_did: IotaDID = "did:iota:0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .parse() + .unwrap(); + let alias_controller: IotaDID = "did:iota:0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + .parse() + .unwrap(); + let external_controller_did: IotaDID = + "did:iota:0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + .parse() + .unwrap(); + + let mut original_doc: IotaDocument = IotaDocument::new_with_id(document_did.clone()); + original_doc.set_controller([external_controller_did.clone()]); + + let alias_output: AliasOutput = AliasOutputBuilder::new_with_amount(1, AliasId::from(&document_did)) + .with_state_metadata(original_doc.pack().unwrap()) + .add_unlock_condition(UnlockCondition::StateControllerAddress( + StateControllerAddressUnlockCondition::new(Address::Alias(AliasAddress::new(AliasId::from(&alias_controller)))), + )) + .add_unlock_condition(UnlockCondition::GovernorAddress(GovernorAddressUnlockCondition::new( + Address::Alias(AliasAddress::new(AliasId::from(&alias_controller))), + ))) + .finish() + .unwrap(); + + let document: IotaDocument = IotaDocument::unpack_from_output(&document_did, &alias_output, true).unwrap(); + let controllers: Vec = document.controller().cloned().collect::>(); + assert_eq!(controllers.first().unwrap(), &external_controller_did); + assert_eq!(controllers.get(1).unwrap(), &alias_controller); + assert_eq!(controllers.len(), 2); + } + #[test] fn test_unpack_empty() { let controller_did: IotaDID = valid_did(); @@ -765,7 +879,10 @@ mod tests { let packed: Vec = document.pack_with_encoding(StateMetadataEncoding::Json).unwrap(); let state_metadata_document: StateMetadataDocument = StateMetadataDocument::unpack(&packed).unwrap(); let unpacked_document: IotaDocument = state_metadata_document.into_iota_document(&did).unwrap(); - assert!(unpacked_document.document.controller().is_none()); + assert_eq!( + unpacked_document.document.controller().unwrap().get(0).unwrap().clone(), + CoreDID::from(controller_did) + ); assert!(unpacked_document.metadata.state_controller_address.is_none()); assert!(unpacked_document.metadata.governor_address.is_none()); } diff --git a/identity_iota_core/src/state_metadata/document.rs b/identity_iota_core/src/state_metadata/document.rs index d15f0d8d26..e14e381f5b 100644 --- a/identity_iota_core/src/state_metadata/document.rs +++ b/identity_iota_core/src/state_metadata/document.rs @@ -79,7 +79,6 @@ impl StateMetadataDocument { // Unset Governor and State Controller Addresses to avoid bloating the payload self.metadata.governor_address = None; self.metadata.state_controller_address = None; - *self.document.controller_mut() = None; let encoded_message_data: Vec = match encoding { StateMetadataEncoding::Json => self @@ -410,8 +409,7 @@ mod tests { let TestSetup { document, .. } = test_document(); let mut state_metadata_doc: StateMetadataDocument = StateMetadataDocument::from(document); let packed: Vec = state_metadata_doc.clone().pack(StateMetadataEncoding::Json).unwrap(); - // Controller and State Controller are set to None when packing - *state_metadata_doc.document.controller_mut() = None; + // Governor and State Controller are set to None when packing state_metadata_doc.metadata.governor_address = None; state_metadata_doc.metadata.state_controller_address = None; let expected_payload: String = format!( @@ -434,6 +432,31 @@ mod tests { assert_eq!(&packed[7..], expected_payload.as_bytes()); } + #[test] + fn test_no_controller() { + let TestSetup { + mut document, did_self, .. + } = test_document(); + *document.core_document_mut().controller_mut() = None; + let state_metadata_doc: StateMetadataDocument = StateMetadataDocument::from(document); + let packed: Vec = state_metadata_doc.clone().pack(StateMetadataEncoding::Json).unwrap(); + let expected_payload: String = format!( + "{{\"doc\":{},\"meta\":{}}}", + state_metadata_doc.document, state_metadata_doc.metadata + ); + assert_eq!(&packed[7..], expected_payload.as_bytes()); + let unpacked = StateMetadataDocument::unpack(&packed).unwrap(); + assert_eq!( + unpacked + .into_iota_document(&did_self) + .unwrap() + .controller() + .collect::>() + .len(), + 0 + ); + } + #[test] fn test_unpack_length_prefix() { // Changing the serialization is a breaking change!