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,