#![allow(missing_docs)]
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
pub type RingFingerprint = [u8; 32];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PeerStatus {
Unknown,
Connected,
Disconnected,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerRecord {
pub fingerprint: RingFingerprint,
pub address: String,
pub ring_signer_pubkey: [u8; 32],
pub status: PeerStatus,
pub last_seen_ns: u64,
pub ring_name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RingAnnounce {
pub ring_fingerprint: RingFingerprint,
pub ring_signer_pubkey: [u8; 32],
pub ring_name: String,
pub capabilities: Vec<String>,
pub cycle_count: u64,
pub orientation: OrientationSummary,
#[serde(with = "sig_bytes")]
pub signature: [u8; 64],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrientationSummary {
pub ring_type: String,
pub organs: Vec<String>,
pub siblings: Vec<String>,
pub command_namespaces: Vec<String>,
pub last_cycle_ns: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescriptorEnvelope {
pub source_fingerprint: RingFingerprint,
pub destination: EnvelopeDestination,
pub payload: DescriptorPayload,
pub source_cycle: u64,
pub timestamp_ns: u64,
#[serde(with = "sig_bytes")]
pub signature: [u8; 64],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EnvelopeDestination {
Ring(RingFingerprint),
Broadcast,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DescriptorPayload {
Descriptor(Vec<u8>),
Command { namespace: String, tree: Vec<u8> },
Orientation(OrientationSummary),
}
pub mod federation_ns {
pub const ANNOUNCE: &str = "federation.announce";
pub const DESCRIPTOR: &str = "federation.descriptor";
pub const HEALTH: &str = "federation.health";
pub const TOPOLOGY: &str = "federation.topology";
pub const COMMAND: &str = "federation.command";
pub const PEERS: &str = "federation.peers";
}
mod sig_bytes {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8; 64], s: S) -> Result<S::Ok, S::Error> {
bytes.as_slice().serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> {
let v: Vec<u8> = Deserialize::deserialize(d)?;
v.try_into()
.map_err(|v: Vec<u8>| serde::de::Error::custom(format!("expected 64 bytes, got {}", v.len())))
}
}
impl RingAnnounce {
fn signable_bytes(&self) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&self.ring_fingerprint);
buf.extend_from_slice(&self.ring_signer_pubkey);
buf.extend_from_slice(self.ring_name.as_bytes());
for cap in &self.capabilities {
buf.extend_from_slice(cap.as_bytes());
}
buf.extend_from_slice(&self.cycle_count.to_le_bytes());
buf.extend_from_slice(&orientation_bytes(&self.orientation));
buf
}
pub fn sign(&mut self, key: &SigningKey) {
let msg = self.signable_bytes();
let sig = key.sign(&msg);
self.signature = sig.to_bytes();
}
pub fn verify(&self, key: &VerifyingKey) -> Result<(), ed25519_dalek::SignatureError> {
let msg = self.signable_bytes();
let sig = Signature::from_bytes(&self.signature);
key.verify(&msg, &sig)
}
}
impl DescriptorEnvelope {
fn signable_bytes(&self) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&self.source_fingerprint);
match &self.destination {
EnvelopeDestination::Ring(fp) => {
buf.push(0x01);
buf.extend_from_slice(fp);
}
EnvelopeDestination::Broadcast => {
buf.push(0x02);
}
}
match &self.payload {
DescriptorPayload::Descriptor(data) => {
buf.push(0x01);
buf.extend_from_slice(data);
}
DescriptorPayload::Command { namespace, tree } => {
buf.push(0x02);
buf.extend_from_slice(namespace.as_bytes());
buf.extend_from_slice(tree);
}
DescriptorPayload::Orientation(o) => {
buf.push(0x03);
buf.extend_from_slice(&orientation_bytes(o));
}
}
buf.extend_from_slice(&self.source_cycle.to_le_bytes());
buf.extend_from_slice(&self.timestamp_ns.to_le_bytes());
buf
}
pub fn sign(&mut self, key: &SigningKey) {
let msg = self.signable_bytes();
let sig = key.sign(&msg);
self.signature = sig.to_bytes();
}
pub fn verify(&self, key: &VerifyingKey) -> Result<(), ed25519_dalek::SignatureError> {
let msg = self.signable_bytes();
let sig = Signature::from_bytes(&self.signature);
key.verify(&msg, &sig)
}
}
fn orientation_bytes(o: &OrientationSummary) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(o.ring_type.as_bytes());
for organ in &o.organs {
buf.extend_from_slice(organ.as_bytes());
}
for sibling in &o.siblings {
buf.extend_from_slice(sibling.as_bytes());
}
for ns in &o.command_namespaces {
buf.extend_from_slice(ns.as_bytes());
}
buf.extend_from_slice(&o.last_cycle_ns.to_le_bytes());
buf
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn test_orientation() -> OrientationSummary {
OrientationSummary {
ring_type: "test-ring".into(),
organs: vec!["organ-a".into(), "organ-b".into(), "organ-c".into()],
siblings: vec!["sibling-x".into()],
command_namespaces: vec!["ns_a".into(), "ns_b".into()],
last_cycle_ns: 1_000_000,
}
}
fn test_signing_key() -> SigningKey {
SigningKey::from_bytes(&[42u8; 32])
}
fn test_announce(key: &SigningKey) -> RingAnnounce {
RingAnnounce {
ring_fingerprint: [1u8; 32],
ring_signer_pubkey: key.verifying_key().to_bytes(),
ring_name: "test-ring".into(),
capabilities: vec!["deploy".into(), "observe".into()],
cycle_count: 100,
orientation: test_orientation(),
signature: [0u8; 64],
}
}
fn test_envelope() -> DescriptorEnvelope {
DescriptorEnvelope {
source_fingerprint: [2u8; 32],
destination: EnvelopeDestination::Broadcast,
payload: DescriptorPayload::Descriptor(vec![10, 20, 30]),
source_cycle: 42,
timestamp_ns: 999_999,
signature: [0u8; 64],
}
}
#[test]
fn announce_json_roundtrip() {
let key = test_signing_key();
let mut a = test_announce(&key);
a.sign(&key);
let json = serde_json::to_string(&a).unwrap();
let b: RingAnnounce = serde_json::from_str(&json).unwrap();
assert_eq!(a, b);
}
#[test]
fn announce_bincode_roundtrip() {
let key = test_signing_key();
let mut a = test_announce(&key);
a.sign(&key);
let bytes = bincode_legacy::serialize(&a).unwrap();
let b: RingAnnounce = bincode_legacy::deserialize(&bytes).unwrap();
assert_eq!(a, b);
}
#[test]
fn envelope_descriptor_json_roundtrip() {
let key = test_signing_key();
let mut e = test_envelope();
e.sign(&key);
let json = serde_json::to_string(&e).unwrap();
let f: DescriptorEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(e, f);
}
#[test]
fn envelope_command_bincode_roundtrip() {
let key = test_signing_key();
let mut e = DescriptorEnvelope {
source_fingerprint: [3u8; 32],
destination: EnvelopeDestination::Ring([4u8; 32]),
payload: DescriptorPayload::Command {
namespace: "ns_a.create".into(),
tree: vec![0xCA, 0xFE],
},
source_cycle: 7,
timestamp_ns: 12345,
signature: [0u8; 64],
};
e.sign(&key);
let bytes = bincode_legacy::serialize(&e).unwrap();
let f: DescriptorEnvelope = bincode_legacy::deserialize(&bytes).unwrap();
assert_eq!(e, f);
}
#[test]
fn envelope_orientation_bincode_roundtrip() {
let key = test_signing_key();
let mut e = DescriptorEnvelope {
source_fingerprint: [5u8; 32],
destination: EnvelopeDestination::Broadcast,
payload: DescriptorPayload::Orientation(test_orientation()),
source_cycle: 99,
timestamp_ns: 777,
signature: [0u8; 64],
};
e.sign(&key);
let bytes = bincode_legacy::serialize(&e).unwrap();
let f: DescriptorEnvelope = bincode_legacy::deserialize(&bytes).unwrap();
assert_eq!(e, f);
}
#[test]
fn announce_sign_populates_signature() {
let key = test_signing_key();
let mut a = test_announce(&key);
assert_eq!(a.signature, [0u8; 64]);
a.sign(&key);
assert_ne!(a.signature, [0u8; 64]);
}
#[test]
fn announce_verify_valid() {
let key = test_signing_key();
let mut a = test_announce(&key);
a.sign(&key);
assert!(a.verify(&key.verifying_key()).is_ok());
}
#[test]
fn announce_verify_tampered() {
let key = test_signing_key();
let mut a = test_announce(&key);
a.sign(&key);
a.cycle_count = 999;
assert!(a.verify(&key.verifying_key()).is_err());
}
#[test]
fn envelope_sign_populates_signature() {
let key = test_signing_key();
let mut e = test_envelope();
assert_eq!(e.signature, [0u8; 64]);
e.sign(&key);
assert_ne!(e.signature, [0u8; 64]);
}
#[test]
fn envelope_verify_valid() {
let key = test_signing_key();
let mut e = test_envelope();
e.sign(&key);
assert!(e.verify(&key.verifying_key()).is_ok());
}
#[test]
fn envelope_verify_tampered() {
let key = test_signing_key();
let mut e = test_envelope();
e.sign(&key);
e.source_cycle = 0;
assert!(e.verify(&key.verifying_key()).is_err());
}
#[test]
fn namespace_constants() {
assert_eq!(federation_ns::ANNOUNCE, "federation.announce");
assert_eq!(federation_ns::DESCRIPTOR, "federation.descriptor");
assert_eq!(federation_ns::HEALTH, "federation.health");
assert_eq!(federation_ns::TOPOLOGY, "federation.topology");
assert_eq!(federation_ns::COMMAND, "federation.command");
}
#[test]
fn orientation_json_roundtrip() {
let o = test_orientation();
let json = serde_json::to_string(&o).unwrap();
let p: OrientationSummary = serde_json::from_str(&json).unwrap();
assert_eq!(o, p);
}
#[test]
fn envelope_broadcast_bincode_roundtrip() {
let key = test_signing_key();
let mut e = test_envelope();
assert!(matches!(e.destination, EnvelopeDestination::Broadcast));
e.sign(&key);
let bytes = bincode_legacy::serialize(&e).unwrap();
let f: DescriptorEnvelope = bincode_legacy::deserialize(&bytes).unwrap();
assert_eq!(e, f);
}
#[test]
fn ring_fingerprint_is_u8_32() {
let fp: RingFingerprint = [0xABu8; 32];
let raw: [u8; 32] = fp;
assert_eq!(raw, [0xABu8; 32]);
}
#[test]
fn peer_record_json_roundtrip() {
let record = PeerRecord {
fingerprint: [0xAAu8; 32],
address: "192.168.1.10:9100".into(),
ring_signer_pubkey: [0xBBu8; 32],
status: PeerStatus::Connected,
last_seen_ns: 1_000_000,
ring_name: Some("test-ring".into()),
};
let json = serde_json::to_string(&record).unwrap();
let decoded: PeerRecord = serde_json::from_str(&json).unwrap();
assert_eq!(record, decoded);
}
#[test]
fn peer_record_bincode_roundtrip() {
let record = PeerRecord {
fingerprint: [0xCCu8; 32],
address: "10.0.0.1:9100".into(),
ring_signer_pubkey: [0xDDu8; 32],
status: PeerStatus::Unknown,
last_seen_ns: 0,
ring_name: None,
};
let bytes = bincode_legacy::serialize(&record).unwrap();
let decoded: PeerRecord = bincode_legacy::deserialize(&bytes).unwrap();
assert_eq!(record, decoded);
}
#[test]
fn peer_status_variants() {
assert_ne!(PeerStatus::Unknown, PeerStatus::Connected);
assert_ne!(PeerStatus::Connected, PeerStatus::Disconnected);
assert_ne!(PeerStatus::Unknown, PeerStatus::Disconnected);
}
#[test]
fn namespace_peers_constant() {
assert_eq!(federation_ns::PEERS, "federation.peers");
}
#[test]
fn federation_signs_with_arbitrary_organ_labels() {
let key = test_signing_key();
let mut announce = RingAnnounce {
ring_fingerprint: [7u8; 32],
ring_signer_pubkey: key.verifying_key().to_bytes(),
ring_name: "custom-ring-application".into(),
capabilities: vec!["domain-specific".into()],
cycle_count: 1,
orientation: OrientationSummary {
ring_type: "alt-app".into(),
organs: vec![
"α".into(),
"β".into(),
"γ".into(),
"δ".into(),
"ε".into(),
"ζ".into(),
],
siblings: vec!["arbitrary-sibling".into()],
command_namespaces: vec!["custom_ns_x".into(), "custom_ns_y".into()],
last_cycle_ns: 0,
},
signature: [0u8; 64],
};
announce.sign(&key);
assert!(announce.verify(&key.verifying_key()).is_ok());
announce.orientation.organs[0] = "ω".into();
assert!(announce.verify(&key.verifying_key()).is_err());
}
#[test]
fn federation_byte_format_unchanged_across_rename() {
let announce = RingAnnounce {
ring_fingerprint: [0x11u8; 32],
ring_signer_pubkey: [0x22u8; 32],
ring_name: "wire-fixture".into(),
capabilities: vec!["cap-a".into()],
cycle_count: 1234,
orientation: OrientationSummary {
ring_type: "fixture".into(),
organs: vec!["A".into()],
siblings: vec![],
command_namespaces: vec!["ns".into()],
last_cycle_ns: 999,
},
signature: [0u8; 64],
};
let bytes = bincode_legacy::serialize(&announce).unwrap();
for i in 0..32 {
assert_eq!(bytes[i], 0x11, "ring_fingerprint byte {i} drift");
}
for i in 32..64 {
assert_eq!(bytes[i], 0x22, "ring_signer_pubkey byte {i} drift");
}
let decoded: RingAnnounce = bincode_legacy::deserialize(&bytes).unwrap();
assert_eq!(decoded, announce);
let peer = PeerRecord {
fingerprint: [0x33u8; 32],
address: "abc".into(),
ring_signer_pubkey: [0x44u8; 32],
status: PeerStatus::Connected,
last_seen_ns: 0,
ring_name: None,
};
let pbytes = bincode_legacy::serialize(&peer).unwrap();
for i in 0..32 {
assert_eq!(pbytes[i], 0x33, "PeerRecord fingerprint byte {i} drift");
}
let signer_off = 43;
for i in 0..32 {
assert_eq!(
pbytes[signer_off + i],
0x44,
"PeerRecord ring_signer_pubkey byte {i} drift"
);
}
let pdecoded: PeerRecord = bincode_legacy::deserialize(&pbytes).unwrap();
assert_eq!(pdecoded, peer);
}
}