diff --git a/tines-core/src/belief.rs b/tines-core/src/belief.rs index 7bc4c948..7e7deb4c 100644 --- a/tines-core/src/belief.rs +++ b/tines-core/src/belief.rs @@ -108,6 +108,7 @@ mod tests { topic: Topic::from_slice(b"door/3").unwrap(), payload: Payload::from_slice(b"open").unwrap(), dest: None, + sig: None, } } diff --git a/tines-core/src/consts.rs b/tines-core/src/consts.rs index ccb71a76..7dd2e4e0 100644 --- a/tines-core/src/consts.rs +++ b/tines-core/src/consts.rs @@ -18,6 +18,11 @@ pub const MAX_TOPIC: usize = 64; /// Maximum payload length in bytes. pub const MAX_PAYLOAD: usize = 1024; +/// Maximum signature length in bytes. The 1.0 signature slot reserves this on +/// every observation (`[crate::observation::Signature]`); it is carried verbatim +/// and never signed or verified until 1.1. +pub const MAX_SIG: usize = 64; + /// Maximum number of peers a node tracks. pub const MAX_PEERS: usize = 8; @@ -36,6 +41,12 @@ pub const REASSEMBLY_TIMEOUT_MS: u64 = 5000; /// Maximum number of frames buffered for outbound delivery. pub const SEND_QUEUE_CAP: usize = 256; +/// Minimum usable UDP frame size in bytes (the BLE floor below which a +/// fragment header leaves no room for payload). [`crate::frag::fragment`] gives +/// up cleanly with `WireError::TooLarge` for any `mtu` below this rather than +/// emitting a truncated frame; the MTU sweep runs from here to [`MAX_FRAME`]. +pub const MIN_MTU: usize = 23; + /// Maximum UDP frame size in bytes. pub const MAX_FRAME: usize = 1500; diff --git a/tines-core/src/exchange.rs b/tines-core/src/exchange.rs index 3187c704..e3f08d79 100644 --- a/tines-core/src/exchange.rs +++ b/tines-core/src/exchange.rs @@ -1292,6 +1292,7 @@ mod tests { topic: Topic::from_slice(topic).unwrap(), payload: Payload::from_slice(payload).unwrap(), dest: None, + sig: None, } } diff --git a/tines-core/src/frag.rs b/tines-core/src/frag.rs index 8a8dde70..61d66f74 100644 --- a/tines-core/src/frag.rs +++ b/tines-core/src/frag.rs @@ -218,6 +218,113 @@ mod tests { topic: Topic::from_slice(b"door/3").unwrap(), payload: Payload::from_slice(&std::vec![0u8; payload_len]).unwrap(), dest: None, + sig: None, + } + } + + fn signed_max_obs(sig: crate::Signature) -> Observation { + let mut obs = obs_with_payload([1; 16], consts::MAX_PAYLOAD); + obs.sig = Some(sig); + obs + } + + #[test] + fn reassembled_bad_signature_is_malformed() { + // A maximum-size signed obs carrying a malformed Ed25519 signature (wrong + // length) must be rejected on reassembly — never returned — exactly as a + // single-frame decode would reject it. + let obs = signed_max_obs(crate::Signature { + version: 1, + algo: crate::SigAlgo::Ed25519, + bytes: heapless::Vec::from_slice(&[0u8; 63]).unwrap(), + }); + let mut frames: Vec> = Vec::new(); + fragment([1; 16], &obs, 200, &mut |b| frames.push(b.to_vec())) + .expect("fragment signed max obs"); + let mut r = Reassembler::new(); + let mut last: Option = None; + for f in frames.iter() { + if let Frame::Frag { + id, + index, + count, + data, + .. + } = Frame::decode(f).unwrap() + { + last = r.feed(id, index, count, &data, 0); + } + } + assert!( + last.is_none(), + "a malformed signature on reassembly must be rejected, never returned" + ); + } + + #[test] + fn reassembled_unknown_algo_signature_roundtrips() { + // An unknown-algorithm signature is carried verbatim on reassembly too. + let obs = signed_max_obs(crate::Signature { + version: 2, + algo: crate::SigAlgo::Unknown(7), + bytes: heapless::Vec::from_slice(&[9u8; 10]).unwrap(), + }); + let mut frames: Vec> = Vec::new(); + fragment([1; 16], &obs, 200, &mut |b| frames.push(b.to_vec())) + .expect("fragment signed max obs"); + let mut r = Reassembler::new(); + let mut last: Option = None; + for f in frames.iter() { + if let Frame::Frag { + id, + index, + count, + data, + .. + } = Frame::decode(f).unwrap() + { + last = r.feed(id, index, count, &data, 0); + } + } + assert_eq!(last, Some(obs), "unknown-algo sig must reassemble verbatim"); + } + + #[test] + fn fragmentation_fits_mtu_sweep() { + // Review Focus #6: a full-size signed obs (MAX_PAYLOAD + 64-byte Ed25519 + // sig) must fragment into <= MAX_FRAGMENTS frames each <= mtu, or — below + // the BLE floor — be given up cleanly as TooLarge, never emitted + // oversized, across every mtu from MIN_MTU to MAX_FRAME. + let obs = signed_max_obs(crate::Signature { + version: 1, + algo: crate::SigAlgo::Ed25519, + bytes: heapless::Vec::from_slice(&[0u8; 64]).unwrap(), + }); + for mtu in consts::MIN_MTU..=consts::MAX_FRAME { + let mut frames: Vec> = Vec::new(); + match fragment([1; 16], &obs, mtu, &mut |b| frames.push(b.to_vec())) { + Ok(count) => { + assert!( + count <= MAX_FRAGMENTS, + "frame count {count} exceeds MAX_FRAGMENTS {MAX_FRAGMENTS}" + ); + assert_eq!(frames.len(), count, "emit count must match at mtu {mtu}"); + for f in &frames { + assert!( + f.len() <= mtu, + "frame of {} bytes exceeds mtu {mtu}", + f.len() + ); + } + } + Err(crate::wire::WireError::TooLarge) => { + assert!( + frames.is_empty(), + "TooLarge at mtu {mtu} must emit no frames" + ); + } + Err(other) => panic!("unexpected error {other:?} at mtu {mtu}"), + } } } diff --git a/tines-core/src/lib.rs b/tines-core/src/lib.rs index 39ef5f30..49bee3b0 100644 --- a/tines-core/src/lib.rs +++ b/tines-core/src/lib.rs @@ -22,7 +22,9 @@ pub use bloom::Bloom; pub use exchange::{DropReason, Exchange, NodeEvent, Peer}; pub use frag::{fragment, Reassembler}; pub use node::{Node, ObserveError}; -pub use observation::{Dest, NodeId, Observation, ObservationId, Payload, Tick, Topic}; +pub use observation::{ + Dest, NodeId, Observation, ObservationId, Payload, SigAlgo, Signature, Tick, Topic, +}; pub use store::{Store, StoreError, TrimPolicy}; pub use transport::{Event, FrameBuf, SendError, Transport}; pub use wire::{Frame, WireError}; diff --git a/tines-core/src/mem_store.rs b/tines-core/src/mem_store.rs index 39d9b0d0..dc9bfd1a 100644 --- a/tines-core/src/mem_store.rs +++ b/tines-core/src/mem_store.rs @@ -215,6 +215,7 @@ mod tests { topic: Topic::from_slice(topic).unwrap(), payload: Payload::from_slice(b"").unwrap(), dest: None, + sig: None, } } diff --git a/tines-core/src/node.rs b/tines-core/src/node.rs index 7646b259..03584871 100644 --- a/tines-core/src/node.rs +++ b/tines-core/src/node.rs @@ -92,6 +92,7 @@ impl Node { topic, payload, dest: None, + sig: None, }; self.next_seq += 1; let id = obs.id(); @@ -128,6 +129,7 @@ impl Node { topic, payload, dest: Some(Dest { to, lifetime_ms }), + sig: None, }; self.next_seq += 1; let id = obs.id(); diff --git a/tines-core/src/observation.rs b/tines-core/src/observation.rs index b6f7b6cd..eae682e0 100644 --- a/tines-core/src/observation.rs +++ b/tines-core/src/observation.rs @@ -3,6 +3,7 @@ use crate::consts; use blake2::{Blake2s256, Digest}; use heapless::Vec; +use serde::de::Deserializer; use serde::{Deserialize, Serialize}; /// 16-byte node identifier. @@ -55,6 +56,13 @@ pub struct Observation { /// Addressing: `None` is a broadcast, `Some(dest)` is addressed to /// `dest.to` and expires per `dest.lifetime_ms`. pub dest: Option, + + /// The observer's signature over this observation, if any. + /// + /// The 1.0 wire reserves this slot on every observation and carries a + /// signature verbatim (see [`Signature`] / [`SigAlgo`]), but 1.0 does not sign + /// or verify it. Signing and verification arrive in 1.1. + pub sig: Option, } impl Observation { @@ -103,6 +111,7 @@ impl Observation { topic: self.topic.clone(), payload: self.payload.clone(), dest: self.dest.clone(), + sig: self.sig.clone(), } } @@ -118,6 +127,7 @@ impl Observation { topic: Topic::from_slice(topic).expect("topic fits MAX_TOPIC"), payload: Payload::from_slice(payload).expect("payload fits MAX_PAYLOAD"), dest: None, + sig: None, } } @@ -141,6 +151,7 @@ impl Observation { topic: Topic::from_slice(topic).expect("topic fits MAX_TOPIC"), payload: Payload::from_slice(payload).expect("payload fits MAX_PAYLOAD"), dest: Some(Dest { to, lifetime_ms }), + sig: None, } } @@ -152,6 +163,115 @@ impl Observation { } } +/// A signature scheme identifier, carried in [`Signature::algo`]. +/// +/// On the wire `algo` is a raw `u8` — the `#[repr(u8)]` discriminant, *not* a +/// postcard enum tag: [`SigAlgo::Ed25519`] is the byte `1`, and an algorithm +/// `tines-core` does not recognize is carried as [`SigAlgo::Unknown`] with its +/// raw id, never rejected. `0` is never valid here (it is `Malformed` on decode). +/// +/// Signing and verification arrive in 1.1. +#[non_exhaustive] +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SigAlgo { + /// Ed25519: a 64-byte Ed25519 signature. + Ed25519 = 1, + /// An algorithm id `tines-core` does not recognize, carried verbatim. + /// Any `u8` other than `0` (the malformed sentinel) and `1` (`Ed25519`) is + /// preserved so a 1.0 node can forward it untouched to a 1.1 node. + Unknown(u8), +} + +/// An observation's signature: the observer's over the whole observation. +/// +/// The slot is reserved on the 1.0 wire but *unsigned* in 1.0: this type only +/// carries a signature verbatim, enforcing the shape rules below on decode. +/// Signing and verification arrive in 1.1. +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct Signature { + /// Signature format version. `0` is rejected as `Malformed` on decode; + /// every other value (including any version `tines-core` does not know) is + /// carried verbatim. + pub version: u8, + /// The algorithm the signature is over. Serialized as a raw `u8` on the + /// wire (its `#[repr(u8)]` discriminant, so [`SigAlgo::Ed25519`] is `1`); + /// unknown ids are preserved as [`SigAlgo::Unknown`] rather than rejected. + #[serde(with = "sig_algo_wire")] + pub algo: SigAlgo, + /// The signature bytes. For [`SigAlgo::Ed25519`] these MUST be exactly 64 + /// bytes (a different length is `Malformed`); other algorithms carry any + /// length. Never rejected for being unparseable — carried as-is. + pub bytes: heapless::Vec, +} + +impl<'de> Deserialize<'de> for Signature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct RawSignature { + version: u8, + #[serde(with = "sig_algo_wire")] + algo: SigAlgo, + bytes: heapless::Vec, + } + let raw = RawSignature::deserialize(deserializer)?; + // A malformed signature (bad version, bad or wrong-length algorithm) is + // carried by no frame: it is a decode failure, never a panic. + if raw.version == 0 { + return Err(serde::de::Error::custom("signature version 0 is malformed")); + } + if matches!(raw.algo, SigAlgo::Ed25519) && raw.bytes.len() != 64 { + return Err(serde::de::Error::custom( + "Ed25519 signature must be exactly 64 bytes", + )); + } + Ok(Signature { + version: raw.version, + algo: raw.algo, + bytes: raw.bytes, + }) + } +} + +/// Wire encoding of [`SigAlgo`]: a raw `u8` (its `#[repr(u8)]` discriminant), +/// not a postcard enum tag. Used via `#[serde(with = "sig_algo_wire")]` on the +/// [`Signature::algo`] field so an unknown id survives decode as [`SigAlgo::Unknown`]. +mod sig_algo_wire { + use super::SigAlgo; + use serde::de::Deserializer; + use serde::ser::Serializer; + use serde::Deserialize; + + pub fn serialize(algo: &SigAlgo, serializer: S) -> Result + where + S: Serializer, + { + match algo { + // `Ed25519` is the byte `1`; any carried `Unknown(n)` is its raw id. + SigAlgo::Ed25519 => serializer.serialize_u8(1), + SigAlgo::Unknown(id) => serializer.serialize_u8(*id), + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw = u8::deserialize(deserializer)?; + // `0` is the malformed sentinel on the wire; `1` is `Ed25519`; every + // other id is carried as `Unknown(n)` so it forwards untouched. + match raw { + 0 => Err(serde::de::Error::custom("signature algo id 0 is malformed")), + 1 => Ok(SigAlgo::Ed25519), + id => Ok(SigAlgo::Unknown(id)), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -165,9 +285,31 @@ mod tests { topic: Topic::from_slice(b"door/3").unwrap(), payload: Payload::from_slice(b"open").unwrap(), dest, + sig: None, } } + fn sig(version: u8, algo: SigAlgo, bytes: &[u8]) -> Signature { + Signature { + version, + algo, + bytes: Vec::from_slice(bytes).unwrap(), + } + } + + #[test] + fn sig_excluded_from_id() { + // A signature must not affect identity: a bare, a full Ed25519 and an + // arbitrary unknown-algorithm sig all share the observation's id. + let base = obs(5, 0, None, None); + let mut signed = obs(5, 0, None, None); + signed.sig = Some(sig(1, SigAlgo::Ed25519, &[0u8; 64])); + assert_eq!(base.id(), signed.id(), "sig must not affect id"); + let mut other = obs(5, 0, None, None); + other.sig = Some(sig(2, SigAlgo::Unknown(9), &[1u8; 10])); + assert_eq!(base.id(), other.id()); + } + #[test] fn id_ignores_age_and_observed_at() { assert_eq!(obs(1, 0, None, None).id(), obs(1, 999, Some(5), None).id()); @@ -360,4 +502,47 @@ mod tests { ] ); } + + #[test] + fn sig_algo_wire_roundtrip() { + // Review Focus #3: the raw-u8 wire encoding round-trips a known Ed25519 + // and every unknown id verbatim, rejects the malformed sentinel, and + // never maps a wire byte of 1 to Unknown(1). Exercises the algo field + // through Signature, since SigAlgo serializes only via its serde module. + let make = |algo: SigAlgo| Signature { + version: 1, + algo, + bytes: heapless::Vec::::from_slice(&[0u8; 64]).unwrap(), + }; + for (algo, wire_byte) in [ + (SigAlgo::Ed25519, 1u8), + (SigAlgo::Unknown(7), 7u8), + (SigAlgo::Unknown(255), 255u8), + ] { + let sig = make(algo); + let mut buf = [0u8; consts::MAX_SIG + 8]; + let n = postcard::to_slice(&sig, &mut buf).unwrap(); + assert_eq!(n[0], 1, "version serializes as the first byte"); + assert_eq!(n[1], wire_byte, "algo is its raw repr discriminant"); + let decoded: Signature = postcard::from_bytes(&n[..]).unwrap(); + assert_eq!(decoded.algo, algo, "algo must round-trip verbatim"); + } + // `0` is the malformed sentinel on the wire — it must fail to decode. + let sig = make(SigAlgo::Unknown(0)); + let mut buf = [0u8; consts::MAX_SIG + 8]; + let n = postcard::to_slice(&sig, &mut buf).unwrap(); + assert_eq!(n[1], 0); + let bad: Result = postcard::from_bytes::(n); + assert!(bad.is_err(), "algo id 0 must be malformed"); + // A wire byte of 1 is Ed25519, never Unknown(1). + let sig = make(SigAlgo::Ed25519); + let mut buf = [0u8; consts::MAX_SIG + 8]; + let n = postcard::to_slice(&sig, &mut buf).unwrap(); + assert_eq!(n[1], 1); + assert_eq!( + postcard::from_bytes::(n).unwrap().algo, + SigAlgo::Ed25519, + "wire byte 1 must decode to Ed25519" + ); + } } diff --git a/tines-core/src/wire.rs b/tines-core/src/wire.rs index 93b9b173..57c38535 100644 --- a/tines-core/src/wire.rs +++ b/tines-core/src/wire.rs @@ -126,8 +126,7 @@ pub enum WireError { #[cfg(test)] mod tests { use super::*; - use crate::Payload; - use crate::Topic; + use crate::{Payload, SigAlgo, Signature, Topic}; fn obs_frame() -> Frame { Frame::Obs { @@ -140,6 +139,7 @@ mod tests { topic: Topic::from_slice(b"door/3").unwrap(), payload: Payload::from_slice(b"open").unwrap(), dest: None, + sig: None, }, } } @@ -155,6 +155,7 @@ mod tests { topic: Topic::from_slice(b"t").unwrap(), payload: Payload::from_slice(&[0u8; 1024]).unwrap(), dest: None, + sig: None, }, } } @@ -476,4 +477,208 @@ mod tests { let mut buf = [0u8; 16]; assert_eq!(f.encode(&mut buf).err(), Some(WireError::TooLarge)); } + + fn sig(version: u8, algo: SigAlgo, bytes: &[u8]) -> Signature { + Signature { + version, + algo, + bytes: Vec::from_slice(bytes).unwrap(), + } + } + + fn signed_obs(sig: Signature) -> Observation { + Observation { + observer: [7; 16], + seq: 42, + age_ms: 123, + observed_at: Some(999), + topic: Topic::from_slice(b"door/3").unwrap(), + payload: Payload::from_slice(b"open").unwrap(), + dest: None, + sig: Some(sig), + } + } + + #[test] + fn obs_roundtrip_with_ed25519_signature() { + let f = Frame::Obs { + from: [7; 16], + obs: signed_obs(sig(1, SigAlgo::Ed25519, &[0u8; 64])), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + let decoded = Frame::decode(&buf[..n]).unwrap(); + assert_eq!(f, decoded); + } + + #[test] + fn obs_roundtrip_with_unknown_algo() { + // An unknown algorithm id with short bytes is carried verbatim. + let f = Frame::Obs { + from: [7; 16], + obs: signed_obs(sig(2, SigAlgo::Unknown(7), &[9u8; 10])), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + let decoded = Frame::decode(&buf[..n]).unwrap(); + assert_eq!(f, decoded); + } + + #[test] + fn algo_zero_is_malformed() { + let f = Frame::Obs { + from: [7; 16], + obs: signed_obs(sig(1, SigAlgo::Unknown(0), &[0u8; 64])), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + assert_eq!(Frame::decode(&buf[..n]).err(), Some(WireError::Malformed)); + } + + #[test] + fn version_zero_is_malformed() { + let f = Frame::Obs { + from: [7; 16], + obs: signed_obs(sig(0, SigAlgo::Ed25519, &[0u8; 64])), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + assert_eq!(Frame::decode(&buf[..n]).err(), Some(WireError::Malformed)); + } + + #[test] + fn ed25519_wrong_length_is_malformed() { + let f = Frame::Obs { + from: [7; 16], + obs: signed_obs(sig(1, SigAlgo::Ed25519, &[0u8; 63])), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + assert_eq!(Frame::decode(&buf[..n]).err(), Some(WireError::Malformed)); + } + + #[test] + fn raw_one_decodes_to_ed25519() { + // A wire algo byte of 1 must decode to Ed25519, never Unknown(1). + let f = Frame::Obs { + from: [7; 16], + obs: signed_obs(sig(1, SigAlgo::Unknown(1), &[0u8; 64])), + }; + let mut buf = [0u8; consts::MAX_FRAME]; + let n = f.encode(&mut buf).unwrap(); + let decoded = Frame::decode(&buf[..n]).unwrap(); + match decoded { + Frame::Obs { obs, .. } => { + assert_eq!(obs.sig.as_ref().unwrap().algo, SigAlgo::Ed25519); + } + other => panic!("expected Obs, got {other:?}"), + } + } + + // Pinned wire vector for a full-size (MAX_PAYLOAD + 64-byte Ed25519) signed + // Observation, exactly as a 1.0 node would emit it: the golden vector for + // review focus #5. Decoding it must yield the bare Observation with a + // SigAlgo::Ed25519 carrying 64 zeroed signature bytes. + const OBS_GOLDEN_VECTOR_ED25519: [u8; 1139] = [ + 0x04, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x06, 0x64, 0x6f, 0x6f, 0x72, 0x2f, 0x33, 0x80, + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + #[test] + fn golden_vector_ed25519_decodes_to_bare_obs() { + // Review Focus #5: the pinned vector is the golden Obs vector — decoding + // it must yield the bare Observation with an Ed25519 signature carrying + // 64 zeroed bytes. + let frame = Frame::decode(&OBS_GOLDEN_VECTOR_ED25519).unwrap(); + let Frame::Obs { from, obs } = frame else { + panic!("expected an Obs frame") + }; + assert_eq!(from, [1; 16]); + assert_eq!(obs.observer, [1; 16]); + assert_eq!(obs.seq, 1); + assert_eq!(obs.age_ms, 0); + assert_eq!(obs.observed_at, None); + assert_eq!(&obs.topic[..], b"door/3"); + assert_eq!(obs.payload.len(), consts::MAX_PAYLOAD); + assert_eq!(obs.dest, None); + let sig = obs.sig.expect("a signed obs carries a signature"); + assert_eq!(sig.version, 1); + assert_eq!(sig.algo, SigAlgo::Ed25519); + assert_eq!(sig.bytes.len(), 64); + assert!(sig.bytes.iter().all(|b| *b == 0)); + } } diff --git a/tines-store/src/log.rs b/tines-store/src/log.rs index 44cdf203..090fe73a 100644 --- a/tines-store/src/log.rs +++ b/tines-store/src/log.rs @@ -1024,6 +1024,7 @@ mod tests { topic: tines_core::Topic::from_slice(b"door/3").unwrap(), payload: tines_core::Payload::from_slice(b"open").unwrap(), dest: None, + sig: None, } } diff --git a/tines-store/tests/persistence.rs b/tines-store/tests/persistence.rs index 4a7ebf52..14545891 100644 --- a/tines-store/tests/persistence.rs +++ b/tines-store/tests/persistence.rs @@ -21,6 +21,7 @@ fn obs(observer: [u8; 16], seq: u64, age: u64, at: Option) -> Observation { topic: Topic::from_slice(b"door/3").unwrap(), payload: Payload::from_slice(b"open").unwrap(), dest: None, + sig: None, } } @@ -346,6 +347,7 @@ mod fleet_fixes_f5 { topic: Topic::from_slice(b"door/3").unwrap(), payload: Payload::from_slice(b"open").unwrap(), dest: None, + sig: None, } }