From 47d3118ac864ee0aba3562ac3e3ce69654808b64 Mon Sep 17 00:00:00 2001 From: TommyLike Date: Sat, 22 Apr 2023 19:46:39 +0800 Subject: [PATCH] Support import pgp and openssl x509 keys --- src/application/datakey.rs | 6 + src/control_admin_entrypoint.rs | 12 +- src/domain/sign_plugin.rs | 5 +- src/domain/sign_service.rs | 1 + src/infra/sign_backend/memory/backend.rs | 8 ++ src/infra/sign_plugin/openpgp.rs | 74 ++++++++---- src/infra/sign_plugin/signers.rs | 9 +- src/infra/sign_plugin/x509.rs | 17 ++- .../handler/control/datakey_handler.rs | 55 ++++++--- .../handler/control/model/datakey/dto.rs | 109 ++++++++++++++---- src/presentation/server/control_server.rs | 2 + 11 files changed, 232 insertions(+), 66 deletions(-) diff --git a/src/application/datakey.rs b/src/application/datakey.rs index ad936c3..1a1552b 100644 --- a/src/application/datakey.rs +++ b/src/application/datakey.rs @@ -26,6 +26,7 @@ use std::collections::HashMap; #[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 get_all(&self) -> Result>; async fn get_one(&self, id: i32) -> Result; async fn delete_one(&self, id: i32) -> Result<()>; @@ -72,6 +73,11 @@ where self.repository.create(data.clone()).await } + async fn import(&self, data: &mut DataKey) -> Result { + self.sign_service.validate_and_update(data).await?; + self.repository.create(data.clone()).await + } + async fn get_all(&self) -> Result> { self.repository.get_all().await } diff --git a/src/control_admin_entrypoint.rs b/src/control_admin_entrypoint.rs index 5a5d9c5..6267f5c 100644 --- a/src/control_admin_entrypoint.rs +++ b/src/control_admin_entrypoint.rs @@ -25,7 +25,7 @@ use clap::{Args}; use crate::client::sign_identity; use crate::domain::datakey::entity::{DataKey}; use crate::domain::user::entity::User; -use crate::presentation::handler::control::model::datakey::dto::DataKeyDTO; +use crate::presentation::handler::control::model::datakey::dto::{CreateDataKeyDTO}; use crate::presentation::handler::control::model::user::dto::UserIdentity; mod util; @@ -157,21 +157,15 @@ async fn main() -> Result<()> { let user = control_server.get_user_by_email(&generate_keys.email).await?; let now = Utc::now(); - let key = DataKeyDTO { - id: 0, + let key = CreateDataKeyDTO { name: generate_keys.name.clone(), description: generate_keys.description.clone(), - user: user.id, - email: user.email.clone(), attributes: generate_keys_parameters(&generate_keys), key_type: generate_keys.key_type.to_string(), - fingerprint: "".to_string(), - create_at: format!("{}", now), expire_at: format!("{}", now + Duration::days(30)), - key_state: Default::default(), }; - let keys = control_server.create_keys(&mut DataKey::convert_from(key, UserIdentity::from(user))?).await?; + let keys = control_server.create_keys(&mut DataKey::create_from(key, UserIdentity::from(user))?).await?; info!("[Result]: Keys {} type {} has been successfully generated", &keys.name, &generate_keys.key_type) } None => {} diff --git a/src/domain/sign_plugin.rs b/src/domain/sign_plugin.rs index 4107365..89dc839 100644 --- a/src/domain/sign_plugin.rs +++ b/src/domain/sign_plugin.rs @@ -16,12 +16,15 @@ use crate::util::error::Result; use std::collections::HashMap; -use crate::domain::datakey::entity::{DataKeyContent, SecDataKey}; +use crate::domain::datakey::entity::{DataKey, DataKeyContent, SecDataKey}; pub trait SignPlugins: Send + Sync { fn new(db: SecDataKey) -> Result where Self: Sized; + fn validate_and_update(key: &mut DataKey) -> Result<()> + where + Self: Sized; fn parse_attributes( private_key: Option>, public_key: Option>, diff --git a/src/domain/sign_service.rs b/src/domain/sign_service.rs index 9864f62..4271baf 100644 --- a/src/domain/sign_service.rs +++ b/src/domain/sign_service.rs @@ -39,6 +39,7 @@ impl FromStr for SignBackendType { #[async_trait] pub trait SignBackend: Send + Sync{ + async fn validate_and_update(&self, data_key: &mut DataKey) -> Result<()>; async fn generate_keys(&self, data_key: &mut DataKey) -> Result<()>; async fn sign(&self, data_key: &DataKey, content: Vec, options: HashMap) -> Result>; async fn decode_public_keys(&self, data_key: &mut DataKey) -> Result<()>; diff --git a/src/infra/sign_backend/memory/backend.rs b/src/infra/sign_backend/memory/backend.rs index ae3eae5..76fe49d 100644 --- a/src/infra/sign_backend/memory/backend.rs +++ b/src/infra/sign_backend/memory/backend.rs @@ -79,6 +79,14 @@ impl MemorySignBackend { #[async_trait] impl SignBackend for MemorySignBackend { + async fn validate_and_update(&self, data_key: &mut DataKey) -> Result<()> { + let _ = Signers::validate_and_update(data_key)?; + data_key.private_key = self.engine.encode(data_key.private_key.clone()).await?; + data_key.public_key = self.engine.encode(data_key.public_key.clone()).await?; + data_key.certificate = self.engine.encode(data_key.certificate.clone()).await?; + Ok(()) + } + 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?; diff --git a/src/infra/sign_plugin/openpgp.rs b/src/infra/sign_plugin/openpgp.rs index 852cbae..1abab8d 100644 --- a/src/infra/sign_plugin/openpgp.rs +++ b/src/infra/sign_plugin/openpgp.rs @@ -38,7 +38,7 @@ use std::str::from_utf8; use validator::{Validate, ValidationError}; use pgp::composed::StandaloneSignature; -use crate::domain::datakey::entity::{DataKeyContent, SecDataKey}; +use crate::domain::datakey::entity::{DataKey, DataKeyContent, SecDataKey}; use crate::util::key::encode_u8_to_hex_string; use super::util::{validate_utc_time_not_expire, validate_utc_time}; @@ -74,28 +74,28 @@ impl PgpKeyGenerationParameter { }; } - pub fn get_digest_algorithm(&self) -> Result { - return match self.digest_algorithm.as_str() { - "none" => Ok(HashAlgorithm::None), - "md5" => Ok(HashAlgorithm::MD5), - "sha1" => Ok(HashAlgorithm::SHA1), - "sha2_256" => Ok(HashAlgorithm::SHA2_256), - "sha2_384" => Ok(HashAlgorithm::SHA2_384), - "sha2_512" => Ok(HashAlgorithm::SHA2_512), - "sha2_224" => Ok(HashAlgorithm::SHA2_224), - "sha3_256" => Ok(HashAlgorithm::SHA3_256), - "sha3_512" => Ok(HashAlgorithm::SHA3_512), - _ => Err(Error::ParameterError( - "invalid digest algorithm for openpgp".to_string(), - )), - }; - } - pub fn get_user_id(&self) -> String { format!("{} <{}>", self.name, self.email) } } +pub fn get_digest_algorithm(hash_digest: &str) -> Result { + return match hash_digest { + "none" => Ok(HashAlgorithm::None), + "md5" => Ok(HashAlgorithm::MD5), + "sha1" => Ok(HashAlgorithm::SHA1), + "sha2_256" => Ok(HashAlgorithm::SHA2_256), + "sha2_384" => Ok(HashAlgorithm::SHA2_384), + "sha2_512" => Ok(HashAlgorithm::SHA2_512), + "sha2_224" => Ok(HashAlgorithm::SHA2_224), + "sha3_256" => Ok(HashAlgorithm::SHA3_256), + "sha3_512" => Ok(HashAlgorithm::SHA3_512), + _ => Err(Error::ParameterError( + "invalid digest algorithm for openpgp".to_string(), + )), + }; +} + fn validate_key_type(key_type: &str) -> std::result::Result<(), ValidationError> { if !vec!["rsa", "ecdh", "eddsa"].contains(&key_type) { return Err(ValidationError::new("invalid key type, possible values are rsa/ecdh/eddsa")); @@ -151,6 +151,32 @@ impl SignPlugins for OpenPGPPlugin { }) } + fn validate_and_update(key: &mut DataKey) -> Result<()> where Self: Sized { + //validate the digest + match key.attributes.get("digest_algorithm") { + Some(digest_str) => { + let _ = get_digest_algorithm(digest_str)?; + } + _ => {} + } + //validate keys + let private = from_utf8(&key.private_key).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(&key.public_key).map_err(|e| Error::KeyParseError(e.to_string()))?; + let (public_key, _) = + SignedPublicKey::from_string(public).map_err(|e| Error::KeyParseError(e.to_string()))?; + //update key attributes + key.fingerprint = encode_u8_to_hex_string(&secret_key.fingerprint()); + match public_key.expires_at() { + None => {} + Some(time) => { + key.expire_at = time + } + } + Ok(()) + } + fn parse_attributes( _private_key: Option>, _public_key: Option>, @@ -173,7 +199,7 @@ impl SignPlugins for OpenPGPPlugin { .can_sign(true) .primary_user_id(parameter.get_user_id()) .preferred_symmetric_algorithms(smallvec![SymmetricKeyAlgorithm::AES256,]) - .preferred_hash_algorithms(smallvec![parameter.get_digest_algorithm()?]) + .preferred_hash_algorithms(smallvec![get_digest_algorithm(parameter.digest_algorithm.as_str())?]) .preferred_compression_algorithms(smallvec![CompressionAlgorithm::ZLIB,]) .created_at(create_at) .expiration(Some(duration)); @@ -192,14 +218,20 @@ impl SignPlugins for OpenPGPPlugin { } fn sign(&self, content: Vec, options: HashMap) -> Result> { - let parameter = OpenPGPPlugin::attributes_validate(&self.attributes)?; + let mut digest = HashAlgorithm::SHA2_256; + match options.get("digest_algorithm") { + Some(digest_str) => { + digest = get_digest_algorithm(digest_str)? + } + _ => {} + } let passwd_fn = String::new; let now = Utc::now(); let sig_cfg = SignatureConfig { version: SignatureVersion::V4, typ: SignatureType::Binary, pub_alg: self.public_key.primary_key.algorithm(), - hash_alg: parameter.get_digest_algorithm()?, + hash_alg: digest, issuer: Some(self.secret_key.key_id()), created: Some(now), unhashed_subpackets: vec![], diff --git a/src/infra/sign_plugin/signers.rs b/src/infra/sign_plugin/signers.rs index d7c74a5..231173d 100644 --- a/src/infra/sign_plugin/signers.rs +++ b/src/infra/sign_plugin/signers.rs @@ -17,7 +17,7 @@ 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::{DataKeyContent, KeyType}; +use crate::domain::datakey::entity::{DataKey, DataKeyContent, KeyType}; use crate::util::error::Result; use std::collections::HashMap; @@ -45,4 +45,11 @@ impl Signers { 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), + } + } } diff --git a/src/infra/sign_plugin/x509.rs b/src/infra/sign_plugin/x509.rs index 913d3fc..ebde0cf 100644 --- a/src/infra/sign_plugin/x509.rs +++ b/src/infra/sign_plugin/x509.rs @@ -15,6 +15,7 @@ */ use std::collections::HashMap; +use std::time::{SystemTime, Duration}; use chrono::{DateTime, Utc}; use openssl::asn1::Asn1Time; @@ -28,7 +29,7 @@ use secstr::SecVec; use serde::Deserialize; use validator::{Validate, ValidationError}; -use crate::domain::datakey::entity::{DataKeyContent, SecDataKey}; +use crate::domain::datakey::entity::{DataKey, DataKeyContent, SecDataKey}; use crate::util::error::{Error, Result}; use crate::domain::sign_plugin::SignPlugins; use crate::util::key::encode_u8_to_hex_string; @@ -155,6 +156,20 @@ impl SignPlugins for X509Plugin { }) } + fn validate_and_update(key: &mut DataKey) -> Result<()> where Self: Sized { + let _private_key = PKey::private_key_from_pem(&key.private_key)?; + let certificate = x509::X509::from_pem(&key.certificate)?; + if !key.public_key.is_empty() { + let _public_key = PKey::public_key_from_pem(&key.public_key)?; + } + let unix_time = Asn1Time::from_unix(0)?.diff(certificate.not_after())?; + let expire = SystemTime::UNIX_EPOCH + Duration::from_secs(unix_time.days as u64 * 86400 + unix_time.secs as u64); + key.expire_at = expire.into(); + key.fingerprint = encode_u8_to_hex_string( + certificate.digest(MessageDigest::from_name("sha1").ok_or(Error::GeneratingKeyError("unable to generate digester".to_string()))?)?.as_ref()); + Ok(()) + } + fn parse_attributes( _private_key: Option>, _public_key: Option>, diff --git a/src/presentation/handler/control/datakey_handler.rs b/src/presentation/handler/control/datakey_handler.rs index 9f34ed2..5a73238 100644 --- a/src/presentation/handler/control/datakey_handler.rs +++ b/src/presentation/handler/control/datakey_handler.rs @@ -19,7 +19,7 @@ use actix_web::{ }; -use crate::presentation::handler::control::model::datakey::dto::{DataKeyDTO, ExportKey}; +use crate::presentation::handler::control::model::datakey::dto::{CreateDataKeyDTO, DataKeyDTO, ExportKey, ImportDataKeyDTO}; use crate::util::error::Error; use validator::Validate; use crate::application::datakey::KeyService; @@ -39,10 +39,8 @@ use super::model::user::dto::UserIdentity; /// ```json /// { /// "name": "test-pgp", -/// "email": "tommylikehu@gmail.com", /// "description": "hello world", /// "key_type": "pgp", -/// "user": "tommylike", /// "attributes": { /// "digest_algorithm": "sha2_256", /// "key_type": "rsa", @@ -69,10 +67,8 @@ use super::model::user::dto::UserIdentity; /// ```json /// { /// "name": "test-x509", -/// "email": "tommylikehu@gmail.com", /// "description": "hello world", /// "key_type": "x509", -/// "user": "tommylike", /// "attributes": { /// "digest_algorithm": "sha2_256", /// "key_type": "rsa", @@ -95,8 +91,8 @@ use super::model::user::dto::UserIdentity; /// ``` #[utoipa::path( post, - path = "/api/v1/keys", - request_body = DataKeyDTO, + path = "/api/v1/keys/", + request_body = CreateDataKeyDTO, security( ("Authorization" = []) ), @@ -107,9 +103,9 @@ use super::model::user::dto::UserIdentity; (status = 500, description = "Server internal error", body = ErrorMessage) ) )] -async fn create_data_key(user: UserIdentity, key_service: web::Data, datakey: web::Json,) -> Result { +async fn create_data_key(user: UserIdentity, key_service: web::Data, datakey: web::Json,) -> Result { datakey.validate()?; - let mut key = DataKey::convert_from(datakey.0, user)?; + let mut key = DataKey::create_from(datakey.0, user)?; Ok(HttpResponse::Created().json(DataKeyDTO::try_from(key_service.into_inner().create(&mut key).await?)?)) } @@ -287,6 +283,37 @@ async fn disable_data_key(_user: UserIdentity, key_service: web::Data Result { - Ok(HttpResponse::Created()) +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?)?)) } @@ -317,10 +346,10 @@ pub fn get_scope() -> Scope { web::resource("/") .route(web::get().to(list_data_key)) .route(web::post().to(create_data_key))) + .service( web::resource("/import").route(web::post().to(import_data_key))) .service( web::resource("/{id}") .route(web::get().to(show_data_key)) .route(web::delete().to(delete_data_key))) - .service( web::resource("/import").route(web::post().to(import_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))) diff --git a/src/presentation/handler/control/model/datakey/dto.rs b/src/presentation/handler/control/model/datakey/dto.rs index 6ca027c..c9302f5 100644 --- a/src/presentation/handler/control/model/datakey/dto.rs +++ b/src/presentation/handler/control/model/datakey/dto.rs @@ -29,39 +29,72 @@ impl TryFrom for ExportKey { } } -#[derive(Debug, Validate, Deserialize, Serialize, ToSchema)] +#[derive(Debug, Validate, Deserialize, ToSchema)] +pub struct CreateDataKeyDTO { + /// Key Name, should be identical, length between 4 and 20 + #[validate(length(min = 4, max = 20))] + pub name: String, + /// Description, length between 0 and 100 + #[validate(length(min = 0, max = 100))] + pub description: 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, + /// Expire utc time, format: 2023-04-08 13:36:35.328324 UTC + #[validate(custom = "validate_utc_time")] + pub expire_at: String, +} + +#[derive(Debug, Validate, Deserialize, ToSchema)] +pub struct ImportDataKeyDTO { + /// Key Name, should be identical, length between 4 and 20 + #[validate(length(min = 4, max = 20))] + pub name: String, + /// Description, length between 0 and 100 + #[validate(length(min = 0, max = 100))] + pub description: String, + /// Attributes in map + pub attributes: HashMap, + /// Key type current support pgp and x509 + #[validate(custom = "validate_key_type")] + pub key_type: String, + /// private key in text format + pub private_key: String, + /// public key in text format + pub public_key: String, + /// certificate in text format + pub certificate: String, +} + +#[derive(Debug, Validate, Serialize, ToSchema)] pub struct DataKeyDTO { - /// Key ID, leave empty when creating - #[serde(skip_deserializing)] + /// Key ID pub id: i32, - /// Key Name, should be identical, length between 4 and 20 + /// Key Name #[validate(length(min = 4, max = 20))] pub name: String, - #[serde(skip_deserializing)] - /// User email, will be removed + /// User email pub email: String, - /// Description, length between 0 and 100 + /// Description #[validate(length(min = 0, max = 100))] pub description: String, - /// User ID, leave empty when creating - #[serde(skip_deserializing)] + /// User ID pub user: i32, /// Attributes in map #[serde(serialize_with = "sorted_map")] pub attributes: HashMap, - /// Key type current support pgp and x509 + /// Key type pub key_type: String, - /// Fingerprint, leave empty when creating - #[serde(skip_deserializing)] + /// Fingerprint pub fingerprint: String, /// Create utc time, format: 2023-04-08 13:36:35.328324 UTC - #[validate(custom = "validate_utc_time")] pub create_at: String, /// Expire utc time, format: 2023-04-08 13:36:35.328324 UTC - #[validate(custom = "validate_utc_time")] pub expire_at: String, - /// Key state, leave empty when creating - #[serde(skip_deserializing)] + /// Key state pub key_state: String, } @@ -72,14 +105,50 @@ fn validate_utc_time(expire: &str) -> std::result::Result<(), ValidationError> { Ok(()) } +fn validate_key_type(key_type: &str) -> std::result::Result<(), ValidationError> { + return match KeyType::from_str(key_type) { + Ok(_) => { + Ok(()) + } + Err(_) => { + Err(ValidationError::new("unsupported key type")) + } + } +} + impl DataKey { - pub fn convert_from(dto: DataKeyDTO, identity: UserIdentity) -> Result { + pub fn import_from(dto: ImportDataKeyDTO, identity: UserIdentity) -> Result { + let now = Utc::now(); let mut combined_attributes = dto.attributes.clone(); combined_attributes.insert("name".to_string(), dto.name.clone()); - combined_attributes.insert("create_at".to_string(), dto.create_at.clone()); + combined_attributes.insert("create_at".to_string(), now.clone().to_string()); + Ok(DataKey { + id: 0, + name: dto.name, + description: dto.description, + user: identity.id, + email: identity.email, + attributes: combined_attributes, + key_type: KeyType::from_str(dto.key_type.as_str())?, + fingerprint: "".to_string(), + private_key: dto.private_key.into_bytes(), + public_key: dto.public_key.into_bytes(), + certificate: dto.certificate.into_bytes(), + create_at: now.clone(), + expire_at: now, + soft_delete: false, + key_state: KeyState::default() + }) + } + + pub fn create_from(dto: CreateDataKeyDTO, identity: UserIdentity) -> Result { + let now = Utc::now(); + 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()); combined_attributes.insert("expire_at".to_string(), dto.expire_at.clone()); Ok(DataKey { - id: dto.id, + id: 0, name: dto.name, description: dto.description, user: identity.id, @@ -90,7 +159,7 @@ impl DataKey { private_key: vec![], public_key: vec![], certificate: vec![], - create_at: dto.create_at.parse()?, + create_at: now, expire_at: dto.expire_at.parse()?, soft_delete: false, key_state: KeyState::default() diff --git a/src/presentation/server/control_server.rs b/src/presentation/server/control_server.rs index dac6005..ecf5943 100644 --- a/src/presentation/server/control_server.rs +++ b/src/presentation/server/control_server.rs @@ -87,6 +87,8 @@ impl Modify for SecurityAddon { ), components( 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::token::dto::TokenDTO, crate::presentation::handler::control::model::user::dto::UserIdentity, -- Gitee