diff --git a/Cargo.toml b/Cargo.toml index c22074eb82ee1a3cc33ce6f5ff520b2e6b6067a4..c9e816aa4162d51d09d1816eba229f67ad8eb416 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,9 @@ sha1 = "0.10.5" sha2 = "0.10.6" bincode = "2.0.0-rc.3" secstr = "0.5.1" -openssl = "0.10.45" +openssl-sys = "0.9.90" +openssl= "0.10.55" +foreign-types-shared = "0.1.1" actix-identity = "0.5.2" actix-session = { version = "0.7.2", features = ["redis-rs-session"]} actix-limitation = "0.4.0" diff --git a/app/src/pages/listShow/ListShow.vue b/app/src/pages/listShow/ListShow.vue index f7cfbc5d0dbf3b45d94a28b97369ec2f628fe29e..cdd1f1053ab05a8a84578f3b49e34a41bde58f92 100644 --- a/app/src/pages/listShow/ListShow.vue +++ b/app/src/pages/listShow/ListShow.vue @@ -11,7 +11,6 @@ import PublicTable from "./PublicTable.vue";
-
diff --git a/config/client.toml b/config/client.toml index d017e483eb24189129b5373ffbc2286dc33576c0..55e84ec768ed8a191409f8115f82aab433b1638c 100644 --- a/config/client.toml +++ b/config/client.toml @@ -3,7 +3,6 @@ worker_threads = 8 buffer_size = 20480 # consider the memory consumption if number bumped since all binaries will be stored in memory max_concurrency = 100 -token = "" [server] domain_name = "signatrust.test.osinfra.cn" tls_cert = "/Users/tommylike/Work/codes/rust-projects/signatrust/.data/certs/client/server.crt" diff --git a/config/server.toml b/config/server.toml index e1123eeb78f34f4f4c3710df5cc6927ff1418fcf..b56f092a290bccbf2cdf5936702041670e250db9 100644 --- a/config/server.toml +++ b/config/server.toml @@ -10,6 +10,8 @@ server_ip = "0.0.0.0" server_port = "8080" cookie_key = "2B5AEC57F7CC4FF8B4120AA7E4527C7B597CAF43183E453A9B981991E6FACB76" redis_connection = "redis://:signatrust-redis@127.0.0.1:6379" +domain_name = "signatrust.test.osinfra.cn" +crl_refresh_interval_days = 7 limits_per_minute = 100 [oidc] client_id = "" diff --git a/migrations/20230614032140_support-ca-and-crl.down.sql b/migrations/20230614032140_support-ca-and-crl.down.sql new file mode 100644 index 0000000000000000000000000000000000000000..45ab6eb93a923f8c68254af7e32cae34eb1dc34d --- /dev/null +++ b/migrations/20230614032140_support-ca-and-crl.down.sql @@ -0,0 +1,18 @@ +-- Add down migration script here +DROP TABLE IF EXISTS x509_keys_revoked; +DROP TABLE IF EXISTS x509_crl_content; +ALTER TABLE data_key DROP parent_id; +ALTER TABLE data_key DROP serial_number; +ALTER TABLE data_key MODIFY COLUMN key_state varchar(10); + +DROP TABLE IF EXISTS pending_operation; +CREATE TABLE request_delete ( + id INT AUTO_INCREMENT, + user_id INT NOT NULL, + key_id INT NOT NULL, + create_at DATETIME, + PRIMARY KEY(id), + FOREIGN KEY (user_id) REFERENCES user(id), + FOREIGN KEY (key_id) REFERENCES data_key(id), + UNIQUE KEY `unique_user_and_key` (`user_id`,`key_id`) +); diff --git a/migrations/20230614032140_support-ca-and-crl.up.sql b/migrations/20230614032140_support-ca-and-crl.up.sql new file mode 100644 index 0000000000000000000000000000000000000000..e3d5b10cff2ef324b4266294b8fcca69e20f2c8d --- /dev/null +++ b/migrations/20230614032140_support-ca-and-crl.up.sql @@ -0,0 +1,42 @@ +-- Add up migration script here +# Support for parent_id and serial_number +ALTER TABLE data_key ADD parent_id INT AFTER `key_type`; +ALTER TABLE data_key ADD serial_number VARCHAR(90) AFTER `fingerprint`; +ALTER TABLE data_key MODIFY COLUMN key_state varchar(20); + +# Break changes: request delete will be dropped&recreated to support different pending operation +DROP TABLE IF EXISTS request_delete; +CREATE TABLE pending_operation ( + id INT AUTO_INCREMENT, + user_id INT NOT NULL, + key_id INT NOT NULL, + request_type VARCHAR(30) NOT NULL, + user_email varchar(60) NOT NULL, + create_at DATETIME, + PRIMARY KEY(id), + FOREIGN KEY (user_id) REFERENCES user(id), + FOREIGN KEY (key_id) REFERENCES data_key(id), + UNIQUE KEY `unique_user_and_key_and_type` (`user_id`,`key_id`, `request_type`) +); + +# Add new table for crl content and revoked certificates +CREATE TABLE x509_crl_content ( + id INT AUTO_INCREMENT, + ca_id INT NOT NULL UNIQUE, + create_at DATETIME, + update_at DATETIME, + data TEXT NOT NULL, + PRIMARY KEY(id), + FOREIGN KEY (ca_id) REFERENCES data_key(id) +); +CREATE TABLE x509_keys_revoked ( + id INT AUTO_INCREMENT, + ca_id INT NOT NULL, + key_id INT NOT NULL, + create_at DATETIME, + reason VARCHAR(30), + FOREIGN KEY (ca_id) REFERENCES data_key(id), + FOREIGN KEY (key_id) REFERENCES data_key(id), + UNIQUE KEY `unique_ca_and_key` (`ca_id`,`key_id`), + PRIMARY KEY(id) +); \ No newline at end of file diff --git a/proto/signatrust.proto b/proto/signatrust.proto index 723df8745999ce9337bf6435c718bb8a5590c029..3e848640ac27eacbce005e2b9edb22b74e9b20d9 100644 --- a/proto/signatrust.proto +++ b/proto/signatrust.proto @@ -13,7 +13,6 @@ message SignStreamRequest { string key_type = 2; string key_id = 3; map options = 4; - string token = 5; } message SignStreamResponse { diff --git a/scripts/initialize-user-and-keys.sh b/scripts/initialize-user-and-keys.sh index 317592bce83bea2ec58fdd451d4613ab55608f77..ee44bc9c387a183e590e1244173b9be3b4fefc85 100755 --- a/scripts/initialize-user-and-keys.sh +++ b/scripts/initialize-user-and-keys.sh @@ -20,19 +20,30 @@ function create_default_admin { RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml create-admin --email tommylikehu@gmail.com } -function create_default_openpgp_keys { - echo "start to create default openpgp keys identified with default-pgp" - RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-pgp --description "used for test purpose only" --key-type pgp --email tommylikehu@gmail.com --param-key-type rsa --param-key-size 2048 --param-pgp-email infra@openeuler.org --param-pgp-passphrase husheng1234 --digest-algorithm sha2_256 +function create_default_x509_ca { + echo "start to create default x509 CA identified with default-x509" + RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-x509ca --description "used for test purpose only" --key-type x509ca --email tommylikehu@gmail.com --param-key-type rsa --param-key-size 2048 \ + --param-x509-common-name Infra --param-x509-organization Huawei --param-x509-locality ShenZhen --param-x509-province-name GuangDong --param-x509-country-name CN --param-x509-organizational-unit Infra --digest-algorithm sha2_256 } +function create_default_x509_ica { + echo "start to create default x509 ICA identified with default-x509" + RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-x509ica --description "used for test purpose only" --key-type x509ica --email tommylikehu@gmail.com --param-key-type rsa --param-key-size 2048 \ + --param-x509-common-name Infra --param-x509-organization Huawei --param-x509-locality ShenZhen --param-x509-province-name GuangDong --param-x509-country-name CN --param-x509-organizational-unit Infra --digest-algorithm sha2_256 --param-x509-parent-name default-x509ca +} +function create_default_x509_ee { + echo "start to create default x509 EE certificate identified with default-x509" + RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-x509ee --description "used for test purpose only" --key-type x509ee --email tommylikehu@gmail.com --param-key-type rsa --param-key-size 2048 \ + --param-x509-common-name Infra --param-x509-organization Huawei --param-x509-locality ShenZhen --param-x509-province-name GuangDong --param-x509-country-name CN --param-x509-organizational-unit Infra --digest-algorithm sha2_256 --param-x509-parent-name default-x509ica +} -function create_default_x509_keys { - echo "start to create default x509 keys identified with default-x509" - RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-x509 --description "used for test purpose only" --key-type x509 --email tommylikehu@gmail.com --param-key-type rsa --param-key-size 2048 \ - --param-x509-common-name Infra --param-x509-organization Huawei --param-x509-locality ShenZhen --param-x509-province-name GuangDong --param-x509-country-name CN --param-x509-organizational-unit Infra --digest-algorithm sha2_256 +function create_default_openpgp_keys { + echo "start to create default openpgp keys identified with default-pgp" + RUST_LOG=info ./target/debug/control-admin --config ./config/server.toml generate-keys --name default-pgp --description "used for test purpose only" --key-type pgp --email tommylikehu@gmail.com --param-key-type rsa --param-key-size 2048 --param-pgp-email infra@openeuler.org --param-pgp-passphrase husheng1234 --digest-algorithm sha2_256 } + echo "Preparing basic keys for signatrust......" check-binary @@ -41,6 +52,11 @@ echo "===========================================" create_default_admin +create_default_x509_ca + +create_default_x509_ica + +create_default_x509_ee + create_default_openpgp_keys -create_default_x509_keys \ No newline at end of file diff --git a/src/application/datakey.rs b/src/application/datakey.rs index b460a98afb158a2fbd8aa03361672f1546e5e41d..2cb07ae4e3dab5d14888d1fed3854f6ac738004b 100644 --- a/src/application/datakey.rs +++ b/src/application/datakey.rs @@ -18,34 +18,44 @@ use crate::domain::datakey::repository::Repository as DatakeyRepository; use crate::domain::sign_service::SignBackend; use crate::util::error::{Error, Result}; use async_trait::async_trait; -use crate::domain::datakey::entity::{DataKey, KeyState, Visibility}; -use tokio::time::{Duration, self}; +use crate::domain::datakey::entity::{DataKey, KeyAction, KeyState, KeyType, X509CRL, X509RevokeReason}; +use tokio::time::{self}; use crate::util::signer_container::DataKeyContainer; use std::collections::HashMap; use std::sync::{Arc}; +use chrono::{Duration, Utc}; use tokio::sync::RwLock; use tokio_util::sync::CancellationToken; +use crate::domain::datakey::entity::KeyType::{OpenPGP, X509CA, X509EE, X509ICA}; use crate::presentation::handler::control::model::user::dto::UserIdentity; #[async_trait] pub trait KeyService: Send + Sync{ async fn create(&self, data: &mut DataKey) -> Result; async fn import(&self, data: &mut DataKey) -> Result; - async fn key_name_exists(&self, name: &String) -> Result; - async fn get_by_visibility(&self, user: Option, visibility: Visibility) -> Result>; - async fn get_all(&self) -> Result>; - async fn get_one(&self, user: Option, id: i32) -> Result; - async fn request_delete(&self, user: UserIdentity, id: i32) -> Result<()>; - async fn cancel_delete(&self, user: UserIdentity, id: i32) -> Result<()>; - async fn export_one(&self, user: Option, id: i32) -> Result; - async fn enable(&self, user: Option, id: i32) -> Result<()>; - async fn disable(&self, user: Option, id: i32) -> Result<()>; + async fn get_by_name(&self, name: &str) -> Result; + async fn get_all(&self, key_type: Option) -> Result>; + async fn get_one(&self, user: Option, id_or_name: String) -> Result; + //get keys content + async fn export_one(&self, user: Option, id_or_name: String) -> Result; + async fn export_cert_crl(&self, user: Option,id_or_name: String) -> Result; + //keys related operation + async fn request_delete(&self, user: UserIdentity, id_or_name: String) -> Result<()>; + async fn cancel_delete(&self, user: UserIdentity, id_or_name: String) -> Result<()>; + async fn request_revoke(&self, user: UserIdentity, id_or_name: String, reason: X509RevokeReason) -> Result<()>; + async fn cancel_revoke(&self, user: UserIdentity, id_or_name: String) -> Result<()>; + async fn enable(&self, user: Option, id_or_name: String) -> Result<()>; + async fn disable(&self, user: Option, id_or_name: String) -> Result<()>; + //used for data server async fn sign(&self, key_type: String, key_name: String, options: &HashMap, data: Vec) ->Result>; //method below used for maintenance fn start_cache_cleanup_loop(&self, cancel_token: CancellationToken) -> Result<()>; fn start_key_rotate_loop(&self, cancel_token: CancellationToken) -> Result<()>; + + //method below used for x509 crl + fn start_key_plugin_maintenance(&self, cancel_token: CancellationToken, refresh_days: i32) -> Result<()>; } @@ -73,15 +83,63 @@ impl DBKeyService } } - async fn get_and_check_permission(&self, user: Option, id: i32) -> Result { - let key = self.repository.get_by_id(id).await?; - if key.visibility == Visibility::Public { - return Ok(key); + async fn get_and_check_permission(&self, _user: Option, id_or_name: String, action: KeyAction) -> Result { + let id = id_or_name.parse::(); + let data_key: DataKey = match id { + Ok(id) => { + self.repository.get_by_id(id).await? + } + Err(_) => { + self.repository.get_by_name(&id_or_name).await? + } + }; + self.validate_type_and_state(&data_key, action)?; + Ok(data_key) + } + + fn validate_type_and_state(&self, key: &DataKey, key_action: KeyAction) -> Result<()> { + let valid_action_by_key_type = HashMap::from([ + (OpenPGP, vec![KeyAction::Delete, KeyAction::CancelDelete, KeyAction::Disable, KeyAction::Enable, KeyAction::Sign, KeyAction::Read]), + (X509CA, vec![KeyAction::Delete, KeyAction::CancelDelete, KeyAction::Disable, KeyAction::Enable, KeyAction::IssueCert, KeyAction::Read]), + (X509ICA, vec![KeyAction::Delete, KeyAction::CancelDelete, KeyAction::Revoke, KeyAction::CancelRevoke, KeyAction::Disable, KeyAction::Enable, KeyAction::Read, KeyAction::IssueCert]), + (X509EE, vec![KeyAction::Delete, KeyAction::CancelDelete, KeyAction::Revoke, KeyAction::CancelRevoke, KeyAction::Disable, KeyAction::Enable, KeyAction::Read, KeyAction::Sign]), + ]); + + let valid_state_by_key_action = HashMap::from([ + (KeyAction::Delete, vec![KeyState::Disabled, KeyState::Revoked, KeyState::PendingDelete]), + (KeyAction::CancelDelete, vec![KeyState::PendingDelete]), + (KeyAction::Revoke, vec![KeyState::Disabled, KeyState::PendingRevoke]), + (KeyAction::CancelRevoke, vec![KeyState::PendingRevoke]), + (KeyAction::Enable, vec![KeyState::Disabled]), + (KeyAction::Disable, vec![KeyState::Enabled]), + (KeyAction::Sign, vec![KeyState::Enabled, KeyState::PendingDelete, KeyState::PendingRevoke]), + (KeyAction::IssueCert, vec![KeyState::Enabled, KeyState::PendingDelete, KeyState::PendingRevoke]), + (KeyAction::Read, vec![KeyState::Enabled, KeyState::PendingDelete, KeyState::PendingRevoke]), + ]); + match valid_action_by_key_type.get(&key.key_type) { + None => { + return Err(Error::ConfigError("key type is missing, please check the key type".to_string())); + } + Some(actions) => { + if !actions.contains(&key_action) { + return Err(Error::ActionsNotAllowedError(format!("action '{}' is not permitted for key type '{}'", key_action, key.key_type))); + } + } } - if user.is_none() || key.user != user.unwrap().id { - return Err(Error::UnprivilegedError); + match valid_state_by_key_action.get(&key_action) { + None => { + return Err(Error::ConfigError("key action is missing, please check the key action".to_string())) + } + Some(states) => { + if !states.contains(&key.key_state) { + return Err(Error::ActionsNotAllowedError(format!("action '{}' is not permitted for state '{}'", key_action, key.key_state))) + } + } } - Ok(key) + if (key_action == KeyAction::Revoke || key_action == KeyAction::CancelRevoke) && key.parent_id.is_none() { + return Err(Error::ActionsNotAllowedError(format!("action '{}' is not permitted for key without parent", key_action))) + } + Ok(()) } } @@ -92,8 +150,18 @@ where S: SignBackend + ?Sized + 'static { async fn create(&self, data: &mut DataKey) -> Result { - self.sign_service.read().await.generate_keys(data).await?; - self.repository.create(data.clone()).await + //we need to create a key in database first, then generate sensitive data + let mut key = self.repository.create(data.clone()).await?; + match self.sign_service.read().await.generate_keys(&mut key).await { + Ok(_) => { + self.repository.update_key_data(key.clone()).await?; + Ok(key) + } + Err(e) => { + self.repository.delete(key.id).await?; + Err(e) + } + } } async fn import(&self, data: &mut DataKey) -> Result { @@ -101,81 +169,78 @@ where self.repository.create(data.clone()).await } - async fn key_name_exists(&self, name: &String) -> Result { - if self.repository.get_by_name(name).await.is_ok() { - return Ok(true); - } - Ok(false) + async fn get_by_name(&self, name: &str) -> Result { + self.repository.get_by_name(name).await } - async fn get_by_visibility(&self, user: Option, visibility: Visibility) -> Result> { - if visibility == Visibility::Private { - if user.is_none() { - return Err(Error::UnprivilegedError); - } - return self.repository.get_private_keys(user.unwrap().id).await; - } - self.repository.get_public_keys().await + async fn get_all(&self, key_type: Option) -> Result> { + self.repository.get_all_keys(key_type).await + } + + async fn get_one(&self, user: Option, id_or_name: String) -> Result { + let datakey = self.get_and_check_permission(user, id_or_name, KeyAction::Read).await?; + Ok(datakey) + } - async fn get_all(&self) -> Result> { - self.repository.get_all_keys().await + async fn export_one(&self, user: Option, id_or_name: String) -> Result { + let mut key = self.get_and_check_permission(user, id_or_name, KeyAction::Read).await?; + self.sign_service.read().await.decode_public_keys(&mut key).await?; + Ok(key) } - async fn get_one(&self, user: Option, id: i32) -> Result { - self.get_and_check_permission(user, id).await + async fn export_cert_crl(&self, user: Option, id_or_name: String) -> Result { + let key = self.get_and_check_permission(user, id_or_name, KeyAction::Read).await?; + let crl = self.repository.get_x509_crl_by_ca_id(key.id).await?; + Ok(crl) } - async fn request_delete(&self, user: UserIdentity, id: i32) -> Result<()> { + async fn request_delete(&self, user: UserIdentity, id_or_name: String) -> Result<()> { let user_id = user.id; let user_email = user.email.clone(); - let key = self.get_and_check_permission(Some(user), id).await?; - if key.key_state == KeyState::Enabled { - return Err(Error::ParameterError("enabled key does not support delete".to_string())); - } - match key.visibility { - Visibility::Public => { - self.repository.request_delete_public_key(user_id, user_email, key.id).await - } - Visibility::Private => { - self.repository.delete_private_key(key.id, user_id).await - } - } + let key = self.get_and_check_permission(Some(user), id_or_name, KeyAction::Delete).await?; + self.repository.request_delete_key(user_id, user_email, key.id).await } - async fn cancel_delete(&self, user: UserIdentity, id: i32) -> Result<()> { + async fn cancel_delete(&self, user: UserIdentity, id_or_name: String) -> Result<()> { let user_id = user.id; - let key = self.get_and_check_permission(Some(user), id).await?; - if key.visibility == Visibility::Private { - return Err(Error::ParameterError("private key does not support cancel delete".to_string())); - } - self.repository.cancel_delete_public_key(user_id, key.id).await + let key = self.get_and_check_permission(Some(user), id_or_name, KeyAction::CancelDelete).await?; + self.repository.cancel_delete_key(user_id, key.id).await } - async fn export_one(&self, user: Option, id: i32) -> Result { - let mut key = self.get_and_check_permission(user, id).await?; - self.sign_service.read().await.decode_public_keys(&mut key).await?; - Ok(key) + async fn request_revoke(&self, user: UserIdentity, id_or_name: String, reason: X509RevokeReason) -> Result<()> { + let user_id = user.id; + let user_email = user.email.clone(); + let key = self.get_and_check_permission(Some(user), id_or_name, KeyAction::Revoke).await?; + self.repository.request_revoke_key(user_id, user_email, key.id, key.parent_id.unwrap(), reason).await?; + Ok(()) } - async fn enable(&self, user: Option, id: i32) -> Result<()> { - let key = self.get_and_check_permission(user, id).await?; + async fn cancel_revoke(&self, user: UserIdentity, id_or_name: String) -> Result<()> { + let user_id = user.id; + let key = self.get_and_check_permission(Some(user), id_or_name, KeyAction::CancelRevoke).await?; + self.repository.cancel_revoke_key(user_id, key.id, key.parent_id.unwrap()).await?; + Ok(()) + } + + async fn enable(&self, user: Option, id_or_name: String) -> Result<()> { + let key = self.get_and_check_permission(user, id_or_name, KeyAction::Enable).await?; self.repository.update_state(key.id, KeyState::Enabled).await } - async fn disable(&self, user: Option, id: i32) -> Result<()> { - let key = self.get_and_check_permission(user, id).await?; + async fn disable(&self, user: Option, id_or_name: String) -> Result<()> { + let key = self.get_and_check_permission(user, id_or_name, KeyAction::Disable).await?; self.repository.update_state(key.id, KeyState::Disabled).await } async fn sign(&self, key_type: String, key_name: String, options: &HashMap, data: Vec) -> Result> { - self.sign_service.read().await.sign( - &self.container.get_data_key(key_type, key_name).await?, data, options.clone()).await + let key = self.container.get_data_key(key_type, key_name).await?; + self.sign_service.read().await.sign(&key, data, options.clone()).await } fn start_cache_cleanup_loop(&self, cancel_token: CancellationToken) -> Result<()> { let container = self.container.clone(); - let mut interval = time::interval(Duration::from_secs(120)); + let mut interval = time::interval(Duration::seconds(120).to_std()?); tokio::spawn(async move { loop { tokio::select! { @@ -196,7 +261,7 @@ where fn start_key_rotate_loop(&self, cancel_token: CancellationToken) -> Result<()> { let sign_service = self.sign_service.clone(); - let mut interval = time::interval(Duration::from_secs(60 * 60 * 2)); + let mut interval = time::interval(Duration::seconds(60 * 60 * 2).to_std()?); tokio::spawn(async move { loop { tokio::select! { @@ -223,4 +288,43 @@ where }); Ok(()) } + + fn start_key_plugin_maintenance(&self, cancel_token: CancellationToken, refresh_days: i32) -> Result<()> { + let mut interval = time::interval(Duration::hours(2).to_std()?); + let duration = Duration::days(refresh_days as i64); + let repository = self.repository.clone(); + let sign_service = self.sign_service.clone(); + tokio::spawn(async move { + loop { tokio::select! { + _ = interval.tick() => { + info!("start to update execute key plugin maintenance"); + match repository.get_keys_for_crl_update(duration).await { + Ok(keys) => { + let now = Utc::now(); + for key in keys { + match repository.get_revoked_serial_number_by_parent_id(key.id).await { + Ok(revoke_keys) => { + match sign_service.read().await.generate_crl_content(&key, revoke_keys, now, now + duration).await { + Ok(data) => { + let crl_content = X509CRL::new(key.id, data, now, now); + if let Err(e) = repository.upsert_x509_crl(crl_content).await { + error!("Failed to update CRL content for key: {} {}, {}", key.key_state, key.id, e); + } else { + info!("CRL has been successfully updated for key: {} {}", key.key_type, key.id); + }} + Err(e) => { + error!("failed to update CRL content for key: {} {} and error {}", key.key_state, key.id, e); + }}} + Err(e) => { + error!("failed to get revoked keys for key {} {}, error {}", key.key_state, key.id, e); + }}}} + Err(e) => { + error!("failed to get keys for CRL update: {}", e); + }}} + _ = cancel_token.cancelled() => { + info!("cancel token received, will quit key plugin maintenance loop"); + break; + }}}}); + Ok(()) + } } diff --git a/src/client/cmd/add.rs b/src/client/cmd/add.rs index 1bf79689f758410db8e1550b3dcb3cf6b29e59f5..1fa7f5fef04024a7fc7c7591bfb60eef4ac335d7 100644 --- a/src/client/cmd/add.rs +++ b/src/client/cmd/add.rs @@ -87,7 +87,6 @@ pub struct CommandAddHandler { detached: bool, max_concurrency: usize, sign_type: SignType, - token: String, } impl CommandAddHandler { @@ -168,7 +167,6 @@ impl SignCommand for CommandAddHandler { detached: command.detached, max_concurrency: config.read()?.get_string("max_concurrency")?.parse()?, sign_type: command.sign_type, - token: config.read()?.get_string("token").unwrap_or("".to_string()), }) } @@ -204,7 +202,7 @@ impl SignCommand for CommandAddHandler { runtime.block_on(async { let channel = ChannelFactory::new( &lb_config).await.unwrap().get_channel().unwrap(); - let mut signer = RemoteSigner::new(channel, self.buffer_size, self.token.clone()); + let mut signer = RemoteSigner::new(channel, self.buffer_size); //split file let send_handlers = files.into_iter().map(|file|{ let task_split_s = split_s.clone(); diff --git a/src/client/file_handler/checksum.rs b/src/client/file_handler/checksum.rs index 4d1333fb5cb64a97c084b473611c7fabe8a00305..8baa303d9364055c7fb9f8091927c3e5c7062b68 100644 --- a/src/client/file_handler/checksum.rs +++ b/src/client/file_handler/checksum.rs @@ -103,7 +103,7 @@ mod test { let result = handler.validate_options(&options); assert!(result.is_ok()); - options.insert(options::KEY_TYPE.to_string(), KeyType::X509.to_string()); + options.insert(options::KEY_TYPE.to_string(), KeyType::X509EE.to_string()); let result = handler.validate_options(&options); assert!(result.is_err()); assert_eq!( diff --git a/src/client/file_handler/efi.rs b/src/client/file_handler/efi.rs index 1b759073b2aec243024acb7b0c262032b3669a86..9b9aa9589486d276901a8e66bd3a129be1a619ff 100644 --- a/src/client/file_handler/efi.rs +++ b/src/client/file_handler/efi.rs @@ -29,7 +29,7 @@ impl FileHandler for EfiFileHandler { } if let Some(key_type) = sign_options.get(options::KEY_TYPE) { - if key_type != KeyType::X509.to_string().as_str() { + if key_type != KeyType::X509EE.to_string().as_str() { return Err(Error::InvalidArgumentError( "EFI image only support x509 key type".to_string(), )); diff --git a/src/client/file_handler/kernel_module.rs b/src/client/file_handler/kernel_module.rs index 0a6a701e7046032744153a35923d13eb329ee187..6e6b706e76b0f20e3723d5bf93282ea03d948db3 100644 --- a/src/client/file_handler/kernel_module.rs +++ b/src/client/file_handler/kernel_module.rs @@ -154,7 +154,7 @@ impl KernelModuleFileHandler { impl FileHandler for KernelModuleFileHandler { fn validate_options(&self, sign_options: &HashMap) -> Result<()> { if let Some(key_type) = sign_options.get(options::KEY_TYPE) { - if key_type != KeyType::X509.to_string().as_str() { + if key_type != KeyType::X509EE.to_string().as_str() { return Err(Error::InvalidArgumentError( "kernel module file only support x509 signature".to_string(), )); @@ -305,7 +305,7 @@ mod test { "invalid argument: kernel module file only support x509 signature" ); - options.insert(options::KEY_TYPE.to_string(), KeyType::X509.to_string()); + options.insert(options::KEY_TYPE.to_string(), KeyType::X509EE.to_string()); options.insert(options::SIGN_TYPE.to_string(), SignType::Authenticode.to_string()); let result = handler.validate_options(&options); assert!(result.is_err()); diff --git a/src/client/file_handler/rpm.rs b/src/client/file_handler/rpm.rs index aa6dba6db8e41e2f1adb453aa93983f12a8f88fb..ee34b519e7899ad489895418e54f72914c2412c5 100644 --- a/src/client/file_handler/rpm.rs +++ b/src/client/file_handler/rpm.rs @@ -137,7 +137,7 @@ impl FileHandler for RpmFileHandler { mod test { use super::*; use std::env; - use std::fs; + use std::io::Write; fn get_signed_rpm() -> Result { @@ -167,7 +167,7 @@ mod test { #[test] fn test_validate_options() { let mut options = HashMap::new(); - options.insert(options::KEY_TYPE.to_string(), KeyType::X509.to_string()); + options.insert(options::KEY_TYPE.to_string(), KeyType::X509EE.to_string()); let handler = RpmFileHandler::new(); let result = handler.validate_options(&options); assert!(result.is_err()); @@ -206,7 +206,7 @@ mod test { let mut sign_options = HashMap::new(); let file_handler = RpmFileHandler::new(); let path = generate_invalid_rpm().expect("generate invalid rpm failed"); - let raw_content = file_handler.split_data(&path, &mut sign_options).await.expect_err("split invalid rpm file would failed"); + let _raw_content = file_handler.split_data(&path, &mut sign_options).await.expect_err("split invalid rpm file would failed"); } #[tokio::test] @@ -215,7 +215,7 @@ mod test { let file_handler = RpmFileHandler::new(); let path = generate_signed_rpm().expect("generate signed rpm failed"); let fake_signature = vec![vec![1,2,3,4], vec![1,2,3,4]]; - let raw_content = file_handler.assemble_data(&path, fake_signature, &env::temp_dir(), &mut sign_options).await.expect("assemble data failed"); + let _raw_content = file_handler.assemble_data(&path, fake_signature, &env::temp_dir(), &mut sign_options).await.expect("assemble data failed"); } } diff --git a/src/client/worker/signer.rs b/src/client/worker/signer.rs index 8c6afafdd702f81f440bf1d2c265deb7c0d003cf..815e8fee394c8076dfb38d40966f4f713ee770a3 100644 --- a/src/client/worker/signer.rs +++ b/src/client/worker/signer.rs @@ -34,17 +34,15 @@ use std::io::{Cursor, Read}; pub struct RemoteSigner { client: SignatrustClient, buffer_size: usize, - token: String } impl RemoteSigner { - pub fn new(channel: Channel, buffer_size: usize, token: String) -> Self { + pub fn new(channel: Channel, buffer_size: usize) -> Self { Self { client: SignatrustClient::new(channel), buffer_size, - token, } } } @@ -68,7 +66,6 @@ impl SignHandler for RemoteSigner { options: item.sign_options.borrow().clone(), key_type: format!("{}", item.key_type), key_id: item.key_id.clone(), - token: self.token.clone() }); } let result = self.client.sign_stream( diff --git a/src/control_admin_entrypoint.rs b/src/control_admin_entrypoint.rs index 6e42b6d6a1fded97afe044b824547aa388f316bf..6995f2cd90646435b5fd7529f83dcfd9c3bbd549 100644 --- a/src/control_admin_entrypoint.rs +++ b/src/control_admin_entrypoint.rs @@ -21,7 +21,7 @@ use std::env; use validator::Validate; use chrono::{Duration, Utc}; use crate::util::error::{Result}; -use crate::util::sign::KeyType; +use crate::domain::datakey::entity::KeyType as EntityKeyTpe; use clap::{Parser, Subcommand}; use clap::{Args}; use tokio_util::sync::CancellationToken; @@ -29,6 +29,8 @@ use crate::domain::datakey::entity::{DataKey}; use crate::domain::user::entity::User; use crate::presentation::handler::control::model::datakey::dto::{CreateDataKeyDTO}; use crate::presentation::handler::control::model::user::dto::UserIdentity; +use std::str::FromStr; + mod util; mod infra; @@ -116,12 +118,14 @@ pub struct CommandGenerateKeys { #[arg(help = "specify the 'CountryName' used for x509 key generation. ")] param_x509_country_name: Option, #[arg(long)] + #[arg(help = "specify the name of the CA or ICA which used for cert issuing. ")] + param_x509_parent_name: Option, + #[arg(long)] #[arg(help = "specify the email of admin which this key bounds to")] email: String, #[arg(long)] - #[arg(value_enum)] #[arg(help = "specify th type of key")] - key_type: KeyType, + key_type: String, } fn generate_keys_parameters(command: &CommandGenerateKeys) -> HashMap { @@ -129,10 +133,11 @@ fn generate_keys_parameters(command: &CommandGenerateKeys) -> HashMap Result<()> { let user = control_server.get_user_by_email(&generate_keys.email).await?; let now = Utc::now(); - let key = CreateDataKeyDTO { + let mut key = CreateDataKeyDTO { name: generate_keys.name.clone(), description: generate_keys.description.clone(), - visibility: "public".to_string(), attributes: generate_keys_parameters(&generate_keys), - key_type: generate_keys.key_type.to_string(), + key_type: generate_keys.key_type.clone(), + parent_id: None, expire_at: format!("{}", now + Duration::days(30)), }; + if let Some(id) = generate_keys.param_x509_parent_name { + let data_key = control_server.get_key_by_name(&id).await?; + key.parent_id = Some(data_key.id); + } key.validate()?; let keys = control_server.create_keys(&mut DataKey::create_from(key, UserIdentity::from(user))?).await?; diff --git a/src/domain/datakey/entity.rs b/src/domain/datakey/entity.rs index f9a9bf1f13502c45104f6a6c2b8b54a1620298bc..23cb90028b3c26ad5d9b10003a283fd10036cef8 100644 --- a/src/domain/datakey/entity.rs +++ b/src/domain/datakey/entity.rs @@ -26,6 +26,8 @@ use std::str::FromStr; use crate::domain::encryption_engine::EncryptionEngine; +pub const INFRA_CONFIG_DOMAIN_NAME: &str = "domain_name"; + #[derive(Debug, Clone, Default, PartialEq)] @@ -33,6 +35,8 @@ pub enum KeyState { Enabled, #[default] Disabled, + PendingRevoke, + Revoked, PendingDelete, Deleted } @@ -44,7 +48,9 @@ impl FromStr for KeyState { match s { "enabled" => Ok(KeyState::Enabled), "disabled" => Ok(KeyState::Disabled), - "pending" => Ok(KeyState::PendingDelete), + "pending_revoke" => Ok(KeyState::PendingRevoke), + "revoked" => Ok(KeyState::Revoked), + "pending_delete" => Ok(KeyState::PendingDelete), "deleted" => Ok(KeyState::Deleted), _ => Err(Error::UnsupportedTypeError(format!("unsupported data key state {}", s))), } @@ -52,20 +58,75 @@ impl FromStr for KeyState { } impl Display for KeyState { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { KeyState::Enabled => write!(f, "enabled"), KeyState::Disabled => write!(f, "disabled"), - KeyState::PendingDelete => write!(f, "pending"), + KeyState::PendingRevoke => write!(f, "pending_revoke"), + KeyState::Revoked => write!(f, "revoked"), + KeyState::PendingDelete => write!(f, "pending_delete"), KeyState::Deleted => write!(f, "deleted"), } } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum KeyAction { + Revoke, + CancelRevoke, + Delete, + CancelDelete, + Disable, + Enable, + IssueCert, + Sign, + Read, +} + +impl FromStr for KeyAction { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "revoke" => Ok(KeyAction::Revoke), + "cancel_revoke" => Ok(KeyAction::CancelRevoke), + "delete" => Ok(KeyAction::Delete), + "cancel_delete" => Ok(KeyAction::CancelDelete), + "disable" => Ok(KeyAction::Disable), + "enable" => Ok(KeyAction::Enable), + "issue_cert" => Ok(KeyAction::IssueCert), + "sign" => Ok(KeyAction::Sign), + "read" => Ok(KeyAction::Read), + _ => Err(Error::UnsupportedTypeError(format!("unsupported data key action {}", s))), + } + } +} + +impl Display for KeyAction { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + KeyAction::Revoke => write!(f, "revoke"), + KeyAction::CancelRevoke => write!(f, "cancel_revoke"), + KeyAction::Delete => write!(f, "delete"), + KeyAction::CancelDelete => write!(f, "cancel_delete"), + KeyAction::Disable => write!(f, "disable"), + KeyAction::Enable => write!(f, "enable"), + KeyAction::IssueCert => write!(f, "issue_cert"), + KeyAction::Read => write!(f, "read"), + KeyAction::Sign => write!(f, "sign"), + } + } +} + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] pub enum KeyType { OpenPGP, - X509 + // X509 Certificate Authority + X509CA, + // X509 Intermediate Certificate Authority + X509ICA, + // X509 End Entity + X509EE, } impl FromStr for KeyType { @@ -74,7 +135,9 @@ impl FromStr for KeyType { fn from_str(s: &str) -> Result { match s { "pgp" => Ok(KeyType::OpenPGP), - "x509" => Ok(KeyType::X509), + "x509ca" => Ok(KeyType::X509CA), + "x509ica" => Ok(KeyType::X509ICA), + "x509ee" => Ok(KeyType::X509EE), _ => Err(Error::UnsupportedTypeError(format!("unsupported data key type {}", s))), } } @@ -84,11 +147,101 @@ impl Display for KeyType { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { KeyType::OpenPGP => write!(f, "pgp"), - KeyType::X509 => write!(f, "x509"), + KeyType::X509CA => write!(f, "x509ca"), + KeyType::X509ICA => write!(f, "x509ica"), + KeyType::X509EE => write!(f, "x509ee"), } } } +#[derive(Debug, Clone)] +pub struct X509CRL { + pub id: i32, + pub ca_id: i32, + pub data: Vec, + pub create_at: DateTime, + pub update_at: DateTime, +} + +impl X509CRL { + pub fn new(ca_id: i32, data: Vec, create_at: DateTime, update_at: DateTime) -> Self { + X509CRL { + id: 0, + ca_id, + data, + create_at, + update_at, + } + } +} + +#[derive(Debug, Clone)] +pub enum X509RevokeReason { + Unspecified, + KeyCompromise, + CACompromise, + AffiliationChanged, + Superseded, + CessationOfOperation, + CertificateHold, + PrivilegeWithdrawn, + AACompromise, +} + +impl FromStr for X509RevokeReason { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + match s { + "unspecified" => Ok(X509RevokeReason::Unspecified), + "key_compromise" => Ok(X509RevokeReason::KeyCompromise), + "ca_compromise" => Ok(X509RevokeReason::CACompromise), + "affiliation_changed" => Ok(X509RevokeReason::AffiliationChanged), + "superseded" => Ok(X509RevokeReason::Superseded), + "cessation_of_operation" => Ok(X509RevokeReason::CessationOfOperation), + "certificate_hold" => Ok(X509RevokeReason::CertificateHold), + "privilege_withdrawn" => Ok(X509RevokeReason::PrivilegeWithdrawn), + "aa_compromise" => Ok(X509RevokeReason::AACompromise), + _ => Err(Error::UnsupportedTypeError(format!("unsupported x509 revoke reason {}", s))), + } + } +} + +impl Display for X509RevokeReason { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + X509RevokeReason::Unspecified => write!(f, "unspecified"), + X509RevokeReason::KeyCompromise => write!(f, "key_compromise"), + X509RevokeReason::CACompromise => write!(f, "ca_compromise"), + X509RevokeReason::AffiliationChanged => write!(f, "affiliation_changed"), + X509RevokeReason::Superseded => write!(f, "superseded"), + X509RevokeReason::CessationOfOperation => write!(f, "cessation_of_operation"), + X509RevokeReason::CertificateHold => write!(f, "certificate_hold"), + X509RevokeReason::PrivilegeWithdrawn => write!(f, "privilege_withdrawn"), + X509RevokeReason::AACompromise => write!(f, "aa_compromise"), + } + } +} + +#[derive(Debug, Clone)] +pub struct ParentKey { + pub name: String, + pub private_key: Vec, + pub public_key: Vec, + pub certificate: Vec, + pub attributes: HashMap, +} + +#[derive(Debug, Clone)] +pub struct RevokedKey { + pub id: i32, + pub key_id: i32, + pub ca_id: i32, + pub reason: X509RevokeReason, + pub create_at: DateTime, + pub serial_number: Option +} + #[derive(Debug, Clone)] pub struct DataKey { pub id: i32, @@ -98,7 +251,9 @@ pub struct DataKey { pub user: i32, pub attributes: HashMap, pub key_type: KeyType, + pub parent_id: Option, pub fingerprint: String, + pub serial_number: Option, pub private_key: Vec, pub public_key: Vec, pub certificate: Vec, @@ -107,6 +262,8 @@ pub struct DataKey { pub key_state: KeyState, pub user_email: Option, pub request_delete_users: Option, + pub request_revoke_users: Option, + pub parent_key: Option } impl ExtendableAttributes for DataKey { @@ -134,23 +291,47 @@ impl Identity for DataKey { } } +#[derive(Clone)] +pub struct SecParentDateKey { + pub name: String, + pub private_key: SecVec, + pub public_key: SecVec, + pub certificate: SecVec, + pub attributes: HashMap +} + pub struct SecDataKey { + pub name: String, pub private_key: SecVec, pub public_key: SecVec, pub certificate: SecVec, pub identity: String, - pub attributes: HashMap + pub attributes: HashMap, + pub parent: Option } impl SecDataKey { pub async fn load(data_key: &DataKey, engine: &Box) -> Result { - Ok(Self { + let mut sec_datakey = Self { + name: data_key.name.clone(), private_key: SecVec::new(engine.decode(data_key.private_key.clone()).await?), public_key: SecVec::new(engine.decode(data_key.public_key.clone()).await?), certificate: SecVec::new(engine.decode(data_key.certificate.clone()).await?), identity: data_key.get_identity(), attributes: data_key.attributes.clone(), - }) + parent: None, + }; + if let Some(parent_key) = data_key.parent_key.clone() { + let sec_parent_key = SecParentDateKey { + name: parent_key.name, + private_key: SecVec::new(engine.decode(parent_key.private_key).await?), + public_key: SecVec::new(engine.decode(parent_key.public_key).await?), + certificate: SecVec::new(engine.decode(parent_key.certificate).await?), + attributes: parent_key.attributes, + }; + sec_datakey.parent = Some(sec_parent_key); + } + Ok(sec_datakey) } } @@ -159,13 +340,15 @@ pub struct DataKeyContent { pub public_key: Vec, pub certificate: Vec, pub fingerprint: String, + pub serial_number: Option, } #[derive(Debug, Clone, Default, PartialEq)] pub enum Visibility { #[default] Public, - Private, + //NOTE: We don't support private key now. + //Private } impl FromStr for Visibility { @@ -174,7 +357,6 @@ impl FromStr for Visibility { fn from_str(s: &str) -> Result { match s { "public" => Ok(Visibility::Public), - "private" => Ok(Visibility::Private), _ => Err(Error::UnsupportedTypeError(format!("unsupported data key visibility {}", s))), } } @@ -184,7 +366,6 @@ impl Display for Visibility { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Visibility::Public => write!(f, "public"), - Visibility::Private => write!(f, "private"), } } } diff --git a/src/domain/datakey/repository.rs b/src/domain/datakey/repository.rs index 8584bdb83e8ffc853192928c9f5c8ab9dcbcb40a..9623e64a69d7abcd8912e565caaed7c863076672 100644 --- a/src/domain/datakey/repository.rs +++ b/src/domain/datakey/repository.rs @@ -17,19 +17,26 @@ use super::entity::DataKey; use crate::util::error::Result; use async_trait::async_trait; -use crate::domain::datakey::entity::{KeyState}; +use chrono::Duration; +use crate::domain::datakey::entity::{KeyState, KeyType, RevokedKey, X509CRL, X509RevokeReason}; #[async_trait] pub trait Repository: Send + Sync { async fn create(&self, data_key: DataKey) -> Result; - async fn get_all_keys(&self) -> Result>; - async fn get_public_keys(&self) -> Result>; - async fn get_private_keys(&self, user_id: i32) -> Result>; + async fn delete(&self, id: i32) -> Result<()>; + async fn get_all_keys(&self, key_type: Option) -> Result>; async fn get_by_id(&self, id: i32) -> Result; - async fn get_by_name(&self, name: &String) -> Result; + async fn get_by_name(&self, name: &str) -> Result; async fn update_state(&self, id: i32, state: KeyState) -> Result<()>; + async fn update_key_data(&self, data_key: DataKey) -> Result<()>; async fn get_enabled_key_by_type_and_name(&self, key_type: String, name: String) -> Result; - async fn delete_private_key(&self, id: i32, user_id: i32) -> Result<()>; - async fn request_delete_public_key(&self, user_id: i32, user_email: String, id: i32) -> Result<()>; - async fn cancel_delete_public_key(&self, user_id: i32, id: i32) -> Result<()>; + async fn request_delete_key(&self, user_id: i32, user_email: String, id: i32) -> Result<()>; + async fn request_revoke_key(&self, user_id: i32, user_email: String, id: i32, parent_id: i32, reason: X509RevokeReason) -> Result<()>; + async fn cancel_delete_key(&self, user_id: i32, id: i32) -> Result<()>; + async fn cancel_revoke_key(&self, user_id: i32, id: i32, parent_id: i32) -> Result<()>; + //crl related methods + async fn get_x509_crl_by_ca_id(&self, id: i32) -> Result; + async fn upsert_x509_crl(&self, crl: X509CRL) -> Result<()>; + async fn get_keys_for_crl_update(&self, duration: Duration) -> Result>; + async fn get_revoked_serial_number_by_parent_id(&self, id: i32) -> Result>; } diff --git a/src/domain/sign_plugin.rs b/src/domain/sign_plugin.rs index 89dc83940d5f5c52318469dba8d709076df7429c..6ad2557fa5bd7e470e24d823fe2f63ac910908d8 100644 --- a/src/domain/sign_plugin.rs +++ b/src/domain/sign_plugin.rs @@ -16,7 +16,8 @@ use crate::util::error::Result; use std::collections::HashMap; -use crate::domain::datakey::entity::{DataKey, DataKeyContent, SecDataKey}; +use chrono::{DateTime, Utc}; +use crate::domain::datakey::entity::{DataKey, DataKeyContent, KeyType, RevokedKey, SecDataKey}; pub trait SignPlugins: Send + Sync { fn new(db: SecDataKey) -> Result @@ -32,10 +33,7 @@ pub trait SignPlugins: Send + Sync { ) -> HashMap where Self: Sized; - fn generate_keys( - attributes: &HashMap, - ) -> Result - where - Self: Sized; + fn generate_keys(&self, key_type: &KeyType, infra_configs: &HashMap) -> Result; fn sign(&self, content: Vec, options: HashMap) -> Result>; + fn generate_crl_content(&self, revoked_keys: Vec, last_update: DateTime, next_update: DateTime) -> Result>; } diff --git a/src/domain/sign_service.rs b/src/domain/sign_service.rs index d3fe97617b70b556d2b1af324788453fdb6326a9..b4e53ab42ba0a5124107215684f1c28f204e2127 100644 --- a/src/domain/sign_service.rs +++ b/src/domain/sign_service.rs @@ -18,8 +18,9 @@ use crate::util::error::{Result, Error}; use std::collections::HashMap; use std::str::FromStr; -use crate::domain::datakey::entity::DataKey; +use crate::domain::datakey::entity::{DataKey, RevokedKey}; use async_trait::async_trait; +use chrono::{DateTime, Utc}; #[derive(Debug, PartialEq)] pub enum SignBackendType { @@ -44,6 +45,7 @@ pub trait SignBackend: Send + Sync{ async fn rotate_key(&mut self) -> Result; async fn sign(&self, data_key: &DataKey, content: Vec, options: HashMap) -> Result>; async fn decode_public_keys(&self, data_key: &mut DataKey) -> Result<()>; + async fn generate_crl_content(&self, data_key: &DataKey, revoked_keys: Vec, last_update: DateTime, next_update: DateTime) -> Result>; } diff --git a/src/infra/database/model/datakey/dto.rs b/src/infra/database/model/datakey/dto.rs index b6d55590bb7539f675d86771124d1b0d49ddd1ee..5dcfad08384bb34cffcb9fb9f5cc127950f658d6 100644 --- a/src/infra/database/model/datakey/dto.rs +++ b/src/infra/database/model/datakey/dto.rs @@ -14,7 +14,7 @@ * */ -use crate::domain::datakey::entity::{DataKey, KeyState, Visibility}; +use crate::domain::datakey::entity::{DataKey, KeyState, Visibility, X509CRL}; use crate::domain::datakey::entity::KeyType; use crate::domain::datakey::traits::ExtendableAttributes; use crate::util::error::{Error}; @@ -33,15 +33,23 @@ pub(super) struct DataKeyDTO { pub user: i32, pub attributes: String, pub key_type: String, + pub parent_id: Option, pub fingerprint: String, + pub serial_number: Option, pub private_key: String, pub public_key: String, pub certificate: String, pub create_at: DateTime, pub expire_at: DateTime, pub key_state: String, + #[sqlx(default)] pub user_email: Option, + #[sqlx(default)] pub request_delete_users: Option, + #[sqlx(default)] + pub request_revoke_users: Option, + #[sqlx(default)] + pub x509_crl_update_at: Option> } @@ -57,7 +65,9 @@ impl TryFrom for DataKey { user: dto.user, attributes: serde_json::from_str(dto.attributes.as_str())?, key_type: KeyType::from_str(&dto.key_type)?, + parent_id: dto.parent_id, fingerprint: dto.fingerprint.clone(), + serial_number: dto.serial_number, private_key: key::decode_hex_string_to_u8(&dto.private_key), public_key: key::decode_hex_string_to_u8(&dto.public_key), certificate: key::decode_hex_string_to_u8(&dto.certificate), @@ -65,7 +75,9 @@ impl TryFrom for DataKey { expire_at: dto.expire_at, key_state: KeyState::from_str(&dto.key_state)?, user_email: dto.user_email, - request_delete_users: dto.request_delete_users + request_delete_users: dto.request_delete_users, + request_revoke_users: dto.request_revoke_users, + parent_key: None, }) } } @@ -82,7 +94,9 @@ impl TryFrom for DataKeyDTO { user: data_key.user, attributes: data_key.serialize_attributes()?, key_type: data_key.key_type.to_string(), + parent_id: data_key.parent_id, fingerprint: data_key.fingerprint.clone(), + serial_number: data_key.serial_number, private_key: key::encode_u8_to_hex_string( &data_key.private_key ), @@ -96,7 +110,46 @@ impl TryFrom for DataKeyDTO { expire_at: data_key.expire_at, key_state: data_key.key_state.to_string(), user_email: None, - request_delete_users: None + request_delete_users: None, + request_revoke_users: None, + x509_crl_update_at: None, + }) + } +} + +#[derive(Debug, FromRow)] +pub struct X509CRLDTO { + pub id: i32, + pub ca_id: i32, + pub data: String, + pub create_at: DateTime, + pub update_at: DateTime, +} + +impl TryFrom for X509CRL { + type Error = Error; + + fn try_from(value: X509CRLDTO) -> Result { + Ok(X509CRL { + id: value.id, + ca_id: value.ca_id, + data: key::decode_hex_string_to_u8(&value.data), + create_at: value.create_at, + update_at: value.update_at, + }) + } +} + +impl TryFrom for X509CRLDTO { + type Error = Error; + + fn try_from(value: X509CRL) -> Result { + Ok(X509CRLDTO { + id: value.id, + ca_id: value.ca_id, + data: key::encode_u8_to_hex_string(&value.data), + create_at: value.create_at, + update_at: value.update_at, }) } } diff --git a/src/infra/database/model/datakey/repository.rs b/src/infra/database/model/datakey/repository.rs index 6f0f7012fa4d52b9ee5841aa77a63eddb673aa52..32aa0278047ea4850bed7abdd4ac12ad06667844 100644 --- a/src/infra/database/model/datakey/repository.rs +++ b/src/infra/database/model/datakey/repository.rs @@ -16,16 +16,19 @@ use super::dto::DataKeyDTO; use crate::infra::database::pool::DbPool; -use crate::domain::datakey::entity::{DataKey, KeyState, Visibility}; +use crate::domain::datakey::entity::{DataKey, KeyState, KeyType, ParentKey, RevokedKey, X509CRL, X509RevokeReason}; use crate::domain::datakey::repository::Repository; use crate::util::error::{Result}; use async_trait::async_trait; use std::boxed::Box; -use chrono::{Utc}; +use chrono::Duration; +use chrono::Utc; use sqlx::{MySql, Transaction}; -use crate::infra::database::model::request_delete::dto::RequestDeleteDTO; +use crate::infra::database::model::datakey::dto::X509CRLDTO; +use crate::infra::database::model::request_delete::dto::{PendingOperationDTO, RequestType, RevokedKeyDTO}; +use crate::util::error; -const DELETE_THRESHOLD: i32 = 3; +const PENDING_THRESHOLD: i32 = 3; #[derive(Clone)] pub struct DataKeyRepository { @@ -39,21 +42,44 @@ impl DataKeyRepository { } } - async fn create_request_delete(&self, user_id: i32, user_email: String, id: i32, tx: &mut Transaction<'_, MySql>) -> Result<()> { - let _ : Option = sqlx::query_as("INSERT IGNORE INTO request_delete(user_id, key_id, user_email, create_at) VALUES (?, ?, ?, ?)") - .bind(user_id) - .bind(id) - .bind(user_email) - .bind(Utc::now()) + async fn create_pending_operation(&self, pending_operation: PendingOperationDTO, tx: &mut Transaction<'_, MySql>) -> Result<()> { + let _ : Option = sqlx::query_as("INSERT IGNORE INTO pending_operation(user_id, key_id, user_email, create_at, request_type) VALUES (?, ?, ?, ?, ?)") + .bind(pending_operation.user_id) + .bind(pending_operation.key_id) + .bind(pending_operation.user_email) + .bind(pending_operation.create_at) + .bind(pending_operation.request_type.to_string()) .fetch_optional(tx) .await?; Ok(()) } - async fn delete_request_delete(&self, user_id: i32, id: i32, tx: &mut Transaction<'_, MySql>) -> Result<()> { - let _ : Option = sqlx::query_as("DELETE FROM request_delete WHERE user_id = ? AND key_id = ?") + async fn delete_pending_operation(&self, user_id: i32, id: i32, request_type: RequestType, tx: &mut Transaction<'_, MySql>) -> Result<()> { + let _ : Option = sqlx::query_as("DELETE FROM pending_operation WHERE user_id = ? AND key_id = ? and request_type = ?") .bind(user_id) .bind(id) + .bind(request_type.to_string()) + .fetch_optional(tx) + .await?; + Ok(()) + } + + async fn create_revoke_record(&self, key_id: i32, ca_id: i32, reason: X509RevokeReason, tx: &mut Transaction<'_, MySql>) -> Result<()> { + let revoked = RevokedKeyDTO::new(key_id, ca_id, reason); + let _ : Option = sqlx::query_as("INSERT IGNORE INTO x509_keys_revoked(ca_id, key_id, create_at, reason) VALUES (?, ?, ?, ?)") + .bind(revoked.ca_id) + .bind(revoked.key_id) + .bind(revoked.create_at) + .bind(revoked.reason) + .fetch_optional(tx) + .await?; + Ok(()) + } + + async fn delete_revoke_record(&self, key_id: i32, ca_id: i32, tx: &mut Transaction<'_, MySql>) -> Result<()> { + let _ : Option = sqlx::query_as("DELETE FROM x509_keys_revoked WHERE key_id = ? AND ca_id = ?") + .bind(key_id) + .bind(ca_id) .fetch_optional(tx) .await?; Ok(()) @@ -64,7 +90,7 @@ impl DataKeyRepository { impl Repository for DataKeyRepository { async fn create(&self, data_key: DataKey) -> Result { let dto = DataKeyDTO::try_from(data_key)?; - let record : u64 = sqlx::query("INSERT INTO data_key(name, description, user, attributes, key_type, fingerprint, private_key, public_key, certificate, create_at, expire_at, key_state, visibility) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + let record : u64 = sqlx::query("INSERT INTO data_key(name, description, user, attributes, key_type, fingerprint, private_key, public_key, certificate, create_at, expire_at, key_state, visibility, parent_id, serial_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") .bind(&dto.name) .bind(&dto.description) .bind(dto.user) @@ -78,23 +104,78 @@ impl Repository for DataKeyRepository { .bind(dto.expire_at) .bind(dto.key_state) .bind(dto.visibility) + .bind(dto.parent_id) + .bind(dto.serial_number) .execute(&self.db_pool) .await?.last_insert_id(); - return self.get_by_id(record as i32).await + let mut datakey = self.get_by_id(record as i32).await?; + //fetch parent key if 'parent_id' exists. + if let Some(parent) = datakey.parent_id { + let result = self.get_by_id(parent).await; + match result { + Ok(parent) => { + datakey.parent_key = Some(ParentKey { + name: parent.name, + private_key: parent.private_key.clone(), + public_key: parent.public_key.clone(), + certificate: parent.certificate.clone(), + attributes: parent.attributes + }) + } + Err(error::Error::NotFoundError) => { + let _ = self.delete(record as i32).await; + return Err(error::Error::InvalidArgumentError("parent key does not exist".to_string())); + } + _ => { + let _ = self.delete(record as i32).await; + return Err(error::Error::DatabaseError("unable to find parent key".to_string())); + } + } + } + Ok(datakey) } - async fn get_public_keys(&self) -> Result> { - let dtos: Vec = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users \ + async fn delete(&self, id: i32) -> Result<()> { + let _: Option = sqlx::query_as("DELETE FROM data_key WHERE id = ?") + .bind(id) + .fetch_optional(&self.db_pool) + .await?; + Ok(()) + } + + async fn get_all_keys(&self, key_type: Option) -> Result> { + let dtos: Vec = match key_type { + None => { + sqlx::query_as( + "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ + GROUP_CONCAT(K.user_email) as request_revoke_users \ FROM data_key D \ INNER JOIN user U ON D.user = U.id \ - LEFT JOIN request_delete R ON D.id = R.key_id \ - WHERE D.key_state != ? and D.visibility = ? \ + LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ + LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ + WHERE D.key_state != ? \ GROUP BY D.id") - .bind(KeyState::Deleted.to_string()) - .bind(Visibility::Public.to_string()) - .fetch_all(&self.db_pool) - .await?; + .bind(KeyState::Deleted.to_string()) + .fetch_all(&self.db_pool) + .await? + } + Some(key_t) => { + sqlx::query_as( + "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ + GROUP_CONCAT(K.user_email) as request_revoke_users \ + FROM data_key D \ + INNER JOIN user U ON D.user = U.id \ + LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ + LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ + WHERE D.key_state != ? AND \ + D.key_type = ? \ + GROUP BY D.id") + .bind(KeyState::Deleted.to_string()) + .bind(key_t.to_string()) + .fetch_all(&self.db_pool) + .await? + } + }; let mut results = vec![]; for dto in dtos.into_iter() { results.push(DataKey::try_from(dto)?); @@ -102,49 +183,57 @@ impl Repository for DataKeyRepository { Ok(results) } - async fn get_all_keys(&self) -> Result> { + async fn get_keys_for_crl_update(&self, duration: Duration) -> Result> { + let now = Utc::now(); let dtos: Vec = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users \ + "SELECT D.id, D.name, D.description, D.user, D.attributes, D.key_type, D.fingerprint, D.private_key, D.public_key, D.certificate, D.create_at, D.expire_at, D.key_state, D.visibility, D.parent_id, D.serial_number, R.update_at AS x509_crl_update_at \ FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN request_delete R ON D.id = R.key_id \ - WHERE D.key_state != ? \ - GROUP BY D.id") + LEFT JOIN x509_crl_content R ON D.id = R.ca_id \ + WHERE (D.key_type = ? OR D.key_type = ?) AND D.key_state != ?") + .bind(KeyType::X509ICA.to_string()) + .bind(KeyType::X509CA.to_string()) .bind(KeyState::Deleted.to_string()) .fetch_all(&self.db_pool) .await?; let mut results = vec![]; for dto in dtos.into_iter() { - results.push(DataKey::try_from(dto)?); + if dto.x509_crl_update_at.is_none() { + results.push(DataKey::try_from(dto)?); + } else { + let update_at = dto.x509_crl_update_at.unwrap(); + if update_at + duration <= now { + results.push(DataKey::try_from(dto)?); + } + } } Ok(results) } - async fn get_private_keys(&self, user_id: i32) -> Result> { - let dtos: Vec = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users \ - FROM data_key D \ - INNER JOIN user U ON D.user = U.id \ - LEFT JOIN request_delete R ON D.id = R.key_id \ - WHERE D.key_state != ? and D.visibility = ? and D.user = ? \ - GROUP BY D.id") - .bind(KeyState::Deleted.to_string()) - .bind(Visibility::Private.to_string()) - .bind(user_id) + + async fn get_revoked_serial_number_by_parent_id(&self, id: i32) -> Result> { + let dtos : Vec = sqlx::query_as( + "SELECT R.*, D.serial_number \ + FROM x509_keys_revoked R \ + INNER JOIN data_key D ON R.key_id = D.id \ + WHERE R.ca_id = ? AND D.key_state = ?") + .bind(id) + .bind(KeyState::Revoked.to_string()) .fetch_all(&self.db_pool) .await?; let mut results = vec![]; for dto in dtos.into_iter() { - results.push(DataKey::try_from(dto)?); + results.push(RevokedKey::try_from(dto)?); } Ok(results) } async fn get_by_id(&self, id: i32) -> Result { let dto: DataKeyDTO = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users \ + "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ + GROUP_CONCAT(K.user_email) as request_revoke_users \ FROM data_key D \ INNER JOIN user U ON D.user = U.id \ - LEFT JOIN request_delete R ON D.id = R.key_id \ + LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ + LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ WHERE D.id = ? AND D.key_state != ? \ GROUP BY D.id") .bind(id) @@ -154,12 +243,14 @@ impl Repository for DataKeyRepository { Ok(DataKey::try_from(dto)?) } - async fn get_by_name(&self, name: &String) -> Result { + async fn get_by_name(&self, name: &str) -> Result { let dto: DataKeyDTO = sqlx::query_as( - "SELECT D.*, U.email AS user_email GROUP_CONCAT(R.user_email) as request_delete_users \ + "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ + GROUP_CONCAT(K.user_email) as request_revoke_users \ FROM data_key D \ INNER JOIN user U ON D.user = U.id \ - LEFT JOIN request_delete R ON D.id = R.key_id \ + LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ + LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ WHERE D.name = ? AND D.key_state != ? \ GROUP BY D.id") .bind(name) @@ -180,12 +271,30 @@ impl Repository for DataKeyRepository { Ok(()) } + async fn update_key_data(&self, data_key: DataKey) -> Result<()> { + //Note: if the key in deleted status, it cannot be updated to other states + let dto = DataKeyDTO::try_from(data_key)?; + let _: Option = sqlx::query_as("UPDATE data_key SET serial_number = ?, fingerprint = ?, private_key = ?, public_key = ?, certificate = ? WHERE id = ? AND key_state != ?") + .bind(dto.serial_number) + .bind(dto.fingerprint) + .bind(dto.private_key) + .bind(dto.public_key) + .bind(dto.certificate) + .bind(dto.id) + .bind(KeyState::Deleted.to_string()) + .fetch_optional(&self.db_pool) + .await?; + Ok(()) + } + async fn get_enabled_key_by_type_and_name(&self, key_type: String, name: String) -> Result { let dto: DataKeyDTO = sqlx::query_as( - "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users \ + "SELECT D.*, U.email AS user_email, GROUP_CONCAT(R.user_email) as request_delete_users, \ + GROUP_CONCAT(K.user_email) as request_revoke_users \ FROM data_key D \ INNER JOIN user U ON D.user = U.id \ - LEFT JOIN request_delete R ON D.id = R.key_id \ + LEFT JOIN pending_operation R ON D.id = R.key_id and R.request_type = 'delete' \ + LEFT JOIN pending_operation K ON D.id = K.key_id and K.request_type = 'revoke' \ WHERE D.name = ? AND D.key_type = ? AND D.key_state = ? \ GROUP BY D.id") .bind(name) @@ -196,59 +305,96 @@ impl Repository for DataKeyRepository { Ok(DataKey::try_from(dto)?) } - async fn delete_private_key(&self, id: i32, user_id: i32) -> Result<()> { - let _: Option = sqlx::query_as("UPDATE data_key SET key_state = ? WHERE id = ? AND visibility = ? AND user = ?") + async fn request_delete_key(&self, user_id: i32, user_email: String, id: i32) -> Result<()> { + let mut tx = self.db_pool.begin().await?; + //1. update key state to pending delete if needed. + let _: Option = sqlx::query_as( + "UPDATE data_key SET key_state = ? \ + WHERE id = ?") + .bind(KeyState::PendingDelete.to_string()) + .bind(id) + .fetch_optional(&mut tx) + .await?; + //2. add request delete record + let pending_delete = PendingOperationDTO::new_for_delete(id, user_id, user_email); + self.create_pending_operation(pending_delete, &mut tx).await?; + //3. delete datakey if pending delete count >= threshold + let _: Option = sqlx::query_as( + "UPDATE data_key SET key_state = ? \ + WHERE id = ? AND ( \ + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) >= ?") .bind(KeyState::Deleted.to_string()) .bind(id) - .bind(Visibility::Private.to_string()) - .bind(user_id) - .fetch_optional(&self.db_pool) + .bind(id) + .bind(PENDING_THRESHOLD) + .fetch_optional(&mut tx) .await?; + tx.commit().await?; Ok(()) } - async fn request_delete_public_key(&self, user_id: i32, user_email: String, id: i32) -> Result<()> { + async fn request_revoke_key(&self, user_id: i32, user_email: String, id: i32, parent_id: i32, reason: X509RevokeReason) -> Result<()> { let mut tx = self.db_pool.begin().await?; //1. update key state to pending delete if needed. let _: Option = sqlx::query_as( "UPDATE data_key SET key_state = ? \ - WHERE id = ? AND visibility = ? AND key_state != ?") - .bind(KeyState::PendingDelete.to_string()) + WHERE id = ?") + .bind(KeyState::PendingRevoke.to_string()) .bind(id) - .bind(Visibility::Public.to_string()) - .bind(KeyState::PendingDelete.to_string()) .fetch_optional(&mut tx) .await?; - //2. add request delete record - self.create_request_delete(user_id, user_email, id, &mut tx).await?; - //3. delete datakey if pending delete count >= threshold + //2. add request revoke pending record + let pending_revoke = PendingOperationDTO::new_for_revoke(id, user_id, user_email); + self.create_pending_operation(pending_revoke, &mut tx).await?; + //3. add revoked record + self.create_revoke_record(id, parent_id, reason, &mut tx).await?; + //4. mark datakey revoked if pending revoke count >= threshold let _: Option = sqlx::query_as( "UPDATE data_key SET key_state = ? \ - WHERE id = ? AND visibility = ? AND ( \ - SELECT COUNT(*) FROM request_delete WHERE key_id = ?) >= ?") - .bind(KeyState::Deleted.to_string()) + WHERE id = ? AND ( \ + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) >= ?") + .bind(KeyState::Revoked.to_string()) .bind(id) - .bind(Visibility::Public.to_string()) .bind(id) - .bind(DELETE_THRESHOLD) + .bind(PENDING_THRESHOLD) .fetch_optional(&mut tx) .await?; tx.commit().await?; Ok(()) } - async fn cancel_delete_public_key(&self, user_id: i32, id: i32) -> Result<()> { + async fn cancel_delete_key(&self, user_id: i32, id: i32) -> Result<()> { let mut tx = self.db_pool.begin().await?; //1. delete pending delete record - self.delete_request_delete(user_id, id, &mut tx).await?; + self.delete_pending_operation(user_id, id, RequestType::Delete, &mut tx).await?; //2. update status if there is not any pending delete record. let _: Option = sqlx::query_as( "UPDATE data_key SET key_state = ? \ - WHERE id = ? AND visibility = ? AND ( \ - SELECT COUNT(*) FROM request_delete WHERE key_id = ?) = ?") + WHERE id = ? AND ( \ + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) = ?") + .bind(KeyState::Disabled.to_string()) + .bind(id) + .bind(id) + .bind(0) + .fetch_optional(&mut tx) + .await?; + tx.commit().await?; + Ok(()) + } + + async fn cancel_revoke_key(&self, user_id: i32, id: i32, parent_id: i32) -> Result<()> { + let mut tx = self.db_pool.begin().await?; + //1. delete pending delete record + self.delete_pending_operation(user_id, id, RequestType::Revoke, &mut tx).await?; + //2. delete revoked record + self.delete_revoke_record(id, parent_id, &mut tx).await?; + //3. update status if there is not any pending delete record. + let _: Option = sqlx::query_as( + "UPDATE data_key SET key_state = ? \ + WHERE id = ? AND ( \ + SELECT COUNT(*) FROM pending_operation WHERE key_id = ?) = ?") .bind(KeyState::Disabled.to_string()) .bind(id) - .bind(Visibility::Public.to_string()) .bind(id) .bind(0) .fetch_optional(&mut tx) @@ -256,4 +402,40 @@ impl Repository for DataKeyRepository { tx.commit().await?; Ok(()) } + + async fn get_x509_crl_by_ca_id(&self, id: i32) -> Result { + let dto: X509CRLDTO = sqlx::query_as( + "SELECT * from x509_crl_content WHERE ca_id = ?") + .bind(id) + .fetch_one(&self.db_pool) + .await?; + Ok( X509CRL::try_from(dto)?) + } + + async fn upsert_x509_crl(&self, crl: X509CRL) -> Result<()> { + let dto = X509CRLDTO::try_from(crl)?; + match self.get_x509_crl_by_ca_id(dto.ca_id).await { + Ok(_) => { + sqlx::query( + "UPDATE x509_crl_content SET data = ?, create_at = ?, update_at = ? WHERE ca_id = ?") + .bind(dto.data) + .bind(dto.create_at) + .bind(dto.update_at) + .bind(dto.ca_id) + .execute(&self.db_pool) + .await?; + } + Err(_) => { + sqlx::query( + "INSERT INTO x509_crl_content(ca_id, data, create_at, update_at) VALUES (?, ?, ?, ?)") + .bind(dto.ca_id) + .bind(dto.data) + .bind(dto.create_at) + .bind(dto.update_at) + .execute(&self.db_pool) + .await?; + } + } + Ok(()) + } } diff --git a/src/infra/database/model/request_delete/dto.rs b/src/infra/database/model/request_delete/dto.rs index 1f0c8f2d65842018398d9669e3bdc1948075daca..72438a86aa555c3f0dbcfc4bcaf1f5936380ec76 100644 --- a/src/infra/database/model/request_delete/dto.rs +++ b/src/infra/database/model/request_delete/dto.rs @@ -13,26 +13,112 @@ * * // See the Mulan PSL v2 for more details. * */ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; use sqlx::FromRow; use chrono::{DateTime, Utc}; +use crate::domain::datakey::entity::{RevokedKey, X509RevokeReason}; +use crate::util::error::Error; + +#[derive(Debug, Clone, PartialEq, sqlx::Type)] +pub enum RequestType { + #[sqlx(rename = "delete")] + Delete, + #[sqlx(rename = "revoke")] + Revoke, +} + +impl Display for RequestType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + RequestType::Delete => write!(f, "delete"), + RequestType::Revoke => write!(f, "revoke"), + } + } +} + +impl FromStr for RequestType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "delete" => Ok(RequestType::Delete), + "revoke" => Ok(RequestType::Revoke), + _ => Err(Error::UnsupportedTypeError(s.to_string())) + } + } +} + +#[derive(Debug, FromRow)] +pub struct RevokedKeyDTO { + pub id: i32, + pub key_id: i32, + pub ca_id: i32, + pub reason: String, + pub serial_number: Option, + pub create_at: DateTime, +} + +impl RevokedKeyDTO { + pub fn new(key_id: i32, ca_id: i32, reason: X509RevokeReason) -> Self { + Self { + id: 0, + key_id, + ca_id, + create_at: Utc::now(), + reason: reason.to_string(), + serial_number: None, + } + } +} + +impl TryFrom for RevokedKey { + type Error = Error; + + fn try_from(dto: RevokedKeyDTO) -> Result { + Ok(RevokedKey { + id: dto.id, + key_id: dto.key_id, + ca_id: dto.ca_id, + reason: X509RevokeReason::from_str(&dto.reason)?, + create_at: dto.create_at, + serial_number: dto.serial_number, + }) + } +} + + #[derive(Debug, FromRow)] -pub struct RequestDeleteDTO { +pub struct PendingOperationDTO { pub id: i32, pub user_id: i32, pub key_id: i32, + pub request_type: RequestType, pub user_email: String, pub create_at: DateTime, } -impl RequestDeleteDTO { - pub fn new(key_id: i32, user_id: i32, user_email: String) -> Self { +impl PendingOperationDTO { + pub fn new_for_delete(key_id: i32, user_id: i32, user_email: String) -> Self { + Self { + id: 0, + user_id, + key_id, + user_email, + create_at: Utc::now(), + request_type: RequestType::Delete, + } + } + + pub fn new_for_revoke(key_id: i32, user_id: i32, user_email: String) -> Self { Self { id: 0, user_id, key_id, user_email, - create_at: Utc::now() + create_at: Utc::now(), + request_type: RequestType::Revoke, } } } \ No newline at end of file diff --git a/src/infra/encryption/dummy_engine.rs b/src/infra/encryption/dummy_engine.rs new file mode 100644 index 0000000000000000000000000000000000000000..4551de540275b10a2ddffe3592110ee58ef6912c --- /dev/null +++ b/src/infra/encryption/dummy_engine.rs @@ -0,0 +1,44 @@ +/* + * // Copyright (c) 2023 Huawei Technologies Co.,Ltd. All rights reserved. + * // + * // signatrust is licensed under Mulan PSL v2. + * // You can use this software according to the terms and conditions of the Mulan + * // PSL v2. + * // You may obtain a copy of Mulan PSL v2 at: + * // http://license.coscl.org.cn/MulanPSL2 + * // THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY + * // KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + * // See the Mulan PSL v2 for more details. + */ + +use crate::domain::encryption_engine::EncryptionEngine; +use crate::util::error::Result; +use async_trait::async_trait; + + +#[derive(Default)] +pub struct DummyEngine {} + +#[async_trait] +impl EncryptionEngine for DummyEngine { + async fn initialize(&mut self) -> Result<()> { + warn!("dummy engine used for encryption, please don't use it in production environment"); + Ok(()) + } + + async fn rotate_key(&mut self) -> Result { + warn!("dummy engine used for encryption, please don't use it in production environment"); + Ok(true) + } + + async fn encode(&self, content: Vec) -> Result> { + warn!("dummy engine used for encryption, please don't use it in production environment"); + Ok(content) + } + + async fn decode(&self, content: Vec) -> Result> { + warn!("dummy engine used for encryption, please don't use it in production environment"); + Ok(content) + } +} \ No newline at end of file diff --git a/src/infra/encryption/engine.rs b/src/infra/encryption/engine.rs index 4eb603d38f2b83c53daf4ebdfbee31c676bbd75c..1857630429a3dc8cca22b81e72d107026620013d 100644 --- a/src/infra/encryption/engine.rs +++ b/src/infra/encryption/engine.rs @@ -163,6 +163,9 @@ where } async fn encode(&self, content: Vec) -> Result> { + if content.is_empty() { + return Ok(content); + } //always use latest cluster key to encode data let mut secret = self .encryptor @@ -171,6 +174,9 @@ where } async fn decode(&self, content: Vec) -> Result> { + if content.is_empty() { + return Ok(content); + } //1. obtain cluster key id from content //2. use cluster key to decrypt data let sec_cluster_key = self.get_used_sec_cluster_key(&content[0..KEY_SIZE]).await?; diff --git a/src/infra/encryption/mod.rs b/src/infra/encryption/mod.rs index 383cc5978f798043366ed3765201068dfb957024..64efc9873dd9f93c27c9cddefd5f71fbf8888871 100644 --- a/src/infra/encryption/mod.rs +++ b/src/infra/encryption/mod.rs @@ -1,2 +1,3 @@ pub mod algorithm; pub mod engine; +pub mod dummy_engine; diff --git a/src/infra/sign_backend/memory/backend.rs b/src/infra/sign_backend/memory/backend.rs index 7d261168d9e37b3ed1a40e9410a30726b05af0ce..a1b51b4bbd36f41cc3ede0c29f3652d6da192cbc 100644 --- a/src/infra/sign_backend/memory/backend.rs +++ b/src/infra/sign_backend/memory/backend.rs @@ -28,18 +28,20 @@ use crate::infra::database::pool::{DbPool}; use crate::infra::kms::factory; use crate::infra::encryption::engine::{EncryptionEngineWithClusterKey}; use crate::domain::encryption_engine::EncryptionEngine; -use crate::domain::datakey::entity::SecDataKey; +use crate::domain::datakey::entity::{INFRA_CONFIG_DOMAIN_NAME, RevokedKey, SecDataKey}; use crate::infra::sign_plugin::signers::Signers; use crate::domain::datakey::entity::DataKey; use crate::util::error::{Error, Result}; use async_trait::async_trait; +use chrono::{DateTime, Utc}; use crate::infra::encryption::algorithm::factory::AlgorithmFactory; /// Memory Sign Backend will perform all sensitive operations directly in host memory. pub struct MemorySignBackend { server_config: Arc>, - engine: Box + engine: Box, + infra_configs: HashMap, } impl MemorySignBackend { @@ -70,8 +72,13 @@ impl MemorySignBackend { )?; engine.initialize().await?; + let infra_configs = HashMap::from([ + (INFRA_CONFIG_DOMAIN_NAME.to_string(), server_config.read()?.get_string("control-server.domain_name")?), + ]); + Ok(MemorySignBackend { server_config, + infra_configs, engine: Box::new(engine), }) } @@ -90,11 +97,14 @@ impl SignBackend for MemorySignBackend { } async fn generate_keys(&self, data_key: &mut DataKey) -> Result<()> { - let keys = Signers::generate_keys(&data_key.key_type, &data_key.attributes)?; - data_key.private_key = self.engine.encode(keys.private_key).await?; - data_key.public_key = self.engine.encode(keys.public_key).await?; - data_key.certificate = self.engine.encode(keys.certificate).await?; - data_key.fingerprint = keys.fingerprint; + let sec_key = SecDataKey::load(data_key, &self.engine).await?; + let content = Signers::load_from_data_key(&data_key.key_type, sec_key)?.generate_keys( + &data_key.key_type, &self.infra_configs)?; + data_key.private_key = self.engine.encode(content.private_key).await?; + data_key.public_key = self.engine.encode(content.public_key).await?; + data_key.certificate = self.engine.encode(content.certificate).await?; + data_key.fingerprint = content.fingerprint; + data_key.serial_number = content.serial_number; Ok(()) } @@ -112,4 +122,9 @@ impl SignBackend for MemorySignBackend { data_key.certificate = self.engine.decode(data_key.certificate.clone()).await?; Ok(()) } + + async fn generate_crl_content(&self, data_key: &DataKey, revoked_keys: Vec, last_update: DateTime, next_update: DateTime) -> Result> { + let sec_key = SecDataKey::load(data_key, &self.engine).await?; + Signers::load_from_data_key(&data_key.key_type, sec_key)?.generate_crl_content(revoked_keys, last_update, next_update) + } } \ No newline at end of file diff --git a/src/infra/sign_plugin/openpgp.rs b/src/infra/sign_plugin/openpgp.rs index 0fa57ce789f9b75889b0a43c781c7d74525dbe64..ea1222dcd39fd0c50c32078d11a04d094cbd4fdb 100644 --- a/src/infra/sign_plugin/openpgp.rs +++ b/src/infra/sign_plugin/openpgp.rs @@ -35,7 +35,7 @@ use std::io::{Cursor}; use std::str::from_utf8; use validator::{Validate, ValidationError}; use pgp::composed::StandaloneSignature; -use crate::domain::datakey::entity::{DataKey, DataKeyContent, SecDataKey}; +use crate::domain::datakey::entity::{DataKey, DataKeyContent, SecDataKey, KeyType as EntityKeyType, RevokedKey}; use crate::util::key::encode_u8_to_hex_string; use super::util::{validate_utc_time_not_expire, validate_utc_time, attributes_validate}; @@ -134,10 +134,11 @@ fn validate_key_size(key_size: &str) -> std::result::Result<(), ValidationError> } pub struct OpenPGPPlugin { - secret_key: SignedSecretKey, - public_key: SignedPublicKey, + name: String, + secret_key: Option, + public_key: Option, identity: String, - attributes: HashMap + attributes: HashMap, } impl OpenPGPPlugin { @@ -153,13 +154,23 @@ impl OpenPGPPlugin { impl SignPlugins for OpenPGPPlugin { fn new(db: SecDataKey) -> Result { - let private = from_utf8(db.private_key.unsecure()).map_err(|e| Error::KeyParseError(e.to_string()))?; - let (secret_key, _) = - SignedSecretKey::from_string(private).map_err(|e| Error::KeyParseError(e.to_string()))?; - let public = from_utf8(db.public_key.unsecure()).map_err(|e| Error::KeyParseError(e.to_string()))?; - let (public_key, _) = - SignedPublicKey::from_string(public).map_err(|e| Error::KeyParseError(e.to_string()))?; + let mut secret_key = None; + let mut public_key = None; + if !db.private_key.unsecure().is_empty() { + let private = from_utf8(db.private_key.unsecure()).map_err(|e| Error::KeyParseError(e.to_string()))?; + let (value, _) = + SignedSecretKey::from_string(private).map_err(|e| Error::KeyParseError(e.to_string()))?; + secret_key = Some(value); + } + + if !db.public_key.unsecure().is_empty() { + let public = from_utf8(db.public_key.unsecure()).map_err(|e| Error::KeyParseError(e.to_string()))?; + let (value, _) = + SignedPublicKey::from_string(public).map_err(|e| Error::KeyParseError(e.to_string()))?; + public_key = Some(value); + } Ok(Self { + name: db.name.clone(), secret_key, public_key, identity: db.identity.clone(), @@ -200,10 +211,8 @@ impl SignPlugins for OpenPGPPlugin { todo!() } - fn generate_keys( - attributes: &HashMap, - ) -> Result { - let parameter = attributes_validate::(attributes)?; + fn generate_keys(&self, _key_type: &EntityKeyType, _infra_config: &HashMap) -> Result { + let parameter = attributes_validate::(&self.attributes)?; let mut key_params = SecretKeyParamsBuilder::default(); let create_at = parameter.create_at.parse()?; let expire :DateTime = parameter.expire_at.parse()?; @@ -236,6 +245,7 @@ impl SignPlugins for OpenPGPPlugin { public_key: signed_public_key.to_armored_bytes(None)?, certificate: vec![], fingerprint: encode_u8_to_hex_string(&signed_secret_key.fingerprint()), + serial_number: Some(encode_u8_to_hex_string(&signed_secret_key.fingerprint())), }) } @@ -253,22 +263,23 @@ impl SignPlugins for OpenPGPPlugin { } }; let now = Utc::now(); + let secret_key_id = self.secret_key.clone().unwrap().key_id(); let sig_cfg = SignatureConfig { version: SignatureVersion::V4, typ: SignatureType::Binary, - pub_alg: self.public_key.primary_key.algorithm(), + pub_alg: self.public_key.clone().unwrap().primary_key.algorithm(), hash_alg: digest, - issuer: Some(self.secret_key.key_id()), + issuer: Some(secret_key_id.clone()), created: Some(now), unhashed_subpackets: vec![], hashed_subpackets: vec![ Subpacket::SignatureCreationTime(now), - Subpacket::Issuer(self.secret_key.key_id()), + Subpacket::Issuer(secret_key_id), ], }; let read_cursor = Cursor::new(content); let signature_packet = sig_cfg - .sign(&self.secret_key, passwd_fn, read_cursor) + .sign(&self.secret_key.clone().unwrap(), passwd_fn, read_cursor) .map_err(|e| Error::SignError(self.identity.clone(), e.to_string()))?; @@ -285,6 +296,10 @@ impl SignPlugins for OpenPGPPlugin { .map_err(|e| Error::SignError(self.identity.clone(), e.to_string()))?; Ok(signature_bytes) } + + fn generate_crl_content(&self, _revoked_keys: Vec, _last_update: DateTime, _next_update: DateTime) -> Result> { + todo!() + } } #[cfg(test)] @@ -295,8 +310,14 @@ mod test { use secstr::SecVec; use crate::domain::datakey::entity::{KeyState, Visibility}; use crate::domain::datakey::entity::{KeyType}; + use crate::domain::encryption_engine::EncryptionEngine; + use crate::infra::encryption::dummy_engine::DummyEngine; use crate::util::options::DETACHED; + fn get_encryption_engine() -> Box { + Box::new(DummyEngine::default()) + } + fn get_default_parameter() -> HashMap { HashMap::from([ ("name".to_string(), "fake_name".to_string()), @@ -310,9 +331,9 @@ mod test { ]) } - fn get_default_datakey() -> DataKey { + fn get_default_datakey(name: Option, parameter: Option>) -> DataKey { let now = Utc::now(); - DataKey { + let mut datakey = DataKey { id: 0, name: "fake".to_string(), visibility: Visibility::Public, @@ -320,7 +341,9 @@ mod test { user: 1, attributes: get_default_parameter(), key_type: KeyType::OpenPGP, + parent_id: None, fingerprint: "".to_string(), + serial_number: None, private_key: vec![], public_key: vec![], certificate: vec![], @@ -329,7 +352,16 @@ mod test { key_state: KeyState::Enabled, user_email: None, request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + if let Some(name) = name { + datakey.name = name; } + if let Some(parameter) = parameter { + datakey.attributes = parameter; + } + datakey } #[test] @@ -406,42 +438,66 @@ mod test { attributes_validate::(¶meter).expect("valid email"); } - #[test] - fn test_generate_key_with_possible_digest_hash() { + #[tokio::test] + async fn test_generate_key_with_possible_digest_hash() { let mut parameter = get_default_parameter(); + let dummy_engine = get_encryption_engine(); //choose 3 random digest algorithm for _ in [1,2,3] { let num = rand::thread_rng().gen_range(0..VALID_DIGEST_ALGORITHM.len()); parameter.insert("digest_algorithm".to_string(), VALID_DIGEST_ALGORITHM[num].to_string()); - OpenPGPPlugin::generate_keys(¶meter).expect(format!("generate key with digest {} successfully", VALID_DIGEST_ALGORITHM[num]).as_str()); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone())), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = OpenPGPPlugin::new(sec_datakey).expect("create openpgp plugin successfully"); + plugin.generate_keys(&KeyType::OpenPGP, &HashMap::new()).expect(format!("generate key with digest {} successfully", VALID_DIGEST_ALGORITHM[num]).as_str()); } } - #[test] - fn test_generate_key_with_possible_length() { + #[tokio::test] + async fn test_generate_key_with_possible_length() { + let mut parameter = get_default_parameter(); + let dummy_engine = get_encryption_engine(); for key_size in VALID_KEY_SIZE{ - let mut parameter = get_default_parameter(); parameter.insert("key_size".to_string(), key_size.to_string()); - OpenPGPPlugin::generate_keys(¶meter).expect("generate key successfully"); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone())), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = OpenPGPPlugin::new(sec_datakey).expect("create openpgp plugin successfully"); + plugin.generate_keys(&KeyType::OpenPGP, &HashMap::new()).expect(format!("generate key with key size {} successfully", key_size).as_str()); } } - #[test] - fn test_generate_key_with_possible_key_type() { + #[tokio::test] + async fn test_generate_key_with_possible_key_type() { + let mut parameter = get_default_parameter(); + let dummy_engine = get_encryption_engine(); for key_type in VALID_KEY_TYPE{ - let mut parameter = get_default_parameter(); parameter.insert("key_type".to_string(), key_type.to_string()); - OpenPGPPlugin::generate_keys(¶meter).expect("generate key successfully"); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone())), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = OpenPGPPlugin::new(sec_datakey).expect("create openpgp plugin successfully"); + plugin.generate_keys(&KeyType::OpenPGP, &HashMap::new()).expect(format!("generate key with key type {} successfully", key_type).as_str()); } } - #[test] - fn test_generate_key_with_without_passphrase() { + #[tokio::test] + async fn test_generate_key_with_without_passphrase() { let mut parameter = get_default_parameter(); - OpenPGPPlugin::generate_keys(¶meter).expect("generate key successfully"); + let dummy_engine = get_encryption_engine(); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone())), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = OpenPGPPlugin::new(sec_datakey).expect("create openpgp plugin successfully"); + plugin.generate_keys(&KeyType::OpenPGP, &HashMap::new()).expect(format!("generate key with no passphrase successfully").as_str()); parameter.insert("passphrase".to_string(), "".to_string()); - OpenPGPPlugin::generate_keys(¶meter).expect("generate key successfully"); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone())), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = OpenPGPPlugin::new(sec_datakey).expect("create openpgp plugin successfully"); + plugin.generate_keys(&KeyType::OpenPGP, &HashMap::new()).expect(format!("generate key with passphrase successfully").as_str()); } #[test] @@ -513,25 +569,32 @@ BCQ921xH/nCtAw20cymy0Wf822PaJRZolSCm1aEDNE45UpGVu88ilLARklDXGhZZ j0AZp6WE =eDZk -----END PGP PRIVATE KEY BLOCK-----"; - let mut datakey = get_default_datakey(); + let mut datakey = get_default_datakey(None, None); datakey.public_key = public_key.as_bytes().to_vec(); datakey.private_key = private_key.as_bytes().to_vec(); OpenPGPPlugin::validate_and_update(&mut datakey).expect("validate and update should work"); assert_eq!("60780E80350801A395B1B08302A5B5FB87CD058E", datakey.fingerprint); } - #[test] - fn test_sign_with_armored_text() { + #[tokio::test] + async fn test_sign_with_armored_text() { let content = "hello world".as_bytes(); let mut parameter = get_default_parameter(); parameter.insert(DETACHED.to_string(), "true".to_string()); - let keys = OpenPGPPlugin::generate_keys(¶meter).expect("generate key successfully"); + let dummy_engine = get_encryption_engine(); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone())), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = OpenPGPPlugin::new(sec_datakey).expect("create openpgp plugin successfully"); + let keys = plugin.generate_keys(&KeyType::OpenPGP, &HashMap::new()).expect(format!("generate key successfully").as_str()); let sec_keys = SecDataKey { + name: "".to_string(), private_key: SecVec::new(keys.private_key.clone()), public_key: SecVec::new(keys.public_key.clone()), certificate: SecVec::new(keys.certificate.clone()), identity: "".to_string(), attributes: Default::default(), + parent: None, }; let instance = OpenPGPPlugin::new(sec_keys).expect("create openpgp instance successfully"); let signature = instance.sign(content.to_vec(), parameter).expect("sign successfully"); diff --git a/src/infra/sign_plugin/signers.rs b/src/infra/sign_plugin/signers.rs index 231173dc259dbe9997387ce6206715d418b3d30e..0576be696e0152d93cb847af564694c2a484f59d 100644 --- a/src/infra/sign_plugin/signers.rs +++ b/src/infra/sign_plugin/signers.rs @@ -17,9 +17,8 @@ use crate::domain::sign_plugin::SignPlugins; use crate::infra::sign_plugin::openpgp::OpenPGPPlugin; use crate::infra::sign_plugin::x509::X509Plugin; -use crate::domain::datakey::entity::{DataKey, DataKeyContent, KeyType}; +use crate::domain::datakey::entity::{DataKey, KeyType}; use crate::util::error::Result; -use std::collections::HashMap; use crate::domain::datakey::entity::SecDataKey; @@ -31,25 +30,15 @@ impl Signers { pub fn load_from_data_key(key_type: &KeyType, data_key: SecDataKey) -> Result> { match key_type { KeyType::OpenPGP => Ok(Box::new(OpenPGPPlugin::new(data_key)?)), - KeyType::X509 => Ok(Box::new(X509Plugin::new(data_key)?)), + KeyType::X509CA | KeyType::X509ICA | KeyType::X509EE => Ok(Box::new(X509Plugin::new(data_key)?)), } } - //generating new key, including private & public keys and the certificate, empty if not required. - pub fn generate_keys( - key_type: &KeyType, - value: &HashMap, - ) -> Result { - match key_type { - KeyType::OpenPGP => OpenPGPPlugin::generate_keys(value), - KeyType::X509 => X509Plugin::generate_keys(value), - } - } pub fn validate_and_update(datakey: &mut DataKey) -> Result<()> { match datakey.key_type { KeyType::OpenPGP => OpenPGPPlugin::validate_and_update(datakey), - KeyType::X509 => X509Plugin::validate_and_update(datakey), + KeyType::X509CA | KeyType::X509ICA | KeyType::X509EE => X509Plugin::validate_and_update(datakey), } } } diff --git a/src/infra/sign_plugin/x509.rs b/src/infra/sign_plugin/x509.rs index d9f6e0b418a6ba535350f1ddee70a3a5fb7b064c..ed626abf89240f0c2e5315446a90348a1556cf1b 100644 --- a/src/infra/sign_plugin/x509.rs +++ b/src/infra/sign_plugin/x509.rs @@ -19,25 +19,31 @@ use std::str::FromStr; use std::time::{SystemTime, Duration}; use chrono::{DateTime, Utc}; -use openssl::asn1::Asn1Time; +use openssl::asn1::{Asn1Integer, Asn1Time}; +use openssl::bn::{BigNum, MsbOption}; use openssl::cms::{CmsContentInfo, CMSOptions}; use openssl::dsa::Dsa; use openssl::hash::MessageDigest; +use openssl::nid::Nid; use openssl::pkcs7::{Pkcs7, Pkcs7Flags}; use openssl::pkey::{PKey, Private}; use openssl::rsa::Rsa; use openssl::stack::Stack; use openssl::x509; +use openssl::x509::extension::{AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectKeyIdentifier}; +use openssl::x509::{X509Crl, X509Extension}; use secstr::SecVec; use serde::Deserialize; +use foreign_types_shared::{ForeignType, ForeignTypeRef}; +use openssl_sys::{X509_CRL_new, X509_CRL_set_issuer_name, X509_CRL_set1_lastUpdate, X509_CRL_add0_revoked, X509_CRL_sign, X509_CRL_set1_nextUpdate, X509_REVOKED_new, X509_REVOKED_set_serialNumber, X509_REVOKED_set_revocationDate}; use validator::{Validate, ValidationError}; use crate::util::options; use crate::util::sign::SignType; -use crate::domain::datakey::entity::{DataKey, DataKeyContent, SecDataKey}; +use crate::domain::datakey::entity::{DataKey, DataKeyContent, INFRA_CONFIG_DOMAIN_NAME, KeyType, RevokedKey, SecDataKey, SecParentDateKey}; use crate::util::error::{Error, Result}; use crate::domain::sign_plugin::SignPlugins; -use crate::util::key::encode_u8_to_hex_string; +use crate::util::key::{decode_hex_string_to_u8, encode_u8_to_hex_string}; use super::util::{validate_utc_time_not_expire, validate_utc_time, attributes_validate}; const VALID_KEY_TYPE: [&str; 2] = ["rsa", "dsa"]; @@ -150,22 +156,188 @@ fn validate_x509_digest_algorithm_type(key_type: &str) -> std::result::Result<() } pub struct X509Plugin { + name: String, private_key: SecVec, public_key: SecVec, certificate: SecVec, identity: String, - attributes: HashMap + attributes: HashMap, + parent_key: Option +} + +impl X509Plugin { + + fn generate_serial_number() -> Result { + let mut serial_number = BigNum::new()?; + serial_number.rand(128, MsbOption::MAYBE_ZERO, true)?; + Ok(serial_number) + } + + fn generate_crl_endpoint(&self, name: &str, infra_config: &HashMap) -> Result{ + let domain_name = infra_config.get(INFRA_CONFIG_DOMAIN_NAME).ok_or( + Error::GeneratingKeyError(format!("{} is not configured", INFRA_CONFIG_DOMAIN_NAME)))?; + Ok(format!("URI:https://{}/api/v1/keys/{}/crl", domain_name, name)) + } + + //The openssl config for ca would be like: + // [ v3_ca ] + // basicConstraints = critical, CA:TRUE, pathlen:1 + // subjectKeyIdentifier = hash + // authorityKeyIdentifier = keyid:always, issuer:always + // keyUsage = critical, cRLSign, digitalSignature, keyCertSign + // nsCertType = objCA + // nsComment = "Signatrust Root CA" + #[allow(deprecated)] + fn generate_x509ca_keys(&self, _infra_config: &HashMap) -> Result { + let parameter = attributes_validate::(&self.attributes)?; + //generate self signed certificate + let keys = parameter.get_key()?; + let mut generator = x509::X509Builder::new()?; + let serial_number = X509Plugin::generate_serial_number()?; + generator.set_subject_name(parameter.get_subject_name()?.as_ref())?; + generator.set_issuer_name(parameter.get_subject_name()?.as_ref())?; + generator.set_pubkey(keys.as_ref())?; + generator.set_version(2)?; + generator.set_serial_number(Asn1Integer::from_bn(serial_number.as_ref())?.as_ref())?; + generator.set_not_before(Asn1Time::days_from_now(days_in_duration(¶meter.create_at)? as u32)?.as_ref())?; + generator.set_not_after(Asn1Time::days_from_now(days_in_duration(¶meter.expire_at)? as u32)?.as_ref())?; + //ca profile + generator.append_extension(BasicConstraints::new().ca().pathlen(1).critical().build()?)?; + generator.append_extension(SubjectKeyIdentifier::new().build(&generator.x509v3_context(None, None))?)?; + generator.append_extension(AuthorityKeyIdentifier::new().keyid(true).issuer(true).build(&generator.x509v3_context(None, None))?)?; + generator.append_extension(KeyUsage::new().crl_sign().digital_signature().key_cert_sign().critical().build()?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::NETSCAPE_COMMENT, "Signatrust Root CA")?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::NETSCAPE_CERT_TYPE, "objCA")?)?; + + generator.sign(keys.as_ref(), parameter.get_digest_algorithm()?)?; + let cert = generator.build(); + Ok(DataKeyContent{ + private_key: keys.private_key_to_pem_pkcs8()?, + public_key: keys.public_key_to_pem()?, + certificate: cert.to_pem()?, + fingerprint: encode_u8_to_hex_string(cert.digest( + MessageDigest::from_name("sha1").ok_or(Error::GeneratingKeyError("unable to generate digester".to_string()))?)?.as_ref()), + serial_number: Some(encode_u8_to_hex_string(&serial_number.to_vec())), + }) + } + + //The openssl config for ca would be like: + // [ v3_ica ] + // basicConstraints = critical, CA:TRUE, pathlen:0 + // subjectKeyIdentifier = hash + // authorityKeyIdentifier = keyid:always, issuer:always + // keyUsage = critical, cRLSign, digitalSignature, keyCertSign + // authorityInfoAccess = OCSP;URI:, caIssuers;URI: + // nsCertType = objCA + // nsComment = "Signatrust Intermediate CA" + #[allow(deprecated)] + fn generate_x509ica_keys(&self, infra_config: &HashMap) -> Result { + let parameter = attributes_validate::(&self.attributes)?; + //load the ca certificate and private key + if self.parent_key.is_none() { + return Err(Error::GeneratingKeyError("parent key is not provided".to_string())); + } + let ca_key = PKey::private_key_from_pem(self.parent_key.clone().unwrap().private_key.unsecure())?; + let ca_cert = x509::X509::from_pem(self.parent_key.clone().unwrap().certificate.unsecure())?; + //generate self signed certificate + let keys = parameter.get_key()?; + let mut generator = x509::X509Builder::new()?; + let serial_number = X509Plugin::generate_serial_number()?; + generator.set_subject_name(parameter.get_subject_name()?.as_ref())?; + generator.set_issuer_name(ca_cert.subject_name())?; + generator.set_pubkey(keys.as_ref())?; + generator.set_version(2)?; + generator.set_serial_number(Asn1Integer::from_bn(serial_number.as_ref())?.as_ref())?; + generator.set_not_before(Asn1Time::days_from_now(days_in_duration(¶meter.create_at)? as u32)?.as_ref())?; + generator.set_not_after(Asn1Time::days_from_now(days_in_duration(¶meter.expire_at)? as u32)?.as_ref())?; + //ca profile + generator.append_extension(BasicConstraints::new().ca().pathlen(0).critical().build()?)?; + generator.append_extension(SubjectKeyIdentifier::new().build(&generator.x509v3_context(Some(ca_cert.as_ref()), None))?)?; + generator.append_extension(AuthorityKeyIdentifier::new().keyid(true).issuer(true).build(&generator.x509v3_context(Some(ca_cert.as_ref()), None))?)?; + generator.append_extension(KeyUsage::new().crl_sign().digital_signature().key_cert_sign().critical().build()?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::CRL_DISTRIBUTION_POINTS, &self.generate_crl_endpoint(&self.parent_key.clone().unwrap().name, infra_config)?)?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::NETSCAPE_COMMENT, "Signatrust Intermediate CA")?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::NETSCAPE_CERT_TYPE, "objCA")?)?; + generator.sign(ca_key.as_ref(), parameter.get_digest_algorithm()?)?; + let cert = generator.build(); + //use parent private key to sign the certificate + Ok(DataKeyContent{ + private_key: keys.private_key_to_pem_pkcs8()?, + public_key: keys.public_key_to_pem()?, + certificate: cert.to_pem()?, + fingerprint: encode_u8_to_hex_string(cert.digest( + MessageDigest::from_name("sha1").ok_or(Error::GeneratingKeyError("unable to generate digester".to_string()))?)?.as_ref()), + serial_number: Some(encode_u8_to_hex_string(&serial_number.to_vec())), + }) + } + + //The openssl config for ee would be like: + // [ v3_ee ] + // basicConstraints = critical, CA:FALSE + // subjectKeyIdentifier = hash + // authorityKeyIdentifier = keyid:always, issuer:always + // keyUsage = critical, digitalSignature, nonRepudiation + // extendedKeyUsage = codeSigning + // authorityInfoAccess = OCSP;URI:, caIssuers;URI: + // nsCertType = objsign + // nsComment = "Signatrust Sign Certificate" + #[allow(deprecated)] + fn generate_x509ee_keys(&self, infra_config: &HashMap) -> Result { + let parameter = attributes_validate::(&self.attributes)?; + //load the ca certificate and private key + if self.parent_key.is_none() { + return Err(Error::GeneratingKeyError("parent key is not provided".to_string())); + } + let ca_key = PKey::private_key_from_pem(self.parent_key.clone().unwrap().private_key.unsecure())?; + let ca_cert = x509::X509::from_pem(self.parent_key.clone().unwrap().certificate.unsecure())?; + //generate self signed certificate + let keys = parameter.get_key()?; + let mut generator = x509::X509Builder::new()?; + let serial_number = X509Plugin::generate_serial_number()?; + generator.set_subject_name(parameter.get_subject_name()?.as_ref())?; + generator.set_issuer_name(ca_cert.subject_name())?; + generator.set_pubkey(keys.as_ref())?; + generator.set_version(2)?; + generator.set_serial_number(Asn1Integer::from_bn(serial_number.as_ref())?.as_ref())?; + generator.set_not_before(Asn1Time::days_from_now(days_in_duration(¶meter.create_at)? as u32)?.as_ref())?; + generator.set_not_after(Asn1Time::days_from_now(days_in_duration(¶meter.expire_at)? as u32)?.as_ref())?; + //ca profile + generator.append_extension(BasicConstraints::new().critical().build()?)?; + generator.append_extension(SubjectKeyIdentifier::new().build(&generator.x509v3_context(Some(ca_cert.as_ref()), None))?)?; + generator.append_extension(AuthorityKeyIdentifier::new().keyid(true).issuer(true).build(&generator.x509v3_context(Some(ca_cert.as_ref()), None))?)?; + generator.append_extension(KeyUsage::new().crl_sign().digital_signature().key_cert_sign().critical().build()?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::CRL_DISTRIBUTION_POINTS, &self.generate_crl_endpoint(&self.parent_key.clone().unwrap().name, infra_config)?)?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::NETSCAPE_COMMENT, "Signatrust Sign Certificate")?)?; + generator.append_extension(X509Extension::new_nid(None, None, Nid::NETSCAPE_CERT_TYPE, "objsign")?)?; + generator.sign(ca_key.as_ref(), parameter.get_digest_algorithm()?)?; + let cert = generator.build(); + //use parent private key to sign the certificate + Ok(DataKeyContent{ + private_key: keys.private_key_to_pem_pkcs8()?, + public_key: keys.public_key_to_pem()?, + certificate: cert.to_pem()?, + fingerprint: encode_u8_to_hex_string(cert.digest( + MessageDigest::from_name("sha1").ok_or(Error::GeneratingKeyError("unable to generate digester".to_string()))?)?.as_ref()), + serial_number: Some(encode_u8_to_hex_string(&serial_number.to_vec())), + }) + } } impl SignPlugins for X509Plugin { fn new(db: SecDataKey) -> Result { - Ok(Self { + let mut plugin = Self { + name: db.name.clone(), private_key: db.private_key.clone(), public_key: db.public_key.clone(), certificate: db.certificate.clone(), identity: db.identity.clone(), - attributes: db.attributes - }) + attributes: db.attributes, + parent_key: None, + }; + if let Some(parent) = db.parent { + plugin.parent_key = Some(parent); + } + Ok(plugin) } fn validate_and_update(key: &mut DataKey) -> Result<()> where Self: Sized { @@ -191,27 +363,13 @@ impl SignPlugins for X509Plugin { todo!() } - fn generate_keys( - attributes: &HashMap, - ) -> Result { - let parameter = attributes_validate::(attributes)?; - let keys = parameter.get_key()?; - let mut generator = x509::X509Builder::new()?; - generator.set_subject_name(parameter.get_subject_name()?.as_ref())?; - generator.set_issuer_name(parameter.get_subject_name()?.as_ref())?; - generator.set_pubkey(keys.as_ref())?; - generator.set_version(2)?; - generator.set_not_before(Asn1Time::days_from_now(days_in_duration(¶meter.create_at)? as u32)?.as_ref())?; - generator.set_not_after(Asn1Time::days_from_now(days_in_duration(¶meter.expire_at)? as u32)?.as_ref())?; - generator.sign(keys.as_ref(), parameter.get_digest_algorithm()?)?; - let cert = generator.build(); - Ok(DataKeyContent{ - private_key: keys.private_key_to_pem_pkcs8()?, - public_key: keys.public_key_to_pem()?, - certificate: cert.to_pem()?, - fingerprint: encode_u8_to_hex_string(cert.digest( - MessageDigest::from_name("sha1").ok_or(Error::GeneratingKeyError("unable to generate digester".to_string()))?)?.as_ref()) - }) + fn generate_keys(&self, key_type: &KeyType, infra_config: &HashMap) -> Result { + match key_type { + KeyType::X509CA => { self.generate_x509ca_keys(infra_config) } + KeyType::X509ICA => { self.generate_x509ica_keys(infra_config) } + KeyType::X509EE => { self.generate_x509ee_keys(infra_config) } + _ => { Err(Error::GeneratingKeyError("x509 plugin only support x509ca, x509ica and x509ee key type".to_string())) } + } } fn sign(&self, content: Vec, options: HashMap) -> Result> { @@ -256,6 +414,33 @@ impl SignPlugins for X509Plugin { } } } + + fn generate_crl_content(&self, revoked_keys: Vec, last_update: DateTime, next_update: DateTime) -> Result> { + let parameter = attributes_validate::(&self.attributes)?; + let private_key = PKey::private_key_from_pem(self.private_key.unsecure())?; + let certificate = x509::X509::from_pem(self.certificate.unsecure())?; + + //prepare raw crl content + let crl = unsafe{ X509_CRL_new() }; + let x509_name= certificate.subject_name().as_ptr(); + + unsafe { X509_CRL_set_issuer_name(crl, x509_name); }; + unsafe {X509_CRL_set1_lastUpdate(crl, Asn1Time::from_unix(last_update.naive_utc().timestamp())?.as_ptr())}; + unsafe {X509_CRL_set1_nextUpdate(crl, Asn1Time::from_unix(next_update.naive_utc().timestamp())?.as_ptr())}; + for revoked_key in revoked_keys { + //TODO: Add revoke reason here. + if let Some(serial_number) = revoked_key.serial_number { + let cert_serial = BigNum::from_slice(&decode_hex_string_to_u8(&serial_number))?; + let revoked =unsafe{X509_REVOKED_new()}; + unsafe {X509_REVOKED_set_serialNumber(revoked, Asn1Integer::from_bn(&cert_serial)?.as_ptr())}; + unsafe {X509_REVOKED_set_revocationDate(revoked, Asn1Time::from_unix(revoked_key.create_at.naive_utc().timestamp())?.as_ptr())}; + unsafe {X509_CRL_add0_revoked(crl, revoked)}; + } + } + unsafe {X509_CRL_sign(crl, private_key.as_ptr(), parameter.get_digest_algorithm()?.as_ptr())}; + let content = unsafe {X509Crl::from_ptr(crl)}; + Ok(content.to_pem()?) + } } #[cfg(test)] @@ -263,9 +448,20 @@ mod test { use super::*; use chrono::{Duration, Utc}; use secstr::SecVec; - use crate::domain::datakey::entity::{KeyState, Visibility}; + use crate::domain::datakey::entity::{KeyState, ParentKey, Visibility, X509RevokeReason}; use crate::domain::datakey::entity::{KeyType}; - use crate::util::options::{SIGN_TYPE}; + use crate::domain::encryption_engine::EncryptionEngine; + use crate::infra::encryption::dummy_engine::DummyEngine; + + fn get_infra_config() -> HashMap { + HashMap::from([ + (INFRA_CONFIG_DOMAIN_NAME.to_string(), "test.hostname".to_string()), + ]) + } + + fn get_encryption_engine() -> Box { + Box::new(DummyEngine::default()) + } fn get_default_parameter() -> HashMap { HashMap::from([ @@ -284,17 +480,19 @@ mod test { ]) } - fn get_default_datakey() -> DataKey { + fn get_default_datakey(name: Option, parameter: Option>, key_type: Option) -> DataKey { let now = Utc::now(); - DataKey { + let mut datakey = DataKey { id: 0, name: "fake".to_string(), visibility: Visibility::Public, description: "fake description".to_string(), user: 1, attributes: get_default_parameter(), - key_type: KeyType::X509, + key_type: KeyType::X509EE, + parent_id: None, fingerprint: "".to_string(), + serial_number: None, private_key: vec![], public_key: vec![], certificate: vec![], @@ -303,7 +501,19 @@ mod test { key_state: KeyState::Enabled, user_email: None, request_delete_users: None, + request_revoke_users: None, + parent_key: None, + }; + if let Some(name) = name { + datakey.name = name; + } + if let Some(parameter) = parameter { + datakey.attributes = parameter; + } + if let Some(key) = key_type { + datakey.key_type = key; } + datakey } #[test] @@ -369,41 +579,71 @@ mod test { attributes_validate::(¶meter).expect("valid expire at"); } - #[test] - fn test_generate_key_with_possible_digest_hash() { + #[tokio::test] + async fn test_generate_ca_with_possible_digest_hash() { let mut parameter = get_default_parameter(); //choose 4 random digest algorithm + let dummy_engine = get_encryption_engine(); + let infra_config = get_infra_config(); for hash in VALID_DIGEST_ALGORITHM { parameter.insert("digest_algorithm".to_string(), hash.to_string()); - X509Plugin::generate_keys(¶meter).expect(format!("generate key with digest {} successfully", hash).as_str()); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone()), Some(KeyType::X509CA)), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + plugin.generate_keys(&KeyType::X509CA, &infra_config).expect(format!("generate ca key with digest {} successfully", hash).as_str()); } } - #[test] - fn test_generate_key_with_possible_length() { + #[tokio::test] + async fn test_generate_key_with_possible_length() { + let mut parameter = get_default_parameter(); + let dummy_engine = get_encryption_engine(); + let infra_config = get_infra_config(); for key_size in VALID_KEY_SIZE{ - let mut parameter = get_default_parameter(); parameter.insert("key_size".to_string(), key_size.to_string()); - X509Plugin::generate_keys(¶meter).expect("generate key successfully"); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone()), Some(KeyType::X509CA)), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + plugin.generate_keys(&KeyType::X509CA, &infra_config).expect(format!("generate ca key with key size {} successfully", key_size).as_str()); } } - #[test] - fn test_generate_key_with_possible_key_type() { + #[tokio::test] + async fn test_generate_key_with_possible_key_type() { + let mut parameter = get_default_parameter(); + let dummy_engine = get_encryption_engine(); + let infra_config = get_infra_config(); for key_type in VALID_KEY_TYPE{ - let mut parameter = get_default_parameter(); parameter.insert("key_type".to_string(), key_type.to_string()); - X509Plugin::generate_keys(¶meter).expect("generate key successfully"); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone()), Some(KeyType::X509CA)), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + plugin.generate_keys(&KeyType::X509CA, &infra_config).expect(format!("generate ca key with key type {} successfully", key_type).as_str()); } } - #[test] - fn test_generate_key_with_without_passphrase() { + #[tokio::test] + async fn test_generate_key_with_without_passphrase() { let mut parameter = get_default_parameter(); - X509Plugin::generate_keys(¶meter).expect("generate key successfully"); + let dummy_engine = get_encryption_engine(); + let infra_config = get_infra_config(); + + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone()), Some(KeyType::X509CA)), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + plugin.generate_keys(&KeyType::X509CA, &infra_config).expect(format!("generate ca key with no passphrase successfully").as_str()); + parameter.insert("passphrase".to_string(), "".to_string()); - X509Plugin::generate_keys(¶meter).expect("generate key successfully"); + let sec_datakey = SecDataKey::load( + &get_default_datakey( + None, Some(parameter.clone()), Some(KeyType::X509CA)), &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + plugin.generate_keys(&KeyType::X509CA, &infra_config).expect(format!("generate ca key with passphrase successfully").as_str()); } #[test] @@ -464,7 +704,7 @@ UDWzemeq/e6D6NJhaESg49Da0N7rsqM+UtBpM/T4Ce9zuPZhLlXJobqmzIYqVDu0 aIVg7wz2RwVCsux1duEoO8ScQghohmzn+7jysGIlN+csOClwSBaLHAIN/PmChZug X5BboR/QJakEK+H+EUQAiDs= -----END PRIVATE KEY-----"; - let mut datakey = get_default_datakey(); + let mut datakey = get_default_datakey(None, None, None); datakey.public_key = public_key.as_bytes().to_vec(); datakey.certificate = certificate.as_bytes().to_vec(); datakey.private_key = private_key.as_bytes().to_vec(); @@ -473,20 +713,104 @@ X5BboR/QJakEK+H+EUQAiDs= assert_eq!("C9345187DFA0BFB6DCBCC4827BBEA7312E43754B", datakey.fingerprint); } - #[test] - fn test_sign_successful() { + #[tokio::test] + async fn test_sign_whole_process_successful() { + let parameter = get_default_parameter(); + let dummy_engine = get_encryption_engine(); + let infra_config = get_infra_config(); let content = "hello world".as_bytes(); - let mut parameter = get_default_parameter(); - parameter.insert(SIGN_TYPE.to_string(), SignType::Cms.to_string()); - let keys = X509Plugin::generate_keys(¶meter).expect("generate key successfully"); + // create ca + let ca_key = get_default_datakey( + Some("fake ca".to_string()), Some(parameter.clone()), Some(KeyType::X509CA)); + let sec_datakey = SecDataKey::load( + &ca_key, &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + let ca_content = plugin.generate_keys(&KeyType::X509CA, &infra_config).expect(format!("generate ca key with no passphrase successfully").as_str()); + + // create ica + let mut ica_key = get_default_datakey( + Some("fake ica".to_string()), Some(parameter.clone()), Some(KeyType::X509CA)); + ica_key.parent_key = Some(ParentKey{ + name: "fake ca".to_string(), + private_key: ca_content.private_key, + public_key: ca_content.public_key, + certificate: ca_content.certificate, + attributes: ca_key.attributes.clone(), + }); + let sec_datakey = SecDataKey::load( + &ica_key, &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + let ica_content = plugin.generate_keys(&KeyType::X509ICA, &infra_config).expect(format!("generate ica key with no passphrase successfully").as_str()); + + //create ee + let mut ee_key = get_default_datakey( + Some("fake ee".to_string()), Some(parameter.clone()), Some(KeyType::X509CA)); + ee_key.parent_key = Some(ParentKey{ + name: "fake ca".to_string(), + private_key: ica_content.private_key, + public_key: ica_content.public_key, + certificate: ica_content.certificate, + attributes: ica_key.attributes.clone(), + }); + let sec_datakey = SecDataKey::load( + &ica_key, &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + let ee_content = plugin.generate_keys(&KeyType::X509EE, &infra_config).expect(format!("generate ee key with no passphrase successfully").as_str()); + let sec_keys = SecDataKey { - private_key: SecVec::new(keys.private_key.clone()), - public_key: SecVec::new(keys.public_key.clone()), - certificate: SecVec::new(keys.certificate.clone()), + name: "".to_string(), + private_key: SecVec::new(ee_content.private_key.clone()), + public_key: SecVec::new(ee_content.public_key.clone()), + certificate: SecVec::new(ee_content.certificate.clone()), identity: "".to_string(), attributes: Default::default(), + parent: None, }; let instance = X509Plugin::new(sec_keys).expect("create x509 instance successfully"); let _signature = instance.sign(content.to_vec(), parameter).expect("sign successfully"); } + + #[tokio::test] + async fn test_crl_generation() { + let parameter = get_default_parameter(); + let dummy_engine = get_encryption_engine(); + let infra_config = get_infra_config(); + // create ca + let mut ca_key = get_default_datakey( + Some("fake ca".to_string()), Some(parameter.clone()), Some(KeyType::X509CA)); + let sec_datakey = SecDataKey::load( + &ca_key, &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(sec_datakey).expect("create plugin successfully"); + let ca_content = plugin.generate_keys(&KeyType::X509CA, &infra_config).expect(format!("generate ca key with no passphrase successfully").as_str()); + ca_key.private_key = ca_content.private_key; + ca_key.public_key = ca_content.public_key; + ca_key.certificate = ca_content.certificate; + ca_key.serial_number = ca_content.serial_number; + ca_key.fingerprint = ca_content.fingerprint; + let crl_sec_datakey = SecDataKey::load( + &ca_key, &dummy_engine).await.expect("load sec datakey successfully"); + let plugin = X509Plugin::new(crl_sec_datakey).expect("create plugin successfully"); + let revoke_time = Utc::now(); + let last_update = Utc::now() + Duration::days(1); + let next_update = Utc::now() + Duration::days(2); + let serial_number = X509Plugin::generate_serial_number().expect("generate serial number successfully"); + let revoked_keys = RevokedKey{ + id: 0, + key_id: 0, + ca_id: 0, + reason: X509RevokeReason::Unspecified, + create_at: revoke_time.clone(), + serial_number: Some(encode_u8_to_hex_string(&serial_number.to_vec())) + }; + //generate crl + let content = plugin.generate_crl_content(vec![revoked_keys], last_update.clone(), next_update.clone()).expect("generate crl successfully"); + let crl = X509Crl::from_pem(&content).expect("load generated crl successfully"); + assert_eq!(crl.last_update()==Asn1Time::from_unix(last_update.naive_utc().timestamp()).expect("convert to asn1 time successfully"), true); + assert_eq!(crl.next_update().expect("next update is set")==Asn1Time::from_unix(next_update.naive_utc().timestamp()).expect("convert to asn1 time successfully"), true); + assert_eq!(crl.get_revoked().is_some(), true); + let revoked = crl.get_revoked().expect("revoke stack is not empty").get(0).expect("first revoke is not empty"); + assert_eq!(revoked.serial_number().to_owned().expect("convert to asn1 number work") == Asn1Integer::from_bn(&serial_number).expect("convert from bn number should work"), true); + assert_eq!(revoked.revocation_date().to_owned()==Asn1Time::from_unix(revoke_time.naive_utc().timestamp()).expect("convert to asn1 time successfully"), true); + + } } diff --git a/src/presentation/handler/control/datakey_handler.rs b/src/presentation/handler/control/datakey_handler.rs index ef1ca0ec1c1d8a8eb3b969cf94075e66b097e656..61517157505939eeb61b7bb8d76c4c37212bf89d 100644 --- a/src/presentation/handler/control/datakey_handler.rs +++ b/src/presentation/handler/control/datakey_handler.rs @@ -20,19 +20,18 @@ use actix_web::{ }; -use crate::presentation::handler::control::model::datakey::dto::{CreateDataKeyDTO, DataKeyDTO, ExportKey, ImportDataKeyDTO, KeyQuery, NameIdenticalQuery}; +use crate::presentation::handler::control::model::datakey::dto::{CertificateContent, CreateDataKeyDTO, CRLContent, DataKeyDTO, ImportDataKeyDTO, ListKeyQuery, NameIdenticalQuery, PublicKeyContent, RevokeCertificateDTO}; use crate::util::error::Error; use validator::Validate; use crate::application::datakey::KeyService; -use crate::domain::datakey::entity::{DataKey, Visibility}; +use crate::domain::datakey::entity::{DataKey, KeyType, X509RevokeReason}; use super::model::user::dto::UserIdentity; /// Create new key /// -/// This will generate either a pgp private/public key pairs or a x509 private/public/cert keys. +/// This will generate either a pgp key pairs or a x509 private/public/cert keys. /// ## Naming convention -/// The name of the key should be unique, and if you want to create a private key, the name will be prefixed with your email address automatically, -/// for example you will get `youremail@address.com:some-private-key-name` when you specify the private key named `some-private-key-name`. +/// The name of the key should be unique. /// ## Generate pgp key /// To generate a pgp key the required parameters in `attributes` are: /// 1. **digest_algorithm**: the digest algorithm used for pgp, for example: sha2_256 @@ -46,7 +45,6 @@ use super::model::user::dto::UserIdentity; /// "name": "test-pgp", /// "description": "hello world", /// "key_type": "pgp", -/// "visibility": "public", /// "attributes": { /// "digest_algorithm": "sha2_256", /// "key_type": "rsa", @@ -70,13 +68,19 @@ use super::model::user::dto::UserIdentity; /// 7. **organization**: organization (organizationName, O), used for certificate. /// 8. **organizational_unit**: organizational unit (organizationalUnitName, OU), used for certificate. /// 9. **province_name**: state or province name (stateOrProvinceName, ST), used for certificate. +/// +/// There are three different keys regarding X509, they are: +/// 1. X509CA: Root CA key, used for issue intermediate CA certificate. +/// 2. X509ICA: Intermediate CA key, used for issue end entity certificate. +/// 3. X509EE: End entity key, used for sign object. +/// You have to specify the parent_id: when you create a X509ICA or X509EE key. /// ### Request body example: /// ```json /// { /// "name": "test-x509", /// "description": "hello world", -/// "key_type": "x509", -/// "visibility": "public", +/// "key_type": "x509CA", +/// "parent_id": "1111", /// "attributes": { /// "digest_algorithm": "sha2_256", /// "key_type": "rsa", @@ -128,7 +132,7 @@ async fn create_data_key(user: UserIdentity, key_service: web::Data, key_query: web::Query) -> Result { - let key_visibility = Visibility::from_str(key_query.visibility.as_str())?; - let keys = key_service.into_inner().get_by_visibility(Some(user), key_visibility).await?; +async fn list_data_key(_user: UserIdentity, key_service: web::Data, key: web::Query) -> Result { + let key_type = match key.key_type { + Some(ref k) => Some(KeyType::from_str(k)?), + None => None, + }; + let keys = key_service.into_inner().get_all(key_type).await?; let mut results = vec![]; for k in keys { results.push(DataKeyDTO::try_from(k)?) @@ -149,18 +156,18 @@ async fn list_data_key(user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { - let key = key_service.into_inner().get_one(Some(user), id.parse::()?).await?; +async fn show_data_key(user: UserIdentity, key_service: web::Data, id_or_name: web::Path) -> Result { + let key = key_service.into_inner().get_one(Some(user), id_or_name.into_inner()).await?; Ok(HttpResponse::Ok().json(DataKeyDTO::try_from(key)?)) } -/// Delete specific key by id from database, only **disabled** key can be deleted. +/// Delete specific key by id or name from database /// +/// only **disabled** key can be deleted. /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X POST https://domain:port/api/v1/keys/{id}/request_delete +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/actions/request_delete /// ``` #[utoipa::path( post, - path = "/api/v1/keys/{id}/request_delete", + path = "/api/v1/keys/{id_or_name}/actions/request_delete", params( - ("id" = i32, Path, description = "Key id"), + ("id_or_name" = String, Path, description = "Key id or key name"), ), security( ("Authorization" = []) @@ -204,27 +212,28 @@ async fn show_data_key(user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { - key_service.into_inner().request_delete(user, id.parse::()?).await?; +async fn delete_data_key(user: UserIdentity, key_service: web::Data, id_or_name: web::Path) -> Result { + key_service.into_inner().request_delete(user, id_or_name.into_inner()).await?; Ok(HttpResponse::Ok()) } -/// Cancel deletion of specific key by id from database, it only works for **public key**. +/// Cancel deletion of specific key by id or name from database /// +/// only **pending_delete** key can be canceled. /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X POST https://domain:port/api/v1/keys/{id}/cancel_delete +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/actions/cancel_delete /// ``` #[utoipa::path( -post, -path = "/api/v1/keys/{id}/cancel_delete", + post, + path = "/api/v1/keys/{id_or_name}/actions/cancel_delete", params( - ("id" = i32, Path, description = "Key id"), - ), + ("id_or_name" = String, Path, description = "Key id or key name"), + ), security( ("Authorization" = []) - ), + ), responses( (status = 200, description = "Key deletion canceled successfully"), (status = 400, description = "Bad request", body = ErrorMessage), @@ -232,31 +241,92 @@ path = "/api/v1/keys/{id}/cancel_delete", (status = 403, description = "Forbidden", body = ErrorMessage), (status = 404, description = "Key not found", body = ErrorMessage), (status = 500, description = "Server internal error", body = ErrorMessage) -) + ) )] -async fn cancel_delete_data_key(user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { - key_service.into_inner().cancel_delete(user, id.parse::()?).await?; +async fn cancel_delete_data_key(user: UserIdentity, key_service: web::Data, id_or_name: web::Path) -> Result { + key_service.into_inner().cancel_delete(user, id_or_name.into_inner()).await?; Ok(HttpResponse::Ok()) } -/// Export content of specific key +/// Revoke a certificate by id or name from database. /// +/// only **disabled** or **pending_revoke** and X509EE/X509ICA key can be revoked. /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X POST https://domain:port/api/v1/keys/{id}/export +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/actions/request_revoke /// ``` #[utoipa::path( post, - path = "/api/v1/keys/{id}/export", + path = "/api/v1/keys/{id_or_name}/actions/request_revoke", params( - ("id" = i32, Path, description = "Key id"), + ("id_or_name" = String, Path, description = "Key id or key name"), ), + request_body = RevokeCertificateDTO, security( ("Authorization" = []) ), responses( - (status = 200, description = "Key successfully exported", body = ExportKey), + (status = 200, description = "Key successfully revoked"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 403, description = "Forbidden", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] +async fn revoke_data_key(user: UserIdentity, key_service: web::Data, id_or_name: web::Path, reason: web::Json) -> Result { + key_service.into_inner().request_revoke(user, id_or_name.into_inner(), X509RevokeReason::from_str(&reason.reason)?).await?; + Ok(HttpResponse::Ok()) +} + +/// Cancel revoke a certificate by id or name from database. +/// +/// only **pending_revoke** and X509EE/X509ICA key can be cancel revoked. +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/actions/cancel_revoke +/// ``` +#[utoipa::path( + post, + path = "/api/v1/keys/{id_or_name}/actions/cancel_revoke", + params( + ("id_or_name" = String, Path, description = "Key id or key name"), + ), + security( + ("Authorization" = []) + ), + responses( + (status = 200, description = "Key successfully deleted"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 403, description = "Forbidden", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] +async fn cancel_revoke_data_key(user: UserIdentity, key_service: web::Data, id_or_name: web::Path) -> Result { + key_service.into_inner().cancel_revoke(user, id_or_name.into_inner()).await?; + Ok(HttpResponse::Ok()) +} + +/// Get public key content of specific key by id or name from database +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/public_key +/// ``` +/// ## Note: this endpoint is public for public keys, for private keys, it requires authentication. +#[utoipa::path( + post, + path = "/api/v1/keys/{id_or_name}/public_key", + params( + ("id_or_name" = String, Path, description = "Key id or key name"), + ), + responses( + (status = 200, description = "Key successfully exported",), (status = 400, description = "Bad request", body = ErrorMessage), (status = 401, description = "Unauthorized", body = ErrorMessage), (status = 403, description = "Forbidden", body = ErrorMessage), @@ -264,22 +334,87 @@ async fn cancel_delete_data_key(user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { - Ok(HttpResponse::Ok().json(ExportKey::try_from(key_service.export_one(Some(user), id.parse::()?).await?)?)) +async fn export_public_key(user: Option, key_service: web::Data, id_or_name: web::Path) -> Result { + let data_key = key_service.export_one(user, id_or_name.into_inner()).await?; + if data_key.key_type != KeyType::OpenPGP { + return Ok(HttpResponse::Forbidden().finish()) + } + Ok(HttpResponse::Ok().content_type("text/plain").body(PublicKeyContent::try_from(data_key)?.content)) } -/// Enable specific key +/// Get certificate content of specific key by id or name from database +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/certificate +/// ``` +/// ## Note: this endpoint is public for public keys, for private keys, it requires authentication. +#[utoipa::path( + post, + path = "/api/v1/keys/{id_or_name}/certificate", + params( + ("id_or_name" = String, Path, description = "Key id or key name"), + ), + responses( + (status = 200, description = "Key successfully exported"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 403, description = "Forbidden", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] +async fn export_certificate(user: Option, key_service: web::Data, id_or_name: web::Path) -> Result { + let data_key = key_service.export_one(user, id_or_name.into_inner()).await?; + if data_key.key_type == KeyType::OpenPGP { + return Ok(HttpResponse::Forbidden().finish()) + } + Ok(HttpResponse::Ok().content_type("text/plain").body(CertificateContent::try_from(data_key)?.content)) +} + +/// Get Client Revoke List content of specific key(cert) by id or name from database +/// +/// ## Example +/// Call the api endpoint with following curl. +/// ```text +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/crl +/// ``` +/// ## Note: this endpoint is public for public keys, for private keys, it requires authentication. +#[utoipa::path( + post, + path = "/api/v1/keys/{id_or_name}/crl", + params( + ("id_or_name" = String, Path, description = "Key id or key name"), + ), + responses( + (status = 200, description = "Key successfully exported"), + (status = 400, description = "Bad request", body = ErrorMessage), + (status = 401, description = "Unauthorized", body = ErrorMessage), + (status = 403, description = "Forbidden", body = ErrorMessage), + (status = 404, description = "Key not found", body = ErrorMessage), + (status = 500, description = "Server internal error", body = ErrorMessage) + ) +)] +async fn export_crl(user: Option, key_service: web::Data, id_or_name: web::Path) -> Result { + //note: we could not get any crl content by a openpgp id. + let crl_content = key_service.export_cert_crl(user, id_or_name.into_inner()).await?; + Ok(HttpResponse::Ok().content_type("text/plain").body(CRLContent::try_from(crl_content)?.content)) +} + + +/// Enable specific key by id or name from database /// /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X POST https://domain:port/api/v1/keys/{id}/enable +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/actions/enable /// ``` #[utoipa::path( post, - path = "/api/v1/keys/{id}/enable", + path = "/api/v1/keys/{id_or_name}/actions/enable", params( - ("id" = i32, Path, description = "Key id"), + ("id_or_name" = String, Path, description = "Key id or key name"), ), security( ("Authorization" = []) @@ -293,23 +428,23 @@ async fn export_data_key(user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { - key_service.enable(Some(user), id.parse::()?).await?; +async fn enable_data_key(user: UserIdentity, key_service: web::Data, id_or_name: web::Path) -> Result { + key_service.enable(Some(user), id_or_name.into_inner()).await?; Ok(HttpResponse::Ok()) } -/// Disable specific key +/// Disable specific key by id or name from database /// /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X POST https://domain:port/api/v1/keys/{id}/disable +/// curl -X POST https://domain:port/api/v1/keys/{id_or_name}/actions/disable /// ``` #[utoipa::path( post, - path = "/api/v1/keys/{id}/disable", + path = "/api/v1/keys/{id_or_name}/actions/disable", params( - ("id" = i32, Path, description = "Key id"), + ("id_or_name" = String, Path, description = "Key id or key name"), ), security( ("Authorization" = []) @@ -323,19 +458,19 @@ async fn enable_data_key(user: UserIdentity, key_service: web::Data, id: web::Path) -> Result { - key_service.disable(Some(user), id.parse::()?).await?; +async fn disable_data_key(user: UserIdentity, key_service: web::Data, id_or_name: web::Path) -> Result { + key_service.disable(Some(user), id_or_name.into_inner()).await?; Ok(HttpResponse::Ok()) } /// Check whether a key name already exists /// /// Use this API to check whether the key name exists in database. -/// `name` and `visibility` are required +/// `name` are required /// ## Example /// Call the api endpoint with following curl. /// ```text -/// curl -X HEAD https://domain:port/api/v1/keys/name_identical?name=xxx&visibility=xxx +/// curl -X HEAD https://domain:port/api/v1/keys/name_identical?name=xxx /// ``` #[utoipa::path( head, @@ -352,11 +487,11 @@ async fn disable_data_key(user: UserIdentity, key_service: web::Data, name_exist: web::Query,) -> Result { +async fn key_name_identical(_user: UserIdentity, key_service: web::Data, name_exist: web::Query,) -> Result { name_exist.validate()?; - match key_service.into_inner().key_name_exists(&name_exist.get_key_name(&user)).await? { - true => Ok(HttpResponse::Conflict()), - false => Ok(HttpResponse::Ok()), + match key_service.into_inner().get_by_name(&name_exist.name.clone()).await { + Ok(_) => Ok(HttpResponse::Conflict()), + Err(_) => Ok(HttpResponse::Ok()), } } @@ -425,7 +560,7 @@ async fn key_name_identical(user: UserIdentity, key_service: web::Data, datakey: web::Json,) -> Result { +async fn import_data_key(user: UserIdentity, key_service: web::Data, datakey: web::Json) -> Result { datakey.validate()?; let mut key = DataKey::import_from(datakey.0, user)?; Ok(HttpResponse::Created().json(DataKeyDTO::try_from(key_service.into_inner().import(&mut key).await?)?)) @@ -440,10 +575,14 @@ pub fn get_scope() -> Scope { .route(web::post().to(create_data_key))) .service( web::resource("/import").route(web::post().to(import_data_key))) .service( web::resource("/name_identical").route(web::head().to(key_name_identical))) - .service( web::resource("/{id}").route(web::get().to(show_data_key))) - .service( web::resource("/{id}/export").route(web::post().to(export_data_key))) - .service( web::resource("/{id}/enable").route(web::post().to(enable_data_key))) - .service( web::resource("/{id}/disable").route(web::post().to(disable_data_key))) - .service( web::resource("/{id}/request_delete").route(web::post().to(delete_data_key))) - .service( web::resource("/{id}/cancel_delete").route(web::post().to(cancel_delete_data_key))) + .service( web::resource("/{id_or_name}").route(web::get().to(show_data_key))) + .service( web::resource("/{id_or_name}/public_key").route(web::post().to(export_public_key))) + .service( web::resource("/{id_or_name}/certificate").route(web::post().to(export_certificate))) + .service( web::resource("/{id_or_name}/crl").route(web::post().to(export_crl))) + .service( web::resource("/{id_or_name}/actions/enable").route(web::post().to(enable_data_key))) + .service( web::resource("/{id_or_name}/actions/disable").route(web::post().to(disable_data_key))) + .service( web::resource("/{id_or_name}/actions/request_delete").route(web::post().to(delete_data_key))) + .service( web::resource("/{id_or_name}/actions/cancel_delete").route(web::post().to(cancel_delete_data_key))) + .service( web::resource("/{id_or_name}/actions/request_revoke").route(web::post().to(revoke_data_key))) + .service( web::resource("/{id_or_name}/actions/cancel_revoke").route(web::post().to(cancel_revoke_data_key))) } diff --git a/src/presentation/handler/control/model/datakey/dto.rs b/src/presentation/handler/control/model/datakey/dto.rs index 74be5af8fd9a2e7b01f8ae3637138ee5da58ea63..fb260ca9eed80d2e7daf8d0c03146b645d9933b0 100644 --- a/src/presentation/handler/control/model/datakey/dto.rs +++ b/src/presentation/handler/control/model/datakey/dto.rs @@ -1,4 +1,4 @@ -use crate::domain::datakey::entity::{DataKey, KeyState, Visibility}; +use crate::domain::datakey::entity::{DataKey, KeyState, Visibility, X509CRL}; use crate::domain::datakey::entity::KeyType; use crate::util::error::Result; use chrono::{DateTime, Utc}; @@ -13,50 +13,65 @@ use utoipa::{IntoParams, ToSchema}; use crate::presentation::handler::control::model::user::dto::UserIdentity; #[derive(Deserialize, Serialize, ToSchema)] -pub struct ExportKey { - pub public_key: String, - pub certificate: String, +pub struct PublicKeyContent { + pub(crate) content: String, } -impl TryFrom for ExportKey { +impl TryFrom for PublicKeyContent { type Error = Error; fn try_from(value: DataKey) -> std::result::Result { - Ok(ExportKey{ - public_key: String::from_utf8_lossy(&value.public_key).to_string(), - certificate: String::from_utf8_lossy(&value.certificate).to_string() + Ok(PublicKeyContent{ + content: String::from_utf8_lossy(&value.public_key).to_string(), }) } } -#[derive(Deserialize, IntoParams, Validate, ToSchema)] -pub struct KeyQuery { - /// The key's visibility - #[validate(custom = "validate_key_visibility")] - pub visibility: String, +#[derive(Deserialize, Serialize, ToSchema)] +pub struct CertificateContent { + pub(crate) content: String, +} + +impl TryFrom for CertificateContent { + type Error = Error; + + fn try_from(value: DataKey) -> std::result::Result { + Ok(CertificateContent{ + content: String::from_utf8_lossy(&value.certificate).to_string(), + }) + } +} + +#[derive(Deserialize, Serialize, ToSchema)] +pub struct CRLContent { + pub(crate) content: String, +} + +impl TryFrom for CRLContent { + type Error = Error; + + fn try_from(value: X509CRL) -> std::result::Result { + Ok(CRLContent{ + content: String::from_utf8_lossy(&value.data).to_string(), + }) + } } #[derive(Deserialize, IntoParams, Validate, ToSchema)] pub struct NameIdenticalQuery { - /// The key's visibility - #[validate(custom = "validate_key_visibility")] - pub visibility: String, /// Key Name, should be identical, length between 4 and 20, not contains any colon symbol. #[validate(length(min = 4, max = 20), custom = "validate_invalid_character")] pub name: String, } -impl NameIdenticalQuery { - pub fn get_key_name(&self, user_id: &UserIdentity) -> String { - if self.visibility == Visibility::Public.to_string() { - self.name.clone() - } else { - format!("{}:{}", user_id.email, self.name) - } - - } +#[derive(Deserialize, IntoParams, Validate, ToSchema)] +pub struct ListKeyQuery { + /// Key type, optional, should be one of x509ca, x509ica, x509ee, or pgp + #[validate(custom = "validate_key_type")] + pub key_type: Option, } + #[derive(Debug, Validate, Deserialize, ToSchema)] pub struct CreateDataKeyDTO { /// Key Name, should be identical, length between 4 and 20, not contains any colon symbol. @@ -65,15 +80,13 @@ pub struct CreateDataKeyDTO { /// Description, length between 0 and 100 #[validate(length(min = 0, max = 100))] pub description: String, - /// The key's visibility - #[validate(custom = "validate_key_visibility")] - pub visibility: String, /// Attributes in map #[serde(serialize_with = "sorted_map")] pub attributes: HashMap, /// Key type current support pgp and x509 #[validate(custom = "validate_key_type")] pub key_type: String, + pub parent_id: Option, /// Expire utc time, format: 2023-04-08 13:36:35.328324 UTC #[validate(custom = "validate_utc_time")] pub expire_at: String, @@ -87,9 +100,6 @@ pub struct ImportDataKeyDTO { /// Description, length between 0 and 100 #[validate(length(min = 0, max = 100))] pub description: String, - /// The key's visibility - #[validate(custom = "validate_key_visibility")] - pub visibility: String, /// Attributes in map pub attributes: HashMap, /// Key type current support pgp and x509 @@ -103,6 +113,12 @@ pub struct ImportDataKeyDTO { pub certificate: String, } +#[derive(Debug, Validate, Deserialize, ToSchema)] +pub struct RevokeCertificateDTO { + /// Revoke reason + pub reason: String, +} + #[derive(Debug, Validate, Serialize, ToSchema)] pub struct DataKeyDTO { /// Key ID @@ -114,7 +130,6 @@ pub struct DataKeyDTO { #[validate(length(min = 0, max = 100))] pub description: String, /// The key's visibility - #[validate(custom = "validate_key_visibility")] pub visibility: String, /// User ID pub user: i32, @@ -123,8 +138,12 @@ pub struct DataKeyDTO { pub attributes: HashMap, /// Key type pub key_type: String, + /// parent id, used for x509ica and x509ee + pub parent_id: Option, /// Fingerprint pub fingerprint: String, + /// Serial number + pub serial_number: Option, /// Create utc time, format: 2023-04-08 13:36:35.328324 UTC pub create_at: String, /// Expire utc time, format: 2023-04-08 13:36:35.328324 UTC @@ -133,8 +152,10 @@ pub struct DataKeyDTO { pub key_state: String, /// User email pub user_email: Option, - /// Request user email list, only for public key + /// Request delete user email list, only for public key pub request_delete_users: Option, + /// Request revoke user email list, only for public key + pub request_revoke_users: Option, } fn validate_utc_time(expire: &str) -> std::result::Result<(), ValidationError> { @@ -155,17 +176,6 @@ fn validate_key_type(key_type: &str) -> std::result::Result<(), ValidationError> } } -fn validate_key_visibility(visibility: &str) -> std::result::Result<(), ValidationError> { - match Visibility::from_str(visibility) { - Ok(_) => { - Ok(()) - } - Err(_) => { - Err(ValidationError::new("unsupported key visibility")) - } - } -} - fn validate_invalid_character(name: &str) -> std::result::Result<(), ValidationError> { if name.contains(':') { return Err(ValidationError::new("invalid character(':') in name")); @@ -179,20 +189,17 @@ impl DataKey { let mut combined_attributes = dto.attributes.clone(); combined_attributes.insert("name".to_string(), dto.name.clone()); combined_attributes.insert("create_at".to_string(), now.clone().to_string()); - let visibility = Visibility::from_str(dto.visibility.as_str())?; - let mut key_name = dto.name; - if visibility == Visibility::Private { - key_name = format!("{}:{}", identity.email, key_name); - } Ok(DataKey { id: 0, - name: key_name, - visibility: Visibility::from_str(dto.visibility.as_str())?, + name: dto.name, + visibility: Visibility::Public, description: dto.description, user: identity.id, attributes: combined_attributes, key_type: KeyType::from_str(dto.key_type.as_str())?, + parent_id: None, fingerprint: "".to_string(), + serial_number: None, private_key: dto.private_key.into_bytes(), public_key: dto.public_key.into_bytes(), certificate: dto.certificate.into_bytes(), @@ -201,6 +208,8 @@ impl DataKey { key_state: KeyState::default(), user_email: None, request_delete_users: None, + request_revoke_users: None, + parent_key: None, }) } @@ -210,20 +219,17 @@ impl DataKey { combined_attributes.insert("name".to_string(), dto.name.clone()); combined_attributes.insert("create_at".to_string(), now.clone().to_string()); combined_attributes.insert("expire_at".to_string(), dto.expire_at.clone()); - let visibility = Visibility::from_str(dto.visibility.as_str())?; - let mut key_name = dto.name; - if visibility == Visibility::Private { - key_name = format!("{}:{}", identity.email, key_name); - } Ok(DataKey { id: 0, - name: key_name, - visibility, + name: dto.name, + visibility: Visibility::Public, description: dto.description, user: identity.id, attributes: combined_attributes, key_type: KeyType::from_str(dto.key_type.as_str())?, + parent_id: dto.parent_id, fingerprint: "".to_string(), + serial_number: None, private_key: vec![], public_key: vec![], certificate: vec![], @@ -232,6 +238,8 @@ impl DataKey { key_state: KeyState::default(), user_email: None, request_delete_users: None, + request_revoke_users: None, + parent_key: None, }) } } @@ -250,12 +258,15 @@ impl TryFrom for DataKeyDTO { user: dto.user, attributes, key_type: dto.key_type.to_string(), + parent_id: dto.parent_id, fingerprint: dto.fingerprint, + serial_number: dto.serial_number, create_at: dto.create_at.to_string(), expire_at: dto.expire_at.to_string(), key_state: dto.key_state.to_string(), user_email: dto.user_email, request_delete_users: dto.request_delete_users, + request_revoke_users: dto.request_revoke_users, }) } } diff --git a/src/presentation/handler/data/sign_handler.rs b/src/presentation/handler/data/sign_handler.rs index 39ebb93a0f7afa59b97e1ad87543589f09b83386..b68574b023674f3382ade6b9a1655562476ea4fa 100644 --- a/src/presentation/handler/data/sign_handler.rs +++ b/src/presentation/handler/data/sign_handler.rs @@ -28,8 +28,6 @@ use signatrust::{ use tonic::{Request, Response, Status, Streaming}; use crate::application::datakey::KeyService; use crate::application::user::UserService; -use crate::util::error::Error; -use crate::util::error::Result as SignatrustResult; pub struct SignHandler where @@ -51,17 +49,6 @@ where user_service } } - - async fn validate_private_key_token(&self, token: &str, name: &str) -> SignatrustResult<()> { - let names: Vec<_> = name.split(':').collect(); - if names.len() <= 1 { - return Ok(()) - } - if token.is_empty() || !self.user_service.validate_token_and_email(names[0], token).await? { - return Err(Error::AuthError("user token and email unmatched".to_string())) - } - Ok(()) - } } #[tonic::async_trait] @@ -78,7 +65,6 @@ where let mut data: Vec = vec![]; let mut key_name: String = "".to_string(); let mut key_type: String = "".to_string(); - let mut token: String = "".to_string(); let mut options: HashMap = HashMap::new(); while let Some(content) = binaries.next().await { let mut inner_result = content.unwrap(); @@ -86,16 +72,8 @@ where key_name = inner_result.key_id; key_type = inner_result.key_type; options = inner_result.options; - token = inner_result.token; } debug!("begin to sign key_type :{} key_name: {}", key_type, key_name); - //perform token validation on private keys - if let Err(err) = self.validate_private_key_token(&token, &key_name).await { - return Ok(Response::new(SignStreamResponse { - signature: vec![], - error: err.to_string(), - })) - } match self.key_service.sign(key_type, key_name, &options, data).await { Ok(content) => { Ok(Response::new(SignStreamResponse { diff --git a/src/presentation/server/control_server.rs b/src/presentation/server/control_server.rs index be7ba1db64290a1efb1cca6eab357bbc48b0d04d..120b631e8cb756b2518bd662418315772d3924f9 100644 --- a/src/presentation/server/control_server.rs +++ b/src/presentation/server/control_server.rs @@ -75,7 +75,11 @@ impl Modify for SecurityAddon { crate::presentation::handler::control::datakey_handler::create_data_key, crate::presentation::handler::control::datakey_handler::delete_data_key, crate::presentation::handler::control::datakey_handler::cancel_delete_data_key, - crate::presentation::handler::control::datakey_handler::export_data_key, + crate::presentation::handler::control::datakey_handler::revoke_data_key, + crate::presentation::handler::control::datakey_handler::cancel_revoke_data_key, + crate::presentation::handler::control::datakey_handler::export_public_key, + crate::presentation::handler::control::datakey_handler::export_certificate, + crate::presentation::handler::control::datakey_handler::export_crl, crate::presentation::handler::control::datakey_handler::enable_data_key, crate::presentation::handler::control::datakey_handler::disable_data_key, crate::presentation::handler::control::datakey_handler::import_data_key, @@ -93,9 +97,9 @@ impl Modify for SecurityAddon { schemas(crate::presentation::handler::control::model::datakey::dto::DataKeyDTO, crate::presentation::handler::control::model::datakey::dto::CreateDataKeyDTO, crate::presentation::handler::control::model::datakey::dto::ImportDataKeyDTO, - crate::presentation::handler::control::model::datakey::dto::ExportKey, - crate::presentation::handler::control::model::datakey::dto::KeyQuery, + crate::presentation::handler::control::model::datakey::dto::RevokeCertificateDTO, crate::presentation::handler::control::model::datakey::dto::NameIdenticalQuery, + crate::presentation::handler::control::model::datakey::dto::ListKeyQuery, crate::presentation::handler::control::model::token::dto::TokenDTO, crate::presentation::handler::control::model::token::dto::CreateTokenDTO, crate::presentation::handler::control::model::user::dto::UserIdentity, @@ -160,6 +164,9 @@ impl ControlServer { self.key_service.clone()); key_service.start_key_rotate_loop(self.cancel_token.clone())?; + key_service.start_key_plugin_maintenance( + self.cancel_token.clone(), + self.server_config.read()?.get_string("control-server.server_port")?.parse()?)?; //prepare redis store let store = RedisSessionStore::new(&redis_connection).await?; @@ -247,10 +254,15 @@ impl ControlServer { //used for control admin cmd pub async fn create_keys(&self, data: &mut DataKey) -> Result { let key = self.key_service.create(data).await?; - self.key_service.enable(None, key.id).await?; + self.key_service.enable(None, format!("{}", key.id)).await?; Ok(key) } + //used for control admin cmd + pub async fn get_key_by_name(&self, name: &str) -> Result { + self.key_service.get_by_name(name).await + } + pub async fn get_user_by_email(&self, email: &str) -> Result { self.user_service.get_by_email(email).await } diff --git a/src/util/error.rs b/src/util/error.rs index 083ff98818066b513671c943d90e030d7cc6acd5..69d8d4ac8adec9615316433dce6cdbee0d3c44db 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -94,6 +94,8 @@ pub enum Error { GeneratingKeyError(String), #[error("not enough privileges")] UnprivilegedError, + #[error("operation disallowed: {0}")] + ActionsNotAllowedError(String), //client error #[error("file extension {0} not supported for file {1}")] @@ -146,6 +148,12 @@ impl ResponseError for Error { detail: self.to_string() }) } + Error::ActionsNotAllowedError(_) => { + warn!("unprivileged: {}", self.to_string()); + HttpResponse::Forbidden().json(ErrorMessage{ + detail: self.to_string() + }) + } Error::UnprivilegedError => { warn!("unprivileged: {}", self.to_string()); HttpResponse::Forbidden().json(ErrorMessage{ diff --git a/src/util/sign.rs b/src/util/sign.rs index 81bc9a0259c9b93f97a9fa91e10fae471cda459b..a19c1d79dd13d63df1c4dc684e47fd9c2f543dcc 100644 --- a/src/util/sign.rs +++ b/src/util/sign.rs @@ -55,13 +55,16 @@ impl Display for FileType { pub enum KeyType { Pgp, X509, + X509EE, } impl Display for KeyType { fn fmt(&self, f: &mut Formatter) -> fmtResult { match self { KeyType::Pgp => write!(f, "pgp"), - KeyType::X509 => write!(f, "x509"), + KeyType::X509EE => write!(f, "x509ee"), + //client can use 'x509' to specify a x509 key type for the purpose of simplicity. + KeyType::X509 => write!(f, "x509ee") } } }