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.
122 lines
15 KiB
122 lines
15 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, y el escritor omite entonces `verification_metadata`. 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. La `.dkk` se sigue emparejando con su cápsula por `capsule_id`.
|
|
- 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.** 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; §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 (§29.13), con su contenido inicial, al medir firmas y sellos reales.
|
|
|