fce44ae9 Andy McCall 1 month ago ↶
1
2
3
use super :: format:: { short_id, signature_line, when} ;
75167672 Andy McCall 1 month ago
4
use super :: json;
fce44ae9 Andy McCall 1 month ago ↶
5
use super :: open;
135212de Andy McCall 1 month ago ↶
6
use super :: ui;
fce44ae9 Andy McCall 1 month ago ↶
7
use anyhow:: Result;
8
use oxy_core:: hash:: ChangeHash;
75167672 Andy McCall 1 month ago
9
use oxy_core:: hash:: KeyId;
7ba155f2 Andy McCall 1 month ago ↶
10
use oxy_core:: identity:: { Delegate, Identity, Scope} ;
28fc23d7 Andy McCall 1 month ago ↶
11
use oxy_core:: objects:: Hlc;
12
use oxy_core:: protocol:: SignedPolicy;
c6aa5bd9 Andy McCall 1 month ago ↶
13
use oxy_core:: repo:: { self , IdentityScope, PolicyUpdate, Repo, Rotation, SignatureStatus} ;
83e0677b Andy McCall 1 month ago ↶
14
use std:: path:: { Path, PathBuf} ;
fce44ae9 Andy McCall 1 month ago ↶
15
28fc23d7 Andy McCall 1 month ago ↶
16
const DAY_MS : u64 = 24 * 60 * 60 * 1000 ;
17
18
fn expiry_from_days ( days : u64 ) -> Option< u64 > {
19
( days > 0 ) .then ( | | Hlc:: now( ) .wall_ms + days * DAY_MS )
20
}
21
7ba155f2 Andy McCall 1 month ago ↶
22
23
fn expiry_from_duration ( s : & str ) -> Result< u64 > {
24
let s = s.trim ( ) ;
25
let ( n, unit) = match s.char_indices ( ) .find ( | ( _, c ) | ! c.is_ascii_digit ( ) ) {
26
Some ( ( i, _ ) ) => ( & s[ .. i] , & s[ i.. ] ) ,
27
None => ( s, " d" ) ,
28
} ;
29
let n: u64 = n.parse ( ) .map_err ( | _| {
30
anyhow:: anyhow! ( " --expires takes 30m, 8h, 7d or a number of days, not {s:?}" )
31
} ) ? ;
32
let ms = match unit {
33
" m" => 60_000 ,
34
" h" => 3_600_000 ,
35
" d" => DAY_MS ,
36
_ => anyhow:: bail! ( " --expires takes 30m, 8h, 7d or a number of days, not {s:?}" ) ,
37
} ;
38
if n = = 0 {
39
anyhow:: bail! ( " --expires needs a length of time; leave it out for no expiry" ) ;
40
}
41
Ok ( Hlc:: now( ) .wall_ms + n * ms)
42
}
43
5cde7882 Andy McCall 1 month ago ↶
44
45
46
fn print_delegates ( repo : & Repo, delegates : & [Delegate]) {
47
if delegates.is_empty ( ) {
48
return ;
49
}
50
println! ( " {} " , ui:: kv( " delegates" , delegates.len ( ) .to_string ( ) ) ) ;
51
let now = Hlc:: now( ) .wall_ms;
52
for d in delegates {
53
let who = repo
54
.identity ( & d.key)
55
.map ( | i | format! ( " {} <{} >" , i.name, i.email) )
56
.unwrap_or_else ( | _| " (unknown identity)" .into ( ) ) ;
57
let until = match d.expires {
58
Some ( e) if now > e => ui:: err( format! ( " expired {} " , ui:: ago( e) ) ) ,
59
Some ( e) => ui:: dim( format! ( " until {} " , when ( e) ) ) ,
60
None => String :: new( ) ,
61
} ;
62
println! (
63
" {} {} {} {} " ,
64
ui:: author( who) ,
65
ui:: hash( d.key.short ( 12 ) ) ,
66
ui:: dim( d.scope.to_string ( ) ) ,
67
until
68
) ;
69
}
7ba155f2 Andy McCall 1 month ago ↶
70
}
71
28fc23d7 Andy McCall 1 month ago ↶
72
73
fn resolve_key ( repo : & Repo, prefix : & str ) -> Result< KeyId> {
74
if let Some ( k) = KeyId:: from_hex( prefix) {
75
return Ok ( k) ;
76
}
77
let mut candidates: Vec< KeyId> = Vec :: new( ) ;
78
if let Some ( ( _ , sp) ) = repo.current_policy ( ) ? {
79
candidates.extend ( sp.policy.identities.iter ( ) .map ( | i | i.hash ( ) ) ) ;
80
candidates.extend ( sp.policy.revoked.iter ( ) .copied ( ) ) ;
81
}
82
if let Some ( own) = repo.config.author_key {
83
candidates.push ( own) ;
84
if let Ok ( id) = repo.identity ( & own) {
7ba155f2 Andy McCall 1 month ago ↶
85
candidates.extend ( id.delegates.iter ( ) .map ( | d | d.key ) ) ;
28fc23d7 Andy McCall 1 month ago ↶
86
}
87
}
88
candidates.sort ( ) ;
89
candidates.dedup ( ) ;
75167672 Andy McCall 1 month ago
90
let matches: Vec< _ > = candidates
91
.into_iter ( )
92
.filter ( | k | k.starts_with_hex ( prefix) )
93
.collect ( ) ;
28fc23d7 Andy McCall 1 month ago ↶
94
match matches.len ( ) {
95
1 => Ok ( matches[ 0 ] ) ,
96
0 => anyhow:: bail! ( " no known key starts with {prefix:?}" ) ,
97
_ => anyhow:: bail! ( " {prefix:?} is ambiguous" ) ,
98
}
99
}
100
101
102
103
fn report_update ( u : & PolicyUpdate, out : Option< PathBuf> ) -> Result< ( ) > {
104
if u.applied {
75167672 Andy McCall 1 month ago
105
println! (
106
" {} policy serial {} {} " ,
107
ui:: done( " Applied" ) ,
108
u.policy.serial,
109
ui:: dim( " (push to publish it)" )
110
) ;
28fc23d7 Andy McCall 1 month ago ↶
111
return Ok ( ( ) ) ;
112
}
113
let path = out.unwrap_or_else ( | | PathBuf:: from( format! ( " policy-{} .proposal" , u.policy.serial) ) ) ;
114
std:: fs:: write( & path, u.signed ( ) .encode ( ) ) ? ;
115
println! (
116
" {} policy serial {} needs {} of {} root signatures; has {} . Wrote {} for another root to run {} " ,
117
ui:: caution( ) ,
118
u.policy.serial,
119
u.policy.root_threshold,
120
u.policy.roots.len ( ) ,
121
u.sigs.len ( ) ,
122
ui:: path( path.display ( ) ) ,
123
ui:: dim( format! ( " oxy policy sign {} " , path.display ( ) ) )
124
) ;
125
Ok ( ( ) )
126
}
127
5cde7882 Andy McCall 1 month ago ↶
128
fn report_rotation ( repo : & Repo, r : & Rotation, verb : & str ) -> Result< ( ) > {
75167672 Andy McCall 1 month ago
129
println! (
130
" {} identity {} <{} >" ,
131
ui:: done( verb) ,
132
ui:: author( & r.identity.name) ,
133
r.identity.email
134
) ;
28fc23d7 Andy McCall 1 month ago ↶
135
println! ( " {} " , ui:: kv( " old key" , ui:: dim( r.old.to_hex ( ) ) ) ) ;
136
println! ( " {} " , ui:: kv( " new key" , r.new.to_hex ( ) ) ) ;
5cde7882 Andy McCall 1 month ago ↶
137
print_delegates ( repo, & r.identity.delegates) ;
28fc23d7 Andy McCall 1 month ago ↶
138
if let Some ( e) = r.identity.expires {
139
println! ( " {} " , ui:: kv( " expires" , when ( e) ) ) ;
140
}
141
match & r.policy {
142
Some ( u) => report_update ( u, None ) ? ,
75167672 Andy McCall 1 month ago
143
None => println! (
144
" {} {} " ,
145
ui:: note( ) ,
146
ui:: dim( " no policy names the old key; export the new identity for policy holders" )
147
) ,
28fc23d7 Andy McCall 1 month ago ↶
148
}
149
Ok ( ( ) )
150
}
151
75167672 Andy McCall 1 month ago
152
pub fn create (
153
name : Option< String> ,
154
email : Option< String> ,
155
local : bool ,
156
expires : Option< u64 > ,
157
) -> Result< i32 > {
fce44ae9 Andy McCall 1 month ago ↶
158
let mut repo = open ( ) ? ;
159
let name = name.unwrap_or_else ( | | repo.config.author_name.clone ( ) ) ;
160
let email = email.unwrap_or_else ( | | repo.config.author_email.clone ( ) ) ;
75167672 Andy McCall 1 month ago
161
let scope = if local {
162
IdentityScope:: Repo
163
} else {
164
IdentityScope:: User
165
} ;
166
let ( key, _ ) =
167
repo.create_identity_with ( & name, & email, scope, expires.and_then ( expiry_from_days) ) ? ;
168
println! (
169
" {} identity {} <{email} >" ,
170
ui:: done( " Created" ) ,
171
ui:: author( & name)
172
) ;
94084087 Andy McCall 1 month ago ↶
173
println! ( " {} " , ui:: kv( " key" , key) ) ;
75167672 Andy McCall 1 month ago
174
println! (
175
" {} " ,
176
ui:: kv( " secret" , ui:: path( repo:: default_keys_dir( ) .display ( ) ) )
177
) ;
fce44ae9 Andy McCall 1 month ago ↶
178
match scope {
75167672 Andy McCall 1 month ago
179
IdentityScope:: User => println! (
180
" {} " ,
181
ui:: kv(
182
" scope" ,
183
format! (
184
" default author in {} " ,
185
ui:: path( repo.user_config_path ( ) .display ( ) )
186
)
187
)
188
) ,
189
IdentityScope:: Repo => {
190
println! ( " {} " , ui:: kv( " scope" , " this repository's .oxy/config only" ) )
191
}
fce44ae9 Andy McCall 1 month ago ↶
192
}
193
if ! repo.config.sign_changes {
94084087 Andy McCall 1 month ago ↶
194
println! ( " {} {} " , ui:: note( ) , ui:: dim( " changes will carry this author; run oxy config --user core.sign change to sign each commit" ) ) ;
fce44ae9 Andy McCall 1 month ago ↶
195
}
196
Ok ( 0 )
197
}
198
199
pub fn show ( ) -> Result< i32 > {
200
let repo = open ( ) ? ;
201
let Some ( key) = repo.config.author_key else {
202
println! ( " No identity configured; run `oxy identity create`." ) ;
203
return Ok ( 0 ) ;
204
} ;
205
let id = repo.identity ( & key) ? ;
75167672 Andy McCall 1 month ago
206
println! (
207
" {} {} <{} >" ,
208
ui:: section( " identity" ) ,
209
ui:: author( & id.name) ,
210
id.email
211
) ;
94084087 Andy McCall 1 month ago ↶
212
println! ( " {} " , ui:: kv( " key" , key) ) ;
75167672 Andy McCall 1 month ago
213
println! (
214
" {} " ,
215
ui:: kv(
216
" created" ,
217
format! (
218
" {} {} " ,
219
when ( id.created) ,
220
ui:: dim( format! ( " ({} )" , ui:: ago( id.created) ) )
221
)
222
)
223
) ;
28fc23d7 Andy McCall 1 month ago ↶
224
if let Some ( e) = id.expires {
225
let expired = id.is_expired_at ( Hlc:: now( ) .wall_ms) ;
75167672 Andy McCall 1 month ago
226
println! (
227
" {} " ,
228
ui:: kv(
229
" expires" ,
230
if expired {
231
ui:: err( format! ( " {} EXPIRED" , when ( e) ) )
232
} else {
233
when ( e)
234
}
235
)
236
) ;
28fc23d7 Andy McCall 1 month ago ↶
237
}
238
if let Some ( p) = id.previous {
239
println! ( " {} " , ui:: kv( " previous" , ui:: dim( p.short ( 12 ) ) ) ) ;
240
}
614f55e1 Andy McCall 1 month ago ↶
241
242
243
if let Some ( pinned) = repo
244
.config
245
.repo_layer ( )
246
.get ( " author.key" )
247
.and_then ( | k | oxy_core:: hash:: KeyId:: from_hex( k) )
248
{
249
let user_key = repo:: read_config_file( & repo:: default_user_config( ) )
250
.ok ( )
251
.and_then ( | m | {
252
m.get ( " author.key" )
253
.and_then ( | k | oxy_core:: hash:: KeyId:: from_hex( k) )
254
} ) ;
255
if let Some ( u) = user_key.filter ( | u | * u ! = pinned) {
256
let related = | from : oxy_core:: hash:: KeyId, to : oxy_core:: hash:: KeyId| {
257
let mut cur = Some ( from) ;
258
for _ in 0 .. 64 {
259
match cur {
260
Some ( k) if k = = to => return true ,
261
Some ( k) => cur = repo.identity ( & k) .ok ( ) .and_then ( | i | i.previous ) ,
262
None => break ,
263
}
264
}
265
false
266
} ;
267
let how = if related ( pinned, u) {
268
" an older key of this identity"
269
} else if related ( u, pinned) {
270
" a newer key of this identity"
271
} else {
272
" another key"
273
} ;
274
println! (
275
" {} {} " ,
276
ui:: caution( ) ,
277
ui:: dim( format! (
278
" this repository pins author.key {} ; your user config names {how} , {} . Other checkouts follow the user config: `oxy config --unset author.key` here, or `oxy config --user author.key {} `" ,
279
pinned.short ( 12 ) ,
280
u.short ( 12 ) ,
281
pinned.to_hex ( )
282
) )
283
) ;
284
}
285
}
5cde7882 Andy McCall 1 month ago ↶
286
print_delegates ( & repo, & id.delegates) ;
28fc23d7 Andy McCall 1 month ago ↶
287
if repo.is_revoked ( & key) ? {
75167672 Andy McCall 1 month ago
288
println! (
289
" {} " ,
290
ui:: kv(
291
" status" ,
292
ui:: err( format! ( " {} REVOKED" , ui:: glyph( " ✖" , " x" ) ) )
293
)
294
) ;
28fc23d7 Andy McCall 1 month ago ↶
295
}
75167672 Andy McCall 1 month ago
296
println! (
297
" {} " ,
298
ui:: kv(
299
" secret" ,
300
if repo.secret_key ( ) ? .is_some ( ) {
301
ui:: ok( format! ( " {} available" , ui:: glyph( " ✔" , " *" ) ) )
302
} else {
303
ui:: err( format! (
304
" {} not found in the key directory" ,
305
ui:: glyph( " ✖" , " x" )
306
) )
307
}
308
)
309
) ;
310
println! (
311
" {} " ,
312
ui:: kv(
313
" signing" ,
314
if repo.config.sign_changes {
315
" every change (core.sign = change)"
316
} else {
317
" at push via seals (core.sign = seal)"
318
}
319
)
320
) ;
fce44ae9 Andy McCall 1 month ago ↶
321
Ok ( 0 )
322
}
323
2c9ddac3 Andy McCall 1 month ago ↶
324
pub fn verify ( change : Option< & str > , revset : Option< & str > , as_json : bool ) -> Result< i32 > {
fce44ae9 Andy McCall 1 month ago ↶
325
let repo = open ( ) ? ;
2c9ddac3 Andy McCall 1 month ago ↶
326
let hashes: Vec< ChangeHash> = match ( change, revset) {
327
( _ , Some ( r) ) => {
328
let wc = repo.working_copy ( ) .ok ( ) .map ( | ( h , _) | h ) ;
329
repo.revset ( r) ?
330
.hashes
331
.into_iter ( )
332
.filter ( | h | Some ( * h) ! = wc)
333
.collect ( )
334
}
335
( Some ( c) , None ) => vec! [ repo.resolve_change ( c) ? ] ,
336
( None , None ) => repo
75167672 Andy McCall 1 month ago
337
.log ( ) ?
338
.into_iter ( )
339
.filter ( | e | ! e.is_working_copy)
340
.map ( | e | e.hash )
341
.collect ( ) ,
fce44ae9 Andy McCall 1 month ago ↶
342
} ;
343
let mut bad = 0 ;
c6aa5bd9 Andy McCall 1 month ago ↶
344
if as_json {
345
let mut items = Vec :: new( ) ;
346
for h in & hashes {
75167672 Andy McCall 1 month ago
347
let Ok ( c) = repo.store ( ) .get_change ( h) else {
348
continue ;
349
} ;
c6aa5bd9 Andy McCall 1 month ago ↶
350
let ( status, key, name, revoked, reason) = match repo.authenticate_change ( h) ? {
351
SignatureStatus:: Unsigned => ( " unsigned" , None , None , false , None ) ,
75167672 Andy McCall 1 month ago
352
SignatureStatus:: Valid {
353
key,
354
name,
355
sealed,
356
revoked,
357
..
358
} => (
359
if sealed { " sealed" } else { " signed" } ,
360
Some ( key.to_hex ( ) ) ,
361
Some ( name) ,
362
revoked,
363
None ,
364
) ,
c6aa5bd9 Andy McCall 1 month ago ↶
365
SignatureStatus:: Invalid( why) => {
366
bad + = 1 ;
367
( " invalid" , None , None , false , Some ( why) )
368
}
369
} ;
370
items.push ( json:: obj( [
371
( " hash" , json:: s( & h.to_hex ( ) ) ) ,
372
( " change_id" , json:: s( & c.change_id.to_hex ( ) ) ) ,
373
( " status" , json:: s( status) ) ,
374
( " key" , json:: opt( key.as_deref ( ) , json:: s) ) ,
375
( " name" , json:: opt( name.as_deref ( ) , json:: s) ) ,
376
( " revoked" , json:: b( revoked) ) ,
377
( " reason" , json:: opt( reason.as_deref ( ) , json:: s) ) ,
378
] ) ) ;
379
}
380
println! ( " {} " , json:: arr( items) ) ;
381
return Ok ( if bad > 0 { 1 } else { 0 } ) ;
382
}
fce44ae9 Andy McCall 1 month ago ↶
383
for h in hashes {
2863456a Andy McCall 1 month ago ↶
384
let Ok ( c) = repo.store ( ) .get_change ( & h) else {
75167672 Andy McCall 1 month ago
385
println! (
386
" {} {} " ,
387
ui:: hash( format! ( " #{} " , h.short ( 12 ) ) ) ,
388
ui:: dim( " not present (shallow)" )
389
) ;
2863456a Andy McCall 1 month ago ↶
390
continue ;
391
} ;
fce44ae9 Andy McCall 1 month ago ↶
392
let line = signature_line ( & repo, & h) ? ;
135212de Andy McCall 1 month ago ↶
393
let styled = if line.starts_with ( " INVALID" ) {
fce44ae9 Andy McCall 1 month ago ↶
394
bad + = 1 ;
94084087 Andy McCall 1 month ago ↶
395
ui:: err( format! ( " {} {line} " , ui:: glyph( " ✖" , " x" ) ) )
135212de Andy McCall 1 month ago ↶
396
} else if line = = " unsigned" {
94084087 Andy McCall 1 month ago ↶
397
ui:: dim( format! ( " {} {line} " , ui:: glyph( " ·" , " -" ) ) )
135212de Andy McCall 1 month ago ↶
398
} else {
94084087 Andy McCall 1 month ago ↶
399
ui:: ok( format! ( " {} {line} " , ui:: glyph( " ✔" , " v" ) ) )
135212de Andy McCall 1 month ago ↶
400
} ;
75167672 Andy McCall 1 month ago
401
println! (
402
" {} {} {styled} " ,
403
ui:: id( short_id ( & c) ) ,
404
ui:: hash( format! ( " #{} " , h.short ( 12 ) ) )
405
) ;
fce44ae9 Andy McCall 1 month ago ↶
406
}
407
Ok ( if bad > 0 { 1 } else { 0 } )
408
}
409
614f55e1 Andy McCall 1 month ago ↶
410
pub fn config ( key : & str , value : & str , user : bool , unset : bool ) -> Result< i32 > {
411
412
let value = if unset { " " } else { value } ;
413
let said = if unset {
414
format! ( " {} {key} " , ui:: done( " Unset" ) )
415
} else {
416
format! ( " {} {key} = {value} " , ui:: done( " Set" ) )
417
} ;
fce44ae9 Andy McCall 1 month ago ↶
418
if user {
419
let path = repo:: default_user_config( ) ;
420
repo:: Config:: set_user( & path, key, value) ? ;
614f55e1 Andy McCall 1 month ago ↶
421
println! ( " {said} {} " , ui:: dim( format! ( " (in {} )" , path.display ( ) ) ) ) ;
fce44ae9 Andy McCall 1 month ago ↶
422
} else {
423
let mut repo = open ( ) ? ;
424
repo.config.set ( key, value) ? ;
425
repo.save_config ( ) ? ;
614f55e1 Andy McCall 1 month ago ↶
426
println! ( " {said} {} " , ui:: dim( " (in this repository)" ) ) ;
fce44ae9 Andy McCall 1 month ago ↶
427
}
428
Ok ( 0 )
429
}
83e0677b Andy McCall 1 month ago ↶
430
431
pub fn export ( path : Option< PathBuf> ) -> Result< i32 > {
432
let repo = open ( ) ? ;
433
let Some ( key) = repo.config.author_key else {
434
anyhow:: bail! ( " no identity configured; run `oxy identity create`" ) ;
435
} ;
436
let id = repo.identity ( & key) ? ;
437
let path = path.unwrap_or_else ( | | PathBuf:: from( format! ( " {} .identity" , key.short ( 16 ) ) ) ) ;
438
std:: fs:: write( & path, id.encode ( ) ) ? ;
75167672 Andy McCall 1 month ago
439
println! (
440
" {} identity {} <{} > ({} ) to {} " ,
441
ui:: done( " Wrote" ) ,
442
ui:: author( & id.name) ,
443
id.email,
444
key.short ( 12 ) ,
445
ui:: path( path.display ( ) )
446
) ;
83e0677b Andy McCall 1 month ago ↶
447
Ok ( 0 )
448
}
449
28fc23d7 Andy McCall 1 month ago ↶
450
pub fn rotate ( ) -> Result< i32 > {
451
let mut repo = open ( ) ? ;
452
let r = repo.rotate_identity ( ) ? ;
5cde7882 Andy McCall 1 month ago ↶
453
report_rotation ( & repo, & r, " Rotated" ) ? ;
28fc23d7 Andy McCall 1 month ago ↶
454
Ok ( 0 )
455
}
456
457
pub fn renew ( expires : u64 ) -> Result< i32 > {
458
let mut repo = open ( ) ? ;
459
let r = repo.renew_identity ( expiry_from_days ( expires) ) ? ;
5cde7882 Andy McCall 1 month ago ↶
460
report_rotation ( & repo, & r, " Renewed" ) ? ;
28fc23d7 Andy McCall 1 month ago ↶
461
Ok ( 0 )
462
}
463
464
pub fn revoke ( key : Option< & str > , reason : & str ) -> Result< i32 > {
465
let repo = open ( ) ? ;
466
let key = match key {
467
Some ( k) => resolve_key ( & repo, k) ? ,
75167672 Andy McCall 1 month ago
468
None => repo
469
.config
470
.author_key
471
.ok_or_else ( | | anyhow:: anyhow! ( " no identity configured" ) ) ? ,
28fc23d7 Andy McCall 1 month ago ↶
472
} ;
473
let update = repo.revoke_identity ( & key, reason) ? ;
75167672 Andy McCall 1 month ago
474
println! (
475
" {} key {} {} " ,
476
ui:: done( " Revoked" ) ,
477
ui:: hash( key.short ( 12 ) ) ,
478
if reason.is_empty ( ) {
479
String :: new( )
480
} else {
481
format! ( " : {reason} " )
482
}
483
) ;
28fc23d7 Andy McCall 1 month ago ↶
484
match update {
485
Some ( u) => report_update ( & u, None ) ? ,
75167672 Andy McCall 1 month ago
486
None => println! (
487
" {} {} " ,
488
ui:: note( ) ,
489
ui:: dim( " recorded locally; no policy named the key" )
490
) ,
28fc23d7 Andy McCall 1 month ago ↶
491
}
7ba155f2 Andy McCall 1 month ago ↶
492
493
494
let own = repo.config.author_key;
495
let delegated = own.is_some_and ( | o | {
496
repo.identity ( & o)
497
.is_ok_and ( | id | id.delegate ( & key) .is_some ( ) )
498
} ) ;
499
if delegated {
500
let mut repo = repo;
501
let r = repo.delegate_remove ( & key) ? ;
5cde7882 Andy McCall 1 month ago ↶
502
report_rotation ( & repo, & r, " Undelegated" ) ? ;
7ba155f2 Andy McCall 1 month ago ↶
503
return Ok ( 0 ) ;
504
}
28fc23d7 Andy McCall 1 month ago ↶
505
if repo.config.author_key = = Some ( key) {
75167672 Andy McCall 1 month ago
506
println! (
507
" {} {} " ,
508
ui:: caution( ) ,
509
ui:: warn( " this was your configured key; run oxy identity create or rotate" )
510
) ;
28fc23d7 Andy McCall 1 month ago ↶
511
}
512
Ok ( 0 )
513
}
514
614f55e1 Andy McCall 1 month ago ↶
515
pub fn delegate_add ( identity_files : & [PathBuf], scope : & str , expires : Option< & str > ) -> Result< i32 > {
28fc23d7 Andy McCall 1 month ago ↶
516
let mut repo = open ( ) ? ;
614f55e1 Andy McCall 1 month ago ↶
517
let ids = identity_files
518
.iter ( )
519
.map ( | f | {
520
Identity:: decode(
521
& std:: fs:: read( f) .map_err ( | e | anyhow:: anyhow! ( " {}: {e}" , f.display ( ) ) ) ? ,
522
)
523
.map_err ( Into :: into)
524
} )
525
.collect:: < Result< Vec< _ > > > ( ) ? ;
7ba155f2 Andy McCall 1 month ago ↶
526
let scope = Scope:: parse( scope) .map_err ( | e | anyhow:: anyhow! ( " {e}" ) ) ? ;
527
let expires = expires.map ( expiry_from_duration) .transpose ( ) ? ;
614f55e1 Andy McCall 1 month ago ↶
528
let r = repo.delegate_add_many ( ids, scope, expires) ? ;
5cde7882 Andy McCall 1 month ago ↶
529
report_rotation ( & repo, & r, " Delegated" ) ? ;
614f55e1 Andy McCall 1 month ago ↶
530
println! (
531
" {} {} " ,
532
ui:: note( ) ,
533
ui:: dim( " an agent configured with your author.key needs the new key: oxy config --user author.key NEW in its home" )
534
) ;
7ba155f2 Andy McCall 1 month ago ↶
535
if ! scope.has ( Scope:: REFS ) {
536
println! (
537
" {} {} " ,
538
ui:: note( ) ,
539
ui:: dim( " it may not move references: a push or tag it attests is refused" )
540
) ;
541
}
28fc23d7 Andy McCall 1 month ago ↶
542
Ok ( 0 )
543
}
544
545
pub fn delegate_remove ( key : & str ) -> Result< i32 > {
546
let mut repo = open ( ) ? ;
547
let k = resolve_key ( & repo, key) ? ;
548
let r = repo.delegate_remove ( & k) ? ;
5cde7882 Andy McCall 1 month ago ↶
549
report_rotation ( & repo, & r, " Undelegated" ) ? ;
28fc23d7 Andy McCall 1 month ago ↶
550
Ok ( 0 )
551
}
552
553
pub fn delegate_list ( ) -> Result< i32 > {
554
let repo = open ( ) ? ;
555
let Some ( key) = repo.config.author_key else {
556
println! ( " No identity configured; run `oxy identity create`." ) ;
557
return Ok ( 0 ) ;
558
} ;
559
let id = repo.identity ( & key) ? ;
560
if id.delegates.is_empty ( ) {
561
println! ( " {} " , ui:: dim( " No delegates." ) ) ;
562
}
7ba155f2 Andy McCall 1 month ago ↶
563
let now = Hlc:: now( ) .wall_ms;
28fc23d7 Andy McCall 1 month ago ↶
564
for d in & id.delegates {
75167672 Andy McCall 1 month ago
565
let who = repo
7ba155f2 Andy McCall 1 month ago ↶
566
.identity ( & d.key)
75167672 Andy McCall 1 month ago
567
.map ( | i | format! ( " {} <{} >" , i.name, i.email) )
568
.unwrap_or_else ( | _| " (unknown identity)" .into ( ) ) ;
7ba155f2 Andy McCall 1 month ago ↶
569
let until = match d.expires {
570
Some ( e) if now > e => ui:: err( format! ( " expired {} " , ui:: ago( e) ) ) ,
571
Some ( e) => ui:: dim( format! ( " until {} " , when ( e) ) ) ,
572
None => String :: new( ) ,
573
} ;
75167672 Andy McCall 1 month ago
574
println! (
7ba155f2 Andy McCall 1 month ago ↶
575
" {} {} {} {} {} " ,
75167672 Andy McCall 1 month ago
576
ui:: ok( ui:: glyph( " 🔑" , " *" ) ) ,
7ba155f2 Andy McCall 1 month ago ↶
577
ui:: hash( d.key.short ( 12 ) ) ,
578
ui:: author( who) ,
579
ui:: dim( d.scope.to_string ( ) ) ,
580
until
75167672 Andy McCall 1 month ago
581
) ;
28fc23d7 Andy McCall 1 month ago ↶
582
}
583
Ok ( 0 )
584
}
585
586
pub fn policy_root_add ( identity_file : & Path, out : Option< PathBuf> ) -> Result< i32 > {
587
let repo = open ( ) ? ;
588
let id = Identity:: decode( & std:: fs:: read( identity_file) ? ) ? ;
589
let key = id.hash ( ) ;
590
let u = repo.policy_root_add ( id.clone ( ) ) ? ;
75167672 Andy McCall 1 month ago
591
println! (
592
" {} root {} <{} > ({} )" ,
593
ui:: done( " Added" ) ,
594
ui:: author( & id.name) ,
595
id.email,
596
key.short ( 12 )
597
) ;
28fc23d7 Andy McCall 1 month ago ↶
598
report_update ( & u, out) ? ;
599
Ok ( 0 )
600
}
601
602
pub fn policy_root_remove ( key : & str , out : Option< PathBuf> ) -> Result< i32 > {
603
let repo = open ( ) ? ;
604
let k = resolve_key ( & repo, key) ? ;
605
let u = repo.policy_root_remove ( & k) ? ;
606
println! ( " {} root {} " , ui:: done( " Removed" ) , ui:: hash( k.short ( 12 ) ) ) ;
607
report_update ( & u, out) ? ;
608
Ok ( 0 )
609
}
610
611
pub fn policy_threshold ( n : u32 , out : Option< PathBuf> ) -> Result< i32 > {
612
let repo = open ( ) ? ;
613
let u = repo.policy_set_threshold ( n) ? ;
614
println! ( " {} root threshold to {n} " , ui:: done( " Set" ) ) ;
615
report_update ( & u, out) ? ;
616
Ok ( 0 )
617
}
618
619
pub fn policy_sign ( proposal : & Path, out : Option< PathBuf> ) -> Result< i32 > {
620
let repo = open ( ) ? ;
621
let sp = SignedPolicy:: decode( & std:: fs:: read( proposal) ? ) ? ;
622
let u = repo.policy_sign ( sp) ? ;
623
println! ( " {} policy serial {} " , ui:: done( " Signed" ) , u.policy.serial) ;
624
report_update ( & u, Some ( out.unwrap_or_else ( | | proposal.to_path_buf ( ) ) ) ) ? ;
625
Ok ( 0 )
626
}
627
83e0677b Andy McCall 1 month ago ↶
628
pub fn policy_init ( ) -> Result< i32 > {
629
let repo = open ( ) ? ;
630
let sp = repo.policy_init ( ) ? ;
75167672 Andy McCall 1 month ago
631
println! (
632
" {} policy with root key {} " ,
633
ui:: done( " Created" ) ,
634
sp.policy.roots[ 0 ] .short ( 12 )
635
) ;
28fc23d7 Andy McCall 1 month ago ↶
636
report_update ( & sp, None ) ? ;
83e0677b Andy McCall 1 month ago ↶
637
Ok ( 0 )
638
}
639
c6aa5bd9 Andy McCall 1 month ago ↶
640
pub fn policy_show ( as_json : bool ) -> Result< i32 > {
83e0677b Andy McCall 1 month ago ↶
641
let repo = open ( ) ? ;
642
let Some ( ( r, sp) ) = repo.current_policy ( ) ? else {
c6aa5bd9 Andy McCall 1 month ago ↶
643
if as_json {
644
println! ( " null" ) ;
645
} else {
75167672 Andy McCall 1 month ago
646
println! (
647
" No policy: this repository is unverified. Run `oxy policy init` to create one."
648
) ;
c6aa5bd9 Andy McCall 1 month ago ↶
649
}
83e0677b Andy McCall 1 month ago ↶
650
return Ok ( 0 ) ;
651
} ;
652
let p = & sp.policy;
c6aa5bd9 Andy McCall 1 month ago ↶
653
if as_json {
654
let ident = | k : & oxy_core:: hash:: KeyId| {
75167672 Andy McCall 1 month ago
655
let ( name, email) = p
656
.identity ( k)
657
.map ( | i | ( i.name.clone ( ) , i.email.clone ( ) ) )
658
.unwrap_or_default ( ) ;
659
json:: obj( [
660
( " key" , json:: s( & k.to_hex ( ) ) ) ,
661
( " name" , json:: s( & name) ) ,
662
( " email" , json:: s( & email) ) ,
663
] )
c6aa5bd9 Andy McCall 1 month ago ↶
664
} ;
665
println! (
666
" {} " ,
667
json:: obj( [
668
( " hash" , json:: s( & r.to_hex ( ) ) ) ,
669
( " serial" , json:: n( p.serial) ) ,
670
( " expires_ms" , json:: n( p.expires) ) ,
75167672 Andy McCall 1 month ago
671
(
672
" expired" ,
673
json:: b( p.is_expired ( oxy_core:: objects:: Hlc:: now( ) .wall_ms) )
674
) ,
c6aa5bd9 Andy McCall 1 month ago ↶
675
( " root_threshold" , json:: n( p.root_threshold) ) ,
676
( " roots" , json:: arr( p.roots.iter ( ) .map ( ident) ) ) ,
677
(
678
" rules" ,
679
json:: arr( p.rules.iter ( ) .map ( | rule | {
680
json:: obj( [
681
( " pattern" , json:: s( & rule.pattern) ) ,
682
( " keys" , json:: arr( rule.keys.iter ( ) .map ( ident) ) ) ,
683
( " threshold" , json:: n( rule.threshold) ) ,
684
( " require" , json:: s( rule.require.name ( ) ) ) ,
685
( " allow_conflicts" , json:: b( rule.allow_conflicts) ) ,
686
] )
687
} ) ) ,
688
) ,
689
( " revoked" , json:: arr( p.revoked.iter ( ) .map ( ident) ) ) ,
690
] )
691
) ;
692
return Ok ( 0 ) ;
693
}
75167672 Andy McCall 1 month ago
694
let name = | k : & oxy_core:: hash:: KeyId| {
695
p.identity ( k)
696
.map ( | i | format! ( " {} <{} >" , i.name, i.email) )
697
.unwrap_or_else ( | | " (unknown identity)" .into ( ) )
698
} ;
94084087 Andy McCall 1 month ago ↶
699
let expired = p.is_expired ( oxy_core:: objects:: Hlc:: now( ) .wall_ms) ;
75167672 Andy McCall 1 month ago
700
println! (
701
" {} {} {} serial {} {} expires {} {} " ,
702
ui:: section( " policy" ) ,
703
ui:: hash( format! ( " #{} " , r.short ( 12 ) ) ) ,
704
ui:: sep( ) ,
705
p.serial,
706
ui:: sep( ) ,
707
when ( p.expires) ,
708
if expired {
709
ui:: err( format! ( " {} EXPIRED" , ui:: glyph( " ✖" , " x" ) ) )
710
} else {
711
String :: new( )
712
}
713
) ;
714
println! (
715
" {} " ,
716
ui:: section( format! (
717
" roots {} " ,
718
ui:: dim( format! ( " (threshold {} )" , p.root_threshold) )
719
) )
720
) ;
83e0677b Andy McCall 1 month ago ↶
721
for k in & p.roots {
75167672 Andy McCall 1 month ago
722
println! (
723
" {} {} {} " ,
724
ui:: ok( ui:: glyph( " 🔑" , " *" ) ) ,
725
ui:: hash( k.short ( 12 ) ) ,
726
ui:: author( name ( k) )
727
) ;
83e0677b Andy McCall 1 month ago ↶
728
}
94084087 Andy McCall 1 month ago ↶
729
println! ( " {} " , ui:: section( " rules" ) ) ;
83e0677b Andy McCall 1 month ago ↶
730
for rule in & p.rules {
75167672 Andy McCall 1 month ago
731
println! (
732
" {} {:12} {} " ,
733
ui:: ok( ui:: glyph( " ▸" , " >" ) ) ,
734
ui:: bookmark( & rule.pattern) ,
735
ui:: dim( format! (
736
" threshold {} , require {} , conflicts {} " ,
737
rule.threshold,
738
rule.require.name ( ) ,
739
if rule.allow_conflicts {
740
" allowed"
741
} else {
742
" refused"
743
}
744
) )
745
) ;
83e0677b Andy McCall 1 month ago ↶
746
for k in & rule.keys {
94084087 Andy McCall 1 month ago ↶
747
println! ( " {} {} " , ui:: hash( k.short ( 12 ) ) , ui:: author( name ( k) ) ) ;
83e0677b Andy McCall 1 month ago ↶
748
}
749
}
750
if ! p.revoked.is_empty ( ) {
94084087 Andy McCall 1 month ago ↶
751
println! ( " {} " , ui:: section( " revoked" ) ) ;
83e0677b Andy McCall 1 month ago ↶
752
for k in & p.revoked {
75167672 Andy McCall 1 month ago
753
println! (
754
" {} {} {} " ,
755
ui:: err( ui:: glyph( " ✖" , " x" ) ) ,
756
ui:: hash( k.short ( 12 ) ) ,
757
ui:: author( name ( k) )
758
) ;
83e0677b Andy McCall 1 month ago ↶
759
}
760
}
761
Ok ( 0 )
762
}
763
75167672 Andy McCall 1 month ago
764
pub fn policy_grant (
765
identity_file : & Path,
766
pattern : Option< & str > ,
767
out : Option< PathBuf> ,
768
) -> Result< i32 > {
83e0677b Andy McCall 1 month ago ↶
769
let repo = open ( ) ? ;
770
let id = Identity:: decode( & std:: fs:: read( identity_file) ? ) ? ;
771
let key = id.hash ( ) ;
28fc23d7 Andy McCall 1 month ago ↶
772
let u = repo.policy_grant ( id.clone ( ) , pattern) ? ;
75167672 Andy McCall 1 month ago
773
println! (
774
" {} {} <{} > ({} ) on {} " ,
775
ui:: done( " Granted" ) ,
776
ui:: author( & id.name) ,
777
id.email,
778
key.short ( 12 ) ,
779
ui:: bookmark( pattern.unwrap_or ( " *" ) )
780
) ;
28fc23d7 Andy McCall 1 month ago ↶
781
report_update ( & u, out) ? ;
83e0677b Andy McCall 1 month ago ↶
782
Ok ( 0 )
783
}
784
28fc23d7 Andy McCall 1 month ago ↶
785
pub fn policy_revoke ( key : & str , out : Option< PathBuf> ) -> Result< i32 > {
83e0677b Andy McCall 1 month ago ↶
786
let repo = open ( ) ? ;
28fc23d7 Andy McCall 1 month ago ↶
787
if repo.current_policy ( ) ? .is_none ( ) {
83e0677b Andy McCall 1 month ago ↶
788
anyhow:: bail! ( " this repository has no policy" ) ;
28fc23d7 Andy McCall 1 month ago ↶
789
}
790
let full = resolve_key ( & repo, key) ? ;
791
let u = repo.policy_revoke ( & full) ? ;
792
println! ( " {} {} " , ui:: done( " Revoked" ) , ui:: hash( full.short ( 12 ) ) ) ;
793
report_update ( & u, out) ? ;
83e0677b Andy McCall 1 month ago ↶
794
Ok ( 0 )
795
}
05e71ed6 Andy McCall 1 month ago ↶
796
797
798
pub fn seal ( specs : & [String], as_json : bool ) -> Result< i32 > {
799
let repo = Repo:: open( & std:: env:: current_dir( ) ? ) ? ;
800
let hashes: Vec< _ > = if specs.is_empty ( ) {
801
repo.sealable ( ) ?
802
} else {
b0e64e81 Andy McCall 1 month ago ↶
803
specs
804
.iter ( )
805
.map ( | s | repo.resolve_change ( s) )
806
.collect:: < std:: result:: Result< _ , _ > > ( ) ?
05e71ed6 Andy McCall 1 month ago ↶
807
} ;
808
if hashes.is_empty ( ) {
809
if as_json {
810
println! ( " {{ \" sealed\" :[]}} " ) ;
811
} else {
b0e64e81 Andy McCall 1 month ago ↶
812
println! (
813
" {} nothing to seal: every change you can authenticate already is" ,
814
ui:: done( " Sealed" )
815
) ;
05e71ed6 Andy McCall 1 month ago ↶
816
}
817
return Ok ( 0 ) ;
818
}
819
let mut refs = Vec :: new( ) ;
820
for chunk in hashes.chunks ( oxy_core:: attest:: MAX_SEAL_HASHES ) {
821
refs.push ( repo.seal ( chunk) ? ) ;
822
}
823
if as_json {
b0e64e81 Andy McCall 1 month ago ↶
824
let list: Vec< String> = hashes
825
.iter ( )
826
.map ( | h | format! ( " \" {} \" " , h.to_hex ( ) ) )
827
.collect ( ) ;
05e71ed6 Andy McCall 1 month ago ↶
828
let seals: Vec< String> = refs.iter ( ) .map ( | r | format! ( " \" {} \" " , r.to_hex ( ) ) ) .collect ( ) ;
b0e64e81 Andy McCall 1 month ago ↶
829
println! (
830
" {{ \" sealed\" :[{} ],\" seals\" :[{} ]}} " ,
831
list.join ( " ," ) ,
832
seals.join ( " ," )
833
) ;
05e71ed6 Andy McCall 1 month ago ↶
834
} else {
b0e64e81 Andy McCall 1 month ago ↶
835
println! (
836
" {} {} change{} in {} seal{} ; they travel with your next push" ,
837
ui:: done( " Sealed" ) ,
838
hashes.len ( ) ,
839
if hashes.len ( ) = = 1 { " " } else { " s" } ,
840
refs.len ( ) ,
841
if refs.len ( ) = = 1 { " " } else { " s" }
842
) ;
05e71ed6 Andy McCall 1 month ago ↶
843
for h in & hashes {
b0e64e81 Andy McCall 1 month ago ↶
844
let title = repo
845
.store ( )
846
.get_change ( h)
847
.map ( | c | c.message.lines ( ) .next ( ) .unwrap_or ( " " ) .to_string ( ) )
848
.unwrap_or_default ( ) ;
05e71ed6 Andy McCall 1 month ago ↶
849
println! ( " {} {} " , ui:: hash( h.short ( 12 ) ) , title) ;
850
}
851
}
852
Ok ( 0 )
853
}
7aa2dc93 Andy McCall 1 month ago ↶
854
855
856
857
pub fn sign_challenge ( challenge_hex : & str , as_json : bool ) -> Result< i32 > {
858
let repo = open ( ) ? ;
859
let Some ( ( key, sk) ) = repo.secret_key ( ) ? else {
860
anyhow:: bail! ( " no secret key: run `oxy identity create` first" ) ;
861
} ;
119bbf50 Andy McCall 1 month ago ↶
862
let challenge = hex_bytes ( challenge_hex.trim ( ) )
863
.ok_or_else ( | | anyhow:: anyhow! ( " the challenge must be hex" ) ) ? ;
864
let sig = sk
865
.sign_challenge ( & key, & challenge)
866
.map_err ( | e | anyhow:: anyhow! ( " {e}" ) ) ? ;
7aa2dc93 Andy McCall 1 month ago ↶
867
let encoded = hex_string ( & oxy_core:: identity:: Signature:: encode_list( & [ sig] ) ) ;
868
if as_json {
119bbf50 Andy McCall 1 month ago ↶
869
println! (
870
" {{ \" key\" :\" {} \" ,\" signature\" :\" {} \" }} " ,
871
key.to_hex ( ) ,
872
encoded
873
) ;
7aa2dc93 Andy McCall 1 month ago ↶
874
} else {
875
println! ( " {encoded} " ) ;
876
}
877
Ok ( 0 )
878
}
879
880
fn hex_bytes ( s : & str ) -> Option< Vec< u8 > > {
881
if ! s.len ( ) .is_multiple_of ( 2 ) {
882
return None ;
883
}
119bbf50 Andy McCall 1 month ago ↶
884
( 0 .. s.len ( ) )
885
.step_by ( 2 )
886
.map ( | i | u8 :: from_str_radix( & s[ i.. i + 2 ] , 16 ) .ok ( ) )
887
.collect ( )
7aa2dc93 Andy McCall 1 month ago ↶
888
}
889
890
fn hex_string ( b : & [u8 ]) -> String {
891
b.iter ( ) .map ( | x | format! ( " {x:02x} " ) ) .collect ( )
892
}