diff --git a/.github/workflows/bar.yml b/.github/workflows/bar.yml new file mode 100644 index 0000000..32457ed --- /dev/null +++ b/.github/workflows/bar.yml @@ -0,0 +1,21 @@ +name: verified bar + +on: + push: + pull_request: + # Rerun when the spec reviewer approves, or an approval is dismissed. + pull_request_review: + types: [submitted, dismissed] + +jobs: + bar: + # pull-requests: read lets the check confirm the spec approval on GitHub. + permissions: + contents: read + pull-requests: read + # Both lines pin the same commit of Proven-Commons/mission: bar v2, as + # proposed in its pull request #2. Move both to a release tag once + # mission publishes one. + uses: Proven-Commons/mission/.github/workflows/verified-bar.yml@6ba85c27abaaf07905c9316aa96dc29f7b5847a2 + with: + bar-ref: 6ba85c27abaaf07905c9316aa96dc29f7b5847a2 diff --git a/.gitignore b/.gitignore index 01f8cdb..dcf83c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .lake/ +__pycache__/ +tests/ntpd-rs/target/ diff --git a/Main.lean b/Main.lean new file mode 100644 index 0000000..904646e --- /dev/null +++ b/Main.lean @@ -0,0 +1,171 @@ +/- + Command line front end. + + ntp serve + + answers requests on standard input, one per line, for the + differential tests. Octets are lower-case hex digits, two per octet, + and an empty list of octets is written `-`. Numbers are decimal, and + a signed one may start with `-`. + + decode HEX prints OK and the packet, or FAIL (decode) + encode PACKET prints OK and the hex of the octets that + `encode` writes for the packet, when + `decode` reads the packet back from + them, or FAIL (encode, decode) + unix ERA RAW prints OK, the seconds and the fraction of + the Unix time of the timestamp RAW in + era ERA, or FAIL (toUnix) + fromunix S F prints OK, the era and the timestamp of the + Unix time S + F / 2^32 seconds, + or FAIL (fromUnix) + + A packet is written as 15 fields, separated by spaces: + - LI, VN, Mode, Stratum, Poll, Precision, Root Delay, Root + Dispersion, Reference ID, and the Reference, Origin, Receive and + Transmit Timestamps, in decimal: Poll and Precision as signed + numbers, the others as the unsigned number their bits hold; + - the extension fields: `-` when there are none, or else each one + as its Field Type in decimal, `:` and the hex of its Value and + Padding (`-` when empty), separated by commas; + - the hex of the MAC, or `-` when there is none. + `decode` prints a packet this way, and `encode` reads one. By + `Spec.decode_encode`, `encode` prints OK exactly when the packet it + is given is valid. + + A malformed request prints `error`: an unknown operation, the wrong + number of fields, hex digits that are not in pairs or not lower case, + or a number that is not decimal. A request line may end in LF or + CR LF. + + Every answer comes from the proved functions in `Ntp/`. The hex + coding, the output and the request handling here are covered by the + tests, not by a proof. +-/ + +import Ntp + +open Ntp Spec + +def hexValue (c : Char) : Option Nat := + if '0' ≤ c ∧ c ≤ '9' then some (c.toNat - '0'.toNat) + else if 'a' ≤ c ∧ c ≤ 'f' then some (c.toNat - 'a'.toNat + 10) + else none + +/-- Octets from hex digits in pairs. Tail recursive, so that long + inputs do not use the stack. -/ +def hexBytes (cs : List Char) (acc : Array UInt8) : Option (Array UInt8) := + match cs with + | [] => some acc + | a :: b :: rest => + match hexValue a, hexValue b with + | some hi, some lo => hexBytes rest (acc.push (hi * 16 + lo).toUInt8) + | _, _ => none + | [_] => none + +/-- The octets a field gives: `-` for none, else hex digits in pairs. -/ +def fromHex (f : String) : Option (List UInt8) := + if f = "-" then some [] else if f.isEmpty then none else (hexBytes f.toList #[]).map (·.toList) + +def hexDigit (n : Nat) : Char := + if n < 10 then Char.ofNat ('0'.toNat + n) else Char.ofNat ('a'.toNat + n - 10) + +/-- A field for a list of octets: `-` when it is empty. -/ +def toHex (bs : List UInt8) : String := + if bs.isEmpty then "-" + else String.ofList (bs.flatMap fun b => [hexDigit (b.toNat / 16), hexDigit (b.toNat % 16)]) + +/-- A decimal number, without sign or leading `+`. -/ +def decimal (s : String) : Option Nat := + if !s.isEmpty && s.all Char.isDigit then s.toNat? else none + +/-- A decimal number, with `-` in front when it is negative. -/ +def integer (s : String) : Option Int := + match s.toList with + | '-' :: cs => (decimal (String.ofList cs)).map fun n => -(n : Int) + | _ => (decimal s).map fun n => (n : Int) + +def showField (e : ExtField) : String := s!"{e.type}:{toHex e.body}" + +def showPacket (p : Packet) : String := + let h := p.header + let exts := if p.exts.isEmpty then "-" else ",".intercalate (p.exts.map showField) + " ".intercalate [toString h.leap, toString h.version, toString h.mode, toString h.stratum, + toString h.poll, toString h.precision, toString h.rootDelay, toString h.rootDispersion, + toString h.refId, toString h.reference, toString h.origin, toString h.receive, + toString h.transmit, exts, toHex p.mac] + +def readField (s : String) : Option ExtField := + match s.splitOn ":" with + | [t, b] => do + let type ← decimal t + let body ← fromHex b + pure { type, body } + | _ => none + +def readFieldList (s : String) : Option (List ExtField) := + if s = "-" then some [] else (s.splitOn ",").mapM readField + +/-- A packet written as `showPacket` writes one. -/ +def readPacket : List String → Option Packet + | [l, v, m, st, po, pr, rd, rs, ri, rt, ot, vt, xt, es, mac] => do + let header : Header := + { leap := ← decimal l, version := ← decimal v, mode := ← decimal m, + stratum := ← decimal st, poll := ← integer po, precision := ← integer pr, + rootDelay := ← decimal rd, rootDispersion := ← decimal rs, refId := ← decimal ri, + reference := ← decimal rt, origin := ← decimal ot, receive := ← decimal vt, + transmit := ← decimal xt } + let exts ← readFieldList es + let mac ← fromHex mac + pure { header, exts, mac } + | _ => none + +/-- The answer to one request line, or `none` when it is malformed. -/ +def request (line : String) : Option String := + match line.splitOn " " with + | ["decode", h] => do + let b ← fromHex h + pure (match decode b with + | some p => s!"OK {showPacket p}" + | none => "FAIL") + | "encode" :: fields => do + let p ← readPacket fields + let b := encode p + pure (if decode b = some p then s!"OK {toHex b}" else "FAIL") + | ["unix", e, r] => do + let era ← integer e + let raw ← decimal r + pure (match toUnix era raw with + | some (s, f) => s!"OK {s} {f}" + | none => "FAIL") + | ["fromunix", s, f] => do + let seconds ← integer s + let fraction ← decimal f + pure (match fromUnix seconds fraction with + | some (era, raw) => s!"OK {era} {raw}" + | none => "FAIL") + | _ => none + +/-- A request line without its line ending, LF or CR LF. -/ +def chomp (line : String) : String := + let cs := line.toList + if cs.getLast? = some '\n' then + let cs := cs.dropLast + String.ofList (if cs.getLast? = some '\r' then cs.dropLast else cs) + else line + +def serve : IO Unit := do + let stdin ← IO.getStdin + let stdout ← IO.getStdout + repeat + let line ← stdin.getLine + if line.isEmpty then break + stdout.putStrLn ((request (chomp line)).getD "error") + stdout.flush + +def usage : String := "usage: ntp serve" + +def main (args : List String) : IO UInt32 := do + match args with + | ["serve"] => serve; return 0 + | _ => IO.eprintln usage; return 2 diff --git a/Ntp.lean b/Ntp.lean new file mode 100644 index 0000000..4212394 --- /dev/null +++ b/Ntp.lean @@ -0,0 +1,2 @@ +import Ntp.Packet +import Ntp.Time diff --git a/Ntp/Packet.lean b/Ntp/Packet.lean new file mode 100644 index 0000000..4885044 --- /dev/null +++ b/Ntp/Packet.lean @@ -0,0 +1,130 @@ +/- + Reading and writing the NTP packet: the 48-octet header of RFC 5905, + then extension fields (RFC 7822) and a MAC. The structures the reader + gives back are those of `Spec/Defs.lean`; the functions here are the + code under proof, and share nothing else with the specification. +-/ + +import Spec.Defs + +namespace Ntp + +open Spec (Header ExtField Packet) + +/-! ## Octets -/ + +/-- Splits off the first `w` octets, if there are that many. Looks at + no more than those `w`. -/ +def take? (w : Nat) (b : List UInt8) : Option (List UInt8 × List UInt8) := + let c := b.take w + if c.length = w then some (c, b.drop w) else none + +/-- The octets as a big-endian unsigned integer. -/ +def bigEndian (bs : List UInt8) : Nat := + bs.foldl (fun n b => n * 256 + b.toNat) 0 + +/-- A `w`-octet big-endian integer at the start of the input, and the + rest. -/ +def readBE (w : Nat) (b : List UInt8) : Option (Nat × List UInt8) := + (take? w b).map fun (c, r) => (bigEndian c, r) + +/-- `n` as `w` octets, most significant first. -/ +def writeBE : Nat → Nat → List UInt8 + | 0, _ => [] + | w + 1, n => writeBE w (n / 256) ++ [(n % 256).toUInt8] + +/-- An octet read as an 8-bit two's complement integer. -/ +def toSigned (n : Nat) : Int := + if n < 128 then n else (n : Int) - 256 + +/-- An 8-bit two's complement integer as an octet. -/ +def ofSigned (z : Int) : Nat := + if z < 0 then (z + 256).toNat else z.toNat + +/-- Whether the list has more than `n` elements. Looks at no more than + `n + 1` of them. -/ +def longer : Nat → List UInt8 → Bool + | _, [] => false + | 0, _ :: _ => true + | n + 1, _ :: t => longer n t + +/-! ## Reading -/ + +/-- The 48-octet header at the start of the input, and the rest. The + first octet holds LI in its top two bits, then VN in three, then + Mode in three. Only versions 3 and 4 and modes 1 to 5 are read. -/ +def readHeader (b : List UInt8) : Option (Header × List UInt8) := do + let (lvm, r) ← readBE 1 b + let version := lvm / 8 % 8 + let mode := lvm % 8 + guard ((version == 3 || version == 4) && 1 ≤ mode && mode ≤ 5) + let (stratum, r) ← readBE 1 r + let (poll, r) ← readBE 1 r + let (precision, r) ← readBE 1 r + let (rootDelay, r) ← readBE 4 r + let (rootDispersion, r) ← readBE 4 r + let (refId, r) ← readBE 4 r + let (reference, r) ← readBE 8 r + let (origin, r) ← readBE 8 r + let (receive, r) ← readBE 8 r + let (transmit, r) ← readBE 8 r + pure ({ leap := lvm / 64, version, mode, stratum, poll := toSigned poll, + precision := toSigned precision, rootDelay, rootDispersion, refId, reference, + origin, receive, transmit }, r) + +/-- Whether what is left after the header and the extension fields is + no MAC, or a MAC of a 4-octet key identifier and a 16- or 20-octet + digest. -/ +def macLength (t : List UInt8) : Bool := + t.length == 0 || t.length == 20 || t.length == 24 + +/-- The extension fields of a version 4 packet and its MAC, from the + octets after the header. While more than 24 octets are left, they + start with a field: a 16-bit Field Type, a 16-bit Length that counts + the whole field, at least 16 and a multiple of 4, then the rest of + the field. What is left after the last field is the MAC. `fuel` + bounds the number of fields. -/ +def readFields : Nat → List UInt8 → Option (List ExtField × List UInt8) + | 0, _ => none + | fuel + 1, t => + if longer 24 t then do + let (type, r) ← readBE 2 t + let (len, r) ← readBE 2 r + guard (16 ≤ len && len % 4 == 0) + let (body, r) ← take? (len - 4) r + let (fields, mac) ← readFields fuel r + pure ({ type, body } :: fields, mac) + else if macLength t then some ([], t) else none + +/-- The extension fields and the MAC after the header. Four zero octets + are a crypto-NAK. Otherwise a version 4 packet has fields and then a + MAC, and a version 3 packet a MAC only. -/ +def readTail (version : Nat) (t : List UInt8) : Option (List ExtField × List UInt8) := + if t = [0, 0, 0, 0] then some ([], t) + else if version = 4 then readFields (t.length + 1) t + else if macLength t then some ([], t) + else none + +/-- The packet in one UDP payload, or `none` when the payload is not a + packet this reader accepts. -/ +def decode (b : List UInt8) : Option Packet := do + let (header, t) ← readHeader b + let (exts, mac) ← readTail header.version t + pure { header, exts, mac } + +/-! ## Writing -/ + +/-- An extension field as its octets. -/ +def writeField (e : ExtField) : List UInt8 := + writeBE 2 e.type ++ writeBE 2 (e.body.length + 4) ++ e.body + +/-- A packet as its octets. -/ +def encode (p : Packet) : List UInt8 := + let h := p.header + writeBE 1 (h.leap * 64 + h.version * 8 + h.mode) ++ writeBE 1 h.stratum ++ + writeBE 1 (ofSigned h.poll) ++ writeBE 1 (ofSigned h.precision) ++ + writeBE 4 h.rootDelay ++ writeBE 4 h.rootDispersion ++ writeBE 4 h.refId ++ + writeBE 8 h.reference ++ writeBE 8 h.origin ++ writeBE 8 h.receive ++ writeBE 8 h.transmit ++ + p.exts.flatMap writeField ++ p.mac + +end Ntp diff --git a/Ntp/Time.lean b/Ntp/Time.lean new file mode 100644 index 0000000..7f2a348 --- /dev/null +++ b/Ntp/Time.lean @@ -0,0 +1,29 @@ +/- + NTP timestamps and Unix time. A timestamp is 32 bits of seconds and 32 + bits of fraction since the start of its era (RFC 5905 section 6); the + era is not in the packet, so it is an argument here. +-/ + +namespace Ntp + +/-- The Unix time of the 64-bit timestamp `raw` in era `era`, as whole + seconds and a fraction in units of 2^-32 seconds; `none` for the raw + value 0, which means unknown, and for a value of more than 64 bits. + The seconds are `era * 2^32` plus the seconds field, less the + 2,208,988,800 seconds from 1900 to 1970. -/ +def toUnix (era : Int) (raw : Nat) : Option (Int × Nat) := + if raw = 0 ∨ 18446744073709551616 ≤ raw then none + else some (era * 4294967296 + (raw / 4294967296 : Nat) - 2208988800, raw % 4294967296) + +/-- The era and the 64-bit timestamp of a Unix time, given as whole + seconds and a fraction in units of 2^-32 seconds; `none` when the + fraction is not below 2^32, and at the first instant of an era, + whose timestamp would be 0, the value that means unknown. -/ +def fromUnix (seconds : Int) (fraction : Nat) : Option (Int × Nat) := + if 4294967296 ≤ fraction then none + else + let s := seconds + 2208988800 + let raw := (s % 4294967296).toNat * 4294967296 + fraction + if raw = 0 then none else some (s / 4294967296, raw) + +end Ntp diff --git a/Proofs.lean b/Proofs.lean new file mode 100644 index 0000000..ad0eed6 --- /dev/null +++ b/Proofs.lean @@ -0,0 +1,6 @@ +import Proofs.Bytes +import Proofs.Header +import Proofs.Tail +import Proofs.Packet +import Proofs.Time +import Proofs.Main diff --git a/Proofs/Bytes.lean b/Proofs/Bytes.lean new file mode 100644 index 0000000..4b65244 --- /dev/null +++ b/Proofs/Bytes.lean @@ -0,0 +1,166 @@ +import Ntp + +/-! + Fixed-width fields: `take?`, big-endian integers, `readBE` and + `writeBE`, against `Spec.beBytes`. +-/ + +namespace NtpProofs + +open Spec Ntp + +theorem take?_eq (w : Nat) (b c r : List UInt8) : + take? w b = some (c, r) ↔ c.length = w ∧ b = c ++ r := by + unfold take? + dsimp only + constructor + · intro h + split at h + · rename_i hl + simp only [Option.some.injEq, Prod.mk.injEq] at h + obtain ⟨rfl, rfl⟩ := h + exact ⟨hl, (List.take_append_drop w b).symm⟩ + · exact absurd h (by simp) + · rintro ⟨hl, rfl⟩ + subst hl + simp + +theorem take?_append (w : Nat) (c r : List UInt8) (h : c.length = w) : + take? w (c ++ r) = some (c, r) := + (take?_eq _ _ _ _).mpr ⟨h, rfl⟩ + +theorem take?_none (w : Nat) (b : List UInt8) (h : b.length < w) : take? w b = none := by + unfold take? + dsimp only + split + · rename_i h' + rw [List.length_take] at h' + omega + · rfl + +theorem beBytes_length (w n : Nat) : (beBytes w n).length = w := by + simp [beBytes] + +theorem foldl_be (acc : Nat) (bs : List UInt8) : + bs.foldl (fun n b => n * 256 + b.toNat) acc = + acc * 256 ^ bs.length + bs.foldl (fun n b => n * 256 + b.toNat) 0 := by + induction bs generalizing acc with + | nil => simp + | cons b bs ih => + simp only [List.foldl_cons, List.length_cons] + rw [ih, ih (0 * 256 + b.toNat)] + simp [Nat.pow_succ, Nat.mul_add, Nat.mul_assoc, Nat.add_assoc, Nat.mul_comm] + +theorem bigEndian_cons (b : UInt8) (bs : List UInt8) : + bigEndian (b :: bs) = b.toNat * 256 ^ bs.length + bigEndian bs := by + unfold bigEndian + rw [List.foldl_cons, foldl_be] + simp + +theorem bigEndian_lt (bs : List UInt8) : bigEndian bs < 256 ^ bs.length := by + induction bs with + | nil => simp [bigEndian] + | cons b bs ih => + rw [bigEndian_cons, List.length_cons, Nat.pow_succ] + have hb : b.toNat ≤ 255 := by have := b.toNat_lt; omega + have h1 : b.toNat * 256 ^ bs.length ≤ 255 * 256 ^ bs.length := Nat.mul_le_mul_right _ hb + omega + +theorem beBytes_succ (w n : Nat) : + beBytes (w + 1) n = (n / 256 ^ w % 256).toUInt8 :: beBytes w n := by + simp [beBytes, List.range_succ] + +theorem toNat_toUInt8 (n : Nat) (h : n < 256) : n.toUInt8.toNat = n := by + simp [Nat.toUInt8, Nat.mod_eq_of_lt h] + +theorem bigEndian_beBytes (w n : Nat) : bigEndian (beBytes w n) = n % 256 ^ w := by + induction w with + | zero => simp [beBytes, bigEndian, Nat.mod_one] + | succ w ih => + rw [beBytes_succ, bigEndian_cons, ih, beBytes_length, + toNat_toUInt8 _ (Nat.mod_lt _ (by decide)), Nat.pow_succ, Nat.mod_mul, + Nat.mul_comm (n / 256 ^ w % 256)] + omega + +theorem bigEndian_beBytes_of_lt (w n : Nat) (h : n < 256 ^ w) : bigEndian (beBytes w n) = n := by + rw [bigEndian_beBytes, Nat.mod_eq_of_lt h] + +theorem beBytes_add (w a n : Nat) : beBytes w (a * 256 ^ w + n) = beBytes w n := by + unfold beBytes + apply List.map_congr_left + intro i hi + simp only [List.mem_reverse, List.mem_range] at hi + congr 1 + obtain ⟨d, rfl⟩ : ∃ d, w = i + d + 1 := ⟨w - i - 1, by omega⟩ + rw [show i + d + 1 = i + (d + 1) by omega, Nat.pow_add, ← Nat.mul_assoc, + Nat.mul_right_comm a, Nat.add_comm, Nat.add_mul_div_right _ _ (Nat.pow_pos (by decide)), + Nat.pow_succ, ← Nat.mul_assoc, Nat.add_mul_mod_self_right] + +theorem beBytes_bigEndian (c : List UInt8) : beBytes c.length (bigEndian c) = c := by + induction c with + | nil => rfl + | cons b c ih => + rw [List.length_cons, beBytes_succ, bigEndian_cons] + have hc := bigEndian_lt c + have hdiv : (b.toNat * 256 ^ c.length + bigEndian c) / 256 ^ c.length = b.toNat := by + rw [Nat.add_comm, Nat.add_mul_div_right _ _ (Nat.pow_pos (by decide)), + Nat.div_eq_of_lt hc, Nat.zero_add] + rw [hdiv, Nat.mod_eq_of_lt b.toNat_lt, beBytes_add, ih] + simp + +theorem readBE_iff (w : Nat) (b : List UInt8) (n : Nat) (r : List UInt8) : + readBE w b = some (n, r) ↔ n < 256 ^ w ∧ b = beBytes w n ++ r := by + unfold readBE + constructor + · intro h + simp only [Option.map_eq_some_iff, Prod.exists] at h + obtain ⟨c, r', hc, he⟩ := h + simp only [Prod.mk.injEq] at he + obtain ⟨rfl, rfl⟩ := he + obtain ⟨hl, rfl⟩ := (take?_eq _ _ _ _).mp hc + subst hl + exact ⟨bigEndian_lt c, by rw [beBytes_bigEndian]⟩ + · rintro ⟨hn, rfl⟩ + rw [take?_append w _ r (beBytes_length _ _)] + simp [bigEndian_beBytes_of_lt _ _ hn] + +theorem readBE_append (w n : Nat) (r : List UInt8) (hn : n < 256 ^ w) : + readBE w (beBytes w n ++ r) = some (n, r) := + (readBE_iff _ _ _ _).mpr ⟨hn, rfl⟩ + +/-- Reading `w` octets from `w` written ones gives the number modulo + `256 ^ w`, whatever it was. -/ +theorem readBE_append_mod (w n : Nat) (r : List UInt8) : + readBE w (beBytes w n ++ r) = some (n % 256 ^ w, r) := by + unfold readBE + rw [take?_append w _ r (beBytes_length _ _)] + simp [bigEndian_beBytes] + +theorem guard_some (p : Prop) [Decidable p] (u : Unit) : + (guard p : Option Unit) = some u ↔ p := by + unfold guard + split <;> simp_all [failure, pure] + +theorem beBytes_one (a : Nat) (h : a < 256) : beBytes 1 a = [a.toUInt8] := by + simp [beBytes, Nat.mod_eq_of_lt h] + +theorem readBE_one (a : UInt8) (r : List UInt8) : readBE 1 (a :: r) = some (a.toNat, r) := by + have := readBE_append 1 a.toNat r (by simpa using a.toNat_lt) + rw [beBytes_one _ a.toNat_lt] at this + simpa using this + +theorem beBytes_snoc (w n : Nat) : + beBytes (w + 1) n = beBytes w (n / 256) ++ [(n % 256).toUInt8] := by + induction w generalizing n with + | zero => simp [beBytes] + | succ w ih => + rw [beBytes_succ, ih, beBytes_succ, Nat.div_div_eq_div_mul, Nat.pow_succ, + Nat.mul_comm (256 ^ w) 256] + rfl + +theorem writeBE_eq (w n : Nat) : writeBE w n = beBytes w n := by + induction w generalizing n with + | zero => rfl + | succ w ih => rw [writeBE, ih, beBytes_snoc] + +end NtpProofs diff --git a/Proofs/Header.lean b/Proofs/Header.lean new file mode 100644 index 0000000..7626a38 --- /dev/null +++ b/Proofs/Header.lean @@ -0,0 +1,87 @@ +import Proofs.Bytes + +/-! + The header reader against `Spec.headerBytes` and `Spec.HeaderValid`. +-/ + +namespace NtpProofs + +open Spec Ntp + +theorem headerBytes_length (h : Header) : (headerBytes h).length = 48 := by + simp [headerBytes, beBytes_length] + +/-- The first octet gives back LI, VN and Mode. -/ +theorem lvm_bits (a : Nat) : a / 64 * 64 + a / 8 % 8 * 8 + a % 8 = a := by + omega + +theorem signedByte_toSigned (n : Nat) (h : n < 256) : signedByte (toSigned n) = n.toUInt8 := by + unfold signedByte toSigned + congr 1 + split <;> omega + +theorem toSigned_signedByte (z : Int) (h1 : -128 ≤ z) (h2 : z < 128) : + toSigned (signedByte z).toNat = z := by + unfold signedByte toSigned + rw [toNat_toUInt8 _ (by omega)] + split <;> omega + +theorem toSigned_range (n : Nat) (h : n < 256) : -128 ≤ toSigned n ∧ toSigned n < 128 := by + unfold toSigned + split <;> omega + +theorem readHeader_some (b : List UInt8) (h : Header) (t : List UInt8) + (H : readHeader b = some (h, t)) : HeaderValid h ∧ b = headerBytes h ++ t := by + unfold readHeader at H + simp only [bind, Option.bind_eq_some_iff, Prod.exists, readBE_iff, guard_some, pure, + Option.some.injEq, Prod.mk.injEq, Bool.and_eq_true, Bool.or_eq_true, beq_iff_eq, + decide_eq_true_eq, Nat.reducePow, exists_and_left, exists_const] at H + obtain ⟨lvm, r, ⟨hlvm, rfl⟩, ⟨⟨hver, hm1⟩, hm2⟩, st, r, ⟨hst, rfl⟩, po, r, ⟨hpo, rfl⟩, + pr, r, ⟨hpr, rfl⟩, rd, r, ⟨hrd, rfl⟩, rs, r, ⟨hrs, rfl⟩, ri, r, ⟨hri, rfl⟩, + rt, r, ⟨hrt, rfl⟩, ot, r, ⟨hot, rfl⟩, vt, r, ⟨hvt, rfl⟩, xt, r, ⟨hxt, rfl⟩, rfl, rfl⟩ := H + have hp := toSigned_range po hpo + have hq := toSigned_range pr hpr + refine ⟨?_, ?_⟩ + · simp only [HeaderValid, Nat.reducePow] + refine ⟨by omega, hver, hm1, hm2, hst, hp.1, hp.2, hq.1, hq.2, hrd, hrs, hri, hrt, hot, hvt, + hxt⟩ + · simp only [headerBytes, List.append_assoc, beBytes_one _ hlvm, beBytes_one _ hst, + beBytes_one _ hpo, beBytes_one _ hpr, lvm_bits lvm, signedByte_toSigned _ hpo, + signedByte_toSigned _ hpr, List.cons_append, List.nil_append] + +theorem readBE_one' (a : UInt8) (l r : List UInt8) : + readBE 1 ((a :: l) ++ r) = some (a.toNat, l ++ r) := + readBE_one a (l ++ r) + +theorem readHeader_bytes (h : Header) (hv : HeaderValid h) (rest : List UInt8) : + readHeader (headerBytes h ++ rest) = some (h, rest) := by + have hv' := hv + simp only [HeaderValid] at hv' + obtain ⟨hl, hver, hm1, hm2, hst, hp1, hp2, hq1, hq2, hrd, hrs, hri, hrt, hot, hvt, hxt⟩ := hv' + have b4 : ∀ n, n < 2 ^ 32 → n < 256 ^ 4 := fun n h => by simpa using h + have b8 : ∀ n, n < 2 ^ 64 → n < 256 ^ 8 := fun n h => by simpa using h + have hlvm : h.leap * 64 + h.version * 8 + h.mode < 256 := by omega + unfold readHeader headerBytes + simp only [bind, List.append_assoc, List.cons_append, List.nil_append, readBE_one, + Option.bind_some, + readBE_append 4 _ _ (b4 _ hrd), readBE_append 4 _ _ (b4 _ hrs), readBE_append 4 _ _ (b4 _ hri), + readBE_append 8 _ _ (b8 _ hrt), readBE_append 8 _ _ (b8 _ hot), readBE_append 8 _ _ (b8 _ hvt), + readBE_append 8 _ _ (b8 _ hxt), toNat_toUInt8 _ hlvm, toNat_toUInt8 _ hst, + toSigned_signedByte _ hp1 hp2, toSigned_signedByte _ hq1 hq2] + have e1 : (h.leap * 64 + h.version * 8 + h.mode) / 8 % 8 = h.version := by omega + have e2 : (h.leap * 64 + h.version * 8 + h.mode) % 8 = h.mode := by omega + have e3 : (h.leap * 64 + h.version * 8 + h.mode) / 64 = h.leap := by omega + have hg : ((h.version == 3 || h.version == 4) && decide (1 ≤ h.mode) && decide (h.mode ≤ 5)) + = true := by + simp only [Bool.and_eq_true, Bool.or_eq_true, beq_iff_eq, decide_eq_true_eq] + exact ⟨⟨hver, hm1⟩, hm2⟩ + simp only [e1, e2, e3, hg, guard, ite_true, Option.bind_some, pure] + +theorem readHeader_iff (b : List UInt8) (h : Header) (t : List UInt8) : + readHeader b = some (h, t) ↔ HeaderValid h ∧ b = headerBytes h ++ t := by + constructor + · exact readHeader_some b h t + · rintro ⟨hv, rfl⟩ + exact readHeader_bytes h hv t + +end NtpProofs diff --git a/Proofs/Main.lean b/Proofs/Main.lean new file mode 100644 index 0000000..eb23bd2 --- /dev/null +++ b/Proofs/Main.lean @@ -0,0 +1,175 @@ +/- + The statements of `Spec.lean`, each proved from the lemmas in the + other files of `Proofs/`. `lake comparator` checks that each theorem + here is exactly the one in `Spec.lean`. +-/ + +import Proofs.Packet +import Proofs.Time +import Spec.Examples + +namespace Spec + +theorem decode_iff (b : List UInt8) (p : Packet) : + Ntp.decode b = some p ↔ Valid p ∧ b = packetBytes p := + NtpProofs.decode_iff b p + +theorem encode_eq (p : Packet) (hp : Valid p) : Ntp.encode p = packetBytes p := + NtpProofs.encode_eq p hp + +theorem decode_encode (p : Packet) : Ntp.decode (Ntp.encode p) = some p ↔ Valid p := by + constructor + · intro h + exact ((decode_iff _ _).mp h).1 + · intro hp + rw [encode_eq p hp] + exact (decode_iff _ _).mpr ⟨hp, rfl⟩ + +theorem encode_decode (b : List UInt8) (p : Packet) (h : Ntp.decode b = some p) : + Ntp.encode p = b := by + obtain ⟨hp, rfl⟩ := (decode_iff _ _).mp h + exact encode_eq p hp + +theorem decode_short (b : List UInt8) (h : b.length < 48) : Ntp.decode b = none := by + cases hd : Ntp.decode b with + | none => rfl + | some p => + obtain ⟨-, rfl⟩ := (decode_iff _ _).mp hd + simp only [packetBytes, List.length_append, NtpProofs.headerBytes_length] at h + omega + +/-- The first octet of an accepted packet is LI, VN and Mode. -/ +theorem first_octet (x : UInt8) (b : List UInt8) (p : Packet) (hd : Ntp.decode (x :: b) = some p) : + x.toNat = p.header.leap * 64 + p.header.version * 8 + p.header.mode ∧ + HeaderValid p.header := by + obtain ⟨⟨hv, -⟩, he⟩ := (decode_iff _ _).mp hd + have hv' := hv + obtain ⟨hl, hver, hm1, hm2, -⟩ := hv' + unfold packetBytes headerBytes at he + simp only [List.append_assoc, List.cons_append, List.cons.injEq] at he + have := congrArg UInt8.toNat he.1 + rw [NtpProofs.toNat_toUInt8 _ (by omega)] at this + exact ⟨this, hv⟩ + +theorem decode_version (x : UInt8) (b : List UInt8) + (h : x.toNat / 8 % 8 ≠ 3 ∧ x.toNat / 8 % 8 ≠ 4) : Ntp.decode (x :: b) = none := by + cases hd : Ntp.decode (x :: b) with + | none => rfl + | some p => + obtain ⟨hx, hl, hver, hm1, hm2, -⟩ := first_octet x b p hd + omega + +theorem decode_mode (x : UInt8) (b : List UInt8) + (h : x.toNat % 8 = 0 ∨ x.toNat % 8 = 6 ∨ x.toNat % 8 = 7) : Ntp.decode (x :: b) = none := by + cases hd : Ntp.decode (x :: b) with + | none => rfl + | some p => + obtain ⟨hx, hl, hver, hm1, hm2, -⟩ := first_octet x b p hd + omega + +theorem decode_ext_overrun (h : List UInt8) (es : List ExtField) (type len : Nat) + (r : List UInt8) (hh : h.length = 48) (hes : ∀ e ∈ es, ExtValid e) (hlen : len < 2 ^ 16) + (hr : 24 < 4 + r.length) (hover : 4 + r.length < len) : + Ntp.decode (h ++ es.flatMap extBytes ++ beBytes 2 type ++ beBytes 2 len ++ r) = none := by + unfold Ntp.decode + cases hh' : Ntp.readHeader (h ++ es.flatMap extBytes ++ beBytes 2 type ++ beBytes 2 len ++ r) + with + | none => rfl + | some ht => + obtain ⟨hd, t⟩ := ht + obtain ⟨-, he⟩ := NtpProofs.readHeader_some _ _ _ hh' + have ht : t = es.flatMap extBytes ++ (beBytes 2 type ++ beBytes 2 len ++ r) := by + simp only [List.append_assoc] at he ⊢ + exact (List.append_inj he (by rw [hh, NtpProofs.headerBytes_length])).2.symm + subst ht + simp only [bind, Option.bind_some] + have hlong : 24 < (es.flatMap extBytes ++ (beBytes 2 type ++ beBytes 2 len ++ r)).length := by + simp only [List.length_append, NtpProofs.beBytes_length] + omega + unfold Ntp.readTail + rw [ite_eq_right (fun h0 => by rw [h0] at hlong; simp at hlong)] + split + · rw [NtpProofs.readFields_overrun es type len r _ hes hlen hr hover] + rfl + · rw [ite_eq_right (fun hm => by rw [NtpProofs.macLength_iff] at hm; omega)] + rfl + +theorem toUnix_zero (era : Int) : Ntp.toUnix era 0 = none := + NtpProofs.toUnix_zero era + +theorem toUnix_spec (era : Int) (raw : Nat) (s : Int) (f : Nat) : + Ntp.toUnix era raw = some (s, f) ↔ + raw ≠ 0 ∧ raw < 2 ^ 64 ∧ f < 2 ^ 32 ∧ + s * 2 ^ 32 + f = era * 2 ^ 64 + raw - unixEpoch * 2 ^ 32 := + NtpProofs.toUnix_spec era raw s f + +theorem fromUnix_toUnix (era : Int) (raw : Nat) (s : Int) (f : Nat) + (h : Ntp.toUnix era raw = some (s, f)) : Ntp.fromUnix s f = some (era, raw) := + NtpProofs.fromUnix_toUnix era raw s f h + +theorem toUnix_fromUnix (s : Int) (f : Nat) (era : Int) (raw : Nat) + (h : Ntp.fromUnix s f = some (era, raw)) : Ntp.toUnix era raw = some (s, f) := + NtpProofs.toUnix_fromUnix s f era raw h + +theorem examples : + Ntp.decode clientRequest = some { header := clientHeader, exts := [], mac := [] } ∧ + Ntp.decode serverReply = some { header := serverHeader, exts := [], mac := [] } ∧ + Ntp.decode ntsReply = + some { header := ntsHeader, + exts := [{ type := 0x0104, body := (ntsReply.drop 52).take 32 }, + { type := 0x0404, body := ntsReply.drop 88 }], + mac := [] } ∧ + Ntp.decode withMac = + some { header := clientHeader, + exts := [{ type := 0x0104, body := field16.drop 4 }, + { type := 0x0204, body := field20.drop 4 }], + mac := mac20 } ∧ + Ntp.decode cryptoNak = some { header := serverHeader, exts := [], mac := [0, 0, 0, 0] } ∧ + Ntp.decode v3WithMac = + some { header := { clientHeader with version := 3 }, exts := [], mac := mac20 } ∧ + Ntp.decode (clientRequest ++ tail24) = + some { header := clientHeader, exts := [], mac := tail24 } ∧ + Ntp.decode (clientRequest ++ autokeyAssoc ++ mac20) = + some { header := clientHeader, exts := [{ type := 0x0102, body := autokeyAssoc.drop 4 }], + mac := mac20 } ∧ + Ntp.encode { header := serverHeader, exts := [], mac := [] } = serverReply ∧ + Ntp.toUnix 0 0xE5F663A8798EAE2B = some (1649141032, 0x798EAE2B) ∧ + Ntp.fromUnix 1649141032 0x798EAE2B = some (0, 0xE5F663A8798EAE2B) ∧ + Ntp.toUnix 0 (2208988800 * 2 ^ 32) = some (0, 0) ∧ + Ntp.toUnix 0 0xFFFFFFFF00000000 = some (2085978495, 0) ∧ + Ntp.toUnix 0 0x0000000100000000 = some (-2208988799, 0) ∧ + Ntp.toUnix 1 0x0000000100000000 = some (2085978497, 0) ∧ + Ntp.toUnix 1 0 = none ∧ + Ntp.fromUnix 2085978496 0 = none ∧ + Ntp.fromUnix 2085978496 1 = some (1, 1) ∧ + Ntp.toUnix (-1) 0xFFFFFFFF00000000 = some (-2208988801, 0) := by + decide +kernel + +theorem rejects : + (Ntp.decode serverReply).isSome ∧ + Ntp.decode (0x14 :: serverReply.tail) = none ∧ + Ntp.decode (0x2C :: serverReply.tail) = none ∧ + Ntp.decode (0x20 :: serverReply.tail) = none ∧ + Ntp.decode (0x26 :: serverReply.tail) = none ∧ + Ntp.decode (0x27 :: serverReply.tail) = none ∧ + Ntp.decode (serverReply.take 47) = none ∧ + (Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x10] ++ List.replicate 12 0x5A ++ + field28)).isSome ∧ + Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x0C] ++ List.replicate 8 0x5A ++ field28) = + none ∧ + (Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x20] ++ List.replicate 28 0x5A)).isSome ∧ + Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x1E] ++ List.replicate 26 0x5A) = none ∧ + Ntp.decode (clientRequest ++ [0x01, 0x04, 0x01, 0x00] ++ List.replicate 28 0x5A) = none ∧ + (Ntp.decode (clientRequest ++ field16 ++ field28)).isSome ∧ + Ntp.decode (clientRequest ++ field28 ++ field16) = none ∧ + (Ntp.decode cryptoNak).isSome ∧ + Ntp.decode (serverReply ++ [0x00, 0x00, 0x00, 0x01]) = none ∧ + (Ntp.decode (serverReply ++ field28)).isSome ∧ + Ntp.decode (serverReply ++ field28 ++ [0, 0, 0, 0]) = none ∧ + (Ntp.decode (clientRequest ++ field28)).isSome ∧ + Ntp.decode ((0x1B :: clientRequest.tail) ++ field28) = none ∧ + (Ntp.decode (serverReply ++ mac20)).isSome ∧ + Ntp.decode (serverReply ++ mac20 ++ [0x00]) = none := by + decide +kernel + +end Spec diff --git a/Proofs/Packet.lean b/Proofs/Packet.lean new file mode 100644 index 0000000..1891db7 --- /dev/null +++ b/Proofs/Packet.lean @@ -0,0 +1,56 @@ +import Proofs.Header +import Proofs.Tail + +/-! + The decoder and the encoder against `Spec.packetBytes` and + `Spec.Valid`. +-/ + +namespace NtpProofs + +open Spec Ntp + +theorem decode_some (b : List UInt8) (p : Packet) (H : decode b = some p) : + Valid p ∧ b = packetBytes p := by + unfold decode at H + simp only [bind, Option.bind_eq_some_iff, Prod.exists, pure, Option.some.injEq] at H + obtain ⟨h, t, hh, es, mac, ht, rfl⟩ := H + obtain ⟨hv, rfl⟩ := readHeader_some _ _ _ hh + obtain ⟨hes, h3, hmac, hlast, rfl⟩ := readTail_some _ _ _ _ ht + refine ⟨⟨hv, hes, fun h' => h3 (by dsimp only at h'; omega), hmac, hlast⟩, ?_⟩ + simp [packetBytes, List.append_assoc] + +theorem decode_bytes (p : Packet) (hp : Valid p) : decode (packetBytes p) = some p := by + obtain ⟨hv, hes, h3, hmac, hlast⟩ := hp + unfold decode packetBytes + rw [List.append_assoc, readHeader_bytes _ hv] + simp only [bind, Option.bind_some] + rw [readTail_bytes _ _ _ hv.2.1 h3 hes hmac hlast] + rfl + +theorem decode_iff (b : List UInt8) (p : Packet) : + decode b = some p ↔ Valid p ∧ b = packetBytes p := by + constructor + · exact decode_some b p + · rintro ⟨hp, rfl⟩ + exact decode_bytes p hp + +theorem writeField_eq (e : ExtField) : writeField e = extBytes e := by + simp only [writeField, extBytes, writeBE_eq, ExtField.length] + +theorem ofSigned_eq (z : Int) (h1 : -128 ≤ z) (h2 : z < 128) : + ofSigned z = (z % 256).toNat := by + unfold ofSigned + split <;> omega + +theorem encode_eq (p : Packet) (hp : Valid p) : encode p = packetBytes p := by + obtain ⟨⟨hl, hver, hm1, hm2, hst, hp1, hp2, hq1, hq2, -⟩, -⟩ := hp + have hlvm : p.header.leap * 64 + p.header.version * 8 + p.header.mode < 256 := by omega + have hs : ∀ z : Int, -128 ≤ z → z < 128 → (z % 256).toNat < 256 := fun z _ _ => by omega + have hw : writeField = extBytes := funext writeField_eq + unfold encode packetBytes headerBytes signedByte + simp only [writeBE_eq, ofSigned_eq _ hp1 hp2, ofSigned_eq _ hq1 hq2, beBytes_one _ hlvm, + beBytes_one _ hst, beBytes_one _ (hs _ hp1 hp2), beBytes_one _ (hs _ hq1 hq2), hw, + List.append_assoc, List.cons_append, List.nil_append] + +end NtpProofs diff --git a/Proofs/Tail.lean b/Proofs/Tail.lean new file mode 100644 index 0000000..3c9a16c --- /dev/null +++ b/Proofs/Tail.lean @@ -0,0 +1,252 @@ +import Proofs.Bytes + +/-! + The octets after the header: extension fields and the MAC, against + `Spec.extBytes`, `Spec.ExtValid` and `Spec.MacValid`. +-/ + +namespace NtpProofs + +open Spec Ntp + +theorem longer_iff (n : Nat) (t : List UInt8) : longer n t = true ↔ n < t.length := by + induction t generalizing n with + | nil => simp [longer] + | cons x t ih => + cases n with + | zero => simp [longer] + | succ n => simp [longer, ih] + +theorem macLength_iff (t : List UInt8) : + macLength t = true ↔ t.length = 0 ∨ t.length = 20 ∨ t.length = 24 := by + simp [macLength, or_assoc] + +theorem extBytes_length (e : ExtField) : (extBytes e).length = e.length := by + simp [extBytes, beBytes_length, ExtField.length] + omega + +theorem flatMap_extBytes_length (es : List ExtField) (h : ∀ e ∈ es, ExtValid e) : + 16 * es.length ≤ (es.flatMap extBytes).length := by + induction es with + | nil => simp + | cons e es ih => + simp only [List.flatMap_cons, List.length_append, List.length_cons, extBytes_length] + have he := (h e (by simp)).2.1 + have := ih (fun x hx => h x (by simp [hx])) + omega + +/-- What `readFields` gives back: valid fields, then a MAC of 0, 20 or + 24 octets, with a last field of at least 28 octets when there is no + MAC, and the fields and the MAC are the octets it read. -/ +theorem readFields_some (fuel : Nat) (t : List UInt8) (es : List ExtField) (mac : List UInt8) + (H : readFields fuel t = some (es, mac)) : + (∀ e ∈ es, ExtValid e) ∧ (mac = [] ∨ mac.length = 20 ∨ mac.length = 24) ∧ + (mac = [] → ∀ e ∈ es.getLast?, 28 ≤ e.length) ∧ t = es.flatMap extBytes ++ mac := by + induction fuel generalizing t es mac with + | zero => simp [readFields] at H + | succ fuel ih => + unfold readFields at H + split at H + · rename_i hlong + simp only [bind, Option.bind_eq_some_iff, Prod.exists, readBE_iff, guard_some, take?_eq, + pure, Option.some.injEq, Prod.mk.injEq, Bool.and_eq_true, beq_iff_eq, + decide_eq_true_eq, Nat.reducePow, exists_and_left, exists_const] at H + obtain ⟨type, r, ⟨htype, rfl⟩, len, r, ⟨hlen, rfl⟩, ⟨h16, h4⟩, body, r, ⟨hbody, rfl⟩, + fields, mac', hrec, rfl, rfl⟩ := H + obtain ⟨hv, hmac, hlast, rfl⟩ := ih _ _ _ hrec + have hel : ({ type, body } : ExtField).length = len := by + simp only [ExtField.length]; omega + refine ⟨?_, hmac, ?_, ?_⟩ + · intro e he + simp only [List.mem_cons] at he + rcases he with rfl | he + · exact ⟨htype, by omega, by omega, by omega⟩ + · exact hv e he + · intro hm e he + cases fields with + | nil => + subst hm + simp only [List.getLast?_singleton, Option.mem_def, Option.some.injEq] at he + subst he + rw [longer_iff] at hlong + simp only [List.flatMap_nil, List.append_nil, List.length_append, beBytes_length] + at hlong + omega + | cons f fs => + rw [List.getLast?_cons_cons] at he + exact hlast hm e he + · simp only [List.flatMap_cons, extBytes, hel, List.append_assoc] + · split at H + · rename_i hm + simp only [Option.some.injEq, Prod.mk.injEq] at H + obtain ⟨rfl, rfl⟩ := H + rw [macLength_iff] at hm + refine ⟨by simp, ?_, by simp, by simp⟩ + rcases hm with h | h | h + · exact Or.inl (List.length_eq_zero_iff.mp h) + · exact Or.inr (Or.inl h) + · exact Or.inr (Or.inr h) + · simp at H + +/-- `readFields` reads back the octets of valid fields and a MAC of 0, + 20 or 24 octets, given enough fuel. -/ +theorem readFields_bytes (es : List ExtField) (mac : List UInt8) (fuel : Nat) + (hv : ∀ e ∈ es, ExtValid e) (hmac : mac = [] ∨ mac.length = 20 ∨ mac.length = 24) + (hlast : mac = [] → ∀ e ∈ es.getLast?, 28 ≤ e.length) + (hfuel : (es.flatMap extBytes ++ mac).length < fuel) : + readFields fuel (es.flatMap extBytes ++ mac) = some (es, mac) := by + induction es generalizing fuel with + | nil => + obtain ⟨fuel, rfl⟩ : ∃ k, fuel = k + 1 := ⟨fuel - 1, by omega⟩ + have hm : mac.length = 0 ∨ mac.length = 20 ∨ mac.length = 24 := by + rcases hmac with rfl | h | h <;> simp_all + unfold readFields + simp only [List.flatMap_nil, List.nil_append] + rw [ite_eq_right (by rw [longer_iff]; omega), ite_eq_left ((macLength_iff _).mpr hm)] + | cons e es ih => + obtain ⟨fuel, rfl⟩ : ∃ k, fuel = k + 1 := ⟨fuel - 1, by omega⟩ + have he := hv e (by simp) + have he16 : 16 ≤ e.length := he.2.1 + have hes : ∀ x ∈ es, ExtValid x := fun x hx => hv x (by simp [hx]) + have hrest := flatMap_extBytes_length es hes + simp only [List.flatMap_cons, List.length_append, extBytes_length] at hfuel + have hlong : 24 < (extBytes e ++ (es.flatMap extBytes ++ mac)).length := by + simp only [List.length_append, extBytes_length] + cases es with + | nil => + rcases hmac with rfl | h | h + · have := hlast rfl e (by simp) + omega + · omega + · omega + | cons f fs => + simp only [List.length_cons] at hrest + omega + have hrec := ih fuel hes + (by + intro hm x hx + cases es with + | nil => simp at hx + | cons f fs => + exact hlast hm x (by rw [List.getLast?_cons_cons]; exact hx)) + (by simp only [List.length_append] at hfuel ⊢; omega) + unfold readFields + simp only [List.flatMap_cons, List.append_assoc] + rw [ite_eq_left ((longer_iff _ _).mpr (by simpa [List.append_assoc] using hlong))] + simp only [extBytes, List.append_assoc, bind, readBE_append 2 _ _ (by simpa using he.1), + Option.bind_some, readBE_append 2 _ _ (by simpa using he.2.2.2)] + have hg : (decide (16 ≤ e.length) && e.length % 4 == 0) = true := by + simp only [Bool.and_eq_true, decide_eq_true_eq, beq_iff_eq] + exact ⟨he.2.1, he.2.2.1⟩ + have hb : e.length - 4 = e.body.length := by simp only [ExtField.length]; omega + simp only [hg, guard, ite_true, Option.bind_some, hb, take?_append _ _ _ rfl, hrec, pure] + +/-- A field whose Length runs past the end, after valid fields, makes + `readFields` refuse. -/ +theorem readFields_overrun (es : List ExtField) (type len : Nat) (r : List UInt8) (fuel : Nat) + (hv : ∀ e ∈ es, ExtValid e) (hlen : len < 2 ^ 16) (hr : 24 < 4 + r.length) + (hover : 4 + r.length < len) : + readFields fuel (es.flatMap extBytes ++ (beBytes 2 type ++ beBytes 2 len ++ r)) = none := by + induction es generalizing fuel with + | nil => + cases fuel with + | zero => rfl + | succ fuel => + unfold readFields + simp only [List.flatMap_nil, List.nil_append] + rw [ite_eq_left ((longer_iff _ _).mpr (by simp [beBytes_length]; omega))] + simp only [bind, List.append_assoc, readBE_append_mod, Option.bind_some, + readBE_append 2 _ _ (by simpa using hlen)] + rw [take?_none _ _ (by omega)] + cases (guard (decide (16 ≤ len) && len % 4 == 0) : Option Unit) <;> rfl + | cons e es ih => + cases fuel with + | zero => rfl + | succ fuel => + have he := hv e (by simp) + have hes : ∀ x ∈ es, ExtValid x := fun x hx => hv x (by simp [hx]) + unfold readFields + simp only [List.flatMap_cons, List.append_assoc] + rw [ite_eq_left ((longer_iff _ _).mpr (by + simp only [List.length_append, beBytes_length, extBytes_length]; omega))] + simp only [extBytes, List.append_assoc, bind, readBE_append 2 _ _ (by simpa using he.1), + Option.bind_some, readBE_append 2 _ _ (by simpa using he.2.2.2)] + have hg : (decide (16 ≤ e.length) && e.length % 4 == 0) = true := by + simp only [Bool.and_eq_true, decide_eq_true_eq, beq_iff_eq] + exact ⟨he.2.1, he.2.2.1⟩ + have hb : e.length - 4 = e.body.length := by simp only [ExtField.length]; omega + have := ih fuel hes + simp only [List.append_assoc] at this + simp only [hg, guard, ite_true, Option.bind_some, hb, take?_append _ _ _ rfl, this] + rfl + +/-- What `readTail` gives back. -/ +theorem readTail_some (v : Nat) (t : List UInt8) (es : List ExtField) (mac : List UInt8) + (H : readTail v t = some (es, mac)) : + (∀ e ∈ es, ExtValid e) ∧ (v ≠ 4 → es = []) ∧ MacValid es mac ∧ + (mac = [] → ∀ e ∈ es.getLast?, 28 ≤ e.length) ∧ t = es.flatMap extBytes ++ mac := by + unfold readTail at H + split at H + · rename_i h0 + simp only [Option.some.injEq, Prod.mk.injEq] at H + obtain ⟨rfl, rfl⟩ := H + subst h0 + exact ⟨by simp, fun _ => rfl, Or.inr (Or.inr (Or.inr ⟨rfl, rfl⟩)), by simp, by simp⟩ + · split at H + · rename_i hv4 + obtain ⟨hv, hmac, hlast, rfl⟩ := readFields_some _ _ _ _ H + have hm : MacValid es mac := by + unfold MacValid + rcases hmac with h | h | h + · exact Or.inl h + · exact Or.inr (Or.inl h) + · exact Or.inr (Or.inr (Or.inl h)) + exact ⟨hv, fun h => absurd hv4 h, hm, hlast, rfl⟩ + · split at H + · rename_i hm + simp only [Option.some.injEq, Prod.mk.injEq] at H + obtain ⟨rfl, rfl⟩ := H + rw [macLength_iff] at hm + refine ⟨by simp, fun _ => rfl, ?_, by simp, by simp⟩ + unfold MacValid + rcases hm with h | h | h + · exact Or.inl (List.length_eq_zero_iff.mp h) + · exact Or.inr (Or.inl h) + · exact Or.inr (Or.inr (Or.inl h)) + · simp at H + +/-- `readTail` reads back the octets of fields and a MAC that the rules + allow. -/ +theorem readTail_bytes (v : Nat) (es : List ExtField) (mac : List UInt8) + (hver : v = 3 ∨ v = 4) (h3 : v = 3 → es = []) (hv : ∀ e ∈ es, ExtValid e) + (hmac : MacValid es mac) (hlast : mac = [] → ∀ e ∈ es.getLast?, 28 ≤ e.length) : + readTail v (es.flatMap extBytes ++ mac) = some (es, mac) := by + unfold readTail + split + · rename_i h0 + -- Four octets hold no field, so they are the MAC. + cases es with + | nil => + simp only [List.flatMap_nil, List.nil_append] at h0 ⊢ + | cons e es => + have he := (hv e (by simp)).2.1 + have := congrArg List.length h0 + simp only [List.flatMap_cons, List.length_append, extBytes_length] at this + simp at this + omega + · rename_i h0 + have hmac' : mac = [] ∨ mac.length = 20 ∨ mac.length = 24 := by + rcases hmac with h | h | h | ⟨rfl, rfl⟩ + · exact Or.inl h + · exact Or.inr (Or.inl h) + · exact Or.inr (Or.inr h) + · simp at h0 + split + · exact readFields_bytes es mac _ hv hmac' hlast (by omega) + · rename_i hv4 + have hes : es = [] := h3 (by omega) + subst hes + simp only [List.flatMap_nil, List.nil_append] + rw [ite_eq_left ((macLength_iff _).mpr (by rcases hmac' with rfl | h | h <;> simp_all))] + +end NtpProofs diff --git a/Proofs/Time.lean b/Proofs/Time.lean new file mode 100644 index 0000000..ad2ee85 --- /dev/null +++ b/Proofs/Time.lean @@ -0,0 +1,60 @@ +import Ntp + +/-! + `toUnix` and `fromUnix`. +-/ + +namespace NtpProofs + +open Spec Ntp + +theorem toUnix_zero (era : Int) : toUnix era 0 = none := by + simp [toUnix] + +theorem toUnix_spec (era : Int) (raw : Nat) (s : Int) (f : Nat) : + toUnix era raw = some (s, f) ↔ + raw ≠ 0 ∧ raw < 2 ^ 64 ∧ f < 2 ^ 32 ∧ + s * 2 ^ 32 + f = era * 2 ^ 64 + raw - unixEpoch * 2 ^ 32 := by + unfold toUnix unixEpoch + simp only [Nat.reducePow, Int.reducePow] + split + · rename_i h + simp only [reduceCtorEq, false_iff] + omega + · rename_i h + simp only [Option.some.injEq, Prod.mk.injEq] + omega + +theorem fromUnix_toUnix (era : Int) (raw : Nat) (s : Int) (f : Nat) + (h : toUnix era raw = some (s, f)) : fromUnix s f = some (era, raw) := by + unfold toUnix at h + split at h + · simp at h + · rename_i hr + simp only [Option.some.injEq, Prod.mk.injEq] at h + obtain ⟨rfl, rfl⟩ := h + unfold fromUnix + rw [ite_eq_right (by omega)] + dsimp only + rw [ite_eq_right (by omega)] + simp only [Option.some.injEq, Prod.mk.injEq] + omega + +theorem toUnix_fromUnix (s : Int) (f : Nat) (era : Int) (raw : Nat) + (h : fromUnix s f = some (era, raw)) : toUnix era raw = some (s, f) := by + unfold fromUnix at h + split at h + · simp at h + · rename_i hf + dsimp only at h + split at h + · simp at h + · rename_i hr + simp only [Option.some.injEq, Prod.mk.injEq] at h + obtain ⟨rfl, rfl⟩ := h + unfold toUnix + rw [ite_eq_right (by omega)] + simp only [Option.some.injEq, Prod.mk.injEq] + omega + +end NtpProofs diff --git a/README.md b/README.md index fba35fb..f3b52e0 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,106 @@ -# ntp +# ntp: reading the NTP packet, proved -A Lean parser for the NTP wire packet. This is a Proven Commons project: it rebuilds a widely used format in Lean and proves the new code follows its written specification. +NTP, the Network Time Protocol, sets the clocks of computers across the internet, and its packets come from anyone who can send a UDP datagram to port 123. A program that reads them takes lengths from the packet and follows them: extension fields carry their own Length, and a MAC may or may not follow them. ntpd has followed lengths from the packet that it did not check, such as the Autokey value length of CVE-2014-9750, which could leak its memory, and readers still disagree on what follows the header of a version 3 packet. -Status: not verified. No code has landed yet. Plan issue 1 holds the first slice, and it is unclaimed. The specification has not been approved by a reviewer, so do not call this verified. +This library reads and writes one NTP packet, the whole of one UDP payload, in Lean: -## First slice +- the 48-octet header of RFC 5905 (Figure 8): leap indicator, version, mode, stratum, poll, precision, root delay, root dispersion, reference ID and the four timestamps; +- the extension fields after it (RFC 7822), each as its Field Type and its octets, and the MAC at the end, told apart by the rule RFC 7822 sets for that; +- timestamps, converted to Unix time and back given their era (RFC 5905 section 6). -The NTP packet header, 48 bytes, plus extension fields parsed as length framed items. Fields are leap indicator, version, mode, stratum, poll, precision, root delay, root dispersion, reference id, and four timestamps. Timestamps convert to seconds since the epoch, with era handling written in the plan. +Machine-checked proofs show: -This is distinct from the rfc repo, which parses RFC 3339 text timestamps. NTP is a binary wire format, not a text format. +- **The reader accepts exactly the packets of the specification, and gives back what they hold.** It accepts a payload exactly when it is the octets of a packet that `Spec/Defs.lean` writes down from RFC 5905 and RFC 7822, and gives that packet. So it reads each payload one way only. +- **The header length and each extension field's Length are checked.** A payload shorter than the header is refused, and so is an extension field whose Length runs past the end of the packet, wherever it starts with more than 24 octets left (24 or fewer octets left are read as the MAC). So are versions other than 3 and 4, and modes 0, 6 and 7. +- **Writing and reading agree.** The writer writes each valid packet as the RFCs lay it out, and reading what it wrote gives the packet back. +- **Timestamp conversion is exact.** A timestamp in a given era converts to the exact Unix time, and back. The timestamp 0, which means unknown, converts to nothing in every era, never to 1900. -## Main claim +It is tested against a model of the RFCs, and against the packet readers of ntpd-rs, NTPsec, Wireshark and ntplib; `tests/TEST-RESULTS.md` holds the record. It is a project of [Proven Commons](https://github.com/Proven-Commons/mission), built to the verified bar published there. -Decoding a packet gives the same timestamps back. A packet shorter than its header is rejected. An extension field that runs past the end of the packet is rejected. A version outside the supported set is rejected. +This reads and writes NTP packets. It does not keep time: it is not an NTP client or server, and it does not check MACs. -## Test oracles +Status: in progress. The specification has not yet been approved by a reviewer apart from its author, so this is not verified. -- ntpd and chrony. -- Python ntplib. -- Wireshark captures. +## Use it -## Score +From Lean, on `List UInt8`, with the structures of `Spec/Defs.lean`: -16 out of 18, first pass score in mission issue 19, the round 4 sheet. Created at the org owner's direction on that sheet, on the judgement that the slice, claim, and oracles are sound and that it duplicates no existing repo. +- `Ntp.decode : List UInt8 → Option Packet`: the packet in one UDP payload: its header, its extension fields and its MAC. +- `Ntp.encode : Packet → List UInt8`: the octets of a packet. +- `Ntp.toUnix : Int → Nat → Option (Int × Nat)`: the Unix time of a 64-bit timestamp in an era, as whole seconds and a fraction in units of 2^-32 seconds. +- `Ntp.fromUnix : Int → Nat → Option (Int × Nat)`: the era and the 64-bit timestamp of a Unix time. -## What is not in this slice +`SPEC.md` says what each function does. -- Clock discipline. -- Peer selection. -- Network Time Security. +## What is proved -## Layout, when the first slice lands +The statements are in `Spec.lean`. The definitions they use are in `Spec/Defs.lean`, written from RFC 5905 (Figure 8 and sections 6 and 7.3) and RFC 7822 (section 3), and the proofs are in `Proofs/`. -The layout follows the Proven Commons template in the mission repo, under bar/template. Spec.lean holds the statements a reviewer approves. Proofs.lean holds the proofs under the same names. Main.lean holds the command line tool used by the tests. The bar check runs in CI on every change. +| Claim | Theorem | +|-------|---------| +| The decoder accepts exactly the octets of a valid packet, and gives that packet. | `Spec.decode_iff` | +| The encoder writes every valid packet as RFC 5905 Figure 8 and RFC 7822 Figure 14 lay it out. | `Spec.encode_eq` | +| Encoding a packet and decoding the octets gives the packet back exactly when it is valid. | `Spec.decode_encode` | +| Encoding a decoded packet gives back the octets it was read from. | `Spec.encode_decode` | +| A payload shorter than the 48-octet header is refused. | `Spec.decode_short` | +| A packet whose version is not 3 or 4 is refused. | `Spec.decode_version` | +| A packet of mode 0, 6 or 7 is refused. | `Spec.decode_mode` | +| An extension field whose Length runs past the end of the packet is refused, wherever it starts with more than 24 octets left (24 or fewer are read as the MAC), and whatever the rest of the packet holds. | `Spec.decode_ext_overrun` | +| The timestamp 0 converts to no Unix time, in any era. | `Spec.toUnix_zero` | +| A timestamp in an era converts to exactly its Unix time: `era * 2^32` plus its seconds and fraction, less 2,208,988,800 seconds. | `Spec.toUnix_spec` | +| Converting a timestamp to Unix time and back gives it back, and so does converting a Unix time to a timestamp and back. | `Spec.fromUnix_toUnix`, `Spec.toUnix_fromUnix` | +| A client request and a server reply captured on a network, read field by field; a captured reply with two extension fields; packets with a MAC, a crypto-NAK, and version 3; an Autokey field whose Value Length runs past the field, given back as it is; the end of era 0 in 2036, worked by hand. Fourteen packets that each break one rule are refused, each after a packet that differs from it only there and is accepted. | `Spec.examples`, `Spec.rejects` | + +The packets this reader accepts are those of version 3 or 4 and mode 1 to 5, whose extension fields each have a Length that is a multiple of 4 from 16 to 65532, whose MAC is none, 20 or 24 octets, or four zero octets (the crypto-NAK) with no extension field, whose last extension field is at least 28 octets when there is no MAC, and which, in version 3, have no extension field. `SPEC.md` gives the RFC text for each rule, says which rules are this reader's own choice where the RFCs leave one open, such as what may follow the header of a version 3 packet, and says where chrony, ntpd, ntpd-rs, NTPsec and Wireshark choose differently. + +## What is not proved + +- **The MAC.** It is given as its octets. No digest is computed or checked. +- **What extension fields hold.** Autokey, Network Time Security and every other field are given as their Field Type and octets. Lengths inside a field, such as the Autokey value length of CVE-2014-9750, are not read. +- **The meaning of the header.** The reference ID, root delay and root dispersion are given as their bits, and nothing checks that the timestamps fit together. +- **Which era a timestamp is in.** The caller gives it, from its own clock. Leap seconds are outside the conversion. +- **The protocol.** No on-wire exchange, clock filter, peer selection or clock discipline. Modes 6 and 7 are refused, not read. +- **Encoding a packet that is not valid.** What the encoder writes for one is not specified. +- **Time and memory.** No proof bounds them. +- **Reading requests and printing answers in `Main.lean`.** The differential tests cover them. + +## What you must trust + +- The Lean kernel and the five external kernels shipped with Lean, which all check the proofs. +- `lake comparator`, which builds the statements and the proofs in a sandbox, exports them, and checks that each proof proves exactly its statement. +- bubblewrap and the Linux kernel's namespaces, which keep the untrusted build from tampering with that check. +- The Lean compiler, with the C compiler and linker shipped with it, and the runtime, which turn the proved definitions into the program that runs. +- Lean's core library, including the compiled code it puts in place of some definitions the proofs reason about, such as natural number and integer arithmetic, `UInt8`, and lists. +- The operating system and hardware it runs on. +- For the tests only: the model in `tests/run_tests.py`, and the other readers of the packet that the record names. + +This project has no dependencies beyond Lean itself. + +## Re-check it + +Needs Linux, [elan](https://github.com/leanprover/elan), bubblewrap (`apt install bubblewrap`) and Python 3. The tests also use, when they are installed at the versions the record names, Rust's `cargo` (to build ntpd-rs's packet parser), NTPsec's Python module (`apt install python3-ntp`), tshark (`apt install tshark`) and ntplib (`pip install ntplib==0.4.0`); one that is missing is left out, and the record says so. + +``` +lake build +lake comparator --paranoid +python3 tests/run_tests.py +python3 tests/plant_bugs.py +``` + +`lake comparator --paranoid` builds the statements and the proofs in a sandbox. It checks that each theorem in `Proofs/` proves exactly the statement in `Spec.lean`, using only the axioms `propext`, `Classical.choice`, and `Quot.sound`, and replays it through every kernel shipped with Lean. + +`tests/run_tests.py` sends the tool packets and timestamps from a recorded seed and checks every answer against a model of RFC 5905 and RFC 7822 written in Python. It reads the same packets with the other readers, and counts each difference under the choice of that reader's that makes it, such as ntpd-rs taking any 4 to 24 octets after the fields as a MAC, or NTPsec reading Poll as unsigned; the model, with those choices, gives each reader's answer. `tests/plant_bugs.py` plants one-line bugs in the code and checks that the tests catch each one. + +## Layout + +- `Ntp/Packet.lean` and `Ntp/Time.lean`: the code under proof. +- `Spec.lean`: the statements, with `sorry` for proofs. A reviewer approves this file. +- `Spec/Defs.lean`: the definitions the statements use, written from the RFCs. +- `Spec/Examples.lean`: the inputs of the examples: three captured packets, and packets written by hand from them. +- `Proofs.lean` and `Proofs/`: the proofs. +- `Main.lean`: the command-line tool the tests drive. +- `lakefile.toml`: the build. It names the package and its version, its libraries and executable, and the default targets, and sets nothing else. +- `comparator.json`: which theorems `lake comparator` checks, and which axioms it allows. +- `bar.toml`: the record for the bar check: status, authors, approvals, and where things are. +- `tests/`: the differential tests, the planted bugs, the program that reads packets with ntpd-rs's parser (`tests/ntpd-rs`), and the vectors of pull request 3. +- `SPEC.md`: the specification in plain words. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..bcdfae9 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,153 @@ +# Specification: ntp + +NTP, the Network Time Protocol, sends its time in one UDP packet: RFC 5905 ("Network Time Protocol Version 4: Protocol and Algorithms Specification") gives the packet, and RFC 7822 ("Network Time Protocol Version 4 (NTPv4) Extension Fields") updates its section 7.5 on extension fields. This library reads and writes one packet, the whole of one UDP payload: + +- the 48-octet header (RFC 5905 Figure 8 and section 7.3); +- the extension fields after it (RFC 7822 section 3, Figure 14), each as its Field Type and its octets; +- the MAC at the end, as its octets. + +It also converts a 64-bit timestamp to Unix time and back, given the era, which is not in the packet (RFC 5905 section 6). + +It does not check a MAC, read what an extension field holds, or run any part of the protocol: no clock filter, peer selection or clock discipline, no Autokey (RFC 5906) and no Network Time Security (RFC 8915). "Not in this slice" below lists what else it leaves out. + +A reviewer who did not write this specification approves it. The approval also covers `Spec.lean`, `Spec/Defs.lean`, `Spec/Examples.lean`, and the other files that `bar.toml` lists. + +## The header + +`Spec.Header` has the fields of Figure 8, in its order, and `Spec.headerBytes` writes them as their 48 octets, every multi-octet field big-endian: + +| Octets | Field | Kept as | +|---|---|---| +| 0, top 2 bits | LI, the "2-bit integer warning of an impending leap second" (Figure 9) | 0 to 3 | +| 0, next 3 bits | VN, the "3-bit integer representing the NTP version number" | 0 to 7 | +| 0, low 3 bits | Mode, the "3-bit integer representing the mode" (Figure 10) | 0 to 7 | +| 1 | Stratum, an "8-bit integer" (Figure 11) | 0 to 255 | +| 2 | Poll, an "8-bit signed integer", in log2 seconds | -128 to 127 | +| 3 | Precision, an "8-bit signed integer", in log2 seconds | -128 to 127 | +| 4 to 7 | Root Delay, in "NTP short format" | its 32 bits, as a number | +| 8 to 11 | Root Dispersion, in "NTP short format" | its 32 bits, as a number | +| 12 to 15 | Reference ID, a "32-bit code" | its 32 bits, as a number, the first octet most significant | +| 16 to 23 | Reference Timestamp, in "NTP timestamp format" | its 64 bits, as a number | +| 24 to 31 | Origin Timestamp | its 64 bits | +| 32 to 39 | Receive Timestamp | its 64 bits | +| 40 to 47 | Transmit Timestamp | its 64 bits | + +Poll and Precision are two's complement. The short format is 16 bits of seconds and 16 of fraction, and the timestamp format 32 bits of seconds and 32 of fraction (section 6); this library keeps each as the number its bits hold and does not divide it. + +`Spec.HeaderValid` says which headers this reader accepts: + +- every field fits in its bits; +- the version is 3 or 4. VN is "currently 4", and the header "fields are backwards compatible with NTPv3" (RFC 5905 7.3); NTPv3 is RFC 1305. Versions 1 and 2 are refused, and so are 0, 5, 6 and 7; +- the mode is 1 to 5: symmetric active, symmetric passive, client, server and broadcast. Mode 0 is "reserved", mode 6 is "NTP control message" and mode 7 is "reserved for private use" (Figure 10). The packets of modes 6 and 7 are not in the format of Figure 8, so they are refused. + +Any leap indicator, stratum, poll and precision is accepted, including stratum 0 (the kiss-o'-death packet of section 7.4) and the "reserved" strata 17 to 255. + +## Extension fields and the MAC + +After the header come "optional extension fields and finally an optional message authentication code (MAC) consisting of the Key Identifier field and Message Digest field" (RFC 5905 7.3). + +**Extension fields.** `Spec.ExtField` is a field of Figure 14: its 16-bit Field Type, then its Value and its Padding, kept together, since where the Value ends is "specific to the defined function". `Spec.extBytes` writes it as the Field Type, the Length, then those octets. The Length "indicates the length of the entire extension field in octets, including the Padding field", so it counts the four octets of Field Type and Length too. `Spec.ExtValid` says which fields this reader accepts, from RFC 7822 section 3: + +- the Length is a multiple of 4: "All extension fields are zero-padded to a word (four octets) boundary"; +- the Length is at least 16: "the minimum field length containing required fields is four words (16 octets)"; +- the Length is at most 65532: "the maximum field length cannot be longer than 65532 octets, due to the maximum size of the Length field". + +Any Field Type is accepted, and the Value and Padding are given as they are. RFC 7822 says a host "SHOULD ignore" a field of unknown type; which types to act on is for the program using this library. The Padding is not checked to be zero, since this library cannot tell where it starts. + +**The MAC.** `Spec.MacValid` says which MACs this reader accepts: + +- none; +- 20 or 24 octets: the 32-bit Key Identifier, then a 128-bit digest (RFC 5905 7.3) or a 160-bit one. "This field can be either 24 octets long, 20 octets long, or a 4-octet crypto-NAK" (RFC 7822 section 1), and "A MAC MUST NOT be longer than 24 octets if there is no extension field present, unless a longer MAC is agreed upon by both client and server" (7.5.1.3); +- the crypto-NAK: "a MAC consisting of four octets of zeros" (RFC 5905 9.2), "the key identifier with value zero" (A.5), in a packet with no extension field. + +These are the MACs of NTPv4. This reader takes the same ones in version 3, which is its own choice: RFC 1305 gives version 3 a 12-octet authenticator, which this reader refuses (see "Choices where readers differ"). + +**`Spec.Valid`** says which packets this reader accepts: + +- the header is valid, each extension field is valid, and the MAC is one of the above; +- a version 3 packet has no extension fields. RFC 7822 starts its section 7.5 with "In NTPv4, one or more extension fields can be inserted after the header and before the MAC". This too is this reader's choice, where readers differ (see "Choices where readers differ"); +- without a MAC, the last extension field is at least 28 octets: "If the packet includes a single extension field, the length of the extension field MUST be at least 7 words, i.e., at least 28 octets. If the packet includes more than one extension field, the length of the last extension field MUST be at least 28 octets. The length of the other extension fields in this case MUST be at least 16 octets each" (7.5.1.4). + +`Spec.packetBytes` writes a packet as its header, its extension fields in order, then its MAC. + +### Reading the octets after the header + +The rules above are the ones that let a receiver tell the last extension field from a MAC without knowing the MAC's length beforehand. A reader can follow them in one pass, as chrony 4.6.1 does for version 4 (`parse_packet` in `ntp_core.c`), and this library reads the octets after the header so: + +1. Four zero octets are a crypto-NAK. +2. In version 3, no octets, or 20 or 24 octets, are the MAC. Anything else is refused. +3. In version 4, while more than 24 octets are left, a field starts there: a Field Type, then a Length that must be a multiple of 4, at least 16, and no more than the octets left. When 24 or fewer octets are left, they must be none, or 20 or 24, the MAC. Anything else is refused. + +This accepts exactly the valid packets, and reads each one way only. With a MAC of 20 or 24 octets, every field leaves more than 24 octets before it is read, so each is read as a field, and then the MAC is what is left. Without a MAC, the last field is all that is left when it is read, so it is more than 24 octets, a multiple of 4, and so at least 28. A field can never be read as part of the MAC, nor the MAC as a field. `Spec.decode_iff` proves that the reader accepts exactly the octets `Spec.packetBytes` writes for a valid packet. + +So 24 or fewer octets after the header are never an extension field. In particular, 24 octets that begin like a field of Length 24 are a MAC. A sender that put that field in a packet without a MAC broke 7.5.1.4, and this reader reads it as the MAC. + +### Choices where readers differ + +The rules above are this reader's. Other readers of the packet choose differently in places, and where the RFCs leave a choice open, this section says so. chrony 4.6.1 and ntpd 4.2.8p18 are described from their source, which was read but not run: `parse_packet` in chrony's `ntp_core.c`, and `receive` and `valid_NAK` in ntpd's `ntp_proto.c`, with `MIN_MAC_LEN` and `MAX_MAC_LEN` in its `ntp.h`. ntpd-rs, NTPsec and Wireshark were run by the tests, and `tests/TEST-RESULTS.md` counts each of their differences under the choice that makes it. + +- **Versions 1 and 2** are refused. chrony and ntpd accept them, and NTPsec and Wireshark read every version. +- **Version 3 with octets after the header.** Here the RFCs do not give this reader its rule; the rule is its own choice. RFC 1305, NTPv3, gives an authenticator of "96 bits, including a 32-bit key identifier and 64-bit crypto-checksum" (12 octets, for DES), and lets "additional information" go "between the (padded) NTP message and the key identifier". The crypto-NAK (RFC 5905 9.2) and the MACs of 20 and 24 octets (RFC 5905 7.3, RFC 7822 section 1) are NTPv4's; RFC 5905 says only that its symmetric key scheme "has been carried over from NTPv3". This reader reads version 3 with no extension fields, which RFC 7822 gives to NTPv4, and with the MACs of version 4: none, the crypto-NAK, 20 or 24 octets. So it refuses RFC 1305's 12-octet authenticator, and every other tail. The other readers: + - chrony takes any whole number of words after a version 3 header as the MAC, including the 72-octet extended authenticator of MS-SNTP; + - ntpd takes 4 to 24 octets, in steps of 4, as the MAC ("if 3 [words], the packet is authenticated with DES"), and walks a longer tail as extension fields, as in version 4; + - ntpd-rs takes any 4 to 24 octets as the MAC; + - NTPsec reads version 3 as version 4, extension fields too. +- **MAC lengths in version 4.** After the extension fields, this reader takes none, 20 or 24 octets as the MAC, following RFC 7822 (sections 1 and 7.5.1.3, and 7.5.1.4, which makes a last field without a MAC at least 28 octets). So does chrony: it takes 20 to 24 octets, but first refuses a packet whose length is not a multiple of 4. A MAC longer than 24 octets is refused, unless its octets happen to read as fields and a MAC; a length agreed by other means is not in the packet, so this reader cannot use it. The other readers: + - ntpd takes any 4 to 24 octets left after the fields, in steps of 4, as the MAC (`MIN_MAC_LEN` is one word and `MAX_MAC_LEN` six): 8, 12 (DES) and 16 octets too, and 4 octets after a field. Such a MAC then fails to authenticate without a matching key, but the packet is not dropped as malformed. So ntpd reads a field of 28 octets and 16 octets after it as a field and a MAC, where this reader refuses them (`Spec.rejects`); + - ntpd-rs takes any 4 to 24 octets as the MAC, whatever they hold, 21 to 23 too; + - NTPsec takes 4, 20 or 24 octets as the MAC (any 4 octets as a crypto-NAK, after fields too), refuses the packet with 8, 12 or 16, and with any other number of octets left reads the packet and passes over those octets without a word; + - Wireshark takes as the MAC whatever its own split leaves (below). +- **The crypto-NAK** is four zero octets, in a packet with no extension field, of any mode from 1 to 5. chrony requires the same. The pseudocode of RFC 5905 A.5.1 treats any MAC of four octets as a crypto-NAK; four octets that are not zero are refused here, and by ntpd (`valid_NAK`), while NTPsec and ntpd-rs take them. ntpd also drops four zero octets in a packet whose mode is not 1, 2 or 4 ("Only server responses can contain NAK's"), and checks a crypto-NAK against its own state: a peer with a key, and the origin timestamp of its request. This reader takes a crypto-NAK in a client request (mode 3) and a broadcast (mode 5) too: whether it means anything there is for the protocol, which this library does not run. +- **The least extension field.** RFC 7822 sets the least Length at 16 (section 3), and this reader and chrony follow it. RFC 5906 section 10 allows Autokey fields of 8 octets. ntpd takes any Length of 4 or more that is a multiple of 4 and fits, where more than 24 octets are left, and ntpd-rs's least Length is 4 too, so both read fields of Length 4, 8 and 12, which this reader refuses. +- **NTPsec** reads an extension field as a 32-bit type and a 32-bit length of the value after them, with no padding or minimum, cut short at the end of the packet, not as RFC 7822 Figure 14 lays it out; so it takes a field that runs past the end of the packet. It also reads Poll as unsigned, where RFC 5905 7.3 says "signed". +- **Wireshark** splits the octets after the header its own way, in every version: fields while 16 or more octets are left, each of a Length of 16 or more that fits; then, when 1 to 19 octets are left that are not a crypto-NAK, it gives the last field back to the MAC; whatever is left is the MAC, of any length. +- **No largest packet.** Any number of octets is read. chrony's packet buffer holds at most 1024 octets of extension fields and a MAC (`NTP_MAX_EXTENSIONS_LENGTH` in `ntp.h`). + +## Timestamps + +A timestamp is 32 bits of seconds and 32 bits of fraction from the start of its era. "The prime epoch, or base date of era 0, is 0 h 1 January 1900 UTC", and to find the time from an era and a timestamp, "s = era * 2^(32) + timestamp" (RFC 5905 section 6). The era is not in the packet: a program knows it from its own clock. So the conversion takes the era as an argument, and it may be any integer, negative for dates before 1900. + +**`Ntp.toUnix era raw`** gives the Unix time of the 64-bit timestamp `raw` in era `era`: the seconds from 1900, `era * 2^32 + raw / 2^32`, less the 2,208,988,800 seconds from 1900 to 1970 (Figure 4: "1 Jan 1970 ... 2,208,988,800 First day UNIX"), plus the fraction `raw mod 2^32` in units of 2^-32 seconds. It gives the whole seconds, which may be negative, and the fraction, below 2^32. It is exact. It gives nothing for a raw value of 0: "a value of zero is a special case representing unknown or unsynchronized time" (section 6), in every era. A value of more than 64 bits gives nothing either. + +**`Ntp.fromUnix seconds fraction`** gives the era and the 64-bit timestamp of a Unix time, by the rule of section 6, "era = s / 2^(32) and timestamp = s - era * 2^(32), which works for positive and negative dates", with the division rounding down. It gives nothing for a fraction of 2^32 or more. The first instant of each era, such as 1900-01-01 00:00:00 UTC and 2036-02-07 06:28:16 UTC, would have the timestamp 0, which means unknown, so it gives nothing for them either. + +Neither NTP timestamps nor Unix time count leap seconds, so the offset between them is the fixed one of Figure 4. What happens during a leap second is outside this library. + +## What must always hold + +Each property is a theorem in `Spec.lean`, under the name given. + +| Property | Theorem | +|----------|---------| +| The decoder accepts exactly the octets of a valid packet, and gives that packet: its header fields, its extension fields and its MAC. So it reads each accepted payload one way only. | `Spec.decode_iff` | +| The encoder writes every valid packet as `Spec.packetBytes` lays it out. | `Spec.encode_eq` | +| Encoding a packet and decoding the octets gives the packet back exactly when it is valid. | `Spec.decode_encode` | +| Encoding a decoded packet gives back the octets it was read from. | `Spec.encode_decode` | +| A payload shorter than 48 octets is refused. | `Spec.decode_short` | +| A packet of a version other than 3 or 4 is refused. | `Spec.decode_version` | +| A packet of mode 0, 6 or 7 is refused. | `Spec.decode_mode` | +| An extension field whose Length runs past the end of the packet is refused, wherever it starts with more than 24 octets left, and whatever the rest of the packet holds. (24 or fewer octets left are never a field: they are read as the MAC, or refused.) | `Spec.decode_ext_overrun` | +| The timestamp 0 has no Unix time, in any era. | `Spec.toUnix_zero` | +| The Unix time of a timestamp in an era is exactly `era * 2^32 + raw / 2^32 - 2208988800` seconds, given as whole seconds and a fraction in units of 2^-32 seconds. | `Spec.toUnix_spec` | +| Converting a timestamp to Unix time and back gives the era and the timestamp back, and converting a Unix time to a timestamp and back gives the Unix time back. | `Spec.fromUnix_toUnix`, `Spec.toUnix_fromUnix` | +| Examples: a client request and a server reply captured on a network, read field by field; a captured Network Time Security reply with two extension fields; packets with two fields and a MAC, a crypto-NAK, a version 3 packet with a MAC, and 24 octets read as a MAC; an Autokey field whose Value Length runs past the field, read by its own Length and given back as it is; the Unix time of a captured timestamp; the end of era 0 in 2036 and the end of era -1 in 1899. | `Spec.examples` | +| Fourteen packets that each break one rule are refused, and each comes after a packet that differs from it only there and is accepted: versions 2 and 5; modes 0, 6 and 7; 47 octets; a field of Length 12; a Length not a multiple of 4; a Length past the end; a last field of 16 octets without a MAC; four octets after the header that are not zero; a field followed by a crypto-NAK; a field in a version 3 packet; 21 octets after the header. | `Spec.rejects` | + +`Spec.decode_short`, `Spec.decode_version`, `Spec.decode_mode` and `Spec.decode_ext_overrun` say what is refused, and a decoder that refuses everything meets them. `Spec.decode_iff`, `Spec.examples` and the accepted packets of `Spec.rejects` rule that decoder out. `tests/plant_bugs.py` plants a reader without each rule that `Spec.rejects` breaks, and the tests catch each one (`tests/PLANTED-BUGS.md`). The same holds for `Spec.toUnix_zero`, which `Spec.toUnix_spec` pairs, and for `Spec.toUnix_fromUnix`, which `Spec.fromUnix_toUnix` pairs. Since `Spec.decode_iff` is exact, it fixes what the decoder gives for every input; `Spec.toUnix_spec` does the same for `Ntp.toUnix`, and the two round trips together for `Ntp.fromUnix`. + +**CVE-2014-9750.** ntpd before 4.2.8p1, with Autokey enabled, could leak its memory or crash on "a packet containing an extension field with an invalid value for the length of its value field" (ntp bug 2671: "vallen is not validated"): it followed a length from the packet without checking it against what holds it. That length, the Value Length inside an Autokey field (RFC 5906 Figure 7), is part of the field's octets, which this library gives as they are and does not read. `Spec.examples` reads a packet whose Autokey field of 28 octets has a Value Length of 0xFFFFFFFF: the field is read by its own Length, and its octets come back unchanged. A program that reads the Value Length must check it against the field. The same flaw at the level this library reads would be a field's own Length that runs past the end of the packet, and `Spec.decode_ext_overrun` proves that such a field is refused. + +## Not in this slice + +- **The MAC.** It is given as its octets; no digest is computed or checked, and no key is looked up. +- **What extension fields hold.** Autokey (RFC 5906), Network Time Security (RFC 8915) and every other field type are given as their octets, and lengths inside them are not read. +- **The meaning of header fields.** The Reference ID is a number, whether it is a kiss code, an IPv4 address or part of a hash; the root delay and dispersion are their bits; nothing checks that the timestamps fit together. +- **Modes 6 and 7.** Their formats are other than Figure 8, and they are refused. +- **Which era a timestamp is in.** The program using this library gives it. +- **Leap seconds**, and time zones. +- **The protocol.** No on-wire exchange, clock filter, selection, clustering or discipline. +- **Encoding a packet that is not valid.** What `Ntp.encode` writes for one is not specified. `Spec.decode_encode` shows how to tell: decoding what it wrote gives the packet back exactly when it was valid. + +## The command-line tool + +`Main.lean` has one mode. `ntp serve` answers requests from the differential tests, one per line: decode a packet, encode one, and convert a timestamp to Unix time and back. The header comment of `Main.lean` gives the protocol. Every answer comes from the functions above. diff --git a/Spec.lean b/Spec.lean new file mode 100644 index 0000000..d45fbde --- /dev/null +++ b/Spec.lean @@ -0,0 +1,203 @@ +import Ntp +import Spec.Defs +import Spec.Examples + +/-! + The statements a reviewer approves. Each `sorry` is proved under the + same name in `Proofs/`, and `lake comparator` checks that each proof + proves exactly the statement here. + + `Spec/Defs.lean` writes the packet down from RFC 5905 and RFC 7822. + The code is in `Ntp/`. +-/ + +namespace Spec + +/-- The decoder accepts exactly the octets of a valid packet, and gives + that packet. So each accepted input is read one way only, and every + field it gives is the field the octets hold. -/ +theorem decode_iff (b : List UInt8) (p : Packet) : + Ntp.decode b = some p ↔ Valid p ∧ b = packetBytes p := by + sorry + +/-- The encoder writes every valid packet as RFC 5905 Figure 8 and RFC + 7822 Figure 14 lay it out. -/ +theorem encode_eq (p : Packet) (hp : Valid p) : Ntp.encode p = packetBytes p := by + sorry + +/-- Encoding a packet and decoding the octets gives the packet back + exactly when it is valid. -/ +theorem decode_encode (p : Packet) : Ntp.decode (Ntp.encode p) = some p ↔ Valid p := by + sorry + +/-- Encoding a decoded packet gives back the octets it was read from. -/ +theorem encode_decode (b : List UInt8) (p : Packet) (h : Ntp.decode b = some p) : + Ntp.encode p = b := by + sorry + +/-- A payload shorter than the 48-octet header is refused, whatever it + holds. -/ +theorem decode_short (b : List UInt8) (h : b.length < 48) : Ntp.decode b = none := by + sorry + +/-- A packet whose version (VN, the three bits after the two of LI at + the start of the first octet: `x / 8 % 8`) is not 3 or 4 is refused, + whatever follows. -/ +theorem decode_version (x : UInt8) (b : List UInt8) + (h : x.toNat / 8 % 8 ≠ 3 ∧ x.toNat / 8 % 8 ≠ 4) : Ntp.decode (x :: b) = none := by + sorry + +/-- A packet whose mode, the low three bits of the first octet, is 0 + (reserved), 6 (control message) or 7 (private use) is refused, + whatever follows. -/ +theorem decode_mode (x : UInt8) (b : List UInt8) + (h : x.toNat % 8 = 0 ∨ x.toNat % 8 = 6 ∨ x.toNat % 8 = 7) : Ntp.decode (x :: b) = none := by + sorry + +/-- An extension field whose Length runs past the end of the packet is + refused: after any 48 octets and any valid extension fields, where + more than 24 octets are left (so a field starts there, not the MAC), + a field whose Length is more than the octets left makes the whole + packet refused, whatever the other octets hold. A reader that reads + past the end, or cuts the field short at the end, accepts some of + these. -/ +theorem decode_ext_overrun (h : List UInt8) (es : List ExtField) (type len : Nat) + (r : List UInt8) (hh : h.length = 48) (hes : ∀ e ∈ es, ExtValid e) (hlen : len < 2 ^ 16) + (hr : 24 < 4 + r.length) (hover : 4 + r.length < len) : + Ntp.decode (h ++ es.flatMap extBytes ++ beBytes 2 type ++ beBytes 2 len ++ r) = none := by + sorry + +/-- A raw timestamp of 0 means unknown in every era: "a value of zero is + a special case representing unknown or unsynchronized time" (RFC 5905 + section 6). It has no Unix time. -/ +theorem toUnix_zero (era : Int) : Ntp.toUnix era 0 = none := by + sorry + +/-- The exact Unix time of a timestamp. In era `era`, the nonzero 64-bit + timestamp `raw` is `era * 2^32 + raw / 2^32` seconds from 1900 + (RFC 5905 section 6: "s = era * 2^(32) + timestamp", the low 32 bits + being the fraction), so `era * 2^32 + raw / 2^32 - 2208988800` + seconds from 1970. `toUnix` gives it as whole seconds `s` and a + fraction `f` below 2^32, in units of 2^-32 seconds: exactly when + `s + f / 2^32` is that number, multiplied out below by 2^32. A raw + value of 0, or of more than 64 bits, gives `none`. -/ +theorem toUnix_spec (era : Int) (raw : Nat) (s : Int) (f : Nat) : + Ntp.toUnix era raw = some (s, f) ↔ + raw ≠ 0 ∧ raw < 2 ^ 64 ∧ f < 2 ^ 32 ∧ + s * 2 ^ 32 + f = era * 2 ^ 64 + raw - unixEpoch * 2 ^ 32 := by + sorry + +/-- `fromUnix` gives back the era and timestamp of every Unix time that + `toUnix` gives. -/ +theorem fromUnix_toUnix (era : Int) (raw : Nat) (s : Int) (f : Nat) + (h : Ntp.toUnix era raw = some (s, f)) : Ntp.fromUnix s f = some (era, raw) := by + sorry + +/-- What `fromUnix` gives, `toUnix` takes back to the same Unix time. -/ +theorem toUnix_fromUnix (s : Int) (f : Nat) (era : Int) (raw : Nat) + (h : Ntp.fromUnix s f = some (era, raw)) : Ntp.toUnix era raw = some (s, f) := by + sorry + +/-- Worked by hand: + - the captured client request and server reply, read field by field, + with no extension field and no MAC; + - a captured Network Time Security reply with two extension fields + and no MAC; + - the client request with fields of 16 and 20 octets and a 20-octet + MAC; + - a crypto-NAK, and a version 3 packet with a MAC; + - 24 octets after the header that begin like a field of Length 24: + they are the MAC; + - an Autokey field whose Value Length, 0xFFFFFFFF, runs far past the + field, as in CVE-2014-9750, then a MAC: the field is read by its + own Length, and its octets come back as they are; + - the transmit timestamp of the reply, 2022-04-05 06:43:52 UTC, and + the Unix epoch; + - the end of era 0 in 2036: the last whole second of era 0, then the + raw value 2^32 in era 0 (in 1900) and in era 1 (in 2036), the raw + value 0 in era 1, which means unknown, and so no timestamp for + 2036-02-07 06:28:16 UTC, the first instant of era 1; + - the last second of era -1, in 1899. -/ +theorem examples : + Ntp.decode clientRequest = some { header := clientHeader, exts := [], mac := [] } ∧ + Ntp.decode serverReply = some { header := serverHeader, exts := [], mac := [] } ∧ + Ntp.decode ntsReply = + some { header := ntsHeader, + exts := [{ type := 0x0104, body := (ntsReply.drop 52).take 32 }, + { type := 0x0404, body := ntsReply.drop 88 }], + mac := [] } ∧ + Ntp.decode withMac = + some { header := clientHeader, + exts := [{ type := 0x0104, body := field16.drop 4 }, + { type := 0x0204, body := field20.drop 4 }], + mac := mac20 } ∧ + Ntp.decode cryptoNak = some { header := serverHeader, exts := [], mac := [0, 0, 0, 0] } ∧ + Ntp.decode v3WithMac = + some { header := { clientHeader with version := 3 }, exts := [], mac := mac20 } ∧ + Ntp.decode (clientRequest ++ tail24) = + some { header := clientHeader, exts := [], mac := tail24 } ∧ + Ntp.decode (clientRequest ++ autokeyAssoc ++ mac20) = + some { header := clientHeader, exts := [{ type := 0x0102, body := autokeyAssoc.drop 4 }], + mac := mac20 } ∧ + Ntp.encode { header := serverHeader, exts := [], mac := [] } = serverReply ∧ + Ntp.toUnix 0 0xE5F663A8798EAE2B = some (1649141032, 0x798EAE2B) ∧ + Ntp.fromUnix 1649141032 0x798EAE2B = some (0, 0xE5F663A8798EAE2B) ∧ + Ntp.toUnix 0 (2208988800 * 2 ^ 32) = some (0, 0) ∧ + Ntp.toUnix 0 0xFFFFFFFF00000000 = some (2085978495, 0) ∧ + Ntp.toUnix 0 0x0000000100000000 = some (-2208988799, 0) ∧ + Ntp.toUnix 1 0x0000000100000000 = some (2085978497, 0) ∧ + Ntp.toUnix 1 0 = none ∧ + Ntp.fromUnix 2085978496 0 = none ∧ + Ntp.fromUnix 2085978496 1 = some (1, 1) ∧ + Ntp.toUnix (-1) 0xFFFFFFFF00000000 = some (-2208988801, 0) := by + sorry + +/-- Packets that each break one rule, each after a packet that differs + from it only there and is accepted: + - versions 2 and 5, and modes 0, 6 and 7, in the server reply, which + is accepted; + - the server reply cut to 47 octets; + - a version 4 field of Length 12 (less than the 16 of RFC 7822), + then a field of 28; accepted with a first field of Length 16; + - a last field of Length 30, not a multiple of 4; accepted with + Length 32; + - a field of Length 256 with 32 octets left, a Length that runs past + the end of the packet (CVE-2014-9750's flaw, at the level of the + field's own Length); accepted with Length 32; + - without a MAC, a last field of 16 octets after one of 28 (RFC 7822 + 7.5.1.4 asks for 28); accepted the other way round; + - four octets after the header that are not zero; accepted as four + zero octets, the crypto-NAK; + - a field, then four zero octets: the crypto-NAK has no fields; + accepted without the four octets; + - a version 3 packet with a field of 28 octets; accepted in version + 4; + - 21 octets after the header; accepted as the 20-octet MAC. -/ +theorem rejects : + (Ntp.decode serverReply).isSome ∧ + Ntp.decode (0x14 :: serverReply.tail) = none ∧ + Ntp.decode (0x2C :: serverReply.tail) = none ∧ + Ntp.decode (0x20 :: serverReply.tail) = none ∧ + Ntp.decode (0x26 :: serverReply.tail) = none ∧ + Ntp.decode (0x27 :: serverReply.tail) = none ∧ + Ntp.decode (serverReply.take 47) = none ∧ + (Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x10] ++ List.replicate 12 0x5A ++ + field28)).isSome ∧ + Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x0C] ++ List.replicate 8 0x5A ++ field28) = + none ∧ + (Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x20] ++ List.replicate 28 0x5A)).isSome ∧ + Ntp.decode (clientRequest ++ [0x01, 0x04, 0x00, 0x1E] ++ List.replicate 26 0x5A) = none ∧ + Ntp.decode (clientRequest ++ [0x01, 0x04, 0x01, 0x00] ++ List.replicate 28 0x5A) = none ∧ + (Ntp.decode (clientRequest ++ field16 ++ field28)).isSome ∧ + Ntp.decode (clientRequest ++ field28 ++ field16) = none ∧ + (Ntp.decode cryptoNak).isSome ∧ + Ntp.decode (serverReply ++ [0x00, 0x00, 0x00, 0x01]) = none ∧ + (Ntp.decode (serverReply ++ field28)).isSome ∧ + Ntp.decode (serverReply ++ field28 ++ [0, 0, 0, 0]) = none ∧ + (Ntp.decode (clientRequest ++ field28)).isSome ∧ + Ntp.decode ((0x1B :: clientRequest.tail) ++ field28) = none ∧ + (Ntp.decode (serverReply ++ mac20)).isSome ∧ + Ntp.decode (serverReply ++ mac20 ++ [0x00]) = none := by + sorry + +end Spec diff --git a/Spec/Defs.lean b/Spec/Defs.lean new file mode 100644 index 0000000..b361ca8 --- /dev/null +++ b/Spec/Defs.lean @@ -0,0 +1,185 @@ +/- + The NTP packet, written from RFC 5905 ("Network Time Protocol Version + 4: Protocol and Algorithms Specification") as RFC 7822 ("NTPv4 + Extension Fields") updates it: the 48-octet header (5905 Figure 8 and + section 7.3), extension fields (7822 section 3, Figure 14), the MAC, + and timestamps (5905 section 6). + + Bytes are `List UInt8`, one UDP payload. Every multi-octet field is + big-endian: the packet is "32-bit (4 octet) words in network byte + order" (RFC 5905 7.3). This file uses nothing from the code. +-/ + +namespace Spec + +/-! ## Integers -/ + +/-- `n` as `w` bytes, most significant first. -/ +def beBytes (w n : Nat) : List UInt8 := + (List.range w).reverse.map fun i => (n / 256 ^ i % 256).toUInt8 + +/-- An 8-bit signed integer as its byte, in two's complement. Poll and + Precision are each an "8-bit signed integer" (RFC 5905 7.3). -/ +def signedByte (z : Int) : UInt8 := (z % 256).toNat.toUInt8 + +/-! ## The header (RFC 5905 Figure 8, section 7.3) -/ + +/-- The fields of the header, in the order of Figure 8: + - `leap`, LI: the "2-bit integer warning of an impending leap + second" (Figure 9); + - `version`, VN: the "3-bit integer representing the NTP version + number"; + - `mode`: the "3-bit integer representing the mode" (Figure 10); + - `stratum`: an "8-bit integer" (Figure 11); + - `poll` and `precision`: each an "8-bit signed integer", in log2 + seconds; + - `rootDelay` and `rootDispersion`: each in "NTP short format", 16 + bits of seconds and 16 of fraction (section 6), kept as its 32 + bits; + - `refId`: the "32-bit code identifying the particular server or + reference clock", kept as its 32 bits, the first octet most + significant; + - the Reference, Origin, Receive and Transmit Timestamps: each in + "NTP timestamp format", 32 bits of seconds and 32 of fraction + (section 6), kept as its 64 bits. -/ +structure Header where + leap : Nat + version : Nat + mode : Nat + stratum : Nat + poll : Int + precision : Int + rootDelay : Nat + rootDispersion : Nat + refId : Nat + reference : Nat + origin : Nat + receive : Nat + transmit : Nat + deriving DecidableEq, Repr + +/-- The header as its 48 octets: LI, VN and Mode packed into the first + octet, two bits, three bits and three bits from the most significant + end; then Stratum, Poll and Precision; then Root Delay, Root + Dispersion and Reference ID, 32 bits each; then the four 64-bit + timestamps. -/ +def headerBytes (h : Header) : List UInt8 := + [(h.leap * 64 + h.version * 8 + h.mode).toUInt8, h.stratum.toUInt8, + signedByte h.poll, signedByte h.precision] ++ + beBytes 4 h.rootDelay ++ beBytes 4 h.rootDispersion ++ beBytes 4 h.refId ++ + beBytes 8 h.reference ++ beBytes 8 h.origin ++ beBytes 8 h.receive ++ beBytes 8 h.transmit + +/-- The headers this reader accepts: + - every field small enough for its bits; + - version 3 or 4 (VN is "currently 4", and the header "fields are + backwards compatible with NTPv3", 5905 section 7.3; NTPv3 is RFC + 1305); + - mode 1 to 5. Mode 0 is "reserved", mode 6 is "NTP control + message" and mode 7 is "reserved for private use" (Figure 10): + their packets are not this format. -/ +def HeaderValid (h : Header) : Prop := + h.leap < 4 ∧ (h.version = 3 ∨ h.version = 4) ∧ 1 ≤ h.mode ∧ h.mode ≤ 5 ∧ + h.stratum < 256 ∧ -128 ≤ h.poll ∧ h.poll < 128 ∧ -128 ≤ h.precision ∧ h.precision < 128 ∧ + h.rootDelay < 2 ^ 32 ∧ h.rootDispersion < 2 ^ 32 ∧ h.refId < 2 ^ 32 ∧ + h.reference < 2 ^ 64 ∧ h.origin < 2 ^ 64 ∧ h.receive < 2 ^ 64 ∧ h.transmit < 2 ^ 64 + +/-! ## Extension fields (RFC 7822 section 3, Figure 14) -/ + +/-- An extension field: its Field Type, and the octets after its Length + field, which are its Value and its Padding. Where the Value ends and + the Padding starts is "specific to the defined function", so they + are kept together. -/ +structure ExtField where + type : Nat + body : List UInt8 + deriving DecidableEq, Repr + +/-- The Length field: "the length of the entire extension field in + octets, including the Padding field", so the four octets of Field + Type and Length as well. -/ +def ExtField.length (e : ExtField) : Nat := e.body.length + 4 + +/-- The field as its octets: the 16-bit Field Type, the 16-bit Length, + then the Value and Padding. -/ +def extBytes (e : ExtField) : List UInt8 := + beBytes 2 e.type ++ beBytes 2 e.length ++ e.body + +/-- An extension field this reader accepts: "All extension fields are + zero-padded to a word (four octets) boundary", so the Length is a + multiple of 4; "the minimum field length containing required fields + is four words (16 octets)"; and "the maximum field length cannot be + longer than 65532 octets, due to the maximum size of the Length + field", which with the first rule is a Length below 2^16. -/ +def ExtValid (e : ExtField) : Prop := + e.type < 2 ^ 16 ∧ 16 ≤ e.length ∧ e.length % 4 = 0 ∧ e.length < 2 ^ 16 + +/-! ## The packet -/ + +/-- A packet: the header, the extension fields in order, and the MAC, + which is empty when the packet has none. -/ +structure Packet where + header : Header + exts : List ExtField + mac : List UInt8 + deriving DecidableEq, Repr + +/-- The packet as its octets: "The NTP packet header shown in Figure 8 + has 12 words followed by optional extension fields and finally an + optional message authentication code (MAC)" (5905 7.3). -/ +def packetBytes (p : Packet) : List UInt8 := + headerBytes p.header ++ p.exts.flatMap extBytes ++ p.mac + +/-- The MACs this reader accepts: + - none; + - a 20- or 24-octet MAC: the 32-bit Key Identifier, then a 128-bit + digest (5905 7.3) or a 160-bit one. RFC 7822 7.5.1.3: "A MAC MUST + NOT be longer than 24 octets if there is no extension field + present"; this reader takes no longer MAC with extension fields + either, since a MAC length agreed by other means is not in the + packet; + - the crypto-NAK, "a MAC consisting of four octets of zeros" (5905 + section 9.2), "the key identifier with value zero" (5905 A.5), in a + packet with no extension field. + + These are the MACs of NTPv4, and this reader takes the same ones in + version 3. That is its choice: RFC 1305, NTPv3, gives a 96-bit + authenticator, a 32-bit key identifier and a 64-bit DES checksum (12 + octets), which this reader refuses. `SPEC.md` gives what other + readers take. -/ +def MacValid (exts : List ExtField) (mac : List UInt8) : Prop := + mac = [] ∨ mac.length = 20 ∨ mac.length = 24 ∨ (mac = [0, 0, 0, 0] ∧ exts = []) + +/-- The packets this reader accepts: + - the header is valid; + - each extension field is valid; + - a version 3 packet has no extension fields: "In NTPv4, one or more + extension fields can be inserted after the header and before the + MAC, if a MAC is present" (7822 section 3). This is this reader's + choice, where readers differ: RFC 1305 lets other data go before + the key identifier of its authenticator, and ntpd walks a version + 3 tail of more than 24 octets as extension fields; + - the MAC is one of `MacValid`; + - without a MAC, the last extension field is at least 28 octets: + "If the packet includes a single extension field, the length of + the extension field MUST be at least 7 words, i.e., at least 28 + octets. If the packet includes more than one extension field, the + length of the last extension field MUST be at least 28 octets" + (7822 7.5.1.4). + + These rules make the octets after the header read one way only: see + `SPEC.md`. -/ +def Valid (p : Packet) : Prop := + HeaderValid p.header ∧ + (∀ e ∈ p.exts, ExtValid e) ∧ + (p.header.version = 3 → p.exts = []) ∧ + MacValid p.exts p.mac ∧ + (p.mac = [] → ∀ e ∈ p.exts.getLast?, 28 ≤ e.length) + +/-! ## Timestamps (RFC 5905 section 6) -/ + +/-- Seconds from the prime epoch, 0 h 1 January 1900 UTC, to the Unix + epoch: "1 Jan 1970 ... 0 ... 2,208,988,800 First day UNIX" (Figure 4: + era 0, era offset 2,208,988,800). -/ +def unixEpoch : Nat := 2208988800 + +end Spec diff --git a/Spec/Examples.lean b/Spec/Examples.lean new file mode 100644 index 0000000..2b54165 --- /dev/null +++ b/Spec/Examples.lean @@ -0,0 +1,131 @@ +import Spec.Defs + +/-! + The inputs of `Spec.examples` and `Spec.rejects`. + + The first three packets were captured on a network by the ntpd-rs + project, which keeps them as test vectors in + `ntp-proto/src/packet/mod.rs` (`test_captured_client`, + `test_captured_server`, `test_nts_captured_server`). ntpd-rs is + released under the Apache License 2.0 or the MIT license, at the + user's choice; these byte strings keep that notice. The others are + written by hand from them. +-/ + +namespace Spec + +/-- A client request, as captured: version 4, mode 3 (client). -/ +def clientRequest : List UInt8 := + [0x23, 0x02, 0x06, 0xE8, 0x00, 0x00, 0x03, 0xFF, 0x00, 0x00, 0x03, 0x7D, 0x5E, 0xC6, 0x9F, 0x0F, + 0xE5, 0xF6, 0x62, 0x98, 0x7B, 0x61, 0xB9, 0xAF, 0xE5, 0xF6, 0x63, 0x66, 0x7B, 0x64, 0x99, 0x5D, + 0xE5, 0xF6, 0x63, 0x66, 0x81, 0x40, 0x55, 0x90, 0xE5, 0xF6, 0x63, 0xA8, 0x76, 0x1D, 0xDE, 0x48] + +/-- Its header, field by field: no leap warning, version 4, client, + stratum 2, poll 2^6 seconds, precision 2^-24 seconds, root delay + 0x03FF/65536 seconds, root dispersion 0x037D/65536 seconds, reference + ID 94.198.159.15, and the four timestamps. -/ +def clientHeader : Header := + { leap := 0, version := 4, mode := 3, stratum := 2, poll := 6, precision := -24, + rootDelay := 0x000003FF, rootDispersion := 0x0000037D, refId := 0x5EC69F0F, + reference := 0xE5F662987B61B9AF, origin := 0xE5F663667B64995D, + receive := 0xE5F6636681405590, transmit := 0xE5F663A8761DDE48 } + +/-- A server reply, as captured: version 4, mode 4 (server). -/ +def serverReply : List UInt8 := + [0x24, 0x02, 0x06, 0xE9, 0x00, 0x00, 0x02, 0x36, 0x00, 0x00, 0x03, 0xB7, 0xC0, 0x35, 0x67, 0x6C, + 0xE5, 0xF6, 0x61, 0xFD, 0x6F, 0x16, 0x5F, 0x03, 0xE5, 0xF6, 0x63, 0xA8, 0x76, 0x19, 0xEF, 0x40, + 0xE5, 0xF6, 0x63, 0xA8, 0x79, 0x8C, 0x65, 0x81, 0xE5, 0xF6, 0x63, 0xA8, 0x79, 0x8E, 0xAE, 0x2B] + +/-- Its header: server, stratum 2, precision 2^-23 seconds, reference ID + 192.53.103.108, and a transmit timestamp of 0xE5F663A8 seconds, + which is 2022-04-05 06:43:52 UTC in era 0. -/ +def serverHeader : Header := + { leap := 0, version := 4, mode := 4, stratum := 2, poll := 6, precision := -23, + rootDelay := 0x00000236, rootDispersion := 0x000003B7, refId := 0xC035676C, + reference := 0xE5F661FD6F165F03, origin := 0xE5F663A87619EF40, + receive := 0xE5F663A8798C6581, transmit := 0xE5F663A8798EAE2B } + +/-- A server reply with two extension fields and no MAC, as captured: + a Network Time Security reply (RFC 8915) with a Unique Identifier + field (type 0x0104, 36 octets) and an Authenticator and Encrypted + Extension Fields field (type 0x0404, 212 octets). The last field is + at least 28 octets, as RFC 7822 7.5.1.4 asks. -/ +def ntsReply : List UInt8 := + [0x24, 0x01, 0x04, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x54, 0x4D, 0x4E, 0x4C, + 0xE8, 0x49, 0x48, 0x92, 0xF9, 0x29, 0x57, 0x9E, 0x62, 0x87, 0xDB, 0x47, 0x3F, 0xF7, 0x5F, 0x58, + 0xE8, 0x49, 0x48, 0xB2, 0xB6, 0x40, 0xD7, 0x01, 0xE8, 0x49, 0x48, 0xB2, 0xB6, 0x44, 0xBF, 0xF8, + 0x01, 0x04, 0x00, 0x24, 0xE4, 0x83, 0x3A, 0x8D, 0x60, 0x0E, 0x13, 0x42, 0x43, 0x5C, 0xB2, 0x9D, + 0xE5, 0x50, 0xAC, 0xC0, 0xF8, 0xD8, 0xFA, 0x16, 0xE5, 0xC5, 0x37, 0x0A, 0x62, 0x0B, 0x15, 0x5F, + 0x58, 0x6A, 0xDA, 0xD6, 0x04, 0x04, 0x00, 0xD4, 0x00, 0x10, 0x00, 0xBC, 0x6A, 0x1D, 0xE3, 0xC2, + 0x6E, 0x13, 0xEB, 0x10, 0xC7, 0x39, 0xD7, 0x0B, 0x84, 0x1F, 0xAD, 0x1B, 0x86, 0xE2, 0x30, 0xC6, + 0x3E, 0x9E, 0xA5, 0xF7, 0x1B, 0x62, 0xA8, 0xA7, 0x98, 0x81, 0xCE, 0x7C, 0x6B, 0x17, 0xCB, 0x31, + 0x32, 0x49, 0x0F, 0xDE, 0xCF, 0x21, 0x10, 0x56, 0x4E, 0x36, 0x88, 0x92, 0xDD, 0xEE, 0xF1, 0xF4, + 0x23, 0xF6, 0x55, 0x53, 0x41, 0xC2, 0xC9, 0x17, 0x61, 0x20, 0xA5, 0x18, 0xDC, 0x1A, 0x7E, 0xDC, + 0x5E, 0xE3, 0xC8, 0x3B, 0x05, 0x08, 0x7B, 0x73, 0x03, 0xF7, 0xAB, 0x86, 0xD5, 0x2C, 0xC7, 0x49, + 0x0C, 0xE8, 0x29, 0x39, 0x72, 0x23, 0xDC, 0xEF, 0x2D, 0x94, 0xFA, 0xF8, 0xD7, 0x1D, 0x12, 0x80, + 0xDA, 0x03, 0x2D, 0xD7, 0x04, 0x69, 0xE9, 0xAC, 0x5F, 0x82, 0xEF, 0x57, 0x81, 0xD2, 0x07, 0xFB, + 0xAC, 0xB4, 0xA8, 0xB6, 0x31, 0x91, 0x14, 0xD5, 0xF5, 0x6F, 0xB2, 0x2A, 0x0C, 0xB6, 0xD7, 0xDC, + 0xF7, 0x7D, 0xF0, 0x21, 0x46, 0xF6, 0x7E, 0x46, 0x01, 0xB5, 0x3B, 0x21, 0x7C, 0xA8, 0xAC, 0x1A, + 0x4D, 0x97, 0xD5, 0x9B, 0xCE, 0xEB, 0x98, 0x33, 0x99, 0x7F, 0x10, 0x0E, 0xD4, 0x69, 0x85, 0x8B, + 0xCD, 0x73, 0x52, 0x01, 0xAD, 0xEC, 0x38, 0xCF, 0x8C, 0xB2, 0xC6, 0xD0, 0x54, 0x1A, 0x97, 0x67, + 0xDD, 0xB3, 0xEA, 0x09, 0x1D, 0x63, 0xD9, 0x8D, 0x03, 0xDD, 0x6E, 0x48, 0x15, 0x3D, 0xC9, 0xB6, + 0x1F, 0xE5, 0xD9, 0x1D, 0x74, 0xAE, 0x35, 0x48] + +/-- Its header: server, stratum 1, poll 2^4 seconds, root dispersion + 0x60/65536 seconds, reference ID "TMNL". -/ +def ntsHeader : Header := + { leap := 0, version := 4, mode := 4, stratum := 1, poll := 4, precision := -24, + rootDelay := 0, rootDispersion := 0x00000060, refId := 0x544D4E4C, + reference := 0xE8494892F929579E, origin := 0x6287DB473FF75F58, + receive := 0xE84948B2B640D701, transmit := 0xE84948B2B644BFF8 } + +/-- A 16-octet extension field of type 0x0104: Field Type, Length 16, + then 12 octets. -/ +def field16 : List UInt8 := + [0x01, 0x04, 0x00, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C] + +/-- A 20-octet extension field of type 0x0204. -/ +def field20 : List UInt8 := + [0x02, 0x04, 0x00, 0x14, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, + 0x2D, 0x2E, 0x2F, 0x30] + +/-- A 28-octet extension field of type 0x0104. -/ +def field28 : List UInt8 := + [0x01, 0x04, 0x00, 0x1C] ++ List.replicate 24 0x5A + +/-- A 20-octet MAC: key identifier 1, then a 128-bit digest. -/ +def mac20 : List UInt8 := + [0x00, 0x00, 0x00, 0x01, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, + 0xAC, 0xAD, 0xAE, 0xAF] + +/-- The client request with two extension fields, of 16 and 20 octets, + and a 20-octet MAC: after the header, 56 octets are left, so a + field; then 40, so a field; then 20, the MAC. -/ +def withMac : List UInt8 := clientRequest ++ field16 ++ field20 ++ mac20 + +/-- The server reply as a crypto-NAK: the header, then four zero + octets. -/ +def cryptoNak : List UInt8 := serverReply ++ [0, 0, 0, 0] + +/-- The client request as version 3 (first octet 0x1B), with a 20-octet + MAC. -/ +def v3WithMac : List UInt8 := (0x1B :: clientRequest.tail) ++ mac20 + +/-- 24 octets after the header that begin like an extension field of + Length 24: 24 octets left is a MAC, so they are read as one. -/ +def tail24 : List UInt8 := + [0x01, 0x04, 0x00, 0x18, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, + 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44] + +/-- A 28-octet Autokey field laid out as RFC 5906 Figure 7, whose Value + Length runs far past the field, as in CVE-2014-9750: the first octet + is the R and E bits (both 0, a request) and the Code (1, ASSOC, + section 10.2); the second, the Field Type, is "the version number, + currently 2"; then the Length, 28; the Association ID, 1; the + Timestamp and the Filestamp, 0; the Value Length, 0xFFFFFFFF; four + octets of Value, "host"; and the Signature Length, 0. -/ +def autokeyAssoc : List UInt8 := + [0x01, 0x02, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF, 0x68, 0x6F, 0x73, 0x74, 0x00, 0x00, 0x00, 0x00] + +end Spec diff --git a/bar.toml b/bar.toml new file mode 100644 index 0000000..59ab76b --- /dev/null +++ b/bar.toml @@ -0,0 +1,32 @@ +# Read by bar/check.py in Proven-Commons/mission. +# See playbook/verified-bar.md there for what each field means. + +[project] +name = "ntp" +# "in-progress" until every bar item passes, then "verified". +status = "in-progress" +page = "README.md" +# GitHub handles of everyone who wrote the specification. It was drafted +# by Claude, an AI agent, for the org: agent fdn-claude, for the fdn-0 +# account, posting as zero-aska on GitHub. +spec_authors = ["claude", "fdn-0", "zero-aska"] +# The program people run. A change to it voids the approval. +entry_points = ["Main.lean"] + +[spec] +files = ["SPEC.md", "Spec.lean", "Spec/Defs.lean", "Spec/Examples.lean"] +# The code under proof: the functions the statements are about. +code = ["Ntp.decode", "Ntp.encode", "Ntp.toUnix", "Ntp.fromUnix"] + +# One entry per approval, added in a pull request the reviewer approves. +# [[spec.approval]] +# reviewer = "github-handle" +# date = "2026-11-02" +# sha256 = "..." +# record = "https://github.com/Proven-Commons/ntp/pull/" + +[tests] +results = "tests/TEST-RESULTS.md" +command = "python3 tests/run_tests.py" +files = ["tests/run_tests.py", "tests/plant_bugs.py", "tests/ntpd-rs/Cargo.toml", "tests/ntpd-rs/Cargo.lock", + "tests/ntpd-rs/src/main.rs", "tests/ntp-3-wire-vectors.jsonl"] diff --git a/comparator.json b/comparator.json new file mode 100644 index 0000000..49fb4e9 --- /dev/null +++ b/comparator.json @@ -0,0 +1,11 @@ +{ + "challenge_module": "Spec", + "solution_module": "Proofs", + "theorem_names": [ + "Spec.decode_iff", "Spec.encode_eq", "Spec.decode_encode", "Spec.encode_decode", + "Spec.decode_short", "Spec.decode_version", "Spec.decode_mode", "Spec.decode_ext_overrun", + "Spec.toUnix_zero", "Spec.toUnix_spec", "Spec.fromUnix_toUnix", "Spec.toUnix_fromUnix", + "Spec.examples", "Spec.rejects" + ], + "permitted_axioms": ["propext", "Quot.sound", "Classical.choice"] +} diff --git a/lake-manifest.json b/lake-manifest.json new file mode 100644 index 0000000..e9d1e7d --- /dev/null +++ b/lake-manifest.json @@ -0,0 +1,6 @@ +{"version": "1.3.0", + "packagesDir": ".lake/packages", + "packages": [], + "name": "ntp", + "lakeDir": ".lake", + "fixedToolchain": false} diff --git a/lakefile.toml b/lakefile.toml new file mode 100644 index 0000000..941c4a8 --- /dev/null +++ b/lakefile.toml @@ -0,0 +1,17 @@ +name = "ntp" +version = "0.1.0" +license = "Apache-2.0" +defaultTargets = ["Ntp", "Spec", "Proofs", "ntp"] + +[[lean_lib]] +name = "Ntp" + +[[lean_lib]] +name = "Spec" + +[[lean_lib]] +name = "Proofs" + +[[lean_exe]] +name = "ntp" +root = "Main" diff --git a/lean-toolchain b/lean-toolchain new file mode 100644 index 0000000..a6aff06 --- /dev/null +++ b/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.35.0 diff --git a/tests/PLANTED-BUGS.md b/tests/PLANTED-BUGS.md new file mode 100644 index 0000000..fac03f3 --- /dev/null +++ b/tests/PLANTED-BUGS.md @@ -0,0 +1,66 @@ +# Planted bugs + +Written by `tests/plant_bugs.py`. Each bug is a change of one line, to `Ntp/Packet.lean` (38 bugs), `Ntp/Time.lean` (8) or `Main.lean` (11), tested with `tests/run_tests.py --count 2000 --no-oracles`, which builds only the executable and compares it with the model alone. Every bug must give failures, and the unchanged code none. A bug that does not build, or a run that gives up or dies, counts as missed. So does a run on other inputs than the unchanged code's: every run must give the same `- Inputs SHA-256:` line, which here is dc7c1a52625cf15fff32335937e7c1700b512af3c4a3332b0d5ff65a24316f23. + +| Planted bug | Failures | +|---|---| +| none | 0 | +| an input shorter than asked for is taken whole, as a reader that runs off the end | 3164 | +| numbers are read little-endian | 31206 | +| numbers are written little-endian | 15461 | +| the octet 128 is read as +128, not -128 | 72 | +| a negative number is written one less | 14670 | +| the version is read from two bits | 29740 | +| the mode is read from two bits | 27022 | +| version 2 is accepted | 135 | +| version 5 is accepted | 131 | +| mode 0 is accepted | 73 | +| mode 6 is accepted | 77 | +| mode 7 is accepted | 71 | +| the leap indicator keeps only its low bit | 3056 | +| Poll is read as unsigned, as NTPsec and ntplib read it | 2975 | +| Precision is read as unsigned | 28329 | +| the root delay and the root dispersion are swapped | 31194 | +| the origin and receive timestamps are swapped | 31198 | +| 16 octets after the fields are taken as a MAC | 81 | +| 21 to 23 octets after the fields are taken as a MAC, as chrony's 20 to 24 would | 241 | +| a field is read while 20 octets are left, not 24, so 24 octets are a field | 1111 | +| a field is read only while 28 octets are left, so 28 octets are a MAC | 521 | +| a field is read while 16 or more octets are left, as Wireshark reads them, so a last field without a MAC may be 16 octets | 2266 | +| a field of Length 12 is accepted | 9 | +| a field of Length 4 is accepted, as ntpd-rs accepts it | 26 | +| a field Length need not be a multiple of 4 | 71 | +| a field Length past the end is cut short at the end, as a clamping reader would | 1839 | +| a field's body is taken as Length less 8 | 4012 | +| the fields come back in reverse order | 2456 | +| a crypto-NAK is taken after fields too | 33 | +| four octets of any value are a crypto-NAK | 72 | +| version 3 packets get extension fields, as ntpd reads a tail of more than 24 octets | 103 | +| at most two extension fields are read | 1110 | +| in version 3, any 24 octets or fewer are the MAC, as ntpd-rs reads them | 150 | +| version 3 is read with the version 4 rule | 103 | +| a field's Length is written without its four header octets | 1841 | +| the mode is written from two bits | 13368 | +| the root dispersion is written before the root delay | 15455 | +| the MAC is not written | 1561 | +| the raw value 0 converts to 1900, in Ntp/Time.lean | 11 | +| a raw value of 2^64 converts, in Ntp/Time.lean | 2 | +| the era is left out of the conversion, in Ntp/Time.lean | 882 | +| the epoch is a day off, in Ntp/Time.lean | 1108 | +| the fraction is taken modulo 2^32 - 1, in Ntp/Time.lean | 1075 | +| a fraction of 2^32 is taken, in Ntp/Time.lean | 7 | +| the era is found by division rounding toward zero, in Ntp/Time.lean | 835 | +| the first instant of an era gets the timestamp 0, in Ntp/Time.lean | 10 | +| Main.lean: upper-case hex digits are read | 5 | +| Main.lean: an empty hex field is read as no octets | 3 | +| Main.lean: hex is printed in upper case | 17991 | +| Main.lean: the two hex digits of an octet are printed the wrong way round | 18029 | +| Main.lean: the sign of a negative number is dropped | 16027 | +| Main.lean: Poll and Precision are printed the wrong way round | 15734 | +| Main.lean: `-` for no extension fields is not read | 13666 | +| Main.lean: `encode` reads the root delay and dispersion the wrong way round | 15455 | +| Main.lean: `encode` answers OK for a packet that is not valid | 88 | +| Main.lean: the CR of a CR LF line ending is kept | 6014 | +| Main.lean: wrong arguments exit with 0 | 8 | + +Bugs caught: 57 of 57 diff --git a/tests/TEST-RESULTS.md b/tests/TEST-RESULTS.md new file mode 100644 index 0000000..40397ef --- /dev/null +++ b/tests/TEST-RESULTS.md @@ -0,0 +1,165 @@ +# Test results + +Written by `tests/run_tests.py`. Rerun it to check these numbers. + +- Oracle: the model of RFC 5905 and RFC 7822 in `tests/run_tests.py`, which every answer of the tool must equal +- Oracle: ntpd-rs: the ntp-proto crate 1.9.0, `NtpPacket::deserialize`, through tests/ntpd-rs (every crate pinned by its Cargo.lock), built with rustc 1.97.0 (2d8144b78 2026-07-07) +- Oracle: NTPsec: `ntp.packet.SyncPacket.analyze` of python3-ntp 1.2.2+dfsg1-4build2 (NTPsec 1.2.2), from /usr/lib/python3/dist-packages/ntp +- Oracle: Wireshark: the NTP dissector of TShark (Wireshark) 4.2.2 (Git v4.2.2 packaged as 4.2.2-1.1build3), through `tshark -T pdml` +- Oracle: ntplib 0.4.0: `NTPPacket.from_data`, for the header fields only +- Oracle: RFC 5905 Figure 4, with errata 4025 and 6524, and Python's datetime module (Python 3.13.17), for the Unix times +- Oracle: the 79 vectors of Proven-Commons/ntp pull request 3 (orda-aska, commit cc5dab4), in `tests/ntp-3-wire-vectors.jsonl` +- Oracles left out: none; and dropped: chrony's parser (the session that wrote these tests was not permitted to build chrony's downloaded source for a harness) +- Lean: Lean (version 4.35.0, x86_64-unknown-linux-gnu, commit eb6cafb7290e8bd5f2937522d8d804f58f02bafc, Release) +- Executable: built by `lake build ntp`; SHA-256 of the copy run efa9661585095f05de2c88da0f0146dd64d4c511dd01f4361d8976eb37d51edf +- Seed: 20261028 +- Count: 20000 +- Inputs: 151345 +- Inputs SHA-256: 0dfa0e6678b162dc9dc569179b480959769be71f28423286cbbe70c3f02d2a94, of the request lines, in order; a rerun with the same seed and count must give the same +- By kind: Spec.lean's examples and rejects (decode) 28, Spec.lean's examples and rejects (encode) 1, Spec.lean's examples and rejects (fromunix) 3, Spec.lean's examples and rejects (unix) 7, a MAC of 4 or 20 to 24 octets, after a field of 16 (decode) 12, a MAC of 4 or 20 to 24 octets, after a field of 28 (decode) 12, a MAC of 4 or 20 to 24 octets, after the header (decode) 12, a field Length running 4 or 1000 octets past the end, or to 65532 (decode) 36, a field of every Length from 0 to 80 (decode) 81, a field of every Length from 0 to 80, after a field of 28 (decode) 81, a field of every Length from 0 to 80, then a 20-octet MAC (decode) 81, a header from random fields (decode) 3235, a packet cut short, at every length (decode) 764, a packet not valid in one way, encoded: a MAC of 16 octets (encode) 1, a packet not valid in one way, encoded: a MAC of 21 octets (encode) 1, a packet not valid in one way, encoded: a MAC of 28 octets (encode) 1, a packet not valid in one way, encoded: a crypto-NAK after a field (encode) 1, a packet not valid in one way, encoded: a field body of 13 octets (encode) 1, a packet not valid in one way, encoded: a field body of 25 octets (encode) 1, a packet not valid in one way, encoded: a field body of 65532 octets (encode) 1, a packet not valid in one way, encoded: a field body of 8 octets (encode) 1, a packet not valid in one way, encoded: a field body of none (encode) 1, a packet not valid in one way, encoded: a field in version 3 (encode) 1, a packet not valid in one way, encoded: a field in version 3, and a MAC (encode) 1, a packet not valid in one way, encoded: a field type of 2^16 (encode) 1, a packet not valid in one way, encoded: a last field of 16 octets with no MAC (encode) 1, a packet not valid in one way, encoded: a last field of 24 octets with no MAC (encode) 1, a packet not valid in one way, encoded: a leap indicator of 4 (encode) 1, a packet not valid in one way, encoded: four octets that are not zero (encode) 1, a packet not valid in one way, encoded: mode 0 (encode) 1, a packet not valid in one way, encoded: mode 6 (encode) 1, a packet not valid in one way, encoded: mode 7 (encode) 1, a packet not valid in one way, encoded: mode 8 (encode) 1, a packet not valid in one way, encoded: origin timestamp 2^64 (encode) 1, a packet not valid in one way, encoded: poll -129 (encode) 1, a packet not valid in one way, encoded: poll 128 (encode) 1, a packet not valid in one way, encoded: precision -129 (encode) 1, a packet not valid in one way, encoded: precision 128 (encode) 1, a packet not valid in one way, encoded: receive timestamp 2^64 (encode) 1, a packet not valid in one way, encoded: reference ID 2^32 (encode) 1, a packet not valid in one way, encoded: reference timestamp 2^64 (encode) 1, a packet not valid in one way, encoded: root delay 2^32 (encode) 1, a packet not valid in one way, encoded: root dispersion 2^32 (encode) 1, a packet not valid in one way, encoded: stratum 256 (encode) 1, a packet not valid in one way, encoded: transmit timestamp 2^64 (encode) 1, a packet not valid in one way, encoded: version 2 (encode) 1, a packet not valid in one way, encoded: version 5 (encode) 1, a packet not valid in one way, encoded: version 8 (encode) 1, a packet the model reads, encoded (encode) 39641, a random packet cut short, at every length (decode) 32844, a random packet not valid in one way, encoded (encode) 50, a random packet, 0 fields (decode) 8285, a random packet, 1 fields (decode) 3325, a random packet, 2 fields (decode) 3393, a random packet, 3 fields (decode) 1705, a random packet, 4 fields (decode) 1635, a random packet, 5 fields (decode) 1657, a random packet, a Length field changed (decode) 401, a random packet, a MAC of 21 to 23 octets (decode) 231, a random packet, a MAC of 28 or 32 octets (decode) 222, a random packet, a MAC of 8, 12 or 16 octets (decode) 242, a random packet, a bit flipped (decode) 558, a random packet, a crypto-NAK after a field (decode) 241, a random packet, a field Length 1000 past the end (decode) 229, a random packet, a field Length 4 more or less (decode) 245, a random packet, a field Length 4 past the end (decode) 228, a random packet, a field Length of 65532 or 65535 past the end (decode) 240, a random packet, a field Length that is not a multiple of 4 (decode) 220, a random packet, a field in version 3 (decode) 232, a random packet, a field of Length 0 to 12 (decode) 219, a random packet, a last field of 16 to 24 octets with no MAC (decode) 268, a random packet, a version other than 3 and 4 (decode) 247, a random packet, an octet changed (decode) 806, a random packet, an octet inserted (decode) 576, a random packet, an octet less at the end (decode) 242, a random packet, an octet more at the end (decode) 240, a random packet, an octet of the tail changed (decode) 486, a random packet, an octet removed (decode) 586, a random packet, four octets that are not zero (decode) 229, a random packet, mode 0, 6 or 7 (decode) 225, a random packet, two or three octets changed (decode) 587, a raw value of 2^64 or more (unix) 8, a row of RFC 5905 Figure 4 (fromunix) 12, a row of RFC 5905 Figure 4 (unix) 12, a tail of every length from 0 to 80, version 3, all ones (decode) 81, a tail of every length from 0 to 80, version 3, one field of that length (decode) 77, a tail of every length from 0 to 80, version 3, random (decode) 81, a tail of every length from 0 to 80, version 3, zeros (decode) 81, a tail of every length from 0 to 80, version 4, all ones (decode) 81, a tail of every length from 0 to 80, version 4, one field of that length (decode) 77, a tail of every length from 0 to 80, version 4, random (decode) 81, a tail of every length from 0 to 80, version 4, zeros (decode) 81, a vector of pull request 3 (decode) 79, an Autokey field whose Value Length runs past it (CVE-2014-9750) (decode) 288, each of eras -1 to 2 with each special raw value (unix) 52, every value of every header octet, in the server reply (decode) 12240, every value of the first octet, in the NTS reply (decode) 255, every value of the first octet, in the client request (decode) 255, every value of the first octet, in the client request with a 24-octet MAC (decode) 255, every value of the first octet, in the client request with a 28-octet field (decode) 255, every value of the first octet, in the client request with two fields and a MAC (decode) 255, every value of the first octet, in the crypto-NAK (decode) 255, every value of the first octet, in the server reply (decode) 255, every value of the first octet, in the version 3 packet with a MAC (decode) 255, random (fromunix) 10000, random (unix) 10000, random octets, 48 of them (decode) 765, the 2036 rollover (unix) 42, the Unix time of a `unix` request, back (fromunix) 10066, the first instant of an era, and around it (fromunix) 35, the largest fields (decode) 5, version 3 with 21 to 23 octets (decode) 3, version 3 with 8, 12 or 16 octets (decode) 3, version 3 with a 20-octet MAC (decode) 1, version 3 with a 24-octet MAC (decode) 1, version 3 with a 72-octet MS-SNTP tail (decode) 1, version 3 with a crypto-NAK (decode) 1, version 3 with a field of 16 octets and a MAC (decode) 1, version 3 with a field of 28 octets (decode) 1, version 3 with a field of 28 octets and a MAC (decode) 1, version 3 with four octets that are not zero (decode) 1, version 3 with none (decode) 1 +- Requests: `decode` 81258, `encode` 39727, `fromunix` 20116, `unix` 10121, malformed 92, odd 17, at the end of input 4, command line 10 +- Answers that differ from the model: 0 +- Spec.lean's `examples` and `rejects`, answered by the model as Spec.lean states them: 39 of 39 +- The model's two readings of each `decode` input, by `Valid` and `packetBytes` and by SPEC.md's one-pass rule, the same: 81258 of 81258 +- Autokey fields (RFC 5906 Figure 7) whose Value Length runs past the field, as in CVE-2014-9750, read by their own Length with their octets unchanged, by the tool and the model: 288 of 288 +- Packets: 81258, of which the model reads 39641; why it refuses the others: a field Length past the end of the packet (CVE-2014-9750's flaw, at the level of the field's own Length) 18909, fewer than 48 octets (RFC 5905 7.3: the header is 12 words) 8774, 1 to 19 octets left that are not a whole number of words (RFC 7822 section 3) 4728, a version other than 3 and 4 2607, 21 to 23 octets left: not a MAC of 20 or 24 octets (RFC 7822 section 1) 1257, 8, 12 or 16 octets left after the fields: not a MAC (RFC 7822 7.5.1.3), and a last field without a MAC is at least 28 (7.5.1.4) 1224, a field Length that is not a multiple of 4 (RFC 7822 section 3) 1204, version 3, with octets after the header that are not a MAC of 20 or 24 octets or a crypto-NAK (this reader's choice: the MACs of NTPv4, and no extension fields, which RFC 7822 section 3 gives to NTPv4; RFC 1305's own authenticator is 12 octets) 1158, mode 0, 6 or 7 (RFC 5905 Figure 10) 507, a field of Length below 16 (RFC 7822 section 3) 454, four octets after the header that are not zero (RFC 5905 9.2: the crypto-NAK is zeros) 406, four zero octets after extension fields: the crypto-NAK is a packet's only tail 225, four octets left after the fields that are not zero: not a crypto-NAK 164 +- Packets the model reads, by version, number of extension fields and MAC: version 3, 0 fields, MAC 20 octets: 1125, version 3, 0 fields, MAC 24 octets: 1157, version 3, 0 fields, MAC a crypto-NAK: 1157, version 3, 0 fields, MAC none: 3191, version 4, 0 fields, MAC 20 octets: 918, version 4, 0 fields, MAC 24 octets: 965, version 4, 0 fields, MAC a crypto-NAK: 775, version 4, 0 fields, MAC none: 16256, version 4, 1 fields, MAC 20 octets: 969, version 4, 1 fields, MAC 24 octets: 861, version 4, 1 fields, MAC none: 2458, version 4, 2 fields, MAC 20 octets: 797, version 4, 2 fields, MAC 24 octets: 926, version 4, 2 fields, MAC none: 2439, version 4, 3 fields, MAC 20 octets: 382, version 4, 3 fields, MAC 24 octets: 461, version 4, 3 fields, MAC none: 1154, version 4, 4 fields, MAC 20 octets: 379, version 4, 4 fields, MAC 24 octets: 343, version 4, 4 fields, MAC none: 1086, version 4, 5+ fields, MAC 20 octets: 393, version 4, 5+ fields, MAC 24 octets: 381, version 4, 5+ fields, MAC none: 1068 +- Malformed requests answered `error`: 92 of 92; odd but well-formed requests answered as the model says: 17 of 17 +- Requests ending in CR LF: every seventh, 21603 +- Last lines sent alone, with no LF, with CR LF, or with a lone CR, answered right: 4 of 4 +- Command line (`ntp serve` and wrong arguments), printing and exiting as the header of Main.lean says: 10 of 10 +- RFC 5905 Figure 4 (`unix` of each row's era and offset, and `fromunix` of its MJD as Unix time): 22 of 24 as the table gives them; named differences: Figure 4 gives 1 Jan 1900 the timestamp 0, which RFC 5905 section 6 makes the value for unknown time: no Unix time, and no timestamp for that instant 2 +- Unix times the tool gives (`unix` answers, and the requests of `fromunix` answers), as Python's datetime gives them: 24439 of 24439; outside datetime's years 1 to 9999: 4512 +- ntpd-rs: 80448 distinct packets; the same answer as the tool: 70891 (of the 39583 the tool reads: 37629); named differences: 9557; the model with ntpd-rs's choices gives its answer: 80448 of 80448 +- NTPsec: 80448 distinct packets; the same answer as the tool: 48906 (of the 39583 the tool reads: 18712); named differences: 31542; the model with NTPsec's choices gives its answer: 80448 of 80448 +- Wireshark: 80448 distinct packets; the same answer as the tool: 48959 (of the 39583 the tool reads: 39304); named differences: 31489; the model with Wireshark's choices gives its answer: 80448 of 80448 +- ntplib: 80448 distinct packets; the same answer as the tool: 34269 (of the 39583 the tool reads: 26011); named differences: 46179; the model with ntplib's choices gives its answer: 80448 of 80448 +- Pull request 3: 79 vectors; the same answer as the tool: 74; named differences: 5; the model with its choices gives the vector's answer: 79 of 79 +- Time: 66 s +- Failures: 0 + +## Where the oracles and the tool differ + +Each difference is counted under the smallest set of the oracle's choices with which the model gives the oracle's answer, so the choices named are the ones that make the difference. The first case that showed each is given. + +### ntpd-rs + +The choices of ntpd-rs that the model can switch on: + +1. ntpd-rs reads every mode, 0, 6 and 7 too, with the header of Figure 8 +2. ntpd-rs takes an extension field of 4 to 12 octets: its minimum is 4 (V4_UNENCRYPTED_MINIMUM_SIZE), where RFC 7822 asks for 16 +3. ntpd-rs takes any 4 to 24 octets left after the extension fields as the MAC, whatever they hold (Mac::deserialize): four octets that are not zero, a crypto-NAK after fields, 8 to 16 octets that RFC 7822 7.5.1.4 rules out, and 21 to 23 +4. ntpd-rs takes any 4 to 24 octets after a version 3 header as the MAC +5. ntpd-rs refuses an NTS Cookie Placeholder (0x0304) whose body is not zero, and an NTS Authenticator and Encrypted Extension Fields field (0x0404) whose nonce and ciphertext lengths run past it (RFC 8915 5.5 and 5.6) + +The differences, by the choices that make them: + +- choice 3: 6624. For example decode 230206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde480104001... (147 characters): the tool refused; ntpd-rs 0 4 3 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields 260:5a5a5a5a5a5a5a5a5a5a5... (214 characters) +- choice 5: 1954. For example decode 2312179528d563fcd3c5a1ebf091b1fa291bf2aed31ab6b193c2bd0b0037b4dcb5c58bcd5d95a4b2fe9632b9cb8899450404001... (147 characters): the tool 0 4 3 18 23 -107 685073404 3552944619 4036080122 2962228012606207665 10647280324003214556 13098028805431600306 18344905905705359685 fields 1028:? MAC ... (198 characters); ntpd-rs refused +- choice 1: 422. For example decode 200206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool refused; ntpd-rs 0 4 0 2 6 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC - +- choice 4: 419. For example decode 5cc41e47f85599ff4ef5a56e7fb625ba632e4eece2a362cfffffffffffffffff329bf29add74dc71f1958e12b24457c30000000... (113 characters): the tool refused; ntpd-rs 1 3 4 196 30 71 4166359551 1324721518 2142643642 7146736438005162703 18446744073709551615 3646775070240857201 17407976145621899203 fields - MAC 000000... (154 characters) +- choice 2: 135. For example decode 230206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde480104000... (147 characters): the tool refused; ntpd-rs 0 4 3 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields 260:5a5a5a5a5a5a5a5a,260:... (204 characters) +- choices 1 and 3: 2. For example decode 20015a130001000041469cd3bcd4523c807ff94241e0fb79b5f9ce29c26be4b2d8b277a960d19553901c479b1c15bed3b18d002... (147 characters): the tool refused; ntpd-rs 0 4 0 1 90 19 65536 1095146707 3168031292 9259393421865450361 13112738468816741554 15614674427404457299 10384253572326211283 fields 45453:c09c5e3e5a95... (660 characters) +- choices 2 and 3: 1. For example decode 231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe000... (147 characters): the tool refused; ntpd-rs 0 4 3 16 -128 -1 2147549186 4294901761 1380013125 0 1 9223372036854775808 18446744073709551615 fields 65278:0000000000000000 MAC 000000000000000000000... (161 characters) + +### NTPsec + +The choices of NTPsec that the model can switch on: + +1. NTPsec refuses a packet whose length is not a multiple of 4 +2. NTPsec reads every version +3. NTPsec reads every mode with the header of Figure 8 +4. NTPsec reads Poll as unsigned (struct format B; RFC 5905 7.3 says signed) +5. NTPsec reads an extension field as a 32-bit type and a 32-bit length of the value after them, with no padding or minimum, cut short at the end of the packet (SyncPacket.analyze), not as RFC 7822 Figure 14 lays it out +6. NTPsec takes four octets left after the fields as a crypto-NAK whatever they hold, fields or not; refuses 8, 12 or 16; and drops any other number of octets but 0, 20 and 24 without a word +7. NTPsec reads version 3 as version 4, extension fields too + +The differences, by the choices that make them: + +- choice 5: 10962. For example decode 240104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff80104002... (147 characters): the tool 0 4 4 1 4 -24 0 96 1414352460 16737989286181820318 7099884436635213656 16737989422498240257 16737989422498496504 fields 260:e4833a8d600e1342435cb29de5... (616 characters); NTPsec 0 4 4 1 4 -24 0 96 1414352460 16737989286181820318 7099884436635213656 16737989422498240257 16737989422498496504 fields 17039396:600e1342435cb29de550a... (615 characters) +- choices 4 and 5: 10046. For example decode 236d942e9c04486cddd507cecfec93c236e105ff823bf6ce3646ceef9046242da3e13902de9881b5278b4acbb59f963bfe04006... (150 characters): the tool 0 4 3 109 -108 46 2617526380 3721725902 3488388034 3954448542767773390 3911040854730875949 11808782382429405621 2849453428027921979 fields 65028:93977... (125484 characters); NTPsec 0 4 3 109 148 46 2617526380 3721725902 3488388034 3954448542767773390 3911040854730875949 11808782382429405621 2849453428027921979 fields 4261675112:c... (125483 characters) +- choice 4: 6774. For example decode 240280e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool 0 4 4 2 -128 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC -; NTPsec 0 4 4 2 128 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC - +- choice 2: 848. For example decode 140206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool refused; NTPsec 0 2 4 2 6 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC - +- choices 2 and 3: 472. For example decode 000206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde48: the tool refused; NTPsec 0 0 0 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields - MAC - +- choices 2 and 5: 446. For example decode 010104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff80104002... (147 characters): the tool refused; NTPsec 0 0 1 1 4 -24 0 96 1414352460 16737989286181820318 7099884436635213656 16737989422498240257 16737989422498496504 fields 17039396:600e1342435cb29de550a... (615 characters) +- choices 5 and 7: 237. For example decode 1b0206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde480104001... (147 characters): the tool refused; NTPsec 0 3 3 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields 17039388:5a5a5a5a5a5a5a5a... (180 characters) +- choices 2 and 4: 225. For example decode b5c79e73d24ac9b6c83d0034c79a9ce62bbe3d04c11b4fce6346bebdbf0ede879d7d288eb789f3ce7d6deb000105ce18: the tool refused; NTPsec 2 6 5 199 158 115 3528116662 3359440948 3348798694 3152023879834881998 7153614780288130695 11348271229473256398 9038138412452138520 fields - MAC - +- choice 6: 220. For example decode 240206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b0000000... (111 characters): the tool refused; NTPsec 0 4 4 2 6 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC 00000001 +- choices 2, 3 and 5: 216. For example decode 000104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff80104002... (147 characters): the tool refused; NTPsec 0 0 0 1 4 -24 0 96 1414352460 16737989286181820318 7099884436635213656 16737989422498240257 16737989422498496504 fields 17039396:600e1342435cb29de550a... (615 characters) +- choices 4 and 6: 186. For example decode 61e8fa61b17e5484fe8510a1ffffffff2f3695dd150e2c3a6c838ef741eecee6c9a0a17c3da86992764e314aa2f03dbf494795e... (111 characters): the tool refused; NTPsec 1 4 1 232 250 97 2977846404 4270133409 4294967295 3402071345298549818 7819250570659548902 14528789952879683986 8524805341289921983 fields - MAC 494795... (152 characters) +- choice 3: 180. For example decode 200206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool refused; NTPsec 0 4 0 2 6 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC - +- choices 3 and 5: 121. For example decode 200104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff80104002... (147 characters): the tool refused; NTPsec 0 4 0 1 4 -24 0 96 1414352460 16737989286181820318 7099884436635213656 16737989422498240257 16737989422498496504 fields 17039396:600e1342435cb29de550a... (615 characters) +- choices 4, 5 and 7: 113. For example decode da1e834c507e08312ad211683eb9c601eb9c42994115c5aa646a35a59fedc36d53160ea13d56e9be4accbc37bf9bcfa64284008... (147 characters): the tool refused; NTPsec 3 3 2 30 131 76 1350436913 718410088 1052362241 16977517921269302698 7235654736793158509 5986988840317282750 5389889801679523750 fields 1115947140:cf3... (441 characters) +- choices 2, 4 and 5: 100. For example decode 82fdb2083ccffa26f68399c4665226b0da3e1aec69806c8fa32b9f2f85ed115083aa7e80000000006e56767d121429e6f132004... (147 characters): the tool refused; NTPsec 2 0 2 253 178 8 1020262950 4135819716 1716659888 15726036551508978831 11757666278639997264 9487534653230284800 7950672471715490278 fields 4046585928:b... (363 characters) +- choices 2, 3 and 4: 99. For example decode 57d7a9d3e465bea64c1d1c80d00273d9444ff307422d175cc92cbf6e76a1fa800b38342c990ab180a99c75ff0015eb76: the tool refused; NTPsec 1 2 7 215 169 -45 3831873190 1276976256 3489821657 4922420130239813468 14496171781756418688 808453499263824256 12221773226855230326 fields - MAC - +- choices 3, 4 and 5: 76. For example decode e03ddd9bc7dc5c329596f6e95037d6b82ab0e3134c817ded1609635957307bcd2a2de4e8c1ba59fdc6cc259abcae3e850304002... (147 characters): the tool refused; NTPsec 3 4 0 61 221 -101 3353107506 2509698793 1345836728 3076208217521487341 1587909578990844877 3039337011878713853 14324865861279432325 fields 50593836:-,... (218 characters) +- choices 3 and 4: 65. For example decode a02ba86ebd0f4f99d03d03d149abc68c87c3141f29f91c3671e4af48cced3b5acf58ca8f863a293bc34e65e7371b14da: the tool refused; NTPsec 2 4 0 43 168 110 3171897241 3493659601 1235994252 9782684939659582518 8206877148186491738 14940914481582713147 14073297929315751130 fields - MAC - +- choices 5 and 6: 41. For example decode 25b4093900000000e25a3c39df1ef7ae3630a4b51e9157fa89053ff0e4953266698f8f7c17990c5ef752a6cdd78cafe0fb8701c... (148 characters): the tool 0 4 5 180 9 57 0 3797564473 3743348654 3904801974739097594 9873368061939364454 7606456058786679902 17821490078473301984 fields 64391:00000000000000000... (1082 characters); NTPsec 0 4 5 180 9 57 0 3797564473 3743348654 3904801974739097594 9873368061939364454 7606456058786679902 17821490078473301984 fields 4219929028:-,0:-,0:-,0:... (370 characters) +- choices 3, 5 and 7: 36. For example decode 180104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff80104002... (147 characters): the tool refused; NTPsec 0 3 0 1 4 -24 0 96 1414352460 16737989286181820318 7099884436635213656 16737989422498240257 16737989422498496504 fields 17039396:600e1342435cb29de550a... (615 characters) +- choices 6 and 7: 30. For example decode 1b0206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde485417f9a... (111 characters): the tool refused; NTPsec 0 3 3 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields - MAC 5417f9ac +- choices 4, 6 and 7: 27. For example decode 9901b5873174475f53cc85475fa56e6b97f54063cf936b009c7d9e11110b258d7effe84efcf2559ada381f6e2aa7085a99fa6ab... (111 characters): the tool refused; NTPsec 2 3 1 1 181 -121 829704031 1405912391 1604677227 10949728866449713920 11276342838143165837 9151288393788970394 15724352656986540122 fields - MAC 99fa6... (153 characters) +- choices 4, 5 and 6: 19. For example decode a381e76dffda00cf05a487ad4ddc93db79e0403a44deddf5dd097044762e17033b878696c512cef28839537c4a533ebf0204001... (147 characters): the tool 2 4 3 129 -25 109 4292477135 94668717 1306301403 8782089892380204533 15927384996515288835 4289545152203247346 9815968656118726335 fields 516:000000000... (226 characters); NTPsec 2 4 3 129 231 109 4292477135 94668717 1306301403 8782089892380204533 15927384996515288835 4289545152203247346 9815968656118726335 fields 33816596:-,0:... (162 characters) +- choices 3, 5 and 6: 2. For example decode e0882e1356dadb138c40407232ae92081bd1f889dc1f48e86d5a15971306f105ee5193f2a866fa5c2c32a84f41a9bced5c8c03f... (148 characters): the tool refused; NTPsec 3 4 0 136 46 19 1457183507 2353021042 850301448 2004656580143827176 7879634236641571077 17172669524557101660 3184792944861887725 fields 1552679924:-,0... (659 characters) +- choices 4, 5, 6 and 7: 1. For example decode 59669f48232895f08d9357985c91f209d8b4bafc0a279a23ffffffff000000001a073475d315ff9536b1960cd7c7b9d30404006... (147 characters): the tool refused; NTPsec 1 3 1 102 159 72 589862384 2375243672 1553068553 15615311399665244707 18446744069414584320 1875525450480353173 3941096130829859283 fields 67371108:4da... (299 characters) + +### Wireshark + +The choices of Wireshark that the model can switch on: + +1. Wireshark reads every version +2. Wireshark reads mode 0 with the header of Figure 8 +3. Wireshark splits the octets after the header its own way (packet-ntp.c, dissect_ntp_std): fields while 16 or more octets are left, each of a Length of 16 or more that fits; then, when 1 to 19 octets are left that are not a crypto-NAK, it gives the last field back to the MAC; whatever is left is the MAC, of any length and content; a Length that is not a multiple of 4 gets an expert note; in every version + +The differences, by the choices that make them: + +- choice 3: 29434. For example decode 230206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde480104001... (147 characters): the tool 0 4 3 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields - MAC 0104001831323334353... (179 characters); Wireshark 0 4 3 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields 260:3132333435363738393a3... (175 characters) +- choice 1: 1490. For example decode 140206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool refused; Wireshark 0 2 4 2 6 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC - +- choices 1 and 2: 235. For example decode 000206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde48: the tool refused; Wireshark 0 0 0 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields - MAC - +- choice 2: 148. For example decode 200206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool refused; Wireshark 0 4 0 2 6 -23 566 951 3224725356 16570539620074675971 16570541454143385408 16570541454201218433 16570541454201368107 fields - MAC - +- choices 1 and 3: 132. For example decode 010206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde480104001... (147 characters): the tool refused; Wireshark 0 0 1 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields 260:3132333435363738393a3... (175 characters) +- choices 2 and 3: 25. For example decode 180104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff80104002... (147 characters): the tool refused; Wireshark 0 3 0 1 4 -24 0 96 1414352460 16737989286181820318 7099884436635213656 16737989422498240257 16737989422498496504 fields 260:e4833a8d600e1342435cb29de5... (616 characters) +- choices 1, 2 and 3: 25. For example decode 000206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde480104001... (147 characters): the tool refused; Wireshark 0 0 0 2 6 -24 1023 893 1590075151 16570540286000871855 16570541170764323165 16570541170862609808 16570541454143643208 fields 260:3132333435363738393a3... (175 characters) + +### ntplib + +The choices of ntplib that the model can switch on: + +1. ntplib reads every version +2. ntplib reads every mode +3. ntplib reads the header only, whatever follows it +4. ntplib reads Poll as unsigned (struct format B) + +The differences, by the choices that make them: + +- choice 3: 16337. For example decode 230206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde480104000... (147 characters): the tool refused; ntplib (0, 4, 3, 2, 6, -24, 0.0156097412109375, 0.0136260986328125, 1590075151, 3858129560.48196, 3858129766.4820037, 3858129766.504888, 3858129832.4613934) +- choice 4: 13572. For example decode 240280e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool (0, 4, 4, 2, -128, -23, 0.008636474609375, 0.0145111083984375, 3224725356, 3858129405.433935, 3858129832.4613333, 3858129832.4747987, 3858129832.47483... (153 characters); ntplib (0, 4, 4, 2, 128, -23, 0.008636474609375, 0.0145111083984375, 3224725356, 3858129405.433935, 3858129832.4613333, 3858129832.4747987, 3858129832.474833... (152 characters) +- choices 3 and 4: 13377. For example decode 21fadfbf318e2494389247013c7e397b258091a39c0cec4adcfcbab9c484079e336f10916c21aad5a6a000ec00e5eb78096c003... (147 characters): the tool refused; ntplib (0, 4, 1, 250, 223, -65, 12686.142883300781, 14482.277359008789, 1014905211, 629182883.6095722, 3707550393.7676396, 862916753.4223887, 2795503852.0035... (153 characters) +- choice 1: 1293. For example decode 140206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool refused; ntplib (0, 2, 4, 2, 6, -23, 0.008636474609375, 0.0145111083984375, 3224725356, 3858129405.433935, 3858129832.4613333, 3858129832.4747987, 3858129832.4748335) +- choices 1 and 2: 688. For example decode 000206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995de5f6636681405590e5f663a8761dde48: the tool refused; ntplib (0, 0, 0, 2, 6, -24, 0.0156097412109375, 0.0136260986328125, 1590075151, 3858129560.48196, 3858129766.4820037, 3858129766.504888, 3858129832.4613934) +- choices 1 and 4: 324. For example decode b5c79e73d24ac9b6c83d0034c79a9ce62bbe3d04c11b4fce6346bebdbf0ede879d7d288eb789f3ce7d6deb000105ce18: the tool refused; ntplib (2, 6, 5, 199, 158, 115, 53834.78793334961, 51261.00079345703, 3348798694, 733887748.754323, 1665580733.7463207, 2642225294.7169485, 2104355584.003995... (151 characters) +- choice 2: 302. For example decode 200206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40e5f663a8798c6581e5f663a8798eae2b: the tool refused; ntplib (0, 4, 0, 2, 6, -23, 0.008636474609375, 0.0145111083984375, 3224725356, 3858129405.433935, 3858129832.4613333, 3858129832.4747987, 3858129832.4748335) +- choices 2 and 4: 140. For example decode a02ba86ebd0f4f99d03d03d149abc68c87c3141f29f91c3671e4af48cced3b5acf58ca8f863a293bc34e65e7371b14da: the tool refused; ntplib (2, 4, 0, 43, 168, 110, 48399.31092834473, 53309.014907836914, 1235994252, 2277708831.1639576, 1910812488.800495, 3478702735.524325, 3276695015.215257... (151 characters) +- choices 1, 2 and 4: 99. For example decode 57d7a9d3e465bea64c1d1c80d00273d9444ff307422d175cc92cbf6e76a1fa800b38342c990ab180a99c75ff0015eb76: the tool refused; ntplib (1, 2, 7, 215, 169, -45, 58469.744720458984, 19485.111328125, 3489821657, 1146090247.2585006, 3375153006.463409, 188232748.59781942, 2845603327.000334... (152 characters) +- choices 2 and 3: 37. For example decode 180104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff80104002... (147 characters): the tool refused; ntplib (0, 3, 0, 1, 4, -24, 0.0, 0.00146484375, 1414352460, 3897116818.973287, 1653070663.2498684, 3897116850.711927, 3897116850.7119865) +- choices 1 and 3: 3. For example decode fc8a47e33656f38fae68f74900000001cabd1423b4e3ab7cb5aa2d4cef46a2666816815f93429ff57f2039e184ff7da5cb70fff... (150 characters): the tool refused; ntplib (3, 7, 4, 138, 71, -29, 13910.951400756836, 44648.9659576416, 1, 3401389091.7065988, 3047828812.9346714, 1746305375.5752354, 2132818401.5195234) +- choices 1, 3 and 4: 2. For example decode d1059c02a02f5d7bc299507f7769a3bb07a922cd1b59adf449580f3885856e571c2c10cceeffaabec4d353dfb8f6f69a0104002... (147 characters): the tool refused; ntplib (3, 2, 1, 5, 156, 2, 41007.365158081055, 49817.31443786621, 2003411899, 128525005.10683715, 1230507832.5215673, 472649932.9335887, 3302183903.7225184) +- choices 1, 2 and 3: 2. For example decode 0f213769582b3f8915ca3dee0fe765113fb9c37cd84408bed6a2c3708a6df0725742b91a4a49c9ed1fe8ad4d3b8f2daf3758940... (147 characters): the tool refused; ntplib (0, 1, 7, 33, 55, 105, 22571.2481842041, 5578.241912841797, 266822929, 1069138812.8447881, 3600991088.54074, 1463990554.2901883, 535342413.2326535) +- choices 1, 2, 3 and 4: 2. For example decode 0eca8cf6ec60478405778a0cd7a2e700000001000000003903ae0a18c9608bbeb2e388256202c3b5e4f60806ae6431c5e700ac0... (147 characters): the tool refused; ntplib (0, 1, 6, 202, 140, -10, 60512.279357910156, 1399.5392456054688, 3617777408, 256.00000001327135, 61737496.78662942, 3001255973.3828545, 3841329158.681... (155 characters) +- choices 2, 3 and 4: 1. For example decode e618a64300010000256cd260bd8c7ea60adb5b1eb7eadb537f06bbe2fe5c1506b4c1325f3178cf672ca6440eccd0d53511a6006... (147 characters): the tool refused; ntplib (3, 4, 6, 24, 166, 67, 1.0, 9580.82177734375, 3180101286, 182147870.7184274, 2131147746.9935925, 3032560223.1932497, 749093902.8000616) + +### Pull request 3 + +The choices of its contract that the model can switch on: + +1. ntp#3 takes the MAC length from the caller (mac_length), so the octets before it must all be extension fields: with mac_length 0, a tail of 20 or 24 octets is a short last field, not a MAC (WIRE-CONTRACT.md) +2. ntp#3 takes a 4-octet MAC of any value, after extension fields too +3. ntp#3 reads extension fields in version 3 +4. ntp#3 reads every mode + +Each vector whose answer differs from the tool's, with the choices that give the vector's answer: + +- `extension-16-with-mac-4` (mac_length 4), choices 1 and 2: the vector fields 65278:000000000000000000000000 MAC 00000000; the tool fields - MAC fefe001000000000000000000000000000000000 +- `opaque-four-byte-nonzero` (mac_length 4), choice 2: the vector fields - MAC 00000001; the tool refused +- `missing-declared-mac` (mac_length 4), choice 1: the vector refused; the tool fields - MAC - +- `final-extension-20-no-mac` (mac_length 0), choice 1: the vector refused; the tool fields - MAC fefe001400000000000000000000000000000000 +- `final-extension-24-no-mac` (mac_length 0), choice 1: the vector refused; the tool fields - MAC fefe00180000000000000000000000000000000000000000 diff --git a/tests/ntp-3-wire-vectors.jsonl b/tests/ntp-3-wire-vectors.jsonl new file mode 100644 index 0000000..4dddb58 --- /dev/null +++ b/tests/ntp-3-wire-vectors.jsonl @@ -0,0 +1,79 @@ +{"expect": {"extensions": [], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "", "mac_length": 0}, "id": "header-distinct-fields", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"error": "short_header"}, "id": "header-short-0", "request": {"mac_length": 0, "packet_hex": ""}} +{"expect": {"error": "short_header"}, "id": "header-short-1", "request": {"mac_length": 0, "packet_hex": "23"}} +{"expect": {"error": "short_header"}, "id": "header-short-2", "request": {"mac_length": 0, "packet_hex": "2310"}} +{"expect": {"error": "short_header"}, "id": "header-short-3", "request": {"mac_length": 0, "packet_hex": "231080"}} +{"expect": {"error": "short_header"}, "id": "header-short-4", "request": {"mac_length": 0, "packet_hex": "231080ff"}} +{"expect": {"error": "short_header"}, "id": "header-short-5", "request": {"mac_length": 0, "packet_hex": "231080ff80"}} +{"expect": {"error": "short_header"}, "id": "header-short-6", "request": {"mac_length": 0, "packet_hex": "231080ff8001"}} +{"expect": {"error": "short_header"}, "id": "header-short-7", "request": {"mac_length": 0, "packet_hex": "231080ff800100"}} +{"expect": {"error": "short_header"}, "id": "header-short-8", "request": {"mac_length": 0, "packet_hex": "231080ff80010002"}} +{"expect": {"error": "short_header"}, "id": "header-short-9", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ff"}} +{"expect": {"error": "short_header"}, "id": "header-short-10", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff"}} +{"expect": {"error": "short_header"}, "id": "header-short-11", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00"}} +{"expect": {"error": "short_header"}, "id": "header-short-12", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001"}} +{"expect": {"error": "short_header"}, "id": "header-short-13", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152"}} +{"expect": {"error": "short_header"}, "id": "header-short-14", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241"}} +{"expect": {"error": "short_header"}, "id": "header-short-15", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154"}} +{"expect": {"error": "short_header"}, "id": "header-short-16", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445"}} +{"expect": {"error": "short_header"}, "id": "header-short-17", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500"}} +{"expect": {"error": "short_header"}, "id": "header-short-18", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000"}} +{"expect": {"error": "short_header"}, "id": "header-short-19", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-20", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-21", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-22", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-23", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-24", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-25", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-26", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500000000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-27", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000000000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-28", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-29", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500000000000000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-30", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000000000000000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-31", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-32", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500000000000000000000000000000001"}} +{"expect": {"error": "short_header"}, "id": "header-short-33", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000000000000000000000000000000180"}} +{"expect": {"error": "short_header"}, "id": "header-short-34", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000"}} +{"expect": {"error": "short_header"}, "id": "header-short-35", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500000000000000000000000000000001800000"}} +{"expect": {"error": "short_header"}, "id": "header-short-36", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000000000000000000000000000000180000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-37", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-38", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff00015241544500000000000000000000000000000001800000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-39", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff0001524154450000000000000000000000000000000180000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-40", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000"}} +{"expect": {"error": "short_header"}, "id": "header-short-41", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ff"}} +{"expect": {"error": "short_header"}, "id": "header-short-42", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffff"}} +{"expect": {"error": "short_header"}, "id": "header-short-43", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffff"}} +{"expect": {"error": "short_header"}, "id": "header-short-44", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffff"}} +{"expect": {"error": "short_header"}, "id": "header-short-45", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffff"}} +{"expect": {"error": "short_header"}, "id": "header-short-46", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffff"}} +{"expect": {"error": "short_header"}, "id": "header-short-47", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffff"}} +{"expect": {"error": "unsupported_version"}, "id": "version-0", "request": {"mac_length": 0, "packet_hex": "031080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"error": "unsupported_version"}, "id": "version-1", "request": {"mac_length": 0, "packet_hex": "0b1080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"error": "unsupported_version"}, "id": "version-2", "request": {"mac_length": 0, "packet_hex": "131080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"extensions": [], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 3}, "mac_hex": "", "mac_length": 0}, "id": "version-3", "request": {"mac_length": 0, "packet_hex": "1b1080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"extensions": [], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "", "mac_length": 0}, "id": "version-4", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"error": "unsupported_version"}, "id": "version-5", "request": {"mac_length": 0, "packet_hex": "2b1080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"error": "unsupported_version"}, "id": "version-6", "request": {"mac_length": 0, "packet_hex": "331080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"error": "unsupported_version"}, "id": "version-7", "request": {"mac_length": 0, "packet_hex": "3b1080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"extensions": [], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "00000000", "mac_length": 4}, "id": "opaque-mac-4", "request": {"mac_length": 4, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff00000000"}} +{"expect": {"extensions": [{"length": 16, "offset": 48, "type": 65278, "value_and_padding_hex": "000000000000000000000000"}], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "00000000", "mac_length": 4}, "id": "extension-16-with-mac-4", "request": {"mac_length": 4, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe001000000000000000000000000000000000"}} +{"expect": {"extensions": [], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "fefe001400000000000000000000000000000000", "mac_length": 20}, "id": "opaque-mac-20", "request": {"mac_length": 20, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe001400000000000000000000000000000000"}} +{"expect": {"extensions": [{"length": 16, "offset": 48, "type": 65278, "value_and_padding_hex": "000000000000000000000000"}], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "fefe001400000000000000000000000000000000", "mac_length": 20}, "id": "extension-16-with-mac-20", "request": {"mac_length": 20, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0010000000000000000000000000fefe001400000000000000000000000000000000"}} +{"expect": {"extensions": [], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "fefe00180000000000000000000000000000000000000000", "mac_length": 24}, "id": "opaque-mac-24", "request": {"mac_length": 24, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe00180000000000000000000000000000000000000000"}} +{"expect": {"extensions": [{"length": 16, "offset": 48, "type": 65278, "value_and_padding_hex": "000000000000000000000000"}], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "fefe00180000000000000000000000000000000000000000", "mac_length": 24}, "id": "extension-16-with-mac-24", "request": {"mac_length": 24, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0010000000000000000000000000fefe00180000000000000000000000000000000000000000"}} +{"expect": {"extensions": [], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "00000001", "mac_length": 4}, "id": "opaque-four-byte-nonzero", "request": {"mac_length": 4, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff00000001"}} +{"expect": {"error": "short_mac"}, "id": "missing-declared-mac", "request": {"mac_length": 4, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000ffffffffffffffff"}} +{"expect": {"error": "short_final_extension_without_mac"}, "id": "final-extension-16-no-mac", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0010000000000000000000000000"}} +{"expect": {"error": "short_final_extension_without_mac"}, "id": "final-extension-20-no-mac", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe001400000000000000000000000000000000"}} +{"expect": {"error": "short_final_extension_without_mac"}, "id": "final-extension-24-no-mac", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe00180000000000000000000000000000000000000000"}} +{"expect": {"extensions": [{"length": 28, "offset": 48, "type": 65278, "value_and_padding_hex": "000000000000000000000000000000000000000000000000"}], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "", "mac_length": 0}, "id": "final-extension-28-no-mac", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe001c000000000000000000000000000000000000000000000000"}} +{"expect": {"extensions": [{"length": 32, "offset": 48, "type": 65278, "value_and_padding_hex": "00000000000000000000000000000000000000000000000000000000"}], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "", "mac_length": 0}, "id": "final-extension-32-no-mac", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe002000000000000000000000000000000000000000000000000000000000"}} +{"expect": {"extensions": [{"length": 16, "offset": 48, "type": 65278, "value_and_padding_hex": "000000000000000000000000"}, {"length": 28, "offset": 64, "type": 65278, "value_and_padding_hex": "000000000000000000000000000000000000000000000000"}], "header": {"leap": 0, "mode": 3, "poll": -128, "precision": -1, "reference_id_hex": "52415445", "root_delay_bits": 2147549186, "root_dispersion_bits": 4294901761, "stratum": 16, "timestamps": [{"fraction": 0, "raw": 0, "seconds": 0, "unknown": true}, {"fraction": 1, "raw": 1, "seconds": 0, "unknown": false}, {"fraction": 0, "raw": 9223372036854775808, "seconds": 2147483648, "unknown": false}, {"fraction": 4294967295, "raw": 18446744073709551615, "seconds": 4294967295, "unknown": false}], "version": 4}, "mac_hex": "", "mac_length": 0}, "id": "two-extensions-no-mac", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0010000000000000000000000000fefe001c000000000000000000000000000000000000000000000000"}} +{"expect": {"error": "short_final_extension_without_mac"}, "id": "two-short-extensions-no-mac", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0010000000000000000000000000fefe0010000000000000000000000000"}} +{"expect": {"error": "short_extension"}, "id": "extension-short-length-0", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0000000000000000000000000000000000000000000000000000"}} +{"expect": {"error": "short_extension"}, "id": "extension-short-length-4", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0004000000000000000000000000000000000000000000000000"}} +{"expect": {"error": "short_extension"}, "id": "extension-short-length-12", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe000c000000000000000000000000000000000000000000000000"}} +{"expect": {"error": "unaligned_extension"}, "id": "extension-unaligned-length", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe001d00000000000000000000000000000000000000000000000000000000"}} +{"expect": {"error": "extension_overrun"}, "id": "extension-overrun", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe0020000000000000000000000000000000000000000000000000"}} +{"expect": {"error": "short_extension_header"}, "id": "extension-partial-header", "request": {"mac_length": 0, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe"}} +{"expect": {"error": "extension_overrun"}, "id": "extension-cannot-consume-mac", "request": {"mac_length": 20, "packet_hex": "231080ff80010002ffff000152415445000000000000000000000000000000018000000000000000fffffffffffffffffefe001c0000000000000000000000000000000000000000000000000000000000000000"}} diff --git a/tests/ntpd-rs/Cargo.lock b/tests/ntpd-rs/Cargo.lock new file mode 100644 index 0000000..53bb4b1 --- /dev/null +++ b/tests/ntpd-rs/Cargo.lock @@ -0,0 +1,907 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-siv" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e08d0cdb774acd1e4dac11478b1a0c0d203134b2aab0ba25eb430de9b18f8b9" +dependencies = [ + "aead", + "aes", + "cipher", + "cmac", + "ctr", + "dbl", + "digest", + "zeroize", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "dbl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "ntp-proto" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73f4f1d273d1cd34c8d821c2767c949d415b91935948f97166ba27cf6628ac0" +dependencies = [ + "aead", + "aes-siv", + "md-5", + "rand", + "rustls", + "rustls-platform-verifier", + "serde", + "tokio", + "tokio-rustls", + "tracing", + "zeroize", +] + +[[package]] +name = "ntpdrs-oracle" +version = "0.1.0" +dependencies = [ + "ntp-proto", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "pin-project-lite", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.11", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" diff --git a/tests/ntpd-rs/Cargo.toml b/tests/ntpd-rs/Cargo.toml new file mode 100644 index 0000000..9e95045 --- /dev/null +++ b/tests/ntpd-rs/Cargo.toml @@ -0,0 +1,15 @@ +# A small program that reads NTP packets with the packet parser of +# ntpd-rs (the ntp-proto crate), for tests/run_tests.py. Cargo.lock pins +# every crate it builds with. The feature __internal-api is the one +# ntpd-rs's own daemon uses to reach the packet type. +[package] +name = "ntpdrs-oracle" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +ntp-proto = { version = "=1.9.0", default-features = false, features = ["rustcrypto", "__internal-api"] } + +[profile.release] +debug-assertions = false diff --git a/tests/ntpd-rs/src/main.rs b/tests/ntpd-rs/src/main.rs new file mode 100644 index 0000000..a59454c --- /dev/null +++ b/tests/ntpd-rs/src/main.rs @@ -0,0 +1,190 @@ +//! Reads NTP packets with ntpd-rs's packet parser, `NtpPacket::deserialize` +//! in the ntp-proto crate, for tests/run_tests.py. +//! +//! Each line of standard input is one UDP payload in lower-case hex (`-` +//! for none). For each, one line of standard output: +//! +//! FAIL ERROR the parser refused it, with its error +//! OK V LI MODE STRATUM POLL PRECISION DELAY DISPERSION REFID REF ORG REC XMT FIELDS MAC NTS +//! +//! - V, LI, MODE, STRATUM, POLL, PRECISION: decimal, as the parser read +//! them (POLL and PRECISION signed); +//! - DELAY, DISPERSION: the 32 bits of each in NTP short format, decimal. +//! The crate keeps them only as a duration, so they are taken from the +//! header its own writer, `NtpPacket::serialize`, writes for the packet; +//! - REFID: the reference ID, decimal; REF ORG REC XMT: the four +//! timestamps, decimal, each its 64 bits as the crate keeps them; +//! - FIELDS: `-`, or each extension field as TYPE:HEX, comma separated, +//! TYPE in decimal and HEX its value and padding as the crate gives +//! them (`-` for none, `*` for an NTS authenticator field, whose +//! contents the crate does not give without a key); +//! - MAC: `-`, or the key identifier and digest in hex; +//! - NTS: `nts-unverified` when the parser gave the packet back with a +//! decryption error (an NTS authenticator field it had no key for), +//! else `-`. +//! +//! The parser is given no key (`NoCipher`). + +use std::fmt::Write as _; +use std::io::{BufRead, Cursor, Write}; + +use ntp_proto::{NoCipher, NtpPacket, NtpVersion, PacketParsingError}; + +fn hex(bytes: &[u8]) -> String { + if bytes.is_empty() { + return "-".to_string(); + } + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + write!(s, "{b:02x}").unwrap(); + } + s +} + +fn unhex(s: &str) -> Option> { + if s == "-" { + return Some(Vec::new()); + } + if s.len() % 2 != 0 { + return None; + } + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()) + .collect() +} + +/// The number in `NAME(n)` within a Debug string, after `key`. +fn debug_number(text: &str, key: &str, name: &str) -> String { + let start = text.find(key).expect("field in Debug output") + key.len(); + let rest = &text[start..]; + let open = rest.find(name).expect("type name") + name.len() + 1; + let close = rest[open..].find(')').expect("closing parenthesis") + open; + rest[open..close].to_string() +} + +/// The bytes of a Debug list, `[1, 2, 3]`. +fn debug_bytes(text: &str) -> Vec { + let open = text.find('[').expect("a list") + 1; + let close = text[open..].find(']').expect("end of list") + open; + text[open..close] + .split(", ") + .filter(|s| !s.is_empty()) + .map(|b| b.parse().expect("a byte")) + .collect() +} + +/// An extension field as TYPE:HEX, from its Debug output: the crate +/// exports its type only to fuzzers, so its variants cannot be named +/// here. +fn field(debug: &str) -> String { + if let Some(rest) = debug.strip_prefix("UniqueIdentifier(") { + format!("{}:{}", 0x0104, hex(&debug_bytes(rest))) + } else if let Some(rest) = debug.strip_prefix("NtsCookie(") { + format!("{}:{}", 0x0204, hex(&debug_bytes(rest))) + } else if let Some(rest) = debug.strip_prefix("NtsCookiePlaceholder { body_length: ") { + let n: usize = rest.trim_end_matches(" }").parse().expect("a length"); + format!("{}:{}", 0x0304, hex(&vec![0u8; n])) + } else if debug.starts_with("InvalidNtsEncryptedField") { + format!("{}:*", 0x0404) + } else if let Some(rest) = debug.strip_prefix("Unknown { typeid: ") { + let t: u16 = rest[..rest.find(',').expect("a comma")].parse().expect("a type"); + format!("{}:{}", t, hex(&debug_bytes(rest))) + } else { + format!("?{}", debug.replace(' ', "_")) + } +} + +fn show(packet: &NtpPacket<'_>, nts: bool) -> String { + let version = match packet.version() { + NtpVersion::V3 => 3, + NtpVersion::V4 => 4, + NtpVersion::V5 => return format!("V5 {}", format!("{packet:?}").replace(' ', "_")), + }; + let debug = format!("{:?}", packet.header()); + // The header as the crate's own writer writes it, for the two + // short-format fields. + let mut buffer = vec![0u8; 1 << 17]; + let mut cursor = Cursor::new(buffer.as_mut_slice()); + let _ = packet.serialize(&mut cursor, &NoCipher, None); + let w = cursor.into_inner(); + let delay = u32::from_be_bytes(w[4..8].try_into().unwrap()); + let dispersion = u32::from_be_bytes(w[8..12].try_into().unwrap()); + let leap = match packet.leap() { + ntp_proto::NtpLeapIndicator::NoWarning => 0, + ntp_proto::NtpLeapIndicator::Leap61 => 1, + ntp_proto::NtpLeapIndicator::Leap59 => 2, + ntp_proto::NtpLeapIndicator::Unknown | ntp_proto::NtpLeapIndicator::Unsynchronized => 3, + }; + let mode = match packet.mode() { + ntp_proto::NtpAssociationMode::Reserved => 0, + ntp_proto::NtpAssociationMode::SymmetricActive => 1, + ntp_proto::NtpAssociationMode::SymmetricPassive => 2, + ntp_proto::NtpAssociationMode::Client => 3, + ntp_proto::NtpAssociationMode::Server => 4, + ntp_proto::NtpAssociationMode::Broadcast => 5, + ntp_proto::NtpAssociationMode::Control => 6, + ntp_proto::NtpAssociationMode::Private => 7, + }; + let fields: Vec = packet + .authenticated_extension_fields() + .chain(packet.untrusted_extension_fields()) + .map(|ef| field(&format!("{ef:?}"))) + .collect(); + let fields = if fields.is_empty() { "-".to_string() } else { fields.join(",") }; + // The MAC is not reachable through the crate's API but its Debug + // output: `mac: None` or `mac: Some(Mac { keyid: K, mac: [..] })`. + let all = format!("{packet:?}"); + let key = "mac: Some(Mac { keyid: "; + let mac = match all.rfind(key) { + None => { + assert!(all.ends_with("mac: None }"), "a MAC or none in Debug output"); + "-".to_string() + } + Some(i) => { + let m = &all[i + key.len()..]; + let keyid: u32 = m[..m.find(',').expect("a comma")].parse().expect("a key ID"); + let mut bytes = keyid.to_be_bytes().to_vec(); + bytes.extend(debug_bytes(m)); + hex(&bytes) + } + }; + format!( + "OK {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}", + version, + leap, + mode, + packet.stratum(), + packet.poll().as_log(), + packet.precision(), + delay, + dispersion, + debug_number(&debug, "reference_id: ", "ReferenceId"), + debug_number(&debug, "reference_timestamp: ", "NtpTimestamp"), + debug_number(&debug, "origin_timestamp: ", "NtpTimestamp"), + debug_number(&debug, "receive_timestamp: ", "NtpTimestamp"), + debug_number(&debug, "transmit_timestamp: ", "NtpTimestamp"), + fields, + mac, + if nts { "nts-unverified" } else { "-" } + ) +} + +fn main() { + let stdin = std::io::stdin(); + let stdout = std::io::stdout(); + let mut out = std::io::BufWriter::new(stdout.lock()); + for line in stdin.lock().lines() { + let line = line.expect("a line of input"); + let answer = match unhex(line.trim_end_matches('\r')) { + None => "error".to_string(), + Some(data) => match NtpPacket::deserialize(&data, &NoCipher) { + Ok((packet, _)) => show(&packet, false), + Err(PacketParsingError::DecryptError(packet)) => show(&packet, true), + Err(e) => format!("FAIL {e:?}"), + }, + }; + writeln!(out, "{answer}").unwrap(); + } + out.flush().unwrap(); +} diff --git a/tests/plant_bugs.py b/tests/plant_bugs.py new file mode 100644 index 0000000..7577702 --- /dev/null +++ b/tests/plant_bugs.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +"""Check that the differential tests catch planted bugs. + +Each bug is a one-line change to Ntp/Packet.lean, or to Ntp/Time.lean +or Main.lean where its entry says so. The script copies the project to a +temporary folder, without .lake/build, .lake/oracles and .git, plants +one bug at a time, runs tests/run_tests.py there with a small count and +without the oracles, and records how many failures the tests report. So +every failure counted is the tool disagreeing with the model of RFC 5905 +and RFC 7822. Every planted bug must give at least one failure. The +unchanged copy must give none. A bug that stops the executable from +building is not a catch: the tests then give up, and the bug counts as +missed. So is any run that gives up (its record shows `- Failures: 1 +(why)`), a run that dies without writing its record, and one whose exit +status does not match the failures it records. Every run must also test +the same inputs as the unchanged copy: its record must give the same +`- Inputs SHA-256:` line, or the bug counts as missed, since its +failures might come from inputs the unchanged copy was not run on. The +tests build only the executable (`lake build ntp`), so a bug that breaks +a proof still runs. + + python3 tests/plant_bugs.py [--count N] + +The script writes tests/PLANTED-BUGS.md and exits non-zero if any bug +goes uncaught. +""" + +import argparse +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +RECORD = ROOT / "tests" / "PLANTED-BUGS.md" +CODE = "Ntp/Packet.lean" +TIME = "Ntp/Time.lean" +MAIN = "Main.lean" + +# (what the bug does, the text as written, the text with the bug[, file]) +BUGS = [ + ("an input shorter than asked for is taken whole, as a reader that runs off the end", + " if c.length = w then some (c, b.drop w) else none", + " if c.length ≤ w then some (c, b.drop w) else none"), + ("numbers are read little-endian", + " bs.foldl (fun n b => n * 256 + b.toNat) 0", + " bs.foldr (fun b n => n * 256 + b.toNat) 0"), + ("numbers are written little-endian", + " | w + 1, n => writeBE w (n / 256) ++ [(n % 256).toUInt8]", + " | w + 1, n => [(n % 256).toUInt8] ++ writeBE w (n / 256)"), + ("the octet 128 is read as +128, not -128", + " if n < 128 then n else (n : Int) - 256", + " if n ≤ 128 then n else (n : Int) - 256"), + ("a negative number is written one less", + " if z < 0 then (z + 256).toNat else z.toNat", + " if z < 0 then (z + 255).toNat else z.toNat"), + ("the version is read from two bits", + " let version := lvm / 8 % 8", + " let version := lvm / 8 % 4"), + ("the mode is read from two bits", + " let mode := lvm % 8", + " let mode := lvm % 4"), + ("version 2 is accepted", + " guard ((version == 3 || version == 4) && 1 ≤ mode && mode ≤ 5)", + " guard ((version == 2 || version == 3 || version == 4) && 1 ≤ mode && mode ≤ 5)"), + ("version 5 is accepted", + " guard ((version == 3 || version == 4) && 1 ≤ mode && mode ≤ 5)", + " guard (3 ≤ version && version ≤ 5 && 1 ≤ mode && mode ≤ 5)"), + ("mode 0 is accepted", + " guard ((version == 3 || version == 4) && 1 ≤ mode && mode ≤ 5)", + " guard ((version == 3 || version == 4) && 0 ≤ mode && mode ≤ 5)"), + ("mode 6 is accepted", + " guard ((version == 3 || version == 4) && 1 ≤ mode && mode ≤ 5)", + " guard ((version == 3 || version == 4) && 1 ≤ mode && mode ≤ 6)"), + ("mode 7 is accepted", + " guard ((version == 3 || version == 4) && 1 ≤ mode && mode ≤ 5)", + " guard ((version == 3 || version == 4) && (1 ≤ mode && mode ≤ 5 || mode == 7))"), + ("the leap indicator keeps only its low bit", + " pure ({ leap := lvm / 64, version, mode, stratum, poll := toSigned poll,", + " pure ({ leap := lvm / 64 % 2, version, mode, stratum, poll := toSigned poll,"), + ("Poll is read as unsigned, as NTPsec and ntplib read it", + " pure ({ leap := lvm / 64, version, mode, stratum, poll := toSigned poll,", + " pure ({ leap := lvm / 64, version, mode, stratum, poll := poll,"), + ("Precision is read as unsigned", + " precision := toSigned precision, rootDelay, rootDispersion, refId, reference,", + " precision := precision, rootDelay, rootDispersion, refId, reference,"), + ("the root delay and the root dispersion are swapped", + " precision := toSigned precision, rootDelay, rootDispersion, refId, reference,", + " precision := toSigned precision, rootDelay := rootDispersion, rootDispersion := rootDelay, " + "refId, reference,"), + ("the origin and receive timestamps are swapped", + " origin, receive, transmit }, r)", + " origin := receive, receive := origin, transmit }, r)"), + ("16 octets after the fields are taken as a MAC", + " t.length == 0 || t.length == 20 || t.length == 24", + " t.length == 0 || t.length == 16 || t.length == 20 || t.length == 24"), + ("21 to 23 octets after the fields are taken as a MAC, as chrony's 20 to 24 would", + " t.length == 0 || t.length == 20 || t.length == 24", + " t.length == 0 || (20 ≤ t.length && t.length ≤ 24)"), + ("a field is read while 20 octets are left, not 24, so 24 octets are a field", + " if longer 24 t then do", + " if longer 20 t then do"), + ("a field is read only while 28 octets are left, so 28 octets are a MAC", + " if longer 24 t then do", + " if longer 28 t then do"), + ("a field is read while 16 or more octets are left, as Wireshark reads them, so a last field " + "without a MAC may be 16 octets", + " if longer 24 t then do", + " if longer 15 t then do"), + ("a field of Length 12 is accepted", + " guard (16 ≤ len && len % 4 == 0)", + " guard (12 ≤ len && len % 4 == 0)"), + ("a field of Length 4 is accepted, as ntpd-rs accepts it", + " guard (16 ≤ len && len % 4 == 0)", + " guard (4 ≤ len && len % 4 == 0)"), + ("a field Length need not be a multiple of 4", + " guard (16 ≤ len && len % 4 == 0)", + " guard (16 ≤ len && len % 2 == 0)"), + ("a field Length past the end is cut short at the end, as a clamping reader would", + " let (body, r) ← take? (len - 4) r", + " let (body, r) := (r.take (len - 4), r.drop (len - 4))"), + ("a field's body is taken as Length less 8", + " let (body, r) ← take? (len - 4) r", + " let (body, r) ← take? (len - 8) r"), + ("the fields come back in reverse order", + " pure ({ type, body } :: fields, mac)", + " pure (fields ++ [{ type, body }], mac)"), + ("a crypto-NAK is taken after fields too", + " else if macLength t then some ([], t) else none", + " else if macLength t || t = [0, 0, 0, 0] then some ([], t) else none"), + ("four octets of any value are a crypto-NAK", + " if t = [0, 0, 0, 0] then some ([], t)", + " if t.length = 4 then some ([], t)"), + ("version 3 packets get extension fields, as ntpd reads a tail of more than 24 octets", + " else if version = 4 then readFields (t.length + 1) t", + " else if version = 4 || version = 3 then readFields (t.length + 1) t"), + ("at most two extension fields are read", + " else if version = 4 then readFields (t.length + 1) t", + " else if version = 4 then readFields 3 t"), + ("in version 3, any 24 octets or fewer are the MAC, as ntpd-rs reads them", + " else if macLength t then some ([], t)\n", + " else if t.length ≤ 24 then some ([], t)\n"), + ("version 3 is read with the version 4 rule", + " let (exts, mac) ← readTail header.version t", + " let (exts, mac) ← readTail 4 t"), + ("a field's Length is written without its four header octets", + " writeBE 2 e.type ++ writeBE 2 (e.body.length + 4) ++ e.body", + " writeBE 2 e.type ++ writeBE 2 e.body.length ++ e.body"), + ("the mode is written from two bits", + " writeBE 1 (h.leap * 64 + h.version * 8 + h.mode) ++ writeBE 1 h.stratum ++", + " writeBE 1 (h.leap * 64 + h.version * 8 + h.mode % 4) ++ writeBE 1 h.stratum ++"), + ("the root dispersion is written before the root delay", + " writeBE 4 h.rootDelay ++ writeBE 4 h.rootDispersion ++ writeBE 4 h.refId ++", + " writeBE 4 h.rootDispersion ++ writeBE 4 h.rootDelay ++ writeBE 4 h.refId ++"), + ("the MAC is not written", + " p.exts.flatMap writeField ++ p.mac", + " p.exts.flatMap writeField"), + ("the raw value 0 converts to 1900, in Ntp/Time.lean", + " if raw = 0 ∨ 18446744073709551616 ≤ raw then none", + " if 18446744073709551616 ≤ raw then none", TIME), + ("a raw value of 2^64 converts, in Ntp/Time.lean", + " if raw = 0 ∨ 18446744073709551616 ≤ raw then none", + " if raw = 0 ∨ 18446744073709551617 ≤ raw then none", TIME), + ("the era is left out of the conversion, in Ntp/Time.lean", + " else some (era * 4294967296 + (raw / 4294967296 : Nat) - 2208988800, raw % 4294967296)", + " else some ((raw / 4294967296 : Nat) - 2208988800, raw % 4294967296)", TIME), + ("the epoch is a day off, in Ntp/Time.lean", + " else some (era * 4294967296 + (raw / 4294967296 : Nat) - 2208988800, raw % 4294967296)", + " else some (era * 4294967296 + (raw / 4294967296 : Nat) - 2208902400, raw % 4294967296)", TIME), + ("the fraction is taken modulo 2^32 - 1, in Ntp/Time.lean", + " else some (era * 4294967296 + (raw / 4294967296 : Nat) - 2208988800, raw % 4294967296)", + " else some (era * 4294967296 + (raw / 4294967296 : Nat) - 2208988800, raw % 4294967295)", TIME), + ("a fraction of 2^32 is taken, in Ntp/Time.lean", + " if 4294967296 ≤ fraction then none", + " if 4294967297 ≤ fraction then none", TIME), + ("the era is found by division rounding toward zero, in Ntp/Time.lean", + " let raw := (s % 4294967296).toNat * 4294967296 + fraction", + " let raw := (s.tmod 4294967296).toNat * 4294967296 + fraction", TIME), + ("the first instant of an era gets the timestamp 0, in Ntp/Time.lean", + " if raw = 0 then none else some (s / 4294967296, raw)", + " some (s / 4294967296, raw)", TIME), + ("Main.lean: upper-case hex digits are read", + " else if 'a' ≤ c ∧ c ≤ 'f' then some (c.toNat - 'a'.toNat + 10)", + " else if 'a' ≤ c.toLower ∧ c.toLower ≤ 'f' then some (c.toLower.toNat - 'a'.toNat + 10)", MAIN), + ("Main.lean: an empty hex field is read as no octets", + " if f = \"-\" then some [] else if f.isEmpty then none else (hexBytes f.toList #[]).map (·.toList)", + " if f = \"-\" ∨ f.isEmpty then some [] else (hexBytes f.toList #[]).map (·.toList)", MAIN), + ("Main.lean: hex is printed in upper case", + " if n < 10 then Char.ofNat ('0'.toNat + n) else Char.ofNat ('a'.toNat + n - 10)", + " if n < 10 then Char.ofNat ('0'.toNat + n) else Char.ofNat ('A'.toNat + n - 10)", MAIN), + ("Main.lean: the two hex digits of an octet are printed the wrong way round", + " else String.ofList (bs.flatMap fun b => [hexDigit (b.toNat / 16), hexDigit (b.toNat % 16)])", + " else String.ofList (bs.flatMap fun b => [hexDigit (b.toNat % 16), hexDigit (b.toNat / 16)])", MAIN), + ("Main.lean: the sign of a negative number is dropped", + " | '-' :: cs => (decimal (String.ofList cs)).map fun n => -(n : Int)", + " | '-' :: cs => (decimal (String.ofList cs)).map fun n => (n : Int)", MAIN), + ("Main.lean: Poll and Precision are printed the wrong way round", + " toString h.poll, toString h.precision, toString h.rootDelay, toString h.rootDispersion,", + " toString h.precision, toString h.poll, toString h.rootDelay, toString h.rootDispersion,", MAIN), + ("Main.lean: `-` for no extension fields is not read", + " if s = \"-\" then some [] else (s.splitOn \",\").mapM readField", + " (s.splitOn \",\").mapM readField", MAIN), + ("Main.lean: `encode` reads the root delay and dispersion the wrong way round", + " rootDelay := ← decimal rd, rootDispersion := ← decimal rs, refId := ← decimal ri,", + " rootDelay := ← decimal rs, rootDispersion := ← decimal rd, refId := ← decimal ri,", MAIN), + ("Main.lean: `encode` answers OK for a packet that is not valid", + " pure (if decode b = some p then s!\"OK {toHex b}\" else \"FAIL\")", + " pure s!\"OK {toHex b}\"", MAIN), + ("Main.lean: the CR of a CR LF line ending is kept", + " String.ofList (if cs.getLast? = some '\\r' then cs.dropLast else cs)", + " String.ofList cs", MAIN), + ("Main.lean: wrong arguments exit with 0", + " | _ => IO.eprintln usage; return 2", + " | _ => IO.eprintln usage; return 0", MAIN), +] + + +def failures(copy, count): + """The failures a run of the tests reports, or None, with the run's + output, when it reports none: the code did not build, or the run did + not finish; and the SHA-256 of the inputs the run made. A run that + dies writes no record, so the old one is removed first rather than + read again.""" + record = copy / "tests" / "TEST-RESULTS.md" + record.unlink(missing_ok=True) + run = subprocess.run([sys.executable, "tests/run_tests.py", "--count", str(count), "--no-oracles"], + cwd=copy, capture_output=True, text=True) + out = run.stdout + run.stderr + if not record.exists(): + return None, f"no record written, exit status {run.returncode}\n{out}", None + text = record.read_text(encoding="utf-8") + m = re.search(r"^- Failures: (\d+)(.*)$", text, re.M) + if m is None or m.group(2).strip(): + return None, out, None # the run gave up, for example the bug did not build: not a catch + d = re.search(r"^- Inputs SHA-256: ([0-9a-f]{64})", text, re.M) + n = int(m.group(1)) + if run.returncode != (1 if n else 0): + return None, f"{n} failures but exit status {run.returncode}\n{out}", None + return n, None, d and d.group(1) + + +def ignore(folder, names): + """Leave out the build, the oracle builds, the git folder and old + records: the copy builds its own executable.""" + leave = {"PLANTED-BUGS.md", "__pycache__", ".git"} + if Path(folder).resolve() == (ROOT / ".lake").resolve(): + leave |= {"build", "oracles"} + return [n for n in names if n in leave] + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--count", type=int, default=2000) + args = ap.parse_args() + rows, missed = [], 0 + with tempfile.TemporaryDirectory() as tmp: + copy = Path(tmp) / "ntp" + shutil.copytree(ROOT, copy, symlinks=True, ignore=ignore) + n, err, inputs = failures(copy, args.count) + rows.append(("none", n, "")) + if n != 0 or inputs is None: + missed += 1 + print(f"none: {n} failures, inputs {inputs}" + (" (did not build or did not finish)" if err else ""), + flush=True) + if err: + print(err[-2000:], flush=True) + for what, line, bug, *where in BUGS: + name = where[0] if where else CODE + code = copy / name + original = code.read_text(encoding="utf-8") + if original.count(line) != 1: + sys.exit(f"{name} no longer holds, once, the line for {what!r}: {line}") + code.write_text(original.replace(line, bug), encoding="utf-8") + n, err, got = failures(copy, args.count) + code.write_text(original, encoding="utf-8") + note = "" if err or got == inputs else " (other inputs than the unchanged copy's)" + rows.append((what, n, note)) + if not n or note: + missed += 1 + print(f"{what}: {n} failures{note}" + (" (did not build or did not finish)" if err else ""), + flush=True) + if err: + print(err[-2000:], flush=True) + counts = {f: sum(1 for b in BUGS if (b[3] if len(b) > 3 else CODE) == f) for f in (CODE, TIME, MAIN)} + lines = ["# Planted bugs", "", + f"Written by `tests/plant_bugs.py`. Each bug is a change of one line, to `{CODE}` " + f"({counts[CODE]} bugs), `{TIME}` ({counts[TIME]}) or `{MAIN}` ({counts[MAIN]}), tested with " + f"`tests/run_tests.py --count {args.count} --no-oracles`, which builds only the executable and " + "compares it with the model alone. Every bug must give failures, and the unchanged code none. " + "A bug that does not build, or a run that gives up or dies, counts as missed. So does a run on " + "other inputs than the unchanged code's: every run must give the same `- Inputs SHA-256:` line, " + f"which here is {inputs or 'missing'}.", "", + "| Planted bug | Failures |", "|---|---|"] + lines += [f"| {what} | {'did not build or did not finish' if n is None else n}{note} |" + for what, n, note in rows] + lines += ["", f"Bugs caught: {sum(1 for what, n, note in rows[1:] if n and not note)} of {len(BUGS)}", ""] + RECORD.write_text("\n".join(lines), encoding="utf-8") + return 1 if missed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..0d01a60 --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,2161 @@ +#!/usr/bin/env python3 +"""Differential tests: the Lean reader and writer of the NTP packet +against a model of RFC 5905 and RFC 7822, and against other NTP +implementations. + +The tool answers `decode`, `encode`, `unix` and `fromunix` requests +(see the header of Main.lean). Its answers are checked three ways. + +1. Against the model, written in Python from RFC 5905 (Figure 8, + sections 6, 7.3 and 9.2), RFC 7822 (sections 3 and 7.5.1.3 to + 7.5.1.4) and Spec/Defs.lean, sharing nothing with the Lean code: + - `valid` and `packet_bytes` are Spec/Defs.lean's `Valid` and + `packetBytes`: the header of Figure 8, big-endian; each extension + field its Field Type, its Length (the whole field) and its Value and + Padding, of a Length that is a multiple of 4 from 16 to 65532; a MAC + of none, 20 or 24 octets, or four zero octets (the crypto-NAK) with + no field; no field in version 3; a last field of at least 28 octets + when there is no MAC; + - `decode_spec` finds the packet that `Spec.decode_iff` says the + decoder must give, from those two alone: for each MAC length that + `valid` allows, it splits the octets before the MAC into fields by + their Length fields, and keeps the packets that are valid and whose + octets are the input. There is at most one, by `Spec.decode_iff`, + and the script fails if it finds two; + - `read` is the one-pass reading SPEC.md describes ("Reading the + octets after the header"): four zero octets are a crypto-NAK; in + version 3 the MAC is 0, 20 or 24 octets; in version 4, while more + than 24 octets are left, a field starts there. On every input it + must give what `decode_spec` gives (a check of SPEC.md's claim that + the rule accepts exactly the valid packets), and it gives the reason + the record counts for each refusal. With switches, each named for a + choice of one of the oracles below, it reads as that oracle does; + - `to_unix` and `from_unix` are RFC 5905 section 6 in exact rational + arithmetic: "s = era * 2^(32) + timestamp", less the 2,208,988,800 + seconds from 1900 to 1970 (Figure 4), with "era = s / 2^(32)" + rounding down; the raw value 0, "unknown or unsynchronized time", + has no Unix time; nor does a value of 2^64 or more, and a fraction + of 2^32 or more has no timestamp; + - the request parsing and the output of the header of Main.lean. + The model must give the answers Spec.lean states in `examples` and + `rejects`. Every answer of the tool must equal the model's. + +2. Against other readers of the packet, on every `decode` input. Each + gives its answer in its own way, and the script brings it to one + form: refused, or the header fields, the extension fields (type and + octets), the MAC, and the octets it shows nowhere. Each oracle + chooses differently from this reader in places, and the model has a + switch for each such choice, named in ORACLE_RULES. The model with + all of an oracle's switches on must give that oracle's answer on + every input, whether or not the tool agrees; any other answer is a + failure. Where the tool and an oracle differ, the difference is + counted under the smallest set of that oracle's switches with which + the model gives the oracle's answer; a difference no set explains is + a failure. The oracles: + - ntpd-rs: the packet parser of its ntp-proto crate, + `NtpPacket::deserialize`, in the small program in tests/ntpd-rs + (its Cargo.lock pins every crate), built with `cargo build + --release --locked`; + - NTPsec: `ntp.packet.SyncPacket.analyze`, from the Python module of + NTPsec (Debian and Ubuntu package python3-ntp); + - Wireshark: its NTP dissector, through tshark, which reads a + capture of every input with the dissector registered for a user + link type, and prints PDML; a packet is refused when Wireshark + marks it malformed or adds an expert note to it, or reads it as a + control or private message (modes 6 and 7); + - ntplib, for the header fields only: `NTPPacket.from_data`, with the + fields as it keeps them (root delay and dispersion divided by 2^16, + timestamps as floats). + An oracle that is not installed, or not at the pinned version, is + left out, and the record says so. chrony's own parser is not among + them: the session that wrote these tests was not permitted to build + chrony's downloaded source for a harness, so it was dropped, and the + record says that too. + + The `unix` answers are also checked against RFC 5905 Figure 4, whose + rows give dates with their Modified Julian Day, era and era offset, + with two errata applied (4025: the MJD of 1 Jan 0; 6524: the era + offset of 1 Jan 1), and against Python's datetime module (the + proleptic Gregorian calendar, with no leap seconds) wherever the + date is within its years 1 to 9999. + +3. Against the vectors of Proven-Commons/ntp pull request 3, a wire + contract and 79 vectors by orda-aska (commit cc5dab4, file + tests/wire_vectors.jsonl, copied as tests/ntp-3-wire-vectors.jsonl and + read as data only). Each vector gives a packet, a MAC length chosen + by the caller, and the expected answer. The tool reads the packet by + its own rule, and each difference is counted under the switches with + which the model gives the vector's answer, as for the oracles. + +Inputs come from a recorded seed: +- the inputs of Spec.lean's `examples` and `rejects`; +- every value of the first octet (leap, version and mode) in eight + packets of every shape; every value of every header octet in the + server reply; headers from random fields and random octets; +- packets of 0 to 5 extension fields of random types (the NTS types, + others, and types whose two octets differ), random bodies and lengths + from 16 to 65532, with no MAC, a MAC of 20 or 24 octets, or a + crypto-NAK, in versions 3 and 4; +- such packets with one rule broken: a field of Length 0 to 12, a Length + that is not a multiple of 4, a Length running 4 or 1000 octets past + the end, or to 65532 or 65535 (CVE-2014-9750's flaw, at the level of + the field's own Length), a last field of 16 to 24 octets with no MAC, + a MAC of 8 to 16, 21 to 23 or 28 octets, four octets that are not + zero, a crypto-NAK after a field, a field in version 3, another + version or mode, an octet more or less; +- Autokey fields laid out as RFC 5906 Figure 7 whose Value Length runs + past the field, as in CVE-2014-9750: each must be read by its own + Length, with its octets unchanged, and the record counts them; +- after the client request and after a version 3 header, tails of every + length from 0 to 80: zeros, random octets, all ones, and one field of + that length; a field of every Length from 0 to 80 with and without a + 20-octet MAC after it; +- truncations of valid packets at every length, and random changes of + valid packets: an octet changed, a bit flipped, octets inserted or + removed, a Length field changed; +- `unix` over eras -1, 0, 1 and 2 with the raw values 0, 1, 2^32 - 1, + 2^32, 2^64 - 1, 2208988800 * 2^32 and others, the 2036 rollover, + values of 2^64 and more, random eras and values, and the rows of + Figure 4; `fromunix` of every Unix time `unix` gave, at and around + the first instant of each era, with fractions of 2^32 and more, and + random; +- `encode` of every packet the model reads, and of packets each with + one field out of range or one rule broken; +- the 79 vectors of pull request 3; +- malformed requests, which the tool must answer with `error`, odd but + well-formed ones, and the command line. +Every seventh request ends in CR LF. The record gives a SHA-256 of the +requests, so that a rerun with the same seed and count can be checked +to have the same inputs. + + python3 tests/run_tests.py [--seed N] [--count N] [--no-oracles] [--no-build] + +`--count` is the number of random packets; a fifth as many random +headers, as many packets with a rule broken and as many changed +packets, a hundredth as many cut at every length, and half as many +random `unix` and as many random `fromunix` requests are made. +`--no-oracles` compares the tool with the model alone, as +tests/plant_bugs.py does, and leaves out Figure 4, datetime and the +vectors of pull request 3 too. The script builds the executable with `lake +build ntp` first, or, with `--no-build`, takes the one in .lake/build/bin +as it is; either way it runs a copy. It writes tests/TEST-RESULTS.md and +exits non-zero if any check fails. +""" + +import argparse +import collections +import datetime +import hashlib +import itertools +import json +import math +import os +import random +import re +import shutil +import struct +import subprocess +import sys +import tempfile +import threading +import time +from fractions import Fraction +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +BUILT_EXE = ROOT / ".lake" / "build" / "bin" / "ntp" +TESTS = ROOT / "tests" +RESULTS = TESTS / "TEST-RESULTS.md" +VECTORS = TESTS / "ntp-3-wire-vectors.jsonl" +NTPDRS_SOURCE = TESTS / "ntpd-rs" +NTPDRS_TARGET = ROOT / ".lake" / "oracles" / "ntpd-rs" +TIMEOUT = 3600 +# The pinned versions: an oracle at another version is left out. +NTP_PROTO_PINNED = "1.9.0" +NTPSEC_PINNED = "1.2.2" +TSHARK_PINNED = "4.2.2" +NTPLIB_PINNED = "0.4.0" +# The largest frame tshark reads (WTAP_MAX_PACKET_SIZE_STANDARD). +TSHARK_MAX = 262144 +# Requests are split among this many `ntp serve` processes. +JOBS = max(1, min(4, os.cpu_count() or 1)) +EXE = None + +# --------------------------------------------------------------------- +# The model: Spec/Defs.lean, from RFC 5905 and RFC 7822. + +T16, T32, T64 = 1 << 16, 1 << 32, 1 << 64 +# Spec.unixEpoch: RFC 5905 Figure 4, "1 Jan 1970 ... 2,208,988,800 First +# day UNIX". +UNIX_EPOCH = 2208988800 + +Header = collections.namedtuple( + "Header", "leap version mode stratum poll precision root_delay root_dispersion ref_id " + "reference origin receive transmit") + + +def be(bs): + return int.from_bytes(bs, "big") + + +def be_bytes(w, n): + """Spec.beBytes: n as w octets, most significant first, each + n / 256^i % 256.""" + return bytes((n // 256 ** i) % 256 for i in reversed(range(w))) + + +def signed(octet): + """An octet as an 8-bit two's complement integer.""" + return octet - 256 if octet >= 128 else octet + + +def header_bytes(h): + """Spec.headerBytes: Figure 8. LI, VN and Mode in the first octet, + two, three and three bits from the top; Poll and Precision in two's + complement (Spec.signedByte).""" + return (bytes([(h.leap * 64 + h.version * 8 + h.mode) % 256, h.stratum % 256, h.poll % 256, + h.precision % 256]) + + be_bytes(4, h.root_delay) + be_bytes(4, h.root_dispersion) + be_bytes(4, h.ref_id) + + be_bytes(8, h.reference) + be_bytes(8, h.origin) + be_bytes(8, h.receive) + + be_bytes(8, h.transmit)) + + +def ext_length(e): + """Spec.ExtField.length: the Length field counts the whole field.""" + return len(e[1]) + 4 + + +def ext_bytes(e): + """Spec.extBytes: RFC 7822 Figure 14.""" + return be_bytes(2, e[0]) + be_bytes(2, ext_length(e)) + e[1] + + +def packet_bytes(p): + """Spec.packetBytes: the header, the fields in order, the MAC.""" + h, exts, mac = p + return header_bytes(h) + b"".join(ext_bytes(e) for e in exts) + mac + + +def header_valid(h): + """Spec.HeaderValid.""" + return (0 <= h.leap < 4 and h.version in (3, 4) and 1 <= h.mode <= 5 and 0 <= h.stratum < 256 + and -128 <= h.poll < 128 and -128 <= h.precision < 128 + and all(0 <= x < T32 for x in (h.root_delay, h.root_dispersion, h.ref_id)) + and all(0 <= x < T64 for x in (h.reference, h.origin, h.receive, h.transmit))) + + +def ext_valid(e): + """Spec.ExtValid: RFC 7822 section 3.""" + n = ext_length(e) + return 0 <= e[0] < T16 and 16 <= n and n % 4 == 0 and n < T16 + + +def mac_valid(exts, mac): + """Spec.MacValid: none, 20 or 24 octets, or the crypto-NAK alone.""" + return mac == b"" or len(mac) in (20, 24) or (mac == bytes(4) and not exts) + + +def valid(p): + """Spec.Valid.""" + h, exts, mac = p + return (header_valid(h) and all(ext_valid(e) for e in exts) and (h.version != 3 or not exts) + and mac_valid(exts, mac) and (mac != b"" or not exts or 28 <= ext_length(exts[-1]))) + + +def header_of(b, poll_unsigned=False): + """The fields of the first 48 octets, as Figure 8 lays them out.""" + return Header(b[0] >> 6, (b[0] >> 3) & 7, b[0] & 7, b[1], b[2] if poll_unsigned else signed(b[2]), + signed(b[3]), be(b[4:8]), be(b[8:12]), be(b[12:16]), be(b[16:24]), be(b[24:32]), + be(b[32:40]), be(b[40:48])) + + +def decode_spec(b): + """The packet p with Valid p and packetBytes p = b, or None: what + Spec.decode_iff says the decoder gives. The MAC is one of the lengths + MacValid allows; the octets between the header and the MAC are then + fields, one after another, each as long as its Length field says, + since that is how packetBytes writes them.""" + if len(b) < 48: + return None + h, tail = header_of(b), b[48:] + found = [] + for m in (0, 4, 20, 24): + if m > len(tail): + continue + body, mac = tail[:len(tail) - m], tail[len(tail) - m:] + exts, i = [], 0 + while i < len(body): + if len(body) - i < 4: + break + n = be(body[i + 2:i + 4]) + if n < 4 or i + n > len(body): + break + exts.append((be(body[i:i + 2]), body[i + 4:i + n])) + i += n + if i != len(body): + continue + p = (h, tuple(exts), mac) + if valid(p) and packet_bytes(p) == b: + found.append(p) + if len(found) > 1: + raise AssertionError(f"two packets for {b.hex()}: Spec.decode_iff says at most one") + return found[0] if found else None + + +# Why the model refuses a packet: the rule of `read` that refuses it. +WHY_SHORT = "fewer than 48 octets (RFC 5905 7.3: the header is 12 words)" +WHY_VERSION = "a version other than 3 and 4" +WHY_MODE = "mode 0, 6 or 7 (RFC 5905 Figure 10)" +WHY_V3 = ("version 3, with octets after the header that are not a MAC of 20 or 24 octets or a " + "crypto-NAK (this reader's choice: the MACs of NTPv4, and no extension fields, which RFC " + "7822 section 3 gives to NTPv4; RFC 1305's own authenticator is 12 octets)") +WHY_NAK = "four octets after the header that are not zero (RFC 5905 9.2: the crypto-NAK is zeros)" +WHY_SMALL = "a field of Length below 16 (RFC 7822 section 3)" +WHY_ALIGN = "a field Length that is not a multiple of 4 (RFC 7822 section 3)" +WHY_PAST = ("a field Length past the end of the packet (CVE-2014-9750's flaw, at the level of the " + "field's own Length)") +WHY_NAK_FIELDS = "four zero octets after extension fields: the crypto-NAK is a packet's only tail" +WHY_LEFT_WORDS = ("8, 12 or 16 octets left after the fields: not a MAC (RFC 7822 7.5.1.3), and a last " + "field without a MAC is at least 28 (7.5.1.4)") +WHY_LEFT_4 = "four octets left after the fields that are not zero: not a crypto-NAK" +WHY_LEFT_21 = "21 to 23 octets left: not a MAC of 20 or 24 octets (RFC 7822 section 1)" +WHY_LEFT_ODD = "1 to 19 octets left that are not a whole number of words (RFC 7822 section 3)" + +# Each oracle's choices where it differs from this reader, as switches +# of `read`. +NR_MODES = "ntpd-rs reads every mode, 0, 6 and 7 too, with the header of Figure 8" +NR_MIN4 = ("ntpd-rs takes an extension field of 4 to 12 octets: its minimum is 4 " + "(V4_UNENCRYPTED_MINIMUM_SIZE), where RFC 7822 asks for 16") +NR_MAC = ("ntpd-rs takes any 4 to 24 octets left after the extension fields as the MAC, whatever " + "they hold (Mac::deserialize): four octets that are not zero, a crypto-NAK after fields, " + "8 to 16 octets that RFC 7822 7.5.1.4 rules out, and 21 to 23") +NR_V3 = "ntpd-rs takes any 4 to 24 octets after a version 3 header as the MAC" +NR_NTS = ("ntpd-rs refuses an NTS Cookie Placeholder (0x0304) whose body is not zero, and an NTS " + "Authenticator and Encrypted Extension Fields field (0x0404) whose nonce and ciphertext " + "lengths run past it (RFC 8915 5.5 and 5.6)") +NS_LEN4 = "NTPsec refuses a packet whose length is not a multiple of 4" +NS_VERSIONS = "NTPsec reads every version" +NS_MODES = "NTPsec reads every mode with the header of Figure 8" +NS_POLL = "NTPsec reads Poll as unsigned (struct format B; RFC 5905 7.3 says signed)" +NS_FIELDS = ("NTPsec reads an extension field as a 32-bit type and a 32-bit length of the value after " + "them, with no padding or minimum, cut short at the end of the packet " + "(SyncPacket.analyze), not as RFC 7822 Figure 14 lays it out") +NS_TAIL = ("NTPsec takes four octets left after the fields as a crypto-NAK whatever they hold, fields " + "or not; refuses 8, 12 or 16; and drops any other number of octets but 0, 20 and 24 " + "without a word") +NS_V3 = "NTPsec reads version 3 as version 4, extension fields too" +WS_VERSIONS = "Wireshark reads every version" +WS_MODE0 = "Wireshark reads mode 0 with the header of Figure 8" +WS_TAIL = ("Wireshark splits the octets after the header its own way (packet-ntp.c, dissect_ntp_std): " + "fields while 16 or more octets are left, each of a Length of 16 or more that fits; then, " + "when 1 to 19 octets are left that are not a crypto-NAK, it gives the last field back to " + "the MAC; whatever is left is the MAC, of any length and content; a Length that is not a " + "multiple of 4 gets an expert note; in every version") +NL_VERSIONS = "ntplib reads every version" +NL_MODES = "ntplib reads every mode" +NL_TAIL = "ntplib reads the header only, whatever follows it" +NL_POLL = "ntplib reads Poll as unsigned (struct format B)" +P_MACLEN = ("ntp#3 takes the MAC length from the caller (mac_length), so the octets before it must " + "all be extension fields: with mac_length 0, a tail of 20 or 24 octets is a short last " + "field, not a MAC (WIRE-CONTRACT.md)") +P_MAC4 = "ntp#3 takes a 4-octet MAC of any value, after extension fields too" +P_V3 = "ntp#3 reads extension fields in version 3" +P_MODES = "ntp#3 reads every mode" + +ORACLE_RULES = { + "ntpd-rs": [NR_MODES, NR_MIN4, NR_MAC, NR_V3, NR_NTS], + "NTPsec": [NS_LEN4, NS_VERSIONS, NS_MODES, NS_POLL, NS_FIELDS, NS_TAIL, NS_V3], + "Wireshark": [WS_VERSIONS, WS_MODE0, WS_TAIL], + "ntplib": [NL_VERSIONS, NL_MODES, NL_TAIL, NL_POLL], + "ntp#3": [P_MACLEN, P_MAC4, P_V3, P_MODES], +} + + +def relevant(rule, b): + """Whether a switch can change the reading of b at all: a rule about + the version, the mode, Poll, the length or version 3 cannot when b + has nothing it applies to. Used only to search fewer sets.""" + if len(b) < 48: + return rule == NS_LEN4 + version, mode = (b[0] >> 3) & 7, b[0] & 7 + if rule in (NS_VERSIONS, NL_VERSIONS, WS_VERSIONS): + return version not in (3, 4) + if rule in (NR_MODES, NS_MODES, NL_MODES, P_MODES): + return not 1 <= mode <= 5 + if rule == WS_MODE0: + return mode == 0 + if rule in (NS_POLL, NL_POLL): + return b[2] >= 128 + if rule == NS_LEN4: + return len(b) % 4 != 0 + if rule in (NR_V3, NS_V3, P_V3): + return version == 3 + return True + + +def nts_fits(body): + """ntpd-rs's RawEncryptedField::from_message_bytes: a 16-bit nonce + length, a 16-bit ciphertext length, the nonce, padding to a word, and + the ciphertext, all within the body.""" + if len(body) < 4: + return False + nonce, ciphertext = be(body[0:2]), be(body[2:4]) + if nonce > len(body) - 4: + return False + start = 4 + ((nonce + (-nonce) % 4) % T16) + return start + ciphertext <= len(body) + + +def why_left(left, fields): + """Why the octets left after the fields are not a MAC.""" + n = len(left) + if n == 4: + return WHY_NAK_FIELDS if left == bytes(4) else (WHY_LEFT_4 if fields else WHY_NAK) + if n in (8, 12, 16): + return WHY_LEFT_WORDS + if 21 <= n <= 23: + return WHY_LEFT_21 + return WHY_LEFT_ODD + + +def ok(h, fields, mac, rest=0): + return (h, tuple(fields), bytes(mac), rest) + + +def read(b, rules=frozenset(), mac_length=None): + """The packet, as (header, fields, MAC, octets shown nowhere), or + None; and why it is refused. With no rules, the one-pass reading of + SPEC.md; each rule switches on one choice of an oracle.""" + R = rules + if len(b) < 48: + return None, WHY_SHORT + if NS_LEN4 in R and len(b) % 4: + return None, "length not a multiple of 4 (NTPsec)" + h = header_of(b, poll_unsigned=bool(R & {NS_POLL, NL_POLL})) + if h.version not in (3, 4) and not R & {NS_VERSIONS, NL_VERSIONS, WS_VERSIONS}: + return None, WHY_VERSION + if not 1 <= h.mode <= 5 and not R & {NR_MODES, NS_MODES, NL_MODES, P_MODES}: + if not (WS_MODE0 in R and h.mode == 0): + return None, WHY_MODE + if NL_TAIL in R: + return ok(h, (), b""), None + tail = b[48:] + if WS_TAIL in R: + return ws_tail(h, tail) + if P_MACLEN in R and mac_length is not None: + return pr3_tail(h, tail, mac_length, R) + if h.version == 3 and not R & {NS_V3, P_V3}: + if NR_V3 in R: + return (ok(h, (), tail), None) if len(tail) == 0 or 4 <= len(tail) <= 24 else (None, WHY_V3) + if len(tail) in (0, 20, 24) or tail == bytes(4) or (P_MAC4 in R and len(tail) == 4): + return ok(h, (), tail), None + return None, WHY_V3 + fields, i, n = [], 0, len(tail) + while n - i > 24: + if NS_FIELDS in R: + size = be(tail[i + 4:i + 8]) + fields.append((be(tail[i:i + 4]), tail[i + 8:i + 8 + size])) + i = min(n, i + 8 + size) + continue + t, size = be(tail[i:i + 2]), be(tail[i + 2:i + 4]) + if size < (4 if NR_MIN4 in R else 16): + return None, WHY_SMALL + if size % 4: + return None, WHY_ALIGN + if size > n - i: + return None, WHY_PAST + body = tail[i + 4:i + size] + if NR_NTS in R and ((t == 0x0304 and any(body)) or (t == 0x0404 and not nts_fits(body))): + return None, "an NTS field ntpd-rs refuses" + fields.append((t, body)) + i += size + left = tail[i:] + if NS_TAIL in R: + if len(left) in (4, 20, 24): + return ok(h, fields, left), None + if len(left) in (8, 12, 16): + return None, "8, 12 or 16 octets left (NTPsec)" + return ok(h, fields, b"", len(left)), None + if NR_MAC in R: + if len(left) == 0 or 4 <= len(left) <= 24: + return ok(h, fields, left), None + return None, why_left(left, fields) + if len(left) in (0, 20, 24) or (left == bytes(4) and not fields) or (P_MAC4 in R and len(left) == 4): + return ok(h, fields, left), None + return None, why_left(left, fields) + + +def ws_tail(h, tail): + """Wireshark 4.2.2, packet-ntp.c, dissect_ntp_std and dissect_ntp_ext.""" + n, end, last = len(tail), 0, 0 + while n - end >= 16: + size = be(tail[end + 2:end + 4]) + if size < 16 or n - end < size: + break + end += size + last = size + left = n - end + if left == 0 or (left == 4 and be(tail[end:end + 4]) == 0): + pass + elif left < 20: + end -= last + fields, i = [], 0 + while i < end: + size = be(tail[i + 2:i + 4]) + if size < 8 or size % 4: + return None, "a field Length Wireshark notes" + fields.append((be(tail[i:i + 2]), tail[i + 4:i + size])) + i += size + if n - i >= 4: + return ok(h, fields, tail[i:]), None + return ok(h, fields, b"", n - i), None + + +def pr3_tail(h, tail, mac_length, R): + """ntp#3's tests/wire_oracle.py `decode`: the MAC is the last + mac_length octets, and every octet before it is in a field.""" + if mac_length not in (0, 4, 20, 24) or mac_length > len(tail): + return None, "ntp#3: no room for the MAC" + end = len(tail) - mac_length + fields, i = [], 0 + while i < end: + if end - i < 4: + return None, WHY_LEFT_ODD + t, size = be(tail[i:i + 2]), be(tail[i + 2:i + 4]) + if size < 16: + return None, WHY_SMALL + if size % 4: + return None, WHY_ALIGN + if size > end - i: + return None, WHY_PAST + fields.append((t, tail[i + 4:i + size])) + i += size + mac = tail[end:] + if mac_length == 0 and fields and len(fields[-1][1]) + 4 < 28: + return None, "ntp#3: a last field below 28 octets with no MAC" + if h.version == 3 and fields and P_V3 not in R: + return None, WHY_V3 + if P_MAC4 not in R and not mac_valid(fields, mac): + return None, why_left(mac, fields) + return ok(h, fields, mac), None + + +def to_unix(era, raw): + """RFC 5905 section 6: the timestamp raw in era `era` is era * 2^32 + + raw / 2^32 seconds from 0 h 1 January 1900; less 2,208,988,800 + seconds to 1970. Whole seconds and a fraction in units of 2^-32.""" + if raw == 0 or raw >= T64: + return None + t = era * T32 + Fraction(raw, T32) - UNIX_EPOCH + s = math.floor(t) + return s, int((t - s) * T32) + + +def from_unix(seconds, fraction): + """RFC 5905 section 6: "era = s / 2^(32) and timestamp = s - era * + 2^(32)", s in seconds from 1900, the division rounding down.""" + if fraction >= T32: + return None + s = Fraction(seconds) + Fraction(fraction, T32) + UNIX_EPOCH + era = math.floor(s / T32) + raw = (s - era * T32) * T32 + assert raw.denominator == 1 + return None if raw == 0 else (era, int(raw)) + + +# --------------------------------------------------------------------- +# Requests and answers, by the header of Main.lean. + + +def hexd(bs): + return bs.hex() if bs else "-" + + +def show_fields(fields): + return ",".join(f"{t}:{hexd(body)}" for t, body in fields) if fields else "-" + + +def show_packet(p): + h, fields, mac = p[0], p[1], p[2] + return " ".join([str(x) for x in h] + [show_fields(fields), hexd(mac)]) + + +def show_answer(answer): + return "FAIL" if answer is None else "OK " + show_packet(answer) + + +def model(op, args): + """The model's answer to a request, and why it is FAIL (or None).""" + if op == "decode": + p = decode_spec(args[0]) + if p is None: + return "FAIL", read(args[0])[1] or "the model's two readings differ" + return "OK " + show_packet(p), None + if op == "encode": + p = args[0] + return ("OK " + packet_bytes(p).hex(), None) if valid(p) else ("FAIL", "not Valid") + if op == "unix": + r = to_unix(*args) + return ("FAIL", "no Unix time") if r is None else (f"OK {r[0]} {r[1]}", None) + if op == "fromunix": + r = from_unix(*args) + return ("FAIL", "no timestamp") if r is None else (f"OK {r[0]} {r[1]}", None) + raise ValueError(op) + + +def request(op, args): + if op == "decode": + return f"decode {hexd(args[0])}" + if op == "encode": + return "encode " + show_packet(args[0]) + return f"{op} {args[0]} {args[1]}" + + +DIGITS = frozenset("0123456789") +HEX_DIGITS = frozenset("0123456789abcdef") + + +def decimal(s): + return int(s) if s and all(c in DIGITS for c in s) else None + + +def integer(s): + if s.startswith("-"): + n = decimal(s[1:]) + return None if n is None else -n + return decimal(s) + + +def hex_field(f): + if f == "-": + return b"" + if not f or len(f) % 2 or any(c not in HEX_DIGITS for c in f): + return None + return bytes.fromhex(f) + + +def field_list(s): + if s == "-": + return () + out = [] + for f in s.split(","): + parts = f.split(":") + if len(parts) != 2: + return None + t, body = decimal(parts[0]), hex_field(parts[1]) + if t is None or body is None: + return None + out.append((t, body)) + return tuple(out) + + +def parse_request(line): + """The operation and arguments of a request line, or None when it is + malformed.""" + fs = line.split(" ") + if fs[0] == "decode" and len(fs) == 2: + b = hex_field(fs[1]) + return None if b is None else ("decode", [b]) + if fs[0] == "encode" and len(fs) == 16: + nums = [integer(x) if k in (4, 5) else decimal(x) for k, x in enumerate(fs[1:14])] + exts, mac = field_list(fs[14]), hex_field(fs[15]) + if any(x is None for x in nums) or exts is None or mac is None: + return None + return ("encode", [(Header(*nums), exts, mac)]) + if fs[0] == "unix" and len(fs) == 3: + era, raw = integer(fs[1]), decimal(fs[2]) + return None if era is None or raw is None else ("unix", [era, raw]) + if fs[0] == "fromunix" and len(fs) == 3: + s, f = integer(fs[1]), decimal(fs[2]) + return None if s is None or f is None else ("fromunix", [s, f]) + return None + + +def model_line(raw): + """What the tool must answer to a request line, malformed or not.""" + line = raw[:-1] if raw.endswith("\n") else raw + if raw.endswith("\n") and line.endswith("\r"): + line = line[:-1] + req = parse_request(line) + return "error" if req is None else model(*req)[0] + + +def parse_decoded(text): + """A `decode` answer of the tool's as (header, fields, MAC, 0), or + None for FAIL.""" + if text == "FAIL": + return None + req = parse_request("encode " + text[3:]) if text.startswith("OK ") else None + if req is None: + raise ValueError(f"not a decode answer: {text[:100]}") + h, exts, mac = req[1][0] + return ok(h, exts, mac) + + +# --------------------------------------------------------------------- +# Spec/Examples.lean, and Spec.lean's `examples` and `rejects`. + +CLIENT = bytes.fromhex("230206e8000003ff0000037d5ec69f0fe5f662987b61b9afe5f663667b64995d" + "e5f6636681405590e5f663a8761dde48") +SERVER = bytes.fromhex("240206e900000236000003b7c035676ce5f661fd6f165f03e5f663a87619ef40" + "e5f663a8798c6581e5f663a8798eae2b") +NTS_REPLY = bytes.fromhex( + "240104e80000000000000060544d4e4ce8494892f929579e6287db473ff75f58e84948b2b640d701e84948b2b644bff8" + "01040024e4833a8d600e1342435cb29de550acc0f8d8fa16e5c5370a620b155f586adad6040400d4001000bc6a1de3c2" + "6e13eb10c739d70b841fad1b86e230c63e9ea5f71b62a8a79881ce7c6b17cb3132490fdecf2110564e368892ddeef1f4" + "23f6555341c2c9176120a518dc1a7edc5ee3c83b05087b7303f7ab86d52cc7490ce829397223dcef2d94faf8d71d1280" + "da032dd70469e9ac5f82ef5781d207fbacb4a8b6319114d5f56fb22a0cb6d7dcf77df02146f67e4601b53b217ca8ac1a" + "4d97d59bceeb9833997f100ed469858bcd735201adec38cf8cb2c6d0541a9767ddb3ea091d63d98d03dd6e48153dc9b6" + "1fe5d91d74ae3548") +FIELD16 = bytes([0x01, 0x04, 0x00, 0x10]) + bytes(range(0x11, 0x1D)) +FIELD20 = bytes([0x02, 0x04, 0x00, 0x14]) + bytes(range(0x21, 0x31)) +FIELD28 = bytes([0x01, 0x04, 0x00, 0x1C]) + bytes([0x5A] * 24) +MAC20 = bytes([0, 0, 0, 1]) + bytes(range(0xA0, 0xB0)) +WITH_MAC = CLIENT + FIELD16 + FIELD20 + MAC20 +CRYPTO_NAK = SERVER + bytes(4) +V3_WITH_MAC = bytes([0x1B]) + CLIENT[1:] + MAC20 +TAIL24 = bytes([0x01, 0x04, 0x00, 0x18]) + bytes(range(0x31, 0x45)) +# An Autokey ASSOC request field (RFC 5906 Figure 7 and 10.2) of 28 +# octets whose Value Length, 0xFFFFFFFF, runs past it (CVE-2014-9750). +AUTOKEY_ASSOC = (bytes([0x01, 0x02, 0x00, 0x1C, 0, 0, 0, 1]) + bytes(8) + b"\xff\xff\xff\xff" + b"host" + + bytes(4)) +# clientHeader, serverHeader and ntsHeader, as Spec/Examples.lean gives +# them field by field. +CLIENT_HEADER = Header(0, 4, 3, 2, 6, -24, 0x000003FF, 0x0000037D, 0x5EC69F0F, 0xE5F662987B61B9AF, + 0xE5F663667B64995D, 0xE5F6636681405590, 0xE5F663A8761DDE48) +SERVER_HEADER = Header(0, 4, 4, 2, 6, -23, 0x00000236, 0x000003B7, 0xC035676C, 0xE5F661FD6F165F03, + 0xE5F663A87619EF40, 0xE5F663A8798C6581, 0xE5F663A8798EAE2B) +NTS_HEADER = Header(0, 4, 4, 1, 4, -24, 0, 0x00000060, 0x544D4E4C, 0xE8494892F929579E, + 0x6287DB473FF75F58, 0xE84948B2B640D701, 0xE84948B2B644BFF8) + + +def stated(h, fields=(), mac=b""): + """A `decode` answer, as Spec.lean states it.""" + return "OK " + show_packet((h, fields, mac)) + + +# What Spec.lean states of an input it says only is accepted +# (`(Ntp.decode b).isSome`): any `OK` answer. +ACCEPTED = "OK, any packet" + + +def as_stated(answer, stated_answer): + if stated_answer == ACCEPTED: + return answer.startswith("OK ") + return answer == stated_answer + + +SPEC_CASES = [ + # examples + ("decode", [CLIENT], stated(CLIENT_HEADER)), + ("decode", [SERVER], stated(SERVER_HEADER)), + ("decode", [NTS_REPLY], stated(NTS_HEADER, ((0x0104, NTS_REPLY[52:84]), (0x0404, NTS_REPLY[88:])))), + ("decode", [WITH_MAC], stated(CLIENT_HEADER, ((0x0104, FIELD16[4:]), (0x0204, FIELD20[4:])), MAC20)), + ("decode", [CRYPTO_NAK], stated(SERVER_HEADER, (), bytes(4))), + ("decode", [V3_WITH_MAC], stated(CLIENT_HEADER._replace(version=3), (), MAC20)), + ("decode", [CLIENT + TAIL24], stated(CLIENT_HEADER, (), TAIL24)), + ("decode", [CLIENT + AUTOKEY_ASSOC + MAC20], stated(CLIENT_HEADER, ((0x0102, AUTOKEY_ASSOC[4:]),), MAC20)), + ("encode", [(SERVER_HEADER, (), b"")], f"OK {SERVER.hex()}"), + ("unix", [0, 0xE5F663A8798EAE2B], f"OK 1649141032 {0x798EAE2B}"), + ("fromunix", [1649141032, 0x798EAE2B], f"OK 0 {0xE5F663A8798EAE2B}"), + ("unix", [0, 2208988800 * T32], "OK 0 0"), + ("unix", [0, 0xFFFFFFFF00000000], "OK 2085978495 0"), + ("unix", [0, 0x0000000100000000], "OK -2208988799 0"), + ("unix", [1, 0x0000000100000000], "OK 2085978497 0"), + ("unix", [1, 0], "FAIL"), + ("fromunix", [2085978496, 0], "FAIL"), + ("fromunix", [2085978496, 1], "OK 1 1"), + ("unix", [-1, 0xFFFFFFFF00000000], "OK -2208988801 0"), + # rejects: each refused packet after the accepted one it differs + # from only in the rule it breaks (the server reply and the + # crypto-NAK are above) + ("decode", [bytes([0x14]) + SERVER[1:]], "FAIL"), + ("decode", [bytes([0x2C]) + SERVER[1:]], "FAIL"), + ("decode", [bytes([0x20]) + SERVER[1:]], "FAIL"), + ("decode", [bytes([0x26]) + SERVER[1:]], "FAIL"), + ("decode", [bytes([0x27]) + SERVER[1:]], "FAIL"), + ("decode", [SERVER[:47]], "FAIL"), + ("decode", [CLIENT + bytes([1, 4, 0, 0x10]) + bytes([0x5A] * 12) + FIELD28], ACCEPTED), + ("decode", [CLIENT + bytes([1, 4, 0, 0x0C]) + bytes([0x5A] * 8) + FIELD28], "FAIL"), + ("decode", [CLIENT + bytes([1, 4, 0, 0x20]) + bytes([0x5A] * 28)], ACCEPTED), + ("decode", [CLIENT + bytes([1, 4, 0, 0x1E]) + bytes([0x5A] * 26)], "FAIL"), + ("decode", [CLIENT + bytes([1, 4, 1, 0]) + bytes([0x5A] * 28)], "FAIL"), + ("decode", [CLIENT + FIELD16 + FIELD28], ACCEPTED), + ("decode", [CLIENT + FIELD28 + FIELD16], "FAIL"), + ("decode", [SERVER + bytes([0, 0, 0, 1])], "FAIL"), + ("decode", [SERVER + FIELD28], ACCEPTED), + ("decode", [SERVER + FIELD28 + bytes(4)], "FAIL"), + ("decode", [CLIENT + FIELD28], ACCEPTED), + ("decode", [bytes([0x1B]) + CLIENT[1:] + FIELD28], "FAIL"), + ("decode", [SERVER + MAC20], ACCEPTED), + ("decode", [SERVER + MAC20 + bytes([0])], "FAIL"), +] + +# --------------------------------------------------------------------- +# Building inputs. + +NTS_TYPES = (0x0104, 0x0204, 0x0304, 0x0404) +OTHER_TYPES = (0x0000, 0xFFFF, 0x0002, 0x0102, 0x0201, 0x8002, 0xF5FF, 0xF501, 0xF503, 0xF504, 0x2005) +SPECIAL_32 = (0, 1, 0xFFFFFFFF, 0x80000000, 0x00010000) +SPECIAL_64 = (0, 1, T32, T32 - 1, T64 - 1, 1 << 63, UNIX_EPOCH << 32, 0xFFFFFFFF00000000) + + +def random_header(rng, version=None, mode=None): + def n32(): + return rng.choice(SPECIAL_32) if rng.random() < 0.1 else rng.getrandbits(32) + + def n64(): + return rng.choice(SPECIAL_64) if rng.random() < 0.1 else rng.getrandbits(64) + + return Header(rng.randrange(4), version or rng.choice((4, 4, 4, 3)), mode or rng.randint(1, 5), + rng.randrange(256), rng.randrange(-128, 128), rng.randrange(-128, 128), n32(), n32(), + n32(), n64(), n64(), n64(), n64()) + + +def field_length(rng, at_least=16): + """A Length: mostly short, sometimes long, at times the largest.""" + r = rng.random() + if r < 0.5: + n = rng.choice((16, 20, 24, 28, 32, 36, 40, 44, 48)) + elif r < 0.85: + n = 4 * rng.randint(4, 64) + elif r < 0.97: + n = 4 * rng.randint(4, 256) + elif r < 0.995: + n = 4 * rng.randint(4, 16383) + else: + n = 65532 + return max(n, at_least) + + +def random_body(rng, t, size): + r = rng.random() + if t == 0x0404 and r < 0.6 and size >= 4: + # An NTS authenticator whose nonce and ciphertext fit (RFC 8915 5.6). + nonce = rng.choice((0, 4, 12, 16, 16, 17)) + nonce = min(nonce, size - 4) + room = size - 4 - (nonce + (-nonce) % 4) + cipher = rng.randint(0, max(0, room)) if room >= 0 else 0 + return be_bytes(2, nonce) + be_bytes(2, cipher) + rng.randbytes(size - 4) + if t == 0x0304 and r < 0.6: + return bytes(size) + if r < 0.2: + return bytes(size) + return rng.randbytes(size) + + +def random_type(rng): + r = rng.random() + if r < 0.3: + return rng.choice(NTS_TYPES) + if r < 0.4: + return rng.choice(OTHER_TYPES) + return rng.getrandbits(16) + + +def random_field(rng, at_least=16): + t = random_type(rng) + return (t, random_body(rng, t, field_length(rng, at_least) - 4)) + + +def random_mac(rng, kind=None): + kind = kind or rng.choice(("none", "none", "20", "24", "nak")) + if kind == "20": + return be_bytes(4, rng.choice((1, rng.getrandbits(32)))) + rng.randbytes(16) + if kind == "24": + return be_bytes(4, rng.choice((1, rng.getrandbits(32)))) + rng.randbytes(20) + if kind == "nak": + return bytes(4) + return b"" + + +def random_packet(rng, nfields=None, version=None): + """A valid packet: a version 4 packet of 0 to 5 fields, or a + version 3 one with none; no MAC, a MAC of 20 or 24 octets, or the + crypto-NAK alone. At most 200000 octets, so tshark reads it.""" + h = random_header(rng, version=version or (4 if nfields else None)) + k = 0 if h.version == 3 else (rng.choice((0, 0, 1, 1, 2, 2, 3, 4, 5)) if nfields is None else nfields) + while True: + exts = [random_field(rng) for _ in range(k)] + mac = random_mac(rng) + if exts and mac == bytes(4): + mac = b"" + if exts and not mac and ext_length(exts[-1]) < 28: + t = exts[-1][0] + exts[-1] = (t, random_body(rng, t, rng.choice((24, 28, 32, 60)))) + p = (h, tuple(exts), mac) + if len(packet_bytes(p)) <= 200000: + assert valid(p) + return p + + +def field_raw(t, length, body): + """A field's octets with any Length field, valid or not.""" + return be_bytes(2, t) + be_bytes(2, length) + body + + +# Autokey fields whose Value Length runs past the field: CVE-2014-9750 +# came from following that length. The reader does not read it, so each +# such field must come back by its own Length, its octets unchanged. +AUTOKEY_LABEL = "an Autokey field whose Value Length runs past it (CVE-2014-9750)" + + +def autokey_field(code, response, length, value_length): + """An Autokey field laid out as RFC 5906 Figure 7: the R and E bits + and the Code, the Field Type (the Autokey version, 2), the Length, + the Association ID, the Timestamp, the Filestamp, the Value Length, + then length - 20 octets of Value and the rest.""" + return (bytes([0x80 * response + code, 2]) + be_bytes(2, length) + be_bytes(4, 0x1234) + + bytes(4) + bytes(4) + be_bytes(4, value_length) + bytes(range(0x41, 0x41 + length - 20))) + + +def autokey_cases(): + """(octets, the answer Spec.decode_iff fixes for them) for each + Autokey case.""" + out = [] + for hb, h in ((CLIENT, CLIENT_HEADER), (SERVER, SERVER_HEADER)): + for code in (1, 2, 3): + for length in (28, 32, 64): + for value_length in (length - 19, 0x10000, 0x80000000, 0xFFFFFFFF): + f = autokey_field(code, hb is SERVER, length, value_length) + e = (be(f[:2]), f[4:]) + for pre, post, mac in (((), (), b""), ((), (), MAC20), + (((0x0104, FIELD16[4:]),), (), MAC20 + b"\1\2\3\4"), + ((), ((0x0104, FIELD28[4:]),), b"")): + fields = pre + (e,) + post + b = hb + b"".join(ext_bytes(x) for x in fields) + mac + out.append((b, stated(h, fields, mac))) + return out + + +def broken_packet(rng): + """A packet with one rule broken, built so that a reader without that + rule could take it: (what was broken, octets).""" + h = random_header(rng, version=4) + hb = header_bytes(h) + k = rng.randint(0, 3) + before = b"".join(ext_bytes(random_field(rng)) for _ in range(k)) + mac = random_mac(rng, rng.choice(("none", "20", "24"))) + what = rng.choice(( + "a field of Length 0 to 12", "a field Length that is not a multiple of 4", + "a field Length 4 past the end", "a field Length 1000 past the end", + "a field Length of 65532 or 65535 past the end", "a last field of 16 to 24 octets with no MAC", + "a MAC of 8, 12 or 16 octets", "a MAC of 21 to 23 octets", "a MAC of 28 or 32 octets", + "four octets that are not zero", "a crypto-NAK after a field", "a field in version 3", + "a version other than 3 and 4", "mode 0, 6 or 7", "an octet more at the end", + "an octet less at the end", "a field Length 4 more or less")) + t = random_type(rng) + if what == "a field of Length 0 to 12": + n = rng.choice((0, 4, 8, 12)) + bad = field_raw(t, n, rng.randbytes(max(0, n - 4)) if n else rng.randbytes(12)) + return what, hb + before + bad + ext_bytes(random_field(rng, 28)) + mac + if what == "a field Length that is not a multiple of 4": + n = rng.choice((17, 18, 19, 29, 30, 31, 4 * rng.randint(5, 40) + rng.randint(1, 3))) + return what, hb + before + field_raw(t, n, rng.randbytes(n - 4)) + mac + if what.startswith("a field Length") and "past the end" in what: + rest_len = rng.choice((24, 28, 32, 60, 100)) + rest = rng.randbytes(rest_len - 4) + if "4 past" in what: + n = rest_len + 4 + elif "1000" in what: + n = rest_len + 1000 + else: + n = rng.choice((65532, 65535)) + return what, hb + before + field_raw(t, n, rest) + if what == "a last field of 16 to 24 octets with no MAC": + n = rng.choice((16, 20, 24)) + return what, hb + before + field_raw(t, n, rng.randbytes(n - 4)) + if what == "a MAC of 8, 12 or 16 octets": + return what, hb + before + rng.randbytes(rng.choice((8, 12, 16))) + if what == "a MAC of 21 to 23 octets": + return what, hb + before + rng.randbytes(rng.choice((21, 22, 23))) + if what == "a MAC of 28 or 32 octets": + return what, hb + before + rng.randbytes(rng.choice((28, 32))) + if what == "four octets that are not zero": + return what, hb + be_bytes(4, rng.randint(1, T32 - 1)) + if what == "a crypto-NAK after a field": + return what, hb + before + ext_bytes(random_field(rng)) + bytes(4) + if what == "a field in version 3": + h3 = header_bytes(h._replace(version=3)) + return what, h3 + ext_bytes(random_field(rng, 28)) + random_mac(rng, rng.choice(("none", "20"))) + if what == "a version other than 3 and 4": + v = rng.choice((0, 1, 2, 5, 6, 7)) + return what, header_bytes(h._replace(version=v)) + before + mac + if what == "mode 0, 6 or 7": + m = rng.choice((0, 6, 7)) + return what, header_bytes(h._replace(mode=m)) + before + mac + p = random_packet(rng) + b = packet_bytes(p) + if what == "an octet more at the end": + return what, b + rng.randbytes(1) + if what == "an octet less at the end": + return what, b[:-1] + # a field Length 4 more or less + if not p[1]: + p = random_packet(rng, nfields=rng.randint(1, 3)) + b = packet_bytes(p) + j = rng.randrange(len(p[1])) + at = 48 + sum(ext_length(e) for e in p[1][:j]) + n = ext_length(p[1][j]) + rng.choice((-4, 4)) + return what, b[:at + 2] + be_bytes(2, n % T16) + b[at + 4:] + + +def changed_packet(rng, b): + """A valid packet changed at random: (how, octets).""" + how = rng.choice(("an octet changed", "an octet of the tail changed", "a bit flipped", + "two or three octets changed", "an octet inserted", "an octet removed", + "a Length field changed")) + b = bytearray(b) + if how == "an octet changed": + i = rng.randrange(len(b)) + b[i] = (b[i] + rng.randint(1, 255)) % 256 + elif how == "an octet of the tail changed" and len(b) > 48: + i = rng.randrange(48, len(b)) + b[i] = (b[i] + rng.randint(1, 255)) % 256 + elif how == "a bit flipped": + i = rng.randrange(len(b)) + b[i] ^= 1 << rng.randrange(8) + elif how == "two or three octets changed": + for i in rng.sample(range(len(b)), min(len(b), rng.randint(2, 3))): + b[i] = (b[i] + rng.randint(1, 255)) % 256 + elif how == "an octet inserted": + b.insert(rng.randint(0, len(b)), rng.randrange(256)) + elif how == "an octet removed": + del b[rng.randrange(len(b))] + elif how == "a Length field changed" and len(b) > 52: + p = decode_spec(bytes(b)) + if p and p[1]: + j = rng.randrange(len(p[1])) + at = 48 + sum(ext_length(e) for e in p[1][:j]) + b[at + 2:at + 4] = be_bytes(2, rng.choice((rng.getrandbits(16), ext_length(p[1][j]) + 1, + ext_length(p[1][j]) - 1, 0, 0xFFFF))) + else: + b[48:52] = rng.randbytes(4) + else: + i = rng.randrange(len(b)) + b[i] = (b[i] + 1) % 256 + how = "an octet changed" + return how, bytes(b) + + +# RFC 5905 Figure 4: (date, MJD, era, era offset), with errata 4025 +# (the MJD of 1 Jan 0 is -678,941) and 6524 (the era offset of 1 Jan 1 is +# 202,934,144) applied: as printed, those two rows disagree with the rest +# of the table. +FIGURE_4 = [ + ("1 Jan -4712", -2400001, -49, 1795583104), ("1 Jan -1", -679306, -14, 139775744), + ("1 Jan 0", -678941, -14, 171311744), ("1 Jan 1", -678575, -14, 202934144), + ("4 Oct 1582", -100851, -3, 2873647488), ("15 Oct 1582", -100840, -3, 2874597888), + ("31 Dec 1899", 15019, -1, 4294880896), ("1 Jan 1900", 15020, 0, 0), + ("1 Jan 1970", 40587, 0, 2208988800), ("1 Jan 1972", 41317, 0, 2272060800), + ("31 Dec 1999", 51543, 0, 3155587200), ("8 Feb 2036", 64731, 1, 63104), +] +ERRATA = {"1 Jan 0": ("MJD -678,491", "https://www.rfc-editor.org/errata/eid4025"), + "1 Jan 1": ("era offset 202,939,144", "https://www.rfc-editor.org/errata/eid6524")} +UNIX_MJD = 40587 # the MJD of 1 Jan 1970 + + +def time_inputs(rng, count): + eras = (-1, 0, 1, 2) + raws = (0, 1, T32 - 1, T32, T32 + 1, T64 - 1, UNIX_EPOCH << 32, 0xFFFFFFFF00000000, + 0xFFFFFFFFFFFFFFFF, 1 << 63, (1 << 63) - 1, 0x00000000FFFFFFFF, 0xE5F663A8798EAE2B) + for e in eras: + for r in raws: + yield ("each of eras -1 to 2 with each special raw value", "unix", [e, r]) + for r in (T64, T64 + 1, 1 << 70, 10 ** 30): + for e in (0, 1): + yield ("a raw value of 2^64 or more", "unix", [e, r]) + # The end of era 0 in 2036 and the start of era 1. + for k in range(-3, 4): + for f in (0, 1, T32 - 1): + yield ("the 2036 rollover", "unix", [0, ((T32 - 1 + k) % T32) * T32 + f]) + yield ("the 2036 rollover", "unix", [1, (k % T32) * T32 + f]) + for date, mjd, era, offset in FIGURE_4: + yield ("a row of RFC 5905 Figure 4", "unix", [era, offset * T32]) + yield ("a row of RFC 5905 Figure 4", "fromunix", [(mjd - UNIX_MJD) * 86400, 0]) + for _ in range(count // 2): + r = rng.random() + era = rng.choice(eras) if r < 0.7 else rng.randint(-20, 20) if r < 0.9 else \ + rng.choice((-1, 1)) * rng.getrandbits(rng.choice((40, 70))) + raw = rng.choice(raws) if rng.random() < 0.05 else rng.getrandbits(64) + yield ("random", "unix", [era, raw]) + for k in range(-3, 4): + s = k * T32 - UNIX_EPOCH + for ds, f in ((0, 0), (0, 1), (-1, T32 - 1), (0, T32 - 1), (1, 0)): + yield ("the first instant of an era, and around it", "fromunix", [s + ds, f]) + for _ in range(count // 2): + r = rng.random() + s = rng.randint(-(1 << 35), 1 << 35) if r < 0.8 else rng.choice((-1, 1)) * rng.getrandbits(70) + f = rng.getrandbits(32) if rng.random() < 0.95 else T32 + rng.getrandbits(rng.choice((1, 40))) + yield ("random", "fromunix", [s, f]) + + +def every_value(label, b, positions): + for i in positions: + for v in range(256): + if v != b[i]: + yield (label, "decode", [b[:i] + bytes([v]) + b[i + 1:]]) + + +def decode_inputs(rng, count): + """Every `decode` input, labelled.""" + for op, args, _ in SPEC_CASES: + if op == "decode": + yield ("Spec.lean's examples and rejects", op, args) + shapes = [("the client request", CLIENT), ("the server reply", SERVER), + ("the NTS reply", NTS_REPLY), ("the client request with two fields and a MAC", WITH_MAC), + ("the crypto-NAK", CRYPTO_NAK), ("the version 3 packet with a MAC", V3_WITH_MAC), + ("the client request with a 24-octet MAC", CLIENT + TAIL24), + ("the client request with a 28-octet field", CLIENT + FIELD28)] + for name, b in shapes: + yield from every_value(f"every value of the first octet, in {name}", b, [0]) + yield from every_value("every value of every header octet, in the server reply", SERVER, range(48)) + for _ in range(count // 5): + if rng.random() < 0.2: + yield ("random octets, 48 of them", "decode", [rng.randbytes(48)]) + else: + yield ("a header from random fields", "decode", [header_bytes(random_header(rng))]) + for _ in range(count): + p = random_packet(rng) + yield (f"a random packet, {len(p[1])} fields", "decode", [packet_bytes(p)]) + for _ in range(count // 5): + what, b = broken_packet(rng) + yield ("a random packet, " + what, "decode", [b]) + # Tails of every length after a version 4 and a version 3 header. + v3 = bytes([0x1B]) + CLIENT[1:] + for name, hb in (("version 4", CLIENT), ("version 3", v3)): + for n in range(81): + yield (f"a tail of every length from 0 to 80, {name}, zeros", "decode", [hb + bytes(n)]) + yield (f"a tail of every length from 0 to 80, {name}, random", "decode", [hb + rng.randbytes(n)]) + yield (f"a tail of every length from 0 to 80, {name}, all ones", "decode", [hb + b"\xff" * n]) + if n >= 4: + yield (f"a tail of every length from 0 to 80, {name}, one field of that length", "decode", + [hb + field_raw(random_type(rng), n, rng.randbytes(n - 4))]) + for n in range(81): + body = rng.randbytes(max(0, n - 4)) if n >= 4 else b"" + f = field_raw(0x0104, n, body) + yield ("a field of every Length from 0 to 80", "decode", [CLIENT + f]) + yield ("a field of every Length from 0 to 80, then a 20-octet MAC", "decode", [CLIENT + f + MAC20]) + yield ("a field of every Length from 0 to 80, after a field of 28", "decode", [CLIENT + FIELD28 + f]) + # Version 3, each kind of tail. + for name, tail in (("none", b""), ("a crypto-NAK", bytes(4)), ("four octets that are not zero", b"\0\0\0\7"), + ("a 20-octet MAC", MAC20), ("a 24-octet MAC", MAC20 + b"\1\2\3\4"), + ("21 to 23 octets", MAC20 + b"\1"), ("21 to 23 octets", MAC20 + b"\1\2"), + ("21 to 23 octets", MAC20 + b"\1\2\3"), ("8, 12 or 16 octets", MAC20[:8]), + ("8, 12 or 16 octets", MAC20[:12]), ("8, 12 or 16 octets", MAC20[:16]), + ("a field of 28 octets", FIELD28), ("a field of 28 octets and a MAC", FIELD28 + MAC20), + ("a field of 16 octets and a MAC", FIELD16 + MAC20), ("a 72-octet MS-SNTP tail", + b"\0\0\0\1" + bytes(68))): + yield (f"version 3 with {name}", "decode", [v3 + tail]) + # MACs and crypto-NAKs after the header and after fields. + for pre_name, pre in (("after the header", b""), ("after a field of 16", FIELD16), + ("after a field of 28", FIELD28)): + for n in (4, 20, 21, 22, 23, 24): + for content in ("zeros", "random"): + tail = bytes(n) if content == "zeros" else rng.randbytes(n) + yield (f"a MAC of 4 or 20 to 24 octets, {pre_name}", "decode", [SERVER + pre + tail]) + # Lengths that run past the end: by 4, by 1000, at each position. + for pos in range(3): + for over in (4, 1000, None): + for left in (24, 28, 32, 64): + before = b"".join(ext_bytes(random_field(rng)) for _ in range(pos)) + n = left + over if over else 65532 + yield ("a field Length running 4 or 1000 octets past the end, or to 65532", "decode", + [CLIENT + before + field_raw(0x0104, n, rng.randbytes(left - 4))]) + for b, _ in autokey_cases(): + yield (AUTOKEY_LABEL, "decode", [b]) + # Truncations of valid packets at every length. + for name, b in shapes: + for n in range(len(b)): + yield ("a packet cut short, at every length", "decode", [b[:n]]) + for _ in range(max(1, count // 100)): + p = random_packet(rng, nfields=rng.randint(0, 3)) + b = packet_bytes(p) + cuts = range(len(b)) if len(b) <= 400 else sorted(set(rng.sample(range(len(b)), 200)) | {47, 48}) + for n in cuts: + yield ("a random packet cut short, at every length", "decode", [b[:n]]) + # Random changes of valid packets. + for _ in range(count // 5): + how, b = changed_packet(rng, packet_bytes(random_packet(rng))) + yield ("a random packet, " + how, "decode", [b]) + # Two of the largest fields, and one field of every size class. + for n in (65532, 65528): + t = rng.getrandbits(16) + yield ("the largest fields", "decode", [CLIENT + field_raw(t, n, rng.randbytes(n - 4))]) + yield ("the largest fields", "decode", [CLIENT + field_raw(t, n, rng.randbytes(n - 4)) + MAC20]) + yield ("the largest fields", "decode", [CLIENT + field_raw(1, 65532, bytes(65528)) + FIELD16 + MAC20]) + for _, b, _, _ in vector_answers(): + yield ("a vector of pull request 3", "decode", [b]) + + +def bad_encodes(rng): + """Packets that are not valid, each in one way, to `encode`.""" + h = SERVER_HEADER + for what, p in ( + ("a leap indicator of 4", (h._replace(leap=4), (), b"")), + ("version 2", (h._replace(version=2), (), b"")), ("version 5", (h._replace(version=5), (), b"")), + ("version 8", (h._replace(version=8), (), b"")), + ("mode 0", (h._replace(mode=0), (), b"")), ("mode 6", (h._replace(mode=6), (), b"")), + ("mode 7", (h._replace(mode=7), (), b"")), ("mode 8", (h._replace(mode=8), (), b"")), + ("stratum 256", (h._replace(stratum=256), (), b"")), + ("poll 128", (h._replace(poll=128), (), b"")), ("poll -129", (h._replace(poll=-129), (), b"")), + ("precision 128", (h._replace(precision=128), (), b"")), + ("precision -129", (h._replace(precision=-129), (), b"")), + ("root delay 2^32", (h._replace(root_delay=T32), (), b"")), + ("root dispersion 2^32", (h._replace(root_dispersion=T32), (), b"")), + ("reference ID 2^32", (h._replace(ref_id=T32), (), b"")), + ("reference timestamp 2^64", (h._replace(reference=T64), (), b"")), + ("origin timestamp 2^64", (h._replace(origin=T64), (), b"")), + ("receive timestamp 2^64", (h._replace(receive=T64), (), b"")), + ("transmit timestamp 2^64", (h._replace(transmit=T64), (), b"")), + ("a field type of 2^16", (h, ((T16, FIELD28[4:]),), b"")), + ("a field body of 13 octets", (h, ((1, bytes(13)), (2, bytes(24))), b"")), + ("a field body of 25 octets", (h, ((1, bytes(25)),), b"")), + ("a field body of 8 octets", (h, ((1, bytes(8)), (2, bytes(24))), b"")), + ("a field body of none", (h, ((1, b""), (2, bytes(24))), b"")), + ("a field body of 65532 octets", (h, ((1, bytes(65532)),), b"")), + ("a last field of 16 octets with no MAC", (h, ((1, bytes(24)), (2, bytes(12))), b"")), + ("a last field of 24 octets with no MAC", (h, ((1, bytes(20)),), b"")), + ("a MAC of 21 octets", (h, (), bytes(21))), ("a MAC of 16 octets", (h, (), bytes(16))), + ("a MAC of 28 octets", (h, (), bytes(28))), + ("four octets that are not zero", (h, (), b"\0\0\0\1")), + ("a crypto-NAK after a field", (h, ((1, bytes(12)),), bytes(4))), + ("a field in version 3", (h._replace(version=3), ((1, bytes(24)),), b"")), + ("a field in version 3, and a MAC", (h._replace(version=3), ((1, bytes(12)),), MAC20)), + ): + yield ("a packet not valid in one way, encoded: " + what, "encode", [p]) + for _ in range(50): + p = random_packet(rng) + what = rng.choice(("a leap indicator of 4 to 7", "version 2 or 5", "a field body not a whole word", + "a MAC of 21 octets")) + h, exts, mac = p + if what == "a leap indicator of 4 to 7": + p = (h._replace(leap=rng.randint(4, 7)), exts, mac) + elif what == "version 2 or 5": + p = (h._replace(version=rng.choice((2, 5))), exts, mac) + elif what == "a field body not a whole word": + p = (h, exts + ((1, bytes(rng.choice((25, 26, 27)))),), mac) + else: + p = (h, exts, bytes(21)) + yield ("a random packet not valid in one way, encoded", "encode", [p]) + + +def inputs(seed, count): + """Every request but the malformed ones, labelled: (label, op, args).""" + rng = random.Random(seed) + items = list(decode_inputs(rng, count)) + out = list(items) + for op, args, _ in SPEC_CASES: + if op != "decode": + out.append(("Spec.lean's examples and rejects", op, args)) + # `encode` of every packet the model reads. + for label, op, args in items: + p = decode_spec(args[0]) + if p is not None: + out.append(("a packet the model reads, encoded", "encode", [p])) + out += list(bad_encodes(rng)) + times = list(time_inputs(rng, count)) + out += times + # `fromunix` of every Unix time `unix` gives. + for label, op, args in times: + if op == "unix": + r = to_unix(*args) + if r is not None: + out.append(("the Unix time of a `unix` request, back", "fromunix", list(r))) + return out + +# --------------------------------------------------------------------- +# Running a program over request lines. + + +def run_lines(cmd, lines, timeout=TIMEOUT, cwd=None, env=None): + """Send `lines` to `cmd` and return its answer lines and a note on + how the run failed, or None.""" + raw = "".join(lines).encode("utf-8") + proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, cwd=cwd, env=env) + out, err = bytearray(), bytearray() + + def feed(): + try: + proc.stdin.write(raw) + proc.stdin.close() + except (BrokenPipeError, ValueError): + pass + + def drain(stream, buf): + while chunk := stream.read1(1 << 16): + buf += chunk + + threads = [threading.Thread(target=feed), + threading.Thread(target=drain, args=(proc.stdout, out)), + threading.Thread(target=drain, args=(proc.stderr, err))] + for t in threads: + t.start() + problem = None + try: + proc.wait(timeout=timeout) + except subprocess.TimeoutExpired: + proc.kill() + problem = f"no end after {timeout} seconds" + for t in threads: + t.join() + proc.wait() + if proc.returncode != 0 and not problem: + tail = " / ".join(err.decode("utf-8", "backslashreplace").splitlines()[-3:]) + problem = f"exit status {proc.returncode}: {tail}" + text = bytes(out).decode("utf-8", "backslashreplace") + if text and not text.endswith("\n"): + problem = problem or "the last answer has no LF" + answers = text.split("\n") + if answers and answers[-1] == "": + answers.pop() + return answers, problem + + +def run_parallel(cmd, lines, jobs): + """`run_lines` over `jobs` processes, each given a run of the lines + in order; the answers in order, and the problems.""" + size = -(-len(lines) // jobs) if lines else 0 + chunks = [lines[k:k + size] for k in range(0, len(lines), size)] if size else [[]] + results = [None] * len(chunks) + + def one(k): + results[k] = run_lines(cmd, chunks[k]) + + threads = [threading.Thread(target=one, args=(k,)) for k in range(len(chunks))] + for t in threads: + t.start() + for t in threads: + t.join() + answers, problems = [], [] + for chunk, (got, problem) in zip(chunks, results): + if problem: + problems.append(problem) + if len(got) != len(chunk): + problems.append(f"{len(got)} answers for {len(chunk)} requests") + got = (got + [""] * len(chunk))[:len(chunk)] + answers += got + return answers, problems + + +def first_line(cmd): + try: + out = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + return (out.stdout or out.stderr).strip().splitlines()[0] + except (OSError, subprocess.SubprocessError, IndexError): + return "unknown" + + +# --------------------------------------------------------------------- +# The oracles. Each gives, for a list of packets, its answers in one +# form: None when it refuses the packet, else (header, fields, MAC, +# octets shown nowhere), with what it does not give as None. + + +def setup_ntpdrs(tmp): + """Build tests/ntpd-rs; its copy in tmp and a description, or None + and why it is left out.""" + lock = (NTPDRS_SOURCE / "Cargo.lock").read_text(encoding="utf-8") + m = re.search(r'name = "ntp-proto"\nversion = "([^"]+)"', lock) + if not m or m.group(1) != NTP_PROTO_PINNED: + return None, f"Cargo.lock does not pin ntp-proto {NTP_PROTO_PINNED}" + if not shutil.which("cargo"): + return None, "cargo is not installed" + env = dict(os.environ, CARGO_TARGET_DIR=str(NTPDRS_TARGET)) + try: + build = subprocess.run(["cargo", "build", "--release", "--locked", "--quiet"], cwd=NTPDRS_SOURCE, + env=env, capture_output=True, text=True, errors="backslashreplace", + timeout=TIMEOUT) + except (OSError, subprocess.SubprocessError) as e: + return None, f"`cargo build` failed: {e}" + if build.returncode != 0: + last = (build.stderr.strip().splitlines() or ["no output"])[-1] + return None, f"`cargo build --release --locked` failed: {last[:200]}" + exe = tmp / "ntpdrs-oracle" + shutil.copy2(NTPDRS_TARGET / "release" / "ntpdrs-oracle", exe) + return exe, (f"ntpd-rs: the ntp-proto crate {m.group(1)}, `NtpPacket::deserialize`, through " + f"tests/ntpd-rs (every crate pinned by its Cargo.lock), built with " + f"{first_line(['rustc', '--version'])}") + + +def ntpdrs_answers(exe, packets): + """The answers of tests/ntpd-rs: OK, the version, the leap indicator + and 11 more header fields, the fields, the MAC and one more column; + or FAIL.""" + lines = [hexd(b) + "\n" for b in packets] + out, problems = run_parallel([str(exe)], lines, JOBS) + answers = [] + for line in out: + if line.startswith("FAIL"): + answers.append(None) + continue + fs = line.split(" ") + if fs[0] != "OK" or len(fs) != 17: + answers.append(("unreadable", line[:200])) + continue + nums = [int(x) for x in fs[1:14]] + nums[0], nums[1] = nums[1], nums[0] + fields = () + if fs[14] != "-": + fields = tuple((int(t), None if body == "*" else (b"" if body == "-" else bytes.fromhex(body))) + for t, body in (f.split(":") for f in fs[14].split(","))) + answers.append((Header(*nums), fields, b"" if fs[15] == "-" else bytes.fromhex(fs[15]), 0)) + return answers, problems + + +def setup_ntpsec(): + try: + import ntp.packet + import ntp.poly + except ImportError as e: + return None, f"the Python module of NTPsec is not installed ({e})" + version = first_line(["dpkg-query", "-W", "-f", "${Version}\n", "python3-ntp"]) + if not version.startswith(NTPSEC_PINNED + "+") and version != NTPSEC_PINNED: + return None, f"python3-ntp {version} is installed, not {NTPSEC_PINNED}" + return ntp, (f"NTPsec: `ntp.packet.SyncPacket.analyze` of python3-ntp {version} (NTPsec " + f"{NTPSEC_PINNED}), from {Path(ntp.packet.__file__).parent}") + + +def ntpsec_answers(ntp, packets): + answers = [] + for b in packets: + p = ntp.packet.SyncPacket() + try: + p.analyze(ntp.poly.polybytes(b)) + except ntp.packet.SyncException: + answers.append(None) + continue + h = Header(p.li_vn_mode >> 6, p.version(), p.mode(), p.stratum, p.poll, p.precision, + p.root_delay, p.root_dispersion, p.refid, p.reference_timestamp, p.origin_timestamp, + p.receive_timestamp, p.transmit_timestamp) + mac = bytes(p.mac) if isinstance(p.mac, (bytes, bytearray)) else b"" + answers.append((h, tuple((t, bytes(d)) for t, d in p.extfields), mac, None)) + return answers, [] + + +def setup_ntplib(): + try: + import ntplib + except ImportError as e: + return None, f"ntplib is not installed ({e})" + try: + from importlib.metadata import version + v = version("ntplib") + except Exception: # no metadata + v = "unknown" + if v != NTPLIB_PINNED: + return None, f"ntplib {v} is installed, not {NTPLIB_PINNED}" + return ntplib, f"ntplib {v}: `NTPPacket.from_data`, for the header fields only" + + +def ntplib_time(raw): + """ntplib's _to_time: a 64-bit timestamp as a float.""" + return (raw >> 32) + float(raw & 0xFFFFFFFF) / 2 ** 32 + + +def ntplib_view(a): + """The header as ntplib keeps it.""" + if a is None: + return None + h = a[0] + return (h.leap, h.version, h.mode, h.stratum, h.poll, h.precision, float(h.root_delay) / 2 ** 16, + float(h.root_dispersion) / 2 ** 16, h.ref_id, ntplib_time(h.reference), ntplib_time(h.origin), + ntplib_time(h.receive), ntplib_time(h.transmit)) + + +def ntplib_answers(ntplib, packets): + answers = [] + for b in packets: + p = ntplib.NTPPacket() + try: + p.from_data(b) + except ntplib.NTPException: + answers.append(None) + continue + answers.append((p.leap, p.version, p.mode, p.stratum, p.poll, p.precision, p.root_delay, + p.root_dispersion, p.ref_id, p.ref_timestamp, p.orig_timestamp, p.recv_timestamp, + p.tx_timestamp)) + return answers, [] + + +def setup_tshark(): + if not shutil.which("tshark"): + return None, "tshark is not installed" + line = first_line(["tshark", "--version"]) + m = re.search(r"TShark \(Wireshark\) (\S+)", line) + if not m or m.group(1) != TSHARK_PINNED: + return None, f"{line} is installed, not Wireshark {TSHARK_PINNED}" + return "tshark", f"Wireshark: the NTP dissector of {line.rstrip('.')}, through `tshark -T pdml`" + + +PDML_ATTR = re.compile(r'(\w+)="([^"]*)"') + + +def tshark_answers(tmp, packets): + """Write the packets as a capture of link type 147 (user 0), have + tshark read it with the NTP dissector registered for that link type, + and read its PDML.""" + path = tmp / "packets.pcap" + sent = [i for i, b in enumerate(packets) if len(b) <= TSHARK_MAX] + with open(path, "wb") as f: + f.write(struct.pack("": + state = {"proto": None, "flagged": False, "f": {}, "exts": [], "mac": b""} + continue + if state is None: + continue + if s == "": + found.append(state) + state = None + continue + if s.startswith(" 1 else "") + nums[-1] + + +class Comparison: + """An oracle's answers against the tool's, with the model's + reasons.""" + + def __init__(self, name): + self.name = name + self.asked = 0 + self.agree = 0 + self.asked_read = 0 + self.agree_read = 0 + self.model_right = 0 + self.named = collections.OrderedDict() + self.failures = [] + + def add(self, b, tool, want, mac_length=None, label=None): + view = VIEWS[self.name] + self.asked += 1 + full = view(read(b, frozenset(ORACLE_RULES[self.name]), mac_length)[0]) + where = label or f"decode {short(hexd(b), 120)}" + if full == want: + self.model_right += 1 + else: + self.failures.append((f"{self.name}, the model with its rules: {where}", short(show_any(want), 300), + short(show_any(full), 300))) + got = view(tool) + reads = tool is not None + self.asked_read += reads + if got == want: + self.agree += 1 + self.agree_read += reads + return + rules = explain(b, self.name, want, view, mac_length) + if not rules: + self.failures.append((f"{self.name}: {where}", short(show_any(want), 300), short(show_any(got), 300))) + return + n, example = self.named.get(rules, (0, None)) + if example is None: + example = f"{short(where, 110)}: the tool {short(show_any(got), 150)}; {self.name} " \ + f"{short(show_any(want), 150)}" + self.named[rules] = (n + 1, example) + +# --------------------------------------------------------------------- +# Malformed and odd requests, and the command line. + +SERVER_FIELDS = show_packet((SERVER_HEADER, (), b"")).split(" ") + + +def encode_with(k, value): + fs = list(SERVER_FIELDS) + fs[k] = value + return "encode " + " ".join(fs) + + +MALFORMED = [ + "", " ", "\t", "decode", "decode 0", "decode 0A", "decode 00 00", "decode zz", "decode 00", " decode 00", + "DECODE 00", "Decode 00", "decode\t00", "decode --", "decode +00", "decode 0x00", "decode é0", "decode ", + "decode -0", "decode 0-", "decode00", "decode FF", "decode Ff", "decode fF", "decode 00\rx", + "encode", "encode -", "encode " + " ".join(SERVER_FIELDS[:14]), "encode " + " ".join(SERVER_FIELDS + ["-"]), + "encode " + " ".join(SERVER_FIELDS), "encode " + " ".join(SERVER_FIELDS) + " ", + encode_with(0, "-0"), encode_with(0, "+0"), encode_with(0, "0x0"), encode_with(0, ""), + encode_with(1, "4.0"), encode_with(3, "-1"), encode_with(4, "--6"), encode_with(4, "+6"), + encode_with(4, "-"), encode_with(4, "6-"), encode_with(5, ""), encode_with(6, "1e3"), + encode_with(8, "٣"), encode_with(9, "0x1"), encode_with(12, "1_000"), + encode_with(13, "260"), encode_with(13, "260:"), encode_with(13, ":00"), encode_with(13, "260:00:00"), + encode_with(13, "x:00"), encode_with(13, "260:0"), encode_with(13, "260:zz"), encode_with(13, "260:00,"), + encode_with(13, ",260:00"), encode_with(13, "-1:00"), encode_with(13, "260:-0"), encode_with(13, "260:AB"), + encode_with(13, ""), encode_with(14, ""), encode_with(14, "0"), encode_with(14, "zz"), encode_with(14, "-0"), + "unix", "unix 0", "unix 0 0 0", "unix +0 1", "unix 0 -1", "unix 0 1.5", "unix x 1", "unix 0 0", + "unix - 1", "unix -- 1", "unix 0 +1", "unix 1e3 1", "unix 0 1", "unix 0 1", "unix 0 1 ", "unix 0 0x1", + "fromunix", "fromunix 0", "fromunix 0 -1", "fromunix 0 1 2", "fromunix 1e3 0", "fromunix 0 0x1", + "fromunix - 0", "fromunix -+1 0", "serve", "unix0 1", "fromunix0 0", "UNIX 0 1", "FromUnix 0 0", +] +ODD = ["decode -", "unix -0 1", "unix 00 01", "unix 0 99999999999999999999999", "unix 99999999999999999999 1", + "unix -99999999999999999999 1", "fromunix 0 4294967296", "fromunix -0 0", + "fromunix 99999999999999999999999 0", "fromunix 0 0", "fromunix -2208988800 0", + "encode " + " ".join("000" + f if f[0].isdigit() else f for f in SERVER_FIELDS), + encode_with(4, "-0"), encode_with(13, "00260:" + FIELD28[4:].hex()), encode_with(3, "99999999999999999999"), + encode_with(13, "260:-"), encode_with(13, "65536:" + FIELD28[4:].hex())] +# Last lines with no LF, with CR LF, with a lone CR: (request, answer). +AT_EOF = [("decode -", "FAIL"), ("unix 0 1\r\n", model_line("unix 0 1\n")), + ("fromunix 0 0", model_line("fromunix 0 0\n")), ("decode -\r", "error")] + + +def cli_cases(): + """Runs of the command line: (arguments, standard input, the lines + the run must print on standard output, its exit status), from the + header of Main.lean: `ntp serve` and nothing else.""" + cases = [(["serve"], b"", [], 0), + (["serve"], b"unix 0 1\ndecode -\n", [model_line("unix 0 1\n"), "FAIL"], 0)] + for args in ([], ["serve", "x"], ["decode", "00"], ["Serve"], ["--help"], [""], ["serve", ""], ["dump"]): + cases.append((args, b"", [], 2)) + return cases + + +def run_cli(cases): + right, failures = 0, [] + for args, stdin, want, status in cases: + try: + p = subprocess.run([str(EXE)] + args, input=stdin, capture_output=True, timeout=600) + text = p.stdout.decode("utf-8", "backslashreplace") + got, code_ = (text[:-1].split("\n") if text.endswith("\n") else ([text] if text else [])), p.returncode + except (OSError, subprocess.SubprocessError) as e: + got, code_ = [str(e)], None + if (got, code_) == (want, status): + right += 1 + else: + failures.append((f"ntp {' '.join(args)}"[:120], f"{want}, exit {status}", f"{got[:3]}, exit {code_}")) + return right, failures + + +# --------------------------------------------------------------------- +# Unix time against RFC 5905 Figure 4 and Python's datetime. + +EPOCH_1900 = datetime.datetime(1900, 1, 1, tzinfo=datetime.timezone.utc) +EPOCH_1970 = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + + +def datetime_unix(era, raw): + """The Unix time of the timestamp, by Python's datetime: 1900-01-01 + plus era * 2^32 + raw / 2^32 seconds; None outside its years.""" + try: + d = EPOCH_1900 + datetime.timedelta(seconds=era * T32 + (raw >> 32)) + except (OverflowError, ValueError): + return None + return (d - EPOCH_1970) // datetime.timedelta(seconds=1), raw & 0xFFFFFFFF + + +def vector_answers(): + """The vectors of pull request 3: (id, octets, MAC length, the + expected answer in the oracles' form).""" + out = [] + for line in VECTORS.read_text(encoding="utf-8").splitlines(): + v = json.loads(line) + req, exp = v["request"], v["expect"] + b = bytes.fromhex(req["packet_hex"]) + if "error" in exp: + ans = None + else: + hd = exp["header"] + h = Header(hd["leap"], hd["version"], hd["mode"], hd["stratum"], hd["poll"], hd["precision"], + hd["root_delay_bits"], hd["root_dispersion_bits"], int(hd["reference_id_hex"], 16), + *(t["raw"] for t in hd["timestamps"])) + fields = tuple((e["type"], bytes.fromhex(e["value_and_padding_hex"])) for e in exp["extensions"]) + ans = (h, fields, bytes.fromhex(exp["mac_hex"]), None) + out.append((v["id"], b, req["mac_length"], ans)) + return out + + +def lean_version(): + try: + out = subprocess.run(["lean", "--version"], cwd=ROOT, capture_output=True, text=True, + errors="backslashreplace", timeout=300) + first = out.stdout.strip().splitlines()[:1] + return first[0] if first else "unknown" + except (OSError, subprocess.TimeoutExpired): + return "unknown" + + +def count_arg(text): + n = int(text) + if n < 1: + raise argparse.ArgumentTypeError("the count must be at least 1") + return n + + +def write(lines): + RESULTS.write_text("\n".join(lines), encoding="utf-8") + + +def code(text): + """`text` as Markdown inline code, whatever backticks it holds.""" + return f"`` {text} ``" if "`" in text else f"`{text}`" + + +def tally(counter): + return ", ".join(f"{k} {v}" for k, v in sorted(counter.items(), key=lambda kv: (-kv[1], kv[0]))) + + +# --------------------------------------------------------------------- + +ORACLE_ORDER = ("ntpd-rs", "NTPsec", "Wireshark", "ntplib") + + +def main(): + global EXE + sys.set_int_max_str_digits(0) + ap = argparse.ArgumentParser() + ap.add_argument("--seed", type=int, default=20261028) + ap.add_argument("--count", type=count_arg, default=20000) + ap.add_argument("--no-oracles", action="store_true", + help="compare with the model only, as tests/plant_bugs.py does") + ap.add_argument("--no-build", action="store_true", + help="use .lake/build/bin/ntp as it is, without `lake build ntp`") + args = ap.parse_args() + os.environ["PATH"] = f"{Path.home() / '.elan' / 'bin'}{os.pathsep}{os.environ.get('PATH', '')}" + started = time.monotonic() + + def give_up(why): + write(["# Test results", "", "- Oracle: none ran", f"- Seed: {args.seed}", "- Inputs: 0", + f"- Failures: 1 ({why})", ""]) + sys.exit(why) + + if args.no_build: + built = "`.lake/build/bin/ntp` as it was, not rebuilt (`--no-build`)" + if not BUILT_EXE.exists(): + give_up("`--no-build`, and there is no .lake/build/bin/ntp") + newest = max(p.stat().st_mtime for p in [ROOT / "Main.lean", ROOT / "Spec" / "Defs.lean"] + + list((ROOT / "Ntp").glob("*.lean"))) + if BUILT_EXE.stat().st_mtime < newest: + built += "; it is older than the sources" + else: + built = "built by `lake build ntp`" + try: + build = subprocess.run(["lake", "build", "ntp"], cwd=ROOT, capture_output=True, text=True, + errors="backslashreplace", timeout=TIMEOUT) + if build.returncode != 0: + print(build.stdout + build.stderr) + give_up("`lake build ntp` failed") + except (OSError, subprocess.SubprocessError) as e: + give_up(f"`lake build ntp` failed: {e}") + + failures = [] + with tempfile.TemporaryDirectory() as tmpname: + tmp = Path(tmpname) + EXE = tmp / "ntp" + shutil.copy2(BUILT_EXE, EXE) + exe_hash = hashlib.sha256(EXE.read_bytes()).hexdigest() + + # The oracles. + oracles, described, left_out = {}, {}, [] + if args.no_oracles: + left_out = [(name, "--no-oracles") for name in ORACLE_ORDER] + else: + for name, setup in (("ntpd-rs", lambda: setup_ntpdrs(tmp)), ("NTPsec", setup_ntpsec), + ("Wireshark", setup_tshark), ("ntplib", setup_ntplib)): + handle, text = setup() + if handle is None: + left_out.append((name, text)) + print(f"oracle left out: {name}: {text}", flush=True) + else: + oracles[name], described[name] = handle, text + + # The requests. + items = inputs(args.seed, args.count) + lines = [request(op, a) + ("\r\n" if i % 7 == 6 else "\n") for i, (_, op, a) in enumerate(items)] + odd = MALFORMED + ODD + digest = hashlib.sha256() + for line in lines + [m + "\n" for m in odd]: + digest.update(line.encode("utf-8")) + inputs_digest = digest.hexdigest() + served = {} + + def serve(): + served["out"] = run_parallel([str(EXE), "serve"], lines + [m + "\n" for m in odd], JOBS) + + server = threading.Thread(target=serve) + server.start() + wants = [model(op, a) for _, op, a in items] + server.join() + out, problems = served["out"] + print(f"{len(items)} requests answered, {time.monotonic() - started:.0f} s", flush=True) + for problem in problems: + failures.append(("(whole run)", "a clean run", problem)) + tool, odd_out = out[:len(items)], out[len(items):] + by_label = collections.Counter(f"{label} ({op})" for label, op, _ in items) + by_op = collections.Counter(op for _, op, _ in items) + refused = collections.Counter() + accepted_shapes = collections.Counter() + differ_model = 0 + spec_right = spec_total = 0 + autokey_stated = dict(autokey_cases()) + autokey_right = 0 + read_agrees = read_total = 0 + decoded = {} # octets -> the tool's answer, parsed + for i, ((label, op, a), (want, why), got) in enumerate(zip(items, wants, tool)): + if op == "decode": + b = a[0] + read_total += 1 + r = show_answer(read(b)[0]) + if r == want: + read_agrees += 1 + else: + failures.append((f"the model's two readings of {short(hexd(b), 120)}", want, r)) + if why is not None: + refused[why] += 1 + else: + p = decode_spec(b) + mac = "none" if not p[2] else "a crypto-NAK" if len(p[2]) == 4 else f"{len(p[2])} octets" + accepted_shapes[(p[0].version, min(len(p[1]), 5), mac)] += 1 + try: + decoded.setdefault(b, parse_decoded(got)) + except ValueError: + decoded.setdefault(b, ("unreadable", got[:200])) + if label == AUTOKEY_LABEL: + if got == want == autokey_stated[a[0]]: + autokey_right += 1 + else: + failures.append((f"the Autokey field in {short(hexd(a[0]), 120)}", autokey_stated[a[0]], + f"model {want}; tool {got}")) + if label == "Spec.lean's examples and rejects": + spec_total += 1 + stated_answer = next(w for o, x, w in SPEC_CASES if o == op and x == a) + spec_right += as_stated(want, stated_answer) + if not as_stated(want, stated_answer): + failures.append((f"the model on {short(request(op, a), 120)}", stated_answer, want)) + if got != want: + differ_model += 1 + failures.append((short(lines[i].rstrip("\r\n"), 160), short(want, 160), short(got, 160))) + malformed_right = sum(1 for x in odd_out[:len(MALFORMED)] if x == "error") + for m in MALFORMED: + if model_line(m + "\n") != "error": + failures.append((f"the model on {m!r}", "error", model_line(m + "\n"))) + odd_right = 0 + for m, x in zip(odd, odd_out): + want = model_line(m + "\n") + if x == want: + odd_right += m in ODD + else: + failures.append((f"request {short(m, 100)!r}", short(want, 100), short(x, 100))) + eof_right = 0 + for req, want in AT_EOF: + if model_line(req) != want: + failures.append((f"the model on {req!r} at end of input", want, model_line(req))) + got, problem = run_lines([str(EXE), "serve"], [req], timeout=60) + if got == [want] and not problem: + eof_right += 1 + else: + failures.append((repr(req) + " (at end of input)", want, f"{got} {problem or ''}".strip())) + cli = cli_cases() + cli_right, cli_failures = run_cli(cli) + failures += cli_failures + + # Unix time: Figure 4 and datetime. + fig4_right, fig4_total, fig4_named = 0, 0, collections.Counter() + dt_right = dt_total = dt_outside = 0 + answers_by_request = {} + for (label, op, a), got in zip(items, tool): + if op in ("unix", "fromunix"): + answers_by_request[(op, tuple(a))] = got + for date, mjd, era, offset in ([] if args.no_oracles else FIGURE_4): + unix_s = (mjd - UNIX_MJD) * 86400 + for op, a, want in (("unix", (era, offset * T32), f"OK {unix_s} 0"), + ("fromunix", (unix_s, 0), f"OK {era} {offset * T32}")): + fig4_total += 1 + got = answers_by_request[(op, a)] + if got == want: + fig4_right += 1 + elif offset == 0 and got == "FAIL": + fig4_named["Figure 4 gives 1 Jan 1900 the timestamp 0, which RFC 5905 section 6 makes " + "the value for unknown time: no Unix time, and no timestamp for that instant"] += 1 + else: + failures.append((f"Figure 4, {date}: {op} {a[0]} {a[1]}", want, got)) + for (op, a), got in ([] if args.no_oracles else answers_by_request.items()): + if not got.startswith("OK "): + continue + x, y = (int(v) for v in got.split(" ")[1:]) + era, raw, s, f = (a[0], a[1], x, y) if op == "unix" else (x, y, a[0], a[1]) + d = datetime_unix(era, raw) + if d is None: + dt_outside += 1 + continue + dt_total += 1 + if d == (s, f): + dt_right += 1 + else: + failures.append((f"datetime: {op} {a[0]} {a[1]}", f"OK {d[0]} {d[1]}", got)) + + # The oracles. + packets = list(decoded) + comparisons = {} + oracle_skipped = collections.Counter() + for name in ORACLE_ORDER: + if name not in oracles: + continue + t0 = time.monotonic() + if name == "ntpd-rs": + answers, problems = ntpdrs_answers(oracles[name], packets) + elif name == "NTPsec": + answers, problems = ntpsec_answers(oracles[name], packets) + elif name == "Wireshark": + answers, problems = tshark_answers(tmp, packets) + else: + answers, problems = ntplib_answers(oracles[name], packets) + for problem in problems: + failures.append((f"{name} (whole run)", "a clean run", problem)) + c = Comparison(name) + for b, want in zip(packets, answers): + if isinstance(want, str): + oracle_skipped[name] += 1 + continue + c.add(b, decoded[b], want) + failures += c.failures + comparisons[name] = c + print(f"{name}: {c.asked} packets, {c.agree} agree, {time.monotonic() - t0:.0f} s", flush=True) + + # The vectors of pull request 3. + vectors = vector_answers() + pr3 = Comparison("ntp#3") + pr3_rows = [] + for vid, b, mac_length, want in ([] if args.no_oracles else vectors): + pr3.add(b, decoded[b], want, mac_length, label=f"vector {vid} (mac_length {mac_length})") + got = VIEWS["ntp#3"](decoded[b]) + if got != want: + pr3_rows.append((vid, mac_length, show_tail(got, want), show_tail(want, got), + explain(b, "ntp#3", want, VIEWS["ntp#3"], mac_length))) + failures += pr3.failures + + n_inputs = len(items) + len(odd) + len(AT_EOF) + len(cli) + accepted = sum(accepted_shapes.values()) + lines_out = [ + "# Test results", + "", + "Written by `tests/run_tests.py`. Rerun it to check these numbers.", + "", + "- Oracle: the model of RFC 5905 and RFC 7822 in `tests/run_tests.py`, which every answer of the " + "tool must equal", + ] + for name in ORACLE_ORDER: + if name in described: + lines_out.append(f"- Oracle: {described[name]}") + if not args.no_oracles: + lines_out += [ + "- Oracle: RFC 5905 Figure 4, with errata 4025 and 6524, and Python's datetime module " + f"(Python {sys.version.split()[0]}), for the Unix times", + "- Oracle: the 79 vectors of Proven-Commons/ntp pull request 3 (orda-aska, commit cc5dab4), in " + "`tests/ntp-3-wire-vectors.jsonl`"] + lines_out += [ + "- Oracles left out: " + ("; ".join(f"{n} ({why})" for n, why in left_out) or "none") + + "; and dropped: chrony's parser (the session that wrote these tests was not permitted to build " + "chrony's downloaded source for a harness)", + f"- Lean: {lean_version()}", + f"- Executable: {built}; SHA-256 of the copy run {exe_hash}", + f"- Seed: {args.seed}", + f"- Count: {args.count}", + f"- Inputs: {n_inputs}", + f"- Inputs SHA-256: {inputs_digest}, of the request lines, in order; a rerun with the same seed and " + "count must give the same", + "- By kind: " + ", ".join(f"{k} {v}" for k, v in sorted(by_label.items())), + "- Requests: " + ", ".join(f"`{op}` {n}" for op, n in sorted(by_op.items())) + + f", malformed {len(MALFORMED)}, odd {len(ODD)}, at the end of input {len(AT_EOF)}, command line " + f"{len(cli)}", + f"- Answers that differ from the model: {differ_model}", + f"- Spec.lean's `examples` and `rejects`, answered by the model as Spec.lean states them: " + f"{spec_right} of {spec_total}", + f"- The model's two readings of each `decode` input, by `Valid` and `packetBytes` and by SPEC.md's " + f"one-pass rule, the same: {read_agrees} of {read_total}", + f"- Autokey fields (RFC 5906 Figure 7) whose Value Length runs past the field, as in CVE-2014-9750, " + f"read by their own Length with their octets unchanged, by the tool and the model: {autokey_right} " + f"of {by_label[AUTOKEY_LABEL + ' (decode)']}", + f"- Packets: {by_op['decode']}, of which the model reads {accepted}; why it refuses the others: " + + (tally(refused) or "none"), + "- Packets the model reads, by version, number of extension fields and MAC: " + + ", ".join(f"version {v}, {k}{'+' if k == 5 else ''} fields, MAC {m}: {n}" + for (v, k, m), n in sorted(accepted_shapes.items())), + f"- Malformed requests answered `error`: {malformed_right} of {len(MALFORMED)}; odd but " + f"well-formed requests answered as the model says: {odd_right} of {len(ODD)}", + f"- Requests ending in CR LF: every seventh, {len(items) // 7}", + f"- Last lines sent alone, with no LF, with CR LF, or with a lone CR, answered right: " + f"{eof_right} of {len(AT_EOF)}", + f"- Command line (`ntp serve` and wrong arguments), printing and exiting as the header of Main.lean " + f"says: {cli_right} of {len(cli)}", + ] + if args.no_oracles: + lines_out.append("- RFC 5905 Figure 4, Python's datetime and the vectors of pull request 3: left out " + "(--no-oracles)") + else: + lines_out += [ + f"- RFC 5905 Figure 4 (`unix` of each row's era and offset, and `fromunix` of its MJD as Unix " + f"time): {fig4_right} of {fig4_total} as the table gives them; named differences: " + + (tally(fig4_named) or "none"), + f"- Unix times the tool gives (`unix` answers, and the requests of `fromunix` answers), as " + f"Python's datetime gives them: {dt_right} of {dt_total}; outside datetime's years 1 to 9999: " + f"{dt_outside}"] + for name in ORACLE_ORDER: + if name not in comparisons: + lines_out.append(f"- {name}: left out") + continue + c = comparisons[name] + named = sum(n for n, _ in c.named.values()) + lines_out.append( + f"- {name}: {c.asked} distinct packets" + + (f" ({oracle_skipped[name]} more not sent: longer than tshark's {TSHARK_MAX}-octet frames)" + if oracle_skipped[name] else "") + + f"; the same answer as the tool: {c.agree} (of the {c.asked_read} the tool reads: " + f"{c.agree_read}); named differences: {named}; the model with {name}'s choices gives its " + f"answer: {c.model_right} of {c.asked}") + named = sum(n for n, _ in pr3.named.values()) + if not args.no_oracles: + lines_out.append(f"- Pull request 3: {pr3.asked} vectors; the same answer as the tool: {pr3.agree}; " + f"named differences: {named}; the model with its choices gives the vector's answer: " + f"{pr3.model_right} of {pr3.asked}") + lines_out += [ + f"- Time: {time.monotonic() - started:.0f} s", + f"- Failures: {len(failures)}", + "", + ] + if not args.no_oracles: + lines_out += ["## Where the oracles and the tool differ", "", + "Each difference is counted under the smallest set of the oracle's choices with which " + "the model gives the oracle's answer, so the choices named are the ones that make the " + "difference. The first case that showed each is given.", ""] + for name in ORACLE_ORDER: + if name not in comparisons: + continue + c = comparisons[name] + lines_out += [f"### {name}", "", f"The choices of {name} that the model can switch on:", ""] + lines_out += [f"{k}. {rule}" for k, rule in enumerate(ORACLE_RULES[name], 1)] + lines_out += ["", "The differences, by the choices that make them:", ""] + if not c.named: + lines_out.append("No difference.") + for rules, (n, example) in sorted(c.named.items(), key=lambda kv: -kv[1][0]): + lines_out.append(f"- {tags(name, rules)}: {n}. For example {example}") + lines_out.append("") + lines_out += ["### Pull request 3", "", "The choices of its contract that the model can switch on:", ""] + lines_out += [f"{k}. {rule}" for k, rule in enumerate(ORACLE_RULES["ntp#3"], 1)] + lines_out += ["", "Each vector whose answer differs from the tool's, with the choices that give the " + "vector's answer:", ""] + for vid, mac_length, tool_answer, want, rules in pr3_rows: + lines_out.append(f"- `{vid}` (mac_length {mac_length}), {tags('ntp#3', rules or ())}: the vector " + f"{short(want, 160)}; the tool {short(tool_answer, 160)}") + lines_out.append("") + if failures: + lines_out += ["## Failures", ""] + lines_out += [f"- {code(short(req, 300))}: expected {code(short(want, 300))}, got {code(short(got, 300))}" + for req, want, got in failures[:60]] + if len(failures) > 60: + lines_out.append(f"- and {len(failures) - 60} more") + lines_out.append("") + write(lines_out) + print(f"{n_inputs} inputs, {len(failures)} failures; results in {RESULTS}") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main())