Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-mader committed Dec 6, 2024
1 parent d91bc0d commit 61c754d
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 29 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ oid4vp = { git = "https://[email protected]/impierce/openid4vc.git", rev = "12fed14
async-trait = "0.1"
axum = { version = "0.7", features = ["tracing"] }
base64 = "0.22"
chrono = "0.4"
cqrs-es = "0.4.2"
futures = "0.3"
identity_core = "1.3"
Expand All @@ -41,11 +42,15 @@ identity_credential = { version = "1.3", default-features = false, features = [
identity_did = { version = "1.3" }
identity_iota = { version = "1.3" }
identity_verification = { version = "1.3", default-features = false }
iso8601-duration = { version = "0.2", features = ["chrono"] }
jsonwebtoken = "9.3"
lazy_static = "1.4"
mime = { version = "0.3" }
once_cell = { version = "1.19" }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
] }
rstest = "0.22"
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = { version = "1.0" }
Expand Down
2 changes: 2 additions & 0 deletions agent_api_rest/src/issuance/credential_issuer/credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ mod tests {
credential: json!(CREDENTIAL_JWT),
is_signed: true,
credential_configuration_id: CREDENTIAL_CONFIGURATION_ID.to_string(),
expires: None,
}
} else {
// ...or else, submitting the data that will be signed inside `UniCore`.
Expand All @@ -229,6 +230,7 @@ mod tests {
}),
is_signed: false,
credential_configuration_id: CREDENTIAL_CONFIGURATION_ID.to_string(),
expires: None,
}
};

Expand Down
4 changes: 4 additions & 0 deletions agent_api_rest/src/issuance/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub struct CredentialsEndpointRequest {
#[serde(default)]
pub is_signed: bool,
pub credential_configuration_id: String,
pub expires: Option<String>,
}

#[axum_macros::debug_handler]
Expand All @@ -50,6 +51,7 @@ pub(crate) async fn credentials(
credential: data,
is_signed,
credential_configuration_id,
expires,
}) = serde_json::from_value(payload)
else {
return (StatusCode::BAD_REQUEST, "invalid payload").into_response();
Expand Down Expand Up @@ -97,6 +99,8 @@ pub(crate) async fn credentials(
credential_id: credential_id.clone(),
data: Data { raw: data },
credential_configuration,
credential_configuration_id,
expires,
}
};

Expand Down
3 changes: 3 additions & 0 deletions agent_application/example.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ credential_configurations:
uri: https://www.impierce.com/external/impierce-logo.png
alt_text: UniCore Logo

# Refer to the README to get an overview over config options
credential_expiry: P1Y # default, if not specified

did_document_cache:
enabled: false
ttl: 5000
Expand Down
13 changes: 6 additions & 7 deletions agent_issuance/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ 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
identity_credential.workspace = true
iso8601-duration.workspace = true
jsonwebtoken.workspace = true
oid4vci.workspace = true
oid4vc-core.workspace = true
Expand All @@ -33,7 +34,9 @@ rstest = { workspace = true, optional = true }
[dev-dependencies]
agent_holder = { path = "../agent_holder", features = ["test_utils"] }
agent_issuance = { path = ".", features = ["test_utils"] }
agent_secret_manager = { path = "../agent_secret_manager", features = ["test_utils"] }
agent_secret_manager = { path = "../agent_secret_manager", features = [
"test_utils",
] }
agent_shared = { path = "../agent_shared", features = ["test_utils"] }

did_manager.workspace = true
Expand All @@ -42,8 +45,4 @@ tracing-test.workspace = true
async-std = { version = "1.5", features = ["attributes", "tokio1"] }

