//! # 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::::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::::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::::get(did).is_none()); }); } #[test] fn el_registro_quema_la_llave_de_nacimiento() { new_test_ext().execute_with(|| { registrar_alice(); assert_eq!( KeyToDid::::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::::get(alice_account()).unwrap().did, did); assert_eq!(DidToAccount::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::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::::get(did), 0); rotar_alice(&alice_pair(), &dave_pair()); assert_eq!(RotationNonce::::get(did), 1); rotar_alice(&dave_pair(), &eve_pair()); assert_eq!(RotationNonce::::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::() }; 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::::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::::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::::KeyAlreadyUsed ); // Y el índice sigue apuntando al DID de alice. assert_eq!(KeyToDid::::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::::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::::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::::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::::InvalidCurrentKeySignature ); }); }