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