Skip to main content

dcp/binary/
signed.rs

1//! Ed25519 signed structures for security.
2
3use crate::DCPError;
4
5/// Ed25519 signed tool definition
6#[repr(C)]
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct SignedToolDef {
9    /// Tool identifier
10    pub tool_id: u32,
11    /// Blake3 hash of schema
12    pub schema_hash: [u8; 32],
13    /// Required capabilities bitfield
14    pub capabilities: u64,
15    /// Ed25519 signature
16    pub signature: [u8; 64],
17    /// Signer's public key
18    pub public_key: [u8; 32],
19}
20
21/// Signed invocation with replay protection
22#[repr(C)]
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct SignedInvocation {
25    /// Tool identifier
26    pub tool_id: u32,
27    /// Unique nonce for replay protection
28    pub nonce: u64,
29    /// Timestamp for expiration
30    pub timestamp: u64,
31    /// Blake3 hash of arguments
32    pub args_hash: [u8; 32],
33    /// Ed25519 signature
34    pub signature: [u8; 64],
35}
36
37impl SignedToolDef {
38    /// Size of the struct in bytes
39    pub const SIZE: usize = 144; // 4 + 32 + 8 + 64 + 32 + padding
40
41    /// Size of the compact payload covered by the signature.
42    pub const SIGNED_BYTES_SIZE: usize = 44;
43
44    /// Parse from canonical bytes.
45    #[inline(always)]
46    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, DCPError> {
47        let bytes = bytes.as_ref();
48        if bytes.len() < Self::SIZE {
49            return Err(DCPError::InsufficientData);
50        }
51        if bytes.len() != Self::SIZE {
52            return Err(DCPError::ValidationFailed);
53        }
54        if bytes[36..40].iter().any(|&byte| byte != 0) {
55            return Err(DCPError::ValidationFailed);
56        }
57
58        let mut schema_hash = [0u8; 32];
59        schema_hash.copy_from_slice(&bytes[4..36]);
60        let mut signature = [0u8; 64];
61        signature.copy_from_slice(&bytes[48..112]);
62        let mut public_key = [0u8; 32];
63        public_key.copy_from_slice(&bytes[112..144]);
64
65        Ok(Self {
66            tool_id: u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
67            schema_hash,
68            capabilities: u64::from_le_bytes(bytes[40..48].try_into().unwrap()),
69            signature,
70            public_key,
71        })
72    }
73
74    /// Serialize to canonical bytes with reserved padding zeroed.
75    #[inline(always)]
76    pub fn as_bytes(&self) -> [u8; Self::SIZE] {
77        let mut bytes = [0u8; Self::SIZE];
78        bytes[0..4].copy_from_slice(&self.tool_id.to_le_bytes());
79        bytes[4..36].copy_from_slice(&self.schema_hash);
80        bytes[40..48].copy_from_slice(&self.capabilities.to_le_bytes());
81        bytes[48..112].copy_from_slice(&self.signature);
82        bytes[112..144].copy_from_slice(&self.public_key);
83        bytes
84    }
85
86    /// Get the compact bytes covered by the signature.
87    pub fn signed_bytes(&self) -> [u8; Self::SIGNED_BYTES_SIZE] {
88        let mut bytes = [0u8; Self::SIGNED_BYTES_SIZE];
89        bytes[0..4].copy_from_slice(&self.tool_id.to_le_bytes());
90        bytes[4..36].copy_from_slice(&self.schema_hash);
91        bytes[36..44].copy_from_slice(&self.capabilities.to_le_bytes());
92        bytes
93    }
94}
95
96impl SignedInvocation {
97    /// Size of the struct in bytes
98    pub const SIZE: usize = 120; // 4 + 8 + 8 + 32 + 64 + padding
99
100    /// Size of the compact payload covered by the signature.
101    pub const SIGNED_BYTES_SIZE: usize = 52;
102
103    /// Parse from canonical bytes.
104    #[inline(always)]
105    pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, DCPError> {
106        let bytes = bytes.as_ref();
107        if bytes.len() < Self::SIZE {
108            return Err(DCPError::InsufficientData);
109        }
110        if bytes.len() != Self::SIZE {
111            return Err(DCPError::ValidationFailed);
112        }
113        if bytes[4..8].iter().any(|&byte| byte != 0) {
114            return Err(DCPError::ValidationFailed);
115        }
116
117        let mut args_hash = [0u8; 32];
118        args_hash.copy_from_slice(&bytes[24..56]);
119        let mut signature = [0u8; 64];
120        signature.copy_from_slice(&bytes[56..120]);
121
122        Ok(Self {
123            tool_id: u32::from_le_bytes(bytes[0..4].try_into().unwrap()),
124            nonce: u64::from_le_bytes(bytes[8..16].try_into().unwrap()),
125            timestamp: u64::from_le_bytes(bytes[16..24].try_into().unwrap()),
126            args_hash,
127            signature,
128        })
129    }
130
131    /// Serialize to canonical bytes with reserved padding zeroed.
132    #[inline(always)]
133    pub fn as_bytes(&self) -> [u8; Self::SIZE] {
134        let mut bytes = [0u8; Self::SIZE];
135        bytes[0..4].copy_from_slice(&self.tool_id.to_le_bytes());
136        bytes[8..16].copy_from_slice(&self.nonce.to_le_bytes());
137        bytes[16..24].copy_from_slice(&self.timestamp.to_le_bytes());
138        bytes[24..56].copy_from_slice(&self.args_hash);
139        bytes[56..120].copy_from_slice(&self.signature);
140        bytes
141    }
142
143    /// Get the compact bytes covered by the signature.
144    pub fn signed_bytes(&self) -> [u8; Self::SIGNED_BYTES_SIZE] {
145        let mut bytes = [0u8; Self::SIGNED_BYTES_SIZE];
146        bytes[0..4].copy_from_slice(&self.tool_id.to_le_bytes());
147        bytes[4..12].copy_from_slice(&self.nonce.to_le_bytes());
148        bytes[12..20].copy_from_slice(&self.timestamp.to_le_bytes());
149        bytes[20..52].copy_from_slice(&self.args_hash);
150        bytes
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_signed_tool_def_size() {
160        assert_eq!(std::mem::size_of::<SignedToolDef>(), SignedToolDef::SIZE);
161    }
162
163    #[test]
164    fn test_signed_invocation_size() {
165        assert_eq!(
166            std::mem::size_of::<SignedInvocation>(),
167            SignedInvocation::SIZE
168        );
169    }
170
171    #[test]
172    fn test_signed_tool_def_round_trip() {
173        let def = SignedToolDef {
174            tool_id: 42,
175            schema_hash: [0xAB; 32],
176            capabilities: 0x1234567890ABCDEF,
177            signature: [0xCD; 64],
178            public_key: [0xEF; 32],
179        };
180        let bytes = def.as_bytes();
181        let parsed = SignedToolDef::from_bytes(bytes).unwrap();
182
183        assert_eq!(parsed.tool_id, 42);
184        assert_eq!(parsed.schema_hash, [0xAB; 32]);
185        assert_eq!(parsed.capabilities, 0x1234567890ABCDEF);
186        assert_eq!(parsed.signature, [0xCD; 64]);
187        assert_eq!(parsed.public_key, [0xEF; 32]);
188    }
189
190    #[test]
191    fn test_signed_invocation_round_trip() {
192        let inv = SignedInvocation {
193            tool_id: 123,
194            nonce: 0xDEADBEEF,
195            timestamp: 1234567890,
196            args_hash: [0x11; 32],
197            signature: [0x22; 64],
198        };
199        let bytes = inv.as_bytes();
200        let parsed = SignedInvocation::from_bytes(bytes).unwrap();
201
202        assert_eq!(parsed.tool_id, 123);
203        assert_eq!(parsed.nonce, 0xDEADBEEF);
204        assert_eq!(parsed.timestamp, 1234567890);
205        assert_eq!(parsed.args_hash, [0x11; 32]);
206        assert_eq!(parsed.signature, [0x22; 64]);
207    }
208
209    #[test]
210    fn test_signed_bytes_length() {
211        let def = SignedToolDef {
212            tool_id: 0,
213            schema_hash: [0; 32],
214            capabilities: 0,
215            signature: [0; 64],
216            public_key: [0; 32],
217        };
218        assert_eq!(def.signed_bytes().len(), 44);
219
220        let inv = SignedInvocation {
221            tool_id: 0,
222            nonce: 0,
223            timestamp: 0,
224            args_hash: [0; 32],
225            signature: [0; 64],
226        };
227        assert_eq!(inv.signed_bytes().len(), 52);
228    }
229}