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 (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.
alg1, con una clave propia Ed25519 (dkauthor1…), como en el diseño.alg2, una firma CAdES separada, la que produce AutoFirma, con el certificado X.509 de la persona: la página da elAUTHOR_MESSAGEcomo 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_type2), 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 (
SignedDataseparado sobreAUTHOR_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
- 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.
- 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_bindingla ata a la cápsula, y la firma la cubre a través decontrol_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
.dkkse empareja con su cápsula porcapsule_idycapsule_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.
- Es informativa, porque nada la ata a la cápsula. La
- Dónde está el
.dkc, también en la.dkky cifrado para la fecha, como pidió el autor: una o varias direcciones (https://…,ipfs://…), dentro de un ficheroagecon 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_digestva dentro, con las direcciones, y el escritor omite entoncesverification_metadata. En IPFS, con CIDv1 y bloques raw, un.dkcde menos de 256 KiB tiene como dirección su propio SHA-256, y el digest en claro la delataría. La.dkkse sigue emparejando con su cápsula porcapsule_id.- Ocultación, a petición del autor (01-10,
48b496f): lo que se guarda fuera es solo el resto de un sobreagedel.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
.dkksin dirección, que abre la cápsula igual, y completarla después: nada ata la.dkka 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
.dkkleerá 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
-noteen 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. 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:
- El problema del ancho de L no existe, y era un error de la consulta.
payload_lengthocupa siempre 8 bytes, así que el área no cambiaSEALED_CONTROL_LENniheader_binding. Para que la firma no dependa del área basta con poner a cero esos 8 bytes en lo firmado: sobranPRELUDE*yheader_commitdel cambio 1. - 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_digestya oculta lo que compromete: quien tengaAUTHOR_MESSAGEno puede confirmar que la cápsula contiene un documento. Un solo hash sigue siendo cómodo para juntar los tres compromisos. REQUIRED_SIGNERSno debe ser una clave nueva del mapa exterior desecurity. Un lector v0.10 daría X (§29.3 ysecurity.json): «No se han podido comprobar la firma ni el sello». Dentro del mapaauthor-signaturedealg2, en su clave 1, da F1, como pide el diseño.- 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.
- Ninguna longitud visible cambia con firma o sello dentro de la reserva: ni
PUBLIC_HEADER_LEN, niSEALED_CONTROL_LEN, ni P. El control no lleva relleno propio, así que la lista de firmantes no debe ir en él, como dicen. - Una incoherencia: la tabla del cambio 6 admite
seal_type1, pero la propuesta deja ese tipo reservado. En esta entrega solo entraseal_type2. - 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. 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:
- 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. capsule_iden la sal de la llave de palabras, para que una ronda popular no permita atacar todas sus cápsulas a la vez.- Sobre opaco para guardar la cápsula fuera: se sube el
.dkccifrado con una clave aleatoria que va en el localizador. AUTHOR_MESSAGElegible, como texto con el hash en hexadecimal y un código corto para comparar.
Arreglos sin decisión, aplicados al borrador:
SIG_PARTsobre el contenido exacto de la clave 2, sea cual sea sualg.- §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
SignerInfopor 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 solosignature-time-stamp; otros atributos no firmados, ignorados; RSA de 2048 a 4096 bits; imprint SHA-256 enseal_type2; 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. algyseal_type4294967295, 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:eipfs:, sin userinfo ni direcciones privadas, con el host visible ycapsule_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
datade una extensión nunca daERR_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_digestomitido con localizador; §29.2 y la regla 13 alineadas; niI_PAYLOADni el contenido en disco mientras se espera una firma; quitareContenty 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_singley 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), enwordkeyy la CLI de Go y en la página, con el vector nuevo de §38.1. La identity se deriva después de generarcapsule_id, así queencryptFilesyEncryptFilesreciben 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/ed25519strictytestdata/vectors/ed25519_strict.json, con 18 firmas;crypto/ed25519acepta 11 que el perfil rechaza. - Paso 2, hecho (
7e7681e):authorkey, con las clavesdkauthor1…yDKAUTHOR-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_PARTySEAL_SUBJECT, yEvaluateSecurityIn, que da F2, F3 o F4 aalg1. Falta conectarlos al escritor y al lector; el handoff dice cómo.
Estado al seguir la sesión del 01-10 (tarde)
- Pasos 3 y 4, hechos para
alg1 (sin confirmar en commit todavía):AreaLen= 32768 yLargeAreaLen= 65536 conEncryptOptions.LargeArea;EncryptOptions.AuthorKeyfirma dentro desealer.write, con un ganchoprepareque recibe elControlfinal y construyeSECURITY_CBORy el marco antes de escribir nada, y lo comprueba conEvaluateSecurityIn(regla 19);OpenOptions.AuthorKeysyopenBodyevalúan la firma con elcontrol_commit, elhead_digesty la hora de la ronda. - Las pruebas del §29.2 de
open3_test.goy los fixtures de la v0.10 usan ahoracapsule.AreaUnit(512), el área de la v0.10;TestEncryptFilesLengthsespera las longitudes con 32 KiB. - Los fixtures
format3_signature_unsupportedyformat3_seal_unsupportedse regeneraron conalg4294967295 (F1): conalg1 y datos al azar el lector da ahora F2. Nueva mutación, fuera del spec, dealg1 que no verifica: F2. - Falta del paso 6:
format3_signed,statements.jsony los vectores de lo firmado; el README detestdataaún dice queEncryptFilesescribe 512 bytes de área.
Estado al cerrar la tarde del 01-10
- Pasos 3 a 6: hechos para
alg1 (c3175a1,d1441de,b88c459).format3_signedy sus vectores están entestdata. alg2 y sello RFC 3161: hechos (55a261c,6cac49b), coninternal/der,internal/cmsy su constructor de pruebasinternal/cms/cmstest.- Nota pública (
2142fcb) y extensióndatekeys.capsulecon localizador y sobre (1df818d). - Paso 7: revisión adversarial hecha por tres revisores independientes, con arreglos en
90e15ebyb0bda15. Las decisiones que quedan para el autor están en el handoff. Falta: los fixtures dealg2, sello y localizador para TypeScript, y el tagspec-v0.11cuando el autor apruebe el texto.