# Especificación pública `deca-enunciado-v1` — verificación independiente de firmas DeCA

**Versión:** 1 (prefijo `deca-enunciado-v1\n`).
**Estado:** normativa para todas las firmas de la era de DATO emitidas por DeCA.
**Propósito:** que cualquier tercero (un juez, un perito, una de las partes) pueda
verificar una firma DeCA **sin ningún software de DeCA**, solo con herramientas
estándar (`openssl`, `node`). Si DeCA cesa su actividad, la verificabilidad de la
evidencia no muere con él: este documento, los artefactos de la firma (campos del
enunciado, JWS compacta, certificado del firmante) y las raíces públicas de la CA
bastan.

**Denominación legal:** lo aquí descrito es una **firma electrónica avanzada
(art. 26 del Reglamento (UE) 910/2014, eIDAS), JWS con estructura basada en
JAdES**. Este documento NO afirma conformidad con ninguna norma técnica ETSI;
las especificaciones ETSI (p. ej. TS 119 182-1) se citan únicamente como
referencia de inspiración estructural del formato, nunca como claim de
conformidad.

Fuentes de las que deriva esta especificación (el código es la norma; si este
documento divergiera del código, prevalece el código y el test
`aws-stack/go/internal/adess/especificacion_test.go` debe delatarlo):

- `aws-stack/go/internal/domain/enunciado.go` (ensamblado y orden de campos)
- `packages/contracts/enunciado.js` (gemelo JS del ensamblador)
- `aws-stack/go/internal/adess/externa.go` y `aws-stack/go/internal/adess/adess.go`
  (cabecera JWS, signing input, verificación, eras)

---

## 1. Artefactos de una firma

Una firma DeCA de la era de DATO se compone de:

| Artefacto | Descripción |
|---|---|
| `campos` | Lista ORDENADA de pares `{k, v}` (clave y valor, cadenas UTF-8) que el servidor computó y el firmante vio y firmó (WYSIWYS). Es lo que devuelve el endpoint `…/preparar` en `campos` y lo que DeCA exporta como evidencia. |
| `jades` | La JWS compacta *detached*: `headerB64 + ".." + firmaB64` (el segmento de payload va VACÍO). |
| `certPem` | El certificado X.509 del firmante (PEM), emitido por la CA de DeCA sobre la clave pública del dispositivo/navegador del firmante. Desde julio de 2026 porta una extensión `certificatePolicies` **no crítica** (política `2.25.95038404504261308680685668407606640840` —rama UUID de ITU-T X.667— desde el 24-sep-2026, `1.3.6.1.4.1.99999.1.1` en los emitidos antes; + `userNotice` con las limitaciones de uso, art. 13.2 eIDAS) y una vigencia de 1 hora; los certificados de eras anteriores no la llevan y verifican igual. |
| raíces de la CA | Certificados raíz públicos, por eras: `GET {base}/api/v1/ca/raices` (§6). |

Notación: `b64url(x)` es base64url **sin relleno** (RFC 4648 §5, sin `=`);
`SHA256(x)` son los 32 bytes crudos del digest.

## 2. Ensamblado canónico del enunciado

Los bytes canónicos del enunciado se construyen así
(`EnsamblarEnunciado`, `aws-stack/go/internal/domain/enunciado.go`):

```
enunciado = UTF8("deca-enunciado-v1\n")
            ∥ para cada campo (k, v), en el orden dado:
                uvarint(len(UTF8(k))) ∥ UTF8(k) ∥ uvarint(len(UTF8(v))) ∥ UTF8(v)
```

- `uvarint` es el varint sin signo LEB128 de Go (`binary.PutUvarint`): 7 bits
  de dato por byte, bit alto = "continúa", little-endian por grupos de 7 bits.
  Para longitudes < 128 es un solo byte con el valor literal.
- `len()` es la longitud en **bytes** de la codificación UTF-8, no en
  caracteres.
- Los valores se usan **verbatim**: ningún verificador debe re-formatear,
  recortar ni normalizar un valor. Un byte de diferencia invalida la firma.
- El prefijo de longitud hace imposible la colisión por concatenación o
  reordenación; NO se usa JCS/RFC 8785 a propósito.

## 3. Orden fijo de campos (`CamposEnunciado`)

El orden es parte del contrato. Para un documento con `n` matrículas:

