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/0_basic/5_create_vc.rs b/examples/0_basic/5_create_vc.rs index 3a14e262e2..6f9e4a904a 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,13 @@ 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!("{}", serde_json::to_string(&credential_jwt).unwrap()); println!("VC successfully validated"); - println!("Credential JSON > {:#}", decoded_credential.credential); + println!("Credential JSON > {:#}", credential_jwt.as_ref()); Ok(()) } 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 242dacea88..30d204368f 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 @@ -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/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_core/src/lib.rs b/identity_core/src/lib.rs index b915fcdeba..1efd298162 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) -> std::result::Result; +} \ No newline at end of file diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 491158c1f1..af1718a323 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,20 +14,22 @@ 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_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"] } 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 } 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 } 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/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/jwt.rs b/identity_credential/src/credential/jwt.rs index c06d2207df..69cef1dc5c 100644 --- a/identity_credential/src/credential/jwt.rs +++ b/identity_credential/src/credential/jwt.rs @@ -1,33 +1,343 @@ // 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::Debug; +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 identity_verification::ProofT; +use identity_verification::VerifierT; 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 crate::revocation::StatusCredentialT; -impl Jwt { - /// Creates a new `Jwt` from the given string. - pub fn new(jwt_string: String) -> Self { - Self(jwt_string) +use super::CredentialT; +use super::Issuer; +use identity_core::ResolverT; +use super::ValidableCredential; + +#[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 DecodedJws { + pub fn claims(&self) -> &[u8] { + &self.raw_claims } +} + +impl ProofT for DecodedJws { + type VerificationMethod = Option; + + 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) -> Self::VerificationMethod { + self + .headers + .protected_header() + .and_then(|header| header.kid()) + .and_then(|kid| Url::parse(kid).ok()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)] +pub struct Jwt { + pub(crate) inner: String, + pub(crate) decoded_jws: DecodedJws, +} + +impl FromStr for Jwt { + type Err = JwtError; + fn from_str(s: &str) -> Result { + Self::parse(s.to_owned()) + } +} - /// Returns a reference of the JWT string. +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), + #[error("Failed to parse packed credential")] + CredentialUnpackingError, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[repr(transparent)] +#[serde(try_from = "i64", into = "i64")] +pub(crate) 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. + #[serde(flatten)] + issuance_date: IssuanceDate, + /// Represents the id property of the credential. + pub jti: Option, + /// Represents the subject's id. + pub sub: Option, + pub vc: Object, + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub custom: Option, +} + +impl JwtCredentialClaims { + pub fn issuance_date(&self) -> Timestamp { + match self.issuance_date { + IssuanceDate::Iat(t) => *t, + IssuanceDate::Nbf(t) => *t, + } + } +} + +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 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 String { - fn from(jwt: Jwt) -> Self { - jwt.0 +impl From> for Jwt +where + C: Into, +{ + fn from(value: JwtCredential) -> Self { + Jwt { + inner: value.inner, + decoded_jws: value.decoded_jws, + } } } + +impl CredentialT for JwtCredential { + type Claim = JwtCredentialClaims; + type Issuer = Issuer; + type Id = Option; + + fn id(&self) -> &Self::Id { + &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 JwtCredential +where + C: for<'a> TryFrom<&'a JwtCredentialClaims>, +{ + pub fn parse(jwt: Jwt) -> Result { + Self::try_from(jwt) + } +} + +impl StatusCredentialT for JwtCredential { + type Status = C::Status; + fn status(&self) -> Option<&Self::Status> { + self.credential.status() + } +} + +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(()) + } +} + +impl AsRef for JwtCredential { + fn as_ref(&self) -> &C { + &self.credential + } +} \ No newline at end of file diff --git a/identity_credential/src/credential/mod.rs b/identity_credential/src/credential/mod.rs index efa20a3c87..d26e2b33d0 100644 --- a/identity_credential/src/credential/mod.rs +++ b/identity_credential/src/credential/mod.rs @@ -5,40 +5,34 @@ #![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; +mod any_credential; +#[cfg(feature = "sd-jwt")] +pub mod sd_jwt; +mod traits; +pub mod vc1_1; +pub mod vc2_0; -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 jwt::*; +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/sd_jwt/credential.rs b/identity_credential/src/credential/sd_jwt/credential.rs new file mode 100644 index 0000000000..41a7c0c147 --- /dev/null +++ b/identity_credential/src/credential/sd_jwt/credential.rs @@ -0,0 +1,177 @@ +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; + type Id = Option; + + fn id(&self) -> &Self::Id { + 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 new file mode 100644 index 0000000000..33cdb1e155 --- /dev/null +++ b/identity_credential/src/credential/traits.rs @@ -0,0 +1,25 @@ +use identity_core::common::Timestamp; + +pub trait CredentialT { + type Issuer; + type Claim; + type Id; + + fn id(&self) -> &Self::Id; + 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()) + } +} + +type ValidationError = (); + +pub trait ValidableCredential: CredentialT { + async fn validate(&self, resolver: &R, verifier: &V) -> Result<(), ValidationError>; +} 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 68% rename from identity_credential/src/credential/credential.rs rename to identity_credential/src/credential/vc1_1/credential.rs index decbb8b7c2..2f93f70a6d 100644 --- a/identity_credential/src/credential/credential.rs +++ b/identity_credential/src/credential/vc1_1/credential.rs @@ -3,10 +3,13 @@ use core::fmt::Display; use core::fmt::Formatter; +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 identity_core::common::Context; @@ -17,8 +20,10 @@ 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::JwtCredentialClaims; use crate::credential::Policy; use crate::credential::RefreshService; use crate::credential::Schema; @@ -27,17 +32,30 @@ 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; +use crate::revocation::StatusCredentialT; 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")] @@ -83,6 +101,28 @@ pub struct Credential { pub proof: Option, } +impl CredentialT for Credential { + type Claim = OneOrMany; + type Issuer = Issuer; + type Id = Option; + + fn id(&self) -> &Self::Id { + &self.id + } + 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 Credential { /// Returns the base JSON-LD context. pub fn base_context() -> &'static Context { @@ -104,7 +144,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(), @@ -185,24 +225,72 @@ 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( + "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())); + } + 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))) + } +} + +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; 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/credential.rs b/identity_credential/src/credential/vc2_0/credential.rs new file mode 100644 index 0000000000..eb36e34c61 --- /dev/null +++ b/identity_credential/src/credential/vc2_0/credential.rs @@ -0,0 +1,126 @@ +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 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", 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")] + 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, +} + +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())); + 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()), + ); + 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 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..c1e5209535 --- /dev/null +++ b/identity_credential/src/credential/vc2_0/mod.rs @@ -0,0 +1,3 @@ +mod credential; + +pub use credential::Credential as Vc2_0; 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..38cd039898 100644 --- a/identity_credential/src/presentation/presentation_builder.rs +++ b/identity_credential/src/presentation/presentation_builder.rs @@ -156,13 +156,14 @@ mod tests { .build() .unwrap(); - let credential_jwt = Jwt::new(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 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(); 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 6732ff4194..45248ff7a9 100644 --- a/identity_credential/src/revocation/mod.rs +++ b/identity_credential/src/revocation/mod.rs @@ -8,7 +8,9 @@ mod error; 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; pub use revocation_bitmap_2022::*; +pub use traits::*; 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 new file mode 100644 index 0000000000..3aad4c9da6 --- /dev/null +++ b/identity_credential/src/revocation/traits.rs @@ -0,0 +1,55 @@ +use crate::credential::CredentialT; +use crate::credential::ValidableCredential; + +pub trait StatusCredentialT: CredentialT { + type Status; + + fn status(&self) -> Option<&Self::Status>; +} + +pub trait StatusT { + type State; + + fn type_(&self) -> &str; +} + +pub trait StatusResolverT { + type Error; + type Status: StatusT; + + async fn state<'c, S>(&self, status: &'c S) -> Result<::State, Self::Error> + where + Self::Status: TryFrom<&'c S>; +} + +pub trait ValidableCredentialStatusExt +where + Self: ValidableCredential + StatusCredentialT, +{ + async fn validate_with_status<'c, SR, F>( + &'c self, + resolver: &R, + verifier: &V, + status_resolver: &SR, + state_predicate: F, + ) -> Result<(), ()> + where + SR: StatusResolverT, + SR::Status: StatusT + TryFrom<&'c Self::Status>, + F: FnOnce(&::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) { + Err(()) // TODO: return non-valid state + } else { + Ok(()) + } + } +} + +impl ValidableCredentialStatusExt for C where C: ValidableCredential + StatusCredentialT {} 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; 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_did/src/did_url.rs b/identity_did/src/did_url.rs index 60c7d6c84e..64070e4d6e 100644 --- a/identity_did/src/did_url.rs +++ b/identity_did/src/did_url.rs @@ -268,6 +268,13 @@ impl Hash for RelativeDIDUrl { } } +impl TryFrom for DIDUrl { + type Error = Error; + fn try_from(url: Url) -> Result { + 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_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..cdabf409e9 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,8 @@ 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 } +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_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/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; 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_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_resolver/Cargo.toml b/identity_resolver/Cargo.toml index 20ec82cd1b..68fbfdf9d6 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,18 @@ 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 } +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 } [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_resolver/src/lib.rs b/identity_resolver/src/lib.rs index b773741b09..301839c727 100644 --- a/identity_resolver/src/lib.rs +++ b/identity_resolver/src/lib.rs @@ -17,7 +17,53 @@ 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 IotaResolver { + pub const fn new(client: Client) -> Self { + Self(client) + } +} + +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() + // TODO: use a better error + .ok_or(IotaError::InvalidNetworkName("invalid method data".to_owned()))?; + + Ok(key) + } +} 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_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_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; 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 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>; +}