Skip to main content

suture_core/
signing.rs

1//! Ed25519 patch signing and verification.
2//!
3//! Each patch is signed by its author's private key. The canonical
4//! representation of a patch (operation type + touch set + target path
5//! + payload + parent IDs + author + message + timestamp) is serialized
6//!   to bytes and signed.
7
8use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
9use rand::rngs::OsRng;
10use suture_common::Hash;
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14pub enum SigningError {
15    #[error("signature verification failed: {0}")]
16    VerificationFailed(String),
17
18    #[error("invalid signature: {0}")]
19    InvalidSignature(#[from] ed25519_dalek::SignatureError),
20
21    #[error("key error: {0}")]
22    KeyError(String),
23
24    #[error("{0}")]
25    Custom(String),
26}
27
28#[derive(Clone)]
29pub struct SigningKeypair {
30    signing_key: SigningKey,
31    verifying_key: VerifyingKey,
32}
33
34impl SigningKeypair {
35    pub fn generate() -> Self {
36        let signing_key = SigningKey::generate(&mut OsRng);
37        let verifying_key = signing_key.verifying_key();
38        Self {
39            signing_key,
40            verifying_key,
41        }
42    }
43
44    pub fn public_key_bytes(&self) -> [u8; 32] {
45        self.verifying_key.to_bytes()
46    }
47
48    pub fn private_key_bytes(&self) -> Vec<u8> {
49        self.signing_key.to_bytes().to_vec()
50    }
51
52    pub fn sign(&self, canonical_bytes: &[u8]) -> Signature {
53        self.signing_key.sign(canonical_bytes)
54    }
55
56    pub fn verifying_key(&self) -> &VerifyingKey {
57        &self.verifying_key
58    }
59}
60
61#[allow(clippy::too_many_arguments)]
62pub fn canonical_patch_bytes(
63    operation_type: &str,
64    touch_set: &[String],
65    target_path: &Option<String>,
66    payload: &[u8],
67    parent_ids: &[Hash],
68    author: &str,
69    message: &str,
70    timestamp: u64,
71) -> Vec<u8> {
72    let mut buf = Vec::new();
73
74    buf.extend_from_slice(operation_type.as_bytes());
75    buf.push(0);
76
77    let mut sorted_touches: Vec<&String> = touch_set.iter().collect();
78    sorted_touches.sort();
79    for touch in &sorted_touches {
80        buf.extend_from_slice(touch.as_bytes());
81        buf.push(0);
82    }
83
84    match target_path {
85        Some(path) => {
86            buf.extend_from_slice(path.as_bytes());
87        }
88        None => {
89            buf.push(0xFF);
90        }
91    }
92    buf.push(0);
93
94    buf.extend_from_slice(&(payload.len() as u64).to_le_bytes());
95    buf.extend_from_slice(payload);
96
97    let mut sorted_parents: Vec<&Hash> = parent_ids.iter().collect();
98    sorted_parents.sort_by_key(|h| h.to_hex());
99    for parent in &sorted_parents {
100        buf.extend_from_slice(parent.to_hex().as_bytes());
101        buf.push(0);
102    }
103
104    buf.extend_from_slice(&(timestamp.to_le_bytes()));
105    buf.push(0);
106
107    buf.extend_from_slice(author.as_bytes());
108    buf.push(0);
109
110    buf.extend_from_slice(message.as_bytes());
111
112    buf
113}
114
115pub fn verify_signature(
116    verifying_key_bytes: &[u8; 32],
117    canonical_bytes: &[u8],
118    signature_bytes: &[u8; 64],
119) -> Result<(), SigningError> {
120    let verifying_key = VerifyingKey::from_bytes(verifying_key_bytes)
121        .map_err(|e| SigningError::VerificationFailed(e.to_string()))?;
122    let signature = Signature::from_bytes(signature_bytes);
123    verifying_key
124        .verify(canonical_bytes, &signature)
125        .map_err(|e| SigningError::VerificationFailed(e.to_string()))
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::patch::types::{OperationType, Patch, TouchSet};
132
133    #[test]
134    fn test_generate_keypair() {
135        let kp = SigningKeypair::generate();
136        assert_eq!(kp.public_key_bytes().len(), 32);
137        assert_eq!(kp.private_key_bytes().len(), 32);
138    }
139
140    #[test]
141    fn test_sign_and_verify() {
142        let kp = SigningKeypair::generate();
143        let data = b"hello, suture!";
144
145        let signature = kp.sign(data);
146        let result = verify_signature(&kp.public_key_bytes(), data, &signature.to_bytes());
147        assert!(result.is_ok());
148    }
149
150    #[test]
151    fn test_verify_wrong_data_fails() {
152        let kp = SigningKeypair::generate();
153        let data = b"hello, suture!";
154        let wrong_data = b"hello, world!";
155
156        let signature = kp.sign(data);
157        let result = verify_signature(&kp.public_key_bytes(), wrong_data, &signature.to_bytes());
158        assert!(result.is_err());
159    }
160
161    #[test]
162    fn test_verify_wrong_key_fails() {
163        let kp1 = SigningKeypair::generate();
164        let kp2 = SigningKeypair::generate();
165        let data = b"hello, suture!";
166
167        let signature = kp1.sign(data);
168        let result = verify_signature(&kp2.public_key_bytes(), data, &signature.to_bytes());
169        assert!(result.is_err());
170    }
171
172    #[test]
173    fn test_canonical_patch_bytes_deterministic() {
174        let bytes1 = canonical_patch_bytes(
175            "Modify",
176            &["a.txt".to_string(), "b.txt".to_string()],
177            &Some("a.txt".to_string()),
178            b"payload",
179            &[],
180            "alice",
181            "test message",
182            1000,
183        );
184        let bytes2 = canonical_patch_bytes(
185            "Modify",
186            &["b.txt".to_string(), "a.txt".to_string()],
187            &Some("a.txt".to_string()),
188            b"payload",
189            &[],
190            "alice",
191            "test message",
192            1000,
193        );
194        assert_eq!(bytes1, bytes2);
195    }
196
197    #[test]
198    fn test_roundtrip_patch_signing() {
199        let kp = SigningKeypair::generate();
200
201        let patch = Patch::new(
202            OperationType::Modify,
203            TouchSet::single("test.txt"),
204            Some("test.txt".to_string()),
205            b"hello".to_vec(),
206            vec![],
207            "alice".to_string(),
208            "test commit".to_string(),
209        );
210
211        let canonical = canonical_patch_bytes(
212            &patch.operation_type.to_string(),
213            &patch.touch_set.addresses(),
214            &patch.target_path,
215            &patch.payload,
216            &patch.parent_ids,
217            &patch.author,
218            &patch.message,
219            patch.timestamp,
220        );
221
222        let signature = kp.sign(&canonical);
223        let result = verify_signature(&kp.public_key_bytes(), &canonical, &signature.to_bytes());
224        assert!(result.is_ok());
225    }
226}