Skip to content
Merged
11 changes: 5 additions & 6 deletions crates/auths-cli/src/bin/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ fn run_sign(args: &Args) -> Result<()> {
mod tests {
use super::*;
use auths_core::crypto::ssh::construct_sshsig_signed_data;
use zeroize::Zeroizing;
use auths_crypto::Pkcs8Der;

#[test]
fn test_args_accepts_o_flag() {
Expand Down Expand Up @@ -458,9 +458,9 @@ mod tests {
let rng = SystemRandom::new();
let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng)
.expect("ring must generate a valid PKCS#8 document");
let pkcs8_bytes = Zeroizing::new(pkcs8_doc.as_ref().to_vec());
let pkcs8 = Pkcs8Der::new(pkcs8_doc.as_ref());

let result = extract_seed_from_pkcs8(&pkcs8_bytes);
let result = extract_seed_from_pkcs8(&pkcs8);
assert!(
result.is_ok(),
"extract_seed_from_pkcs8 must succeed on a ring-generated key, got: {:?}",
Expand All @@ -472,8 +472,7 @@ mod tests {

let derived = Ed25519KeyPair::from_seed_unchecked(seed.as_bytes())
.expect("extracted seed must be valid");
let original =
Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).expect("original key must parse");
let original = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).expect("original key must parse");
assert_eq!(
derived.public_key().as_ref(),
original.public_key().as_ref(),
Expand All @@ -485,7 +484,7 @@ mod tests {
fn test_extract_seed_from_pkcs8_rejects_invalid_input() {
use auths_core::crypto::ssh::extract_seed_from_pkcs8;

let bad_input = Zeroizing::new(vec![0u8; 50]);
let bad_input = Pkcs8Der::new(vec![0u8; 50]);
let result = extract_seed_from_pkcs8(&bad_input);
assert!(result.is_err(), "must reject non-PKCS#8 input");
}
Expand Down
19 changes: 18 additions & 1 deletion crates/auths-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::commands::sign::SignCommand;
use crate::commands::status::StatusCommand;
use crate::commands::trust::TrustCommand;
use crate::commands::unified_verify::UnifiedVerifyCommand;
use crate::commands::whoami::WhoamiCommand;
use crate::commands::witness::WitnessCommand;
use crate::config::OutputFormat;

Expand All @@ -44,7 +45,7 @@ fn cli_styles() -> Styles {
#[command(
name = "auths",
about = "auths \u{2014} cryptographic identity for developers",
long_about = "Commands:\n init Set up your cryptographic identity and Git signing\n sign Sign a Git commit or artifact\n verify Verify a signed commit or attestation\n status Show identity and signing status\n\nMore commands (run with --help for details):\n auths device, auths id, auths key, auths policy, auths emergency, ...",
long_about = "auths \u{2014} cryptographic identity for developers\n\nCore commands:\n init Set up your cryptographic identity and Git signing\n sign Sign a Git commit or artifact\n verify Verify a signed commit or attestation\n status Show identity and signing status\n\nMore commands:\n id, device, key, approval, artifact, policy, git, trust, org,\n audit, agent, witness, scim, config, emergency\n\nRun `auths <command> --help` for details on any command.",
version,
styles = cli_styles()
)]
Expand Down Expand Up @@ -97,24 +98,40 @@ pub enum RootCommand {
Sign(SignCommand),
Verify(UnifiedVerifyCommand),
Status(StatusCommand),
Whoami(WhoamiCommand),
Tutorial(LearnCommand),
Doctor(DoctorCommand),
Completions(CompletionsCommand),
#[command(hide = true)]
Emergency(EmergencyCommand),

#[command(hide = true)]
Id(IdCommand),
#[command(hide = true)]
Device(DeviceCommand),
#[command(hide = true)]
Key(KeyCommand),
#[command(hide = true)]
Approval(ApprovalCommand),
#[command(hide = true)]
Artifact(ArtifactCommand),
#[command(hide = true)]
Policy(PolicyCommand),
#[command(hide = true)]
Git(GitCommand),
#[command(hide = true)]
Trust(TrustCommand),
#[command(hide = true)]
Org(OrgCommand),
#[command(hide = true)]
Audit(AuditCommand),
#[command(hide = true)]
Agent(AgentCommand),
#[command(hide = true)]
Witness(WitnessCommand),
#[command(hide = true)]
Scim(ScimCommand),
#[command(hide = true)]
Config(ConfigCommand),

#[command(hide = true)]
Expand Down
40 changes: 40 additions & 0 deletions crates/auths-cli/src/commands/device/authorization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ pub enum DeviceSubcommand {

#[arg(long, help = "Optional note explaining the revocation.")]
note: Option<String>,

#[arg(long, help = "Preview actions without making changes.")]
dry_run: bool,
},

/// Resolve a device DID to its controller identity DID.
Expand Down Expand Up @@ -276,7 +279,12 @@ pub fn handle_device(
device_did,
identity_key_alias,
note,
dry_run,
} => {
if dry_run {
return display_dry_run_revoke(&device_did, &identity_key_alias);
}

let ctx = build_auths_context(
&repo_path,
env_config,
Expand Down Expand Up @@ -325,6 +333,38 @@ fn display_link_result(
Ok(())
}

fn display_dry_run_revoke(device_did: &str, identity_key_alias: &str) -> Result<()> {
if is_json_mode() {
JsonResponse::success(
"device revoke",
&serde_json::json!({
"dry_run": true,
"device_did": device_did,
"identity_key_alias": identity_key_alias,
"actions": [
"Revoke device authorization",
"Create signed revocation attestation",
"Store revocation in Git repository"
]
}),
)
.print()
.map_err(|e| anyhow!("{e}"))
} else {
let out = crate::ux::format::Output::new();
out.print_info("Dry run mode — no changes will be made");
out.newline();
out.println("Would perform the following actions:");
out.println(&format!(
" 1. Revoke device authorization for {}",
device_did
));
out.println(" 2. Create signed revocation attestation");
out.println(" 3. Store revocation in Git repository");
Ok(())
}
}

fn display_revoke_result(device_did: &str, repo_path: &Path) -> Result<()> {
let identity_storage = RegistryIdentityStorage::new(repo_path.to_path_buf());
let identity: ManagedIdentity = identity_storage
Expand Down
13 changes: 9 additions & 4 deletions crates/auths-cli/src/commands/device/pair/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub struct PairCommand {
pub registry: Option<String>,

/// Don't display QR code (only show short code)
#[clap(long)]
#[clap(long, hide_short_help = true)]
pub no_qr: bool,

/// Custom timeout in seconds for the pairing session (default: 300 = 5 minutes)
Expand All @@ -47,16 +47,21 @@ pub struct PairCommand {
pub timeout: u64,

/// Skip registry server (offline mode, for testing)
#[clap(long)]
#[clap(long, hide_short_help = true)]
pub offline: bool,

/// Capabilities to grant the paired device (comma-separated)
#[clap(long, value_delimiter = ',', default_value = "sign_commit")]
#[clap(
long,
value_delimiter = ',',
default_value = "sign_commit",
hide_short_help = true
)]
pub capabilities: Vec<String>,

/// Disable mDNS advertisement/discovery in LAN mode
#[cfg(feature = "lan-pairing")]
#[clap(long)]
#[clap(long, hide_short_help = true)]
pub no_mdns: bool,
}

Expand Down
57 changes: 57 additions & 0 deletions crates/auths-cli/src/commands/id/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ pub enum IdSubcommand {
help = "New simple verification threshold count (e.g., 1 for 1-of-N)."
)]
witness_threshold: Option<u64>,

/// Preview actions without making changes.
#[arg(long)]
dry_run: bool,
},

/// Export an identity bundle for stateless CI/CD verification.
Expand Down Expand Up @@ -196,6 +200,50 @@ pub enum IdSubcommand {
Migrate(super::migrate::MigrateCommand),
}

fn display_dry_run_rotate(
repo_path: &std::path::Path,
current_alias: Option<&str>,
next_alias: Option<&str>,
) -> Result<()> {
if is_json_mode() {
JsonResponse::success(
"id rotate",
&serde_json::json!({
"dry_run": true,
"repo_path": repo_path.display().to_string(),
"current_key_alias": current_alias,
"next_key_alias": next_alias,
"actions": [
"Generate new Ed25519 keypair",
"Create rotation event in KERI event log",
"Update key alias mappings",
"All devices will need to re-authorize"
]
}),
)
.print()
.map_err(|e| anyhow!("{e}"))
} else {
let out = crate::ux::format::Output::new();
out.print_info("Dry run mode — no changes will be made");
out.newline();
out.println(&format!(" Repository: {:?}", repo_path));
if let Some(alias) = current_alias {
out.println(&format!(" Current Key Alias: {}", alias));
}
if let Some(alias) = next_alias {
out.println(&format!(" New Key Alias: {}", alias));
}
out.newline();
out.println("Would perform the following actions:");
out.println(" 1. Generate new Ed25519 keypair");
out.println(" 2. Create rotation event in KERI event log");
out.println(" 3. Update key alias mappings");
out.println(" 4. All devices will need to re-authorize");
Ok(())
}
}

// --- Command Handler ---

/// Handles the `id` subcommand, accepting the specific subcommand details
Expand Down Expand Up @@ -425,9 +473,18 @@ pub fn handle_id(
add_witness,
remove_witness,
witness_threshold,
dry_run,
} => {
let identity_key_alias = alias.or(current_key_alias);

if dry_run {
return display_dry_run_rotate(
&repo_path,
identity_key_alias.as_deref(),
next_key_alias.as_deref(),
);
}

println!("🔄 Rotating KERI identity keys...");
println!(" Using Repository: {:?}", repo_path);
if let Some(ref a) = identity_key_alias {
Expand Down
26 changes: 7 additions & 19 deletions crates/auths-cli/src/commands/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::factories::storage::build_auths_context;

use super::init_helpers::{
check_git_version, detect_ci_environment, get_auths_repo_path, offer_shell_completions,
select_agent_capabilities, short_did, write_allowed_signers,
select_agent_capabilities, write_allowed_signers,
};
use crate::config::CliConfig;
use crate::ux::format::Output;
Expand Down Expand Up @@ -179,14 +179,8 @@ pub fn handle_init(cmd: InitCommand, ctx: &CliConfig) -> Result<()> {
_ => unreachable!(),
};

out.print_success(&format!(
"Identity ready: {}",
short_did(&result.identity_did)
));
out.print_success(&format!(
"Device linked: {}",
short_did(result.device_did.as_str())
));
out.print_success(&format!("Identity ready: {}", &result.identity_did));
out.print_success(&format!("Device linked: {}", result.device_did.as_str()));
out.newline();

// Post-execute: platform verification (interactive CLI concern)
Expand Down Expand Up @@ -452,7 +446,7 @@ fn prompt_for_conflict_policy(
if let Ok(existing) = identity_storage.load_identity() {
out.println(&format!(
" Found existing identity: {}",
out.info(&short_did(existing.controller_did.as_str()))
out.info(existing.controller_did.as_str())
));

if !interactive {
Expand Down Expand Up @@ -627,10 +621,7 @@ fn display_developer_result(
out.newline();
out.print_heading("You are on the Web of Trust!");
out.newline();
out.println(&format!(
" Identity: {}",
out.info(&short_did(&result.identity_did))
));
out.println(&format!(" Identity: {}", out.info(&result.identity_did)));
out.println(&format!(" Key alias: {}", out.info(&result.key_alias)));
if let Some(registry) = registered {
out.println(&format!(" Registry: {}", out.info(registry)));
Expand All @@ -653,7 +644,7 @@ fn display_ci_result(
result: &auths_sdk::result::CiIdentityResult,
ci_vendor: Option<&str>,
) {
out.print_success(&format!("CI identity: {}", short_did(&result.identity_did)));
out.print_success(&format!("CI identity: {}", &result.identity_did));
out.newline();

out.print_heading("Add these to your CI secrets:");
Expand All @@ -676,10 +667,7 @@ fn display_ci_result(
fn display_agent_result(out: &Output, result: &auths_sdk::result::AgentIdentityResult) {
out.print_heading("Agent Setup Complete!");
out.newline();
out.println(&format!(
" Identity: {}",
out.info(&short_did(&result.agent_did))
));
out.println(&format!(" Identity: {}", out.info(&result.agent_did)));
let cap_display: Vec<String> = result.capabilities.iter().map(|c| c.to_string()).collect();
out.println(&format!(" Capabilities: {}", cap_display.join(", ")));
out.newline();
Expand Down
17 changes: 0 additions & 17 deletions crates/auths-cli/src/commands/init_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ pub(crate) fn get_auths_repo_path() -> Result<PathBuf> {
auths_core::paths::auths_home().map_err(|e| anyhow!(e))
}

pub(crate) fn short_did(did: &str) -> String {
if did.len() <= 24 {
did.to_string()
} else {
format!("{}...{}", &did[..16], &did[did.len() - 8..])
}
}

pub(crate) fn check_git_version(out: &Output) -> Result<()> {
let output = Command::new("git")
.arg("--version")
Expand Down Expand Up @@ -340,15 +332,6 @@ mod tests {
assert_eq!(parse_git_version("git version 2.30").unwrap(), (2, 30, 0));
}

#[test]
fn test_short_did() {
assert_eq!(short_did("did:key:z123"), "did:key:z123");
assert_eq!(
short_did("did:keri:EAbcdefghijklmnopqrstuvwxyz123456789"),
"did:keri:EAbcdef...23456789"
);
}

#[test]
fn test_min_git_version() {
assert!(MIN_GIT_VERSION <= (2, 34, 0));
Expand Down
1 change: 1 addition & 0 deletions crates/auths-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ pub mod unified_verify;
pub mod utils;
pub mod verify_commit;
pub mod verify_helpers;
pub mod whoami;
pub mod witness;
Loading
Loading