| # | clave | valor |
|---|---|---|
| 1 | `docUUID` | UUID del documento |
| 2 | `rev` | revisión firmada, entero decimal (`strconv.Itoa`) |
| 3 | `nucleoHash` | SHA-256 hex (minúsculas) del núcleo congelado (§3.1) |
| 4 | `ambito` | `""` (≡ nacional), `nacional` o `internacional` — determina el régimen legal aplicable |
| 5 | `origen.via` | vía del origen |
| 6 | `origen.municipio` | municipio del origen |
| 7 | `origen.codigoPostal` | código postal del origen |
| 8 | `origen.pais` | país del origen, ISO 3166-1 alfa-2 |
| 9 | `destino.via` | vía del destino |
| 10 | `destino.municipio` | municipio del destino |
| 11 | `destino.codigoPostal` | código postal del destino |
| 12 | `destino.pais` | país del destino, ISO 3166-1 alfa-2 |
| 13 | `naturalezaMercancia` | naturaleza de la mercancía |
| 14 | `peso.valor` | peso, formateado según §3.2 |
| 15 | `peso.unidad` | unidad del peso |
| 16 | `fechaTransporte` | fecha del transporte |
| 17 | `matriculas.n` | número de matrículas, entero decimal |
| 18…| `matricula.0` … `matricula.{n-1}` | cada matrícula, índice desde 0 |
| 19 | `precio` | precio del transporte (vacío si el documento no lleva carta de porte) |
| 20 | `gastosPrevisibles` | gastos previsibles (ídem) |
| 21 | `condicionesPago` | condiciones de pago: porte pagado o debido (ídem) |
| 22 | `cargaEn` | lugar y fecha de carga (ídem) |
| 23 | `descargaEn` | lugar y fecha de descarga (ídem) |
| 24 | `bultos` | número y clase de bultos — art. 10.1.e Ley 15/2009 (ídem) |
| 25 | `signosIdentificacion` | marcas y señales de identificación de los bultos — art. 10.1.e (ídem) |
| 26 | `embalaje` | forma de embalaje — art. 10.1.f (ídem) |
| 27 | `instrucciones` | instrucciones para el transporte (ídem) |
| 28 | `lugarEmision` | lugar de emisión del documento — art. 10.1.a (ídem) |
| 29 | `declaracionValor` | declaración de valor de la mercancía; eleva el techo indemnizatorio (arts. 23.6, 24 y 26 CMR) |
| 29a | `expedidor` | expedidor, si es distinto del cargador — art. 10.1.b Ley 15/2009 (ídem) |
| 29b | `terceroReceptor` | tercero que recibe la mercancía por cuenta del destinatario — art. 10.1.c (ídem) |
| 29c | `domicilioNotificaciones` | domicilio del destinatario para recibir notificaciones — art. 10.1.f (ídem) |
| 30 | `adr` | mención ADR de la carta de porte, texto libre (ídem) |
| 31 | `autorizacionEspecial` | autorización especial de circulación; vacío si no hay |
| 32 | `observaciones` | observaciones del documento; vacío si no hay |
| — | `nivelFirmas` | nivel eIDAS exigido a las firmas: `""`/`ades` (avanzada) o `qes` (cualificada) |
| 33 | `adrDetalle.numeroUn` | número ONU de la materia (ADR 5.4.1) |
| 34 | `adrDetalle.designacionOficial` | designación oficial de transporte |
| 35 | `adrDetalle.designacionOficialIngles` | designación oficial en inglés |
| 36 | `adrDetalle.clasePeligro` | clase de peligro |
| 37 | `adrDetalle.grupoEmbalaje` | grupo de embalaje: `I`, `II` o `III` |
| 38 | `adrDetalle.codigoTuneles` | código de restricción en túneles |
| 38a | `adrDetalle.cantidad` | cantidad total de la materia (ADR 5.4.1.1.1 f); vacío si hay una sola materia, en cuyo caso la da el peso |
| 53b | `adr.materias.n` | número de materias ADR ADICIONALES a `adrDetalle`, entero decimal |
| 53c… | `adr.materia.0.numeroUn` (y su equivalente en cada materia adicional `adr.materia.{i}.`) | número ONU de la materia adicional 0 |
| 53d… | `adr.materia.0.designacionOficial` (ídem) | designación oficial de transporte de la materia adicional |
| 53e… | `adr.materia.0.designacionOficialIngles` (ídem) | designación oficial en inglés |
| 53f… | `adr.materia.0.clasePeligro` (ídem) | clase de peligro |
| 53g… | `adr.materia.0.grupoEmbalaje` (ídem) | grupo de embalaje: `I`, `II` o `III` |
| 53h… | `adr.materia.0.codigoTuneles` (ídem) | código de restricción en túneles |
| 53i… | `adr.materia.0.cantidad` (ídem) | cantidad total de esa materia (ADR 5.4.1.1.1 f) |
| 54 | `cmr.instruccionesAduana` | instrucciones para las formalidades de aduana (art. 6.2 CMR) |
| 55 | `cmr.documentosEntregados` | documentos entregados al porteador |
| 56 | `cmr.reembolso` | reembolso a percibir contra entrega |
| 57 | `cmr.interesEspecialEntrega` | interés especial en la entrega (art. 26 CMR) |
| 58 | `cmr.plazoConvenido` | plazo convenido para el transporte (art. 19 CMR) |
| 59 | `cmr.intermodal` | mención de transporte intermodal |
| 59a | `cmr.prohibicionTransbordo` | prohibición de transbordo (art. 6.2.a CMR): `si` si está marcada, cadena vacía si no. El JSON del documento la lleva como booleano |
| 59b | `cmr.gastosRemitente` | gastos que el remitente toma a su cargo (art. 6.2.b) |
| 59c | `cmr.instruccionesSeguro` | instrucciones del remitente al porteador sobre el seguro de la mercancía (art. 6.2.e) |
| 60 | `cmr.riesgosEspeciales.n` | número de riesgos especiales del art. 17.4 CMR, entero decimal |
| 61… | `cmr.riesgoEspecial.0` … `cmr.riesgoEspecial.{r-1}` | cada riesgo especial declarado, índice desde 0 |
| 62 | `envios.n` | número de fracciones del grupaje, entero decimal |
| 63… | `envio.0.cargadorNif` (y su equivalente en cada fracción `envio.{i}.`) | NIF del cargador de la fracción 0 |
| 64… | `envio.0.transportistaNif` (y su equivalente en cada fracción `envio.{i}.`) | NIF del porteador de la fracción |
| 65… | `envio.0.origen.via` (y su equivalente en cada fracción `envio.{i}.`) | vía del origen de la fracción |
| 66… | `envio.0.origen.municipio` (y su equivalente en cada fracción `envio.{i}.`) | municipio del origen de la fracción |
| 67… | `envio.0.origen.codigoPostal` (y su equivalente en cada fracción `envio.{i}.`) | código postal del origen de la fracción |
| 68… | `envio.0.origen.pais` (y su equivalente en cada fracción `envio.{i}.`) | país del origen de la fracción |
| 69… | `envio.0.destino.via` (y su equivalente en cada fracción `envio.{i}.`) | vía del destino de la fracción |
| 70… | `envio.0.destino.municipio` (y su equivalente en cada fracción `envio.{i}.`) | municipio del destino de la fracción |
| 71… | `envio.0.destino.codigoPostal` (y su equivalente en cada fracción `envio.{i}.`) | código postal del destino de la fracción |
| 72… | `envio.0.destino.pais` (y su equivalente en cada fracción `envio.{i}.`) | país del destino de la fracción |
| 73… | `envio.0.naturalezaMercancia` (y su equivalente en cada fracción `envio.{i}.`) | naturaleza de la mercancía de la fracción |
| 74… | `envio.0.peso.valor` (y su equivalente en cada fracción `envio.{i}.`) | peso de la fracción, formateado según §3.2 |
| 75… | `envio.0.peso.unidad` (y su equivalente en cada fracción `envio.{i}.`) | unidad del peso de la fracción |
| 76 | `parte.razonSocial` | razón social del PROPIO firmante |
| 77 | `parte.nif` | NIF del propio firmante |
| 78 | `parte.domicilio` | domicilio del propio firmante |
| 79 | `parte.reservas` | texto de la reserva de esta parte; cadena vacía si firmó sin reservas (§3.3) |
| 80 | `parte.declaracion` | declaración responsable literal del rol (§3.4) |
| 81 | `rol` | rol del firmante: `cargador`, `transportista`, `transportista-entrega` o `destinatario` |
| 82 | `sigT` | instante de la firma en RFC 3339 UTC; es el `iat` de la cabecera JWS (§4) convertido desde NumericDate |

