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.
dateKeys-dart/CHANGELOG.md

64 KiB

Changelog

Cambios notables de la librería Dart. El proyecto usa versionado semántico; mientras sea 0.x, no hay promesa de estabilidad.

Especificación 0.12, en la rama v0.12 — sin versión

La especificación 0.12, aprobada (06-10-2026)

  • El autor aprobó el 6 de octubre de 2026 el borrador v0.12, tal como estaba: datekeys-go lo cierra con el tag spec-v0.12 (fe405e2). La librería pasa a la rama v0.12, como dice el plan: la rama v0.11 se queda en bb66e48.
  • specVersion pasa a 0.12, y testdata se sincroniza con el tag: solo cambia el campo spec de cada fichero.
  • Los vectores de test/vectors/ que escriben los generadores de Go cambian solo su campo spec, en los .json y en sus copias .g.dart: entre c531e93 y fe405e2, Go solo cambia SpecVersion y añade wordkey.json, así que el resto sale igual. Dos pruebas que comparaban con 0.11 comparan ahora con specVersion.
  • La librería ya seguía el borrador: no cambia nada más. tool/check.sh pasa con 2 131 pruebas en la VM y 665 en Node.

Especificación 0.11, en la rama v0.11 — sin versión

Los vectores compartidos de la llave de palabras (06-10-2026)

  • testdata se sincroniza con datekeys-go en 084728d, que añade vectors/wordkey.json: los casos de la llave de palabras que pide el §64 de la v0.11, que hasta ahora solo estaban en las pruebas de Go. Ningún otro fichero cambia.
  • test/wordkey_testdata_test.dart los corre, en la VM: las palabras de 45 textos, entre ellos cada espacio del §38.1 y tres que no lo son; lo que hace un escritor con 20 textos, con el texto del error de Go; y 6 identidades con su recipient, el vector del §38.1 el primero. La librería no cambia: todos coinciden.

Etapa 7b: las claves de autor y el sellado del localizador (06-10-2026)

  • Las claves de autor (lib/src/authorkey.dart), port del paquete authorkey de datekeys-go en c531e93, el borrador v0.12 (§29.9, §29.12), con las mismas comprobaciones en el mismo orden y los mismos textos de error:

    • AuthorKey: generate, con la RandomSource de la 6a, fromSeed, publicKey, publicString, sign, clear y secret, y un toString que oculta la clave secreta, como String y GoString de Go;
    • las cadenas: authorPublicString, parseAuthorPublic, con una clave que el perfil estricto podría aceptar, y parseAuthorSecret, también sobre los bytes de un string de Go;
    • el fichero: marshalAuthorKey; encryptAuthorKey, con ScryptRecipient y ageEncrypt de la 6a y un factor de trabajo de 16; y readAuthorKey, en claro o cifrado, con el lector de age y un máximo de 16, de 64 KiB como mucho y con las líneas de bufio.Scanner.

    Los errores son una AuthorKeyException con el texto de Go. La caja de una cadena y los espacios de una línea son los de strings.ToLower, strings.ToUpper y strings.TrimSpace de Go, con sus tablas de Unicode 15.0.0 (lib/src/go_unicode.dart, que escribe tool/go_unicode_tables.go), así que cualquier entrada, también la que no es UTF-8, da el error de Go.

  • La firma Ed25519 (lib/src/ed25519_sign.dart, interno), el crypto_sign de TweetNaCl en su versión de JavaScript, con el SHA-512 de package:crypto: el cuerpo de curve25519.dart, copiado con el producto como un bucle, y modL en 64 limbs de 8 bits, exacto en la VM y en la web. El escalar secreto y el nonce no pasan por BigInt ni por una rama, y se borran tras usarlos; nada promete tiempo constante.

  • El sellado del localizador y el sobre (lib/src/locator_seal.dart), Seal y NewEnvelope del paquete locator: sealLocator escribe el texto del localizador, crea el recipient tlock de la ronda y cifra, en el orden de Go; newEnvelope saca I_SOBRE, cifra el .dkc con ageEncrypt y lo corta con splitEnvelope de la 7a.

  • Una diferencia con Go, a propósito: una clave borrada con clear lanza un StateError en cada uso, donde Go da los valores de una clave a ceros y una firma que nunca verifica.

  • Algo de Go que se conserva: el texto de PublicString con una clave de otra longitud tiene los números al revés, «a public key has 32 bytes, not 31».

  • lib/datekeys.dart exporta authorkey.dart, salvo parseAuthorPublicUtf8 y parseAuthorSecretUtf8, y locator_seal.dart. La documentación de locator.dart remite al sellado.

  • Vectores de Go, en el módulo de datekeys-go y sin cambiarlo, mientras crypto/rand lee el keystream de SeededRandomSource:

    • tool/authorkey_go_vectors.go escribe authorkey.json: las firmas de crypto/ed25519 sobre líneas de sign.input, con los tests 1, 2, 3 y 1024 de la RFC 8032, TEST SHA(abc), semillas de una semilla con mensajes de hasta 1 MiB y otras claves públicas; los escalares de math/big; y authorkey entero con sus textos: 1 288 cadenas, 1 620 runas de los bordes de los conjuntos de Go y 111 ficheros. Generate lee crypto/rand.Reader gracias a //go:debug cryptocustomrand=1. authorkey.g.dart lleva una parte, para Node.js;
    • tool/locator_seal_go_vectors.go escribe locator_seal.json: Seal de localizadores de uno a tres bloques para rondas de 1 a la última de Quicknet, sus rechazos, NewEnvelope de .dkc de 0 bytes a 1 MiB y el camino entero.

    Con la misma semilla, esta librería saca los mismos valores y escribe los mismos bytes que Go en cada caso, y da los mismos textos.

  • En la otra dirección: tool/seal_interop_dart_samples.dart escribe, de las recetas de test/seal_interop_support.dart, localizadores sellados de las rondas 1000, 1001, 1004 y 2000 con sus sobres, ficheros de clave en claro y cifrados y firmas de 0 a 70 000 bytes, y tool/seal_interop_go_verdicts.go los abre con locator.Open, OpenEnvelope, authorkey.Read y crypto/ed25519. Go lee los quince, y seal_interop.json guarda sus veredictos y el SHA-256 de cada fichero, que las pruebas escriben otra vez.

  • Pruebas: 171 nuevas en la VM y 160 en Node.js: 1884 y 592 en total. En Node.js corren las firmas marcadas, una de cada ocho runas y todo lo demás, salvo cuatro ficheros con scrypt de logN 16 o 17 y los sobres de más de 64 KiB.

  • Fallos inyectados, uno a uno y revertidos, con las pruebas de la parte en la VM. Las pruebas detectan los veinte: la firma sin el bit 254 del escalar, sin el bit 0 de la escalera, con R y A en otro orden en el hash o sin el signo de x en R; modL sin el redondeo de sus acarreos o sin su última resta de ℓ; un byte que no es UTF-8 que no cambia la caja; los espacios de Unicode no recortados por la izquierda, y solo el espacio y el tabulador por la derecha; el token de bufio.Scanner un byte más largo; un factor de trabajo de 17 aceptado al leer, y uno de 15 al escribir; el comentario mirado antes de recortar la línea; una runa aceptada al final de una línea sin que acabe en ella; una clave de orden pequeño aceptada; la posición del separador comprobada con un byte menos; una semilla de 33 bytes sacada al generar; el recipient tlock creado antes de escribir el localizador; el sobre cifrado con otra fuente de lo aleatorio; y las tablas de Go sin el paso de sus rachas. Dos se escapaban al principio, la runa al final de una línea y el separador, y el generador escribe ahora sus casos. Otros dos fallos eran equivalentes, sin efecto posible: el bit 255 de la escalera, siempre cero en un escalar recortado o reducido, y un límite de tres bytes en DecodeLastRune, porque ningún espacio mide cuatro.

  • tool/authorkey_bench.dart mide la parte. En la VM, una firma de 99 bytes tarda 4,5 ms, como la verificación estricta, y en Node.js 9 ms; el fichero de una clave, con scrypt de logN 16, 0,57 s al escribirlo o leerlo, y entre 0,8 y 1,0 s en Node.js; y sellar un localizador, 30 ms y 0,52 s. Las cifras, en el README.

  • Integración con la 6b: la 7b se puso encima de la 6b con rebase. La interfaz del enganche de la firma de alg 1, que la 6b llamaba AuthorKey, pasa a llamarse AuthorSigner, y la AuthorKey de la 7b la implementa. Las pruebas del escritor firman ahora las cápsulas de alg 1 con la AuthorKey de la semilla de Go, no con la firma grabada, y escriben los mismos bytes que Go. Con las dos partes, el gate pasa con 2059 pruebas en la VM y 665 en Node.js.

