Skip to content

Commit deecc7e

Browse files
daniel-maderUMR1352wulfraem
authored
Linked Verifiable Presentations (#1398)
* feat: implement `Service` for Linked Verifiable Presentations * feat: add example for Linked Verifiable Presentations * cargo clippy, fmt, code * cargo clippy + fmt * fix linked vp example * wasm bindings * Update bindings/wasm/src/credential/linked_verifiable_presentation_service.rs Co-authored-by: wulfraem <wulfraem@users.noreply.github.com> * cargo fmt --------- Co-authored-by: Enrico Marconi <enrico.marconi@hotmail.it> Co-authored-by: Enrico Marconi <31142849+UMR1352@users.noreply.github.com> Co-authored-by: wulfraem <wulfraem@users.noreply.github.com>
1 parent 02a0857 commit deecc7e

File tree

8 files changed

+536
-14
lines changed

8 files changed

+536
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2020-2023 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use crate::common::ArrayString;
5+
use crate::did::WasmService;
6+
use crate::error::Result;
7+
use crate::error::WasmResult;
8+
use identity_iota::core::Object;
9+
use identity_iota::core::OneOrSet;
10+
use identity_iota::core::Url;
11+
use identity_iota::credential::LinkedVerifiablePresentationService;
12+
use identity_iota::did::DIDUrl;
13+
use identity_iota::document::Service;
14+
use proc_typescript::typescript;
15+
use wasm_bindgen::prelude::wasm_bindgen;
16+
use wasm_bindgen::prelude::*;
17+
use wasm_bindgen::JsCast;
18+
19+
#[wasm_bindgen(js_name = LinkedVerifiablePresentationService, inspectable)]
20+
pub struct WasmLinkedVerifiablePresentationService(LinkedVerifiablePresentationService);
21+
22+
/// A service wrapper for a [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint).
23+
#[wasm_bindgen(js_class = LinkedVerifiablePresentationService)]
24+
impl WasmLinkedVerifiablePresentationService {
25+
/// Constructs a new {@link LinkedVerifiablePresentationService} that wraps a spec compliant [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint).
26+
#[wasm_bindgen(constructor)]
27+
pub fn new(options: ILinkedVerifiablePresentationService) -> Result<WasmLinkedVerifiablePresentationService> {
28+
let ILinkedVerifiablePresentationServiceHelper {
29+
id,
30+
linked_vp,
31+
properties,
32+
} = options
33+
.into_serde::<ILinkedVerifiablePresentationServiceHelper>()
34+
.wasm_result()?;
35+
Ok(Self(
36+
LinkedVerifiablePresentationService::new(id, linked_vp, properties).wasm_result()?,
37+
))
38+
}
39+
40+
/// Returns the domains contained in the Linked Verifiable Presentation Service.
41+
#[wasm_bindgen(js_name = verifiablePresentationUrls)]
42+
pub fn vp_urls(&self) -> ArrayString {
43+
self
44+
.0
45+
.verifiable_presentation_urls()
46+
.iter()
47+
.map(|url| url.to_string())
48+
.map(JsValue::from)
49+
.collect::<js_sys::Array>()
50+
.unchecked_into::<ArrayString>()
51+
}
52+
53+
/// Returns the inner service which can be added to a DID Document.
54+
#[wasm_bindgen(js_name = toService)]
55+
pub fn to_service(&self) -> WasmService {
56+
let service: Service = self.0.clone().into();
57+
WasmService(service)
58+
}
59+
60+
/// Creates a new {@link LinkedVerifiablePresentationService} from a {@link Service}.
61+
///
62+
/// # Error
63+
///
64+
/// Errors if `service` is not a valid Linked Verifiable Presentation Service.
65+
#[wasm_bindgen(js_name = fromService)]
66+
pub fn from_service(service: &WasmService) -> Result<WasmLinkedVerifiablePresentationService> {
67+
Ok(Self(
68+
LinkedVerifiablePresentationService::try_from(service.0.clone()).wasm_result()?,
69+
))
70+
}
71+
72+
/// Returns `true` if a {@link Service} is a valid Linked Verifiable Presentation Service.
73+
#[wasm_bindgen(js_name = isValid)]
74+
pub fn is_valid(service: &WasmService) -> bool {
75+
LinkedVerifiablePresentationService::check_structure(&service.0).is_ok()
76+
}
77+
}
78+
79+
#[wasm_bindgen]
80+
extern "C" {
81+
#[wasm_bindgen(typescript_type = "ILinkedVerifiablePresentationService")]
82+
pub type ILinkedVerifiablePresentationService;
83+
}
84+
85+
/// Fields for constructing a new {@link LinkedVerifiablePresentationService}.
86+
#[derive(Deserialize)]
87+
#[serde(rename_all = "camelCase")]
88+
#[typescript(name = "ILinkedVerifiablePresentationService", readonly, optional)]
89+
struct ILinkedVerifiablePresentationServiceHelper {
90+
/// Service id.
91+
#[typescript(optional = false, type = "DIDUrl")]
92+
id: DIDUrl,
93+
/// A unique URI that may be used to identify the {@link Credential}.
94+
#[typescript(optional = false, type = "string | string[]")]
95+
linked_vp: OneOrSet<Url>,
96+
/// Miscellaneous properties.
97+
#[serde(flatten)]
98+
#[typescript(optional = false, name = "[properties: string]", type = "unknown")]
99+
properties: Object,
100+
}
101+
102+
impl_wasm_clone!(
103+
WasmLinkedVerifiablePresentationService,
104+
LinkedVerifiablePresentationService
105+
);
106+
impl_wasm_json!(
107+
WasmLinkedVerifiablePresentationService,
108+
LinkedVerifiablePresentationService
109+
);

bindings/wasm/src/credential/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub use self::jws::WasmJws;
1313
pub use self::jwt::WasmJwt;
1414
pub use self::jwt_credential_validation::*;
1515
pub use self::jwt_presentation_validation::*;
16+
pub use self::linked_verifiable_presentation_service::*;
1617
pub use self::options::WasmFailFast;
1718
pub use self::options::WasmSubjectHolderRelationship;
1819
pub use self::presentation::*;
@@ -33,6 +34,7 @@ mod jwt;
3334
mod jwt_credential_validation;
3435
mod jwt_presentation_validation;
3536
mod linked_domain_service;
37+
mod linked_verifiable_presentation_service;
3638
mod options;
3739
mod presentation;
3840
mod proof;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright 2020-2024 IOTA Stiftung
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use anyhow::Context;
5+
use examples::create_did;
6+
use examples::random_stronghold_path;
7+
use examples::MemStorage;
8+
use examples::API_ENDPOINT;
9+
use identity_eddsa_verifier::EdDSAJwsVerifier;
10+
use identity_iota::core::FromJson;
11+
use identity_iota::core::Object;
12+
use identity_iota::core::OrderedSet;
13+
use identity_iota::core::Url;
14+
use identity_iota::credential::CompoundJwtPresentationValidationError;
15+
use identity_iota::credential::CredentialBuilder;
16+
use identity_iota::credential::DecodedJwtPresentation;
17+
use identity_iota::credential::Jwt;
18+
use identity_iota::credential::JwtPresentationOptions;
19+
use identity_iota::credential::JwtPresentationValidationOptions;
20+
use identity_iota::credential::JwtPresentationValidator;
21+
use identity_iota::credential::JwtPresentationValidatorUtils;
22+
use identity_iota::credential::LinkedVerifiablePresentationService;
23+
use identity_iota::credential::PresentationBuilder;
24+
use identity_iota::credential::Subject;
25+
use identity_iota::did::CoreDID;
26+
use identity_iota::did::DIDUrl;
27+
use identity_iota::did::DID;
28+
use identity_iota::document::verifiable::JwsVerificationOptions;
29+
use identity_iota::iota::IotaClientExt;
30+
use identity_iota::iota::IotaDID;
31+
use identity_iota::iota::IotaDocument;
32+
use identity_iota::iota::IotaIdentityClientExt;
33+
use identity_iota::resolver::Resolver;
34+
use identity_iota::storage::JwkDocumentExt;
35+
use identity_iota::storage::JwkMemStore;
36+
use identity_iota::storage::JwsSignatureOptions;
37+
use identity_iota::storage::KeyIdMemstore;
38+
use iota_sdk::client::secret::stronghold::StrongholdSecretManager;
39+
use iota_sdk::client::secret::SecretManager;
40+
use iota_sdk::client::Client;
41+
use iota_sdk::client::Password;
42+
use iota_sdk::types::block::address::Address;
43+
use iota_sdk::types::block::output::AliasOutput;
44+
use iota_sdk::types::block::output::AliasOutputBuilder;
45+
use iota_sdk::types::block::output::RentStructure;
46+
47+
#[tokio::main]
48+
async fn main() -> anyhow::Result<()> {
49+
// Create a new client to interact with the IOTA ledger.
50+
let client: Client = Client::builder()
51+
.with_primary_node(API_ENDPOINT, None)?
52+
.finish()
53+
.await?;
54+
let stronghold_path = random_stronghold_path();
55+
56+
println!("Using stronghold path: {stronghold_path:?}");
57+
// Create a new secret manager backed by a Stronghold.
58+
let mut secret_manager: SecretManager = SecretManager::Stronghold(
59+
StrongholdSecretManager::builder()
60+
.password(Password::from("secure_password".to_owned()))
61+
.build(stronghold_path)?,
62+
);
63+
64+
// Create a DID for the entity that will be the holder of the Verifiable Presentation.
65+
let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new());
66+
let (_, mut did_document, fragment): (Address, IotaDocument, String) =
67+
create_did(&client, &mut secret_manager, &storage).await?;
68+
let did: IotaDID = did_document.id().clone();
69+
70+
// =====================================================
71+
// Create Linked Verifiable Presentation service
72+
// =====================================================
73+
74+
// The DID should link to the following VPs.
75+
let verifiable_presentation_url_1: Url = Url::parse("https://foo.example.com/verifiable-presentation.jwt")?;
76+
let verifiable_presentation_url_2: Url = Url::parse("https://bar.example.com/verifiable-presentation.jsonld")?;
77+
78+
let mut verifiable_presentation_urls: OrderedSet<Url> = OrderedSet::new();
79+
verifiable_presentation_urls.append(verifiable_presentation_url_1.clone());
80+
verifiable_presentation_urls.append(verifiable_presentation_url_2.clone());
81+
82+
// Create a Linked Verifiable Presentation Service to enable the discovery of the linked VPs through the DID Document.
83+
// This is optional since it is not a hard requirement by the specs.
84+
let service_url: DIDUrl = did.clone().join("#linked-vp")?;
85+
let linked_verifiable_presentation_service =
86+
LinkedVerifiablePresentationService::new(service_url, verifiable_presentation_urls, Object::new())?;
87+
did_document.insert_service(linked_verifiable_presentation_service.into())?;
88+
let updated_did_document: IotaDocument = publish_document(client.clone(), secret_manager, did_document).await?;
89+
90+
println!("DID document with linked verifiable presentation service: {updated_did_document:#}");
91+
92+
// =====================================================
93+
// Verification
94+
// =====================================================
95+
96+
// Init a resolver for resolving DID Documents.
97+
let mut resolver: Resolver<IotaDocument> = Resolver::new();
98+
resolver.attach_iota_handler(client.clone());
99+
100+
// Resolve the DID Document of the DID that issued the credential.
101+
let did_document: IotaDocument = resolver.resolve(&did).await?;
102+
103+
// Get the Linked Verifiable Presentation Services from the DID Document.
104+
let linked_verifiable_presentation_services: Vec<LinkedVerifiablePresentationService> = did_document
105+
.service()
106+
.iter()
107+
.cloned()
108+
.filter_map(|service| LinkedVerifiablePresentationService::try_from(service).ok())
109+
.collect();
110+
assert_eq!(linked_verifiable_presentation_services.len(), 1);
111+
112+
// Get the VPs included in the service.
113+
let _verifiable_presentation_urls: &[Url] = linked_verifiable_presentation_services
114+
.first()
115+
.ok_or_else(|| anyhow::anyhow!("expected verifiable presentation urls"))?
116+
.verifiable_presentation_urls();
117+
118+
// Fetch the verifiable presentation from the URL (for example using `reqwest`).
119+
// But since the URLs do not point to actual online resource, we will simply create an example JWT.
120+
let presentation_jwt: Jwt = make_vp_jwt(&did_document, &storage, &fragment).await?;
121+
122+
// Resolve the holder's document.
123+
let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?;
124+
let holder: IotaDocument = resolver.resolve(&holder_did).await?;
125+
126+
// Validate linked presentation. Note that this doesn't validate the included credentials.
127+
let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default();
128+
let presentation_validation_options =
129+
JwtPresentationValidationOptions::default().presentation_verifier_options(presentation_verifier_options);
130+
let validation_result: Result<DecodedJwtPresentation<Jwt>, CompoundJwtPresentationValidationError> =
131+
JwtPresentationValidator::with_signature_verifier(EdDSAJwsVerifier::default()).validate(
132+
&presentation_jwt,
133+
&holder,
134+
&presentation_validation_options,
135+
);
136+
137+
assert!(validation_result.is_ok());
138+
139+
Ok(())
140+
}
141+
142+
async fn publish_document(
143+
client: Client,
144+
secret_manager: SecretManager,
145+
document: IotaDocument,
146+
) -> anyhow::Result<IotaDocument> {
147+
// Resolve the latest output and update it with the given document.
148+
let alias_output: AliasOutput = client.update_did_output(document.clone()).await?;
149+
150+
// Because the size of the DID document increased, we have to increase the allocated storage deposit.
151+
// This increases the deposit amount to the new minimum.
152+
let rent_structure: RentStructure = client.get_rent_structure().await?;
153+
let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output)
154+
.with_minimum_storage_deposit(rent_structure)
155+
.finish()?;
156+
157+
// Publish the updated Alias Output.
158+
Ok(client.publish_did_output(&secret_manager, alias_output).await?)
159+
}
160+
161+
async fn make_vp_jwt(did_doc: &IotaDocument, storage: &MemStorage, fragment: &str) -> anyhow::Result<Jwt> {
162+
// first we create a credential encoding it as jwt
163+
let credential = CredentialBuilder::new(Object::default())
164+
.id(Url::parse("https://example.edu/credentials/3732")?)
165+
.issuer(Url::parse(did_doc.id().as_str())?)
166+
.type_("UniversityDegreeCredential")
167+
.subject(Subject::from_json_value(serde_json::json!({
168+
"id": did_doc.id().as_str(),
169+
"name": "Alice",
170+
"degree": {
171+
"type": "BachelorDegree",
172+
"name": "Bachelor of Science and Arts",
173+
},
174+
"GPA": "4.0",
175+
}))?)
176+
.build()?;
177+
let credential = did_doc
178+
.create_credential_jwt(&credential, storage, fragment, &JwsSignatureOptions::default(), None)
179+
.await?;
180+
// then we create a presentation including the just created JWT encoded credential.
181+
let presentation = PresentationBuilder::new(Url::parse(did_doc.id().as_str())?, Object::default())
182+
.credential(credential)
183+
.build()?;
184+
// we encode the presentation as JWT
185+
did_doc
186+
.create_presentation_jwt(
187+
&presentation,
188+
storage,
189+
fragment,
190+
&JwsSignatureOptions::default(),
191+
&JwtPresentationOptions::default(),
192+
)
193+
.await
194+
.context("jwt presentation failed")
195+
}

