From 842f483c1aab7760a2e0bddad515b932ecbed7ba Mon Sep 17 00:00:00 2001 From: Frederik Rothenberger Date: Mon, 2 Sep 2024 13:01:22 +0200 Subject: [PATCH 1/5] Add feature to support custom `now_utc` implementations (#1397) * Add feature to support custom `now_utc` implementations This PR adds a feature to `identity_core` to allow specifying a custom function to get the current time (`Timestamp::now_utc`). The feature is disabled by default. Closes #1391. * Formatting * Fix wrong comment * chore: clippy fixes and fmt * chore: clippy fixes and fmt * Allow compilation for target wasm32-unknown-unknown without js-sys Also removes the unused dependency on `iota-crypto` (which also had a dependency on `js-sys` through the `random` feature). * chore(ci): Fix CI actions; add random feature to iota crypto --------- Co-authored-by: Yasir --- .github/workflows/build-and-test.yml | 11 ++- identity_core/Cargo.toml | 11 ++- identity_core/src/common/ordered_set.rs | 2 +- identity_core/src/common/timestamp.rs | 16 +++- identity_core/src/custom_time.rs | 88 +++++++++++++++++++ identity_core/src/lib.rs | 7 +- identity_core/tests/custom_time.rs | 18 ++++ .../src/credential/jwt_serialization.rs | 2 +- .../domain_linkage_validator.rs | 6 +- .../revocation/status_list_2021/credential.rs | 2 +- .../jwt_presentation_validator.rs | 8 +- .../src/validator/sd_jwt/validator.rs | 6 +- .../src/document/core_document.rs | 4 +- identity_jose/src/jwu/serde.rs | 3 +- identity_jose/src/tests/rfc8037.rs | 24 +++-- identity_resolver/src/resolution/resolver.rs | 8 +- identity_storage/Cargo.toml | 13 ++- identity_stronghold/Cargo.toml | 17 ++-- 18 files changed, 193 insertions(+), 53 deletions(-) create mode 100644 identity_core/src/custom_time.rs create mode 100644 identity_core/tests/custom_time.rs diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e24c9a171d..8919e81020 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -127,14 +127,17 @@ jobs: # Build the library, tests, and examples without running them to avoid recompilation in the run tests step - name: Build with all features - run: cargo build --workspace --tests --examples --all-features --release + run: cargo build --workspace --tests --examples --release - name: Start iota sandbox if: matrix.os == 'ubuntu-latest' uses: './.github/actions/iota-sandbox/setup' - - name: Run tests - run: cargo test --workspace --all-features --release + - name: Run tests excluding `custom_time` feature + run: cargo test --workspace --release + + - name: Run tests with `custom_time` feature + run: cargo test --test custom_time --features="custom_time" - name: Run Rust examples # run examples only on ubuntu for now @@ -157,7 +160,7 @@ jobs: - name: Tear down iota sandbox if: matrix.os == 'ubuntu-latest' && always() uses: './.github/actions/iota-sandbox/tear-down' - + - name: Stop sccache uses: './.github/actions/rust/sccache/stop-sccache' with: diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index 0f7a8a34eb..f8aa615b28 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true description = "The core traits and types for the identity-rs library." [dependencies] -iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "random", "sha", "x25519", "std"] } multibase = { version = "0.9", default-features = false, features = ["std"] } serde = { workspace = true, features = ["std"] } serde_json = { workspace = true, features = ["std"] } @@ -22,7 +21,7 @@ time = { version = "0.3.23", default-features = false, features = ["std", "serde url = { version = "2.4", default-features = false, features = ["serde"] } zeroize = { version = "1.6", default-features = false } -[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] +[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))'.dependencies] js-sys = { version = "0.3.55", default-features = false } [dev-dependencies] @@ -38,3 +37,11 @@ rustdoc-args = ["--cfg", "docsrs"] [lints] workspace = true + +[features] +# Enables a macro to provide a custom time (Timestamp::now_utc) implementation, see src/custom_time.rs +custom_time = [] + +[[test]] +name = "custom_time" +required-features = ["custom_time"] diff --git a/identity_core/src/common/ordered_set.rs b/identity_core/src/common/ordered_set.rs index b3650490ef..885342409b 100644 --- a/identity_core/src/common/ordered_set.rs +++ b/identity_core/src/common/ordered_set.rs @@ -488,7 +488,7 @@ mod tests { /// Produces a strategy for generating an ordered set together with two values according to the following algorithm: /// 1. Call `f` to get a pair of sets (x,y). /// 2. Toss a coin to decide whether to pick an element from x at random, or from y (if the chosen set is empty - /// Default is called). 3. Repeat step 2 and let the two outcomes be denoted a and b. + /// Default is called). 3. Repeat step 2 and let the two outcomes be denoted a and b. /// 4. Toss a coin to decide whether to swap the keys of a and b. /// 5. return (x,a,b) fn set_with_values(f: F) -> impl Strategy, T, T)> diff --git a/identity_core/src/common/timestamp.rs b/identity_core/src/common/timestamp.rs index 8de1832409..4f03db2cea 100644 --- a/identity_core/src/common/timestamp.rs +++ b/identity_core/src/common/timestamp.rs @@ -42,7 +42,10 @@ impl Timestamp { /// fractional seconds truncated. /// /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production). - #[cfg(not(all(target_arch = "wasm32", not(target_os = "wasi"))))] + #[cfg(all( + not(all(target_arch = "wasm32", not(target_os = "wasi"))), + not(feature = "custom_time") + ))] pub fn now_utc() -> Self { Self(truncate_fractional_seconds(OffsetDateTime::now_utc())) } @@ -51,7 +54,7 @@ impl Timestamp { /// fractional seconds truncated. /// /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production). - #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] + #[cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))] pub fn now_utc() -> Self { let milliseconds_since_unix_epoch: i64 = js_sys::Date::now() as i64; let seconds: i64 = milliseconds_since_unix_epoch / 1000; @@ -59,6 +62,15 @@ impl Timestamp { Self::from_unix(seconds).expect("Timestamp failed to convert system datetime") } + /// Creates a new `Timestamp` with the current date and time, normalized to UTC+00:00 with + /// fractional seconds truncated. + /// + /// See the [`datetime` DID-core specification](https://www.w3.org/TR/did-core/#production). + #[cfg(feature = "custom_time")] + pub fn now_utc() -> Self { + crate::custom_time::now_utc_custom() + } + /// Returns the `Timestamp` as an [RFC 3339](https://tools.ietf.org/html/rfc3339) `String`. pub fn to_rfc3339(&self) -> String { // expect is okay, constructors ensure RFC 3339 compatible timestamps. diff --git a/identity_core/src/custom_time.rs b/identity_core/src/custom_time.rs new file mode 100644 index 0000000000..ef509a19de --- /dev/null +++ b/identity_core/src/custom_time.rs @@ -0,0 +1,88 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +//! An implementation of `now_utc` which calls out to an externally defined function. +use crate::common::Timestamp; + +/// Register a function to be invoked by `identity_core` in order to get a [Timestamp] representing +/// "now". +/// +/// ## Writing a custom `now_utc` implementation +/// +/// The function to register must have the same signature as +/// [`Timestamp::now_utc`](Timestamp::now_utc). The function can be defined +/// wherever you want, either in root crate or a dependent crate. +/// +/// For example, if we wanted a `static_now_utc` crate containing an +/// implementation that always returns the same timestamp, we would first depend on `identity_core` +/// (for the [`Timestamp`] type) in `static_now_utc/Cargo.toml`: +/// ```toml +/// [dependencies] +/// identity_core = "1" +/// ``` +/// Note that the crate containing this function does **not** need to enable the +/// `"custom_time"` Cargo feature. +/// +/// Next, in `static_now_utc/src/lib.rs`, we define our function: +/// ```rust +/// use identity_core::common::Timestamp; +/// +/// // Some fixed timestamp +/// const MY_FIXED_TIMESTAMP: i64 = 1724402964; +/// pub fn static_now_utc() -> Timestamp { +/// Timestamp::from_unix(MY_FIXED_TIMESTAMP).unwrap() +/// } +/// ``` +/// +/// ## Registering a custom `now_utc` implementation +/// +/// Functions can only be registered in the root binary crate. Attempting to +/// register a function in a non-root crate will result in a linker error. +/// This is similar to +/// [`#[panic_handler]`](https://doc.rust-lang.org/nomicon/panic-handler.html) or +/// [`#[global_allocator]`](https://doc.rust-lang.org/edition-guide/rust-2018/platform-and-target-support/global-allocators.html), +/// where helper crates define handlers/allocators but only the binary crate +/// actually _uses_ the functionality. +/// +/// To register the function, we first depend on `static_now_utc` _and_ +/// `identity_core` in `Cargo.toml`: +/// ```toml +/// [dependencies] +/// static_now_utc = "0.1" +/// identity_core = { version = "1", features = ["custom_time"] } +/// ``` +/// +/// Then, we register the function in `src/main.rs`: +/// ```rust +/// # mod static_now_utc { pub fn static_now_utc() -> Timestamp { unimplemented!() } } +/// +/// use identity_core::register_custom_now_utc; +/// use static_now_utc::static_now_utc; +/// +/// register_custom_now_utc!(static_now_utc); +/// ``` +/// +/// Now any user of `now_utc` (direct or indirect) on this target will use the +/// registered function. +#[macro_export] +macro_rules! register_custom_now_utc { + ($path:path) => { + const __GET_TIME_INTERNAL: () = { + // We use Rust ABI to be safe against potential panics in the passed function. + #[no_mangle] + unsafe fn __now_utc_custom() -> Timestamp { + // Make sure the passed function has the type of `now_utc_custom` + type F = fn() -> Timestamp; + let f: F = $path; + f() + } + }; + }; +} + +pub(crate) fn now_utc_custom() -> Timestamp { + extern "Rust" { + fn __now_utc_custom() -> Timestamp; + } + unsafe { __now_utc_custom() } +} diff --git a/identity_core/src/lib.rs b/identity_core/src/lib.rs index b915fcdeba..0e439441e4 100644 --- a/identity_core/src/lib.rs +++ b/identity_core/src/lib.rs @@ -1,7 +1,6 @@ // Copyright 2020-2021 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#![forbid(unsafe_code)] #![doc = include_str!("./../README.md")] #![allow(clippy::upper_case_acronyms)] #![warn( @@ -19,9 +18,15 @@ #[doc(inline)] pub use serde_json::json; +#[forbid(unsafe_code)] pub mod common; +#[forbid(unsafe_code)] pub mod convert; +#[forbid(unsafe_code)] pub mod error; +#[cfg(feature = "custom_time")] +pub mod custom_time; + pub use self::error::Error; pub use self::error::Result; diff --git a/identity_core/tests/custom_time.rs b/identity_core/tests/custom_time.rs new file mode 100644 index 0000000000..9c700d523e --- /dev/null +++ b/identity_core/tests/custom_time.rs @@ -0,0 +1,18 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Timestamp; +use identity_core::register_custom_now_utc; + +const STATIC_TIME: i64 = 1724402964; // 2024-08-23T11:33:30+00:00 +pub fn static_now_utc() -> Timestamp { + Timestamp::from_unix(STATIC_TIME).unwrap() +} + +register_custom_now_utc!(static_now_utc); + +#[test] +fn should_use_registered_static_time() { + let timestamp = Timestamp::now_utc(); + assert_eq!(timestamp.to_unix(), STATIC_TIME) +} diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 6ce3c60b67..3f2a33f0a7 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -32,7 +32,7 @@ use crate::Result; /// This type is opinionated in the following ways: /// 1. Serialization tries to duplicate as little as possible between the required registered claims and the `vc` entry. /// 2. Only allows serializing/deserializing claims "exp, iss, nbf &/or iat, jti, sub and vc". Other custom properties -/// must be set in the `vc` entry. +/// must be set in the `vc` entry. #[derive(Serialize, Deserialize)] pub(crate) struct CredentialJwtClaims<'credential, T = Object> where diff --git a/identity_credential/src/domain_linkage/domain_linkage_validator.rs b/identity_credential/src/domain_linkage/domain_linkage_validator.rs index 24969c1c65..be67c96832 100644 --- a/identity_credential/src/domain_linkage/domain_linkage_validator.rs +++ b/identity_credential/src/domain_linkage/domain_linkage_validator.rs @@ -38,15 +38,15 @@ impl JwtDomainLinkageValidator { /// Validates the linkage between a domain and a DID. /// [`DomainLinkageConfiguration`] is validated according to [DID Configuration Resource Verification](https://identity.foundation/.well-known/resources/did-configuration/#did-configuration-resource-verification). /// - /// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included - /// in the Domain Linkage Configuration. + /// * `issuer`: DID Document of the linked DID. Issuer of the Domain Linkage Credential included in the Domain Linkage + /// Configuration. /// * `configuration`: Domain Linkage Configuration fetched from the domain at "/.well-known/did-configuration.json". /// * `domain`: domain from which the Domain Linkage Configuration has been fetched. /// * `validation_options`: Further validation options to be applied on the Domain Linkage Credential. /// /// # Note: /// - Only the [JSON Web Token Proof Format](https://identity.foundation/.well-known/resources/did-configuration/#json-web-token-proof-format) - /// is supported. + /// is supported. /// - Only the Credential issued by `issuer` is verified. /// /// # Errors diff --git a/identity_credential/src/revocation/status_list_2021/credential.rs b/identity_credential/src/revocation/status_list_2021/credential.rs index 4402283e1a..ed96643cb7 100644 --- a/identity_credential/src/revocation/status_list_2021/credential.rs +++ b/identity_credential/src/revocation/status_list_2021/credential.rs @@ -123,7 +123,7 @@ impl StatusList2021Credential { /// /// ## Note: /// - A revoked credential cannot ever be unrevoked and will lead to a - /// [`StatusList2021CredentialError::UnreversibleRevocation`]. + /// [`StatusList2021CredentialError::UnreversibleRevocation`]. /// - Trying to set `revoked_or_suspended` to `false` for an already valid credential will have no impact. pub fn set_credential_status( &mut self, diff --git a/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs index a02b2cf56a..d6d97fcf6e 100644 --- a/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs +++ b/identity_credential/src/validator/jwt_presentation_validation/jwt_presentation_validator.rs @@ -47,11 +47,11 @@ where /// # Warning /// /// * This method does NOT validate the constituent credentials and therefore also not the relationship between the - /// credentials' subjects and the presentation holder. This can be done with - /// [`JwtCredentialValidationOptions`](crate::validator::JwtCredentialValidationOptions). + /// credentials' subjects and the presentation holder. This can be done with + /// [`JwtCredentialValidationOptions`](crate::validator::JwtCredentialValidationOptions). /// * The lack of an error returned from this method is in of itself not enough to conclude that the presentation can - /// be trusted. This section contains more information on additional checks that should be carried out before and - /// after calling this method. + /// be trusted. This section contains more information on additional checks that should be carried out before and + /// after calling this method. /// /// ## The state of the supplied DID Documents. /// diff --git a/identity_credential/src/validator/sd_jwt/validator.rs b/identity_credential/src/validator/sd_jwt/validator.rs index 0eedf13bf5..e01985fa01 100644 --- a/identity_credential/src/validator/sd_jwt/validator.rs +++ b/identity_credential/src/validator/sd_jwt/validator.rs @@ -53,10 +53,10 @@ impl SdJwtCredentialValidator { /// /// # Warning /// * The key binding JWT is not validated. If needed, it must be validated separately using - /// `SdJwtValidator::validate_key_binding_jwt`. + /// `SdJwtValidator::validate_key_binding_jwt`. /// * The lack of an error returned from this method is in of itself not enough to conclude that the credential can be - /// trusted. This section contains more information on additional checks that should be carried out before and after - /// calling this method. + /// trusted. This section contains more information on additional checks that should be carried out before and after + /// calling this method. /// /// ## The state of the issuer's DID Document /// The caller must ensure that `issuer` represents an up-to-date DID Document. diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 1b226f9585..2f6bcd593f 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -938,8 +938,8 @@ impl CoreDocument { /// Regardless of which options are passed the following conditions must be met in order for a verification attempt to /// take place. /// - The JWS must be encoded according to the JWS compact serialization. - /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document, - /// or set explicitly in the `options`. + /// - The `kid` value in the protected header must be an identifier of a verification method in this DID document, or + /// set explicitly in the `options`. // // NOTE: This is tested in `identity_storage` and `identity_credential`. pub fn verify_jws<'jws, T: JwsVerifier>( diff --git a/identity_jose/src/jwu/serde.rs b/identity_jose/src/jwu/serde.rs index cd80a1c949..e875da0e10 100644 --- a/identity_jose/src/jwu/serde.rs +++ b/identity_jose/src/jwu/serde.rs @@ -57,8 +57,7 @@ pub(crate) fn validate_jws_headers(protected: Option<&JwsHeader>, unprotected: O /// Validates that the "crit" parameter satisfies the following requirements: /// 1. It is integrity protected. /// 2. It is not encoded as an empty list. -/// 3. It does not contain any header parameters defined by the -/// JOSE JWS/JWA specifications. +/// 3. It does not contain any header parameters defined by the JOSE JWS/JWA specifications. /// 4. It's values are contained in the given `permitted` array. /// 5. All values in "crit" are present in at least one of the `protected` or `unprotected` headers. /// diff --git a/identity_jose/src/tests/rfc8037.rs b/identity_jose/src/tests/rfc8037.rs index aada7a7369..d83f22eb89 100644 --- a/identity_jose/src/tests/rfc8037.rs +++ b/identity_jose/src/tests/rfc8037.rs @@ -50,20 +50,18 @@ fn test_rfc8037_ed25519() { .and_then(|decoded| decoded.verify(&jws_verifier, &public)) .unwrap(); - #[cfg(feature = "eddsa")] - { - let jws_signature_verifier = JwsVerifierFn::from(|input: VerificationInput, key: &Jwk| match input.alg { - JwsAlgorithm::EdDSA => ed25519::verify(input, key), - other => unimplemented!("{other}"), - }); + let jws_signature_verifier = JwsVerifierFn::from(|input: VerificationInput, key: &Jwk| match input.alg { + JwsAlgorithm::EdDSA => ed25519::verify(input, key), + other => unimplemented!("{other}"), + }); + + let decoder = Decoder::new(); + let token_with_default = decoder + .decode_compact_serialization(jws.as_bytes(), None) + .and_then(|decoded| decoded.verify(&jws_signature_verifier, &public)) + .unwrap(); - let decoder = Decoder::new(); - let token_with_default = decoder - .decode_compact_serialization(jws.as_bytes(), None) - .and_then(|decoded| decoded.verify(&jws_signature_verifier, &public)) - .unwrap(); - assert_eq!(token, token_with_default); - } + assert_eq!(token, token_with_default); assert_eq!(token.protected, header); assert_eq!(token.claims, tv.payload.as_bytes()); } diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index b8ceffbc7f..eff86351be 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -301,10 +301,10 @@ mod iota_handler { /// /// # Note /// - /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all - /// previously added clients. - /// - This function does not validate the provided configuration. Ensure that the provided - /// network name corresponds with the client, possibly by using `client.network_name()`. + /// - Using `attach_iota_handler` or `attach_handler` for the IOTA method would override all previously added + /// clients. + /// - This function does not validate the provided configuration. Ensure that the provided network name corresponds + /// with the client, possibly by using `client.network_name()`. pub fn attach_multiple_iota_handlers(&mut self, clients: I) where CLI: IotaIdentityClientExt + Send + Sync + 'static, diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 08a0d68d7d..5b2ff0c8f6 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -21,11 +21,11 @@ identity_credential = { version = "=1.3.1", path = "../identity_credential", def identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } identity_iota_core = { version = "=1.3.1", path = "../identity_iota_core", default-features = false, optional = true } -identity_verification = { version = "=1.3.1", path = "../identity_verification", default_features = false } -iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } +identity_verification = { version = "=1.3.1", path = "../identity_verification", default-features = false } +iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "random"], optional = true } json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } -seahash = { version = "4.1.0", default_features = false } +seahash = { version = "4.1.0", default-features = false } serde.workspace = true serde_json.workspace = true thiserror.workspace = true @@ -47,7 +47,12 @@ send-sync-storage = [] # Implements the JwkStorageDocumentExt trait for IotaDocument iota-document = ["dep:identity_iota_core"] # Enables JSON Proof Token & BBS+ related features -jpt-bbs-plus = ["identity_credential/jpt-bbs-plus", "dep:zkryptium", "dep:bls12_381_plus", "dep:json-proof-token"] +jpt-bbs-plus = [ + "identity_credential/jpt-bbs-plus", + "dep:zkryptium", + "dep:bls12_381_plus", + "dep:json-proof-token", +] [lints] workspace = true diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index 56ae126bdc..10d58da369 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -14,22 +14,22 @@ description = "Secure JWK storage with Stronghold for IOTA Identity" [dependencies] async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } -identity_storage = { version = "=1.3.1", path = "../identity_storage", default_features = false } -identity_verification = { version = "=1.3.1", path = "../identity_verification", default_features = false } +identity_storage = { version = "=1.3.1", path = "../identity_storage", default-features = false } +identity_verification = { version = "=1.3.1", path = "../identity_verification", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } iota-sdk = { version = "1.1.5", default-features = false, features = ["client", "stronghold"] } iota_stronghold = { version = "2.1.0", default-features = false } json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"] } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"] } -zeroize = { version = "1.6.0", default_features = false } +zeroize = { version = "1.6.0", default-features = false } zkryptium = { workspace = true, optional = true } [dev-dependencies] anyhow = "1.0.82" bls12_381_plus = { workspace = true } -identity_did = { version = "=1.3.1", path = "../identity_did", default_features = false } -identity_storage = { version = "=1.3.1", path = "../identity_storage", default_features = false, features = ["jpt-bbs-plus"] } +identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } +identity_storage = { version = "=1.3.1", path = "../identity_storage", default-features = false, features = ["jpt-bbs-plus"] } json-proof-token = { workspace = true } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } zkryptium = { workspace = true } @@ -38,7 +38,12 @@ zkryptium = { workspace = true } default = [] # Enables `Send` + `Sync` bounds for the trait implementations on `StrongholdStorage`. send-sync-storage = ["identity_storage/send-sync-storage"] -bbs-plus = ["identity_storage/jpt-bbs-plus", "dep:zkryptium", "dep:bls12_381_plus", "dep:json-proof-token"] +bbs-plus = [ + "identity_storage/jpt-bbs-plus", + "dep:zkryptium", + "dep:bls12_381_plus", + "dep:json-proof-token", +] [lints] workspace = true From b355b47bd1fd796bfc4f978785f1206218a34aa8 Mon Sep 17 00:00:00 2001 From: wulfraem Date: Mon, 2 Sep 2024 16:22:49 +0200 Subject: [PATCH 2/5] Make `bls12_381_plus` dependency more flexible again (#1393) * update `bls12_381_plus` dependency - making version range more flexible again * fix clippy warning * fix clippy warning * fix clippy warnings * remove undefined feature check * bump depdendency version for `iota-crypto` * bump dependency version of bls12_381_plus in wasm bindings * update sandbox ci action to use 'docker compose' instead of 'docker-compose' * update step name to match latest updates --- .github/workflows/build-and-test.yml | 2 +- Cargo.toml | 2 +- bindings/wasm/Cargo.toml | 2 +- identity_credential/Cargo.toml | 2 +- identity_eddsa_verifier/Cargo.toml | 2 +- identity_iota_core/Cargo.toml | 2 +- identity_jose/Cargo.toml | 4 ++-- identity_jose/src/tests/rfc8037.rs | 5 +++-- identity_storage/Cargo.toml | 2 +- identity_stronghold/Cargo.toml | 2 +- 10 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 8919e81020..ba3b7ef0b1 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -126,7 +126,7 @@ jobs: run: cargo clean # Build the library, tests, and examples without running them to avoid recompilation in the run tests step - - name: Build with all features + - name: Build with default features run: cargo build --workspace --tests --examples --release - name: Start iota sandbox diff --git a/Cargo.toml b/Cargo.toml index 0b349651b5..7dfcbcadd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ exclude = ["bindings/wasm", "bindings/grpc"] [workspace.dependencies] -bls12_381_plus = { version = "=0.8.15" } +bls12_381_plus = { version = "0.8.17" } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } thiserror = { version = "1.0", default-features = false } strum = { version = "0.25", default-features = false, features = ["std", "derive"] } diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index 9e264b3b6d..1acaf0ce96 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -17,7 +17,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] async-trait = { version = "0.1", default-features = false } -bls12_381_plus = "=0.8.15" +bls12_381_plus = "0.8.17" console_error_panic_hook = { version = "0.1" } futures = { version = "0.3" } identity_eddsa_verifier = { path = "../../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 2a4b11d09c..aaba6c974e 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -39,7 +39,7 @@ zkryptium = { workspace = true, optional = true } [dev-dependencies] anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } -iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "std", "random"] } +iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "std", "random"] } proptest = { version = "1.4.0", default-features = false, features = ["std"] } tokio = { version = "1.35.0", default-features = false, features = ["rt-multi-thread", "macros"] } diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml index eedd1d652e..97308beebf 100644 --- a/identity_eddsa_verifier/Cargo.toml +++ b/identity_eddsa_verifier/Cargo.toml @@ -13,7 +13,7 @@ description = "JWS EdDSA signature verification for IOTA Identity" [dependencies] identity_jose = { version = "=1.3.1", path = "../identity_jose", default-features = false } -iota-crypto = { version = "0.23", default-features = false, features = ["std"] } +iota-crypto = { version = "0.23.2", default-features = false, features = ["std"] } [features] ed25519 = ["iota-crypto/ed25519"] diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 0ddb2bd6b9..f44a3ca27c 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -31,7 +31,7 @@ thiserror.workspace = true [dev-dependencies] anyhow = { version = "1.0.57" } -iota-crypto = { version = "0.23", default-features = false, features = ["bip39", "bip39-en"] } +iota-crypto = { version = "0.23.2", default-features = false, features = ["bip39", "bip39-en"] } proptest = { version = "1.0.0", default-features = false, features = ["std"] } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index 32d3824a9a..da8ddba3d2 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -14,7 +14,7 @@ description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] bls12_381_plus.workspace = true identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } -iota-crypto = { version = "0.23", default-features = false, features = ["std", "sha"] } +iota-crypto = { version = "0.23.2", default-features = false, features = ["std", "sha"] } json-proof-token.workspace = true serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } @@ -24,7 +24,7 @@ zeroize = { version = "1.6", default-features = false, features = ["std", "zeroi [dev-dependencies] anyhow = "1" -iota-crypto = { version = "0.23", features = ["ed25519", "random", "hmac"] } +iota-crypto = { version = "0.23.2", features = ["ed25519", "random", "hmac"] } p256 = { version = "0.12.0", default-features = false, features = ["std", "ecdsa", "ecdsa-core"] } signature = { version = "2", default-features = false } diff --git a/identity_jose/src/tests/rfc8037.rs b/identity_jose/src/tests/rfc8037.rs index d83f22eb89..27bb755979 100644 --- a/identity_jose/src/tests/rfc8037.rs +++ b/identity_jose/src/tests/rfc8037.rs @@ -50,6 +50,9 @@ fn test_rfc8037_ed25519() { .and_then(|decoded| decoded.verify(&jws_verifier, &public)) .unwrap(); + assert_eq!(token.protected, header); + assert_eq!(token.claims, tv.payload.as_bytes()); + let jws_signature_verifier = JwsVerifierFn::from(|input: VerificationInput, key: &Jwk| match input.alg { JwsAlgorithm::EdDSA => ed25519::verify(input, key), other => unimplemented!("{other}"), @@ -62,7 +65,5 @@ fn test_rfc8037_ed25519() { .unwrap(); assert_eq!(token, token_with_default); - assert_eq!(token.protected, header); - assert_eq!(token.claims, tv.payload.as_bytes()); } } diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 5b2ff0c8f6..fbbe93b346 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -22,7 +22,7 @@ identity_did = { version = "=1.3.1", path = "../identity_did", default-features identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } identity_iota_core = { version = "=1.3.1", path = "../identity_iota_core", default-features = false, optional = true } identity_verification = { version = "=1.3.1", path = "../identity_verification", default-features = false } -iota-crypto = { version = "0.23", default-features = false, features = ["ed25519", "random"], optional = true } +iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "random"], optional = true } json-proof-token = { workspace = true, optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default-features = false } diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index 10d58da369..d6b0825cba 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -16,7 +16,7 @@ async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } identity_storage = { version = "=1.3.1", path = "../identity_storage", default-features = false } identity_verification = { version = "=1.3.1", path = "../identity_verification", default-features = false } -iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } +iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519"] } iota-sdk = { version = "1.1.5", default-features = false, features = ["client", "stronghold"] } iota_stronghold = { version = "2.1.0", default-features = false } json-proof-token = { workspace = true, optional = true } From 84f1b7e133933f58504e387e84b7eae9a6298bff Mon Sep 17 00:00:00 2001 From: Frederik Rothenberger Date: Wed, 4 Sep 2024 10:02:55 +0200 Subject: [PATCH 3/5] Mark `js-sys` as optional for identity_core (#1405) * Mark `js-sys` as optional for identity_core In #1397 my intention was to make the `js-sys` dependency mutually exclusive with the feature `custom_time`. As it turns out, it's not that simple and mixing target specific dependencies with feature specific dependencies does not actually work. See [here](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) and [here](https://doc.rust-lang.org/cargo/reference/features.html#mutually-exclusive-features). So this removes the broken feature reference in the dependency declaration and instead marks it as `optional`. Also, the feature `custom_time` takes precedence over `js-sys` so these do not actually conflict and one _could_ enable both. * Make js-sys a default feature * Fix defaults switch * Don't expose `js-sys` feature Co-authored-by: Yasir --------- Co-authored-by: Yasir --- identity_core/Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index f8aa615b28..239383c2f6 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -21,8 +21,8 @@ time = { version = "0.3.23", default-features = false, features = ["std", "serde url = { version = "2.4", default-features = false, features = ["serde"] } zeroize = { version = "1.6", default-features = false } -[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi"), not(feature = "custom_time")))'.dependencies] -js-sys = { version = "0.3.55", default-features = false } +[target.'cfg(all(target_arch = "wasm32", not(target_os = "wasi")))'.dependencies] +js-sys = { version = "0.3.55", default-features = false, optional = true } [dev-dependencies] proptest = { version = "1.0.0" } @@ -39,6 +39,7 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [features] +default = ["dep:js-sys"] # Enables a macro to provide a custom time (Timestamp::now_utc) implementation, see src/custom_time.rs custom_time = [] From 02a08574a698bb2b571abe10d910608397d3943e Mon Sep 17 00:00:00 2001 From: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:46:38 +0200 Subject: [PATCH 4/5] Add support for `did:jwk` resolution (#1404) * did:jwk implementation & resolution * did:jwk WASM bindings * wasm did jwk test * cargo fmt * add missing license header * Update identity_did/src/did_jwk.rs Co-authored-by: wulfraem * Update identity_did/src/did_jwk.rs Co-authored-by: wulfraem --------- Co-authored-by: wulfraem --- bindings/wasm/docs/api-reference.md | 347 ++++++++++++------ .../examples/src/0_basic/2_resolve_did.ts | 27 +- bindings/wasm/src/did/did_jwk.rs | 105 ++++++ bindings/wasm/src/did/mod.rs | 2 + bindings/wasm/src/did/wasm_core_document.rs | 7 + identity_did/Cargo.toml | 1 + identity_did/src/did_jwk.rs | 123 +++++++ identity_did/src/lib.rs | 2 + .../src/document/core_document.rs | 47 +++ .../src/document/iota_document.rs | 9 + identity_resolver/src/resolution/resolver.rs | 28 ++ .../src/verification_method/method.rs | 9 + 12 files changed, 602 insertions(+), 105 deletions(-) create mode 100644 bindings/wasm/src/did/did_jwk.rs create mode 100644 identity_did/src/did_jwk.rs diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index db03dc07ec..6dd0837a69 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -14,6 +14,9 @@ if the object is being concurrently modified.

CustomMethodData

A custom verification method data format.

+
DIDJwk
+

did:jwk DID.

+
DIDUrl

A method agnostic DID Url.

@@ -254,29 +257,6 @@ working with storage backed DID documents.

PresentationProofAlgorithm
-
ProofAlgorithm
-
-
StatusCheck
-

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

-
-
Strict
-

Validate the status if supported, reject any unsupported -credentialStatus types.

-

Only RevocationBitmap2022 is currently supported.

-

This is the default.

-
-
SkipUnsupported
-

Validate the status if supported, skip any unsupported -credentialStatus types.

-
-
SkipAll
-

Skip all status checks.

-
-
SerializationType
-
-
MethodRelationship
-
SubjectHolderRelationship

Declares how credential subjects must relate to the presentation holder.

See also the Subject-Holder Relationship section of the specification.

@@ -291,11 +271,8 @@ This variant is the default.

Any

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

-
CredentialStatus
+
ProofAlgorithm
-
StatusPurpose
-

Purpose of a StatusList2021.

-
StateMetadataEncoding
FailFast
@@ -307,12 +284,6 @@ This variant is the default.

FirstError

Return after the first error occurs.

-
PayloadType
-
-
MethodRelationship
-
-
CredentialStatus
-
StatusCheck

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

@@ -330,11 +301,28 @@ This variant is the default.

SkipAll

Skip all status checks.

+
SerializationType
+
+
PayloadType
+
+
StatusPurpose
+

Purpose of a StatusList2021.

+
+
MethodRelationship
+
+
CredentialStatus
+
## Functions
+
encodeB64(data)string
+

Encode the given bytes in url-safe base64.

+
+
decodeB64(data)Uint8Array
+

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

+
verifyEd25519(alg, signingInput, decodedSignature, publicKey)

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

This function is useful when one is composing a IJwsVerifier that delegates @@ -346,15 +334,6 @@ prior to calling the function.

start()

Initializes the console error panic hook for better error messages

-
encodeB64(data)string
-

Encode the given bytes in url-safe base64.

-
-
decodeB64(data)Uint8Array
-

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

-
-
start()
-

Initializes the console error panic hook for better error messages

-
@@ -592,6 +571,7 @@ if the object is being concurrently modified. * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#CoreDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) * _static_ * [.fromJSON(json)](#CoreDocument.fromJSON) ⇒ [CoreDocument](#CoreDocument) + * [.expandDIDJwk(did)](#CoreDocument.expandDIDJwk) ⇒ [CoreDocument](#CoreDocument) @@ -1030,6 +1010,17 @@ Deserializes an instance from a plain JS representation. | --- | --- | | json | any | + + +### CoreDocument.expandDIDJwk(did) ⇒ [CoreDocument](#CoreDocument) +Creates a [CoreDocument](#CoreDocument) from the given [DIDJwk](#DIDJwk). + +**Kind**: static method of [CoreDocument](#CoreDocument) + +| Param | Type | +| --- | --- | +| did | [DIDJwk](#DIDJwk) | + ## Credential @@ -1282,6 +1273,136 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## DIDJwk +`did:jwk` DID. + +**Kind**: global class + +* [DIDJwk](#DIDJwk) + * [new DIDJwk(did)](#new_DIDJwk_new) + * _instance_ + * [.jwk()](#DIDJwk+jwk) ⇒ [Jwk](#Jwk) + * [.scheme()](#DIDJwk+scheme) ⇒ string + * [.authority()](#DIDJwk+authority) ⇒ string + * [.method()](#DIDJwk+method) ⇒ string + * [.methodId()](#DIDJwk+methodId) ⇒ string + * [.toString()](#DIDJwk+toString) ⇒ string + * [.toCoreDid()](#DIDJwk+toCoreDid) ⇒ [CoreDID](#CoreDID) + * [.toJSON()](#DIDJwk+toJSON) ⇒ any + * [.clone()](#DIDJwk+clone) ⇒ [DIDJwk](#DIDJwk) + * _static_ + * [.parse(input)](#DIDJwk.parse) ⇒ [DIDJwk](#DIDJwk) + * [.fromJSON(json)](#DIDJwk.fromJSON) ⇒ [DIDJwk](#DIDJwk) + + + +### new DIDJwk(did) +Creates a new [DIDJwk](#DIDJwk) from a [CoreDID](#CoreDID). + +### Errors +Throws an error if the given did is not a valid `did:jwk` DID. + + +| Param | Type | +| --- | --- | +| did | [CoreDID](#CoreDID) \| IToCoreDID | + + + +### didJwk.jwk() ⇒ [Jwk](#Jwk) +Returns the JSON WEB KEY (JWK) encoded inside this `did:jwk`. + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.scheme() ⇒ string +Returns the [CoreDID](#CoreDID) scheme. + +E.g. +- `"did:example:12345678" -> "did"` +- `"did:iota:smr:12345678" -> "did"` + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.authority() ⇒ string +Returns the [CoreDID](#CoreDID) authority: the method name and method-id. + +E.g. +- `"did:example:12345678" -> "example:12345678"` +- `"did:iota:smr:12345678" -> "iota:smr:12345678"` + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.method() ⇒ string +Returns the [CoreDID](#CoreDID) method name. + +E.g. +- `"did:example:12345678" -> "example"` +- `"did:iota:smr:12345678" -> "iota"` + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.methodId() ⇒ string +Returns the [CoreDID](#CoreDID) method-specific ID. + +E.g. +- `"did:example:12345678" -> "12345678"` +- `"did:iota:smr:12345678" -> "smr:12345678"` + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.toString() ⇒ string +Returns the [CoreDID](#CoreDID) as a string. + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.toCoreDid() ⇒ [CoreDID](#CoreDID) +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### didJwk.clone() ⇒ [DIDJwk](#DIDJwk) +Deep clones the object. + +**Kind**: instance method of [DIDJwk](#DIDJwk) + + +### DIDJwk.parse(input) ⇒ [DIDJwk](#DIDJwk) +Parses a [DIDJwk](#DIDJwk) from the given `input`. + +### Errors + +Throws an error if the input is not a valid [DIDJwk](#DIDJwk). + +**Kind**: static method of [DIDJwk](#DIDJwk) + +| Param | Type | +| --- | --- | +| input | string | + + + +### DIDJwk.fromJSON(json) ⇒ [DIDJwk](#DIDJwk) +Deserializes an instance from a JSON object. + +**Kind**: static method of [DIDJwk](#DIDJwk) + +| Param | Type | +| --- | --- | +| json | any | + ## DIDUrl @@ -3224,7 +3345,7 @@ Utility functions for validating JPT credentials. ### JptCredentialValidatorUtils.extractIssuer(credential) ⇒ [CoreDID](#CoreDID) -Utility for extracting the issuer field of a [`Credential`](`Credential`) as a DID. +Utility for extracting the issuer field of a [Credential](#Credential) as a DID. # Errors Fails if the issuer field is not a valid DID. @@ -5450,7 +5571,8 @@ Supported verification method types. * _static_ * [.Ed25519VerificationKey2018()](#MethodType.Ed25519VerificationKey2018) ⇒ [MethodType](#MethodType) * [.X25519KeyAgreementKey2019()](#MethodType.X25519KeyAgreementKey2019) ⇒ [MethodType](#MethodType) - * [.JsonWebKey()](#MethodType.JsonWebKey) ⇒ [MethodType](#MethodType) + * ~~[.JsonWebKey()](#MethodType.JsonWebKey)~~ + * [.JsonWebKey2020()](#MethodType.JsonWebKey2020) ⇒ [MethodType](#MethodType) * [.custom(type_)](#MethodType.custom) ⇒ [MethodType](#MethodType) * [.fromJSON(json)](#MethodType.fromJSON) ⇒ [MethodType](#MethodType) @@ -5482,7 +5604,13 @@ Deep clones the object. **Kind**: static method of [MethodType](#MethodType) -### MethodType.JsonWebKey() ⇒ [MethodType](#MethodType) +### ~~MethodType.JsonWebKey()~~ +***Deprecated*** + +**Kind**: static method of [MethodType](#MethodType) + + +### MethodType.JsonWebKey2020() ⇒ [MethodType](#MethodType) A verification method for use with JWT verification as prescribed by the [Jwk](#Jwk) in the `publicKeyJwk` entry. @@ -7529,46 +7657,9 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + -**Kind**: global variable - - -## StatusCheck -Controls validation behaviour when checking whether or not a credential has been revoked by its -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). - -**Kind**: global variable - - -## Strict -Validate the status if supported, reject any unsupported -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. - -Only `RevocationBitmap2022` is currently supported. - -This is the default. - -**Kind**: global variable - - -## SkipUnsupported -Validate the status if supported, skip any unsupported -[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. - -**Kind**: global variable - - -## SkipAll -Skip all status checks. - -**Kind**: global variable - - -## SerializationType -**Kind**: global variable - - -## MethodRelationship +## PresentationProofAlgorithm **Kind**: global variable @@ -7596,7 +7687,10 @@ The holder must match the subject only for credentials where the [`nonTransferab ## Any The holder is not required to have any kind of relationship to any credential subject. -## StateMetadataEncoding +**Kind**: global variable + + +## ProofAlgorithm **Kind**: global variable @@ -7620,36 +7714,59 @@ Return all errors that occur during validation. Return after the first error occurs. **Kind**: global variable + + +## StatusCheck +Controls validation behaviour when checking whether or not a credential has been revoked by its +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status). **Kind**: global variable - + -## verifyEd25519(alg, signingInput, decodedSignature, publicKey) -Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`. +## Strict +Validate the status if supported, reject any unsupported +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. -This function is useful when one is composing a `IJwsVerifier` that delegates -`EdDSA` verification with curve `Ed25519` to this function. +Only `RevocationBitmap2022` is currently supported. -# Warning +This is the default. -This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this -prior to calling the function. +**Kind**: global variable + -**Kind**: global function +## SkipUnsupported +Validate the status if supported, skip any unsupported +[`credentialStatus`](https://www.w3.org/TR/vc-data-model/#status) types. -| Param | Type | -| --- | --- | -| alg | JwsAlgorithm | -| signingInput | Uint8Array | -| decodedSignature | Uint8Array | -| publicKey | [Jwk](#Jwk) | +**Kind**: global variable + - +## SkipAll +Skip all status checks. -## start() -Initializes the console error panic hook for better error messages +**Kind**: global variable + -**Kind**: global function +## SerializationType +**Kind**: global variable + + +## PayloadType +**Kind**: global variable + + +## StatusPurpose +Purpose of a [StatusList2021](#StatusList2021). + +**Kind**: global variable + + +## MethodRelationship +**Kind**: global variable + + +## CredentialStatus +**Kind**: global variable ## encodeB64(data) ⇒ string @@ -7672,6 +7789,28 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | + + +## verifyEd25519(alg, signingInput, decodedSignature, publicKey) +Verify a JWS signature secured with the `EdDSA` algorithm and curve `Ed25519`. + +This function is useful when one is composing a `IJwsVerifier` that delegates +`EdDSA` verification with curve `Ed25519` to this function. + +# Warning + +This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this +prior to calling the function. + +**Kind**: global function + +| Param | Type | +| --- | --- | +| alg | JwsAlgorithm | +| signingInput | Uint8Array | +| decodedSignature | Uint8Array | +| publicKey | [Jwk](#Jwk) | + ## start() diff --git a/bindings/wasm/examples/src/0_basic/2_resolve_did.ts b/bindings/wasm/examples/src/0_basic/2_resolve_did.ts index 58bc205b6a..ce8ea7c3e1 100644 --- a/bindings/wasm/examples/src/0_basic/2_resolve_did.ts +++ b/bindings/wasm/examples/src/0_basic/2_resolve_did.ts @@ -1,10 +1,23 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { IotaDocument, IotaIdentityClient, JwkMemStore, KeyIdMemStore, Storage } from "@iota/identity-wasm/node"; +import { + CoreDocument, + DIDJwk, + IotaDocument, + IotaIdentityClient, + IToCoreDocument, + JwkMemStore, + KeyIdMemStore, + Resolver, + Storage, +} from "@iota/identity-wasm/node"; import { AliasOutput, Client, MnemonicSecretManager, Utils } from "@iota/sdk-wasm/node"; import { API_ENDPOINT, createDid } from "../util"; +const DID_JWK: string = + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"; + /** Demonstrates how to resolve an existing DID in an Alias Output. */ export async function resolveIdentity() { const client = new Client({ @@ -34,4 +47,16 @@ export async function resolveIdentity() { // We can also resolve the Alias Output directly. const aliasOutput: AliasOutput = await didClient.resolveDidOutput(did); console.log("The Alias Output holds " + aliasOutput.getAmount() + " tokens"); + + // did:jwk can be resolved as well. + const handlers = new Map Promise>(); + handlers.set("jwk", didJwkHandler); + const resolver = new Resolver({ handlers }); + const did_jwk_resolved_doc = await resolver.resolve(DID_JWK); + console.log(`DID ${DID_JWK} resolves to:\n ${JSON.stringify(did_jwk_resolved_doc, null, 2)}`); } + +const didJwkHandler = async (did: string) => { + let did_jwk = DIDJwk.parse(did); + return CoreDocument.expandDIDJwk(did_jwk); +}; diff --git a/bindings/wasm/src/did/did_jwk.rs b/bindings/wasm/src/did/did_jwk.rs new file mode 100644 index 0000000000..15ce291eca --- /dev/null +++ b/bindings/wasm/src/did/did_jwk.rs @@ -0,0 +1,105 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::did::DIDJwk; +use identity_iota::did::DID as _; +use wasm_bindgen::prelude::*; + +use super::wasm_core_did::get_core_did_clone; +use super::IToCoreDID; +use super::WasmCoreDID; +use crate::error::Result; +use crate::error::WasmResult; +use crate::jose::WasmJwk; + +/// `did:jwk` DID. +#[wasm_bindgen(js_name = DIDJwk)] +pub struct WasmDIDJwk(pub(crate) DIDJwk); + +#[wasm_bindgen(js_class = DIDJwk)] +impl WasmDIDJwk { + #[wasm_bindgen(constructor)] + /// Creates a new {@link DIDJwk} from a {@link CoreDID}. + /// + /// ### Errors + /// Throws an error if the given did is not a valid `did:jwk` DID. + pub fn new(did: IToCoreDID) -> Result { + let did = get_core_did_clone(&did).0; + DIDJwk::try_from(did).wasm_result().map(Self) + } + /// Parses a {@link DIDJwk} from the given `input`. + /// + /// ### Errors + /// + /// Throws an error if the input is not a valid {@link DIDJwk}. + #[wasm_bindgen] + pub fn parse(input: &str) -> Result { + DIDJwk::parse(input).wasm_result().map(Self) + } + + /// Returns the JSON WEB KEY (JWK) encoded inside this `did:jwk`. + #[wasm_bindgen] + pub fn jwk(&self) -> WasmJwk { + self.0.jwk().into() + } + + // =========================================================================== + // DID trait + // =========================================================================== + + /// Returns the {@link CoreDID} scheme. + /// + /// E.g. + /// - `"did:example:12345678" -> "did"` + /// - `"did:iota:smr:12345678" -> "did"` + #[wasm_bindgen] + pub fn scheme(&self) -> String { + self.0.scheme().to_owned() + } + + /// Returns the {@link CoreDID} authority: the method name and method-id. + /// + /// E.g. + /// - `"did:example:12345678" -> "example:12345678"` + /// - `"did:iota:smr:12345678" -> "iota:smr:12345678"` + #[wasm_bindgen] + pub fn authority(&self) -> String { + self.0.authority().to_owned() + } + + /// Returns the {@link CoreDID} method name. + /// + /// E.g. + /// - `"did:example:12345678" -> "example"` + /// - `"did:iota:smr:12345678" -> "iota"` + #[wasm_bindgen] + pub fn method(&self) -> String { + self.0.method().to_owned() + } + + /// Returns the {@link CoreDID} method-specific ID. + /// + /// E.g. + /// - `"did:example:12345678" -> "12345678"` + /// - `"did:iota:smr:12345678" -> "smr:12345678"` + #[wasm_bindgen(js_name = methodId)] + pub fn method_id(&self) -> String { + self.0.method_id().to_owned() + } + + /// Returns the {@link CoreDID} as a string. + #[allow(clippy::inherent_to_string)] + #[wasm_bindgen(js_name = toString)] + pub fn to_string(&self) -> String { + self.0.to_string() + } + + // Only intended to be called internally. + #[wasm_bindgen(js_name = toCoreDid, skip_typescript)] + pub fn to_core_did(&self) -> WasmCoreDID { + WasmCoreDID(self.0.clone().into()) + } +} + +impl_wasm_json!(WasmDIDJwk, DIDJwk); +impl_wasm_clone!(WasmDIDJwk, DIDJwk); diff --git a/bindings/wasm/src/did/mod.rs b/bindings/wasm/src/did/mod.rs index b89db3edbf..ae2e89bc0c 100644 --- a/bindings/wasm/src/did/mod.rs +++ b/bindings/wasm/src/did/mod.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod did_jwk; mod jws_verification_options; mod service; mod wasm_core_did; @@ -19,5 +20,6 @@ pub use self::wasm_core_document::PromiseJws; pub use self::wasm_core_document::PromiseJwt; pub use self::wasm_core_document::WasmCoreDocument; pub use self::wasm_did_url::WasmDIDUrl; +pub use did_jwk::*; pub use self::jws_verification_options::*; diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 0fe08e6675..2a7d896ac8 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -24,6 +24,7 @@ use crate::credential::WasmJwt; use crate::credential::WasmPresentation; use crate::did::service::WasmService; use crate::did::wasm_did_url::WasmDIDUrl; +use crate::did::WasmDIDJwk; use crate::error::Result; use crate::error::WasmResult; use crate::jose::WasmDecodedJws; @@ -765,6 +766,12 @@ impl WasmCoreDocument { }); Ok(promise.unchecked_into()) } + + /// Creates a {@link CoreDocument} from the given {@link DIDJwk}. + #[wasm_bindgen(js_name = expandDIDJwk)] + pub fn expand_did_jwk(did: WasmDIDJwk) -> Result { + CoreDocument::expand_did_jwk(did.0).wasm_result().map(Self::from) + } } #[wasm_bindgen] diff --git a/identity_did/Cargo.toml b/identity_did/Cargo.toml index 5b4e85069c..18b32330ca 100644 --- a/identity_did/Cargo.toml +++ b/identity_did/Cargo.toml @@ -14,6 +14,7 @@ description = "Agnostic implementation of the Decentralized Identifiers (DID) st did_url_parser = { version = "0.2.0", features = ["std", "serde"] } form_urlencoded = { version = "1.2.0", default-features = false, features = ["alloc"] } identity_core = { version = "=1.3.1", path = "../identity_core" } +identity_jose = { version = "=1.3.1", path = "../identity_jose" } serde.workspace = true strum.workspace = true thiserror.workspace = true diff --git a/identity_did/src/did_jwk.rs b/identity_did/src/did_jwk.rs new file mode 100644 index 0000000000..5ebd61021c --- /dev/null +++ b/identity_did/src/did_jwk.rs @@ -0,0 +1,123 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Debug; +use std::fmt::Display; +use std::str::FromStr; + +use identity_jose::jwk::Jwk; +use identity_jose::jwu::decode_b64_json; + +use crate::CoreDID; +use crate::Error; +use crate::DID; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize)] +#[repr(transparent)] +#[serde(into = "CoreDID", try_from = "CoreDID")] +/// A type representing a `did:jwk` DID. +pub struct DIDJwk(CoreDID); + +impl DIDJwk { + /// [`DIDJwk`]'s method. + pub const METHOD: &'static str = "jwk"; + + /// Tries to parse a [`DIDJwk`] from a string. + pub fn parse(s: &str) -> Result { + s.parse() + } + + /// Returns the JWK encoded inside this did:jwk. + pub fn jwk(&self) -> Jwk { + decode_b64_json(self.method_id()).expect("did:jwk encodes a valid JWK") + } +} + +impl AsRef for DIDJwk { + fn as_ref(&self) -> &CoreDID { + &self.0 + } +} + +impl From for CoreDID { + fn from(value: DIDJwk) -> Self { + value.0 + } +} + +impl<'a> TryFrom<&'a str> for DIDJwk { + type Error = Error; + fn try_from(value: &'a str) -> Result { + value.parse() + } +} + +impl Display for DIDJwk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for DIDJwk { + type Err = Error; + fn from_str(s: &str) -> Result { + s.parse::().and_then(TryFrom::try_from) + } +} + +impl From for String { + fn from(value: DIDJwk) -> Self { + value.to_string() + } +} + +impl TryFrom for DIDJwk { + type Error = Error; + fn try_from(value: CoreDID) -> Result { + let Self::METHOD = value.method() else { + return Err(Error::InvalidMethodName); + }; + decode_b64_json::(value.method_id()) + .map(|_| Self(value)) + .map_err(|_| Error::InvalidMethodId) + } +} + +#[cfg(test)] +mod tests { + use identity_core::convert::FromJson; + + use super::*; + + #[test] + fn test_valid_deserialization() -> Result<(), Error> { + "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::()?; + "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9".parse::()?; + + Ok(()) + } + + #[test] + fn test_jwk() { + let did = DIDJwk::parse("did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9").unwrap(); + let target_jwk = Jwk::from_json_value(serde_json::json!({ + "kty":"OKP","crv":"X25519","use":"enc","x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" + })) + .unwrap(); + + assert_eq!(did.jwk(), target_jwk); + } + + #[test] + fn test_invalid_deserialization() { + assert!( + "did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a" + .parse::() + .is_err() + ); + assert!("did:jwk:".parse::().is_err()); + assert!("did:jwk:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + .parse::() + .is_err()); + } +} diff --git a/identity_did/src/lib.rs b/identity_did/src/lib.rs index 9289419211..62c846847e 100644 --- a/identity_did/src/lib.rs +++ b/identity_did/src/lib.rs @@ -18,6 +18,7 @@ #[allow(clippy::module_inception)] mod did; +mod did_jwk; mod did_url; mod error; @@ -26,4 +27,5 @@ pub use crate::did_url::RelativeDIDUrl; pub use ::did_url_parser::DID as BaseDIDUrl; pub use did::CoreDID; pub use did::DID; +pub use did_jwk::*; pub use error::Error; diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 2f6bcd593f..2747f7fae6 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -7,6 +7,7 @@ use core::fmt::Formatter; use std::collections::HashMap; use std::convert::Infallible; +use identity_did::DIDJwk; use identity_verification::jose::jwk::Jwk; use identity_verification::jose::jws::DecodedJws; use identity_verification::jose::jws::Decoder; @@ -984,6 +985,23 @@ impl CoreDocument { } } +impl CoreDocument { + /// Creates a [`CoreDocument`] from a did:jwk DID. + pub fn expand_did_jwk(did_jwk: DIDJwk) -> Result { + let verification_method = VerificationMethod::try_from(did_jwk.clone()).map_err(Error::InvalidKeyMaterial)?; + let verification_method_id = verification_method.id().clone(); + + DocumentBuilder::default() + .id(did_jwk.into()) + .verification_method(verification_method) + .assertion_method(verification_method_id.clone()) + .authentication(verification_method_id.clone()) + .capability_invocation(verification_method_id.clone()) + .capability_delegation(verification_method_id.clone()) + .build() + } +} + #[cfg(test)] mod tests { use identity_core::convert::FromJson; @@ -1682,4 +1700,33 @@ mod tests { verifier(json); } } + + #[test] + fn test_did_jwk_expansion() { + let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9" + .parse::() + .unwrap(); + let target_doc = serde_json::from_value(serde_json::json!({ + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "verificationMethod": [ + { + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0", + "type": "JsonWebKey2020", + "controller": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "publicKeyJwk": { + "kty":"OKP", + "crv":"X25519", + "use":"enc", + "x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" + } + } + ], + "assertionMethod": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"], + "authentication": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"], + "capabilityInvocation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"], + "capabilityDelegation": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"] + })).unwrap(); + + assert_eq!(CoreDocument::expand_did_jwk(did_jwk).unwrap(), target_doc); + } } diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 7ae60381d7..bd3404045c 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -555,6 +555,15 @@ impl From for CoreDocument { } } +impl From for IotaDocument { + fn from(value: CoreDocument) -> Self { + IotaDocument { + document: value, + metadata: IotaDocumentMetadata::default(), + } + } +} + impl TryFrom<(CoreDocument, IotaDocumentMetadata)> for IotaDocument { type Error = Error; /// Converts the tuple into an [`IotaDocument`] if the given [`CoreDocument`] has an identifier satisfying the diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs index eff86351be..228a65582b 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -4,6 +4,7 @@ use core::future::Future; use futures::stream::FuturesUnordered; use futures::TryStreamExt; +use identity_did::DIDJwk; use identity_did::DID; use std::collections::HashSet; @@ -247,6 +248,22 @@ impl Resolver> { } } +impl + 'static> Resolver> { + /// Attaches a handler capable of resolving `did:jwk` DIDs. + pub fn attach_did_jwk_handler(&mut self) { + let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) }; + self.attach_handler(DIDJwk::METHOD.to_string(), handler) + } +} + +impl + 'static> Resolver> { + /// Attaches a handler capable of resolving `did:jwk` DIDs. + pub fn attach_did_jwk_handler(&mut self) { + let handler = |did_jwk: DIDJwk| async move { CoreDocument::expand_did_jwk(did_jwk) }; + self.attach_handler(DIDJwk::METHOD.to_string(), handler) + } +} + #[cfg(feature = "iota")] mod iota_handler { use crate::ErrorCause; @@ -414,4 +431,15 @@ mod tests { let doc = resolver.resolve(&did2).await.unwrap(); assert_eq!(doc.id(), &did2); } + + #[tokio::test] + async fn test_did_jwk_resolution() { + let mut resolver = Resolver::::new(); + resolver.attach_did_jwk_handler(); + + let did_jwk = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".parse::().unwrap(); + + let doc = resolver.resolve(&did_jwk).await.unwrap(); + assert_eq!(doc.id(), did_jwk.as_ref()); + } } diff --git a/identity_verification/src/verification_method/method.rs b/identity_verification/src/verification_method/method.rs index 65c838639f..084956c3a9 100644 --- a/identity_verification/src/verification_method/method.rs +++ b/identity_verification/src/verification_method/method.rs @@ -5,6 +5,7 @@ use core::fmt::Display; use core::fmt::Formatter; use std::borrow::Cow; +use identity_did::DIDJwk; use identity_jose::jwk::Jwk; use serde::de; use serde::Deserialize; @@ -247,6 +248,14 @@ impl KeyComparable for VerificationMethod { } } +impl TryFrom for VerificationMethod { + type Error = Error; + fn try_from(did: DIDJwk) -> Result { + let jwk = did.jwk(); + Self::new_from_jwk(did, jwk, Some("0")) + } +} + // Horrible workaround for a tracked serde issue https://github.com/serde-rs/serde/issues/2200. Serde doesn't "consume" // the input when deserializing flattened enums (MethodData in this case) causing duplication of data (in this case // it ends up in the properties object). This workaround simply removes the duplication. From deecc7ec76bda4aa5c92fe1b502799662d1fbc73 Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Thu, 5 Sep 2024 11:47:12 +0200 Subject: [PATCH 5/5] Linked Verifiable Presentations (#1398) * feat: implement `Service` for Linked Verifiable Presentations * feat: add example for Linked Verifiable Presentations * cargo clippy, fmt, code * cargo clippy + fmt * fix linked vp example * wasm bindings * Update bindings/wasm/src/credential/linked_verifiable_presentation_service.rs Co-authored-by: wulfraem * cargo fmt --------- Co-authored-by: Enrico Marconi Co-authored-by: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Co-authored-by: wulfraem --- .../linked_verifiable_presentation_service.rs | 109 +++++++++ bindings/wasm/src/credential/mod.rs | 2 + .../11_linked_verifiable_presentation.rs | 195 +++++++++++++++++ examples/Cargo.toml | 6 +- examples/README.md | 26 +-- .../linked_verifiable_presentation_service.rs | 207 ++++++++++++++++++ identity_credential/src/credential/mod.rs | 2 + identity_credential/src/error.rs | 3 + 8 files changed, 536 insertions(+), 14 deletions(-) create mode 100644 bindings/wasm/src/credential/linked_verifiable_presentation_service.rs create mode 100644 examples/1_advanced/11_linked_verifiable_presentation.rs create mode 100644 identity_credential/src/credential/linked_verifiable_presentation_service.rs diff --git a/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs b/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs new file mode 100644 index 0000000000..1033316cc7 --- /dev/null +++ b/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs @@ -0,0 +1,109 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::common::ArrayString; +use crate::did::WasmService; +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::core::Object; +use identity_iota::core::OneOrSet; +use identity_iota::core::Url; +use identity_iota::credential::LinkedVerifiablePresentationService; +use identity_iota::did::DIDUrl; +use identity_iota::document::Service; +use proc_typescript::typescript; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +#[wasm_bindgen(js_name = LinkedVerifiablePresentationService, inspectable)] +pub struct WasmLinkedVerifiablePresentationService(LinkedVerifiablePresentationService); + +/// A service wrapper for a [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). +#[wasm_bindgen(js_class = LinkedVerifiablePresentationService)] +impl WasmLinkedVerifiablePresentationService { + /// Constructs a new {@link LinkedVerifiablePresentationService} that wraps a spec compliant [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). + #[wasm_bindgen(constructor)] + pub fn new(options: ILinkedVerifiablePresentationService) -> Result { + let ILinkedVerifiablePresentationServiceHelper { + id, + linked_vp, + properties, + } = options + .into_serde::() + .wasm_result()?; + Ok(Self( + LinkedVerifiablePresentationService::new(id, linked_vp, properties).wasm_result()?, + )) + } + + /// Returns the domains contained in the Linked Verifiable Presentation Service. + #[wasm_bindgen(js_name = verifiablePresentationUrls)] + pub fn vp_urls(&self) -> ArrayString { + self + .0 + .verifiable_presentation_urls() + .iter() + .map(|url| url.to_string()) + .map(JsValue::from) + .collect::() + .unchecked_into::() + } + + /// Returns the inner service which can be added to a DID Document. + #[wasm_bindgen(js_name = toService)] + pub fn to_service(&self) -> WasmService { + let service: Service = self.0.clone().into(); + WasmService(service) + } + + /// Creates a new {@link LinkedVerifiablePresentationService} from a {@link Service}. + /// + /// # Error + /// + /// Errors if `service` is not a valid Linked Verifiable Presentation Service. + #[wasm_bindgen(js_name = fromService)] + pub fn from_service(service: &WasmService) -> Result { + Ok(Self( + LinkedVerifiablePresentationService::try_from(service.0.clone()).wasm_result()?, + )) + } + + /// Returns `true` if a {@link Service} is a valid Linked Verifiable Presentation Service. + #[wasm_bindgen(js_name = isValid)] + pub fn is_valid(service: &WasmService) -> bool { + LinkedVerifiablePresentationService::check_structure(&service.0).is_ok() + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "ILinkedVerifiablePresentationService")] + pub type ILinkedVerifiablePresentationService; +} + +/// Fields for constructing a new {@link LinkedVerifiablePresentationService}. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[typescript(name = "ILinkedVerifiablePresentationService", readonly, optional)] +struct ILinkedVerifiablePresentationServiceHelper { + /// Service id. + #[typescript(optional = false, type = "DIDUrl")] + id: DIDUrl, + /// A unique URI that may be used to identify the {@link Credential}. + #[typescript(optional = false, type = "string | string[]")] + linked_vp: OneOrSet, + /// Miscellaneous properties. + #[serde(flatten)] + #[typescript(optional = false, name = "[properties: string]", type = "unknown")] + properties: Object, +} + +impl_wasm_clone!( + WasmLinkedVerifiablePresentationService, + LinkedVerifiablePresentationService +); +impl_wasm_json!( + WasmLinkedVerifiablePresentationService, + LinkedVerifiablePresentationService +); diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 033a8cefd6..408f302f11 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -13,6 +13,7 @@ pub use self::jws::WasmJws; pub use self::jwt::WasmJwt; pub use self::jwt_credential_validation::*; pub use self::jwt_presentation_validation::*; +pub use self::linked_verifiable_presentation_service::*; pub use self::options::WasmFailFast; pub use self::options::WasmSubjectHolderRelationship; pub use self::presentation::*; @@ -33,6 +34,7 @@ mod jwt; mod jwt_credential_validation; mod jwt_presentation_validation; mod linked_domain_service; +mod linked_verifiable_presentation_service; mod options; mod presentation; mod proof; diff --git a/examples/1_advanced/11_linked_verifiable_presentation.rs b/examples/1_advanced/11_linked_verifiable_presentation.rs new file mode 100644 index 0000000000..550bad3d41 --- /dev/null +++ b/examples/1_advanced/11_linked_verifiable_presentation.rs @@ -0,0 +1,195 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Context; +use examples::create_did; +use examples::random_stronghold_path; +use examples::MemStorage; +use examples::API_ENDPOINT; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::core::FromJson; +use identity_iota::core::Object; +use identity_iota::core::OrderedSet; +use identity_iota::core::Url; +use identity_iota::credential::CompoundJwtPresentationValidationError; +use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::DecodedJwtPresentation; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtPresentationOptions; +use identity_iota::credential::JwtPresentationValidationOptions; +use identity_iota::credential::JwtPresentationValidator; +use identity_iota::credential::JwtPresentationValidatorUtils; +use identity_iota::credential::LinkedVerifiablePresentationService; +use identity_iota::credential::PresentationBuilder; +use identity_iota::credential::Subject; +use identity_iota::did::CoreDID; +use identity_iota::did::DIDUrl; +use identity_iota::did::DID; +use identity_iota::document::verifiable::JwsVerificationOptions; +use identity_iota::iota::IotaClientExt; +use identity_iota::iota::IotaDID; +use identity_iota::iota::IotaDocument; +use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::resolver::Resolver; +use identity_iota::storage::JwkDocumentExt; +use identity_iota::storage::JwkMemStore; +use identity_iota::storage::JwsSignatureOptions; +use identity_iota::storage::KeyIdMemstore; +use iota_sdk::client::secret::stronghold::StrongholdSecretManager; +use iota_sdk::client::secret::SecretManager; +use iota_sdk::client::Client; +use iota_sdk::client::Password; +use iota_sdk::types::block::address::Address; +use iota_sdk::types::block::output::AliasOutput; +use iota_sdk::types::block::output::AliasOutputBuilder; +use iota_sdk::types::block::output::RentStructure; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Create a new client to interact with the IOTA ledger. + let client: Client = Client::builder() + .with_primary_node(API_ENDPOINT, None)? + .finish() + .await?; + let stronghold_path = random_stronghold_path(); + + println!("Using stronghold path: {stronghold_path:?}"); + // Create a new secret manager backed by a Stronghold. + let mut secret_manager: SecretManager = SecretManager::Stronghold( + StrongholdSecretManager::builder() + .password(Password::from("secure_password".to_owned())) + .build(stronghold_path)?, + ); + + // Create a DID for the entity that will be the holder of the Verifiable Presentation. + let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let (_, mut did_document, fragment): (Address, IotaDocument, String) = + create_did(&client, &mut secret_manager, &storage).await?; + let did: IotaDID = did_document.id().clone(); + + // ===================================================== + // Create Linked Verifiable Presentation service + // ===================================================== + + // The DID should link to the following VPs. + let verifiable_presentation_url_1: Url = Url::parse("https://foo.example.com/verifiable-presentation.jwt")?; + let verifiable_presentation_url_2: Url = Url::parse("https://bar.example.com/verifiable-presentation.jsonld")?; + + let mut verifiable_presentation_urls: OrderedSet = OrderedSet::new(); + verifiable_presentation_urls.append(verifiable_presentation_url_1.clone()); + verifiable_presentation_urls.append(verifiable_presentation_url_2.clone()); + + // Create a Linked Verifiable Presentation Service to enable the discovery of the linked VPs through the DID Document. + // This is optional since it is not a hard requirement by the specs. + let service_url: DIDUrl = did.clone().join("#linked-vp")?; + let linked_verifiable_presentation_service = + LinkedVerifiablePresentationService::new(service_url, verifiable_presentation_urls, Object::new())?; + did_document.insert_service(linked_verifiable_presentation_service.into())?; + let updated_did_document: IotaDocument = publish_document(client.clone(), secret_manager, did_document).await?; + + println!("DID document with linked verifiable presentation service: {updated_did_document:#}"); + + // ===================================================== + // Verification + // ===================================================== + + // Init a resolver for resolving DID Documents. + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + + // Resolve the DID Document of the DID that issued the credential. + let did_document: IotaDocument = resolver.resolve(&did).await?; + + // Get the Linked Verifiable Presentation Services from the DID Document. + let linked_verifiable_presentation_services: Vec = did_document + .service() + .iter() + .cloned() + .filter_map(|service| LinkedVerifiablePresentationService::try_from(service).ok()) + .collect(); + assert_eq!(linked_verifiable_presentation_services.len(), 1); + + // Get the VPs included in the service. + let _verifiable_presentation_urls: &[Url] = linked_verifiable_presentation_services + .first() + .ok_or_else(|| anyhow::anyhow!("expected verifiable presentation urls"))? + .verifiable_presentation_urls(); + + // Fetch the verifiable presentation from the URL (for example using `reqwest`). + // But since the URLs do not point to actual online resource, we will simply create an example JWT. + let presentation_jwt: Jwt = make_vp_jwt(&did_document, &storage, &fragment).await?; + + // Resolve the holder's document. + let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; + let holder: IotaDocument = resolver.resolve(&holder_did).await?; + + // Validate linked presentation. Note that this doesn't validate the included credentials. + let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default(); + let presentation_validation_options = + JwtPresentationValidationOptions::default().presentation_verifier_options(presentation_verifier_options); + let validation_result: Result, CompoundJwtPresentationValidationError> = + JwtPresentationValidator::with_signature_verifier(EdDSAJwsVerifier::default()).validate( + &presentation_jwt, + &holder, + &presentation_validation_options, + ); + + assert!(validation_result.is_ok()); + + Ok(()) +} + +async fn publish_document( + client: Client, + secret_manager: SecretManager, + document: IotaDocument, +) -> anyhow::Result { + // Resolve the latest output and update it with the given document. + let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; + + // Because the size of the DID document increased, we have to increase the allocated storage deposit. + // This increases the deposit amount to the new minimum. + let rent_structure: RentStructure = client.get_rent_structure().await?; + let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) + .with_minimum_storage_deposit(rent_structure) + .finish()?; + + // Publish the updated Alias Output. + Ok(client.publish_did_output(&secret_manager, alias_output).await?) +} + +async fn make_vp_jwt(did_doc: &IotaDocument, storage: &MemStorage, fragment: &str) -> anyhow::Result { + // first we create a credential encoding it as jwt + let credential = CredentialBuilder::new(Object::default()) + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(did_doc.id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(Subject::from_json_value(serde_json::json!({ + "id": did_doc.id().as_str(), + "name": "Alice", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?) + .build()?; + let credential = did_doc + .create_credential_jwt(&credential, storage, fragment, &JwsSignatureOptions::default(), None) + .await?; + // then we create a presentation including the just created JWT encoded credential. + let presentation = PresentationBuilder::new(Url::parse(did_doc.id().as_str())?, Object::default()) + .credential(credential) + .build()?; + // we encode the presentation as JWT + did_doc + .create_presentation_jwt( + &presentation, + storage, + fragment, + &JwsSignatureOptions::default(), + &JwtPresentationOptions::default(), + ) + .await + .context("jwt presentation failed") +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 9fe5984123..9866115ad3 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -9,7 +9,7 @@ publish = false anyhow = "1.0.62" bls12_381_plus.workspace = true identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } -identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus"] } +identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus", "resolver"] } identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["bbs-plus"] } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } json-proof-token.workspace = true @@ -101,3 +101,7 @@ name = "9_zkp" [[example]] path = "1_advanced/10_zkp_revocation.rs" name = "10_zkp_revocation" + +[[example]] +path = "1_advanced/11_linked_verifiable_presentation.rs" +name = "11_linked_verifiable_presentation" diff --git a/examples/README.md b/examples/README.md index 2076f8b4b2..8ea9ab2145 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,13 +18,12 @@ cargo run --release --example 0_create_did ### Note: Running the examples with the release flag will be significantly faster due to stronghold performance issues in debug mode. - ## Basic Examples The following basic CRUD (Create, Read, Update, Delete) examples are available: | Name | Information | -|:--------------------------------------------------|:-------------------------------------------------------------------------------------| +| :------------------------------------------------ | :----------------------------------------------------------------------------------- | | [0_create_did](./0_basic/0_create_did.rs) | Demonstrates how to create a DID Document and publish it in a new Alias Output. | | [1_update_did](./0_basic/1_update_did.rs) | Demonstrates how to update a DID document in an existing Alias Output. | | [2_resolve_did](./0_basic/2_resolve_did.rs) | Demonstrates how to resolve an existing DID in an Alias Output. | @@ -38,14 +37,15 @@ The following basic CRUD (Create, Read, Update, Delete) examples are available: The following advanced examples are available: -| Name | Information | -|:-----------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------| -| [0_did_controls_did](./1_advanced/0_did_controls_did.rs) | Demonstrates how an identity can control another identity. | -| [1_did_issues_nft](./1_advanced/1_did_issues_nft.rs) | Demonstrates how an identity can issue and own NFTs, and how observers can verify the issuer of the NFT. | -| [2_nft_owns_did](./1_advanced/2_nft_owns_did.rs) | Demonstrates how an identity can be owned by NFTs, and how observers can verify that relationship. | -| [3_did_issues_tokens](./1_advanced/3_did_issues_tokens.rs) | Demonstrates how an identity can issue and control a Token Foundry and its tokens. | -| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. | -| [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. | -| [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. | -| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. | -| [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. | +| Name | Information | +| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | +| [0_did_controls_did](./1_advanced/0_did_controls_did.rs) | Demonstrates how an identity can control another identity. | +| [1_did_issues_nft](./1_advanced/1_did_issues_nft.rs) | Demonstrates how an identity can issue and own NFTs, and how observers can verify the issuer of the NFT. | +| [2_nft_owns_did](./1_advanced/2_nft_owns_did.rs) | Demonstrates how an identity can be owned by NFTs, and how observers can verify that relationship. | +| [3_did_issues_tokens](./1_advanced/3_did_issues_tokens.rs) | Demonstrates how an identity can issue and control a Token Foundry and its tokens. | +| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. | +| [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. | +| [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. | +| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. | +| [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. | +| [11_linked_verifiable_presentation](./1_advanced/11_linked_verifiable_presentation.rs) | Demonstrates how to link a public Verifiable Presentation to an identity and how it can be verified. | diff --git a/identity_credential/src/credential/linked_verifiable_presentation_service.rs b/identity_credential/src/credential/linked_verifiable_presentation_service.rs new file mode 100644 index 0000000000..20e34e0a97 --- /dev/null +++ b/identity_credential/src/credential/linked_verifiable_presentation_service.rs @@ -0,0 +1,207 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_core::common::OrderedSet; +use identity_core::common::Url; +use identity_did::DIDUrl; +use identity_document::service::Service; +use identity_document::service::ServiceBuilder; +use identity_document::service::ServiceEndpoint; +use serde::Deserialize; +use serde::Serialize; + +use crate::error::Result; +use crate::Error; +use crate::Error::LinkedVerifiablePresentationError; + +/// A service wrapper for a [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "Service", into = "Service")] +pub struct LinkedVerifiablePresentationService(Service); + +impl TryFrom for LinkedVerifiablePresentationService { + type Error = Error; + + fn try_from(service: Service) -> std::result::Result { + LinkedVerifiablePresentationService::check_structure(&service)?; + Ok(LinkedVerifiablePresentationService(service)) + } +} + +impl From for Service { + fn from(service: LinkedVerifiablePresentationService) -> Self { + service.0 + } +} + +impl LinkedVerifiablePresentationService { + pub(crate) fn linked_verifiable_presentation_service_type() -> &'static str { + "LinkedVerifiablePresentation" + } + + /// Constructs a new `LinkedVerifiablePresentationService` that wraps a spec compliant + /// [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). + pub fn new( + did_url: DIDUrl, + verifiable_presentation_urls: impl Into>, + properties: Object, + ) -> Result { + let verifiable_presentation_urls: OrderedSet = verifiable_presentation_urls.into(); + let builder: ServiceBuilder = Service::builder(properties) + .id(did_url) + .type_(Self::linked_verifiable_presentation_service_type()); + if verifiable_presentation_urls.len() == 1 { + let vp_url = verifiable_presentation_urls + .into_iter() + .next() + .expect("element 0 exists"); + let service = builder + .service_endpoint(vp_url) + .build() + .map_err(|err| LinkedVerifiablePresentationError(Box::new(err)))?; + Ok(Self(service)) + } else { + let service = builder + .service_endpoint(ServiceEndpoint::Set(verifiable_presentation_urls)) + .build() + .map_err(|err| LinkedVerifiablePresentationError(Box::new(err)))?; + Ok(Self(service)) + } + } + + /// Checks the semantic structure of a Linked Verifiable Presentation Service. + /// + /// Note: `{"type": ["LinkedVerifiablePresentation"]}` might be serialized the same way as `{"type": + /// "LinkedVerifiablePresentation"}` which passes the semantic check. + pub fn check_structure(service: &Service) -> Result<()> { + if service.type_().len() != 1 { + return Err(LinkedVerifiablePresentationError("invalid service type".into())); + } + + let service_type = service + .type_() + .get(0) + .ok_or_else(|| LinkedVerifiablePresentationError("missing service type".into()))?; + + if service_type != Self::linked_verifiable_presentation_service_type() { + return Err(LinkedVerifiablePresentationError( + format!( + "expected `{}` service type", + Self::linked_verifiable_presentation_service_type() + ) + .into(), + )); + } + + match service.service_endpoint() { + ServiceEndpoint::One(_) => Ok(()), + ServiceEndpoint::Set(_) => Ok(()), + ServiceEndpoint::Map(_) => Err(LinkedVerifiablePresentationError( + "service endpoints must be either a string or a set".into(), + )), + } + } + + /// Returns the Verifiable Presentations contained in the Linked Verifiable Presentation Service. + pub fn verifiable_presentation_urls(&self) -> &[Url] { + match self.0.service_endpoint() { + ServiceEndpoint::One(endpoint) => std::slice::from_ref(endpoint), + ServiceEndpoint::Set(endpoints) => endpoints.as_slice(), + ServiceEndpoint::Map(_) => { + unreachable!("the service endpoint is never a map per the `LinkedVerifiablePresentationService` type invariant") + } + } + } + + /// Returns a reference to the `Service` id. + pub fn id(&self) -> &DIDUrl { + self.0.id() + } +} + +#[cfg(test)] +mod tests { + use crate::credential::linked_verifiable_presentation_service::LinkedVerifiablePresentationService; + use identity_core::common::Object; + use identity_core::common::OrderedSet; + use identity_core::common::Url; + use identity_core::convert::FromJson; + use identity_did::DIDUrl; + use identity_document::service::Service; + use serde_json::json; + + #[test] + fn test_create_service_single_vp() { + let mut linked_vps: OrderedSet = OrderedSet::new(); + linked_vps.append(Url::parse("https://foo.example-1.com").unwrap()); + + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::new( + DIDUrl::parse("did:example:123#foo").unwrap(), + linked_vps, + Object::new(), + ) + .unwrap(); + + let service_from_json: Service = Service::from_json_value(json!({ + "id": "did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": "https://foo.example-1.com" + })) + .unwrap(); + assert_eq!(Service::from(service), service_from_json); + } + + #[test] + fn test_create_service_multiple_vps() { + let url_1 = "https://foo.example-1.com"; + let url_2 = "https://bar.example-2.com"; + let mut linked_vps = OrderedSet::new(); + linked_vps.append(Url::parse(url_1).unwrap()); + linked_vps.append(Url::parse(url_2).unwrap()); + + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::new( + DIDUrl::parse("did:example:123#foo").unwrap(), + linked_vps, + Object::new(), + ) + .unwrap(); + + let service_from_json: Service = Service::from_json_value(json!({ + "id":"did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": [url_1, url_2] + })) + .unwrap(); + assert_eq!(Service::from(service), service_from_json); + } + + #[test] + fn test_valid_single_vp() { + let service: Service = Service::from_json_value(json!({ + "id": "did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": "https://foo.example-1.com" + })) + .unwrap(); + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::try_from(service).unwrap(); + let linked_vps: Vec = vec![Url::parse("https://foo.example-1.com").unwrap()]; + assert_eq!(service.verifiable_presentation_urls(), linked_vps); + } + + #[test] + fn test_valid_multiple_vps() { + let service: Service = Service::from_json_value(json!({ + "id": "did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": ["https://foo.example-1.com", "https://foo.example-2.com"] + })) + .unwrap(); + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::try_from(service).unwrap(); + let linked_vps: Vec = vec![ + Url::parse("https://foo.example-1.com").unwrap(), + Url::parse("https://foo.example-2.com").unwrap(), + ]; + assert_eq!(service.verifiable_presentation_urls(), linked_vps); + } +} diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 72f3b5d7a8..07b15f4eba 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -17,6 +17,7 @@ mod jws; mod jwt; mod jwt_serialization; mod linked_domain_service; +mod linked_verifiable_presentation_service; mod policy; mod proof; mod refresh; @@ -37,6 +38,7 @@ pub use self::jwp_credential_options::JwpCredentialOptions; pub use self::jws::Jws; pub use self::jwt::Jwt; pub use self::linked_domain_service::LinkedDomainService; +pub use self::linked_verifiable_presentation_service::LinkedVerifiablePresentationService; pub use self::policy::Policy; pub use self::proof::Proof; pub use self::refresh::RefreshService; diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 468370e460..1c814c3899 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -37,6 +37,9 @@ pub enum Error { /// Caused when constructing an invalid `LinkedDomainService` or `DomainLinkageConfiguration`. #[error("domain linkage error: {0}")] DomainLinkageError(#[source] Box), + /// Caused when constructing an invalid `LinkedVerifiablePresentationService`. + #[error("linked verifiable presentation error: {0}")] + LinkedVerifiablePresentationError(#[source] Box), /// Caused when attempting to encode a `Credential` containing multiple subjects as a JWT. #[error("could not create JWT claim set from verifiable credential: more than one subject")] MoreThanOneSubjectInJwt,