Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SD-JWT VC implementation #1413

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ members = [
"identity_ecdsa_verifier",
"identity_eddsa_verifier",
"examples",
"compound_resolver",
]

exclude = ["bindings/wasm", "bindings/grpc"]
Expand Down
21 changes: 21 additions & 0 deletions compound_resolver/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[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
162 changes: 162 additions & 0 deletions compound_resolver/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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<T, I> 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<Self::Target, ::identity_iota::resolver::Error> {
#impl_block
}
}
}
})
.collect::<proc_macro2::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<Item = (Ident, Option<Expr>)>,
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<ResolverImpl>,
}

impl ResolverField {
pub fn from_field(field: Field) -> Option<Self> {
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::<Vec<_>>();

if !impls.is_empty() {
Some(ResolverField { ident, impls })
} else {
None
}
}
}

fn parse_resolver_attribute(attr: Attribute) -> Vec<ResolverImpl> {
if attr.path().is_ident("resolver") {
attr
.parse_args_with(Punctuated::<ResolverImpl, Token![,]>::parse_terminated)
.expect("invalid resolver annotation")
.into_iter()
.collect()
} else {
vec![]
}
}

struct ResolverImpl {
pub input: Type,
pub target: Type,
pub pred: Option<Expr>,
}

impl Parse for ResolverImpl {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let input_ty = input.parse::<Type>()?;
let _ = input.parse::<Token![->]>()?;
let target_ty = input.parse::<Type>()?;
let pred = if input.peek(Token![if]) {
let _ = input.parse::<Token![if]>()?;
Some(input.parse::<Expr>()?)
} else {
None
};

Ok({
ResolverImpl {
input: input_ty,
target: target_ty,
pred,
}
})
}
}

#[test]
fn test_parse_resolver_attribute() {
syn::parse_str::<ResolverImpl>("DidKey -> CoreDoc").unwrap();
syn::parse_str::<ResolverImpl>("DidKey -> Vec<u8>").unwrap();
syn::parse_str::<ResolverImpl>("Vec<u8> -> &str").unwrap();
syn::parse_str::<ResolverImpl>("DidIota -> IotaDoc if input.method_id() == \"iota\"").unwrap();
}
14 changes: 0 additions & 14 deletions examples/0_basic/2_resolve_did.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<IotaDocument>::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?;

Expand Down
11 changes: 3 additions & 8 deletions examples/0_basic/6_create_vp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -190,12 +188,9 @@ async fn main() -> anyhow::Result<()> {
let presentation_verifier_options: JwsVerificationOptions =
JwsVerificationOptions::default().nonce(challenge.to_owned());

let mut resolver: Resolver<IotaDocument> = 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 =
Expand All @@ -211,7 +206,7 @@ async fn main() -> anyhow::Result<()> {
.iter()
.map(JwtCredentialValidatorUtils::extract_issuer_from_jwt)
.collect::<Result<Vec<CoreDID>, _>>()?;
let issuers_documents: HashMap<CoreDID, IotaDocument> = resolver.resolve_multiple(&issuers).await?;
let issuers_documents: Vec<IotaDocument> = client.resolve_multiple(&issuers).await?;

// Validate the credentials in the presentation.
let credential_validator: JwtCredentialValidator<EdDSAJwsVerifier> =
Expand All @@ -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<Object> = credential_validator
.validate::<_, Object>(jwt_vc, issuer_document, &validation_options, FailFast::FirstError)
Expand Down
4 changes: 1 addition & 3 deletions examples/0_basic/7_revoke_vc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IotaDocument> = 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,
Expand Down
4 changes: 1 addition & 3 deletions examples/0_basic/8_stronghold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ async fn main() -> anyhow::Result<()> {
.await?;

// Resolve the published DID Document.
let mut resolver = Resolver::<IotaDocument>::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);

Expand Down
5 changes: 1 addition & 4 deletions examples/1_advanced/10_zkp_revocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,12 +413,9 @@ async fn main() -> anyhow::Result<()> {
let presentation_verifier_options: JwsVerificationOptions =
JwsVerificationOptions::default().nonce(challenge.to_owned());

let mut resolver: Resolver<IotaDocument> = 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 =
Expand Down
8 changes: 2 additions & 6 deletions examples/1_advanced/11_linked_verifiable_presentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,8 @@ async fn main() -> anyhow::Result<()> {
// Verification
// =====================================================

// Init a resolver for resolving DID Documents.
let mut resolver: Resolver<IotaDocument> = 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<LinkedVerifiablePresentationService> = did_document
Expand All @@ -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();
Expand Down
Loading