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

Linked Verifiable Presentations #1398

Merged
merged 9 commits into from
Sep 5, 2024
Merged
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
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
Loading