Etapa 6b: el escritor de cápsulas del formato 3 (06-10-2026)

  • El escritor (lib/src/encrypt3.dart), port de EncryptFiles, sealer y el área de seguridad de encrypt.go y encrypt3.go de datekeys-go en c531e93, con las mismas comprobaciones en el mismo orden, los mismos códigos y textos de error y los mismos valores aleatorios, sacados en el mismo orden:

    • encryptFiles escribe en un ByteSink los ficheros de sus FileSource, cada uno leído dos veces en streaming, con su SHA-256 comprobado en la segunda lectura. El sink se cierra cuando la cápsula está completa y comprobada, y se aborta ante cualquier fallo;
    • time_only y time_and_key, con recipients X25519, una .dkk nueva y la llave de palabras, de 1 a 16 credenciales, en los 16 huecos de INNER_ACCESS_AGE con señuelos y en un orden al azar;
    • el head, con los ficheros en el orden de los bytes de sus rutas, su mtime, el comentario y el autor declarado; la nota pública; las extensiones de cada objeto, con la regla del §72; y el relleno;
    • el área de seguridad de 32 KiB, la de 64 KiB con largeArea y la de prueba con testAreaLen: vacía, o con la firma de AuthorSigner (alg 1) o de CmsSigner (alg 2) y el sello de Sealer, hechos antes de escribir nada y evaluados por el lector de esta librería;
    • las autocomprobaciones del §62.1: PUBLIC_HEADER, CONTROL_CBOR, el head, la trama de BODY, INNER_ACCESS_AGE con su .dkk, PAYLOAD_AGE y el área de seguridad;
    • EncryptOptions.now, obligatorio, es el reloj;
    • los errores son una DateKeysException con su código, un ArgumentError para las opciones que Go rechaza sin código, y una CapsuleWriteException para los demás.

    La longitud de SEALED_CONTROL sale de la forma de sus stanzas, sin el sellado de prueba con el que Go la mide. Los valores aleatorios de ese sellado se sacan y se tiran, para que los siguientes sean los de Go.

  • lib/datekeys.dart exporta encrypt3.dart, y X25519Recipient, checkX25519Recipient, RandomSource y secureRandom, que necesitan sus opciones. Tres pruebas de la 6a pierden un import que ya no hace falta.

  • Vectores de Go: tool/capsule_writer_go_vectors_test.go corre capsule.EncryptFiles como una prueba de Go en una exportación de datekeys-go, con crypto/rand leyendo el keystream de SeededRandomSource y los enganches de las pruebas de Go: una clave de autor de una semilla, y las firmas CMS y los sellos de internal/cms/cmstest, que fija testing/cryptotest.SetGlobalRandom. capsule_writer.json guarda 87 recetas, con el tamaño de cada valor que saca Go y lo que recibió y devolvió cada enganche:

    • 21 cápsulas, con su SHA-256, la cápsula entera si es pequeña, la .dkk, el Result, sus capas, y la apertura de Go con cada credencial, todas juntas y ninguna;
    • 66 errores, con su texto, su código y los bytes escritos antes: las opciones, las rutas, los textos, las credenciales, las extensiones, el reloj, el perfil, las fuentes que fallan o cambian en cada lectura, y los enganches.

    Las pruebas escriben cada receta otra vez: los mismos valores aleatorios, en el mismo orden y del mismo tamaño, las mismas peticiones a los enganches, y los mismos bytes o el mismo error. Esta librería abre cada cápsula como la abrió Go.

  • En la otra dirección: tool/capsule_interop_dart_samples.dart escribe doce cápsulas, seis con SeededRandomSource y seis con el CSPRNG de la plataforma, entre ellas tres MiB en tres ficheros, doscientos ficheros y catorce recipients con palabras y .dkk. tool/capsule_interop_go_verdicts.go las inspecciona y las abre con Go, con cada credencial, todas juntas y ninguna, codifica otra vez sus capas y su .dkk, y escribe las seis primeras con capsule.EncryptFiles y la misma semilla. Go abre cada cápsula con los ficheros de su receta, codifica sus capas igual y escribe los mismos bytes.

  • Pruebas: 175 nuevas en la VM y 73 en Node.js, 1888 y 505 en total. Además de los vectores: nada llega al sink antes de la firma; el sink recibe primero el PRELUDE, y cada trozo es suyo; un sink que falla para la escritura y se aborta, y uno que no se cierra no se aborta; una fuente o un enganche que lanza una DateKeysException conserva su código; un String mal formado no es UTF-8 válido para Go; una fuente se lee en streaming; y, en la VM, dos cápsulas del CSPRNG difieren y se abren.

  • Fallos inyectados, uno a uno y revertidos. Las pruebas detectan los veinte: los valores del sellado de prueba sin sacar; los huecos sin permutar; las rutas ordenadas por unidades UTF-16; un mtime anterior a 1970 guardado; un CR suelto del comentario conservado; el área de 64 KiB siempre que se permite; los veredictos del área sin comprobar; el SHA-256 de la segunda lectura sin comparar; una fuente que da un byte de más en un trozo, aceptada; la nota pública perdida; un recipient repetido aceptado; un credential_id a ceros; la llave de palabras salada con la ronda siguiente; el sink sin abortar y sin cerrar; un área de prueba con largeArea; un instante igual al reloj aceptado; el relleno con un byte de menos; la sal del head con un byte a cero; y SEALED_CONTROL_LEN con uno de más.

  • tool/encrypt3_bench.dart mide el escritor: una cápsula pequeña tarda 48 ms en la VM y 0,71 s en Node.js, 0,12 s y 0,75 s con time_and_key y una .dkk; un fichero de 64 MiB, 4,7 s en la VM, y uno de 16 MiB, 1,95 s en Node.js. Las cifras, en el README.

Etapa 7a: el localizador y el sobre de datekeys.capsule (06-10-2026)

  • El localizador y el sobre (lib/src/locator.dart), port del paquete locator de datekeys-go en c531e93, el borrador v0.12 (§43 a §44.1, con el cambio 6 del §76), salvo Seal, con las mismas comprobaciones en el mismo orden, los mismos códigos y los mismos textos de error:

    • los datos de la extensión: CapsuleInfo; parseCapsuleInfo, la nota con las reglas del §24.1, la DateKey canónica y un localizador que es un fichero age con un stanza tlock de su ronda, con ERR_EXTENSION_DATA_INVALID como único código; CapsuleInfo.toExtension, que lee lo que escribe (§72); y checkCapsuleData, el ValidateCapsule de locator.Standard;
    • las direcciones: checkAddressUri y LocatorAddress.host, sobre los bytes UTF-8 de la dirección y con el %q de Go; y checkResolvedIp, la comprobación de la IP a la que resuelve un nombre, que la app hace en cada conexión, sobre los 4 o 16 bytes de la dirección. Go no la tiene, porque su lector no descarga;
    • el localizador: unmarshalLocator, con la longitud que da Marshal y ninguna otra; Locator.marshal, con la clave 6 hasta el menor múltiplo de 4096 que llena, y locatorPlaintextLength; Locator.usable; y su apertura, openLocator y CapsuleInfo.openLocator, que lee como Go a lo sumo 1 MiB de texto, como io.LimitReader;
    • el sobre: Locator.restIn, Locator.openEnvelope, hideRest y splitEnvelope, el corte del fichero age de NewEnvelope.

    Los errores no llevan código, como en Go: son una LocatorException con su texto. Un Locator tiene los tipos de Go: claves y resúmenes de 32 bytes, y ningún tamaño ni desplazamiento negativo.

  • Las direcciones IP (lib/src/ipaddr.dart, interno): la aceptación exacta de netip.ParseAddr, su String y publicIP con los bloques del §44.1, byte a byte para que una IPv6 sea exacta en la web, y sin dart:io.

  • StandardExtensions comprueba por defecto la data de datekeys.capsule con checkCapsuleData, como locator.Standard de Go; con validateCapsule: null, solo que la haya, como extension.Standard. El escritor de la .dkk (lib/src/accesskey.dart) usa este último, como el de Go, y también las pruebas de los formatos que comparan con extension.Standard (formats_extension_test.dart y formats_differential.dart). La inspección y la apertura dan lo mismo que antes en cada fixture y cada vector.

  • lib/datekeys.dart exporta locator.dart.

  • Vectores de Go: tool/locator_go_vectors.go corre en el módulo de datekeys-go, sin cambiarlo, y escribe:

    • locator_uris.json: CheckURI y Address.Host en las 247 direcciones de locator.json y en 2 800 más, en cada borde del §44.1 y de una semilla; netip.ParseAddr en 1 700 cadenas; y publicIP, al que llega con go:linkname, en 868 direcciones;
    • locator_vectors.json: los textos de los demás casos de locator.json, PlaintextLength de -4100 a 16484, 482 plaintexts, Marshal en sus límites, 118 aperturas de cuatro rondas, ficheros que pasan de 1 MiB de texto, el sobre, sus restos y su corte, Info.Extension, Info.OpenLocator, ParseInfo, locator.Standard como registro y la apertura de un fixture cuya .dkk lleva datekeys.capsule.

    crypto/rand.Reader es un ChaCha8 de una semilla fija, así que la salida es la misma en cada ejecución. locator_uris.g.dart lleva todo el primero, y locator_vectors.g.dart una parte del segundo, para Node.js.

  • Pruebas. Todo se compara con Go: cada caso de locator.json, con su texto y su código, y cada uno de los dos ficheros de vectores. En Node.js corren las direcciones, las IP y una parte de los demás casos. 43 pruebas nuevas en la VM y 20 en Node.js: 1615 y 352 en total antes de integrarse; encima de la 6a, 1713 en la VM y 432 en Node.js.

  • Fallos inyectados, uno a uno y revertidos, cada uno con las pruebas del localizador en la VM y en Node.js. Las pruebas detectan los diez del encargo: un bloque no público aceptado en su primera dirección, y en su última; una IPv4 con un cero a la izquierda; el puerto :0443; el esquema en mayúsculas; un segmento de 64 caracteres; un CID con los bits sobrantes distintos de cero; una dirección de NAT64 con una IPv4 pública dentro; un resto con otro SHA-256; y la comparación de los bloques de IPv6 sobre 64 bits, que detecta Node.js y no la VM, donde un int tiene 64 bits exactos. Y cuatro de cinco más: el texto de un localizador leído más allá de 1 MiB, que detectan las pruebas de la VM, las únicas que escriben esos ficheros; el byte fuera de RFC 3986 citado como byte y no como runa; un último segmento numérico con ceros a la izquierda leído como nombre; y el registro por defecto sin la comprobación de datekeys.capsule. El quinto, una zona aceptada en un literal IPv6, no lo detecta ninguna prueba porque ninguna dirección llega a él: el % de una zona hace rechazar antes la autoridad, también en Go, cuya comprobación de la zona tampoco se alcanza.

  • Para la parte 7b: Seal y el cifrado de NewEnvelope, sobre el escritor de age de la 6a: un recipient tlock con wrapTlockStanza, un recipient X25519, el STREAM, la cabecera con su MAC y el azar, y después splitEnvelope.