El bloque de parte (`parte.*`) es el del propio firmante: `cargador` →
`cargadorContractual`, `transportista` → `transportistaEfectivo`,
`destinatario` → `cartaPorte.destinatario` del cuerpo del documento. Cada
parte firma SU declaración, no las ajenas.

Todos los campos entre `ambito` y `envio.{i}.peso.unidad` son el contenido del
documento, y **se muestran íntegros en la pantalla de firma** (WYSIWYS: lo
pintado ES lo firmado). Los campos de bloques que no apliquen —ADR, CMR,
grupaje— viajan VACÍOS, no se omiten: el orden es fijo, y una
lista cuya longitud dependiera de qué hay relleno obligaría a quien verifica a
adivinar la forma. Las superficies que la pintan se saltan los valores vacíos.

El 5-sep-2026 (R-C240…R-C244) entraron las claves marcadas con letra
(`29a`…`59c`): menciones de la Ley 15/2009 y del CMR que no tenían campo, la
cantidad por materia ADR y las materias ADR adicionales. Al no existir eras
(§3.5), toda firma anterior a esa fecha deja de verificar contra el enunciado
nuevo; no había usuarios reales y el corpus de casos se regeneró.

El mismo 5-sep-2026 (R-C245) se RETIRARON las claves `residuos.*` (39…48) y
`sandach.*` (49…53a): ninguno de los dos bloques podía ser el documento oficial
que su norma exige (el DI lo emite eSIR, RD 553/2020 art. 6; el documento
comercial SANDACH alternativo es la aplicación del MAPA, RD 476/2014 art. 2.2),
así que solo obligaban a teclear dos veces. Los números de fila no se
reutilizan. Misma consecuencia sobre las firmas anteriores, misma regeneración.

