diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ee4099d..1284a5ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -168,7 +168,7 @@ jobs: run: | cp config/bootstrap_peers.toml target/${{ matrix.target }}/release/ cd target/${{ matrix.target }}/release - tar -czvf ../../../ant-node-cli-${{ matrix.friendly_name }}.tar.gz ${{ matrix.binary }} ant-keygen bootstrap_peers.toml + tar -czvf ../../../ant-node-cli-${{ matrix.friendly_name }}.tar.gz ${{ matrix.binary }} bootstrap_peers.toml cd ../../.. - name: Create archive (Windows) @@ -177,7 +177,7 @@ jobs: run: | Copy-Item "config/bootstrap_peers.toml" "target/${{ matrix.target }}/release/bootstrap_peers.toml" Push-Location "target/${{ matrix.target }}/release" - Compress-Archive -Path "${{ matrix.binary }}", "ant-keygen.exe", "bootstrap_peers.toml" -DestinationPath "../../../ant-node-cli-${{ matrix.friendly_name }}.zip" + Compress-Archive -Path "${{ matrix.binary }}", "bootstrap_peers.toml" -DestinationPath "../../../ant-node-cli-${{ matrix.friendly_name }}.zip" Pop-Location - name: Upload artifact @@ -192,13 +192,6 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - - uses: Swatinem/rust-cache@v2 - - name: Download all artifacts uses: actions/download-artifact@v4 with: @@ -208,20 +201,25 @@ jobs: - name: List artifacts run: ls -la artifacts/ + - name: Download ant-keygen + run: | + gh release download --repo WithAutonomi/ant-keygen --pattern 'ant-keygen-linux-x64.tar.gz' --dir /tmp + tar -xzf /tmp/ant-keygen-linux-x64.tar.gz -C /tmp + chmod +x /tmp/ant-keygen + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Decode signing key run: | echo "${{ secrets.ANT_NODE_SIGNING_KEY }}" | xxd -r -p > /tmp/signing-key.secret chmod 600 /tmp/signing-key.secret - - name: Build signing tool - run: cargo build --release --bin ant-keygen - - name: Sign all release files run: | for file in artifacts/ant-node-cli-*.tar.gz artifacts/ant-node-cli-*.zip; do if [ -f "$file" ]; then echo "Signing $file..." - ./target/release/ant-keygen sign \ + /tmp/ant-keygen sign \ --key /tmp/signing-key.secret \ --input "$file" \ --output "${file}.sig" @@ -317,7 +315,8 @@ jobs: ### Verification All downloads are signed with ML-DSA-65 (FIPS 204) post-quantum signatures. - Download the corresponding `.sig` file and verify: + Download `ant-keygen` from [WithAutonomi/ant-keygen](https://github.com/WithAutonomi/ant-keygen/releases) + and verify: ```bash ant-keygen verify --key release-signing-key.pub --input --signature .sig diff --git a/Cargo.lock b/Cargo.lock index 7f98bc13..7658a3e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5217,6 +5217,8 @@ dependencies = [ [[package]] name = "saorsa-core" version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d3d05b97f789b0e0b7d54b2fe05f05edfafb94f72d065482fc20ce1e9fab69e" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index c6c46e4c..fbec7554 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,6 @@ path = "src/lib.rs" name = "ant-node" path = "src/bin/ant-node/main.rs" -[[bin]] -name = "ant-keygen" -path = "src/bin/keygen.rs" - [[bin]] name = "ant-devnet" path = "src/bin/ant-devnet/main.rs" diff --git a/deploy/scripts/spawn-nodes.sh b/deploy/scripts/spawn-nodes.sh index bb5aacb8..49f741c4 100755 --- a/deploy/scripts/spawn-nodes.sh +++ b/deploy/scripts/spawn-nodes.sh @@ -40,7 +40,6 @@ download_binary() { echo "Extracting..." tar -xzf /tmp/ant-node.tar.gz -C /tmp mv /tmp/ant-node "$BINARY_PATH" - mv /tmp/ant-keygen /usr/local/bin/ant-keygen 2>/dev/null || true chmod +x "$BINARY_PATH" rm -f /tmp/ant-node.tar.gz diff --git a/deploy/terraform/cloud-init/worker.yml b/deploy/terraform/cloud-init/worker.yml index 7a916a5d..faf74877 100644 --- a/deploy/terraform/cloud-init/worker.yml +++ b/deploy/terraform/cloud-init/worker.yml @@ -38,18 +38,16 @@ write_files: ARCHIVE_URL="https://github.com/WithAutonomi/ant-node/releases/download/v$${ANT_VERSION}/ant-node-cli-$${PLATFORM}.tar.gz" SIG_URL="$${ARCHIVE_URL}.sig" BINARY_PATH="/usr/local/bin/ant-node" - KEYGEN_PATH="/usr/local/bin/ant-keygen" echo "Downloading ant-node v$${ANT_VERSION} for $${PLATFORM}..." cd /tmp curl -L -o ant-node.tar.gz "$${ARCHIVE_URL}" curl -L -o ant-node.tar.gz.sig "$${SIG_URL}" || echo "No signature file (dev release)" - # Extract binaries + # Extract binary tar -xzf ant-node.tar.gz mv ant-node "$${BINARY_PATH}" - mv ant-keygen "$${KEYGEN_PATH}" || true - chmod +x "$${BINARY_PATH}" "$${KEYGEN_PATH}" 2>/dev/null || true + chmod +x "$${BINARY_PATH}" rm -f ant-node.tar.gz ant-node.tar.gz.sig echo "Installed ant-node v$${ANT_VERSION}" diff --git a/scripts/testnet/build-and-deploy.sh b/scripts/testnet/build-and-deploy.sh index 2f8e4658..bf3ac2b0 100755 --- a/scripts/testnet/build-and-deploy.sh +++ b/scripts/testnet/build-and-deploy.sh @@ -66,7 +66,6 @@ ssh -o StrictHostKeyChecking=no "root@${BUILD_HOST}" " echo "=== Installing on build host ===" ssh -o StrictHostKeyChecking=no "root@${BUILD_HOST}" " cp /root/ant-node/target/release/ant-node /usr/local/bin/ - cp /root/ant-node/target/release/ant-keygen /usr/local/bin/ 2>/dev/null || true chmod +x /usr/local/bin/ant-node /usr/local/bin/ant-node --version " diff --git a/scripts/testnet/deploy-all.sh b/scripts/testnet/deploy-all.sh index 389181fe..40aa7e72 100755 --- a/scripts/testnet/deploy-all.sh +++ b/scripts/testnet/deploy-all.sh @@ -44,7 +44,6 @@ for i in "${!WORKERS[@]}"; do curl -sL '${BINARY_URL}' -o ant-node.tar.gz tar xzf ant-node.tar.gz mv ant-node /usr/local/bin/ - mv ant-keygen /usr/local/bin/ 2>/dev/null || true chmod +x /usr/local/bin/ant-node rm -f ant-node.tar.gz /usr/local/bin/ant-node --version diff --git a/src/bin/keygen.rs b/src/bin/keygen.rs deleted file mode 100644 index a0b565cd..00000000 --- a/src/bin/keygen.rs +++ /dev/null @@ -1,323 +0,0 @@ -//! ML-DSA-65 key management utility for ant-node release signing. -//! -//! This utility provides: -//! - Keypair generation for release signing -//! - Binary signing with ML-DSA-65 -//! - Signature verification -//! -//! # Usage -//! -//! ```text -//! ant-keygen generate [output-dir] Generate a new keypair -//! ant-keygen sign --key --input --output -//! ant-keygen verify --key --input --signature -//! ``` - -// This is a standalone CLI tool that exits on any error, so expect/unwrap is acceptable -#![allow(clippy::unwrap_used, clippy::expect_used)] - -use clap::{Parser, Subcommand}; -use saorsa_pqc::api::sig::{ - ml_dsa_65, MlDsaPublicKey, MlDsaSecretKey, MlDsaSignature, MlDsaVariant, -}; -use std::fs; -use std::io::Write; -use std::path::PathBuf; -use std::process; - -/// Signing context for domain separation (prevents cross-protocol attacks). -const SIGNING_CONTEXT: &[u8] = b"ant-node-release-v1"; - -#[derive(Parser)] -#[command(name = "ant-keygen")] -#[command(about = "ML-DSA-65 key management for ant-node releases")] -#[command(version)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Generate a new ML-DSA-65 keypair - Generate { - /// Output directory for keys - #[arg(default_value = ".")] - output_dir: PathBuf, - }, - /// Sign a file with ML-DSA-65 - Sign { - /// Path to the secret key file - #[arg(short, long)] - key: PathBuf, - /// Path to the file to sign - #[arg(short, long)] - input: PathBuf, - /// Path to write the signature - #[arg(short, long)] - output: PathBuf, - }, - /// Verify a signature - Verify { - /// Path to the public key file - #[arg(short, long)] - key: PathBuf, - /// Path to the file that was signed - #[arg(short, long)] - input: PathBuf, - /// Path to the signature file - #[arg(short, long)] - signature: PathBuf, - }, - /// Validate a hex-encoded secret key (for CI secret verification) - VerifyKey { - /// Hex-encoded secret key string (reads from stdin if not provided) - #[arg(short = 'x', long)] - hex: Option, - }, -} - -fn main() { - let cli = Cli::parse(); - - match cli.command { - Commands::Generate { output_dir } => generate_keypair(&output_dir), - Commands::Sign { key, input, output } => sign_file(&key, &input, &output), - Commands::Verify { - key, - input, - signature, - } => verify_signature(&key, &input, &signature), - Commands::VerifyKey { hex } => verify_hex_key(hex), - } -} - -fn generate_keypair(output_dir: &PathBuf) { - println!("ML-DSA-65 Keypair Generator for ant-node releases\n"); - - // Create output directory if it doesn't exist - fs::create_dir_all(output_dir).expect("Failed to create output directory"); - - println!("Generating ML-DSA-65 keypair..."); - - // Generate keypair - let dsa = ml_dsa_65(); - let (public_key, secret_key) = dsa.generate_keypair().expect("Failed to generate keypair"); - - let pk_bytes = public_key.to_bytes(); - let sk_bytes = secret_key.to_bytes(); - - println!(" Public key size: {} bytes", pk_bytes.len()); - println!(" Secret key size: {} bytes", sk_bytes.len()); - - // Save secret key to file (KEEP THIS SECURE!) - let sk_path = output_dir.join("release-signing-key.secret"); - fs::write(&sk_path, sk_bytes).expect("Failed to write secret key"); - println!("\nSecret key saved to: {}", sk_path.display()); - println!(" WARNING: Keep this file secure! It's needed for signing releases."); - - // Save public key to file - let pk_path = output_dir.join("release-signing-key.pub"); - fs::write(&pk_path, &pk_bytes).expect("Failed to write public key"); - println!("Public key saved to: {}", pk_path.display()); - - // Generate Rust code for embedding - let rust_code_path = output_dir.join("release_key_embed.rs"); - let mut rust_file = fs::File::create(&rust_code_path).expect("Failed to create Rust file"); - - writeln!( - rust_file, - "/// Embedded release signing public key (ML-DSA-65)." - ) - .unwrap(); - writeln!(rust_file, "///").unwrap(); - writeln!( - rust_file, - "/// This key is used to verify signatures on released binaries." - ) - .unwrap(); - writeln!( - rust_file, - "/// The corresponding private key is held by authorized release signers." - ) - .unwrap(); - writeln!( - rust_file, - "/// Generated: {}", - chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") - ) - .unwrap(); - writeln!(rust_file, "const RELEASE_SIGNING_KEY: &[u8] = &[").unwrap(); - - // Write bytes in rows of 16 for readability - for (i, byte) in pk_bytes.iter().enumerate() { - if i % 16 == 0 { - write!(rust_file, " ").unwrap(); - } - write!(rust_file, "0x{byte:02x},").unwrap(); - if i % 16 == 15 { - writeln!(rust_file).unwrap(); - } else { - write!(rust_file, " ").unwrap(); - } - } - - // Handle last line if not complete - if pk_bytes.len() % 16 != 0 { - writeln!(rust_file).unwrap(); - } - - writeln!(rust_file, "];").unwrap(); - - println!("Rust embed code saved to: {}", rust_code_path.display()); - - // Also print to stdout for convenience - println!("\n--- Rust code for signature.rs ---\n"); - println!("const RELEASE_SIGNING_KEY: &[u8] = &["); - for (i, byte) in pk_bytes.iter().enumerate() { - if i % 16 == 0 { - print!(" "); - } - print!("0x{byte:02x},"); - if i % 16 == 15 { - println!(); - } else { - print!(" "); - } - } - if pk_bytes.len() % 16 != 0 { - println!(); - } - println!("];"); - - println!("\n--- End of Rust code ---"); - println!("\nDone! Copy the above code to src/upgrade/signature.rs"); -} - -fn sign_file(key_path: &PathBuf, input_path: &PathBuf, output_path: &PathBuf) { - println!("Signing {} with ML-DSA-65...", input_path.display()); - - // Load secret key - let sk_bytes = fs::read(key_path).expect("Failed to read secret key"); - - // Parse secret key - let secret_key = MlDsaSecretKey::from_bytes(MlDsaVariant::MlDsa65, &sk_bytes) - .expect("Failed to parse secret key"); - - // Load file to sign - let data = fs::read(input_path).expect("Failed to read input file"); - - // Create DSA instance and sign with context - let dsa = ml_dsa_65(); - let signature = dsa - .sign_with_context(&secret_key, &data, SIGNING_CONTEXT) - .expect("Failed to create signature"); - - let sig_bytes = signature.to_bytes(); - - // Write signature - fs::write(output_path, &sig_bytes).expect("Failed to write signature"); - - println!("Signature written to: {}", output_path.display()); - println!(" Signature size: {} bytes", sig_bytes.len()); -} - -fn verify_hex_key(hex_input: Option) { - println!("Validating hex-encoded ML-DSA-65 secret key...\n"); - - let hex_str = hex_input.unwrap_or_else(|| { - eprintln!("Reading hex from stdin..."); - let mut buf = String::new(); - std::io::Read::read_to_string(&mut std::io::stdin(), &mut buf) - .expect("Failed to read from stdin"); - buf - }); - - // Strip whitespace/newlines that might have been introduced during copy-paste - let hex_clean: String = hex_str.chars().filter(char::is_ascii_hexdigit).collect(); - - println!("Hex length: {} characters", hex_clean.len()); - println!("Expected: {} characters ({} bytes * 2)", 4032 * 2, 4032); - - if hex_clean.len() != 4032 * 2 { - eprintln!( - "\nERROR: Hex string decodes to {} bytes, expected 4032", - hex_clean.len() / 2 - ); - process::exit(1); - } - - // Decode hex to bytes - let bytes: Vec = (0..hex_clean.len()) - .step_by(2) - .map(|i| { - u8::from_str_radix(&hex_clean[i..i + 2], 16).unwrap_or_else(|e| { - eprintln!("ERROR: Invalid hex at position {i}: {e}"); - process::exit(1); - }) - }) - .collect(); - - println!("Decoded: {} bytes", bytes.len()); - - // Try to parse as ML-DSA-65 secret key - match MlDsaSecretKey::from_bytes(MlDsaVariant::MlDsa65, &bytes) { - Ok(secret_key) => { - println!("\nSecret key parsed successfully."); - - // Test-sign something to confirm it's functional - let dsa = ml_dsa_65(); - let test_data = b"ant-node-key-validation-test"; - match dsa.sign_with_context(&secret_key, test_data, SIGNING_CONTEXT) { - Ok(_) => println!("Test signature created successfully."), - Err(e) => { - eprintln!("\nERROR: Key parsed but signing failed: {e}"); - process::exit(1); - } - } - - println!("\nKey is VALID and ready for use as ANT_NODE_SIGNING_KEY."); - } - Err(e) => { - eprintln!("\nERROR: Failed to parse secret key: {e}"); - process::exit(1); - } - } -} - -fn verify_signature(key_path: &PathBuf, input_path: &PathBuf, sig_path: &PathBuf) { - println!("Verifying signature for {}...", input_path.display()); - - // Load public key - let pk_bytes = fs::read(key_path).expect("Failed to read public key"); - - // Parse public key - let public_key = MlDsaPublicKey::from_bytes(MlDsaVariant::MlDsa65, &pk_bytes) - .expect("Failed to parse public key"); - - // Load file that was signed - let data = fs::read(input_path).expect("Failed to read input file"); - - // Load signature - let sig_bytes = fs::read(sig_path).expect("Failed to read signature"); - - // Parse signature - let signature = MlDsaSignature::from_bytes(MlDsaVariant::MlDsa65, &sig_bytes) - .expect("Failed to parse signature"); - - // Create DSA instance and verify with context - let dsa = ml_dsa_65(); - match dsa.verify_with_context(&public_key, &data, &signature, SIGNING_CONTEXT) { - Ok(true) => { - println!("Signature is VALID"); - } - Ok(false) => { - eprintln!("Signature is INVALID"); - process::exit(1); - } - Err(e) => { - eprintln!("Signature verification error: {e}"); - process::exit(1); - } - } -}