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 }