Esto se corrigió el 19-ago-2026. Hasta ese día el enunciado llevaba congelado
desde el 16-jul con los campos que existían entonces, mientras el documento
seguía creciendo: 53 campos quedaban cubiertos por `nucleoHash` (§3.1) —o sea,
firmados e inalterables— pero fuera de lo que cualquier pantalla enseñaba. Entre
ellos, menciones obligatorias del art. 10 de la Ley 15/2009 y el bloque
entero de mercancía peligrosa (y los de residuos y SANDACH, hoy retirados).

### 3.1 `nucleoHash`

SHA-256 (hex, minúsculas) de la serialización canónica del **núcleo** del
cuerpo del documento (`HashNucleo`, `aws-stack/go/internal/domain/enunciado.go`):

1. Parsear el JSON del cuerpo del documento a un objeto.
2. Eliminar las claves de primer nivel `cargadorContractual`,
   `transportistaEfectivo`, `firmas` y `partes`.
3. Si existe `cartaPorte`, eliminar dentro de ella la clave `destinatario`.
4. Re-serializar con la forma canónica de `encoding/json` de Go sobre mapas:
   claves ordenadas lexicográficamente (por bytes UTF-8) en cada objeto, sin
   espacios en blanco, y con el escapado HTML por defecto de Go en las cadenas
   (los caracteres `<`, `>` y `&` se escapan como `\u003c`, `\u003e` y
   `\u0026`).
5. `nucleoHash = hex(SHA256(bytes del paso 4))`.

Nota de honestidad: el paso 4 depende de la forma canónica de Go. Para
verificar la FIRMA no hace falta recomputarlo (el valor viaja dentro de
`campos` y queda cubierto por la firma); recomputarlo solo es necesario para
la comprobación adicional de que el cuerpo del documento archivado no fue
alterado tras la firma (art. 26.4 eIDAS).

### 3.2 `peso.valor`

Única representación admitida (`FormatearPesoCanonico`):
`strconv.FormatFloat(valor, 'f', -1, 64)` de Go — decimal sin exponente, con
los mínimos dígitos que reproducen exactamente el float64 (p. ej. `24000`,
`1500.5`). El verificador nunca lo recomputa: usa el valor verbatim de
`campos`.

### 3.3 `parte.reservas` — la reserva de la propia parte

Una firma puede llevar una **reserva** de quien firma: un texto que esa parte
avala junto al resto del enunciado, de modo que quede criptográficamente atado a
SU firma y a nadie más.

Dos roles pueden reservar, y reservan cosas distintas:

- El **porteador**, al hacerse cargo de la mercancía (art. 8.2 CMR, art. 11.2 de
  la Ley 15/2009): hace constar las reservas sobre el estado aparente de la
  mercancía y su embalaje, o sobre las marcas y el número de bultos. Sin
  reservas, se presume que la recibió en buen estado (art. 9.2 CMR).
- El **destinatario**, al recibirla (art. 30 CMR, art. 26.4 de la Ley 15/2009).

El **cargador** firma este mismo campo —el enunciado tiene UNA sola forma— pero
siempre vacío: el servidor no le admite reservas, porque declarar el estado de
lo que uno mismo entrega no es una reserva.

El campo está **siempre presente**, con cadena vacía cuando se firmó sin
reservas. No hay ningún discriminador que lo omita: quien reconstruya el
enunciado inserta el texto verbatim —o la cadena vacía— en la posición 25+n.

El valor se archiva junto a la firma en el campo `reservas` y se imprime en el
PDF exhibido bajo la firma correspondiente (WYSIWYS: el texto pintado ES el
firmado).

### 3.4 `parte.declaracion` — qué afirma cada rol

