/ crates / auths-cli / src / commands / utils.rs
utils.rs
  1  use anyhow::{Context, Result, anyhow};
  2  use clap::{Parser, Subcommand};
  3  use ring::signature::KeyPair;
  4  use std::convert::TryInto;
  5  use std::path::PathBuf;
  6  
  7  use auths_crypto::{ed25519_pubkey_to_did_key, openssh_pub_to_raw_ed25519};
  8  use auths_id::identity::helpers::{encode_seed_as_pkcs8, load_keypair_from_der_or_seed};
  9  
 10  use 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.")]
 15  pub struct UtilCommand {
 16      #[command(subcommand)]
 17      pub command: UtilSubcommand,
 18  }
 19  
 20  /// All available utility subcommands
 21  #[derive(Subcommand, Debug, Clone)]
 22  pub 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  
 52  pub 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              let did = ed25519_pubkey_to_did_key(&pubkey_fixed);
 84              if crate::ux::format::is_json_mode() {
 85                  crate::ux::format::JsonResponse::success(
 86                      "derive-did",
 87                      &serde_json::json!({ "did": did }),
 88                  )
 89                  .print()?;
 90              } else {
 91                  println!("✅ Identity ID: {}", did);
 92              }
 93              Ok(())
 94          }
 95  
 96          UtilSubcommand::PubkeyToDid { openssh_pub } => {
 97              let raw = openssh_pub_to_raw_ed25519(&openssh_pub)
 98                  .map_err(anyhow::Error::from)
 99                  .context("Failed to parse OpenSSH public key")?;
100              let did = ed25519_pubkey_to_did_key(&raw);
101              if crate::ux::format::is_json_mode() {
102                  crate::ux::format::JsonResponse::success(
103                      "pubkey-to-did",
104                      &serde_json::json!({ "did": did }),
105                  )
106                  .print()?;
107              } else {
108                  println!("{}", did);
109              }
110              Ok(())
111          }
112  
113          UtilSubcommand::VerifyAttestation {
114              attestation_file,
115              issuer_pubkey,
116          } => {
117              let rt = tokio::runtime::Runtime::new()?;
118              rt.block_on(handle_verify_attestation(&attestation_file, &issuer_pubkey))
119          }
120      }
121  }
122  
123  impl crate::commands::executable::ExecutableCommand for UtilCommand {
124      fn execute(&self, _ctx: &crate::config::CliConfig) -> anyhow::Result<()> {
125          handle_util(self.clone())
126      }
127  }