use crate::server::config::{SecretsConfig, StoreConfig};
use crate::server::error::AppError;
use crate::server::identity::{IdentityGeneration, load_generations};
use crate::server::secret_store::create_secret_store;
use crate::server::store::{KS_IDENTITY, Store};
type CliResult = Result<(), Box<dyn std::error::Error>>;
async fn load(store_config: &StoreConfig) -> Result<(Store, Vec<IdentityGeneration>), AppError> {
let store = Store::open(store_config).await?;
let ks = store.keyspace(KS_IDENTITY)?;
let now = crate::server::auth::session::now_epoch();
let generations = load_generations(&ks, now).await?;
Ok((store, generations))
}
pub async fn run_list_generations(store_config: &StoreConfig) -> CliResult {
let (_store, generations) = load(store_config).await?;
if generations.is_empty() {
eprintln!();
eprintln!(" No identity generations recorded.");
eprintln!(" (The service records one the first time it resolves its own DID.)");
eprintln!();
return Ok(());
}
let now = crate::server::auth::session::now_epoch();
let current_id = generations.first().map(|g| g.id);
eprintln!();
for g in &generations {
let is_current = Some(g.id) == current_id;
eprintln!(
" Generation {}{}",
g.id,
if is_current { " (current)" } else { "" }
);
eprintln!(" key agreement : {}", g.ka_kid);
eprintln!(" signing : {}", g.signing_kid);
if let Some(ref m) = g.mediator_did {
eprintln!(" mediator : {m}");
}
eprintln!(
" transports : {}",
match (g.protocols.didcomm, g.protocols.tsp) {
(true, true) => "DIDComm + TSP",
(true, false) => "DIDComm",
(false, true) => "TSP",
(false, false) => "none",
}
);
match g.expires_at {
Some(expires) => {
let remaining = expires.saturating_sub(now);
eprintln!(
" retired : still honoured for {}m {}s",
remaining / 60,
remaining % 60
);
}
None if is_current => {}
None => eprintln!(" retired : no expiry recorded"),
}
eprintln!();
}
if generations.len() > 1 {
eprintln!(
" Superseded generations stay decryptable so peers holding a cached DID document"
);
eprintln!(" can still reach this service. To stop honouring one immediately (a key");
eprintln!(" compromise), use `identity-retire-now --generation <id>`.");
eprintln!();
}
Ok(())
}
pub async fn run_retire_generation(
store_config: &StoreConfig,
secrets_config: &SecretsConfig,
config_path: &std::path::Path,
generation_id: u64,
) -> CliResult {
let (store, generations) = load(store_config).await?;
let Some(target) = generations.iter().find(|g| g.id == generation_id) else {
return Err(format!(
"no live identity generation with id {generation_id} \
(run `identity-list` to see what there is)"
)
.into());
};
if generations.first().map(|g| g.id) == Some(generation_id) {
return Err(
"refusing to retire the current generation — it is the key this service is \
actively using, and dropping it would leave the service unable to decrypt \
anything. Publish a new DID document first; that supersedes this generation, \
and then it can be retired."
.into(),
);
}
let secret_store = create_secret_store(secrets_config, config_path)?;
if let Some(mut secrets) = secret_store.get().await? {
let before = secrets.retired.len();
secrets.retired.retain(|r| r.ka_kid != target.ka_kid);
if secrets.retired.len() != before {
secret_store.set(&secrets).await?;
}
}
let ks = store.keyspace(KS_IDENTITY)?;
ks.remove(format!("identity:gen:{generation_id:020}"))
.await?;
store.persist().await?;
eprintln!();
eprintln!(" Retired identity generation {generation_id}.");
eprintln!(" key agreement : {}", target.ka_kid);
eprintln!();
eprintln!(" Messages still addressed to that key-agreement key will no longer decrypt.");
eprintln!(" Peers whose cached DID document still names it cannot reach this service");
eprintln!(" until their cache expires — which is the point, if the key was compromised.");
eprintln!();
eprintln!(" If the service is running, restart it (or wait one sweep interval) for the");
eprintln!(" change to take effect in the live process.");
eprintln!();
Ok(())
}