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