keytool 0.1.0

A command-line tool for managing certificates, similar to Java keytool.
Documentation
use crate::commands::command::Cmd;
use crate::error::KeyToolError;
use clap::Args;
use openssl::asn1::Asn1Time;
use openssl::bn::BigNum;
use openssl::hash::MessageDigest;
use openssl::pkcs12::Pkcs12;
use openssl::pkey::PKey;
use openssl::rsa::Rsa;
use openssl::x509::{X509Builder, X509NameBuilder};
use std::fs::File;
use std::io::Write;

/**
cargo run -- genkeypair \
--alias mykey \
--keyalg RSA \
--keysize 2048 \
--sigalg SHA256withRSA \
--validity 365 \
--storetype PKCS12 \
--keystore mykeystore.p12 \
--storepass mypassword \
--keypass mypassword \
--dname "CN=Test,O=MyOrg,L=MyCity,ST=MyState,C=CN"
**/

/**
keytool -v -list -keystore mykeystore.p12 -storetype PKCS12 -storepass mypassword
**/

#[derive(Args, Debug, Clone)]
pub struct GenKeyPairCmd {
    /// Alias name of the key (required)
    #[arg(long)]
    pub alias: String,

    /// Key algorithm (default: RSA)
    #[arg(long)]
    pub keyalg: Option<String>,

    /// Key size in bits (default: 2048)
    #[arg(long)]
    pub keysize: Option<u32>,

    /// Signature algorithm (default: SHA256withRSA)
    #[arg(long)]
    pub sigalg: Option<String>,

    /// Validity period in days (default: 90)
    #[arg(long)]
    pub validity: Option<u32>,

    /// Keystore type (default: PKCS12)
    #[arg(long)]
    pub storetype: Option<String>,

    /// Keystore file path (required)
    #[arg(long)]
    pub keystore: Option<String>,

    /// Keystore password
    #[arg(long)]
    pub storepass: Option<String>,

    /// Key password
    #[arg(long)]
    pub keypass: Option<String>,

    /// Distinguished name (DN) for the certificate, e.g. "CN=Test,O=Org,L=City,ST=State,C=CN"
    #[arg(long)]
    pub dname: Option<String>,

    /// Certificate extensions
    #[arg(long)]
    pub ext: Option<String>,

    /// Provider name
    #[arg(long)]
    pub providername: Option<String>,
}

impl Cmd for GenKeyPairCmd {
    fn run(&self) -> Result<(), KeyToolError> {
        let keyalg = self.keyalg.as_deref().unwrap_or("RSA");
        let keysize = self.keysize.unwrap_or(2048);
        let sigalg = self.sigalg.as_deref().unwrap_or("SHA256withRSA");
        let validity = self.validity.unwrap_or(90);
        let storetype = self.storetype.as_deref().unwrap_or("PKCS12");
        let keystore_path = self
            .keystore
            .as_ref()
            .ok_or_else(|| KeyToolError::new("Missing --keystore argument"))?;
        let storepass = self.storepass.as_deref().unwrap_or("");
        let _keypass = self.keypass.as_deref().unwrap_or(storepass);
        let dname = self.dname.as_deref().unwrap_or("CN=Unknown");

        if keyalg.to_uppercase() != "RSA" {
            return Err(KeyToolError::new(
                "Currently only RSA algorithm is supported",
            ));
        }

        // Generate RSA key pair
        let rsa = Rsa::generate(keysize)?;
        let pkey = PKey::from_rsa(rsa)?;

        // Build X509 certificate
        let mut name_builder = X509NameBuilder::new()?;

        // Simple DN string parsing
        for part in dname.split(',') {
            let kv: Vec<&str> = part.trim().splitn(2, '=').collect();
            if kv.len() == 2 {
                let (k, v) = (kv[0].to_uppercase(), kv[1]);
                match k.as_str() {
                    "CN" => name_builder.append_entry_by_text("CN", v)?,
                    "O" => name_builder.append_entry_by_text("O", v)?,
                    "OU" => name_builder.append_entry_by_text("OU", v)?,
                    "L" => name_builder.append_entry_by_text("L", v)?,
                    "ST" => name_builder.append_entry_by_text("ST", v)?,
                    "C" => name_builder.append_entry_by_text("C", v)?,
                    _ => (), // Ignore other fields
                }
            }
        }
        let name = name_builder.build();

        let mut builder = X509Builder::new()?;
        builder.set_version(2)?;

        // Certificate serial number
        let serial_number = {
            let mut bn = BigNum::new()?;
            bn.rand(64, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
            bn.to_asn1_integer()?
        };
        builder.set_serial_number(&serial_number)?;

        builder.set_subject_name(&name)?;
        builder.set_issuer_name(&name)?; // Self-signed

        builder.set_pubkey(&pkey)?;

        // Set validity period
        let not_before = Asn1Time::days_from_now(0)?;
        let not_after = Asn1Time::days_from_now(validity)?;
        builder.set_not_before(&not_before)?;
        builder.set_not_after(&not_after)?;

        // Choose signature algorithm
        let md = match sigalg.to_uppercase().as_str() {
            "SHA256WITHRSA" => MessageDigest::sha256(),
            "SHA1WITHRSA" => MessageDigest::sha1(),
            _ => MessageDigest::sha256(),
        };

        builder.sign(&pkey, md)?;

        let cert = builder.build();

        if storetype.to_uppercase() == "PKCS12" {
            let friendly_name = self.alias.as_str();

            let mut pkcs12_builder = Pkcs12::builder();
            let pkcs12 = pkcs12_builder
                .name(friendly_name)
                .pkey(&pkey)
                .cert(&cert)
                .build2(storepass)
                .map_err(|e| KeyToolError::new(format!("Failed to generate PKCS12: {}", e)))?;

            let der = pkcs12.to_der()?;

            let mut file = File::create(keystore_path)?;
            file.write_all(&der)?;

            println!(
                "Successfully generated PKCS12 keystore file: {}",
                keystore_path
            );
            return Ok(());
        }

        Err(KeyToolError::new(format!(
            "Unsupported keystore type: {}, only PKCS12 is supported currently",
            storetype
        )))
    }
}