use anyhow::{Context, Result, anyhow};
use clap::{Parser, Subcommand};
use ring::signature::KeyPair;
use std::convert::TryInto;
use std::path::PathBuf;
use auths_crypto::openssh_pub_to_raw;
use auths_sdk::identity::{encode_seed_as_pkcs8, load_keypair_from_der_or_seed};
use auths_verifier::types::CanonicalDid;
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()
));
}
#[allow(clippy::expect_used)] let seed: [u8; 32] = bytes.try_into().expect("Length already checked");
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 = CanonicalDid::from_public_key_did_key(
&pubkey_fixed,
auths_crypto::CurveType::Ed25519,
)
.to_string();
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 (curve, raw) = openssh_pub_to_raw(&openssh_pub)
.map_err(anyhow::Error::from)
.context("Failed to parse OpenSSH public key")?;
let did = CanonicalDid::from_public_key_did_key(&raw, curve).to_string();
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())
}
}