Skip to main content

hive_btle/security/
signed_payload.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Signed payload utilities for transport-agnostic message authentication
17//!
18//! Provides helpers for encoding and verifying signed messages that work
19//! across transport layers (BLE, WiFi, IP-based networks).
20//!
21//! # Wire Format
22//!
23//! ```text
24//! [marker:1][payload:N][signature:64]
25//! ```
26//!
27//! - **marker**: Single byte identifying the message type
28//! - **payload**: Variable-length application data (type-specific)
29//! - **signature**: Ed25519 signature over (marker || payload)
30//!
31//! The signature covers both the marker and payload, binding the message
32//! type to the content to prevent cross-protocol attacks.
33//!
34//! # Example
35//!
36//! ```
37//! use hive_btle::security::{DeviceIdentity, SignedPayload};
38//!
39//! let identity = DeviceIdentity::generate();
40//!
41//! // Encode a signed message
42//! let marker = 0xAF;
43//! let payload = [0x01, 0x02, 0x03, 0x04];
44//! let wire = SignedPayload::encode(marker, &payload, &identity);
45//!
46//! // Decode and verify
47//! let pubkey = identity.public_key();
48//! assert!(SignedPayload::verify(&wire, &pubkey));
49//!
50//! // Extract components
51//! let decoded = SignedPayload::decode(&wire).unwrap();
52//! assert_eq!(decoded.marker, marker);
53//! assert_eq!(decoded.payload, &payload);
54//! ```
55
56#[cfg(not(feature = "std"))]
57use alloc::vec::Vec;
58
59use super::identity::{verify_signature, DeviceIdentity};
60
61/// Signature size in bytes (Ed25519)
62pub const SIGNATURE_SIZE: usize = 64;
63
64/// Minimum wire size: marker (1) + signature (64)
65pub const MIN_WIRE_SIZE: usize = 1 + SIGNATURE_SIZE;
66
67/// Decoded signed payload
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct DecodedPayload<'a> {
70    /// Message type marker
71    pub marker: u8,
72    /// Payload bytes (between marker and signature)
73    pub payload: &'a [u8],
74    /// Ed25519 signature
75    pub signature: &'a [u8; 64],
76}
77
78/// Signed payload encoding and verification utilities
79///
80/// Transport-agnostic helpers for creating and verifying signed messages.
81/// Used by hive-lite CannedMessage and HIVE protocol messages.
82pub struct SignedPayload;
83
84impl SignedPayload {
85    /// Encode a signed payload
86    ///
87    /// Creates wire format: `[marker:1][payload:N][signature:64]`
88    ///
89    /// The signature covers `marker || payload`, binding the message type
90    /// to the content.
91    ///
92    /// # Arguments
93    /// * `marker` - Message type identifier
94    /// * `payload` - Application data to sign
95    /// * `identity` - Signer's identity (holds private key)
96    ///
97    /// # Returns
98    /// Wire bytes ready for transmission
99    pub fn encode(marker: u8, payload: &[u8], identity: &DeviceIdentity) -> Vec<u8> {
100        // Build message to sign: marker || payload
101        let mut to_sign = Vec::with_capacity(1 + payload.len());
102        to_sign.push(marker);
103        to_sign.extend_from_slice(payload);
104
105        // Sign
106        let signature = identity.sign(&to_sign);
107
108        // Build wire: marker || payload || signature
109        let mut wire = Vec::with_capacity(1 + payload.len() + SIGNATURE_SIZE);
110        wire.push(marker);
111        wire.extend_from_slice(payload);
112        wire.extend_from_slice(&signature);
113
114        wire
115    }
116
117    /// Encode with pre-computed signature
118    ///
119    /// Use when the signature is computed externally (e.g., by secure enclave).
120    ///
121    /// # Arguments
122    /// * `marker` - Message type identifier
123    /// * `payload` - Application data
124    /// * `signature` - Pre-computed Ed25519 signature over (marker || payload)
125    pub fn encode_with_signature(marker: u8, payload: &[u8], signature: &[u8; 64]) -> Vec<u8> {
126        let mut wire = Vec::with_capacity(1 + payload.len() + SIGNATURE_SIZE);
127        wire.push(marker);
128        wire.extend_from_slice(payload);
129        wire.extend_from_slice(signature);
130        wire
131    }
132
133    /// Decode a signed payload without verification
134    ///
135    /// Extracts marker, payload, and signature from wire format.
136    /// Does NOT verify the signature - call `verify()` separately.
137    ///
138    /// # Arguments
139    /// * `wire` - Wire bytes in format `[marker:1][payload:N][signature:64]`
140    ///
141    /// # Returns
142    /// `Some(DecodedPayload)` if wire is at least 65 bytes, `None` otherwise
143    pub fn decode(wire: &[u8]) -> Option<DecodedPayload<'_>> {
144        if wire.len() < MIN_WIRE_SIZE {
145            return None;
146        }
147
148        let marker = wire[0];
149        let payload_end = wire.len() - SIGNATURE_SIZE;
150        let payload = &wire[1..payload_end];
151
152        // Safe because we checked length above
153        let signature: &[u8; 64] = wire[payload_end..].try_into().ok()?;
154
155        Some(DecodedPayload {
156            marker,
157            payload,
158            signature,
159        })
160    }
161
162    /// Verify a signed payload
163    ///
164    /// Checks that the signature is valid for the given public key.
165    ///
166    /// # Arguments
167    /// * `wire` - Wire bytes in format `[marker:1][payload:N][signature:64]`
168    /// * `public_key` - Signer's Ed25519 public key
169    ///
170    /// # Returns
171    /// `true` if signature is valid, `false` otherwise
172    pub fn verify(wire: &[u8], public_key: &[u8; 32]) -> bool {
173        let Some(decoded) = Self::decode(wire) else {
174            return false;
175        };
176
177        // Reconstruct signed message: marker || payload
178        let signed_len = wire.len() - SIGNATURE_SIZE;
179        let to_verify = &wire[..signed_len];
180
181        verify_signature(public_key, to_verify, decoded.signature)
182    }
183
184    /// Decode and verify in one step
185    ///
186    /// Convenience method that decodes and verifies, returning the decoded
187    /// payload only if verification succeeds.
188    ///
189    /// # Arguments
190    /// * `wire` - Wire bytes
191    /// * `public_key` - Expected signer's public key
192    ///
193    /// # Returns
194    /// `Some(DecodedPayload)` if valid, `None` if malformed or signature invalid
195    pub fn decode_verified<'a>(
196        wire: &'a [u8],
197        public_key: &[u8; 32],
198    ) -> Option<DecodedPayload<'a>> {
199        if !Self::verify(wire, public_key) {
200            return None;
201        }
202        Self::decode(wire)
203    }
204
205    /// Get the payload size from total wire size
206    ///
207    /// Useful for pre-allocating buffers.
208    #[inline]
209    pub const fn payload_size(wire_size: usize) -> usize {
210        wire_size.saturating_sub(MIN_WIRE_SIZE)
211    }
212
213    /// Get the wire size from payload size
214    ///
215    /// Useful for pre-allocating buffers.
216    #[inline]
217    pub const fn wire_size(payload_size: usize) -> usize {
218        1 + payload_size + SIGNATURE_SIZE
219    }
220
221    /// Extract the marker byte without full decode
222    ///
223    /// Quick check for message type routing.
224    #[inline]
225    pub fn peek_marker(wire: &[u8]) -> Option<u8> {
226        wire.first().copied()
227    }
228
229    /// Extract signature bytes without full verification
230    ///
231    /// Useful for caching or deferred verification.
232    pub fn extract_signature(wire: &[u8]) -> Option<&[u8; 64]> {
233        if wire.len() < MIN_WIRE_SIZE {
234            return None;
235        }
236        let sig_start = wire.len() - SIGNATURE_SIZE;
237        wire[sig_start..].try_into().ok()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_encode_decode_roundtrip() {
247        let identity = DeviceIdentity::generate();
248        let marker = 0xAF;
249        let payload = [0x01, 0x02, 0x03, 0x04, 0x05];
250
251        let wire = SignedPayload::encode(marker, &payload, &identity);
252
253        // Check wire size
254        assert_eq!(wire.len(), SignedPayload::wire_size(payload.len()));
255        assert_eq!(wire.len(), 1 + 5 + 64);
256
257        // Decode
258        let decoded = SignedPayload::decode(&wire).unwrap();
259        assert_eq!(decoded.marker, marker);
260        assert_eq!(decoded.payload, &payload);
261    }
262
263    #[test]
264    fn test_verify_valid_signature() {
265        let identity = DeviceIdentity::generate();
266        let marker = 0xAF;
267        let payload = b"Hello, mesh!";
268
269        let wire = SignedPayload::encode(marker, payload, &identity);
270        let pubkey = identity.public_key();
271
272        assert!(SignedPayload::verify(&wire, &pubkey));
273    }
274
275    #[test]
276    fn test_verify_wrong_pubkey() {
277        let identity1 = DeviceIdentity::generate();
278        let identity2 = DeviceIdentity::generate();
279
280        let wire = SignedPayload::encode(0xAF, b"test", &identity1);
281        let wrong_pubkey = identity2.public_key();
282
283        assert!(!SignedPayload::verify(&wire, &wrong_pubkey));
284    }
285
286    #[test]
287    fn test_verify_tampered_payload() {
288        let identity = DeviceIdentity::generate();
289        let mut wire = SignedPayload::encode(0xAF, b"original", &identity);
290        let pubkey = identity.public_key();
291
292        // Tamper with payload
293        wire[1] ^= 0xFF;
294
295        assert!(!SignedPayload::verify(&wire, &pubkey));
296    }
297
298    #[test]
299    fn test_verify_tampered_marker() {
300        let identity = DeviceIdentity::generate();
301        let mut wire = SignedPayload::encode(0xAF, b"test", &identity);
302        let pubkey = identity.public_key();
303
304        // Change marker
305        wire[0] = 0xBF;
306
307        assert!(!SignedPayload::verify(&wire, &pubkey));
308    }
309
310    #[test]
311    fn test_decode_verified() {
312        let identity = DeviceIdentity::generate();
313        let marker = 0xAF;
314        let payload = b"verified content";
315
316        let wire = SignedPayload::encode(marker, payload, &identity);
317        let pubkey = identity.public_key();
318
319        let decoded = SignedPayload::decode_verified(&wire, &pubkey).unwrap();
320        assert_eq!(decoded.marker, marker);
321        assert_eq!(decoded.payload, payload);
322    }
323
324    #[test]
325    fn test_decode_verified_fails_bad_sig() {
326        let identity1 = DeviceIdentity::generate();
327        let identity2 = DeviceIdentity::generate();
328
329        let wire = SignedPayload::encode(0xAF, b"test", &identity1);
330        let wrong_pubkey = identity2.public_key();
331
332        assert!(SignedPayload::decode_verified(&wire, &wrong_pubkey).is_none());
333    }
334
335    #[test]
336    fn test_empty_payload() {
337        let identity = DeviceIdentity::generate();
338        let marker = 0x00;
339        let payload: &[u8] = &[];
340
341        let wire = SignedPayload::encode(marker, payload, &identity);
342        assert_eq!(wire.len(), MIN_WIRE_SIZE);
343
344        let decoded = SignedPayload::decode(&wire).unwrap();
345        assert_eq!(decoded.marker, marker);
346        assert!(decoded.payload.is_empty());
347
348        assert!(SignedPayload::verify(&wire, &identity.public_key()));
349    }
350
351    #[test]
352    fn test_peek_marker() {
353        let identity = DeviceIdentity::generate();
354        let wire = SignedPayload::encode(0xAB, b"test", &identity);
355
356        assert_eq!(SignedPayload::peek_marker(&wire), Some(0xAB));
357        assert_eq!(SignedPayload::peek_marker(&[]), None);
358    }
359
360    #[test]
361    fn test_extract_signature() {
362        let identity = DeviceIdentity::generate();
363        let wire = SignedPayload::encode(0xAF, b"test", &identity);
364
365        let sig = SignedPayload::extract_signature(&wire).unwrap();
366        assert_eq!(sig.len(), 64);
367
368        // Too short
369        assert!(SignedPayload::extract_signature(&[0x01; 10]).is_none());
370    }
371
372    #[test]
373    fn test_encode_with_signature() {
374        let identity = DeviceIdentity::generate();
375        let marker = 0xAF;
376        let payload = b"external sig";
377
378        // Compute signature externally
379        let mut to_sign = Vec::new();
380        to_sign.push(marker);
381        to_sign.extend_from_slice(payload);
382        let signature = identity.sign(&to_sign);
383
384        // Encode with pre-computed signature
385        let wire = SignedPayload::encode_with_signature(marker, payload, &signature);
386
387        // Should verify
388        assert!(SignedPayload::verify(&wire, &identity.public_key()));
389    }
390
391    #[test]
392    fn test_wire_size_calculation() {
393        assert_eq!(SignedPayload::wire_size(0), 65);
394        assert_eq!(SignedPayload::wire_size(21), 86); // CannedMessage size
395        assert_eq!(SignedPayload::wire_size(100), 165);
396
397        assert_eq!(SignedPayload::payload_size(65), 0);
398        assert_eq!(SignedPayload::payload_size(86), 21);
399        assert_eq!(SignedPayload::payload_size(165), 100);
400    }
401
402    #[test]
403    fn test_canned_message_size() {
404        // CannedMessage: [0xAF][msg_code:1][src:4][tgt:4][timestamp:8][seq:4] = 22 bytes unsigned
405        // Signed: [0xAF][msg_code:1][src:4][tgt:4][timestamp:8][seq:4][signature:64] = 86 bytes
406        let payload_size = 1 + 4 + 4 + 8 + 4; // 21 bytes (without marker)
407        assert_eq!(SignedPayload::wire_size(payload_size), 86);
408    }
409}