Stage 7b: exports, README, changelog; the author key signs in the writer

Rebased on stage 6b. The hook of the signature of alg 1, which 6b named
AuthorKey, is now AuthorSigner, and AuthorKey of authorkey.dart implements
it. The tests of the writer sign the capsules of alg 1 with the AuthorKey
of Go's seed instead of replaying the recorded signature, and write the
bytes of Go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 1 day ago
parent 2f35979e45
commit d77ef9edec

@ -4,13 +4,37 @@ Cambios notables de la librería Dart. El proyecto usa versionado semántico; mi
## Especificación 0.11, en la rama `v0.11` — sin versión ## Especificación 0.11, en la rama `v0.11` — sin versión
### 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) ### 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: - **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; - `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; - `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 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 `AuthorKey` (`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; - 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; - 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; - `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. - 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.
@ -26,7 +50,6 @@ Cambios notables de la librería Dart. El proyecto usa versionado semántico; mi
- **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. - **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. - **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. - **`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.
- **Para después de integrar la 7b:** la firma de `alg` 1 con la clave de autor de la 7b, que implementa `AuthorKey`.
### Etapa 7a: el localizador y el sobre de `datekeys.capsule` (06-10-2026) ### Etapa 7a: el localizador y el sobre de `datekeys.capsule` (06-10-2026)

@ -6,7 +6,7 @@ Es la tercera implementación de la especificación, después de la de referenci
## Estado ## Estado
Están hechas las etapas 0 a 5 del plan (`docs/PLAN_dart.md` del espacio de trabajo), las partes 6a y 6b de la etapa 6 y la parte 7a de la etapa 7: Están hechas las etapas 0 a 5 del plan (`docs/PLAN_dart.md` del espacio de trabajo), las partes 6a y 6b de la etapa 6 y la etapa 7:
- la etapa 0, el paquete, sus herramientas y `testdata/` sincronizado; - la etapa 0, el paquete, sus herramientas y `testdata/` sincronizado;
- la etapa 1, los errores normativos, los bytes, el perfil CBOR del §58 y el DER estricto, también el de los tiempos; - la etapa 1, los errores normativos, los bytes, el perfil CBOR del §58 y el DER estricto, también el de los tiempos;
- la etapa 2, las primitivas y la lectura de `age`; - la etapa 2, las primitivas y la lectura de `age`;
@ -19,13 +19,15 @@ Están hechas las etapas 0 a 5 del plan (`docs/PLAN_dart.md` del espacio de trab
- la 5b, los compromisos de lo que firma un autor y sella un sello, `SECURITY_CBOR`, la firma de `alg` 1 y los veredictos con sus textos y sus líneas; - la 5b, los compromisos de lo que firma un autor y sella un sello, `SECURITY_CBOR`, la firma de `alg` 1 y los veredictos con sus textos y sus líneas;
- la 5a, el lector de las firmas CMS y de los sellos RFC 3161 con el perfil de certificado del borrador v0.12, ECDSA y RSA. Es interno; - la 5a, el lector de las firmas CMS y de los sellos RFC 3161 con el perfil de certificado del borrador v0.12, ECDSA y RSA. Es interno;
- la 5c, la firma de `alg` 2 y el sello de `seal_type` 2 en los veredictos, que une el lector de la 5a a la evaluación de la 5b; - la 5c, la firma de `alg` 2 y el sello de `seal_type` 2 en los veredictos, que une el lector de la 5a a la evaluación de la 5b;
- de la etapa 6, la parte 6a: la escritura de `age`, con la fuente inyectable de lo aleatorio, los recipients X25519, scrypt y tlock y las longitudes de un fichero antes de escribirlo. Es interna: la usa el escritor de la cápsula, la parte 6b, y la usarán las claves de autor, la etapa 7b. - de la etapa 6, la parte 6a: la escritura de `age`, con la fuente inyectable de lo aleatorio, los recipients X25519, scrypt y tlock y las longitudes de un fichero antes de escribirlo. Es interna: la usan el escritor de la cápsula, la parte 6b, y las claves de autor, la parte 7b.
- la parte 6b de la etapa 6, el escritor de cápsulas del formato 3: `time_only` y `time_and_key`, los 16 huecos con señuelos, la `.dkk`, la llave de palabras, la nota pública, el área de seguridad con la firma y el sello, el relleno y las autocomprobaciones del §62.1, con las fuentes leídas en streaming. - la parte 6b de la etapa 6, el escritor de cápsulas del formato 3: `time_only` y `time_and_key`, los 16 huecos con señuelos, la `.dkk`, la llave de palabras, la nota pública, el área de seguridad con la firma y el sello, el relleno y las autocomprobaciones del §62.1, con las fuentes leídas en streaming.
- la parte 7a de la etapa 7, el localizador y el sobre de `datekeys.capsule`: los datos de la extensión, la lectura y la apertura del localizador, las reglas de sus direcciones y de la IP a la que resuelve un nombre, y el resto del sobre. Sellar un localizador y crear un sobre necesitan el escritor de `age` de la etapa 6: son de la parte 7b. - la etapa 7, en dos partes:
- la 7a, el localizador y el sobre de `datekeys.capsule`: los datos de la extensión, la lectura y la apertura del localizador, las reglas de sus direcciones y de la IP a la que resuelve un nombre, y el resto del sobre;
- la 7b, las claves de autor `dkauthor1…`, con su firma Ed25519 y su fichero cifrado con scrypt, y el sellado del localizador y la creación del sobre, sobre el escritor de `age` de la 6a.
La librería ya abre cápsulas reales, de los tres formatos, con todas sus credenciales, y evalúa toda su área de seguridad como Go: la firma de `alg` 1, la de `alg` 2 con certificados, el sello de `seal_type` 2 y todos los veredictos de su forma. Y ya escribe cápsulas del formato 3, con todas sus credenciales, su firma y su sello, byte a byte como Go con los mismos valores aleatorios, y Go las abre. La librería ya abre cápsulas reales, de los tres formatos, con todas sus credenciales, y evalúa toda su área de seguridad como Go: la firma de `alg` 1, la de `alg` 2 con certificados, el sello de `seal_type` 2 y todos los veredictos de su forma. Y ya escribe cápsulas del formato 3, con todas sus credenciales, su firma y su sello, byte a byte como Go con los mismos valores aleatorios, y Go las abre. Firma con una clave de autor y sella localizadores.
Las etapas 2 y 3 se hicieron en paralelo, en ramas aparte desde la etapa 1, y se integraron el 5 de octubre de 2026. Las partes 4a y 4b también: la 4a, en la rama `stage4a`, se integró encima de la 4b el mismo día. La 4c se hizo después, en la rama `v0.11`, y la 5b también, en paralelo con la 5a, el lector de CMS, que se hizo en la rama `stage5a` y se integró encima de la 5b el mismo día. La 5c, que necesitaba las dos, se hizo después, en la rama `v0.11`. La 6a se hizo en la rama `v0.11`, en paralelo con la 7a, la lectura del localizador, en la rama `stage7a`. La 7a se integró encima de la 6a el mismo día. La 6b, el escritor de cápsulas, se hizo después en la rama `v0.11`, en paralelo con la 7b, las claves de autor, en la rama `stage7b`. Las etapas 2 y 3 se hicieron en paralelo, en ramas aparte desde la etapa 1, y se integraron el 5 de octubre de 2026. Las partes 4a y 4b también: la 4a, en la rama `stage4a`, se integró encima de la 4b el mismo día. La 4c se hizo después, en la rama `v0.11`, y la 5b también, en paralelo con la 5a, el lector de CMS, que se hizo en la rama `stage5a` y se integró encima de la 5b el mismo día. La 5c, que necesitaba las dos, se hizo después, en la rama `v0.11`. La 6a se hizo en la rama `v0.11`, en paralelo con la 7a, la lectura del localizador, en la rama `stage7a`. La 7a se integró encima de la 6a el mismo día. La 6b, el escritor de cápsulas, se hizo en la rama `v0.11`, en paralelo con la 7b, las claves de autor y el sellado del localizador, en la rama `stage7b`, desde `b23a0ee`. La 7b se integró encima de la 6b el mismo día.
La etapa 1 porta tres ficheros de `datekeys-go` en `601e6d2`, con las mismas lecturas, las mismas comprobaciones en el mismo orden y los mismos textos de error: La etapa 1 porta tres ficheros de `datekeys-go` en `601e6d2`, con las mismas lecturas, las mismas comprobaciones en el mismo orden y los mismos textos de error:
@ -221,7 +223,7 @@ La parte 6b de la etapa 6 porta el escritor de cápsulas del formato 3 de `datek
| Módulo | Contenido | En Go | | Módulo | Contenido | En Go |
|---|---|---| |---|---|---|
| `lib/src/encrypt3.dart` | `encryptFiles`, con `EncryptOptions`, `FileSource`, `EncryptResult` y `CapsuleWriteException`; y los enganches de la firma y del sello: `AuthorKey` (`alg` 1), `CmsSigner` (`alg` 2) y `Sealer` (`seal_type` 2) | `EncryptFiles`, `EncryptOptions`, `Source`, `Result`, `AuthorKey`, `CMSSigner` y `Sealer` de `capsule` | | `lib/src/encrypt3.dart` | `encryptFiles`, con `EncryptOptions`, `FileSource`, `EncryptResult` y `CapsuleWriteException`; y los enganches de la firma y del sello: `AuthorSigner` (`alg` 1), que es la `AuthorKey` de la 7b, `CmsSigner` (`alg` 2) y `Sealer` (`seal_type` 2) | `EncryptFiles`, `EncryptOptions`, `Source`, `Result`, `AuthorKey`, `CMSSigner` y `Sealer` de `capsule` |
Notas de la parte 6b: Notas de la parte 6b:
- **Lo que escribe:** - **Lo que escribe:**
@ -241,7 +243,7 @@ Notas de la parte 6b:
- la trama de `BODY` y el final del head frente a `CONTENT`; - la trama de `BODY` y el final del head frente a `CONTENT`;
- el área de seguridad se evalúa con el lector de esta librería, en el contexto de la cápsula, y debe dar los veredictos que se pidieron. - el área de seguridad se evalúa con el lector de esta librería, en el contexto de la cápsula, y debe dar los veredictos que se pidieron.
- **La firma y el sello:** - **La firma y el sello:**
- `AuthorKey` firma `AUTHOR_MESSAGE` con `alg` 1, y la firma se comprueba con el perfil estricto antes de escribir nada; - `authorKey`, un `AuthorSigner` como la `AuthorKey` de la 7b, firma `AUTHOR_MESSAGE` con `alg` 1, y la firma se comprueba con el perfil estricto antes de escribir nada;
- `CmsSigner` recibe `AUTHOR_MESSAGE` y devuelve la firma CMS, que debe estar completa, con un sello por firmante (F6); - `CmsSigner` recibe `AUTHOR_MESSAGE` y devuelve la firma CMS, que debe estar completa, con un sello por firmante (F6);
- `Sealer` sella `SEAL_SUBJECT` después de la firma; un sello S5 vale también, porque el reloj del escritor y el de la autoridad pueden diferir. - `Sealer` sella `SEAL_SUBJECT` después de la firma; un sello S5 vale también, porque el reloj del escritor y el de la autoridad pueden diferir.
@ -279,6 +281,26 @@ Notas de la parte 7a:
- **Los tipos de Go.** Un `Locator` tiene claves y resúmenes de 32 bytes, y ningún tamaño ni desplazamiento negativo, como los arrays y los `uint64` de Go: lo contrario es un `ArgumentError`. `CapsuleInfo.openLocator` usa el registro de perfiles por defecto, Quicknet, si no se le da uno, como la apertura de una cápsula; Go lo exige. - **Los tipos de Go.** Un `Locator` tiene claves y resúmenes de 32 bytes, y ningún tamaño ni desplazamiento negativo, como los arrays y los `uint64` de Go: lo contrario es un `ArgumentError`. `CapsuleInfo.openLocator` usa el registro de perfiles por defecto, Quicknet, si no se le da uno, como la apertura de una cápsula; Go lo exige.
- **Lo que se exporta.** `lib/datekeys.dart` exporta `locator.dart` entero, como el paquete público `locator` de Go. `ipaddr.dart` es interno. - **Lo que se exporta.** `lib/datekeys.dart` exporta `locator.dart` entero, como el paquete público `locator` de Go. `ipaddr.dart` es interno.
La parte 7b de la etapa 7 porta el paquete `authorkey` de `datekeys-go` en `c531e93`, el borrador v0.12 (§29.9, §29.12), y `Seal` y `NewEnvelope` del paquete `locator` (§44.1), con las mismas comprobaciones en el mismo orden y los mismos textos de error. Con los mismos valores aleatorios escribe los mismos bytes que Go:
| Módulo | Contenido | En Go |
|---|---|---|
| `lib/src/authorkey.dart` | `AuthorKey`, con `generate`, `fromSeed`, `publicKey`, `publicString`, `sign`, `clear`, `secret` y un `toString` que la oculta; `authorPublicString`, `parseAuthorPublic` y `parseAuthorSecret`, también sobre los bytes de un string de Go (`parseAuthorPublicUtf8`, `parseAuthorSecretUtf8`); `marshalAuthorKey`; `encryptAuthorKey`, con scrypt de logN 16; `readAuthorKey`; y `AuthorKeyException` | `authorkey` |
| `lib/src/ed25519_sign.dart` | La firma Ed25519 (RFC 8032, 5.1.5 y 5.1.6): `ed25519PublicKey` y `ed25519Sign`, el `crypto_sign` de TweetNaCl con el SHA-512 de `package:crypto`, y la reducción módulo ℓ de TweetNaCl, `modL` | `crypto/ed25519` |
| `lib/src/go_unicode.dart` | Los puntos de código que cambian `unicode.ToLower` y `unicode.ToUpper` de Go 1.26.8, Unicode 15.0.0, y los espacios de `unicode.IsSpace`, que escribe `tool/go_unicode_tables.go`; no se edita | `unicode` |
| `lib/src/locator_seal.dart` | `sealLocator`, `Seal` de Go, y `newEnvelope`, `NewEnvelope` de Go, sobre `TimeRecipient`, `generateX25519Identity` y `ageEncrypt` de la 6a y `splitEnvelope` de la 7a | `Seal` y `NewEnvelope` de `locator.go` |
Notas de la parte 7b:
- **La firma** es la de TweetNaCl en su versión de JavaScript: el cuerpo de `curve25519.dart`, dieciséis limbs de 16 bits en un `Float64List`, con el producto como un bucle, porque `curve25519.dart` guarda su aritmética como privada y la parte solo crea ficheros nuevos; y los escalares módulo ℓ en 64 limbs de 8 bits, con los acarreos de `modL` como divisiones por 256 redondeadas hacia abajo, que son los desplazamientos de TweetNaCl para sus valores, por debajo de 2^34. El escalar secreto y el nonce no pasan nunca por `BigInt` ni por una rama. Como Go, `ed25519Sign` mete en el hash la clave pública que recibe, sin derivarla otra vez de la semilla.
- **Las cadenas y las líneas de un fichero** se leen como las lee Go, como bytes: la mayúscula o la minúscula de una clave es la de `strings.ToUpper` y `strings.ToLower` de Go, y los espacios alrededor de una línea los de `strings.TrimSpace`, con las tablas de `go_unicode.dart`, ni las de la plataforma ni las de las rutas (Unicode 18.0.0); un byte que no es UTF-8 es U+FFFD, como en Go. Un string que no es ASCII no llega al Bech32 de la etapa 2, cuya comprobación de «mixed case» usa las mayúsculas de la plataforma: el error de Go se calcula aquí, sobre las runas. Las líneas son las de `bufio.Scanner`: una de 64 KiB o más da `bufio.Scanner: token too long`, sin el prefijo `authorkey:`, como en Go.
- **El fichero de una clave** se escribe con `ScryptRecipient` y `ageEncrypt` de la 6a, con un factor de trabajo de 16, y se lee con el lector de `age` de la etapa 2, con un máximo de 16, como `SetMaxWorkFactor(16)`: un fichero de 64 KiB como mucho, en claro o cifrado.
- **Los errores** son una `AuthorKeyException` con el texto de Go, sin código normativo. Una semilla o una clave pública de otra longitud son un `ArgumentError` con el texto de Go: un error del llamador. El de `PublicString` de Go tiene los dos números al revés, «a public key has 32 bytes, not 31», y aquí también.
- **Una diferencia con Go, a propósito:** tras `clear`, una clave lanza un `StateError` en cada uso; en Go, `Clear` deja a ceros la semilla y la clave pública, y la clave sigue dando sus valores y firmando con ellos, una firma que nunca verifica.
- **El material de la clave.** `clear` borra la semilla y la clave pública de la clave, y las funciones borran sus copias, también el texto de un fichero de clave y el del localizador sellado. Dart no puede prometer más: el recolector puede haber copiado un búfer, `package:crypto` guarda los suyos, y un `String`, como el de `AuthorKey.secret` o una contraseña, no se puede borrar. Nada es de tiempo constante: ni la VM ni un motor de JavaScript lo prometen.
- **El sellado** es el de Go, en su orden: `sealLocator` escribe primero el texto del localizador, con las comprobaciones de `Locator.marshal`, después crea el recipient tlock de la ronda, con las de `TimeRecipient`, y cifra; un fallo de cualquiera de los dos llega antes de sacar ningún valor aleatorio. `newEnvelope` saca primero I_SOBRE y después lo que saca `age`, como `GenerateX25519Identity` y `age.Encrypt` en Go. El cifrado tlock no es de tiempo constante (parte 6a).
- **Con la parte 6b:** `AuthorKey` implementa `AuthorSigner`, el enganche de la firma de `alg` 1 del escritor, que es la interfaz `AuthorKey` del paquete `capsule` de Go, con `Public()` y `Sign(msg)`. Las pruebas del escritor firman las cápsulas de `alg` 1 con la `AuthorKey` de la semilla de Go, y salen los mismos bytes.
- **Lo que se exporta.** `lib/datekeys.dart` exporta `authorkey.dart`, salvo las dos funciones sobre bytes, como el paquete público `authorkey` de Go, y `locator_seal.dart`. La firma Ed25519 y las tablas de Unicode de Go son internas.
Los enteros son exactos en la VM y en la web. El `int` de Dart tiene 64 bits con signo en la VM y en la web es un double, exacto hasta 2^53. Por eso la librería no usa un `int` por encima de 2^53-1, ni desplazamientos u operaciones de bits de más de 31 bits: Los enteros son exactos en la VM y en la web. El `int` de Dart tiene 64 bits con signo en la VM y en la web es un double, exacto hasta 2^53. Por eso la librería no usa un `int` por encima de 2^53-1, ni desplazamientos u operaciones de bits de más de 31 bits:
- un entero de CBOR es un `int` hasta 2^53-1 y un `BigInt` por encima, como el `number | bigint` de `datekeys-ts`; - un entero de CBOR es un `int` hasta 2^53-1 y un `BigInt` por encima, como el `number | bigint` de `datekeys-ts`;
- `CborDecoder.uint` devuelve un `int`, porque todos los esquemas acotan sus enteros en 2^53-1, y `uint64` devuelve un `BigInt`; - `CborDecoder.uint` devuelve un `int`, porque todos los esquemas acotan sus enteros en 2^53-1, y `uint64` devuelve un `BigInt`;
@ -291,19 +313,16 @@ Los enteros son exactos en la VM y en la web. El `int` de Dart tiene 64 bits con
- al escribir `age`, el contador de 11 bytes del nonce del STREAM se incrementa byte a byte, las longitudes son sumas y cocientes por debajo de 2^53, y `randomIndex` lee sus cuatro bytes como mucho multiplicando por 256, sin un desplazamiento que la web truncaría. - al escribir `age`, el contador de 11 bytes del nonce del STREAM se incrementa byte a byte, las longitudes son sumas y cocientes por debajo de 2^53, y `randomIndex` lee sus cuatro bytes como mucho multiplicando por 256, sin un desplazamiento que la web truncaría.
- en el escritor de cápsulas, el tamaño de cada fichero, su suma, L y P son un `int` hasta L_MAX, por debajo de 2^53, y el escritor rechaza unos ficheros que pasan de L_MAX antes de sumar el siguiente; - en el escritor de cápsulas, el tamaño de cada fichero, su suma, L y P son un `int` hasta L_MAX, por debajo de 2^53, y el escritor rechaza unos ficheros que pasan de L_MAX antes de sumar el siguiente;
- en el localizador, una IPv6 son 16 bytes y sus bloques se comparan byte a byte, nunca como un entero de 64 o 128 bits, y los varint de un CID, de hasta 63 bits, son `BigInt`; el tamaño del resto y los desplazamientos son un `int` hasta 2^53-1. - en el localizador, una IPv6 son 16 bytes y sus bloques se comparan byte a byte, nunca como un entero de 64 o 128 bits, y los varint de un CID, de hasta 63 bits, son `BigInt`; el tamaño del resto y los desplazamientos son un `int` hasta 2^53-1.
- en el localizador, una IPv6 son 16 bytes y sus bloques se comparan byte a byte, nunca como un entero de 64 o 128 bits, y los varint de un CID, de hasta 63 bits, son `BigInt`; el tamaño del resto y los desplazamientos son un `int` hasta 2^53-1;
- en la firma Ed25519, los escalares módulo ℓ son 64 limbs de 8 bits en un `Float64List`, sumas de productos de bytes por debajo de 2^21 que `modL` reduce con valores por debajo de 2^34, y sus acarreos son divisiones redondeadas hacia abajo, no desplazamientos.
**`BigInt` no es de tiempo constante.** En las primitivas se usa solo con datos públicos: la reducción de escalares módulo ℓ y las comprobaciones de `onCurve` en Ed25519, que solo verifica, y el factor de trabajo de un stanza scrypt. Los secretos de las primitivas (el escalar de X25519, las claves de HMAC, ChaCha20 y Poly1305) van en la aritmética de limbs, sin ramas ni índices que dependan de ellos; ni la VM ni un motor de JavaScript prometen tiempo constante, aun así. **`BigInt` no es de tiempo constante.** En las primitivas se usa solo con datos públicos: la reducción de escalares módulo ℓ y las comprobaciones de `onCurve` en Ed25519, que solo verifica, y el factor de trabajo de un stanza scrypt. Los secretos de las primitivas (el escalar de X25519, las claves de HMAC, ChaCha20 y Poly1305) van en la aritmética de limbs, sin ramas ni índices que dependan de ellos; ni la VM ni un motor de JavaScript prometen tiempo constante, aun así. La firma Ed25519 de la parte 7b sigue la misma regla: el escalar secreto y el nonce van en limbs, también al reducirlos módulo ℓ, nunca en `BigInt`.
ECDSA y RSA hacen con `BigInt` toda su aritmética, y solo verifican: la clave de un certificado, la firma y el mensaje son públicos. La librería no firma con ninguno de los dos. ECDSA y RSA hacen con `BigInt` toda su aritmética, y solo verifican: la clave de un certificado, la firma y el mensaje son públicos. La librería no firma con ninguno de los dos.
BLS12-381, en cambio, hace con `BigInt` toda su aritmética. Verificar un release y descifrar un stanza tlock solo manejan datos públicos: la firma de la ronda lo es desde que drand la publica, y el stanza va en la cápsula. Cifrar no: sigma y r son secretos, y el tiempo del código depende de ellos. El autor aceptó el 6 de octubre de 2026 que el escritor cifre así el stanza tlock (parte 6a); una capa del cuerpo de limbs fijos, sin ramas que dependan de los datos, lo evitaría. BLS12-381, en cambio, hace con `BigInt` toda su aritmética. Verificar un release y descifrar un stanza tlock solo manejan datos públicos: la firma de la ronda lo es desde que drand la publica, y el stanza va en la cápsula. Cifrar no: sigma y r son secretos, y el tiempo del código depende de ellos. El autor aceptó el 6 de octubre de 2026 que el escritor cifre así el stanza tlock (parte 6a); una capa del cuerpo de limbs fijos, sin ramas que dependan de los datos, lo evitaría.
Las etapas siguientes traen el resto del protocolo en este orden: Con la 6b y la 7b están hechas todas las etapas del plan, también la firma de `alg` 1 en el escritor con la clave de autor de la 7b.
| Etapa | Contenido |
|---|---|
| 6 | Escritor: la parte 6b, la cápsula de formato 3, `time_only` y `time_and_key`, con la llave de palabras y la nota pública |
| 7 | El sellado del localizador y el sobre nuevo (parte 7b), y las claves de autor |
## Rendimiento ## Rendimiento
@ -462,6 +481,33 @@ Lo aleatorio sale del CSPRNG de la plataforma, y el sink solo cuenta lo que reci
Una cápsula pequeña cuesta sobre todo el cifrado del stanza tlock, el mismo de la 6a; con `time_and_key`, también los 16 stanzas X25519 y la `.dkk`, que se comprueba abriendo `INNER_ACCESS_AGE`. En un fichero grande manda el SHA-256, que se calcula tres veces, como en Go: el de cada lectura del fichero y el de la cápsula para la `.dkk`, a unos 62 MiB/s cada uno en la VM. Con una llave de palabras se suman los 600 000 pasos de PBKDF2. Una cápsula pequeña cuesta sobre todo el cifrado del stanza tlock, el mismo de la 6a; con `time_and_key`, también los 16 stanzas X25519 y la `.dkk`, que se comprueba abriendo `INNER_ACCESS_AGE`. En un fichero grande manda el SHA-256, que se calcula tres veces, como en Go: el de cada lectura del fichero y el de la cápsula para la `.dkk`, a unos 62 MiB/s cada uno en la VM. Con una llave de palabras se suman los 600 000 pasos de PBKDF2.
### Las claves de autor y el sellado del localizador
`dart run tool/authorkey_bench.dart` mide la parte 7b en la VM. Compilado a JavaScript:
```bash
dart compile js -O2 -o authorkey_bench.js tool/authorkey_bench.dart
```
```bash
node -e "globalThis.self = globalThis; require('./authorkey_bench.js')"
```
Lo aleatorio sale del CSPRNG de la plataforma, como en producción. Son medianas de entre cinco y treinta y una ejecuciones, el 6 de octubre de 2026:
| Operación | VM | Node.js |
|---|---|---|
| `AuthorKey.generate` | 4,5 ms | 9 ms |
| Una firma Ed25519 de 99 bytes, lo que mide `AUTHOR_MESSAGE` | 4,5 ms | 9 ms |
| La verificación estricta de esa firma, para comparar | 4,6 ms | 9 ms |
| Una firma Ed25519 de 1 MiB | 26 ms | 0,92 s |
| `encryptAuthorKey`, scrypt con logN 16 | 0,57 s | 0,77 s |
| `readAuthorKey` de ese fichero | 0,57 s | 1,0 s |
| `sealLocator` para la ronda 1000 | 30 ms | 0,52 s |
| `newEnvelope` de un `.dkc` de 1 MiB | 56 ms | 42 ms |
Una firma es una multiplicación escalar del punto base y dos SHA-512; la verificación, dos multiplicaciones con el producto desenrollado, tarda lo mismo. En un mensaje grande manda el SHA-512 de `package:crypto`, lento compilado a JavaScript, donde trabaja con enteros de 64 bits emulados; la firma de `alg` 1 firma siempre los 99 bytes de `AUTHOR_MESSAGE`. El fichero de una clave es casi todo scrypt, y sellar un localizador, el cifrado tlock.
### ECDSA, RSA y CMS ### ECDSA, RSA y CMS
`dart run tool/cms_bench.dart` mide ECDSA, RSA y el lector de CMS en la VM. Compilado a JavaScript: `dart run tool/cms_bench.dart` mide ECDSA, RSA y el lector de CMS en la VM. Compilado a JavaScript:
@ -501,7 +547,7 @@ La rama sigue la versión de la especificación: `v0.11`, hasta que `datekeys-go
## Dependencias ## Dependencias
- **En ejecución:** solo `package:crypto`, para SHA-1, SHA-2 y HMAC. Todo lo demás es código propio: HKDF, PBKDF2, scrypt, ChaCha20-Poly1305, X25519, Ed25519, BLS12-381, ECDSA, RSA, DER, CMS y CBOR. Hoy la librería usa de `package:crypto` SHA-512, en Ed25519, SHA-256, en BLS12-381 y tlock, y SHA-1, SHA-256, SHA-384 y SHA-512, en CMS. Las primitivas tienen además su propio SHA-256 con HMAC-SHA256, porque PBKDF2 los necesita con los estados de la clave calculados una vez. - **En ejecución:** solo `package:crypto`, para SHA-1, SHA-2 y HMAC. Todo lo demás es código propio: HKDF, PBKDF2, scrypt, ChaCha20-Poly1305, X25519, Ed25519, BLS12-381, ECDSA, RSA, DER, CMS y CBOR. Hoy la librería usa de `package:crypto` SHA-512, en Ed25519, al verificar y al firmar, SHA-256, en BLS12-381 y tlock, y SHA-1, SHA-256, SHA-384 y SHA-512, en CMS. Las primitivas tienen además su propio SHA-256 con HMAC-SHA256, porque PBKDF2 los necesita con los estados de la clave calculados una vez.
- **En desarrollo:** solo `package:test`, aprobado el 5 de octubre de 2026. No hay paquete de lints: las reglas del análisis están en `analysis_options.yaml`. - **En desarrollo:** solo `package:test`, aprobado el 5 de octubre de 2026. No hay paquete de lints: las reglas del análisis están en `analysis_options.yaml`.
- **`pubspec.lock`** va en el repositorio, para que las pruebas usen siempre las mismas versiones. - **`pubspec.lock`** va en el repositorio, para que las pruebas usen siempre las mismas versiones.
@ -540,7 +586,7 @@ dart run tool/sync_testdata.dart check --against ../datekeys-go
- Los ficheros de `testdata/` no se editan ni se generan aquí. - Los ficheros de `testdata/` no se editan ni se generan aquí.
- En cada `dart test`, `test/testdata_test.dart` comprueba la copia, y que cada fichero nombre `specVersion`. - En cada `dart test`, `test/testdata_test.dart` comprueba la copia, y que cada fichero nombre `specVersion`.
`test/vectors/` tiene los vectores de las primitivas, de la lectura y la escritura de `age`, de BLS12-381, de tlock, de las reglas de rutas y textos, de la llave de palabras, de los formatos, de la apertura, del área de seguridad, de la firma con certificados y el sello, del lector de CMS y del localizador. Los escribe Go, con las librerías de la caché de módulos que usa `datekeys-go` (`x/crypto`, `filippo.io/age`, `kilic/bls12-381`, `drand/kyber`, `kyber-bls12381` y `tlock`) y sus paquetes, como `provider`, `agewrap`, `capsule`, `internal/pathrule`, `internal/testkit` y `wordkey`; ningún valor esperado se escribe a mano. Los generadores van en `tool/`, con `//go:build ignore`, y se ejecutan en el contexto del módulo de `datekeys-go`, sin cambiar nada en él; los de las rutas, de la apertura, del lector de CMS y del escritor de `age`, en una exportación suya, y el último, `tool/cms_go_vectors_test.go`, como una prueba de Go. Los de BLS12-381 y tlock dan la misma salida con el tag `spec-v0.11` y con el borrador v0.12, y leen ficheros congelados de `datekeys-ts`, cuya carpeta en esta máquina se llama todavía `App`: `test/vectors/` tiene los vectores de las primitivas, de la lectura y la escritura de `age`, de BLS12-381, de tlock, de las reglas de rutas y textos, de la llave de palabras, de los formatos, de la apertura, del área de seguridad, de la firma con certificados y el sello, del lector de CMS, del localizador, de las claves de autor y de la firma Ed25519, y del sellado del localizador y del sobre. Los escribe Go, con las librerías de la caché de módulos que usa `datekeys-go` (`x/crypto`, `filippo.io/age`, `kilic/bls12-381`, `drand/kyber`, `kyber-bls12381` y `tlock`) y sus paquetes, como `provider`, `agewrap`, `capsule`, `internal/pathrule`, `internal/testkit` y `wordkey`; ningún valor esperado se escribe a mano. Los generadores van en `tool/`, con `//go:build ignore`, y se ejecutan en el contexto del módulo de `datekeys-go`, sin cambiar nada en él; los de las rutas, de la apertura, del lector de CMS y del escritor de `age`, en una exportación suya, y el último, `tool/cms_go_vectors_test.go`, como una prueba de Go. Los de BLS12-381 y tlock dan la misma salida con el tag `spec-v0.11` y con el borrador v0.12, y leen ficheros congelados de `datekeys-ts`, cuya carpeta en esta máquina se llama todavía `App`:
```bash ```bash
cd ../datekeys-go && go run ../datekeys-dart/tool/gen_primitive_vectors.go -out ../datekeys-dart/test/vectors cd ../datekeys-go && go run ../datekeys-dart/tool/gen_primitive_vectors.go -out ../datekeys-dart/test/vectors
@ -638,6 +684,22 @@ cp tool/age_writer_go_vectors.go "$tmp"
rm -rf "$tmp" rm -rf "$tmp"
``` ```
Los de la parte 7b corren en el módulo de `datekeys-go`, porque solo importan paquetes públicos: `authorkey`, `locator`, `profile`, `provider` y `codec/bech32`, cuyo `createChecksum` el primero alcanza con `go:linkname` para escribir cadenas Bech32 con cualquier relleno. Mientras escriben cada caso, `crypto/rand.Reader` lee el keystream de `SeededRandomSource`, como en el generador del escritor de `age`; `authorkey.Generate` lo lee también, gracias al `//go:debug cryptocustomrand=1` del generador, sin el que `ed25519.GenerateKey` de Go 1.26 no mira `crypto/rand.Reader`:
```bash
cd ../datekeys-go && go run ../datekeys-dart/tool/authorkey_go_vectors.go -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata -out ../datekeys-dart/test/vectors
```
```bash
cd ../datekeys-go && go run ../datekeys-dart/tool/locator_seal_go_vectors.go -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata -out ../datekeys-dart/test/vectors
```
Las tablas de Unicode de Go de `lib/src/go_unicode.dart` las escribe otro programa, con la versión de Go de `datekeys-go`:
```bash
cd ../datekeys-go && go run ../datekeys-dart/tool/go_unicode_tables.go -out ../datekeys-dart/lib/src/go_unicode.dart
```
En la otra dirección, este repositorio escribe los ficheros y Go los abre: `tool/age_interop_dart_samples.dart` escribe los 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 sus veredictos: En la otra dirección, este repositorio escribe los ficheros y Go los abre: `tool/age_interop_dart_samples.dart` escribe los 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 sus veredictos:
```bash ```bash
@ -651,6 +713,15 @@ cp tool/age_interop_go_verdicts.go "$tmp"
rm -rf "$tmp" rm -rf "$tmp"
``` ```
Los de la parte 7b, igual: `tool/seal_interop_dart_samples.dart` escribe los ficheros de las recetas de `test/seal_interop_support.dart`, y `tool/seal_interop_go_verdicts.go` los abre en el módulo de `datekeys-go`:
```bash
tmp=$(mktemp -d)
dart run tool/seal_interop_dart_samples.dart "$tmp"
(cd ../datekeys-go && go run ../datekeys-dart/tool/seal_interop_go_verdicts.go -source $(git rev-parse v0.12) -testdata ../datekeys-dart/testdata -samples "$tmp" -out ../datekeys-dart/test/vectors)
rm -rf "$tmp"
```
Los del escritor de cápsulas corren como una prueba de Go en una exportación, como los del lector de CMS, porque los enganches de la firma y del sello usan `internal/cms/cmstest`, cuyas claves y firmas solo fija `testing/cryptotest.SetGlobalRandom`. Mientras escribe cada receta, `crypto/rand.Reader` lee el keystream de `SeededRandomSource`, así que `capsule.EncryptFiles` saca de él todos sus valores, y la salida es la misma en cada ejecución: Los del escritor de cápsulas corren como una prueba de Go en una exportación, como los del lector de CMS, porque los enganches de la firma y del sello usan `internal/cms/cmstest`, cuyas claves y firmas solo fija `testing/cryptotest.SetGlobalRandom`. Mientras escribe cada receta, `crypto/rand.Reader` lee el keystream de `SeededRandomSource`, así que `capsule.EncryptFiles` saca de él todos sus valores, y la salida es la misma en cada ejecución:
```bash ```bash
@ -723,9 +794,15 @@ rm -rf "$tmp"
| `capsule_writer.g.dart` | Los casos de `capsule_writer.json` marcados `node`, como constante de Dart | | `capsule_writer.g.dart` | Los casos de `capsule_writer.json` marcados `node`, como constante de Dart |
| `capsule_interop.json` | Las cápsulas que escribe este repositorio con las recetas de `test/capsule_interop_support.dart`, seis con `SeededRandomSource` y seis con el CSPRNG de la plataforma: su longitud y su SHA-256, `capsule.Inspect`, `capsule.Open` con cada credencial, todas juntas y ninguna, si sus capas y su `.dkk` se codifican otra vez igual, y si `capsule.EncryptFiles` escribe los mismos bytes con la misma semilla | | `capsule_interop.json` | Las cápsulas que escribe este repositorio con las recetas de `test/capsule_interop_support.dart`, seis con `SeededRandomSource` y seis con el CSPRNG de la plataforma: su longitud y su SHA-256, `capsule.Inspect`, `capsule.Open` con cada credencial, todas juntas y ninguna, si sus capas y su `.dkk` se codifican otra vez igual, y si `capsule.EncryptFiles` escribe los mismos bytes con la misma semilla |
| `capsule_interop.g.dart` | Las muestras de `capsule_interop.json` que no están marcadas `node` a falso, como constante de Dart | | `capsule_interop.g.dart` | Las muestras de `capsule_interop.json` que no están marcadas `node` a falso, como constante de Dart |
| `authorkey.json` | Las firmas de `crypto/ed25519`: líneas de `sign.input`, cuyas 0, 1, 2 y 1023 son los tests 1, 2, 3 y 1024 de la RFC 8032, TEST SHA(abc), semillas de una semilla con mensajes de 0 bytes a 1 MiB, y claves privadas cuya segunda mitad es otra clave pública; los escalares de `math/big`, x mod ℓ y (a·b + c) mod ℓ, en sus esquinas y al azar; y `authorkey`: `NewFromSeed`, `Public`, `PublicString`, `Secret`, `Marshal`, `String` y sus errores, `Generate` y `Encrypt` con cada valor que sacan, `ParsePublic` y `ParseSecret` sobre cadenas válidas, en otra caja, de otras longitudes, con cada error de Bech32, otros prefijos y rellenos, claves fuera del perfil estricto y bytes que no son UTF-8, y sobre las runas de cada borde de los conjuntos de Go; y `Read` de ficheros en claro y cifrados, con sus límites, cada espacio de Go y sus vecinos, y factores de trabajo de 1 a 31. Cada caso con el resultado o el texto de Go |
| `authorkey.g.dart` | `authorkey.json` con solo las firmas marcadas para Node.js y una de cada ocho runas, como constante de Dart |
| `locator_seal.json` | `Seal` de localizadores de uno a tres bloques para rondas de 1 a la última de Quicknet, que Go abre con el release de su ronda cuando lo tiene; sus rechazos; `NewEnvelope` de `.dkc` de 0 bytes a 1 MiB; y el camino entero, del sobre a la apertura del `.dkc`. Con lo que saca `crypto/rand` en cada caso |
| `locator_seal.g.dart` | El mismo JSON como constante de Dart |
| `seal_interop.json` | Lo que hace Go con lo que escribe este repositorio en la parte 7b, con las recetas de `test/seal_interop_support.dart`: localizadores sellados de cuatro rondas con sus sobres, que abre con `locator.Open` y `OpenEnvelope`; ficheros de clave, en claro y cifrados, que lee con `authorkey.Read`; y firmas de 0 bytes a 70 000, que comprueba con `crypto/ed25519`. Con el SHA-256 de cada fichero y el veredicto de Go |
| `seal_interop.g.dart` | El mismo JSON como constante de Dart |
- Los fixtures que leen los generadores son los de `testdata/` de este repositorio, la copia sincronizada. - Los fixtures que leen los generadores son los de `testdata/` de este repositorio, la copia sincronizada.
- `primitives.json`, los cuatro ficheros de BLS12-381 y tlock, `pathrule_vectors.json`, `wordkey_vectors.json`, los de los formatos, `security_vectors.json`, `securitycms_vectors.json`, los del lector de CMS, los del escritor de `age`, los del localizador y `capsule_writer.json` salen iguales en cada ejecución, los del lector de CMS, los del localizador y `capsule_writer.json` con Go 1.26.8. `capsule_interop.json` cambia en cada ejecución en sus seis muestras del CSPRNG, que es lo que comprueban. Lo aleatorio de tlock, los ciphertexts de kyber con su sigma y los de `datekeys-ts`, se lee de los ficheros congelados de `datekeys-ts` en `289fe71`, y Go los descifra otra vez. `age`, en cambio, saca sus claves y nonces de `crypto/rand`, así que `age.json` y `age_fixtures.json` cambian en cada ejecución; las pruebas leen lo que esté en el repositorio. `age.json` no lee `testdata/` y es el congelado de la etapa 2; `age_fixtures.json` se escribió otra vez con el `testdata/` de la v0.12, y fuera de los dos fixtures nuevos y del de `seal_type` 4294967295 sale igual. - `primitives.json`, los cuatro ficheros de BLS12-381 y tlock, `pathrule_vectors.json`, `wordkey_vectors.json`, los de los formatos, `security_vectors.json`, `securitycms_vectors.json`, los del lector de CMS, los del escritor de `age`, los del localizador, los de la parte 7b y `capsule_writer.json` salen iguales en cada ejecución, los del lector de CMS, los del localizador y `capsule_writer.json` con Go 1.26.8. `capsule_interop.json` cambia en cada ejecución en sus seis muestras del CSPRNG, que es lo que comprueban. Lo aleatorio de tlock, los ciphertexts de kyber con su sigma y los de `datekeys-ts`, se lee de los ficheros congelados de `datekeys-ts` en `289fe71`, y Go los descifra otra vez. `age`, en cambio, saca sus claves y nonces de `crypto/rand`, así que `age.json` y `age_fixtures.json` cambian en cada ejecución; las pruebas leen lo que esté en el repositorio. `age.json` no lee `testdata/` y es el congelado de la etapa 2; `age_fixtures.json` se escribió otra vez con el `testdata/` de la v0.12, y fuera de los dos fixtures nuevos y del de `seal_type` 4294967295 sale igual.
- Un fichero `age` de más de un chunk se guarda como su cabecera, su nonce y su file key: la prueba cifra otra vez el texto documentado y comprueba el SHA-256 del fichero entero antes de leerlo. Las cabeceras de megabytes se escriben como partes que se repiten. - Un fichero `age` de más de un chunk se guarda como su cabecera, su nonce y su file key: la prueba cifra otra vez el texto documentado y comprueba el SHA-256 del fichero entero antes de leerlo. Las cabeceras de megabytes se escriben como partes que se repiten.
- `locator_vectors.json` guarda un valor editado como ediciones de una base, con una forma más: `[at, 0, byte, n]` inserta n copias del byte. Una dirección con una racha de 32 bytes iguales o más va como `[antes, byte, n, después]`, y un texto de error, como su índice en `texts`. Los ficheros cuyo texto pasa de 1 MiB van como su cabecera, su nonce, su file key y la lista de sus chunks: la prueba los escribe otra vez y comprueba su SHA-256 antes de abrirlos. - `locator_vectors.json` guarda un valor editado como ediciones de una base, con una forma más: `[at, 0, byte, n]` inserta n copias del byte. Una dirección con una racha de 32 bytes iguales o más va como `[antes, byte, n, después]`, y un texto de error, como su índice en `texts`. Los ficheros cuyo texto pasa de 1 MiB van como su cabecera, su nonce, su file key y la lista de sus chunks: la prueba los escribe otra vez y comprueba su SHA-256 antes de abrirlos.
- Los casos de `open_cases.json` que editan un fixture lo sellan otra vez como el `testkit` de la referencia, con las file keys y los nonces del fixture, así que salen iguales en cada ejecución, y se guardan como ediciones del fixture. - Los casos de `open_cases.json` que editan un fixture lo sellan otra vez como el `testkit` de la referencia, con las file keys y los nonces del fixture, así que salen iguales en cada ejecución, y se guardan como ediciones del fixture.

