qualia_client_core/
node_identity.rs1use ed25519_dalek::{SigningKey, VerifyingKey};
15
16#[cfg(not(target_arch = "wasm32"))]
17use qualia_core_db::p2p::wireguard_userspace::WgKeypair;
18
19#[derive(serde::Serialize, serde::Deserialize, Clone)]
25pub struct NodeIdentity {
26 pub ed25519_secret: [u8; 32],
28 pub wg_secret: [u8; 32],
30}
31
32impl NodeIdentity {
33 pub fn signing_key(&self) -> SigningKey {
35 SigningKey::from_bytes(&self.ed25519_secret)
36 }
37
38 pub fn identity_pubkey_hex(&self) -> String {
43 hex::encode(VerifyingKey::from(&self.signing_key()).to_bytes())
44 }
45
46 #[cfg(not(target_arch = "wasm32"))]
48 pub fn wireguard_pubkey_hex(&self) -> String {
49 WgKeypair::from_secret_bytes(self.wg_secret).public_hex()
50 }
51
52 #[cfg(not(target_arch = "wasm32"))]
57 pub fn overlay_addr(&self) -> String {
58 crate::connection_identifier::derive_overlay_addr(
59 &WgKeypair::from_secret_bytes(self.wg_secret).public_bytes(),
60 )
61 }
62
63 pub fn load_or_create() -> Result<NodeIdentity, String> {
73 let path = crate::state::app_meta_dir().join("node_identity.json");
74
75 if path.exists() {
76 let bytes = std::fs::read(&path)
77 .map_err(|e| format!("failed to read {}: {e}", path.display()))?;
78 if let Ok(identity) = serde_json::from_slice::<NodeIdentity>(&bytes) {
79 return Ok(identity);
80 }
81 }
83
84 let mut e = [0u8; 32];
85 rand::fill(&mut e);
86 let mut w = [0u8; 32];
87 rand::fill(&mut w);
88 let identity = NodeIdentity {
89 ed25519_secret: e,
90 wg_secret: w,
91 };
92
93 if let Some(parent) = path.parent() {
94 std::fs::create_dir_all(parent)
95 .map_err(|err| format!("failed to create {}: {err}", parent.display()))?;
96 }
97 let json = serde_json::to_string_pretty(&identity)
98 .map_err(|err| format!("failed to serialize node identity: {err}"))?;
99 std::fs::write(&path, json)
100 .map_err(|err| format!("failed to write {}: {err}", path.display()))?;
101
102 Ok(identity)
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use ed25519_dalek::{Signer, Verifier};
110
111 fn sample() -> NodeIdentity {
112 NodeIdentity {
113 ed25519_secret: [7u8; 32],
114 wg_secret: [9u8; 32],
115 }
116 }
117
118 #[test]
119 fn identity_pubkey_hex_is_64_chars_and_deterministic() {
120 let id = sample();
121 let a = id.identity_pubkey_hex();
122 let b = id.identity_pubkey_hex();
123 assert_eq!(a.len(), 64, "identity pubkey hex must be 64 chars");
124 assert!(
125 a.chars().all(|c| c.is_ascii_hexdigit()),
126 "identity pubkey hex must be all hex digits: {a}"
127 );
128 assert_eq!(a, b, "identity pubkey hex must be deterministic");
129 }
130
131 #[test]
132 fn signing_key_produces_verifiable_signatures() {
133 let id = sample();
134 let sk = id.signing_key();
135 let msg = b"node challenge response";
136 let sig = sk.sign(msg);
137 let vk = VerifyingKey::from(&sk);
138 assert!(
139 vk.verify(msg, &sig).is_ok(),
140 "signature must verify under the derived verifying key"
141 );
142 }
143
144 #[cfg(not(target_arch = "wasm32"))]
145 #[test]
146 fn wireguard_pubkey_hex_is_64_chars() {
147 let id = sample();
148 let wg = id.wireguard_pubkey_hex();
149 assert_eq!(wg.len(), 64, "wireguard pubkey hex must be 64 chars");
150 assert!(
151 wg.chars().all(|c| c.is_ascii_hexdigit()),
152 "wireguard pubkey hex must be all hex digits: {wg}"
153 );
154 }
155
156 #[cfg(not(target_arch = "wasm32"))]
157 #[test]
158 fn overlay_addr_is_ula() {
159 let id = sample();
160 let addr = id.overlay_addr();
161 assert!(
162 addr.starts_with("fd"),
163 "overlay address must be an fd… ULA: {addr}"
164 );
165 }
166}