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#[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
55/// Trait for cryptographic signers.
56pub 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
90/// Load a signer from a key file. When `algorithm` is `None`, the PEM
91/// header (or raw-seed shape) selects the backend via
92/// [`pem_loader::load_signer_from_pem`].
93pub 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/// Reject a private-key file whose permissions expose it to group/world
114/// readers. The single source of the `0600`-or-stricter rule: the key-file
115/// signer loader ([`load_signer`]) and the auto-signing identity loader
116/// (`repo::identity`) both call this so the threshold lives in one place. On
117/// unix, errors with [`SignerError::InsecureKeyPermissions`] when any of the
118/// group/world bits (`0o077`) are set; a no-op on platforms without a unix
119/// permission model. Propagates I/O errors (e.g. `NotFound`) from the stat.
120#[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/// Non-unix stub: no permission model to enforce. See the unix variant.
135#[cfg(not(unix))]
136pub fn reject_group_or_world_readable_key(_path: &Path) -> Result<(), SignerError> {
137    Ok(())
138}
139
140/// Verify a state's signature.
141pub 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
155/// Verify a detached signature over an arbitrary payload. Used by
156/// non-state-signature flows (e.g. `ReviewSignature`) that already have a
157/// canonical byte payload built upstream.
158pub 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        // The refusal must be actionable: name the offending path, the
250        // observed + required modes, and the exact chmod to run.
251        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}