Skip to content

Implement bssh-keygen tool #143

@inureyes

Description

@inureyes

Summary

Implement bssh-keygen tool for generating SSH key pairs in OpenSSH format, supporting Ed25519 and RSA algorithms.

Parent Epic

Implementation Details

1. Key Generation Module

// src/keygen/mod.rs
pub mod ed25519;
pub mod rsa;

use std::path::Path;

/// Key generation result
pub struct GeneratedKey {
    pub private_key_pem: String,
    pub public_key_openssh: String,
    pub fingerprint: String,
}

/// Generate an Ed25519 key pair
pub fn generate_ed25519(output_path: &Path, comment: Option<&str>) -> Result<GeneratedKey> {
    ed25519::generate(output_path, comment)
}

/// Generate an RSA key pair
pub fn generate_rsa(output_path: &Path, bits: u32, comment: Option<&str>) -> Result<GeneratedKey> {
    rsa::generate(output_path, bits, comment)
}

2. Ed25519 Key Generation

// src/keygen/ed25519.rs
use rand::rngs::OsRng;
use russh_keys::key::{KeyPair, PublicKey};

pub fn generate(output_path: &Path, comment: Option<&str>) -> Result<GeneratedKey> {
    // Generate key pair
    let keypair = KeyPair::generate_ed25519()
        .context("Failed to generate Ed25519 key")?;

    // Get public key
    let public_key = keypair.public_key();
    let fingerprint = public_key.fingerprint();

    // Format private key (OpenSSH format)
    let private_key_pem = keypair.to_openssh_string(comment)?;

    // Format public key
    let comment_str = comment.unwrap_or("bssh-keygen");
    let public_key_openssh = format!(
        "{} {}",
        public_key.to_openssh_string()?,
        comment_str
    );

    // Write private key
    {
        use std::os::unix::fs::OpenOptionsExt;
        let mut file = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .mode(0o600)  // -rw-------
            .open(output_path)?;

        use std::io::Write;
        file.write_all(private_key_pem.as_bytes())?;
    }

    // Write public key
    let pub_path = format!("{}.pub", output_path.display());
    std::fs::write(&pub_path, &public_key_openssh)?;

    tracing::info!("Generated Ed25519 key: {}", output_path.display());
    tracing::info!("Fingerprint: {}", fingerprint);

    Ok(GeneratedKey {
        private_key_pem,
        public_key_openssh,
        fingerprint,
    })
}

3. RSA Key Generation

// src/keygen/rsa.rs
pub fn generate(output_path: &Path, bits: u32, comment: Option<&str>) -> Result<GeneratedKey> {
    // Validate key size
    if bits < 2048 {
        anyhow::bail!("RSA key size must be at least 2048 bits");
    }
    if bits > 16384 {
        anyhow::bail!("RSA key size must not exceed 16384 bits");
    }

    // Generate key pair
    let keypair = KeyPair::generate_rsa(bits as usize)
        .context("Failed to generate RSA key")?;

    // Same output format as Ed25519...
    let public_key = keypair.public_key();
    let fingerprint = public_key.fingerprint();

    let private_key_pem = keypair.to_openssh_string(comment)?;
    let comment_str = comment.unwrap_or("bssh-keygen");
    let public_key_openssh = format!(
        "{} {}",
        public_key.to_openssh_string()?,
        comment_str
    );

    // Write files with proper permissions
    {
        use std::os::unix::fs::OpenOptionsExt;
        let mut file = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .mode(0o600)
            .open(output_path)?;

        use std::io::Write;
        file.write_all(private_key_pem.as_bytes())?;
    }

    let pub_path = format!("{}.pub", output_path.display());
    std::fs::write(&pub_path, &public_key_openssh)?;

    tracing::info!("Generated RSA-{} key: {}", bits, output_path.display());
    tracing::info!("Fingerprint: {}", fingerprint);

    Ok(GeneratedKey {
        private_key_pem,
        public_key_openssh,
        fingerprint,
    })
}

4. CLI Binary

// src/bin/bssh_keygen.rs
use clap::Parser;
use std::path::PathBuf;

#[derive(Parser)]
#[command(name = "bssh-keygen")]
#[command(about = "Generate SSH key pairs in OpenSSH format")]
#[command(version)]
pub struct Cli {
    /// Key type: ed25519 (recommended) or rsa
    #[arg(short = 't', long, default_value = "ed25519")]
    key_type: String,

