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