@ -28,11 +28,13 @@
/// Go, and what it points to, the locator sealed for the date and the /// Go, and what it points to, the locator sealed for the date and the
/// envelope of the .dkc: reading and opening a locator, the rules of its /// envelope of the .dkc: reading and opening a locator, the rules of its
/// addresses and of the IP that a name resolves to, and the rest of the /// addresses and of the IP that a name resolves to, and the rest of the
/// envelope. Sealing a locator and making an envelope come with part 7b. /// envelope. And part 7b: sealing a locator and making an envelope, and the
/// author keys of alg 1, dkauthor1… and DKAUTHOR-SECRET-KEY-1…, with their
/// Ed25519 signature and their key file encrypted with scrypt.
/// ///
/// And part 6b of stage 6: the writer of capsules of format 3, /// And part 6b of stage 6: the writer of capsules of format 3,
/// [encryptFiles], with its options, its sources read in streaming, its /// [encryptFiles], with its options, its sources read in streaming, its
/// result and the hooks of the signature and the seal, [AuthorKey], /// result and the hooks of the signature and the seal, [AuthorSigner],
/// [CmsSigner] and [Sealer]; and what its options take: the X25519 /// [CmsSigner] and [Sealer]; and what its options take: the X25519
/// recipients and the source of the random values. /// recipients and the source of the random values.
/// ///
@ -40,12 +42,14 @@
/// and the random sources, agewrap, the curve arithmetic, the IBE of tlock /// and the random sources, agewrap, the curve arithmetic, the IBE of tlock
/// and its stanza, the rules of paths and texts on the Unicode tables, the /// and its stanza, the rules of paths and texts on the Unicode tables, the
/// frame of BODY, the digest of a capsule, the steps of the opening, the /// frame of BODY, the digest of a capsule, the steps of the opening, the
/// reader of CMS with its ECDSA and RSA, and the parser of IP addresses are /// reader of CMS with its ECDSA and RSA, the parser of IP addresses, the
/// internal, as in the Go reference and datekeys-ts. /// Ed25519 signing and the Unicode sets of Go are internal, as in the Go
/// reference and datekeys-ts.
library; library;
export 'src/accesskey.dart'; export 'src/accesskey.dart';
export 'src/author.dart'; export 'src/author.dart';
export 'src/authorkey.dart' hide parseAuthorPublicUtf8, parseAuthorSecretUtf8;
export 'src/bls12381_curve.dart' export 'src/bls12381_curve.dart'
show BlsGroup, PointVerdict, checkCompressedPoint; show BlsGroup, PointVerdict, checkCompressedPoint;
export 'src/bytes.dart' export 'src/bytes.dart'
@ -71,6 +75,7 @@ export 'src/inspect.dart'
inspectedLength, inspectedLength,
maxAccessKeyRead; maxAccessKeyRead;
export 'src/locator.dart'; export 'src/locator.dart';
export 'src/locator_seal.dart';
export 'src/note.dart'; export 'src/note.dart';
export 'src/open.dart' show OpenOptions, Opened, openCapsule, openCapsuleSource; export 'src/open.dart' show OpenOptions, Opened, openCapsule, openCapsuleSource;
export 'src/padding.dart'; export 'src/padding.dart';

