diff --git a/DECISIONS.es.md b/DECISIONS.es.md index 70652f83..8bb77d79 100644 --- a/DECISIONS.es.md +++ b/DECISIONS.es.md @@ -73,3 +73,4 @@ _Nota: Las decisiones universales se heredan del Upstream Base ([evolith_arch32] | T-054 | Gate de frontera en tiempo de edición: adoptar acotado, diferido a EAG-11 | Definir | Accepted | [T-054](./docs/adrs/T-054-edit-time-gate-adoption.es.md) | Adopta el edit-gate del Core pero DIFERIDO: `.claude/` está en `.gitignore`, `EAG-11` aún no da la fuente única de reglas, y el matcher puede misfirear. Activación condicionada a `EAG-11` + versionar `.claude/settings.json` + acotar rutas + vía de escape. | | T-055 | El Core deposita sus veredictos; el Tracker posee el ledger y deriva el tenant de la clave | Definir | Accepted | [T-055](./docs/adrs/T-055-core-initiated-evidence-ingest.es.md) | Segunda dirección del tráfico de evidencia: `POST /core-evaluation-transactions` autenticado por clave de máquina atada al esquema POR NOMBRE, permiso propio `:ingest` SIN `:read`, tenant derivado de QUÉ CLAVE encajó (un `tenantId` en el cuerpo se rechaza con 400, no se ignora), idempotencia por `(tenant, correlationId)` respaldada por índice único, motor de cada regla VERBATIM (vocabulario abierto) y los dos responsables —quien pidió y quien debe arreglar— en columnas distintas. Estado `ingested`, distinto de `completed`. El DTO derivado a mano cumple `T-038` con guarda de deriva por fixture. Sigue siendo advisory (`T-039`). Cierra `GT-604`. | | T-056 | El sellado no lleva lógica, la validación de contenido es del tenant, y la IA propone pero nunca decide | Definir | Accepted | [T-056](./docs/adrs/T-056-three-layer-separation.es.md) | Tres capas con frontera dura, ratificadas al cablear `GT-588`. **(1) El sellado no mira el contenido:** `payload` es opaco y lo único que se fija es la regla de ESCRITURA (claves ordenadas en toda profundidad, comparación ordinal para que el idioma de la máquina no las reordene, una codificación). Por eso la interoperabilidad C#/TypeScript no crea un problema de estandarización por tenant — un notario sella documentos distintos con un procedimiento invariable. **(2) La validación de contenido la configura el tenant:** en cuanto la semántica de un cliente llega al motor como código, el motor pasa a ser un catálogo de casos particulares y cada cliente nuevo es una release. Misma frontera que `T-039` aplicada al contenido. **(3) La IA propone, nunca decide:** un decisor probabilístico vuelve la garantía incomprobable, porque dos ejecuciones podrían diferir y nadie sabría cuál vale; detrás tiene que haber un verificador determinista y, si la propuesta es probabilística, su confianza se muestra a quien la ratifica (`GT-590`, `GT-584`). | +| T-057 | El tenant firma lo que afirma, la plataforma atestigua que quedó registrado | Definir | Accepted | [T-057](./docs/adrs/T-057-signing-identities.es.md) | Dos identidades de firma, y sólo una es del tenant. **La clave de ISSUER es del tenant** y vive en su configuración, junto a la matriz de compuerta y el overlay de artefactos — es su propia afirmación, así que es su clave. Una clave por tenant hace la custodia más difícil, no más fácil (N que guardar y rotar), y ese coste se acepta porque es lo que hace independiente a cada tenant. **La clave del SERVICIO DE TRANSPARENCIA se queda en la plataforma:** RFC 9943 sitúa esa autoridad en una entidad SEPARADA, y `MerkleTransparencyService` ya explica por qué — un statement y un recibo firmados con la misma clave son una autoafirmación con ceremonia, así que un tenant firmando su propio recibo podría reescribir su historia y volver a firmarla. **Ninguna de las dos claves hace inalterable el rastro:** eso es la tabla de auditoría append-only (`MakeAuditEntriesAppendOnly`); la firma añade procedencia atribuible, no inmutabilidad. `AUD-TRANSP-04` ya se niega a llamarlo no-repudio cuando ambas identidades resuelven a la misma clave. | diff --git a/DECISIONS.md b/DECISIONS.md index 4db1bce1..53dc1b5d 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -81,5 +81,6 @@ _Nota: Las decisiones universales se heredan del Upstream Base ([evolith_arch32] | T-055 | El Core deposita sus veredictos; el Tracker posee el ledger y deriva el tenant de la clave | Definir | Accepted | [T-055](./docs/adrs/T-055-core-initiated-evidence-ingest.md) | Segunda dirección del tráfico de evidencia: `POST /core-evaluation-transactions` autenticado por clave de máquina atada al esquema POR NOMBRE, permiso propio `:ingest` SIN `:read`, tenant derivado de QUÉ CLAVE encajó (un `tenantId` en el cuerpo se rechaza con 400, no se ignora), idempotencia por `(tenant, correlationId)` respaldada por índice único, motor de cada regla VERBATIM (vocabulario abierto) y los dos responsables —quien pidió y quien debe arreglar— en columnas distintas. Estado `ingested`, distinto de `completed`. El DTO derivado a mano cumple `T-038` con guarda de deriva por fixture. Sigue siendo advisory (`T-039`). Cierra `GT-604`. | | T-056 | El sellado no lleva lógica, la validación de contenido es del tenant, y la IA propone pero nunca decide | Definir | Accepted | [T-056](./docs/adrs/T-056-three-layer-separation.md) | Tres capas con frontera dura, ratificadas al cablear `GT-588`. **(1) El sellado no mira el contenido:** `payload` es opaco y lo único que se fija es la regla de ESCRITURA (claves ordenadas en toda profundidad, comparación ordinal para que el idioma de la máquina no las reordene, una codificación). Por eso la interoperabilidad C#/TypeScript no crea un problema de estandarización por tenant — un notario sella documentos distintos con un procedimiento invariable. **(2) La validación de contenido la configura el tenant:** en cuanto la semántica de un cliente llega al motor como código, el motor pasa a ser un catálogo de casos particulares y cada cliente nuevo es una release. Misma frontera que `T-039` aplicada al contenido. **(3) La IA propone, nunca decide:** un decisor probabilístico vuelve la garantía incomprobable, porque dos ejecuciones podrían diferir y nadie sabría cuál vale; detrás tiene que haber un verificador determinista y, si la propuesta es probabilística, su confianza se muestra a quien la ratifica (`GT-590`, `GT-584`). | +| T-057 | The tenant signs what it asserts, the platform attests that it was recorded | Definir | Accepted | [T-057](./docs/adrs/T-057-signing-identities.md) | Two signing identities, and only one belongs to the tenant. **The ISSUER key is the tenant's** and lives in tenant configuration, alongside the gate matrix and the artifact overlay — it is the tenant's own claim, so it is the tenant's key. Per-tenant keys make custody harder, not easier (N to store and rotate), and that cost is accepted because it is what makes each tenant independent. **The TRANSPARENCY SERVICE key stays with the platform:** RFC 9943 puts that authority in a SEPARATE entity, and `MerkleTransparencyService` already says why — a statement and a receipt signed with the same key are a self-assertion with ceremony, so a tenant signing its own receipt could rewrite its history and re-sign it. **Neither key is what makes the trail immutable:** that is the append-only audit table (`MakeAuditEntriesAppendOnly`); the signature adds attributable provenance, not immutability. `AUD-TRANSP-04` already refuses to call it non-repudiation when both identities resolve to the same key. | > **Plantilla para nuevos ADRs:** Al crear un nuevo documento para "ADR Local", utilice el esquema de Frontmatter definido en los estándares de Evolith Core y ubíquelo en la carpeta de gobernanza correspondiente. diff --git a/README.es.md b/README.es.md index f3191938..4afded3d 100644 --- a/README.es.md +++ b/README.es.md @@ -8,7 +8,7 @@ [![Plataforma](https://img.shields.io/badge/.NET_10_%7C_PostgreSQL_%7C_React_19-informational?style=for-the-badge)]() [![Arquitectura](https://img.shields.io/badge/Evolith-Satélite-blueviolet?style=for-the-badge)](https://github.com/beyondnetcode/evolith_arch32) -[![ADRs](https://img.shields.io/badge/ADRs-56_decisiones-orange?style=for-the-badge)](./DECISIONS.md) +[![ADRs](https://img.shields.io/badge/ADRs-57_decisiones-orange?style=for-the-badge)](./DECISIONS.md) [![Licencia](https://img.shields.io/badge/Licencia-Dual_License-informational?style=for-the-badge)](./LICENSE) diff --git a/README.md b/README.md index ff9d7f2a..0eb12ac6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Platform](https://img.shields.io/badge/.NET_10_%7C_PostgreSQL_%7C_React_19-informational?style=for-the-badge)]() [![Architecture](https://img.shields.io/badge/Evolith-Satellite_Product-blueviolet?style=for-the-badge)](https://github.com/beyondnetcode/evolith_arch32) -[![ADRs](https://img.shields.io/badge/ADRs-56_decisions-orange?style=for-the-badge)](./DECISIONS.md) +[![ADRs](https://img.shields.io/badge/ADRs-57_decisions-orange?style=for-the-badge)](./DECISIONS.md) [![License](https://img.shields.io/badge/License-Dual_License-informational?style=for-the-badge)](./LICENSE) diff --git a/docs/adrs/T-057-signing-identities.es.md b/docs/adrs/T-057-signing-identities.es.md new file mode 100644 index 00000000..20cbb1ca --- /dev/null +++ b/docs/adrs/T-057-signing-identities.es.md @@ -0,0 +1,88 @@ +--- +adr: T-057 +title: El tenant firma lo que afirma, la plataforma atestigua que quedó registrado +status: Accepted +date: 2026-08-03 +tags: [EvolithSatellite, transparency, multi-tenancy, security, governance] +authority: Decisión de producto, ratificada el 2026-08-03 al resolver la custodia de clave de GT-588 +relates: [T-056 separación de tres capas, core/GT-588 la procedencia se registra pero sin firmar] +gaps: [GT-588] +--- + +# ADR T-057 — Dos identidades de firma, y sólo una es del tenant + +## Status + +Aceptada (2026-08-03). Ratificada por el dueño de producto, que propuso que **cada tenant sea +responsable de su propio mecanismo de firma, en la zona de configuración de tenant**. Esa propuesta +se adopta para una de las dos identidades y se rechaza deliberadamente para la otra, y este +registro existe porque esa diferencia es todo el valor de la firma. + +## Contexto + +`GT-588` cableó un firmante Ed25519 y un servicio de transparencia Merkle, y dejó una pregunta +abierta: dónde vive la clave privada. La respuesta del dueño —es del tenant— es coherente con +[T-056](./T-056-three-layer-separation.es.md): el tenant configura, el motor no lo cablea. + +Pero RFC 9943 nombra **dos** identidades, y `MerkleTransparencyService` ya las lleva como campos +distintos con un comentario que explica por qué: + +> Un statement y un recibo producidos por el mismo proceso con la misma clave son una +> autoafirmación con ceremonia añadida: detectan accidentes y ediciones a posteriori, y NO +> establecen no-repudio frente a quien los produjo. + +Si ambas claves fueran del tenant, un tenant podría reescribir su propia historia y volver a +firmarla. La firma seguiría detectando una edición accidental; dejaría de probar nada frente a un +tercero — que es el caso que el dueño dijo que le importa. + +## Decisión + +### 1. La clave de ISSUER es del tenant y vive en su configuración + +El Issuer afirma «este tenant aprobó esta compuerta». Es su propia afirmación, así que es su clave +y su custodia es su responsabilidad. Va en la superficie de configuración de tenant, junto a la +matriz de compuerta (`CoreSignalRule`) y el overlay de artefactos (`ArtifactApplicabilityRule`) — +la misma forma, por la misma razón. + +Una clave por tenant hace la custodia **más difícil**, no más fácil: N claves que guardar y rotar +en vez de una. Ese coste se acepta a propósito, porque es lo que hace independiente a cada tenant. + +### 2. La clave del SERVICIO DE TRANSPARENCIA es de la plataforma, nunca del tenant + +El Servicio de Transparencia atestigua «esto quedó registrado y el log no se ha alterado». Un +tenant firmando su propio recibo es un tenant respondiendo por sí mismo. Esta clave se queda en la +plataforma. + +### 3. Ninguna de las dos claves es lo que hace inalterable el rastro + +Esa garantía es la tabla de auditoría append-only (`MakeAuditEntriesAppendOnly`), aplicada en la +base de datos. La firma añade **procedencia atribuible**, no inmutabilidad. Decirlo en voz alta +evita el error de tratar como poco fiable algo sin firmar pero append-only, o como prueba algo +firmado pero mutable. + +## Consecuencias + +- La superficie de configuración de tenant pasa a manejar material de clave. Nunca debe aceptar una + clave privada en un cuerpo que se devuelve al llamante, y un tenant no debe poder leer la de otro + — la misma frontera que ya sostiene cualquier otro ajuste por tenant. +- Un despliegue que rellene las dos identidades con la misma clave es un despliegue de DESARROLLO. + La regla `AUD-TRANSP-04` ya existe para notarlo y negarse a llamarlo no-repudio; este ADR es lo + que esa regla aplica. +- Hasta que existan claves de Issuer por tenant, el rastro lo firma la plataforma para todos. Eso + es más débil que esta decisión, y es la lectura honesta del estado de hoy — no un detalle + temporal que se deje sin documentar. + +## Validación + +- `AUD-TRANSP-04` falla cuando Issuer y Servicio de Transparencia resuelven a la misma clave. +- `TransparencyWiringTests` prueba que la firma se invoca y que activarla es una decisión de + despliegue explícita, no un valor por defecto. +- `TransparencyInteropTests` prueba que un statement firmado en C# se verifica desde TypeScript, + así que las identidades son separables por un consumidor y no sólo por nosotros. + +## Referencias + +- RFC 9943 (arquitectura SCITT) · RFC 9052 (COSE) · RFC 9597 (claims CWT en cabeceras COSE) +- `src/apps/tracker-api/Tracker.Infrastructure/Transparency/MerkleTransparencyService.cs` +- [T-056](./T-056-three-layer-separation.es.md) — el tenant configura, el motor no +- `GT-588` en `evolith_arch32` — la procedencia se registra pero sin firmar diff --git a/docs/adrs/T-057-signing-identities.md b/docs/adrs/T-057-signing-identities.md new file mode 100644 index 00000000..b13bab67 --- /dev/null +++ b/docs/adrs/T-057-signing-identities.md @@ -0,0 +1,87 @@ +--- +adr: T-057 +title: The tenant signs what it asserts, the platform attests that it was recorded +status: Accepted +date: 2026-08-03 +tags: [EvolithSatellite, transparency, multi-tenancy, security, governance] +authority: Product decision, ratified 2026-08-03 while deciding GT-588 key custody +relates: [T-056 three-layer separation, core/GT-588 provenance is recorded but unsigned] +gaps: [GT-588] +--- + +# ADR T-057 — Two signing identities, and only one of them belongs to the tenant + +## Status + +Accepted (2026-08-03). Ratified by the product owner, who proposed that **each tenant be +responsible for its own signing mechanism, configured in the tenant configuration area**. That +proposal is adopted for one of the two identities and deliberately refused for the other, and this +record exists because the difference is the entire value of the signature. + +## Context + +`GT-588` wired an Ed25519 signer and a Merkle transparency service, and left one question open: +where does the private key live. The owner's answer — the tenant owns it — is coherent with +[T-056](./T-056-three-layer-separation.md): the tenant configures, the engine does not hardwire. + +But RFC 9943 names **two** identities, and `MerkleTransparencyService` already carries them as +separate fields with a comment saying why: + +> A statement and a receipt produced by the same process with the same key are a self-assertion +> with added ceremony: they detect accidents and after-the-fact edits, and do NOT establish +> non-repudiation against whoever produced them. + +If both keys were the tenant's, a tenant could rewrite its own history and re-sign it. The +signature would still detect an accidental edit; it would stop proving anything to a third party — +which is the case the owner said matters. + +## Decision + +### 1. The ISSUER key is the tenant's, and lives in tenant configuration + +The Issuer asserts *"this tenant approved this gate"*. It is the tenant's own claim, so it is the +tenant's key and its custody is the tenant's responsibility. It belongs in the tenant configuration +surface alongside the gate matrix (`CoreSignalRule`) and the artifact overlay +(`ArtifactApplicabilityRule`) — the same shape, for the same reason. + +Per-tenant keys make custody harder, not easier: N keys to store and rotate rather than one. That +cost is accepted deliberately, because it is what makes each tenant independent. + +### 2. The TRANSPARENCY SERVICE key is the platform's, and never the tenant's + +The Transparency Service attests *"this was recorded, and the log has not been altered"*. A tenant +signing its own receipt is a tenant vouching for itself. This key stays with the platform. + +### 3. Neither key is what makes the trail immutable + +That guarantee is the append-only audit table (`MakeAuditEntriesAppendOnly`), enforced at the +database. The signature adds **attributable provenance**, not immutability. Saying so out loud +prevents the mistake of treating unsigned-but-append-only as untrustworthy, or +signed-but-mutable as proof. + +## Consequences + +- The tenant configuration surface gains key material. It must never accept a private key in a + request body that is echoed back, and a tenant must not be able to read another tenant's — the + same boundary every other per-tenant setting already holds. +- A deployment that fills both identities with the same key is a DEVELOPMENT deployment. Rule + `AUD-TRANSP-04` already exists to notice that and refuse to call it non-repudiation; this ADR is + what that rule is enforcing. +- Until per-tenant Issuer keys exist, the trail is signed by the platform for everyone. That is + weaker than this decision, and it is the honest reading of today's state — not a temporary + detail to leave undocumented. + +## Validation + +- `AUD-TRANSP-04` fails when Issuer and Transparency Service resolve to the same key. +- `TransparencyWiringTests` proves the signing is invoked and that enabling it is an explicit + deployment decision rather than a default. +- `TransparencyInteropTests` proves a C#-signed statement verifies from TypeScript, so the + identities are separable by a consumer and not only by us. + +## References + +- RFC 9943 (SCITT architecture) · RFC 9052 (COSE) · RFC 9597 (CWT claims in COSE headers) +- `src/apps/tracker-api/Tracker.Infrastructure/Transparency/MerkleTransparencyService.cs` +- [T-056](./T-056-three-layer-separation.md) — the tenant configures, the engine does not +- `GT-588` in `evolith_arch32` — provenance is recorded but unsigned