Cada firma incorpora, **literalmente**, la declaración responsable del rol que
firma. No es adorno: el sello de tiempo prueba CUÁNDO se firmó y la declaración
prueba QUÉ se estaba afirmando en ese momento. Va dentro del enunciado, así que
la cubre la firma y no puede reescribirse después.

Los textos son estos, carácter a carácter:

| rol | declaración |
|---|---|
| `cargador` | Declaro bajo mi responsabilidad que la mercancía descrita en este documento ha sido cargada, y que los datos que he aportado sobre ella son ciertos y completos. |
| `transportista` | Declaro bajo mi responsabilidad que me hago cargo de la mercancía descrita en este documento, que he podido comprobar las marcas y números de los bultos y el estado aparente de la mercancía y su embalaje, y que firmo en ese momento. Cualquier reserva que haya formulado consta en este documento. |
| `destinatario` | Declaro bajo mi responsabilidad que he recibido la mercancía, que he podido comprobar su estado y su cantidad, y que firmo después de haberlo hecho. Cualquier daño o falta que haya observado consta en las reservas de este documento. |

El rol `transportista-entrega` (llegada del porteador al destino) tiene su propia
declaración, que acredita únicamente su llegada y NO la recepción de la
mercancía —esa corresponde declararla al destinatario—. Es una firma adicional y
opcional: no forma parte de las firmas imprescindibles de la carta.

**Estas declaraciones son inmutables.** Cambiar una coma cambia los bytes
firmados y deja sin verificar toda firma anterior, y a una persona que ya firmó
no se le puede volver a pedir. Cualquier retoque exige abrir una era nueva y
conservar la anterior.

### 3.5 No hay eras del enunciado

Hasta el 4-ago-2026 esta especificación describía dos eras (`v1` sin reserva y
`v2` con ella), distinguidas por un entero `enunciadoVersion` archivado junto a
la firma. **Ese modelo se retiró**: el enunciado tiene una sola forma, no existe
ningún parámetro capaz de bifurcar lo que se firma, y `enunciadoVersion` ya no
se escribe ni se lee en ninguna parte. Un verificador que encuentre ese campo en
evidencia antigua debe ignorarlo.

## 4. Cabecera JWS y qué se firma

La cabecera protegida es el JSON (serializado por el servidor; el verificador
usa el `headerB64` **verbatim**, jamás re-serializa):

```json
{"alg":"ES256","cty":"application/deca+json","iat":1783933200,"x5t#S256":"…"}
```

- `alg`: siempre `ES256` (ECDSA P-256 + SHA-256, RFC 7518 §3.4).
- `cty`: el discriminador de **era** (§5).
- `iat`: instante de la firma, **NumericDate** (RFC 7519: segundos UTC enteros
  desde el epoch). El último campo del enunciado es el MISMO instante, pero
  escrito en RFC 3339 (`sigT`, §6) porque es lo que el firmante lee antes de
  firmar; para reconstruir el enunciado se convierte:
  `new Date(iat * 1000).toISOString().replace('.000','')`.

  > **Por qué `iat` y no `sigT` (31-ago-2026).** JAdES declaró `sigT` OBSOLETO
  > para firmas posteriores al 2025-07-15. Medido contra el validador DSS de la
  > Comisión Europea: con `sigT` la firma se clasifica `JSON-NOT-ETSI`; con
  > `iat` sale `JAdES-BASELINE-B`. Y no valía llevar los dos —`iat` + `sigT`
  > juntos vuelven a `JSON-NOT-ETSI`—, así que la cabecera **no lleva `sigT`**.
  > Tampoco lleva `crit`: existía para declarar `sigT` como crítico, y `iat`
  > está registrado en RFC 7519.
  >
  > **LEGADO — obligatorio para verificar el pasado.** Todas las firmas
  > emitidas ANTES del 31-ago-2026 llevan `sigT` (RFC 3339, string) en lugar
  > de `iat`. Un verificador conforme DEBE aceptar ambas formas: usa `iat` si
  > está presente; en su ausencia, `sigT` legado. (La omisión de esta regla
  > rompió EN VIVO la verificación de todo lo emitido el mismo día del cambio:
  > las firmas históricas pasaron a «NO verifica» en el verificador público
  > hasta restaurar la lectura del legado.)
- `x5t#S256`: `b64url(SHA256(DER del certificado del firmante))` — ata la
  cabecera al certificado (anti-sustitución).
- `x5c` (desde 30-ago-2026): cadena de certificados `[firmante, CA]` en DER
  **base64 estándar** (RFC 7515 §4.1.6 — es el único miembro del JOSE header
  que NO usa base64url). Hace la JAdES **autocontenida**: un validador externo
  (p. ej. el DSS de la Comisión Europea) puede verificarla sin recibir el
  certificado por otro canal. `x5c[0]` DEBE ser el mismo certificado que
  referencia `x5t#S256` (el empaquetado lo rechaza si no casan). Las firmas
  anteriores a esta fecha no lo llevan y verifican igual (el certificado viaja
  junto a la firma, campo `certPem`).