Etapa 6a: la escritura de age (06-10-2026)

  • La escritura de los ficheros age (lib/src/age_writer.dart), port de age.Encrypt, internal/format e internal/stream de filippo.io/age v1.3.2, con sus comprobaciones y sus textos:

    • AgeEncryptor y ageEncrypt sacan la file key, envuelven la file key para cada recipient, en su orden y con sus etiquetas, calculan el MAC de la cabecera y sacan el nonce. Rechazan, con el texto de Go, una lista sin recipients, unas etiquetas que no se pueden mezclar, un recipient que no envuelve la file key y unos stanzas que no se pueden escribir;
    • AgePayloadEncryptor cifra el STREAM según llega el texto, como el EncryptWriter de Go: en chunks de 64 KiB, con el último completo cuando el texto es un múltiplo de 64 KiB distinto de cero y vacío solo cuando el texto lo es, y con el texto de Go tras close;
    • las longitudes de un fichero salen, antes de escribirlo, de la de su texto y de la forma de sus stanzas: ageStreamLength, ageStanzaLength, ageHeaderLength y ageFileLength.
  • Los recipients (lib/src/recipient.dart):

    • X25519Recipient, con las cadenas age1… y los textos de ParseX25519Recipient, y checkX25519Recipient, las reglas del §37 con los textos de agewrap;
    • ScryptRecipient, con su factor de trabajo y su etiqueta al azar;
    • TimeRecipient, el de tlock, con la etiqueta datekeys-tlock-… de agewrap;
    • generateX25519Identity, rawX25519Identity, rawX25519Recipient y la longitud de cada stanza.
  • La fuente de lo aleatorio (lib/src/random.dart): RandomSource, inyectable; secureRandom, la de por defecto, con Random.secure; y SeededRandomSource, determinista, el keystream de ChaCha20 bajo el SHA-256 de una semilla, para pruebas y vectores. Todo lo aleatorio de un fichero sale de ella, en el orden de Go, sigma de tlock incluido: encryptOnG2 y wrapTlockStanza la reciben, y ya no usan un Random.secure propio. Para los 16 huecos de la parte 6b trae además randomIndex, como crypto/rand.Int, y permute, como el de capsule.Encrypt.

  • Vectores de Go: tool/age_writer_go_vectors.go corre en una exportación de datekeys-go y hace que crypto/rand lea el keystream de SeededRandomSource. Así age.Encrypt, con los recipients X25519, scrypt y tlock reales, saca valores conocidos, y age_writer.json guarda cada uno, con su tamaño y su orden, junto a los ficheros:

    • X25519 sobre textos de 0, 1, 64 KiB − 1, 64 KiB, 64 KiB + 1, 128 KiB y más bytes, hasta 3 MiB;
    • dos, tres y dieciséis recipients X25519, y uno con el bit 255 a 1, que age acepta;
    • scrypt con factores de trabajo de 1 a 16;
    • el stanza tlock de rondas de 1 a 11 cifras.

    Cada fichero se comprueba con testkit.SealAge, que lo sella otra vez con la primera y la última extracción como file key y nonce, y Go lo abre. También guarda los errores de age.Encrypt y de los constructores, ParseX25519Recipient, CheckX25519Recipient, GenerateX25519Identity, crypto/rand.Int, el permute de capsule y las longitudes de testkit y capsule. age_writer.g.dart lleva el mismo JSON para Node.js.

  • En la otra dirección: tool/age_interop_dart_samples.dart escribe los ficheros de las recetas de test/age_interop_support.dart, y tool/age_interop_go_verdicts.go los abre con age y las identities de agewrap y escribe age_interop.json. Son X25519 de 0 a 3 MiB, uno escrito en trozos, tres y dieciséis recipients, tlock abierto con el release de la ronda 1000 de los fixtures, tlock sobre dieciséis X25519 como un SEALED_CONTROL y scrypt con los factores de trabajo 10 y 16. Las pruebas escriben cada fichero otra vez, y debe ser el que leyó Go; Go lo abrió y sacó el texto de la receta, o lo rechazó con la identity que no debía abrirlo; y esta librería hace con él lo mismo que Go, con los mismos textos y las mismas reglas de stanzas.

  • Pruebas: 98 nuevas en la VM y 80 en Node.js: 1670 y 412 en total. En Node.js corren los mismos vectores sin los casos caros: los ficheros de megabytes, scrypt con logN 16 y la mayoría de los de tlock.

  • Fallos inyectados, uno a uno y revertidos: los ocho del encargo, el sexto y el séptimo en dos formas. Las pruebas los detectan todos, en la VM y en Node.js: el flag del último chunk en uno que no lo es; un chunk vacío al final tras chunks llenos; un contador del nonce que no avanza; la sal de HKDF en el orden inverso; el MAC de la cabecera sobre los stanzas en otro orden; un recipient de orden bajo aceptado, al envolver la file key y en checkX25519Recipient; un recipient scrypt junto a uno X25519, sin comparar las etiquetas y con un scrypt sin etiqueta; y la misma clave efímera en varios stanzas.

  • tool/age_writer_bench.dart mide el escritor. En la VM, 64 MiB en streaming tardan 1,4 s, unos 46 MiB/s; compilado a JavaScript, 16 MiB tardan 0,33 s, unos 49 MiB/s. La cabecera de PAYLOAD_AGE cuesta 4 ms; INNER_ACCESS_AGE, con 16 stanzas, unos 45 ms en la VM y 35 ms en Node.js; OUTER_TIME_AGE, 40 ms y 0,55 s; y un fichero de clave de autor, con scrypt de logN 16, 0,6 s y 0,85 s. Las cifras, en el README.

