|
|
… |
113 unchanged lines |
| 114 |
114 |
|
hub: &str, |
| 115 |
115 |
|
text: &str, |
| 116 |
116 |
|
options: &RemoteOptions, |
| 117 |
|
- |
) -> Result<std::result::Result<Checkpoint, String>> { |
|
117 |
+ |
) -> Result<std::result::Result<(Checkpoint, Vec<String>), String>> { |
| 118 |
118 |
|
let unchecked = |why: String| Ok(Err(why)); |
| 119 |
119 |
|
let note = match Note::parse(text) { |
| 120 |
120 |
|
Ok(n) => n, |
|
|
… |
3 unchanged lines |
| 124 |
124 |
|
Ok(c) => c, |
| 125 |
125 |
|
Err(e) => return unchecked(e.to_string()), |
| 126 |
126 |
|
}; |
| 127 |
|
- |
let seen = self.seen_log(hub); |
| 128 |
|
- |
let identity = match &seen { |
| 129 |
|
- |
Some(s) => s.identity.clone(), |
| 130 |
|
- |
None => match http_get(&format!("{hub}/.well-known/oxyhub-key"), options) |
|
127 |
+ |
let fetch_key = || { |
|
128 |
+ |
http_get(&format!("{hub}/.well-known/oxyhub-key"), options) |
| 131 |
129 |
|
.and_then(|b| Ok(Identity::decode(&b)?)) |
| 132 |
|
- |
{ |
|
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() { |
| 133 |
136 |
|
Ok(i) => i, |
| 134 |
137 |
|
Err(e) => return unchecked(format!("its key could not be fetched ({e})")), |
| 135 |
138 |
|
}, |
| 136 |
139 |
|
}; |
|
140 |
+ |
if let Err(e) = note.verify(&cp.origin, &identity) { |
|
141 |
+ |
// Signed by another key than the one pinned: a hub given a new |
|
142 |
+ |
// key, or one starting another log. Not proof of a fork, so a |
|
143 |
+ |
// loud note while logs are new (decision 3), and the new key is |
|
144 |
+ |
// pinned; a fork under one key is what fails. |
|
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 |
+ |
} |
| 137 |
161 |
|
let two = |why: String| { |
| 138 |
162 |
|
fail(format!( |
| 139 |
163 |
|
"{hub} showed two histories: {why}. Do not trust what it serves; tell its operator. \ |
|
|
… |
1 unchanged lines |
| 141 |
165 |
|
self.tlog_file(hub).display() |
| 142 |
166 |
|
)) |
| 143 |
167 |
|
}; |
| 144 |
|
- |
if let Err(e) = note.verify(&cp.origin, &identity) { |
| 145 |
|
- |
return match &seen { |
| 146 |
|
- |
Some(_) => two(format!( |
| 147 |
|
- |
"its checkpoint is not signed by the log key it showed before ({e})" |
| 148 |
|
- |
)), |
| 149 |
|
- |
None => unchecked(e.to_string()), |
| 150 |
|
- |
}; |
| 151 |
|
- |
} |
| 152 |
168 |
|
if let Some(s) = &seen { |
| 153 |
169 |
|
let old = &s.checkpoint; |
| 154 |
170 |
|
if old.origin != cp.origin { |
| 155 |
|
- |
return two(format!("its log is now {}, not {}", cp.origin, old.origin)); |
| 156 |
|
- |
} |
| 157 |
|
- |
if cp.size < old.size { |
|
171 |
+ |
// A hub moved to another name keeps its key: a new log as |
|
172 |
+ |
// far as this user can tell, said so. |
|
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 { |
| 158 |
178 |
|
return two(format!( |
| 159 |
179 |
|
"its log has {} entries, fewer than the {} it showed before", |
| 160 |
180 |
|
cp.size, old.size |
| 161 |
181 |
|
)); |
| 162 |
|
- |
} |
| 163 |
|
- |
if cp.size == old.size && cp.root != old.root { |
|
182 |
+ |
} else if cp.size == old.size && cp.root != old.root { |
| 164 |
183 |
|
return two(format!("two different logs of {} entries", cp.size)); |
| 165 |
|
- |
} |
| 166 |
|
- |
if cp.size > old.size && old.size > 0 { |
|
184 |
+ |
} else if cp.size > old.size && old.size > 0 { |
| 167 |
185 |
|
let tiles = Tiles { |
| 168 |
186 |
|
hub, |
| 169 |
187 |
|
size: cp.size, |
|
|
… |
16 unchanged lines |
| 186 |
204 |
|
} |
| 187 |
205 |
|
} |
| 188 |
206 |
|
self.remember_log(hub, &identity, ¬e)?; |
| 189 |
|
- |
Ok(Ok(cp)) |
|
207 |
+ |
Ok(Ok((cp, notes))) |
| 190 |
208 |
|
} |
| 191 |
209 |
|
|
| 192 |
210 |
|
/// Check what a refs answer from `url` says about its host's log. |
| 193 |
|
- |
/// Fails only when the host showed two histories (or a log key other |
| 194 |
|
- |
/// than the one it showed before); everything else is a note. |
|
211 |
+ |
/// Fails only when the host showed two histories under one key; |
|
212 |
+ |
/// everything else is a note. |
| 195 |
213 |
|
pub(crate) fn check_log( |
| 196 |
214 |
|
&self, |
| 197 |
215 |
|
url: &str, |
|
|
… |
7 unchanged lines |
| 205 |
223 |
|
ca: ca.map(Path::to_path_buf), |
| 206 |
224 |
|
..RemoteOptions::default() |
| 207 |
225 |
|
}; |
| 208 |
|
- |
let cp = match self.accept_checkpoint(&hub, text, &options)? { |
| 209 |
|
- |
Ok(cp) => cp, |
|
226 |
+ |
let (cp, mut notes) = match self.accept_checkpoint(&hub, text, &options)? { |
|
227 |
+ |
Ok(c) => c, |
| 210 |
228 |
|
Err(why) => { |
| 211 |
229 |
|
return Ok(vec![format!( |
| 212 |
230 |
|
"{hub}'s transparency log was not checked: {why}" |
| 213 |
231 |
|
)]) |
| 214 |
232 |
|
} |
| 215 |
233 |
|
}; |
| 216 |
|
- |
let mut notes = Vec::new(); |
| 217 |
234 |
|
let mut missing = 0; |
| 218 |
235 |
|
for r in &resp.refs { |
| 219 |
236 |
|
let Some(a) = &r.attestation else { continue }; |
|
|
… |
38 unchanged lines |
| 258 |
275 |
|
pub checkpoint: Checkpoint, |
| 259 |
276 |
|
/// Every entry, numbered from 0. |
| 260 |
277 |
|
pub entries: Vec<Entry>, |
|
278 |
+ |
/// What accepting the checkpoint noted (a new log key, a new name). |
|
279 |
+ |
pub notes: Vec<String>, |
| 261 |
280 |
|
} |
| 262 |
281 |
|
|
| 263 |
282 |
|
impl Repo { |
|
|
… |
10 unchanged lines |
| 274 |
293 |
|
let tl = |e: TlogError| RepoError::Msg(e.to_string()); |
| 275 |
294 |
|
let text = String::from_utf8(http_get(&format!("{hub}/log/checkpoint"), &options)?) |
| 276 |
295 |
|
.map_err(|_| RepoError::Msg(format!("{hub}'s checkpoint is not text")))?; |
| 277 |
|
- |
let cp = match self.accept_checkpoint(&hub, &text, &options)? { |
|
296 |
+ |
let (cp, notes) = match self.accept_checkpoint(&hub, &text, &options)? { |
| 278 |
297 |
|
Ok(c) => c, |
| 279 |
298 |
|
Err(why) => return fail(format!("{hub}'s log could not be checked: {why}")), |
| 280 |
299 |
|
}; |
|
|
… |
34 unchanged lines |
| 315 |
334 |
|
hub, |
| 316 |
335 |
|
checkpoint: cp, |
| 317 |
336 |
|
entries, |
|
337 |
+ |
notes, |
| 318 |
338 |
|
}) |
| 319 |
339 |
|
} |
| 320 |
340 |
|
} |