Skip to main content

syncweb_core/node/
signed_message.rs

1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
2use iroh::PublicKey;
3
4use crate::error::{Result, SyncwebError};
5
6/// Wire format header size: 32-byte `VerifyingKey` + 64-byte Signature.
7const HEADER_LEN: usize = 96;
8
9/// Ed25519-signed gossip envelope that prevents identity spoofing.
10///
11/// Wire format: `[verifying_key: 32 bytes][signature: 64 bytes][content: N bytes]`
12///
13/// Since iroh's [`PublicKey`] (`NodeId`) **is** an Ed25519 [`VerifyingKey`], a valid
14/// signature cryptographically proves the message originated from the claimed
15/// `NodeId`.
16#[derive(Copy, Clone)]
17#[non_exhaustive]
18pub struct SignedMessage;
19
20impl SignedMessage {
21    /// Sign `content` with the node's Ed25519 secret key.
22    ///
23    /// Returns the serialized wire format:
24    /// `[verifying_key: 32B][signature: 64B][content]`.
25    #[must_use]
26    pub fn sign(secret_key: &SigningKey, content: &[u8]) -> Vec<u8> {
27        let signature = secret_key.sign(content);
28        let mut wire = Vec::with_capacity(HEADER_LEN.saturating_add(content.len()));
29        wire.extend_from_slice(&secret_key.verifying_key().to_bytes());
30        wire.extend_from_slice(&signature.to_bytes());
31        wire.extend_from_slice(content);
32        wire
33    }
34
35    /// Verify a wire-format signed message and extract the original content.
36    ///
37    /// Returns `(verifying_key, content_bytes)` on success.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error if the message is shorter than 96 bytes, the signature
42    /// is invalid, or the key bytes do not represent a valid Ed25519 public key.
43    pub fn verify(wire_bytes: &[u8]) -> Result<(VerifyingKey, Vec<u8>)> {
44        if wire_bytes.len() < HEADER_LEN {
45            return Err(SyncwebError::operation(
46                "signed message too short",
47                format!("got {} bytes, need at least {}", wire_bytes.len(), HEADER_LEN),
48            ));
49        }
50
51        let (key_bytes, rest) = wire_bytes.split_at(32);
52        let (sig_bytes, content) = rest.split_at(64);
53
54        let verifying_key = VerifyingKey::from_bytes(
55            key_bytes
56                .try_into()
57                .map_err(|error| SyncwebError::operation("invalid verifying key bytes", error))?,
58        )
59        .map_err(|error| SyncwebError::operation("invalid Ed25519 verifying key", error))?;
60
61        let signature = Signature::from_bytes(
62            sig_bytes
63                .try_into()
64                .map_err(|error| SyncwebError::operation("invalid signature bytes", error))?,
65        );
66
67        verifying_key
68            .verify(content, &signature)
69            .map_err(|error| SyncwebError::operation("gossip message signature verification failed", error))?;
70
71        Ok((verifying_key, content.to_vec()))
72    }
73
74    /// Verify a wire-format message and check that the embedded `NodeId` matches
75    /// the signing key. This is a convenience wrapper around [`verify`].
76    ///
77    /// # Errors
78    ///
79    /// Returns an error if verification fails or the `NodeId` does not match.
80    pub fn verify_with_expected(wire_bytes: &[u8], expected_node_id: &PublicKey) -> Result<Vec<u8>> {
81        let (verifying_key, content) = Self::verify(wire_bytes)?;
82        let actual_node_id = PublicKey::from_bytes(&verifying_key.to_bytes())
83            .map_err(|error| SyncwebError::operation("failed to derive NodeId from verifying key", error))?;
84        if actual_node_id != *expected_node_id {
85            return Err(SyncwebError::operation(
86                "gossip message NodeId mismatch",
87                format!("expected {expected_node_id}, got {actual_node_id}"),
88            ));
89        }
90        Ok(content)
91    }
92
93    /// The length of the signing header in bytes.
94    #[must_use]
95    pub const fn header_len() -> usize {
96        HEADER_LEN
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn random_signing_key() -> SigningKey {
105        use std::sync::atomic::{AtomicU64, Ordering};
106        static COUNTER: AtomicU64 = AtomicU64::new(0);
107        let mut seed = [0_u8; 32];
108        let count = COUNTER.fetch_add(1, Ordering::Relaxed);
109        seed[..8].copy_from_slice(&count.to_le_bytes());
110        SigningKey::from_bytes(&seed)
111    }
112
113    #[test]
114    fn test_sign_and_verify_roundtrip() {
115        let secret = random_signing_key();
116        let content = b"hello gossip";
117
118        let wire = SignedMessage::sign(&secret, content);
119        let (key, decoded) = SignedMessage::verify(&wire).unwrap();
120
121        assert_eq!(decoded, content);
122        assert_eq!(key, secret.verifying_key());
123    }
124
125    #[test]
126    fn test_verify_rejects_tampered_content() {
127        let secret = random_signing_key();
128        let wire = SignedMessage::sign(&secret, b"hello");
129
130        let mut tampered = wire;
131        *tampered
132            .get_mut(HEADER_LEN)
133            .expect("tampered should be long enough to have HEADER_LEN") ^= 0xFF; // flip a bit in the content
134        assert!(SignedMessage::verify(&tampered).is_err());
135    }
136
137    #[test]
138    fn test_verify_rejects_tampered_signature() {
139        let secret = random_signing_key();
140        let wire = SignedMessage::sign(&secret, b"hello");
141
142        let mut tampered = wire;
143        *tampered
144            .get_mut(40)
145            .expect("tampered should be long enough to have byte at index 40") ^= 0xFF; // flip a bit in the signature
146        assert!(SignedMessage::verify(&tampered).is_err());
147    }
148
149    #[test]
150    fn test_verify_rejects_short_message() {
151        assert!(SignedMessage::verify(&[0_u8; 32]).is_err());
152        assert!(SignedMessage::verify(&[0_u8; 95]).is_err());
153    }
154
155    #[test]
156    fn test_verify_rejects_empty_message() {
157        assert!(SignedMessage::verify(&[]).is_err());
158    }
159
160    #[test]
161    fn test_node_id_derived_from_verifying_key() {
162        let secret = random_signing_key();
163        let wire = SignedMessage::sign(&secret, b"hello");
164
165        let (key, _) = SignedMessage::verify(&wire).unwrap();
166
167        // iroh PublicKey (NodeId) must match the Ed25519 verifying key
168        let node_id = PublicKey::from_bytes(&key.to_bytes()).unwrap();
169        let expected = PublicKey::from_bytes(&secret.verifying_key().to_bytes()).unwrap();
170        assert_eq!(node_id, expected);
171    }
172
173    #[test]
174    fn test_verify_with_expected_success() {
175        let secret = random_signing_key();
176        let node_id = PublicKey::from_bytes(&secret.verifying_key().to_bytes()).unwrap();
177        let wire = SignedMessage::sign(&secret, b"hello");
178
179        let content = SignedMessage::verify_with_expected(&wire, &node_id).unwrap();
180        assert_eq!(content, b"hello");
181    }
182
183    #[test]
184    fn test_verify_with_expected_rejects_wrong_key() {
185        let alice = random_signing_key();
186        let bob = random_signing_key();
187        let bob_node_id = PublicKey::from_bytes(&bob.verifying_key().to_bytes()).unwrap();
188
189        let wire = SignedMessage::sign(&alice, b"hello");
190        assert!(SignedMessage::verify_with_expected(&wire, &bob_node_id).is_err());
191    }
192
193    #[test]
194    fn test_header_len_constant() {
195        assert_eq!(SignedMessage::header_len(), 96);
196    }
197}