You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

144 lines
18 KiB

# Plan: firma de autor en Go (spec v0.11, entrega 2 del formato 3)
*1 de octubre de 2026. El autor pidió poder firmar cápsulas («no tengo opción de meter firmas en la página») y dio el visto bueno a empezar por el spec y Go. La base es la parte 2 del [diseño del formato 3](spec_v0.10/formato3_diseno.md) (apartados 10 a 15), ya revisada. Se hace en la rama `v0.11` de `datekeys-go`, que ya tiene la llave de palabras (`5b3d2d4`).*
## Por qué primero el spec y Go
- **El spec** es el contrato: dice qué bytes se firman, cómo se verifica una firma y qué ve quien abre. Lo que no está en el spec no puede comprobarlo otra implementación.
- **Go** es la implementación de referencia: escribe los vectores y las cápsulas de prueba que después tiene que reproducir TypeScript byte a byte, como en el formato 3.
- **La página** va después, con su propio plan, y reutiliza esas pruebas.
## Pasos
| Paso | Contenido | Resultado |
|---|---|---|
| 0 | Borrador del spec v0.11: qué se firma (`AUTHOR_MESSAGE`), Ed25519 estricto, las claves `dkauthor1…`, los veredictos de la firma, la llave de palabras y la nota pública | Texto para que lo apruebe el autor |
| 1 | `ed25519strict`: la verificación estricta, con código propio para lo que `ed25519.Verify` de Go acepta de más | Los casos de «Taming the many EdDSAs» en `ed25519_strict.json` |
| 2 | `authorkey`: generar la clave, escribirla como `dkauthor1…` y `DKAUTHOR-SECRET-KEY-1…`, y guardarla en un fichero cifrado con contraseña (`age` scrypt, logN 16) | Pruebas de ida y vuelta y de errores |
| 3 | Firmar en el escritor: `payload_commit`, `control_commit`, `head_digest` y la firma en el área de seguridad, verificada antes de escribir | Una cápsula firmada que abre un lector de la v0.10, que muestra F1 |
| 4 | Verificar en el lector: los veredictos de la firma, y la comparación con la clave que se espera | Firmas válidas, alteradas, quitadas, rehechas y trasplantadas |
| 5 | CLI: `author keygen`, `author public`, `encrypt -sign` y `decrypt -expect-author` | Prueba de ida y vuelta con la CLI |
| 6 | Datos de prueba: el fixture `format3_signed`, `statements.json` y las mutaciones de la firma | Lo que comprobará TypeScript |
| 7 | Documentación, `check.sh`, revisión adversarial y, con permiso del autor, el tag `spec-v0.11` | Rama lista |
Queda para después la verificación por un tercero (`statement export|verify`, apartado 13), que no hace falta para firmar y comprobar firmas.
## Cambio tras hablar con el autor (01-10)
El autor quiere que se sepa quién firma: con un certificado reconocido (FNMT, DNIe), firmado fuera de la página con su aplicación de firma.
- **Dos tipos de firma.** `alg` 1, con una clave propia Ed25519 (`dkauthor1…`), como en el diseño. `alg` 2, una firma CAdES separada, la que produce AutoFirma, con el certificado X.509 de la persona: la página da el `AUTHOR_MESSAGE` como fichero, la persona lo firma fuera y carga la firma.
- **Escritura en dos tiempos.** El escritor prepara la cápsula (sorteos, head y compromisos), devuelve `AUTHOR_MESSAGE`, espera la firma y entonces escribe. Los secretos solo viven en memoria entre los dos tiempos.
- **Área mayor.** 512 bytes no caben una firma CAdES con su cadena. La v0.11 fija un área mayor, de 32 KiB provisionales (apartado «Propuesta de Fable y Astra»), la misma para toda cápsula, firmada o no (§29.2, hallazgo 5).
- **Verificación.** DateKeys comprueba la firma y enseña el titular del certificado. La validez legal (cadena de confianza, revocación) la dan VALIDe o AutoFirma, a los que la página exporta la firma y el mensaje.
- **Sello de tiempo** RFC 3161 de una autoridad cualificada (`seal_type` 2), dentro de la firma (CAdES-T) o pedido aparte, en lugar del servicio propio de la entrega 3.
## Las aplicaciones (01-10)
DateKeys tendrá aplicación móvil, de escritorio (macOS, Windows y Linux), de línea de órdenes y la web. Todas escriben y leen la misma cápsula firmada. Lo que cambia es cómo consigue cada una la firma con certificado:
| Aplicación | Cómo firma con certificado |
|---|---|
| Web | Fuera, con la aplicación de firma de la persona (AutoFirma), y carga la firma |
| Escritorio | Con el certificado del sistema o de la tarjeta: almacén de Windows, llavero de macOS, PKCS#11 en Linux |
| Móvil | Con el certificado importado en la app, o con el DNIe por NFC |
| CLI | Con un fichero `.p12` o la tarjeta (PKCS#11) |
Por eso:
- la API del escritor, en Go y en TypeScript, trabaja en dos tiempos, preparar y escribir con la firma, con un firmante que aporta cada aplicación;
- la firma con certificado es CMS/CAdES estándar (`SignedData` separado sobre `AUTHOR_MESSAGE`), que todas esas plataformas producen de serie;
- la librería de Go sirve para la CLI, el escritorio y el móvil (compilada para Android e iOS), y la de TypeScript para la web.
## Decisiones del autor
1. **Dónde se guarda la clave secreta de autor.** El diseño propone un fichero cifrado con contraseña. Como con la llave de palabras, podría poder guardarse también como palabras. Pero a diferencia de esa llave, la clave de autor es una sola para todas tus cápsulas, así que unas palabras flojas la exponen todas a la vez. Recomendación: fichero cifrado y, como alternativa, 12 palabras generadas al azar, nunca elegidas por la persona.
2. **Una sola firma por cápsula.** El diseño deja un único hueco de firma, ampliable más adelante sin cambiar el formato. Recomendación: así.
## Otras novedades de la v0.11 (01-10)
- **Nota pública.** Un texto corto que se ve antes de la fecha, para saber qué es cada cápsula: una extensión no crítica de `PUBLIC_HEADER`, registrada en §72, de hasta 1 KiB y con las reglas de texto de §29.6. Un lector de la v0.10 la ignora y abre la cápsula igual.
- Antes de la fecha no está comprobada: cualquiera puede fabricar una cápsula con la nota que quiera. La página y la CLI la muestran como nota de quien la creó, sin comprobar.
- Al abrir, `header_binding` la ata a la cápsula, y la firma la cubre a través de `control_commit`. Cambiarla después rompe la cápsula.
- **La nota y la fecha, también en la `.dkk`**, como pidió el autor, en una extensión no crítica (§44), para reconocer una llave suelta.
- Es informativa, porque nada la ata a la cápsula. La `.dkk` se empareja con su cápsula por `capsule_id` y `capsule_digest`, que ya escriben Go y TypeScript, nunca por la nota.
- Si la cápsula y la llave traen notas distintas, manda la de la cápsula y se avisa de la diferencia.
- **Dónde está el `.dkc`, también en la `.dkk` y cifrado para la fecha**, como pidió el autor: una o varias direcciones (`https://…`, `ipfs://…`), dentro de un fichero `age` con un stanza tlock para la ronda de la cápsula, en la misma extensión que la nota.
- Nadie las lee antes de la fecha, tampoco el destinatario. En la fecha, la misma firma de drand descifra las direcciones y abre la cápsula.
- `capsule_digest` va dentro, con las direcciones. *Superado por el sobre opaco de abajo: lo que se guarda fuera tiene otro hash, así que el spec aprobado mantiene `verification_metadata` en la `.dkk` (§43, v0.11). La idea original era omitirlo, porque en IPFS, con CIDv1 y bloques raw, un `.dkc` de menos de 256 KiB tiene como dirección su propio SHA-256 y el digest en claro la delataría.*
- Ocultación, a petición del autor (01-10, `48b496f`): lo que se guarda fuera es solo el resto de un sobre `age` del `.dkc`, sin cabecera (que va en el localizador), así que son bytes sin marca; puede ir dentro de otro fichero, una imagen, un vídeo o cualquiera, y cada dirección dice en qué byte empieza. No es esteganografía: un análisis del huésped ve bytes de más. Solo vale un almacenamiento que conserva el fichero byte a byte.
- El orden, también con IPFS: crear la cápsula, subirla y obtener su dirección, y solo entonces generar la `.dkk`. Así nunca se entrega una llave que apunta a un sitio vacío.
- Entre medias, la llave vive solo en memoria, como en la firma en dos tiempos. Como red de seguridad, la página puede guardar antes una `.dkk` sin dirección, que abre la cápsula igual, y completarla después: nada ata la `.dkk` a la cápsula, y cifrar hacia una ronda no exige ningún secreto.
- Lo que se acepta: nadie puede bajar ni guardar la cápsula antes de la fecha, ni comprobar que sigue ahí; si el enlace muere, se pierde; y quien robe la `.dkk` leerá la dirección en la fecha. Quien crea la cápsula tiene que guardar su propia copia.
- **Código:** en Go, las dos extensiones y `-note` en la CLI, con el paso 5; en la página, con su propio plan.
- **Ficheros fuera de la cápsula**, propuesta pendiente de que el autor la confirme. Cada fichero iría dentro, como hoy, o fuera (IPFS, Drive, una URL), cifrado con la misma clave, con su hash, su tamaño y sus direcciones en una extensión crítica del head. Sus riesgos: que el fichero dure, que se pueda borrar antes de la fecha, que el cifrado quede público y que la web no pueda descargarlo de cualquier sitio.
## Propuesta de Fable y Astra (01-10)
La propuesta consolidada está en [spec_v0.11/revision_fable_astra.md](spec_v0.11/revision_fable_astra.md). Con ella, el autor cerró:
- **A1 se mantiene:** un área fija e igual para todo escritor de la versión, firme o no, de 32 KiB provisionales hasta medir. La única excepción es la ampliación expresa a 64 KiB.
- **Sello de tiempo obligatorio con certificado,** un CAdES-T por firmante. En las demás cápsulas es opcional.
- **Sin firma del servicio DateKeys** en esta entrega.
Los revisores no vieron la spec ni el código. Comprobado contra los dos:
1. **El problema del ancho de L no existe,** y era un error de la consulta. `payload_length` ocupa siempre 8 bytes, así que el área no cambia `SEALED_CONTROL_LEN` ni `header_binding`. Para que la firma no dependa del área basta con poner a cero esos 8 bytes en lo firmado: sobran `PRELUDE*` y `header_commit` del cambio 1.
2. **El cambio 2 no hace falta por el motivo que dan.** El head lleva una sal de 32 bytes (§29.4, clave 2), así que `head_digest` ya oculta lo que compromete: quien tenga `AUTHOR_MESSAGE` no puede confirmar que la cápsula contiene un documento. Un solo hash sigue siendo cómodo para juntar los tres compromisos.
3. **`REQUIRED_SIGNERS` no debe ser una clave nueva del mapa exterior de `security`.** Un lector v0.10 daría X (§29.3 y `security.json`): «No se han podido comprobar la firma ni el sello». Dentro del mapa `author-signature` de `alg` 2, en su clave 1, da F1, como pide el diseño.
4. **El lector estricto del cambio 1 ya existe** en la spec y en los dos lectores: la trama del área, los ceros del área y del relleno, un texto en claro de exactamente P bytes y el CBOR canónico.
5. **Ninguna longitud visible cambia con firma o sello** dentro de la reserva: ni `PUBLIC_HEADER_LEN`, ni `SEALED_CONTROL_LEN`, ni P. El control no lleva relleno propio, así que la lista de firmantes no debe ir en él, como dicen.
6. **Una incoherencia:** la tabla del cambio 6 admite `seal_type` 1, pero la propuesta deja ese tipo reservado. En esta entrega solo entra `seal_type` 2.
7. **Por comprobar:** si la TSA elegida acepta peticiones desde el navegador (CORS). Si no, el sello de la web depende de que AutoFirma añada el CAdES-T.
El autor confirmó los tres el 01-10. El paso 0, el borrador del spec v0.11, está en `datekeys-go`, rama `v0.11`, en `e224119`, sin aprobar ni subir.
## Revisión del borrador (01-10)
Tres revisores adversariales (criptografía, interoperabilidad y privacidad) revisaron el borrador `e224119`: [spec_v0.11/revision_borrador.md](spec_v0.11/revision_borrador.md). Coinciden en un bloqueante: el sello y los nombres de los certificados no se anclan en nada.
**Decisiones del autor**, tomadas el 01-10 con la recomendación y aplicadas, con los arreglos de abajo, en `5a0b95b` de `datekeys-go`:
1. **Autoridades de confianza.** *Revocada el 01-10 (`46ca90b`): el autor quita la lista. La firma solo se comprueba al abrir, quizá décadas después, y quien necesite saber quién firmó usará el validador oficial de su país; la cápsula guarda las pruebas del momento de la firma. DateKeys comprueba la criptografía y avisa de que no comprueba quién emitió el certificado ni el sello.* Lo que se había propuesto: Fijar en el SDK las autoridades de los certificados de los firmantes (FNMT, DNIe) y las TSA admitidas, y comprobar la cadena hasta ellas en el instante del sello, sin revocación; o no comprobar nada y rebajar los textos de F6 y S4.
2. **`capsule_id` en la sal de la llave de palabras,** para que una ronda popular no permita atacar todas sus cápsulas a la vez.
3. **Sobre opaco** para guardar la cápsula fuera: se sube el `.dkc` cifrado con una clave aleatoria que va en el localizador.
4. **`AUTHOR_MESSAGE` legible,** como texto con el hash en hexadecimal y un código corto para comparar.
**Arreglos sin decisión**, aplicados al borrador:
- `SIG_PART` sobre el contenido exacto de la clave 2, sea cual sea su `alg`.
- §29.10: un procedimiento por firmante. F1 solo para la forma de lo presente; F5 para un firmante ausente, sin sello, no verificable o con el certificado fuera de validez en t; los firmantes ajenos, aparte; un `SignerInfo` por certificado.
- Perfil CMS y del token: DER de X.690 con los SET OF ordenados; una tabla cerrada de OID; el hash de la firma igual a `digestAlgorithm`; el content-type y el message-digest del token; ESSCertID o ESSCertIDv2 de la TSA; un solo `signature-time-stamp`; otros atributos no firmados, ignorados; RSA de 2048 a 4096 bits; imprint SHA-256 en `seal_type` 2; S4 solo con genTime + accuracy < round_time.
- §29.9: otra longitud es F1 y un fallo de las condiciones, F2; el texto sobre noble, corregido; el resultado esperado de cada caso, en `ed25519_strict.json`.
- `alg` y `seal_type` 4294967295, reservados para pruebas: los vectores «no soportado» de la v0.10 se rehacen con ellos, y los actuales pasan a casos de F2 y S2.
- Los restos de la v0.10 en §55.1, §27, §63 paso 15, §72 y §73.
- La nota: las reglas del autor declarado (una línea), una línea fija antes de la fecha, ni enlace ni edición, §7.3 y otro ejemplo.
- §44.1: solo `https:` e `ipfs:`, sin userinfo ni direcciones privadas, con el host visible y `capsule_size`; un localizador de tamaño fijo, con la ronda y la cadena de la DateKey; las notas, comparadas por bytes; el aviso, corregido.
- El CDDL y §57: la `data` de una extensión nunca da `ERR_NON_CANONICAL_CBOR`.
- §55.2: lo que revelan una firma con certificado, el OCSP, un intermediario y la exportación a VALIDe; el nombre, sin `serialNumber`.
- §7.9: la firma a ciegas, la aplicación de firma falsa, el firmante coaccionado, las claves robadas y el borrado de una clave guardada.
- Flujos: la regla 17 sobre lo definitivo; `capsule_digest` omitido con localizador (superado: con el sobre opaco se mantiene, §43); §29.2 y la regla 13 alineadas; ni `I_PAYLOAD` ni el contenido en disco mientras se espera una firma; quitar `eContent` y podar la cadena (MAY).
- Las cifras de §76, la redacción de los prefijos, el mensaje de la firma de prueba, los vectores de lo firmado sobre `format3_single` y una tabla de minúsculas de Unicode 18 para Go y TypeScript.
**Código pendiente por la revisión**, antes de los pasos de Go. Hechos los dos primeros, el 01-10: en `datekeys-go`, `150d952` (la tabla) y `2213b8c` (la llave), y en `datekeys-ts`, `c3c124a`.
- La llave de palabras con la sal v2 (`capsule_id`), en `wordkey` y la CLI de Go y en la página, con el vector nuevo de §38.1. La identity se deriva después de generar `capsule_id`, así que `encryptFiles` y `EncryptFiles` reciben las palabras.
- Una tabla de minúsculas de Unicode 18.0.0, generada con `pathrule/gen`, para Go y TypeScript; y el rechazo de controles, Default_Ignorable y Cn al escribir.
- ~~La lista de autoridades reconocidas~~: descartada.
## Estado al cerrar la sesión del 01-10
- Paso 0: el borrador del spec v0.11, a falta de la aprobación escrita del autor.
- Paso 1, hecho (`c402857`): `internal/ed25519strict` y `testdata/vectors/ed25519_strict.json`, con 18 firmas; `crypto/ed25519` acepta 11 que el perfil rechaza.
- Paso 2, hecho (`7e7681e`): `authorkey`, con las claves `dkauthor1…` y `DKAUTHOR-SECRET-KEY-1…` y el fichero cifrado con scrypt de factor 16.
- Pasos 3 y 4, empezados (`3d85a0b`): `capsule/signature.go`, con los compromisos, `AUTHOR_MESSAGE`, `SIG_PART` y `SEAL_SUBJECT`, y `EvaluateSecurityIn`, que da F2, F3 o F4 a `alg` 1. Falta conectarlos al escritor y al lector; el [handoff](HANDOFF.md) dice cómo.
## Estado al seguir la sesión del 01-10 (tarde)
- Pasos 3 y 4, hechos para `alg` 1 (sin confirmar en commit todavía): `AreaLen` = 32768 y `LargeAreaLen` = 65536 con `EncryptOptions.LargeArea`; `EncryptOptions.AuthorKey` firma dentro de `sealer.write`, con un gancho `prepare` que recibe el `Control` final y construye `SECURITY_CBOR` y el marco antes de escribir nada, y lo comprueba con `EvaluateSecurityIn` (regla 19); `OpenOptions.AuthorKeys` y `openBody` evalúan la firma con el `control_commit`, el `head_digest` y la hora de la ronda.
- Las pruebas del §29.2 de `open3_test.go` y los fixtures de la v0.10 usan ahora `capsule.AreaUnit` (512), el área de la v0.10; `TestEncryptFilesLengths` espera las longitudes con 32 KiB.
- Los fixtures `format3_signature_unsupported` y `format3_seal_unsupported` se regeneraron con `alg` 4294967295 (F1): con `alg` 1 y datos al azar el lector da ahora F2. Nueva mutación, fuera del spec, de `alg` 1 que no verifica: F2.
- Falta del paso 6: `format3_signed`, `statements.json` y los vectores de lo firmado; el README de `testdata` aún dice que `EncryptFiles` escribe 512 bytes de área.
## Estado al cerrar la tarde del 01-10
- Pasos 3 a 6: hechos para `alg` 1 (`c3175a1`, `d1441de`, `b88c459`). `format3_signed` y sus vectores están en `testdata`.
- `alg` 2 y sello RFC 3161: hechos (`55a261c`, `6cac49b`), con `internal/der`, `internal/cms` y su constructor de pruebas `internal/cms/cmstest`.
- Nota pública (`2142fcb`) y extensión `datekeys.capsule` con localizador y sobre (`1df818d`).
- Paso 7: revisión adversarial hecha por tres revisores independientes, con arreglos en `90e15eb` y `b0bda15`. Las decisiones que quedan para el autor están en el [handoff](HANDOFF.md). Falta: los fixtures de `alg` 2, sello y localizador para TypeScript, y el tag `spec-v0.11` cuando el autor apruebe el texto.

Powered by TurnKey Linux.