Repository: adorsys/status-list-server
Pull request #55: feat: add keypair generation to startup logic
Author: login: Ngha-Boris
Created: 2025-04-24T12:13:50Z | Merged: 2025-05-23T09:31:33Z



Diff:
diff --git a/Cargo.toml b/Cargo.toml
index f050b72e..121d03e4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,4 @@
 [package]
-
 name = "status-list-server"
 version = "0.1.0"
 edition = "2021"
@@ -26,7 +25,7 @@ instant-acme = "0.7"
 serde = { version = "1.0", features = ["derive"] }
 sqlx = { version = "0.8", features = ["postgres", "runtime-tokio"] }
 thiserror = "2.0"
-tokio = { version = "1.16", features = ["full"] }
+tokio = { version = "1", features = ["full", "macros", "net", "rt-multi-thread"] }
 tower = "0.5"
 tower-http = { version = "0.6", features = ["cors", "trace", "catch-panic"] }
 tracing = "0.1.41"
@@ -37,9 +36,9 @@ sea-orm = { version = "1.1.7", features = [
     "runtime-tokio-native-tls",
     "macros",
 ] }
-aws-config = "1.6"
-aws-sdk-secretsmanager = "1.68"
-
+aws-config = {version = "1.6", features = ["behavior-version-latest"] }
+aws-sdk-secretsmanager = "1.71"
+aws_secretsmanager_caching = "1.2.1"
 
 [dev-dependencies]
 tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
@@ -47,4 +46,4 @@ sea-orm = { version = "1.1.7", features = ["mock"] }
 
 [dependencies.sea-orm-migration]
 version = "1.1.7"
-features = ["sqlx-postgres", "runtime-tokio-native-tls"]
+features = ["sqlx-postgres", "runtime-tokio-native-tls"]
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index 9a1bee9c..1aeedcc0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,5 +2,6 @@ pub mod auth;
 mod database;
 pub mod model;
 mod test_resources;
+mod test_utils;
 pub mod utils;
 pub mod web;
diff --git a/src/test_resources/helper.rs b/src/test_resources/helper.rs
index e23c8e54..cb89b159 100644
--- a/src/test_resources/helper.rs
+++ b/src/test_resources/helper.rs
@@ -1,6 +1,6 @@
-use crate::model::StatusRequest;
 #[cfg(test)]
-use crate::{model::StatusEntry, utils::keygen::Keypair};
+use crate::model::StatusEntry;
+use crate::model::StatusRequest;
 
 // Helper to create a test request payload with customizable bits
 #[cfg(test)]
@@ -10,10 +10,3 @@ pub fn publish_test_token(list_id: &str, status: Vec<StatusEntry>) -> StatusRequ
         status,
     }
 }
-
-// Helper to generate a test server key
-// Note: It does nothing, it's just use to build the AppState
-#[cfg(test)]
-pub fn server_key() -> Keypair {
-    Keypair::generate().unwrap()
-}
diff --git a/src/test_utils/mod.rs b/src/test_utils/mod.rs
new file mode 100644
index 00000000..7c8dd6bb
--- /dev/null
+++ b/src/test_utils/mod.rs
@@ -0,0 +1,46 @@
+#[cfg(test)]
+pub mod test {
+    use crate::utils::errors::SecretCacheError;
+    use crate::utils::state::{AppState, CacheConfig, SecretCache, SecretManager};
+    use async_trait::async_trait;
+    use aws_config::{BehaviorVersion, SdkConfig};
+    use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+    use std::sync::Arc;
+
+    // Mock implementation for testing
+    pub struct MockSecretCache {
+        pub value: Option<String>,
+    }
+
+    #[async_trait]
+    impl SecretCache for MockSecretCache {
+        async fn get_secret_string(
+            &self,
+            _secret_id: String,
+        ) -> Result<Option<String>, SecretCacheError> {
+            Ok(self.value.clone())
+        }
+    }
+
+    pub fn test_app_state(db_conn: Arc<sea_orm::DatabaseConnection>) -> AppState {
+        use crate::database::queries::SeaOrmStore;
+
+        let pem = include_str!("../test_resources/ec-private.pem").to_string();
+        let config = SdkConfig::builder()
+            .behavior_version(BehaviorVersion::latest())
+            .build();
+        let secret_manager = SecretManager::new(
+            MockSecretCache { value: Some(pem) },
+            SecretsManagerClient::new(&config),
+            "test-server-key".to_string(),
+            CacheConfig::default(),
+        );
+
+        AppState {
+            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
+            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
+            server_public_domain: "example.com".to_string(),
+            secret_manager: Arc::new(secret_manager),
+        }
+    }
+}
diff --git a/src/utils/errors.rs b/src/utils/errors.rs
index 0ed6c9b4..104eff3b 100644
--- a/src/utils/errors.rs
+++ b/src/utils/errors.rs
@@ -23,3 +23,11 @@ pub enum Error {
     #[error("Failed to decode lst")]
     DecodeError,
 }
+
+#[derive(Error, Debug)]
+pub enum SecretCacheError {
+    #[error("Failed to retrieve secret: {0}")]
+    RetrieveFailed(String),
+    #[error("AWS SDK error: {0}")]
+    AwsSdkError(String),
+}
diff --git a/src/utils/state.rs b/src/utils/state.rs
index 6380ab3d..20d7f930 100644
--- a/src/utils/state.rs
+++ b/src/utils/state.rs
@@ -1,18 +1,242 @@
+use super::keygen::Keypair;
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::{Error, SecretCacheError},
+};
+use async_trait::async_trait;
+use aws_config::BehaviorVersion;
+use aws_sdk_secretsmanager::{
+    config::Region, error::ProvideErrorMetadata, Client as SecretsManagerClient,
 };
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
-use std::sync::Arc;
+use std::{num::NonZeroUsize, sync::Arc, time::Duration};
+use tracing::info;
 
-use super::keygen::Keypair;
+// Define the SecretCache
+#[async_trait]
+pub trait SecretCache: Send + Sync {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError>;
+}
+
+// Define a wrapper struct for SecretsManagerCachingClient
+pub struct AwsSecretCache {
+    inner: SecretsManagerCachingClient,
+}
+
+// Implement SecretCache for AwsSecretCache
+#[async_trait]
+impl SecretCache for AwsSecretCache {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError> {
+        match self
+            .inner
+            .get_secret_value(&secret_id, None, None, false)
+            .await
+        {
+            Ok(output) => Ok(output.secret_string),
+            Err(e) => Err(SecretCacheError::AwsSdkError(e.to_string())),
+        }
+    }
+}
+
+/// Configuration for secret caching
+#[derive(Clone)]
+pub struct CacheConfig {
+    pub enabled: bool,
+    pub cache_size: NonZeroUsize,
+    pub ttl: Duration,
+}
+
+impl Default for CacheConfig {
+    fn default() -> Self {
+        Self {
+            enabled: true,
+            cache_size: NonZeroUsize::new(1024).unwrap(),
+            ttl: Duration::from_secs(3600),
+        }
+    }
+}
+
+/// A type that manages server secrets and their caching
+#[derive(Clone)]
+pub struct SecretManager {
+    cache: Arc<dyn SecretCache>,
+    client: Arc<SecretsManagerClient>,
+    server_secret_name: String,
+    cache_config: CacheConfig,
+}
+
+impl SecretManager {
+    pub fn new(
+        cache: impl SecretCache + 'static,
+        client: SecretsManagerClient,
+        server_secret_name: String,
+        cache_config: CacheConfig,
+    ) -> Self {
+        Self {
+            cache: Arc::new(cache),
+            client: Arc::new(client),
+            server_secret_name,
+            cache_config,
+        }
+    }
+
+    pub async fn get_secret_from_cache_or_aws(
+        &self,
+        secret_name: String,
+        use_cache: bool,
+    ) -> Result<Option<String>, SecretCacheError> {
+        if use_cache && self.cache_config.enabled {
+            self.cache.get_secret_string(secret_name).await
+        } else {
+            let result = self
+                .client
+                .get_secret_value()
+                .secret_id(secret_name)
+                .send()
+                .await;
+            match result {
+                Ok(output) => Ok(output.secret_string().map(String::from)),
+                Err(e) => Err(SecretCacheError::AwsSdkError(e.to_string())),
+            }
+        }
+    }
+
+    pub async fn get_server_secret(&self) -> Result<Option<String>, SecretCacheError> {
+        if self.cache_config.enabled {
+            self.cache
+                .get_secret_string(self.server_secret_name.clone())
+                .await
+        } else {
+            let result = self
+                .client
+                .get_secret_value()
+                .secret_id(self.server_secret_name.clone())
+                .send()
+                .await;
+            match result {
+                Ok(output) => Ok(output.secret_string().map(String::from)),
+                Err(e) => Err(SecretCacheError::AwsSdkError(e.to_string())),
+            }
+        }
+    }
+
+    /// Creates a new secret with the given name and value
+    pub async fn create_secret(&self, name: String, value: String) -> Result<(), Error> {
+        self.client
+            .create_secret()
+            .name(name)
+            .secret_string(value)
+            .send()
+            .await
+            .map_err(|e| Error::Generic(e.to_string()))?;
+        Ok(())
+    }
+
+    /// Deletes a secret by name
+    pub async fn delete_secret(&self, name: String) -> Result<(), Error> {
+        self.client
+            .delete_secret()
+            .secret_id(name)
+            .force_delete_without_recovery(true)
+            .send()
+            .await
+            .map_err(|e| Error::Generic(e.to_string()))?;
+        Ok(())
+    }
+
+    /// Updates the cache configuration
+    pub fn update_cache_config(&mut self, config: CacheConfig) {
+        self.cache_config = config;
+    }
+
+    /// Creates a new SecretManager instance and ensures the server secret exists
+    pub async fn setup(
+        secret_name: String,
+        region: String,
+        cache_config: Option<CacheConfig>,
+    ) -> Result<Self, Error> {
+        let config = aws_config::load_defaults(BehaviorVersion::latest())
+            .await
+            .into_builder()
+            .region(Region::new(region))
+            .build();
+        let client = SecretsManagerClient::new(&config);
+
+        // Ensure secret exists
+        match client
+            .describe_secret()
+            .secret_id(secret_name.clone())
+            .send()
+            .await
+        {
+            Ok(e) => {
+                tracing::info!("Server key secret already exists in AWS Secrets Manager");
+                info!("{}", e.name().unwrap_or_default());
+            }
+            Err(e) => {
+                let error_message = e.to_string();
+                let service_error = e.into_service_error();
+                if service_error.code() == Some("ResourceNotFoundException") {
+                    tracing::info!(
+                        "No server key secret found in AWS, generating and storing new keypair"
+                    );
+                    let keypair = Keypair::generate().map_err(|_| Error::KeyGenFailed)?;
+                    let pem = keypair.to_pkcs8_pem().map_err(|_| Error::PemGenFailed)?;
+                    client
+                        .create_secret()
+                        .name(secret_name.clone())
+                        .secret_string(pem)
+                        .send()
+                        .await
+                        .map_err(|e| Error::Generic(e.to_string()))?;
+                } else {
+                    tracing::error!("Error describing secret: {:?}", error_message);
+                    return Err(Error::Generic(error_message));
+                }
+            }
+        }
+
+        let cache_config = cache_config.unwrap_or_default();
+        let asm_builder = aws_sdk_secretsmanager::config::Builder::from(&config);
+        let caching_client = SecretsManagerCachingClient::from_builder(
+            asm_builder,
+            if cache_config.enabled {
+                cache_config.cache_size
+            } else {
+                NonZeroUsize::new(1).unwrap() // Minimal cache size when disabled
+            },
+            if cache_config.enabled {
+                cache_config.ttl
+            } else {
+                Duration::from_secs(1) // Minimal TTL when disabled
+            },
+            false,
+        )
+        .await
+        .map_err(|e| Error::Generic(e.to_string()))?;
+
+        let cache = AwsSecretCache {
+            inner: caching_client,
+        };
+
+        Ok(Self::new(cache, client, secret_name, cache_config))
+    }
+}
 
 #[derive(Clone)]
 pub struct AppState {
     pub credential_repository: Arc<SeaOrmStore<Credentials>>,
     pub status_list_token_repository: Arc<SeaOrmStore<StatusListToken>>,
-    pub server_key: Arc<Keypair>,
+    pub secret_manager: Arc<SecretManager>,
     pub server_public_domain: String,
 }
 
