Skip to content

GT-588: el cable de firma — toda decisión asentada emite un statement verificable - #92

Merged
beyondnetPeru merged 2 commits into
developfrom
feat/gt-588-signing-wire
Aug 1, 2026
Merged

GT-588: el cable de firma — toda decisión asentada emite un statement verificable#92
beyondnetPeru merged 2 commits into
developfrom
feat/gt-588-signing-wire

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

La maquinaria de firma ya existía y estaba probada. Lo que no existía era nada que la invocara: AuditService recibía el grabador como parámetro opcional y new AuditService(...) no aparecía fuera de tests. La regla AUD-TRANSP castigaba un ledger que no verifica mientras nadie emitía ninguno.

Esto es ese cable, en el Tracker — donde ya viven las decisiones de compuerta y el expediente, y donde el Core no puede estar porque ADR-0101 lo define sin estado.

Cómo engancha

Un decorador de IAuditEntryRepository, no un cambio en cada llamante. Los caminos que asientan decisiones —evaluación de compuerta, registro de aprobación, publicación de decisión, turno de agente— ya pasan todos por esa interfaz. Enchufar ahí firma los cuatro sin tocar ninguno, y firma también el quinto que alguien añada mañana sin acordarse de esta ficha.

Lo que el decorador no hace: tumbar la operación si la firma falla. Una decisión de gobierno ya tomada no puede perderse porque el volumen del ledger no esté montado — eso convertiría una garantía de auditoría en un punto de caída del producto. La ausencia se nota después y con dientes: AUD-TRANSP-01 pone en rojo un ledger inexistente o vacío.

La parte difícil: interoperabilidad entre dos lenguajes

El Tracker firma en C# y quien verifica es evolith audit verify, escrito en TypeScript. Entre ambos hay cuatro capas donde un solo byte de diferencia lo rompe todo:

Capa Cómo se hizo coincidir
JSON canónico Claves ordenadas en toda profundidad, StringComparer.Ordinal para que otro locale no reordene, escapado relajado para igualar a JSON.stringify
CBOR determinista CborConformanceMode.Canonical, que ordena las entradas de mapa por los bytes de la clave — igual que su codificador
COSE_Sign1 Cabecera protegida con claims CWT iss/sub, vds=1, payload desacoplado en el recibo
Merkle RFC 9162 Prefijos de dominio 0x00/0x01, y la misma k como mayor potencia de dos

Verificación: contra el CLI real, no contra estructuras C#

Los tests generan un ledger y se lo dan al evolith audit verify de verdad:

Entries     5
Anchors     anchored
Tree head   63d2f5c4...
Receipts    verify
Verdict     PASS

Y las dos direcciones que importan más ponen el ledger en rojo: editar un veredicto sin volver a firmar, y borrar una entrada del medio — cada línea restante sigue firmada y verifica por sí sola; lo que no cuadra es la raíz del árbol, que es exactamente para lo que el árbol está.

Un hallazgo por el camino. La primera versión firmaba con CreateDevelopment y esperaba verde. El CLI reportó Receipts verify —la interoperabilidad era correcta a la primera— y aun así Verdict FAIL, porque AUD-TRANSP-04 rechaza una clave que el proceso se acuñó a sí mismo. La regla tenía razón y mi expectativa no. Quedó como test propio.

Desactivado por defecto, y exigente cuando se activa

  • Sin semillas de clave, el arranque falla en vez de caer a una clave de desarrollo: eso produciría un ledger que parece firmado y no prueba nada — peor que no tenerlo, porque induce confianza.
  • El Issuer y el Servicio de Transparencia no pueden compartir clave. RFC 9943 sitúa esa autoridad en una entidad separada; con una sola, el recibo es una autoafirmación.

Dependencias

Solo NSec (Ed25519, que .NET no trae y el verificador exige). System.Formats.Cbor resultó venir ya en .NET 10, así que se quitó.

Alcance

