1use sha2::Digest;
26use sha2::Sha256;
27
28pub const MAX_RESULT_LENGTH: usize = 8192;
32
33const RECURSION_LIMIT: u32 = 256;
36
37pub const FILE_MAGIC: [u8; 31] = [
39 0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x00, 0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, 0xbf, 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94,
42];
43
44const MAJOR_VERSION: u8 = 0x01;
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
48pub enum Op {
49 Sha256,
51 Append(Vec<u8>),
53 Prepend(Vec<u8>),
55 Reverse,
57 Hexlify,
59}
60
61impl Op {
62 fn tag(&self) -> u8 {
63 match self {
64 Self::Sha256 => 0x08,
65 Self::Append(_) => 0xf0,
66 Self::Prepend(_) => 0xf1,
67 Self::Reverse => 0xf2,
68 Self::Hexlify => 0xf3,
69 }
70 }
71
72 pub fn apply(&self, msg: &[u8]) -> Result<Vec<u8>, WireError> {
74 match self {
75 Self::Sha256 => {
76 let mut h = Sha256::new();
77 h.update(msg);
78 Ok(h.finalize().to_vec())
79 }
80 Self::Append(suffix) => {
81 let mut out = Vec::with_capacity(msg.len() + suffix.len());
82 out.extend_from_slice(msg);
83 out.extend_from_slice(suffix);
84 Ok(out)
85 }
86 Self::Prepend(prefix) => {
87 let mut out = Vec::with_capacity(msg.len() + prefix.len());
88 out.extend_from_slice(prefix);
89 out.extend_from_slice(msg);
90 Ok(out)
91 }
92 Self::Reverse => {
93 if msg.is_empty() {
94 return Err(WireError::InvalidMessage(
95 "cannot reverse an empty message".into(),
96 ));
97 }
98 Ok(msg.iter().rev().copied().collect())
99 }
100 Self::Hexlify => Ok(hex::encode(msg).into_bytes()),
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
107pub enum Attestation {
108 Pending(String),
111 BitcoinBlockHeader(u32),
113 LitecoinBlockHeader(u32),
115}
116
117impl Attestation {
118 fn tag(&self) -> [u8; 8] {
119 match self {
120 Self::Pending(_) => [0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e],
121 Self::BitcoinBlockHeader(_) => [0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01],
122 Self::LitecoinBlockHeader(_) => [0x06, 0x86, 0x9a, 0x0d, 0x73, 0xd7, 0x1b, 0x45],
123 }
124 }
125}
126
127#[derive(Debug, Clone, Default, PartialEq, Eq)]
129pub struct TimestampNode {
130 pub attestations: Vec<Attestation>,
132 pub ops: Vec<(Op, TimestampNode)>,
134}
135
136impl TimestampNode {
137 pub fn is_empty(&self) -> bool {
139 self.attestations.is_empty() && self.ops.is_empty()
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct OtsFile {
146 pub digest: Vec<u8>,
149 pub root: TimestampNode,
151}
152
153#[derive(Debug, thiserror::Error)]
155pub enum WireError {
156 #[error("malformed OTS proof: {0}")]
158 Malformed(String),
159 #[error("unknown op tag 0x{0:02x}")]
161 UnknownTag(u8),
162 #[error("truncated OTS proof: {0}")]
164 Truncated(String),
165 #[error("invalid message for op: {0}")]
167 InvalidMessage(String),
168 #[error("recursion limit exceeded")]
170 RecursionLimit,
171}
172
173pub fn replay(file: &OtsFile) -> Result<Vec<(Vec<u8>, Attestation)>, WireError> {
180 let mut out = Vec::new();
181 replay_node(&file.root, &file.digest, &mut out, 0)?;
182 Ok(out)
183}
184
185fn replay_node(
186 node: &TimestampNode,
187 msg: &[u8],
188 out: &mut Vec<(Vec<u8>, Attestation)>,
189 depth: u32,
190) -> Result<(), WireError> {
191 if depth > RECURSION_LIMIT {
192 return Err(WireError::RecursionLimit);
193 }
194 for attestation in &node.attestations {
195 out.push((msg.to_vec(), attestation.clone()));
196 }
197 for (op, child) in &node.ops {
198 let next = op.apply(msg)?;
199 replay_node(child, &next, out, depth + 1)?;
200 }
201 Ok(())
202}
203
204#[derive(Debug, Clone, Default, PartialEq, Eq)]
207pub struct OtsWireVerification {
208 pub pending: Vec<(Vec<u8>, String)>,
210 pub bitcoin: Vec<(Vec<u8>, u32)>,
212 pub litecoin: Vec<(Vec<u8>, u32)>,
214}
215
216impl OtsWireVerification {
217 pub fn has_attestation(&self) -> bool {
219 !self.pending.is_empty() || !self.bitcoin.is_empty() || !self.litecoin.is_empty()
220 }
221}
222
223pub fn verify(file: &OtsFile) -> Result<OtsWireVerification, WireError> {
227 let pairs = replay(file)?;
228 let mut out = OtsWireVerification::default();
229 for (msg, attestation) in pairs {
230 match attestation {
231 Attestation::Pending(uri) => out.pending.push((msg, uri)),
232 Attestation::BitcoinBlockHeader(h) => out.bitcoin.push((msg, h)),
233 Attestation::LitecoinBlockHeader(h) => out.litecoin.push((msg, h)),
234 }
235 }
236 Ok(out)
237}
238
239pub fn parse(digest: &[u8], bytes: &[u8]) -> Result<OtsFile, WireError> {
241 if bytes.len() < FILE_MAGIC.len() + 1 {
242 return Err(WireError::Truncated("shorter than the file header".into()));
243 }
244 if bytes[..FILE_MAGIC.len()] != FILE_MAGIC {
245 return Err(WireError::Malformed("bad file header magic".into()));
246 }
247 let mut pos = FILE_MAGIC.len();
248 if bytes[pos] != MAJOR_VERSION {
249 return Err(WireError::Malformed(format!(
250 "unsupported major version {}",
251 bytes[pos]
252 )));
253 }
254 pos += 1;
255
256 let mut cursor = Cursor { bytes, pos };
257 let root = parse_node(&mut cursor)?;
258 if cursor.pos != bytes.len() {
259 return Err(WireError::Malformed(format!(
260 "{} trailing bytes after the proof tree",
261 bytes.len() - cursor.pos
262 )));
263 }
264 Ok(OtsFile {
265 digest: digest.to_vec(),
266 root,
267 })
268}
269
270struct Cursor<'a> {
271 bytes: &'a [u8],
272 pos: usize,
273}
274
275impl Cursor<'_> {
276 fn u8(&mut self) -> Result<u8, WireError> {
277 let b = *self
278 .bytes
279 .get(self.pos)
280 .ok_or_else(|| WireError::Truncated("expected a byte".into()))?;
281 self.pos += 1;
282 Ok(b)
283 }
284
285 fn take(&mut self, n: usize) -> Result<&[u8], WireError> {
286 let end = self
287 .pos
288 .checked_add(n)
289 .ok_or_else(|| WireError::Malformed("length overflow".into()))?;
290 let s = self
291 .bytes
292 .get(self.pos..end)
293 .ok_or_else(|| WireError::Truncated(format!("expected {n} bytes")))?;
294 self.pos = end;
295 Ok(s)
296 }
297
298 fn varuint(&mut self) -> Result<u64, WireError> {
299 let mut value: u64 = 0;
300 let mut shift = 0u32;
301 loop {
302 let b = self.u8()?;
303 value |= u64::from(b & 0x7f) << shift;
304 if b & 0x80 == 0 {
305 return Ok(value);
306 }
307 shift += 7;
308 if shift > 63 {
309 return Err(WireError::Malformed("varuint too long".into()));
310 }
311 }
312 }
313
314 fn varbytes(&mut self, max: usize) -> Result<Vec<u8>, WireError> {
315 let len = self.varuint()?;
316 if len > max as u64 {
317 return Err(WireError::Malformed(format!(
318 "payload length {len} exceeds {max}"
319 )));
320 }
321 Ok(self.take(len as usize)?.to_vec())
322 }
323}
324
325fn parse_node(cur: &mut Cursor<'_>) -> Result<TimestampNode, WireError> {
326 parse_node_depth(cur, 0)
327}
328
329fn parse_node_depth(cur: &mut Cursor<'_>, depth: u32) -> Result<TimestampNode, WireError> {
330 if depth > RECURSION_LIMIT {
331 return Err(WireError::RecursionLimit);
332 }
333 let mut node = TimestampNode::default();
334
335 let mut tag = cur.u8()?;
336 while tag == 0xff {
337 tag = cur.u8()?;
339 apply_tag(cur, &mut node, tag, depth)?;
340 tag = cur.u8()?;
341 }
342 apply_tag(cur, &mut node, tag, depth)?;
343 Ok(node)
344}
345
346fn apply_tag(
347 cur: &mut Cursor<'_>,
348 node: &mut TimestampNode,
349 tag: u8,
350 depth: u32,
351) -> Result<(), WireError> {
352 match tag {
353 0x00 => {
354 node.attestations.push(parse_attestation(cur)?);
355 }
356 0x08 => {
357 let child = parse_node_depth(cur, depth + 1)?;
358 node.ops.push((Op::Sha256, child));
359 }
360 0xf0 => {
361 let arg = cur.varbytes(MAX_RESULT_LENGTH)?;
362 if arg.is_empty() {
363 return Err(WireError::Malformed("append arg can't be empty".into()));
364 }
365 let child = parse_node_depth(cur, depth + 1)?;
366 node.ops.push((Op::Append(arg), child));
367 }
368 0xf1 => {
369 let arg = cur.varbytes(MAX_RESULT_LENGTH)?;
370 if arg.is_empty() {
371 return Err(WireError::Malformed("prepend arg can't be empty".into()));
372 }
373 let child = parse_node_depth(cur, depth + 1)?;
374 node.ops.push((Op::Prepend(arg), child));
375 }
376 0xf2 => {
377 let child = parse_node_depth(cur, depth + 1)?;
378 node.ops.push((Op::Reverse, child));
379 }
380 0xf3 => {
381 let child = parse_node_depth(cur, depth + 1)?;
382 node.ops.push((Op::Hexlify, child));
383 }
384 other => return Err(WireError::UnknownTag(other)),
385 }
386 Ok(())
387}
388
389fn parse_attestation(cur: &mut Cursor<'_>) -> Result<Attestation, WireError> {
390 let tag: [u8; 8] = cur.take(8)?.try_into().expect("take(8) yields 8 bytes");
391 match tag {
392 [0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e] => {
393 let uri = cur.varbytes(1000)?;
394 let uri = String::from_utf8(uri)
395 .map_err(|_| WireError::Malformed("pending attestation URI is not UTF-8".into()))?;
396 Ok(Attestation::Pending(uri))
397 }
398 [0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01] => {
399 let h = cur.take(4)?;
400 Ok(Attestation::BitcoinBlockHeader(u32::from_le_bytes(
401 h.try_into().expect("take(4) yields 4 bytes"),
402 )))
403 }
404 [0x06, 0x86, 0x9a, 0x0d, 0x73, 0xd7, 0x1b, 0x45] => {
405 let h = cur.take(4)?;
406 Ok(Attestation::LitecoinBlockHeader(u32::from_le_bytes(
407 h.try_into().expect("take(4) yields 4 bytes"),
408 )))
409 }
410 _ => Err(WireError::UnknownTag(tag[0])),
411 }
412}
413
414pub fn serialize(file: &OtsFile) -> Result<Vec<u8>, WireError> {
417 let mut out = Vec::new();
418 out.extend_from_slice(&FILE_MAGIC);
419 out.push(MAJOR_VERSION);
420 serialize_node(&file.root, &mut out)?;
421 Ok(out)
422}
423
424fn serialize_node(node: &TimestampNode, out: &mut Vec<u8>) -> Result<(), WireError> {
425 if node.is_empty() {
426 return Err(WireError::Malformed(
427 "an empty timestamp node can't be serialized".into(),
428 ));
429 }
430 let mut attestations = node.attestations.clone();
431 attestations.sort();
432
433 let mut ops = node.ops.clone();
434 ops.sort_by(|a, b| a.0.cmp(&b.0));
435
436 let total = attestations.len() + ops.len();
437 let mut emitted = 0usize;
438
439 for attestation in &attestations {
440 if emitted + 1 < total {
441 out.extend_from_slice(&[0xff]);
442 }
443 out.push(0x00);
444 serialize_attestation(attestation, out);
445 emitted += 1;
446 }
447 for (op, child) in &ops {
448 if emitted + 1 < total {
449 out.extend_from_slice(&[0xff]);
450 }
451 serialize_op(op, out);
452 serialize_node(child, out)?;
453 emitted += 1;
454 }
455 Ok(())
456}
457
458fn serialize_op(op: &Op, out: &mut Vec<u8>) {
459 match op {
460 Op::Sha256 | Op::Reverse | Op::Hexlify => out.push(op.tag()),
461 Op::Append(arg) | Op::Prepend(arg) => {
462 out.push(op.tag());
463 write_varbytes(arg, out);
464 }
465 }
466}
467
468fn serialize_attestation(attestation: &Attestation, out: &mut Vec<u8>) {
469 match attestation {
470 Attestation::Pending(uri) => {
471 out.extend_from_slice(&attestation.tag());
472 write_varbytes(uri.as_bytes(), out);
473 }
474 Attestation::BitcoinBlockHeader(h) | Attestation::LitecoinBlockHeader(h) => {
475 out.extend_from_slice(&attestation.tag());
476 out.extend_from_slice(&h.to_le_bytes());
477 }
478 }
479}
480
481fn write_varbytes(bytes: &[u8], out: &mut Vec<u8>) {
482 write_varuint(bytes.len() as u64, out);
483 out.extend_from_slice(bytes);
484}
485
486fn write_varuint(mut value: u64, out: &mut Vec<u8>) {
487 loop {
488 let mut b = (value & 0x7f) as u8;
489 value >>= 7;
490 if value != 0 {
491 b |= 0x80;
492 }
493 out.push(b);
494 if value == 0 {
495 return;
496 }
497 }
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 pub(super) fn digest(b: u8) -> Vec<u8> {
505 vec![b; 32]
506 }
507
508 #[test]
509 fn round_trip_sha256_append_pending() {
510 let file = OtsFile {
511 digest: digest(1),
512 root: TimestampNode {
513 attestations: vec![],
514 ops: vec![(
515 Op::Sha256,
516 TimestampNode {
517 attestations: vec![],
518 ops: vec![(
519 Op::Append(vec![0xaa, 0xbb]),
520 TimestampNode {
521 attestations: vec![Attestation::Pending(
522 "https://alice.btc.calendar.opentimestamps.org".into(),
523 )],
524 ops: vec![],
525 },
526 )],
527 },
528 )],
529 },
530 };
531 let bytes = serialize(&file).unwrap();
532 let parsed = parse(&digest(1), &bytes).unwrap();
533 assert_eq!(parsed, file);
534
535 let pairs = replay(&file).unwrap();
536 assert_eq!(pairs.len(), 1);
537 let (msg, att) = &pairs[0];
538 assert!(matches!(att, Attestation::Pending(_)));
539 let mut h = Sha256::new();
542 h.update(digest(1));
543 let mut expected = h.finalize().to_vec();
544 expected.extend_from_slice(&[0xaa, 0xbb]);
545 assert_eq!(msg, &expected);
546 }
547
548 #[test]
549 fn round_trip_multiple_siblings_and_bitcoin_attestation() {
550 let file = OtsFile {
551 digest: digest(2),
552 root: TimestampNode {
553 attestations: vec![Attestation::BitcoinBlockHeader(800_123)],
554 ops: vec![
558 (
559 Op::Prepend(vec![0x01]),
560 TimestampNode {
561 attestations: vec![],
562 ops: vec![(
563 Op::Hexlify,
564 TimestampNode {
565 attestations: vec![Attestation::Pending(
566 "https://hex.cal".into(),
567 )],
568 ops: vec![],
569 },
570 )],
571 },
572 ),
573 (
574 Op::Reverse,
575 TimestampNode {
576 attestations: vec![Attestation::Pending("https://example.org".into())],
577 ops: vec![],
578 },
579 ),
580 ],
581 },
582 };
583 let bytes = serialize(&file).unwrap();
584 let parsed = parse(&digest(2), &bytes).unwrap();
585 assert_eq!(parsed, file);
586
587 let pairs = replay(&file).unwrap();
588 assert_eq!(pairs.len(), 3);
590 assert!(
591 pairs
592 .iter()
593 .any(|(_, a)| matches!(a, Attestation::BitcoinBlockHeader(800_123)))
594 );
595 let hex_msg = pairs
597 .iter()
598 .map(|(m, _)| m)
599 .find(|m| m.len() == 66)
600 .expect("hexlified message present");
601 let mut expected = vec![0x01];
602 expected.extend(digest(2));
603 assert_eq!(hex_msg, hex::encode(expected).as_bytes());
604 }
605
606 #[test]
607 fn file_magic_is_the_canonical_31_bytes() {
608 assert_eq!(FILE_MAGIC.len(), 31);
611 assert_eq!(
612 &FILE_MAGIC[23..],
613 &[0xbf, 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94]
614 );
615 }
616
617 #[test]
618 fn varuint_leb128_round_trip() {
619 for value in [0u64, 1, 127, 128, 300, 8192, 1 << 20, u32::MAX as u64] {
620 let mut buf = Vec::new();
621 write_varuint(value, &mut buf);
622 let mut cur = Cursor {
623 bytes: &buf,
624 pos: 0,
625 };
626 assert_eq!(cur.varuint().unwrap(), value);
627 }
628 }
629
630 #[test]
631 fn rejects_bad_magic() {
632 let mut bytes = Vec::new();
633 bytes.extend_from_slice(&FILE_MAGIC);
634 bytes.push(MAJOR_VERSION);
635 bytes.push(0x00); bytes.extend_from_slice(&[0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e]);
637 write_varbytes(b"https://example.org", &mut bytes);
638
639 let mut bad = bytes.clone();
640 bad[0] = 0x01;
641 assert!(matches!(
642 parse(&digest(9), &bad),
643 Err(WireError::Malformed(_))
644 ));
645 assert!(parse(&digest(9), &bytes).is_ok());
646 }
647
648 #[test]
649 fn rejects_truncated_stream() {
650 let bytes = {
651 let mut b = Vec::new();
652 b.extend_from_slice(&FILE_MAGIC);
653 b.push(MAJOR_VERSION);
654 b.push(0x08); b
656 };
657 assert!(matches!(
658 parse(&digest(9), &bytes),
659 Err(WireError::Truncated(_))
660 ));
661 }
662
663 #[test]
664 fn rejects_unknown_op_tag() {
665 let mut bytes = Vec::new();
666 bytes.extend_from_slice(&FILE_MAGIC);
667 bytes.push(MAJOR_VERSION);
668 bytes.push(0x67); assert!(matches!(
670 parse(&digest(9), &bytes),
671 Err(WireError::UnknownTag(0x67))
672 ));
673 }
674
675 #[test]
676 fn rejects_trailing_bytes() {
677 let mut bytes = Vec::new();
678 bytes.extend_from_slice(&FILE_MAGIC);
679 bytes.push(MAJOR_VERSION);
680 bytes.push(0x00);
681 bytes.extend_from_slice(&[0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e]);
682 write_varbytes(b"https://example.org", &mut bytes);
683 bytes.push(0xff);
684 assert!(matches!(
685 parse(&digest(9), &bytes),
686 Err(WireError::Malformed(_))
687 ));
688 }
689
690 #[test]
691 fn rejects_oversized_append_payload() {
692 let mut bytes = Vec::new();
693 bytes.extend_from_slice(&FILE_MAGIC);
694 bytes.push(MAJOR_VERSION);
695 bytes.push(0xf0);
696 write_varbytes(&vec![0u8; MAX_RESULT_LENGTH + 1], &mut bytes);
697 assert!(matches!(
698 parse(&digest(9), &bytes),
699 Err(WireError::Malformed(_))
700 ));
701 }
702
703 #[test]
704 fn replay_diverges_on_tampered_op() {
705 let file = OtsFile {
706 digest: digest(5),
707 root: TimestampNode {
708 attestations: vec![],
709 ops: vec![(
710 Op::Append(vec![0x01]),
711 TimestampNode {
712 attestations: vec![Attestation::BitcoinBlockHeader(1)],
713 ops: vec![],
714 },
715 )],
716 },
717 };
718 let pairs = replay(&file).unwrap();
719 let (msg, _) = &pairs[0];
720
721 let tampered = OtsFile {
722 digest: digest(6), ..file.clone()
724 };
725 let pairs2 = replay(&tampered).unwrap();
726 assert_ne!(msg, &pairs2[0].0);
727 }
728
729 #[test]
730 fn empty_node_cannot_serialize() {
731 let file = OtsFile {
732 digest: digest(1),
733 root: TimestampNode::default(),
734 };
735 assert!(matches!(serialize(&file), Err(WireError::Malformed(_))));
736 }
737}
738
739#[cfg(test)]
740mod adversarial_tests {
741 use super::tests::digest;
744 use super::*;
745
746 #[test]
747 fn replay_rejects_reverse_of_empty_intermediate() {
748 let file = OtsFile {
752 digest: Vec::new(),
753 root: TimestampNode {
754 attestations: vec![],
755 ops: vec![(
756 Op::Reverse,
757 TimestampNode {
758 attestations: vec![Attestation::Pending("https://x".into())],
759 ops: vec![],
760 },
761 )],
762 },
763 };
764 assert!(matches!(replay(&file), Err(WireError::InvalidMessage(_))));
765 }
766
767 #[test]
768 fn parse_rejects_empty_append_argument() {
769 let mut bytes = Vec::new();
770 bytes.extend_from_slice(&FILE_MAGIC);
771 bytes.push(MAJOR_VERSION);
772 bytes.push(0xf0);
773 write_varbytes(b"", &mut bytes); bytes.push(0x00);
775 assert!(matches!(
776 parse(&digest(9), &bytes),
777 Err(WireError::Malformed(_))
778 ));
779 }
780}