Skip to main content

crypto/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Cryptographic signing for Heddle states and attestations, plus the
3//! versioned AEAD envelope used by confidential-runtime profiles (ADR 0051).
4
5mod 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
26#[cfg(test)]
27mod behavior_tests;
28
29use std::path::Path;
30
31pub use aead::{
32    AEAD_AES256_GCM_V1, AeadCiphertext, AeadError, Dek, PAD_BUCKETS, SoftwareRecipientSecret,
33    WRAP_HKDF_INFO, WrappedDek, decrypt_padded, encrypt_padded, pad_bucket_for, unwrap_dek,
34    wrap_dek,
35};
36pub use ci_verdict::{
37    Basis, BasisKind, CI_VERDICT_BODY_SCHEMA_VERSION, CI_VERDICT_DOMAIN, CheckClass,
38    CheckDescriptor, CiVerdictBody, Conclusion, Execution, FailureClass, FailureDetail, LogRef,
39    Outcome, Repro, SIGNED_VERDICT_FORMAT_VERSION, SignedVerdict, SignedVerdictError, SignerKind,
40    StateRef, ci_verdict_signing_payload, signed_verdict_from_signer,
41};
42pub use ed25519::Ed25519Signer;
43pub use error::SignerError;
44use heddle_object_model::object::ContentHash;
45pub use heddle_object_model::object::SignatureStatus;
46pub use p256::P256Signer;
47pub use pem_loader::{PemKind, classify_pem};
48pub use state_signature::{
49    StateSignatureError, public_key_bytes, signature_bytes, state_signature_from_signer,
50    verify_state_signature_bytes,
51};
52
53/// Trait for cryptographic signers.
54pub trait Signer: Send + Sync {
55    fn algorithm(&self) -> &'static str;
56    fn public_key(&self) -> &[u8];
57    fn sign(&self, data: &[u8]) -> Result<Vec<u8>, SignerError>;
58    fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), SignerError>;
59}
60
61impl Signer for Box<dyn Signer> {
62    fn algorithm(&self) -> &'static str {
63        (**self).algorithm()
64    }
65
66    fn public_key(&self) -> &[u8] {
67        (**self).public_key()
68    }
69
70    fn sign(&self, data: &[u8]) -> Result<Vec<u8>, SignerError> {
71        (**self).sign(data)
72    }
73
74    fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), SignerError> {
75        (**self).verify(data, signature)
76    }
77}
78
79const STATE_SIGNATURE_DOMAIN: &[u8; 16] = b"hd-state-sig-v1\x00";
80
81fn state_signature_payload(content_hash: &ContentHash) -> [u8; 48] {
82    let mut payload = [0; 48];
83    payload[..STATE_SIGNATURE_DOMAIN.len()].copy_from_slice(STATE_SIGNATURE_DOMAIN);
84    payload[STATE_SIGNATURE_DOMAIN.len()..].copy_from_slice(content_hash.as_bytes());
85    payload
86}
87
88/// Load a signer from a key file. When `algorithm` is `None`, the PEM
89/// header (or raw-seed shape) selects the backend via
90/// [`pem_loader::load_signer_from_pem`].
91pub fn load_signer(path: &Path, algorithm: Option<&str>) -> Result<Box<dyn Signer>, SignerError> {
92    reject_group_or_world_readable_key(path)?;
93    let key_data = std::fs::read(path)?;
94    let pem_content = String::from_utf8_lossy(&key_data);
95
96    if let Some(algo) = algorithm {
97        return match algo.to_lowercase().as_str() {
98            "ed25519" => {
99                Ed25519Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
100            }
101            "p256" | "ecdsa-p256" => {
102                P256Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
103            }
104            _ => Err(SignerError::UnsupportedAlgorithm(algo.to_string())),
105        };
106    }
107
108    pem_loader::load_signer_from_pem(&pem_content)
109}
110
111/// Reject a private-key file whose permissions expose it to group/world
112/// readers. The single source of the `0600`-or-stricter rule: the key-file
113/// signer loader ([`load_signer`]) and the auto-signing identity loader
114/// (`repo::identity`) both call this so the threshold lives in one place. On
115/// unix, errors with [`SignerError::InsecureKeyPermissions`] when any of the
116/// group/world bits (`0o077`) are set; a no-op on platforms without a unix
117/// permission model. Propagates I/O errors (e.g. `NotFound`) from the stat.
118#[cfg(unix)]
119pub fn reject_group_or_world_readable_key(path: &Path) -> Result<(), SignerError> {
120    use std::os::unix::fs::PermissionsExt;
121
122    let mode = std::fs::metadata(path)?.permissions().mode() & 0o777;
123    if mode & 0o077 != 0 {
124        return Err(SignerError::InsecureKeyPermissions {
125            path: path.to_path_buf(),
126            mode,
127        });
128    }
129    Ok(())
130}
131
132/// Non-unix stub: no permission model to enforce. See the unix variant.
133#[cfg(not(unix))]
134pub fn reject_group_or_world_readable_key(_path: &Path) -> Result<(), SignerError> {
135    Ok(())
136}
137
138/// Verify a state's signature.
139pub fn verify_state_signature(
140    content_hash: &ContentHash,
141    algorithm: &str,
142    public_key: &[u8],
143    signature: &[u8],
144) -> Result<(), SignerError> {
145    verify_payload_signature(
146        &state_signature_payload(content_hash),
147        algorithm,
148        public_key,
149        signature,
150    )
151}
152
153/// Verify a detached signature over an arbitrary payload. Used by
154/// non-state-signature flows (e.g. `ReviewSignature`) that already have a
155/// canonical byte payload built upstream.
156pub fn verify_payload_signature(
157    payload: &[u8],
158    algorithm: &str,
159    public_key: &[u8],
160    signature: &[u8],
161) -> Result<(), SignerError> {
162    match algorithm.to_lowercase().as_str() {
163        "ed25519" => Ed25519Signer::verify_with_public_key(payload, public_key, signature),
164        "p256" | "ecdsa-p256" => P256Signer::verify_with_public_key(payload, public_key, signature),
165        _ => Err(SignerError::UnsupportedAlgorithm(algorithm.to_string())),
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    #[cfg(unix)]
172    use std::os::unix::fs::PermissionsExt;
173
174    use heddle_fs_prims::fs_atomic::write_file_atomic_secret;
175    use tempfile::TempDir;
176
177    use super::*;
178
179    #[test]
180    fn ed25519_seed_round_trips_through_pem() {
181        let signer = Ed25519Signer::generate().expect("generate key");
182        let seed = signer.to_seed();
183        let from_seed = Ed25519Signer::from_seed(&seed).expect("reload seed");
184        let from_pem =
185            Ed25519Signer::from_pem(&signer.to_pem().expect("export pem")).expect("reload pem");
186        assert_eq!(from_seed.to_seed(), seed);
187        assert_eq!(from_pem.to_seed(), seed);
188        assert_eq!(from_seed.public_key(), signer.public_key());
189    }
190
191    #[test]
192    fn test_ed25519_sign_verify_roundtrip() {
193        let signer = Ed25519Signer::generate().expect("generate key");
194        let data = b"test data for signing";
195
196        let signature = signer.sign(data).expect("sign data");
197        signer.verify(data, &signature).expect("verify signature");
198    }
199
200    #[test]
201    fn test_ed25519_sign_verify_invalid_signature_fails_explicitly() {
202        let signer = Ed25519Signer::generate().expect("generate key");
203        let data = b"test data for signing";
204
205        let signature = signer.sign(data).expect("sign data");
206        let error = signer
207            .verify(b"wrong data", &signature)
208            .expect_err("verify should fail");
209
210        assert!(matches!(error, SignerError::VerificationFailed));
211    }
212
213    #[test]
214    fn test_load_signer_ed25519() {
215        let temp = TempDir::new().expect("create temp dir");
216        let key_path = temp.path().join("test_ed25519.pem");
217
218        let signer = Ed25519Signer::generate().expect("generate key");
219        let pem = signer.to_pem().expect("export to PEM");
220        write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
221
222        let loaded = load_signer(&key_path, Some("ed25519")).expect("load signer");
223        assert_eq!(loaded.algorithm(), "ed25519");
224        assert_eq!(loaded.public_key(), signer.public_key());
225    }
226
227    #[cfg(unix)]
228    #[test]
229    fn load_signer_refuses_group_or_world_readable_private_key() {
230        let temp = TempDir::new().expect("create temp dir");
231        let key_path = temp.path().join("test_ed25519.pem");
232
233        let signer = Ed25519Signer::generate().expect("generate key");
234        let pem = signer.to_pem().expect("export to PEM");
235        write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
236        std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o644))
237            .expect("make key insecure");
238
239        let err = match load_signer(&key_path, Some("ed25519")) {
240            Ok(_) => panic!("insecure key must fail"),
241            Err(err) => err,
242        };
243        assert!(matches!(
244            err,
245            SignerError::InsecureKeyPermissions { mode: 0o644, .. }
246        ));
247        // The refusal must be actionable: name the offending path, the
248        // observed + required modes, and the exact chmod to run.
249        let msg = err.to_string();
250        assert!(msg.contains(&key_path.display().to_string()), "{msg}");
251        assert!(msg.contains("0644"), "{msg}");
252        assert!(msg.contains("0600"), "{msg}");
253        assert!(msg.contains("chmod 600"), "{msg}");
254    }
255
256    #[cfg(unix)]
257    #[test]
258    fn load_signer_accepts_owner_only_private_key() {
259        let temp = TempDir::new().expect("create temp dir");
260        let key_path = temp.path().join("test_ed25519.pem");
261
262        let signer = Ed25519Signer::generate().expect("generate key");
263        let pem = signer.to_pem().expect("export to PEM");
264        write_file_atomic_secret(&key_path, pem.as_bytes()).expect("write key file");
265        std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))
266            .expect("set owner-only mode");
267
268        let loaded = load_signer(&key_path, Some("ed25519")).expect("0600 key must load");
269        assert_eq!(loaded.public_key(), signer.public_key());
270    }
271}