Last active
October 18, 2025 09:18
-
-
Save daemondevin/c3d1f6f2fa2b9e48d0566c6bddc42aed to your computer and use it in GitHub Desktop.
Code signing utility written in Rust. This utility has support for generating a new RSA key pair, generating a self-signed certificate for code signing, signing a file with a certificate, and verifying a file signature.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Code Signing Utility with Certificate Support | |
| // Signs programs and verifies signatures using RSA cryptography with X.509 certificates | |
| // | |
| use base64::{engine::general_purpose, Engine as _}; | |
| use chrono::{Duration, Utc}; | |
| use clap::{Parser, Subcommand}; | |
| use der::{Decode, Encode}; | |
| use rsa::{ | |
| pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding}, | |
| RsaPrivateKey, RsaPublicKey, | |
| signature::{RandomizedSigner, SignatureEncoding, Verifier}, | |
| pss::{BlindedSigningKey, Signature, VerifyingKey}, | |
| }; | |
| use serde::{Deserialize, Serialize}; | |
| use sha2::{Digest, Sha256}; | |
| use spki::SubjectPublicKeyInfoOwned; | |
| use std::{ | |
| fs, | |
| path::{Path, PathBuf}, | |
| process, | |
| str::FromStr, | |
| }; | |
| use x509_cert::{ | |
| builder::{Builder, CertificateBuilder, Profile}, | |
| name::Name, | |
| serial_number::SerialNumber, | |
| time::Validity, | |
| Certificate, | |
| }; | |
| #[derive(Parser)] | |
| #[command(name = "codesign")] | |
| #[command(about = "Code Signing Utility with Certificate Support", long_about = None)] | |
| struct Cli { | |
| #[command(subcommand)] | |
| command: Commands, | |
| #[arg(long, default_value = ".keys", global = true)] | |
| key_dir: PathBuf, | |
| } | |
| #[derive(Subcommand)] | |
| enum Commands { | |
| /// Generate a new RSA key pair | |
| GenerateKeys { | |
| #[arg(long, default_value_t = 2048)] | |
| bits: usize, | |
| }, | |
| /// Generate a self-signed certificate for code signing | |
| GenerateCert { | |
| /// Common Name (e.g., "Your Name" or "Your Company") | |
| #[arg(long)] | |
| cn: String, | |
| /// Organization | |
| #[arg(long)] | |
| org: Option<String>, | |
| /// Country (2-letter code) | |
| #[arg(long)] | |
| country: Option<String>, | |
| /// Days until expiration | |
| #[arg(long, default_value_t = 365)] | |
| days: i64, | |
| }, | |
| /// Sign a file with certificate | |
| Sign { | |
| /// File to sign | |
| file: PathBuf, | |
| /// Use certificate-based signing | |
| #[arg(long)] | |
| with_cert: bool, | |
| }, | |
| /// Verify a file signature | |
| Verify { | |
| /// File to verify | |
| file: PathBuf, | |
| }, | |
| /// Show certificate information | |
| ShowCert, | |
| } | |
| #[derive(Serialize, Deserialize)] | |
| struct SignatureData { | |
| file: String, | |
| size: u64, | |
| sha256: String, | |
| signature: String, | |
| #[serde(skip_serializing_if = "Option::is_none")] | |
| certificate: Option<String>, | |
| #[serde(skip_serializing_if = "Option::is_none")] | |
| cert_subject: Option<String>, | |
| #[serde(skip_serializing_if = "Option::is_none")] | |
| cert_issuer: Option<String>, | |
| #[serde(skip_serializing_if = "Option::is_none")] | |
| cert_valid_from: Option<String>, | |
| #[serde(skip_serializing_if = "Option::is_none")] | |
| cert_valid_to: Option<String>, | |
| } | |
| struct CodeSigner { | |
| key_dir: PathBuf, | |
| private_key_path: PathBuf, | |
| public_key_path: PathBuf, | |
| cert_path: PathBuf, | |
| } | |
| impl CodeSigner { | |
| fn new(key_dir: PathBuf) -> Self { | |
| let private_key_path = key_dir.join("private_key.pem"); | |
| let public_key_path = key_dir.join("public_key.pem"); | |
| let cert_path = key_dir.join("certificate.pem"); | |
| Self { | |
| key_dir, | |
| private_key_path, | |
| public_key_path, | |
| cert_path, | |
| } | |
| } | |
| fn generate_keys(&self, bits: usize) -> Result<(), Box<dyn std::error::Error>> { | |
| println!("Generating {}-bit RSA key pair...", bits); | |
| fs::create_dir_all(&self.key_dir)?; | |
| let mut rng = rand::thread_rng(); | |
| let private_key = RsaPrivateKey::new(&mut rng, bits)?; | |
| let public_key = RsaPublicKey::from(&private_key); | |
| // Save private key | |
| private_key.write_pkcs8_pem_file(&self.private_key_path, LineEnding::LF)?; | |
| // Save public key | |
| public_key.write_public_key_pem_file(&self.public_key_path, LineEnding::LF)?; | |
| println!("Keys generated successfully"); | |
| println!(" Private key: {}", self.private_key_path.display()); | |
| println!(" Public key: {}", self.public_key_path.display()); | |
| Ok(()) | |
| } | |
| fn generate_certificate( | |
| &self, | |
| cn: &str, | |
| org: Option<&str>, | |
| country: Option<&str>, | |
| days: i64, | |
| ) -> Result<(), Box<dyn std::error::Error>> { | |
| if !self.private_key_path.exists() { | |
| eprintln!("Error: Private key not found. Generate keys first with: generate-keys"); | |
| process::exit(1); | |
| } | |
| println!("Generating self-signed certificate..."); | |
| let private_key = self.load_private_key()?; | |
| let public_key = RsaPublicKey::from(&private_key); | |
| // Build subject/issuer name | |
| let mut subject_parts = vec![format!("CN={}", cn)]; | |
| if let Some(o) = org { | |
| subject_parts.push(format!("O={}", o)); | |
| } | |
| if let Some(c) = country { | |
| subject_parts.push(format!("C={}", c)); | |
| } | |
| let subject_str = subject_parts.join(","); | |
| let subject = Name::from_str(&subject_str)?; | |
| // Generate serial number | |
| let mut rng = rand::thread_rng(); | |
| let serial_num: u64 = rand::random(); | |
| let serial = SerialNumber::from(serial_num); | |
| // Set validity period | |
| let not_before = Utc::now(); | |
| let not_after = not_before + Duration::days(days); | |
| let validity = Validity::from_now(std::time::Duration::from_secs((days * 86400) as u64))?; | |
| // Convert public key to SPKI | |
| let spki = SubjectPublicKeyInfoOwned::from_key(public_key)?; | |
| // Create signing key for certificate generation (using PKCS1v15 which is better supported) | |
| let signing_key = rsa::pkcs1v15::SigningKey::<Sha256>::new(private_key); | |
| // Build certificate | |
| let builder = CertificateBuilder::new( | |
| Profile::Root, | |
| serial, | |
| validity, | |
| subject.clone(), | |
| spki, | |
| &signing_key, | |
| )?; | |
| let cert = builder.build::<rsa::pkcs1v15::Signature>()?; | |
| // Save certificate in PEM format | |
| let cert_der = cert.to_der()?; | |
| let cert_pem = format!( | |
| "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n", | |
| general_purpose::STANDARD.encode(&cert_der) | |
| ); | |
| fs::write(&self.cert_path, cert_pem)?; | |
| println!("Certificate generated successfully"); | |
| println!(" Certificate: {}", self.cert_path.display()); | |
| println!(" Subject: {}", subject_str); | |
| println!(" Valid from: {}", not_before.format("%Y-%m-%d %H:%M:%S UTC")); | |
| println!(" Valid to: {}", not_after.format("%Y-%m-%d %H:%M:%S UTC")); | |
| Ok(()) | |
| } | |
| fn load_private_key(&self) -> Result<RsaPrivateKey, Box<dyn std::error::Error>> { | |
| let key = RsaPrivateKey::read_pkcs8_pem_file(&self.private_key_path)?; | |
| Ok(key) | |
| } | |
| fn load_public_key(&self) -> Result<RsaPublicKey, Box<dyn std::error::Error>> { | |
| let key = RsaPublicKey::read_public_key_pem_file(&self.public_key_path)?; | |
| Ok(key) | |
| } | |
| fn load_certificate(&self) -> Result<Certificate, Box<dyn std::error::Error>> { | |
| let pem = fs::read_to_string(&self.cert_path)?; | |
| let cert_b64: String = pem | |
| .lines() | |
| .filter(|line| !line.starts_with("-----")) | |
| .collect(); | |
| let cert_der = general_purpose::STANDARD.decode(cert_b64)?; | |
| let cert = Certificate::from_der(&cert_der)?; | |
| Ok(cert) | |
| } | |
| fn sign_file(&self, file_path: &Path, with_cert: bool) -> Result<(), Box<dyn std::error::Error>> { | |
| if !file_path.exists() { | |
| eprintln!("Error: File not found: {}", file_path.display()); | |
| process::exit(1); | |
| } | |
| if !self.private_key_path.exists() { | |
| eprintln!("Error: Private key not found. Generate keys first with: generate-keys"); | |
| process::exit(1); | |
| } | |
| if with_cert && !self.cert_path.exists() { | |
| eprintln!("Error: Certificate not found. Generate certificate first with: generate-cert"); | |
| process::exit(1); | |
| } | |
| println!("Signing {}...", file_path.display()); | |
| if with_cert { | |
| println!(" Using certificate-based signing"); | |
| } | |
| // Read file and compute hash | |
| let file_data = fs::read(file_path)?; | |
| let mut hasher = Sha256::new(); | |
| hasher.update(&file_data); | |
| let file_hash = hasher.finalize(); | |
| // Sign the hash | |
| let private_key = self.load_private_key()?; | |
| let signing_key = BlindedSigningKey::<Sha256>::new(private_key); | |
| let mut rng = rand::thread_rng(); | |
| let signature = signing_key.sign_with_rng(&mut rng, &file_hash); | |
| // Create signature data | |
| let mut sig_data = SignatureData { | |
| file: file_path | |
| .file_name() | |
| .unwrap() | |
| .to_string_lossy() | |
| .to_string(), | |
| size: file_data.len() as u64, | |
| sha256: general_purpose::STANDARD.encode(&file_hash[..]), | |
| signature: general_purpose::STANDARD.encode(signature.to_bytes()), | |
| certificate: None, | |
| cert_subject: None, | |
| cert_issuer: None, | |
| cert_valid_from: None, | |
| cert_valid_to: None, | |
| }; | |
| // Add certificate info if requested | |
| if with_cert { | |
| let cert = self.load_certificate()?; | |
| let cert_der = cert.to_der()?; | |
| sig_data.certificate = Some(general_purpose::STANDARD.encode(&cert_der)); | |
| sig_data.cert_subject = Some(cert.tbs_certificate.subject.to_string()); | |
| sig_data.cert_issuer = Some(cert.tbs_certificate.issuer.to_string()); | |
| sig_data.cert_valid_from = Some(cert.tbs_certificate.validity.not_before.to_string()); | |
| sig_data.cert_valid_to = Some(cert.tbs_certificate.validity.not_after.to_string()); | |
| } | |
| let sig_path = file_path.with_extension( | |
| format!( | |
| "{}.sig", | |
| file_path.extension().unwrap_or_default().to_string_lossy() | |
| ) | |
| ); | |
| let sig_json = serde_json::to_string_pretty(&sig_data)?; | |
| fs::write(&sig_path, sig_json)?; | |
| println!("Signature created: {}", sig_path.display()); | |
| Ok(()) | |
| } | |
| fn verify_file(&self, file_path: &Path) -> Result<bool, Box<dyn std::error::Error>> { | |
| let sig_path = file_path.with_extension( | |
| format!( | |
| "{}.sig", | |
| file_path.extension().unwrap_or_default().to_string_lossy() | |
| ) | |
| ); | |
| if !file_path.exists() { | |
| eprintln!("Error: File not found: {}", file_path.display()); | |
| return Ok(false); | |
| } | |
| if !sig_path.exists() { | |
| eprintln!("Error: Signature file not found: {}", sig_path.display()); | |
| return Ok(false); | |
| } | |
| println!("Verifying {}...", file_path.display()); | |
| // Load signature data | |
| let sig_json = fs::read_to_string(&sig_path)?; | |
| let sig_data: SignatureData = serde_json::from_str(&sig_json)?; | |
| // Read and hash file | |
| let file_data = fs::read(file_path)?; | |
| let mut hasher = Sha256::new(); | |
| hasher.update(&file_data); | |
| let file_hash = hasher.finalize(); | |
| // Check file hash matches | |
| let expected_hash = general_purpose::STANDARD.decode(&sig_data.sha256)?; | |
| if &file_hash[..] != expected_hash.as_slice() { | |
| println!("Verification FAILED: File has been modified"); | |
| return Ok(false); | |
| } | |
| // Get public key (from certificate if present, otherwise from key file) | |
| let public_key = if let Some(cert_b64) = &sig_data.certificate { | |
| println!(" Certificate found in signature"); | |
| let cert_der = general_purpose::STANDARD.decode(cert_b64)?; | |
| let cert = Certificate::from_der(&cert_der)?; | |
| // Display certificate info | |
| if let Some(subject) = &sig_data.cert_subject { | |
| println!(" Signed by: {}", subject); | |
| } | |
| if let (Some(from), Some(to)) = (&sig_data.cert_valid_from, &sig_data.cert_valid_to) { | |
| println!(" Valid: {} to {}", from, to); | |
| } | |
| // Extract public key from certificate | |
| let spki = &cert.tbs_certificate.subject_public_key_info; | |
| let spki_der = spki.to_der()?; | |
| RsaPublicKey::from_public_key_der(&spki_der)? | |
| } else { | |
| if !self.public_key_path.exists() { | |
| eprintln!("Error: Public key not found"); | |
| return Ok(false); | |
| } | |
| self.load_public_key()? | |
| }; | |
| // Verify signature | |
| let verifying_key = VerifyingKey::<Sha256>::new(public_key); | |
| let signature_bytes = general_purpose::STANDARD.decode(&sig_data.signature)?; | |
| let signature = Signature::try_from(signature_bytes.as_slice())?; | |
| match verifying_key.verify(&file_hash, &signature) { | |
| Ok(_) => { | |
| println!("Signature is VALID"); | |
| Ok(true) | |
| } | |
| Err(_) => { | |
| println!("Signature verification FAILED"); | |
| Ok(false) | |
| } | |
| } | |
| } | |
| fn show_certificate(&self) -> Result<(), Box<dyn std::error::Error>> { | |
| if !self.cert_path.exists() { | |
| eprintln!("Error: Certificate not found at {}", self.cert_path.display()); | |
| eprintln!("Generate one with: generate-cert --cn \"Your Name\""); | |
| process::exit(1); | |
| } | |
| let cert = self.load_certificate()?; | |
| println!("Certificate Information:"); | |
| println!(" Subject: {}", cert.tbs_certificate.subject); | |
| println!(" Issuer: {}", cert.tbs_certificate.issuer); | |
| println!(" Serial: {:?}", cert.tbs_certificate.serial_number); | |
| println!(" Valid From: {}", cert.tbs_certificate.validity.not_before); | |
| println!(" Valid To: {}", cert.tbs_certificate.validity.not_after); | |
| println!(" Location: {}", self.cert_path.display()); | |
| Ok(()) | |
| } | |
| } | |
| fn main() { | |
| let cli = Cli::parse(); | |
| let signer = CodeSigner::new(cli.key_dir); | |
| let result = match cli.command { | |
| Commands::GenerateKeys { bits } => signer.generate_keys(bits), | |
| Commands::GenerateCert { cn, org, country, days } => { | |
| signer.generate_certificate(&cn, org.as_deref(), country.as_deref(), days) | |
| } | |
| Commands::Sign { file, with_cert } => signer.sign_file(&file, with_cert), | |
| Commands::Verify { file } => signer.verify_file(&file).map(|_| ()), | |
| Commands::ShowCert => signer.show_certificate(), | |
| }; | |
| if let Err(e) = result { | |
| eprintln!("Error: {}", e); | |
| process::exit(1); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment