1mod ed25519;
5mod error;
6mod p256;
7mod pem_loader;
8mod state_signature;
9mod state_signing;
10
11#[cfg(test)]
12mod behavior_tests;
13
14use std::path::Path;
15
16pub use ed25519::Ed25519Signer;
17pub use error::SignerError;
18use objects::object::ContentHash;
19pub use objects::object::SignatureStatus;
20pub use p256::P256Signer;
21pub use pem_loader::{PemKind, classify_pem};
22pub use state_signature::{
23 StateSignatureError, public_key_bytes, signature_bytes, state_signature_from_signer,
24 verify_state_signature_bytes,
25};
26pub use state_signing::StateSigningExt;
27
28pub trait Signer: Send + Sync {
30 fn algorithm(&self) -> &'static str;
31 fn public_key(&self) -> &[u8];
32 fn sign(&self, data: &[u8]) -> Result<Vec<u8>, SignerError>;
33 fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), SignerError>;
34}
35
36pub fn load_signer(path: &Path, algorithm: Option<&str>) -> Result<Box<dyn Signer>, SignerError> {
40 reject_group_or_world_readable_key(path)?;
41 let key_data = std::fs::read(path)?;
42 let pem_content = String::from_utf8_lossy(&key_data);
43
44 if let Some(algo) = algorithm {
45 return match algo.to_lowercase().as_str() {
46 "ed25519" => {
47 Ed25519Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
48 }
49 "p256" | "ecdsa-p256" => {
50 P256Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
51 }
52 _ => Err(SignerError::UnsupportedAlgorithm(algo.to_string())),
53 };
54 }
55
56 pem_loader::load_signer_from_pem(&pem_content)
57}
58
59#[cfg(unix)]
67pub fn reject_group_or_world_readable_key(path: &Path) -> Result<(), SignerError> {
68 use std::os::unix::fs::PermissionsExt;
69
70 let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
71 if mode & 0o077 != 0 {
72 return Err(SignerError::InsecureKeyPermissions {
73 path: path.to_path_buf(),
74 mode,
75 });
76 }
77 Ok(())
78}
79
80#[cfg(not(unix))]
82pub fn reject_group_or_world_readable_key(_path: &Path) -> Result<(), SignerError> {
83 Ok(())
84}
85
86pub fn verify_state_signature(
88 content_hash: &ContentHash,
89 algorithm: &str,
90 public_key: &[u8],
91 signature: &[u8],
92) -> Result<(), SignerError> {
93 verify_payload_signature(content_hash.as_bytes(), algorithm, public_key, signature)
94}
95
96pub fn verify_payload_signature(
100 payload: &[u8],
101 algorithm: &str,
102 public_key: &[u8],
103 signature: &[u8],
104) -> Result<(), SignerError> {
105 match algorithm.to_lowercase().as_str() {
106 "ed25519" => Ed25519Signer::verify_with_public_key(payload, public_key, signature),
107 "p256" | "ecdsa-p256" => P256Signer::verify_with_public_key(payload, public_key, signature),
108 _ => Err(SignerError::UnsupportedAlgorithm(algorithm.to_string())),
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 #[cfg(unix)]
115 use std::os::unix::fs::PermissionsExt;
116
117 use objects::fs_atomic::write_file_atomic_secret;
118 use tempfile::TempDir;
119
120 use super::*;
121
122 #[test]
123 fn test_ed25519_sign_verify_roundtrip() {
124 let signer = Ed25519Signer::generate().expect("generate key");
125 let data = b"test data for signing";
126
127 let signature = signer.sign(data).expect("sign data");
128 signer.verify(data, &signature).expect("verify signature");
129 }
130
131 #[test]
132 fn test_ed25519_sign_verify_invalid_signature_fails_explicitly() {
133 let signer = Ed25519Signer::generate().expect("generate key");
134 let data = b"test data for signing";
135
136 let signature = signer.sign(data).expect("sign data");
137 let error = signer
138 .verify(b"wrong data", &signature)
139 .expect_err("verify should fail");
140
141 assert!(matches!(error, SignerError::VerificationFailed));
142 }
143
144 #[test]
145 fn test_load_signer_ed25519() {
146 let temp = TempDir::new().expect("create temp dir");
147 let key_path = temp.path().join("test_ed25519.pem");
148
149 let signer = Ed25519Signer::generate().expect("generate key");
150 let pem = signer.to_pem().expect("export to PEM");
151 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
152
153 let loaded = load_signer(&key_path, Some("ed25519")).expect("load signer");
154 assert_eq!(loaded.algorithm(), "ed25519");
155 assert_eq!(loaded.public_key(), signer.public_key());
156 }
157
158 #[cfg(unix)]
159 #[test]
160 fn load_signer_refuses_group_or_world_readable_private_key() {
161 let temp = TempDir::new().expect("create temp dir");
162 let key_path = temp.path().join("test_ed25519.pem");
163
164 let signer = Ed25519Signer::generate().expect("generate key");
165 let pem = signer.to_pem().expect("export to PEM");
166 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
167 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o644))
168 .expect("make key insecure");
169
170 let err = match load_signer(&key_path, Some("ed25519")) {
171 Ok(_) => panic!("insecure key must fail"),
172 Err(err) => err,
173 };
174 assert!(matches!(
175 err,
176 SignerError::InsecureKeyPermissions { mode: 0o644, .. }
177 ));
178 let msg = err.to_string();
181 assert!(msg.contains(&key_path.display().to_string()), "{msg}");
182 assert!(msg.contains("0644"), "{msg}");
183 assert!(msg.contains("0600"), "{msg}");
184 assert!(msg.contains("chmod 600"), "{msg}");
185 }
186
187 #[cfg(unix)]
188 #[test]
189 fn load_signer_accepts_owner_only_private_key() {
190 let temp = TempDir::new().expect("create temp dir");
191 let key_path = temp.path().join("test_ed25519.pem");
192
193 let signer = Ed25519Signer::generate().expect("generate key");
194 let pem = signer.to_pem().expect("export to PEM");
195 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
196 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
197 .expect("set owner-only mode");
198
199 let loaded = load_signer(&key_path, Some("ed25519")).expect("0600 key must load");
200 assert_eq!(loaded.public_key(), signer.public_key());
201 }
202}