diff --git a/Cargo.lock b/Cargo.lock index da7ddb7e..6f6b6224 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -425,6 +425,8 @@ dependencies = [ "p256 0.13.2", "ring", "serde", + "strum 0.26.3", + "thiserror 1.0.69", "tokio", "url", ] diff --git a/agent_api_http/openapi.yaml b/agent_api_http/openapi.yaml index da7fc56c..5202203c 100644 --- a/agent_api_http/openapi.yaml +++ b/agent_api_http/openapi.yaml @@ -1396,6 +1396,111 @@ paths: "201": description: Linked VP service created successfully + /v0/keys/generate-new-key: + post: + tags: + - Identity + summary: Generate a new key + description: Generates a new cryptographic key and stores it in the key management system. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - alias + properties: + alias: + type: string + description: A human-readable name for this key. + example: "my-signing-key-01" + signatureAlgorithm: + type: string + description: The signature algorithm to use for this key. Defaults to Ed25519 if not specified. + example: "Ed25519" + + /v0/keys/remove-key: + post: + tags: + - Identity + summary: Remove an existing key + description: Removes an existing cryptographic key from the key management system. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - keyId + properties: + keyId: + type: string + description: The ID of the key to remove. + example: "a81bc81b-d7a7-4e5d-abff-90865d1e13b1" + + /v0/keys/rename-key-alias: + post: + tags: + - Identity + summary: Rename the alias of a key + description: Renames the human-readable alias of an existing cryptographic key in the key management system. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - keyId + - newAlias + properties: + keyId: + type: string + description: The ID of the key to rename. + example: "a81bc81b-d7a7-4e5d-abff-90865d1e13b1" + newAlias: + type: string + description: The new alias for the key. + example: "my-new-key-alias" + + /v0/keys/set-signing-key: + post: + tags: + - Identity + summary: Set a key as the active signing key + description: Designates a specific managed key to be used for signing operations. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - keyId + properties: + keyId: + type: string + description: The ID of the key to set as the signing key. + example: "a81bc81b-abcd-4e5d-abff-90865d1e13b1" + + /v0/keys/list-all: + get: + tags: + - Identity + summary: List all managed keys + description: Retrieves a list of all cryptographic keys managed by the key management system. + responses: + "200": + description: Keys retrieved successfully + content: + application/json: + schema: + type: array + items: + type: object + /auth/token: post: summary: Standard OAuth 2.0 endpoint for fetching a token diff --git a/agent_api_http/postman/ssi-agent.postman_collection.json b/agent_api_http/postman/ssi-agent.postman_collection.json index 5184de5a..d5f4477a 100644 --- a/agent_api_http/postman/ssi-agent.postman_collection.json +++ b/agent_api_http/postman/ssi-agent.postman_collection.json @@ -1,9 +1,10 @@ { "info": { - "_postman_id": "e9b3fbae-f20e-4957-9d3a-6a310f5fda62", + "_postman_id": "4da8f32a-5645-4785-911d-5c2c729e2092", "name": "ssi-agent", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "38461474" + "_exporter_id": "41786572", + "_collection_link": "https://impierce-technologies-bv.postman.co/workspace/New-Team-Workspace~1aa38760-77fc-4608-a1f5-e8a6fdaa0ad9/collection/41786572-4da8f32a-5645-4785-911d-5c2c729e2092?action=share&source=collection_link&creator=41786572" }, "item": [ { @@ -2281,6 +2282,152 @@ "response": [] } ] + }, + { + "name": "v1", + "item": [ + { + "name": "Identity", + "item": [ + { + "name": "Keys", + "item": [ + { + "name": "Generate Key", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"alias\": \"An Example Alias\",\n \"signingAlgorithm\": \"ES256\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/keys/generate-new-key", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "keys", + "generate-new-key" + ] + } + }, + "response": [] + }, + { + "name": "Remove Key", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"keyId\": \"your-managed-key-id\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/keys/remove-key", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "keys", + "remove-key" + ] + } + }, + "response": [] + }, + { + "name": "Rename Key Alias", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"keyId\": \"your-managed-key-id\",\n \"newAlias\": \"new-key-alias\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/keys/rename-key-alias", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "keys", + "rename-key-alias" + ] + } + }, + "response": [] + }, + { + "name": "Set Signing Key", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"keyId\": \"your-managed-key-id\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{HOST}}/v0/keys/set-signing-key", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "keys", + "set-signing-key" + ] + } + }, + "response": [] + }, + { + "name": "List all Keys", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{HOST}}/v0/keys/list-all", + "host": [ + "{{HOST}}" + ], + "path": [ + "v0", + "keys", + "list-all" + ] + } + }, + "response": [] + } + ] + } + ] + } + ] } ], "auth": { diff --git a/agent_api_http/src/lib.rs b/agent_api_http/src/lib.rs index fc92ad94..29d88b37 100644 --- a/agent_api_http/src/lib.rs +++ b/agent_api_http/src/lib.rs @@ -1,10 +1,12 @@ pub mod v0; +pub mod v1; pub mod error; pub mod handlers; pub mod metrics; pub mod utils; +use crate::v1::identity::IdentityContext; use agent_authorization::state::AuthorizationState; use agent_holder::state::HolderState; use agent_identity::{ @@ -55,13 +57,24 @@ pub fn app( issuance_state, holder_state, verification_state, - - key_generation_saga: _, - key_removal_saga: _, + key_generation_saga, + key_removal_saga, }: ApplicationState, ) -> Router { + let v1_identity_router = match (identity_state.clone(), key_generation_saga, key_removal_saga) { + (Some(state), Some(gen_saga), Some(rem_saga)) => { + let context = IdentityContext { + state, + key_generation_saga: gen_saga, + key_removal_saga: rem_saga, + }; + v1::identity::router(context) + } + _ => Router::new(), + }; let app = Router::new() .merge(identity_state.map(v0::identity::router).unwrap_or_default()) + .merge(v1_identity_router) .merge(library_state.map(v0::library::router).unwrap_or_default()) .merge( authorization_state diff --git a/agent_api_http/src/v0/identity/mod.rs b/agent_api_http/src/v0/identity/mod.rs index 945d8332..4cdca393 100644 --- a/agent_api_http/src/v0/identity/mod.rs +++ b/agent_api_http/src/v0/identity/mod.rs @@ -7,6 +7,10 @@ pub mod well_known; pub mod error; +use crate::{ + v0::identity::profiles::{get_profile, patch_profile}, + API_VERSION, +}; use agent_identity::state::IdentityState; use axum::{ routing::{get, post}, @@ -18,11 +22,6 @@ use services::{linked_vp::linked_vp, service, services}; use std::sync::Arc; use well_known::{did::did, did_configuration::did_configuration}; -use crate::{ - v0::identity::profiles::{get_profile, patch_profile}, - API_VERSION, -}; - pub fn router(identity_state: Arc) -> Router { Router::new() .nest( diff --git a/agent_api_http/src/v1/identity/keys/mod.rs b/agent_api_http/src/v1/identity/keys/mod.rs new file mode 100644 index 00000000..794f8b86 --- /dev/null +++ b/agent_api_http/src/v1/identity/keys/mod.rs @@ -0,0 +1,175 @@ +use crate::IdentityContext; +use agent_identity::managed_key::aggregate::{ManagedKey, SigningAlgorithm}; +use agent_identity::managed_key::command::ManagedKeyCommand; + +use agent_shared::handlers::{command_handler, query_handler}; +use axum::extract::{Json, State}; +use http_api_problem::ApiError; +use hyper::StatusCode; +use serde::{Deserialize, Serialize}; + +/// Data transfer object for Managed Keys. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ManagedKeyDto { + #[serde(rename = "id")] + pub managed_key_id: String, + pub key_id: String, + pub alias: String, + pub signing_algorithm: Option, +} + +impl From for ManagedKeyDto { + fn from(value: ManagedKey) -> Self { + Self { + managed_key_id: value.managed_key_id, + key_id: value.key_id, + alias: value.alias, + signing_algorithm: value.signing_algorithm, + } + } +} + +#[derive(Deserialize, Serialize, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct PostGenerateKey { + pub alias: String, + pub signature_algorithm: Option, +} + +#[axum_macros::debug_handler] +pub(crate) async fn generate_key( + State(context): State, + Json(payload): Json, +) -> Result<(StatusCode, String), ApiError> { + let signing_algorithm = payload.signature_algorithm.unwrap_or(SigningAlgorithm::ES256); + + let managed_key_id = context + .key_generation_saga + .generate_key(payload.alias, signing_algorithm) + .await + .map_err(|e| { + tracing::error!("Saga failed: {:?}", e); + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR) + })?; + + Ok((StatusCode::CREATED, managed_key_id)) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PostRemoveKey { + pub key_id: String, +} + +pub(crate) async fn remove_key( + State(context): State, + Json(payload): Json, +) -> Result { + let managed_key_id = get_managed_key_id(&payload.key_id, &context).await?; + + context.key_removal_saga.remove_key(managed_key_id).await; + + Ok(StatusCode::NO_CONTENT) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PostRenameKeyAlias { + pub key_id: String, + pub new_alias: String, +} + +pub(crate) async fn rename_key_alias( + State(context): State, + Json(payload): Json, +) -> Result { + let managed_key_id = get_managed_key_id(&payload.key_id, &context).await?; + + let command = ManagedKeyCommand::UpdateKeyAlias { + new_alias: payload.new_alias, + }; + + command_handler(&managed_key_id, &context.state.command.managed_key, command) + .await + .map_err(|e| { + tracing::error!("Saga failed: {:?}", e); + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR) + })?; + + Ok(StatusCode::NO_CONTENT) +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PostSetSigningKey { + pub key_id: String, +} + +pub(crate) async fn set_signing_key( + State(context): State, + Json(payload): Json, +) -> Result { + let managed_key_id = get_managed_key_id(&payload.key_id, &context).await?; + + let command = ManagedKeyCommand::SetSigningKey {}; + + command_handler(&managed_key_id, &context.state.command.managed_key, command) + .await + .map_err(|e| { + tracing::error!("Saga failed: {:?}", e); + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR) + })?; + + Ok(StatusCode::NO_CONTENT) +} + +#[axum_macros::debug_handler] +pub(crate) async fn list_all( + State(context): State, +) -> Result<(StatusCode, Json>), ApiError> { + let view = query_handler("all_managed_keys", &context.state.query.all_managed_keys) + .await + .map_err(|e| { + tracing::error!("Query failed: {:?}", e); + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR) + })?; + + let keys = match view { + Some(all_keys_view) => all_keys_view + .managed_keys + .into_values() + .filter_map(|key_view| { + (!key_view.is_removed).then(|| ManagedKeyDto { + managed_key_id: key_view.managed_key_id, + key_id: key_view.key_id, + alias: key_view.alias, + signing_algorithm: key_view.signing_algorithm, + }) + }) + .collect(), + None => Vec::new(), + }; + + Ok((StatusCode::OK, Json(keys))) +} + +// Helper function to fetch the managed_key_id by its key_id. +async fn get_managed_key_id(key_id: &str, context: &IdentityContext) -> Result { + let view = query_handler("all_managed_keys", &context.state.query.all_managed_keys) + .await + .map_err(|e| { + tracing::error!("Query failed: {:?}", e); + ApiError::new(StatusCode::INTERNAL_SERVER_ERROR) + })?; + + match view { + Some(all_keys_view) => all_keys_view + .managed_keys + .into_values() + .find(|key_view| key_view.key_id == key_id && !key_view.is_removed) + .map(|key_view| key_view.managed_key_id) + .ok_or_else(|| ApiError::new(StatusCode::NOT_FOUND)), + None => Err(ApiError::new(StatusCode::NOT_FOUND)), + } +} diff --git a/agent_api_http/src/v1/identity/mod.rs b/agent_api_http/src/v1/identity/mod.rs new file mode 100644 index 00000000..988501d7 --- /dev/null +++ b/agent_api_http/src/v1/identity/mod.rs @@ -0,0 +1,34 @@ +pub mod keys; + +use crate::v1::identity::keys::{generate_key, list_all, remove_key, rename_key_alias, set_signing_key}; +use crate::API_VERSION; +use agent_identity::application::sagas::key_generation_saga::KeyGenerationSaga; +use agent_identity::application::sagas::key_removal_saga::KeyRemovalSaga; +use agent_identity::state::IdentityState; +use axum::{ + routing::{get, post}, + Router, +}; + +use std::sync::Arc; + +#[derive(Clone)] +pub struct IdentityContext { + pub state: Arc, + pub key_generation_saga: KeyGenerationSaga, + pub key_removal_saga: KeyRemovalSaga, +} + +pub fn router(context: IdentityContext) -> Router { + Router::new() + .nest( + API_VERSION, + Router::new() + .route("/keys/generate-new-key", post(generate_key)) + .route("/keys/remove-key", post(remove_key)) + .route("/keys/rename-key-alias", post(rename_key_alias)) + .route("/keys/set-signing-key", post(set_signing_key)) + .route("/keys/list-all", get(list_all)), + ) + .with_state(context) +} diff --git a/agent_api_http/src/v1/mod.rs b/agent_api_http/src/v1/mod.rs new file mode 100644 index 00000000..db53a0c9 --- /dev/null +++ b/agent_api_http/src/v1/mod.rs @@ -0,0 +1 @@ +pub mod identity; diff --git a/agent_identity/src/application/sagas/key_generation_saga.rs b/agent_identity/src/application/sagas/key_generation_saga.rs index b63ee3ab..a13d4a56 100644 --- a/agent_identity/src/application/sagas/key_generation_saga.rs +++ b/agent_identity/src/application/sagas/key_generation_saga.rs @@ -15,6 +15,7 @@ use crate::{ }; use std::sync::Arc; +#[derive(Clone)] pub struct KeyGenerationSaga { identity_state: Arc, identity_services: Arc, @@ -28,7 +29,11 @@ impl KeyGenerationSaga { } } - pub async fn generate_key(&self, alias: String, signing_algorithm: SigningAlgorithm) { + pub async fn generate_key( + &self, + alias: String, + signing_algorithm: SigningAlgorithm, + ) -> Result> { // TODO: Add undo logic!!! let managed_key_id = uuid::Uuid::new_v4().to_string(); @@ -39,20 +44,14 @@ impl KeyGenerationSaga { signing_algorithm, }; - command_handler(&managed_key_id, &self.identity_state.command.managed_key, command) - .await - .unwrap(); + command_handler(&managed_key_id, &self.identity_state.command.managed_key, command).await?; - if let Some(managed_key_view) = query_handler(&managed_key_id, &self.identity_state.query.managed_key) - .await - .unwrap() - { + if let Some(managed_key_view) = query_handler(&managed_key_id, &self.identity_state.query.managed_key).await? { let key_id = managed_key_view.key_id.clone(); let signing_algorithm = managed_key_view.signing_algorithm.unwrap(); - if let Some(all_documents_view) = query_handler("all_documents", &self.identity_state.query.all_documents) - .await - .unwrap() + if let Some(all_documents_view) = + query_handler("all_documents", &self.identity_state.query.all_documents).await? { for (document_id, document_view) in all_documents_view.documents { if document_view @@ -65,9 +64,7 @@ impl KeyGenerationSaga { signing_algorithm: signing_algorithm.clone(), }; - command_handler(&document_id, &self.identity_state.command.document, command) - .await - .unwrap(); + command_handler(&document_id, &self.identity_state.command.document, command).await?; if document_view .did_method @@ -76,13 +73,12 @@ impl KeyGenerationSaga { { let command = DocumentCommand::PublishDocument; - command_handler(&document_id, &self.identity_state.command.document, command) - .await - .unwrap(); + command_handler(&document_id, &self.identity_state.command.document, command).await?; } } } } } + Ok(managed_key_id) } } diff --git a/agent_identity/src/application/sagas/key_removal_saga.rs b/agent_identity/src/application/sagas/key_removal_saga.rs index fdc8cba2..2e173dea 100644 --- a/agent_identity/src/application/sagas/key_removal_saga.rs +++ b/agent_identity/src/application/sagas/key_removal_saga.rs @@ -15,6 +15,7 @@ use crate::{ }; use std::sync::Arc; +#[derive(Clone)] pub struct KeyRemovalSaga { identity_state: Arc, identity_services: Arc, diff --git a/agent_secret_manager/Cargo.toml b/agent_secret_manager/Cargo.toml index 73f21a4f..35d696c3 100644 --- a/agent_secret_manager/Cargo.toml +++ b/agent_secret_manager/Cargo.toml @@ -34,7 +34,9 @@ log = "0.4" oid4vc-core.workspace = true p256 = { version = "0.13", features = ["jwk"] } serde.workspace = true +strum.workspace = true tokio.workspace = true +thiserror.workspace = true url.workspace = true [dev-dependencies]