1use anyhow::{Context, Result, anyhow};
2use chrono::Utc;
3use clap::{ArgAction, Parser, Subcommand};
4use ring::signature::KeyPair;
5use serde::Serialize;
6use serde_json;
7use std::fs;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use auths_core::{
12 config::EnvironmentConfig,
13 signing::PassphraseProvider,
14 storage::keychain::{KeyAlias, get_platform_keychain},
15};
16use auths_verifier::{IdentityBundle, Prefix};
17use clap::ValueEnum;
18
19use crate::commands::registry_overrides::RegistryOverrides;
20use crate::ux::format::{JsonResponse, is_json_mode};
21
22#[derive(Debug, Serialize)]
24struct IdShowResponse {
25 controller_did: String,
26 storage_id: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 metadata: Option<serde_json::Value>,
29}
30
31use auths_id::{
32 identity::initialize::initialize_registry_identity,
33 ports::registry::RegistryBackend,
34 storage::{
35 attestation::AttestationSource,
36 identity::IdentityStorage,
37 layout::{self, BlobName, GitRef, StorageLayoutConfig},
38 },
39};
40use auths_storage::git::{
41 GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage,
42};
43
44#[derive(Debug, Clone, Copy, ValueEnum, Default)]
46pub enum LayoutPreset {
47 #[default]
49 Default,
50 Radicle,
52 Gitoxide,
54}
55
56impl LayoutPreset {
57 pub fn to_config(self) -> StorageLayoutConfig {
59 match self {
60 LayoutPreset::Default | LayoutPreset::Radicle => StorageLayoutConfig {
61 identity_ref: GitRef::new("refs/rad/id"),
62 device_attestation_prefix: GitRef::new("refs/keys"),
63 attestation_blob_name: BlobName::new(layout::ATTESTATION_JSON),
64 identity_blob_name: BlobName::new(layout::IDENTITY_JSON),
65 },
66 LayoutPreset::Gitoxide => StorageLayoutConfig {
67 identity_ref: GitRef::new("refs/auths/id"),
68 device_attestation_prefix: GitRef::new("refs/auths/devices"),
69 attestation_blob_name: BlobName::new(layout::ATTESTATION_JSON),
70 identity_blob_name: BlobName::new(layout::IDENTITY_JSON),
71 },
72 }
73 }
74}
75
76#[derive(Parser, Debug, Clone)]
77#[command(about = "Manage identities stored in Git repositories.")]
78pub struct IdCommand {
79 #[clap(subcommand)]
80 pub subcommand: IdSubcommand,
81
82 #[command(flatten)]
83 pub overrides: RegistryOverrides,
84}
85
86#[derive(Subcommand, Debug, Clone)]
87pub enum IdSubcommand {
88 #[command(name = "create")]
90 Create {
91 #[arg(
93 long,
94 value_parser,
95 help = "Path to JSON file with arbitrary identity metadata."
96 )]
97 metadata_file: PathBuf,
98
99 #[arg(
101 long,
102 help = "Alias for storing the NEWLY generated private key in the secure keychain."
103 )]
104 local_key_alias: String,
105
106 #[arg(
110 long,
111 value_enum,
112 default_value = "default",
113 help = "Storage layout preset (default, radicle, gitoxide)"
114 )]
115 preset: LayoutPreset,
116 },
117
118 Show,
120
121 Rotate {
123 #[arg(long, help = "Alias of the identity key to rotate.")]
125 alias: Option<String>,
126
127 #[arg(
129 long,
130 help = "Alias of the CURRENT private key controlling the identity.",
131 conflicts_with = "alias"
132 )]
133 current_key_alias: Option<String>,
134
135 #[arg(long, help = "Alias to store the NEWLY generated private key under.")]
137 next_key_alias: Option<String>,
138
139 #[arg(
141 long,
142 action = ArgAction::Append,
143 help = "Verification server prefix to add (e.g., B...). Can be specified multiple times."
144 )]
145 add_witness: Vec<String>,
146
147 #[arg(
149 long,
150 action = ArgAction::Append,
151 help = "Verification server prefix to remove (e.g., B...). Can be specified multiple times."
152 )]
153 remove_witness: Vec<String>,
154
155 #[arg(
157 long,
158 help = "New simple verification threshold count (e.g., 1 for 1-of-N)."
159 )]
160 witness_threshold: Option<u64>,
161 },
162
163 ExportBundle {
169 #[arg(long, help = "Key alias to include in bundle")]
171 alias: String,
172
173 #[arg(long, short, help = "Output file path")]
175 output: PathBuf,
176
177 #[arg(
179 long,
180 help = "Maximum bundle age in seconds before it is considered stale"
181 )]
182 max_age_secs: u64,
183 },
184
185 Register {
187 #[arg(long, default_value = "https://auths-registry.fly.dev")]
189 registry: String,
190 },
191
192 Claim(super::claim::ClaimCommand),
194
195 Migrate(super::migrate::MigrateCommand),
197}
198
199#[allow(clippy::too_many_arguments)]
204pub fn handle_id(
205 cmd: IdCommand,
206 repo_opt: Option<PathBuf>,
207 identity_ref_override: Option<String>,
208 identity_blob_name_override: Option<String>,
209 attestation_prefix_override: Option<String>,
210 attestation_blob_name_override: Option<String>,
211 passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
212 http_client: &reqwest::Client,
213 env_config: &EnvironmentConfig,
214) -> Result<()> {
215 let repo_path = layout::resolve_repo_path(repo_opt)?;
217
218 let mut config = StorageLayoutConfig::default();
221 if let Some(ref identity_ref) = identity_ref_override {
222 config.identity_ref = identity_ref.clone().into();
223 }
224 if let Some(ref blob_name) = identity_blob_name_override {
225 config.identity_blob_name = blob_name.clone().into();
226 }
227 if let Some(ref prefix) = attestation_prefix_override {
228 config.device_attestation_prefix = prefix.clone().into();
229 }
230 if let Some(ref blob_name) = attestation_blob_name_override {
231 config.attestation_blob_name = blob_name.clone().into();
232 }
233
234 match cmd.subcommand {
235 IdSubcommand::Create {
236 metadata_file,
237 local_key_alias,
238 preset,
239 } => {
240 let mut config = preset.to_config();
242 if let Some(ref identity_ref) = identity_ref_override {
243 config.identity_ref = identity_ref.clone().into();
244 }
245 if let Some(ref blob_name) = identity_blob_name_override {
246 config.identity_blob_name = blob_name.clone().into();
247 }
248 if let Some(ref prefix) = attestation_prefix_override {
249 config.device_attestation_prefix = prefix.clone().into();
250 }
251 if let Some(ref blob_name) = attestation_blob_name_override {
252 config.attestation_blob_name = blob_name.clone().into();
253 }
254 let metadata_file_path = metadata_file;
255
256 println!("š Creating new cryptographic identity...");
258 println!(" Repository path: {:?}", repo_path);
259 println!(" Local Key Alias: {}", local_key_alias);
260 println!(" Metadata File: {:?}", metadata_file_path);
261 println!(" Using Identity Ref: '{}'", config.identity_ref);
262 println!(" Using Identity Blob: '{}'", config.identity_blob_name);
263
264 use crate::factories::storage::{ensure_git_repo, open_git_repo};
266
267 let identity_storage_check = RegistryIdentityStorage::new(repo_path.clone());
268 if repo_path.exists() {
269 match open_git_repo(&repo_path) {
270 Ok(_repo) => {
271 println!(" Git repository found at {:?}.", repo_path);
272 if identity_storage_check.load_identity().is_ok() {
273 eprintln!(
274 "ā ļø Primary identity already initialized and loadable at {:?} using ref '{}'. Aborting.",
275 repo_path,
276 identity_storage_check.get_identity_ref()?
277 );
278 return Err(anyhow!("Identity already exists in this repository"));
279 } else {
280 println!(
281 " Repository exists, but primary identity ref/data is missing or invalid. Proceeding..."
282 );
283 }
284 }
285 Err(_) => {
286 println!(
287 " Path {:?} exists but is not a Git repository. Initializing...",
288 repo_path
289 );
290 ensure_git_repo(&repo_path).map_err(|e| {
291 anyhow!(
292 "Path {:?} exists but failed to initialize as Git repository: {}",
293 repo_path,
294 e
295 )
296 })?;
297 println!(" Successfully initialized Git repository.");
298 }
299 }
300 } else {
301 println!(" Initializing Git repository at {:?}...", repo_path);
302 ensure_git_repo(&repo_path).map_err(|e| {
303 anyhow!(
304 "Failed to initialize Git repository at {:?}: {}",
305 repo_path,
306 e
307 )
308 })?;
309 println!(" Successfully initialized Git repository.");
310 }
311
312 if !metadata_file_path.exists() {
314 return Err(anyhow!("Metadata file not found: {:?}", metadata_file_path));
315 }
316 let metadata_content = fs::read_to_string(&metadata_file_path).with_context(|| {
317 format!("Failed to read metadata file: {:?}", metadata_file_path)
318 })?;
319 let metadata_value: serde_json::Value = serde_json::from_str(&metadata_content)
320 .with_context(|| {
321 format!(
322 "Failed to parse JSON from metadata file: {:?}",
323 metadata_file_path
324 )
325 })?;
326 println!(" Metadata loaded successfully from file.");
327
328 println!(" Initializing using did:keri method (default)...");
330
331 let _metadata_value = metadata_value; let backend: Arc<dyn RegistryBackend + Send + Sync> =
334 Arc::new(GitRegistryBackend::from_config_unchecked(
335 RegistryConfig::single_tenant(&repo_path),
336 ));
337 let local_key_alias = KeyAlias::new_unchecked(local_key_alias);
338 match initialize_registry_identity(
339 backend,
340 &local_key_alias,
341 passphrase_provider.as_ref(),
342 &get_platform_keychain()?,
343 None,
344 ) {
345 Ok((controller_did_keri, alias)) => {
346 println!("\nā
Identity (did:keri) initialized successfully!");
347 println!(
348 " Repository: {:?}",
349 repo_path
350 .canonicalize()
351 .unwrap_or_else(|_| repo_path.clone())
352 );
353 println!(" Controller DID: {}", controller_did_keri);
354 println!(
355 " Local Key Alias: {} (Use this for local signing/rotations)",
356 alias
357 );
358 let did_prefix = controller_did_keri
359 .as_str()
360 .strip_prefix("did:keri:")
361 .unwrap_or("");
362 if !did_prefix.is_empty() {
363 println!(
364 " KEL Ref Used: '{}'",
365 layout::keri_kel_ref(&Prefix::new_unchecked(did_prefix.to_string()))
366 );
367 }
368 println!(" Identity Ref Used: '{}'", config.identity_ref);
369 println!(
370 " Identity Blob Used: '{}'",
371 layout::identity_blob_name(&config)
372 );
373 println!(" Metadata stored from: {:?}", metadata_file_path);
374 println!("š Keep your passphrase secure!");
375 Ok(())
376 }
377 Err(e) => Err(e).context("Failed to initialize KERI identity"),
378 }
379 }
380
381 IdSubcommand::Show => {
382 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
383
384 let identity = identity_storage
385 .load_identity()
386 .with_context(|| format!("Failed to load identity from {:?}", repo_path))?;
387
388 if is_json_mode() {
389 let response = JsonResponse::success(
390 "id show",
391 IdShowResponse {
392 controller_did: identity.controller_did.to_string(),
393 storage_id: identity.storage_id.clone(),
394 metadata: identity.metadata.clone(),
395 },
396 );
397 response.print()?;
398 } else {
399 println!("Showing identity details...");
400 println!(" Using Repository: {:?}", repo_path);
401 println!(" Using Identity Ref: '{}'", config.identity_ref);
402 println!(" Using Identity Blob: '{}'", config.identity_blob_name);
403
404 println!("Controller DID: {}", identity.controller_did);
405 println!("Storage ID (RID): {}", identity.storage_id);
406 println!("Metadata (raw JSON, interpretation depends on convention):");
407 if let Some(meta) = &identity.metadata {
408 println!(
409 "{}",
410 serde_json::to_string_pretty(meta)
411 .unwrap_or_else(|_| " <Error serializing metadata>".to_string())
412 );
413 } else {
414 println!(" (None)");
415 }
416
417 println!("\nUse 'auths device list' to see authorized devices");
418 }
419 Ok(())
420 }
421
422 IdSubcommand::Rotate {
423 alias,
424 current_key_alias,
425 next_key_alias,
426 add_witness,
427 remove_witness,
428 witness_threshold,
429 } => {
430 let identity_key_alias = alias.or(current_key_alias);
431
432 println!("š Rotating KERI identity keys...");
433 println!(" Using Repository: {:?}", repo_path);
434 if let Some(ref a) = identity_key_alias {
435 println!(" Current Key Alias: {}", a);
436 }
437 if let Some(ref a) = next_key_alias {
438 println!(" New Key Alias: {}", a);
439 }
440 if !add_witness.is_empty() {
441 println!(" Witnesses to Add: {:?}", add_witness);
442 }
443 if !remove_witness.is_empty() {
444 println!(" Witnesses to Remove: {:?}", remove_witness);
445 }
446 if let Some(thresh) = witness_threshold {
447 println!(" New Witness Threshold: {}", thresh);
448 }
449
450 let rotation_config = auths_sdk::types::RotationConfig {
451 repo_path: repo_path.clone(),
452 identity_key_alias: identity_key_alias.map(KeyAlias::new_unchecked),
453 next_key_alias: next_key_alias.map(KeyAlias::new_unchecked),
454 };
455 let rotation_ctx = {
456 use auths_core::storage::keychain::get_platform_keychain_with_config;
457 use auths_id::attestation::export::AttestationSink;
458 use auths_id::storage::attestation::AttestationSource;
459 use auths_id::storage::identity::IdentityStorage;
460 use auths_sdk::context::AuthsContext;
461 use auths_storage::git::{
462 GitRegistryBackend, RegistryAttestationStorage, RegistryConfig,
463 RegistryIdentityStorage,
464 };
465 let backend: Arc<dyn auths_id::ports::registry::RegistryBackend + Send + Sync> =
466 Arc::new(GitRegistryBackend::from_config_unchecked(
467 RegistryConfig::single_tenant(&repo_path),
468 ));
469 let identity_storage: Arc<dyn IdentityStorage + Send + Sync> =
470 Arc::new(RegistryIdentityStorage::new(repo_path.clone()));
471 let attestation_store = Arc::new(RegistryAttestationStorage::new(&repo_path));
472 let attestation_sink: Arc<dyn AttestationSink + Send + Sync> =
473 Arc::clone(&attestation_store) as Arc<dyn AttestationSink + Send + Sync>;
474 let attestation_source: Arc<dyn AttestationSource + Send + Sync> =
475 attestation_store as Arc<dyn AttestationSource + Send + Sync>;
476 let key_storage: Arc<dyn auths_core::storage::keychain::KeyStorage + Send + Sync> =
477 Arc::from(
478 get_platform_keychain_with_config(env_config)
479 .context("Failed to access keychain")?,
480 );
481 AuthsContext::builder()
482 .registry(backend)
483 .key_storage(key_storage)
484 .clock(Arc::new(auths_core::ports::clock::SystemClock))
485 .identity_storage(identity_storage)
486 .attestation_sink(attestation_sink)
487 .attestation_source(attestation_source)
488 .passphrase_provider(Arc::clone(&passphrase_provider))
489 .build()?
490 };
491 let result = auths_sdk::workflows::rotation::rotate_identity(
492 rotation_config,
493 &rotation_ctx,
494 &auths_core::ports::clock::SystemClock,
495 )
496 .with_context(|| "Failed to rotate KERI identity keys")?;
497
498 println!("\nā
KERI identity keys rotated successfully!");
499 println!(" Identity DID: {}", result.controller_did);
500 println!(
501 " Old key fingerprint: {}...",
502 result.previous_key_fingerprint
503 );
504 println!(" New key fingerprint: {}...", result.new_key_fingerprint);
505 println!(
506 "ā ļø The previous key alias is no longer the active signing key for this identity."
507 );
508
509 log::info!(
510 "Key rotation completed: old_key={}, new_key={}",
511 result.previous_key_fingerprint,
512 result.new_key_fingerprint,
513 );
514
515 Ok(())
516 }
517
518 IdSubcommand::ExportBundle {
519 alias,
520 output,
521 max_age_secs,
522 } => {
523 println!("š¦ Exporting identity bundle...");
524 println!(" Using Repository: {:?}", repo_path);
525 println!(" Key Alias: {}", alias);
526 println!(" Output File: {:?}", output);
527
528 let identity_storage = RegistryIdentityStorage::new(repo_path.clone());
530 let identity = identity_storage
531 .load_identity()
532 .with_context(|| format!("Failed to load identity from {:?}", repo_path))?;
533
534 println!(" Identity DID: {}", identity.controller_did);
535
536 let attestation_storage = RegistryAttestationStorage::new(repo_path.clone());
538 let attestations = attestation_storage
539 .load_all_attestations()
540 .unwrap_or_default();
541
542 let keychain = get_platform_keychain()?;
544 let (_, encrypted_key) = keychain
545 .load_key(&KeyAlias::new_unchecked(&alias))
546 .with_context(|| format!("Key '{}' not found in keychain", alias))?;
547
548 let pass = passphrase_provider
550 .get_passphrase(&format!("Enter passphrase for key '{}':", alias))?;
551 let pkcs8_bytes = auths_core::crypto::signer::decrypt_keypair(&encrypted_key, &pass)
552 .context("Failed to decrypt key")?;
553 let keypair = auths_id::identity::helpers::load_keypair_from_der_or_seed(&pkcs8_bytes)?;
554 let public_key_hex = hex::encode(keypair.public_key().as_ref());
555
556 let bundle = IdentityBundle {
558 identity_did: identity.controller_did.to_string(),
559 public_key_hex,
560 attestation_chain: attestations,
561 bundle_timestamp: Utc::now(),
562 max_valid_for_secs: max_age_secs,
563 };
564
565 let json = serde_json::to_string_pretty(&bundle)
567 .context("Failed to serialize identity bundle")?;
568 fs::write(&output, &json)
569 .with_context(|| format!("Failed to write bundle to {:?}", output))?;
570
571 println!("\nā
Identity bundle exported successfully!");
572 println!(" Output: {:?}", output);
573 println!(" Attestations: {}", bundle.attestation_chain.len());
574 println!("\nUsage in CI:");
575 println!(" auths verify-commit --identity-bundle {:?} HEAD", output);
576
577 Ok(())
578 }
579
580 IdSubcommand::Register { registry } => {
581 super::register::handle_register(&repo_path, ®istry)
582 }
583
584 IdSubcommand::Claim(claim_cmd) => {
585 super::claim::handle_claim(&claim_cmd, &repo_path, passphrase_provider, http_client)
586 }
587
588 IdSubcommand::Migrate(migrate_cmd) => super::migrate::handle_migrate(migrate_cmd),
589 }
590}