-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]> * cargo fmt --------- Co-authored-by: Enrico Marconi <[email protected]> Co-authored-by: Enrico Marconi <[email protected]> Co-authored-by: wulfraem <[email protected]>
- Loading branch information
1 parent
02a0857
commit deecc7e
Showing
8 changed files
with
536 additions
and
14 deletions.
There are no files selected for viewing
109 changes: 109 additions & 0 deletions
109
bindings/wasm/src/credential/linked_verifiable_presentation_service.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// Copyright 2020-2023 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
use crate::common::ArrayString; | ||
use crate::did::WasmService; | ||
use crate::error::Result; | ||
use crate::error::WasmResult; | ||
use identity_iota::core::Object; | ||
use identity_iota::core::OneOrSet; | ||
use identity_iota::core::Url; | ||
use identity_iota::credential::LinkedVerifiablePresentationService; | ||
use identity_iota::did::DIDUrl; | ||
use identity_iota::document::Service; | ||
use proc_typescript::typescript; | ||
use wasm_bindgen::prelude::wasm_bindgen; | ||
use wasm_bindgen::prelude::*; | ||
use wasm_bindgen::JsCast; | ||
|
||
#[wasm_bindgen(js_name = LinkedVerifiablePresentationService, inspectable)] | ||
pub struct WasmLinkedVerifiablePresentationService(LinkedVerifiablePresentationService); | ||
|
||
/// A service wrapper for a [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). | ||
#[wasm_bindgen(js_class = LinkedVerifiablePresentationService)] | ||
impl WasmLinkedVerifiablePresentationService { | ||
/// Constructs a new {@link LinkedVerifiablePresentationService} that wraps a spec compliant [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). | ||
#[wasm_bindgen(constructor)] | ||
pub fn new(options: ILinkedVerifiablePresentationService) -> Result<WasmLinkedVerifiablePresentationService> { | ||
let ILinkedVerifiablePresentationServiceHelper { | ||
id, | ||
linked_vp, | ||
properties, | ||
} = options | ||
.into_serde::<ILinkedVerifiablePresentationServiceHelper>() | ||
.wasm_result()?; | ||
Ok(Self( | ||
LinkedVerifiablePresentationService::new(id, linked_vp, properties).wasm_result()?, | ||
)) | ||
} | ||
|
||
/// Returns the domains contained in the Linked Verifiable Presentation Service. | ||
#[wasm_bindgen(js_name = verifiablePresentationUrls)] | ||
pub fn vp_urls(&self) -> ArrayString { | ||
self | ||
.0 | ||
.verifiable_presentation_urls() | ||
.iter() | ||
.map(|url| url.to_string()) | ||
.map(JsValue::from) | ||
.collect::<js_sys::Array>() | ||
.unchecked_into::<ArrayString>() | ||
} | ||
|
||
/// Returns the inner service which can be added to a DID Document. | ||
#[wasm_bindgen(js_name = toService)] | ||
pub fn to_service(&self) -> WasmService { | ||
let service: Service = self.0.clone().into(); | ||
WasmService(service) | ||
} | ||
|
||
/// Creates a new {@link LinkedVerifiablePresentationService} from a {@link Service}. | ||
/// | ||
/// # Error | ||
/// | ||
/// Errors if `service` is not a valid Linked Verifiable Presentation Service. | ||
#[wasm_bindgen(js_name = fromService)] | ||
pub fn from_service(service: &WasmService) -> Result<WasmLinkedVerifiablePresentationService> { | ||
Ok(Self( | ||
LinkedVerifiablePresentationService::try_from(service.0.clone()).wasm_result()?, | ||
)) | ||
} | ||
|
||
/// Returns `true` if a {@link Service} is a valid Linked Verifiable Presentation Service. | ||
#[wasm_bindgen(js_name = isValid)] | ||
pub fn is_valid(service: &WasmService) -> bool { | ||
LinkedVerifiablePresentationService::check_structure(&service.0).is_ok() | ||
} | ||
} | ||
|
||
#[wasm_bindgen] | ||
extern "C" { | ||
#[wasm_bindgen(typescript_type = "ILinkedVerifiablePresentationService")] | ||
pub type ILinkedVerifiablePresentationService; | ||
} | ||
|
||
/// Fields for constructing a new {@link LinkedVerifiablePresentationService}. | ||
#[derive(Deserialize)] | ||
#[serde(rename_all = "camelCase")] | ||
#[typescript(name = "ILinkedVerifiablePresentationService", readonly, optional)] | ||
struct ILinkedVerifiablePresentationServiceHelper { | ||
/// Service id. | ||
#[typescript(optional = false, type = "DIDUrl")] | ||
id: DIDUrl, | ||
/// A unique URI that may be used to identify the {@link Credential}. | ||
#[typescript(optional = false, type = "string | string[]")] | ||
linked_vp: OneOrSet<Url>, | ||
/// Miscellaneous properties. | ||
#[serde(flatten)] | ||
#[typescript(optional = false, name = "[properties: string]", type = "unknown")] | ||
properties: Object, | ||
} | ||
|
||
impl_wasm_clone!( | ||
WasmLinkedVerifiablePresentationService, | ||
LinkedVerifiablePresentationService | ||
); | ||
impl_wasm_json!( | ||
WasmLinkedVerifiablePresentationService, | ||
LinkedVerifiablePresentationService | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
195 changes: 195 additions & 0 deletions
195
examples/1_advanced/11_linked_verifiable_presentation.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
// Copyright 2020-2024 IOTA Stiftung | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
use anyhow::Context; | ||
use examples::create_did; | ||
use examples::random_stronghold_path; | ||
use examples::MemStorage; | ||
use examples::API_ENDPOINT; | ||
use identity_eddsa_verifier::EdDSAJwsVerifier; | ||
use identity_iota::core::FromJson; | ||
use identity_iota::core::Object; | ||
use identity_iota::core::OrderedSet; | ||
use identity_iota::core::Url; | ||
use identity_iota::credential::CompoundJwtPresentationValidationError; | ||
use identity_iota::credential::CredentialBuilder; | ||
use identity_iota::credential::DecodedJwtPresentation; | ||
use identity_iota::credential::Jwt; | ||
use identity_iota::credential::JwtPresentationOptions; | ||
use identity_iota::credential::JwtPresentationValidationOptions; | ||
use identity_iota::credential::JwtPresentationValidator; | ||
use identity_iota::credential::JwtPresentationValidatorUtils; | ||
use identity_iota::credential::LinkedVerifiablePresentationService; | ||
use identity_iota::credential::PresentationBuilder; | ||
use identity_iota::credential::Subject; | ||
use identity_iota::did::CoreDID; | ||
use identity_iota::did::DIDUrl; | ||
use identity_iota::did::DID; | ||
use identity_iota::document::verifiable::JwsVerificationOptions; | ||
use identity_iota::iota::IotaClientExt; | ||
use identity_iota::iota::IotaDID; | ||
use identity_iota::iota::IotaDocument; | ||
use identity_iota::iota::IotaIdentityClientExt; | ||
use identity_iota::resolver::Resolver; | ||
use identity_iota::storage::JwkDocumentExt; | ||
use identity_iota::storage::JwkMemStore; | ||
use identity_iota::storage::JwsSignatureOptions; | ||
use identity_iota::storage::KeyIdMemstore; | ||
use iota_sdk::client::secret::stronghold::StrongholdSecretManager; | ||
use iota_sdk::client::secret::SecretManager; | ||
use iota_sdk::client::Client; | ||
use iota_sdk::client::Password; | ||
use iota_sdk::types::block::address::Address; | ||
use iota_sdk::types::block::output::AliasOutput; | ||
use iota_sdk::types::block::output::AliasOutputBuilder; | ||
use iota_sdk::types::block::output::RentStructure; | ||
|
||
#[tokio::main] | ||
async fn main() -> anyhow::Result<()> { | ||
// Create a new client to interact with the IOTA ledger. | ||
let client: Client = Client::builder() | ||
.with_primary_node(API_ENDPOINT, None)? | ||
.finish() | ||
.await?; | ||
let stronghold_path = random_stronghold_path(); | ||
|
||
println!("Using stronghold path: {stronghold_path:?}"); | ||
// Create a new secret manager backed by a Stronghold. | ||
let mut secret_manager: SecretManager = SecretManager::Stronghold( | ||
StrongholdSecretManager::builder() | ||
.password(Password::from("secure_password".to_owned())) | ||
.build(stronghold_path)?, | ||
); | ||
|
||
// Create a DID for the entity that will be the holder of the Verifiable Presentation. | ||
let storage: MemStorage = MemStorage::new(JwkMemStore::new(), KeyIdMemstore::new()); | ||
let (_, mut did_document, fragment): (Address, IotaDocument, String) = | ||
create_did(&client, &mut secret_manager, &storage).await?; | ||
let did: IotaDID = did_document.id().clone(); | ||
|
||
// ===================================================== | ||
// Create Linked Verifiable Presentation service | ||
// ===================================================== | ||
|
||
// The DID should link to the following VPs. | ||
let verifiable_presentation_url_1: Url = Url::parse("https://foo.example.com/verifiable-presentation.jwt")?; | ||
let verifiable_presentation_url_2: Url = Url::parse("https://bar.example.com/verifiable-presentation.jsonld")?; | ||
|
||
let mut verifiable_presentation_urls: OrderedSet<Url> = OrderedSet::new(); | ||
verifiable_presentation_urls.append(verifiable_presentation_url_1.clone()); | ||
verifiable_presentation_urls.append(verifiable_presentation_url_2.clone()); | ||
|
||
// Create a Linked Verifiable Presentation Service to enable the discovery of the linked VPs through the DID Document. | ||
// This is optional since it is not a hard requirement by the specs. | ||
let service_url: DIDUrl = did.clone().join("#linked-vp")?; | ||
let linked_verifiable_presentation_service = | ||
LinkedVerifiablePresentationService::new(service_url, verifiable_presentation_urls, Object::new())?; | ||
did_document.insert_service(linked_verifiable_presentation_service.into())?; | ||
let updated_did_document: IotaDocument = publish_document(client.clone(), secret_manager, did_document).await?; | ||
|
||
println!("DID document with linked verifiable presentation service: {updated_did_document:#}"); | ||
|
||
// ===================================================== | ||
// Verification | ||
// ===================================================== | ||
|
||
// Init a resolver for resolving DID Documents. | ||
let mut resolver: Resolver<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?; | ||
|
||
// Get the Linked Verifiable Presentation Services from the DID Document. | ||
let linked_verifiable_presentation_services: Vec<LinkedVerifiablePresentationService> = did_document | ||
.service() | ||
.iter() | ||
.cloned() | ||
.filter_map(|service| LinkedVerifiablePresentationService::try_from(service).ok()) | ||
.collect(); | ||
assert_eq!(linked_verifiable_presentation_services.len(), 1); | ||
|
||
// Get the VPs included in the service. | ||
let _verifiable_presentation_urls: &[Url] = linked_verifiable_presentation_services | ||
.first() | ||
.ok_or_else(|| anyhow::anyhow!("expected verifiable presentation urls"))? | ||
.verifiable_presentation_urls(); | ||
|
||
// Fetch the verifiable presentation from the URL (for example using `reqwest`). | ||
// But since the URLs do not point to actual online resource, we will simply create an example JWT. | ||
let presentation_jwt: Jwt = make_vp_jwt(&did_document, &storage, &fragment).await?; | ||
|
||
// Resolve the holder's document. | ||
let holder_did: CoreDID = JwtPresentationValidatorUtils::extract_holder(&presentation_jwt)?; | ||
let holder: IotaDocument = resolver.resolve(&holder_did).await?; | ||
|
||
// Validate linked presentation. Note that this doesn't validate the included credentials. | ||
let presentation_verifier_options: JwsVerificationOptions = JwsVerificationOptions::default(); | ||
let presentation_validation_options = | ||
JwtPresentationValidationOptions::default().presentation_verifier_options(presentation_verifier_options); | ||
let validation_result: Result<DecodedJwtPresentation<Jwt>, CompoundJwtPresentationValidationError> = | ||
JwtPresentationValidator::with_signature_verifier(EdDSAJwsVerifier::default()).validate( | ||
&presentation_jwt, | ||
&holder, | ||
&presentation_validation_options, | ||
); | ||
|
||
assert!(validation_result.is_ok()); | ||
|
||
Ok(()) | ||
} | ||
|
||
async fn publish_document( | ||
client: Client, | ||
secret_manager: SecretManager, | ||
document: IotaDocument, | ||
) -> anyhow::Result<IotaDocument> { | ||
// Resolve the latest output and update it with the given document. | ||
let alias_output: AliasOutput = client.update_did_output(document.clone()).await?; | ||
|
||
// Because the size of the DID document increased, we have to increase the allocated storage deposit. | ||
// This increases the deposit amount to the new minimum. | ||
let rent_structure: RentStructure = client.get_rent_structure().await?; | ||
let alias_output: AliasOutput = AliasOutputBuilder::from(&alias_output) | ||
.with_minimum_storage_deposit(rent_structure) | ||
.finish()?; | ||
|
||
// Publish the updated Alias Output. | ||
Ok(client.publish_did_output(&secret_manager, alias_output).await?) | ||
} | ||
|
||
async fn make_vp_jwt(did_doc: &IotaDocument, storage: &MemStorage, fragment: &str) -> anyhow::Result<Jwt> { | ||
// first we create a credential encoding it as jwt | ||
let credential = CredentialBuilder::new(Object::default()) | ||
.id(Url::parse("https://example.edu/credentials/3732")?) | ||
.issuer(Url::parse(did_doc.id().as_str())?) | ||
.type_("UniversityDegreeCredential") | ||
.subject(Subject::from_json_value(serde_json::json!({ | ||
"id": did_doc.id().as_str(), | ||
"name": "Alice", | ||
"degree": { | ||
"type": "BachelorDegree", | ||
"name": "Bachelor of Science and Arts", | ||
}, | ||
"GPA": "4.0", | ||
}))?) | ||
.build()?; | ||
let credential = did_doc | ||
.create_credential_jwt(&credential, storage, fragment, &JwsSignatureOptions::default(), None) | ||
.await?; | ||
// then we create a presentation including the just created JWT encoded credential. | ||
let presentation = PresentationBuilder::new(Url::parse(did_doc.id().as_str())?, Object::default()) | ||
.credential(credential) | ||
.build()?; | ||
// we encode the presentation as JWT | ||
did_doc | ||
.create_presentation_jwt( | ||
&presentation, | ||
storage, | ||
fragment, | ||
&JwsSignatureOptions::default(), | ||
&JwtPresentationOptions::default(), | ||
) | ||
.await | ||
.context("jwt presentation failed") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.