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
35const STATE_SIGNATURE_DOMAIN: &[u8; 16] = b"hd-state-sig-v1\x00";
36
37fn state_signature_payload(content_hash: &ContentHash) -> [u8; 48] {
38 let mut payload = [0; 48];
39 payload[..STATE_SIGNATURE_DOMAIN.len()].copy_from_slice(STATE_SIGNATURE_DOMAIN);
40 payload[STATE_SIGNATURE_DOMAIN.len()..].copy_from_slice(content_hash.as_bytes());
41 payload
42}
43
44pub fn load_signer(path: &Path, algorithm: Option<&str>) -> Result<Box<dyn Signer>, SignerError> {
48 reject_group_or_world_readable_key(path)?;
49 let key_data = std::fs::read(path)?;
50 let pem_content = String::from_utf8_lossy(&key_data);
51
52 if let Some(algo) = algorithm {
53 return match algo.to_lowercase().as_str() {
54 "ed25519" => {
55 Ed25519Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
56 }
57 "p256" | "ecdsa-p256" => {
58 P256Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
59 }
60 _ => Err(SignerError::UnsupportedAlgorithm(algo.to_string())),
61 };
62 }
63
64 pem_loader::load_signer_from_pem(&pem_content)
65}
66
67#[cfg(unix)]
75pub fn reject_group_or_world_readable_key(path: &Path) -> Result<(), SignerError> {
76 use std::os::unix::fs::PermissionsExt;
77
78 let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
79 if mode & 0o077 != 0 {
80 return Err(SignerError::InsecureKeyPermissions {
81 path: path.to_path_buf(),
82 mode,
83 });
84 }
85 Ok(())
86}
87
88#[cfg(not(unix))]
90pub fn reject_group_or_world_readable_key(_path: &Path) -> Result<(), SignerError> {
91 Ok(())
92}
93
94pub fn verify_state_signature(
96 content_hash: &ContentHash,
97 algorithm: &str,
98 public_key: &[u8],
99 signature: &[u8],
100) -> Result<(), SignerError> {
101 verify_payload_signature(
102 &state_signature_payload(content_hash),
103 algorithm,
104 public_key,
105 signature,
106 )
107}
108
109pub fn verify_payload_signature(
113 payload: &[u8],
114 algorithm: &str,
115 public_key: &[u8],
116 signature: &[u8],
117) -> Result<(), SignerError> {
118 match algorithm.to_lowercase().as_str() {
119 "ed25519" => Ed25519Signer::verify_with_public_key(payload, public_key, signature),
120 "p256" | "ecdsa-p256" => P256Signer::verify_with_public_key(payload, public_key, signature),
121 _ => Err(SignerError::UnsupportedAlgorithm(algorithm.to_string())),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 #[cfg(unix)]
128 use std::os::unix::fs::PermissionsExt;
129
130 use objects::fs_atomic::write_file_atomic_secret;
131 use tempfile::TempDir;
132
133 use super::*;
134
135 #[test]
136 fn test_ed25519_sign_verify_roundtrip() {
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 signer.verify(data, &signature).expect("verify signature");
142 }
143
144 #[test]
145 fn test_ed25519_sign_verify_invalid_signature_fails_explicitly() {
146 let signer = Ed25519Signer::generate().expect("generate key");
147 let data = b"test data for signing";
148
149 let signature = signer.sign(data).expect("sign data");
150 let error = signer
151 .verify(b"wrong data", &signature)
152 .expect_err("verify should fail");
153
154 assert!(matches!(error, SignerError::VerificationFailed));
155 }
156
157 #[test]
158 fn test_load_signer_ed25519() {
159 let temp = TempDir::new().expect("create temp dir");
160 let key_path = temp.path().join("test_ed25519.pem");
161
162 let signer = Ed25519Signer::generate().expect("generate key");
163 let pem = signer.to_pem().expect("export to PEM");
164 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
165
166 let loaded = load_signer(&key_path, Some("ed25519")).expect("load signer");
167 assert_eq!(loaded.algorithm(), "ed25519");
168 assert_eq!(loaded.public_key(), signer.public_key());
169 }
170
171 #[cfg(unix)]
172 #[test]
173 fn load_signer_refuses_group_or_world_readable_private_key() {
174 let temp = TempDir::new().expect("create temp dir");
175 let key_path = temp.path().join("test_ed25519.pem");
176
177 let signer = Ed25519Signer::generate().expect("generate key");
178 let pem = signer.to_pem().expect("export to PEM");
179 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
180 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o644))
181 .expect("make key insecure");
182
183 let err = match load_signer(&key_path, Some("ed25519")) {
184 Ok(_) => panic!("insecure key must fail"),
185 Err(err) => err,
186 };
187 assert!(matches!(
188 err,
189 SignerError::InsecureKeyPermissions { mode: 0o644, .. }
190 ));
191 let msg = err.to_string();
194 assert!(msg.contains(&key_path.display().to_string()), "{msg}");
195 assert!(msg.contains("0644"), "{msg}");
196 assert!(msg.contains("0600"), "{msg}");
197 assert!(msg.contains("chmod 600"), "{msg}");
198 }
199
200 #[cfg(unix)]
201 #[test]
202 fn load_signer_accepts_owner_only_private_key() {
203 let temp = TempDir::new().expect("create temp dir");
204 let key_path = temp.path().join("test_ed25519.pem");
205
206 let signer = Ed25519Signer::generate().expect("generate key");
207 let pem = signer.to_pem().expect("export to PEM");
208 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
209 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
210 .expect("set owner-only mode");
211
212 let loaded = load_signer(&key_path, Some("ed25519")).expect("0600 key must load");
213 assert_eq!(loaded.public_key(), signer.public_key());
214 }
215}