1use 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 Status,
30 Keygen {
32 #[arg(long, default_value = "default")]
34 name: String,
35 },
36 Sign {
38 path: String,
40 #[arg(long, default_value = "default")]
42 key: String,
43 },
44 Verify {
46 path: String,
48 },
49 Encrypt {
51 path: String,
53 #[arg(long, default_value = "default")]
55 key: String,
56 },
57 Decrypt {
59 path: String,
61 #[arg(long, default_value = "default")]
63 key: String,
64 },
65 BootVerify,
67 Keys,
69}
70
71#[derive(serde::Serialize, serde::Deserialize)]
73struct PqcKeypair {
74 version: String,
75 name: String,
76 algorithm: String,
77 created_at: String,
78 key_id: String,
80 secret_key: String,
82 public_key: String,
84}
85
86#[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
116fn generate_key_material() -> (Vec<u8>, Vec<u8>) {
118 let mut secret = vec![0u8; 32]; let mut public = vec![0u8; 32]; OsRng.fill_bytes(&mut secret);
121 OsRng.fill_bytes(&mut public);
122 (secret, public)
123}
124
125fn 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 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
161mod hex {
163 pub fn encode(bytes: impl AsRef<[u8]>) -> String {
164 bytes.as_ref().iter().map(|b| format!("{b:02x}")).collect()
165 }
166}
167
168fn 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
176fn 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 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 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 #[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 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: {}", ¤t_hash[..32]);
332 return Ok(());
333 }
334
335 if verify_signature(¤t_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 let plaintext = std::fs::read(target)?;
368
369 let mut nonce_bytes = [0u8; 12];
371 OsRng.fill_bytes(&mut nonce_bytes);
372 let nonce = Nonce::from_slice(&nonce_bytes);
373
374 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 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 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 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 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 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 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}