Cierra el criterio 1 de GT-588. Los criterios 2 y 3 ya estaban (ver beyondnetcode/evolith_arch32#375).

Suite completa: 1160/1160 con Postgres.

🤖 Generated with Claude Code

beyondnetPeru and others added 2 commits August 1, 2026 12:38
…un statement verificable (GT-588)

La maquinaria de firma existia y estaba probada. Lo que no existia era nada que la
invocara: `AuditService` recibia el grabador como parametro OPCIONAL y
`new AuditService(...)` no aparecia fuera de tests. La regla `AUD-TRANSP` castigaba un
ledger que no verifica mientras nadie emitia ninguno.

Esto es ese cable, en el Tracker — que es donde ya viven las decisiones de compuerta y
el expediente, y donde el Core no puede estar porque ADR-0101 lo define sin estado.

COMO ENGANCHA. Un DECORADOR de `IAuditEntryRepository`, no un cambio en cada llamante.
Los caminos que asientan decisiones —evaluacion de compuerta, registro de aprobacion,
publicacion de decision, turno de agente— ya pasan todos por esa interfaz. Enchufar ahi
firma los cuatro sin tocar ninguno, y firma tambien el quinto que alguien anada manana
sin acordarse de esta ficha.

LO QUE EL DECORADOR NO HACE: tumbar la operacion si la firma falla. Una decision de
gobierno ya tomada no puede perderse porque el volumen del ledger no este montado; eso
convertiria una garantia de auditoria en un punto de caida del producto. La ausencia se
nota despues y con dientes — `AUD-TRANSP-01` pone en rojo un ledger inexistente o vacio.

INTEROPERABILIDAD, que es lo dificil. El Tracker firma en C# y quien verifica es
`evolith audit verify`, escrito en TypeScript. Entre ambos hay cuatro capas donde un
solo byte de diferencia lo rompe todo: JSON canonico (claves ordenadas en toda
profundidad, `StringComparer.Ordinal` para que un locale distinto no reordene),
CBOR determinista (`CborConformanceMode.Canonical`, que ordena las entradas de mapa por
los bytes de la clave igual que su codificador), cabeceras COSE_Sign1 con los claims CWT
`iss`/`sub` y `vds`=1, y el arbol de Merkle SHA-256 de RFC 9162.

VERIFICADO CONTRA EL CLI REAL, no contra estructuras C#. Los tests generan un ledger y
se lo dan a `evolith audit verify`: veredicto PASS, `Receipts verify`. Y las dos
direcciones que importan mas — editar un veredicto sin volver a firmar, y borrar una
entrada del MEDIO (cada linea restante sigue firmada; lo que no cuadra es la raiz del
arbol, que es para lo que el arbol esta) — ponen el ledger en rojo.

Un hallazgo por el camino: la primera version firmaba con `CreateDevelopment` y esperaba
verde. El CLI reporto `Receipts verify` —la interoperabilidad era correcta a la primera—
y aun asi `Verdict FAIL`, porque `AUD-TRANSP-04` rechaza una clave que el proceso se
acuno a si mismo. La regla tenia razon y la expectativa no. Queda como test propio.

DESACTIVADO POR DEFECTO. Activarlo cambia lo que el producto promete sobre su propio
expediente y exige material de clave explicito: sin semillas el arranque FALLA en vez de
caer a una clave de desarrollo, que produciria un ledger que parece firmado y no prueba
nada — peor que no tenerlo, porque induce confianza. Y el Issuer y el Servicio de
Transparencia no pueden compartir clave: RFC 9943 situa esa autoridad en una entidad
separada, y con una sola el recibo es una autoafirmacion.

Sin dependencias nuevas salvo NSec (Ed25519, que .NET no trae y el verificador exige).
`System.Formats.Cbor` resulto venir ya en .NET 10.

Verificado: 4 tests de interoperabilidad contra el CLI real, 5 de cableado, y la suite
completa en 1160/1160 con Postgres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ADR T-056

DOS COSAS, y la primera es un fallo que introduje hoy.

1) EL FALSO VERDE. `TransparencyInteropTests` hacia `return` cuando no encontraba el
   CLI del Core, y el job `Backend` de CI solo hace checkout del Tracker. Es decir:
   las cuatro pruebas que justifican todo el cable de firma se saltaban en el
   pipeline y este reportaba verde sin haber comprobado una sola firma. Escribi el
   comentario diciendo que saltar sin la otra mitad presente es el falso verde que
   este gap existe para cerrar, y acto seguido hice exactamente eso.

   Ahora FALLAN. Solo hay dos desenlaces: verde porque se verifico, o rojo.

   Y una ruta EXPLICITA que no existe tambien falla, en vez de caer al descubrimiento
   automatico. Lo descubri escribiendo la prueba de este mismo arreglo: apunte
   `EVOLITH_CLI_MAIN` a una ruta inexistente esperando rojo y salio VERDE, porque el
   fallback encontro el CLI de mi maquina. Verificar contra un CLI distinto del que
   se pidio es peor que no verificar, porque el resultado parece valido.

   Se anade el job `transparency-interop`, que hace checkout de ambos repositorios,
   construye el CLI del Core y comprueba que expone `audit verify` ANTES de correr
   los tests — asi un CLI construido pero incompleto da un mensaje claro en vez de un
   fallo de proceso dentro de una asercion. Va en job propio porque construir el Core
   es caro y no tiene por que frenar al resto; `Backend` excluye la categoria.

   Verificadas las tres condiciones a mano: sin CLI da 4/4 en rojo, con CLI 4/4 en
   verde, y el filtro deja el job rapido en 1156 en vez de 1160.

2) ADR T-056 — las tres capas, ratificadas hoy al decidir donde vive la firma:

   · El sellado no lleva logica ni opinion. `payload` es opaco; lo unico que se fija
     es la regla de ESCRITURA. Por eso la interoperabilidad C#/TypeScript NO crea un
     problema de estandarizacion por tenant: un notario sella documentos distintos con
     un procedimiento invariable.
   · La validacion de contenido la configura el tenant. En cuanto la semantica de un
     cliente llega al motor como codigo, el motor es un catalogo de casos particulares
     y cada cliente nuevo es una release.
   · La IA propone, nunca decide. Un decisor probabilistico vuelve la garantia
     incomprobable: dos ejecuciones podrian diferir y nadie sabria cual vale.

   Se registra porque el error que previene es uno que un ingeniero razonable cometeria
   a proposito, creyendo que simplifica.

Inventario regenerado (56 decisiones, 50 tablas — la tabla de aristas de GT-605 entro
en el mapa de esquemas). `validate-docs` en verde sobre 357 ficheros.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@beyondnetPeru
beyondnetPeru merged commit e176f3c into develop Aug 1, 2026
6 checks passed
@beyondnetPeru
beyondnetPeru deleted the feat/gt-588-signing-wire branch August 1, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant