Skip to main content

kindly_guard_server/
signing.rs

1// Copyright 2025 Kindly Software Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Message signing and verification for secure MCP communication
15//! Implements HMAC-SHA256 for message integrity and Ed25519 for authenticity
16
17use anyhow::{Context, Result};
18use base64::{engine::general_purpose, Engine as _};
19use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
20use hmac::{Hmac, Mac};
21use serde::{Deserialize, Serialize};
22use sha2::Sha256;
23use std::time::{SystemTime, UNIX_EPOCH};
24
25type HmacSha256 = Hmac<Sha256>;
26
27/// Signing configuration
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SigningConfig {
30    /// Enable message signing
31    pub enabled: bool,
32
33    /// Algorithm to use (hmac-sha256 or ed25519)
34    pub algorithm: SigningAlgorithm,
35
36    /// HMAC secret key (base64 encoded)
37    pub hmac_secret: Option<String>,
38
39    /// Ed25519 private key (base64 encoded)
40    pub ed25519_private_key: Option<String>,
41
42    /// Require signatures on incoming messages
43    pub require_signatures: bool,
44
45    /// Allow unsigned messages during grace period
46    pub grace_period_seconds: u64,
47
48    /// Include timestamp in signatures
49    pub include_timestamp: bool,
50
51    /// Maximum clock skew allowed (seconds)
52    pub max_clock_skew_seconds: u64,
53}
54
55/// Signing algorithms
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
57#[serde(rename_all = "snake_case")]
58pub enum SigningAlgorithm {
59    HmacSha256,
60    Ed25519,
61}
62
63impl std::fmt::Display for SigningAlgorithm {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        match self {
66            Self::HmacSha256 => write!(f, "hmac-sha256"),
67            Self::Ed25519 => write!(f, "ed25519"),
68        }
69    }
70}
71
72impl Default for SigningConfig {
73    fn default() -> Self {
74        Self {
75            enabled: false,
76            algorithm: SigningAlgorithm::HmacSha256,
77            hmac_secret: None,
78            ed25519_private_key: None,
79            require_signatures: false,
80            grace_period_seconds: 86400, // 24 hours
81            include_timestamp: true,
82            max_clock_skew_seconds: 300, // 5 minutes
83        }
84    }
85}
86
87/// Message signature with metadata
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct MessageSignature {
90    /// Algorithm used
91    pub algorithm: SigningAlgorithm,
92
93    /// The signature value (base64)
94    pub signature: String,
95
96    /// Timestamp when signed
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub timestamp: Option<u64>,
99
100    /// Key ID (for key rotation)
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub key_id: Option<String>,
103}
104
105/// Signing key manager
106pub struct SigningManager {
107    config: SigningConfig,
108    hmac_key: Option<Vec<u8>>,
109    signing_key: Option<SigningKey>,
110    verifying_key: Option<VerifyingKey>,
111    start_time: SystemTime,
112}
113
114/// Signed message wrapper
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct SignedMessage {
117    /// The original message
118    pub message: serde_json::Value,
119
120    /// Signature information
121    pub signature: MessageSignature,
122}
123
124impl SigningManager {
125    /// Create a new signing manager
126    pub fn new(config: SigningConfig) -> Result<Self> {
127        let mut hmac_key = None;
128        let mut signing_key = None;
129        let mut verifying_key = None;
130
131        if config.enabled {
132            match config.algorithm {
133                SigningAlgorithm::HmacSha256 => {
134                    if let Some(secret) = &config.hmac_secret {
135                        let key = general_purpose::STANDARD
136                            .decode(secret)
137                            .context("Invalid HMAC secret base64")?;
138                        if key.len() < 32 {
139                            anyhow::bail!("HMAC secret must be at least 32 bytes");
140                        }
141                        hmac_key = Some(key);
142                    } else {
143                        anyhow::bail!("HMAC secret required when HMAC-SHA256 is enabled");
144                    }
145                },
146                SigningAlgorithm::Ed25519 => {
147                    if let Some(private_key) = &config.ed25519_private_key {
148                        let key_bytes = general_purpose::STANDARD
149                            .decode(private_key)
150                            .context("Invalid Ed25519 private key base64")?;
151
152                        if key_bytes.len() != 32 {
153                            anyhow::bail!("Ed25519 private key must be exactly 32 bytes");
154                        }
155
156                        let key_array: [u8; 32] = match key_bytes.try_into() {
157                            Ok(arr) => arr,
158                            Err(_) => anyhow::bail!("Failed to convert key bytes to array"),
159                        };
160                        let secret = SigningKey::from_bytes(&key_array);
161                        verifying_key = Some(secret.verifying_key());
162                        signing_key = Some(secret);
163                    } else {
164                        anyhow::bail!("Ed25519 private key required when Ed25519 is enabled");
165                    }
166                },
167            }
168        }
169
170        Ok(Self {
171            config,
172            hmac_key,
173            signing_key,
174            verifying_key,
175            start_time: SystemTime::now(),
176        })
177    }
178
179    /// Sign a message
180    pub fn sign_message(&self, message: &serde_json::Value) -> Result<SignedMessage> {
181        if !self.config.enabled {
182            anyhow::bail!("Message signing is not enabled");
183        }
184
185        let timestamp = if self.config.include_timestamp {
186            Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
187        } else {
188            None
189        };
190
191        // Create canonical message representation
192        let canonical = self.canonicalize_message(message, timestamp)?;
193
194        let signature_value = match self.config.algorithm {
195            SigningAlgorithm::HmacSha256 => {
196                let key = self
197                    .hmac_key
198                    .as_ref()
199                    .ok_or_else(|| anyhow::anyhow!("HMAC key not initialized"))?;
200
201                let mut mac = HmacSha256::new_from_slice(key)?;
202                mac.update(canonical.as_bytes());
203                let result = mac.finalize();
204
205                general_purpose::STANDARD.encode(result.into_bytes())
206            },
207            SigningAlgorithm::Ed25519 => {
208                let signing_key = self
209                    .signing_key
210                    .as_ref()
211                    .ok_or_else(|| anyhow::anyhow!("Ed25519 key not initialized"))?;
212
213                let signature = signing_key.sign(canonical.as_bytes());
214                general_purpose::STANDARD.encode(signature.to_bytes())
215            },
216        };
217
218        Ok(SignedMessage {
219            message: message.clone(),
220            signature: MessageSignature {
221                algorithm: self.config.algorithm.clone(),
222                signature: signature_value,
223                timestamp,
224                key_id: None, // TODO: Implement key rotation
225            },
226        })
227    }
228
229    /// Verify a signed message
230    pub fn verify_message(&self, signed: &SignedMessage) -> Result<()> {
231        if !self.config.enabled {
232            // If signing is disabled, accept all messages
233            return Ok(());
234        }
235
236        // Check if we're in grace period
237        if !self.config.require_signatures {
238            let elapsed = SystemTime::now().duration_since(self.start_time)?.as_secs();
239
240            if elapsed < self.config.grace_period_seconds {
241                // Still in grace period, accept unsigned
242                return Ok(());
243            }
244        }
245
246        // Verify timestamp if present
247        if let Some(timestamp) = signed.signature.timestamp {
248            let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
249
250            let time_diff = if timestamp > now {
251                timestamp - now
252            } else {
253                now - timestamp
254            };
255
256            if time_diff > self.config.max_clock_skew_seconds {
257                anyhow::bail!("Message timestamp outside acceptable range");
258            }
259        }
260
261        // Check algorithm matches
262        if signed.signature.algorithm != self.config.algorithm {
263            anyhow::bail!("Signature algorithm mismatch");
264        }
265
266        // Create canonical representation
267        let canonical = self.canonicalize_message(&signed.message, signed.signature.timestamp)?;
268
269        // Verify signature
270        match signed.signature.algorithm {
271            SigningAlgorithm::HmacSha256 => {
272                let key = self
273                    .hmac_key
274                    .as_ref()
275                    .ok_or_else(|| anyhow::anyhow!("HMAC key not initialized"))?;
276
277                let mut mac = HmacSha256::new_from_slice(key)?;
278                mac.update(canonical.as_bytes());
279
280                let expected = general_purpose::STANDARD.decode(&signed.signature.signature)?;
281
282                mac.verify_slice(&expected)
283                    .map_err(|_| anyhow::anyhow!("HMAC verification failed"))?;
284            },
285            SigningAlgorithm::Ed25519 => {
286                let verifying_key = self
287                    .verifying_key
288                    .as_ref()
289                    .ok_or_else(|| anyhow::anyhow!("Ed25519 verifying key not initialized"))?;
290
291                let signature_bytes =
292                    general_purpose::STANDARD.decode(&signed.signature.signature)?;
293
294                let signature = Signature::from_slice(&signature_bytes)
295                    .context("Invalid Ed25519 signature format")?;
296
297                verifying_key
298                    .verify(canonical.as_bytes(), &signature)
299                    .map_err(|_| anyhow::anyhow!("Ed25519 verification failed"))?;
300            },
301        }
302
303        Ok(())
304    }
305
306    /// Create canonical message representation for signing
307    fn canonicalize_message(
308        &self,
309        message: &serde_json::Value,
310        timestamp: Option<u64>,
311    ) -> Result<String> {
312        // Create a deterministic representation
313        let mut canonical = serde_json::to_string(message)?;
314
315        if let Some(ts) = timestamp {
316            canonical.push_str(&format!("|timestamp:{ts}"));
317        }
318
319        Ok(canonical)
320    }
321
322    /// Extract signature from authorization header
323    pub fn extract_signature(authorization: &str) -> Option<MessageSignature> {
324        // Format: "Signature algorithm=hmac-sha256,signature=base64,timestamp=123"
325        if !authorization.starts_with("Signature ") {
326            return None;
327        }
328
329        let parts = authorization.trim_start_matches("Signature ");
330        let mut algorithm = None;
331        let mut signature = None;
332        let mut timestamp = None;
333        let mut key_id = None;
334
335        for part in parts.split(',') {
336            let kv: Vec<&str> = part.trim().splitn(2, '=').collect();
337            if kv.len() != 2 {
338                continue;
339            }
340
341            match kv[0] {
342                "algorithm" => {
343                    algorithm = match kv[1] {
344                        "hmac-sha256" => Some(SigningAlgorithm::HmacSha256),
345                        "ed25519" => Some(SigningAlgorithm::Ed25519),
346                        _ => None,
347                    };
348                },
349                "signature" => signature = Some(kv[1].to_string()),
350                "timestamp" => timestamp = kv[1].parse().ok(),
351                "keyid" => key_id = Some(kv[1].to_string()),
352                _ => {},
353            }
354        }
355
356        match (algorithm, signature) {
357            (Some(alg), Some(sig)) => Some(MessageSignature {
358                algorithm: alg,
359                signature: sig,
360                timestamp,
361                key_id,
362            }),
363            _ => None,
364        }
365    }
366
367    /// Create authorization header from signature
368    pub fn create_auth_header(signature: &MessageSignature) -> String {
369        let mut parts = vec![
370            format!(
371                "algorithm={}",
372                match signature.algorithm {
373                    SigningAlgorithm::HmacSha256 => "hmac-sha256",
374                    SigningAlgorithm::Ed25519 => "ed25519",
375                }
376            ),
377            format!("signature={}", signature.signature),
378        ];
379
380        if let Some(ts) = signature.timestamp {
381            parts.push(format!("timestamp={ts}"));
382        }
383
384        if let Some(kid) = &signature.key_id {
385            parts.push(format!("keyid={kid}"));
386        }
387
388        format!("Signature {}", parts.join(","))
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn test_hmac_signing() {
398        let config = SigningConfig {
399            enabled: true,
400            algorithm: SigningAlgorithm::HmacSha256,
401            hmac_secret: Some(
402                general_purpose::STANDARD.encode(b"FAKE-TEST-KEY-DO-NOT-USE-IN-PROD-32b"),
403            ),
404            include_timestamp: false,
405            require_signatures: true,
406            grace_period_seconds: 0, // No grace period
407            ..Default::default()
408        };
409
410        let manager = SigningManager::new(config).unwrap();
411        let message = serde_json::json!({"method": "test", "params": {}});
412
413        let signed = manager.sign_message(&message).unwrap();
414        assert_eq!(signed.signature.algorithm, SigningAlgorithm::HmacSha256);
415
416        // Verify should succeed
417        manager.verify_message(&signed).unwrap();
418
419        // Tampered message should fail
420        let mut tampered = signed.clone();
421        tampered.message = serde_json::json!({"method": "tampered", "params": {}});
422        assert!(manager.verify_message(&tampered).is_err());
423    }
424
425    #[test]
426    fn test_ed25519_signing() {
427        use rand::{rngs::OsRng, RngCore};
428
429        // Generate a test key
430        let mut secret_key_bytes = [0u8; 32];
431        OsRng.fill_bytes(&mut secret_key_bytes);
432        let signing_key = SigningKey::from_bytes(&secret_key_bytes);
433        let private_key_base64 = general_purpose::STANDARD.encode(signing_key.to_bytes());
434
435        let config = SigningConfig {
436            enabled: true,
437            algorithm: SigningAlgorithm::Ed25519,
438            ed25519_private_key: Some(private_key_base64),
439            include_timestamp: true,
440            ..Default::default()
441        };
442
443        let manager = SigningManager::new(config).unwrap();
444        let message = serde_json::json!({"method": "test", "params": {}});
445
446        let signed = manager.sign_message(&message).unwrap();
447        assert_eq!(signed.signature.algorithm, SigningAlgorithm::Ed25519);
448        assert!(signed.signature.timestamp.is_some());
449
450        // Verify should succeed
451        manager.verify_message(&signed).unwrap();
452    }
453
454    #[test]
455    fn test_signature_extraction() {
456        let auth = "Signature algorithm=hmac-sha256,signature=abc123,timestamp=1234567890";
457        let sig = SigningManager::extract_signature(auth).unwrap();
458
459        assert_eq!(sig.algorithm, SigningAlgorithm::HmacSha256);
460        assert_eq!(sig.signature, "abc123");
461        assert_eq!(sig.timestamp, Some(1234567890));
462    }
463}