    /// Output file path
    #[arg(short = 'f', long)]
    output: Option<PathBuf>,

    /// RSA key bits (only for RSA, minimum 2048)
    #[arg(short = 'b', long, default_value = "4096")]
    bits: u32,

    /// Comment for the key
    #[arg(short = 'C', long)]
    comment: Option<String>,

    /// Overwrite existing files without prompting
    #[arg(short = 'y', long)]
    yes: bool,

    /// Quiet mode (no output except errors)
    #[arg(short = 'q', long)]
    quiet: bool,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    // Determine output path
    let output = cli.output.unwrap_or_else(|| {
        let home = dirs::home_dir().expect("Cannot determine home directory");
        match cli.key_type.as_str() {
            "ed25519" => home.join(".ssh/id_ed25519"),
            "rsa" => home.join(".ssh/id_rsa"),
            _ => home.join(".ssh/id_key"),
        }
    });

    // Check if file exists
    if output.exists() && !cli.yes {
        print!("{} already exists. Overwrite? (y/n) ", output.display());
        std::io::Write::flush(&mut std::io::stdout())?;

        let mut response = String::new();
        std::io::stdin().read_line(&mut response)?;
        if !response.trim().eq_ignore_ascii_case("y") {
            println!("Aborted.");
            return Ok(());
        }
    }

    // Generate key
    let result = match cli.key_type.to_lowercase().as_str() {
        "ed25519" => bssh::keygen::generate_ed25519(&output, cli.comment.as_deref())?,
        "rsa" => bssh::keygen::generate_rsa(&output, cli.bits, cli.comment.as_deref())?,
        other => anyhow::bail!("Unknown key type: {}. Use 'ed25519' or 'rsa'", other),
    };

    if !cli.quiet {
        println!("Your identification has been saved in {}", output.display());
        println!("Your public key has been saved in {}.pub", output.display());
        println!("The key fingerprint is:");
        println!("{}", result.fingerprint);
    }

    Ok(())
}

5. CLI Help Output

bssh-keygen 1.0.0
Generate SSH key pairs in OpenSSH format

USAGE:
    bssh-keygen [OPTIONS]

OPTIONS:
    -t, --key-type <TYPE>    Key type: ed25519 (recommended) or rsa [default: ed25519]
    -f, --output <FILE>      Output file path [default: ~/.ssh/id_<type>]
    -b, --bits <BITS>        RSA key bits (minimum 2048) [default: 4096]
    -C, --comment <COMMENT>  Comment for the key
    -y, --yes                Overwrite existing files without prompting
    -q, --quiet              Quiet mode
    -h, --help               Print help
    -V, --version            Print version

Files to Create/Modify

File Action
src/keygen/mod.rs Create - Module exports
src/keygen/ed25519.rs Create - Ed25519 generation
src/keygen/rsa.rs Create - RSA generation
src/bin/bssh_keygen.rs Create - CLI binary
src/lib.rs Modify - Add keygen module
Cargo.toml Modify - Add binary target

Testing Requirements

  1. Unit test: Ed25519 key generation
  2. Unit test: RSA key generation (various sizes)
  3. Unit test: Key file permissions (0600)
  4. Unit test: Public key format
  5. Integration test: Generated keys work with ssh-keygen -y
  6. Integration test: Generated keys work with OpenSSH
# Test Ed25519
bssh-keygen -t ed25519 -f /tmp/test_ed25519 -y
ssh-keygen -y -f /tmp/test_ed25519  # Should output same public key

# Test RSA
bssh-keygen -t rsa -b 4096 -f /tmp/test_rsa -y
ssh-keygen -y -f /tmp/test_rsa  # Should output same public key

# Verify key works
ssh-keygen -lf /tmp/test_ed25519.pub

Acceptance Criteria

  • Ed25519 key generation
  • RSA key generation (2048-16384 bits)
  • OpenSSH private key format
  • OpenSSH public key format (.pub file)
  • Proper file permissions (0600 for private key)
  • Comment support
  • Fingerprint display
  • Overwrite confirmation
  • Quiet mode
  • Keys compatible with OpenSSH
  • Tests passing

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions