From 9ea2cf3a17dfd799e9b90c1970caf7923c74e252 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Wed, 17 May 2023 01:23:57 +0200 Subject: [PATCH 01/31] add JwtPresentation --- identity_credential/src/credential/jwt.rs | 2 +- .../src/credential/jwt_serialization.rs | 4 +- identity_credential/src/error.rs | 2 + .../src/presentation/jwt_presentation.rs | 236 ++++++++++++++++++ .../presentation/jwt_presentation_builder.rs | 234 +++++++++++++++++ .../presentation/jwt_presentation_options.rs | 22 ++ .../src/presentation/jwt_serialization.rs | 122 +++++++++ identity_credential/src/presentation/mod.rs | 7 + .../presentation_jwt_validator.rs | 4 + .../src/storage/jwk_storage_document_ext.rs | 73 +++++- 10 files changed, 702 insertions(+), 4 deletions(-) create mode 100644 identity_credential/src/presentation/jwt_presentation.rs create mode 100644 identity_credential/src/presentation/jwt_presentation_builder.rs create mode 100644 identity_credential/src/presentation/jwt_presentation_options.rs create mode 100644 identity_credential/src/presentation/jwt_serialization.rs create mode 100644 identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs diff --git a/identity_credential/src/credential/jwt.rs b/identity_credential/src/credential/jwt.rs index b5f78df06c..ab42844340 100644 --- a/identity_credential/src/credential/jwt.rs +++ b/identity_credential/src/credential/jwt.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /// A wrapper around a JSON Web Token (JWK). -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] pub struct Jwt(String); impl Jwt { diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index 4eb775dd9b..ff35da9e77 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -231,7 +231,7 @@ where /// but `iat` is also used in the ecosystem. This type aims to take care of this discrepancy on /// a best effort basis. #[derive(Serialize, Deserialize, Clone, Copy)] -struct IssuanceDateClaims { +pub(crate) struct IssuanceDateClaims { #[serde(skip_serializing_if = "Option::is_none")] iat: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -239,7 +239,7 @@ struct IssuanceDateClaims { } impl IssuanceDateClaims { - fn new(issuance_date: Timestamp) -> Self { + pub(crate) fn new(issuance_date: Timestamp) -> Self { Self { iat: None, nbf: Some(issuance_date.to_unix()), diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 797eda90c6..caf35174ea 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -18,6 +18,8 @@ pub enum Error { /// Caused when constructing a credential without an issuer. #[error("missing credential issuer")] MissingIssuer, + #[error("missing presentation holder")] + MissingHolder, /// Caused when constructing a credential without a subject. #[error("missing credential subject")] MissingSubject, diff --git a/identity_credential/src/presentation/jwt_presentation.rs b/identity_credential/src/presentation/jwt_presentation.rs new file mode 100644 index 0000000000..2d621ab475 --- /dev/null +++ b/identity_credential/src/presentation/jwt_presentation.rs @@ -0,0 +1,236 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use core::fmt::Display; +use core::fmt::Formatter; + +use identity_core::convert::ToJson; +use serde::Serialize; + +use identity_core::common::Context; +use identity_core::common::Object; +use identity_core::common::OneOrMany; +use identity_core::common::Url; +use identity_core::convert::FmtJson; +use identity_core::crypto::GetSignature; +use identity_core::crypto::GetSignatureMut; +use identity_core::crypto::Proof; +use identity_core::crypto::SetSignature; +use identity_verification::MethodUriType; +use identity_verification::TryMethod; + +use crate::credential::Credential; +use crate::credential::Jwt; +use crate::credential::Policy; +use crate::credential::RefreshService; +use crate::error::Error; +use crate::error::Result; +use crate::presentation::PresentationBuilder; + +use super::jwt_serialization::PresentationJwtClaims; +use super::JwtPresentationBuilder; +use super::JwtPresentationOptions; + +/// Represents a bundle of one or more [Credential]s. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct JwtPresentation { + /// The JSON-LD context(s) applicable to the `Presentation`. + #[serde(rename = "@context")] + pub context: OneOrMany, + /// A unique `URI` that may be used to identify the `Presentation`. + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// One or more URIs defining the type of the `Presentation`. + #[serde(rename = "type")] + pub types: OneOrMany, + /// Credential(s) expressing the claims of the `Presentation`. + #[serde(default = "Default::default", rename = "verifiableCredential")] + pub verifiable_credential: OneOrMany, + /// The entity that generated the `Presentation`. + #[serde(skip_serializing_if = "Option::is_none")] + pub holder: Option, + /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. + #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] + pub refresh_service: OneOrMany, + /// Terms-of-use specified by the `Presentation` holder. + #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")] + pub terms_of_use: OneOrMany, + /// Miscellaneous properties. + #[serde(flatten)] + pub properties: T, + /// Proof(s) used to verify a `Presentation` + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, +} + +impl JwtPresentation { + /// Returns the base JSON-LD context for `Presentation`s. + pub fn base_context() -> &'static Context { + Credential::::base_context() + } + + /// Returns the base type for `Presentation`s. + pub const fn base_type() -> &'static str { + "VerifiablePresentation" + } + + /// Creates a `PresentationBuilder` to configure a new Presentation. + /// + /// This is the same as [PresentationBuilder::new]. + pub fn builder(properties: T) -> PresentationBuilder { + PresentationBuilder::new(properties) + } + + /// Returns a new `Presentation` based on the `PresentationBuilder` configuration. + pub fn from_builder(builder: JwtPresentationBuilder) -> Result { + let this: Self = Self { + context: builder.context.into(), + id: builder.id, + types: builder.types.into(), + verifiable_credential: builder.credentials.into(), + holder: builder.holder, + refresh_service: builder.refresh_service.into(), + terms_of_use: builder.terms_of_use.into(), + properties: builder.properties, + proof: None, + }; + + this.check_structure()?; + + Ok(this) + } + + /// Validates the semantic structure of the `Presentation`. + pub fn check_structure(&self) -> Result<()> { + // Ensure the base context is present and in the correct location + match self.context.get(0) { + Some(context) if context == Self::base_context() => {} + Some(_) | None => return Err(Error::MissingBaseContext), + } + + // The set of types MUST contain the base type + if !self.types.iter().any(|type_| type_ == Self::base_type()) { + return Err(Error::MissingBaseType); + } + + // Check all credentials. + // for credential in self.verifiable_credential.iter() { + // credential.check_structure()?; + // } + + Ok(()) + } + + /// Serializes the [`Credential`] as a JWT claims set + /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). + /// + /// The resulting string can be used as the payload of a JWS when issuing the credential. + pub fn serialize_jwt(&self, options: &JwtPresentationOptions) -> Result + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + let jwt_representation: PresentationJwtClaims<'_, T> = PresentationJwtClaims::new(self, options)?; + jwt_representation + .to_json() + .map_err(|err| Error::JwtClaimsSetSerializationError(err.into())) + } + + /// Returns a reference to the `Presentation` proof. + pub fn proof(&self) -> Option<&Proof> { + self.proof.as_ref() + } + + /// Returns a mutable reference to the `Presentation` proof. + pub fn proof_mut(&mut self) -> Option<&mut Proof> { + self.proof.as_mut() + } +} + +impl Display for JwtPresentation +where + T: Serialize, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + self.fmt_json(f) + } +} +// +// impl GetSignature for JwtPresentation { +// fn signature(&self) -> Option<&Proof> { +// self.proof.as_ref() +// } +// } +// +// impl GetSignatureMut for JwtPresentation { +// fn signature_mut(&mut self) -> Option<&mut Proof> { +// self.proof.as_mut() +// } +// } +// +// impl SetSignature for JwtPresentation { +// fn set_signature(&mut self, value: Proof) { +// self.proof.replace(value); +// } +// } + +impl TryMethod for JwtPresentation { + const TYPE: MethodUriType = MethodUriType::Absolute; +} + +#[cfg(test)] +mod tests { + use identity_core::convert::FromJson; + + use crate::credential::Credential; + use crate::credential::Subject; + + use super::JwtPresentation; + + const JSON: &str = include_str!("../../tests/fixtures/presentation-1.json"); + + // #[test] + // fn test_from_json() { + // let presentation: JwtPresentation = JwtPresentation::from_json(JSON).unwrap(); + // let credential: &Credential = presentation.verifiable_credential.get(0).unwrap(); + // let subject: &Subject = credential.credential_subject.get(0).unwrap(); + // + // assert_eq!( + // presentation.context.as_slice(), + // [ + // "https://www.w3.org/2018/credentials/v1", + // "https://www.w3.org/2018/credentials/examples/v1" + // ] + // ); + // assert_eq!( + // presentation.id.as_ref().unwrap(), + // "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5" + // ); + // assert_eq!( + // presentation.types.as_slice(), + // ["VerifiablePresentation", "CredentialManagerPresentation"] + // ); + // assert_eq!(presentation.proof().unwrap().type_(), "RsaSignature2018"); + // assert_eq!( + // credential.context.as_slice(), + // [ + // "https://www.w3.org/2018/credentials/v1", + // "https://www.w3.org/2018/credentials/examples/v1" + // ] + // ); + // assert_eq!(credential.id.as_ref().unwrap(), "http://example.edu/credentials/3732"); + // assert_eq!( + // credential.types.as_slice(), + // ["VerifiableCredential", "UniversityDegreeCredential"] + // ); + // assert_eq!(credential.issuer.url(), "https://example.edu/issuers/14"); + // assert_eq!(credential.issuance_date, "2010-01-01T19:23:24Z".parse().unwrap()); + // assert_eq!(credential.proof().unwrap().type_(), "RsaSignature2018"); + // + // assert_eq!(subject.id.as_ref().unwrap(), "did:example:ebfeb1f712ebc6f1c276e12ec21"); + // assert_eq!(subject.properties["degree"]["type"], "BachelorDegree"); + // assert_eq!( + // subject.properties["degree"]["name"], + // "Bachelor of Science in Mechanical Engineering" + // ); + // } +} diff --git a/identity_credential/src/presentation/jwt_presentation_builder.rs b/identity_credential/src/presentation/jwt_presentation_builder.rs new file mode 100644 index 0000000000..096f5c233f --- /dev/null +++ b/identity_credential/src/presentation/jwt_presentation_builder.rs @@ -0,0 +1,234 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Context; +use identity_core::common::Object; +use identity_core::common::Url; +use identity_core::common::Value; + +use crate::credential::Credential; +use crate::credential::Jwt; +use crate::credential::Policy; +use crate::credential::RefreshService; +use crate::error::Result; +use crate::presentation::Presentation; + +use super::JwtPresentation; + +/// A `PresentationBuilder` is used to create a customized [Presentation]. +#[derive(Clone, Debug)] +pub struct JwtPresentationBuilder { + pub(crate) context: Vec, + pub(crate) id: Option, + pub(crate) types: Vec, + pub(crate) credentials: Vec, + pub(crate) holder: Option, + pub(crate) refresh_service: Vec, + pub(crate) terms_of_use: Vec, + pub(crate) properties: T, +} + +impl JwtPresentationBuilder { + /// Creates a new `PresentationBuilder`. + pub fn new(properties: T) -> Self { + Self { + context: vec![Presentation::::base_context().clone()], + id: None, + types: vec![Presentation::::base_type().into()], + credentials: Vec::new(), + holder: None, + refresh_service: Vec::new(), + terms_of_use: Vec::new(), + properties, + } + } + + /// Adds a value to the `context` set. + #[must_use] + pub fn context(mut self, value: impl Into) -> Self { + self.context.push(value.into()); + self + } + + /// Sets the unique identifier of the presentation. + #[must_use] + pub fn id(mut self, value: Url) -> Self { + self.id = Some(value); + self + } + + /// Adds a value to the `type` set. + #[must_use] + pub fn type_(mut self, value: impl Into) -> Self { + self.types.push(value.into()); + self + } + + /// Adds a value to the `verifiableCredential` set. + #[must_use] + pub fn credential(mut self, value: Jwt) -> Self { + self.credentials.push(value); + self + } + + /// Sets the value of the `holder`. + #[must_use] + pub fn holder(mut self, value: Url) -> Self { + self.holder = Some(value); + self + } + + /// Adds a value to the `refreshService` set. + #[must_use] + pub fn refresh_service(mut self, value: RefreshService) -> Self { + self.refresh_service.push(value); + self + } + + /// Adds a value to the `termsOfUse` set. + #[must_use] + pub fn terms_of_use(mut self, value: Policy) -> Self { + self.terms_of_use.push(value); + self + } + + /// Returns a new `Presentation` based on the `PresentationBuilder` configuration. + pub fn build(self) -> Result> { + JwtPresentation::from_builder(self) + } +} + +impl JwtPresentationBuilder { + /// Adds a new custom property. + #[must_use] + pub fn property(mut self, key: K, value: V) -> Self + where + K: Into, + V: Into, + { + self.properties.insert(key.into(), value.into()); + self + } + + /// Adds a series of custom properties. + #[must_use] + pub fn properties(mut self, iter: I) -> Self + where + I: IntoIterator, + K: Into, + V: Into, + { + self + .properties + .extend(iter.into_iter().map(|(k, v)| (k.into(), v.into()))); + self + } +} + +impl Default for JwtPresentationBuilder +where + T: Default, +{ + fn default() -> Self { + Self::new(T::default()) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use serde_json::Value; + + use identity_core::common::Object; + use identity_core::common::Url; + use identity_core::convert::FromJson; + use identity_core::crypto::KeyPair; + use identity_core::crypto::KeyType; + use identity_did::CoreDID; + use identity_did::DID; + use identity_document::document::CoreDocument; + use identity_document::document::DocumentBuilder; + use identity_verification::MethodBuilder; + use identity_verification::MethodData; + use identity_verification::MethodType; + use identity_verification::VerificationMethod; + + use crate::credential::Credential; + use crate::credential::CredentialBuilder; + use crate::credential::Subject; + use crate::presentation::Presentation; + use crate::presentation::PresentationBuilder; + + fn subject() -> Subject { + let json: Value = json!({ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + }); + + Subject::from_json_value(json).unwrap() + } + + fn issuer() -> Url { + Url::parse("did:example:issuer").unwrap() + } + + #[test] + fn test_presentation_builder_valid() { + let keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); + let controller: CoreDID = "did:example:1234".parse().unwrap(); + + let method: VerificationMethod = MethodBuilder::default() + .id(controller.to_url().join("#key-1").unwrap()) + .controller(controller.clone()) + .type_(MethodType::ED25519_VERIFICATION_KEY_2018) + .data(MethodData::new_multibase(keypair.public())) + .build() + .unwrap(); + + let document: CoreDocument = DocumentBuilder::default() + .id(controller) + .verification_method(method) + .build() + .unwrap(); + + let mut credential: Credential = CredentialBuilder::default() + .type_("ExampleCredential") + .subject(subject()) + .issuer(issuer()) + .build() + .unwrap(); + + document + .signer(keypair.private()) + .method("#key-1") + .sign(&mut credential) + .unwrap(); + + let presentation: Presentation = PresentationBuilder::default() + .type_("ExamplePresentation") + .credential(credential) + .build() + .unwrap(); + + assert_eq!(presentation.context.len(), 1); + assert_eq!( + presentation.context.get(0).unwrap(), + Presentation::::base_context() + ); + assert_eq!(presentation.types.len(), 2); + assert_eq!(presentation.types.get(0).unwrap(), Presentation::::base_type()); + assert_eq!(presentation.types.get(1).unwrap(), "ExamplePresentation"); + assert_eq!(presentation.verifiable_credential.len(), 1); + assert_eq!( + presentation.verifiable_credential.get(0).unwrap().types.get(0).unwrap(), + Credential::::base_type() + ); + assert_eq!( + presentation.verifiable_credential.get(0).unwrap().types.get(1).unwrap(), + "ExampleCredential" + ); + } +} diff --git a/identity_credential/src/presentation/jwt_presentation_options.rs b/identity_credential/src/presentation/jwt_presentation_options.rs new file mode 100644 index 0000000000..2f9c9a9d05 --- /dev/null +++ b/identity_credential/src/presentation/jwt_presentation_options.rs @@ -0,0 +1,22 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Timestamp; +use identity_core::common::Url; + +#[derive(Clone, Debug)] +pub struct JwtPresentationOptions { + pub expiration_date: Option, + pub issuance_date: Option, + pub audience: Option, +} + +impl Default for JwtPresentationOptions { + fn default() -> Self { + Self { + expiration_date: None, + issuance_date: Some(Timestamp::now_utc()), + audience: None, + } + } +} diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs new file mode 100644 index 0000000000..5c17dbcbbb --- /dev/null +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -0,0 +1,122 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::borrow::Cow; + +use serde::Deserialize; +use serde::Serialize; + +use identity_core::common::Context; +use identity_core::common::Object; +use identity_core::common::OneOrMany; +use identity_core::common::Url; +use identity_core::crypto::Proof; +use serde::de::DeserializeOwned; + +use crate::credential::IssuanceDateClaims; +use crate::credential::Jwt; +use crate::credential::Policy; +use crate::credential::RefreshService; +use crate::presentation::JwtPresentation; +use crate::Error; +use crate::Result; + +use super::JwtPresentationOptions; + +#[derive(Serialize, Deserialize)] +pub(crate) struct PresentationJwtClaims<'presentation, T = Object> +where + T: ToOwned + Serialize, + ::Owned: DeserializeOwned, +{ + /// Represents the expirationDate encoded as a UNIX timestamp. + #[serde(skip_serializing_if = "Option::is_none")] + exp: Option, + /// Represents the issuer of the presentation who is the same as the holder of the verifiable + /// credentials. + iss: Cow<'presentation, Url>, + + /// Represents the issuanceDate encoded as a UNIX timestamp. + #[serde(flatten)] + issuance_date: Option, + + /// Represents the id property of the credential. + #[serde(skip_serializing_if = "Option::is_none")] + jti: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + aud: Option, + + vp: InnerPresentation<'presentation, T>, +} + +impl<'presentation, T> PresentationJwtClaims<'presentation, T> +where + T: ToOwned + Serialize + DeserializeOwned, +{ + pub(super) fn new(presentation: &'presentation JwtPresentation, options: &JwtPresentationOptions) -> Result { + let JwtPresentation { + context, + id, + types, + verifiable_credential, + holder: Some(holder_url), + refresh_service, + terms_of_use, + properties, + proof + } = presentation else { + return Err(Error::MissingHolder) + }; + + Ok(Self { + iss: Cow::Borrowed(holder_url), + jti: id.as_ref().map(Cow::Borrowed), + vp: InnerPresentation { + context: Cow::Borrowed(context), + id: None, + types: Cow::Borrowed(types), + verifiable_credential: Cow::Borrowed(verifiable_credential), + refresh_service: Cow::Borrowed(refresh_service), + terms_of_use: Cow::Borrowed(terms_of_use), + properties: Cow::Borrowed(properties), + proof: proof.as_ref().map(Cow::Borrowed), + }, + exp: options.expiration_date.map(|expiration_date| expiration_date.to_unix()), + issuance_date: options.issuance_date.map(IssuanceDateClaims::new), + aud: options.audience.clone(), + }) + } +} + +#[derive(Serialize, Deserialize)] +struct InnerPresentation<'presentation, T = Object> +where + T: ToOwned + Serialize, + ::Owned: DeserializeOwned, +{ + /// The JSON-LD context(s) applicable to the `Presentation`. + #[serde(rename = "@context")] + context: Cow<'presentation, OneOrMany>, + /// A unique `URI` that may be used to identify the `Presentation`. + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, + /// One or more URIs defining the type of the `Presentation`. + #[serde(rename = "type")] + types: Cow<'presentation, OneOrMany>, + /// Credential(s) expressing the claims of the `Presentation`. + #[serde(default = "Default::default", rename = "verifiableCredential")] + verifiable_credential: Cow<'presentation, OneOrMany>, + /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. + #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] + refresh_service: Cow<'presentation, OneOrMany>, + /// Terms-of-use specified by the `Presentation` holder. + #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")] + terms_of_use: Cow<'presentation, OneOrMany>, + /// Miscellaneous properties. + #[serde(flatten)] + properties: Cow<'presentation, T>, + /// Proof(s) used to verify a `Presentation` + #[serde(skip_serializing_if = "Option::is_none")] + proof: Option>, +} diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs index 7eaf3b420e..4303fbcd7c 100644 --- a/identity_credential/src/presentation/mod.rs +++ b/identity_credential/src/presentation/mod.rs @@ -6,7 +6,14 @@ #![allow(clippy::module_inception)] mod builder; +mod jwt_presentation; +mod jwt_presentation_builder; +mod jwt_presentation_options; +mod jwt_serialization; mod presentation; pub use self::builder::PresentationBuilder; +pub use self::jwt_presentation::JwtPresentation; +pub use self::jwt_presentation_builder::JwtPresentationBuilder; +pub use self::jwt_presentation_options::JwtPresentationOptions; pub use self::presentation::Presentation; diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs new file mode 100644 index 0000000000..d60e7ce787 --- /dev/null +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -0,0 +1,4 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +pub struct PresentationJwtValidator {} diff --git a/identity_storage/src/storage/jwk_storage_document_ext.rs b/identity_storage/src/storage/jwk_storage_document_ext.rs index 2b5f31b292..f1b22502b5 100644 --- a/identity_storage/src/storage/jwk_storage_document_ext.rs +++ b/identity_storage/src/storage/jwk_storage_document_ext.rs @@ -18,6 +18,8 @@ use async_trait::async_trait; use identity_credential::credential::Credential; use identity_credential::credential::Jws; use identity_credential::credential::Jwt; +use identity_credential::presentation::JwtPresentation; +use identity_credential::presentation::JwtPresentationOptions; use identity_did::DIDUrl; use identity_document::document::CoreDocument; use identity_verification::jose::jws::CompactJwsEncoder; @@ -88,8 +90,25 @@ pub trait JwkStorageDocumentExt: private::Sealed { K: JwkStorage, I: KeyIdStorage, T: ToOwned + Serialize + DeserializeOwned + Sync; -} + /// Produces a JWS where the payload is produced from the given `presentation` + /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). + /// + /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be + /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + async fn sign_presentation( + &self, + presentation: &JwtPresentation, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + jwt_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStorage, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync; +} mod private { pub trait Sealed {} impl Sealed for identity_document::document::CoreDocument {} @@ -396,6 +415,40 @@ impl JwkStorageDocumentExt for CoreDocument { .await .map(|jws| Jwt::new(jws.into())) } + + async fn sign_presentation( + &self, + presentation: &JwtPresentation, + storage: &Storage, + fragment: &str, + jws_options: &JwsSignatureOptions, + jwt_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStorage, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + if jws_options.detached_payload { + return Err(Error::EncodingError(Box::::from( + "cannot use detached payload for presentation signing", + ))); + } + + if !jws_options.b64.unwrap_or(true) { + // JWTs should not have `b64` set per https://datatracker.ietf.org/doc/html/rfc7797#section-7. + return Err(Error::EncodingError(Box::::from( + "cannot use `b64 = false` with JWTs", + ))); + } + let payload = presentation + .serialize_jwt(jwt_options) + .map_err(Error::ClaimsSerializationError)?; + self + .sign_bytes(storage, fragment, payload.as_bytes(), jws_options) + .await + .map(|jws| Jwt::new(jws.into())) + } } /// Attempt to revert key generation if this succeeds the original `source_error` is returned, @@ -489,5 +542,23 @@ mod iota_document { .sign_credential(credential, storage, fragment, options) .await } + async fn sign_presentation( + &self, + presentation: &JwtPresentation, + storage: &Storage, + fragment: &str, + options: &JwsSignatureOptions, + jwt_options: &JwtPresentationOptions, + ) -> StorageResult + where + K: JwkStorage, + I: KeyIdStorage, + T: ToOwned + Serialize + DeserializeOwned + Sync, + { + self + .core_document() + .sign_presentation(presentation, storage, fragment, options, jwt_options) + .await + } } } From 141595708fd4cbfa977612e1e7b491f497d18c15 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 22 May 2023 15:49:10 +0200 Subject: [PATCH 02/31] work on jwt_serialization --- .../src/credential/jwt_serialization.rs | 2 +- .../src/presentation/jwt_serialization.rs | 14 ++++ identity_credential/src/presentation/mod.rs | 3 + identity_credential/src/validator/mod.rs | 2 + .../decoded_jwt_presentation.rs | 19 +++++ .../src/validator/vp_jwt_validation/error.rs | 49 ++++++++++++ .../src/validator/vp_jwt_validation/mod.rs | 12 +++ .../presentation_jwt_validation_options.rs | 78 +++++++++++++++++++ .../presentation_jwt_validator.rs | 46 +++++++++++ 9 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs create mode 100644 identity_credential/src/validator/vp_jwt_validation/error.rs create mode 100644 identity_credential/src/validator/vp_jwt_validation/mod.rs create mode 100644 identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index ff35da9e77..b1b05f6694 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -248,7 +248,7 @@ impl IssuanceDateClaims { /// Produces the `issuanceDate` value from `nbf` if it is set, /// otherwise falls back to `iat`. If none of these values are set an error is returned. #[cfg(feature = "validator")] - fn to_issuance_date(self) -> Result { + pub(crate) fn to_issuance_date(self) -> Result { if let Some(timestamp) = self .nbf .map(Timestamp::from_unix) diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index 5c17dbcbbb..66148fefa3 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -120,3 +120,17 @@ where #[serde(skip_serializing_if = "Option::is_none")] proof: Option>, } + +impl<'presentation, T> PresentationJwtClaims<'presentation, T> +where + T: ToOwned + Serialize + DeserializeOwned, +{ + pub(crate) fn try_into_presentation(&self) -> Result { + OK(()) + } + + fn check_consistency(&self) -> Result<()> { + // todo! + OK(()) + } +} diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs index 4303fbcd7c..2f19c481c0 100644 --- a/identity_credential/src/presentation/mod.rs +++ b/identity_credential/src/presentation/mod.rs @@ -17,3 +17,6 @@ pub use self::jwt_presentation::JwtPresentation; pub use self::jwt_presentation_builder::JwtPresentationBuilder; pub use self::jwt_presentation_options::JwtPresentationOptions; pub use self::presentation::Presentation; + +#[cfg(feature = "validator")] +pub(crate) use self::jwt_serialization::*; diff --git a/identity_credential/src/validator/mod.rs b/identity_credential/src/validator/mod.rs index edd9f22758..00df967835 100644 --- a/identity_credential/src/validator/mod.rs +++ b/identity_credential/src/validator/mod.rs @@ -16,6 +16,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 vp_jwt_validation::*; mod credential_validator; mod domain_linkage_validator; @@ -24,6 +25,7 @@ mod presentation_validator; #[cfg(test)] mod test_utils; mod validation_options; +mod vp_jwt_validation; // Currently conflicting names with the old validator/validation options // so we do not re-export the items in vc_jwt_validation for now. diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs new file mode 100644 index 0000000000..f945152129 --- /dev/null +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -0,0 +1,19 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_verification::jws::JwsHeader; + +use crate::presentation::JwtPresentation; + +/// Decoded [`Credential`] from a cryptographically verified JWS. +/// Note that having an instance of this type only means the JWS it was constructed from was verified. +/// It does not imply anything about a potentially present proof property on the credential itself. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub struct DecodedJwtPresentation { + /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). + pub presentation: JwtPresentation, + /// The protected header parsed from the JWS. + pub header: Box, +} diff --git a/identity_credential/src/validator/vp_jwt_validation/error.rs b/identity_credential/src/validator/vp_jwt_validation/error.rs new file mode 100644 index 0000000000..e08a1b135b --- /dev/null +++ b/identity_credential/src/validator/vp_jwt_validation/error.rs @@ -0,0 +1,49 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; +use std::error::Error; +use std::fmt::Display; + +use crate::validator::vc_jwt_validation::CompoundCredentialValidationError; +use crate::validator::vc_jwt_validation::ValidationError; + +use super::DecodedJwtPresentation; +type PresentationValidationResult = std::result::Result; + +#[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, +} + +impl CompoundPresentationValidationError { + pub(crate) fn one_prsentation_error(error: ValidationError) -> Self { + Self { + credential_errors: BTreeMap::new(), + presentation_validation_errors: vec![error], + } + } +} + +impl Display for CompoundPresentationValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let credential_error_formatter = |(position, reason): (&usize, &CompoundCredentialValidationError)| -> String { + format!("credential num. {} errors: {}", position, reason.to_string().as_str()) + }; + + let error_string_iter = self + .presentation_validation_errors + .iter() + .map(|error| error.to_string()) + .chain(self.credential_errors.iter().map(credential_error_formatter)); + let detailed_information: String = itertools::intersperse(error_string_iter, "; ".to_string()).collect(); + write!(f, "[{detailed_information}]") + } +} + +impl Error for CompoundPresentationValidationError {} diff --git a/identity_credential/src/validator/vp_jwt_validation/mod.rs b/identity_credential/src/validator/vp_jwt_validation/mod.rs new file mode 100644 index 0000000000..31ad81e700 --- /dev/null +++ b/identity_credential/src/validator/vp_jwt_validation/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod decoded_jwt_presentation; +mod error; +mod presentation_jwt_validation_options; +mod presentation_jwt_validator; + +pub use decoded_jwt_presentation::*; +pub use error::*; +pub use presentation_jwt_validation_options::*; +pub use presentation_jwt_validator::*; diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs new file mode 100644 index 0000000000..7e1c1f9d73 --- /dev/null +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs @@ -0,0 +1,78 @@ +use identity_document::verifiable::JwsVerificationOptions; + +use crate::validator::vc_jwt_validation::CredentialValidationOptions; + +/// 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")] +pub struct JwtPresentationValidationOptions { + /// Options which affect the validation of *all* credentials in the presentation. + #[serde(default)] + pub shared_validation_options: CredentialValidationOptions, + /// Options which affect the verification of the signature on the presentation. + #[serde(default)] + pub presentation_verifier_options: JwsVerificationOptions, + /// Declares how the presentation's credential subjects must relate to the holder. + /// Default: [`SubjectHolderRelationship::AlwaysSubject`]. + #[serde(default)] + pub subject_holder_relationship: SubjectHolderRelationship, + + /// Determines if the JWT expiration date claim `exp` should be skipped during validation. + /// Default: false. + #[serde(default)] + pub skip_exp: bool, +} + +fn bool_true() -> bool { + true +} + +impl JwtPresentationValidationOptions { + /// Constructor that sets all options to their defaults. + pub fn new() -> Self { + Self::default() + } + + /// Set options which affect the validation of *all* credentials in the presentation. + pub fn shared_validation_options(mut self, options: CredentialValidationOptions) -> Self { + self.shared_validation_options = options; + self + } + /// Set options which affect the verification of the signature on the presentation. + pub fn presentation_verifier_options(mut self, options: JwsVerificationOptions) -> Self { + self.presentation_verifier_options = options; + self + } + + /// Declares how the presentation's holder must relate to the credential subjects. + pub fn subject_holder_relationship(mut self, options: SubjectHolderRelationship) -> Self { + self.subject_holder_relationship = options; + self + } +} + +/// Declares how credential subjects must relate to the presentation holder during validation. +/// See [`PresentationValidationOptions::subject_holder_relationship()`]. +/// +/// See also the [Subject-Holder Relationship](https://www.w3.org/TR/vc-data-model/#subject-holder-relationships) section of the specification. +// Need to use serde_repr to make this work with duck typed interfaces in the Wasm bindings. +#[derive(Debug, Clone, Copy, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)] +#[repr(u8)] +pub enum SubjectHolderRelationship { + /// The holder must always match the subject on all credentials, regardless of their [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property. + /// This is the variant returned by [Self::default](Self::default()) and the default used in + /// [`PresentationValidationOptions`]. + AlwaysSubject = 0, + /// The holder must match the subject only for credentials where the [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property is `true`. + SubjectOnNonTransferable = 1, + /// Declares that the subject is not required to have any kind of relationship to the holder. + Any = 2, +} + +impl Default for SubjectHolderRelationship { + fn default() -> Self { + Self::AlwaysSubject + } +} diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index d60e7ce787..56bdd80007 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -1,4 +1,50 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_core::convert::FromJson; +use identity_document::document::CoreDocument; +use identity_verification::jws::DecodedJws; +use identity_verification::jws::EdDSAJwsSignatureVerifier; +use identity_verification::jws::JwsSignatureVerifier; +use identity_verification::jws::JwsSignatureVerifierFn; + +use crate::credential::Jwt; +use crate::presentation::PresentationJwtClaims; +use crate::validator::vc_jwt_validation::ValidationError; +use crate::validator::FailFast; + +use super::CompoundPresentationValidationError; +use super::JwtPresentationValidationOptions; + +#[derive(Debug, Clone)] +#[non_exhaustive] pub struct PresentationJwtValidator {} +type PresentationValidationResult = std::result::Result<(), CompoundPresentationValidationError>; + +impl PresentationJwtValidator { + pub fn validate + ?Sized, IDOC: AsRef>( + presentation: &Jwt, + holder: &HDOC, + issuers: &[IDOC], + options: &JwtPresentationValidationOptions, + fail_fast: FailFast, + ) -> PresentationValidationResult { + let decoded_jws = holder + .as_ref() + .verify_jws( + presentation.as_str(), + None, + &EdDSAJwsSignatureVerifier::default(), + &options.presentation_verifier_options, + ) + .unwrap(); + + let claims: PresentationJwtClaims = PresentationJwtClaims::from_json_slice(&decoded_jws.claims).map_err(|err| { + CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + })?; + + Ok(()) + } +} From a4d0f0a934eacea0354efafb666d29a42a7a9eb9 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 23 May 2023 23:54:43 +0200 Subject: [PATCH 03/31] poc validator --- identity_credential/src/error.rs | 4 ++ .../src/presentation/jwt_serialization.rs | 49 +++++++++++++++++-- .../presentation_jwt_validator.rs | 40 +++++++++++---- 3 files changed, 79 insertions(+), 14 deletions(-) diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index caf35174ea..5673a85248 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -46,6 +46,10 @@ pub enum Error { #[error("could not convert JWT to the VC data model: {0}")] InconsistentCredentialJwtClaims(&'static str), + /// Caused when attempting to convert a JWT to a `JwtPresentation` that has conflicting values + /// between the registered claims and those in the `vp` object. + #[error("could not convert JWT to the VP data model: {0}")] + InconsistentPresentationJwtClaims(&'static str), /// Caused when attempting to parse a timestamp value that is outside the /// valid range defined in [RFC 3339](https://tools.ietf.org/html/rfc3339). #[error("timestamp conversion failed")] diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index 66148fefa3..53473d5e1d 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -125,12 +125,53 @@ impl<'presentation, T> PresentationJwtClaims<'presentation, T> where T: ToOwned + Serialize + DeserializeOwned, { - pub(crate) fn try_into_presentation(&self) -> Result { - OK(()) + pub(crate) fn try_into_presentation(self) -> Result> { + self.check_consistency()?; + let Self { + exp, + iss, + issuance_date, + jti, + aud, + vp, + } = self; + let InnerPresentation { + context, + id, + types, + verifiable_credential, + refresh_service, + terms_of_use, + properties, + proof, + } = vp; + + let presentation = JwtPresentation { + context: context.into_owned(), + id: jti.map(Cow::into_owned), + types: types.into_owned(), + verifiable_credential: verifiable_credential.into_owned(), + holder: Some(iss.into_owned()), + refresh_service: refresh_service.into_owned(), + terms_of_use: terms_of_use.into_owned(), + properties: properties.into_owned(), + proof: proof.map(Cow::into_owned), + }; + + Ok(presentation) } fn check_consistency(&self) -> Result<()> { - // todo! - OK(()) + // Check consistency of id + if !self + .vp + .id + .as_ref() + .map(|value| self.jti.as_ref().filter(|jti| jti.as_ref() == value).is_some()) + .unwrap_or(true) + { + return Err(Error::InconsistentPresentationJwtClaims("inconsistent presentation id")); + }; + Ok(()) } } diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 56bdd80007..e33cf83511 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -9,26 +9,37 @@ use identity_verification::jws::JwsSignatureVerifier; use identity_verification::jws::JwsSignatureVerifierFn; use crate::credential::Jwt; +use crate::presentation::JwtPresentation; use crate::presentation::PresentationJwtClaims; use crate::validator::vc_jwt_validation::ValidationError; use crate::validator::FailFast; use super::CompoundPresentationValidationError; +use super::DecodedJwtPresentation; use super::JwtPresentationValidationOptions; #[derive(Debug, Clone)] #[non_exhaustive] -pub struct PresentationJwtValidator {} -type PresentationValidationResult = std::result::Result<(), CompoundPresentationValidationError>; +pub struct PresentationJwtValidator(V); +// type PresentationValidationResult = +// std::result::Result, CompoundPresentationValidationError>; -impl PresentationJwtValidator { - pub fn validate + ?Sized, IDOC: AsRef>( +impl PresentationJwtValidator +where + V: JwsSignatureVerifier, +{ + pub fn validate( presentation: &Jwt, holder: &HDOC, issuers: &[IDOC], options: &JwtPresentationValidationOptions, fail_fast: FailFast, - ) -> PresentationValidationResult { + ) -> Result, CompoundPresentationValidationError> + where + HDOC: AsRef + ?Sized, + IDOC: AsRef, + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { let decoded_jws = holder .as_ref() .verify_jws( @@ -39,12 +50,21 @@ impl PresentationJwtValidator { ) .unwrap(); - let claims: PresentationJwtClaims = PresentationJwtClaims::from_json_slice(&decoded_jws.claims).map_err(|err| { - CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( - crate::Error::JwtClaimsSetDeserializationError(err.into()), - )) + let claims: PresentationJwtClaims = + PresentationJwtClaims::from_json_slice(&decoded_jws.claims).map_err(|err| { + CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + })?; + let presentation: JwtPresentation = claims.try_into_presentation().map_err(|err| { + CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) })?; - Ok(()) + let decoded_jwt_presentation: DecodedJwtPresentation = DecodedJwtPresentation { + presentation, + header: Box::new(decoded_jws.protected), + }; + + Ok(decoded_jwt_presentation) } } From 6d2a9057e7639c5a673613bbe974be723c0f417d Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Thu, 25 May 2023 00:11:35 +0200 Subject: [PATCH 04/31] poc validator without credentials --- .../src/presentation/jwt_serialization.rs | 10 ++-- .../credential_jwt_validator.rs | 2 +- .../decoded_jwt_presentation.rs | 6 +- .../presentation_jwt_validation_options.rs | 20 ++++++- .../presentation_jwt_validator.rs | 55 ++++++++++++++++++- 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index 53473d5e1d..108e071661 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -31,23 +31,23 @@ where { /// Represents the expirationDate encoded as a UNIX timestamp. #[serde(skip_serializing_if = "Option::is_none")] - exp: Option, + pub(crate) exp: Option, /// Represents the issuer of the presentation who is the same as the holder of the verifiable /// credentials. iss: Cow<'presentation, Url>, /// Represents the issuanceDate encoded as a UNIX timestamp. #[serde(flatten)] - issuance_date: Option, + pub(crate) issuance_date: Option, /// Represents the id property of the credential. #[serde(skip_serializing_if = "Option::is_none")] jti: Option>, #[serde(skip_serializing_if = "Option::is_none")] - aud: Option, + pub(crate) aud: Option, - vp: InnerPresentation<'presentation, T>, + pub(crate) vp: InnerPresentation<'presentation, T>, } impl<'presentation, T> PresentationJwtClaims<'presentation, T> @@ -90,7 +90,7 @@ where } #[derive(Serialize, Deserialize)] -struct InnerPresentation<'presentation, T = Object> +pub(crate) struct InnerPresentation<'presentation, T = Object> where T: ToOwned + Serialize, ::Owned: DeserializeOwned, diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs index 5ce7c6417f..ee709bf395 100644 --- a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs +++ b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs @@ -35,7 +35,7 @@ use crate::validator::SubjectHolderRelationship; #[non_exhaustive] pub struct CredentialValidator(V); -type ValidationUnitResult = std::result::Result; +pub type ValidationUnitResult = std::result::Result; impl CredentialValidator where diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index f945152129..a78f93f80a 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -1,7 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_core::common::Object; +use identity_core::common::{Object, Timestamp, Url}; use identity_verification::jws::JwsHeader; use crate::presentation::JwtPresentation; @@ -16,4 +16,8 @@ pub struct DecodedJwtPresentation { pub presentation: JwtPresentation, /// The protected header parsed from the JWS. pub header: Box, + + pub expiration_date: Option, + + pub aud: Option, } diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs index 7e1c1f9d73..a7979cf16d 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs @@ -1,3 +1,4 @@ +use identity_core::common::Timestamp; use identity_document::verifiable::JwsVerificationOptions; use crate::validator::vc_jwt_validation::CredentialValidationOptions; @@ -19,10 +20,21 @@ pub struct JwtPresentationValidationOptions { #[serde(default)] pub subject_holder_relationship: SubjectHolderRelationship, - /// Determines if the JWT expiration date claim `exp` should be skipped during validation. - /// Default: false. + // /// Determines if the JWT expiration date claim `exp` should be skipped during validation. + // /// Default: false. + // #[serde(default)] + // pub skip_exp: bool, + /// Declares that the credential is **not** considered valid if it expires before this + /// [`Timestamp`]. + /// Uses the current datetime during validation if not set. #[serde(default)] - pub skip_exp: bool, + pub earliest_expiry_date: Option, + + /// Declares that the credential is **not** considered valid if it was issued later than this + /// [`Timestamp`]. + /// Uses the current datetime during validation if not set. + #[serde(default)] + pub latest_issuance_date: Option, } fn bool_true() -> bool { @@ -51,6 +63,8 @@ impl JwtPresentationValidationOptions { self.subject_holder_relationship = options; self } + + //todo expiry date } /// Declares how credential subjects must relate to the presentation holder during validation. diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index e33cf83511..8a9e8b0f6c 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -1,16 +1,16 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_core::common::Timestamp; use identity_core::convert::FromJson; use identity_document::document::CoreDocument; -use identity_verification::jws::DecodedJws; use identity_verification::jws::EdDSAJwsSignatureVerifier; use identity_verification::jws::JwsSignatureVerifier; -use identity_verification::jws::JwsSignatureVerifierFn; use crate::credential::Jwt; use crate::presentation::JwtPresentation; use crate::presentation::PresentationJwtClaims; +use crate::validator::vc_jwt_validation::CredentialValidator; use crate::validator::vc_jwt_validation::ValidationError; use crate::validator::FailFast; @@ -28,6 +28,7 @@ impl PresentationJwtValidator where V: JwsSignatureVerifier, { + /// todo pub fn validate( presentation: &Jwt, holder: &HDOC, @@ -50,12 +51,54 @@ where ) .unwrap(); - let claims: PresentationJwtClaims = + let claims: PresentationJwtClaims<'_, T> = PresentationJwtClaims::from_json_slice(&decoded_jws.claims).map_err(|err| { CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( crate::Error::JwtClaimsSetDeserializationError(err.into()), )) })?; + + // Check the expiration date + let expiration_date: Option = claims + .exp + .map(|exp| { + Timestamp::from_unix(exp).map_err(|err| { + CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + }) + }) + .transpose()?; + + (expiration_date.is_none() || expiration_date >= Some(options.earliest_expiry_date.unwrap_or_default())) + .then_some(()) + .ok_or(CompoundPresentationValidationError::one_prsentation_error( + ValidationError::ExpirationDate, + ))?; + + // Check issuance date. + let issuance_date: Option = claims + .issuance_date + .map(|iss| { + iss.to_issuance_date().map_err(|err| { + CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + }) + }) + .transpose()?; + + (issuance_date.is_none() || issuance_date <= Some(options.latest_issuance_date.unwrap_or_default())) + .then_some(()) + .ok_or(CompoundPresentationValidationError::one_prsentation_error( + ValidationError::ExpirationDate, + ))?; + + // Check credentials. + let credential_validator = CredentialValidator::new(); + + let aud = claims.aud.clone(); + let presentation: JwtPresentation = claims.try_into_presentation().map_err(|err| { CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) })?; @@ -63,8 +106,14 @@ where let decoded_jwt_presentation: DecodedJwtPresentation = DecodedJwtPresentation { presentation, header: Box::new(decoded_jws.protected), + expiration_date, + aud, }; + for credential in decoded_jwt_presentation.presentation.verifiable_credential.to_vec() { + credential_validator.validate_extended() + } + Ok(decoded_jwt_presentation) } } From 0a9aa12463087d307543a98afbb14e5a10e4b5b3 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Fri, 26 May 2023 13:08:27 +0200 Subject: [PATCH 05/31] validate_credentials --- .../decoded_jwt_presentation.rs | 6 +- .../presentation_jwt_validation_options.rs | 41 +++----- .../presentation_jwt_validator.rs | 96 ++++++++++++++++--- 3 files changed, 100 insertions(+), 43 deletions(-) diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index a78f93f80a..a852515896 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -4,14 +4,14 @@ use identity_core::common::{Object, Timestamp, Url}; use identity_verification::jws::JwsHeader; -use crate::presentation::JwtPresentation; +use crate::{presentation::JwtPresentation, validator::vc_jwt_validation::DecodedJwtCredential}; /// Decoded [`Credential`] from a cryptographically verified JWS. /// Note that having an instance of this type only means the JWS it was constructed from was verified. /// It does not imply anything about a potentially present proof property on the credential itself. #[non_exhaustive] #[derive(Debug, Clone)] -pub struct DecodedJwtPresentation { +pub struct DecodedJwtPresentation { /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). pub presentation: JwtPresentation, /// The protected header parsed from the JWS. @@ -20,4 +20,6 @@ pub struct DecodedJwtPresentation { pub expiration_date: Option, pub aud: Option, + + pub credentials: Vec>, } diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs index a7979cf16d..272df02ff8 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs @@ -1,7 +1,7 @@ use identity_core::common::Timestamp; use identity_document::verifiable::JwsVerificationOptions; -use crate::validator::vc_jwt_validation::CredentialValidationOptions; +use crate::validator::{vc_jwt_validation::CredentialValidationOptions, SubjectHolderRelationship}; /// Criteria for validating a [`Presentation`](crate::presentation::Presentation), such as with /// [`PresentationValidator::validate`](crate::validator::PresentationValidator::validate()). @@ -37,10 +37,6 @@ pub struct JwtPresentationValidationOptions { pub latest_issuance_date: Option, } -fn bool_true() -> bool { - true -} - impl JwtPresentationValidationOptions { /// Constructor that sets all options to their defaults. pub fn new() -> Self { @@ -63,30 +59,17 @@ impl JwtPresentationValidationOptions { self.subject_holder_relationship = options; self } + /// Declare that the presentation is **not** considered valid if it expires before this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn earliest_expiry_date(mut self, timestamp: Timestamp) -> Self { + self.earliest_expiry_date = Some(timestamp); + self + } - //todo expiry date -} - -/// Declares how credential subjects must relate to the presentation holder during validation. -/// See [`PresentationValidationOptions::subject_holder_relationship()`]. -/// -/// See also the [Subject-Holder Relationship](https://www.w3.org/TR/vc-data-model/#subject-holder-relationships) section of the specification. -// Need to use serde_repr to make this work with duck typed interfaces in the Wasm bindings. -#[derive(Debug, Clone, Copy, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)] -#[repr(u8)] -pub enum SubjectHolderRelationship { - /// The holder must always match the subject on all credentials, regardless of their [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property. - /// This is the variant returned by [Self::default](Self::default()) and the default used in - /// [`PresentationValidationOptions`]. - AlwaysSubject = 0, - /// The holder must match the subject only for credentials where the [`nonTransferable`](https://www.w3.org/TR/vc-data-model/#nontransferable-property) property is `true`. - SubjectOnNonTransferable = 1, - /// Declares that the subject is not required to have any kind of relationship to the holder. - Any = 2, -} - -impl Default for SubjectHolderRelationship { - fn default() -> Self { - Self::AlwaysSubject + /// Declare that the presentation is **not** considered valid if it was issued later than this [`Timestamp`]. + /// Uses the current datetime during validation if not set. + pub fn latest_issuance_date(mut self, timestamp: Timestamp) -> Self { + self.latest_issuance_date = Some(timestamp); + self } } diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 8a9e8b0f6c..a2dad635de 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -1,6 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::collections::BTreeMap; + use identity_core::common::Timestamp; use identity_core::convert::FromJson; use identity_document::document::CoreDocument; @@ -10,7 +12,9 @@ use identity_verification::jws::JwsSignatureVerifier; use crate::credential::Jwt; use crate::presentation::JwtPresentation; use crate::presentation::PresentationJwtClaims; +use crate::validator::vc_jwt_validation::CompoundCredentialValidationError; use crate::validator::vc_jwt_validation::CredentialValidator; +use crate::validator::vc_jwt_validation::DecodedJwtCredential; use crate::validator::vc_jwt_validation::ValidationError; use crate::validator::FailFast; @@ -24,29 +28,40 @@ pub struct PresentationJwtValidator = // std::result::Result, CompoundPresentationValidationError>; +impl PresentationJwtValidator { + pub fn new() -> Self { + Self(EdDSAJwsSignatureVerifier::default()) + } +} impl PresentationJwtValidator where V: JwsSignatureVerifier, { + pub fn with_signature_verifier(signature_verifier: V) -> Self { + Self(signature_verifier) + } + /// todo - pub fn validate( + pub fn validate( + &self, presentation: &Jwt, holder: &HDOC, issuers: &[IDOC], options: &JwtPresentationValidationOptions, fail_fast: FailFast, - ) -> Result, CompoundPresentationValidationError> + ) -> Result, CompoundPresentationValidationError> where HDOC: AsRef + ?Sized, IDOC: AsRef, T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + U: ToOwned + serde::Serialize + serde::de::DeserializeOwned, { let decoded_jws = holder .as_ref() .verify_jws( presentation.as_str(), None, - &EdDSAJwsSignatureVerifier::default(), + &self.0, &options.presentation_verifier_options, ) .unwrap(); @@ -58,7 +73,7 @@ where )) })?; - // Check the expiration date + // Check the expiration date. let expiration_date: Option = claims .exp .map(|exp| { @@ -94,26 +109,83 @@ where ValidationError::ExpirationDate, ))?; - // Check credentials. - let credential_validator = CredentialValidator::new(); - let aud = claims.aud.clone(); + // Validate credentials. let presentation: JwtPresentation = claims.try_into_presentation().map_err(|err| { CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) })?; - let decoded_jwt_presentation: DecodedJwtPresentation = DecodedJwtPresentation { + let credentials: Vec> = self + .validate_credentials::(&presentation, issuers, options, fail_fast) + .map_err(|err| CompoundPresentationValidationError { + credential_errors: err, + presentation_validation_errors: vec![], + })?; + + let decoded_jwt_presentation: DecodedJwtPresentation = DecodedJwtPresentation { presentation, header: Box::new(decoded_jws.protected), expiration_date, aud, + credentials, }; + //todo: check holder id; + //todo: check subject relationship + Ok(decoded_jwt_presentation) + } - for credential in decoded_jwt_presentation.presentation.verifiable_credential.to_vec() { - credential_validator.validate_extended() - } + fn validate_credentials( + &self, + presentation: &JwtPresentation, + issuers: &[DOC], + options: &JwtPresentationValidationOptions, + fail_fast: FailFast, + ) -> Result>, BTreeMap> + where + DOC: AsRef, + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + U: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + { + let number_of_credentials = presentation.verifiable_credential.len(); + let mut decoded_credentials: Vec> = vec![]; + let credential_errors_iter = presentation + .verifiable_credential + .iter() + .map(|credential| { + CredentialValidator::::validate_extended::( + &self.0, + credential, + issuers, + &options.shared_validation_options, + presentation + .holder + .as_ref() + .map(|holder_url| (holder_url, options.subject_holder_relationship)), + fail_fast, + ) + }) + .enumerate() + .filter_map(|(position, result)| { + if let Ok(decoded_credential) = result { + decoded_credentials.push(decoded_credential); + None + } else { + result.err().map(|error| (position, error)) + } + }); + + let credential_errors: BTreeMap = credential_errors_iter + .take(match fail_fast { + FailFast::FirstError => 1, + FailFast::AllErrors => number_of_credentials, + }) + .collect(); - Ok(decoded_jwt_presentation) + if credential_errors.is_empty() { + Ok(decoded_credentials) + } else { + Err(credential_errors) + } } } From dbbb4c9a95a15748dd76b9b571277277119c7f85 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Fri, 26 May 2023 16:49:04 +0200 Subject: [PATCH 06/31] implement `extract_holder` --- .../src/presentation/jwt_serialization.rs | 2 +- .../presentation_jwt_validator.rs | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index 108e071661..bba0ffe16d 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -34,7 +34,7 @@ where pub(crate) exp: Option, /// Represents the issuer of the presentation who is the same as the holder of the verifiable /// credentials. - iss: Cow<'presentation, Url>, + pub(crate) iss: Cow<'presentation, Url>, /// Represents the issuanceDate encoded as a UNIX timestamp. #[serde(flatten)] diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index a2dad635de..98bad56e73 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -2,10 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use std::collections::BTreeMap; +use std::str::FromStr; use identity_core::common::Timestamp; +use identity_core::common::Url; use identity_core::convert::FromJson; +use identity_did::CoreDID; +use identity_did::DID; use identity_document::document::CoreDocument; +use identity_verification::jws::DecodedJws; +use identity_verification::jws::Decoder; use identity_verification::jws::EdDSAJwsSignatureVerifier; use identity_verification::jws::JwsSignatureVerifier; @@ -15,6 +21,7 @@ use crate::presentation::PresentationJwtClaims; use crate::validator::vc_jwt_validation::CompoundCredentialValidationError; use crate::validator::vc_jwt_validation::CredentialValidator; use crate::validator::vc_jwt_validation::DecodedJwtCredential; +use crate::validator::vc_jwt_validation::SignerContext; use crate::validator::vc_jwt_validation::ValidationError; use crate::validator::FailFast; @@ -25,8 +32,6 @@ use super::JwtPresentationValidationOptions; #[derive(Debug, Clone)] #[non_exhaustive] pub struct PresentationJwtValidator(V); -// type PresentationValidationResult = -// std::result::Result, CompoundPresentationValidationError>; impl PresentationJwtValidator { pub fn new() -> Self { @@ -56,7 +61,18 @@ where T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, U: ToOwned + serde::Serialize + serde::de::DeserializeOwned, { - let decoded_jws = holder + // Verify that holder document matches holder in presentation. + let holder_did: CoreDID = Self::extract_holder::(presentation) + .map_err(|err| CompoundPresentationValidationError::one_prsentation_error(err))?; + + if &holder_did != ::id(holder.as_ref()) { + return Err(CompoundPresentationValidationError::one_prsentation_error( + ValidationError::DocumentMismatch(SignerContext::Holder), + )); + } + + // Verify JWS. + let decoded_jws: DecodedJws<'_> = holder .as_ref() .verify_jws( presentation.as_str(), @@ -111,11 +127,11 @@ where let aud = claims.aud.clone(); - // Validate credentials. let presentation: JwtPresentation = claims.try_into_presentation().map_err(|err| { CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) })?; + // Validate credentials. let credentials: Vec> = self .validate_credentials::(&presentation, issuers, options, fail_fast) .map_err(|err| CompoundPresentationValidationError { @@ -130,11 +146,30 @@ where aud, credentials, }; - //todo: check holder id; - //todo: check subject relationship + Ok(decoded_jwt_presentation) } + pub fn extract_holder(presentation: &Jwt) -> std::result::Result + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + ::Err: std::error::Error + Send + Sync + 'static, + { + let validation_item = Decoder::new() + .decode_compact_serialization(presentation.as_str().as_bytes(), None) + .map_err(ValidationError::JwsDecodingError)?; + + let claims: PresentationJwtClaims<'_, T> = PresentationJwtClaims::from_json_slice(&validation_item.claims()) + .map_err(|err| { + ValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + let iss: Url = claims.iss.into_owned(); + D::from_str(iss.as_str()).map_err(|err| ValidationError::SignerUrl { + signer_ctx: SignerContext::Holder, + source: err.into(), + }) + } + fn validate_credentials( &self, presentation: &JwtPresentation, From 067604bb493f6f2dc09386adf29a0bacdebda42d Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Sun, 28 May 2023 01:13:33 +0200 Subject: [PATCH 07/31] small fix --- .../vp_jwt_validation/presentation_jwt_validator.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 98bad56e73..3eccfa03e2 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -12,8 +12,8 @@ use identity_did::DID; use identity_document::document::CoreDocument; use identity_verification::jws::DecodedJws; use identity_verification::jws::Decoder; -use identity_verification::jws::EdDSAJwsSignatureVerifier; -use identity_verification::jws::JwsSignatureVerifier; +use identity_verification::jws::EdDSAJwsVerifier; +use identity_verification::jws::JwsVerifier; use crate::credential::Jwt; use crate::presentation::JwtPresentation; @@ -31,16 +31,16 @@ use super::JwtPresentationValidationOptions; #[derive(Debug, Clone)] #[non_exhaustive] -pub struct PresentationJwtValidator(V); +pub struct PresentationJwtValidator(V); impl PresentationJwtValidator { pub fn new() -> Self { - Self(EdDSAJwsSignatureVerifier::default()) + Self(EdDSAJwsVerifier::default()) } } impl PresentationJwtValidator where - V: JwsSignatureVerifier, + V: JwsVerifier, { pub fn with_signature_verifier(signature_verifier: V) -> Self { Self(signature_verifier) From a0ccfb3d56821c48c20243e511ecea8135953159 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Sun, 28 May 2023 21:38:08 +0200 Subject: [PATCH 08/31] POC test --- .../src/presentation/jwt_presentation.rs | 2 +- .../presentation_jwt_validator.rs | 1 - identity_storage/src/storage/tests/api.rs | 157 ++++++++++++++++++ identity_storage/src/storage/tests/mod.rs | 1 + .../storage/tests/presentation_validation.rs | 61 +++++++ 5 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 identity_storage/src/storage/tests/presentation_validation.rs diff --git a/identity_credential/src/presentation/jwt_presentation.rs b/identity_credential/src/presentation/jwt_presentation.rs index 2d621ab475..bae8f1148a 100644 --- a/identity_credential/src/presentation/jwt_presentation.rs +++ b/identity_credential/src/presentation/jwt_presentation.rs @@ -121,7 +121,7 @@ impl JwtPresentation { Ok(()) } - /// Serializes the [`Credential`] as a JWT claims set + /// Serializes the [`JwtPresentation`] as a JWT claims set /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The resulting string can be used as the payload of a JWS when issuing the credential. diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 3eccfa03e2..9eddcce982 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -46,7 +46,6 @@ where Self(signature_verifier) } - /// todo pub fn validate( &self, presentation: &Jwt, diff --git a/identity_storage/src/storage/tests/api.rs b/identity_storage/src/storage/tests/api.rs index 994a538a9b..9cd0ddf038 100644 --- a/identity_storage/src/storage/tests/api.rs +++ b/identity_storage/src/storage/tests/api.rs @@ -1,12 +1,18 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::thread::Builder; + use identity_core::common::Object; use identity_core::convert::FromJson; use identity_credential::credential::Credential; +use identity_credential::presentation::JwtPresentation; +use identity_credential::presentation::JwtPresentationBuilder; +use identity_credential::presentation::JwtPresentationOptions; use identity_credential::validator::vc_jwt_validation::CredentialValidationOptions; use identity_did::DIDUrl; +use identity_did::DID; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; use identity_verification::jose::jws::EdDSAJwsVerifier; @@ -200,6 +206,157 @@ async fn signing_credential() { .is_ok()); } +#[tokio::test] +async fn signing_presentation() { + let (mut issuer_document, issuer_storage) = setup(); + let (mut holder_document, holder_storage) = setup(); + + let method_fragment_issuer: String = issuer_document + .generate_method( + &issuer_storage, + JwkMemStore::ED25519_KEY_TYPE, + JwsAlgorithm::EdDSA, + None, + MethodScope::VerificationMethod, + ) + .await + .unwrap(); + + let method_fragment_holder: String = holder_document + .generate_method( + &holder_storage, + JwkMemStore::ED25519_KEY_TYPE, + JwsAlgorithm::EdDSA, + None, + MethodScope::VerificationMethod, + ) + .await + .unwrap(); + + let credential_json: &str = r#" + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.edu/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "did:bar:Hyx62wPQGyvXCoihZq1BrbUjBRh2LuNxWiiqMkfAuSZr", + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science in Mechanical Engineering" + } + } + }"#; + + let credential: Credential = Credential::from_json(credential_json).unwrap(); + let jws = issuer_document + .sign_credential( + &credential, + &issuer_storage, + &method_fragment_issuer, + &JwsSignatureOptions::default(), + ) + .await + .unwrap(); + + let presentation: JwtPresentation = JwtPresentationBuilder::default() + .holder(holder_document.id().to_url().into()) + .credential(jws) + .build() + .unwrap(); + + println!("{}", presentation); + println!("{:?}", presentation.serialize_jwt(&JwtPresentationOptions::default())); + + let jws_2 = holder_document + .sign_presentation( + &presentation, + &holder_storage, + &method_fragment_holder, + &JwsSignatureOptions::default(), + &JwtPresentationOptions::default(), + ) + .await + .unwrap(); + println!("{}", jws_2.as_str()); + + // let validator = identity_credential::validator::vc_jwt_validation::CredentialValidator::new(); + // assert!(validator + // .validate::<_, Object>( + // &jws, + // &issuer_document, + // &CredentialValidationOptions::default(), + // identity_credential::validator::FailFast::FirstError + // ) + // .is_ok()); + + // let (mut issuer, storage) = setup(); + // let (mut holder, storage2) = setup(); + // + // // Generate a method with the kid as fragment + // let fragment: String = issuer + // .generate_method( + // &storage, + // JwkMemStore::ED25519_KEY_TYPE, + // JwsAlgorithm::EdDSA, + // None, + // MethodScope::VerificationMethod, + // ) + // .await + // .unwrap(); + // + // let credential_json: &str = r#" + // { + // "@context": [ + // "https://www.w3.org/2018/credentials/v1", + // "https://www.w3.org/2018/credentials/examples/v1" + // ], + // "id": "http://example.edu/credentials/3732", + // "type": ["VerifiableCredential", "UniversityDegreeCredential"], + // "issuer": "did:bar:Hyx62wPQGyvXCoihZq1BrbUjBRh2LuNxWiiqMkfAuSZr", + // "issuanceDate": "2010-01-01T19:23:24Z", + // "credentialSubject": { + // "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + // "degree": { + // "type": "BachelorDegree", + // "name": "Bachelor of Science in Mechanical Engineering" + // } + // } + // }"#; + // + // let credential: Credential = Credential::from_json(credential_json).unwrap(); + // let jws = issuer + // .sign_credential( + // &credential, + // &storage, + // kid.as_deref().unwrap(), + // &JwsSignatureOptions::default(), + // ) + // .await + // .unwrap(); + // + // let presentation: JwtPresentation = JwtPresentationBuilder::default() + // .holder(holder.id().to_url().into()) + // .build() + // .unwrap(); + // + // println!("{}", presentation); + // + // // Verify the credential + // let validator = identity_credential::validator::vc_jwt_validation::CredentialValidator::new(); + // assert!(validator + // .validate::<_, Object>( + // &jws, + // &document, + // &CredentialValidationOptions::default(), + // identity_credential::validator::FailFast::FirstError + // ) + // .is_ok()); +} #[tokio::test] async fn purging() { let (mut document, storage) = setup(); diff --git a/identity_storage/src/storage/tests/mod.rs b/identity_storage/src/storage/tests/mod.rs index e469e0e1d8..219423f2be 100644 --- a/identity_storage/src/storage/tests/mod.rs +++ b/identity_storage/src/storage/tests/mod.rs @@ -4,4 +4,5 @@ mod api; mod credential_jws; mod credential_validation; +mod presentation_validation; mod test_utils; diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs new file mode 100644 index 0000000000..ce1a7fd6cf --- /dev/null +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -0,0 +1,61 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_core::common::Object; +use identity_credential::{ + presentation::{JwtPresentation, JwtPresentationBuilder, JwtPresentationOptions}, + validator::{FailFast, JwtPresentationValidationOptions, PresentationJwtValidator}, +}; +use identity_did::DID; +use identity_document::document::CoreDocument; + +use crate::{ + storage::tests::test_utils::{generate_credential, setup_coredocument, Setup}, + JwkDocumentExt, JwsSignatureOptions, +}; + +#[tokio::test] +async fn test_presentation() { + let issuer_setup: Setup = setup_coredocument(None).await; + let holder_setup: Setup = setup_coredocument(None).await; + let credential = generate_credential(&issuer_setup.issuer_doc, &[&issuer_setup.issuer_doc], None, None); + let jws = issuer_setup + .issuer_doc + .sign_credential( + &credential.credential, + &issuer_setup.storage, + &issuer_setup.method_fragment, + &JwsSignatureOptions::default(), + ) + .await + .unwrap(); + + let presentation: JwtPresentation = JwtPresentationBuilder::default() + .holder(holder_setup.issuer_doc.id().to_url().into()) + .credential(jws) + .build() + .unwrap(); + + let presentation_jwt = holder_setup + .issuer_doc + .sign_presentation( + &presentation, + &holder_setup.storage, + &holder_setup.method_fragment, + &JwsSignatureOptions::default(), + &JwtPresentationOptions::default(), + ) + .await + .unwrap(); + + let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + validator + .validate::<_, _, Object, Object>( + &presentation_jwt, + &holder_setup.issuer_doc, + &vec![issuer_setup.issuer_doc], + &JwtPresentationValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); +} From 17263a35672dc1f34bd17114b218e47a075f8107 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 29 May 2023 15:41:16 +0200 Subject: [PATCH 09/31] add basic presentation validation tests --- identity_credential/src/error.rs | 1 + .../src/validator/vc_jwt_validation/error.rs | 3 + .../src/validator/vp_jwt_validation/error.rs | 11 +- .../presentation_jwt_validator.rs | 26 +++-- .../storage/tests/credential_validation.rs | 71 +++++++----- .../storage/tests/presentation_validation.rs | 109 +++++++++++++++--- .../src/storage/tests/test_utils.rs | 65 ++++++++--- 7 files changed, 199 insertions(+), 87 deletions(-) diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 5673a85248..622ed8bf63 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -18,6 +18,7 @@ pub enum Error { /// Caused when constructing a credential without an issuer. #[error("missing credential issuer")] MissingIssuer, + /// Holder of verifiable presentation is missing. #[error("missing presentation holder")] MissingHolder, /// Caused when constructing a credential without a subject. diff --git a/identity_credential/src/validator/vc_jwt_validation/error.rs b/identity_credential/src/validator/vc_jwt_validation/error.rs index bd161ee57a..8845463e90 100644 --- a/identity_credential/src/validator/vc_jwt_validation/error.rs +++ b/identity_credential/src/validator/vc_jwt_validation/error.rs @@ -14,6 +14,9 @@ pub enum ValidationError { #[error("could not decode jws")] JwsDecodingError(#[source] identity_verification::jose::error::Error), + #[error("could not verify jws")] + PresentationJwsError(#[source] identity_document::error::Error), + /// Indicates that a verification method that both matches the DID Url specified by /// the `kid` value and contains a public key in the JWK format could not be found. #[error("could not find verification material")] diff --git a/identity_credential/src/validator/vp_jwt_validation/error.rs b/identity_credential/src/validator/vp_jwt_validation/error.rs index e08a1b135b..6adfd19bc6 100644 --- a/identity_credential/src/validator/vp_jwt_validation/error.rs +++ b/identity_credential/src/validator/vp_jwt_validation/error.rs @@ -8,12 +8,9 @@ use std::fmt::Display; use crate::validator::vc_jwt_validation::CompoundCredentialValidationError; use crate::validator::vc_jwt_validation::ValidationError; -use super::DecodedJwtPresentation; -type PresentationValidationResult = std::result::Result; - #[derive(Debug)] /// An error caused by a failure to validate a Presentation. -pub struct CompoundPresentationValidationError { +pub struct CompoundJwtPresentationValidationError { /// Errors that occurred during validation of individual credentials, mapped by index of their /// order in the presentation. pub credential_errors: BTreeMap, @@ -21,7 +18,7 @@ pub struct CompoundPresentationValidationError { pub presentation_validation_errors: Vec, } -impl CompoundPresentationValidationError { +impl CompoundJwtPresentationValidationError { pub(crate) fn one_prsentation_error(error: ValidationError) -> Self { Self { credential_errors: BTreeMap::new(), @@ -30,7 +27,7 @@ impl CompoundPresentationValidationError { } } -impl Display for CompoundPresentationValidationError { +impl Display for CompoundJwtPresentationValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let credential_error_formatter = |(position, reason): (&usize, &CompoundCredentialValidationError)| -> String { format!("credential num. {} errors: {}", position, reason.to_string().as_str()) @@ -46,4 +43,4 @@ impl Display for CompoundPresentationValidationError { } } -impl Error for CompoundPresentationValidationError {} +impl Error for CompoundJwtPresentationValidationError {} diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 9eddcce982..640a219a1c 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -25,7 +25,7 @@ use crate::validator::vc_jwt_validation::SignerContext; use crate::validator::vc_jwt_validation::ValidationError; use crate::validator::FailFast; -use super::CompoundPresentationValidationError; +use super::CompoundJwtPresentationValidationError; use super::DecodedJwtPresentation; use super::JwtPresentationValidationOptions; @@ -53,7 +53,7 @@ where issuers: &[IDOC], options: &JwtPresentationValidationOptions, fail_fast: FailFast, - ) -> Result, CompoundPresentationValidationError> + ) -> Result, CompoundJwtPresentationValidationError> where HDOC: AsRef + ?Sized, IDOC: AsRef, @@ -62,10 +62,10 @@ where { // Verify that holder document matches holder in presentation. let holder_did: CoreDID = Self::extract_holder::(presentation) - .map_err(|err| CompoundPresentationValidationError::one_prsentation_error(err))?; + .map_err(|err| CompoundJwtPresentationValidationError::one_prsentation_error(err))?; if &holder_did != ::id(holder.as_ref()) { - return Err(CompoundPresentationValidationError::one_prsentation_error( + return Err(CompoundJwtPresentationValidationError::one_prsentation_error( ValidationError::DocumentMismatch(SignerContext::Holder), )); } @@ -79,11 +79,13 @@ where &self.0, &options.presentation_verifier_options, ) - .unwrap(); + .map_err(|err| { + CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationJwsError(err)) + })?; let claims: PresentationJwtClaims<'_, T> = PresentationJwtClaims::from_json_slice(&decoded_jws.claims).map_err(|err| { - CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( crate::Error::JwtClaimsSetDeserializationError(err.into()), )) })?; @@ -93,7 +95,7 @@ where .exp .map(|exp| { Timestamp::from_unix(exp).map_err(|err| { - CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( crate::Error::JwtClaimsSetDeserializationError(err.into()), )) }) @@ -102,7 +104,7 @@ where (expiration_date.is_none() || expiration_date >= Some(options.earliest_expiry_date.unwrap_or_default())) .then_some(()) - .ok_or(CompoundPresentationValidationError::one_prsentation_error( + .ok_or(CompoundJwtPresentationValidationError::one_prsentation_error( ValidationError::ExpirationDate, ))?; @@ -111,7 +113,7 @@ where .issuance_date .map(|iss| { iss.to_issuance_date().map_err(|err| { - CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( crate::Error::JwtClaimsSetDeserializationError(err.into()), )) }) @@ -120,20 +122,20 @@ where (issuance_date.is_none() || issuance_date <= Some(options.latest_issuance_date.unwrap_or_default())) .then_some(()) - .ok_or(CompoundPresentationValidationError::one_prsentation_error( + .ok_or(CompoundJwtPresentationValidationError::one_prsentation_error( ValidationError::ExpirationDate, ))?; let aud = claims.aud.clone(); let presentation: JwtPresentation = claims.try_into_presentation().map_err(|err| { - CompoundPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) + CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) })?; // Validate credentials. let credentials: Vec> = self .validate_credentials::(&presentation, issuers, options, fail_fast) - .map_err(|err| CompoundPresentationValidationError { + .map_err(|err| CompoundJwtPresentationValidationError { credential_errors: err, presentation_validation_errors: vec![], })?; diff --git a/identity_storage/src/storage/tests/credential_validation.rs b/identity_storage/src/storage/tests/credential_validation.rs index dddeeec12c..12a51f77d8 100644 --- a/identity_storage/src/storage/tests/credential_validation.rs +++ b/identity_storage/src/storage/tests/credential_validation.rs @@ -88,15 +88,17 @@ proptest! { } } -async fn invalid_expiration_or_issuance_date_impl(setup: Setup) +async fn invalid_expiration_or_issuance_date_impl(setup: Setup) where T: JwkDocumentExt + AsRef, { let Setup { issuer_doc, subject_doc, - storage, - method_fragment, + issuer_storage: storage, + issuer_method_fragment: method_fragment, + subject_storage, + subject_method_fragment, } = setup; let CredentialSetup { @@ -164,19 +166,21 @@ where #[tokio::test] async fn invalid_expiration_or_issuance_date() { - invalid_expiration_or_issuance_date_impl(test_utils::setup_coredocument(None).await).await; - invalid_expiration_or_issuance_date_impl(test_utils::setup_iotadocument(None).await).await; + invalid_expiration_or_issuance_date_impl(test_utils::setup_coredocument(None, None).await).await; + invalid_expiration_or_issuance_date_impl(test_utils::setup_iotadocument(None, None).await).await; } -async fn full_validation_impl(setup: Setup) +async fn full_validation_impl(setup: Setup) where T: JwkDocumentExt + AsRef, { let Setup { issuer_doc, subject_doc, - storage, - method_fragment, + issuer_storage: storage, + issuer_method_fragment: method_fragment, + subject_storage, + subject_method_fragment, } = setup; let CredentialSetup { @@ -207,19 +211,21 @@ where #[tokio::test] async fn full_validation() { - full_validation_impl(test_utils::setup_coredocument(None).await).await; - full_validation_impl(test_utils::setup_iotadocument(None).await).await; + full_validation_impl(test_utils::setup_coredocument(None, None).await).await; + full_validation_impl(test_utils::setup_iotadocument(None, None).await).await; } -async fn matches_issuer_did_unrelated_issuer_impl(setup: Setup) +async fn matches_issuer_did_unrelated_issuer_impl(setup: Setup) where T: JwkDocumentExt + AsRef, { let Setup { issuer_doc, subject_doc, - storage, - method_fragment, + issuer_storage: storage, + issuer_method_fragment: method_fragment, + subject_storage: _, + subject_method_fragment: _, } = setup; let CredentialSetup { credential, .. } = test_utils::generate_credential(&issuer_doc, &[&subject_doc], None, None); @@ -261,11 +267,11 @@ where #[tokio::test] async fn matches_issuer_did_unrelated_issuer() { - matches_issuer_did_unrelated_issuer_impl(test_utils::setup_coredocument(None).await).await; - matches_issuer_did_unrelated_issuer_impl(test_utils::setup_iotadocument(None).await).await; + matches_issuer_did_unrelated_issuer_impl(test_utils::setup_coredocument(None, None).await).await; + matches_issuer_did_unrelated_issuer_impl(test_utils::setup_iotadocument(None, None).await).await; } -async fn verify_invalid_signature_impl(setup: Setup, other_setup: Setup, fragment: &'static str) +async fn verify_invalid_signature_impl(setup: Setup, other_setup: Setup, fragment: &'static str) where T: JwkDocumentExt + AsRef, { @@ -277,7 +283,7 @@ where let Setup { issuer_doc: other_issuer_doc, - storage: other_storage, + issuer_storage: other_storage, .. } = other_setup; @@ -328,20 +334,20 @@ async fn verify_invalid_signature() { // Ensure the fragment is the same on both documents so we can produce the signature verification error. let fragment = "signing-key"; verify_invalid_signature_impl( - test_utils::setup_coredocument(Some(fragment)).await, - test_utils::setup_coredocument(Some(fragment)).await, + test_utils::setup_coredocument(Some(fragment), None).await, + test_utils::setup_coredocument(Some(fragment), None).await, fragment, ) .await; verify_invalid_signature_impl( - test_utils::setup_iotadocument(Some(fragment)).await, - test_utils::setup_iotadocument(Some(fragment)).await, + test_utils::setup_iotadocument(Some(fragment), None).await, + test_utils::setup_iotadocument(Some(fragment), None).await, fragment, ) .await; } -async fn check_subject_holder_relationship_impl(setup: Setup) +async fn check_subject_holder_relationship_impl(setup: Setup) where T: JwkDocumentExt + AsRef, { @@ -452,11 +458,11 @@ where #[tokio::test] async fn check_subject_holder_relationship() { - check_subject_holder_relationship_impl(test_utils::setup_coredocument(None).await).await; - check_subject_holder_relationship_impl(test_utils::setup_iotadocument(None).await).await; + check_subject_holder_relationship_impl(test_utils::setup_coredocument(None, None).await).await; + check_subject_holder_relationship_impl(test_utils::setup_iotadocument(None, None).await).await; } -fn check_status_impl(setup: Setup, insert_service: F) +fn check_status_impl(setup: Setup, insert_service: F) where T: JwkDocumentExt + AsRef + RevocationDocumentExt, F: Fn(&mut T, Service), @@ -543,22 +549,25 @@ where #[tokio::test] async fn check_status() { check_status_impl( - test_utils::setup_coredocument(None).await, + test_utils::setup_coredocument(None, None).await, |document: &mut CoreDocument, service: Service| { document.insert_service(service).unwrap(); }, ); } -async fn full_validation_fail_fast_impl(setup: Setup) +async fn full_validation_fail_fast_impl(setup: Setup) where T: JwkDocumentExt + AsRef, + U: JwkDocumentExt + AsRef, { let Setup { issuer_doc, subject_doc, - storage, - method_fragment, + issuer_storage: storage, + issuer_method_fragment: method_fragment, + subject_storage: _, + subject_method_fragment: _, } = setup; let CredentialSetup { @@ -602,6 +611,6 @@ where #[tokio::test] async fn full_validation_fail_fast() { - full_validation_fail_fast_impl(test_utils::setup_coredocument(None).await).await; - full_validation_fail_fast_impl(test_utils::setup_iotadocument(None).await).await; + full_validation_fail_fast_impl(test_utils::setup_coredocument(None, None).await).await; + full_validation_fail_fast_impl(test_utils::setup_iotadocument(None, None).await).await; } diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index ce1a7fd6cf..441fbcf5c7 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -3,45 +3,92 @@ use identity_core::common::Object; use identity_credential::{ + credential::{Credential, Jwt}, presentation::{JwtPresentation, JwtPresentationBuilder, JwtPresentationOptions}, - validator::{FailFast, JwtPresentationValidationOptions, PresentationJwtValidator}, + validator::{ + vc_jwt_validation::ValidationError, FailFast, JwtPresentationValidationOptions, PresentationJwtValidator, + }, }; use identity_did::DID; use identity_document::document::CoreDocument; use crate::{ - storage::tests::test_utils::{generate_credential, setup_coredocument, Setup}, + storage::tests::test_utils::{generate_credential, setup_coredocument, setup_iotadocument, Setup}, JwkDocumentExt, JwsSignatureOptions, }; +use super::test_utils::CredentialSetup; + #[tokio::test] async fn test_presentation() { - let issuer_setup: Setup = setup_coredocument(None).await; - let holder_setup: Setup = setup_coredocument(None).await; - let credential = generate_credential(&issuer_setup.issuer_doc, &[&issuer_setup.issuer_doc], None, None); - let jws = issuer_setup - .issuer_doc - .sign_credential( - &credential.credential, - &issuer_setup.storage, - &issuer_setup.method_fragment, + test_presentation_impl(setup_coredocument(None, None).await).await; + test_presentation_impl(setup_iotadocument(None, None).await).await; +} +async fn test_presentation_impl(setup: Setup) +where + T: JwkDocumentExt + AsRef, +{ + let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); + let jws = sign_credential(&setup, &credential.credential).await; + + let presentation: JwtPresentation = JwtPresentationBuilder::default() + .holder(setup.subject_doc.as_ref().id().to_url().into()) + .credential(jws) + .build() + .unwrap(); + + let presentation_jwt = setup + .subject_doc + .sign_presentation( + &presentation, + &setup.subject_storage, + &setup.subject_method_fragment, &JwsSignatureOptions::default(), + &JwtPresentationOptions::default(), ) .await .unwrap(); + let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + validator + .validate::<_, _, Object, Object>( + &presentation_jwt, + &setup.subject_doc, + &vec![setup.issuer_doc], + &JwtPresentationValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); +} + +#[tokio::test] +async fn presentation_jws_error() { + presentation_jws_error_impl(setup_coredocument(None, None).await).await; + presentation_jws_error_impl(setup_iotadocument(None, None).await).await; +} + +async fn presentation_jws_error_impl(setup: Setup) +where + T: JwkDocumentExt + AsRef + Clone, +{ + let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); + let jws = sign_credential(&setup, &credential.credential).await; + let presentation: JwtPresentation = JwtPresentationBuilder::default() - .holder(holder_setup.issuer_doc.id().to_url().into()) + .holder(setup.subject_doc.as_ref().id().to_url().into()) .credential(jws) .build() .unwrap(); - let presentation_jwt = holder_setup + // Sign presentation using the issuer's method and try to verify it using the holder's document. + // Since the holder's document doesn't include that verification method, Error is returned. + + let presentation_jwt = setup .issuer_doc .sign_presentation( &presentation, - &holder_setup.storage, - &holder_setup.method_fragment, + &setup.issuer_storage, + &setup.issuer_method_fragment, &JwsSignatureOptions::default(), &JwtPresentationOptions::default(), ) @@ -49,13 +96,39 @@ async fn test_presentation() { .unwrap(); let validator: PresentationJwtValidator = PresentationJwtValidator::new(); - validator + let validation_error: ValidationError = validator .validate::<_, _, Object, Object>( &presentation_jwt, - &holder_setup.issuer_doc, - &vec![issuer_setup.issuer_doc], + &setup.subject_doc, + &vec![setup.issuer_doc], &JwtPresentationValidationOptions::default(), FailFast::FirstError, ) + .err() + .unwrap() + .presentation_validation_errors + .into_iter() + .next() .unwrap(); + + assert!(matches!( + validation_error, + ValidationError::PresentationJwsError(identity_document::Error::MethodNotFound) + )); +} + +async fn sign_credential(setup: &Setup, credential: &Credential) -> Jwt +where + T: JwkDocumentExt + AsRef, +{ + setup + .issuer_doc + .sign_credential( + credential, + &setup.issuer_storage, + &setup.issuer_method_fragment, + &JwsSignatureOptions::default(), + ) + .await + .unwrap() } diff --git a/identity_storage/src/storage/tests/test_utils.rs b/identity_storage/src/storage/tests/test_utils.rs index ec278407d0..16fe2dcbfa 100644 --- a/identity_storage/src/storage/tests/test_utils.rs +++ b/identity_storage/src/storage/tests/test_utils.rs @@ -31,6 +31,17 @@ const SUBJECT_DOCUMENT_JSON: &str = r#" "id": "did:foo:0xabcdef" }"#; +const SUBJECT_IOTA_DOCUMENT_JSON: &str = r#" +{ + "doc": { + "id": "did:iota:tst2:0xdfda8bcfb959c3e6ef261343c3e1a8310e9c8294eeafee326a4e96d65dbeaca0" + }, + "meta": { + "created": "2023-05-12T15:09:50Z", + "updated": "2023-05-12T15:09:50Z" + } +}"#; + const ISSUER_IOTA_DOCUMENT_JSON: &str = r#" { "doc": { @@ -42,40 +53,56 @@ const ISSUER_IOTA_DOCUMENT_JSON: &str = r#" } }"#; -pub(super) struct Setup { +pub(super) struct Setup { pub issuer_doc: T, - pub subject_doc: CoreDocument, - pub storage: MemStorage, - pub method_fragment: String, + pub subject_doc: U, + pub issuer_storage: MemStorage, + pub issuer_method_fragment: String, + pub subject_storage: MemStorage, + pub subject_method_fragment: String, } -pub(super) async fn setup_iotadocument(fragment: Option<&'static str>) -> Setup { +pub(super) async fn setup_iotadocument( + issuer_fragment: Option<&'static str>, + subject_fragment: Option<&'static str>, +) -> Setup { let mut issuer_doc = IotaDocument::from_json(ISSUER_IOTA_DOCUMENT_JSON).unwrap(); - let subject_doc = CoreDocument::from_json(SUBJECT_DOCUMENT_JSON).unwrap(); - let storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let mut subject_doc = IotaDocument::from_json(SUBJECT_IOTA_DOCUMENT_JSON).unwrap(); + let issuer_storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let subject_storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let method_fragment: String = generate_method(&storage, &mut issuer_doc, fragment).await; + let issuer_method_fragment: String = generate_method(&issuer_storage, &mut issuer_doc, issuer_fragment).await; + let subject_method_fragment: String = generate_method(&subject_storage, &mut subject_doc, subject_fragment).await; Setup { issuer_doc, subject_doc, - storage, - method_fragment, + issuer_storage, + subject_storage, + issuer_method_fragment, + subject_method_fragment, } } -pub(super) async fn setup_coredocument(fragment: Option<&'static str>) -> Setup { +pub(super) async fn setup_coredocument( + issuer_fragment: Option<&'static str>, + subject_fragment: Option<&'static str>, +) -> Setup { let mut issuer_doc = CoreDocument::from_json(ISSUER_DOCUMENT_JSON).unwrap(); - let subject_doc = CoreDocument::from_json(SUBJECT_DOCUMENT_JSON).unwrap(); - let storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let mut subject_doc = CoreDocument::from_json(SUBJECT_DOCUMENT_JSON).unwrap(); + let issuer_storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); + let subject_storage = Storage::new(JwkMemStore::new(), KeyIdMemstore::new()); - let method_fragment: String = generate_method(&storage, &mut issuer_doc, fragment).await; + let issuer_method_fragment: String = generate_method(&issuer_storage, &mut issuer_doc, issuer_fragment).await; + let subject_method_fragment: String = generate_method(&subject_storage, &mut subject_doc, subject_fragment).await; Setup { issuer_doc, subject_doc, - storage, - method_fragment, + issuer_storage, + subject_storage, + issuer_method_fragment, + subject_method_fragment, } } @@ -101,9 +128,9 @@ pub(super) struct CredentialSetup { pub expiration_date: Timestamp, } -pub(super) fn generate_credential>( +pub(super) fn generate_credential, U: AsRef>( issuer: T, - subjects: &[&CoreDocument], + subjects: &[&U], issuance_date: Option, expiration_date: Option, ) -> CredentialSetup { @@ -114,7 +141,7 @@ pub(super) fn generate_credential>( .iter() .map(|subject| { Subject::from_json_value(json!({ - "id": subject.id().as_str(), + "id": subject.as_ref().id().as_str(), "name": "Alice", "degree": { "type": "BachelorDegree", From 02f67e37a9d00bb63f5fcac35d6cd002125b2c42 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 29 May 2023 17:24:49 +0200 Subject: [PATCH 10/31] make holder not optional, make `proof` type of Object --- identity_credential/src/error.rs | 3 - .../src/presentation/jwt_presentation.rs | 113 +++--------------- .../presentation/jwt_presentation_builder.rs | 60 ++++------ .../presentation/jwt_presentation_options.rs | 7 ++ .../src/presentation/jwt_serialization.rs | 37 +++--- .../decoded_jwt_presentation.rs | 12 +- .../presentation_jwt_validator.rs | 35 +++++- 7 files changed, 97 insertions(+), 170 deletions(-) diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 622ed8bf63..cb5fb656b1 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -18,9 +18,6 @@ pub enum Error { /// Caused when constructing a credential without an issuer. #[error("missing credential issuer")] MissingIssuer, - /// Holder of verifiable presentation is missing. - #[error("missing presentation holder")] - MissingHolder, /// Caused when constructing a credential without a subject. #[error("missing credential subject")] MissingSubject, diff --git a/identity_credential/src/presentation/jwt_presentation.rs b/identity_credential/src/presentation/jwt_presentation.rs index bae8f1148a..fc707a1189 100644 --- a/identity_credential/src/presentation/jwt_presentation.rs +++ b/identity_credential/src/presentation/jwt_presentation.rs @@ -12,10 +12,6 @@ use identity_core::common::Object; use identity_core::common::OneOrMany; use identity_core::common::Url; use identity_core::convert::FmtJson; -use identity_core::crypto::GetSignature; -use identity_core::crypto::GetSignatureMut; -use identity_core::crypto::Proof; -use identity_core::crypto::SetSignature; use identity_verification::MethodUriType; use identity_verification::TryMethod; @@ -47,8 +43,8 @@ pub struct JwtPresentation { #[serde(default = "Default::default", rename = "verifiableCredential")] pub verifiable_credential: OneOrMany, /// The entity that generated the `Presentation`. - #[serde(skip_serializing_if = "Option::is_none")] - pub holder: Option, + #[serde()] + pub holder: Url, /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] pub refresh_service: OneOrMany, @@ -58,30 +54,30 @@ pub struct JwtPresentation { /// Miscellaneous properties. #[serde(flatten)] pub properties: T, - /// Proof(s) used to verify a `Presentation` + /// Optional proof that can be verified by users in addition to JWS. #[serde(skip_serializing_if = "Option::is_none")] - pub proof: Option, + pub proof: Option, } impl JwtPresentation { - /// Returns the base JSON-LD context for `Presentation`s. + /// Returns the base JSON-LD context for `JwtPresentation`s. pub fn base_context() -> &'static Context { Credential::::base_context() } - /// Returns the base type for `Presentation`s. + /// Returns the base type for `JwtPresentation`s. pub const fn base_type() -> &'static str { "VerifiablePresentation" } - /// Creates a `PresentationBuilder` to configure a new Presentation. + /// Creates a `JwtPresentationBuilder` to configure a new Presentation. /// - /// This is the same as [PresentationBuilder::new]. + /// This is the same as [JwtPresentationBuilder::new]. pub fn builder(properties: T) -> PresentationBuilder { PresentationBuilder::new(properties) } - /// Returns a new `Presentation` based on the `PresentationBuilder` configuration. + /// Returns a new `JwtPresentation` based on the `JwtPresentationBuilder` configuration. pub fn from_builder(builder: JwtPresentationBuilder) -> Result { let this: Self = Self { context: builder.context.into(), @@ -100,7 +96,7 @@ impl JwtPresentation { Ok(this) } - /// Validates the semantic structure of the `Presentation`. + /// Validates the semantic structure of the `JwtPresentation`. pub fn check_structure(&self) -> Result<()> { // Ensure the base context is present and in the correct location match self.context.get(0) { @@ -113,10 +109,7 @@ impl JwtPresentation { return Err(Error::MissingBaseType); } - // Check all credentials. - // for credential in self.verifiable_credential.iter() { - // credential.check_structure()?; - // } + //Todo: should check credentials structure? Ok(()) } @@ -135,13 +128,13 @@ impl JwtPresentation { .map_err(|err| Error::JwtClaimsSetSerializationError(err.into())) } - /// Returns a reference to the `Presentation` proof. - pub fn proof(&self) -> Option<&Proof> { + /// Returns a reference to the `JwtPresentation` proof. + pub fn proof(&self) -> Option<&Object> { self.proof.as_ref() } - /// Returns a mutable reference to the `Presentation` proof. - pub fn proof_mut(&mut self) -> Option<&mut Proof> { + /// Returns a mutable reference to the `JwtPresentation` proof. + pub fn proof_mut(&mut self) -> Option<&mut Object> { self.proof.as_mut() } } @@ -154,83 +147,7 @@ where self.fmt_json(f) } } -// -// impl GetSignature for JwtPresentation { -// fn signature(&self) -> Option<&Proof> { -// self.proof.as_ref() -// } -// } -// -// impl GetSignatureMut for JwtPresentation { -// fn signature_mut(&mut self) -> Option<&mut Proof> { -// self.proof.as_mut() -// } -// } -// -// impl SetSignature for JwtPresentation { -// fn set_signature(&mut self, value: Proof) { -// self.proof.replace(value); -// } -// } impl TryMethod for JwtPresentation { const TYPE: MethodUriType = MethodUriType::Absolute; } - -#[cfg(test)] -mod tests { - use identity_core::convert::FromJson; - - use crate::credential::Credential; - use crate::credential::Subject; - - use super::JwtPresentation; - - const JSON: &str = include_str!("../../tests/fixtures/presentation-1.json"); - - // #[test] - // fn test_from_json() { - // let presentation: JwtPresentation = JwtPresentation::from_json(JSON).unwrap(); - // let credential: &Credential = presentation.verifiable_credential.get(0).unwrap(); - // let subject: &Subject = credential.credential_subject.get(0).unwrap(); - // - // assert_eq!( - // presentation.context.as_slice(), - // [ - // "https://www.w3.org/2018/credentials/v1", - // "https://www.w3.org/2018/credentials/examples/v1" - // ] - // ); - // assert_eq!( - // presentation.id.as_ref().unwrap(), - // "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5" - // ); - // assert_eq!( - // presentation.types.as_slice(), - // ["VerifiablePresentation", "CredentialManagerPresentation"] - // ); - // assert_eq!(presentation.proof().unwrap().type_(), "RsaSignature2018"); - // assert_eq!( - // credential.context.as_slice(), - // [ - // "https://www.w3.org/2018/credentials/v1", - // "https://www.w3.org/2018/credentials/examples/v1" - // ] - // ); - // assert_eq!(credential.id.as_ref().unwrap(), "http://example.edu/credentials/3732"); - // assert_eq!( - // credential.types.as_slice(), - // ["VerifiableCredential", "UniversityDegreeCredential"] - // ); - // assert_eq!(credential.issuer.url(), "https://example.edu/issuers/14"); - // assert_eq!(credential.issuance_date, "2010-01-01T19:23:24Z".parse().unwrap()); - // assert_eq!(credential.proof().unwrap().type_(), "RsaSignature2018"); - // - // assert_eq!(subject.id.as_ref().unwrap(), "did:example:ebfeb1f712ebc6f1c276e12ec21"); - // assert_eq!(subject.properties["degree"]["type"], "BachelorDegree"); - // assert_eq!( - // subject.properties["degree"]["name"], - // "Bachelor of Science in Mechanical Engineering" - // ); - // } -} diff --git a/identity_credential/src/presentation/jwt_presentation_builder.rs b/identity_credential/src/presentation/jwt_presentation_builder.rs index 096f5c233f..4d81dfc146 100644 --- a/identity_credential/src/presentation/jwt_presentation_builder.rs +++ b/identity_credential/src/presentation/jwt_presentation_builder.rs @@ -6,7 +6,6 @@ use identity_core::common::Object; use identity_core::common::Url; use identity_core::common::Value; -use crate::credential::Credential; use crate::credential::Jwt; use crate::credential::Policy; use crate::credential::RefreshService; @@ -15,28 +14,28 @@ use crate::presentation::Presentation; use super::JwtPresentation; -/// A `PresentationBuilder` is used to create a customized [Presentation]. +/// A `JwtPresentationBuilder` is used to create a customized [JwtPresentation]. #[derive(Clone, Debug)] pub struct JwtPresentationBuilder { pub(crate) context: Vec, pub(crate) id: Option, pub(crate) types: Vec, pub(crate) credentials: Vec, - pub(crate) holder: Option, + pub(crate) holder: Url, pub(crate) refresh_service: Vec, pub(crate) terms_of_use: Vec, pub(crate) properties: T, } impl JwtPresentationBuilder { - /// Creates a new `PresentationBuilder`. - pub fn new(properties: T) -> Self { + /// Creates a new `JwtPresentationBuilder`. + pub fn new(holder: Url, properties: T) -> Self { Self { context: vec![Presentation::::base_context().clone()], id: None, types: vec![Presentation::::base_type().into()], credentials: Vec::new(), - holder: None, + holder, refresh_service: Vec::new(), terms_of_use: Vec::new(), properties, @@ -71,13 +70,6 @@ impl JwtPresentationBuilder { self } - /// Sets the value of the `holder`. - #[must_use] - pub fn holder(mut self, value: Url) -> Self { - self.holder = Some(value); - self - } - /// Adds a value to the `refreshService` set. #[must_use] pub fn refresh_service(mut self, value: RefreshService) -> Self { @@ -125,15 +117,6 @@ impl JwtPresentationBuilder { } } -impl Default for JwtPresentationBuilder -where - T: Default, -{ - fn default() -> Self { - Self::new(T::default()) - } -} - #[cfg(test)] mod tests { use serde_json::json; @@ -155,9 +138,10 @@ mod tests { use crate::credential::Credential; use crate::credential::CredentialBuilder; + use crate::credential::Jwt; use crate::credential::Subject; - use crate::presentation::Presentation; - use crate::presentation::PresentationBuilder; + use crate::presentation::JwtPresentation; + use crate::presentation::JwtPresentationBuilder; fn subject() -> Subject { let json: Value = json!({ @@ -201,34 +185,32 @@ mod tests { .build() .unwrap(); + let credential_jwt = Jwt::new(credential.serialize_jwt().unwrap()); + document .signer(keypair.private()) .method("#key-1") .sign(&mut credential) .unwrap(); - let presentation: Presentation = PresentationBuilder::default() - .type_("ExamplePresentation") - .credential(credential) - .build() - .unwrap(); + let presentation: JwtPresentation = + JwtPresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new()) + .type_("ExamplePresentation") + .credential(credential_jwt) + .build() + .unwrap(); assert_eq!(presentation.context.len(), 1); assert_eq!( presentation.context.get(0).unwrap(), - Presentation::::base_context() + JwtPresentation::::base_context() ); assert_eq!(presentation.types.len(), 2); - assert_eq!(presentation.types.get(0).unwrap(), Presentation::::base_type()); - assert_eq!(presentation.types.get(1).unwrap(), "ExamplePresentation"); - assert_eq!(presentation.verifiable_credential.len(), 1); assert_eq!( - presentation.verifiable_credential.get(0).unwrap().types.get(0).unwrap(), - Credential::::base_type() - ); - assert_eq!( - presentation.verifiable_credential.get(0).unwrap().types.get(1).unwrap(), - "ExampleCredential" + presentation.types.get(0).unwrap(), + JwtPresentation::::base_type() ); + assert_eq!(presentation.types.get(1).unwrap(), "ExamplePresentation"); + assert_eq!(presentation.verifiable_credential.len(), 1); } } diff --git a/identity_credential/src/presentation/jwt_presentation_options.rs b/identity_credential/src/presentation/jwt_presentation_options.rs index 2f9c9a9d05..cf82340fd6 100644 --- a/identity_credential/src/presentation/jwt_presentation_options.rs +++ b/identity_credential/src/presentation/jwt_presentation_options.rs @@ -4,10 +4,17 @@ use identity_core::common::Timestamp; use identity_core::common::Url; +/// Option to be set in the JWT claims of a verifiable presentation. #[derive(Clone, Debug)] pub struct JwtPresentationOptions { + /// Set the presentation's expiration date. + /// Default: `None`. pub expiration_date: Option, + /// Set the issuance date. + /// Default: current datetime current datetime. pub issuance_date: Option, + /// Sets the audience for presentation. + /// Default: `None`. pub audience: Option, } diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index bba0ffe16d..031ce450de 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -10,7 +10,6 @@ use identity_core::common::Context; use identity_core::common::Object; use identity_core::common::OneOrMany; use identity_core::common::Url; -use identity_core::crypto::Proof; use serde::de::DeserializeOwned; use crate::credential::IssuanceDateClaims; @@ -56,21 +55,19 @@ where { pub(super) fn new(presentation: &'presentation JwtPresentation, options: &JwtPresentationOptions) -> Result { let JwtPresentation { - context, - id, - types, - verifiable_credential, - holder: Some(holder_url), - refresh_service, - terms_of_use, - properties, - proof - } = presentation else { - return Err(Error::MissingHolder) - }; + context, + id, + types, + verifiable_credential, + holder, + refresh_service, + terms_of_use, + properties, + proof, + } = presentation; Ok(Self { - iss: Cow::Borrowed(holder_url), + iss: Cow::Borrowed(holder), jti: id.as_ref().map(Cow::Borrowed), vp: InnerPresentation { context: Cow::Borrowed(context), @@ -118,7 +115,7 @@ where properties: Cow<'presentation, T>, /// Proof(s) used to verify a `Presentation` #[serde(skip_serializing_if = "Option::is_none")] - proof: Option>, + proof: Option>, } impl<'presentation, T> PresentationJwtClaims<'presentation, T> @@ -128,16 +125,16 @@ where pub(crate) fn try_into_presentation(self) -> Result> { self.check_consistency()?; let Self { - exp, + exp: _, iss, - issuance_date, + issuance_date: _, jti, - aud, + aud: _, vp, } = self; let InnerPresentation { context, - id, + id: _, types, verifiable_credential, refresh_service, @@ -151,7 +148,7 @@ where id: jti.map(Cow::into_owned), types: types.into_owned(), verifiable_credential: verifiable_credential.into_owned(), - holder: Some(iss.into_owned()), + holder: iss.into_owned(), refresh_service: refresh_service.into_owned(), terms_of_use: terms_of_use.into_owned(), properties: properties.into_owned(), diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index a852515896..d78416fd10 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -6,20 +6,20 @@ use identity_verification::jws::JwsHeader; use crate::{presentation::JwtPresentation, validator::vc_jwt_validation::DecodedJwtCredential}; -/// Decoded [`Credential`] from a cryptographically verified JWS. +/// Decoded [`JwtPresentation`] from a cryptographically verified JWS. /// Note that having an instance of this type only means the JWS it was constructed from was verified. -/// It does not imply anything about a potentially present proof property on the credential itself. +/// It does not imply anything about a potentially present proof property on the presentation itself. #[non_exhaustive] #[derive(Debug, Clone)] pub struct DecodedJwtPresentation { - /// The decoded credential parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). + /// The decoded presentation parsed to the [Verifiable Credentials Data model](https://www.w3.org/TR/vc-data-model/). pub presentation: JwtPresentation, /// The protected header parsed from the JWS. pub header: Box, - + /// The expiration dated parsed from the JWT claims. pub expiration_date: Option, - + /// The `aud` property parsed from the JWT claims. pub aud: Option, - + /// The credentials included in the presentation (decoded). pub credentials: Vec>, } diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 640a219a1c..bf4110e23a 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -29,11 +29,13 @@ use super::CompoundJwtPresentationValidationError; use super::DecodedJwtPresentation; use super::JwtPresentationValidationOptions; +/// Struct for validating [`JwtPresentation`]. #[derive(Debug, Clone)] #[non_exhaustive] pub struct PresentationJwtValidator(V); impl PresentationJwtValidator { + /// Creates a new [`PresentationJwtValidator`]. pub fn new() -> Self { Self(EdDSAJwsVerifier::default()) } @@ -42,10 +44,37 @@ impl PresentationJwtValidator where V: JwsVerifier, { + /// Creates a new [`PresentationJwtValidator`] using a specific [`JwsVerifier`]. pub fn with_signature_verifier(signature_verifier: V) -> Self { Self(signature_verifier) } + /// Validates a [`JwtPresentation`]. + /// + /// The following properties are validated according to `options`: + /// - the JWT can be decoded into semantically valid presentation, + /// - the expiration and issuance date contained in the JWT claims. + /// - the holder's signature, + /// - the relationship between the holder and the credential subjects, + /// - the signatures and some properties of the constituent credentials (see [`CredentialValidator`]). + /// + /// Validation is done with respect to the properties set in [`options`]. + /// + /// # Warning + /// The lack of an error returned from this method is in of itself not enough to conclude that the presentation can be + /// trusted. This section contains more information on additional checks that should be carried out before and after + /// 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. + /// + /// ## 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: + /// `credentialStatus`, `type`, `credentialSchema`, `refreshService`, **and more**. + /// These should be manually checked after validation, according to your requirements. + /// + /// # Errors + /// An error is returned whenever a validated condition is not satisfied or when decoding fails. pub fn validate( &self, presentation: &Jwt, @@ -151,6 +180,7 @@ where Ok(decoded_jwt_presentation) } + /// Extracts the holder from a JWT presentation.. pub fn extract_holder(presentation: &Jwt) -> std::result::Result where T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, @@ -194,10 +224,7 @@ where credential, issuers, &options.shared_validation_options, - presentation - .holder - .as_ref() - .map(|holder_url| (holder_url, options.subject_holder_relationship)), + Some((&presentation.holder, options.subject_holder_relationship)), fail_fast, ) }) From 52f4b2e00fe6fc3a143cc3ec7819a9361f0a1c73 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 29 May 2023 19:28:34 +0200 Subject: [PATCH 11/31] clean up tests --- .../credential_jwt_validator.rs | 2 +- .../src/validator/vc_jwt_validation/error.rs | 5 +- .../presentation_jwt_validator.rs | 2 +- identity_storage/src/storage/tests/api.rs | 157 ------------------ .../storage/tests/credential_validation.rs | 10 +- .../storage/tests/presentation_validation.rs | 20 +-- 6 files changed, 20 insertions(+), 176 deletions(-) diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs index e84c2f464c..77ee421c58 100644 --- a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs +++ b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs @@ -35,7 +35,7 @@ use crate::validator::SubjectHolderRelationship; #[non_exhaustive] pub struct CredentialValidator(V); -pub type ValidationUnitResult = std::result::Result; +type ValidationUnitResult = std::result::Result; impl CredentialValidator where diff --git a/identity_credential/src/validator/vc_jwt_validation/error.rs b/identity_credential/src/validator/vc_jwt_validation/error.rs index 8845463e90..d98adf37bf 100644 --- a/identity_credential/src/validator/vc_jwt_validation/error.rs +++ b/identity_credential/src/validator/vc_jwt_validation/error.rs @@ -14,6 +14,7 @@ pub enum ValidationError { #[error("could not decode jws")] JwsDecodingError(#[source] identity_verification::jose::error::Error), + /// Indicates error while verifying the JWS of a presentation. #[error("could not verify jws")] PresentationJwsError(#[source] identity_document::error::Error), @@ -39,10 +40,10 @@ pub enum ValidationError { signer_ctx: SignerContext, }, - /// Indicates that the expiration date of the credential is not considered valid. + /// Indicates that the expiration date of the credential or presentation is not considered valid. #[error("the expiration date is in the past or earlier than required")] ExpirationDate, - /// Indicates that the issuance date of the credential is not considered valid. + /// Indicates that the issuance date of the credential or presentation 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 diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index bf4110e23a..3d27d1f09c 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -152,7 +152,7 @@ where (issuance_date.is_none() || issuance_date <= Some(options.latest_issuance_date.unwrap_or_default())) .then_some(()) .ok_or(CompoundJwtPresentationValidationError::one_prsentation_error( - ValidationError::ExpirationDate, + ValidationError::IssuanceDate, ))?; let aud = claims.aud.clone(); diff --git a/identity_storage/src/storage/tests/api.rs b/identity_storage/src/storage/tests/api.rs index 9cd0ddf038..994a538a9b 100644 --- a/identity_storage/src/storage/tests/api.rs +++ b/identity_storage/src/storage/tests/api.rs @@ -1,18 +1,12 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::thread::Builder; - use identity_core::common::Object; use identity_core::convert::FromJson; use identity_credential::credential::Credential; -use identity_credential::presentation::JwtPresentation; -use identity_credential::presentation::JwtPresentationBuilder; -use identity_credential::presentation::JwtPresentationOptions; use identity_credential::validator::vc_jwt_validation::CredentialValidationOptions; use identity_did::DIDUrl; -use identity_did::DID; use identity_document::document::CoreDocument; use identity_document::verifiable::JwsVerificationOptions; use identity_verification::jose::jws::EdDSAJwsVerifier; @@ -206,157 +200,6 @@ async fn signing_credential() { .is_ok()); } -#[tokio::test] -async fn signing_presentation() { - let (mut issuer_document, issuer_storage) = setup(); - let (mut holder_document, holder_storage) = setup(); - - let method_fragment_issuer: String = issuer_document - .generate_method( - &issuer_storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await - .unwrap(); - - let method_fragment_holder: String = holder_document - .generate_method( - &holder_storage, - JwkMemStore::ED25519_KEY_TYPE, - JwsAlgorithm::EdDSA, - None, - MethodScope::VerificationMethod, - ) - .await - .unwrap(); - - let credential_json: &str = r#" - { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "id": "http://example.edu/credentials/3732", - "type": ["VerifiableCredential", "UniversityDegreeCredential"], - "issuer": "did:bar:Hyx62wPQGyvXCoihZq1BrbUjBRh2LuNxWiiqMkfAuSZr", - "issuanceDate": "2010-01-01T19:23:24Z", - "credentialSubject": { - "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science in Mechanical Engineering" - } - } - }"#; - - let credential: Credential = Credential::from_json(credential_json).unwrap(); - let jws = issuer_document - .sign_credential( - &credential, - &issuer_storage, - &method_fragment_issuer, - &JwsSignatureOptions::default(), - ) - .await - .unwrap(); - - let presentation: JwtPresentation = JwtPresentationBuilder::default() - .holder(holder_document.id().to_url().into()) - .credential(jws) - .build() - .unwrap(); - - println!("{}", presentation); - println!("{:?}", presentation.serialize_jwt(&JwtPresentationOptions::default())); - - let jws_2 = holder_document - .sign_presentation( - &presentation, - &holder_storage, - &method_fragment_holder, - &JwsSignatureOptions::default(), - &JwtPresentationOptions::default(), - ) - .await - .unwrap(); - println!("{}", jws_2.as_str()); - - // let validator = identity_credential::validator::vc_jwt_validation::CredentialValidator::new(); - // assert!(validator - // .validate::<_, Object>( - // &jws, - // &issuer_document, - // &CredentialValidationOptions::default(), - // identity_credential::validator::FailFast::FirstError - // ) - // .is_ok()); - - // let (mut issuer, storage) = setup(); - // let (mut holder, storage2) = setup(); - // - // // Generate a method with the kid as fragment - // let fragment: String = issuer - // .generate_method( - // &storage, - // JwkMemStore::ED25519_KEY_TYPE, - // JwsAlgorithm::EdDSA, - // None, - // MethodScope::VerificationMethod, - // ) - // .await - // .unwrap(); - // - // let credential_json: &str = r#" - // { - // "@context": [ - // "https://www.w3.org/2018/credentials/v1", - // "https://www.w3.org/2018/credentials/examples/v1" - // ], - // "id": "http://example.edu/credentials/3732", - // "type": ["VerifiableCredential", "UniversityDegreeCredential"], - // "issuer": "did:bar:Hyx62wPQGyvXCoihZq1BrbUjBRh2LuNxWiiqMkfAuSZr", - // "issuanceDate": "2010-01-01T19:23:24Z", - // "credentialSubject": { - // "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", - // "degree": { - // "type": "BachelorDegree", - // "name": "Bachelor of Science in Mechanical Engineering" - // } - // } - // }"#; - // - // let credential: Credential = Credential::from_json(credential_json).unwrap(); - // let jws = issuer - // .sign_credential( - // &credential, - // &storage, - // kid.as_deref().unwrap(), - // &JwsSignatureOptions::default(), - // ) - // .await - // .unwrap(); - // - // let presentation: JwtPresentation = JwtPresentationBuilder::default() - // .holder(holder.id().to_url().into()) - // .build() - // .unwrap(); - // - // println!("{}", presentation); - // - // // Verify the credential - // let validator = identity_credential::validator::vc_jwt_validation::CredentialValidator::new(); - // assert!(validator - // .validate::<_, Object>( - // &jws, - // &document, - // &CredentialValidationOptions::default(), - // identity_credential::validator::FailFast::FirstError - // ) - // .is_ok()); -} #[tokio::test] async fn purging() { let (mut document, storage) = setup(); diff --git a/identity_storage/src/storage/tests/credential_validation.rs b/identity_storage/src/storage/tests/credential_validation.rs index 12a51f77d8..3b048ce98e 100644 --- a/identity_storage/src/storage/tests/credential_validation.rs +++ b/identity_storage/src/storage/tests/credential_validation.rs @@ -26,9 +26,9 @@ use identity_document::verifiable::JwsVerificationOptions; use once_cell::sync::Lazy; use proptest::proptest; +use crate::storage::tests::test_utils; use crate::storage::tests::test_utils::CredentialSetup; use crate::storage::tests::test_utils::Setup; -use crate::storage::tests::test_utils::{self}; use crate::storage::JwkDocumentExt; use crate::storage::JwsSignatureOptions; @@ -97,8 +97,8 @@ where subject_doc, issuer_storage: storage, issuer_method_fragment: method_fragment, - subject_storage, - subject_method_fragment, + subject_storage: _, + subject_method_fragment: _, } = setup; let CredentialSetup { @@ -179,8 +179,8 @@ where subject_doc, issuer_storage: storage, issuer_method_fragment: method_fragment, - subject_storage, - subject_method_fragment, + subject_storage: _, + subject_method_fragment: _, } = setup; let CredentialSetup { diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 441fbcf5c7..9e940e566c 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -31,11 +31,11 @@ where let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); let jws = sign_credential(&setup, &credential.credential).await; - let presentation: JwtPresentation = JwtPresentationBuilder::default() - .holder(setup.subject_doc.as_ref().id().to_url().into()) - .credential(jws) - .build() - .unwrap(); + let presentation: JwtPresentation = + JwtPresentationBuilder::new(setup.subject_doc.as_ref().id().to_url().into(), Object::new()) + .credential(jws) + .build() + .unwrap(); let presentation_jwt = setup .subject_doc @@ -74,11 +74,11 @@ where let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); let jws = sign_credential(&setup, &credential.credential).await; - let presentation: JwtPresentation = JwtPresentationBuilder::default() - .holder(setup.subject_doc.as_ref().id().to_url().into()) - .credential(jws) - .build() - .unwrap(); + let presentation: JwtPresentation = + JwtPresentationBuilder::new(setup.subject_doc.as_ref().id().to_url().into(), Object::new()) + .credential(jws) + .build() + .unwrap(); // Sign presentation using the issuer's method and try to verify it using the holder's document. // Since the holder's document doesn't include that verification method, Error is returned. From 4b0cbf7954e483203ad7ff2d3108341195b0b205 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 29 May 2023 19:30:24 +0200 Subject: [PATCH 12/31] fix format issues --- .../decoded_jwt_presentation.rs | 7 +++-- .../presentation_jwt_validation_options.rs | 3 ++- .../storage/tests/presentation_validation.rs | 26 +++++++++++-------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index d78416fd10..14e164c3b6 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -1,10 +1,13 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use identity_core::common::{Object, Timestamp, Url}; +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; use identity_verification::jws::JwsHeader; -use crate::{presentation::JwtPresentation, validator::vc_jwt_validation::DecodedJwtCredential}; +use crate::presentation::JwtPresentation; +use crate::validator::vc_jwt_validation::DecodedJwtCredential; /// Decoded [`JwtPresentation`] from a cryptographically verified JWS. /// Note that having an instance of this type only means the JWS it was constructed from was verified. diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs index 272df02ff8..25291c00d5 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs @@ -1,7 +1,8 @@ use identity_core::common::Timestamp; use identity_document::verifiable::JwsVerificationOptions; -use crate::validator::{vc_jwt_validation::CredentialValidationOptions, SubjectHolderRelationship}; +use crate::validator::vc_jwt_validation::CredentialValidationOptions; +use crate::validator::SubjectHolderRelationship; /// Criteria for validating a [`Presentation`](crate::presentation::Presentation), such as with /// [`PresentationValidator::validate`](crate::validator::PresentationValidator::validate()). diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 9e940e566c..7add5f9060 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -2,20 +2,24 @@ // SPDX-License-Identifier: Apache-2.0 use identity_core::common::Object; -use identity_credential::{ - credential::{Credential, Jwt}, - presentation::{JwtPresentation, JwtPresentationBuilder, JwtPresentationOptions}, - validator::{ - vc_jwt_validation::ValidationError, FailFast, JwtPresentationValidationOptions, PresentationJwtValidator, - }, -}; +use identity_credential::credential::Credential; +use identity_credential::credential::Jwt; +use identity_credential::presentation::JwtPresentation; +use identity_credential::presentation::JwtPresentationBuilder; +use identity_credential::presentation::JwtPresentationOptions; +use identity_credential::validator::vc_jwt_validation::ValidationError; +use identity_credential::validator::FailFast; +use identity_credential::validator::JwtPresentationValidationOptions; +use identity_credential::validator::PresentationJwtValidator; use identity_did::DID; use identity_document::document::CoreDocument; -use crate::{ - storage::tests::test_utils::{generate_credential, setup_coredocument, setup_iotadocument, Setup}, - JwkDocumentExt, JwsSignatureOptions, -}; +use crate::storage::tests::test_utils::generate_credential; +use crate::storage::tests::test_utils::setup_coredocument; +use crate::storage::tests::test_utils::setup_iotadocument; +use crate::storage::tests::test_utils::Setup; +use crate::JwkDocumentExt; +use crate::JwsSignatureOptions; use super::test_utils::CredentialSetup; From ba59de07e7f933198a5fad00b3e53d039bc58f67 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 29 May 2023 19:36:20 +0200 Subject: [PATCH 13/31] fix clippy issues --- .../vp_jwt_validation/presentation_jwt_validator.rs | 8 +++++++- .../src/storage/tests/presentation_validation.rs | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 3d27d1f09c..f254272bf3 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -40,6 +40,12 @@ impl PresentationJwtValidator { Self(EdDSAJwsVerifier::default()) } } +impl Default for PresentationJwtValidator { + fn default() -> Self { + Self::new() + } +} + impl PresentationJwtValidator where V: JwsVerifier, @@ -91,7 +97,7 @@ where { // Verify that holder document matches holder in presentation. let holder_did: CoreDID = Self::extract_holder::(presentation) - .map_err(|err| CompoundJwtPresentationValidationError::one_prsentation_error(err))?; + .map_err(CompoundJwtPresentationValidationError::one_prsentation_error)?; if &holder_did != ::id(holder.as_ref()) { return Err(CompoundJwtPresentationValidationError::one_prsentation_error( diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 7add5f9060..44c86aac36 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -58,7 +58,7 @@ where .validate::<_, _, Object, Object>( &presentation_jwt, &setup.subject_doc, - &vec![setup.issuer_doc], + &[setup.issuer_doc], &JwtPresentationValidationOptions::default(), FailFast::FirstError, ) @@ -104,7 +104,7 @@ where .validate::<_, _, Object, Object>( &presentation_jwt, &setup.subject_doc, - &vec![setup.issuer_doc], + &[setup.issuer_doc], &JwtPresentationValidationOptions::default(), FailFast::FirstError, ) From 4ab1e0b9b212efdd19fe231f2f1cdc6341dede0c Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 29 May 2023 19:39:46 +0200 Subject: [PATCH 14/31] fix `builder(..)` in JwtPresentation --- identity_credential/src/presentation/jwt_presentation.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/identity_credential/src/presentation/jwt_presentation.rs b/identity_credential/src/presentation/jwt_presentation.rs index fc707a1189..a0b0b173f6 100644 --- a/identity_credential/src/presentation/jwt_presentation.rs +++ b/identity_credential/src/presentation/jwt_presentation.rs @@ -21,7 +21,6 @@ use crate::credential::Policy; use crate::credential::RefreshService; use crate::error::Error; use crate::error::Result; -use crate::presentation::PresentationBuilder; use super::jwt_serialization::PresentationJwtClaims; use super::JwtPresentationBuilder; @@ -73,8 +72,8 @@ impl JwtPresentation { /// Creates a `JwtPresentationBuilder` to configure a new Presentation. /// /// This is the same as [JwtPresentationBuilder::new]. - pub fn builder(properties: T) -> PresentationBuilder { - PresentationBuilder::new(properties) + pub fn builder(holder: Url, properties: T) -> JwtPresentationBuilder { + JwtPresentationBuilder::new(holder, properties) } /// Returns a new `JwtPresentation` based on the `JwtPresentationBuilder` configuration. From 7cd28d254999ea8d5e664b47cccf72756cf1e887 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Mon, 29 May 2023 22:11:37 +0200 Subject: [PATCH 15/31] fix test --- .../src/presentation/jwt_presentation_builder.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/identity_credential/src/presentation/jwt_presentation_builder.rs b/identity_credential/src/presentation/jwt_presentation_builder.rs index 4d81dfc146..7728eab340 100644 --- a/identity_credential/src/presentation/jwt_presentation_builder.rs +++ b/identity_credential/src/presentation/jwt_presentation_builder.rs @@ -172,13 +172,13 @@ mod tests { .build() .unwrap(); - let document: CoreDocument = DocumentBuilder::default() + let _document: CoreDocument = DocumentBuilder::default() .id(controller) .verification_method(method) .build() .unwrap(); - let mut credential: Credential = CredentialBuilder::default() + let credential: Credential = CredentialBuilder::default() .type_("ExampleCredential") .subject(subject()) .issuer(issuer()) @@ -187,12 +187,6 @@ mod tests { let credential_jwt = Jwt::new(credential.serialize_jwt().unwrap()); - document - .signer(keypair.private()) - .method("#key-1") - .sign(&mut credential) - .unwrap(); - let presentation: JwtPresentation = JwtPresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new()) .type_("ExamplePresentation") From c92e016f49fabf43d1a33e982d472dd434b0c948 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 30 May 2023 00:12:33 +0200 Subject: [PATCH 16/31] fix issuance date, add more tests --- .../presentation/jwt_presentation_options.rs | 4 +- .../decoded_jwt_presentation.rs | 2 + .../presentation_jwt_validator.rs | 1 + .../src/storage/signature_options.rs | 2 +- .../storage/tests/presentation_validation.rs | 273 +++++++++++++++++- 5 files changed, 272 insertions(+), 10 deletions(-) diff --git a/identity_credential/src/presentation/jwt_presentation_options.rs b/identity_credential/src/presentation/jwt_presentation_options.rs index cf82340fd6..6ed8e1e9ac 100644 --- a/identity_credential/src/presentation/jwt_presentation_options.rs +++ b/identity_credential/src/presentation/jwt_presentation_options.rs @@ -11,9 +11,9 @@ pub struct JwtPresentationOptions { /// Default: `None`. pub expiration_date: Option, /// Set the issuance date. - /// Default: current datetime current datetime. + /// Default: current datetime. pub issuance_date: Option, - /// Sets the audience for presentation. + /// Sets the audience for presentation (`aud` property in JWT claims). /// Default: `None`. pub audience: Option, } diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index 14e164c3b6..4b231a9f5d 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -21,6 +21,8 @@ pub struct DecodedJwtPresentation { pub header: Box, /// The expiration dated parsed from the JWT claims. pub expiration_date: Option, + /// The issuance dated parsed from the JWT claims. + pub issuance_date: Option, /// The `aud` property parsed from the JWT claims. pub aud: Option, /// The credentials included in the presentation (decoded). diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index f254272bf3..8f83cf12ed 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -179,6 +179,7 @@ where presentation, header: Box::new(decoded_jws.protected), expiration_date, + issuance_date, aud, credentials, }; diff --git a/identity_storage/src/storage/signature_options.rs b/identity_storage/src/storage/signature_options.rs index 604c7227e3..553d7e2f60 100644 --- a/identity_storage/src/storage/signature_options.rs +++ b/identity_storage/src/storage/signature_options.rs @@ -11,10 +11,10 @@ pub struct JwsSignatureOptions { /// to the JWS header. pub attach_jwk: bool, - #[serde(skip_serializing_if = "Option::is_none")] /// Whether to Base64url encode the payload or not. /// /// [More Info](https://tools.ietf.org/html/rfc7797#section-3) + #[serde(skip_serializing_if = "Option::is_none")] pub b64: Option, /// The Type value to be placed in the protected header. diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 44c86aac36..3ca5727759 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -1,34 +1,45 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_core::common::Duration; use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; use identity_credential::credential::Credential; use identity_credential::credential::Jwt; use identity_credential::presentation::JwtPresentation; use identity_credential::presentation::JwtPresentationBuilder; use identity_credential::presentation::JwtPresentationOptions; use identity_credential::validator::vc_jwt_validation::ValidationError; +use identity_credential::validator::DecodedJwtPresentation; use identity_credential::validator::FailFast; use identity_credential::validator::JwtPresentationValidationOptions; use identity_credential::validator::PresentationJwtValidator; use identity_did::DID; use identity_document::document::CoreDocument; +use identity_verification::jws::JwsAlgorithm; +use identity_verification::jws::SignatureVerificationError; +use identity_verification::MethodScope; use crate::storage::tests::test_utils::generate_credential; use crate::storage::tests::test_utils::setup_coredocument; use crate::storage::tests::test_utils::setup_iotadocument; use crate::storage::tests::test_utils::Setup; use crate::JwkDocumentExt; +use crate::JwkMemStore; +use crate::JwkStorage; use crate::JwsSignatureOptions; +use crate::KeyType; +use crate::MethodDigest; use super::test_utils::CredentialSetup; #[tokio::test] -async fn test_presentation() { - test_presentation_impl(setup_coredocument(None, None).await).await; - test_presentation_impl(setup_iotadocument(None, None).await).await; +async fn test_valid_presentation() { + test_valid_presentation_impl(setup_coredocument(None, None).await).await; + test_valid_presentation_impl(setup_iotadocument(None, None).await).await; } -async fn test_presentation_impl(setup: Setup) +async fn test_valid_presentation_impl(setup: Setup) where T: JwkDocumentExt + AsRef, { @@ -41,6 +52,12 @@ where .build() .unwrap(); + let presentation_options = JwtPresentationOptions { + expiration_date: Some(Timestamp::now_utc().checked_add(Duration::hours(10)).unwrap()), + issuance_date: Some(Timestamp::now_utc().checked_sub(Duration::hours(10)).unwrap()), + audience: Some(Url::parse("did:test:123").unwrap()), + }; + let presentation_jwt = setup .subject_doc .sign_presentation( @@ -48,13 +65,13 @@ where &setup.subject_storage, &setup.subject_method_fragment, &JwsSignatureOptions::default(), - &JwtPresentationOptions::default(), + &presentation_options, ) .await .unwrap(); let validator: PresentationJwtValidator = PresentationJwtValidator::new(); - validator + let decoded_presentation: DecodedJwtPresentation = validator .validate::<_, _, Object, Object>( &presentation_jwt, &setup.subject_doc, @@ -63,6 +80,248 @@ where FailFast::FirstError, ) .unwrap(); + + assert_eq!( + decoded_presentation.expiration_date, + presentation_options.expiration_date + ); + assert_eq!(decoded_presentation.issuance_date, presentation_options.issuance_date); + assert_eq!(decoded_presentation.aud, presentation_options.audience); + assert_eq!( + decoded_presentation.credentials.into_iter().next().unwrap().credential, + credential.credential + ); +} + +// > Create a VP signed by a verification method with `subject_method_fragment`. +// > Replace the verification method but keep the same fragment. +// > Validation fails due to invalid signature since key material changed. +#[tokio::test] +async fn test_invalid_signature() { + test_invalid_signature_impl(setup_coredocument(None, None).await).await; + test_invalid_signature_impl(setup_iotadocument(None, None).await).await; +} +async fn test_invalid_signature_impl(mut setup: Setup) +where + T: JwkDocumentExt + AsRef, +{ + let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); + let jws = sign_credential(&setup, &credential.credential).await; + + let presentation: JwtPresentation = + JwtPresentationBuilder::new(setup.subject_doc.as_ref().id().to_url().into(), Object::new()) + .credential(jws) + .build() + .unwrap(); + + let presentation_options = JwtPresentationOptions { + expiration_date: Some(Timestamp::now_utc().checked_add(Duration::hours(10)).unwrap()), + issuance_date: Some(Timestamp::now_utc().checked_sub(Duration::hours(10)).unwrap()), + audience: Some(Url::parse("did:test:123").unwrap()), + }; + + let presentation_jwt = setup + .subject_doc + .sign_presentation( + &presentation, + &setup.subject_storage, + &setup.subject_method_fragment, + &JwsSignatureOptions::default(), + &presentation_options, + ) + .await + .unwrap(); + + let method_url = setup + .subject_doc + .as_ref() + .id() + .to_url() + .clone() + .join(format!("#{}", setup.subject_method_fragment.clone())) + .unwrap(); + + setup + .subject_doc + .purge_method(&setup.subject_storage, &method_url) + .await + .unwrap(); + + setup + .subject_doc + .generate_method( + &setup.subject_storage, + JwkMemStore::ED25519_KEY_TYPE, + JwsAlgorithm::EdDSA, + Some(&setup.subject_method_fragment), + MethodScope::assertion_method(), + ) + .await + .unwrap(); + // setup.subject_doc.generate_method(setup.subject_storage, KeyType::, alg, fragment, scope) + let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validation_error: ValidationError = validator + .validate::<_, _, Object, Object>( + &presentation_jwt, + &setup.subject_doc, + &[&setup.issuer_doc], + &JwtPresentationValidationOptions::default(), + FailFast::FirstError, + ) + .err() + .unwrap() + .presentation_validation_errors + .into_iter() + .next() + .unwrap(); + + assert!(matches!( + validation_error, + ValidationError::PresentationJwsError(identity_document::Error::JwsVerificationError(_)) + )); +} + +#[tokio::test] +async fn expiration_date() { + expiration_date_impl(setup_coredocument(None, None).await).await; + expiration_date_impl(setup_iotadocument(None, None).await).await; +} +async fn expiration_date_impl(setup: Setup) +where + T: JwkDocumentExt + AsRef + Clone, +{ + let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); + let jws = sign_credential(&setup, &credential.credential).await; + + let presentation: JwtPresentation = + JwtPresentationBuilder::new(setup.subject_doc.as_ref().id().to_url().into(), Object::new()) + .credential(jws) + .build() + .unwrap(); + + // Presentation expired in the past must be invalid. + + let mut presentation_options = JwtPresentationOptions::default(); + presentation_options.expiration_date = Some(Timestamp::now_utc().checked_sub(Duration::hours(1)).unwrap()); + + let presentation_jwt = setup + .subject_doc + .sign_presentation( + &presentation, + &setup.subject_storage, + &setup.subject_method_fragment, + &JwsSignatureOptions::default(), + &presentation_options, + ) + .await + .unwrap(); + + let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validation_error: ValidationError = validator + .validate::<_, _, Object, Object>( + &presentation_jwt, + &setup.subject_doc, + &[&setup.issuer_doc], + &JwtPresentationValidationOptions::default(), + FailFast::FirstError, + ) + .err() + .unwrap() + .presentation_validation_errors + .into_iter() + .next() + .unwrap(); + + assert!(matches!(validation_error, ValidationError::ExpirationDate)); + + // Set Validation options to allow expired presentation that were valid 2 hours back. + + let mut validation_options = JwtPresentationValidationOptions::default(); + validation_options = + validation_options.earliest_expiry_date(Timestamp::now_utc().checked_sub(Duration::hours(2)).unwrap()); + + let validation_ok: bool = validator + .validate::<_, _, Object, Object>( + &presentation_jwt, + &setup.subject_doc, + &[&setup.issuer_doc], + &validation_options, + FailFast::FirstError, + ) + .is_ok(); + assert!(validation_ok); +} + +#[tokio::test] +async fn issuance_date() { + issuance_date_impl(setup_coredocument(None, None).await).await; + issuance_date_impl(setup_iotadocument(None, None).await).await; +} + +async fn issuance_date_impl(setup: Setup) +where + T: JwkDocumentExt + AsRef + Clone, +{ + let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); + let jws = sign_credential(&setup, &credential.credential).await; + + let presentation: JwtPresentation = + JwtPresentationBuilder::new(setup.subject_doc.as_ref().id().to_url().into(), Object::new()) + .credential(jws) + .build() + .unwrap(); + + // Presentation issued in the future must be invalid. + + let mut presentation_options = JwtPresentationOptions::default(); + presentation_options.issuance_date = Some(Timestamp::now_utc().checked_add(Duration::hours(1)).unwrap()); + + let presentation_jwt = setup + .subject_doc + .sign_presentation( + &presentation, + &setup.subject_storage, + &setup.subject_method_fragment, + &JwsSignatureOptions::default(), + &presentation_options, + ) + .await + .unwrap(); + + let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validation_error: ValidationError = validator + .validate::<_, _, Object, Object>( + &presentation_jwt, + &setup.subject_doc, + &[&setup.issuer_doc], + &JwtPresentationValidationOptions::default(), + FailFast::FirstError, + ) + .err() + .unwrap() + .presentation_validation_errors + .into_iter() + .next() + .unwrap(); + + assert!(matches!(validation_error, ValidationError::IssuanceDate)); + + // Set Validation options to allow presentation "issued" 2 hours in the future. + + let mut validation_options = JwtPresentationValidationOptions::default(); + validation_options = + validation_options.latest_issuance_date(Timestamp::now_utc().checked_add(Duration::hours(2)).unwrap()); + + let validation_ok: bool = validator + .validate::<_, _, Object, Object>( + &presentation_jwt, + &setup.subject_doc, + &[&setup.issuer_doc], + &validation_options, + FailFast::FirstError, + ) + .is_ok(); + assert!(validation_ok); } #[tokio::test] @@ -85,7 +344,7 @@ where .unwrap(); // Sign presentation using the issuer's method and try to verify it using the holder's document. - // Since the holder's document doesn't include that verification method, Error is returned. + // Since the holder's document doesn't include that verification method, `MethodNotFound`is returned. let presentation_jwt = setup .issuer_doc From 59317957c0fb83279a8dd3b5884ab9a6116c0fcb Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 30 May 2023 00:32:46 +0200 Subject: [PATCH 17/31] small improvements --- .../src/presentation/jwt_presentation.rs | 2 -- .../src/presentation/jwt_serialization.rs | 19 ++++++++--------- .../decoded_jwt_presentation.rs | 2 +- .../src/validator/vp_jwt_validation/error.rs | 2 +- .../presentation_jwt_validation_options.rs | 7 +------ .../presentation_jwt_validator.rs | 6 +++--- .../storage/tests/presentation_validation.rs | 21 +++++++++++-------- 7 files changed, 27 insertions(+), 32 deletions(-) diff --git a/identity_credential/src/presentation/jwt_presentation.rs b/identity_credential/src/presentation/jwt_presentation.rs index a0b0b173f6..af9f6ac50f 100644 --- a/identity_credential/src/presentation/jwt_presentation.rs +++ b/identity_credential/src/presentation/jwt_presentation.rs @@ -42,7 +42,6 @@ pub struct JwtPresentation { #[serde(default = "Default::default", rename = "verifiableCredential")] pub verifiable_credential: OneOrMany, /// The entity that generated the `Presentation`. - #[serde()] pub holder: Url, /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] @@ -89,7 +88,6 @@ impl JwtPresentation { properties: builder.properties, proof: None, }; - this.check_structure()?; Ok(this) diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index 031ce450de..fc48c1c6ea 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -31,8 +31,8 @@ where /// Represents the expirationDate encoded as a UNIX timestamp. #[serde(skip_serializing_if = "Option::is_none")] pub(crate) exp: Option, - /// Represents the issuer of the presentation who is the same as the holder of the verifiable - /// credentials. + + /// Represents the holder of the verifiable presentation. pub(crate) iss: Cow<'presentation, Url>, /// Represents the issuanceDate encoded as a UNIX timestamp. @@ -92,28 +92,28 @@ where T: ToOwned + Serialize, ::Owned: DeserializeOwned, { - /// The JSON-LD context(s) applicable to the `Presentation`. + /// The JSON-LD context(s) applicable to the `JwtPresentation`. #[serde(rename = "@context")] context: Cow<'presentation, OneOrMany>, - /// A unique `URI` that may be used to identify the `Presentation`. + /// A unique `URI` that may be used to identify the `JwtPresentation`. #[serde(skip_serializing_if = "Option::is_none")] id: Option, - /// One or more URIs defining the type of the `Presentation`. + /// One or more URIs defining the type of the `JwtPresentation`. #[serde(rename = "type")] types: Cow<'presentation, OneOrMany>, - /// Credential(s) expressing the claims of the `Presentation`. + /// Credential(s) expressing the claims of the `JwtPresentation`. #[serde(default = "Default::default", rename = "verifiableCredential")] verifiable_credential: Cow<'presentation, OneOrMany>, - /// Service(s) used to refresh an expired [`Credential`] in the `Presentation`. + /// Service(s) used to refresh an expired [`Credential`] in the `JwtPresentation`. #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] refresh_service: Cow<'presentation, OneOrMany>, - /// Terms-of-use specified by the `Presentation` holder. + /// Terms-of-use specified by the `JwtPresentation` holder. #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")] terms_of_use: Cow<'presentation, OneOrMany>, /// Miscellaneous properties. #[serde(flatten)] properties: Cow<'presentation, T>, - /// Proof(s) used to verify a `Presentation` + /// Proof(s) used to verify a `JwtPresentation` #[serde(skip_serializing_if = "Option::is_none")] proof: Option>, } @@ -159,7 +159,6 @@ where } fn check_consistency(&self) -> Result<()> { - // Check consistency of id if !self .vp .id diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index 4b231a9f5d..4b2b8ad1b0 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -19,7 +19,7 @@ pub struct DecodedJwtPresentation { pub presentation: JwtPresentation, /// The protected header parsed from the JWS. pub header: Box, - /// The expiration dated parsed from the JWT claims. + /// The expiration date parsed from the JWT claims. pub expiration_date: Option, /// The issuance dated parsed from the JWT claims. pub issuance_date: Option, diff --git a/identity_credential/src/validator/vp_jwt_validation/error.rs b/identity_credential/src/validator/vp_jwt_validation/error.rs index 6adfd19bc6..92d7b5bad6 100644 --- a/identity_credential/src/validator/vp_jwt_validation/error.rs +++ b/identity_credential/src/validator/vp_jwt_validation/error.rs @@ -9,7 +9,7 @@ use crate::validator::vc_jwt_validation::CompoundCredentialValidationError; use crate::validator::vc_jwt_validation::ValidationError; #[derive(Debug)] -/// An error caused by a failure to validate a Presentation. +/// An error caused by a failure to validate a `JwtPresentation`. pub struct CompoundJwtPresentationValidationError { /// Errors that occurred during validation of individual credentials, mapped by index of their /// order in the presentation. diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs index 25291c00d5..d8f73eaa26 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs @@ -4,8 +4,7 @@ use identity_document::verifiable::JwsVerificationOptions; use crate::validator::vc_jwt_validation::CredentialValidationOptions; use crate::validator::SubjectHolderRelationship; -/// Criteria for validating a [`Presentation`](crate::presentation::Presentation), such as with -/// [`PresentationValidator::validate`](crate::validator::PresentationValidator::validate()). +/// Criteria for validating a [`JwtPresentation`](crate::presentation::JwtPresentation). #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[non_exhaustive] #[serde(rename_all = "camelCase")] @@ -21,10 +20,6 @@ pub struct JwtPresentationValidationOptions { #[serde(default)] pub subject_holder_relationship: SubjectHolderRelationship, - // /// Determines if the JWT expiration date claim `exp` should be skipped during validation. - // /// Default: false. - // #[serde(default)] - // pub skip_exp: bool, /// Declares that the credential is **not** considered valid if it expires before this /// [`Timestamp`]. /// Uses the current datetime during validation if not set. diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 8f83cf12ed..7fefd3fcf0 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -58,10 +58,10 @@ where /// Validates a [`JwtPresentation`]. /// /// The following properties are validated according to `options`: - /// - the JWT can be decoded into semantically valid presentation, + /// - the JWT can be decoded into semantically valid presentation. /// - the expiration and issuance date contained in the JWT claims. - /// - the holder's signature, - /// - the relationship between the holder and the credential subjects, + /// - the holder's signature. + /// - the relationship between the holder and the credential subjects. /// - the signatures and some properties of the constituent credentials (see [`CredentialValidator`]). /// /// Validation is done with respect to the properties set in [`options`]. diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 3ca5727759..5dfb544eed 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -18,7 +18,7 @@ use identity_credential::validator::PresentationJwtValidator; use identity_did::DID; use identity_document::document::CoreDocument; use identity_verification::jws::JwsAlgorithm; -use identity_verification::jws::SignatureVerificationError; + use identity_verification::MethodScope; use crate::storage::tests::test_utils::generate_credential; @@ -27,10 +27,8 @@ use crate::storage::tests::test_utils::setup_iotadocument; use crate::storage::tests::test_utils::Setup; use crate::JwkDocumentExt; use crate::JwkMemStore; -use crate::JwkStorage; + use crate::JwsSignatureOptions; -use crate::KeyType; -use crate::MethodDigest; use super::test_utils::CredentialSetup; @@ -137,7 +135,6 @@ where .as_ref() .id() .to_url() - .clone() .join(format!("#{}", setup.subject_method_fragment.clone())) .unwrap(); @@ -201,8 +198,11 @@ where // Presentation expired in the past must be invalid. - let mut presentation_options = JwtPresentationOptions::default(); - presentation_options.expiration_date = Some(Timestamp::now_utc().checked_sub(Duration::hours(1)).unwrap()); + let presentation_options = JwtPresentationOptions { + issuance_date: None, + expiration_date: Some(Timestamp::now_utc().checked_sub(Duration::hours(1)).unwrap()), + audience: None, + }; let presentation_jwt = setup .subject_doc @@ -273,8 +273,11 @@ where // Presentation issued in the future must be invalid. - let mut presentation_options = JwtPresentationOptions::default(); - presentation_options.issuance_date = Some(Timestamp::now_utc().checked_add(Duration::hours(1)).unwrap()); + let presentation_options = JwtPresentationOptions { + issuance_date: Some(Timestamp::now_utc().checked_add(Duration::hours(1)).unwrap()), + expiration_date: None, + audience: None, + }; let presentation_jwt = setup .subject_doc From 1f72d3fe53f163dbe1548b221b1fd0d9e72c27b4 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 30 May 2023 01:24:45 +0200 Subject: [PATCH 18/31] fix error thrown if issuance date not set --- .../src/credential/jwt_serialization.rs | 4 ++-- .../presentation_jwt_validator.rs | 24 +++++++++++-------- .../storage/tests/presentation_validation.rs | 13 ++++++---- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index b1b05f6694..88a2211330 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -233,9 +233,9 @@ where #[derive(Serialize, Deserialize, Clone, Copy)] pub(crate) struct IssuanceDateClaims { #[serde(skip_serializing_if = "Option::is_none")] - iat: Option, + pub(crate) iat: Option, #[serde(skip_serializing_if = "Option::is_none")] - nbf: Option, + pub(crate) nbf: Option, } impl IssuanceDateClaims { diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 7fefd3fcf0..3d5da26d34 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -144,16 +144,20 @@ where ))?; // Check issuance date. - let issuance_date: Option = claims - .issuance_date - .map(|iss| { - iss.to_issuance_date().map_err(|err| { - CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( - crate::Error::JwtClaimsSetDeserializationError(err.into()), - )) - }) - }) - .transpose()?; + let issuance_date: Option = match claims.issuance_date { + Some(iss) => { + if iss.iat.is_some() || iss.nbf.is_some() { + Some(iss.to_issuance_date().map_err(|err| { + CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + crate::Error::JwtClaimsSetDeserializationError(err.into()), + )) + })?) + } else { + None + } + } + None => None, + }; (issuance_date.is_none() || issuance_date <= Some(options.latest_issuance_date.unwrap_or_default())) .then_some(()) diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 5dfb544eed..944795f6e5 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -200,9 +200,11 @@ where let presentation_options = JwtPresentationOptions { issuance_date: None, - expiration_date: Some(Timestamp::now_utc().checked_sub(Duration::hours(1)).unwrap()), + expiration_date: Some(Timestamp::now_utc().checked_sub(Duration::days(1)).unwrap()), audience: None, }; + // let mut presentation_options = JwtPresentationOptions::default(); + // presentation_options.expiration_date = Some(Timestamp::now_utc().checked_sub(Duration::hours(1)).unwrap()); let presentation_jwt = setup .subject_doc @@ -238,9 +240,10 @@ where let mut validation_options = JwtPresentationValidationOptions::default(); validation_options = - validation_options.earliest_expiry_date(Timestamp::now_utc().checked_sub(Duration::hours(2)).unwrap()); + validation_options.earliest_expiry_date(Timestamp::now_utc().checked_sub(Duration::days(2)).unwrap()); - let validation_ok: bool = validator + // let validation_ok: bool = validator + validator .validate::<_, _, Object, Object>( &presentation_jwt, &setup.subject_doc, @@ -248,8 +251,8 @@ where &validation_options, FailFast::FirstError, ) - .is_ok(); - assert!(validation_ok); + .unwrap(); + // assert!(validation_ok); } #[tokio::test] From 048ad2682155dbade7b401c9e9d22fca9ed8ad7d Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 30 May 2023 01:36:01 +0200 Subject: [PATCH 19/31] small improvements --- .../vp_jwt_validation/presentation_jwt_validation_options.rs | 3 +++ .../validator/vp_jwt_validation/presentation_jwt_validator.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs index d8f73eaa26..bb33dd69dc 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs @@ -1,3 +1,6 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + use identity_core::common::Timestamp; use identity_document::verifiable::JwsVerificationOptions; diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 3d5da26d34..52ca407c57 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -64,7 +64,7 @@ where /// - the relationship between the holder and the credential subjects. /// - the signatures and some properties of the constituent credentials (see [`CredentialValidator`]). /// - /// Validation is done with respect to the properties set in [`options`]. + /// Validation is done with respect to the properties set in `options`. /// /// # Warning /// The lack of an error returned from this method is in of itself not enough to conclude that the presentation can be From c7daa63f68f007c69beaacf028be5a587dea5707 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 30 May 2023 21:34:34 +0200 Subject: [PATCH 20/31] fix `validator` feature not compiling --- identity_credential/src/credential/mod.rs | 1 - identity_credential/src/presentation/jwt_serialization.rs | 1 + identity_credential/src/presentation/mod.rs | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 07d7ca0729..6c8f5c4fb2 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -40,5 +40,4 @@ pub use self::schema::Schema; pub use self::status::Status; pub use self::subject::Subject; -#[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::*; diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index fc48c1c6ea..1e46ae16ea 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -118,6 +118,7 @@ where proof: Option>, } +#[cfg(feature = "validator")] impl<'presentation, T> PresentationJwtClaims<'presentation, T> where T: ToOwned + Serialize + DeserializeOwned, diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs index 2f19c481c0..436c06b263 100644 --- a/identity_credential/src/presentation/mod.rs +++ b/identity_credential/src/presentation/mod.rs @@ -18,5 +18,4 @@ pub use self::jwt_presentation_builder::JwtPresentationBuilder; pub use self::jwt_presentation_options::JwtPresentationOptions; pub use self::presentation::Presentation; -#[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::*; From 5e59f54cee5ba3d0df3fe7bbb327bce70a8e8687 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 30 May 2023 21:40:22 +0200 Subject: [PATCH 21/31] rename `PresentationJwtValidator` --- .../vp_jwt_validation/presentation_jwt_validator.rs | 12 ++++++------ .../src/storage/tests/presentation_validation.rs | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 52ca407c57..77af9e912f 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -32,25 +32,25 @@ use super::JwtPresentationValidationOptions; /// Struct for validating [`JwtPresentation`]. #[derive(Debug, Clone)] #[non_exhaustive] -pub struct PresentationJwtValidator(V); +pub struct JwtPresentationValidator(V); -impl PresentationJwtValidator { - /// Creates a new [`PresentationJwtValidator`]. +impl JwtPresentationValidator { + /// Creates a new [`JwtPresentationValidator`]. pub fn new() -> Self { Self(EdDSAJwsVerifier::default()) } } -impl Default for PresentationJwtValidator { +impl Default for JwtPresentationValidator { fn default() -> Self { Self::new() } } -impl PresentationJwtValidator +impl JwtPresentationValidator where V: JwsVerifier, { - /// Creates a new [`PresentationJwtValidator`] using a specific [`JwsVerifier`]. + /// Creates a new [`JwtPresentationValidator`] using a specific [`JwsVerifier`]. pub fn with_signature_verifier(signature_verifier: V) -> Self { Self(signature_verifier) } diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 944795f6e5..be2e68f401 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -14,7 +14,7 @@ use identity_credential::validator::vc_jwt_validation::ValidationError; use identity_credential::validator::DecodedJwtPresentation; use identity_credential::validator::FailFast; use identity_credential::validator::JwtPresentationValidationOptions; -use identity_credential::validator::PresentationJwtValidator; +use identity_credential::validator::JwtPresentationValidator; use identity_did::DID; use identity_document::document::CoreDocument; use identity_verification::jws::JwsAlgorithm; @@ -68,7 +68,7 @@ where .await .unwrap(); - let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validator: JwtPresentationValidator = JwtPresentationValidator::new(); let decoded_presentation: DecodedJwtPresentation = validator .validate::<_, _, Object, Object>( &presentation_jwt, @@ -156,7 +156,7 @@ where .await .unwrap(); // setup.subject_doc.generate_method(setup.subject_storage, KeyType::, alg, fragment, scope) - let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validator: JwtPresentationValidator = JwtPresentationValidator::new(); let validation_error: ValidationError = validator .validate::<_, _, Object, Object>( &presentation_jwt, @@ -218,7 +218,7 @@ where .await .unwrap(); - let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validator: JwtPresentationValidator = JwtPresentationValidator::new(); let validation_error: ValidationError = validator .validate::<_, _, Object, Object>( &presentation_jwt, @@ -294,7 +294,7 @@ where .await .unwrap(); - let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validator: JwtPresentationValidator = JwtPresentationValidator::new(); let validation_error: ValidationError = validator .validate::<_, _, Object, Object>( &presentation_jwt, @@ -364,7 +364,7 @@ where .await .unwrap(); - let validator: PresentationJwtValidator = PresentationJwtValidator::new(); + let validator: JwtPresentationValidator = JwtPresentationValidator::new(); let validation_error: ValidationError = validator .validate::<_, _, Object, Object>( &presentation_jwt, From 01b012fe0d45ffb3010f628e9b497e38bb95887d Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab <31316147+abdulmth@users.noreply.github.com> Date: Thu, 1 Jun 2023 20:19:56 +0200 Subject: [PATCH 22/31] Apply suggestions from code review Co-authored-by: Philipp Gackstatter --- .../src/presentation/jwt_presentation.rs | 12 ++++++++++-- .../src/presentation/jwt_presentation_options.rs | 2 +- .../vp_jwt_validation/decoded_jwt_presentation.rs | 1 + .../presentation_jwt_validation_options.rs | 4 ++-- .../vp_jwt_validation/presentation_jwt_validator.rs | 2 +- identity_storage/src/storage/jwk_document_ext.rs | 2 +- .../src/storage/tests/presentation_validation.rs | 9 --------- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/identity_credential/src/presentation/jwt_presentation.rs b/identity_credential/src/presentation/jwt_presentation.rs index af9f6ac50f..f4046b5661 100644 --- a/identity_credential/src/presentation/jwt_presentation.rs +++ b/identity_credential/src/presentation/jwt_presentation.rs @@ -26,7 +26,7 @@ use super::jwt_serialization::PresentationJwtClaims; use super::JwtPresentationBuilder; use super::JwtPresentationOptions; -/// Represents a bundle of one or more [Credential]s. +/// Represents a bundle of one or more [`Credential`]s expressed as [`Jwt`]s. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct JwtPresentation { /// The JSON-LD context(s) applicable to the `Presentation`. @@ -94,6 +94,11 @@ impl JwtPresentation { } /// Validates the semantic structure of the `JwtPresentation`. + /// + /// # Warning + /// + /// This does not check the semantic structure of the contained credentials. This needs to be done as part of + /// signature validation on the credentials as they are encoded as JWTs. pub fn check_structure(&self) -> Result<()> { // Ensure the base context is present and in the correct location match self.context.get(0) { @@ -125,7 +130,10 @@ impl JwtPresentation { .map_err(|err| Error::JwtClaimsSetSerializationError(err.into())) } - /// Returns a reference to the `JwtPresentation` proof. + /// Returns a reference to the `JwtPresentation` proof, if it exists. + /// + /// Note that this is not the JWS or JWT of the presentation but a separate field that can be used to + /// prove additional claims or include proofs not based on digital signatures like Proof-of-Work. pub fn proof(&self) -> Option<&Object> { self.proof.as_ref() } diff --git a/identity_credential/src/presentation/jwt_presentation_options.rs b/identity_credential/src/presentation/jwt_presentation_options.rs index 6ed8e1e9ac..925ffe0812 100644 --- a/identity_credential/src/presentation/jwt_presentation_options.rs +++ b/identity_credential/src/presentation/jwt_presentation_options.rs @@ -4,7 +4,7 @@ use identity_core::common::Timestamp; use identity_core::common::Url; -/// Option to be set in the JWT claims of a verifiable presentation. +/// Options to be set in the JWT claims of a verifiable presentation. #[derive(Clone, Debug)] pub struct JwtPresentationOptions { /// Set the presentation's expiration date. diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index 4b2b8ad1b0..c81c6a1130 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -10,6 +10,7 @@ use crate::presentation::JwtPresentation; use crate::validator::vc_jwt_validation::DecodedJwtCredential; /// Decoded [`JwtPresentation`] from a cryptographically verified JWS. +/// /// Note that having an instance of this type only means the JWS it was constructed from was verified. /// It does not imply anything about a potentially present proof property on the presentation itself. #[non_exhaustive] diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs index bb33dd69dc..378fae504b 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs @@ -23,13 +23,13 @@ pub struct JwtPresentationValidationOptions { #[serde(default)] pub subject_holder_relationship: SubjectHolderRelationship, - /// Declares that the credential is **not** considered valid if it expires before this + /// Declares that the presentation is **not** considered valid if it expires before this /// [`Timestamp`]. /// Uses the current datetime during validation if not set. #[serde(default)] pub earliest_expiry_date: Option, - /// Declares that the credential is **not** considered valid if it was issued later than this + /// Declares that the presentation is **not** considered valid if it was issued later than this /// [`Timestamp`]. /// Uses the current datetime during validation if not set. #[serde(default)] diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs index 77af9e912f..f9c0dae874 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs @@ -165,7 +165,7 @@ where ValidationError::IssuanceDate, ))?; - let aud = claims.aud.clone(); + let aud: Option = claims.aud.clone(); let presentation: JwtPresentation = claims.try_into_presentation().map_err(|err| { CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index e77a45e213..cb8e607826 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -99,7 +99,7 @@ pub trait JwkDocumentExt: private::Sealed { T: ToOwned + Serialize + DeserializeOwned + Sync; /// Produces a JWS where the payload is produced from the given `presentation` - /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). + /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index be2e68f401..bbc4dafcf5 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -155,7 +155,6 @@ where ) .await .unwrap(); - // setup.subject_doc.generate_method(setup.subject_storage, KeyType::, alg, fragment, scope) let validator: JwtPresentationValidator = JwtPresentationValidator::new(); let validation_error: ValidationError = validator .validate::<_, _, Object, Object>( @@ -197,14 +196,11 @@ where .unwrap(); // Presentation expired in the past must be invalid. - let presentation_options = JwtPresentationOptions { issuance_date: None, expiration_date: Some(Timestamp::now_utc().checked_sub(Duration::days(1)).unwrap()), audience: None, }; - // let mut presentation_options = JwtPresentationOptions::default(); - // presentation_options.expiration_date = Some(Timestamp::now_utc().checked_sub(Duration::hours(1)).unwrap()); let presentation_jwt = setup .subject_doc @@ -237,12 +233,10 @@ where assert!(matches!(validation_error, ValidationError::ExpirationDate)); // Set Validation options to allow expired presentation that were valid 2 hours back. - let mut validation_options = JwtPresentationValidationOptions::default(); validation_options = validation_options.earliest_expiry_date(Timestamp::now_utc().checked_sub(Duration::days(2)).unwrap()); - // let validation_ok: bool = validator validator .validate::<_, _, Object, Object>( &presentation_jwt, @@ -252,7 +246,6 @@ where FailFast::FirstError, ) .unwrap(); - // assert!(validation_ok); } #[tokio::test] @@ -275,7 +268,6 @@ where .unwrap(); // Presentation issued in the future must be invalid. - let presentation_options = JwtPresentationOptions { issuance_date: Some(Timestamp::now_utc().checked_add(Duration::hours(1)).unwrap()), expiration_date: None, @@ -313,7 +305,6 @@ where assert!(matches!(validation_error, ValidationError::IssuanceDate)); // Set Validation options to allow presentation "issued" 2 hours in the future. - let mut validation_options = JwtPresentationValidationOptions::default(); validation_options = validation_options.latest_issuance_date(Timestamp::now_utc().checked_add(Duration::hours(2)).unwrap()); From 52824586ffba085696192fbb7e25bd244abb42ec Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Fri, 2 Jun 2023 01:17:36 +0200 Subject: [PATCH 23/31] code review fixes --- identity_credential/src/credential/mod.rs | 1 + .../src/presentation/jwt_presentation.rs | 9 +- .../presentation/jwt_presentation_builder.rs | 27 ----- .../credential_jwt_validator.rs | 30 +++++ .../src/validator/vp_jwt_validation/error.rs | 2 +- ...=> jwt_presentation_validation_options.rs} | 0 ...dator.rs => jwt_presentation_validator.rs} | 107 ++++++++++++------ .../src/validator/vp_jwt_validation/mod.rs | 8 +- .../src/storage/jwk_document_ext.rs | 4 +- .../storage/tests/presentation_validation.rs | 46 ++++++++ 10 files changed, 158 insertions(+), 76 deletions(-) rename identity_credential/src/validator/vp_jwt_validation/{presentation_jwt_validation_options.rs => jwt_presentation_validation_options.rs} (100%) rename identity_credential/src/validator/vp_jwt_validation/{presentation_jwt_validator.rs => jwt_presentation_validator.rs} (76%) diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 6c8f5c4fb2..07d7ca0729 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -40,4 +40,5 @@ pub use self::schema::Schema; pub use self::status::Status; pub use self::subject::Subject; +#[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::*; diff --git a/identity_credential/src/presentation/jwt_presentation.rs b/identity_credential/src/presentation/jwt_presentation.rs index f4046b5661..e790438ebb 100644 --- a/identity_credential/src/presentation/jwt_presentation.rs +++ b/identity_credential/src/presentation/jwt_presentation.rs @@ -110,9 +110,6 @@ impl JwtPresentation { if !self.types.iter().any(|type_| type_ == Self::base_type()) { return Err(Error::MissingBaseType); } - - //Todo: should check credentials structure? - Ok(()) } @@ -138,9 +135,9 @@ impl JwtPresentation { self.proof.as_ref() } - /// Returns a mutable reference to the `JwtPresentation` proof. - pub fn proof_mut(&mut self) -> Option<&mut Object> { - self.proof.as_mut() + /// Sets the value of the proof property. + pub fn set_proof(&mut self, proof: Option) { + self.proof = proof; } } diff --git a/identity_credential/src/presentation/jwt_presentation_builder.rs b/identity_credential/src/presentation/jwt_presentation_builder.rs index 7728eab340..4c8f41a739 100644 --- a/identity_credential/src/presentation/jwt_presentation_builder.rs +++ b/identity_credential/src/presentation/jwt_presentation_builder.rs @@ -125,16 +125,6 @@ mod tests { use identity_core::common::Object; use identity_core::common::Url; use identity_core::convert::FromJson; - use identity_core::crypto::KeyPair; - use identity_core::crypto::KeyType; - use identity_did::CoreDID; - use identity_did::DID; - use identity_document::document::CoreDocument; - use identity_document::document::DocumentBuilder; - use identity_verification::MethodBuilder; - use identity_verification::MethodData; - use identity_verification::MethodType; - use identity_verification::VerificationMethod; use crate::credential::Credential; use crate::credential::CredentialBuilder; @@ -161,23 +151,6 @@ mod tests { #[test] fn test_presentation_builder_valid() { - let keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap(); - let controller: CoreDID = "did:example:1234".parse().unwrap(); - - let method: VerificationMethod = MethodBuilder::default() - .id(controller.to_url().join("#key-1").unwrap()) - .controller(controller.clone()) - .type_(MethodType::ED25519_VERIFICATION_KEY_2018) - .data(MethodData::new_multibase(keypair.public())) - .build() - .unwrap(); - - let _document: CoreDocument = DocumentBuilder::default() - .id(controller) - .verification_method(method) - .build() - .unwrap(); - let credential: Credential = CredentialBuilder::default() .type_("ExampleCredential") .subject(subject()) diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs index 77ee421c58..d887955635 100644 --- a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs +++ b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs @@ -467,6 +467,36 @@ impl CredentialValidator { source: err.into(), }) } + + /// Utility for extracting the issuer field of a credential in JWT representation as DID. + /// + /// # Errors + /// + /// If the JWT decoding fails or the issuer field is not a valid DID. + pub fn extract_issuer_from_jwt(credential: &Jwt) -> std::result::Result + where + D: DID, + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + ::Err: std::error::Error + Send + Sync + 'static, + { + let validation_item = Decoder::new() + .decode_compact_serialization(credential.as_str().as_bytes(), None) + .map_err(ValidationError::JwsDecodingError)?; + + let claims: CredentialJwtClaims<'_, T> = + CredentialJwtClaims::from_json_slice(&validation_item.claims()).map_err(|err| { + ValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let credential = claims.try_into_credential().map_err(|err| { + ValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + D::from_str(credential.issuer.url().as_str()).map_err(|err| ValidationError::SignerUrl { + signer_ctx: SignerContext::Issuer, + source: err.into(), + }) + } } impl Default for CredentialValidator { diff --git a/identity_credential/src/validator/vp_jwt_validation/error.rs b/identity_credential/src/validator/vp_jwt_validation/error.rs index 92d7b5bad6..9c8fb5088c 100644 --- a/identity_credential/src/validator/vp_jwt_validation/error.rs +++ b/identity_credential/src/validator/vp_jwt_validation/error.rs @@ -19,7 +19,7 @@ pub struct CompoundJwtPresentationValidationError { } impl CompoundJwtPresentationValidationError { - pub(crate) fn one_prsentation_error(error: ValidationError) -> Self { + pub(crate) fn one_presentation_error(error: ValidationError) -> Self { Self { credential_errors: BTreeMap::new(), presentation_validation_errors: vec![error], diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validation_options.rs similarity index 100% rename from identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validation_options.rs rename to identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validation_options.rs diff --git a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs similarity index 76% rename from identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs rename to identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs index f9c0dae874..8d6bfa32ce 100644 --- a/identity_credential/src/validator/vp_jwt_validation/presentation_jwt_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs @@ -95,16 +95,6 @@ where T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, U: ToOwned + serde::Serialize + serde::de::DeserializeOwned, { - // Verify that holder document matches holder in presentation. - let holder_did: CoreDID = Self::extract_holder::(presentation) - .map_err(CompoundJwtPresentationValidationError::one_prsentation_error)?; - - if &holder_did != ::id(holder.as_ref()) { - return Err(CompoundJwtPresentationValidationError::one_prsentation_error( - ValidationError::DocumentMismatch(SignerContext::Holder), - )); - } - // Verify JWS. let decoded_jws: DecodedJws<'_> = holder .as_ref() @@ -115,22 +105,37 @@ where &options.presentation_verifier_options, ) .map_err(|err| { - CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationJwsError(err)) + CompoundJwtPresentationValidationError::one_presentation_error(ValidationError::PresentationJwsError(err)) })?; let claims: PresentationJwtClaims<'_, T> = PresentationJwtClaims::from_json_slice(&decoded_jws.claims).map_err(|err| { - CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + CompoundJwtPresentationValidationError::one_presentation_error(ValidationError::PresentationStructure( crate::Error::JwtClaimsSetDeserializationError(err.into()), )) })?; + // Verify that holder document matches holder in presentation. + let iss: Url = claims.iss.clone().into_owned(); + let holder_did: CoreDID = CoreDID::from_str(iss.as_str()).map_err(|err| { + CompoundJwtPresentationValidationError::one_presentation_error(ValidationError::SignerUrl { + signer_ctx: SignerContext::Holder, + source: err.into(), + }) + })?; + + if &holder_did != ::id(holder.as_ref()) { + return Err(CompoundJwtPresentationValidationError::one_presentation_error( + ValidationError::DocumentMismatch(SignerContext::Holder), + )); + } + // Check the expiration date. let expiration_date: Option = claims .exp .map(|exp| { Timestamp::from_unix(exp).map_err(|err| { - CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + CompoundJwtPresentationValidationError::one_presentation_error(ValidationError::PresentationStructure( crate::Error::JwtClaimsSetDeserializationError(err.into()), )) }) @@ -139,7 +144,7 @@ where (expiration_date.is_none() || expiration_date >= Some(options.earliest_expiry_date.unwrap_or_default())) .then_some(()) - .ok_or(CompoundJwtPresentationValidationError::one_prsentation_error( + .ok_or(CompoundJwtPresentationValidationError::one_presentation_error( ValidationError::ExpirationDate, ))?; @@ -148,7 +153,7 @@ where Some(iss) => { if iss.iat.is_some() || iss.nbf.is_some() { Some(iss.to_issuance_date().map_err(|err| { - CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure( + CompoundJwtPresentationValidationError::one_presentation_error(ValidationError::PresentationStructure( crate::Error::JwtClaimsSetDeserializationError(err.into()), )) })?) @@ -161,14 +166,14 @@ where (issuance_date.is_none() || issuance_date <= Some(options.latest_issuance_date.unwrap_or_default())) .then_some(()) - .ok_or(CompoundJwtPresentationValidationError::one_prsentation_error( + .ok_or(CompoundJwtPresentationValidationError::one_presentation_error( ValidationError::IssuanceDate, ))?; let aud: Option = claims.aud.clone(); let presentation: JwtPresentation = claims.try_into_presentation().map_err(|err| { - CompoundJwtPresentationValidationError::one_prsentation_error(ValidationError::PresentationStructure(err)) + CompoundJwtPresentationValidationError::one_presentation_error(ValidationError::PresentationStructure(err)) })?; // Validate credentials. @@ -191,25 +196,11 @@ where Ok(decoded_jwt_presentation) } - /// Extracts the holder from a JWT presentation.. - pub fn extract_holder(presentation: &Jwt) -> std::result::Result - where - T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, - ::Err: std::error::Error + Send + Sync + 'static, - { - let validation_item = Decoder::new() - .decode_compact_serialization(presentation.as_str().as_bytes(), None) - .map_err(ValidationError::JwsDecodingError)?; - - let claims: PresentationJwtClaims<'_, T> = PresentationJwtClaims::from_json_slice(&validation_item.claims()) - .map_err(|err| { - ValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) - })?; - let iss: Url = claims.iss.into_owned(); - D::from_str(iss.as_str()).map_err(|err| ValidationError::SignerUrl { - signer_ctx: SignerContext::Holder, - source: err.into(), - }) + /// Validates the semantic structure of the `JwtPresentation`. + pub fn check_structure(presentation: &JwtPresentation) -> Result<(), ValidationError> { + presentation + .check_structure() + .map_err(ValidationError::PresentationStructure) } fn validate_credentials( @@ -263,3 +254,47 @@ where } } } + +impl JwtPresentationValidator { + /// Attempt to extract the holder of the presentation and the issuers of the included + /// credentials. + /// + /// # Errors: + /// * If deserialization/decoding of the presentation or any of the credentials + /// fails. + /// * If the holder or any of the issuers can't be parsed as DIDs. + /// * If the presentation has inconsistent claims. + /// + /// Returned tuple: (presentation_holder, credentials_issuers). + pub fn extract_dids(presentation: &Jwt) -> std::result::Result<(H, Vec), ValidationError> + where + T: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + U: ToOwned + serde::Serialize + serde::de::DeserializeOwned, + ::Err: std::error::Error + Send + Sync + 'static, + ::Err: std::error::Error + Send + Sync + 'static, + { + let validation_item = Decoder::new() + .decode_compact_serialization(presentation.as_str().as_bytes(), None) + .map_err(ValidationError::JwsDecodingError)?; + + let claims: PresentationJwtClaims<'_, T> = PresentationJwtClaims::from_json_slice(&validation_item.claims()) + .map_err(|err| { + ValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let presentation = claims.try_into_presentation().map_err(|err| { + ValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) + })?; + + let holder: H = H::from_str(presentation.holder.as_str()).map_err(|err| ValidationError::SignerUrl { + signer_ctx: SignerContext::Holder, + source: err.into(), + })?; + + let mut issuers: Vec = vec![]; + for vc in presentation.verifiable_credential { + issuers.push(CredentialValidator::extract_issuer_from_jwt::(&vc)?) + } + Ok((holder, issuers)) + } +} diff --git a/identity_credential/src/validator/vp_jwt_validation/mod.rs b/identity_credential/src/validator/vp_jwt_validation/mod.rs index 31ad81e700..c5dccc7db2 100644 --- a/identity_credential/src/validator/vp_jwt_validation/mod.rs +++ b/identity_credential/src/validator/vp_jwt_validation/mod.rs @@ -3,10 +3,10 @@ mod decoded_jwt_presentation; mod error; -mod presentation_jwt_validation_options; -mod presentation_jwt_validator; +mod jwt_presentation_validation_options; +mod jwt_presentation_validator; pub use decoded_jwt_presentation::*; pub use error::*; -pub use presentation_jwt_validation_options::*; -pub use presentation_jwt_validator::*; +pub use jwt_presentation_validation_options::*; +pub use jwt_presentation_validator::*; diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index cb8e607826..ade03d8b63 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -108,8 +108,8 @@ pub trait JwkDocumentExt: private::Sealed { presentation: &JwtPresentation, storage: &Storage, fragment: &str, - options: &JwsSignatureOptions, - jwt_options: &JwtPresentationOptions, + signature_options: &JwsSignatureOptions, + presentation_options: &JwtPresentationOptions, ) -> StorageResult where K: JwkStorage, diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index bbc4dafcf5..8bfd3f33ed 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -15,6 +15,7 @@ use identity_credential::validator::DecodedJwtPresentation; use identity_credential::validator::FailFast; use identity_credential::validator::JwtPresentationValidationOptions; use identity_credential::validator::JwtPresentationValidator; +use identity_did::CoreDID; use identity_did::DID; use identity_document::document::CoreDocument; use identity_verification::jws::JwsAlgorithm; @@ -91,6 +92,51 @@ where ); } +#[tokio::test] +async fn test_extract_dids() { + test_extract_dids_impl(setup_coredocument(None, None).await).await; + test_extract_dids_impl(setup_iotadocument(None, None).await).await; +} +async fn test_extract_dids_impl(setup: Setup) +where + T: JwkDocumentExt + AsRef, +{ + let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); + let jws = sign_credential(&setup, &credential.credential).await; + + let presentation: JwtPresentation = + JwtPresentationBuilder::new(setup.subject_doc.as_ref().id().to_url().into(), Object::new()) + .credential(jws) + .build() + .unwrap(); + + let presentation_options = JwtPresentationOptions { + expiration_date: Some(Timestamp::now_utc().checked_add(Duration::hours(10)).unwrap()), + issuance_date: Some(Timestamp::now_utc().checked_sub(Duration::hours(10)).unwrap()), + audience: Some(Url::parse("did:test:123").unwrap()), + }; + + let presentation_jwt = setup + .subject_doc + .sign_presentation( + &presentation, + &setup.subject_storage, + &setup.subject_method_fragment, + &JwsSignatureOptions::default(), + &presentation_options, + ) + .await + .unwrap(); + + let (holder, issuers) = + JwtPresentationValidator::extract_dids::(&presentation_jwt).unwrap(); + assert_eq!(holder.to_url(), setup.subject_doc.as_ref().id().to_url()); + assert_eq!( + issuers.into_iter().next().unwrap().to_url(), + setup.issuer_doc.as_ref().id().to_url() + ); +} + // > Create a VP signed by a verification method with `subject_method_fragment`. // > Replace the verification method but keep the same fragment. // > Validation fails due to invalid signature since key material changed. From 3990d84a2c7cd839b8fac5c9dd253c3ea4f49c4e Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Fri, 2 Jun 2023 01:45:36 +0200 Subject: [PATCH 24/31] remove feature validator --- identity_credential/src/credential/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 07d7ca0729..6c8f5c4fb2 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -40,5 +40,4 @@ pub use self::schema::Schema; pub use self::status::Status; pub use self::subject::Subject; -#[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::*; From 4218b728b43de7a0e0c621ee63eae4fa3ba8e220 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Fri, 2 Jun 2023 13:07:33 +0200 Subject: [PATCH 25/31] code review suggestions --- examples/utils/utils.rs | 2 +- identity_core/src/crypto/key/ed25519.rs | 2 +- identity_core/src/crypto/proof/jcs_ed25519.rs | 4 ++-- .../src/credential/jwt_serialization.rs | 2 +- identity_credential/src/credential/mod.rs | 5 ++++- .../src/presentation/jwt_serialization.rs | 3 ++- identity_credential/src/presentation/mod.rs | 3 ++- .../vc_jwt_validation/credential_jwt_validator.rs | 6 +----- .../jwt_presentation_validator.rs | 14 ++++---------- identity_storage/src/key_storage/memstore.rs | 2 +- .../src/storage/tests/credential_validation.rs | 2 +- .../src/storage/tests/presentation_validation.rs | 9 ++++++++- 12 files changed, 28 insertions(+), 26 deletions(-) diff --git a/examples/utils/utils.rs b/examples/utils/utils.rs index dd506fa57a..3ac61a0a03 100644 --- a/examples/utils/utils.rs +++ b/examples/utils/utils.rs @@ -148,7 +148,7 @@ async fn get_address_balance(client: &Client, address: &str) -> anyhow::Result, /// Represents the issuer. - iss: Cow<'credential, Issuer>, + pub(crate) iss: Cow<'credential, Issuer>, /// Represents the issuanceDate encoded as a UNIX timestamp. #[serde(flatten)] diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 6c8f5c4fb2..d0c6f76a0c 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -40,4 +40,7 @@ pub use self::schema::Schema; pub use self::status::Status; pub use self::subject::Subject; -pub(crate) use self::jwt_serialization::*; +#[cfg(feature = "validator")] +pub(crate) use self::jwt_serialization::CredentialJwtClaims; +#[cfg(feature = "presentation")] +pub(crate) use self::jwt_serialization::IssuanceDateClaims; diff --git a/identity_credential/src/presentation/jwt_serialization.rs b/identity_credential/src/presentation/jwt_serialization.rs index 1e46ae16ea..49179285c0 100644 --- a/identity_credential/src/presentation/jwt_serialization.rs +++ b/identity_credential/src/presentation/jwt_serialization.rs @@ -17,6 +17,7 @@ use crate::credential::Jwt; use crate::credential::Policy; use crate::credential::RefreshService; use crate::presentation::JwtPresentation; +#[cfg(feature = "validator")] use crate::Error; use crate::Result; @@ -103,7 +104,7 @@ where types: Cow<'presentation, OneOrMany>, /// Credential(s) expressing the claims of the `JwtPresentation`. #[serde(default = "Default::default", rename = "verifiableCredential")] - verifiable_credential: Cow<'presentation, OneOrMany>, + pub(crate) verifiable_credential: Cow<'presentation, OneOrMany>, /// Service(s) used to refresh an expired [`Credential`] in the `JwtPresentation`. #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] refresh_service: Cow<'presentation, OneOrMany>, diff --git a/identity_credential/src/presentation/mod.rs b/identity_credential/src/presentation/mod.rs index 436c06b263..b0b6c5118e 100644 --- a/identity_credential/src/presentation/mod.rs +++ b/identity_credential/src/presentation/mod.rs @@ -18,4 +18,5 @@ pub use self::jwt_presentation_builder::JwtPresentationBuilder; pub use self::jwt_presentation_options::JwtPresentationOptions; pub use self::presentation::Presentation; -pub(crate) use self::jwt_serialization::*; +#[cfg(feature = "validator")] +pub(crate) use self::jwt_serialization::PresentationJwtClaims; diff --git a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs index d887955635..c19a51149e 100644 --- a/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs +++ b/identity_credential/src/validator/vc_jwt_validation/credential_jwt_validator.rs @@ -488,11 +488,7 @@ impl CredentialValidator { ValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) })?; - let credential = claims.try_into_credential().map_err(|err| { - ValidationError::CredentialStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) - })?; - - D::from_str(credential.issuer.url().as_str()).map_err(|err| ValidationError::SignerUrl { + D::from_str(claims.iss.url().as_str()).map_err(|err| ValidationError::SignerUrl { signer_ctx: SignerContext::Issuer, source: err.into(), }) diff --git a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs index 8d6bfa32ce..595e6a8f95 100644 --- a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs @@ -116,8 +116,7 @@ where })?; // Verify that holder document matches holder in presentation. - let iss: Url = claims.iss.clone().into_owned(); - let holder_did: CoreDID = CoreDID::from_str(iss.as_str()).map_err(|err| { + let holder_did: CoreDID = CoreDID::from_str(claims.iss.as_str()).map_err(|err| { CompoundJwtPresentationValidationError::one_presentation_error(ValidationError::SignerUrl { signer_ctx: SignerContext::Holder, source: err.into(), @@ -263,7 +262,6 @@ impl JwtPresentationValidator { /// * If deserialization/decoding of the presentation or any of the credentials /// fails. /// * If the holder or any of the issuers can't be parsed as DIDs. - /// * If the presentation has inconsistent claims. /// /// Returned tuple: (presentation_holder, credentials_issuers). pub fn extract_dids(presentation: &Jwt) -> std::result::Result<(H, Vec), ValidationError> @@ -282,18 +280,14 @@ impl JwtPresentationValidator { ValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) })?; - let presentation = claims.try_into_presentation().map_err(|err| { - ValidationError::PresentationStructure(crate::Error::JwtClaimsSetDeserializationError(err.into())) - })?; - - let holder: H = H::from_str(presentation.holder.as_str()).map_err(|err| ValidationError::SignerUrl { + let holder: H = H::from_str(claims.iss.as_str()).map_err(|err| ValidationError::SignerUrl { signer_ctx: SignerContext::Holder, source: err.into(), })?; let mut issuers: Vec = vec![]; - for vc in presentation.verifiable_credential { - issuers.push(CredentialValidator::extract_issuer_from_jwt::(&vc)?) + for vc in claims.vp.verifiable_credential.iter() { + issuers.push(CredentialValidator::extract_issuer_from_jwt::(vc)?) } Ok((holder, issuers)) } diff --git a/identity_storage/src/key_storage/memstore.rs b/identity_storage/src/key_storage/memstore.rs index cb4d702741..fd50454efb 100644 --- a/identity_storage/src/key_storage/memstore.rs +++ b/identity_storage/src/key_storage/memstore.rs @@ -425,7 +425,7 @@ mod tests { ec_params.d = Some("".to_owned()); let jwk_ec = Jwk::from_params(ec_params); - let err: _ = store.insert(jwk_ec).await.unwrap_err(); + let err = store.insert(jwk_ec).await.unwrap_err(); assert!(matches!(err.kind(), KeyStorageErrorKind::UnsupportedKeyType)); } diff --git a/identity_storage/src/storage/tests/credential_validation.rs b/identity_storage/src/storage/tests/credential_validation.rs index 3b048ce98e..3bb2fad2b5 100644 --- a/identity_storage/src/storage/tests/credential_validation.rs +++ b/identity_storage/src/storage/tests/credential_validation.rs @@ -299,7 +299,7 @@ where .await .unwrap(); - let err: _ = CredentialValidator::new() + let err = CredentialValidator::new() .verify_signature::<_, Object>(&jwt, &[&issuer_doc], &JwsVerificationOptions::default()) .unwrap_err(); diff --git a/identity_storage/src/storage/tests/presentation_validation.rs b/identity_storage/src/storage/tests/presentation_validation.rs index 8bfd3f33ed..3745bc459f 100644 --- a/identity_storage/src/storage/tests/presentation_validation.rs +++ b/identity_storage/src/storage/tests/presentation_validation.rs @@ -5,6 +5,7 @@ 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_credential::credential::Credential; use identity_credential::credential::Jwt; use identity_credential::presentation::JwtPresentation; @@ -102,11 +103,16 @@ where T: JwkDocumentExt + AsRef, { let credential: CredentialSetup = generate_credential(&setup.issuer_doc, &[&setup.subject_doc], None, None); + + let issuer_2 = CoreDocument::from_json(r#"{"id": "did:test:123"}"#).unwrap(); + let credential_2: CredentialSetup = generate_credential(&issuer_2, &[&setup.subject_doc], None, None); let jws = sign_credential(&setup, &credential.credential).await; + let jws_2 = sign_credential(&setup, &credential_2.credential).await; let presentation: JwtPresentation = JwtPresentationBuilder::new(setup.subject_doc.as_ref().id().to_url().into(), Object::new()) .credential(jws) + .credential(jws_2) .build() .unwrap(); @@ -132,9 +138,10 @@ where JwtPresentationValidator::extract_dids::(&presentation_jwt).unwrap(); assert_eq!(holder.to_url(), setup.subject_doc.as_ref().id().to_url()); assert_eq!( - issuers.into_iter().next().unwrap().to_url(), + issuers.get(0).unwrap().to_url(), setup.issuer_doc.as_ref().id().to_url() ); + assert_eq!(issuers.get(1).unwrap().to_url(), issuer_2.as_ref().id().to_url()); } // > Create a VP signed by a verification method with `subject_method_fragment`. From da3029e0cd9dcdbfc6ddbb3e55bba18dfbd93167 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Thu, 1 Jun 2023 19:10:13 +0200 Subject: [PATCH 26/31] jwtPresentation wasm --- bindings/wasm/docs/api-reference.md | 221 ++++++++++++++++-- .../jwt_presentation/jwt_presentation.rs | 147 ++++++++++++ .../jwt_presentation_builder.rs | 108 +++++++++ .../src/credential/jwt_presentation/mod.rs | 7 + .../jwt_presentation_validation/mod.rs | 4 + .../jwt_presentation_validation/options.rs | 71 ++++++ bindings/wasm/src/credential/mod.rs | 4 +- bindings/wasm/src/credential/types.rs | 3 + 8 files changed, 546 insertions(+), 19 deletions(-) create mode 100644 bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs create mode 100644 bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs create mode 100644 bindings/wasm/src/credential/jwt_presentation/mod.rs create mode 100644 bindings/wasm/src/credential/jwt_presentation_validation/mod.rs create mode 100644 bindings/wasm/src/credential/jwt_presentation_validation/options.rs diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 0cb70751b9..64434c9a7d 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -79,6 +79,11 @@ and resolution of DID documents in Alias Outputs.

