feat(identity): RV14 — una identidad sobrevive a perder su llave, y la revision encontro que el storage salia gratis
QUE NACE. rotate_agent_key (call_index 1 de agent-identity) y la runtime API AgentKeyApi::current_key. Hasta hoy un DID quedaba atado de hecho a su llave de nacimiento: no se re-derivaba, pero tampoco habia forma de pasar la autoridad a otra llave. Perder o filtrar la llave obligaba a registrarse otra vez con OTRO DID y perder credenciales, creditos e historial. Ahora el DID es de por vida y lo que rota es que llave manda sobre el. Es la dependencia de dos hilos abiertos: el DID del niño (Marihan) y los creditos de referido indexados por DID (Codex). Diseño: CurrentKey[did] (ausente = manda la de nacimiento, que ya vivia en creator; sin migracion), KeyToDid[llave] (una llave manda sobre un solo DID en toda la vida de la cadena; las retiradas quedan quemadas) y RotationNonce[did]. Dos firmas sobre DOMAIN || genesis || did || vigente || nueva || nonce || who: la vigente autoriza, la nueva prueba control. LO QUE LA REVISION ADVERSARIAL ENCONTRO ANTES DE PUBLICAR. Cinco lentes, 18 hallazgos, 17 confirmados. Todos arreglados aqui y cada arreglo con su contrafactual visto en ROJO: 1. DbWeight = () EN TODO EL RUNTIME. frame_system heredaba el valor por defecto de SolochainDefaultConfig sin declararlo. Cada reads(n)/writes(n) de TODOS los weights.rs valia cero: la E/S de storage no se cobraba en fees ni llenaba el bloque. Ahora RocksDbWeight, con un test que reprueba si vuelve a cero. ESTO CORRIGE LO QUE YO DIJE EN RV13: los reads/writes que medi alli no tenian efecto en el runtime. Lo que RV13 si arreglo es el proof_size y el ref_time de ejecucion, que no pasan por DbWeight. 2. LLAVES DE ORDEN PEQUEÑO. El verificador del host (ed25519-zebra, ZIP215) las acepta. Con el punto identidad como llave y la firma trivial R=identidad s=0, la «prueba de control» se superaba sin llave privada, en rotacion Y en registro: el DID quedaba bajo una llave con la que cualquiera firma en su nombre. Para un DID de un niño en Ethos eso es suplantacion. Ahora is_strong_ed25519_key descomprime el punto, exige codificacion canonica y rechaza orden pequeño (curve25519-dalek, que ya iba dentro del WASM). 3. EL REGISTRO ESCRIBIA KeyToDid PERO NO LO CONSULTABA. Una llave a la que otro DID ya habia rotado fundaba un DID nuevo y pisaba el indice. Hallado cinco veces por separado. Va detras de la colision de DID para no cambiar el error documentado de scenario_12. 4. REPLAY ENTRE CADENAS: el mensaje no llevaba genesis. Ahora si, mismo patron que provenance. Y menores: el evento nuevo va al final del enum (insertarlo en medio corria el indice SCALE de AgentUpdated); la insercion redundante de la llave vieja ya no pisa entradas; la doc dice que perder la CUENTA tambien bloquea la rotacion, no solo perder la llave; el comentario de creator decia «la cuenta que registro» y el codigo guarda la llave de nacimiento. DOS TESTS MIOS PASABAN POR LA RAZON EQUIVOCADA, y lo digo. El de «llave retirada quemada» volvia a la de nacimiento, que atrapa otra clausula. El del ladron firmaba con la llave vigente nueva, algo que un ladron real no tiene; con el pallet roto seguia verde en su mitad de ataque. Reescritos, y ahora el rojo sale de la mitad correcta (verificado por linea del panic). LO QUE QUEDA ABIERTO Y NO ES DE ESTE COMMIT: tras rotar, derive(llave_vigente) ya no es el DID. OCF y Palace hoy DERIVAN el identificador; para un usuario que roto tienen que RESOLVERLO contra KeyToDid. Es una enmienda al DC de unificacion con sus firmantes. La recuperacion por guardianes va aparte. Y KeyToDid crece sin deposito; se reevalua ahora que las escrituras cuestan de verdad. Medido sobre el WASM real (bencher 0.15.0, Mac, no canonico): rotate 97 us, 11 lecturas, 5 escrituras, proof 6100. 318 tests verde. RV13 -> RV14. TRANSACTION_VERSION intacto: call nueva en indice nuevo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7f68c5b656
commit
defd3fa580
12 files changed
+1146
-20
No files matched your search
@@ -0,0 +1,533 @@
|
||||
//! # Rotación de llave — la identidad sobrevive a perder o filtrar su llave
|
||||
//!
|
||||
//! Estos tests fijan tres propiedades, en orden de gravedad:
|
||||
//!
|
||||
//! 1. **Tras rotar, la llave vieja pierde la autoridad.** Es la razón de existir de
|
||||
//! la rotación: se rota porque la llave se filtró, y si la vieja siguiera
|
||||
//! mandando, rotar no protegería de nada.
|
||||
//! 2. **No se puede ceder la autoridad a una llave ajena.** Hace falta la firma de
|
||||
//! la llave nueva; sin ella un titular podría poner como autoridad de su DID la
|
||||
//! llave pública de un tercero.
|
||||
//! 3. **El DID no cambia.** Es el identificador de por vida; lo que cambia es qué
|
||||
//! llave manda sobre él.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
use crate::mock::{
|
||||
alice_account, alice_origin, alice_pair, alice_pubkey, bob_account, bob_origin, bob_pair,
|
||||
bob_pubkey, dave_pair, eve_pair, new_test_ext, sign_registration, sign_rotation,
|
||||
sign_rotation_on, AgentIdentity as Pallet, RuntimeEvent, System, Test, GENESIS,
|
||||
};
|
||||
use crate::{
|
||||
derive_did, CurrentKey, DidToAccount, Error, Event, Identities, KeyToDid, RotationNonce,
|
||||
};
|
||||
use frame_support::{assert_noop, assert_ok};
|
||||
use origin_chain_types::AgentKind;
|
||||
use sp_core::Pair;
|
||||
|
||||
/// Registra a ALICE con su llave de nacimiento y devuelve su DID.
|
||||
fn registrar_alice() -> [u8; 32] {
|
||||
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||
assert_ok!(Pallet::register_agent_did(
|
||||
alice_origin(),
|
||||
alice_pubkey(),
|
||||
AgentKind::Human,
|
||||
alloc::vec::Vec::new(),
|
||||
sig,
|
||||
));
|
||||
*derive_did(&alice_pubkey()).as_bytes()
|
||||
}
|
||||
|
||||
/// Rota la autoridad del DID de ALICE desde `actual` hacia `nueva`, firmando bien.
|
||||
fn rotar_alice(actual: &sp_core::ed25519::Pair, nueva: &sp_core::ed25519::Pair) {
|
||||
let did = *derive_did(&alice_pubkey()).as_bytes();
|
||||
let nonce = RotationNonce::<Test>::get(derive_did(&alice_pubkey()));
|
||||
let (k_act, k_new) = (actual.public().0, nueva.public().0);
|
||||
let s_act = sign_rotation(actual, &did, &k_act, &k_new, nonce, &alice_account());
|
||||
let s_new = sign_rotation(nueva, &did, &k_act, &k_new, nonce, &alice_account());
|
||||
assert_ok!(Pallet::rotate_agent_key(
|
||||
alice_origin(),
|
||||
k_new,
|
||||
s_act,
|
||||
s_new
|
||||
));
|
||||
}
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[test]
|
||||
fn sin_rotacion_la_llave_vigente_es_la_de_nacimiento() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
let did = derive_did(&alice_pubkey());
|
||||
assert_eq!(Pallet::current_key_of(&did), Some(alice_pubkey()));
|
||||
// Fija lo que `creator` guarda de verdad: la llave de nacimiento, no la cuenta.
|
||||
let id = Identities::<Test>::get(alice_account()).expect("registrada");
|
||||
assert_eq!(id.creator.0, alice_pubkey());
|
||||
// Y ningún CurrentKey escrito: el resolutor cae a `creator` sin migración.
|
||||
assert!(CurrentKey::<Test>::get(did).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el_registro_quema_la_llave_de_nacimiento() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
assert_eq!(
|
||||
KeyToDid::<Test>::get(alice_pubkey()),
|
||||
Some(derive_did(&alice_pubkey()))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotar_conserva_el_did_y_cambia_la_autoridad() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
let did = derive_did(&alice_pubkey());
|
||||
rotar_alice(&alice_pair(), &dave_pair());
|
||||
|
||||
// El DID es el mismo en los dos índices.
|
||||
assert_eq!(Identities::<Test>::get(alice_account()).unwrap().did, did);
|
||||
assert_eq!(DidToAccount::<Test>::get(did), Some(alice_account()));
|
||||
// La autoridad es la nueva.
|
||||
assert_eq!(Pallet::current_key_of(&did), Some(dave_pair().public().0));
|
||||
|
||||
System::assert_last_event(RuntimeEvent::AgentIdentity(Event::KeyRotated {
|
||||
account: alice_account(),
|
||||
did,
|
||||
old_key: alice_pubkey(),
|
||||
new_key: dave_pair().public().0,
|
||||
nonce: 0,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/// LA PROPIEDAD QUE JUSTIFICA LA ROTACIÓN. Si la llave vieja conservara la autoridad,
|
||||
/// rotar tras una filtración no protegería de nada: el ladrón seguiría mandando.
|
||||
#[test]
|
||||
fn tras_rotar_la_llave_vieja_pierde_la_autoridad() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
rotar_alice(&alice_pair(), &dave_pair());
|
||||
|
||||
let did = *derive_did(&alice_pubkey()).as_bytes();
|
||||
let (dave, eve) = (dave_pair().public().0, eve_pair().public().0);
|
||||
|
||||
// El ladrón solo tiene la llave VIEJA (y la cuenta). Firma lo único que puede
|
||||
// firmar: una rotación que declara como vigente a la vieja.
|
||||
//
|
||||
// ⚠️ La primera versión de este test firmaba con `current = dave`, que es algo que
|
||||
// un ladrón real no conoce como suyo, y el test pasaba por la razón equivocada. Lo
|
||||
// midió el crítico de la revisión adversarial: con el pallet roto para ignorar
|
||||
// `CurrentKey`, esta mitad seguía verde y el ataque real devolvía `Ok`. El test
|
||||
// solo se ponía rojo por su mitad POSITIVA. Ahora el ladrón firma con la vieja.
|
||||
let alice = alice_pubkey();
|
||||
let robada = sign_rotation(&alice_pair(), &did, &alice, &eve, 1, &alice_account());
|
||||
let de_eve = sign_rotation(&eve_pair(), &did, &alice, &eve, 1, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), eve, robada, de_eve),
|
||||
Error::<Test>::InvalidCurrentKeySignature
|
||||
);
|
||||
|
||||
// Y la llave vigente sí puede.
|
||||
rotar_alice(&dave_pair(), &eve_pair());
|
||||
assert_eq!(
|
||||
Pallet::current_key_of(&derive_did(&alice_pubkey())),
|
||||
Some(eve)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sin_firma_de_la_llave_vigente_no_se_rota() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let (a, d) = (alice_pubkey(), dave_pair().public().0);
|
||||
// La "firma de la vigente" la produce otra llave.
|
||||
let falsa = sign_rotation(&eve_pair(), &did, &a, &d, 0, &alice_account());
|
||||
let nueva = sign_rotation(&dave_pair(), &did, &a, &d, 0, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), d, falsa, nueva),
|
||||
Error::<Test>::InvalidCurrentKeySignature
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Sin la firma de la llave nueva, un titular podría poner como autoridad de su DID
|
||||
/// la llave pública de un tercero — que figuraría como dueña de una identidad ajena.
|
||||
#[test]
|
||||
fn no_se_puede_ceder_la_autoridad_a_una_llave_ajena() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let (a, e) = (alice_pubkey(), eve_pair().public().0);
|
||||
let vigente = sign_rotation(&alice_pair(), &did, &a, &e, 0, &alice_account());
|
||||
// Alice "firma" también por Eve, cuya llave privada no tiene.
|
||||
let suplantada = sign_rotation(&alice_pair(), &did, &a, &e, 0, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), e, vigente, suplantada),
|
||||
Error::<Test>::InvalidNewKeySignature
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn la_misma_rotacion_no_puede_reenviarse() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let (a, d) = (alice_pubkey(), dave_pair().public().0);
|
||||
let s_a = sign_rotation(&alice_pair(), &did, &a, &d, 0, &alice_account());
|
||||
let s_d = sign_rotation(&dave_pair(), &did, &a, &d, 0, &alice_account());
|
||||
assert_ok!(Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d));
|
||||
// Reenviada tal cual: ahora rotar a `d` es rotar a la vigente.
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d),
|
||||
Error::<Test>::SameKey
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Una llave retirada queda quemada para siempre.
|
||||
///
|
||||
/// ⚠️ La primera versión de este test volvía a la llave de NACIMIENTO, y pasaba por
|
||||
/// la razón equivocada: esa la atrapa la cláusula de `derive_did`, no el índice
|
||||
/// `KeyToDid`. Lo destapó el contrafactual —se borró el chequeo de `KeyToDid` y el
|
||||
/// test siguió verde—. Por eso aquí se rota a una llave retirada que NO es de
|
||||
/// nacimiento de ningún DID (dave): solo el índice puede rechazarla.
|
||||
#[test]
|
||||
fn una_llave_retirada_queda_quemada() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
rotar_alice(&alice_pair(), &dave_pair()); // dave manda
|
||||
rotar_alice(&dave_pair(), &eve_pair()); // dave queda retirada, eve manda
|
||||
|
||||
let (e, d) = (eve_pair().public().0, dave_pair().public().0);
|
||||
// dave no es llave de nacimiento de nadie: si se rechaza, es por el índice.
|
||||
assert!(!DidToAccount::<Test>::contains_key(derive_did(&d)));
|
||||
let s_e = sign_rotation(&eve_pair(), &did, &e, &d, 2, &alice_account());
|
||||
let s_d = sign_rotation(&dave_pair(), &did, &e, &d, 2, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), d, s_e, s_d),
|
||||
Error::<Test>::KeyAlreadyUsed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// La llave de nacimiento propia también está quemada una vez retirada. La atrapa
|
||||
/// una cláusula distinta (`derive_did`), así que se prueba aparte.
|
||||
#[test]
|
||||
fn no_se_vuelve_a_la_llave_de_nacimiento_propia() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
rotar_alice(&alice_pair(), &dave_pair());
|
||||
let (d, a) = (dave_pair().public().0, alice_pubkey());
|
||||
let s_d = sign_rotation(&dave_pair(), &did, &d, &a, 1, &alice_account());
|
||||
let s_a = sign_rotation(&alice_pair(), &did, &d, &a, 1, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), a, s_d, s_a),
|
||||
Error::<Test>::KeyAlreadyUsed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_se_rota_a_la_llave_de_nacimiento_de_otro_did() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let sig_bob = sign_registration(&bob_pair(), &bob_account());
|
||||
assert_ok!(Pallet::register_agent_did(
|
||||
bob_origin(),
|
||||
bob_pubkey(),
|
||||
AgentKind::Human,
|
||||
alloc::vec::Vec::new(),
|
||||
sig_bob
|
||||
));
|
||||
let (a, b) = (alice_pubkey(), bob_pubkey());
|
||||
let s_a = sign_rotation(&alice_pair(), &did, &a, &b, 0, &alice_account());
|
||||
let s_b = sign_rotation(&bob_pair(), &did, &a, &b, 0, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), b, s_a, s_b),
|
||||
Error::<Test>::KeyAlreadyUsed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_se_rota_a_la_misma_llave() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let a = alice_pubkey();
|
||||
let s = sign_rotation(&alice_pair(), &did, &a, &a, 0, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), a, s, s),
|
||||
Error::<Test>::SameKey
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sin_identidad_no_hay_nada_que_rotar() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let (d, e) = (dave_pair().public().0, eve_pair().public().0);
|
||||
let s = sign_rotation(&dave_pair(), &[0u8; 32], &d, &e, 0, &bob_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(bob_origin(), e, s, s),
|
||||
Error::<Test>::NotRegistered
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// La firma queda atada a la cuenta que la envía. Todo lo demás es correcto —mismo
|
||||
/// DID, mismas llaves, mismo nonce— salvo `who`, y basta para que no verifique.
|
||||
#[test]
|
||||
fn la_firma_de_rotacion_esta_atada_a_la_cuenta() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let (a, d) = (alice_pubkey(), dave_pair().public().0);
|
||||
let s_a = sign_rotation(&alice_pair(), &did, &a, &d, 0, &bob_account());
|
||||
let s_d = sign_rotation(&dave_pair(), &did, &a, &d, 0, &bob_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d),
|
||||
Error::<Test>::InvalidCurrentKeySignature
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn el_nonce_avanza_con_cada_rotacion() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
let did = derive_did(&alice_pubkey());
|
||||
assert_eq!(RotationNonce::<Test>::get(did), 0);
|
||||
rotar_alice(&alice_pair(), &dave_pair());
|
||||
assert_eq!(RotationNonce::<Test>::get(did), 1);
|
||||
rotar_alice(&dave_pair(), &eve_pair());
|
||||
assert_eq!(RotationNonce::<Test>::get(did), 2);
|
||||
});
|
||||
}
|
||||
|
||||
/// Imprime y FIJA los vectores dorados que usa el benchmark de `rotate_agent_key`.
|
||||
///
|
||||
/// El benchmark corre en `no_std` y no puede firmar, así que lleva las firmas
|
||||
/// precomputadas. Este test las recalcula con el mismo builder y falla si alguien
|
||||
/// cambia el mensaje de rotación sin regenerarlas: un benchmark con firmas viejas
|
||||
/// mediría el camino de ERROR (la verificación falla pronto) y daría un peso falso.
|
||||
#[test]
|
||||
fn vectores_dorados_del_benchmark_de_rotacion() {
|
||||
use crate::benchmark_golden::{ROT_DAVE_PUBKEY, ROT_SIG_CURRENT, ROT_SIG_NEW};
|
||||
let did = *derive_did(&alice_pubkey()).as_bytes();
|
||||
let (a, d) = (alice_pubkey(), dave_pair().public().0);
|
||||
let s_a = sign_rotation(&alice_pair(), &did, &a, &d, 0, &alice_account());
|
||||
let s_d = sign_rotation(&dave_pair(), &did, &a, &d, 0, &alice_account());
|
||||
let hex = |b: &[u8]| {
|
||||
b.iter()
|
||||
.map(|x| alloc::format!("{x:02x}"))
|
||||
.collect::<alloc::string::String>()
|
||||
};
|
||||
std::println!("ROT_DAVE_PUBKEY={}", hex(&d));
|
||||
std::println!("ROT_SIG_CURRENT={}", hex(&s_a));
|
||||
std::println!("ROT_SIG_NEW={}", hex(&s_d));
|
||||
assert_eq!(hex(&d), ROT_DAVE_PUBKEY, "pubkey de dave cambió");
|
||||
assert_eq!(
|
||||
hex(&s_a),
|
||||
ROT_SIG_CURRENT,
|
||||
"el mensaje de rotación cambió: regenera la firma vigente"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&s_d),
|
||||
ROT_SIG_NEW,
|
||||
"el mensaje de rotación cambió: regenera la firma nueva"
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Defectos encontrados por la revisión adversarial de RV14 ───────────────
|
||||
|
||||
/// Firma trivial `R = identidad, s = 0`. Con una llave de orden pequeño, el verificador
|
||||
/// ZIP215 del host la da por buena para CUALQUIER mensaje.
|
||||
fn firma_trivial() -> [u8; 64] {
|
||||
let mut s = [0u8; 64];
|
||||
s[0] = 1; // R = punto identidad comprimido (01 00..00)
|
||||
s
|
||||
}
|
||||
|
||||
fn hex32(h: &str) -> [u8; 32] {
|
||||
let mut out = [0u8; 32];
|
||||
for i in 0..32 {
|
||||
out[i] = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).unwrap();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Las tres llaves débiles medidas por la sonda: identidad canónica, orden 2, e
|
||||
/// identidad con codificación NO canónica (y = p + 1).
|
||||
fn llaves_debiles() -> [[u8; 32]; 3] {
|
||||
[
|
||||
hex32("0100000000000000000000000000000000000000000000000000000000000000"),
|
||||
hex32("ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"),
|
||||
hex32("eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"),
|
||||
]
|
||||
}
|
||||
|
||||
/// CONTROL POSITIVO del verificador: sin él, un `is_strong` que devolviera siempre
|
||||
/// `false` haría verdes todos los tests de rechazo y bloquearía a todo el mundo.
|
||||
#[test]
|
||||
fn las_llaves_reales_son_fuertes_y_las_debiles_no() {
|
||||
for p in [alice_pair(), bob_pair(), dave_pair(), eve_pair()] {
|
||||
assert!(
|
||||
crate::is_strong_ed25519_key(&p.public().0),
|
||||
"llave real rechazada"
|
||||
);
|
||||
}
|
||||
for k in llaves_debiles() {
|
||||
assert!(
|
||||
!crate::is_strong_ed25519_key(&k),
|
||||
"llave débil aceptada: {k:02x?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// El ataque que hace grave el hallazgo: rotar a una llave de orden pequeño con una
|
||||
/// firma trivial en lugar de la prueba de control. Antes daba `Ok` y dejaba el DID bajo
|
||||
/// una llave con la que cualquiera firma en su nombre.
|
||||
#[test]
|
||||
fn no_se_rota_a_una_llave_debil() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let a = alice_pubkey();
|
||||
for debil in llaves_debiles() {
|
||||
let vigente = sign_rotation(&alice_pair(), &did, &a, &debil, 0, &alice_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), debil, vigente, firma_trivial()),
|
||||
Error::<Test>::WeakKey
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// El registro tenía el mismo agujero: cualquiera fundaba un DID que nadie controla.
|
||||
#[test]
|
||||
fn no_se_registra_con_una_llave_debil() {
|
||||
new_test_ext().execute_with(|| {
|
||||
for debil in llaves_debiles() {
|
||||
assert_noop!(
|
||||
Pallet::register_agent_did(
|
||||
bob_origin(),
|
||||
debil,
|
||||
AgentKind::Human,
|
||||
alloc::vec::Vec::new(),
|
||||
firma_trivial()
|
||||
),
|
||||
Error::<Test>::WeakKey
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// El hallazgo que salió CINCO veces: el registro escribía en `KeyToDid` pero no lo
|
||||
/// consultaba. Una llave retirada de un DID fundaba otro DID y pisaba el índice.
|
||||
#[test]
|
||||
fn no_se_registra_con_una_llave_retirada_de_otro_did() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
rotar_alice(&alice_pair(), &dave_pair());
|
||||
rotar_alice(&dave_pair(), &eve_pair()); // dave retirada: el caso típico de una filtrada
|
||||
let d = dave_pair().public().0;
|
||||
let sig = sign_registration(&dave_pair(), &bob_account());
|
||||
assert_noop!(
|
||||
Pallet::register_agent_did(
|
||||
bob_origin(),
|
||||
d,
|
||||
AgentKind::Human,
|
||||
alloc::vec::Vec::new(),
|
||||
sig
|
||||
),
|
||||
Error::<Test>::KeyAlreadyUsed
|
||||
);
|
||||
// Y el índice sigue apuntando al DID de alice.
|
||||
assert_eq!(KeyToDid::<Test>::get(d), Some(derive_did(&alice_pubkey())));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_se_registra_con_la_llave_vigente_de_otro_did() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
rotar_alice(&alice_pair(), &dave_pair()); // dave manda sobre el DID de alice
|
||||
let d = dave_pair().public().0;
|
||||
let sig = sign_registration(&dave_pair(), &bob_account());
|
||||
assert_noop!(
|
||||
Pallet::register_agent_did(
|
||||
bob_origin(),
|
||||
d,
|
||||
AgentKind::Human,
|
||||
alloc::vec::Vec::new(),
|
||||
sig
|
||||
),
|
||||
Error::<Test>::KeyAlreadyUsed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Hueco medido por el crítico: borrando `KeyToDid::insert(new_public_key, …)` pasaban
|
||||
/// los 38 tests. Faltaba el caso «rotar a la llave vigente, NO de nacimiento, de otro DID».
|
||||
#[test]
|
||||
fn no_se_rota_a_la_llave_vigente_no_natal_de_otro_did() {
|
||||
new_test_ext().execute_with(|| {
|
||||
registrar_alice();
|
||||
rotar_alice(&alice_pair(), &dave_pair()); // dave: vigente de alice, no natal de nadie
|
||||
let sig_bob = sign_registration(&bob_pair(), &bob_account());
|
||||
assert_ok!(Pallet::register_agent_did(
|
||||
bob_origin(),
|
||||
bob_pubkey(),
|
||||
AgentKind::Human,
|
||||
alloc::vec::Vec::new(),
|
||||
sig_bob
|
||||
));
|
||||
let did_bob = *derive_did(&bob_pubkey()).as_bytes();
|
||||
let (b, d) = (bob_pubkey(), dave_pair().public().0);
|
||||
assert!(
|
||||
!DidToAccount::<Test>::contains_key(derive_did(&d)),
|
||||
"dave no es natal"
|
||||
);
|
||||
let s_b = sign_rotation(&bob_pair(), &did_bob, &b, &d, 0, &bob_account());
|
||||
let s_d = sign_rotation(&dave_pair(), &did_bob, &b, &d, 0, &bob_account());
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(bob_origin(), d, s_b, s_d),
|
||||
Error::<Test>::KeyAlreadyUsed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Replay entre cadenas: una rotación firmada con OTRO génesis no vale aquí. Todo lo
|
||||
/// demás es idéntico —cuenta, DID, llaves, nonce—.
|
||||
#[test]
|
||||
fn una_rotacion_firmada_para_otra_cadena_no_vale() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let did = registrar_alice();
|
||||
let otra_cadena = [0xCDu8; 32];
|
||||
assert_ne!(otra_cadena, GENESIS);
|
||||
let (a, d) = (alice_pubkey(), dave_pair().public().0);
|
||||
let s_a = sign_rotation_on(
|
||||
&otra_cadena,
|
||||
&alice_pair(),
|
||||
&did,
|
||||
&a,
|
||||
&d,
|
||||
0,
|
||||
&alice_account(),
|
||||
);
|
||||
let s_d = sign_rotation_on(
|
||||
&otra_cadena,
|
||||
&dave_pair(),
|
||||
&did,
|
||||
&a,
|
||||
&d,
|
||||
0,
|
||||
&alice_account(),
|
||||
);
|
||||
assert_noop!(
|
||||
Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d),
|
||||
Error::<Test>::InvalidCurrentKeySignature
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user