examples/Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ publish = false
99
anyhow = "1.0.62"
1010
bls12_381_plus.workspace = true
1111
identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false }
12-
identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus"] }
12+
identity_iota = { path = "../identity_iota", default-features = false, features = ["iota-client", "client", "memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "jpt-bbs-plus", "resolver"] }
1313
identity_stronghold = { path = "../identity_stronghold", default-features = false, features = ["bbs-plus"] }
1414
iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] }
1515
json-proof-token.workspace = true
@@ -101,3 +101,7 @@ name = "9_zkp"
101101
[[example]]
102102
path = "1_advanced/10_zkp_revocation.rs"
103103
name = "10_zkp_revocation"
104+
105+
[[example]]
106+
path = "1_advanced/11_linked_verifiable_presentation.rs"
107+
name = "11_linked_verifiable_presentation"

examples/README.md

+13-13
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@ cargo run --release --example 0_create_did
1818

1919
### Note: Running the examples with the release flag will be significantly faster due to stronghold performance issues in debug mode.
2020

21-
2221
## Basic Examples
2322

2423
The following basic CRUD (Create, Read, Update, Delete) examples are available:
2524

2625
| Name | Information |
27-
|:--------------------------------------------------|:-------------------------------------------------------------------------------------|
26+
| :------------------------------------------------ | :----------------------------------------------------------------------------------- |
2827
| [0_create_did](./0_basic/0_create_did.rs) | Demonstrates how to create a DID Document and publish it in a new Alias Output. |
2928
| [1_update_did](./0_basic/1_update_did.rs) | Demonstrates how to update a DID document in an existing Alias Output. |
3029
| [2_resolve_did](./0_basic/2_resolve_did.rs) | Demonstrates how to resolve an existing DID in an Alias Output. |
@@ -38,14 +37,15 @@ The following basic CRUD (Create, Read, Update, Delete) examples are available:
3837