Etapa 5c: la firma de alg 2 y el sello de seal_type 2 en los veredictos (06-10-2026)

  • Los veredictos de la firma con certificados y del sello RFC 3161 (lib/src/securitycms.dart), port de evaluateCMS, signerLine y evaluateSeal de signature2.go de datekeys-go en c531e93, el borrador v0.12, sobre el lector de CMS de la 5a, con el mismo orden de comprobaciones:

    • SIGNERS con su perfil, de 1 a 16 entradas en orden estricto, y después la SignedData: un fallo de cualquiera de los dos es F1;
    • cada firmante exigido, en el orden de SIGNERS, y cada ajeno, en el orden de la codificación, con su resultado: válido, inválido, ausente, no verificable, sin sello, con el sello inválido o con el certificado fuera de validez en t, el instante de su sello, con los dos extremos incluidos;
    • F2 si un exigido es inválido, F5 si falta algo o hay una clave 3, y F6 si todos son válidos y están sellados, con el Detail de los firmantes;
    • el sello sobre SEAL_SUBJECT: S2 por la forma, S1 por un algoritmo fuera de la tabla o un messageImprint que no es SHA-256, S3 si no verifica, y S4 o S5 con la autoridad y t;
    • un round_time en el instante cero de Go, 0001-01-01T00:00:00Z, cuenta como ninguno, como IsZero, y Verdicts.sealedAt no cuenta un sello en ese instante, como SealedAt.
  • El evaluador por defecto lo evalúa todo como Go. cmsReader es el cms por defecto de evaluateSecurity, y con él el de evaluateSecurityInput y de la apertura: nada que Go evalúe queda sin evaluar. Con cms: null, la firma de alg 2 y el sello de seal_type 2 siguen quedando sin evaluar.

  • lib/datekeys.dart exporta encodeSigners y maxSigners, como EncodeSigners y MaxSigners del paquete capsule de Go, y cmsReader. cms.dart sigue interno.

  • Vectores de Go: tool/security_go_vectors.go escribe además securitycms_vectors.json: 760 áreas cuya firma CMS o sello RFC 3161 hace el propio generador, como los hace internal/cms/cmstest para las pruebas de la referencia, con los veredictos de EvaluateSecurityIn, las líneas, el detalle de cada firmante y del sello y el sello más temprano:

    • un firmante exigido de cada resultado junto a ajenos de cada resultado, sin round_time, al abrir en el instante de los sellos, en el contexto de otro head y con una clave 3; tres firmantes exigidos, sacados de una semilla; SIGNERS de 16 y 17 entradas;
    • la validez de un certificado en el instante de su sello, al nanosegundo, de un UTCTime a un GeneralizedTime y con diez cifras de fracción, y la de la autoridad en el de su token;
    • t más la precisión frente a round_time, a un nanosegundo de cada lado, con cada forma de la precisión, en el instante cero de Go y en el último segundo de 9999;
    • un sello de cada veredicto junto a una firma de cada veredicto, y mutaciones de la SignedData, de SIGNERS y de los tokens.

    cmstest no se puede importar desde fuera del árbol de datekeys-go: el generador reescribe la parte que necesita. Las claves salen de etiquetas, ECDSA firma con el nonce del RFC 6979 y RSA con PKCS #1 v1.5, así que el fichero sale igual en cada ejecución, y security_vectors.json sale como antes. securitycms_vectors.g.dart lleva, para Node.js, una parte de los casos, con cada par de veredictos de cada grupo, y los fixtures format3_signed_cms y format3_sealed.

  • Pruebas. Todo se compara con Go:

    • los 135 casos de security_cms.json, con el resultado de cada firmante, el sello y las líneas, sin ningún carácter de control ni bidireccional;
    • los 24 de security.json, ya enteros;
    • las 56 firmas de alg 2 y los 105 sellos de seal_type 2 de security_vectors.json que la 5b dejaba sin evaluar;
    • los 760 casos de securitycms_vectors.json, con su detalle y su sello más temprano;
    • y los fixtures format3_signed_cms y format3_sealed: su área frente a su registro, y su apertura con cada credencial de open_cases.json.

    En Node.js corren una parte de los vectores y la apertura entera de los dos fixtures. 150 pruebas nuevas en la VM y 10 en Node.js: 1572 y 332 en total.

  • Fallos inyectados, uno a uno y revertidos: los ocho del encargo, el primero en dos formas, y tres más. Las pruebas los detectan todos, en la VM y en Node.js: un certificado comprobado con el reloj de la apertura, y otro en round_time, en lugar de en el instante de su sello; un sello aceptado con t más la precisión después de round_time; F6 sin que todos los firmantes sean válidos; más de 16 firmantes aceptados; el aviso de los sellos ausente de las líneas de F6; un nombre de 65 puntos de código mostrado; un messageImprint que no es SHA-256 aceptado; el resultado de un firmante ajeno en inglés; los firmantes ajenos en el orden inverso; un sello de la clave 3 comprobado sin el SIG_PART de la clave 2; y una clave 3 junto a una firma de alg 2 ignorada. Dos se escapaban al principio en Node.js, más de 16 firmantes y el orden de los ajenos, porque sus casos solo los leían las pruebas de la VM: la parte de Node.js lleva ahora SIGNERS de 16 y 17 entradas y dos firmantes ajenos.

  • tool/open_bench.dart abre también los dos fixtures y mide, dentro de cada apertura, la evaluación de su área de seguridad: en la VM, una apertura tarda unos 55 ms, de los que el área son 9,4 ms con la firma de dos firmantes sellados y 8,5 ms con la de alg 1 y el sello; compilada a JavaScript, unos 0,9 s, de los que el área son 0,13 y 0,05 s. Las cifras, en el README.

Etapa 5a: el lector de CMS, ECDSA y RSA (05-10-2026)

  • El lector de las firmas CMS y de los sellos RFC 3161 (lib/src/cms.dart), port de internal/cms de datekeys-go en c531e93, con el perfil de certificado del borrador v0.12 (§29.7, §29.10, §29.11), el mismo orden de comprobaciones y los textos de Go:
    • parseCert lee un certificado campo a campo, sin una librería de X.509; Cert da el titular, de givenName y surname antes que del commonName, el emisor, del organizationName si no hay un commonName con texto, y validAt, con los dos extremos incluidos;
    • parseSignature lee una firma separada: el ContentInfo, los certificados, las respuestas OCSP, los atributos firmados y sin firmar y cada SignerInfo, con su certificado; SignerInfo.check da CmsResult.valid, invalid o notVerifiable con la tabla cerrada de algoritmos, y una clave de otro esquema que el algoritmo es invalid;
    • parseToken lee un token y su TSTInfo campo a campo, con los errores de forma antes que los de algoritmo, y Token.check lo verifica sobre lo sellado: un messageImprint de otra longitud no vale, y la precisión llega hasta 2³¹ − 1 segundos;
    • los identificadores de objeto se comparan por sus bytes, y un SET OF puede repetir un elemento.
  • ECDSA (lib/src/ecdsa.dart, nist_curves.dart) en P-256, P-384 y P-521, como ecdsa.VerifyASN1 de Go, y RSA (lib/src/rsa.dart), PKCS #1 v1.5 y PSS como rsa.VerifyPKCS1v15 y rsa.VerifyPSS, de código propio sobre BigInt, que no es de tiempo constante: solo verifican, con valores públicos. Los hashes son los de package:crypto.
  • Vectores de Go: tool/cms_go_vectors_test.go corre como una prueba de Go en una exportación de datekeys-go, porque importa internal/cms, y con testing/cryptotest hace deterministas las claves y las firmas. Escribe cms_ecdsa.json, cms_rsa.json, cms_certs.json, cms_signatures.json, cms_algorithms.json, cms_tokens.json, cms_mutations.json y cms_corpus.json: las pruebas de internal/cms con sus resultados y textos, identificadores cuyos arcos dan la vuelta en 32 o 64 bits, certificados, firmas y tokens editados nodo a nodo y bit a bit, y cada firma y cada token de security_cms.json y de los fixtures format3_signed_cms y format3_sealed del borrador v0.12. cms_vectors.g.dart lleva una parte de cada uno para Node.js.
  • Pruebas. 44 nuevas en la VM y 17 en Node.js. Integrada encima de la 5b, el total es de 1422 pruebas en la VM y 322 en Node.js.
  • Fallos inyectados, uno a uno y revertidos: un identificador comparado como texto, un SET OF estrictamente ascendente, el titular tomado primero del commonName, una s de ECDSA sin comparar con el orden, el relleno de RSA sin comprobar su longitud (y, aparte, la longitud de la firma), un messageImprint de otra longitud aceptado, una precisión de 2³¹ segundos aceptada y un certificado válido un segundo fuera de su periodo. Las pruebas de la VM los detectan todos; las de Node.js, todos menos los de RSA y la precisión, cuyos casos no van en la parte de Node.js.
  • tool/cms_bench.dart mide ECDSA, RSA y el lector: en la VM, una verificación de P-256 tarda 2,4 ms y una de RSA-2048, 0,10 ms. Las cifras, en el README.

