The handoff, the plans and the protocol review describe the whole project, not this implementation, so they move unchanged to the private docs repository next to this one (../docs, commit 6e8d6c6). The folder is being renamed from App to datekeys-ts, to match datekeys-go. The package name, the README title and the site's licence notice follow. The README points to the prototype's new place, ../archive/prototype. npm run verify is green: 2,611 tests, build and build checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
f660f0c0d8
commit
5ee8813981
@ -1,197 +0,0 @@
|
||||
# Plan: fase 2 del SDK TypeScript. Descifrado y cifrado tlock sobre noble 2, verificación local de releases y canonicidad de puntos
|
||||
|
||||
Estado: v2, 26 de septiembre de 2026. Decisiones de la sección 2 confirmadas por el autor el 26-09-2026, con los ajustes de dos revisiones (la de Claude Opus y la de Fable), verificados contra el código, el spec y npm. Sustituye la "Fase 2, cifrar y descifrar" de la sección 8 del plan v2 (`PLAN_codec_cbor_y_pagina_svelte.md`), cuyo bloque "Piezas de `tlock-js` y `drand-client`" contenía dos errores: para Quicknet el descifrador es `decryptOnG2`, no `decryptOnG1`, y `drand-client` no hace falta.
|
||||
|
||||
Alcance: abrir y crear cápsulas del perfil Quicknet en el navegador, sin red, sobre la librería TypeScript de `App`. El núcleo IBE se escribe en el proyecto sobre `@noble/curves` 2.x, los releases se verifican con noble y el perfil pinneado, y la envoltura `age` la pone `age-encryption`. Ni `tlock-js` ni `drand-client` entran en el bundle.
|
||||
|
||||
Regla de dependencias: la del plan v2. En ejecución, solo `age`, `drand`, `tlock` y lo que ellas arrastran, en cualquier lenguaje; `@noble/curves` y `@noble/hashes` entran por ser lo que `age-encryption` arrastra. Toda dependencia nueva se propone por escrito y no se instala sin aprobación (sección 3).
|
||||
|
||||
---
|
||||
|
||||
## 1. Situación de partida
|
||||
|
||||
Todo lo de esta tabla se ejecutó el 26-09-2026 sobre `tlock-js` 0.9.0, `@noble/curves` 1.9.7 y 2.4.0, los fixtures de `App/testdata` y la librería Go. Los scripts quedaron en el scratchpad de la sesión (`…\scratchpad\noble2ibe\c`, `…\scratchpad\override`, `…\scratchpad\noblehist`); si el directorio temporal ya no existe, cada punto se reproduce a partir de la descripción.
|
||||
|
||||
| Hecho | Detalle |
|
||||
|---|---|
|
||||
| Las 5 615 discrepancias de noble 1.9.7 frente a Go son un solo fenómeno | 5 612 codificaciones no canónicas de coordenada (x + p, c0 o c1 + k·p) que decodifican al punto correcto y 3 identidades con flags o carga no nula. Cero en sentido contrario, cero fallos de subgrupo: `fromHex` de 1.9.7 ya ejecuta `assertValidity`. Sin la comprobación de longitud, 102 más por codificaciones sin comprimir. |
|
||||
| Efecto en tlock-js sobre 1.9.7 | `decryptOnG2` acepta un U recodificado como c0 + p y una firma x + p y devuelve la misma file key. `bls12-381.js:258` (G1) y `:358` (G2) reducen módulo p. |
|
||||
| Efecto en Go | U no canónico, identidad, fuera de curva, fuera de subgrupo o cuerpo de 127 o 129 bytes: `ERR_INTEGRITY` en el paso 11. Firma no canónica, identidad, negada o de otra ronda: `ERR_RELEASE_INVALID` en el paso 10. Firma y U malos a la vez: gana el paso 10. Con el cliente drand incorporado, una firma mala servida por el relay es `ERR_RELEASE_UNAVAILABLE` en el paso 9. |
|
||||
| `checkCompressedPoint` como puerta | Exigiendo `'point'`, bloquea todas las variantes anteriores, incluida la identidad canónica, y coincide con Go en las 49 451 entradas de los corpus. 11,4 ms por punto G2 y 6,5 ms por G1 en Node 24. |
|
||||
| Un override a noble 2.4.0 no carga | `npm install` termina con exit 0 y sin avisos. Después: `ERR_PACKAGE_PATH_NOT_EXPORTED` (2.x es ESM y solo exporta subrutas `.js`), `ProjectivePoint` y `toRawBytes` no existen, `fromHex` solo acepta string, `sha256.update("IBE-H2")` lanza con hashes 2.x. Con 12 a 18 líneas cambiadas el núcleo corre e interopera en ambos sentidos con 1.9.7. |
|
||||
| Módulo mínimo sobre noble 2.4.0 | 44 líneas de código (64 con comentarios), solo descifrado Quicknet. Pasa el tsconfig estricto de `App`. Descifra los cinco fixtures con las mismas file keys que tlock-js y que `tlock.TimeUnlock` en Go; el MAC de la cabecera `age` verifica; no carga nada de tlock-js. Rechaza el U no canónico en `fromBytes` como Go. |
|
||||
| Serialización de GT | `Fp12.toBytes` de noble (igual en 1.9.7 y 2.4.0) escribe c0 primero en cada nivel. kilic, kyber y `fp12ToBytes` de tlock-js escriben los doce limbs de 48 bytes en orden inverso. Con el orden de noble la clave sale mal (control negativo). |
|
||||
| Verificación de releases sin drand-client | `shortSignatures.verify(sig, shortSignatures.hash(sha256(uint64be(ronda)), DST), pk)` de noble 2.4.0 da `true` para la ronda 1000 y `false` para la 999 con la firma real. El DST por defecto de G1 es el de Quicknet. Ninguna versión de `drand-client` ha estado en noble 2.x; su verificación rfc9380 aceptó tres codificaciones alternativas de la firma real, cada una con un `randomness` distinto. |
|
||||
| tlock-js 0.9.0 | `gitHead` `17d817e` (18-03-2024) es el commit que pasó a `@noble/curves`; el código auditado en 2022-2023 corría sobre `@noble/bls12-381`; `fp.ts` es nuevo en 0.9.0 y cerca del 40 % de `ibe.ts` cambió después. Sin tags ni releases, master sin cambios desde entonces. Cinco dependencias de ejecución; instalación limpia de 33 paquetes y 10,6 MB. `npm audit` sin avisos. Su guarda de longitud de mensaje solo salta a partir de 512 bytes. |
|
||||
| Spec v0.8.2 | No nombra U, V ni W, ni sus tamaños, ni la canonicidad de las codificaciones. §12.1 pide "la codificación comprimida de BLS12-381 que usa drand", subgrupo y no infinito, sin x < p. El paso 11 de §63 solo dice que un cuerpo que no es un ciphertext tlock del scheme da `ERR_INTEGRITY`. La regla de facto es el decodificador kilic. |
|
||||
| Librería TypeScript hoy | `src/lib/dkc` ejecuta los pasos 1 a 8 e `inspect`; `age.ts` parsea cabeceras `age` y `checkTimeStanzas` aplica cardinalidad, tipo, argumentos, ronda y chain hash con los códigos de Go; `bls12381.ts` valida puntos comprimidos; no hay writer de cápsulas. `@noble/curves` 2.4.0 es dependencia de desarrollo y un test prohíbe importarla fuera de tests. |
|
||||
| Repositorio | `main` en `d5e3236`, en verde: la alineación con `datekeys-go` está fusionada y `testdata` sincronizado a `692cf87` (rama `v0.8.2`). La rama `wip/align-3820066` ya no existe. |
|
||||
| `age-encryption` 0.3.1 | Declara `@noble/curves ^2.0.1` y `@noble/hashes ^2.0.1`, que resuelven a la 2.4.0 de `App` sin override. Pero arrastra `@noble/post-quantum` 0.5.4, que declara `@noble/curves ~2.0.0` y `@noble/hashes ~2.0.0`: habrá una segunda copia de noble 2.0.x anidada bajo post-quantum, que la usa para ML-KEM y X25519, no para BLS. Admite la Streams API para cifrar y descifrar. |
|
||||
|
||||
---
|
||||
|
||||
## 2. Decisiones
|
||||
|
||||
Confirmadas por el autor el 26-09-2026 (paso 0 de la sección 10).
|
||||
|
||||
1. **Núcleo IBE propio** en `src/lib/dkc/ibe.ts`, derivado de `tlock-js/crypto/ibe.ts` (commit `17d817e`, licencia Apache-2.0 OR MIT, con aviso de procedencia) y con la semántica de `drand/kyber encrypt/ibe`. Sobre `@noble/curves` 2.3.0 o posterior. Se descarta parchear `tlock-js` y se descarta una puerta alrededor de `tlock-js` sobre noble 1.9.7, que dejaría dos versiones mayores de noble en el bundle.
|
||||
2. **Puerta de canonicidad**: toda firma, U y clave pública pasa por `checkCompressedPoint` y solo sigue si el resultado es exactamente `'point'`. Aunque noble 2.3.0 y posteriores rechazan lo mismo, la puerta fija la precedencia y el código de error, y trata la identidad canónica antes del pairing, donde noble la acepta y falla con otro mensaje.
|
||||
3. **Solo Quicknet** (`bls-unchained-g1-rfc9380`: clave pública en G2, firmas en G1, U en G2). Los otros dos schemes de drand quedan fuera de esta fase; se anota en la sección 12.
|
||||
4. **Releases sin red en esta fase**: la obtención (paso 9) queda fuera del SDK. El SDK recibe el release y lo verifica localmente (paso 10). La CSP `connect-src 'self'` de la página no cambia. El contrato de `ReleaseSource` se fija ya (sección 5): una fuente verifica cada respuesta, y si no obtiene ningún release verificado el resultado es `ERR_RELEASE_UNAVAILABLE` en el paso 9, como el cliente drand de Go (`provider/drand/client.go`); un release entregado por quien llama que no verifica es `ERR_RELEASE_INVALID` en el paso 10. Para el producto: la página habla con un único origen, la Release API propia, que consulta varios relays y verifica en el servidor (§47, §48); el SDK ofrece además la consulta directa a drand como fuente opcional (§49 SHOULD), nunca como opción por defecto de la página.
|
||||
5. **`age-encryption` 0.3.1** para las tres envolturas `age`, con `Identity` y `Recipient` propios para el stanza `tlock`. Confirma la decisión 4 del plan v2. Sin overrides de npm: el rango `^2.0.1` resuelve a 2.4.0, y la copia 2.0.x que trae `@noble/post-quantum` (que declara `~2.0.0` a propósito) se acepta si solo la usa post-quantum. Las guardas de la sección 3 lo comprueban y el paso 2 mide su coste en el bundle.
|
||||
6. **Cambio de spec**: canonicidad de puntos y contenido del cuerpo del stanza tlock (sección 7), como **enmienda de v0.8.2**, que no está fusionada ni etiquetada. En curso en `datekeys-go`, rama `v0.8.2`, junto con la limpieza del texto de error de kyber.
|
||||
7. **Apertura en streaming**: `PAYLOAD_AGE` se descifra con la Streams API de `age-encryption` hacia un fichero temporal en OPFS, que solo se muestra o se ofrece para descargar cuando `age` termina sin error (§56). §57 no acota `PAYLOAD_AGE`, así que el límite es la cuota de almacenamiento del navegador, que se consulta con `navigator.storage.estimate()` antes de empezar.
|
||||
8. **Licencia de `App`**: Apache-2.0, como `datekeys-go`. `ibe.ts` conserva el aviso de copyright y licencia de `tlock-js` (Apache-2.0 OR MIT).
|
||||
|
||||
---
|
||||
|
||||
## 3. Dependencias de ejecución y guardas
|
||||
|
||||
| Paquete | Versión | Estado |
|
||||
|---|---|---|
|
||||
| `age-encryption` | 0.3.1 | aprobada (decisión 5) e instalada el 28-09-2026. Arrastra `@noble/ciphers` 2.4.0, `@noble/curves` 2, `@noble/hashes` 2, `@noble/post-quantum` 0.5.4 (con su propia copia de `@noble/curves` y `@noble/hashes` 2.0.1) y `@scure/base` 2.4.0. |
|
||||
| `@noble/curves` | 2.4.0 | pasó de desarrollo a ejecución el 28-09-2026. Fijada exacta. |
|
||||
| `@noble/hashes` | 2.4.0 | dependencia exacta de `@noble/curves` 2.4.0; declarada explícita el 28-09-2026 por importarse directamente. |
|
||||
| `tlock-js`, `drand-client` | — | no se instalan. |
|
||||
|
||||
Guardas, todas en tests que corren en cada ejecución:
|
||||
|
||||
- ningún fichero de `src/` importa `tlock-js` ni `drand-client`;
|
||||
- `@noble/*` solo se importa desde `src/lib/dkc/ibe.ts`, `src/lib/dkc/release.ts` y los tests. El test actual "only tests import @noble/curves" de `bls12381.contrast.test.ts` pasa a una lista blanca con esos dos ficheros;
|
||||
- `ibe.ts`, `release.ts` y el test de contraste de `bls12381.ts` resuelven `@noble/curves` y `@noble/hashes` a la copia de la raíz, exactamente 2.4.0 (2.3.0 o posterior es el mínimo por la corrección de canonicidad), y ningún fichero de `src/` importa una copia anidada;
|
||||
- `package-lock.json` no contiene ninguna versión 1.x de `@noble/curves` ni de `@noble/hashes`, y cualquier copia 2.x distinta de la raíz está anidada bajo `@noble/post-quantum`;
|
||||
- `scripts/check-build.mjs` comprueba además que el sitio construido no contiene `tlock-js`, `drand-client` ni `@babel`, e informa del tamaño del bundle de la página de apertura.
|
||||
|
||||
---
|
||||
|
||||
## 4. Módulo IBE
|
||||
|
||||
`src/lib/dkc/ibe.ts`, sin dependencias fuera de `@noble/curves`, `@noble/hashes` y `bls12381.ts`.
|
||||
|
||||
**Semilla.** El módulo de descifrado medido el 26-09-2026 (`scratchpad\noble2ibe\c\ibe.ts`), con sus importaciones cambiadas a especificadores normales (`@noble/curves/bls12-381.js`, `@noble/curves/utils.js`, `@noble/hashes/sha2.js`).
|
||||
|
||||
**Contenido.**
|
||||
|
||||
- `Ciphertext { U, V, W }` y `decryptOnG2(signature, ct)`: longitudes (48, 96, `|V| == |W| <= 32`), puerta `'point'` sobre firma y U, `G1.Point.fromBytes` y `G2.Point.fromBytes` con `assertValidity`, `pairing(sig, U)`, `sigma = V xor H2(gt)`, `msg = W xor H4(sigma)`, `r = H3(sigma, msg)`, comprobación `r·G2 == U`.
|
||||
- `encryptOnG2RFC9380(publicKey, id, msg)`: puerta `'point'` sobre la clave, `|msg| <= 32` (kyber comprueba `len(msg) > Hash().Size()`; tlock-js tiene aquí un fallo que no se copia), `Qid = G1.hashToCurve(id, { DST: 'BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_' })`, `gt = pairing(Qid, pk)`, `sigma` de `|msg|` bytes con `crypto.getRandomValues`, `r = H3(sigma, msg)`, `U = r·G2`, `V = sigma xor H2(gt^r)`, `W = msg xor H4(sigma)`. `Fp12.pow` para `gt^r`.
|
||||
- `H2(gt) = SHA-256("IBE-H2" ‖ GT)[:len]` con GT serializado en el orden de kilic: para `[gt.c1, gt.c0]`, para `[a.c2, a.c1, a.c0]`, `Fp.toBytes(b.c1) ‖ Fp.toBytes(b.c0)`. Nunca `Fp12.toBytes` de noble.
|
||||
- `H3(sigma, msg)`: `base = SHA-256("IBE-H3" ‖ sigma ‖ msg)`; para `i = 1 … 65534`, `d = SHA-256(uint16le(i) ‖ base)`, `d[0] >>= 1`, se acepta el primer `d` big-endian menor que `Fr.ORDER`. Si el bucle termina, error.
|
||||
- `H4(sigma) = SHA-256("IBE-H4" ‖ sigma)[:len]`.
|
||||
- `roundIdentity(round) = SHA-256(uint64be(round))`.
|
||||
- Errores: una clase `IbeError` con un motivo fijo (`length`, `encoding`, `identity`, `proof`). Ningún mensaje de error incluye `sigma`, `msg`, `r` ni bytes de entrada. Es la lección del texto de error de kyber, que `datekeys-go` copiaba a sus diagnósticos (se corrige en la misma rama que la enmienda de la sección 7).
|
||||
- `sigma` y la file key se borran (`fill(0)`) en cuanto dejan de usarse, en todos los caminos, como hace la librería con `access_material` e `I_PAYLOAD`.
|
||||
- Para tests, una variante interna de `encryptOnG2RFC9380` recibe `sigma` en vez de generarlo, de modo que el cifrado se compara byte a byte con Go. No se exporta desde `index.ts`.
|
||||
|
||||
**Vectores de Go.** La referencia de los tests IBE es la librería Go, no los tests de `tlock-js`, cuyo `ibe.ts` cambió en un 40 % tras la auditoría. `scripts/ibe-go-vectors.go`, como `scripts/bls12381-go-verdicts.go`, genera con kyber y tlock: los bytes de GT de e(G1, G2) y de su cuadrado, H2, H3 y H4 sobre entradas fijas, la file key de cada stanza de los fixtures con la firma de su sidecar y un cifrado con `sigma` fijo. El resultado se congela en `src/lib/dkc/testing/`. El vector de GT de `tlock-js` (`cb87319f24560b5231579a09ad79f12e`) coincide con el de kyber (comprobado el 26-09-2026) y se conserva como contraste.
|
||||
|
||||
**Serialización del stanza.** Cuerpo `U ‖ V ‖ W` de 128 bytes para una file key de 16; argumentos `[decimal canónico de la ronda, chain hash en hexadecimal minúsculo]`. La lectura ya existe en `age.ts`; la escritura se añade en `age.ts` junto a ella.
|
||||
|
||||
---
|
||||
|
||||
## 5. Verificación de releases
|
||||
|
||||
`src/lib/dkc/release.ts`, `verifyRelease(profile, round, release)`, en el mismo orden que `provider.Verify` de Go (`provider/provider.go:53-76`), que decodifica la clave antes de mirar la firma:
|
||||
|
||||
1. ronda fuera del rango del perfil → `ERR_DATEKEY_INVALID`;
|
||||
2. ronda del release distinta de la ronda de la condición → `ERR_ROUND_MISMATCH`;
|
||||
3. firma que no mide 48 bytes → `ERR_RELEASE_INVALID`;
|
||||
4. clave del perfil pinneado, ya validada en los pasos 1 a 8; si no decodifica → `ERR_UNKNOWN_PROFILE`;
|
||||
5. `checkCompressedPoint('G1', firma) !== 'point'` → `ERR_RELEASE_INVALID`;
|
||||
6. `shortSignatures.verify(firma, shortSignatures.hash(sha256(uint64be(ronda)), DST_QUICKNET), clave)` distinto de `true` → `ERR_RELEASE_INVALID`. Una excepción de noble en este punto también es `ERR_RELEASE_INVALID`.
|
||||
|
||||
La interfaz `ReleaseSource` del SDK solo tiene una implementación estática en esta fase: el release embebido en el sidecar del fixture o suministrado por la aplicación. Su contrato se fija ya para las fuentes con red de fases posteriores: una fuente llama a `verifyRelease` con cada respuesta y, si ninguna verifica, falla con `ERR_RELEASE_UNAVAILABLE` en el paso 9; solo un release entregado directamente por quien llama da `ERR_RELEASE_INVALID` en el paso 10. Es el comportamiento de `provider/drand/client.go` en Go.
|
||||
|
||||
Además, desde la corrección 6 de §76, cualquier fallo de una fuente se informa en el paso 9 con `ERR_RELEASE_UNAVAILABLE` y ningún otro código. Si el error de la fuente lleva otro código normativo, o ninguno, se conserva solo su texto. Si el contexto termina, se sigue pudiendo detectar. Así lo hacen `capsule.Open` (`sourceFailure`) y `provider/drand.Client` en Go `9ac9cd9`, y `open.ts` debe hacer lo mismo.
|
||||
|
||||
---
|
||||
|
||||
## 6. Apertura y cifrado con `age-encryption`
|
||||
|
||||
`src/lib/dkc/open.ts` reproduce los pasos 9 a 18 de `capsule/open.go`, sobre los pasos 1 a 8 que ya ejecuta `inspect`:
|
||||
|
||||
- paso 9, release por `ReleaseSource`; paso 10, `verifyRelease`;
|
||||
- paso 11, `OUTER_TIME_AGE` con `Decrypter` de `age-encryption` y una `Identity` propia cuyo `unwrapFileKey(stanzas)` ejecuta, en este orden, `checkTimeStanzas` (cardinalidad de §63, como `agewrap.TimeIdentity.Unwrap` en Go), `verifyRelease` otra vez, la comprobación de 128 bytes del cuerpo, `decryptOnG2` y la longitud 16 de la file key. Cada fallo de cuerpo o de descifrado es `ERR_INTEGRITY`; el MAC de la cabecera lo comprueba `age-encryption`;
|
||||
- pasos 12 a 18 como en Go: estructura de política, `INNER_ACCESS_AGE` con las identidades del llamante, `CONTROL_CBOR` canónico, `header_binding` sobre los bytes exactos, identidad de payload y `PAYLOAD_AGE`;
|
||||
- `PAYLOAD_AGE` se descifra en streaming (decisión 7): `File.slice(offset).stream()` entra en `Decrypter.decrypt`, la salida se escribe en un fichero temporal de OPFS y el plaintext solo se entrega cuando el stream termina sin error; si falla la autenticación en cualquier chunk, el fichero temporal se borra y no se muestra nada (§56). Antes de empezar se compara el tamaño de `PAYLOAD_AGE` con la cuota libre.
|
||||
|
||||
Cifrado: `Encrypter` con un `Recipient` propio cuyo `wrapFileKey(fileKey)` llama a `encryptOnG2RFC9380(clave del perfil, roundIdentity(ronda), fileKey)` y devuelve el stanza `tlock`. La construcción completa de un `.dkc` (prelude, cabecera, control sellado) requiere el writer TypeScript, que es fase 3; en esta fase el cifrado se prueba a nivel de stanza y de fichero `age` (sección 8).
|
||||
|
||||
Antes de escribir código se comprueba en el paquete instalado la forma exacta de `Identity`, `Recipient` y `Stanza` de `age-encryption` 0.3.1 y se anota en este plan.
|
||||
|
||||
---
|
||||
|
||||
## 7. Cambios en la especificación y en `datekeys-go`
|
||||
|
||||
Todo en un único cambio normativo, con vectores congelados, según la política de §76 (caso de "segunda implementación independiente" y "prueba de interoperabilidad").
|
||||
|
||||
- **Definición de codificación canónica de punto**, en §12 o en un apartado nuevo: la serialización comprimida de BLS12-381 que produce drand, 48 bytes en G1 y 96 en G2; bit de compresión a 1; bit de infinito solo en el punto en el infinito, con el resto de bytes a cero; coordenadas big-endian menores que p, y en G2 c1 seguido de c0, ambas menores que p; el punto está en el subgrupo de orden primo. Un decodificador MUST rechazar cualquier otra cadena de bytes, en particular x + p y una identidad con carga o con el bit de compresión ausente.
|
||||
- **§12.1, punto 2**: `public_key` es una codificación canónica según esa definición, distinta del infinito. Código sin cambio: `ERR_UNKNOWN_PROFILE`.
|
||||
- **§63, paso 10**: la firma es una codificación canónica de un punto del grupo de firmas del scheme, distinta del infinito, y verifica como firma de la ronda; en otro caso `ERR_RELEASE_INVALID`. Se mantiene la precedencia de `ERR_ROUND_MISMATCH`.
|
||||
- **§63, paso 11**: el cuerpo del stanza `tlock` es `U ‖ V ‖ W`, con `|U|` igual al tamaño de punto del grupo de claves del scheme (96 para Quicknet), `|V| = |W| = 16`; U es una codificación canónica de un punto de ese grupo distinta del infinito; el descifrado IBE-CCA comprueba `r·G == U`. Cualquier fallo, incluida una longitud distinta de 128, es `ERR_INTEGRITY`. Se cita drand/tlock para H2, H3 y H4.
|
||||
- **§64, mutaciones nuevas** con código y paso, verificados en Go el 26-09-2026: U con c0 + p, U identidad canónica, U con flag de infinito y carga, cuerpo de 127 y 129 bytes → `ERR_INTEGRITY`, paso 11; firma x + p (requiere un release de prueba cuya x lo permita, o un vector sintético documentado), firma identidad, firma con flag de infinito y carga, firma negada → `ERR_RELEASE_INVALID`, paso 10; firma mala y U malo a la vez → `ERR_RELEASE_INVALID`, paso 10.
|
||||
- **`datekeys-go`**: las mismas mutaciones en `internal/testkit/mutations.go`, regeneración de `testdata/vectors/mutations.json`, `traceability.md` y `CHANGELOG.md`. El comportamiento del código no cambia. Aparte, la tarea ya propuesta de no copiar el texto de error de kyber a los errores y a `Inspection.Checks[].Detail`.
|
||||
- **`App`**: `testdata:sync` al commit resultante y reproducción en TypeScript de código y paso de cada mutación nueva.
|
||||
|
||||
---
|
||||
|
||||
## 8. Tests
|
||||
|
||||
1. **`ibe.test.ts`.** Los vectores de Go de la sección 4 (GT, H2, H3, H4, las file keys de los fixtures y el cifrado con `sigma` fijo), y como contraste el de tlock-js (`gtToHash` de 16 bytes `cb87319f24560b5231579a09ad79f12e`, igual al de kyber). Los cinco fixtures: la file key obtenida con la firma del sidecar abre la cabecera `age`. Control negativo: serializar GT con `Fp12.toBytes` de noble da otra clave. U con c0 + p, U identidad, U negado y firma de otra ronda: rechazados con el motivo esperado. Ida y vuelta `encryptOnG2RFC9380` → `decryptOnG2` con una clave sintética. Cobertura del 100 %.
|
||||
2. **`release.test.ts`.** Ronda 1000 con la firma real: válida; 999: inválida; codificaciones alternativas de la misma firma (sin comprimir, x + p, flag de signo alterado): `ERR_RELEASE_INVALID`; longitud 47 y 96; identidad. Cobertura del 100 %.
|
||||
3. **`open.test.ts`.** Los cinco fixtures se abren con el release del sidecar y el SHA-256 del plaintext coincide con el sidecar; los `.dkk` de `time_and_key` abren con sus identidades. El corpus de mutaciones exportado, incluidas las nuevas de la sección 7, reproduce código y paso.
|
||||
4. **Interoperabilidad TS → Go a nivel IBE.** `scripts/ibe-go-roundtrip.go`, como `scripts/bls12381-go-verdicts.go`: TypeScript cifra 16 bytes para la ronda 1000 con la clave de Quicknet y escribe `U ‖ V ‖ W`; Go los abre con `tlock.BytesToCiphertext` y `tlock.TimeUnlock` con la firma real y compara. Se ejecuta a mano y su resultado se congela como vector.
|
||||
5. **Interoperabilidad TS → Go a nivel de fichero `age`.** Un fichero `age` con un stanza `tlock` escrito por `age-encryption` con el `Recipient` propio, abierto por `agewrap` de Go con la firma real. Se ejecuta a mano y se congela.
|
||||
6. **Guardas** de la sección 3.
|
||||
7. **Rendimiento**, informativo: tiempo de `decryptOnG2` en frío y en caliente en Node y en el navegador de la página.
|
||||
|
||||
---
|
||||
|
||||
## 9. Página
|
||||
|
||||
La ruta `/inspect` gana una acción "abrir": con un fixture o un `.dkc` arrastrado, un release pegado o el del sidecar, y las identidades `.dkk` cuando la política las exige. Muestra el resultado de cada paso 9 a 18 como hoy muestra 1 a 8, y el SHA-256 del plaintext; para los fixtures, además el plaintext. Sin red: la CSP no cambia y `check-build` lo comprueba. El plaintext nunca se envía.
|
||||
|
||||
Precisión aprobada por el autor el 28-09-2026, tras revisar lo que dice el protocolo:
|
||||
- **El plaintext de un fichero propio se guarda solo en un fichero temporal** del almacenamiento privado del navegador (OPFS), hasta la descarga. Es el "fichero temporal" que recomienda §56, y ninguna regla del protocolo prohíbe guardarlo en el cliente. Se borra al pedirlo, al abrir o cargar otra cápsula, al salir de la página y, si quedó, en la siguiente visita.
|
||||
- **El release lo suministra quien abre** (§63 paso 10): pegado de la respuesta de drand, que la persona abre en otra pestaña desde un enlace de la página, o el del registro de un fixture. Solo se leen su ronda y su firma (§11, §13).
|
||||
- **§48 y §49 siguen pendientes.** Con solo releases suministrados, la librería no cumple aún el SHOULD de §48 (varios relays) y no resuelve por sí misma el de §49 (obtener el release directamente del proveedor, sin la API DateKeys). Los cubrirá la fuente drand opcional del SDK, nunca activa por defecto en la página (decisión 4, sección 12).
|
||||
|
||||
---
|
||||
|
||||
## 10. Orden de trabajo y criterios de aceptación
|
||||
|
||||
| Paso | Contenido | Hecho cuando |
|
||||
|---|---|---|
|
||||
| 0 | Confirmar las decisiones de la sección 2 y aprobar las dependencias de la sección 3 | hecho el 26-09-2026 |
|
||||
| 1 | Precondición: `main` verde con `testdata` sincronizado al último commit de `datekeys-go` | cumplida en `d5e3236` (`692cf87`) y de nuevo en `71ab8fb`, con `testdata` en `9ac9cd9` (`spec-v0.8.2`) |
|
||||
| 2 | Dependencias y guardas | instaladas con versiones exactas; `npm audit --omit=dev` sin avisos; guardas en verde; en el lockfile, un solo noble 2.4.0 en la raíz, la copia 2.0.1 solo bajo `@noble/post-quantum` y ningún 1.x. Hecho el 28-09-2026: el README de `App` recoge las guardas, la medida del bundle y el resultado de `npm audit` |
|
||||
| 3 | `ibe.ts` de descifrado desde la semilla, `roundIdentity`, escritura del stanza en `age.ts`, `ibe.test.ts` sin la parte de cifrado | los cinco fixtures dan la file key correcta; U no canónico e identidad rechazados; cobertura 100 %. Hecho el 28-09-2026: vectores de `scripts/ibe-go-vectors.go` en `src/lib/dkc/testing/ibe-vectors.json`; `ibe.ts` al 100 %, fijado como umbral. `ibe.ts` también pasa el cuerpo `U ‖ V ‖ W` a bytes; los argumentos del stanza y su paso al `Stanza` de `age-encryption`, que guarda el tipo en `args[0]`, van al paso 7, con el `Recipient` que los usa |
|
||||
| 4 | `release.ts` y `release.test.ts` | ronda real válida, alias rechazados. Hecho el 28-09-2026:<br>- `verifyRelease` con el orden y los textos de `provider.Verify`;<br>- `ReleaseSource`, con el contrato de la sección 5, y `suppliedRelease`;<br>- los casos de `TestVerifyRejects` de Go y los 7 del corpus de mutaciones que fallan en el paso 10;<br>- las firmas publicadas de las rondas 1000, 1001, 2000 y 1004, esta última obtenida restando p a la codificación x + p del corpus;<br>- cobertura del 100 %, fijada como umbral |
|
||||
| 5 | `open.ts` con la `Identity` propia, pasos 9 a 18, `open.test.ts` | los cinco fixtures se abren y el plaintext coincide con el sidecar; el corpus de mutaciones existente reproduce código y paso.<br>5a hecho el 28-09-2026, con el texto en claro en memoria:<br>- los 65 casos del corpus pasan por `open` con el código y el paso de Go;<br>- los cinco fixtures se abren con cada credencial;<br>- los textos siguen a `capsule.Open` y `agewrap`.<br>El paso 13 exige probar cada identity contra cada stanza X25519, y `age-encryption` no expone su `X25519Identity`. Por eso `x25519.ts` abre los stanzas de uno en uno, con ChaCha20-Poly1305 de `@noble/ciphers` 2.4.0, dependencia aprobada el 28-09-2026 porque es la copia que ya usa `age-encryption`. `bech32.ts` lee las identidades `AGE-SECRET-KEY-1…`.<br>`index.ts` aún no reexporta la apertura, que metería noble en `/inspect`: el paso 8 la cargará bajo demanda.<br>5b hecho el 28-09-2026: `open` acepta un `Blob`, del que lee solo el prefijo de los pasos 1 a 8 (`prefix.ts`, movido de la página a la librería), calcula el `capsule_digest` en streaming (`digest.ts`) y descifra `PAYLOAD_AGE` en streaming. La salida puede ser un `WritableStream`, que se cierra tras el paso 18 y se aborta ante cualquier fallo. Los 65 casos del corpus pasan también así.<br>Comprobado en el navegador con un fichero OPFS (`createWritable`): `time_only` se abre con el SHA-256 del sidecar, y un fallo de STREAM deja intacto el contenido anterior del fichero.<br>La consulta de cuota (`navigator.storage.estimate()`), el fichero temporal y la descarga van con la página, en el paso 8 |
|
||||
| 6 | Spec, mutaciones en Go, `testdata:sync`, reproducción en TypeScript (sección 7) | texto aprobado; vectores congelados en ambos repositorios; commits en Gitea. Hecho: la enmienda de canonicidad entró en la v0.8.2 (`f6f2e9f`, tag `spec-v0.8.2`), y sus 10 mutaciones pasan por `open` en TypeScript desde el paso 5a |
|
||||
| 7 | `encryptOnG2RFC9380`, `Recipient` propio, ida y vuelta, interoperabilidad TS → Go a nivel IBE y de fichero `age` (sección 8, puntos 4 y 5) | Go abre lo que TypeScript cifra; vectores congelados.<br>Hecho el 28-09-2026:<br>- `ibe.ts` gana `encryptOnG2RFC9380` y `encryptOnG2WithSigma` (solo para tests);<br>- `tlock.ts` gana `timeRecipient`, con las comprobaciones y textos de `NewTimeRecipient`;<br>- `scripts/tlock-go-vectors.go` reescribe `EncryptCCAonG2` con sigma fijo, lo comprueba con kyber y tlock, y abre las muestras de `scripts/tlock-ts-samples.mjs`: Go abrió los cuerpos IBE y los ficheros `age` de las rondas 1000 y 1001 con la misma file key y el mismo texto;<br>- todo congelado en `src/lib/dkc/testing/tlock-vectors.json`;<br>- cobertura del 100 % de `ibe.ts` y `tlock.ts` |
|
||||
| 8 | Página (sección 9) | un fixture `time_and_key` se abre en el navegador sin red; `check-build` en verde; tamaño del bundle anotado en el README.<br>Hecho el 28-09-2026:<br>- `OpenPanel.svelte`, con `release-input.ts` (release pegado o del registro del fixture), `opener.ts` (cargado con `import()`), `opening.ts` (pasos 9 a 18 como los registra la referencia) y `tempfile.ts` (fichero temporal de OPFS, un directorio y un Web Lock por pestaña, cuota libre y limpieza);<br>- `readAccessKey` en `prefix.ts`;<br>- `licenses.txt` con los avisos de `tlock-js` y `age` y los de cada paquete del bundle, escrito por `vite.config.ts`;<br>- nuevas guardas de `check-build`: ninguna página carga noble, `@scure/base` ni `age-encryption` en la primera carga, y `licenses.txt` está completo;<br>- cobertura del 100 % de los módulos nuevos.<br>Comprobado en el navegador con la compilación de producción:<br>- se abren los fixtures `time_only`, `time_and_key_portable` (con su `.dkk`) y `time_and_key_recipients` (con una identidad pegada), cada uno con el SHA-256 de su registro;<br>- una firma alterada da `ERR_RELEASE_INVALID` en el paso 10, y un fragmento STREAM alterado da `ERR_INTEGRITY` en el paso 17, sin descarga ni fichero temporal;<br>- un fichero propio se abre al fichero temporal, se descarga sin violar la CSP y se borra con su lock;<br>- lo que dejó una pestaña terminada se borra en la visita siguiente;<br>- ninguna petición sale del origen, y la página no se desborda a 375 px de ancho.<br>Revisión adversarial, en cuatro dimensiones (protocolo, seguridad, estado de la interfaz, tests y guardas), con cada hallazgo contrastado por un revisor que intentaba refutarlo: se confirmaron 15 hallazgos, algunos repetidos, y se refutaron 5. Todos se corrigieron:<br>- la fecha se vuelve a mirar cuando llega y cuando la pestaña vuelve a verse;<br>- el fichero de una apertura en curso es de la página: se borra con ella, y la apertura se detiene si se carga otra cápsula (`cancellable`);<br>- se vuelve a comprobar tras cada espera si la apertura sigue vigente;<br>- si el navegador rechaza OPFS, la apertura se hace en memoria;<br>- el nombre ofrecido para descargar no lleva caracteres invisibles;<br>- las glosas de `ERR_INVALID_MAGIC` y `ERR_EXTENSION_CRITICAL_UNKNOWN` valen también para la `.dkk`;<br>- el foco va al campo o al mensaje del error;<br>- tras volver de la caché de atrás y adelante no se ofrece un enlace muerto;<br>- un nombre largo no desborda la página;<br>- `licenses.txt` incluye el código de Vite y rolldown que entra en el bundle;<br>- la guarda de la primera carga resuelve los scripts respecto a cada página y falla si no encuentra ninguno |
|
||||
|
||||
Cada paso termina con `npm run verify` en verde y un commit en Gitea. El paso 6 puede ir en paralelo con el 4 y el 5.
|
||||
|
||||
---
|
||||
|
||||
## 11. Riesgos
|
||||
|
||||
- **API de `age-encryption` 0.3.1** para `Identity`, `Recipient` y `Stanza`. Mitigación: comprobarla en el paquete instalado antes del paso 5 y fijar la versión exacta.
|
||||
- **Cambios de API de noble 2.x.** Ya ocurrió entre 1.x y 2.x. Mitigación: versión exacta, guardas y el test del vector de GT, que detecta cualquier cambio de orden o de `Fp.toBytes`.
|
||||
- **Orden de bytes de GT.** Es el único punto donde una implementación puede coincidir en todo lo demás y fallar aquí. Mitigación: el vector de tlock-js y el control negativo.
|
||||
- **Doble noble en el bundle.** `@noble/post-quantum` 0.5.4 ya trae su propia copia 2.0.x; si alguna dependencia futura arrastra 1.x, sería peor. Mitigación: las guardas de la sección 3 (ningún 1.x, copias 2.x distintas solo anidadas bajo post-quantum, el código BLS en la 2.4.0 exacta) y la medida del bundle en el paso 2.
|
||||
- **Diagnósticos con material interno.** Mitigación: la regla de errores de la sección 4 y un test que busca en los mensajes de error los bytes de `sigma`, `msg` y `r`.
|
||||
- **`H3` devuelve 0**, con probabilidad 2⁻²⁵⁵: `multiply(0)` lanza `RangeError`. Mitigación: tratar cualquier excepción del cálculo como `proof` y cubrirlo con un test que inyecte `r = 0`.
|
||||
- **Rendimiento en el navegador.** Un pairing y una multiplicación escalar en G2 rondan los 40 a 300 ms en Node; la puerta añade 11 ms por punto. Mitigación: medir en el paso 8 y, si hace falta, descifrar en un worker. Medido en el paso 8, en Chromium, sin contar la descarga del código: 0,34 s los pasos 9 a 18 de `time_only`, la primera apertura, y 0,11 s los de `time_and_key_portable`. No hace falta un worker, que la CSP (`worker-src 'none'`) tampoco permite.
|
||||
- **Rama `wip/align-3820066`.** Si no se fusiona antes, el paso 3 partiría de un árbol distinto del que verifican los tests actuales. Mitigación: el paso 1.
|
||||
- **Aprobación de la spec.** El paso 6 depende del autor; los pasos 3 a 5 no.
|
||||
|
||||
---
|
||||
|
||||
## 12. Fuera de alcance y decisiones aplazadas
|
||||
|
||||
- Writer completo de `.dkc` y `.dkk` en TypeScript y la prueba TS → Go a nivel de cápsula: fase 3.
|
||||
- Los schemes `pedersen-bls-unchained` y `bls-unchained-on-g1`: sin uso previsto; si se añaden, el módulo crece con `encryptOnG1` y `decryptOnG1` y la puerta cambia de grupo.
|
||||
- Obtención de releases por red desde el SDK: fuera de esta fase; el servidor y la CLI Go siguen siendo la vía. Es lo que cubrirá los SHOULD de §48 (varios relays) y §49 (release directo del proveedor): una fuente drand opcional del SDK, que verifica cada respuesta y descarta las que fallan (corrección 6 de §76), nunca activa por defecto en la página.
|
||||
- Reutilización de la implementación `age` de tlock-js: descartada; `age-encryption` es la implementación oficial.
|
||||
@ -1,276 +0,0 @@
|
||||
# Plan de implementación: `datekeys-go`
|
||||
|
||||
Implementación de referencia en Go de la **DateKeys Protocol Specification v0.8.1**.
|
||||
|
||||
Estado: borrador para revisión. Fecha: 25 de septiembre de 2026.
|
||||
|
||||
---
|
||||
|
||||
## 1. Objetivo y principios
|
||||
|
||||
Construir desde cero una librería Go que implemente el protocolo base tal como lo fija la v0.8.1, con estas reglas:
|
||||
|
||||
1. **La especificación manda.** El código no añade semántica. Si el código descubre un problema en el spec, se abre un caso reproducible según la política de cambios de la sección 76.
|
||||
2. **Cero criptografía propia.** Solo `age`, `tlock` y la verificación BLS de `drand`. La librería aporta framing, CBOR, bindings, reglas de verificación y flujo.
|
||||
3. **Validar todo lo verificable localmente antes de tocar red o secretos.** Sección 63.
|
||||
4. **Fallar cerrado.** Ningún plaintext parcial, ningún error enmascarado, ningún "verified: true" ajeno.
|
||||
5. **Superficie pequeña y auditable.** Pocas dependencias, todas pinneadas, builds reproducibles, y un mapa de trazabilidad spec ↔ código para la revisión externa.
|
||||
6. **Nueva librería, no refactor.** Lo aprovechable del prototipo se copia y se adapta; el repositorio actual no se modifica.
|
||||
|
||||
---
|
||||
|
||||
## 2. Alcance de la primera versión de la librería (v0.1.0)
|
||||
|
||||
Implementa:
|
||||
|
||||
- DateKey: resolución fecha → ronda, `dk1_` canónico, vectores (secciones 14 a 19).
|
||||
- Provider Profile Quicknet: struct, CBOR canónico, `profile_hash`, registro pinneado (secciones 10 a 13).
|
||||
- Adaptador drand: obtención de releases por relays, verificación BLS local, modo estricto (secciones 35, 45 a 52).
|
||||
- `.dkc`: prelude, PUBLIC_HEADER, CONTROL_CBOR, `header_binding`, `time_only` y `time_and_key`, flujo de cifrado y descifrado completo con SHOULD y MUST (secciones 20 a 39, 61 a 63).
|
||||
- `.dkk`: framing, body, identity portable de un solo uso (secciones 40 a 44).
|
||||
- Extensiones genéricas con reglas de duplicados y orden canónico (sección 54).
|
||||
- Catálogo de errores (sección 69). Límites de parser (57). Canonicidad CBOR y omisión de opcionales (58, 58.1).
|
||||
- Fixtures, mutaciones, fuzzing y CLI mínima.
|
||||
|
||||
No implementa, a propósito:
|
||||
|
||||
- Servidor de Release API ni Release Queue. La librería define la interfaz de fuente de releases; el servidor actual podrá consumirla más adelante.
|
||||
- Almacenamiento, entrega de `.dkk`, servicios, extensiones concretas.
|
||||
- Cliente TypeScript. Vendrá después con la misma suite de fixtures.
|
||||
|
||||
---
|
||||
|
||||
## 3. Decisiones de diseño
|
||||
|
||||
### 3.1 Módulo y layout
|
||||
|
||||
Repositorio: Gitea propio, `https://g.activething.com/go/DateKeys.git`. Módulo: `g.activething.com/go/DateKeys`, la ruta que el propio Gitea anuncia en su etiqueta `go-import`. Si más adelante se quiere una ruta en `datekeys.com` que no dependa del servidor, es un cambio de una línea en `go.mod` y un `sed` en las importaciones, siempre antes de publicar. Go 1.26, con `go.mod` fijando la última versión de parche de la serie. No se usa GitHub para nada del proyecto; las dependencias alojadas allí se obtienen como módulos Go, igual que cualquier otra.
|
||||
|
||||
```text
|
||||
datekeys-go/
|
||||
go.mod
|
||||
LICENSE Apache-2.0 (código)
|
||||
README.md SECURITY.md CONTRIBUTING.md TRADEMARKS.md CHANGELOG.md
|
||||
spec/
|
||||
DateKeys_Protocol_Specification_v0.8.1.md copia congelada
|
||||
datekeys.cddl schema normativo de todos los CBOR
|
||||
datekey/ DateKey, resolución, dk1_ canónico
|
||||
profile/ Provider Profile, CBOR canónico, hash, registro pinneado, quicknet.go
|
||||
provider/ interfaces Condition, Release, ReleaseSource, Verify
|
||||
provider/drand/ relays HTTP, verificación BLS, modo estricto
|
||||
codec/ CBOR determinista: Encode, Decode con comprobación por reencodificación, límites
|
||||
codec/bech32/ codificación Bech32 para la forma humana de identities (ver 4.3)
|
||||
agewrap/ identities y recipients envolventes de age: cardinalidad, sonda de inspección, stanza tlock
|
||||
extension/ tipos y reglas de extensiones
|
||||
capsule/ .dkc: framing, cabecera, control, binding, Encrypt, Inspect, Open
|
||||
accesskey/ .dkk: framing, body, identity
|
||||
errors.go sentinel errors del catálogo de la sección 69
|
||||
internal/testkit/ generación de fixtures, escritor de cabeceras age malformadas, mutadores
|
||||
testdata/fixtures/ fixtures oficiales y sus valores esperados
|
||||
docs/traceability.md mapa sección del spec → paquete, función, test
|
||||
cmd/datekeys/ CLI: encrypt, decrypt, inspect, datekey, profile
|
||||
```
|
||||
|
||||
### 3.2 API pública, deliberadamente pequeña
|
||||
|
||||
```go
|
||||
// datekey
|
||||
type DateKey struct { ProfileID string; Round uint64 }
|
||||
func Resolve(p *profile.Profile, at time.Time) (DateKey, error) // §15, precisión completa
|
||||
func Parse(s string) (DateKey, error) // §19, solo forma canónica
|
||||
func (d DateKey) Compact() string // dk1_...
|
||||
func (d DateKey) UnlockAt(p *profile.Profile) time.Time
|
||||
|
||||
// profile
|
||||
type Profile struct { ID, Provider, Network string; ChainHash [32]byte; PublicKey []byte;
|
||||
Period time.Duration; GenesisTime int64; Scheme string; GenesisSeed [32]byte }
|
||||
func (p *Profile) CanonicalCBOR() []byte
|
||||
func (p *Profile) Hash() [32]byte // §11
|
||||
var Quicknet *Profile // §12, pinneado en el binario
|
||||
type Registry interface { Lookup(id string) (*Profile, bool) }
|
||||
|
||||
// provider
|
||||
type Condition struct { Round uint64 }
|
||||
type Release struct { Round uint64; Signature []byte }
|
||||
type ReleaseSource interface { Fetch(ctx context.Context, p *profile.Profile, c Condition) (Release, error) }
|
||||
func Verify(p *profile.Profile, c Condition, r Release) error // §51, BLS local siempre
|
||||
|
||||
// capsule
|
||||
type Policy uint8 // TimeOnly = 0, TimeAndKey = 1
|
||||
type EncryptOptions struct {
|
||||
Profile *profile.Profile
|
||||
UnlockAt time.Time
|
||||
Policy Policy
|
||||
Recipients []age.Recipient // X25519 de destinatarios conocidos
|
||||
NewPortableKey bool // genera I_ACCESS y devuelve la .dkk
|
||||
Critical, Noncritical []extension.Extension
|
||||
}
|
||||
type Result struct { DateKey datekey.DateKey; CapsuleID [16]byte; PortableKey *accesskey.AccessKey }
|
||||
func Encrypt(dst io.Writer, src io.Reader, opts EncryptOptions) (*Result, error)
|
||||
|
||||
type Inspection struct { /* prelude, cabecera, política, ronda, unlock, resultado de cada comprobación */ }
|
||||
func Inspect(r io.ReadSeeker, reg profile.Registry) (*Inspection, error) // pasos 1 a 8, sin red ni secretos
|
||||
|
||||
type OpenOptions struct {
|
||||
Registry profile.Registry
|
||||
Source provider.ReleaseSource
|
||||
Identities []age.Identity // X25519 propias del destinatario
|
||||
AccessKey *accesskey.AccessKey // .dkk portable
|
||||
Now func() time.Time // inyectable para tests
|
||||
}
|
||||
func Open(dst io.Writer, r io.ReadSeeker, opts OpenOptions) error // pasos 9 a 18
|
||||
|
||||
// accesskey
|
||||
type AccessKey struct { CredentialID, CapsuleID [16]byte; Type string; Material []byte;
|
||||
Verification *Verification; Critical, Noncritical []extension.Extension }
|
||||
func Encode(w io.Writer, k *AccessKey) error
|
||||
func Decode(r io.Reader) (*AccessKey, error)
|
||||
func (k *AccessKey) Identity() (age.Identity, error)
|
||||
```
|
||||
|
||||
Reglas de la API:
|
||||
|
||||
- `io.Reader` y `io.Writer` en todo; el payload nunca se carga entero en memoria.
|
||||
- `Open` recibe `io.ReadSeeker` para poder inspeccionar la cabecera de PAYLOAD_AGE en el offset `16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN` antes de pedir el release. Si el lector no permite seek, se omite el SHOULD y se aplica el MUST al abrir.
|
||||
- El reloj se inyecta. Ningún paquete llama a `time.Now` directamente salvo la CLI.
|
||||
- Ningún tipo imprime secretos en `String()`. Las identities se pasan como tipos de `age`, no como bytes sueltos, salvo en `accesskey`.
|
||||
|
||||
### 3.3 Cómo se usan `age` y `tlock`
|
||||
|
||||
- **PAYLOAD_AGE e INNER_ACCESS_AGE:** `age.Encrypt` y `age.Decrypt` con recipients e identities X25519 de la API pública de `age`.
|
||||
- **OUTER_TIME_AGE:** `age.Encrypt` con un recipient propio en `agewrap` que llama a `tlock.TimeLock` y `tlock.CiphertextToBytes`, ambos exportados, y emite el stanza `tlock <ronda> <chainhash>` con el mismo formato que `tlock`. Para abrir, una identity propia que comprueba el conjunto completo de stanzas, la ronda y el chain hash contra la DateKey y el perfil, y llama a `tlock.TimeUnlock`, que verifica el beacon antes de descifrar.
|
||||
- Motivo: `tlock.New(...).Decrypt` usa una identity no exportada en la que no podemos imponer la cardinalidad ni evitar que todo error se convierta en `ErrTooEarly`. Usando su núcleo exportado se mantiene la compatibilidad de stanza con `tle` y se gana control del flujo.
|
||||
- **Cardinalidad MUST:** en las tres identities envolventes, dentro de `Unwrap`, que recibe todos los stanzas del fichero. Sin APIs internas.
|
||||
- **Inspección SHOULD:** `age.ExtractHeader` lee únicamente la cabecera del fichero, y `age.DecryptHeader` con una identity sonda, cuyo `Unwrap` registra los stanzas recibidos y devuelve un error centinela distinto de `ErrIncorrectIdentity`, entrega los stanzas parseados por `age` sin descifrar nada ni usar secretos. Ambas funciones son API pública de `age` 1.3. Para PAYLOAD_AGE se aplica en el offset del prelude. No hace falta parser propio.
|
||||
|
||||
---
|
||||
|
||||
## 4. Dependencias
|
||||
|
||||
| Dependencia | Versión | Uso | Nota |
|
||||
|---|---|---|---|
|
||||
| `filippo.io/age` | v1.3.2 | ficheros age, X25519, `Stanza`, `Recipient`, `Identity` | única implementación de STREAM; no se reimplementa |
|
||||
| `github.com/drand/tlock` | v1.2.0 | `TimeLock`, `TimeUnlock`, `CiphertextToBytes`, `BytesToCiphertext` | no se usa `tlock.New`, `Encrypt` ni `Decrypt` |
|
||||
| `github.com/drand/drand/v2` | v2.1.7 | `crypto.Scheme`, `VerifyBeacon`, `chain.Info` | arrastra gRPC y otros; `govulncheck` ya confirmó que no son alcanzables |
|
||||
| `github.com/drand/kyber` y `kyber-bls12381` | transitivas | pairing BLS12-381 | sobre `kilic/bls12-381`, archivado; vigilar y documentar en SECURITY.md |
|
||||
| `github.com/fxamacker/cbor/v2` | v2.9.4 | CBOR determinista | `CoreDetEncOptions` para codificar; límites en decodificación |
|
||||
|
||||
Sin otras dependencias en la librería. Tests con la biblioteca estándar. CLI con `flag`. HTTP con `net/http`.
|
||||
|
||||
### 4.1 Política de versiones
|
||||
|
||||
`go.mod` pinneado, `go.sum` verificado, `go mod verify` en CI, `govulncheck` en cada PR, Renovate (compatible con Gitea) solo para parches, con revisión manual de cualquier cambio en `age`, `tlock`, `drand` o `kyber`.
|
||||
|
||||
### 4.2 Canonicidad CBOR
|
||||
|
||||
Codificar con Core Deterministic Encoding. Al decodificar, además de los límites de `fxamacker` (sin longitudes indefinidas, sin tags, sin claves duplicadas, profundidad y tamaños acotados), se **reencodifica y se compara byte a byte** con la entrada. Cualquier diferencia es `ErrNonCanonicalCBOR`. Es el mismo principio que `dk1_` y no depende de que una librería prometa rechazar todas las formas no canónicas.
|
||||
|
||||
### 4.3 Bech32
|
||||
|
||||
`age` solo acepta identities y recipients X25519 en Bech32 por su API pública, y su paquete Bech32 es `internal`. El `.dkk` guarda 32 bytes crudos por spec. Se necesita un codificador Bech32 de unas cien líneas para pasar de bytes a `AGE-SECRET-KEY-1...` y de `age1...` a bytes. Es codificación, no criptografía. Opciones: copiar el paquete interno de `age` con su licencia BSD y atribución, o reimplementar BIP-173 con vectores de prueba. Propuesta: copiar, para no divergir.
|
||||
|
||||
---
|
||||
|
||||
## 5. Qué se copia del prototipo
|
||||
|
||||
| Origen | Destino | Cambios |
|
||||
|---|---|---|
|
||||
| `pkg/datekeys/key.go`: `RoundForTime`, `TimeForRound`, `ParseDateKey`, `FromRound` | `datekey/` | quitar campos de API y el alias `EncryptionKey`; el struct de respuesta HTTP se queda en el servidor |
|
||||
| `pkg/datekeys/key_test.go`, `FuzzRecipient` | `datekey/` | renombrar, ampliar con vectores de la sección 65 |
|
||||
| `internal/quicknet/trust.go` | `profile/quicknet.go` | añadir CBOR canónico y hash; conservar la autocomprobación de chain hash |
|
||||
| `internal/drand/client.go` y tests | `provider/drand/` | carrera entre relays, respuestas malformadas, cancelación; adaptar a `ReleaseSource` |
|
||||
| `pkg/datekeys/crypto.go` | no se copia | sustituido por `agewrap` sobre el núcleo exportado de `tlock`; se conserva la doble verificación BLS |
|
||||
| `tests/integration/quicknet_test.go` | `provider/drand/`, build tag `integration` | prueba en vivo contra Quicknet |
|
||||
| `cmd/datekeys` | reescrito | misma ergonomía, nueva API |
|
||||
| `internal/api`, `web-client` | no se tocan | consumirán la librería más adelante |
|
||||
|
||||
---
|
||||
|
||||
## 6. Estilo de código
|
||||
|
||||
- Identificadores, comentarios de código, mensajes de error y commits en **inglés**. Documentación de usuario en inglés con versión en español. Motivo: la revisión externa y la comunidad de `age` y drand.
|
||||
- `gofmt`, `goimports`, `go vet`, `staticcheck`, `golangci-lint` con configuración corta: `errcheck`, `govet`, `staticcheck`, `ineffassign`, `unparam`, `gosec` en modo aviso.
|
||||
- Errores: un sentinel por entrada del catálogo de la sección 69, envueltos con `%w` y contexto; nunca se sustituye un error por otro más cómodo. Los tests de mutación comprueban `errors.Is`.
|
||||
- Parsers: límites antes de reservar memoria, `io.LimitReader`, ningún `panic` alcanzable desde la entrada. Todo parser tiene un objetivo de fuzzing.
|
||||
- Sin estado global mutable. El registro de perfiles se pasa explícitamente; el registro por defecto contiene solo Quicknet.
|
||||
- Comentarios de paquete y de función citan la sección del spec que implementan, por ejemplo `// Spec §26`.
|
||||
- Secretos: no se registran en logs, no aparecen en `String()`, se borran de buffers propios al terminar, sin prometer más de lo que Go garantiza.
|
||||
- Salida atómica en la CLI: fichero temporal en el mismo directorio y renombrado al final; nunca sobrescribir destinos existentes.
|
||||
- Versionado semántico. `v0.x` hasta que el spec sea v1.0. Sin promesa de estabilidad de API antes de `v1.0.0`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Estrategia de tests
|
||||
|
||||
1. **Unitarios** por paquete, tabla-driven, biblioteca estándar.
|
||||
2. **Vectores de spec**, generados por el código y guardados como golden files versionados: ronda (frontera, un segundo antes y después, fracción de segundo tras frontera, 2030-01-01, cercanos al genesis), `dk1_` (objeto, JSON canónico, base64url, cadena), `profile_hash` de Quicknet.
|
||||
3. **Fixtures `.dkc` y `.dkk`** (secciones 67 y 68): generados una vez con `internal/testkit` sobre una **ronda ya publicada**, con la firma BLS embebida en el fixture. Los tests verifican la firma contra la clave pinneada y descifran **sin red**. Se comparan todos los valores intermedios: cabecera, prelude, binding, control, `I_PAYLOAD`, plaintext y el error esperado en cada etapa.
|
||||
4. **Corpus de mutaciones** (sección 64): las veinte mutaciones como funciones sobre un fixture válido; cada una afirma su sentinel error concreto.
|
||||
5. **Escritor de cabeceras age malformadas** en `testkit`: añade stanzas o cambia tipos manteniendo el MAC válido, porque en el test se conoce la file key. Demuestra que la comprobación estructural rechaza lo que el MAC aceptaría. Es la prueba directa de la amenaza del creador. Implementación: según la especificación C2SP, el MAC de cabecera es HMAC-SHA-256 con clave HKDF-SHA-256 de la file key e info `header`, sobre el texto de la cabecera; `testkit` lo recalcula con `golang.org/x/crypto/hkdf`, ya presente en el grafo por `age`. Para abrir ficheros de test con una file key conocida se usa `age.NewInjectedFileKeyIdentity`, también API pública.
|
||||
6. **Propiedades:** `Encode(Decode(x)) == x` para todo fixture; toda reordenación de claves o ensanchamiento de enteros se rechaza como no canónico; `Parse(Compact(d)) == d`.
|
||||
7. **Fuzzing** nativo de Go: prelude, PUBLIC_HEADER, CONTROL_CBOR, `.dkk`, `dk1_`, cabecera age vía sonda. Corpus sembrado con fixtures y mutaciones. Presupuesto corto en cada PR, largo en nightly.
|
||||
8. **Integración en vivo** con build tag: cifrar hacia ahora más treinta segundos, esperar, abrir por relays reales. Nightly, no en cada PR.
|
||||
9. **Interoperabilidad con herramientas ajenas:** extraer SEALED_CONTROL de un fixture `time_only` y abrirlo con la CLI `tle` oficial; extraer PAYLOAD_AGE y abrirlo con la CLI `age` usando `I_PAYLOAD`. Si ambas pasan, la afirmación "son ficheros age estándar" queda demostrada por terceros.
|
||||
10. **Cobertura:** mínimo 90 % en `codec`, `capsule`, `accesskey`, `datekey`, `agewrap`; `-race` siempre.
|
||||
11. **CDDL:** `spec/datekeys.cddl` valida los fixtures en un job opcional de CI con la herramienta `cddl`; si no se quiere otra toolchain, se mantiene como documento normativo y se comprueba a mano en cada cambio de schema.
|
||||
|
||||
---
|
||||
|
||||
## 8. CI, releases y cadena de suministro
|
||||
|
||||
- Gitea Actions, con la misma sintaxis que GitHub Actions: `test` en Linux y Windows con la versión estable de Go y la anterior, macOS cuando haya runner; `lint`; `vuln` con `govulncheck`; `fuzz-short`; `integration` nightly por cron; `sbom` con `cyclonedx-gomod`.
|
||||
- Releases con `goreleaser`, que publica en Gitea: `-trimpath`, `CGO_ENABLED=0`, checksums publicados, firma con `cosign` con clave propia, ya que la firma sin clave depende de un proveedor OIDC que Gitea no ofrece.
|
||||
- `SECURITY.md` con canal de reporte, versiones soportadas y la dependencia de `kilic/bls12-381` declarada como riesgo conocido y vigilado.
|
||||
- `TRADEMARKS.md`: "DateKeys" reservado; "implements the DateKey protocol" permitido.
|
||||
|
||||
---
|
||||
|
||||
## 9. Hitos y criterios de aceptación
|
||||
|
||||
**M0. Cimientos.** Esqueleto, `go.mod` pinneado, CI con lint y `govulncheck`, `codec` con canonicidad por reencodificación y límites, `spec/datekeys.cddl` inicial, `profile` con Quicknet, su CBOR y su hash como primer vector oficial.
|
||||
Hecho cuando: el hash del perfil está congelado en un golden file y CI está verde en tres sistemas.
|
||||
|
||||
**M1. Objetos.** `datekey` copiado y ampliado; framing DKC1 y DKK1; codecs de PUBLIC_HEADER, CONTROL_CBOR y `.dkk`; `header_binding`; `extension`; catálogo de errores; fuzzing de todos los parsers.
|
||||
Hecho cuando: los vectores de ronda y `dk1_` están generados y versionados y el fuzzing lleva una noche sin fallos.
|
||||
|
||||
**M2. `time_only`.** `agewrap` con recipient e identity tlock estrictos, sonda de inspección, PAYLOAD_AGE, `Encrypt`, `Inspect`, `Open` con los 18 pasos; `provider/drand` copiado.
|
||||
Hecho cuando: un fixture `time_only` sobre ronda pasada se abre offline, la prueba con `tle` y `age` externas pasa, y las mutaciones aplicables fallan con su error exacto.
|
||||
|
||||
**M3. `time_and_key`.** INNER_ACCESS_AGE con varios recipients, `.dkk` portable de un solo uso, `Identities` y `AccessKey` en `Open`.
|
||||
Hecho cuando: fixtures `time_and_key` con uno y varios recipients se abren offline, la reutilización de `I_ACCESS` está prohibida en `Encrypt` y probada, y las veinte mutaciones están cubiertas.
|
||||
|
||||
**M4. Conformidad.** Suite de fixtures oficial, golden files, `docs/traceability.md` completo, integración nightly, SBOM y release firmada `v0.1.0`.
|
||||
Hecho cuando: cada sección normativa de la 14 a la 69 apunta a al menos un test, y `v0.1.0` se construye de forma reproducible.
|
||||
|
||||
**M5. CLI.** `datekeys encrypt`, `decrypt`, `inspect`, `datekey resolve`, `profile hash`. `inspect` ejecuta solo los pasos 1 a 8 y nunca pide un release ni usa secretos.
|
||||
Hecho cuando: la CLI abre los fixtures y reproduce la ergonomía del prototipo con salida atómica.
|
||||
|
||||
Fuera de estos hitos: SDK TypeScript, migración del cliente web, servidor de Release API sobre la librería, segundo perfil `evmnet`.
|
||||
|
||||
---
|
||||
|
||||
## 10. Trazabilidad
|
||||
|
||||
`docs/traceability.md` mantiene una tabla sección → paquete → función → test. Se actualiza en el mismo PR que cualquier cambio de código normativo. Es el documento que se entrega al revisor externo junto con el spec, los fixtures y el corpus de mutaciones.
|
||||
|
||||
---
|
||||
|
||||
## 11. Decisiones a revisar
|
||||
|
||||
1. Decidida: ruta `g.activething.com/go/DateKeys`. Queda abierto si se pasa a una ruta en `datekeys.com` antes de publicar. Nota: el servidor ejecuta Gitea 1.18.3, sin Actions; los workflows de `.gitea/workflows` quedan listos para Gitea 1.21 o superior con un `act_runner`, y hasta entonces `scripts/check.sh` es el control obligatorio antes de cada push.
|
||||
2. Inglés para código, errores y commits.
|
||||
3. Licencia Apache-2.0 para el código, CC-BY-4.0 para el spec.
|
||||
4. Copiar el Bech32 interno de `age` frente a reimplementarlo.
|
||||
5. Depender del módulo `drand/v2` completo o extraer solo la verificación BLS a un paquete propio sobre `kyber-bls12381`. La primera opción es más simple; la segunda reduce el grafo y el ruido de `govulncheck`.
|
||||
6. Incluir en la librería un cliente de la Release API de DateKeys además del de relays drand, o dejarlo para el servidor.
|
||||
7. Validación CDDL en CI con toolchain externa, o solo como documento.
|
||||
8. Si `Open` exige `io.ReadSeeker` o acepta `io.Reader` y degrada el SHOULD.
|
||||
|
||||
---
|
||||
|
||||
## 12. Riesgos conocidos
|
||||
|
||||
- `kilic/bls12-381` archivado bajo `drand` y `tlock`. Mitigación: pinneado, vigilancia, plan de fork o de migración documentado en `SECURITY.md`.
|
||||
- `tlock` sin etiqueta desde agosto de 2024. Mitigación: pinneado por versión; solo se usa su núcleo exportado.
|
||||
- Fixtures sobre rondas pasadas dependen de que la firma embebida sea auténtica. Mitigación: se verifica BLS en cada ejecución contra la clave pinneada.
|
||||
- Desviación spec ↔ código. Mitigación: trazabilidad, política de cambios de la sección 76, y una segunda implementación en TypeScript como prueba de interoperabilidad.
|
||||
@ -1,202 +0,0 @@
|
||||
# Revisión de completitud del protocolo DateKeys v0.8.2
|
||||
|
||||
29 de septiembre de 2026. Revisión interna asistida por IA, no una revisión externa. La hicieron cuatro revisores automáticos con enfoques distintos (criptografía y seguridad, longevidad, casos de uso, madurez del spec), y un verificador contrastó cada hueco con el texto del spec antes de esta síntesis. Los números de línea se refieren a `spec/DateKeys_Protocol_Specification_v0.8.2.md` de `datekeys-go`.
|
||||
|
||||
Lo que ya recoge el borrador v0.9 (rama `v0.9` de `datekeys-go`):
|
||||
- el versionado, con la promesa de compatibilidad (punto 1);
|
||||
- las reglas del escritor (punto 2);
|
||||
- la privacidad, con los 16 huecos fijos y el relleno del contenido (punto 7b).
|
||||
|
||||
El resto queda como trabajo futuro, en §74 del borrador.
|
||||
|
||||
---
|
||||
|
||||
## 1. Veredicto
|
||||
|
||||
No, no lo resuelve todo, y el propio texto tampoco lo pretende. El núcleo criptográfico es sólido para lo que promete: una fecha mínima de apertura que el cliente comprueba por sí mismo y, si se quiere, una llave encima. Antes de la v1.0 no hace falta criptografía nueva. Hace falta decidir qué se congela, reglas para quien escribe cápsulas, un modelo de amenaza del proveedor bien escrito, una revisión externa humana y dos piezas que casi todos los casos reales necesitan: la firma y el archivo de releases.
|
||||
|
||||
## 2. Lo que hace especialmente bien
|
||||
|
||||
1. **No confía en el servidor para nada que el cliente pueda comprobar.** §3 l.54: «Nunca confiar en el servidor cuando la misma propiedad puede verificarse criptográficamente en el cliente». Y lo cumple:
|
||||
- el perfil va pinneado (§13 l.401);
|
||||
- la ronda se calcula en local (§17);
|
||||
- el release se verifica en local;
|
||||
- la ronda se comprueba antes que la firma (§63 paso 10, l.1811-1823).
|
||||
|
||||
Si la empresa desaparece, las cápsulas siguen abriéndose. §49 l.1352 dice que la API es «no una autoridad criptográfica obligatoria». Esa es la diferencia de fondo con SealedFor, donde la fecha la impone el servidor.
|
||||
|
||||
2. **No inventa criptografía.** Son tres ficheros age v1 completos, y OUTER es un fichero tlock estándar. El MAC de cabecera, el STREAM y la detección de truncado vienen de age (§28 l.768-788, §30 l.863-878). §37 l.1115: «No se define un KEM propio».
|
||||
|
||||
3. **time_and_key es un AND de verdad y está anidado en el orden correcto.** tlock va por fuera (§33 l.981-1031). Antes de la fecha nadie ve los stanzas de los recipients. Si drand cae, la capa X25519 sigue protegiendo frente a terceros.
|
||||
|
||||
4. **Las vinculaciones están bien pensadas.**
|
||||
- Hay tres file keys independientes (§28 l.788, §62 l.1735).
|
||||
- I_PAYLOAD ata el payload al control (§30.1).
|
||||
- header_binding hashea los bytes exactos del PRELUDE y de PUBLIC_HEADER, sin volver a serializarlos (§26).
|
||||
- Cada intercambio posible tiene su test de mutación (§64).
|
||||
- El MAC de age compromete la file key, así que el creador no puede enseñar plaintexts distintos a recipients distintos (§63 pasos 11 y 13).
|
||||
- La cardinalidad de stanzas hace visible un escrow en OUTER y en PAYLOAD (§32 l.967, §29 l.849). En INNER, en cambio, un recipient extra es legítimo (§39).
|
||||
|
||||
5. **La canonicidad llega a todas las capas:**
|
||||
- CBOR que se vuelve a codificar y se compara (§58);
|
||||
- dk1_ canónico (§19);
|
||||
- una sola codificación por punto BLS12-381 (§12.2);
|
||||
- el orden de GT en H2, con su vector (l.1918-1926).
|
||||
|
||||
Esto reduce mucho las divergencias entre implementaciones, aunque no las elimina (puntos 2 y 5 de la sección 4). Varias de estas reglas salieron de fallos reales encontrados por la segunda implementación (§76).
|
||||
|
||||
6. **El modelo de confianza es honesto y normativo.** §36.1 l.1093: «`time_only` **NO proporciona autenticidad del creador**». §55.1 l.1503 prohíbe con un MUST NOT presentar una sección como prueba de lo que no prueba. Además, todo lo que se puede comprobar en local se comprueba antes de cualquier petición de red (§27 l.758). La Release API pide por ronda, no por cápsula (§45).
|
||||
|
||||
## 3. Lo que deja fuera a propósito, y por qué es razonable
|
||||
|
||||
1. **Que Quicknet exista siempre y que su histórico se conserve (§5 l.97-98).** Nadie puede garantizar una red ajena. Esto excluye de paso cualquier vía de respaldo: OUTER lleva «exactamente un stanza, de tipo tlock» (§32 l.967). Es coherente, porque es lo que da sentido a la defensa estructural contra el escrow de §27 l.760. Lo que falta es escribir el coste (punto 3 de la sección 4).
|
||||
|
||||
2. **Resistencia post-cuántica del timelock (§5 l.99, §7.7, §53).** Hoy no hay ningún beacon de umbral post-cuántico en producción. Un matiz: la exclusión solo cubre el timelock. El texto fija además X25519 en la capa de acceso (§33 l.1002), y eso ninguna exclusión lo obliga (punto 1).
|
||||
|
||||
3. **Revocar copias, anonimato absoluto, dispositivo comprometido y control del plaintext tras abrir (§5 l.100-105, §7.8).** Es inherente a cualquier cifrado de ficheros. El anonimato está bien como no-objetivo, pero falta decir qué queda siempre visible (punto 7).
|
||||
|
||||
4. **Autoría legal y fecha probatoria de creación (§5 l.102-103, §36.1, §55.1).** Un timelock dice cuándo se puede abrir, no quién selló ni cuándo. Está bien fuera del núcleo.
|
||||
- La autoría depende de una extensión de firma que aún no existe (punto 8).
|
||||
- La fecha de creación se puede cubrir con un sello de tiempo (RFC 3161 u OpenTimestamps) sobre capsule_digest, en un fichero aparte.
|
||||
|
||||
5. **Almacenamiento, descubrimiento, distribución y entrega de la .dkk (§6 l.109-120).** Es una buena separación de capas. Por la misma razón, los umbrales N-de-M y el AND de varias llaves no deben ir al núcleo: se construyen anidando cápsulas o en la capa de credencial. Si algún día se hace Shamir para herencia, conviene revisar antes las patentes de SafeTech/Inheriti.
|
||||
|
||||
## 4. Lo que abordaría antes de la v1.0, por orden de importancia
|
||||
|
||||
### 1. Versionado: qué se congela y cómo evoluciona (núcleo)
|
||||
|
||||
- **Problema:** no está decidido si un lector v1.0 abrirá las cápsulas v0.8.2, y los lectores v1 verán los objetos futuros como corruptos.
|
||||
- **Por qué importa:**
|
||||
- La fase 3 va a sellar cápsulas con fecha a años vista.
|
||||
- §74 l.2306-2308 sigue dando por provisional el «schema CBOR final byte-a-byte» de los tres objetos.
|
||||
- La v0.8.2 ya invalidó objetos v0.8.1 sin cambiar de versión (§76 l.2390).
|
||||
- Hacia delante, un `access_policy` 2 da `ERR_NON_CANONICAL_CBOR` (§69.1 l.2116).
|
||||
- §70 l.2139 habla de «major versions», un concepto que el formato no tiene.
|
||||
- **Propuesta:**
|
||||
- Congelar el formato en bytes de la v0.8.2: la v1.0 solo añade reglas que ningún objeto válido incumpla. Los fixtures y el corpus de mutaciones pasan a ser una suite permanente.
|
||||
- Añadir a §70: «toda implementación posterior MUST abrir todo objeto DKC1/DKK1 versión 1 válido».
|
||||
- Regla de evolución: una política, un tipo de acceso o un proveedor nuevos implican schema N+1 o prefijo `dk2_`. Así un lector v1 responde `ERR_UNSUPPORTED_VERSION`.
|
||||
- Decidir ahora si entra el access_type `mlkem768x25519`. Lo trae age v1.3.2, que ya usan las dos implementaciones. En INNER, los stanzas serían todos X25519 o todos híbridos, nunca mezclados. Después de congelar, añadirlo exige versión nueva.
|
||||
- Sustituir §74/§75 por una tabla de estado (elemento, estado, evidencia, criterio de salida).
|
||||
- Quitar «prevista» de l.8.
|
||||
|
||||
### 2. El lado del escritor (núcleo)
|
||||
|
||||
- **Problema:** la especificación dice con precisión cómo leer, pero no cómo escribir, y un escritor defectuoso produce cápsulas que solo fallan en la fecha.
|
||||
- **Por qué importa:** en un timelock, el peor fallo es una cápsula que parece sana durante veinte años. El caso 5 de §76 (l.2387) ya produjo una que se sellaba y no se abría. Hoy:
|
||||
- El orden de §61 l.1674-1686 es circular. El PRELUDE se hashea en el paso 7, pero SEALED_CONTROL_LEN no existe hasta el paso 9. Go lo resuelve con un sellado borrador y una suposición que no está escrita.
|
||||
- Ningún escritor descifra lo que escribe.
|
||||
- Los límites «No son normativos» (§74 l.2315). Un escritor puede sellar 1.025 recipients y la referencia rechaza la cápsula tras la apertura.
|
||||
- I_PAYLOAD no tiene ningún MUST de frescura ni de no reutilización; solo lo tiene I_ACCESS (§38 l.1132). Si se reutiliza, al madurar la cápsula A se puede abrir la B años antes.
|
||||
- Nada exige que la ronda objetivo siga sin publicarse. Con el reloj atrasado se crea una cápsula que cualquiera puede abrir ya (App/docs/PLAN_fase3_escritura.md l.479).
|
||||
- **Propuesta:**
|
||||
- Una sección normativa de escritura con el orden correcto y la regla de predicción de longitud.
|
||||
- Los límites como MUST para lectores y escritores.
|
||||
- «I_PAYLOAD MUST generarse con un CSPRNG para cada cápsula y MUST NOT reutilizarse ni derivarse de otro secreto». Pasar §28 l.788 a MUST NOT.
|
||||
- MUST rechazar un instante que no sea futuro. SHOULD tomar now = max(reloj local, round_time del último release verificado); un relay que mienta no puede empeorarlo.
|
||||
- SHOULD autoverificarse antes de emitir: abrir INNER con cada identidad generada, abrir la cabecera de PAYLOAD con I_PAYLOAD y hacer una prueba tlock con respuesta conocida sobre una ronda ya publicada.
|
||||
- SHOULD borrar I_PAYLOAD, FK_* e I_ACCESS tras sellar.
|
||||
- Publicar vectores de escritura por capa.
|
||||
|
||||
### 3. El riesgo del proveedor, bien escrito (núcleo en el texto y los estados de perfil; servicio para el re-sellado)
|
||||
|
||||
- **Problema:** el mayor riesgo real ocupa una sola frase: «Si deja de cumplirse el supuesto de seguridad del provider, puede fallar la confidencialidad temporal» (§7.6 l.176).
|
||||
- **Por qué importa:**
|
||||
- Si t miembros de la League of Entropy se ponen de acuerdo, o se les obliga, o se filtra la clave de grupo, se abren todas las cápsulas pendientes a la vez. Nadie lo nota y no tiene vuelta atrás.
|
||||
- El resharing conserva la clave de grupo, así que la exposición crece con el horizonte.
|
||||
- Si la red se para antes de la ronda, la cápsula se pierde. drand programó el borrado de la clave de fastnet para el 6 de noviembre de 2024, y §12.1 l.338 todavía admite su scheme.
|
||||
- El único aviso obligatorio de horizonte largo (§53 l.1406-1412) habla solo del riesgo post-cuántico, y lo describe como harvest-now-decrypt-later. Para time_only el riesgo cuántico es otro: abrir antes de tiempo, porque la clave maestra de drand se puede sacar de la clave pública pinneada.
|
||||
- **Propuesta:**
|
||||
- Reescribir §7.6: el supuesto t-de-n con fuente y fecha, qué pasa si se rompe y qué pasa si la red se para.
|
||||
- Escribir en §5 y §73: «un .dkc V1 no contiene ninguna vía alternativa de apertura; si un perfil deja de operar antes de la ronda, solo puede re-sellar quien conserve el plaintext».
|
||||
- Unir §50 y §53 en un único aviso normativo. El riesgo cuántico va en dos casos: time_only (apertura anticipada) y time_and_key con recipients age1 públicos (descifrado posterior).
|
||||
- Recomendar time_and_key para cápsulas largas o valiosas, porque sigue protegiendo frente a terceros tras una brecha de drand. Hoy §36.1 l.1099 lo presenta solo como «una barrera adicional de acceso».
|
||||
- Dar valores al campo «estado» de §71:
|
||||
- `active`;
|
||||
- `deprecated`: no se sella nada nuevo;
|
||||
- `halted_after_round N`: se rechazan las rondas posteriores y el lector las declara irrecuperables;
|
||||
- `compromised_since T`: se muestra un aviso.
|
||||
- Añadir: «un perfil publicado nunca se retira de los lectores conformes».
|
||||
- En el servicio: ofrecer re-sellado cuando se anuncie un apagado.
|
||||
|
||||
### 4. Revisión externa y evidencia de independencia (proceso)
|
||||
|
||||
- **Problema:** el punto 10 de §75 sigue abierto, y la evidencia actual es más débil de lo que sugiere el texto.
|
||||
- **Por qué importa:**
|
||||
- §76 l.2429 atribuye correcciones a «la revisión formal e independiente». En realidad fue una revisión hecha por agentes en estas mismas sesiones.
|
||||
- TypeScript sigue los textos de error de Go «byte a byte» (HANDOFF l.44), y §16 l.498 exige que los vectores salgan de la referencia.
|
||||
- Las 407.196 entradas diferenciales prueban que TypeScript coincide con Go. No prueban que el texto baste para implementar.
|
||||
- Los objetivos de §4 son informales.
|
||||
- **Propuesta:**
|
||||
- Una revisión criptográfica humana, con revisor, alcance y fecha anotados en §76.
|
||||
- Etiquetar la revisión actual como interna y asistida por IA.
|
||||
- Un anexo breve de modelo de seguridad: los adversarios de §7, qué se garantiza antes de la ronda, la confidencialidad del factor de acceso, la integridad frente a quien no tiene la llave y qué vale frente al creador.
|
||||
- Una tercera implementación hecha por otra persona sin ver el código existente, solo con la spec y testdata, anotando cada pregunta que surja.
|
||||
- Antes de la revisión: la versión inglesa y una regla de precedencia. El texto manda sobre el CDDL y testdata/README, y hay que declarar el idioma normativo.
|
||||
|
||||
### 5. Raíz de confianza y algoritmos de verificación (núcleo)
|
||||
|
||||
- **Problema:** para verificar un release hay que leer código ajeno, y la raíz de confianza tiene una puerta sin especificar.
|
||||
- **Por qué importa:**
|
||||
- El paso 10 verifica «según el scheme de drand» (l.1818).
|
||||
- H3 y H4 son las de kyber (l.1843-1847).
|
||||
- El mensaje de ronda y el DST no aparecen en el texto.
|
||||
- §12.1 admite tres schemes, y las implementaciones ya divergen: Go acepta los tres y TypeScript solo el de Quicknet.
|
||||
- §12 l.323 permite «una representación firmada equivalente» sin decir quién firma ni si puede introducir un profile_id nuevo. Quien tenga esa clave podría publicar una red falsa coherente.
|
||||
- **Propuesta:**
|
||||
- Limitar §12.1 a `bls-unchained-g1-rfc9380`.
|
||||
- Junto a «Serialización de GT en H2», escribir msg = SHA-256(uint64_be(round)), el DST, Q_ID = hash_to_G1(msg, DST), H3 (con su iteración y su máscara) y H4.
|
||||
- Poner valores intermedios en tlock_ibe.json y citar ePrint 2023/189 y RFC 9380.
|
||||
- Para V1, pinnear Quicknet en el código y quitar «o una representación firmada equivalente». La alternativa es especificar esa clave, su distribución y su rotación.
|
||||
|
||||
### 6. Recuperación a largo plazo (núcleo para el objeto, el paso 9.c y el anexo; servicio para el archivo)
|
||||
|
||||
- **Problema:** el release, que es la pieza que abre la cápsula, no tiene formato definido, y el reloj local puede vetar uno válido.
|
||||
- **Por qué importa:**
|
||||
- §1 dice que el documento define la Release API y la Release Cache, pero §47 l.1313 deja `release_material` sin definir. Nadie puede guardar el release de su cápsula junto al .dkc en una forma que cualquier lector acepte.
|
||||
- §49 solo nombra relays de drand, y fastnet demostró que los relays pueden dejar de servir una red entera.
|
||||
- El paso 9.c (l.1800-1801) da `ERR_RELEASE_UNAVAILABLE` si el reloj va atrasado, aunque se aporte un release que el paso 10 verificaría. Así, un dato local que no se puede verificar pesa más que uno que sí: lo contrario de §3.
|
||||
- La recuperación sin software DateKeys no está escrita. tle solo se prueba con time_only, y fuera de scripts/check.sh.
|
||||
- **Propuesta:**
|
||||
- Un objeto release canónico (profile_id, round, signature) en CBOR determinista y en JSON, con semántica HTTP: 200; 404 antes de round_time; 400 si el perfil es desconocido. La alternativa es pasar §45-§47 a un anexo informativo.
|
||||
- Añadir «release desde fichero o archivo» como fuente en §49.
|
||||
- SHOULD guardar el release verificado junto a la cápsula cuando madure.
|
||||
- Aplicar 9.c solo a las fuentes de red y caché.
|
||||
- Un anexo informativo «Recuperación sin software DateKeys»: offsets del PRELUDE, tle, la clave 3 de CONTROL_CBOR como AGE-SECRET-KEY-1 y age. Añadir un caso time_and_key a las pruebas locales.
|
||||
- En el servicio: un archivo completo de Quicknet en instantáneas que cualquiera pueda replicar. Son 48 bytes por ronda, unos 505 MB al año, y cada release se verifica por sí solo.
|
||||
|
||||
### 7. Escribir lo que no garantiza (núcleo para el texto; extensión para el relleno; servicio para redondear horas)
|
||||
|
||||
- **Problema:** cuatro límites reales no están en el texto, y un texto de producto podría prometer lo contrario.
|
||||
- **Por qué importa:**
|
||||
- **(a)** Nadie puede comprobar antes de la fecha que una cápsula abrirá. La ronda del stanza tlock es una etiqueta que nada liga al cifrado IBE. Un creador puede cifrar a otra ronda, o romper el stanza de un solo recipient, y se descubre tras el release (pasos 11 y 13). En pujas o predicciones, eso le permite no revelar. §4 l.77 («El SDK debe detectar condiciones, perfiles y releases manipulados.») puede leerse como una promesa mayor.
|
||||
- **(b)** Hay metadatos siempre visibles: la ronda, la política, el número de recipients y el tamaño exacto del plaintext. En los fixtures, SEALED_CONTROL_LEN vale 446, 646 y 842 bytes (98 por recipient). Un PAYLOAD_AGE de 78.216 bytes corresponde exactamente a 78.000 bytes de plaintext.
|
||||
- **(c)** En el cliente web, el mismo origen del que §3 desconfía sirve el código que maneja el plaintext, I_PAYLOAD e I_ACCESS. §7.1 no lo menciona.
|
||||
- **(d)** La caducidad, las condiciones por evento, varias fechas en un objeto y cancelar antes de la fecha no están en §5.
|
||||
- **Propuesta:**
|
||||
- Añadir a §5: «verificar antes de la madurez, frente a un creador malicioso, que la cápsula se abrirá o que su ronda es la declarada». Añadir también los no-objetivos de (d).
|
||||
- Añadir a §4 un objetivo de unicidad de apertura, con sus supuestos.
|
||||
- Una sección «Consideraciones de privacidad» con la lista de metadatos visibles y un relleno opcional:
|
||||
- recipients X25519 ficticios hasta un tamaño fijo, lo que no cambia el protocolo;
|
||||
- una extensión crítica de CONTROL_CBOR con la longitud real del payload.
|
||||
- Añadir «servir un cliente web malicioso» a §7.1. En §59: hashes de build publicados, URLs versionadas inmutables, SRI y una build offline.
|
||||
- En producto, redondear la hora de apertura al minuto.
|
||||
|
||||
### 8. Firma del creador y .dkk protegida (extensión)
|
||||
|
||||
- **Problema:** las dos piezas que casi todos los casos reales necesitan (herencia, pujas, embargos) son extensiones sin definir.
|
||||
- **Por qué importa:**
|
||||
- §27 l.764 dice: «Solo una extensión de firma puede aportar autenticidad del creador». No hay ninguna registrada.
|
||||
- Colocar la firma tiene trampas. En PUBLIC_HEADER es circular con header_binding. Debe cubrir SHA-256(PAYLOAD_AGE), porque §55.1 l.1509 admite que, tras abrir, se puede cifrar otro payload. Eso obliga a escribir en dos pasadas. Y en time_only cualquiera puede quitarla y volver a sellar.
|
||||
- La .dkk son «32 bytes crudos» (§38 l.1130) y «no lleva MAC ni firma» (§55.1 l.1510). Un bit cambiado se descubre en la fecha. Sin un formato estándar protegido con contraseña, cada aplicación inventará el suyo.
|
||||
- **Propuesta:**
|
||||
- Registrar `datekeys.signature` v1 en CONTROL_CBOR antes de la revisión externa, para que se revise junto al resto.
|
||||
- La firma lleva una etiqueta de dominio y cubre header_binding, SHA-256(PAYLOAD_AGE) y los bytes exactos de las demás extensiones.
|
||||
- Su ausencia no es un error. El firmante esperado se conoce por fuera de la cápsula.
|
||||
- Mutaciones nuevas: firma quitada, payload recifrado y control trasplantado.
|
||||
- Para la .dkk: una forma opcional dentro de un fichero age con scrypt, y un valor de comprobación de 4 bytes, SHA-256("datekeys-dkk-check" || I_ACCESS), en verification_metadata.
|
||||
- De paso, reservar el prefijo `datekeys.` en extension_id y exigir a terceros nombres de dominio invertidos (§31 l.919).
|
||||
|
||||
## 5. Frase final
|
||||
|
||||
Es un núcleo bueno y honesto, pero no un sistema completo: antes de la v1.0, congela lo que después no podrás cambiar y escribe lo que no garantiza, antes de que lo prometa un texto comercial.
|
||||
Loading…
Reference in new issue