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
@@ -19,7 +19,9 @@
|
||||
use super::*;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::pallet_prelude::Decode;
|
||||
use frame_system::pallet_prelude::BlockNumberFor;
|
||||
use frame_system::RawOrigin;
|
||||
use sp_runtime::traits::Zero;
|
||||
// `AgentKind` llega vía `super::*` (re-exportado en el crate root).
|
||||
|
||||
use alloc::vec::Vec;
|
||||
@@ -92,5 +94,53 @@ mod benchmarks {
|
||||
assert!(Identities::<T>::contains_key(&who));
|
||||
}
|
||||
|
||||
/// Rotación ALICE→DAVE con las dos firmas dorados (`crate::benchmark_golden`).
|
||||
///
|
||||
/// Mide el camino FELIZ completo: dos verificaciones Ed25519 y las cuatro
|
||||
/// escrituras. Si las firmas quedaran viejas, la primera verificación fallaría y
|
||||
/// el benchmark mediría el error temprano — por eso el assert del final, y por
|
||||
/// eso un test fija los vectores contra el builder del mensaje.
|
||||
#[benchmark]
|
||||
fn rotate_agent_key() {
|
||||
let who: T::AccountId =
|
||||
Decode::decode(&mut &CALLER[..]).expect("AccountId32 decodes from 32 bytes");
|
||||
// El mensaje de rotación lleva el génesis (anti-replay entre cadenas). Las firmas
|
||||
// doradas se calcularon con `0xAB×32`, el mismo valor que siembra el mock; en el
|
||||
// runtime real `block_hash(0)` es otro, así que hay que sembrarlo aquí.
|
||||
//
|
||||
// Sin esta línea el benchmark pasaba en el mock y FALLABA en el WASM real con
|
||||
// `InvalidCurrentKeySignature`: lo cazó el bencher en el primer intento de medir
|
||||
// RV14. Si no fallara ruidoso, habría medido el camino de error y producido un
|
||||
// peso falso — por eso el assert del final existe.
|
||||
let genesis: T::Hash =
|
||||
Decode::decode(&mut &[0xABu8; 32][..]).expect("H256 decodes from 32 bytes");
|
||||
frame_system::BlockHash::<T>::insert(BlockNumberFor::<T>::zero(), genesis);
|
||||
// Precondición: ALICE registrada con su llave de nacimiento.
|
||||
Pallet::<T>::register_agent_did(
|
||||
RawOrigin::Signed(who.clone()).into(),
|
||||
to_32(BENCH_PUBKEY),
|
||||
AgentKind::Human,
|
||||
Vec::new(),
|
||||
to_64(BENCH_SIG),
|
||||
)
|
||||
.expect("registro dorado válido");
|
||||
|
||||
let new_key = to_32(crate::benchmark_golden::ROT_DAVE_PUBKEY);
|
||||
let sig_current = to_64(crate::benchmark_golden::ROT_SIG_CURRENT);
|
||||
let sig_new = to_64(crate::benchmark_golden::ROT_SIG_NEW);
|
||||
|
||||
#[extrinsic_call]
|
||||
rotate_agent_key(
|
||||
RawOrigin::Signed(who.clone()),
|
||||
new_key,
|
||||
sig_current,
|
||||
sig_new,
|
||||
);
|
||||
|
||||
// Si esto falla, se midió el camino de error y el peso es falso.
|
||||
let did = crate::derive_did(&to_32(BENCH_PUBKEY));
|
||||
assert_eq!(CurrentKey::<T>::get(did), Some(new_key));
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -127,6 +127,83 @@ pub fn agent_did_register_message(who_encoded: &[u8]) -> alloc::vec::Vec<u8> {
|
||||
msg
|
||||
}
|
||||
|
||||
/// Dominio de la firma de ROTACIÓN de llave. Distinto del de registro a propósito:
|
||||
/// una firma de registro nunca puede reutilizarse como autorización de rotación.
|
||||
pub const AGENT_KEY_ROTATE_DOMAIN: &[u8] = b"origin-chain/agent-key-rotate/v1";
|
||||
|
||||
/// Mensaje que firman AMBAS llaves —la vigente y la nueva— para rotar.
|
||||
///
|
||||
/// `AGENT_KEY_ROTATE_DOMAIN || genesis || did || llave_vigente || llave_nueva || nonce_le || who`
|
||||
///
|
||||
/// Cada campo cierra un ataque distinto, y por eso están todos:
|
||||
/// - `genesis`: la firma solo vale en ESTA cadena. Sin él, una rotación firmada en
|
||||
/// testnet se podía reenviar en mainnet si alguien reusaba cuenta y llaves, y quien
|
||||
/// robara la cuenta en mainnet podía aplicar una autorización vieja de la llave sin
|
||||
/// tener la llave. Encontrado por la revisión adversarial de RV14, medido con dos
|
||||
/// entornos de prueba independientes. Mismo patrón que `origin-chain-provenance`.
|
||||
/// - `did`: la firma solo vale para ESTA identidad.
|
||||
/// - `llave_vigente`: ata la autorización a la llave que manda HOY; tras rotar, la
|
||||
/// misma firma ya no describe el estado y no puede reutilizarse.
|
||||
/// - `llave_nueva`: la nueva llave prueba que su dueño consiente ser la autoridad.
|
||||
/// - `nonce`: anti-replay explícito, independiente de lo anterior.
|
||||
/// - `who`: la firma queda atada a la cuenta que la envía; nadie puede tomar una
|
||||
/// rotación firmada del mempool y enviarla desde otra cuenta.
|
||||
///
|
||||
/// Una sola fuente de verdad para el verificador on-chain y para el firmante.
|
||||
pub fn agent_key_rotate_message(
|
||||
genesis_hash: &[u8; 32],
|
||||
did: &[u8; 32],
|
||||
current_key: &[u8; 32],
|
||||
new_key: &[u8; 32],
|
||||
nonce: u32,
|
||||
who_encoded: &[u8],
|
||||
) -> alloc::vec::Vec<u8> {
|
||||
let mut msg = alloc::vec::Vec::with_capacity(
|
||||
AGENT_KEY_ROTATE_DOMAIN.len() + 32 * 4 + 4 + who_encoded.len(),
|
||||
);
|
||||
msg.extend_from_slice(AGENT_KEY_ROTATE_DOMAIN);
|
||||
msg.extend_from_slice(genesis_hash);
|
||||
msg.extend_from_slice(did);
|
||||
msg.extend_from_slice(current_key);
|
||||
msg.extend_from_slice(new_key);
|
||||
msg.extend_from_slice(&nonce.to_le_bytes());
|
||||
msg.extend_from_slice(who_encoded);
|
||||
msg
|
||||
}
|
||||
|
||||
/// ¿Es esta llave Ed25519 una llave con la que solo su dueño puede firmar?
|
||||
///
|
||||
/// Exige tres cosas: que el punto esté en la curva, que su codificación sea la
|
||||
/// CANÓNICA (volver a comprimirlo da los mismos 32 bytes) y que **no sea de orden
|
||||
/// pequeño**.
|
||||
///
|
||||
/// **Por qué hace falta, y es grave:** el verificador del host
|
||||
/// (`sp_io::crypto::ed25519_verify`, ed25519-zebra con reglas ZIP215) ACEPTA llaves de
|
||||
/// orden pequeño por compatibilidad de consenso. Con el punto identidad como llave
|
||||
/// (`01 00..00`) y la firma trivial `R = identidad, s = 0`, la verificación da `true`
|
||||
/// **para cualquier mensaje**. Sin este chequeo, la «prueba de control de la llave
|
||||
/// nueva» de una rotación —y la firma de registro— se superaban sin llave privada, y
|
||||
/// el DID quedaba bajo una llave con la que CUALQUIERA firma en su nombre. Para
|
||||
/// Ethos, donde un DID puede ser el de un niño, eso es suplantación.
|
||||
///
|
||||
/// Encontrado por la revisión adversarial de RV14 y medido con sonda: la rotación y el
|
||||
/// registro devolvían `Ok` con `01 00..00`, con el punto de orden 2 (`ec ff..ff 7f`) y
|
||||
/// con una codificación NO canónica de la identidad (`ee ff..ff 7f`). Tener que
|
||||
/// cubrir codificaciones no canónicas es la razón para comprimir de vuelta en vez de
|
||||
/// mantener una lista: una lista escrita a mano se deja fuera alguna.
|
||||
///
|
||||
/// No se exige `is_torsion_free`: una llave de orden mixto no se puede falsificar (hace
|
||||
/// falta el logaritmo discreto de su componente de orden primo) y ese chequeo cuesta
|
||||
/// una multiplicación escalar completa por extrinsic.
|
||||
pub fn is_strong_ed25519_key(key: &[u8; 32]) -> bool {
|
||||
use curve25519_dalek::edwards::CompressedEdwardsY;
|
||||
let compressed = CompressedEdwardsY(*key);
|
||||
let Some(point) = compressed.decompress() else {
|
||||
return false;
|
||||
};
|
||||
point.compress().to_bytes() == *key && !point.is_small_order()
|
||||
}
|
||||
|
||||
/// Runtime API canonical para cross-pillar consumers + light clients.
|
||||
///
|
||||
/// Permite query canonical de agent identity canonical sin necesidad de full
|
||||
@@ -149,6 +226,10 @@ sp_api::decl_runtime_apis! {
|
||||
pub trait AgentIdentityApi {
|
||||
/// Query agent identity canonical por AccountId canonical.
|
||||
///
|
||||
/// ⚠️ El campo `creator` que devuelve es la llave de NACIMIENTO y no cambia al
|
||||
/// rotar. **No lo uses para verificar firmas del titular**: usa
|
||||
/// `AgentKeyApi::current_key`.
|
||||
///
|
||||
/// Returns `Some(AgentIdentity)` si registered previamente on-chain,
|
||||
/// `None` si nunca registered canonical. Read-only canonical sin
|
||||
/// transaction fee canonical (off-chain via RPC).
|
||||
@@ -156,6 +237,36 @@ sp_api::decl_runtime_apis! {
|
||||
}
|
||||
}
|
||||
|
||||
sp_api::decl_runtime_apis! {
|
||||
/// Resolución DID → llave vigente.
|
||||
///
|
||||
/// Es la pregunta que hace cualquiera que verifique una firma del titular de un
|
||||
/// DID (Ethos para `subject_pubkey`, Codex para créditos, un light client): no
|
||||
/// «¿qué llave generó este DID?» sino «¿qué llave manda HOY sobre él?». Tras una
|
||||
/// rotación son distintas, y un consumidor que cachee la llave de nacimiento
|
||||
/// dejaría de validar al titular legítimo.
|
||||
///
|
||||
/// Trait aparte de `AgentIdentityApi` a propósito: añadir un método a una API ya
|
||||
/// publicada obliga a versionarla, y una API nueva no toca a sus consumidores.
|
||||
///
|
||||
/// **Tres reglas para quien consuma esta API**, y las tres salieron de la revisión
|
||||
/// adversarial de RV14:
|
||||
/// 1. **Responde «ahora».** Para validar una firma hecha en el bloque N, llama a esta
|
||||
/// API EN el hash del bloque N (toda runtime API de Substrate lo admite), o
|
||||
/// reconstruye la historia con los eventos `KeyRotated`. Verificar una firma
|
||||
/// antigua contra la llave de hoy la rechaza aunque fuera legítima.
|
||||
/// 2. **Verifica con reglas estrictas** (`verify_strict` en dalek o equivalente). La
|
||||
/// cadena ya rechaza llaves de orden pequeño, pero un verificador permisivo fuera
|
||||
/// de cadena sigue aceptando firmas maleables.
|
||||
/// 3. **Separa dominios.** Esta misma llave firma cosas fuera de cadena. Si algún
|
||||
/// protocolo le hace firmar bytes elegidos por otro sin prefijo propio, quien tenga
|
||||
/// solo la cuenta podría conseguir una firma de rotación.
|
||||
pub trait AgentKeyApi {
|
||||
/// Llave Ed25519 vigente de `did`, o `None` si el DID no está registrado.
|
||||
fn current_key(did: origin_chain_types::Did) -> Option<[u8; 32]>;
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use alloc::vec::Vec;
|
||||
@@ -163,7 +274,7 @@ pub mod pallet {
|
||||
use frame_system::pallet_prelude::*;
|
||||
use origin_chain_types::{AccountId, AgentChain, AgentIdentity, AgentKind, Did};
|
||||
use sp_core::ed25519;
|
||||
use sp_runtime::traits::SaturatedConversion;
|
||||
use sp_runtime::traits::{SaturatedConversion, Zero};
|
||||
// Trait de weights en scope para resolver `T::WeightInfo::<method>()`.
|
||||
use crate::weights::WeightInfo;
|
||||
|
||||
@@ -216,6 +327,32 @@ pub mod pallet {
|
||||
pub type DidToAccount<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, Did, T::AccountId, OptionQuery>;
|
||||
|
||||
/// Llave Ed25519 VIGENTE de cada DID.
|
||||
///
|
||||
/// **Ausente ⇒ manda la llave de nacimiento**, que ya vive en
|
||||
/// `Identities[cuenta].creator`. Así las identidades registradas antes de que
|
||||
/// existiera la rotación no necesitan migración: para ellas la vigente y la de
|
||||
/// nacimiento son la misma, y el resolutor lo sabe.
|
||||
///
|
||||
/// El DID NO cambia al rotar. Nunca se re-deriva: se calculó una vez en el
|
||||
/// registro y es el identificador de por vida. Lo que cambia es qué llave tiene
|
||||
/// autoridad sobre él.
|
||||
#[pallet::storage]
|
||||
pub type CurrentKey<T: Config> = StorageMap<_, Blake2_128Concat, Did, [u8; 32], OptionQuery>;
|
||||
|
||||
/// Registro de TODA llave que alguna vez tuvo autoridad sobre un DID.
|
||||
///
|
||||
/// Una llave manda sobre un solo DID en toda la vida de la cadena. Una llave
|
||||
/// retirada queda **quemada**: no puede volver ni al mismo DID ni a otro. La
|
||||
/// razón es la más común para rotar —que la llave vieja se haya filtrado— y
|
||||
/// dejar reutilizarla sería devolverle la autoridad a quien la robó.
|
||||
#[pallet::storage]
|
||||
pub type KeyToDid<T: Config> = StorageMap<_, Blake2_128Concat, [u8; 32], Did, OptionQuery>;
|
||||
|
||||
/// Contador de rotaciones por DID. Entra en el mensaje firmado (anti-replay).
|
||||
#[pallet::storage]
|
||||
pub type RotationNonce<T: Config> = StorageMap<_, Blake2_128Concat, Did, u32, ValueQuery>;
|
||||
|
||||
/// Events emisión canonical (CHN-7 + Inmutable #41).
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
@@ -249,6 +386,23 @@ pub mod pallet {
|
||||
/// T::AccountId del agent canonical.
|
||||
account: T::AccountId,
|
||||
},
|
||||
/// La autoridad sobre un DID pasó a otra llave. El DID no cambió.
|
||||
///
|
||||
/// Va AL FINAL del enum a propósito: insertarlo en medio corría el índice SCALE de
|
||||
/// `AgentUpdated` de 2 a 3, y un decodificador estático generado con la metadata de
|
||||
/// RV13 leería un `KeyRotated` como `AgentUpdated`.
|
||||
KeyRotated {
|
||||
/// Cuenta titular de la identidad.
|
||||
account: T::AccountId,
|
||||
/// El DID, idéntico antes y después.
|
||||
did: Did,
|
||||
/// Llave que tenía la autoridad y queda quemada.
|
||||
old_key: [u8; 32],
|
||||
/// Llave que la tiene ahora.
|
||||
new_key: [u8; 32],
|
||||
/// Nonce consumido por esta rotación.
|
||||
nonce: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Pallet errors canonical.
|
||||
@@ -269,6 +423,25 @@ pub mod pallet {
|
||||
/// debe ser `AGENT_DID_REGISTER_DOMAIN || who.encode()`, firmado con la
|
||||
/// private key de `public_key`.
|
||||
InvalidSignature,
|
||||
/// La cuenta no tiene identidad registrada: no hay nada que rotar.
|
||||
NotRegistered,
|
||||
/// La llave nueva es la vigente. Rotar a la misma llave no cambia nada y
|
||||
/// consumiría un nonce en vano.
|
||||
SameKey,
|
||||
/// La llave nueva ya tuvo autoridad sobre algún DID —éste u otro, vigente o
|
||||
/// retirada—, o es la llave de nacimiento de otro DID. Una llave manda sobre
|
||||
/// un solo DID en toda la vida de la cadena.
|
||||
KeyAlreadyUsed,
|
||||
/// La firma de la llave VIGENTE no verifica: quien pide rotar no tiene la
|
||||
/// autoridad actual sobre el DID.
|
||||
InvalidCurrentKeySignature,
|
||||
/// La firma de la llave NUEVA no verifica: no se puede ceder la autoridad a
|
||||
/// una llave cuyo dueño no ha probado controlarla.
|
||||
InvalidNewKeySignature,
|
||||
/// La llave no es una llave con la que solo su dueño pueda firmar: no está en la
|
||||
/// curva, su codificación no es canónica o es de orden pequeño. Con una llave así
|
||||
/// cualquiera produce firmas válidas en nombre del DID.
|
||||
WeakKey,
|
||||
}
|
||||
|
||||
/// Dispatchables canonical (real impl v0.0.26).
|
||||
@@ -309,6 +482,13 @@ pub mod pallet {
|
||||
Error::<T>::AlreadyRegistered
|
||||
);
|
||||
|
||||
// Step 2.2: la llave tiene que ser una con la que solo su dueño firme. Sin esto
|
||||
// una llave de orden pequeño supera la verificación con una firma trivial.
|
||||
ensure!(
|
||||
crate::is_strong_ed25519_key(&public_key),
|
||||
Error::<T>::WeakKey
|
||||
);
|
||||
|
||||
// Step 2.5: FIX-3 (V3-H1) DID BINDING — prueba de control de public_key.
|
||||
// El registrante presenta una firma Ed25519 (por la private key de
|
||||
// public_key) sobre AGENT_DID_REGISTER_DOMAIN || who.encode(). Cierra el
|
||||
@@ -333,6 +513,21 @@ pub mod pallet {
|
||||
Error::<T>::DidAlreadyTaken
|
||||
);
|
||||
|
||||
// Step 4.5: una llave manda sobre un solo DID en toda la vida de la cadena.
|
||||
// Faltaba: el registro ESCRIBÍA en `KeyToDid` pero no lo CONSULTABA, así que
|
||||
// quien tuviera una llave a la que otro DID ya había rotado —vigente o
|
||||
// retirada— podía fundar con ella un DID nuevo y sobrescribir el índice.
|
||||
// Encontrado cinco veces, por separado, en la revisión adversarial de RV14.
|
||||
//
|
||||
// Va DETRÁS de la colisión de DID a propósito: registrar dos veces la misma
|
||||
// llave desde dos cuentas sigue devolviendo `DidAlreadyTaken`, el error
|
||||
// documentado de ese caso (scenario_12). Este chequeo atrapa lo que aquel no
|
||||
// ve: llaves que entraron por ROTACIÓN, cuyo `derive_did` nunca se registró.
|
||||
ensure!(
|
||||
!KeyToDid::<T>::contains_key(public_key),
|
||||
Error::<T>::KeyAlreadyUsed
|
||||
);
|
||||
|
||||
// Step 5: build AgentChain canonical BoundedVec<AccountId, ConstU32<10>>
|
||||
let mut agent_chain = AgentChain::default();
|
||||
for raw in co_authors_raw {
|
||||
@@ -364,6 +559,9 @@ pub mod pallet {
|
||||
|
||||
Identities::<T>::insert(&who, identity);
|
||||
DidToAccount::<T>::insert(did, &who);
|
||||
// La llave de nacimiento entra en el registro de llaves usadas desde el
|
||||
// primer bloque: ninguna rotación posterior de nadie podrá apuntar a ella.
|
||||
KeyToDid::<T>::insert(public_key, did);
|
||||
|
||||
// Step 8: emit events canonical
|
||||
Self::deposit_event(Event::AgentRegistered {
|
||||
@@ -381,6 +579,116 @@ pub mod pallet {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rota la llave con autoridad sobre el DID del firmante. **El DID no cambia.**
|
||||
///
|
||||
/// Existe porque sin ella una identidad no sobrevive a perder o filtrar su
|
||||
/// llave: la única salida era registrarse otra vez, que genera OTRO DID, y con
|
||||
/// él se pierden credenciales, créditos e historial. Es la misma ley que cerró
|
||||
/// la condición #4 del DC de identidad: una identidad tiene que sobrevivir a
|
||||
/// aquello en lo que se le permite convertirse.
|
||||
///
|
||||
/// Exige DOS firmas sobre `agent_key_rotate_message`:
|
||||
/// - la de la llave **vigente**, que autoriza ceder la autoridad;
|
||||
/// - la de la llave **nueva**, que prueba que su dueño la controla.
|
||||
///
|
||||
/// Sin la segunda, un titular podría «rotar» su DID a la llave pública de un
|
||||
/// tercero, que pasaría a figurar como autoridad de una identidad que no es suya.
|
||||
///
|
||||
/// **Hacen falta LAS DOS cosas: la cuenta titular Y la llave vigente.** Son dos
|
||||
/// secretos distintos (sr25519 para firmar la extrinsic, Ed25519 para la autoridad
|
||||
/// sobre el DID) y perder CUALQUIERA de los dos deja la rotación imposible:
|
||||
/// - sin la llave vigente, nadie produce la primera firma;
|
||||
/// - sin la cuenta, `Identities[who]` no existe para la cuenta nueva
|
||||
/// (`NotRegistered`), aunque la llave esté a salvo.
|
||||
///
|
||||
/// La versión anterior de este comentario decía que el único caso sin cubrir era
|
||||
/// perder la llave. Era falso a medias, y la revisión adversarial lo midió. Los dos
|
||||
/// casos son la RECUPERACIÓN por guardianes, que va aparte porque tiene su propio
|
||||
/// ataque (robar la cuenta y retirar a los guardianes antes de que el dueño
|
||||
/// reaccione).
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::rotate_agent_key())]
|
||||
pub fn rotate_agent_key(
|
||||
origin: OriginFor<T>,
|
||||
new_public_key: [u8; 32],
|
||||
current_key_signature: [u8; 64],
|
||||
new_key_signature: [u8; 64],
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let identity = Identities::<T>::get(&who).ok_or(Error::<T>::NotRegistered)?;
|
||||
let did = identity.did;
|
||||
let current_key = CurrentKey::<T>::get(did).unwrap_or(identity.creator.0);
|
||||
|
||||
ensure!(new_public_key != current_key, Error::<T>::SameKey);
|
||||
ensure!(
|
||||
crate::is_strong_ed25519_key(&new_public_key),
|
||||
Error::<T>::WeakKey
|
||||
);
|
||||
|
||||
// Quemada si alguna vez mandó sobre un DID, o si es la llave de nacimiento
|
||||
// de un DID registrado (la segunda condición cubre registros que
|
||||
// precedieran al índice `KeyToDid`).
|
||||
ensure!(
|
||||
!KeyToDid::<T>::contains_key(new_public_key)
|
||||
&& !DidToAccount::<T>::contains_key(crate::derive_did(&new_public_key)),
|
||||
Error::<T>::KeyAlreadyUsed
|
||||
);
|
||||
|
||||
let nonce = RotationNonce::<T>::get(did);
|
||||
let genesis_full = <frame_system::Pallet<T>>::block_hash(BlockNumberFor::<T>::zero());
|
||||
let genesis_ref: &[u8] = genesis_full.as_ref();
|
||||
let mut genesis_hash = [0u8; 32];
|
||||
let n = genesis_ref.len().min(32);
|
||||
genesis_hash[..n].copy_from_slice(&genesis_ref[..n]);
|
||||
let message = crate::agent_key_rotate_message(
|
||||
&genesis_hash,
|
||||
did.as_bytes(),
|
||||
¤t_key,
|
||||
&new_public_key,
|
||||
nonce,
|
||||
&who.encode(),
|
||||
);
|
||||
|
||||
ensure!(
|
||||
sp_io::crypto::ed25519_verify(
|
||||
&ed25519::Signature::from_raw(current_key_signature),
|
||||
&message,
|
||||
&ed25519::Public::from_raw(current_key),
|
||||
),
|
||||
Error::<T>::InvalidCurrentKeySignature
|
||||
);
|
||||
ensure!(
|
||||
sp_io::crypto::ed25519_verify(
|
||||
&ed25519::Signature::from_raw(new_key_signature),
|
||||
&message,
|
||||
&ed25519::Public::from_raw(new_public_key),
|
||||
),
|
||||
Error::<T>::InvalidNewKeySignature
|
||||
);
|
||||
|
||||
CurrentKey::<T>::insert(did, new_public_key);
|
||||
// La vieja ya está en el índice en todo camino actual (el registro la escribe y
|
||||
// cada rotación escribe la nueva). Solo se escribe si falta, y NUNCA se
|
||||
// sobrescribe: antes era un insert incondicional que podía pisar una entrada
|
||||
// que apuntaba a OTRO DID.
|
||||
if !KeyToDid::<T>::contains_key(current_key) {
|
||||
KeyToDid::<T>::insert(current_key, did);
|
||||
}
|
||||
KeyToDid::<T>::insert(new_public_key, did);
|
||||
RotationNonce::<T>::insert(did, nonce.saturating_add(1));
|
||||
|
||||
Self::deposit_event(Event::KeyRotated {
|
||||
account: who,
|
||||
did,
|
||||
old_key: current_key,
|
||||
new_key: new_public_key,
|
||||
nonce,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Pure helper canonical para Runtime API v0.0.27 consumer.
|
||||
@@ -389,6 +697,16 @@ pub mod pallet {
|
||||
pub fn query_identity_of(account: &T::AccountId) -> Option<AgentIdentity> {
|
||||
Identities::<T>::get(account)
|
||||
}
|
||||
|
||||
/// Llave Ed25519 con autoridad HOY sobre `did`: la rotada si hubo rotación, la
|
||||
/// de nacimiento si no. `None` si el DID no está registrado.
|
||||
pub fn current_key_of(did: &Did) -> Option<[u8; 32]> {
|
||||
if let Some(k) = CurrentKey::<T>::get(did) {
|
||||
return Some(k);
|
||||
}
|
||||
let account = DidToAccount::<T>::get(did)?;
|
||||
Identities::<T>::get(account).map(|id| id.creator.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +720,22 @@ pub mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests_integration;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_rotation;
|
||||
|
||||
/// Vectores dorados de la rotación para el benchmark (que no puede firmar en no_std).
|
||||
/// Los fija `tests_rotation::vectores_dorados_del_benchmark_de_rotacion`.
|
||||
#[cfg(any(test, feature = "runtime-benchmarks"))]
|
||||
pub mod benchmark_golden {
|
||||
/// Llave pública de DAVE (seed `[0x44;32]`), destino de la rotación.
|
||||
pub const ROT_DAVE_PUBKEY: &str =
|
||||
"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48";
|
||||
/// ALICE (vigente) firma la rotación ALICE→DAVE, génesis `0xAB×32`, nonce 0, who=`[1u8;32]`.
|
||||
pub const ROT_SIG_CURRENT: &str = "4492d319dfbd4c2e4701b3b0b3e4391f269b96b8c85f3bd388c8275a8d0c5ef0c2af41d47d422db53571f2f93ad7abb9b0b2603813169223d3a0b756202afb05";
|
||||
/// DAVE (nueva) firma la misma rotación.
|
||||
pub const ROT_SIG_NEW: &str = "122f3d749410f0029610c402e9096d0709f570fbd1819db057fac2d698654019ed0766c3ff08959be7a14ecf65c47fb46c93aa3361bc102dcf639975f154e60b";
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -547,4 +881,3 @@ mod tests {
|
||||
assert_eq!(core::mem::size_of::<Did>(), 32);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,17 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
.into();
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
// El mensaje de rotación lleva el hash de génesis (anti-replay entre cadenas).
|
||||
// Se siembra un valor fijo para que las firmas de los tests sean deterministas,
|
||||
// igual que `origin-chain-provenance`.
|
||||
frame_system::BlockHash::<Test>::insert(0u64, sp_core::H256(GENESIS));
|
||||
});
|
||||
ext
|
||||
}
|
||||
|
||||
/// Génesis de los tests de rotación. Mismo valor que usa `origin-chain-provenance`.
|
||||
pub const GENESIS: [u8; 32] = [0xABu8; 32];
|
||||
|
||||
/// Test origin canonical ALICE.
|
||||
pub fn alice_origin() -> RuntimeOrigin {
|
||||
RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32]))
|
||||
@@ -103,3 +110,50 @@ pub fn sign_registration(pair: &ed25519::Pair, who: &sp_runtime::AccountId32) ->
|
||||
let msg = origin_chain_agent_identity::agent_did_register_message(&who.encode());
|
||||
pair.sign(&msg).0
|
||||
}
|
||||
|
||||
// ─── Rotación de llave ────────────────────────────────────────────────────
|
||||
|
||||
/// Ed25519 pair DAVE — llave NUEVA a la que rota ALICE en los tests.
|
||||
pub fn dave_pair() -> ed25519::Pair {
|
||||
ed25519::Pair::from_seed(&[0x44u8; 32])
|
||||
}
|
||||
/// Ed25519 pair EVE — segunda rotación / atacante.
|
||||
pub fn eve_pair() -> ed25519::Pair {
|
||||
ed25519::Pair::from_seed(&[0x55u8; 32])
|
||||
}
|
||||
|
||||
/// Firma de rotación: `pair` firma el mensaje de rotación con EL MISMO builder que
|
||||
/// el pallet. Se usa dos veces por rotación —llave vigente y llave nueva—.
|
||||
pub fn sign_rotation(
|
||||
pair: &ed25519::Pair,
|
||||
did: &[u8; 32],
|
||||
current_key: &[u8; 32],
|
||||
new_key: &[u8; 32],
|
||||
nonce: u32,
|
||||
who: &sp_runtime::AccountId32,
|
||||
) -> [u8; 64] {
|
||||
sign_rotation_on(&GENESIS, pair, did, current_key, new_key, nonce, who)
|
||||
}
|
||||
|
||||
/// Igual que `sign_rotation` pero firmando para OTRA cadena (otro génesis). Existe para
|
||||
/// probar que una rotación firmada en una cadena no vale en otra.
|
||||
pub fn sign_rotation_on(
|
||||
genesis: &[u8; 32],
|
||||
pair: &ed25519::Pair,
|
||||
did: &[u8; 32],
|
||||
current_key: &[u8; 32],
|
||||
new_key: &[u8; 32],
|
||||
nonce: u32,
|
||||
who: &sp_runtime::AccountId32,
|
||||
) -> [u8; 64] {
|
||||
use parity_scale_codec::Encode;
|
||||
let msg = origin_chain_agent_identity::agent_key_rotate_message(
|
||||
genesis,
|
||||
did,
|
||||
current_key,
|
||||
new_key,
|
||||
nonce,
|
||||
&who.encode(),
|
||||
);
|
||||
pair.sign(&msg).0
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
//! procedimiento completo y el hardware al que hay que creerle).
|
||||
//!
|
||||
//! GENERADO POR SUBSTRATE BENCHMARK CLI VERSION 51.0.0
|
||||
//! FECHA: 2026-09-09, STEPS: `50`, REPEAT: `20`
|
||||
//! FECHA: 2026-09-16, STEPS: `50`, REPEAT: `20`
|
||||
//! HOSTNAME: `MacBookProM3Max.local`, CPU: `<UNKNOWN>`
|
||||
//! WASM-EXECUTION: `Compiled`, DB CACHE: 1024
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// /Users/davidherran/originmetaverse/artifacts/origin-chain/origin_chain_runtime_rv12_bench.compact.compressed.wasm
|
||||
// target/release/wbuild/origin-chain-runtime/origin_chain_runtime.compact.compressed.wasm
|
||||
// --genesis-builder-preset
|
||||
// development
|
||||
// --pallet
|
||||
@@ -36,11 +36,11 @@
|
||||
// --repeat
|
||||
// 20
|
||||
// --template
|
||||
// /Users/davidherran/originmetaverse/external/arkhe-origin-chain/scripts/bench/origin-weights-template.hbs
|
||||
// scripts/bench/origin-weights-template.hbs
|
||||
// --header
|
||||
// /Users/davidherran/originmetaverse/external/arkhe-origin-chain/scripts/bench/HEADER.txt
|
||||
// scripts/bench/HEADER.txt
|
||||
// --output
|
||||
// /Users/davidherran/originmetaverse/external/arkhe-origin-chain/crates/origin-chain-agent-identity/src/weights.rs
|
||||
// crates/origin-chain-agent-identity/src/weights.rs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
@@ -54,6 +54,8 @@ use frame_support::{traits::Get, weights::constants::RocksDbWeight, weights::Wei
|
||||
pub trait WeightInfo {
|
||||
/// Peso de `register_agent_did` — MEDIDO sobre el WASM, no estimado a mano.
|
||||
fn register_agent_did() -> Weight;
|
||||
/// Peso de `rotate_agent_key` — MEDIDO sobre el WASM, no estimado a mano.
|
||||
fn rotate_agent_key() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights parametrizados por el `DbWeight` real del runtime.
|
||||
@@ -63,6 +65,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
/// Proof: `AgentIdentity::Identities` (`max_values`: None, `max_size`: Some(475), added: 2950, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AgentIdentity::DidToAccount` (r:1 w:1)
|
||||
/// Proof: `AgentIdentity::DidToAccount` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AgentIdentity::KeyToDid` (r:1 w:1)
|
||||
/// Proof: `AgentIdentity::KeyToDid` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Number` (r:1 w:0)
|
||||
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::ExecutionPhase` (r:1 w:0)
|
||||
@@ -75,21 +79,57 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
// Proof size en bytes:
|
||||
// Medido: `25`
|
||||
// Estimado: `3940`
|
||||
// Tiempo minimo de ejecucion: 41_000_000 picosegundos.
|
||||
Weight::from_parts(43_000_000, 0)
|
||||
// Tiempo minimo de ejecucion: 54_000_000 picosegundos.
|
||||
Weight::from_parts(55_000_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3940))
|
||||
.saturating_add(T::DbWeight::get().reads(6_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
.saturating_add(T::DbWeight::get().reads(7_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: `AgentIdentity::Identities` (r:1 w:0)
|
||||
/// Proof: `AgentIdentity::Identities` (`max_values`: None, `max_size`: Some(475), added: 2950, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AgentIdentity::CurrentKey` (r:1 w:1)
|
||||
/// Proof: `AgentIdentity::CurrentKey` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AgentIdentity::KeyToDid` (r:2 w:1)
|
||||
/// Proof: `AgentIdentity::KeyToDid` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AgentIdentity::DidToAccount` (r:1 w:0)
|
||||
/// Proof: `AgentIdentity::DidToAccount` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AgentIdentity::RotationNonce` (r:1 w:1)
|
||||
/// Proof: `AgentIdentity::RotationNonce` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::BlockHash` (r:1 w:0)
|
||||
/// Proof: `System::BlockHash` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Number` (r:1 w:0)
|
||||
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::ExecutionPhase` (r:1 w:0)
|
||||
/// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::EventCount` (r:1 w:1)
|
||||
/// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Events` (r:1 w:1)
|
||||
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||
fn rotate_agent_key() -> Weight {
|
||||
// Proof size en bytes:
|
||||
// Medido: `434`
|
||||
// Estimado: `6100`
|
||||
// Tiempo minimo de ejecucion: 93_000_000 picosegundos.
|
||||
Weight::from_parts(97_000_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 6100))
|
||||
.saturating_add(T::DbWeight::get().reads(11_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(5_u64))
|
||||
}
|
||||
}
|
||||
|
||||
/// Impl para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`.
|
||||
impl WeightInfo for () {
|
||||
fn register_agent_did() -> Weight {
|
||||
Weight::from_parts(43_000_000, 0)
|
||||
Weight::from_parts(55_000_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3940))
|
||||
.saturating_add(RocksDbWeight::get().reads(6_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(5_u64))
|
||||
}
|
||||
fn rotate_agent_key() -> Weight {
|
||||
Weight::from_parts(97_000_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 6100))
|
||||
.saturating_add(RocksDbWeight::get().reads(11_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(5_u64))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +159,7 @@ mod guard {
|
||||
fn ningun_weight_declara_proof_size_cero() {
|
||||
let casos: &[(&str, Weight)] = &[
|
||||
("register_agent_did", <() as WeightInfo>::register_agent_did()),
|
||||
("rotate_agent_key", <() as WeightInfo>::rotate_agent_key()),
|
||||
];
|
||||
for (nombre, w) in casos {
|
||||
assert!(
|
||||
|
||||
Reference in new issue
Block a user