Etapa 5b: los compromisos, SECURITY_CBOR, la firma de alg 1 y los veredictos (05-10-2026)

  • testdata/ de la rama v0.12 de datekeys-go, en c531e93, cuyo testdata/ es el de 601e6d2, el de datekeys-ts: 133 ficheros, que dicen aún "spec": "0.11", como el SpecVersion de Go, hasta que el autor apruebe el borrador. Frente al tag spec-v0.11, trae los fixtures format3_unsigned y format3_note, format3_seal_unsupported con seal_type 4294967295, los registros nuevos de format3_sealed y format3_signed_cms, el corpus de 218 casos, note.json, security.json con su contexto y sus líneas, security_cms.json de 135 casos y locator.json, que es de la etapa 7.
    • Los generadores de tool/ que leen el testdata/ lo leen otra vez: mutation_texts.json, open_cases.json, formats_*.json con formats_vectors.g.dart, ibe_vectors.json y age_fixtures.json cambian; release_vectors.json, primitives.json y las vistas de open_vectors.g.dart salen iguales. age.json no lee el testdata/ y queda congelado.
    • Las pruebas cuentan 26 fixtures y 218 casos, la inspección de format3_note da la nota de su registro, y los 16 casos de note.json dan el resultado y el detalle de Go. Ningún caso descubrió una diferencia con Go: la librería no cambia.
  • Los compromisos (lib/src/author.dart), port de signature.go: payloadCommit, controlCommit sobre CONTROL_SIG en cada formato, headDigest, signersDigest, authorMessage con su prefijo y sus 99 bytes, authorCode, que toma los bytes como Go, sigPart y sealSubject.
  • SECURITY_CBOR (lib/src/security.dart), port de format3.go y de signature.go: el mapa exterior, author-signature y seal con los esquemas y los límites de Go, sus codificadores y sus lectores, y evaluateSecurity, que nunca lanza:
    • X para un mapa exterior que falla su capa 2 o 3, la versión 2 incluida;
    • F0 a F4 para la firma, con verifyStrict en alg 1 y la clave buscada entre las guardadas por su cadena dkauthor1…, y S0 a S2 para el sello;
    • un fallo dentro de una parte es de esa parte, F1 o S2, como Go recupera un panic;
    • sin contexto, lee como un lector de la v0.10.
  • La frontera con el lector de CMS de la parte 5c es CmsEvaluator, con evaluateSignature y evaluateSeal, como evaluateCMS y evaluateSeal de Go. Sin él, la firma de alg 2 y el sello de seal_type 2 en un contexto quedan sin evaluar, en null, nunca con un veredicto supuesto; la otra parte se evalúa igual. holderText, el nombre de un certificado del §29.7, ya está aquí.
  • Los veredictos (lib/src/verdicts.dart) con los textos del borrador v0.12: Verdict.text, Verdicts.lines, con los nombres entre « y », la autoridad de cada sello en las líneas de F6 con su aviso, los resultados en español y t en RFC 3339 con su fracción, y Verdicts.sealedAt, con Detail, SignerLine y SignerResult. Verdicts puede tener evaluada una parte sola. Partir las líneas en filas con ↳ es de la CLI de Go, no de su librería, y no se porta.
  • La apertura evalúa el área como Go: OpenOptions.evaluator es ahora evaluateSecurityInput, con securityContext, el newSecurityContext de Go con el head_digest. format3_signed, format3_unsigned, format3_signature_unsupported, format3_seal_unsupported, format3_security_v2 y format3_note dan al abrirse los veredictos y las líneas de Go; format3_signed_cms y format3_sealed, la parte que no necesita el lector de CMS.
  • lib/datekeys.dart exporta author.dart y security.dart, como el paquete capsule de Go.
  • Vectores de Go:
    • tool/security_go_vectors.go, en el contexto del módulo de datekeys-go en c531e93, escribe security_vectors.json: los compromisos, también del control de cada fixture en cada formato; los codificadores; 1730 evaluaciones de EvaluateSecurityIn en 23 contextos y sin contexto, del mapa exterior, de author-signature y de seal rotos de todas las formas de sus esquemas y en sus límites, de firmas de alg 1 válidas e inválidas, también los casos de «Taming the many EdDSAs» hechos sobre AUTHOR_MESSAGE buscando el contexto, y de 1500 mutaciones de una semilla fija; las líneas y SealedAt de veredictos con cada detalle; y holderText, al que llega con go:linkname. security_vectors.g.dart lleva todo salvo siete de cada ocho evaluaciones, para Node.js;
    • tool/open_go_vectors.go guarda en open_cases.json los veredictos de cada cápsula del formato 3 que se abre, con sus líneas, y abre los fixtures firmados con alg 1 también con su clave guardada, F3, y con otra, F4.
  • Pruebas. Las 1730 evaluaciones de Go, con sus veredictos, sus líneas, alg y seal_type y las partes del lector de CMS; los 24 casos de security.json, 23 enteros y uno sin su sello de seal_type 2; los compromisos, la firma y el sello de cada fixture del formato 3 frente a su registro; los veredictos de cada apertura de open_cases.json y de los 11 casos del corpus que se abren; y la frontera con un lector de CMS de prueba. 58 pruebas nuevas en la VM y 14 en Node.js: 1378 y 305 en total.
  • Fallos inyectados, uno a uno y revertidos: 12, y las pruebas los detectan todos en la VM y en Node.js: los compromisos de D y de SEAL_SUBJECT en otro orden, el prefijo de AUTHOR_MESSAGE cambiado, una clave de orden pequeño aceptada, F4 con la clave guardada y F3 con la etiqueta de otra clave, un evaluador que falla la apertura, la línea de F0 y el resultado de un firmante en inglés, un nombre de 65 puntos de código mostrado, y F1 o S1 supuestos para alg 2 o seal_type 2 sin el lector de CMS.

