Skip to main content

origin_crypto_sdk/seed/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Seed handle with time-to-live and memory security.
4//!
5//! `SeedHandle` wraps raw seed bytes with:
6//! - Optional TTL (automatic zeroization after expiry)
7//! - mlock to prevent swapping to disk (Sovereign tier)
8//! - Deterministic key derivation via HKDF-SHA3-256
9//! - Fingerprint for identity commitment
10//!
11//! # Submodules
12//!
13//! - [`seed::gen`] — Multi-hash seed generation with hash diversity.
14
15pub mod gen;
16
17use std::time::{Duration, Instant};
18
19use crate::internal::zeroize::Zeroize;
20use crate::primitives::sha3::sha3_256;
21use crate::primitives::MemoryTier;
22
23/// A handle to a cryptographic seed with TTL and memory security.
24///
25/// The seed is held in memory until the TTL expires (or indefinitely if None).
26/// On drop, the seed is zeroized. Sovereign tier uses mlock to prevent swapping.
27#[derive(Clone)]
28pub struct SeedHandle {
29    seed: Vec<u8>,
30    created: Instant,
31    ttl: Option<Duration>,
32    tier: MemoryTier,
33}
34
35impl SeedHandle {
36    /// Create a new seed handle from raw seed bytes.
37    ///
38    /// # Arguments
39    /// * `seed` - Raw seed bytes (will be copied and zeroized on drop)
40    /// * `ttl` - Optional time-to-live. None = never expires.
41    pub fn new(seed: &[u8], ttl: Option<Duration>) -> Self {
42        let seed_vec = seed.to_vec();
43        // mlock for Sovereign tier to prevent swapping
44        #[cfg(unix)]
45        if !seed_vec.is_empty() {
46            let _ = unsafe {
47                extern "C" {
48                    fn mlock(addr: *const u8, len: usize) -> i32;
49                }
50                mlock(seed_vec.as_ptr(), seed_vec.len());
51            };
52        }
53
54        Self {
55            seed: seed_vec,
56            created: Instant::now(),
57            ttl,
58            tier: MemoryTier::default(),
59        }
60    }
61
62    /// Create with explicit memory tier.
63    pub fn with_tier(seed: &[u8], ttl: Option<Duration>, tier: MemoryTier) -> Self {
64        let mut handle = Self::new(seed, ttl);
65        handle.tier = tier;
66        handle
67    }
68
69    /// Check if this handle has expired.
70    pub fn is_expired(&self) -> bool {
71        match self.ttl {
72            Some(ttl) => self.created.elapsed() > ttl,
73            None => false,
74        }
75    }
76
77    /// Get the seed bytes. Returns None if expired.
78    pub fn as_bytes(&self) -> Option<&[u8]> {
79        if self.is_expired() {
80            None
81        } else {
82            Some(&self.seed)
83        }
84    }
85
86    /// Get seed bytes without TTL check (use with caution).
87    pub fn as_bytes_unchecked(&self) -> &[u8] {
88        &self.seed
89    }
90
91    /// Compute a fingerprint (SHA3-256 of the seed).
92    ///
93    /// This is safe to share publicly -- it's a one-way hash of the seed.
94    pub fn fingerprint(&self) -> [u8; 32] {
95        sha3_256(&self.seed)
96    }
97
98    /// Derive a domain-specific key using HKDF-SHA3-256.
99    ///
100    /// # Arguments
101    /// * `domain` - Domain string (e.g., "signing", "encryption", "did:origin:abc")
102    /// * `info` - Additional info for key separation
103    /// Derive a domain-specific key using HKDF-SHA3-256.
104    pub fn derive_key(&self, domain: &str, info: &str, len: usize) -> Option<Vec<u8>> {
105        if self.is_expired() {
106            return None;
107        }
108        let mut output = vec![0u8; len];
109        crate::kdf::hkdf::hkdf_sha3_256(
110            &self.seed,
111            Some(domain.as_bytes()),
112            info.as_bytes(),
113            &mut output,
114        )
115        .ok()
116        .map(|_| output)
117    }
118
119    /// Get the memory tier.
120    pub fn tier(&self) -> MemoryTier {
121        self.tier
122    }
123
124    /// Get remaining TTL duration (None if no TTL set).
125    pub fn remaining(&self) -> Option<Duration> {
126        self.ttl.map(|ttl| {
127            let elapsed = self.created.elapsed();
128            if elapsed >= ttl {
129                Duration::ZERO
130            } else {
131                ttl - elapsed
132            }
133        })
134    }
135}
136
137impl Drop for SeedHandle {
138    fn drop(&mut self) {
139        // Zeroize seed bytes
140        self.seed.zeroize();
141
142        // munlock for Sovereign tier
143        #[cfg(unix)]
144        if !self.seed.is_empty() {
145            // seed is already zeroized, but munlock the pages
146            unsafe {
147                extern "C" {
148                    fn munlock(addr: *const u8, len: usize) -> i32;
149                }
150                munlock(self.seed.as_ptr(), self.seed.capacity());
151            }
152        }
153    }
154}
155
156impl std::fmt::Debug for SeedHandle {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.debug_struct("SeedHandle")
159            .field("seed_len", &self.seed.len())
160            .field("expired", &self.is_expired())
161            .field("tier", &self.tier)
162            .field("ttl", &self.ttl)
163            .finish()
164    }
165}
166
167/// Derive a child seed from a parent seed using HKDF.
168///
169/// Used for hierarchical key derivation (BIP-32 style).
170/// The child seed is derived as: HKDF-SHA3-256(salt=domain, ikm=parent_seed, info="origin-child-seed")
171pub fn derive_child_seed(parent: &[u8], domain: &str) -> Result<Vec<u8>, String> {
172    if parent.is_empty() {
173        return Err("Parent seed is empty".to_string());
174    }
175    if domain.is_empty() {
176        return Err("Domain is empty".to_string());
177    }
178    let mut output = [0u8; 32];
179    crate::kdf::hkdf::hkdf_sha3_256(
180        parent,
181        Some(domain.as_bytes()),
182        b"origin-child-seed",
183        &mut output,
184    )
185    .map_err(|e| format!("HKDF failed: {}", e))?;
186    Ok(output.to_vec())
187}
188
189/// Derive signing keys (Ed25519 + Falcon-1024) from a seed and domain.
190///
191/// Uses HKDF to derive separate seeds for each algorithm from the master seed.
192/// Returns (ed25519_sk, falcon_sk).
193pub fn derive_signing_keys(seed: &[u8], domain: &str) -> Result<(Vec<u8>, Vec<u8>), String> {
194    // Derive Ed25519 seed (32 bytes)
195    let mut ed_seed = [0u8; 32];
196    crate::kdf::hkdf::hkdf_sha3_256(
197        seed,
198        Some(b"signing"),
199        &[domain.as_bytes(), b"ed25519"].concat(),
200        &mut ed_seed,
201    )
202    .map_err(|e| format!("Ed25519 key derivation failed: {}", e))?;
203
204    // Derive Falcon seed (32 bytes)
205    let mut falcon_seed = [0u8; 32];
206    crate::kdf::hkdf::hkdf_sha3_256(
207        seed,
208        Some(b"signing"),
209        &[domain.as_bytes(), b"falcon1024"].concat(),
210        &mut falcon_seed,
211    )
212    .map_err(|e| format!("Falcon key derivation failed: {}", e))?;
213
214    // Get Ed25519 secret key from seed
215    let ed25519_sk = ed_seed.to_vec();
216
217    // Get Falcon keypair from seed -- pack both SK and PK
218    let (falcon_pk, falcon_sk) = crate::pqc::falcon1024::generate_keypair_from_seed(&falcon_seed)
219        .map_err(|e| format!("Falcon keypair generation failed: {}", e))?;
220
221    // Pack: [sk_bytes | pk_bytes] so derive_verifying_keys can extract the PK
222    let mut falcon_packed = falcon_sk.as_bytes().to_vec();
223    falcon_packed.extend_from_slice(falcon_pk.as_bytes());
224
225    Ok((ed25519_sk, falcon_packed))
226}
227
228/// Derive verifying keys from signing keys.
229///
230/// Extracts the Falcon PK from the packed [SK|PK] bytes and
231/// derives the Ed25519 PK from the Ed25519 SK.
232pub fn derive_verifying_keys(
233    ed25519_sk: &[u8],
234    falcon_packed: &[u8],
235) -> Result<(Vec<u8>, Vec<u8>), String> {
236    // Ed25519: expand secret key to get public key
237    let ed_secret = ed25519_dalek::SigningKey::from_bytes(
238        ed25519_sk
239            .try_into()
240            .map_err(|_| "Invalid Ed25519 secret key length")?,
241    );
242    let ed_public = ed_secret.verifying_key();
243    let ed25519_pk = ed_public.to_bytes().to_vec();
244
245    // Falcon: extract PK from packed [SK|PK] bytes
246    // Falcon-1024: SK = 2305 bytes, PK = 1793 bytes
247    const FALCON_SK_SIZE: usize = 2305;
248    if falcon_packed.len() <= FALCON_SK_SIZE {
249        return Err("Packed Falcon bytes too short -- missing public key".to_string());
250    }
251    let falcon_pk = falcon_packed[FALCON_SK_SIZE..].to_vec();
252
253    Ok((ed25519_pk, falcon_pk))
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_seed_handle_basic() {
262        let seed = [42u8; 32];
263        let handle = SeedHandle::new(&seed, None);
264        assert!(!handle.is_expired());
265        assert_eq!(handle.as_bytes().unwrap(), &seed);
266        assert_eq!(handle.fingerprint(), sha3_256(&seed));
267    }
268
269    #[test]
270    fn test_seed_handle_ttl() {
271        let seed = [42u8; 32];
272        let handle = SeedHandle::new(&seed, Some(Duration::from_secs(1)));
273        assert!(!handle.is_expired());
274        assert!(handle.remaining().unwrap() > Duration::ZERO);
275    }
276
277    #[test]
278    fn test_seed_handle_derive_key() {
279        let seed = [42u8; 32];
280        let handle = SeedHandle::new(&seed, None);
281        let key1 = handle.derive_key("signing", "ed25519", 32).unwrap();
282        let key2 = handle.derive_key("signing", "ed25519", 32).unwrap();
283        assert_eq!(key1, key2, "Same domain+info should produce same key");
284
285        let key3 = handle.derive_key("encryption", "xchacha20", 32).unwrap();
286        assert_ne!(key1, key3, "Different domain should produce different key");
287    }
288
289    #[test]
290    fn test_derive_child_seed() {
291        let parent = [1u8; 32];
292        let child1 = derive_child_seed(&parent, "domain1").unwrap();
293        let child2 = derive_child_seed(&parent, "domain2").unwrap();
294        assert_ne!(
295            child1, child2,
296            "Different domains should produce different children"
297        );
298        assert_eq!(child1.len(), 32, "Child seed should be 32 bytes");
299    }
300
301    #[test]
302    fn test_derive_signing_keys() {
303        let seed = [42u8; 32];
304        let (ed_sk, falcon_sk) = derive_signing_keys(&seed, "test-domain").unwrap();
305        assert_eq!(ed_sk.len(), 32, "Ed25519 SK should be 32 bytes");
306        assert!(!falcon_sk.is_empty(), "Falcon SK should not be empty");
307    }
308
309    #[test]
310    fn test_seed_handle_zeroize() {
311        let seed = [42u8; 32];
312        let handle = SeedHandle::new(&seed, None);
313        let _ = handle; // drop triggers zeroize
314                        // Can't directly verify zeroization, but no panic = success
315    }
316}