1mod ci_verdict;
5mod ed25519;
6mod error;
7mod p256;
8mod pem_loader;
9pub mod pop;
10mod state_signature;
11
12#[cfg(test)]
13mod behavior_tests;
14
15use std::path::Path;
16
17pub use ci_verdict::{
18 Basis, BasisKind, CI_VERDICT_BODY_SCHEMA_VERSION, CI_VERDICT_DOMAIN, CheckClass,
19 CheckDescriptor, CiVerdictBody, Conclusion, Execution, FailureClass, FailureDetail, LogRef,
20 Outcome, Repro, SIGNED_VERDICT_FORMAT_VERSION, SignedVerdict, SignedVerdictError, SignerKind,
21 StateRef, ci_verdict_signing_payload, signed_verdict_from_signer,
22};
23pub use ed25519::Ed25519Signer;
24pub use error::SignerError;
25use objects::object::ContentHash;
26pub use objects::object::SignatureStatus;
27pub use p256::P256Signer;
28pub use pem_loader::{PemKind, classify_pem};
29pub use state_signature::{
30 StateSignatureError, public_key_bytes, signature_bytes, state_signature_from_signer,
31 verify_state_signature_bytes,
32};
33
34pub trait Signer: Send + Sync {
36 fn algorithm(&self) -> &'static str;
37 fn public_key(&self) -> &[u8];
38 fn sign(&self, data: &[u8]) -> Result<Vec<u8>, SignerError>;
39 fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), SignerError>;
40}
41
42const STATE_SIGNATURE_DOMAIN: &[u8; 16] = b"hd-state-sig-v1\x00";
43
44fn state_signature_payload(content_hash: &ContentHash) -> [u8; 48] {
45 let mut payload = [0; 48];
46 payload[..STATE_SIGNATURE_DOMAIN.len()].copy_from_slice(STATE_SIGNATURE_DOMAIN);
47 payload[STATE_SIGNATURE_DOMAIN.len()..].copy_from_slice(content_hash.as_bytes());
48 payload
49}
50
51pub fn load_signer(path: &Path, algorithm: Option<&str>) -> Result<Box<dyn Signer>, SignerError> {
55 reject_group_or_world_readable_key(path)?;
56 let key_data = std::fs::read(path)?;
57 let pem_content = String::from_utf8_lossy(&key_data);
58
59 if let Some(algo) = algorithm {
60 return match algo.to_lowercase().as_str() {
61 "ed25519" => {
62 Ed25519Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
63 }
64 "p256" | "ecdsa-p256" => {
65 P256Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
66 }
67 _ => Err(SignerError::UnsupportedAlgorithm(algo.to_string())),
68 };
69 }
70
71 pem_loader::load_signer_from_pem(&pem_content)
72}
73
74#[cfg(unix)]
82pub fn reject_group_or_world_readable_key(path: &Path) -> Result<(), SignerError> {
83 use std::os::unix::fs::PermissionsExt;
84
85 let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
86 if mode & 0o077 != 0 {
87 return Err(SignerError::InsecureKeyPermissions {
88 path: path.to_path_buf(),
89 mode,
90 });
91 }
92 Ok(())
93}
94
95#[cfg(not(unix))]
97pub fn reject_group_or_world_readable_key(_path: &Path) -> Result<(), SignerError> {
98 Ok(())
99}
100
101pub fn verify_state_signature(
103 content_hash: &ContentHash,
104 algorithm: &str,
105 public_key: &[u8],
106 signature: &[u8],
107) -> Result<(), SignerError> {
108 verify_payload_signature(
109 &state_signature_payload(content_hash),
110 algorithm,
111 public_key,
112 signature,
113 )
114}
115
116pub fn verify_payload_signature(
120 payload: &[u8],
121 algorithm: &str,
122 public_key: &[u8],
123 signature: &[u8],
124) -> Result<(), SignerError> {
125 match algorithm.to_lowercase().as_str() {
126 "ed25519" => Ed25519Signer::verify_with_public_key(payload, public_key, signature),
127 "p256" | "ecdsa-p256" => P256Signer::verify_with_public_key(payload, public_key, signature),
128 _ => Err(SignerError::UnsupportedAlgorithm(algorithm.to_string())),
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 #[cfg(unix)]
135 use std::os::unix::fs::PermissionsExt;
136
137 use objects::fs_atomic::write_file_atomic_secret;
138 use tempfile::TempDir;
139
140 use super::*;
141
142 #[test]
143 fn ed25519_seed_round_trips_through_pem() {
144 let signer = Ed25519Signer::generate().expect("generate key");
145 let seed = signer.to_seed();
146 let from_seed = Ed25519Signer::from_seed(&seed).expect("reload seed");
147 let from_pem =
148 Ed25519Signer::from_pem(&signer.to_pem().expect("export pem")).expect("reload pem");
149 assert_eq!(from_seed.to_seed(), seed);
150 assert_eq!(from_pem.to_seed(), seed);
151 assert_eq!(from_seed.public_key(), signer.public_key());
152 }
153
154 #[test]
155 fn test_ed25519_sign_verify_roundtrip() {
156 let signer = Ed25519Signer::generate().expect("generate key");
157 let data = b"test data for signing";
158
159 let signature = signer.sign(data).expect("sign data");
160 signer.verify(data, &signature).expect("verify signature");
161 }
162
163 #[test]
164 fn test_ed25519_sign_verify_invalid_signature_fails_explicitly() {
165 let signer = Ed25519Signer::generate().expect("generate key");
166 let data = b"test data for signing";
167
168 let signature = signer.sign(data).expect("sign data");
169 let error = signer
170 .verify(b"wrong data", &signature)
171 .expect_err("verify should fail");
172
173 assert!(matches!(error, SignerError::VerificationFailed));
174 }
175
176 #[test]
177 fn test_load_signer_ed25519() {
178 let temp = TempDir::new().expect("create temp dir");
179 let key_path = temp.path().join("test_ed25519.pem");
180
181 let signer = Ed25519Signer::generate().expect("generate key");
182 let pem = signer.to_pem().expect("export to PEM");
183 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
184
185 let loaded = load_signer(&key_path, Some("ed25519")).expect("load signer");
186 assert_eq!(loaded.algorithm(), "ed25519");
187 assert_eq!(loaded.public_key(), signer.public_key());
188 }
189
190 #[cfg(unix)]
191 #[test]
192 fn load_signer_refuses_group_or_world_readable_private_key() {
193 let temp = TempDir::new().expect("create temp dir");
194 let key_path = temp.path().join("test_ed25519.pem");
195
196 let signer = Ed25519Signer::generate().expect("generate key");
197 let pem = signer.to_pem().expect("export to PEM");
198 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
199 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o644))
200 .expect("make key insecure");
201
202 let err = match load_signer(&key_path, Some("ed25519")) {
203 Ok(_) => panic!("insecure key must fail"),
204 Err(err) => err,
205 };
206 assert!(matches!(
207 err,
208 SignerError::InsecureKeyPermissions { mode: 0o644, .. }
209 ));
210 let msg = err.to_string();
213 assert!(msg.contains(&key_path.display().to_string()), "{msg}");
214 assert!(msg.contains("0644"), "{msg}");
215 assert!(msg.contains("0600"), "{msg}");
216 assert!(msg.contains("chmod 600"), "{msg}");
217 }
218
219 #[cfg(unix)]
220 #[test]
221 fn load_signer_accepts_owner_only_private_key() {
222 let temp = TempDir::new().expect("create temp dir");
223 let key_path = temp.path().join("test_ed25519.pem");
224
225 let signer = Ed25519Signer::generate().expect("generate key");
226 let pem = signer.to_pem().expect("export to PEM");
227 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
228 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
229 .expect("set owner-only mode");
230
231 let loaded = load_signer(&key_path, Some("ed25519")).expect("0600 key must load");
232 assert_eq!(loaded.public_key(), signer.public_key());
233 }
234}