- `trazoS256` (opcional): SHA-256 hex del path SVG del trazo manuscrito. Va en
  la cabecera PROTEGIDA (firmada), así que el trazo queda ATADO a la firma. Lo
  llevan tanto las firmas de la era PDF como las de la era de DATO con trazo
  (plan-trazo-atado): el trazo biométrico es obligatorio para todos los
  firmantes y su hash queda cubierto por la firma.
- `geo` (opcional): lugar de la firma en formato compacto `"lat,lon~acc"` —
  latitud y longitud con 5 decimales y precisión `acc` en metros enteros
  (p. ej. `"40.41689,-3.70379~25"`). Rangos: lat ∈ [-90, 90], lon ∈ [-180, 180],
  acc ∈ [0, 99999]. Es una ÚNICA coordenada puntual (minimización RGPD: ni
  histórico ni tracking; el instante ya lo da `iat`). **Su AUSENCIA es el caso
  normal**: solo aparece si el PROPIO firmante marcó, en su dispositivo, la
  casilla opt-in "Adjuntar el lugar de la firma como prueba de entrega" (jamás
  el emisor por él). La captura es fail-soft: si se deniega el permiso, hay
  timeout o el dispositivo no da posición, se firma SIN `geo`. Va en la cabecera
  PROTEGIDA, así que la firma lo cubre y es tamper-evident, sin entrar en el
  enunciado (§2-§3 no cambian). **El PDF SÍ pinta `geo`** cuando el firmante lo
  activó: `renderfirmas.go:86-96` lo extrae de la evidencia firmada —nunca de un
  campo suelto, para que los píxeles no digan nada que la firma no avale— por
  petición del CEO del 21-jul-2026. También se expone al verificar (verificador
  público) y queda dentro de la JAdES almacenada.
  *(Corregido el 2026-09-02 en el barrido R-C204: esta línea decía «el PDF NO
  pinta `geo`», que es la decisión de fase ANTERIOR a la petición del CEO. Un
  documento propio que contradice al producto es lo que un perito lee primero.)* Los verificadores lo tratan como informativo: no altera el
  resultado criptográfico de §7.2 (la firma cubre la cabecera tal cual, con o
  sin `geo`).

**Payload JWS (detached).** En la era de DATO el payload de la JWS son los 32
bytes `SHA256(enunciado)` (§2). Por tanto:

```
payloadB64   = b64url( SHA256(enunciado) )
signingInput = headerB64 ∥ "." ∥ payloadB64          (ASCII)
```

**Firma.** El dispositivo/navegador firma con ECDSA P-256 sobre
`SHA256(signingInput)`. La JWS compacta almacenada es *detached*:

```
jades = headerB64 ∥ ".." ∥ b64url(R ∥ S)      (R y S de 32 bytes cada uno)
```

El segmento central (payload) va vacío; la signature value son exactamente 64
bytes `R||S` (formato JWS, no DER).

## 5. Las dos eras, por `cty`

| `cty` | era | payload detached de la JWS |
|---|---|---|
| `application/deca+json` | DATO (esta especificación) | `SHA256(enunciado canónico §2-§3)` |
| `application/pdf` | PDF (legacy, histórica) | `SHA256(bytes del PDF exhibido)` — el `hashPdf` hex archivado junto a la firma son esos 32 bytes en hex |

Regla de eras: una firma se verifica SIEMPRE con la lógica de su era declarada
en `cty`. Una firma `application/pdf` jamás se verifica contra un enunciado, y
viceversa. Las firmas de la era PDF que llevan `trazoS256` exigen además que
el SVG del trazo archivado cumpla `sha256hex(trazoSvg) == trazoS256`.

Dentro de la era de DATO (`application/deca+json`) **no hay sub-eras**: el
enunciado tiene una sola forma y se reconstruye siempre igual, con
`parte.reservas` y `parte.declaracion` presentes —vacía la primera cuando se
firmó sin reservas— (§3.3, §3.5). Una era futura con más campos se definirá en
su propia sección antes de existir.

## 6. Raíces públicas de la CA

`GET {base}/api/v1/ca/raices` (sin autenticación; los certificados raíz son
públicos por naturaleza):

```json
{"raices":[{"era":1,"pem":"-----BEGIN CERTIFICATE-----\n…"}, …]}
```

