# 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 guarda ni se envía. --- ## 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:
- `verifyRelease` con el orden y los textos de `provider.Verify`;
- `ReleaseSource`, con el contrato de la sección 5, y `suppliedRelease`;
- los casos de `TestVerifyRejects` de Go y los 7 del corpus de mutaciones que fallan en el paso 10;
- las firmas publicadas de las rondas 1000, 1001, 2000 y 1004, esta última obtenida restando p a la codificación x + p del corpus;
- 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 | | 6 | Spec, mutaciones en Go, `testdata:sync`, reproducción en TypeScript (sección 7) | texto aprobado; vectores congelados en ambos repositorios; commits en Gitea | | 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 | | 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 | 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. - **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. - Reutilización de la implementación `age` de tlock-js: descartada; `age-encryption` es la implementación oficial.