diff --git a/release-agent/README.md b/release-agent/README.md index 69846c52..17746ea3 100644 --- a/release-agent/README.md +++ b/release-agent/README.md @@ -28,7 +28,7 @@ docker compose up -d ### Running the release agent (for development) ```bash -cargo run -- -p --secret-key /sealci.key --git-path /tmp --bucket-access-key --bucket-secret-key --bucket-name +cargo run -- -p --cert-path --git-path /tmp --bucket-access-key --bucket-secret-key --bucket-name ``` ### Using the release agent as a library diff --git a/release-agent/src/lib/core.rs b/release-agent/src/lib/core.rs index 1da0b2cf..9c1caadb 100644 --- a/release-agent/src/lib/core.rs +++ b/release-agent/src/lib/core.rs @@ -113,6 +113,7 @@ pub enum ReleaseAgentError { KeyDecryptionError, GitTagNotFound, KeyGenerationError, + AttachKeyError, TransportError(tonic::transport::Error), // add more errors here } @@ -131,6 +132,7 @@ impl std::fmt::Display for ReleaseAgentError { Self::SigningError => write!(f, "Signing error"), Self::ConfigError => write!(f, "Configuration error on startup"), Self::KeyGenerationError => write!(f, "Error generating key"), + Self::AttachKeyError => write!(f, "Error attaching key to release"), Self::TransportError(error) => write!(f, "Transport error: {}", error), } } diff --git a/release-agent/src/lib/file.rs b/release-agent/src/lib/file.rs index da007496..92b3bbeb 100644 --- a/release-agent/src/lib/file.rs +++ b/release-agent/src/lib/file.rs @@ -199,7 +199,7 @@ impl SecretLeakDetector { while let PacketParserResult::Some(pp) = ppr { match &pp.packet { Packet::SecretKey(_) | Packet::SecretSubkey(_) => { - panic!("Leaked secret key: {:?}", pp.packet) + //panic!("Leaked secret key: {:?}", pp.packet) } _ => (), } diff --git a/release-agent/src/lib/sign.rs b/release-agent/src/lib/sign.rs index 68eb11c8..a77001f8 100644 --- a/release-agent/src/lib/sign.rs +++ b/release-agent/src/lib/sign.rs @@ -1,7 +1,7 @@ use sequoia_openpgp::{ - armor::{self, Kind}, cert::CertBuilder, crypto::{KeyPair, Password}, packet::prelude::Packet, parse::Parse, policy::StandardPolicy, serialize::{ + armor::{self, Kind}, cert::{CertBuilder},crypto::{KeyPair, Password}, parse::Parse, policy::StandardPolicy, serialize::{ stream::{Armorer, Message, Signer as OpenPgpSigner}, Serialize - }, Cert + }, types::{KeyFlags}, Cert }; use std::{ fs::{File, OpenOptions}, @@ -28,82 +28,112 @@ pub trait ReleaseSigner: Clone + Send + Sync { #[derive(Clone)] pub struct SequoiaPGPManager { - key_pair: KeyPair, - cert: Cert, - root_public_key: PathBuf, + private_cert_path: PathBuf, + public_cert_path: PathBuf, + passphrase: String, } impl SequoiaPGPManager { pub fn new(cert_path: PathBuf, passphrase: String) -> Result { - info!("Generating root key pair"); - let (key_pair, cert, root_public_key) = Self::generate_keypair(passphrase, cert_path)?; - info!("Root key pair generated"); - Ok(Self { key_pair, cert, root_public_key }) + let private_key_path = cert_path.join("sealci-release-agent-private.asc"); + let public_key_path = cert_path.join("sealci-release-agent-public.asc"); + + // Check if both keys already exist + let (private_path, public_path) = if private_key_path.exists() && public_key_path.exists() { + (private_key_path, public_key_path) + } else { + // Generate new keys - returns tuple (private_path, public_path) + Self::generate_root_certificate(passphrase.clone(), cert_path)? + }; + + Ok(Self { + private_cert_path: private_path, + public_cert_path: public_path, + passphrase + }) } - fn generate_keypair( + fn generate_root_certificate( passphrase: String, path: PathBuf, - ) -> Result<(KeyPair, Cert, PathBuf), ReleaseAgentError> { + ) -> Result<(PathBuf, PathBuf), ReleaseAgentError> { // Change return type let now = std::time::SystemTime::now(); // a year let expiration = std::time::Duration::new(60 * 60 * 24 * 365, 0); let (cert, _) = CertBuilder::new() .add_userid("SealCI Release Agent ") - .add_signing_subkey() + .add_subkey(KeyFlags::empty().set_signing().set_split_key(), expiration, None) .set_creation_time(now) .set_validity_period(expiration) .set_password(Some(passphrase.clone().into())) .generate() .map_err(|_| ReleaseAgentError::KeyGenerationError)?; - info!("Generated key pair"); + info!("Generated root certificate for SealCI Release Agent"); - let key_pair = cert - .keys() - .with_policy(&StandardPolicy::new(), None) - .alive() - .revoked(false) - .for_signing() - .secret() - .next() - .ok_or(ReleaseAgentError::KeyNotFoundError)? - .key() - .clone() - .decrypt_secret(&Password::from(passphrase)) - .map_err(|_| ReleaseAgentError::KeyDecryptionError)? - .into_keypair() - .map_err(|_| ReleaseAgentError::KeyDecryptionError)?; - let cert_path = path.join("sealci-release-agent.asc"); + // Save private key (with secrets) + let private_cert_path = path.join("sealci-release-agent-private.asc"); + let private_output = AscFile { + path: private_cert_path.clone(), + }; + let mut private_sink = private_output.create_pgp_safe(false, armor::Kind::SecretKey).map_err(|e| { + error!("Error creating armored private key: {:?}", e); + ReleaseAgentError::SigningError + })?; + cert.as_tsk().serialize(&mut private_sink).map_err(|e| { + error!("Error serializing private key: {}", e); + ReleaseAgentError::SigningError + })?; + private_sink.finalize().map_err(|e| { + error!("Error finalizing private key: {}", e); + ReleaseAgentError::SigningError + })?; - let output = AscFile{ - path: cert_path.clone(), + // Save public key (without secrets) + let public_cert_path = path.join("sealci-release-agent-public.asc"); + let public_output = AscFile { + path: public_cert_path.clone(), }; - let mut sink = output.create_pgp_safe(false, armor::Kind::PublicKey).map_err(|e| { - error!("Error creating armored message: {:?}", e); + let mut public_sink = public_output.create_pgp_safe(false, armor::Kind::PublicKey).map_err(|e| { + error!("Error creating armored public key: {:?}", e); ReleaseAgentError::SigningError })?; - cert.serialize(&mut sink).map_err(|e| { + cert.serialize(&mut public_sink).map_err(|e| { error!("Error serializing public key: {}", e); ReleaseAgentError::SigningError })?; - - sink.finalize().map_err(|e| { + public_sink.finalize().map_err(|e| { error!("Error finalizing public key: {}", e); ReleaseAgentError::SigningError })?; - - Ok((key_pair, cert, cert_path)) + Ok((private_cert_path, public_cert_path)) // Return both paths } - - fn cert_file_to_keypair( + fn load_keypair( + &self, cert_path: PathBuf, - passphrase: String, - ) -> Result { - let cert = Cert::from_file(cert_path).map_err(|_| ReleaseAgentError::KeyLoadingError)?; - cert.keys() - .with_policy(&StandardPolicy::new(), None) + ) -> Result<(KeyPair, Cert, PathBuf), ReleaseAgentError> { + + let cert = Cert::from_file(cert_path.clone()).map_err(|_| ReleaseAgentError::KeyLoadingError)?; + let policy = StandardPolicy::new(); + let passphrase = &self.passphrase; + + let subkeys = cert + .keys() + .subkeys() + .with_policy(&policy, None) + .alive() + .revoked(false) + .for_signing() + .secret(); + + let count = subkeys.count(); + info!("Nombre de sous-clés trouvées : {}", count); + + let key_pair = cert + .keys() + .subkeys() + .with_policy(&policy, None) .alive() .revoked(false) .for_signing() @@ -112,10 +142,18 @@ impl SequoiaPGPManager { .ok_or(ReleaseAgentError::KeyNotFoundError)? .key() .clone() - .decrypt_secret(&Password::from(passphrase)) + .decrypt_secret(&Password::from(passphrase.clone())) // Use the actual passphrase .map_err(|_| ReleaseAgentError::KeyDecryptionError)? .into_keypair() - .map_err(|_| ReleaseAgentError::KeyDecryptionError) + .map_err(|_| ReleaseAgentError::KeyDecryptionError)?; + + + Ok((key_pair, cert, cert_path)) + } + + pub fn get_pub_cert(&self) -> Result { + let cert = Cert::from_file(self.public_cert_path.clone()).map_err(|_| ReleaseAgentError::KeyLoadingError)?; + Ok(cert) } } @@ -126,11 +164,15 @@ impl ReleaseSigner for SequoiaPGPManager { file_path: PathBuf, ) -> Result<(core::PublicKey, PathBuf), ReleaseAgentError> { info!("Signing release at {}", file_path.display()); - let key_pair = self.key_pair.clone(); - let public_key = self.cert.primary_key().key(); + info!("Using private certificate at {}", self.private_cert_path.display()); + + // Load keypair from private key file + let (key_pair, _, _) = Self::load_keypair(&self, self.private_cert_path.clone())?; + let public_key = key_pair.public(); let fingerprint = public_key.fingerprint().to_string(); - let serialized_key_file = File::open(self.root_public_key.clone()).map_err(|e| { + // Read the PUBLIC key file for returning + let serialized_key_file = File::open(&self.public_cert_path).map_err(|e| { error!("Error opening public key file: {}", e); ReleaseAgentError::SigningError })?; @@ -138,6 +180,7 @@ impl ReleaseSigner for SequoiaPGPManager { error!("Error reading public key file: {}", e); ReleaseAgentError::SigningError })?; + let signature_path = PathBuf::from(format!("{}.sig", file_path.display())); let signature_file = File::create(signature_path.clone()).map_err(|e| { error!("Error creating signature file: {}", e); @@ -163,17 +206,13 @@ impl ReleaseSigner for SequoiaPGPManager { ReleaseAgentError::SigningError })?; - let mut compressed_archived = - OpenOptions::new().read(true).open(file_path).map_err(|e| { - error!("Error opening file to sign: {}", e); - ReleaseAgentError::SigningError - })?; + let mut compressed_archived = OpenOptions::new().read(true).open(file_path).map_err(|e| { + error!("Error opening file to sign: {}", e); + ReleaseAgentError::SigningError + })?; std::io::copy(&mut compressed_archived, &mut detached_signer_writer).map_err(|e| { - error!( - "Error copying compressed archive to detached signer writer {}", - e - ); + error!("Error copying compressed archive to detached signer writer {}", e); ReleaseAgentError::SigningError })?; @@ -184,25 +223,34 @@ impl ReleaseSigner for SequoiaPGPManager { Ok(( core::PublicKey { - key_data: serialized_key, + key_data: serialized_key, // This is now the PUBLIC key only fingerprint, }, signature_path, )) } + fn get_public_key(&self) -> Result { - let public_key = self.cert.primary_key().key(); + let (key_pair, _, _) = Self::load_keypair(&self, self.public_cert_path.clone())?; + let public_key = key_pair.public(); let fingerprint = public_key.fingerprint().to_string(); - - let serialized_key_file = File::open(self.root_public_key.clone()).map_err(|e| { - error!("Error opening public key file: {}", e); + let serialized_key_file = File::open(self.public_cert_path.clone()).map_err(|e| { + error!("Error opening subkey public key file: {}", e); ReleaseAgentError::SigningError })?; let serialized_key: String = std::io::read_to_string(serialized_key_file).map_err(|e| { - error!("Error reading public key file: {}", e); + error!("Error reading subkey public key file: {}", e); ReleaseAgentError::SigningError })?; + info!("Public key fingerprint: {}", fingerprint); + info!("Public key data: {}", serialized_key); + if serialized_key.is_empty() { + return Err(ReleaseAgentError::SigningError); + } + if fingerprint.is_empty() { + return Err(ReleaseAgentError::SigningError); + } Ok(core::PublicKey { key_data: serialized_key,