From 65c965359765a03b6f5d3f5bf51995f235a2667f Mon Sep 17 00:00:00 2001 From: Daniel Mader Date: Fri, 13 Dec 2024 15:23:45 +0100 Subject: [PATCH] feat: require credential expiration date, use credential `id` in JWT (#150) BREAKING CHANGE: require `expiresAt` property when creating a credential via the HTTP API The `expiresAt` field is now _required_ when creating a credential on `/v0/credentials`. Accepted values are ISO 8601 timestamps (UTC) such as `2024-12-09T16:51:29Z` or the value `"never"`. ### Migration guide - supply `expiresAt` for each request to `/v0/credentials` --------- Co-authored-by: Nander Stabel --- Cargo.lock | 2 + Cargo.toml | 1 + .../postman/ssi-agent.postman_collection.json | 6 +- .../issuance/credential_issuer/credential.rs | 5 +- agent_api_rest/src/issuance/credentials.rs | 8 +- agent_application/docker/db/init.sql | 1 - agent_holder/src/offer/aggregate.rs | 5 +- agent_holder/src/presentation/aggregate.rs | 2 +- agent_issuance/Cargo.toml | 2 +- agent_issuance/README.md | 51 +++--- agent_issuance/src/credential/aggregate.rs | 172 +++++++++++++++--- agent_issuance/src/credential/command.rs | 3 +- agent_issuance/src/credential/error.rs | 7 +- agent_shared/Cargo.toml | 1 + agent_verification/Cargo.toml | 1 + .../src/authorization_request/aggregate.rs | 8 +- 16 files changed, 207 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 172bf82a..0b0047b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -287,6 +287,7 @@ name = "agent_shared" version = "0.1.0" dependencies = [ "async-trait", + "chrono", "config", "cqrs-es", "dotenvy", @@ -336,6 +337,7 @@ dependencies = [ "anyhow", "async-std", "async-trait", + "chrono", "cqrs-es", "did_manager", "futures", diff --git a/Cargo.toml b/Cargo.toml index 0d01895f..b6772fe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ oid4vp = { git = "https://git@github.com/impierce/openid4vc.git", rev = "7be5b72 async-trait = "0.1" axum = { version = "0.7", features = ["tracing"] } base64 = "0.22" +chrono = { version = "0.4", features = ["serde"] } cqrs-es = "0.4.2" futures = "0.3" identity_core = "1.3" diff --git a/agent_api_rest/postman/ssi-agent.postman_collection.json b/agent_api_rest/postman/ssi-agent.postman_collection.json index 413bfdb0..40e4a720 100644 --- a/agent_api_rest/postman/ssi-agent.postman_collection.json +++ b/agent_api_rest/postman/ssi-agent.postman_collection.json @@ -43,7 +43,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"openbadge_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"type\": [ \"AchievementSubject\" ],\n \"achievement\": {\n \"id\": \"https://example.com/achievements/21st-century-skills/teamwork\",\n \"type\": \"Achievement\",\n \"criteria\": {\n \"narrative\": \"Team members are nominated for this badge by their peers and recognized upon review by Example Corp management.\"\n },\n \"description\": \"This badge recognizes the development of the capacity to collaborate within a group environment.\",\n \"name\": \"Teamwork\"\n }\n }\n }\n}", + "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"openbadge_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"type\": [ \"AchievementSubject\" ],\n \"achievement\": {\n \"id\": \"https://example.com/achievements/21st-century-skills/teamwork\",\n \"type\": \"Achievement\",\n \"criteria\": {\n \"narrative\": \"Team members are nominated for this badge by their peers and recognized upon review by Example Corp management.\"\n },\n \"description\": \"This badge recognizes the development of the capacity to collaborate within a group environment.\",\n \"name\": \"Teamwork\"\n }\n }\n },\n \"expiresAt\": \"2028-04-12T09:15:23Z\"\n}", "options": { "raw": { "language": "json" @@ -97,7 +97,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"first_name\": \"Ferris\",\n \"last_name\": \"Crabman\",\n \"dob\": \"1982-01-01\"\n }\n }\n}", + "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"first_name\": \"Ferris\",\n \"last_name\": \"Crabman\",\n \"dob\": \"1982-01-01\"\n }\n },\n \"expiresAt\": \"never\"\n}", "options": { "raw": { "language": "json" @@ -151,7 +151,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"credentialSubject\": {\n \"id\": \"https://ecommerce.impierce.com/\",\n \"image\": \"https://static.wikia.nocookie.net/fictionalcompanies/images/c/c2/ACME_Corporation.png\",\n \"name\": \"VirtualVendors\",\n \"certificaat\": {\n \"type\": \"ACMECorpCredential\",\n \"certificeringsDatum\": \"2024-06-26\",\n \"geldigheidsPeriode\": \"1 jaar\",\n \"garanties\": [\n \"Het bedrijf is echt en bereikbaar.\",\n \"Voldoet aan de Thuiswinkel Algemene Voorwaarden.\",\n \"14 dagen bedenktijd.\",\n \"Veilige betaalmethoden.\",\n \"Duidelijke product/servicebeschrijvingen.\",\n \"Transparant bestelproces.\",\n \"Duidelijke prijzen.\",\n \"Veilige betaalomgeving.\",\n \"Veilige omgang met persoonlijke gegevens.\",\n \"Effectieve klachtenafhandeling en onafhankelijke geschillenbemiddeling.\"\n ]\n }\n }\n }\n}", + "raw": "{\n \"offerId\":\"{{OFFER_ID}}\",\n \"credentialConfigurationId\": \"w3c_vc_credential\",\n \"credential\": {\n \"id\": \"https://acme.example.org/1a2b3c4d5e6f\",\n \"credentialSubject\": {\n \"id\": \"https://ecommerce.impierce.com/\",\n \"image\": \"https://static.wikia.nocookie.net/fictionalcompanies/images/c/c2/ACME_Corporation.png\",\n \"name\": \"VirtualVendors\",\n \"certificaat\": {\n \"type\": \"ACMECorpCredential\",\n \"certificeringsDatum\": \"2024-06-26\",\n \"geldigheidsPeriode\": \"1 jaar\",\n \"garanties\": [\n \"Het bedrijf is echt en bereikbaar.\",\n \"Voldoet aan de Thuiswinkel Algemene Voorwaarden.\",\n \"14 dagen bedenktijd.\",\n \"Veilige betaalmethoden.\",\n \"Duidelijke product/servicebeschrijvingen.\",\n \"Transparant bestelproces.\",\n \"Duidelijke prijzen.\",\n \"Veilige betaalomgeving.\",\n \"Veilige omgang met persoonlijke gegevens.\",\n \"Effectieve klachtenafhandeling en onafhankelijke geschillenbemiddeling.\"\n ]\n }\n }\n },\n \"expiresAt\": \"2035-12-31T23:59:59Z\"\n}", "options": { "raw": { "language": "json" diff --git a/agent_api_rest/src/issuance/credential_issuer/credential.rs b/agent_api_rest/src/issuance/credential_issuer/credential.rs index b9e2376d..6e74cca8 100644 --- a/agent_api_rest/src/issuance/credential_issuer/credential.rs +++ b/agent_api_rest/src/issuance/credential_issuer/credential.rs @@ -153,6 +153,7 @@ mod tests { tests::{BASE_URL, CREDENTIAL_CONFIGURATION_ID, OFFER_ID}, }; use agent_event_publisher_http::EventPublisherHttp; + use agent_issuance::credential::aggregate::CredentialExpiry; use agent_issuance::{offer::event::OfferEvent, startup_commands::startup_commands, state::initialize}; use agent_secret_manager::service::Service; use agent_shared::config::{set_config, Events}; @@ -172,7 +173,7 @@ mod tests { Mock, MockServer, ResponseTemplate, }; - const CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIn0sImlzc3VlciI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQwMDowMDowMFoifX0.d4QN73vDtZu79RP6GldHObu6rGsjidkLYp0XMRQNbNPY75LJoSv2iXk2Rz5M-VMBZGSU3YPZHytlrKBjxr1IBQ"; + const CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2lpZXlvTE1TVnNKQVp2N0pqZTV3V1NrREV5bVVna3lGOGtiY3JqWnBYM3FkIiwibmJmIjoxMjYyMzA0MDAwLCJpYXQiOjEyNjIzMDQwMDAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1raWlleW9MTVNWc0pBWnY3SmplNXdXU2tERXltVWdreUY4a2JjcmpacFgzcWQiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIn0sImlzc3VlciI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQwMDowMDowMFoifX0.9IMQOMPD3V350XXlMthINwIT38gUC6WsPHKFpuR5hJ0w2DArrY5pjf2nG-_Ba5sSa3utKcc0QPHMaMCBPLpdAw"; trait CredentialEventTrigger { async fn prepare_credential_event_trigger( @@ -215,6 +216,7 @@ mod tests { credential: json!(CREDENTIAL_JWT), is_signed: true, credential_configuration_id: CREDENTIAL_CONFIGURATION_ID.to_string(), + expires_at: CredentialExpiry::Never, } } else { // ...or else, submitting the data that will be signed inside `UniCore`. @@ -229,6 +231,7 @@ mod tests { }), is_signed: false, credential_configuration_id: CREDENTIAL_CONFIGURATION_ID.to_string(), + expires_at: CredentialExpiry::Never, } }; diff --git a/agent_api_rest/src/issuance/credentials.rs b/agent_api_rest/src/issuance/credentials.rs index 563711da..fa60ca81 100644 --- a/agent_api_rest/src/issuance/credentials.rs +++ b/agent_api_rest/src/issuance/credentials.rs @@ -1,6 +1,6 @@ use crate::API_VERSION; use agent_issuance::{ - credential::{command::CredentialCommand, entity::Data, views::CredentialView}, + credential::{aggregate::CredentialExpiry, command::CredentialCommand, entity::Data, views::CredentialView}, offer::command::OfferCommand, server_config::queries::ServerConfigView, state::{IssuanceState, SERVER_CONFIG_ID}, @@ -36,6 +36,7 @@ pub struct CredentialsEndpointRequest { #[serde(default)] pub is_signed: bool, pub credential_configuration_id: String, + pub expires_at: CredentialExpiry, } #[axum_macros::debug_handler] @@ -50,6 +51,7 @@ pub(crate) async fn credentials( credential: data, is_signed, credential_configuration_id, + expires_at, }) = serde_json::from_value(payload) else { return (StatusCode::BAD_REQUEST, "invalid payload").into_response(); @@ -97,6 +99,7 @@ pub(crate) async fn credentials( credential_id: credential_id.clone(), data: Data { raw: data }, credential_configuration, + expires_at, } }; @@ -223,7 +226,8 @@ pub mod tests { "credential": { "credentialSubject": CREDENTIAL_SUBJECT.clone() }, - "credentialConfigurationId": CREDENTIAL_CONFIGURATION_ID + "credentialConfigurationId": CREDENTIAL_CONFIGURATION_ID, + "expiresAt": "never" })) .unwrap(), )) diff --git a/agent_application/docker/db/init.sql b/agent_application/docker/db/init.sql index 49be6ff2..223f8cba 100644 --- a/agent_application/docker/db/init.sql +++ b/agent_application/docker/db/init.sql @@ -18,7 +18,6 @@ CREATE TABLE connection PRIMARY KEY (view_id) ); - CREATE TABLE all_connections ( view_id text NOT NULL, diff --git a/agent_holder/src/offer/aggregate.rs b/agent_holder/src/offer/aggregate.rs index 16bed424..26386c4a 100644 --- a/agent_holder/src/offer/aggregate.rs +++ b/agent_holder/src/offer/aggregate.rs @@ -335,7 +335,8 @@ pub mod tests { "name": "Master of Oceanography" } }}, - "credentialConfigurationId": "badge" + "credentialConfigurationId": "badge", + "expiresAt": "never" })) .unwrap(), )) @@ -515,6 +516,6 @@ pub mod test_utils { #[fixture] pub fn signed_credentials(holder_credential_id: String) -> Vec { - vec![OfferCredential { holder_credential_id, credential: Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jQEpI7DhjOcmyhPEpfGARwcRyzor_fUvynb43-eqD9175FBoshENX0S-8qlloQ7vbT5gat8TjvcDlGDN720ZBw".to_string())}] + vec![OfferCredential { holder_credential_id, credential: Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwibmJmIjoxMjYyMzA0MDAwLCJpYXQiOjEyNjIzMDQwMDAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJkZWdyZWUiOnsidHlwZSI6Ik1hc3RlckRlZ3JlZSIsIm5hbWUiOiJNYXN0ZXIgb2YgT2NlYW5vZ3JhcGh5In0sImZpcnN0X25hbWUiOiJGZXJyaXMiLCJsYXN0X25hbWUiOiJSdXN0YWNlYW4ifSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.jUt911ms74K282EUQSbisnOZGf1ALUvpnTJfJ1PSwp6sBcoStZTX52H6k5b6o6nNRvBr6nxQWkywib_S0AzACQ".to_string())}] } } diff --git a/agent_holder/src/presentation/aggregate.rs b/agent_holder/src/presentation/aggregate.rs index 3b9524d9..622c741a 100644 --- a/agent_holder/src/presentation/aggregate.rs +++ b/agent_holder/src/presentation/aggregate.rs @@ -176,6 +176,6 @@ pub mod test_utils { #[fixture] pub fn signed_presentation() -> Jwt { - Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I2tleS0wIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsIm5iZiI6MCwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlpFUlRRU0lzSW10cFpDSTZJbVJwWkRwclpYazZlalpOYTJkRk9EUk9RMDF3VFdWQmVEbHFTemxqWmpWWE5FYzRaMk5hT1hoMWQwcDJSekZsTjNkT2F6aExRMmQwSTNvMlRXdG5SVGcwVGtOTmNFMWxRWGc1YWtzNVkyWTFWelJIT0dkaldqbDRkWGRLZGtjeFpUZDNUbXM0UzBObmRDSjkuZXlKcGMzTWlPaUprYVdRNmEyVjVPbm8yVFd0blJUZzBUa05OY0UxbFFYZzVha3M1WTJZMVZ6UkhPR2RqV2psNGRYZEtka2N4WlRkM1RtczRTME5uZENJc0luTjFZaUk2SW1ScFpEcHJaWGs2ZWpaTmEyZEZPRFJPUTAxd1RXVkJlRGxxU3psalpqVlhORWM0WjJOYU9YaDFkMHAyUnpGbE4zZE9hemhMUTJkMElpd2laWGh3SWpvNU9UazVPVGs1T1RrNUxDSnBZWFFpT2pBc0luWmpJanA3SWtCamIyNTBaWGgwSWpvaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZEY21Wa1pXNTBhV0ZzSWwwc0ltTnlaV1JsYm5ScFlXeFRkV0pxWldOMElqcDdJbWxrSWpvaVpHbGtPbXRsZVRwNk5rMXJaMFU0TkU1RFRYQk5aVUY0T1dwTE9XTm1OVmMwUnpoblkxbzVlSFYzU25aSE1XVTNkMDVyT0V0RFozUWlMQ0prWldkeVpXVWlPbnNpZEhsd1pTSTZJazFoYzNSbGNrUmxaM0psWlNJc0ltNWhiV1VpT2lKTllYTjBaWElnYjJZZ1QyTmxZVzV2WjNKaGNHaDVJbjBzSW1acGNuTjBYMjVoYldVaU9pSkdaWEp5YVhNaUxDSnNZWE4wWDI1aGJXVWlPaUpTZFhOMFlXTmxZVzRpZlN3aWFYTnpkV1Z5SWpvaVpHbGtPbXRsZVRwNk5rMXJaMFU0TkU1RFRYQk5aVUY0T1dwTE9XTm1OVmMwUnpoblkxbzVlSFYzU25aSE1XVTNkMDVyT0V0RFozUWlMQ0pwYzNOMVlXNWpaVVJoZEdVaU9pSXlNREV3TFRBeExUQXhWREF3T2pBd09qQXdXaUo5ZlEualFFcEk3RGhqT2NteWhQRXBmR0FSd2NSeXpvcl9mVXZ5bmI0My1lcUQ5MTc1RkJvc2hFTlgwUy04cWxsb1E3dmJUNWdhdDhUanZjRGxHRE43MjBaQnciXX19.2iIO7zlcLsceC5P0X3p9yICrqRXj8A9VcTVJkUUiALufEm72urbJFRbkvrXGNWwYezFzAOz-4WrGpUNHWtTDCA".to_string()) + Jwt::from("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I2tleS0wIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsIm5iZiI6MCwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKRlpFUlRRU0lzSW10cFpDSTZJbVJwWkRwclpYazZlalpOYTJkRk9EUk9RMDF3VFdWQmVEbHFTemxqWmpWWE5FYzRaMk5hT1hoMWQwcDJSekZsTjNkT2F6aExRMmQwSTNvMlRXdG5SVGcwVGtOTmNFMWxRWGc1YWtzNVkyWTFWelJIT0dkaldqbDRkWGRLZGtjeFpUZDNUbXM0UzBObmRDSjkuZXlKcGMzTWlPaUprYVdRNmEyVjVPbm8yVFd0blJUZzBUa05OY0UxbFFYZzVha3M1WTJZMVZ6UkhPR2RqV2psNGRYZEtka2N4WlRkM1RtczRTME5uZENJc0luTjFZaUk2SW1ScFpEcHJaWGs2ZWpaTmEyZEZPRFJPUTAxd1RXVkJlRGxxU3psalpqVlhORWM0WjJOYU9YaDFkMHAyUnpGbE4zZE9hemhMUTJkMElpd2libUptSWpveE1qWXlNekEwTURBd0xDSnBZWFFpT2pFeU5qSXpNRFF3TURBc0luWmpJanA3SWtCamIyNTBaWGgwSWpvaWFIUjBjSE02THk5M2QzY3Vkek11YjNKbkx6SXdNVGd2WTNKbFpHVnVkR2xoYkhNdmRqRWlMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZEY21Wa1pXNTBhV0ZzSWwwc0ltTnlaV1JsYm5ScFlXeFRkV0pxWldOMElqcDdJbWxrSWpvaVpHbGtPbXRsZVRwNk5rMXJaMFU0TkU1RFRYQk5aVUY0T1dwTE9XTm1OVmMwUnpoblkxbzVlSFYzU25aSE1XVTNkMDVyT0V0RFozUWlMQ0prWldkeVpXVWlPbnNpZEhsd1pTSTZJazFoYzNSbGNrUmxaM0psWlNJc0ltNWhiV1VpT2lKTllYTjBaWElnYjJZZ1QyTmxZVzV2WjNKaGNHaDVJbjBzSW1acGNuTjBYMjVoYldVaU9pSkdaWEp5YVhNaUxDSnNZWE4wWDI1aGJXVWlPaUpTZFhOMFlXTmxZVzRpZlN3aWFYTnpkV1Z5SWpvaVpHbGtPbXRsZVRwNk5rMXJaMFU0TkU1RFRYQk5aVUY0T1dwTE9XTm1OVmMwUnpoblkxbzVlSFYzU25aSE1XVTNkMDVyT0V0RFozUWlMQ0pwYzNOMVlXNWpaVVJoZEdVaU9pSXlNREV3TFRBeExUQXhWREF3T2pBd09qQXdXaUo5ZlEualV0OTExbXM3NEsyODJFVVFTYmlzbk9aR2YxQUxVdnBuVEpmSjFQU3dwNnNCY29TdFpUWDUySDZrNWI2bzZuTlJ2QnI2bnhRV2t5d2liX1MwQXpBQ1EiXX19.1iNUrWs6dIy9T5-lrxHjBwZPcLZIhOJUhTQtLPxBSRfOyvoC2Jfw1aBCu6Tu8HvIQyJj0HIJAO_vYXAsLugRAg".to_string()) } } diff --git a/agent_issuance/Cargo.toml b/agent_issuance/Cargo.toml index a6ba06fa..ce08e198 100644 --- a/agent_issuance/Cargo.toml +++ b/agent_issuance/Cargo.toml @@ -10,7 +10,7 @@ agent_secret_manager = { path = "../agent_secret_manager" } async-trait.workspace = true cqrs-es.workspace = true -chrono = "0.4" +chrono.workspace = true types-ob-v3 = { git = "https://github.com/impierce/digital-credential-data-models.git", rev = "9f16c27" } derivative = "2.2" identity_core.workspace = true diff --git a/agent_issuance/README.md b/agent_issuance/README.md index 41940e50..8f67c2b6 100644 --- a/agent_issuance/README.md +++ b/agent_issuance/README.md @@ -3,32 +3,33 @@ This module contains business logic for issuing credentials. This ranges from using a credential template, applying user-specific subject data to it and offering the credential to a user wallet via the [OpenID4VCI](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) standard protocol. - ## Configuration The `agent_issuance` module is configured via the `issuance-config.yml` file. The following properties are available: -* `server_config`: **REQUIRED** The server configuration for Issuance. It contains the following properties: - * `credential_configurations`: **REQUIRED** An array of Credential Configurations. As of now, UniCore **requires the - array to contain exactly one Credential Configuration**. The Credential Configuration has the following properties: - * `credential_configuration_id`: **REQUIRED** The ID of the Credential Configuration. This ID will be used to - reference the Credential Configuration in the REST API's `/v0/credentials` endpoint. - * `format`: **REQUIRED** The format of the Credential. As of now, UniCore only supports `jwt_vc_json`. - * `credential_definition`: **REQUIRED** An object describing the properties of the Credentials that will be - issued. This object contains the following properties: - * `type`: **REQUIRED** an array of strings that describe the type of the Credential. - * `credentialSubject`: **OPTIONAL** an object that describes the properties of the Credential Subject. For - more information, see the [OpenID4VCI - specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-13.html#appendix-A.1.1.2-3.1.2.2.1) - * `display`: **OPTIONAL** An object describing the display properties of the to be issued Credentials. This - object contains the following properties: - * `name`: **REQUIRED** The name of the Credential. - * `locale`: **OPTIONAL** The locale of the Credential. - * `logo`: **OPTIONAL** The logo properties of the to be issued Credentials. This object contains the - following properties: - * `url`: **REQUIRED** The URL of the logo. - * `alt_text`: **OPTIONAL** String that describes the logo. + +- `server_config`: **REQUIRED** The server configuration for Issuance. It contains the following properties: + - `credential_configurations`: **REQUIRED** An array of Credential Configurations. As of now, UniCore **requires the + array to contain exactly one Credential Configuration**. The Credential Configuration has the following properties: + - `credential_configuration_id`: **REQUIRED** The ID of the Credential Configuration. This ID will be used to + reference the Credential Configuration in the REST API's `/v0/credentials` endpoint. + - `format`: **REQUIRED** The format of the Credential. As of now, UniCore only supports `jwt_vc_json`. + - `credential_definition`: **REQUIRED** An object describing the properties of the Credentials that will be + issued. This object contains the following properties: + - `type`: **REQUIRED** an array of strings that describe the type of the Credential. + - `credentialSubject`: **OPTIONAL** an object that describes the properties of the Credential Subject. For + more information, see the [OpenID4VCI + specification](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-13.html#appendix-A.1.1.2-3.1.2.2.1) + - `display`: **OPTIONAL** An object describing the display properties of the to be issued Credentials. This + object contains the following properties: + - `name`: **REQUIRED** The name of the Credential. + - `locale`: **OPTIONAL** The locale of the Credential. + - `logo`: **OPTIONAL** The logo properties of the to be issued Credentials. This object contains the + following properties: + - `url`: **REQUIRED** The URL of the logo. + - `alt_text`: **OPTIONAL** String that describes the logo. Example of configuration options in `issuance-config.yml`: + ```yaml server_config: credential_configurations: @@ -45,3 +46,11 @@ server_config: uri: https://impierce.com/images/logo-blue.png alt_text: UniCore Logo ``` + +### Credential expiration + +You are required to set the expiration of a credential by providing an expiration date after which the credential should be rejected by verifiers. + +Expiration dates need to contain date and time (relative to UTC) following the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) standard, such as `2024-12-09T10:15:00Z`. + +> If you want the credential to be valid forever, you can set the expiration to `never`. The `exp` claim in the JWT will then be omitted. diff --git a/agent_issuance/src/credential/aggregate.rs b/agent_issuance/src/credential/aggregate.rs index 1c96469d..9a9f1958 100644 --- a/agent_issuance/src/credential/aggregate.rs +++ b/agent_issuance/src/credential/aggregate.rs @@ -27,6 +27,7 @@ use types_ob_v3::prelude::{ AchievementCredential, AchievementCredentialBuilder, AchievementCredentialType, AchievementSubject, Profile, ProfileBuilder, }; +use url::Url; #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] pub enum Status { @@ -35,6 +36,37 @@ pub enum Status { Issued, } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(untagged)] +pub enum CredentialExpiry { + Fixed(chrono::DateTime), + #[serde(with = "never_as_str")] + Never, +} + +mod never_as_str { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str("never") + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + if s == "never" { + Ok(()) + } else { + Err(serde::de::Error::custom("expected 'never'")) + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Default, Derivative)] #[derivative(PartialEq)] pub struct Credential { @@ -69,6 +101,7 @@ impl Aggregate for Credential { credential_id, data, credential_configuration, + expires_at, } => match &credential_configuration.credential_format { CredentialFormats::JwtVcJson(Parameters:: { parameters: @@ -78,7 +111,7 @@ impl Aggregate for Credential { }, }) => { #[cfg(feature = "test_utils")] - let issuance_date = "2010-01-01T00:00:00Z"; + let issuance_date = "2010-01-01T00:00:00Z".to_string(); #[cfg(not(feature = "test_utils"))] let issuance_date = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); @@ -96,11 +129,36 @@ impl Aggregate for Credential { .try_into() .expect("Could not build issuer profile"); + let issuance_date = + identity_core::common::Timestamp::parse(&issuance_date).expect("Could not parse issuance_date"); + + let expiration_date = match expires_at { + CredentialExpiry::Fixed(fixed) => { + let fixed = identity_core::common::Timestamp::from_unix(fixed.timestamp()) + .map_err(|_| InvalidExpirationDateError)?; + + Some(fixed) + } + CredentialExpiry::Never => None, + }; + let mut credential_types: Vec = type_.clone(); let credential_subject_json = data.raw.get("credentialSubject").ok_or(MissingCredentialSubjectError)?; + let id = data.raw.get("id").map_or(Ok(None), |id| { + id.as_str() + .ok_or_else(|| { + InvalidVerifiableCredentialError("Invalid format: `id` must be a string".to_string()) + }) + .and_then(|id_str| { + Url::parse(id_str).map(Some).map_err(|_| { + InvalidVerifiableCredentialError(format!("Could not parse `id` as URL: `{id_str}`")) + }) + }) + })?; + // Loop through all the items in the `type` array in reverse until we find a match. while let Some(credential_format) = credential_types.pop() { match credential_format.as_str() { @@ -118,10 +176,24 @@ impl Aggregate for Credential { Err(_) => unreachable!("Couldn't parse issuer"), }; - let credential: W3CVerifiableCredential = W3CVerifiableCredentialBuilder::default() + let builder = W3CVerifiableCredentialBuilder::default() .issuer(issuer) .subject(subject) - .issuance_date(issuance_date.parse().expect("Could not parse issuance_date")) + .issuance_date(issuance_date); + + let builder = if let Some(expiration_date) = expiration_date { + builder.expiration_date(expiration_date) + } else { + builder + }; + + let builder = if let Some(id) = id { + builder.id(id.into()) + } else { + builder + }; + + let credential: W3CVerifiableCredential = builder .build() .map_err(|e| InvalidVerifiableCredentialError(e.to_string()))?; @@ -148,7 +220,7 @@ impl Aggregate for Credential { serde_json::from_value::(credential_subject_json.clone()) .map_err(|e| InvalidVerifiableCredentialError(e.to_string()))?; - let credential: AchievementCredential = AchievementCredentialBuilder::default() + let builder = AchievementCredentialBuilder::default() .context(vec![ "https://www.w3.org/2018/credentials/v1", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json", @@ -157,14 +229,21 @@ impl Aggregate for Credential { "VerifiableCredential", &credential_format, ])) - // TODO: Come up with a way to get the credential id. - .id("http://example.com/credentials/3527") .name(name) .issuer(issuer) .credential_subject(credential_subject) - .issuance_date(issuance_date) - .try_into() - .map_err(InvalidVerifiableCredentialError)?; + .issuance_date(issuance_date.to_rfc3339()); + + let builder = if let Some(expiration_date) = expiration_date { + builder.expiration_date(expiration_date.to_rfc3339()) + } else { + builder + }; + + let builder = if let Some(id) = id { builder.id(id) } else { builder }; + + let credential: AchievementCredential = + builder.try_into().map_err(InvalidVerifiableCredentialError)?; return Ok(vec![UnsignedCredentialCreated { credential_id, @@ -196,6 +275,13 @@ impl Aggregate for Credential { return Ok(vec![]); } + let id: Option = self + .data + .as_ref() + .and_then(|data| data.raw.get("id")) + .and_then(|id| id.as_str()) + .and_then(|id| Url::parse(id).ok()); + let default_did_method = get_preferred_did_method(); let issuer_did = services @@ -206,6 +292,10 @@ impl Aggregate for Credential { let signed_credential = { let mut credential = self.data.as_ref().ok_or(MissingCredentialDataError)?.clone(); + if let Some(ref id) = id { + credential.raw["id"] = json!(id); + }; + credential.raw["issuer"] = json!(issuer_did); let credential_subject = credential.raw["credentialSubject"].as_object().unwrap().clone(); @@ -229,7 +319,7 @@ impl Aggregate for Credential { info!("Credential: {:?}", credential); #[cfg(feature = "test_utils")] - let iat = 0; + let iat = 1262304000; // 2010-01-01T00:00:00Z #[cfg(not(feature = "test_utils"))] let iat = credential.raw["issuanceDate"] .as_str() @@ -237,23 +327,37 @@ impl Aggregate for Credential { .parse::>() .unwrap() .timestamp(); - // let iat = std::time::SystemTime::now() - // .duration_since(std::time::UNIX_EPOCH) - // .unwrap() - // .as_secs() as i64; + + let exp = credential.raw["expirationDate"].as_str().map(|expiration_date| { + expiration_date + .parse::>() + .expect("Could not parse `expirationDate` to DateTime") + .timestamp() + }); + + // Add standard claims + let vc_jwt_builder = VerifiableCredentialJwt::builder() + .sub(subject_id) + .iss(issuer_did) + .iat(iat) + .nbf(iat); // TODO: setting the `nbf` to `iat` makes the JWT immediately usable + + let vc_jwt_builder = if let Some(exp) = exp { + vc_jwt_builder.exp(exp) + } else { + vc_jwt_builder + }; + + let vc_jwt_builder = if let Some(id) = id { + vc_jwt_builder.jti(id.to_string()) + } else { + vc_jwt_builder + }; json!(jwt::encode( services.issuer.clone(), Header::new(get_preferred_signing_algorithm()), - VerifiableCredentialJwt::builder() - .sub(subject_id) - .iss(issuer_did) - .iat(iat) - // TODO: find out whether this is a required field. - .exp(9999999999i64) - .verifiable_credential(credential.raw) - .build() - .ok(), + vc_jwt_builder.verifiable_credential(credential.raw).build().ok(), &default_did_method.to_string() ) .await @@ -349,6 +453,7 @@ pub mod credential_tests { raw: credential_subject, }, credential_configuration: Box::new(credential_configuration.clone()), + expires_at: CredentialExpiry::Never, }) .then_expect_events(vec![CredentialEvent::UnsignedCredentialCreated { credential_id, @@ -396,8 +501,20 @@ pub mod credential_tests { status: Status::Issued, }]) } -} + pub mod expiry_tests { + use super::*; + + #[test] + fn custom_serializer_for_credential_expiry() { + let deserialized: CredentialExpiry = serde_json::from_value(serde_json::json!("never")).unwrap(); + assert_eq!(deserialized, CredentialExpiry::Never); + + let serialized = serde_json::to_value(&CredentialExpiry::Never).unwrap(); + assert_eq!(serialized, serde_json::json!("never")); + } + } +} #[cfg(feature = "test_utils")] pub mod test_utils { use super::*; @@ -414,9 +531,9 @@ pub mod test_utils { use serde_json::json; use std::collections::HashMap; - pub const OPENBADGE_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHA6Ly9leGFtcGxlLmNvbS9jcmVkZW50aWFscy8zNTI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIk9wZW5CYWRnZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsIm5hbWUiOiJUZWFtd29yayBCYWRnZSIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJ0eXBlIjoiQWNoaWV2ZW1lbnQiLCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4iLCJuYW1lIjoiVGVhbXdvcmsifX19fQ.SkC7IvpBGB9e98eobnE9qcLjs-yoZup3cieBla3DRTlcRezXEDPv4YRoUgffho9LJ0rkmfFPsPwb-owXMWyPAA"; + pub const OPENBADGE_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwibmJmIjoxMjYyMzA0MDAwLCJpYXQiOjEyNjIzMDQwMDAsImp0aSI6Imh0dHBzOi8vZXhhbXBsZS5jb20vY3JlZGVudGlhbHMvMzUyNyIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiJdLCJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vY3JlZGVudGlhbHMvMzUyNyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sImlzc3VlciI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQwMDowMDowMFoiLCJuYW1lIjoiVGVhbXdvcmsgQmFkZ2UiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuY29tL2FjaGlldmVtZW50cy8yMXN0LWNlbnR1cnktc2tpbGxzL3RlYW13b3JrIiwidHlwZSI6IkFjaGlldmVtZW50IiwiY3JpdGVyaWEiOnsibmFycmF0aXZlIjoiVGVhbSBtZW1iZXJzIGFyZSBub21pbmF0ZWQgZm9yIHRoaXMgYmFkZ2UgYnkgdGhlaXIgcGVlcnMgYW5kIHJlY29nbml6ZWQgdXBvbiByZXZpZXcgYnkgRXhhbXBsZSBDb3JwIG1hbmFnZW1lbnQuIn0sImRlc2NyaXB0aW9uIjoiVGhpcyBiYWRnZSByZWNvZ25pemVzIHRoZSBkZXZlbG9wbWVudCBvZiB0aGUgY2FwYWNpdHkgdG8gY29sbGFib3JhdGUgd2l0aGluIGEgZ3JvdXAgZW52aXJvbm1lbnQuIiwibmFtZSI6IlRlYW13b3JrIn19fX0.GBbACSRxM_nBdhWqntrVaMA78ftPR0fO0sHM1v5sYMIJUWCrOamo9EN_67nAHuvwl_og6EVz36o1we7U9M6oCA"; - pub const W3C_VC_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwiZXhwIjo5OTk5OTk5OTk5LCJpYXQiOjAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIiwiZGVncmVlIjp7InR5cGUiOiJNYXN0ZXJEZWdyZWUiLCJuYW1lIjoiTWFzdGVyIG9mIE9jZWFub2dyYXBoeSJ9fSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.MUDBbPJfXe0G9sjVTF3RuR6ukRM0d4N57iMGNFcIKMFPIEdig12v-YFB0qfnSghGcQo8hUw3jzxZXTSJATEgBg"; + pub const W3C_VC_VERIFIABLE_CREDENTIAL_JWT: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0I3o2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtnRTg0TkNNcE1lQXg5aks5Y2Y1VzRHOGdjWjl4dXdKdkcxZTd3Tms4S0NndCIsInN1YiI6ImRpZDprZXk6ejZNa2dFODROQ01wTWVBeDlqSzljZjVXNEc4Z2NaOXh1d0p2RzFlN3dOazhLQ2d0IiwibmJmIjoxMjYyMzA0MDAwLCJpYXQiOjEyNjIzMDQwMDAsInZjIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJmaXJzdF9uYW1lIjoiRmVycmlzIiwibGFzdF9uYW1lIjoiUnVzdGFjZWFuIiwiZGVncmVlIjp7InR5cGUiOiJNYXN0ZXJEZWdyZWUiLCJuYW1lIjoiTWFzdGVyIG9mIE9jZWFub2dyYXBoeSJ9fSwiaXNzdWVyIjoiZGlkOmtleTp6Nk1rZ0U4NE5DTXBNZUF4OWpLOWNmNVc0RzhnY1o5eHV3SnZHMWU3d05rOEtDZ3QiLCJpc3N1YW5jZURhdGUiOiIyMDEwLTAxLTAxVDAwOjAwOjAwWiJ9fQ.Zhdom31SC8oh7h4DHz6hoTQeHIV5iA2jUO8gbt3KfYeZFiPbmJiBWNBy3ZmcSJ961YNOdK0I5MWqDw5nlsZpDw"; #[fixture] pub fn credential_id() -> String { @@ -492,6 +609,7 @@ pub mod test_utils { }; pub static ref OPENBADGE_CREDENTIAL_SUBJECT: serde_json::Value = json!( { + "id": "https://example.com/credentials/3527", "credentialSubject": { "type": [ "AchievementSubject" ], "achievement": { @@ -523,7 +641,7 @@ pub mod test_utils { "https://www.w3.org/2018/credentials/v1", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.2.json" ], - "id": "http://example.com/credentials/3527", + "id": "https://example.com/credentials/3527", "type": ["VerifiableCredential", "OpenBadgeCredential"], "issuer": { "id": "https://my-domain.example.org/", diff --git a/agent_issuance/src/credential/command.rs b/agent_issuance/src/credential/command.rs index 02bdcec0..cd2494e2 100644 --- a/agent_issuance/src/credential/command.rs +++ b/agent_issuance/src/credential/command.rs @@ -1,7 +1,7 @@ use oid4vci::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; use serde::Deserialize; -use super::entity::Data; +use super::{aggregate::CredentialExpiry, entity::Data}; #[derive(Debug, Deserialize)] #[serde(untagged)] @@ -10,6 +10,7 @@ pub enum CredentialCommand { credential_id: String, data: Data, credential_configuration: Box, + expires_at: CredentialExpiry, }, CreateSignedCredential { credential_id: String, diff --git a/agent_issuance/src/credential/error.rs b/agent_issuance/src/credential/error.rs index c03f6492..62fa1298 100644 --- a/agent_issuance/src/credential/error.rs +++ b/agent_issuance/src/credential/error.rs @@ -4,19 +4,16 @@ use thiserror::Error; pub enum CredentialError { #[error("Credential must be an object")] InvalidCredentialError, - #[error("This Credential format it not supported")] UnsupportedCredentialFormat, - #[error("The `credentialSubject` parameter is missing")] MissingCredentialSubjectError, - #[error("The supplied `credentialSubject` is invalid: {0}")] InvalidCredentialSubjectError(String), - #[error("The verifiable credential is invalid: {0}")] InvalidVerifiableCredentialError(String), - #[error("Could not find any data to be signed")] MissingCredentialDataError, + #[error("Invalid expiration data: The expiration date must not exceed `9999-12-31T23:59:59`. Please provide a valid date within the supported range.")] + InvalidExpirationDateError, } diff --git a/agent_shared/Cargo.toml b/agent_shared/Cargo.toml index df6e68f8..724864bd 100644 --- a/agent_shared/Cargo.toml +++ b/agent_shared/Cargo.toml @@ -6,6 +6,7 @@ rust-version.workspace = true [dependencies] async-trait.workspace = true +chrono.workspace = true config = { version = "0.14" } cqrs-es.workspace = true dotenvy = { version = "0.15" } diff --git a/agent_verification/Cargo.toml b/agent_verification/Cargo.toml index e9dd3029..4d0571a4 100644 --- a/agent_verification/Cargo.toml +++ b/agent_verification/Cargo.toml @@ -10,6 +10,7 @@ agent_shared = { path = "../agent_shared" } anyhow = "1.0" async-trait.workspace = true +chrono.workspace = true cqrs-es.workspace = true futures.workspace = true jsonwebtoken.workspace = true diff --git a/agent_verification/src/authorization_request/aggregate.rs b/agent_verification/src/authorization_request/aggregate.rs index 8c0b2e35..9d9e9678 100644 --- a/agent_verification/src/authorization_request/aggregate.rs +++ b/agent_verification/src/authorization_request/aggregate.rs @@ -392,12 +392,14 @@ pub mod tests { let issuer_did = verifier_did(&default_did_method).await; let subject_did = issuer_did.clone(); + let issuance_date_str = "2010-01-01T00:00:00Z"; + let issuance_date = issuance_date_str.parse::>().unwrap(); + // Create a new verifiable credential. let verifiable_credential = VerifiableCredentialJwt::builder() .sub(&subject_did) .iss(&issuer_did) - .iat(0) - .exp(9999999999i64) + .iat(issuance_date.timestamp()) .verifiable_credential(serde_json::json!({ "@context": [ "https://www.w3.org/2018/credentials/v1", @@ -407,7 +409,7 @@ pub mod tests { "VerifiableCredential", "TestCredential" ], - "issuanceDate": "2022-01-01T00:00:00Z", + "issuanceDate": issuance_date_str, "issuer": issuer_did, "credentialSubject": { "id": subject_did,