4b9d0677 Andy McCall 18 days ago ↶
1
2
3
4
5
6
7
8
9
10
11
028545df Andy McCall 17 days ago ↶
12
13
14
15
16
17
4b9d0677 Andy McCall 18 days ago ↶
18
19
use super :: * ;
20
use crate :: credentials:: hub_of;
21
use crate :: protocol:: http_get;
028545df Andy McCall 17 days ago ↶
22
use crate :: tlog:: { self , Checkpoint, Entry, Hash, HashReader, Integrated, Note, Tile, TlogError} ;
4b9d0677 Andy McCall 18 days ago ↶
23
use std:: cell:: RefCell;
24
25
26
type TileCache = RefCell< HashMap< ( u32 , u64 ) , ( Tile, Vec< u8 > ) > > ;
27
28
29
struct Tiles < 'a > {
30
hub : & 'a str ,
31
size : u64 ,
32
options : & 'a RemoteOptions,
33
cache : TileCache,
34
}
35
36
impl HashReader for Tiles < '_ > {
37
fn read_hashes ( & self , indexes : & [u64 ]) -> std:: result:: Result< Vec< Hash> , TlogError> {
38
let mut out = Vec :: with_capacity( indexes.len ( ) ) ;
39
for & i in indexes {
40
let ( t, .. ) = Tile:: for_index( i) ;
41
let key = ( t.level, t.n) ;
42
if ! self .cache.borrow ( ) .contains_key ( & key) {
43
let held =
44
( self .size > > ( tlog:: TILE_HEIGHT * t.level) ) - ( t.n < < tlog:: TILE_HEIGHT ) ;
45
let tile = Tile {
46
width: held.min ( tlog:: TILE_WIDTH ) as u32 ,
47
.. t
48
} ;
49
let data = http_get ( & format! ( " {} /log/{} " , self .hub, tile.path ( ) ) , self .options)
50
.map_err ( | e | TlogError:: Read( e.to_string ( ) ) ) ? ;
51
self .cache.borrow_mut ( ) .insert ( key, ( tile, data) ) ;
52
}
53
let cache = self .cache.borrow ( ) ;
54
let ( tile, data) = & cache[ & key] ;
55
out.push ( tile.hash_at ( data, i) ? ) ;
56
}
57
Ok ( out)
58
}
59
}
60
61
62
struct Seen {
63
identity : Identity,
64
checkpoint : Checkpoint,
65
}
66
67
fn file_name ( hub : & str ) -> String {
68
hub.chars ( )
69
.map ( | c | {
70
if c.is_ascii_alphanumeric ( ) | | c = = ' .' | | c = = ' -' {
71
c
72
} else {
73
' _'
74
}
75
} )
76
.collect ( )
77
}
78
79
impl Repo {
80
fn tlog_file ( & self , hub : & str ) -> PathBuf {
81
self .user_config
82
.parent ( )
83
.map ( Path:: to_path_buf)
84
.unwrap_or_else ( default_home)
85
.join ( " tlog" )
86
.join ( file_name ( hub) )
87
}
88
89
fn seen_log ( & self , hub : & str ) -> Option< Seen> {
90
let text = fs:: read_to_string( self .tlog_file ( hub) ) .ok ( ) ? ;
91
let ( key, note) = text.split_once ( ' \n ' ) ? ;
92
let identity = Identity:: decode( & crate :: dsse:: base64_decode( key) ? ) .ok ( ) ? ;
93
let note = Note:: parse( note) .ok ( ) ? ;
94
Some ( Seen {
95
checkpoint: Checkpoint:: parse( & note.text) .ok ( ) ? ,
96
identity,
97
} )
98
}
99
028545df Andy McCall 17 days ago ↶
100
pub ( crate ) fn remember_log ( & self , hub : & str , identity : & Identity, note : & Note) -> Result< ( ) > {
4b9d0677 Andy McCall 18 days ago ↶
101
let path = self .tlog_file ( hub) ;
102
if let Some ( dir) = path.parent ( ) {
103
fs:: create_dir_all( dir) ? ;
104
}
105
let text = format! (
106
" {} \n {} " ,
107
crate :: dsse:: base64( & identity.encode ( ) ) ,
108
note.encode ( )
109
) ;
110
crate :: durable:: write_atomic( & path, text.as_bytes ( ) ) ? ;
111
Ok ( ( ) )
112
}
113
e59512f2 Andy McCall 18 days ago ↶
114
115
116
117
118
pub ( crate ) fn accept_checkpoint (
4b9d0677 Andy McCall 18 days ago ↶
119
& self ,
e59512f2 Andy McCall 18 days ago ↶
120
hub : & str ,
121
text : & str ,
122
options : & RemoteOptions,
028545df Andy McCall 17 days ago ↶
123
) -> Result< std:: result:: Result< Accepted, String> > {
e59512f2 Andy McCall 18 days ago ↶
124
let unchecked = | why : String| Ok ( Err ( why) ) ;
4b9d0677 Andy McCall 18 days ago ↶
125
let note = match Note:: parse( text) {
126
Ok ( n) => n,
127
Err ( e) => return unchecked ( e.to_string ( ) ) ,
128
} ;
129
let cp = match Checkpoint:: parse( & note.text) {
130
Ok ( c) => c,
131
Err ( e) => return unchecked ( e.to_string ( ) ) ,
132
} ;
b333a199 Andy McCall 18 days ago ↶
133
let fetch_key = | | {
134
http_get ( & format! ( " {hub} /.well-known/oxyhub-key" ) , options)
4b9d0677 Andy McCall 18 days ago ↶
135
.and_then ( | b | Ok ( Identity:: decode( & b) ? ) )
b333a199 Andy McCall 18 days ago ↶
136
} ;
137
let mut notes = Vec :: new( ) ;
138
let mut seen = self .seen_log ( hub) ;
139
let mut identity = match & seen {
140
Some ( s) => s.identity.clone ( ) ,
141
None => match fetch_key ( ) {
4b9d0677 Andy McCall 18 days ago ↶
142
Ok ( i) => i,
143
Err ( e) => return unchecked ( format! ( " its key could not be fetched ({e} )" ) ) ,
144
} ,
145
} ;
b333a199 Andy McCall 18 days ago ↶
146
if let Err ( e) = note.verify ( & cp.origin, & identity) {
147
148
149
150
151
let Some ( old) = & seen else {
152
return unchecked ( e.to_string ( ) ) ;
153
} ;
154
match fetch_key ( ) {
155
Ok ( new) if note.verify ( & cp.origin, & new) .is_ok ( ) => {
156
notes.push ( format! (
157
" {hub} 's log is signed by another key than before ({} now, {} then): if its operator did not announce a new key, it may be showing others a different log" ,
158
new.hash ( ) .short ( 12 ) ,
159
old.identity.hash ( ) .short ( 12 )
160
) ) ;
161
identity = new;
162
seen = None ;
163
}
164
_ => return unchecked ( format! ( " its checkpoint does not verify ({e} )" ) ) ,
165
}
166
}
4b9d0677 Andy McCall 18 days ago ↶
167
let two = | why : String| {
168
fail ( format! (
169
" {hub} showed two histories: {why} . Do not trust what it serves; tell its operator. \
170
(What this user saw of its log is in {} .)" ,
e59512f2 Andy McCall 18 days ago ↶
171
self .tlog_file ( hub) .display ( )
4b9d0677 Andy McCall 18 days ago ↶
172
) )
173
} ;
174
if let Some ( s) = & seen {
175
let old = & s.checkpoint;
176
if old.origin ! = cp.origin {
b333a199 Andy McCall 18 days ago ↶
177
178
179
notes.push ( format! (
180
" {hub} 's log is now {} , not {} : checking it afresh" ,
181
cp.origin, old.origin
182
) ) ;
183
} else if cp.size < old.size {
4b9d0677 Andy McCall 18 days ago ↶
184
return two ( format! (
185
" its log has {} entries, fewer than the {} it showed before" ,
186
cp.size, old.size
187
) ) ;
b333a199 Andy McCall 18 days ago ↶
188
} else if cp.size = = old.size & & cp.root ! = old.root {
4b9d0677 Andy McCall 18 days ago ↶
189
return two ( format! ( " two different logs of {} entries" , cp.size) ) ;
b333a199 Andy McCall 18 days ago ↶
190
} else if cp.size > old.size & & old.size > 0 {
4b9d0677 Andy McCall 18 days ago ↶
191
let tiles = Tiles {
e59512f2 Andy McCall 18 days ago ↶
192
hub,
4b9d0677 Andy McCall 18 days ago ↶
193
size: cp.size,
e59512f2 Andy McCall 18 days ago ↶
194
options,
4b9d0677 Andy McCall 18 days ago ↶
195
cache: RefCell:: new( HashMap:: new( ) ) ,
196
} ;
197
match tlog:: prove_consistency( cp.size, old.size, & tiles) .and_then ( | p | {
198
tlog:: check_consistency( & p, cp.size, & cp.root, old.size, & old.root)
199
} ) {
200
Ok ( ( ) ) => { }
201
Err ( TlogError:: ProofFailed) | Err ( TlogError:: BadIndex( _ ) ) => {
202
return two ( format! (
203
" its log of {} entries does not extend the one of {} it showed before" ,
204
cp.size, old.size
205
) )
206
}
207
208
Err ( e) => return unchecked ( format! ( " its tiles could not be read ({e} )" ) ) ,
209
}
210
}
211
}
e59512f2 Andy McCall 18 days ago ↶
212
self .remember_log ( hub, & identity, & note) ? ;
028545df Andy McCall 17 days ago ↶
213
Ok ( Ok ( Accepted {
214
checkpoint: cp,
215
identity,
216
note,
217
notes,
218
} ) )
219
}
220
221
fn witnesses_file ( & self , hub : & str ) -> PathBuf {
222
let mut p = self .tlog_file ( hub) ;
223
p.set_file_name ( format! ( " {} .witnesses" , file_name ( hub) ) ) ;
224
p
225
}
226
227
228
229
pub ( crate ) fn pinned_witnesses ( & self , hub : & str ) -> Vec< ( String, Identity) > {
230
let Ok ( text) = fs:: read_to_string( self .witnesses_file ( hub) ) else {
231
return vec! [ ] ;
232
} ;
233
text.lines ( )
234
.filter_map ( | l | {
235
let ( name, key) = l.split_once ( ' ' ) ? ;
236
let id = Identity:: decode( & crate :: dsse:: base64_decode( key) ? ) .ok ( ) ? ;
237
Some ( ( name.to_string ( ) , id) )
238
} )
239
.collect ( )
240
}
241
242
243
244
245
246
pub ( crate ) fn pin_witnesses (
247
& self ,
248
hub : & str ,
249
listed : & [( String , Vec <u8 >) ],
250
notes : & mut Vec< String> ,
251
) -> Result< Vec< ( String, Identity) > > {
252
let path = self .witnesses_file ( hub) ;
253
let first = ! path.exists ( ) ;
254
let mut pinned = self .pinned_witnesses ( hub) ;
255
let mut changed = false ;
256
for ( name, bytes) in listed {
257
if name.is_empty ( ) | | name.contains ( char :: is_whitespace) {
258
notes.push ( format! (
259
" {hub} lists a witness named {name:?} , which cannot name one"
260
) ) ;
261
continue ;
262
}
263
let Ok ( id) = Identity:: decode( bytes) else {
264
notes.push ( format! (
265
" {hub} lists witness {name} with a key that does not decode"
266
) ) ;
267
continue ;
268
} ;
269
match pinned.iter_mut ( ) .find ( | ( n , _) | n = = name ) {
270
Some ( ( _ , old) ) if old.hash ( ) = = id.hash ( ) => { }
271
Some ( ( _ , old) ) => {
272
notes.push ( format! (
273
" {hub} 's witness {name} has another key than before ({} now, {} then): if its operator did not announce one, {hub} may be naming a witness of its own" ,
274
id.hash ( ) .short ( 12 ) ,
275
old.hash ( ) .short ( 12 )
276
) ) ;
277
* old = id;
278
changed = true ;
279
}
280
None => {
281
if ! first {
282
notes.push ( format! (
283
" {hub} is now cosigned by witness {name} (key {} ), trusted from now on" ,
284
id.hash ( ) .short ( 12 )
285
) ) ;
286
}
287
pinned.push ( ( name.clone ( ) , id) ) ;
288
changed = true ;
289
}
290
}
291
}
292
if changed {
293
if let Some ( dir) = path.parent ( ) {
294
fs:: create_dir_all( dir) ? ;
295
}
296
let text: String = pinned
297
.iter ( )
298
.map ( | ( n , id ) | format! ( " {n} {} \n " , crate :: dsse:: base64( & id.encode ( ) ) ) )
299
.collect ( ) ;
300
crate :: durable:: write_atomic( & path, text.as_bytes ( ) ) ? ;
301
}
302
Ok ( pinned)
e59512f2 Andy McCall 18 days ago ↶
303
}
304
028545df Andy McCall 17 days ago ↶
305
306
307
308
e59512f2 Andy McCall 18 days ago ↶
309
pub ( crate ) fn check_log (
310
& self ,
311
url : & str ,
312
ca : Option< & Path> ,
313
resp : & RefsResponse,
028545df Andy McCall 17 days ago ↶
314
) -> Result< LogCheck> {
315
let mut out = LogCheck:: default( ) ;
e59512f2 Andy McCall 18 days ago ↶
316
let ( Some ( text) , Some ( hub) ) = ( & resp.log, hub_of ( url) ) else {
028545df Andy McCall 17 days ago ↶
317
return Ok ( out) ;
e59512f2 Andy McCall 18 days ago ↶
318
} ;
028545df Andy McCall 17 days ago ↶
319
out.kept = true ;
e59512f2 Andy McCall 18 days ago ↶
320
let options = RemoteOptions {
321
ca: ca.map ( Path:: to_path_buf) ,
322
.. RemoteOptions:: default( )
323
} ;
028545df Andy McCall 17 days ago ↶
324
let accepted = match self .accept_checkpoint ( & hub, text, & options) ? {
b333a199 Andy McCall 18 days ago ↶
325
Ok ( c) => c,
e59512f2 Andy McCall 18 days ago ↶
326
Err ( why) => {
028545df Andy McCall 17 days ago ↶
327
out.notes
328
.push ( format! ( " {hub} 's transparency log was not checked: {why} " ) ) ;
329
out.unchecked = Some ( why) ;
330
return Ok ( out) ;
e59512f2 Andy McCall 18 days ago ↶
331
}
332
} ;
028545df Andy McCall 17 days ago ↶
333
let cp = & accepted.checkpoint;
334
out.notes.extend ( accepted.notes.iter ( ) .cloned ( ) ) ;
335
let witnesses = self .pin_witnesses ( & hub, & resp.witnesses, & mut out.notes) ? ;
4b9d0677 Andy McCall 18 days ago ↶
336
let mut missing = 0 ;
337
for r in & resp.refs {
338
let Some ( a) = & r.attestation else { continue } ;
339
let Some ( l) = & r.logged else {
340
missing + = 1 ;
028545df Andy McCall 17 days ago ↶
341
out.times
342
.insert ( r.name.clone ( ) , Err ( " it is not in the log yet" .into ( ) ) ) ;
4b9d0677 Andy McCall 18 days ago ↶
343
continue ;
344
} ;
345
let fits = Entry:: decode( & l.entry) .is_ok_and ( | e | {
346
e.kind = = " ref" & & e.object = = a.attest.hash ( ) & & e.repo = = a.attest.repo
347
} ) ;
348
let included = fits
349
& & tlog:: check_inclusion(
350
& l.proof,
351
cp.size,
352
& cp.root,
353
l.index,
354
& tlog:: leaf_hash( & l.entry) ,
355
)
356
.is_ok ( ) ;
357
if ! included {
028545df Andy McCall 17 days ago ↶
358
out.notes.push ( format! (
4b9d0677 Andy McCall 18 days ago ↶
359
" {hub} 's log entry {} for {} does not prove its attestation is in the log" ,
360
l.index, r.name
361
) ) ;
362
}
028545df Andy McCall 17 days ago ↶
363
let time = if ! fits {
364
Err ( " its log entry is not its attestation's" .to_string ( ) )
365
} else if witnesses.is_empty ( ) {
366
Err ( format! ( " {hub} names no witness" ) )
367
} else {
368
let cosigned: Vec< ( & str , & [Hash]) > = r
369
.integration
370
.iter ( )
371
.map ( | i | ( i.note.as_str ( ) , i.proof.as_slice ( ) ) )
372
.collect ( ) ;
373
tlog:: integrated_time(
374
& l.entry,
375
l.index,
376
& cosigned,
377
& cp.origin,
378
& accepted.identity,
379
& witnesses,
380
)
381
} ;
382
out.times.insert ( r.name.clone ( ) , time) ;
4b9d0677 Andy McCall 18 days ago ↶
383
}
384
if missing > 0 {
028545df Andy McCall 17 days ago ↶
385
out.notes.push ( format! (
4b9d0677 Andy McCall 18 days ago ↶
386
" {} of {hub} 's attestations {} not in its transparency log yet (logged within seconds of a push; checked on a later fetch)" ,
387
missing,
388
if missing = = 1 { " is" } else { " are" }
389
) ) ;
028545df Andy McCall 17 days ago ↶
390
}
391
Ok ( out)
392
}
393
}
394
395
396
397
pub ( crate ) struct Accepted {
398
pub checkpoint : Checkpoint,
399
pub identity : Identity,
400
pub note : Note,
401
pub notes : Vec< String> ,
402
}
403
404
405
# [ derive ( Debug, Default ) ]
406
pub ( crate ) struct LogCheck {
407
408
pub kept : bool ,
409
410
pub unchecked : Option< String> ,
411
pub notes : Vec< String> ,
412
413
pub times : HashMap< String, std:: result:: Result< Integrated, String> > ,
414
}
415
416
impl LogCheck {
417
418
pub fn time_of ( & self , name : & str ) -> std:: result:: Result< & Integrated, String> {
419
if let Some ( why) = & self .unchecked {
420
return Err ( format! ( " its host's log was not checked ({why} )" ) ) ;
421
}
422
if ! self .kept {
423
return Err ( " its host keeps no transparency log" .into ( ) ) ;
4b9d0677 Andy McCall 18 days ago ↶
424
}
028545df Andy McCall 17 days ago ↶
425
match self .times.get ( name) {
426
Some ( Ok ( i) ) => Ok ( i) ,
427
Some ( Err ( why) ) => Err ( why.clone ( ) ) ,
428
None => Err ( " it has no log entry" .into ( ) ) ,
429
}
430
}
431
}
432
433
434
435
# [ derive ( Clone, Copy, Debug, PartialEq, Eq ) ]
436
pub ( crate ) enum Timed {
437
438
Integrated,
439
440
441
Fallback,
442
443
444
Disagrees,
445
446
Refused,
447
}
448
449
450
451
pub ( crate ) fn judge (
452
integrated : Option< u64 > ,
453
at : impl Fn( u64 ) -> bool ,
454
before : impl FnOnce( ) -> bool ,
455
) -> Timed {
456
match integrated {
457
Some ( t) if at ( t) => Timed:: Integrated,
458
Some ( _ ) => {
459
if before ( ) {
460
Timed:: Disagrees
461
} else {
462
Timed:: Refused
463
}
464
}
465
None => {
466
if before ( ) {
467
Timed:: Fallback
468
} else {
469
Timed:: Refused
470
}
471
}
472
}
473
}
474
475
476
477
pub ( crate ) fn gap_note ( claimed : u64 , integrated : & Integrated) -> Option< String> {
478
let t = integrated.ms ( ) ;
479
let minutes = | d : u64 | d.div_ceil ( 60_000 ) ;
480
if claimed.saturating_add ( tlog:: TIME_TOLERANCE_MS ) < t {
481
Some ( format! (
482
" claims to be from {} minutes before witness {} first saw it: backdated, held back before it was logged, or signed on a slow clock" ,
483
minutes ( t - claimed) ,
484
integrated.witness
485
) )
486
} else if claimed > t.saturating_add ( tlog:: TIME_TOLERANCE_MS ) {
487
Some ( format! (
488
" claims to be from {} minutes after witness {} saw it: signed on a fast clock" ,
489
minutes ( claimed - t) ,
490
integrated.witness
491
) )
492
} else {
493
None
e59512f2 Andy McCall 18 days ago ↶
494
}
495
}
496
497
498
# [ derive ( Clone, Debug ) ]
499
pub struct LogAudit {
500
pub hub : String,
501
pub checkpoint : Checkpoint,
502
503
pub entries : Vec< Entry> ,
b333a199 Andy McCall 18 days ago ↶
504
505
pub notes : Vec< String> ,
028545df Andy McCall 17 days ago ↶
506
507
508
pub witnessed : Vec< ( String, u64 ) > ,
e59512f2 Andy McCall 18 days ago ↶
509
}
510
511
impl Repo {
512
513
514
515
pub fn audit_log ( & self , url : & str , ca : Option< & Path> ) -> Result< LogAudit> {
516
let hub =
517
hub_of ( url) .ok_or_else ( | | RepoError:: Msg( format! ( " {url} is not a hub's address" ) ) ) ? ;
518
let options = RemoteOptions {
519
ca: ca.map ( Path:: to_path_buf) ,
520
.. RemoteOptions:: default( )
521
} ;
522
let tl = | e : TlogError| RepoError:: Msg( e.to_string ( ) ) ;
523
let text = String :: from_utf8( http_get ( & format! ( " {hub} /log/checkpoint" ) , & options) ? )
524
.map_err ( | _| RepoError:: Msg( format! ( " {hub} 's checkpoint is not text" ) ) ) ? ;
028545df Andy McCall 17 days ago ↶
525
let accepted = match self .accept_checkpoint ( & hub, & text, & options) ? {
e59512f2 Andy McCall 18 days ago ↶
526
Ok ( c) => c,
527
Err ( why) => return fail ( format! ( " {hub} 's log could not be checked: {why} " ) ) ,
528
} ;
028545df Andy McCall 17 days ago ↶
529
let mut notes = accepted.notes;
530
531
532
533
let listed: Vec< ( String, Vec< u8 > ) > = http_get ( & format! ( " {hub} /log/witnesses" ) , & options)
534
.ok ( )
535
.and_then ( | b | String :: from_utf8( b) .ok ( ) )
536
.map ( | t | {
537
t.lines ( )
538
.filter_map ( | l | {
539
let ( n, k) = l.split_once ( ' ' ) ? ;
540
Some ( ( n.to_string ( ) , crate :: dsse:: base64_decode( k.trim ( ) ) ? ) )
541
} )
542
.collect ( )
543
} )
544
.unwrap_or_default ( ) ;
545
let witnesses = self .pin_witnesses ( & hub, & listed, & mut notes) ? ;
546
let mut witnessed = accepted.note.cosignatures ( & witnesses) ;
547
witnessed.sort_by_key ( | ( _, t ) | * t) ;
548
let cp = accepted.checkpoint;
e59512f2 Andy McCall 18 days ago ↶
549
let mut entries = Vec :: with_capacity( cp.size as usize ) ;
550
let mut stored: Vec< Hash> = Vec :: new( ) ;
551
let mut n = 0 u64 ;
552
while n < cp.size {
553
let t = Tile {
554
level: 0 ,
555
n: n > > tlog:: TILE_HEIGHT ,
556
width: ( cp.size - n) .min ( tlog:: TILE_WIDTH ) as u32 ,
557
} ;
558
let bytes = http_get ( & format! ( " {hub} /log/{} " , t.entries_path ( ) ) , & options) ? ;
559
let bundle = tlog:: decode_bundle( & bytes) .map_err ( tl) ? ;
560
if bundle.len ( ) ! = t.width as usize {
561
return fail ( format! (
562
" {hub} 's entry bundle {} holds {} entries, not {} " ,
563
t.entries_path ( ) ,
564
bundle.len ( ) ,
565
t.width
566
) ) ;
567
}
568
for b in bundle {
569
let add =
570
tlog:: stored_hashes_for_leaf( n, tlog:: leaf_hash( & b) , & stored) .map_err ( tl) ? ;
571
stored.extend ( add) ;
572
entries.push ( Entry:: decode( & b) .map_err ( tl) ? ) ;
573
n + = 1 ;
574
}
575
}
576
if tlog:: tree_hash( cp.size, & stored) .map_err ( tl) ? ! = cp.root {
577
return fail ( format! (
578
" {hub} serves entries that do not make the root it signed for {} entries: its log is not the one it attests" ,
579
cp.size
580
) ) ;
581
}
582
Ok ( LogAudit {
583
hub,
584
checkpoint: cp,
585
entries,
b333a199 Andy McCall 18 days ago ↶
586
notes,
028545df Andy McCall 17 days ago ↶
587
witnessed,
e59512f2 Andy McCall 18 days ago ↶
588
} )
4b9d0677 Andy McCall 18 days ago ↶
589
}
590
}