Etapa 4c: el head, la nota pública, la inspección y la apertura (05-10-2026)

  • La nota pública del §24.1 (lib/src/note.dart), port de CheckNote, NewNote y Note de extension de datekeys-go en c531e93: checkNote y checkNoteData, con la longitud, el UTF-8 y las reglas de texto del autor declarado en ese orden y con los textos de Go; newNote, publicNote y unusableNote, y Header.publicNote y Header.unusableNote. StandardExtensions comprueba ya la nota como el Standard de Go, y su parámetro validateNote desaparece.
  • El head del formato 3 (lib/src/head.dart): decodeHead en las capas del §69.1, con R1 y R8 en la tercera, sobre los bytes UTF-8 de las rutas, nunca sobre sus unidades UTF-16; las reglas de rutas, del comentario y del autor de pathrule en la cuarta, como ERR_HEAD_INVALID con el texto de Go; la maquetación de los ficheros por restas; las extensiones críticas del head; checkHeadEnd; y encodeHead.
  • La inspección, pasos 1 a 8 (lib/src/inspect.dart): inspectCapsule e inspectCapsuleSource, Inspection con la comprobación de cada paso y su detalle, como el Inspection de Go; inspectedLength, para leer de un fichero grande solo su principio; maxAccessKeyRead; y inspectView e inspectJson, la salida exacta de datekeys inspect -json.
  • La apertura, pasos 9 a 18 (lib/src/open.dart, open3.dart): openCapsule, de una cápsula en memoria, y openCapsuleSource, de una ByteSource que se lee por tramos (lib/src/source.dart); OpenOptions y Opened, como los de Go.
    • La .dkk del paso 9.a, decodificada o todavía codificada, con su material, sus extensiones críticas, su capsule_id y su capsule_digest, calculado sobre la fuente por tramos; las identities X25519 del llamador y la llave de palabras como una identity más.
    • El release con la regla del paso 9, nunca antes de su ronda, y su verificación en el paso 10; OUTER_TIME_AGE con el stanza tlock; INNER_ACCESS_AGE con las reglas de los huecos; CONTROL_CBOR, header_binding, I_PAYLOAD y P.
    • PAYLOAD_AGE en streaming: el contenido de los formatos 1 y 2 a un ByteSink según age autentica cada chunk, con el relleno del formato 2 comprobado y nunca entregado; y en el formato 3 la trama de BODY, el área, el head y cada fichero a un FileSink, con su SHA-256, en el orden y con la precedencia del §63 (lib/src/sink.dart, con MemoryByteSink y MemoryFileSink).
    • Cada fallo con el código, el paso y el texto de Go, también los de age, por fases, como classify, y los de la fuente del release, como sourceFailure. Nada se presenta como válido antes de que acabe el paso 17: la salida se cierra al publicar y se aborta tras cualquier fallo, y el sink se aborta tras cualquier fallo posterior a su begin (§56).
  • La firma y el sello quedan para la etapa 5, detrás de un punto de enganche (lib/src/verdicts.dart): el área de security se lee solo hasta donde lo exigen la trama de BODY y el área, y sus veredictos los da un SecurityEvaluator, que recibe SECURITY_CBOR, los bytes del head, el control y el formato, el round_time y las claves de autor, lo que toman newSecurityContext y EvaluateSecurityIn de Go. El de hoy, notEvaluated, no evalúa nada, y uno que falla no impide abrir. OpenOptions.accept, el Accept de Go, ve los veredictos antes del paso 18 y puede negarse a publicar los ficheros.
  • AgePayloadDecryptor.wipe borra la clave del STREAM de una apertura que acaba antes del final de PAYLOAD_AGE.
  • Vectores de Go:
    • tool/mutation_go_texts.go, port de scripts/mutation-go-texts.go de datekeys-ts sobre el testdata/ de este repositorio, escribe mutation_texts.json: el texto de capsule.Open y sus comprobaciones, con su detalle, en los 210 casos del corpus. Go en c531e93 y el corpus de spec-v0.11 coinciden en el código y el paso de todos, y Go en el tag spec-v0.11 da el mismo fichero, byte a byte;
    • tool/open_go_vectors.go, en una exportación de datekeys-go porque usa internal/testkit, escribe open_cases.json (cada fixture con cada credencial, y 117 aperturas de fixtures editados o con otras opciones, en cada paso que el corpus no alcanza, con los sinks y la salida que fallan y el rechazo de Accept), open_heads.json, open_notes.json, open_inspect.json (el texto de capsule.Inspect en las 5110 mutaciones de inspect_differential.json, donde Go en c531e93 y el fichero también coinciden, y la salida de la CLI con notas públicas) y open_vectors.g.dart, con siete fixtures pequeños y una parte de cada fichero para Node.js. Los casos editados se sellan otra vez con las claves y los nonces de los fixtures, así que la salida es la misma en cada ejecución.
  • Pruebas. Los 24 fixtures se abren con cada credencial que documentan, y sus 24 .inspect.json salen byte a byte; los 210 casos del corpus dan el código, el paso, el texto y cada comprobación de Go; los 169 casos de open_cases.json, también el estado del sink, el contenido o los ficheros y las extensiones inutilizables; todo, en memoria y desde una fuente que se lee a trozos, con el mismo resultado. Cápsulas de varios MiB, hechas desde los fixtures, prueban el streaming: la salida recibe cada chunk al autenticarse, y un chunk posterior que falla la aborta sin cerrarla. Una cápsula con un stanza para una llave de palabras se abre con ella. 518 pruebas nuevas en la VM y 83 en Node.js: 1320 y 291 en total.
  • Fallos inyectados, uno a uno y revertidos: 15. Las pruebas los detectan todos en la VM: un paso fuera de orden, el capsule_digest sin comprobar, la salida publicada antes del final del paso 17, el SHA-256 de un fichero sin comparar, el relleno sin comprobar en los formatos 2 y 3, R8 sobre unidades UTF-16, una nota de 1025 bytes aceptada, un fallo de age clasificado en la otra fase, el error de una fuente con su propio código en el paso 9, una identity que abre dos stanzas aceptada, un head inválido informado sin leer hasta el final, el autor comprobado antes que el comentario, el sink sin abortar y el prefijo de la inspección un byte corto. En Node.js, 13: el orden del comentario y del autor y el prefijo solo los ven las pruebas de la VM.
  • tool/open_bench.dart mide la apertura: en la VM, una cápsula pequeña tarda unos 50 ms y 64 MiB en streaming, 1,5 s en el formato 1 y 2,6 s en un fichero del formato 3. Las cifras, en el README.
  • Un fallo de dart2js de Dart 3.13, ajeno a la librería: un objeto que llega al campo de otro a través de c ? null : objeto puede perder las escrituras que reciba allí. El README lo explica.

Etapa 4a: rutas, textos y llave de palabras (05-10-2026)

  • Las reglas de rutas y de textos (lib/src/pathrule.dart), port de internal/pathrule de datekeys-go en c531e93, sobre las tablas de Unicode 18.0.0 y WindowsBestFit que genera datekeys-go (lib/src/pathrule_tables.dart):
    • NFD con el orden canónico y Hangul, el pliegue de CaseFolding con U+0131 → U+0069, la minúscula simple y Default_Ignorable con la lista blanca de R4;
    • las reglas de una ruta, de R2 a R6c y R10, con las proyecciones de las 15 tablas best-fit; las del árbol, R7 con su clave y las dos rutas que nombra, y R9; y las de los textos del §29.6, el comentario y el autor declarado;
    • los textos de error de Go, en PathRuleException, sin código normativo: la cabecera y la nota lo pondrán en la etapa 4c. Como en Go, cada regla devuelve su violación y solo las funciones públicas lanzan;
    • canonicalTables, el texto de pathrule.Canonical: una prueba recalcula tablesDigest desde las listas.
  • Bytes, como Go. Las funciones acabadas en Utf8 toman los bytes de un string de Go, donde un byte que no es UTF-8 válido es U+FFFD, y las demás un String como lo escribe utf8Bytes. Los límites cuentan bytes, y R6b puntos de código, nunca unidades UTF-16. Así cualquier entrada da el resultado de Go, también el final de R4b tras bytes no válidos, que Go cuenta como U+FFFD de tres bytes.
  • La llave de palabras del §38.1 (lib/src/wordkey.dart), port de wordkey: normalizeWords, checkWords con los textos de Go, wordKey, con el PBKDF2-HMAC-SHA256 de la etapa 2 y 600 000 iteraciones, wordKeyPassword y wordKeySalt, la P y la S del §38.1, y wordIdentity. lib/datekeys.dart exporta normalizeWords, checkWords, wordKey, WordKeyException y sus constantes, como el paquete público wordkey de Go; wordIdentity queda interna, porque devuelve una identity de age.dart.
  • Vectores de Go en test/vectors/, con su copia en Dart para Node.js:
    • pathrule_vectors.json, de tool/pathrule_go_vectors.go, que corre en una exportación de datekeys-go porque internal/pathrule no se puede importar desde fuera de su árbol: los casos de las pruebas de Go y de datekeys-ts, los dos lados de cada límite de R2, R3 y R6b, 1300 cadenas y 350 árboles de una semilla fija, también con bytes que no son UTF-8 válido, los casos de R9 y, por plano, el SHA-256 de una línea por punto de código de cada función;
    • wordkey_vectors.json, de tool/wordkey_go_vectors.go: 400 textos y sus palabras, 515 listas de palabras con el resultado de Check y cuatro llaves, la primera la del §38.1.
  • Pruebas. 31 nuevas en la VM y 18 en Node.js. Integrada encima de la 4b, el total es de 802 pruebas en la VM, sin ninguna aplazada, y 208 en Node.js. Cada punto de código de los 17 planos da el resultado de Go en las siete funciones; los de los planos 0, 1 y 14, también en Node.js. Las llaves de 600 000 iteraciones corren solo en la VM.
  • Fallos inyectados, uno a uno y revertidos: 45, en el orden canónico, los ignorables, la lista blanca, el pliegue y la minúscula, las descomposiciones, los límites, las cuentas en UTF-16, las tablas best-fit, Hangul, los bytes no válidos, R4b, R6, R7, R9, R10, los textos, el texto canónico, y en la llave de palabras el separador, los espacios, las marcas, la cuenta de letras, las palabras repetidas y la sal. Las pruebas los detectan todos, cada uno con una prueba que corre también en Node.js. Cuatro se escapaban al principio, uno de R2, uno de R3 en unidades UTF-16, U+036F en las palabras y DEL en checkWords: los vectores llevan ahora los dos lados de cada límite.
  • tool/pathrule_bench.dart mide las reglas y la llave de palabras: en la VM, una ruta tarda de 9,5 a 28 µs, un comentario de 16 KiB 0,7 ms y la llave 1,1 s. Las cifras, en el README.

