From 9bf01bb7eda6697ef628ee5f882619b9ec0391bf Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 18 Nov 2021 14:20:46 +0100 Subject: [PATCH] Refactor the `Account`: internal state, one identity (#453) * Refactor `Account` to handle just one identity (#436) * Partial impl to use did as storage identifier * Move `CreateIdentity` into a separate type Update Account API to return IdentityState from find_identity and create_identity (#414) * Update Account::create_identity() to return IdentityState. * Update Account::find_identity() to return IdentityState. Update `IdentityUpdater` to only use account ref Remove resolve, find and list Replace `IdentityId` with `IotaDID` in account Use `IotaDID` over `IdentityId` in stronghold More replacement of id with did Remove index from account Fix stream impl Fix stronghold impl, migrate `MemStore` Re-add `resolve_identity` so tests can run Fix Wasm network calls, pin reqwest to 0.11.4 (#439) Rename `Command` -> `Update` Rearrange `process_update` * Fix clippy lints in memstore * Let `RemoteKey` take a `dyn Storage` * Implement `AccountConfig` * Impl create_identity and load_identity in builder * Rename JSON serialization field name to match spec. (#412) * Rename JSON serialization field name to match spec. * Rename notSupported -> representationNotSupported * Reduce WASM build size (#427) * reduce wasm build size * Enable lto for Wasm release Add `build-dev` task for Wasm for debugging. Co-authored-by: Craig Bester * Chore/combine examples (#420) * Merged Stronghold with basic examples in account * Updated explorer to identity resolver * Added explorer URL after basic example * Renamed examples * Completed manipulate did example * Fixed suggestions, removed replaced examples. * Improved example readme * Consistently use Stronghold and Resolver URL * Fix examples * Merged config example with private tangle * low-level-api private-network example runs * cargo fmt * cargo fmt with nightly * Impl suggestions * Refactor config once more * Update examples to use single-id account * Don't pass did into `load_snapshot` * Fix clippy lints * Partially update `update` tests * Fix update tests * Ensure did exists in storage in `load` * Remote `State` in account * Rename `load` -> `load_identity` * Remove `Identity{Id,Index,Key,Name,Tag}` * Let `update_identity` take `&mut self` * Document new and existing types * Update top-level README example * Fix visibility on `CreateIdentity` * Add try_from implementation for ed25519 keypair * Combine account tests * Rename updates > commands for better reviewability * Also rename in mod.rs * Impl `AccountBuilder` without pub async fns * Update README example with latest builder change * Change visibility of setup, config, constructors * Revert to the old `create_did` example * Save to disk when creating identity, fix doc fmt Co-authored-by: Craig Bester * Use `authority` instead of `tag` in stronghold * Rename `Config` -> `AccountConfig` * Prevent two accounts from managing the same did * Impl `IdentityBuilder` * Only impl `IdentityCreate` under `#[cfg(test)]` * Use `IdentityBuilder` in examples * Remove unused `name` field * Run workflow for `epic/*` branches * Relase the lease in account tests * Also run format, clippy and coverage workflows * Revert "Use `IdentityBuilder` in examples" This reverts commit 3d1b716da3f6088069ec2692d187552a71037e44. * Revert "Only impl `IdentityCreate` under `#[cfg(test)]`" This reverts commit 08ada0979f337a7143abf75160fe9e0714066846. * Revert "Impl `IdentityBuilder`" This reverts commit 1a5d6452572f5ce89e3a757141a697c45de56952. * Rename `IdentityCreate` -> `IdentitySetup` * Add multi-identity example * Simplify README example * Rename `identity_setup` module * Implement the `IdentityLease` newtype * Rename `IdentityLease` -> `DIDLease` * Update `resolve_identity` docstring Co-authored-by: Matt Renaud Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Co-authored-by: Craig Bester Co-authored-by: Jelle Femmo Millenaar * Apply new fmt granularity * 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 * Fix READMEs * Deduplicate setup code in builder * Remove tangle dependency in autopublish test * Add docs for attach/detach relationship * Deduplicate signing key extraction * Fix documentation & annotate types in examples Co-authored-by: Matt Renaud Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Co-authored-by: Craig Bester Co-authored-by: Jelle Femmo Millenaar --- .github/workflows/build-and-test.yml | 1 + .github/workflows/clippy.yml | 1 + .github/workflows/coverage.yml | 1 + .github/workflows/format.yml | 1 + README.md | 24 +- .../wasm/src/did/wasm_verification_method.rs | 2 +- examples/Cargo.toml | 4 + examples/README.md | 3 +- examples/account/config.rs | 30 +- examples/account/create_did.rs | 23 +- examples/account/lazy.rs | 31 +- examples/account/manipulate_did.rs | 33 +- examples/account/multiple_identities.rs | 72 ++ examples/account/signing.rs | 29 +- 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 | 732 ++++++------------ identity-account/src/account/builder.rs | 142 +++- identity-account/src/account/config.rs | 153 ++++ identity-account/src/account/mod.rs | 2 + identity-account/src/crypto/remote.rs | 27 +- identity-account/src/error.rs | 33 +- identity-account/src/events/command.rs | 377 --------- identity-account/src/events/commit.rs | 45 -- identity-account/src/events/context.rs | 29 - identity-account/src/events/event.rs | 125 --- identity-account/src/events/mod.rs | 17 - identity-account/src/identity/chain_state.rs | 64 ++ identity-account/src/identity/did_lease.rs | 39 + identity-account/src/identity/identity_id.rs | 124 --- .../src/identity/identity_index.rs | 169 ---- identity-account/src/identity/identity_key.rs | 55 -- .../src/identity/identity_name.rs | 36 - .../{identity_create.rs => identity_setup.rs} | 20 +- .../src/identity/identity_snapshot.rs | 43 - .../src/identity/identity_state.rs | 625 +-------------- identity-account/src/identity/identity_tag.rs | 182 ----- .../src/identity/identity_updater.rs | 15 +- identity-account/src/identity/mod.rs | 20 +- identity-account/src/lib.rs | 2 +- identity-account/src/storage/memstore.rs | 143 ++-- identity-account/src/storage/stronghold.rs | 284 ++----- identity-account/src/storage/traits.rs | 130 ++-- identity-account/src/tests/account.rs | 239 ++++++ identity-account/src/tests/commands.rs | 444 ----------- identity-account/src/tests/lazy.rs | 157 ---- identity-account/src/tests/mod.rs | 4 +- 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 | 28 +- identity-account/src/updates/mod.rs | 11 + identity-account/src/updates/update.rs | 455 +++++++++++ identity-core/src/common/mod.rs | 2 - identity-core/src/common/unix_timestamp.rs | 74 -- identity-core/src/crypto/key/pair.rs | 21 + identity-core/src/utils/ed25519.rs | 10 + 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 | 92 ++- .../src/did/doc/iota_verification_method.rs | 11 +- identity-iota/src/tangle/mod.rs | 3 + identity-iota/src/tangle/publish.rs | 253 ++++++ identity/src/lib.rs | 2 +- 69 files changed, 3031 insertions(+), 3627 deletions(-) create mode 100644 examples/account/multiple_identities.rs create mode 100644 identity-account/src/account/config.rs delete mode 100644 identity-account/src/events/command.rs 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/mod.rs create mode 100644 identity-account/src/identity/chain_state.rs create mode 100644 identity-account/src/identity/did_lease.rs delete mode 100644 identity-account/src/identity/identity_id.rs delete mode 100644 identity-account/src/identity/identity_index.rs delete mode 100644 identity-account/src/identity/identity_key.rs delete mode 100644 identity-account/src/identity/identity_name.rs rename identity-account/src/identity/{identity_create.rs => identity_setup.rs} (79%) delete mode 100644 identity-account/src/identity/identity_snapshot.rs delete mode 100644 identity-account/src/identity/identity_tag.rs create mode 100644 identity-account/src/tests/account.rs delete mode 100644 identity-account/src/tests/commands.rs delete mode 100644 identity-account/src/tests/lazy.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 (62%) create mode 100644 identity-account/src/updates/mod.rs 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/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 92e55dc6a1..c94c47f1a8 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -9,6 +9,7 @@ on: branches: - main - dev + - epic/* paths: - '.github/workflows/build-and-test.yml' - '.github/actions' diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index 470f8f30ce..48076c6c20 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -9,6 +9,7 @@ on: branches: - main - dev + - epic/* paths: - '.github/workflows/clippy.yml' - '**.rs' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b836bd0502..a97864a6b7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -9,6 +9,7 @@ on: branches: - main - dev + - epic/* paths: - '.github/workflows/coverage.yml' - '.github/workflows/scripts/coverage.sh' diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index e2c3581d99..9b238753b1 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -9,6 +9,7 @@ on: branches: - main - dev + - epic/* paths: - '.github/workflows/format.yml' - '**.rs' diff --git a/README.md b/README.md index 364cdb699b..47685cb191 100644 --- a/README.md +++ b/README.md @@ -91,10 +91,8 @@ use std::path::PathBuf; use identity::account::Account; use identity::account::AccountStorage; -use identity::account::IdentityCreate; -use identity::account::IdentityState; +use identity::account::IdentitySetup; use identity::account::Result; -use identity::iota::IotaDID; use identity::iota::IotaDocument; #[tokio::main] @@ -102,28 +100,22 @@ async fn main() -> Result<()> { pretty_env_logger::init(); // The Stronghold settings for the storage. - let snapshot: PathBuf = "./example-strong.hodl".into(); + let stronghold_path: PathBuf = "./example-strong.hodl".into(); let password: String = "my-password".into(); - // Create a new Account with Stronghold as the storage adapter. + // Create a new identity with default settings and + // Stronghold as the storage. let account: Account = Account::builder() - .storage(AccountStorage::Stronghold(snapshot, Some(password))) - .build() + .storage(AccountStorage::Stronghold(stronghold_path, Some(password))) + .create_identity(IdentitySetup::default()) .await?; - // Create a new Identity with default settings. - let identity: IdentityState = account.create_identity(IdentityCreate::default()).await?; - - // Retrieve the DID from the newly created Identity state. - let did: &IotaDID = identity.try_did()?; - - println!("[Example] Local Document = {:#?}", identity.to_document()?); - println!("[Example] Local Document List = {:#?}", account.list_identities().await); + println!("[Example] Local Document = {:#?}", account.document()); // Fetch the DID Document from the Tangle // // This is an optional step to ensure DID Document consistency. - let resolved: IotaDocument = account.resolve_identity(did).await?; + let resolved: IotaDocument = account.resolve_identity().await?; println!("[Example] Tangle Document = {:#?}", resolved); 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/Cargo.toml b/examples/Cargo.toml index 0e83a189a4..0d8a500f8e 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -34,6 +34,10 @@ path = "account/lazy.rs" name = "account_signing" path = "account/signing.rs" +[[example]] +name = "account_multiple" +path = "account/multiple_identities.rs" + [[example]] name = "create_did" path = "low-level-api/create_did.rs" diff --git a/examples/README.md b/examples/README.md index 2fca72270f..79dd870942 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,11 +23,12 @@ The following examples are available for using the basic account (A high-level A | # | Name | Information | | :--: | :----------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | | 1 | [getting_started](./getting_started.rs) | Introductory example for you to test whether the library is set up / working properly and compiles. | -| 2 | [account_create_did](./account/create_did.rs) | A basic example that generates and publishes a DID Document, the fundamental building block for decentralized identity. | +| 2 | [account_create](./account/create_did.rs) | A basic example that generates and publishes a DID Document, the fundamental building block for decentralized identity. | | 3 | [account_config](./account/config.rs) | How to configure the account to work with different networks and other settings. | | 4 | [account_manipulate](./account/manipulate_did.rs) | How to manipulate a DID Document by adding/removing Verification Methods and Services. | | 5 | [account_lazy](./account/lazy.rs) | How to take control over publishing DID updates manually, instead of the default automated behavior. | | 6 | [account_signing](./account/signing.rs) | Using a DID to sign arbitrary statements and validating them. | +| 7 | [account_multiple](./account/multiple_identities.rs) | How to create multiple identities from a builder and how to load existing identities into an account. | The following examples are available for using the low-level APIs, which provides more flexibility at the cost of complexity: diff --git a/examples/account/config.rs b/examples/account/config.rs index 27e7f29e74..a905ac48b2 100644 --- a/examples/account/config.rs +++ b/examples/account/config.rs @@ -4,10 +4,10 @@ //! cargo run --example account_config use identity::account::Account; +use identity::account::AccountBuilder; use identity::account::AccountStorage; use identity::account::AutoSave; -use identity::account::IdentityCreate; -use identity::account::IdentityState; +use identity::account::IdentitySetup; use identity::account::Result; use identity::iota::IotaDID; use identity::iota::Network; @@ -16,7 +16,7 @@ use identity::iota::Network; async fn main() -> Result<()> { pretty_env_logger::init(); - // Set-up for private Tangle + // Set-up for a private Tangle // You can use https://github.com/iotaledger/one-click-tangle for a local setup. // The `network_name` needs to match the id of the network or a part of it. // As an example we are treating the devnet as a private tangle, so we use `dev`. @@ -33,13 +33,14 @@ async fn main() -> Result<()> { let private_node_url = "https://api.lb-0.h.chrysalis-devnet.iota.cafe"; // Create a new Account with explicit configuration - let account: Account = Account::builder() + let mut builder: AccountBuilder = Account::builder() .autosave(AutoSave::Never) // never auto-save. rely on the drop save .autosave(AutoSave::Every) // save immediately after every action .autosave(AutoSave::Batch(10)) // save after every 10 actions - .dropsave(false) // save the account state on drop + .autopublish(true) // publish to the tangle automatically on every update + .dropsave(true) // save the account state on drop .milestone(4) // save a snapshot every 4 actions - .storage(AccountStorage::Memory) // use the default in-memory storage adapter + .storage(AccountStorage::Memory) // use the default in-memory storage // configure a mainnet Tangle client with node and permanode .client(Network::Mainnet, |builder| { builder @@ -55,16 +56,13 @@ async fn main() -> Result<()> { .client(network.clone(), |builder| { // unwrap is safe, we provided a valid node URL builder.node(private_node_url).unwrap() - }) - .build() - .await?; + }); - // Create an Identity specifically on the devnet by passing `network_name` + // Create an identity specifically on the devnet by passing `network_name` // The same applies if we wanted to create an identity on a private tangle - let id_create = IdentityCreate::new().network(network_name)?; + let identity_setup: IdentitySetup = IdentitySetup::new().network(network_name)?; - // Create a new Identity with the network name set. - let identity: IdentityState = match account.create_identity(id_create).await { + let identity: Account = match builder.create_identity(identity_setup).await { Ok(identity) => identity, Err(err) => { eprintln!("[Example] Error: {:?} {}", err, err.to_string()); @@ -72,9 +70,11 @@ async fn main() -> Result<()> { return Ok(()); } }; - let iota_did: &IotaDID = identity.try_did()?; - // Prints the Identity Resolver Explorer URL, the entire history can be observed on this page by "Loading History". + let iota_did: &IotaDID = identity.did(); + + // Prints the Identity Resolver Explorer URL. + // The entire history can be observed on this page by clicking "Loading History". println!( "[Example] Explore the DID Document = {}{}", network.explorer_url().expect("no explorer url was set").to_string(), diff --git a/examples/account/create_did.rs b/examples/account/create_did.rs index 77e160b547..115a5a0d67 100644 --- a/examples/account/create_did.rs +++ b/examples/account/create_did.rs @@ -7,8 +7,7 @@ use std::path::PathBuf; use identity::account::Account; use identity::account::AccountStorage; -use identity::account::IdentityCreate; -use identity::account::IdentityState; +use identity::account::IdentitySetup; use identity::account::Result; use identity::iota::IotaDID; @@ -23,30 +22,32 @@ async fn main() -> Result<()> { let stronghold_path: PathBuf = "./example-strong.hodl".into(); let password: String = "my-password".into(); - // Create a new Account with the default configuration + // Create a new identity using Stronghold as local storage. + // + // The creation step generates a keypair, builds an identity + // and publishes it to the IOTA mainnet. let account: Account = Account::builder() .storage(AccountStorage::Stronghold(stronghold_path, Some(password))) - .build() + .create_identity(IdentitySetup::default()) .await?; - // Create a new Identity with default settings - // - // This step generates a keypair, creates an identity and publishes it to the IOTA mainnet. - let identity: IdentityState = account.create_identity(IdentityCreate::default()).await?; - let iota_did: &IotaDID = identity.try_did()?; + // Retrieve the did of the newly created identity. + let iota_did: &IotaDID = account.did(); // Print the local state of the DID Document println!( "[Example] Local Document from {} = {:#?}", iota_did, - identity.to_document() + account.state().document() ); - // Prints the Identity Resolver Explorer URL, the entire history can be observed on this page by "Loading History". + // Prints the Identity Resolver Explorer URL. + // The entire history can be observed on this page by clicking "Loading History". println!( "[Example] Explore the DID Document = {}{}", iota_did.network()?.explorer_url().unwrap().to_string(), iota_did.to_string() ); + Ok(()) } diff --git a/examples/account/lazy.rs b/examples/account/lazy.rs index 5b8cf7d4c2..4e4716794d 100644 --- a/examples/account/lazy.rs +++ b/examples/account/lazy.rs @@ -6,8 +6,7 @@ use std::path::PathBuf; use identity::account::Account; use identity::account::AccountStorage; -use identity::account::IdentityCreate; -use identity::account::IdentityState; +use identity::account::IdentitySetup; use identity::account::Result; use identity::core::Url; use identity::iota::IotaDID; @@ -23,21 +22,15 @@ async fn main() -> Result<()> { // Create a new Account with auto publishing set to false. // This means updates are not pushed to the tangle automatically. // Rather, when we publish, multiple updates are batched together. - let account: Account = Account::builder() + let mut account: Account = Account::builder() .storage(AccountStorage::Stronghold(stronghold_path, Some(password))) .autopublish(false) - .build() + .create_identity(IdentitySetup::default()) .await?; - // Create a new Identity with default settings. - // The identity will only be written to the local storage - not published to the tangle. - let identity: IdentityState = account.create_identity(IdentityCreate::default()).await?; - - // Retrieve the DID from the newly created Identity state. - let iota_did: &IotaDID = identity.try_did()?; - + // Add a new service to the local DID document. account - .update_identity(iota_did) + .update_identity() .create_service() .fragment("example-service") .type_("LinkedDomains") @@ -47,11 +40,11 @@ async fn main() -> Result<()> { // Publish the newly created DID document, // including the new service, to the tangle. - account.publish_updates(iota_did).await?; + account.publish_updates().await?; // Add another service. account - .update_identity(iota_did) + .update_identity() .create_service() .fragment("another-service") .type_("LinkedDomains") @@ -61,16 +54,20 @@ async fn main() -> Result<()> { // Delete the previously added service. account - .update_identity(iota_did) + .update_identity() .delete_service() .fragment("example-service") .apply() .await?; // Publish the updates as one message to the tangle. - account.publish_updates(iota_did).await?; + account.publish_updates().await?; + + // Retrieve the DID from the newly created identity. + let iota_did: &IotaDID = account.did(); - // Prints the Identity Resolver Explorer URL, the entire history can be observed on this page by "Loading History". + // Prints the Identity Resolver Explorer URL. + // The entire history can be observed on this page by clicking "Loading History". println!( "[Example] Explore the DID Document = {}{}", iota_did.network()?.explorer_url().unwrap().to_string(), diff --git a/examples/account/manipulate_did.rs b/examples/account/manipulate_did.rs index 33e56a89ea..b096b92cf5 100644 --- a/examples/account/manipulate_did.rs +++ b/examples/account/manipulate_did.rs @@ -7,11 +7,10 @@ use std::path::PathBuf; use identity::account::Account; use identity::account::AccountStorage; -use identity::account::IdentityCreate; -use identity::account::IdentityState; +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] @@ -27,24 +26,18 @@ async fn main() -> Result<()> { let password: String = "my-password".into(); // Create a new Account with the default configuration - let account: Account = Account::builder() + let mut account: Account = Account::builder() .storage(AccountStorage::Stronghold(stronghold_path, Some(password))) - .build() + .create_identity(IdentitySetup::default()) .await?; - // Create a new Identity with default settings - // - // This step generates a keypair, creates an identity and publishes it to the IOTA mainnet. - let identity: IdentityState = account.create_identity(IdentityCreate::default()).await?; - let iota_did: &IotaDID = identity.try_did()?; - // =========================================================================== // Identity Manipulation // =========================================================================== // Add another Ed25519 verification method to the identity account - .update_identity(&iota_did) + .update_identity() .create_method() .fragment("my-next-key") .apply() @@ -52,17 +45,17 @@ async fn main() -> Result<()> { // Associate the newly created method with additional verification relationships account - .update_identity(&iota_did) + .update_identity() .attach_method() .fragment("my-next-key") - .scope(MethodScope::CapabilityDelegation) - .scope(MethodScope::CapabilityInvocation) + .relationship(MethodRelationship::CapabilityDelegation) + .relationship(MethodRelationship::CapabilityInvocation) .apply() .await?; // Add a new service to the identity. account - .update_identity(&iota_did) + .update_identity() .create_service() .fragment("my-service-1") .type_("MyCustomService") @@ -72,13 +65,17 @@ async fn main() -> Result<()> { // Remove the Ed25519 verification method account - .update_identity(&iota_did) + .update_identity() .delete_method() .fragment("my-next-key") .apply() .await?; - // Prints the Identity Resolver Explorer URL, the entire history can be observed on this page by "Loading History". + // Retrieve the DID from the newly created identity. + let iota_did: &IotaDID = account.did(); + + // Prints the Identity Resolver Explorer URL. + // The entire history can be observed on this page by clicking "Loading History". println!( "[Example] Explore the DID Document = {}{}", iota_did.network()?.explorer_url().unwrap().to_string(), diff --git a/examples/account/multiple_identities.rs b/examples/account/multiple_identities.rs new file mode 100644 index 0000000000..84ed69cf05 --- /dev/null +++ b/examples/account/multiple_identities.rs @@ -0,0 +1,72 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! cargo run --example account_multiple + +use std::path::PathBuf; + +use identity::account::Account; +use identity::account::AccountBuilder; +use identity::account::AccountStorage; +use identity::account::IdentitySetup; +use identity::account::Result; +use identity::iota::IotaDID; + +#[tokio::main] +async fn main() -> Result<()> { + pretty_env_logger::init(); + + // Sets the location and password for the Stronghold + // + // Stronghold is an encrypted file that manages private keys. + // It implements best practices for security and is the recommended way of handling private keys. + let stronghold_path: PathBuf = "./example-strong.hodl".into(); + let password: String = "my-password".into(); + + // Create an AccountBuilder to make it easier to create multiple identities. + // Every account created from the builder will use the same storage - stronghold in this case. + let mut builder: AccountBuilder = + Account::builder().storage(AccountStorage::Stronghold(stronghold_path, Some(password))); + + // The creation step generates a keypair, builds an identity + // and publishes it to the IOTA mainnet. + let account1: Account = builder.create_identity(IdentitySetup::default()).await?; + + // Create a second identity. + let account2: Account = builder.create_identity(IdentitySetup::default()).await?; + + // Retrieve the did of the identity that account1 manages. + let iota_did1: IotaDID = account1.did().to_owned(); + + // Suppose we're done with account1 and drop it. + std::mem::drop(account1); + + // Now we want to modify the iota_did1 identity - how do we do that? + // We can load the identity from storage into an account using the builder. + let mut account1: Account = builder.load_identity(iota_did1).await?; + + // Now we can modify the identity. + account1 + .update_identity() + .create_method() + .fragment("my-key") + .apply() + .await?; + + // Note that there can only ever be one account that manages the same did. + // If we attempt to create another account that manages the same did as account2, we get an error. + assert!(matches!( + builder.load_identity(account2.did().to_owned()).await.unwrap_err(), + identity::account::Error::IdentityInUse + )); + + // Prints the Identity Resolver Explorer URL. + // The entire history can be observed on this page by clicking "Loading History". + println!( + "[Example] Explore the DID Document = {}/{}", + account1.did().network()?.explorer_url().unwrap().to_string(), + account1.did().to_string() + ); + + Ok(()) +} diff --git a/examples/account/signing.rs b/examples/account/signing.rs index bbd390ecb4..d6cafa1840 100644 --- a/examples/account/signing.rs +++ b/examples/account/signing.rs @@ -7,8 +7,7 @@ use std::path::PathBuf; use identity::account::Account; use identity::account::AccountStorage; -use identity::account::IdentityCreate; -use identity::account::IdentityState; +use identity::account::IdentitySetup; use identity::account::Result; use identity::core::json; use identity::core::FromJson; @@ -32,25 +31,19 @@ async fn main() -> Result<()> { let stronghold_path: PathBuf = "./example-strong.hodl".into(); let password: String = "my-password".into(); - // Create a new Account with the default configuration - let account: Account = Account::builder() + // Create a new Account with stronghold storage. + let mut account: Account = Account::builder() .storage(AccountStorage::Stronghold(stronghold_path, Some(password))) - .build() + .create_identity(IdentitySetup::default()) .await?; - // Create a new Identity with default settings - // - // This step generates a keypair, creates an identity and publishes it to the IOTA mainnet. - let identity: IdentityState = account.create_identity(IdentityCreate::default()).await?; - let iota_did: &IotaDID = identity.try_did()?; - // =========================================================================== // Signing Example // =========================================================================== // Add a new Ed25519 Verification Method to the identity account - .update_identity(&iota_did) + .update_identity() .create_method() .fragment("key-1") .apply() @@ -71,22 +64,26 @@ async fn main() -> Result<()> { // Issue an unsigned Credential... let mut credential: Credential = Credential::builder(Default::default()) - .issuer(Url::parse(&iota_did.as_str())?) + .issuer(Url::parse(account.did().as_str())?) .type_("UniversityDegreeCredential") .subject(subject) .build()?; // ...and sign the Credential with the previously created Verification Method - account.sign(&iota_did, "key-1", &mut credential).await?; + account.sign("key-1", &mut credential).await?; println!("[Example] Local Credential = {:#}", credential); // Fetch the DID Document from the Tangle // // This is an optional step to ensure DID Document consistency. - let resolved: IotaDocument = account.resolve_identity(&iota_did).await?; + let resolved: IotaDocument = account.resolve_identity().await?; + + // Retrieve the DID from the newly created identity. + let iota_did: &IotaDID = account.did(); - // Prints the Identity Resolver Explorer URL, the entire history can be observed on this page by "Loading History". + // Prints the Identity Resolver Explorer URL. + // The entire history can be observed on this page by clicking "Loading History". println!( "[Example] Explore the DID Document = {}{}", iota_did.network()?.explorer_url().unwrap().to_string(), 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 b3c7b4a3c1..590bcb214d 100644 --- a/identity-account/src/account/account.rs +++ b/identity-account/src/account/account.rs @@ -2,84 +2,124 @@ // 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; use std::sync::atomic::Ordering; use std::sync::Arc; -use tokio::sync::RwLock; -use tokio::sync::RwLockWriteGuard; use crate::account::AccountBuilder; -use crate::error::Error; use crate::error::Result; -use crate::events::Command; -use crate::events::Commit; -use crate::events::Context; -use crate::events::Event; -use crate::events::EventData; -use crate::identity::IdentityCreate; -use crate::identity::IdentityId; -use crate::identity::IdentityIndex; -use crate::identity::IdentityKey; -use crate::identity::IdentityLock; -use crate::identity::IdentitySnapshot; +use crate::identity::ChainState; +use crate::identity::DIDLease; +use crate::identity::IdentitySetup; use crate::identity::IdentityState; -use crate::identity::IdentityTag; use crate::identity::IdentityUpdater; -use crate::identity::TinyMethod; use crate::storage::Storage; -use crate::types::Generation; use crate::types::KeyLocation; - -const OSC: Ordering = Ordering::SeqCst; - +use crate::updates::create_identity; +use crate::updates::Update; +use crate::Error; + +use super::config::AccountSetup; +use super::config::AutoSave; +use super::AccountConfig; + +/// An account manages one identity. +/// +/// It handles private keys, writing to storage and +/// publishing to the Tangle. #[derive(Debug)] pub struct Account { - config: Arc, - state: State, - store: Box, - index: RwLock, + config: AccountConfig, + storage: Arc, + client_map: Arc, + actions: AtomicUsize, + chain_state: ChainState, + state: IdentityState, + did_lease: DIDLease, } impl Account { + // =========================================================================== + // Constructors + // =========================================================================== + /// Creates a new [AccountBuilder]. pub fn builder() -> AccountBuilder { AccountBuilder::new() } - /// Creates a new `Account` instance. - pub async fn new(store: impl Storage) -> Result { - Self::with_config(store, Config::new()).await - } - /// Creates a new `Account` instance with the given `config`. - pub async fn with_config(store: impl Storage, config: Config) -> Result { - let index: IdentityIndex = store.index().await?; - + async fn with_setup( + setup: AccountSetup, + chain_state: ChainState, + state: IdentityState, + did_lease: DIDLease, + ) -> Result { Ok(Self { - store: Box::new(store), - state: State::new(), - config: Arc::new(config), - index: RwLock::new(index), + config: setup.config, + storage: setup.storage, + client_map: setup.client_map, + actions: AtomicUsize::new(0), + 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 [`IdentitySetup`] 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 store(&self) -> &dyn Storage { - &*self.store + pub fn storage(&self) -> &dyn Storage { + self.storage.as_ref() + } + + /// Returns whether auto-publish is enabled. + pub fn autopublish(&self) -> bool { + self.config.autopublish } /// Returns the auto-save configuration value. @@ -87,106 +127,70 @@ impl Account { self.config.autosave } - /// Returns the save-on-drop configuration value. + /// Returns whether save-on-drop is enabled. pub fn dropsave(&self) -> bool { self.config.dropsave } /// Returns the total number of actions executed by this instance. pub fn actions(&self) -> usize { - self.state.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. pub fn set_client(&self, client: Client) { - self.state.clients.insert(client); + self.client_map.insert(client); } - // =========================================================================== - // Identity - // =========================================================================== - - /// Returns a list of tags identifying the identities in the account. - pub async fn list_identities(&self) -> Vec { - self.index.read().await.tags() + /// Returns the did of the managed identity. + pub fn did(&self) -> &IotaDID { + self.document().did() } - /// Finds and returns the identity state for the identity specified by given `key`. - pub async fn find_identity(&self, key: K) -> Result> { - match self.resolve_id(&key).await { - Some(identity) => self - .load_snapshot(identity) - .await - .map(IdentitySnapshot::into_identity) - .map(Some), - None => Ok(None), - } + /// Return the latest state of the identity. + pub fn state(&self) -> &IdentityState { + &self.state } - /// Create an identity from the specified configuration options. - pub async fn create_identity(&self, input: IdentityCreate) -> Result { - // Acquire write access to the index. - let mut index: RwLockWriteGuard<'_, _> = self.index.write().await; - - let identity: IdentityId = index.try_next_id()?; - - // Create the initialization command - let command: Command = Command::CreateIdentity { - network: input.network, - method_secret: input.method_secret, - method_type: Self::key_to_method(input.key_type), - }; - - // Process the command - self.process(identity, command, false).await?; - - // Read the latest snapshot - let snapshot: IdentitySnapshot = self.load_snapshot(identity).await?; - let did: &IotaDID = snapshot.identity().try_did()?; - - // Add the identity to the index - if let Some(name) = input.name { - index.set_named(identity, did, name)?; - } else { - index.set(identity, did)?; - } + /// Return the chain state of the identity. + pub fn chain_state(&self) -> &ChainState { + &self.chain_state + } - // Store the updated identity index - self.store.set_index(&index).await?; + /// Returns the DID document of the identity, which this account manages, + /// with all updates applied. + pub fn document(&self) -> &IotaDocument { + self.state.document() + } - // Write the changes to disk - self.save(false).await?; + // =========================================================================== + // Identity + // =========================================================================== - // Return the identity state - Ok(snapshot.into_identity()) + /// 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 the given `key`. + /// Returns the [`IdentityUpdater`] for this identity. /// /// On this type, various operations can be executed /// that modify an identity, such as creating services or methods. - pub fn update_identity<'account, 'key, K: IdentityKey>( - &'account self, - key: &'key K, - ) -> IdentityUpdater<'account, 'key, K> { - IdentityUpdater::new(self, key) + pub fn update_identity(&mut self) -> IdentityUpdater<'_> { + IdentityUpdater::new(self) } - /// Removes the identity specified by the given `key`. + /// Removes the identity from the local storage entirely. /// - /// Note: This will remove all associated events and key material - recovery is NOT POSSIBLE! - pub async fn delete_identity(&self, key: K) -> Result<()> { - // Acquire write access to the index. - let mut index: RwLockWriteGuard<'_, _> = self.index.write().await; - - // Remove the identity from the index - let identity: IdentityId = index.del(key)?.1; - + /// Note: This will remove all associated document updates and key material - recovery is NOT POSSIBLE! + pub async fn delete_identity(self) -> Result<()> { // Remove all associated keys and events - self.store.purge(identity).await?; - - // Store the updated identity index - self.store.set_index(&index).await?; + self.storage().purge(self.did()).await?; // Write the changes to disk self.save(false).await?; @@ -194,89 +198,50 @@ impl Account { Ok(()) } - /// Resolves the DID Document associated with the specified `key`. - pub async fn resolve_identity(&self, key: K) -> Result { - let identity: IdentityId = self.try_resolve_id(&key).await?; - let snapshot: IdentitySnapshot = self.load_snapshot(identity).await?; - let did: &IotaDID = snapshot.identity().try_did()?; - - // Fetch the DID Document from the Tangle - self.state.clients.resolve(did).await.map_err(Into::into) - } - /// Signs `data` with the key specified by `fragment`. - pub async fn sign(&self, key: K, fragment: &str, target: &mut U) -> Result<()> + pub async fn sign(&self, fragment: &str, target: &mut U) -> Result<()> where - K: IdentityKey, U: Serialize + SetSignature, { - let identity: IdentityId = self.try_resolve_id(&key).await?; - let snapshot: IdentitySnapshot = self.load_snapshot(identity).await?; - let state: &IdentityState = snapshot.identity(); + let state: &IdentityState = self.state(); - let fragment: Fragment = Fragment::new(fragment); - let method: &TinyMethod = state.methods().fetch(fragment.name())?; - let location: &KeyLocation = method.location(); + let method: &IotaVerificationMethod = state.document().resolve_method(fragment).ok_or(Error::MethodNotFound)?; - state.sign_data(&self.store, location, target).await?; + let location: KeyLocation = state.method_location(method.key_type(), fragment.to_owned())?; - Ok(()) - } + state.sign_data(self.did(), self.storage(), &location, target).await?; - async fn resolve_id(&self, key: &K) -> Option { - self.index.read().await.get(key) + Ok(()) } - async fn try_resolve_id(&self, key: &K) -> Result { - self.resolve_id(key).await.ok_or(Error::IdentityNotFound) - } + /// Push all unpublished changes to the tangle in a single message. + pub async fn publish_updates(&mut self) -> Result<()> { + self.publish(true).await?; - async fn try_resolve_id_lock(&self, key: &K) -> Result { - self.index.write().await.get_lock(key).ok_or(Error::IdentityNotFound) + Ok(()) } // =========================================================================== // Misc. Private // =========================================================================== - /// Updates the identity specified by the given `key` with the given `command`. - pub(crate) async fn apply_command(&self, key: &K, command: Command) -> Result<()> { - // Hold on to an `IdentityId`s individual lock until we've finished processing the update. - let identity_lock = self.try_resolve_id_lock(key).await?; - let identity: RwLockWriteGuard<'_, IdentityId> = identity_lock.write().await; - - self.process(*identity, command, true).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) } - pub(crate) async fn process(&self, id: IdentityId, command: Command, persist: bool) -> Result<()> { - // Load the latest state snapshot from storage - let root: IdentitySnapshot = self.load_snapshot(id).await?; - - debug!("[Account::process] Root = {:#?}", root); + pub(crate) async fn process_update(&mut self, update: Update) -> Result<()> { + let did = self.did().to_owned(); + let storage = Arc::clone(&self.storage); - // Process the command with a read-only view of the state - let context: Context<'_> = Context::new(root.identity(), self.store()); - let events: Option> = command.process(context).await?; - - debug!("[Account::process] Events = {:#?}", events); - - if let Some(events) = events { - // Commit all events returned by the command - let commits: Vec = self.commit_events(&root, &events).await?; - - debug!("[Account::process] Commits = {:#?}", commits); - - self.publish(root, commits, false).await?; - } + update.process(&did, &mut self.state, storage.as_ref()).await?; - // Update the total number of executions - self.state.actions.fetch_add(1, OSC); + self.increment_actions(); - if persist { - self.save(false).await?; - } + self.publish(false).await?; Ok(()) } @@ -287,376 +252,177 @@ 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.store, location, document).await?; + new_state + .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.store, location, document).await?; + old_state + .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(old_root.id()).await?; + /// Publishes according to the autopublish configuration. + async fn publish(&mut self, force: bool) -> Result<()> { + if !force && !self.config.autopublish { + return Ok(()); + } - let old_state: &IdentityState = old_root.identity(); - let new_state: &IdentityState = new_root.identity(); + if self.chain_state().is_new_identity() { + // New identity + self.publish_integration_change(None).await?; + } else { + // 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 mut new_doc: IotaDocument = new_state.to_document()?; + self.state.increment_generation()?; - self.sign_self(old_state, new_state, &mut new_doc).await?; + self.store_state().await?; - let message: MessageId = if self.config.testmode { - MessageId::null() - } else { - self.state.clients.publish_document(&new_doc).await?.into() - }; + Ok(()) + } - let events: [Event; 1] = [Event::new(EventData::IntegrationMessage(message))]; + 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?; - self.commit_events(&new_root, &events).await?; + self.save(false).await?; Ok(()) } - async fn process_diff_change(&self, old_root: IdentitySnapshot) -> Result<()> { - let new_root: IdentitySnapshot = self.load_snapshot(old_root.id()).await?; - - let old_state: &IdentityState = old_root.identity(); - let new_state: &IdentityState = new_root.identity(); + async fn publish_integration_change(&mut self, old_state: Option<&IdentityState>) -> Result<()> { + log::debug!("[publish_integration_change] publishing {:?}", self.document().did()); - let old_doc: IotaDocument = old_state.to_document()?; - let new_doc: IotaDocument = new_state.to_document()?; + let new_state: &IdentityState = self.state(); - let diff_id: &MessageId = old_state.diff_message_id(); + let mut new_doc: IotaDocument = new_state.document().to_owned(); - let mut diff: DocumentDiff = DocumentDiff::new(&old_doc, &new_doc, *diff_id)?; + new_doc.set_previous_message_id(*self.chain_state().last_integration_message_id()); - // Sign the update using a capability invocation method. - let method: &TinyMethod = old_state.capability_invocation()?; - let location: &KeyLocation = method.location(); + self + .sign_self(old_state.unwrap_or(new_state), new_state, &mut new_doc) + .await?; - old_state.sign_data(&self.store, location, &mut diff).await?; + log::debug!( + "[publish_integration_change] publishing on index {}", + new_doc.integration_index() + ); - let message: MessageId = if self.config.testmode { - MessageId::null() + 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 - .state - .clients - .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(&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?, - }) - } + async fn publish_diff_change(&mut self, old_state: &IdentityState) -> Result<()> { + log::debug!("[publish_diff_change] publishing {:?}", self.document().did()); - #[doc(hidden)] - pub async fn load_snapshot(&self, id: IdentityId) -> Result { - // Retrieve the state snapshot from storage or create a new one. - let initial: IdentitySnapshot = self - .store - .snapshot(id) - .await? - .unwrap_or_else(|| IdentitySnapshot::new(IdentityState::new(id))); - - // Apply all recent events to the state and create a new snapshot - self - .store - .stream(id, initial.sequence()) - .await? - .try_fold(initial, Self::fold_snapshot) - .await - } - - async fn load_snapshot_at(&self, id: IdentityId, generation: Generation) -> Result { - let initial: IdentitySnapshot = IdentitySnapshot::new(IdentityState::new(id)); + let old_doc: &IotaDocument = old_state.document(); + let new_doc: &IotaDocument = self.state().document(); - // Apply all events up to `generation` - self - .store - .stream(id, 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(&self, 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(state.id(), sequence, event.clone())); - } + let method: &IotaVerificationMethod = old_state.document().default_signing_method()?; - // Append the list of commits to the store - self.store.append(state.id(), &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() % self.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 - self.store.set_snapshot(state.id(), &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() + }; + + self.chain_state.set_last_diff_message_id(message_id); - // Return the list of stored events - Ok(commits) + Ok(()) } async fn save(&self, force: bool) -> Result<()> { match self.config.autosave { AutoSave::Every => { - self.store.flush_changes().await?; + self.storage().flush_changes().await?; } AutoSave::Batch(step) if force || (step != 0 && self.actions() % step == 0) => { - self.store.flush_changes().await?; + self.storage().flush_changes().await?; } AutoSave::Batch(_) | AutoSave::Never => {} } Ok(()) } - - /// Push all unpublished changes for the given identity to the tangle in a single message. - pub async fn publish_updates(&self, key: K) -> Result<()> { - let identity_lock: IdentityLock = self.try_resolve_id_lock(&key).await?; - let identity: RwLockWriteGuard<'_, IdentityId> = identity_lock.write().await; - - // Get the last commit generation that was published to the tangle. - let last_published: Generation = self.store.published_generation(*identity).await?.unwrap_or_default(); - - // Get the commits that need to be published. - let commits: Vec = self.store.collect(*identity, 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(*identity, 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(()); - } - - let id: IdentityId = snapshot.id(); - - 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.store.set_published_generation(id, generation).await?; - } - - Ok(()) - } - - fn key_to_method(type_: KeyType) -> MethodType { - match type_ { - KeyType::Ed25519 => MethodType::Ed25519VerificationKey2018, - } - } } impl Drop for Account { fn drop(&mut self) { if self.config.dropsave && self.actions() != 0 { // TODO: Handle Result (?) - let _ = executor::block_on(self.store.flush_changes()); - } - } -} - -// ============================================================================= -// Config -// ============================================================================= - -/// Top-level configuration for Identity [Account]s -#[derive(Clone, Debug)] -pub struct Config { - autosave: AutoSave, - autopublish: bool, - dropsave: bool, - testmode: bool, - milestone: u32, -} - -impl Config { - const MILESTONE: u32 = 1; - - /// Creates a new default `Config`. - pub fn new() -> Self { - Self { - autosave: AutoSave::Every, - autopublish: true, - dropsave: true, - testmode: false, - milestone: Self::MILESTONE, - } - } - - /// Sets the account auto-save behaviour. - /// - [`Every`][AutoSave::Every] => Save to storage on every update - /// - [`Never`][AutoSave::Never] => Never save to storage when updating - /// - [`Batch(n)`][AutoSave::Batch] => Save to storage after every `n` updates. - /// - /// Note that when [`Never`][AutoSave::Never] is selected, you will most - /// likely want to set [`dropsave`][Self::dropsave] to `true`. - /// - /// Default: [`Every`][AutoSave::Every] - pub fn autosave(mut self, value: AutoSave) -> Self { - self.autosave = value; - self - } - - /// Sets the account auto-publish behaviour. - /// - `true` => publish to the Tangle on every DID document change - /// - `false` => never publish automatically - /// - /// Default: `true` - pub fn autopublish(mut self, value: bool) -> Self { - self.autopublish = value; - self - } - - /// Save the account state on drop. - /// If set to `false`, set [`autosave`][Self::autosave] to - /// either [`Every`][AutoSave::Every] or [`Batch(n)`][AutoSave::Batch]. - /// - /// Default: `true` - pub fn dropsave(mut self, value: bool) -> Self { - self.dropsave = value; - self - } - - /// Save a state snapshot every N actions. - pub fn milestone(mut self, value: u32) -> Self { - self.milestone = value; - self - } - - #[doc(hidden)] - pub fn testmode(mut self, value: bool) -> Self { - self.testmode = value; - self - } -} - -impl Default for Config { - fn default() -> Self { - Self::new() - } -} - -// ============================================================================= -// AutoSave -// ============================================================================= - -/// Available auto-save behaviours. -#[derive(Clone, Copy, Debug)] -pub enum AutoSave { - /// Never save - Never, - /// Save after every action - Every, - /// Save after every N actions - Batch(usize), -} - -// ============================================================================= -// State -// ============================================================================= - -// Internal account state -#[derive(Debug)] -struct State { - actions: AtomicUsize, - clients: ClientMap, -} - -impl State { - /// Creates a new `State` instance. - fn new() -> Self { - Self { - actions: AtomicUsize::new(0), - clients: ClientMap::new(), - } - } -} - -// ============================================================================= -// 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, + let _ = executor::block_on(self.storage().flush_changes()); } } } diff --git a/identity-account/src/account/builder.rs b/identity-account/src/account/builder.rs index c6d1675efd..1ce73b187e 100644 --- a/identity-account/src/account/builder.rs +++ b/identity-account/src/account/builder.rs @@ -1,24 +1,30 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use hashbrown::HashMap; +use identity_iota::did::IotaDID; use identity_iota::tangle::ClientBuilder; +use identity_iota::tangle::ClientMap; use identity_iota::tangle::Network; use identity_iota::tangle::NetworkName; +use std::collections::HashMap; #[cfg(feature = "stronghold")] use std::path::PathBuf; +use std::sync::Arc; #[cfg(feature = "stronghold")] use zeroize::Zeroize; use crate::account::Account; -use crate::account::AutoSave; -use crate::account::Config; use crate::error::Result; +use crate::identity::IdentitySetup; use crate::storage::MemStore; use crate::storage::Storage; #[cfg(feature = "stronghold")] use crate::storage::Stronghold; +use super::config::AccountConfig; +use super::config::AccountSetup; +use super::config::AutoSave; + /// The storage adapter used by an [Account]. /// /// Note that [AccountStorage::Stronghold] is only available if the `stronghold` feature is activated, which it is by @@ -28,30 +34,44 @@ pub enum AccountStorage { Memory, #[cfg(feature = "stronghold")] Stronghold(PathBuf, Option), - Custom(Box), + Custom(Arc), } -/// An [Account] builder for easier account configuration. +/// An [`Account`] builder for easy account configuration. +/// +/// To reduce memory usage, accounts created from the same builder share the same [`Storage`], +/// used to store identities, and [`ClientMap`], used to +/// publish identities to the Tangle. This means using [`AccountBuilder::client`] +/// to customize a client, will modify the existing client map in previously +/// built accounts, when the next account is built. +/// +/// The configuration on the other hand is cloned, and therefore unique for each built account. +/// This means a builder can be reconfigured in-between account creations, without affecting +/// the configuration of previously built accounts. #[derive(Debug)] pub struct AccountBuilder { - config: Config, - storage: AccountStorage, - clients: Option>, + config: AccountConfig, + storage_template: Option, + storage: Option>, + client_builders: Option>, + client_map: Arc, } impl AccountBuilder { /// Creates a new `AccountBuilder`. pub fn new() -> Self { Self { - config: Config::new(), - storage: AccountStorage::Memory, - clients: None, + config: AccountConfig::new(), + storage_template: Some(AccountStorage::Memory), + storage: Some(Arc::new(MemStore::new())), + client_builders: None, + client_map: Arc::new(ClientMap::new()), } } /// Sets the account auto-save behaviour. /// - /// See the config's [`autosave`][Config::autosave] documentation for details. + /// See the config's [`autosave`][AccountConfig::autosave] documentation for details. pub fn autosave(mut self, value: AutoSave) -> Self { self.config = self.config.autosave(value); self @@ -59,7 +79,7 @@ impl AccountBuilder { /// Sets the account auto-publish behaviour. /// - /// See the config's [`autopublish`][Config::autopublish] documentation for details. + /// See the config's [`autopublish`][AccountConfig::autopublish] documentation for details. pub fn autopublish(mut self, value: bool) -> Self { self.config = self.config.autopublish(value); self @@ -67,7 +87,7 @@ impl AccountBuilder { /// Save the account state on drop. /// - /// See the config's [`dropsave`][Config::dropsave] documentation for details. + /// See the config's [`dropsave`][AccountConfig::dropsave] documentation for details. pub fn dropsave(mut self, value: bool) -> Self { self.config = self.config.dropsave(value); self @@ -79,30 +99,28 @@ impl AccountBuilder { self } - /// Sets the account storage adapter. - pub fn storage(mut self, value: AccountStorage) -> Self { - self.storage = value; + #[cfg(test)] + /// Set whether the account is in testmode or not. + /// In testmode, the account skips publishing to the tangle. + pub(crate) fn testmode(mut self, value: bool) -> Self { + self.config = self.config.testmode(value); self } - /// Apply configuration to the IOTA Tangle client for the given `Network`. - pub fn client(mut self, network: Network, f: F) -> Self - where - F: FnOnce(ClientBuilder) -> ClientBuilder, - { - self - .clients - .get_or_insert_with(HashMap::new) - .insert(network.name(), f(ClientBuilder::new().network(network))); + /// Sets the account storage adapter. + pub fn storage(mut self, value: AccountStorage) -> Self { + self.storage_template = Some(value); self } - /// Creates a new [Account] based on the builder configuration. - pub async fn build(mut self) -> Result { - let account: Account = match self.storage { - AccountStorage::Memory => Account::with_config(MemStore::new(), self.config).await?, + async fn get_storage(&mut self) -> Result> { + match self.storage_template.take() { + Some(AccountStorage::Memory) => { + let storage = Arc::new(MemStore::new()); + self.storage = Some(storage); + } #[cfg(feature = "stronghold")] - AccountStorage::Stronghold(snapshot, password) => { + Some(AccountStorage::Stronghold(snapshot, password)) => { let passref: Option<&str> = password.as_deref(); let adapter: Stronghold = Stronghold::new(&snapshot, passref).await?; @@ -110,18 +128,68 @@ impl AccountBuilder { password.zeroize(); } - Account::with_config(adapter, self.config).await? + let storage = Arc::new(adapter); + self.storage = Some(storage); + } + Some(AccountStorage::Custom(storage)) => { + self.storage = Some(storage); } - AccountStorage::Custom(adapter) => Account::with_config(adapter, self.config).await?, + None => (), }; - if let Some(clients) = self.clients.take() { - for (_, client) in clients.into_iter() { - account.set_client(client.build().await?); + // unwrap is fine, since by default, storage_template is `Some`, + // which results in storage being `Some`. + // Overwriting storage_template always produces `Some` storage. + Ok(Arc::clone(self.storage.as_ref().unwrap())) + } + + /// Apply configuration to the IOTA Tangle client for the given [`Network`]. + pub fn client(mut self, network: Network, f: F) -> Self + where + F: FnOnce(ClientBuilder) -> ClientBuilder, + { + self + .client_builders + .get_or_insert_with(HashMap::new) + .insert(network.name(), f(ClientBuilder::new().network(network))); + self + } + + async fn build_clients(&mut self) -> Result<()> { + if let Some(hmap) = self.client_builders.take() { + for builder in hmap.into_iter() { + self.client_map.insert(builder.1.build().await?) } } - Ok(account) + Ok(()) + } + + async fn build_setup(&mut self) -> Result { + self.build_clients().await?; + + Ok(AccountSetup::new_with_options( + self.get_storage().await?, + Some(self.config.clone()), + Some(Arc::clone(&self.client_map)), + )) + } + + /// Creates a new identity based on the builder configuration and returns + /// an [`Account`] instance to manage it. + /// The identity is stored locally in the [`Storage`]. + /// + /// See [`IdentitySetup`] to customize the identity creation. + pub async fn create_identity(&mut self, input: IdentitySetup) -> Result { + let setup: AccountSetup = self.build_setup().await?; + Account::create_identity(setup, input).await + } + + /// Loads an existing identity with the specified `did` using the current builder configuration. + /// The identity must exist in the configured [`Storage`]. + pub async fn load_identity(&mut self, did: IotaDID) -> Result { + let setup: AccountSetup = self.build_setup().await?; + Account::load_identity(setup, did).await } } diff --git a/identity-account/src/account/config.rs b/identity-account/src/account/config.rs new file mode 100644 index 0000000000..8d583c5942 --- /dev/null +++ b/identity-account/src/account/config.rs @@ -0,0 +1,153 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use identity_iota::tangle::ClientMap; + +use crate::storage::MemStore; +use crate::storage::Storage; + +/// A wrapper that holds configuration for an [`Account`] instantiation. +/// +/// The setup implements `Clone` so multiple [`Account`]s can be created +/// from the same setup. [`Storage`] and [`ClientMap`] are shared among +/// those accounts, while the [`Config`] is unique to each account. +/// +/// [`Account`]([crate::account::Account]) +#[derive(Clone, Debug)] +pub(crate) struct AccountSetup { + pub(crate) config: AccountConfig, + pub(crate) storage: Arc, + pub(crate) client_map: Arc, +} + +impl Default for AccountSetup { + fn default() -> Self { + Self::new(Arc::new(MemStore::new())) + } +} + +impl AccountSetup { + /// Create a new setup from the given [`Storage`] implementation + /// and with defaults for [`Config`] and [`ClientMap`]. + pub(crate) fn new(storage: Arc) -> Self { + Self { + config: AccountConfig::new(), + storage, + client_map: Arc::new(ClientMap::new()), + } + } + + /// Create a new setup from the given [`Storage`] implementation, + /// as well as optional [`Config`] and [`ClientMap`]. + /// If `None` is passed, the defaults will be used. + pub(crate) fn new_with_options( + storage: Arc, + config: Option, + client_map: Option>, + ) -> Self { + Self { + config: config.unwrap_or_default(), + storage, + client_map: client_map.unwrap_or_else(|| Arc::new(ClientMap::new())), + } + } + + #[cfg(test)] + /// Set the [`Config`] for this setup. + pub(crate) fn config(mut self, value: AccountConfig) -> Self { + self.config = value; + self + } +} + +/// Configuration for [`Account`][crate::account::Account]s. +#[derive(Clone, Debug)] +pub(crate) struct AccountConfig { + pub(crate) autosave: AutoSave, + pub(crate) autopublish: bool, + pub(crate) dropsave: bool, + pub(crate) testmode: bool, + pub(crate) milestone: u32, +} + +impl AccountConfig { + const MILESTONE: u32 = 1; + + /// Creates a new default [`Config`]. + pub(crate) fn new() -> Self { + Self { + autosave: AutoSave::Every, + autopublish: true, + dropsave: true, + testmode: false, + milestone: Self::MILESTONE, + } + } + + /// Sets the account auto-save behaviour. + /// - [`Every`][AutoSave::Every] => Save to storage on every update + /// - [`Never`][AutoSave::Never] => Never save to storage when updating + /// - [`Batch(n)`][AutoSave::Batch] => Save to storage after every `n` updates. + /// + /// Note that when [`Never`][AutoSave::Never] is selected, you will most + /// likely want to set [`dropsave`][Self::dropsave] to `true`. + /// + /// Default: [`Every`][AutoSave::Every] + pub(crate) fn autosave(mut self, value: AutoSave) -> Self { + self.autosave = value; + self + } + + /// Sets the account auto-publish behaviour. + /// - `true` => publish to the Tangle on every DID document change + /// - `false` => never publish automatically + /// + /// Default: `true` + pub(crate) fn autopublish(mut self, value: bool) -> Self { + self.autopublish = value; + self + } + + /// Save the account state on drop. + /// If set to `false`, set [`autosave`][Self::autosave] to + /// either [`Every`][AutoSave::Every] or [`Batch(n)`][AutoSave::Batch]. + /// + /// Default: `true` + pub(crate) fn dropsave(mut self, value: bool) -> Self { + self.dropsave = value; + self + } + + /// Save a state snapshot every N actions. + pub(crate) fn milestone(mut self, value: u32) -> Self { + self.milestone = value; + self + } + + #[cfg(test)] + /// Set whether the account is in testmode or not. + /// In testmode, the account skips publishing to the tangle. + pub(crate) fn testmode(mut self, value: bool) -> Self { + self.testmode = value; + self + } +} + +impl Default for AccountConfig { + fn default() -> Self { + Self::new() + } +} + +/// Available auto-save behaviours. +#[derive(Clone, Copy, Debug)] +pub enum AutoSave { + /// Never save + Never, + /// Save after every action + Every, + /// Save after every N actions + Batch(usize), +} diff --git a/identity-account/src/account/mod.rs b/identity-account/src/account/mod.rs index 5e5ef250cb..e46fbe52ab 100644 --- a/identity-account/src/account/mod.rs +++ b/identity-account/src/account/mod.rs @@ -5,6 +5,8 @@ mod account; mod builder; +mod config; pub use self::account::*; pub use self::builder::*; +pub use self::config::*; diff --git a/identity-account/src/crypto/remote.rs b/identity-account/src/crypto/remote.rs index 034c68f1dc..c03abf569b 100644 --- a/identity-account/src/crypto/remote.rs +++ b/identity-account/src/crypto/remote.rs @@ -6,23 +6,23 @@ use futures::executor; use identity_core::crypto::Sign; use identity_core::error::Error; use identity_core::error::Result; +use identity_iota::did::IotaDID; -use crate::identity::IdentityId; use crate::storage::Storage; use crate::types::KeyLocation; /// A reference to a storage instance and identity key location. #[derive(Debug)] -pub struct RemoteKey<'a, T> { - id: IdentityId, +pub struct RemoteKey<'a> { + did: &'a IotaDID, location: &'a KeyLocation, - store: &'a T, + store: &'a dyn Storage, } -impl<'a, T> RemoteKey<'a, T> { +impl<'a> RemoteKey<'a> { /// Creates a new `RemoteKey` instance. - pub fn new(id: IdentityId, location: &'a KeyLocation, store: &'a T) -> Self { - Self { id, location, store } + pub fn new(did: &'a IotaDID, location: &'a KeyLocation, store: &'a dyn Storage) -> Self { + Self { did, location, store } } } @@ -34,19 +34,16 @@ impl<'a, T> RemoteKey<'a, T> { /// /// Note: The signature implementation is specified by the associated `RemoteKey`. #[derive(Clone, Copy, Debug)] -pub struct RemoteSign<'a, T> { - marker: PhantomData>, +pub struct RemoteSign<'a> { + marker: PhantomData>, } -impl<'a, T> Sign for RemoteSign<'a, T> -where - T: Storage, -{ - type Private = RemoteKey<'a, T>; +impl<'a> Sign for RemoteSign<'a> { + type Private = RemoteKey<'a>; type Output = Vec; fn sign(message: &[u8], key: &Self::Private) -> Result { - let future: _ = key.store.key_sign(key.id, key.location, message.to_vec()); + let future: _ = key.store.key_sign(key.did, key.location, message.to_vec()); executor::block_on(future) .map_err(|_| Error::InvalidProofValue("remote sign")) diff --git a/identity-account/src/error.rs b/identity-account/src/error.rs index 81224a6777..6f44d6884f 100644 --- a/identity-account/src/error.rs +++ b/identity-account/src/error.rs @@ -58,41 +58,30 @@ 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}")] InvalidPrivateKey(String), + /// 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/command.rs b/identity-account/src/events/command.rs deleted file mode 100644 index d2634f7f3f..0000000000 --- a/identity-account/src/events/command.rs +++ /dev/null @@ -1,377 +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::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::IdentityId; -use crate::identity::IdentityKey; -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-"; - -#[derive(Clone, Debug)] -pub(crate) enum Command { - CreateIdentity { - network: Option, - method_secret: Option, - method_type: MethodType, - }, - 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 Command { - pub(crate) async fn process(self, context: Context<'_>) -> Result>> { - 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::CreateIdentity { - network, - method_secret, - method_type, - } => { - // The state must not be initialized. - ensure!(state.did().is_none(), UpdateError::DocumentAlreadyExists); - - // The method type must be able to sign document updates. - ensure!( - UPDATE_METHOD_TYPES.contains(&method_type), - UpdateError::InvalidMethodType(method_type) - ); - - let generation: Generation = state.integration_generation(); - let fragment: String = format!("{}{}", DEFAULT_UPDATE_METHOD_PREFIX, generation.to_u32()); - let location: KeyLocation = KeyLocation::new(method_type, fragment, generation, Generation::new()); - - // The key location must be available. - // TODO: config: strict - ensure!( - !store.key_exists(state.id(), &location).await?, - UpdateError::DuplicateKeyLocation(location) - ); - - let public: PublicKey = if let Some(method_private_key) = method_secret { - insert_method_secret(store, state.id(), &location, method_type, method_private_key).await - } else { - store.key_new(state.id(), &location).await - }?; - - let data: MethodData = MethodData::new_multibase(public.as_ref()); - let method: TinyMethod = TinyMethod::new(location, data, None); - - // Generate a new DID from the public key. - let did: IotaDID = if let Some(network) = network { - IotaDID::new_with_network(public.as_ref(), network)? - } else { - IotaDID::new(public.as_ref())? - }; - - Ok(Some(vec![ - Event::new(EventData::IdentityCreated(did)), - Event::new(EventData::MethodCreated(MethodScope::CapabilityInvocation, method)), - ])) - } - Self::CreateMethod { - type_, - scope, - fragment, - method_secret, - } => { - // The state must be initialized. - ensure!(state.did().is_some(), UpdateError::DocumentNotFound); - - let location: KeyLocation = state.key_location(type_, fragment)?; - - // The key location must be available. - // TODO: config: strict - ensure!( - !store.key_exists(state.id(), &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, state.id(), &location, type_, method_private_key).await - } else { - store.key_new(state.id(), &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 } => { - // The state must be initialized. - ensure!(state.did().is_some(), UpdateError::DocumentNotFound); - - 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 } => { - // The state must be initialized. - ensure!(state.did().is_some(), UpdateError::DocumentNotFound); - - 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 } => { - // The state must be initialized. - ensure!(state.did().is_some(), UpdateError::DocumentNotFound); - - 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 state must be initialized - ensure!(state.did().is_some(), UpdateError::DocumentNotFound); - - // 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 } => { - // The state must be initialized - ensure!(state.did().is_some(), UpdateError::DocumentNotFound); - - 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, - identity_id: IdentityId, - 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(identity_id, 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, 'key, K: IdentityKey> AttachMethodBuilder<'account, 'key, K> { - 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, 'key, K: IdentityKey> DetachMethodBuilder<'account, 'key, K> { - 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/events/commit.rs b/identity-account/src/events/commit.rs deleted file mode 100644 index 744310fe75..0000000000 --- a/identity-account/src/events/commit.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use crate::events::Event; -use crate::identity::IdentityId; -use crate::types::Generation; - -/// An [event][Event] and position in an identity event sequence. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -pub struct Commit { - identity: IdentityId, - sequence: Generation, - event: Event, -} - -impl Commit { - /// Creates a new `Commit`. - pub const fn new(identity: IdentityId, sequence: Generation, event: Event) -> Self { - Self { - identity, - sequence, - event, - } - } - - /// Returns the identifier of the associated identity. - pub const fn identity(&self) -> IdentityId { - 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 6f0d4afc2e..0000000000 --- a/identity-account/src/events/context.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -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> { - state: &'a IdentityState, - store: &'a dyn Storage, -} - -impl<'a> Context<'a> { - /// Creates a new `Context`. - pub fn new(state: &'a IdentityState, store: &'a dyn Storage) -> Self { - Self { state, store } - } - - /// 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/mod.rs b/identity-account/src/events/mod.rs deleted file mode 100644 index 494f6919a1..0000000000 --- a/identity-account/src/events/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -#[macro_use] -mod macros; - -mod command; -mod commit; -mod context; -mod error; -mod event; - -pub use self::command::*; -pub use self::commit::*; -pub use self::context::*; -pub use self::error::*; -pub use self::event::*; 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/did_lease.rs b/identity-account/src/identity/did_lease.rs new file mode 100644 index 0000000000..43354c48e9 --- /dev/null +++ b/identity-account/src/identity/did_lease.rs @@ -0,0 +1,39 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +/// A type that represents the permission to modify an identity. +/// +/// Holds an `AtomicBool` that is set to `false` on drop, signifying +/// the release of the lease. +#[derive(Debug, Clone)] +pub struct DIDLease(Arc); + +impl DIDLease { + pub fn new() -> Self { + Self(Arc::new(AtomicBool::new(true))) + } + + pub fn store(&self, value: bool) { + self.0.store(value, Ordering::SeqCst); + } + + pub fn load(&self) -> bool { + self.0.load(Ordering::SeqCst) + } +} + +impl Drop for DIDLease { + fn drop(&mut self) { + self.store(false); + } +} + +impl Default for DIDLease { + fn default() -> Self { + Self::new() + } +} diff --git a/identity-account/src/identity/identity_id.rs b/identity-account/src/identity/identity_id.rs deleted file mode 100644 index 54a855e6a0..0000000000 --- a/identity-account/src/identity/identity_id.rs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use core::convert::TryInto; -use core::fmt::Debug; -use core::fmt::Display; -use core::fmt::Formatter; -use core::fmt::Result as FmtResult; - -use crate::error::Error; -use crate::error::Result; - -/// A 32-bit identifier for stored Identities. -#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] -#[serde(from = "u32", into = "u32")] -#[repr(transparent)] -pub struct IdentityId([u8; 4]); - -impl IdentityId { - const ZERO: Self = Self::from_u32(0); - - /// Creates a new identity id from a slice of bytes. - /// - /// # Errors - /// - /// Fails if the given bytes are not a valid id. - pub fn from_slice(slice: &[u8]) -> Result { - slice - .try_into() - .map_err(|_| Error::IdentityIdInvalid) - .map(Self::from_bytes) - } - - /// Creates a new identity id from an array of bytes. - pub const fn from_bytes(bytes: [u8; 4]) -> Self { - Self(bytes) - } - - /// Creates a new identity id from a 32-bit integer. - pub const fn from_u32(value: u32) -> Self { - Self(value.to_be_bytes()) - } - - /// Returns the identity id as a slice of bytes. - pub const fn as_bytes(&self) -> &[u8] { - &self.0 - } - - /// Returns the identity id as an array of bytes. - pub const fn to_bytes(self) -> [u8; 4] { - self.0 - } - - /// Returns the identity id as a 32-bit integer. - pub const fn to_u32(self) -> u32 { - u32::from_be_bytes(self.0) - } - - /// Returns the next identity id in the sequence. - /// - /// # Errors - /// - /// Fails if the current id is the maximum supported value. - pub fn try_next(self) -> Result { - self - .to_u32() - .checked_add(1) - .map(Self::from_u32) - .ok_or(Error::IdentityIdOverflow) - } -} - -impl Debug for IdentityId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - f.write_fmt(format_args!("IdentityId({:#010x})", self.to_u32())) - } -} - -impl Display for IdentityId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - f.write_fmt(format_args!("{:#010x}", self.to_u32())) - } -} - -impl Default for IdentityId { - fn default() -> Self { - Self::ZERO - } -} - -impl From for IdentityId { - fn from(other: u32) -> Self { - Self::from_u32(other) - } -} - -impl From for u32 { - fn from(other: IdentityId) -> Self { - other.to_u32() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_roundtrip() { - let id: IdentityId = IdentityId::from_u32(123); - - assert_eq!(id.to_u32(), 123); - assert_eq!(IdentityId::from_bytes(id.to_bytes()), id); - assert_eq!(IdentityId::from_slice(id.as_bytes()).unwrap(), id); - } - - #[test] - fn test_from_slice() { - assert!(IdentityId::from_slice(&[]).is_err()); - assert!(IdentityId::from_slice(&[0x0]).is_err()); - assert!(IdentityId::from_slice(&[0x0; 3]).is_err()); - assert!(IdentityId::from_slice(&[0x0; 5]).is_err()); - assert!(IdentityId::from_slice(&[0x0; 4]).is_ok()); - } -} diff --git a/identity-account/src/identity/identity_index.rs b/identity-account/src/identity/identity_index.rs deleted file mode 100644 index fc08ecc366..0000000000 --- a/identity-account/src/identity/identity_index.rs +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::Arc; - -use hashbrown::hash_map::Entry; -use hashbrown::HashMap; -use tokio::sync::RwLock; - -use identity_did::did::DID; -use identity_iota::did::IotaDID; - -use crate::error::Error; -use crate::error::Result; -use crate::identity::IdentityId; -use crate::identity::IdentityKey; -use crate::identity::IdentityTag; - -pub(crate) type IdentityLock = Arc>; - -/// An mapping between [IdentityTag]s and [IdentityId]s. -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(transparent)] -pub struct IdentityIndex { - data: HashMap, - #[serde(skip)] - locks: HashMap, -} - -impl IdentityIndex { - /// Creates a new `IdentityIndex`. - pub fn new() -> Self { - Self { - data: HashMap::new(), - locks: HashMap::new(), - } - } - - /// Returns the next IdentityId in the sequence. - /// - /// # Errors - /// - /// Fails if the current id is the maximum supported value. - pub fn try_next_id(&self) -> Result { - self.data.values().max().copied().unwrap_or_default().try_next() - } - - /// Returns a list of all tags in the index. - pub fn tags(&self) -> Vec { - self.data.keys().cloned().collect() - } - - /// Returns the id of the identity matching the given `key`. - pub fn get(&self, key: &K) -> Option { - key.scan(self.data.iter()) - } - - /// Returns the id of the identity matching the given `key` wrapped in a lock. - /// - /// Should be used to synchronize write access to the given `key`. - pub fn get_lock(&mut self, key: K) -> Option { - if let Some(identity_id) = key.scan(self.data.iter()) { - match self.locks.entry(identity_id) { - Entry::Occupied(lock) => Some(Arc::clone(lock.get())), - Entry::Vacant(entry) => { - let lock = entry.insert(Arc::new(RwLock::new(identity_id))); - Some(Arc::clone(lock)) - } - } - } else { - None - } - } - - /// Adds a new unnamed identity to the index. - pub fn set(&mut self, id: IdentityId, did: &IotaDID) -> Result<()> { - self.insert(id, IdentityTag::new(did.method_id().into())) - } - - /// Adds a new named identity to the index. - pub fn set_named(&mut self, id: IdentityId, did: &IotaDID, name: String) -> Result<()> { - self.insert(id, IdentityTag::named(did.method_id().into(), name)) - } - - /// Removes the identity specified by `key` from the index. - pub fn del(&mut self, key: K) -> Result<(IdentityTag, IdentityId)> { - let removed_id = self - .data - .drain_filter(|tag, id| key.equals(tag, *id)) - .next() - .ok_or(Error::IdentityNotFound)?; - - self.locks.remove(&removed_id.1); - - Ok(removed_id) - } - - fn insert(&mut self, id: IdentityId, tag: IdentityTag) -> Result<()> { - match self.data.entry(tag) { - Entry::Occupied(_) => Err(Error::IdentityAlreadyExists), - Entry::Vacant(entry) => { - entry.insert(id); - Ok(()) - } - } - } -} - -impl Default for IdentityIndex { - fn default() -> Self { - Self::new() - } -} - -impl PartialEq for IdentityIndex { - fn eq(&self, other: &Self) -> bool { - self.data == other.data - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_basics() { - let mut index: IdentityIndex = IdentityIndex::new(); - assert!(index.tags().is_empty()); - - let target1: IotaDID = format!("did:iota:{}", IotaDID::encode_key(b"123")).parse().unwrap(); - let target2: IotaDID = format!("did:iota:{}", IotaDID::encode_key(b"456")).parse().unwrap(); - let target3: IotaDID = format!("did:iota:{}", IotaDID::encode_key(b"789")).parse().unwrap(); - - index.set(1.into(), &target1).unwrap(); - index.set(2.into(), &target2).unwrap(); - index.set(3.into(), &target3).unwrap(); - - assert_eq!(index.tags().len(), 3); - - assert_eq!(index.get(&target1).unwrap().to_u32(), 1); - assert_eq!(index.get(&target2).unwrap().to_u32(), 2); - assert_eq!(index.get(&target3).unwrap().to_u32(), 3); - - assert_eq!(index.del(&target1).unwrap().1.to_u32(), 1); - assert_eq!(index.del(&target2).unwrap().1.to_u32(), 2); - - assert_eq!(index.tags().len(), 1); - } - - #[test] - fn test_next_id() { - let mut index: IdentityIndex = IdentityIndex::new(); - assert_eq!(index.try_next_id().unwrap().to_u32(), 1); - - index.insert(1.into(), IdentityTag::new("foo-1".into())).unwrap(); - assert_eq!(index.try_next_id().unwrap().to_u32(), 2); - - index.insert(2.into(), IdentityTag::new("foo-2".into())).unwrap(); - assert_eq!(index.try_next_id().unwrap().to_u32(), 3); - - let target: IdentityId = IdentityId::from(1); - let (tag, id): (IdentityTag, IdentityId) = index.del(target).unwrap(); - assert_eq!(tag.name(), None); - assert_eq!(tag.method_id(), "foo-1"); - assert_eq!(id, target); - - assert_eq!(index.try_next_id().unwrap().to_u32(), 3); - } -} diff --git a/identity-account/src/identity/identity_key.rs b/identity-account/src/identity/identity_key.rs deleted file mode 100644 index 89507d8408..0000000000 --- a/identity-account/src/identity/identity_key.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use identity_did::did::DID; -use identity_iota::did::IotaDID; - -use crate::identity::IdentityId; -use crate::identity::IdentityTag; - -type Item<'a> = (&'a IdentityTag, &'a IdentityId); - -pub trait IdentityKey { - fn equals(&self, tag: &IdentityTag, id: IdentityId) -> bool; - - fn scan<'a, I: Iterator>>(&self, mut iter: I) -> Option { - iter.find(|(tag, id)| self.equals(tag, **id)).map(|(_, id)| *id) - } -} - -impl<'a, T> IdentityKey for &'a T -where - T: IdentityKey + ?Sized, -{ - fn equals(&self, tag: &IdentityTag, id: IdentityId) -> bool { - (**self).equals(tag, id) - } -} - -impl IdentityKey for IotaDID { - fn equals(&self, tag: &IdentityTag, _: IdentityId) -> bool { - tag.method_id() == self.method_id() - } -} - -impl IdentityKey for str { - fn equals(&self, tag: &IdentityTag, id: IdentityId) -> bool { - tag.fullname(id).as_ref() == self - } -} - -impl IdentityKey for String { - fn equals(&self, tag: &IdentityTag, id: IdentityId) -> bool { - self[..].equals(tag, id) - } -} - -impl IdentityKey for IdentityId { - fn equals(&self, _: &IdentityTag, id: IdentityId) -> bool { - id == *self - } - - fn scan<'a, I: Iterator>>(&self, _: I) -> Option { - Some(*self) - } -} diff --git a/identity-account/src/identity/identity_name.rs b/identity-account/src/identity/identity_name.rs deleted file mode 100644 index 064359f1fb..0000000000 --- a/identity-account/src/identity/identity_name.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::borrow::Cow; - -use crate::identity::IdentityId; - -/// Represents an Identity name, whether explicitly set or default. -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(untagged)] -pub enum IdentityName { - Default, - Literal(String), -} - -impl IdentityName { - /// Returns the user-assigned name, if any. - pub fn as_opt(&self) -> Option<&str> { - match self { - Self::Default => None, - Self::Literal(ref inner) => Some(inner), - } - } - - /// Returns the name of the identity, whether user-assigned or default. - pub fn as_str(&self, id: IdentityId) -> Cow<'_, str> { - match self { - Self::Default => Cow::Owned(default_identifier(id)), - Self::Literal(ref inner) => Cow::Borrowed(inner), - } - } -} - -fn default_identifier(id: IdentityId) -> String { - format!("Identity {}", id.to_u32()) -} diff --git a/identity-account/src/identity/identity_create.rs b/identity-account/src/identity/identity_setup.rs similarity index 79% rename from identity-account/src/identity/identity_create.rs rename to identity-account/src/identity/identity_setup.rs index 7fc8e02a6d..2804ba5f3f 100644 --- a/identity-account/src/identity/identity_create.rs +++ b/identity-account/src/identity/identity_setup.rs @@ -11,19 +11,17 @@ use crate::Result; /// Configuration used to create a new Identity. #[derive(Clone, Debug)] -pub struct IdentityCreate { +pub struct IdentitySetup { pub(crate) key_type: KeyType, - pub(crate) name: Option, pub(crate) network: Option, pub(crate) method_secret: Option, } -impl IdentityCreate { - /// Creates a new `IdentityCreate` instance. +impl IdentitySetup { + /// Creates a new `IdentitySetup` instance. pub const fn new() -> Self { Self { key_type: KeyType::Ed25519, - name: None, network: None, method_secret: None, } @@ -36,16 +34,6 @@ impl IdentityCreate { self } - /// Sets the name of the Identity. - #[must_use] - pub fn name(mut self, value: T) -> Self - where - T: Into, - { - self.name = Some(value.into()); - self - } - /// Sets the IOTA Tangle network of the Identity DID. #[allow(clippy::double_must_use)] #[must_use] @@ -67,7 +55,7 @@ impl IdentityCreate { } } -impl Default for IdentityCreate { +impl Default for IdentitySetup { 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 99f525f4c5..0000000000 --- a/identity-account/src/identity/identity_snapshot.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use crate::identity::IdentityId; -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 identifier for this identity. - pub fn id(&self) -> IdentityId { - self.identity.id() - } - - /// 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 76940d71df..e44cb67ad8 100644 --- a/identity-account/src/identity/identity_state.rs +++ b/identity-account/src/identity/identity_state.rs @@ -1,102 +1,44 @@ // 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; use crate::crypto::RemoteSign; use crate::error::Error; use crate::error::Result; -use crate::identity::IdentityId; use crate::storage::Storage; use crate::types::Generation; use crate::types::KeyLocation; -type Properties = VerifiableProperties; -type BaseDocument = CoreDocument; +pub type RemoteEd25519<'a> = JcsEd25519>; -pub type RemoteEd25519<'a, T> = JcsEd25519>; - -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct IdentityState { - // =========== // - // Chain State // - // =========== // - id: IdentityId, - 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(id: IdentityId) -> Self { + pub fn new(document: IotaDocument) -> Self { Self { - id, - 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, } } @@ -104,253 +46,66 @@ impl IdentityState { // Internal State // =========================================================================== - /// Returns the identifier for this identity. - pub fn id(&self) -> IdentityId { - self.id - } - /// 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(); + /// 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)?; - // Set the new integration message id - self.this_message_id = message; - } - - /// 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 + pub fn document(&self) -> &IotaDocument { + &self.document } - /// 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) - } - - /// 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(&self, store: &T, location: &KeyLocation, target: &mut U) -> Result<()> + pub async fn sign_data( + &self, + did: &IotaDID, + store: &dyn Storage, + location: &KeyLocation, + target: &mut U, + ) -> Result<()> where - T: Storage, U: Serialize + SetSignature, { // Create a private key suitable for identity_core::crypto - let private: RemoteKey<'_, T> = RemoteKey::new(self.id, location, store); + let private: RemoteKey<'_> = RemoteKey::new(did, location, store); // 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 => { @@ -364,305 +119,3 @@ impl IdentityState { Ok(()) } } - -// ============================================================================= -// 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/identity_tag.rs b/identity-account/src/identity/identity_tag.rs deleted file mode 100644 index a2106eb742..0000000000 --- a/identity-account/src/identity/identity_tag.rs +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use core::fmt::Debug; -use core::fmt::Formatter; -use core::fmt::Result as FmtResult; -use core::hash::Hash; -use core::hash::Hasher; -use identity_core::convert::FromJson; -use identity_core::convert::ToJson; -use identity_core::utils::decode_b64; -use identity_core::utils::encode_b64; -use serde::de; -use serde::ser; -use serde::Deserialize; -use serde::Serialize; -use std::borrow::Cow; - -use crate::error::Result; -use crate::identity::IdentityId; -use crate::identity::IdentityName; - -/// Information used to identify an identity. -#[derive(Clone)] -pub struct IdentityTag { - name: IdentityName, - method_id: String, -} - -impl IdentityTag { - /// Creates a new IdentityTag with a default name. - pub fn new(method_id: String) -> Self { - Self { - name: IdentityName::Default, - method_id, - } - } - - /// Creates a new IdentityTag with an explicit name. - pub fn named(method_id: String, name: String) -> Self { - Self { - name: IdentityName::Literal(name), - method_id, - } - } - - /// Returns the user-assigned name of the identity. - pub fn name(&self) -> Option<&str> { - self.name.as_opt() - } - - /// Returns the name of the identity, whether user-assigned or default. - pub fn fullname(&self, id: IdentityId) -> Cow<'_, str> { - self.name.as_str(id) - } - - /// Returns the method id of the Identity DID Document. - pub fn method_id(&self) -> &str { - &self.method_id - } - - // Returns the identity tag as a base64-encoded JSON string. - fn encode(&self) -> Result { - let data: ProxySerialize<'_> = ProxySerialize { - method_id: &self.method_id, - name: &self.name, - }; - - let json: Vec = data.to_json_vec()?; - let base: String = encode_b64(&json); - - Ok(base) - } - - // Decodes an identity tag from a base64-encoded JSON string. - fn decode(string: &str) -> Result { - let json: Vec = decode_b64(string)?; - let data: ProxyDeserialize = ProxyDeserialize::from_json_slice(&json)?; - - Ok(Self { - method_id: data.method_id, - name: data.name, - }) - } -} - -impl Debug for IdentityTag { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - f.write_fmt(format_args!("IdentityTag({}, {:?})", self.method_id, self.name)) - } -} - -impl PartialEq for IdentityTag { - fn eq(&self, other: &Self) -> bool { - self.method_id.eq(&other.method_id) - } -} - -impl Eq for IdentityTag {} - -impl Hash for IdentityTag { - fn hash(&self, hasher: &mut H) { - self.method_id.hash(hasher); - } -} - -// ============================================================================= -// Serde -// ============================================================================= - -#[derive(Serialize)] -struct ProxySerialize<'a> { - #[serde(rename = "1")] - method_id: &'a str, - #[serde(rename = "2")] - name: &'a IdentityName, -} - -#[derive(Deserialize)] -struct ProxyDeserialize { - #[serde(rename = "1")] - method_id: String, - #[serde(rename = "2")] - name: IdentityName, -} - -impl Serialize for IdentityTag { - fn serialize(&self, serializer: S) -> Result - where - S: ser::Serializer, - { - match self.encode() { - Ok(data) => serializer.serialize_str(&data), - Err(error) => Err(ser::Error::custom(error)), - } - } -} - -impl<'de> Deserialize<'de> for IdentityTag { - fn deserialize(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - struct Visitor; - - impl<'de> de::Visitor<'de> for Visitor { - type Value = IdentityTag; - - fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult { - f.write_str("a base64-encoded string") - } - - fn visit_str(self, value: &str) -> Result - where - E: de::Error, - { - IdentityTag::decode(value).map_err(E::custom) - } - } - - deserializer.deserialize_str(Visitor) - } -} - -// ============================================================================= -// Tests -// ============================================================================= - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_compare() { - let a: IdentityTag = IdentityTag::new("abcde".into()); - let b: IdentityTag = IdentityTag::named("abcde".into(), "Foo".into()); - - assert_eq!(a, b); - assert_eq!(a.method_id(), b.method_id()); - assert_ne!(a.name(), b.name()); - } -} diff --git a/identity-account/src/identity/identity_updater.rs b/identity-account/src/identity/identity_updater.rs index e5e9f9f6fb..5c70ac18ac 100644 --- a/identity-account/src/identity/identity_updater.rs +++ b/identity-account/src/identity/identity_updater.rs @@ -3,18 +3,15 @@ use crate::account::Account; -use super::IdentityKey; - /// A struct created by the [`Account::update_identity`] method, that /// allows executing various updates on the identity it was created on. -#[derive(Debug, Clone)] -pub struct IdentityUpdater<'account, 'key, K: IdentityKey> { - pub(crate) account: &'account Account, - pub(crate) key: &'key K, +#[derive(Debug)] +pub struct IdentityUpdater<'account> { + pub(crate) account: &'account mut Account, } -impl<'account, 'key, K: IdentityKey> IdentityUpdater<'account, 'key, K> { - pub(crate) fn new(account: &'account Account, key: &'key K) -> Self { - Self { account, key } +impl<'account> IdentityUpdater<'account> { + pub(crate) fn new(account: &'account mut Account) -> Self { + Self { account } } } diff --git a/identity-account/src/identity/mod.rs b/identity-account/src/identity/mod.rs index 246933ce9e..d3f75cd0dc 100644 --- a/identity-account/src/identity/mod.rs +++ b/identity-account/src/identity/mod.rs @@ -1,22 +1,14 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -mod identity_create; -mod identity_id; -mod identity_index; -mod identity_key; -mod identity_name; -mod identity_snapshot; +mod chain_state; +mod did_lease; +mod identity_setup; mod identity_state; -mod identity_tag; mod identity_updater; -pub use self::identity_create::*; -pub use self::identity_id::*; -pub use self::identity_index::*; -pub use self::identity_key::*; -pub use self::identity_name::*; -pub use self::identity_snapshot::*; +pub use self::chain_state::*; +pub use self::did_lease::*; +pub use self::identity_setup::*; pub use self::identity_state::*; -pub use self::identity_tag::*; 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 aacb09c9de..6230e95374 100644 --- a/identity-account/src/storage/memstore.rs +++ b/identity-account/src/storage/memstore.rs @@ -5,9 +5,7 @@ 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; use identity_core::crypto::KeyPair; @@ -16,17 +14,18 @@ use identity_core::crypto::PrivateKey; use identity_core::crypto::PublicKey; use identity_core::crypto::Sign; use identity_did::verification::MethodType; +use identity_iota::did::IotaDID; use std::convert::TryFrom; use std::sync::RwLockReadGuard; use std::sync::RwLockWriteGuard; +use tokio::sync::Mutex; use zeroize::Zeroize; use crate::error::Error; use crate::error::Result; -use crate::events::Commit; -use crate::identity::IdentityId; -use crate::identity::IdentityIndex; -use crate::identity::IdentitySnapshot; +use crate::identity::ChainState; +use crate::identity::DIDLease; +use crate::identity::IdentityState; use crate::storage::Storage; use crate::types::Generation; use crate::types::KeyLocation; @@ -36,16 +35,16 @@ use crate::utils::Shared; type MemVault = HashMap; -type Events = HashMap>; -type States = HashMap; -type Vaults = HashMap; -type PublishedGenerations = HashMap; +type ChainStates = HashMap; +type States = HashMap; +type Vaults = HashMap; +type PublishedGenerations = HashMap; pub struct MemStore { expand: bool, - index: Shared, published_generations: Shared, - events: Shared, + did_leases: Mutex>, + chain_states: Shared, states: Shared, vaults: Shared, } @@ -54,9 +53,9 @@ impl MemStore { pub fn new() -> Self { Self { expand: false, - index: Shared::new(IdentityIndex::new()), published_generations: Shared::new(HashMap::new()), - events: Shared::new(HashMap::new()), + did_leases: Mutex::new(HashMap::new()), + chain_states: Shared::new(HashMap::new()), states: Shared::new(HashMap::new()), vaults: Shared::new(HashMap::new()), } @@ -70,18 +69,6 @@ impl MemStore { self.expand = value; } - pub fn index(&self) -> Result { - self.index.read().map(|data| data.clone()) - } - - 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()) } @@ -97,9 +84,29 @@ impl Storage for MemStore { Ok(()) } - async fn key_new(&self, id: IdentityId, location: &KeyLocation) -> Result { + async fn lease_did(&self, did: &IotaDID) -> Result { + let mut hmap = self.did_leases.lock().await; + + match hmap.entry(did.clone()) { + Entry::Occupied(entry) => { + if entry.get().load() { + Err(Error::IdentityInUse) + } else { + entry.get().store(true); + Ok(entry.get().clone()) + } + } + Entry::Vacant(entry) => { + let did_lease = DIDLease::new(); + entry.insert(did_lease.clone()); + Ok(did_lease) + } + } + } + + async fn key_new(&self, did: &IotaDID, location: &KeyLocation) -> Result { let mut vaults: RwLockWriteGuard<'_, _> = self.vaults.write()?; - let vault: &mut MemVault = vaults.entry(id).or_default(); + let vault: &mut MemVault = vaults.entry(did.clone()).or_default(); match location.method() { MethodType::Ed25519VerificationKey2018 => { @@ -116,9 +123,9 @@ impl Storage for MemStore { } } - async fn key_insert(&self, id: IdentityId, location: &KeyLocation, private_key: PrivateKey) -> Result { + async fn key_insert(&self, did: &IotaDID, location: &KeyLocation, private_key: PrivateKey) -> Result { let mut vaults: RwLockWriteGuard<'_, _> = self.vaults.write()?; - let vault: &mut MemVault = vaults.entry(id).or_default(); + let vault: &mut MemVault = vaults.entry(did.clone()).or_default(); match location.method() { MethodType::Ed25519VerificationKey2018 => { @@ -144,37 +151,37 @@ impl Storage for MemStore { } } - async fn key_exists(&self, id: IdentityId, location: &KeyLocation) -> Result { + async fn key_exists(&self, did: &IotaDID, location: &KeyLocation) -> Result { let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?; - if let Some(vault) = vaults.get(&id) { + if let Some(vault) = vaults.get(did) { return Ok(vault.contains_key(location)); } Ok(false) } - async fn key_get(&self, id: IdentityId, location: &KeyLocation) -> Result { + async fn key_get(&self, did: &IotaDID, location: &KeyLocation) -> Result { let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?; - let vault: &MemVault = vaults.get(&id).ok_or(Error::KeyVaultNotFound)?; - let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyPairNotFound)?; + let vault: &MemVault = vaults.get(did).ok_or(Error::KeyVaultNotFound)?; + let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyNotFound)?; Ok(keypair.public().clone()) } - async fn key_del(&self, id: IdentityId, location: &KeyLocation) -> Result<()> { + async fn key_del(&self, did: &IotaDID, location: &KeyLocation) -> Result<()> { let mut vaults: RwLockWriteGuard<'_, _> = self.vaults.write()?; - let vault: &mut MemVault = vaults.get_mut(&id).ok_or(Error::KeyVaultNotFound)?; + let vault: &mut MemVault = vaults.get_mut(did).ok_or(Error::KeyVaultNotFound)?; vault.remove(location); Ok(()) } - async fn key_sign(&self, id: IdentityId, location: &KeyLocation, data: Vec) -> Result { + async fn key_sign(&self, did: &IotaDID, location: &KeyLocation, data: Vec) -> Result { let vaults: RwLockReadGuard<'_, _> = self.vaults.read()?; - let vault: &MemVault = vaults.get(&id).ok_or(Error::KeyVaultNotFound)?; - let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyPairNotFound)?; + let vault: &MemVault = vaults.get(did).ok_or(Error::KeyVaultNotFound)?; + let keypair: &KeyPair = vault.get(location).ok_or(Error::KeyNotFound)?; match location.method() { MethodType::Ed25519VerificationKey2018 => { @@ -192,59 +199,40 @@ impl Storage for MemStore { } } - async fn index(&self) -> Result { - self.index.read().map(|index| index.clone()) + async fn chain_state(&self, did: &IotaDID) -> Result> { + self.chain_states.read().map(|states| states.get(did).cloned()) } - async fn set_index(&self, index: &IdentityIndex) -> Result<()> { - *self.index.write()? = index.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 snapshot(&self, id: IdentityId) -> Result> { - self.states.read().map(|states| states.get(&id).cloned()) - } - - async fn set_snapshot(&self, id: IdentityId, snapshot: &IdentitySnapshot) -> Result<()> { - self.states.write()?.insert(id, snapshot.clone()); - - Ok(()) + async fn state(&self, did: &IotaDID) -> Result> { + self.states.read().map(|states| states.get(did).cloned()) } - async fn append(&self, id: IdentityId, commits: &[Commit]) -> Result<()> { - let mut state: RwLockWriteGuard<'_, _> = self.events.write()?; - let queue: &mut Vec = state.entry(id).or_default(); - - for commit in commits { - queue.push(commit.clone()); - } + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()> { + self.states.write()?.insert(did.clone(), state.clone()); Ok(()) } - async fn stream(&self, id: IdentityId, index: Generation) -> Result>> { - let state: RwLockReadGuard<'_, _> = self.events.read()?; - let queue: Vec = state.get(&id).cloned().unwrap_or_default(); - let index: usize = index.to_u32() as usize; - - Ok(stream::iter(queue.into_iter().skip(index)).map(Ok).boxed()) - } - - async fn purge(&self, id: IdentityId) -> Result<()> { - let _ = self.events.write()?.remove(&id); - let _ = self.states.write()?.remove(&id); - let _ = self.vaults.write()?.remove(&id); + async fn purge(&self, did: &IotaDID) -> Result<()> { + let _ = self.states.write()?.remove(did); + let _ = self.vaults.write()?.remove(did); + let _ = self.chain_states.write()?.remove(did); Ok(()) } - async fn published_generation(&self, id: IdentityId) -> Result> { - Ok(self.published_generations.read()?.get(&id).copied()) + async fn published_generation(&self, did: &IotaDID) -> Result> { + Ok(self.published_generations.read()?.get(did).copied()) } - async fn set_published_generation(&self, id: IdentityId, index: Generation) -> Result<()> { - self.published_generations.write()?.insert(id, index); + async fn set_published_generation(&self, did: &IotaDID, index: Generation) -> Result<()> { + self.published_generations.write()?.insert(did.clone(), index); Ok(()) } } @@ -253,8 +241,7 @@ impl Debug for MemStore { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { if self.expand { f.debug_struct("MemStore") - .field("index", &self.index) - .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 a22cf4bbb7..69810a20b6 100644 --- a/identity-account/src/storage/stronghold.rs +++ b/identity-account/src/storage/stronghold.rs @@ -1,34 +1,30 @@ // 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::HashSet; + +use hashbrown::hash_map::Entry; +use hashbrown::HashMap; use identity_core::convert::FromJson; use identity_core::convert::ToJson; use identity_core::crypto::PrivateKey; use identity_core::crypto::PublicKey; +use identity_did::did::DID; use identity_did::verification::MethodType; +use identity_iota::did::IotaDID; use iota_stronghold::Location; use iota_stronghold::SLIP10DeriveInput; use std::convert::TryFrom; use std::io; use std::path::Path; use std::sync::Arc; +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::IdentityId; -use crate::identity::IdentityIndex; -use crate::identity::IdentitySnapshot; +use crate::identity::ChainState; +use crate::identity::DIDLease; +use crate::identity::IdentityState; use crate::storage::Storage; use crate::stronghold::default_hint; use crate::stronghold::Snapshot; @@ -40,17 +36,9 @@ use crate::types::Signature; use crate::utils::derive_encryption_key; use crate::utils::EncryptionKey; -// name of the metadata store -const META: &str = "$meta"; - -// event concurrency limit -const ECL: usize = 8; - -// ============================================================================= -// ============================================================================= - #[derive(Debug)] pub struct Stronghold { + did_leases: Mutex>, snapshot: Arc, } @@ -67,6 +55,7 @@ impl Stronghold { } Ok(Self { + did_leases: Mutex::new(HashMap::new()), snapshot: Arc::new(snapshot), }) } @@ -75,8 +64,8 @@ impl Stronghold { self.snapshot.store(name, &[]) } - fn vault(&self, id: IdentityId) -> Vault<'_> { - self.snapshot.vault(&fmt_id(id), &[]) + fn vault(&self, id: &IotaDID) -> Vault<'_> { + self.snapshot.vault(&fmt_did(id), &[]) } } @@ -90,8 +79,28 @@ impl Storage for Stronghold { self.snapshot.save().await } - async fn key_new(&self, id: IdentityId, location: &KeyLocation) -> Result { - let vault: Vault<'_> = self.vault(id); + async fn lease_did(&self, did: &IotaDID) -> Result { + let mut hmap = self.did_leases.lock().await; + + match hmap.entry(did.clone()) { + Entry::Occupied(entry) => { + if entry.get().load() { + Err(Error::IdentityInUse) + } else { + entry.get().store(true); + Ok(entry.get().clone()) + } + } + Entry::Vacant(entry) => { + let did_lease = DIDLease::new(); + entry.insert(did_lease.clone()); + Ok(did_lease) + } + } + } + + async fn key_new(&self, did: &IotaDID, location: &KeyLocation) -> Result { + let vault: Vault<'_> = self.vault(did); let public: PublicKey = match location.method() { MethodType::Ed25519VerificationKey2018 => generate_ed25519(&vault, location).await?, @@ -101,8 +110,8 @@ impl Storage for Stronghold { Ok(public) } - async fn key_insert(&self, id: IdentityId, location: &KeyLocation, private_key: PrivateKey) -> Result { - let vault = self.vault(id); + async fn key_insert(&self, did: &IotaDID, location: &KeyLocation, private_key: PrivateKey) -> Result { + let vault = self.vault(did); vault .insert(location_skey(location), private_key.as_ref(), default_hint(), &[]) @@ -114,8 +123,8 @@ impl Storage for Stronghold { } } - async fn key_get(&self, id: IdentityId, location: &KeyLocation) -> Result { - let vault: Vault<'_> = self.vault(id); + async fn key_get(&self, did: &IotaDID, location: &KeyLocation) -> Result { + let vault: Vault<'_> = self.vault(did); match location.method() { MethodType::Ed25519VerificationKey2018 => retrieve_ed25519(&vault, location).await, @@ -123,8 +132,8 @@ impl Storage for Stronghold { } } - async fn key_del(&self, id: IdentityId, location: &KeyLocation) -> Result<()> { - let vault: Vault<'_> = self.vault(id); + async fn key_del(&self, did: &IotaDID, location: &KeyLocation) -> Result<()> { + let vault: Vault<'_> = self.vault(did); match location.method() { MethodType::Ed25519VerificationKey2018 => { @@ -139,8 +148,8 @@ impl Storage for Stronghold { Ok(()) } - async fn key_sign(&self, id: IdentityId, location: &KeyLocation, data: Vec) -> Result { - let vault: Vault<'_> = self.vault(id); + async fn key_sign(&self, did: &IotaDID, location: &KeyLocation, data: Vec) -> Result { + let vault: Vault<'_> = self.vault(did); match location.method() { MethodType::Ed25519VerificationKey2018 => sign_ed25519(&vault, data, location).await, @@ -148,8 +157,8 @@ impl Storage for Stronghold { } } - async fn key_exists(&self, id: IdentityId, location: &KeyLocation) -> Result { - let vault: Vault<'_> = self.vault(id); + async fn key_exists(&self, did: &IotaDID, location: &KeyLocation) -> Result { + let vault: Vault<'_> = self.vault(did); match location.method() { MethodType::Ed25519VerificationKey2018 => vault.exists(location_skey(location)).await, @@ -157,184 +166,66 @@ impl Storage for Stronghold { } } - async fn index(&self) -> Result { - // Load the metadata actor - let store: Store<'_> = self.store(META); + async fn chain_state(&self, did: &IotaDID) -> Result> { + // Load the chain-specific store + let store: Store<'_> = self.store(&fmt_did(did)); - // Read the index from the snapshot - let data: Vec = store.get(location_index()).await?; + let data: Vec = store.get(location_chain_state()).await?; - // No index data (new snapshot) if data.is_empty() { - return Ok(IdentityIndex::new()); + return Ok(None); } - // Deserialize and return - Ok(IdentityIndex::from_json_slice(&data)?) + Ok(Some(ChainState::from_json_slice(&data)?)) } - async fn set_index(&self, index: &IdentityIndex) -> Result<()> { - // Load the metadata actor - let store: Store<'_> = self.store(META); + 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 index - let json: Vec = index.to_json_vec()?; + let json: Vec = chain_state.to_json_vec()?; - // Write the index to the snapshot - store.set(location_index(), json, None).await?; + store.set(location_chain_state(), json, None).await?; Ok(()) } - async fn snapshot(&self, id: IdentityId) -> Result> { + async fn state(&self, did: &IotaDID) -> Result> { // Load the chain-specific store - let store: Store<'_> = self.store(&fmt_id(id)); + let store: Store<'_> = self.store(&fmt_did(did)); - // Read the event snapshot from the stronghold snapshot - let data: Vec = store.get(location_snapshot()).await?; + // Read the state from the stronghold snapshot + let data: Vec = store.get(location_state()).await?; - // No snapshot data found + // No state data found if data.is_empty() { return Ok(None); } // Deserialize and return - Ok(Some(IdentitySnapshot::from_json_slice(&data)?)) + Ok(Some(IdentityState::from_json_slice(&data)?)) } - async fn set_snapshot(&self, id: IdentityId, snapshot: &IdentitySnapshot) -> Result<()> { + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()> { // Load the chain-specific store - let store: Store<'_> = self.store(&fmt_id(id)); - - // Serialize the state snapshot - let json: Vec = snapshot.to_json_vec()?; - - // Write the state snapshot to the stronghold snapshot - store.set(location_snapshot(), json, None).await?; - - Ok(()) - } - - async fn append(&self, id: IdentityId, commits: &[Commit]) -> Result<()> { - fn encode(commit: &Commit) -> Result<(Generation, Vec)> { - Ok((commit.sequence(), commit.event().to_json_vec()?)) - } - - let store: Store<'_> = self.store(&fmt_id(id)); + 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(()))); + // Serialize the state + let json: Vec = state.to_json_vec()?; - // Write all events to the snapshot - future.await?; + // Write the state to the stronghold snapshot + store.set(location_state(), json, None).await?; Ok(()) } - async fn stream(&self, id: IdentityId, index: Generation) -> Result>> { - let name: String = fmt_id(id); - let range: RangeFrom = (index.to_u32() + 1)..; - - 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?; - - Ok((index, event)) - } - }) - // ================================ - // Parse the event - // ================================ - .try_filter_map(move |(index, json)| async move { - if json.is_empty() { - Err(Error::EventNotFound) - } else { - let event: Event = Event::from_json_slice(&json)?; - let commit: Commit = Commit::new(id, 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) - } - - async fn purge(&self, id: IdentityId) -> 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 - let store: Store<'_> = self.store(&fmt_id(id)); - let vault: Vault<'_> = self.vault(id); - - // Scan the event stream and collect a set of all key locations - let output: (Generation, HashSet) = self - .stream(id, Generation::new()) - .await? - .try_fold((Generation::new(), HashSet::new()), fold) - .await?; - - // 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") - } - } - } - - 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, id: IdentityId) -> Result> { - let store: Store<'_> = self.store(&fmt_id(id)); + async fn published_generation(&self, did: &IotaDID) -> Result> { + let store: Store<'_> = self.store(&fmt_did(did)); let bytes = store.get(location_published_generation()).await?; @@ -357,8 +248,8 @@ impl Storage for Stronghold { Ok(Some(gen)) } - async fn set_published_generation(&self, id: IdentityId, index: Generation) -> Result<()> { - let store: Store<'_> = self.store(&fmt_id(id)); + async fn set_published_generation(&self, did: &IotaDID, index: Generation) -> Result<()> { + let store: Store<'_> = self.store(&fmt_did(did)); store .set(location_published_generation(), index.to_u32().to_le_bytes(), None) @@ -400,16 +291,12 @@ async fn sign_ed25519(vault: &Vault<'_>, payload: Vec, location: &KeyLocatio Ok(Signature::new(public_key, signature.into())) } -fn location_index() -> Location { - Location::generic("$index", Vec::new()) -} - -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 { @@ -425,16 +312,9 @@ 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_id(id: IdentityId) -> String { - format!("$identity:{}", id) +fn fmt_did(did: &IotaDID) -> String { + format!("$identity:{}", did.authority()) } diff --git a/identity-account/src/storage/traits.rs b/identity-account/src/storage/traits.rs index ffceb76340..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::IdentityId; -use crate::identity::IdentityIndex; -use crate::identity::IdentitySnapshot; +use crate::identity::ChainState; +use crate::identity::DIDLease; +use crate::identity::IdentityState; use crate::types::Generation; use crate::types::KeyLocation; use crate::types::Signature; @@ -28,59 +26,49 @@ 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`. + /// 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; + /// Creates a new keypair at the specified `location` - async fn key_new(&self, id: IdentityId, location: &KeyLocation) -> Result; + async fn key_new(&self, did: &IotaDID, location: &KeyLocation) -> Result; /// Inserts a private key at the specified `location`. - async fn key_insert(&self, id: IdentityId, location: &KeyLocation, private_key: PrivateKey) -> Result; + async fn key_insert(&self, did: &IotaDID, location: &KeyLocation, private_key: PrivateKey) -> Result; /// Retrieves the public key at the specified `location`. - async fn key_get(&self, id: IdentityId, location: &KeyLocation) -> Result; + async fn key_get(&self, did: &IotaDID, location: &KeyLocation) -> Result; /// Deletes the keypair specified by `location`. - async fn key_del(&self, id: IdentityId, location: &KeyLocation) -> Result<()>; + async fn key_del(&self, did: &IotaDID, location: &KeyLocation) -> Result<()>; /// Signs `data` with the private key at the specified `location`. - async fn key_sign(&self, id: IdentityId, location: &KeyLocation, data: Vec) -> Result; + async fn key_sign(&self, did: &IotaDID, location: &KeyLocation, data: Vec) -> Result; /// Returns `true` if a keypair exists at the specified `location`. - async fn key_exists(&self, id: IdentityId, location: &KeyLocation) -> Result; - - /// Returns the account identity index. - async fn index(&self) -> Result; + 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`. - async fn published_generation(&self, id: IdentityId) -> Result>; + /// 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`. - async fn set_published_generation(&self, id: IdentityId, index: Generation) -> Result<()>; + /// 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<()>; - /// Sets a new account identity index. - async fn set_index(&self, index: &IdentityIndex) -> Result<()>; + /// Returns the chain state of the identity specified by `did`. + async fn chain_state(&self, did: &IotaDID) -> Result>; - /// Returns the state snapshot of the identity specified by `id`. - async fn snapshot(&self, id: IdentityId) -> Result>; + /// Set the chain state of the identity specified by `did`. + async fn set_chain_state(&self, did: &IotaDID, chain_state: &ChainState) -> Result<()>; - /// Sets a new state snapshot for the identity specified by `id`. - async fn set_snapshot(&self, id: IdentityId, snapshot: &IdentitySnapshot) -> Result<()>; + /// Returns the state of the identity specified by `did`. + async fn state(&self, did: &IotaDID) -> Result>; - /// Appends a set of commits to the event stream for the identity specified by `id`. - async fn append(&self, id: IdentityId, commits: &[Commit]) -> Result<()>; + /// Sets a new state for the identity specified by `did`. + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()>; - /// Returns a stream of commits for the identity specified by `id`. - /// - /// The stream may be offset by `index`. - async fn stream(&self, id: IdentityId, index: Generation) -> Result>>; - - /// Returns a list of all commits for the identity specified by `id`. - /// - /// The list may be offset by `index`. - async fn collect(&self, id: IdentityId, index: Generation) -> Result> { - self.stream(id, index).await?.try_collect().await - } - - /// Removes the event stream and state snapshot for the identity specified by `id`. - async fn purge(&self, id: IdentityId) -> Result<()>; + /// Removes the keys and any state for the identity specified by `did`. + async fn purge(&self, did: &IotaDID) -> Result<()>; } #[async_trait::async_trait] @@ -93,63 +81,59 @@ impl Storage for Box { (**self).flush_changes().await } - async fn key_new(&self, id: IdentityId, location: &KeyLocation) -> Result { - (**self).key_new(id, location).await - } - - async fn key_insert(&self, id: IdentityId, location: &KeyLocation, private_key: PrivateKey) -> Result { - (**self).key_insert(id, location, private_key).await + async fn lease_did(&self, did: &IotaDID) -> Result { + (**self).lease_did(did).await } - async fn key_get(&self, id: IdentityId, location: &KeyLocation) -> Result { - (**self).key_get(id, location).await + async fn key_new(&self, did: &IotaDID, location: &KeyLocation) -> Result { + (**self).key_new(did, location).await } - async fn key_del(&self, id: IdentityId, location: &KeyLocation) -> Result<()> { - (**self).key_del(id, location).await + async fn key_insert(&self, did: &IotaDID, location: &KeyLocation, private_key: PrivateKey) -> Result { + (**self).key_insert(did, location, private_key).await } - async fn key_sign(&self, id: IdentityId, location: &KeyLocation, data: Vec) -> Result { - (**self).key_sign(id, location, data).await + async fn key_get(&self, did: &IotaDID, location: &KeyLocation) -> Result { + (**self).key_get(did, location).await } - async fn key_exists(&self, id: IdentityId, location: &KeyLocation) -> Result { - (**self).key_exists(id, location).await + async fn key_del(&self, did: &IotaDID, location: &KeyLocation) -> Result<()> { + (**self).key_del(did, location).await } - async fn index(&self) -> Result { - (**self).index().await + async fn key_sign(&self, did: &IotaDID, location: &KeyLocation, data: Vec) -> Result { + (**self).key_sign(did, location, data).await } - async fn set_index(&self, index: &IdentityIndex) -> Result<()> { - (**self).set_index(index).await + async fn key_exists(&self, did: &IotaDID, location: &KeyLocation) -> Result { + (**self).key_exists(did, location).await } - async fn snapshot(&self, id: IdentityId) -> Result> { - (**self).snapshot(id).await + async fn chain_state(&self, did: &IotaDID) -> Result> { + (**self).chain_state(did).await } - async fn set_snapshot(&self, id: IdentityId, snapshot: &IdentitySnapshot) -> Result<()> { - (**self).set_snapshot(id, 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, id: IdentityId, commits: &[Commit]) -> Result<()> { - (**self).append(id, commits).await + async fn state(&self, did: &IotaDID) -> Result> { + (**self).state(did).await } - async fn stream(&self, id: IdentityId, index: Generation) -> Result>> { - (**self).stream(id, index).await + async fn set_state(&self, did: &IotaDID, state: &IdentityState) -> Result<()> { + (**self).set_state(did, state).await } - async fn purge(&self, id: IdentityId) -> Result<()> { - (**self).purge(id).await + async fn purge(&self, did: &IotaDID) -> Result<()> { + (**self).purge(did).await } - async fn published_generation(&self, id: IdentityId) -> Result> { - (**self).published_generation(id).await + async fn published_generation(&self, did: &IotaDID) -> Result> { + (**self).published_generation(did).await } - async fn set_published_generation(&self, id: IdentityId, index: Generation) -> Result<()> { - (**self).set_published_generation(id, index).await + async fn set_published_generation(&self, did: &IotaDID, index: Generation) -> Result<()> { + (**self).set_published_generation(did, index).await } } diff --git a/identity-account/src/tests/account.rs b/identity-account/src/tests/account.rs new file mode 100644 index 0000000000..01e2a0855d --- /dev/null +++ b/identity-account/src/tests/account.rs @@ -0,0 +1,239 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 +use std::sync::Arc; + +use crate::account::Account; +use crate::account::AccountBuilder; +use crate::account::AccountConfig; +use crate::account::AccountSetup; +use crate::identity::IdentitySetup; +use crate::storage::MemStore; +use crate::Result; + +use identity_core::common::Url; +use identity_did::verification::MethodScope; +use identity_iota::did::IotaDID; +use identity_iota::tangle::MessageId; +use identity_iota::tangle::MessageIdExt; + +#[tokio::test] +async fn test_account_builder() -> Result<()> { + let mut builder: AccountBuilder = AccountBuilder::default().testmode(true); + + let account1: Account = builder.create_identity(IdentitySetup::default()).await?; + + builder = builder.autopublish(false); + + let account2: Account = builder.create_identity(IdentitySetup::default()).await?; + + assert!(account1.autopublish()); + assert!(!account2.autopublish()); + + let did1 = account1.did().to_owned(); + let did2 = account2.did().to_owned(); + account2.delete_identity().await?; + + assert!(matches!( + builder.load_identity(did2).await.unwrap_err(), + crate::Error::IdentityNotFound + )); + + // Relase the lease on did1. + std::mem::drop(account1); + + assert!(builder.load_identity(did1).await.is_ok()); + + Ok(()) +} + +#[tokio::test] +async fn test_account_did_lease() -> Result<()> { + let mut builder: AccountBuilder = AccountBuilder::default().testmode(true); + + let did: IotaDID = { + let account: Account = builder.create_identity(IdentitySetup::default()).await?; + account.did().to_owned() + }; // <-- Lease released here. + + // Lease is not in-use + let _account = builder.load_identity(did.clone()).await.unwrap(); + + // Lease is in-use + assert!(matches!( + builder.load_identity(did).await.unwrap_err(), + crate::Error::IdentityInUse + )); + + 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(()) +} + +#[tokio::test] +async fn test_account_autopublish() -> Result<()> { + // =========================================================================== + // Create, update and "publish" an identity + // =========================================================================== + + let config = AccountConfig::default().autopublish(false).testmode(true); + let account_config = AccountSetup::new(Arc::new(MemStore::new())).config(config); + + let mut account = Account::create_identity(account_config, IdentitySetup::new()).await?; + + account + .update_identity() + .create_service() + .fragment("my-service") + .type_("LinkedDomains") + .endpoint(Url::parse("https://example.org").unwrap()) + .apply() + .await?; + + account + .update_identity() + .create_service() + .fragment("my-other-service") + .type_("LinkedDomains") + .endpoint(Url::parse("https://example.org").unwrap()) + .apply() + .await?; + + assert!(account.chain_state().last_integration_message_id().is_null()); + assert!(account.chain_state().last_diff_message_id().is_null()); + + account.publish_updates().await?; + + let last_int_message_id = *account.chain_state().last_integration_message_id(); + + assert!(!last_int_message_id.is_null()); + assert!(account.chain_state().last_diff_message_id().is_null()); + + // =========================================================================== + // Service assertions + // =========================================================================== + + let doc = account.document(); + + assert_eq!(doc.methods().count(), 1); + assert_eq!(doc.service().len(), 2); + + for service in ["my-service", "my-other-service"] { + assert!(doc.service().query(service).is_some()); + } + + // =========================================================================== + // More updates to the identity + // =========================================================================== + + account + .update_identity() + .delete_service() + .fragment("my-service") + .apply() + .await?; + + account + .update_identity() + .delete_service() + .fragment("my-other-service") + .apply() + .await?; + + account + .update_identity() + .create_method() + .fragment("new-method") + .apply() + .await?; + + account.publish_updates().await?; + + // No integration message was published + assert_eq!( + &last_int_message_id, + account.chain_state().last_integration_message_id() + ); + + let last_diff_message_id = *account.chain_state().last_diff_message_id(); + assert!(!last_diff_message_id.is_null()); + + // =========================================================================== + // Second round of assertions + // =========================================================================== + + let doc = account.document(); + + assert_eq!(doc.service().len(), 0); + assert_eq!(doc.methods().count(), 2); + + for method in ["sign-0", "new-method"] { + assert!(doc.resolve_method(method).is_some()); + } + + // =========================================================================== + // 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?; + + // Another int update was published. + assert_ne!( + &last_int_message_id, + account.chain_state().last_integration_message_id() + ); + + // Diff message id was reset. + assert!(account.chain_state().last_diff_message_id().is_null()); + + Ok(()) +} diff --git a/identity-account/src/tests/commands.rs b/identity-account/src/tests/commands.rs deleted file mode 100644 index e2c6292227..0000000000 --- a/identity-account/src/tests/commands.rs +++ /dev/null @@ -1,444 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use crate::account::Account; -use crate::account::Config; -use crate::error::Error; -use crate::error::Result; -use crate::events::Command; -use crate::events::UpdateError; -use crate::identity::IdentityCreate; -use crate::identity::IdentityId; -use crate::identity::IdentitySnapshot; -use crate::identity::IdentityState; -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; - -async fn new_account() -> Result { - let store: MemStore = MemStore::new(); - let config: Config = Config::new().testmode(true); - - Account::with_config(store, config).await -} - -#[tokio::test] -async fn test_create_identity() -> Result<()> { - let account: Account = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - assert_eq!(snapshot.sequence(), Generation::new()); - assert_eq!(snapshot.id(), identity); - assert!(snapshot.identity().did().is_none()); - assert_eq!(snapshot.identity().created(), UnixTimestamp::EPOCH); - assert_eq!(snapshot.identity().updated(), UnixTimestamp::EPOCH); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - assert_eq!(snapshot.sequence(), Generation::from_u32(3)); - assert_eq!(snapshot.id(), identity); - 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_invalid_method() -> Result<()> { - const TYPES: &[MethodType] = &[MethodType::MerkleKeyCollection2021]; - - let account: Account = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - // initial snapshot version = 0 - assert_eq!(snapshot.sequence(), Generation::new()); - - for type_ in TYPES.iter().copied() { - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: type_, - }; - - let output: Result<()> = account.process(identity, command, false).await; - - assert!(matches!( - output.unwrap_err(), - Error::UpdateError(UpdateError::InvalidMethodType(_)) - )); - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - // version is still 0, no events have been committed - assert_eq!(snapshot.sequence(), Generation::new()); - } - - Ok(()) -} - -#[tokio::test] -async fn test_create_identity_network() -> Result<()> { - let account: Account = new_account().await?; - - // Create an identity with a valid network string. - let create_identity: IdentityCreate = IdentityCreate::new().network("dev")?.key_type(KeyType::Ed25519); - let identity: IdentityState = account.create_identity(create_identity).await?; - - // Ensure the identity creation was successful. - assert!(identity.did().is_some()); - assert!(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 = IdentityCreate::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 account: Account = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - // initial snapshot version = 0 - assert_eq!(snapshot.sequence(), Generation::new()); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command.clone(), false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - // Version is now 3. - assert_eq!(snapshot.sequence(), Generation::from(3)); - - let output: Result<()> = account.process(identity, command, false).await; - - assert!(matches!( - output.unwrap_err(), - Error::UpdateError(UpdateError::DocumentAlreadyExists), - )); - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - // Version is still 3, no events have been committed. - assert_eq!(snapshot.sequence(), Generation::from(3)); - - Ok(()) -} - -#[tokio::test] -async fn test_create_identity_from_private_key() -> Result<()> { - let account: Account = new_account().await?; - let account2: Account = new_account().await?; - - let identity: IdentityId = IdentityId::from_u32(1); - - let private_key = KeyPair::new_ed25519()?.private().clone(); - - let id_create = IdentityCreate::new() - .key_type(KeyType::Ed25519) - .method_secret(MethodSecret::Ed25519(private_key)); - - account.create_identity(id_create.clone()).await?; - account2.create_identity(id_create).await?; - - let ident = account.find_identity(identity).await.unwrap().unwrap(); - let ident2 = account.find_identity(identity).await.unwrap().unwrap(); - - // The same private key should result in the same did - assert_eq!(ident.did(), ident2.did()); - assert_eq!(ident.capability_invocation()?, ident2.capability_invocation()?); - - Ok(()) -} - -#[tokio::test] -async fn test_create_identity_from_invalid_private_key() -> Result<()> { - let account: Account = new_account().await?; - - let private_bytes: Box<[u8]> = Box::new([0; 33]); - let private_key: PrivateKey = PrivateKey::from(private_bytes); - - let id_create = IdentityCreate::new() - .key_type(KeyType::Ed25519) - .method_secret(MethodSecret::Ed25519(private_key)); - - let err = account.create_identity(id_create).await.unwrap_err(); - - assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); - - Ok(()) -} - -#[tokio::test] -async fn test_create_method() -> Result<()> { - let account: Account = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command, false).await?; - - let command: Command = Command::CreateMethod { - scope: MethodScope::default(), - method_secret: None, - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - account.process(identity, command, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - assert_eq!(snapshot.sequence(), Generation::from_u32(5)); - assert_eq!(snapshot.id(), identity); - 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 = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command, false).await?; - - let command: Command = Command::CreateMethod { - scope: MethodScope::default(), - method_secret: None, - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(3)); - - account.process(identity, command.clone(), false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(5)); - - let output: _ = account.process(identity, command, false).await; - - assert!(matches!( - output.unwrap_err(), - Error::UpdateError(UpdateError::DuplicateKeyFragment(_)), - )); - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(5)); - - Ok(()) -} - -#[tokio::test] -async fn test_create_method_from_private_key() -> Result<()> { - let account: Account = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command, false).await?; - - let keypair = KeyPair::new_ed25519()?; - - let command: Command = Command::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::Ed25519(keypair.private().clone())), - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - account.process(identity, command, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - - let method: &TinyMethod = snapshot.identity().methods().fetch("key-1")?; - - let public_key = account.store().key_get(identity, 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 = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command, false).await?; - - let private_bytes: Box<[u8]> = Box::new([0; 33]); - let private_key = PrivateKey::from(private_bytes); - - let command: Command = Command::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::Ed25519(private_key)), - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - let err = account.process(identity, command, 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 = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command, false).await?; - - let private_bytes: Box<[u8]> = Box::new([0; 32]); - let private_key = PrivateKey::from(private_bytes); - - let command: Command = Command::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::Ed25519(private_key)), - type_: MethodType::MerkleKeyCollection2021, - fragment: "key-1".to_owned(), - }; - - let err = account.process(identity, command, false).await.unwrap_err(); - - assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); - - let key_collection = KeyCollection::new_ed25519(4).unwrap(); - - let command: Command = Command::CreateMethod { - scope: MethodScope::default(), - method_secret: Some(MethodSecret::MerkleKeyCollection(key_collection)), - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-2".to_owned(), - }; - - let err = account.process(identity, command, false).await.unwrap_err(); - - assert!(matches!(err, Error::UpdateError(UpdateError::InvalidMethodSecret(_)))); - - Ok(()) -} - -#[tokio::test] -async fn test_delete_method() -> Result<()> { - let account: Account = new_account().await?; - let identity: IdentityId = IdentityId::from_u32(1); - - let command: Command = Command::CreateIdentity { - network: None, - method_secret: None, - method_type: MethodType::Ed25519VerificationKey2018, - }; - - account.process(identity, command, false).await?; - - let command: Command = Command::CreateMethod { - scope: MethodScope::default(), - method_secret: None, - type_: MethodType::Ed25519VerificationKey2018, - fragment: "key-1".to_owned(), - }; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).await?; - assert_eq!(snapshot.sequence(), Generation::from_u32(3)); - - account.process(identity, command, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).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 command: Command = Command::DeleteMethod { - fragment: "key-1".to_owned(), - }; - - account.process(identity, command, false).await?; - - let snapshot: IdentitySnapshot = account.load_snapshot(identity).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 deleted file mode 100644 index 268e2e473c..0000000000 --- a/identity-account/src/tests/lazy.rs +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2020-2021 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::pin::Pin; - -use futures::Future; - -use identity_core::common::Url; -use identity_iota::chain::DocumentHistory; -use identity_iota::did::IotaDID; -use identity_iota::did::IotaVerificationMethod; -use identity_iota::tangle::Client; -use identity_iota::tangle::Network; -use identity_iota::Error as IotaError; - -use crate::account::Account; -use crate::identity::IdentityCreate; -use crate::identity::IdentityState; -use crate::identity::IdentityUpdater; -use crate::Error as AccountError; -use crate::Result; - -#[tokio::test] -async fn test_lazy_updates() -> Result<()> { - network_resilient_test(2, |test_run| { - Box::pin(async move { - // =========================================================================== - // Create, update and publish an identity - // =========================================================================== - let account: Account = Account::builder().autopublish(false).build().await?; - - let network = if test_run % 2 == 0 { - Network::Devnet - } else { - Network::Mainnet - }; - - let identity: IdentityState = account - .create_identity(IdentityCreate::new().network(network.name()).unwrap()) - .await?; - - let did: &IotaDID = identity.try_did()?; - - let did_updater: IdentityUpdater<'_, '_, _> = account.update_identity(did); - - did_updater - .create_service() - .fragment("my-service") - .type_("LinkedDomains") - .endpoint(Url::parse("https://example.org").unwrap()) - .apply() - .await?; - - did_updater - .create_service() - .fragment("my-other-service") - .type_("LinkedDomains") - .endpoint(Url::parse("https://example.org").unwrap()) - .apply() - .await?; - - account.publish_updates(did).await?; - - // =========================================================================== - // First round of assertions - // =========================================================================== - - let doc = account.resolve_identity(identity.did().unwrap()).await?; - - let services = doc.service(); - - assert_eq!(doc.methods().count(), 1); - assert_eq!(services.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)); - } - - // =========================================================================== - // More updates to the identity - // =========================================================================== - - did_updater.delete_service().fragment("my-service").apply().await?; - - did_updater - .delete_service() - .fragment("my-other-service") - .apply() - .await?; - - did_updater.create_method().fragment("new-method").apply().await?; - - account.publish_updates(did).await?; - - // =========================================================================== - // Second round of assertions - // =========================================================================== - - let doc = account.resolve_identity(identity.did().unwrap()).await?; - let methods = doc.methods().collect::>(); - - assert_eq!(doc.service().len(), 0); - assert_eq!(methods.len(), 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)); - } - - // =========================================================================== - // History assertions - // =========================================================================== - - let client: Client = Client::from_network(network).await?; - - let history: DocumentHistory = client.resolve_history(did).await?; - - assert_eq!(history.integration_chain_data.len(), 1); - assert_eq!(history.diff_chain_data.len(), 1); - - Ok(()) - }) - }) - .await?; - - Ok(()) -} - -// Repeats the test in the closure `test_runs` number of times. -// Network problems, i.e. a ClientError trigger a re-run. -// Other errors end the test immediately. -async fn network_resilient_test( - test_runs: u32, - f: impl Fn(u32) -> Pin>>>, -) -> Result<()> { - for test_run in 0..test_runs { - let test_attempt = f(test_run).await; - - match test_attempt { - error @ Err(AccountError::IotaError(IotaError::ClientError(_))) => { - eprintln!("test run {} errored with {:?}", test_run, error); - - if test_run == test_runs - 1 { - return error; - } - } - other => return other, - } - } - - Ok(()) -} diff --git a/identity-account/src/tests/mod.rs b/identity-account/src/tests/mod.rs index 4cc77c616c..21a66ace56 100644 --- a/identity-account/src/tests/mod.rs +++ b/identity-account/src/tests/mod.rs @@ -1,5 +1,5 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -mod commands; -mod lazy; +mod account; +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 62% rename from identity-account/src/events/macros.rs rename to identity-account/src/updates/macros.rs index b47621aeef..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,23 +26,22 @@ 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)), )), } }; ($(#[$doc:meta])* $ident:ident { $(@ $requirement:ident $field:ident $ty:ty $(= $value:expr)?),* $(,)* }) => { paste::paste! { $(#[$doc])* - #[derive(Clone, Debug)] - pub struct [<$ident Builder>]<'account, 'key, K: $crate::identity::IdentityKey> { - account: &'account Account, - key: &'key K, + #[derive(Debug)] + pub struct [<$ident Builder>]<'account> { + account: &'account mut Account, $( $field: Option<$ty>, )* } - impl<'account, 'key, K: $crate::identity::IdentityKey> [<$ident Builder>]<'account, 'key, K> { + impl<'account> [<$ident Builder>]<'account> { $( pub fn $field>(mut self, value: VALUE) -> Self { self.$field = Some(value.into()); @@ -50,10 +49,9 @@ macro_rules! impl_command_builder { } )* - pub fn new(account: &'account Account, key: &'key K) -> [<$ident Builder>]<'account, 'key, K> { + pub fn new(account: &'account mut Account) -> [<$ident Builder>]<'account> { [<$ident Builder>] { account, - key, $( $field: None, )* @@ -61,20 +59,20 @@ macro_rules! impl_command_builder { } pub async fn apply(self) -> $crate::Result<()> { - let update = $crate::events::Command::$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.apply_command(self.key, update).await + self.account.process_update(update).await } } - impl<'account, 'key, K: $crate::identity::IdentityKey> $crate::identity::IdentityUpdater<'account, 'key, K> { + impl<'account> $crate::identity::IdentityUpdater<'account> { /// Creates a new builder to modify the identity. See the documentation of the return type for details. - pub fn [<$ident:snake>](&self) -> [<$ident Builder>]<'account, 'key, K> { - [<$ident Builder>]::new(self.account, self.key) + pub fn [<$ident:snake>](&'account mut self) -> [<$ident Builder>]<'account> { + [<$ident Builder>]::new(self.account) } } } diff --git a/identity-account/src/updates/mod.rs b/identity-account/src/updates/mod.rs new file mode 100644 index 0000000000..b932f8cecf --- /dev/null +++ b/identity-account/src/updates/mod.rs @@ -0,0 +1,11 @@ +// Copyright 2020-2021 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[macro_use] +mod macros; + +mod error; +mod update; + +pub use self::error::*; +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-core/src/crypto/key/pair.rs b/identity-core/src/crypto/key/pair.rs index c5283a6446..a1fcad0533 100644 --- a/identity-core/src/crypto/key/pair.rs +++ b/identity-core/src/crypto/key/pair.rs @@ -1,6 +1,9 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::convert::TryInto; + +use crypto::signatures::ed25519; use zeroize::Zeroize; use crate::crypto::KeyRef; @@ -9,6 +12,7 @@ use crate::crypto::PrivateKey; use crate::crypto::PublicKey; use crate::error::Result; use crate::utils::generate_ed25519_keypair; +use crate::utils::keypair_from_ed25519_private_key; /// A convenient type for representing a pair of cryptographic keys. #[derive(Clone, Debug)] @@ -33,6 +37,23 @@ impl KeyPair { Ok(Self { type_, public, private }) } + /// Reconstructs the [`Ed25519`][`KeyType::Ed25519`] [`KeyPair`] from a private key. + pub fn try_from_ed25519_bytes(private_key_bytes: &[u8]) -> Result { + let private_key_bytes: [u8; ed25519::SECRET_KEY_LENGTH] = private_key_bytes + .try_into() + .map_err(|_| crypto::Error::PrivateKeyError)?; + + let private_key = ed25519::SecretKey::from_bytes(private_key_bytes); + + let (public, private) = keypair_from_ed25519_private_key(private_key); + + Ok(Self { + type_: KeyType::Ed25519, + public, + private, + }) + } + /// Returns the [`type`][`KeyType`] of the `KeyPair` object. pub const fn type_(&self) -> KeyType { self.type_ diff --git a/identity-core/src/utils/ed25519.rs b/identity-core/src/utils/ed25519.rs index 83c4644c75..cf4826375d 100644 --- a/identity-core/src/utils/ed25519.rs +++ b/identity-core/src/utils/ed25519.rs @@ -18,6 +18,16 @@ pub fn generate_ed25519_keypair() -> Result<(PublicKey, PrivateKey)> { Ok((public, private)) } +/// Reconstructs the ed25519 public key given the private key. +pub fn keypair_from_ed25519_private_key(private_key: ed25519::SecretKey) -> (PublicKey, PrivateKey) { + let public: ed25519::PublicKey = private_key.public_key(); + + let private: PrivateKey = private_key.to_bytes().to_vec().into(); + let public: PublicKey = public.to_bytes().to_vec().into(); + + (public, private) +} + /// Generates a list of public/private ed25519 keys. pub fn generate_ed25519_keypairs(count: usize) -> Result> { (0..count).map(|_| generate_ed25519_keypair()).collect() diff --git a/identity-did/src/document/core_document.rs b/identity-did/src/document/core_document.rs index ad2659d24a..bc40573607 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..062c8891ed 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,23 @@ impl IotaDocument { Ok(()) } + /// 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(&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) + } + + /// Detaches the given relationship from the given method, if the method exists. + 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 +475,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 +521,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 +662,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 +676,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 +1224,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 +1267,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 +1291,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 +1303,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 +1338,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 +1366,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 +1390,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 +1473,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 +1511,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 +1522,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 +1635,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 +1647,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..9cbb700bfe --- /dev/null +++ b/identity-iota/src/tangle/publish.rs @@ -0,0 +1,253 @@ +// 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> = Self::extract_signing_keys(old_doc); + let new_capability_invocation_set: Vec> = Self::extract_signing_keys(new_doc); + + if old_capability_invocation_set != new_capability_invocation_set { + Some(PublishType::Integration) + } else { + Some(PublishType::Diff) + } + } + + fn extract_signing_keys(document: &IotaDocument) -> Vec> { + document + .as_document() + .capability_invocation() + .iter() + .map(|method_ref| match method_ref { + MethodRef::Embed(method) => Some(method), + MethodRef::Refer(did_url) => document.as_document().resolve_method(did_url), + }) + .filter(|method| { + if let Some(method) = method { + UPDATE_METHOD_TYPES.contains(&method.key_type()) + } else { + true + } + }) + .collect() + } +} + +#[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::*; }