[features]
test_utils = [
"dep:lazy_static",
"dep:once_cell",
"dep:rstest",
]
test_utils = ["dep:lazy_static", "dep:once_cell", "dep:rstest"]
66 changes: 45 additions & 21 deletions agent_issuance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -45,3 +46,26 @@ server_config:
uri: https://impierce.com/images/logo-blue.png
alt_text: UniCore Logo
```
### Credential expiration
You can set the expiration of a credential by providing a **fixed** expiration date so that the credential is only valid until that time or a **relative** expiration ("duration") that will be added on top of the current time when the issuance is happening.
Values are accepted in the ISO 8601 format. The following examples are valid:
- Fixed expiry date:
- `2024-12-06T14:30:00Z`
- Relative from the point of issuance:
- `P7D` (7 days)
- `P1Y` (1 year)
- `P3Y6M` (3 years and 6 months)

> If you want the credential to be valid forever, you can set the expiration to `never`.

#### Config hierarchy

There are multiple ways to set the expiration date for a credential. The hierarchy is as follows:

- If not specified, the default expiration is set to one year from the time of issuance.
- If there is a value set in the `config.yaml` under the `credential_expiry` key, it will be used as the default expiration for that credential.
- If a value is directly provided in the `expires` key when a call towards the Issuance API is made, it overwrite all other settings.
96 changes: 96 additions & 0 deletions agent_issuance/src/credential/aggregate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ impl Aggregate for Credential {
credential_id,
data,
credential_configuration,
credential_configuration_id,
expires,
} => match &credential_configuration.credential_format {
CredentialFormats::JwtVcJson(Parameters::<JwtVcJson> {
parameters:
Expand Down Expand Up @@ -97,6 +99,57 @@ impl Aggregate for Credential {
.try_into()
.expect("Could not build issuer profile");

// ####### Determine credential expiration date

println!("Expiration date (provided): {:?}", expires);

// find expiration_date in config() expiration settings
let expiration_date = config().clone().credential_expiry.and_then(|v| {
v.into_iter()
.find(|c| c.credential_configuration_id == credential_configuration_id)
.map(|c| c.expires)
});
println!("Expiration date (config): {:?}", expiration_date);

// overwrite expiration_date with "expires" if is Some
let expiration_date = expires.or(expiration_date);
println!("Expiration date (provided or config): {:?}", expiration_date);

// if expiration_data is still None, set it to 1 year from issuance_date
let expiration_date = expiration_date.unwrap_or_else(|| {
let issuance_date = chrono::DateTime::parse_from_rfc3339(&issuance_date)
.expect("Could not parse issuance_date")
.timestamp();
(issuance_date + 60 * 60 * 24 * 365).to_string()
});

println!("Expiration date (fallback): {:?}", expiration_date);

let issuance_date =
chrono::DateTime::parse_from_rfc3339(&issuance_date).expect("Could not parse issuance_date");

let x = expiration_date
.parse::<iso8601_duration::Duration>()
.unwrap()
.to_chrono_at_datetime(chrono::Utc::now());
println!("Parsed duration: {:?}", x);

let valid_until = chrono::Utc::now() + x;
println!("Valid until: {:?}", valid_until.to_rfc3339());

let issuance_date = issuance_date.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);

// Try parsing to Duration
// let time_delta: chrono::TimeDelta = expiration_date
// .parse::<iso8601_duration::Duration>()
// .unwrap()
// .to_chrono()
// .unwrap();

// let x = expiration_date.parse::<chrono::DateTime<chrono::TimeDelta>>().unwrap();

// ####### Determine credential expiration date

let mut credential_types: Vec<String> = type_.clone();

let credential_subject_json =
Expand Down Expand Up @@ -352,12 +405,15 @@ impl Aggregate for Credential {
}
}

fn determine_expiry() {}

#[cfg(test)]
pub mod credential_tests {
use super::test_utils::*;
use super::*;

use agent_secret_manager::service::Service;
use agent_shared::config::set_config;
use jsonwebtoken::Algorithm;

use rstest::rstest;
Expand Down Expand Up @@ -397,6 +453,8 @@ pub mod credential_tests {
raw: credential_subject,
},
credential_configuration: Box::new(credential_configuration.clone()),
credential_configuration_id: "foobar".to_string(),
expires: None,
})
.then_expect_events(vec![CredentialEvent::UnsignedCredentialCreated {
credential_id,
Expand Down Expand Up @@ -444,6 +502,44 @@ pub mod credential_tests {
status: Status::Issued,
}])
}

#[rstest]
#[case::w3c_vc(
W3C_VC_CREDENTIAL_SUBJECT.clone(),
W3C_VC_CREDENTIAL_CONFIGURATION.clone(),
UNSIGNED_W3C_VC_CREDENTIAL.clone()
)]
#[serial_test::serial]
async fn test_expiry(
#[case] credential_subject: serde_json::Value,
#[case] credential_configuration: CredentialConfigurationsSupportedObject,
#[case] unsigned_credential: serde_json::Value,
credential_id: String,
) {
set_config().credential_expiry = Some(vec![agent_shared::config::CredentialExpiry {
credential_configuration_id: "foobar".to_string(),
expires: "P1D".to_string(),
}]);
CredentialTestFramework::with(Service::default())
.given_no_previous_events()
.when(CredentialCommand::CreateUnsignedCredential {
credential_id: credential_id.clone(),
data: Data {
raw: credential_subject,
},
credential_configuration: Box::new(credential_configuration.clone()),
credential_configuration_id: "foobar".to_string(),
expires: Some("P5Y".to_string()),
// expires: None,
})
.then_expect_events(vec![CredentialEvent::UnsignedCredentialCreated {
credential_id,
data: Data {
raw: unsigned_credential,
},
credential_configuration: Box::new(credential_configuration),
}])
}
}

#[cfg(feature = "test_utils")]
Expand Down
2 changes: 2 additions & 0 deletions agent_issuance/src/credential/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum CredentialCommand {
credential_id: String,
data: Data,
credential_configuration: Box<CredentialConfigurationsSupportedObject>,
credential_configuration_id: String, // required to look up expiration settings from config
expires: Option<String>,
},
CreateSignedCredential {
credential_id: String,
Expand Down
2 changes: 2 additions & 0 deletions agent_shared/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ rust-version.workspace = true

[dependencies]
async-trait.workspace = true
chrono.workspace = true
config = { version = "0.14" }
cqrs-es.workspace = true
dotenvy = { version = "0.15" }
http-serde = "2.1"
# TODO: replace all identity_* with identity_iota?
identity_iota.workspace = true
iso8601-duration.workspace = true
jsonwebtoken.workspace = true
oid4vc-core.workspace = true
oid4vci.workspace = true
Expand Down
Loading

0 comments on commit 61c754d

Please sign in to comment.