Etapa 4b: los formatos de la cápsula y de la llave de acceso (05-10-2026)

  • Las tramas (lib/src/framing.dart): el PRELUDE de un .dkc y la trama de una .dkk, con las comprobaciones de los §23 y §40 en su orden y los textos de Go; splitCapsule, los pasos 1 a 3 de la inspección, con su FramingException; y headerBinding. El formato de una cápsula es el enum CapsuleFormat.
  • El relleno (lib/src/padding.dart): paddedLength y payloadAgeLength con los códigos de PaddingRule, exactos hasta L_MAX también en la web, sin desplazamientos ni máscaras de más de 31 bits; y PaddingCheck, la comprobación del plaintext de PAYLOAD_AGE frente a L y P del paso 17, por trozos.
  • BODY del formato 3 (lib/src/body.dart): su trama y el área, sin el head. Y el capsule_digest incremental (lib/src/digest.dart). Son internos, como en datekeys-ts.
  • Las extensiones (lib/src/extension.dart): las reglas de un array, al decodificarlo y antes de escribirlo; los registros, con la comprobación de la data y los lugares del §72; las extensiones críticas y no críticas de cada objeto; y la regla de los codificadores del §72, checkWrite, con StandardExtensions, a la que se dan las comprobaciones de la nota y del localizador.
  • El Provider Profile (lib/src/profile.dart): su CBOR, profile_hash, las reglas 1 a 3 del §12.1 en su orden, el chain hash de drand, maxRound y el registro con Quicknet pinneado. Profile implementa el PinnedProfile de la etapa 3.
  • La DateKey (lib/src/datekey.dart): dk1_ con la aceptación y los textos de Go, también los de su JSON; la resolución de un instante a su ronda y la hora de una ronda; e Instant, al nanosegundo, con el RFC 3339 de time.Parse y de Format de Go.
  • PUBLIC_HEADER, CONTROL_CBOR de las versiones 1 a 3 y la .dkk (lib/src/header.dart, control.dart y accesskey.dart), en las capas del §69.1. I_PAYLOAD y access_material se copian una vez y se borran en todos los caminos.
  • Los errores que Go devuelve sin código normativo, como un código de relleno que no existe o una extensión fuera de su sitio al escribir, son ArgumentError con el texto de Go.
  • lib/datekeys.dart exporta los formatos, como index.ts de datekeys-ts.
  • Vectores de Go (tool/formats_go_vectors.go, en el contexto del módulo de datekeys-go en c531e93, sin cambiar nada en él): unos 6 400 casos en test/vectors/formats_*.json, con el resultado, el código y el texto de Go: tramas, cabeceras, controles, .dkk, perfiles, extensiones, dk1_, RFC 3339, rondas, relleno hasta L_MAX, la comprobación del relleno de capsule.Open, los codificadores, los límites del §57, BODY, y cada fallo de una lista, solo y con cada otro, para la precedencia del §69.1. formats_vectors.g.dart lleva uno de cada ocho para Node.js. Con el tag spec-v0.11 el generador da la misma salida, salvo la regla de los codificadores del §72, que ese tag no tiene.
  • Pruebas. Los 24 fixtures y las 6 .dkk; dk1.json, quicknet_rounds.json, profile_quicknet.json, padding.json y los 172 esquemas de cbor.json que la etapa 1 dejó aplazados; y el diferencial. 404 pruebas nuevas en la VM y 77 en Node.js: 771 y 190 en total, sin ninguna aplazada.
  • Fallos inyectados, uno a uno y revertidos: 24. Las pruebas detectan 21, en la VM, en Node.js o en las dos; el de Padmé con desplazamientos de más de 31 bits, solo en Node.js, como debe ser. Dos no se detectaban al principio, y por ellos el generador escribe ahora cada par de fallos de capas distintas y cada bit de FLAGS y RESERVED: una DateKey comprobada antes que la regla entre los arrays de extensiones, y el bit alto de FLAGS ignorado. Los otros tres no cambian ningún resultado: quitar la comprobación de la recodificación de PUBLIC_HEADER o de CONTROL_CBOR, porque sus decodificadores, como los de Go, ya rechazan toda forma no canónica, y leer una longitud con un desplazamiento de 24 bits, porque los operadores de bits compilados a JavaScript dan 32 bits sin signo.

Etapa 3: BLS12-381 y tlock (05-10-2026)

  • BLS12-381, de código propio, como lo calcula kilic/bls12-381 v0.1.0 para drand/kyber-bls12381 v0.3.4:
    • la capa del cuerpo, Fp, un extension type sobre BigInt, con FpWide para las sumas de productos sin reducir; la única que toca la representación, para que unos limbs fijos la puedan sustituir sola;
    • Fp2, Fp6 y Fp12 con las fórmulas de kilic, reduciendo cada coeficiente una vez, el Frobenius con sus coeficientes y el cuadrado ciclotómico;
    • G1 y G2, su codificación comprimida y los veredictos de FromCompressed (§12.2), con checkCompressedPoint, que lib/datekeys.dart exporta como datekeys-ts. El subgrupo de G2 se comprueba con ψ(P) = ·P;
    • el emparejamiento ate óptimo con las rectas y la exponenciación final de kilic: GT es su valor, serializado c1 antes que c0 en cada nivel;
    • el hash a G1 del RFC 9380 con el DST de Quicknet y de tlock, sumando las salidas del mapa en E′ como kilic.
  • El IBE de tlock (lib/src/ibe.dart), DecryptCCAonG2 y EncryptCCAonG2 de drand/kyber v1.3.2 para Quicknet, como ibe.ts: H2 sobre GT en el orden de kilic, H3 con su rechazo de candidatos, H4, la identidad de la ronda y las puertas de la firma y de U, con razones y textos fijos que no llevan ningún valor del cálculo. El cifrado admite un sigma dado, para reproducir byte a byte los vectores de Go; es interno hasta el escritor, la etapa 6.
  • Releases (lib/src/release.dart): verifyRelease es provider.Verify, en su orden y con sus textos, solo para el scheme de Quicknet, como release.ts; suppliedRelease; y fetchRelease, la regla del paso 9: lo que lance una fuente es ERR_RELEASE_UNAVAILABLE. Los exporta lib/datekeys.dart, con PinnedProfile, lo que lee del perfil, que llega con la etapa 4.
  • El stanza tlock (lib/src/tlock.dart): unwrapTlockStanza hace lo que NewTimeIdentity y su Unwrap con los argumentos y el cuerpo del stanza, y wrapTlockStanza lo que NewTimeRecipient, con los códigos y los textos de agewrap. Es interno: lo usará la apertura de la etapa 4.
  • Diferencias con Go, a propósito, las de datekeys-ts: otro scheme que el de Quicknet da ERR_UNKNOWN_PROFILE con un texto propio, y el punto en el infinito nunca es una firma válida, mientras que Go la acepta si la clave pública también es el punto en el infinito, una clave que ningún perfil pinneado tiene.
  • BigInt no es de tiempo constante. Verificar y descifrar solo manejan datos públicos; al cifrar, sigma y r son secretos. El README lo explica.
  • Vectores de Go en test/vectors/, que escriben cuatro programas de tool/ con las librerías de la caché de módulos, en el contexto del módulo de datekeys-go y sin cambiar nada en él:
    • bls12381_vectors.json: las 157 codificaciones límite de datekeys-ts con el veredicto de Go, y decodificaciones, sumas, múltiplos, emparejamientos, hashes a G1, el mapa de un elemento y firmas BLS con una semilla fija;
    • ibe_vectors.json, port del generador de datekeys-ts sobre los fixtures de testdata/: GT y H2, H3, H4, identidades de rondas, el stanza de cada fixture con su file key, los ciphertexts de kyber y los veredictos de DecryptCCAonG2;
    • tlock_vectors.json, port también: el cifrado de kyber con sigma fijo y los ciphertexts que escribió datekeys-ts y abre Go;
    • release_vectors.json: los veredictos, códigos y textos de provider.Verify, NewTimeIdentity con Unwrap y NewTimeRecipient.
  • Pruebas. 71 nuevas en la VM y 30 en Node.js. La etapa se hizo en paralelo con la 2, en otra rama, desde la etapa 1; con las dos integradas hay 367 pruebas en la VM, más una aplazada, y 113 en Node.js. Las que leen los vectores llevan @TestOn('vm'); en Node.js corren las propiedades de la aritmética y unos pocos vectores de Go copiados en Dart, que una prueba en la VM compara con los JSON. Un emparejamiento tarda allí un cuarto de segundo, así que los bucles sacan menos casos.
  • Fallos inyectados, uno a uno y revertidos: 25, en la torre, las curvas, el emparejamiento, el hash, el IBE, los releases y el stanza. Las pruebas los detectan todos, en la VM y en Node.js.
  • tool/bls12381_bench.dart mide BLS12-381 y tlock en la VM y compilado a JavaScript: en la VM, un emparejamiento tarda unos 11 ms, la verificación de la firma de una ronda 16 ms y el descifrado de un stanza 20 ms. Las cifras, en el README.

