Skip to main content

oris_evolution_network/
signing.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
5
6use crate::EvolutionEnvelope;
7
8#[derive(Debug)]
9pub enum SigningError {
10    HomeDirectoryUnavailable,
11    Io(std::io::Error),
12    InvalidKeyMaterial(&'static str),
13    InvalidHex(hex::FromHexError),
14    InvalidSignature,
15    MissingSignature,
16    ContentHashMismatch,
17}
18
19impl std::fmt::Display for SigningError {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            SigningError::HomeDirectoryUnavailable => write!(f, "home directory unavailable"),
23            SigningError::Io(error) => write!(f, "io error: {error}"),
24            SigningError::InvalidKeyMaterial(message) => {
25                write!(f, "invalid key material: {message}")
26            }
27            SigningError::InvalidHex(error) => write!(f, "invalid hex: {error}"),
28            SigningError::InvalidSignature => write!(f, "invalid signature"),
29            SigningError::MissingSignature => write!(f, "missing signature"),
30            SigningError::ContentHashMismatch => write!(f, "content hash mismatch"),
31        }
32    }
33}
34
35impl std::error::Error for SigningError {}
36
37impl From<std::io::Error> for SigningError {
38    fn from(value: std::io::Error) -> Self {
39        Self::Io(value)
40    }
41}
42
43impl From<hex::FromHexError> for SigningError {
44    fn from(value: hex::FromHexError) -> Self {
45        Self::InvalidHex(value)
46    }
47}
48
49pub type SigningResult<T> = Result<T, SigningError>;
50pub type SignedEnvelope = EvolutionEnvelope;
51
52pub struct NodeKeypair {
53    signing_key: SigningKey,
54    path: PathBuf,
55}
56
57impl NodeKeypair {
58    pub fn generate() -> SigningResult<Self> {
59        let home = std::env::var_os("HOME").ok_or(SigningError::HomeDirectoryUnavailable)?;
60        let path = PathBuf::from(home).join(".oris").join("node.key");
61        Self::generate_at(path)
62    }
63
64    pub fn generate_at(path: impl AsRef<Path>) -> SigningResult<Self> {
65        let path = path.as_ref().to_path_buf();
66        if let Some(parent) = path.parent() {
67            fs::create_dir_all(parent)?;
68        }
69
70        let mut secret = [0u8; 32];
71        getrandom::getrandom(&mut secret)
72            .map_err(|_| SigningError::InvalidKeyMaterial("failed to generate randomness"))?;
73
74        let signing_key = SigningKey::from_bytes(&secret);
75        fs::write(&path, hex::encode(secret))?;
76        Ok(Self { signing_key, path })
77    }
78
79    pub fn from_path(path: impl AsRef<Path>) -> SigningResult<Self> {
80        let path = path.as_ref().to_path_buf();
81        let contents = fs::read_to_string(&path)?;
82        let secret = hex::decode(contents.trim())?;
83        let secret: [u8; 32] = secret
84            .try_into()
85            .map_err(|_| SigningError::InvalidKeyMaterial("expected 32-byte secret key"))?;
86        Ok(Self {
87            signing_key: SigningKey::from_bytes(&secret),
88            path,
89        })
90    }
91
92    pub fn public_key_hex(&self) -> String {
93        hex::encode(self.signing_key.verifying_key().to_bytes())
94    }
95
96    pub fn path(&self) -> &Path {
97        &self.path
98    }
99}
100
101pub fn sign_envelope(keypair: &NodeKeypair, envelope: &EvolutionEnvelope) -> SignedEnvelope {
102    let mut signed = envelope.clone();
103    signed.signature = None;
104    signed.content_hash = signed.compute_content_hash();
105    let signature = keypair.signing_key.sign(signed.content_hash.as_bytes());
106    signed.signature = Some(hex::encode(signature.to_bytes()));
107    signed
108}
109
110pub fn verify_envelope(
111    public_key_hex: &str,
112    signed_envelope: &SignedEnvelope,
113) -> SigningResult<()> {
114    if signed_envelope.compute_content_hash() != signed_envelope.content_hash {
115        return Err(SigningError::ContentHashMismatch);
116    }
117
118    let signature_hex = signed_envelope
119        .signature
120        .as_ref()
121        .ok_or(SigningError::MissingSignature)?;
122    let signature_bytes = hex::decode(signature_hex)?;
123    let signature_bytes: [u8; 64] = signature_bytes
124        .try_into()
125        .map_err(|_| SigningError::InvalidSignature)?;
126    let signature = Signature::from_bytes(&signature_bytes);
127
128    let public_key_bytes = hex::decode(public_key_hex)?;
129    let public_key_bytes: [u8; 32] = public_key_bytes
130        .try_into()
131        .map_err(|_| SigningError::InvalidKeyMaterial("expected 32-byte public key"))?;
132    let public_key = VerifyingKey::from_bytes(&public_key_bytes)
133        .map_err(|_| SigningError::InvalidKeyMaterial("invalid public key"))?;
134
135    public_key
136        .verify(signed_envelope.content_hash.as_bytes(), &signature)
137        .map_err(|_| SigningError::InvalidSignature)
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::{EvolutionEnvelope, NetworkAsset};
144    use oris_evolution::{AssetState, Gene};
145
146    fn sample_gene(id: &str) -> Gene {
147        Gene {
148            id: id.to_string(),
149            signals: vec!["sig.test".to_string()],
150            strategy: vec!["check signature".to_string()],
151            validation: vec!["cargo test".to_string()],
152            state: AssetState::Candidate,
153            task_class_id: None,
154        }
155    }
156
157    #[test]
158    fn node_keypair_generate_persists_secret() {
159        let temp_path = std::env::temp_dir().join(format!(
160            "oris-node-key-{}.key",
161            std::time::SystemTime::now()
162                .duration_since(std::time::UNIX_EPOCH)
163                .unwrap()
164                .as_nanos()
165        ));
166        let keypair =
167            NodeKeypair::generate_at(&temp_path).expect("keypair generation should succeed");
168        assert!(temp_path.exists());
169        let loaded = NodeKeypair::from_path(&temp_path).expect("keypair should reload from disk");
170        assert_eq!(keypair.public_key_hex(), loaded.public_key_hex());
171        let _ = std::fs::remove_file(temp_path);
172    }
173
174    #[test]
175    fn sign_and_verify_round_trip_succeeds() {
176        let temp_path = std::env::temp_dir().join(format!(
177            "oris-node-key-{}.key",
178            std::time::SystemTime::now()
179                .duration_since(std::time::UNIX_EPOCH)
180                .unwrap()
181                .as_nanos()
182        ));
183        let keypair =
184            NodeKeypair::generate_at(&temp_path).expect("keypair generation should succeed");
185        let envelope = EvolutionEnvelope::publish(
186            "node-a",
187            vec![NetworkAsset::Gene {
188                gene: sample_gene("gene-sign"),
189            }],
190        );
191        let signed = sign_envelope(&keypair, &envelope);
192        assert!(verify_envelope(&keypair.public_key_hex(), &signed).is_ok());
193        let _ = std::fs::remove_file(temp_path);
194    }
195}