1use coz::base64ct::Encoding;
4use cyphr_storage::{Genesis, export_commits, load_principal_from_commits};
5use indexmap::IndexMap;
6use serde_json::Value;
7
8use super::common::{
9 current_timestamp, extract_genesis_from_commits, generate_key, load_key_from_keystore,
10 parse_principal_genesis, parse_store,
11};
12use crate::keystore::{JsonKeyStore, KeyStore};
13use crate::{Cli, KeyCommands, OutputFormat};
14
15pub fn run(cli: &Cli, command: &KeyCommands) -> crate::Result<()> {
17 match command {
18 KeyCommands::Generate { algo, tag } => generate(cli, algo, tag.as_deref()),
19 KeyCommands::Add {
20 identity,
21 key,
22 signer,
23 } => add(cli, identity, key.as_deref(), signer, &cli.authority),
24 KeyCommands::Revoke {
25 identity,
26 key,
27 signer,
28 } => revoke(cli, identity, key, signer, &cli.authority),
29 KeyCommands::List { identity } => list(cli, identity.as_deref()),
30 }
31}
32
33fn generate(cli: &Cli, algo: &str, tag: Option<&str>) -> crate::Result<()> {
35 let mut keystore = JsonKeyStore::open(&cli.keystore)?;
36
37 let (tmb, stored_key, _key) = generate_key(algo, tag)?;
38 keystore.store(&tmb, stored_key)?;
39 keystore.save()?;
40
41 match cli.output {
42 OutputFormat::Json => {
43 let output = serde_json::json!({
44 "tmb": tmb,
45 "alg": algo,
46 "tag": tag,
47 });
48 println!("{}", serde_json::to_string_pretty(&output)?);
49 },
50 OutputFormat::Table => {
51 println!("Generated {algo} key");
52 println!(" tmb: {tmb}");
53 if let Some(t) = tag {
54 println!(" tag: {t}");
55 }
56 println!(" stored: {}", cli.keystore.display());
57 },
58 }
59
60 Ok(())
61}
62
63fn add(
65 cli: &Cli,
66 identity: &str,
67 key_tmb: Option<&str>,
68 signer_tmb: &str,
69 authority: &str,
70) -> crate::Result<()> {
71 let mut keystore = JsonKeyStore::open(&cli.keystore)?;
72 let store = parse_store(&cli.store)?;
73 let pr = parse_principal_genesis(identity)?;
74
75 let commits = store.get_commits(&pr).unwrap_or_default();
77
78 let is_implicit_genesis = keystore.get(identity).is_ok();
80
81 let mut principal = if commits.is_empty() {
82 let genesis_key = load_key_from_keystore(&keystore, identity)?;
84 cyphr::Principal::implicit(genesis_key)?
85 } else if is_implicit_genesis {
86 let genesis_key = load_key_from_keystore(&keystore, identity)?;
88 let genesis = Genesis::Implicit(genesis_key);
89 load_principal_from_commits(genesis, &commits)?
90 } else {
91 let genesis = extract_genesis_from_commits(&commits, None)?;
93 load_principal_from_commits(genesis, &commits)?
94 };
95
96 let (new_key_tmb, new_key) = match key_tmb {
98 Some(tmb) => {
99 let key = load_key_from_keystore(&keystore, tmb)?;
100 (tmb.to_string(), key)
101 },
102 None => {
103 let signer_stored = keystore.get(signer_tmb)?;
105 let (tmb, stored, key) = generate_key(&signer_stored.alg, None)?;
106 keystore.store(&tmb, stored)?;
107 keystore.save()?;
108 (tmb, key)
109 },
110 };
111
112 let signer_stored = keystore.get(signer_tmb)?;
114
115 let now = current_timestamp();
117 let pre = principal.pr_tagged()?;
118
119 let mut pay_map: IndexMap<String, Value> = IndexMap::new();
120 pay_map.insert("alg".to_string(), Value::String(signer_stored.alg.clone()));
121 pay_map.insert("id".to_string(), Value::String(new_key_tmb.clone()));
122 pay_map.insert("now".to_string(), Value::Number(now.into()));
123 pay_map.insert("pre".to_string(), Value::String(pre));
124 pay_map.insert("tmb".to_string(), Value::String(signer_tmb.to_string()));
125 pay_map.insert(
126 "typ".to_string(),
127 Value::String(format!(
128 "{}/{}",
129 authority,
130 cyphr::parsed_coz::typ::KEY_CREATE
131 )),
132 );
133
134 let pay_value: Value = serde_json::to_value(&pay_map)?;
135
136 let pay_vec = serde_json::to_vec(&pay_value)?;
138 let (sig_bytes, cad) = coz::sign_json(
139 &pay_vec,
140 &signer_stored.alg,
141 &signer_stored.prv_key,
142 &signer_stored.pub_key,
143 )
144 .ok_or_else(|| crate::Error::Signing("sign_json failed".into()))?;
145 let czd = coz::czd_for_alg(&cad, &sig_bytes, &signer_stored.alg)
146 .ok_or_else(|| crate::Error::Signing("czd_for_alg failed".into()))?;
147 let mut scope = principal.begin_commit();
148 scope.verify_and_apply(&pay_vec, &sig_bytes, czd, Some(new_key.clone()))?;
149 let tmb = coz::Thumbprint::from_bytes(
150 coz::base64ct::Base64UrlUnpadded::decode_vec(signer_tmb)
151 .map_err(|e| crate::Error::Signing(format!("invalid tmb base64: {}", e)))?,
152 );
153 scope.finalize_with_arrow(
154 &signer_stored.alg,
155 &signer_stored.prv_key,
156 &signer_stored.pub_key,
157 &tmb,
158 now,
159 authority,
160 )?;
161
162 let new_commits = export_commits(&principal)?;
164 for commit in new_commits.iter().skip(commits.len()) {
166 store.append_commit(&pr, commit)?;
167 }
168
169 match cli.output {
170 OutputFormat::Json => {
171 let output = serde_json::json!({
172 "identity": identity,
173 "added_key": new_key_tmb,
174 "signed_by": signer_tmb,
175 });
176 println!("{}", serde_json::to_string_pretty(&output)?);
177 },
178 OutputFormat::Table => {
179 println!("Added key to identity");
180 println!(" identity: {identity}");
181 println!(" key: {new_key_tmb}");
182 println!(" signed by: {signer_tmb}");
183 },
184 }
185
186 Ok(())
187}
188
189fn revoke(
191 cli: &Cli,
192 identity: &str,
193 key_tmb: &str,
194 signer_tmb: &str,
195 authority: &str,
196) -> crate::Result<()> {
197 let keystore = JsonKeyStore::open(&cli.keystore)?;
198 let store = parse_store(&cli.store)?;
199 let pr = parse_principal_genesis(identity)?;
200
201 let commits = store.get_commits(&pr).unwrap_or_default();
203
204 let is_implicit_genesis = keystore.get(identity).is_ok();
206
207 let mut principal = if commits.is_empty() {
208 let genesis_key = load_key_from_keystore(&keystore, identity)?;
210 cyphr::Principal::implicit(genesis_key)?
211 } else if is_implicit_genesis {
212 let genesis_key = load_key_from_keystore(&keystore, identity)?;
214 let genesis = Genesis::Implicit(genesis_key);
215 load_principal_from_commits(genesis, &commits)?
216 } else {
217 let genesis = extract_genesis_from_commits(&commits, None)?;
219 load_principal_from_commits(genesis, &commits)?
220 };
221
222 let signer_stored = keystore.get(signer_tmb)?;
224
225 if key_tmb != signer_tmb {
228 return Err(crate::Error::InvalidArgument(
229 "key revoke only supports self-revoke: --key must equal --signer".into(),
230 ));
231 }
232
233 let now = current_timestamp();
236 let pre = principal.pr_tagged()?;
237
238 let mut pay_map: IndexMap<String, Value> = IndexMap::new();
239 pay_map.insert("alg".to_string(), Value::String(signer_stored.alg.clone()));
240 pay_map.insert("now".to_string(), Value::Number(now.into()));
241 pay_map.insert("pre".to_string(), Value::String(pre));
242 pay_map.insert("rvk".to_string(), Value::Number(now.into()));
243 pay_map.insert("tmb".to_string(), Value::String(signer_tmb.to_string()));
244 pay_map.insert(
245 "typ".to_string(),
246 Value::String(format!(
247 "{}/{}",
248 authority,
249 cyphr::parsed_coz::typ::KEY_REVOKE
250 )),
251 );
252
253 let pay_value: Value = serde_json::to_value(&pay_map)?;
254
255 let pay_vec = serde_json::to_vec(&pay_value)?;
257 let (sig_bytes, cad) = coz::sign_json(
258 &pay_vec,
259 &signer_stored.alg,
260 &signer_stored.prv_key,
261 &signer_stored.pub_key,
262 )
263 .ok_or_else(|| crate::Error::Signing("sign_json failed".into()))?;
264 let czd = coz::czd_for_alg(&cad, &sig_bytes, &signer_stored.alg)
265 .ok_or_else(|| crate::Error::Signing("czd_for_alg failed".into()))?;
266 let mut scope = principal.begin_commit();
267 scope.verify_and_apply(&pay_vec, &sig_bytes, czd, None)?;
268 let tmb = coz::Thumbprint::from_bytes(
269 coz::base64ct::Base64UrlUnpadded::decode_vec(signer_tmb)
270 .map_err(|e| crate::Error::Signing(format!("invalid tmb base64: {}", e)))?,
271 );
272 scope.finalize_with_arrow(
273 &signer_stored.alg,
274 &signer_stored.prv_key,
275 &signer_stored.pub_key,
276 &tmb,
277 now,
278 authority,
279 )?;
280
281 let new_commits = export_commits(&principal)?;
283 for commit in new_commits.iter().skip(commits.len()) {
284 store.append_commit(&pr, commit)?;
285 }
286
287 match cli.output {
288 OutputFormat::Json => {
289 let output = serde_json::json!({
290 "identity": identity,
291 "revoked_key": key_tmb,
292 "signed_by": signer_tmb,
293 });
294 println!("{}", serde_json::to_string_pretty(&output)?);
295 },
296 OutputFormat::Table => {
297 println!("Revoked key from identity");
298 println!(" identity: {identity}");
299 println!(" key: {key_tmb}");
300 println!(" signed by: {signer_tmb}");
301 },
302 }
303
304 Ok(())
305}
306
307fn list(cli: &Cli, identity: Option<&str>) -> crate::Result<()> {
309 match identity {
310 None => list_keystore(cli),
311 Some(pr) => list_identity(cli, pr),
312 }
313}
314
315fn list_keystore(cli: &Cli) -> crate::Result<()> {
317 let keystore = JsonKeyStore::open(&cli.keystore)?;
318 let thumbprints = keystore.list();
319
320 if thumbprints.is_empty() {
321 match cli.output {
322 OutputFormat::Json => println!("[]"),
323 OutputFormat::Table => println!("No keys in keystore"),
324 }
325 return Ok(());
326 }
327
328 match cli.output {
329 OutputFormat::Json => {
330 let mut keys = Vec::new();
331 for tmb in &thumbprints {
332 let key = keystore.get(tmb)?;
333 keys.push(serde_json::json!({
334 "tmb": tmb,
335 "alg": key.alg,
336 "tag": key.tag,
337 }));
338 }
339 println!("{}", serde_json::to_string_pretty(&keys)?);
340 },
341 OutputFormat::Table => {
342 println!("Keys in keystore:");
343 for tmb in thumbprints {
344 let key = keystore.get(tmb)?;
345 let tag_str = key.tag.as_deref().unwrap_or("-");
346 println!(" {} ({}) [{}]", tmb, key.alg, tag_str);
347 }
348 },
349 }
350
351 Ok(())
352}
353
354fn list_identity(cli: &Cli, identity: &str) -> crate::Result<()> {
356 let keystore = JsonKeyStore::open(&cli.keystore)?;
357 let store = parse_store(&cli.store)?;
358 let pr = parse_principal_genesis(identity)?;
359
360 let commits = store.get_commits(&pr).unwrap_or_default();
361
362 let is_implicit_genesis = keystore.get(identity).is_ok();
364
365 let principal = if commits.is_empty() {
366 let genesis_key = load_key_from_keystore(&keystore, identity)?;
367 cyphr::Principal::implicit(genesis_key)?
368 } else if is_implicit_genesis {
369 let genesis_key = load_key_from_keystore(&keystore, identity)?;
370 let genesis = Genesis::Implicit(genesis_key);
371 load_principal_from_commits(genesis, &commits)?
372 } else {
373 let genesis = extract_genesis_from_commits(&commits, None)?;
374 load_principal_from_commits(genesis, &commits)?
375 };
376
377 let active: Vec<_> = principal.active_keys().collect();
378
379 match cli.output {
380 OutputFormat::Json => {
381 let keys: Vec<_> = active
382 .iter()
383 .map(|k| {
384 serde_json::json!({
385 "tmb": k.tmb.to_b64(),
386 "alg": k.alg,
387 "tag": k.tag,
388 })
389 })
390 .collect();
391 let output = serde_json::json!({
392 "identity": identity,
393 "active_keys": keys,
394 });
395 println!("{}", serde_json::to_string_pretty(&output)?);
396 },
397 OutputFormat::Table => {
398 println!("Active keys for {identity}:");
399 for key in active {
400 let tag_str = key.tag.as_deref().unwrap_or("-");
401 println!(" {} ({}) [{}]", key.tmb.to_b64(), key.alg, tag_str);
402 }
403 },
404 }
405
406 Ok(())
407}