diff --git a/ceres/src/api_service/mono_api_service.rs b/ceres/src/api_service/mono_api_service.rs index dfb5cc8ee..25c4d7646 100644 --- a/ceres/src/api_service/mono_api_service.rs +++ b/ceres/src/api_service/mono_api_service.rs @@ -47,7 +47,6 @@ use callisto::sea_orm_active_enums::ConvTypeEnum; use callisto::{mega_blob, mega_mr, mega_tree, raw_blob}; use common::errors::MegaError; use common::model::Pagination; -// use common::utils::parse_commit_msg; use jupiter::storage::base_storage::StorageConnector; use jupiter::storage::Storage; use jupiter::utils::converter::generate_git_keep_with_timestamp; @@ -58,8 +57,6 @@ use mercury::internal::object::commit::Commit; use mercury::internal::object::tree::{Tree, TreeItem, TreeItemMode}; use neptune::model::diff_model::DiffItem; use neptune::neptune_engine::Diff; -// use pgp::{Deserializable, SignedPublicKey, StandaloneSignature}; -use regex::Regex; #[derive(Clone)] pub struct MonoApiService { @@ -590,87 +587,6 @@ impl MonoApiService { Ok(res) } - pub async fn verify_mr(&self, mr_link: &str) -> Result, MegaError> { - let stg = self.storage.mr_storage(); - let mr = stg.get_mr(mr_link).await?.ok_or_else(|| { - MegaError::with_message(format!("Merge request not found: {mr_link}")) - })?; - - let commit = self - .storage - .mono_storage() - .get_commit_by_hash(&mr.to_hash) - .await? - .ok_or_else(|| MegaError::with_message("Commit not found"))?; - - let mut res = HashMap::new(); - let content = commit.content.clone().unwrap_or_default(); - let _user_id = self - .storage - .user_storage() - .find_user_by_email(&self.extract_email(&content).await.unwrap_or_default()) - .await? - .unwrap() - .id; - // let verified = self.verify_commit_gpg_signature(&content, user_id).await?; - // TODO: Temporarily disable GPG verification - let verified = false; - res.insert(commit.commit_id.clone(), verified); - Ok(res) - } - - async fn extract_email(&self, s: &str) -> Option { - let re = Regex::new(r"<\s*(?P[^<>@\s]+@[^<>@\s]+)\s*>").unwrap(); - re.captures(s) - .and_then(|c| c.get(1)) - .map(|m| m.as_str().to_string()) - } - - // TODO: implement GPG - // async fn verify_commit_gpg_signature( - // &self, - // commit_content: &str, - // user_id: String, - // ) -> Result { - // let (commit_msg, signature) = parse_commit_msg(commit_content); - // if signature.is_none() { - // return Ok(false); // No signature to verify - // } - // - // let sig_str = signature.unwrap(); - // - // // Remove "gpgsig " prefix if present - // let sig = sig_str - // .strip_prefix("gpgsig ") - // .map(|s| s.trim()) - // .unwrap_or(sig_str); - // - // let keys = self.storage.gpg_storage().list_user_gpg(user_id).await?; - // - // for key in keys { - // let verified = self - // .verify_signature_with_key(&key.public_key, sig, commit_msg) - // .await?; - // if verified { - // return Ok(true); // Signature verified successfully - // } - // } - // - // Ok(false) // No key could verify the signature - // } - // - // async fn verify_signature_with_key( - // &self, - // public_key: &str, - // signature: &str, - // message: &str, - // ) -> Result { - // let (public_key, _) = SignedPublicKey::from_string(public_key)?; - // let (signature, _) = StandaloneSignature::from_string(signature)?; - // - // Ok(signature.verify(&public_key, message.as_bytes()).is_ok()) - // } - pub async fn get_commit_blobs( &self, commit_hash: &str, diff --git a/ceres/src/merge_checker/gpg_signature_checker.rs b/ceres/src/merge_checker/gpg_signature_checker.rs new file mode 100644 index 000000000..92461c9c3 --- /dev/null +++ b/ceres/src/merge_checker/gpg_signature_checker.rs @@ -0,0 +1,118 @@ +use crate::merge_checker::{CheckResult, CheckType, Checker}; +use async_trait::async_trait; +use common::errors::MegaError; +use common::utils::parse_commit_msg; +use jupiter::model::mr_dto::MrInfoDto; +use jupiter::storage::Storage; +use pgp::{Deserializable, SignedPublicKey, StandaloneSignature}; +use serde::Deserialize; +use serde_json::Value; +use std::sync::Arc; + +pub struct GpgSignatureChecker { + pub storage: Arc, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct GpgSignatureParams { + mr_to: String, + committer: String, +} + +impl GpgSignatureParams { + fn from_value(v: &serde_json::Value) -> anyhow::Result { + Ok(serde_json::from_value(v.clone())?) + } +} + +#[async_trait] +impl Checker for GpgSignatureChecker { + async fn run(&self, params: &serde_json::Value) -> crate::merge_checker::CheckResult { + let params = GpgSignatureParams::from_value(params).expect("parse params err"); + let mut res = CheckResult { + check_type_code: CheckType::GpgSignature, + status: String::from("PENDING"), + message: String::new(), + }; + + let is_verified = self + .verify_mr(¶ms.mr_to, params.committer) + .await + .expect("cannot verify commits"); + if is_verified { + res.status = String::from("PASSED"); + } else { + res.status = String::from("FAILED"); + res.message = String::from("The commit GPG signature verification failed"); + } + + res + } + + async fn build_params(&self, mr_info: &MrInfoDto) -> Result { + Ok(serde_json::json!({ + "mr_to": mr_info.to_hash, + "committer": mr_info.username, + })) + } +} + +impl GpgSignatureChecker { + async fn verify_mr(&self, mr_to: &str, assignee: String) -> Result { + let commit = self + .storage + .mono_storage() + .get_commit_by_hash(mr_to) + .await? + .ok_or_else(|| MegaError::with_message("Commit not found"))?; + + let content = commit.content.clone().unwrap_or_default(); + let verified = self.verify_commit_gpg_signature(&content, assignee).await?; + + Ok(verified) + } + + async fn verify_commit_gpg_signature( + &self, + commit_content: &str, + assignee: String, + ) -> Result { + let (commit_msg, signature) = parse_commit_msg(commit_content); + if signature.is_none() { + return Ok(false); // No signature to verify + } + + let sig_str = signature.unwrap(); + + // Remove "gpgsig " prefix if present + let sig = sig_str + .strip_prefix("gpgsig ") + .map(|s| s.trim()) + .unwrap_or(sig_str); + + let keys = self.storage.gpg_storage().list_user_gpg(assignee).await?; + + for key in keys { + let verified = self + .verify_signature_with_key(&key.public_key, sig, commit_msg) + .await?; + if verified { + return Ok(true); // Signature verified successfully + } + } + + Ok(false) // No key could verify the signature + } + + async fn verify_signature_with_key( + &self, + public_key: &str, + signature: &str, + message: &str, + ) -> Result { + let (public_key, _) = SignedPublicKey::from_string(public_key)?; + let (signature, _) = StandaloneSignature::from_string(signature)?; + + Ok(signature.verify(&public_key, message.as_bytes()).is_ok()) + } +} diff --git a/ceres/src/merge_checker/mod.rs b/ceres/src/merge_checker/mod.rs index a91534b04..a5c2ff912 100644 --- a/ceres/src/merge_checker/mod.rs +++ b/ceres/src/merge_checker/mod.rs @@ -4,12 +4,13 @@ use async_trait::async_trait; use serde::Serialize; use utoipa::ToSchema; +use crate::merge_checker::gpg_signature_checker::GpgSignatureChecker; +use crate::merge_checker::mr_sync_checker::MrSyncChecker; use callisto::{check_result, sea_orm_active_enums::CheckTypeEnum}; use common::errors::MegaError; use jupiter::{model::mr_dto::MrInfoDto, storage::Storage}; -use crate::merge_checker::mr_sync_checker::MrSyncChecker; - +mod gpg_signature_checker; pub mod mr_sync_checker; #[async_trait] @@ -105,7 +106,18 @@ impl CheckerRegistry { storage: storage.clone(), username, }; - r.register(CheckType::MrSync, Box::new(MrSyncChecker { storage })); + r.register( + CheckType::MrSync, + Box::new(MrSyncChecker { + storage: storage.clone(), + }), + ); + r.register( + CheckType::GpgSignature, + Box::new(GpgSignatureChecker { + storage: storage.clone(), + }), + ); r } diff --git a/jupiter/src/storage/gpg_storage.rs b/jupiter/src/storage/gpg_storage.rs index 0f9ecb8db..c1ce76888 100644 --- a/jupiter/src/storage/gpg_storage.rs +++ b/jupiter/src/storage/gpg_storage.rs @@ -31,7 +31,6 @@ impl GpgStorage { &self, user_id: String, gpg_content: String, - expires_days: Option, ) -> Result { let (pk, _headers) = SignedPublicKey::from_string(&gpg_content).map_err(|e| { tracing::error!("{:?}", e); @@ -40,8 +39,8 @@ impl GpgStorage { let key_id = format!("{:016X}", pk.key_id()); let fingerprint = format!("{:?}", pk.fingerprint()); - let created_at = chrono::Utc::now().naive_utc(); - let expires_at = expires_days.map(|days| created_at + chrono::Duration::days(days as i64)); + let created_at = pk.created_at().naive_utc(); + let expires_at = pk.expires_at().map(|t| t.naive_utc()); let key = gpg_key::Model { id: generate_id(), @@ -57,13 +56,8 @@ impl GpgStorage { Ok(key) } - pub async fn add_gpg_key( - &self, - user_id: String, - gpg_content: String, - expired_at: Option, - ) -> Result<(), MegaError> { - let key = self.create_key(user_id, gpg_content, expired_at)?; + pub async fn add_gpg_key(&self, user_id: String, gpg_content: String) -> Result<(), MegaError> { + let key = self.create_key(user_id, gpg_content)?; let a_model = key.into_active_model(); a_model.insert(self.get_connection()).await.map_err(|e| { tracing::error!("{:?}", e); diff --git a/mono/src/api/gpg/gpg_router.rs b/mono/src/api/gpg/gpg_router.rs index 86ba4cb1d..0343dbf91 100644 --- a/mono/src/api/gpg/gpg_router.rs +++ b/mono/src/api/gpg/gpg_router.rs @@ -56,10 +56,7 @@ async fn add_gpg( // let uid = "exampleid".to_string(); let uid = user.campsite_user_id.clone(); println!("Adding GPG key for user: {}", req.gpg_content.clone()); - state - .gpg_stg() - .add_gpg_key(uid, req.gpg_content, req.expires_days) - .await?; + state.gpg_stg().add_gpg_key(uid, req.gpg_content).await?; Ok(Json(CommonResult::success(None))) } diff --git a/mono/src/api/gpg/model.rs b/mono/src/api/gpg/model.rs index 2d8baf4f3..2e28c5960 100644 --- a/mono/src/api/gpg/model.rs +++ b/mono/src/api/gpg/model.rs @@ -6,7 +6,6 @@ use utoipa::ToSchema; #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct NewGpgRequest { pub gpg_content: String, - pub expires_days: Option, } #[derive(Debug, Serialize, Deserialize, ToSchema)] diff --git a/mono/src/api/mr/model.rs b/mono/src/api/mr/model.rs index 8b1378917..8c770a489 100644 --- a/mono/src/api/mr/model.rs +++ b/mono/src/api/mr/model.rs @@ -1 +1,4 @@ - +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, utoipa::ToSchema)] +pub struct VerifyMrPayload { + pub assignees: Vec, +} diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index 66fac5ff5..bbb2f6fe2 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::path::PathBuf; use axum::{ @@ -45,8 +44,7 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(save_comment)) .routes(routes!(labels)) .routes(routes!(assignees)) - .routes(routes!(edit_title)) - .routes(routes!(verify_mr_signature)), + .routes(routes!(edit_title)), ) } @@ -492,25 +490,6 @@ async fn assignees( api_common::label_assignee::assignees_update(user, state, payload, String::from("mr")).await } -#[utoipa::path( - get, - params( - ("link", description = "MR link"), - ), - path = "/{link}/verify-signature", - responses( - (status = 200, body = CommonResult>, content_type = "application/json") - ), - tag = MR_TAG -)] -async fn verify_mr_signature( - Path(link): Path, - state: State, -) -> Result>>, ApiError> { - let res = state.monorepo().verify_mr(&link).await?; - Ok(Json(CommonResult::success(Some(res)))) -} - fn build_forest(paths: Vec) -> Vec { let mut roots: Vec = Vec::new();