diff --git a/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs b/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs new file mode 100644 index 0000000000..1033316cc7 --- /dev/null +++ b/bindings/wasm/src/credential/linked_verifiable_presentation_service.rs @@ -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 { + let ILinkedVerifiablePresentationServiceHelper { + id, + linked_vp, + properties, + } = options + .into_serde::() + .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::() + .unchecked_into::() + } + + /// 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 { + 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, + /// Miscellaneous properties. + #[serde(flatten)] + #[typescript(optional = false, name = "[properties: string]", type = "unknown")] + properties: Object, +} + +impl_wasm_clone!( + WasmLinkedVerifiablePresentationService, + LinkedVerifiablePresentationService +); +impl_wasm_json!( + WasmLinkedVerifiablePresentationService, + LinkedVerifiablePresentationService +); diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 033a8cefd6..408f302f11 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -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::*; @@ -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; diff --git a/examples/1_advanced/11_linked_verifiable_presentation.rs b/examples/1_advanced/11_linked_verifiable_presentation.rs new file mode 100644 index 0000000000..550bad3d41 --- /dev/null +++ b/examples/1_advanced/11_linked_verifiable_presentation.rs @@ -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 = 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 = 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 = 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, 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 { + // 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 { + // 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") +} diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 9fe5984123..9866115ad3 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -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 @@ -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" diff --git a/examples/README.md b/examples/README.md index 2076f8b4b2..8ea9ab2145 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. | @@ -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. | diff --git a/identity_credential/src/credential/linked_verifiable_presentation_service.rs b/identity_credential/src/credential/linked_verifiable_presentation_service.rs new file mode 100644 index 0000000000..20e34e0a97 --- /dev/null +++ b/identity_credential/src/credential/linked_verifiable_presentation_service.rs @@ -0,0 +1,207 @@ +// Copyright 2020-2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_core::common::OrderedSet; +use identity_core::common::Url; +use identity_did::DIDUrl; +use identity_document::service::Service; +use identity_document::service::ServiceBuilder; +use identity_document::service::ServiceEndpoint; +use serde::Deserialize; +use serde::Serialize; + +use crate::error::Result; +use crate::Error; +use crate::Error::LinkedVerifiablePresentationError; + +/// A service wrapper for a [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "Service", into = "Service")] +pub struct LinkedVerifiablePresentationService(Service); + +impl TryFrom for LinkedVerifiablePresentationService { + type Error = Error; + + fn try_from(service: Service) -> std::result::Result { + LinkedVerifiablePresentationService::check_structure(&service)?; + Ok(LinkedVerifiablePresentationService(service)) + } +} + +impl From for Service { + fn from(service: LinkedVerifiablePresentationService) -> Self { + service.0 + } +} + +impl LinkedVerifiablePresentationService { + pub(crate) fn linked_verifiable_presentation_service_type() -> &'static str { + "LinkedVerifiablePresentation" + } + + /// Constructs a new `LinkedVerifiablePresentationService` that wraps a spec compliant + /// [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint). + pub fn new( + did_url: DIDUrl, + verifiable_presentation_urls: impl Into>, + properties: Object, + ) -> Result { + let verifiable_presentation_urls: OrderedSet = verifiable_presentation_urls.into(); + let builder: ServiceBuilder = Service::builder(properties) + .id(did_url) + .type_(Self::linked_verifiable_presentation_service_type()); + if verifiable_presentation_urls.len() == 1 { + let vp_url = verifiable_presentation_urls + .into_iter() + .next() + .expect("element 0 exists"); + let service = builder + .service_endpoint(vp_url) + .build() + .map_err(|err| LinkedVerifiablePresentationError(Box::new(err)))?; + Ok(Self(service)) + } else { + let service = builder + .service_endpoint(ServiceEndpoint::Set(verifiable_presentation_urls)) + .build() + .map_err(|err| LinkedVerifiablePresentationError(Box::new(err)))?; + Ok(Self(service)) + } + } + + /// Checks the semantic structure of a Linked Verifiable Presentation Service. + /// + /// Note: `{"type": ["LinkedVerifiablePresentation"]}` might be serialized the same way as `{"type": + /// "LinkedVerifiablePresentation"}` which passes the semantic check. + pub fn check_structure(service: &Service) -> Result<()> { + if service.type_().len() != 1 { + return Err(LinkedVerifiablePresentationError("invalid service type".into())); + } + + let service_type = service + .type_() + .get(0) + .ok_or_else(|| LinkedVerifiablePresentationError("missing service type".into()))?; + + if service_type != Self::linked_verifiable_presentation_service_type() { + return Err(LinkedVerifiablePresentationError( + format!( + "expected `{}` service type", + Self::linked_verifiable_presentation_service_type() + ) + .into(), + )); + } + + match service.service_endpoint() { + ServiceEndpoint::One(_) => Ok(()), + ServiceEndpoint::Set(_) => Ok(()), + ServiceEndpoint::Map(_) => Err(LinkedVerifiablePresentationError( + "service endpoints must be either a string or a set".into(), + )), + } + } + + /// Returns the Verifiable Presentations contained in the Linked Verifiable Presentation Service. + pub fn verifiable_presentation_urls(&self) -> &[Url] { + match self.0.service_endpoint() { + ServiceEndpoint::One(endpoint) => std::slice::from_ref(endpoint), + ServiceEndpoint::Set(endpoints) => endpoints.as_slice(), + ServiceEndpoint::Map(_) => { + unreachable!("the service endpoint is never a map per the `LinkedVerifiablePresentationService` type invariant") + } + } + } + + /// Returns a reference to the `Service` id. + pub fn id(&self) -> &DIDUrl { + self.0.id() + } +} + +#[cfg(test)] +mod tests { + use crate::credential::linked_verifiable_presentation_service::LinkedVerifiablePresentationService; + use identity_core::common::Object; + use identity_core::common::OrderedSet; + use identity_core::common::Url; + use identity_core::convert::FromJson; + use identity_did::DIDUrl; + use identity_document::service::Service; + use serde_json::json; + + #[test] + fn test_create_service_single_vp() { + let mut linked_vps: OrderedSet = OrderedSet::new(); + linked_vps.append(Url::parse("https://foo.example-1.com").unwrap()); + + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::new( + DIDUrl::parse("did:example:123#foo").unwrap(), + linked_vps, + Object::new(), + ) + .unwrap(); + + let service_from_json: Service = Service::from_json_value(json!({ + "id": "did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": "https://foo.example-1.com" + })) + .unwrap(); + assert_eq!(Service::from(service), service_from_json); + } + + #[test] + fn test_create_service_multiple_vps() { + let url_1 = "https://foo.example-1.com"; + let url_2 = "https://bar.example-2.com"; + let mut linked_vps = OrderedSet::new(); + linked_vps.append(Url::parse(url_1).unwrap()); + linked_vps.append(Url::parse(url_2).unwrap()); + + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::new( + DIDUrl::parse("did:example:123#foo").unwrap(), + linked_vps, + Object::new(), + ) + .unwrap(); + + let service_from_json: Service = Service::from_json_value(json!({ + "id":"did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": [url_1, url_2] + })) + .unwrap(); + assert_eq!(Service::from(service), service_from_json); + } + + #[test] + fn test_valid_single_vp() { + let service: Service = Service::from_json_value(json!({ + "id": "did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": "https://foo.example-1.com" + })) + .unwrap(); + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::try_from(service).unwrap(); + let linked_vps: Vec = vec![Url::parse("https://foo.example-1.com").unwrap()]; + assert_eq!(service.verifiable_presentation_urls(), linked_vps); + } + + #[test] + fn test_valid_multiple_vps() { + let service: Service = Service::from_json_value(json!({ + "id": "did:example:123#foo", + "type": "LinkedVerifiablePresentation", + "serviceEndpoint": ["https://foo.example-1.com", "https://foo.example-2.com"] + })) + .unwrap(); + let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::try_from(service).unwrap(); + let linked_vps: Vec = vec![ + Url::parse("https://foo.example-1.com").unwrap(), + Url::parse("https://foo.example-2.com").unwrap(), + ]; + assert_eq!(service.verifiable_presentation_urls(), linked_vps); + } +} diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 72f3b5d7a8..07b15f4eba 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -17,6 +17,7 @@ mod jws; mod jwt; mod jwt_serialization; mod linked_domain_service; +mod linked_verifiable_presentation_service; mod policy; mod proof; mod refresh; @@ -37,6 +38,7 @@ pub use self::jwp_credential_options::JwpCredentialOptions; pub use self::jws::Jws; pub use self::jwt::Jwt; pub use self::linked_domain_service::LinkedDomainService; +pub use self::linked_verifiable_presentation_service::LinkedVerifiablePresentationService; pub use self::policy::Policy; pub use self::proof::Proof; pub use self::refresh::RefreshService; diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 468370e460..1c814c3899 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -37,6 +37,9 @@ pub enum Error { /// Caused when constructing an invalid `LinkedDomainService` or `DomainLinkageConfiguration`. #[error("domain linkage error: {0}")] DomainLinkageError(#[source] Box), + /// Caused when constructing an invalid `LinkedVerifiablePresentationService`. + #[error("linked verifiable presentation error: {0}")] + LinkedVerifiablePresentationError(#[source] Box), /// Caused when attempting to encode a `Credential` containing multiple subjects as a JWT. #[error("could not create JWT claim set from verifiable credential: more than one subject")] MoreThanOneSubjectInJwt,