JwtCredentialValidator

A type for decoding and validating Credentials.

+
JwtPresentation
+
+
JwtPresentationValidationOptions
+

Options to declare validation criteria when validating presentation.

+
KeyPair
LinkedDomainService
@@ -151,12 +156,6 @@ See IVerifierOptions.

## Members
-
KeyType
-
-
StateMetadataEncoding
-
-
MethodRelationship
-
StatusCheck

Controls validation behaviour when checking whether or not a credential has been revoked by its credentialStatus.

@@ -199,6 +198,12 @@ This variant is the default used if no other variant is specified when construct
FirstError

Return after the first error occurs.

+
KeyType
+
+
StateMetadataEncoding
+
+
MethodRelationship
+
## Functions @@ -3815,6 +3820,186 @@ Fails if the issuer field is not a valid DID. | --- | --- | | credential | [Credential](#Credential) | + + +## JwtPresentation +**Kind**: global class + +* [JwtPresentation](#JwtPresentation) + * [new JwtPresentation(values)](#new_JwtPresentation_new) + * _instance_ + * [.context()](#JwtPresentation+context) ⇒ Array.<(string\|Record.<string, any>)> + * [.id()](#JwtPresentation+id) ⇒ string \| undefined + * [.type()](#JwtPresentation+type) ⇒ Array.<string> + * [.verifiableCredential()](#JwtPresentation+verifiableCredential) ⇒ [Array.<Jwt>](#Jwt) + * [.holder()](#JwtPresentation+holder) ⇒ string + * [.refreshService()](#JwtPresentation+refreshService) ⇒ Array.<RefreshService> + * [.termsOfUse()](#JwtPresentation+termsOfUse) ⇒ Array.<Policy> + * [.proof()](#JwtPresentation+proof) ⇒ Map.<string, any> \| undefined + * [.properties()](#JwtPresentation+properties) ⇒ Map.<string, any> + * [.toJSON()](#JwtPresentation+toJSON) ⇒ any + * [.clone()](#JwtPresentation+clone) ⇒ [JwtPresentation](#JwtPresentation) + * _static_ + * [.BaseContext()](#JwtPresentation.BaseContext) ⇒ string + * [.BaseType()](#JwtPresentation.BaseType) ⇒ string + * [.fromJSON(json)](#JwtPresentation.fromJSON) ⇒ [JwtPresentation](#JwtPresentation) + + + +### new JwtPresentation(values) +Constructs a new presentation. + + +| Param | Type | +| --- | --- | +| values | IJwtPresentation | + + + +### jwtPresentation.context() ⇒ Array.<(string\|Record.<string, any>)> +Returns a copy of the JSON-LD context(s) applicable to the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.id() ⇒ string \| undefined +Returns a copy of the unique `URI` identifying the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.type() ⇒ Array.<string> +Returns a copy of the URIs defining the type of the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.verifiableCredential() ⇒ [Array.<Jwt>](#Jwt) +Returns a copy of the [Credential](#Credential)(s) expressing the claims of the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.holder() ⇒ string +Returns a copy of the URI of the entity that generated the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.refreshService() ⇒ Array.<RefreshService> +Returns a copy of the service(s) used to refresh an expired [Credential](#Credential) in the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.termsOfUse() ⇒ Array.<Policy> +Returns a copy of the terms-of-use specified by the presentation holder + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.proof() ⇒ Map.<string, any> \| undefined +Returns a copy of the proof property. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.properties() ⇒ Map.<string, any> +Returns a copy of the miscellaneous properties on the presentation. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### jwtPresentation.clone() ⇒ [JwtPresentation](#JwtPresentation) +Deep clones the object. + +**Kind**: instance method of [JwtPresentation](#JwtPresentation) + + +### JwtPresentation.BaseContext() ⇒ string +Returns the base JSON-LD context. + +**Kind**: static method of [JwtPresentation](#JwtPresentation) + + +### JwtPresentation.BaseType() ⇒ string +Returns the base type. + +**Kind**: static method of [JwtPresentation](#JwtPresentation) + + +### JwtPresentation.fromJSON(json) ⇒ [JwtPresentation](#JwtPresentation) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwtPresentation](#JwtPresentation) + +| Param | Type | +| --- | --- | +| json | any | + + + +## JwtPresentationValidationOptions +Options to declare validation criteria when validating presentation. + +**Kind**: global class + +* [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + * [new JwtPresentationValidationOptions(options)](#new_JwtPresentationValidationOptions_new) + * _instance_ + * [.toJSON()](#JwtPresentationValidationOptions+toJSON) ⇒ any + * [.clone()](#JwtPresentationValidationOptions+clone) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + * _static_ + * [.default()](#JwtPresentationValidationOptions.default) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + * [.fromJSON(json)](#JwtPresentationValidationOptions.fromJSON) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + + +### new JwtPresentationValidationOptions(options) +Creates a new `JwtPresentationValidationOptions` from the given fields. + +Throws an error if any of the options are invalid. + + +| Param | Type | +| --- | --- | +| options | IJwtPresentationValidationOptions | + + + +### jwtPresentationValidationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + +### jwtPresentationValidationOptions.clone() ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) +Deep clones the object. + +**Kind**: instance method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + +### JwtPresentationValidationOptions.default() ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) +Creates a new `JwtPresentationValidationOptions` with defaults. + +**Kind**: static method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + + +### JwtPresentationValidationOptions.fromJSON(json) ⇒ [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) + +| Param | Type | +| --- | --- | +| json | any | + ## KeyPair @@ -5431,18 +5616,6 @@ This is possible because Ed25519 is birationally equivalent to Curve25519 used b | --- | --- | | publicKey | Uint8Array | - - -## KeyType -**Kind**: global variable - - -## StateMetadataEncoding -**Kind**: global variable - - -## MethodRelationship -**Kind**: global variable ## StatusCheck @@ -5520,6 +5693,18 @@ Return all errors that occur during validation. ## FirstError Return after the first error occurs. +**Kind**: global variable + + +## KeyType +**Kind**: global variable + + +## StateMetadataEncoding +**Kind**: global variable + + +## MethodRelationship **Kind**: global variable diff --git a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs new file mode 100644 index 0000000000..5804b1fff1 --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs @@ -0,0 +1,147 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Context; +use identity_iota::core::Object; +use identity_iota::credential::JwtPresentation; +use identity_iota::credential::JwtPresentationBuilder; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; + +use crate::common::ArrayString; +use crate::common::MapStringAny; +use crate::credential::jwt_presentation::jwt_presentation_builder::IJwtPresentation; +use crate::credential::ArrayContext; +use crate::credential::ArrayJwt; +use crate::credential::ArrayPolicy; +use crate::credential::ArrayRefreshService; +use crate::credential::WasmJwt; +use crate::error::Result; +use crate::error::WasmResult; + +#[wasm_bindgen(js_name = JwtPresentation, inspectable)] +pub struct WasmJwtPresentation(pub(crate) JwtPresentation); + +#[wasm_bindgen(js_class = JwtPresentation)] +impl WasmJwtPresentation { + /// Returns the base JSON-LD context. + #[wasm_bindgen(js_name = "BaseContext")] + pub fn base_context() -> Result { + match JwtPresentation::::base_context() { + Context::Url(url) => Ok(url.to_string()), + Context::Obj(_) => Err(JsError::new("JwtPresentation.BaseContext should be a single URL").into()), + } + } + + /// Returns the base type. + #[wasm_bindgen(js_name = "BaseType")] + pub fn base_type() -> String { + JwtPresentation::::base_type().to_owned() + } + + /// Constructs a new presentation. + #[wasm_bindgen(constructor)] + pub fn new(values: IJwtPresentation) -> Result { + let builder: JwtPresentationBuilder = JwtPresentationBuilder::try_from(values)?; + builder.build().map(Self).wasm_result() + } + + /// Returns a copy of the JSON-LD context(s) applicable to the presentation. + #[wasm_bindgen] + pub fn context(&self) -> Result { + self + .0 + .context + .iter() + .map(JsValue::from_serde) + .collect::>() + .wasm_result() + .map(|value| value.unchecked_into::()) + } + + /// Returns a copy of the unique `URI` identifying the presentation. + #[wasm_bindgen] + pub fn id(&self) -> Option { + self.0.id.as_ref().map(|url| url.to_string()) + } + + /// Returns a copy of the URIs defining the type of the presentation. + #[wasm_bindgen(js_name = "type")] + pub fn types(&self) -> ArrayString { + self + .0 + .types + .iter() + .map(|s| s.as_str()) + .map(JsValue::from_str) + .collect::() + .unchecked_into::() + } + + /// Returns a copy of the {@link Credential}(s) expressing the claims of the presentation. + #[wasm_bindgen(js_name = verifiableCredential)] + pub fn verifiable_credential(&self) -> ArrayJwt { + self + .0 + .verifiable_credential + .iter() + .cloned() + .map(WasmJwt::new) + .map(JsValue::from) + .collect::() + .unchecked_into::() + } + + /// Returns a copy of the URI of the entity that generated the presentation. + #[wasm_bindgen] + pub fn holder(&self) -> String { + self.0.holder.as_ref().to_string() + } + + /// Returns a copy of the service(s) used to refresh an expired {@link Credential} in the presentation. + #[wasm_bindgen(js_name = "refreshService")] + pub fn refresh_service(&self) -> Result { + self + .0 + .refresh_service + .iter() + .map(JsValue::from_serde) + .collect::>() + .wasm_result() + .map(|value| value.unchecked_into::()) + } + + /// Returns a copy of the terms-of-use specified by the presentation holder + #[wasm_bindgen(js_name = "termsOfUse")] + pub fn terms_of_use(&self) -> Result { + self + .0 + .terms_of_use + .iter() + .map(JsValue::from_serde) + .collect::>() + .wasm_result() + .map(|value| value.unchecked_into::()) + } + + /// Returns a copy of the proof property. + #[wasm_bindgen] + pub fn proof(&self) -> Result> { + self.0.proof.clone().map(MapStringAny::try_from).transpose() + } + + /// Returns a copy of the miscellaneous properties on the presentation. + #[wasm_bindgen] + pub fn properties(&self) -> Result { + MapStringAny::try_from(&self.0.properties) + } +} + +impl_wasm_json!(WasmJwtPresentation, JwtPresentation); +impl_wasm_clone!(WasmJwtPresentation, JwtPresentation); + +impl From for WasmJwtPresentation { + fn from(presentation: JwtPresentation) -> WasmJwtPresentation { + Self(presentation) + } +} diff --git a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs new file mode 100644 index 0000000000..8abccef68c --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs @@ -0,0 +1,108 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::core::Context; +use identity_iota::core::Object; +use identity_iota::core::OneOrMany; +use identity_iota::core::Url; +use identity_iota::credential::Jwt; +use identity_iota::credential::JwtPresentationBuilder; +use identity_iota::credential::Policy; +use identity_iota::credential::RefreshService; +use proc_typescript::typescript; +use wasm_bindgen::prelude::*; + +use crate::error::WasmResult; + +impl TryFrom for JwtPresentationBuilder { + type Error = JsValue; + + fn try_from(values: IJwtPresentation) -> std::result::Result { + let IJwtPresentationHelper { + context, + id, + r#type, + verifiable_credential, + holder, + refresh_service, + terms_of_use, + properties, + } = values.into_serde::().wasm_result()?; + + let mut builder: JwtPresentationBuilder = + JwtPresentationBuilder::new(Url::parse(holder).wasm_result()?, properties); + + if let Some(context) = context { + for value in context.into_vec() { + builder = builder.context(value); + } + } + if let Some(id) = id { + builder = builder.id(Url::parse(id).wasm_result()?); + } + if let Some(types) = r#type { + for value in types.iter() { + builder = builder.type_(value); + } + } + if let Some(credentials) = verifiable_credential { + for credential in credentials.into_vec() { + builder = builder.credential(credential); + } + } + if let Some(refresh_service) = refresh_service { + for service in refresh_service.into_vec() { + builder = builder.refresh_service(service); + } + } + if let Some(terms_of_use) = terms_of_use { + for policy in terms_of_use.into_vec() { + builder = builder.terms_of_use(policy); + } + } + + Ok(builder) + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwtPresentation")] + pub type IJwtPresentation; +} + +/// Fields for constructing a new {@link Presentation}. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[typescript(name = "IJwtPresentation", readonly, optional)] +struct IJwtPresentationHelper { + /// The JSON-LD context(s) applicable to the presentation. + #[typescript(type = "string | Record | Array>")] + context: Option>, + /// A unique URI that may be used to identify the presentation. + #[typescript(type = "string")] + id: Option, + /// One or more URIs defining the type of the presentation. Contains the base context by default. + #[typescript(name = "type", type = "string | Array")] + r#type: Option>, + /// Credential(s) expressing the claims of the presentation. + #[typescript( + optional = false, + name = "verifiableCredential", + type = "Credential | Array" + )] + verifiable_credential: Option>, + /// The entity that generated the presentation. + #[typescript(optional = false, type = "string | CoreDID | IotaDID ")] + holder: String, + /// Service(s) used to refresh an expired {@link Credential} in the presentation. + #[typescript(name = "refreshService", type = "RefreshService | Array")] + refresh_service: Option>, + /// Terms-of-use specified by the presentation holder. + #[typescript(name = "termsOfUse", type = "Policy | Array")] + terms_of_use: Option>, + /// Miscellaneous properties. + #[serde(flatten)] + #[typescript(optional = false, name = "[properties: string]", type = "unknown")] + properties: Object, +} diff --git a/bindings/wasm/src/credential/jwt_presentation/mod.rs b/bindings/wasm/src/credential/jwt_presentation/mod.rs new file mode 100644 index 0000000000..3f4bb3b90c --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod jwt_presentation; +mod jwt_presentation_builder; + +pub use self::jwt_presentation::*; diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs new file mode 100644 index 0000000000..bd493cc4ab --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +mod options; diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/options.rs b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs new file mode 100644 index 0000000000..e006dc1f8e --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs @@ -0,0 +1,71 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Result; +use crate::error::WasmResult; +use identity_iota::credential::JwtPresentationValidationOptions; +use wasm_bindgen::prelude::*; + +/// Options to declare validation criteria when validating presentation. +#[wasm_bindgen(js_name = JwtPresentationValidationOptions)] +pub struct WasmJwtPresentationValidationOptions(pub(crate) JwtPresentationValidationOptions); + +#[wasm_bindgen(js_class = JwtPresentationValidationOptions)] +impl WasmJwtPresentationValidationOptions { + /// Creates a new `JwtPresentationValidationOptions` from the given fields. + /// + /// Throws an error if any of the options are invalid. + #[wasm_bindgen(constructor)] + pub fn new(options: IJwtPresentationValidationOptions) -> Result { + let options: JwtPresentationValidationOptions = options.into_serde().wasm_result()?; + Ok(WasmJwtPresentationValidationOptions::from(options)) + } + + /// Creates a new `JwtPresentationValidationOptions` with defaults. + #[allow(clippy::should_implement_trait)] + #[wasm_bindgen] + pub fn default() -> WasmJwtPresentationValidationOptions { + WasmJwtPresentationValidationOptions::from(JwtPresentationValidationOptions::default()) + } +} + +impl_wasm_json!(WasmJwtPresentationValidationOptions, JwtPresentationValidationOptions); +impl_wasm_clone!(WasmJwtPresentationValidationOptions, JwtPresentationValidationOptions); + +impl From for WasmJwtPresentationValidationOptions { + fn from(options: JwtPresentationValidationOptions) -> Self { + Self(options) + } +} + +impl From for JwtPresentationValidationOptions { + fn from(options: WasmJwtPresentationValidationOptions) -> Self { + options.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwtPresentationValidationOptions")] + pub type IJwtPresentationValidationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#" +/** Holds options to create a new `JwtPresentationValidationOptions`. */ +interface IJwtPresentationValidationOptions { + /** Declare that the credentials of the presentation must all be validated according to these `CredentialValidationOptions`. */ + readonly sharedValidationOptions?: CredentialValidationOptions; + + /** Options which affect the verification of the signature on the presentation. */ + readonly presentationVerifierOptions?: VerifierOptions; + + /** Declare how the presentation's credential subjects must relate to the holder. + * + * Default: SubjectHolderRelationship.AlwaysSubject + */ + readonly subjectHolderRelationship?: SubjectHolderRelationship; + + /** Options which affect the verification of the signature on the credential. */ + readonly verifierOptions?: VerifierOptions; +}"#; diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index c5e562d143..166e31485d 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2022 IOTA Stiftung +// Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 #![allow(clippy::module_inception)] @@ -28,6 +28,8 @@ mod domain_linkage_validator; mod jws; mod jwt; mod jwt_credential_validation; +mod jwt_presentation; +mod jwt_presentation_validation; mod linked_domain_service; mod presentation; mod presentation_builder; diff --git a/bindings/wasm/src/credential/types.rs b/bindings/wasm/src/credential/types.rs index 54c94c86b9..b47a32b6c3 100644 --- a/bindings/wasm/src/credential/types.rs +++ b/bindings/wasm/src/credential/types.rs @@ -31,6 +31,9 @@ extern "C" { #[wasm_bindgen(typescript_type = "Array")] pub type ArrayCredential; + + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayJwt; } #[wasm_bindgen(typescript_custom_section)] From 62b67238676672a064b3f5cadd851bbcca281295 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 6 Jun 2023 17:25:13 +0200 Subject: [PATCH 27/31] working bindings --- bindings/wasm/docs/api-reference.md | 283 +++++++++++++++--- .../decoded_jwt_credential.rs | 6 + .../jwt_credential_validation/options.rs | 3 +- .../jwt_presentation_builder.rs | 18 +- .../decoded_jwt_presentation.rs | 78 +++++ .../jwt_presentation_validator.rs | 95 ++++++ .../jwt_presentation_validation/mod.rs | 6 + .../jwt_presentation_validation/options.rs | 8 + bindings/wasm/src/credential/mod.rs | 2 + bindings/wasm/src/credential/types.rs | 25 ++ bindings/wasm/src/did/wasm_core_document.rs | 46 ++- bindings/wasm/src/error.rs | 10 + bindings/wasm/src/iota/iota_document.rs | 44 ++- .../src/storage/jwt_presentation_options.rs | 80 +++++ bindings/wasm/src/storage/mod.rs | 2 + bindings/wasm/tests/storage.ts | 214 ++++++++----- .../presentation/jwt_presentation_options.rs | 3 +- .../jwt_presentation_validator.rs | 14 +- .../src/storage/jwk_document_ext.rs | 4 +- 19 files changed, 805 insertions(+), 136 deletions(-) create mode 100644 bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs create mode 100644 bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs create mode 100644 bindings/wasm/src/storage/jwt_presentation_options.rs diff --git a/bindings/wasm/docs/api-reference.md b/bindings/wasm/docs/api-reference.md index 64434c9a7d..631b6a8194 100644 --- a/bindings/wasm/docs/api-reference.md +++ b/bindings/wasm/docs/api-reference.md @@ -26,6 +26,11 @@

