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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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`.