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 <file> --signature <file>.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 <key> --input <file> --output <sig>
-//! ant-keygen verify --key <key> --input <file> --signature <sig>
-//! ```
-
-// 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<String>,
-    },
-}
-
-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<String>) {
-    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<u8> = (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);
-        }
-    }
-}