Etapa 2: primitivas y age (05-10-2026)

  • Primitivas, de código propio. Son internas: lib/datekeys.dart no las exporta.
    • SHA-256 con su compresión, y HMAC-SHA256 con los estados interior y exterior de la clave calculados una vez. Sobre ellos, HKDF-SHA256 (RFC 5869) y PBKDF2-HMAC-SHA256 (RFC 8018), cuyas iteraciones son dos compresiones sobre palabras: 600 000 iteraciones tardan 1,1 s en la VM, frente a unos 3,5 s con el HMAC de package:crypto.
    • scrypt (RFC 7914) con Salsa20/8, con las comprobaciones y los textos de scrypt.Key de Go.
    • ChaCha20, Poly1305 en diez limbs de 13 bits, y ChaCha20-Poly1305 (RFC 8439), con el tag comparado en tiempo constante.
    • X25519 (RFC 7748) sobre el cuerpo de TweetNaCl en doubles, con clamping y el secreto todo a ceros rechazado con el texto de crypto/ecdh.
    • Ed25519 estricto (§29.9), port de internal/ed25519strict: verifyStrict, canonical, smallOrder y onCurve. La aritmética de los puntos es la de TweetNaCl; los escalares módulo ℓ y onCurve usan BigInt, que no es de tiempo constante, solo con datos públicos.
    • El Base64 de Go, con su modo estricto y el offset de sus errores, y el Bech32 de age, con sus textos.
    • De package:crypto solo se usa SHA-512, en Ed25519.
  • Lectura de age v1, port de filippo.io/age v1.3.2 (lib/src/age.dart):
    • la cabecera y sus límites de internal/format, con los textos de error de Go;
    • el MAC de la cabecera, sobre la cabecera escrita otra vez como la escribe age;
    • los stanzas X25519 y scrypt. La identity de scrypt rechaza por defecto un factor de trabajo por encima de 16, como authorkey de Go; el de age es 22;
    • la clave del payload y el STREAM de internal/stream, que se descifra según llega el texto cifrado, con los mismos casos de fin de fichero que el lector de Go: sin chunk final, chunk que no se autentica, último chunk vacío y datos tras el final;
    • cada fallo es una AgeException con el texto de Go y su fase, la cabecera o el payload, para que la etapa 4 dé las razones fijas de capsule.classify. La DateKeysException de una identity pasa sin cambios.
  • agewrap (lib/src/agewrap.dart): las reglas de stanzas de OUTER_TIME_AGE, PAYLOAD_AGE e INNER_ACCESS_AGE, la lectura de los stanzas sin secretos, PayloadIdentity y AccessIdentity, con los textos y los códigos de Go. checkTimeStanzas recibe la ronda, el chain hash y el id del perfil, que llegará con la etapa 4.
  • Vectores de Go en test/vectors/, que escriben tool/gen_primitive_vectors.go y tool/gen_age_vectors.go con las librerías de la caché de módulos, en el contexto del módulo de datekeys-go y sin cambiar nada en él:
    • primitives.json y su copia en Dart, primitives.g.dart: RFC 5869, 7748, 7914, 8032 (por sign.input de Go) y 8439, PBKDF2 con el vector del §38.1, los puntos de orden pequeño, Base64 y Bech32. Incluye dos mensajes con los que una suma de productos de Poly1305 pasa de 2^32: un acarreo tomado con un desplazamiento fallaría en la web, y una prueba lo detecta en Node;
    • age.json: ficheros X25519 y scrypt, sus cortes y manipulaciones, un corpus de cabeceras contra la gramática del §28.1 y el límite de 2 MiB, y las reglas e identities de agewrap, con el texto de Go en cada caso;
    • age_fixtures.json: el PAYLOAD_AGE de los 24 fixtures con su payload_identity, y el INNER_ACCESS_AGE de los 6 time_and_key, sacado de OUTER_TIME_AGE con el release del fixture.
  • Pruebas. 296 en la VM y una aplazada, 127 más que en la etapa 1; 83 en Node.js, 27 más. Las de las primitivas y las de age sin ficheros corren también compiladas a JavaScript, sin los casos largos (PBKDF2 a 600 000 iteraciones, scrypt con logN 16, X25519 iterada 1000 veces).
  • Fallos inyectados, uno a uno y revertidos: 46 en las primitivas, age y agewrap. Las pruebas detectan todos los que cambian un resultado. Cuatro no lo cambian: la máscara de una mitad de rotación de SHA-256 cuyos bits altos solo llegan a sumas enmascaradas, el redondeo de _car cuando sus entradas nunca son negativas, el de un acarreo del producto del cuerpo, que conserva el valor, y el acarreo del limb 1 de Poly1305 tomado con desplazamiento, cuya suma no pasa de 2^32.
  • tool/bench.dart mide las primitivas en la VM y compilado a JavaScript; las cifras, en el README.

El gate con Node.js (05-10-2026)

  • tool/check.sh corre también dart test -p node: las pruebas que no leen ficheros, compiladas a JavaScript, comprueban en cada commit que los enteros son exactos en la web. Lo decidió el autor.

Etapa 1: errores, bytes, CBOR y DER (05-10-2026)

  • Errores normativos (§69). ErrorCode con los 19 códigos en el orden del spec y DateKeysException, con el mensaje contexto: CÓDIGO de Go. wrap y withContext hacen lo que fmt.Errorf("prefijo: %w"), y errorCode lo que datekeys.Code. Una prueba compara el catálogo con las líneas ERR_ del §69, leído de datekeys-go en el tag spec-v0.11.
  • Bytes. Hexadecimal, comparación y concatenación.
    • UTF-8 estricto: rechaza lo que rechaza utf8.Valid de Go y conserva un U+FEFF inicial. El Utf8Decoder de dart:convert lo descarta, en la VM y en la web, así que no se usa.
    • Un String con un surrogate suelto se escribe en UTF-8 generalizado (WTF-8), que no es UTF-8 válido.
    • utf8.DecodeRune y el %q de Go (strconv.Quote). La tabla de strconv.IsPrint de Go 1.26 se copia de datekeys-ts, y una prueba fija el SHA-256 del conjunto de runas.
  • CBOR (§58, §58.1). Port de codec/codec.go: CborEncoder, CborDecoder, unmarshalCbor, peekSchema, checkSchema y walkCbor, con los mismos textos de error.
    • Los enteros son exactos en la VM y en la web: un int hasta 2^53-1 y un BigInt por encima, también en las claves de los mapas y en los textos de error.
    • El Encoder de Go rechaza el UTF-8 inválido; aquí el único texto inválido es un surrogate suelto, y el error lo cita como Go citaría sus bytes en UTF-8 generalizado.
    • CborEncoder.uint exige 0..2^53-1, con un texto propio, como en datekeys-ts; uint64 escribe hasta 2^64-1.
  • DER estricto. Port de internal/der/der.go en 601e6d2, ya del borrador v0.12: check, split, content, setOfSorted y parseTime. parseTime devuelve un DerTime con los campos y los segundos Unix, exacto al nanosegundo. Es interno: lib/datekeys.dart no lo exporta.
  • Pruebas. Port de errors_test.go, codec_test.go, internal_test.go, vectors_test.go y der_test.go, de cbor.test.ts y de bytes.test.ts, con los textos de error que imprime Go.
    • Los objetivos de fuzzing de Go son propiedades con semilla, contra un codificador y un decodificador de referencia escritos aparte, como internal/cbortest.
    • testdata/vectors/cbor.json: los 36 accept y los 67 reject, con los límites de walk y los valores. Los 172 vectores de schemas se leen y se aplazan: necesitan los decodificadores de esquema de la etapa 4.
    • 169 pruebas en la VM y una aplazada. Las 56 que no leen ficheros pasan también en Node.js (dart test -p node); las que leen ficheros llevan @TestOn('vm').

Etapa 0: el paquete (05-10-2026)

  • Paquete datekeys en Dart puro, sin Flutter, para Dart 3.13 y sin publicar (publish_to: none).
  • testdata/ sincronizado con el tag spec-v0.11 de datekeys-go (ae33434): 124 ficheros. Su testdata/SOURCE.json es el mismo, byte a byte, que escribe datekeys-ts para ese commit.
  • tool/sync_testdata.dart, el equivalente de scripts/sync-testdata.mjs. test/testdata_test.dart comprueba la copia y que cada fichero nombre la versión de la especificación.
  • tool/check.sh, el gate local.
  • Dependencias: package:crypto en ejecución y package:test en desarrollo.
  • Licencia Apache-2.0.

Powered by TurnKey Linux.