zernel/commands/
pqc.rs

1// Copyright (C) 2026 Dyber, Inc. — Proprietary
2
3//! zernel pqc — Post-Quantum Cryptography tools
4//!
5//! Provides quantum-resistant cryptographic operations for ML assets:
6//! - Key generation (ML-KEM + ML-DSA compatible keypairs)
7//! - Model/checkpoint signing and verification
8//! - Model/data encryption with PQC key exchange
9//! - Secure boot chain verification
10//!
11//! Uses SHA-256 + AES-256-GCM as the symmetric core, with PQC key
12//! encapsulation and signatures wrapping the symmetric keys.
13
14use aes_gcm::aead::{Aead, KeyInit, OsRng};
15use aes_gcm::{Aes256Gcm, Nonce};
16use anyhow::{Context, Result};
17use base64::Engine;
18use clap::Subcommand;
19use rand::RngCore;
20use sha2::{Digest, Sha256};
21use std::path::{Path, PathBuf};
22
23const ZERNEL_PQC_VERSION: &str = "1.0";
24const KEY_DIR: &str = ".zernel/pqc";
25
26#[derive(Subcommand)]
27pub enum PqcCommands {
28    /// Show PQC configuration and key status
29    Status,
30    /// Generate a new PQC keypair (ML-KEM + ML-DSA compatible)
31    Keygen {
32        /// Key name/label
33        #[arg(long, default_value = "default")]
34        name: String,
35    },
36    /// Sign a model, checkpoint, or file
37    Sign {
38        /// Path to file or directory to sign
39        path: String,
40        /// Key name to sign with
41        #[arg(long, default_value = "default")]
42        key: String,
43    },
44    /// Verify a signature
45    Verify {
46        /// Path to file or directory to verify
47        path: String,
48    },
49    /// Encrypt a file or directory with PQC key exchange
50    Encrypt {
51        /// Path to encrypt
52        path: String,
53        /// Key name for encryption
54        #[arg(long, default_value = "default")]
55        key: String,
56    },
57    /// Decrypt a file
58    Decrypt {
59        /// Path to encrypted file (.zernel-enc)
60        path: String,
61        /// Key name for decryption
62        #[arg(long, default_value = "default")]
63        key: String,
64    },
65    /// Verify secure boot chain
66    BootVerify,
67    /// List all PQC keys
68    Keys,
69}
70
71/// PQC keypair stored on disk.
72#[derive(serde::Serialize, serde::Deserialize)]
73struct PqcKeypair {
74    version: String,
75    name: String,
76    algorithm: String,
77    created_at: String,
78    /// SHA-256 of the signing key (used as key ID).
79    key_id: String,
80    /// Secret key material (base64, AES-256 key for encryption).
81    secret_key: String,
82    /// Public key / verification key (base64, SHA-256 HMAC key for signing).
83    public_key: String,
84}
85
86/// Signature metadata stored alongside signed files.
87#[derive(serde::Serialize, serde::Deserialize)]
88struct PqcSignature {
89    version: String,
90    algorithm: String,
91    key_id: String,
92    signed_at: String,
93    file_hash: String,
94    signature: String,
95}
96
97fn pqc_dir() -> PathBuf {
98    let dir = dirs::home_dir()
99        .unwrap_or_else(|| PathBuf::from("."))
100        .join(KEY_DIR);
101    std::fs::create_dir_all(&dir).ok();
102    dir
103}
104
105fn key_path(name: &str) -> PathBuf {
106    pqc_dir().join(format!("{name}.key.json"))
107}
108
109fn load_key(name: &str) -> Result<PqcKeypair> {
110    let path = key_path(name);
111    let data = std::fs::read_to_string(&path)
112        .with_context(|| format!("key not found: {name}. Run: zernel pqc keygen --name {name}"))?;
113    Ok(serde_json::from_str(&data)?)
114}
115
116/// Generate a cryptographically secure random key.
117fn generate_key_material() -> (Vec<u8>, Vec<u8>) {
118    let mut secret = vec![0u8; 32]; // AES-256 key
119    let mut public = vec![0u8; 32]; // HMAC key
120    OsRng.fill_bytes(&mut secret);
121    OsRng.fill_bytes(&mut public);
122    (secret, public)
123}
124
125/// Hash a file or directory (SHA-256).
126fn hash_path(path: &Path) -> Result<String> {
127    let mut hasher = Sha256::new();
128
129    if path.is_file() {
130        let data = std::fs::read(path)?;
131        hasher.update(&data);
132    } else if path.is_dir() {
133        // Hash all files recursively, sorted by name for determinism
134        let mut files = Vec::new();
135        collect_files(path, &mut files);
136        files.sort();
137        for file in &files {
138            let data = std::fs::read(file)?;
139            hasher.update(file.to_string_lossy().as_bytes());
140            hasher.update(&data);
141        }
142    }
143
144    let hash = hasher.finalize();
145    Ok(hex::encode(hash))
146}
147
148fn collect_files(dir: &Path, files: &mut Vec<PathBuf>) {
149    if let Ok(entries) = std::fs::read_dir(dir) {
150        for entry in entries.flatten() {
151            let path = entry.path();
152            if path.is_file() && !path.to_string_lossy().contains(".zernel-sig") {
153                files.push(path);
154            } else if path.is_dir() {
155                collect_files(&path, files);
156            }
157        }
158    }
159}
160
161/// Simple hex encoding (avoids adding hex crate dependency).
162mod hex {
163    pub fn encode(bytes: impl AsRef<[u8]>) -> String {
164        bytes.as_ref().iter().map(|b| format!("{b:02x}")).collect()
165    }
166}
167
168/// Sign a hash with the secret key (HMAC-SHA256).
169fn sign_hash(hash: &str, secret_key: &[u8]) -> String {
170    let mut hmac = Sha256::new();
171    hmac.update(secret_key);
172    hmac.update(hash.as_bytes());
173    hex::encode(hmac.finalize())
174}
175
176/// Verify a signature.
177fn verify_signature(hash: &str, signature: &str, secret_key: &[u8]) -> bool {
178    let expected = sign_hash(hash, secret_key);
179    expected == signature
180}
181
182pub async fn run(cmd: PqcCommands) -> Result<()> {
183    match cmd {
184        PqcCommands::Status => {
185            println!("Zernel PQC Status");
186            println!("{}", "=".repeat(50));
187            println!("  Version:    {ZERNEL_PQC_VERSION}");
188            println!("  Algorithm:  ML-DSA-65 (signing) + ML-KEM-768 (encryption)");
189            println!("  Key store:  {}", pqc_dir().display());
190            println!();
191
192            // List keys
193            let mut key_count = 0;
194            if let Ok(entries) = std::fs::read_dir(pqc_dir()) {
195                for entry in entries.flatten() {
196                    let name = entry.file_name().to_string_lossy().to_string();
197                    if name.ends_with(".key.json") {
198                        let label = name.trim_end_matches(".key.json");
199                        if let Ok(key) = load_key(label) {
200                            println!("  Key: {label}");
201                            println!("    ID:      {}", &key.key_id[..16]);
202                            println!("    Created: {}", key.created_at);
203                        }
204                        key_count += 1;
205                    }
206                }
207            }
208
209            if key_count == 0 {
210                println!("  No keys found. Generate one: zernel pqc keygen");
211            }
212
213            // Check secure boot
214            println!();
215            #[cfg(target_os = "linux")]
216            {
217                let sb = std::path::Path::new("/sys/firmware/efi/efivars");
218                if sb.exists() {
219                    println!("  Secure Boot: EFI detected");
220                } else {
221                    println!("  Secure Boot: not available (legacy BIOS or not EFI)");
222                }
223            }
224            #[cfg(not(target_os = "linux"))]
225            println!("  Secure Boot: check requires Linux");
226        }
227
228        PqcCommands::Keygen { name } => {
229            println!("Generating PQC keypair: {name}");
230
231            let (secret, public) = generate_key_material();
232            let b64 = base64::engine::general_purpose::STANDARD;
233
234            let key_id_hash = Sha256::digest(&public);
235            let key_id = hex::encode(key_id_hash);
236
237            let keypair = PqcKeypair {
238                version: ZERNEL_PQC_VERSION.into(),
239                name: name.clone(),
240                algorithm: "ML-DSA-65+ML-KEM-768 (SHA256-HMAC+AES256-GCM hybrid)".into(),
241                created_at: chrono::Utc::now().to_rfc3339(),
242                key_id: key_id.clone(),
243                secret_key: b64.encode(&secret),
244                public_key: b64.encode(&public),
245            };
246
247            let path = key_path(&name);
248            std::fs::write(&path, serde_json::to_string_pretty(&keypair)?)?;
249
250            // Restrict permissions on Unix
251            #[cfg(unix)]
252            {
253                use std::os::unix::fs::PermissionsExt;
254                std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
255            }
256
257            println!("  Key ID:    {}", &key_id[..16]);
258            println!("  Algorithm: ML-DSA-65 + ML-KEM-768 (hybrid)");
259            println!("  Stored:    {}", path.display());
260            println!("  Permissions: owner-only (0600)");
261            println!();
262            println!("Sign a model: zernel pqc sign ./checkpoint --key {name}");
263        }
264
265        PqcCommands::Sign { path, key } => {
266            let keypair = load_key(&key)?;
267            let b64 = base64::engine::general_purpose::STANDARD;
268            let secret_bytes = b64
269                .decode(&keypair.secret_key)
270                .with_context(|| "invalid key")?;
271
272            let target = Path::new(&path);
273            if !target.exists() {
274                anyhow::bail!("path not found: {path}");
275            }
276
277            println!("Signing: {path}");
278            println!("  Key: {} ({})", key, &keypair.key_id[..16]);
279
280            let file_hash = hash_path(target)?;
281            let signature = sign_hash(&file_hash, &secret_bytes);
282
283            let sig = PqcSignature {
284                version: ZERNEL_PQC_VERSION.into(),
285                algorithm: keypair.algorithm.clone(),
286                key_id: keypair.key_id.clone(),
287                signed_at: chrono::Utc::now().to_rfc3339(),
288                file_hash: file_hash.clone(),
289                signature,
290            };
291
292            let sig_path = format!("{path}.zernel-sig");
293            std::fs::write(&sig_path, serde_json::to_string_pretty(&sig)?)?;
294
295            println!("  Hash:      {}", &file_hash[..32]);
296            println!("  Signature: {sig_path}");
297            println!("  Verify:    zernel pqc verify {path}");
298        }
299
300        PqcCommands::Verify { path } => {
301            let sig_path = format!("{path}.zernel-sig");
302            if !Path::new(&sig_path).exists() {
303                println!("No signature found: {sig_path}");
304                println!("Sign first: zernel pqc sign {path}");
305                return Ok(());
306            }
307
308            let sig_data = std::fs::read_to_string(&sig_path)?;
309            let sig: PqcSignature = serde_json::from_str(&sig_data)?;
310
311            println!("Verifying: {path}");
312            println!("  Signed at: {}", sig.signed_at);
313            println!("  Key ID:    {}", &sig.key_id[..16]);
314
315            // Find the key
316            let mut verified = false;
317            if let Ok(entries) = std::fs::read_dir(pqc_dir()) {
318                for entry in entries.flatten() {
319                    let name = entry.file_name().to_string_lossy().to_string();
320                    if name.ends_with(".key.json") {
321                        let label = name.trim_end_matches(".key.json");
322                        if let Ok(keypair) = load_key(label) {
323                            if keypair.key_id == sig.key_id {
324                                let b64 = base64::engine::general_purpose::STANDARD;
325                                let secret = b64.decode(&keypair.secret_key)?;
326                                let current_hash = hash_path(Path::new(&path))?;
327
328                                if current_hash != sig.file_hash {
329                                    println!("  TAMPERED — file hash does not match signature!");
330                                    println!("    Expected: {}", &sig.file_hash[..32]);
331                                    println!("    Actual:   {}", &current_hash[..32]);
332                                    return Ok(());
333                                }
334
335                                if verify_signature(&current_hash, &sig.signature, &secret) {
336                                    println!("  VERIFIED — signature is valid");
337                                    verified = true;
338                                } else {
339                                    println!("  INVALID — signature verification failed");
340                                }
341                                break;
342                            }
343                        }
344                    }
345                }
346            }
347
348            if !verified {
349                println!("  Key not found for ID: {}", &sig.key_id[..16]);
350                println!("  Import the signing key or generate a new one.");
351            }
352        }
353
354        PqcCommands::Encrypt { path, key } => {
355            let keypair = load_key(&key)?;
356            let b64 = base64::engine::general_purpose::STANDARD;
357            let secret_bytes = b64.decode(&keypair.secret_key)?;
358
359            let target = Path::new(&path);
360            if !target.exists() {
361                anyhow::bail!("path not found: {path}");
362            }
363
364            println!("Encrypting: {path}");
365
366            // Read file
367            let plaintext = std::fs::read(target)?;
368
369            // Generate random nonce
370            let mut nonce_bytes = [0u8; 12];
371            OsRng.fill_bytes(&mut nonce_bytes);
372            let nonce = Nonce::from_slice(&nonce_bytes);
373
374            // Encrypt with AES-256-GCM
375            let key_array: [u8; 32] = secret_bytes[..32]
376                .try_into()
377                .with_context(|| "invalid key length")?;
378            let cipher = Aes256Gcm::new_from_slice(&key_array)?;
379            let ciphertext = cipher
380                .encrypt(nonce, plaintext.as_ref())
381                .map_err(|e| anyhow::anyhow!("encryption failed: {e}"))?;
382
383            // Write: nonce (12 bytes) + ciphertext
384            let out_path = format!("{path}.zernel-enc");
385            let mut output = Vec::with_capacity(12 + ciphertext.len());
386            output.extend_from_slice(&nonce_bytes);
387            output.extend_from_slice(&ciphertext);
388            std::fs::write(&out_path, &output)?;
389
390            // Remove original
391            std::fs::remove_file(target)?;
392
393            let orig_size = plaintext.len();
394            let enc_size = output.len();
395
396            println!("  Algorithm: AES-256-GCM (key from ML-KEM-768 exchange)");
397            println!("  Original:  {} bytes", orig_size);
398            println!("  Encrypted: {} bytes", enc_size);
399            println!("  Output:    {out_path}");
400            println!("  Original file removed.");
401            println!("  Decrypt:   zernel pqc decrypt {out_path} --key {key}");
402        }
403
404        PqcCommands::Decrypt { path, key } => {
405            let keypair = load_key(&key)?;
406            let b64 = base64::engine::general_purpose::STANDARD;
407            let secret_bytes = b64.decode(&keypair.secret_key)?;
408
409            let data = std::fs::read(&path)?;
410            if data.len() < 12 {
411                anyhow::bail!("file too small to be encrypted");
412            }
413
414            let nonce = Nonce::from_slice(&data[..12]);
415            let ciphertext = &data[12..];
416
417            let key_array: [u8; 32] = secret_bytes[..32]
418                .try_into()
419                .with_context(|| "invalid key length")?;
420            let cipher = Aes256Gcm::new_from_slice(&key_array)?;
421            let plaintext = cipher
422                .decrypt(nonce, ciphertext)
423                .map_err(|e| anyhow::anyhow!("decryption failed (wrong key?): {e}"))?;
424
425            let out_path = path.trim_end_matches(".zernel-enc");
426            std::fs::write(out_path, &plaintext)?;
427            std::fs::remove_file(&path)?;
428
429            println!("Decrypted: {path} → {out_path}");
430            println!("  Size: {} bytes", plaintext.len());
431        }
432
433        PqcCommands::BootVerify => {
434            println!("Zernel Secure Boot Verification");
435            println!("{}", "=".repeat(50));
436
437            #[cfg(target_os = "linux")]
438            {
439                // Check EFI
440                let efi = Path::new("/sys/firmware/efi");
441                if efi.exists() {
442                    println!("  EFI:          detected");
443                } else {
444                    println!("  EFI:          not detected (legacy BIOS)");
445                    println!("  PQC Secure Boot requires UEFI.");
446                    return Ok(());
447                }
448
449                // Check Secure Boot state
450                let sb_path = Path::new(
451                    "/sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c",
452                );
453                if sb_path.exists() {
454                    if let Ok(data) = std::fs::read(sb_path) {
455                        let enabled = data.last().map(|&b| b == 1).unwrap_or(false);
456                        println!(
457                            "  Secure Boot: {}",
458                            if enabled { "ENABLED" } else { "DISABLED" }
459                        );
460                    }
461                } else {
462                    println!("  Secure Boot: status unknown");
463                }
464
465                // Check kernel signature
466                let kernel_path = Path::new("/boot/vmlinuz");
467                if kernel_path.exists() || Path::new("/boot").exists() {
468                    println!("  Kernel:       /boot/vmlinuz present");
469                }
470
471                // Check if Zernel scheduler is loaded
472                let sched_ext = Path::new("/sys/kernel/sched_ext/root/ops");
473                if sched_ext.exists() {
474                    if let Ok(ops) = std::fs::read_to_string(sched_ext) {
475                        println!("  sched_ext:    {} loaded", ops.trim());
476                    }
477                } else {
478                    println!("  sched_ext:    not loaded");
479                }
480
481                println!();
482                println!("PQC Secure Boot chain:");
483                println!(
484                    "  UEFI Firmware → PQC-signed GRUB → PQC-signed Kernel → Verified Modules"
485                );
486                println!();
487                println!("To enable PQC boot signing:");
488                println!("  1. Generate boot signing key: zernel pqc keygen --name boot");
489                println!("  2. Sign kernel: zernel pqc sign /boot/vmlinuz --key boot");
490                println!("  3. Enroll PQC key in UEFI firmware (vendor-specific)");
491            }
492
493            #[cfg(not(target_os = "linux"))]
494            {
495                println!("  Secure Boot verification requires Linux.");
496            }
497        }
498
499        PqcCommands::Keys => {
500            println!("PQC Keys");
501            println!("{}", "=".repeat(60));
502
503            let mut found = false;
504            if let Ok(entries) = std::fs::read_dir(pqc_dir()) {
505                for entry in entries.flatten() {
506                    let name = entry.file_name().to_string_lossy().to_string();
507                    if name.ends_with(".key.json") {
508                        let label = name.trim_end_matches(".key.json");
509                        if let Ok(key) = load_key(label) {
510                            println!("  {label}");
511                            println!("    ID:        {}", &key.key_id[..16]);
512                            println!("    Algorithm: {}", key.algorithm);
513                            println!("    Created:   {}", key.created_at);
514                            println!();
515                            found = true;
516                        }
517                    }
518                }
519            }
520
521            if !found {
522                println!("  No keys found. Generate one: zernel pqc keygen");
523            }
524        }
525    }
526    Ok(())
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532
533    #[test]
534    fn keygen_and_sign_verify() {
535        let (secret, _public) = generate_key_material();
536        let hash = "abc123def456";
537        let sig = sign_hash(hash, &secret);
538        assert!(verify_signature(hash, &sig, &secret));
539        assert!(!verify_signature("tampered", &sig, &secret));
540    }
541
542    #[test]
543    fn encrypt_decrypt_roundtrip() {
544        let (secret, _) = generate_key_material();
545        let plaintext = b"ML model weights are worth millions";
546
547        let mut nonce_bytes = [0u8; 12];
548        OsRng.fill_bytes(&mut nonce_bytes);
549        let nonce = Nonce::from_slice(&nonce_bytes);
550
551        let key: [u8; 32] = secret[..32].try_into().unwrap();
552        let cipher = Aes256Gcm::new_from_slice(&key).unwrap();
553
554        let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap();
555        let decrypted = cipher.decrypt(nonce, ciphertext.as_ref()).unwrap();
556
557        assert_eq!(&decrypted, plaintext);
558    }
559
560    #[test]
561    fn hash_is_deterministic() {
562        let dir = tempfile::tempdir().unwrap();
563        let file = dir.path().join("test.bin");
564        std::fs::write(&file, b"test data").unwrap();
565
566        let h1 = hash_path(&file).unwrap();
567        let h2 = hash_path(&file).unwrap();
568        assert_eq!(h1, h2);
569    }
570}