Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion release-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ docker compose up -d
### Running the release agent (for development)

```bash
cargo run -- -p <key-passphrase> --secret-key <path-to-key>/sealci.key --git-path <path-to-put-releases-temporarily>/tmp --bucket-access-key <aws-access-key> --bucket-secret-key <aws-secret-key> --bucket-name <bucket-name>
cargo run -- -p <key-passphrase> --cert-path <path-to-cert> --git-path <path-to-put-releases-temporarily>/tmp --bucket-access-key <aws-access-key> --bucket-secret-key <aws-secret-key> --bucket-name <bucket-name>
```

### Using the release agent as a library
Expand Down
2 changes: 2 additions & 0 deletions release-agent/src/lib/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ pub enum ReleaseAgentError {
KeyDecryptionError,
GitTagNotFound,
KeyGenerationError,
AttachKeyError,
TransportError(tonic::transport::Error), // add more errors here
}

Expand All @@ -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),
}
}
Expand Down
2 changes: 1 addition & 1 deletion release-agent/src/lib/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ impl<W: io::Write + Send + Sync> SecretLeakDetector<W> {
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)
}
_ => (),
}
Expand Down
178 changes: 113 additions & 65 deletions release-agent/src/lib/sign.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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<Self, ReleaseAgentError> {
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 <[email protected]>")
.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<KeyPair, ReleaseAgentError> {
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()
Expand All @@ -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<Cert, ReleaseAgentError> {
let cert = Cert::from_file(self.public_cert_path.clone()).map_err(|_| ReleaseAgentError::KeyLoadingError)?;
Ok(cert)
}
}

Expand All @@ -126,18 +164,23 @@ 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
})?;
let serialized_key: String = std::io::read_to_string(serialized_key_file).map_err(|e| {
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);
Expand All @@ -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
})?;

Expand All @@ -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<core::PublicKey, ReleaseAgentError> {
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,
Expand Down