From f48934b8e9657912aad2dd449cddf6b3d1201f0e Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 19 Feb 2024 12:26:58 +0100 Subject: [PATCH 01/10] Crate structure --- .../src/credential/{ => common}/evidence.rs | 4 +- .../src/credential/{ => common}/issuer.rs | 4 +- .../{ => common}/linked_domain_service.rs | 2 +- .../src/credential/common/mod.rs | 17 ++++++++ .../src/credential/{ => common}/policy.rs | 4 +- .../src/credential/{ => common}/proof.rs | 0 .../src/credential/{ => common}/refresh.rs | 2 +- .../src/credential/{ => common}/schema.rs | 6 +-- .../src/credential/{ => common}/subject.rs | 20 +++++----- identity_credential/src/credential/mod.rs | 39 ++++++++----------- identity_credential/src/credential/traits.rs | 28 +++++++++++++ .../src/credential/{ => vc1_1}/builder.rs | 2 +- .../src/credential/{ => vc1_1}/credential.rs | 28 ++++++------- .../src/credential/vc1_1/mod.rs | 7 ++++ .../src/credential/{ => vc1_1}/status.rs | 2 +- .../src/credential/vc2_0/mod.rs | 0 16 files changed, 106 insertions(+), 59 deletions(-) rename identity_credential/src/credential/{ => common}/evidence.rs (95%) rename identity_credential/src/credential/{ => common}/issuer.rs (92%) rename identity_credential/src/credential/{ => common}/linked_domain_service.rs (99%) create mode 100644 identity_credential/src/credential/common/mod.rs rename identity_credential/src/credential/{ => common}/policy.rs (95%) rename identity_credential/src/credential/{ => common}/proof.rs (100%) rename identity_credential/src/credential/{ => common}/refresh.rs (95%) rename identity_credential/src/credential/{ => common}/schema.rs (89%) rename identity_credential/src/credential/{ => common}/subject.rs (87%) create mode 100644 identity_credential/src/credential/traits.rs rename identity_credential/src/credential/{ => vc1_1}/builder.rs (99%) rename identity_credential/src/credential/{ => vc1_1}/credential.rs (87%) create mode 100644 identity_credential/src/credential/vc1_1/mod.rs rename identity_credential/src/credential/{ => vc1_1}/status.rs (94%) create mode 100644 identity_credential/src/credential/vc2_0/mod.rs diff --git a/identity_credential/src/credential/evidence.rs b/identity_credential/src/credential/common/evidence.rs similarity index 95% rename from identity_credential/src/credential/evidence.rs rename to identity_credential/src/credential/common/evidence.rs index 016feef5d1..9123051bba 100644 --- a/identity_credential/src/credential/evidence.rs +++ b/identity_credential/src/credential/common/evidence.rs @@ -77,8 +77,8 @@ mod tests { use crate::credential::Evidence; - const JSON1: &str = include_str!("../../tests/fixtures/evidence-1.json"); - const JSON2: &str = include_str!("../../tests/fixtures/evidence-2.json"); + const JSON1: &str = include_str!("../../../tests/fixtures/evidence-1.json"); + const JSON2: &str = include_str!("../../../tests/fixtures/evidence-2.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/issuer.rs b/identity_credential/src/credential/common/issuer.rs similarity index 92% rename from identity_credential/src/credential/issuer.rs rename to identity_credential/src/credential/common/issuer.rs index 23754579dd..a290465a24 100644 --- a/identity_credential/src/credential/issuer.rs +++ b/identity_credential/src/credential/common/issuer.rs @@ -55,8 +55,8 @@ mod tests { use crate::credential::Issuer; - const JSON1: &str = include_str!("../../tests/fixtures/issuer-1.json"); - const JSON2: &str = include_str!("../../tests/fixtures/issuer-2.json"); + const JSON1: &str = include_str!("../../../tests/fixtures/issuer-1.json"); + const JSON2: &str = include_str!("../../../tests/fixtures/issuer-2.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/linked_domain_service.rs b/identity_credential/src/credential/common/linked_domain_service.rs similarity index 99% rename from identity_credential/src/credential/linked_domain_service.rs rename to identity_credential/src/credential/common/linked_domain_service.rs index c6efbae255..af44f08613 100644 --- a/identity_credential/src/credential/linked_domain_service.rs +++ b/identity_credential/src/credential/common/linked_domain_service.rs @@ -148,7 +148,7 @@ impl LinkedDomainService { #[cfg(test)] mod tests { - use crate::credential::linked_domain_service::LinkedDomainService; + use crate::credential::common::linked_domain_service::LinkedDomainService; use identity_core::common::Object; use identity_core::common::OrderedSet; use identity_core::common::Url; diff --git a/identity_credential/src/credential/common/mod.rs b/identity_credential/src/credential/common/mod.rs new file mode 100644 index 0000000000..00fcfe1e17 --- /dev/null +++ b/identity_credential/src/credential/common/mod.rs @@ -0,0 +1,17 @@ +mod evidence; +mod issuer; +mod linked_domain_service; +mod policy; +mod proof; +mod refresh; +mod schema; +mod subject; + +pub use evidence::*; +pub use issuer::*; +pub use linked_domain_service::*; +pub use policy::*; +pub use proof::*; +pub use refresh::*; +pub use schema::*; +pub use subject::*; diff --git a/identity_credential/src/credential/policy.rs b/identity_credential/src/credential/common/policy.rs similarity index 95% rename from identity_credential/src/credential/policy.rs rename to identity_credential/src/credential/common/policy.rs index e8a5278f4a..7f0ef3a6fb 100644 --- a/identity_credential/src/credential/policy.rs +++ b/identity_credential/src/credential/common/policy.rs @@ -81,8 +81,8 @@ mod tests { use crate::credential::Policy; - const JSON1: &str = include_str!("../../tests/fixtures/policy-1.json"); - const JSON2: &str = include_str!("../../tests/fixtures/policy-2.json"); + const JSON1: &str = include_str!("../../../tests/fixtures/policy-1.json"); + const JSON2: &str = include_str!("../../../tests/fixtures/policy-2.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/proof.rs b/identity_credential/src/credential/common/proof.rs similarity index 100% rename from identity_credential/src/credential/proof.rs rename to identity_credential/src/credential/common/proof.rs diff --git a/identity_credential/src/credential/refresh.rs b/identity_credential/src/credential/common/refresh.rs similarity index 95% rename from identity_credential/src/credential/refresh.rs rename to identity_credential/src/credential/common/refresh.rs index 46f9a43117..2c7dfd3344 100644 --- a/identity_credential/src/credential/refresh.rs +++ b/identity_credential/src/credential/common/refresh.rs @@ -51,7 +51,7 @@ mod tests { use crate::credential::RefreshService; - const JSON: &str = include_str!("../../tests/fixtures/refresh-1.json"); + const JSON: &str = include_str!("../../../tests/fixtures/refresh-1.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/schema.rs b/identity_credential/src/credential/common/schema.rs similarity index 89% rename from identity_credential/src/credential/schema.rs rename to identity_credential/src/credential/common/schema.rs index 0841a7d084..45ea0990cf 100644 --- a/identity_credential/src/credential/schema.rs +++ b/identity_credential/src/credential/common/schema.rs @@ -51,9 +51,9 @@ mod tests { use crate::credential::Schema; - const JSON1: &str = include_str!("../../tests/fixtures/schema-1.json"); - const JSON2: &str = include_str!("../../tests/fixtures/schema-2.json"); - const JSON3: &str = include_str!("../../tests/fixtures/schema-3.json"); + const JSON1: &str = include_str!("../../../tests/fixtures/schema-1.json"); + const JSON2: &str = include_str!("../../../tests/fixtures/schema-2.json"); + const JSON3: &str = include_str!("../../../tests/fixtures/schema-3.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/subject.rs b/identity_credential/src/credential/common/subject.rs similarity index 87% rename from identity_credential/src/credential/subject.rs rename to identity_credential/src/credential/common/subject.rs index efecdad9f6..e5e892df2e 100644 --- a/identity_credential/src/credential/subject.rs +++ b/identity_credential/src/credential/common/subject.rs @@ -51,16 +51,16 @@ mod tests { use crate::credential::Subject; - const JSON1: &str = include_str!("../../tests/fixtures/subject-1.json"); - const JSON2: &str = include_str!("../../tests/fixtures/subject-2.json"); - const JSON3: &str = include_str!("../../tests/fixtures/subject-3.json"); - const JSON4: &str = include_str!("../../tests/fixtures/subject-4.json"); - const JSON5: &str = include_str!("../../tests/fixtures/subject-5.json"); - const JSON6: &str = include_str!("../../tests/fixtures/subject-6.json"); - const JSON7: &str = include_str!("../../tests/fixtures/subject-7.json"); - const JSON8: &str = include_str!("../../tests/fixtures/subject-8.json"); - const JSON9: &str = include_str!("../../tests/fixtures/subject-9.json"); - const JSON10: &str = include_str!("../../tests/fixtures/subject-10.json"); + const JSON1: &str = include_str!("../../../tests/fixtures/subject-1.json"); + const JSON2: &str = include_str!("../../../tests/fixtures/subject-2.json"); + const JSON3: &str = include_str!("../../../tests/fixtures/subject-3.json"); + const JSON4: &str = include_str!("../../../tests/fixtures/subject-4.json"); + const JSON5: &str = include_str!("../../../tests/fixtures/subject-5.json"); + const JSON6: &str = include_str!("../../../tests/fixtures/subject-6.json"); + const JSON7: &str = include_str!("../../../tests/fixtures/subject-7.json"); + const JSON8: &str = include_str!("../../../tests/fixtures/subject-8.json"); + const JSON9: &str = include_str!("../../../tests/fixtures/subject-9.json"); + const JSON10: &str = include_str!("../../../tests/fixtures/subject-10.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index efa20a3c87..1ef99d66c7 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -5,38 +5,33 @@ #![allow(clippy::module_inception)] -mod builder; -mod credential; -mod evidence; -mod issuer; +pub mod common; mod jws; mod jwt; mod jwt_serialization; -mod linked_domain_service; -mod policy; -mod proof; -mod refresh; #[cfg(feature = "revocation-bitmap")] mod revocation_bitmap_status; -mod schema; -mod status; -mod subject; +pub mod vc1_1; +pub mod vc2_0; +mod traits; + -pub use self::builder::CredentialBuilder; -pub use self::credential::Credential; -pub use self::evidence::Evidence; -pub use self::issuer::Issuer; pub use self::jws::Jws; pub use self::jwt::Jwt; -pub use self::linked_domain_service::LinkedDomainService; -pub use self::policy::Policy; -pub use self::proof::Proof; -pub use self::refresh::RefreshService; #[cfg(feature = "revocation-bitmap")] pub use self::revocation_bitmap_status::RevocationBitmapStatus; -pub use self::schema::Schema; -pub use self::status::Status; -pub use self::subject::Subject; +pub use common::Evidence; +pub use common::Issuer; +pub use common::LinkedDomainService; +pub use common::Policy; +pub use common::Proof; +pub use common::RefreshService; +pub use common::Schema; +pub use common::Subject; +pub use vc1_1::Credential; +pub use vc1_1::CredentialBuilder; +pub use vc1_1::Status; +pub use traits::*; #[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::CredentialJwtClaims; diff --git a/identity_credential/src/credential/traits.rs b/identity_credential/src/credential/traits.rs new file mode 100644 index 0000000000..933211422e --- /dev/null +++ b/identity_credential/src/credential/traits.rs @@ -0,0 +1,28 @@ +use identity_core::common::{Timestamp, Url}; + +pub trait CredentialT { + type Issuer; + type Claim; + + fn id(&self) -> &Url; + fn issuer(&self) -> &Self::Issuer; + fn claim(&self) -> &Self::Claim; + fn is_valid_at(&self, timestamp: &Timestamp) -> bool; + fn check_validity_time_frame(&self) -> bool { + self.is_valid_at(&Timestamp::now_utc()) + } +} + +pub trait VerifiableCredentialT<'c, P>: CredentialT { + fn proof(&'c self) -> P; +} + +pub trait ProofT { + fn signature(&self) -> impl AsRef<[u8]>; + fn verification_method(&self) -> Option<&impl Into>; +} + +pub trait StatusT { + type State; + fn type_(&self) -> &str; +} \ No newline at end of file diff --git a/identity_credential/src/credential/builder.rs b/identity_credential/src/credential/vc1_1/builder.rs similarity index 99% rename from identity_credential/src/credential/builder.rs rename to identity_credential/src/credential/vc1_1/builder.rs index f95771c500..7a5eef206e 100644 --- a/identity_credential/src/credential/builder.rs +++ b/identity_credential/src/credential/vc1_1/builder.rs @@ -17,7 +17,7 @@ use crate::credential::Status; use crate::credential::Subject; use crate::error::Result; -use super::Proof; +use crate::credential::common::Proof; /// A `CredentialBuilder` is used to create a customized `Credential`. #[derive(Clone, Debug)] diff --git a/identity_credential/src/credential/credential.rs b/identity_credential/src/credential/vc1_1/credential.rs similarity index 87% rename from identity_credential/src/credential/credential.rs rename to identity_credential/src/credential/vc1_1/credential.rs index decbb8b7c2..e6ca37900b 100644 --- a/identity_credential/src/credential/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -27,8 +27,8 @@ use crate::credential::Subject; use crate::error::Error; use crate::error::Result; -use super::jwt_serialization::CredentialJwtClaims; -use super::Proof; +use crate::credential::common::Proof; +use crate::credential::jwt_serialization::CredentialJwtClaims; static BASE_CONTEXT: Lazy = Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/2018/credentials/v1").unwrap())); @@ -191,18 +191,18 @@ mod tests { use crate::credential::Credential; - const JSON1: &str = include_str!("../../tests/fixtures/credential-1.json"); - const JSON2: &str = include_str!("../../tests/fixtures/credential-2.json"); - const JSON3: &str = include_str!("../../tests/fixtures/credential-3.json"); - const JSON4: &str = include_str!("../../tests/fixtures/credential-4.json"); - const JSON5: &str = include_str!("../../tests/fixtures/credential-5.json"); - const JSON6: &str = include_str!("../../tests/fixtures/credential-6.json"); - const JSON7: &str = include_str!("../../tests/fixtures/credential-7.json"); - const JSON8: &str = include_str!("../../tests/fixtures/credential-8.json"); - const JSON9: &str = include_str!("../../tests/fixtures/credential-9.json"); - const JSON10: &str = include_str!("../../tests/fixtures/credential-10.json"); - const JSON11: &str = include_str!("../../tests/fixtures/credential-11.json"); - const JSON12: &str = include_str!("../../tests/fixtures/credential-12.json"); + const JSON1: &str = include_str!("../../../tests/fixtures/credential-1.json"); + const JSON2: &str = include_str!("../../../tests/fixtures/credential-2.json"); + const JSON3: &str = include_str!("../../../tests/fixtures/credential-3.json"); + const JSON4: &str = include_str!("../../../tests/fixtures/credential-4.json"); + const JSON5: &str = include_str!("../../../tests/fixtures/credential-5.json"); + const JSON6: &str = include_str!("../../../tests/fixtures/credential-6.json"); + const JSON7: &str = include_str!("../../../tests/fixtures/credential-7.json"); + const JSON8: &str = include_str!("../../../tests/fixtures/credential-8.json"); + const JSON9: &str = include_str!("../../../tests/fixtures/credential-9.json"); + const JSON10: &str = include_str!("../../../tests/fixtures/credential-10.json"); + const JSON11: &str = include_str!("../../../tests/fixtures/credential-11.json"); + const JSON12: &str = include_str!("../../../tests/fixtures/credential-12.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/vc1_1/mod.rs b/identity_credential/src/credential/vc1_1/mod.rs new file mode 100644 index 0000000000..f01d314273 --- /dev/null +++ b/identity_credential/src/credential/vc1_1/mod.rs @@ -0,0 +1,7 @@ +mod builder; +mod credential; +mod status; + +pub use builder::*; +pub use credential::Credential; +pub use status::*; diff --git a/identity_credential/src/credential/status.rs b/identity_credential/src/credential/vc1_1/status.rs similarity index 94% rename from identity_credential/src/credential/status.rs rename to identity_credential/src/credential/vc1_1/status.rs index bc5d0d3f8d..63844568d2 100644 --- a/identity_credential/src/credential/status.rs +++ b/identity_credential/src/credential/vc1_1/status.rs @@ -42,7 +42,7 @@ mod tests { use super::*; - const JSON: &str = include_str!("../../tests/fixtures/status-1.json"); + const JSON: &str = include_str!("../../../tests/fixtures/status-1.json"); #[test] fn test_from_json() { diff --git a/identity_credential/src/credential/vc2_0/mod.rs b/identity_credential/src/credential/vc2_0/mod.rs new file mode 100644 index 0000000000..e69de29bb2 From 944e57c7ef03552c2ad85d36be344376b2d41f23 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 22 Feb 2024 17:04:44 +0100 Subject: [PATCH 02/10] merge main --- CHANGELOG.md | 10 +++++++++- README.md | 4 ++-- examples/Cargo.toml | 2 +- identity_core/Cargo.toml | 2 +- identity_credential/Cargo.toml | 12 ++++++------ identity_did/Cargo.toml | 4 ++-- identity_document/Cargo.toml | 8 ++++---- identity_eddsa_verifier/Cargo.toml | 4 ++-- identity_iota/Cargo.toml | 18 +++++++++--------- identity_iota/README.md | 4 ++-- identity_iota_core/Cargo.toml | 12 ++++++------ identity_jose/Cargo.toml | 4 ++-- identity_resolver/Cargo.toml | 12 ++++++------ identity_storage/Cargo.toml | 18 +++++++++--------- identity_stronghold/Cargo.toml | 8 ++++---- identity_verification/Cargo.toml | 8 ++++---- 16 files changed, 69 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7c3937447..94cc55d62b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog -## [v1.1.0](https://github.com/iotaledger/identity.rs/tree/v1.1.0) (2024-02-06) +## [v1.1.1](https://github.com/iotaledger/identity.rs/tree/v1.1.1) (2024-02-19) + +[Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.1.0...v1.1.1) + +### Patch + +- Fix compilation error caused by the roaring crate [\#1306](https://github.com/iotaledger/identity.rs/pull/1306) + +## [v1.1.0](https://github.com/iotaledger/identity.rs/tree/v1.1.0) (2024-02-07) [Full Changelog](https://github.com/iotaledger/identity.rs/compare/v1.0.0...v1.1.0) diff --git a/README.md b/README.md index 18a90879eb..658479444f 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ If you want to include IOTA Identity in your project, simply add it as a depende ```toml [dependencies] -identity_iota = { version = "1.1.0" } +identity_iota = { version = "1.1.1" } ``` To try out the [examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples), you can also do this: @@ -85,7 +85,7 @@ version = "1.0.0" edition = "2021" [dependencies] -identity_iota = {version = "1.1.0", features = ["memstore"]} +identity_iota = { version = "1.1.1", features = ["memstore"] } iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0.62" diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 242dacea88..a9337d6a33 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "examples" -version = "1.1.0" +version = "1.1.1" authors = ["IOTA Stiftung"] edition = "2021" publish = false diff --git a/identity_core/Cargo.toml b/identity_core/Cargo.toml index 17199e528f..120d6dc9be 100644 --- a/identity_core/Cargo.toml +++ b/identity_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_core" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 491158c1f1..876d5577d5 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_credential" -version = "1.1.0" +version = "1.1.1" authors = ["IOTA Stiftung"] edition = "2021" homepage.workspace = true @@ -14,15 +14,15 @@ description = "An implementation of the Verifiable Credentials standard." [dependencies] flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } futures = { version = "0.3", default-features = false, optional = true } -identity_core = { version = "=1.1.0", path = "../identity_core", default-features = false } -identity_did = { version = "=1.1.0", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.0", path = "../identity_document", default-features = false } -identity_verification = { version = "=1.1.0", path = "../identity_verification", default-features = false } +identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } +identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } +identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } reqwest = { version = "0.11", default-features = false, features = ["default-tls", "json", "stream"], optional = true } -roaring = { version = "0.10", default-features = false, optional = true } +roaring = { version = "0.10", default-features = false, features = ["std"], optional = true } sd-jwt-payload = { version = "0.2.0", default-features = false, features = ["sha"], optional = true } serde.workspace = true serde-aux = { version = "4.3.1", default-features = false, optional = true } diff --git a/identity_did/Cargo.toml b/identity_did/Cargo.toml index 2668c55b08..bed6f9012a 100644 --- a/identity_did/Cargo.toml +++ b/identity_did/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_did" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition = "2021" homepage.workspace = true @@ -13,7 +13,7 @@ description = "Agnostic implementation of the Decentralized Identifiers (DID) st [dependencies] did_url = { version = "0.1", default-features = false, features = ["std", "serde"] } form_urlencoded = { version = "1.2.0", default-features = false, features = ["alloc"] } -identity_core = { version = "=1.1.0", path = "../identity_core" } +identity_core = { version = "=1.1.1", path = "../identity_core" } serde.workspace = true strum.workspace = true thiserror.workspace = true diff --git a/identity_document/Cargo.toml b/identity_document/Cargo.toml index 2ab23ef7d9..bebbd8f070 100644 --- a/identity_document/Cargo.toml +++ b/identity_document/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_document" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -13,9 +13,9 @@ description = "Method-agnostic implementation of the Decentralized Identifiers ( [dependencies] did_url = { version = "0.1", default-features = false, features = ["std", "serde"] } -identity_core = { version = "=1.1.0", path = "../identity_core" } -identity_did = { version = "=1.1.0", path = "../identity_did" } -identity_verification = { version = "=1.1.0", path = "../identity_verification", default-features = false } +identity_core = { version = "=1.1.1", path = "../identity_core" } +identity_did = { version = "=1.1.1", path = "../identity_did" } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } serde.workspace = true strum.workspace = true diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml index 516b544af6..257fa5d5a4 100644 --- a/identity_eddsa_verifier/Cargo.toml +++ b/identity_eddsa_verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_eddsa_verifier" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,7 +12,7 @@ rust-version.workspace = true description = "JWS EdDSA signature verification for IOTA Identity" [dependencies] -identity_jose = { version = "=1.1.0", path = "../identity_jose", default-features = false } +identity_jose = { version = "=1.1.1", path = "../identity_jose", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std"] } [features] diff --git a/identity_iota/Cargo.toml b/identity_iota/Cargo.toml index fdf4edc9a3..bd5aa25125 100644 --- a/identity_iota/Cargo.toml +++ b/identity_iota/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_iota" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,14 +12,14 @@ rust-version.workspace = true description = "Framework for Self-Sovereign Identity with IOTA DID." [dependencies] -identity_core = { version = "=1.1.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0", path = "../identity_credential", features = ["validator"], default-features = false } -identity_did = { version = "=1.1.0", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.0", path = "../identity_document", default-features = false } -identity_iota_core = { version = "=1.1.0", path = "../identity_iota_core", default-features = false } -identity_resolver = { version = "=1.1.0", path = "../identity_resolver", default-features = false, optional = true } -identity_storage = { version = "=1.1.0", path = "../identity_storage", default-features = false, features = ["iota-document"] } -identity_verification = { version = "=1.1.0", path = "../identity_verification", default-features = false } +identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["validator"], default-features = false } +identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } +identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } +identity_iota_core = { version = "=1.1.1", path = "../identity_iota_core", default-features = false } +identity_resolver = { version = "=1.1.1", path = "../identity_resolver", default-features = false, optional = true } +identity_storage = { version = "=1.1.1", path = "../identity_storage", default-features = false, features = ["iota-document"] } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } [dev-dependencies] anyhow = "1.0.64" diff --git a/identity_iota/README.md b/identity_iota/README.md index 0881e9809b..e210d6e10d 100644 --- a/identity_iota/README.md +++ b/identity_iota/README.md @@ -51,7 +51,7 @@ If you want to include IOTA Identity in your project, simply add it as a depende ```toml [dependencies] -identity_iota = { version = "1.1.0" } +identity_iota = { version = "1.1.1" } ``` To try out the [examples](https://github.com/iotaledger/identity.rs/blob/HEAD/examples), you can also do this: @@ -74,7 +74,7 @@ version = "1.0.0" edition = "2021" [dependencies] -identity_iota = {version = "1.1.0", features = ["memstore"]} +identity_iota = {version = "1.1.1", features = ["memstore"]} iota-sdk = { version = "1.0.2", default-features = true, features = ["tls", "client", "stronghold"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0.62" diff --git a/identity_iota_core/Cargo.toml b/identity_iota_core/Cargo.toml index 6d09722b5e..ea8d43a845 100644 --- a/identity_iota_core/Cargo.toml +++ b/identity_iota_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_iota_core" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -14,11 +14,11 @@ description = "An IOTA Ledger integration for the IOTA DID Method." [dependencies] async-trait = { version = "0.1.56", default-features = false, optional = true } futures = { version = "0.3", default-features = false } -identity_core = { version = "=1.1.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0", path = "../identity_credential", default-features = false, features = ["validator"] } -identity_did = { version = "=1.1.0", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.0", path = "../identity_document", default-features = false } -identity_verification = { version = "=1.1.0", path = "../identity_verification", default-features = false } +identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["validator"] } +identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } +identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } iota-sdk = { version = "1.0.2", default-features = false, features = ["serde", "std"], optional = true } num-derive = { version = "0.4", default-features = false } num-traits = { version = "0.2", default-features = false, features = ["std"] } diff --git a/identity_jose/Cargo.toml b/identity_jose/Cargo.toml index dd961fbe08..aa2a53f13a 100644 --- a/identity_jose/Cargo.toml +++ b/identity_jose/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_jose" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -12,7 +12,7 @@ rust-version.workspace = true description = "A library for JOSE (JSON Object Signing and Encryption)" [dependencies] -identity_core = { version = "=1.1.0", path = "../identity_core", default-features = false } +identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std", "sha"] } serde.workspace = true serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 20ec82cd1b..60bda28350 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_resolver" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -15,16 +15,16 @@ description = "DID Resolution utilities for the identity.rs library." # This is currently necessary for the ResolutionHandler trait. This can be made an optional dependency if alternative ways of attaching handlers are introduced. async-trait = { version = "0.1", default-features = false } futures = { version = "0.3" } -identity_core = { version = "=1.1.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0", path = "../identity_credential", default-features = false, features = ["validator"] } -identity_did = { version = "=1.1.0", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.0", path = "../identity_document", default-features = false } +identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["validator"] } +identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } +identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } strum.workspace = true thiserror = { version = "1.0", default-features = false } [dependencies.identity_iota_core] -version = "=1.1.0" +version = "=1.1.1" path = "../identity_iota_core" default-features = false features = ["send-sync-client-ext", "iota-client"] diff --git a/identity_storage/Cargo.toml b/identity_storage/Cargo.toml index b7b0ce0b26..75086ccab9 100644 --- a/identity_storage/Cargo.toml +++ b/identity_storage/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_storage" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -14,12 +14,12 @@ description = "Abstractions over storage for cryptographic keys used in DID Docu [dependencies] async-trait = { version = "0.1.64", default-features = false } futures = { version = "0.3.27", default-features = false, features = ["async-await"] } -identity_core = { version = "=1.1.0", path = "../identity_core", default-features = false } -identity_credential = { version = "=1.1.0", path = "../identity_credential", default-features = false, features = ["credential", "presentation"] } -identity_did = { version = "=1.1.0", path = "../identity_did", default-features = false } -identity_document = { version = "=1.1.0", path = "../identity_document", default-features = false } -identity_iota_core = { version = "=1.1.0", path = "../identity_iota_core", default-features = false, optional = true } -identity_verification = { version = "=1.1.0", path = "../identity_verification", default_features = false } +identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } +identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["credential", "presentation"] } +identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } +identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } +identity_iota_core = { version = "=1.1.1", path = "../identity_iota_core", default-features = false, optional = true } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"], optional = true } rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } seahash = { version = "4.1.0", default_features = false } @@ -29,8 +29,8 @@ thiserror.workspace = true tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync"], optional = true } [dev-dependencies] -identity_credential = { version = "=1.1.0", path = "../identity_credential", features = ["revocation-bitmap"] } -identity_eddsa_verifier = { version = "=1.1.0", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } +identity_credential = { version = "=1.1.1", path = "../identity_credential", features = ["revocation-bitmap"] } +identity_eddsa_verifier = { version = "=1.1.1", path = "../identity_eddsa_verifier", default-features = false, features = ["ed25519"] } once_cell = { version = "1.18", default-features = false } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } diff --git a/identity_stronghold/Cargo.toml b/identity_stronghold/Cargo.toml index de0197a3ef..e45a6d4eb7 100644 --- a/identity_stronghold/Cargo.toml +++ b/identity_stronghold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_stronghold" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -13,8 +13,8 @@ description = "Secure JWK storage with Stronghold for IOTA Identity" [dependencies] async-trait = { version = "0.1.64", default-features = false } -identity_storage = { version = "=1.1.0", path = "../identity_storage", default_features = false } -identity_verification = { version = "=1.1.0", path = "../identity_verification", default_features = false } +identity_storage = { version = "=1.1.1", path = "../identity_storage", default_features = false } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default_features = false } iota-crypto = { version = "0.23", default-features = false, features = ["ed25519"] } iota-sdk = { version = "1.0.2", default-features = false, features = ["client", "stronghold"] } iota_stronghold = { version = "2.0", default-features = false } @@ -23,7 +23,7 @@ tokio = { version = "1.29.0", default-features = false, features = ["macros", "s zeroize = { version = "1.6.0", default_features = false } [dev-dependencies] -identity_did = { version = "=1.1.0", path = "../identity_did", default_features = false } +identity_did = { version = "=1.1.1", path = "../identity_did", default_features = false } tokio = { version = "1.29.0", default-features = false, features = ["macros", "sync", "rt"] } [features] diff --git a/identity_verification/Cargo.toml b/identity_verification/Cargo.toml index 7d965f60fc..1b6bb11d77 100644 --- a/identity_verification/Cargo.toml +++ b/identity_verification/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "identity_verification" -version = "1.1.0" +version = "1.1.1" authors.workspace = true edition.workspace = true homepage.workspace = true @@ -9,9 +9,9 @@ rust-version.workspace = true description = "Verification data types and functionality for identity.rs" [dependencies] -identity_core = { version = "=1.1.0", path = "./../identity_core", default-features = false } -identity_did = { version = "=1.1.0", path = "./../identity_did", default-features = false } -identity_jose = { version = "=1.1.0", path = "./../identity_jose", default-features = false } +identity_core = { version = "=1.1.1", path = "./../identity_core", default-features = false } +identity_did = { version = "=1.1.1", path = "./../identity_did", default-features = false } +identity_jose = { version = "=1.1.1", path = "./../identity_jose", default-features = false } serde.workspace = true strum.workspace = true thiserror.workspace = true From 61fb6627f539542448e8e8c7b3bc919e6f1b5c17 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 27 Feb 2024 17:44:19 +0100 Subject: [PATCH 03/10] JwtCredential, traits, validation --- Cargo.toml | 3 +- examples/0_basic/5_create_vc.rs | 19 +- examples/Cargo.toml | 1 + identity_credential/Cargo.toml | 2 + identity_credential/src/credential/jwt.rs | 249 +++++++++++++++++- identity_credential/src/credential/mod.rs | 2 +- identity_credential/src/credential/traits.rs | 41 +-- .../src/credential/vc1_1/credential.rs | 31 +++ .../domain_linkage_validator.rs | 2 +- .../src/presentation/presentation_builder.rs | 4 +- identity_credential/src/revocation/mod.rs | 2 + identity_credential/src/revocation/traits.rs | 22 ++ identity_jose/src/jws/decoder.rs | 15 +- .../src/storage/jwk_document_ext.rs | 4 +- identity_validator/Cargo.toml | 21 ++ identity_validator/src/lib.rs | 3 + identity_validator/src/validator.rs | 145 ++++++++++ 17 files changed, 512 insertions(+), 54 deletions(-) create mode 100644 identity_credential/src/revocation/traits.rs create mode 100644 identity_validator/Cargo.toml create mode 100644 identity_validator/src/lib.rs create mode 100644 identity_validator/src/validator.rs diff --git a/Cargo.toml b/Cargo.toml index d4726e0d4c..06429dc0c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ members = [ "identity_stronghold", "identity_jose", "identity_eddsa_verifier", - "examples", + "identity_validator", + "examples", ] exclude = ["bindings/wasm"] diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index 3a14e262e2..b789cf6d19 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -16,12 +16,15 @@ use identity_iota::core::Object; use identity_iota::credential::DecodedJwtCredential; use identity_iota::credential::Jwt; +use identity_iota::credential::JwtCredential; use identity_iota::credential::JwtCredentialValidationOptions; use identity_iota::credential::JwtCredentialValidator; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; use identity_iota::storage::KeyIdMemstore; +use identity_validator::IotaCredentialValidator; +use identity_validator::ValidatorT; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; use iota_sdk::client::secret::SecretManager; use iota_sdk::client::Client; @@ -96,25 +99,19 @@ async fn main() -> anyhow::Result<()> { None, ) .await?; - + + let credential_jwt = JwtCredential::parse(credential_jwt)?; // Before sending this credential to the holder the issuer wants to validate that some properties // of the credential satisfy their expectations. // Validate the credential's signature using the issuer's DID Document, the credential's semantic structure, // that the issuance date is not in the future and that the expiration date is not in the past: - let decoded_credential: DecodedJwtCredential = - JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()) - .validate::<_, Object>( - &credential_jwt, - &issuer_document, - &JwtCredentialValidationOptions::default(), - FailFast::FirstError, - ) - .unwrap(); + let iota_validator = IotaCredentialValidator::new(client.clone(), EdDSAJwsVerifier::default()); + iota_validator.validate(&credential_jwt).await?; println!("VC successfully validated"); - println!("Credential JSON > {:#}", decoded_credential.credential); + //println!("Credential JSON > {:#}", decoded_credential.credential); Ok(()) } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index a9337d6a33..776ec341ee 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,6 +10,7 @@ anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] } identity_stronghold = { path = "../identity_stronghold", default-features = false } +identity_validator = { path = "../identity_validator" } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } primitive-types = "0.12.1" rand = "0.8.5" diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 876d5577d5..7c116e647c 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -18,6 +18,7 @@ identity_core = { version = "=1.1.1", path = "../identity_core", default-feature identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } +identity_jose = { version = "=1.1.1", path = "../identity_jose" } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } @@ -31,6 +32,7 @@ serde_repr = { version = "0.1", default-features = false, optional = true } strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } +serde_with = "3.6.1" [dev-dependencies] anyhow = "1.0.62" diff --git a/identity_credential/src/credential/jwt.rs b/identity_credential/src/credential/jwt.rs index c06d2207df..9f9843707e 100644 --- a/identity_credential/src/credential/jwt.rs +++ b/identity_credential/src/credential/jwt.rs @@ -1,33 +1,254 @@ // Copyright 2020-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +// TODO: +// Jwt should be decoded using the facilities we already have in the library (identity_jose::jws::decoder). +// Decoder gives us all we need. We need to unpack it into a JWS (alg, sig_input, sig_challenge) and claims. +// The claims will be parsed into a JwtCredentialClaims. + +use std::fmt::Display; +use std::ops::Deref; +use std::str::FromStr; + +use identity_core::common::Object; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use identity_jose::error::Error as JoseError; +use identity_jose::jws::DecodedHeaders; +use identity_jose::jws::Decoder as JwsDecoder; use serde::Deserialize; use serde::Serialize; +use serde_with::DeserializeFromStr; +use serde_with::SerializeDisplay; +use thiserror::Error; -/// A wrapper around a JSON Web Token (JWK). -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -pub struct Jwt(String); +use super::CredentialT; +use super::Issuer; +use super::ProofT; +use super::VerifiableCredentialT; -impl Jwt { - /// Creates a new `Jwt` from the given string. - pub fn new(jwt_string: String) -> Self { - Self(jwt_string) +#[derive(Error, Debug)] +pub enum JwtError { + #[error(transparent)] + DecodingError(#[from] JoseError), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodedJws { + headers: DecodedHeaders, + signing_input: Box<[u8]>, + raw_claims: Box<[u8]>, + signature: Box<[u8]>, +} + +impl<'a> ProofT for &'a DecodedJws { + fn algorithm(&self) -> &str { + DecodedJws::algorithm(self) + } + fn signature(&self) -> &[u8] { + DecodedJws::signature(self) + } + fn signing_input(&self) -> &[u8] { + DecodedJws::signing_input(self) } + fn verification_method(&self) -> Option { + DecodedJws::verification_method(self) + } +} + +impl ProofT for DecodedJws { + fn algorithm(&self) -> &str { + self + .headers + .protected_header() + .and_then(|header| header.alg()) + .unwrap_or(identity_jose::jws::JwsAlgorithm::NONE) + .name() + } + fn signature(&self) -> &[u8] { + self.signature.as_ref() + } + fn signing_input(&self) -> &[u8] { + self.signing_input.as_ref() + } + fn verification_method(&self) -> Option { + self + .headers + .protected_header() + .and_then(|header| header.kid()) + .and_then(|kid| Url::parse(kid).ok()) + } +} - /// Returns a reference of the JWT string. +#[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)] +pub struct Jwt { + inner: String, + decoded_jws: DecodedJws, +} + +impl FromStr for Jwt { + type Err = JwtError; + fn from_str(s: &str) -> Result { + Self::parse(s.to_owned()) + } +} + +impl Display for Jwt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self.inner) + } +} + +impl Jwt { + pub fn parse(token: String) -> Result { + let decoder = JwsDecoder::new(); + let decoded_jws = decoder.decode_compact_serialization(token.as_bytes(), None)?; + let (headers, signing_input, signature, raw_claims) = decoded_jws.into_parts(); + let decoded_jws = DecodedJws { + headers, + signing_input, + raw_claims, + signature, + }; + + Ok(Self { + inner: token, + decoded_jws, + }) + } pub fn as_str(&self) -> &str { + self.inner.as_str() + } +} + +#[derive(Debug, Error)] +pub enum JwtCredentialError { + #[error(transparent)] + DeserializationError(#[from] serde_json::Error), +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[repr(transparent)] +#[serde(try_from = "i64", into = "i64")] +struct UnixTimestampWrapper(Timestamp); + +impl Deref for UnixTimestampWrapper { + type Target = Timestamp; + fn deref(&self) -> &Self::Target { &self.0 } } -impl From for Jwt { - fn from(jwt: String) -> Self { - Self::new(jwt) +impl TryFrom for UnixTimestampWrapper { + type Error = identity_core::Error; + fn try_from(value: i64) -> Result { + Timestamp::from_unix(value).map(Self) + } +} + +impl From for i64 { + fn from(value: UnixTimestampWrapper) -> Self { + value.0.to_unix() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +enum IssuanceDate { + Iat(UnixTimestampWrapper), + Nbf(UnixTimestampWrapper), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct JwtCredentialClaims { + /// Represents the expirationDate encoded as a UNIX timestamp. + pub exp: Option, + /// Represents the issuer. + pub iss: Issuer, + /// Represents the issuanceDate encoded as a UNIX timestamp. + issuance_date: IssuanceDate, + /// Represents the id property of the credential. + pub jti: Url, + /// Represents the subject's id. + pub sub: Option, + pub vc: Object, + pub custom: Option, +} + +impl JwtCredentialClaims { + pub fn issuance_date(&self) -> Timestamp { + match self.issuance_date { + IssuanceDate::Iat(t) => *t, + IssuanceDate::Nbf(t) => *t, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "Jwt", into = "Jwt")] +pub struct JwtCredential { + inner: String, + parsed_claims: JwtCredentialClaims, + decoded_jws: DecodedJws, +} + +impl TryFrom for JwtCredential { + type Error = JwtCredentialError; + + fn try_from(jwt: Jwt) -> Result { + let Jwt { inner, decoded_jws } = jwt; + let parsed_claims = serde_json::from_slice(&decoded_jws.raw_claims)?; + Ok(Self { + inner, + decoded_jws, + parsed_claims, + }) } } -impl From for String { - fn from(jwt: Jwt) -> Self { - jwt.0 +impl From for Jwt { + fn from(value: JwtCredential) -> Self { + Jwt { + inner: value.inner, + decoded_jws: value.decoded_jws, + } + } +} + +impl CredentialT for JwtCredential { + type Claim = JwtCredentialClaims; + type Issuer = Issuer; + + fn id(&self) -> &Url { + &self.parsed_claims.jti + } + fn issuer(&self) -> &Self::Issuer { + &self.parsed_claims.iss + } + fn claim(&self) -> &Self::Claim { + &self.parsed_claims + } + fn valid_from(&self) -> Timestamp { + self.parsed_claims.issuance_date() + } + fn valid_until(&self) -> Option { + self.parsed_claims.exp.as_deref().copied() + } +} + +impl<'c> VerifiableCredentialT<'c> for JwtCredential { + type Proof = &'c DecodedJws; + + fn proof(&'c self) -> Self::Proof { + &self.decoded_jws + } +} + +impl JwtCredential { + pub fn parse(jwt: Jwt) -> Result { + Self::try_from(jwt) + } + pub fn try_into_credential>(self) -> Result { + C::try_from(self.parsed_claims) } } diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 1ef99d66c7..f2fefce28f 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -17,7 +17,7 @@ mod traits; pub use self::jws::Jws; -pub use self::jwt::Jwt; +pub use jwt::*; #[cfg(feature = "revocation-bitmap")] pub use self::revocation_bitmap_status::RevocationBitmapStatus; pub use common::Evidence; diff --git a/identity_credential/src/credential/traits.rs b/identity_credential/src/credential/traits.rs index 933211422e..6da5d64965 100644 --- a/identity_credential/src/credential/traits.rs +++ b/identity_credential/src/credential/traits.rs @@ -1,28 +1,31 @@ use identity_core::common::{Timestamp, Url}; pub trait CredentialT { - type Issuer; - type Claim; - - fn id(&self) -> &Url; - fn issuer(&self) -> &Self::Issuer; - fn claim(&self) -> &Self::Claim; - fn is_valid_at(&self, timestamp: &Timestamp) -> bool; - fn check_validity_time_frame(&self) -> bool { - self.is_valid_at(&Timestamp::now_utc()) - } + type Issuer; + type Claim; + + fn id(&self) -> &Url; + fn issuer(&self) -> &Self::Issuer; + fn claim(&self) -> &Self::Claim; + fn valid_from(&self) -> Timestamp; + fn valid_until(&self) -> Option; + fn is_valid_at(&self, timestamp: &Timestamp) -> bool { + self.valid_from() <= *timestamp && self.valid_until().map(|t| t > *timestamp).unwrap_or(true) + } + fn check_validity_time_frame(&self) -> bool { + self.is_valid_at(&Timestamp::now_utc()) + } } -pub trait VerifiableCredentialT<'c, P>: CredentialT { - fn proof(&'c self) -> P; +pub trait VerifiableCredentialT<'c>: CredentialT { + type Proof; + + fn proof(&'c self) -> Self::Proof; } pub trait ProofT { - fn signature(&self) -> impl AsRef<[u8]>; - fn verification_method(&self) -> Option<&impl Into>; + fn algorithm(&self) -> &str; + fn signature(&self) -> &[u8]; + fn signing_input(&self) -> &[u8]; + fn verification_method(&self) -> Option; } - -pub trait StatusT { - type State; - fn type_(&self) -> &str; -} \ No newline at end of file diff --git a/identity_credential/src/credential/vc1_1/credential.rs b/identity_credential/src/credential/vc1_1/credential.rs index e6ca37900b..1963931923 100644 --- a/identity_credential/src/credential/vc1_1/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -17,6 +17,7 @@ use identity_core::common::Url; use identity_core::convert::FmtJson; use crate::credential::CredentialBuilder; +use crate::credential::CredentialT; use crate::credential::Evidence; use crate::credential::Issuer; use crate::credential::Policy; @@ -24,6 +25,7 @@ use crate::credential::RefreshService; use crate::credential::Schema; use crate::credential::Status; use crate::credential::Subject; +use crate::credential::VerifiableCredentialT; use crate::error::Error; use crate::error::Result; @@ -83,6 +85,35 @@ pub struct Credential { pub proof: Option, } +impl CredentialT for Credential { + type Claim = OneOrMany; + type Issuer = Issuer; + + fn id(&self) -> &Url { + self.id.as_ref().unwrap() + } + fn claim(&self) -> &Self::Claim { + &self.credential_subject + } + fn issuer(&self) -> &Self::Issuer { + &self.issuer + } + fn valid_from(&self) -> Timestamp { + self.issuance_date + } + fn valid_until(&self) -> Option { + self.expiration_date + } +} + +impl<'c> VerifiableCredentialT<'c> for Credential { + type Proof = Option<&'c Proof>; + + fn proof(&'c self) -> Self::Proof { + self.proof.as_ref() + } +} + impl Credential { /// Returns the base JSON-LD context. pub fn base_context() -> &'static Context { diff --git a/identity_credential/src/domain_linkage/domain_linkage_validator.rs b/identity_credential/src/domain_linkage/domain_linkage_validator.rs index 24969c1c65..aee4067b23 100644 --- a/identity_credential/src/domain_linkage/domain_linkage_validator.rs +++ b/identity_credential/src/domain_linkage/domain_linkage_validator.rs @@ -545,7 +545,7 @@ mod tests { secret_key: &SecretKey, ) -> Jwt { let payload: String = credential.serialize_jwt(None).unwrap(); - Jwt::new(sign_bytes(document, fragment, payload.as_ref(), secret_key).into()) + Jwt::parse(sign_bytes(document, fragment, payload.as_ref(), secret_key).into()).unwrap() } fn sign_bytes(document: &CoreDocument, fragment: &str, payload: &[u8], secret_key: &SecretKey) -> Jws { diff --git a/identity_credential/src/presentation/presentation_builder.rs b/identity_credential/src/presentation/presentation_builder.rs index 3c9e2ac9d4..af7dc4d1c1 100644 --- a/identity_credential/src/presentation/presentation_builder.rs +++ b/identity_credential/src/presentation/presentation_builder.rs @@ -156,9 +156,9 @@ mod tests { .build() .unwrap(); - let credential_jwt = Jwt::new(credential.serialize_jwt(None).unwrap()); + let credential_jwt = credential.serialize_jwt(None).unwrap(); - let presentation: Presentation = PresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new()) + let presentation: Presentation = PresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new()) .type_("ExamplePresentation") .credential(credential_jwt) .build() diff --git a/identity_credential/src/revocation/mod.rs b/identity_credential/src/revocation/mod.rs index 6732ff4194..1a6a908c71 100644 --- a/identity_credential/src/revocation/mod.rs +++ b/identity_credential/src/revocation/mod.rs @@ -5,6 +5,7 @@ //! framework. mod error; +mod traits; mod revocation_bitmap_2022; #[cfg(feature = "status-list-2021")] pub mod status_list_2021; @@ -12,3 +13,4 @@ pub mod status_list_2021; pub use self::error::RevocationError; pub use self::error::RevocationResult; pub use revocation_bitmap_2022::*; +pub use traits::*; diff --git a/identity_credential/src/revocation/traits.rs b/identity_credential/src/revocation/traits.rs new file mode 100644 index 0000000000..18ef6924ae --- /dev/null +++ b/identity_credential/src/revocation/traits.rs @@ -0,0 +1,22 @@ +use crate::credential::CredentialT; + +pub trait StatusCredentialT: CredentialT { + type Status; + + fn status(&self) -> Option<&Self::Status>; + fn set_status(&mut self, status: Option); +} + +pub trait StatusT { + type State; + + fn type_(&self) -> &str; +} + +pub trait StatusResolverT { + type Error; + + async fn state<'c, S1>(&self, status: &'c S1) -> Result + where + S: TryFrom<&'c S1>; +} \ No newline at end of file diff --git a/identity_jose/src/jws/decoder.rs b/identity_jose/src/jws/decoder.rs index 6b93488acf..0884241069 100644 --- a/identity_jose/src/jws/decoder.rs +++ b/identity_jose/src/jws/decoder.rs @@ -33,7 +33,8 @@ pub struct DecodedJws<'a> { pub claims: Cow<'a, [u8]>, } -enum DecodedHeaders { +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DecodedHeaders { Protected(JwsHeader), Unprotected(JwsHeader), Both { @@ -56,7 +57,7 @@ impl DecodedHeaders { } } - fn protected_header(&self) -> Option<&JwsHeader> { + pub fn protected_header(&self) -> Option<&JwsHeader> { match self { DecodedHeaders::Protected(ref header) => Some(header), DecodedHeaders::Both { ref protected, .. } => Some(protected), @@ -64,7 +65,7 @@ impl DecodedHeaders { } } - fn unprotected_header(&self) -> Option<&JwsHeader> { + pub fn unprotected_header(&self) -> Option<&JwsHeader> { match self { DecodedHeaders::Unprotected(ref header) => Some(header), DecodedHeaders::Both { ref unprotected, .. } => Some(unprotected.as_ref()), @@ -83,6 +84,14 @@ pub struct JwsValidationItem<'a> { claims: Cow<'a, [u8]>, } impl<'a> JwsValidationItem<'a> { + /// Returns all fields, consuming the structure + pub fn into_parts(self) -> (DecodedHeaders, Box<[u8]>, Box<[u8]>, Box<[u8]>) { + let claims = match self.claims { + Cow::Borrowed(c) => c.to_owned().into_boxed_slice(), + Cow::Owned(c) => c.into_boxed_slice(), + }; + (self.headers, self.signing_input, self.decoded_signature, claims) + } /// Returns the decoded protected header if it exists. pub fn protected_header(&self) -> Option<&JwsHeader> { self.headers.protected_header() diff --git a/identity_storage/src/storage/jwk_document_ext.rs b/identity_storage/src/storage/jwk_document_ext.rs index 8b412a285a..649e3482e5 100644 --- a/identity_storage/src/storage/jwk_document_ext.rs +++ b/identity_storage/src/storage/jwk_document_ext.rs @@ -463,7 +463,7 @@ impl JwkDocumentExt for CoreDocument { self .create_jws(storage, fragment, payload.as_bytes(), options) .await - .map(|jws| Jwt::new(jws.into())) + .map(|jws| Jwt::parse(jws.into()).unwrap()) } async fn create_presentation_jwt( @@ -498,7 +498,7 @@ impl JwkDocumentExt for CoreDocument { self .create_jws(storage, fragment, payload.as_bytes(), jws_options) .await - .map(|jws| Jwt::new(jws.into())) + .map(|jws| Jwt::parse(jws.into()).unwrap()) } } diff --git a/identity_validator/Cargo.toml b/identity_validator/Cargo.toml new file mode 100644 index 0000000000..a30e60aab9 --- /dev/null +++ b/identity_validator/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "identity_validator" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +identity_did = { path = "../identity_did" } +identity_credential = { path = "../identity_credential" } +identity_iota_core = { path = "../identity_iota_core", features = ["iota-client"] } +identity_jose = { path = "../identity_jose" } +identity_verification = { path = "../identity_verification" } +identity_eddsa_verifier = { path = "../identity_eddsa_verifier", features = ["ed25519"] } +iota-sdk = { version = "1.1.4", features = ["client"] } +thiserror.workspace = true \ No newline at end of file diff --git a/identity_validator/src/lib.rs b/identity_validator/src/lib.rs new file mode 100644 index 0000000000..4768ccdaa4 --- /dev/null +++ b/identity_validator/src/lib.rs @@ -0,0 +1,3 @@ +mod validator; + +pub use validator::*; diff --git a/identity_validator/src/validator.rs b/identity_validator/src/validator.rs new file mode 100644 index 0000000000..a7c0864499 --- /dev/null +++ b/identity_validator/src/validator.rs @@ -0,0 +1,145 @@ +use identity_credential::{ + credential::{ProofT, VerifiableCredentialT}, + revocation::{StatusCredentialT, StatusResolverT, StatusT}, +}; +use identity_did::DIDUrl; +use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota_core::{Error as IotaError, IotaDID, IotaIdentityClientExt}; +use identity_jose::{ + jwk::Jwk, + jws::{JwsVerifier, SignatureVerificationError, SignatureVerificationErrorKind, VerificationInput}, +}; +use identity_verification::MethodData; +use iota_sdk::{client::Client, Url}; + +pub trait ResolverT { + type Error; + + async fn fetch(&mut self, url: &Url) -> Result; +} + +pub trait VerifierT { + type Error; + + fn verify(&mut self, proof: &P, key: &K) -> Result<(), Self::Error>; +} + +pub trait ValidatorT<'c, C> { + type Error; + + async fn validate(&self, credential: &'c C) -> Result<(), Self::Error>; +} + +pub trait ValidatorStatusExt<'c, C: StatusCredentialT>: ValidatorT<'c, C> { + async fn validate_with_status( + &self, + credential: &'c C, + status_resolver: &SR, + state_predicate: F, + ) -> Result + where + F: FnOnce(&S::State) -> bool, + SR: StatusResolverT, + Self::Error: From, + S: StatusT + TryFrom<&'c C::Status>, + { + self.validate(credential).await?; + let Some(status) = credential.status() else { + return Ok(true); + }; + let credential_state = status_resolver.state(status).await?; + + Ok(state_predicate(&credential_state)) + } +} + +impl<'c, V, C> ValidatorStatusExt<'c, C> for V +where + V: ValidatorT<'c, C>, + C: StatusCredentialT, +{ +} + +#[derive(Debug, thiserror::Error)] +pub enum IotaValidationError { + #[error("Resolution error")] + Resolution(()), + #[error("Signature verification error")] + Verification(()), + #[error("Status verification error")] + Status(()), +} + +#[derive(Debug)] +pub struct IotaCredentialValidator { + resolver: Client, + verifier: V, +} + +impl IotaCredentialValidator { + pub fn new(resolver: Client, verifier: V) -> Self { + Self { resolver, verifier } + } +} + +impl<'c, C> ValidatorT<'c, C> for IotaCredentialValidator +where + C: VerifiableCredentialT<'c>, + C::Proof: ProofT, +{ + type Error = IotaValidationError; + + async fn validate(&self, credential: &'c C) -> Result<(), Self::Error> { + let is_valid_time_wise = credential.check_validity_time_frame(); + let verification_method_url = credential + .proof() + .verification_method() + .ok_or(todo!("missing verification method error"))?; + let MethodData::PublicKeyJwk(jwk) = self + .resolver + .fetch(&verification_method_url) + .await + .map_err(|_| IotaValidationError::Resolution(()))? + else { + todo!("unsupported key error"); + }; + VerifierT::verify(&mut self.verifier, &credential.proof(), &jwk) + .map_err(|_| IotaValidationError::Verification(()))?; + + Ok(()) + } +} + +impl ResolverT for Client { + type Error = IotaError; + + async fn fetch(&mut self, url: &Url) -> Result { + let did_url = DIDUrl::parse(url.as_str()).map_err(IotaError::DIDSyntaxError)?; + let did = IotaDID::try_from_core(did_url.did().clone()).map_err(IotaError::DIDSyntaxError)?; + let mut doc = self.resolve_did(&did).await?; + + let key = doc + .resolve_method(&did_url, None) + .map(|method| method.data()) + .cloned() + .ok_or(todo!("an error for verification method not found"))?; + + Ok(key) + } +} + +impl VerifierT for EdDSAJwsVerifier { + type Error = SignatureVerificationError; + fn verify(&mut self, proof: &P, key: &Jwk) -> Result<(), Self::Error> { + let input = VerificationInput { + alg: proof + .algorithm() + .parse() + .map_err(|_| SignatureVerificationError::new(SignatureVerificationErrorKind::UnsupportedAlg))?, + signing_input: proof.signing_input().into(), + decoded_signature: proof.signature().into(), + }; + + JwsVerifier::verify(self, input, key) + } +} From 1f6be96ad2c7c62e8438f08809e883787328b67e Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 28 Feb 2024 20:00:21 +0100 Subject: [PATCH 04/10] generic credential validator, IotaCredentialValidator --- examples/0_basic/5_create_vc.rs | 7 +- identity_credential/src/credential/jwt.rs | 22 +- identity_credential/src/credential/mod.rs | 7 +- identity_credential/src/credential/traits.rs | 26 ++- .../src/credential/vc2_0/mod.rs | 1 + .../src/presentation/presentation_builder.rs | 11 +- identity_credential/src/revocation/mod.rs | 2 +- identity_credential/src/revocation/traits.rs | 10 +- identity_did/src/did_url.rs | 9 + identity_validator/src/validator.rs | 199 +++++++++--------- 10 files changed, 149 insertions(+), 145 deletions(-) diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index b789cf6d19..69a0f9429e 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -24,7 +24,6 @@ use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; use identity_iota::storage::KeyIdMemstore; use identity_validator::IotaCredentialValidator; -use identity_validator::ValidatorT; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; use iota_sdk::client::secret::SecretManager; use iota_sdk::client::Client; @@ -99,15 +98,15 @@ async fn main() -> anyhow::Result<()> { None, ) .await?; - + let credential_jwt = JwtCredential::parse(credential_jwt)?; // Before sending this credential to the holder the issuer wants to validate that some properties // of the credential satisfy their expectations. // Validate the credential's signature using the issuer's DID Document, the credential's semantic structure, // that the issuance date is not in the future and that the expiration date is not in the past: - let iota_validator = IotaCredentialValidator::new(client.clone(), EdDSAJwsVerifier::default()); - iota_validator.validate(&credential_jwt).await?; + let validator = IotaCredentialValidator::new(client, EdDSAJwsVerifier::default()); + validator.validate(&credential_jwt).await.unwrap(); println!("VC successfully validated"); diff --git a/identity_credential/src/credential/jwt.rs b/identity_credential/src/credential/jwt.rs index 9f9843707e..1d1c965471 100644 --- a/identity_credential/src/credential/jwt.rs +++ b/identity_credential/src/credential/jwt.rs @@ -41,22 +41,9 @@ pub struct DecodedJws { signature: Box<[u8]>, } -impl<'a> ProofT for &'a DecodedJws { - fn algorithm(&self) -> &str { - DecodedJws::algorithm(self) - } - fn signature(&self) -> &[u8] { - DecodedJws::signature(self) - } - fn signing_input(&self) -> &[u8] { - DecodedJws::signing_input(self) - } - fn verification_method(&self) -> Option { - DecodedJws::verification_method(self) - } -} - impl ProofT for DecodedJws { + type VerificationMethod = Option; + fn algorithm(&self) -> &str { self .headers @@ -71,7 +58,7 @@ impl ProofT for DecodedJws { fn signing_input(&self) -> &[u8] { self.signing_input.as_ref() } - fn verification_method(&self) -> Option { + fn verification_method(&self) -> Self::VerificationMethod { self .headers .protected_header() @@ -248,7 +235,4 @@ impl JwtCredential { pub fn parse(jwt: Jwt) -> Result { Self::try_from(jwt) } - pub fn try_into_credential>(self) -> Result { - C::try_from(self.parsed_claims) - } } diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index f2fefce28f..3d7c5860c7 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -11,13 +11,11 @@ mod jwt; mod jwt_serialization; #[cfg(feature = "revocation-bitmap")] mod revocation_bitmap_status; +mod traits; pub mod vc1_1; pub mod vc2_0; -mod traits; - pub use self::jws::Jws; -pub use jwt::*; #[cfg(feature = "revocation-bitmap")] pub use self::revocation_bitmap_status::RevocationBitmapStatus; pub use common::Evidence; @@ -28,10 +26,11 @@ pub use common::Proof; pub use common::RefreshService; pub use common::Schema; pub use common::Subject; +pub use jwt::*; +pub use traits::*; pub use vc1_1::Credential; pub use vc1_1::CredentialBuilder; pub use vc1_1::Status; -pub use traits::*; #[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::CredentialJwtClaims; diff --git a/identity_credential/src/credential/traits.rs b/identity_credential/src/credential/traits.rs index 6da5d64965..92416d84f8 100644 --- a/identity_credential/src/credential/traits.rs +++ b/identity_credential/src/credential/traits.rs @@ -1,4 +1,5 @@ -use identity_core::common::{Timestamp, Url}; +use identity_core::common::Timestamp; +use identity_core::common::Url; pub trait CredentialT { type Issuer; @@ -24,8 +25,29 @@ pub trait VerifiableCredentialT<'c>: CredentialT { } pub trait ProofT { + type VerificationMethod; + fn algorithm(&self) -> &str; fn signature(&self) -> &[u8]; fn signing_input(&self) -> &[u8]; - fn verification_method(&self) -> Option; + fn verification_method(&self) -> Self::VerificationMethod; +} + +impl<'a, P> ProofT for &'a P +where + P: ProofT, +{ + type VerificationMethod = P::VerificationMethod; + fn algorithm(&self) -> &str { + P::algorithm(self) + } + fn signature(&self) -> &[u8] { + P::signature(self) + } + fn signing_input(&self) -> &[u8] { + P::signature(self) + } + fn verification_method(&self) -> Self::VerificationMethod { + P::verification_method(self) + } } diff --git a/identity_credential/src/credential/vc2_0/mod.rs b/identity_credential/src/credential/vc2_0/mod.rs index e69de29bb2..8b13789179 100644 --- a/identity_credential/src/credential/vc2_0/mod.rs +++ b/identity_credential/src/credential/vc2_0/mod.rs @@ -0,0 +1 @@ + diff --git a/identity_credential/src/presentation/presentation_builder.rs b/identity_credential/src/presentation/presentation_builder.rs index af7dc4d1c1..38cd039898 100644 --- a/identity_credential/src/presentation/presentation_builder.rs +++ b/identity_credential/src/presentation/presentation_builder.rs @@ -158,11 +158,12 @@ mod tests { let credential_jwt = credential.serialize_jwt(None).unwrap(); - let presentation: Presentation = PresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new()) - .type_("ExamplePresentation") - .credential(credential_jwt) - .build() - .unwrap(); + let presentation: Presentation = + PresentationBuilder::new(Url::parse("did:test:abc1").unwrap(), Object::new()) + .type_("ExamplePresentation") + .credential(credential_jwt) + .build() + .unwrap(); assert_eq!(presentation.context.len(), 1); assert_eq!( diff --git a/identity_credential/src/revocation/mod.rs b/identity_credential/src/revocation/mod.rs index 1a6a908c71..45248ff7a9 100644 --- a/identity_credential/src/revocation/mod.rs +++ b/identity_credential/src/revocation/mod.rs @@ -5,10 +5,10 @@ //! framework. mod error; -mod traits; mod revocation_bitmap_2022; #[cfg(feature = "status-list-2021")] pub mod status_list_2021; +mod traits; pub use self::error::RevocationError; pub use self::error::RevocationResult; diff --git a/identity_credential/src/revocation/traits.rs b/identity_credential/src/revocation/traits.rs index 18ef6924ae..e354b674b3 100644 --- a/identity_credential/src/revocation/traits.rs +++ b/identity_credential/src/revocation/traits.rs @@ -14,9 +14,9 @@ pub trait StatusT { } pub trait StatusResolverT { - type Error; + type Error; - async fn state<'c, S1>(&self, status: &'c S1) -> Result - where - S: TryFrom<&'c S1>; -} \ No newline at end of file + async fn state<'c, S1>(&self, status: &'c S1) -> Result + where + S: TryFrom<&'c S1>; +} diff --git a/identity_did/src/did_url.rs b/identity_did/src/did_url.rs index 60c7d6c84e..b030bc2e70 100644 --- a/identity_did/src/did_url.rs +++ b/identity_did/src/did_url.rs @@ -268,6 +268,15 @@ impl Hash for RelativeDIDUrl { } } +impl TryFrom> for DIDUrl { + type Error = Error; + fn try_from(value: Option) -> Result { + value + .ok_or(Error::Other("Missing URL")) + .and_then(|url| Self::parse(url.as_str())) + } +} + impl DIDUrl { /// Construct a new [`DIDUrl`] with optional [`RelativeDIDUrl`]. pub fn new(did: CoreDID, url: Option) -> Self { diff --git a/identity_validator/src/validator.rs b/identity_validator/src/validator.rs index a7c0864499..95b4525355 100644 --- a/identity_validator/src/validator.rs +++ b/identity_validator/src/validator.rs @@ -1,145 +1,134 @@ -use identity_credential::{ - credential::{ProofT, VerifiableCredentialT}, - revocation::{StatusCredentialT, StatusResolverT, StatusT}, -}; +use identity_credential::credential::ProofT; +use identity_credential::credential::VerifiableCredentialT; +use identity_credential::revocation::StatusCredentialT; +use identity_credential::revocation::StatusResolverT; +use identity_credential::revocation::StatusT; use identity_did::DIDUrl; use identity_eddsa_verifier::EdDSAJwsVerifier; -use identity_iota_core::{Error as IotaError, IotaDID, IotaIdentityClientExt}; -use identity_jose::{ - jwk::Jwk, - jws::{JwsVerifier, SignatureVerificationError, SignatureVerificationErrorKind, VerificationInput}, -}; +use identity_iota_core::Error as IotaError; +use identity_iota_core::IotaDID; +use identity_iota_core::IotaIdentityClientExt; +use identity_jose::jws::JwsVerifier; +use identity_jose::jws::SignatureVerificationError; +use identity_jose::jws::SignatureVerificationErrorKind; +use identity_jose::jws::VerificationInput; use identity_verification::MethodData; -use iota_sdk::{client::Client, Url}; +use iota_sdk::client::Client; +use std::marker::PhantomData; pub trait ResolverT { type Error; + type Input; - async fn fetch(&mut self, url: &Url) -> Result; + async fn fetch(&self, input: &Self::Input) -> Result; } pub trait VerifierT { type Error; - fn verify(&mut self, proof: &P, key: &K) -> Result<(), Self::Error>; + fn verify(&self, proof: &P, key: &K) -> Result<(), Self::Error>; } -pub trait ValidatorT<'c, C> { - type Error; +impl ResolverT for Client { + type Input = DIDUrl; + type Error = IotaError; - async fn validate(&self, credential: &'c C) -> Result<(), Self::Error>; -} + async fn fetch(&self, input: &Self::Input) -> Result { + let did = IotaDID::try_from_core(input.did().clone()).map_err(IotaError::DIDSyntaxError)?; + let doc = self.resolve_did(&did).await?; -pub trait ValidatorStatusExt<'c, C: StatusCredentialT>: ValidatorT<'c, C> { - async fn validate_with_status( - &self, - credential: &'c C, - status_resolver: &SR, - state_predicate: F, - ) -> Result - where - F: FnOnce(&S::State) -> bool, - SR: StatusResolverT, - Self::Error: From, - S: StatusT + TryFrom<&'c C::Status>, - { - self.validate(credential).await?; - let Some(status) = credential.status() else { - return Ok(true); - }; - let credential_state = status_resolver.state(status).await?; + let key = doc + .resolve_method(input, None) + .map(|method| method.data()) + .cloned() + .ok_or(todo!("an error for verification method not found"))?; - Ok(state_predicate(&credential_state)) + Ok(key) } } -impl<'c, V, C> ValidatorStatusExt<'c, C> for V -where - V: ValidatorT<'c, C>, - C: StatusCredentialT, -{ -} +impl VerifierT for EdDSAJwsVerifier { + type Error = SignatureVerificationError; + fn verify(&self, proof: &P, key: &MethodData) -> Result<(), Self::Error> { + let MethodData::PublicKeyJwk(jwk) = key else { + todo!("Unsupported key") + }; + let input = VerificationInput { + alg: proof + .algorithm() + .parse() + .map_err(|_| SignatureVerificationError::new(SignatureVerificationErrorKind::UnsupportedAlg))?, + signing_input: proof.signing_input().into(), + decoded_signature: proof.signature().into(), + }; -#[derive(Debug, thiserror::Error)] -pub enum IotaValidationError { - #[error("Resolution error")] - Resolution(()), - #[error("Signature verification error")] - Verification(()), - #[error("Status verification error")] - Status(()), + JwsVerifier::verify(self, input, jwk) + } } -#[derive(Debug)] -pub struct IotaCredentialValidator { - resolver: Client, +pub struct CredentialValidator { + resolver: R, verifier: V, + _key: PhantomData, } -impl IotaCredentialValidator { - pub fn new(resolver: Client, verifier: V) -> Self { - Self { resolver, verifier } +pub type IotaCredentialValidator = CredentialValidator; + +impl CredentialValidator { + pub fn new(resolver: R, verifier: V) -> Self { + Self { + resolver, + verifier, + _key: PhantomData, + } } } -impl<'c, C> ValidatorT<'c, C> for IotaCredentialValidator +impl CredentialValidator where - C: VerifiableCredentialT<'c>, - C::Proof: ProofT, + R: ResolverT, + V: VerifierT, { - type Error = IotaValidationError; - - async fn validate(&self, credential: &'c C) -> Result<(), Self::Error> { - let is_valid_time_wise = credential.check_validity_time_frame(); - let verification_method_url = credential - .proof() - .verification_method() - .ok_or(todo!("missing verification method error"))?; - let MethodData::PublicKeyJwk(jwk) = self - .resolver - .fetch(&verification_method_url) - .await - .map_err(|_| IotaValidationError::Resolution(()))? - else { - todo!("unsupported key error"); + pub async fn validate<'c, C>(&self, credential: &'c C) -> Result<(), ()> + where + C: VerifiableCredentialT<'c>, + C::Proof: ProofT, + ::VerificationMethod: TryInto, + { + let proof = credential.proof(); + let Ok(verification_method) = proof.verification_method().try_into() else { + todo!("Failed to convert to valid verification method type") }; - VerifierT::verify(&mut self.verifier, &credential.proof(), &jwk) - .map_err(|_| IotaValidationError::Verification(()))?; + let key = self.resolver.fetch(&verification_method).await.map_err(|_| ())?; + self.verifier.verify(&proof, &key).map_err(|_| ())?; Ok(()) } -} - -impl ResolverT for Client { - type Error = IotaError; - - async fn fetch(&mut self, url: &Url) -> Result { - let did_url = DIDUrl::parse(url.as_str()).map_err(IotaError::DIDSyntaxError)?; - let did = IotaDID::try_from_core(did_url.did().clone()).map_err(IotaError::DIDSyntaxError)?; - let mut doc = self.resolve_did(&did).await?; - - let key = doc - .resolve_method(&did_url, None) - .map(|method| method.data()) - .cloned() - .ok_or(todo!("an error for verification method not found"))?; - - Ok(key) - } -} -impl VerifierT for EdDSAJwsVerifier { - type Error = SignatureVerificationError; - fn verify(&mut self, proof: &P, key: &Jwk) -> Result<(), Self::Error> { - let input = VerificationInput { - alg: proof - .algorithm() - .parse() - .map_err(|_| SignatureVerificationError::new(SignatureVerificationErrorKind::UnsupportedAlg))?, - signing_input: proof.signing_input().into(), - decoded_signature: proof.signature().into(), + async fn validate_with_status<'c, C, S, SR, F>( + &self, + credential: &'c C, + status_resolver: &SR, + state_predicate: F, + ) -> Result<(), ()> + where + C: VerifiableCredentialT<'c> + StatusCredentialT, + C::Proof: ProofT, + ::VerificationMethod: TryInto, + SR: StatusResolverT, + S: StatusT + TryFrom<&'c C::Status>, + F: FnOnce(&S::State) -> bool, + { + self.validate(credential).await?; + let Some(status) = credential.status() else { + return Ok(()); }; + let credential_state = status_resolver.state(status).await.map_err(|_| ())?; - JwsVerifier::verify(self, input, key) + if !state_predicate(&credential_state) { + todo!("Non-valid state!") + } else { + Ok(()) + } } } From 531cd1fa78db91ebccfd493cd289ebe2885b0828 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 5 Mar 2024 13:38:43 +0100 Subject: [PATCH 05/10] ValidableCredential, SdJwtCredential --- Cargo.toml | 3 +- examples/0_basic/5_create_vc.rs | 16 +- examples/Cargo.toml | 1 - identity_core/src/lib.rs | 7 + identity_credential/Cargo.toml | 4 +- identity_credential/src/credential/jwt.rs | 138 +++++++++++--- identity_credential/src/credential/mod.rs | 2 + .../src/credential/sd_jwt/credential.rs | 176 ++++++++++++++++++ .../src/credential/sd_jwt/mod.rs | 2 + identity_credential/src/credential/traits.rs | 34 +--- .../src/credential/vc1_1/credential.rs | 9 - .../src/credential/vc2_0/credential.rs | 65 +++++++ .../src/credential/vc2_0/mod.rs | 2 + identity_credential/src/revocation/traits.rs | 34 +++- identity_eddsa_verifier/Cargo.toml | 1 + identity_eddsa_verifier/src/eddsa_verifier.rs | 22 +++ identity_resolver/Cargo.toml | 2 + identity_resolver/src/lib.rs | 39 ++++ identity_validator/Cargo.toml | 21 --- identity_validator/src/lib.rs | 3 - identity_validator/src/validator.rs | 134 ------------- identity_verification/src/lib.rs | 34 ++++ 22 files changed, 518 insertions(+), 231 deletions(-) create mode 100644 identity_credential/src/credential/sd_jwt/credential.rs create mode 100644 identity_credential/src/credential/sd_jwt/mod.rs create mode 100644 identity_credential/src/credential/vc2_0/credential.rs delete mode 100644 identity_validator/Cargo.toml delete mode 100644 identity_validator/src/lib.rs delete mode 100644 identity_validator/src/validator.rs diff --git a/Cargo.toml b/Cargo.toml index 06429dc0c6..d4726e0d4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,7 @@ members = [ "identity_stronghold", "identity_jose", "identity_eddsa_verifier", - "identity_validator", - "examples", + "examples", ] exclude = ["bindings/wasm"] diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index 69a0f9429e..3a14e262e2 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -16,14 +16,12 @@ use identity_iota::core::Object; use identity_iota::credential::DecodedJwtCredential; use identity_iota::credential::Jwt; -use identity_iota::credential::JwtCredential; use identity_iota::credential::JwtCredentialValidationOptions; use identity_iota::credential::JwtCredentialValidator; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; use identity_iota::storage::KeyIdMemstore; -use identity_validator::IotaCredentialValidator; use iota_sdk::client::secret::stronghold::StrongholdSecretManager; use iota_sdk::client::secret::SecretManager; use iota_sdk::client::Client; @@ -99,18 +97,24 @@ async fn main() -> anyhow::Result<()> { ) .await?; - let credential_jwt = JwtCredential::parse(credential_jwt)?; // Before sending this credential to the holder the issuer wants to validate that some properties // of the credential satisfy their expectations. // Validate the credential's signature using the issuer's DID Document, the credential's semantic structure, // that the issuance date is not in the future and that the expiration date is not in the past: - let validator = IotaCredentialValidator::new(client, EdDSAJwsVerifier::default()); - validator.validate(&credential_jwt).await.unwrap(); + let decoded_credential: DecodedJwtCredential = + JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()) + .validate::<_, Object>( + &credential_jwt, + &issuer_document, + &JwtCredentialValidationOptions::default(), + FailFast::FirstError, + ) + .unwrap(); println!("VC successfully validated"); - //println!("Credential JSON > {:#}", decoded_credential.credential); + println!("Credential JSON > {:#}", decoded_credential.credential); Ok(()) } diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 776ec341ee..a9337d6a33 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,7 +10,6 @@ anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] } identity_stronghold = { path = "../identity_stronghold", default-features = false } -identity_validator = { path = "../identity_validator" } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } primitive-types = "0.12.1" rand = "0.8.5" diff --git a/identity_core/src/lib.rs b/identity_core/src/lib.rs index b915fcdeba..0727c06d95 100644 --- a/identity_core/src/lib.rs +++ b/identity_core/src/lib.rs @@ -25,3 +25,10 @@ pub mod error; pub use self::error::Error; pub use self::error::Result; + +pub trait ResolverT { + type Error; + type Input; + + async fn fetch(&self, input: &Self::Input) -> Result; +} \ No newline at end of file diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 7c116e647c..af1718a323 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -17,8 +17,8 @@ futures = { version = "0.3", default-features = false, optional = true } identity_core = { version = "=1.1.1", path = "../identity_core", default-features = false } identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } -identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } identity_jose = { version = "=1.1.1", path = "../identity_jose" } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } itertools = { version = "0.11", default-features = false, features = ["use_std"], optional = true } once_cell = { version = "1.18", default-features = false, features = ["std"] } @@ -29,10 +29,10 @@ serde.workspace = true serde-aux = { version = "4.3.1", default-features = false, optional = true } serde_json.workspace = true serde_repr = { version = "0.1", default-features = false, optional = true } +serde_with = "3.6.1" strum.workspace = true thiserror.workspace = true url = { version = "2.5", default-features = false } -serde_with = "3.6.1" [dev-dependencies] anyhow = "1.0.62" diff --git a/identity_credential/src/credential/jwt.rs b/identity_credential/src/credential/jwt.rs index 1d1c965471..c2c215af4c 100644 --- a/identity_credential/src/credential/jwt.rs +++ b/identity_credential/src/credential/jwt.rs @@ -6,6 +6,7 @@ // Decoder gives us all we need. We need to unpack it into a JWS (alg, sig_input, sig_challenge) and claims. // The claims will be parsed into a JwtCredentialClaims. +use std::fmt::Debug; use std::fmt::Display; use std::ops::Deref; use std::str::FromStr; @@ -16,16 +17,20 @@ use identity_core::common::Url; use identity_jose::error::Error as JoseError; use identity_jose::jws::DecodedHeaders; use identity_jose::jws::Decoder as JwsDecoder; +use identity_verification::ProofT; +use identity_verification::VerifierT; use serde::Deserialize; use serde::Serialize; use serde_with::DeserializeFromStr; use serde_with::SerializeDisplay; use thiserror::Error; +use crate::revocation::StatusCredentialT; + use super::CredentialT; use super::Issuer; -use super::ProofT; -use super::VerifiableCredentialT; +use identity_core::ResolverT; +use super::ValidableCredential; #[derive(Error, Debug)] pub enum JwtError { @@ -41,6 +46,12 @@ pub struct DecodedJws { signature: Box<[u8]>, } +impl DecodedJws { + pub fn claims(&self) -> &[u8] { + &self.raw_claims + } +} + impl ProofT for DecodedJws { type VerificationMethod = Option; @@ -69,8 +80,8 @@ impl ProofT for DecodedJws { #[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)] pub struct Jwt { - inner: String, - decoded_jws: DecodedJws, + pub(crate) inner: String, + pub(crate) decoded_jws: DecodedJws, } impl FromStr for Jwt { @@ -112,6 +123,8 @@ impl Jwt { pub enum JwtCredentialError { #[error(transparent)] DeserializationError(#[from] serde_json::Error), + #[error("Failed to parse packed credential")] + CredentialUnpackingError, } #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] @@ -171,30 +184,89 @@ impl JwtCredentialClaims { } } -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(try_from = "Jwt", into = "Jwt")] -pub struct JwtCredential { - inner: String, - parsed_claims: JwtCredentialClaims, - decoded_jws: DecodedJws, +pub struct JwtCredential { + pub(crate) inner: String, + pub(crate) credential: C, + pub(crate) parsed_claims: JwtCredentialClaims, + pub(crate) decoded_jws: DecodedJws, +} + +impl Debug for JwtCredential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JwtCredential") + .field("inner", &self.inner.as_str()) + .field("credential", &self.credential) + .field("parsed_claims", &self.parsed_claims) + .field("decoded_jws", &self.decoded_jws) + .finish() + } +} + +impl Clone for JwtCredential { + fn clone(&self) -> Self { + let JwtCredential { + inner, + credential, + parsed_claims, + decoded_jws, + } = self; + Self { + inner: inner.clone(), + credential: credential.clone(), + parsed_claims: parsed_claims.clone(), + decoded_jws: decoded_jws.clone(), + } + } } -impl TryFrom for JwtCredential { +impl serde::Serialize for JwtCredential { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.inner.as_str().serialize(serializer) + } +} + +impl<'de, C> serde::Deserialize<'de> for JwtCredential +where + C: for<'a> TryFrom<&'a JwtCredentialClaims>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + let jwt = Jwt::deserialize(deserializer)?; + Self::try_from(jwt).map_err(|e| D::Error::custom(e)) + } +} + +impl TryFrom for JwtCredential +where + C: for<'a> TryFrom<&'a JwtCredentialClaims>, +{ type Error = JwtCredentialError; fn try_from(jwt: Jwt) -> Result { let Jwt { inner, decoded_jws } = jwt; let parsed_claims = serde_json::from_slice(&decoded_jws.raw_claims)?; + let credential = C::try_from(&parsed_claims).map_err(|_| JwtCredentialError::CredentialUnpackingError)?; Ok(Self { inner, decoded_jws, parsed_claims, + credential, }) } } -impl From for Jwt { - fn from(value: JwtCredential) -> Self { +impl From> for Jwt +where + C: Into, +{ + fn from(value: JwtCredential) -> Self { Jwt { inner: value.inner, decoded_jws: value.decoded_jws, @@ -202,7 +274,7 @@ impl From for Jwt { } } -impl CredentialT for JwtCredential { +impl CredentialT for JwtCredential { type Claim = JwtCredentialClaims; type Issuer = Issuer; @@ -223,16 +295,40 @@ impl CredentialT for JwtCredential { } } -impl<'c> VerifiableCredentialT<'c> for JwtCredential { - type Proof = &'c DecodedJws; +impl JwtCredential +where + C: for<'a> TryFrom<&'a JwtCredentialClaims>, +{ + pub fn parse(jwt: Jwt) -> Result { + Self::try_from(jwt) + } +} - fn proof(&'c self) -> Self::Proof { - &self.decoded_jws +impl StatusCredentialT for JwtCredential { + type Status = C::Status; + fn status(&self) -> Option<&Self::Status> { + self.credential.status() } } -impl JwtCredential { - pub fn parse(jwt: Jwt) -> Result { - Self::try_from(jwt) +impl ValidableCredential for JwtCredential +where + R: ResolverT, + R::Input: TryFrom, + V: VerifierT, +{ + async fn validate(&self, resolver: &R, verifier: &V) -> Result<(), ()> { + if !self.check_validity_time_frame() { + todo!("expired credential err"); + } + let kid = self + .decoded_jws + .verification_method() + .ok_or(()) + .and_then(|kid| R::Input::try_from(kid).map_err(|_| ()))?; + let key = resolver.fetch(&kid).await.map_err(|_| ())?; + verifier.verify(&self.decoded_jws, &key).map_err(|_| ())?; + + Ok(()) } } diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 3d7c5860c7..260932a004 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -11,6 +11,8 @@ mod jwt; mod jwt_serialization; #[cfg(feature = "revocation-bitmap")] mod revocation_bitmap_status; +#[cfg(feature = "sd-jwt")] +pub mod sd_jwt; mod traits; pub mod vc1_1; pub mod vc2_0; diff --git a/identity_credential/src/credential/sd_jwt/credential.rs b/identity_credential/src/credential/sd_jwt/credential.rs new file mode 100644 index 0000000000..580be9c1af --- /dev/null +++ b/identity_credential/src/credential/sd_jwt/credential.rs @@ -0,0 +1,176 @@ +use std::fmt::Debug; + +use identity_core::common::Timestamp; +use identity_core::common::Url; +use itertools::Itertools; +use sd_jwt_payload::SdJwt; +use sd_jwt_payload::SdObjectDecoder; + +use crate::credential::CredentialT; +use crate::credential::Issuer; +use crate::credential::Jwt; +use crate::credential::JwtCredential; +use crate::credential::JwtCredentialClaims; +use identity_core::ResolverT; +use crate::credential::ValidableCredential; +use crate::revocation::StatusCredentialT; +use identity_verification::VerifierT; + +pub struct SdJwtCredential { + jwt_credential: JwtCredential, + disclosures: Vec, + key_binding_jwt: Option, +} + +impl Debug for SdJwtCredential { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SdJwtCredential") + .field("jwt_credential", &self.jwt_credential) + .field("disclosures", &self.disclosures) + .field("key_binding_jwt", &self.key_binding_jwt) + .finish() + } +} + +impl Clone for SdJwtCredential { + fn clone(&self) -> Self { + Self { + jwt_credential: self.jwt_credential.clone(), + disclosures: self.disclosures.clone(), + key_binding_jwt: self.key_binding_jwt.clone(), + } + } +} + +impl serde::Serialize for SdJwtCredential { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let disclosures = self.disclosures.iter().join("~"); + let key_bindings = self.key_binding_jwt.as_deref().unwrap_or(""); + format!("{}~{}~{}", &self.jwt_credential.inner, disclosures, key_bindings).serialize(serializer) + } +} + +impl<'de, C> serde::Deserialize<'de> for SdJwtCredential +where + C: for<'a> TryFrom<&'a JwtCredentialClaims>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + todo!() + } +} + +impl TryFrom for SdJwtCredential +where + C: for<'a> TryFrom<&'a JwtCredentialClaims>, +{ + type Error = (); + fn try_from(sd_jwt: SdJwt) -> Result { + Self::parse(sd_jwt) + } +} + +impl SdJwtCredential +where + C: for<'a> TryFrom<&'a JwtCredentialClaims>, +{ + pub fn parse(sd_jwt: SdJwt) -> Result { + Self::parse_with_decoder(sd_jwt, SdObjectDecoder::default()) + } + pub fn parse_with_decoder(sd_jwt: SdJwt, decoder: SdObjectDecoder) -> Result { + let SdJwt { + jwt, + disclosures, + key_binding_jwt, + } = sd_jwt; + let jwt = Jwt::parse(jwt).map_err(|_| ())?; + let serde_json::Value::Object(raw_claims) = + serde_json::from_slice::(jwt.decoded_jws.claims()).map_err(|_| ())? + else { + todo!("invalid claims") + }; + let parsed_claims = decoder + .decode(&raw_claims, &disclosures) + .map_err(|_| ()) + .map(serde_json::Value::Object) + .and_then(|claims| serde_json::from_value::(claims).map_err(|_| ()))?; + let credential = C::try_from(&parsed_claims).map_err(|_| ())?; + let jwt_credential = JwtCredential { + decoded_jws: jwt.decoded_jws, + inner: jwt.inner, + credential, + parsed_claims, + }; + + Ok(Self { + jwt_credential, + disclosures, + key_binding_jwt, + }) + } +} + +impl CredentialT for SdJwtCredential { + type Claim = JwtCredentialClaims; + type Issuer = Issuer; + + fn id(&self) -> &Url { + self.jwt_credential.id() + } + fn issuer(&self) -> &Self::Issuer { + self.jwt_credential.issuer() + } + fn claim(&self) -> &Self::Claim { + self.jwt_credential.claim() + } + fn valid_from(&self) -> Timestamp { + self.jwt_credential.valid_from() + } + fn valid_until(&self) -> Option { + self.jwt_credential.valid_until() + } +} + +impl StatusCredentialT for SdJwtCredential { + type Status = C::Status; + fn status(&self) -> Option<&Self::Status> { + self.jwt_credential.status() + } +} + +impl Into for SdJwtCredential { + fn into(self) -> SdJwt { + let Self { + jwt_credential, + disclosures, + key_binding_jwt, + } = self; + SdJwt { + jwt: jwt_credential.inner, + disclosures, + key_binding_jwt, + } + } +} + +impl SdJwtCredential { + pub fn credential(&self) -> &C { + &self.jwt_credential.credential + } +} + +impl ValidableCredential for SdJwtCredential +where + R: ResolverT, + R::Input: TryFrom, + V: VerifierT, +{ + async fn validate(&self, resolver: &R, verifier: &V) -> Result<(), ()> { + self.jwt_credential.validate(resolver, verifier).await + } +} diff --git a/identity_credential/src/credential/sd_jwt/mod.rs b/identity_credential/src/credential/sd_jwt/mod.rs new file mode 100644 index 0000000000..1bf287ec98 --- /dev/null +++ b/identity_credential/src/credential/sd_jwt/mod.rs @@ -0,0 +1,2 @@ +mod credential; +pub use credential::*; diff --git a/identity_credential/src/credential/traits.rs b/identity_credential/src/credential/traits.rs index 92416d84f8..45446ff9e5 100644 --- a/identity_credential/src/credential/traits.rs +++ b/identity_credential/src/credential/traits.rs @@ -18,36 +18,8 @@ pub trait CredentialT { } } -pub trait VerifiableCredentialT<'c>: CredentialT { - type Proof; +type ValidationError = (); - fn proof(&'c self) -> Self::Proof; -} - -pub trait ProofT { - type VerificationMethod; - - fn algorithm(&self) -> &str; - fn signature(&self) -> &[u8]; - fn signing_input(&self) -> &[u8]; - fn verification_method(&self) -> Self::VerificationMethod; -} - -impl<'a, P> ProofT for &'a P -where - P: ProofT, -{ - type VerificationMethod = P::VerificationMethod; - fn algorithm(&self) -> &str { - P::algorithm(self) - } - fn signature(&self) -> &[u8] { - P::signature(self) - } - fn signing_input(&self) -> &[u8] { - P::signature(self) - } - fn verification_method(&self) -> Self::VerificationMethod { - P::verification_method(self) - } +pub trait ValidableCredential: CredentialT { + async fn validate(&self, resolver: &R, verifier: &V) -> Result<(), ValidationError>; } diff --git a/identity_credential/src/credential/vc1_1/credential.rs b/identity_credential/src/credential/vc1_1/credential.rs index 1963931923..b13a74505c 100644 --- a/identity_credential/src/credential/vc1_1/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -25,7 +25,6 @@ use crate::credential::RefreshService; use crate::credential::Schema; use crate::credential::Status; use crate::credential::Subject; -use crate::credential::VerifiableCredentialT; use crate::error::Error; use crate::error::Result; @@ -106,14 +105,6 @@ impl CredentialT for Credential { } } -impl<'c> VerifiableCredentialT<'c> for Credential { - type Proof = Option<&'c Proof>; - - fn proof(&'c self) -> Self::Proof { - self.proof.as_ref() - } -} - impl Credential { /// Returns the base JSON-LD context. pub fn base_context() -> &'static Context { diff --git a/identity_credential/src/credential/vc2_0/credential.rs b/identity_credential/src/credential/vc2_0/credential.rs new file mode 100644 index 0000000000..3f7e0f8529 --- /dev/null +++ b/identity_credential/src/credential/vc2_0/credential.rs @@ -0,0 +1,65 @@ +use identity_core::common::Context; +use identity_core::common::Object; +use identity_core::common::OneOrMany; +use identity_core::common::Timestamp; +use identity_core::common::Url; +use serde::Deserialize; +use serde::Serialize; + +use crate::credential::Evidence; +use crate::credential::Issuer; +use crate::credential::Policy; +use crate::credential::Proof; +use crate::credential::RefreshService; +use crate::credential::Schema; +use crate::credential::Status; +use crate::credential::Subject; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct Credential { + /// The JSON-LD context(s) applicable to the `Credential`. + #[serde(rename = "@context")] + pub context: OneOrMany, + /// A unique `URI` that may be used to identify the `Credential`. + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + /// One or more URIs defining the type of the `Credential`. + #[serde(rename = "type")] + pub types: OneOrMany, + /// One or more `Object`s representing the `Credential` subject(s). + #[serde(rename = "credentialSubject")] + pub credential_subject: OneOrMany, + /// A reference to the issuer of the `Credential`. + pub issuer: Issuer, + /// A timestamp of when the `Credential` becomes valid. + #[serde(rename = "validFrom")] + pub valid_from: Timestamp, + /// A timestamp of when the `Credential` should no longer be considered valid. + #[serde(rename = "validUntil", skip_serializing_if = "Option::is_none")] + pub valid_until: Option, + /// Information used to determine the current status of the `Credential`. + #[serde(default, rename = "credentialStatus", skip_serializing_if = "Option::is_none")] + pub credential_status: Option, + /// Information used to assist in the enforcement of a specific `Credential` structure. + #[serde(default, rename = "credentialSchema", skip_serializing_if = "OneOrMany::is_empty")] + pub credential_schema: OneOrMany, + /// Service(s) used to refresh an expired `Credential`. + #[serde(default, rename = "refreshService", skip_serializing_if = "OneOrMany::is_empty")] + pub refresh_service: OneOrMany, + /// Terms-of-use specified by the `Credential` issuer. + #[serde(default, rename = "termsOfUse", skip_serializing_if = "OneOrMany::is_empty")] + pub terms_of_use: OneOrMany, + /// Human-readable evidence used to support the claims within the `Credential`. + #[serde(default, skip_serializing_if = "OneOrMany::is_empty")] + pub evidence: OneOrMany, + /// Indicates that the `Credential` must only be contained within a + /// [`Presentation`][crate::presentation::Presentation] with a proof issued from the `Credential` subject. + #[serde(rename = "nonTransferable", skip_serializing_if = "Option::is_none")] + pub non_transferable: Option, + /// Miscellaneous properties. + #[serde(flatten)] + pub properties: T, + /// Optional cryptographic proof, unrelated to JWT. + #[serde(skip_serializing_if = "Option::is_none")] + pub proof: Option, +} diff --git a/identity_credential/src/credential/vc2_0/mod.rs b/identity_credential/src/credential/vc2_0/mod.rs index 8b13789179..c1e5209535 100644 --- a/identity_credential/src/credential/vc2_0/mod.rs +++ b/identity_credential/src/credential/vc2_0/mod.rs @@ -1 +1,3 @@ +mod credential; +pub use credential::Credential as Vc2_0; diff --git a/identity_credential/src/revocation/traits.rs b/identity_credential/src/revocation/traits.rs index e354b674b3..400749ec00 100644 --- a/identity_credential/src/revocation/traits.rs +++ b/identity_credential/src/revocation/traits.rs @@ -1,10 +1,10 @@ use crate::credential::CredentialT; +use crate::credential::ValidableCredential; pub trait StatusCredentialT: CredentialT { type Status; fn status(&self) -> Option<&Self::Status>; - fn set_status(&mut self, status: Option); } pub trait StatusT { @@ -20,3 +20,35 @@ pub trait StatusResolverT { where S: TryFrom<&'c S1>; } + +pub trait ValidableCredentialStatusExt +where + Self: ValidableCredential + StatusCredentialT, +{ + async fn validate_with_status<'c, S, SR, F>( + &'c self, + resolver: &R, + verifier: &V, + status_resolver: &SR, + state_predicate: F, + ) -> Result<(), ()> + where + SR: StatusResolverT, + S: StatusT + TryFrom<&'c Self::Status>, + F: FnOnce(&S::State) -> bool, + { + self.validate(resolver, verifier).await?; + let Some(status) = self.status() else { + return Ok(()); + }; + let credential_state = status_resolver.state(status).await.map_err(|_| ())?; + + if !state_predicate(&credential_state) { + todo!("Non-valid state!") + } else { + Ok(()) + } + } +} + +impl ValidableCredentialStatusExt for C where C: ValidableCredential + StatusCredentialT {} diff --git a/identity_eddsa_verifier/Cargo.toml b/identity_eddsa_verifier/Cargo.toml index 257fa5d5a4..cdabf409e9 100644 --- a/identity_eddsa_verifier/Cargo.toml +++ b/identity_eddsa_verifier/Cargo.toml @@ -13,6 +13,7 @@ description = "JWS EdDSA signature verification for IOTA Identity" [dependencies] identity_jose = { version = "=1.1.1", path = "../identity_jose", default-features = false } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } iota-crypto = { version = "0.23", default-features = false, features = ["std"] } [features] diff --git a/identity_eddsa_verifier/src/eddsa_verifier.rs b/identity_eddsa_verifier/src/eddsa_verifier.rs index a61e100568..90c4fdb9b2 100644 --- a/identity_eddsa_verifier/src/eddsa_verifier.rs +++ b/identity_eddsa_verifier/src/eddsa_verifier.rs @@ -6,6 +6,9 @@ use identity_jose::jws::JwsVerifier; use identity_jose::jws::SignatureVerificationError; use identity_jose::jws::SignatureVerificationErrorKind; use identity_jose::jws::VerificationInput; +use identity_verification::MethodData; +use identity_verification::ProofT; +use identity_verification::VerifierT; /// An implementor of [`JwsVerifier`] that can handle the /// [`JwsAlgorithm::EdDSA`](identity_jose::jws::JwsAlgorithm::EdDSA) algorithm. @@ -33,3 +36,22 @@ impl JwsVerifier for EdDSAJwsVerifier { } } } + +impl VerifierT for EdDSAJwsVerifier { + type Error = SignatureVerificationError; + fn verify(&self, proof: &P, key: &MethodData) -> Result<(), Self::Error> { + let MethodData::PublicKeyJwk(jwk) = key else { + todo!("Unsupported key") + }; + let input = VerificationInput { + alg: proof + .algorithm() + .parse() + .map_err(|_| SignatureVerificationError::new(SignatureVerificationErrorKind::UnsupportedAlg))?, + signing_input: proof.signing_input().into(), + decoded_signature: proof.signature().into(), + }; + + JwsVerifier::verify(self, input, jwk) + } +} diff --git a/identity_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 60bda28350..68fbfdf9d6 100644 --- a/identity_resolver/Cargo.toml +++ b/identity_resolver/Cargo.toml @@ -19,6 +19,8 @@ identity_core = { version = "=1.1.1", path = "../identity_core", default-feature identity_credential = { version = "=1.1.1", path = "../identity_credential", default-features = false, features = ["validator"] } identity_did = { version = "=1.1.1", path = "../identity_did", default-features = false } identity_document = { version = "=1.1.1", path = "../identity_document", default-features = false } +identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } +iota-sdk = { version = "1.0", features = ["client"] } serde = { version = "1.0", default-features = false, features = ["std", "derive"] } strum.workspace = true thiserror = { version = "1.0", default-features = false } diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs index b773741b09..ab01987a09 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -17,7 +17,46 @@ mod error; mod resolution; +use std::ops::Deref; + pub use self::error::Error; pub use self::error::ErrorCause; pub use self::error::Result; +use identity_core::ResolverT; pub use resolution::*; + +use identity_did::DIDUrl; +use identity_iota_core::Error as IotaError; +use identity_iota_core::IotaDID; +use identity_iota_core::IotaIdentityClientExt; +use identity_verification::MethodData; +use iota_sdk::client::Client; + +#[derive(Clone, Debug)] +#[repr(transparent)] +pub struct IotaResolver(Client); + +impl Deref for IotaResolver { + type Target = Client; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ResolverT for IotaResolver { + type Input = DIDUrl; + type Error = IotaError; + + async fn fetch(&self, input: &Self::Input) -> Result { + let did = IotaDID::try_from_core(input.did().clone()).map_err(IotaError::DIDSyntaxError)?; + let doc = self.resolve_did(&did).await?; + + let key = doc + .resolve_method(input, None) + .map(|method| method.data()) + .cloned() + .ok_or(todo!("an error for verification method not found"))?; + + Ok(key) + } +} diff --git a/identity_validator/Cargo.toml b/identity_validator/Cargo.toml deleted file mode 100644 index a30e60aab9..0000000000 --- a/identity_validator/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "identity_validator" -version = "0.1.0" -authors.workspace = true -edition.workspace = true -homepage.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -identity_did = { path = "../identity_did" } -identity_credential = { path = "../identity_credential" } -identity_iota_core = { path = "../identity_iota_core", features = ["iota-client"] } -identity_jose = { path = "../identity_jose" } -identity_verification = { path = "../identity_verification" } -identity_eddsa_verifier = { path = "../identity_eddsa_verifier", features = ["ed25519"] } -iota-sdk = { version = "1.1.4", features = ["client"] } -thiserror.workspace = true \ No newline at end of file diff --git a/identity_validator/src/lib.rs b/identity_validator/src/lib.rs deleted file mode 100644 index 4768ccdaa4..0000000000 --- a/identity_validator/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod validator; - -pub use validator::*; diff --git a/identity_validator/src/validator.rs b/identity_validator/src/validator.rs deleted file mode 100644 index 95b4525355..0000000000 --- a/identity_validator/src/validator.rs +++ /dev/null @@ -1,134 +0,0 @@ -use identity_credential::credential::ProofT; -use identity_credential::credential::VerifiableCredentialT; -use identity_credential::revocation::StatusCredentialT; -use identity_credential::revocation::StatusResolverT; -use identity_credential::revocation::StatusT; -use identity_did::DIDUrl; -use identity_eddsa_verifier::EdDSAJwsVerifier; -use identity_iota_core::Error as IotaError; -use identity_iota_core::IotaDID; -use identity_iota_core::IotaIdentityClientExt; -use identity_jose::jws::JwsVerifier; -use identity_jose::jws::SignatureVerificationError; -use identity_jose::jws::SignatureVerificationErrorKind; -use identity_jose::jws::VerificationInput; -use identity_verification::MethodData; -use iota_sdk::client::Client; -use std::marker::PhantomData; - -pub trait ResolverT { - type Error; - type Input; - - async fn fetch(&self, input: &Self::Input) -> Result; -} - -pub trait VerifierT { - type Error; - - fn verify(&self, proof: &P, key: &K) -> Result<(), Self::Error>; -} - -impl ResolverT for Client { - type Input = DIDUrl; - type Error = IotaError; - - async fn fetch(&self, input: &Self::Input) -> Result { - let did = IotaDID::try_from_core(input.did().clone()).map_err(IotaError::DIDSyntaxError)?; - let doc = self.resolve_did(&did).await?; - - let key = doc - .resolve_method(input, None) - .map(|method| method.data()) - .cloned() - .ok_or(todo!("an error for verification method not found"))?; - - Ok(key) - } -} - -impl VerifierT for EdDSAJwsVerifier { - type Error = SignatureVerificationError; - fn verify(&self, proof: &P, key: &MethodData) -> Result<(), Self::Error> { - let MethodData::PublicKeyJwk(jwk) = key else { - todo!("Unsupported key") - }; - let input = VerificationInput { - alg: proof - .algorithm() - .parse() - .map_err(|_| SignatureVerificationError::new(SignatureVerificationErrorKind::UnsupportedAlg))?, - signing_input: proof.signing_input().into(), - decoded_signature: proof.signature().into(), - }; - - JwsVerifier::verify(self, input, jwk) - } -} - -pub struct CredentialValidator { - resolver: R, - verifier: V, - _key: PhantomData, -} - -pub type IotaCredentialValidator = CredentialValidator; - -impl CredentialValidator { - pub fn new(resolver: R, verifier: V) -> Self { - Self { - resolver, - verifier, - _key: PhantomData, - } - } -} - -impl CredentialValidator -where - R: ResolverT, - V: VerifierT, -{ - pub async fn validate<'c, C>(&self, credential: &'c C) -> Result<(), ()> - where - C: VerifiableCredentialT<'c>, - C::Proof: ProofT, - ::VerificationMethod: TryInto, - { - let proof = credential.proof(); - let Ok(verification_method) = proof.verification_method().try_into() else { - todo!("Failed to convert to valid verification method type") - }; - let key = self.resolver.fetch(&verification_method).await.map_err(|_| ())?; - self.verifier.verify(&proof, &key).map_err(|_| ())?; - - Ok(()) - } - - async fn validate_with_status<'c, C, S, SR, F>( - &self, - credential: &'c C, - status_resolver: &SR, - state_predicate: F, - ) -> Result<(), ()> - where - C: VerifiableCredentialT<'c> + StatusCredentialT, - C::Proof: ProofT, - ::VerificationMethod: TryInto, - SR: StatusResolverT, - S: StatusT + TryFrom<&'c C::Status>, - F: FnOnce(&S::State) -> bool, - { - self.validate(credential).await?; - let Some(status) = credential.status() else { - return Ok(()); - }; - let credential_state = status_resolver.state(status).await.map_err(|_| ())?; - - if !state_predicate(&credential_state) { - todo!("Non-valid state!") - } else { - Ok(()) - } - } -} diff --git a/identity_verification/src/lib.rs b/identity_verification/src/lib.rs index 100acd138b..98b5400cb2 100644 --- a/identity_verification/src/lib.rs +++ b/identity_verification/src/lib.rs @@ -27,3 +27,37 @@ pub use jose::jwk; pub use jose::jws; pub use jose::jwu; pub use verification_method::*; + +pub trait ProofT { + type VerificationMethod; + + fn algorithm(&self) -> &str; + fn signature(&self) -> &[u8]; + fn signing_input(&self) -> &[u8]; + fn verification_method(&self) -> Self::VerificationMethod; +} + +impl<'a, P> ProofT for &'a P +where + P: ProofT, +{ + type VerificationMethod = P::VerificationMethod; + fn algorithm(&self) -> &str { + P::algorithm(self) + } + fn signature(&self) -> &[u8] { + P::signature(self) + } + fn signing_input(&self) -> &[u8] { + P::signature(self) + } + fn verification_method(&self) -> Self::VerificationMethod { + P::verification_method(self) + } +} + +pub trait VerifierT { + type Error; + + fn verify(&self, proof: &P, key: &K) -> Result<(), Self::Error>; +} From 2c73faade75a185c68918bd2cf51f4079c9ced5a Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Tue, 5 Mar 2024 16:53:46 +0100 Subject: [PATCH 06/10] create vc example --- examples/0_basic/5_create_vc.rs | 28 ++++--------- identity_credential/src/credential/jwt.rs | 10 ++++- .../src/credential/vc1_1/credential.rs | 40 +++++++++++++++++++ identity_did/src/did_url.rs | 8 ++-- identity_resolver/src/lib.rs | 9 ++++- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index 3a14e262e2..ef686bc294 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -9,15 +9,14 @@ //! //! cargo run --release --example 5_create_vc +use anyhow::anyhow; use examples::create_did; use examples::MemStorage; use identity_eddsa_verifier::EdDSAJwsVerifier; -use identity_iota::core::Object; - -use identity_iota::credential::DecodedJwtCredential; use identity_iota::credential::Jwt; -use identity_iota::credential::JwtCredentialValidationOptions; -use identity_iota::credential::JwtCredentialValidator; +use identity_iota::credential::JwtCredential; +use identity_iota::credential::ValidableCredential; +use identity_iota::resolver::IotaResolver; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; @@ -35,7 +34,6 @@ use identity_iota::core::FromJson; use identity_iota::core::Url; use identity_iota::credential::Credential; use identity_iota::credential::CredentialBuilder; -use identity_iota::credential::FailFast; use identity_iota::credential::Subject; use identity_iota::did::DID; use identity_iota::iota::IotaDocument; @@ -97,24 +95,12 @@ async fn main() -> anyhow::Result<()> { ) .await?; - // Before sending this credential to the holder the issuer wants to validate that some properties - // of the credential satisfy their expectations. - - // Validate the credential's signature using the issuer's DID Document, the credential's semantic structure, - // that the issuance date is not in the future and that the expiration date is not in the past: - let decoded_credential: DecodedJwtCredential = - JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()) - .validate::<_, Object>( - &credential_jwt, - &issuer_document, - &JwtCredentialValidationOptions::default(), - FailFast::FirstError, - ) - .unwrap(); + let credential_jwt = JwtCredential::::parse(credential_jwt)?; + credential_jwt.validate(&IotaResolver::new(client), &EdDSAJwsVerifier::default()).await.map_err(|_| anyhow!("oops"))?; println!("VC successfully validated"); - println!("Credential JSON > {:#}", decoded_credential.credential); + println!("Credential JSON > {:#}", credential_jwt.as_ref()); Ok(()) } diff --git a/identity_credential/src/credential/jwt.rs b/identity_credential/src/credential/jwt.rs index c2c215af4c..8631313e06 100644 --- a/identity_credential/src/credential/jwt.rs +++ b/identity_credential/src/credential/jwt.rs @@ -130,7 +130,7 @@ pub enum JwtCredentialError { #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] #[repr(transparent)] #[serde(try_from = "i64", into = "i64")] -struct UnixTimestampWrapper(Timestamp); +pub(crate) struct UnixTimestampWrapper(Timestamp); impl Deref for UnixTimestampWrapper { type Target = Timestamp; @@ -166,12 +166,14 @@ pub struct JwtCredentialClaims { /// Represents the issuer. pub iss: Issuer, /// Represents the issuanceDate encoded as a UNIX timestamp. + #[serde(flatten)] issuance_date: IssuanceDate, /// Represents the id property of the credential. pub jti: Url, /// Represents the subject's id. pub sub: Option, pub vc: Object, + #[serde(flatten, skip_serializing_if = "Option::is_none")] pub custom: Option, } @@ -332,3 +334,9 @@ where Ok(()) } } + +impl AsRef for JwtCredential { + fn as_ref(&self) -> &C { + &self.credential + } +} \ No newline at end of file diff --git a/identity_credential/src/credential/vc1_1/credential.rs b/identity_credential/src/credential/vc1_1/credential.rs index b13a74505c..2c1d3d15e1 100644 --- a/identity_credential/src/credential/vc1_1/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -20,6 +20,7 @@ use crate::credential::CredentialBuilder; use crate::credential::CredentialT; use crate::credential::Evidence; use crate::credential::Issuer; +use crate::credential::JwtCredentialClaims; use crate::credential::Policy; use crate::credential::RefreshService; use crate::credential::Schema; @@ -207,6 +208,45 @@ where } } +impl<'a> TryFrom<&'a JwtCredentialClaims> for Credential { + type Error = Error; + fn try_from(value: &'a JwtCredentialClaims) -> std::result::Result { + let JwtCredentialClaims { + exp, + iss, + jti, + sub, + vc, + custom, + .. + } = value; + let mut vc = vc.clone(); + vc.insert("issuer".to_owned(), serde_json::Value::String(iss.url().to_string())); + vc.insert("id".to_owned(), serde_json::Value::String(jti.to_string())); + vc.insert( + "issuanceDate".to_owned(), + serde_json::Value::String(value.issuance_date().to_string()), + ); + if let Some(exp) = exp.as_deref() { + vc.insert("expirationDate".to_owned(), serde_json::Value::String(exp.to_string())); + } + if let Some(sub) = sub { + vc.entry("credentialSubject".to_owned()) + .or_insert(serde_json::json!({})) + .as_object_mut() + .unwrap() + .insert("id".to_owned(), serde_json::Value::String(sub.to_string())); + } + if let Some(custom) = custom { + for (key, value) in custom { + vc.insert(key.clone(), value.clone()); + } + } + let vc = serde_json::to_value(vc).expect("out of memory"); + serde_json::from_value(vc).map_err(|e| Error::JwtClaimsSetDeserializationError(Box::new(e))) + } +} + #[cfg(test)] mod tests { use identity_core::convert::FromJson; diff --git a/identity_did/src/did_url.rs b/identity_did/src/did_url.rs index b030bc2e70..64070e4d6e 100644 --- a/identity_did/src/did_url.rs +++ b/identity_did/src/did_url.rs @@ -268,12 +268,10 @@ impl Hash for RelativeDIDUrl { } } -impl TryFrom> for DIDUrl { +impl TryFrom for DIDUrl { type Error = Error; - fn try_from(value: Option) -> Result { - value - .ok_or(Error::Other("Missing URL")) - .and_then(|url| Self::parse(url.as_str())) + fn try_from(url: Url) -> Result { + Self::parse(url.as_str()) } } diff --git a/identity_resolver/src/lib.rs b/identity_resolver/src/lib.rs index ab01987a09..301839c727 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -43,6 +43,12 @@ impl Deref for IotaResolver { } } +impl IotaResolver { + pub const fn new(client: Client) -> Self { + Self(client) + } +} + impl ResolverT for IotaResolver { type Input = DIDUrl; type Error = IotaError; @@ -55,7 +61,8 @@ impl ResolverT for IotaResolver { .resolve_method(input, None) .map(|method| method.data()) .cloned() - .ok_or(todo!("an error for verification method not found"))?; + // TODO: use a better error + .ok_or(IotaError::InvalidNetworkName("invalid method data".to_owned()))?; Ok(key) } From e9b98fdcd8253555c472897a4741e43b73adc655 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 6 Mar 2024 16:34:26 +0100 Subject: [PATCH 07/10] AnyCredential --- examples/0_basic/5_create_vc.rs | 1 + .../src/credential/any_credential.rs | 96 +++++++++++++++++++ identity_credential/src/credential/mod.rs | 4 +- .../src/credential/vc1_1/credential.rs | 19 +++- .../src/credential/vc2_0/credential.rs | 61 +++++++++++- 5 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 identity_credential/src/credential/any_credential.rs diff --git a/examples/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index ef686bc294..6f9e4a904a 100644 --- a/examples/0_basic/5_create_vc.rs +++ b/examples/0_basic/5_create_vc.rs @@ -97,6 +97,7 @@ async fn main() -> anyhow::Result<()> { let credential_jwt = JwtCredential::::parse(credential_jwt)?; credential_jwt.validate(&IotaResolver::new(client), &EdDSAJwsVerifier::default()).await.map_err(|_| anyhow!("oops"))?; + println!("{}", serde_json::to_string(&credential_jwt).unwrap()); println!("VC successfully validated"); diff --git a/identity_credential/src/credential/any_credential.rs b/identity_credential/src/credential/any_credential.rs new file mode 100644 index 0000000000..a72b64b21a --- /dev/null +++ b/identity_credential/src/credential/any_credential.rs @@ -0,0 +1,96 @@ +use std::error::Error as StdError; +use std::str::FromStr; + +use identity_core::convert::FromJson; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "sd-jwt")] +use super::sd_jwt::SdJwtCredential; +use super::vc2_0::Vc2_0; +use super::{Credential as Vc1_1, Jwt, JwtCredential, JwtCredentialClaims}; + +#[derive(Debug, thiserror::Error)] +#[error("Failed to parse into any credential")] +pub struct Error(#[source] Box); + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AnyCredentialModel { + Vc1_1(Vc1_1), + Vc2_0(Vc2_0), +} + +impl<'a> TryFrom<&'a JwtCredentialClaims> for AnyCredentialModel { + type Error = (); // TODO: proper error + fn try_from(value: &'a JwtCredentialClaims) -> Result { + Vc1_1::try_from(value) + .map(Self::Vc1_1) + .map_err(|_| ()) + .or(Vc2_0::try_from(value).map(Self::Vc2_0).map_err(|_| ())) + } +} + +#[derive(Debug)] +pub enum AnyCredential { + Plaintext(AnyCredentialModel), + Jwt(JwtCredential), + #[cfg(feature = "sd-jwt")] + SdJwt(SdJwtCredential), +} + +impl AnyCredential { + pub fn parse_plaintext(s: &str) -> Result { + AnyCredentialModel::from_json(s) + .map(Self::Plaintext) + .map_err(|e| Error(e.into())) + } + pub fn parse_jwt(s: &str) -> Result { + s.parse::() + .map_err(|e| Error(e.into())) + .and_then(|jwt| JwtCredential::try_from(jwt).map_err(|e| Error(e.into()))) + .map(Self::Jwt) + } + #[cfg(feature = "sd-jwt")] + pub fn parse_sd_jwt(s: &str) -> Result { + use sd_jwt_payload::SdJwt; + + SdJwt::parse(s) + .map_err(|e| Error(e.into())) + .and_then(|sd_jwt| SdJwtCredential::try_from(sd_jwt).map_err(|_| todo!("error handling"))) + .map(Self::SdJwt) + } +} + +impl FromStr for AnyCredential { + type Err = Error; + fn from_str(s: &str) -> Result { + let sd_jwt = if cfg!(feature = "sd-jwt") { + AnyCredential::parse_sd_jwt(s) + } else { + todo!("proper error handling") + }; + sd_jwt + .or(AnyCredential::parse_plaintext(s)) + .or(AnyCredential::parse_jwt(s)) + } +} + +#[cfg(test)] +mod tests { + use crate::credential::{AnyCredential, AnyCredentialModel}; + + const JSON1: &str = include_str!("../../tests/fixtures/credential-1.json"); + const JWT_VC1_1: &str = "eyJraWQiOiJkaWQ6aW90YTpzbmQ6MHhhY2Q0MTQzYjVjNzg5NWUzMDRlNjQyYTEyNWQwOWFlNTNlMjNiY2U3NWZmMGYwZGFiNzNiY2FmYjZjYmUxMjAxI2dJT1ZPSHlQM25BN3c4Yl9NMTFhcVVYaXBqSnc0ZzVtWnF0ZlJKa1IzWFUiLCJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSJ9.eyJpc3MiOiJkaWQ6aW90YTpzbmQ6MHhhY2Q0MTQzYjVjNzg5NWUzMDRlNjQyYTEyNWQwOWFlNTNlMjNiY2U3NWZmMGYwZGFiNzNiY2FmYjZjYmUxMjAxIiwibmJmIjoxNzA5NzI4NDA3LCJqdGkiOiJodHRwczovL2V4YW1wbGUuZWR1L2NyZWRlbnRpYWxzLzM3MzIiLCJzdWIiOiJkaWQ6aW90YTpzbmQ6MHg5YzVhMGQyMTUxOWYxMjhlZDAwOTNiNDBiMjVhM2ZjMWFhOGNjZDQ3ZTA1ZDczMjlkY2Q1M2I2ZWY5OTAwZGM1IiwidmMiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJHUEEiOiI0LjAiLCJkZWdyZWUiOnsibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMiLCJ0eXBlIjoiQmFjaGVsb3JEZWdyZWUifSwibmFtZSI6IkFsaWNlIn19fQ.8URsHoPW6xl1ic66Vq5iUEG5s-IVuQvFilR_olgeuip-0L2_myATHmrk1iBvPLtZvCyjChzzXq1pe9e0qYv5DA"; + + #[test] + fn vc1_1_deserialization() { + let cred = JSON1.parse::().unwrap(); + assert!(matches!(cred, AnyCredential::Plaintext(AnyCredentialModel::Vc1_1(_)))); + } + #[test] + fn jwt_vc1_1_deserialization() { + let cred = JWT_VC1_1.parse::().unwrap(); + let AnyCredential::Jwt(jwt) = cred else { panic!("WOOT") }; + assert!(matches!(jwt.as_ref(), &AnyCredentialModel::Vc1_1(_))); + } +} diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index 260932a004..c0de80b6a9 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -9,6 +9,7 @@ pub mod common; mod jws; mod jwt; mod jwt_serialization; +mod any_credential; #[cfg(feature = "revocation-bitmap")] mod revocation_bitmap_status; #[cfg(feature = "sd-jwt")] @@ -33,8 +34,9 @@ pub use traits::*; pub use vc1_1::Credential; pub use vc1_1::CredentialBuilder; pub use vc1_1::Status; +pub use any_credential::*; #[cfg(feature = "validator")] pub(crate) use self::jwt_serialization::CredentialJwtClaims; #[cfg(feature = "presentation")] -pub(crate) use self::jwt_serialization::IssuanceDateClaims; +pub(crate) use self::jwt_serialization::IssuanceDateClaims; \ No newline at end of file diff --git a/identity_credential/src/credential/vc1_1/credential.rs b/identity_credential/src/credential/vc1_1/credential.rs index 2c1d3d15e1..738c2010d7 100644 --- a/identity_credential/src/credential/vc1_1/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -3,11 +3,14 @@ use core::fmt::Display; use core::fmt::Formatter; +use std::ops::Deref; use identity_core::convert::ToJson; use once_cell::sync::Lazy; use serde::Deserialize; +use serde::Deserializer; use serde::Serialize; +use serde::de::Error as _; use identity_core::common::Context; use identity_core::common::Object; @@ -35,11 +38,23 @@ use crate::credential::jwt_serialization::CredentialJwtClaims; static BASE_CONTEXT: Lazy = Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/2018/credentials/v1").unwrap())); +fn deserialize_vc1_1_context<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let ctx = OneOrMany::::deserialize(deserializer)?; + if ctx.contains(&BASE_CONTEXT) { + Ok(ctx) + } else { + Err(D::Error::custom("Missing base context")) + } +} + /// Represents a set of claims describing an entity. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct Credential { /// The JSON-LD context(s) applicable to the `Credential`. - #[serde(rename = "@context")] + #[serde(rename = "@context", deserialize_with = "deserialize_vc1_1_context")] pub context: OneOrMany, /// A unique `URI` that may be used to identify the `Credential`. #[serde(skip_serializing_if = "Option::is_none")] @@ -127,7 +142,7 @@ impl Credential { /// Returns a new `Credential` based on the `CredentialBuilder` configuration. pub fn from_builder(builder: CredentialBuilder) -> Result { let this: Self = Self { - context: builder.context.into(), + context: OneOrMany::from(builder.context), id: builder.id, types: builder.types.into(), credential_subject: builder.subject.into(), diff --git a/identity_credential/src/credential/vc2_0/credential.rs b/identity_credential/src/credential/vc2_0/credential.rs index 3f7e0f8529..f4ba66edcd 100644 --- a/identity_credential/src/credential/vc2_0/credential.rs +++ b/identity_credential/src/credential/vc2_0/credential.rs @@ -3,22 +3,42 @@ use identity_core::common::Object; use identity_core::common::OneOrMany; use identity_core::common::Timestamp; use identity_core::common::Url; +use once_cell::sync::Lazy; use serde::Deserialize; +use serde::Deserializer; +use serde::de::Error as _; use serde::Serialize; use crate::credential::Evidence; use crate::credential::Issuer; +use crate::credential::JwtCredentialClaims; use crate::credential::Policy; use crate::credential::Proof; use crate::credential::RefreshService; use crate::credential::Schema; use crate::credential::Status; use crate::credential::Subject; +use crate::Error; + +static BASE_CONTEXT: Lazy = + Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/ns/credentials/v2").unwrap())); + +fn deserialize_vc2_0_context<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let ctx = OneOrMany::::deserialize(deserializer)?; + if ctx.contains(&BASE_CONTEXT) { + Ok(ctx) + } else { + Err(D::Error::custom("Missing base context")) + } +} #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct Credential { /// The JSON-LD context(s) applicable to the `Credential`. - #[serde(rename = "@context")] + #[serde(rename = "@context", deserialize_with = "deserialize_vc2_0_context")] pub context: OneOrMany, /// A unique `URI` that may be used to identify the `Credential`. #[serde(skip_serializing_if = "Option::is_none")] @@ -63,3 +83,42 @@ pub struct Credential { #[serde(skip_serializing_if = "Option::is_none")] pub proof: Option, } + +impl<'a> TryFrom<&'a JwtCredentialClaims> for Credential { + type Error = Error; + fn try_from(value: &'a JwtCredentialClaims) -> std::result::Result { + let JwtCredentialClaims { + exp, + iss, + jti, + sub, + vc, + custom, + .. + } = value; + let mut vc = vc.clone(); + vc.insert("issuer".to_owned(), serde_json::Value::String(iss.url().to_string())); + vc.insert("id".to_owned(), serde_json::Value::String(jti.to_string())); + vc.insert( + "validFrom".to_owned(), + serde_json::Value::String(value.issuance_date().to_string()), + ); + if let Some(exp) = exp.as_deref() { + vc.insert("validUntil".to_owned(), serde_json::Value::String(exp.to_string())); + } + if let Some(sub) = sub { + vc.entry("credentialSubject".to_owned()) + .or_insert(serde_json::json!({})) + .as_object_mut() + .unwrap() + .insert("id".to_owned(), serde_json::Value::String(sub.to_string())); + } + if let Some(custom) = custom { + for (key, value) in custom { + vc.insert(key.clone(), value.clone()); + } + } + let vc = serde_json::to_value(vc).expect("out of memory"); + serde_json::from_value(vc).map_err(|e| Error::JwtClaimsSetDeserializationError(Box::new(e))) + } +} \ No newline at end of file From f822fb9346c0811b0c0eb299a056543a39f44e2e Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Wed, 6 Mar 2024 17:23:15 +0100 Subject: [PATCH 08/10] StatusList2021's resolver --- identity_credential/src/credential/mod.rs | 4 -- .../revocation/revocation_bitmap_2022/mod.rs | 2 + .../revocation_bitmap_2022/status.rs} | 0 .../src/revocation/status_list_2021/entry.rs | 9 +++++ .../src/revocation/status_list_2021/mod.rs | 2 + .../revocation/status_list_2021/resolver.rs | 40 +++++++++++++++++++ identity_credential/src/revocation/traits.rs | 15 +++---- .../jwt_credential_validator_utils.rs | 7 ++-- 8 files changed, 64 insertions(+), 15 deletions(-) rename identity_credential/src/{credential/revocation_bitmap_status.rs => revocation/revocation_bitmap_2022/status.rs} (100%) create mode 100644 identity_credential/src/revocation/status_list_2021/resolver.rs diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index c0de80b6a9..d26e2b33d0 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -10,8 +10,6 @@ mod jws; mod jwt; mod jwt_serialization; mod any_credential; -#[cfg(feature = "revocation-bitmap")] -mod revocation_bitmap_status; #[cfg(feature = "sd-jwt")] pub mod sd_jwt; mod traits; @@ -19,8 +17,6 @@ pub mod vc1_1; pub mod vc2_0; pub use self::jws::Jws; -#[cfg(feature = "revocation-bitmap")] -pub use self::revocation_bitmap_status::RevocationBitmapStatus; pub use common::Evidence; pub use common::Issuer; pub use common::LinkedDomainService; diff --git a/identity_credential/src/revocation/revocation_bitmap_2022/mod.rs b/identity_credential/src/revocation/revocation_bitmap_2022/mod.rs index 609cba5277..dc55b362da 100644 --- a/identity_credential/src/revocation/revocation_bitmap_2022/mod.rs +++ b/identity_credential/src/revocation/revocation_bitmap_2022/mod.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 mod bitmap; +mod status; mod document_ext; pub use bitmap::*; +pub use status::*; pub use document_ext::*; diff --git a/identity_credential/src/credential/revocation_bitmap_status.rs b/identity_credential/src/revocation/revocation_bitmap_2022/status.rs similarity index 100% rename from identity_credential/src/credential/revocation_bitmap_status.rs rename to identity_credential/src/revocation/revocation_bitmap_2022/status.rs diff --git a/identity_credential/src/revocation/status_list_2021/entry.rs b/identity_credential/src/revocation/status_list_2021/entry.rs index 7eecf2f28e..476ef8de29 100644 --- a/identity_credential/src/revocation/status_list_2021/entry.rs +++ b/identity_credential/src/revocation/status_list_2021/entry.rs @@ -8,8 +8,10 @@ use serde::Deserialize; use serde::Serialize; use crate::credential::Status; +use crate::revocation::StatusT; use super::credential::StatusPurpose; +use super::CredentialStatus; const CREDENTIAL_STATUS_TYPE: &str = "StatusList2021Entry"; @@ -65,6 +67,13 @@ impl From for Status { } } +impl StatusT for StatusList2021Entry { + type State = CredentialStatus; + fn type_(&self) -> &str { + CREDENTIAL_STATUS_TYPE + } +} + impl StatusList2021Entry { /// Creates a new [`StatusList2021Entry`]. pub fn new(status_list: Url, purpose: StatusPurpose, index: usize, id: Option) -> Self { diff --git a/identity_credential/src/revocation/status_list_2021/mod.rs b/identity_credential/src/revocation/status_list_2021/mod.rs index 39c4dfbcf4..7e8ea2302b 100644 --- a/identity_credential/src/revocation/status_list_2021/mod.rs +++ b/identity_credential/src/revocation/status_list_2021/mod.rs @@ -7,7 +7,9 @@ mod credential; mod entry; mod status_list; +mod resolver; pub use credential::*; pub use entry::*; pub use status_list::*; +pub use resolver::*; diff --git a/identity_credential/src/revocation/status_list_2021/resolver.rs b/identity_credential/src/revocation/status_list_2021/resolver.rs new file mode 100644 index 0000000000..a77d1ad435 --- /dev/null +++ b/identity_credential/src/revocation/status_list_2021/resolver.rs @@ -0,0 +1,40 @@ +use identity_core::{common::Url, ResolverT}; + +use crate::revocation::StatusResolverT; + +use super::{StatusList2021Credential, StatusList2021Entry}; + +#[derive(Clone, Debug)] +pub struct StatusList2021Resolver(R); + +impl StatusList2021Resolver { + pub const fn new(resolver: R) -> Self { + Self(resolver) + } +} + +impl StatusResolverT for StatusList2021Resolver +where + R: ResolverT, + R::Input: TryFrom, +{ + type Error = (); + type Status = StatusList2021Entry; + async fn state<'c, S>( + &self, + status: &'c S, + ) -> Result<::State, Self::Error> + where + Self::Status: TryFrom<&'c S>, + { + // Convert the provided status into a status we can work with. + let status = Self::Status::try_from(status).map_err(|_| ())?; + // Get the StatusList2021Credential's URL and convert it to something the resolver can work with. + let credential_location = R::Input::try_from(status.status_list_credential().clone()).map_err(|_| ())?; + // Fetch the credential. + let credential = self.0.fetch(&credential_location).await.map_err(|_| ())?; + + // Return the entry specified in status + credential.entry(status.index()).map_err(|_| ()) + } +} diff --git a/identity_credential/src/revocation/traits.rs b/identity_credential/src/revocation/traits.rs index 400749ec00..da2b52c6e3 100644 --- a/identity_credential/src/revocation/traits.rs +++ b/identity_credential/src/revocation/traits.rs @@ -13,19 +13,20 @@ pub trait StatusT { fn type_(&self) -> &str; } -pub trait StatusResolverT { +pub trait StatusResolverT { type Error; + type Status: StatusT; - async fn state<'c, S1>(&self, status: &'c S1) -> Result + async fn state<'c, S>(&self, status: &'c S) -> Result<::State, Self::Error> where - S: TryFrom<&'c S1>; + Self::Status: TryFrom<&'c S>; } pub trait ValidableCredentialStatusExt where Self: ValidableCredential + StatusCredentialT, { - async fn validate_with_status<'c, S, SR, F>( + async fn validate_with_status<'c, SR, F>( &'c self, resolver: &R, verifier: &V, @@ -33,9 +34,9 @@ where state_predicate: F, ) -> Result<(), ()> where - SR: StatusResolverT, - S: StatusT + TryFrom<&'c Self::Status>, - F: FnOnce(&S::State) -> bool, + SR: StatusResolverT, + SR::Status: StatusT + TryFrom<&'c Self::Status>, + F: FnOnce(&::State) -> bool, { self.validate(resolver, verifier).await?; let Some(status) = self.status() else { diff --git a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs index e7a43bcdab..56e55d35a2 100644 --- a/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs +++ b/identity_credential/src/validator/jwt_credential_validation/jwt_credential_validator_utils.rs @@ -153,9 +153,8 @@ impl JwtCredentialValidatorUtils { status.type_ )))); } - let status: crate::credential::RevocationBitmapStatus = - crate::credential::RevocationBitmapStatus::try_from(status.clone()) - .map_err(JwtValidationError::InvalidStatus)?; + let status = crate::revocation::RevocationBitmapStatus::try_from(status.clone()) + .map_err(JwtValidationError::InvalidStatus)?; // Check the credential index against the issuer's DID Document. let issuer_did: CoreDID = Self::extract_issuer(credential)?; @@ -173,7 +172,7 @@ impl JwtCredentialValidatorUtils { #[cfg(feature = "revocation-bitmap")] fn check_revocation_bitmap_status + ?Sized>( issuer: &DOC, - status: crate::credential::RevocationBitmapStatus, + status: crate::revocation::RevocationBitmapStatus, ) -> ValidationUnitResult { use crate::revocation::RevocationDocumentExt; From 8a6a9893bba7bf176ef94e5769c62b7a7f5a8073 Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 7 Mar 2024 12:26:51 +0100 Subject: [PATCH 09/10] CredentialT's generic ID --- identity_credential/src/credential/jwt.rs | 5 +++-- identity_credential/src/credential/sd_jwt/credential.rs | 3 ++- identity_credential/src/credential/traits.rs | 4 ++-- identity_credential/src/credential/vc1_1/credential.rs | 9 ++++++--- identity_credential/src/credential/vc2_0/credential.rs | 4 +++- .../src/storage/tests/credential_validation.rs | 2 +- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/identity_credential/src/credential/jwt.rs b/identity_credential/src/credential/jwt.rs index 8631313e06..69cef1dc5c 100644 --- a/identity_credential/src/credential/jwt.rs +++ b/identity_credential/src/credential/jwt.rs @@ -169,7 +169,7 @@ pub struct JwtCredentialClaims { #[serde(flatten)] issuance_date: IssuanceDate, /// Represents the id property of the credential. - pub jti: Url, + pub jti: Option, /// Represents the subject's id. pub sub: Option, pub vc: Object, @@ -279,8 +279,9 @@ where impl CredentialT for JwtCredential { type Claim = JwtCredentialClaims; type Issuer = Issuer; + type Id = Option; - fn id(&self) -> &Url { + fn id(&self) -> &Self::Id { &self.parsed_claims.jti } fn issuer(&self) -> &Self::Issuer { diff --git a/identity_credential/src/credential/sd_jwt/credential.rs b/identity_credential/src/credential/sd_jwt/credential.rs index 580be9c1af..41a7c0c147 100644 --- a/identity_credential/src/credential/sd_jwt/credential.rs +++ b/identity_credential/src/credential/sd_jwt/credential.rs @@ -118,8 +118,9 @@ where impl CredentialT for SdJwtCredential { type Claim = JwtCredentialClaims; type Issuer = Issuer; + type Id = Option; - fn id(&self) -> &Url { + fn id(&self) -> &Self::Id { self.jwt_credential.id() } fn issuer(&self) -> &Self::Issuer { diff --git a/identity_credential/src/credential/traits.rs b/identity_credential/src/credential/traits.rs index 45446ff9e5..33cdb1e155 100644 --- a/identity_credential/src/credential/traits.rs +++ b/identity_credential/src/credential/traits.rs @@ -1,11 +1,11 @@ use identity_core::common::Timestamp; -use identity_core::common::Url; pub trait CredentialT { type Issuer; type Claim; + type Id; - fn id(&self) -> &Url; + fn id(&self) -> &Self::Id; fn issuer(&self) -> &Self::Issuer; fn claim(&self) -> &Self::Claim; fn valid_from(&self) -> Timestamp; diff --git a/identity_credential/src/credential/vc1_1/credential.rs b/identity_credential/src/credential/vc1_1/credential.rs index 738c2010d7..633cb01ef1 100644 --- a/identity_credential/src/credential/vc1_1/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -103,9 +103,10 @@ pub struct Credential { impl CredentialT for Credential { type Claim = OneOrMany; type Issuer = Issuer; + type Id = Option; - fn id(&self) -> &Url { - self.id.as_ref().unwrap() + fn id(&self) -> &Self::Id { + &self.id } fn claim(&self) -> &Self::Claim { &self.credential_subject @@ -237,11 +238,13 @@ impl<'a> TryFrom<&'a JwtCredentialClaims> for Credential { } = value; let mut vc = vc.clone(); vc.insert("issuer".to_owned(), serde_json::Value::String(iss.url().to_string())); - vc.insert("id".to_owned(), serde_json::Value::String(jti.to_string())); vc.insert( "issuanceDate".to_owned(), serde_json::Value::String(value.issuance_date().to_string()), ); + if let Some(jti) = jti { + vc.insert("id".to_owned(), serde_json::Value::String(jti.to_string())); + } if let Some(exp) = exp.as_deref() { vc.insert("expirationDate".to_owned(), serde_json::Value::String(exp.to_string())); } diff --git a/identity_credential/src/credential/vc2_0/credential.rs b/identity_credential/src/credential/vc2_0/credential.rs index f4ba66edcd..eb36e34c61 100644 --- a/identity_credential/src/credential/vc2_0/credential.rs +++ b/identity_credential/src/credential/vc2_0/credential.rs @@ -98,7 +98,9 @@ impl<'a> TryFrom<&'a JwtCredentialClaims> for Credential { } = value; let mut vc = vc.clone(); vc.insert("issuer".to_owned(), serde_json::Value::String(iss.url().to_string())); - vc.insert("id".to_owned(), serde_json::Value::String(jti.to_string())); + if let Some(jti) = jti { + vc.insert("id".to_owned(), serde_json::Value::String(jti.to_string())); + } vc.insert( "validFrom".to_owned(), serde_json::Value::String(value.issuance_date().to_string()), diff --git a/identity_storage/src/storage/tests/credential_validation.rs b/identity_storage/src/storage/tests/credential_validation.rs index b1b3d873c8..1fac707419 100644 --- a/identity_storage/src/storage/tests/credential_validation.rs +++ b/identity_storage/src/storage/tests/credential_validation.rs @@ -6,7 +6,7 @@ use identity_core::common::Object; use identity_core::common::Timestamp; use identity_core::common::Url; use identity_credential::credential::Jwt; -use identity_credential::credential::RevocationBitmapStatus; +use identity_credential::revocation::RevocationBitmapStatus; use identity_credential::credential::Status; use identity_credential::revocation::RevocationBitmap; use identity_credential::revocation::RevocationDocumentExt; From d3a6a028fd0b24abeffb60b90cbedf1e207709eb Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Thu, 7 Mar 2024 15:21:48 +0100 Subject: [PATCH 10/10] update status_list_2021 example --- examples/1_advanced/8_status_list_2021.rs | 90 ++++++++++--------- examples/Cargo.toml | 2 +- identity_core/src/lib.rs | 2 +- .../src/credential/vc1_1/credential.rs | 10 ++- identity_credential/src/revocation/traits.rs | 2 +- identity_iota/src/lib.rs | 1 + 6 files changed, 63 insertions(+), 44 deletions(-) diff --git a/examples/1_advanced/8_status_list_2021.rs b/examples/1_advanced/8_status_list_2021.rs index 0a70690e91..6cefd83a88 100644 --- a/examples/1_advanced/8_status_list_2021.rs +++ b/examples/1_advanced/8_status_list_2021.rs @@ -1,34 +1,34 @@ // Copyright 2020-2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use anyhow::anyhow; use examples::create_did; use examples::random_stronghold_path; use examples::MemStorage; use examples::API_ENDPOINT; use identity_eddsa_verifier::EdDSAJwsVerifier; use identity_iota::core::FromJson; -use identity_iota::core::Object; +use identity_iota::core::ResolverT; use identity_iota::core::ToJson; use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::CredentialStatus; use identity_iota::credential::status_list_2021::StatusList2021; use identity_iota::credential::status_list_2021::StatusList2021Credential; use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder; use identity_iota::credential::status_list_2021::StatusList2021Entry; +use identity_iota::credential::status_list_2021::StatusList2021Resolver; use identity_iota::credential::status_list_2021::StatusPurpose; use identity_iota::credential::Credential; use identity_iota::credential::CredentialBuilder; -use identity_iota::credential::FailFast; use identity_iota::credential::Issuer; use identity_iota::credential::Jwt; -use identity_iota::credential::JwtCredentialValidationOptions; -use identity_iota::credential::JwtCredentialValidator; -use identity_iota::credential::JwtCredentialValidatorUtils; -use identity_iota::credential::JwtValidationError; +use identity_iota::credential::JwtCredential; use identity_iota::credential::Status; -use identity_iota::credential::StatusCheck; use identity_iota::credential::Subject; +use identity_iota::credential::ValidableCredentialStatusExt; use identity_iota::did::DID; use identity_iota::iota::IotaDocument; +use identity_iota::resolver::IotaResolver; use identity_iota::storage::JwkDocumentExt; use identity_iota::storage::JwkMemStore; use identity_iota::storage::JwsSignatureOptions; @@ -40,6 +40,29 @@ use iota_sdk::client::Password; use iota_sdk::types::block::address::Address; use serde_json::json; +struct MockStatusListClient(StatusList2021Credential); + +impl MockStatusListClient { + pub fn new(status_list: StatusList2021Credential) -> Self { + Self(status_list) + } + pub async fn get(&self, url: &Url) -> Option { + if self.0.id().is_some_and(|id| id == url) { + Some(self.0.clone()) + } else { + None + } + } +} + +impl ResolverT for MockStatusListClient { + type Error = (); + type Input = Url; + async fn fetch(&self, input: &Self::Input) -> Result { + self.get(input).await.ok_or(()) + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { // =========================================================================== @@ -52,6 +75,9 @@ async fn main() -> anyhow::Result<()> { .finish() .await?; + let iota_resolver = IotaResolver::new(client.clone()); + let eddsa_verifier = EdDSAJwsVerifier::default(); + let mut secret_manager_issuer: SecretManager = SecretManager::Stronghold( StrongholdSecretManager::builder() .password(Password::from("secure_password_1".to_owned())) @@ -130,26 +156,15 @@ async fn main() -> anyhow::Result<()> { ) .await?; - let validator: JwtCredentialValidator = - JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); - - // The validator has no way of retriving the status list to check for the - // revocation of the credential. Let's skip that pass and perform the operation manually. - let mut validation_options = JwtCredentialValidationOptions::default(); - validation_options.status = StatusCheck::SkipUnsupported; - // Validate the credential's signature using the issuer's DID Document. - validator.validate::<_, Object>( - &credential_jwt, - &issuer_document, - &validation_options, - FailFast::FirstError, - )?; - // Check manually for revocation - JwtCredentialValidatorUtils::check_status_with_status_list_2021( - &credential, - &status_list_credential, - StatusCheck::Strict, - )?; + let status_list_resolver = StatusList2021Resolver::new(MockStatusListClient::new(status_list_credential.clone())); + + let jwt_credential = JwtCredential::::parse(credential_jwt)?; + jwt_credential + .validate_with_status(&iota_resolver, &eddsa_verifier, &status_list_resolver, |state| { + *state == CredentialStatus::Valid + }) + .await + .map_err(|_| anyhow!("ooops"))?; println!("Credential is valid."); let status_list_credential_json = status_list_credential.to_json().unwrap(); @@ -169,19 +184,14 @@ async fn main() -> anyhow::Result<()> { status_list_credential.set_credential_status(&mut credential, credential_index, true)?; // validate the credential and check for revocation - validator.validate::<_, Object>( - &credential_jwt, - &issuer_document, - &validation_options, - FailFast::FirstError, - )?; - let revocation_result = JwtCredentialValidatorUtils::check_status_with_status_list_2021( - &credential, - &status_list_credential, - StatusCheck::Strict, - ); - - assert!(revocation_result.is_err_and(|e| matches!(e, JwtValidationError::Revoked))); + let status_list_resolver = StatusList2021Resolver::new(MockStatusListClient::new(status_list_credential)); + + jwt_credential + .validate_with_status(&iota_resolver, &eddsa_verifier, &status_list_resolver, |state| { + *state == CredentialStatus::Valid + }) + .await + .unwrap_err(); println!("The credential has been successfully revoked."); Ok(()) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index a9337d6a33..30d204368f 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] anyhow = "1.0.62" identity_eddsa_verifier = { path = "../identity_eddsa_verifier", default-features = false } -identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage", "revocation-bitmap", "status-list-2021"] } +identity_iota = { path = "../identity_iota", default-features = false, features = ["memstore", "domain-linkage", "revocation-bitmap", "status-list-2021", "resolver"] } identity_stronghold = { path = "../identity_stronghold", default-features = false } iota-sdk = { version = "1.0", default-features = false, features = ["tls", "client", "stronghold"] } primitive-types = "0.12.1" diff --git a/identity_core/src/lib.rs b/identity_core/src/lib.rs index 0727c06d95..1efd298162 100644 --- a/identity_core/src/lib.rs +++ b/identity_core/src/lib.rs @@ -30,5 +30,5 @@ pub trait ResolverT { type Error; type Input; - async fn fetch(&self, input: &Self::Input) -> Result; + async fn fetch(&self, input: &Self::Input) -> std::result::Result; } \ No newline at end of file diff --git a/identity_credential/src/credential/vc1_1/credential.rs b/identity_credential/src/credential/vc1_1/credential.rs index 633cb01ef1..2f93f70a6d 100644 --- a/identity_credential/src/credential/vc1_1/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -7,10 +7,10 @@ use std::ops::Deref; use identity_core::convert::ToJson; use once_cell::sync::Lazy; +use serde::de::Error as _; use serde::Deserialize; use serde::Deserializer; use serde::Serialize; -use serde::de::Error as _; use identity_core::common::Context; use identity_core::common::Object; @@ -34,6 +34,7 @@ use crate::error::Result; use crate::credential::common::Proof; use crate::credential::jwt_serialization::CredentialJwtClaims; +use crate::revocation::StatusCredentialT; static BASE_CONTEXT: Lazy = Lazy::new(|| Context::Url(Url::parse("https://www.w3.org/2018/credentials/v1").unwrap())); @@ -265,6 +266,13 @@ impl<'a> TryFrom<&'a JwtCredentialClaims> for Credential { } } +impl StatusCredentialT for Credential { + type Status = Status; + fn status(&self) -> Option<&Self::Status> { + self.credential_status.as_ref() + } +} + #[cfg(test)] mod tests { use identity_core::convert::FromJson; diff --git a/identity_credential/src/revocation/traits.rs b/identity_credential/src/revocation/traits.rs index da2b52c6e3..3aad4c9da6 100644 --- a/identity_credential/src/revocation/traits.rs +++ b/identity_credential/src/revocation/traits.rs @@ -45,7 +45,7 @@ where let credential_state = status_resolver.state(status).await.map_err(|_| ())?; if !state_predicate(&credential_state) { - todo!("Non-valid state!") + Err(()) // TODO: return non-valid state } else { Ok(()) } diff --git a/identity_iota/src/lib.rs b/identity_iota/src/lib.rs index 24a20359eb..16937c3d87 100644 --- a/identity_iota/src/lib.rs +++ b/identity_iota/src/lib.rs @@ -23,6 +23,7 @@ pub mod core { pub use identity_core::common::*; pub use identity_core::convert::*; pub use identity_core::error::*; + pub use identity_core::ResolverT; #[doc(inline)] pub use identity_core::json;