@ -35,6 +35,7 @@ import 'bech32.dart';
import 'bytes.dart'; import 'bytes.dart';
import 'curve25519.dart' show canonical, onCurve, smallOrder; import 'curve25519.dart' show canonical, onCurve, smallOrder;
import 'ed25519_sign.dart'; import 'ed25519_sign.dart';
import 'encrypt3.dart' show AuthorSigner;
import 'go_unicode.dart'; import 'go_unicode.dart';
import 'random.dart'; import 'random.dart';
import 'recipient.dart'; import 'recipient.dart';
@ -75,12 +76,12 @@ final class AuthorKeyException implements Exception {
String toString() => message; String toString() => message;
} }
/// A secret key of an author: Go's authorkey.Key. Its [publicKey] and /// A secret key of an author: Go's authorkey.Key. It is the [AuthorSigner]
/// [sign] are what the writer of a capsule needs to sign with `alg` 1. /// that the writer of a capsule needs to sign with `alg` 1.
/// ///
/// Unlike Go, a cleared key refuses to be used: Go gives the values of a /// Unlike Go, a cleared key refuses to be used: Go gives the values of a
/// key of zeros after Clear, and a signature that never verifies. /// key of zeros after Clear, and a signature that never verifies.
final class AuthorKey { final class AuthorKey implements AuthorSigner {
AuthorKey._(this._seed, this._public); AuthorKey._(this._seed, this._public);
/// A new key from [random], the CSPRNG of the platform by default, as /// A new key from [random], the CSPRNG of the platform by default, as
@ -119,6 +120,7 @@ final class AuthorKey {
} }
/// The public key A, 32 bytes, as Public of Go. /// The public key A, 32 bytes, as Public of Go.
@override
Uint8List get publicKey { Uint8List get publicKey {
_check(); _check();
return Uint8List.fromList(_public); return Uint8List.fromList(_public);
@ -128,6 +130,7 @@ final class AuthorKey {
String get publicString => authorPublicString(publicKey); String get publicString => authorPublicString(publicKey);
/// The Ed25519 signature of [message], 64 bytes, as Sign of Go. /// The Ed25519 signature of [message], 64 bytes, as Sign of Go.
@override
Uint8List sign(List<int> message) { Uint8List sign(List<int> message) {
_check(); _check();
return ed25519Sign(_seed, _public, message); return ed25519Sign(_seed, _public, message);

@ -39,6 +39,7 @@ import 'age.dart';
import 'age_writer.dart'; import 'age_writer.dart';
import 'agewrap.dart'; import 'agewrap.dart';
import 'author.dart'; import 'author.dart';
import 'authorkey.dart' show AuthorKey;
import 'body.dart'; import 'body.dart';
import 'bytes.dart'; import 'bytes.dart';
import 'control.dart'; import 'control.dart';
@ -66,10 +67,11 @@ import 'wordkey.dart';
// The hooks // The hooks
/// The author key that signs a capsule with alg 1, a strict Ed25519 /// The author key that signs a capsule with alg 1, a strict Ed25519
/// signature (spec §29.9), as AuthorKey of Go: [encryptFiles] signs /// signature (spec §29.9), as the interface AuthorKey of package capsule of
/// AUTHOR_MESSAGE with it and checks the signature with the strict profile /// Go: [encryptFiles] signs AUTHOR_MESSAGE with it and checks the signature
/// before it writes anything. /// with the strict profile before it writes anything. [AuthorKey], the key
abstract interface class AuthorKey { /// of package authorkey, is one.
abstract interface class AuthorSigner {
/// The public key A, 32 bytes. /// The public key A, 32 bytes.
Uint8List get publicKey; Uint8List get publicKey;
@ -235,7 +237,7 @@ final class EncryptOptions {
/// The key that signs the capsule with alg 1 (spec §29.9); null for no /// The key that signs the capsule with alg 1 (spec §29.9); null for no
/// signature. Exclusive with [cmsSigner]. /// signature. Exclusive with [cmsSigner].
final AuthorKey? authorKey; final AuthorSigner? authorKey;
/// The signer of alg 2 (spec §29.10); null for none. Exclusive with /// The signer of alg 2 (spec §29.10); null for none. Exclusive with
/// [authorKey] and [sealer]: with it the seal goes inside each signature. /// [authorKey] and [sealer]: with it the seal goes inside each signature.

@ -21,9 +21,10 @@
/// bytes of the rest, and gives them to [Locator.openEnvelope], which checks /// bytes of the rest, and gives them to [Locator.openEnvelope], which checks
/// their SHA-256, decrypts the .dkc and checks its SHA-256 (spec §44.1). /// their SHA-256, decrypts the .dkc and checks its SHA-256 (spec §44.1).
/// ///
/// Sealing a locator, Seal of Go, and making an envelope, the age encryption /// Sealing a locator, Seal of Go, and making an envelope, NewEnvelope, are
/// of NewEnvelope, need the writer of age: [splitEnvelope] is the rest of /// in locator_seal.dart, over the writer of age: [splitEnvelope] is the
/// NewEnvelope, the split of the age file of the envelope. /// part of NewEnvelope after the encryption, the split of the age file of
/// the envelope.
/// ///
/// The errors of the locator carry no normative code, as in Go: a locator /// The errors of the locator carry no normative code, as in Go: a locator
/// that does not read or does not open, or an address that breaks the rules /// that does not read or does not open, or an address that breaks the rules
@ -1189,10 +1190,10 @@ Uint8List _readLimited(Uint8List file, AgeOpened opened) {
/// and the length of the rest and the SHA-256 of the .dkc, without /// and the length of the rest and the SHA-256 of the .dkc, without
/// addresses; and the rest, the nonce and the STREAM, with no mark, which is /// addresses; and the rest, the nonce and the STREAM, with no mark, which is
/// what the person keeps outside. It is the part of NewEnvelope of Go after /// what the person keeps outside. It is the part of NewEnvelope of Go after
/// the encryption, which needs the writer of age; [ageFile] is not /// the encryption, which newEnvelope of locator_seal.dart makes first;
/// decrypted. A caller adds the addresses where it stored the rest /// [ageFile] is not decrypted. A caller adds the addresses where it stored
/// ([Locator.withAddresses]), alone or inside another file ([hideRest]), /// the rest ([Locator.withAddresses]), alone or inside another file
/// and then seals the locator. /// ([hideRest]), and then seals the locator (sealLocator).
/// ///
/// The header ends at the line feed after the first line that starts with /// The header ends at the line feed after the first line that starts with
/// `--- `: no line of the header before it starts so, and the lines of the /// `--- `: no line of the header before it starts so, and the lines of the

@ -166,15 +166,45 @@ final class Hooks {
final Json recorded; final Json recorded;
final Json asked = {}; final Json asked = {};
AuthorKey? get authorKey => AuthorSigner? get authorKey => switch (recipe['author_key']) {
recipe['author_key'] == null ? null : _ReplayAuthor(this); null => null,
// A key that Go broke on purpose: what Go's gave.
{'bad': _} => _ReplayAuthor(this),
// The key of the seed, signing as Go's did: Ed25519 is deterministic.
final k => _RecordingAuthor(
this,
AuthorKey.fromSeed(hexOf((k as Json)['seed'])),
),
};
CmsSigner? get cmsSigner => recipe['cms'] == null ? null : _ReplayCms(this); CmsSigner? get cmsSigner => recipe['cms'] == null ? null : _ReplayCms(this);
Sealer? get sealer => recipe['sealer'] == null ? null : _ReplaySealer(this); Sealer? get sealer => recipe['sealer'] == null ? null : _ReplaySealer(this);
} }
final class _ReplayAuthor implements AuthorKey { final class _RecordingAuthor implements AuthorSigner {
_RecordingAuthor(this.h, this.key);
final Hooks h;
final AuthorKey key;
@override
Uint8List get publicKey {
final pub = key.publicKey;
h.asked['author_public'] = toHex(pub);
return pub;
}
@override
Uint8List sign(Uint8List message) {
h.asked['author_message'] = toHex(message);
final sig = key.sign(message);
h.asked['author_signature'] = toHex(sig);
return sig;
}
}
final class _ReplayAuthor implements AuthorSigner {
_ReplayAuthor(this.h); _ReplayAuthor(this.h);
final Hooks h; final Hooks h;

@ -23,7 +23,7 @@ EncryptOptions small({
RandomSource? random, RandomSource? random,
AccessPolicy policy = AccessPolicy.timeOnly, AccessPolicy policy = AccessPolicy.timeOnly,
bool portable = false, bool portable = false,
AuthorKey? authorKey, AuthorSigner? authorKey,
CmsSigner? cmsSigner, CmsSigner? cmsSigner,
Sealer? sealer, Sealer? sealer,
String comment = '', String comment = '',
@ -69,7 +69,7 @@ final class FailingSink implements ByteSink {
/// The author key of the case of alg 1, which records whether anything of /// The author key of the case of alg 1, which records whether anything of
/// the capsule had reached the sink when it was asked to sign. /// the capsule had reached the sink when it was asked to sign.
final class WatchingKey implements AuthorKey { final class WatchingKey implements AuthorSigner {
WatchingKey(this.hooks, this.sink); WatchingKey(this.hooks, this.sink);
final Json hooks; final Json hooks;

Loading…
Cancel
Save

Powered by TurnKey Linux.