auths_cli/commands/
utils.rs1use anyhow::{Context, Result, anyhow};
2use clap::{Parser, Subcommand};
3use ring::signature::KeyPair;
4use std::convert::TryInto;
5use std::path::PathBuf;
6
7use auths_crypto::openssh_pub_to_raw;
8use auths_sdk::identity::{encode_seed_as_pkcs8, load_keypair_from_der_or_seed};
9use auths_verifier::types::CanonicalDid;
10
11use crate::commands::device::verify_attestation::handle_verify_attestation;
12
13#[derive(Parser, Debug, Clone)]
15#[command(name = "util", about = "Utility commands for common operations.")]
16pub struct UtilCommand {
17 #[command(subcommand)]
18 pub command: UtilSubcommand,
19}
20
21#[derive(Subcommand, Debug, Clone)]
23pub enum UtilSubcommand {
24 DeriveDid {
26 #[arg(
27 long,
28 help = "The 32-byte Ed25519 seed encoded as a 64-character hex string."
29 )]
30 seed_hex: String,
31 },
32
33 #[command(name = "pubkey-to-did")]
35 PubkeyToDid {
36 #[arg(help = "OpenSSH Ed25519 public key line.")]
38 openssh_pub: String,
39 },
40
41 VerifyAttestation {
43 #[arg(long, value_parser, value_name = "FILE_PATH")]
45 attestation_file: PathBuf,
46
47 #[arg(long, value_name = "HEX_PUBKEY")]
49 issuer_pubkey: String,
50 },
51}
52
53pub fn handle_util(cmd: UtilCommand) -> Result<()> {
54 match cmd.command {
55 UtilSubcommand::DeriveDid { seed_hex } => {
56 let bytes =
58 hex::decode(seed_hex.trim()).context("Failed to decode seed from hex string")?;
59 if bytes.len() != 32 {
61 return Err(anyhow!(
62 "Seed must be exactly 32 bytes (64 hex characters), got {} bytes",
63 bytes.len()
64 ));
65 }
66
67 #[allow(clippy::expect_used)] let seed: [u8; 32] = bytes.try_into().expect("Length already checked");
70
71 let pkcs8_der =
73 encode_seed_as_pkcs8(&seed).context("Failed to encode seed as PKCS#8")?;
74 let keypair = load_keypair_from_der_or_seed(&pkcs8_der)
75 .context("Failed to construct Ed25519 keypair from seed")?;
76
77 let pubkey_bytes = keypair.public_key().as_ref();
79 let pubkey_fixed: [u8; 32] = pubkey_bytes
80 .try_into()
81 .context("Failed to convert public key to fixed array")?; let did = CanonicalDid::from_public_key_did_key(
84 &pubkey_fixed,
85 auths_crypto::CurveType::Ed25519,
86 )
87 .to_string();
88 if crate::ux::format::is_json_mode() {
89 crate::ux::format::JsonResponse::success(
90 "derive-did",
91 &serde_json::json!({ "did": did }),
92 )
93 .print()?;
94 } else {
95 println!("✅ Identity ID: {}", did);
96 }
97 Ok(())
98 }
99
100 UtilSubcommand::PubkeyToDid { openssh_pub } => {
101 let (curve, raw) = openssh_pub_to_raw(&openssh_pub)
102 .map_err(anyhow::Error::from)
103 .context("Failed to parse OpenSSH public key")?;
104 let did = CanonicalDid::from_public_key_did_key(&raw, curve).to_string();
105 if crate::ux::format::is_json_mode() {
106 crate::ux::format::JsonResponse::success(
107 "pubkey-to-did",
108 &serde_json::json!({ "did": did }),
109 )
110 .print()?;
111 } else {
112 println!("{}", did);
113 }
114 Ok(())
115 }
116
117 UtilSubcommand::VerifyAttestation {
118 attestation_file,
119 issuer_pubkey,
120 } => {
121 let rt = tokio::runtime::Runtime::new()?;
122 rt.block_on(handle_verify_attestation(&attestation_file, &issuer_pubkey))
123 }
124 }
125}
126
127impl crate::commands::executable::ExecutableCommand for UtilCommand {
128 fn execute(&self, _ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
129 handle_util(self.clone())
130 }
131}