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

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b7853fb
Resolver trait and CompoundResolver macro
UMR1352 Jun 11, 2024
0114b35
invert Resolver type parameters
UMR1352 Jun 11, 2024
88a538e
associated type Target instead of type parameter T
UMR1352 Jun 11, 2024
bfd5d59
fix type issue in #[resolver(..)] annotation, support for multiple re…
UMR1352 Jun 12, 2024
5671118
resolver integration
UMR1352 Jun 13, 2024
d7c2cd9
Merge branch 'main' into identity2/resolver
UMR1352 Sep 16, 2024
7c815c5
feature gate resolver-v2
UMR1352 Sep 16, 2024
b28fa9a
structures & basic operations
UMR1352 Sep 17, 2024
2f7fadf
SdJwtVc behaves as a superset of SdJwt
UMR1352 Sep 17, 2024
0d3127f
issuer's metadata fetching & validation
UMR1352 Sep 18, 2024
55319c5
type metadata & credential verification
UMR1352 Sep 19, 2024
704b395
change resolver's constraints
UMR1352 Sep 19, 2024
130af00
integrity metadata
UMR1352 Sep 19, 2024
d278060
display metadata
UMR1352 Sep 20, 2024
0a1ed27
claim metadata
UMR1352 Sep 20, 2024
8d00263
fetch issuer's JWK (to ease verification)
UMR1352 Sep 20, 2024
2111e3b
validate claim disclosability
UMR1352 Sep 24, 2024
06e31f2
add missing license header
UMR1352 Sep 24, 2024
e5e8537
resolver change, validation
UMR1352 Sep 24, 2024
9bf907f
SdJwtVcBuilder & tests
UMR1352 Sep 30, 2024
67a0abc
validation test
UMR1352 Oct 1, 2024
a510185
KB-JWT validation
UMR1352 Oct 2, 2024
654b188
review comment
UMR1352 Oct 11, 2024
72333bb
undo resolver-v2
UMR1352 Dec 11, 2024
468809e
Merge branch 'main' into feat/sd-jwt-vc
UMR1352 Dec 11, 2024
3f992c4
fix CI errors
UMR1352 Dec 11, 2024
66f7b79
make clippy happy
UMR1352 Dec 11, 2024
03f1483
add missing license headers
UMR1352 Dec 11, 2024
51e1b28
add 'SdJwtVcBuilder::from_credential' to easily convert into a SD-JWT VC
UMR1352 Dec 11, 2024
035b50f
cargo clippy
UMR1352 Dec 12, 2024
f2c0704
fix wasm compilation errors, clippy
UMR1352 Dec 12, 2024
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