From 3352f96b20c85efd8a9134cac595e0515945d65c Mon Sep 17 00:00:00 2001 From: Enrico Marconi Date: Mon, 18 Mar 2024 14:37:34 +0100 Subject: [PATCH] validation of StatusList2020-revoked credentials --- .../src/services/credential/validation.rs | 42 +++++++++-- .../grpc/src/services/status_list_2021.rs | 4 +- .../grpc/tests/api/credential_validation.rs | 69 +++++++++++++++++++ 3 files changed, 105 insertions(+), 10 deletions(-) diff --git a/bindings/grpc/src/services/credential/validation.rs b/bindings/grpc/src/services/credential/validation.rs index 5f309a41d0..5ddefecfd0 100644 --- a/bindings/grpc/src/services/credential/validation.rs +++ b/bindings/grpc/src/services/credential/validation.rs @@ -1,12 +1,15 @@ use identity_eddsa_verifier::EdDSAJwsVerifier; +use identity_iota::core::FromJson; use identity_iota::core::Object; use identity_iota::core::ToJson; +use identity_iota::credential::status_list_2021::StatusList2021Credential; use identity_iota::credential::FailFast; 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::StatusCheck; use identity_iota::iota::IotaDID; use identity_iota::resolver; use identity_iota::resolver::Resolver; @@ -16,6 +19,7 @@ use _credentials::vc_validation_server::VcValidation; use _credentials::vc_validation_server::VcValidationServer; use _credentials::VcValidationRequest; use _credentials::VcValidationResponse; +use tonic::Code; use tonic::Request; use tonic::Response; use tonic::Status; @@ -30,15 +34,24 @@ pub enum VcValidationError { JwtValidationError(#[from] JwtValidationError), #[error("DID resolution error")] DidResolutionError(#[source] resolver::Error), + #[error("Provided an invalid StatusList2021Credential")] + InvalidStatusList2020Credential(#[source] identity_iota::core::Error), #[error("The provided credential has been revoked")] RevokedCredential, #[error("The provided credential has expired")] ExpiredCredential, + #[error("The provided credential has been suspended")] + SuspendedCredential, } impl From for Status { fn from(error: VcValidationError) -> Self { - Status::unknown(error.to_string()) + let code = match &error { + VcValidationError::InvalidStatusList2020Credential(_) => Code::InvalidArgument, + _ => Code::Internal, + }; + + Status::new(code, error.to_string()) } } @@ -77,20 +90,35 @@ impl VcValidation for VcValidator { .await .map_err(VcValidationError::DidResolutionError)?; + let mut validation_option = JwtCredentialValidationOptions::default(); + if status_list_credential_json.is_some() { + validation_option = validation_option.status_check(StatusCheck::SkipAll); + } + let validator = JwtCredentialValidator::with_signature_verifier(EdDSAJwsVerifier::default()); let decoded_credential = validator - .validate::<_, Object>( - &jwt, - &issuer_doc, - &JwtCredentialValidationOptions::default(), - FailFast::FirstError, - ) + .validate::<_, Object>(&jwt, &issuer_doc, &validation_option, FailFast::FirstError) .map_err(|mut e| match e.validation_errors.swap_remove(0) { JwtValidationError::Revoked => VcValidationError::RevokedCredential, JwtValidationError::ExpirationDate | JwtValidationError::IssuanceDate => VcValidationError::ExpiredCredential, e => VcValidationError::JwtValidationError(e), })?; + if let Some(status_list_json) = status_list_credential_json { + let status_list = StatusList2021Credential::from_json(&status_list_json) + .map_err(VcValidationError::InvalidStatusList2020Credential)?; + JwtCredentialValidatorUtils::check_status_with_status_list_2021( + &decoded_credential.credential, + &status_list, + StatusCheck::Strict, + ) + .map_err(|e| match e { + JwtValidationError::Revoked => VcValidationError::RevokedCredential, + JwtValidationError::Suspended => VcValidationError::SuspendedCredential, + e => VcValidationError::JwtValidationError(e), + })?; + } + let response = Response::new(VcValidationResponse { credential_json: decoded_credential.credential.to_json().unwrap(), }); diff --git a/bindings/grpc/src/services/status_list_2021.rs b/bindings/grpc/src/services/status_list_2021.rs index a87b6b2cac..80a808084f 100644 --- a/bindings/grpc/src/services/status_list_2021.rs +++ b/bindings/grpc/src/services/status_list_2021.rs @@ -144,9 +144,7 @@ impl StatusList2021Svc for StatusList2021Service { status_list_credential .update(move |status_list| { for (idx, value) in entries { - if let Err(e) = status_list.set_entry(idx as usize, value) { - return Err(e); - } + status_list.set_entry(idx as usize, value)? } Ok(()) diff --git a/bindings/grpc/tests/api/credential_validation.rs b/bindings/grpc/tests/api/credential_validation.rs index 9146380848..a35bc44191 100644 --- a/bindings/grpc/tests/api/credential_validation.rs +++ b/bindings/grpc/tests/api/credential_validation.rs @@ -1,9 +1,14 @@ use _credentials::vc_validation_client::VcValidationClient; use _credentials::VcValidationRequest; use identity_iota::core::FromJson; +use identity_iota::core::ToJson; use identity_iota::core::Url; +use identity_iota::credential::status_list_2021::StatusList2021; +use identity_iota::credential::status_list_2021::StatusList2021CredentialBuilder; +use identity_iota::credential::status_list_2021::StatusPurpose; use identity_iota::credential::Credential; use identity_iota::credential::CredentialBuilder; +use identity_iota::credential::Issuer; use identity_iota::credential::Subject; use identity_iota::did::DID; use identity_storage::JwkDocumentExt; @@ -77,3 +82,67 @@ async fn credential_validation() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn revoked_credential_validation() -> anyhow::Result<()> { + let stronghold = StrongholdStorage::new(make_stronghold()); + let server = TestServer::new_with_stronghold(stronghold.clone()).await; + let api_client = server.client(); + + let mut issuer = Entity::new_with_stronghold(stronghold); + issuer.create_did(api_client).await?; + + let mut holder = Entity::new(); + holder.create_did(api_client).await?; + + let subject = Subject::from_json_value(json!({ + "id": holder.document().unwrap().id().as_str(), + "name": "Alice", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "GPA": "4.0", + }))?; + + let mut status_list_credential = StatusList2021CredentialBuilder::new(StatusList2021::default()) + .issuer(Issuer::Url(Url::parse(issuer.document().unwrap().id().as_str())?)) + .purpose(StatusPurpose::Revocation) + .subject_id(Url::parse("https://example.edu/credentials/status/1")?) + .build()?; + + // Build credential using subject above and issuer. + let mut credential: Credential = CredentialBuilder::default() + .id(Url::parse("https://example.edu/credentials/3732")?) + .issuer(Url::parse(issuer.document().unwrap().id().as_str())?) + .type_("UniversityDegreeCredential") + .subject(subject) + .build()?; + status_list_credential.set_credential_status(&mut credential, 0, true)?; + + let credential_jwt = issuer + .document() + .unwrap() + .create_credential_jwt( + &credential, + &issuer.storage(), + &issuer.fragment().unwrap(), + &JwsSignatureOptions::default(), + None, + ) + .await? + .into(); + + let mut grpc_client = VcValidationClient::connect(server.endpoint()).await?; + let error = grpc_client + .validate(VcValidationRequest { + credential_jwt, + status_list_credential_json: Some(status_list_credential.to_json()?), + }) + .await + .unwrap_err(); + + assert!(error.message().contains("revoked")); + + Ok(()) +}