3938
The following advanced examples are available:
4039

41-
| Name | Information |
42-
|:-----------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------|
43-
| [0_did_controls_did](./1_advanced/0_did_controls_did.rs) | Demonstrates how an identity can control another identity. |
44-
| [1_did_issues_nft](./1_advanced/1_did_issues_nft.rs) | Demonstrates how an identity can issue and own NFTs, and how observers can verify the issuer of the NFT. |
45-
| [2_nft_owns_did](./1_advanced/2_nft_owns_did.rs) | Demonstrates how an identity can be owned by NFTs, and how observers can verify that relationship. |
46-
| [3_did_issues_tokens](./1_advanced/3_did_issues_tokens.rs) | Demonstrates how an identity can issue and control a Token Foundry and its tokens. |
47-
| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. |
48-
| [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. |
49-
| [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. |
50-
| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. |
51-
| [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. |
40+
| Name | Information |
41+
| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- |
42+
| [0_did_controls_did](./1_advanced/0_did_controls_did.rs) | Demonstrates how an identity can control another identity. |
43+
| [1_did_issues_nft](./1_advanced/1_did_issues_nft.rs) | Demonstrates how an identity can issue and own NFTs, and how observers can verify the issuer of the NFT. |
44+
| [2_nft_owns_did](./1_advanced/2_nft_owns_did.rs) | Demonstrates how an identity can be owned by NFTs, and how observers can verify that relationship. |
45+
| [3_did_issues_tokens](./1_advanced/3_did_issues_tokens.rs) | Demonstrates how an identity can issue and control a Token Foundry and its tokens. |
46+
| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. |
47+
| [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. |
48+
| [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. |
49+
| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. |
50+
| [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. |
51+
| [11_linked_verifiable_presentation](./1_advanced/11_linked_verifiable_presentation.rs) | Demonstrates how to link a public Verifiable Presentation to an identity and how it can be verified. |

0 commit comments

Comments
 (0)