From 24de8d80d8b0cb7d94c648dd28ccac641f5631d0 Mon Sep 17 00:00:00 2001 From: Philipp Date: Wed, 17 Nov 2021 08:51:59 +0100 Subject: [PATCH] Refactor the `Account` state (#462) * Get rid of `IdentitySnapshot` * Start replacing parts of `IdentityState` * Create identity document from core structures * Impl int/diff update determination * Remove `Deref` impls for IdentityState * Implement `CreateMethod` update * Impl attach/detach `MethodRelationship` * Update to cap inv refactor * Use `IotaDocument` directly to construct the doc * Update tests for account updates * Remove unused types & variables * Return to unified publish method * Implement & test attaching relationships * Fix attach/detach tests post-merge * Implement detach relationship test * Implement insert and remove service * Move `Publish` to low-level API * Remove did field from account * Refactor publishing & storing * Rename int/diff generations * Change fresh id detection & fix `store_state` call * Factor out `ChainState` from `IdentityState` * Remove `this_message_id` from chain state * Test the chain state * Fix some error TODOs * Fix some TODOs * Fix some storage todos/errors * Make `IotaDocument::remove_method` fallible again * Use name instead of identifier to call from_did * Move state loading into the else clause * Remove `persist` flag on `process_update` * Remove unnecessary cloning * Rename `as_document` -> `document` * Improve `ChainState` docs * Fix post-merge things & clippy lint * Remove `Tiny*` and other unused state types * `InvalidMethodTarget` -> `InvalidTargetMethod` * `as_document_mut` -> `document_mut` * Expand lazy test * Rename impl_command_builder to update * Remove event context * Rename `events` module to `updates` * Apply fmt * Rename `commands` -> `updates` * Remove `UnixTimestamp` * Remove unused errors in account & update * Change `fromDID` method in Wasm * Return `Option` from constructor * Use strum, add todo!(), rename `PublishType` Co-authored-by: Craig Bester * Move attach/detach relationship to `CoreDocument` * Return bool from `detach_method_relationship` * Use `MethodQuery` in attach/detach Co-authored-by: Craig Bester * Revert "Change `fromDID` method in Wasm" This reverts commit cb0cf3574842e9d17c161b3ef65444d6d98cdf4c. * Nest `MethodRelationship` in `MethodScope` * Move some chain state logic to publish_diff_change * Don't expect, bubble up * Return early if there's nothing to publish * Add todo about error handling * Remove mut state methods * Move account constructors to the top * Use `try_resolve_method_with_scope` in tests * Remove todos, inline code, explicitly ignore res. * Remove todo comments, `unwrap` * Rename chain state fields to `last_*` * Improve `InvalidTargetMethod` Error name * Increment actions when updating * check for updated referenced method in publish * Enable diff updates for merkle cap. inv. methods * Only attach relationship on non-embedded methods * Add proper type annotations in publish * Improve chain state docs & variable naming Co-authored-by: Craig Bester --- .../wasm/src/did/wasm_verification_method.rs | 2 +- examples/account/create_did.rs | 2 +- examples/account/manipulate_did.rs | 6 +- examples/low-level-api/common.rs | 3 +- examples/low-level-api/manipulate_did.rs | 3 +- examples/low-level-api/resolve_history.rs | 4 +- examples/low-level-api/revoke_vc.rs | 3 +- identity-account/src/account/account.rs | 501 ++++++-------- identity-account/src/error.rs | 28 +- identity-account/src/events/commit.rs | 46 -- identity-account/src/events/context.rs | 36 - identity-account/src/events/event.rs | 125 ---- identity-account/src/events/update.rs | 385 ----------- identity-account/src/identity/chain_state.rs | 64 ++ .../src/identity/identity_snapshot.rs | 37 - .../src/identity/identity_state.rs | 610 +--------------- identity-account/src/identity/mod.rs | 4 +- identity-account/src/lib.rs | 2 +- identity-account/src/storage/memstore.rs | 58 +- identity-account/src/storage/stronghold.rs | 185 +---- identity-account/src/storage/traits.rs | 55 +- identity-account/src/tests/account.rs | 46 ++ identity-account/src/tests/commands.rs | 299 -------- identity-account/src/tests/lazy.rs | 52 +- identity-account/src/tests/mod.rs | 2 +- identity-account/src/tests/updates.rs | 653 ++++++++++++++++++ identity-account/src/types/key_location.rs | 28 +- .../src/{events => updates}/error.rs | 5 +- .../src/{events => updates}/macros.rs | 10 +- .../src/{events => updates}/mod.rs | 6 - identity-account/src/updates/update.rs | 455 ++++++++++++ identity-core/src/common/mod.rs | 2 - identity-core/src/common/unix_timestamp.rs | 74 -- identity-did/src/document/core_document.rs | 188 ++++- identity-did/src/utils/ordered_set.rs | 11 +- .../src/verification/method_relationship.rs | 12 + identity-did/src/verification/method_scope.rs | 52 +- identity-did/src/verification/mod.rs | 2 + identity-iota/src/did/doc/iota_document.rs | 87 ++- .../src/did/doc/iota_verification_method.rs | 11 +- identity-iota/src/tangle/mod.rs | 3 + identity-iota/src/tangle/publish.rs | 265 +++++++ identity/src/lib.rs | 2 +- 43 files changed, 2159 insertions(+), 2265 deletions(-) delete mode 100644 identity-account/src/events/commit.rs delete mode 100644 identity-account/src/events/context.rs delete mode 100644 identity-account/src/events/event.rs delete mode 100644 identity-account/src/events/update.rs create mode 100644 identity-account/src/identity/chain_state.rs delete mode 100644 identity-account/src/identity/identity_snapshot.rs delete mode 100644 identity-account/src/tests/commands.rs create mode 100644 identity-account/src/tests/updates.rs rename identity-account/src/{events => updates}/error.rs (86%) rename identity-account/src/{events => updates}/macros.rs (86%) rename identity-account/src/{events => updates}/mod.rs (61%) create mode 100644 identity-account/src/updates/update.rs delete mode 100644 identity-core/src/common/unix_timestamp.rs create mode 100644 identity-did/src/verification/method_relationship.rs create mode 100644 identity-iota/src/tangle/publish.rs diff --git a/bindings/wasm/src/did/wasm_verification_method.rs b/bindings/wasm/src/did/wasm_verification_method.rs index 59dd3f0609..4399b696f0 100644 --- a/bindings/wasm/src/did/wasm_verification_method.rs +++ b/bindings/wasm/src/did/wasm_verification_method.rs @@ -31,7 +31,7 @@ impl WasmVerificationMethod { /// Creates a new `VerificationMethod` object from the given `did` and `key`. #[wasm_bindgen(js_name = fromDID)] pub fn from_did(did: &WasmDID, key: &KeyPair, fragment: String) -> Result { - IotaVerificationMethod::from_did(did.0.clone(), &key.0, &fragment) + IotaVerificationMethod::from_did(did.0.clone(), key.0.type_(), key.0.public(), &fragment) .map_err(wasm_error) .map(Self) } diff --git a/examples/account/create_did.rs b/examples/account/create_did.rs index 423937d044..115a5a0d67 100644 --- a/examples/account/create_did.rs +++ b/examples/account/create_did.rs @@ -38,7 +38,7 @@ async fn main() -> Result<()> { println!( "[Example] Local Document from {} = {:#?}", iota_did, - account.state().await?.to_document() + account.state().document() ); // Prints the Identity Resolver Explorer URL. diff --git a/examples/account/manipulate_did.rs b/examples/account/manipulate_did.rs index faa1cc3a84..b096b92cf5 100644 --- a/examples/account/manipulate_did.rs +++ b/examples/account/manipulate_did.rs @@ -10,7 +10,7 @@ use identity::account::AccountStorage; use identity::account::IdentitySetup; use identity::account::Result; use identity::core::Url; -use identity::did::MethodScope; +use identity::did::MethodRelationship; use identity::iota::IotaDID; #[tokio::main] @@ -48,8 +48,8 @@ async fn main() -> Result<()> { .update_identity() .attach_method() .fragment("my-next-key") - .scope(MethodScope::CapabilityDelegation) - .scope(MethodScope::CapabilityInvocation) + .relationship(MethodRelationship::CapabilityDelegation) + .relationship(MethodRelationship::CapabilityInvocation) .apply() .await?; diff --git a/examples/low-level-api/common.rs b/examples/low-level-api/common.rs index 373b06a135..6c6c334dd4 100644 --- a/examples/low-level-api/common.rs +++ b/examples/low-level-api/common.rs @@ -76,7 +76,8 @@ pub async fn add_new_key( // Add #newKey to the document let new_key: KeyPair = KeyPair::new_ed25519()?; - let method: IotaVerificationMethod = IotaVerificationMethod::from_did(updated_doc.did().clone(), &new_key, "newKey")?; + let method: IotaVerificationMethod = + IotaVerificationMethod::from_did(updated_doc.did().clone(), new_key.type_(), new_key.public(), "newKey")?; assert!(updated_doc.insert_method(method, MethodScope::VerificationMethod)); // Prepare the update diff --git a/examples/low-level-api/manipulate_did.rs b/examples/low-level-api/manipulate_did.rs index e32e608aa3..73eb81b6e9 100644 --- a/examples/low-level-api/manipulate_did.rs +++ b/examples/low-level-api/manipulate_did.rs @@ -29,7 +29,8 @@ pub async fn run() -> Result<(IotaDocument, KeyPair, KeyPair, Receipt, Receipt)> // Add a new VerificationMethod with a new keypair let new_key: KeyPair = KeyPair::new_ed25519()?; - let method: IotaVerificationMethod = IotaVerificationMethod::from_did(document.did().clone(), &new_key, "newKey")?; + let method: IotaVerificationMethod = + IotaVerificationMethod::from_did(document.did().clone(), new_key.type_(), new_key.public(), "newKey")?; assert!(document.insert_method(method, MethodScope::VerificationMethod)); // Add a new Service diff --git a/examples/low-level-api/resolve_history.rs b/examples/low-level-api/resolve_history.rs index e04962ff9c..b29b79eea7 100644 --- a/examples/low-level-api/resolve_history.rs +++ b/examples/low-level-api/resolve_history.rs @@ -61,7 +61,7 @@ async fn main() -> Result<()> { // Add a new VerificationMethod with a new KeyPair, with the tag "keys-1" let keys_1: KeyPair = KeyPair::new_ed25519()?; - let method_1: IotaVerificationMethod = IotaVerificationMethod::from_did(int_doc_1.id().clone(), &keys_1, "keys-1")?; + let method_1: IotaVerificationMethod = IotaVerificationMethod::from_did(int_doc_1.id().clone(), keys_1.type_(), keys_1.public(), "keys-1")?; assert!(int_doc_1.insert_method(method_1, MethodScope::VerificationMethod)); // Add the `message_id` of the previous message in the chain. @@ -176,7 +176,7 @@ async fn main() -> Result<()> { // Add a VerificationMethod with a new KeyPair, called "keys-2" let keys_2: KeyPair = KeyPair::new_ed25519()?; - let method_2: IotaVerificationMethod = IotaVerificationMethod::from_did(int_doc_2.id().clone(), &keys_2, "keys-2")?; + let method_2: IotaVerificationMethod = IotaVerificationMethod::from_did(int_doc_2.id().clone(), keys_2.type_(), keys_2.public(), "keys-2")?; assert!(int_doc_2.insert_method(method_2, MethodScope::VerificationMethod)); // Note: the `previous_message_id` points to the `message_id` of the last integration chain diff --git a/examples/low-level-api/revoke_vc.rs b/examples/low-level-api/revoke_vc.rs index a77465c8ff..376822c73f 100644 --- a/examples/low-level-api/revoke_vc.rs +++ b/examples/low-level-api/revoke_vc.rs @@ -104,7 +104,8 @@ pub async fn add_new_key( // Add #newKey to the document let new_key: KeyPair = KeyPair::new_ed25519()?; - let method: IotaVerificationMethod = IotaVerificationMethod::from_did(updated_doc.did().clone(), &new_key, "newKey")?; + let method: IotaVerificationMethod = + IotaVerificationMethod::from_did(updated_doc.did().clone(), new_key.type_(), new_key.public(), "newKey")?; assert!(updated_doc.insert_method(method, MethodScope::VerificationMethod)); // Prepare the update diff --git a/identity-account/src/account/account.rs b/identity-account/src/account/account.rs index 91fb14a205..dd26679146 100644 --- a/identity-account/src/account/account.rs +++ b/identity-account/src/account/account.rs @@ -2,18 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 use futures::executor; -use futures::StreamExt; -use futures::TryStreamExt; -use identity_core::common::Fragment; -use identity_core::crypto::KeyType; + use identity_core::crypto::SetSignature; -use identity_did::verification::MethodType; use identity_iota::did::DocumentDiff; use identity_iota::did::IotaDID; use identity_iota::did::IotaDocument; +use identity_iota::did::IotaVerificationMethod; use identity_iota::tangle::Client; use identity_iota::tangle::ClientMap; use identity_iota::tangle::MessageId; +use identity_iota::tangle::MessageIdExt; +use identity_iota::tangle::PublishType; +use identity_iota::tangle::TangleRef; use identity_iota::tangle::TangleResolve; use serde::Serialize; use std::sync::atomic::AtomicUsize; @@ -22,29 +22,21 @@ use std::sync::Arc; use crate::account::AccountBuilder; use crate::error::Result; -use crate::events::Commit; -use crate::events::Context; -use crate::events::CreateIdentity; -use crate::events::Event; -use crate::events::EventData; -use crate::events::Update; +use crate::identity::ChainState; use crate::identity::DIDLease; use crate::identity::IdentitySetup; -use crate::identity::IdentitySnapshot; use crate::identity::IdentityState; use crate::identity::IdentityUpdater; -use crate::identity::TinyMethod; use crate::storage::Storage; -use crate::types::Generation; use crate::types::KeyLocation; +use crate::updates::create_identity; +use crate::updates::Update; use crate::Error; use super::config::AccountSetup; use super::config::AutoSave; use super::AccountConfig; -const OSC: Ordering = Ordering::SeqCst; - /// An account manages one identity. /// /// It handles private keys, writing to storage and @@ -55,38 +47,71 @@ pub struct Account { storage: Arc, client_map: Arc, actions: AtomicUsize, - did: IotaDID, + chain_state: ChainState, + state: IdentityState, did_lease: DIDLease, } impl Account { + // =========================================================================== + // Constructors + // =========================================================================== + /// Creates a new [AccountBuilder]. pub fn builder() -> AccountBuilder { AccountBuilder::new() } - /// Creates an [`Account`] for an existing identity, if it exists in the [`Storage`]. - pub(crate) async fn load_identity(setup: AccountSetup, did: IotaDID) -> Result { - // Ensure the did exists in storage - setup.storage.snapshot(&did).await?.ok_or(Error::IdentityNotFound)?; - - let did_lease = setup.storage.lease_did(&did).await?; - - Self::with_setup(setup, did, did_lease).await - } - /// Creates a new `Account` instance with the given `config`. - async fn with_setup(setup: AccountSetup, did: IotaDID, did_lease: DIDLease) -> Result { + async fn with_setup( + setup: AccountSetup, + chain_state: ChainState, + state: IdentityState, + did_lease: DIDLease, + ) -> Result { Ok(Self { config: setup.config, storage: setup.storage, client_map: setup.client_map, actions: AtomicUsize::new(0), - did, + chain_state, + state, did_lease, }) } + /// Creates a new identity and returns an [`Account`] instance to manage it. + /// The identity is stored locally in the [`Storage`] given in [`AccountSetup`], and published + /// using the [`ClientMap`]. + /// + /// See [`IdentityCreate`] to customize the identity creation. + pub(crate) async fn create_identity(setup: AccountSetup, input: IdentitySetup) -> Result { + let (did_lease, state): (DIDLease, IdentityState) = create_identity(input, setup.storage.as_ref()).await?; + + let mut account = Self::with_setup(setup, ChainState::new(), state, did_lease).await?; + + account.store_state().await?; + + account.publish(false).await?; + + Ok(account) + } + + /// Creates an [`Account`] for an existing identity, if it exists in the [`Storage`]. + pub(crate) async fn load_identity(setup: AccountSetup, did: IotaDID) -> Result { + // Ensure the did exists in storage + let state = setup.storage.state(&did).await?.ok_or(Error::IdentityNotFound)?; + let chain_state = setup.storage.chain_state(&did).await?.ok_or(Error::IdentityNotFound)?; + + let did_lease = setup.storage.lease_did(&did).await?; + + Self::with_setup(setup, chain_state, state, did_lease).await + } + + // =========================================================================== + // Getters & Setters + // =========================================================================== + /// Returns a reference to the [Storage] implementation. pub fn storage(&self) -> &dyn Storage { self.storage.as_ref() @@ -109,7 +134,12 @@ impl Account { /// Returns the total number of actions executed by this instance. pub fn actions(&self) -> usize { - self.actions.load(OSC) + self.actions.load(Ordering::SeqCst) + } + + /// Increments the total number of actions executed by this instance. + fn increment_actions(&self) { + self.actions.fetch_add(1, Ordering::SeqCst); } /// Adds a pre-configured `Client` for Tangle interactions. @@ -119,52 +149,32 @@ impl Account { /// Returns the did of the managed identity. pub fn did(&self) -> &IotaDID { - &self.did + self.document().did() } - // =========================================================================== - // Identity - // =========================================================================== - - /// Return a copy of the latest state of the identity - pub async fn state(&self) -> Result { - Ok(self.load_snapshot().await?.into_identity()) + /// Return the latest state of the identity. + pub fn state(&self) -> &IdentityState { + &self.state } - /// Resolves the DID Document associated with this `Account` from the Tangle. - pub async fn resolve_identity(&self) -> Result { - let snapshot: IdentitySnapshot = self.load_snapshot().await?; - let did: &IotaDID = snapshot.identity().try_did()?; - - // Fetch the DID Document from the Tangle - self.client_map.resolve(did).await.map_err(Into::into) + /// Return the chain state of the identity. + pub fn chain_state(&self) -> &ChainState { + &self.chain_state } - /// Creates a new identity and returns an [`Account`] instance to manage it. - /// The identity is stored locally in the [`Storage`] given in [`AccountSetup`], and published - /// using the [`ClientMap`]. - /// - /// See [`IdentityCreate`] to customize the identity creation. - pub(crate) async fn create_identity(setup: AccountSetup, input: IdentitySetup) -> Result { - let command = CreateIdentity { - network: input.network, - method_secret: input.method_secret, - method_type: Self::key_to_method(input.key_type), - }; - - let snapshot = IdentitySnapshot::new(IdentityState::new()); - - let (did, did_lease, events): (IotaDID, DIDLease, Vec) = command - .process(snapshot.identity().integration_generation(), setup.storage.as_ref()) - .await?; - - let commits = Self::commit_events(&did, &setup.config, setup.storage.as_ref(), &snapshot, &events).await?; - - let account = Self::with_setup(setup, did, did_lease).await?; + /// Returns the DID document of the identity, which this account manages, + /// with all updates applied. + pub fn document(&self) -> &IotaDocument { + self.state.document() + } - account.publish_commits(snapshot, commits, true).await?; + // =========================================================================== + // Identity + // =========================================================================== - Ok(account) + /// Resolves the DID Document associated with this `Account` from the Tangle. + pub async fn resolve_identity(&self) -> Result { + self.client_map.resolve(self.did()).await.map_err(Into::into) } /// Returns the [`IdentityUpdater`] for this identity. @@ -193,14 +203,20 @@ impl Account { where U: Serialize + SetSignature, { - let snapshot: IdentitySnapshot = self.load_snapshot().await?; - let state: &IdentityState = snapshot.identity(); + let state: &IdentityState = self.state(); + + let method: &IotaVerificationMethod = state.document().resolve_method(fragment).ok_or(Error::MethodNotFound)?; + + let location: KeyLocation = state.method_location(method.key_type(), fragment.to_owned())?; - let fragment: Fragment = Fragment::new(fragment); - let method: &TinyMethod = state.methods().fetch(fragment.name())?; - let location: &KeyLocation = method.location(); + state.sign_data(self.did(), self.storage(), &location, target).await?; - state.sign_data(self.did(), self.storage(), location, target).await?; + Ok(()) + } + + /// Push all unpublished changes to the tangle in a single message. + pub async fn publish_updates(&mut self) -> Result<()> { + self.publish(true).await?; Ok(()) } @@ -208,43 +224,24 @@ impl Account { // =========================================================================== // Misc. Private // =========================================================================== - pub(crate) async fn process_update(&self, command: Update, persist: bool) -> Result<()> { - // Load the latest state snapshot from storage - let root: IdentitySnapshot = self.load_snapshot().await?; - debug!("[Account::process] Root = {:#?}", root); - - // Process the command with a read-only view of the state - let context: Context<'_> = Context::new(self.did(), root.identity(), self.storage()); - let events: Option> = command.process(context).await?; - - debug!("[Account::process] Events = {:#?}", events); - - if let Some(events) = events { - let commits: Vec = Self::commit_events(self.did(), &self.config, self.storage(), &root, &events).await?; - self.publish_commits(root, commits, persist).await?; - } - - Ok(()) + #[doc(hidden)] + pub async fn load_state(&self) -> Result { + // TODO: An account always holds a valid identity, + // so if None is returned, that's a broken invariant. + // This should be mapped to a fatal error in the future. + self.storage().state(self.did()).await?.ok_or(Error::IdentityNotFound) } - /// Publishes the given commits to the tangle. - pub(crate) async fn publish_commits( - &self, - root: IdentitySnapshot, - commits: Vec, - persist: bool, - ) -> Result<()> { - debug!("[Account::process] Commits = {:#?}", commits); + pub(crate) async fn process_update(&mut self, update: Update) -> Result<()> { + let did = self.did().to_owned(); + let storage = Arc::clone(&self.storage); - self.publish(root, commits, false).await?; + update.process(&did, &mut self.state, storage.as_ref()).await?; - // Update the total number of executions - self.actions.fetch_add(1, OSC); + self.increment_actions(); - if persist { - self.save(false).await?; - } + self.publish(false).await?; Ok(()) } @@ -255,166 +252,155 @@ impl Account { new_state: &IdentityState, document: &mut IotaDocument, ) -> Result<()> { - if new_state.integration_generation() == Generation::new() { - let method: &TinyMethod = new_state.capability_invocation()?; - let location: &KeyLocation = method.location(); + if self.chain_state().is_new_identity() { + let method: &IotaVerificationMethod = new_state.document().default_signing_method()?; + let location: KeyLocation = new_state.method_location( + method.key_type(), + // TODO: Should be a fatal error. + method.id().fragment().ok_or(Error::MethodMissingFragment)?.to_owned(), + )?; // Sign the DID Document with the current capability invocation method new_state - .sign_data(self.did(), self.storage(), location, document) + .sign_data(self.did(), self.storage(), &location, document) .await?; } else { - let method: &TinyMethod = old_state.capability_invocation()?; - let location: &KeyLocation = method.location(); + let method: &IotaVerificationMethod = old_state.document().default_signing_method()?; + let location: KeyLocation = new_state.method_location( + method.key_type(), + // TODO: Should be a fatal error. + method.id().fragment().ok_or(Error::MethodMissingFragment)?.to_owned(), + )?; // Sign the DID Document with the previous capability invocation method old_state - .sign_data(self.did(), self.storage(), location, document) + .sign_data(self.did(), self.storage(), &location, document) .await?; } Ok(()) } - async fn process_integration_change(&self, old_root: IdentitySnapshot) -> Result<()> { - let new_root: IdentitySnapshot = self.load_snapshot().await?; - - let old_state: &IdentityState = old_root.identity(); - let new_state: &IdentityState = new_root.identity(); - - let mut new_doc: IotaDocument = new_state.to_document()?; - - self.sign_self(old_state, new_state, &mut new_doc).await?; + /// Publishes according to the autopublish configuration. + async fn publish(&mut self, force: bool) -> Result<()> { + if !force && !self.config.autopublish { + return Ok(()); + } - let message: MessageId = if self.config.testmode { - MessageId::null() + if self.chain_state().is_new_identity() { + // New identity + self.publish_integration_change(None).await?; } else { - self.client_map.publish_document(&new_doc).await?.into() - }; + // Existing identity + let old_state: IdentityState = self.load_state().await?; + let new_state: &IdentityState = self.state(); + + match PublishType::new(old_state.document(), new_state.document()) { + Some(PublishType::Integration) => self.publish_integration_change(Some(&old_state)).await?, + Some(PublishType::Diff) => self.publish_diff_change(&old_state).await?, + None => { + // Can return early, as there is nothing new to publish or store. + return Ok(()); + } + } + } - let events: [Event; 1] = [Event::new(EventData::IntegrationMessage(message))]; + self.state.increment_generation()?; - Self::commit_events(self.did(), &self.config, self.storage(), &new_root, &events).await?; + self.store_state().await?; Ok(()) } - async fn process_diff_change(&self, old_root: IdentitySnapshot) -> Result<()> { - let new_root: IdentitySnapshot = self.load_snapshot().await?; + async fn store_state(&self) -> Result<()> { + self.storage.set_state(self.did(), self.state()).await?; + self.storage.set_chain_state(self.did(), self.chain_state()).await?; - let old_state: &IdentityState = old_root.identity(); - let new_state: &IdentityState = new_root.identity(); + self.save(false).await?; - let old_doc: IotaDocument = old_state.to_document()?; - let new_doc: IotaDocument = new_state.to_document()?; + Ok(()) + } - let diff_id: &MessageId = old_state.diff_message_id(); + async fn publish_integration_change(&mut self, old_state: Option<&IdentityState>) -> Result<()> { + log::debug!("[publish_integration_change] publishing {:?}", self.document().did()); - let mut diff: DocumentDiff = DocumentDiff::new(&old_doc, &new_doc, *diff_id)?; + let new_state: &IdentityState = self.state(); - // Sign the update using a capability invocation method. - let method: &TinyMethod = old_state.capability_invocation()?; - let location: &KeyLocation = method.location(); + let mut new_doc: IotaDocument = new_state.document().to_owned(); - old_state - .sign_data(self.did(), self.storage(), location, &mut diff) + new_doc.set_previous_message_id(*self.chain_state().last_integration_message_id()); + + self + .sign_self(old_state.unwrap_or(new_state), new_state, &mut new_doc) .await?; - let message: MessageId = if self.config.testmode { - MessageId::null() + log::debug!( + "[publish_integration_change] publishing on index {}", + new_doc.integration_index() + ); + + let message_id: MessageId = if self.config.testmode { + // Fake publishing by returning a random message id. + MessageId::new(unsafe { crypto::utils::rand::gen::<[u8; 32]>().unwrap() }) } else { - self - .client_map - .publish_diff(old_state.this_message_id(), &diff) - .await? - .into() + self.client_map.publish_document(&new_doc).await?.into() }; - let events: [Event; 1] = [Event::new(EventData::DiffMessage(message))]; - - Self::commit_events(self.did(), &self.config, self.storage(), &new_root, &events).await?; + self.chain_state.set_last_integration_message_id(message_id); Ok(()) } - async fn fold_snapshot(snapshot: IdentitySnapshot, commit: Commit) -> Result { - Ok(IdentitySnapshot { - sequence: commit.sequence().max(snapshot.sequence), - identity: commit.into_event().apply(snapshot.identity).await?, - }) - } - - #[doc(hidden)] - pub async fn load_snapshot(&self) -> Result { - // Retrieve the state snapshot from storage or create a new one. - let initial: IdentitySnapshot = self - .storage() - .snapshot(self.did()) - .await? - .unwrap_or_else(|| IdentitySnapshot::new(IdentityState::new())); - - // Apply all recent events to the state and create a new snapshot - self - .storage() - .stream(self.did(), initial.sequence()) - .await? - .try_fold(initial, Self::fold_snapshot) - .await - } + async fn publish_diff_change(&mut self, old_state: &IdentityState) -> Result<()> { + log::debug!("[publish_diff_change] publishing {:?}", self.document().did()); - async fn load_snapshot_at(&self, generation: Generation) -> Result { - let initial: IdentitySnapshot = IdentitySnapshot::new(IdentityState::new()); + let old_doc: &IotaDocument = old_state.document(); + let new_doc: &IotaDocument = self.state().document(); - // Apply all events up to `generation` - self - .storage() - .stream(self.did(), Generation::new()) - .await? - .take(generation.to_u32() as usize) - .try_fold(initial, Self::fold_snapshot) - .await - } + let mut previous_message_id: &MessageId = self.chain_state().last_diff_message_id(); - async fn commit_events( - did: &IotaDID, - config: &AccountConfig, - storage: &dyn Storage, - state: &IdentitySnapshot, - events: &[Event], - ) -> Result> { - // Bail early if there are no new events - if events.is_empty() { - return Ok(Vec::new()); + // If there was no previous diff message, use the previous int message. + if previous_message_id.is_null() { + if !self.chain_state.last_integration_message_id().is_null() { + previous_message_id = self.chain_state.last_integration_message_id(); + } else { + // TODO: Return a fatal error about the invalid chain state. + } } - // Get the current sequence index of the snapshot - let mut sequence: Generation = state.sequence(); - let mut commits: Vec = Vec::with_capacity(events.len()); + let mut diff: DocumentDiff = DocumentDiff::new(old_doc, new_doc, *previous_message_id)?; - // Iterate over the events and create a new commit with the correct sequence - for event in events { - sequence = sequence.try_increment()?; - commits.push(Commit::new(did.clone(), sequence, event.clone())); - } + let method: &IotaVerificationMethod = old_state.document().default_signing_method()?; - // Append the list of commits to the store - storage.append(did, &commits).await?; + let location: KeyLocation = old_state.method_location( + method.key_type(), + // TODO: Should be a fatal error. + method.id().fragment().ok_or(Error::MethodMissingFragment)?.to_owned(), + )?; - // Store a snapshot every N events - if sequence.to_u32() % config.milestone == 0 { - let mut state: IdentitySnapshot = state.clone(); + old_state + .sign_data(self.did(), self.storage(), &location, &mut diff) + .await?; - // Fold the new commits into the snapshot - for commit in commits.iter().cloned() { - state = Self::fold_snapshot(state, commit).await?; - } + log::debug!( + "[publish_diff_change] publishing on index {}", + IotaDocument::diff_index(self.chain_state().last_integration_message_id())? + ); - // Store the new snapshot - storage.set_snapshot(did, &state).await?; - } + let message_id: MessageId = if self.config.testmode { + // Fake publishing by returning a random message id. + MessageId::new(unsafe { crypto::utils::rand::gen::<[u8; 32]>().unwrap() }) + } else { + self + .client_map + .publish_diff(self.chain_state().last_integration_message_id(), &diff) + .await? + .into() + }; - // Return the list of stored events - Ok(commits) + self.chain_state.set_last_diff_message_id(message_id); + + Ok(()) } async fn save(&self, force: bool) -> Result<()> { @@ -430,60 +416,6 @@ impl Account { Ok(()) } - - /// Push all unpublished changes to the tangle in a single message. - pub async fn publish_updates(&mut self) -> Result<()> { - // Get the last commit generation that was published to the tangle. - let last_published: Generation = self - .storage() - .published_generation(self.did()) - .await? - .unwrap_or_default(); - - // Get the commits that need to be published. - let commits: Vec = self.storage().collect(self.did(), last_published).await?; - - if commits.is_empty() { - return Ok(()); - } - - // Load the snapshot that represents the state on the tangle. - let snapshot: IdentitySnapshot = self.load_snapshot_at(last_published).await?; - - self.publish(snapshot, commits, true).await?; - - Ok(()) - } - - /// Publishes according to the autopublish configuration. - async fn publish(&self, snapshot: IdentitySnapshot, commits: Vec, force: bool) -> Result<()> { - if !force && !self.config.autopublish { - return Ok(()); - } - - match Publish::new(&commits) { - Publish::Integration => self.process_integration_change(snapshot).await?, - Publish::Diff => self.process_diff_change(snapshot).await?, - Publish::None => {} - } - - if !commits.is_empty() { - let last_commit_generation: Generation = commits.last().unwrap().sequence(); - // Publishing adds an AuthMessage or DiffMessage event, that contains the message id - // which is required to be set for subsequent updates. - // The next snapshot that loads the tangle state will require this message id to be set. - let generation: Generation = Generation::from_u32(last_commit_generation.to_u32() + 1); - self.storage().set_published_generation(self.did(), generation).await?; - } - - Ok(()) - } - - fn key_to_method(type_: KeyType) -> MethodType { - match type_ { - KeyType::Ed25519 => MethodType::Ed25519VerificationKey2018, - } - } } impl Drop for Account { @@ -494,30 +426,3 @@ impl Drop for Account { } } } - -// ============================================================================= -// Publish -// ============================================================================= - -#[derive(Clone, Copy, Debug)] -enum Publish { - None, - Integration, - Diff, -} - -impl Publish { - fn new(commits: &[Commit]) -> Self { - commits.iter().fold(Self::None, Self::apply) - } - - const fn apply(self, commit: &Commit) -> Self { - match (self, commit.event().data()) { - (Self::Integration, _) => Self::Integration, - (_, EventData::IdentityCreated(..)) => Self::Integration, - (_, EventData::IntegrationMessage(_)) => self, - (_, EventData::DiffMessage(_)) => self, - (_, _) => Self::Diff, - } - } -} diff --git a/identity-account/src/error.rs b/identity-account/src/error.rs index 0726ef5777..6f44d6884f 100644 --- a/identity-account/src/error.rs +++ b/identity-account/src/error.rs @@ -58,39 +58,21 @@ pub enum Error { /// Caused by attempting to decrement a generation below the minimum value. #[error("Generation underflow")] GenerationUnderflow, - /// Caused by attempting to add a new identity when an account is at capacity. - #[error("Too many identities")] - IdentityIdOverflow, - /// Caused by attempting to parse an invalid identity id. - #[error("Invalid identity id")] - IdentityIdInvalid, - /// Caused by attempting to read a DID from an unintialized identity state. - #[error("Document id not found")] - MissingDocumentId, /// Caused by attempting to find an identity key vault that does not exist. #[error("Key vault not found")] KeyVaultNotFound, - /// Caused by attempting to find an identity key pair that does not exist. - #[error("Key pair not found")] - KeyPairNotFound, + /// Caused by attempting to find a key in storage that does not exist. + #[error("key not found")] + KeyNotFound, /// Caused by attempting to find an identity that does not exist. #[error("Identity not found")] IdentityNotFound, - /// Caused by attempting to find an identity event that does not exist. - #[error("Event not found")] - EventNotFound, - /// Caused by attempting to re-initialize an existing identity. - #[error("Identity already exists")] - IdentityAlreadyExists, /// Caused by attempting to find a verification method that does not exist. #[error("Verification Method not found")] MethodNotFound, - /// Caused by attempting to find a service that does not exist. - #[error("Service not found")] - ServiceNotFound, /// Caused by attempting to perform an upate in an invalid context. #[error("Update Error: {0}")] - UpdateError(#[from] crate::events::UpdateError), + UpdateError(#[from] crate::updates::UpdateError), /// Caused by providing bytes that cannot be used as a private key of the /// [`KeyType`][identity_core::crypto::KeyType]. #[error("Invalid Private Key: {0}")] @@ -98,6 +80,8 @@ pub enum Error { /// Caused by attempting to create an account for an identity that is already managed by another account. #[error("Identity Is In-use")] IdentityInUse, + #[error("method missing fragment")] + MethodMissingFragment, } #[doc(hidden)] diff --git a/identity-account/src/events/commit.rs b/identity-account/src/events/commit.rs deleted file mode 100644 index 1bba5778cd..0000000000 --- a/identity-account/src/events/commit.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use identity_iota::did::IotaDID; - -use crate::events::Event; -use crate::types::Generation; - -/// An [event][Event] and position in an identity event sequence. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct Commit { - identity: IotaDID, - sequence: Generation, - event: Event, -} - -impl Commit { - /// Creates a new `Commit`. - pub const fn new(identity: IotaDID, sequence: Generation, event: Event) -> Self { - Self { - identity, - sequence, - event, - } - } - - /// Returns the identifier of the associated identity. - pub const fn identity(&self) -> &IotaDID { - &self.identity - } - - /// Returns the sequence index of the event. - pub const fn sequence(&self) -> Generation { - self.sequence - } - - /// Returns a reference to the underlying event. - pub const fn event(&self) -> &Event { - &self.event - } - - /// Consumes the commit and returns the underlying event. - pub fn into_event(self) -> Event { - self.event - } -} diff --git a/identity-account/src/events/context.rs b/identity-account/src/events/context.rs deleted file mode 100644 index f147d23310..0000000000 --- a/identity-account/src/events/context.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use identity_iota::did::IotaDID; - -use crate::identity::IdentityState; -use crate::storage::Storage; - -/// A read-only view of an identity state with a read-write storage instance. -#[derive(Debug)] -pub struct Context<'a> { - did: &'a IotaDID, - state: &'a IdentityState, - store: &'a dyn Storage, -} - -impl<'a> Context<'a> { - /// Creates a new `Context`. - pub fn new(did: &'a IotaDID, state: &'a IdentityState, store: &'a dyn Storage) -> Self { - Self { did, state, store } - } - - pub fn did(&self) -> &IotaDID { - self.did - } - - /// Returns the context `state`. - pub fn state(&self) -> &IdentityState { - self.state - } - - /// Returns the context `store`. - pub fn store(&self) -> &dyn Storage { - self.store - } -} diff --git a/identity-account/src/events/event.rs b/identity-account/src/events/event.rs deleted file mode 100644 index 2a4e9ca778..0000000000 --- a/identity-account/src/events/event.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use identity_core::common::Fragment; -use identity_core::common::UnixTimestamp; -use identity_did::verification::MethodScope; -use identity_iota::did::IotaDID; -use identity_iota::tangle::MessageId; - -use crate::error::Result; -use crate::identity::IdentityState; -use crate::identity::TinyMethod; -use crate::identity::TinyMethodRef; -use crate::identity::TinyService; - -/// Event data tagged with a timestamp. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct Event { - data: EventData, - time: UnixTimestamp, -} - -impl Event { - /// Creates a new `Event` instance. - pub fn new(data: EventData) -> Self { - Self { - data, - time: UnixTimestamp::now_utc(), - } - } - - /// Returns a reference to the raw event data. - pub const fn data(&self) -> &EventData { - &self.data - } - - /// Returns the unix timestamp of when the event was created. - pub const fn time(&self) -> UnixTimestamp { - self.time - } - - /// Returns a new state created by applying the event to the given `state`. - pub async fn apply(self, mut state: IdentityState) -> Result { - debug!("[Event::apply] Event = {:?}", self); - trace!("[Event::apply] State = {:?}", state); - - match self.data { - EventData::IntegrationMessage(message) => { - state.set_integration_message_id(message); - state.increment_integration_generation()?; - } - EventData::DiffMessage(message) => { - state.set_diff_message_id(message); - state.increment_diff_generation()?; - } - EventData::IdentityCreated(did) => { - state.set_did(did); - state.set_created(self.time); - state.set_updated(self.time); - } - EventData::MethodCreated(scope, method) => { - state.methods_mut().insert(scope, TinyMethodRef::Embed(method)); - state.set_updated(self.time); - } - EventData::MethodDeleted(fragment) => { - state.methods_mut().delete(fragment.name()); - state.set_updated(self.time); - } - EventData::MethodAttached(fragment, scopes) => { - let method: TinyMethodRef = TinyMethodRef::Refer(fragment); - - for scope in scopes { - state.methods_mut().insert(scope, method.clone()); - } - - state.set_updated(self.time); - } - EventData::MethodDetached(fragment, scopes) => { - for scope in scopes { - state.methods_mut().detach(scope, fragment.name()); - } - - state.set_updated(self.time); - } - EventData::ServiceCreated(service) => { - state.services_mut().insert(service); - state.set_updated(self.time); - } - EventData::ServiceDeleted(fragment) => { - state.services_mut().delete(fragment.name()); - state.set_updated(self.time); - } - } - - Ok(state) - } -} - -// ============================================================================= -// EventData -// ============================================================================= - -/// Raw event data. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(tag = "type", content = "data")] -pub enum EventData { - /// Emitted when a new int message is published to the IOTA Tangle. - IntegrationMessage(MessageId), - /// Emitted when a new diff message is published to the IOTA Tangle. - DiffMessage(MessageId), - /// Emitted when a new identity state is created. - IdentityCreated(IotaDID), - /// Emitted when a new verification method is created. - MethodCreated(MethodScope, TinyMethod), - /// Emitted when a verification method is deleted. - MethodDeleted(Fragment), - /// Emitted when a verification method is attached to one or more scopes. - MethodAttached(Fragment, Vec), - /// Emitted when a verification method is detached from one or more scopes. - MethodDetached(Fragment, Vec), - /// Emitted when a new service is created. - ServiceCreated(TinyService), - /// Emitted when a service is deleted. - ServiceDeleted(Fragment), -} diff --git a/identity-account/src/events/update.rs b/identity-account/src/events/update.rs deleted file mode 100644 index 98f04efd0c..0000000000 --- a/identity-account/src/events/update.rs +++ /dev/null @@ -1,385 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use crypto::signatures::ed25519; - -use identity_core::common::Fragment; -use identity_core::common::Object; -use identity_core::crypto::KeyPair; -use identity_core::crypto::PublicKey; -use identity_did::service::ServiceEndpoint; -use identity_did::verification::MethodData; -use identity_did::verification::MethodScope; -use identity_did::verification::MethodType; -use identity_iota::did::IotaDID; -use identity_iota::tangle::NetworkName; - -use crate::account::Account; -use crate::error::Result; -use crate::events::Context; -use crate::events::Event; -use crate::events::EventData; -use crate::events::UpdateError; -use crate::identity::DIDLease; -use crate::identity::IdentityState; -use crate::identity::TinyMethod; -use crate::identity::TinyService; -use crate::storage::Storage; -use crate::types::Generation; -use crate::types::KeyLocation; -use crate::types::MethodSecret; - -// Method types allowed to sign a DID document update. -pub const UPDATE_METHOD_TYPES: &[MethodType] = &[MethodType::Ed25519VerificationKey2018]; -pub const DEFAULT_UPDATE_METHOD_PREFIX: &str = "sign-"; - -pub(crate) struct CreateIdentity { - pub(crate) network: Option, - pub(crate) method_secret: Option, - pub(crate) method_type: MethodType, -} - -impl CreateIdentity { - pub(crate) async fn process( - &self, - integration_generation: Generation, - store: &dyn Storage, - ) -> Result<(IotaDID, DIDLease, Vec)> { - // The method type must be able to sign document updates. - ensure!( - UPDATE_METHOD_TYPES.contains(&self.method_type), - UpdateError::InvalidMethodType(self.method_type) - ); - - let fragment: String = format!("{}{}", DEFAULT_UPDATE_METHOD_PREFIX, integration_generation.to_u32()); - let location: KeyLocation = KeyLocation::new(self.method_type, fragment, integration_generation, Generation::new()); - - let keypair: KeyPair = if let Some(MethodSecret::Ed25519(private_key)) = &self.method_secret { - ensure!( - private_key.as_ref().len() == ed25519::SECRET_KEY_LENGTH, - UpdateError::InvalidMethodSecret(format!( - "an ed25519 private key requires {} bytes, found {}", - ed25519::SECRET_KEY_LENGTH, - private_key.as_ref().len() - )) - ); - - KeyPair::try_from_ed25519_bytes(private_key.as_ref())? - } else { - KeyPair::new_ed25519()? - }; - - // Generate a new DID URL from the public key - let did: IotaDID = if let Some(network) = &self.network { - IotaDID::new_with_network(keypair.public().as_ref(), network.clone())? - } else { - IotaDID::new(keypair.public().as_ref())? - }; - - ensure!( - !store.key_exists(&did, &location).await?, - UpdateError::DocumentAlreadyExists - ); - - let did_lease = store.lease_did(&did).await?; - - let private_key = keypair.private().to_owned(); - std::mem::drop(keypair); - - let public: PublicKey = insert_method_secret( - store, - &did, - &location, - self.method_type, - MethodSecret::Ed25519(private_key), - ) - .await?; - - let data: MethodData = MethodData::new_b58(public.as_ref()); - let method: TinyMethod = TinyMethod::new(location, data, None); - - Ok(( - did.clone(), - did_lease, - vec![ - Event::new(EventData::IdentityCreated(did)), - Event::new(EventData::MethodCreated(MethodScope::CapabilityInvocation, method)), - ], - )) - } -} - -#[derive(Clone, Debug)] -pub(crate) enum Update { - CreateMethod { - scope: MethodScope, - type_: MethodType, - fragment: String, - method_secret: Option, - }, - DeleteMethod { - fragment: String, - }, - AttachMethod { - fragment: String, - scopes: Vec, - }, - DetachMethod { - fragment: String, - scopes: Vec, - }, - CreateService { - fragment: String, - type_: String, - endpoint: ServiceEndpoint, - properties: Option, - }, - DeleteService { - fragment: String, - }, -} - -impl Update { - pub(crate) async fn process(self, context: Context<'_>) -> Result>> { - let did: &IotaDID = context.did(); - let state: &IdentityState = context.state(); - let store: &dyn Storage = context.store(); - - debug!("[Command::process] Command = {:?}", self); - trace!("[Command::process] State = {:?}", state); - trace!("[Command::process] Store = {:?}", store); - - match self { - Self::CreateMethod { - type_, - scope, - fragment, - method_secret, - } => { - let location: KeyLocation = state.key_location(type_, fragment)?; - - // The key location must be available. - // TODO: config: strict - ensure!( - !store.key_exists(did, &location).await?, - UpdateError::DuplicateKeyLocation(location) - ); - - // The verification method must not exist. - ensure!( - !state.methods().contains(location.fragment_name()), - UpdateError::DuplicateKeyFragment(location.fragment().clone()), - ); - - let public: PublicKey = if let Some(method_private_key) = method_secret { - insert_method_secret(store, did, &location, type_, method_private_key).await - } else { - store.key_new(did, &location).await - }?; - - let data: MethodData = MethodData::new_multibase(public.as_ref()); - let method: TinyMethod = TinyMethod::new(location, data, None); - - Ok(Some(vec![Event::new(EventData::MethodCreated(scope, method))])) - } - Self::DeleteMethod { fragment } => { - let fragment: Fragment = Fragment::new(fragment); - - // The verification method must exist. - ensure!(state.methods().contains(fragment.name()), UpdateError::MethodNotFound); - - // Prevent deleting the last method capable of signing the DID document. - let is_capability_invocation = state - .methods() - .slice(MethodScope::CapabilityInvocation) - .iter() - .any(|method_ref| method_ref.fragment() == &fragment); - ensure!( - !(is_capability_invocation && state.methods().slice(MethodScope::CapabilityInvocation).len() == 1), - UpdateError::InvalidMethodFragment("cannot remove last signing method") - ); - - Ok(Some(vec![Event::new(EventData::MethodDeleted(fragment))])) - } - Self::AttachMethod { fragment, scopes } => { - let fragment: Fragment = Fragment::new(fragment); - - // The verification method must exist. - ensure!(state.methods().contains(fragment.name()), UpdateError::MethodNotFound); - - Ok(Some(vec![Event::new(EventData::MethodAttached(fragment, scopes))])) - } - Self::DetachMethod { fragment, scopes } => { - let fragment: Fragment = Fragment::new(fragment); - - // The verification method must exist. - ensure!(state.methods().contains(fragment.name()), UpdateError::MethodNotFound); - - // Prevent detaching the last method capable of signing the DID document. - let is_capability_invocation = state - .methods() - .slice(MethodScope::CapabilityInvocation) - .iter() - .any(|method_ref| method_ref.fragment() == &fragment); - ensure!( - !(is_capability_invocation && state.methods().slice(MethodScope::CapabilityInvocation).len() == 1), - UpdateError::InvalidMethodFragment("cannot remove last signing method") - ); - - Ok(Some(vec![Event::new(EventData::MethodDetached(fragment, scopes))])) - } - Self::CreateService { - fragment, - type_, - endpoint, - properties, - } => { - // The service must not exist - ensure!( - !state.services().contains(&fragment), - UpdateError::DuplicateServiceFragment(fragment), - ); - - let service: TinyService = TinyService::new(fragment, type_, endpoint, properties); - - Ok(Some(vec![Event::new(EventData::ServiceCreated(service))])) - } - Self::DeleteService { fragment } => { - let fragment: Fragment = Fragment::new(fragment); - - // The service must exist - ensure!(state.services().contains(fragment.name()), UpdateError::ServiceNotFound); - - Ok(Some(vec![Event::new(EventData::ServiceDeleted(fragment))])) - } - } - } -} - -async fn insert_method_secret( - store: &dyn Storage, - did: &IotaDID, - location: &KeyLocation, - method_type: MethodType, - method_secret: MethodSecret, -) -> Result { - match method_secret { - MethodSecret::Ed25519(private_key) => { - ensure!( - private_key.as_ref().len() == ed25519::SECRET_KEY_LENGTH, - UpdateError::InvalidMethodSecret(format!( - "an ed25519 private key requires {} bytes, found {}", - ed25519::SECRET_KEY_LENGTH, - private_key.as_ref().len() - )) - ); - - ensure!( - matches!(method_type, MethodType::Ed25519VerificationKey2018), - UpdateError::InvalidMethodSecret( - "MethodType::Ed25519VerificationKey2018 can only be used with an ed25519 method secret".to_owned(), - ) - ); - - store.key_insert(did, location, private_key).await - } - MethodSecret::MerkleKeyCollection(_) => { - ensure!( - matches!(method_type, MethodType::MerkleKeyCollection2021), - UpdateError::InvalidMethodSecret( - "MethodType::MerkleKeyCollection2021 can only be used with a MerkleKeyCollection method secret".to_owned(), - ) - ); - - todo!("[Command::CreateMethod] Handle MerkleKeyCollection") - } - } -} - -// ============================================================================= -// Command Builders -// ============================================================================= - -impl_command_builder!( -/// Create a new method on an identity. -/// -/// # Parameters -/// - `type_`: the type of the method, defaults to [`MethodType::Ed25519VerificationKey2018`]. -/// - `scope`: the scope of the method, defaults to [`MethodScope::default`]. -/// - `fragment`: the identifier of the method in the document, required. -/// - `method_secret`: the secret key to use for the method, optional. Will be generated when omitted. -CreateMethod { - @defaulte type_ MethodType = Ed25519VerificationKey2018, - @default scope MethodScope, - @required fragment String, - @optional method_secret MethodSecret -}); - -impl_command_builder!( -/// Delete a method on an identity. -/// -/// # Parameters -/// - `fragment`: the identifier of the method in the document, required. -DeleteMethod { - @required fragment String, -}); - -impl_command_builder!( -/// Attach one or more verification relationships to a method on an identity. -/// -/// # Parameters -/// - `scopes`: the scopes to add, defaults to an empty [`Vec`]. -/// - `fragment`: the identifier of the method in the document, required. -AttachMethod { - @required fragment String, - @default scopes Vec, -}); - -impl<'account> AttachMethodBuilder<'account> { - pub fn scope(mut self, value: MethodScope) -> Self { - self.scopes.get_or_insert_with(Default::default).push(value); - self - } -} - -impl_command_builder!( -/// Detaches one or more verification relationships from a method on an identity. -/// -/// # Parameters -/// - `scopes`: the scopes to remove, defaults to an empty [`Vec`]. -/// - `fragment`: the identifier of the method in the document, required. -DetachMethod { - @required fragment String, - @default scopes Vec, -}); - -impl<'account> DetachMethodBuilder<'account> { - pub fn scope(mut self, value: MethodScope) -> Self { - self.scopes.get_or_insert_with(Default::default).push(value); - self - } -} - -impl_command_builder!( -/// Create a new service on an identity. -/// -/// # Parameters -/// - `type_`: the type of the service, e.g. `"LinkedDomains"`, required. -/// - `fragment`: the identifier of the service in the document, required. -/// - `endpoint`: the `ServiceEndpoint` of the service, required. -/// - `properties`: additional properties of the service, optional. -CreateService { - @required fragment String, - @required type_ String, - @required endpoint ServiceEndpoint, - @optional properties Object, -}); - -impl_command_builder!( -/// Delete a service on an identity. -/// -/// # Parameters -/// - `fragment`: the identifier of the service in the document, required. -DeleteService { - @required fragment String, -}); diff --git a/identity-account/src/identity/chain_state.rs b/identity-account/src/identity/chain_state.rs new file mode 100644 index 0000000000..30fd554f4b --- /dev/null +++ b/identity-account/src/identity/chain_state.rs @@ -0,0 +1,64 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Serialize; + +use identity_iota::tangle::MessageId; +use identity_iota::tangle::MessageIdExt; + +/// Holds the last published message ids of the integration and diff chains. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct ChainState { + #[serde(default = "MessageId::null", skip_serializing_if = "MessageId::is_null")] + last_integration_message_id: MessageId, + #[serde(default = "MessageId::null", skip_serializing_if = "MessageId::is_null")] + last_diff_message_id: MessageId, +} + +impl ChainState { + pub fn new() -> Self { + Self { + last_integration_message_id: MessageId::null(), + last_diff_message_id: MessageId::null(), + } + } + + /// Returns the integration message id of the last published update. + /// + /// Note: [`MessageId`] has a built-in `null` variant that needs to be checked for. + pub fn last_integration_message_id(&self) -> &MessageId { + &self.last_integration_message_id + } + + /// Returns the diff message id of the last published update. + /// + /// Note: [`MessageId`] has a built-in `null` variant that needs to be checked for. + pub fn last_diff_message_id(&self) -> &MessageId { + &self.last_diff_message_id + } + + /// Sets the last integration message id and resets the + /// last diff message id to [`MessageId::null()`]. + pub fn set_last_integration_message_id(&mut self, message: MessageId) { + self.last_integration_message_id = message; + + // Clear the diff message id + self.last_diff_message_id = MessageId::null(); + } + + /// Sets the last diff message id. + pub fn set_last_diff_message_id(&mut self, message: MessageId) { + self.last_diff_message_id = message; + } + + /// Returns whether the identity has been published before. + pub fn is_new_identity(&self) -> bool { + self.last_integration_message_id.is_null() + } +} + +impl Default for ChainState { + fn default() -> Self { + Self::new() + } +} diff --git a/identity-account/src/identity/identity_snapshot.rs b/identity-account/src/identity/identity_snapshot.rs deleted file mode 100644 index dcc55a14c4..0000000000 --- a/identity-account/src/identity/identity_snapshot.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use crate::identity::IdentityState; -use crate::types::Generation; - -/// A snapshot of an identity state at a particular index. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct IdentitySnapshot { - pub(crate) sequence: Generation, - pub(crate) identity: IdentityState, -} - -impl IdentitySnapshot { - /// Creates a new `IdentitySnapshot` instance. - pub fn new(identity: IdentityState) -> Self { - Self { - sequence: Generation::new(), - identity, - } - } - - /// Returns the sequence index of the snapshot. - pub fn sequence(&self) -> Generation { - self.sequence - } - - /// Returns the identity state of the snapshot. - pub fn identity(&self) -> &IdentityState { - &self.identity - } - - /// Returns the identity state of the snapshot (consuming self). - pub fn into_identity(self) -> IdentityState { - self.identity - } -} diff --git a/identity-account/src/identity/identity_state.rs b/identity-account/src/identity/identity_state.rs index 6485db1f30..e44cb67ad8 100644 --- a/identity-account/src/identity/identity_state.rs +++ b/identity-account/src/identity/identity_state.rs @@ -1,36 +1,18 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use core::convert::TryInto; - use hashbrown::HashMap; +use identity_did::did::DID; use serde::Serialize; use identity_core::common::Fragment; -use identity_core::common::Object; -use identity_core::common::UnixTimestamp; -use identity_core::common::Url; use identity_core::crypto::JcsEd25519; use identity_core::crypto::SetSignature; use identity_core::crypto::Signer; -use identity_did::did::CoreDIDUrl; -use identity_did::did::DID; -use identity_did::document::CoreDocument; -use identity_did::document::DocumentBuilder; -use identity_did::service::Service as CoreService; -use identity_did::service::ServiceEndpoint; -use identity_did::verifiable::Properties as VerifiableProperties; -use identity_did::verification::MethodData; -use identity_did::verification::MethodRef as CoreMethodRef; -use identity_did::verification::MethodScope; use identity_did::verification::MethodType; -use identity_did::verification::VerificationMethod; use identity_iota::did::IotaDID; use identity_iota::did::IotaDIDUrl; use identity_iota::did::IotaDocument; -use identity_iota::did::Properties as BaseProperties; -use identity_iota::tangle::MessageId; -use identity_iota::tangle::MessageIdExt; use identity_iota::tangle::TangleRef; use crate::crypto::RemoteKey; @@ -41,59 +23,22 @@ use crate::storage::Storage; use crate::types::Generation; use crate::types::KeyLocation; -type Properties = VerifiableProperties; -type BaseDocument = CoreDocument; - pub type RemoteEd25519<'a> = JcsEd25519>; -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct IdentityState { - // =========== // - // Chain State // - // =========== // - integration_generation: Generation, - diff_generation: Generation, - #[serde(default = "MessageId::null", skip_serializing_if = "MessageId::is_null")] - this_message_id: MessageId, - #[serde(default = "MessageId::null", skip_serializing_if = "MessageId::is_null")] - last_integration_message_id: MessageId, - #[serde(default = "MessageId::null", skip_serializing_if = "MessageId::is_null")] - last_diff_message_id: MessageId, - - // ============== // - // Document State // - // ============== // - #[serde(skip_serializing_if = "Option::is_none")] - did: Option, - #[serde(skip_serializing_if = "Option::is_none")] - controller: Option, - #[serde(skip_serializing_if = "Option::is_none")] - also_known_as: Option>, - #[serde(skip_serializing_if = "Methods::is_empty")] - methods: Methods, - #[serde(default, skip_serializing_if = "Services::is_empty")] - services: Services, - #[serde(default, skip_serializing_if = "UnixTimestamp::is_epoch")] - created: UnixTimestamp, - #[serde(default, skip_serializing_if = "UnixTimestamp::is_epoch")] - updated: UnixTimestamp, + generation: Generation, + #[serde(skip_serializing_if = "HashMap::is_empty")] + method_generations: HashMap, + document: IotaDocument, } impl IdentityState { - pub fn new() -> Self { + pub fn new(document: IotaDocument) -> Self { Self { - integration_generation: Generation::new(), - diff_generation: Generation::new(), - this_message_id: MessageId::null(), - last_integration_message_id: MessageId::null(), - last_diff_message_id: MessageId::null(), - did: None, - controller: None, - also_known_as: None, - methods: Methods::new(), - services: Services::new(), - created: UnixTimestamp::EPOCH, - updated: UnixTimestamp::EPOCH, + generation: Generation::new(), + method_generations: HashMap::new(), + document, } } @@ -102,234 +47,47 @@ impl IdentityState { // =========================================================================== /// Returns the current generation of the identity integration chain. - pub fn integration_generation(&self) -> Generation { - self.integration_generation - } - - /// Returns the current generation of the identity diff chain. - pub fn diff_generation(&self) -> Generation { - self.diff_generation - } - - /// Increments the generation of the identity integration chain. - pub fn increment_integration_generation(&mut self) -> Result<()> { - self.integration_generation = self.integration_generation.try_increment()?; - self.diff_generation = Generation::new(); - - Ok(()) + pub fn generation(&self) -> Generation { + self.generation } /// Increments the generation of the identity diff chain. - pub fn increment_diff_generation(&mut self) -> Result<()> { - self.diff_generation = self.diff_generation.try_increment()?; + pub fn increment_generation(&mut self) -> Result<()> { + self.generation = self.generation.try_increment()?; Ok(()) } - // =========================================================================== - // Tangle State - // =========================================================================== - - /// Returns the current integration Tangle message id of the identity. - pub fn this_message_id(&self) -> &MessageId { - &self.this_message_id + /// Stores the generations at which the method was inserted. + pub fn store_method_generations(&mut self, fragment: Fragment) { + self.method_generations.insert(fragment, self.generation()); } - /// Returns the previous integration Tangle message id of the identity. - pub fn last_message_id(&self) -> &MessageId { - &self.last_integration_message_id - } - - /// Returns the previous diff Tangle message id, or the current integration message id. - pub fn diff_message_id(&self) -> &MessageId { - if self.last_diff_message_id.is_null() { - &self.this_message_id - } else { - &self.last_diff_message_id - } - } - - /// Sets the current Tangle integration message id of the identity. - pub fn set_integration_message_id(&mut self, message: MessageId) { - // Set the current integration message id as the previous integration message. - self.last_integration_message_id = self.this_message_id; - - // Clear the diff message id - self.last_diff_message_id = MessageId::null(); - - // Set the new integration message id - self.this_message_id = message; - } + /// Return the `KeyLocation` of the given method. + pub fn method_location(&self, method_type: MethodType, fragment: String) -> Result { + let fragment = Fragment::new(fragment); + // We don't return `MethodNotFound`, as the `KeyNotFound` error might occur when a method exists + // in the document, but the key is not present locally (e.g. in a distributed setup). + let generation = self.method_generations.get(&fragment).ok_or(Error::KeyNotFound)?; - /// Sets the current Tangle diff message id of the identity. - pub fn set_diff_message_id(&mut self, message: MessageId) { - self.last_diff_message_id = message; + Ok(KeyLocation::new(method_type, fragment.into(), *generation)) } // =========================================================================== // Document State // =========================================================================== - /// Returns the DID identifying the DID Document for the state. - pub fn did(&self) -> Option<&IotaDID> { - self.did.as_ref() - } - - /// Returns the DID identifying the DID Document for the state. - /// - /// # Errors - /// - /// Fails if the DID is not set. - pub fn try_did(&self) -> Result<&IotaDID> { - self.did().ok_or(Error::MissingDocumentId) - } - - /// Sets the DID identifying the DID Document for the state. - pub fn set_did(&mut self, did: IotaDID) { - self.did = Some(did); - } - - /// Returns the timestamp of when the state was created. - pub fn created(&self) -> UnixTimestamp { - self.created - } - - /// Returns the timestamp of when the state was last updated. - pub fn updated(&self) -> UnixTimestamp { - self.updated - } - - /// Sets the timestamp of when the state was created. - pub fn set_created(&mut self, timestamp: UnixTimestamp) { - self.created = timestamp; - } - - /// Sets the timestamp of when the state was last updated. - pub fn set_updated(&mut self, timestamp: UnixTimestamp) { - self.updated = timestamp; - } - - /// Returns a reference to the state methods. - pub fn methods(&self) -> &Methods { - &self.methods - } - - /// Returns a mutable reference to the state methods. - pub fn methods_mut(&mut self) -> &mut Methods { - &mut self.methods - } - - /// Returns a reference to the state services. - pub fn services(&self) -> &Services { - &self.services - } - - /// Returns a mutable reference to the state services. - pub fn services_mut(&mut self) -> &mut Services { - &mut self.services - } - - /// Returns the latest authentication method in the state. - pub fn authentication(&self) -> Result<&TinyMethod> { - self - .methods() - .slice(MethodScope::Authentication) - .iter() - .filter_map(|method_ref| self.methods.get(&method_ref.fragment().to_string())) - .max_by_key(|method| method.location().integration_generation()) - .ok_or(Error::MethodNotFound) + pub fn document(&self) -> &IotaDocument { + &self.document } - /// Returns the latest capability invocation method in the state. - pub fn capability_invocation(&self) -> Result<&TinyMethod> { - self - .methods() - .slice(MethodScope::CapabilityInvocation) - .iter() - .filter_map(|method_ref| self.methods.get(&method_ref.fragment().to_string())) - .max_by_key(|method| method.location().integration_generation()) - .ok_or(Error::MethodNotFound) + pub fn document_mut(&mut self) -> &mut IotaDocument { + &mut self.document } /// Returns a key location suitable for the specified `fragment`. pub fn key_location(&self, method: MethodType, fragment: String) -> Result { - Ok(KeyLocation::new( - method, - fragment, - self.integration_generation(), - self.diff_generation(), - )) - } - - // =========================================================================== - // DID Document Helpers - // =========================================================================== - - /// Creates a new DID Document based on the identity state. - pub fn to_document(&self) -> Result { - let properties: BaseProperties = BaseProperties::new(); - let properties: Properties = VerifiableProperties::new(properties); - let mut builder: DocumentBuilder<_, _, _> = BaseDocument::builder(properties); - - let document_id: &IotaDID = self.try_did()?; - - builder = builder.id(document_id.clone().into()); - - if let Some(value) = self.controller.as_ref() { - builder = builder.controller(value.clone().into()); - } - - if let Some(values) = self.also_known_as.as_deref() { - for value in values { - builder = builder.also_known_as(value.clone()); - } - } - - for method in self.methods.slice(MethodScope::VerificationMethod) { - builder = match method.to_core(document_id)? { - CoreMethodRef::Embed(inner) => builder.verification_method(inner), - CoreMethodRef::Refer(_) => unreachable!(), - }; - } - - for method in self.methods.slice(MethodScope::Authentication) { - builder = builder.authentication(method.to_core(document_id)?); - } - - for method in self.methods.slice(MethodScope::AssertionMethod) { - builder = builder.assertion_method(method.to_core(document_id)?); - } - - for method in self.methods.slice(MethodScope::KeyAgreement) { - builder = builder.key_agreement(method.to_core(document_id)?); - } - - for method in self.methods.slice(MethodScope::CapabilityDelegation) { - builder = builder.capability_delegation(method.to_core(document_id)?); - } - - for method in self.methods.slice(MethodScope::CapabilityInvocation) { - builder = builder.capability_invocation(method.to_core(document_id)?); - } - - for service in self.services.iter() { - builder = builder.service(service.to_core(document_id)?); - } - - let mut document: IotaDocument = builder.build()?.try_into()?; - - if !self.this_message_id.is_null() { - document.set_message_id(self.this_message_id); - } - - if !self.last_integration_message_id.is_null() { - document.set_previous_message_id(self.last_integration_message_id); - } - - document.set_created(self.created.into()); - document.set_updated(self.updated.into()); - - Ok(document) + Ok(KeyLocation::new(method, fragment, self.generation())) } pub async fn sign_data( @@ -347,7 +105,7 @@ impl IdentityState { // Create the Verification Method identifier let fragment: &str = location.fragment().identifier(); - let method_url: IotaDIDUrl = self.try_did()?.to_url().join(fragment)?; + let method_url: IotaDIDUrl = self.document.did().to_url().join(fragment)?; match location.method() { MethodType::Ed25519VerificationKey2018 => { @@ -361,311 +119,3 @@ impl IdentityState { Ok(()) } } - -impl Default for IdentityState { - fn default() -> Self { - Self::new() - } -} - -// ============================================================================= -// TinyMethodRef -// ============================================================================= - -/// A thin representation of a Verification Method reference. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(untagged)] -pub enum TinyMethodRef { - Embed(TinyMethod), - Refer(Fragment), -} - -impl TinyMethodRef { - /// Returns the fragment identifying the Verification Method reference. - pub fn fragment(&self) -> &Fragment { - match self { - Self::Embed(inner) => inner.location.fragment(), - Self::Refer(inner) => inner, - } - } - - /// Creates a new `CoreMethodRef` from the method reference state. - pub fn to_core(&self, did: &IotaDID) -> Result { - match self { - Self::Embed(inner) => inner.to_core(did).map(CoreMethodRef::Embed), - Self::Refer(inner) => did - .to_url() - .join(inner.identifier()) - .map(CoreDIDUrl::from) - .map(CoreMethodRef::Refer) - .map_err(Into::into), - } - } - - fn __embed(method: &TinyMethodRef) -> Option<&TinyMethod> { - match method { - Self::Embed(inner) => Some(inner), - Self::Refer(_) => None, - } - } -} - -// ============================================================================= -// TinyMethod -// ============================================================================= - -/// A thin representation of a Verification Method. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct TinyMethod { - #[serde(rename = "1")] - location: KeyLocation, - #[serde(rename = "2")] - key_data: MethodData, - #[serde(rename = "3")] - properties: Option, -} - -impl TinyMethod { - /// Creates a new `TinyMethod`. - pub fn new(location: KeyLocation, key_data: MethodData, properties: Option) -> Self { - Self { - location, - key_data, - properties, - } - } - - /// Returns the key location of the Verification Method. - pub fn location(&self) -> &KeyLocation { - &self.location - } - - /// Returns the computed method data of the Verification Method. - pub fn key_data(&self) -> &MethodData { - &self.key_data - } - - /// Returns any additional Verification Method properties. - pub fn properties(&self) -> Option<&Object> { - self.properties.as_ref() - } - - /// Creates a new [VerificationMethod]. - pub fn to_core(&self, did: &IotaDID) -> Result { - let properties: Object = self.properties.clone().unwrap_or_default(); - let id: IotaDIDUrl = did.to_url().join(self.location.fragment().identifier())?; - - VerificationMethod::builder(properties) - .id(CoreDIDUrl::from(id)) - .controller(did.clone().into()) - .key_type(self.location.method()) - .key_data(self.key_data.clone()) - .build() - .map_err(Into::into) - } -} - -// ============================================================================= -// Methods -// ============================================================================= - -/// A map of Verification Method states. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(transparent)] -pub struct Methods { - data: HashMap>, -} - -impl Methods { - /// Creates a new `Methods` instance. - pub fn new() -> Self { - Self { data: HashMap::new() } - } - - /// Returns the total number of Verification Methods in the map. - /// - /// Note: This does not include Verification Method references. - pub fn len(&self) -> usize { - self.iter().count() - } - - /// Returns true if the map has no Verification Methods. - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Returns a slice of the Verification Methods applicable to the given `scope`. - pub fn slice(&self, scope: MethodScope) -> &[TinyMethodRef] { - self.data.get(&scope).map(|data| &**data).unwrap_or_default() - } - - /// Returns an iterator over all embedded Verification Methods. - pub fn iter(&self) -> impl Iterator { - self.iter_ref().filter_map(TinyMethodRef::__embed) - } - - /// Returns an iterator over all Verification Methods. - /// - /// Note: This includes Verification Method references. - pub fn iter_ref(&self) -> impl Iterator { - self - .slice(MethodScope::VerificationMethod) - .iter() - .chain(self.slice(MethodScope::Authentication).iter()) - .chain(self.slice(MethodScope::AssertionMethod).iter()) - .chain(self.slice(MethodScope::KeyAgreement).iter()) - .chain(self.slice(MethodScope::CapabilityDelegation).iter()) - .chain(self.slice(MethodScope::CapabilityInvocation).iter()) - } - - /// Returns a reference to the Verification Method identified by the given - /// `fragment`. - pub fn get(&self, fragment: &str) -> Option<&TinyMethod> { - self.iter().find(|method| method.location().fragment_name() == fragment) - } - - /// Returns a reference to the Verification Method identified by the given - /// `fragment`. - /// - /// # Errors - /// - /// Fails if no matching Verification Method is found. - pub fn fetch(&self, fragment: &str) -> Result<&TinyMethod> { - self.get(fragment).ok_or(Error::MethodNotFound) - } - - /// Returns true if the map contains a method with the given `fragment`. - pub fn contains(&self, fragment: &str) -> bool { - self.iter().any(|method| method.location().fragment_name() == fragment) - } - - /// Adds a new method to the map - no validation is performed. - pub fn insert(&mut self, scope: MethodScope, method: TinyMethodRef) { - self.data.entry(scope).or_default().push(method); - } - - /// Removes the method specified by `fragment` from the given `scope`. - pub fn detach(&mut self, scope: MethodScope, fragment: &str) { - if let Some(list) = self.data.get_mut(&scope) { - list.retain(|method| method.fragment().name() != fragment); - } - } - - /// Removes the Verification Method specified by the given `fragment`. - /// - /// Note: This includes both references and embedded structures. - pub fn delete(&mut self, fragment: &str) { - for (_, list) in self.data.iter_mut() { - list.retain(|method| method.fragment().name() != fragment); - } - } -} - -// ============================================================================= -// TinyService -// ============================================================================= - -/// A thin representation of a DID Document service. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct TinyService { - #[serde(rename = "1")] - fragment: Fragment, - #[serde(rename = "2")] - type_: String, - #[serde(rename = "3")] - endpoint: ServiceEndpoint, - #[serde(rename = "4")] - properties: Option, -} - -impl TinyService { - /// Creates a new `TinyService`. - pub fn new(fragment: String, type_: String, endpoint: ServiceEndpoint, properties: Option) -> Self { - Self { - fragment: Fragment::new(fragment), - type_, - endpoint, - properties, - } - } - - /// Returns the fragment identifying the service. - pub fn fragment(&self) -> &Fragment { - &self.fragment - } - - /// Creates a new `CoreService` from the service state. - pub fn to_core(&self, did: &IotaDID) -> Result> { - let properties: Object = self.properties.clone().unwrap_or_default(); - let id: IotaDIDUrl = did.to_url().join(self.fragment().identifier())?; - - CoreService::builder(properties) - .id(CoreDIDUrl::from(id)) - .type_(&self.type_) - .service_endpoint(self.endpoint.clone()) - .build() - .map_err(Into::into) - } -} - -// ============================================================================= -// Services -// ============================================================================= - -/// A set of DID Document service states. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[serde(transparent)] -pub struct Services { - data: Vec, -} - -impl Services { - /// Creates a new `Services` instance. - pub fn new() -> Self { - Self { data: Vec::new() } - } - - /// Returns the total number of services in the set. - pub fn len(&self) -> usize { - self.data.len() - } - - /// Returns true if the set has no services. - pub fn is_empty(&self) -> bool { - self.data.is_empty() - } - - /// Returns an iterator over the services in the set. - pub fn iter(&self) -> impl Iterator { - self.data.iter() - } - - /// Returns a reference to the service identified by the given `fragment`. - pub fn get(&self, fragment: &str) -> Option<&TinyService> { - self.iter().find(|service| service.fragment().name() == fragment) - } - - /// Returns a reference to the service identified by the given `fragment`. - /// - /// # Errors - /// - /// Fails if no matching service is found. - pub fn fetch(&self, fragment: &str) -> Result<&TinyService> { - self.get(fragment).ok_or(Error::ServiceNotFound) - } - - /// Returns true if the set contains a service with the given `fragment`. - pub fn contains(&self, fragment: &str) -> bool { - self.iter().any(|service| service.fragment().name() == fragment) - } - - /// Adds a new `service` to the set - no validation is performed. - pub fn insert(&mut self, service: TinyService) { - self.data.push(service); - } - - /// Removes the service specified by the given `fragment`. - pub fn delete(&mut self, fragment: &str) { - self.data.retain(|service| service.fragment().name() != fragment); - } -} diff --git a/identity-account/src/identity/mod.rs b/identity-account/src/identity/mod.rs index 3ed0a6556b..d3f75cd0dc 100644 --- a/identity-account/src/identity/mod.rs +++ b/identity-account/src/identity/mod.rs @@ -1,14 +1,14 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod chain_state; mod did_lease; mod identity_setup; -mod identity_snapshot; mod identity_state; mod identity_updater; +pub use self::chain_state::*; pub use self::did_lease::*; pub use self::identity_setup::*; -pub use self::identity_snapshot::*; pub use self::identity_state::*; pub use self::identity_updater::*; diff --git a/identity-account/src/lib.rs b/identity-account/src/lib.rs index 0b2cf51b1a..962b308818 100644 --- a/identity-account/src/lib.rs +++ b/identity-account/src/lib.rs @@ -23,7 +23,6 @@ extern crate serde; pub mod account; pub mod crypto; pub mod error; -pub mod events; pub mod identity; pub mod storage; #[cfg(feature = "stronghold")] @@ -31,6 +30,7 @@ pub mod stronghold; #[cfg(test)] mod tests; pub mod types; +pub mod updates; pub mod utils; pub use self::error::Error; diff --git a/identity-account/src/storage/memstore.rs b/identity-account/src/storage/memstore.rs index f0f292cfb0..6230e95374 100644 --- a/identity-account/src/storage/memstore.rs +++ b/identity-account/src/storage/memstore.rs @@ -5,9 +5,6 @@ use core::fmt::Debug; use core::fmt::Formatter; use core::fmt::Result as FmtResult; use crypto::signatures::ed25519; -use futures::stream; -use futures::stream::BoxStream; -use futures::StreamExt; use hashbrown::hash_map::Entry; use hashbrown::HashMap; use identity_core::crypto::Ed25519; @@ -26,9 +23,9 @@ use zeroize::Zeroize; use crate::error::Error; use crate::error::Result; -use crate::events::Commit; +use crate::identity::ChainState; use crate::identity::DIDLease; -use crate::identity::IdentitySnapshot; +use crate::identity::IdentityState; use crate::storage::Storage; use crate::types::Generation; use crate::types::KeyLocation; @@ -38,8 +35,8 @@ use crate::utils::Shared; type MemVault = HashMap; -type Events = HashMap>; -type States = HashMap; +type ChainStates = HashMap; +type States = HashMap; type Vaults = HashMap; type PublishedGenerations = HashMap; @@ -47,7 +44,7 @@ pub struct MemStore { expand: bool, published_generations: Shared, did_leases: Mutex>, - events: Shared, + chain_states: Shared, states: Shared, vaults: Shared, } @@ -58,7 +55,7 @@ impl MemStore { expand: false, published_generations: Shared::new(HashMap::new()), did_leases: Mutex::new(HashMap::new()), - events: Shared::new(HashMap::new()), + chain_states: Shared::new(HashMap::new()), states: Shared::new(HashMap::new()), vaults: Shared::new(HashMap::new()), } @@ -72,14 +69,6 @@ impl MemStore { self.expand = value; } - pub fn events(&self) -> Result { - self.events.read().map(|data| data.clone()) - } - - pub fn states(&self) -> Result { - self.states.read().map(|data| data.clone()) - } - pub fn vaults(&self) -> Result { self.vaults.read().map(|data| data.clone()) } @@ -175,7 +164,7 @@ impl Storage for MemStore { async fn key_get(&self, did: &IotaDID, location: &KeyLocation) -> Result { let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?; let vault: &MemVault = vaults.get(did).ok_or(Error::KeyVaultNotFound)?; - let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyPairNotFound)?; + let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyNotFound)?; Ok(keypair.public().clone()) } @@ -192,7 +181,7 @@ impl Storage for MemStore { async fn key_sign(&self, did: &IotaDID, location: &KeyLocation, data: Vec) -> Result { let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?; let vault: &MemVault = vaults.get(did).ok_or(Error::KeyVaultNotFound)?; - let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyPairNotFound)?; + let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyNotFound)?; match location.method() { MethodType::Ed25519VerificationKey2018 => { @@ -210,39 +199,30 @@ impl Storage for MemStore { } } - async fn snapshot(&self, did: &IotaDID) -> Result> { - self.states.read().map(|states| states.get(did).cloned()) + async fn chain_state(&self, did: &IotaDID) -> Result> { + self.chain_states.read().map(|states| states.get(did).cloned()) } - async fn set_snapshot(&self, did: &IotaDID, snapshot: &IdentitySnapshot) -> Result<()> { - self.states.write()?.insert(did.clone(), snapshot.clone()); + async fn set_chain_state(&self, did: &IotaDID, chain_state: &ChainState) -> Result<()> { + self.chain_states.write()?.insert(did.clone(), chain_state.clone()); Ok(()) } - async fn append(&self, did: &IotaDID, commits: &[Commit]) -> Result<()> { - let mut state: RwLockWriteGuard<'_, _> = self.events.write()?; - let queue: &mut Vec = state.entry(did.clone()).or_default(); - - for commit in commits { - queue.push(commit.clone()); - } - - Ok(()) + async fn state(&self, did: &IotaDID) -> Result> { + self.states.read().map(|states| states.get(did).cloned()) } - async fn stream(&self, did: &IotaDID, index: Generation) -> Result>> { - let state: RwLockReadGuard<'_, _> = self.events.read()?; - let queue: Vec = state.get(did).cloned().unwrap_or_default(); - let index: usize = index.to_u32() as usize; + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()> { + self.states.write()?.insert(did.clone(), state.clone()); - Ok(stream::iter(queue.into_iter().skip(index)).map(Ok).boxed()) + Ok(()) } async fn purge(&self, did: &IotaDID) -> Result<()> { - let _ = self.events.write()?.remove(did); let _ = self.states.write()?.remove(did); let _ = self.vaults.write()?.remove(did); + let _ = self.chain_states.write()?.remove(did); Ok(()) } @@ -261,7 +241,7 @@ impl Debug for MemStore { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { if self.expand { f.debug_struct("MemStore") - .field("events", &self.events) + .field("chain_states", &self.chain_states) .field("states", &self.states) .field("vaults", &self.vaults) .finish() diff --git a/identity-account/src/storage/stronghold.rs b/identity-account/src/storage/stronghold.rs index 46d81a5ef4..69810a20b6 100644 --- a/identity-account/src/storage/stronghold.rs +++ b/identity-account/src/storage/stronghold.rs @@ -1,16 +1,10 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use core::ops::RangeFrom; use crypto::keys::slip10::Chain; -use futures::future; -use futures::stream; -use futures::stream::BoxStream; -use futures::StreamExt; -use futures::TryStreamExt; + use hashbrown::hash_map::Entry; use hashbrown::HashMap; -use hashbrown::HashSet; use identity_core::convert::FromJson; use identity_core::convert::ToJson; use identity_core::crypto::PrivateKey; @@ -28,11 +22,9 @@ use tokio::sync::Mutex; use crate::error::Error; use crate::error::Result; -use crate::events::Commit; -use crate::events::Event; -use crate::events::EventData; +use crate::identity::ChainState; use crate::identity::DIDLease; -use crate::identity::IdentitySnapshot; +use crate::identity::IdentityState; use crate::storage::Storage; use crate::stronghold::default_hint; use crate::stronghold::Snapshot; @@ -44,12 +36,6 @@ use crate::types::Signature; use crate::utils::derive_encryption_key; use crate::utils::EncryptionKey; -// event concurrency limit -const ECL: usize = 8; - -// ============================================================================= -// ============================================================================= - #[derive(Debug)] pub struct Stronghold { did_leases: Mutex>, @@ -180,158 +166,64 @@ impl Storage for Stronghold { } } - async fn snapshot(&self, did: &IotaDID) -> Result> { + async fn chain_state(&self, did: &IotaDID) -> Result> { // Load the chain-specific store let store: Store<'_> = self.store(&fmt_did(did)); - // Read the event snapshot from the stronghold snapshot - let data: Vec = store.get(location_snapshot()).await?; + let data: Vec = store.get(location_chain_state()).await?; - // No snapshot data found if data.is_empty() { return Ok(None); } - // Deserialize and return - Ok(Some(IdentitySnapshot::from_json_slice(&data)?)) + Ok(Some(ChainState::from_json_slice(&data)?)) } - async fn set_snapshot(&self, did: &IotaDID, snapshot: &IdentitySnapshot) -> Result<()> { + async fn set_chain_state(&self, did: &IotaDID, chain_state: &ChainState) -> Result<()> { // Load the chain-specific store let store: Store<'_> = self.store(&fmt_did(did)); - // Serialize the state snapshot - let json: Vec = snapshot.to_json_vec()?; + let json: Vec = chain_state.to_json_vec()?; - // Write the state snapshot to the stronghold snapshot - store.set(location_snapshot(), json, None).await?; + store.set(location_chain_state(), json, None).await?; Ok(()) } - async fn append(&self, did: &IotaDID, commits: &[Commit]) -> Result<()> { - fn encode(commit: &Commit) -> Result<(Generation, Vec)> { - Ok((commit.sequence(), commit.event().to_json_vec()?)) - } - + async fn state(&self, did: &IotaDID) -> Result> { + // Load the chain-specific store let store: Store<'_> = self.store(&fmt_did(did)); - let future: _ = stream::iter(commits.iter().map(encode)) - .into_stream() - .and_then(|(index, json)| store.set(location_event(index), json, None)) - .try_for_each_concurrent(ECL, |()| future::ready(Ok(()))); - - // Write all events to the snapshot - future.await?; + // Read the state from the stronghold snapshot + let data: Vec = store.get(location_state()).await?; - Ok(()) - } - - async fn stream(&self, did: &IotaDID, index: Generation) -> Result>> { - let name: String = fmt_did(did); - let range: RangeFrom = (index.to_u32() + 1)..; - - let did = did.clone(); - - let stream: BoxStream<'_, Result> = stream::iter(range) - .map(Generation::from) - .map(Ok) - // ================================ - // Load the event from the snapshot - // ================================ - .and_then(move |index| { - let name: String = name.clone(); - let snap: Arc = Arc::clone(&self.snapshot); - - async move { - let location: Location = location_event(index); - let store: Store<'_> = snap.store(&name, &[]); - let event: Vec = store.get(location).await?; + // No state data found + if data.is_empty() { + return Ok(None); + } - Ok((index, event)) - } - }) - // ================================ - // Parse the event - // ================================ - .try_filter_map(move |(index, json)| { - let did = did.clone(); - async move { - if json.is_empty() { - Err(Error::EventNotFound) - } else { - let event: Event = Event::from_json_slice(&json)?; - let commit: Commit = Commit::new(did, index, event); - - Ok(Some(commit)) - } - } - }) - // ================================ - // Downcast to "traditional" stream - // ================================ - .into_stream() - // ================================ - // Bail on any invalid event - // ================================ - .take_while(|event| future::ready(event.is_ok())) - // ================================ - // Create a boxed stream - // ================================ - .boxed(); - - Ok(stream) + // Deserialize and return + Ok(Some(IdentityState::from_json_slice(&data)?)) } - async fn purge(&self, did: &IotaDID) -> Result<()> { - type PurgeSet = (Generation, HashSet); - - async fn fold(mut output: PurgeSet, commit: Commit) -> Result { - if let EventData::MethodCreated(_, method) = commit.event().data() { - output.1.insert(method.location().clone()); - } - - let gen_a: u32 = commit.sequence().to_u32(); - let gen_b: u32 = output.0.to_u32(); - - Ok((Generation::from_u32(gen_a.max(gen_b)), output.1)) - } - - // Load the chain-specific store/vault + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()> { + // Load the chain-specific store let store: Store<'_> = self.store(&fmt_did(did)); - let vault: Vault<'_> = self.vault(did); - // Scan the event stream and collect a set of all key locations - let output: (Generation, HashSet) = self - .stream(did, Generation::new()) - .await? - .try_fold((Generation::new(), HashSet::new()), fold) - .await?; + // Serialize the state + let json: Vec = state.to_json_vec()?; - // Remove the state snapshot - store.del(location_snapshot()).await?; - - // Remove all events - for index in 0..output.0.to_u32() { - store.del(location_event(Generation::from_u32(index))).await?; - } - - // Remove all keys - for location in output.1 { - match location.method() { - MethodType::Ed25519VerificationKey2018 => { - vault.delete(location_seed(&location), false).await?; - vault.delete(location_skey(&location), false).await?; - } - MethodType::MerkleKeyCollection2021 => { - todo!("[Stronghold::purge] Handle MerkleKeyCollection2021") - } - } - } + // Write the state to the stronghold snapshot + store.set(location_state(), json, None).await?; Ok(()) } + async fn purge(&self, _did: &IotaDID) -> Result<()> { + // TODO: Will be re-implemented later with the key location refactor + todo!("stronghold purge not implemented"); + } + async fn published_generation(&self, did: &IotaDID) -> Result> { let store: Store<'_> = self.store(&fmt_did(did)); @@ -399,12 +291,12 @@ async fn sign_ed25519(vault: &Vault<'_>, payload: Vec, location: &KeyLocatio Ok(Signature::new(public_key, signature.into())) } -fn location_snapshot() -> Location { - Location::generic("$snapshot", Vec::new()) +fn location_chain_state() -> Location { + Location::generic("$chain_state", Vec::new()) } -fn location_event(index: Generation) -> Location { - Location::generic(format!("$event:{}", index), Vec::new()) +fn location_state() -> Location { + Location::generic("$state", Vec::new()) } fn location_seed(location: &KeyLocation) -> Location { @@ -420,14 +312,7 @@ fn location_published_generation() -> Location { } fn fmt_key(prefix: &str, location: &KeyLocation) -> Vec { - format!( - "{}:{}:{}:{}", - prefix, - location.integration_generation(), - location.diff_generation(), - location.fragment_name(), - ) - .into_bytes() + format!("{}:{}:{}", prefix, location.generation(), location.fragment_name()).into_bytes() } fn fmt_did(did: &IotaDID) -> String { diff --git a/identity-account/src/storage/traits.rs b/identity-account/src/storage/traits.rs index abb828dbe2..21c0981627 100644 --- a/identity-account/src/storage/traits.rs +++ b/identity-account/src/storage/traits.rs @@ -2,16 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 use core::fmt::Debug; -use futures::stream::BoxStream; -use futures::TryStreamExt; use identity_core::crypto::PrivateKey; use identity_core::crypto::PublicKey; use identity_iota::did::IotaDID; use crate::error::Result; -use crate::events::Commit; +use crate::identity::ChainState; use crate::identity::DIDLease; -use crate::identity::IdentitySnapshot; +use crate::identity::IdentityState; use crate::types::Generation; use crate::types::KeyLocation; use crate::types::Signature; @@ -28,7 +26,7 @@ pub trait Storage: Debug + Send + Sync + 'static { /// Write any unsaved changes to disk. async fn flush_changes(&self) -> Result<()>; - /// Attempt to obtain the exclusive permission to modify the given did. + /// Attempt to obtain the exclusive permission to modify the given `did`. /// The caller is expected to make no more modifications after the lease has been dropped. /// Returns an [`IdentityInUse`][crate::Error::IdentityInUse] error if already leased. async fn lease_did(&self, did: &IotaDID) -> Result; @@ -51,34 +49,25 @@ pub trait Storage: Debug + Send + Sync + 'static { /// Returns `true` if a keypair exists at the specified `location`. async fn key_exists(&self, did: &IotaDID, location: &KeyLocation) -> Result; - /// Returns the last generation that has been published to the tangle for the given `id`. + /// Returns the last generation that has been published to the tangle for the given `did`. async fn published_generation(&self, did: &IotaDID) -> Result>; - /// Sets the last generation that has been published to the tangle for the given `id`. + /// Sets the last generation that has been published to the tangle for the given `did`. async fn set_published_generation(&self, did: &IotaDID, index: Generation) -> Result<()>; - /// Returns the state snapshot of the identity specified by `id`. - async fn snapshot(&self, did: &IotaDID) -> Result>; + /// Returns the chain state of the identity specified by `did`. + async fn chain_state(&self, did: &IotaDID) -> Result>; - /// Sets a new state snapshot for the identity specified by `id`. - async fn set_snapshot(&self, did: &IotaDID, snapshot: &IdentitySnapshot) -> Result<()>; + /// Set the chain state of the identity specified by `did`. + async fn set_chain_state(&self, did: &IotaDID, chain_state: &ChainState) -> Result<()>; - /// Appends a set of commits to the event stream for the identity specified by `id`. - async fn append(&self, did: &IotaDID, commits: &[Commit]) -> Result<()>; + /// Returns the state of the identity specified by `did`. + async fn state(&self, did: &IotaDID) -> Result>; - /// Returns a stream of commits for the identity specified by `id`. - /// - /// The stream may be offset by `index`. - async fn stream(&self, did: &IotaDID, index: Generation) -> Result>>; + /// Sets a new state for the identity specified by `did`. + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()>; - /// Returns a list of all commits for the identity specified by `id`. - /// - /// The list may be offset by `index`. - async fn collect(&self, did: &IotaDID, index: Generation) -> Result> { - self.stream(did, index).await?.try_collect().await - } - - /// Removes the event stream and state snapshot for the identity specified by `id`. + /// Removes the keys and any state for the identity specified by `did`. async fn purge(&self, did: &IotaDID) -> Result<()>; } @@ -120,20 +109,20 @@ impl Storage for Box { (**self).key_exists(did, location).await } - async fn snapshot(&self, did: &IotaDID) -> Result> { - (**self).snapshot(did).await + async fn chain_state(&self, did: &IotaDID) -> Result> { + (**self).chain_state(did).await } - async fn set_snapshot(&self, did: &IotaDID, snapshot: &IdentitySnapshot) -> Result<()> { - (**self).set_snapshot(did, snapshot).await + async fn set_chain_state(&self, did: &IotaDID, chain_state: &ChainState) -> Result<()> { + (**self).set_chain_state(did, chain_state).await } - async fn append(&self, did: &IotaDID, commits: &[Commit]) -> Result<()> { - (**self).append(did, commits).await + async fn state(&self, did: &IotaDID) -> Result> { + (**self).state(did).await } - async fn stream(&self, did: &IotaDID, index: Generation) -> Result>> { - (**self).stream(did, index).await + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()> { + (**self).set_state(did, state).await } async fn purge(&self, did: &IotaDID) -> Result<()> { diff --git a/identity-account/src/tests/account.rs b/identity-account/src/tests/account.rs index f56f6ffe46..bd32fd2780 100644 --- a/identity-account/src/tests/account.rs +++ b/identity-account/src/tests/account.rs @@ -1,7 +1,10 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_core::common::Url; +use identity_did::verification::MethodScope; use identity_iota::did::IotaDID; +use identity_iota::tangle::MessageId; use crate::account::Account; use crate::account::AccountBuilder; @@ -58,3 +61,46 @@ async fn test_account_did_lease() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_account_chain_state() -> Result<()> { + let mut builder: AccountBuilder = AccountBuilder::default().testmode(true); + + let mut account: Account = builder.create_identity(IdentitySetup::default()).await?; + + let last_int_id = *account.chain_state().last_integration_message_id(); + + assert_ne!(last_int_id, MessageId::null()); + + // Assert that the last_diff_message_id is still null. + assert_eq!(account.chain_state().last_diff_message_id(), &MessageId::null()); + + // A diff update. + account + .update_identity() + .create_service() + .fragment("my-service-1") + .type_("MyCustomService") + .endpoint(Url::parse("https://example.com")?) + .apply() + .await?; + + // A diff update does not overwrite the int message id. + assert_eq!(&last_int_id, account.chain_state().last_integration_message_id()); + + // Assert that the last_diff_message_id was set. + assert_ne!(account.chain_state().last_diff_message_id(), &MessageId::null()); + + account + .update_identity() + .create_method() + .fragment("my-new-key") + .scope(MethodScope::capability_invocation()) + .apply() + .await?; + + // Int message id was overwritten. + assert_ne!(&last_int_id, account.chain_state().last_integration_message_id()); + + Ok(()) +} diff --git a/identity-account/src/tests/commands.rs b/identity-account/src/tests/commands.rs deleted file mode 100644 index c63cb75e56..0000000000 --- a/identity-account/src/tests/commands.rs +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::Arc; - -use crate::account::Account; -use crate::account::AccountConfig; -use crate::account::AccountSetup; -use crate::error::Error; -use crate::error::Result; -use crate::events::Update; -use crate::events::UpdateError; -use crate::identity::IdentitySetup; -use crate::identity::IdentitySnapshot; -use crate::identity::TinyMethod; -use crate::storage::MemStore; -use crate::types::Generation; -use crate::types::MethodSecret; -use identity_core::common::UnixTimestamp; -use identity_core::crypto::KeyCollection; -use identity_core::crypto::KeyPair; -use identity_core::crypto::KeyType; -use identity_core::crypto::PrivateKey; -use identity_did::verification::MethodScope; -use identity_did::verification::MethodType; - -fn account_setup() -> AccountSetup { - AccountSetup::new_with_options( - Arc::new(MemStore::new()), - Some(AccountConfig::new().testmode(true)), - None, - ) -} - -#[tokio::test] -async fn test_create_identity() -> Result<()> { - let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - - assert_eq!(snapshot.sequence(), Generation::from_u32(3)); - assert!(snapshot.identity().did().is_some()); - assert_ne!(snapshot.identity().created(), UnixTimestamp::EPOCH); - assert_ne!(snapshot.identity().updated(), UnixTimestamp::EPOCH); - - Ok(()) -} - -#[tokio::test] -async fn test_create_identity_network() -> Result<()> { - // Create an identity with a valid network string - let create_identity: IdentitySetup = IdentitySetup::new().network("dev")?.key_type(KeyType::Ed25519); - let account = Account::create_identity(account_setup(), create_identity).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - - // Ensure the identity creation was successful. - assert!(snapshot.identity().capability_invocation().is_ok()); - - Ok(()) -} - -#[tokio::test] -async fn test_create_identity_invalid_network() -> Result<()> { - // Attempt to create an identity with an invalid network string - let result: Result = IdentitySetup::new().network("Invalid=Network!"); - - // Ensure an `InvalidNetworkName` error is thrown - assert!(matches!( - result.unwrap_err(), - Error::IotaError(identity_iota::Error::InvalidNetworkName), - )); - - Ok(()) -} - -#[tokio::test] -async fn test_create_identity_already_exists() -> Result<()> { - let keypair = KeyPair::new_ed25519()?; - let identity_create = IdentitySetup::default() - .key_type(KeyType::Ed25519) - .method_secret(MethodSecret::Ed25519(keypair.private().clone())); - let account_setup = account_setup(); - - let account = Account::create_identity(account_setup.clone(), identity_create.clone()).await?; - - assert_eq!(account.load_snapshot().await?.sequence(), Generation::from(3)); - - let output = Account::create_identity(account_setup, identity_create).await; - - assert!(matches!( - output.unwrap_err(), - Error::UpdateError(UpdateError::DocumentAlreadyExists), - )); - - // version is still 3, no events have been committed - assert_eq!(account.load_snapshot().await?.sequence(), Generation::from(3)); - - Ok(()) -} - -#[tokio::test] -async fn test_create_identity_from_invalid_private_key() -> Result<()> { - let private_bytes: Box<[u8]> = Box::new([0; 33]); - let private_key: PrivateKey = PrivateKey::from(private_bytes); - - let id_create = IdentitySetup::new() - .key_type(KeyType::Ed25519) - .method_secret(MethodSecret::Ed25519(private_key)); - - let err = Account::create_identity(account_setup(), id_create).await.unwrap_err(); - - assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); - - Ok(()) -} - -#[tokio::test] -async fn test_create_method() -> Result<()> { - let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; - - let update: Update = Update::CreateMethod { - scope: MethodScope::default(), - method_secret: None, - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - account.process_update(update, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - - assert_eq!(snapshot.sequence(), Generation::from_u32(5)); - assert!(snapshot.identity().did().is_some()); - assert_ne!(snapshot.identity().created(), UnixTimestamp::EPOCH); - assert_ne!(snapshot.identity().updated(), UnixTimestamp::EPOCH); - assert_eq!(snapshot.identity().methods().len(), 2); - - let method: &TinyMethod = snapshot.identity().methods().fetch("key-1")?; - - assert_eq!(method.location().fragment_name(), "key-1"); - assert_eq!(method.location().method(), MethodType::Ed25519VerificationKey2018); - - Ok(()) -} - -#[tokio::test] -async fn test_create_method_duplicate_fragment() -> Result<()> { - let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; - - let update: Update = Update::CreateMethod { - scope: MethodScope::default(), - method_secret: None, - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(3)); - - account.process_update(update.clone(), false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(5)); - - let output: _ = account.process_update(update, false).await; - - assert!(matches!( - output.unwrap_err(), - Error::UpdateError(UpdateError::DuplicateKeyFragment(_)), - )); - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(5)); - - Ok(()) -} - -#[tokio::test] -async fn test_create_method_from_private_key() -> Result<()> { - let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; - - let keypair = KeyPair::new_ed25519()?; - - let update: Update = Update::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::Ed25519(keypair.private().clone())), - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - account.process_update(update, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - - let method: &TinyMethod = snapshot.identity().methods().fetch("key-1")?; - - let public_key = account.storage().key_get(account.did(), method.location()).await?; - - assert_eq!(public_key.as_ref(), keypair.public().as_ref()); - - Ok(()) -} - -#[tokio::test] -async fn test_create_method_from_invalid_private_key() -> Result<()> { - let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; - - let private_bytes: Box<[u8]> = Box::new([0; 33]); - let private_key = PrivateKey::from(private_bytes); - - let update: Update = Update::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::Ed25519(private_key)), - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - let err = account.process_update(update, false).await.unwrap_err(); - - assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); - - Ok(()) -} - -#[tokio::test] -async fn test_create_method_with_type_secret_mismatch() -> Result<()> { - let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; - - let private_bytes: Box<[u8]> = Box::new([0; 32]); - let private_key = PrivateKey::from(private_bytes); - - let update: Update = Update::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::Ed25519(private_key)), - type_: MethodType::MerkleKeyCollection2021, - fragment: "key-1".to_owned(), - }; - - let err = account.process_update(update, false).await.unwrap_err(); - - assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); - - let key_collection = KeyCollection::new_ed25519(4).unwrap(); - - let update: Update = Update::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::MerkleKeyCollection(key_collection)), - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-2".to_owned(), - }; - - let err = account.process_update(update, false).await.unwrap_err(); - - assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_method() -> Result<()> { - let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; - - let update: Update = Update::CreateMethod { - scope: MethodScope::default(), - method_secret: None, - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(3)); - - account.process_update(update, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - - assert_eq!(snapshot.sequence(), Generation::from_u32(5)); - assert_eq!(snapshot.identity().methods().len(), 2); - assert!(snapshot.identity().methods().contains("key-1")); - assert!(snapshot.identity().methods().get("key-1").is_some()); - assert!(snapshot.identity().methods().fetch("key-1").is_ok()); - - let update: Update = Update::DeleteMethod { - fragment: "key-1".to_owned(), - }; - - account.process_update(update, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot().await?; - - assert_eq!(snapshot.sequence(), Generation::from_u32(7)); - assert_eq!(snapshot.identity().methods().len(), 1); - assert!(!snapshot.identity().methods().contains("key-1")); - assert!(snapshot.identity().methods().get("key-1").is_none()); - assert!(snapshot.identity().methods().fetch("key-1").is_err()); - - Ok(()) -} diff --git a/identity-account/src/tests/lazy.rs b/identity-account/src/tests/lazy.rs index 007775ddde..c6312326c7 100644 --- a/identity-account/src/tests/lazy.rs +++ b/identity-account/src/tests/lazy.rs @@ -10,8 +10,8 @@ use crate::storage::MemStore; use futures::Future; use identity_core::common::Url; +use identity_did::verification::MethodScope; use identity_iota::chain::DocumentHistory; -use identity_iota::did::IotaVerificationMethod; use identity_iota::tangle::Client; use identity_iota::tangle::Network; use identity_iota::Error as IotaError; @@ -38,8 +38,7 @@ async fn test_lazy_updates() -> Result<()> { let config = AccountConfig::default().autopublish(false); let account_config = AccountSetup::new(Arc::new(MemStore::new())).config(config); - let mut account = - Account::create_identity(account_config, IdentitySetup::new().network(network.name()).unwrap()).await?; + let mut account = Account::create_identity(account_config, IdentitySetup::new().network(network.name())?).await?; account .update_identity() @@ -67,16 +66,11 @@ async fn test_lazy_updates() -> Result<()> { let doc = account.resolve_identity().await?; - let services = doc.service(); - assert_eq!(doc.methods().count(), 1); - assert_eq!(services.len(), 2); + assert_eq!(doc.service().len(), 2); - for service in services.iter() { - let service_fragment = service.id().fragment().unwrap(); - assert!(["my-service", "my-other-service"] - .iter() - .any(|fragment| *fragment == service_fragment)); + for service in ["my-service", "my-other-service"] { + assert!(doc.service().query(service).is_some()); } // =========================================================================== @@ -111,20 +105,16 @@ async fn test_lazy_updates() -> Result<()> { // =========================================================================== let doc = account.resolve_identity().await?; - let methods = doc.methods().collect::>(); assert_eq!(doc.service().len(), 0); - assert_eq!(methods.len(), 2); + assert_eq!(doc.methods().count(), 2); - for method in methods { - let method_fragment = method.id_core().fragment().unwrap_or_default(); - assert!(["sign-0", "new-method"] - .iter() - .any(|fragment| *fragment == method_fragment)); + for method in ["sign-0", "new-method"] { + assert!(doc.resolve_method(method).is_some()); } // =========================================================================== - // History assertions + // History assertions 1 // =========================================================================== let client: Client = Client::from_network(network).await?; @@ -134,6 +124,30 @@ async fn test_lazy_updates() -> Result<()> { assert_eq!(history.integration_chain_data.len(), 1); assert_eq!(history.diff_chain_data.len(), 1); + // =========================================================================== + // More updates to the identity + // =========================================================================== + + account + .update_identity() + .create_method() + .fragment("signing-key") + // Forces an integration update by adding a method able to update the document. + .scope(MethodScope::capability_invocation()) + .apply() + .await?; + + account.publish_updates().await?; + + // =========================================================================== + // History assertions 2 + // =========================================================================== + + let history: DocumentHistory = client.resolve_history(account.did()).await?; + + assert_eq!(history.integration_chain_data.len(), 2); + assert_eq!(history.diff_chain_data.len(), 0); + Ok(()) }) }) diff --git a/identity-account/src/tests/mod.rs b/identity-account/src/tests/mod.rs index 4c9d831550..e8ff16525b 100644 --- a/identity-account/src/tests/mod.rs +++ b/identity-account/src/tests/mod.rs @@ -2,5 +2,5 @@ // SPDX-License-Identifier: Apache-2.0 mod account; -mod commands; mod lazy; +mod updates; diff --git a/identity-account/src/tests/updates.rs b/identity-account/src/tests/updates.rs new file mode 100644 index 0000000000..b7a78d84ba --- /dev/null +++ b/identity-account/src/tests/updates.rs @@ -0,0 +1,653 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use crate::account::Account; +use crate::account::AccountConfig; +use crate::account::AccountSetup; +use crate::error::Error; +use crate::error::Result; +use crate::identity::IdentitySetup; +use crate::updates::Update; +use crate::updates::UpdateError; + +use crate::identity::IdentityState; +use crate::storage::MemStore; +use crate::types::Generation; +use crate::types::KeyLocation; +use crate::types::MethodSecret; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_core::crypto::KeyCollection; +use identity_core::crypto::KeyPair; +use identity_core::crypto::KeyType; +use identity_core::crypto::PrivateKey; +use identity_did::did::DID; +use identity_did::service::ServiceEndpoint; +use identity_did::verification::MethodRelationship; +use identity_did::verification::MethodScope; +use identity_did::verification::MethodType; +use identity_iota::did::IotaDID; +use identity_iota::tangle::Network; + +fn account_setup() -> AccountSetup { + AccountSetup::new_with_options( + Arc::new(MemStore::new()), + Some(AccountConfig::new().testmode(true)), + None, + ) +} + +#[tokio::test] +async fn test_create_identity() -> Result<()> { + let account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let expected_fragment = format!("{}{}", crate::updates::DEFAULT_UPDATE_METHOD_PREFIX, Generation::new()); + + let state: &IdentityState = account.state(); + + assert!(state.document().resolve_method(&expected_fragment).is_some()); + assert_eq!(state.document().as_document().verification_relationships().count(), 1); + assert_eq!(state.document().as_document().methods().count(), 1); + + let location = state + .method_location(MethodType::Ed25519VerificationKey2018, expected_fragment.clone()) + .unwrap(); + + // Ensure we can retrieve the correct location for the key. + assert_eq!( + location, + KeyLocation::new( + MethodType::Ed25519VerificationKey2018, + expected_fragment, + Generation::new() + ) + ); + + // Ensure the key exists in storage. + assert!(account.storage().key_exists(account.did(), &location).await.unwrap()); + + // Enure the state was written to storage. + assert!(account.load_state().await.is_ok()); + + // Ensure timestamps were recently set. + assert!(state.document().created() > Timestamp::from_unix(Timestamp::now_utc().to_unix() - 15)); + assert!(state.document().updated() > Timestamp::from_unix(Timestamp::now_utc().to_unix() - 15)); + + Ok(()) +} + +#[tokio::test] +async fn test_create_identity_network() -> Result<()> { + // Create an identity with a valid network string + let create_identity: IdentitySetup = IdentitySetup::new().network("dev")?.key_type(KeyType::Ed25519); + let account = Account::create_identity(account_setup(), create_identity).await?; + + assert_eq!( + account.did().network().unwrap().name(), + Network::try_from_name("dev").unwrap().name() + ); + + Ok(()) +} + +#[tokio::test] +async fn test_create_identity_invalid_network() -> Result<()> { + // Attempt to create an identity with an invalid network string + let result: Result = IdentitySetup::new().network("Invalid=Network!"); + + // Ensure an `InvalidNetworkName` error is thrown + assert!(matches!( + result.unwrap_err(), + Error::IotaError(identity_iota::Error::InvalidNetworkName), + )); + + Ok(()) +} + +#[tokio::test] +async fn test_create_identity_already_exists() -> Result<()> { + let keypair = KeyPair::new_ed25519()?; + let identity_create = IdentitySetup::default() + .key_type(KeyType::Ed25519) + .method_secret(MethodSecret::Ed25519(keypair.private().clone())); + let account_setup = account_setup(); + + let account = Account::create_identity(account_setup.clone(), identity_create.clone()).await?; + let did: IotaDID = account.did().to_owned(); + + let initial_state = account_setup.storage.state(&did).await?.unwrap(); + + let output = Account::create_identity(account_setup.clone(), identity_create).await; + + assert!(matches!( + output.unwrap_err(), + Error::UpdateError(UpdateError::DocumentAlreadyExists), + )); + + // Ensure nothing was overwritten in storage + assert_eq!(initial_state, account_setup.storage.state(&did).await?.unwrap()); + + Ok(()) +} + +#[tokio::test] +async fn test_create_identity_from_invalid_private_key() -> Result<()> { + let private_bytes: Box<[u8]> = Box::new([0; 33]); + let private_key: PrivateKey = PrivateKey::from(private_bytes); + + let id_create = IdentitySetup::new() + .key_type(KeyType::Ed25519) + .method_secret(MethodSecret::Ed25519(private_key)); + + let err = Account::create_identity(account_setup(), id_create).await.unwrap_err(); + + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); + + Ok(()) +} + +#[tokio::test] +async fn test_create_method() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let initial_state: IdentityState = account.state().to_owned(); + let method_type = MethodType::Ed25519VerificationKey2018; + + let fragment = "key-1".to_owned(); + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: method_type, + fragment: fragment.clone(), + }; + + account.process_update(update).await?; + + let state: &IdentityState = account.state(); + + // Ensure existence and key type + assert_eq!( + state.document().resolve_method(&fragment).unwrap().key_type(), + method_type + ); + + // Still only the default relationship. + assert_eq!(state.document().as_document().verification_relationships().count(), 1); + assert_eq!(state.document().as_document().methods().count(), 2); + + let location = state.method_location(method_type, fragment.clone()).unwrap(); + + // Ensure we can retrieve the correct location for the key. + assert_eq!( + location, + KeyLocation::new( + method_type, + fragment, + // `create_identity` calls publish, which increments the generation. + Generation::new().try_increment().unwrap(), + ) + ); + + // Ensure the key exists in storage. + assert!(account.storage().key_exists(account.did(), &location).await.unwrap()); + + // Ensure `created` wasn't updated. + assert_eq!(initial_state.document().created(), state.document().created()); + // Ensure `updated` was recently set. + assert!(state.document().updated() > Timestamp::from_unix(Timestamp::now_utc().to_unix() - 15)); + + Ok(()) +} + +#[tokio::test] +async fn test_create_scoped_method() -> Result<()> { + for scope in &[ + MethodScope::assertion_method(), + MethodScope::authentication(), + MethodScope::capability_delegation(), + MethodScope::capability_invocation(), + MethodScope::key_agreement(), + ] { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let fragment = "#key-1".to_owned(); + + let update: Update = Update::CreateMethod { + scope: *scope, + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: fragment.clone(), + }; + + account.process_update(update).await?; + + let state: &IdentityState = account.state(); + + assert_eq!(state.document().as_document().verification_relationships().count(), 2); + + assert_eq!(state.document().as_document().methods().count(), 2); + + let core_doc = state.document().as_document(); + + let contains = match scope { + MethodScope::VerificationRelationship(MethodRelationship::Authentication) => core_doc + .try_resolve_method_with_scope(&fragment, MethodScope::authentication()) + .is_ok(), + MethodScope::VerificationRelationship(MethodRelationship::AssertionMethod) => core_doc + .try_resolve_method_with_scope(&fragment, MethodScope::assertion_method()) + .is_ok(), + MethodScope::VerificationRelationship(MethodRelationship::KeyAgreement) => core_doc + .try_resolve_method_with_scope(&fragment, MethodScope::key_agreement()) + .is_ok(), + MethodScope::VerificationRelationship(MethodRelationship::CapabilityDelegation) => core_doc + .try_resolve_method_with_scope(&fragment, MethodScope::capability_delegation()) + .is_ok(), + MethodScope::VerificationRelationship(MethodRelationship::CapabilityInvocation) => core_doc + .try_resolve_method_with_scope(&fragment, MethodScope::capability_invocation()) + .is_ok(), + _ => unreachable!(), + }; + + assert!(contains); + } + + Ok(()) +} + +#[tokio::test] +async fn test_create_method_duplicate_fragment() -> Result<()> { + let mut account_setup = account_setup(); + account_setup.config = account_setup.config.testmode(true).autopublish(false); + + let mut account = Account::create_identity(account_setup, IdentitySetup::default()).await?; + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-1".to_owned(), + }; + + account.process_update(update.clone()).await?; + + let output = account.process_update(update.clone()).await; + + // Attempting to add a method with the same fragment in the same int and diff generation. + assert!(matches!( + output.unwrap_err(), + Error::UpdateError(UpdateError::DuplicateKeyLocation(_)), + )); + + // This increments the generation internally. + account.publish_updates().await?; + + let output = account.process_update(update).await; + + // Now the location is different due to the incremented generation, but the fragment is the same. + assert!(matches!( + output.unwrap_err(), + Error::UpdateError(UpdateError::DuplicateKeyFragment(_)), + )); + + Ok(()) +} + +#[tokio::test] +async fn test_create_method_from_private_key() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let keypair = KeyPair::new_ed25519()?; + let fragment = "key-1".to_owned(); + let method_type = MethodType::Ed25519VerificationKey2018; + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::Ed25519(keypair.private().clone())), + type_: method_type, + fragment: fragment.clone(), + }; + + account.process_update(update).await?; + + let state: &IdentityState = account.state(); + + assert!(state.document().resolve_method(&fragment).is_some()); + + let location = state.method_location(method_type, fragment).unwrap(); + let public_key = account.storage().key_get(account.did(), &location).await?; + + assert_eq!(public_key.as_ref(), keypair.public().as_ref()); + + Ok(()) +} + +#[tokio::test] +async fn test_create_method_from_invalid_private_key() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let private_bytes: Box<[u8]> = Box::new([0; 33]); + let private_key = PrivateKey::from(private_bytes); + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::Ed25519(private_key)), + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-1".to_owned(), + }; + + let err = account.process_update(update).await.unwrap_err(); + + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); + + Ok(()) +} + +#[tokio::test] +async fn test_attach_method_relationship() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let fragment = "key-1".to_owned(); + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: fragment.clone(), + }; + + account.process_update(update).await?; + + // One relationship by default. + assert_eq!( + account + .state() + .document() + .as_document() + .verification_relationships() + .count(), + 1 + ); + + let default_method_fragment = account + .document() + .default_signing_method() + .unwrap() + .id() + .fragment() + .unwrap() + .to_owned(); + + // Attempt attaching a relationship to an embedded method. + let update: Update = Update::AttachMethod { + relationships: vec![MethodRelationship::AssertionMethod, MethodRelationship::KeyAgreement], + fragment: default_method_fragment, + }; + + let err = account.process_update(update).await.unwrap_err(); + + assert!(matches!( + err, + Error::UpdateError(UpdateError::InvalidTargetEmbeddedMethod) + )); + + // No relationships were created. + assert_eq!(account.document().as_document().verification_relationships().count(), 1); + + assert_eq!(account.document().as_document().assertion_method().iter().count(), 0); + assert_eq!(account.document().as_document().key_agreement().iter().count(), 0); + + let update: Update = Update::AttachMethod { + relationships: vec![MethodRelationship::AssertionMethod, MethodRelationship::KeyAgreement], + fragment: fragment.clone(), + }; + + account.process_update(update).await?; + + // Relationships were created. + assert_eq!(account.document().as_document().verification_relationships().count(), 3); + + assert_eq!(account.document().as_document().assertion_method().len(), 1); + assert_eq!( + account + .document() + .as_document() + .assertion_method() + .first() + .unwrap() + .id() + .fragment() + .unwrap(), + fragment + ); + assert_eq!(account.document().as_document().key_agreement().len(), 1); + assert_eq!( + account + .document() + .as_document() + .key_agreement() + .first() + .unwrap() + .id() + .fragment() + .unwrap(), + fragment + ); + + Ok(()) +} + +#[tokio::test] +async fn test_detach_method_relationship() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let generic_fragment = "key-1".to_owned(); + let embedded_fragment = "embedded-1".to_owned(); + + // Add an embedded method. + let update: Update = Update::CreateMethod { + scope: MethodScope::authentication(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: embedded_fragment.clone(), + }; + + account.process_update(update).await?; + + // Attempt detaching a relationship from an embedded method. + let update: Update = Update::DetachMethod { + relationships: vec![MethodRelationship::Authentication], + fragment: embedded_fragment, + }; + + let err = account.process_update(update).await.unwrap_err(); + + assert!(matches!( + err, + Error::UpdateError(UpdateError::InvalidTargetEmbeddedMethod) + )); + + // No relationships were removed. + assert_eq!(account.document().as_document().verification_relationships().count(), 2); + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: MethodType::Ed25519VerificationKey2018, + fragment: generic_fragment.clone(), + }; + + account.process_update(update).await?; + + let update: Update = Update::AttachMethod { + relationships: vec![MethodRelationship::AssertionMethod, MethodRelationship::KeyAgreement], + fragment: generic_fragment.clone(), + }; + + account.process_update(update).await?; + + assert_eq!(account.document().as_document().assertion_method().len(), 1); + assert_eq!(account.document().as_document().key_agreement().len(), 1); + + let update: Update = Update::DetachMethod { + relationships: vec![MethodRelationship::AssertionMethod, MethodRelationship::KeyAgreement], + fragment: generic_fragment.clone(), + }; + + account.process_update(update).await?; + + assert_eq!(account.document().as_document().assertion_method().len(), 0); + assert_eq!(account.document().as_document().key_agreement().len(), 0); + + Ok(()) +} + +#[tokio::test] +async fn test_create_method_with_type_secret_mismatch() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let private_bytes: Box<[u8]> = Box::new([0; 32]); + let private_key = PrivateKey::from(private_bytes); + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::Ed25519(private_key)), + type_: MethodType::MerkleKeyCollection2021, + fragment: "key-1".to_owned(), + }; + + let err = account.process_update(update).await.unwrap_err(); + + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); + + let key_collection = KeyCollection::new_ed25519(4).unwrap(); + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: Some(MethodSecret::MerkleKeyCollection(key_collection)), + type_: MethodType::Ed25519VerificationKey2018, + fragment: "key-2".to_owned(), + }; + + let err = account.process_update(update).await.unwrap_err(); + + assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); + + Ok(()) +} + +#[tokio::test] +async fn test_delete_method() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let fragment = "key-1".to_owned(); + let method_type = MethodType::Ed25519VerificationKey2018; + let initial_state = account.state().to_owned(); + + let update: Update = Update::CreateMethod { + scope: MethodScope::default(), + method_secret: None, + type_: method_type, + fragment: fragment.clone(), + }; + + account.process_update(update).await?; + + // Ensure it was added. + assert!(account.state().document().resolve_method(&fragment).is_some()); + + let update: Update = Update::DeleteMethod { + fragment: "key-1".to_owned(), + }; + + account.process_update(update).await?; + + let state: &IdentityState = account.state(); + + // Ensure it no longer exists. + assert!(state.document().resolve_method(&fragment).is_none()); + + // Still only the default relationship. + assert_eq!(state.document().as_document().verification_relationships().count(), 1); + + assert_eq!(state.document().as_document().methods().count(), 1); + + let location = state.method_location(method_type, fragment.clone()).unwrap(); + + // Ensure the key still exists in storage - deletion in storage happens after successful publication. + assert!(account.storage().key_exists(account.did(), &location).await.unwrap()); + + // Ensure `created` wasn't updated. + assert_eq!(initial_state.document().created(), state.document().created()); + // Ensure `updated` was recently set. + assert!(state.document().updated() > Timestamp::from_unix(Timestamp::now_utc().to_unix() - 15)); + + Ok(()) +} + +#[tokio::test] +async fn test_insert_service() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + assert_eq!(account.document().service().len(), 0); + + let fragment = "#service-42".to_owned(); + + let update: Update = Update::CreateService { + fragment: fragment.clone(), + type_: "LinkedDomains".to_owned(), + endpoint: ServiceEndpoint::One(Url::parse("https://iota.org").unwrap()), + properties: None, + }; + + account.process_update(update.clone()).await?; + + assert_eq!(account.document().service().len(), 1); + + // Ensure the service can be queried. + let service_url = account.did().to_url().join(fragment).unwrap(); + assert!(account.document().service().query(service_url).is_some()); + + let err = account.process_update(update.clone()).await.unwrap_err(); + + assert!(matches!( + err, + Error::UpdateError(UpdateError::DuplicateServiceFragment(_)) + )); + + Ok(()) +} + +#[tokio::test] +async fn test_remove_service() -> Result<()> { + let mut account = Account::create_identity(account_setup(), IdentitySetup::default()).await?; + + let fragment = "#service-42".to_owned(); + + let update: Update = Update::CreateService { + fragment: fragment.clone(), + type_: "LinkedDomains".to_owned(), + endpoint: ServiceEndpoint::One(Url::parse("https://iota.org").unwrap()), + properties: None, + }; + + account.process_update(update).await.unwrap(); + + assert_eq!(account.document().service().len(), 1); + + let update: Update = Update::DeleteService { + fragment: fragment.clone(), + }; + + account.process_update(update.clone()).await.unwrap(); + + assert_eq!(account.document().service().len(), 0); + + // Attempting to remove a non-existing service returns an error. + let err = account.process_update(update).await.unwrap_err(); + + assert!(matches!(err, Error::UpdateError(UpdateError::ServiceNotFound))); + + Ok(()) +} diff --git a/identity-account/src/types/key_location.rs b/identity-account/src/types/key_location.rs index c78cebef56..c178d196d9 100644 --- a/identity-account/src/types/key_location.rs +++ b/identity-account/src/types/key_location.rs @@ -15,23 +15,17 @@ use crate::types::Generation; pub struct KeyLocation { method: MethodType, fragment: Fragment, - integration_generation: Generation, - diff_generation: Generation, + generation: Generation, } impl KeyLocation { /// Creates a new `KeyLocation`. - pub fn new( - method: MethodType, - fragment: String, - integration_generation: Generation, - diff_generation: Generation, - ) -> Self { + pub fn new(method: MethodType, fragment: String, generation: Generation) -> Self { Self { method, fragment: Fragment::new(fragment), - integration_generation, - diff_generation, + + generation, } } @@ -51,22 +45,16 @@ impl KeyLocation { } /// Returns the integration generation when this key was created. - pub fn integration_generation(&self) -> Generation { - self.integration_generation - } - - /// Returns the diff generation when this key was created. - pub fn diff_generation(&self) -> Generation { - self.diff_generation + pub fn generation(&self) -> Generation { + self.generation } } impl Display for KeyLocation { fn fmt(&self, f: &mut Formatter<'_>) -> Result { f.write_fmt(format_args!( - "({}:{}:{}:{})", - self.integration_generation, - self.diff_generation, + "({}:{}:{})", + self.generation, self.fragment, self.method.as_u32() )) diff --git a/identity-account/src/events/error.rs b/identity-account/src/updates/error.rs similarity index 86% rename from identity-account/src/events/error.rs rename to identity-account/src/updates/error.rs index 5333edab80..8e351d7350 100644 --- a/identity-account/src/events/error.rs +++ b/identity-account/src/updates/error.rs @@ -11,8 +11,6 @@ use crate::types::KeyLocation; pub enum UpdateError { #[error("document already exists")] DocumentAlreadyExists, - #[error("document not found")] - DocumentNotFound, #[error("verification method not found")] MethodNotFound, #[error("service not found")] @@ -23,6 +21,9 @@ pub enum UpdateError { InvalidMethodFragment(&'static str), #[error("invalid method secret: {0}")] InvalidMethodSecret(String), + /// Caused by attempting to attach or detach a relationship on an embedded method. + #[error("invalid target method - method is embedded")] + InvalidTargetEmbeddedMethod, #[error("missing required field - {0}")] MissingRequiredField(&'static str), #[error("duplicate key location - {0}")] diff --git a/identity-account/src/events/macros.rs b/identity-account/src/updates/macros.rs similarity index 86% rename from identity-account/src/events/macros.rs rename to identity-account/src/updates/macros.rs index c538273e4e..6bf3b97cb4 100644 --- a/identity-account/src/events/macros.rs +++ b/identity-account/src/updates/macros.rs @@ -9,7 +9,7 @@ macro_rules! ensure { }; } -macro_rules! impl_command_builder { +macro_rules! impl_update_builder { (@finish $this:ident optional $field:ident $ty:ty) => { $this.$field }; @@ -26,7 +26,7 @@ macro_rules! impl_command_builder { match $this.$field { Some(value) => value, None => return Err($crate::Error::UpdateError( - $crate::events::UpdateError::MissingRequiredField(stringify!($field)), + $crate::updates::UpdateError::MissingRequiredField(stringify!($field)), )), } }; @@ -59,13 +59,13 @@ macro_rules! impl_command_builder { } pub async fn apply(self) -> $crate::Result<()> { - let update = $crate::events::Update::$ident { + let update = $crate::updates::Update::$ident { $( - $field: impl_command_builder!(@finish self $requirement $field $ty $(= $value)?), + $field: impl_update_builder!(@finish self $requirement $field $ty $(= $value)?), )* }; - self.account.process_update(update, false).await + self.account.process_update(update).await } } diff --git a/identity-account/src/events/mod.rs b/identity-account/src/updates/mod.rs similarity index 61% rename from identity-account/src/events/mod.rs rename to identity-account/src/updates/mod.rs index f5555618cf..b932f8cecf 100644 --- a/identity-account/src/events/mod.rs +++ b/identity-account/src/updates/mod.rs @@ -4,14 +4,8 @@ #[macro_use] mod macros; -mod commit; -mod context; mod error; -mod event; mod update; -pub use self::commit::*; -pub use self::context::*; pub use self::error::*; -pub use self::event::*; pub use self::update::*; diff --git a/identity-account/src/updates/update.rs b/identity-account/src/updates/update.rs new file mode 100644 index 0000000000..2f29e15774 --- /dev/null +++ b/identity-account/src/updates/update.rs @@ -0,0 +1,455 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crypto::signatures::ed25519; + +use identity_core::common::Fragment; +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::crypto::KeyPair; +use identity_core::crypto::KeyType; +use identity_core::crypto::PublicKey; +use identity_did::did::CoreDIDUrl; +use identity_did::did::DID; +use identity_did::service::Service; +use identity_did::service::ServiceEndpoint; +use identity_did::verification::MethodRef; +use identity_did::verification::MethodRelationship; +use identity_did::verification::MethodScope; +use identity_did::verification::MethodType; +use identity_iota::did::IotaDID; +use identity_iota::did::IotaDIDUrl; +use identity_iota::did::IotaDocument; +use identity_iota::did::IotaVerificationMethod; +use identity_iota::tangle::UPDATE_METHOD_TYPES; + +use crate::account::Account; +use crate::error::Result; +use crate::identity::DIDLease; +use crate::identity::IdentitySetup; +use crate::identity::IdentityState; +use crate::storage::Storage; +use crate::types::Generation; +use crate::types::KeyLocation; +use crate::types::MethodSecret; +use crate::updates::UpdateError; + +pub const DEFAULT_UPDATE_METHOD_PREFIX: &str = "sign-"; + +pub(crate) async fn create_identity(setup: IdentitySetup, store: &dyn Storage) -> Result<(DIDLease, IdentityState)> { + let method_type = match setup.key_type { + KeyType::Ed25519 => MethodType::Ed25519VerificationKey2018, + }; + + // The method type must be able to sign document updates. + ensure!( + UPDATE_METHOD_TYPES.contains(&method_type), + UpdateError::InvalidMethodType(method_type) + ); + + let generation = Generation::new(); + let fragment: String = format!("{}{}", DEFAULT_UPDATE_METHOD_PREFIX, generation.to_u32()); + + let location: KeyLocation = KeyLocation::new(method_type, fragment, generation); + + let keypair: KeyPair = if let Some(MethodSecret::Ed25519(private_key)) = &setup.method_secret { + ensure!( + private_key.as_ref().len() == ed25519::SECRET_KEY_LENGTH, + UpdateError::InvalidMethodSecret(format!( + "an ed25519 private key requires {} bytes, found {}", + ed25519::SECRET_KEY_LENGTH, + private_key.as_ref().len() + )) + ); + + KeyPair::try_from_ed25519_bytes(private_key.as_ref())? + } else { + KeyPair::new_ed25519()? + }; + + // Generate a new DID from the public key + let did: IotaDID = if let Some(network) = &setup.network { + IotaDID::new_with_network(keypair.public().as_ref(), network.clone())? + } else { + IotaDID::new(keypair.public().as_ref())? + }; + + ensure!( + !store.key_exists(&did, &location).await?, + UpdateError::DocumentAlreadyExists + ); + + let did_lease = store.lease_did(&did).await?; + + let private_key = keypair.private().to_owned(); + std::mem::drop(keypair); + + let public: PublicKey = + insert_method_secret(store, &did, &location, method_type, MethodSecret::Ed25519(private_key)).await?; + + let method_fragment = location.fragment().to_owned(); + + let method: IotaVerificationMethod = + IotaVerificationMethod::from_did(did, setup.key_type, &public, method_fragment.name())?; + + let document = IotaDocument::from_verification_method(method)?; + + let mut state = IdentityState::new(document); + + // Store the generations at which the method was added + state.store_method_generations(method_fragment); + + Ok((did_lease, state)) +} + +#[derive(Clone, Debug)] +pub(crate) enum Update { + CreateMethod { + scope: MethodScope, + type_: MethodType, + fragment: String, + method_secret: Option, + }, + DeleteMethod { + fragment: String, + }, + AttachMethod { + fragment: String, + relationships: Vec, + }, + DetachMethod { + fragment: String, + relationships: Vec, + }, + CreateService { + fragment: String, + type_: String, + endpoint: ServiceEndpoint, + properties: Option, + }, + DeleteService { + fragment: String, + }, +} + +impl Update { + pub(crate) async fn process(self, did: &IotaDID, state: &mut IdentityState, storage: &dyn Storage) -> Result<()> { + debug!("[Update::process] Update = {:?}", self); + trace!("[Update::process] State = {:?}", state); + trace!("[Update::process] Store = {:?}", storage); + + match self { + Self::CreateMethod { + type_, + scope, + fragment, + method_secret, + } => { + let location: KeyLocation = state.key_location(type_, fragment)?; + + // The key location must be available. + // TODO: config: strict + ensure!( + !storage.key_exists(did, &location).await?, + UpdateError::DuplicateKeyLocation(location) + ); + + // The verification method must not exist. + ensure!( + state + .document() + .resolve_method(location.fragment().identifier()) + .is_none(), + UpdateError::DuplicateKeyFragment(location.fragment().clone()), + ); + + let public: PublicKey = if let Some(method_private_key) = method_secret { + insert_method_secret(storage, did, &location, type_, method_private_key).await + } else { + storage.key_new(did, &location).await + }?; + + let method: IotaVerificationMethod = + IotaVerificationMethod::from_did(did.to_owned(), KeyType::Ed25519, &public, location.fragment().name())?; + + state.store_method_generations(location.fragment().clone()); + + // We can ignore the result: we just checked that the method does not exist. + let _ = state.document_mut().insert_method(method, scope); + } + Self::DeleteMethod { fragment } => { + let fragment: Fragment = Fragment::new(fragment); + + // The verification method must exist + ensure!( + state.document().resolve_method(fragment.identifier()).is_some(), + UpdateError::MethodNotFound + ); + + let method_url: IotaDIDUrl = did.to_url().join(fragment.identifier())?; + let core_method_url: CoreDIDUrl = CoreDIDUrl::from(method_url.clone()); + + // Prevent deleting the last method capable of signing the DID document. + let capability_invocation_set = state.document().as_document().capability_invocation(); + let is_capability_invocation = capability_invocation_set + .iter() + .any(|method_ref| method_ref.id() == &core_method_url); + + ensure!( + !(is_capability_invocation && capability_invocation_set.len() == 1), + UpdateError::InvalidMethodFragment("cannot remove last signing method") + ); + + state.document_mut().remove_method(method_url)?; + } + Self::AttachMethod { + fragment, + relationships, + } => { + let fragment: Fragment = Fragment::new(fragment); + + let method_url: IotaDIDUrl = did.to_url().join(fragment.identifier())?; + + // The verification method must exist + ensure!( + state.document().resolve_method(fragment.identifier()).is_some(), + UpdateError::MethodNotFound + ); + + // The verification method must not be embedded. + ensure!( + !state + .document() + .as_document() + .verification_relationships() + .any(|method_ref| match method_ref { + MethodRef::Embed(method) => method.id().fragment() == method_url.fragment(), + MethodRef::Refer(_) => false, + }), + UpdateError::InvalidTargetEmbeddedMethod + ); + + for relationship in relationships { + // We ignore the boolean result: if the relationship already existed, that's fine. + let _ = state + .document_mut() + .attach_method_relationship(method_url.clone(), relationship) + .map_err(|_| UpdateError::MethodNotFound)?; + } + } + Self::DetachMethod { + fragment, + relationships, + } => { + let fragment: Fragment = Fragment::new(fragment); + + // The verification method must exist + ensure!( + state.document().resolve_method(fragment.identifier()).is_some(), + UpdateError::MethodNotFound + ); + + let method_url: IotaDIDUrl = did.to_url().join(fragment.identifier())?; + let core_method_url: CoreDIDUrl = CoreDIDUrl::from(method_url.clone()); + + // Prevent detaching the last method capable of signing the DID document. + let capability_invocation_set = state.document().as_document().capability_invocation(); + let is_capability_invocation = capability_invocation_set + .iter() + .any(|method_ref| method_ref.id() == &core_method_url); + + ensure!( + !(is_capability_invocation && capability_invocation_set.len() == 1), + UpdateError::InvalidMethodFragment("cannot remove last signing method") + ); + + // The verification method must not be embedded. + ensure!( + !state + .document() + .as_document() + .verification_relationships() + .any(|method_ref| match method_ref { + MethodRef::Embed(method) => method.id().fragment() == method_url.fragment(), + MethodRef::Refer(_) => false, + }), + UpdateError::InvalidTargetEmbeddedMethod + ); + + for relationship in relationships { + state + .document_mut() + .detach_method_relationship(method_url.clone(), relationship) + .map_err(|_| UpdateError::MethodNotFound)?; + } + } + Self::CreateService { + fragment, + type_, + endpoint, + properties, + } => { + let fragment = Fragment::new(fragment); + let did_url: CoreDIDUrl = did.as_ref().to_owned().join(fragment.identifier())?; + + // The service must not exist + ensure!( + state.document().service().query(&did_url).is_none(), + UpdateError::DuplicateServiceFragment(fragment.name().to_owned()), + ); + + let service: Service = Service::builder(properties.unwrap_or_default()) + .id(did_url) + .service_endpoint(endpoint) + .type_(type_) + .build()?; + + state.document_mut().insert_service(service); + } + Self::DeleteService { fragment } => { + let fragment: Fragment = Fragment::new(fragment); + let service_url = did.to_url().join(fragment.identifier())?; + + // The service must exist + ensure!( + state.document().service().query(&service_url).is_some(), + UpdateError::ServiceNotFound + ); + + state.document_mut().remove_service(service_url)?; + } + } + + state.document_mut().set_updated(Timestamp::now_utc()); + + Ok(()) + } +} + +async fn insert_method_secret( + store: &dyn Storage, + did: &IotaDID, + location: &KeyLocation, + method_type: MethodType, + method_secret: MethodSecret, +) -> Result { + match method_secret { + MethodSecret::Ed25519(private_key) => { + ensure!( + private_key.as_ref().len() == ed25519::SECRET_KEY_LENGTH, + UpdateError::InvalidMethodSecret(format!( + "an ed25519 private key requires {} bytes, found {}", + ed25519::SECRET_KEY_LENGTH, + private_key.as_ref().len() + )) + ); + + ensure!( + matches!(method_type, MethodType::Ed25519VerificationKey2018), + UpdateError::InvalidMethodSecret( + "MethodType::Ed25519VerificationKey2018 can only be used with an ed25519 method secret".to_owned(), + ) + ); + + store.key_insert(did, location, private_key).await + } + MethodSecret::MerkleKeyCollection(_) => { + ensure!( + matches!(method_type, MethodType::MerkleKeyCollection2021), + UpdateError::InvalidMethodSecret( + "MethodType::MerkleKeyCollection2021 can only be used with a MerkleKeyCollection method secret".to_owned(), + ) + ); + + todo!("[Update::CreateMethod] Handle MerkleKeyCollection") + } + } +} + +// ============================================================================= +// Update Builders +// ============================================================================= + +impl_update_builder!( +/// Create a new method on an identity. +/// +/// # Parameters +/// - `type_`: the type of the method, defaults to [`MethodType::Ed25519VerificationKey2018`]. +/// - `scope`: the scope of the method, defaults to [`MethodScope::default`]. +/// - `fragment`: the identifier of the method in the document, required. +/// - `method_secret`: the secret key to use for the method, optional. Will be generated when omitted. +CreateMethod { + @defaulte type_ MethodType = Ed25519VerificationKey2018, + @default scope MethodScope, + @required fragment String, + @optional method_secret MethodSecret +}); + +impl_update_builder!( +/// Delete a method on an identity. +/// +/// # Parameters +/// - `fragment`: the identifier of the method in the document, required. +DeleteMethod { + @required fragment String, +}); + +impl_update_builder!( +/// Attach one or more verification relationships to a method on an identity. +/// +/// # Parameters +/// - `relationships`: the relationships to add, defaults to an empty [`Vec`]. +/// - `fragment`: the identifier of the method in the document, required. +AttachMethod { + @required fragment String, + @default relationships Vec, +}); + +impl<'account> AttachMethodBuilder<'account> { + pub fn relationship(mut self, value: MethodRelationship) -> Self { + self.relationships.get_or_insert_with(Default::default).push(value); + self + } +} + +impl_update_builder!( +/// Detaches one or more verification relationships from a method on an identity. +/// +/// # Parameters +/// - `relationships`: the relationships to remove, defaults to an empty [`Vec`]. +/// - `fragment`: the identifier of the method in the document, required. +DetachMethod { + @required fragment String, + @default relationships Vec, +}); + +impl<'account> DetachMethodBuilder<'account> { + pub fn relationship(mut self, value: MethodRelationship) -> Self { + self.relationships.get_or_insert_with(Default::default).push(value); + self + } +} + +impl_update_builder!( +/// Create a new service on an identity. +/// +/// # Parameters +/// - `type_`: the type of the service, e.g. `"LinkedDomains"`, required. +/// - `fragment`: the identifier of the service in the document, required. +/// - `endpoint`: the `ServiceEndpoint` of the service, required. +/// - `properties`: additional properties of the service, optional. +CreateService { + @required fragment String, + @required type_ String, + @required endpoint ServiceEndpoint, + @optional properties Object, +}); + +impl_update_builder!( +/// Delete a service on an identity. +/// +/// # Parameters +/// - `fragment`: the identifier of the service in the document, required. +DeleteService { + @required fragment String, +}); diff --git a/identity-core/src/common/mod.rs b/identity-core/src/common/mod.rs index 53f8ad07e7..3f8c6cf2eb 100644 --- a/identity-core/src/common/mod.rs +++ b/identity-core/src/common/mod.rs @@ -9,7 +9,6 @@ mod fragment; mod object; mod one_or_many; mod timestamp; -mod unix_timestamp; mod url; pub use self::bitset::BitSet; @@ -19,5 +18,4 @@ pub use self::object::Object; pub use self::object::Value; pub use self::one_or_many::OneOrMany; pub use self::timestamp::Timestamp; -pub use self::unix_timestamp::UnixTimestamp; pub use self::url::Url; diff --git a/identity-core/src/common/unix_timestamp.rs b/identity-core/src/common/unix_timestamp.rs deleted file mode 100644 index 5a67ad8b26..0000000000 --- a/identity-core/src/common/unix_timestamp.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use core::fmt::Debug; -use core::fmt::Display; -use core::fmt::Formatter; -use core::fmt::Result; - -use crate::common::Timestamp; - -/// A simple representation of a unix timestamp. -#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] -#[repr(transparent)] -#[serde(transparent)] -pub struct UnixTimestamp(i64); - -impl UnixTimestamp { - /// Returns the default timestamp value. - pub const EPOCH: Self = Self(0); - - /// Returns the current time as a unix timestamp. - pub fn now_utc() -> Self { - Timestamp::now_utc().into() - } - - /// Returns true if this time is the unix epoch. - pub fn is_epoch(&self) -> bool { - static EPOCH: &UnixTimestamp = &UnixTimestamp::EPOCH; - self == EPOCH - } -} - -impl Debug for UnixTimestamp { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - f.write_fmt(format_args!("UnixTimestamp({})", self.0)) - } -} - -impl Display for UnixTimestamp { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - Display::fmt(&self.0, f) - } -} - -impl Default for UnixTimestamp { - fn default() -> Self { - Self::EPOCH - } -} - -impl From for UnixTimestamp { - fn from(other: Timestamp) -> Self { - Self(other.to_unix()) - } -} - -impl From for Timestamp { - fn from(other: UnixTimestamp) -> Self { - Timestamp::from_unix(other.0) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_roundtrip() { - let time: UnixTimestamp = UnixTimestamp::now_utc(); - let core: Timestamp = time.into(); - - assert_eq!(time, UnixTimestamp::from(core)); - } -} diff --git a/identity-did/src/document/core_document.rs b/identity-did/src/document/core_document.rs index ad2659d24a..f0d6ba8bb1 100644 --- a/identity-did/src/document/core_document.rs +++ b/identity-did/src/document/core_document.rs @@ -22,6 +22,7 @@ use crate::service::Service; use crate::utils::OrderedSet; use crate::verification::MethodQuery; use crate::verification::MethodRef; +use crate::verification::MethodRelationship; use crate::verification::MethodScope; use crate::verification::VerificationMethod; @@ -238,11 +239,21 @@ impl CoreDocument { pub fn insert_method(&mut self, method: VerificationMethod, scope: MethodScope) -> bool { match scope { MethodScope::VerificationMethod => self.verification_method.append(method), - MethodScope::Authentication => self.authentication.append(MethodRef::Embed(method)), - MethodScope::AssertionMethod => self.assertion_method.append(MethodRef::Embed(method)), - MethodScope::KeyAgreement => self.key_agreement.append(MethodRef::Embed(method)), - MethodScope::CapabilityDelegation => self.capability_delegation.append(MethodRef::Embed(method)), - MethodScope::CapabilityInvocation => self.capability_invocation.append(MethodRef::Embed(method)), + MethodScope::VerificationRelationship(MethodRelationship::Authentication) => { + self.authentication.append(MethodRef::Embed(method)) + } + MethodScope::VerificationRelationship(MethodRelationship::AssertionMethod) => { + self.assertion_method.append(MethodRef::Embed(method)) + } + MethodScope::VerificationRelationship(MethodRelationship::KeyAgreement) => { + self.key_agreement.append(MethodRef::Embed(method)) + } + MethodScope::VerificationRelationship(MethodRelationship::CapabilityDelegation) => { + self.capability_delegation.append(MethodRef::Embed(method)) + } + MethodScope::VerificationRelationship(MethodRelationship::CapabilityInvocation) => { + self.capability_invocation.append(MethodRef::Embed(method)) + } } } @@ -256,6 +267,59 @@ impl CoreDocument { self.verification_method.remove(did); } + /// Attaches the relationship to the given method, if the method exists. + /// + /// Note: The method needs to be in the set of verification methods, + /// so it cannot be an embedded one. + pub fn attach_method_relationship<'query, Q>( + &mut self, + method_query: Q, + relationship: MethodRelationship, + ) -> Result + where + Q: Into>, + { + let method: &VerificationMethod<_> = self + .resolve_method_with_scope(method_query, MethodScope::VerificationMethod) + .ok_or(Error::QueryMethodNotFound)?; + + let method_ref = MethodRef::Refer(method.id().clone()); + + let was_attached = match relationship { + MethodRelationship::Authentication => self.authentication_mut().append(method_ref), + MethodRelationship::AssertionMethod => self.assertion_method_mut().append(method_ref), + MethodRelationship::KeyAgreement => self.key_agreement_mut().append(method_ref), + MethodRelationship::CapabilityDelegation => self.capability_delegation_mut().append(method_ref), + MethodRelationship::CapabilityInvocation => self.capability_invocation_mut().append(method_ref), + }; + + Ok(was_attached) + } + + // Detaches the given relationship from the given method, if the method exists. + pub fn detach_method_relationship<'query, Q>( + &mut self, + method_query: Q, + relationship: MethodRelationship, + ) -> Result + where + Q: Into>, + { + let method: &VerificationMethod<_> = self.resolve_method(method_query).ok_or(Error::QueryMethodNotFound)?; + + let did_url: CoreDIDUrl = method.id().clone(); + + let was_detached = match relationship { + MethodRelationship::Authentication => self.authentication_mut().remove(&did_url), + MethodRelationship::AssertionMethod => self.assertion_method_mut().remove(&did_url), + MethodRelationship::KeyAgreement => self.key_agreement_mut().remove(&did_url), + MethodRelationship::CapabilityDelegation => self.capability_delegation_mut().remove(&did_url), + MethodRelationship::CapabilityInvocation => self.capability_invocation_mut().remove(&did_url), + }; + + Ok(was_detached) + } + /// Returns an iterator over all embedded verification methods in the DID Document. /// /// This excludes verification methods that are referenced by the DID Document. @@ -314,26 +378,32 @@ impl CoreDocument { /// Returns the first [`VerificationMethod`] with an `id` property matching the provided `query` /// and the verification relationship specified by `scope`. - pub fn resolve_method_with_scope<'query, 's: 'query, Q>( - &'s self, + pub fn resolve_method_with_scope<'query, 'me, Q>( + &'me self, query: Q, scope: MethodScope, ) -> Option<&VerificationMethod> where Q: Into>, { - let resolve_ref_helper = |method_ref: &'s MethodRef| self.resolve_method_ref(method_ref); + let resolve_ref_helper = |method_ref: &'me MethodRef| self.resolve_method_ref(method_ref); match scope { MethodScope::VerificationMethod => self.verification_method.query(query.into()), - MethodScope::Authentication => self.authentication.query(query.into()).and_then(resolve_ref_helper), - MethodScope::AssertionMethod => self.assertion_method.query(query.into()).and_then(resolve_ref_helper), - MethodScope::KeyAgreement => self.key_agreement.query(query.into()).and_then(resolve_ref_helper), - MethodScope::CapabilityDelegation => self + MethodScope::VerificationRelationship(MethodRelationship::Authentication) => { + self.authentication.query(query.into()).and_then(resolve_ref_helper) + } + MethodScope::VerificationRelationship(MethodRelationship::AssertionMethod) => { + self.assertion_method.query(query.into()).and_then(resolve_ref_helper) + } + MethodScope::VerificationRelationship(MethodRelationship::KeyAgreement) => { + self.key_agreement.query(query.into()).and_then(resolve_ref_helper) + } + MethodScope::VerificationRelationship(MethodRelationship::CapabilityDelegation) => self .capability_delegation .query(query.into()) .and_then(resolve_ref_helper), - MethodScope::CapabilityInvocation => self + MethodScope::VerificationRelationship(MethodRelationship::CapabilityInvocation) => self .capability_invocation .query(query.into()) .and_then(resolve_ref_helper), @@ -473,6 +543,8 @@ mod tests { use crate::did::DID; use crate::document::CoreDocument; use crate::verification::MethodData; + use crate::verification::MethodRelationship; + use crate::verification::MethodScope; use crate::verification::MethodType; use crate::verification::VerificationMethod; @@ -549,4 +621,94 @@ mod tests { assert_eq!(document.methods().next().unwrap().id().to_string(), "did:example:1234#key-1"); assert_eq!(document.methods().nth(2).unwrap().id().to_string(), "did:example:1234#key-3"); } + + #[test] + fn test_attach_verification_relationships() { + let mut document: CoreDocument = document(); + + let fragment = "#attach-test"; + let method = method(document.id(), fragment); + document.insert_method(method, MethodScope::VerificationMethod); + + assert!(document + .attach_method_relationship( + document.id().to_url().join(fragment).unwrap(), + MethodRelationship::CapabilityDelegation, + ) + .unwrap()); + + assert_eq!(document.verification_relationships().count(), 4); + + // Adding it a second time returns Ok(false). + assert!(!document + .attach_method_relationship( + document.id().to_url().join(fragment).unwrap(), + MethodRelationship::CapabilityDelegation, + ) + .unwrap()); + + // len is still 2. + assert_eq!(document.verification_relationships().count(), 4); + + // Attempting to attach a relationship to a non-existing method fails. + assert!(document + .attach_method_relationship( + document.id().to_url().join("#doesNotExist").unwrap(), + MethodRelationship::CapabilityDelegation, + ) + .is_err()); + + // Attempt to attach to an embedded method. + assert!(document + .attach_method_relationship( + document.id().to_url().join("#auth-key").unwrap(), + MethodRelationship::CapabilityDelegation, + ) + .is_err()); + } + + #[test] + fn test_detach_verification_relationships() { + let mut document: CoreDocument = document(); + + let fragment = "#detach-test"; + let method = method(document.id(), fragment); + document.insert_method(method, MethodScope::VerificationMethod); + + assert!(document + .attach_method_relationship( + document.id().to_url().join(fragment).unwrap(), + MethodRelationship::AssertionMethod, + ) + .unwrap()); + + assert!(document + .detach_method_relationship( + document.id().to_url().join(fragment).unwrap(), + MethodRelationship::AssertionMethod, + ) + .unwrap()); + + // len is 1; the relationship was removed. + assert_eq!(document.verification_relationships().count(), 3); + + // Removing it a second time returns Ok(false). + assert!(!document + .detach_method_relationship( + document.id().to_url().join(fragment).unwrap(), + MethodRelationship::AssertionMethod, + ) + .unwrap()); + + // len is still 1. + assert_eq!(document.verification_relationships().count(), 3); + + // Attempting to detach a relationship from a non-existing method fails. + assert!(document + .detach_method_relationship( + document.id().to_url().join("#doesNotExist").unwrap(), + MethodRelationship::AssertionMethod, + ) + .is_err()); + } } diff --git a/identity-did/src/utils/ordered_set.rs b/identity-did/src/utils/ordered_set.rs index 6e76eeb66f..0c970886cb 100644 --- a/identity-did/src/utils/ordered_set.rs +++ b/identity-did/src/utils/ordered_set.rs @@ -162,12 +162,17 @@ impl OrderedSet { /// Removes all matching items from the set. #[inline] - pub fn remove(&mut self, item: &U) + pub fn remove(&mut self, item: &U) -> bool where T: KeyComparable, U: KeyComparable, { - self.0.retain(|this| this.borrow().as_key() != item.as_key()); + if self.contains(item) { + self.0.retain(|this| this.borrow().as_key() != item.as_key()); + true + } else { + false + } } fn change(&mut self, data: T, f: F) -> bool @@ -257,7 +262,7 @@ impl OrderedSet where T: AsRef, { - pub(crate) fn query<'query, Q>(&self, query: Q) -> Option<&T> + pub fn query<'query, Q>(&self, query: Q) -> Option<&T> where Q: Into>, { diff --git a/identity-did/src/verification/method_relationship.rs b/identity-did/src/verification/method_relationship.rs new file mode 100644 index 0000000000..66ce038431 --- /dev/null +++ b/identity-did/src/verification/method_relationship.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Verification relationships. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, strum::IntoStaticStr)] +pub enum MethodRelationship { + Authentication, + AssertionMethod, + KeyAgreement, + CapabilityDelegation, + CapabilityInvocation, +} diff --git a/identity-did/src/verification/method_scope.rs b/identity-did/src/verification/method_scope.rs index 61e6a46b6d..c473a1caa4 100644 --- a/identity-did/src/verification/method_scope.rs +++ b/identity-did/src/verification/method_scope.rs @@ -6,28 +6,42 @@ use core::str::FromStr; use crate::error::Error; use crate::error::Result; +use crate::verification::MethodRelationship; + /// Verification method group used to refine the scope of a method query. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] pub enum MethodScope { VerificationMethod, - Authentication, - AssertionMethod, - KeyAgreement, - CapabilityDelegation, - CapabilityInvocation, + VerificationRelationship(MethodRelationship), } impl MethodScope { - pub const fn as_str(&self) -> &'static str { + pub fn as_str(&self) -> &'static str { match self { Self::VerificationMethod => "VerificationMethod", - Self::Authentication => "Authentication", - Self::AssertionMethod => "AssertionMethod", - Self::KeyAgreement => "KeyAgreement", - Self::CapabilityDelegation => "CapabilityDelegation", - Self::CapabilityInvocation => "CapabilityInvocation", + Self::VerificationRelationship(relationship) => relationship.into(), } } + + pub const fn authentication() -> Self { + Self::VerificationRelationship(MethodRelationship::Authentication) + } + + pub const fn capability_delegation() -> Self { + Self::VerificationRelationship(MethodRelationship::CapabilityDelegation) + } + + pub const fn capability_invocation() -> Self { + Self::VerificationRelationship(MethodRelationship::CapabilityInvocation) + } + + pub const fn assertion_method() -> Self { + Self::VerificationRelationship(MethodRelationship::AssertionMethod) + } + + pub const fn key_agreement() -> Self { + Self::VerificationRelationship(MethodRelationship::KeyAgreement) + } } impl Default for MethodScope { @@ -42,12 +56,18 @@ impl FromStr for MethodScope { fn from_str(string: &str) -> Result { match string { "VerificationMethod" => Ok(Self::VerificationMethod), - "Authentication" => Ok(Self::Authentication), - "AssertionMethod" => Ok(Self::AssertionMethod), - "KeyAgreement" => Ok(Self::KeyAgreement), - "CapabilityDelegation" => Ok(Self::CapabilityDelegation), - "CapabilityInvocation" => Ok(Self::CapabilityInvocation), + "Authentication" => Ok(Self::VerificationRelationship(MethodRelationship::Authentication)), + "AssertionMethod" => Ok(Self::VerificationRelationship(MethodRelationship::AssertionMethod)), + "KeyAgreement" => Ok(Self::VerificationRelationship(MethodRelationship::KeyAgreement)), + "CapabilityDelegation" => Ok(Self::VerificationRelationship(MethodRelationship::CapabilityDelegation)), + "CapabilityInvocation" => Ok(Self::VerificationRelationship(MethodRelationship::CapabilityInvocation)), _ => Err(Error::UnknownMethodScope), } } } + +impl From for MethodScope { + fn from(relationship: MethodRelationship) -> Self { + Self::VerificationRelationship(relationship) + } +} diff --git a/identity-did/src/verification/mod.rs b/identity-did/src/verification/mod.rs index 593959570d..6426a2ae91 100644 --- a/identity-did/src/verification/mod.rs +++ b/identity-did/src/verification/mod.rs @@ -10,6 +10,7 @@ mod builder; mod method_data; mod method_query; mod method_ref; +mod method_relationship; mod method_scope; mod method_type; mod traits; @@ -19,6 +20,7 @@ pub use self::builder::MethodBuilder; pub use self::method_data::MethodData; pub use self::method_query::MethodQuery; pub use self::method_ref::MethodRef; +pub use self::method_relationship::MethodRelationship; pub use self::method_scope::MethodScope; pub use self::method_type::MethodType; pub use self::traits::MethodUriType; diff --git a/identity-iota/src/did/doc/iota_document.rs b/identity-iota/src/did/doc/iota_document.rs index 1fcf54acf8..c8b8335754 100644 --- a/identity-iota/src/did/doc/iota_document.rs +++ b/identity-iota/src/did/doc/iota_document.rs @@ -8,6 +8,7 @@ use core::fmt::Display; use core::fmt::Formatter; use core::fmt::Result as FmtResult; +use identity_did::verification::MethodRelationship; use serde::Serialize; use identity_core::common::Object; @@ -134,8 +135,12 @@ impl IotaDocument { IotaDID::new(public_key.as_ref())? }; - let method: IotaVerificationMethod = - IotaVerificationMethod::from_did(did, keypair, fragment.unwrap_or(Self::DEFAULT_METHOD_FRAGMENT))?; + let method: IotaVerificationMethod = IotaVerificationMethod::from_did( + did, + keypair.type_(), + keypair.public(), + fragment.unwrap_or(Self::DEFAULT_METHOD_FRAGMENT), + )?; Self::from_verification_method(method) } @@ -388,6 +393,18 @@ impl IotaDocument { Ok(()) } + pub fn attach_method_relationship(&mut self, did_url: IotaDIDUrl, relationship: MethodRelationship) -> Result { + let core_did_url: CoreDIDUrl = CoreDIDUrl::from(did_url); + let was_attached = self.document.attach_method_relationship(core_did_url, relationship)?; + Ok(was_attached) + } + + pub fn detach_method_relationship(&mut self, did_url: IotaDIDUrl, relationship: MethodRelationship) -> Result { + let core_did_url: CoreDIDUrl = CoreDIDUrl::from(did_url); + let was_detached = self.document.detach_method_relationship(core_did_url, relationship)?; + Ok(was_detached) + } + /// Returns the first [`IotaVerificationMethod`] with an `id` property /// matching the provided `query`. pub fn resolve_method<'query, Q>(&self, query: Q) -> Option<&IotaVerificationMethod> @@ -453,7 +470,7 @@ impl IotaDocument { // Ensure signing method has a capability invocation verification relationship. let method: &VerificationMethod<_> = self .as_document() - .try_resolve_method_with_scope(method_query.into(), MethodScope::CapabilityInvocation)?; + .try_resolve_method_with_scope(method_query.into(), MethodScope::capability_invocation())?; let _ = Self::check_signing_method(method)?; // Specify the full method DID Url if the verification method id does not match the document id. @@ -499,7 +516,7 @@ impl IotaDocument { let signature: &Signature = signed.try_signature()?; let method: &VerificationMethod<_> = signer .as_document() - .try_resolve_method_with_scope(signature, MethodScope::CapabilityInvocation)?; + .try_resolve_method_with_scope(signature, MethodScope::capability_invocation())?; // Verify signature. let public: PublicKey = method.key_data().try_decode()?.into(); @@ -640,7 +657,7 @@ impl IotaDocument { let method_query = method_query.into(); let _ = self .as_document() - .try_resolve_method_with_scope(method_query.clone(), MethodScope::CapabilityInvocation)?; + .try_resolve_method_with_scope(method_query.clone(), MethodScope::capability_invocation())?; self.sign_data(&mut diff, private_key, method_query)?; @@ -654,7 +671,7 @@ impl IotaDocument { /// /// Fails if an unsupported verification method is used or the verification operation fails. pub fn verify_diff(&self, diff: &DocumentDiff) -> Result<()> { - self.verify_data_with_scope(diff, MethodScope::CapabilityInvocation) + self.verify_data_with_scope(diff, MethodScope::capability_invocation()) } /// Verifies a `DocumentDiff` signature and merges the changes into `self`. @@ -1202,7 +1219,7 @@ mod tests { // Add a new capability invocation method directly let new_keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); let new_method: IotaVerificationMethod = IotaVerificationMethod::from_keypair(&new_keypair, "new_signer").unwrap(); - document.insert_method(new_method, MethodScope::CapabilityInvocation); + document.insert_method(new_method, MethodScope::capability_invocation()); // INVALID - try sign using the wrong private key document.sign_self(keypair.private(), "#new_signer").unwrap(); @@ -1245,10 +1262,10 @@ mod tests { // INVALID - try sign using any verification relationship other than capability invocation. for method_scope in [ MethodScope::VerificationMethod, - MethodScope::AssertionMethod, - MethodScope::CapabilityDelegation, - MethodScope::Authentication, - MethodScope::KeyAgreement, + MethodScope::assertion_method(), + MethodScope::capability_delegation(), + MethodScope::authentication(), + MethodScope::key_agreement(), ] { let (mut document, _) = generate_document(); // Add a new method unable to sign the document. @@ -1269,7 +1286,7 @@ mod tests { let merkle_key_method = IotaVerificationMethod::create_merkle_key::(document.id().clone(), &key_collection, "merkle-key") .unwrap(); - document.insert_method(merkle_key_method, MethodScope::CapabilityInvocation); + document.insert_method(merkle_key_method, MethodScope::capability_invocation()); assert!(document .sign_self(key_collection.private(0).unwrap(), "merkle-key") .is_err()); @@ -1281,11 +1298,11 @@ mod tests { fn test_diff() { // Ensure only capability invocation methods are allowed to sign a diff. for scope in [ - MethodScope::AssertionMethod, - MethodScope::Authentication, - MethodScope::CapabilityDelegation, - MethodScope::CapabilityInvocation, - MethodScope::KeyAgreement, + MethodScope::assertion_method(), + MethodScope::authentication(), + MethodScope::capability_delegation(), + MethodScope::capability_invocation(), + MethodScope::key_agreement(), MethodScope::VerificationMethod, ] { let key1: KeyPair = generate_testkey(); @@ -1316,7 +1333,7 @@ mod tests { // Try generate and sign a diff using the specified method. let diff_result = doc1.diff(&doc2, *doc1.message_id(), key2.private(), method_fragment.as_str()); - if scope == MethodScope::CapabilityInvocation { + if scope == MethodScope::capability_invocation() { let diff = diff_result.unwrap(); assert!(doc1.verify_data(&diff).is_ok()); assert!(doc1.verify_diff(&diff).is_ok()); @@ -1344,11 +1361,11 @@ mod tests { // Try sign using each type of verification relationship. for scope in [ - MethodScope::AssertionMethod, - MethodScope::Authentication, - MethodScope::CapabilityDelegation, - MethodScope::CapabilityInvocation, - MethodScope::KeyAgreement, + MethodScope::assertion_method(), + MethodScope::authentication(), + MethodScope::capability_delegation(), + MethodScope::capability_invocation(), + MethodScope::key_agreement(), MethodScope::VerificationMethod, ] { // Add a new method. @@ -1368,11 +1385,11 @@ mod tests { // Ensure only the correct scope is valid. for scope_check in [ - MethodScope::AssertionMethod, - MethodScope::Authentication, - MethodScope::CapabilityDelegation, - MethodScope::CapabilityInvocation, - MethodScope::KeyAgreement, + MethodScope::assertion_method(), + MethodScope::authentication(), + MethodScope::capability_delegation(), + MethodScope::capability_invocation(), + MethodScope::key_agreement(), MethodScope::VerificationMethod, ] { let result = document.verify_data_with_scope(&data, scope_check); @@ -1451,7 +1468,7 @@ mod tests { let keypair_new: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); let method_new: IotaVerificationMethod = IotaVerificationMethod::from_keypair(&keypair_new, "new_signer").unwrap(); - document.insert_method(method_new, MethodScope::CapabilityInvocation); + document.insert_method(method_new, MethodScope::capability_invocation()); // Sign the document using the new key. document.sign_self(keypair_new.private(), "#new_signer").unwrap(); assert!(document.verify_self_signed().is_ok()); @@ -1489,7 +1506,7 @@ mod tests { let capability_invocation: IotaVerificationMethod = IotaVerificationMethod::try_from_core( document .as_document() - .try_resolve_method_with_scope(signing_method.id(), MethodScope::CapabilityInvocation) + .try_resolve_method_with_scope(signing_method.id(), MethodScope::capability_invocation()) .unwrap() .clone(), ) @@ -1500,7 +1517,7 @@ mod tests { let new_keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); let new_method: IotaVerificationMethod = IotaVerificationMethod::from_keypair(&new_keypair, "new_signer").unwrap(); let new_method_id: IotaDIDUrl = new_method.id(); - document.insert_method(new_method, MethodScope::CapabilityInvocation); + document.insert_method(new_method, MethodScope::capability_invocation()); assert_eq!(document.default_signing_method().unwrap().id(), signing_method.id()); // Removing the original signing method returns the next one. @@ -1613,10 +1630,10 @@ mod tests { // Update the key material of the existing verification method test-0. let keypair2: KeyPair = KeyPair::new_ed25519().unwrap(); let method2: IotaVerificationMethod = - IotaVerificationMethod::from_did(doc1.id().to_owned(), &keypair2, "test-0").unwrap(); + IotaVerificationMethod::from_did(doc1.id().to_owned(), keypair2.type_(), keypair2.public(), "test-0").unwrap(); doc1.remove_method(doc1.id().to_url().join("#test-0").unwrap()).unwrap(); - doc1.insert_method(method2, MethodScope::CapabilityInvocation); + doc1.insert_method(method2, MethodScope::capability_invocation()); // Even though the method fragment is the same, the key material has been updated // so the two documents are expected to not be equal. @@ -1625,9 +1642,9 @@ mod tests { let mut doc2 = doc1.clone(); let keypair3: KeyPair = KeyPair::new_ed25519().unwrap(); let method3: IotaVerificationMethod = - IotaVerificationMethod::from_did(doc1.id().to_owned(), &keypair3, "test-0").unwrap(); + IotaVerificationMethod::from_did(doc1.id().to_owned(), keypair3.type_(), keypair3.public(), "test-0").unwrap(); - let was_inserted = doc2.insert_method(method3, MethodScope::CapabilityInvocation); + let was_inserted = doc2.insert_method(method3, MethodScope::capability_invocation()); // Nothing was inserted, because a method with the same fragment already existed. assert!(!was_inserted); diff --git a/identity-iota/src/did/doc/iota_verification_method.rs b/identity-iota/src/did/doc/iota_verification_method.rs index f26f024ad2..7e455fa632 100644 --- a/identity-iota/src/did/doc/iota_verification_method.rs +++ b/identity-iota/src/did/doc/iota_verification_method.rs @@ -15,6 +15,7 @@ use identity_core::crypto::merkle_key::MerkleDigest; use identity_core::crypto::KeyCollection; use identity_core::crypto::KeyPair; use identity_core::crypto::KeyType; +use identity_core::crypto::PublicKey; use identity_did::did::CoreDID; use identity_did::did::CoreDIDUrl; use identity_did::did::DID; @@ -65,7 +66,7 @@ impl IotaVerificationMethod { let key: &[u8] = keypair.public().as_ref(); let did: IotaDID = IotaDID::new(key)?; - Self::from_did(did, keypair, fragment) + Self::from_did(did, keypair.type_(), keypair.public(), fragment) } /// Creates a new [`IotaVerificationMethod`] from the given `keypair` on the specified @@ -74,11 +75,11 @@ impl IotaVerificationMethod { let key: &[u8] = keypair.public().as_ref(); let did: IotaDID = IotaDID::new_with_network(key, network)?; - Self::from_did(did, keypair, fragment) + Self::from_did(did, keypair.type_(), keypair.public(), fragment) } /// Creates a new [`IotaVerificationMethod`] from the given `did` and `keypair`. - pub fn from_did(did: IotaDID, keypair: &KeyPair, fragment: &str) -> Result { + pub fn from_did(did: IotaDID, key_type: KeyType, public_key: &PublicKey, fragment: &str) -> Result { let tag: String = format!("#{}", fragment); let key: IotaDIDUrl = did.to_url().join(tag)?; @@ -86,10 +87,10 @@ impl IotaVerificationMethod { .id(CoreDIDUrl::from(key)) .controller(did.into()); - match keypair.type_() { + match key_type { KeyType::Ed25519 => { builder = builder.key_type(MethodType::Ed25519VerificationKey2018); - builder = builder.key_data(MethodData::new_multibase(keypair.public())); + builder = builder.key_data(MethodData::new_multibase(public_key)); } } diff --git a/identity-iota/src/tangle/mod.rs b/identity-iota/src/tangle/mod.rs index 00ed3d95f2..a157501886 100644 --- a/identity-iota/src/tangle/mod.rs +++ b/identity-iota/src/tangle/mod.rs @@ -21,6 +21,8 @@ pub use self::message::MessageIndex; pub use self::message::TryFromMessage; pub use self::network::Network; pub use self::network::NetworkName; +pub use self::publish::PublishType; +pub use self::publish::UPDATE_METHOD_TYPES; pub use self::receipt::Receipt; pub use self::traits::TangleRef; pub use self::traits::TangleResolve; @@ -30,5 +32,6 @@ mod client_builder; mod client_map; mod message; mod network; +mod publish; mod receipt; mod traits; diff --git a/identity-iota/src/tangle/publish.rs b/identity-iota/src/tangle/publish.rs new file mode 100644 index 0000000000..2abbe54a1c --- /dev/null +++ b/identity-iota/src/tangle/publish.rs @@ -0,0 +1,265 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_did::verification::MethodRef; +use identity_did::verification::MethodType; +use identity_did::verification::VerificationMethod; + +use crate::did::IotaDocument; + +// Method types allowed to sign a DID document update. +pub const UPDATE_METHOD_TYPES: &[MethodType] = &[MethodType::Ed25519VerificationKey2018]; + +/// Determines whether an updated document needs to be published as an integration or diff message. +#[derive(Clone, Copy, Debug)] +pub enum PublishType { + Integration, + Diff, +} + +impl PublishType { + /// Compares two versions of a document and returns whether it needs to be published + /// as an integration or diff message. If `None` is returned, no update is required. + /// + /// Note: A newly created document must always be published as an integration message, and + /// this method does not handle this case. + pub fn new(old_doc: &IotaDocument, new_doc: &IotaDocument) -> Option { + if old_doc == new_doc { + return None; + } + + let old_capability_invocation_set: Vec> = old_doc + .as_document() + .capability_invocation() + .iter() + .map(|method_ref| match method_ref { + MethodRef::Embed(method) => Some(method), + MethodRef::Refer(did_url) => old_doc.as_document().resolve_method(did_url), + }) + .filter(|method| { + if let Some(method) = method { + UPDATE_METHOD_TYPES.contains(&method.key_type()) + } else { + true + } + }) + .collect(); + + let new_capability_invocation_set: Vec> = new_doc + .as_document() + .capability_invocation() + .iter() + .map(|method_ref| match method_ref { + MethodRef::Embed(method) => Some(method), + MethodRef::Refer(did_url) => new_doc.as_document().resolve_method(did_url), + }) + .filter(|method| { + if let Some(method) = method { + UPDATE_METHOD_TYPES.contains(&method.key_type()) + } else { + true + } + }) + .collect(); + + if old_capability_invocation_set != new_capability_invocation_set { + Some(PublishType::Integration) + } else { + Some(PublishType::Diff) + } + } +} + +#[cfg(test)] +mod test { + use identity_core::crypto::merkle_key::Sha256; + use identity_core::crypto::KeyCollection; + use identity_core::crypto::KeyPair; + use identity_did::did::DID; + use identity_did::verification::MethodScope; + + use crate::did::IotaVerificationMethod; + use crate::tangle::TangleRef; + use crate::Result; + + use super::*; + + // Returns a document with an embedded capability invocation method, and a generic verification method, + // that also has as an attached capability invocation verification relationship. + fn document() -> IotaDocument { + let initial_keypair: KeyPair = KeyPair::new_ed25519().unwrap(); + let method: IotaVerificationMethod = IotaVerificationMethod::from_keypair(&initial_keypair, "embedded").unwrap(); + + let mut old_doc: IotaDocument = IotaDocument::from_verification_method(method).unwrap(); + + let keypair: KeyPair = KeyPair::new_ed25519().unwrap(); + let method2: IotaVerificationMethod = + IotaVerificationMethod::from_did(old_doc.did().to_owned(), keypair.type_(), keypair.public(), "generic").unwrap(); + + let method3_url = method2.id(); + + old_doc.insert_method(method2, MethodScope::VerificationMethod); + old_doc + .attach_method_relationship( + method3_url, + identity_did::verification::MethodRelationship::CapabilityInvocation, + ) + .unwrap(); + + old_doc + } + + #[test] + fn test_publish_type_insert_new_embedded_capability_invocation_method() -> Result<()> { + let old_doc = document(); + + assert!(matches!(PublishType::new(&old_doc, &old_doc), None)); + + let mut new_doc = old_doc.clone(); + + let keypair: KeyPair = KeyPair::new_ed25519()?; + let method2: IotaVerificationMethod = + IotaVerificationMethod::from_did(old_doc.did().to_owned(), keypair.type_(), keypair.public(), "test-2")?; + + new_doc.insert_method(method2, MethodScope::capability_invocation()); + + assert!(matches!( + PublishType::new(&old_doc, &new_doc), + Some(PublishType::Integration) + )); + + Ok(()) + } + + #[test] + fn test_publish_type_update_key_material_of_existing_embedded_method() -> Result<()> { + let old_doc = document(); + + let mut new_doc = old_doc.clone(); + + let keypair: KeyPair = KeyPair::new_ed25519()?; + let verif_method2: IotaVerificationMethod = + IotaVerificationMethod::from_did(new_doc.did().to_owned(), keypair.type_(), keypair.public(), "embedded")?; + + new_doc + .remove_method(new_doc.did().to_url().join("#embedded").unwrap()) + .unwrap(); + new_doc.insert_method(verif_method2, MethodScope::capability_invocation()); + + assert!(matches!( + PublishType::new(&old_doc, &new_doc), + Some(PublishType::Integration) + )); + + Ok(()) + } + + #[test] + fn test_publish_type_update_key_material_of_existing_generic_method() -> Result<()> { + let old_doc = document(); + + let mut new_doc = old_doc.clone(); + + let keypair: KeyPair = KeyPair::new_ed25519()?; + let method_updated: IotaVerificationMethod = + IotaVerificationMethod::from_did(new_doc.did().to_owned(), keypair.type_(), keypair.public(), "generic")?; + + assert!(unsafe { + new_doc + .as_document_mut() + .verification_method_mut() + .update(method_updated.into()) + }); + + assert!(matches!( + PublishType::new(&old_doc, &new_doc), + Some(PublishType::Integration) + )); + + Ok(()) + } + + #[test] + fn test_publish_type_add_non_capability_invocation_method() -> Result<()> { + let old_doc = document(); + + let mut new_doc = old_doc.clone(); + + let keypair: KeyPair = KeyPair::new_ed25519()?; + let verif_method2: IotaVerificationMethod = + IotaVerificationMethod::from_did(new_doc.did().to_owned(), keypair.type_(), keypair.public(), "test-2")?; + + new_doc.insert_method(verif_method2, MethodScope::authentication()); + + assert!(matches!(PublishType::new(&old_doc, &new_doc), Some(PublishType::Diff))); + + Ok(()) + } + + #[test] + fn test_publish_type_add_non_capability_invocation_relationship() -> Result<()> { + let old_doc = document(); + + let mut new_doc = old_doc.clone(); + + let method_url = new_doc.resolve_method("generic").unwrap().id(); + + new_doc + .attach_method_relationship( + method_url, + identity_did::verification::MethodRelationship::AssertionMethod, + ) + .unwrap(); + + assert!(matches!(PublishType::new(&old_doc, &new_doc), Some(PublishType::Diff))); + + Ok(()) + } + + #[test] + fn test_publish_type_update_method_with_non_update_method_type() -> Result<()> { + let old_doc = document(); + + let mut new_doc = old_doc.clone(); + + let collection = KeyCollection::new_ed25519(8)?; + let method: IotaVerificationMethod = + IotaVerificationMethod::create_merkle_key::(new_doc.did().to_owned(), &collection, "merkle")?; + + new_doc.insert_method(method, MethodScope::authentication()); + + assert!(matches!(PublishType::new(&old_doc, &new_doc), Some(PublishType::Diff))); + + Ok(()) + } + + #[test] + fn test_publish_type_update_method_with_non_update_method_type2() -> Result<()> { + let mut old_doc = document(); + + let collection = KeyCollection::new_ed25519(8)?; + let method: IotaVerificationMethod = + IotaVerificationMethod::create_merkle_key::(old_doc.did().to_owned(), &collection, "merkle")?; + + old_doc.insert_method(method, MethodScope::capability_invocation()); + + let mut new_doc = old_doc.clone(); + + // Replace the key collection. + let new_collection = KeyCollection::new_ed25519(8)?; + + let method_new: IotaVerificationMethod = + IotaVerificationMethod::create_merkle_key::(new_doc.did().to_owned(), &new_collection, "merkle")?; + + assert!(unsafe { + new_doc + .as_document_mut() + .capability_invocation_mut() + .update(method_new.into()) + }); + + assert!(matches!(PublishType::new(&old_doc, &new_doc), Some(PublishType::Diff))); + + Ok(()) + } +} diff --git a/identity/src/lib.rs b/identity/src/lib.rs index bab3d1b6be..767b5f4a03 100644 --- a/identity/src/lib.rs +++ b/identity/src/lib.rs @@ -86,11 +86,11 @@ pub mod account { pub use identity_account::account::*; pub use identity_account::crypto::*; pub use identity_account::error::*; - pub use identity_account::events::*; pub use identity_account::identity::*; pub use identity_account::storage::*; pub use identity_account::stronghold::*; pub use identity_account::types::*; + pub use identity_account::updates::*; pub use identity_account::utils::*; }