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