`era` es un ordinal: `1` es la raíz vigente (la que emite hoy); las siguientes
son eras anteriores que siguen siendo de confianza para verificar firmas
históricas (activar una raíz nueva jamás invalida las firmas de la anterior).
Un certificado de firmante es válido si fue emitido por **cualquiera** de las
raíces publicadas. Archívelas junto a la evidencia: son suficientes para
verificar sin DeCA.

## 7. Procedimiento de verificación reproducible (sin software de DeCA)

Entradas: `campos.json` (la lista de pares, p. ej.
`[{"k":"docUUID","v":"…"}, …]`), `jades.txt` (la compacta), `cert.pem`,
`raiz.pem` (una raíz de §6).

### 7.1 Reensamblar el enunciado y derivar el payload (node, sin dependencias)

```js
// ensambla.js — uso: node ensambla.js campos.json > enunciado.bin
const fs = require('fs');
const campos = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const lista = Array.isArray(campos) ? campos : campos.campos;
function uvarint(n) {
  const b = [];
  while (n >= 0x80) { b.push((n & 0x7f) | 0x80); n = Math.floor(n / 128); }
  b.push(n); return Buffer.from(b);
}
const trozos = [Buffer.from('deca-enunciado-v1\n', 'utf8')];
for (const c of lista) {
  const k = Buffer.from(c.k, 'utf8'), v = Buffer.from(c.v, 'utf8');
  trozos.push(uvarint(k.length), k, uvarint(v.length), v);
}
process.stdout.write(Buffer.concat(trozos));
```

```sh
node ensambla.js campos.json > enunciado.bin
# payloadB64 = b64url(SHA256(enunciado)):
openssl dgst -sha256 -binary enunciado.bin | basenc --base64url | tr -d '=\n'
```

### 7.2 Verificar la firma ES256 (node)

```js
// verifica.js — uso: node verifica.js jades.txt cert.pem enunciado.bin
const fs = require('fs'), crypto = require('crypto');
const jades = fs.readFileSync(process.argv[2], 'utf8').trim();
const [headerB64, payloadVacio, firmaB64] = jades.split('.');
if (payloadVacio !== '') throw new Error('no es una JWS compacta detached');
const header = JSON.parse(Buffer.from(headerB64, 'base64url'));
if (header.alg !== 'ES256') throw new Error('alg no admitido: ' + header.alg);
if (header.cty !== 'application/deca+json') throw new Error('no es era de dato: ' + header.cty);
const cert = new crypto.X509Certificate(fs.readFileSync(process.argv[3]));
// x5t#S256 ata la cabecera al certificado:
const huella = crypto.createHash('sha256').update(cert.raw).digest('base64url');
if (header['x5t#S256'] !== huella) throw new Error('x5t#S256 no casa con el certificado');
// payload detached = SHA256(enunciado):
const payloadB64 = crypto.createHash('sha256')
  .update(fs.readFileSync(process.argv[4])).digest('base64url');
const signingInput = Buffer.from(headerB64 + '.' + payloadB64, 'ascii');
const ok = crypto.verify('sha256', signingInput,
  { key: cert.publicKey, dsaEncoding: 'ieee-p1363' },   // firma = R||S de 64 bytes
  Buffer.from(firmaB64, 'base64url'));
console.log(ok ? 'FIRMA VERIFICA' : 'FIRMA NO VERIFICA');
if (!ok) process.exit(1);
console.log('instante de firma:', new Date(header.iat * 1000).toISOString());
```

```sh
node verifica.js jades.txt cert.pem enunciado.bin
```

### 7.3 Validar la cadena y la vigencia en el instante de firma (openssl)

```sh
# El certificado del firmante debe estar emitido por una raíz publicada:
openssl verify -no_check_time -CAfile raiz.pem cert.pem
# Vigencia: el instante `iat` de la cabecera debe caer dentro de [notBefore, notAfter]:
openssl x509 -in cert.pem -noout -dates -subject -serial
```

(`-no_check_time` porque lo jurídicamente relevante es la vigencia en el
instante `sigT` de la firma, no en el momento de la verificación; compruebe a
mano que `sigT` cae dentro de las fechas mostradas.)

### 7.4 Comprobaciones de coherencia adicionales

1. El campo `sigT` del enunciado (`campos`) debe ser idéntico al claim `sigT`
   de la cabecera.