Note that having an instance of this type only means the JWS it was constructed from was verified. It does not imply anything about a potentially present proof property on the credential itself.

+
DecodedJwtPresentation
+

A cryptographically verified and decoded presentation.

+

Note that having an instance of this type only means the JWS it was constructed from was verified. +It does not imply anything about a potentially present proof property on the presentation itself.

+
DomainLinkageConfiguration

DID Configuration Resource which contains Domain Linkage Credentials. It can be placed in an origin's .well-known directory to prove linkage between the origin and a DID. @@ -81,9 +86,13 @@ and resolution of DID documents in Alias Outputs.

JwtPresentation
+
JwtPresentationOptions
+
JwtPresentationValidationOptions

Options to declare validation criteria when validating presentation.

+
JwtPresentationValidator
+
KeyPair
LinkedDomainService
@@ -156,6 +165,8 @@ See IVerifierOptions.

## Members
+
StateMetadataEncoding
+
StatusCheck

Controls validation behaviour when checking whether or not a credential has been revoked by its credentialStatus.

@@ -200,8 +211,6 @@ This variant is the default used if no other variant is specified when construct
KeyType
-
StateMetadataEncoding
-
MethodRelationship
@@ -209,15 +218,6 @@ This variant is the default used if no other variant is specified when construct ## Functions
-
start()
-

