Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DECISIONS.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
1 change: 1 addition & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<!-- BEGIN GENERATED: adr-count — derivado por .harness/scripts/doc-inventory.mjs; NO editar a mano -->
[![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)
<!-- END GENERATED: adr-count -->
[![Licencia](https://img.shields.io/badge/Licencia-Dual_License-informational?style=for-the-badge)](./LICENSE)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<!-- BEGIN GENERATED: adr-count — derivado por .harness/scripts/doc-inventory.mjs; NO editar a mano -->
[![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)
<!-- END GENERATED: adr-count -->
[![License](https://img.shields.io/badge/License-Dual_License-informational?style=for-the-badge)](./LICENSE)

Expand Down
88 changes: 88 additions & 0 deletions docs/adrs/T-057-signing-identities.es.md
Original file line number Diff line number Diff line change
@@ -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
87 changes: 87 additions & 0 deletions docs/adrs/T-057-signing-identities.md
Original file line number Diff line number Diff line change
@@ -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
Loading