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 // 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 // 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 // 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 // (determinista cross-runtime).
// esquema previo (BLAKE3(pk || version)) — aceptable pre-mainnet (greenfield). //
let did_bytes = blake3::derive_key("origin-chain/agent-did/v1", public_key); // 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) 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), /// 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` /// V3-H1 anti-front-running). El registrante prueba control de `public_key`
/// firmando este dominio + el AccountId del caller (`who`). /// firmando este dominio + el AccountId del caller (`who`).
@@ -402,6 +426,90 @@ mod tests {
assert_ne!(did_a, did_b); 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] #[test]
fn re_export_agent_identity_canonical() { fn re_export_agent_identity_canonical() {
// Verify re-exports canonical desde origin-chain-types // 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]); let _did_alias: Did = origin_chain_types::AssetHash::new([0u8; 32]);
} }
} }
+18 -8
View File
@@ -266,7 +266,16 @@ pub const IMPL_NAME: &str = "origin-chain-charon";
/// Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. La feature /// Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. La feature
/// `runtime-benchmarks` gana `define_benchmarks!` + runtime API `Benchmark` (inerte /// `runtime-benchmarks` gana `define_benchmarks!` + runtime API `Benchmark` (inerte
/// sin la feature) + fix del bug `pallet-grandpa/try-runtime` en la feature. /// sin la feature) + fix del bug `pallet-grandpa/try-runtime` en la feature.
pub const RUNTIME_VERSION: u32 = 9; ///
/// **v10 (identidad unificada)**: DC ORIGIN IDENTITY UNIFICATION V1 — `derive_did`
/// pasa del contexto propio de la chain (`origin-chain/agent-did/v1`) al contexto
/// COMPARTIDO `origin/identity/v1`, de modo que la misma pubkey Ed25519 produce EL
/// MISMO identificador de 32 bytes en chain y en OCF (antes eran distintos: dos
/// identidades para el mismo usuario). 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. Sin
/// migración: verificado que ningún estado on-chain dependía del esquema viejo.
pub const RUNTIME_VERSION: u32 = 10;
/// Canonical authoring version per Substrate semver canonical. /// Canonical authoring version per Substrate semver canonical.
pub const AUTHORING_VERSION: u32 = 1; pub const AUTHORING_VERSION: u32 = 1;
@@ -925,13 +934,14 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn runtime_version_v9_benchmarking_weights() { fn runtime_version_v10_unified_identity() {
// v9 = benchmarking real machinery: los 19 extrinsics de los 5 pallets // v10 = DC ORIGIN IDENTITY UNIFICATION V1: derive_did usa el contexto KDF
// custom pasan de weights placeholder (proof_size=0, DoS) a weights INTERIM // COMPARTIDO `origin/identity/v1` → la misma pubkey Ed25519 produce el
// hand-derived DB-aware vía WeightInfo. Los weights son consenso-relevantes // MISMO identificador de 32 bytes en chain y en OCF. Cambia la transición
// (fee + llenado de bloque) ⇒ spec bump. Las FIRMAS de los extrinsics NO // de estado de register_agent_did ⇒ spec bump. Las FIRMAS de los extrinsics
// cambian ⇒ TRANSACTION_VERSION intacto. // NO cambian ⇒ TRANSACTION_VERSION intacto.
assert_eq!(RUNTIME_VERSION, 9); // (v9 fue la maquinaria de benchmarking: weights INTERIM DB-aware.)
assert_eq!(RUNTIME_VERSION, 10);
assert_eq!(TRANSACTION_VERSION, 1); assert_eq!(TRANSACTION_VERSION, 1);
} }