Skip to main content

auths_cli/commands/
utils.rs

1use 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/// Top-level wrapper to group utility subcommands
14#[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/// All available utility subcommands
22#[derive(Subcommand, Debug, Clone)]
23pub enum UtilSubcommand {
24    /// Derive an identity ID from a raw Ed25519 seed.
25    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    /// Convert an OpenSSH Ed25519 public key to a did:key identifier.
34    #[command(name = "pubkey-to-did")]
35    PubkeyToDid {
36        /// The full OpenSSH public key line (e.g. "ssh-ed25519 AAAA... comment").
37        #[arg(help = "OpenSSH Ed25519 public key line.")]
38        openssh_pub: String,
39    },
40
41    /// Verify an authorization signature from a file using an explicit issuer public key.
42    VerifyAttestation {
43        /// Path to the authorization JSON file.
44        #[arg(long, value_parser, value_name = "FILE_PATH")]
45        attestation_file: PathBuf,
46
47        /// Issuer's Ed25519 public key (32 bytes) as a hex string (64 characters).
48        #[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            // Decode hex string to bytes
57            let bytes =
58                hex::decode(seed_hex.trim()).context("Failed to decode seed from hex string")?;
59            // Validate length
60            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            // Convert Vec<u8> to [u8; 32]
68            #[allow(clippy::expect_used)] // INVARIANT: length validated to be 32 bytes on line 59
69            let seed: [u8; 32] = bytes.try_into().expect("Length already checked");
70
71            // Create keypair from seed by encoding as PKCS#8 first
72            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            // Get public key bytes
78            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")?; // Should not fail
82
83            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}