anubis-rage 1.4.0

Post-quantum secure file encryption tool with hybrid X25519+ML-KEM-1024. Defense-in-depth security.
Documentation
//! anubis-rage-sign: Sign encrypted age files with ML-DSA-87 signatures.

#![forbid(unsafe_code)]

use anubis_age::pqc::mldsa::SigningKey;
use anubis_age::pqc::signed;
use std::fs;
use std::io::{self, Read, Write};
use std::path::PathBuf;

use clap::Parser;

#[derive(Debug, Parser)]
#[command(
    name = "anubis-rage-sign",
    about = "Sign encrypted age files with post-quantum ML-DSA-87 signatures",
    version
)]
struct Opts {
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Parser)]
enum Command {
    /// Generate a new ML-DSA-87 signing key
    Keygen {
        /// Output file for the signing key
        #[arg(short, long, value_name = "OUTPUT")]
        output: PathBuf,
    },

    /// Extract the verification key from a signing key
    Extract {
        /// Input signing key file
        #[arg(short, long, value_name = "IDENTITY")]
        identity: PathBuf,
    },

    /// Sign an encrypted age file
    Sign {
        /// Signing key file
        #[arg(short = 'k', long, value_name = "KEY")]
        signing_key: PathBuf,

        /// Input encrypted age file
        #[arg(short, long, value_name = "INPUT")]
        input: Option<PathBuf>,

        /// Output signed file
        #[arg(short, long, value_name = "OUTPUT")]
        output: Option<PathBuf>,
    },

    /// Verify a signed age file
    Verify {
        /// Verification key (Bech32 string or file)
        #[arg(short = 'k', long, value_name = "KEY")]
        verification_key: String,

        /// Input signed file
        #[arg(short, long, value_name = "INPUT")]
        input: Option<PathBuf>,

        /// Output unsigned encrypted file
        #[arg(short, long, value_name = "OUTPUT")]
        output: Option<PathBuf>,
    },
}

fn main() -> io::Result<()> {
    let opts = Opts::parse();

    match opts.command {
        Command::Keygen { output } => {
            let signing_key = SigningKey::generate();
            let key_str = signing_key.to_string();

            let content = format!(
                "# created: {}\n# verification key: {}\n{}\n",
                chrono::Local::now().to_rfc3339(),
                signing_key.to_public(),
                key_str
            );
            fs::write(&output, content)?;

            eprintln!("Generated ML-DSA-87 signing key: {}", output.display());
            eprintln!("Verification key: {}", signing_key.to_public());

            Ok(())
        }

        Command::Extract { identity } => {
            let key_contents = fs::read_to_string(&identity)?;
            let signing_key: SigningKey = key_contents
                .lines()
                .find(|line| line.starts_with("ANUBIS-MLDSA-87-SECRET"))
                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no signing key found"))?
                .parse()
                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

            println!("{}", signing_key.to_public());
            Ok(())
        }

        Command::Sign {
            signing_key: key_path,
            input,
            output,
        } => {
            // Load signing key
            let key_contents = fs::read_to_string(&key_path)?;
            let signing_key: SigningKey = key_contents
                .lines()
                .find(|line| line.starts_with("ANUBIS-MLDSA-87-SECRET"))
                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "no signing key found"))?
                .parse()
                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

            // Read encrypted file
            let encrypted_data = if let Some(input_path) = input {
                fs::read(input_path)?
            } else {
                let mut data = Vec::new();
                io::stdin().read_to_end(&mut data)?;
                data
            };

            // Sign
            let mut signed_output = Vec::new();
            signed::sign_encrypted_file(&encrypted_data, &signing_key, &mut signed_output)?;

            // Write output
            if let Some(output_path) = output {
                fs::write(output_path, signed_output)?;
            } else {
                io::stdout().write_all(&signed_output)?;
            }

            eprintln!("File signed with ML-DSA-87");
            Ok(())
        }

        Command::Verify {
            verification_key: key_str,
            input,
            output,
        } => {
            // Parse verification key
            let verification_key = if let Ok(vk) = key_str.parse() {
                vk
            } else {
                // Try to read as file
                let key_contents = fs::read_to_string(&key_str)?;
                key_contents
                    .lines()
                    .find(|line| line.starts_with("anubis1mldsa87"))
                    .ok_or_else(|| {
                        io::Error::new(io::ErrorKind::InvalidData, "no verification key found")
                    })?
                    .parse()
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
            };

            // Read signed file
            let signed_data = if let Some(input_path) = input {
                fs::read(input_path)?
            } else {
                let mut data = Vec::new();
                io::stdin().read_to_end(&mut data)?;
                data
            };

            // Verify and extract
            let encrypted_data = signed::verify_and_extract(&signed_data, &verification_key)?;

            // Write output
            if let Some(output_path) = output {
                fs::write(output_path, encrypted_data)?;
            } else {
                io::stdout().write_all(&encrypted_data)?;
            }

            eprintln!("Signature verified successfully");
            Ok(())
        }
    }
}