@@ -26,10 +250,13 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    let secret_name =
+        std::env::var("SERVER_KEY_SECRET_NAME").expect("SERVER_KEY_SECRET_NAME env not set");
+    let region = std::env::var("AWS_REGION").expect("AWS_REGION env not set");
+
+    let secret_manager = SecretManager::setup(secret_name, region, None)
+        .await
+        .expect("Failed to setup secret manager");
 
     let server_public_domain =
         std::env::var("SERVER_PUBLIC_DOMAIN").expect("SERVER_PUBLIC_DOMAIN env not set");
@@ -38,7 +265,7 @@ pub async fn setup() -> AppState {
     AppState {
         credential_repository: Arc::new(SeaOrmStore::new(Arc::clone(&db))),
         status_list_token_repository: Arc::new(SeaOrmStore::new(Arc::clone(&db))),
-        server_key: Arc::new(server_key),
+        secret_manager: Arc::new(secret_manager),
         server_public_domain,
     }
 }
diff --git a/src/web/handlers/status_list/handler.rs b/src/web/handlers/status_list/handler.rs
index bf81bb31..733ff95a 100644
--- a/src/web/handlers/status_list/handler.rs
+++ b/src/web/handlers/status_list/handler.rs
@@ -110,7 +110,20 @@ async fn build_status_list_token(
         }
     };
 
-    let server_key = repo.server_key.clone();
+    // Load the server key from the secret manager
+    let pem = repo
+        .secret_manager
+        .get_server_secret()
+        .await
+        .map_err(|e| {
+            tracing::error!("Failed to load server key from cache: {e}");
+            StatusListError::InternalServerError
+        })?
+        .ok_or(StatusListError::InternalServerError)?;
+    let server_key = Keypair::from_pkcs8_pem(&pem).map_err(|e| {
+        tracing::error!("Failed to parse server key: {e:?}");
+        StatusListError::InternalServerError
+    })?;
 
     let apply_gzip = |data: &[u8]| -> Result<Vec<u8>, StatusListError> {
         let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
@@ -410,9 +423,8 @@ fn update_status(lst: &str, updates: Vec<StatusEntry>) -> Result<String, StatusL
 mod tests {
     use super::*;
     use crate::{
-        database::queries::SeaOrmStore,
         model::{status_list_tokens, StatusList, StatusListToken},
-        utils::state::AppState,
+        test_utils::test::test_app_state,
     };
     use axum::{
         body::to_bytes,
@@ -430,10 +442,6 @@ mod tests {
     use serde_json::json;
     use std::{io::Read, sync::Arc};
 
-    fn server_key() -> Keypair {
-        Keypair::from_pkcs8_pem(include_str!("../../../test_resources/ec-private.pem")).unwrap()
-    }
-
     #[tokio::test]
     async fn test_get_status_list_jwt_success() {
         let mock_db = MockDatabase::new(DatabaseBackend::Postgres);
@@ -458,12 +466,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let mut headers = HeaderMap::new();
         headers.insert(
@@ -490,8 +493,15 @@ mod tests {
         decoder.read_to_end(&mut body_bytes).unwrap();
         let body_str = std::str::from_utf8(&body_bytes).unwrap();
 
-        let decoding_key_pem = app_state
-            .server_key
+        // Load the key from the cache
+        let pem = app_state
+            .secret_manager
+            .get_server_secret()
+            .await
+            .unwrap()
+            .unwrap();
+        let server_key = Keypair::from_pkcs8_pem(&pem).unwrap();
+        let decoding_key_pem = server_key
             .verifying_key()
             .to_public_key_pem(LineEnding::default())
             .unwrap()
@@ -536,12 +546,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let mut headers = HeaderMap::new();
         headers.insert(
@@ -569,10 +574,18 @@ mod tests {
 
         let cwt = CoseSign1::from_slice(&body_bytes).unwrap();
 
-        // verify signature
-        let binding = app_state.server_key.clone();
-        let signing_key = binding.signing_key();
+        // Load the key from the cache
+        let pem = app_state
+            .secret_manager
+            .get_server_secret()
+            .await
+            .unwrap()
+            .unwrap();
+        let server_key = Keypair::from_pkcs8_pem(&pem).unwrap();
+        let signing_key = server_key.signing_key();
         let verifying_key = VerifyingKey::from(signing_key);
+
+        // Verify signature
         let result = cwt.verify_signature(&[], |sig, data| {
             let signature = Signature::from_slice(sig).unwrap();
             verifying_key.verify(data, &signature)
@@ -640,12 +653,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let mut headers = HeaderMap::new();
         headers.insert(
@@ -667,12 +675,7 @@ mod tests {
         let mock_db = MockDatabase::new(DatabaseBackend::Postgres);
         let db_conn = Arc::new(mock_db.into_connection());
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let mut headers = HeaderMap::new();
         headers.insert(http::header::ACCEPT, "application/xml".parse().unwrap()); // unsupported
@@ -728,12 +731,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = Arc::new(AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        });
+        let app_state = test_app_state(db_conn.clone());
 
         let update_body = json!({
             "updates": [
@@ -742,7 +740,7 @@ mod tests {
         });
 
         let response = update_statuslist(
-            State(app_state),
+            State(app_state.into()),
             Path("test_list".to_string()),
             Json(update_body),
         )
@@ -761,12 +759,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = Arc::new(AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        });
+        let app_state = test_app_state(db_conn.clone());
 
         let update_body = json!({
             "updates": [
@@ -775,7 +768,7 @@ mod tests {
         });
 
         let response = update_statuslist(
-            State(app_state),
+            State(app_state.into()),
             Path("test_list".to_string()),
             Json(update_body),
         )
diff --git a/src/web/handlers/status_list/publish_token_status.rs b/src/web/handlers/status_list/publish_token_status.rs
index 22ffeafc..c636795a 100644
--- a/src/web/handlers/status_list/publish_token_status.rs
+++ b/src/web/handlers/status_list/publish_token_status.rs
@@ -83,10 +83,9 @@ pub async fn publish_token_status(
 mod tests {
     use super::*;
     use crate::{
-        database::queries::SeaOrmStore,
         model::{status_list_tokens, Status, StatusEntry, StatusListToken},
-        test_resources::helper::{publish_test_token, server_key},
-        utils::state::AppState,
+        test_resources::helper::publish_test_token,
+        test_utils::test::test_app_state,
     };
     use axum::{extract::State, Json};
     use sea_orm::{DatabaseBackend, MockDatabase};
@@ -141,12 +140,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let response = publish_token_status(
             State(app_state),
@@ -209,12 +203,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         // Perform the insertion
         let _ = publish_token_status(
@@ -271,12 +260,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let response = match publish_token_status(
             State(app_state),
@@ -328,12 +312,7 @@ mod tests {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let response = publish_token_status(
             State(app_state.clone()),
@@ -399,12 +378,7 @@ mod tests {
                 ])
                 .into_connection(),
         );
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let response = publish_token_status(
             State(app_state),
@@ -429,12 +403,7 @@ mod tests {
             }],
         );
         let db_conn = Arc::new(mock_db.into_connection());
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
+        let app_state = test_app_state(db_conn.clone());
 
         let response = match publish_token_status(
             State(app_state),
diff --git a/src/web/handlers/status_list/update_token_status.rs b/src/web/handlers/status_list/update_token_status.rs
index 507ecdbd..d952c038 100644
--- a/src/web/handlers/status_list/update_token_status.rs
+++ b/src/web/handlers/status_list/update_token_status.rs
@@ -94,12 +94,11 @@ mod test {
     use sea_orm::{DatabaseBackend, MockDatabase};
 
     use crate::{
-        database::queries::SeaOrmStore,
         model::{
             status_list_tokens, Status, StatusEntry, StatusList, StatusListToken, StatusRequest,
         },
-        test_resources::helper::server_key,
-        utils::{lst_gen::create_status_list, state::AppState},
+        test_utils::test::test_app_state,
+        utils::lst_gen::create_status_list,
         web::handlers::status_list::update_token_status::update_token_status,
     };
 
@@ -163,13 +162,7 @@ mod test {
                 .into_connection(),
         );
 
-        let app_state = AppState {
-            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
-            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
-            server_key: Arc::new(server_key()),
-            server_public_domain: "example.com".to_string(),
-        };
-
+        let app_state = test_app_state(db_conn.clone());
         let response = update_token_status(
             State(app_state),
             Extension("issuer".to_string()),


Reviews:
user: login: Christiantyemele
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T08:23:27Z
user: login: Christiantyemele
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T08:44:53Z
user: login: Christiantyemele
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T08:55:36Z
user: login: Hermann-Core
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T09:26:32Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T09:42:51Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T09:42:54Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T09:44:55Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T09:45:04Z
user: login: IngridPuppet
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T14:49:44Z
user: login: Hermann-Core
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-05T16:15:35Z
user: login: Christiantyemele
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T11:03:55Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T11:23:26Z
user: login: Hermann-Core
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T09:21:34Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T09:39:52Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T09:40:22Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T09:41:02Z
user: login: Hermann-Core
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T09:03:14Z
user: login: Hermann-Core
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T14:21:06Z
user: login: Hermann-Core
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-19T14:21:37Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T07:15:08Z
user: login: Ngha-Boris
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T07:24:27Z
user: login: Hermann-Core
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T07:52:38Z
user: login: Hermann-Core
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T08:10:07Z
user: login: Hermann-Core
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T09:24:28Z
user: login: IngridPuppet
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T09:33:47Z
user: login: Christiantyemele
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-22T15:24:12Z

Review comments:
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key_mut().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
+        tracing::info!("Loading existing keypair from {}", pem_path.display());
+        let pem_content = fs::read_to_string(pem_path).map_err(|e| {
+            tracing::error!("Failed to read key file: {}", e);
+            Error::ReadCertificate(pem_path.to_path_buf())
+        })?;
+
+        if pem_content
+            .trim()
+            .starts_with("-----BEGIN PRIVATE KEY-----")
+        {
+            let normalized_pem = pem_content.trim();
+
+            match Keypair::from_pkcs8_pem(normalized_pem) {
+                Ok(mut keypair) => {
+                    if let Err(e) = validate_keypair(&mut keypair) {
+                        tracing::warn!("Loaded keypair validation failed: {}", e);
+                    } else {
+                        tracing::info!("Successfully loaded and validated keypair");
+                        return Ok(keypair);
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to parse key file: {}", e);
+                }
+            }
+        } else {
+            tracing::warn!("Key file does not contain a valid PEM key");
+        }
+    } else {
+        tracing::info!("Key file does not exist at {}", pem_path.display());
+    }
+
+    tracing::info!("Generating new keypair");
+    let mut keypair = Keypair::generate().map_err(|e| {
+        tracing::error!("Failed to generate new keypair: {}", e);
+        e
+    })?;
+
+    validate_keypair(&mut keypair).map_err(|e| {
+        tracing::error!("Failed to validate new keypair: {}", e);
+        e
+    })?;
path: src/utils/state.rs
user: login: Christiantyemele
body: why are you validating the keypair here? you just created it not need doing this
created_at: 2025-04-25T08:23:25Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key_mut().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
+        tracing::info!("Loading existing keypair from {}", pem_path.display());
+        let pem_content = fs::read_to_string(pem_path).map_err(|e| {
+            tracing::error!("Failed to read key file: {}", e);
+            Error::ReadCertificate(pem_path.to_path_buf())
+        })?;
+
+        if pem_content
+            .trim()
+            .starts_with("-----BEGIN PRIVATE KEY-----")
+        {
+            let normalized_pem = pem_content.trim();
+
+            match Keypair::from_pkcs8_pem(normalized_pem) {
+                Ok(mut keypair) => {
+                    if let Err(e) = validate_keypair(&mut keypair) {
+                        tracing::warn!("Loaded keypair validation failed: {}", e);
+                    } else {
+                        tracing::info!("Successfully loaded and validated keypair");
+                        return Ok(keypair);
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to parse key file: {}", e);
+                }
+            }
+        } else {
+            tracing::warn!("Key file does not contain a valid PEM key");
+        }
+    } else {
+        tracing::info!("Key file does not exist at {}", pem_path.display());
+    }
+
+    tracing::info!("Generating new keypair");
+    let mut keypair = Keypair::generate().map_err(|e| {
+        tracing::error!("Failed to generate new keypair: {}", e);
+        e
+    })?;
+
+    validate_keypair(&mut keypair).map_err(|e| {
+        tracing::error!("Failed to validate new keypair: {}", e);
+        e
+    })?;
path: src/utils/state.rs
user: login: Christiantyemele
body: Thinking of it again i think this check might usefull
created_at: 2025-04-25T08:44:51Z
author_association: COLLABORATOR
diff_hunk: @@ -52,6 +53,11 @@ impl Keypair {
         &self.repr.key
     }
 
+    /// Get mutable access to the signing key
+    pub fn signing_key_mut(&mut self) -> &mut SigningKey {
+        &mut self.repr.key
+    }
+
path: src/utils/keygen.rs
user: login: Christiantyemele
body: you don not need this you can use the existing function, just bring the `Signer` trait from the ``p256` instead of the `SignerMut` 
created_at: 2025-04-25T08:55:26Z
author_association: COLLABORATOR
diff_hunk: @@ -39,6 +39,7 @@ sea-orm = { version = "1.1.7", features = [
 
 [dev-dependencies]
 sea-orm = { version = "1.1.7", features = ["mock"] }
+tempfile = "3.8"
path: Cargo.toml
user: login: Hermann-Core
body: Do you use this dep?
created_at: 2025-04-30T08:10:22Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
path: src/utils/state.rs
user: login: Hermann-Core
body: The keypair has already being tested in the `tests` module? What is the use case of this validation?
Additionnally it is not a good idea to take a mutable access to the key. Do you have a good reason for that?
created_at: 2025-04-30T08:40:16Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
path: src/utils/state.rs
user: login: Hermann-Core
body: Prefer using `try_exists` instead as suggested by the stdlib doc:
> Returns true if the path points at an existing entity.
>
> Warning: this method may be error-prone, consider using [try_exists()](https://doc.rust-lang.org/stable/std/path/struct.Path.html#method.try_exists) instead! It also has a risk of introducing time-of-check to time-of-use (TOCTOU) bugs.
created_at: 2025-04-30T08:43:56Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
+        tracing::info!("Loading existing keypair from {}", pem_path.display());
+        let pem_content = fs::read_to_string(pem_path).map_err(|e| {
+            tracing::error!("Failed to read key file: {}", e);
+            Error::ReadCertificate(pem_path.to_path_buf())
+        })?;
path: src/utils/state.rs
user: login: Hermann-Core
body: There is convenient associated function for loading a keypair from a PEM file: `fn read_pkcs8_pem_file(path: impl AsRef<Path>) -> Result<Self>`. I suggest wrapping it into an associated function within our existing Keypair implementation.
created_at: 2025-04-30T08:56:52Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
+        tracing::info!("Loading existing keypair from {}", pem_path.display());
+        let pem_content = fs::read_to_string(pem_path).map_err(|e| {
+            tracing::error!("Failed to read key file: {}", e);
+            Error::ReadCertificate(pem_path.to_path_buf())
+        })?;
+
+        if pem_content
+            .trim()
+            .starts_with("-----BEGIN PRIVATE KEY-----")
+        {
+            let normalized_pem = pem_content.trim();
path: src/utils/state.rs
user: login: Hermann-Core
body: [`pem`](https://docs.rs/pem/3.0.5/pem/) crate exists for this purpose.
created_at: 2025-04-30T09:01:53Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
+        tracing::info!("Loading existing keypair from {}", pem_path.display());
+        let pem_content = fs::read_to_string(pem_path).map_err(|e| {
+            tracing::error!("Failed to read key file: {}", e);
+            Error::ReadCertificate(pem_path.to_path_buf())
+        })?;
+
+        if pem_content
+            .trim()
+            .starts_with("-----BEGIN PRIVATE KEY-----")
+        {
+            let normalized_pem = pem_content.trim();
+
+            match Keypair::from_pkcs8_pem(normalized_pem) {
+                Ok(mut keypair) => {
+                    if let Err(e) = validate_keypair(&mut keypair) {
+                        tracing::warn!("Loaded keypair validation failed: {}", e);
+                    } else {
+                        tracing::info!("Successfully loaded and validated keypair");
+                        return Ok(keypair);
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to parse key file: {}", e);
+                }
+            }
+        } else {
+            tracing::warn!("Key file does not contain a valid PEM key");
+        }
+    } else {
+        tracing::info!("Key file does not exist at {}", pem_path.display());
+    }
path: src/utils/state.rs
user: login: Hermann-Core
body: All these logics can be simplified with a single call to `read_pkcs8_pem_file`, which handles both PEM loading and keypair validation in one step. It encapsulates all necessary error handling, covering the full range of possible issues during key loading and validation.
created_at: 2025-04-30T09:09:37Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
+        tracing::info!("Loading existing keypair from {}", pem_path.display());
+        let pem_content = fs::read_to_string(pem_path).map_err(|e| {
+            tracing::error!("Failed to read key file: {}", e);
+            Error::ReadCertificate(pem_path.to_path_buf())
+        })?;
+
+        if pem_content
+            .trim()
+            .starts_with("-----BEGIN PRIVATE KEY-----")
+        {
+            let normalized_pem = pem_content.trim();
+
+            match Keypair::from_pkcs8_pem(normalized_pem) {
+                Ok(mut keypair) => {
+                    if let Err(e) = validate_keypair(&mut keypair) {
+                        tracing::warn!("Loaded keypair validation failed: {}", e);
+                    } else {
+                        tracing::info!("Successfully loaded and validated keypair");
+                        return Ok(keypair);
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to parse key file: {}", e);
+                }
+            }
+        } else {
+            tracing::warn!("Key file does not contain a valid PEM key");
+        }
+    } else {
+        tracing::info!("Key file does not exist at {}", pem_path.display());
+    }
+
+    tracing::info!("Generating new keypair");
+    let mut keypair = Keypair::generate().map_err(|e| {
+        tracing::error!("Failed to generate new keypair: {}", e);
+        e
+    })?;
+
+    validate_keypair(&mut keypair).map_err(|e| {
+        tracing::error!("Failed to validate new keypair: {}", e);
+        e
+    })?;
path: src/utils/state.rs
user: login: Hermann-Core
body: I strongly don't think this function is necessary. The key validation is already done internally.
created_at: 2025-04-30T09:11:08Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
+        tracing::info!("Loading existing keypair from {}", pem_path.display());
+        let pem_content = fs::read_to_string(pem_path).map_err(|e| {
+            tracing::error!("Failed to read key file: {}", e);
+            Error::ReadCertificate(pem_path.to_path_buf())
+        })?;
+
+        if pem_content
+            .trim()
+            .starts_with("-----BEGIN PRIVATE KEY-----")
+        {
+            let normalized_pem = pem_content.trim();
+
+            match Keypair::from_pkcs8_pem(normalized_pem) {
+                Ok(mut keypair) => {
+                    if let Err(e) = validate_keypair(&mut keypair) {
+                        tracing::warn!("Loaded keypair validation failed: {}", e);
+                    } else {
+                        tracing::info!("Successfully loaded and validated keypair");
+                        return Ok(keypair);
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to parse key file: {}", e);
+                }
+            }
+        } else {
+            tracing::warn!("Key file does not contain a valid PEM key");
+        }
+    } else {
+        tracing::info!("Key file does not exist at {}", pem_path.display());
+    }
+
+    tracing::info!("Generating new keypair");
+    let mut keypair = Keypair::generate().map_err(|e| {
+        tracing::error!("Failed to generate new keypair: {}", e);
+        e
+    })?;
+
+    validate_keypair(&mut keypair).map_err(|e| {
+        tracing::error!("Failed to validate new keypair: {}", e);
+        e
+    })?;
+
+    tracing::info!("Saving new keypair to {}", pem_path.display());
+
+    if let Some(parent) = pem_path.parent() {
+        fs::create_dir_all(parent).map_err(|e| {
+            tracing::error!("Failed to create key directory: {}", e);
+            Error::Generic("Failed to create key directory".to_string())
+        })?;
+    }
+
+    let pem_content = keypair.to_pkcs8_pem().map_err(|e| {
+        tracing::error!("Failed to convert key to PEM: {}", e);
+        e
+    })?;
+
+    fs::write(pem_path, pem_content.as_bytes()).map_err(|e| {
+        tracing::error!("Failed to write key file: {}", e);
+        Error::Generic("Failed to write key file".to_string())
+    })?;
path: src/utils/state.rs
user: login: Hermann-Core
body: Again, there's a more convenient and concise way to handle all of these using a single call to `fn write_pkcs8_pem_file(&self, path: impl AsRef<Path>) -> Result<()>`. It takes care of writing the keypair with proper error handling. Consider wrapping this in a method within our existing Keypair implementation for ease of use.

**Edit:** Changing `fn write_pkcs8_der_file(&self, path: impl AsRef<Path>) -> Result<()>` to `fn write_pkcs8_pem_file(&self, path: impl AsRef<Path>) -> Result<()>`
created_at: 2025-04-30T09:18:05Z
author_association: COLLABORATOR
diff_hunk: @@ -37,3 +125,85 @@ pub async fn setup() -> AppState {
         server_key: Arc::new(server_key),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs;
+    use std::path::Path;
+
+    #[test]
+    fn test_load_or_generate_keypair_with_env() {
+        let temp_path_str = "./src/test_resources/test_key_roundtrip.pem".to_string();
+        let temp_path = Path::new(&temp_path_str);
+
+        let test_resources_dir = Path::new("./src/test_resources");
+        if !test_resources_dir.exists() {
+            fs::create_dir_all(test_resources_dir)
+                .expect("Failed to create test_resources directory");
+        }
+
+        if temp_path.exists() {
+            fs::remove_file(temp_path).expect("Failed to remove existing test file");
+        }
+
+        // First call: Generate and save a keypair
+        let mut keypair1 =
+            load_or_generate_keypair(temp_path).expect("Failed to load or generate first keypair");
+        assert!(validate_keypair(&mut keypair1).is_ok());
+        assert!(temp_path.exists(), "File should exist after first call");
+
+        // Second call: Load the saved keypair
+        let mut keypair2 =
+            load_or_generate_keypair(temp_path).expect("Failed to load or generate second keypair");
+        assert!(validate_keypair(&mut keypair2).is_ok());
+
+        // Verify both keypairs are identical
+        let pem1 = keypair1
+            .to_pkcs8_pem()
+            .expect("Failed to get PEM from first keypair");
+        let pem2 = keypair2
+            .to_pkcs8_pem()
+            .expect("Failed to get PEM from second keypair");
+        assert_eq!(pem1, pem2, "Keypairs should match when loaded from file");
+    }
+
+    #[test]
+    fn test_load_or_generate_keypair_invalid_file() {
+        let temp_path_str = "./src/test_resources/test_key_invalid_content.pem".to_string();
+        let temp_path = Path::new(&temp_path_str);
+
+        let test_resources_dir = Path::new("./src/test_resources");
+        if !test_resources_dir.exists() {
+            fs::create_dir_all(test_resources_dir)
+                .expect("Failed to create test_resources directory");
+        }
+
+        if temp_path.exists() {
+            fs::remove_file(temp_path).expect("Failed to remove existing test file");
+        }
+
+        // Write invalid content
+        fs::write(temp_path, "invalid key content").expect("Failed to write invalid content");
+
+        // Call the function: Should overwrite with a valid keypair
+        let mut keypair =
+            load_or_generate_keypair(temp_path).expect("Failed to load or generate keypair");
+        assert!(validate_keypair(&mut keypair).is_ok());
+
+        // Verify the file was overwritten
+        let saved_content = fs::read_to_string(temp_path).expect("Failed to read saved key");
+        assert!(!saved_content.is_empty(), "Saved key should not be empty");
+        assert!(
+            saved_content
+                .trim()
+                .starts_with("-----BEGIN PRIVATE KEY-----"),
+            "Saved content should be a valid PEM key"
+        );
+        assert_ne!(
+            saved_content.trim(),
+            "invalid key content",
+            "Saved key should replace invalid content"
+        );
+    }
+}
path: src/utils/state.rs
user: login: Hermann-Core
body: Consider using the `tempfile` crate to avoid leaving unnecessary files on the filesystem. It provides a temporary directory that is automatically cleaned up on drop.
created_at: 2025-04-30T09:25:01Z
author_association: COLLABORATOR
path: src/test_resources/test_key_invalid_content.pem
user: login: Hermann-Core
body: Do we really want this file to be persitent?
created_at: 2025-04-30T09:26:16Z
author_association: COLLABORATOR
path: src/test_resources/test_key_roundtrip.pem
user: login: Hermann-Core
body: Do we really want this file to be persitent?
created_at: 2025-04-30T09:26:25Z
author_association: COLLABORATOR
path: src/test_resources/test_key_roundtrip.pem
user: login: Ngha-Boris
body: Still just for test purposes
created_at: 2025-04-30T09:42:51Z
author_association: COLLABORATOR
path: src/test_resources/test_key_invalid_content.pem
user: login: Ngha-Boris
body: It is just for a test purposes
created_at: 2025-04-30T09:42:53Z
author_association: COLLABORATOR
diff_hunk: @@ -39,6 +39,7 @@ sea-orm = { version = "1.1.7", features = [
 
 [dev-dependencies]
 sea-orm = { version = "1.1.7", features = ["mock"] }
+tempfile = "3.8"
path: Cargo.toml
user: login: Ngha-Boris
body: No I forgot to remove it. Let me remove it right a way.
created_at: 2025-04-30T09:44:55Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +20,90 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn validate_keypair(keypair: &mut Keypair) -> Result<(), Error> {
+    // Test signing and verifying to ensure the keypair is valid
+    let test_data = b"test data";
+    let signature: Signature = keypair.signing_key().sign(test_data);
+    if keypair
+        .verifying_key()
+        .verify(test_data, &signature)
+        .is_err()
+    {
+        return Err(Error::Generic("Keypair validation failed".to_string()));
+    }
+    Ok(())
+}
+
+fn load_or_generate_keypair(pem_path: &Path) -> Result<Keypair, Error> {
+    tracing::debug!("Loading keypair from {}", pem_path.display());
+
+    if pem_path.exists() {
path: src/utils/state.rs
user: login: Ngha-Boris
body: Okay 
created_at: 2025-04-30T09:45:04Z
author_association: COLLABORATOR
diff_hunk: @@ -72,18 +74,37 @@ impl Keypair {
     pub fn to_pkcs8_pem(&self) -> Result<String, Error> {
         self.repr
             .key
-            .to_pkcs8_pem(LineEnding::default())
+            .to_pkcs8_pem(LineEnding::LF)
             .map_err(|err| {
                 tracing::error!("Failed to convert signing key to PEM: {err:?}");
                 Error::PemGenFailed
             })
-            .map(|pem| pem.to_string())
+            .map(|pem| pem.to_string().trim().to_string())
path: src/utils/keygen.rs
user: login: IngridPuppet
body: Does this work? I believe there is a shorter syntax.
```suggestion
            .map(|pem| pem.trim().to_owned())
```
created_at: 2025-05-05T14:32:51Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +18,77 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn load_or_generate_keypair() -> Result<Keypair, Error> {
+    if let Ok(pem_path) = std::env::var("SERVER_KEY_PATH") {
+        let pem_path = Path::new(&pem_path);
+        if pem_path.try_exists().unwrap_or(false) {
+            match fs::read_to_string(pem_path) {
+                Ok(pem_content) => {
+                    let trimmed_pem = pem_content.trim();
+                    match Keypair::from_pkcs8_pem(trimmed_pem) {
+                        Ok(keypair) => {
+                            tracing::info!("Loaded server key from {}", pem_path.display());
+                            return Ok(keypair);
+                        }
+                        Err(e) => {
+                            tracing::warn!(
+                                "Failed to parse keypair from {}: {}; generating a new one",
+                                pem_path.display(),
+                                e
+                            );
+                        }
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!(
+                        "Failed to read key file {}: {}; generating a new one",
+                        pem_path.display(),
+                        e
+                    );
+                }
+            }
+        } else {
+            tracing::info!(
+                "Key file does not exist at {}; generating a new one",
+                pem_path.display()
+            );
+        }
+
+        // Generate and save a new keypair
+        let keypair = Keypair::generate().map_err(|e| {
+            tracing::error!("Failed to generate new keypair: {}", e);
+            e
+        })?;
+
+        if let Some(parent) = pem_path.parent() {
+            fs::create_dir_all(parent).map_err(|e| {
+                tracing::error!("Failed to create key directory: {}", e);
+                Error::Generic("Failed to create key directory".to_string())
+            })?;
+        }
+
+        // Save the PEM content with consistent trimming
+        let pem_content = keypair.to_pkcs8_pem().map_err(|e| {
+            tracing::error!("Failed to serialize new keypair to PEM: {}", e);
+            e
+        })?;
+        fs::write(pem_path, pem_content).map_err(|e| {
+            tracing::error!("Failed to save new keypair: {}", e);
+            Error::WriteCertificate(pem_path.to_path_buf())
+        })?;
path: src/utils/state.rs
user: login: IngridPuppet
body: How is `pem_content` a certificate?
created_at: 2025-05-05T14:42:46Z
author_association: COLLABORATOR
diff_hunk: @@ -37,3 +109,93 @@ pub async fn setup() -> AppState {
         server_key: Arc::new(server_key),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env;
+    use tempfile::NamedTempFile;
+
+    #[test]
+    fn test_load_or_generate_keypair_no_env() {
+        // Clear any existing SERVER_KEY_PATH
+        env::remove_var("SERVER_KEY_PATH");
+
+        let keypair = load_or_generate_keypair().expect("Failed to load or generate keypair");
+        // Verify the keypair can be serialized to PEM
+        assert!(
+            keypair.to_pkcs8_pem().is_ok(),
+            "Keypair should serialize to PEM successfully"
+        );
+    }
+
+    #[test]
+    fn test_load_or_generate_keypair_with_env() {
+        // Create a temporary file that will be automatically deleted when the test ends
+        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
+        let temp_path = temp_file.path().to_path_buf();
+        let temp_path_str = temp_path
+            .to_str()
+            .expect("Failed to get temp path as string");
+
+        println!("Temporary file created at: {}", temp_path.display());
+
+        // Ensure we start with a clean state
+        env::remove_var("SERVER_KEY_PATH");
+        env::set_var("SERVER_KEY_PATH", temp_path_str);
+
+        // Generate a keypair and save it
+        let keypair1 = Keypair::generate().expect("Failed to generate keypair");
+        let pem1 = keypair1
+            .to_pkcs8_pem()
+            .expect("Failed to serialize keypair to PEM");
+        fs::write(&temp_path, &pem1).expect("Failed to write keypair to file");
+
+        // Test the load_or_generate_keypair function
+        let keypair2 =
+            load_or_generate_keypair().expect("Failed to load or generate second keypair");
+        let pem2 = keypair2
+            .to_pkcs8_pem()
+            .expect("Failed to serialize second keypair to PEM");
+
+        // Compare the PEMs after trimming
+        assert_eq!(
+            pem1.trim(),
+            pem2.trim(),
+            "Loaded keypair should match the original keypair"
+        );
+
+        // Clean up
+        env::remove_var("SERVER_KEY_PATH");
+    }
+
+    #[test]
+    fn test_load_or_generate_keypair_invalid_file() {
+        // Create a temporary file with invalid content
+        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
+        let temp_path = temp_file.path().to_str().expect("Failed to get temp path");
+        let invalid_content = "invalid key content";
+        std::fs::write(temp_path, invalid_content).expect("Failed to write invalid content");
+
+        // Set the environment variable
+        env::set_var("SERVER_KEY_PATH", temp_path);
+
+        // Should generate a new key despite invalid file
+        let _keypair = load_or_generate_keypair().expect("Failed to load or generate keypair");
+
+        // Verify the key was saved and is different from the invalid content
+        let saved_content = std::fs::read_to_string(temp_path).expect("Failed to read saved key");
path: src/utils/state.rs
user: login: IngridPuppet
body: I do not think it is a good idea to generate and overwrite the existing key file if you can't load it. If the problem happens to be with the server, there is a risk of losing private key files. I believe it is safer to panic. WDYT?
created_at: 2025-05-05T14:49:04Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +18,77 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn load_or_generate_keypair() -> Result<Keypair, Error> {
+    if let Ok(pem_path) = std::env::var("SERVER_KEY_PATH") {
+        let pem_path = Path::new(&pem_path);
+        if pem_path.try_exists().unwrap_or(false) {
+            match fs::read_to_string(pem_path) {
path: src/utils/state.rs
user: login: IngridPuppet
body: This logic takes too much space. Consider having a dedicated module.
created_at: 2025-05-05T14:49:34Z
author_association: COLLABORATOR
diff_hunk: @@ -22,4 +23,10 @@ pub enum Error {
     UnsupportedBits,
     #[error("Failed to decode lst")]
     DecodeError,
+    #[error("IO error: {0}")]
+    Io(#[from] io::Error),
+    #[error("Failed to save new keypair {0:?}")]
+    WriteCertificate(std::path::PathBuf),
+    #[error("Failed to parse keypair")]
path: src/utils/errors.rs
user: login: Hermann-Core
body: If the parsing fails, does this automatically means that the key is invalid?
created_at: 2025-05-05T15:36:33Z
author_association: COLLABORATOR
diff_hunk: @@ -72,18 +74,37 @@ impl Keypair {
     pub fn to_pkcs8_pem(&self) -> Result<String, Error> {
         self.repr
             .key
-            .to_pkcs8_pem(LineEnding::default())
+            .to_pkcs8_pem(LineEnding::LF)
path: src/utils/keygen.rs
user: login: Hermann-Core
body: Is there a specific reason for this change? Because the `default` associated function will choose the correct line ending depending on the target platform (from the docs).
created_at: 2025-05-05T15:39:57Z
author_association: COLLABORATOR
diff_hunk: @@ -72,18 +74,37 @@ impl Keypair {
     pub fn to_pkcs8_pem(&self) -> Result<String, Error> {
         self.repr
             .key
-            .to_pkcs8_pem(LineEnding::default())
+            .to_pkcs8_pem(LineEnding::LF)
             .map_err(|err| {
                 tracing::error!("Failed to convert signing key to PEM: {err:?}");
                 Error::PemGenFailed
             })
-            .map(|pem| pem.to_string())
+            .map(|pem| pem.to_string().trim().to_string())
     }
 
     /// Convert the private key to a pkcs8 PEM bytes
     pub fn to_pkcs8_pem_bytes(&self) -> Result<Vec<u8>, Error> {
         self.to_pkcs8_pem().map(|pem| pem.into_bytes())
     }
+
+    /// Load a keypair from a PEM file
+    pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, Error> {
+        let pem_content = std::fs::read_to_string(path.as_ref())?;
+        Self::from_pkcs8_pem(&pem_content)
path: src/utils/keygen.rs
user: login: Hermann-Core
body: You don't need to explicitly rely on the std library fs API. There is an [explicit associated function](https://docs.rs/pkcs8/0.10.2/pkcs8/trait.DecodePrivateKey.html#method.read_pkcs8_der_file) for doin as suggested the last time.
created_at: 2025-05-05T15:46:48Z
author_association: COLLABORATOR
diff_hunk: @@ -72,18 +74,37 @@ impl Keypair {
     pub fn to_pkcs8_pem(&self) -> Result<String, Error> {
         self.repr
             .key
-            .to_pkcs8_pem(LineEnding::default())
+            .to_pkcs8_pem(LineEnding::LF)
             .map_err(|err| {
                 tracing::error!("Failed to convert signing key to PEM: {err:?}");
                 Error::PemGenFailed
             })
-            .map(|pem| pem.to_string())
+            .map(|pem| pem.to_string().trim().to_string())
     }
 
     /// Convert the private key to a pkcs8 PEM bytes
     pub fn to_pkcs8_pem_bytes(&self) -> Result<Vec<u8>, Error> {
         self.to_pkcs8_pem().map(|pem| pem.into_bytes())
     }
+
+    /// Load a keypair from a PEM file
+    pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, Error> {
+        let pem_content = std::fs::read_to_string(path.as_ref())?;
+        Self::from_pkcs8_pem(&pem_content)
+    }
+
+    /// Save the keypair to a PEM file
+    pub fn to_pem_file(&self, path: impl AsRef<Path>) -> Result<(), Error> {
+        let pem = self.to_pkcs8_pem()?;
+        std::fs::write(path, pem)?;
path: src/utils/keygen.rs
user: login: Hermann-Core
body: You don't need to explicitly rely on the std library filesystem API. There is an [explicit associated function](https://docs.rs/pkcs8/0.10.2/pkcs8/trait.EncodePrivateKey.html#method.write_pkcs8_pem_file) for doin as suggested the last time.
created_at: 2025-05-05T15:48:29Z
author_association: COLLABORATOR
diff_hunk: @@ -135,4 +157,22 @@ mod tests {
 
         assert!(decoded.is_ok(), "Decoding failed: {:?}", decoded.err());
     }
+
+    #[test]
+    fn test_keypair_file_operations() {
+        let temp_file = NamedTempFile::new().unwrap();
+        let temp_path = temp_file.path();
+
+        // Generate a keypair and save it
+        let keypair1 = Keypair::generate().unwrap();
path: src/utils/keygen.rs
user: login: Hermann-Core
body: I suggest giving better descriptive names rather than numbered names.
```suggestion
        let stored_keypair = Keypair::generate().unwrap();
```
created_at: 2025-05-05T15:51:03Z
author_association: COLLABORATOR
diff_hunk: @@ -135,4 +157,22 @@ mod tests {
 
         assert!(decoded.is_ok(), "Decoding failed: {:?}", decoded.err());
     }
+
+    #[test]
+    fn test_keypair_file_operations() {
+        let temp_file = NamedTempFile::new().unwrap();
+        let temp_path = temp_file.path();
+
+        // Generate a keypair and save it
+        let keypair1 = Keypair::generate().unwrap();
+        keypair1.to_pem_file(temp_path).unwrap();
+
+        // Load the keypair back
+        let keypair2 = Keypair::from_pem_file(temp_path).unwrap();
path: src/utils/keygen.rs
user: login: Hermann-Core
body: I suggest giving better descriptive names rather than numbered names.
```suggestion
        let loaded_keypair = Keypair::from_pem_file(temp_path).unwrap();
```
created_at: 2025-05-05T15:51:29Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +18,77 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn load_or_generate_keypair() -> Result<Keypair, Error> {
+    if let Ok(pem_path) = std::env::var("SERVER_KEY_PATH") {
+        let pem_path = Path::new(&pem_path);
+        if pem_path.try_exists().unwrap_or(false) {
path: src/utils/state.rs
user: login: Hermann-Core
body: That behave exactly like if you were using the `exists ` method and that is not what we want. You should instead find a better mechanism for handling errors at this level. By propagating it for example?
created_at: 2025-05-05T15:54:48Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +18,77 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn load_or_generate_keypair() -> Result<Keypair, Error> {
+    if let Ok(pem_path) = std::env::var("SERVER_KEY_PATH") {
+        let pem_path = Path::new(&pem_path);
+        if pem_path.try_exists().unwrap_or(false) {
+            match fs::read_to_string(pem_path) {
+                Ok(pem_content) => {
+                    let trimmed_pem = pem_content.trim();
+                    match Keypair::from_pkcs8_pem(trimmed_pem) {
+                        Ok(keypair) => {
+                            tracing::info!("Loaded server key from {}", pem_path.display());
+                            return Ok(keypair);
+                        }
+                        Err(e) => {
+                            tracing::warn!(
+                                "Failed to parse keypair from {}: {}; generating a new one",
+                                pem_path.display(),
+                                e
+                            );
+                        }
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!(
+                        "Failed to read key file {}: {}; generating a new one",
+                        pem_path.display(),
+                        e
+                    );
+                }
+            }
+        } else {
+            tracing::info!(
+                "Key file does not exist at {}; generating a new one",
+                pem_path.display()
+            );
+        }
path: src/utils/state.rs
user: login: Hermann-Core
body: The main idea is to leverage existing mechanisms for storing and retrieving the keypair and avoid doing things manually. I suggest using existing tools like `Keypair::from_pem_file` or similar.
created_at: 2025-05-05T16:00:52Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +18,77 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn load_or_generate_keypair() -> Result<Keypair, Error> {
+    if let Ok(pem_path) = std::env::var("SERVER_KEY_PATH") {
+        let pem_path = Path::new(&pem_path);
+        if pem_path.try_exists().unwrap_or(false) {
+            match fs::read_to_string(pem_path) {
+                Ok(pem_content) => {
+                    let trimmed_pem = pem_content.trim();
+                    match Keypair::from_pkcs8_pem(trimmed_pem) {
+                        Ok(keypair) => {
+                            tracing::info!("Loaded server key from {}", pem_path.display());
+                            return Ok(keypair);
+                        }
+                        Err(e) => {
+                            tracing::warn!(
+                                "Failed to parse keypair from {}: {}; generating a new one",
+                                pem_path.display(),
+                                e
+                            );
+                        }
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!(
+                        "Failed to read key file {}: {}; generating a new one",
+                        pem_path.display(),
+                        e
+                    );
+                }
+            }
+        } else {
+            tracing::info!(
+                "Key file does not exist at {}; generating a new one",
+                pem_path.display()
+            );
+        }
+
+        // Generate and save a new keypair
+        let keypair = Keypair::generate().map_err(|e| {
+            tracing::error!("Failed to generate new keypair: {}", e);
+            e
+        })?;
+
+        if let Some(parent) = pem_path.parent() {
+            fs::create_dir_all(parent).map_err(|e| {
+                tracing::error!("Failed to create key directory: {}", e);
+                Error::Generic("Failed to create key directory".to_string())
+            })?;
+        }
+
+        // Save the PEM content with consistent trimming
+        let pem_content = keypair.to_pkcs8_pem().map_err(|e| {
+            tracing::error!("Failed to serialize new keypair to PEM: {}", e);
+            e
+        })?;
+        fs::write(pem_path, pem_content).map_err(|e| {
+            tracing::error!("Failed to save new keypair: {}", e);
+            Error::WriteCertificate(pem_path.to_path_buf())
+        })?;
path: src/utils/state.rs
user: login: Hermann-Core
body: The main idea is to leverage existing mechanisms for storing and retrieving the keypair and avoid doing things manually. I suggest using existing tools like `Keypair::write_to_file` or similar that will internally handle all these logic internally.
created_at: 2025-05-05T16:02:25Z
author_association: COLLABORATOR
diff_hunk: @@ -15,6 +18,77 @@ pub struct AppState {
     pub server_key: Arc<Keypair>,
 }
 
+fn load_or_generate_keypair() -> Result<Keypair, Error> {
+    if let Ok(pem_path) = std::env::var("SERVER_KEY_PATH") {
+        let pem_path = Path::new(&pem_path);
+        if pem_path.try_exists().unwrap_or(false) {
+            match fs::read_to_string(pem_path) {
+                Ok(pem_content) => {
+                    let trimmed_pem = pem_content.trim();
+                    match Keypair::from_pkcs8_pem(trimmed_pem) {
+                        Ok(keypair) => {
+                            tracing::info!("Loaded server key from {}", pem_path.display());
+                            return Ok(keypair);
+                        }
+                        Err(e) => {
+                            tracing::warn!(
+                                "Failed to parse keypair from {}: {}; generating a new one",
+                                pem_path.display(),
+                                e
+                            );
+                        }
+                    }
+                }
+                Err(e) => {
+                    tracing::warn!(
+                        "Failed to read key file {}: {}; generating a new one",
+                        pem_path.display(),
+                        e
+                    );
+                }
+            }
+        } else {
+            tracing::info!(
+                "Key file does not exist at {}; generating a new one",
+                pem_path.display()
+            );
+        }
+
+        // Generate and save a new keypair
+        let keypair = Keypair::generate().map_err(|e| {
+            tracing::error!("Failed to generate new keypair: {}", e);
+            e
+        })?;
+
+        if let Some(parent) = pem_path.parent() {
+            fs::create_dir_all(parent).map_err(|e| {
+                tracing::error!("Failed to create key directory: {}", e);
+                Error::Generic("Failed to create key directory".to_string())
+            })?;
+        }
+
+        // Save the PEM content with consistent trimming
+        let pem_content = keypair.to_pkcs8_pem().map_err(|e| {
+            tracing::error!("Failed to serialize new keypair to PEM: {}", e);
+            e
+        })?;
+        fs::write(pem_path, pem_content).map_err(|e| {
+            tracing::error!("Failed to save new keypair: {}", e);
+            Error::WriteCertificate(pem_path.to_path_buf())
+        })?;
+
+        tracing::info!(
+            "Generated and saved new server key to {}",
+            pem_path.display()
+        );
+        Ok(keypair)
+    } else {
+        // No SERVER_KEY_PATH, just generate
+        tracing::info!("Generating new server keypair (no path specified)");
+        Keypair::generate()
path: src/utils/state.rs
user: login: Hermann-Core
body: Instead of doing nothing with the generated keypair when the `SERVER_KEY_PATH` isn not specified, assume a default path (a protected path maybe?) or throw an error is it is mandatory to have it.
created_at: 2025-05-05T16:06:29Z
author_association: COLLABORATOR
diff_hunk: @@ -25,10 +99,8 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    // Load or generate server key
+    let server_key = load_or_generate_keypair().expect("Failed to load or generate server key");
path: src/utils/state.rs
user: login: Hermann-Core
body: Trace the error?
created_at: 2025-05-05T16:06:57Z
author_association: COLLABORATOR
diff_hunk: @@ -37,3 +109,93 @@ pub async fn setup() -> AppState {
         server_key: Arc::new(server_key),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env;
+    use tempfile::NamedTempFile;
+
+    #[test]
+    fn test_load_or_generate_keypair_no_env() {
+        // Clear any existing SERVER_KEY_PATH
+        env::remove_var("SERVER_KEY_PATH");
+
+        let keypair = load_or_generate_keypair().expect("Failed to load or generate keypair");
+        // Verify the keypair can be serialized to PEM
+        assert!(
+            keypair.to_pkcs8_pem().is_ok(),
+            "Keypair should serialize to PEM successfully"
+        );
+    }
path: src/utils/state.rs
user: login: Hermann-Core
body: What are you trying to assert here? It is necessary foy the logic being implemented here? The keypair serialization mechanism has already being tested inside the `keygen` module.
created_at: 2025-05-05T16:09:47Z
author_association: COLLABORATOR
diff_hunk: @@ -37,3 +109,93 @@ pub async fn setup() -> AppState {
         server_key: Arc::new(server_key),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env;
+    use tempfile::NamedTempFile;
+
+    #[test]
+    fn test_load_or_generate_keypair_no_env() {
+        // Clear any existing SERVER_KEY_PATH
+        env::remove_var("SERVER_KEY_PATH");
+
+        let keypair = load_or_generate_keypair().expect("Failed to load or generate keypair");
+        // Verify the keypair can be serialized to PEM
+        assert!(
+            keypair.to_pkcs8_pem().is_ok(),
+            "Keypair should serialize to PEM successfully"
+        );
+    }
+
+    #[test]
+    fn test_load_or_generate_keypair_with_env() {
+        // Create a temporary file that will be automatically deleted when the test ends
+        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
+        let temp_path = temp_file.path().to_path_buf();
+        let temp_path_str = temp_path
+            .to_str()
+            .expect("Failed to get temp path as string");
+
+        println!("Temporary file created at: {}", temp_path.display());
+
+        // Ensure we start with a clean state
+        env::remove_var("SERVER_KEY_PATH");
+        env::set_var("SERVER_KEY_PATH", temp_path_str);
+
+        // Generate a keypair and save it
+        let keypair1 = Keypair::generate().expect("Failed to generate keypair");
+        let pem1 = keypair1
path: src/utils/state.rs
user: login: Hermann-Core
body: Consider giving better descriptive names to the variables.
created_at: 2025-05-05T16:11:34Z
author_association: COLLABORATOR
diff_hunk: @@ -37,3 +109,93 @@ pub async fn setup() -> AppState {
         server_key: Arc::new(server_key),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env;
+    use tempfile::NamedTempFile;
+
+    #[test]
+    fn test_load_or_generate_keypair_no_env() {
+        // Clear any existing SERVER_KEY_PATH
+        env::remove_var("SERVER_KEY_PATH");
+
+        let keypair = load_or_generate_keypair().expect("Failed to load or generate keypair");
+        // Verify the keypair can be serialized to PEM
+        assert!(
+            keypair.to_pkcs8_pem().is_ok(),
+            "Keypair should serialize to PEM successfully"
+        );
+    }
+
+    #[test]
+    fn test_load_or_generate_keypair_with_env() {
+        // Create a temporary file that will be automatically deleted when the test ends
+        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
+        let temp_path = temp_file.path().to_path_buf();
+        let temp_path_str = temp_path
+            .to_str()
+            .expect("Failed to get temp path as string");
+
+        println!("Temporary file created at: {}", temp_path.display());
+
+        // Ensure we start with a clean state
+        env::remove_var("SERVER_KEY_PATH");
+        env::set_var("SERVER_KEY_PATH", temp_path_str);
+
+        // Generate a keypair and save it
+        let keypair1 = Keypair::generate().expect("Failed to generate keypair");
+        let pem1 = keypair1
+            .to_pkcs8_pem()
+            .expect("Failed to serialize keypair to PEM");
+        fs::write(&temp_path, &pem1).expect("Failed to write keypair to file");
path: src/utils/state.rs
user: login: Hermann-Core
body: Consider reusing existing components.
created_at: 2025-05-05T16:12:33Z
author_association: COLLABORATOR
diff_hunk: @@ -37,3 +109,93 @@ pub async fn setup() -> AppState {
         server_key: Arc::new(server_key),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env;
+    use tempfile::NamedTempFile;
+
+    #[test]
+    fn test_load_or_generate_keypair_no_env() {
+        // Clear any existing SERVER_KEY_PATH
+        env::remove_var("SERVER_KEY_PATH");
+
+        let keypair = load_or_generate_keypair().expect("Failed to load or generate keypair");
+        // Verify the keypair can be serialized to PEM
+        assert!(
+            keypair.to_pkcs8_pem().is_ok(),
+            "Keypair should serialize to PEM successfully"
+        );
+    }
+
+    #[test]
+    fn test_load_or_generate_keypair_with_env() {
+        // Create a temporary file that will be automatically deleted when the test ends
+        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
+        let temp_path = temp_file.path().to_path_buf();
+        let temp_path_str = temp_path
+            .to_str()
+            .expect("Failed to get temp path as string");
+
+        println!("Temporary file created at: {}", temp_path.display());
+
+        // Ensure we start with a clean state
+        env::remove_var("SERVER_KEY_PATH");
+        env::set_var("SERVER_KEY_PATH", temp_path_str);
+
+        // Generate a keypair and save it
+        let keypair1 = Keypair::generate().expect("Failed to generate keypair");
+        let pem1 = keypair1
+            .to_pkcs8_pem()
+            .expect("Failed to serialize keypair to PEM");
+        fs::write(&temp_path, &pem1).expect("Failed to write keypair to file");
+
+        // Test the load_or_generate_keypair function
+        let keypair2 =
+            load_or_generate_keypair().expect("Failed to load or generate second keypair");
+        let pem2 = keypair2
path: src/utils/state.rs
user: login: Hermann-Core
body: Consider giving better descriptive names to the variables.
created_at: 2025-05-05T16:13:59Z
author_association: COLLABORATOR
diff_hunk: @@ -37,3 +109,93 @@ pub async fn setup() -> AppState {
         server_key: Arc::new(server_key),
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::env;
+    use tempfile::NamedTempFile;
+
+    #[test]
+    fn test_load_or_generate_keypair_no_env() {
+        // Clear any existing SERVER_KEY_PATH
+        env::remove_var("SERVER_KEY_PATH");
+
+        let keypair = load_or_generate_keypair().expect("Failed to load or generate keypair");
+        // Verify the keypair can be serialized to PEM
+        assert!(
+            keypair.to_pkcs8_pem().is_ok(),
+            "Keypair should serialize to PEM successfully"
+        );
+    }
+
+    #[test]
+    fn test_load_or_generate_keypair_with_env() {
+        // Create a temporary file that will be automatically deleted when the test ends
+        let temp_file = NamedTempFile::new().expect("Failed to create temp file");
+        let temp_path = temp_file.path().to_path_buf();
+        let temp_path_str = temp_path
+            .to_str()
+            .expect("Failed to get temp path as string");
+
+        println!("Temporary file created at: {}", temp_path.display());
+
+        // Ensure we start with a clean state
+        env::remove_var("SERVER_KEY_PATH");
+        env::set_var("SERVER_KEY_PATH", temp_path_str);
+
+        // Generate a keypair and save it
+        let keypair1 = Keypair::generate().expect("Failed to generate keypair");
+        let pem1 = keypair1
+            .to_pkcs8_pem()
+            .expect("Failed to serialize keypair to PEM");
+        fs::write(&temp_path, &pem1).expect("Failed to write keypair to file");
+
+        // Test the load_or_generate_keypair function
+        let keypair2 =
+            load_or_generate_keypair().expect("Failed to load or generate second keypair");
+        let pem2 = keypair2
+            .to_pkcs8_pem()
+            .expect("Failed to serialize second keypair to PEM");
+
+        // Compare the PEMs after trimming
+        assert_eq!(
+            pem1.trim(),
+            pem2.trim(),
path: src/utils/state.rs
user: login: Hermann-Core
body: By leveraging existing components you will not be forced to trim the strings.
created_at: 2025-05-05T16:15:07Z
author_association: COLLABORATOR
diff_hunk: @@ -25,15 +58,69 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    let secret_name =
+        std::env::var("SERVER_KEY_SECRET_NAME").expect("SERVER_KEY_SECRET_NAME env not set");
+    let region = std::env::var("AWS_REGION").expect("AWS_REGION env not set");
+    let aws_config = aws_config::from_env()
+        .region(Region::new(region))
+        .load()
+        .await;
+    let client = SecretsManagerClient::new(&aws_config);
+
+    // Ensure secret exists
+    match client
+        .describe_secret()
+        .secret_id(secret_name.clone())
+        .send()
+        .await
+    {
+        Ok(_) => {
+            tracing::info!("Server key secret already exists in AWS Secrets Manager");
+        }
+        Err(e) => {
+            let error_message = e.to_string();
+            let service_error = e.into_service_error();
+            if service_error.code() == Some("ResourceNotFoundException") {
path: src/utils/state.rs
user: login: Christiantyemele
body: how sure are you of the variants/error codes in `service_error.code()` cause i also see a `service_error.message()` can you provide some ressource to defend this comparison ?
created_at: 2025-05-09T10:39:24Z
author_association: COLLABORATOR
diff_hunk: @@ -25,15 +58,69 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    let secret_name =
+        std::env::var("SERVER_KEY_SECRET_NAME").expect("SERVER_KEY_SECRET_NAME env not set");
+    let region = std::env::var("AWS_REGION").expect("AWS_REGION env not set");
+    let aws_config = aws_config::from_env()
+        .region(Region::new(region))
+        .load()
+        .await;
+    let client = SecretsManagerClient::new(&aws_config);
+
+    // Ensure secret exists
+    match client
+        .describe_secret()
+        .secret_id(secret_name.clone())
+        .send()
+        .await
+    {
+        Ok(_) => {
+            tracing::info!("Server key secret already exists in AWS Secrets Manager");
+        }
+        Err(e) => {
+            let error_message = e.to_string();
+            let service_error = e.into_service_error();
+            if service_error.code() == Some("ResourceNotFoundException") {
+                tracing::info!(
+                    "No server key secret found in AWS, generating and storing new keypair"
+                );
+                let keypair = Keypair::generate()
+                    .map_err(|_| Error::KeyGenFailed)
+                    .expect("Failed to generate keypair");
+                let pem = keypair
+                    .to_pkcs8_pem()
+                    .map_err(|_| Error::PemGenFailed)
+                    .expect("Failed to serialize keypair");
+                client
+                    .create_secret()
+                    .name(secret_name.clone())
+                    .secret_string(pem)
+                    .send()
+                    .await
+                    .map_err(|e| Error::Generic(e.to_string()))
+                    .expect("Failed to create secret");
path: src/utils/state.rs
user: login: Christiantyemele
body: leverage the existing function in `secretmanager` for this creating and storing operation
created_at: 2025-05-09T10:41:24Z
author_association: COLLABORATOR
diff_hunk: @@ -25,15 +58,69 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    let secret_name =
+        std::env::var("SERVER_KEY_SECRET_NAME").expect("SERVER_KEY_SECRET_NAME env not set");
+    let region = std::env::var("AWS_REGION").expect("AWS_REGION env not set");
+    let aws_config = aws_config::from_env()
+        .region(Region::new(region))
+        .load()
+        .await;
+    let client = SecretsManagerClient::new(&aws_config);
+
+    // Ensure secret exists
+    match client
+        .describe_secret()
+        .secret_id(secret_name.clone())
+        .send()
+        .await
+    {
+        Ok(_) => {
+            tracing::info!("Server key secret already exists in AWS Secrets Manager");
+        }
+        Err(e) => {
+            let error_message = e.to_string();
+            let service_error = e.into_service_error();
+            if service_error.code() == Some("ResourceNotFoundException") {
+                tracing::info!(
+                    "No server key secret found in AWS, generating and storing new keypair"
+                );
+                let keypair = Keypair::generate()
+                    .map_err(|_| Error::KeyGenFailed)
+                    .expect("Failed to generate keypair");
+                let pem = keypair
+                    .to_pkcs8_pem()
+                    .map_err(|_| Error::PemGenFailed)
+                    .expect("Failed to serialize keypair");
+                client
+                    .create_secret()
+                    .name(secret_name.clone())
+                    .secret_string(pem)
+                    .send()
+                    .await
+                    .map_err(|e| Error::Generic(e.to_string()))
+                    .expect("Failed to create secret");
path: src/utils/state.rs
user: login: Christiantyemele
body: can you create a test environment then called the `setup` function and assert it's output 
created_at: 2025-05-09T10:43:34Z
author_association: COLLABORATOR
diff_hunk: @@ -408,8 +421,16 @@ mod tests {
     use serde_json::json;
     use std::sync::Arc;
 
-    fn server_key() -> Keypair {
-        Keypair::from_pkcs8_pem(include_str!("../../../test_resources/ec-private.pem")).unwrap()
+    fn test_app_state(db_conn: Arc<sea_orm::DatabaseConnection>) -> AppState {
+        use crate::utils::state::MockSecretCache;
+
+        let pem = include_str!("../../../test_resources/ec-private.pem").to_string();
+        AppState {
+            credential_repository: Arc::new(SeaOrmStore::new(db_conn.clone())),
+            status_list_token_repository: Arc::new(SeaOrmStore::new(db_conn)),
+            secret_cache: Arc::new(MockSecretCache { value: Some(pem) }),
+            server_secret_name: "test-server-key".to_string(),
+        }
path: src/web/handlers/status_list/handler.rs
user: login: Christiantyemele
body: this function is repeating can you add it to a test_utils  folder 
created_at: 2025-05-09T10:53:29Z
author_association: COLLABORATOR
diff_hunk: @@ -25,15 +58,69 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    let secret_name =
+        std::env::var("SERVER_KEY_SECRET_NAME").expect("SERVER_KEY_SECRET_NAME env not set");
+    let region = std::env::var("AWS_REGION").expect("AWS_REGION env not set");
+    let aws_config = aws_config::from_env()
+        .region(Region::new(region))
+        .load()
+        .await;
+    let client = SecretsManagerClient::new(&aws_config);
+
+    // Ensure secret exists
+    match client
+        .describe_secret()
+        .secret_id(secret_name.clone())
+        .send()
+        .await
+    {
+        Ok(_) => {
+            tracing::info!("Server key secret already exists in AWS Secrets Manager");
+        }
+        Err(e) => {
+            let error_message = e.to_string();
+            let service_error = e.into_service_error();
+            if service_error.code() == Some("ResourceNotFoundException") {
path: src/utils/state.rs
user: login: Ngha-Boris
body: The use of `service_error.code() == Some("ResourceNotFoundException")`  is based on the AWS SDK for Rust’s error handling mechanism in `aws-sdk-secretsmanager v0.26.0`. The code method, provided by the `ProvideErrorMetadata `trait, returns the AWS-specific error code for a service error, such as DescribeSecretError. In this case, `"ResourceNotFoundException"` is the standard error code returned by AWS Secrets Manager when a secret does not exist, as documented in the [AWS Secrets Manager API Reference](https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_DescribeSecret.html#API_DescribeSecret_Errors).
created_at: 2025-05-09T11:23:26Z
author_association: COLLABORATOR
diff_hunk: @@ -24,7 +23,7 @@ hyper = "1.6"
 serde = { version = "1.0", features = ["derive"] }
 sqlx = { version = "0.8", features = ["postgres", "runtime-tokio"] }
 thiserror = "2.0"
-tokio = { version = "1.16", features = ["full"] }
+tokio = { version = "1.16", features = ["full", "macros", "net", "rt-multi-thread"] }
path: Cargo.toml
user: login: Hermann-Core
body: just specify the major version here.
```suggestion
tokio = { version = "1", features = ["full", "macros", "net", "rt-multi-thread"] }
```
created_at: 2025-05-13T08:58:12Z
author_association: COLLABORATOR
diff_hunk: @@ -35,14 +34,14 @@ sea-orm = { version = "1.1.7", features = [
     "runtime-tokio-native-tls",
     "macros",
 ] }
-aws-config = "1.6"
-aws-sdk-secretsmanager = "1.68"
-
+aws-config = "0.55.3"
+aws-sdk-secretsmanager = "0.26.0"
+aws-secretsmanager-cache = "0.5.0"
path: Cargo.toml
user: login: Hermann-Core
body: You are using outdated versions of these crates. Is there a reason for this? If not, it is advisable to go to the latest available versions.
Additionally, make a habit to specify versions that are subject to breaking changes ( as major sometimes minor versions).
Use aws-secretsmanager-caching instead of aws-secretsmanager-cache which is not longer maintained.
```suggestion
aws-config = "1.6"
aws-sdk-secretsmanager = "1.71"
aws-secretsmanager-caching = "1.2"
```
created_at: 2025-05-13T09:19:48Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,6 @@
 pub mod auth;
 mod database;
 pub mod model;
+pub mod test_utils;
path: src/lib.rs
user: login: Hermann-Core
body: Why are you exposing the `test_utils` module?
created_at: 2025-05-13T09:21:16Z
author_association: COLLABORATOR
diff_hunk: @@ -35,14 +34,14 @@ sea-orm = { version = "1.1.7", features = [
     "runtime-tokio-native-tls",
     "macros",
 ] }
-aws-config = "1.6"
-aws-sdk-secretsmanager = "1.68"
-
+aws-config = "0.55.3"
+aws-sdk-secretsmanager = "0.26.0"
+aws-secretsmanager-cache = "0.5.0"
path: Cargo.toml
user: login: Ngha-Boris
body: We can not use these versions because they are not compactatble with the aws-secretsmanager-cache.
created_at: 2025-05-13T09:39:51Z
author_association: COLLABORATOR
diff_hunk: @@ -24,7 +23,7 @@ hyper = "1.6"
 serde = { version = "1.0", features = ["derive"] }
 sqlx = { version = "0.8", features = ["postgres", "runtime-tokio"] }
 thiserror = "2.0"
-tokio = { version = "1.16", features = ["full"] }
+tokio = { version = "1.16", features = ["full", "macros", "net", "rt-multi-thread"] }
path: Cargo.toml
user: login: Ngha-Boris
body: Thank you for your review.
created_at: 2025-05-13T09:40:22Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,6 @@
 pub mod auth;
 mod database;
 pub mod model;
+pub mod test_utils;
path: src/lib.rs
user: login: Ngha-Boris
body: This is for the test function that i created for the Appstate.
created_at: 2025-05-13T09:41:02Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,6 @@
 pub mod auth;
 mod database;
 pub mod model;
+pub mod test_utils;
path: src/lib.rs
user: login: Hermann-Core
body: Yes but does it need to be publicy visible? I don't think
created_at: 2025-05-14T07:46:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,15 @@
+use crate::{
+    database::queries::SeaOrmStore,
+    utils::state::{AppState, MockSecretCache},
+};
+use std::sync::Arc;
+
+pub fn test_app_state(db_conn: Arc<sea_orm::DatabaseConnection>) -> AppState {
path: src/test_utils/mod.rs
user: login: Hermann-Core
body: Since this is only used for tests, consider annotating it with `#[cfg(test)]`.
created_at: 2025-05-14T07:49:13Z
author_association: COLLABORATOR
diff_hunk: @@ -130,10 +131,15 @@ mod tests {
         }
     }
 
-    // Helper to generate a test server key
-    // Note: It does nothing, it's just use to build the AppState
-    fn server_key() -> Keypair {
-        Keypair::generate().unwrap()
+    fn test_app_state(db_conn: Arc<sea_orm::DatabaseConnection>) -> AppState {
path: src/web/handlers/status_list/publish_token_status.rs
user: login: Hermann-Core
body: Seems like this function is duplicated. Could you consider reusing the existing function?
created_at: 2025-05-14T08:10:18Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,65 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::Error,
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
 
 use super::keygen::Keypair;
 
+// Define the SecretCacheTrait
+#[async_trait]
+pub trait SecretCacheTrait: Send + Sync {
path: src/utils/state.rs
user: login: Hermann-Core
body: Please don't append the `Trait` suffix to the end of a trait name, as it is unnecessary and unconventional.
created_at: 2025-05-14T08:18:19Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,65 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::Error,
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
 
 use super::keygen::Keypair;
 
+// Define the SecretCacheTrait
+#[async_trait]
+pub trait SecretCacheTrait: Send + Sync {
+    async fn get_secret_string(&self, secret_id: String) -> Result<Option<String>, String>;
path: src/utils/state.rs
user: login: Hermann-Core
body: Intead of string could you consider using an explicit error ?
created_at: 2025-05-14T08:20:03Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,65 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::Error,
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
 
 use super::keygen::Keypair;
 
+// Define the SecretCacheTrait
+#[async_trait]
+pub trait SecretCacheTrait: Send + Sync {
+    async fn get_secret_string(&self, secret_id: String) -> Result<Option<String>, String>;
+}
+
+// Define a wrapper struct for SecretsManagerCachingClient
+pub struct AwsSecretCache {
+    inner: SecretsManagerCachingClient,
+}
+
+// Implement SecretCacheTrait for AwsSecretCache
+#[async_trait]
+impl SecretCacheTrait for AwsSecretCache {
+    async fn get_secret_string(&self, secret_id: String) -> Result<Option<String>, String> {
+        match self
+            .inner
+            .get_secret_value(&secret_id, None, None, false)
+            .await
+        {
+            Ok(output) => Ok(output.secret_string),
+            Err(e) => Err(e.to_string()),
+        }
+    }
+}
+
+// Mock implementation for testing
+pub struct MockSecretCache {
path: src/utils/state.rs
user: login: Hermann-Core
body: Since this will only be used for tests, could you consider limiting its scope to tests only and moving it to the tests_utils module as well?
created_at: 2025-05-14T08:22:39Z
author_association: COLLABORATOR
diff_hunk: @@ -25,15 +72,72 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    let secret_name =
+        std::env::var("SERVER_KEY_SECRET_NAME").expect("SERVER_KEY_SECRET_NAME env not set");
+    let region = std::env::var("AWS_REGION").expect("AWS_REGION env not set");
+    let aws_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
+        .region(Region::new(region))
+        .load()
+        .await;
+    let client = SecretsManagerClient::new(&aws_config);
+
+    // Ensure secret exists
+    match client
+        .describe_secret()
+        .secret_id(secret_name.clone())
+        .send()
+        .await
+    {
+        Ok(_) => {
+            tracing::info!("Server key secret already exists in AWS Secrets Manager");
+        }
+        Err(e) => {
+            let error_message = e.to_string();
+            let service_error = e.into_service_error();
+            if service_error.code() == Some("ResourceNotFoundException") {
+                tracing::info!(
+                    "No server key secret found in AWS, generating and storing new keypair"
+                );
+                let keypair = Keypair::generate()
+                    .map_err(|_| Error::KeyGenFailed)
+                    .expect("Failed to generate keypair");
+                let pem = keypair
+                    .to_pkcs8_pem()
+                    .map_err(|_| Error::PemGenFailed)
+                    .expect("Failed to serialize keypair");
+                client
+                    .create_secret()
+                    .name(secret_name.clone())
+                    .secret_string(pem)
+                    .send()
+                    .await
+                    .map_err(|e| Error::Generic(e.to_string()))
+                    .expect("Failed to create secret");
+            } else {
+                tracing::error!("Error describing secret: {:?}", error_message);
+                panic!(
+                    "Failed to access AWS Secrets Manager: {}",
+                    Error::Generic(error_message)
+                );
+            }
+        }
+    }
+
+    // Create secret cache with max size and TTL
+    let max_size = NonZeroUsize::new(1024).unwrap();
+    let ttl = Duration::from_secs(3600); // 1 hour
+    let caching_client = SecretsManagerCachingClient::default(max_size, ttl)
+        .await
+        .expect("Failed to create cache");
+    let cache = AwsSecretCache {
+        inner: caching_client,
+    };
path: src/utils/state.rs
user: login: Hermann-Core
body: Consider reusing the existing configuration for the `SecretsManagerCachingClient` too to have a consistent configuration. See [`SecretsManagerCachingClient::from_builder`](https://docs.rs/aws_secretsmanager_caching/1.2.1/aws_secretsmanager_caching/struct.SecretsManagerCachingClient.html#method.from_builder).
created_at: 2025-05-14T08:55:32Z
author_association: COLLABORATOR
diff_hunk: @@ -25,15 +72,72 @@ pub async fn setup() -> AppState {
         .await
         .expect("Failed to apply migrations");
 
-    // TODO : Not secure. We should find a way to store this key in a secure way
-    // TODO : For example, using a vault or pkcs#8 encrypted pem
-    let server_key = Keypair::from_pkcs8_pem(include_str!("../test_resources/ec-private.pem"))
-        .expect("Failed to load server key");
+    let secret_name =
+        std::env::var("SERVER_KEY_SECRET_NAME").expect("SERVER_KEY_SECRET_NAME env not set");
+    let region = std::env::var("AWS_REGION").expect("AWS_REGION env not set");
+    let aws_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
+        .region(Region::new(region))
+        .load()
+        .await;
+    let client = SecretsManagerClient::new(&aws_config);
+
+    // Ensure secret exists
+    match client
+        .describe_secret()
+        .secret_id(secret_name.clone())
+        .send()
+        .await
+    {
+        Ok(_) => {
+            tracing::info!("Server key secret already exists in AWS Secrets Manager");
+        }
+        Err(e) => {
+            let error_message = e.to_string();
+            let service_error = e.into_service_error();
+            if service_error.code() == Some("ResourceNotFoundException") {
+                tracing::info!(
+                    "No server key secret found in AWS, generating and storing new keypair"
+                );
+                let keypair = Keypair::generate()
+                    .map_err(|_| Error::KeyGenFailed)
+                    .expect("Failed to generate keypair");
+                let pem = keypair
+                    .to_pkcs8_pem()
+                    .map_err(|_| Error::PemGenFailed)
+                    .expect("Failed to serialize keypair");
+                client
+                    .create_secret()
+                    .name(secret_name.clone())
+                    .secret_string(pem)
+                    .send()
+                    .await
+                    .map_err(|e| Error::Generic(e.to_string()))
+                    .expect("Failed to create secret");
+            } else {
+                tracing::error!("Error describing secret: {:?}", error_message);
+                panic!(
+                    "Failed to access AWS Secrets Manager: {}",
+                    Error::Generic(error_message)
+                );
+            }
+        }
+    }
+
+    // Create secret cache with max size and TTL
+    let max_size = NonZeroUsize::new(1024).unwrap();
+    let ttl = Duration::from_secs(3600); // 1 hour
+    let caching_client = SecretsManagerCachingClient::default(max_size, ttl)
+        .await
+        .expect("Failed to create cache");
+    let cache = AwsSecretCache {
+        inner: caching_client,
+    };
 
     let db = Arc::new(db);
     AppState {
         credential_repository: Arc::new(SeaOrmStore::new(Arc::clone(&db))),
         status_list_token_repository: Arc::new(SeaOrmStore::new(Arc::clone(&db))),
-        server_key: Arc::new(server_key),
+        secret_cache: Arc::new(cache),
+        server_secret_name: secret_name,
path: src/utils/state.rs
user: login: Hermann-Core
body: Given how `AppState` currently `secret_cache` and `server_secret_name` separately, you could encapsulate this secret-related logic into a dedicated, opaque type something like `SecretManager` and handle the secret machinery internally instead of decoupling it like this.
created_at: 2025-05-14T09:02:21Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,139 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::{Error, SecretCacheError},
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
 
 use super::keygen::Keypair;
 
+// Define the SecretCache
+#[async_trait]
+pub trait SecretCache: Send + Sync {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError>;
+}
+
+// Define a wrapper struct for SecretsManagerCachingClient
+pub struct AwsSecretCache {
+    inner: SecretsManagerCachingClient,
+}
+
+// Implement SecretCache for AwsSecretCache
+#[async_trait]
+impl SecretCache for AwsSecretCache {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError> {
+        match self
+            .inner
+            .get_secret_value(&secret_id, None, None, false)
+            .await
+        {
+            Ok(output) => Ok(output.secret_string),
+            Err(e) => Err(SecretCacheError::AwsSdkError(e.to_string())),
+        }
+    }
+}
+
+/// A type that manages server secrets and their caching
+#[derive(Clone)]
+pub struct SecretManager {
+    cache: Arc<dyn SecretCache>,
+    server_secret_name: String,
+}
+
+impl SecretManager {
+    pub fn new(cache: Arc<dyn SecretCache>, server_secret_name: String) -> Self {
+        Self {
+            cache,
+            server_secret_name,
+        }
+    }
+
+    pub async fn get_server_secret(&self) -> Result<Option<String>, SecretCacheError> {
+        self.cache
+            .get_secret_string(self.server_secret_name.clone())
+            .await
+    }
+
+    /// Creates a new SecretManager instance and ensures the server secret exists
+    pub async fn setup(secret_name: String, region: String) -> Result<Self, Error> {
+        let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest())
path: src/utils/state.rs
user: login: Hermann-Core
body: This can be reduced:
```suggestion
        let config = aws_config::load_from_env()
```
created_at: 2025-05-19T14:21:06Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,139 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::{Error, SecretCacheError},
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
path: src/utils/state.rs
user: login: Hermann-Core
body: Could you group imports related to the same crate?
created_at: 2025-05-19T14:21:37Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,139 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::{Error, SecretCacheError},
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
path: src/utils/state.rs
user: login: Ngha-Boris
body: Okay
created_at: 2025-05-21T07:15:08Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,139 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::{Error, SecretCacheError},
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
 
 use super::keygen::Keypair;
 
+// Define the SecretCache
+#[async_trait]
+pub trait SecretCache: Send + Sync {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError>;
+}
+
+// Define a wrapper struct for SecretsManagerCachingClient
+pub struct AwsSecretCache {
+    inner: SecretsManagerCachingClient,
+}
+
+// Implement SecretCache for AwsSecretCache
+#[async_trait]
+impl SecretCache for AwsSecretCache {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError> {
+        match self
+            .inner
+            .get_secret_value(&secret_id, None, None, false)
+            .await
+        {
+            Ok(output) => Ok(output.secret_string),
+            Err(e) => Err(SecretCacheError::AwsSdkError(e.to_string())),
+        }
+    }
+}
+
+/// A type that manages server secrets and their caching
+#[derive(Clone)]
+pub struct SecretManager {
+    cache: Arc<dyn SecretCache>,
+    server_secret_name: String,
+}
+
+impl SecretManager {
+    pub fn new(cache: Arc<dyn SecretCache>, server_secret_name: String) -> Self {
+        Self {
+            cache,
+            server_secret_name,
+        }
+    }
+
+    pub async fn get_server_secret(&self) -> Result<Option<String>, SecretCacheError> {
+        self.cache
+            .get_secret_string(self.server_secret_name.clone())
+            .await
+    }
+
+    /// Creates a new SecretManager instance and ensures the server secret exists
+    pub async fn setup(secret_name: String, region: String) -> Result<Self, Error> {
+        let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest())
path: src/utils/state.rs
user: login: Ngha-Boris
body: Hello @Hermann-Core thank you for your review, I tried this `let config = aws_config::load_from_env()` but in the library they suggested that we use ` let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest())`
created_at: 2025-05-21T07:24:27Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,139 @@
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::{Error, SecretCacheError},
 };
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::config::Region;
+use aws_sdk_secretsmanager::error::ProvideErrorMetadata;
+use aws_sdk_secretsmanager::Client as SecretsManagerClient;
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
+use std::num::NonZeroUsize;
 use std::sync::Arc;
+use std::time::Duration;
 
 use super::keygen::Keypair;
 
+// Define the SecretCache
+#[async_trait]
+pub trait SecretCache: Send + Sync {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError>;
+}
+
+// Define a wrapper struct for SecretsManagerCachingClient
+pub struct AwsSecretCache {
+    inner: SecretsManagerCachingClient,
+}
+
+// Implement SecretCache for AwsSecretCache
+#[async_trait]
+impl SecretCache for AwsSecretCache {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError> {
+        match self
+            .inner
+            .get_secret_value(&secret_id, None, None, false)
+            .await
+        {
+            Ok(output) => Ok(output.secret_string),
+            Err(e) => Err(SecretCacheError::AwsSdkError(e.to_string())),
+        }
+    }
+}
+
+/// A type that manages server secrets and their caching
+#[derive(Clone)]
+pub struct SecretManager {
+    cache: Arc<dyn SecretCache>,
+    server_secret_name: String,
+}
+
+impl SecretManager {
+    pub fn new(cache: Arc<dyn SecretCache>, server_secret_name: String) -> Self {
+        Self {
+            cache,
+            server_secret_name,
+        }
+    }
+
+    pub async fn get_server_secret(&self) -> Result<Option<String>, SecretCacheError> {
+        self.cache
+            .get_secret_string(self.server_secret_name.clone())
+            .await
+    }
+
+    /// Creates a new SecretManager instance and ensures the server secret exists
+    pub async fn setup(secret_name: String, region: String) -> Result<Self, Error> {
+        let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest())
path: src/utils/state.rs
user: login: Hermann-Core
body: From the documentation of the function, it is evident that it exhibits exactly the same behavior, but in a more convenient manner.
> Load default configuration with the latest defaults.
>
> Convenience wrapper equivalent to `aws_config::load_defaults(BehaviorVersion::latest()).await`
>
> For more information about default configuration, refer to the AWS SDKs and Tools [shared configuration documentation](https://docs.aws.amazon.com/sdkref/latest/guide/creds-config-files.html).
created_at: 2025-05-21T07:52:38Z
author_association: COLLABORATOR
diff_hunk: @@ -1,18 +1,138 @@
+use super::keygen::Keypair;
 use crate::{
     database::queries::SeaOrmStore,
     model::{Credentials, StatusListToken},
+    utils::errors::{Error, SecretCacheError},
+};
+use async_trait::async_trait;
+use aws_sdk_secretsmanager::{
+    config::Region, error::ProvideErrorMetadata, Client as SecretsManagerClient,
 };
+use aws_secretsmanager_caching::SecretsManagerCachingClient;
 use sea_orm::{Database, DatabaseConnection};
 use sea_orm_migration::MigratorTrait;
-use std::sync::Arc;
+use std::{num::NonZeroUsize, sync::Arc, time::Duration};
+use tracing::info;
 
-use super::keygen::Keypair;
+// Define the SecretCache
+#[async_trait]
+pub trait SecretCache: Send + Sync {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError>;
+}
+
+// Define a wrapper struct for SecretsManagerCachingClient
+pub struct AwsSecretCache {
+    inner: SecretsManagerCachingClient,
+}
+
+// Implement SecretCache for AwsSecretCache
+#[async_trait]
+impl SecretCache for AwsSecretCache {
+    async fn get_secret_string(
+        &self,
+        secret_id: String,
+    ) -> Result<Option<String>, SecretCacheError> {
+        match self
+            .inner
+            .get_secret_value(&secret_id, None, None, false)
+            .await
+        {
+            Ok(output) => Ok(output.secret_string),
+            Err(e) => Err(SecretCacheError::AwsSdkError(e.to_string())),
+        }
+    }
+}
+
+/// A type that manages server secrets and their caching
+#[derive(Clone)]
+pub struct SecretManager {
+    cache: Arc<dyn SecretCache>,
+    server_secret_name: String,
+}
+
+impl SecretManager {
path: src/utils/state.rs
user: login: Hermann-Core
body: Could you consider exposing methods to create and delete secrets in the secret manager, with an optional way to enable or disable caching during creation and retrieval? This would provide more flexibility for us to choose whether to leverage the cache. Currently, the secret manager only creates the key internally and exposes a method to retrieve secrets.
created_at: 2025-05-21T08:10:07Z
author_association: COLLABORATOR

PR comments:
user: login: Hermann-Core
created_at: 2025-05-16T17:07:02Z
body: > Hi @Hermann-Core, This functionality has been tested by @Christiantyemele and me on AWS, and everything is working well. Given that, I don't currently see a strong need to implement tests using LocalStack. However, if there are specific concerns or scenarios you think we might be missing, I'm happy to discuss them further.

It you have tested it directly on AWS and it’s working then you don’t need to test again with LocalStack. I just wanted you to validate your implementation. Good job 👍🏽
author_association: COLLABORATOR