⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/modules/semantic_dht/minga-core/src/identity.rs
Historia
Quién tocó qué
crudo
chore: monorepo inicial con arje + minga + yahweh absorbidos
1//! Identidad self-sovereign basada en Ed25519.
2//!
3//! Cada peer (y cada autor humano o agente IA) se identifica por un
4//! `Did` — el bytestring de su clave pública Ed25519. La clave privada
5//! vive en su `Keypair` y nunca sale del nodo. Firmar un mensaje con la
6//! `Keypair` produce una `Signature` que cualquiera con el `Did` puede
7//! verificar — la atribución es irrefutable bajo el modelo
8//! criptográfico estándar (asumiendo que la clave privada no fugó).
9//!
10//! El esquema es deliberadamente minimalista: no hay rotación de
11//! claves, ni revocación, ni metadatos en el DID. Esas capas (DID
12//! Documents, métodos `did:web`/`did:ion`, claves de firma versus de
13//! cifrado, etc.) se construyen encima cuando la complejidad del
14//! producto lo justifique. Por ahora, el `Did` ES la clave pública.
15
16use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
17use argon2::Argon2;
18use ed25519_dalek::{
19 Signature as Ed25519Sig, Signer, SigningKey, Verifier, VerifyingKey, SECRET_KEY_LENGTH,
20 SIGNATURE_LENGTH,
21};
22use rand::rngs::OsRng;
23use rand::RngCore;
24
25/// Cabecera del formato de keypair cifrado en disco.
26const KEYPAIR_MAGIC: &[u8; 8] = b"MINGAKEY";
27const KEYPAIR_VERSION: u8 = 1;
28const ARGON2_SALT_LEN: usize = 16;
29const AES_NONCE_LEN: usize = 12;
30const KEYPAIR_HEADER_LEN: usize = 8 + 1 + ARGON2_SALT_LEN + AES_NONCE_LEN;
31
32#[derive(Debug, thiserror::Error)]
33pub enum KeypairCryptoError {
34 #[error("formato inválido: faltan magic / versión / longitud")]
35 InvalidFormat,
36
37 #[error("passphrase incorrecta o cifrado manipulado")]
38 DecryptFailed,
39
40 #[error("argon2: {0}")]
41 Argon2(String),
42}
43
44/// Decentralized Identifier: 32 bytes de la clave pública Ed25519.
45#[derive(
46 Debug,
47 Clone,
48 Copy,
49 PartialEq,
50 Eq,
51 Hash,
52 PartialOrd,
53 Ord,
54 serde::Serialize,
55 serde::Deserialize,
56)]
57#[serde(transparent)]
58pub struct Did(pub [u8; SECRET_KEY_LENGTH]);
59
60impl Did {
61 pub fn as_bytes(&self) -> &[u8; SECRET_KEY_LENGTH] {
62 &self.0
63 }
64
65 /// Verifica que `sig` sea una firma válida sobre `msg` producida
66 /// con la llave privada correspondiente a este DID. Devuelve
67 /// `false` ante cualquier irregularidad: bytes de DID que no son
68 /// un punto válido en la curva, firma malformada, mensaje que no
69 /// coincide.
70 pub fn verify(&self, msg: &[u8], sig: &Signature) -> bool {
71 let Ok(vk) = VerifyingKey::from_bytes(&self.0) else {
72 return false;
73 };
74 let ed_sig = Ed25519Sig::from_bytes(&sig.0);
75 vk.verify(msg, &ed_sig).is_ok()
76 }
77}
78
79impl std::fmt::Display for Did {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 write!(f, "did:key:")?;
82 for b in &self.0 {
83 write!(f, "{:02x}", b)?;
84 }
85 Ok(())
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
90#[serde(transparent)]
91pub struct Signature(
92 #[serde(with = "serde_big_array::BigArray")] pub [u8; SIGNATURE_LENGTH],
93);
94
95impl Signature {
96 pub fn as_bytes(&self) -> &[u8; SIGNATURE_LENGTH] {
97 &self.0
98 }
99}
100
101/// Llave criptográfica completa: priva (para firmar) + pública (para
102/// que otros verifiquen). Por convención llamamos `Did` al lado público
103/// expuesto al mundo, pero el `Keypair` mantiene ambos lados juntos.
104#[derive(Clone)]
105pub struct Keypair {
106 signing: SigningKey,
107}
108
109impl Keypair {
110 /// Genera un nuevo `Keypair` usando aleatoriedad del sistema
111 /// operativo (`/dev/urandom` en Unix, `BCryptGenRandom` en
112 /// Windows). Para producción.
113 pub fn generate() -> Self {
114 let mut seed = [0u8; SECRET_KEY_LENGTH];
115 OsRng.fill_bytes(&mut seed);
116 Self::from_seed(&seed)
117 }
118
119 /// Reconstruye un `Keypair` desde una semilla de 32 bytes. Misma
120 /// semilla → mismo `Keypair` (mismo `Did`, mismas firmas). Útil
121 /// para tests reproducibles y para escenarios donde la semilla
122 /// proviene de otra fuente determinista (HKDF, BIP39, etc.).
123 pub fn from_seed(seed: &[u8; SECRET_KEY_LENGTH]) -> Self {
124 Self {
125 signing: SigningKey::from_bytes(seed),
126 }
127 }
128
129 pub fn did(&self) -> Did {
130 Did(self.signing.verifying_key().to_bytes())
131 }
132
133 pub fn sign(&self, msg: &[u8]) -> Signature {
134 Signature(self.signing.sign(msg).to_bytes())
135 }
136
137 /// Cifra la parte privada del keypair con una passphrase humana.
138 /// Esquema:
139 ///
140 /// 1. Genera un salt aleatorio de 16 bytes y un nonce de 12 bytes.
141 /// 2. Deriva una clave AES-256 desde la passphrase vía Argon2id
142 /// (parámetros por defecto OWASP).
143 /// 3. Cifra los 32 bytes de la clave secreta con AES-256-GCM
144 /// (autenticado: integrity built-in).
145 /// 4. Compone el blob:
146 /// `MAGIC(8) || VERSION(1) || SALT(16) || NONCE(12) || CIPHERTEXT+TAG(48)`.
147 ///
148 /// Total: 85 bytes. La passphrase nunca se almacena; quien no la
149 /// conozca no puede recuperar la identidad.
150 pub fn encrypt(&self, passphrase: &str) -> Result<Vec<u8>, KeypairCryptoError> {
151 let mut salt = [0u8; ARGON2_SALT_LEN];
152 let mut nonce_bytes = [0u8; AES_NONCE_LEN];
153 OsRng.fill_bytes(&mut salt);
154 OsRng.fill_bytes(&mut nonce_bytes);
155
156 let aes_key = derive_aes_key(passphrase, &salt)?;
157
158 let cipher = Aes256Gcm::new_from_slice(&aes_key)
159 .map_err(|_| KeypairCryptoError::DecryptFailed)?;
160 let nonce = Nonce::from_slice(&nonce_bytes);
161 let secret_bytes = self.signing.to_bytes();
162 let ciphertext = cipher
163 .encrypt(nonce, secret_bytes.as_ref())
164 .map_err(|_| KeypairCryptoError::DecryptFailed)?;
165
166 let mut out = Vec::with_capacity(KEYPAIR_HEADER_LEN + ciphertext.len());
167 out.extend_from_slice(KEYPAIR_MAGIC);
168 out.push(KEYPAIR_VERSION);
169 out.extend_from_slice(&salt);
170 out.extend_from_slice(&nonce_bytes);
171 out.extend_from_slice(&ciphertext);
172 Ok(out)
173 }
174
175 /// Descifra un keypair cifrado con `encrypt`. Falla con
176 /// `DecryptFailed` si la passphrase es incorrecta **o** si los
177 /// bytes han sido manipulados (AES-GCM detecta ambas vías).
178 pub fn decrypt(bytes: &[u8], passphrase: &str) -> Result<Self, KeypairCryptoError> {
179 if bytes.len() < KEYPAIR_HEADER_LEN {
180 return Err(KeypairCryptoError::InvalidFormat);
181 }
182 if &bytes[..8] != KEYPAIR_MAGIC {
183 return Err(KeypairCryptoError::InvalidFormat);
184 }
185 if bytes[8] != KEYPAIR_VERSION {
186 return Err(KeypairCryptoError::InvalidFormat);
187 }
188
189 let salt = &bytes[9..9 + ARGON2_SALT_LEN];
190 let nonce_bytes = &bytes[9 + ARGON2_SALT_LEN..KEYPAIR_HEADER_LEN];
191 let ciphertext = &bytes[KEYPAIR_HEADER_LEN..];
192
193 let aes_key = derive_aes_key(passphrase, salt)?;
194 let cipher = Aes256Gcm::new_from_slice(&aes_key)
195 .map_err(|_| KeypairCryptoError::DecryptFailed)?;
196 let nonce = Nonce::from_slice(nonce_bytes);
197 let plaintext = cipher
198 .decrypt(nonce, ciphertext)
199 .map_err(|_| KeypairCryptoError::DecryptFailed)?;
200
201 if plaintext.len() != SECRET_KEY_LENGTH {
202 return Err(KeypairCryptoError::InvalidFormat);
203 }
204 let mut seed = [0u8; SECRET_KEY_LENGTH];
205 seed.copy_from_slice(&plaintext);
206 Ok(Self::from_seed(&seed))
207 }
208}
209
210fn derive_aes_key(passphrase: &str, salt: &[u8]) -> Result<[u8; 32], KeypairCryptoError> {
211 let mut key = [0u8; 32];
212 Argon2::default()
213 .hash_password_into(passphrase.as_bytes(), salt, &mut key)
214 .map_err(|e| KeypairCryptoError::Argon2(e.to_string()))?;
215 Ok(key)
216}
217
218impl std::fmt::Debug for Keypair {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 // Nunca exponemos la parte privada en debug. Solo el DID.
221 write!(f, "Keypair {{ did: {} }}", self.did())
222 }
223}