Skip to main content

speck_core/
crypto.rs

1//! Cryptographic primitives for module signing
2//! 
3//! Uses Ed25519 for compact, fast signatures suitable for embedded systems.
4
5use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey, Signature as DalekSignature};
6use rand::rngs::OsRng;
7use sha2::{Sha256, Digest};
8use alloc::vec::Vec;
9use alloc::string::{String, ToString};
10use core::convert::TryInto;
11
12use crate::error::{Error, Result};
13use crate::format::Module;
14
15/// Size of Ed25519 public key in bytes
16pub const PUBLIC_KEY_SIZE: usize = 32;
17
18/// Size of Ed25519 secret key in bytes
19pub const SECRET_KEY_SIZE: usize = 32;
20
21/// Size of Ed25519 signature in bytes
22pub const SIGNATURE_SIZE: usize = 64;
23
24/// Ed25519 public key wrapper with additional metadata
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct PublicKey {
27    inner: VerifyingKey,
28}
29
30impl PublicKey {
31    /// Create from raw bytes
32    pub fn from_bytes(bytes: &[u8; PUBLIC_KEY_SIZE]) -> Result<Self> {
33        VerifyingKey::from_bytes(bytes)
34            .map(|inner| Self { inner })
35            .map_err(|e| Error::crypto(format!("invalid public key: {}", e)))
36    }
37    
38    /// Export as raw bytes
39    pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] {
40        self.inner.to_bytes()
41    }
42    
43    /// Compute fingerprint (SHA256 truncated to 16 bytes for display)
44    pub fn fingerprint(&self) -> [u8; 16] {
45        let mut hasher = Sha256::new();
46        hasher.update(self.to_bytes());
47        let result = hasher.finalize();
48        let mut fp = [0u8; 16];
49        fp.copy_from_slice(&result[..16]);
50        fp
51    }
52    
53    /// Format fingerprint as hexadecimal string
54    pub fn fingerprint_hex(&self) -> String {
55        hex::encode(self.fingerprint())
56    }
57    
58    /// Verify a signature on a message
59    pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<()> {
60        self.inner.verify(message, &signature.inner)
61            .map_err(|_| Error::VerificationFailed {
62                details: "signature verification failed".into(),
63            })
64    }
65}
66
67impl AsRef<VerifyingKey> for PublicKey {
68    fn as_ref(&self) -> &VerifyingKey {
69        &self.inner
70    }
71}
72
73/// Ed25519 signature wrapper
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub struct Signature {
76    inner: DalekSignature,
77}
78
79impl Signature {
80    /// Create from raw bytes
81    pub fn from_bytes(bytes: &[u8; SIGNATURE_SIZE]) -> Result<Self> {
82        Ok(Self {
83            inner: DalekSignature::from_bytes(bytes),
84        })
85    }
86    
87    /// Export as raw bytes
88    pub fn to_bytes(&self) -> [u8; SIGNATURE_SIZE] {
89        self.inner.to_bytes()
90    }
91}
92
93/// Ed25519 key pair for signing
94#[derive(Debug)]
95pub struct KeyPair {
96    signing: SigningKey,
97}
98
99impl KeyPair {
100    /// Generate a new random key pair using OS RNG
101    pub fn generate() -> Self {
102        let mut csprng = OsRng;
103        let signing = SigningKey::generate(&mut csprng);
104        Self { signing }
105    }
106    
107    /// Load from secret key seed (32 bytes)
108    pub fn from_seed(seed: &[u8; SECRET_KEY_SIZE]) -> Self {
109        let signing = SigningKey::from_bytes(seed);
110        Self { signing }
111    }
112    
113    /// Export secret key seed
114    pub fn to_seed(&self) -> [u8; SECRET_KEY_SIZE] {
115        self.signing.to_bytes()
116    }
117    
118    /// Get the public key
119    pub fn public(&self) -> PublicKey {
120        PublicKey {
121            inner: self.signing.verifying_key(),
122        }
123    }
124    
125    /// Sign a message
126    pub fn sign(&self, message: &[u8]) -> Signature {
127        Signature {
128            inner: self.signing.sign(message),
129        }
130    }
131    
132    /// Sign a module in-place, updating its headers
133    pub fn sign_module(&self, module: &mut Module) {
134        let signature = self.sign(&module.code);
135        module.signature = signature.to_bytes();
136        module.public_key = self.public().to_bytes();
137        module.header.flags |= format::FLAG_SIGNED;
138    }
139
140    /// Verify a module using embedded public key
141    pub fn verify_module(module: &Module) -> Result<()> {
142        if module.header.flags & format::FLAG_SIGNED == 0 {
143            return Err(Error::VerificationFailed {
144                details: "module is not signed".into(),
145            });
146        }
147        
148        let public_key = PublicKey::from_bytes(&module.public_key)?;
149        let signature = Signature::from_bytes(&module.signature)?;
150        
151        public_key.verify(&module.code, &signature)
152    }
153}
154
155impl Clone for KeyPair {
156    fn clone(&self) -> Self {
157        Self::from_seed(&self.to_seed())
158    }
159}
160
161use crate::format;
162
163/// Parse a key from hex string or file path
164#[cfg(feature = "std")]
165pub fn parse_key(source: &str) -> Result<KeyPair> {
166    use std::fs;
167    use std::path::Path;
168    
169    // Try reading as file first
170    if Path::new(source).exists() {
171        let contents = fs::read_to_string(source)
172            .map_err(|e| Error::crypto(format!("failed to read key file: {}", e)))?;
173        return parse_key_hex(&contents);
174    }
175    
176    // Otherwise treat as hex string
177    parse_key_hex(source)
178}
179
180fn parse_key_hex(hex_str: &str) -> Result<KeyPair> {
181    let cleaned: Vec<u8> = hex_str.chars()
182        .filter(|c| c.is_ascii_hexdigit())
183        .collect::<String>()
184        .as_bytes()
185        .chunks(2)
186        .map(|chunk| {
187            u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 16).unwrap()
188        })
189        .collect();
190    
191    if cleaned.len() != SECRET_KEY_SIZE {
192        return Err(Error::crypto(format!(
193            "invalid key length: expected {} bytes, got {}", 
194            SECRET_KEY_SIZE, cleaned.len()
195        )));
196    }
197    
198    let mut seed = [0u8; SECRET_KEY_SIZE];
199    seed.copy_from_slice(&cleaned);
200    Ok(KeyPair::from_seed(&seed))
201}