From b7853fb0d561e796990a486cb23d5a47c11fafe4 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 11 Jun 2024 14:30:31 +0200 Subject: [PATCH 01/29] Resolver trait and CompoundResolver macro --- Cargo.toml | 2 + compound_resolver/Cargo.toml | 20 +++++ compound_resolver/src/lib.rs | 106 ++++++++++++++++++++++++ identity_new_resolver/Cargo.toml | 20 +++++ identity_new_resolver/src/error.rs | 13 +++ identity_new_resolver/src/lib.rs | 7 ++ identity_new_resolver/src/resolver.rs | 7 ++ identity_new_resolver/tests/compound.rs | 47 +++++++++++ 8 files changed, 222 insertions(+) create mode 100644 compound_resolver/Cargo.toml create mode 100644 compound_resolver/src/lib.rs create mode 100644 identity_new_resolver/Cargo.toml create mode 100644 identity_new_resolver/src/error.rs create mode 100644 identity_new_resolver/src/lib.rs create mode 100644 identity_new_resolver/src/resolver.rs create mode 100644 identity_new_resolver/tests/compound.rs diff --git a/Cargo.toml b/Cargo.toml index a0375aa810..861ef7dbc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ members = [ "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", + "identity_new_resolver", + "compound_resolver", ] exclude = ["bindings/wasm", "bindings/grpc"] diff --git a/compound_resolver/Cargo.toml b/compound_resolver/Cargo.toml new file mode 100644 index 0000000000..af45437b9c --- /dev/null +++ b/compound_resolver/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "compound_resolver" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +proc-macro2 = "1.0.85" +quote = "1.0.36" +syn = { versin = "2.0.66", features = ["parsing"] } + +[lints] +workspace = true + +[lib] +proc-macro = true diff --git a/compound_resolver/src/lib.rs b/compound_resolver/src/lib.rs new file mode 100644 index 0000000000..02e6368752 --- /dev/null +++ b/compound_resolver/src/lib.rs @@ -0,0 +1,106 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse::Parse, punctuated::Punctuated, Attribute, Data, DeriveInput, Field, Ident, Token}; + +#[proc_macro_derive(CompoundResolver, attributes(resolver))] +pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { + let DeriveInput { + ident: struct_ident, + data, + generics, + .. + } = syn::parse_macro_input!(input); + + let Data::Struct(data) = data else { + panic!("Derive macro \"CompoundResolver\" only works on structs"); + }; + + data + .fields + .into_iter() + // parse all the fields that are annoted with #[resolver(..)] + .filter_map(ResolverField::from_field) + // create an iterator over (field_name, Resolver::I, Resolver::T) + .flat_map(|ResolverField { ident, impls }| { + impls + .into_iter() + .map(move |(input_ty, target_ty)| (ident.clone(), input_ty, target_ty)) + }) + // generates code that forward the implementation of Resolver to field_name. + .map(|(field_name, input_ty, target_ty)| { + quote! { + impl ::identity_new_resolver::Resolver<#target_ty, #input_ty> for #struct_ident #generics { + async fn resolve(&self, input: &#input_ty) -> std::result::Result<#target_ty, ::identity_new_resolver::Error> { + self.#field_name.resolve(input).await + } + } + } + }) + .collect::() + .into() +} + +/// A field annotated with `#[resolver(Input -> Target, ..)]` +struct ResolverField { + ident: Ident, + impls: Vec<(Ident, Ident)>, +} + +impl ResolverField { + pub fn from_field(field: Field) -> Option { + let Field { attrs, ident, .. } = field; + let Some(ident) = ident else { + panic!("Derive macro \"CompoundResolver\" only works on struct with named fields"); + }; + + let impls: Vec<(Ident, Ident)> = attrs + .into_iter() + .flat_map(|attr| parse_resolver_attribute(attr).into_iter()) + .collect(); + + if !impls.is_empty() { + Some(ResolverField { ident, impls }) + } else { + None + } + } +} + +fn parse_resolver_attribute(attr: Attribute) -> Vec<(Ident, Ident)> { + if attr.path().is_ident("resolver") { + attr + .parse_args_with(Punctuated::::parse_terminated) + .expect("invalid resolver annotation") + .into_iter() + .map(Into::into) + .collect() + } else { + vec![] + } +} + +struct ResolverTy { + pub input: Ident, + pub target: Ident, +} + +impl From for (Ident, Ident) { + fn from(value: ResolverTy) -> Self { + (value.input, value.target) + } +} + +impl Parse for ResolverTy { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let mut tys = Punctuated::]>::parse_separated_nonempty(input)? + .into_iter() + .take(2); + + Ok({ + ResolverTy { + input: tys.next().unwrap(), + target: tys.next().unwrap(), + } + }) + } +} diff --git a/identity_new_resolver/Cargo.toml b/identity_new_resolver/Cargo.toml new file mode 100644 index 0000000000..aadc8c7b0c --- /dev/null +++ b/identity_new_resolver/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "identity_new_resolver" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = "1.0.86" +thiserror.workspace = true +compound_resolver = { path = "../compound_resolver" } + +[lints] +workspace = true + +[dev-dependencies] +tokio = { version = "1.38.0", features = ["macros", "rt"] } diff --git a/identity_new_resolver/src/error.rs b/identity_new_resolver/src/error.rs new file mode 100644 index 0000000000..d9d9b984a9 --- /dev/null +++ b/identity_new_resolver/src/error.rs @@ -0,0 +1,13 @@ +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("The requested item \"{0}\" was not found.")] + NotFound(String), + #[error("Failed to parse the provided input into a resolvable type: {0}")] + ParsingFailure(#[source] anyhow::Error), + #[error(transparent)] + Generic(anyhow::Error), +} diff --git a/identity_new_resolver/src/lib.rs b/identity_new_resolver/src/lib.rs new file mode 100644 index 0000000000..6238ab24db --- /dev/null +++ b/identity_new_resolver/src/lib.rs @@ -0,0 +1,7 @@ +mod resolver; +mod error; + +pub use error::{Result, Error}; +pub use resolver::Resolver; + +pub use compound_resolver::CompoundResolver; \ No newline at end of file diff --git a/identity_new_resolver/src/resolver.rs b/identity_new_resolver/src/resolver.rs new file mode 100644 index 0000000000..1a95ad8537 --- /dev/null +++ b/identity_new_resolver/src/resolver.rs @@ -0,0 +1,7 @@ +#![allow(async_fn_in_trait)] + +use crate::Result; + +pub trait Resolver { + async fn resolve(&self, input: &I) -> Result; +} diff --git a/identity_new_resolver/tests/compound.rs b/identity_new_resolver/tests/compound.rs new file mode 100644 index 0000000000..602a750c2b --- /dev/null +++ b/identity_new_resolver/tests/compound.rs @@ -0,0 +1,47 @@ +use identity_new_resolver::{CompoundResolver, Result, Resolver}; + +struct DidKey; +struct DidJwk; +struct DidWeb; + +struct CoreDoc; + +struct DidKeyResolver; +impl Resolver for DidKeyResolver { + async fn resolve(&self, _input: &DidKey) -> Result { + Ok(CoreDoc {}) + } +} +struct DidJwkResolver; +impl Resolver for DidJwkResolver { + async fn resolve(&self, _input: &DidJwk) -> Result { + Ok(CoreDoc {}) + } +} +struct DidWebResolver; +impl Resolver for DidWebResolver { + async fn resolve(&self, _input: &DidWeb) -> Result { + Ok(CoreDoc {}) + } +} + +#[derive(CompoundResolver)] +struct SuperDidResolver { + #[resolver(DidKey -> CoreDoc)] + did_key: DidKeyResolver, + #[resolver(DidJwk -> CoreDoc)] + did_jwk: DidJwkResolver, + #[resolver(DidWeb -> CoreDoc)] + did_web: DidWebResolver, +} + +#[tokio::test] +async fn test_compound_resolver() { + let super_resolver = SuperDidResolver { + did_key: DidKeyResolver {}, + did_jwk: DidJwkResolver {}, + did_web: DidWebResolver {}, + }; + + assert!(super_resolver.resolve(&DidJwk {}).await.is_ok()); +} From 0114b35677e63e5c74d2837e770d40e62138ae01 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 11 Jun 2024 14:56:56 +0200 Subject: [PATCH 02/29] invert Resolver type parameters --- compound_resolver/src/lib.rs | 2 +- identity_new_resolver/src/resolver.rs | 2 +- identity_new_resolver/tests/compound.rs | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/compound_resolver/src/lib.rs b/compound_resolver/src/lib.rs index 02e6368752..7c2b29e35c 100644 --- a/compound_resolver/src/lib.rs +++ b/compound_resolver/src/lib.rs @@ -29,7 +29,7 @@ pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { // generates code that forward the implementation of Resolver to field_name. .map(|(field_name, input_ty, target_ty)| { quote! { - impl ::identity_new_resolver::Resolver<#target_ty, #input_ty> for #struct_ident #generics { + impl ::identity_new_resolver::Resolver<#input_ty, #target_ty> for #struct_ident #generics { async fn resolve(&self, input: &#input_ty) -> std::result::Result<#target_ty, ::identity_new_resolver::Error> { self.#field_name.resolve(input).await } diff --git a/identity_new_resolver/src/resolver.rs b/identity_new_resolver/src/resolver.rs index 1a95ad8537..e9f27a767b 100644 --- a/identity_new_resolver/src/resolver.rs +++ b/identity_new_resolver/src/resolver.rs @@ -2,6 +2,6 @@ use crate::Result; -pub trait Resolver { +pub trait Resolver { async fn resolve(&self, input: &I) -> Result; } diff --git a/identity_new_resolver/tests/compound.rs b/identity_new_resolver/tests/compound.rs index 602a750c2b..5a9efe80cf 100644 --- a/identity_new_resolver/tests/compound.rs +++ b/identity_new_resolver/tests/compound.rs @@ -1,4 +1,4 @@ -use identity_new_resolver::{CompoundResolver, Result, Resolver}; +use identity_new_resolver::{CompoundResolver, Resolver, Result}; struct DidKey; struct DidJwk; @@ -7,19 +7,19 @@ struct DidWeb; struct CoreDoc; struct DidKeyResolver; -impl Resolver for DidKeyResolver { +impl Resolver for DidKeyResolver { async fn resolve(&self, _input: &DidKey) -> Result { Ok(CoreDoc {}) } } struct DidJwkResolver; -impl Resolver for DidJwkResolver { +impl Resolver for DidJwkResolver { async fn resolve(&self, _input: &DidJwk) -> Result { Ok(CoreDoc {}) } } struct DidWebResolver; -impl Resolver for DidWebResolver { +impl Resolver for DidWebResolver { async fn resolve(&self, _input: &DidWeb) -> Result { Ok(CoreDoc {}) } @@ -37,11 +37,11 @@ struct SuperDidResolver { #[tokio::test] async fn test_compound_resolver() { - let super_resolver = SuperDidResolver { - did_key: DidKeyResolver {}, - did_jwk: DidJwkResolver {}, - did_web: DidWebResolver {}, - }; + let super_resolver = SuperDidResolver { + did_key: DidKeyResolver {}, + did_jwk: DidJwkResolver {}, + did_web: DidWebResolver {}, + }; - assert!(super_resolver.resolve(&DidJwk {}).await.is_ok()); + assert!(super_resolver.resolve(&DidJwk {}).await.is_ok()); } From 88a538e864074d82e121e534e9e00c1fbefc7c4c Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 11 Jun 2024 15:18:15 +0200 Subject: [PATCH 03/29] associated type Target instead of type parameter T --- compound_resolver/src/lib.rs | 5 +++-- identity_new_resolver/src/resolver.rs | 5 +++-- identity_new_resolver/tests/compound.rs | 15 +++++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/compound_resolver/src/lib.rs b/compound_resolver/src/lib.rs index 7c2b29e35c..f5a3a06b92 100644 --- a/compound_resolver/src/lib.rs +++ b/compound_resolver/src/lib.rs @@ -29,8 +29,9 @@ pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { // generates code that forward the implementation of Resolver to field_name. .map(|(field_name, input_ty, target_ty)| { quote! { - impl ::identity_new_resolver::Resolver<#input_ty, #target_ty> for #struct_ident #generics { - async fn resolve(&self, input: &#input_ty) -> std::result::Result<#target_ty, ::identity_new_resolver::Error> { + impl ::identity_new_resolver::Resolver<#input_ty> for #struct_ident #generics { + type Target = #target_ty; + async fn resolve(&self, input: &#input_ty) -> std::result::Result { self.#field_name.resolve(input).await } } diff --git a/identity_new_resolver/src/resolver.rs b/identity_new_resolver/src/resolver.rs index e9f27a767b..b7499a3991 100644 --- a/identity_new_resolver/src/resolver.rs +++ b/identity_new_resolver/src/resolver.rs @@ -2,6 +2,7 @@ use crate::Result; -pub trait Resolver { - async fn resolve(&self, input: &I) -> Result; +pub trait Resolver { + type Target; + async fn resolve(&self, input: &I) -> Result; } diff --git a/identity_new_resolver/tests/compound.rs b/identity_new_resolver/tests/compound.rs index 5a9efe80cf..3b6901b329 100644 --- a/identity_new_resolver/tests/compound.rs +++ b/identity_new_resolver/tests/compound.rs @@ -7,20 +7,23 @@ struct DidWeb; struct CoreDoc; struct DidKeyResolver; -impl Resolver for DidKeyResolver { - async fn resolve(&self, _input: &DidKey) -> Result { +impl Resolver for DidKeyResolver { + type Target = CoreDoc; + async fn resolve(&self, _input: &DidKey) -> Result { Ok(CoreDoc {}) } } struct DidJwkResolver; -impl Resolver for DidJwkResolver { - async fn resolve(&self, _input: &DidJwk) -> Result { +impl Resolver for DidJwkResolver { + type Target = CoreDoc; + async fn resolve(&self, _input: &DidJwk) -> Result { Ok(CoreDoc {}) } } struct DidWebResolver; -impl Resolver for DidWebResolver { - async fn resolve(&self, _input: &DidWeb) -> Result { +impl Resolver for DidWebResolver { + type Target = CoreDoc; + async fn resolve(&self, _input: &DidWeb) -> Result { Ok(CoreDoc {}) } } From bfd5d59af8944cfdb9bf6cadb41e068c86e8ee8c Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 12 Jun 2024 13:00:13 +0200 Subject: [PATCH 04/29] fix type issue in #[resolver(..)] annotation, support for multiple resolvers with the same signature --- compound_resolver/Cargo.toml | 3 +- compound_resolver/src/lib.rs | 103 +++++++++++++++++------- identity_new_resolver/tests/compound.rs | 51 +++++++++++- 3 files changed, 126 insertions(+), 31 deletions(-) diff --git a/compound_resolver/Cargo.toml b/compound_resolver/Cargo.toml index af45437b9c..a78d82539d 100644 --- a/compound_resolver/Cargo.toml +++ b/compound_resolver/Cargo.toml @@ -9,9 +9,10 @@ repository.workspace = true rust-version.workspace = true [dependencies] +itertools = "0.13.0" proc-macro2 = "1.0.85" quote = "1.0.36" -syn = { versin = "2.0.66", features = ["parsing"] } +syn = { versin = "2.0.66", features = ["full", "extra-traits"] } [lints] workspace = true diff --git a/compound_resolver/src/lib.rs b/compound_resolver/src/lib.rs index f5a3a06b92..7e6d1bf7e1 100644 --- a/compound_resolver/src/lib.rs +++ b/compound_resolver/src/lib.rs @@ -1,6 +1,7 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse::Parse, punctuated::Punctuated, Attribute, Data, DeriveInput, Field, Ident, Token}; +use syn::{parse::Parse, punctuated::Punctuated, Attribute, Data, DeriveInput, Expr, Field, Ident, Token, Type}; +use itertools::Itertools; #[proc_macro_derive(CompoundResolver, attributes(resolver))] pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { @@ -20,19 +21,25 @@ pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { .into_iter() // parse all the fields that are annoted with #[resolver(..)] .filter_map(ResolverField::from_field) - // create an iterator over (field_name, Resolver::I, Resolver::T) + // create an iterator over (field_name, Resolver::I, Resolver::T, predicate) .flat_map(|ResolverField { ident, impls }| { impls .into_iter() - .map(move |(input_ty, target_ty)| (ident.clone(), input_ty, target_ty)) + .map(move |ResolverImpl { input, target, pred }| ((input, target), (ident.clone(), pred))) }) - // generates code that forward the implementation of Resolver to field_name. - .map(|(field_name, input_ty, target_ty)| { + // Group together all resolvers with the same signature (input_ty, target_ty). + .into_group_map() + .into_iter() + // generates code that forward the implementation of Resolver to field_name, if there's multiple fields + // implementing that trait, use `pred` to decide which one to call. + .map(|((input_ty, target_ty), impls)| { + let len = impls.len(); + let impl_block = gen_impl_block_multiple_resolvers(impls.into_iter(), len); quote! { impl ::identity_new_resolver::Resolver<#input_ty> for #struct_ident #generics { type Target = #target_ty; async fn resolve(&self, input: &#input_ty) -> std::result::Result { - self.#field_name.resolve(input).await + #impl_block } } } @@ -41,10 +48,38 @@ pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { .into() } +fn gen_impl_block_single_resolver(field_name: Ident) -> proc_macro2::TokenStream { + quote! { + self.#field_name.resolve(input).await + } +} + +fn gen_impl_block_single_resolver_with_pred(field_name: Ident, pred: Expr) -> proc_macro2::TokenStream { + let invocation_block = gen_impl_block_single_resolver(field_name); + quote! { + if #pred { return #invocation_block } + } +} + +fn gen_impl_block_multiple_resolvers(impls: impl Iterator)>, len: usize) -> proc_macro2::TokenStream { + impls + .enumerate() + .map(|(i, (field_name, pred))| { + if let Some(pred) = pred { + gen_impl_block_single_resolver_with_pred(field_name, pred) + } else if i == len - 1 { + gen_impl_block_single_resolver(field_name) + } else { + panic!("Multiple resolvers with the same signature. Expected predicate"); + } + }) + .collect() +} + /// A field annotated with `#[resolver(Input -> Target, ..)]` struct ResolverField { ident: Ident, - impls: Vec<(Ident, Ident)>, + impls: Vec, } impl ResolverField { @@ -54,10 +89,10 @@ impl ResolverField { panic!("Derive macro \"CompoundResolver\" only works on struct with named fields"); }; - let impls: Vec<(Ident, Ident)> = attrs + let impls = attrs .into_iter() .flat_map(|attr| parse_resolver_attribute(attr).into_iter()) - .collect(); + .collect::>(); if !impls.is_empty() { Some(ResolverField { ident, impls }) @@ -67,41 +102,53 @@ impl ResolverField { } } -fn parse_resolver_attribute(attr: Attribute) -> Vec<(Ident, Ident)> { +fn parse_resolver_attribute(attr: Attribute) -> Vec { if attr.path().is_ident("resolver") { attr - .parse_args_with(Punctuated::::parse_terminated) + .parse_args_with(Punctuated::::parse_terminated) .expect("invalid resolver annotation") .into_iter() - .map(Into::into) .collect() } else { vec![] } } -struct ResolverTy { - pub input: Ident, - pub target: Ident, +struct ResolverImpl { + pub input: Type, + pub target: Type, + pub pred: Option, } -impl From for (Ident, Ident) { - fn from(value: ResolverTy) -> Self { - (value.input, value.target) - } -} - -impl Parse for ResolverTy { +impl Parse for ResolverImpl { fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut tys = Punctuated::]>::parse_separated_nonempty(input)? - .into_iter() - .take(2); + // let mut tys = Punctuated::]>::parse_separated_nonempty(input)? + // .into_iter() + // .take(2); + let input_ty = input.parse::()?; + let _ = input.parse::]>()?; + let target_ty = input.parse::()?; + let pred = if input.peek(Token![if]) { + let _ = input.parse::()?; + Some(input.parse::()?) + } else { + None + }; Ok({ - ResolverTy { - input: tys.next().unwrap(), - target: tys.next().unwrap(), + ResolverImpl { + input: input_ty, + target: target_ty, + pred } }) } } + +#[test] +fn test_parse_resolver_attribute() { + syn::parse_str::("DidKey -> CoreDoc").unwrap(); + syn::parse_str::("DidKey -> Vec").unwrap(); + syn::parse_str::("Vec -> &str").unwrap(); + syn::parse_str::("DidIota -> IotaDoc if input.method_id() == \"iota\"").unwrap(); +} diff --git a/identity_new_resolver/tests/compound.rs b/identity_new_resolver/tests/compound.rs index 3b6901b329..b60480dfcb 100644 --- a/identity_new_resolver/tests/compound.rs +++ b/identity_new_resolver/tests/compound.rs @@ -1,10 +1,14 @@ -use identity_new_resolver::{CompoundResolver, Resolver, Result}; +use identity_new_resolver::{CompoundResolver, Resolver, Result, Error}; struct DidKey; struct DidJwk; struct DidWeb; +struct DidIota { + network: u32, +} struct CoreDoc; +struct IotaDoc; struct DidKeyResolver; impl Resolver for DidKeyResolver { @@ -28,6 +32,21 @@ impl Resolver for DidWebResolver { } } +struct Client { + network: u32, +} + +impl Resolver for Client { + type Target = IotaDoc; + async fn resolve(&self, input: &DidIota) -> Result { + if input.network == self.network { + Ok(IotaDoc {}) + } else { + Err(Error::Generic(anyhow::anyhow!("Invalid network"))) + } + } +} + #[derive(CompoundResolver)] struct SuperDidResolver { #[resolver(DidKey -> CoreDoc)] @@ -38,8 +57,18 @@ struct SuperDidResolver { did_web: DidWebResolver, } +#[derive(CompoundResolver)] +struct IdentityClient { + #[resolver(DidKey -> CoreDoc, DidJwk -> CoreDoc, DidWeb -> CoreDoc)] + dids: SuperDidResolver, + #[resolver(DidIota -> IotaDoc if input.network == 0)] + iota: Client, + #[resolver(DidIota -> IotaDoc)] + shimmer: Client, +} + #[tokio::test] -async fn test_compound_resolver() { +async fn test_compound_resolver_simple() { let super_resolver = SuperDidResolver { did_key: DidKeyResolver {}, did_jwk: DidJwkResolver {}, @@ -48,3 +77,21 @@ async fn test_compound_resolver() { assert!(super_resolver.resolve(&DidJwk {}).await.is_ok()); } + +#[tokio::test] +async fn test_compound_resolver_conflicts() { + let super_resolver = SuperDidResolver { + did_key: DidKeyResolver {}, + did_jwk: DidJwkResolver {}, + did_web: DidWebResolver {}, + }; + let identity_client = IdentityClient { + dids: super_resolver, + iota: Client { network: 0}, + shimmer: Client {network: 1}, + }; + + assert!(identity_client.resolve(&DidJwk {}).await.is_ok()); + assert!(identity_client.resolve(&DidIota { network: 1}).await.is_ok()); + assert!(identity_client.resolve(&DidIota { network: 0}).await.is_ok()); +} From 5671118e4e85f1a3a7301b9da1229bd0555cdfe2 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 13 Jun 2024 16:06:04 +0200 Subject: [PATCH 05/29] resolver integration --- Cargo.toml | 1 - compound_resolver/Cargo.toml | 4 +- compound_resolver/src/lib.rs | 32 +- examples/0_basic/2_resolve_did.rs | 14 - examples/0_basic/6_create_vp.rs | 11 +- examples/0_basic/7_revoke_vc.rs | 4 +- examples/0_basic/8_stronghold.rs | 4 +- examples/1_advanced/10_zkp_revocation.rs | 5 +- examples/1_advanced/5_custom_resolution.rs | 82 ++-- examples/1_advanced/6_domain_linkage.rs | 8 +- examples/1_advanced/9_zkp.rs | 7 +- identity_iota/Cargo.toml | 8 +- identity_iota/src/lib.rs | 1 + identity_new_resolver/Cargo.toml | 20 - identity_new_resolver/src/error.rs | 13 - identity_new_resolver/src/lib.rs | 7 - identity_new_resolver/src/resolver.rs | 8 - identity_new_resolver/tests/compound.rs | 97 ---- identity_resolver/Cargo.toml | 39 +- identity_resolver/README.md | 4 - identity_resolver/src/error.rs | 85 +--- identity_resolver/src/iota.rs | 64 +++ identity_resolver/src/lib.rs | 27 +- identity_resolver/src/resolution/commands.rs | 142 ------ identity_resolver/src/resolution/mod.rs | 14 - identity_resolver/src/resolution/resolver.rs | 417 ------------------ identity_resolver/src/resolution/tests/mod.rs | 6 - .../src/resolution/tests/resolution.rs | 281 ------------ .../src/resolution/tests/send_sync.rs | 26 -- identity_resolver/src/resolver.rs | 17 + 30 files changed, 176 insertions(+), 1272 deletions(-) delete mode 100644 identity_new_resolver/Cargo.toml delete mode 100644 identity_new_resolver/src/error.rs delete mode 100644 identity_new_resolver/src/lib.rs delete mode 100644 identity_new_resolver/src/resolver.rs delete mode 100644 identity_new_resolver/tests/compound.rs delete mode 100644 identity_resolver/README.md create mode 100644 identity_resolver/src/iota.rs delete mode 100644 identity_resolver/src/resolution/commands.rs delete mode 100644 identity_resolver/src/resolution/mod.rs delete mode 100644 identity_resolver/src/resolution/resolver.rs delete mode 100644 identity_resolver/src/resolution/tests/mod.rs delete mode 100644 identity_resolver/src/resolution/tests/resolution.rs delete mode 100644 identity_resolver/src/resolution/tests/send_sync.rs create mode 100644 identity_resolver/src/resolver.rs diff --git a/Cargo.toml b/Cargo.toml index 861ef7dbc1..bb908e0ecf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ members = [ "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", - "identity_new_resolver", "compound_resolver", ] diff --git a/compound_resolver/Cargo.toml b/compound_resolver/Cargo.toml index a78d82539d..51c009c12c 100644 --- a/compound_resolver/Cargo.toml +++ b/compound_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "compound_resolver" -version = "0.1.0" +version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,7 +12,7 @@ rust-version.workspace = true itertools = "0.13.0" proc-macro2 = "1.0.85" quote = "1.0.36" -syn = { versin = "2.0.66", features = ["full", "extra-traits"] } +syn = { version = "2.0.66", features = ["full", "extra-traits"] } [lints] workspace = true diff --git a/compound_resolver/src/lib.rs b/compound_resolver/src/lib.rs index 7e6d1bf7e1..a778c9141a 100644 --- a/compound_resolver/src/lib.rs +++ b/compound_resolver/src/lib.rs @@ -1,7 +1,16 @@ +use itertools::Itertools; use proc_macro::TokenStream; use quote::quote; -use syn::{parse::Parse, punctuated::Punctuated, Attribute, Data, DeriveInput, Expr, Field, Ident, Token, Type}; -use itertools::Itertools; +use syn::parse::Parse; +use syn::punctuated::Punctuated; +use syn::Attribute; +use syn::Data; +use syn::DeriveInput; +use syn::Expr; +use syn::Field; +use syn::Ident; +use syn::Token; +use syn::Type; #[proc_macro_derive(CompoundResolver, attributes(resolver))] pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { @@ -21,13 +30,12 @@ pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { .into_iter() // parse all the fields that are annoted with #[resolver(..)] .filter_map(ResolverField::from_field) - // create an iterator over (field_name, Resolver::I, Resolver::T, predicate) + // Group together all resolvers with the same signature (input_ty, target_ty). .flat_map(|ResolverField { ident, impls }| { impls .into_iter() .map(move |ResolverImpl { input, target, pred }| ((input, target), (ident.clone(), pred))) }) - // Group together all resolvers with the same signature (input_ty, target_ty). .into_group_map() .into_iter() // generates code that forward the implementation of Resolver to field_name, if there's multiple fields @@ -36,9 +44,9 @@ pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { let len = impls.len(); let impl_block = gen_impl_block_multiple_resolvers(impls.into_iter(), len); quote! { - impl ::identity_new_resolver::Resolver<#input_ty> for #struct_ident #generics { + impl ::identity_iota::resolver::Resolver<#input_ty> for #struct_ident #generics { type Target = #target_ty; - async fn resolve(&self, input: &#input_ty) -> std::result::Result { + async fn resolve(&self, input: &#input_ty) -> std::result::Result { #impl_block } } @@ -61,8 +69,11 @@ fn gen_impl_block_single_resolver_with_pred(field_name: Ident, pred: Expr) -> pr } } -fn gen_impl_block_multiple_resolvers(impls: impl Iterator)>, len: usize) -> proc_macro2::TokenStream { - impls +fn gen_impl_block_multiple_resolvers( + impls: impl Iterator)>, + len: usize, +) -> proc_macro2::TokenStream { + impls .enumerate() .map(|(i, (field_name, pred))| { if let Some(pred) = pred { @@ -122,9 +133,6 @@ struct ResolverImpl { impl Parse for ResolverImpl { fn parse(input: syn::parse::ParseStream) -> syn::Result { - // let mut tys = Punctuated::]>::parse_separated_nonempty(input)? - // .into_iter() - // .take(2); let input_ty = input.parse::()?; let _ = input.parse::]>()?; let target_ty = input.parse::()?; @@ -139,7 +147,7 @@ impl Parse for ResolverImpl { ResolverImpl { input: input_ty, target: target_ty, - pred + pred, } }) } diff --git a/examples/0_basic/2_resolve_did.rs b/examples/0_basic/2_resolve_did.rs index 4e648f8370..e56167d54c 100644 --- a/examples/0_basic/2_resolve_did.rs +++ b/examples/0_basic/2_resolve_did.rs @@ -9,7 +9,6 @@ use identity_iota::iota::block::address::Address; use identity_iota::iota::IotaDocument; use identity_iota::iota::IotaIdentityClientExt; -use identity_iota::prelude::Resolver; use identity_iota::storage::JwkMemStore; use identity_iota::storage::KeyIdMemstore; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; @@ -45,19 +44,6 @@ async fn main() -> anyhow::Result<()> { let client_document: IotaDocument = client.resolve_did(&did).await?; println!("Client resolved DID Document: {client_document:#}"); - // We can also create a `Resolver` that has additional convenience methods, - // for example to resolve presentation issuers or to verify presentations. - let mut resolver = Resolver::::new(); - - // We need to register a handler that can resolve IOTA DIDs. - // This convenience method only requires us to provide a client. - resolver.attach_iota_handler(client.clone()); - - let resolver_document: IotaDocument = resolver.resolve(&did).await.unwrap(); - - // Client and Resolver resolve to the same document in this case. - assert_eq!(client_document, resolver_document); - // We can also resolve the Alias Output directly. let alias_output: AliasOutput = client.resolve_did_output(&did).await?; diff --git a/examples/0_basic/6_create_vp.rs b/examples/0_basic/6_create_vp.rs index 8c157295ef..138c763c05 100644 --- a/examples/0_basic/6_create_vp.rs +++ b/examples/0_basic/6_create_vp.rs @@ -7,8 +7,6 @@ //! //! cargo run --release --example 6_create_vp -use std::collections::HashMap; - use examples::create_did; use examples::MemStorage; use identity_eddsa_verifier::EdDSAJwsVerifier; @@ -190,12 +188,9 @@ async fn main() -> anyhow::Result<()> { let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default().nonce(challenge.to_owned()); - let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); - // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; - let holder: IotaDocument = resolver.resolve(&holder_did).await?; + let holder: IotaDocument = client.resolve(&holder_did).await?; // Validate presentation. Note that this doesn't validate the included credentials. let presentation_validation_options = @@ -211,7 +206,7 @@ async fn main() -> anyhow::Result<()> { .iter() .map(JwtCredentialValidatorUtils::extract_issuer_from_jwt) .collect::, _>>()?; - let issuers_documents: HashMap = resolver.resolve_multiple(&issuers).await?; + let issuers_documents: Vec = client.resolve_multiple(&issuers).await?; // Validate the credentials in the presentation. let credential_validator: JwtCredentialValidator = @@ -221,7 +216,7 @@ async fn main() -> anyhow::Result<()> { for (index, jwt_vc) in jwt_credentials.iter().enumerate() { // SAFETY: Indexing should be fine since we extracted the DID from each credential and resolved it. - let issuer_document: &IotaDocument = &issuers_documents[&issuers[index]]; + let issuer_document: &IotaDocument = &issuers_documents[index]; let _decoded_credential: DecodedJwtCredential = credential_validator .validate::<_, Object>(jwt_vc, issuer_document, &validation_options, FailFast::FirstError) diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs index 864041f3e3..b0512bb913 100644 --- a/examples/0_basic/7_revoke_vc.rs +++ b/examples/0_basic/7_revoke_vc.rs @@ -211,10 +211,8 @@ async fn main() -> anyhow::Result<()> { client.publish_did_output(&secret_manager_issuer, alias_output).await?; // We expect the verifiable credential to be revoked. - let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); let resolved_issuer_did: IotaDID = JwtCredentialValidatorUtils::extract_issuer_from_jwt(&credential_jwt)?; - let resolved_issuer_doc: IotaDocument = resolver.resolve(&resolved_issuer_did).await?; + let resolved_issuer_doc: IotaDocument = client.resolve(&resolved_issuer_did).await?; let validation_result = validator.validate::<_, Object>( &credential_jwt, diff --git a/examples/0_basic/8_stronghold.rs b/examples/0_basic/8_stronghold.rs index 0681e5b612..4aa223a027 100644 --- a/examples/0_basic/8_stronghold.rs +++ b/examples/0_basic/8_stronghold.rs @@ -89,9 +89,7 @@ async fn main() -> anyhow::Result<()> { .await?; // Resolve the published DID Document. - let mut resolver = Resolver::::new(); - resolver.attach_iota_handler(client.clone()); - let resolved_document: IotaDocument = resolver.resolve(document.id()).await.unwrap(); + let resolved_document: IotaDocument = client.resolve(document.id()).await.unwrap(); drop(stronghold_storage); diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index a78dea0e76..55c57f1467 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -413,12 +413,9 @@ async fn main() -> anyhow::Result<()> { let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default().nonce(challenge.to_owned()); - let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); - // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; - let holder: IotaDocument = resolver.resolve(&holder_did).await?; + let holder: IotaDocument = client.resolve(&holder_did).await?; // Validate presentation. Note that this doesn't validate the included credentials. let presentation_validation_options = diff --git a/examples/1_advanced/5_custom_resolution.rs b/examples/1_advanced/5_custom_resolution.rs index b0675c8dd5..4492d3ea6d 100644 --- a/examples/1_advanced/5_custom_resolution.rs +++ b/examples/1_advanced/5_custom_resolution.rs @@ -12,6 +12,8 @@ use identity_iota::did::DID; use identity_iota::document::CoreDocument; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; +use identity_iota::resolver::CompoundResolver; +use identity_iota::resolver::Error as ResolverError; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkMemStore; use identity_iota::storage::KeyIdMemstore; @@ -25,21 +27,38 @@ use iota_sdk::types::block::address::Address; /// /// NOTE: Since both `IotaDocument` and `FooDocument` implement `Into` we could have used /// Resolver in this example and just worked with `CoreDocument` representations throughout. + +// Create a resolver capable of resolving FooDocument. +struct FooResolver; +impl Resolver for FooResolver { + type Target = FooDocument; + async fn resolve(&self, input: &CoreDID) -> Result { + Ok(resolve_did_foo(input.clone()).await?) + } +} + +// Combine it with a resolver of IotaDocuments, creating a new resolver capable of resolving both. +#[derive(CompoundResolver)] +struct FooAndIotaResolver { + #[resolver(CoreDID -> FooDocument)] + foo: FooResolver, + #[resolver(IotaDID -> IotaDocument)] + iota: Client, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { - // Create a resolver returning an enum of the documents we are interested in and attach handlers for the "foo" and - // "iota" methods. - let mut resolver: Resolver = Resolver::new(); - // Create a new client to interact with the IOTA ledger. let client: Client = Client::builder() .with_primary_node(API_ENDPOINT, None)? .finish() .await?; - // This is a convenience method for attaching a handler for the "iota" method by providing just a client. - resolver.attach_iota_handler(client.clone()); - resolver.attach_handler("foo".to_owned(), resolve_did_foo); + // Create a resolver capable of resolving both IotaDocument and FooDocument. + let resolver = FooAndIotaResolver { + iota: client.clone(), + foo: FooResolver {}, + }; // A fake did:foo DID for demonstration purposes. let did_foo: CoreDID = "did:foo:0e9c8294eeafee326a4e96d65dbeaca0".parse()?; @@ -58,28 +77,14 @@ async fn main() -> anyhow::Result<()> { let iota_did: IotaDID = iota_document.id().clone(); // Resolve did_foo to get an abstract document. - let did_foo_doc: Document = resolver.resolve(&did_foo).await?; + let did_foo_doc: FooDocument = resolver.resolve(&did_foo).await?; // Resolve iota_did to get an abstract document. - let iota_doc: Document = resolver.resolve(&iota_did).await?; - - // The Resolver is mainly meant for validating presentations, but here we will just - // check that the resolved documents match our expectations. - - let Document::Foo(did_foo_document) = did_foo_doc else { - anyhow::bail!("expected a foo DID document when resolving a foo DID"); - }; + let iota_doc: IotaDocument = resolver.resolve(&iota_did).await?; - println!( - "Resolved DID foo document: {}", - did_foo_document.as_ref().to_json_pretty()? - ); - - let Document::Iota(iota_document) = iota_doc else { - anyhow::bail!("expected an IOTA DID document when resolving an IOTA DID") - }; + println!("Resolved DID foo document: {}", did_foo_doc.as_ref().to_json_pretty()?); - println!("Resolved IOTA DID document: {}", iota_document.to_json_pretty()?); + println!("Resolved IOTA DID document: {}", iota_doc.to_json_pretty()?); Ok(()) } @@ -108,33 +113,6 @@ impl From for CoreDocument { } } -// Enum of the document types we want to handle. -enum Document { - Foo(FooDocument), - Iota(IotaDocument), -} - -impl From for Document { - fn from(value: FooDocument) -> Self { - Self::Foo(value) - } -} - -impl From for Document { - fn from(value: IotaDocument) -> Self { - Self::Iota(value) - } -} - -impl AsRef for Document { - fn as_ref(&self) -> &CoreDocument { - match self { - Self::Foo(doc) => doc.as_ref(), - Self::Iota(doc) => doc.as_ref(), - } - } -} - /// Resolve a did to a DID document if the did method is "foo". async fn resolve_did_foo(did: CoreDID) -> anyhow::Result { let doc = CoreDocument::from_json(&format!( diff --git a/examples/1_advanced/6_domain_linkage.rs b/examples/1_advanced/6_domain_linkage.rs index 6e7a629110..20d23f975c 100644 --- a/examples/1_advanced/6_domain_linkage.rs +++ b/examples/1_advanced/6_domain_linkage.rs @@ -134,10 +134,6 @@ async fn main() -> anyhow::Result<()> { // while the second answers "What domain is this DID linked to?". // ===================================================== - // Init a resolver for resolving DID Documents. - let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client.clone()); - // ===================================================== // → Case 1: starting from domain // ===================================================== @@ -152,7 +148,7 @@ async fn main() -> anyhow::Result<()> { assert_eq!(linked_dids.len(), 1); // Resolve the DID Document of the DID that issued the credential. - let issuer_did_document: IotaDocument = resolver.resolve(&did).await?; + let issuer_did_document: IotaDocument = client.resolve(&did).await?; // Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID. let validation_result: Result<(), DomainLinkageValidationError> = @@ -167,7 +163,7 @@ async fn main() -> anyhow::Result<()> { // ===================================================== // → Case 2: starting from a DID // ===================================================== - let did_document: IotaDocument = resolver.resolve(&did).await?; + let did_document: IotaDocument = client.resolve(&did).await?; // Get the Linked Domain Services from the DID Document. let linked_domain_services: Vec = did_document diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index eeb4246280..97a10b0864 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -164,12 +164,9 @@ async fn main() -> anyhow::Result<()> { // Step 4: Holder resolves Issuer's DID, retrieve Issuer's document and validate the Credential // ============================================================================================ - let mut resolver: Resolver = Resolver::new(); - resolver.attach_iota_handler(client); - // Holder resolves issuer's DID let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap(); - let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; + let issuer_document: IotaDocument = client.resolve(&issuer).await?; // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented let decoded_credential = JptCredentialValidator::validate::<_, Object>( @@ -237,7 +234,7 @@ async fn main() -> anyhow::Result<()> { // Verifier resolve Issuer DID let issuer: CoreDID = JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation_jpt).unwrap(); - let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; + let issuer_document: IotaDocument = client.resolve(&issuer).await?; let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge); diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 0e0187801e..8d6092880a 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -17,9 +17,10 @@ identity_credential = { version = "=1.3.0", path = "../identity_credential", fea identity_did = { version = "=1.3.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.0", path = "../identity_document", default-features = false } identity_iota_core = { version = "=1.3.0", path = "../identity_iota_core", default-features = false } -identity_resolver = { version = "=1.3.0", path = "../identity_resolver", default-features = false, optional = true } identity_storage = { version = "=1.3.0", path = "../identity_storage", default-features = false, features = ["iota-document"] } identity_verification = { version = "=1.3.0", path = "../identity_verification", default-features = false } +identity_resolver = { version = "=1.3.0", path = "../identity_resolver", default-features = false, features = ["iota"] } +compound_resolver = { version = "=1.3.0", path = "../compound_resolver" } [dev-dependencies] anyhow = "1.0.64" @@ -34,20 +35,19 @@ default = ["revocation-bitmap", "client", "iota-client", "resolver"] client = ["identity_iota_core/client"] # Enables the iota-client integration, the client trait implementations for it, and the `IotaClientExt` trait. -iota-client = ["identity_iota_core/iota-client", "identity_resolver?/iota"] +iota-client = ["identity_iota_core/iota-client", "identity_resolver/iota"] # Enables revocation with `RevocationBitmap2022`. revocation-bitmap = [ "identity_credential/revocation-bitmap", "identity_iota_core/revocation-bitmap", - "identity_resolver?/revocation-bitmap", ] # Enables revocation with `StatusList2021`. status-list-2021 = ["revocation-bitmap", "identity_credential/status-list-2021"] # Enables support for the `Resolver`. -resolver = ["dep:identity_resolver"] +resolver = [] # Enables `Send` + `Sync` bounds for the storage traits. send-sync-storage = ["identity_storage/send-sync-storage"] diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 9ab2e53805..d84eb2765f 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -95,6 +95,7 @@ pub mod prelude { pub mod resolver { //! DID resolution utilities + pub use compound_resolver::CompoundResolver; pub use identity_resolver::*; } diff --git a/identity_new_resolver/Cargo.toml b/identity_new_resolver/Cargo.toml deleted file mode 100644 index aadc8c7b0c..0000000000 --- a/identity_new_resolver/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "identity_new_resolver" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true - -[dependencies] -anyhow = "1.0.86" -thiserror.workspace = true -compound_resolver = { path = "../compound_resolver" } - -[lints] -workspace = true - -[dev-dependencies] -tokio = { version = "1.38.0", features = ["macros", "rt"] } diff --git a/identity_new_resolver/src/error.rs b/identity_new_resolver/src/error.rs deleted file mode 100644 index d9d9b984a9..0000000000 --- a/identity_new_resolver/src/error.rs +++ /dev/null @@ -1,13 +0,0 @@ -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum Error { - #[error("The requested item \"{0}\" was not found.")] - NotFound(String), - #[error("Failed to parse the provided input into a resolvable type: {0}")] - ParsingFailure(#[source] anyhow::Error), - #[error(transparent)] - Generic(anyhow::Error), -} diff --git a/identity_new_resolver/src/lib.rs b/identity_new_resolver/src/lib.rs deleted file mode 100644 index 6238ab24db..0000000000 --- a/identity_new_resolver/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod resolver; -mod error; - -pub use error::{Result, Error}; -pub use resolver::Resolver; - -pub use compound_resolver::CompoundResolver; \ No newline at end of file diff --git a/identity_new_resolver/src/resolver.rs b/identity_new_resolver/src/resolver.rs deleted file mode 100644 index b7499a3991..0000000000 --- a/identity_new_resolver/src/resolver.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![allow(async_fn_in_trait)] - -use crate::Result; - -pub trait Resolver { - type Target; - async fn resolve(&self, input: &I) -> Result; -} diff --git a/identity_new_resolver/tests/compound.rs b/identity_new_resolver/tests/compound.rs deleted file mode 100644 index b60480dfcb..0000000000 --- a/identity_new_resolver/tests/compound.rs +++ /dev/null @@ -1,97 +0,0 @@ -use identity_new_resolver::{CompoundResolver, Resolver, Result, Error}; - -struct DidKey; -struct DidJwk; -struct DidWeb; -struct DidIota { - network: u32, -} - -struct CoreDoc; -struct IotaDoc; - -struct DidKeyResolver; -impl Resolver for DidKeyResolver { - type Target = CoreDoc; - async fn resolve(&self, _input: &DidKey) -> Result { - Ok(CoreDoc {}) - } -} -struct DidJwkResolver; -impl Resolver for DidJwkResolver { - type Target = CoreDoc; - async fn resolve(&self, _input: &DidJwk) -> Result { - Ok(CoreDoc {}) - } -} -struct DidWebResolver; -impl Resolver for DidWebResolver { - type Target = CoreDoc; - async fn resolve(&self, _input: &DidWeb) -> Result { - Ok(CoreDoc {}) - } -} - -struct Client { - network: u32, -} - -impl Resolver for Client { - type Target = IotaDoc; - async fn resolve(&self, input: &DidIota) -> Result { - if input.network == self.network { - Ok(IotaDoc {}) - } else { - Err(Error::Generic(anyhow::anyhow!("Invalid network"))) - } - } -} - -#[derive(CompoundResolver)] -struct SuperDidResolver { - #[resolver(DidKey -> CoreDoc)] - did_key: DidKeyResolver, - #[resolver(DidJwk -> CoreDoc)] - did_jwk: DidJwkResolver, - #[resolver(DidWeb -> CoreDoc)] - did_web: DidWebResolver, -} - -#[derive(CompoundResolver)] -struct IdentityClient { - #[resolver(DidKey -> CoreDoc, DidJwk -> CoreDoc, DidWeb -> CoreDoc)] - dids: SuperDidResolver, - #[resolver(DidIota -> IotaDoc if input.network == 0)] - iota: Client, - #[resolver(DidIota -> IotaDoc)] - shimmer: Client, -} - -#[tokio::test] -async fn test_compound_resolver_simple() { - let super_resolver = SuperDidResolver { - did_key: DidKeyResolver {}, - did_jwk: DidJwkResolver {}, - did_web: DidWebResolver {}, - }; - - assert!(super_resolver.resolve(&DidJwk {}).await.is_ok()); -} - -#[tokio::test] -async fn test_compound_resolver_conflicts() { - let super_resolver = SuperDidResolver { - did_key: DidKeyResolver {}, - did_jwk: DidJwkResolver {}, - did_web: DidWebResolver {}, - }; - let identity_client = IdentityClient { - dids: super_resolver, - iota: Client { network: 0}, - shimmer: Client {network: 1}, - }; - - assert!(identity_client.resolve(&DidJwk {}).await.is_ok()); - assert!(identity_client.resolve(&DidIota { network: 1}).await.is_ok()); - assert!(identity_client.resolve(&DidIota { network: 0}).await.is_ok()); -} diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index e6d93b03f0..d28ba767a2 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -4,42 +4,23 @@ version = "1.3.0" authors.workspace = true edition.workspace = true homepage.workspace = true -keywords = ["iota", "did", "identity", "resolver", "resolution"] license.workspace = true -readme = "./README.md" repository.workspace = true rust-version.workspace = true -description = "DID Resolution utilities for the identity.rs library." [dependencies] -# This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. -async-trait = { version = "0.1", default-features = false } -futures = { version = "0.3" } -identity_core = { version = "=1.3.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.3.0", path = "../identity_credential", default-features = false, features = ["validator"] } -identity_did = { version = "=1.3.0", path = "../identity_did", default-features = false } -identity_document = { version = "=1.3.0", path = "../identity_document", default-features = false } -serde = { version = "1.0", default-features = false, features = ["std", "derive"] } -strum.workspace = true -thiserror = { version = "1.0", default-features = false } +anyhow = "1.0.86" +thiserror.workspace = true +identity_iota_core = { version = "=1.3.0", path = "../identity_iota_core", optional = true } +identity_did = { version = "=1.3.0", path = "../identity_did", optional = true } +iota-sdk = { version = "1.1.5", default-features = false, features = ["client"], optional = true } -[dependencies.identity_iota_core] -version = "=1.3.0" -path = "../identity_iota_core" -default-features = false -features = ["send-sync-client-ext", "iota-client"] -optional = true +[lints] +workspace = true [dev-dependencies] -identity_iota_core = { path = "../identity_iota_core", features = ["test"] } -iota-sdk = { version = "1.1.5" } -tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.38.0", features = ["macros", "rt"] } [features] -default = ["revocation-bitmap", "iota"] -revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] -# Enables the IOTA integration for the resolver. -iota = ["dep:identity_iota_core"] - -[lints] -workspace = true +default = ["iota"] +iota = ["dep:iota-sdk", "dep:identity_iota_core", "dep:identity_did"] diff --git a/identity_resolver/README.md b/identity_resolver/README.md deleted file mode 100644 index 4ce84dce0e..0000000000 --- a/identity_resolver/README.md +++ /dev/null @@ -1,4 +0,0 @@ -IOTA Identity - Resolver -=== - -This crate provides a pluggable Resolver implementation that allows for abstracting over the resolution of different DID methods. diff --git a/identity_resolver/src/error.rs b/identity_resolver/src/error.rs index d72a78fd4a..a7e1ddfecf 100644 --- a/identity_resolver/src/error.rs +++ b/identity_resolver/src/error.rs @@ -1,74 +1,13 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/// Alias for a `Result` with the error type [`Error`]. -pub type Result = core::result::Result; - -/// Error returned from the [Resolver's](crate::Resolver) methods. -/// -/// The [`Self::error_cause`](Self::error_cause()) method provides information about the cause of the error. -#[derive(Debug)] -pub struct Error { - error_cause: ErrorCause, -} - -impl Error { - pub(crate) fn new(cause: ErrorCause) -> Self { - Self { error_cause: cause } - } - - /// Returns the cause of the error. - pub fn error_cause(&self) -> &ErrorCause { - &self.error_cause - } - - /// Converts the error into [`ErrorCause`]. - pub fn into_error_cause(self) -> ErrorCause { - self.error_cause - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.error_cause) - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.error_cause.source() - } -} - -/// Error failure modes associated with the methods on the [Resolver's](crate::Resolver). -/// -/// NOTE: This is a "read only error" in the sense that it can only be constructed by the methods in this crate. -#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] -#[non_exhaustive] -pub enum ErrorCause { - /// Caused by a failure to parse a DID string during DID resolution. - #[error("did resolution failed: could not parse the given did")] - #[non_exhaustive] - DIDParsingError { - /// The source of the parsing error. - source: Box, - }, - /// A handler attached to the [`Resolver`](crate::resolution::Resolver) attempted to resolve the DID, but the - /// resolution did not succeed. - #[error("did resolution failed: the attached handler failed")] - #[non_exhaustive] - HandlerError { - /// The source of the handler error. - source: Box, - }, - /// Caused by attempting to resolve a DID whose method does not have a corresponding handler attached to the - /// [`Resolver`](crate::resolution::Resolver). - #[error("did resolution failed: the DID method \"{method}\" is not supported by the resolver")] - UnsupportedMethodError { - /// The method that is unsupported. - method: String, - }, - /// No client attached to the specific network. - #[error("none of the attached clients support the network {0}")] - UnsupportedNetwork(String), +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("The requested item \"{0}\" was not found.")] + NotFound(String), + #[error("Failed to parse the provided input into a resolvable type: {0}")] + ParsingFailure(#[source] anyhow::Error), + #[error(transparent)] + Generic(#[from] anyhow::Error), } diff --git a/identity_resolver/src/iota.rs b/identity_resolver/src/iota.rs new file mode 100644 index 0000000000..533a1f46b8 --- /dev/null +++ b/identity_resolver/src/iota.rs @@ -0,0 +1,64 @@ +use super::Error; +use super::Resolver; +use super::Result; +use identity_did::CoreDID; +use identity_iota_core::Error as IdentityError; +use identity_iota_core::IotaDID; +use identity_iota_core::IotaDocument; +use identity_iota_core::IotaIdentityClientExt; +use iota_sdk::client::node_api::error::Error as IotaApiError; +use iota_sdk::client::Client; +use iota_sdk::client::Error as SdkError; + +impl Resolver for Client { + type Target = IotaDocument; + async fn resolve(&self, did: &IotaDID) -> Result { + self.resolve_did(did).await.map_err(|e| match e { + IdentityError::DIDResolutionError(SdkError::Node(IotaApiError::NotFound(_))) => Error::NotFound(did.to_string()), + e => Error::Generic(e.into()), + }) + } +} + +impl Resolver for Client { + type Target = IotaDocument; + async fn resolve(&self, did: &CoreDID) -> Result { + let iota_did = IotaDID::try_from(did.clone()).map_err(|e| Error::ParsingFailure(e.into()))?; + self.resolve(&iota_did).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Context; + + async fn get_iota_client() -> anyhow::Result { + const API_ENDPOINT: &str = "https://api.stardust-mainnet.iotaledger.net"; + Client::builder() + .with_primary_node(API_ENDPOINT, None)? + .finish() + .await + .context("Failed to create client to iota mainnet") + } + + #[tokio::test] + async fn resolution_of_existing_doc_works() -> anyhow::Result<()> { + let client = get_iota_client().await?; + let did = "did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a".parse::()?; + + assert!(client.resolve(&did).await.is_ok()); + + Ok(()) + } + + #[tokio::test] + async fn resolution_of_non_existing_doc_fails_with_not_found() -> anyhow::Result<()> { + let client = get_iota_client().await?; + let did = "did:iota:0xf4d6f08f5a1b80ee578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a".parse::()?; + + assert!(matches!(client.resolve(&did).await.unwrap_err(), Error::NotFound(_))); + + Ok(()) + } +} diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs index b773741b09..d984f7e052 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -1,23 +1,8 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -#![forbid(unsafe_code)] -#![doc = include_str!("./../README.md")] -#![warn( - rust_2018_idioms, - unreachable_pub, - missing_docs, - rustdoc::missing_crate_level_docs, - rustdoc::broken_intra_doc_links, - rustdoc::private_intra_doc_links, - rustdoc::private_doc_tests, - clippy::missing_safety_doc -)] - mod error; -mod resolution; +#[cfg(feature = "iota")] +mod iota; +mod resolver; -pub use self::error::Error; -pub use self::error::ErrorCause; -pub use self::error::Result; -pub use resolution::*; +pub use error::Error; +pub use error::Result; +pub use resolver::Resolver; diff --git a/identity_resolver/src/resolution/commands.rs b/identity_resolver/src/resolution/commands.rs deleted file mode 100644 index 6dd186a841..0000000000 --- a/identity_resolver/src/resolution/commands.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use core::future::Future; -use identity_did::DID; - -use crate::Error; -use crate::ErrorCause; -use crate::Result; -use std::pin::Pin; - -/// Internal trait used by the resolver to apply the command pattern. -/// -/// The resolver is generic over the type of command which enables -/// support for both multi-threaded and single threaded use cases. -pub trait Command<'a, T>: std::fmt::Debug + private::Sealed { - type Output: Future + 'a; - - fn apply(&self, input: &'a str) -> Self::Output; -} - -mod private { - use super::SendSyncCommand; - use super::SingleThreadedCommand; - pub trait Sealed {} - impl Sealed for SendSyncCommand {} - impl Sealed for SingleThreadedCommand {} -} - -impl std::fmt::Debug for SendSyncCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("") - } -} - -impl std::fmt::Debug for SingleThreadedCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("") - } -} - -/// Internal representation of a thread safe handler. -type SendSyncCallback = - Box Fn(&'r str) -> Pin> + 'r + Send>> + Send + Sync>; - -/// Wrapper around a thread safe callback. -pub struct SendSyncCommand { - fun: SendSyncCallback, -} - -impl<'a, DOC: 'static> Command<'a, Result> for SendSyncCommand { - type Output = Pin> + 'a + Send>>; - fn apply(&self, input: &'a str) -> Self::Output { - (self.fun)(input) - } -} - -impl SendSyncCommand { - /// Converts a handler represented as a closure to a command. - /// - /// This is achieved by first producing a callback represented as a dynamic asynchronous function pointer - /// which is invoked by the [Resolver](crate::Resolver) at a later point. - /// When the callback is invoked the `Resolver` will then pass a DID represented as a string slice which is then - /// converted to the DID type required by the handler and then the handler is called. - pub(super) fn new(handler: F) -> Self - where - D: DID + Send + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static, - DOCUMENT: 'static + Into, - F: Fn(D) -> Fut + 'static + Clone + Send + Sync, - Fut: Future> + Send, - E: Into>, - DIDERR: Into>, - { - let fun: SendSyncCallback = Box::new(move |input: &str| { - let handler_clone: F = handler.clone(); - let did_parse_attempt = D::try_from(input) - .map_err(|error| ErrorCause::DIDParsingError { source: error.into() }) - .map_err(Error::new); - - Box::pin(async move { - let did: D = did_parse_attempt?; - handler_clone(did) - .await - .map(Into::into) - .map_err(|error| ErrorCause::HandlerError { source: error.into() }) - .map_err(Error::new) - }) - }); - - Self { fun } - } -} - -// =========================================================================== -// Single threaded commands -// =========================================================================== - -/// Internal representation of a single threaded handler. -pub(super) type SingleThreadedCallback = - Box Fn(&'r str) -> Pin> + 'r>>>; - -/// Wrapper around a single threaded callback. -pub struct SingleThreadedCommand { - fun: SingleThreadedCallback, -} -impl<'a, DOC: 'static> Command<'a, Result> for SingleThreadedCommand { - type Output = Pin> + 'a>>; - fn apply(&self, input: &'a str) -> Self::Output { - (self.fun)(input) - } -} - -impl SingleThreadedCommand { - /// Equivalent to [`SendSyncCommand::new`](SendSyncCommand::new()), but with less `Send` + `Sync` bounds. - pub(super) fn new(handler: F) -> Self - where - D: DID + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static, - DOCUMENT: 'static + Into, - F: Fn(D) -> Fut + 'static + Clone, - Fut: Future>, - E: Into>, - DIDERR: Into>, - { - let fun: SingleThreadedCallback = Box::new(move |input: &str| { - let handler_clone: F = handler.clone(); - let did_parse_attempt = D::try_from(input) - .map_err(|error| ErrorCause::DIDParsingError { source: error.into() }) - .map_err(Error::new); - - Box::pin(async move { - let did: D = did_parse_attempt?; - handler_clone(did) - .await - .map(Into::into) - .map_err(|error| ErrorCause::HandlerError { source: error.into() }) - .map_err(Error::new) - }) - }); - - Self { fun } - } -} diff --git a/identity_resolver/src/resolution/mod.rs b/identity_resolver/src/resolution/mod.rs deleted file mode 100644 index 06a923d446..0000000000 --- a/identity_resolver/src/resolution/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -mod commands; -mod resolver; -#[cfg(test)] -mod tests; - -use self::commands::SingleThreadedCommand; -use identity_document::document::CoreDocument; - -pub use resolver::Resolver; -/// Alias for a [`Resolver`] that is not [`Send`] + [`Sync`]. -pub type SingleThreadedResolver = Resolver>; diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/resolution/resolver.rs deleted file mode 100644 index b8ceffbc7f..0000000000 --- a/identity_resolver/src/resolution/resolver.rs +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use core::future::Future; -use futures::stream::FuturesUnordered; -use futures::TryStreamExt; -use identity_did::DID; -use std::collections::HashSet; - -use identity_document::document::CoreDocument; -use std::collections::HashMap; -use std::marker::PhantomData; - -use crate::Error; -use crate::ErrorCause; -use crate::Result; - -use super::commands::Command; -use super::commands::SendSyncCommand; -use super::commands::SingleThreadedCommand; - -/// Convenience type for resolving DID documents from different DID methods. -/// -/// # Configuration -/// -/// The resolver will only be able to resolve DID documents for methods it has been configured for. This is done by -/// attaching method specific handlers with [`Self::attach_handler`](Self::attach_handler()). -pub struct Resolver> -where - CMD: for<'r> Command<'r, Result>, -{ - command_map: HashMap, - _required: PhantomData, -} - -impl Resolver -where - M: for<'r> Command<'r, Result>, -{ - /// Constructs a new [`Resolver`]. - /// - /// # Example - /// - /// Construct a `Resolver` that resolves DID documents of type - /// [`CoreDocument`](::identity_document::document::CoreDocument). - /// ``` - /// # use identity_resolver::Resolver; - /// # use identity_document::document::CoreDocument; - /// - /// let mut resolver = Resolver::::new(); - /// // Now attach some handlers whose output can be converted to a `CoreDocument`. - /// ``` - pub fn new() -> Self { - Self { - command_map: HashMap::new(), - _required: PhantomData::, - } - } - - /// Fetches the DID Document of the given DID. - /// - /// # Errors - /// - /// Errors if the resolver has not been configured to handle the method corresponding to the given DID or the - /// resolution process itself fails. - /// - /// ## Example - /// - /// ``` - /// # use identity_resolver::Resolver; - /// # use identity_did::CoreDID; - /// # use identity_document::document::CoreDocument; - /// - /// async fn configure_and_resolve( - /// did: CoreDID, - /// ) -> std::result::Result> { - /// let resolver: Resolver = configure_resolver(Resolver::new()); - /// let resolved_doc: CoreDocument = resolver.resolve(&did).await?; - /// Ok(resolved_doc) - /// } - /// - /// fn configure_resolver(mut resolver: Resolver) -> Resolver { - /// resolver.attach_handler("foo".to_owned(), resolve_foo); - /// // Attach handlers for other DID methods we are interested in. - /// resolver - /// } - /// - /// async fn resolve_foo(did: CoreDID) -> std::result::Result { - /// todo!() - /// } - /// ``` - pub async fn resolve(&self, did: &D) -> Result { - let method: &str = did.method(); - let delegate: &M = self - .command_map - .get(method) - .ok_or_else(|| ErrorCause::UnsupportedMethodError { - method: method.to_owned(), - }) - .map_err(Error::new)?; - - delegate.apply(did.as_str()).await - } - - /// Concurrently fetches the DID Documents of the multiple given DIDs. - /// - /// # Errors - /// * If the resolver has not been configured to handle the method of any of the given DIDs. - /// * If the resolution process of any DID fails. - /// - /// ## Note - /// * If `dids` contains duplicates, these will be resolved only once. - pub async fn resolve_multiple(&self, dids: &[D]) -> Result> { - let futures = FuturesUnordered::new(); - - // Create set to remove duplicates to avoid unnecessary resolution. - let dids_set: HashSet = dids.iter().cloned().collect(); - for did in dids_set { - futures.push(async move { - let doc = self.resolve(&did).await; - doc.map(|doc| (did, doc)) - }); - } - - let documents: HashMap = futures.try_collect().await?; - - Ok(documents) - } -} - -impl Resolver> { - /// Attach a new handler responsible for resolving DIDs of the given DID method. - /// - /// The `handler` is expected to be a closure taking an owned DID and asynchronously returning a DID Document - /// which can be converted to the type this [`Resolver`] is parametrized over. The `handler` is required to be - /// [`Clone`], [`Send`], [`Sync`] and `'static` hence all captured variables must satisfy these bounds. In this regard - /// the `move` keyword and (possibly) wrapping values in an [`Arc`](std::sync::Arc) may come in handy (see the example - /// below). - /// - /// NOTE: If there already exists a handler for this method then it will be replaced with the new handler. - /// In the case where one would like to have a "backup handler" for the same DID method, one can achieve this with - /// composition. - /// - /// # Example - /// ``` - /// # use identity_resolver::Resolver; - /// # use identity_did::CoreDID; - /// # use identity_document::document::CoreDocument; - /// - /// // A client that can resolve DIDs of our invented "foo" method. - /// struct Client; - /// - /// impl Client { - /// // Resolves some of the DIDs we are interested in. - /// async fn resolve(&self, _did: &CoreDID) -> std::result::Result { - /// todo!() - /// } - /// } - /// - /// // This way we can essentially produce (cheap) clones of our client. - /// let client = std::sync::Arc::new(Client {}); - /// - /// // Get a clone we can move into a handler. - /// let client_clone = client.clone(); - /// - /// // Construct a resolver that resolves documents of type `CoreDocument`. - /// let mut resolver = Resolver::::new(); - /// - /// // Now we want to attach a handler that uses the client to resolve DIDs whose method is "foo". - /// resolver.attach_handler("foo".to_owned(), move |did: CoreDID| { - /// // We want to resolve the did asynchronously, but since we do not know when it will be awaited we - /// // let the future take ownership of the client by moving a clone into the asynchronous block. - /// let future_client = client_clone.clone(); - /// async move { future_client.resolve(&did).await } - /// }); - /// ``` - pub fn attach_handler(&mut self, method: String, handler: F) - where - D: DID + Send + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static, - DOCUMENT: 'static + Into, - F: Fn(D) -> Fut + 'static + Clone + Send + Sync, - Fut: Future> + Send, - E: Into>, - DIDERR: Into>, - { - let command = SendSyncCommand::new(handler); - self.command_map.insert(method, command); - } -} - -impl Resolver> { - /// Attach a new handler responsible for resolving DIDs of the given DID method. - /// - /// The `handler` is expected to be a closure taking an owned DID and asynchronously returning a DID Document - /// which can be converted to the type this [`Resolver`] is parametrized over. The `handler` is required to be - /// [`Clone`] and `'static` hence all captured variables must satisfy these bounds. In this regard the - /// `move` keyword and (possibly) wrapping values in an [`std::rc::Rc`] may come in handy (see the example below). - /// - /// NOTE: If there already exists a handler for this method then it will be replaced with the new handler. - /// In the case where one would like to have a "backup handler" for the same DID method, one can achieve this with - /// composition. - /// - /// # Example - /// ``` - /// # use identity_resolver::SingleThreadedResolver; - /// # use identity_did::CoreDID; - /// # use identity_document::document::CoreDocument; - /// - /// // A client that can resolve DIDs of our invented "foo" method. - /// struct Client; - /// - /// impl Client { - /// // Resolves some of the DIDs we are interested in. - /// async fn resolve(&self, _did: &CoreDID) -> std::result::Result { - /// todo!() - /// } - /// } - /// - /// // This way we can essentially produce (cheap) clones of our client. - /// let client = std::rc::Rc::new(Client {}); - /// - /// // Get a clone we can move into a handler. - /// let client_clone = client.clone(); - /// - /// // Construct a resolver that resolves documents of type `CoreDocument`. - /// let mut resolver = SingleThreadedResolver::::new(); - /// - /// // Now we want to attach a handler that uses the client to resolve DIDs whose method is "foo". - /// resolver.attach_handler("foo".to_owned(), move |did: CoreDID| { - /// // We want to resolve the did asynchronously, but since we do not know when it will be awaited we - /// // let the future take ownership of the client by moving a clone into the asynchronous block. - /// let future_client = client_clone.clone(); - /// async move { future_client.resolve(&did).await } - /// }); - /// ``` - pub fn attach_handler(&mut self, method: String, handler: F) - where - D: DID + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static, - DOCUMENT: 'static + Into, - F: Fn(D) -> Fut + 'static + Clone, - Fut: Future>, - E: Into>, - DIDERR: Into>, - { - let command = SingleThreadedCommand::new(handler); - self.command_map.insert(method, command); - } -} - -#[cfg(feature = "iota")] -mod iota_handler { - use crate::ErrorCause; - - use super::Resolver; - use identity_document::document::CoreDocument; - use identity_iota_core::IotaDID; - use identity_iota_core::IotaDocument; - use identity_iota_core::IotaIdentityClientExt; - use std::collections::HashMap; - use std::sync::Arc; - - impl Resolver - where - DOC: From + AsRef + 'static, - { - /// Convenience method for attaching a new handler responsible for resolving IOTA DIDs. - /// - /// See also [`attach_handler`](Self::attach_handler). - pub fn attach_iota_handler(&mut self, client: CLI) - where - CLI: IotaIdentityClientExt + Send + Sync + 'static, - { - let arc_client: Arc = Arc::new(client); - - let handler = move |did: IotaDID| { - let future_client = arc_client.clone(); - async move { future_client.resolve_did(&did).await } - }; - - self.attach_handler(IotaDID::METHOD.to_owned(), handler); - } - - /// Convenience method for attaching multiple handlers responsible for resolving IOTA DIDs - /// on multiple networks. - /// - /// - /// # Arguments - /// - /// * `clients` - A collection of tuples where each tuple contains the name of the network name and its - /// corresponding client. - /// - /// # Examples - /// - /// ```ignore - /// // Assume `smr_client` and `iota_client` are instances IOTA clients `iota_sdk::client::Client`. - /// attach_multiple_iota_handlers(vec![("smr", smr_client), ("iota", iota_client)]); - /// ``` - /// - /// # See Also - /// - [`attach_handler`](Self::attach_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()`. - pub fn attach_multiple_iota_handlers(&mut self, clients: I) - where - CLI: IotaIdentityClientExt + Send + Sync + 'static, - I: IntoIterator, - { - let arc_clients = Arc::new(clients.into_iter().collect::>()); - - let handler = move |did: IotaDID| { - let future_client = arc_clients.clone(); - async move { - let did_network = did.network_str(); - let client: &CLI = - future_client - .get(did_network) - .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( - did_network.to_string(), - )))?; - client - .resolve_did(&did) - .await - .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) - } - }; - - self.attach_handler(IotaDID::METHOD.to_owned(), handler); - } - } -} - -impl Default for Resolver -where - CMD: for<'r> Command<'r, Result>, - DOC: AsRef, -{ - fn default() -> Self { - Self::new() - } -} - -impl std::fmt::Debug for Resolver -where - CMD: for<'r> Command<'r, Result>, - DOC: AsRef, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Resolver") - .field("command_map", &self.command_map) - .finish() - } -} - -#[cfg(test)] -mod tests { - use identity_iota_core::block::output::AliasId; - use identity_iota_core::block::output::AliasOutput; - use identity_iota_core::block::output::OutputId; - use identity_iota_core::block::protocol::ProtocolParameters; - use identity_iota_core::IotaDID; - use identity_iota_core::IotaDocument; - use identity_iota_core::IotaIdentityClient; - use identity_iota_core::IotaIdentityClientExt; - - use super::*; - - struct DummyClient(IotaDocument); - - #[async_trait::async_trait] - impl IotaIdentityClient for DummyClient { - async fn get_alias_output(&self, _id: AliasId) -> identity_iota_core::Result<(OutputId, AliasOutput)> { - unreachable!() - } - async fn get_protocol_parameters(&self) -> identity_iota_core::Result { - unreachable!() - } - } - - #[async_trait::async_trait] - impl IotaIdentityClientExt for DummyClient { - async fn resolve_did(&self, did: &IotaDID) -> identity_iota_core::Result { - if self.0.id().as_str() == did.as_str() { - Ok(self.0.clone()) - } else { - Err(identity_iota_core::Error::DIDResolutionError( - iota_sdk::client::error::Error::NoOutput(did.to_string()), - )) - } - } - } - - #[tokio::test] - async fn test_multiple_handlers() { - let did1 = - IotaDID::parse("did:iota:smr:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap(); - let document = IotaDocument::new_with_id(did1.clone()); - let dummy_smr_client = DummyClient(document); - - let did2 = IotaDID::parse("did:iota:0x0101010101010101010101010101010101010101010101010101010101010101").unwrap(); - let document = IotaDocument::new_with_id(did2.clone()); - let dummy_iota_client = DummyClient(document); - - let mut resolver = Resolver::::new(); - resolver.attach_multiple_iota_handlers(vec![("iota", dummy_iota_client), ("smr", dummy_smr_client)]); - - let doc = resolver.resolve(&did1).await.unwrap(); - assert_eq!(doc.id(), &did1); - - let doc = resolver.resolve(&did2).await.unwrap(); - assert_eq!(doc.id(), &did2); - } -} diff --git a/identity_resolver/src/resolution/tests/mod.rs b/identity_resolver/src/resolution/tests/mod.rs deleted file mode 100644 index 082def4c42..0000000000 --- a/identity_resolver/src/resolution/tests/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use super::resolver::*; -mod resolution; -mod send_sync; diff --git a/identity_resolver/src/resolution/tests/resolution.rs b/identity_resolver/src/resolution/tests/resolution.rs deleted file mode 100644 index ce68fd4b9b..0000000000 --- a/identity_resolver/src/resolution/tests/resolution.rs +++ /dev/null @@ -1,281 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use std::collections::HashMap; -use std::error::Error; -use std::fmt::Debug; -use std::str::FromStr; - -use identity_did::BaseDIDUrl; -use identity_did::CoreDID; -use identity_did::Error as DIDError; -use identity_did::DID; -use identity_document::document::CoreDocument; -use identity_document::document::DocumentBuilder; - -use crate::Error as ResolverError; -use crate::ErrorCause; -use crate::Resolver; - -/// A very simple handler -async fn mock_handler(did: CoreDID) -> std::result::Result { - Ok(core_document(did)) -} - -/// Create a [`CoreDocument`] -fn core_document(did: CoreDID) -> CoreDocument { - DocumentBuilder::default().id(did).build().unwrap() -} - -/// A custom document type -#[derive(Debug, Clone)] -struct FooDocument(CoreDocument); -impl AsRef for FooDocument { - fn as_ref(&self) -> &CoreDocument { - &self.0 - } -} -impl From for FooDocument { - fn from(value: CoreDocument) -> Self { - Self(value) - } -} - -// =========================================================================== -// Missing handler for DID method failure tests -// =========================================================================== -#[tokio::test] -async fn missing_handler_errors() { - let method_name: String = "foo".to_owned(); - let bad_did: CoreDID = CoreDID::parse(format!("did:{method_name}:1234")).unwrap(); - let other_method: String = "bar".to_owned(); - let good_did: CoreDID = CoreDID::parse(format!("did:{other_method}:1234")).unwrap(); - - // configure `resolver` to resolve the "bar" method - let mut resolver_foo: Resolver = Resolver::new(); - let mut resolver_core: Resolver = Resolver::new(); - resolver_foo.attach_handler(other_method.clone(), mock_handler); - resolver_core.attach_handler(other_method, mock_handler); - - let err: ResolverError = resolver_foo.resolve(&bad_did).await.unwrap_err(); - let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { - unreachable!() - }; - assert_eq!(method_name, method); - - let err: ResolverError = resolver_core.resolve(&bad_did).await.unwrap_err(); - let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { - unreachable!() - }; - assert_eq!(method_name, method); - - assert!(resolver_foo.resolve(&good_did).await.is_ok()); - assert!(resolver_core.resolve(&good_did).await.is_ok()); - - let both_dids = [good_did, bad_did]; - let err: ResolverError = resolver_foo.resolve_multiple(&both_dids).await.unwrap_err(); - let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { - unreachable!() - }; - assert_eq!(method_name, method); - - let err: ResolverError = resolver_core.resolve_multiple(&both_dids).await.unwrap_err(); - let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { - unreachable!() - }; - assert_eq!(method_name, method); -} - -// =========================================================================== -// DID Parsing failure tests -// =========================================================================== - -// Implement the DID trait for a new type -#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Clone)] -struct FooDID(CoreDID); - -impl FooDID { - const METHOD_ID_LENGTH: usize = 5; - - fn try_from_core(did: CoreDID) -> std::result::Result { - Some(did) - .filter(|did| did.method() == "foo" && did.method_id().len() == FooDID::METHOD_ID_LENGTH) - .map(Self) - .ok_or(DIDError::InvalidMethodName) - } -} - -impl AsRef for FooDID { - fn as_ref(&self) -> &CoreDID { - &self.0 - } -} - -impl From for String { - fn from(did: FooDID) -> Self { - String::from(did.0) - } -} -impl From for CoreDID { - fn from(value: FooDID) -> Self { - value.0 - } -} - -impl TryFrom for FooDID { - type Error = DIDError; - fn try_from(value: CoreDID) -> Result { - Self::try_from_core(value) - } -} - -impl FromStr for FooDID { - type Err = DIDError; - fn from_str(s: &str) -> Result { - CoreDID::from_str(s).and_then(Self::try_from_core) - } -} - -impl TryFrom for FooDID { - type Error = DIDError; - fn try_from(value: BaseDIDUrl) -> Result { - CoreDID::try_from(value).and_then(Self::try_from_core) - } -} - -impl<'a> TryFrom<&'a str> for FooDID { - type Error = DIDError; - fn try_from(value: &'a str) -> Result { - CoreDID::try_from(value).and_then(Self::try_from_core) - } -} - -#[tokio::test] -async fn resolve_unparsable() { - let mut resolver_foo: Resolver = Resolver::new(); - let mut resolver_core: Resolver = Resolver::new(); - - // register a handler that wants `did` to be of type `FooDID`. - async fn handler(did: FooDID) -> std::result::Result { - mock_handler(did.as_ref().clone()).await - } - - resolver_foo.attach_handler("foo".to_owned(), handler); - resolver_core.attach_handler("foo".to_owned(), handler); - - let bad_did: CoreDID = CoreDID::parse("did:foo:1234").unwrap(); - // ensure that the DID we created does not satisfy the requirements of the "foo" method - assert!(bad_did.method_id().len() < FooDID::METHOD_ID_LENGTH); - - let good_did: FooDID = FooDID::try_from("did:foo:12345").unwrap(); - - let error_matcher = |err: ErrorCause| { - assert!(matches!( - err - .source() - .unwrap() - .downcast_ref::() - .unwrap_or_else(|| panic!("{:?}", &err)), - &DIDError::InvalidMethodName - )); - - match err { - ErrorCause::DIDParsingError { .. } => {} - _ => unreachable!(), - } - }; - - let err_cause: ErrorCause = resolver_foo.resolve(&bad_did).await.unwrap_err().into_error_cause(); - error_matcher(err_cause); - - let err_cause: ErrorCause = resolver_core.resolve(&bad_did).await.unwrap_err().into_error_cause(); - error_matcher(err_cause); - - assert!(resolver_foo.resolve(&good_did).await.is_ok()); - assert!(resolver_core.resolve(&good_did).await.is_ok()); -} - -// =========================================================================== -// Failing handler tests -// =========================================================================== - -#[tokio::test] -async fn handler_failure() { - #[derive(Debug, thiserror::Error)] - #[error("resolution failed")] - struct ResolutionError; - async fn failing_handler(_did: CoreDID) -> std::result::Result { - Err(ResolutionError) - } - - let mut resolver_foo: Resolver = Resolver::new(); - let mut resolver_core: Resolver = Resolver::new(); - resolver_foo.attach_handler("foo".to_owned(), failing_handler); - resolver_core.attach_handler("foo".to_owned(), failing_handler); - - let bad_did: CoreDID = CoreDID::parse("did:foo:1234").unwrap(); - let good_did: CoreDID = CoreDID::parse("did:bar:1234").unwrap(); - resolver_foo.attach_handler(good_did.method().to_owned(), mock_handler); - resolver_core.attach_handler(good_did.method().to_owned(), mock_handler); - - // to avoid boiler plate - let error_matcher = |err: ErrorCause| { - assert!(err.source().unwrap().downcast_ref::().is_some()); - match err { - ErrorCause::HandlerError { .. } => {} - _ => unreachable!(), - } - }; - - let err_cause: ErrorCause = resolver_foo.resolve(&bad_did).await.unwrap_err().into_error_cause(); - error_matcher(err_cause); - - let err_cause: ErrorCause = resolver_core.resolve(&bad_did).await.unwrap_err().into_error_cause(); - error_matcher(err_cause); - - assert!(resolver_foo.resolve(&good_did).await.is_ok()); - assert!(resolver_core.resolve(&good_did).await.is_ok()); -} - -// =========================================================================== -// Resolve Multiple. -// =========================================================================== - -#[tokio::test] -async fn resolve_multiple() { - let method_name: String = "foo".to_owned(); - - let did_1: CoreDID = CoreDID::parse(format!("did:{method_name}:1111")).unwrap(); - let did_2: CoreDID = CoreDID::parse(format!("did:{method_name}:2222")).unwrap(); - let did_3: CoreDID = CoreDID::parse(format!("did:{method_name}:3333")).unwrap(); - let did_1_clone: CoreDID = CoreDID::parse(format!("did:{method_name}:1111")).unwrap(); - - let mut resolver: Resolver = Resolver::new(); - resolver.attach_handler(method_name, mock_handler); - - // Resolve with duplicate `did_3`. - let resolved_dids: HashMap = resolver - .resolve_multiple(&[ - did_1.clone(), - did_2.clone(), - did_3.clone(), - did_1_clone.clone(), - did_3.clone(), - ]) - .await - .unwrap(); - - assert_eq!(resolved_dids.len(), 3); - assert_eq!(resolved_dids.get(&did_1).unwrap().id(), &did_1); - assert_eq!(resolved_dids.get(&did_2).unwrap().id(), &did_2); - assert_eq!(resolved_dids.get(&did_3).unwrap().id(), &did_3); - assert_eq!(resolved_dids.get(&did_1_clone).unwrap().id(), &did_1_clone); - - let dids: &[CoreDID] = &[]; - let resolved_dids: HashMap = resolver.resolve_multiple(dids).await.unwrap(); - assert_eq!(resolved_dids.len(), 0); - - let resolved_dids: HashMap = resolver.resolve_multiple(&[did_1.clone()]).await.unwrap(); - assert_eq!(resolved_dids.len(), 1); - assert_eq!(resolved_dids.get(&did_1).unwrap().id(), &did_1); -} diff --git a/identity_resolver/src/resolution/tests/send_sync.rs b/identity_resolver/src/resolution/tests/send_sync.rs deleted file mode 100644 index 99d8ef0fe4..0000000000 --- a/identity_resolver/src/resolution/tests/send_sync.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020-2023 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -use super::*; - -use identity_did::DID; -use identity_document::document::CoreDocument; - -fn is_send(_t: T) {} -fn is_send_sync(_t: T) {} - -#[allow(dead_code)] -fn default_resolver_is_send_sync + Send + Sync + 'static>() { - let resolver = Resolver::::new(); - is_send_sync(resolver); -} - -#[allow(dead_code)] -fn resolver_methods_give_send_futures(did: D) -where - DOC: AsRef + Send + Sync + 'static, - D: DID + Send + Sync + 'static, -{ - let resolver = Resolver::::new(); - is_send(resolver.resolve(&did)); -} diff --git a/identity_resolver/src/resolver.rs b/identity_resolver/src/resolver.rs new file mode 100644 index 0000000000..daf600239e --- /dev/null +++ b/identity_resolver/src/resolver.rs @@ -0,0 +1,17 @@ +#![allow(async_fn_in_trait)] + +use crate::Result; + +pub trait Resolver { + type Target; + async fn resolve(&self, input: &I) -> Result; + async fn resolve_multiple(&self, inputs: impl AsRef<[I]>) -> Result> { + let mut results = Vec::::with_capacity(inputs.as_ref().len()); + for input in inputs.as_ref() { + let result = self.resolve(input).await?; + results.push(result); + } + + Ok(results) + } +} From 7c815c5b53ba6d4bbef8b37919e44a894f6a9dfd Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 16 Sep 2024 14:54:35 +0200 Subject: [PATCH 06/29] feature gate resolver-v2 --- .../11_linked_verifiable_presentation.rs | 8 +- examples/Cargo.toml | 2 +- identity_iota/Cargo.toml | 4 + identity_iota/src/lib.rs | 1 + identity_resolver/Cargo.toml | 4 +- identity_resolver/src/legacy/commands.rs | 141 ++++++++++++++++++ identity_resolver/src/legacy/error.rs | 75 ++++++++++ identity_resolver/src/legacy/mod.rs | 15 ++ .../src/{resolution => legacy}/resolver.rs | 11 +- identity_resolver/src/lib.rs | 28 +++- 10 files changed, 268 insertions(+), 21 deletions(-) create mode 100644 identity_resolver/src/legacy/commands.rs create mode 100644 identity_resolver/src/legacy/error.rs create mode 100644 identity_resolver/src/legacy/mod.rs rename identity_resolver/src/{resolution => legacy}/resolver.rs (98%) diff --git a/examples/1_advanced/11_linked_verifiable_presentation.rs b/examples/1_advanced/11_linked_verifiable_presentation.rs index 550bad3d41..066f7b9123 100644 --- a/examples/1_advanced/11_linked_verifiable_presentation.rs +++ b/examples/1_advanced/11_linked_verifiable_presentation.rs @@ -93,12 +93,8 @@ async fn main() -> anyhow::Result<()> { // 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?; + let did_document: IotaDocument = client.resolve(&did).await?; // Get the Linked Verifiable Presentation Services from the DID Document. let linked_verifiable_presentation_services: Vec = did_document @@ -121,7 +117,7 @@ async fn main() -> anyhow::Result<()> { // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; - let holder: IotaDocument = resolver.resolve(&holder_did).await?; + let holder: IotaDocument = client.resolve(&holder_did).await?; // Validate linked presentation. Note that this doesn't validate the included credentials. let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default(); diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 9866115ad3..2081164038 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", "resolver"] } +identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus", "resolver-v2"] } 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 diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 6ba8f5d7aa..12964a40e5 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true description = "Framework for Self-Sovereign Identity with IOTA DID." [dependencies] +compound_resolver = { path = "../compound_resolver", optional = true } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } identity_credential = { version = "=1.3.1", path = "../identity_credential", features = ["validator"], default-features = false } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } @@ -48,6 +49,9 @@ status-list-2021 = ["revocation-bitmap", "identity_credential/status-list-2021"] # Enables support for the `Resolver`. resolver = [] +# Enable support for the new resolver interface. +resolver-v2 = ["dep:compound_resolver", "identity_resolver/v2", "resolver"] + # Enables `Send` + `Sync` bounds for the storage traits. send-sync-storage = ["identity_storage/send-sync-storage"] diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index d84eb2765f..5d1e9806aa 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -95,6 +95,7 @@ pub mod prelude { pub mod resolver { //! DID resolution utilities + #[cfg(feature = "resolver-v2")] pub use compound_resolver::CompoundResolver; pub use identity_resolver::*; } diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 165c09ab5d..88f1102057 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -11,6 +11,7 @@ rust-version.workspace = true [dependencies] anyhow = "1.0.86" thiserror.workspace = true +iota-sdk = { version = "1.1.5" } # This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } @@ -30,11 +31,12 @@ optional = true [dev-dependencies] identity_iota_core = { path = "../identity_iota_core", features = ["test"] } -iota-sdk = { version = "1.1.5" } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } [features] default = ["revocation-bitmap", "iota"] +# Enables the new Resolver interface. +v2 = [] revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] # Enables the IOTA integration for the resolver. iota = ["dep:identity_iota_core"] diff --git a/identity_resolver/src/legacy/commands.rs b/identity_resolver/src/legacy/commands.rs new file mode 100644 index 0000000000..676a77f381 --- /dev/null +++ b/identity_resolver/src/legacy/commands.rs @@ -0,0 +1,141 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use core::future::Future; +use identity_did::DID; + +use super::error::*; +use std::pin::Pin; + +/// Internal trait used by the resolver to apply the command pattern. +/// +/// The resolver is generic over the type of command which enables +/// support for both multi-threaded and single threaded use cases. +pub trait Command<'a, T>: std::fmt::Debug + private::Sealed { + type Output: Future + 'a; + + fn apply(&self, input: &'a str) -> Self::Output; +} + +mod private { + use super::SendSyncCommand; + use super::SingleThreadedCommand; + pub trait Sealed {} + impl Sealed for SendSyncCommand {} + impl Sealed for SingleThreadedCommand {} +} + +impl std::fmt::Debug for SendSyncCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("") + } +} + +impl std::fmt::Debug for SingleThreadedCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("") + } +} + +/// Internal representation of a thread safe handler. +type SendSyncCallback = + Box Fn(&'r str) -> Pin> + 'r + Send>> + Send + Sync>; + +/// Wrapper around a thread safe callback. +pub struct SendSyncCommand { + fun: SendSyncCallback, +} + +impl<'a, DOC: 'static> Command<'a, Result> for SendSyncCommand { + type Output = Pin> + 'a + Send>>; + fn apply(&self, input: &'a str) -> Self::Output { + (self.fun)(input) + } +} + +impl SendSyncCommand { + /// Converts a handler represented as a closure to a command. + /// + /// This is achieved by first producing a callback represented as a dynamic asynchronous function pointer + /// which is invoked by the [Resolver](crate::Resolver) at a later point. + /// When the callback is invoked the `Resolver` will then pass a DID represented as a string slice which is then + /// converted to the DID type required by the handler and then the handler is called. + pub(super) fn new(handler: F) -> Self + where + D: DID + Send + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static, + DOCUMENT: 'static + Into, + F: Fn(D) -> Fut + 'static + Clone + Send + Sync, + Fut: Future> + Send, + E: Into>, + DIDERR: Into>, + { + let fun: SendSyncCallback = Box::new(move |input: &str| { + let handler_clone: F = handler.clone(); + let did_parse_attempt = D::try_from(input) + .map_err(|error| ErrorCause::DIDParsingError { source: error.into() }) + .map_err(Error::new); + + Box::pin(async move { + let did: D = did_parse_attempt?; + handler_clone(did) + .await + .map(Into::into) + .map_err(|error| ErrorCause::HandlerError { source: error.into() }) + .map_err(Error::new) + }) + }); + + Self { fun } + } +} + +// =========================================================================== +// Single threaded commands +// =========================================================================== + +/// Internal representation of a single threaded handler. +pub(super) type SingleThreadedCallback = + Box Fn(&'r str) -> Pin> + 'r>>>; + +/// Wrapper around a single threaded callback. +pub struct SingleThreadedCommand { + fun: SingleThreadedCallback, +} +impl<'a, DOC: 'static> Command<'a, Result> for SingleThreadedCommand { + type Output = Pin> + 'a>>; + fn apply(&self, input: &'a str) -> Self::Output { + (self.fun)(input) + } +} + +impl SingleThreadedCommand { + /// Equivalent to [`SendSyncCommand::new`](SendSyncCommand::new()), but with less `Send` + `Sync` bounds. + pub(super) fn new(handler: F) -> Self + where + D: DID + for<'r> TryFrom<&'r str, Error = DIDERR> + 'static, + DOCUMENT: 'static + Into, + F: Fn(D) -> Fut + 'static + Clone, + Fut: Future>, + E: Into>, + DIDERR: Into>, + { + let fun: SingleThreadedCallback = Box::new(move |input: &str| { + let handler_clone: F = handler.clone(); + let did_parse_attempt = D::try_from(input) + .map_err(|error| ErrorCause::DIDParsingError { source: error.into() }) + .map_err(Error::new); + + Box::pin(async move { + let did: D = did_parse_attempt?; + handler_clone(did) + .await + .map(Into::into) + .map_err(|error| ErrorCause::HandlerError { source: error.into() }) + .map_err(Error::new) + }) + }); + + Self { fun } + } +} + diff --git a/identity_resolver/src/legacy/error.rs b/identity_resolver/src/legacy/error.rs new file mode 100644 index 0000000000..99db7e5381 --- /dev/null +++ b/identity_resolver/src/legacy/error.rs @@ -0,0 +1,75 @@ +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Alias for a `Result` with the error type [`Error`]. +pub type Result = core::result::Result; + +/// Error returned from the [Resolver's](crate::Resolver) methods. +/// +/// The [`Self::error_cause`](Self::error_cause()) method provides information about the cause of the error. +#[derive(Debug)] +pub struct Error { + error_cause: ErrorCause, +} + +impl Error { + pub(crate) fn new(cause: ErrorCause) -> Self { + Self { error_cause: cause } + } + + /// Returns the cause of the error. + pub fn error_cause(&self) -> &ErrorCause { + &self.error_cause + } + + /// Converts the error into [`ErrorCause`]. + pub fn into_error_cause(self) -> ErrorCause { + self.error_cause + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.error_cause) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.error_cause.source() + } +} + +/// Error failure modes associated with the methods on the [Resolver's](crate::Resolver). +/// +/// NOTE: This is a "read only error" in the sense that it can only be constructed by the methods in this crate. +#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] +#[non_exhaustive] +pub enum ErrorCause { + /// Caused by a failure to parse a DID string during DID resolution. + #[error("did resolution failed: could not parse the given did")] + #[non_exhaustive] + DIDParsingError { + /// The source of the parsing error. + source: Box, + }, + /// A handler attached to the [`Resolver`](crate::resolution::Resolver) attempted to resolve the DID, but the + /// resolution did not succeed. + #[error("did resolution failed: the attached handler failed")] + #[non_exhaustive] + HandlerError { + /// The source of the handler error. + source: Box, + }, + /// Caused by attempting to resolve a DID whose method does not have a corresponding handler attached to the + /// [`Resolver`](crate::resolution::Resolver). + #[error("did resolution failed: the DID method \"{method}\" is not supported by the resolver")] + UnsupportedMethodError { + /// The method that is unsupported. + method: String, + }, + /// No client attached to the specific network. + #[error("none of the attached clients support the network {0}")] + UnsupportedNetwork(String), +} + diff --git a/identity_resolver/src/legacy/mod.rs b/identity_resolver/src/legacy/mod.rs new file mode 100644 index 0000000000..56ef0891cc --- /dev/null +++ b/identity_resolver/src/legacy/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod commands; +mod resolver; +mod error; + +use self::commands::SingleThreadedCommand; +use identity_document::document::CoreDocument; + +pub use resolver::Resolver; +/// Alias for a [`Resolver`] that is not [`Send`] + [`Sync`]. +pub type SingleThreadedResolver = Resolver>; +pub use error::Error; +pub use error::Result; diff --git a/identity_resolver/src/resolution/resolver.rs b/identity_resolver/src/legacy/resolver.rs similarity index 98% rename from identity_resolver/src/resolution/resolver.rs rename to identity_resolver/src/legacy/resolver.rs index 228a65582b..9a675a3b8e 100644 --- a/identity_resolver/src/resolution/resolver.rs +++ b/identity_resolver/src/legacy/resolver.rs @@ -12,9 +12,7 @@ use identity_document::document::CoreDocument; use std::collections::HashMap; use std::marker::PhantomData; -use crate::Error; -use crate::ErrorCause; -use crate::Result; +use super::error::*; use super::commands::Command; use super::commands::SendSyncCommand; @@ -266,8 +264,7 @@ impl + 'static> Resolver> { #[cfg(feature = "iota")] mod iota_handler { - use crate::ErrorCause; - + use super::ErrorCause; use super::Resolver; use identity_document::document::CoreDocument; use identity_iota_core::IotaDID; @@ -336,13 +333,13 @@ mod iota_handler { let client: &CLI = future_client .get(did_network) - .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( + .ok_or(super::Error::new(ErrorCause::UnsupportedNetwork( did_network.to_string(), )))?; client .resolve_did(&did) .await - .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) + .map_err(|err| super::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) } }; diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs index d984f7e052..7d5bf1d8d7 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -1,8 +1,24 @@ -mod error; -#[cfg(feature = "iota")] +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "v2")] +#[path = ""] +mod v2 { + mod error; + mod resolver; + + pub use error::Error; + pub use error::Result; + pub use resolver::Resolver; +} + +#[cfg(all(feature = "iota", feature = "v2"))] mod iota; -mod resolver; -pub use error::Error; -pub use error::Result; -pub use resolver::Resolver; +#[cfg(not(feature = "v2"))] +mod legacy; +#[cfg(not(feature = "v2"))] +pub use legacy::*; + +#[cfg(feature = "v2")] +pub use v2::*; From b28fa9a47bd8b8d11838b8b50435357c9a9ae86b Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 17 Sep 2024 18:09:01 +0200 Subject: [PATCH 07/29] structures & basic operations --- identity_core/src/common/mod.rs | 2 + identity_core/src/common/string_or_url.rs | 139 +++++++++++++++ identity_credential/Cargo.toml | 4 +- identity_credential/src/lib.rs | 7 + identity_credential/src/sd_jwt_vc/claims.rs | 188 ++++++++++++++++++++ identity_credential/src/sd_jwt_vc/error.rs | 36 ++++ identity_credential/src/sd_jwt_vc/mod.rs | 13 ++ identity_credential/src/sd_jwt_vc/status.rs | 49 +++++ identity_credential/src/sd_jwt_vc/token.rs | 115 ++++++++++++ 9 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 identity_core/src/common/string_or_url.rs create mode 100644 identity_credential/src/sd_jwt_vc/claims.rs create mode 100644 identity_credential/src/sd_jwt_vc/error.rs create mode 100644 identity_credential/src/sd_jwt_vc/mod.rs create mode 100644 identity_credential/src/sd_jwt_vc/status.rs create mode 100644 identity_credential/src/sd_jwt_vc/token.rs diff --git a/identity_core/src/common/mod.rs b/identity_core/src/common/mod.rs index 8d6be52251..0b6049504f 100644 --- a/identity_core/src/common/mod.rs +++ b/identity_core/src/common/mod.rs @@ -14,6 +14,7 @@ pub use self::single_struct_error::*; pub use self::timestamp::Duration; pub use self::timestamp::Timestamp; pub use self::url::Url; +pub use string_or_url::StringOrUrl; mod context; mod key_comparable; @@ -24,3 +25,4 @@ mod ordered_set; mod single_struct_error; mod timestamp; mod url; +mod string_or_url; diff --git a/identity_core/src/common/string_or_url.rs b/identity_core/src/common/string_or_url.rs new file mode 100644 index 0000000000..8a3019e515 --- /dev/null +++ b/identity_core/src/common/string_or_url.rs @@ -0,0 +1,139 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::{fmt::Display, str::FromStr}; + +use serde::{Deserialize, Serialize}; + +use super::Url; + +/// A type that represents either an arbitrary string or a URL. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(untagged)] +pub enum StringOrUrl { + /// A well-formed URL. + Url(Url), + /// An arbitrary UTF-8 string. + String(String), +} + +impl StringOrUrl { + /// Parses a [`StringOrUrl`] from a string. + pub fn parse(s: &str) -> Result { + s.parse() + } + /// Returns a [`Url`] reference if `self` is [`StringOrUrl::Url`]. + pub fn as_url(&self) -> Option<&Url> { + match self { + Self::Url(url) => Some(url), + _ => None, + } + } + + /// Returns a [`str`] reference if `self` is [`StringOrUrl::String`]. + pub fn as_string(&self) -> Option<&str> { + match self { + Self::String(s) => Some(s), + _ => None, + } + } + + /// Returns whether `self` is a [`StringOrUrl::Url`]. + pub fn is_url(&self) -> bool { + matches!(self, Self::Url(_)) + } + + /// Returns whether `self` is a [`StringOrUrl::String`]. + pub fn is_string(&self) -> bool { + matches!(self, Self::String(_)) + } +} + +impl Default for StringOrUrl { + fn default() -> Self { + StringOrUrl::String(String::default()) + } +} + +impl Display for StringOrUrl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Url(url) => write!(f, "{url}"), + Self::String(s) => write!(f, "{s}"), + } + } +} + +impl FromStr for StringOrUrl { + // Cannot fail. + type Err = (); + fn from_str(s: &str) -> Result { + Ok( + s.parse::() + .map(Self::Url) + .unwrap_or_else(|_| Self::String(s.to_string())), + ) + } +} + +impl AsRef for StringOrUrl { + fn as_ref(&self) -> &str { + match self { + Self::String(s) => s, + Self::Url(url) => url.as_str(), + } + } +} + +impl From for StringOrUrl { + fn from(value: Url) -> Self { + Self::Url(value) + } +} + +impl From for StringOrUrl { + fn from(value: String) -> Self { + Self::String(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Serialize, Deserialize)] + struct TestData { + string_or_url: StringOrUrl, + } + + impl Default for TestData { + fn default() -> Self { + Self { + string_or_url: StringOrUrl::Url(TEST_URL.parse().unwrap()), + } + } + } + + const TEST_URL: &str = "file:///tmp/file.txt"; + + #[test] + fn deserialization_works() { + let test_data: TestData = serde_json::from_value(serde_json::json!({ "string_or_url": TEST_URL })).unwrap(); + let target_url: Url = TEST_URL.parse().unwrap(); + assert_eq!(test_data.string_or_url.as_url(), Some(&target_url)); + } + + #[test] + fn serialization_works() { + assert_eq!( + serde_json::to_value(TestData::default()).unwrap(), + serde_json::json!({ "string_or_url": TEST_URL }) + ) + } + + #[test] + fn parsing_works() { + assert!(TEST_URL.parse::().unwrap().is_url()); + assert!("I'm a random string :)".parse::().unwrap().is_string()); + } +} diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index aaba6c974e..02608a771c 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -27,6 +27,7 @@ once_cell = { version = "1.18", default-features = false, features = ["std"] } reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true } +sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", branch = "feat/sd-jwt-v11", default-features = false, features = ["sha"], optional = true } serde.workspace = true serde-aux = { version = "4.3.1", default-features = false } serde_json.workspace = true @@ -50,7 +51,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["revocation-bitmap", "validator", "credential", "presentation", "domain-linkage-fetch", "sd-jwt"] +default = ["revocation-bitmap", "validator", "credential", "presentation", "domain-linkage-fetch", "sd-jwt", "sd-jwt-vc"] credential = [] presentation = ["credential"] revocation-bitmap = ["dep:flate2", "dep:roaring"] @@ -59,6 +60,7 @@ validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] +sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework"] jpt-bbs-plus = ["credential", "validator", "dep:zkryptium", "dep:bls12_381_plus", "dep:json-proof-token"] [lints] diff --git a/identity_credential/src/lib.rs b/identity_credential/src/lib.rs index 3111b72e0a..3c03954b85 100644 --- a/identity_credential/src/lib.rs +++ b/identity_credential/src/lib.rs @@ -27,8 +27,15 @@ mod utils; #[cfg(feature = "validator")] pub mod validator; +/// Implementation of the SD-JWT VC token specification. +// #[cfg(feature = "sd-jwt-vc")] +pub mod sd_jwt_vc; + pub use error::Error; pub use error::Result; #[cfg(feature = "sd-jwt")] pub use sd_jwt_payload; + +#[cfg(feature = "sd-jwt-vc")] +pub use sd_jwt_payload_rework as sd_jwt_v2; diff --git a/identity_credential/src/sd_jwt_vc/claims.rs b/identity_credential/src/sd_jwt_vc/claims.rs new file mode 100644 index 0000000000..83f7cf11d8 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/claims.rs @@ -0,0 +1,188 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; +use std::ops::DerefMut; + +use identity_core::common::StringOrUrl; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use sd_jwt_payload_rework::Disclosure; +use sd_jwt_payload_rework::SdJwtClaims; + +use super::Error; +use super::Result; +use super::Status; + +/// JOSE payload claims for SD-JWT VC. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct SdJwtVcClaims { + /// Issuer. + pub iss: Url, + /// Not before. + /// See [RFC7519 section 4.1.5](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5) for more information. + pub nbf: Option, + /// Expiration. + /// See [RFC7519 section 4.1.4](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.4) for more information. + pub exp: Option, + /// Verifiable credential type. + /// See [SD-JWT VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html#type-claim) + /// for more information. + pub vct: StringOrUrl, + /// Token's status. + /// See [OAuth status list specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-02) + /// for more information. + pub status: Option, + /// Issued at. + /// See [RFC7519 section 4.1.6](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.6) for more information. + pub iat: Option, + /// Subject. + /// See [RFC7519 section 4.1.2](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2) for more information. + pub sub: Option, + sd_jwt_claims: SdJwtClaims, +} + +impl Deref for SdJwtVcClaims { + type Target = SdJwtClaims; + fn deref(&self) -> &Self::Target { + &self.sd_jwt_claims + } +} + +impl DerefMut for SdJwtVcClaims { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sd_jwt_claims + } +} + +impl SdJwtVcClaims { + pub(crate) fn try_from_sd_jwt_claims(mut claims: SdJwtClaims, disclosures: &[Disclosure]) -> Result { + let check_disclosed = |claim_name: &'static str| { + disclosures + .iter() + .any(|disclosure| disclosure.claim_name.as_deref() == Some(claim_name)) + .then_some(Error::DisclosedClaim(claim_name)) + }; + let iss = claims + .remove("iss") + .ok_or(Error::MissingClaim("iss")) + .map_err(|e| check_disclosed("iss").unwrap_or(e)) + .and_then(|value| { + value + .as_str() + .and_then(|s| Url::parse(s).ok()) + .ok_or_else(|| Error::InvalidClaimValue { + name: "iss", + expected: "URL", + found: value, + }) + })?; + let nbf = { + if let Some(value) = claims.remove("nbf") { + value + .as_number() + .and_then(|t| t.as_i64()) + .and_then(|t| Timestamp::from_unix(t).ok()) + .ok_or_else(|| Error::InvalidClaimValue { + name: "nbf", + expected: "unix timestamp", + found: value, + }) + .map(Some)? + } else { + if let Some(err) = check_disclosed("nbf") { + return Err(err); + } + None + } + }; + let exp = { + if let Some(value) = claims.remove("exp") { + value + .as_number() + .and_then(|t| t.as_i64()) + .and_then(|t| Timestamp::from_unix(t).ok()) + .ok_or_else(|| Error::InvalidClaimValue { + name: "exp", + expected: "unix timestamp", + found: value, + }) + .map(Some)? + } else { + if let Some(err) = check_disclosed("exp") { + return Err(err); + } + None + } + }; + let vct = claims + .remove("vct") + .ok_or(Error::MissingClaim("vct")) + .map_err(|e| check_disclosed("vct").unwrap_or(e)) + .and_then(|value| { + value + .as_str() + .and_then(|s| StringOrUrl::parse(s).ok()) + .ok_or_else(|| Error::InvalidClaimValue { + name: "vct", + expected: "String or URL", + found: value, + }) + })?; + let status = { + if let Some(value) = claims.remove("status") { + serde_json::from_value::(value.clone()) + .map_err(|_| Error::InvalidClaimValue { + name: "status", + expected: "credential's status object", + found: value, + }) + .map(Some)? + } else { + if let Some(err) = check_disclosed("status") { + return Err(err); + } + None + } + }; + let sub = claims + .remove("sub") + .map(|value| { + value + .as_str() + .and_then(|s| StringOrUrl::parse(s).ok()) + .ok_or_else(|| Error::InvalidClaimValue { + name: "sub", + expected: "String or URL", + found: value, + }) + }) + .transpose()?; + let iat = claims + .remove("iat") + .map(|value| { + value + .as_number() + .and_then(|t| t.as_i64()) + .and_then(|t| Timestamp::from_unix(t).ok()) + .ok_or_else(|| Error::InvalidClaimValue { + name: "iat", + expected: "unix timestamp", + found: value, + }) + }) + .transpose()?; + + Ok(Self { + iss, + nbf, + exp, + vct, + status, + iat, + sub, + sd_jwt_claims: claims, + }) + } +} diff --git a/identity_credential/src/sd_jwt_vc/error.rs b/identity_credential/src/sd_jwt_vc/error.rs new file mode 100644 index 0000000000..67d5639812 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/error.rs @@ -0,0 +1,36 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde_json::Value; +use thiserror::Error; + +/// Error type that represents failures that might arise when dealing +/// with `SdJwtVc`s. +#[derive(Error, Debug)] +pub enum Error { + /// A JWT claim required for an operation is missing. + #[error("missing required claim \"{0}\"")] + MissingClaim(&'static str), + /// A JWT claim that must not be disclosed was found among the disclosed values. + #[error("claim \"{0}\" cannot be disclosed")] + DisclosedClaim(&'static str), + /// Invalid value for a given JWT claim. + #[error("invalid value for claim \"{name}\"; expected value of type {expected}, but {found} was found")] + InvalidClaimValue { + /// Name of the invalid claim. + name: &'static str, + /// Type expected for the claim's value. + expected: &'static str, + /// The claim's value. + found: Value, + }, + /// A low level SD-JWT error. + #[error(transparent)] + SdJwt(#[from] sd_jwt_payload_rework::Error), + /// Value of header parameter `typ` is not valid. + #[error("invalid \"typ\" value; expected \"vc+sd-jwt\" (or a superset) but found \"{0}\"")] + InvalidJoseType(String), +} + +/// Either a value of type `T` or an [`Error`]. +pub type Result = std::result::Result; diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs new file mode 100644 index 0000000000..50e3c22d1b --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -0,0 +1,13 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod claims; +mod error; +mod status; +mod token; + +pub use claims::*; +pub use error::Error; +pub use error::Result; +pub use status::*; +pub use token::*; diff --git a/identity_credential/src/sd_jwt_vc/status.rs b/identity_credential/src/sd_jwt_vc/status.rs new file mode 100644 index 0000000000..1738447293 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/status.rs @@ -0,0 +1,49 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Url; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// SD-JWT VC's `status` claim value. Used to retrieve the status of the token. +pub struct Status(StatusMechanism); + +/// Mechanism used for representing the status of an SD-JWT VC token. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[non_exhaustive] +pub enum StatusMechanism { + /// Reference to a status list containing this token's status. + #[serde(rename = "status_list")] + StatusList(StatusListRef), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +/// A reference to an OAuth status list. +/// See [OAuth StatusList specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-status-list-02) +/// for more information. +pub struct StatusListRef { + /// URI of the status list. + pub uri: Url, + /// Index of the entry containing this token's status. + pub idx: usize, +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde_json::json; + + #[test] + fn round_trip() { + let status_value = json!({ + "status_list": { + "idx": 420, + "uri": "https://example.com/statuslists/1" + } + }); + let status: Status = serde_json::from_value(status_value.clone()).unwrap(); + assert_eq!(serde_json::to_value(status).unwrap(), status_value); + } +} diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs new file mode 100644 index 0000000000..f3f02ee986 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -0,0 +1,115 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; +use std::ops::Deref; +use std::str::FromStr; + +use super::claims::SdJwtVcClaims; +use super::Error; +use super::Result; +use sd_jwt_payload_rework::SdJwt; +use serde_json::Value; + +/// SD-JWT VC's JOSE header `typ`'s value. +pub const SD_JWT_VC_TYP: &str = "vc+sd-jwt"; + +#[derive(Debug, Clone, PartialEq, Eq)] +/// An SD-JWT carrying a verifiable credential as described in +/// [SD-JWT VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html). +pub struct SdJwtVc { + sd_jwt: SdJwt, + parsed_claims: SdJwtVcClaims, +} + +impl Deref for SdJwtVc { + type Target = SdJwt; + fn deref(&self) -> &Self::Target { + &self.sd_jwt + } +} + +impl SdJwtVc { + /// Parses a string into an [`SdJwtVc`]. + pub fn parse(s: &str) -> Result { + s.parse() + } + + /// Returns a reference to this [`SdJwtVc`]'s JWT claims. + pub fn claims(&self) -> &SdJwtVcClaims { + &self.parsed_claims + } +} + +impl TryFrom for SdJwtVc { + type Error = Error; + fn try_from(mut sd_jwt: SdJwt) -> std::result::Result { + // Validate claims. + let claims = { + let claims = std::mem::take(sd_jwt.claims_mut()); + SdJwtVcClaims::try_from_sd_jwt_claims(claims, sd_jwt.disclosures())? + }; + + // Validate Header's typ. + let typ = sd_jwt + .header() + .get("typ") + .and_then(Value::as_str) + .ok_or_else(|| Error::InvalidJoseType("null".to_string()))?; + if !typ.contains(SD_JWT_VC_TYP) { + return Err(Error::InvalidJoseType(typ.to_string())); + } + + Ok(Self { + sd_jwt, + parsed_claims: claims, + }) + } +} + +impl FromStr for SdJwtVc { + type Err = Error; + fn from_str(s: &str) -> std::result::Result { + s.parse::().map_err(Error::SdJwt).and_then(TryInto::try_into) + } +} + +impl Display for SdJwtVc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.sd_jwt) + } +} + +#[cfg(test)] +mod tests { + use std::cell::LazyCell; + + use identity_core::common::StringOrUrl; + use identity_core::common::Url; + + use super::*; + + const EXAMPLE_SD_JWT_VC: &str = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCJ9.eyJfc2QiOiBbIjBIWm1uU0lQejMzN2tTV2U3QzM0bC0tODhnekppLWVCSjJWel9ISndBVGciLCAiOVpicGxDN1RkRVc3cWFsNkJCWmxNdHFKZG1lRU9pWGV2ZEpsb1hWSmRSUSIsICJJMDBmY0ZVb0RYQ3VjcDV5eTJ1anFQc3NEVkdhV05pVWxpTnpfYXdEMGdjIiwgIklFQllTSkdOaFhJbHJRbzU4eWtYbTJaeDN5bGw5WmxUdFRvUG8xN1FRaVkiLCAiTGFpNklVNmQ3R1FhZ1hSN0F2R1RyblhnU2xkM3o4RUlnX2Z2M2ZPWjFXZyIsICJodkRYaHdtR2NKUXNCQ0EyT3RqdUxBY3dBTXBEc2FVMG5rb3ZjS09xV05FIiwgImlrdXVyOFE0azhxM1ZjeUE3ZEMtbU5qWkJrUmVEVFUtQ0c0bmlURTdPVFUiLCAicXZ6TkxqMnZoOW80U0VYT2ZNaVlEdXZUeWtkc1dDTmcwd1RkbHIwQUVJTSIsICJ3elcxNWJoQ2t2a3N4VnZ1SjhSRjN4aThpNjRsbjFqb183NkJDMm9hMXVnIiwgInpPZUJYaHh2SVM0WnptUWNMbHhLdUVBT0dHQnlqT3FhMXoySW9WeF9ZRFEiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgInZjdCI6ICJodHRwczovL2JtaS5idW5kLmV4YW1wbGUvY3JlZGVudGlhbC9waWQvMS4wIiwgImFnZV9lcXVhbF9vcl9vdmVyIjogeyJfc2QiOiBbIkZjOElfMDdMT2NnUHdyREpLUXlJR085N3dWc09wbE1Makh2UkM0UjQtV2ciLCAiWEx0TGphZFVXYzl6Tl85aE1KUm9xeTQ2VXNDS2IxSXNoWnV1cVVGS1NDQSIsICJhb0NDenNDN3A0cWhaSUFoX2lkUkNTQ2E2NDF1eWNuYzh6UGZOV3o4bngwIiwgImYxLVAwQTJkS1dhdnYxdUZuTVgyQTctRVh4dmhveHY1YUhodUVJTi1XNjQiLCAiazVoeTJyMDE4dnJzSmpvLVZqZDZnNnl0N0Fhb25Lb25uaXVKOXplbDNqbyIsICJxcDdaX0t5MVlpcDBzWWdETzN6VnVnMk1GdVBOakh4a3NCRG5KWjRhSS1jIl19LCAiX3NkX2FsZyI6ICJzaGEtMjU2IiwgImNuZiI6IHsiandrIjogeyJrdHkiOiAiRUMiLCAiY3J2IjogIlAtMjU2IiwgIngiOiAiVENBRVIxOVp2dTNPSEY0ajRXNHZmU1ZvSElQMUlMaWxEbHM3dkNlR2VtYyIsICJ5IjogIlp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.CaXec2NNooWAy4eTxYbGWI--UeUL0jpC7Zb84PP_09Z655BYcXUTvfj6GPk4mrNqZUU5GT6QntYR8J9rvcBjvA~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d~WyJNMEpiNTd0NDF1YnJrU3V5ckRUM3hBIiwgIjE4IiwgdHJ1ZV0~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE3MjA0NTQyOTUsICJzZF9oYXNoIjogIlZFejN0bEtqOVY0UzU3TTZoRWhvVjRIc19SdmpXZWgzVHN1OTFDbmxuZUkifQ.GqtiTKNe3O95GLpdxFK_2FZULFk6KUscFe7RPk8OeVLiJiHsGvtPyq89e_grBplvGmnDGHoy8JAt1wQqiwktSg"; + const EXAMPLE_ISSUER: LazyCell = LazyCell::new(|| "https://example.com/issuer".parse().unwrap()); + const EXAMPLE_VCT: LazyCell = LazyCell::new(|| { + "https://bmi.bund.example/credential/pid/1.0" + .parse::() + .unwrap() + .into() + }); + + #[test] + fn simple_sd_jwt_is_not_a_valid_sd_jwt_vc() { + let sd_jwt: SdJwt = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1FyazFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZzZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZkx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSLWFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzIiwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTSjFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1cFJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZXd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2bmNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4anZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25VbGRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0MG9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.gR6rSL7urX79CNEvTQnP1MH5xthG11ucIV44SqKFZ4Pvlu_u16RfvXQd4k4CAIBZNKn2aTI18TfvFwV97gJFoA~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~" + .parse().unwrap(); + let err = SdJwtVc::try_from(sd_jwt).unwrap_err(); + assert!(matches!(err, Error::MissingClaim("vct"))) + } + + #[test] + fn parsing_a_valid_sd_jwt_vc_works() { + let sd_jwt_vc: SdJwtVc = EXAMPLE_SD_JWT_VC.parse().unwrap(); + assert_eq!(sd_jwt_vc.claims().iss, *EXAMPLE_ISSUER); + assert_eq!(sd_jwt_vc.claims().vct, *EXAMPLE_VCT); + } +} From 2f7fadf41df16f16ed61db535ebd11576b3bf132 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 17 Sep 2024 19:00:38 +0200 Subject: [PATCH 08/29] SdJwtVc behaves as a superset of SdJwt --- identity_core/src/common/string_or_url.rs | 9 ++++ identity_credential/src/sd_jwt_vc/claims.rs | 26 ++++++++++ identity_credential/src/sd_jwt_vc/mod.rs | 2 + .../src/sd_jwt_vc/presentation.rs | 48 +++++++++++++++++++ identity_credential/src/sd_jwt_vc/token.rs | 41 +++++++++++++++- 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/presentation.rs diff --git a/identity_core/src/common/string_or_url.rs b/identity_core/src/common/string_or_url.rs index 8a3019e515..823dd87016 100644 --- a/identity_core/src/common/string_or_url.rs +++ b/identity_core/src/common/string_or_url.rs @@ -97,6 +97,15 @@ impl From for StringOrUrl { } } +impl From for String { + fn from(value: StringOrUrl) -> Self { + match value { + StringOrUrl::String(s) => s, + StringOrUrl::Url(url) => url.into_string(), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/identity_credential/src/sd_jwt_vc/claims.rs b/identity_credential/src/sd_jwt_vc/claims.rs index 83f7cf11d8..c3ac85b8ed 100644 --- a/identity_credential/src/sd_jwt_vc/claims.rs +++ b/identity_credential/src/sd_jwt_vc/claims.rs @@ -9,6 +9,7 @@ use identity_core::common::Timestamp; use identity_core::common::Url; use sd_jwt_payload_rework::Disclosure; use sd_jwt_payload_rework::SdJwtClaims; +use serde_json::Value; use super::Error; use super::Result; @@ -186,3 +187,28 @@ impl SdJwtVcClaims { }) } } + +impl From for SdJwtClaims { + fn from(claims: SdJwtVcClaims) -> Self { + let SdJwtVcClaims { + iss, + nbf, + exp, + vct, + status, + iat, + sub, + mut sd_jwt_claims, + } = claims; + + sd_jwt_claims.insert("iss".to_string(), Value::String(iss.into_string())); + nbf.and_then(|t| sd_jwt_claims.insert("nbf".to_string(), Value::Number(t.to_unix().into()))); + exp.and_then(|t| sd_jwt_claims.insert("exp".to_string(), Value::Number(t.to_unix().into()))); + sd_jwt_claims.insert("vct".to_string(), Value::String(vct.into())); + status.and_then(|status| sd_jwt_claims.insert("status".to_string(), serde_json::to_value(status).unwrap())); + iat.and_then(|t| sd_jwt_claims.insert("iat".to_string(), Value::Number(t.to_unix().into()))); + sub.and_then(|sub| sd_jwt_claims.insert("sub".to_string(), Value::String(sub.into()))); + + sd_jwt_claims + } +} diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index 50e3c22d1b..f5b45119d1 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -5,8 +5,10 @@ mod claims; mod error; mod status; mod token; +mod presentation; pub use claims::*; +pub use presentation::*; pub use error::Error; pub use error::Result; pub use status::*; diff --git a/identity_credential/src/sd_jwt_vc/presentation.rs b/identity_credential/src/sd_jwt_vc/presentation.rs new file mode 100644 index 0000000000..400b9d84bd --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/presentation.rs @@ -0,0 +1,48 @@ +use super::Error; +use super::Result; +use super::SdJwtVc; +use super::SdJwtVcClaims; + +use sd_jwt_payload_rework::Disclosure; +use sd_jwt_payload_rework::Hasher; +use sd_jwt_payload_rework::KeyBindingJwt; +use sd_jwt_payload_rework::SdJwtPresentationBuilder; + +/// Builder structure to create an SD-JWT VC presentation. +/// It allows users to conceal claims and attach a key binding JWT. +#[derive(Debug, Clone)] +pub struct SdJwtVcPresentationBuilder { + vc_claims: SdJwtVcClaims, + builder: SdJwtPresentationBuilder, +} + +impl SdJwtVcPresentationBuilder { + /// Prepare a presentation for a given [`SdJwtVc`]. + pub fn new(token: SdJwtVc, hasher: &dyn Hasher) -> Result { + let SdJwtVc { sd_jwt, parsed_claims: vc_claims } = token; + let builder = sd_jwt.into_presentation(hasher).map_err(Error::SdJwt)?; + + Ok(Self { vc_claims, builder }) + } + /// Removes the disclosure for the property at `path`, conceiling it. + /// + /// ## Notes + /// - When concealing a claim more than one disclosure may be removed: the disclosure for the claim itself and the + /// disclosures for any concealable sub-claim. + pub fn conceal(mut self, path: &str) -> Result { + self.builder = self.builder.conceal(path).map_err(Error::SdJwt)?; + Ok(self) + } + + /// Adds a [`KeyBindingJwt`] to this [`SdJwtVc`]'s presentation. + pub fn attach_key_binding_jwt(mut self, kb_jwt: KeyBindingJwt) -> Self { + self.builder = self.builder.attach_key_binding_jwt(kb_jwt); + self + } + + /// Returns the resulting [`SdJwtVc`] together with all removed disclosures. + pub fn finish(self) -> (SdJwtVc, Vec) { + let (sd_jwt, disclosures) = self.builder.finish(); + (SdJwtVc::new(sd_jwt, self.vc_claims), disclosures) + } +} diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index f3f02ee986..2dfb285f04 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -8,6 +8,9 @@ use std::str::FromStr; use super::claims::SdJwtVcClaims; use super::Error; use super::Result; +use super::SdJwtVcPresentationBuilder; +use sd_jwt_payload_rework::Hasher; +use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::SdJwt; use serde_json::Value; @@ -18,8 +21,8 @@ pub const SD_JWT_VC_TYP: &str = "vc+sd-jwt"; /// An SD-JWT carrying a verifiable credential as described in /// [SD-JWT VC specification](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html). pub struct SdJwtVc { - sd_jwt: SdJwt, - parsed_claims: SdJwtVcClaims, + pub(crate) sd_jwt: SdJwt, + pub(crate) parsed_claims: SdJwtVcClaims, } impl Deref for SdJwtVc { @@ -30,6 +33,13 @@ impl Deref for SdJwtVc { } impl SdJwtVc { + pub(crate) fn new(sd_jwt: SdJwt, claims: SdJwtVcClaims) -> Self { + Self { + sd_jwt, + parsed_claims: claims, + } + } + /// Parses a string into an [`SdJwtVc`]. pub fn parse(s: &str) -> Result { s.parse() @@ -39,6 +49,20 @@ impl SdJwtVc { pub fn claims(&self) -> &SdJwtVcClaims { &self.parsed_claims } + + /// Prepares this [`SdJwtVc`] for a presentation, returning an [`SdJwtVcPresentationBuilder`]. + /// ## Errors + /// - [`Error::SdJwt`] is returned if the provided `hasher`'s algorithm doesn't match the algorithm specified + /// by SD-JWT's `_sd_alg` claim. "sha-256" is used if the claim is missing. + pub fn into_presentation(self, hasher: &dyn Hasher) -> Result { + SdJwtVcPresentationBuilder::new(self, hasher) + } + + /// Returns the JSON object obtained by replacing all disclosures into their + /// corresponding JWT concealable claims. + pub fn into_disclosed_object(self, hasher: &dyn Hasher) -> Result { + SdJwt::from(self).into_disclosed_object(hasher).map_err(Error::SdJwt) + } } impl TryFrom for SdJwtVc { @@ -80,6 +104,19 @@ impl Display for SdJwtVc { } } +impl From for SdJwt { + fn from(value: SdJwtVc) -> Self { + let SdJwtVc { + mut sd_jwt, + parsed_claims, + } = value; + // Put back `parsed_claims`. + *sd_jwt.claims_mut() = parsed_claims.into(); + + sd_jwt + } +} + #[cfg(test)] mod tests { use std::cell::LazyCell; From 0d3127f178a1f029b246ea76ff10963fbaa28328 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 18 Sep 2024 14:57:01 +0200 Subject: [PATCH 09/29] issuer's metadata fetching & validation --- identity_credential/Cargo.toml | 65 ++++++++++--- identity_credential/src/sd_jwt_vc/error.rs | 12 +++ .../src/sd_jwt_vc/metadata/issuer.rs | 94 +++++++++++++++++++ .../src/sd_jwt_vc/metadata/mod.rs | 6 ++ identity_credential/src/sd_jwt_vc/mod.rs | 5 + identity_credential/src/sd_jwt_vc/resolver.rs | 34 +++++++ identity_credential/src/sd_jwt_vc/token.rs | 31 ++++++ identity_resolver/Cargo.toml | 4 +- 8 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/metadata/issuer.rs create mode 100644 identity_credential/src/sd_jwt_vc/metadata/mod.rs create mode 100644 identity_credential/src/sd_jwt_vc/resolver.rs diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 02608a771c..9c93a4f32b 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -14,20 +14,37 @@ description = "An implementation of the Verifiable Credentials standard." [dependencies] async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } -flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } +flate2 = { version = "1.0.28", default-features = false, features = [ + "rust_backend", +], optional = true } futures = { version = "0.3", default-features = false, optional = true } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } 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_verification = { version = "=1.3.1", path = "../identity_verification", default-features = false } -indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } -itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } +indexmap = { version = "2.0", default-features = false, features = [ + "std", + "serde", +] } +itertools = { version = "0.11", default-features = false, features = [ + "use_std", +], optional = true } json-proof-token = { workspace = true, optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } -reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } -roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } -sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true } -sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", branch = "feat/sd-jwt-v11", default-features = false, features = ["sha"], optional = true } +reqwest = { version = "0.11", default-features = false, features = [ + "default-tls", + "json", + "stream", +], optional = true } +roaring = { version = "0.10.2", default-features = false, features = [ + "serde", +], optional = true } +sd-jwt-payload = { version = "0.2.1", default-features = false, features = [ + "sha", +], optional = true } +sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", branch = "feat/sd-jwt-v11", default-features = false, features = [ + "sha", +], optional = true } serde.workspace = true serde-aux = { version = "4.3.1", default-features = false } serde_json.workspace = true @@ -36,13 +53,23 @@ strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } zkryptium = { workspace = true, optional = true } +anyhow = { version = "1" } [dev-dependencies] anyhow = "1.0.62" -identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } -iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "std", "random"] } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = [ + "ed25519", +] } +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"] } +tokio = { version = "1.35.0", default-features = false, features = [ + "rt-multi-thread", + "macros", +] } [package.metadata.docs.rs] # To build locally: @@ -51,7 +78,15 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["revocation-bitmap", "validator", "credential", "presentation", "domain-linkage-fetch", "sd-jwt", "sd-jwt-vc"] +default = [ + "revocation-bitmap", + "validator", + "credential", + "presentation", + "domain-linkage-fetch", + "sd-jwt", + "sd-jwt-vc", +] credential = [] presentation = ["credential"] revocation-bitmap = ["dep:flate2", "dep:roaring"] @@ -61,7 +96,13 @@ domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework"] -jpt-bbs-plus = ["credential", "validator", "dep:zkryptium", "dep:bls12_381_plus", "dep:json-proof-token"] +jpt-bbs-plus = [ + "credential", + "validator", + "dep:zkryptium", + "dep:bls12_381_plus", + "dep:json-proof-token", +] [lints] workspace = true diff --git a/identity_credential/src/sd_jwt_vc/error.rs b/identity_credential/src/sd_jwt_vc/error.rs index 67d5639812..48e25bb172 100644 --- a/identity_credential/src/sd_jwt_vc/error.rs +++ b/identity_credential/src/sd_jwt_vc/error.rs @@ -30,6 +30,18 @@ pub enum Error { /// Value of header parameter `typ` is not valid. #[error("invalid \"typ\" value; expected \"vc+sd-jwt\" (or a superset) but found \"{0}\"")] InvalidJoseType(String), + /// Resolution error. + #[error("failed to resolve \"{input}\"")] + Resolution { + /// The resource's identifier. + input: String, + /// Low level error. + #[source] + source: super::resolver::Error, + }, + /// Invalid issuer Metadata object. + #[error("invalid Issuer Metadata: {0}")] + InvalidIssuerMetadata(#[source] anyhow::Error) } /// Either a value of type `T` or an [`Error`]. diff --git a/identity_credential/src/sd_jwt_vc/metadata/issuer.rs b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs new file mode 100644 index 0000000000..bdf4eb9ae7 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs @@ -0,0 +1,94 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Url; +use identity_verification::jwk::Jwk; +use serde::{Deserialize, Serialize}; + +#[allow(unused_imports)] +use crate::sd_jwt_vc::SdJwtVcClaims; +use crate::sd_jwt_vc::{Error, SdJwtVc}; + +/// Path used to query [`IssuerMetadata`] for a given JWT VC issuer. +pub const WELL_KNOWN_VC_ISSUER: &str = "/.well-known/jwt-vc-issuer"; + +/// SD-JWT VC issuer's metadata. Contains information about one issuer's +/// public keys, either as an embedded JWK Set or a reference to one. +/// ## Notes +/// - [`IssuerMetadata::issuer`] must exactly match [`SdJwtVcClaims::iss`] in +/// order to be considered valid. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub struct IssuerMetadata { + /// Issuer URI. + pub issuer: Url, + /// JWK Set containing the issuer's public keys. + #[serde(flatten)] + pub jwks: Jwks, +} + +impl IssuerMetadata { + /// Checks the validity of this [`IssuerMetadata`]. + /// [`IssuerMetadata::issuer`] must match `sd_jwt_vc`'s iss claim's value. + pub fn validate(&self, sd_jwt_vc: &SdJwtVc) -> Result<(), Error> { + let expected_issuer = &sd_jwt_vc.claims().iss; + let actual_issuer = &self.issuer; + if actual_issuer != expected_issuer { + Err(Error::InvalidIssuerMetadata(anyhow::anyhow!("expected issuer \"{expected_issuer}\", but found \"{actual_issuer}\""))) + } else { + Ok(()) + } + } +} + +/// A JWK Set used for [`IssuerMetadata`]. +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +pub enum Jwks { + /// Reference to a JWK set. + #[serde(rename = "jwks_uri")] + Uri(Url), + /// An embedded JWK set. + #[serde(rename = "jwks")] + Object { + /// List of JWKs. + keys: Vec, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + const EXAMPLE_URI_ISSUER_METADATA: &str = r#" +{ + "issuer":"https://example.com", + "jwks_uri":"https://jwt-vc-issuer.example.org/my_public_keys.jwks" +} + "#; + const EXAMPLE_JWKS_ISSUER_METADATA: &str = r#" +{ + "issuer":"https://example.com", + "jwks":{ + "keys":[ + { + "kid":"doc-signer-05-25-2022", + "e":"AQAB", + "n":"nj3YJwsLUFl9BmpAbkOswCNVx17Eh9wMO-_AReZwBqfaWFcfGHrZXsIV2VMCNVNU8Tpb4obUaSXcRcQ-VMsfQPJm9IzgtRdAY8NN8Xb7PEcYyklBjvTtuPbpzIaqyiUepzUXNDFuAOOkrIol3WmflPUUgMKULBN0EUd1fpOD70pRM0rlp_gg_WNUKoW1V-3keYUJoXH9NztEDm_D2MQXj9eGOJJ8yPgGL8PAZMLe2R7jb9TxOCPDED7tY_TU4nFPlxptw59A42mldEmViXsKQt60s1SLboazxFKveqXC_jpLUt22OC6GUG63p-REw-ZOr3r845z50wMuzifQrMI9bQ", + "kty":"RSA" + } + ] + } +} + "#; + + #[test] + fn deserializing_uri_metadata_works() { + let issuer_metadata: IssuerMetadata = serde_json::from_str(EXAMPLE_URI_ISSUER_METADATA).unwrap(); + assert!(matches!(issuer_metadata.jwks, Jwks::Uri(_))); + } + + #[test] + fn deserializing_jwks_metadata_works() { + let issuer_metadata: IssuerMetadata = serde_json::from_str(EXAMPLE_JWKS_ISSUER_METADATA).unwrap(); + assert!(matches!(issuer_metadata.jwks, Jwks::Object{ .. })); + } +} diff --git a/identity_credential/src/sd_jwt_vc/metadata/mod.rs b/identity_credential/src/sd_jwt_vc/metadata/mod.rs new file mode 100644 index 0000000000..f4ba426d9f --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/metadata/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod issuer; + +pub use issuer::*; diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index f5b45119d1..9f579aeb91 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -6,8 +6,13 @@ mod error; mod status; mod token; mod presentation; +/// Additional metadata defined by the SD-JWT VC specification +/// such as issuer's metadata and credential type metadata. +pub mod metadata; +mod resolver; pub use claims::*; +pub use resolver::*; pub use presentation::*; pub use error::Error; pub use error::Result; diff --git a/identity_credential/src/sd_jwt_vc/resolver.rs b/identity_credential/src/sd_jwt_vc/resolver.rs new file mode 100644 index 0000000000..3bd8469a5b --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/resolver.rs @@ -0,0 +1,34 @@ +#![allow(async_fn_in_trait)] + +use thiserror::Error; + +type Result = std::result::Result; + +#[derive(Debug, Error)] +pub enum Error { + #[error("The requested item \"{0}\" was not found.")] + NotFound(String), + #[error("Failed to parse the provided input into a resolvable type: {0}")] + ParsingFailure(#[source] anyhow::Error), + #[error(transparent)] + Generic(#[from] anyhow::Error), +} + + +/// A type capable of asynchronously producing values of type [`Resolver::Target`] from inputs of type `I`. +pub trait Resolver { + /// The resulting type. + type Target; + /// Fetch the resource of type [`Resolver::Target`] using `input`. + async fn resolve(&self, input: &I) -> Result; + /// Like [`Resolver::resolve`] but with multiple inputs. + async fn resolve_multiple(&self, inputs: impl AsRef<[I]>) -> Result> { + let mut results = Vec::::with_capacity(inputs.as_ref().len()); + for input in inputs.as_ref() { + let result = self.resolve(input).await?; + results.push(result); + } + + Ok(results) + } +} \ No newline at end of file diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index 2dfb285f04..bae91a0dc7 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -6,9 +6,14 @@ use std::ops::Deref; use std::str::FromStr; use super::claims::SdJwtVcClaims; +use super::metadata::IssuerMetadata; +use super::metadata::WELL_KNOWN_VC_ISSUER; +use super::resolver::Error as ResolverErr; use super::Error; +use super::Resolver; use super::Result; use super::SdJwtVcPresentationBuilder; +use identity_core::common::Url; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::SdJwt; @@ -63,6 +68,32 @@ impl SdJwtVc { pub fn into_disclosed_object(self, hasher: &dyn Hasher) -> Result { SdJwt::from(self).into_disclosed_object(hasher).map_err(Error::SdJwt) } + + /// Retrieves this SD-JWT VC's issuer's metadata by querying its default location. + /// ## Notes + /// This method doesn't perform any validation of the retrieved [`IssuerMetadata`] + /// besides its syntactical validity. + /// To check if the retrieved [`IssuerMetadata`] is valid use [`IssuerMetadata::validate`]. + pub async fn issuer_metadata(&self, resolver: &R) -> Result> + where + R: Resolver, + { + let metadata_url = { + let origin = self.claims().iss.origin().ascii_serialization(); + let path = self.claims().iss.path(); + format!("{origin}{WELL_KNOWN_VC_ISSUER}{path}").parse().unwrap() + }; + match resolver.resolve(&metadata_url).await { + Err(ResolverErr::NotFound(_)) => Ok(None), + Err(e) => Err(Error::Resolution { + input: metadata_url.to_string(), + source: e, + }), + Ok(json_res) => serde_json::from_value(json_res) + .map_err(|e| Error::InvalidIssuerMetadata(e.into())) + .map(Some), + } + } } impl TryFrom for SdJwtVc { diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 88f1102057..c5025fb527 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -16,7 +16,6 @@ iota-sdk = { version = "1.1.5" } async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.3.1", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } @@ -34,10 +33,9 @@ identity_iota_core = { path = "../identity_iota_core", features = ["test"] } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } [features] -default = ["revocation-bitmap", "iota"] +default = ["iota"] # Enables the new Resolver interface. v2 = [] -revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] # Enables the IOTA integration for the resolver. iota = ["dep:identity_iota_core"] From 55319c51450f57490c65f2a9a6e872e09673f21d Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 19 Sep 2024 12:11:46 +0200 Subject: [PATCH 10/29] type metadata & credential verification --- identity_credential/Cargo.toml | 5 +- identity_credential/src/sd_jwt_vc/error.rs | 8 +- .../src/sd_jwt_vc/metadata/issuer.rs | 15 +- .../src/sd_jwt_vc/metadata/mod.rs | 2 + .../src/sd_jwt_vc/metadata/vc_type.rs | 242 ++++++++++++++++++ identity_credential/src/sd_jwt_vc/mod.rs | 10 +- .../src/sd_jwt_vc/presentation.rs | 5 +- identity_credential/src/sd_jwt_vc/resolver.rs | 19 +- identity_credential/src/sd_jwt_vc/token.rs | 46 +++- 9 files changed, 320 insertions(+), 32 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/metadata/vc_type.rs diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 9c93a4f32b..e69fcf001c 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -17,7 +17,7 @@ bls12_381_plus = { workspace = true, optional = true } flate2 = { version = "1.0.28", default-features = false, features = [ "rust_backend", ], optional = true } -futures = { version = "0.3", default-features = false, optional = true } +futures = { version = "0.3", default-features = false, optional = true, features = ["alloc"] } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } @@ -54,6 +54,7 @@ thiserror.workspace = true url = { version = "2.5", default-features = false } zkryptium = { workspace = true, optional = true } anyhow = { version = "1" } +jsonschema = { version = "0.19", optional = true, default-features = false } [dev-dependencies] anyhow = "1.0.62" @@ -95,7 +96,7 @@ validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] -sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework"] +sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework", "dep:jsonschema"] jpt-bbs-plus = [ "credential", "validator", diff --git a/identity_credential/src/sd_jwt_vc/error.rs b/identity_credential/src/sd_jwt_vc/error.rs index 48e25bb172..21246bba90 100644 --- a/identity_credential/src/sd_jwt_vc/error.rs +++ b/identity_credential/src/sd_jwt_vc/error.rs @@ -41,7 +41,13 @@ pub enum Error { }, /// Invalid issuer Metadata object. #[error("invalid Issuer Metadata: {0}")] - InvalidIssuerMetadata(#[source] anyhow::Error) + InvalidIssuerMetadata(#[source] anyhow::Error), + /// Invalid credential type metadata object. + #[error("invalid Type Metadata: {0}")] + InvalidTypeMetadata(#[source] anyhow::Error), + /// Credential validation failed. + #[error("credential validation failed: {0}")] + Validation(#[source] anyhow::Error), } /// Either a value of type `T` or an [`Error`]. diff --git a/identity_credential/src/sd_jwt_vc/metadata/issuer.rs b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs index bdf4eb9ae7..676200d904 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/issuer.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs @@ -3,11 +3,13 @@ use identity_core::common::Url; use identity_verification::jwk::Jwk; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use serde::Serialize; +use crate::sd_jwt_vc::Error; +use crate::sd_jwt_vc::SdJwtVc; #[allow(unused_imports)] use crate::sd_jwt_vc::SdJwtVcClaims; -use crate::sd_jwt_vc::{Error, SdJwtVc}; /// Path used to query [`IssuerMetadata`] for a given JWT VC issuer. pub const WELL_KNOWN_VC_ISSUER: &str = "/.well-known/jwt-vc-issuer"; @@ -15,8 +17,7 @@ pub const WELL_KNOWN_VC_ISSUER: &str = "/.well-known/jwt-vc-issuer"; /// SD-JWT VC issuer's metadata. Contains information about one issuer's /// public keys, either as an embedded JWK Set or a reference to one. /// ## Notes -/// - [`IssuerMetadata::issuer`] must exactly match [`SdJwtVcClaims::iss`] in -/// order to be considered valid. +/// - [`IssuerMetadata::issuer`] must exactly match [`SdJwtVcClaims::iss`] in order to be considered valid. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] pub struct IssuerMetadata { /// Issuer URI. @@ -33,7 +34,9 @@ impl IssuerMetadata { let expected_issuer = &sd_jwt_vc.claims().iss; let actual_issuer = &self.issuer; if actual_issuer != expected_issuer { - Err(Error::InvalidIssuerMetadata(anyhow::anyhow!("expected issuer \"{expected_issuer}\", but found \"{actual_issuer}\""))) + Err(Error::InvalidIssuerMetadata(anyhow::anyhow!( + "expected issuer \"{expected_issuer}\", but found \"{actual_issuer}\"" + ))) } else { Ok(()) } @@ -89,6 +92,6 @@ mod tests { #[test] fn deserializing_jwks_metadata_works() { let issuer_metadata: IssuerMetadata = serde_json::from_str(EXAMPLE_JWKS_ISSUER_METADATA).unwrap(); - assert!(matches!(issuer_metadata.jwks, Jwks::Object{ .. })); + assert!(matches!(issuer_metadata.jwks, Jwks::Object { .. })); } } diff --git a/identity_credential/src/sd_jwt_vc/metadata/mod.rs b/identity_credential/src/sd_jwt_vc/metadata/mod.rs index f4ba426d9f..9d3be4070f 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/mod.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/mod.rs @@ -2,5 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 mod issuer; +mod vc_type; pub use issuer::*; +pub use vc_type::*; diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs new file mode 100644 index 0000000000..ccd0af9d37 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -0,0 +1,242 @@ +use futures::future::BoxFuture; +use futures::future::FutureExt; +use identity_core::common::StringOrUrl; +use identity_core::common::Url; +use itertools::Itertools as _; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; + +use crate::sd_jwt_vc::Error; +use crate::sd_jwt_vc::Resolver; +use crate::sd_jwt_vc::Result; + +/// Path used to retrieve VC Type Metadata. +pub const WELL_KNOWN_VCT: &str = "/.well-known/vct"; + +/// SD-JWT VC's credential type. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +pub struct TypeMetadata { + name: Option, + description: Option, + extends: Option, + #[serde(rename = "extends#integrity")] + extends_integrity: Option, + #[serde(flatten)] + schema: Option, +} + +impl TypeMetadata { + /// Returns the name of this VC type, if any. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + /// Returns the description of this VC type, if any. + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } + /// Returns the URI or string of the type this VC type extends, if any. + pub fn extends(&self) -> Option<&StringOrUrl> { + self.extends.as_ref() + } + /// Returns the integrity string of the extended type object, if any. + pub fn extends_integrity(&self) -> Option<&str> { + self.extends_integrity.as_deref() + } + /// Uses this [`TypeMetadata`] to validate JSON object `credential`. This method fails + /// if the schema is referenced instead of embedded. + /// Use [`TypeMetadata::validate_credential_with_resolver`] for such cases. + /// ## Notes + /// This method ignores type extensions. + pub fn validate_credential(&self, credential: &Value) -> Result<()> { + match &self.schema { + Some(TypeSchema::Object { schema, .. }) => validate_credential_with_schema(schema, credential), + Some(_) => Err(Error::Validation(anyhow::anyhow!( + "this credential type references a schema; resolution is required" + ))), + None => Ok(()), + } + } + + /// Similar to [`TypeMetadata::validate_credential`], but accepts a [`Resolver`] + /// [`StringOrUrl`] -> [`Value`] that is used to resolve any reference to either + /// another type or JSON schema. + pub async fn validate_credential_with_resolver(&self, credential: &Value, resolver: &R) -> Result<()> + where + R: Resolver + Sync, + { + validate_credential_impl(self.clone(), credential, resolver, vec![]).await + } +} + +fn validate_credential_impl<'c, 'r, R>( + current_type: TypeMetadata, + credential: &'c Value, + resolver: &'r R, + mut passed_types: Vec, +) -> BoxFuture<'c, Result<()>> +where + R: Resolver + Sync, + 'r: 'c, +{ + async move { + // Check if current type has already been checked. + let is_type_already_checked = passed_types.contains(¤t_type); + if is_type_already_checked { + // This is a dependency cycle! + return Err(Error::Validation(anyhow::anyhow!("dependency cycle detected"))); + } + + // Check if `validate_credential` should have been called instead. + let has_extend = current_type.extends.is_none(); + let is_immediate = current_type + .schema + .as_ref() + .map(|schema| matches!(schema, &TypeSchema::Object { .. })) + .unwrap_or(true); + + if is_immediate && !has_extend { + return current_type.validate_credential(credential); + } + + if !is_immediate { + // Fetch schema and validate `current_type`. + let TypeSchema::Uri { schema_uri, .. } = current_type.schema.as_ref().unwrap() else { + unreachable!("schema is provided through `schema_uri` as checked by `validate_credential`"); + }; + let schema_uri = StringOrUrl::Url(schema_uri.clone()); + let schema = resolver.resolve(&schema_uri).await.map_err(|e| Error::Resolution { + input: schema_uri.to_string(), + source: e, + })?; + validate_credential_with_schema(&schema, credential)?; + } + + // Check for extends. + if let Some(extends_uri) = current_type.extends() { + // Fetch the extended type metadata and parse it. + let raw_type_metadata = resolver.resolve(extends_uri).await.map_err(|e| Error::Resolution { + input: extends_uri.to_string(), + source: e, + })?; + let type_metadata = + serde_json::from_value(raw_type_metadata).map_err(|e| Error::InvalidTypeMetadata(e.into()))?; + // Forward validation of new type. + passed_types.push(current_type); + validate_credential_impl(type_metadata, credential, resolver, passed_types).await + } else { + Ok(()) + } + } + .boxed() +} + +fn validate_credential_with_schema(schema: &Value, credential: &Value) -> Result<()> { + dbg!(&schema); + dbg!(&credential); + let schema = jsonschema::compile(schema).map_err(|e| Error::Validation(anyhow::anyhow!(e.to_string())))?; + schema.validate(credential).map_err(|errors| { + let error_msg = errors.map(|e| e.to_string()).join("; "); + Error::Validation(anyhow::anyhow!(error_msg)) + }) +} + +/// Either a reference to or an embedded JSON Schema. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[serde(untagged)] +pub enum TypeSchema { + /// URI reference to a JSON schema. + Uri { + /// URI of the referenced JSON schema. + schema_uri: Url, + /// Integrity string for the referenced schema. + #[serde(rename = "schema_uri#integrity")] + schema_uri_integrity: Option, + }, + /// An embedded JSON schema. + Object { + /// The JSON schema. + schema: Value, + /// Integrity of the JSON schema. + #[serde(rename = "schema#integrity")] + schema_integrity: Option, + }, +} + +#[cfg(test)] +mod tests { + use std::cell::LazyCell; + + use async_trait::async_trait; + use serde_json::json; + + use crate::sd_jwt_vc::resolver; + + use super::*; + + const IMMEDIATE_TYPE_METADATA: LazyCell = LazyCell::new(|| TypeMetadata { + name: Some("immediate credential".to_string()), + description: None, + extends: None, + extends_integrity: None, + schema: Some(TypeSchema::Object { + schema: json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "number" + } + }, + "required": ["name", "age"] + }), + schema_integrity: None, + }), + }); + const REFERENCED_TYPE_METADATA: LazyCell = LazyCell::new(|| TypeMetadata { + name: Some("immediate credential".to_string()), + description: None, + extends: None, + extends_integrity: None, + schema: Some(TypeSchema::Uri { + schema_uri: Url::parse("https://example.com/vc_types/1").unwrap(), + schema_uri_integrity: None, + }), + }); + + struct SchemaResolver; + #[async_trait] + impl Resolver for SchemaResolver { + type Target = Value; + async fn resolve(&self, _input: &StringOrUrl) -> resolver::Result { + Ok(serde_json::to_value(IMMEDIATE_TYPE_METADATA.clone().schema).unwrap()) + } + } + + #[test] + fn validation_of_immediate_type_metadata_works() { + IMMEDIATE_TYPE_METADATA + .validate_credential(&json!({ + "name": "John Doe", + "age": 42 + })) + .unwrap(); + } + + #[tokio::test] + async fn validation_of_referenced_type_metadata_works() { + REFERENCED_TYPE_METADATA + .validate_credential_with_resolver( + &json!({ + "name": "Aristide Zantedeschi", + "age": 90, + }), + &SchemaResolver, + ) + .await + .unwrap(); + } +} diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index 9f579aeb91..f576b4ecda 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -3,18 +3,18 @@ mod claims; mod error; -mod status; -mod token; -mod presentation; /// Additional metadata defined by the SD-JWT VC specification /// such as issuer's metadata and credential type metadata. pub mod metadata; +mod presentation; mod resolver; +mod status; +mod token; pub use claims::*; -pub use resolver::*; -pub use presentation::*; pub use error::Error; pub use error::Result; +pub use presentation::*; +pub use resolver::*; pub use status::*; pub use token::*; diff --git a/identity_credential/src/sd_jwt_vc/presentation.rs b/identity_credential/src/sd_jwt_vc/presentation.rs index 400b9d84bd..ec3c2bef4b 100644 --- a/identity_credential/src/sd_jwt_vc/presentation.rs +++ b/identity_credential/src/sd_jwt_vc/presentation.rs @@ -19,7 +19,10 @@ pub struct SdJwtVcPresentationBuilder { impl SdJwtVcPresentationBuilder { /// Prepare a presentation for a given [`SdJwtVc`]. pub fn new(token: SdJwtVc, hasher: &dyn Hasher) -> Result { - let SdJwtVc { sd_jwt, parsed_claims: vc_claims } = token; + let SdJwtVc { + sd_jwt, + parsed_claims: vc_claims, + } = token; let builder = sd_jwt.into_presentation(hasher).map_err(Error::SdJwt)?; Ok(Self { vc_claims, builder }) diff --git a/identity_credential/src/sd_jwt_vc/resolver.rs b/identity_credential/src/sd_jwt_vc/resolver.rs index 3bd8469a5b..43baddf49f 100644 --- a/identity_credential/src/sd_jwt_vc/resolver.rs +++ b/identity_credential/src/sd_jwt_vc/resolver.rs @@ -1,8 +1,9 @@ #![allow(async_fn_in_trait)] +use async_trait::async_trait; use thiserror::Error; -type Result = std::result::Result; +pub(crate) type Result = std::result::Result; #[derive(Debug, Error)] pub enum Error { @@ -14,21 +15,11 @@ pub enum Error { Generic(#[from] anyhow::Error), } - /// A type capable of asynchronously producing values of type [`Resolver::Target`] from inputs of type `I`. -pub trait Resolver { +#[async_trait] +pub trait Resolver { /// The resulting type. type Target; /// Fetch the resource of type [`Resolver::Target`] using `input`. async fn resolve(&self, input: &I) -> Result; - /// Like [`Resolver::resolve`] but with multiple inputs. - async fn resolve_multiple(&self, inputs: impl AsRef<[I]>) -> Result> { - let mut results = Vec::::with_capacity(inputs.as_ref().len()); - for input in inputs.as_ref() { - let result = self.resolve(input).await?; - results.push(result); - } - - Ok(results) - } -} \ No newline at end of file +} diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index bae91a0dc7..ee1062caa2 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -7,12 +7,16 @@ use std::str::FromStr; use super::claims::SdJwtVcClaims; use super::metadata::IssuerMetadata; +use super::metadata::TypeMetadata; +#[allow(unused_imports)] +use super::metadata::WELL_KNOWN_VCT; use super::metadata::WELL_KNOWN_VC_ISSUER; use super::resolver::Error as ResolverErr; use super::Error; use super::Resolver; use super::Result; use super::SdJwtVcPresentationBuilder; +use identity_core::common::StringOrUrl; use identity_core::common::Url; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; @@ -57,8 +61,8 @@ impl SdJwtVc { /// Prepares this [`SdJwtVc`] for a presentation, returning an [`SdJwtVcPresentationBuilder`]. /// ## Errors - /// - [`Error::SdJwt`] is returned if the provided `hasher`'s algorithm doesn't match the algorithm specified - /// by SD-JWT's `_sd_alg` claim. "sha-256" is used if the claim is missing. + /// - [`Error::SdJwt`] is returned if the provided `hasher`'s algorithm doesn't match the algorithm specified by + /// SD-JWT's `_sd_alg` claim. "sha-256" is used if the claim is missing. pub fn into_presentation(self, hasher: &dyn Hasher) -> Result { SdJwtVcPresentationBuilder::new(self, hasher) } @@ -72,7 +76,7 @@ impl SdJwtVc { /// Retrieves this SD-JWT VC's issuer's metadata by querying its default location. /// ## Notes /// This method doesn't perform any validation of the retrieved [`IssuerMetadata`] - /// besides its syntactical validity. + /// besides its syntactical validity. /// To check if the retrieved [`IssuerMetadata`] is valid use [`IssuerMetadata::validate`]. pub async fn issuer_metadata(&self, resolver: &R) -> Result> where @@ -94,6 +98,42 @@ impl SdJwtVc { .map(Some), } } + + /// Retrieve this SD-JWT VC credential's type metadata [`TypeMetadata`]. + /// ## Notes + /// `resolver` is fed with whatever value [`SdJwtVc`]'s `vct` might have. + /// If `vct` is a URI with scheme `https`, `resolver` must fetch the [`TypeMetadata`] + /// resource by combining `vct`'s value with [`WELL_KNOWN_VCT`]. To simplify this process + /// the utility function [`vct_to_url`] is provided. + /// + /// Returns the parsed [`TypeMetadata`] along with the raw [`Resolver`]'s response. + /// The latter can be used to validate the `vct#integrity` claim if present. + pub async fn type_metadata(&self, resolver: &R) -> Result<(TypeMetadata, Vec)> + where + R: Resolver>, + { + let vct = &self.claims().vct; + let raw = resolver.resolve(vct).await.map_err(|e| Error::Resolution { + input: vct.to_string(), + source: e, + })?; + let metadata = serde_json::from_slice(&raw).map_err(|e| Error::InvalidTypeMetadata(e.into()))?; + + Ok((metadata, raw)) + } +} + +/// Converts `vct` claim's URI value into the appropriate well-known URL. +/// ## Warnings +/// Returns an [`Option::None`] if the URI's scheme is not `https`. +pub fn vct_to_url(resource: &Url) -> Option { + if resource.scheme() != "https" { + None + } else { + let origin = resource.origin().ascii_serialization(); + let path = resource.path(); + Some(format!("{origin}{WELL_KNOWN_VC_ISSUER}{path}").parse().unwrap()) + } } impl TryFrom for SdJwtVc { From 704b39513d8383174f48d1658cf676c208ef1ef1 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 19 Sep 2024 15:53:21 +0200 Subject: [PATCH 11/29] change resolver's constraints --- identity_credential/src/sd_jwt_vc/metadata/vc_type.rs | 2 -- identity_credential/src/sd_jwt_vc/token.rs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index ccd0af9d37..51ae8a50ab 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -132,8 +132,6 @@ where } fn validate_credential_with_schema(schema: &Value, credential: &Value) -> Result<()> { - dbg!(&schema); - dbg!(&credential); let schema = jsonschema::compile(schema).map_err(|e| Error::Validation(anyhow::anyhow!(e.to_string())))?; schema.validate(credential).map_err(|errors| { let error_msg = errors.map(|e| e.to_string()).join("; "); diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index ee1062caa2..dd6fc1ab1e 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -80,7 +80,7 @@ impl SdJwtVc { /// To check if the retrieved [`IssuerMetadata`] is valid use [`IssuerMetadata::validate`]. pub async fn issuer_metadata(&self, resolver: &R) -> Result> where - R: Resolver, + R: Resolver>, { let metadata_url = { let origin = self.claims().iss.origin().ascii_serialization(); @@ -93,7 +93,7 @@ impl SdJwtVc { input: metadata_url.to_string(), source: e, }), - Ok(json_res) => serde_json::from_value(json_res) + Ok(json_res) => serde_json::from_slice(&json_res) .map_err(|e| Error::InvalidIssuerMetadata(e.into())) .map(Some), } From 130af00ecfa7b9d98cc327ff746943b61c2842c7 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 19 Sep 2024 17:23:47 +0200 Subject: [PATCH 12/29] integrity metadata --- .../src/sd_jwt_vc/metadata/integrity.rs | 100 ++++++++++++++++++ .../src/sd_jwt_vc/metadata/mod.rs | 2 + .../src/sd_jwt_vc/metadata/vc_type.rs | 11 +- 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/metadata/integrity.rs diff --git a/identity_credential/src/sd_jwt_vc/metadata/integrity.rs b/identity_credential/src/sd_jwt_vc/metadata/integrity.rs new file mode 100644 index 0000000000..bbff7a86a1 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/metadata/integrity.rs @@ -0,0 +1,100 @@ +use std::{fmt::Display, str::FromStr}; + +use anyhow::anyhow; +use identity_core::convert::{Base, BaseEncoding}; +use serde::{Deserialize, Serialize}; + +/// An integrity metadata string as defined in [W3C SRI](https://www.w3.org/TR/SRI/#integrity-metadata). +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(try_from = "String")] +pub struct IntegrityMetadata(String); + +impl IntegrityMetadata { + /// Parses an [`IntegrityMetadata`] from a string. + /// ## Example + /// ```rust + /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; + /// + /// let integrity_data = IntegrityMetadata::parse("sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd").unwrap(); + /// ``` + pub fn parse(s: &str) -> Result { + s.parse() + } + + /// Returns the digest algorithm's identifier string. + /// ## Example + /// ```rust + /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; + /// + /// let integrity_data: IntegrityMetadata = "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd".parse().unwrap(); + /// assert_eq!(integrity_data.alg(), "sha384"); + /// ``` + pub fn alg(&self) -> &str { + self.0.split_once('-').unwrap().0 + } + + /// Returns the base64 encoded digest part. + /// ## Example + /// ```rust + /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; + /// + /// let integrity_data: IntegrityMetadata = "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd".parse().unwrap(); + /// assert_eq!(integrity_data.digest(), "dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd"); + /// ``` + pub fn digest(&self) -> &str { + self.0.split('-').nth(1).unwrap() + } + + /// Returns the digest's bytes. + pub fn digest_bytes(&self) -> Vec { + BaseEncoding::decode(self.digest(), Base::Base64).unwrap() + } + + /// Returns the option part. + /// ## Example + /// ```rust + /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; + /// + /// let integrity_data: IntegrityMetadata = "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd".parse().unwrap(); + /// assert!(integrity_data.options().is_none()); + /// ``` + pub fn options(&self) -> Option<&str> { + self.0.splitn(3, '-').nth(2) + } +} + +impl AsRef for IntegrityMetadata { + fn as_ref(&self) -> &str { + self.0.as_str() + } +} + +impl Display for IntegrityMetadata { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.0) + } +} + +impl FromStr for IntegrityMetadata { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result { + Self::try_from(s.to_owned()) + } +} + +impl TryFrom for IntegrityMetadata { + type Error = anyhow::Error; + fn try_from(value: String) -> Result { + let mut metadata_parts = value.splitn(3, '-'); + let _alg = metadata_parts + .next() + .ok_or_else(|| anyhow!("invalid integrity metadata"))?; + let _digest = metadata_parts + .next() + .and_then(|digest| BaseEncoding::decode(digest, Base::Base64).ok()) + .ok_or_else(|| anyhow!("invalid integrity metadata"))?; + let _options = metadata_parts.next(); + + Ok(Self(value)) + } +} diff --git a/identity_credential/src/sd_jwt_vc/metadata/mod.rs b/identity_credential/src/sd_jwt_vc/metadata/mod.rs index 9d3be4070f..97aa05614f 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/mod.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/mod.rs @@ -3,6 +3,8 @@ mod issuer; mod vc_type; +mod integrity; pub use issuer::*; pub use vc_type::*; +pub use integrity::*; diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index 51ae8a50ab..cf72cba17d 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -11,6 +11,8 @@ use crate::sd_jwt_vc::Error; use crate::sd_jwt_vc::Resolver; use crate::sd_jwt_vc::Result; +use super::IntegrityMetadata; + /// Path used to retrieve VC Type Metadata. pub const WELL_KNOWN_VCT: &str = "/.well-known/vct"; @@ -21,7 +23,7 @@ pub struct TypeMetadata { description: Option, extends: Option, #[serde(rename = "extends#integrity")] - extends_integrity: Option, + extends_integrity: Option, #[serde(flatten)] schema: Option, } @@ -41,7 +43,7 @@ impl TypeMetadata { } /// Returns the integrity string of the extended type object, if any. pub fn extends_integrity(&self) -> Option<&str> { - self.extends_integrity.as_deref() + self.extends_integrity.as_ref().map(|meta| meta.as_ref()) } /// Uses this [`TypeMetadata`] to validate JSON object `credential`. This method fails /// if the schema is referenced instead of embedded. @@ -69,6 +71,7 @@ impl TypeMetadata { } } +/// Does this method signature look weird? Turns out having recursive async functions is not that ez :'(. fn validate_credential_impl<'c, 'r, R>( current_type: TypeMetadata, credential: &'c Value, @@ -149,7 +152,7 @@ pub enum TypeSchema { schema_uri: Url, /// Integrity string for the referenced schema. #[serde(rename = "schema_uri#integrity")] - schema_uri_integrity: Option, + schema_uri_integrity: Option, }, /// An embedded JSON schema. Object { @@ -157,7 +160,7 @@ pub enum TypeSchema { schema: Value, /// Integrity of the JSON schema. #[serde(rename = "schema#integrity")] - schema_integrity: Option, + schema_integrity: Option, }, } From d278060acef970ee829754edca52942139cc73d8 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 20 Sep 2024 12:18:05 +0200 Subject: [PATCH 13/29] display metadata --- identity_core/src/common/mod.rs | 2 +- identity_core/src/common/string_or_url.rs | 6 ++- .../src/sd_jwt_vc/metadata/display.rs | 21 ++++++++++ .../src/sd_jwt_vc/metadata/integrity.rs | 42 +++++++++++++------ .../src/sd_jwt_vc/metadata/mod.rs | 6 ++- .../src/sd_jwt_vc/metadata/vc_type.rs | 36 +++++++++------- identity_resolver/src/legacy/commands.rs | 1 - identity_resolver/src/legacy/error.rs | 1 - identity_resolver/src/legacy/mod.rs | 2 +- identity_resolver/src/lib.rs | 2 +- 10 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/metadata/display.rs diff --git a/identity_core/src/common/mod.rs b/identity_core/src/common/mod.rs index 0b6049504f..04568e05b5 100644 --- a/identity_core/src/common/mod.rs +++ b/identity_core/src/common/mod.rs @@ -23,6 +23,6 @@ mod one_or_many; mod one_or_set; mod ordered_set; mod single_struct_error; +mod string_or_url; mod timestamp; mod url; -mod string_or_url; diff --git a/identity_core/src/common/string_or_url.rs b/identity_core/src/common/string_or_url.rs index 823dd87016..99f4e57f7a 100644 --- a/identity_core/src/common/string_or_url.rs +++ b/identity_core/src/common/string_or_url.rs @@ -1,9 +1,11 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::{fmt::Display, str::FromStr}; +use std::fmt::Display; +use std::str::FromStr; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; +use serde::Serialize; use super::Url; diff --git a/identity_credential/src/sd_jwt_vc/metadata/display.rs b/identity_credential/src/sd_jwt_vc/metadata/display.rs new file mode 100644 index 0000000000..95368a9f78 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/metadata/display.rs @@ -0,0 +1,21 @@ +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; + +/// Credential type's display information of a given languange. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct DisplayMetadata { + /// Language tag as defined in [RFC5646](https://www.rfc-editor.org/rfc/rfc5646.txt). + pub lang: String, + /// VC type's human-readable name. + pub name: String, + /// VC type's human-readable description. + pub description: Option, + /// Optional rendering information. + pub rendering: Option>, +} + +/// Information on how to render a given credential type. +// TODO: model the actual object properties. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct RenderingMetadata(serde_json::Map); diff --git a/identity_credential/src/sd_jwt_vc/metadata/integrity.rs b/identity_credential/src/sd_jwt_vc/metadata/integrity.rs index bbff7a86a1..2065f50736 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/integrity.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/integrity.rs @@ -1,8 +1,11 @@ -use std::{fmt::Display, str::FromStr}; +use std::fmt::Display; +use std::str::FromStr; use anyhow::anyhow; -use identity_core::convert::{Base, BaseEncoding}; -use serde::{Deserialize, Serialize}; +use identity_core::convert::Base; +use identity_core::convert::BaseEncoding; +use serde::Deserialize; +use serde::Serialize; /// An integrity metadata string as defined in [W3C SRI](https://www.w3.org/TR/SRI/#integrity-metadata). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] @@ -14,8 +17,11 @@ impl IntegrityMetadata { /// ## Example /// ```rust /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; - /// - /// let integrity_data = IntegrityMetadata::parse("sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd").unwrap(); + /// + /// let integrity_data = IntegrityMetadata::parse( + /// "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd", + /// ) + /// .unwrap(); /// ``` pub fn parse(s: &str) -> Result { s.parse() @@ -25,8 +31,11 @@ impl IntegrityMetadata { /// ## Example /// ```rust /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; - /// - /// let integrity_data: IntegrityMetadata = "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd".parse().unwrap(); + /// + /// let integrity_data: IntegrityMetadata = + /// "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd" + /// .parse() + /// .unwrap(); /// assert_eq!(integrity_data.alg(), "sha384"); /// ``` pub fn alg(&self) -> &str { @@ -37,9 +46,15 @@ impl IntegrityMetadata { /// ## Example /// ```rust /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; - /// - /// let integrity_data: IntegrityMetadata = "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd".parse().unwrap(); - /// assert_eq!(integrity_data.digest(), "dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd"); + /// + /// let integrity_data: IntegrityMetadata = + /// "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd" + /// .parse() + /// .unwrap(); + /// assert_eq!( + /// integrity_data.digest(), + /// "dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd" + /// ); /// ``` pub fn digest(&self) -> &str { self.0.split('-').nth(1).unwrap() @@ -54,8 +69,11 @@ impl IntegrityMetadata { /// ## Example /// ```rust /// use identity_credential::sd_jwt_vc::metadata::IntegrityMetadata; - /// - /// let integrity_data: IntegrityMetadata = "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd".parse().unwrap(); + /// + /// let integrity_data: IntegrityMetadata = + /// "sha384-dOTZf16X8p34q2/kYyEFm0jh89uTjikhnzjeLeF0FHsEaYKb1A1cv+Lyv4Hk8vHd" + /// .parse() + /// .unwrap(); /// assert!(integrity_data.options().is_none()); /// ``` pub fn options(&self) -> Option<&str> { diff --git a/identity_credential/src/sd_jwt_vc/metadata/mod.rs b/identity_credential/src/sd_jwt_vc/metadata/mod.rs index 97aa05614f..0413c4ce44 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/mod.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/mod.rs @@ -1,10 +1,12 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod display; +mod integrity; mod issuer; mod vc_type; -mod integrity; +pub use display::*; +pub use integrity::*; pub use issuer::*; pub use vc_type::*; -pub use integrity::*; diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index cf72cba17d..255ffaf8af 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -1,6 +1,5 @@ use futures::future::BoxFuture; use futures::future::FutureExt; -use identity_core::common::StringOrUrl; use identity_core::common::Url; use itertools::Itertools as _; use serde::Deserialize; @@ -11,21 +10,29 @@ use crate::sd_jwt_vc::Error; use crate::sd_jwt_vc::Resolver; use crate::sd_jwt_vc::Result; +use super::DisplayMetadata; use super::IntegrityMetadata; /// Path used to retrieve VC Type Metadata. pub const WELL_KNOWN_VCT: &str = "/.well-known/vct"; /// SD-JWT VC's credential type. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TypeMetadata { - name: Option, - description: Option, - extends: Option, + /// A human-readable name for the type, intended for developers reading the JSON document. + pub name: Option, + /// A human-readable description for the type, intended for developers reading the JSON document. + pub description: Option, + /// A URI of another type that this type extends. + pub extends: Option, + /// Integrity metadata for the extended type. #[serde(rename = "extends#integrity")] - extends_integrity: Option, + pub extends_integrity: Option, + /// Either an embedded schema or a reference to one. #[serde(flatten)] - schema: Option, + pub schema: Option, + /// An object containing display information for the type. + pub display: Option, } impl TypeMetadata { @@ -38,7 +45,7 @@ impl TypeMetadata { self.description.as_deref() } /// Returns the URI or string of the type this VC type extends, if any. - pub fn extends(&self) -> Option<&StringOrUrl> { + pub fn extends(&self) -> Option<&Url> { self.extends.as_ref() } /// Returns the integrity string of the extended type object, if any. @@ -65,7 +72,7 @@ impl TypeMetadata { /// another type or JSON schema. pub async fn validate_credential_with_resolver(&self, credential: &Value, resolver: &R) -> Result<()> where - R: Resolver + Sync, + R: Resolver + Sync, { validate_credential_impl(self.clone(), credential, resolver, vec![]).await } @@ -79,7 +86,7 @@ fn validate_credential_impl<'c, 'r, R>( mut passed_types: Vec, ) -> BoxFuture<'c, Result<()>> where - R: Resolver + Sync, + R: Resolver + Sync, 'r: 'c, { async move { @@ -107,8 +114,7 @@ where let TypeSchema::Uri { schema_uri, .. } = current_type.schema.as_ref().unwrap() else { unreachable!("schema is provided through `schema_uri` as checked by `validate_credential`"); }; - let schema_uri = StringOrUrl::Url(schema_uri.clone()); - let schema = resolver.resolve(&schema_uri).await.map_err(|e| Error::Resolution { + let schema = resolver.resolve(schema_uri).await.map_err(|e| Error::Resolution { input: schema_uri.to_string(), source: e, })?; @@ -180,6 +186,7 @@ mod tests { description: None, extends: None, extends_integrity: None, + display: None, schema: Some(TypeSchema::Object { schema: json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -202,6 +209,7 @@ mod tests { description: None, extends: None, extends_integrity: None, + display: None, schema: Some(TypeSchema::Uri { schema_uri: Url::parse("https://example.com/vc_types/1").unwrap(), schema_uri_integrity: None, @@ -210,9 +218,9 @@ mod tests { struct SchemaResolver; #[async_trait] - impl Resolver for SchemaResolver { + impl Resolver for SchemaResolver { type Target = Value; - async fn resolve(&self, _input: &StringOrUrl) -> resolver::Result { + async fn resolve(&self, _input: &Url) -> resolver::Result { Ok(serde_json::to_value(IMMEDIATE_TYPE_METADATA.clone().schema).unwrap()) } } diff --git a/identity_resolver/src/legacy/commands.rs b/identity_resolver/src/legacy/commands.rs index 676a77f381..41129a6c7d 100644 --- a/identity_resolver/src/legacy/commands.rs +++ b/identity_resolver/src/legacy/commands.rs @@ -138,4 +138,3 @@ impl SingleThreadedCommand { Self { fun } } } - diff --git a/identity_resolver/src/legacy/error.rs b/identity_resolver/src/legacy/error.rs index 99db7e5381..d72a78fd4a 100644 --- a/identity_resolver/src/legacy/error.rs +++ b/identity_resolver/src/legacy/error.rs @@ -72,4 +72,3 @@ pub enum ErrorCause { #[error("none of the attached clients support the network {0}")] UnsupportedNetwork(String), } - diff --git a/identity_resolver/src/legacy/mod.rs b/identity_resolver/src/legacy/mod.rs index 56ef0891cc..b62ee0f338 100644 --- a/identity_resolver/src/legacy/mod.rs +++ b/identity_resolver/src/legacy/mod.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 mod commands; -mod resolver; mod error; +mod resolver; use self::commands::SingleThreadedCommand; use identity_document::document::CoreDocument; diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs index 7d5bf1d8d7..d68b59eb90 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -4,7 +4,7 @@ #[cfg(feature = "v2")] #[path = ""] mod v2 { - mod error; + mod error; mod resolver; pub use error::Error; From 0a1ed278a9056b1a26f81050a579909b442d2341 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 20 Sep 2024 13:21:35 +0200 Subject: [PATCH 14/29] claim metadata --- .../src/sd_jwt_vc/metadata/claim.rs | 104 ++++++++++++++++++ .../src/sd_jwt_vc/metadata/mod.rs | 2 + .../src/sd_jwt_vc/metadata/vc_type.rs | 15 ++- 3 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/metadata/claim.rs diff --git a/identity_credential/src/sd_jwt_vc/metadata/claim.rs b/identity_credential/src/sd_jwt_vc/metadata/claim.rs new file mode 100644 index 0000000000..1c6c852ba5 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/metadata/claim.rs @@ -0,0 +1,104 @@ +use std::ops::Deref; + +use serde::Deserialize; +use serde::Serialize; +use serde::Serializer; +use serde_json::Value; + +/// Information about a particular claim for displaying and validation purposes. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ClaimMetadata { + /// [`ClaimPath`] of the claim or claims that are being addressed. + pub path: ClaimPath, + /// Object containing display information for the claim. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub display: Vec, + /// A string indicating whether the claim is selectively disclosable. + pub sd: Option, + /// A string defining the ID of the claim for reference in the SVG template. + pub svg_id: Option, +} + +/// A non-empty list of string, `null` values, or non-negative integers. +/// It is used to selected a particular claim in the credential or a +/// set of claims. See [Claim Path](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-05.html#name-claim-path) for more information. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "Vec")] +pub struct ClaimPath(Vec); + +impl TryFrom> for ClaimPath { + type Error = anyhow::Error; + fn try_from(value: Vec) -> Result { + if value.is_empty() { + Err(anyhow::anyhow!("`ClaimPath` cannot be empty")) + } else { + Ok(Self(value)) + } + } +} + +impl Deref for ClaimPath { + type Target = [ClaimPathSegment]; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// A single segment of a [`ClaimPath`]. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged, try_from = "Value")] +pub enum ClaimPathSegment { + /// JSON object property. + Name(String), + /// JSON array entry. + Position(usize), + /// All properties or entries. + #[serde(serialize_with = "serialize_all_variant")] + All, +} + +impl TryFrom for ClaimPathSegment { + type Error = anyhow::Error; + fn try_from(value: Value) -> Result { + match value { + Value::Null => Ok(ClaimPathSegment::All), + Value::String(s) => Ok(ClaimPathSegment::Name(s)), + Value::Number(n) => n + .as_u64() + .ok_or_else(|| anyhow::anyhow!("expected number greater or equal to 0")) + .map(|n| ClaimPathSegment::Position(n as usize)), + _ => Err(anyhow::anyhow!("expected either a string, number, or null")), + } + } +} + +fn serialize_all_variant(serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_none() +} + +/// Information about whether a given claim is selectively disclosable. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ClaimDisclosability { + /// The issuer **must** make the claim selectively disclosable. + Always, + /// The issuer **may** make the claim selectively disclosable. + #[default] + Allowed, + /// The issuer **must not** make the claim selectively disclosable. + Never, +} + +/// Display information for a given claim. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ClaimDisplay { + /// A language tag as defined in [RFC5646](https://www.rfc-editor.org/rfc/rfc5646.txt). + pub lang: String, + /// A human-readable label for the claim. + pub label: String, + /// A human-readable description for the claim. + pub description: Option, +} diff --git a/identity_credential/src/sd_jwt_vc/metadata/mod.rs b/identity_credential/src/sd_jwt_vc/metadata/mod.rs index 0413c4ce44..662c42032f 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/mod.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/mod.rs @@ -1,11 +1,13 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod claim; mod display; mod integrity; mod issuer; mod vc_type; +pub use claim::*; pub use display::*; pub use integrity::*; pub use issuer::*; diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index 255ffaf8af..dd4389aca8 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -10,6 +10,7 @@ use crate::sd_jwt_vc::Error; use crate::sd_jwt_vc::Resolver; use crate::sd_jwt_vc::Result; +use super::ClaimMetadata; use super::DisplayMetadata; use super::IntegrityMetadata; @@ -31,8 +32,12 @@ pub struct TypeMetadata { /// Either an embedded schema or a reference to one. #[serde(flatten)] pub schema: Option, - /// An object containing display information for the type. - pub display: Option, + /// A list containing display information for the type. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub display: Vec, + /// A list of [`ClaimMetadata`] containing information about particular claims. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub claims: Vec, } impl TypeMetadata { @@ -186,7 +191,8 @@ mod tests { description: None, extends: None, extends_integrity: None, - display: None, + display: vec![], + claims: vec![], schema: Some(TypeSchema::Object { schema: json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -209,7 +215,8 @@ mod tests { description: None, extends: None, extends_integrity: None, - display: None, + display: vec![], + claims: vec![], schema: Some(TypeSchema::Uri { schema_uri: Url::parse("https://example.com/vc_types/1").unwrap(), schema_uri_integrity: None, From 8d00263860e42756578617075b172fd380ab2e8e Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 20 Sep 2024 18:06:16 +0200 Subject: [PATCH 15/29] fetch issuer's JWK (to ease verification) --- Cargo.toml | 2 +- identity_credential/Cargo.toml | 74 ++++++------------- identity_credential/src/sd_jwt_vc/error.rs | 3 + .../src/sd_jwt_vc/metadata/issuer.rs | 7 +- .../src/sd_jwt_vc/metadata/vc_type.rs | 8 ++ identity_credential/src/sd_jwt_vc/mod.rs | 2 + identity_credential/src/sd_jwt_vc/token.rs | 70 ++++++++++++++++++ .../src/sd_jwt_vc/validation.rs | 11 +++ identity_resolver/Cargo.toml | 4 +- 9 files changed, 123 insertions(+), 58 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/validation.rs diff --git a/Cargo.toml b/Cargo.toml index d005618aac..6f35f6713f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ members = [ "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", - "compound_resolver", + "compound_resolver", ] exclude = ["bindings/wasm", "bindings/grpc"] diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index e69fcf001c..b847e5b56c 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -12,39 +12,24 @@ rust-version.workspace = true description = "An implementation of the Verifiable Credentials standard." [dependencies] +anyhow = { version = "1" } async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } -flate2 = { version = "1.0.28", default-features = false, features = [ - "rust_backend", -], optional = true } +flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } futures = { version = "0.3", default-features = false, optional = true, features = ["alloc"] } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } 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_verification = { version = "=1.3.1", path = "../identity_verification", default-features = false } -indexmap = { version = "2.0", default-features = false, features = [ - "std", - "serde", -] } -itertools = { version = "0.11", default-features = false, features = [ - "use_std", -], optional = true } +indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } +itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } json-proof-token = { workspace = true, optional = true } +jsonschema = { version = "0.19", optional = true, default-features = false } once_cell = { version = "1.18", default-features = false, features = ["std"] } -reqwest = { version = "0.11", default-features = false, features = [ - "default-tls", - "json", - "stream", -], optional = true } -roaring = { version = "0.10.2", default-features = false, features = [ - "serde", -], optional = true } -sd-jwt-payload = { version = "0.2.1", default-features = false, features = [ - "sha", -], optional = true } -sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", branch = "feat/sd-jwt-v11", default-features = false, features = [ - "sha", -], optional = true } +reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } +roaring = { version = "0.10.2", default-features = false, features = ["serde"], optional = true } +sd-jwt-payload = { version = "0.2.1", default-features = false, features = ["sha"], optional = true } +sd-jwt-payload-rework = { package = "sd-jwt-payload", git = "https://github.com/iotaledger/sd-jwt-payload.git", branch = "feat/sd-jwt-v11", default-features = false, features = ["sha"], optional = true } serde.workspace = true serde-aux = { version = "4.3.1", default-features = false } serde_json.workspace = true @@ -53,24 +38,13 @@ strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } zkryptium = { workspace = true, optional = true } -anyhow = { version = "1" } -jsonschema = { version = "0.19", optional = true, default-features = false } [dev-dependencies] anyhow = "1.0.62" -identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = [ - "ed25519", -] } -iota-crypto = { version = "0.23.2", default-features = false, features = [ - "ed25519", - "std", - "random", -] } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +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", -] } +tokio = { version = "1.35.0", default-features = false, features = ["rt-multi-thread", "macros"] } [package.metadata.docs.rs] # To build locally: @@ -80,13 +54,13 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [ - "revocation-bitmap", - "validator", - "credential", - "presentation", - "domain-linkage-fetch", - "sd-jwt", - "sd-jwt-vc", + "revocation-bitmap", + "validator", + "credential", + "presentation", + "domain-linkage-fetch", + "sd-jwt", + "sd-jwt-vc", ] credential = [] presentation = ["credential"] @@ -98,11 +72,11 @@ domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework", "dep:jsonschema"] jpt-bbs-plus = [ - "credential", - "validator", - "dep:zkryptium", - "dep:bls12_381_plus", - "dep:json-proof-token", + "credential", + "validator", + "dep:zkryptium", + "dep:bls12_381_plus", + "dep:json-proof-token", ] [lints] diff --git a/identity_credential/src/sd_jwt_vc/error.rs b/identity_credential/src/sd_jwt_vc/error.rs index 21246bba90..13af8911a3 100644 --- a/identity_credential/src/sd_jwt_vc/error.rs +++ b/identity_credential/src/sd_jwt_vc/error.rs @@ -48,6 +48,9 @@ pub enum Error { /// Credential validation failed. #[error("credential validation failed: {0}")] Validation(#[source] anyhow::Error), + /// SD-JWT VC signature verification failed. + #[error("verification failed: {0}")] + Verification(#[source] anyhow::Error), } /// Either a value of type `T` or an [`Error`]. diff --git a/identity_credential/src/sd_jwt_vc/metadata/issuer.rs b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs index 676200d904..7af0effd6c 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/issuer.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/issuer.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use identity_core::common::Url; -use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkSet; use serde::Deserialize; use serde::Serialize; @@ -51,10 +51,7 @@ pub enum Jwks { Uri(Url), /// An embedded JWK set. #[serde(rename = "jwks")] - Object { - /// List of JWKs. - keys: Vec, - }, + Object(JwkSet), } #[cfg(test)] diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index dd4389aca8..280f001d45 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -57,6 +57,14 @@ impl TypeMetadata { pub fn extends_integrity(&self) -> Option<&str> { self.extends_integrity.as_ref().map(|meta| meta.as_ref()) } + /// Returns the [`ClaimMetadata`]s associated with this credential type. + pub fn claim_metadata(&self) -> &[ClaimMetadata] { + &self.claims + } + /// Returns the [`DisplayMetadata`]s associated with this credential type. + pub fn display_metadata(&self) -> &[DisplayMetadata] { + &self.display + } /// Uses this [`TypeMetadata`] to validate JSON object `credential`. This method fails /// if the schema is referenced instead of embedded. /// Use [`TypeMetadata::validate_credential_with_resolver`] for such cases. diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index f576b4ecda..0b6aa746d9 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -10,6 +10,7 @@ mod presentation; mod resolver; mod status; mod token; +mod validation; pub use claims::*; pub use error::Error; @@ -18,3 +19,4 @@ pub use presentation::*; pub use resolver::*; pub use status::*; pub use token::*; +pub use validation::*; diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index dd6fc1ab1e..5ee3104418 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use super::claims::SdJwtVcClaims; use super::metadata::IssuerMetadata; +use super::metadata::Jwks; use super::metadata::TypeMetadata; #[allow(unused_imports)] use super::metadata::WELL_KNOWN_VCT; @@ -16,8 +17,11 @@ use super::Error; use super::Resolver; use super::Result; use super::SdJwtVcPresentationBuilder; +use anyhow::anyhow; use identity_core::common::StringOrUrl; use identity_core::common::Url; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkSet; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::SdJwt; @@ -121,7 +125,73 @@ impl SdJwtVc { Ok((metadata, raw)) } + + /// Resolves the issuer's public key in JWK format. + pub async fn issuer_jwk(&self, resolver: &R) -> Result + where + R: Resolver>, + { + let kid = self + .header() + .get("kid") + .and_then(|value| value.as_str()) + .ok_or_else(|| Error::Verification(anyhow!("missing header claim `kid`")))?; + + // Try to find the key among issuer metadata jwk set. + if let jwk @ Ok(_) = self.issuer_jwk_from_iss_metadata(resolver, kid).await { + jwk + } else { + // Issuer has no metadata that can lead to its JWK. Let's see if it can be resolved directly. + let jwk_uri = kid.parse::().map_err(|e| Error::Verification(e.into()))?; + resolver + .resolve(&jwk_uri) + .await + .map_err(|e| Error::Resolution { + input: jwk_uri.to_string(), + source: e, + }) + .and_then(|bytes| { + serde_json::from_slice(&bytes).map_err(|e| Error::Verification(anyhow!("invalid JWK: {}", e))) + }) + } + } + + async fn issuer_jwk_from_iss_metadata(&self, resolver: &R, kid: &str) -> Result + where + R: Resolver>, + { + let metadata = self + .issuer_metadata(resolver) + .await? + .ok_or_else(|| Error::Verification(anyhow!("missing issuer metadata")))?; + metadata.validate(self)?; + + let jwks = match metadata.jwks { + Jwks::Object(jwks) => jwks, + Jwks::Uri(jwks_uri) => resolver + .resolve(&jwks_uri) + .await + .map_err(|e| Error::Resolution { + input: jwks_uri.into_string(), + source: e, + }) + .and_then(|bytes| serde_json::from_slice::(&bytes).map_err(|e| Error::Verification(e.into())))?, + }; + jwks + .iter() + .find(|jwk| jwk.kid() == Some(kid)) + .cloned() + .ok_or_else(|| Error::Verification(anyhow!("missing key \"{kid}\" in issuer JWK set"))) + } } +// Verifies SD-JWT signature. +// pub async fn verify(&self, resolver: &R, jws_verifier: &V) -> Result<(), SignatureVerificationError> +// where +// R: Resolver, +// V: JwsVerifier, +// { +// // Fetch the issuer's public key. +// } /// Converts `vct` claim's URI value into the appropriate well-known URL. /// ## Warnings diff --git a/identity_credential/src/sd_jwt_vc/validation.rs b/identity_credential/src/sd_jwt_vc/validation.rs new file mode 100644 index 0000000000..adce6ed4c3 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/validation.rs @@ -0,0 +1,11 @@ +use crate::validator::JwtCredentialValidationOptions; + +/// Options to decide which operations should be performed during SD-JWT VC validation. +#[derive(Debug, Clone)] +pub struct ValidationOptions { + /// Credential validation options. + pub credential_validation_options: JwtCredentialValidationOptions, + /// The credential will be checked using the credential type + /// specified through the `vct` claim. + pub vct: bool, +} diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index c5025fb527..5bfdf7ccd1 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -10,16 +10,16 @@ rust-version.workspace = true [dependencies] anyhow = "1.0.86" -thiserror.workspace = true -iota-sdk = { version = "1.1.5" } # This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } +iota-sdk = { version = "1.1.5" } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } strum.workspace = true +thiserror.workspace = true [dependencies.identity_iota_core] version = "=1.3.1" From 2111e3b94fa9e443d6d6d9fc13d07e328ee56cad Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 24 Sep 2024 15:26:56 +0200 Subject: [PATCH 16/29] validate claim disclosability --- .../src/sd_jwt_vc/metadata/claim.rs | 183 +++++++++++++++++- identity_credential/src/sd_jwt_vc/mod.rs | 2 +- identity_credential/src/sd_jwt_vc/token.rs | 41 +++- .../jwt_credential_validator.rs | 2 +- 4 files changed, 217 insertions(+), 11 deletions(-) diff --git a/identity_credential/src/sd_jwt_vc/metadata/claim.rs b/identity_credential/src/sd_jwt_vc/metadata/claim.rs index 1c6c852ba5..1b663fb376 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/claim.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/claim.rs @@ -1,10 +1,16 @@ +use std::fmt::Display; use std::ops::Deref; +use anyhow::anyhow; +use anyhow::Context; +use itertools::Itertools; use serde::Deserialize; use serde::Serialize; use serde::Serializer; use serde_json::Value; +use crate::sd_jwt_vc::Error; + /// Information about a particular claim for displaying and validation purposes. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ClaimMetadata { @@ -19,13 +25,49 @@ pub struct ClaimMetadata { pub svg_id: Option, } +impl ClaimMetadata { + /// Checks wheter `value` is compliant with the disclosability policy imposed by this [`ClaimMetadata`]. + pub fn check_value_disclosability(&self, value: &Value) -> Result<(), Error> { + if self.sd.unwrap_or_default() == ClaimDisclosability::Allowed { + return Ok(()); + } + + let interested_claims = self.path.reverse_index(value); + if self.sd.unwrap_or_default() == ClaimDisclosability::Always && interested_claims.is_ok() { + return Err(Error::Validation(anyhow!( + "claim or claims with path {} must always be disclosable", + &self.path + ))); + } + + if self.sd.unwrap_or_default() == ClaimDisclosability::Never && interested_claims.is_err() { + return Err(Error::Validation(anyhow!( + "claim or claims with path {} must never be disclosable", + &self.path + ))); + } + + Ok(()) + } +} + /// A non-empty list of string, `null` values, or non-negative integers. -/// It is used to selected a particular claim in the credential or a +/// It is used to select a particular claim in the credential or a /// set of claims. See [Claim Path](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-05.html#name-claim-path) for more information. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(try_from = "Vec")] pub struct ClaimPath(Vec); +impl ClaimPath { + fn reverse_index<'v>(&self, value: &'v Value) -> anyhow::Result> { + let mut segments = self.iter(); + let first_segment = segments.next().context("empty claim path")?; + segments.try_fold(index_value(value, first_segment)?, |values, segment| { + values.get(segment) + }) + } +} + impl TryFrom> for ClaimPath { type Error = anyhow::Error; fn try_from(value: Vec) -> Result { @@ -37,6 +79,13 @@ impl TryFrom> for ClaimPath { } } +impl Display for ClaimPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let segments = self.iter().join(", "); + write!(f, "[{segments}]") + } +} + impl Deref for ClaimPath { type Target = [ClaimPathSegment]; fn deref(&self) -> &Self::Target { @@ -57,6 +106,16 @@ pub enum ClaimPathSegment { All, } +impl Display for ClaimPathSegment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::All => write!(f, "null"), + Self::Name(name) => write!(f, "\"{name}\""), + Self::Position(i) => write!(f, "{i}"), + } + } +} + impl TryFrom for ClaimPathSegment { type Error = anyhow::Error; fn try_from(value: Value) -> Result { @@ -102,3 +161,125 @@ pub struct ClaimDisplay { /// A human-readable description for the claim. pub description: Option, } + +enum OneOrManyValue<'v> { + One(&'v Value), + Many(Box + 'v>), +} + +impl<'v> OneOrManyValue<'v> { + fn get(self, segment: &ClaimPathSegment) -> anyhow::Result> { + match self { + Self::One(value) => index_value(value, segment), + Self::Many(values) => { + let new_values = values + .map(|value| index_value(value, segment)) + .collect::>>()? + .into_iter() + .flatten(); + + Ok(OneOrManyValue::Many(Box::new(new_values))) + } + } + } +} + +struct OneOrManyValueIter<'v>(Option>); + +impl<'v> OneOrManyValueIter<'v> { + fn new(value: OneOrManyValue<'v>) -> Self { + Self(Some(value)) + } +} + +impl<'v> IntoIterator for OneOrManyValue<'v> { + type IntoIter = OneOrManyValueIter<'v>; + type Item = &'v Value; + fn into_iter(self) -> Self::IntoIter { + OneOrManyValueIter::new(self) + } +} + +impl<'v> Iterator for OneOrManyValueIter<'v> { + type Item = &'v Value; + fn next(&mut self) -> Option { + match self.0.take()? { + OneOrManyValue::One(v) => Some(v), + OneOrManyValue::Many(mut values) => { + let value = values.next(); + self.0 = Some(OneOrManyValue::Many(values)); + + value + } + } + } +} + +fn index_value<'v>(value: &'v Value, segment: &ClaimPathSegment) -> anyhow::Result> { + match segment { + ClaimPathSegment::Name(name) => value.get(name).map(OneOrManyValue::One), + ClaimPathSegment::Position(i) => value.get(i).map(OneOrManyValue::One), + ClaimPathSegment::All => value + .as_array() + .map(|values| OneOrManyValue::Many(Box::new(values.iter()))), + } + .ok_or_else(|| anyhow::anyhow!("value {value:#} has no element {segment}")) +} + +#[cfg(test)] +mod tests { + use std::cell::LazyCell; + + use serde_json::json; + + use super::*; + + const SAMPLE_OBJ: LazyCell = LazyCell::new(|| { + json!({ + "vct": "https://betelgeuse.example.com/education_credential", + "name": "Arthur Dent", + "address": { + "street_address": "42 Market Street", + "city": "Milliways", + "postal_code": "12345" + }, + "degrees": [ + { + "type": "Bachelor of Science", + "university": "University of Betelgeuse" + }, + { + "type": "Master of Science", + "university": "University of Betelgeuse" + } + ], + "nationalities": ["British", "Betelgeusian"] + }) + }); + + #[test] + fn claim_path_works() { + let name_path = serde_json::from_value::(json!(["name"])).unwrap(); + let city_path = serde_json::from_value::(json!(["address", "city"])).unwrap(); + let first_degree_path = serde_json::from_value::(json!(["degrees", 0])).unwrap(); + let degrees_types_path = serde_json::from_value::(json!(["degrees", null, "type"])).unwrap(); + + assert!(matches!( + name_path.reverse_index(&SAMPLE_OBJ).unwrap(), + OneOrManyValue::One(&Value::String(_)) + )); + assert!(matches!( + city_path.reverse_index(&SAMPLE_OBJ).unwrap(), + OneOrManyValue::One(&Value::String(_)) + )); + assert!(matches!( + first_degree_path.reverse_index(&SAMPLE_OBJ).unwrap(), + OneOrManyValue::One(&Value::Object(_)) + )); + let obj = &*SAMPLE_OBJ; + let mut degree_types = degrees_types_path.reverse_index(obj).unwrap().into_iter(); + assert_eq!(degree_types.next().unwrap().as_str(), Some("Bachelor of Science")); + assert_eq!(degree_types.next().unwrap().as_str(), Some("Master of Science")); + assert_eq!(degree_types.next(), None); + } +} diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index 0b6aa746d9..96fd69bce8 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -16,7 +16,7 @@ pub use claims::*; pub use error::Error; pub use error::Result; pub use presentation::*; -pub use resolver::*; +pub use resolver::Resolver; pub use status::*; pub use token::*; pub use validation::*; diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index 5ee3104418..1e1c7bdd40 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -6,6 +6,7 @@ use std::ops::Deref; use std::str::FromStr; use super::claims::SdJwtVcClaims; +use super::metadata::ClaimMetadata; use super::metadata::IssuerMetadata; use super::metadata::Jwks; use super::metadata::TypeMetadata; @@ -17,11 +18,13 @@ use super::Error; use super::Resolver; use super::Result; use super::SdJwtVcPresentationBuilder; +use crate::validator::JwtCredentialValidator as JwsUtils; use anyhow::anyhow; use identity_core::common::StringOrUrl; use identity_core::common::Url; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkSet; +use identity_verification::jws::JwsVerifier; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::SdJwt; @@ -127,6 +130,9 @@ impl SdJwtVc { } /// Resolves the issuer's public key in JWK format. + /// The issuer's JWK is first fetched through the issuer's metadata, + /// if this attempt fails `resolver` is used to query the key directly + /// through `kid`'s value. pub async fn issuer_jwk(&self, resolver: &R) -> Result where R: Resolver>, @@ -183,15 +189,34 @@ impl SdJwtVc { .cloned() .ok_or_else(|| Error::Verification(anyhow!("missing key \"{kid}\" in issuer JWK set"))) } + + /// Verifies this [`SdJwtVc`] JWT's signature. + pub fn verify_signature(&self, jws_verifier: &V, jwk: &Jwk) -> Result<()> + where + V: JwsVerifier, + { + let sd_jwt_str = self.sd_jwt.to_string(); + let jws_input = { + let jwt_str = sd_jwt_str.split_once('~').unwrap().0; + JwsUtils::::decode(jwt_str).map_err(|e| Error::Verification(e.into()))? + }; + + JwsUtils::::verify_signature_raw(jws_input, jwk, jws_verifier) + .map_err(|e| Error::Verification(e.into())) + .and(Ok(())) + } + + /// Checks the disclosability of this [`SdJwtVc`]'s claims against a list of [`ClaimMetadata`]. + /// ## Notes + /// This check should be performed by the token's holder in order to assert the issuer's compliance with + /// the credential's type. + pub fn validate_claims_disclosability(&self, hasher: &dyn Hasher, claims_metadata: &[ClaimMetadata]) -> Result<()> { + let disclosed_object = Value::Object(self.clone().into_disclosed_object(hasher)?); + claims_metadata + .iter() + .try_fold((), |_, meta| meta.check_value_disclosability(&disclosed_object)) + } } -// Verifies SD-JWT signature. -// pub async fn verify(&self, resolver: &R, jws_verifier: &V) -> Result<(), SignatureVerificationError> -// where -// R: Resolver, -// V: JwsVerifier, -// { -// // Fetch the issuer's public key. -// } /// Converts `vct` claim's URI value into the appropriate well-known URL. /// ## Warnings diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs index c099d763ab..acaa991e45 100644 --- a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator.rs @@ -297,7 +297,7 @@ impl JwtCredentialValidator { } /// Verify the signature using the given `public_key` and `signature_verifier`. - fn verify_decoded_signature( + pub(crate) fn verify_decoded_signature( decoded: JwsValidationItem<'_>, public_key: &Jwk, signature_verifier: &S, From 06e31f288dd663db21f7a297047d1af0a380b46e Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 24 Sep 2024 16:40:46 +0200 Subject: [PATCH 17/29] add missing license header --- identity_credential/src/sd_jwt_vc/metadata/claim.rs | 3 +++ identity_credential/src/sd_jwt_vc/metadata/display.rs | 3 +++ identity_credential/src/sd_jwt_vc/metadata/integrity.rs | 3 +++ 3 files changed, 9 insertions(+) diff --git a/identity_credential/src/sd_jwt_vc/metadata/claim.rs b/identity_credential/src/sd_jwt_vc/metadata/claim.rs index 1b663fb376..28a0b0faef 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/claim.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/claim.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::fmt::Display; use std::ops::Deref; diff --git a/identity_credential/src/sd_jwt_vc/metadata/display.rs b/identity_credential/src/sd_jwt_vc/metadata/display.rs index 95368a9f78..dade4816b5 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/display.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/display.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use serde::Deserialize; use serde::Serialize; use serde_json::Value; diff --git a/identity_credential/src/sd_jwt_vc/metadata/integrity.rs b/identity_credential/src/sd_jwt_vc/metadata/integrity.rs index 2065f50736..d41ca1f097 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/integrity.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/integrity.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::fmt::Display; use std::str::FromStr; From e5e8537e37d9e55caa363418efb72bc6a1106703 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 24 Sep 2024 17:16:27 +0200 Subject: [PATCH 18/29] resolver change, validation --- .../src/sd_jwt_vc/metadata/vc_type.rs | 9 ++-- identity_credential/src/sd_jwt_vc/resolver.rs | 8 ++-- identity_credential/src/sd_jwt_vc/token.rs | 48 ++++++++++++++++--- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index 280f001d45..9cb07d9cc3 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -85,7 +85,7 @@ impl TypeMetadata { /// another type or JSON schema. pub async fn validate_credential_with_resolver(&self, credential: &Value, resolver: &R) -> Result<()> where - R: Resolver + Sync, + R: Resolver + Sync, { validate_credential_impl(self.clone(), credential, resolver, vec![]).await } @@ -99,7 +99,7 @@ fn validate_credential_impl<'c, 'r, R>( mut passed_types: Vec, ) -> BoxFuture<'c, Result<()>> where - R: Resolver + Sync, + R: Resolver + Sync, 'r: 'c, { async move { @@ -233,9 +233,8 @@ mod tests { struct SchemaResolver; #[async_trait] - impl Resolver for SchemaResolver { - type Target = Value; - async fn resolve(&self, _input: &Url) -> resolver::Result { + impl Resolver for SchemaResolver { + async fn resolve(&self, _input: &Url) -> resolver::Result { Ok(serde_json::to_value(IMMEDIATE_TYPE_METADATA.clone().schema).unwrap()) } } diff --git a/identity_credential/src/sd_jwt_vc/resolver.rs b/identity_credential/src/sd_jwt_vc/resolver.rs index 43baddf49f..fbcd0fb273 100644 --- a/identity_credential/src/sd_jwt_vc/resolver.rs +++ b/identity_credential/src/sd_jwt_vc/resolver.rs @@ -15,11 +15,9 @@ pub enum Error { Generic(#[from] anyhow::Error), } -/// A type capable of asynchronously producing values of type [`Resolver::Target`] from inputs of type `I`. +/// A type capable of asynchronously producing values of type `T` from inputs of type `I`. #[async_trait] -pub trait Resolver { - /// The resulting type. - type Target; +pub trait Resolver { /// Fetch the resource of type [`Resolver::Target`] using `input`. - async fn resolve(&self, input: &I) -> Result; + async fn resolve(&self, input: &I) -> Result; } diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index 1e1c7bdd40..27448ad718 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -87,7 +87,7 @@ impl SdJwtVc { /// To check if the retrieved [`IssuerMetadata`] is valid use [`IssuerMetadata::validate`]. pub async fn issuer_metadata(&self, resolver: &R) -> Result> where - R: Resolver>, + R: Resolver>, { let metadata_url = { let origin = self.claims().iss.origin().ascii_serialization(); @@ -117,10 +117,13 @@ impl SdJwtVc { /// The latter can be used to validate the `vct#integrity` claim if present. pub async fn type_metadata(&self, resolver: &R) -> Result<(TypeMetadata, Vec)> where - R: Resolver>, + R: Resolver>, { - let vct = &self.claims().vct; - let raw = resolver.resolve(vct).await.map_err(|e| Error::Resolution { + let vct = match self.claims().vct.clone() { + StringOrUrl::Url(url) => StringOrUrl::Url(vct_to_url(&url).unwrap_or(url)), + s => s, + }; + let raw = resolver.resolve(&vct).await.map_err(|e| Error::Resolution { input: vct.to_string(), source: e, })?; @@ -135,7 +138,7 @@ impl SdJwtVc { /// through `kid`'s value. pub async fn issuer_jwk(&self, resolver: &R) -> Result where - R: Resolver>, + R: Resolver>, { let kid = self .header() @@ -164,7 +167,7 @@ impl SdJwtVc { async fn issuer_jwk_from_iss_metadata(&self, resolver: &R, kid: &str) -> Result where - R: Resolver>, + R: Resolver>, { let metadata = self .issuer_metadata(resolver) @@ -216,6 +219,39 @@ impl SdJwtVc { .iter() .try_fold((), |_, meta| meta.check_value_disclosability(&disclosed_object)) } + + /// Check whether this [`SdJwtVc`] is valid. + /// + /// This method checks: + /// - JWS signature + /// - credential's type + /// - claims' disclosability + pub async fn validate(&self, resolver: &R, jws_verifier: &V, hasher: &dyn Hasher) -> Result<()> + where + R: Resolver>, + R: Resolver>, + R: Resolver, + R: Sync, + V: JwsVerifier, + { + // Signature verification. + // Fetch issuer's JWK. + let jwk = self.issuer_jwk(resolver).await?; + self.verify_signature(jws_verifier, &jwk)?; + + // Credential type. + // Fetch type metadata. Skip integrity check. + let fully_disclosed_token = self.clone().into_disclosed_object(hasher).map(Value::Object)?; + let (type_metadata, _) = self.type_metadata(resolver).await?; + type_metadata + .validate_credential_with_resolver(&fully_disclosed_token, resolver) + .await?; + + // Claims' disclosability. + self.validate_claims_disclosability(hasher, type_metadata.claim_metadata())?; + + Ok(()) + } } /// Converts `vct` claim's URI value into the appropriate well-known URL. From 9bf907fd2840e8bff144bd139685ccd82faed31d Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 30 Sep 2024 10:59:20 +0200 Subject: [PATCH 19/29] SdJwtVcBuilder & tests --- identity_credential/Cargo.toml | 1 + identity_credential/src/sd_jwt_vc/builder.rs | 324 ++++++++++++++++++ identity_credential/src/sd_jwt_vc/claims.rs | 5 +- identity_credential/src/sd_jwt_vc/mod.rs | 2 + .../src/sd_jwt_vc/presentation.rs | 6 +- 5 files changed, 334 insertions(+), 4 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/builder.rs diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index b847e5b56c..7ae55656aa 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -45,6 +45,7 @@ identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-feature 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"] } +josekit = "0.8" [package.metadata.docs.rs] # To build locally: diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs new file mode 100644 index 0000000000..29081f4fad --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -0,0 +1,324 @@ +use std::cell::LazyCell; + +use identity_core::common::{StringOrUrl, Timestamp, Url}; +use sd_jwt_payload_rework::{Hasher, JsonObject, JwsSigner, RequiredKeyBinding}; +use sd_jwt_payload_rework::{SdJwtBuilder, Sha256Hasher}; +use serde::Serialize; +use serde_json::json; + +use super::SdJwtVc; +use super::{Error, Status}; +use super::{Result, SD_JWT_VC_TYP}; + +const DEFAULT_HEADER: LazyCell = LazyCell::new(|| { + let mut object = JsonObject::default(); + object.insert("typ".to_string(), SD_JWT_VC_TYP.into()); + object +}); + +macro_rules! claim_to_key_value_pair { + ( $( $claim:ident ),+ ) => { + { + let mut claim_list = Vec::<(&'static str, serde_json::Value)>::new(); + $( + claim_list.push((stringify!($claim), serde_json::to_value($claim).unwrap())); + )* + claim_list + } + }; +} + +/// A structure to ease the creation of an [`SdJwtVc`]. +#[derive(Debug)] +pub struct SdJwtVcBuilder { + inner_builder: SdJwtBuilder, + header: JsonObject, + iss: Option, + nbf: Option, + exp: Option, + iat: Option, + vct: Option, + sub: Option, + status: Option, +} + +impl Default for SdJwtVcBuilder { + fn default() -> Self { + Self { + inner_builder: SdJwtBuilder::::new(json!({})).unwrap(), + header: DEFAULT_HEADER.clone(), + iss: None, + nbf: None, + exp: None, + iat: None, + vct: None, + sub: None, + status: None, + } + } +} + +impl SdJwtVcBuilder { + /// Creates a new [`SdJwtVcBuilder`] using `object` JSON representation and default + /// `sha-256` hasher. + pub fn new(object: T) -> Result { + let inner_builder = SdJwtBuilder::::new(object)?; + Ok(Self { + header: DEFAULT_HEADER.clone(), + inner_builder, + ..Default::default() + }) + } +} + +impl SdJwtVcBuilder { + /// Creates a new [`SdJwtVcBuilder`] using `object` JSON representation and a given + /// hasher `hasher`. + pub fn new_with_hasher(object: T, hasher: H) -> Result { + let inner_builder = SdJwtBuilder::new_with_hasher(object, hasher)?; + Ok(Self { + inner_builder, + header: DEFAULT_HEADER.clone(), + iss: None, + nbf: None, + exp: None, + iat: None, + vct: None, + sub: None, + status: None, + }) + } + + /// Substitutes a value with the digest of its disclosure. + /// + /// ## Notes + /// - `path` indicates the pointer to the value that will be concealed using the syntax of [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). + /// + /// ## Example + /// ```rust + /// use serde_json::json; + /// use identity_credential::sd_jwt_vc::SdJwtVcBuilder; + /// + /// let obj = json!({ + /// "id": "did:value", + /// "claim1": { + /// "abc": true + /// }, + /// "claim2": ["val_1", "val_2"] + /// }); + /// let builder = SdJwtVcBuilder::new(obj) + /// .unwrap() + /// .make_concealable("/id").unwrap() //conceals "id": "did:value" + /// .make_concealable("/claim1/abc").unwrap() //"abc": true + /// .make_concealable("/claim2/0").unwrap(); //conceals "val_1" + /// ``` + pub fn make_concealable(mut self, path: &str) -> Result { + self.inner_builder = self.inner_builder.make_concealable(path)?; + Ok(self) + } + + /// Sets the JWT header. + /// ## Notes + /// - if [`SdJwtVcBuilder::header`] is not called, the default header is used: + /// ```json + /// { + /// "typ": "sd-jwt", + /// "alg": "" + /// } + /// ``` + /// - `alg` is always replaced with the value passed to [`SdJwtVcBuilder::finish`]. + pub fn header(mut self, header: JsonObject) -> Self { + self.header = header; + self + } + + /// Adds a decoy digest to the specified path. + /// + /// `path` indicates the pointer to the value that will be concealed using the syntax of + /// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901). + /// + /// Use `path` = "" to add decoys to the top level. + pub fn add_decoys(mut self, path: &str, number_of_decoys: usize) -> Result { + self.inner_builder = self.inner_builder.add_decoys(path, number_of_decoys)?; + + Ok(self) + } + + /// Require a proof of possession of a given key from the holder. + /// + /// This operation adds a JWT confirmation (`cnf`) claim as specified in + /// [RFC8300](https://www.rfc-editor.org/rfc/rfc7800.html#section-3). + pub fn require_key_binding(mut self, key_bind: RequiredKeyBinding) -> Self { + self.inner_builder = self.inner_builder.require_key_binding(key_bind); + self + } + + /// Inserts an `iss` claim. See [`super::SdJwtVcClaims::iss`]. + pub fn iss(mut self, issuer: Url) -> Self { + self.iss = Some(issuer); + self + } + + /// Inserts a `nbf` claim. See [`super::SdJwtVcClaims::nbf`]. + pub fn nbf(mut self, nbf: Timestamp) -> Self { + self.nbf = Some(nbf.to_unix()); + self + } + + /// Inserts a `exp` claim. See [`super::SdJwtVcClaims::exp`]. + pub fn exp(mut self, exp: Timestamp) -> Self { + self.exp = Some(exp.to_unix()); + self + } + + /// Inserts a `iat` claim. See [`super::SdJwtVcClaims::iat`]. + pub fn iat(mut self, iat: Timestamp) -> Self { + self.iat = Some(iat.to_unix()); + self + } + + /// Inserts a `vct` claim. See [`super::SdJwtVcClaims::vct`]. + pub fn vct(mut self, vct: impl Into) -> Self { + self.vct = Some(vct.into()); + self + } + + /// Inserts a `sub` claim. See [`super::SdJwtVcClaims::sub`]. + pub fn sub(mut self, sub: impl Into) -> Self { + self.sub = Some(sub.into()); + self + } + + /// Inserts a `status` claim. See [`super::SdJwtVcClaims::status`]. + pub fn status(mut self, status: Status) -> Self { + self.status = Some(status); + self + } + + /// Creates an [`SdJwtVc`] with the provided data. + pub async fn finish(self, signer: &S, alg: &str) -> Result + where + S: JwsSigner, + { + let Self { + inner_builder, + mut header, + iss, + nbf, + exp, + iat, + vct, + sub, + status, + } = self; + // Check header. + header + .entry("typ") + .or_insert_with(|| SD_JWT_VC_TYP.to_owned().into()) + .as_str() + .filter(|typ| typ.contains(SD_JWT_VC_TYP)) + .ok_or_else(|| Error::InvalidJoseType(String::default()))?; + + let builder = inner_builder.header(header); + + // Insert SD-JWT VC claims into object. + let builder = claim_to_key_value_pair![iss, nbf, exp, iat, vct, sub, status] + .into_iter() + .filter(|(_, value)| !value.is_null()) + .fold(builder, |builder, (key, value)| builder.insert_claim(key, value)); + + let sd_jwt = builder.finish(signer, alg).await?; + SdJwtVc::try_from(sd_jwt) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use josekit::{ + jwt::{self, JwtPayload}, + {jws::JwsHeader, jws::HS256}, + }; + use serde_json::json; + + struct TestSigner; + + #[async_trait] + impl JwsSigner for TestSigner { + type Error = josekit::JoseError; + async fn sign(&self, header: &JsonObject, payload: &JsonObject) -> std::result::Result, Self::Error> { + let signer = HS256.signer_from_bytes(b"0123456789ABCDEF0123456789ABCDEF")?; + let header = JwsHeader::from_map(header.clone())?; + let payload = JwtPayload::from_map(payload.clone())?; + let jws = jwt::encode_with_signer(&payload, &header, &signer)?; + + Ok(jws.into_bytes()) + } + } + + #[tokio::test] + async fn building_valid_vc_works() -> anyhow::Result<()> { + let credential = json!({ + "name": "John Doe", + "birthdate": "1970-01-01" + }); + + SdJwtVcBuilder::new(credential)? + .vct("https://bmi.bund.example/credential/pid/1.0".parse::()?) + .iat(Timestamp::now_utc()) + .iss("https://example.com/".parse()?) + .make_concealable("/birthdate")? + .finish(&TestSigner, "HS256") + .await?; + + Ok(()) + } + + #[tokio::test] + async fn building_vc_with_missing_mandatory_claims_fails() -> anyhow::Result<()> { + let credential = json!({ + "name": "John Doe", + "birthdate": "1970-01-01" + }); + + let err = SdJwtVcBuilder::new(credential)? + .vct("https://bmi.bund.example/credential/pid/1.0".parse::()?) + .iat(Timestamp::now_utc()) + // issuer is missing. + .make_concealable("/birthdate")? + .finish(&TestSigner, "HS256") + .await + .unwrap_err(); + assert!(matches!(err, Error::MissingClaim("iss"))); + + Ok(()) + } + + #[tokio::test] + async fn building_vc_with_invalid_mandatory_claims_fails() -> anyhow::Result<()> { + let credential = json!({ + "name": "John Doe", + "birthdate": "1970-01-01", + "vct": { "id": 1234567890 } + }); + + let err = SdJwtVcBuilder::new(credential)? + .iat(Timestamp::now_utc()) + .iss("https://example.com".parse()?) + .make_concealable("/birthdate")? + .finish(&TestSigner, "HS256") + .await + .unwrap_err(); + + assert!(matches!( + err, + Error::InvalidClaimValue { + name: "vct", + .. + } + )); + + Ok(()) + } +} diff --git a/identity_credential/src/sd_jwt_vc/claims.rs b/identity_credential/src/sd_jwt_vc/claims.rs index c3ac85b8ed..848cc6d6af 100644 --- a/identity_credential/src/sd_jwt_vc/claims.rs +++ b/identity_credential/src/sd_jwt_vc/claims.rs @@ -9,6 +9,8 @@ use identity_core::common::Timestamp; use identity_core::common::Url; use sd_jwt_payload_rework::Disclosure; use sd_jwt_payload_rework::SdJwtClaims; +use serde::Deserialize; +use serde::Serialize; use serde_json::Value; use super::Error; @@ -16,7 +18,7 @@ use super::Result; use super::Status; /// JOSE payload claims for SD-JWT VC. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[non_exhaustive] pub struct SdJwtVcClaims { /// Issuer. @@ -41,6 +43,7 @@ pub struct SdJwtVcClaims { /// Subject. /// See [RFC7519 section 4.1.2](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2) for more information. pub sub: Option, + #[serde(flatten)] sd_jwt_claims: SdJwtClaims, } diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index 96fd69bce8..df9bd7c4a5 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -11,6 +11,7 @@ mod resolver; mod status; mod token; mod validation; +mod builder; pub use claims::*; pub use error::Error; @@ -20,3 +21,4 @@ pub use resolver::Resolver; pub use status::*; pub use token::*; pub use validation::*; +pub use builder::*; diff --git a/identity_credential/src/sd_jwt_vc/presentation.rs b/identity_credential/src/sd_jwt_vc/presentation.rs index ec3c2bef4b..26a80a2164 100644 --- a/identity_credential/src/sd_jwt_vc/presentation.rs +++ b/identity_credential/src/sd_jwt_vc/presentation.rs @@ -44,8 +44,8 @@ impl SdJwtVcPresentationBuilder { } /// Returns the resulting [`SdJwtVc`] together with all removed disclosures. - pub fn finish(self) -> (SdJwtVc, Vec) { - let (sd_jwt, disclosures) = self.builder.finish(); - (SdJwtVc::new(sd_jwt, self.vc_claims), disclosures) + pub fn finish(self) -> Result<(SdJwtVc, Vec)> { + let (sd_jwt, disclosures) = self.builder.finish()?; + Ok((SdJwtVc::new(sd_jwt, self.vc_claims), disclosures)) } } From 67a0abce255dc8c7fe8524055dcf055549b0cb47 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 1 Oct 2024 13:17:16 +0200 Subject: [PATCH 20/29] validation test --- identity_credential/src/sd_jwt_vc/builder.rs | 81 +++++----- identity_credential/src/sd_jwt_vc/claims.rs | 2 +- identity_credential/src/sd_jwt_vc/mod.rs | 8 +- .../src/sd_jwt_vc/tests/mod.rs | 110 ++++++++++++++ .../src/sd_jwt_vc/tests/validation.rs | 139 ++++++++++++++++++ identity_credential/src/sd_jwt_vc/token.rs | 10 +- .../src/sd_jwt_vc/validation.rs | 11 -- 7 files changed, 299 insertions(+), 62 deletions(-) create mode 100644 identity_credential/src/sd_jwt_vc/tests/mod.rs create mode 100644 identity_credential/src/sd_jwt_vc/tests/validation.rs delete mode 100644 identity_credential/src/sd_jwt_vc/validation.rs diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index 29081f4fad..4b31b876b9 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -1,14 +1,22 @@ use std::cell::LazyCell; -use identity_core::common::{StringOrUrl, Timestamp, Url}; -use sd_jwt_payload_rework::{Hasher, JsonObject, JwsSigner, RequiredKeyBinding}; -use sd_jwt_payload_rework::{SdJwtBuilder, Sha256Hasher}; +use identity_core::common::StringOrUrl; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use sd_jwt_payload_rework::Hasher; +use sd_jwt_payload_rework::JsonObject; +use sd_jwt_payload_rework::JwsSigner; +use sd_jwt_payload_rework::RequiredKeyBinding; +use sd_jwt_payload_rework::SdJwtBuilder; +use sd_jwt_payload_rework::Sha256Hasher; use serde::Serialize; use serde_json::json; +use super::Error; +use super::Result; use super::SdJwtVc; -use super::{Error, Status}; -use super::{Result, SD_JWT_VC_TYP}; +use super::Status; +use super::SD_JWT_VC_TYP; const DEFAULT_HEADER: LazyCell = LazyCell::new(|| { let mut object = JsonObject::default(); @@ -119,13 +127,8 @@ impl SdJwtVcBuilder { /// Sets the JWT header. /// ## Notes - /// - if [`SdJwtVcBuilder::header`] is not called, the default header is used: - /// ```json - /// { - /// "typ": "sd-jwt", - /// "alg": "" - /// } - /// ``` + /// - if [`SdJwtVcBuilder::header`] is not called, the default header is used: ```json { "typ": "sd-jwt", "alg": + /// "" } ``` /// - `alg` is always replaced with the value passed to [`SdJwtVcBuilder::finish`]. pub fn header(mut self, header: JsonObject) -> Self { self.header = header; @@ -235,27 +238,7 @@ impl SdJwtVcBuilder { #[cfg(test)] mod tests { use super::*; - use async_trait::async_trait; - use josekit::{ - jwt::{self, JwtPayload}, - {jws::JwsHeader, jws::HS256}, - }; - use serde_json::json; - - struct TestSigner; - - #[async_trait] - impl JwsSigner for TestSigner { - type Error = josekit::JoseError; - async fn sign(&self, header: &JsonObject, payload: &JsonObject) -> std::result::Result, Self::Error> { - let signer = HS256.signer_from_bytes(b"0123456789ABCDEF0123456789ABCDEF")?; - let header = JwsHeader::from_map(header.clone())?; - let payload = JwtPayload::from_map(payload.clone())?; - let jws = jwt::encode_with_signer(&payload, &header, &signer)?; - - Ok(jws.into_bytes()) - } - } + use crate::sd_jwt_vc::tests::TestSigner; #[tokio::test] async fn building_valid_vc_works() -> anyhow::Result<()> { @@ -310,14 +293,30 @@ mod tests { .finish(&TestSigner, "HS256") .await .unwrap_err(); - - assert!(matches!( - err, - Error::InvalidClaimValue { - name: "vct", - .. - } - )); + + assert!(matches!(err, Error::InvalidClaimValue { name: "vct", .. })); + + Ok(()) + } + + #[tokio::test] + async fn building_vc_with_disclosed_mandatory_claim_fails() -> anyhow::Result<()> { + let credential = json!({ + "name": "John Doe", + "birthdate": "1970-01-01", + "vct": { "id": 1234567890 } + }); + + let err = SdJwtVcBuilder::new(credential)? + .iat(Timestamp::now_utc()) + .iss("https://example.com".parse()?) + .make_concealable("/birthdate")? + .make_concealable("/vct")? + .finish(&TestSigner, "HS256") + .await + .unwrap_err(); + + assert!(matches!(err, Error::DisclosedClaim("vct"))); Ok(()) } diff --git a/identity_credential/src/sd_jwt_vc/claims.rs b/identity_credential/src/sd_jwt_vc/claims.rs index 848cc6d6af..9fcec11ce3 100644 --- a/identity_credential/src/sd_jwt_vc/claims.rs +++ b/identity_credential/src/sd_jwt_vc/claims.rs @@ -44,7 +44,7 @@ pub struct SdJwtVcClaims { /// See [RFC7519 section 4.1.2](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2) for more information. pub sub: Option, #[serde(flatten)] - sd_jwt_claims: SdJwtClaims, + pub(crate) sd_jwt_claims: SdJwtClaims, } impl Deref for SdJwtVcClaims { diff --git a/identity_credential/src/sd_jwt_vc/mod.rs b/identity_credential/src/sd_jwt_vc/mod.rs index df9bd7c4a5..f3173264bf 100644 --- a/identity_credential/src/sd_jwt_vc/mod.rs +++ b/identity_credential/src/sd_jwt_vc/mod.rs @@ -1,6 +1,7 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod builder; mod claims; mod error; /// Additional metadata defined by the SD-JWT VC specification @@ -9,10 +10,11 @@ pub mod metadata; mod presentation; mod resolver; mod status; +#[cfg(test)] +pub(crate) mod tests; mod token; -mod validation; -mod builder; +pub use builder::*; pub use claims::*; pub use error::Error; pub use error::Result; @@ -20,5 +22,3 @@ pub use presentation::*; pub use resolver::Resolver; pub use status::*; pub use token::*; -pub use validation::*; -pub use builder::*; diff --git a/identity_credential/src/sd_jwt_vc/tests/mod.rs b/identity_credential/src/sd_jwt_vc/tests/mod.rs new file mode 100644 index 0000000000..e2453eb145 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/tests/mod.rs @@ -0,0 +1,110 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use identity_core::convert::Base; +use identity_core::convert::BaseEncoding; +use identity_verification::jwk::Jwk; +use identity_verification::jwk::JwkParamsOct; +use identity_verification::jws::JwsVerifier; +use josekit::jws::JwsHeader; +use josekit::jws::HS256; +use josekit::jwt::JwtPayload; +use josekit::jwt::{self}; +use sd_jwt_payload_rework::JsonObject; +use sd_jwt_payload_rework::JwsSigner; +use serde::Serialize; +use serde_json::Value; + +use super::resolver; +use super::Resolver; + +mod validation; + +pub(crate) const ISSUER_SECRET: &[u8] = b"0123456789ABCDEF0123456789ABCDEF"; + +/// A JWS signer that uses HS256 with a static secret string. +pub(crate) struct TestSigner; + +pub(crate) fn signer_secret_jwk() -> Jwk { + let mut params = JwkParamsOct::new(); + params.k = BaseEncoding::encode(ISSUER_SECRET, Base::Base64Url); + let mut jwk = Jwk::from_params(params); + jwk.set_kid("key1"); + + jwk +} + +#[async_trait] +impl JwsSigner for TestSigner { + type Error = josekit::JoseError; + async fn sign(&self, header: &JsonObject, payload: &JsonObject) -> std::result::Result, Self::Error> { + let signer = HS256.signer_from_bytes(ISSUER_SECRET)?; + let header = JwsHeader::from_map(header.clone())?; + let payload = JwtPayload::from_map(payload.clone())?; + let jws = jwt::encode_with_signer(&payload, &header, &signer)?; + + Ok(jws.into_bytes()) + } +} + +#[derive(Default, Debug, Clone)] +pub(crate) struct TestResolver(HashMap>); + +impl TestResolver { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn insert_resource(&mut self, id: K, value: V) + where + K: ToString, + V: Serialize, + { + let value = serde_json::to_vec(&value).unwrap(); + self.0.insert(id.to_string(), value); + } +} + +#[async_trait] +impl Resolver> for TestResolver +where + I: ToString + Sync, +{ + async fn resolve(&self, id: &I) -> Result, resolver::Error> { + let id = id.to_string(); + self.0.get(&id).cloned().ok_or_else(|| resolver::Error::NotFound(id)) + } +} + +#[async_trait] +impl Resolver for TestResolver +where + I: ToString + Sync, +{ + async fn resolve(&self, id: &I) -> Result { + let id = id.to_string(); + self + .0 + .get(&id) + .ok_or_else(|| resolver::Error::NotFound(id)) + .and_then(|bytes| serde_json::from_slice(bytes).map_err(|e| resolver::Error::ParsingFailure(e.into()))) + } +} + +pub(crate) struct TestJwsVerifier; + +impl JwsVerifier for TestJwsVerifier { + fn verify( + &self, + input: identity_verification::jws::VerificationInput, + public_key: &Jwk, + ) -> Result<(), identity_verification::jws::SignatureVerificationError> { + let key = serde_json::to_value(public_key.clone()) + .and_then(serde_json::from_value) + .unwrap(); + let verifier = HS256.verifier_from_jwk(&key).unwrap(); + verifier.verify(&input.signing_input, &input.decoded_signature).unwrap(); + + Ok(()) + } +} diff --git a/identity_credential/src/sd_jwt_vc/tests/validation.rs b/identity_credential/src/sd_jwt_vc/tests/validation.rs new file mode 100644 index 0000000000..9c6263dac6 --- /dev/null +++ b/identity_credential/src/sd_jwt_vc/tests/validation.rs @@ -0,0 +1,139 @@ +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_verification::jwk::JwkSet; +use sd_jwt_payload_rework::Sha256Hasher; +use serde_json::json; + +use crate::sd_jwt_vc::metadata::IssuerMetadata; +use crate::sd_jwt_vc::metadata::Jwks; +use crate::sd_jwt_vc::metadata::TypeMetadata; +use crate::sd_jwt_vc::tests::TestJwsVerifier; +use crate::sd_jwt_vc::SdJwtVcBuilder; + +use super::TestResolver; +use super::TestSigner; + +fn issuer_metadata() -> IssuerMetadata { + let mut jwk_set = JwkSet::new(); + jwk_set.add(super::signer_secret_jwk()); + + IssuerMetadata { + issuer: "https://example.com".parse().unwrap(), + jwks: Jwks::Object(jwk_set), + } +} + +fn test_resolver() -> TestResolver { + let mut test_resolver = TestResolver::new(); + test_resolver.insert_resource("https://example.com/.well-known/jwt-vc-issuer/", issuer_metadata()); + test_resolver.insert_resource( + "https://example.com/.well-known/vct/education_credential", + vc_metadata(), + ); + + test_resolver +} + +#[tokio::test] +async fn validation_works() -> anyhow::Result<()> { + let sd_jwt_credential = SdJwtVcBuilder::new(json!({ + "name": "John Doe", + "address": { + "street_address": "A random street", + "number": "3a" + }, + "degree": [] + }))? + .header(std::iter::once(("kid".to_string(), serde_json::Value::String("key1".to_string()))).collect()) + .vct("https://example.com/education_credential".parse::()?) + .iat(Timestamp::now_utc()) + .iss("https://example.com".parse()?) + .make_concealable("/address/street_address")? + .make_concealable("/address")? + .finish(&TestSigner, "HS256") + .await?; + + let resolver = test_resolver(); + sd_jwt_credential + .validate(&resolver, &TestJwsVerifier, &Sha256Hasher::new()) + .await?; + Ok(()) +} + +fn vc_metadata() -> TypeMetadata { + serde_json::from_str( + r#"{ + "vct": "https://example.com/education_credential", + "name": "Betelgeuse Education Credential - Preliminary Version", + "description": "This is our development version of the education credential. Don't panic.", + "claims": [ + { + "path": ["name"], + "display": [ + { + "lang": "de-DE", + "label": "Vor- und Nachname", + "description": "Der Name des Studenten" + }, + { + "lang": "en-US", + "label": "Name", + "description": "The name of the student" + } + ], + "sd": "allowed" + }, + { + "path": ["address"], + "display": [ + { + "lang": "de-DE", + "label": "Adresse", + "description": "Adresse zum Zeitpunkt des Abschlusses" + }, + { + "lang": "en-US", + "label": "Address", + "description": "Address at the time of graduation" + } + ], + "sd": "always" + }, + { + "path": ["address", "street_address"], + "display": [ + { + "lang": "de-DE", + "label": "Straße" + }, + { + "lang": "en-US", + "label": "Street Address" + } + ], + "sd": "always", + "svg_id": "address_street_address" + }, + { + "path": ["degrees", null], + "display": [ + { + "lang": "de-DE", + "label": "Abschluss", + "description": "Der Abschluss des Studenten" + }, + { + "lang": "en-US", + "label": "Degree", + "description": "Degree earned by the student" + } + ], + "sd": "allowed" + } + ], + "schema_url": "https://example.com/credential-schema", + "schema_url#integrity": "sha256-o984vn819a48ui1llkwPmKjZ5t0WRL5ca_xGgX3c1VLmXfh" +}"#, + ) + .unwrap() +} diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index 27448ad718..2c9ec5fc3e 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -213,11 +213,11 @@ impl SdJwtVc { /// ## Notes /// This check should be performed by the token's holder in order to assert the issuer's compliance with /// the credential's type. - pub fn validate_claims_disclosability(&self, hasher: &dyn Hasher, claims_metadata: &[ClaimMetadata]) -> Result<()> { - let disclosed_object = Value::Object(self.clone().into_disclosed_object(hasher)?); + pub fn validate_claims_disclosability(&self, claims_metadata: &[ClaimMetadata]) -> Result<()> { + let claims = Value::Object(self.parsed_claims.sd_jwt_claims.deref().clone()); claims_metadata .iter() - .try_fold((), |_, meta| meta.check_value_disclosability(&disclosed_object)) + .try_fold((), |_, meta| meta.check_value_disclosability(&claims)) } /// Check whether this [`SdJwtVc`] is valid. @@ -248,7 +248,7 @@ impl SdJwtVc { .await?; // Claims' disclosability. - self.validate_claims_disclosability(hasher, type_metadata.claim_metadata())?; + self.validate_claims_disclosability(type_metadata.claim_metadata())?; Ok(()) } @@ -263,7 +263,7 @@ pub fn vct_to_url(resource: &Url) -> Option { } else { let origin = resource.origin().ascii_serialization(); let path = resource.path(); - Some(format!("{origin}{WELL_KNOWN_VC_ISSUER}{path}").parse().unwrap()) + Some(format!("{origin}{WELL_KNOWN_VCT}{path}").parse().unwrap()) } } diff --git a/identity_credential/src/sd_jwt_vc/validation.rs b/identity_credential/src/sd_jwt_vc/validation.rs deleted file mode 100644 index adce6ed4c3..0000000000 --- a/identity_credential/src/sd_jwt_vc/validation.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::validator::JwtCredentialValidationOptions; - -/// Options to decide which operations should be performed during SD-JWT VC validation. -#[derive(Debug, Clone)] -pub struct ValidationOptions { - /// Credential validation options. - pub credential_validation_options: JwtCredentialValidationOptions, - /// The credential will be checked using the credential type - /// specified through the `vct` claim. - pub vct: bool, -} From a510185d80edfef91a72d675962e23eebb068ab0 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 2 Oct 2024 16:07:25 +0200 Subject: [PATCH 21/29] KB-JWT validation --- identity_credential/src/sd_jwt_vc/builder.rs | 6 +- .../src/sd_jwt_vc/tests/validation.rs | 32 ++++- identity_credential/src/sd_jwt_vc/token.rs | 123 ++++++++++++++++++ 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index 4b31b876b9..d0a83887b6 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -1,4 +1,5 @@ -use std::cell::LazyCell; +#![allow(clippy::vec_init_then_push)] +use std::sync::LazyLock; use identity_core::common::StringOrUrl; use identity_core::common::Timestamp; @@ -18,7 +19,7 @@ use super::SdJwtVc; use super::Status; use super::SD_JWT_VC_TYP; -const DEFAULT_HEADER: LazyCell = LazyCell::new(|| { +static DEFAULT_HEADER: LazyLock = LazyLock::new(|| { let mut object = JsonObject::default(); object.insert("typ".to_string(), SD_JWT_VC_TYP.into()); object @@ -187,6 +188,7 @@ impl SdJwtVcBuilder { } /// Inserts a `sub` claim. See [`super::SdJwtVcClaims::sub`]. + #[allow(clippy::should_implement_trait)] pub fn sub(mut self, sub: impl Into) -> Self { self.sub = Some(sub.into()); self diff --git a/identity_credential/src/sd_jwt_vc/tests/validation.rs b/identity_credential/src/sd_jwt_vc/tests/validation.rs index 9c6263dac6..2e165ac186 100644 --- a/identity_credential/src/sd_jwt_vc/tests/validation.rs +++ b/identity_credential/src/sd_jwt_vc/tests/validation.rs @@ -8,6 +8,7 @@ use crate::sd_jwt_vc::metadata::IssuerMetadata; use crate::sd_jwt_vc::metadata::Jwks; use crate::sd_jwt_vc::metadata::TypeMetadata; use crate::sd_jwt_vc::tests::TestJwsVerifier; +use crate::sd_jwt_vc::Error; use crate::sd_jwt_vc::SdJwtVcBuilder; use super::TestResolver; @@ -35,7 +36,7 @@ fn test_resolver() -> TestResolver { } #[tokio::test] -async fn validation_works() -> anyhow::Result<()> { +async fn validation_of_valid_token_works() -> anyhow::Result<()> { let sd_jwt_credential = SdJwtVcBuilder::new(json!({ "name": "John Doe", "address": { @@ -60,6 +61,35 @@ async fn validation_works() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn validation_of_invalid_token_fails() -> anyhow::Result<()> { + let sd_jwt_credential = SdJwtVcBuilder::new(json!({ + "name": "John Doe", + "address": { + "street_address": "A random street", + "number": "3a" + }, + "degree": [] + }))? + .header(std::iter::once(("kid".to_string(), serde_json::Value::String("invalid_key".to_string()))).collect()) + .vct("https://example.com/education_credential".parse::()?) + .iat(Timestamp::now_utc()) + .iss("https://example.com".parse()?) + .make_concealable("/address/street_address")? + .make_concealable("/address")? + .finish(&TestSigner, "HS256") + .await?; + + let resolver = test_resolver(); + let error = sd_jwt_credential + .validate(&resolver, &TestJwsVerifier, &Sha256Hasher::new()) + .await + .unwrap_err(); + assert!(matches!(error, Error::Verification(_))); + + Ok(()) +} + fn vc_metadata() -> TypeMetadata { serde_json::from_str( r#"{ diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index 2c9ec5fc3e..a76ecebb36 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -19,15 +19,21 @@ use super::Resolver; use super::Result; use super::SdJwtVcPresentationBuilder; use crate::validator::JwtCredentialValidator as JwsUtils; +use crate::validator::KeyBindingJWTValidationOptions; use anyhow::anyhow; use identity_core::common::StringOrUrl; +use identity_core::common::Timestamp; use identity_core::common::Url; +use identity_core::convert::ToJson as _; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkSet; use identity_verification::jws::JwsVerifier; +use itertools::Itertools; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; +use sd_jwt_payload_rework::RequiredKeyBinding; use sd_jwt_payload_rework::SdJwt; +use sd_jwt_payload_rework::SHA_ALG_NAME; use serde_json::Value; /// SD-JWT VC's JOSE header `typ`'s value. @@ -250,6 +256,123 @@ impl SdJwtVc { // Claims' disclosability. self.validate_claims_disclosability(type_metadata.claim_metadata())?; + // + + Ok(()) + } + + /// Verify the signature of this [`SdJwtVc`]'s [sd_jwt_payload_rework::KeyBindingJwt]. + pub fn verify_key_binding(&self, jws_verifier: &V, jwk: &Jwk) -> Result<()> { + let Some(kb_jwt) = self.key_binding_jwt() else { + return Ok(()); + }; + let kb_jwt_str = kb_jwt.to_string(); + let jws_input = JwsUtils::::decode(&kb_jwt_str).map_err(|e| Error::Verification(e.into()))?; + + JwsUtils::::verify_signature_raw(jws_input, jwk, jws_verifier) + .map_err(|e| Error::Verification(e.into())) + .and(Ok(())) + } + + /// Check the validity of this [`SdJwtVc`]'s [sd_jwt_payload_rework::KeyBindingJwt]. + /// # Notes + /// Validation of the required key binding (specified through the `cnf` JWT's claim) + /// is only partially validated - custom and "jwe" requirement are not checked. + pub fn validate_key_binding( + &self, + jws_verifier: &V, + jwk: &Jwk, + hasher: &dyn Hasher, + options: &KeyBindingJWTValidationOptions, + ) -> Result<()> { + self.verify_key_binding(jws_verifier, jwk)?; + + if let Some(requirement) = self.required_key_bind() { + if self.key_binding_jwt().is_none() { + return Err(Error::Validation(anyhow!( + "a key binding was required but none was provided" + ))); + } + match requirement { + RequiredKeyBinding::Jwk(json_jwk) => { + if jwk.to_json_value().unwrap().as_object().unwrap() != json_jwk { + return Err(Error::Validation(anyhow!( + "key used for signing KB-JWT does not match the key required in this SD-JWT" + ))); + } + } + RequiredKeyBinding::Kid(kid) | RequiredKeyBinding::Jwu { kid, .. } => jwk + .kid() + .filter(|id| id == kid) + .ok_or_else(|| { + Error::Validation(anyhow::anyhow!( + "the provided JWK doesn't have required `kid` \"{kid}\"" + )) + }) + .map(|_| ())?, + _ => (), + } + } + + let Some(kb_jwt) = self.key_binding_jwt() else { + return Ok(()); + }; + let KeyBindingJWTValidationOptions { + nonce, + aud, + earliest_issuance_date, + latest_issuance_date, + .. + } = options; + + let issuance_date = + Timestamp::from_unix(kb_jwt.claims().iat).map_err(|_| Error::Validation(anyhow!("invalid `iat` value")))?; + + if let Some(earliest_issuance_date) = earliest_issuance_date { + if issuance_date < *earliest_issuance_date { + return Err(Error::Validation(anyhow!( + "this KB-JWT has been created earlier than `earliest_issuance_date`" + ))); + } + } + + if let Some(latest_issuance_date) = latest_issuance_date { + if issuance_date > *latest_issuance_date { + return Err(Error::Validation(anyhow!( + "this KB-JWT has been created later than `latest_issuance_date`" + ))); + } + } else if issuance_date > Timestamp::now_utc() { + return Err(Error::Validation(anyhow!("this KB-JWT has been created in the future"))); + } + + if let Some(nonce) = nonce { + if nonce != &kb_jwt.claims().nonce { + return Err(Error::Validation(anyhow!("invalid KB-JWT's nonce: expected {nonce}"))); + } + } + + if let Some(aud) = aud { + if aud != &kb_jwt.claims().aud { + return Err(Error::Validation(anyhow!("invalid KB-JWT's `aud`: expected \"{aud}\""))); + } + } + + // Validate SD-JWT digest. + if self.claims()._sd_alg.as_deref().unwrap_or(SHA_ALG_NAME) != hasher.alg_name() { + return Err(Error::Validation(anyhow!("invalid hasher"))); + } + let encoded_sd_jwt = self.to_string(); + let digest = { + let last_tilde_idx = encoded_sd_jwt.chars().rev().find_position(|ch| *ch == '~').unwrap().0; + let sd_jwt_no_kb = &encoded_sd_jwt[..=last_tilde_idx]; + + hasher.encoded_digest(sd_jwt_no_kb) + }; + if kb_jwt.claims().sd_hash != digest { + return Err(Error::Validation(anyhow!("invalid KB-JWT's `sd_hash`"))); + } + Ok(()) } } From 654b188efaa53d990cc84faacea7e9b0b152792d Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Fri, 11 Oct 2024 15:24:38 +0200 Subject: [PATCH 22/29] review comment --- identity_credential/Cargo.toml | 1 + identity_credential/src/sd_jwt_vc/token.rs | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 7ae55656aa..f7943ce8c7 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -78,6 +78,7 @@ jpt-bbs-plus = [ "dep:zkryptium", "dep:bls12_381_plus", "dep:json-proof-token", + "dep:futures", ] [lints] diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index a76ecebb36..e898fd6ac9 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -28,7 +28,6 @@ use identity_core::convert::ToJson as _; use identity_verification::jwk::Jwk; use identity_verification::jwk::JwkSet; use identity_verification::jws::JwsVerifier; -use itertools::Itertools; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::RequiredKeyBinding; @@ -364,7 +363,7 @@ impl SdJwtVc { } let encoded_sd_jwt = self.to_string(); let digest = { - let last_tilde_idx = encoded_sd_jwt.chars().rev().find_position(|ch| *ch == '~').unwrap().0; + let last_tilde_idx = encoded_sd_jwt.rfind('~').expect("SD-JWT has a '~'"); let sd_jwt_no_kb = &encoded_sd_jwt[..=last_tilde_idx]; hasher.encoded_digest(sd_jwt_no_kb) From 72333bb019a198db3ae7d7b8e9e6852205d6b07d Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 11 Dec 2024 14:19:56 +0100 Subject: [PATCH 23/29] undo resolver-v2 --- examples/0_basic/2_resolve_did.rs | 14 + examples/0_basic/6_create_vp.rs | 11 +- examples/0_basic/7_revoke_vc.rs | 4 +- examples/0_basic/8_stronghold.rs | 4 +- examples/1_advanced/10_zkp_revocation.rs | 5 +- .../11_linked_verifiable_presentation.rs | 8 +- examples/1_advanced/5_custom_resolution.rs | 82 +++-- examples/1_advanced/6_domain_linkage.rs | 8 +- examples/1_advanced/9_zkp.rs | 7 +- examples/Cargo.toml | 2 +- identity_credential/Cargo.toml | 2 +- identity_credential/src/sd_jwt_vc/builder.rs | 4 +- identity_iota/Cargo.toml | 4 - identity_iota/src/lib.rs | 3 - identity_resolver/Cargo.toml | 14 +- identity_resolver/README.md | 4 + identity_resolver/src/error.rs | 85 +++++- identity_resolver/src/iota.rs | 64 ---- identity_resolver/src/legacy/error.rs | 74 ----- identity_resolver/src/lib.rs | 39 ++- .../src/{legacy => resolution}/commands.rs | 4 +- .../src/{legacy => resolution}/mod.rs | 5 +- .../src/{legacy => resolution}/resolver.rs | 11 +- identity_resolver/src/resolution/tests/mod.rs | 6 + .../src/resolution/tests/resolution.rs | 281 ++++++++++++++++++ .../src/resolution/tests/send_sync.rs | 26 ++ identity_resolver/src/resolver.rs | 17 -- 27 files changed, 535 insertions(+), 253 deletions(-) create mode 100644 identity_resolver/README.md delete mode 100644 identity_resolver/src/iota.rs delete mode 100644 identity_resolver/src/legacy/error.rs rename identity_resolver/src/{legacy => resolution}/commands.rs (98%) rename identity_resolver/src/{legacy => resolution}/mod.rs (87%) rename identity_resolver/src/{legacy => resolution}/resolver.rs (98%) create mode 100644 identity_resolver/src/resolution/tests/mod.rs create mode 100644 identity_resolver/src/resolution/tests/resolution.rs create mode 100644 identity_resolver/src/resolution/tests/send_sync.rs delete mode 100644 identity_resolver/src/resolver.rs diff --git a/examples/0_basic/2_resolve_did.rs b/examples/0_basic/2_resolve_did.rs index e56167d54c..4e648f8370 100644 --- a/examples/0_basic/2_resolve_did.rs +++ b/examples/0_basic/2_resolve_did.rs @@ -9,6 +9,7 @@ use identity_iota::iota::block::address::Address; use identity_iota::iota::IotaDocument; use identity_iota::iota::IotaIdentityClientExt; +use identity_iota::prelude::Resolver; use identity_iota::storage::JwkMemStore; use identity_iota::storage::KeyIdMemstore; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; @@ -44,6 +45,19 @@ async fn main() -> anyhow::Result<()> { let client_document: IotaDocument = client.resolve_did(&did).await?; println!("Client resolved DID Document: {client_document:#}"); + // We can also create a `Resolver` that has additional convenience methods, + // for example to resolve presentation issuers or to verify presentations. + let mut resolver = Resolver::::new(); + + // We need to register a handler that can resolve IOTA DIDs. + // This convenience method only requires us to provide a client. + resolver.attach_iota_handler(client.clone()); + + let resolver_document: IotaDocument = resolver.resolve(&did).await.unwrap(); + + // Client and Resolver resolve to the same document in this case. + assert_eq!(client_document, resolver_document); + // We can also resolve the Alias Output directly. let alias_output: AliasOutput = client.resolve_did_output(&did).await?; diff --git a/examples/0_basic/6_create_vp.rs b/examples/0_basic/6_create_vp.rs index 138c763c05..8c157295ef 100644 --- a/examples/0_basic/6_create_vp.rs +++ b/examples/0_basic/6_create_vp.rs @@ -7,6 +7,8 @@ //! //! cargo run --release --example 6_create_vp +use std::collections::HashMap; + use examples::create_did; use examples::MemStorage; use identity_eddsa_verifier::EdDSAJwsVerifier; @@ -188,9 +190,12 @@ async fn main() -> anyhow::Result<()> { let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default().nonce(challenge.to_owned()); + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client); + // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; - let holder: IotaDocument = client.resolve(&holder_did).await?; + let holder: IotaDocument = resolver.resolve(&holder_did).await?; // Validate presentation. Note that this doesn't validate the included credentials. let presentation_validation_options = @@ -206,7 +211,7 @@ async fn main() -> anyhow::Result<()> { .iter() .map(JwtCredentialValidatorUtils::extract_issuer_from_jwt) .collect::, _>>()?; - let issuers_documents: Vec = client.resolve_multiple(&issuers).await?; + let issuers_documents: HashMap = resolver.resolve_multiple(&issuers).await?; // Validate the credentials in the presentation. let credential_validator: JwtCredentialValidator = @@ -216,7 +221,7 @@ async fn main() -> anyhow::Result<()> { for (index, jwt_vc) in jwt_credentials.iter().enumerate() { // SAFETY: Indexing should be fine since we extracted the DID from each credential and resolved it. - let issuer_document: &IotaDocument = &issuers_documents[index]; + let issuer_document: &IotaDocument = &issuers_documents[&issuers[index]]; let _decoded_credential: DecodedJwtCredential = credential_validator .validate::<_, Object>(jwt_vc, issuer_document, &validation_options, FailFast::FirstError) diff --git a/examples/0_basic/7_revoke_vc.rs b/examples/0_basic/7_revoke_vc.rs index b0512bb913..864041f3e3 100644 --- a/examples/0_basic/7_revoke_vc.rs +++ b/examples/0_basic/7_revoke_vc.rs @@ -211,8 +211,10 @@ async fn main() -> anyhow::Result<()> { client.publish_did_output(&secret_manager_issuer, alias_output).await?; // We expect the verifiable credential to be revoked. + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client); let resolved_issuer_did: IotaDID = JwtCredentialValidatorUtils::extract_issuer_from_jwt(&credential_jwt)?; - let resolved_issuer_doc: IotaDocument = client.resolve(&resolved_issuer_did).await?; + let resolved_issuer_doc: IotaDocument = resolver.resolve(&resolved_issuer_did).await?; let validation_result = validator.validate::<_, Object>( &credential_jwt, diff --git a/examples/0_basic/8_stronghold.rs b/examples/0_basic/8_stronghold.rs index 4aa223a027..0681e5b612 100644 --- a/examples/0_basic/8_stronghold.rs +++ b/examples/0_basic/8_stronghold.rs @@ -89,7 +89,9 @@ async fn main() -> anyhow::Result<()> { .await?; // Resolve the published DID Document. - let resolved_document: IotaDocument = client.resolve(document.id()).await.unwrap(); + let mut resolver = Resolver::::new(); + resolver.attach_iota_handler(client.clone()); + let resolved_document: IotaDocument = resolver.resolve(document.id()).await.unwrap(); drop(stronghold_storage); diff --git a/examples/1_advanced/10_zkp_revocation.rs b/examples/1_advanced/10_zkp_revocation.rs index 55c57f1467..a78dea0e76 100644 --- a/examples/1_advanced/10_zkp_revocation.rs +++ b/examples/1_advanced/10_zkp_revocation.rs @@ -413,9 +413,12 @@ async fn main() -> anyhow::Result<()> { let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default().nonce(challenge.to_owned()); + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; - let holder: IotaDocument = client.resolve(&holder_did).await?; + let holder: IotaDocument = resolver.resolve(&holder_did).await?; // Validate presentation. Note that this doesn't validate the included credentials. let presentation_validation_options = diff --git a/examples/1_advanced/11_linked_verifiable_presentation.rs b/examples/1_advanced/11_linked_verifiable_presentation.rs index 066f7b9123..550bad3d41 100644 --- a/examples/1_advanced/11_linked_verifiable_presentation.rs +++ b/examples/1_advanced/11_linked_verifiable_presentation.rs @@ -93,8 +93,12 @@ async fn main() -> anyhow::Result<()> { // 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 = client.resolve(&did).await?; + 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 @@ -117,7 +121,7 @@ async fn main() -> anyhow::Result<()> { // Resolve the holder's document. let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; - let holder: IotaDocument = client.resolve(&holder_did).await?; + 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(); diff --git a/examples/1_advanced/5_custom_resolution.rs b/examples/1_advanced/5_custom_resolution.rs index 4492d3ea6d..b0675c8dd5 100644 --- a/examples/1_advanced/5_custom_resolution.rs +++ b/examples/1_advanced/5_custom_resolution.rs @@ -12,8 +12,6 @@ use identity_iota::did::DID; use identity_iota::document::CoreDocument; use identity_iota::iota::IotaDID; use identity_iota::iota::IotaDocument; -use identity_iota::resolver::CompoundResolver; -use identity_iota::resolver::Error as ResolverError; use identity_iota::resolver::Resolver; use identity_iota::storage::JwkMemStore; use identity_iota::storage::KeyIdMemstore; @@ -27,38 +25,21 @@ use iota_sdk::types::block::address::Address; /// /// NOTE: Since both `IotaDocument` and `FooDocument` implement `Into` we could have used /// Resolver in this example and just worked with `CoreDocument` representations throughout. - -// Create a resolver capable of resolving FooDocument. -struct FooResolver; -impl Resolver for FooResolver { - type Target = FooDocument; - async fn resolve(&self, input: &CoreDID) -> Result { - Ok(resolve_did_foo(input.clone()).await?) - } -} - -// Combine it with a resolver of IotaDocuments, creating a new resolver capable of resolving both. -#[derive(CompoundResolver)] -struct FooAndIotaResolver { - #[resolver(CoreDID -> FooDocument)] - foo: FooResolver, - #[resolver(IotaDID -> IotaDocument)] - iota: Client, -} - #[tokio::main] async fn main() -> anyhow::Result<()> { + // Create a resolver returning an enum of the documents we are interested in and attach handlers for the "foo" and + // "iota" methods. + let mut resolver: Resolver = Resolver::new(); + // Create a new client to interact with the IOTA ledger. let client: Client = Client::builder() .with_primary_node(API_ENDPOINT, None)? .finish() .await?; - // Create a resolver capable of resolving both IotaDocument and FooDocument. - let resolver = FooAndIotaResolver { - iota: client.clone(), - foo: FooResolver {}, - }; + // This is a convenience method for attaching a handler for the "iota" method by providing just a client. + resolver.attach_iota_handler(client.clone()); + resolver.attach_handler("foo".to_owned(), resolve_did_foo); // A fake did:foo DID for demonstration purposes. let did_foo: CoreDID = "did:foo:0e9c8294eeafee326a4e96d65dbeaca0".parse()?; @@ -77,14 +58,28 @@ async fn main() -> anyhow::Result<()> { let iota_did: IotaDID = iota_document.id().clone(); // Resolve did_foo to get an abstract document. - let did_foo_doc: FooDocument = resolver.resolve(&did_foo).await?; + let did_foo_doc: Document = resolver.resolve(&did_foo).await?; // Resolve iota_did to get an abstract document. - let iota_doc: IotaDocument = resolver.resolve(&iota_did).await?; + let iota_doc: Document = resolver.resolve(&iota_did).await?; + + // The Resolver is mainly meant for validating presentations, but here we will just + // check that the resolved documents match our expectations. + + let Document::Foo(did_foo_document) = did_foo_doc else { + anyhow::bail!("expected a foo DID document when resolving a foo DID"); + }; - println!("Resolved DID foo document: {}", did_foo_doc.as_ref().to_json_pretty()?); + println!( + "Resolved DID foo document: {}", + did_foo_document.as_ref().to_json_pretty()? + ); + + let Document::Iota(iota_document) = iota_doc else { + anyhow::bail!("expected an IOTA DID document when resolving an IOTA DID") + }; - println!("Resolved IOTA DID document: {}", iota_doc.to_json_pretty()?); + println!("Resolved IOTA DID document: {}", iota_document.to_json_pretty()?); Ok(()) } @@ -113,6 +108,33 @@ impl From for CoreDocument { } } +// Enum of the document types we want to handle. +enum Document { + Foo(FooDocument), + Iota(IotaDocument), +} + +impl From for Document { + fn from(value: FooDocument) -> Self { + Self::Foo(value) + } +} + +impl From for Document { + fn from(value: IotaDocument) -> Self { + Self::Iota(value) + } +} + +impl AsRef for Document { + fn as_ref(&self) -> &CoreDocument { + match self { + Self::Foo(doc) => doc.as_ref(), + Self::Iota(doc) => doc.as_ref(), + } + } +} + /// Resolve a did to a DID document if the did method is "foo". async fn resolve_did_foo(did: CoreDID) -> anyhow::Result { let doc = CoreDocument::from_json(&format!( diff --git a/examples/1_advanced/6_domain_linkage.rs b/examples/1_advanced/6_domain_linkage.rs index 4c13464c5f..03d0472a37 100644 --- a/examples/1_advanced/6_domain_linkage.rs +++ b/examples/1_advanced/6_domain_linkage.rs @@ -136,6 +136,10 @@ async fn main() -> anyhow::Result<()> { // while the second answers "What domain is this DID linked to?". // ===================================================== + // Init a resolver for resolving DID Documents. + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client.clone()); + // ===================================================== // → Case 1: starting from domain // ===================================================== @@ -150,7 +154,7 @@ async fn main() -> anyhow::Result<()> { assert_eq!(linked_dids.len(), 1); // Resolve the DID Document of the DID that issued the credential. - let issuer_did_document: IotaDocument = client.resolve(&did).await?; + let issuer_did_document: IotaDocument = resolver.resolve(&did).await?; // Validate the linkage between the Domain Linkage Credential in the configuration and the provided issuer DID. let validation_result: Result<(), DomainLinkageValidationError> = @@ -165,7 +169,7 @@ async fn main() -> anyhow::Result<()> { // ===================================================== // → Case 2: starting from a DID // ===================================================== - let did_document: IotaDocument = client.resolve(&did).await?; + let did_document: IotaDocument = resolver.resolve(&did).await?; // Get the Linked Domain Services from the DID Document. let linked_domain_services: Vec = did_document diff --git a/examples/1_advanced/9_zkp.rs b/examples/1_advanced/9_zkp.rs index 97a10b0864..eeb4246280 100644 --- a/examples/1_advanced/9_zkp.rs +++ b/examples/1_advanced/9_zkp.rs @@ -164,9 +164,12 @@ async fn main() -> anyhow::Result<()> { // Step 4: Holder resolves Issuer's DID, retrieve Issuer's document and validate the Credential // ============================================================================================ + let mut resolver: Resolver = Resolver::new(); + resolver.attach_iota_handler(client); + // Holder resolves issuer's DID let issuer: CoreDID = JptCredentialValidatorUtils::extract_issuer_from_issued_jpt(&credential_jpt).unwrap(); - let issuer_document: IotaDocument = client.resolve(&issuer).await?; + let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; // Holder validates the credential and retrieve the JwpIssued, needed to construct the JwpPresented let decoded_credential = JptCredentialValidator::validate::<_, Object>( @@ -234,7 +237,7 @@ async fn main() -> anyhow::Result<()> { // Verifier resolve Issuer DID let issuer: CoreDID = JptPresentationValidatorUtils::extract_issuer_from_presented_jpt(&presentation_jpt).unwrap(); - let issuer_document: IotaDocument = client.resolve(&issuer).await?; + let issuer_document: IotaDocument = resolver.resolve(&issuer).await?; let presentation_validation_options = JptPresentationValidationOptions::default().nonce(challenge); diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 2081164038..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", "resolver-v2"] } +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 diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index f7943ce8c7..ab77b10d36 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -71,7 +71,7 @@ validator = ["dep:itertools", "dep:serde_repr", "credential", "presentation"] domain-linkage = ["validator"] domain-linkage-fetch = ["domain-linkage", "dep:reqwest", "dep:futures"] sd-jwt = ["credential", "validator", "dep:sd-jwt-payload"] -sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework", "dep:jsonschema"] +sd-jwt-vc = ["sd-jwt", "dep:sd-jwt-payload-rework", "dep:jsonschema", "dep:futures"] jpt-bbs-plus = [ "credential", "validator", diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index d0a83887b6..77a2f0d98a 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -230,7 +230,9 @@ impl SdJwtVcBuilder { let builder = claim_to_key_value_pair![iss, nbf, exp, iat, vct, sub, status] .into_iter() .filter(|(_, value)| !value.is_null()) - .fold(builder, |builder, (key, value)| builder.insert_claim(key, value)); + .fold(builder, |builder, (key, value)| { + builder.insert_claim(key, value).expect("value is a JSON Value") + }); let sd_jwt = builder.finish(signer, alg).await?; SdJwtVc::try_from(sd_jwt) diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 12964a40e5..6ba8f5d7aa 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -12,7 +12,6 @@ rust-version.workspace = true description = "Framework for Self-Sovereign Identity with IOTA DID." [dependencies] -compound_resolver = { path = "../compound_resolver", optional = true } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } identity_credential = { version = "=1.3.1", path = "../identity_credential", features = ["validator"], default-features = false } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } @@ -49,9 +48,6 @@ status-list-2021 = ["revocation-bitmap", "identity_credential/status-list-2021"] # Enables support for the `Resolver`. resolver = [] -# Enable support for the new resolver interface. -resolver-v2 = ["dep:compound_resolver", "identity_resolver/v2", "resolver"] - # Enables `Send` + `Sync` bounds for the storage traits. send-sync-storage = ["identity_storage/send-sync-storage"] diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 5d1e9806aa..0a16a7e819 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -94,9 +94,6 @@ pub mod prelude { #[cfg_attr(docsrs, doc(cfg(feature = "resolver")))] pub mod resolver { //! DID resolution utilities - - #[cfg(feature = "resolver-v2")] - pub use compound_resolver::CompoundResolver; pub use identity_resolver::*; } diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 5bfdf7ccd1..a85969c286 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -4,22 +4,24 @@ version = "1.3.1" authors.workspace = true edition.workspace = true homepage.workspace = true +keywords = ["iota", "did", "identity", "resolver", "resolution"] license.workspace = true +readme = "./README.md" repository.workspace = true rust-version.workspace = true +description = "DID Resolution utilities for the identity.rs library." [dependencies] -anyhow = "1.0.86" # This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } identity_core = { version = "=1.3.1", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.3.1", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=1.3.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.3.1", path = "../identity_document", default-features = false } -iota-sdk = { version = "1.1.5" } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } strum.workspace = true -thiserror.workspace = true +thiserror = { version = "1.0", default-features = false } [dependencies.identity_iota_core] version = "=1.3.1" @@ -30,12 +32,12 @@ optional = true [dev-dependencies] identity_iota_core = { path = "../identity_iota_core", features = ["test"] } +iota-sdk = { version = "1.1.5" } tokio = { version = "1.29.0", default-features = false, features = ["rt-multi-thread", "macros"] } [features] -default = ["iota"] -# Enables the new Resolver interface. -v2 = [] +default = ["revocation-bitmap", "iota"] +revocation-bitmap = ["identity_credential/revocation-bitmap", "identity_iota_core?/revocation-bitmap"] # Enables the IOTA integration for the resolver. iota = ["dep:identity_iota_core"] diff --git a/identity_resolver/README.md b/identity_resolver/README.md new file mode 100644 index 0000000000..4ce84dce0e --- /dev/null +++ b/identity_resolver/README.md @@ -0,0 +1,4 @@ +IOTA Identity - Resolver +=== + +This crate provides a pluggable Resolver implementation that allows for abstracting over the resolution of different DID methods. diff --git a/identity_resolver/src/error.rs b/identity_resolver/src/error.rs index a7e1ddfecf..d72a78fd4a 100644 --- a/identity_resolver/src/error.rs +++ b/identity_resolver/src/error.rs @@ -1,13 +1,74 @@ -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Debug, Error)] -pub enum Error { - #[error("The requested item \"{0}\" was not found.")] - NotFound(String), - #[error("Failed to parse the provided input into a resolvable type: {0}")] - ParsingFailure(#[source] anyhow::Error), - #[error(transparent)] - Generic(#[from] anyhow::Error), +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +/// Alias for a `Result` with the error type [`Error`]. +pub type Result = core::result::Result; + +/// Error returned from the [Resolver's](crate::Resolver) methods. +/// +/// The [`Self::error_cause`](Self::error_cause()) method provides information about the cause of the error. +#[derive(Debug)] +pub struct Error { + error_cause: ErrorCause, +} + +impl Error { + pub(crate) fn new(cause: ErrorCause) -> Self { + Self { error_cause: cause } + } + + /// Returns the cause of the error. + pub fn error_cause(&self) -> &ErrorCause { + &self.error_cause + } + + /// Converts the error into [`ErrorCause`]. + pub fn into_error_cause(self) -> ErrorCause { + self.error_cause + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.error_cause) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.error_cause.source() + } +} + +/// Error failure modes associated with the methods on the [Resolver's](crate::Resolver). +/// +/// NOTE: This is a "read only error" in the sense that it can only be constructed by the methods in this crate. +#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] +#[non_exhaustive] +pub enum ErrorCause { + /// Caused by a failure to parse a DID string during DID resolution. + #[error("did resolution failed: could not parse the given did")] + #[non_exhaustive] + DIDParsingError { + /// The source of the parsing error. + source: Box, + }, + /// A handler attached to the [`Resolver`](crate::resolution::Resolver) attempted to resolve the DID, but the + /// resolution did not succeed. + #[error("did resolution failed: the attached handler failed")] + #[non_exhaustive] + HandlerError { + /// The source of the handler error. + source: Box, + }, + /// Caused by attempting to resolve a DID whose method does not have a corresponding handler attached to the + /// [`Resolver`](crate::resolution::Resolver). + #[error("did resolution failed: the DID method \"{method}\" is not supported by the resolver")] + UnsupportedMethodError { + /// The method that is unsupported. + method: String, + }, + /// No client attached to the specific network. + #[error("none of the attached clients support the network {0}")] + UnsupportedNetwork(String), } diff --git a/identity_resolver/src/iota.rs b/identity_resolver/src/iota.rs deleted file mode 100644 index 533a1f46b8..0000000000 --- a/identity_resolver/src/iota.rs +++ /dev/null @@ -1,64 +0,0 @@ -use super::Error; -use super::Resolver; -use super::Result; -use identity_did::CoreDID; -use identity_iota_core::Error as IdentityError; -use identity_iota_core::IotaDID; -use identity_iota_core::IotaDocument; -use identity_iota_core::IotaIdentityClientExt; -use iota_sdk::client::node_api::error::Error as IotaApiError; -use iota_sdk::client::Client; -use iota_sdk::client::Error as SdkError; - -impl Resolver for Client { - type Target = IotaDocument; - async fn resolve(&self, did: &IotaDID) -> Result { - self.resolve_did(did).await.map_err(|e| match e { - IdentityError::DIDResolutionError(SdkError::Node(IotaApiError::NotFound(_))) => Error::NotFound(did.to_string()), - e => Error::Generic(e.into()), - }) - } -} - -impl Resolver for Client { - type Target = IotaDocument; - async fn resolve(&self, did: &CoreDID) -> Result { - let iota_did = IotaDID::try_from(did.clone()).map_err(|e| Error::ParsingFailure(e.into()))?; - self.resolve(&iota_did).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use anyhow::Context; - - async fn get_iota_client() -> anyhow::Result { - const API_ENDPOINT: &str = "https://api.stardust-mainnet.iotaledger.net"; - Client::builder() - .with_primary_node(API_ENDPOINT, None)? - .finish() - .await - .context("Failed to create client to iota mainnet") - } - - #[tokio::test] - async fn resolution_of_existing_doc_works() -> anyhow::Result<()> { - let client = get_iota_client().await?; - let did = "did:iota:0xf4d6f08f5a1b80dd578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a".parse::()?; - - assert!(client.resolve(&did).await.is_ok()); - - Ok(()) - } - - #[tokio::test] - async fn resolution_of_non_existing_doc_fails_with_not_found() -> anyhow::Result<()> { - let client = get_iota_client().await?; - let did = "did:iota:0xf4d6f08f5a1b80ee578da7dc1b49c886d580acd4cf7d48119dfeb82b538ad88a".parse::()?; - - assert!(matches!(client.resolve(&did).await.unwrap_err(), Error::NotFound(_))); - - Ok(()) - } -} diff --git a/identity_resolver/src/legacy/error.rs b/identity_resolver/src/legacy/error.rs deleted file mode 100644 index d72a78fd4a..0000000000 --- a/identity_resolver/src/legacy/error.rs +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2020-2022 IOTA Stiftung -// SPDX-License-Identifier: Apache-2.0 - -/// Alias for a `Result` with the error type [`Error`]. -pub type Result = core::result::Result; - -/// Error returned from the [Resolver's](crate::Resolver) methods. -/// -/// The [`Self::error_cause`](Self::error_cause()) method provides information about the cause of the error. -#[derive(Debug)] -pub struct Error { - error_cause: ErrorCause, -} - -impl Error { - pub(crate) fn new(cause: ErrorCause) -> Self { - Self { error_cause: cause } - } - - /// Returns the cause of the error. - pub fn error_cause(&self) -> &ErrorCause { - &self.error_cause - } - - /// Converts the error into [`ErrorCause`]. - pub fn into_error_cause(self) -> ErrorCause { - self.error_cause - } -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.error_cause) - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.error_cause.source() - } -} - -/// Error failure modes associated with the methods on the [Resolver's](crate::Resolver). -/// -/// NOTE: This is a "read only error" in the sense that it can only be constructed by the methods in this crate. -#[derive(Debug, thiserror::Error, strum::IntoStaticStr)] -#[non_exhaustive] -pub enum ErrorCause { - /// Caused by a failure to parse a DID string during DID resolution. - #[error("did resolution failed: could not parse the given did")] - #[non_exhaustive] - DIDParsingError { - /// The source of the parsing error. - source: Box, - }, - /// A handler attached to the [`Resolver`](crate::resolution::Resolver) attempted to resolve the DID, but the - /// resolution did not succeed. - #[error("did resolution failed: the attached handler failed")] - #[non_exhaustive] - HandlerError { - /// The source of the handler error. - source: Box, - }, - /// Caused by attempting to resolve a DID whose method does not have a corresponding handler attached to the - /// [`Resolver`](crate::resolution::Resolver). - #[error("did resolution failed: the DID method \"{method}\" is not supported by the resolver")] - UnsupportedMethodError { - /// The method that is unsupported. - method: String, - }, - /// No client attached to the specific network. - #[error("none of the attached clients support the network {0}")] - UnsupportedNetwork(String), -} diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs index d68b59eb90..b773741b09 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -1,24 +1,23 @@ -// Copyright 2020-2024 IOTA Stiftung +// Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "v2")] -#[path = ""] -mod v2 { - mod error; - mod resolver; +#![forbid(unsafe_code)] +#![doc = include_str!("./../README.md")] +#![warn( + rust_2018_idioms, + unreachable_pub, + missing_docs, + rustdoc::missing_crate_level_docs, + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::private_doc_tests, + clippy::missing_safety_doc +)] - pub use error::Error; - pub use error::Result; - pub use resolver::Resolver; -} +mod error; +mod resolution; -#[cfg(all(feature = "iota", feature = "v2"))] -mod iota; - -#[cfg(not(feature = "v2"))] -mod legacy; -#[cfg(not(feature = "v2"))] -pub use legacy::*; - -#[cfg(feature = "v2")] -pub use v2::*; +pub use self::error::Error; +pub use self::error::ErrorCause; +pub use self::error::Result; +pub use resolution::*; diff --git a/identity_resolver/src/legacy/commands.rs b/identity_resolver/src/resolution/commands.rs similarity index 98% rename from identity_resolver/src/legacy/commands.rs rename to identity_resolver/src/resolution/commands.rs index 41129a6c7d..6dd186a841 100644 --- a/identity_resolver/src/legacy/commands.rs +++ b/identity_resolver/src/resolution/commands.rs @@ -4,7 +4,9 @@ use core::future::Future; use identity_did::DID; -use super::error::*; +use crate::Error; +use crate::ErrorCause; +use crate::Result; use std::pin::Pin; /// Internal trait used by the resolver to apply the command pattern. diff --git a/identity_resolver/src/legacy/mod.rs b/identity_resolver/src/resolution/mod.rs similarity index 87% rename from identity_resolver/src/legacy/mod.rs rename to identity_resolver/src/resolution/mod.rs index b62ee0f338..06a923d446 100644 --- a/identity_resolver/src/legacy/mod.rs +++ b/identity_resolver/src/resolution/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 mod commands; -mod error; mod resolver; +#[cfg(test)] +mod tests; use self::commands::SingleThreadedCommand; use identity_document::document::CoreDocument; @@ -11,5 +12,3 @@ use identity_document::document::CoreDocument; pub use resolver::Resolver; /// Alias for a [`Resolver`] that is not [`Send`] + [`Sync`]. pub type SingleThreadedResolver = Resolver>; -pub use error::Error; -pub use error::Result; diff --git a/identity_resolver/src/legacy/resolver.rs b/identity_resolver/src/resolution/resolver.rs similarity index 98% rename from identity_resolver/src/legacy/resolver.rs rename to identity_resolver/src/resolution/resolver.rs index 9a675a3b8e..228a65582b 100644 --- a/identity_resolver/src/legacy/resolver.rs +++ b/identity_resolver/src/resolution/resolver.rs @@ -12,7 +12,9 @@ use identity_document::document::CoreDocument; use std::collections::HashMap; use std::marker::PhantomData; -use super::error::*; +use crate::Error; +use crate::ErrorCause; +use crate::Result; use super::commands::Command; use super::commands::SendSyncCommand; @@ -264,7 +266,8 @@ impl + 'static> Resolver> { #[cfg(feature = "iota")] mod iota_handler { - use super::ErrorCause; + use crate::ErrorCause; + use super::Resolver; use identity_document::document::CoreDocument; use identity_iota_core::IotaDID; @@ -333,13 +336,13 @@ mod iota_handler { let client: &CLI = future_client .get(did_network) - .ok_or(super::Error::new(ErrorCause::UnsupportedNetwork( + .ok_or(crate::Error::new(ErrorCause::UnsupportedNetwork( did_network.to_string(), )))?; client .resolve_did(&did) .await - .map_err(|err| super::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) + .map_err(|err| crate::Error::new(ErrorCause::HandlerError { source: Box::new(err) })) } }; diff --git a/identity_resolver/src/resolution/tests/mod.rs b/identity_resolver/src/resolution/tests/mod.rs new file mode 100644 index 0000000000..082def4c42 --- /dev/null +++ b/identity_resolver/src/resolution/tests/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::resolver::*; +mod resolution; +mod send_sync; diff --git a/identity_resolver/src/resolution/tests/resolution.rs b/identity_resolver/src/resolution/tests/resolution.rs new file mode 100644 index 0000000000..ce68fd4b9b --- /dev/null +++ b/identity_resolver/src/resolution/tests/resolution.rs @@ -0,0 +1,281 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::HashMap; +use std::error::Error; +use std::fmt::Debug; +use std::str::FromStr; + +use identity_did::BaseDIDUrl; +use identity_did::CoreDID; +use identity_did::Error as DIDError; +use identity_did::DID; +use identity_document::document::CoreDocument; +use identity_document::document::DocumentBuilder; + +use crate::Error as ResolverError; +use crate::ErrorCause; +use crate::Resolver; + +/// A very simple handler +async fn mock_handler(did: CoreDID) -> std::result::Result { + Ok(core_document(did)) +} + +/// Create a [`CoreDocument`] +fn core_document(did: CoreDID) -> CoreDocument { + DocumentBuilder::default().id(did).build().unwrap() +} + +/// A custom document type +#[derive(Debug, Clone)] +struct FooDocument(CoreDocument); +impl AsRef for FooDocument { + fn as_ref(&self) -> &CoreDocument { + &self.0 + } +} +impl From for FooDocument { + fn from(value: CoreDocument) -> Self { + Self(value) + } +} + +// =========================================================================== +// Missing handler for DID method failure tests +// =========================================================================== +#[tokio::test] +async fn missing_handler_errors() { + let method_name: String = "foo".to_owned(); + let bad_did: CoreDID = CoreDID::parse(format!("did:{method_name}:1234")).unwrap(); + let other_method: String = "bar".to_owned(); + let good_did: CoreDID = CoreDID::parse(format!("did:{other_method}:1234")).unwrap(); + + // configure `resolver` to resolve the "bar" method + let mut resolver_foo: Resolver = Resolver::new(); + let mut resolver_core: Resolver = Resolver::new(); + resolver_foo.attach_handler(other_method.clone(), mock_handler); + resolver_core.attach_handler(other_method, mock_handler); + + let err: ResolverError = resolver_foo.resolve(&bad_did).await.unwrap_err(); + let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { + unreachable!() + }; + assert_eq!(method_name, method); + + let err: ResolverError = resolver_core.resolve(&bad_did).await.unwrap_err(); + let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { + unreachable!() + }; + assert_eq!(method_name, method); + + assert!(resolver_foo.resolve(&good_did).await.is_ok()); + assert!(resolver_core.resolve(&good_did).await.is_ok()); + + let both_dids = [good_did, bad_did]; + let err: ResolverError = resolver_foo.resolve_multiple(&both_dids).await.unwrap_err(); + let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { + unreachable!() + }; + assert_eq!(method_name, method); + + let err: ResolverError = resolver_core.resolve_multiple(&both_dids).await.unwrap_err(); + let ErrorCause::UnsupportedMethodError { method } = err.into_error_cause() else { + unreachable!() + }; + assert_eq!(method_name, method); +} + +// =========================================================================== +// DID Parsing failure tests +// =========================================================================== + +// Implement the DID trait for a new type +#[derive(Hash, Ord, PartialOrd, Eq, PartialEq, Clone)] +struct FooDID(CoreDID); + +impl FooDID { + const METHOD_ID_LENGTH: usize = 5; + + fn try_from_core(did: CoreDID) -> std::result::Result { + Some(did) + .filter(|did| did.method() == "foo" && did.method_id().len() == FooDID::METHOD_ID_LENGTH) + .map(Self) + .ok_or(DIDError::InvalidMethodName) + } +} + +impl AsRef for FooDID { + fn as_ref(&self) -> &CoreDID { + &self.0 + } +} + +impl From for String { + fn from(did: FooDID) -> Self { + String::from(did.0) + } +} +impl From for CoreDID { + fn from(value: FooDID) -> Self { + value.0 + } +} + +impl TryFrom for FooDID { + type Error = DIDError; + fn try_from(value: CoreDID) -> Result { + Self::try_from_core(value) + } +} + +impl FromStr for FooDID { + type Err = DIDError; + fn from_str(s: &str) -> Result { + CoreDID::from_str(s).and_then(Self::try_from_core) + } +} + +impl TryFrom for FooDID { + type Error = DIDError; + fn try_from(value: BaseDIDUrl) -> Result { + CoreDID::try_from(value).and_then(Self::try_from_core) + } +} + +impl<'a> TryFrom<&'a str> for FooDID { + type Error = DIDError; + fn try_from(value: &'a str) -> Result { + CoreDID::try_from(value).and_then(Self::try_from_core) + } +} + +#[tokio::test] +async fn resolve_unparsable() { + let mut resolver_foo: Resolver = Resolver::new(); + let mut resolver_core: Resolver = Resolver::new(); + + // register a handler that wants `did` to be of type `FooDID`. + async fn handler(did: FooDID) -> std::result::Result { + mock_handler(did.as_ref().clone()).await + } + + resolver_foo.attach_handler("foo".to_owned(), handler); + resolver_core.attach_handler("foo".to_owned(), handler); + + let bad_did: CoreDID = CoreDID::parse("did:foo:1234").unwrap(); + // ensure that the DID we created does not satisfy the requirements of the "foo" method + assert!(bad_did.method_id().len() < FooDID::METHOD_ID_LENGTH); + + let good_did: FooDID = FooDID::try_from("did:foo:12345").unwrap(); + + let error_matcher = |err: ErrorCause| { + assert!(matches!( + err + .source() + .unwrap() + .downcast_ref::() + .unwrap_or_else(|| panic!("{:?}", &err)), + &DIDError::InvalidMethodName + )); + + match err { + ErrorCause::DIDParsingError { .. } => {} + _ => unreachable!(), + } + }; + + let err_cause: ErrorCause = resolver_foo.resolve(&bad_did).await.unwrap_err().into_error_cause(); + error_matcher(err_cause); + + let err_cause: ErrorCause = resolver_core.resolve(&bad_did).await.unwrap_err().into_error_cause(); + error_matcher(err_cause); + + assert!(resolver_foo.resolve(&good_did).await.is_ok()); + assert!(resolver_core.resolve(&good_did).await.is_ok()); +} + +// =========================================================================== +// Failing handler tests +// =========================================================================== + +#[tokio::test] +async fn handler_failure() { + #[derive(Debug, thiserror::Error)] + #[error("resolution failed")] + struct ResolutionError; + async fn failing_handler(_did: CoreDID) -> std::result::Result { + Err(ResolutionError) + } + + let mut resolver_foo: Resolver = Resolver::new(); + let mut resolver_core: Resolver = Resolver::new(); + resolver_foo.attach_handler("foo".to_owned(), failing_handler); + resolver_core.attach_handler("foo".to_owned(), failing_handler); + + let bad_did: CoreDID = CoreDID::parse("did:foo:1234").unwrap(); + let good_did: CoreDID = CoreDID::parse("did:bar:1234").unwrap(); + resolver_foo.attach_handler(good_did.method().to_owned(), mock_handler); + resolver_core.attach_handler(good_did.method().to_owned(), mock_handler); + + // to avoid boiler plate + let error_matcher = |err: ErrorCause| { + assert!(err.source().unwrap().downcast_ref::().is_some()); + match err { + ErrorCause::HandlerError { .. } => {} + _ => unreachable!(), + } + }; + + let err_cause: ErrorCause = resolver_foo.resolve(&bad_did).await.unwrap_err().into_error_cause(); + error_matcher(err_cause); + + let err_cause: ErrorCause = resolver_core.resolve(&bad_did).await.unwrap_err().into_error_cause(); + error_matcher(err_cause); + + assert!(resolver_foo.resolve(&good_did).await.is_ok()); + assert!(resolver_core.resolve(&good_did).await.is_ok()); +} + +// =========================================================================== +// Resolve Multiple. +// =========================================================================== + +#[tokio::test] +async fn resolve_multiple() { + let method_name: String = "foo".to_owned(); + + let did_1: CoreDID = CoreDID::parse(format!("did:{method_name}:1111")).unwrap(); + let did_2: CoreDID = CoreDID::parse(format!("did:{method_name}:2222")).unwrap(); + let did_3: CoreDID = CoreDID::parse(format!("did:{method_name}:3333")).unwrap(); + let did_1_clone: CoreDID = CoreDID::parse(format!("did:{method_name}:1111")).unwrap(); + + let mut resolver: Resolver = Resolver::new(); + resolver.attach_handler(method_name, mock_handler); + + // Resolve with duplicate `did_3`. + let resolved_dids: HashMap = resolver + .resolve_multiple(&[ + did_1.clone(), + did_2.clone(), + did_3.clone(), + did_1_clone.clone(), + did_3.clone(), + ]) + .await + .unwrap(); + + assert_eq!(resolved_dids.len(), 3); + assert_eq!(resolved_dids.get(&did_1).unwrap().id(), &did_1); + assert_eq!(resolved_dids.get(&did_2).unwrap().id(), &did_2); + assert_eq!(resolved_dids.get(&did_3).unwrap().id(), &did_3); + assert_eq!(resolved_dids.get(&did_1_clone).unwrap().id(), &did_1_clone); + + let dids: &[CoreDID] = &[]; + let resolved_dids: HashMap = resolver.resolve_multiple(dids).await.unwrap(); + assert_eq!(resolved_dids.len(), 0); + + let resolved_dids: HashMap = resolver.resolve_multiple(&[did_1.clone()]).await.unwrap(); + assert_eq!(resolved_dids.len(), 1); + assert_eq!(resolved_dids.get(&did_1).unwrap().id(), &did_1); +} diff --git a/identity_resolver/src/resolution/tests/send_sync.rs b/identity_resolver/src/resolution/tests/send_sync.rs new file mode 100644 index 0000000000..99d8ef0fe4 --- /dev/null +++ b/identity_resolver/src/resolution/tests/send_sync.rs @@ -0,0 +1,26 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use super::*; + +use identity_did::DID; +use identity_document::document::CoreDocument; + +fn is_send(_t: T) {} +fn is_send_sync(_t: T) {} + +#[allow(dead_code)] +fn default_resolver_is_send_sync + Send + Sync + 'static>() { + let resolver = Resolver::::new(); + is_send_sync(resolver); +} + +#[allow(dead_code)] +fn resolver_methods_give_send_futures(did: D) +where + DOC: AsRef + Send + Sync + 'static, + D: DID + Send + Sync + 'static, +{ + let resolver = Resolver::::new(); + is_send(resolver.resolve(&did)); +} diff --git a/identity_resolver/src/resolver.rs b/identity_resolver/src/resolver.rs deleted file mode 100644 index daf600239e..0000000000 --- a/identity_resolver/src/resolver.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![allow(async_fn_in_trait)] - -use crate::Result; - -pub trait Resolver { - type Target; - async fn resolve(&self, input: &I) -> Result; - async fn resolve_multiple(&self, inputs: impl AsRef<[I]>) -> Result> { - let mut results = Vec::::with_capacity(inputs.as_ref().len()); - for input in inputs.as_ref() { - let result = self.resolve(input).await?; - results.push(result); - } - - Ok(results) - } -} From 3f992c45d3d5481fdb89c0dc82a1355ccb54ef33 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 11 Dec 2024 17:14:06 +0100 Subject: [PATCH 24/29] fix CI errors --- identity_core/src/common/string_or_url.rs | 5 +++-- identity_credential/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/identity_core/src/common/string_or_url.rs b/identity_core/src/common/string_or_url.rs index 99f4e57f7a..11e4e5dff2 100644 --- a/identity_core/src/common/string_or_url.rs +++ b/identity_core/src/common/string_or_url.rs @@ -1,6 +1,7 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::convert::Infallible; use std::fmt::Display; use std::str::FromStr; @@ -21,7 +22,7 @@ pub enum StringOrUrl { impl StringOrUrl { /// Parses a [`StringOrUrl`] from a string. - pub fn parse(s: &str) -> Result { + pub fn parse(s: &str) -> Result { s.parse() } /// Returns a [`Url`] reference if `self` is [`StringOrUrl::Url`]. @@ -68,7 +69,7 @@ impl Display for StringOrUrl { impl FromStr for StringOrUrl { // Cannot fail. - type Err = (); + type Err = Infallible; fn from_str(s: &str) -> Result { Ok( s.parse::() diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 91ff18c974..8aae0a2c2c 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -43,9 +43,9 @@ zkryptium = { workspace = true, optional = true } anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } iota-crypto = { version = "0.23.2", default-features = false, features = ["ed25519", "std", "random"] } +josekit = "0.8" proptest = { version = "1.4.0", default-features = false, features = ["std"] } tokio = { version = "1.35.0", default-features = false, features = ["rt-multi-thread", "macros"] } -josekit = "0.8" [package.metadata.docs.rs] # To build locally: From 66f7b795d6b30d94a1316fcea00cbbcfc9acd5cd Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 11 Dec 2024 18:30:54 +0100 Subject: [PATCH 25/29] make clippy happy --- Cargo.toml | 2 - compound_resolver/Cargo.toml | 21 --- compound_resolver/src/lib.rs | 162 ------------------ identity_core/Cargo.toml | 1 - identity_credential/Cargo.toml | 1 - .../src/credential/jwt_serialization.rs | 2 +- .../domain_linkage_validator.rs | 1 - .../src/presentation/jwt_serialization.rs | 2 +- .../src/revocation/status_list_2021/entry.rs | 2 +- .../revocation_timeframe_status.rs | 2 +- .../src/sd_jwt_vc/metadata/claim.rs | 14 +- .../src/sd_jwt_vc/metadata/vc_type.rs | 6 +- identity_credential/src/sd_jwt_vc/token.rs | 6 +- identity_document/Cargo.toml | 1 - .../src/document/core_document.rs | 4 +- identity_document/src/utils/did_url_query.rs | 4 +- identity_ecdsa_verifier/Cargo.toml | 1 - identity_eddsa_verifier/Cargo.toml | 1 - identity_iota/Cargo.toml | 1 - identity_iota_core/Cargo.toml | 1 - .../src/document/iota_document.rs | 4 +- identity_jose/Cargo.toml | 1 - identity_jose/src/jws/decoder.rs | 2 +- identity_jose/src/jws/encoding/utils.rs | 4 +- identity_jose/src/jws/recipient.rs | 2 +- identity_resolver/Cargo.toml | 1 - identity_storage/Cargo.toml | 1 - identity_stronghold/Cargo.toml | 1 - identity_verification/Cargo.toml | 1 - 29 files changed, 26 insertions(+), 226 deletions(-) delete mode 100644 compound_resolver/Cargo.toml delete mode 100644 compound_resolver/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 6f35f6713f..6dc4d7dae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ members = [ "identity_ecdsa_verifier", "identity_eddsa_verifier", "examples", - "compound_resolver", ] exclude = ["bindings/wasm", "bindings/grpc"] @@ -35,7 +34,6 @@ edition = "2021" homepage = "https://www.iota.org" license = "Apache-2.0" repository = "https://github.com/iotaledger/identity.rs" -rust-version = "1.65" [workspace.lints.clippy] result_large_err = "allow" diff --git a/compound_resolver/Cargo.toml b/compound_resolver/Cargo.toml deleted file mode 100644 index 51c009c12c..0000000000 --- a/compound_resolver/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "compound_resolver" -version = "1.3.0" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true - -[dependencies] -itertools = "0.13.0" -proc-macro2 = "1.0.85" -quote = "1.0.36" -syn = { version = "2.0.66", features = ["full", "extra-traits"] } - -[lints] -workspace = true - -[lib] -proc-macro = true diff --git a/compound_resolver/src/lib.rs b/compound_resolver/src/lib.rs deleted file mode 100644 index a778c9141a..0000000000 --- a/compound_resolver/src/lib.rs +++ /dev/null @@ -1,162 +0,0 @@ -use itertools::Itertools; -use proc_macro::TokenStream; -use quote::quote; -use syn::parse::Parse; -use syn::punctuated::Punctuated; -use syn::Attribute; -use syn::Data; -use syn::DeriveInput; -use syn::Expr; -use syn::Field; -use syn::Ident; -use syn::Token; -use syn::Type; - -#[proc_macro_derive(CompoundResolver, attributes(resolver))] -pub fn derive_macro_compound_resolver(input: TokenStream) -> TokenStream { - let DeriveInput { - ident: struct_ident, - data, - generics, - .. - } = syn::parse_macro_input!(input); - - let Data::Struct(data) = data else { - panic!("Derive macro \"CompoundResolver\" only works on structs"); - }; - - data - .fields - .into_iter() - // parse all the fields that are annoted with #[resolver(..)] - .filter_map(ResolverField::from_field) - // Group together all resolvers with the same signature (input_ty, target_ty). - .flat_map(|ResolverField { ident, impls }| { - impls - .into_iter() - .map(move |ResolverImpl { input, target, pred }| ((input, target), (ident.clone(), pred))) - }) - .into_group_map() - .into_iter() - // generates code that forward the implementation of Resolver to field_name, if there's multiple fields - // implementing that trait, use `pred` to decide which one to call. - .map(|((input_ty, target_ty), impls)| { - let len = impls.len(); - let impl_block = gen_impl_block_multiple_resolvers(impls.into_iter(), len); - quote! { - impl ::identity_iota::resolver::Resolver<#input_ty> for #struct_ident #generics { - type Target = #target_ty; - async fn resolve(&self, input: &#input_ty) -> std::result::Result { - #impl_block - } - } - } - }) - .collect::() - .into() -} - -fn gen_impl_block_single_resolver(field_name: Ident) -> proc_macro2::TokenStream { - quote! { - self.#field_name.resolve(input).await - } -} - -fn gen_impl_block_single_resolver_with_pred(field_name: Ident, pred: Expr) -> proc_macro2::TokenStream { - let invocation_block = gen_impl_block_single_resolver(field_name); - quote! { - if #pred { return #invocation_block } - } -} - -fn gen_impl_block_multiple_resolvers( - impls: impl Iterator)>, - len: usize, -) -> proc_macro2::TokenStream { - impls - .enumerate() - .map(|(i, (field_name, pred))| { - if let Some(pred) = pred { - gen_impl_block_single_resolver_with_pred(field_name, pred) - } else if i == len - 1 { - gen_impl_block_single_resolver(field_name) - } else { - panic!("Multiple resolvers with the same signature. Expected predicate"); - } - }) - .collect() -} - -/// A field annotated with `#[resolver(Input -> Target, ..)]` -struct ResolverField { - ident: Ident, - impls: Vec, -} - -impl ResolverField { - pub fn from_field(field: Field) -> Option { - let Field { attrs, ident, .. } = field; - let Some(ident) = ident else { - panic!("Derive macro \"CompoundResolver\" only works on struct with named fields"); - }; - - let impls = attrs - .into_iter() - .flat_map(|attr| parse_resolver_attribute(attr).into_iter()) - .collect::>(); - - if !impls.is_empty() { - Some(ResolverField { ident, impls }) - } else { - None - } - } -} - -fn parse_resolver_attribute(attr: Attribute) -> Vec { - if attr.path().is_ident("resolver") { - attr - .parse_args_with(Punctuated::::parse_terminated) - .expect("invalid resolver annotation") - .into_iter() - .collect() - } else { - vec![] - } -} - -struct ResolverImpl { - pub input: Type, - pub target: Type, - pub pred: Option, -} - -impl Parse for ResolverImpl { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let input_ty = input.parse::()?; - let _ = input.parse::]>()?; - let target_ty = input.parse::()?; - let pred = if input.peek(Token![if]) { - let _ = input.parse::()?; - Some(input.parse::()?) - } else { - None - }; - - Ok({ - ResolverImpl { - input: input_ty, - target: target_ty, - pred, - } - }) - } -} - -#[test] -fn test_parse_resolver_attribute() { - syn::parse_str::("DidKey -> CoreDoc").unwrap(); - syn::parse_str::("DidKey -> Vec").unwrap(); - syn::parse_str::("Vec -> &str").unwrap(); - syn::parse_str::("DidIota -> IotaDoc if input.method_id() == \"iota\"").unwrap(); -} diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index fcdd263cc7..619745cb85 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "tangle", "identity"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "The core traits and types for the identity-rs library." [dependencies] diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 8aae0a2c2c..7372290a8b 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "tangle", "identity"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "An implementation of the Verifiable Credentials standard." [dependencies] diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 3f2a33f0a7..a92eb78ce8 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -118,7 +118,7 @@ where } #[cfg(feature = "validator")] -impl<'credential, T> CredentialJwtClaims<'credential, T> +impl CredentialJwtClaims<'_, T> where T: ToOwned + Serialize + DeserializeOwned, { diff --git a/identity_credential/src/domain_linkage/domain_linkage_validator.rs b/identity_credential/src/domain_linkage/domain_linkage_validator.rs index be67c96832..746a9b2f5a 100644 --- a/identity_credential/src/domain_linkage/domain_linkage_validator.rs +++ b/identity_credential/src/domain_linkage/domain_linkage_validator.rs @@ -21,7 +21,6 @@ use super::DomainLinkageValidationResult; use crate::utils::url_only_includes_origin; /// A validator for a Domain Linkage Configuration and Credentials. - pub struct JwtDomainLinkageValidator { validator: JwtCredentialValidator, } diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index d8bb18c238..50aab3d428 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -136,7 +136,7 @@ where } #[cfg(feature = "validator")] -impl<'presentation, CRED, T> PresentationJwtClaims<'presentation, CRED, T> +impl PresentationJwtClaims<'_, CRED, T> where CRED: ToOwned + Serialize + DeserializeOwned + Clone, T: ToOwned + Serialize + DeserializeOwned, diff --git a/identity_credential/src/revocation/status_list_2021/entry.rs b/identity_credential/src/revocation/status_list_2021/entry.rs index 92415d06b7..1108b5e7c1 100644 --- a/identity_credential/src/revocation/status_list_2021/entry.rs +++ b/identity_credential/src/revocation/status_list_2021/entry.rs @@ -18,7 +18,7 @@ where D: serde::Deserializer<'de>, { struct ExactStrVisitor(&'static str); - impl<'a> Visitor<'a> for ExactStrVisitor { + impl Visitor<'_> for ExactStrVisitor { type Value = &'static str; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(formatter, "the exact string \"{}\"", self.0) diff --git a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs index 0a70589112..6ae6ea74f8 100644 --- a/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs +++ b/identity_credential/src/revocation/validity_timeframe_2024/revocation_timeframe_status.rs @@ -18,7 +18,7 @@ where D: serde::Deserializer<'de>, { struct ExactStrVisitor(&'static str); - impl<'a> Visitor<'a> for ExactStrVisitor { + impl Visitor<'_> for ExactStrVisitor { type Value = &'static str; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(formatter, "the exact string \"{}\"", self.0) diff --git a/identity_credential/src/sd_jwt_vc/metadata/claim.rs b/identity_credential/src/sd_jwt_vc/metadata/claim.rs index 28a0b0faef..8dcfdde0c1 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/claim.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/claim.rs @@ -231,13 +231,11 @@ fn index_value<'v>(value: &'v Value, segment: &ClaimPathSegment) -> anyhow::Resu #[cfg(test)] mod tests { - use std::cell::LazyCell; - use serde_json::json; use super::*; - const SAMPLE_OBJ: LazyCell = LazyCell::new(|| { + fn sample_obj() -> Value { json!({ "vct": "https://betelgeuse.example.com/education_credential", "name": "Arthur Dent", @@ -258,7 +256,7 @@ mod tests { ], "nationalities": ["British", "Betelgeusian"] }) - }); + } #[test] fn claim_path_works() { @@ -268,18 +266,18 @@ mod tests { let degrees_types_path = serde_json::from_value::(json!(["degrees", null, "type"])).unwrap(); assert!(matches!( - name_path.reverse_index(&SAMPLE_OBJ).unwrap(), + name_path.reverse_index(&sample_obj()).unwrap(), OneOrManyValue::One(&Value::String(_)) )); assert!(matches!( - city_path.reverse_index(&SAMPLE_OBJ).unwrap(), + city_path.reverse_index(&sample_obj()).unwrap(), OneOrManyValue::One(&Value::String(_)) )); assert!(matches!( - first_degree_path.reverse_index(&SAMPLE_OBJ).unwrap(), + first_degree_path.reverse_index(&sample_obj()).unwrap(), OneOrManyValue::One(&Value::Object(_)) )); - let obj = &*SAMPLE_OBJ; + let obj = &sample_obj(); let mut degree_types = degrees_types_path.reverse_index(obj).unwrap().into_iter(); assert_eq!(degree_types.next().unwrap().as_str(), Some("Bachelor of Science")); assert_eq!(degree_types.next().unwrap().as_str(), Some("Master of Science")); diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index 9cb07d9cc3..ad4ae5e0e9 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -185,7 +185,7 @@ pub enum TypeSchema { #[cfg(test)] mod tests { - use std::cell::LazyCell; + use std::sync::LazyLock; use async_trait::async_trait; use serde_json::json; @@ -194,7 +194,7 @@ mod tests { use super::*; - const IMMEDIATE_TYPE_METADATA: LazyCell = LazyCell::new(|| TypeMetadata { + static IMMEDIATE_TYPE_METADATA: LazyLock = LazyLock::new(|| TypeMetadata { name: Some("immediate credential".to_string()), description: None, extends: None, @@ -218,7 +218,7 @@ mod tests { schema_integrity: None, }), }); - const REFERENCED_TYPE_METADATA: LazyCell = LazyCell::new(|| TypeMetadata { + static REFERENCED_TYPE_METADATA: LazyLock = LazyLock::new(|| TypeMetadata { name: Some("immediate credential".to_string()), description: None, extends: None, diff --git a/identity_credential/src/sd_jwt_vc/token.rs b/identity_credential/src/sd_jwt_vc/token.rs index e898fd6ac9..f2635c7f7a 100644 --- a/identity_credential/src/sd_jwt_vc/token.rs +++ b/identity_credential/src/sd_jwt_vc/token.rs @@ -443,7 +443,7 @@ impl From for SdJwt { #[cfg(test)] mod tests { - use std::cell::LazyCell; + use std::sync::LazyLock; use identity_core::common::StringOrUrl; use identity_core::common::Url; @@ -451,8 +451,8 @@ mod tests { use super::*; const EXAMPLE_SD_JWT_VC: &str = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCJ9.eyJfc2QiOiBbIjBIWm1uU0lQejMzN2tTV2U3QzM0bC0tODhnekppLWVCSjJWel9ISndBVGciLCAiOVpicGxDN1RkRVc3cWFsNkJCWmxNdHFKZG1lRU9pWGV2ZEpsb1hWSmRSUSIsICJJMDBmY0ZVb0RYQ3VjcDV5eTJ1anFQc3NEVkdhV05pVWxpTnpfYXdEMGdjIiwgIklFQllTSkdOaFhJbHJRbzU4eWtYbTJaeDN5bGw5WmxUdFRvUG8xN1FRaVkiLCAiTGFpNklVNmQ3R1FhZ1hSN0F2R1RyblhnU2xkM3o4RUlnX2Z2M2ZPWjFXZyIsICJodkRYaHdtR2NKUXNCQ0EyT3RqdUxBY3dBTXBEc2FVMG5rb3ZjS09xV05FIiwgImlrdXVyOFE0azhxM1ZjeUE3ZEMtbU5qWkJrUmVEVFUtQ0c0bmlURTdPVFUiLCAicXZ6TkxqMnZoOW80U0VYT2ZNaVlEdXZUeWtkc1dDTmcwd1RkbHIwQUVJTSIsICJ3elcxNWJoQ2t2a3N4VnZ1SjhSRjN4aThpNjRsbjFqb183NkJDMm9hMXVnIiwgInpPZUJYaHh2SVM0WnptUWNMbHhLdUVBT0dHQnlqT3FhMXoySW9WeF9ZRFEiXSwgImlzcyI6ICJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsICJpYXQiOiAxNjgzMDAwMDAwLCAiZXhwIjogMTg4MzAwMDAwMCwgInZjdCI6ICJodHRwczovL2JtaS5idW5kLmV4YW1wbGUvY3JlZGVudGlhbC9waWQvMS4wIiwgImFnZV9lcXVhbF9vcl9vdmVyIjogeyJfc2QiOiBbIkZjOElfMDdMT2NnUHdyREpLUXlJR085N3dWc09wbE1Makh2UkM0UjQtV2ciLCAiWEx0TGphZFVXYzl6Tl85aE1KUm9xeTQ2VXNDS2IxSXNoWnV1cVVGS1NDQSIsICJhb0NDenNDN3A0cWhaSUFoX2lkUkNTQ2E2NDF1eWNuYzh6UGZOV3o4bngwIiwgImYxLVAwQTJkS1dhdnYxdUZuTVgyQTctRVh4dmhveHY1YUhodUVJTi1XNjQiLCAiazVoeTJyMDE4dnJzSmpvLVZqZDZnNnl0N0Fhb25Lb25uaXVKOXplbDNqbyIsICJxcDdaX0t5MVlpcDBzWWdETzN6VnVnMk1GdVBOakh4a3NCRG5KWjRhSS1jIl19LCAiX3NkX2FsZyI6ICJzaGEtMjU2IiwgImNuZiI6IHsiandrIjogeyJrdHkiOiAiRUMiLCAiY3J2IjogIlAtMjU2IiwgIngiOiAiVENBRVIxOVp2dTNPSEY0ajRXNHZmU1ZvSElQMUlMaWxEbHM3dkNlR2VtYyIsICJ5IjogIlp4amlXV2JaTVFHSFZXS1ZRNGhiU0lpcnNWZnVlY0NFNnQ0alQ5RjJIWlEifX19.CaXec2NNooWAy4eTxYbGWI--UeUL0jpC7Zb84PP_09Z655BYcXUTvfj6GPk4mrNqZUU5GT6QntYR8J9rvcBjvA~WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d~WyJNMEpiNTd0NDF1YnJrU3V5ckRUM3hBIiwgIjE4IiwgdHJ1ZV0~eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImtiK2p3dCJ9.eyJub25jZSI6ICIxMjM0NTY3ODkwIiwgImF1ZCI6ICJodHRwczovL2V4YW1wbGUuY29tL3ZlcmlmaWVyIiwgImlhdCI6IDE3MjA0NTQyOTUsICJzZF9oYXNoIjogIlZFejN0bEtqOVY0UzU3TTZoRWhvVjRIc19SdmpXZWgzVHN1OTFDbmxuZUkifQ.GqtiTKNe3O95GLpdxFK_2FZULFk6KUscFe7RPk8OeVLiJiHsGvtPyq89e_grBplvGmnDGHoy8JAt1wQqiwktSg"; - const EXAMPLE_ISSUER: LazyCell = LazyCell::new(|| "https://example.com/issuer".parse().unwrap()); - const EXAMPLE_VCT: LazyCell = LazyCell::new(|| { + static EXAMPLE_ISSUER: LazyLock = LazyLock::new(|| "https://example.com/issuer".parse().unwrap()); + static EXAMPLE_VCT: LazyLock = LazyLock::new(|| { "https://bmi.bund.example/credential/pid/1.0" .parse::() .unwrap() diff --git a/identity_document/Cargo.toml b/identity_document/Cargo.toml index 4bb50dd09d..cf212716ba 100644 --- a/identity_document/Cargo.toml +++ b/identity_document/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "tangle", "identity", "did"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "Method-agnostic implementation of the Decentralized Identifiers (DID) standard." [dependencies] diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 2747f7fae6..1e1a340bb4 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -690,7 +690,7 @@ impl CoreDocument { &'me self, method_query: Q, scope: Option, - ) -> Option<&VerificationMethod> + ) -> Option<&'me VerificationMethod> where Q: Into>, { @@ -773,7 +773,7 @@ impl CoreDocument { /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present. // NOTE: This method demonstrates unexpected behavior in the edge cases where the document contains // services whose ids are of the form #. - pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service> + pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&'me Service> where Q: Into>, { diff --git a/identity_document/src/utils/did_url_query.rs b/identity_document/src/utils/did_url_query.rs index 1af2b80b4c..d9399457e3 100644 --- a/identity_document/src/utils/did_url_query.rs +++ b/identity_document/src/utils/did_url_query.rs @@ -13,7 +13,7 @@ use identity_did::DID; #[repr(transparent)] pub struct DIDUrlQuery<'query>(Cow<'query, str>); -impl<'query> DIDUrlQuery<'query> { +impl DIDUrlQuery<'_> { /// Returns whether this query matches the given DIDUrl. pub(crate) fn matches(&self, did_url: &DIDUrl) -> bool { // Ensure the DID matches if included in the query. @@ -81,7 +81,7 @@ impl<'query> From<&'query DIDUrl> for DIDUrlQuery<'query> { } } -impl<'query> From for DIDUrlQuery<'query> { +impl From for DIDUrlQuery<'_> { fn from(other: DIDUrl) -> Self { Self(Cow::Owned(other.to_string())) } diff --git a/identity_ecdsa_verifier/Cargo.toml b/identity_ecdsa_verifier/Cargo.toml index 6829d41ae0..6c7e70a954 100644 --- a/identity_ecdsa_verifier/Cargo.toml +++ b/identity_ecdsa_verifier/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "identity", "jose", "jwk", "jws"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "JWS ECDSA signature verification for IOTA Identity" [lints] diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml index b7da49295a..745f9b6b0d 100644 --- a/identity_eddsa_verifier/Cargo.toml +++ b/identity_eddsa_verifier/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "identity", "jose", "jwk", "jws"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "JWS EdDSA signature verification for IOTA Identity" [dependencies] diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index 6d1bf0a9a5..e77e68a151 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "tangle", "identity", "did", "ssi"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "Framework for Self-Sovereign Identity with IOTA DID." [dependencies] diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 73dcc4190e..0303e351a3 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "tangle", "utxo", "shimmer", "identity"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "An IOTA Ledger integration for the IOTA DID Method." [dependencies] diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index bd3404045c..5c0813f28c 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -332,7 +332,7 @@ impl IotaDocument { /// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present. // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains // services whose ids are of the form #. - pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service> + pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&'me Service> where Q: Into>, { @@ -347,7 +347,7 @@ impl IotaDocument { &'me self, method_query: Q, scope: Option, - ) -> Option<&VerificationMethod> + ) -> Option<&'me VerificationMethod> where Q: Into>, { diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index 73a7fa3cdb..e5449c30a6 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "identity", "jose", "jwk", "jws"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] diff --git a/identity_jose/src/jws/decoder.rs b/identity_jose/src/jws/decoder.rs index 6b93488acf..c1635c86d3 100644 --- a/identity_jose/src/jws/decoder.rs +++ b/identity_jose/src/jws/decoder.rs @@ -322,7 +322,7 @@ pub struct JwsValidationIter<'decoder, 'payload, 'signatures> { payload: &'payload [u8], } -impl<'decoder, 'payload, 'signatures> Iterator for JwsValidationIter<'decoder, 'payload, 'signatures> { +impl<'payload> Iterator for JwsValidationIter<'_, 'payload, '_> { type Item = Result>; fn next(&mut self) -> Option { diff --git a/identity_jose/src/jws/encoding/utils.rs b/identity_jose/src/jws/encoding/utils.rs index b1d903e612..2be2703488 100644 --- a/identity_jose/src/jws/encoding/utils.rs +++ b/identity_jose/src/jws/encoding/utils.rs @@ -86,7 +86,7 @@ pub(super) struct Flatten<'payload, 'unprotected> { pub(super) signature: JwsSignature<'unprotected>, } -impl<'payload, 'unprotected> Flatten<'payload, 'unprotected> { +impl Flatten<'_, '_> { pub(super) fn to_json(&self) -> Result { serde_json::to_string(&self).map_err(Error::InvalidJson) } @@ -99,7 +99,7 @@ pub(super) struct General<'payload, 'unprotected> { pub(super) signatures: Vec>, } -impl<'payload, 'unprotected> General<'payload, 'unprotected> { +impl General<'_, '_> { pub(super) fn to_json(&self) -> Result { serde_json::to_string(&self).map_err(Error::InvalidJson) } diff --git a/identity_jose/src/jws/recipient.rs b/identity_jose/src/jws/recipient.rs index 602f1e6f3f..96dd410fa0 100644 --- a/identity_jose/src/jws/recipient.rs +++ b/identity_jose/src/jws/recipient.rs @@ -15,7 +15,7 @@ pub struct Recipient<'a> { pub unprotected: Option<&'a JwsHeader>, } -impl<'a> Default for Recipient<'a> { +impl Default for Recipient<'_> { fn default() -> Self { Self::new() } diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index d99158835d..fd8ffd7a0b 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "did", "identity", "resolver", "resolution"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "DID Resolution utilities for the identity.rs library." [dependencies] diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index 5331dc725f..2e07548630 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "storage", "identity", "kms"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "Abstractions over storage for cryptographic keys used in DID Documents" [dependencies] diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index b7c61a998f..693dfa271e 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -8,7 +8,6 @@ keywords = ["iota", "storage", "identity", "kms", "stronghold"] license.workspace = true readme = "./README.md" repository.workspace = true -rust-version.workspace = true description = "Secure JWK storage with Stronghold for IOTA Identity" [dependencies] diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml index 46fcc5ac24..9c122cec93 100644 --- a/identity_verification/Cargo.toml +++ b/identity_verification/Cargo.toml @@ -6,7 +6,6 @@ edition.workspace = true homepage.workspace = true license.workspace = true repository.workspace = true -rust-version.workspace = true description = "Verification data types and functionality for identity.rs" [dependencies] From 03f1483f85aa8d0bfd98bf12dc4704cfc57e3401 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 11 Dec 2024 19:37:30 +0100 Subject: [PATCH 26/29] add missing license headers --- identity_credential/src/sd_jwt_vc/builder.rs | 3 +++ identity_credential/src/sd_jwt_vc/metadata/vc_type.rs | 3 +++ identity_credential/src/sd_jwt_vc/presentation.rs | 3 +++ identity_credential/src/sd_jwt_vc/resolver.rs | 3 +++ identity_credential/src/sd_jwt_vc/tests/mod.rs | 3 +++ identity_credential/src/sd_jwt_vc/tests/validation.rs | 3 +++ 6 files changed, 18 insertions(+) diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index 77a2f0d98a..df7daafa30 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + #![allow(clippy::vec_init_then_push)] use std::sync::LazyLock; diff --git a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs index ad4ae5e0e9..97be651392 100644 --- a/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs +++ b/identity_credential/src/sd_jwt_vc/metadata/vc_type.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use futures::future::BoxFuture; use futures::future::FutureExt; use identity_core::common::Url; diff --git a/identity_credential/src/sd_jwt_vc/presentation.rs b/identity_credential/src/sd_jwt_vc/presentation.rs index 26a80a2164..06a2d2feac 100644 --- a/identity_credential/src/sd_jwt_vc/presentation.rs +++ b/identity_credential/src/sd_jwt_vc/presentation.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use super::Error; use super::Result; use super::SdJwtVc; diff --git a/identity_credential/src/sd_jwt_vc/resolver.rs b/identity_credential/src/sd_jwt_vc/resolver.rs index fbcd0fb273..5e0993a90a 100644 --- a/identity_credential/src/sd_jwt_vc/resolver.rs +++ b/identity_credential/src/sd_jwt_vc/resolver.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + #![allow(async_fn_in_trait)] use async_trait::async_trait; diff --git a/identity_credential/src/sd_jwt_vc/tests/mod.rs b/identity_credential/src/sd_jwt_vc/tests/mod.rs index e2453eb145..f93fe20784 100644 --- a/identity_credential/src/sd_jwt_vc/tests/mod.rs +++ b/identity_credential/src/sd_jwt_vc/tests/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use std::collections::HashMap; use async_trait::async_trait; diff --git a/identity_credential/src/sd_jwt_vc/tests/validation.rs b/identity_credential/src/sd_jwt_vc/tests/validation.rs index 2e165ac186..bc17b33952 100644 --- a/identity_credential/src/sd_jwt_vc/tests/validation.rs +++ b/identity_credential/src/sd_jwt_vc/tests/validation.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_core::common::Timestamp; use identity_core::common::Url; use identity_verification::jwk::JwkSet; From 51e1b280445815a807b5e6e8c88129c166a947fe Mon Sep 17 00:00:00 2001 From: umr1352 Date: Wed, 11 Dec 2024 21:04:44 +0100 Subject: [PATCH 27/29] add 'SdJwtVcBuilder::from_credential' to easily convert into a SD-JWT VC --- identity_credential/Cargo.toml | 2 +- .../src/credential/jwt_serialization.rs | 2 +- identity_credential/src/error.rs | 7 +++ identity_credential/src/sd_jwt_vc/builder.rs | 56 +++++++++++++++++++ identity_credential/src/sd_jwt_vc/status.rs | 3 + 5 files changed, 68 insertions(+), 2 deletions(-) diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 7372290a8b..1562305623 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -15,7 +15,7 @@ anyhow = { version = "1" } async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } -futures = { version = "0.3", default-features = false, optional = true } +futures = { version = "0.3", default-features = false, features = ["alloc"], optional = true } identity_core = { version = "=1.4.0", path = "../identity_core", default-features = false } identity_did = { version = "=1.4.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.4.0", path = "../identity_document", default-features = false } diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index a92eb78ce8..feb3de531d 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -67,7 +67,7 @@ impl<'credential, T> CredentialJwtClaims<'credential, T> where T: ToOwned + Serialize + DeserializeOwned, { - pub(super) fn new(credential: &'credential Credential, custom: Option) -> Result { + pub(crate) fn new(credential: &'credential Credential, custom: Option) -> Result { let Credential { context, id, diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 1c814c3899..a5bbc1f86a 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -3,6 +3,8 @@ //! Errors that may occur when working with Verifiable Credentials. +use crate::sd_jwt_vc; + /// Alias for a `Result` with the error type [`Error`]. pub type Result = ::core::result::Result; @@ -79,4 +81,9 @@ pub enum Error { /// Cause by an invalid attribute path #[error("Attribute Not found")] SelectiveDisclosureError, + + /// Failure of an SD-JWT VC operation. + #[cfg(feature = "sd-jwt-vc")] + #[error(transparent)] + SdJwtVc(#[from] sd_jwt_vc::Error), } diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index df7daafa30..8bef4059ce 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -7,6 +7,7 @@ use std::sync::LazyLock; use identity_core::common::StringOrUrl; use identity_core::common::Timestamp; use identity_core::common::Url; +use identity_core::convert::ToJson; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::JwsSigner; @@ -15,6 +16,10 @@ use sd_jwt_payload_rework::SdJwtBuilder; use sd_jwt_payload_rework::Sha256Hasher; use serde::Serialize; use serde_json::json; +use serde_json::Value; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; use super::Error; use super::Result; @@ -101,6 +106,25 @@ impl SdJwtVcBuilder { }) } + /// Creates a new [`SdJwtVcBuilder`] starting from a [`Credential`] that is converted to a JWT claim set. + pub fn new_from_credential(credential: Credential, hasher: H) -> std::result::Result { + let mut vc_jwt_claims = CredentialJwtClaims::new(&credential, None)? + .to_json_value() + .map_err(|e| crate::Error::JwtClaimsSetSerializationError(Box::new(e)))?; + // When converting a VC to its JWT claims representation, some VC specific claims are putted into a `vc` object + // property. Flatten out `vc`, keeping the other JWT claims intact. + { + let claims = vc_jwt_claims.as_object_mut().expect("serialized VC is a JSON object"); + let Value::Object(vc_properties) = claims.remove("vc").expect("serialized VC has `vc` property") else { + unreachable!("`vc` property's value is a JSON object"); + }; + for (key, value) in vc_properties { + claims.insert(key, value); + } + } + Ok(Self::new_with_hasher(vc_jwt_claims, hasher)?) + } + /// Substitutes a value with the digest of its disclosure. /// /// ## Notes @@ -244,7 +268,10 @@ impl SdJwtVcBuilder { #[cfg(test)] mod tests { + use super::*; + use crate::credential::CredentialBuilder; + use crate::credential::Subject; use crate::sd_jwt_vc::tests::TestSigner; #[tokio::test] @@ -327,4 +354,33 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn building_sd_jwt_vc_from_credential_works() -> anyhow::Result<()> { + let credential = CredentialBuilder::default() + .id(Url::parse("https://example.com/credentials/42")?) + .issuance_date(Timestamp::now_utc()) + .issuer(Url::parse("https://example.com/issuers/42")?) + .subject(Subject::with_id(Url::parse("https://example.com/subjects/42")?)) + .build()?; + + let sd_jwt_vc = SdJwtVcBuilder::new_from_credential(credential.clone(), Sha256Hasher::default())? + .vct(Url::parse("https://example.com/types/0")?) + .finish(&TestSigner, "HS256") + .await?; + + assert_eq!(sd_jwt_vc.claims().nbf.as_ref().unwrap(), &credential.issuance_date); + assert_eq!(&sd_jwt_vc.claims().iss, credential.issuer.url()); + assert_eq!( + sd_jwt_vc.claims().sub.as_ref().unwrap().as_url(), + credential.credential_subject.first().unwrap().id.as_ref() + ); + assert_eq!( + sd_jwt_vc.claims().get("jti"), + Some(&json!(credential.id.as_ref().unwrap())) + ); + assert_eq!(sd_jwt_vc.claims().get("type"), Some(&json!("VerifiableCredential"))); + + Ok(()) + } } diff --git a/identity_credential/src/sd_jwt_vc/status.rs b/identity_credential/src/sd_jwt_vc/status.rs index 1738447293..1c68db6d4c 100644 --- a/identity_credential/src/sd_jwt_vc/status.rs +++ b/identity_credential/src/sd_jwt_vc/status.rs @@ -16,6 +16,9 @@ pub enum StatusMechanism { /// Reference to a status list containing this token's status. #[serde(rename = "status_list")] StatusList(StatusListRef), + /// A non-standard status mechanism. + #[serde(untagged)] + Custom(serde_json::Value), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] From 035b50f266588e6cc47f85aadc0efa6b4c1cbd82 Mon Sep 17 00:00:00 2001 From: umr1352 Date: Thu, 12 Dec 2024 10:57:04 +0100 Subject: [PATCH 28/29] cargo clippy --- identity_credential/src/sd_jwt_vc/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index 8bef4059ce..46b28bc9b2 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -364,7 +364,7 @@ mod tests { .subject(Subject::with_id(Url::parse("https://example.com/subjects/42")?)) .build()?; - let sd_jwt_vc = SdJwtVcBuilder::new_from_credential(credential.clone(), Sha256Hasher::default())? + let sd_jwt_vc = SdJwtVcBuilder::new_from_credential(credential.clone(), Sha256Hasher)? .vct(Url::parse("https://example.com/types/0")?) .finish(&TestSigner, "HS256") .await?; From f2c0704aa1949f8785c5c20c5a6a9771e49d7e5e Mon Sep 17 00:00:00 2001 From: umr1352 Date: Thu, 12 Dec 2024 13:33:14 +0100 Subject: [PATCH 29/29] fix wasm compilation errors, clippy --- bindings/wasm/src/common/imported_document_lock.rs | 2 +- bindings/wasm/src/error.rs | 2 +- identity_credential/src/error.rs | 4 +--- identity_credential/src/lib.rs | 2 +- identity_iota/Cargo.toml | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/bindings/wasm/src/common/imported_document_lock.rs b/bindings/wasm/src/common/imported_document_lock.rs index 4852ab216e..4452ce6dd2 100644 --- a/bindings/wasm/src/common/imported_document_lock.rs +++ b/bindings/wasm/src/common/imported_document_lock.rs @@ -79,7 +79,7 @@ impl From<&ArrayIToCoreDocument> for Vec { pub(crate) struct ImportedDocumentReadGuard<'a>(tokio::sync::RwLockReadGuard<'a, CoreDocument>); -impl<'a> AsRef for ImportedDocumentReadGuard<'a> { +impl AsRef for ImportedDocumentReadGuard<'_> { fn as_ref(&self) -> &CoreDocument { self.0.as_ref() } diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 035e7838bf..34d4c98d8b 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -151,7 +151,7 @@ fn error_chain_fmt(e: &impl std::error::Error, f: &mut std::fmt::Formatter<'_>) struct ErrorMessage<'a, E: std::error::Error>(&'a E); -impl<'a, E: std::error::Error> Display for ErrorMessage<'a, E> { +impl Display for ErrorMessage<'_, E> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { error_chain_fmt(self.0, f) } diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index a5bbc1f86a..18b31d69c3 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -3,8 +3,6 @@ //! Errors that may occur when working with Verifiable Credentials. -use crate::sd_jwt_vc; - /// Alias for a `Result` with the error type [`Error`]. pub type Result = ::core::result::Result; @@ -85,5 +83,5 @@ pub enum Error { /// Failure of an SD-JWT VC operation. #[cfg(feature = "sd-jwt-vc")] #[error(transparent)] - SdJwtVc(#[from] sd_jwt_vc::Error), + SdJwtVc(#[from] crate::sd_jwt_vc::Error), } diff --git a/identity_credential/src/lib.rs b/identity_credential/src/lib.rs index 3c03954b85..236329ab4c 100644 --- a/identity_credential/src/lib.rs +++ b/identity_credential/src/lib.rs @@ -28,7 +28,7 @@ mod utils; pub mod validator; /// Implementation of the SD-JWT VC token specification. -// #[cfg(feature = "sd-jwt-vc")] +#[cfg(feature = "sd-jwt-vc")] pub mod sd_jwt_vc; pub use error::Error; diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index e77e68a151..cdddbcebc9 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -45,7 +45,7 @@ revocation-bitmap = [ status-list-2021 = ["revocation-bitmap", "identity_credential/status-list-2021"] # Enables support for the `Resolver`. -resolver = [] +resolver = ["dep:identity_resolver"] # Enables `Send` + `Sync` bounds for the storage traits. send-sync-storage = ["identity_storage/send-sync-storage"]