use coz::base64ct::Encoding;
use cyphr_storage::{Genesis, export_commits, load_principal_from_commits};
use indexmap::IndexMap;
use serde_json::Value;
use super::common::{
current_timestamp, extract_genesis_from_commits, generate_key, load_key_from_keystore,
parse_principal_genesis, parse_store,
};
use crate::keystore::{JsonKeyStore, KeyStore};
use crate::{Cli, KeyCommands, OutputFormat};
pub fn run(cli: &Cli, command: &KeyCommands) -> crate::Result<()> {
match command {
KeyCommands::Generate { algo, tag } => generate(cli, algo, tag.as_deref()),
KeyCommands::Add {
identity,
key,
signer,
} => add(cli, identity, key.as_deref(), signer, &cli.authority),
KeyCommands::Revoke {
identity,
key,
signer,
} => revoke(cli, identity, key, signer, &cli.authority),
KeyCommands::List { identity } => list(cli, identity.as_deref()),
}
}
fn generate(cli: &Cli, algo: &str, tag: Option<&str>) -> crate::Result<()> {
let mut keystore = JsonKeyStore::open(&cli.keystore)?;
let (tmb, stored_key, _key) = generate_key(algo, tag)?;
keystore.store(&tmb, stored_key)?;
keystore.save()?;
match cli.output {
OutputFormat::Json => {
let output = serde_json::json!({
"tmb": tmb,
"alg": algo,
"tag": tag,
});
println!("{}", serde_json::to_string_pretty(&output)?);
},
OutputFormat::Table => {
println!("Generated {algo} key");
println!(" tmb: {tmb}");
if let Some(t) = tag {
println!(" tag: {t}");
}
println!(" stored: {}", cli.keystore.display());
},
}
Ok(())
}
fn add(
cli: &Cli,
identity: &str,
key_tmb: Option<&str>,
signer_tmb: &str,
authority: &str,
) -> crate::Result<()> {
let mut keystore = JsonKeyStore::open(&cli.keystore)?;
let store = parse_store(&cli.store)?;
let pr = parse_principal_genesis(identity)?;
let commits = store.get_commits(&pr).unwrap_or_default();
let is_implicit_genesis = keystore.get(identity).is_ok();
let mut principal = if commits.is_empty() {
let genesis_key = load_key_from_keystore(&keystore, identity)?;
cyphr::Principal::implicit(genesis_key)?
} else if is_implicit_genesis {
let genesis_key = load_key_from_keystore(&keystore, identity)?;
let genesis = Genesis::Implicit(genesis_key);
load_principal_from_commits(genesis, &commits)?
} else {
let genesis = extract_genesis_from_commits(&commits, None)?;
load_principal_from_commits(genesis, &commits)?
};
let (new_key_tmb, new_key) = match key_tmb {
Some(tmb) => {
let key = load_key_from_keystore(&keystore, tmb)?;
(tmb.to_string(), key)
},
None => {
let signer_stored = keystore.get(signer_tmb)?;
let (tmb, stored, key) = generate_key(&signer_stored.alg, None)?;
keystore.store(&tmb, stored)?;
keystore.save()?;
(tmb, key)
},
};
let signer_stored = keystore.get(signer_tmb)?;
let now = current_timestamp();
let pre = principal.pr_tagged()?;
let mut pay_map: IndexMap<String, Value> = IndexMap::new();
pay_map.insert("alg".to_string(), Value::String(signer_stored.alg.clone()));
pay_map.insert("id".to_string(), Value::String(new_key_tmb.clone()));
pay_map.insert("now".to_string(), Value::Number(now.into()));
pay_map.insert("pre".to_string(), Value::String(pre));
pay_map.insert("tmb".to_string(), Value::String(signer_tmb.to_string()));
pay_map.insert(
"typ".to_string(),
Value::String(format!(
"{}/{}",
authority,
cyphr::parsed_coz::typ::KEY_CREATE
)),
);
let pay_value: Value = serde_json::to_value(&pay_map)?;
let pay_vec = serde_json::to_vec(&pay_value)?;
let (sig_bytes, cad) = coz::sign_json(
&pay_vec,
&signer_stored.alg,
&signer_stored.prv_key,
&signer_stored.pub_key,
)
.ok_or_else(|| crate::Error::Signing("sign_json failed".into()))?;
let czd = coz::czd_for_alg(&cad, &sig_bytes, &signer_stored.alg)
.ok_or_else(|| crate::Error::Signing("czd_for_alg failed".into()))?;
let mut scope = principal.begin_commit();
scope.verify_and_apply(&pay_vec, &sig_bytes, czd, Some(new_key.clone()))?;
let tmb = coz::Thumbprint::from_bytes(
coz::base64ct::Base64UrlUnpadded::decode_vec(signer_tmb)
.map_err(|e| crate::Error::Signing(format!("invalid tmb base64: {}", e)))?,
);
scope.finalize_with_arrow(
&signer_stored.alg,
&signer_stored.prv_key,
&signer_stored.pub_key,
&tmb,
now,
authority,
)?;
let new_commits = export_commits(&principal)?;
for commit in new_commits.iter().skip(commits.len()) {
store.append_commit(&pr, commit)?;
}
match cli.output {
OutputFormat::Json => {
let output = serde_json::json!({
"identity": identity,
"added_key": new_key_tmb,
"signed_by": signer_tmb,
});
println!("{}", serde_json::to_string_pretty(&output)?);
},
OutputFormat::Table => {
println!("Added key to identity");
println!(" identity: {identity}");
println!(" key: {new_key_tmb}");
println!(" signed by: {signer_tmb}");
},
}
Ok(())
}
fn revoke(
cli: &Cli,
identity: &str,
key_tmb: &str,
signer_tmb: &str,
authority: &str,
) -> crate::Result<()> {
let keystore = JsonKeyStore::open(&cli.keystore)?;
let store = parse_store(&cli.store)?;
let pr = parse_principal_genesis(identity)?;
let commits = store.get_commits(&pr).unwrap_or_default();
let is_implicit_genesis = keystore.get(identity).is_ok();
let mut principal = if commits.is_empty() {
let genesis_key = load_key_from_keystore(&keystore, identity)?;
cyphr::Principal::implicit(genesis_key)?
} else if is_implicit_genesis {
let genesis_key = load_key_from_keystore(&keystore, identity)?;
let genesis = Genesis::Implicit(genesis_key);
load_principal_from_commits(genesis, &commits)?
} else {
let genesis = extract_genesis_from_commits(&commits, None)?;
load_principal_from_commits(genesis, &commits)?
};
let signer_stored = keystore.get(signer_tmb)?;
if key_tmb != signer_tmb {
return Err(crate::Error::InvalidArgument(
"key revoke only supports self-revoke: --key must equal --signer".into(),
));
}
let now = current_timestamp();
let pre = principal.pr_tagged()?;
let mut pay_map: IndexMap<String, Value> = IndexMap::new();
pay_map.insert("alg".to_string(), Value::String(signer_stored.alg.clone()));
pay_map.insert("now".to_string(), Value::Number(now.into()));
pay_map.insert("pre".to_string(), Value::String(pre));
pay_map.insert("rvk".to_string(), Value::Number(now.into()));
pay_map.insert("tmb".to_string(), Value::String(signer_tmb.to_string()));
pay_map.insert(
"typ".to_string(),
Value::String(format!(
"{}/{}",
authority,
cyphr::parsed_coz::typ::KEY_REVOKE
)),
);
let pay_value: Value = serde_json::to_value(&pay_map)?;
let pay_vec = serde_json::to_vec(&pay_value)?;
let (sig_bytes, cad) = coz::sign_json(
&pay_vec,
&signer_stored.alg,
&signer_stored.prv_key,
&signer_stored.pub_key,
)
.ok_or_else(|| crate::Error::Signing("sign_json failed".into()))?;
let czd = coz::czd_for_alg(&cad, &sig_bytes, &signer_stored.alg)
.ok_or_else(|| crate::Error::Signing("czd_for_alg failed".into()))?;
let mut scope = principal.begin_commit();
scope.verify_and_apply(&pay_vec, &sig_bytes, czd, None)?;
let tmb = coz::Thumbprint::from_bytes(
coz::base64ct::Base64UrlUnpadded::decode_vec(signer_tmb)
.map_err(|e| crate::Error::Signing(format!("invalid tmb base64: {}", e)))?,
);
scope.finalize_with_arrow(
&signer_stored.alg,
&signer_stored.prv_key,
&signer_stored.pub_key,
&tmb,
now,
authority,
)?;
let new_commits = export_commits(&principal)?;
for commit in new_commits.iter().skip(commits.len()) {
store.append_commit(&pr, commit)?;
}
match cli.output {
OutputFormat::Json => {
let output = serde_json::json!({
"identity": identity,
"revoked_key": key_tmb,
"signed_by": signer_tmb,
});
println!("{}", serde_json::to_string_pretty(&output)?);
},
OutputFormat::Table => {
println!("Revoked key from identity");
println!(" identity: {identity}");
println!(" key: {key_tmb}");
println!(" signed by: {signer_tmb}");
},
}
Ok(())
}
fn list(cli: &Cli, identity: Option<&str>) -> crate::Result<()> {
match identity {
None => list_keystore(cli),
Some(pr) => list_identity(cli, pr),
}
}
fn list_keystore(cli: &Cli) -> crate::Result<()> {
let keystore = JsonKeyStore::open(&cli.keystore)?;
let thumbprints = keystore.list();
if thumbprints.is_empty() {
match cli.output {
OutputFormat::Json => println!("[]"),
OutputFormat::Table => println!("No keys in keystore"),
}
return Ok(());
}
match cli.output {
OutputFormat::Json => {
let mut keys = Vec::new();
for tmb in &thumbprints {
let key = keystore.get(tmb)?;
keys.push(serde_json::json!({
"tmb": tmb,
"alg": key.alg,
"tag": key.tag,
}));
}
println!("{}", serde_json::to_string_pretty(&keys)?);
},
OutputFormat::Table => {
println!("Keys in keystore:");
for tmb in thumbprints {
let key = keystore.get(tmb)?;
let tag_str = key.tag.as_deref().unwrap_or("-");
println!(" {} ({}) [{}]", tmb, key.alg, tag_str);
}
},
}
Ok(())
}
fn list_identity(cli: &Cli, identity: &str) -> crate::Result<()> {
let keystore = JsonKeyStore::open(&cli.keystore)?;
let store = parse_store(&cli.store)?;
let pr = parse_principal_genesis(identity)?;
let commits = store.get_commits(&pr).unwrap_or_default();
let is_implicit_genesis = keystore.get(identity).is_ok();
let principal = if commits.is_empty() {
let genesis_key = load_key_from_keystore(&keystore, identity)?;
cyphr::Principal::implicit(genesis_key)?
} else if is_implicit_genesis {
let genesis_key = load_key_from_keystore(&keystore, identity)?;
let genesis = Genesis::Implicit(genesis_key);
load_principal_from_commits(genesis, &commits)?
} else {
let genesis = extract_genesis_from_commits(&commits, None)?;
load_principal_from_commits(genesis, &commits)?
};
let active: Vec<_> = principal.active_keys().collect();
match cli.output {
OutputFormat::Json => {
let keys: Vec<_> = active
.iter()
.map(|k| {
serde_json::json!({
"tmb": k.tmb.to_b64(),
"alg": k.alg,
"tag": k.tag,
})
})
.collect();
let output = serde_json::json!({
"identity": identity,
"active_keys": keys,
});
println!("{}", serde_json::to_string_pretty(&output)?);
},
OutputFormat::Table => {
println!("Active keys for {identity}:");
for key in active {
let tag_str = key.tag.as_deref().unwrap_or("-");
println!(" {} ({}) [{}]", key.tmb.to_b64(), key.alg, tag_str);
}
},
}
Ok(())
}