1use base64::engine::general_purpose::STANDARD as BASE64;
8use base64::Engine;
9use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
10use sha2::{Digest, Sha256};
11use std::path::Path;
12
13use super::ConstitutionError;
14
15#[derive(Debug)]
17pub struct ConstitutionKeypair {
18 signing_key: SigningKey,
19}
20
21#[derive(Debug, Clone)]
23pub struct SignatureResult {
24 pub checksum: String,
26 pub signature: String,
28 pub public_key: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum VerifyResult {
35 Valid,
37 ChecksumMismatch {
39 expected: String,
40 actual: String,
41 },
42 SignatureInvalid,
44 MissingFields(String),
46}
47
48impl ConstitutionKeypair {
49 pub fn generate() -> Self {
51 let mut rng = rand::thread_rng();
52 let signing_key = SigningKey::generate(&mut rng);
53 Self { signing_key }
54 }
55
56 pub fn from_base64(key_b64: &str) -> Result<Self, ConstitutionError> {
58 let bytes = BASE64.decode(key_b64).map_err(|e| {
59 ConstitutionError::SigningError(format!("Invalid base64 key: {}", e))
60 })?;
61 let key_bytes: [u8; 32] = bytes.try_into().map_err(|_| {
62 ConstitutionError::SigningError("Key must be exactly 32 bytes".into())
63 })?;
64 Ok(Self {
65 signing_key: SigningKey::from_bytes(&key_bytes),
66 })
67 }
68
69 pub fn private_key_base64(&self) -> String {
71 BASE64.encode(self.signing_key.to_bytes())
72 }
73
74 pub fn public_key_base64(&self) -> String {
76 BASE64.encode(self.signing_key.verifying_key().to_bytes())
77 }
78
79 pub fn sign_file(&self, path: &Path) -> Result<SignatureResult, ConstitutionError> {
87 let content = std::fs::read_to_string(path)?;
88 self.sign_content(&content)
89 }
90
91 pub fn sign_content(&self, content: &str) -> Result<SignatureResult, ConstitutionError> {
93 let signable = extract_signable_content(content);
94 let checksum = compute_sha256(&signable);
95 let checksum_bytes = hex::decode(&checksum).map_err(|e| {
96 ConstitutionError::SigningError(format!("Hex decode error: {}", e))
97 })?;
98 let signature = self.signing_key.sign(&checksum_bytes);
99
100 Ok(SignatureResult {
101 checksum,
102 signature: BASE64.encode(signature.to_bytes()),
103 public_key: self.public_key_base64(),
104 })
105 }
106}
107
108pub fn verify_constitution(
114 content: &str,
115 stored_checksum: &str,
116 stored_signature_b64: &str,
117 public_key_b64: &str,
118) -> VerifyResult {
119 if stored_checksum.is_empty() || stored_signature_b64.is_empty() || public_key_b64.is_empty() {
121 return VerifyResult::MissingFields(
122 "Constitution must have checksum, signature, and public key".into(),
123 );
124 }
125
126 let signable = extract_signable_content(content);
128 let actual_checksum = compute_sha256(&signable);
129
130 if actual_checksum != stored_checksum {
132 return VerifyResult::ChecksumMismatch {
133 expected: stored_checksum.to_string(),
134 actual: actual_checksum,
135 };
136 }
137
138 let sig_bytes = match BASE64.decode(stored_signature_b64) {
140 Ok(b) => b,
141 Err(_) => return VerifyResult::SignatureInvalid,
142 };
143
144 let signature = match Signature::from_slice(&sig_bytes) {
145 Ok(s) => s,
146 Err(_) => return VerifyResult::SignatureInvalid,
147 };
148
149 let pk_bytes = match BASE64.decode(public_key_b64) {
150 Ok(b) => b,
151 Err(_) => return VerifyResult::SignatureInvalid,
152 };
153
154 let pk_array: [u8; 32] = match pk_bytes.try_into() {
155 Ok(a) => a,
156 Err(_) => return VerifyResult::SignatureInvalid,
157 };
158
159 let verifying_key = match VerifyingKey::from_bytes(&pk_array) {
160 Ok(k) => k,
161 Err(_) => return VerifyResult::SignatureInvalid,
162 };
163
164 let checksum_bytes = match hex::decode(&actual_checksum) {
165 Ok(b) => b,
166 Err(_) => return VerifyResult::SignatureInvalid,
167 };
168
169 match verifying_key.verify(&checksum_bytes, &signature) {
170 Ok(()) => VerifyResult::Valid,
171 Err(_) => VerifyResult::SignatureInvalid,
172 }
173}
174
175pub fn compute_sha256(content: &str) -> String {
177 let mut hasher = Sha256::new();
178 hasher.update(content.as_bytes());
179 let result = hasher.finalize();
180 hex::encode(result)
181}
182
183fn extract_signable_content(content: &str) -> String {
189 let mut signable_lines = Vec::new();
190 let mut in_identity = false;
191
192 for line in content.lines() {
193 let trimmed = line.trim();
194
195 if trimmed.starts_with('[') && !trimmed.starts_with("[[") {
197 in_identity = trimmed == "[identity]";
198 }
199
200 if in_identity {
202 continue;
203 }
204
205 signable_lines.push(line);
206 }
207
208 signable_lines.join("\n")
209}
210
211mod hex {
213 pub fn encode(bytes: impl AsRef<[u8]>) -> String {
214 bytes
215 .as_ref()
216 .iter()
217 .map(|b| format!("{:02x}", b))
218 .collect()
219 }
220
221 pub fn decode(s: &str) -> Result<Vec<u8>, String> {
222 if !s.len().is_multiple_of(2) {
223 return Err("Hex string must have even length".into());
224 }
225 (0..s.len())
226 .step_by(2)
227 .map(|i| {
228 u8::from_str_radix(&s[i..i + 2], 16)
229 .map_err(|e| format!("Invalid hex at position {}: {}", i, e))
230 })
231 .collect()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 const SAMPLE_CONSTITUTION: &str = r#"
240[identity]
241version = "1.0.0"
242checksum = ""
243signed_by = ""
244signature = ""
245
246[boundaries]
247forbidden = ["rm -rf /", "DROP DATABASE"]
248requires_approval = ["git push", "deploy *"]
249auto_allowed = ["git status", "run tests"]
250
251[resource_limits]
252max_api_cost_per_run = 5.0
253max_api_cost_per_day = 50.0
254max_execution_time = 3600
255max_concurrent_workflows = 3
256max_file_write_size = "10MB"
257allowed_directories = ["~/Projects"]
258blocked_directories = ["/etc"]
259
260[model_permissions]
261thinking_model = { can_execute = false, can_read = true }
262coding_model = { can_execute = true, can_read = true, sandbox_only = true }
263task_model = { can_execute = true, can_read = true }
264"#;
265
266 #[test]
267 fn test_generate_keypair() {
268 let kp = ConstitutionKeypair::generate();
269 let pk = kp.public_key_base64();
270 let sk = kp.private_key_base64();
271 assert!(!pk.is_empty());
272 assert!(!sk.is_empty());
273 assert_ne!(pk, sk);
275 }
276
277 #[test]
278 fn test_keypair_roundtrip() {
279 let kp = ConstitutionKeypair::generate();
280 let sk_b64 = kp.private_key_base64();
281 let pk_b64 = kp.public_key_base64();
282
283 let kp2 = ConstitutionKeypair::from_base64(&sk_b64).unwrap();
284 assert_eq!(kp2.public_key_base64(), pk_b64);
285 }
286
287 #[test]
288 fn test_sign_and_verify() {
289 let kp = ConstitutionKeypair::generate();
290 let result = kp.sign_content(SAMPLE_CONSTITUTION).unwrap();
291
292 assert!(!result.checksum.is_empty());
293 assert!(!result.signature.is_empty());
294
295 let verify = verify_constitution(
296 SAMPLE_CONSTITUTION,
297 &result.checksum,
298 &result.signature,
299 &result.public_key,
300 );
301 assert_eq!(verify, VerifyResult::Valid);
302 }
303
304 #[test]
305 fn test_tamper_detection_content_modified() {
306 let kp = ConstitutionKeypair::generate();
307 let result = kp.sign_content(SAMPLE_CONSTITUTION).unwrap();
308
309 let tampered = SAMPLE_CONSTITUTION.replace("rm -rf /", "rm -rf /home");
311
312 let verify = verify_constitution(
313 &tampered,
314 &result.checksum,
315 &result.signature,
316 &result.public_key,
317 );
318 assert!(matches!(verify, VerifyResult::ChecksumMismatch { .. }));
319 }
320
321 #[test]
322 fn test_tamper_detection_signature_modified() {
323 let kp = ConstitutionKeypair::generate();
324 let result = kp.sign_content(SAMPLE_CONSTITUTION).unwrap();
325
326 let kp2 = ConstitutionKeypair::generate();
328 let result2 = kp2.sign_content(SAMPLE_CONSTITUTION).unwrap();
329
330 let verify = verify_constitution(
331 SAMPLE_CONSTITUTION,
332 &result.checksum,
333 &result2.signature, &result.public_key,
335 );
336 assert_eq!(verify, VerifyResult::SignatureInvalid);
337 }
338
339 #[test]
340 fn test_tamper_detection_wrong_public_key() {
341 let kp = ConstitutionKeypair::generate();
342 let result = kp.sign_content(SAMPLE_CONSTITUTION).unwrap();
343
344 let kp2 = ConstitutionKeypair::generate();
346
347 let verify = verify_constitution(
348 SAMPLE_CONSTITUTION,
349 &result.checksum,
350 &result.signature,
351 &kp2.public_key_base64(), );
353 assert_eq!(verify, VerifyResult::SignatureInvalid);
354 }
355
356 #[test]
357 fn test_missing_fields() {
358 let verify = verify_constitution(SAMPLE_CONSTITUTION, "", "sig", "pk");
359 assert!(matches!(verify, VerifyResult::MissingFields(_)));
360
361 let verify = verify_constitution(SAMPLE_CONSTITUTION, "checksum", "", "pk");
362 assert!(matches!(verify, VerifyResult::MissingFields(_)));
363 }
364
365 #[test]
366 fn test_sha256_deterministic() {
367 let hash1 = compute_sha256("hello world");
368 let hash2 = compute_sha256("hello world");
369 assert_eq!(hash1, hash2);
370 assert_eq!(hash1.len(), 64); }
372
373 #[test]
374 fn test_sha256_different_inputs() {
375 let hash1 = compute_sha256("hello");
376 let hash2 = compute_sha256("world");
377 assert_ne!(hash1, hash2);
378 }
379
380 #[test]
381 fn test_extract_signable_content_excludes_identity() {
382 let signable = extract_signable_content(SAMPLE_CONSTITUTION);
383 assert!(!signable.contains("[identity]"));
384 assert!(!signable.contains("checksum"));
385 assert!(!signable.contains("signed_by"));
386 assert!(signable.contains("[boundaries]"));
388 assert!(signable.contains("[resource_limits]"));
389 }
390
391 #[test]
392 fn test_identity_changes_dont_affect_checksum() {
393 let content1 = SAMPLE_CONSTITUTION;
395 let content2 = SAMPLE_CONSTITUTION.replace(
396 "checksum = \"\"",
397 "checksum = \"abc123\"",
398 );
399
400 let signable1 = extract_signable_content(content1);
401 let signable2 = extract_signable_content(&content2);
402
403 assert_eq!(
404 compute_sha256(&signable1),
405 compute_sha256(&signable2),
406 );
407 }
408
409 #[test]
410 fn test_hex_roundtrip() {
411 let original = vec![0u8, 1, 255, 128, 64];
412 let encoded = hex::encode(&original);
413 let decoded = hex::decode(&encoded).unwrap();
414 assert_eq!(original, decoded);
415 }
416}