2. `rol` y `parte.*` de `campos` deben coincidir con la identidad que ata el
   certificado. **Léala así, y no por el `CN` a secas** — el `CN` no siempre es
   la parte:

   | Atributo del `subject` | Qué contiene |
   |---|---|
   | Atributo | Qué contiene |
   |---|---|
   | `CN` (commonName) | la **persona física** que firmó, cuando consta; si no consta, la razón social de la parte |
   | `O` (organizationName) | la **razón social de la parte** en cuyo nombre se firma (`parte.razonSocial`) |
   | `organizationIdentifier` (OID 2.5.4.97) | el NIF **de la parte**, con el prefijo del ETSI: `VATES-<NIF>` (`parte.nif`) |
   | `serialNumber` | **depende del perfil, y es el atributo que más se malinterpreta:** en un certificado de **persona física** es el **DNI o NIE de esa persona**, sin prefijo (desde R-C301, 10-sep-2026); en uno **sin** persona física —donde el `CN` ya es la razón social— es el NIF **de la parte**. Nunca son la misma cosa: el primero identifica a quien firma, el segundo a la sociedad obligada |
   | `C` (countryName) | `ES`, presente solo cuando consta el documento de la persona |
   | `givenName` (2.5.4.42) y `surname` (2.5.4.4) | nombre de pila y apellidos de la persona, presentes solo si ella los declaró por separado |

   O sea: **la parte se comprueba contra `O` y `organizationIdentifier`, y la
   persona contra `CN` y `serialNumber`.** Un certificado emitido a «Marta Ruiz
   Cano» en nombre de «Distribuciones Cádiz SA» es correcto y frecuente: el
   firmante es una persona y la parte obligada es la sociedad.

   **Por qué el `serialNumber` de una persona NO lleva el prefijo `IDCES-`.** Esa
   estructura («3 caracteres de tipo de documento + 2 de país + guion +
   identificador») la define ETSI EN 319 412-1 cláusula 5.1.3 **solo** cuando el
   certificado incluye el *natural person semantics identifier*, que vive en el
   `qcStatement-2` de los certificados **cualificados**. Estos no lo son y no
   emiten `qcStatements`, así que escribir el prefijo aseveraría una semántica
   que no declaramos. Lo que ampara el identificador a pelo es la NAT-4.2.4-9 de
   EN 319 412-2: *«The serialNumber attribute has no defined semantics beyond
   ensuring uniqueness of subject names. It may contain … an identifier assigned
   by a government or civil authority.»*

   ```sh
   # Vuelca el sujeto completo con los atributos por OID (no los abrevie):
   openssl x509 -in cert.pem -noout -subject -nameopt multiline,utf8,oid
   ```

   Un certificado con `serialNumber` vacío **no es defectuoso**: significa que el
   `CN` identifica a una persona física que **no declaró su documento**, y que el
   NIF de la parte está en `organizationIdentifier`. Rechazar por eso una firma
   válida es el error que este apartado existe para evitar. Lo que sí se puede
   decir de un certificado así es que **no resuelve homonimias**: dos personas
   del mismo nombre en la misma parte son, para él, el mismo sujeto.
3. Si el documento completo está disponible, recomputar `nucleoHash` (§3.1) y
   contrastarlo con el campo 3 del enunciado detecta alteraciones del cuerpo
   posteriores a la firma.
4. Firmas con sello de tiempo (perfil B-T): el token RFC 3161 archivado
   (`sigTst`, base64) sella el digest SHA-256 de la signature value; se
   inspecciona con `openssl ts -reply -in sigTst.der -text`.

### 7.5 Era PDF (legacy)

Para `cty: application/pdf` el procedimiento es idéntico salvo que
`payloadB64 = b64url(bytes del hashPdf)` (los 32 bytes cuyo hex se archiva como
`hashPdf`, que a su vez es `SHA256(pdf exhibido)`); no hay enunciado. Si la
cabecera lleva `trazoS256`, compruebe además `sha256hex(trazoSvg) == trazoS256`.

### 7.6 Trazo atado (era de DATO)

Una firma de la era de DATO también puede llevar `trazoS256` en la cabecera (el
trazo manuscrito, obligatorio para todos los firmantes). Como la cabecera está
firmada, el trazo queda ATADO: tras verificar la firma (§7.2) el tercero DEBE
comprobar `sha256hex(trazoSvg) == trazoS256` con el trazo archivado junto a la
firma. Si no casa, el trazo exhibido no es el firmado — rechácela. Una firma de
dato SIN `trazoS256` (históricos anteriores a esta regla) se verifica igual, sin
el chequeo del trazo.

## 8. Verificador de cortesía

DeCA publica una página estática de cortesía en `GET {base}/verificar` que
ejecuta §7.1–§7.2 en el navegador y compara el emisor del certificado con las
raíces de §6. Es una comodidad, no una dependencia: todo lo que hace está
descrito arriba y es reproducible sin DeCA. La validación X.509 completa de la
cadena se hace con `openssl` (§7.3).
