Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions atomic-cli/src/commands/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ pub mod list;
pub mod new;
pub mod register;
pub mod show;
pub mod sign;
pub mod verify;
pub mod whoami;

// Re-export command structs
Expand All @@ -51,6 +53,8 @@ pub use list::List;
pub use new::New;
pub use register::Register;
pub use show::Show;
pub use sign::Sign;
pub use verify::Verify;
pub use whoami::WhoAmI;

use clap::Subcommand;
Expand Down Expand Up @@ -197,6 +201,38 @@ pub enum IdentityCommands {
/// atomic identity register https://atomic.storage --identity alice-work
/// ```
Register(Register),

/// Sign bytes from stdin and output a JSON signature object.
///
/// Reads raw bytes from stdin, signs them with the identity's Ed25519
/// secret key, and prints a JSON object containing the signature,
/// public key, identity name, and algorithm.
///
/// # Examples
///
/// ```text
/// # Sign with the default identity
/// atomic identity sign < file.bin
///
/// # Sign with a specific identity
/// echo -n "hello" | atomic identity sign --identity alice-work
/// ```
Sign(Sign),

/// Verify an Ed25519 signature against bytes from stdin.
///
/// Reads raw bytes from stdin and checks the provided signature
/// against the provided public key. Exits 0 if valid, 1 if invalid.
///
/// # Examples
///
/// ```text
/// atomic identity verify \
/// --signature <base64-sig> \
/// --public-key <base32-key> \
/// < file.bin
/// ```
Verify(Verify),
}

impl Command for Identity {
Expand All @@ -209,6 +245,8 @@ impl Command for Identity {
IdentityCommands::Delete(cmd) => cmd.run(),
IdentityCommands::WhoAmI(cmd) => cmd.run(),
IdentityCommands::Register(cmd) => cmd.run(),
IdentityCommands::Sign(cmd) => cmd.run(),
IdentityCommands::Verify(cmd) => cmd.run(),
}
}
}
Expand Down
86 changes: 86 additions & 0 deletions atomic-cli/src/commands/identity/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use clap::Parser;
use data_encoding::BASE64;
use std::io::Read;

use atomic_identity::IdentityStore;

use crate::commands::Command;
use crate::error::{CliError, CliResult};

/// Sign bytes from stdin using an identity's Ed25519 key.
///
/// Reads raw bytes from stdin and outputs a JSON object with the
/// signature, public key, identity name, and algorithm.
///
/// # Examples
///
/// ```text
/// # Sign a file with the default identity
/// atomic identity sign < file.bin
///
/// # Sign with a specific identity
/// atomic identity sign --identity alice-work < file.bin
///
/// # Sign a string
/// echo -n "hello world" | atomic identity sign
/// ```
#[derive(Debug, Parser)]
pub struct Sign {
/// Identity to sign with. Defaults to the current default identity.
#[arg(short, long)]
pub identity: Option<String>,
}

impl Command for Sign {
fn run(&self) -> CliResult<()> {
let store = IdentityStore::open_default().map_err(|e| {
CliError::Internal(anyhow::anyhow!("Failed to open identity store: {}", e))
})?;

let identity = if let Some(name) = &self.identity {
store
.load_by_name(name)
.map_err(|_| CliError::IdentityNotFound(name.clone()))?
} else {
store
.get_default()
.map_err(|e| {
CliError::Internal(anyhow::anyhow!("Failed to load default identity: {}", e))
})?
.ok_or_else(|| {
CliError::Internal(anyhow::anyhow!(
"No default identity set. Create one first:\n \
atomic identity new <name> --email <email> --set-default"
))
})?
};

let keypair = store.load_keypair(&identity.id, None).map_err(|e| {
CliError::Internal(anyhow::anyhow!(
"Failed to load keypair for '{}': {}",
identity.name,
e
))
})?;

let mut data = Vec::new();
std::io::stdin()
.read_to_end(&mut data)
.map_err(|e| CliError::Internal(anyhow::anyhow!("Failed to read stdin: {}", e)))?;

let sig_bytes = keypair.sign(&data);
let signature = BASE64.encode(&sig_bytes);
let public_key = identity.public_key_base32();
let name = &identity.name;

let output = serde_json::json!({
"signature": signature,
"public_key": public_key,
"identity": name,
"alg": "ed25519",
});

println!("{}", serde_json::to_string_pretty(&output).unwrap());
Ok(())
}
}
70 changes: 70 additions & 0 deletions atomic-cli/src/commands/identity/verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use clap::Parser;
use data_encoding::BASE64;
use std::io::Read;

use atomic_identity::keypair::PublicKey;

use crate::commands::Command;
use crate::error::{CliError, CliResult};

/// Verify an Ed25519 signature against bytes from stdin.
///
/// Reads raw bytes from stdin and checks the signature. Exits 0 if
/// valid, 1 if invalid.
///
/// # Examples
///
/// ```text
/// # Verify a signature
/// atomic identity verify \
/// --signature <base64-sig> \
/// --public-key <base32-key> \
/// < file.bin
/// ```
#[derive(Debug, Parser)]
pub struct Verify {
/// Base64-encoded signature to verify.
#[arg(long)]
pub signature: String,

/// Base32-encoded public key to verify against.
#[arg(long)]
pub public_key: String,
}

impl Command for Verify {
fn run(&self) -> CliResult<()> {
let sig_bytes = BASE64.decode(self.signature.as_bytes()).map_err(|e| {
CliError::Internal(anyhow::anyhow!("Invalid signature encoding: {}", e))
})?;

if sig_bytes.len() != 64 {
return Err(CliError::Internal(anyhow::anyhow!(
"Invalid signature: expected 64 bytes, got {}",
sig_bytes.len()
)));
}

let mut sig_arr = [0u8; 64];
sig_arr.copy_from_slice(&sig_bytes);

let public_key = PublicKey::from_base32(&self.public_key)
.map_err(|e| CliError::Internal(anyhow::anyhow!("Invalid public key: {}", e)))?;

let mut data = Vec::new();
std::io::stdin()
.read_to_end(&mut data)
.map_err(|e| CliError::Internal(anyhow::anyhow!("Failed to read stdin: {}", e)))?;

match public_key.verify(&data, &sig_arr) {
Ok(()) => {
eprintln!("Signature valid");
std::process::exit(0);
}
Err(_) => {
eprintln!("Signature invalid");
std::process::exit(1);
}
}
}
}
Loading