1use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
2use sha2::{Digest, Sha256};
3use std::fs;
4use std::path::Path;
5
6fn generate_master_secret() -> Result<[u8; 32], String> {
7 let mut secret = [0u8; 32];
8 getrandom::fill(&mut secret).map_err(|e| format!("OS RNG failed: {e}"))?;
9 Ok(secret)
10}
11
12#[cfg(not(target_arch = "wasm32"))]
13fn keyring_entry_for(storage_dir: &str) -> Result<keyring::Entry, String> {
14 let mut hasher = Sha256::new();
15 hasher.update(storage_dir.as_bytes());
16 let digest = hasher.finalize();
17 let username = format!("master_{}", hex::encode(&digest[..8]));
18 keyring::Entry::new("qualia_db", &username).map_err(|e| format!("Keyring error: {e}"))
19}
20
21pub struct KeyVault {
23 master_key: Option<SigningKey>,
24 storage_dir: Option<String>,
25}
26
27impl KeyVault {
28 pub fn new() -> Self {
30 use sha2::{Digest, Sha256};
31 let mut hasher = Sha256::new();
32 hasher.update(b"ephemeral");
33 let result = hasher.finalize();
34 let mut secret = [0u8; 32];
35 secret.copy_from_slice(&result);
36 Self {
37 master_key: Some(SigningKey::from_bytes(&secret)),
38 storage_dir: None,
39 }
40 }
41
42 pub fn is_locked(&self) -> bool {
43 self.master_key.is_none()
44 }
45
46 pub fn lock(&mut self) {
47 self.master_key = None;
48 }
49
50 #[cfg(not(target_arch = "wasm32"))]
51 pub fn unlock(&mut self) -> Result<(), String> {
52 let dir = self
53 .storage_dir
54 .as_ref()
55 .ok_or("No storage dir configured")?;
56 let temp = Self::load_or_generate(dir)?;
57 self.master_key = temp.master_key;
58 Ok(())
59 }
60
61 #[cfg(target_arch = "wasm32")]
62 pub fn unlock(&mut self) -> Result<(), String> {
63 let temp = Self::load_or_generate("")?;
64 self.master_key = temp.master_key;
65 Ok(())
66 }
67
68 #[cfg(not(target_arch = "wasm32"))]
69 pub fn load_or_generate(storage_dir: &str) -> Result<Self, String> {
70 let vault_path = Path::new(storage_dir).join("keystore.bin");
71 let entry = keyring_entry_for(storage_dir)?;
72
73 let master_key = match entry.get_password() {
74 Ok(secret_hex) => {
75 let bytes =
76 hex::decode(secret_hex).map_err(|e| format!("Invalid hex in keyring: {e}"))?;
77 if bytes.len() != 32 {
78 return Err("Corrupted master key length in keyring".into());
79 }
80 let mut secret = [0u8; 32];
81 secret.copy_from_slice(&bytes[0..32]);
82 SigningKey::from_bytes(&secret)
83 }
84 Err(_) => {
85 if vault_path.exists() {
86 let bytes = fs::read(&vault_path)
87 .map_err(|e| format!("Failed to read keystore: {e}"))?;
88 if bytes.len() != 32 {
89 return Err("Corrupted master key length".into());
90 }
91 let secret_hex = hex::encode(&bytes[0..32]);
92 let _ = entry.set_password(&secret_hex);
93 let mut secret = [0u8; 32];
94 secret.copy_from_slice(&bytes[0..32]);
95 SigningKey::from_bytes(&secret)
96 } else {
97 let secret = generate_master_secret()?;
98 let new_key = SigningKey::from_bytes(&secret);
99 let secret_hex = hex::encode(secret);
100 let _ = entry.set_password(&secret_hex);
101 if let Some(parent) = vault_path.parent() {
102 fs::create_dir_all(parent)
103 .map_err(|e| format!("Failed to create keystore dir: {e}"))?;
104 }
105 fs::write(&vault_path, secret)
106 .map_err(|e| format!("Failed to write keystore: {e}"))?;
107 new_key
108 }
109 }
110 };
111
112 Ok(Self {
113 master_key: Some(master_key),
114 storage_dir: Some(storage_dir.to_string()),
115 })
116 }
117
118 #[cfg(target_arch = "wasm32")]
119 pub fn load_or_generate(storage_dir: &str) -> Result<Self, String> {
120 let vault_path = Path::new(storage_dir).join("keystore.bin");
121
122 let master_key = if vault_path.exists() {
123 let bytes =
124 fs::read(&vault_path).map_err(|e| format!("Failed to read keystore: {}", e))?;
125 if bytes.len() != 32 {
126 return Err("Corrupted master key length".into());
127 }
128 let mut secret = [0u8; 32];
129 secret.copy_from_slice(&bytes[0..32]);
130 SigningKey::from_bytes(&secret)
131 } else {
132 let secret = generate_master_secret()?;
133 if let Some(parent) = vault_path.parent() {
134 if !parent.as_os_str().is_empty() {
135 fs::create_dir_all(parent)
136 .map_err(|e| format!("Failed to create keystore dir: {e}"))?;
137 }
138 }
139 if !storage_dir.is_empty() {
140 fs::write(&vault_path, secret)
141 .map_err(|e| format!("Failed to write keystore: {e}"))?;
142 }
143 SigningKey::from_bytes(&secret)
144 };
145
146 Ok(Self {
147 master_key: Some(master_key),
148 storage_dir: Some(storage_dir.to_string()),
149 })
150 }
151
152 pub fn derive_key(&self, context_id: &str) -> SigningKey {
155 let master_key = self.master_key.as_ref().expect("Vault is locked");
156 let mut hasher = Sha256::new();
157 hasher.update(master_key.to_bytes());
158 hasher.update(context_id.as_bytes());
159 let result = hasher.finalize();
160
161 let mut child_secret = [0u8; 32];
162 child_secret.copy_from_slice(&result);
163 SigningKey::from_bytes(&child_secret)
164 }
165
166 pub fn sign_payload(&self, signing_key: &SigningKey, payload: &[u8]) -> Signature {
168 signing_key.sign(payload)
169 }
170
171 pub fn get_master_key_bytes(&self) -> [u8; 32] {
173 self.master_key
174 .as_ref()
175 .expect("Vault is locked")
176 .to_bytes()
177 }
178
179 pub fn public_key_bytes_for_context(&self, context_id: &str) -> [u8; 32] {
181 VerifyingKey::from(&self.derive_key(context_id)).to_bytes()
182 }
183
184 pub fn verify_signature(
186 public_key_bytes: &[u8; 32],
187 payload: &[u8],
188 signature_bytes: &[u8; 64],
189 ) -> Result<(), String> {
190 let verifying_key =
191 VerifyingKey::from_bytes(public_key_bytes).map_err(|_| "Invalid public key")?;
192 let signature = Signature::from_bytes(signature_bytes);
193
194 verifying_key
195 .verify(payload, &signature)
196 .map_err(|_| "Invalid signature".to_string())
197 }
198
199 pub fn generate_webid_tls_cert(
206 &self,
207 key: &SigningKey,
208 did_uri: &str,
209 ) -> Result<(String, String), String> {
210 generate_webid_tls_cert_for_seed(&key.to_bytes(), did_uri)
211 }
212}
213
214pub fn generate_webid_tls_cert_for_seed(
216 seed: &[u8; 32],
217 did_uri: &str,
218) -> Result<(String, String), String> {
219 use rcgen::{
220 CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SanType,
221 };
222
223 if did_uri.trim().is_empty() {
224 return Err("did_uri must not be empty".into());
225 }
226
227 let key_pem = format_ed25519_pkcs8_pem(seed);
228 let key_pair =
229 KeyPair::from_pem(&key_pem).map_err(|e| format!("rcgen KeyPair::from_pem: {e}"))?;
230
231 let mut params =
232 CertificateParams::new(Vec::<String>::new()).map_err(|e| format!("cert params: {e}"))?;
233 params.distinguished_name.push(DnType::CommonName, did_uri);
234 params.subject_alt_names = vec![SanType::URI(
235 did_uri.try_into().map_err(|e| format!("SAN URI: {e}"))?,
236 )];
237 params.is_ca = IsCa::NoCa;
238 params.key_usages = vec![
239 KeyUsagePurpose::DigitalSignature,
240 KeyUsagePurpose::KeyEncipherment,
241 ];
242 params.extended_key_usages = vec![
243 ExtendedKeyUsagePurpose::ClientAuth,
244 ExtendedKeyUsagePurpose::ServerAuth,
245 ];
246
247 let cert = params
248 .self_signed(&key_pair)
249 .map_err(|e| format!("self_signed: {e}"))?;
250 Ok((cert.pem(), key_pair.serialize_pem()))
251}
252
253#[cfg(test)]
254mod webid_tls_tests {
255 use super::*;
256
257 #[test]
258 fn webid_tls_cert_is_real_pem_with_did_san() {
259 let seed = [9u8; 32];
260 let did = "did:q42:person:aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
261 let (cert, key) = generate_webid_tls_cert_for_seed(&seed, did).expect("cert");
262 assert!(cert.contains("BEGIN CERTIFICATE"));
263 assert!(key.contains("BEGIN PRIVATE KEY") || key.contains("BEGIN"));
264 assert!(!cert.contains("MIIMOCK"));
265 }
266}
267
268fn format_ed25519_pkcs8_pem(seed: &[u8; 32]) -> String {
270 let mut inner_octet = Vec::with_capacity(2 + 32);
276 inner_octet.push(0x04); inner_octet.push(32);
278 inner_octet.extend_from_slice(seed);
279
280 let mut body = Vec::new();
281 body.extend_from_slice(&[0x02, 0x01, 0x00]);
283 body.extend_from_slice(&[0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70]);
285 body.push(0x04);
287 body.push(inner_octet.len() as u8);
288 body.extend_from_slice(&inner_octet);
289
290 let mut der = Vec::new();
291 der.push(0x30);
292 der.push(body.len() as u8);
293 der.extend_from_slice(&body);
294
295 use base64::Engine as _;
296 let b64 = base64::engine::general_purpose::STANDARD.encode(&der);
297 let mut pem = String::from("-----BEGIN PRIVATE KEY-----\n");
298 for chunk in b64.as_bytes().chunks(64) {
299 pem.push_str(std::str::from_utf8(chunk).unwrap_or(""));
300 pem.push('\n');
301 }
302 pem.push_str("-----END PRIVATE KEY-----\n");
303 pem
304}
305
306impl KeyVault {
307 pub fn issue_qapp_token(
310 &self,
311 qapp_did: &str,
312 audience: &str,
313 expiry_epoch: u64,
314 nonce: &str,
315 capabilities: Vec<String>,
316 sensitivity_clearance: SubgraphLayer,
317 ) -> Result<String, String> {
318 let payload = QappSessionTokenV2 {
319 qapp_did: qapp_did.to_string(),
320 expiry_epoch,
321 audience: audience.to_string(),
322 nonce: nonce.to_string(),
323 capabilities,
324 sensitivity_clearance,
325 };
326 let payload_json =
327 serde_json::to_string(&payload).map_err(|e| format!("Serialization error: {}", e))?;
328
329 let master_key = self.master_key.as_ref().expect("Vault is locked");
330 let signature = self.sign_payload(master_key, payload_json.as_bytes());
331 let signature_hex = hex::encode(signature.to_bytes());
332 let payload_hex = hex::encode(payload_json.as_bytes());
333
334 Ok(format!("{}.{}", payload_hex, signature_hex))
336 }
337
338 pub fn verify_qapp_token(
340 &self,
341 token: &str,
342 expected_audience: &str,
343 ) -> Result<QappSessionTokenV2, String> {
344 let parts: Vec<&str> = token.split('.').collect();
345 if parts.len() != 2 {
346 return Err("Invalid semantic token format".into());
347 }
348
349 let payload_bytes =
350 hex::decode(parts[0]).map_err(|_| "Invalid payload hex representation".to_string())?;
351 let signature_bytes = hex::decode(parts[1])
352 .map_err(|_| "Invalid signature hex representation".to_string())?;
353
354 if signature_bytes.len() != 64 {
355 return Err("Invalid signature byte length".into());
356 }
357 let mut sig_array = [0u8; 64];
358 sig_array.copy_from_slice(&signature_bytes);
359
360 let master_key = self.master_key.as_ref().expect("Vault is locked");
361 let verifying_key = VerifyingKey::from(master_key);
362 let signature = Signature::from_bytes(&sig_array);
363 verifying_key
364 .verify(&payload_bytes, &signature)
365 .map_err(|_| "Invalid token signature".to_string())?;
366
367 let payload: QappSessionTokenV2 = serde_json::from_slice(&payload_bytes)
368 .map_err(|e| format!("Failed to parse token payload: {}", e))?;
369
370 let now = std::time::SystemTime::now()
371 .duration_since(std::time::UNIX_EPOCH)
372 .map_err(|_| "Time went backwards")?
373 .as_secs();
374
375 if payload.expiry_epoch < now {
376 return Err("Token expired".into());
377 }
378
379 if payload.audience != expected_audience {
380 return Err("Token audience mismatch".into());
381 }
382
383 Ok(payload)
384 }
385}
386
387#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
388pub struct QappSessionTokenV2 {
389 pub qapp_did: String,
390 pub expiry_epoch: u64,
391 pub audience: String,
392 pub nonce: String,
393 pub capabilities: Vec<String>,
394 pub sensitivity_clearance: SubgraphLayer,
395}
396
397#[derive(
405 serde::Serialize, serde::Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord,
406)]
407#[repr(u8)]
408pub enum SubgraphLayer {
409 Public = 0,
410 Professional = 1,
411 Legal = 2,
412 Medical = 3,
413 Fiduciary = 4,
414}
415
416impl SubgraphLayer {
417 pub fn label(self) -> &'static str {
419 match self {
420 Self::Public => "qualia:subgraph:layer:public",
421 Self::Professional => "qualia:subgraph:layer:professional",
422 Self::Legal => "qualia:subgraph:layer:legal",
423 Self::Medical => "qualia:subgraph:layer:medical",
424 Self::Fiduciary => "qualia:subgraph:layer:fiduciary",
425 }
426 }
427
428 pub fn sensitivity_tier(self) -> u8 {
430 self as u8
431 }
432}
433
434pub struct SubgraphKey {
436 layer: SubgraphLayer,
437 key_bytes: [u8; 32],
438}
439
440impl std::fmt::Debug for SubgraphKey {
441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
442 f.debug_struct("SubgraphKey")
443 .field("layer", &self.layer)
444 .field("key_bytes", &"[REDACTED]")
445 .finish()
446 }
447}
448
449impl zeroize::Zeroize for SubgraphKey {
450 fn zeroize(&mut self) {
451 self.key_bytes.zeroize();
452 }
453}
454
455impl Drop for SubgraphKey {
456 fn drop(&mut self) {
457 use zeroize::Zeroize;
458 self.key_bytes.zeroize();
459 }
460}
461
462impl SubgraphKey {
463 #[inline]
465 pub fn raw(&self) -> &[u8; 32] {
466 &self.key_bytes
467 }
468
469 pub fn layer(&self) -> SubgraphLayer {
470 self.layer
471 }
472}
473
474#[derive(Debug, Clone)]
482pub struct EncapsulatedKey {
483 pub layer: SubgraphLayer,
484 pub ephemeral_public: [u8; 32],
485 pub ciphertext: [u8; 48],
487 pub nonce: [u8; 12],
489}
490
491impl KeyVault {
492 pub fn generate_layer_key(&self, layer: SubgraphLayer) -> SubgraphKey {
498 use hkdf::Hkdf;
499 use sha2::Sha256;
500
501 let master_key = self.master_key.as_ref().expect("Vault is locked");
502 let ikm = master_key.to_bytes();
503 let salt: &[u8] = b"qualia:subgraph:salt:v1";
504 let hk = Hkdf::<Sha256>::new(Some(salt), &ikm);
505
506 let mut key_bytes = [0u8; 32];
507 hk.expand(layer.label().as_bytes(), &mut key_bytes)
508 .expect("HKDF expand: 32 bytes always fits");
509
510 SubgraphKey { layer, key_bytes }
511 }
512
513 pub fn encapsulate_for_recipient(
521 &self,
522 layer_key: &SubgraphKey,
523 recipient_x25519_pub: &[u8; 32],
524 nonce_entropy: &[u8; 32],
525 ) -> Result<EncapsulatedKey, String> {
526 use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit};
527 use x25519_dalek::PublicKey;
528
529 let ephemeral_scalar = {
531 let mut h = Sha256::new();
532 h.update(b"qualia:ecdh:ephemeral:");
533 h.update(nonce_entropy);
534 h.update(layer_key.layer().label().as_bytes());
535 let digest = h.finalize();
536 let mut scalar = [0u8; 32];
537 scalar.copy_from_slice(&digest);
538 scalar
539 };
540
541 let ephemeral_secret = x25519_dalek::StaticSecret::from(ephemeral_scalar);
543 let ephemeral_public = PublicKey::from(&ephemeral_secret);
544
545 let recipient_pub = PublicKey::from(*recipient_x25519_pub);
547 let shared_secret = ephemeral_secret.diffie_hellman(&recipient_pub);
548
549 let wrap_key_bytes = {
551 let mut h = Sha256::new();
552 h.update(shared_secret.as_bytes());
553 h.update(b":qualia:wrap:");
554 h.update(layer_key.layer().label().as_bytes());
555 h.finalize()
556 };
557
558 let nonce_bytes: [u8; 12] = {
560 let mut h = Sha256::new();
561 h.update(b"qualia:nonce:");
562 h.update(nonce_entropy);
563 let d = h.finalize();
564 let mut n = [0u8; 12];
565 n.copy_from_slice(&d[..12]);
566 n
567 };
568
569 let cipher =
570 Aes256Gcm::new_from_slice(&wrap_key_bytes).map_err(|_| "AES-GCM key init failed")?;
571 let aes_nonce = aes_gcm::Nonce::try_from(nonce_bytes.as_slice()).unwrap();
572
573 let ct_vec = cipher
574 .encrypt(&aes_nonce, layer_key.raw().as_ref())
575 .map_err(|_| "AES-GCM encryption failed")?;
576
577 let mut ciphertext = [0u8; 48];
578 if ct_vec.len() != 48 {
579 return Err(format!("unexpected ciphertext length {}", ct_vec.len()));
580 }
581 ciphertext.copy_from_slice(&ct_vec);
582
583 Ok(EncapsulatedKey {
584 layer: layer_key.layer(),
585 ephemeral_public: *ephemeral_public.as_bytes(),
586 ciphertext,
587 nonce: nonce_bytes,
588 })
589 }
590
591 pub fn decapsulate(
595 &self,
596 encapsulated: &EncapsulatedKey,
597 recipient_x25519_secret: &[u8; 32],
598 ) -> Result<SubgraphKey, String> {
599 use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit};
600 use x25519_dalek::{PublicKey, StaticSecret};
601
602 let secret = StaticSecret::from(*recipient_x25519_secret);
603 let ephemeral_pub = PublicKey::from(encapsulated.ephemeral_public);
604 let shared_secret = secret.diffie_hellman(&ephemeral_pub);
605
606 let wrap_key_bytes = {
607 let mut h = Sha256::new();
608 h.update(shared_secret.as_bytes());
609 h.update(b":qualia:wrap:");
610 h.update(encapsulated.layer.label().as_bytes());
611 h.finalize()
612 };
613
614 let cipher =
615 Aes256Gcm::new_from_slice(&wrap_key_bytes).map_err(|_| "AES-GCM key init failed")?;
616 let nonce = aes_gcm::Nonce::try_from(encapsulated.nonce.as_slice()).unwrap();
617
618 let plaintext = cipher
619 .decrypt(&nonce, encapsulated.ciphertext.as_ref())
620 .map_err(|_| "AES-GCM decryption failed — wrong key or tampered ciphertext")?;
621
622 if plaintext.len() != 32 {
623 return Err(format!("unexpected plaintext length {}", plaintext.len()));
624 }
625 let mut key_bytes = [0u8; 32];
626 key_bytes.copy_from_slice(&plaintext);
627
628 Ok(SubgraphKey {
629 layer: encapsulated.layer,
630 key_bytes,
631 })
632 }
633
634 pub fn derive_x25519_secret(&self) -> [u8; 32] {
638 let master_key = self.master_key.as_ref().expect("Vault is locked");
639 let mut h = Sha256::new();
640 h.update(master_key.to_bytes());
641 h.update(b"qualia:x25519:static");
642 h.finalize().into()
643 }
644}
645
646#[cfg(test)]
647mod subgraph_key_tests {
648 use super::*;
649
650 fn test_vault() -> KeyVault {
651 let tmp = tempfile::tempdir().expect("tmpdir");
652 KeyVault::load_or_generate(tmp.path().to_str().unwrap()).expect("vault")
653 }
654
655 #[test]
656 fn layer_key_derivation_is_deterministic() {
657 let vault = test_vault();
658 let k1 = vault.generate_layer_key(SubgraphLayer::Medical);
659 let k2 = vault.generate_layer_key(SubgraphLayer::Medical);
660 assert_eq!(k1.raw(), k2.raw());
661 }
662
663 #[test]
664 fn different_layers_produce_different_keys() {
665 let vault = test_vault();
666 let med = vault.generate_layer_key(SubgraphLayer::Medical);
667 let leg = vault.generate_layer_key(SubgraphLayer::Legal);
668 let fid = vault.generate_layer_key(SubgraphLayer::Fiduciary);
669 assert_ne!(med.raw(), leg.raw());
670 assert_ne!(leg.raw(), fid.raw());
671 assert_ne!(med.raw(), fid.raw());
672 }
673
674 #[test]
675 fn encapsulate_decapsulate_roundtrip() {
676 let vault = test_vault();
677 let layer_key = vault.generate_layer_key(SubgraphLayer::Fiduciary);
678
679 let recipient_secret = vault.derive_x25519_secret();
681 let recipient_pub = {
682 use x25519_dalek::{PublicKey, StaticSecret};
683 let s = StaticSecret::from(recipient_secret);
684 *PublicKey::from(&s).as_bytes()
685 };
686
687 let nonce_entropy = [0x42u8; 32];
688 let encapsulated = vault
689 .encapsulate_for_recipient(&layer_key, &recipient_pub, &nonce_entropy)
690 .expect("encapsulate");
691
692 let recovered = vault
693 .decapsulate(&encapsulated, &recipient_secret)
694 .expect("decapsulate");
695
696 assert_eq!(recovered.raw(), layer_key.raw());
697 assert_eq!(recovered.layer(), SubgraphLayer::Fiduciary);
698 }
699
700 #[test]
701 fn lock_and_unlock_roundtrip() {
702 let tmp = tempfile::tempdir().expect("tmpdir");
703 let path = tmp.path().to_str().unwrap();
704 let mut vault = KeyVault::load_or_generate(path).expect("vault");
705 assert!(!vault.is_locked());
706
707 let before = vault.get_master_key_bytes();
708 vault.lock();
709 assert!(vault.is_locked());
710
711 vault.unlock().expect("unlock");
712 assert!(!vault.is_locked());
713 assert_eq!(vault.get_master_key_bytes(), before);
714 }
715
716 #[test]
717 fn decapsulate_wrong_key_fails() {
718 let vault = test_vault();
719 let layer_key = vault.generate_layer_key(SubgraphLayer::Legal);
720
721 let recipient_secret = vault.derive_x25519_secret();
722 let recipient_pub = {
723 use x25519_dalek::{PublicKey, StaticSecret};
724 let s = StaticSecret::from(recipient_secret);
725 *PublicKey::from(&s).as_bytes()
726 };
727
728 let nonce_entropy = [0x99u8; 32];
729 let encapsulated = vault
730 .encapsulate_for_recipient(&layer_key, &recipient_pub, &nonce_entropy)
731 .expect("encapsulate");
732
733 let wrong_secret = [0xFFu8; 32];
734 let result = vault.decapsulate(&encapsulated, &wrong_secret);
735 assert!(result.is_err());
736 }
737}