diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 81d4124b69..cd49b97317 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -222,9 +222,9 @@ publishing to the Tangle. **Kind**: global class * [Account](#Account) - * [.attachMethodRelationships(options)](#Account+attachMethodRelationships) ⇒ Promise.<void> + * [.createService(options)](#Account+createService) ⇒ Promise.<void> * [.createMethod(options)](#Account+createMethod) ⇒ Promise.<void> - * [.detachMethodRelationships(options)](#Account+detachMethodRelationships) ⇒ Promise.<void> + * [.attachMethodRelationships(options)](#Account+attachMethodRelationships) ⇒ Promise.<void> * [.did()](#Account+did) ⇒ [DID](#DID) * [.autopublish()](#Account+autopublish) ⇒ boolean * [.autosave()](#Account+autosave) ⇒ [AutoSave](#AutoSave) @@ -242,25 +242,22 @@ publishing to the Tangle. * [.unrevokeCredentials(fragment, credentialIndices)](#Account+unrevokeCredentials) ⇒ Promise.<void> * [.encryptData(plaintext, associated_data, encryption_algorithm, cek_algorithm, public_key)](#Account+encryptData) ⇒ [Promise.<EncryptedData>](#EncryptedData) * [.decryptData(data, encryption_algorithm, cek_algorithm, fragment)](#Account+decryptData) ⇒ Promise.<Uint8Array> - * [.setAlsoKnownAs(options)](#Account+setAlsoKnownAs) ⇒ Promise.<void> - * [.deleteMethod(options)](#Account+deleteMethod) ⇒ Promise.<void> + * [.detachMethodRelationships(options)](#Account+detachMethodRelationships) ⇒ Promise.<void> * [.deleteService(options)](#Account+deleteService) ⇒ Promise.<void> + * [.setAlsoKnownAs(options)](#Account+setAlsoKnownAs) ⇒ Promise.<void> * [.setController(options)](#Account+setController) ⇒ Promise.<void> - * [.createService(options)](#Account+createService) ⇒ Promise.<void> - - + * [.deleteMethod(options)](#Account+deleteMethod) ⇒ Promise.<void> -### account.attachMethodRelationships(options) ⇒ Promise.<void> -Attach one or more verification relationships to a method. + -Note: the method must exist and be in the set of verification methods; -it cannot be an embedded method. +### account.createService(options) ⇒ Promise.<void> +Adds a new Service to the DID Document. **Kind**: instance method of [Account](#Account) | Param | Type | | --- | --- | -| options | AttachMethodRelationshipOptions | +| options | CreateServiceOptions | @@ -273,16 +270,19 @@ Adds a new verification method to the DID document. | --- | --- | | options | CreateMethodOptions | - + -### account.detachMethodRelationships(options) ⇒ Promise.<void> -Detaches the given relationship from the given method, if the method exists. +### account.attachMethodRelationships(options) ⇒ Promise.<void> +Attach one or more verification relationships to a method. + +Note: the method must exist and be in the set of verification methods; +it cannot be an embedded method. **Kind**: instance method of [Account](#Account) | Param | Type | | --- | --- | -| options | DetachMethodRelationshipOptions | +| options | AttachMethodRelationshipOptions | @@ -475,38 +475,38 @@ Returns the decrypted text. | cek_algorithm | [CekAlgorithm](#CekAlgorithm) | | fragment | string | - + -### account.setAlsoKnownAs(options) ⇒ Promise.<void> -Sets the `alsoKnownAs` property in the DID document. +### account.detachMethodRelationships(options) ⇒ Promise.<void> +Detaches the given relationship from the given method, if the method exists. **Kind**: instance method of [Account](#Account) | Param | Type | | --- | --- | -| options | SetAlsoKnownAsOptions | +| options | DetachMethodRelationshipOptions | - + -### account.deleteMethod(options) ⇒ Promise.<void> -Deletes a verification method if the method exists. +### account.deleteService(options) ⇒ Promise.<void> +Deletes a Service if it exists. **Kind**: instance method of [Account](#Account) | Param | Type | | --- | --- | -| options | DeleteMethodOptions | +| options | DeleteServiceOptions | - + -### account.deleteService(options) ⇒ Promise.<void> -Deletes a Service if it exists. +### account.setAlsoKnownAs(options) ⇒ Promise.<void> +Sets the `alsoKnownAs` property in the DID document. **Kind**: instance method of [Account](#Account) | Param | Type | | --- | --- | -| options | DeleteServiceOptions | +| options | SetAlsoKnownAsOptions | @@ -519,16 +519,16 @@ Sets the controllers of the DID document. | --- | --- | | options | SetControllerOptions | - + -### account.createService(options) ⇒ Promise.<void> -Adds a new Service to the DID Document. +### account.deleteMethod(options) ⇒ Promise.<void> +Deletes a verification method if the method exists. **Kind**: instance method of [Account](#Account) | Param | Type | | --- | --- | -| options | CreateServiceOptions | +| options | DeleteMethodOptions | @@ -1176,6 +1176,7 @@ Deserializes a `CredentialValidationOptions` from a JSON object. * [.verifySignature(credential, trusted_issuers, options)](#CredentialValidator.verifySignature) * [.check_subject_holder_relationship(credential, holder_url, relationship)](#CredentialValidator.check_subject_holder_relationship) * [.checkStatus(credential, trustedIssuers, statusCheck)](#CredentialValidator.checkStatus) + * [.extractIssuer(credential)](#CredentialValidator.extractIssuer) ⇒ [DID](#DID) @@ -1269,7 +1270,7 @@ to verify the credential's signature will be made and an error is returned upon | Param | Type | | --- | --- | | credential | [Credential](#Credential) | -| trusted_issuers | [Array.<Document>](#Document) \| [Array.<ResolvedDocument>](#ResolvedDocument) | +| trusted_issuers | Array.<(Document\|ResolvedDocument)> | | options | [VerifierOptions](#VerifierOptions) | @@ -1298,9 +1299,24 @@ Only supports `BitmapRevocation2022`. | Param | Type | | --- | --- | | credential | [Credential](#Credential) | -| trustedIssuers | [Array.<Document>](#Document) \| [Array.<ResolvedDocument>](#ResolvedDocument) | +| trustedIssuers | Array.<(Document\|ResolvedDocument)> | | statusCheck | number | + + +### CredentialValidator.extractIssuer(credential) ⇒ [DID](#DID) +Utility for extracting the issuer field of a `Credential` as a DID. + +### Errors + +Fails if the issuer field is not a valid DID. + +**Kind**: static method of [CredentialValidator](#CredentialValidator) + +| Param | Type | +| --- | --- | +| credential | [Credential](#Credential) | + ## DID @@ -1755,6 +1771,7 @@ Deserializes a `DiffMessage` from a JSON object. * [.service()](#Document+service) ⇒ [Array.<Service>](#Service) * [.insertService(service)](#Document+insertService) ⇒ boolean * [.removeService(did)](#Document+removeService) ⇒ boolean + * [.resolveService(query)](#Document+resolveService) ⇒ [Service](#Service) \| undefined * [.methods()](#Document+methods) ⇒ [Array.<VerificationMethod>](#VerificationMethod) * [.insertMethod(method, scope)](#Document+insertMethod) * [.removeMethod(did)](#Document+removeMethod) @@ -1782,8 +1799,8 @@ Deserializes a `DiffMessage` from a JSON object. * [.metadataPreviousMessageId()](#Document+metadataPreviousMessageId) ⇒ string * [.setMetadataPreviousMessageId(value)](#Document+setMetadataPreviousMessageId) * [.proof()](#Document+proof) ⇒ [Proof](#Proof) \| undefined - * [.revokeCredentials(fragment, credentialIndices)](#Document+revokeCredentials) - * [.unrevokeCredentials(fragment, credentialIndices)](#Document+unrevokeCredentials) + * [.revokeCredentials(serviceQuery, credentialIndices)](#Document+revokeCredentials) + * [.unrevokeCredentials(serviceQuery, credentialIndices)](#Document+unrevokeCredentials) * [.toJSON()](#Document+toJSON) ⇒ any * [.clone()](#Document+clone) ⇒ [Document](#Document) * _static_ @@ -1916,6 +1933,18 @@ Returns `true` if a service was removed. | --- | --- | | did | [DIDUrl](#DIDUrl) | + + +### document.resolveService(query) ⇒ [Service](#Service) \| undefined +Returns the first [Service](#Service) with an `id` property matching the provided `query`, +if present. + +**Kind**: instance method of [Document](#Document) + +| Param | Type | +| --- | --- | +| query | [DIDUrl](#DIDUrl) \| string | + ### document.methods() ⇒ [Array.<VerificationMethod>](#VerificationMethod) @@ -2252,28 +2281,28 @@ Returns a copy of the proof. **Kind**: instance method of [Document](#Document) -### document.revokeCredentials(fragment, credentialIndices) -If the document has a `RevocationBitmap` service identified by `fragment`, +### document.revokeCredentials(serviceQuery, credentialIndices) +If the document has a `RevocationBitmap` service identified by `serviceQuery`, revoke all credentials with a revocationBitmapIndex in `credentialIndices`. **Kind**: instance method of [Document](#Document) | Param | Type | | --- | --- | -| fragment | string | +| serviceQuery | [DIDUrl](#DIDUrl) \| string | | credentialIndices | number \| Array.<number> | -### document.unrevokeCredentials(fragment, credentialIndices) -If the document has a `RevocationBitmap` service identified by `fragment`, +### document.unrevokeCredentials(serviceQuery, credentialIndices) +If the document has a `RevocationBitmap` service identified by `serviceQuery`, unrevoke all credentials with a revocationBitmapIndex in `credentialIndices`. **Kind**: instance method of [Document](#Document) | Param | Type | | --- | --- | -| fragment | string | +| serviceQuery | [DIDUrl](#DIDUrl) \| string | | credentialIndices | number \| Array.<number> | @@ -3591,6 +3620,7 @@ Deserializes a `PresentationValidationOptions` from a JSON object. * [.validate(presentation, holder, issuers, options, fail_fast)](#PresentationValidator.validate) * [.verifyPresentationSignature(presentation, holder, options)](#PresentationValidator.verifyPresentationSignature) * [.checkStructure(presentation)](#PresentationValidator.checkStructure) + * [.extractHolder(presentation)](#PresentationValidator.extractHolder) ⇒ [DID](#DID) @@ -3628,7 +3658,7 @@ An error is returned whenever a validated condition is not satisfied. | --- | --- | | presentation | [Presentation](#Presentation) | | holder | [Document](#Document) \| [ResolvedDocument](#ResolvedDocument) | -| issuers | [Array.<Document>](#Document) \| [Array.<ResolvedDocument>](#ResolvedDocument) | +| issuers | Array.<(Document\|ResolvedDocument)> | | options | [PresentationValidationOptions](#PresentationValidationOptions) | | fail_fast | number | @@ -3663,6 +3693,21 @@ Validates the semantic structure of the `Presentation`. | --- | --- | | presentation | [Presentation](#Presentation) | + + +### PresentationValidator.extractHolder(presentation) ⇒ [DID](#DID) +Utility for extracting the holder field of a `Presentation` as a DID. + +### Errors + +Fails if the holder field is missing or not a valid DID. + +**Kind**: static method of [PresentationValidator](#PresentationValidator) + +| Param | Type | +| --- | --- | +| presentation | [Presentation](#Presentation) | + ## Proof @@ -4185,8 +4230,8 @@ according to the `fail_fast` parameter. | presentation | [Presentation](#Presentation) | | options | [PresentationValidationOptions](#PresentationValidationOptions) | | fail_fast | number | -| holder | [ResolvedDocument](#ResolvedDocument) \| undefined | -| issuers | [Array.<ResolvedDocument>](#ResolvedDocument) \| undefined | +| holder | [Document](#Document) \| [ResolvedDocument](#ResolvedDocument) \| undefined | +| issuers | Array.<(Document\|ResolvedDocument)> \| undefined | diff --git a/bindings/wasm/src/credential/credential_validator.rs b/bindings/wasm/src/credential/credential_validator.rs index 8b26671556..2995e6539a 100644 --- a/bindings/wasm/src/credential/credential_validator.rs +++ b/bindings/wasm/src/credential/credential_validator.rs @@ -1,19 +1,21 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_iota::client::CredentialValidator; use identity_iota::client::ResolvedIotaDocument; -use identity_iota::client::StatusCheck; -use identity_iota::client::ValidationError; use identity_iota::core::Url; +use identity_iota::credential::CredentialValidator; +use identity_iota::credential::StatusCheck; +use identity_iota::credential::ValidationError; +use identity_iota::iota_core::IotaDID; use identity_iota::iota_core::IotaDocument; use wasm_bindgen::prelude::*; use crate::common::WasmTimestamp; use crate::credential::validation_options::WasmFailFast; use crate::credential::validation_options::WasmStatusCheck; -use crate::did::ArrayDocumentOrArrayResolvedDocument; +use crate::did::ArrayDocumentOrResolvedDocument; use crate::did::DocumentOrResolvedDocument; +use crate::did::WasmDID; use crate::did::WasmVerifierOptions; use crate::error::Result; use crate::error::WasmResult; @@ -59,7 +61,7 @@ impl WasmCredentialValidator { fail_fast: WasmFailFast, ) -> Result<()> { let issuer_doc: ResolvedIotaDocument = issuer.into_serde().wasm_result()?; - CredentialValidator::validate(&credential.0, &issuer_doc, &options.0, fail_fast.into()).wasm_result() + CredentialValidator::validate(&credential.0, &issuer_doc.document, &options.0, fail_fast.into()).wasm_result() } /// Validates the semantic structure of the `Credential`. @@ -98,11 +100,11 @@ impl WasmCredentialValidator { #[wasm_bindgen(js_name = verifySignature)] pub fn verify_signature( credential: &WasmCredential, - trusted_issuers: &ArrayDocumentOrArrayResolvedDocument, + trusted_issuers: &ArrayDocumentOrResolvedDocument, options: &WasmVerifierOptions, ) -> Result<()> { - let trusted_issuers: Vec = trusted_issuers.into_serde().wasm_result()?; - CredentialValidator::verify_signature(&credential.0, trusted_issuers.as_slice(), &options.0).wasm_result() + let issuers: Vec = trusted_issuers.into_serde().wasm_result()?; + CredentialValidator::verify_signature(&credential.0, &issuers, &options.0).wasm_result() } /// Validate that the relationship between the `holder` and the credential subjects is in accordance with @@ -123,11 +125,22 @@ impl WasmCredentialValidator { #[allow(non_snake_case)] pub fn check_status( credential: &WasmCredential, - trustedIssuers: &ArrayDocumentOrArrayResolvedDocument, + trustedIssuers: &ArrayDocumentOrResolvedDocument, statusCheck: WasmStatusCheck, ) -> Result<()> { let trusted_issuers: Vec = trustedIssuers.into_serde().wasm_result()?; let status_check: StatusCheck = StatusCheck::from(statusCheck); - CredentialValidator::check_status(&credential.0, trusted_issuers.as_slice(), status_check).wasm_result() + CredentialValidator::check_status(&credential.0, &trusted_issuers, status_check).wasm_result() + } + + /// Utility for extracting the issuer field of a `Credential` as a DID. + /// + /// ### Errors + /// + /// Fails if the issuer field is not a valid DID. + #[wasm_bindgen(js_name = extractIssuer)] + pub fn extract_issuer(credential: &WasmCredential) -> Result { + let did: IotaDID = CredentialValidator::extract_issuer(&credential.0).wasm_result()?; + Ok(WasmDID::from(did)) } } diff --git a/bindings/wasm/src/credential/presentation_validator.rs b/bindings/wasm/src/credential/presentation_validator.rs index 61ca4e556b..df4cf58678 100644 --- a/bindings/wasm/src/credential/presentation_validator.rs +++ b/bindings/wasm/src/credential/presentation_validator.rs @@ -1,17 +1,20 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_iota::credential::PresentationValidator; +use identity_iota::iota_core::IotaDID; +use identity_iota::iota_core::IotaDocument; +use wasm_bindgen::prelude::*; + use crate::credential::WasmFailFast; use crate::credential::WasmPresentation; use crate::credential::WasmPresentationValidationOptions; -use crate::did::ArrayDocumentOrArrayResolvedDocument; +use crate::did::ArrayDocumentOrResolvedDocument; use crate::did::DocumentOrResolvedDocument; +use crate::did::WasmDID; use crate::did::WasmVerifierOptions; use crate::error::Result; use crate::error::WasmResult; -use identity_iota::client::PresentationValidator; -use identity_iota::client::ResolvedIotaDocument; -use wasm_bindgen::prelude::*; #[wasm_bindgen(js_name = PresentationValidator, inspectable)] pub struct WasmPresentationValidator; @@ -48,12 +51,12 @@ impl WasmPresentationValidator { pub fn validate( presentation: &WasmPresentation, holder: &DocumentOrResolvedDocument, - issuers: &ArrayDocumentOrArrayResolvedDocument, + issuers: &ArrayDocumentOrResolvedDocument, options: &WasmPresentationValidationOptions, fail_fast: WasmFailFast, ) -> Result<()> { - let holder: ResolvedIotaDocument = holder.into_serde().wasm_result()?; - let issuers: Vec = issuers.into_serde().wasm_result()?; + let holder: IotaDocument = holder.into_serde().wasm_result()?; + let issuers: Vec = issuers.into_serde().wasm_result()?; PresentationValidator::validate(&presentation.0, &holder, &issuers, &options.0, fail_fast.into()).wasm_result() } @@ -71,7 +74,7 @@ impl WasmPresentationValidator { holder: &DocumentOrResolvedDocument, options: &WasmVerifierOptions, ) -> Result<()> { - let holder: ResolvedIotaDocument = holder.into_serde().wasm_result()?; + let holder: IotaDocument = holder.into_serde().wasm_result()?; PresentationValidator::verify_presentation_signature(&presentation.0, &holder, &options.0).wasm_result() } @@ -80,4 +83,15 @@ impl WasmPresentationValidator { pub fn check_structure(presentation: &WasmPresentation) -> Result<()> { PresentationValidator::check_structure(&presentation.0).wasm_result() } + + /// Utility for extracting the holder field of a `Presentation` as a DID. + /// + /// ### Errors + /// + /// Fails if the holder field is missing or not a valid DID. + #[wasm_bindgen(js_name = extractHolder)] + pub fn extract_holder(presentation: &WasmPresentation) -> Result { + let did: IotaDID = PresentationValidator::extract_holder(&presentation.0).wasm_result()?; + Ok(WasmDID::from(did)) + } } diff --git a/bindings/wasm/src/credential/validation_options.rs b/bindings/wasm/src/credential/validation_options.rs index 7b0958c367..be2f9174ec 100644 --- a/bindings/wasm/src/credential/validation_options.rs +++ b/bindings/wasm/src/credential/validation_options.rs @@ -1,11 +1,11 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_iota::client::CredentialValidationOptions; -use identity_iota::client::FailFast; -use identity_iota::client::PresentationValidationOptions; -use identity_iota::client::StatusCheck; -use identity_iota::client::SubjectHolderRelationship; +use identity_iota::credential::CredentialValidationOptions; +use identity_iota::credential::FailFast; +use identity_iota::credential::PresentationValidationOptions; +use identity_iota::credential::StatusCheck; +use identity_iota::credential::SubjectHolderRelationship; use serde_repr::Deserialize_repr; use serde_repr::Serialize_repr; use wasm_bindgen::prelude::*; diff --git a/bindings/wasm/src/did/mod.rs b/bindings/wasm/src/did/mod.rs index 509133e3c8..37910196ea 100644 --- a/bindings/wasm/src/did/mod.rs +++ b/bindings/wasm/src/did/mod.rs @@ -13,7 +13,7 @@ pub use self::wasm_method_scope::OptionMethodScope; pub use self::wasm_method_scope::RefMethodScope; pub use self::wasm_method_scope::WasmMethodScope; pub use self::wasm_method_type::WasmMethodType; -pub use self::wasm_resolved_document::ArrayDocumentOrArrayResolvedDocument; +pub use self::wasm_resolved_document::ArrayDocumentOrResolvedDocument; pub use self::wasm_resolved_document::ArrayResolvedDocument; pub use self::wasm_resolved_document::DocumentOrResolvedDocument; pub use self::wasm_resolved_document::PromiseArrayResolvedDocument; diff --git a/bindings/wasm/src/did/wasm_document.rs b/bindings/wasm/src/did/wasm_document.rs index 7aee10ed4a..5f95d3e10e 100644 --- a/bindings/wasm/src/did/wasm_document.rs +++ b/bindings/wasm/src/did/wasm_document.rs @@ -13,15 +13,15 @@ use identity_iota::credential::Presentation; use identity_iota::crypto::PrivateKey; use identity_iota::crypto::ProofOptions; use identity_iota::did::verifiable::VerifiableProperties; +use identity_iota::did::Document; use identity_iota::did::MethodScope; -use identity_iota::did::DID; use identity_iota::iota_core::IotaDID; -use identity_iota::iota_core::IotaDIDUrl; use identity_iota::iota_core::IotaDocument; use identity_iota::iota_core::IotaVerificationMethod; use identity_iota::iota_core::MessageId; use identity_iota::iota_core::NetworkName; use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; use crate::account::wasm_account::UOneOrManyNumber; use crate::common::MapStringAny; @@ -44,7 +44,6 @@ use crate::did::WasmVerificationMethod; use crate::did::WasmVerifierOptions; use crate::error::Result; use crate::error::WasmResult; -use wasm_bindgen::JsCast; // ============================================================================= // ============================================================================= @@ -229,6 +228,14 @@ impl WasmDocument { self.0.remove_service(&did.0) } + /// Returns the first {@link Service} with an `id` property matching the provided `query`, + /// if present. + #[wasm_bindgen(js_name = resolveService)] + pub fn resolve_service(&self, query: &UDIDUrlQuery) -> Option { + let service_query: String = query.into_serde().ok()?; + self.0.resolve_service(&service_query).cloned().map(WasmService::from) + } + // =========================================================================== // Verification Methods // =========================================================================== @@ -631,33 +638,35 @@ impl WasmDocument { self.0.proof.clone().map(WasmProof::from) } - /// If the document has a `RevocationBitmap` service identified by `fragment`, + /// If the document has a `RevocationBitmap` service identified by `serviceQuery`, /// revoke all credentials with a revocationBitmapIndex in `credentialIndices`. #[wasm_bindgen(js_name = revokeCredentials)] #[allow(non_snake_case)] - pub fn revoke_credentials(&mut self, fragment: &str, credentialIndices: UOneOrManyNumber) -> Result<()> { + pub fn revoke_credentials(&mut self, serviceQuery: &UDIDUrlQuery, credentialIndices: UOneOrManyNumber) -> Result<()> { + let query: String = serviceQuery.into_serde().wasm_result()?; let credentials_indices: OneOrMany = credentialIndices.into_serde().wasm_result()?; - let mut service_id: IotaDIDUrl = self.0.id().to_url(); - service_id.set_fragment(Some(fragment)).wasm_result()?; self .0 - .revoke_credentials(&service_id, credentials_indices.as_slice()) + .revoke_credentials(&query, credentials_indices.as_slice()) .wasm_result() } - /// If the document has a `RevocationBitmap` service identified by `fragment`, + /// If the document has a `RevocationBitmap` service identified by `serviceQuery`, /// unrevoke all credentials with a revocationBitmapIndex in `credentialIndices`. #[wasm_bindgen(js_name = unrevokeCredentials)] #[allow(non_snake_case)] - pub fn unrevoke_credentials(&mut self, fragment: &str, credentialIndices: UOneOrManyNumber) -> Result<()> { + pub fn unrevoke_credentials( + &mut self, + serviceQuery: &UDIDUrlQuery, + credentialIndices: UOneOrManyNumber, + ) -> Result<()> { + let query: String = serviceQuery.into_serde().wasm_result()?; let credentials_indices: OneOrMany = credentialIndices.into_serde().wasm_result()?; - let mut service_id: IotaDIDUrl = self.0.id().to_url(); - service_id.set_fragment(Some(fragment)).wasm_result()?; self .0 - .unrevoke_credentials(&service_id, credentials_indices.as_slice()) + .unrevoke_credentials(&query, credentials_indices.as_slice()) .wasm_result() } @@ -691,6 +700,7 @@ impl From for IotaDocument { wasm_document.0 } } + /// Duck-typed union to pass either a string or WasmDIDUrl as a parameter. #[wasm_bindgen] extern "C" { diff --git a/bindings/wasm/src/did/wasm_resolved_document.rs b/bindings/wasm/src/did/wasm_resolved_document.rs index c90768b886..e7b4e3208a 100644 --- a/bindings/wasm/src/did/wasm_resolved_document.rs +++ b/bindings/wasm/src/did/wasm_resolved_document.rs @@ -35,8 +35,8 @@ extern "C" { #[wasm_bindgen(typescript_type = "Document | ResolvedDocument")] pub type DocumentOrResolvedDocument; - #[wasm_bindgen(typescript_type = "Array | Array")] - pub type ArrayDocumentOrArrayResolvedDocument; + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayDocumentOrResolvedDocument; } #[wasm_bindgen(js_class = ResolvedDocument)] diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 296b42b315..ed0cbf80cb 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -97,7 +97,7 @@ impl_wasm_error_from!( identity_iota::did::Error, identity_iota::did::DIDError, identity_iota::iota_core::Error, - identity_iota::client::ValidationError + identity_iota::credential::ValidationError ); // Similar to `impl_wasm_error_from`, but uses the types name instead of requiring/calling Into &'static str @@ -177,8 +177,8 @@ impl From for WasmError<'_> { } } -impl From for WasmError<'_> { - fn from(error: identity_iota::client::CompoundCredentialValidationError) -> Self { +impl From for WasmError<'_> { + fn from(error: identity_iota::credential::CompoundCredentialValidationError) -> Self { Self { name: Cow::Borrowed("CompoundCredentialValidationError"), message: Cow::Owned(error.to_string()), @@ -186,8 +186,8 @@ impl From for WasmErro } } -impl From for WasmError<'_> { - fn from(error: identity_iota::client::CompoundPresentationValidationError) -> Self { +impl From for WasmError<'_> { + fn from(error: identity_iota::credential::CompoundPresentationValidationError) -> Self { Self { name: Cow::Borrowed("CompoundPresentationValidationError"), message: Cow::Owned(error.to_string()), diff --git a/bindings/wasm/src/tangle/resolver.rs b/bindings/wasm/src/tangle/resolver.rs index d2c6b83ba8..531f6ce550 100644 --- a/bindings/wasm/src/tangle/resolver.rs +++ b/bindings/wasm/src/tangle/resolver.rs @@ -10,8 +10,10 @@ use identity_iota::client::ClientBuilder; use identity_iota::client::ResolvedIotaDocument; use identity_iota::client::Resolver; use identity_iota::client::ResolverBuilder; -use identity_iota::core::Url; +use identity_iota::credential::PresentationValidator; +use identity_iota::credential::ValidatorDocument; use identity_iota::iota_core::IotaDID; +use identity_iota::iota_core::IotaDocument; use identity_iota::iota_core::NetworkName; use js_sys::Promise; use wasm_bindgen::prelude::*; @@ -27,7 +29,8 @@ use crate::credential::WasmCredential; use crate::credential::WasmFailFast; use crate::credential::WasmPresentation; use crate::credential::WasmPresentationValidationOptions; -use crate::did::ArrayResolvedDocument; +use crate::did::ArrayDocumentOrResolvedDocument; +use crate::did::DocumentOrResolvedDocument; use crate::did::PromiseArrayResolvedDocument; use crate::did::PromiseResolvedDocument; use crate::did::UWasmDID; @@ -45,10 +48,6 @@ extern "C" { // Workaround for Typescript type annotations on async function returns. #[wasm_bindgen(typescript_type = "Promise")] pub type PromiseResolver; - - // Workaround for lack of &Option/Option<&Type> support. - #[wasm_bindgen(typescript_type = "ResolvedDocument")] - pub type RefResolvedDocument; } #[wasm_bindgen(js_class = Resolver)] @@ -215,14 +214,7 @@ impl WasmResolver { pub fn resolve_presentation_holder(&self, presentation: &WasmPresentation) -> Result { // TODO: reimplemented function to avoid cloning the entire presentation. // Would be solved with Rc internal representation, pending memory leak discussions. - let holder_url: &Url = presentation - .0 - .holder - .as_ref() - .ok_or(identity_iota::client::ValidationError::MissingPresentationHolder) - .map_err(identity_iota::client::Error::from) - .wasm_result()?; - let holder: IotaDID = IotaDID::parse(holder_url.as_str()).wasm_result()?; + let holder: IotaDID = PresentationValidator::extract_holder(&presentation.0).wasm_result()?; let resolver: Rc>> = Rc::clone(&self.0); let promise: Promise = future_to_promise(async move { @@ -260,25 +252,29 @@ impl WasmResolver { presentation: &WasmPresentation, options: &WasmPresentationValidationOptions, fail_fast: WasmFailFast, - holder: Option, - issuers: Option, + holder: Option, + issuers: Option, ) -> Result { // TODO: reimplemented function to avoid cloning the entire presentation and validation options. // Would be solved with Rc internal representation, pending memory leak discussions. - let holder: Option = holder.map(|js| js.into_serde().wasm_result()).transpose()?; - let issuers: Option> = issuers.map(|js| js.into_serde().wasm_result()).transpose()?; + let holder: Option = holder.map(|js| js.into_serde().wasm_result()).transpose()?; + let issuers: Option> = issuers.map(|js| js.into_serde().wasm_result()).transpose()?; + let resolver: Rc>> = Rc::clone(&self.0); let presentation: WasmPresentation = presentation.clone(); let options: WasmPresentationValidationOptions = options.clone(); let promise: Promise = future_to_promise(async move { + let issuer_refs: Option> = issuers + .as_ref() + .map(|issuers| issuers.iter().map(ValidatorDocument::as_validator).collect()); resolver .verify_presentation( &presentation.0, &options.0, fail_fast.into(), - holder.as_ref(), - issuers.as_deref(), + holder.as_ref().map(ValidatorDocument::as_validator), + issuer_refs.as_deref(), ) .await .map(|_| JsValue::UNDEFINED) diff --git a/examples/account/create_vc.rs b/examples/account/create_vc.rs index 760e2c6c96..fdf432ae43 100644 --- a/examples/account/create_vc.rs +++ b/examples/account/create_vc.rs @@ -12,16 +12,15 @@ use identity_iota::account::AccountBuilder; use identity_iota::account::IdentitySetup; use identity_iota::account::MethodContent; use identity_iota::account::Result; - -use identity_iota::client::CredentialValidationOptions; -use identity_iota::client::CredentialValidator; -use identity_iota::client::FailFast; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::ToJson; use identity_iota::core::Url; use identity_iota::credential::Credential; use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::CredentialValidationOptions; +use identity_iota::credential::CredentialValidator; +use identity_iota::credential::FailFast; use identity_iota::credential::Subject; use identity_iota::crypto::ProofOptions; use identity_iota::did::DID; @@ -79,7 +78,7 @@ pub async fn create_vc() -> Result { // that the issuance date is not in the future and that the expiration date is not in the past: CredentialValidator::validate( &credential, - &issuer.document(), + issuer.document(), &CredentialValidationOptions::default(), FailFast::FirstError, ) diff --git a/examples/account/create_vp.rs b/examples/account/create_vp.rs index 091afef443..8534195661 100644 --- a/examples/account/create_vp.rs +++ b/examples/account/create_vp.rs @@ -10,6 +10,8 @@ use identity_iota::account::Account; use identity_iota::account::AccountBuilder; use identity_iota::account::IdentitySetup; use identity_iota::account::MethodContent; +use identity_iota::account::Result; +use identity_iota::client::Resolver; use identity_iota::core::json; use identity_iota::core::Duration; use identity_iota::core::FromJson; @@ -18,20 +20,16 @@ use identity_iota::core::ToJson; use identity_iota::core::Url; use identity_iota::credential::Credential; use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::CredentialValidationOptions; +use identity_iota::credential::FailFast; use identity_iota::credential::Presentation; use identity_iota::credential::PresentationBuilder; +use identity_iota::credential::PresentationValidationOptions; use identity_iota::credential::Subject; +use identity_iota::credential::SubjectHolderRelationship; use identity_iota::crypto::ProofOptions; use identity_iota::did::verifiable::VerifierOptions; -use identity_iota::account::Result; -use identity_iota::client::CredentialValidationOptions; -use identity_iota::client::FailFast; -use identity_iota::client::PresentationValidationOptions; - -use identity_iota::client::Resolver; -use identity_iota::client::SubjectHolderRelationship; - #[tokio::main] async fn main() -> Result<()> { // =========================================================================== diff --git a/examples/account/revoke_vc.rs b/examples/account/revoke_vc.rs index 4518a9a98b..3154c345ea 100644 --- a/examples/account/revoke_vc.rs +++ b/examples/account/revoke_vc.rs @@ -17,19 +17,20 @@ use identity_iota::account::AccountBuilder; use identity_iota::account::IdentitySetup; use identity_iota::account::MethodContent; use identity_iota::account::Result; -use identity_iota::client::CredentialValidationOptions; -use identity_iota::client::CredentialValidator; use identity_iota::client::ResolvedIotaDocument; use identity_iota::client::Resolver; -use identity_iota::client::ValidationError; use identity_iota::core::json; use identity_iota::core::FromJson; use identity_iota::core::Url; use identity_iota::credential::Credential; use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::CredentialValidationOptions; +use identity_iota::credential::CredentialValidator; +use identity_iota::credential::FailFast; use identity_iota::credential::RevocationBitmapStatus; use identity_iota::credential::Status; use identity_iota::credential::Subject; +use identity_iota::credential::ValidationError; use identity_iota::crypto::ProofOptions; use identity_iota::did::RevocationBitmap; use identity_iota::did::DID; @@ -97,9 +98,9 @@ async fn main() -> Result<()> { let validation_result = CredentialValidator::validate( &credential, - &issuer.document(), + issuer.document(), &CredentialValidationOptions::default(), - identity_iota::client::FailFast::FirstError, + FailFast::FirstError, ); // The credential wasn't revoked, so we expect the validation to succeed. @@ -117,9 +118,9 @@ async fn main() -> Result<()> { let validation_result = CredentialValidator::validate( &credential, - &issuer.document(), + issuer.document(), &CredentialValidationOptions::default(), - identity_iota::client::FailFast::FirstError, + FailFast::FirstError, ); // We expect validation to no longer succeed because the credential was revoked. @@ -146,9 +147,9 @@ async fn main() -> Result<()> { let resolved_issuer_doc: ResolvedIotaDocument = resolver.resolve_credential_issuer(&credential).await?; let validation_result = CredentialValidator::validate( &credential, - &resolved_issuer_doc, + &resolved_issuer_doc.document, &CredentialValidationOptions::default(), - identity_iota::client::FailFast::FirstError, + FailFast::FirstError, ); println!("VC validation result: {:?}", validation_result); diff --git a/identity_account_storage/src/storage/memstore.rs b/identity_account_storage/src/storage/memstore.rs index b404830e36..48303a220d 100644 --- a/identity_account_storage/src/storage/memstore.rs +++ b/identity_account_storage/src/storage/memstore.rs @@ -549,6 +549,7 @@ impl Default for MemStore { } #[cfg(test)] +#[cfg(feature = "storage-test-suite")] mod tests { use crate::storage::Storage; use crate::storage::StorageTestSuite; diff --git a/identity_account_storage/src/stronghold/tests.rs b/identity_account_storage/src/stronghold/tests.rs index 00025a2321..e4b47acdb5 100644 --- a/identity_account_storage/src/stronghold/tests.rs +++ b/identity_account_storage/src/stronghold/tests.rs @@ -1,6 +1,12 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use iota_stronghold::procedures; +use iota_stronghold::procedures::GenerateKey; +use iota_stronghold::Client; +use iota_stronghold::ClientVault; +use iota_stronghold::Location; + use identity_core::crypto::KeyPair; use identity_core::crypto::KeyType; use identity_core::crypto::PrivateKey; @@ -9,11 +15,6 @@ use identity_core::crypto::X25519; use identity_core::utils::Base::Base16Lower; use identity_core::utils::BaseEncoding; use identity_iota_core::did::IotaDID; -use iota_stronghold::procedures; -use iota_stronghold::procedures::GenerateKey; -use iota_stronghold::Client; -use iota_stronghold::ClientVault; -use iota_stronghold::Location; use crate::storage::stronghold::aead_decrypt; use crate::storage::stronghold::aead_encrypt; @@ -23,8 +24,6 @@ use crate::storage::stronghold::generate_private_key; use crate::storage::stronghold::insert_private_key; use crate::storage::stronghold::random_location; use crate::storage::stronghold::retrieve_public_key; -use crate::storage::Storage; -use crate::storage::StorageTestSuite; use crate::stronghold::test_util::random_did; use crate::stronghold::test_util::random_key_location; use crate::stronghold::test_util::random_string; @@ -190,74 +189,82 @@ async fn test_ecdhes_encryption() { assert_eq!(plaintext.to_vec(), data); } -async fn test_stronghold() -> impl Storage { - Stronghold::new(&random_temporary_path(), random_string(), Some(false)) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_did_create_with_private_key() { - StorageTestSuite::did_create_private_key_test(test_stronghold().await) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_did_create_generate_key() { - StorageTestSuite::did_create_generate_key_test(test_stronghold().await) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_key_generate() { - StorageTestSuite::key_generate_test(test_stronghold().await) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_key_delete() { - StorageTestSuite::key_delete_test(test_stronghold().await) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_did_list() { - StorageTestSuite::did_list_test(test_stronghold().await).await.unwrap() -} - -#[tokio::test] -async fn test_stronghold_key_insert() { - StorageTestSuite::key_insert_test(test_stronghold().await) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_key_sign_ed25519() { - StorageTestSuite::key_sign_ed25519_test(test_stronghold().await) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_key_value_store() { - StorageTestSuite::key_value_store_test(test_stronghold().await) - .await - .unwrap() -} - -#[tokio::test] -async fn test_stronghold_did_purge() { - StorageTestSuite::did_purge_test(test_stronghold().await).await.unwrap() -} - -#[tokio::test] -async fn test_stronghold_encryption() { - StorageTestSuite::encryption_test(test_stronghold().await, test_stronghold().await) - .await - .unwrap() +#[cfg(feature = "storage-test-suite")] +mod stronghold_storage_test_suite { + use crate::storage::Storage; + use crate::storage::StorageTestSuite; + + use super::*; + + async fn test_stronghold() -> impl Storage { + Stronghold::new(&random_temporary_path(), random_string(), Some(false)) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_did_create_with_private_key() { + StorageTestSuite::did_create_private_key_test(test_stronghold().await) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_did_create_generate_key() { + StorageTestSuite::did_create_generate_key_test(test_stronghold().await) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_key_generate() { + StorageTestSuite::key_generate_test(test_stronghold().await) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_key_delete() { + StorageTestSuite::key_delete_test(test_stronghold().await) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_did_list() { + StorageTestSuite::did_list_test(test_stronghold().await).await.unwrap() + } + + #[tokio::test] + async fn test_stronghold_key_insert() { + StorageTestSuite::key_insert_test(test_stronghold().await) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_key_sign_ed25519() { + StorageTestSuite::key_sign_ed25519_test(test_stronghold().await) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_key_value_store() { + StorageTestSuite::key_value_store_test(test_stronghold().await) + .await + .unwrap() + } + + #[tokio::test] + async fn test_stronghold_did_purge() { + StorageTestSuite::did_purge_test(test_stronghold().await).await.unwrap() + } + + #[tokio::test] + async fn test_stronghold_encryption() { + StorageTestSuite::encryption_test(test_stronghold().await, test_stronghold().await) + .await + .unwrap() + } } diff --git a/identity_core/src/crypto/proof/jcs_ed25519.rs b/identity_core/src/crypto/proof/jcs_ed25519.rs index b41d586c25..272fb39dac 100644 --- a/identity_core/src/crypto/proof/jcs_ed25519.rs +++ b/identity_core/src/crypto/proof/jcs_ed25519.rs @@ -56,7 +56,7 @@ where { fn verify(data: &X, signature: &ProofValue, public: &T::Public) -> Result<()> where - X: Serialize, + X: Serialize + ?Sized, { let signature: &str = signature .as_signature() diff --git a/identity_core/src/crypto/signature/core.rs b/identity_core/src/crypto/signature/core.rs index c5adbb2664..ea97561f99 100644 --- a/identity_core/src/crypto/signature/core.rs +++ b/identity_core/src/crypto/signature/core.rs @@ -73,17 +73,17 @@ pub trait Signer: Named { // ============================================================================= // ============================================================================= -/// A common interface for digital signature verification +/// A common interface for digital signature verification. pub trait Verifier: Named { /// Verifies the authenticity of `data` and `signature`. fn verify(data: &T, signature: &ProofValue, public: &Public) -> Result<()> where - T: Serialize; + T: Serialize + ?Sized; /// Extracts and verifies a proof [signature][`Proof`] from the given `data`. fn verify_signature(data: &T, public: &Public) -> Result<()> where - T: Serialize + GetSignature, + T: Serialize + GetSignature + ?Sized, { let signature: &Proof = data.signature().ok_or(Error::MissingSignature)?; @@ -93,7 +93,7 @@ pub trait Verifier: Named { signature.hide_value(); - Self::verify(&data, signature.value(), public)?; + Self::verify(data, signature.value(), public)?; signature.show_value(); diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 520fc407d3..4e386489c7 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -8,17 +8,22 @@ keywords = ["iota", "tangle", "identity"] license = "Apache-2.0" readme = "./README.md" repository = "https://github.com/iotaledger/identity.rs" +rust-version = "1.60" description = "An implementation of the Verifiable Credentials standard." [dependencies] +erased-serde = { version = "0.3.21", default-features = false, features = ["std"], optional = true } identity_core = { version = "=0.6.0", path = "../identity_core", default-features = false } identity_did = { version = "=0.6.0", path = "../identity_did", default-features = false } +itertools = { version = "0.10", default-features = false, features = ["use_std"], optional = true } lazy_static = { version = "1.4", default-features = false } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } +serde_repr = { version = "0.1", default-features = false, optional = true } strum = { version = "0.24.0", default-features = false, features = ["std", "derive"] } thiserror = { version = "1.0", default-features = false } [dev-dependencies] +proptest = { version = "1.0.0", default-features = false, features = ["std"] } serde_json = { version = "1.0", default-features = false } [package.metadata.docs.rs] @@ -28,5 +33,6 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["revocation-bitmap"] -revocation-bitmap = [] +default = ["revocation-bitmap", "validator"] +revocation-bitmap = ["identity_did/revocation-bitmap"] +validator = ["dep:itertools", "dep:erased-serde", "dep:serde_repr"] diff --git a/identity_credential/README.md b/identity_credential/README.md index b7fa06a6d2..715b84c59b 100644 --- a/identity_credential/README.md +++ b/identity_credential/README.md @@ -5,6 +5,11 @@ This crate contains types representing verifiable credentials and verifiable pre The [IOTA Identity Framework Wiki](https://wiki.iota.org/identity.rs/concepts/verifiable_credentials/overview) provides an overview of verifiable credentials and demonstrates how they may be constructed using the building blocks from this crate. +Convenience methods for validating [Verifiable Credentials](https://wiki.iota.org/identity.rs/concepts/verifiable_credentials/overview) and [Verifiable Presentations](https://wiki.iota.org/identity.rs/concepts/verifiable_credentials/verifiable_presentations) are also provided: + +- [`CredentialValidator`](crate::validator::CredentialValidator) +- [`PresentationValidator`](crate::validator::PresentationValidator) + ## Construction This crate follows the [builder pattern](https://rust-unofficial.github.io/patterns/patterns/creational/builder.html) for the creation of [`Credentials`](crate::credential::Credential) and [`Presentations`](crate::presentation::Presentation). @@ -158,4 +163,4 @@ let presentation_json: &'static str = r#"{ "#; let presentation: Presentation = serde_json::from_str(presentation_json).unwrap(); - ``` \ No newline at end of file +``` diff --git a/identity_credential/src/credential/revocation_bitmap_status.rs b/identity_credential/src/credential/revocation_bitmap_status.rs index ed8a295fc7..e26f0f59a1 100644 --- a/identity_credential/src/credential/revocation_bitmap_status.rs +++ b/identity_credential/src/credential/revocation_bitmap_status.rs @@ -13,13 +13,14 @@ use crate::credential::Status; use crate::error::Error; use crate::error::Result; -/// Information used to determine the current status of a [`Credential`][crate::credential::Credential]. +/// Information used to determine the current status of a [`Credential`][crate::credential::Credential] +/// using the `RevocationBitmap2022` specification. #[derive(Clone, Debug, PartialEq)] pub struct RevocationBitmapStatus(Status); impl RevocationBitmapStatus { const INDEX_PROPERTY_NAME: &'static str = "revocationBitmapIndex"; - /// The type name of the revocation bitmap. + /// Type name of the revocation bitmap. pub const TYPE: &'static str = "RevocationBitmap2022"; /// Creates a new `RevocationBitmapStatus`. @@ -33,7 +34,8 @@ impl RevocationBitmapStatus { )) } - /// Returns the [`DIDUrl`] of the bitmap status. + /// Returns the [`DIDUrl`] of the `RevocationBitmapStatus`, which should resolve + /// to a `RevocationBitmap2022` service in a DID Document. pub fn id(&self) -> Result> { DIDUrl::parse(self.0.id.as_str()) .map_err(|err| Error::InvalidStatus(format!("invalid DID Url '{}': {:?}", self.0.id, err))) diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index bd5ca9da2e..f713166b62 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -6,23 +6,23 @@ /// Alias for a `Result` with the error type [`Error`]. pub type Result = ::core::result::Result; -/// This type represents all possible errors that can occur in the library. +/// This type represents errors that can occur when constructing credentials and presentations. #[derive(Debug, thiserror::Error, strum::IntoStaticStr)] pub enum Error { - /// Caused when validating a Credential without a valid base context. - #[error("Missing Base Context")] + /// Caused when constructing a credential or presentation without a valid base context. + #[error("missing base context")] MissingBaseContext, - /// Caused when validating a Credential without a valid base type. - #[error("Missing Base Type")] + /// Caused when constructing a credential or presentation without a valid base type. + #[error("missing base type")] MissingBaseType, - /// Caused when validating a Credential without an issuer. - #[error("Missing Credential Issuer")] + /// Caused when constructing a credential without an issuer. + #[error("missing credential issuer")] MissingIssuer, - /// Caused when validating a Credential without a subject. - #[error("Missing Credential Subject")] + /// Caused when constructing a credential without a subject. + #[error("missing Credential subject")] MissingSubject, - /// Caused when validating a Credential with a malformed subject. - #[error("Invalid Credential Subject")] + /// Caused when constructing a credential with a malformed subject. + #[error("invalid credential subject")] InvalidSubject, /// Caused when trying to construct an invalid status. #[error("invalid credential status: {0}")] diff --git a/identity_credential/src/lib.rs b/identity_credential/src/lib.rs index 454447edf7..11e39cce7f 100644 --- a/identity_credential/src/lib.rs +++ b/identity_credential/src/lib.rs @@ -3,7 +3,6 @@ #![forbid(unsafe_code)] #![doc = include_str!("./../README.md")] -#![allow(clippy::upper_case_acronyms)] #![warn( rust_2018_idioms, unreachable_pub, @@ -18,13 +17,15 @@ #[macro_use] extern crate lazy_static; - #[macro_use] extern crate serde; +pub use self::error::Error; +pub use self::error::Result; + pub mod credential; pub mod error; pub mod presentation; -pub use self::error::Error; -pub use self::error::Result; +#[cfg(feature = "validator")] +pub mod validator; diff --git a/identity_iota_client/src/credential/credential_validator.rs b/identity_credential/src/validator/credential_validator.rs similarity index 84% rename from identity_iota_client/src/credential/credential_validator.rs rename to identity_credential/src/validator/credential_validator.rs index e6bda891db..81cb80eca9 100644 --- a/identity_iota_client/src/credential/credential_validator.rs +++ b/identity_credential/src/validator/credential_validator.rs @@ -1,23 +1,23 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::str::FromStr; + use serde::Serialize; use identity_core::common::OneOrMany; use identity_core::common::Timestamp; use identity_core::common::Url; -use identity_credential::credential::Credential; -#[cfg(feature = "revocation-bitmap")] -use identity_credential::credential::RevocationBitmapStatus; +use identity_did::did::CoreDID; +use identity_did::did::CoreDIDUrl; +use identity_did::did::DID; #[cfg(feature = "revocation-bitmap")] use identity_did::revocation::RevocationBitmap; use identity_did::verifiable::VerifierOptions; -use identity_iota_core::did::IotaDID; -#[cfg(feature = "revocation-bitmap")] -use identity_iota_core::did::IotaDIDUrl; -use identity_iota_core::document::IotaDocument; -use crate::Result; +use crate::credential::Credential; +#[cfg(feature = "revocation-bitmap")] +use crate::credential::RevocationBitmapStatus; use super::errors::CompoundCredentialValidationError; use super::errors::SignerContext; @@ -27,6 +27,7 @@ use super::validation_options::StatusCheck; use super::CredentialValidationOptions; use super::FailFast; use super::SubjectHolderRelationship; +use super::ValidatorDocument; /// A struct for validating [`Credential`]s. #[derive(Debug, Clone)] @@ -51,9 +52,7 @@ impl CredentialValidator { /// calling this method. /// /// ## The state of the issuer's DID Document - /// The caller must ensure that `issuer` represents an up-to-date DID Document. The convenience method - /// [`Resolver::resolve_credential_issuer`](crate::tangle::Resolver::resolve_credential_issuer()) can help extract - /// the latest available state of the issuer's DID Document. + /// The caller must ensure that `issuer` represents an up-to-date DID Document. /// /// ## Properties that are not validated /// There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as: @@ -62,9 +61,9 @@ impl CredentialValidator { /// /// # Errors /// An error is returned whenever a validated condition is not satisfied. - pub fn validate>( + pub fn validate( credential: &Credential, - issuer: &D, + issuer: &DOC, options: &CredentialValidationOptions, fail_fast: FailFast, ) -> CredentialValidationResult { @@ -107,42 +106,24 @@ impl CredentialValidator { /// This method immediately returns an error if /// the credential issuer' url cannot be parsed to a DID belonging to one of the trusted issuers. Otherwise an attempt /// to verify the credential's signature will be made and an error is returned upon failure. - pub fn verify_signature>( + pub fn verify_signature( credential: &Credential, - trusted_issuers: &[D], + trusted_issuers: &[DOC], options: &VerifierOptions, ) -> ValidationUnitResult { - // try to extract the corresponding issuer from `trusted_issuers` - let extracted_issuer_result: std::result::Result<&IotaDocument, ValidationError> = { - let issuer_did: Result = credential.issuer.url().as_str().parse().map_err(Into::into); - match issuer_did { - Ok(did) => { - // if the issuer_did corresponds to one of the trusted issuers we use the corresponding DID Document to verify - // the signature - trusted_issuers - .iter() - .map(|issuer_doc| issuer_doc.as_ref()) - .find(|issuer_doc| issuer_doc.id() == &did) - .ok_or(ValidationError::DocumentMismatch(SignerContext::Issuer)) - } - Err(error) => { - // the issuer's url could not be parsed to a valid IotaDID - Err(ValidationError::SignerUrl { - source: error.into(), + let issuer_did: CoreDID = Self::extract_issuer(credential)?; + trusted_issuers + .iter() + .find(|issuer_doc| issuer_doc.did_str() == issuer_did.as_str()) + .ok_or(ValidationError::DocumentMismatch(SignerContext::Issuer)) + .and_then(|issuer| { + issuer + .verify_data(credential, options) + .map_err(|err| ValidationError::Signature { + source: err.into(), signer_ctx: SignerContext::Issuer, }) - } - } - }; - // use the extracted document to verify the signature - extracted_issuer_result.and_then(|issuer| { - issuer - .verify_data(credential, options) - .map_err(|error| ValidationError::Signature { - source: error.into(), - signer_ctx: SignerContext::Issuer, - }) - }) + }) } /// Validate that the relationship between the `holder` and the credential subjects is in accordance with @@ -184,9 +165,9 @@ impl CredentialValidator { /// /// Only supports `BitmapRevocation2022`. #[cfg(feature = "revocation-bitmap")] - pub fn check_status>( + pub fn check_status( credential: &Credential, - trusted_issuers: &[D], + trusted_issuers: &[DOC], status_check: StatusCheck, ) -> ValidationUnitResult { if status_check == StatusCheck::SkipAll { @@ -201,22 +182,19 @@ impl CredentialValidator { if status_check == StatusCheck::SkipUnsupported { return Ok(()); } - return Err(ValidationError::InvalidStatus( - identity_credential::Error::InvalidStatus(format!("unsupported type '{}'", status.type_)), - )); + return Err(ValidationError::InvalidStatus(crate::Error::InvalidStatus(format!( + "unsupported type '{}'", + status.type_ + )))); } let status: RevocationBitmapStatus = RevocationBitmapStatus::try_from(status.clone()).map_err(ValidationError::InvalidStatus)?; // Check the credential index against the issuer's DID Document. - let issuer_did: IotaDID = - IotaDID::parse(credential.issuer.url().as_str()).map_err(|e| ValidationError::SignerUrl { - source: e.into(), - signer_ctx: SignerContext::Issuer, - })?; + let issuer_did: CoreDID = Self::extract_issuer(credential)?; trusted_issuers .iter() - .find(|issuer| issuer.as_ref().id() == &issuer_did) + .find(|issuer| issuer.did_str() == issuer_did.as_str()) .ok_or(ValidationError::DocumentMismatch(SignerContext::Issuer)) .and_then(|issuer| CredentialValidator::check_revocation_bitmap_status(issuer, status)) } @@ -226,24 +204,16 @@ impl CredentialValidator { /// Check the given `status` against the matching [`RevocationBitmap`] service in the /// issuer's DID Document. #[cfg(feature = "revocation-bitmap")] - fn check_revocation_bitmap_status>( - issuer: D, + fn check_revocation_bitmap_status( + issuer: &DOC, status: RevocationBitmapStatus, ) -> ValidationUnitResult { - let issuer_service_url: IotaDIDUrl = status.id().map_err(ValidationError::InvalidStatus)?; - - // Lookup service. - let service = issuer - .as_ref() - .service() - .iter() - .find(|service| &issuer_service_url == service.id()) - .ok_or(ValidationError::InvalidService(identity_did::Error::InvalidService( - "revocation bitmap service not found", - )))?; + let issuer_service_url: CoreDIDUrl = status.id().map_err(ValidationError::InvalidStatus)?; // Check whether index is revoked. - let revocation_bitmap: RevocationBitmap = service.try_into().map_err(ValidationError::InvalidService)?; + let revocation_bitmap: RevocationBitmap = issuer + .resolve_revocation_bitmap(issuer_service_url.into()) + .map_err(ValidationError::InvalidService)?; let index: u32 = status.index().map_err(ValidationError::InvalidStatus)?; if revocation_bitmap.is_revoked(index) { Err(ValidationError::Revoked) @@ -255,9 +225,9 @@ impl CredentialValidator { // This method takes a slice of issuer's instead of a single issuer in order to better accommodate presentation // validation. It also validates the relation ship between a holder and the credential subjects when // `relationship_criterion` is Some. - pub(crate) fn validate_extended>( + pub(crate) fn validate_extended( credential: &Credential, - issuers: &[D], + issuers: &[DOC], options: &CredentialValidationOptions, relationship_criterion: Option<(&Url, SubjectHolderRelationship)>, fail_fast: FailFast, @@ -306,6 +276,21 @@ impl CredentialValidator { Err(CompoundCredentialValidationError { validation_errors }) } } + + /// Utility for extracting the issuer field of a [`Credential`] as a DID. + /// + /// # Errors + /// + /// Fails if the issuer field is not a valid DID. + pub fn extract_issuer(credential: &Credential) -> std::result::Result + where + ::Err: std::error::Error + Send + Sync + 'static, + { + D::from_str(credential.issuer.url().as_str()).map_err(|err| ValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } } #[cfg(test)] @@ -319,14 +304,14 @@ mod tests { use identity_core::convert::FromJson; use identity_core::crypto::KeyPair; use identity_core::crypto::ProofOptions; - use identity_credential::credential::Status; - use identity_credential::credential::Subject; use identity_did::did::DID; - use identity_iota_core::document::IotaDocument; - use identity_iota_core::document::IotaService; + use identity_did::document::CoreDocument; + use identity_did::service::Service; - use crate::credential::test_utils; - use crate::credential::CredentialValidationOptions; + use crate::credential::Status; + use crate::credential::Subject; + use crate::validator::test_utils; + use crate::validator::CredentialValidationOptions; use super::*; @@ -359,7 +344,7 @@ mod tests { // Setup parameters shared by many of the tests in this module struct Setup { - issuer_doc: IotaDocument, + issuer_doc: CoreDocument, issuer_key: KeyPair, unsigned_credential: Credential, issuance_date: Timestamp, @@ -427,12 +412,10 @@ mod tests { issuance_date, } = Setup::new(); issuer_doc - .sign_data( - &mut credential, - issuer_key.private(), - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_key.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); // declare the credential validation parameters @@ -499,12 +482,10 @@ mod tests { issuance_date, } = Setup::new(); issuer_doc - .sign_data( - &mut credential, - issuer_key.private(), - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_key.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); // declare the credential validation parameters @@ -539,15 +520,13 @@ mod tests { expiration_date, } = Setup::new(); issuer_doc - .sign_data( - &mut credential, - issuer_key.private(), - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_key.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); - // declare the credential validation parameters + // declare the credential validation parameters let issued_on_or_before = issuance_date.checked_add(Duration::days(14)).unwrap(); let expires_on_or_after = expiration_date.checked_sub(Duration::hours(1)).unwrap(); let options = CredentialValidationOptions::default() @@ -567,12 +546,10 @@ mod tests { } = Setup::new(); let (other_doc, _) = test_utils::generate_document_with_keys(); issuer_doc - .sign_data( - &mut credential, - issuer_key.private(), - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_key.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); // the credential was not signed by this issuer @@ -580,12 +557,7 @@ mod tests { // check that `verify_signature` returns the expected error assert!(matches!( - CredentialValidator::verify_signature( - &credential, - std::slice::from_ref(&other_doc), - &VerifierOptions::default() - ) - .unwrap_err(), + CredentialValidator::verify_signature(&credential, &[&other_doc], &VerifierOptions::default()).unwrap_err(), ValidationError::DocumentMismatch { .. } )); @@ -621,22 +593,15 @@ mod tests { let (_, other_keys) = test_utils::generate_document_with_keys(); issuer_doc - .sign_data( - &mut credential, - other_keys.private(), // sign with other keys - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(other_keys.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); // run the validation unit assert!(matches!( - CredentialValidator::verify_signature( - &credential, - std::slice::from_ref(&issuer_doc), - &VerifierOptions::default() - ) - .unwrap_err(), + CredentialValidator::verify_signature(&credential, &[&issuer_doc], &VerifierOptions::default()).unwrap_err(), ValidationError::Signature { .. } )); @@ -797,7 +762,7 @@ mod tests { } // Add a RevocationBitmap status to the credential. - let service_url: IotaDIDUrl = issuer_doc.id().to_url().join("#revocation-service").unwrap(); + let service_url: CoreDIDUrl = issuer_doc.id().to_url().join("#revocation-service").unwrap(); let index: u32 = 42; credential.credential_status = Some(RevocationBitmapStatus::new(service_url.clone(), index).into()); @@ -815,13 +780,13 @@ mod tests { // Add a RevocationBitmap service to the issuer. let bitmap: RevocationBitmap = RevocationBitmap::new(); - assert!(issuer_doc.insert_service( - IotaService::builder(Object::new()) + assert!(issuer_doc.service_mut().append( + Service::builder(Object::new()) .id(service_url.clone()) .type_(RevocationBitmap::TYPE) .service_endpoint(bitmap.to_endpoint().unwrap()) .build() - .unwrap(), + .unwrap() )); // 3: un-revoked index always succeeds. @@ -854,12 +819,10 @@ mod tests { } = Setup::new(); issuer_doc - .sign_data( - &mut credential, - issuer_key.private(), - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_key.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); // the credential now has no credential subjects which is not semantically correct credential.credential_subject = OneOrMany::default(); @@ -896,12 +859,10 @@ mod tests { let (other_doc, _) = test_utils::generate_document_with_keys(); issuer_doc - .sign_data( - &mut credential, - issuer_key.private(), - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_key.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); // the credential now has no credential subjects which is not semantically correct credential.credential_subject = OneOrMany::default(); @@ -937,12 +898,10 @@ mod tests { let (other_doc, _) = test_utils::generate_document_with_keys(); issuer_doc - .sign_data( - &mut credential, - issuer_key.private(), - issuer_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_key.private()) + .options(ProofOptions::default()) + .method(issuer_doc.methods().next().unwrap().id()) + .sign(&mut credential) .unwrap(); // the credential now has no credential subjects which is not semantically correct credential.credential_subject = OneOrMany::default(); diff --git a/identity_iota_client/src/credential/errors.rs b/identity_credential/src/validator/errors.rs similarity index 76% rename from identity_iota_client/src/credential/errors.rs rename to identity_credential/src/validator/errors.rs index edcaebc38a..1ae8683876 100644 --- a/identity_iota_client/src/credential/errors.rs +++ b/identity_credential/src/validator/errors.rs @@ -4,6 +4,8 @@ use std::collections::BTreeMap; use std::fmt::Display; +use itertools; + #[derive(Debug, thiserror::Error, strum::IntoStaticStr)] #[non_exhaustive] /// An error associated with validating credentials and presentations. @@ -14,38 +16,44 @@ pub enum ValidationError { /// Indicates that the issuance date of the credential is not considered valid. #[error("issuance date is in the future or later than required")] IssuanceDate, - /// Indicates that the credential's (resp. presentation's) signature could not be verified using the issuer's (resp. - /// holder's) DID Document. + /// Indicates that the credential's (resp. presentation's) signature could not be verified using + /// the issuer's (resp. holder's) DID Document. #[error("could not verify the {signer_ctx}'s signature")] #[non_exhaustive] Signature { + /// Signature verification error. source: Box, + /// Specifies whether the error was from the DID Document of a credential issuer + /// or the presentation holder. signer_ctx: SignerContext, }, - /// Indicates that the credential's (resp. presentation's) issuer's (resp. holder's) URL could not be parsed as a - /// valid DID. + /// Indicates that the credential's (resp. presentation's) issuer's (resp. holder's) URL could + /// not be parsed as a valid DID. #[error("{signer_ctx} URL is not a valid DID")] #[non_exhaustive] SignerUrl { + /// DID parsing error. source: Box, + /// Specifies whether the error relates to the DID of a credential issuer + /// or the presentation holder. signer_ctx: SignerContext, }, - /// Indicates an attempt to verify a signature of a credential (resp. presentation) using a DID Document not matching - /// the issuer's (resp. holder's) id. + /// Indicates an attempt to verify a signature of a credential (resp. presentation) using a + /// DID Document not matching the issuer's (resp. holder's) id. #[error("the {0}'s id does not match the provided DID Document(s)")] #[non_exhaustive] DocumentMismatch(SignerContext), - /// Indicates that the structure of the [Credential](identity_credential::credential::Credential) is not semantically + /// Indicates that the structure of the [Credential](crate::credential::Credential) is not semantically /// correct. #[error("the credential's structure is not semantically correct")] - CredentialStructure(#[source] identity_credential::Error), - /// Indicates that the structure of the [Presentation](identity_credential::presentation::Presentation) is not + CredentialStructure(#[source] crate::Error), + /// Indicates that the structure of the [Presentation](crate::presentation::Presentation) is not /// semantically correct. #[error("the presentation's structure is not semantically correct")] - PresentationStructure(#[source] identity_credential::Error), + PresentationStructure(#[source] crate::Error), /// Indicates that the relationship between the presentation holder and one of the credential subjects is not valid. #[error("expected holder = subject of the credential")] #[non_exhaustive] @@ -55,7 +63,7 @@ pub enum ValidationError { MissingPresentationHolder, /// Indicates that the credential's status is invalid. #[error("invalid credential status")] - InvalidStatus(#[source] identity_credential::Error), + InvalidStatus(#[source] crate::Error), /// Indicates that the the credential's service is invalid. #[error("invalid service")] InvalidService(#[source] identity_did::Error), @@ -64,10 +72,13 @@ pub enum ValidationError { Revoked, } +/// Specifies whether an error is related to a credential issuer or the presentation holder. #[derive(Debug)] #[non_exhaustive] pub enum SignerContext { + /// Credential issuer. Issuer, + /// Presentation holder. Holder, } @@ -81,9 +92,10 @@ impl Display for SignerContext { } } +/// Errors caused by a failure to validate a credential. #[derive(Debug)] -/// An error caused by a failure to validate a Credential. pub struct CompoundCredentialValidationError { + /// List of credential validation errors. pub validation_errors: Vec, } @@ -104,7 +116,10 @@ impl std::error::Error for CompoundCredentialValidationError {} #[derive(Debug)] /// An error caused by a failure to validate a Presentation. pub struct CompoundPresentationValidationError { + /// Errors that occurred during validation of individual credentials, mapped by index of their + /// order in the presentation. pub credential_errors: BTreeMap, + /// Errors that occurred during validation of the presentation. pub presentation_validation_errors: Vec, } diff --git a/identity_iota_client/src/credential/mod.rs b/identity_credential/src/validator/mod.rs similarity index 91% rename from identity_iota_client/src/credential/mod.rs rename to identity_credential/src/validator/mod.rs index 35a87e8491..f494dc2bdf 100644 --- a/identity_iota_client/src/credential/mod.rs +++ b/identity_credential/src/validator/mod.rs @@ -14,6 +14,7 @@ pub use self::validation_options::FailFast; pub use self::validation_options::PresentationValidationOptions; pub use self::validation_options::StatusCheck; pub use self::validation_options::SubjectHolderRelationship; +pub use self::validator_document::ValidatorDocument; mod credential_validator; mod errors; @@ -21,3 +22,4 @@ mod presentation_validator; #[cfg(test)] mod test_utils; mod validation_options; +mod validator_document; diff --git a/identity_iota_client/src/credential/presentation_validator.rs b/identity_credential/src/validator/presentation_validator.rs similarity index 84% rename from identity_iota_client/src/credential/presentation_validator.rs rename to identity_credential/src/validator/presentation_validator.rs index 0aa23bc385..ddae49f703 100644 --- a/identity_iota_client/src/credential/presentation_validator.rs +++ b/identity_credential/src/validator/presentation_validator.rs @@ -2,20 +2,25 @@ // SPDX-License-Identifier: Apache-2.0 use std::collections::BTreeMap; +use std::str::FromStr; -use identity_credential::presentation::Presentation; -use identity_did::verifiable::VerifierOptions; -use identity_iota_core::did::IotaDID; -use identity_iota_core::document::IotaDocument; use serde::Serialize; +use identity_core::common::Url; +use identity_did::did::CoreDID; +use identity_did::did::DID; +use identity_did::verifiable::VerifierOptions; + +use crate::presentation::Presentation; + use super::errors::CompoundCredentialValidationError; use super::errors::CompoundPresentationValidationError; use super::errors::SignerContext; use super::errors::ValidationError; +use super::CredentialValidator; use super::FailFast; use super::PresentationValidationOptions; -use crate::credential::credential_validator::CredentialValidator; +use super::ValidatorDocument; /// A struct for validating [`Presentation`]s. #[derive(Debug, Clone)] @@ -41,10 +46,7 @@ impl PresentationValidator { /// calling this method. /// /// ## The state of the supplied DID Documents. - /// The caller must ensure that the DID Documents in `holder` and `issuers` are up-to-date. The convenience methods - /// [`Resolver::resolve_presentation_holder`](crate::tangle::Resolver::resolve_presentation_holder()) - /// and [`Resolver::resolve_presentation_issuers`](crate::tangle::Resolver::resolve_presentation_issuers()) - /// can help extract the latest available states of these DID Documents. + /// The caller must ensure that the DID Documents in `holder` and `issuers` are up-to-date. /// /// ## Properties that are not validated /// There are many properties defined in [The Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/) that are **not** validated, such as: @@ -53,10 +55,10 @@ impl PresentationValidator { /// /// # Errors /// An error is returned whenever a validated condition is not satisfied. - pub fn validate>( + pub fn validate( presentation: &Presentation, - holder: &D, - issuers: &[D], + holder: &HDOC, + issuers: &[IDOC], options: &PresentationValidationOptions, fail_fast: FailFast, ) -> PresentationValidationResult { @@ -109,29 +111,19 @@ impl PresentationValidator { /// # Errors /// Fails if the `holder` does not match the `presentation`'s holder property. /// Fails if signature verification against the holder document fails. - pub fn verify_presentation_signature>( + pub fn verify_presentation_signature( presentation: &Presentation, - holder: &D, + holder: &DOC, options: &VerifierOptions, ) -> ValidationUnitResult { - let did: IotaDID = presentation - .holder - .as_ref() - .ok_or(ValidationError::MissingPresentationHolder) - .and_then(|value| { - IotaDID::parse(value.as_str()).map_err(|error| ValidationError::SignerUrl { - source: error.into(), - signer_ctx: SignerContext::Holder, - }) - })?; - if &did != holder.as_ref().id() { + let did: CoreDID = Self::extract_holder(presentation)?; + if did.as_str() != holder.did_str() { return Err(ValidationError::DocumentMismatch(SignerContext::Holder)); } holder - .as_ref() .verify_data(&presentation, options) - .map_err(|error| ValidationError::Signature { - source: error.into(), + .map_err(|err| ValidationError::Signature { + source: err.into(), signer_ctx: SignerContext::Holder, }) } @@ -148,9 +140,9 @@ impl PresentationValidator { // The following properties are validated according to `options`: // - the semantic structure of the presentation, // - the holder's signature, - fn validate_presentation_without_credentials>( + fn validate_presentation_without_credentials( presentation: &Presentation, - holder: &D, + holder: &DOC, options: &PresentationValidationOptions, fail_fast: FailFast, ) -> Result<(), Vec> { @@ -181,9 +173,9 @@ impl PresentationValidator { // - the relationship between the holder and the credential subjects, // - the signatures and some properties of the constituent credentials (see // [`CredentialValidator::validate`]). - fn validate_credentials>( + fn validate_credentials( presentation: &Presentation, - issuers: &[D], + issuers: &[DOC], options: &PresentationValidationOptions, fail_fast: FailFast, ) -> Result<(), BTreeMap> { @@ -219,6 +211,25 @@ impl PresentationValidator { Err(credential_errors) } } + + /// Utility for extracting the holder field of a [`Presentation`] as a DID. + /// + /// # Errors + /// + /// Fails if the holder field is missing or not a valid DID. + pub fn extract_holder(presentation: &Presentation) -> std::result::Result + where + ::Err: std::error::Error + Send + Sync + 'static, + { + let holder: &Url = presentation + .holder + .as_ref() + .ok_or(ValidationError::MissingPresentationHolder)?; + D::from_str(holder.as_str()).map_err(|err| ValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } } #[cfg(test)] @@ -227,18 +238,19 @@ mod tests { use identity_core::common::Url; use identity_core::crypto::KeyPair; use identity_core::crypto::ProofOptions; - use identity_credential::credential::Credential; - use identity_credential::presentation::PresentationBuilder; - use identity_iota_core::document::IotaDocument; + use identity_did::document::CoreDocument; + + use crate::credential::Credential; + use crate::presentation::PresentationBuilder; + use crate::validator::test_utils; + use crate::validator::CredentialValidationOptions; + use crate::validator::SubjectHolderRelationship; use super::*; - use crate::credential::test_utils; - use crate::credential::CredentialValidationOptions; - use crate::credential::SubjectHolderRelationship; - fn build_presentation(holder: &IotaDocument, credentials: Vec) -> Presentation { + fn build_presentation(holder: &CoreDocument, credentials: Vec) -> Presentation { let mut builder = PresentationBuilder::default() - .id(Url::parse("http://example.org/credentials/3732").unwrap()) + .id(Url::parse("https://example.org/credentials/3732").unwrap()) .holder(Url::parse(holder.id().as_ref()).unwrap()); for credential in credentials { builder = builder.credential(credential); @@ -249,14 +261,14 @@ mod tests { // Convenience struct for setting up tests. struct TestSetup { // issuer of credential_foo - issuer_foo_doc: IotaDocument, + issuer_foo_doc: CoreDocument, issuer_foo_key: KeyPair, // subject of credential_foo - subject_foo_doc: IotaDocument, + subject_foo_doc: CoreDocument, subject_foo_key: KeyPair, credential_foo: Credential, // issuer of credential_bar - issuer_bar_doc: IotaDocument, + issuer_bar_doc: CoreDocument, issuer_bar_key: KeyPair, credential_bar: Credential, } @@ -304,21 +316,17 @@ mod tests { } = setup; // sign the credential issuer_foo_doc - .sign_data( - credential_foo, - issuer_foo_key.private(), - issuer_foo_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_foo_key.private()) + .options(ProofOptions::default()) + .method(issuer_foo_doc.methods().next().unwrap().id()) + .sign(credential_foo) .unwrap(); issuer_bar_doc - .sign_data( - credential_bar, - issuer_bar_key.private(), - issuer_bar_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_bar_key.private()) + .options(ProofOptions::default()) + .method(issuer_bar_doc.methods().next().unwrap().id()) + .sign(credential_bar) .unwrap(); setup } @@ -339,14 +347,11 @@ mod tests { let mut presentation = build_presentation(&subject_foo_doc, [credential_foo, credential_bar].to_vec()); // sign the presentation using subject_foo's document and private key - subject_foo_doc - .sign_data( - &mut presentation, - subject_foo_key.private(), - subject_foo_doc.default_signing_method().unwrap().id(), - ProofOptions::new().challenge("475a7984-1bb5-4c4c-a56f-822bccd46440".to_owned()), - ) + .signer(subject_foo_key.private()) + .options(ProofOptions::new().challenge("475a7984-1bb5-4c4c-a56f-822bccd46440".to_owned())) + .method(subject_foo_doc.methods().next().unwrap().id()) + .sign(&mut presentation) .unwrap(); // validate the presentation @@ -362,15 +367,13 @@ mod tests { .presentation_verifier_options(presentation_verifier_options) .subject_holder_relationship(SubjectHolderRelationship::SubjectOnNonTransferable); - let trusted_issuers = [issuer_foo_doc, issuer_bar_doc]; - let holder_doc = subject_foo_doc; assert!(dbg!(PresentationValidator::validate( &presentation, &holder_doc, - &trusted_issuers, + &[&issuer_foo_doc, &issuer_bar_doc], &presentation_validation_options, - FailFast::FirstError + FailFast::FirstError, )) .is_ok()); } @@ -390,18 +393,14 @@ mod tests { let mut presentation = build_presentation(&subject_foo_doc, [credential_foo, credential_bar].to_vec()); // sign the presentation using subject_foo's document and private key - subject_foo_doc - .sign_data( - &mut presentation, - subject_foo_key.private(), - subject_foo_doc.default_signing_method().unwrap().id(), - ProofOptions::new().challenge("some challenge".to_owned()), - ) + .signer(subject_foo_key.private()) + .options(ProofOptions::new().challenge("some challenge".to_owned())) + .method(subject_foo_doc.methods().next().unwrap().id()) + .sign(&mut presentation) .unwrap(); // check the validation unit - let trusted_issuers = [issuer_foo_doc, issuer_bar_doc]; let presentation_verifier_options = VerifierOptions::default().challenge("another challenge".to_owned()); //validate with another challenge let holder_doc = subject_foo_doc; @@ -410,7 +409,7 @@ mod tests { assert!(PresentationValidator::verify_presentation_signature( &presentation, &holder_doc, - &presentation_verifier_options + &presentation_verifier_options, ) .is_err()); @@ -429,7 +428,7 @@ mod tests { let error = PresentationValidator::validate( &presentation, &holder_doc, - &trusted_issuers, + &[&issuer_foo_doc, &issuer_bar_doc], &presentation_validation_options, FailFast::FirstError, ) @@ -459,14 +458,11 @@ mod tests { let mut presentation = build_presentation(&subject_foo_doc, [credential_foo, credential_bar].to_vec()); // sign the presentation using subject_foo's document and private key - subject_foo_doc - .sign_data( - &mut presentation, - subject_foo_key.private(), - subject_foo_doc.default_signing_method().unwrap().id(), - ProofOptions::new().challenge("some challenge".to_owned()), - ) + .signer(subject_foo_key.private()) + .options(ProofOptions::new().challenge("some challenge".to_owned())) + .method(subject_foo_doc.methods().next().unwrap().id()) + .sign(&mut presentation) .unwrap(); // validate the presentation @@ -481,13 +477,11 @@ mod tests { .presentation_verifier_options(presentation_verifier_options) .subject_holder_relationship(SubjectHolderRelationship::SubjectOnNonTransferable); - let trusted_issuers = [issuer_foo_doc, issuer_bar_doc]; - let holder_doc = subject_foo_doc; let error = PresentationValidator::validate( &presentation, &holder_doc, - &trusted_issuers, + &[&issuer_foo_doc, &issuer_bar_doc], &presentation_validation_options, FailFast::FirstError, ) @@ -517,12 +511,10 @@ mod tests { credential_bar.non_transferable = Some(true); // sign the credential issuer_bar_doc - .sign_data( - &mut credential_bar, - issuer_bar_key.private(), - issuer_bar_doc.default_signing_method().unwrap().id(), - ProofOptions::default(), - ) + .signer(issuer_bar_key.private()) + .options(ProofOptions::default()) + .method(issuer_bar_doc.methods().next().unwrap().id()) + .sign(&mut credential_bar) .unwrap(); // create a presentation where the subject of the first credential is the holder. @@ -530,14 +522,11 @@ mod tests { let mut presentation = build_presentation(&subject_foo_doc, [credential_foo, credential_bar].to_vec()); // sign the presentation using subject_foo's document and private key. - subject_foo_doc - .sign_data( - &mut presentation, - subject_foo_key.private(), - subject_foo_doc.default_signing_method().unwrap().id(), - ProofOptions::new().challenge("some challenge".to_owned()), - ) + .signer(subject_foo_key.private()) + .options(ProofOptions::new().challenge("some challenge".to_owned())) + .method(subject_foo_doc.methods().next().unwrap().id()) + .sign(&mut presentation) .unwrap(); // validate the presentation @@ -552,14 +541,13 @@ mod tests { .presentation_verifier_options(presentation_verifier_options) .subject_holder_relationship(SubjectHolderRelationship::SubjectOnNonTransferable); - let trusted_issuers = [issuer_foo_doc, issuer_bar_doc]; - + let trusted_issuers = &[issuer_foo_doc, issuer_bar_doc]; let holder_doc = subject_foo_doc; let error = PresentationValidator::validate( &presentation, &holder_doc, - &trusted_issuers, + trusted_issuers, &presentation_validation_options, FailFast::FirstError, ) @@ -580,7 +568,7 @@ mod tests { assert!(PresentationValidator::validate( &presentation, &holder_doc, - &trusted_issuers, + trusted_issuers, &options, FailFast::FirstError, ) @@ -591,7 +579,7 @@ mod tests { assert!(PresentationValidator::validate( &presentation, &holder_doc, - &trusted_issuers, + trusted_issuers, &options, FailFast::FirstError, ) @@ -628,14 +616,11 @@ mod tests { .build() .unwrap(); // sign the presentation using subject_foo's document and private key - subject_foo_doc - .sign_data( - &mut presentation, - subject_foo_key.private(), - subject_foo_doc.default_signing_method().unwrap().id(), - ProofOptions::new().challenge("some challenge".to_owned()), - ) + .signer(subject_foo_key.private()) + .options(ProofOptions::new().challenge("some challenge".to_owned())) + .method(subject_foo_doc.methods().next().unwrap().id()) + .sign(&mut presentation) .unwrap(); // validate the presentation @@ -649,8 +634,7 @@ mod tests { .shared_validation_options(credential_validation_options) .presentation_verifier_options(presentation_verifier_options); - let trusted_issuers = [issuer_foo_doc, issuer_bar_doc]; - + let trusted_issuers = &[issuer_foo_doc, issuer_bar_doc]; let holder_document = subject_foo_doc; let CompoundPresentationValidationError { @@ -659,19 +643,19 @@ mod tests { } = PresentationValidator::validate( &presentation, &holder_document, - &trusted_issuers, + trusted_issuers, &presentation_validation_options, FailFast::FirstError, ) .unwrap_err(); - assert!( + assert_eq!( + 1, presentation_validation_errors.len() + credential_errors .values() .map(|error| error.validation_errors.len()) .sum::() - == 1 ); } @@ -706,12 +690,10 @@ mod tests { // sign the presentation using subject_foo's document and private key subject_foo_doc - .sign_data( - &mut presentation, - subject_foo_key.private(), - subject_foo_doc.default_signing_method().unwrap().id(), - ProofOptions::new().challenge("some challenge".to_owned()), - ) + .signer(subject_foo_key.private()) + .options(ProofOptions::new().challenge("some challenge".to_owned())) + .method(subject_foo_doc.methods().next().unwrap().id()) + .sign(&mut presentation) .unwrap(); // validate the presentation @@ -726,8 +708,7 @@ mod tests { .shared_validation_options(credential_validation_options) .presentation_verifier_options(presentation_verifier_options); - let trusted_issuers = [issuer_foo_doc, issuer_bar_doc]; - + let trusted_issuers = &[issuer_foo_doc, issuer_bar_doc]; let holder_doc = subject_foo_doc; let CompoundPresentationValidationError { @@ -736,7 +717,7 @@ mod tests { } = PresentationValidator::validate( &presentation, &holder_doc, - &trusted_issuers, + trusted_issuers, &presentation_validation_options, FailFast::AllErrors, ) diff --git a/identity_iota_client/src/credential/test_utils.rs b/identity_credential/src/validator/test_utils.rs similarity index 63% rename from identity_iota_client/src/credential/test_utils.rs rename to identity_credential/src/validator/test_utils.rs index b568fe8f70..45e70aa44f 100644 --- a/identity_iota_client/src/credential/test_utils.rs +++ b/identity_credential/src/validator/test_utils.rs @@ -1,33 +1,45 @@ // Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_core::common::Object; use identity_core::common::Timestamp; use identity_core::common::Url; use identity_core::convert::FromJson; use identity_core::crypto::KeyPair; use identity_core::crypto::KeyType; use identity_core::json; -use identity_credential::credential::Credential; -use identity_credential::credential::CredentialBuilder; -use identity_credential::credential::Subject; +use identity_core::utils::BaseEncoding; +use identity_did::did::CoreDID; use identity_did::did::DID; -use identity_iota_core::document::IotaDocument; +use identity_did::document::CoreDocument; +use identity_did::verification::VerificationMethod; -use crate::Result; +use crate::credential::Credential; +use crate::credential::CredentialBuilder; +use crate::credential::Subject; -pub(super) fn generate_document_with_keys() -> (IotaDocument, KeyPair) { +pub(super) fn generate_document_with_keys() -> (CoreDocument, KeyPair) { let keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); - let document: IotaDocument = IotaDocument::new(&keypair).unwrap(); + let did: CoreDID = CoreDID::parse(&format!( + "did:example:{}", + BaseEncoding::encode_base58(keypair.public()) + )) + .unwrap(); + let document: CoreDocument = CoreDocument::builder(Object::new()) + .id(did.clone()) + .verification_method(VerificationMethod::new(did, KeyType::Ed25519, keypair.public(), "#root").unwrap()) + .build() + .unwrap(); (document, keypair) } pub(super) fn generate_credential( - issuer: &IotaDocument, - subjects: &[IotaDocument], + issuer: &CoreDocument, + subjects: &[CoreDocument], issuance_date: Timestamp, expiration_date: Timestamp, ) -> Credential { - let credential_subjects: Result> = subjects + let credential_subjects: Vec = subjects .iter() .map(|subject| { Subject::from_json_value(json!({ @@ -39,7 +51,7 @@ pub(super) fn generate_credential( }, "GPA": "4.0", })) - .map_err(Into::into) + .unwrap() }) .collect(); @@ -48,7 +60,7 @@ pub(super) fn generate_credential( .id(Url::parse("https://example.edu/credentials/3732").unwrap()) .issuer(Url::parse(issuer.id().as_str()).unwrap()) .type_("UniversityDegreeCredential") - .subjects(credential_subjects.unwrap()) + .subjects(credential_subjects) .issuance_date(issuance_date) .expiration_date(expiration_date) .build() @@ -56,7 +68,7 @@ pub(super) fn generate_credential( } // generates a triple: issuer document, issuer's keys, unsigned credential issued by issuer -pub(super) fn credential_setup() -> (IotaDocument, KeyPair, Credential) { +pub(super) fn credential_setup() -> (CoreDocument, KeyPair, Credential) { let (issuer_doc, issuer_key) = generate_document_with_keys(); let (subject_doc, _) = generate_document_with_keys(); let issuance_date = Timestamp::parse("2020-01-01T00:00:00Z").unwrap(); diff --git a/identity_iota_client/src/credential/validation_options.rs b/identity_credential/src/validator/validation_options.rs similarity index 96% rename from identity_iota_client/src/credential/validation_options.rs rename to identity_credential/src/validator/validation_options.rs index c37a4b74d4..34e7416b4f 100644 --- a/identity_iota_client/src/credential/validation_options.rs +++ b/identity_credential/src/validator/validation_options.rs @@ -124,9 +124,8 @@ pub enum FailFast { FirstError, } -/// Options to declare validation criteria for validation methods such as -/// [`PresentationValidator::validate`](super::PresentationValidator::validate()) and -/// [`Resolver::verify_presentation`](crate::tangle::Resolver::verify_presentation()). +/// Criteria for validating a [`Presentation`](crate::presentation::Presentation), such as with +/// [`PresentationValidator::validate`](crate::validator::PresentationValidator::validate()). #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] diff --git a/identity_credential/src/validator/validator_document.rs b/identity_credential/src/validator/validator_document.rs new file mode 100644 index 0000000000..118466d9bc --- /dev/null +++ b/identity_credential/src/validator/validator_document.rs @@ -0,0 +1,111 @@ +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::crypto::GetSignature; +use identity_did::did::DID; +use identity_did::document::Document; +use identity_did::revocation::RevocationBitmap; +use identity_did::utils::DIDUrlQuery; +use identity_did::verifiable::VerifierOptions; + +use self::private::Sealed; +use self::private::Verifiable; + +/// Abstraction over DID Documents for validating presentations and credentials. +/// +/// NOTE: this is a sealed trait and not intended to be used externally or implemented manually. +/// A blanket implementation is provided for the [`Document`] trait, which can be implemented +/// instead to be compatible. Any changes to this trait will be considered non-breaking. +pub trait ValidatorDocument: Sealed { + /// Convenience function for casting self to the trait. + /// + /// Equivalent to `self as &dyn ValidatorDocument`. + fn as_validator(&self) -> &dyn ValidatorDocument + where + Self: Sized, + { + self as &dyn ValidatorDocument + } + + /// Returns the string identifier of the DID Document. + fn did_str(&self) -> &str; + + /// Verifies the signature of the provided data against the DID Document. + /// + /// # Errors + /// + /// Fails if an unsupported verification method is used, data + /// serialization fails, or the verification operation fails. + fn verify_data(&self, data: &dyn Verifiable, options: &VerifierOptions) -> identity_did::Result<()>; + + /// Extracts the `RevocationBitmap` from the referenced service in the DID Document. + /// + /// # Errors + /// + /// Fails if the referenced service is not found, or is not a + /// valid `RevocationBitmap2022` service. + #[cfg(feature = "revocation-bitmap")] + fn resolve_revocation_bitmap(&self, query: DIDUrlQuery<'_>) -> identity_did::Result; +} + +mod private { + use super::*; + + pub trait Sealed {} + + impl Sealed for T where T: Document {} + impl Sealed for &dyn ValidatorDocument {} + + /// Object-safe trait workaround to satisfy the trait bounds + /// [`serde::Serialize`] + [`GetSignature`]. + pub trait Verifiable: erased_serde::Serialize + GetSignature {} + + impl Verifiable for T where T: erased_serde::Serialize + GetSignature {} + + impl<'a> serde::Serialize for dyn Verifiable + 'a { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + erased_serde::serialize(self, serializer) + } + } +} + +impl ValidatorDocument for &dyn ValidatorDocument { + fn did_str(&self) -> &str { + (*self).did_str() + } + + fn verify_data(&self, data: &dyn Verifiable, options: &VerifierOptions) -> identity_did::Result<()> { + (*self).verify_data(data, options) + } + + #[cfg(feature = "revocation-bitmap")] + fn resolve_revocation_bitmap(&self, query: DIDUrlQuery<'_>) -> identity_did::Result { + (*self).resolve_revocation_bitmap(query) + } +} + +impl ValidatorDocument for DOC +where + DOC: Document, +{ + fn did_str(&self) -> &str { + self.id().as_str() + } + + fn verify_data(&self, data: &dyn Verifiable, options: &VerifierOptions) -> identity_did::Result<()> { + self.verify_data(data, options).map_err(Into::into) + } + + #[cfg(feature = "revocation-bitmap")] + fn resolve_revocation_bitmap(&self, query: DIDUrlQuery<'_>) -> identity_did::Result { + self + .resolve_service(query) + .ok_or(identity_did::Error::InvalidService( + "revocation bitmap service not found", + )) + .and_then(RevocationBitmap::try_from) + } +} diff --git a/identity_did/src/document/core_document.rs b/identity_did/src/document/core_document.rs index c3f9860cfe..b3a855f147 100644 --- a/identity_did/src/document/core_document.rs +++ b/identity_did/src/document/core_document.rs @@ -25,6 +25,7 @@ use identity_core::crypto::Verifier; use crate::did::CoreDID; use crate::did::DIDUrl; use crate::did::DID; +use crate::document::Document; use crate::document::DocumentBuilder; use crate::error::Error; use crate::error::Result; @@ -672,7 +673,7 @@ where /// serialization fails, or the verification operation fails. pub fn verify_data(&self, data: &X, options: &VerifierOptions) -> Result<()> where - X: Serialize + GetSignature, + X: Serialize + GetSignature + ?Sized, { let signature: &Proof = data.signature().ok_or(Error::InvalidSignature("missing signature"))?; @@ -736,7 +737,7 @@ where /// serialization fails, or the verification operation fails. fn do_verify(method: &VerificationMethod, data: &X) -> Result<()> where - X: Serialize + GetSignature, + X: Serialize + GetSignature + ?Sized, { let public_key: Vec = method.data().try_decode()?; @@ -753,6 +754,114 @@ where } } +impl Document for CoreDocument +where + D: DID + KeyComparable, +{ + type D = D; + type U = U; + type V = V; + + fn id(&self) -> &Self::D { + CoreDocument::id(self) + } + + fn resolve_service<'query, 'me, Q>(&'me self, query: Q) -> Option<&Service> + where + Q: Into>, + { + self.service().query(query.into()) + } + + fn resolve_method<'query, 'me, Q>( + &'me self, + query: Q, + scope: Option, + ) -> Option<&VerificationMethod> + where + Q: Into>, + { + CoreDocument::resolve_method(self, query, scope) + } + + fn verify_data(&self, data: &X, options: &VerifierOptions) -> Result<()> + where + X: Serialize + GetSignature + ?Sized, + { + CoreDocument::verify_data(self, data, options) + } +} + +#[cfg(feature = "revocation-bitmap")] +mod core_document_revocation { + use identity_core::common::KeyComparable; + + use crate::did::DID; + use crate::revocation::RevocationBitmap; + use crate::service::Service; + use crate::utils::DIDUrlQuery; + use crate::utils::Queryable; + use crate::Error; + use crate::Result; + + use super::CoreDocument; + + impl CoreDocument + where + D: DID + KeyComparable, + { + /// If the document has a [`RevocationBitmap`] service identified by `service_query`, + /// revoke all credentials with a `revocationBitmapIndex` in `credential_indices`. + pub fn revoke_credentials<'query, 'me, Q>(&mut self, service_query: Q, credential_indices: &[u32]) -> Result<()> + where + Q: Into>, + { + self.update_revocation_bitmap(service_query, |revocation_bitmap| { + // Revoke all given credential indices. + for credential in credential_indices { + revocation_bitmap.revoke(*credential); + } + }) + } + + /// If the document has a [`RevocationBitmap`] service identified by `service_query`, + /// unrevoke all credentials with a `revocationBitmapIndex` in `credential_indices`. + pub fn unrevoke_credentials<'query, 'me, Q>( + &'me mut self, + service_query: Q, + credential_indices: &[u32], + ) -> Result<()> + where + Q: Into>, + { + self.update_revocation_bitmap(service_query, |revocation_bitmap| { + // Unrevoke all given credential indices. + for credential in credential_indices { + revocation_bitmap.unrevoke(*credential); + } + }) + } + + fn update_revocation_bitmap<'query, 'me, F, Q>(&'me mut self, service_query: Q, f: F) -> Result<()> + where + F: FnOnce(&mut RevocationBitmap), + Q: Into>, + { + let service: &mut Service = self + .service_mut() + .query_mut(service_query) + .ok_or(Error::InvalidService("invalid id - service not found"))?; + + let mut revocation_bitmap: RevocationBitmap = RevocationBitmap::try_from(&*service)?; + f(&mut revocation_bitmap); + + std::mem::swap(service.service_endpoint_mut(), &mut revocation_bitmap.to_endpoint()?); + + Ok(()) + } + } +} + // ============================================================================= // Signature Extensions // ============================================================================= diff --git a/identity_did/src/document/mod.rs b/identity_did/src/document/mod.rs index bcd021e45e..c3de65a652 100644 --- a/identity_did/src/document/mod.rs +++ b/identity_did/src/document/mod.rs @@ -1,12 +1,14 @@ -// Copyright 2020-2021 IOTA Stiftung +// Copyright 2020-2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 //! Defines the core (implementation agnostic) DID Document type. #![allow(clippy::module_inception)] -mod builder; -mod core_document; - pub use self::builder::DocumentBuilder; pub use self::core_document::CoreDocument; +pub use self::traits::Document; + +mod builder; +mod core_document; +mod traits; diff --git a/identity_did/src/document/traits.rs b/identity_did/src/document/traits.rs new file mode 100644 index 0000000000..8451786ae2 --- /dev/null +++ b/identity_did/src/document/traits.rs @@ -0,0 +1,88 @@ +// Copyright 2020-2022 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use serde::Serialize; + +use identity_core::crypto::GetSignature; + +use crate::did::DID; +use crate::service::Service; +use crate::utils::DIDUrlQuery; +use crate::verifiable::VerifierOptions; +use crate::verification::MethodScope; +use crate::verification::VerificationMethod; +use crate::Result; + +// TODO: add sign_data, split sign/verify to separate trait as first step towards +// supporting custom signature schemes. Replace DocumentSigner with trait? +// Add DocumentMut for mutable function returns? +// Blanket impl for &T, &mut T, Box etc.? +/// Common operations for DID Documents. +pub trait Document { + type D: DID; + type U; + type V; + + /// Returns a reference to the `Document` id. + fn id(&self) -> &Self::D; + + /// Returns the first [`Service`] with an `id` property matching the provided `query`, if present. + fn resolve_service<'query, 'me, Q>(&'me self, query: Q) -> Option<&Service> + where + Q: Into>; + + /// Returns the first [`VerificationMethod`] with an `id` property matching the + /// provided `query` and the verification relationship specified by `scope` if present. + fn resolve_method<'query, 'me, Q>( + &'me self, + query: Q, + scope: Option, + ) -> Option<&VerificationMethod> + where + Q: Into>; + + /// Verifies the signature of the provided data. + /// + /// # Errors + /// + /// Fails if an unsupported verification method is used, data + /// serialization fails, or the verification operation fails. + fn verify_data(&self, data: &X, options: &VerifierOptions) -> Result<()> + where + X: Serialize + GetSignature + ?Sized; +} + +impl Document for &DOC { + type D = DOC::D; + type U = DOC::U; + type V = DOC::V; + + fn id(&self) -> &Self::D { + DOC::id(self) + } + + fn resolve_service<'query, 'me, Q>(&'me self, query: Q) -> Option<&Service> + where + Q: Into>, + { + DOC::resolve_service(self, query) + } + + fn resolve_method<'query, 'me, Q>( + &'me self, + query: Q, + scope: Option, + ) -> Option<&VerificationMethod> + where + Q: Into>, + { + DOC::resolve_method(self, query, scope) + } + + fn verify_data(&self, data: &X, options: &VerifierOptions) -> Result<()> + where + X: Serialize + GetSignature + ?Sized, + { + DOC::verify_data(self, data, options) + } +} diff --git a/identity_did/src/revocation/bitmap.rs b/identity_did/src/revocation/bitmap.rs index bffd474066..70045a844a 100644 --- a/identity_did/src/revocation/bitmap.rs +++ b/identity_did/src/revocation/bitmap.rs @@ -141,14 +141,12 @@ impl RevocationBitmap { } } -impl TryFrom<&Service> for RevocationBitmap { +impl TryFrom<&Service> for RevocationBitmap { type Error = Error; - fn try_from(service: &Service) -> Result { + fn try_from(service: &Service) -> Result { if service.type_() != Self::TYPE { - return Err(Error::InvalidService( - "invalid service - expected a `RevocationBitmap2022`", - )); + return Err(Error::InvalidService("invalid type - expected `RevocationBitmap2022`")); } Self::from_endpoint(service.service_endpoint()) diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 7447e12e51..9b79dc78e2 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -48,6 +48,7 @@ pub mod credential { pub use identity_credential::credential::*; pub use identity_credential::error::*; pub use identity_credential::presentation::*; + pub use identity_credential::validator::*; } pub mod did { @@ -72,7 +73,6 @@ pub mod client { //! IOTA DID Tangle client and validators. pub use identity_iota_client::chain::*; - pub use identity_iota_client::credential::*; pub use identity_iota_client::document::*; pub use identity_iota_client::tangle::*; diff --git a/identity_iota_client/Cargo.toml b/identity_iota_client/Cargo.toml index de4826834b..c69ac99918 100644 --- a/identity_iota_client/Cargo.toml +++ b/identity_iota_client/Cargo.toml @@ -17,7 +17,7 @@ brotli = { version = "3.3", default-features = false, features = ["std"] } form_urlencoded = { version = "1.0" } futures = { version = "0.3" } identity_core = { version = "=0.6.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=0.6.0", path = "../identity_credential" } +identity_credential = { version = "=0.6.0", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=0.6.0", path = "../identity_did", default-features = false } identity_iota_core = { version = "=0.6.0", path = "../identity_iota_core", default-features = false } itertools = { version = "0.10" } @@ -26,7 +26,6 @@ log = { version = "0.4", default-features = false } num-derive = { version = "0.3", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } -serde_repr = { version = "0.1", default-features = false } strum = { version = "0.24.0", default-features = false, features = ["std", "derive"] } thiserror = { version = "1.0", default-features = false } @@ -46,14 +45,13 @@ default-features = false features = ["blake2b"] [dev-dependencies] -proptest = { version = "1.0.0", default-features = false, features = ["std"] } tokio = { version = "1.17.0", default-features = false, features = ["macros"] } [features] default = ["revocation-bitmap"] # Enables revocation with `RevocationBitmap2022`. -revocation-bitmap = ["identity_iota_core/revocation-bitmap"] +revocation-bitmap = ["identity_iota_core/revocation-bitmap", "identity_credential/revocation-bitmap"] [package.metadata.docs.rs] # To build locally: diff --git a/identity_iota_client/README.md b/identity_iota_client/README.md index 1564148a8e..76519d0801 100644 --- a/identity_iota_client/README.md +++ b/identity_iota_client/README.md @@ -5,8 +5,3 @@ This crate provides interfaces for publishing and resolving DID Documents to and - [`Client`](crate::tangle::Client) - [`Resolver`](crate::tangle::Resolver) - -Convenience methods for validating [Verifiable Credentials](https://wiki.iota.org/identity.rs/concepts/verifiable_credentials/overview) and [Verifiable Presentations](https://wiki.iota.org/identity.rs/concepts/verifiable_credentials/verifiable_presentations) are also provided: - -- [`CredentialValidator`](crate::credential::CredentialValidator) -- [`PresentationValidator`](crate::credential::PresentationValidator) diff --git a/identity_iota_client/src/error.rs b/identity_iota_client/src/error.rs index a393607ff9..4e207a68af 100644 --- a/identity_iota_client/src/error.rs +++ b/identity_iota_client/src/error.rs @@ -34,11 +34,11 @@ pub enum Error { InvalidMessageFlags, /// Caused by a single concern credential or presentation validation method failing. #[error("A validation unit failed")] - IsolatedValidationError(#[from] crate::credential::ValidationError), + IsolatedValidationError(#[from] identity_credential::validator::ValidationError), /// Caused by one or more failures when validating a credential. #[error("credential validation failed")] - CredentialValidationError(#[from] crate::credential::CompoundCredentialValidationError), + CredentialValidationError(#[from] identity_credential::validator::CompoundCredentialValidationError), /// Caused by one or more failures when validating a presentation. #[error("presentation validation failed")] - PresentationValidationError(#[from] crate::credential::CompoundPresentationValidationError), + PresentationValidationError(#[from] identity_credential::validator::CompoundPresentationValidationError), } diff --git a/identity_iota_client/src/lib.rs b/identity_iota_client/src/lib.rs index 6df6a22f3e..c89051d4ba 100644 --- a/identity_iota_client/src/lib.rs +++ b/identity_iota_client/src/lib.rs @@ -4,7 +4,6 @@ #![forbid(unsafe_code)] #![allow(deprecated)] #![doc = include_str!("./../README.md")] -#![allow(clippy::upper_case_acronyms)] #![warn( rust_2018_idioms, unreachable_pub, @@ -21,7 +20,6 @@ pub use self::error::Error; pub use self::error::Result; pub mod chain; -pub mod credential; pub mod document; pub mod tangle; diff --git a/identity_iota_client/src/tangle/resolver.rs b/identity_iota_client/src/tangle/resolver.rs index 275acc8224..db94ebccc5 100644 --- a/identity_iota_client/src/tangle/resolver.rs +++ b/identity_iota_client/src/tangle/resolver.rs @@ -5,19 +5,22 @@ use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; -use identity_core::common::Url; +use serde::Serialize; + use identity_credential::credential::Credential; use identity_credential::presentation::Presentation; +use identity_credential::validator::CredentialValidator; +use identity_credential::validator::FailFast; +use identity_credential::validator::PresentationValidationOptions; +use identity_credential::validator::PresentationValidator; +use identity_credential::validator::ValidatorDocument; use identity_iota_core::did::IotaDID; use identity_iota_core::diff::DiffMessage; +use identity_iota_core::document::IotaDocument; use identity_iota_core::tangle::NetworkName; -use serde::Serialize; use crate::chain::ChainHistory; use crate::chain::DocumentHistory; -use crate::credential::FailFast; -use crate::credential::PresentationValidationOptions; -use crate::credential::PresentationValidator; use crate::document::ResolvedIotaDocument; use crate::error::Error; use crate::error::Result; @@ -106,12 +109,7 @@ where &self, credential: &Credential, ) -> Result { - let issuer: IotaDID = IotaDID::parse(credential.issuer.url().as_str()).map_err(|error| { - Error::IsolatedValidationError(crate::credential::ValidationError::SignerUrl { - signer_ctx: crate::credential::SignerContext::Issuer, - source: error.into(), - }) - })?; + let issuer: IotaDID = CredentialValidator::extract_issuer(credential).map_err(Error::IsolatedValidationError)?; self.resolve(&issuer).await } @@ -129,14 +127,8 @@ where let issuers: HashSet = presentation .verifiable_credential .iter() - .map(|credential| IotaDID::parse(credential.issuer.url().as_str())) - .map(|url_result| { - url_result.map_err(|error| { - Error::IsolatedValidationError(crate::credential::ValidationError::SignerUrl { - signer_ctx: crate::credential::SignerContext::Issuer, - source: error.into(), - }) - }) + .map(|credential| { + CredentialValidator::extract_issuer::(credential).map_err(Error::IsolatedValidationError) }) .collect::>()?; @@ -153,15 +145,8 @@ where &self, presentation: &Presentation, ) -> Result { - let holder_url: &Url = presentation.holder.as_ref().ok_or(Error::IsolatedValidationError( - crate::credential::ValidationError::MissingPresentationHolder, - ))?; - let holder: IotaDID = IotaDID::parse(holder_url.as_str()).map_err(|error| { - Error::IsolatedValidationError(crate::credential::ValidationError::SignerUrl { - signer_ctx: crate::credential::SignerContext::Holder, - source: error.into(), - }) - })?; + let holder: IotaDID = + PresentationValidator::extract_holder(presentation).map_err(Error::IsolatedValidationError)?; self.resolve(&holder).await } @@ -186,28 +171,30 @@ where presentation: &Presentation, options: &PresentationValidationOptions, fail_fast: FailFast, - holder: Option<&ResolvedIotaDocument>, - issuers: Option<&[ResolvedIotaDocument]>, + holder: Option<&dyn ValidatorDocument>, + issuers: Option<&[&dyn ValidatorDocument]>, ) -> Result<()> { match (holder, issuers) { (Some(holder), Some(issuers)) => { - PresentationValidator::validate(presentation, holder, issuers, options, fail_fast) + PresentationValidator::validate(presentation, &holder, issuers, options, fail_fast) } (Some(holder), None) => { - let issuers: Vec = self.resolve_presentation_issuers(presentation).await?; - PresentationValidator::validate(presentation, holder, &issuers, options, fail_fast) + let resolved_issuers: Vec = self.resolve_presentation_issuers(presentation).await?; + let issuers: Vec = resolved_issuers.into_iter().map(|resolved| resolved.document).collect(); + PresentationValidator::validate(presentation, &holder, issuers.as_slice(), options, fail_fast) } (None, Some(issuers)) => { let holder: ResolvedIotaDocument = self.resolve_presentation_holder(presentation).await?; - PresentationValidator::validate(presentation, &holder, issuers, options, fail_fast) + PresentationValidator::validate(presentation, &holder.document, issuers, options, fail_fast) } (None, None) => { - let (holder, issuers): (ResolvedIotaDocument, Vec) = futures::future::try_join( + let (holder, resolved_issuers): (ResolvedIotaDocument, Vec) = futures::future::try_join( self.resolve_presentation_holder(presentation), self.resolve_presentation_issuers(presentation), ) .await?; - PresentationValidator::validate(presentation, &holder, &issuers, options, fail_fast) + let issuers: Vec = resolved_issuers.into_iter().map(|resolved| resolved.document).collect(); + PresentationValidator::validate(presentation, &holder.document, &issuers, options, fail_fast) } } .map_err(Into::into) @@ -282,3 +269,322 @@ impl TangleResolve for Resolver { self.resolve(did).await } } + +#[cfg(test)] +mod tests { + use identity_core::common::Duration; + use identity_core::common::Object; + use identity_core::common::Timestamp; + use identity_core::common::Url; + use identity_core::convert::FromJson; + use identity_core::crypto::KeyPair; + use identity_core::crypto::KeyType; + use identity_core::crypto::ProofOptions; + use identity_core::json; + use identity_core::utils::BaseEncoding; + use identity_credential::credential::Credential; + use identity_credential::credential::CredentialBuilder; + use identity_credential::credential::Subject; + use identity_credential::validator::CredentialValidationOptions; + use identity_credential::validator::SubjectHolderRelationship; + use identity_did::did::CoreDID; + use identity_did::did::DID; + use identity_did::document::CoreDocument; + use identity_did::verifiable::VerifierOptions; + use identity_did::verification::VerificationMethod; + use identity_iota_core::document::IotaDocument; + use identity_iota_core::tangle::Network; + + use super::*; + + fn generate_core_document() -> (CoreDocument, KeyPair) { + let keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); + let did: CoreDID = CoreDID::parse(&format!( + "did:example:{}", + BaseEncoding::encode_base58(keypair.public()) + )) + .unwrap(); + let document: CoreDocument = CoreDocument::builder(Object::new()) + .id(did.clone()) + .verification_method(VerificationMethod::new(did, KeyType::Ed25519, keypair.public(), "#root").unwrap()) + .build() + .unwrap(); + (document, keypair) + } + + fn generate_credential(issuer: &str, subject: &str) -> Credential { + let credential_subject: Subject = Subject::from_json_value(json!({ + "id": subject, + "name": "Alice", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + })) + .unwrap(); + + // Build credential using subject above and issuer. + CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732").unwrap()) + .issuer(Url::parse(issuer).unwrap()) + .type_("UniversityDegreeCredential") + .subject(credential_subject) + .issuance_date(Timestamp::now_utc()) + .expiration_date(Timestamp::now_utc().checked_add(Duration::days(1)).unwrap()) + .build() + .unwrap() + } + + fn generate_presentation(holder: &str, credentials: Vec) -> Presentation { + let mut builder = Presentation::builder(Object::new()) + .id(Url::parse("https://example.org/credentials/3732").unwrap()) + .holder(Url::parse(holder).unwrap()); + for credential in credentials { + builder = builder.credential(credential); + } + builder.build().unwrap() + } + + // Convenience struct for setting up tests. + struct MixedTestSetup { + // Issuer of credential_iota. + issuer_iota_doc: IotaDocument, + issuer_iota_key: KeyPair, + credential_iota: Credential, + // Issuer of credential_core. + issuer_core_doc: CoreDocument, + issuer_core_key: KeyPair, + credential_core: Credential, + // Subject of both credentials. + subject_doc: CoreDocument, + subject_key: KeyPair, + } + + impl MixedTestSetup { + // Creates DID Documents and unsigned credentials. + fn new() -> Self { + let (issuer_iota_doc, issuer_iota_key) = { + let keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); + (IotaDocument::new(&keypair).unwrap(), keypair) + }; + let (subject_doc, subject_key) = generate_core_document(); + let credential_iota = generate_credential(issuer_iota_doc.id().as_str(), subject_doc.id().as_str()); + + let (issuer_core_doc, issuer_core_key) = generate_core_document(); + let credential_core = generate_credential(issuer_core_doc.id().as_str(), subject_doc.id().as_str()); + + Self { + issuer_iota_doc, + issuer_iota_key, + subject_doc, + subject_key, + credential_iota, + issuer_core_doc, + issuer_core_key, + credential_core, + } + } + + // Creates DID Documents with signed credentials. + fn new_with_signed_credentials() -> Self { + let mut setup = Self::new(); + let MixedTestSetup { + ref issuer_iota_doc, + ref issuer_iota_key, + ref mut credential_iota, + ref issuer_core_doc, + ref issuer_core_key, + ref mut credential_core, + .. + } = setup; + + issuer_iota_doc + .sign_data( + credential_iota, + issuer_iota_key.private(), + issuer_iota_doc.default_signing_method().unwrap().id(), + ProofOptions::default(), + ) + .unwrap(); + issuer_core_doc + .signer(issuer_core_key.private()) + .options(ProofOptions::default()) + .method(issuer_core_doc.methods().next().unwrap().id()) + .sign(credential_core) + .unwrap(); + setup + } + } + + #[tokio::test] + async fn test_resolver_verify_presentation_mixed() { + let MixedTestSetup { + issuer_iota_doc, + credential_iota, + issuer_core_doc, + credential_core, + subject_doc, + subject_key, + .. + } = MixedTestSetup::new_with_signed_credentials(); + + // Subject signs the presentation. + let mut presentation = + generate_presentation(subject_doc.id().as_str(), [credential_iota, credential_core].to_vec()); + let challenge: String = "475a7984-1bb5-4c4c-a56f-822bccd46441".to_owned(); + subject_doc + .signer(subject_key.private()) + .options(ProofOptions::new().challenge(challenge.clone())) + .method(subject_doc.methods().next().unwrap().id()) + .sign(&mut presentation) + .unwrap(); + + // VALID: resolver supports presentations with issuers from different DID Methods. + let resolver: Resolver = Resolver::>::builder() + .client_builder(Client::builder().network(Network::Devnet).node_sync_disabled()) + .build() + .await + .unwrap(); + assert!(resolver + .verify_presentation( + &presentation, + &PresentationValidationOptions::new() + .presentation_verifier_options(VerifierOptions::new().challenge(challenge)) + .subject_holder_relationship(SubjectHolderRelationship::AlwaysSubject), + FailFast::FirstError, + Some(&subject_doc), + Some(&[issuer_iota_doc.as_validator(), issuer_core_doc.as_validator()]) + ) + .await + .is_ok()); + } + + #[test] + fn test_validate_presentation_mixed() { + let MixedTestSetup { + issuer_iota_doc, + credential_iota, + issuer_core_doc, + credential_core, + subject_doc, + subject_key, + .. + } = MixedTestSetup::new_with_signed_credentials(); + + // Subject signs the presentation. + let mut presentation = + generate_presentation(subject_doc.id().as_str(), [credential_iota, credential_core].to_vec()); + let challenge: String = "475a7984-1bb5-4c4c-a56f-822bccd46440".to_owned(); + subject_doc + .signer(subject_key.private()) + .options(ProofOptions::new().challenge(challenge.clone())) + .method(subject_doc.methods().next().unwrap().id()) + .sign(&mut presentation) + .unwrap(); + + // Validate presentation. + let presentation_validation_options = PresentationValidationOptions::new() + .shared_validation_options( + CredentialValidationOptions::new() + .earliest_expiry_date(Timestamp::now_utc().checked_add(Duration::days(1)).unwrap()) + .latest_issuance_date(Timestamp::now_utc()), + ) + .presentation_verifier_options(VerifierOptions::new().challenge(challenge)) + .subject_holder_relationship(SubjectHolderRelationship::AlwaysSubject); + + // VALID: presentations with issuers from different DID Methods are supported. + assert!(PresentationValidator::validate( + &presentation, + &subject_doc, + &[issuer_iota_doc.as_validator(), issuer_core_doc.as_validator()], + &presentation_validation_options, + FailFast::FirstError, + ) + .is_ok()); + + // INVALID: wrong holder fails. + assert!(PresentationValidator::validate( + &presentation, + &issuer_iota_doc, + &[issuer_iota_doc.as_validator(), issuer_core_doc.as_validator()], + &presentation_validation_options, + FailFast::FirstError, + ) + .is_err()); + assert!(PresentationValidator::validate( + &presentation, + &issuer_core_doc, + &[issuer_iota_doc.as_validator(), issuer_core_doc.as_validator()], + &presentation_validation_options, + FailFast::FirstError, + ) + .is_err()); + + // INVALID: excluding the IOTA DID Document issuer fails. + assert!(PresentationValidator::validate( + &presentation, + &subject_doc, + &[issuer_core_doc.as_validator()], + &presentation_validation_options, + FailFast::FirstError, + ) + .is_err()); + + // INVALID: excluding the core DID Document issuer fails. + assert!(PresentationValidator::validate( + &presentation, + &subject_doc, + &[&issuer_iota_doc], + &presentation_validation_options, + FailFast::FirstError, + ) + .is_err()); + + // INVALID: using the wrong core DID Document fails. + assert!(PresentationValidator::validate( + &presentation, + &subject_doc, + &[issuer_iota_doc.as_validator(), subject_doc.as_validator()], + &presentation_validation_options, + FailFast::FirstError, + ) + .is_err()); + + // INVALID: excluding all issuers fails. + let empty_issuers: &[&dyn ValidatorDocument] = &[]; + assert!(PresentationValidator::validate( + &presentation, + &subject_doc, + empty_issuers, + &presentation_validation_options, + FailFast::FirstError, + ) + .is_err()); + } + + #[test] + fn test_validate_credential_mixed() { + let MixedTestSetup { + issuer_iota_doc, + credential_iota, + issuer_core_doc, + credential_core, + subject_doc, + .. + } = MixedTestSetup::new_with_signed_credentials(); + let options = CredentialValidationOptions::new() + .earliest_expiry_date(Timestamp::now_utc().checked_add(Duration::days(1)).unwrap()) + .latest_issuance_date(Timestamp::now_utc()); + + // VALID: credential validation works for issuers with different DID Methods. + assert!(CredentialValidator::validate(&credential_iota, &issuer_iota_doc, &options, FailFast::FirstError).is_ok()); + assert!(CredentialValidator::validate(&credential_core, &issuer_core_doc, &options, FailFast::FirstError).is_ok()); + + // INVALID: wrong issuer fails. + assert!(CredentialValidator::validate(&credential_iota, &issuer_core_doc, &options, FailFast::FirstError).is_err()); + assert!(CredentialValidator::validate(&credential_iota, &subject_doc, &options, FailFast::FirstError).is_err()); + assert!(CredentialValidator::validate(&credential_core, &issuer_iota_doc, &options, FailFast::FirstError).is_err()); + assert!(CredentialValidator::validate(&credential_core, &subject_doc, &options, FailFast::FirstError).is_err()); + } +} diff --git a/identity_iota_core/src/document/iota_document.rs b/identity_iota_core/src/document/iota_document.rs index 875e98008d..9ed8315efa 100644 --- a/identity_iota_core/src/document/iota_document.rs +++ b/identity_iota_core/src/document/iota_document.rs @@ -5,6 +5,10 @@ use core::fmt; use core::fmt::Debug; use core::fmt::Display; +use serde; +use serde::Deserialize; +use serde::Serialize; + use identity_core::common::Object; use identity_core::common::OneOrSet; use identity_core::common::OrderedSet; @@ -22,6 +26,7 @@ use identity_core::crypto::PublicKey; use identity_core::crypto::SetSignature; use identity_core::crypto::Signer; use identity_did::document::CoreDocument; +use identity_did::document::Document; use identity_did::service::Service; use identity_did::utils::DIDUrlQuery; use identity_did::verifiable::DocumentSigner; @@ -33,9 +38,6 @@ use identity_did::verification::MethodType; use identity_did::verification::MethodUriType; use identity_did::verification::TryMethod; use identity_did::verification::VerificationMethod; -use serde; -use serde::Deserialize; -use serde::Serialize; use crate::did::IotaDID; use crate::did::IotaDIDUrl; @@ -448,7 +450,7 @@ impl IotaDocument { /// serialization fails, or the verification operation fails. pub fn verify_data(&self, data: &X, options: &VerifierOptions) -> Result<()> where - X: Serialize + GetSignature, + X: Serialize + GetSignature + ?Sized, { self.document.verify_data(data, options).map_err(Into::into) } @@ -619,59 +621,79 @@ impl IotaDocument { } } +impl Document for IotaDocument { + type D = IotaDID; + type U = Object; + type V = Object; + + fn id(&self) -> &Self::D { + IotaDocument::id(self) + } + + fn resolve_service<'query, 'me, Q>(&'me self, query: Q) -> Option<&Service> + where + Q: Into>, + { + self.core_document().resolve_service(query) + } + + fn resolve_method<'query, 'me, Q>( + &'me self, + query: Q, + scope: Option, + ) -> Option<&VerificationMethod> + where + Q: Into>, + { + self.core_document().resolve_method(query, scope) + } + + fn verify_data(&self, data: &X, options: &VerifierOptions) -> identity_did::Result<()> + where + X: Serialize + GetSignature + ?Sized, + { + self.core_document().verify_data(data, options) + } +} + #[cfg(feature = "revocation-bitmap")] mod iota_document_revocation { - use super::IotaDocument; - use crate::did::IotaDID; - use crate::did::IotaDIDUrl; + use identity_did::utils::DIDUrlQuery; + use crate::Error; use crate::Result; - use identity_did::revocation::RevocationBitmap; - use identity_did::service::Service; - impl IotaDocument { - /// If the document has a [`RevocationBitmap`] service identified by `fragment`, - /// revoke all credentials with a `revocationBitmapIndex` in `credential_indices`. - pub fn revoke_credentials(&mut self, service_id: &IotaDIDUrl, credential_indices: &[u32]) -> Result<()> { - self.update_revocation_bitmap(service_id, |revocation_bitmap| { - // Revoke all given credential indices. - for credential in credential_indices { - revocation_bitmap.revoke(*credential); - } - }) - } + use super::IotaDocument; - /// If the document has a [`RevocationBitmap`] service identified by `fragment`, - /// unrevoke all credentials with a `revocationBitmapIndex` in `credential_indices`. - pub fn unrevoke_credentials(&mut self, service_id: &IotaDIDUrl, credential_indices: &[u32]) -> Result<()> { - self.update_revocation_bitmap(service_id, |revocation_bitmap| { - // Unrevoke all given credential indices. - for credential in credential_indices { - revocation_bitmap.unrevoke(*credential); - } - }) + impl IotaDocument { + /// If the document has a [`RevocationBitmap`](identity_did::revocation::RevocationBitmap) + /// service identified by `service_query`, revoke all credentials with a + /// `revocationBitmapIndex` in `credential_indices`. + pub fn revoke_credentials<'query, 'me, Q>(&mut self, service_query: Q, credential_indices: &[u32]) -> Result<()> + where + Q: Into>, + { + self + .core_document_mut() + .revoke_credentials(service_query, credential_indices) + .map_err(Error::RevocationError) } - fn update_revocation_bitmap(&mut self, service_id: &IotaDIDUrl, f: F) -> Result<()> + /// If the document has a [`RevocationBitmap`](identity_did::revocation::RevocationBitmap) + /// service with an id by `service_query`, unrevoke all credentials with a + /// `revocationBitmapIndex` in `credential_indices`. + pub fn unrevoke_credentials<'query, 'me, Q>( + &'me mut self, + service_query: Q, + credential_indices: &[u32], + ) -> Result<()> where - F: FnOnce(&mut RevocationBitmap), + Q: Into>, { - let service: &mut Service = self + self .core_document_mut() - .service_mut() - .iter_mut_unchecked() - .find(|service| service.id() == service_id) - .ok_or(Error::RevocationError(identity_did::Error::InvalidService( - "invalid id - service not found", - )))?; - - let mut revocation_bitmap: RevocationBitmap = (&*service).try_into().map_err(Error::RevocationError)?; - - f(&mut revocation_bitmap); - - std::mem::swap(service.service_endpoint_mut(), &mut revocation_bitmap.to_endpoint()?); - - Ok(()) + .unrevoke_credentials(service_query, credential_indices) + .map_err(Error::RevocationError) } } }