4b9d0677 Andy McCall 18 days ago ↶
1
2
3
4
5
6
7
8
9
10
11
12
13
use super :: * ;
14
use crate :: credentials:: hub_of;
15
use crate :: protocol:: http_get;
16
use crate :: tlog:: { self , Checkpoint, Entry, Hash, HashReader, Note, Tile, TlogError} ;
17
use std:: cell:: RefCell;
18
19
20
type TileCache = RefCell< HashMap< ( u32 , u64 ) , ( Tile, Vec< u8 > ) > > ;
21
22
23
struct Tiles < 'a > {
24
hub : & 'a str ,
25
size : u64 ,
26
options : & 'a RemoteOptions,
27
cache : TileCache,
28
}
29
30
impl HashReader for Tiles < '_ > {
31
fn read_hashes ( & self , indexes : & [u64 ]) -> std:: result:: Result< Vec< Hash> , TlogError> {
32
let mut out = Vec :: with_capacity( indexes.len ( ) ) ;
33
for & i in indexes {
34
let ( t, .. ) = Tile:: for_index( i) ;
35
let key = ( t.level, t.n) ;
36
if ! self .cache.borrow ( ) .contains_key ( & key) {
37
let held =
38
( self .size > > ( tlog:: TILE_HEIGHT * t.level) ) - ( t.n < < tlog:: TILE_HEIGHT ) ;
39
let tile = Tile {
40
width: held.min ( tlog:: TILE_WIDTH ) as u32 ,
41
.. t
42
} ;
43
let data = http_get ( & format! ( " {} /log/{} " , self .hub, tile.path ( ) ) , self .options)
44
.map_err ( | e | TlogError:: Read( e.to_string ( ) ) ) ? ;
45
self .cache.borrow_mut ( ) .insert ( key, ( tile, data) ) ;
46
}
47
let cache = self .cache.borrow ( ) ;
48
let ( tile, data) = & cache[ & key] ;
49
out.push ( tile.hash_at ( data, i) ? ) ;
50
}
51
Ok ( out)
52
}
53
}
54
55
56
struct Seen {
57
identity : Identity,
58
checkpoint : Checkpoint,
59
}
60
61
fn file_name ( hub : & str ) -> String {
62
hub.chars ( )
63
.map ( | c | {
64
if c.is_ascii_alphanumeric ( ) | | c = = ' .' | | c = = ' -' {
65
c
66
} else {
67
' _'
68
}
69
} )
70
.collect ( )
71
}
72
73
impl Repo {
74
fn tlog_file ( & self , hub : & str ) -> PathBuf {
75
self .user_config
76
.parent ( )
77
.map ( Path:: to_path_buf)
78
.unwrap_or_else ( default_home)
79
.join ( " tlog" )
80
.join ( file_name ( hub) )
81
}
82
83
fn seen_log ( & self , hub : & str ) -> Option< Seen> {
84
let text = fs:: read_to_string( self .tlog_file ( hub) ) .ok ( ) ? ;
85
let ( key, note) = text.split_once ( ' \n ' ) ? ;
86
let identity = Identity:: decode( & crate :: dsse:: base64_decode( key) ? ) .ok ( ) ? ;
87
let note = Note:: parse( note) .ok ( ) ? ;
88
Some ( Seen {
89
checkpoint: Checkpoint:: parse( & note.text) .ok ( ) ? ,
90
identity,
91
} )
92
}
93
94
fn remember_log ( & self , hub : & str , identity : & Identity, note : & Note) -> Result< ( ) > {
95
let path = self .tlog_file ( hub) ;
96
if let Some ( dir) = path.parent ( ) {
97
fs:: create_dir_all( dir) ? ;
98
}
99
let text = format! (
100
" {} \n {} " ,
101
crate :: dsse:: base64( & identity.encode ( ) ) ,
102
note.encode ( )
103
) ;
104
crate :: durable:: write_atomic( & path, text.as_bytes ( ) ) ? ;
105
Ok ( ( ) )
106
}
107
e59512f2 Andy McCall 18 days ago ↶
108
109
110
111
112
pub ( crate ) fn accept_checkpoint (
4b9d0677 Andy McCall 18 days ago ↶
113
& self ,
e59512f2 Andy McCall 18 days ago ↶
114
hub : & str ,
115
text : & str ,
116
options : & RemoteOptions,
b333a199 Andy McCall 18 days ago ↶
117
) -> Result< std:: result:: Result< ( Checkpoint, Vec< String> ) , String> > {
e59512f2 Andy McCall 18 days ago ↶
118
let unchecked = | why : String| Ok ( Err ( why) ) ;
4b9d0677 Andy McCall 18 days ago ↶
119
let note = match Note:: parse( text) {
120
Ok ( n) => n,
121
Err ( e) => return unchecked ( e.to_string ( ) ) ,
122
} ;
123
let cp = match Checkpoint:: parse( & note.text) {
124
Ok ( c) => c,
125
Err ( e) => return unchecked ( e.to_string ( ) ) ,
126
} ;
b333a199 Andy McCall 18 days ago ↶
127
let fetch_key = | | {
128
http_get ( & format! ( " {hub} /.well-known/oxyhub-key" ) , options)
4b9d0677 Andy McCall 18 days ago ↶
129
.and_then ( | b | Ok ( Identity:: decode( & b) ? ) )
b333a199 Andy McCall 18 days ago ↶
130
} ;
131
let mut notes = Vec :: new( ) ;
132
let mut seen = self .seen_log ( hub) ;
133
let mut identity = match & seen {
134
Some ( s) => s.identity.clone ( ) ,
135
None => match fetch_key ( ) {
4b9d0677 Andy McCall 18 days ago ↶
136
Ok ( i) => i,
137
Err ( e) => return unchecked ( format! ( " its key could not be fetched ({e} )" ) ) ,
138
} ,
139
} ;
b333a199 Andy McCall 18 days ago ↶
140
if let Err ( e) = note.verify ( & cp.origin, & identity) {
141
142
143
144
145
let Some ( old) = & seen else {
146
return unchecked ( e.to_string ( ) ) ;
147
} ;
148
match fetch_key ( ) {
149
Ok ( new) if note.verify ( & cp.origin, & new) .is_ok ( ) => {
150
notes.push ( format! (
151
" {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" ,
152
new.hash ( ) .short ( 12 ) ,
153
old.identity.hash ( ) .short ( 12 )
154
) ) ;
155
identity = new;
156
seen = None ;
157
}
158
_ => return unchecked ( format! ( " its checkpoint does not verify ({e} )" ) ) ,
159
}
160
}
4b9d0677 Andy McCall 18 days ago ↶
161
let two = | why : String| {
162
fail ( format! (
163
" {hub} showed two histories: {why} . Do not trust what it serves; tell its operator. \
164
(What this user saw of its log is in {} .)" ,
e59512f2 Andy McCall 18 days ago ↶
165
self .tlog_file ( hub) .display ( )
4b9d0677 Andy McCall 18 days ago ↶
166
) )
167
} ;
168
if let Some ( s) = & seen {
169
let old = & s.checkpoint;
170
if old.origin ! = cp.origin {
b333a199 Andy McCall 18 days ago ↶
171
172
173
notes.push ( format! (
174
" {hub} 's log is now {} , not {} : checking it afresh" ,
175
cp.origin, old.origin
176
) ) ;
177
} else if cp.size < old.size {
4b9d0677 Andy McCall 18 days ago ↶
178
return two ( format! (
179
" its log has {} entries, fewer than the {} it showed before" ,
180
cp.size, old.size
181
) ) ;
b333a199 Andy McCall 18 days ago ↶
182
} else if cp.size = = old.size & & cp.root ! = old.root {
4b9d0677 Andy McCall 18 days ago ↶
183
return two ( format! ( " two different logs of {} entries" , cp.size) ) ;
b333a199 Andy McCall 18 days ago ↶
184
} else if cp.size > old.size & & old.size > 0 {
4b9d0677 Andy McCall 18 days ago ↶
185
let tiles = Tiles {
e59512f2 Andy McCall 18 days ago ↶
186
hub,
4b9d0677 Andy McCall 18 days ago ↶
187
size: cp.size,
e59512f2 Andy McCall 18 days ago ↶
188
options,
4b9d0677 Andy McCall 18 days ago ↶
189
cache: RefCell:: new( HashMap:: new( ) ) ,
190
} ;
191
match tlog:: prove_consistency( cp.size, old.size, & tiles) .and_then ( | p | {
192
tlog:: check_consistency( & p, cp.size, & cp.root, old.size, & old.root)
193
} ) {
194
Ok ( ( ) ) => { }
195
Err ( TlogError:: ProofFailed) | Err ( TlogError:: BadIndex( _ ) ) => {
196
return two ( format! (
197
" its log of {} entries does not extend the one of {} it showed before" ,
198
cp.size, old.size
199
) )
200
}
201
202
Err ( e) => return unchecked ( format! ( " its tiles could not be read ({e} )" ) ) ,
203
}
204
}
205
}
e59512f2 Andy McCall 18 days ago ↶
206
self .remember_log ( hub, & identity, & note) ? ;
b333a199 Andy McCall 18 days ago ↶
207
Ok ( Ok ( ( cp, notes) ) )
e59512f2 Andy McCall 18 days ago ↶
208
}
209
210
b333a199 Andy McCall 18 days ago ↶
211
212
e59512f2 Andy McCall 18 days ago ↶
213
pub ( crate ) fn check_log (
214
& self ,
215
url : & str ,
216
ca : Option< & Path> ,
217
resp : & RefsResponse,
218
) -> Result< Vec< String> > {
219
let ( Some ( text) , Some ( hub) ) = ( & resp.log, hub_of ( url) ) else {
220
return Ok ( vec! [ ] ) ;
221
} ;
222
let options = RemoteOptions {
223
ca: ca.map ( Path:: to_path_buf) ,
224
.. RemoteOptions:: default( )
225
} ;
b333a199 Andy McCall 18 days ago ↶
226
let ( cp, mut notes) = match self .accept_checkpoint ( & hub, text, & options) ? {
227
Ok ( c) => c,
e59512f2 Andy McCall 18 days ago ↶
228
Err ( why) => {
229
return Ok ( vec! [ format! (
230
" {hub} 's transparency log was not checked: {why} "
231
) ] )
232
}
233
} ;
4b9d0677 Andy McCall 18 days ago ↶
234
let mut missing = 0 ;
235
for r in & resp.refs {
236
let Some ( a) = & r.attestation else { continue } ;
237
let Some ( l) = & r.logged else {
238
missing + = 1 ;
239
continue ;
240
} ;
241
let fits = Entry:: decode( & l.entry) .is_ok_and ( | e | {
242
e.kind = = " ref" & & e.object = = a.attest.hash ( ) & & e.repo = = a.attest.repo
243
} ) ;
244
let included = fits
245
& & tlog:: check_inclusion(
246
& l.proof,
247
cp.size,
248
& cp.root,
249
l.index,
250
& tlog:: leaf_hash( & l.entry) ,
251
)
252
.is_ok ( ) ;
253
if ! included {
254
notes.push ( format! (
255
" {hub} 's log entry {} for {} does not prove its attestation is in the log" ,
256
l.index, r.name
257
) ) ;
258
}
259
}
260
if missing > 0 {
261
notes.push ( format! (
262
" {} of {hub} 's attestations {} not in its transparency log yet (logged within seconds of a push; checked on a later fetch)" ,
263
missing,
264
if missing = = 1 { " is" } else { " are" }
265
) ) ;
266
}
267
Ok ( notes)
e59512f2 Andy McCall 18 days ago ↶
268
}
269
}
270
271
272
# [ derive ( Clone, Debug ) ]
273
pub struct LogAudit {
274
pub hub : String,
275
pub checkpoint : Checkpoint,
276
277
pub entries : Vec< Entry> ,
b333a199 Andy McCall 18 days ago ↶
278
279
pub notes : Vec< String> ,
e59512f2 Andy McCall 18 days ago ↶
280
}
281
282
impl Repo {
283
284
285
286
pub fn audit_log ( & self , url : & str , ca : Option< & Path> ) -> Result< LogAudit> {
287
let hub =
288
hub_of ( url) .ok_or_else ( | | RepoError:: Msg( format! ( " {url} is not a hub's address" ) ) ) ? ;
289
let options = RemoteOptions {
290
ca: ca.map ( Path:: to_path_buf) ,
291
.. RemoteOptions:: default( )
292
} ;
293
let tl = | e : TlogError| RepoError:: Msg( e.to_string ( ) ) ;
294
let text = String :: from_utf8( http_get ( & format! ( " {hub} /log/checkpoint" ) , & options) ? )
295
.map_err ( | _| RepoError:: Msg( format! ( " {hub} 's checkpoint is not text" ) ) ) ? ;
b333a199 Andy McCall 18 days ago ↶
296
let ( cp, notes) = match self .accept_checkpoint ( & hub, & text, & options) ? {
e59512f2 Andy McCall 18 days ago ↶
297
Ok ( c) => c,
298
Err ( why) => return fail ( format! ( " {hub} 's log could not be checked: {why} " ) ) ,
299
} ;
300
let mut entries = Vec :: with_capacity( cp.size as usize ) ;
301
let mut stored: Vec< Hash> = Vec :: new( ) ;
302
let mut n = 0 u64 ;
303
while n < cp.size {
304
let t = Tile {
305
level: 0 ,
306
n: n > > tlog:: TILE_HEIGHT ,
307
width: ( cp.size - n) .min ( tlog:: TILE_WIDTH ) as u32 ,
308
} ;
309
let bytes = http_get ( & format! ( " {hub} /log/{} " , t.entries_path ( ) ) , & options) ? ;
310
let bundle = tlog:: decode_bundle( & bytes) .map_err ( tl) ? ;
311
if bundle.len ( ) ! = t.width as usize {
312
return fail ( format! (
313
" {hub} 's entry bundle {} holds {} entries, not {} " ,
314
t.entries_path ( ) ,
315
bundle.len ( ) ,
316
t.width
317
) ) ;
318
}
319
for b in bundle {
320
let add =
321
tlog:: stored_hashes_for_leaf( n, tlog:: leaf_hash( & b) , & stored) .map_err ( tl) ? ;
322
stored.extend ( add) ;
323
entries.push ( Entry:: decode( & b) .map_err ( tl) ? ) ;
324
n + = 1 ;
325
}
326
}
327
if tlog:: tree_hash( cp.size, & stored) .map_err ( tl) ? ! = cp.root {
328
return fail ( format! (
329
" {hub} serves entries that do not make the root it signed for {} entries: its log is not the one it attests" ,
330
cp.size
331
) ) ;
332
}
333
Ok ( LogAudit {
334
hub,
335
checkpoint: cp,
336
entries,
b333a199 Andy McCall 18 days ago ↶
337
notes,
e59512f2 Andy McCall 18 days ago ↶
338
} )
4b9d0677 Andy McCall 18 days ago ↶
339
}
340
}