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