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.

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. 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. 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. 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 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. 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.