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::{ed25519_pubkey_to_did_key, openssh_pub_to_raw_ed25519};
8use auths_id::identity::helpers::{encode_seed_as_pkcs8, load_keypair_from_der_or_seed};
9
10use crate::commands::device::verify_attestation::handle_verify_attestation;
11
12/// Top-level wrapper to group utility subcommands
13#[derive(Parser, Debug, Clone)]
14#[command(name = "util", about = "Utility commands for common operations.")]
15pub struct UtilCommand {
16    #[command(subcommand)]
17    pub command: UtilSubcommand,
18}
19
20/// All available utility subcommands
21#[derive(Subcommand, Debug, Clone)]
22pub enum UtilSubcommand {
23    /// Derive an identity ID from a raw Ed25519 seed.
24    DeriveDid {
25        #[arg(
26            long,
27            help = "The 32-byte Ed25519 seed encoded as a 64-character hex string."
28        )]
29        seed_hex: String,
30    },
31
32    /// Convert an OpenSSH Ed25519 public key to a did:key identifier.
33    #[command(name = "pubkey-to-did")]
34    PubkeyToDid {
35        /// The full OpenSSH public key line (e.g. "ssh-ed25519 AAAA... comment").
36        #[arg(help = "OpenSSH Ed25519 public key line.")]
37        openssh_pub: String,
38    },
39
40    /// Verify an authorization signature from a file using an explicit issuer public key.
41    VerifyAttestation {
42        /// Path to the authorization JSON file.
43        #[arg(long, value_parser, value_name = "FILE_PATH")]
44        attestation_file: PathBuf,
45
46        /// Issuer's Ed25519 public key (32 bytes) as a hex string (64 characters).
47        #[arg(long, value_name = "HEX_PUBKEY")]
48        issuer_pubkey: String,
49    },
50}
51
52pub fn handle_util(cmd: UtilCommand) -> Result<()> {
53    match cmd.command {
54        UtilSubcommand::DeriveDid { seed_hex } => {
55            // Decode hex string to bytes
56            let bytes =
57                hex::decode(seed_hex.trim()).context("Failed to decode seed from hex string")?;
58            // Validate length
59            if bytes.len() != 32 {
60                return Err(anyhow!(
61                    "Seed must be exactly 32 bytes (64 hex characters), got {} bytes",
62                    bytes.len()
63                ));
64            }
65
66            // Convert Vec<u8> to [u8; 32]
67            let seed: [u8; 32] = bytes
68                .try_into()
69                .expect("Length already checked, conversion should succeed"); // Safe due to check above
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            // Derive the identity ID string
84            let did = ed25519_pubkey_to_did_key(&pubkey_fixed);
85            // Print the result clearly
86            println!("✅ Identity ID: {}", did);
87            Ok(())
88        }
89
90        UtilSubcommand::PubkeyToDid { openssh_pub } => {
91            let raw = openssh_pub_to_raw_ed25519(&openssh_pub)
92                .map_err(anyhow::Error::from)
93                .context("Failed to parse OpenSSH public key")?;
94            let did = ed25519_pubkey_to_did_key(&raw);
95            println!("{}", did);
96            Ok(())
97        }
98
99        UtilSubcommand::VerifyAttestation {
100            attestation_file,
101            issuer_pubkey,
102        } => {
103            let rt = tokio::runtime::Runtime::new()?;
104            rt.block_on(handle_verify_attestation(&attestation_file, &issuer_pubkey))
105        }
106    }
107}
108
109impl crate::commands::executable::ExecutableCommand for UtilCommand {
110    fn execute(&self, _ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
111        handle_util(self.clone())
112    }
113}