Initializes the console error panic hook for better error messages

-
-
encodeB64(data) ⇒ string
-

Encode the given bytes in url-safe base64.

-
-
decodeB64(data) ⇒ Uint8Array
-

Decode the given url-safe base64-encoded slice into its raw bytes.

-
verifyEdDSA(alg, signingInput, decodedSignature, publicKey)

Verify a JWS signature secured with the JwsAlgorithm::EdDSA algorithm. Only the EdCurve::Ed25519 variant is supported for now.

@@ -227,6 +227,15 @@ the IOTA Identity Framework.

This function does not check whether alg = EdDSA in the protected header. Callers are expected to assert this prior to calling the function.

+
encodeB64(data) ⇒ string
+

Encode the given bytes in url-safe base64.

+
+
decodeB64(data) ⇒ Uint8Array
+

Decode the given url-safe base64-encoded slice into its raw bytes.

+
+
start()
+

Initializes the console error panic hook for better error messages

+
@@ -460,6 +469,7 @@ A method-agnostic DID Document. * [.purgeMethod(storage, id)](#CoreDocument+purgeMethod) ⇒ Promise.<void> * [.createJws(storage, fragment, payload, options)](#CoreDocument+createJws) ⇒ [Promise.<Jws>](#Jws) * [.createCredentialJwt(storage, fragment, credential, options)](#CoreDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#CoreDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) * _static_ * [.fromJSON(json)](#CoreDocument.fromJSON) ⇒ [CoreDocument](#CoreDocument) @@ -893,6 +903,19 @@ produced by the corresponding private key backed by the `storage` in accordance | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | + + +### coreDocument.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options) ⇒ [Promise.<Jwt>](#Jwt) +**Kind**: instance method of [CoreDocument](#CoreDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| fragment | string | +| presentation | [JwtPresentation](#JwtPresentation) | +| signature_options | [JwsSignatureOptions](#JwsSignatureOptions) | +| presentation_options | [JwtPresentationOptions](#JwtPresentationOptions) | + ### CoreDocument.fromJSON(json) ⇒ [CoreDocument](#CoreDocument) @@ -1525,6 +1548,61 @@ Consumes the object and returns the decoded credential. This destroys the `DecodedCredential` object. **Kind**: instance method of [DecodedJwtCredential](#DecodedJwtCredential) + + +## DecodedJwtPresentation +A cryptographically verified and decoded presentation. + +Note that having an instance of this type only means the JWS it was constructed from was verified. +It does not imply anything about a potentially present proof property on the presentation itself. + +**Kind**: global class + +* [DecodedJwtPresentation](#DecodedJwtPresentation) + * [.presentation()](#DecodedJwtPresentation+presentation) ⇒ [JwtPresentation](#JwtPresentation) + * [.protectedHeader()](#DecodedJwtPresentation+protectedHeader) ⇒ [JwsHeader](#JwsHeader) + * [.intoCredential()](#DecodedJwtPresentation+intoCredential) ⇒ [JwtPresentation](#JwtPresentation) + * [.expirationDate()](#DecodedJwtPresentation+expirationDate) ⇒ [Timestamp](#Timestamp) \| undefined + * [.issuanceDate()](#DecodedJwtPresentation+issuanceDate) ⇒ [Timestamp](#Timestamp) \| undefined + * [.credentials()](#DecodedJwtPresentation+credentials) ⇒ [Array.<DecodedJwtCredential>](#DecodedJwtCredential) + + + +### decodedJwtPresentation.presentation() ⇒ [JwtPresentation](#JwtPresentation) +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.protectedHeader() ⇒ [JwsHeader](#JwsHeader) +Returns a copy of the protected header parsed from the decoded JWS. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.intoCredential() ⇒ [JwtPresentation](#JwtPresentation) +Consumes the object and returns the decoded presentation. + +### Warning +This destroys the `DecodedJwtPresentation` object. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.expirationDate() ⇒ [Timestamp](#Timestamp) \| undefined +The expiration date parsed from the JWT claims. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.issuanceDate() ⇒ [Timestamp](#Timestamp) \| undefined +The issuance dated parsed from the JWT claims. + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) + + +### decodedJwtPresentation.credentials() ⇒ [Array.<DecodedJwtCredential>](#DecodedJwtCredential) +The credentials included in the presentation (decoded). + +**Kind**: instance method of [DecodedJwtPresentation](#DecodedJwtPresentation) ## DomainLinkageConfiguration @@ -2063,6 +2141,7 @@ Deserializes an instance from a JSON object. * [.purgeMethod(storage, id)](#IotaDocument+purgeMethod) ⇒ Promise.<void> * [.createJwt(storage, fragment, payload, options)](#IotaDocument+createJwt) ⇒ [Promise.<Jws>](#Jws) * [.createCredentialJwt(storage, fragment, credential, options)](#IotaDocument+createCredentialJwt) ⇒ [Promise.<Jwt>](#Jwt) + * [.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options)](#IotaDocument+createPresentationJwt) ⇒ [Promise.<Jwt>](#Jwt) * _static_ * [.newWithId(id)](#IotaDocument.newWithId) ⇒ [IotaDocument](#IotaDocument) * [.unpackFromOutput(did, aliasOutput, allowEmpty, tokenSupply)](#IotaDocument.unpackFromOutput) ⇒ [IotaDocument](#IotaDocument) @@ -2569,6 +2648,25 @@ produced by the corresponding private key backed by the `storage` in accordance | credential | [Credential](#Credential) | | options | [JwsSignatureOptions](#JwsSignatureOptions) | + + +### iotaDocument.createPresentationJwt(storage, fragment, presentation, signature_options, presentation_options) ⇒ [Promise.<Jwt>](#Jwt) +Produces a JWT where the payload is produced from the given `presentation` +in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). + +The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be +produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + +**Kind**: instance method of [IotaDocument](#IotaDocument) + +| Param | Type | +| --- | --- | +| storage | [Storage](#Storage) | +| fragment | string | +| presentation | [JwtPresentation](#JwtPresentation) | +| signature_options | [JwsSignatureOptions](#JwsSignatureOptions) | +| presentation_options | [JwtPresentationOptions](#JwtPresentationOptions) | + ### IotaDocument.newWithId(id) ⇒ [IotaDocument](#IotaDocument) @@ -3943,6 +4041,61 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## JwtPresentationOptions +**Kind**: global class + +* [JwtPresentationOptions](#JwtPresentationOptions) + * [new JwtPresentationOptions(options)](#new_JwtPresentationOptions_new) + * _instance_ + * [.toJSON()](#JwtPresentationOptions+toJSON) ⇒ any + * [.clone()](#JwtPresentationOptions+clone) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) + * _static_ + * [.default()](#JwtPresentationOptions.default) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) + * [.fromJSON(json)](#JwtPresentationOptions.fromJSON) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) + + + +### new JwtPresentationOptions(options) +Creates a new `JwtPresentationOptions` from the given fields. + +Throws an error if any of the options are invalid. + + +| Param | Type | +| --- | --- | +| options | IJwtPresentationOptions \| undefined | + + + +### jwtPresentationOptions.toJSON() ⇒ any +Serializes this to a JSON object. + +**Kind**: instance method of [JwtPresentationOptions](#JwtPresentationOptions) + + +### jwtPresentationOptions.clone() ⇒ [JwtPresentationOptions](#JwtPresentationOptions) +Deep clones the object. + +**Kind**: instance method of [JwtPresentationOptions](#JwtPresentationOptions) + + +### JwtPresentationOptions.default() ⇒ [JwtPresentationOptions](#JwtPresentationOptions) +Creates a new `JwtPresentationOptions` with defaults. + +**Kind**: static method of [JwtPresentationOptions](#JwtPresentationOptions) + + +### JwtPresentationOptions.fromJSON(json) ⇒ [JwtPresentationOptions](#JwtPresentationOptions) +Deserializes an instance from a JSON object. + +**Kind**: static method of [JwtPresentationOptions](#JwtPresentationOptions) + +| Param | Type | +| --- | --- | +| json | any | + ## JwtPresentationValidationOptions @@ -4000,6 +4153,62 @@ Deserializes an instance from a JSON object. | --- | --- | | json | any | + + +## JwtPresentationValidator +**Kind**: global class + +* [JwtPresentationValidator](#JwtPresentationValidator) + * [new JwtPresentationValidator(signature_verifier)](#new_JwtPresentationValidator_new) + * _instance_ + * [.validate(presentation_jwt, holder, issuers, options, fail_fast)](#JwtPresentationValidator+validate) ⇒ [DecodedJwtPresentation](#DecodedJwtPresentation) + * _static_ + * [.checkStructure(presentation)](#JwtPresentationValidator.checkStructure) + * [.extractDids(presentation)](#JwtPresentationValidator.extractDids) ⇒ JwtPresentationDids + + + +### new JwtPresentationValidator(signature_verifier) +Creates a new `JwtPresentationValidator`. If a `signature_verifier` is provided it will be used when +verifying decoded JWS signatures, otherwise the default which is only capable of handling the `EdDSA` +algorithm will be used. + + +| Param | Type | +| --- | --- | +| signature_verifier | IJwsVerifier \| undefined | + + + +### jwtPresentationValidator.validate(presentation_jwt, holder, issuers, options, fail_fast) ⇒ [DecodedJwtPresentation](#DecodedJwtPresentation) +**Kind**: instance method of [JwtPresentationValidator](#JwtPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation_jwt | [Jwt](#Jwt) | +| holder | [CoreDocument](#CoreDocument) \| IToCoreDocument | +| issuers | Array.<(CoreDocument\|IToCoreDocument)> | +| options | [JwtPresentationValidationOptions](#JwtPresentationValidationOptions) | +| fail_fast | number | + + + +### JwtPresentationValidator.checkStructure(presentation) +**Kind**: static method of [JwtPresentationValidator](#JwtPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation | [JwtPresentation](#JwtPresentation) | + + + +### JwtPresentationValidator.extractDids(presentation) ⇒ JwtPresentationDids +**Kind**: static method of [JwtPresentationValidator](#JwtPresentationValidator) + +| Param | Type | +| --- | --- | +| presentation | [Jwt](#Jwt) | + ## KeyPair @@ -5616,6 +5825,10 @@ This is possible because Ed25519 is birationally equivalent to Curve25519 used b | --- | --- | | publicKey | Uint8Array | + + +## StateMetadataEncoding +**Kind**: global variable ## StatusCheck @@ -5698,20 +5911,32 @@ Return after the first error occurs. ## KeyType **Kind**: global variable - - -## StateMetadataEncoding -**Kind**: global variable ## MethodRelationship **Kind**: global variable - + -## start() -Initializes the console error panic hook for better error messages +## verifyEdDSA(alg, signingInput, decodedSignature, publicKey) +Verify a JWS signature secured with the `JwsAlgorithm::EdDSA` algorithm. +Only the `EdCurve::Ed25519` variant is supported for now. + +This function is useful when one is building an `IJwsVerifier` that extends the default provided by +the IOTA Identity Framework. + +# Warning +This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this +prior to calling the function. **Kind**: global function + +| Param | Type | +| --- | --- | +| alg | JwsAlgorithm | +| signingInput | Uint8Array | +| decodedSignature | Uint8Array | +| publicKey | [Jwk](#Jwk) | + ## encodeB64(data) ⇒ string @@ -5734,25 +5959,9 @@ Decode the given url-safe base64-encoded slice into its raw bytes. | --- | --- | | data | Uint8Array | - - -## verifyEdDSA(alg, signingInput, decodedSignature, publicKey) -Verify a JWS signature secured with the `JwsAlgorithm::EdDSA` algorithm. -Only the `EdCurve::Ed25519` variant is supported for now. - -This function is useful when one is building an `IJwsVerifier` that extends the default provided by -the IOTA Identity Framework. + -# Warning -This function does not check whether `alg = EdDSA` in the protected header. Callers are expected to assert this -prior to calling the function. +## start() +Initializes the console error panic hook for better error messages **Kind**: global function - -| Param | Type | -| --- | --- | -| alg | JwsAlgorithm | -| signingInput | Uint8Array | -| decodedSignature | Uint8Array | -| publicKey | [Jwk](#Jwk) | - diff --git a/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs b/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs index 14dfb1ce5a..4630c82e45 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/decoded_jwt_credential.rs @@ -37,3 +37,9 @@ impl WasmDecodedJwtCredential { WasmCredential(self.0.credential) } } + +impl From for WasmDecodedJwtCredential { + fn from(credential: DecodedJwtCredential) -> Self { + Self(credential) + } +} diff --git a/bindings/wasm/src/credential/jwt_credential_validation/options.rs b/bindings/wasm/src/credential/jwt_credential_validation/options.rs index 84a40393fe..9c1af47991 100644 --- a/bindings/wasm/src/credential/jwt_credential_validation/options.rs +++ b/bindings/wasm/src/credential/jwt_credential_validation/options.rs @@ -69,6 +69,5 @@ interface IJwtCredentialValidationOptions { readonly status?: StatusCheck; /** Options which affect the verification of the signature on the credential. */ - readonly verifierOptions?: VerifierOptions; - + readonly verifierOptions?: JwsVerificationOptions; }"#; diff --git a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs index 8abccef68c..5fee2025a3 100644 --- a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs +++ b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation_builder.rs @@ -45,10 +45,8 @@ impl TryFrom for JwtPresentationBuilder { builder = builder.type_(value); } } - if let Some(credentials) = verifiable_credential { - for credential in credentials.into_vec() { - builder = builder.credential(credential); - } + for credential in verifiable_credential.into_vec() { + builder = builder.credential(Jwt::new(credential)); } if let Some(refresh_service) = refresh_service { for service in refresh_service.into_vec() { @@ -71,7 +69,7 @@ extern "C" { pub type IJwtPresentation; } -/// Fields for constructing a new {@link Presentation}. +/// Fields for constructing a new {@link JwtPresentation}. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] #[typescript(name = "IJwtPresentation", readonly, optional)] @@ -85,13 +83,9 @@ struct IJwtPresentationHelper { /// One or more URIs defining the type of the presentation. Contains the base context by default. #[typescript(name = "type", type = "string | Array")] r#type: Option>, - /// Credential(s) expressing the claims of the presentation. - #[typescript( - optional = false, - name = "verifiableCredential", - type = "Credential | Array" - )] - verifiable_credential: Option>, + /// JWT Credential(s) expressing the claims of the presentation. + #[typescript(optional = false, name = "verifiableCredential", type = "string | Array")] + verifiable_credential: OneOrMany, /// The entity that generated the presentation. #[typescript(optional = false, type = "string | CoreDID | IotaDID ")] holder: String, diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs new file mode 100644 index 0000000000..cd5d9b6e51 --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs @@ -0,0 +1,78 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use identity_iota::credential::DecodedJwtPresentation; +use wasm_bindgen::prelude::*; + +use crate::common::WasmTimestamp; +use crate::credential::jwt_presentation::WasmJwtPresentation; +use crate::credential::{ArrayDecodedJwtCredential, WasmDecodedJwtCredential}; +use crate::jose::WasmJwsHeader; + +/// A cryptographically verified and decoded presentation. +/// +/// Note that having an instance of this type only means the JWS it was constructed from was verified. +/// It does not imply anything about a potentially present proof property on the presentation itself. +#[wasm_bindgen(js_name = DecodedJwtPresentation)] +pub struct WasmDecodedJwtPresentation(pub(crate) DecodedJwtPresentation); + +#[wasm_bindgen(js_class = DecodedJwtPresentation)] +impl WasmDecodedJwtPresentation { + #[wasm_bindgen] + pub fn presentation(&self) -> WasmJwtPresentation { + WasmJwtPresentation(self.0.presentation.clone()) + } + + /// Returns a copy of the protected header parsed from the decoded JWS. + #[wasm_bindgen(js_name = protectedHeader)] + pub fn protected_header(&self) -> WasmJwsHeader { + WasmJwsHeader(self.0.header.as_ref().clone()) + } + + /// Consumes the object and returns the decoded presentation. + /// + /// ### Warning + /// This destroys the `DecodedJwtPresentation` object. + #[wasm_bindgen(js_name = intoCredential)] + pub fn into_presentation(self) -> WasmJwtPresentation { + WasmJwtPresentation(self.0.presentation) + } + + /// The expiration date parsed from the JWT claims. + #[wasm_bindgen(js_name = expirationDate)] + pub fn expiration_date(&self) -> Option { + self.0.expiration_date.map(WasmTimestamp::from) + } + + /// The issuance dated parsed from the JWT claims. + #[wasm_bindgen(js_name = "issuanceDate")] + pub fn issuance_date(&self) -> Option { + self.0.issuance_date.map(WasmTimestamp::from) + } + + /// The `aud` property parsed from JWT claims. + #[wasm_bindgen] + pub fn audience(&self) -> Option { + self.0.aud.clone().map(|aud| aud.to_string()) + } + + /// The credentials included in the presentation (decoded). + #[wasm_bindgen(js_name = "credentials")] + pub fn credentials(&self) -> ArrayDecodedJwtCredential { + self + .0 + .credentials + .iter() + .cloned() + .map(WasmDecodedJwtCredential::from) + .map(JsValue::from) + .collect::() + .unchecked_into::() + } +} + +impl From for WasmDecodedJwtPresentation { + fn from(decoded_presentation: DecodedJwtPresentation) -> Self { + Self(decoded_presentation) + } +} diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs new file mode 100644 index 0000000000..dc667608db --- /dev/null +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs @@ -0,0 +1,95 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use std::collections::BTreeMap; + +use identity_iota::core::Object; +use identity_iota::core::OneOrMany; +use identity_iota::core::Url; +use identity_iota::credential::vc_jwt_validation::CredentialValidator as JwtCredentialValidator; +use identity_iota::credential::DecodedJwtPresentation; +use identity_iota::credential::StatusCheck; +use identity_iota::did::CoreDID; + +use crate::common::ImportedDocumentLock; +use crate::common::ImportedDocumentReadGuard; +use crate::common::WasmTimestamp; +use crate::credential::jwt_presentation::WasmJwtPresentation; +use crate::credential::types::ArrayCoreDID; +use crate::credential::JwtPresentationDids; +use crate::credential::WasmFailFast; +use crate::credential::WasmJwt; +use crate::did::ArrayIToCoreDocument; +use crate::did::IToCoreDocument; +use crate::did::WasmCoreDID; +use crate::error::Result; +use crate::error::WasmResult; +use crate::verification::IJwsVerifier; +use crate::verification::WasmJwsVerifier; + +use identity_iota::credential::JwtPresentationValidator; +use wasm_bindgen::prelude::*; + +use super::decoded_jwt_presentation::WasmDecodedJwtPresentation; +use super::options::WasmJwtPresentationValidationOptions; + +#[wasm_bindgen(js_name = JwtPresentationValidator, inspectable)] +pub struct WasmJwtPresentationValidator(JwtPresentationValidator); + +#[wasm_bindgen(js_class = JwtPresentationValidator)] +impl WasmJwtPresentationValidator { + /// Creates a new `JwtPresentationValidator`. If a `signature_verifier` is provided it will be used when + /// verifying decoded JWS signatures, otherwise the default which is only capable of handling the `EdDSA` + /// algorithm will be used. + #[wasm_bindgen(constructor)] + pub fn new(signature_verifier: Option) -> WasmJwtPresentationValidator { + let signature_verifier = WasmJwsVerifier::new(signature_verifier); + WasmJwtPresentationValidator(JwtPresentationValidator::with_signature_verifier(signature_verifier)) + } + + #[wasm_bindgen] + pub fn validate( + &self, + presentation_jwt: &WasmJwt, + holder: &IToCoreDocument, + issuers: &ArrayIToCoreDocument, + options: &WasmJwtPresentationValidationOptions, + fail_fast: WasmFailFast, + ) -> Result { + let issuer_locks: Vec = issuers.into(); + let issuers_guards: Vec> = + issuer_locks.iter().map(ImportedDocumentLock::blocking_read).collect(); + + let holder_lock = ImportedDocumentLock::from(holder); + let holder_guard = holder_lock.blocking_read(); + + self + .0 + .validate( + &presentation_jwt.0, + &holder_guard, + &issuers_guards, + &options.0, + fail_fast.into(), + ) + .map(WasmDecodedJwtPresentation::from) + .wasm_result() + } + + #[wasm_bindgen(js_name = checkStructure)] + pub fn check_structure(presentation: &WasmJwtPresentation) -> Result<()> { + JwtPresentationValidator::check_structure(&presentation.0).wasm_result()?; + Ok(()) + } + + #[wasm_bindgen(js_name = extractDids)] + pub fn extract_dids(presentation: &WasmJwt) -> Result { + let (holder, issuers) = + JwtPresentationValidator::extract_dids::(&presentation.0).wasm_result()?; + let mut map = BTreeMap::<&str, OneOrMany>::new(); + map.insert("holder", OneOrMany::One(holder)); + map.insert("issuers", OneOrMany::Many(issuers)); + + Ok(JsValue::from_serde(&map).wasm_result()?.unchecked_into()) + } +} diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs index bd493cc4ab..12c556852e 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/mod.rs @@ -1,4 +1,10 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +mod decoded_jwt_presentation; +mod jwt_presentation_validator; mod options; + +pub use self::decoded_jwt_presentation::*; +pub use self::jwt_presentation_validator::*; +pub use self::options::*; diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/options.rs b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs index e006dc1f8e..72ed50e115 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/options.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs @@ -68,4 +68,12 @@ interface IJwtPresentationValidationOptions { /** Options which affect the verification of the signature on the credential. */ readonly verifierOptions?: VerifierOptions; + + /** Declare that the presentation is **not** considered valid if it expires before this `Timestamp`. + * Uses the current datetime during validation if not set. */ + readonly earliestExpiryDate?: Timestamp; + + /** Declare that the presentation is **not** considered valid if it was issued later than this `Timestamp`. + * Uses the current datetime during validation if not set. */ + readonly latestIssuanceDate?: Timestamp; }"#; diff --git a/bindings/wasm/src/credential/mod.rs b/bindings/wasm/src/credential/mod.rs index 166e31485d..63b6654d2f 100644 --- a/bindings/wasm/src/credential/mod.rs +++ b/bindings/wasm/src/credential/mod.rs @@ -10,6 +10,8 @@ pub use self::domain_linkage_configuration::WasmDomainLinkageConfiguration; pub use self::jws::WasmJws; pub use self::jwt::WasmJwt; pub use self::jwt_credential_validation::*; +pub use self::jwt_presentation::*; +pub use self::jwt_presentation_validation::*; pub use self::presentation::WasmPresentation; pub use self::presentation_builder::*; pub use self::presentation_validator::WasmPresentationValidator; diff --git a/bindings/wasm/src/credential/types.rs b/bindings/wasm/src/credential/types.rs index b47a32b6c3..5ba93ffc91 100644 --- a/bindings/wasm/src/credential/types.rs +++ b/bindings/wasm/src/credential/types.rs @@ -32,8 +32,17 @@ extern "C" { #[wasm_bindgen(typescript_type = "Array")] pub type ArrayCredential; + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayDecodedJwtCredential; + #[wasm_bindgen(typescript_type = "Array")] pub type ArrayJwt; + + #[wasm_bindgen(typescript_type = "Array")] + pub type ArrayCoreDID; + + #[wasm_bindgen(typescript_type = "JwtPresentationDids")] + pub type JwtPresentationDids; } #[wasm_bindgen(typescript_custom_section)] @@ -129,3 +138,19 @@ interface Subject { /** Additional properties of the credential subject. */ readonly [properties: string]: unknown; }"#; + +#[wasm_bindgen(typescript_custom_section)] +const I_SUBJECT: &'static str = r#" +/** + * DIDs of the presentation holder and the issuers of the contained credentials. + */ +interface JwtPresentationDids { + /** + * Presentation holder. + */ + holder: CoreDID; + /** + * Issuers of the verifiable credentials contained in the presentation. + */ + issuers: Array; +}"#; diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index 99683450fd..e8fdb94bff 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -18,6 +18,7 @@ use crate::common::UOneOrManyNumber; use crate::credential::WasmCredential; use crate::credential::WasmJws; use crate::credential::WasmJwt; +use crate::credential::WasmJwtPresentation; use crate::crypto::WasmProofOptions; use crate::did::service::WasmService; use crate::did::wasm_did_url::WasmDIDUrl; @@ -27,6 +28,7 @@ use crate::error::WasmResult; use crate::jose::WasmDecodedJws; use crate::jose::WasmJwsAlgorithm; use crate::storage::WasmJwsSignatureOptions; +use crate::storage::WasmJwtPresentationOptions; use crate::storage::WasmStorage; use crate::storage::WasmStorageInner; use crate::verification::IJwsVerifier; @@ -41,6 +43,8 @@ use identity_iota::core::OneOrSet; use identity_iota::core::OrderedSet; use identity_iota::core::Url; use identity_iota::credential::Credential; +use identity_iota::credential::JwtPresentation; +use identity_iota::credential::JwtPresentationOptions; use identity_iota::credential::RevocationDocumentExt; use identity_iota::crypto::PrivateKey; use identity_iota::crypto::ProofOptions; @@ -702,13 +706,11 @@ impl WasmCoreDocument { Ok(promise.unchecked_into()) } - /// Produces a JWS where the payload is produced from the given `credential` + /// Produces a JWT where the payload is produced from the given `credential` /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. - // TODO: Perhaps this should be called `signCredential` (and the old `signCredential` method would have to be updated - // or removed)? #[wasm_bindgen(js_name = createCredentialJwt)] pub fn create_credential_jwt( &self, @@ -733,6 +735,44 @@ impl WasmCoreDocument { }); Ok(promise.unchecked_into()) } + + /// Produces a JWT where the payload is produced from the given `presentation` + /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). + /// + /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be + /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwt)] + pub fn create_presentation_jwt( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmJwtPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: JwtPresentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .sign_presentation( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } } #[wasm_bindgen] diff --git a/bindings/wasm/src/error.rs b/bindings/wasm/src/error.rs index 122f2b0cf9..a345af664a 100644 --- a/bindings/wasm/src/error.rs +++ b/bindings/wasm/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use identity_iota::credential::CompoundJwtPresentationValidationError; use identity_iota::resolver; use identity_iota::storage::key_id_storage::KeyIdStorageError; use identity_iota::storage::key_id_storage::KeyIdStorageErrorKind; @@ -259,6 +260,15 @@ impl From for WasmError<'_> { + fn from(error: CompoundJwtPresentationValidationError) -> Self { + Self { + name: Cow::Borrowed("CompoundJwtPresentationValidationError"), + message: Cow::Owned(ErrorMessage(&error).to_string()), + } + } +} + /// Convenience struct to convert Result to errors in the Rust library. pub struct JsValueResult(pub(crate) Result); diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index 5f21d0d465..d52d7174f6 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -8,6 +8,8 @@ use identity_iota::core::OrderedSet; use identity_iota::core::Timestamp; use identity_iota::core::Url; use identity_iota::credential::Credential; +use identity_iota::credential::JwtPresentation; +use identity_iota::credential::JwtPresentationOptions; use identity_iota::credential::Presentation; use identity_iota::crypto::PrivateKey; use identity_iota::crypto::ProofOptions; @@ -46,6 +48,7 @@ use crate::common::WasmTimestamp; use crate::credential::WasmCredential; use crate::credential::WasmJws; use crate::credential::WasmJwt; +use crate::credential::WasmJwtPresentation; use crate::credential::WasmPresentation; use crate::crypto::WasmProofOptions; use crate::did::CoreDocumentLock; @@ -65,6 +68,7 @@ use crate::iota::WasmStateMetadataEncoding; use crate::jose::WasmDecodedJws; use crate::jose::WasmJwsAlgorithm; use crate::storage::WasmJwsSignatureOptions; +use crate::storage::WasmJwtPresentationOptions; use crate::storage::WasmStorage; use crate::storage::WasmStorageInner; use crate::verification::IJwsVerifier; @@ -827,8 +831,6 @@ impl WasmIotaDocument { /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. - // TODO: Perhaps this should be called `signCredential` (and the old `signCredential` method would have to be updated - // or removed)? #[wasm_bindgen(js_name = createCredentialJwt)] pub fn create_credential_jwt( &self, @@ -853,6 +855,44 @@ impl WasmIotaDocument { }); Ok(promise.unchecked_into()) } + + /// Produces a JWT where the payload is produced from the given `presentation` + /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). + /// + /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be + /// produced by the corresponding private key backed by the `storage` in accordance with the passed `options`. + #[wasm_bindgen(js_name = createPresentationJwt)] + pub fn create_presentation_jwt( + &self, + storage: &WasmStorage, + fragment: String, + presentation: &WasmJwtPresentation, + signature_options: &WasmJwsSignatureOptions, + presentation_options: &WasmJwtPresentationOptions, + ) -> Result { + let storage_clone: Rc = storage.0.clone(); + let options_clone: JwsSignatureOptions = signature_options.0.clone(); + let document_lock_clone: Rc = self.0.clone(); + let presentation_clone: JwtPresentation = presentation.0.clone(); + let presentation_options_clone: JwtPresentationOptions = presentation_options.0.clone(); + let promise: Promise = future_to_promise(async move { + document_lock_clone + .read() + .await + .sign_presentation( + &presentation_clone, + &storage_clone, + &fragment, + &options_clone, + &presentation_options_clone, + ) + .await + .wasm_result() + .map(WasmJwt::new) + .map(JsValue::from) + }); + Ok(promise.unchecked_into()) + } } impl From for WasmIotaDocument { diff --git a/bindings/wasm/src/storage/jwt_presentation_options.rs b/bindings/wasm/src/storage/jwt_presentation_options.rs new file mode 100644 index 0000000000..a512644536 --- /dev/null +++ b/bindings/wasm/src/storage/jwt_presentation_options.rs @@ -0,0 +1,80 @@ +// Copyright 2020-2023 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::{Result, WasmResult}; +use identity_iota::credential::JwtPresentationOptions; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(js_name = JwtPresentationOptions)] +pub struct WasmJwtPresentationOptions(pub(crate) JwtPresentationOptions); + +#[wasm_bindgen(js_class = JwtPresentationOptions)] +impl WasmJwtPresentationOptions { + /// Creates a new `JwtPresentationOptions` from the given fields. + /// + /// Throws an error if any of the options are invalid. + #[wasm_bindgen(constructor)] + pub fn new(options: Option) -> Result { + if let Some(options) = options { + let options: JwtPresentationOptions = options.into_serde().wasm_result()?; + Ok(WasmJwtPresentationOptions::from(options)) + } else { + Ok(WasmJwtPresentationOptions::from(JwtPresentationOptions::default())) + } + } + + /// Creates a new `JwtPresentationOptions` with defaults. + #[allow(clippy::should_implement_trait)] + #[wasm_bindgen] + pub fn default() -> WasmJwtPresentationOptions { + WasmJwtPresentationOptions::from(JwtPresentationOptions::default()) + } +} + +impl_wasm_json!(WasmJwtPresentationOptions, JwtPresentationOptions); +impl_wasm_clone!(WasmJwtPresentationOptions, JwtPresentationOptions); + +impl From for WasmJwtPresentationOptions { + fn from(options: JwtPresentationOptions) -> Self { + Self(options) + } +} + +impl From for JwtPresentationOptions { + fn from(options: WasmJwtPresentationOptions) -> Self { + options.0 + } +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IJwtPresentationOptions")] + pub type IJwtPresentationOptions; +} + +#[wasm_bindgen(typescript_custom_section)] +const I_PRESENTATION_OPTIONS: &'static str = r#" +/** Options to be set in the JWT claims of a verifiable presentation. */ +interface IJwtPresentationOptions { + /** + * Set the presentation's expiration date. + * Default: `undefined`. + **/ + readonly expirationDate?: Timestamp; + + /** + * Set the issuance date. + * Default: current datetime. + */ + readonly issuanceDate?: Timestamp; + + /** + * Sets the audience for presentation (`aud` property in JWT claims). + * + * ## Note: + * Value must be a valid URL. + * + * Default: `undefined` + */ + readonly audience?: string; +}"#; diff --git a/bindings/wasm/src/storage/mod.rs b/bindings/wasm/src/storage/mod.rs index 88a36f4c04..8295d95e88 100644 --- a/bindings/wasm/src/storage/mod.rs +++ b/bindings/wasm/src/storage/mod.rs @@ -3,6 +3,7 @@ mod jwk_gen_output; mod jwk_storage; +mod jwt_presentation_options; mod key_id_storage; mod method_digest; mod signature_options; @@ -10,6 +11,7 @@ mod wasm_storage; pub use jwk_gen_output::*; pub use jwk_storage::*; +pub use jwt_presentation_options::*; pub use key_id_storage::*; pub use method_digest::*; pub use signature_options::*; diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index 100132839b..b3cb095a2a 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -1,8 +1,9 @@ const assert = require("assert"); -import { RandomHelper } from "@iota/util.js"; import { CoreDocument, Credential, + DecodedJwtPresentation, + Duration, FailFast, IJwsVerifier, IotaDocument, @@ -10,18 +11,24 @@ import { JwsAlgorithm, JwsSignatureOptions, JwsVerificationOptions, + Jwt, JwtCredentialValidationOptions, JwtCredentialValidator, + JwtPresentation, + JwtPresentationOptions, + JwtPresentationValidationOptions, + JwtPresentationValidator, MethodDigest, MethodScope, Storage, + Timestamp, VerificationMethod, - verifyEdDSA, + verifyEdDSA } from "../node"; import { JwkMemStore } from "./jwk_storage"; import { createVerificationMethod, KeyIdMemStore } from "./key_id_storage"; -describe("#JwkStorageDocument", function() { +describe("#JwkStorageDocument", function () { it("storage getters should work", async () => { const keystore = new JwkMemStore(); // Put some data in the keystore @@ -58,7 +65,7 @@ describe("#JwkStorageDocument", function() { const storage = new Storage(keystore, keyIdStore); const VALID_DID_EXAMPLE = "did:example:123"; const doc = new CoreDocument({ - id: VALID_DID_EXAMPLE, + id: VALID_DID_EXAMPLE }); const fragment = "#key-1"; await doc.generateMethod( @@ -66,7 +73,7 @@ describe("#JwkStorageDocument", function() { JwkMemStore.ed25519KeyType(), JwsAlgorithm.EdDSA, fragment, - MethodScope.VerificationMethod(), + MethodScope.VerificationMethod() ); // Check that we can resolve the generated method. let method = doc.resolveMethod(fragment); @@ -96,11 +103,11 @@ describe("#JwkStorageDocument", function() { id: "did:example:ebfeb1f712ebc6f1c276e12ec21", degree: { type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, + name: "Bachelor of Science and Arts" + } }, issuer: doc.id(), - issuanceDate: "2010-01-01T00:00:00Z", + issuanceDate: "2010-01-01T00:00:00Z" }; const credential = new Credential(credentialFields); @@ -109,30 +116,23 @@ describe("#JwkStorageDocument", function() { // Check that the credentialJwt can be decoded and verified let credentialValidator = new JwtCredentialValidator(); - const credentialRetrieved = credentialValidator.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), - FailFast.FirstError, - ).credential(); + const credentialRetrieved = credentialValidator + .validate(credentialJwt, doc, JwtCredentialValidationOptions.default(), FailFast.FirstError) + .credential(); assert.deepStrictEqual(credentialRetrieved.toJSON(), credential.toJSON()); // Also check using our custom verifier let credentialValidatorCustom = new JwtCredentialValidator(customVerifier); - const credentialRetrievedCustom = credentialValidatorCustom.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), - FailFast.AllErrors, - ).credential(); + const credentialRetrievedCustom = credentialValidatorCustom + .validate(credentialJwt, doc, JwtCredentialValidationOptions.default(), FailFast.AllErrors) + .credential(); // Check that customVerifer.verify was indeed called assert.deepStrictEqual(customVerifier.verifications(), 2); assert.deepStrictEqual(credentialRetrievedCustom.toJSON(), credential.toJSON()); // Delete the method const methodId = (method as VerificationMethod).id(); - await doc.purgeMethod(storage, methodId); - // Check that the method can no longer be resolved. + await doc.purgeMethod(storage, methodId); // Check that the method can no longer be resolved. assert.deepStrictEqual(doc.resolveMethod(fragment), undefined); // The storage should now be empty assert.deepStrictEqual((storage.keyIdStorage() as KeyIdMemStore).count(), 0); @@ -151,7 +151,7 @@ describe("#JwkStorageDocument", function() { JwkMemStore.ed25519KeyType(), JwsAlgorithm.EdDSA, fragment, - MethodScope.VerificationMethod(), + MethodScope.VerificationMethod() ); // Check that we can resolve the generated method. let method = doc.resolveMethod(fragment); @@ -180,64 +180,138 @@ describe("#JwkStorageDocument", function() { id: "did:example:ebfeb1f712ebc6f1c276e12ec21", degree: { type: "BachelorDegree", - name: "Bachelor of Science and Arts", - }, + name: "Bachelor of Science and Arts" + } }, issuer: doc.id(), - issuanceDate: "2010-01-01T00:00:00Z", + issuanceDate: "2010-01-01T00:00:00Z" + }; + }); + + it("JwtPresentation should work", async () => { + const keystore = new JwkMemStore(); + const keyIdStore = new KeyIdMemStore(); + const storage = new Storage(keystore, keyIdStore); + const issuerDoc = new IotaDocument("n1"); + const fragment = "#key-1"; + await issuerDoc.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + fragment, + MethodScope.VerificationMethod() + ); + + const holderDoc = new IotaDocument("n2"); + await holderDoc.generateMethod( + storage, + JwkMemStore.ed25519KeyType(), + JwsAlgorithm.EdDSA, + fragment, + MethodScope.VerificationMethod() + ); + + let customVerifier = new CustomVerifier(); + const credentialFields = { + context: "https://www.w3.org/2018/credentials/examples/v1", + id: "https://example.edu/credentials/3732", + type: "UniversityDegreeCredential", + credentialSubject: { + id: holderDoc.id(), + degree: { + type: "BachelorDegree", + name: "Bachelor of Science and Arts" + } + }, + issuer: issuerDoc.id(), + issuanceDate: Timestamp.nowUTC() }; const credential = new Credential(credentialFields); - // Create the JWT - const credentialJwt = await doc.createCredentialJwt(storage, fragment, credential, new JwsSignatureOptions()); + const credentialJwt: Jwt = await issuerDoc.createCredentialJwt( + storage, + fragment, + credential, + new JwsSignatureOptions() + ); - // Check that the credentialJwt can be decoded and verified - let credentialValidator = new JwtCredentialValidator(); - const credentialRetrieved = credentialValidator.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), - FailFast.FirstError, - ).credential(); - assert.deepStrictEqual(credentialRetrieved.toJSON(), credential.toJSON()); + const presentation = new JwtPresentation({ + holder: holderDoc.id(), + verifiableCredential: [credentialJwt.toString(), credentialJwt.toString()] + }); - // Also check using our custom verifier - let credentialValidatorCustom = new JwtCredentialValidator(customVerifier); - const credentialRetrievedCustom = credentialValidatorCustom.validate( - credentialJwt, - doc, - JwtCredentialValidationOptions.default(), - FailFast.AllErrors, - ).credential(); - // Check that customVerifer.verify was indeed called - assert.deepStrictEqual(customVerifier.verifications(), 2); - assert.deepStrictEqual(credentialRetrievedCustom.toJSON(), credential.toJSON()); + const expirationDate = Timestamp.nowUTC().checkedAdd(Duration.days(2)); + const audience = "did:test:123"; + const presentationJwt = await holderDoc.createPresentationJwt( + storage, + fragment, + presentation, + new JwsSignatureOptions(), + new JwtPresentationOptions({ + expirationDate, + issuanceDate: Timestamp.nowUTC(), + audience + }) + ); - // Delete the method - const methodId = (method as VerificationMethod).id(); - await doc.purgeMethod(storage, methodId); - // Check that the method can no longer be resolved. - assert.deepStrictEqual(doc.resolveMethod(fragment), undefined); - // The storage should now be empty - assert.deepStrictEqual((storage.keyIdStorage() as KeyIdMemStore).count(), 0); - assert.deepStrictEqual((storage.keyStorage() as JwkMemStore).count(), 0); + let validator = new JwtPresentationValidator(customVerifier); + let decoded: DecodedJwtPresentation = validator.validate( + presentationJwt, + holderDoc, + [issuerDoc], + JwtPresentationValidationOptions.default(), + FailFast.FirstError + ); + + assert.deepStrictEqual(decoded.credentials()[0].credential().toJSON(), credential.toJSON()); + assert.equal(decoded.expirationDate()?.toString(), expirationDate?.toString()); + assert.deepStrictEqual(decoded.presentation().toJSON(), presentation.toJSON()); + assert.equal(decoded.audience(), audience); + + // check issuance date validation. + let options = new JwtPresentationValidationOptions({ + latestIssuanceDate: Timestamp.nowUTC().checkedSub(Duration.days(1)) + }); + assert.throws(() => { + validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); + }); + + // Check expiration date validation. + options = new JwtPresentationValidationOptions({ + earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(1)) + }); + validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); + + options = new JwtPresentationValidationOptions({ + earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(3)) + }); + assert.throws(() => { + validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); + }); + + // Check `extractDids`. + let presentationDids = JwtPresentationValidator.extractDids(presentationJwt); + assert.equal(presentationDids.holder.toString(), holderDoc.id().toString()); + assert.equal(presentationDids.issuers.length, 2); + assert.equal(presentationDids.issuers[0].toString(), issuerDoc.id().toString()); + assert.equal(presentationDids.issuers[1].toString(), issuerDoc.id().toString()); }); -}); -class CustomVerifier implements IJwsVerifier { - private _verifications: number; + class CustomVerifier implements IJwsVerifier { + private _verifications: number; - constructor() { - this._verifications = 0; - } + constructor() { + this._verifications = 0; + } - public verifications(): number { - return this._verifications; - } + public verifications(): number { + return this._verifications; + } - public verify(alg: JwsAlgorithm, signingInput: Uint8Array, decodedSignature: Uint8Array, publicKey: Jwk): void { - verifyEdDSA(alg, signingInput, decodedSignature, publicKey); - this._verifications += 1; - return; + public verify(alg: JwsAlgorithm, signingInput: Uint8Array, decodedSignature: Uint8Array, publicKey: Jwk): void { + verifyEdDSA(alg, signingInput, decodedSignature, publicKey); + this._verifications += 1; + return; + } } -} +}); diff --git a/identity_credential/src/presentation/jwt_presentation_options.rs b/identity_credential/src/presentation/jwt_presentation_options.rs index 925ffe0812..926d4621a4 100644 --- a/identity_credential/src/presentation/jwt_presentation_options.rs +++ b/identity_credential/src/presentation/jwt_presentation_options.rs @@ -5,7 +5,8 @@ use identity_core::common::Timestamp; use identity_core::common::Url; /// Options to be set in the JWT claims of a verifiable presentation. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JwtPresentationOptions { /// Set the presentation's expiration date. /// Default: `None`. diff --git a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs index 595e6a8f95..605a7aa5a4 100644 --- a/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs +++ b/identity_credential/src/validator/vp_jwt_validation/jwt_presentation_validator.rs @@ -195,13 +195,6 @@ where Ok(decoded_jwt_presentation) } - /// Validates the semantic structure of the `JwtPresentation`. - pub fn check_structure(presentation: &JwtPresentation) -> Result<(), ValidationError> { - presentation - .check_structure() - .map_err(ValidationError::PresentationStructure) - } - fn validate_credentials( &self, presentation: &JwtPresentation, @@ -291,4 +284,11 @@ impl JwtPresentationValidator { } Ok((holder, issuers)) } + + /// Validates the semantic structure of the `JwtPresentation`. + pub fn check_structure(presentation: &JwtPresentation) -> Result<(), ValidationError> { + presentation + .check_structure() + .map_err(ValidationError::PresentationStructure) + } } diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index ade03d8b63..2177a9139a 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -81,7 +81,7 @@ pub trait JwkDocumentExt: private::Sealed { K: JwkStorage, I: KeyIdStorage; - /// Produces a JWS where the payload is produced from the given `credential` + /// Produces a JWT where the payload is produced from the given `credential` /// in accordance with [VC-JWT version 1.1.](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be @@ -98,7 +98,7 @@ pub trait JwkDocumentExt: private::Sealed { I: KeyIdStorage, T: ToOwned + Serialize + DeserializeOwned + Sync; - /// Produces a JWS where the payload is produced from the given `presentation` + /// Produces a JWT where the payload is produced from the given `presentation` /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be From 28c2b5203449bd2a7870eda515a71b8d48f6bd7f Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 6 Jun 2023 18:11:43 +0200 Subject: [PATCH 28/31] fix fmt and clippy issues --- .../decoded_jwt_presentation.rs | 3 ++- .../jwt_presentation_validator.rs | 11 ++++------- bindings/wasm/src/storage/jwt_presentation_options.rs | 3 ++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs index cd5d9b6e51..85903b5793 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs @@ -6,7 +6,8 @@ use wasm_bindgen::prelude::*; use crate::common::WasmTimestamp; use crate::credential::jwt_presentation::WasmJwtPresentation; -use crate::credential::{ArrayDecodedJwtCredential, WasmDecodedJwtCredential}; +use crate::credential::ArrayDecodedJwtCredential; +use crate::credential::WasmDecodedJwtCredential; use crate::jose::WasmJwsHeader; /// A cryptographically verified and decoded presentation. diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs index dc667608db..794012853a 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs @@ -5,23 +5,20 @@ use std::collections::BTreeMap; use identity_iota::core::Object; use identity_iota::core::OneOrMany; -use identity_iota::core::Url; -use identity_iota::credential::vc_jwt_validation::CredentialValidator as JwtCredentialValidator; -use identity_iota::credential::DecodedJwtPresentation; -use identity_iota::credential::StatusCheck; + use identity_iota::did::CoreDID; use crate::common::ImportedDocumentLock; use crate::common::ImportedDocumentReadGuard; -use crate::common::WasmTimestamp; + use crate::credential::jwt_presentation::WasmJwtPresentation; -use crate::credential::types::ArrayCoreDID; + use crate::credential::JwtPresentationDids; use crate::credential::WasmFailFast; use crate::credential::WasmJwt; use crate::did::ArrayIToCoreDocument; use crate::did::IToCoreDocument; -use crate::did::WasmCoreDID; + use crate::error::Result; use crate::error::WasmResult; use crate::verification::IJwsVerifier; diff --git a/bindings/wasm/src/storage/jwt_presentation_options.rs b/bindings/wasm/src/storage/jwt_presentation_options.rs index a512644536..248f4f89c9 100644 --- a/bindings/wasm/src/storage/jwt_presentation_options.rs +++ b/bindings/wasm/src/storage/jwt_presentation_options.rs @@ -1,7 +1,8 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use crate::error::{Result, WasmResult}; +use crate::error::Result; +use crate::error::WasmResult; use identity_iota::credential::JwtPresentationOptions; use wasm_bindgen::prelude::*; From 469d5502afefd1685193bc66ad6c44cf14a8da20 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 6 Jun 2023 21:18:47 +0200 Subject: [PATCH 29/31] dprint fix --- .../jwt_presentation_validator.rs | 20 +++----- bindings/wasm/tests/storage.ts | 48 +++++++++---------- 2 files changed, 30 insertions(+), 38 deletions(-) diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs index 794012853a..c87f44e53d 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs @@ -1,35 +1,27 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::collections::BTreeMap; - -use identity_iota::core::Object; -use identity_iota::core::OneOrMany; - -use identity_iota::did::CoreDID; - +use super::decoded_jwt_presentation::WasmDecodedJwtPresentation; +use super::options::WasmJwtPresentationValidationOptions; use crate::common::ImportedDocumentLock; use crate::common::ImportedDocumentReadGuard; - use crate::credential::jwt_presentation::WasmJwtPresentation; - use crate::credential::JwtPresentationDids; use crate::credential::WasmFailFast; use crate::credential::WasmJwt; use crate::did::ArrayIToCoreDocument; use crate::did::IToCoreDocument; - use crate::error::Result; use crate::error::WasmResult; use crate::verification::IJwsVerifier; use crate::verification::WasmJwsVerifier; - +use identity_iota::core::Object; +use identity_iota::core::OneOrMany; use identity_iota::credential::JwtPresentationValidator; +use identity_iota::did::CoreDID; +use std::collections::BTreeMap; use wasm_bindgen::prelude::*; -use super::decoded_jwt_presentation::WasmDecodedJwtPresentation; -use super::options::WasmJwtPresentationValidationOptions; - #[wasm_bindgen(js_name = JwtPresentationValidator, inspectable)] pub struct WasmJwtPresentationValidator(JwtPresentationValidator); diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index b3cb095a2a..17857e4c9d 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -23,12 +23,12 @@ import { Storage, Timestamp, VerificationMethod, - verifyEdDSA + verifyEdDSA, } from "../node"; import { JwkMemStore } from "./jwk_storage"; import { createVerificationMethod, KeyIdMemStore } from "./key_id_storage"; -describe("#JwkStorageDocument", function () { +describe("#JwkStorageDocument", function() { it("storage getters should work", async () => { const keystore = new JwkMemStore(); // Put some data in the keystore @@ -65,7 +65,7 @@ describe("#JwkStorageDocument", function () { const storage = new Storage(keystore, keyIdStore); const VALID_DID_EXAMPLE = "did:example:123"; const doc = new CoreDocument({ - id: VALID_DID_EXAMPLE + id: VALID_DID_EXAMPLE, }); const fragment = "#key-1"; await doc.generateMethod( @@ -73,7 +73,7 @@ describe("#JwkStorageDocument", function () { JwkMemStore.ed25519KeyType(), JwsAlgorithm.EdDSA, fragment, - MethodScope.VerificationMethod() + MethodScope.VerificationMethod(), ); // Check that we can resolve the generated method. let method = doc.resolveMethod(fragment); @@ -103,11 +103,11 @@ describe("#JwkStorageDocument", function () { id: "did:example:ebfeb1f712ebc6f1c276e12ec21", degree: { type: "BachelorDegree", - name: "Bachelor of Science and Arts" - } + name: "Bachelor of Science and Arts", + }, }, issuer: doc.id(), - issuanceDate: "2010-01-01T00:00:00Z" + issuanceDate: "2010-01-01T00:00:00Z", }; const credential = new Credential(credentialFields); @@ -151,7 +151,7 @@ describe("#JwkStorageDocument", function () { JwkMemStore.ed25519KeyType(), JwsAlgorithm.EdDSA, fragment, - MethodScope.VerificationMethod() + MethodScope.VerificationMethod(), ); // Check that we can resolve the generated method. let method = doc.resolveMethod(fragment); @@ -180,11 +180,11 @@ describe("#JwkStorageDocument", function () { id: "did:example:ebfeb1f712ebc6f1c276e12ec21", degree: { type: "BachelorDegree", - name: "Bachelor of Science and Arts" - } + name: "Bachelor of Science and Arts", + }, }, issuer: doc.id(), - issuanceDate: "2010-01-01T00:00:00Z" + issuanceDate: "2010-01-01T00:00:00Z", }; }); @@ -199,7 +199,7 @@ describe("#JwkStorageDocument", function () { JwkMemStore.ed25519KeyType(), JwsAlgorithm.EdDSA, fragment, - MethodScope.VerificationMethod() + MethodScope.VerificationMethod(), ); const holderDoc = new IotaDocument("n2"); @@ -208,7 +208,7 @@ describe("#JwkStorageDocument", function () { JwkMemStore.ed25519KeyType(), JwsAlgorithm.EdDSA, fragment, - MethodScope.VerificationMethod() + MethodScope.VerificationMethod(), ); let customVerifier = new CustomVerifier(); @@ -220,11 +220,11 @@ describe("#JwkStorageDocument", function () { id: holderDoc.id(), degree: { type: "BachelorDegree", - name: "Bachelor of Science and Arts" - } + name: "Bachelor of Science and Arts", + }, }, issuer: issuerDoc.id(), - issuanceDate: Timestamp.nowUTC() + issuanceDate: Timestamp.nowUTC(), }; const credential = new Credential(credentialFields); @@ -232,12 +232,12 @@ describe("#JwkStorageDocument", function () { storage, fragment, credential, - new JwsSignatureOptions() + new JwsSignatureOptions(), ); const presentation = new JwtPresentation({ holder: holderDoc.id(), - verifiableCredential: [credentialJwt.toString(), credentialJwt.toString()] + verifiableCredential: [credentialJwt.toString(), credentialJwt.toString()], }); const expirationDate = Timestamp.nowUTC().checkedAdd(Duration.days(2)); @@ -250,8 +250,8 @@ describe("#JwkStorageDocument", function () { new JwtPresentationOptions({ expirationDate, issuanceDate: Timestamp.nowUTC(), - audience - }) + audience, + }), ); let validator = new JwtPresentationValidator(customVerifier); @@ -260,7 +260,7 @@ describe("#JwkStorageDocument", function () { holderDoc, [issuerDoc], JwtPresentationValidationOptions.default(), - FailFast.FirstError + FailFast.FirstError, ); assert.deepStrictEqual(decoded.credentials()[0].credential().toJSON(), credential.toJSON()); @@ -270,7 +270,7 @@ describe("#JwkStorageDocument", function () { // check issuance date validation. let options = new JwtPresentationValidationOptions({ - latestIssuanceDate: Timestamp.nowUTC().checkedSub(Duration.days(1)) + latestIssuanceDate: Timestamp.nowUTC().checkedSub(Duration.days(1)), }); assert.throws(() => { validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); @@ -278,12 +278,12 @@ describe("#JwkStorageDocument", function () { // Check expiration date validation. options = new JwtPresentationValidationOptions({ - earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(1)) + earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(1)), }); validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); options = new JwtPresentationValidationOptions({ - earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(3)) + earliestExpiryDate: Timestamp.nowUTC().checkedAdd(Duration.days(3)), }); assert.throws(() => { validator.validate(presentationJwt, holderDoc, [issuerDoc], options, FailFast.FirstError); From e442a09762b1c784c1428c2efbe53f5c5389511e Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Tue, 6 Jun 2023 22:09:20 +0200 Subject: [PATCH 30/31] fix interfaces and add documentations --- .../jwt_presentation/jwt_presentation.rs | 4 +- .../decoded_jwt_presentation.rs | 4 +- .../jwt_presentation_validator.rs | 38 ++++++++++++++++++- .../jwt_presentation_validation/options.rs | 34 ++++++++++------- bindings/wasm/src/credential/types.rs | 2 +- bindings/wasm/src/did/wasm_core_document.rs | 2 +- bindings/wasm/src/iota/iota_document.rs | 2 +- .../src/storage/jwt_presentation_options.rs | 6 +-- bindings/wasm/tests/storage.ts | 4 +- .../decoded_jwt_presentation.rs | 2 +- 10 files changed, 69 insertions(+), 29 deletions(-) diff --git a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs index 5804b1fff1..05209b90dd 100644 --- a/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs +++ b/bindings/wasm/src/credential/jwt_presentation/jwt_presentation.rs @@ -78,7 +78,7 @@ impl WasmJwtPresentation { .unchecked_into::() } - /// Returns a copy of the {@link Credential}(s) expressing the claims of the presentation. + /// Returns the JWT credentials expressing the claims of the presentation. #[wasm_bindgen(js_name = verifiableCredential)] pub fn verifiable_credential(&self) -> ArrayJwt { self @@ -124,7 +124,7 @@ impl WasmJwtPresentation { .map(|value| value.unchecked_into::()) } - /// Returns a copy of the proof property. + /// Optional proof that can be verified by users in addition to JWS. #[wasm_bindgen] pub fn proof(&self) -> Result> { self.0.proof.clone().map(MapStringAny::try_from).transpose() diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs index 85903b5793..71ae171405 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/decoded_jwt_presentation.rs @@ -34,7 +34,7 @@ impl WasmDecodedJwtPresentation { /// /// ### Warning /// This destroys the `DecodedJwtPresentation` object. - #[wasm_bindgen(js_name = intoCredential)] + #[wasm_bindgen(js_name = intoPresentation)] pub fn into_presentation(self) -> WasmJwtPresentation { WasmJwtPresentation(self.0.presentation) } @@ -45,7 +45,7 @@ impl WasmDecodedJwtPresentation { self.0.expiration_date.map(WasmTimestamp::from) } - /// The issuance dated parsed from the JWT claims. + /// The issuance date parsed from the JWT claims. #[wasm_bindgen(js_name = "issuanceDate")] pub fn issuance_date(&self) -> Option { self.0.issuance_date.map(WasmTimestamp::from) diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs index c87f44e53d..b159bc16b9 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/jwt_presentation_validator.rs @@ -36,13 +36,39 @@ impl WasmJwtPresentationValidator { WasmJwtPresentationValidator(JwtPresentationValidator::with_signature_verifier(signature_verifier)) } + /// Validates a `JwtPresentation`. + /// + /// The following properties are validated according to `options`: + /// - the JWT can be decoded into semantically valid presentation. + /// - the expiration and issuance date contained in the JWT claims. + /// - the holder's signature. + /// - the relationship between the holder and the credential subjects. + /// - the signatures and some properties of the constituent credentials (see `CredentialValidator`). + /// + /// Validation is done with respect to the properties set in `options`. + /// + /// # Warning + /// The lack of an error returned from this method is in of itself not enough to conclude that the presentation can be + /// trusted. This section contains more information on additional checks that should be carried out before and after + /// 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. + /// + /// ## 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: + /// `credentialStatus`, `type`, `credentialSchema`, `refreshService`, **and more**. + /// These should be manually checked after validation, according to your requirements. + /// + /// # Errors + /// An error is returned whenever a validated condition is not satisfied or when decoding fails. #[wasm_bindgen] pub fn validate( &self, presentation_jwt: &WasmJwt, holder: &IToCoreDocument, issuers: &ArrayIToCoreDocument, - options: &WasmJwtPresentationValidationOptions, + validation_options: &WasmJwtPresentationValidationOptions, fail_fast: WasmFailFast, ) -> Result { let issuer_locks: Vec = issuers.into(); @@ -58,19 +84,27 @@ impl WasmJwtPresentationValidator { &presentation_jwt.0, &holder_guard, &issuers_guards, - &options.0, + &validation_options.0, fail_fast.into(), ) .map(WasmDecodedJwtPresentation::from) .wasm_result() } + /// Validates the semantic structure of the `JwtPresentation`. #[wasm_bindgen(js_name = checkStructure)] pub fn check_structure(presentation: &WasmJwtPresentation) -> Result<()> { JwtPresentationValidator::check_structure(&presentation.0).wasm_result()?; Ok(()) } + /// Attempt to extract the holder of the presentation and the issuers of the included + /// credentials. + /// + /// # Errors: + /// * If deserialization/decoding of the presentation or any of the constituent credentials + /// fails. + /// * If the holder or any of the issuers can't be parsed as DIDs. #[wasm_bindgen(js_name = extractDids)] pub fn extract_dids(presentation: &WasmJwt) -> Result { let (holder, issuers) = diff --git a/bindings/wasm/src/credential/jwt_presentation_validation/options.rs b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs index 72ed50e115..7fd6f4781d 100644 --- a/bindings/wasm/src/credential/jwt_presentation_validation/options.rs +++ b/bindings/wasm/src/credential/jwt_presentation_validation/options.rs @@ -51,29 +51,35 @@ extern "C" { } #[wasm_bindgen(typescript_custom_section)] -const I_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#" +const I_JWT_PRESENTATION_VALIDATION_OPTIONS: &'static str = r#" /** Holds options to create a new `JwtPresentationValidationOptions`. */ interface IJwtPresentationValidationOptions { - /** Declare that the credentials of the presentation must all be validated according to these `CredentialValidationOptions`. */ - readonly sharedValidationOptions?: CredentialValidationOptions; + /** + * Options which affect the validation of *all* credentials in the presentation. + */ + readonly sharedValidationOptions?: JwtCredentialValidationOptions; - /** Options which affect the verification of the signature on the presentation. */ - readonly presentationVerifierOptions?: VerifierOptions; + /** + * Options which affect the verification of the signature on the presentation. + */ + readonly presentationVerifierOptions?: JwsVerificationOptions; - /** Declare how the presentation's credential subjects must relate to the holder. + /** + * Declare how the presentation's credential subjects must relate to the holder. * - * Default: SubjectHolderRelationship.AlwaysSubject + * Default: `SubjectHolderRelationship.AlwaysSubject` */ readonly subjectHolderRelationship?: SubjectHolderRelationship; - /** Options which affect the verification of the signature on the credential. */ - readonly verifierOptions?: VerifierOptions; - - /** Declare that the presentation is **not** considered valid if it expires before this `Timestamp`. - * Uses the current datetime during validation if not set. */ + /** + * Declare that the presentation is **not** considered valid if it expires before this `Timestamp`. + * Uses the current datetime during validation if not set. + */ readonly earliestExpiryDate?: Timestamp; - /** Declare that the presentation is **not** considered valid if it was issued later than this `Timestamp`. - * Uses the current datetime during validation if not set. */ + /** + * Declare that the presentation is **not** considered valid if it was issued later than this `Timestamp`. + * Uses the current datetime during validation if not set. + */ readonly latestIssuanceDate?: Timestamp; }"#; diff --git a/bindings/wasm/src/credential/types.rs b/bindings/wasm/src/credential/types.rs index 5ba93ffc91..615d4c0441 100644 --- a/bindings/wasm/src/credential/types.rs +++ b/bindings/wasm/src/credential/types.rs @@ -140,7 +140,7 @@ interface Subject { }"#; #[wasm_bindgen(typescript_custom_section)] -const I_SUBJECT: &'static str = r#" +const I_JWT_PRESENTATOIN_DIDS: &'static str = r#" /** * DIDs of the presentation holder and the issuers of the contained credentials. */ diff --git a/bindings/wasm/src/did/wasm_core_document.rs b/bindings/wasm/src/did/wasm_core_document.rs index e8fdb94bff..6b0150692b 100644 --- a/bindings/wasm/src/did/wasm_core_document.rs +++ b/bindings/wasm/src/did/wasm_core_document.rs @@ -736,7 +736,7 @@ impl WasmCoreDocument { Ok(promise.unchecked_into()) } - /// Produces a JWT where the payload is produced from the given `presentation` + /// Produces a JWT where the payload is produced from the given presentation. /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be diff --git a/bindings/wasm/src/iota/iota_document.rs b/bindings/wasm/src/iota/iota_document.rs index d52d7174f6..ff965ef8b0 100644 --- a/bindings/wasm/src/iota/iota_document.rs +++ b/bindings/wasm/src/iota/iota_document.rs @@ -856,7 +856,7 @@ impl WasmIotaDocument { Ok(promise.unchecked_into()) } - /// Produces a JWT where the payload is produced from the given `presentation` + /// Produces a JWT where the payload is produced from the given presentation. /// in accordance with [VC-JWT version 1.1](https://w3c.github.io/vc-jwt/#version-1.1). /// /// The `kid` in the protected header is the `id` of the method identified by `fragment` and the JWS signature will be diff --git a/bindings/wasm/src/storage/jwt_presentation_options.rs b/bindings/wasm/src/storage/jwt_presentation_options.rs index 248f4f89c9..ef41c6ff3a 100644 --- a/bindings/wasm/src/storage/jwt_presentation_options.rs +++ b/bindings/wasm/src/storage/jwt_presentation_options.rs @@ -54,7 +54,7 @@ extern "C" { } #[wasm_bindgen(typescript_custom_section)] -const I_PRESENTATION_OPTIONS: &'static str = r#" +const I_JWT_PRESENTATION_OPTIONS: &'static str = r#" /** Options to be set in the JWT claims of a verifiable presentation. */ interface IJwtPresentationOptions { /** @@ -64,7 +64,7 @@ interface IJwtPresentationOptions { readonly expirationDate?: Timestamp; /** - * Set the issuance date. + * Set the presentation's issuance date. * Default: current datetime. */ readonly issuanceDate?: Timestamp; @@ -75,7 +75,7 @@ interface IJwtPresentationOptions { * ## Note: * Value must be a valid URL. * - * Default: `undefined` + * Default: `undefined`. */ readonly audience?: string; }"#; diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index 17857e4c9d..dc7beb9277 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -192,7 +192,7 @@ describe("#JwkStorageDocument", function() { const keystore = new JwkMemStore(); const keyIdStore = new KeyIdMemStore(); const storage = new Storage(keystore, keyIdStore); - const issuerDoc = new IotaDocument("n1"); + const issuerDoc = new IotaDocument("tst1"); const fragment = "#key-1"; await issuerDoc.generateMethod( storage, @@ -202,7 +202,7 @@ describe("#JwkStorageDocument", function() { MethodScope.VerificationMethod(), ); - const holderDoc = new IotaDocument("n2"); + const holderDoc = new IotaDocument("tst2"); await holderDoc.generateMethod( storage, JwkMemStore.ed25519KeyType(), diff --git a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs index c81c6a1130..d84c5130bb 100644 --- a/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs +++ b/identity_credential/src/validator/vp_jwt_validation/decoded_jwt_presentation.rs @@ -22,7 +22,7 @@ pub struct DecodedJwtPresentation { pub header: Box, /// The expiration date parsed from the JWT claims. pub expiration_date: Option, - /// The issuance dated parsed from the JWT claims. + /// The issuance date parsed from the JWT claims. pub issuance_date: Option, /// The `aud` property parsed from the JWT claims. pub aud: Option, From c92296a7aef69191d475add22a4a8cebc958a7d7 Mon Sep 17 00:00:00 2001 From: Abdulrahim Al Methiab Date: Sun, 11 Jun 2023 21:01:37 +0200 Subject: [PATCH 31/31] improve test --- bindings/wasm/tests/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/wasm/tests/storage.ts b/bindings/wasm/tests/storage.ts index dc7beb9277..d9c3b1dd4a 100644 --- a/bindings/wasm/tests/storage.ts +++ b/bindings/wasm/tests/storage.ts @@ -264,7 +264,7 @@ describe("#JwkStorageDocument", function() { ); assert.deepStrictEqual(decoded.credentials()[0].credential().toJSON(), credential.toJSON()); - assert.equal(decoded.expirationDate()?.toString(), expirationDate?.toString()); + assert.equal(decoded.expirationDate()!.toString(), expirationDate!.toString()); assert.deepStrictEqual(decoded.presentation().toJSON(), presentation.toJSON()); assert.equal(decoded.audience(), audience);