Skip to main content

crypto/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Cryptographic signing for Heddle states.
3
4mod ed25519;
5mod error;
6mod p256;
7mod pem_loader;
8mod rsa;
9mod state_signature;
10mod state_signing;
11
12use std::path::Path;
13
14pub use ed25519::Ed25519Signer;
15pub use error::SignerError;
16use objects::object::ContentHash;
17pub use objects::object::SignatureStatus;
18pub use p256::P256Signer;
19pub use pem_loader::{PemKind, classify_pem};
20pub use rsa::RsaSigner;
21pub use state_signature::{
22    StateSignatureError, public_key_bytes, signature_bytes, state_signature_from_signer,
23    verify_state_signature_bytes,
24};
25pub use state_signing::StateSigningExt;
26
27/// Trait for cryptographic signers.
28pub trait Signer: Send + Sync {
29    fn algorithm(&self) -> &'static str;
30    fn public_key(&self) -> Vec<u8>;
31    fn sign(&self, data: &[u8]) -> Result<Vec<u8>, SignerError>;
32    fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), SignerError>;
33}
34
35/// Load a signer from a key file. When `algorithm` is `None`, the PEM
36/// header (or raw-seed shape) selects the backend via
37/// [`pem_loader::load_signer_from_pem`].
38pub fn load_signer(path: &Path, algorithm: Option<&str>) -> Result<Box<dyn Signer>, SignerError> {
39    let key_data = std::fs::read(path)?;
40    let pem_content = String::from_utf8_lossy(&key_data);
41
42    if let Some(algo) = algorithm {
43        return match algo.to_lowercase().as_str() {
44            "ed25519" => {
45                Ed25519Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
46            }
47            "rsa" => RsaSigner::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>),
48            "p256" | "ecdsa-p256" => {
49                P256Signer::from_pem(&pem_content).map(|s| Box::new(s) as Box<dyn Signer>)
50            }
51            _ => Err(SignerError::UnsupportedAlgorithm(algo.to_string())),
52        };
53    }
54
55    pem_loader::load_signer_from_pem(&pem_content)
56}
57
58/// Verify a state's signature.
59pub fn verify_state_signature(
60    content_hash: &ContentHash,
61    algorithm: &str,
62    public_key: &[u8],
63    signature: &[u8],
64) -> Result<(), SignerError> {
65    verify_payload_signature(content_hash.as_bytes(), algorithm, public_key, signature)
66}
67
68/// Verify a detached signature over an arbitrary payload. Used by
69/// non-state-signature flows (e.g. `ReviewSignature`) that already have a
70/// canonical byte payload built upstream.
71pub fn verify_payload_signature(
72    payload: &[u8],
73    algorithm: &str,
74    public_key: &[u8],
75    signature: &[u8],
76) -> Result<(), SignerError> {
77    match algorithm.to_lowercase().as_str() {
78        "ed25519" => Ed25519Signer::verify_with_public_key(payload, public_key, signature),
79        "rsa" => RsaSigner::verify_with_public_key(payload, public_key, signature),
80        "p256" | "ecdsa-p256" => P256Signer::verify_with_public_key(payload, public_key, signature),
81        _ => Err(SignerError::UnsupportedAlgorithm(algorithm.to_string())),
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use tempfile::TempDir;
88
89    use super::*;
90
91    #[test]
92    fn test_ed25519_sign_verify_roundtrip() {
93        let signer = Ed25519Signer::generate().expect("generate key");
94        let data = b"test data for signing";
95
96        let signature = signer.sign(data).expect("sign data");
97        signer.verify(data, &signature).expect("verify signature");
98    }
99
100    #[test]
101    fn test_ed25519_sign_verify_invalid_signature_fails_explicitly() {
102        let signer = Ed25519Signer::generate().expect("generate key");
103        let data = b"test data for signing";
104
105        let signature = signer.sign(data).expect("sign data");
106        let error = signer
107            .verify(b"wrong data", &signature)
108            .expect_err("verify should fail");
109
110        assert!(matches!(error, SignerError::VerificationFailed));
111    }
112
113    #[test]
114    fn test_load_signer_ed25519() {
115        let temp = TempDir::new().expect("create temp dir");
116        let key_path = temp.path().join("test_ed25519.pem");
117
118        let signer = Ed25519Signer::generate().expect("generate key");
119        let pem = signer.to_pem().expect("export to PEM");
120        std::fs::write(&key_path, &pem).expect("write key file");
121
122        let loaded = load_signer(&key_path, Some("ed25519")).expect("load signer");
123        assert_eq!(loaded.algorithm(), "ed25519");
124        assert_eq!(loaded.public_key(), signer.public_key());
125    }
126}