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 test_ed25519_sign_verify_roundtrip() {
144 let signer = Ed25519Signer::generate().expect("generate key");
145 let data = b"test data for signing";
146
147 let signature = signer.sign(data).expect("sign data");
148 signer.verify(data, &signature).expect("verify signature");
149 }
150
151 #[test]
152 fn test_ed25519_sign_verify_invalid_signature_fails_explicitly() {
153 let signer = Ed25519Signer::generate().expect("generate key");
154 let data = b"test data for signing";
155
156 let signature = signer.sign(data).expect("sign data");
157 let error = signer
158 .verify(b"wrong data", &signature)
159 .expect_err("verify should fail");
160
161 assert!(matches!(error, SignerError::VerificationFailed));
162 }
163
164 #[test]
165 fn test_load_signer_ed25519() {
166 let temp = TempDir::new().expect("create temp dir");
167 let key_path = temp.path().join("test_ed25519.pem");
168
169 let signer = Ed25519Signer::generate().expect("generate key");
170 let pem = signer.to_pem().expect("export to PEM");
171 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
172
173 let loaded = load_signer(&key_path, Some("ed25519")).expect("load signer");
174 assert_eq!(loaded.algorithm(), "ed25519");
175 assert_eq!(loaded.public_key(), signer.public_key());
176 }
177
178 #[cfg(unix)]
179 #[test]
180 fn load_signer_refuses_group_or_world_readable_private_key() {
181 let temp = TempDir::new().expect("create temp dir");
182 let key_path = temp.path().join("test_ed25519.pem");
183
184 let signer = Ed25519Signer::generate().expect("generate key");
185 let pem = signer.to_pem().expect("export to PEM");
186 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
187 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o644))
188 .expect("make key insecure");
189
190 let err = match load_signer(&key_path, Some("ed25519")) {
191 Ok(_) => panic!("insecure key must fail"),
192 Err(err) => err,
193 };
194 assert!(matches!(
195 err,
196 SignerError::InsecureKeyPermissions { mode: 0o644, .. }
197 ));
198 let msg = err.to_string();
201 assert!(msg.contains(&key_path.display().to_string()), "{msg}");
202 assert!(msg.contains("0644"), "{msg}");
203 assert!(msg.contains("0600"), "{msg}");
204 assert!(msg.contains("chmod 600"), "{msg}");
205 }
206
207 #[cfg(unix)]
208 #[test]
209 fn load_signer_accepts_owner_only_private_key() {
210 let temp = TempDir::new().expect("create temp dir");
211 let key_path = temp.path().join("test_ed25519.pem");
212
213 let signer = Ed25519Signer::generate().expect("generate key");
214 let pem = signer.to_pem().expect("export to PEM");
215 write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
216 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
217 .expect("set owner-only mode");
218
219 let loaded = load_signer(&key_path, Some("ed25519")).expect("0600 key must load");
220 assert_eq!(loaded.public_key(), signer.public_key());
221 }
222}