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