feat(identity): UNA identidad para todo Origin — contexto KDF compartido (RV 9→10)

DC ORIGIN IDENTITY UNIFICATION V1 (Larihana), Opción A + enmienda D-ID-7 mía.
Ratificado por David 2026-07-28. Firmado approve (pendientes Larihana + Mathi).

EL PROBLEMA (medido por Larihana, verificado por mí en ambos lados):
  chain: blake3::derive_key("origin-chain/agent-did/v1", pk) → Did
  OCF:   blake3::hash(pk)                                    → OitFingerprint
La MISMA pubkey Ed25519 producía 32 bytes DISTINTOS en cada capa. No era un bug
de criptografía: cada capa hizo domain separation correctamente por separado y
nadie decidió la relación entre las dos. Consecuencia: viendo un fingerprint en
un canal de chat NO se podía resolver el DID on-chain sin la pubkey completa —
un usuario tenía una identidad en el chat y otra en la blockchain. Directiva de
David: "que todo lo que creamos tenga una misma identidad para poder acceder".

EL CAMBIO:
- Nueva constante pública ORIGIN_IDENTITY_KDF_CONTEXT = "origin/identity/v1",
  el contexto CANÓNICO compartido por todas las capas de Origin.
- derive_did pasa a derivar con ese contexto ⇒ el identificador de 32 bytes es
  ahora EL MISMO en chain, chat (OCF), assets y credenciales.
- RUNTIME_VERSION 9→10: cambia la transición de estado de register_agent_did (el
  DID calculado y almacenado) ⇒ spec bump obligatorio. Las FIRMAS de los
  extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto.

POR QUÉ SIN MIGRACIÓN (la verificación que el DC me delegó, medida no asumida):
- Sin GenesisConfig en el pallet → no se siembran identidades en génesis.
- Sin DIDs en los 3 presets de génesis.
- Sin golden vectors ni DID hex hardcodeado en el repo.
- StorageVersion = 0, nunca migrado — greenfield puro.
- derive_did = UNA función, UN call-site; los tests lo invocan (no pinnean bytes).
⇒ Ningún estado on-chain dependía del esquema viejo. La ventana pre-mainnet
  estaba abierta y el costo fue una constante de string.

ENMIENDA D-ID-7 — golden vectors cross-repo (lo que aporté al DC):
El DC arreglaba la INSTANCIA pero no la CLASE. Origin Chain es submódulo y NO
puede depender de arkhe-fabric-core (dep-direction submódulo→monorepo = ILEGAL,
la misma ley que impide el swap del attestation SSOT), así que las dos capas no
pueden compartir la constante por Cargo — y sin ancla podían volver a divergir en
silencio, que es justo el bug que cerramos. Añadidos 3 vectores pinneados a bytes
(2 patrones + 1 pubkey Ed25519 real) para que fabric-core pinnee LOS MISMOS.
Si cualquiera de los dos lados cambia el contexto o la función, su test falla.
Mismo patrón que ya nos salvó con el attestation SSOT: paridad por matemática,
no por disciplina humana.

NO VACUO — verificado empíricamente: saboteé el contexto a "v2-DRIFT" y AMBOS
tests fallaron con el diagnóstico correcto; restaurado, verdes de nuevo. (Un test
que pasa no prueba nada hasta que se le ve fallar sin el fix.)

TRADE-OFF asumido y dejado explícito en el acta del DC: un identificador único en
chat + chain + assets + credenciales implica que ver el fingerprint de alguien en
un canal permite resolver sus activos y transacciones on-chain — no hay frontera
de seudonimato entre capas. Es INHERENTE a "una identidad para todo"; la
alternativa (derivación por-contexto tipo HD) reintroduce los dos identificadores
en dos pantallas que justamente eliminamos. Decisión consciente, no efecto
secundario descubierto después.

Verificado: workspace completo verde (0 fallos), 20/20 en agent-identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Opus 4.8 committed 2026-07-28 12:25:05 -05:00
1 parent 266ee4084d
commit 13f8ac00a5
2 files changed
+130 -11

No files matched your search

+112 -3
View File
@@ -75,12 +75,36 @@ pub fn derive_did(public_key: &[u8; 32]) -> Did {
// FIX-3 (V3-H2): KDF domain-separado vía blake3::derive_key (context string
// como dominio) en vez de un hash ad-hoc concat. Es el patrón KDF canonical de
// BLAKE3 y previene colisión cross-uso del hash. Inmutable #16 preservado
// (determinista cross-runtime). ⚠️ Cambia el DID para un mismo public_key vs el
// esquema previo (BLAKE3(pk || version)) — aceptable pre-mainnet (greenfield).
let did_bytes = blake3::derive_key("origin-chain/agent-did/v1", public_key);
// (determinista cross-runtime).
//
// DC ORIGIN IDENTITY UNIFICATION V1 (2026-07-28, David): el contexto pasó de
// `"origin-chain/agent-did/v1"` (propio de la chain) al contexto COMPARTIDO
// `origin/identity/v1` — la MISMA pubkey Ed25519 ahora produce EL MISMO
// identificador de 32 bytes en chain, chat (OCF), assets y credenciales.
// Antes, chain derivaba con su contexto y OCF hacía `blake3::hash(pubkey)`
// plano: dos identificadores para el mismo usuario en dos pantallas.
// ⚠️ Cambia el DID para un mismo public_key vs el esquema previo — aceptable
// pre-mainnet (greenfield): verificado que NINGÚN estado on-chain depende del
// esquema viejo (sin GenesisConfig, sin DIDs en presets, StorageVersion 0).
let did_bytes = blake3::derive_key(ORIGIN_IDENTITY_KDF_CONTEXT, public_key);
origin_chain_types::AssetHash::new(did_bytes)
}
/// Contexto KDF **canónico de identidad de Origin** — DC ORIGIN IDENTITY
/// UNIFICATION V1 (D-ID-3, Opción A).
///
/// Una sola cuerda de dominio para TODAS las capas: `derive_key(este_contexto,
/// pubkey_ed25519)` es el identificador unificado de 32 bytes. Origin Chain lo usa
/// para el `Did`; `arkhe-fabric-core` lo usa para `OitFingerprint`.
///
/// ⚠️ **NO puede compartirse como constante Rust entre los dos repos**: Origin Chain
/// es submódulo y depender de `arkhe-fabric-core` invertiría la dirección de
/// dependencias (submódulo→monorepo = ILEGAL; el chain dejaría de buildear
/// standalone). La paridad se preserva por el **golden vector** de D-ID-7 —
/// ver `tests::did_golden_vectors_cross_repo_parity`. Si cualquiera de las dos
/// capas cambia el contexto o la función, su test falla y la deriva se caza ahí.
pub const ORIGIN_IDENTITY_KDF_CONTEXT: &str = "origin/identity/v1";
/// Domain tag canonical para la firma de binding del registro de DID (FIX-3 (b),
/// V3-H1 anti-front-running). El registrante prueba control de `public_key`
/// firmando este dominio + el AccountId del caller (`who`).
@@ -402,6 +426,90 @@ mod tests {
assert_ne!(did_a, did_b);
}
// ═══════════════════════════════════════════════════════════════
// D-ID-7 — GOLDEN VECTORS CROSS-REPO (DC ORIGIN IDENTITY UNIFICATION V1)
// ═══════════════════════════════════════════════════════════════
//
// El DC unificó la derivación de identidad: la MISMA pubkey Ed25519 debe
// producir EL MISMO identificador de 32 bytes en Origin Chain (`Did`) y en
// OCF / arkhe-fabric-core (`OitFingerprint::from_ed25519_pubkey`).
//
// PROBLEMA QUE ESTE TEST RESUELVE: Origin Chain es submódulo y NO puede
// depender de `arkhe-fabric-core` (dep-direction submódulo→monorepo = ILEGAL),
// así que las dos capas NO pueden compartir la constante del contexto por
// Cargo. Sin un ancla, pueden volver a divergir en silencio — que es
// exactamente el bug que el DC cierra. Estos vectores son ese ancla: están
// pinneados a bytes en AMBOS repos. Si cualquiera de los dos lados cambia el
// contexto o la función de derivación, su copia de este test falla.
//
// MISMO PATRÓN que los golden vectors del attestation SSOT
// (`origin-chain-types::attestation_verify`): la paridad cross-repo se
// preserva por matemática pinneada, no por disciplina humana.
//
// Contraparte a pinnear en fabric-core:
// OitFingerprint::from_ed25519_pubkey(&pk).as_bytes() == <mismos bytes>
//
// Regenerar SOLO si el DC cambia el contexto canónico (requiere re-firma).
/// Vectores canónicos: (pubkey, identificador unificado esperado).
/// Derivación: `blake3::derive_key("origin/identity/v1", pubkey)`.
const IDENTITY_GOLDEN_VECTORS: [([u8; 32], &str); 3] = [
// Degenerado todo-ceros — trivialmente reproducible en cualquier lenguaje.
(
[0x00u8; 32],
"d2d1e2fa7be9e8424f1528de50ec58c4e87c357c6b98204882272be56fb8f7db",
),
// Patrón 0x11 — segundo control simple.
(
[0x11u8; 32],
"668b46ad620dc9dd0b5f17abceb6357de9bfa5a6102a4174b03c8e13cdbac85b",
),
// Pubkey Ed25519 REAL (par derivado de la seed [0x11; 32]) — prueba que
// funciona con un punto de curva legítimo, no solo con patrones.
(
[
0xd0, 0x4a, 0xb2, 0x32, 0x74, 0x2b, 0xb4, 0xab, 0x3a, 0x13, 0x68, 0xbd, 0x46, 0x15,
0xe4, 0xe6, 0xd0, 0x22, 0x4a, 0xb7, 0x1a, 0x01, 0x6b, 0xaf, 0x85, 0x20, 0xa3, 0x32,
0xc9, 0x77, 0x87, 0x37,
],
"ea484da28858c031dd846d75f443efa429c142466e68df478ec0761dcb1bc544",
),
];
fn hex32(bytes: &[u8; 32]) -> alloc::string::String {
use alloc::string::String;
let mut s = String::with_capacity(64);
for b in bytes {
s.push(core::char::from_digit((b >> 4) as u32, 16).unwrap());
s.push(core::char::from_digit((b & 0x0f) as u32, 16).unwrap());
}
s
}
#[test]
fn did_golden_vectors_cross_repo_parity() {
for (pubkey, expected_hex) in IDENTITY_GOLDEN_VECTORS {
let did = derive_did(&pubkey);
assert_eq!(
hex32(did.as_bytes()),
expected_hex,
"DERIVA DE IDENTIDAD ROTA para pubkey {}: el DC ORIGIN IDENTITY \
UNIFICATION V1 exige que este byte-a-byte coincida con \
OitFingerprint::from_ed25519_pubkey en arkhe-fabric-core. \
Si cambiaste el contexto KDF, el DC debe re-firmarse y AMBOS \
repos regenerar sus vectores.",
hex32(&pubkey)
);
}
}
#[test]
fn identity_kdf_context_is_the_shared_canonical_one() {
// El contexto NO es libre: es el ratificado en el DC. Un cambio aquí
// rompe la unificación con OCF aunque los vectores se regeneren.
assert_eq!(ORIGIN_IDENTITY_KDF_CONTEXT, "origin/identity/v1");
}
#[test]
fn re_export_agent_identity_canonical() {
// Verify re-exports canonical desde origin-chain-types
@@ -409,3 +517,4 @@ mod tests {
let _did_alias: Did = origin_chain_types::AssetHash::new([0u8; 32]);
}
}