| e5f8eb2a Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
use crate::cbor::{self, Value}; |
| 18 |
use crate::hash::{KeyId, ObjectRef}; |
| 19 |
use crate::identity::{verify, Domain, Identity, SecretKey, Signature}; |
| 20 |
use crate::objects::{array, bytes, check_version, field, map, oref, text, uint, ObjectError}; |
| 21 |
use sha2::{Digest, Sha256}; |
| 22 |
|
| 23 |
|
| 24 |
pub type Hash = [u8; 32]; |
| 25 |
|
| 26 |
|
| 27 |
pub const TILE_HEIGHT: u32 = 8; |
| 28 |
|
| 29 |
pub const TILE_WIDTH: u64 = 1 << TILE_HEIGHT; |
| 30 |
|
| 31 |
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] |
| 32 |
pub enum TlogError { |
| 33 |
#[error("the proof does not verify")] |
| 34 |
ProofFailed, |
| 35 |
#[error("{0}")] |
| 36 |
BadIndex(String), |
| 37 |
#[error("reading the log's hashes: {0}")] |
| 38 |
Read(String), |
| 39 |
#[error("not a valid note: {0}")] |
| 40 |
BadNote(String), |
| 41 |
#[error("{0}")] |
| 42 |
BadSignature(String), |
| 43 |
#[error("{0}")] |
| 44 |
Schema(String), |
| 45 |
} |
| 46 |
|
| 47 |
impl From<ObjectError> for TlogError { |
| 48 |
fn from(e: ObjectError) -> Self { |
| 49 |
TlogError::Schema(e.to_string()) |
| 50 |
} |
| 51 |
} |
| 52 |
|
| 53 |
type Result<T> = std::result::Result<T, TlogError>; |
| 54 |
|
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
pub fn leaf_hash(entry: &[u8]) -> Hash { |
| 59 |
let mut h = blake3::Hasher::new(); |
| 60 |
h.update(b"oxygen-tlog-leaf\0"); |
| 61 |
h.update(entry); |
| 62 |
*h.finalize().as_bytes() |
| 63 |
} |
| 64 |
|
| 65 |
|
| 66 |
pub fn node_hash(left: &Hash, right: &Hash) -> Hash { |
| 67 |
let mut h = blake3::Hasher::new(); |
| 68 |
h.update(b"oxygen-tlog-node\0"); |
| 69 |
h.update(left); |
| 70 |
h.update(right); |
| 71 |
*h.finalize().as_bytes() |
| 72 |
} |
| 73 |
|
| 74 |
|
| 75 |
pub fn empty_root() -> Hash { |
| 76 |
*blake3::hash(b"").as_bytes() |
| 77 |
} |
| 78 |
|
| 79 |
|
| 80 |
|
| 81 |
|
| 82 |
|
| 83 |
|
| 84 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 85 |
pub struct Entry { |
| 86 |
|
| 87 |
|
| 88 |
pub kind: String, |
| 89 |
|
| 90 |
pub repo: [u8; 16], |
| 91 |
|
| 92 |
pub object: ObjectRef, |
| 93 |
|
| 94 |
pub signers: Vec<KeyId>, |
| 95 |
|
| 96 |
pub received: u64, |
| 97 |
} |
| 98 |
|
| 99 |
impl Entry { |
| 100 |
pub fn to_value(&self) -> Value { |
| 101 |
Value::map([ |
| 102 |
(0, Value::Uint(1)), |
| 103 |
(1, Value::Text(self.kind.clone())), |
| 104 |
(2, Value::Bytes(self.repo.to_vec())), |
| 105 |
(3, Value::Bytes(self.object.0.to_vec())), |
| 106 |
( |
| 107 |
4, |
| 108 |
Value::Array( |
| 109 |
self.signers |
| 110 |
.iter() |
| 111 |
.map(|k| Value::Bytes(k.0.to_vec())) |
| 112 |
.collect(), |
| 113 |
), |
| 114 |
), |
| 115 |
(5, Value::Uint(self.received)), |
| 116 |
]) |
| 117 |
} |
| 118 |
|
| 119 |
pub fn encode(&self) -> Vec<u8> { |
| 120 |
cbor::encode(&self.to_value()) |
| 121 |
} |
| 122 |
|
| 123 |
pub fn decode(b: &[u8]) -> Result<Entry> { |
| 124 |
let v = cbor::decode(b).map_err(ObjectError::from)?; |
| 125 |
let m = map(&v, "log entry")?; |
| 126 |
check_version(m)?; |
| 127 |
let repo = bytes(field(m, 2, "repo")?, "repo")?; |
| 128 |
let repo: [u8; 16] = repo |
| 129 |
.try_into() |
| 130 |
.map_err(|_| TlogError::Schema("log entry: repo is not 16 bytes".into()))?; |
| 131 |
let mut signers = Vec::new(); |
| 132 |
for s in array(field(m, 4, "signers")?, "signers")? { |
| 133 |
signers.push(oref(s, "signer")?); |
| 134 |
} |
| 135 |
Ok(Entry { |
| 136 |
kind: text(field(m, 1, "kind")?, "kind")?.to_owned(), |
| 137 |
repo, |
| 138 |
object: oref(field(m, 3, "object")?, "object")?, |
| 139 |
signers, |
| 140 |
received: uint(field(m, 5, "received")?, "received")?, |
| 141 |
}) |
| 142 |
} |
| 143 |
|
| 144 |
|
| 145 |
pub fn leaf_hash(&self) -> Hash { |
| 146 |
leaf_hash(&self.encode()) |
| 147 |
} |
| 148 |
} |
| 149 |
|
| 150 |
|
| 151 |
|
| 152 |
pub fn encode_bundle(entries: &[Vec<u8>]) -> Result<Vec<u8>> { |
| 153 |
let mut out = Vec::new(); |
| 154 |
for e in entries { |
| 155 |
let n = u16::try_from(e.len()) |
| 156 |
.map_err(|_| TlogError::Schema("a log entry over 65535 bytes".into()))?; |
| 157 |
out.extend_from_slice(&n.to_be_bytes()); |
| 158 |
out.extend_from_slice(e); |
| 159 |
} |
| 160 |
Ok(out) |
| 161 |
} |
| 162 |
|
| 163 |
|
| 164 |
pub fn decode_bundle(mut b: &[u8]) -> Result<Vec<Vec<u8>>> { |
| 165 |
let mut out = Vec::new(); |
| 166 |
while !b.is_empty() { |
| 167 |
if b.len() < 2 { |
| 168 |
return Err(TlogError::Schema("entry bundle: a truncated length".into())); |
| 169 |
} |
| 170 |
let n = u16::from_be_bytes([b[0], b[1]]) as usize; |
| 171 |
if b.len() < 2 + n { |
| 172 |
return Err(TlogError::Schema("entry bundle: a truncated entry".into())); |
| 173 |
} |
| 174 |
out.push(b[2..2 + n].to_vec()); |
| 175 |
b = &b[2 + n..]; |
| 176 |
} |
| 177 |
Ok(out) |
| 178 |
} |
| 179 |
|
| 180 |
|
| 181 |
|
| 182 |
|
| 183 |
pub trait HashReader { |
| 184 |
|
| 185 |
fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>>; |
| 186 |
} |
| 187 |
|
| 188 |
|
| 189 |
impl HashReader for [Hash] { |
| 190 |
fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> { |
| 191 |
indexes |
| 192 |
.iter() |
| 193 |
.map(|&i| { |
| 194 |
self.get(i as usize) |
| 195 |
.copied() |
| 196 |
.ok_or_else(|| TlogError::Read(format!("no stored hash {i}"))) |
| 197 |
}) |
| 198 |
.collect() |
| 199 |
} |
| 200 |
} |
| 201 |
|
| 202 |
impl HashReader for Vec<Hash> { |
| 203 |
fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> { |
| 204 |
self.as_slice().read_hashes(indexes) |
| 205 |
} |
| 206 |
} |
| 207 |
|
| 208 |
|
| 209 |
fn maxpow2(n: u64) -> (u64, u32) { |
| 210 |
let mut l = 0; |
| 211 |
while (1u64 << (l + 1)) < n { |
| 212 |
l += 1; |
| 213 |
} |
| 214 |
(1 << l, l) |
| 215 |
} |
| 216 |
|
| 217 |
|
| 218 |
|
| 219 |
|
| 220 |
pub fn stored_hash_index(level: u32, mut n: u64) -> u64 { |
| 221 |
for _ in 0..level { |
| 222 |
n = 2 * n + 1; |
| 223 |
} |
| 224 |
let mut i = 0; |
| 225 |
let mut m = n; |
| 226 |
while m > 0 { |
| 227 |
i += m; |
| 228 |
m >>= 1; |
| 229 |
} |
| 230 |
i + level as u64 |
| 231 |
} |
| 232 |
|
| 233 |
|
| 234 |
pub fn split_stored_hash_index(index: u64) -> (u32, u64) { |
| 235 |
let mut n = index / 2; |
| 236 |
let mut index_n = stored_hash_index(0, n); |
| 237 |
debug_assert!(index_n <= index); |
| 238 |
loop { |
| 239 |
|
| 240 |
let x = index_n + 1 + (n + 1).trailing_zeros() as u64; |
| 241 |
if x > index { |
| 242 |
break; |
| 243 |
} |
| 244 |
n += 1; |
| 245 |
index_n = x; |
| 246 |
} |
| 247 |
let level = (index - index_n) as u32; |
| 248 |
(level, n >> level) |
| 249 |
} |
| 250 |
|
| 251 |
|
| 252 |
pub fn stored_hash_count(n: u64) -> u64 { |
| 253 |
if n == 0 { |
| 254 |
return 0; |
| 255 |
} |
| 256 |
let mut count = stored_hash_index(0, n - 1) + 1; |
| 257 |
let mut i = n - 1; |
| 258 |
while i & 1 != 0 { |
| 259 |
count += 1; |
| 260 |
i >>= 1; |
| 261 |
} |
| 262 |
count |
| 263 |
} |
| 264 |
|
| 265 |
|
| 266 |
|
| 267 |
pub fn stored_hashes_for_leaf( |
| 268 |
n: u64, |
| 269 |
leaf: Hash, |
| 270 |
r: &(impl HashReader + ?Sized), |
| 271 |
) -> Result<Vec<Hash>> { |
| 272 |
let m = (n + 1).trailing_zeros(); |
| 273 |
let indexes: Vec<u64> = (0..m) |
| 274 |
.rev() |
| 275 |
.map(|i| stored_hash_index(i, (n >> i) - 1)) |
| 276 |
.collect(); |
| 277 |
let old = r.read_hashes(&indexes)?; |
| 278 |
let mut h = leaf; |
| 279 |
let mut out = vec![h]; |
| 280 |
for i in 0..m as usize { |
| 281 |
h = node_hash(&old[m as usize - 1 - i], &h); |
| 282 |
out.push(h); |
| 283 |
} |
| 284 |
Ok(out) |
| 285 |
} |
| 286 |
|
| 287 |
fn sub_tree_index(mut lo: u64, hi: u64, need: &mut Vec<u64>) { |
| 288 |
while lo < hi { |
| 289 |
let (k, level) = maxpow2(hi - lo + 1); |
| 290 |
debug_assert_eq!(lo & (k - 1), 0); |
| 291 |
need.push(stored_hash_index(level, lo >> level)); |
| 292 |
lo += k; |
| 293 |
} |
| 294 |
} |
| 295 |
|
| 296 |
fn sub_tree_hash(mut lo: u64, hi: u64, hashes: &[Hash]) -> (Hash, &[Hash]) { |
| 297 |
let mut count = 0; |
| 298 |
while lo < hi { |
| 299 |
let (k, _) = maxpow2(hi - lo + 1); |
| 300 |
count += 1; |
| 301 |
lo += k; |
| 302 |
} |
| 303 |
let mut h = hashes[count - 1]; |
| 304 |
for i in (0..count - 1).rev() { |
| 305 |
h = node_hash(&hashes[i], &h); |
| 306 |
} |
| 307 |
(h, &hashes[count..]) |
| 308 |
} |
| 309 |
|
| 310 |
|
| 311 |
pub fn tree_hash(n: u64, r: &(impl HashReader + ?Sized)) -> Result<Hash> { |
| 312 |
if n == 0 { |
| 313 |
return Ok(empty_root()); |
| 314 |
} |
| 315 |
let mut need = Vec::new(); |
| 316 |
sub_tree_index(0, n, &mut need); |
| 317 |
let hashes = r.read_hashes(&need)?; |
| 318 |
Ok(sub_tree_hash(0, n, &hashes).0) |
| 319 |
} |
| 320 |
|
| 321 |
fn leaf_proof_index(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) { |
| 322 |
if lo + 1 == hi { |
| 323 |
return; |
| 324 |
} |
| 325 |
let (k, _) = maxpow2(hi - lo); |
| 326 |
if n < lo + k { |
| 327 |
leaf_proof_index(lo, lo + k, n, need); |
| 328 |
sub_tree_index(lo + k, hi, need); |
| 329 |
} else { |
| 330 |
sub_tree_index(lo, lo + k, need); |
| 331 |
leaf_proof_index(lo + k, hi, n, need); |
| 332 |
} |
| 333 |
} |
| 334 |
|
| 335 |
fn leaf_proof(lo: u64, hi: u64, n: u64, hashes: &[Hash]) -> (Vec<Hash>, &[Hash]) { |
| 336 |
if lo + 1 == hi { |
| 337 |
|
| 338 |
return (Vec::new(), hashes); |
| 339 |
} |
| 340 |
let (k, _) = maxpow2(hi - lo); |
| 341 |
let (mut p, th, rest); |
| 342 |
if n < lo + k { |
| 343 |
let (pp, h1) = leaf_proof(lo, lo + k, n, hashes); |
| 344 |
let (t, h2) = sub_tree_hash(lo + k, hi, h1); |
| 345 |
(p, th, rest) = (pp, t, h2); |
| 346 |
} else { |
| 347 |
let (t, h1) = sub_tree_hash(lo, lo + k, hashes); |
| 348 |
let (pp, h2) = leaf_proof(lo + k, hi, n, h1); |
| 349 |
(p, th, rest) = (pp, t, h2); |
| 350 |
} |
| 351 |
p.push(th); |
| 352 |
(p, rest) |
| 353 |
} |
| 354 |
|
| 355 |
|
| 356 |
pub fn prove_inclusion(t: u64, n: u64, r: &(impl HashReader + ?Sized)) -> Result<Vec<Hash>> { |
| 357 |
if n >= t { |
| 358 |
return Err(TlogError::BadIndex(format!( |
| 359 |
"leaf {n} is not in a tree of {t}" |
| 360 |
))); |
| 361 |
} |
| 362 |
let mut need = Vec::new(); |
| 363 |
leaf_proof_index(0, t, n, &mut need); |
| 364 |
if need.is_empty() { |
| 365 |
return Ok(Vec::new()); |
| 366 |
} |
| 367 |
let hashes = r.read_hashes(&need)?; |
| 368 |
Ok(leaf_proof(0, t, n, &hashes).0) |
| 369 |
} |
| 370 |
|
| 371 |
fn run_inclusion(p: &[Hash], lo: u64, hi: u64, n: u64, leaf: Hash) -> Result<Hash> { |
| 372 |
if lo + 1 == hi { |
| 373 |
return if p.is_empty() { |
| 374 |
Ok(leaf) |
| 375 |
} else { |
| 376 |
Err(TlogError::ProofFailed) |
| 377 |
}; |
| 378 |
} |
| 379 |
let Some((last, rest)) = p.split_last() else { |
| 380 |
return Err(TlogError::ProofFailed); |
| 381 |
}; |
| 382 |
let (k, _) = maxpow2(hi - lo); |
| 383 |
if n < lo + k { |
| 384 |
Ok(node_hash(&run_inclusion(rest, lo, lo + k, n, leaf)?, last)) |
| 385 |
} else { |
| 386 |
Ok(node_hash(last, &run_inclusion(rest, lo + k, hi, n, leaf)?)) |
| 387 |
} |
| 388 |
} |
| 389 |
|
| 390 |
|
| 391 |
|
| 392 |
pub fn check_inclusion(proof: &[Hash], t: u64, root: &Hash, n: u64, leaf: &Hash) -> Result<()> { |
| 393 |
if n >= t { |
| 394 |
return Err(TlogError::ProofFailed); |
| 395 |
} |
| 396 |
if run_inclusion(proof, 0, t, n, *leaf)? == *root { |
| 397 |
Ok(()) |
| 398 |
} else { |
| 399 |
Err(TlogError::ProofFailed) |
| 400 |
} |
| 401 |
} |
| 402 |
|
| 403 |
fn tree_proof_index(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) { |
| 404 |
if n == hi { |
| 405 |
if lo != 0 { |
| 406 |
sub_tree_index(lo, hi, need); |
| 407 |
} |
| 408 |
return; |
| 409 |
} |
| 410 |
let (k, _) = maxpow2(hi - lo); |
| 411 |
if n <= lo + k { |
| 412 |
tree_proof_index(lo, lo + k, n, need); |
| 413 |
sub_tree_index(lo + k, hi, need); |
| 414 |
} else { |
| 415 |
sub_tree_index(lo, lo + k, need); |
| 416 |
tree_proof_index(lo + k, hi, n, need); |
| 417 |
} |
| 418 |
} |
| 419 |
|
| 420 |
fn tree_proof(lo: u64, hi: u64, n: u64, hashes: &[Hash]) -> (Vec<Hash>, &[Hash]) { |
| 421 |
if n == hi { |
| 422 |
if lo == 0 { |
| 423 |
|
| 424 |
return (Vec::new(), hashes); |
| 425 |
} |
| 426 |
let (th, rest) = sub_tree_hash(lo, hi, hashes); |
| 427 |
return (vec![th], rest); |
| 428 |
} |
| 429 |
let (k, _) = maxpow2(hi - lo); |
| 430 |
let (mut p, th, rest); |
| 431 |
if n <= lo + k { |
| 432 |
let (pp, h1) = tree_proof(lo, lo + k, n, hashes); |
| 433 |
let (t, h2) = sub_tree_hash(lo + k, hi, h1); |
| 434 |
(p, th, rest) = (pp, t, h2); |
| 435 |
} else { |
| 436 |
let (t, h1) = sub_tree_hash(lo, lo + k, hashes); |
| 437 |
let (pp, h2) = tree_proof(lo + k, hi, n, h1); |
| 438 |
(p, th, rest) = (pp, t, h2); |
| 439 |
} |
| 440 |
p.push(th); |
| 441 |
(p, rest) |
| 442 |
} |
| 443 |
|
| 444 |
|
| 445 |
|
| 446 |
pub fn prove_consistency(t: u64, n: u64, r: &(impl HashReader + ?Sized)) -> Result<Vec<Hash>> { |
| 447 |
if n < 1 || n > t { |
| 448 |
return Err(TlogError::BadIndex(format!( |
| 449 |
"no consistency proof from {n} to {t}" |
| 450 |
))); |
| 451 |
} |
| 452 |
let mut need = Vec::new(); |
| 453 |
tree_proof_index(0, t, n, &mut need); |
| 454 |
if need.is_empty() { |
| 455 |
return Ok(Vec::new()); |
| 456 |
} |
| 457 |
let hashes = r.read_hashes(&need)?; |
| 458 |
Ok(tree_proof(0, t, n, &hashes).0) |
| 459 |
} |
| 460 |
|
| 461 |
fn run_consistency(p: &[Hash], lo: u64, hi: u64, n: u64, old: Hash) -> Result<(Hash, Hash)> { |
| 462 |
if n == hi { |
| 463 |
if lo == 0 { |
| 464 |
return if p.is_empty() { |
| 465 |
Ok((old, old)) |
| 466 |
} else { |
| 467 |
Err(TlogError::ProofFailed) |
| 468 |
}; |
| 469 |
} |
| 470 |
return match p { |
| 471 |
[h] => Ok((*h, *h)), |
| 472 |
_ => Err(TlogError::ProofFailed), |
| 473 |
}; |
| 474 |
} |
| 475 |
let Some((last, rest)) = p.split_last() else { |
| 476 |
return Err(TlogError::ProofFailed); |
| 477 |
}; |
| 478 |
let (k, _) = maxpow2(hi - lo); |
| 479 |
if n <= lo + k { |
| 480 |
let (oh, th) = run_consistency(rest, lo, lo + k, n, old)?; |
| 481 |
Ok((oh, node_hash(&th, last))) |
| 482 |
} else { |
| 483 |
let (oh, th) = run_consistency(rest, lo + k, hi, n, old)?; |
| 484 |
Ok((node_hash(last, &oh), node_hash(last, &th))) |
| 485 |
} |
| 486 |
} |
| 487 |
|
| 488 |
|
| 489 |
|
| 490 |
pub fn check_consistency( |
| 491 |
proof: &[Hash], |
| 492 |
t: u64, |
| 493 |
root: &Hash, |
| 494 |
n: u64, |
| 495 |
old_root: &Hash, |
| 496 |
) -> Result<()> { |
| 497 |
if n < 1 || n > t { |
| 498 |
return Err(TlogError::ProofFailed); |
| 499 |
} |
| 500 |
let (oh, th) = run_consistency(proof, 0, t, n, *old_root)?; |
| 501 |
if oh == *old_root && th == *root { |
| 502 |
Ok(()) |
| 503 |
} else { |
| 504 |
Err(TlogError::ProofFailed) |
| 505 |
} |
| 506 |
} |
| 507 |
|
| 508 |
|
| 509 |
|
| 510 |
|
| 511 |
|
| 512 |
#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 513 |
pub struct Tile { |
| 514 |
pub level: u32, |
| 515 |
pub n: u64, |
| 516 |
pub width: u32, |
| 517 |
} |
| 518 |
|
| 519 |
|
| 520 |
|
| 521 |
pub fn index_path(n: u64) -> String { |
| 522 |
let mut groups = Vec::new(); |
| 523 |
let mut m = n; |
| 524 |
loop { |
| 525 |
groups.push(m % 1000); |
| 526 |
m /= 1000; |
| 527 |
if m == 0 { |
| 528 |
break; |
| 529 |
} |
| 530 |
} |
| 531 |
groups.reverse(); |
| 532 |
let last = groups.len() - 1; |
| 533 |
groups |
| 534 |
.iter() |
| 535 |
.enumerate() |
| 536 |
.map(|(i, g)| { |
| 537 |
if i < last { |
| 538 |
format!("x{g:03}") |
| 539 |
} else { |
| 540 |
format!("{g:03}") |
| 541 |
} |
| 542 |
}) |
| 543 |
.collect::<Vec<_>>() |
| 544 |
.join("/") |
| 545 |
} |
| 546 |
|
| 547 |
|
| 548 |
pub fn parse_index_path(path: &str) -> Option<u64> { |
| 549 |
let parts: Vec<&str> = path.split('/').collect(); |
| 550 |
let mut n: u64 = 0; |
| 551 |
for (i, p) in parts.iter().enumerate() { |
| 552 |
let digits = if i + 1 < parts.len() { |
| 553 |
p.strip_prefix('x')? |
| 554 |
} else { |
| 555 |
p |
| 556 |
}; |
| 557 |
if digits.len() != 3 || !digits.bytes().all(|c| c.is_ascii_digit()) { |
| 558 |
return None; |
| 559 |
} |
| 560 |
n = n.checked_mul(1000)?.checked_add(digits.parse().ok()?)?; |
| 561 |
} |
| 562 |
Some(n) |
| 563 |
} |
| 564 |
|
| 565 |
impl Tile { |
| 566 |
|
| 567 |
pub fn path(&self) -> String { |
| 568 |
format!( |
| 569 |
"tile/{}/{}{}", |
| 570 |
self.level, |
| 571 |
index_path(self.n), |
| 572 |
self.partial() |
| 573 |
) |
| 574 |
} |
| 575 |
|
| 576 |
|
| 577 |
pub fn entries_path(&self) -> String { |
| 578 |
format!("tile/entries/{}{}", index_path(self.n), self.partial()) |
| 579 |
} |
| 580 |
|
| 581 |
fn partial(&self) -> String { |
| 582 |
if (self.width as u64) < TILE_WIDTH { |
| 583 |
format!(".p/{}", self.width) |
| 584 |
} else { |
| 585 |
String::new() |
| 586 |
} |
| 587 |
} |
| 588 |
|
| 589 |
|
| 590 |
pub fn parse(path: &str) -> Option<Tile> { |
| 591 |
let (level, rest) = path.split_once('/')?; |
| 592 |
let level: u32 = level.parse().ok()?; |
| 593 |
let (index, width) = match rest.split_once(".p/") { |
| 594 |
Some((i, w)) => { |
| 595 |
let w: u32 = w.parse().ok()?; |
| 596 |
if w == 0 || w as u64 >= TILE_WIDTH { |
| 597 |
return None; |
| 598 |
} |
| 599 |
(i, w) |
| 600 |
} |
| 601 |
None => (rest, TILE_WIDTH as u32), |
| 602 |
}; |
| 603 |
Some(Tile { |
| 604 |
level, |
| 605 |
n: parse_index_path(index)?, |
| 606 |
width, |
| 607 |
}) |
| 608 |
} |
| 609 |
|
| 610 |
|
| 611 |
|
| 612 |
pub fn for_index(index: u64) -> (Tile, usize, usize) { |
| 613 |
let (level, mut n) = split_stored_hash_index(index); |
| 614 |
let tl = level / TILE_HEIGHT; |
| 615 |
let within = level - tl * TILE_HEIGHT; |
| 616 |
let tn = (n << within) >> TILE_HEIGHT; |
| 617 |
n -= (tn << TILE_HEIGHT) >> within; |
| 618 |
let width = ((n + 1) << within) as u32; |
| 619 |
let tile = Tile { |
| 620 |
level: tl, |
| 621 |
n: tn, |
| 622 |
width, |
| 623 |
}; |
| 624 |
( |
| 625 |
tile, |
| 626 |
((n << within) * 32) as usize, |
| 627 |
(((n + 1) << within) * 32) as usize, |
| 628 |
) |
| 629 |
} |
| 630 |
|
| 631 |
|
| 632 |
pub fn read_data(&self, r: &(impl HashReader + ?Sized)) -> Result<Vec<u8>> { |
| 633 |
let start = self.n << TILE_HEIGHT; |
| 634 |
let indexes: Vec<u64> = (0..self.width as u64) |
| 635 |
.map(|i| stored_hash_index(TILE_HEIGHT * self.level, start + i)) |
| 636 |
.collect(); |
| 637 |
Ok(r.read_hashes(&indexes)?.concat()) |
| 638 |
} |
| 639 |
|
| 640 |
|
| 641 |
pub fn hash_at(&self, data: &[u8], index: u64) -> Result<Hash> { |
| 642 |
if self.width == 0 |
| 643 |
|| self.width as u64 > TILE_WIDTH |
| 644 |
|| data.len() < self.width as usize * 32 |
| 645 |
{ |
| 646 |
return Err(TlogError::Read(format!("tile {} is short", self.path()))); |
| 647 |
} |
| 648 |
let (t, start, end) = Tile::for_index(index); |
| 649 |
if t.level != self.level || t.n != self.n || t.width > self.width { |
| 650 |
return Err(TlogError::Read(format!( |
| 651 |
"stored hash {index} is not in tile {}", |
| 652 |
self.path() |
| 653 |
))); |
| 654 |
} |
| 655 |
Ok(tile_hash(&data[start..end])) |
| 656 |
} |
| 657 |
|
| 658 |
|
| 659 |
|
| 660 |
pub fn new_tiles(old: u64, new: u64) -> Vec<Tile> { |
| 661 |
let mut tiles = Vec::new(); |
| 662 |
let mut level = 0u32; |
| 663 |
while level < 8 && (new >> (TILE_HEIGHT * level)) > 0 { |
| 664 |
let old_n = old >> (TILE_HEIGHT * level); |
| 665 |
let new_n = new >> (TILE_HEIGHT * level); |
| 666 |
if old_n != new_n { |
| 667 |
for n in (old_n >> TILE_HEIGHT)..(new_n >> TILE_HEIGHT) { |
| 668 |
tiles.push(Tile { |
| 669 |
level, |
| 670 |
n, |
| 671 |
width: TILE_WIDTH as u32, |
| 672 |
}); |
| 673 |
} |
| 674 |
let n = new_n >> TILE_HEIGHT; |
| 675 |
let w = new_n - (n << TILE_HEIGHT); |
| 676 |
if w > 0 { |
| 677 |
tiles.push(Tile { |
| 678 |
level, |
| 679 |
n, |
| 680 |
width: w as u32, |
| 681 |
}); |
| 682 |
} |
| 683 |
} |
| 684 |
level += 1; |
| 685 |
} |
| 686 |
tiles |
| 687 |
} |
| 688 |
} |
| 689 |
|
| 690 |
fn tile_hash(data: &[u8]) -> Hash { |
| 691 |
if data.len() == 32 { |
| 692 |
let mut h = [0u8; 32]; |
| 693 |
h.copy_from_slice(data); |
| 694 |
return h; |
| 695 |
} |
| 696 |
let half = data.len() / 2; |
| 697 |
node_hash(&tile_hash(&data[..half]), &tile_hash(&data[half..])) |
| 698 |
} |
| 699 |
|
| 700 |
|
| 701 |
|
| 702 |
|
| 703 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 704 |
pub struct NoteSignature { |
| 705 |
pub name: String, |
| 706 |
pub key_hash: [u8; 4], |
| 707 |
pub sig: Vec<u8>, |
| 708 |
} |
| 709 |
|
| 710 |
|
| 711 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 712 |
pub struct Note { |
| 713 |
|
| 714 |
pub text: String, |
| 715 |
pub sigs: Vec<NoteSignature>, |
| 716 |
} |
| 717 |
|
| 718 |
|
| 719 |
const SIG_ED25519: u8 = 0x01; |
| 720 |
|
| 721 |
const SIG_COMPOSITE: u8 = 0x4f; |
| 722 |
|
| 723 |
fn key_hash(name: &str, sig_type: u8, key: &[u8]) -> [u8; 4] { |
| 724 |
let mut h = Sha256::new(); |
| 725 |
h.update(name.as_bytes()); |
| 726 |
h.update(b"\n"); |
| 727 |
h.update([sig_type]); |
| 728 |
h.update(key); |
| 729 |
let d = h.finalize(); |
| 730 |
[d[0], d[1], d[2], d[3]] |
| 731 |
} |
| 732 |
|
| 733 |
|
| 734 |
fn note_target(text: &str) -> ObjectRef { |
| 735 |
let mut h = blake3::Hasher::new(); |
| 736 |
h.update(b"oxygen-tlog-note\0"); |
| 737 |
h.update(text.as_bytes()); |
| 738 |
ObjectRef(*h.finalize().as_bytes()) |
| 739 |
} |
| 740 |
|
| 741 |
fn valid_name(name: &str) -> bool { |
| 742 |
!name.is_empty() && !name.contains(|c: char| c.is_whitespace() || c == '+') |
| 743 |
} |
| 744 |
|
| 745 |
impl Note { |
| 746 |
|
| 747 |
pub fn encode(&self) -> String { |
| 748 |
let mut out = self.text.clone(); |
| 749 |
out.push('\n'); |
| 750 |
for s in &self.sigs { |
| 751 |
let mut b = s.key_hash.to_vec(); |
| 752 |
b.extend_from_slice(&s.sig); |
| 753 |
out.push_str(&format!( |
| 754 |
"\u{2014} {} {}\n", |
| 755 |
s.name, |
| 756 |
crate::dsse::base64(&b) |
| 757 |
)); |
| 758 |
} |
| 759 |
out |
| 760 |
} |
| 761 |
|
| 762 |
pub fn parse(s: &str) -> Result<Note> { |
| 763 |
let bad = |w: &str| TlogError::BadNote(w.to_string()); |
| 764 |
let split = s |
| 765 |
.rfind("\n\n") |
| 766 |
.ok_or_else(|| bad("no blank line before the signatures"))?; |
| 767 |
let text = &s[..split + 1]; |
| 768 |
if text.chars().any(|c| c.is_control() && c != '\n') { |
| 769 |
return Err(bad("control characters in the text")); |
| 770 |
} |
| 771 |
let mut sigs = Vec::new(); |
| 772 |
for line in s[split + 2..].lines() { |
| 773 |
let rest = line |
| 774 |
.strip_prefix("\u{2014} ") |
| 775 |
.ok_or_else(|| bad("a signature line without its dash"))?; |
| 776 |
let (name, b64) = rest |
| 777 |
.split_once(' ') |
| 778 |
.ok_or_else(|| bad("a signature line without a key"))?; |
| 779 |
let b = |
| 780 |
crate::dsse::base64_decode(b64).ok_or_else(|| bad("a signature is not base64"))?; |
| 781 |
if b.len() < 5 || !valid_name(name) { |
| 782 |
return Err(bad("a malformed signature line")); |
| 783 |
} |
| 784 |
sigs.push(NoteSignature { |
| 785 |
name: name.to_string(), |
| 786 |
key_hash: [b[0], b[1], b[2], b[3]], |
| 787 |
sig: b[4..].to_vec(), |
| 788 |
}); |
| 789 |
} |
| 790 |
if sigs.is_empty() { |
| 791 |
return Err(bad("no signatures")); |
| 792 |
} |
| 793 |
Ok(Note { |
| 794 |
text: text.to_string(), |
| 795 |
sigs, |
| 796 |
}) |
| 797 |
} |
| 798 |
|
| 799 |
|
| 800 |
|
| 801 |
pub fn sign(text: &str, name: &str, identity: &Identity, sk: &SecretKey) -> Result<Note> { |
| 802 |
if !valid_name(name) { |
| 803 |
return Err(TlogError::BadNote(format!( |
| 804 |
"{name:?} cannot name a note's key" |
| 805 |
))); |
| 806 |
} |
| 807 |
if !text.ends_with('\n') || text.contains("\n\n") { |
| 808 |
return Err(TlogError::BadNote( |
| 809 |
"the text must end with one newline and hold no blank line".into(), |
| 810 |
)); |
| 811 |
} |
| 812 |
let ed = NoteSignature { |
| 813 |
name: name.to_string(), |
| 814 |
key_hash: key_hash(name, SIG_ED25519, &identity.ed25519_pk), |
| 815 |
sig: sk.sign_ed25519(text.as_bytes()).to_vec(), |
| 816 |
}; |
| 817 |
let composite = NoteSignature { |
| 818 |
name: name.to_string(), |
| 819 |
key_hash: key_hash(name, SIG_COMPOSITE, &identity.encode()), |
| 820 |
sig: cbor::encode( |
| 821 |
&sk.sign(Domain::Checkpoint, &identity.hash(), ¬e_target(text)) |
| 822 |
.to_value(), |
| 823 |
), |
| 824 |
}; |
| 825 |
Ok(Note { |
| 826 |
text: text.to_string(), |
| 827 |
sigs: vec![ed, composite], |
| 828 |
}) |
| 829 |
} |
| 830 |
|
| 831 |
|
| 832 |
|
| 833 |
pub fn verify(&self, name: &str, identity: &Identity) -> Result<()> { |
| 834 |
let find = |t: u8, key: &[u8]| { |
| 835 |
let h = key_hash(name, t, key); |
| 836 |
self.sigs.iter().find(|s| s.name == name && s.key_hash == h) |
| 837 |
}; |
| 838 |
let ed = find(SIG_ED25519, &identity.ed25519_pk) |
| 839 |
.ok_or_else(|| TlogError::BadSignature(format!("no Ed25519 signature by {name}")))?; |
| 840 |
identity |
| 841 |
.verify_ed25519(self.text.as_bytes(), &ed.sig) |
| 842 |
.map_err(|_| { |
| 843 |
TlogError::BadSignature(format!("{name}'s Ed25519 signature does not verify")) |
| 844 |
})?; |
| 845 |
let c = find(SIG_COMPOSITE, &identity.encode()) |
| 846 |
.ok_or_else(|| TlogError::BadSignature(format!("no composite signature by {name}")))?; |
| 847 |
let sig = cbor::decode(&c.sig) |
| 848 |
.map_err(|e| e.to_string()) |
| 849 |
.and_then(|v| Signature::from_value(&v).map_err(|e| e.to_string())) |
| 850 |
.map_err(|e| TlogError::BadSignature(format!("{name}'s composite signature: {e}")))?; |
| 851 |
verify(identity, &sig, Domain::Checkpoint, ¬e_target(&self.text)).map_err(|e| { |
| 852 |
TlogError::BadSignature(format!("{name}'s composite signature does not verify: {e}")) |
| 853 |
}) |
| 854 |
} |
| 855 |
} |
| 856 |
|
| 857 |
|
| 858 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 859 |
pub struct Checkpoint { |
| 860 |
|
| 861 |
pub origin: String, |
| 862 |
pub size: u64, |
| 863 |
pub root: Hash, |
| 864 |
|
| 865 |
pub extensions: Vec<String>, |
| 866 |
} |
| 867 |
|
| 868 |
impl Checkpoint { |
| 869 |
pub fn text(&self) -> String { |
| 870 |
let mut t = format!( |
| 871 |
"{}\n{}\n{}\n", |
| 872 |
self.origin, |
| 873 |
self.size, |
| 874 |
crate::dsse::base64(&self.root) |
| 875 |
); |
| 876 |
for e in &self.extensions { |
| 877 |
t.push_str(e); |
| 878 |
t.push('\n'); |
| 879 |
} |
| 880 |
t |
| 881 |
} |
| 882 |
|
| 883 |
pub fn parse(text: &str) -> Result<Checkpoint> { |
| 884 |
let bad = |w: &str| TlogError::BadNote(format!("checkpoint: {w}")); |
| 885 |
if !text.ends_with('\n') { |
| 886 |
return Err(bad("no final newline")); |
| 887 |
} |
| 888 |
let mut lines = text[..text.len() - 1].split('\n'); |
| 889 |
let origin = lines |
| 890 |
.next() |
| 891 |
.filter(|o| !o.is_empty()) |
| 892 |
.ok_or_else(|| bad("no origin"))?; |
| 893 |
let size = lines |
| 894 |
.next() |
| 895 |
.and_then(|s| { |
| 896 |
(s == "0" || !s.starts_with('0')) |
| 897 |
.then(|| s.parse().ok()) |
| 898 |
.flatten() |
| 899 |
}) |
| 900 |
.ok_or_else(|| bad("no tree size"))?; |
| 901 |
let root = lines |
| 902 |
.next() |
| 903 |
.and_then(crate::dsse::base64_decode) |
| 904 |
.and_then(|b| <Hash>::try_from(b).ok()) |
| 905 |
.ok_or_else(|| bad("no root hash"))?; |
| 906 |
Ok(Checkpoint { |
| 907 |
origin: origin.to_string(), |
| 908 |
size, |
| 909 |
root, |
| 910 |
extensions: lines.map(String::from).collect(), |
| 911 |
}) |
| 912 |
} |
| 913 |
|
| 914 |
|
| 915 |
pub fn sign(&self, identity: &Identity, sk: &SecretKey) -> Result<Note> { |
| 916 |
Note::sign(&self.text(), &self.origin, identity, sk) |
| 917 |
} |
| 918 |
|
| 919 |
|
| 920 |
|
| 921 |
pub fn verify(note: &Note, origin: &str, identity: &Identity) -> Result<Checkpoint> { |
| 922 |
note.verify(origin, identity)?; |
| 923 |
let c = Checkpoint::parse(¬e.text)?; |
| 924 |
if c.origin != origin { |
| 925 |
return Err(TlogError::BadNote(format!( |
| 926 |
"checkpoint of {}, not of {origin}", |
| 927 |
c.origin |
| 928 |
))); |
| 929 |
} |
| 930 |
Ok(c) |
| 931 |
} |
| 932 |
} |
| 933 |
|
| 934 |
#[cfg(test)] |
| 935 |
mod tests { |
| 936 |
use super::*; |
| 937 |
|
| 938 |
|
| 939 |
fn grow(leaves: u64) -> (Vec<Hash>, Vec<Hash>) { |
| 940 |
let mut stored: Vec<Hash> = Vec::new(); |
| 941 |
let mut leaf_hashes = Vec::new(); |
| 942 |
for n in 0..leaves { |
| 943 |
let h = leaf_hash(format!("entry {n}").as_bytes()); |
| 944 |
leaf_hashes.push(h); |
| 945 |
let add = stored_hashes_for_leaf(n, h, &stored).unwrap(); |
| 946 |
assert_eq!(stored.len() as u64, stored_hash_index(0, n)); |
| 947 |
stored.extend(add); |
| 948 |
assert_eq!(stored.len() as u64, stored_hash_count(n + 1)); |
| 949 |
} |
| 950 |
(stored, leaf_hashes) |
| 951 |
} |
| 952 |
|
| 953 |
|
| 954 |
fn mth(leaves: &[Hash]) -> Hash { |
| 955 |
match leaves.len() { |
| 956 |
0 => empty_root(), |
| 957 |
1 => leaves[0], |
| 958 |
n => { |
| 959 |
let (k, _) = maxpow2(n as u64); |
| 960 |
node_hash(&mth(&leaves[..k as usize]), &mth(&leaves[k as usize..])) |
| 961 |
} |
| 962 |
} |
| 963 |
} |
| 964 |
|
| 965 |
#[test] |
| 966 |
fn stored_indexes_round_trip() { |
| 967 |
for level in 0..6 { |
| 968 |
for n in 0..300 { |
| 969 |
let i = stored_hash_index(level, n); |
| 970 |
assert_eq!(split_stored_hash_index(i), (level, n)); |
| 971 |
} |
| 972 |
} |
| 973 |
} |
| 974 |
|
| 975 |
#[test] |
| 976 |
fn roots_match_rfc_6962_and_every_proof_verifies() { |
| 977 |
let (stored, leaves) = grow(140); |
| 978 |
for t in 1..=140u64 { |
| 979 |
let root = tree_hash(t, &stored).unwrap(); |
| 980 |
assert_eq!(root, mth(&leaves[..t as usize]), "root of {t}"); |
| 981 |
for n in 0..t { |
| 982 |
let p = prove_inclusion(t, n, &stored).unwrap(); |
| 983 |
check_inclusion(&p, t, &root, n, &leaves[n as usize]).unwrap(); |
| 984 |
|
| 985 |
let other = (n + 1) % t; |
| 986 |
if other != n { |
| 987 |
assert!(check_inclusion(&p, t, &root, n, &leaves[other as usize]).is_err()); |
| 988 |
} |
| 989 |
if let Some(first) = p.first() { |
| 990 |
let mut bad = p.clone(); |
| 991 |
bad[0] = node_hash(first, first); |
| 992 |
assert!(check_inclusion(&bad, t, &root, n, &leaves[n as usize]).is_err()); |
| 993 |
} |
| 994 |
let old = n + 1; |
| 995 |
let old_root = tree_hash(old, &stored).unwrap(); |
| 996 |
let c = prove_consistency(t, old, &stored).unwrap(); |
| 997 |
check_consistency(&c, t, &root, old, &old_root).unwrap(); |
| 998 |
if old < t { |
| 999 |
let fork = node_hash(&old_root, &old_root); |
| 1000 |
assert!( |
| 1001 |
check_consistency(&c, t, &root, old, &fork).is_err(), |
| 1002 |
"a forked history" |
| 1003 |
); |
| 1004 |
} |
| 1005 |
} |
| 1006 |
} |
| 1007 |
assert_eq!(tree_hash(0, &stored).unwrap(), empty_root()); |
| 1008 |
} |
| 1009 |
|
| 1010 |
#[test] |
| 1011 |
fn tiles_hold_the_stored_hashes() { |
| 1012 |
let n = 70_000u64; |
| 1013 |
let (stored, _) = grow(n); |
| 1014 |
let tiles = Tile::new_tiles(0, n); |
| 1015 |
|
| 1016 |
|
| 1017 |
assert_eq!(tiles.iter().filter(|t| t.level == 0).count(), 274); |
| 1018 |
assert_eq!(tiles.iter().filter(|t| t.level == 2).count(), 1); |
| 1019 |
let datas: Vec<(Tile, Vec<u8>)> = tiles |
| 1020 |
.iter() |
| 1021 |
.map(|t| (*t, t.read_data(&stored).unwrap())) |
| 1022 |
.collect(); |
| 1023 |
for (i, want) in stored.iter().enumerate().step_by(7) { |
| 1024 |
let (t, ..) = Tile::for_index(i as u64); |
| 1025 |
let (tile, data) = datas |
| 1026 |
.iter() |
| 1027 |
.find(|(d, _)| d.level == t.level && d.n == t.n && d.width >= t.width) |
| 1028 |
.expect("the tile holding it was written"); |
| 1029 |
assert_eq!( |
| 1030 |
tile.hash_at(data, i as u64).unwrap(), |
| 1031 |
*want, |
| 1032 |
"stored hash {i}" |
| 1033 |
); |
| 1034 |
} |
| 1035 |
|
| 1036 |
let grown = Tile::new_tiles(n, n + 300); |
| 1037 |
assert!(grown.iter().all(|t| t.level > 0 || t.n >= n / 256)); |
| 1038 |
} |
| 1039 |
|
| 1040 |
#[test] |
| 1041 |
fn tile_paths() { |
| 1042 |
assert_eq!(index_path(1_234_067), "x001/x234/067"); |
| 1043 |
assert_eq!(index_path(5), "005"); |
| 1044 |
assert_eq!(index_path(1000), "x001/000"); |
| 1045 |
assert_eq!(parse_index_path("x001/x234/067"), Some(1_234_067)); |
| 1046 |
assert_eq!(parse_index_path("x001/234/067"), None); |
| 1047 |
let t = Tile { |
| 1048 |
level: 1, |
| 1049 |
n: 1234, |
| 1050 |
width: 17, |
| 1051 |
}; |
| 1052 |
assert_eq!(t.path(), "tile/1/x001/234.p/17"); |
| 1053 |
assert_eq!(Tile::parse("1/x001/234.p/17"), Some(t)); |
| 1054 |
assert_eq!( |
| 1055 |
Tile { |
| 1056 |
level: 0, |
| 1057 |
n: 3, |
| 1058 |
width: 256 |
| 1059 |
} |
| 1060 |
.entries_path(), |
| 1061 |
"tile/entries/003" |
| 1062 |
); |
| 1063 |
assert_eq!(Tile::parse("0/003.p/256"), None, "a full tile has no .p"); |
| 1064 |
} |
| 1065 |
|
| 1066 |
#[test] |
| 1067 |
fn entries_and_bundles_round_trip() { |
| 1068 |
let e = Entry { |
| 1069 |
kind: "ref".into(), |
| 1070 |
repo: [7; 16], |
| 1071 |
object: ObjectRef([1; 32]), |
| 1072 |
signers: vec![ObjectRef([2; 32])], |
| 1073 |
received: 1_792_000_000_000, |
| 1074 |
}; |
| 1075 |
assert_eq!(Entry::decode(&e.encode()).unwrap(), e); |
| 1076 |
let b = encode_bundle(&[e.encode(), b"x".to_vec()]).unwrap(); |
| 1077 |
assert_eq!(decode_bundle(&b).unwrap(), vec![e.encode(), b"x".to_vec()]); |
| 1078 |
assert!(decode_bundle(&b[..b.len() - 1]).is_err()); |
| 1079 |
assert_eq!(e.leaf_hash(), leaf_hash(&e.encode())); |
| 1080 |
} |
| 1081 |
|
| 1082 |
#[test] |
| 1083 |
fn checkpoints_are_signed_twice_and_checked() { |
| 1084 |
let sk = SecretKey::generate(); |
| 1085 |
let hub = sk.identity("oxygenhub.io", "[email protected]", 1); |
| 1086 |
let (stored, _) = grow(10); |
| 1087 |
let c = Checkpoint { |
| 1088 |
origin: "oxygenhub.io/log".into(), |
| 1089 |
size: 10, |
| 1090 |
root: tree_hash(10, &stored).unwrap(), |
| 1091 |
extensions: vec![], |
| 1092 |
}; |
| 1093 |
let note = c.sign(&hub, &sk).unwrap(); |
| 1094 |
let served = note.encode(); |
| 1095 |
assert!(served.starts_with("oxygenhub.io/log\n10\n")); |
| 1096 |
assert_eq!(served.matches("\n\u{2014} oxygenhub.io/log ").count(), 2); |
| 1097 |
let back = Note::parse(&served).unwrap(); |
| 1098 |
assert_eq!( |
| 1099 |
Checkpoint::verify(&back, "oxygenhub.io/log", &hub).unwrap(), |
| 1100 |
c |
| 1101 |
); |
| 1102 |
|
| 1103 |
let other_sk = SecretKey::generate(); |
| 1104 |
let other = other_sk.identity("x", "x@x", 1); |
| 1105 |
assert!(Checkpoint::verify(&back, "oxygenhub.io/log", &other).is_err()); |
| 1106 |
assert!(Checkpoint::verify(&back, "elsewhere/log", &hub).is_err()); |
| 1107 |
let forged = Note { |
| 1108 |
text: back.text.replace("\n10\n", "\n11\n"), |
| 1109 |
..back.clone() |
| 1110 |
}; |
| 1111 |
assert!(Checkpoint::verify(&forged, "oxygenhub.io/log", &hub).is_err()); |
| 1112 |
let ed_only = Note { |
| 1113 |
sigs: vec![back.sigs[0].clone()], |
| 1114 |
..back.clone() |
| 1115 |
}; |
| 1116 |
assert!( |
| 1117 |
matches!(Checkpoint::verify(&ed_only, "oxygenhub.io/log", &hub), Err(TlogError::BadSignature(m)) if m.contains("composite")), |
| 1118 |
"the post-quantum line is required" |
| 1119 |
); |
| 1120 |
|
| 1121 |
let h = key_hash("oxygenhub.io/log", SIG_ED25519, &hub.ed25519_pk); |
| 1122 |
assert_eq!(back.sigs[0].key_hash, h); |
| 1123 |
assert!(hub |
| 1124 |
.verify_ed25519(back.text.as_bytes(), &back.sigs[0].sig) |
| 1125 |
.is_ok()); |
| 1126 |
} |
| 1127 |
} |