Skip to content

Commit

Permalink
Linked Verifiable Presentations (#1398)
Browse files Browse the repository at this point in the history
* 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
4 people authored Sep 5, 2024
1 parent 02a0857 commit deecc7e
Show file tree
Hide file tree
Showing 8 changed files with 536 additions and 14 deletions.
109 changes: 109 additions & 0 deletions bindings/wasm/src/credential/linked_verifiable_presentation_service.rs
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
);
2 changes: 2 additions & 0 deletions bindings/wasm/src/credential/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub use self::jws::WasmJws;
pub use self::jwt::WasmJwt;
pub use self::jwt_credential_validation::*;
pub use self::jwt_presentation_validation::*;
pub use self::linked_verifiable_presentation_service::*;
pub use self::options::WasmFailFast;
pub use self::options::WasmSubjectHolderRelationship;
pub use self::presentation::*;
Expand All @@ -33,6 +34,7 @@ mod jwt;
mod jwt_credential_validation;
mod jwt_presentation_validation;
mod linked_domain_service;
mod linked_verifiable_presentation_service;
mod options;
mod presentation;
mod proof;
Expand Down
195 changes: 195 additions & 0 deletions examples/1_advanced/11_linked_verifiable_presentation.rs
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")
}
6 changes: 5 additions & 1 deletion examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
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
Expand Down Expand Up @@ -101,3 +101,7 @@ name = "9_zkp"
[[example]]
path = "1_advanced/10_zkp_revocation.rs"
name = "10_zkp_revocation"

[[example]]
path = "1_advanced/11_linked_verifiable_presentation.rs"
name = "11_linked_verifiable_presentation"
26 changes: 13 additions & 13 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ cargo run --release --example 0_create_did

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


## Basic Examples

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

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

The following advanced examples are available:

| Name | Information |
|:-----------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------|
| [0_did_controls_did](./1_advanced/0_did_controls_did.rs) | Demonstrates how an identity can control another identity. |
| [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. |
| [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. |
| [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. |
| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. |
| [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. |
| [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. |
| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. |
| [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. |
| Name | Information |
| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- |
| [0_did_controls_did](./1_advanced/0_did_controls_did.rs) | Demonstrates how an identity can control another identity. |
| [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. |
| [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. |
| [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. |
| [4_alias_output_history](./1_advanced/4_alias_output_history.rs) | Demonstrates fetching the history of an Alias Output. |
| [5_custom_resolution](./1_advanced/5_custom_resolution.rs) | Demonstrates how to set up a resolver using custom handlers. |
| [6_domain_linkage](./1_advanced/6_domain_linkage) | Demonstrates how to link a domain and a DID and verify the linkage. |
| [7_sd_jwt](./1_advanced/7_sd_jwt) | Demonstrates how to create and verify selective disclosure verifiable credentials. |
| [8_status_list_2021](./1_advanced/8_status_list_2021.rs) | Demonstrates how to revoke a credential using `StatusList2021`. |
| [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. |
Loading

0 comments on commit deecc7e

Please sign in to comment.