use anyhow::{Context, Result, anyhow};
use clap::{Parser, Subcommand};
use ring::signature::KeyPair;
use std::convert::TryInto;
use std::path::PathBuf;
use auths_crypto::{ed25519_pubkey_to_did_key, openssh_pub_to_raw_ed25519};
use auths_id::identity::helpers::{encode_seed_as_pkcs8, load_keypair_from_der_or_seed};
use crate::commands::device::verify_attestation::handle_verify_attestation;
#[derive(Parser, Debug, Clone)]
#[command(name = "util", about = "Utility commands for common operations.")]
pub struct UtilCommand {
#[command(subcommand)]
pub command: UtilSubcommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum UtilSubcommand {
DeriveDid {
#[arg(
long,
help = "The 32-byte Ed25519 seed encoded as a 64-character hex string."
)]
seed_hex: String,
},
#[command(name = "pubkey-to-did")]
PubkeyToDid {
#[arg(help = "OpenSSH Ed25519 public key line.")]
openssh_pub: String,
},
VerifyAttestation {
#[arg(long, value_parser, value_name = "FILE_PATH")]
attestation_file: PathBuf,
#[arg(long, value_name = "HEX_PUBKEY")]
issuer_pubkey: String,
},
}
pub fn handle_util(cmd: UtilCommand) -> Result<()> {
match cmd.command {
UtilSubcommand::DeriveDid { seed_hex } => {
let bytes =
hex::decode(seed_hex.trim()).context("Failed to decode seed from hex string")?;
if bytes.len() != 32 {
return Err(anyhow!(
"Seed must be exactly 32 bytes (64 hex characters), got {} bytes",
bytes.len()
));
}
let seed: [u8; 32] = bytes
.try_into()
.expect("Length already checked, conversion should succeed");
let pkcs8_der =
encode_seed_as_pkcs8(&seed).context("Failed to encode seed as PKCS#8")?;
let keypair = load_keypair_from_der_or_seed(&pkcs8_der)
.context("Failed to construct Ed25519 keypair from seed")?;
let pubkey_bytes = keypair.public_key().as_ref();
let pubkey_fixed: [u8; 32] = pubkey_bytes
.try_into()
.context("Failed to convert public key to fixed array")?;
let did = ed25519_pubkey_to_did_key(&pubkey_fixed);
if crate::ux::format::is_json_mode() {
crate::ux::format::JsonResponse::success(
"derive-did",
&serde_json::json!({ "did": did }),
)
.print()?;
} else {
println!("✅ Identity ID: {}", did);
}
Ok(())
}
UtilSubcommand::PubkeyToDid { openssh_pub } => {
let raw = openssh_pub_to_raw_ed25519(&openssh_pub)
.map_err(anyhow::Error::from)
.context("Failed to parse OpenSSH public key")?;
let did = ed25519_pubkey_to_did_key(&raw);
if crate::ux::format::is_json_mode() {
crate::ux::format::JsonResponse::success(
"pubkey-to-did",
&serde_json::json!({ "did": did }),
)
.print()?;
} else {
println!("{}", did);
}
Ok(())
}
UtilSubcommand::VerifyAttestation {
attestation_file,
issuer_pubkey,
} => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(handle_verify_attestation(&attestation_file, &issuer_pubkey))
}
}
}
impl crate::commands::executable::ExecutableCommand for UtilCommand {
fn execute(&self, _ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
handle_util(self.clone())
}
}