|
|
# DateKeys, formato de cápsula 3 (spec v0.10)
|
|
|
|
|
|
*Diseño, todavía sin implementar. 30 de septiembre de 2026. Contrastado con el spec v0.9 (`datekeys-go` 7e2d83c, tag `spec-v0.9`) y con `datekeys-ts` da26862. Incorpora la revisión adversarial interna, la revisión de seguridad de Fable ([revision_fable.md](revision_fable.md)), las decisiones del autor sobre ella y una revisión adversarial de esta versión. Es la única fuente del diseño: sustituye a `formato3_revision_intro.md` y a `formato3_para_revision.md`, que repetían su texto.*
|
|
|
|
|
|
## Cómo leer este documento
|
|
|
|
|
|
- **La parte 0** da el contexto a quien llega de fuera: el protocolo v0.9 en una página, un glosario, las decisiones del autor y el modelo de amenazas.
|
|
|
- **Las partes 1 a 3 son las tres entregas.** El formato se congela en la primera. Las entregas 2 y 3 no cambian ningún byte de la trama ni del head: definen qué va dentro del área `security` y cómo se comprueba. Un lector de una entrega anterior muestra «no soportado» para lo nuevo y abre igual.
|
|
|
- **Entrega 1:** contenedor, varios ficheros y rutas, con `security` presente y vacío.
|
|
|
- **Entrega 2:** firma de autor Ed25519 y claves de autor.
|
|
|
- **Entrega 3:** sello de tiempo, cuando exista el documento «Servicio de sellado DateKeys v1».
|
|
|
|
|
|
Cada entrega es una versión del spec: la 1 es la v0.10, y la 2 y la 3 abrirán las versiones siguientes, porque una versión cerrada no cambia.
|
|
|
- **Los anexos** recogen las revisiones: dónde se resuelve cada hallazgo de Fable, las 24 objeciones de la revisión interna y los hallazgos de la revisión adversarial de esta versión.
|
|
|
|
|
|
Los § son del spec v0.9; los apartados de este documento se citan como «apartado 3». Las referencias a código (`framing.go:56`, `tempfile.ts:198`…) apuntan a las implementaciones actuales y no hace falta abrirlas.
|
|
|
|
|
|
### Para quien revisa
|
|
|
|
|
|
**Qué es.** Una ampliación del protocolo DateKeys: el formato de cápsula 3. Hasta la v0.9, una cápsula guarda un solo fichero, sin nombre ni fecha. El formato 3 guarda varios ficheros y carpetas, con sus nombres, tamaños, hashes y fechas de modificación. Añade además una firma de autor opcional y un sello de tiempo opcional, emitido por un servicio de DateKeys.
|
|
|
|
|
|
**Estado.** Es un diseño que ya pasó tres revisiones. El spec v0.9 existe, y sus dos implementaciones, la de referencia en Go y la de TypeScript para el navegador, están hechas y probadas byte a byte entre sí. Nada del formato 3 está programado.
|
|
|
|
|
|
**Qué se pide.** Encontrar vulnerabilidades en el diseño, en particular:
|
|
|
- **Criptografía:** los enunciados que se firman y se sellan, los compromisos, el perfil Ed25519 estricto y los trasplantes entre cápsulas o formatos.
|
|
|
- **Privacidad:** qué se filtra antes de la fecha de apertura, qué ve el servicio de sellado y qué queda vinculado después.
|
|
|
- **El lector ante una cápsula maliciosa:** el análisis del CBOR, las reglas de rutas, la extracción a un ZIP o a una carpeta, y el consumo de memoria y de CPU.
|
|
|
- **El servicio de sellado:** la retrodatación, el robo de claves, el abuso y la verificación a décadas vista.
|
|
|
- **Ambigüedades** que dos implementaciones independientes podrían resolver de forma distinta.
|
|
|
|
|
|
**Cómo informar.** Para cada hallazgo:
|
|
|
- la gravedad: bloqueante, mayor o menor;
|
|
|
- un escenario concreto: qué entrada produce qué resultado;
|
|
|
- el apartado afectado;
|
|
|
- una propuesta de arreglo.
|
|
|
|
|
|
**Ya revisado.** Los anexos A, B y C recogen las revisiones anteriores y cómo se resolvió cada punto. No hace falta repetirlos, pero sí comprobar que los arreglos son correctos.
|
|
|
|
|
|
**Preguntas concretas:**
|
|
|
1. ¿Atan `control_commit` y `head_digest` la firma y el sello a esta cápsula y solo a ella? ¿Se pueden trasplantar entre cápsulas, entre formatos o a un paquete de verificación? Entregas 2 y 3.
|
|
|
2. ¿Es correcto el perfil «Ed25519 estricto» (apartado 11)? ¿Da el mismo resultado en Go 1.26 y en noble 2.4? Entrega 2.
|
|
|
3. ¿Filtran algo antes de la fecha la trama del contenido, el área de tamaño fijo o la cota de ficheros que se deduce de P? Entrega 1.
|
|
|
4. ¿Cierran R1 a R10 el escape de carpeta, las colisiones de nombres y los trucos de Windows, de macOS y de Linux (best-fit, alias 8.3, nombres reservados, caracteres ignorables, casefold) al extraer a un ZIP y a una carpeta? Entrega 1.
|
|
|
5. ¿Garantizan la precedencia del paso 17 y la entrega atómica que nada se presenta antes de autenticarlo todo, y que Go y TypeScript dan el mismo código de error? Entrega 1.
|
|
|
6. ¿Puede un escritor malicioso agotar memoria o CPU? Por ejemplo, con un head de 16 MiB, 65 535 ficheros, rutas largas o el plegado Unicode. Entrega 1.
|
|
|
7. ¿Pueden inducir a error los textos de los veredictos? ¿Pueden suplantarlos el comentario o el autor declarado? Entregas 1 a 3.
|
|
|
8. ¿Son seguras las claves de autor, con su formato `dkauthor1…`, el scrypt de logN 16 y la regla para guardar una clave (apartado 14)? Entrega 2.
|
|
|
9. ¿Resiste el servicio de sellado la retrodatación y el robo de claves, con la prueba dentro de la cápsula, la raíz offline, las claves anuales, el anclaje en OpenTimestamps y un `max_anchor_delay` de 26 h? ¿Qué aprende, qué vincula después de la apertura, y basta con no guardar registros de acceso? Entrega 3.
|
|
|
10. ¿Hay alguna ambigüedad de codificación que dos implementaciones resolverían de forma distinta?
|
|
|
|
|
|
---
|
|
|
|
|
|
## Parte 0. Contexto
|
|
|
|
|
|
### 0.1 El protocolo base (v0.9) en una página
|
|
|
|
|
|
|
|
|
**Objetivo.** Cifrar hoy un fichero que nadie pueda abrir antes de una fecha, sin confiar en ningún servidor. La clave depende de una firma BLS que la red drand publica en esa fecha. drand Quicknet publica una ronda cada 3 segundos, y el cifrado usa tlock, un IBE sobre BLS12-381. Nadie tiene esa firma antes de la ronda, tampoco quien cifra.
|
|
|
|
|
|
**Estructura de una cápsula (`.dkc`)**, en este orden:
|
|
|
1. **PRELUDE**, 16 bytes en claro: la marca `DKC1`, `VERSION` (el formato de la cápsula), `PUBLIC_HEADER_LEN` y `SEALED_CONTROL_LEN`.
|
|
|
2. **PUBLIC_HEADER**, CBOR en claro: `capsule_id` (16 bytes aleatorios), la DateKey (el perfil del proveedor y la ronda), la política de acceso y extensiones públicas.
|
|
|
3. **SEALED_CONTROL = OUTER_TIME_AGE**, un fichero `age` con un único stanza `tlock` para la ronda. Dentro va una de dos cosas:
|
|
|
- con la política `time_only`, directamente `CONTROL_CBOR`;
|
|
|
- con `time_and_key`, `INNER_ACCESS_AGE`: un fichero `age` con exactamente 16 stanzas X25519, que contiene `CONTROL_CBOR`. Esos 16 stanzas son las credenciales (destinatarios `age1…` y, opcionalmente, una clave portable `.dkk`) más señuelos, en orden aleatorio.
|
|
|
4. **PAYLOAD_AGE**, un fichero `age` con un stanza X25519 para `R_PAYLOAD`. Su texto en claro es el contenido, L bytes, seguido de ceros hasta P = regla(L). Las reglas son `bloque256` y `reforzado`, que es max(bloque256, Padmé).
|
|
|
|
|
|
**`CONTROL_CBOR`** contiene:
|
|
|
- `header_binding`, el SHA-256 de PRELUDE ‖ PUBLIC_HEADER;
|
|
|
- `payload_identity`, que es `I_PAYLOAD`, la identidad X25519 cuya pública es `R_PAYLOAD`;
|
|
|
- las extensiones de control;
|
|
|
- `payload_length` (L);
|
|
|
- el código de la regla de relleno.
|
|
|
|
|
|
**Apertura** (§63, pasos 1 a 18):
|
|
|
- **Pasos 1 a 8:** estructura, sin red y sin secretos.
|
|
|
- **Paso 9:** la firma de la ronda (el release), que se da a mano o se descarga, y en `time_and_key`, la credencial.
|
|
|
- **Paso 10:** la firma BLS se verifica contra la clave pública del perfil, que va fijada en el software.
|
|
|
- **Paso 11:** se abre `OUTER_TIME_AGE`.
|
|
|
- **Paso 12:** estructura de la política.
|
|
|
- **Paso 13:** la credencial abre exactamente uno de los 16 stanzas de `INNER_ACCESS_AGE`.
|
|
|
- **Paso 14:** se decodifica `CONTROL_CBOR`.
|
|
|
- **Paso 15:** se comprueba `header_binding`.
|
|
|
- **Paso 16:** `I_PAYLOAD` y P.
|
|
|
- **Paso 17:** se abre `PAYLOAD_AGE` y se comprueba el relleno.
|
|
|
- **Paso 18:** se entrega.
|
|
|
|
|
|
Nada se presenta antes de autenticar el contenido entero (§56), y solo se entregan L bytes, nunca el relleno.
|
|
|
|
|
|
**Qué se ve antes de la fecha** (formato 2): la DateKey (la fecha), la política, `capsule_id` y el tamaño relleno. No se ve la longitud exacta ni el número de credenciales.
|
|
|
|
|
|
**La `.dkk`**, la clave portable, contiene:
|
|
|
- la identidad X25519 de acceso;
|
|
|
- `capsule_id`;
|
|
|
- el SHA-256 del `.dkc` exacto (`capsule_digest`).
|
|
|
|
|
|
No lleva MAC ni firma.
|
|
|
|
|
|
**Lo que v0.9 no garantiza** (§5, §36.1, §55.1):
|
|
|
- autoría: cualquiera puede cifrar hacia una fecha futura con la clave pública del perfil;
|
|
|
- fecha probatoria de creación;
|
|
|
- integridad frente a quien ya abrió la cápsula: puede cifrar otro `PAYLOAD_AGE` para `R_PAYLOAD` y volver a sellar el control;
|
|
|
- resistencia poscuántica.
|
|
|
|
|
|
**Implementaciones.** La de Go es la de referencia, con la CLI `datekeys`. La de TypeScript funciona en el navegador sin red: la página `/create` cifra y `/inspect` abre. Las dos son iguales byte a byte, con fixtures, vectores y mutaciones compartidas. Evitan dependencias nuevas: tienen su propio códec CBOR determinista. En criptografía usan noble (curves, hashes, ciphers) y `age-encryption` en TypeScript, y la biblioteca estándar, `filippo.io/age` y drand/tlock en Go.
|
|
|
|
|
|
|
|
|
### 0.2 Glosario
|
|
|
|
|
|
- **L y P.** L es la longitud real del texto en claro de `PAYLOAD_AGE`, que va en la clave 6 de `CONTROL_CBOR`. P = regla(L) es la longitud con relleno, la que se cifra. En el formato 3, L es la longitud de `BODY` (apartado 2).
|
|
|
- **L_MAX** = 2⁵³ − 2⁴⁶, el mayor L admitido (§29.1). Es múltiplo de 2⁴⁶, el mayor redondeo de Padmé por debajo de 2⁵³, así que P nunca pasa de L_MAX y todo cabe en un entero exacto de JavaScript.
|
|
|
- **Capas de validación (§69.1).** Cada objeto CBOR se valida en cuatro capas, en este orden, y se informa el código de la primera que falla:
|
|
|
1. **trama:** las longitudes y los límites del contenedor;
|
|
|
2. **tipo y versión:** el type tag de la clave 0 y la versión de la clave 1;
|
|
|
3. **codificación y schema:** el perfil CBOR canónico (§58) y el CDDL, con tipos, tamaños y rangos, que dan `ERR_NON_CANONICAL_CBOR`;
|
|
|
4. **campos con código propio,** en orden ascendente de clave.
|
|
|
- **Trama del contenido:** los 12 primeros bytes de `BODY`, con `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN` (apartado 2).
|
|
|
- **Área:** el espacio reservado para `SECURITY_CBOR`, completado con ceros. Su tamaño lo fija la versión del spec del escritor, no lo que lleva, así que P no revela si se eligió firmar o sellar. La única excepción es la fuga del área de 512 (apartado 0.4).
|
|
|
- **Sal del head:** 32 bytes de un CSPRNG en la clave 2 del head, nuevos en cada cápsula. Hacen de `head_digest` un compromiso que oculta. Quien ve `head_digest` sin el head, como un dispositivo de firma externo que recibe `AUTHOR_MESSAGE` en claro antes de la fecha, no puede confirmar una conjetura sobre nombres, tamaños o hashes previsibles. Los SHA-256 de cada fichero van sin sal: quien recibe el head los ve y puede compararlos con `sha256sum`.
|
|
|
- **round_time:** el instante en que drand publica la ronda de la cápsula. Antes, nadie puede abrirla.
|
|
|
- **Release:** la firma BLS de esa ronda, que abre la capa tlock.
|
|
|
- **Veredicto:** el resultado de comprobar la firma o el sello. Es siempre un texto fijo y nunca impide abrir (apartado 4).
|
|
|
- **Lista blanca de R4:** ZWNJ (U+200C), ZWJ (U+200D), VS15 (U+FE0E) y VS16 (U+FE0F), los únicos puntos `Default_Ignorable_Code_Point` que admite una ruta (apartado 3).
|
|
|
|
|
|
### 0.3 Decisiones del autor
|
|
|
|
|
|
**Decisiones iniciales.** Son el punto de partida del diseño, no algo a revisar. Un hallazgo puede, eso sí, mostrar que alguna es insegura.
|
|
|
|
|
|
1. **Formato 3.** El texto en claro de `PAYLOAD_AGE`, cifrado y bajo el relleno, pasa a ser tres bloques seguidos: `security | head | content`. Nada va en `PUBLIC_HEADER`, en `CONTROL_CBOR` ni en el nombre del fichero. Los formatos 1 y 2 siguen abriéndose, y un lector v0.9 rechaza el formato 3 en el paso 2.
|
|
|
2. **`head`** lleva:
|
|
|
- su propia versión;
|
|
|
- una sal aleatoria;
|
|
|
- un comentario opcional por cápsula;
|
|
|
- un autor declarado opcional, que es texto y nunca prueba nada;
|
|
|
- una entrada por fichero, con la ruta relativa (con carpetas), `size`, `start`, `end`, el SHA-256 y la fecha de modificación. `start`, `end` y `size` deben cuadrar entre sí.
|
|
|
|
|
|
No lleva tipo de contenido, hash global, permisos ni fecha de creación.
|
|
|
3. **La fecha de modificación** se toma sola al cargar cada fichero. Se incluye por defecto, con una casilla para quitarla, y es informativa.
|
|
|
4. **`security`** lleva una firma de autor Ed25519 y un sello de tiempo, los dos opcionales, sobre `head` y el contexto de la cápsula. Si van los dos, primero se firma y el sello cubre también la firma. Ninguno de los dos hace falta para abrir.
|
|
|
5. **El servicio de sellado** es de DateKeys y firma con claves fijadas en el protocolo. Lleva un registro de solo añadir, anclado periódicamente en OpenTimestamps. Sellar es opcional y pide consentimiento. La decisión inicial lo ponía en el mismo origen que la página; la segunda decisión sobre la revisión lo cambia.
|
|
|
6. **Varios ficheros:** la página entrega un ZIP sin compresión que construye con código propio, y la CLI escribe el árbol en un directorio nuevo, nunca fuera de él.
|
|
|
|
|
|
**Decisiones sobre la revisión de Fable,** del 30-09:
|
|
|
|
|
|
1. **Tres entregas,** con el formato congelado desde la primera y `security` presente pero vacío:
|
|
|
- la 1, el contenedor, varios ficheros y las rutas;
|
|
|
- la 2, la firma y las claves de autor;
|
|
|
- la 3, el sello, cuando exista el documento «Servicio de sellado DateKeys v1».
|
|
|
|
|
|
Las entregas 2 y 3 no cambian el formato: un lector anterior muestra «no soportado» y abre igual.
|
|
|
2. **Origen del servicio (hallazgo 1).** El autor no tenía preferencia y se tomó la recomendación: un subdominio aparte, `seal.datekeys.com`, que solo sirve la API y nunca HTML, con `nosniff` y `CSP: sandbox` de todos modos. La CSP de `/create` añade ese origen a `connect-src`, solo en esa página. Es de la entrega 3 y se puede revisar entonces.
|
|
|
3. **Vida del sello (hallazgo 3).** La prueba va dentro de la cápsula. El token lleva la prueba de inclusión y el checkpoint firmado, y una raíz offline, fijada desde el primer perfil, certifica las claves anuales. Así el sello se verifica sin red para siempre, y la auditoría del anclaje queda como comprobación online opcional. Esto agranda el área (apartado 2).
|
|
|
4. **Abuso (hallazgo 4).** Prueba de trabajo SHA-256 en cada petición, de un segundo en el navegador, sin guardar estado sobre quién pide.
|
|
|
|
|
|
### 0.4 Modelo de amenazas
|
|
|
|
|
|
| Adversario | Qué tiene | Qué no debe conseguir |
|
|
|
|---|---|---|
|
|
|
| A1, observador antes de la fecha | El `.dkc`; quizá, el tráfico hacia el servicio de sellado | Nombres, número de ficheros, tamaños, hashes, fechas, autor o comentario, ni si hay firma o sello, salvo lo que delatan el tráfico y un área de 512 (fugas aceptadas) |
|
|
|
| A2, quien abre | Todo el contenido, después de la fecha | Hacer pasar una cápsula manipulada por la original, con firma o sello que lo respalden |
|
|
|
| A3, escritor malicioso | La libertad de fabricar cualquier cápsula | Que el lector escriba fuera de su carpeta o sobrescriba ficheros, agotarlo, o engañar con veredictos falsos, nombres que se confunden o un ZIP malicioso |
|
|
|
| A4, servicio de sellado deshonesto o comprometido | Sus claves anuales y su registro | Retrodatar sin que se note, o vincular las peticiones con quien crea la cápsula antes de la fecha |
|
|
|
| A5, red | El camino entre la página y el servicio | Alterar o suplantar el sello sin que se detecte |
|
|
|
| A6, tercero que verifica | El paquete de verificación, sin `I_PAYLOAD` | Obtener `I_PAYLOAD`, o un contenido que no está en el paquete |
|
|
|
|
|
|
**Fugas aceptadas:**
|
|
|
- P acota el número de ficheros y la longitud de los nombres y del comentario (apartado 6; objeción 20 del anexo B).
|
|
|
- Un área de 512 bytes, la de los escritores anteriores a la entrega 3, no admite un sello. Cuando P es pequeña, delata esa área y, con ella, que la cápsula no lleva sello (apartado 2).
|
|
|
- El registro del servicio publica la t de cada sello. Con poco tráfico, quien sepa cuándo se creó una cápsula puede sospechar que lleva sello.
|
|
|
- Con sello, el tráfico hacia `seal.datekeys.com` delata que la cápsula lleva sello y, más o menos, cuándo se creó (hallazgo 8; apartado 20).
|
|
|
- Tras la apertura, el sello vincula la cápsula con su petición al servicio (objeción 9).
|
|
|
- La auditoría del anclaje acota la retrodatación a `max_anchor_delay`, 26 h, y no a minutos, hasta que haya testigos (objeción 2).
|
|
|
|
|
|
---
|
|
|
|
|
|
## Parte 1. Entrega 1: contenedor, varios ficheros y rutas
|
|
|
|
|
|
La entrega 1 congela el formato. Define la trama, el contenedor `security` con sus huecos para la firma y el sello, el head, las rutas, los veredictos y su presentación, el lector y el escritor. Sus escritores dejan `security` vacío, y sus lectores muestran como no soportada cualquier firma o sello que encuentren.
|
|
|
|
|
|
### 1. Resumen
|
|
|
|
|
|
1. `VERSION` = 3; `CONTROL_CBOR` en schema 3 con las claves de la 2 (103 bytes: `SEALED_CONTROL_LEN` no cambia). No cambian `PUBLIC_HEADER`, la `.dkk`, los 16 huecos, L_MAX ni los códigos de relleno.
|
|
|
2. Texto en claro de `PAYLOAD_AGE` = `BODY || 0x00^(P − L)`, con `BODY = AREA_LEN || SECURITY_LEN || HEAD_LEN || SECURITY_CBOR || ceros del área || HEAD_CBOR || CONTENT` y L = |BODY| (clave 6).
|
|
|
3. HEAD: versión propia, sal, comentario, autor declarado y, por fichero, ruta, size, start, end, SHA-256 y mtime.
|
|
|
4. SECURITY: siempre presente, en un área cuyo tamaño declara la trama y no depende del contenido.
|
|
|
5. SECURITY nunca decide la apertura (objetivo 6 de §4, recuperación independiente): solo da veredictos de texto fijo.
|
|
|
6. Rutas estrictas, con tablas propias de Unicode 18.0.0 y las proyecciones best-fit de Windows.
|
|
|
7. En el paso 17 prevalece cualquier fallo de `age` (`ERR_INTEGRITY`); si no lo hay, el orden del texto en claro. Código nuevo `ERR_HEAD_INVALID`.
|
|
|
8. Entrega del contenido: un fichero, un ZIP propio o un árbol en un directorio nuevo. Un lector v0.9 rechaza el formato 3 en el paso 2.
|
|
|
|
|
|
**Desacuerdos resueltos durante el diseño:**
|
|
|
|
|
|
- **Paso 17: primero STREAM.** `filippo.io/age` v1.3.2 (`stream.go:117-156`) entrega un chunk completo como no final antes de `ErrUnexpectedEOF`; `age-encryption` 0.3.1 solo lo prueba como final en `flush`. Si decidiera la posición, Go daría `ERR_HEAD_INVALID` y TypeScript `ERR_INTEGRITY`.
|
|
|
- **SECURITY sin código** (decisión inicial 4); **HEAD con versión propia** (decisión inicial 2), con el precedente del paso 14; **área de tamaño fijo**.
|
|
|
- **Tablas Unicode propias:** Go 1.26.8 trae Unicode 15.0.0 y Node 24.9 el 16.0; un repertorio fijo rechazaría ¿, ° y emoji.
|
|
|
|
|
|
### 2. Estructura del contenido
|
|
|
|
|
|
```text
|
|
|
BODY = AREA_LEN (uint32 BE) || SECURITY_LEN (uint32 BE) || HEAD_LEN (uint32 BE)
|
|
|
|| SECURITY_CBOR || 0x00^(AREA_LEN − SECURITY_LEN)
|
|
|
|| HEAD_CBOR || CONTENT
|
|
|
|
|
|
AREA_LEN = 512·k, con 1 ≤ k ≤ 128 1 ≤ SECURITY_LEN ≤ AREA_LEN
|
|
|
1 ≤ HEAD_LEN ≤ 2^24 12 + AREA_LEN + HEAD_LEN ≤ L ≤ L_MAX
|
|
|
C = L − 12 − AREA_LEN − HEAD_LEN; CONTENT = los ficheros concatenados en el orden del head
|
|
|
```
|
|
|
|
|
|
**El área (hallazgo 5).**
|
|
|
- Su tamaño va en la trama y depende solo de la versión del spec con la que se escribe: lo usa todo escritor de esa versión, sepa sellar o no, y nunca depende de la invocación, de `-seal` ni de que se pase un `sealer`. Los escritores de las entregas 1 y 2 MUST escribir `AREA_LEN` = 512, y todos MUST escribir siempre `SECURITY_CBOR`, aunque vaya vacío. Antes, el área medía 512·⌈`SECURITY_LEN`/512⌉, y un sello de más de 512 bytes habría cambiado L y delatado su presencia a través de P.
|
|
|
- El lector acepta cualquier `AREA_LEN` que cumpla la trama y no lo compara con la versión del spec. Así lee sin cambios el área mayor de un escritor posterior.
|
|
|
- La entrega 3 fijará un área mayor para su versión del spec, porque el sello lleva su prueba dentro. Sus escritores la usarán siempre, sellen o no, así que P seguirá sin revelar si se eligió sellar. P sí puede revelar la versión del escritor, igual que `VERSION` revela el formato, y con un área de 512, que no hay sello (apartado 0.4).
|
|
|
- El mapa `security` sigue en su versión 1 en las tres entregas. Un lector de la entrega 2 sigue comprobando la firma en una cápsula de la entrega 3.
|
|
|
|
|
|
Cada CBOR ocupa exactamente sus bytes (`unmarshal` actual). Los buffers solo crecen con bytes autenticados, nunca según L, P o los size (§57).
|
|
|
|
|
|
```text
|
|
|
security = { 0: "datekeys-security", 1: 1,
|
|
|
? 2: bstr .size (1..65536), ; author-signature, codificado aparte
|
|
|
? 3: bstr .size (1..65536) } ; seal, codificado aparte
|
|
|
author-signature = { 0: alg (uint 1..2^32−1; 1 = Ed25519 estricto, entrega 2),
|
|
|
1: bstr, ; clave pública
|
|
|
2: bstr } ; firma
|
|
|
seal = { 0: seal_type (uint 1..2^32−1; 1 = DateKeys, entrega 3;
|
|
|
2 = RFC 3161 y 3 = OpenTimestamps, reservados),
|
|
|
1: bstr } ; token
|
|
|
head = { 0: "datekeys-head", 1: 1, 2: bstr .size 32, ; sal nueva de un CSPRNG
|
|
|
? 3: tstr .size (1..16384), ; comentario
|
|
|
? 4: tstr .size (1..256), ; autor declarado
|
|
|
? 5: [1*65535 file], ; orden ascendente estricto de bytes de ruta
|
|
|
? 6: [1*64 extension], ? 7: [1*64 extension] } ; críticas, no críticas
|
|
|
file = { 0: tstr .size (1..1024), ; ruta
|
|
|
1: uint, 2: uint, 3: uint, ; size, start, end exclusivo; ≤ L_MAX
|
|
|
4: bstr .size 32, ; SHA-256
|
|
|
? 5: uint .le 253402300799 } ; mtime, segundos UTC, informativa
|
|
|
```
|
|
|
|
|
|
Las claves 2 y 3 de `security` son cadenas de bytes con su propio CBOR dentro: el mapa `author-signature` y el mapa `seal`. La capa 3 de `security` solo comprueba el mapa exterior. El lector decodifica cada uno aparte en el paso 17.6, con el perfil de §58 y su CDDL, y un fallo solo cambia el veredicto de ese elemento (apartado 4). Así, los lectores de las tres entregas validan igual el mapa exterior, una firma o un sello mal formados nunca arrastran al otro, y un `alg` o un `seal_type` futuros pueden tener el tamaño que quepa en el área. Los tamaños propios de cada `alg`, 32 y 64 bytes en Ed25519, también se comprueban al evaluar el veredicto.
|
|
|
|
|
|
**Tamaños.**
|
|
|
- SECURITY mide 22 bytes vacía y 130 con una firma Ed25519. El sello de la entrega 3 no cabe en 512 bytes.
|
|
|
- HEAD mínimo: 53 bytes; cada entrada, al menos 45.
|
|
|
- «nota.txt» de 1000 bytes, con mtime: HEAD 117, L = 1641, P = 1792.
|
|
|
- Cápsula mínima que acepta el lector, sin ficheros ni comentario: L = 577, P = 768.
|
|
|
|
|
|
**Maquetación:** start₀ = 0, startᵢ = endᵢ₋₁ y endᵢ − startᵢ = sizeᵢ, comprobado con restas: en JavaScript nunca se suman valores sin comprobar. end_último = C, o C = 0 sin ficheros.
|
|
|
|
|
|
**Texto.** «Espacio» es U+0020; MUST NOT usarse `trim`, `\s`, `TrimSpace` ni `unicode.IsSpace`.
|
|
|
|
|
|
- **Comentario:** prohibidos U+0000–0008, U+000B–001F (el escritor convierte CR LF y CR en LF), U+007F–009F, U+202A–202E, U+2066–2069, U+2028, U+2029, U+FEFF y los no-caracteres.
|
|
|
- **Autor:** lo mismo, más TAB, LF, U+061C, U+200E y U+200F, sin U+0020 en los extremos.
|
|
|
- **Invisibles:** ni el comentario ni el autor llevan puntos `Default_Ignorable_Code_Point` salvo la lista blanca de R4, y esa solo donde R4b la admite. Quedan fuera las etiquetas y los selectores de variante sueltos, con los que se esconde texto que un modelo de IA sí lee.
|
|
|
|
|
|
Ningún texto ni ruta puede llevar ESC o C1 a un terminal.
|
|
|
|
|
|
**mtime (hallazgo 9).** El escritor la toma al cargar cada fichero: ⌊lastModified/1000⌋ en la página y `ModTime().Unix()` en la CLI. La omite si no la conoce o si cae fuera de 0..253402300799, por ejemplo una fecha anterior a 1970, y nunca la recorta. Una mtime futura se guarda tal cual: es informativa.
|
|
|
|
|
|
**Capas de HEAD** (§69.1):
|
|
|
|
|
|
- **Capa 2:** un type tag ajeno da `ERR_NON_CANONICAL_CBOR`; una versión ≠ 1, `ERR_UNSUPPORTED_VERSION`.
|
|
|
- **Capa 3,** sobre todo el objeto (`ERR_NON_CANONICAL_CBOR`): §58, el CDDL (con R1), el orden de rutas (R8) y las extensiones.
|
|
|
- **Capa 4,** por clave:
|
|
|
- claves 3 y 4 → `ERR_HEAD_INVALID`: las reglas de texto del comentario y del autor;
|
|
|
- clave 5 → `ERR_HEAD_INVALID`: cada entrada con R2 a R6c, R10 y su maquetación, y tras el bucle, R7 y R9;
|
|
|
- clave 6 → `ERR_EXTENSION_CRITICAL_UNKNOWN`, luego `ERR_EXTENSION_DATA_INVALID`.
|
|
|
|
|
|
En el formato 3 el head va siempre en su versión 1. Una versión nueva del head exige un formato nuevo (§22), que un lector v0.10 rechaza en el paso 2, sin red; la capa 2 queda como defensa, igual que la versión de `CONTROL_CBOR` en el paso 14.
|
|
|
|
|
|
**Capas de SECURITY:** las mismas capas 2 y 3 sobre el mapa exterior, sin código. Un fallo da el veredicto X (apartado 4).
|
|
|
|
|
|
### 3. Rutas y nombres
|
|
|
|
|
|
R1 y R8 son capa 3 sobre todo el array; las demás, capa 4. Así, [«b/..», «a»] da `ERR_NON_CANONICAL_CBOR`.
|
|
|
|
|
|
- **R1.** De 1 a 1024 bytes UTF-8.
|
|
|
- **R2.** Separada por '/', de 1 a 32 segmentos no vacíos: sin rutas absolutas ni '//'.
|
|
|
- **R3.** Cada segmento mide de 1 a 255 bytes y no es «.» ni «..». Tampoco lo es, ni queda vacío, al quitarle los puntos de la lista blanca de R4: HFS+ ignora ZWNJ y ZWJ al comparar nombres, así que allí un «.» o un «..» seguido de ZWJ se vería y se compararía como ellos; VS15 y VS16 se quitan también, por prudencia. Además, el NFD del segmento, con las tablas de R7, mide como mucho 255 unidades UTF-16, el límite de HFS+: 127 veces «ΐ» (U+0390) son 254 bytes, pero 381 unidades tras NFD.
|
|
|
- **R4.** Puntos de código prohibidos (hallazgo 2):
|
|
|
- C0 y U+007F–009F;
|
|
|
- `" * : < > ? \ |`;
|
|
|
- U+2028 y U+2029;
|
|
|
- todo punto con la propiedad `Default_Ignorable_Code_Point` de Unicode 18.0.0, salvo la lista blanca: ZWNJ, ZWJ, VS15 y VS16, que hacen falta en emoji y en varias escrituras. La propiedad cubre los controles bidi (U+061C, U+200E, U+200F, U+202A–202E, U+2066–2069), U+00AD, U+034F, U+200B, U+2060–206F, U+3164, U+FEFF, los demás selectores de variante y las etiquetas U+E0000–E007F, que permiten nombres visualmente idénticos;
|
|
|
- los no-caracteres y todo Cn (sin asignar) de Unicode 18.0.0, que una versión futura podría plegar o volver ignorable;
|
|
|
- U+F000–U+F0FF, del área de uso privado: Cygwin, WSL y el SMB de macOS representan con ellos los caracteres prohibidos en Windows, así que «informe», U+F03A y «anexo» se verían allí como «informe:anexo».
|
|
|
- **R4b.** Los puntos de la lista blanca solo van donde su uso es conforme: VS15 y VS16 justo detrás de un carácter para el que `emoji-variation-sequences.txt` define esa secuencia, y ZWJ y ZWNJ nunca al principio ni al final del segmento, ni dos seguidos. Así ningún nombre lleva secuencias invisibles, que según Unicode 18 sirven para esconder datos y atacar aplicaciones de IA.
|
|
|
- **R5.** Ningún segmento empieza por U+0020 ni termina en U+0020 o '.'.
|
|
|
- **R6.** El segmento hasta el primer '.', sin U+0020 finales y sin distinguir mayúsculas ASCII, no puede ser CON, PRN, AUX, NUL, CONIN$, CONOUT$, COM0–9, LPT0–9, COM¹²³ ni LPT¹²³.
|
|
|
- **R6b.** Alias 8.3 (hallazgo 9). Se rechaza un segmento que, contando puntos de código, cumple las tres condiciones:
|
|
|
- tiene como mucho un '.';
|
|
|
- la parte anterior al '.', o todo el segmento si no lo tiene, mide de 1 a 8 y cumple `^[^.]*~[0-9]{1,6}$`, es decir, acaba en '~' y de 1 a 6 dígitos ASCII;
|
|
|
- la parte posterior mide de 0 a 3.
|
|
|
|
|
|
La expresión regular no usa búsquedas anticipadas, que el paquete `regexp` de Go no admite; en JavaScript va con la bandera `u`. «~1» a secas también se rechaza. En NTFS «ABCDEF~1» resuelve a «ABCDEFGHIJ» (comprobado), así que `Root.MkdirAll` y el Explorador fusionarían dos carpetas distintas.
|
|
|
- **R6c.** Best-fit. Para cada `bestfit*.txt` de WindowsBestFit, fijadas por digest, se sustituye cada punto no ASCII que la tabla lleva a ASCII por ese byte. Se rechaza si alguna proyección contiene '/', '\', ':' o U+0000, o incumple R3, R5, R6 o R6b. Cubre ∖, ∶, ¥ (cp932), ₩ (cp949), ´ (cp1253), las formas de ancho completo, «CON.txt» y U+3000 en los extremos, que bestfit1252 lleva a U+0020. Los demás caracteres de R4 que dé una proyección no se rechazan: con la API ANSI de Windows hacen fallar la creación del fichero, pero no cambian su destino. Así se aceptan «¿», que bestfit1250 lleva a '?', y «§», «♥» y las flechas, que bestfit874 lleva a controles C0 (anexo D).
|
|
|
- **R7.** Árbol plegado.
|
|
|
- Nodos: las rutas y sus prefijos.
|
|
|
- Clave de cada segmento: NFD(pliegue(NFD(s′))), con s′ el segmento sin los puntos de la lista blanca de R4 y el pliegue C+F de CaseFolding 18.0.0 más ı → i (NTFS). Se quitan antes de normalizar porque los cuatro son starters (ccc 0): entre dos marcas combinantes, cambiarían su orden canónico. Los directorios con casefold de ext4 y f2fs también ignoraban los puntos ignorables al comparar, hasta 2025; no está comprobado aquí.
|
|
|
- Se rechazan dos hermanos con la misma clave (A.txt/a.txt, NFC/NFD, «Fotos/a» y «fotos/b», Straße/STRASSE, Kelvin/k, «ab» con y sin ZWNJ) y una ruta que es a la vez fichero y carpeta («A» y «a/b»).
|
|
|
- **R8.** Orden estrictamente ascendente de los bytes UTF-8 (§54). TypeScript MUST comparar los bytes, no los strings: `<` ordena por unidades UTF-16, y U+FF5E y U+1F600 salen al revés que en Go (hallazgo 6); `localeCompare` ordena por colación. Ninguno de los dos sirve.
|
|
|
- **R9.** Como mucho 65535 carpetas implícitas.
|
|
|
- **R10.** Ningún primer segmento cuya clave de R7 empiece por «.datekeys-». La CLI escribe el árbol en `DIR/.datekeys-*` antes de moverlo, y una entrada con ese nombre chocaría con él (hallazgo 10).
|
|
|
|
|
|
**Tablas.** Un generador en `datekeys-go` las produce desde UCD 18.0.0 (UnicodeData, DerivedCoreProperties, CaseFolding, emoji-variation-sequences y las descomposiciones) y desde WindowsBestFit, y emite código para los dos repos; sus suites comprueban el digest. MUST NOT usarse `normalize`, `toLowerCase`, las clases `\p{…}` de las expresiones regulares, el paquete `unicode` ni x/text: dependen de la versión de Unicode de cada motor.
|
|
|
|
|
|
**Escritor.**
|
|
|
|
|
|
- Guarda los nombres tal como llegan.
|
|
|
- Rechaza un UTF-16 mal formado y, con un mensaje que nombra el carácter, los puntos de R4 y los posteriores a Unicode 18.0.0.
|
|
|
- No conserva carpetas vacías y deja editar las rutas.
|
|
|
- Excluye por defecto `.DS_Store`, `Thumbs.db`, `desktop.ini`, `._*` y `__MACOSX/`, que se muestran tachados con un conmutador.
|
|
|
|
|
|
**Nombres de salida.**
|
|
|
|
|
|
- El `.dkc` y la `.dkk` siguen llamándose `capsula-<fecha de apertura en UTC>`.
|
|
|
- Un fichero único se descarga con su nombre, pasado por `safeFileName`. En OPFS conserva el nombre fijo `TEMP_FILE` (`tempfile.ts:24`): sin Web Locks, `isStale` busca ese nombre y borraría el directorio de otra pestaña.
|
|
|
- El ZIP se llama `<primer segmento común>.zip` o `<nombre del .dkc>.zip`.
|
|
|
- Las rutas se muestran solo como texto, en un `<bdi dir=auto>`.
|
|
|
- Se avisa de `.lnk`, `.url`, `.library-ms`, `.searchConnector-ms`, `desktop.ini`, `.git`, los ejecutables y un '-' inicial. Los avisos comparan la clave de R7 del segmento y de su extensión, como R10, así que «.GIT», «Informe.LNK» o «.git» con un ZWJ en medio también avisan.
|
|
|
|
|
|
### 4. Veredictos y presentación
|
|
|
|
|
|
**Evaluación (hallazgo 9).** El lector evalúa `security` en el paso 17.6, y el resultado solo se muestra tras el paso 18. La firma y el sello se evalúan por separado:
|
|
|
|
|
|
| Situación | Firma | Sello |
|
|
|
|---|---|---|
|
|
|
| El mapa exterior de `security` falla la capa 2 o la 3, o su versión no es 1 | X | X |
|
|
|
| Sin clave 2 | F0 | no cambia |
|
|
|
| La clave 2 no decodifica o incumple el CDDL de `author-signature` | F1 | no cambia |
|
|
|
| `alg` que el lector no implementa | F1 | no cambia |
|
|
|
| `alg` conocido, con una clave o una firma de otra longitud | F1 | no cambia |
|
|
|
| Firma que no verifica | F2 | no cambia |
|
|
|
| Firma válida | F3 o F4 | no cambia |
|
|
|
| Sin clave 3 | no cambia | S0 |
|
|
|
| El sello no se puede evaluar, según el orden de abajo | no cambia | S1 o S2 |
|
|
|
| Sello que no verifica | no cambia | S3 |
|
|
|
| Sello válido | no cambia | S4 o S5; S6 en su lugar si la clave anual está comprometida |
|
|
|
|
|
|
**Orden del sello:** decide la primera condición que se cumple (anexo C, hallazgo 8).
|
|
|
|
|
|
1. La clave 3 no decodifica o incumple el CDDL de `seal` → S2.
|
|
|
2. `seal_type` desconocido → S1.
|
|
|
3. El token tiene otro type tag o no llega a su versión → S2.
|
|
|
4. Versión del token desconocida → S1.
|
|
|
5. El token incumple su CDDL, que trata el certificado como una cadena de bytes → S2.
|
|
|
6. El certificado tiene otro type tag o no llega a su versión → S2; su versión es desconocida → S1; incumple su CDDL → S2.
|
|
|
7. Raíz desconocida → S1.
|
|
|
8. Falla cualquier comprobación de firma, de rango o de inclusión → S3.
|
|
|
|
|
|
- X es una sola línea, en lugar de la de la firma y la del sello.
|
|
|
- `SIG_PART` (apartado 17) se calcula sobre el contenido exacto de la cadena de bytes de la clave 2, sin su cabecera CBOR, sea cual sea su veredicto. Una firma ilegible, no soportada o inválida no cambia el veredicto del sello, y un sello válido prueba que esos bytes existían en t.
|
|
|
- Solo «guardar esta clave» combina los dos veredictos (apartado 14).
|
|
|
- En la entrega 1 el lector no implementa ningún `alg` ni ningún `seal_type`, así que solo llega a X, F0, F1, S0, S1 y S2.
|
|
|
|
|
|
**Textos** (normativos):
|
|
|
|
|
|
| Veredicto | Texto | Desde |
|
|
|
|---|---|---|
|
|
|
| X | «No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada.» | 1 |
|
|
|
| F0, firma ausente | «Sin firma de autor.» | 1 |
|
|
|
| F1, firma ilegible o no soportada | «No se ha comprobado ninguna firma: trátala como no firmada.» | 1 |
|
|
|
| F2, firma inválida | «La firma no corresponde a este contenido.» | 2 |
|
|
|
| F3, válida con una clave guardada | «Firmado con la clave que guardaste como ‹etiqueta›.» | 2 |
|
|
|
| F4, válida con otra clave | «Firmado con la clave dkauthor1… (completa). No prueba quién la tiene.» | 2 |
|
|
|
| S0, sello ausente | nada sobre la fecha | 1 |
|
|
|
| S1, sello no soportado | «Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada.» | 1 |
|
|
|
| S2, sello ilegible | «El sello de tiempo es ilegible: no prueba nada.» | 1 |
|
|
|
| S3, sello inválido | «El sello no corresponde a este contenido.» | 3 |
|
|
|
| S4, válido, t < round_time | «Según el servicio de sellado de DateKeys, existía el ‹t›, antes de que la cápsula pudiera abrirse.» | 3 |
|
|
|
| S5, válido, t ≥ round_time | «Sellado después de la fecha de apertura: no prueba nada anterior.» | 3 |
|
|
|
| S6, clave del servicio comprometida | «Firma del servicio válida, pero su clave está comprometida: requiere auditoría.» | 3 |
|
|
|
|
|
|
Con un sello válido, una mtime posterior a t se muestra como incoherencia.
|
|
|
|
|
|
**Presentación** (normativa):
|
|
|
|
|
|
- Primero van los veredictos.
|
|
|
- Después, «autor declarado (texto del creador, sin comprobar)» y el comentario, en un recuadro titulado «Comentario del creador (sin comprobar)».
|
|
|
- **En la CLI (hallazgo 7), sea o no su salida un terminal:**
|
|
|
- expande cada TAB del comentario a espacios, hasta la siguiente columna múltiplo de 8;
|
|
|
- parte cada línea del autor, del comentario y de cada ruta que muestre en trozos de ancho visual como mucho W − 3, con W el ancho del terminal, u 80 si la salida no es un terminal, y nunca menos de 20; cada trozo lleva al menos un punto de código;
|
|
|
- cuenta el ancho por lo alto: 1 por carácter ASCII imprimible y 2 por cualquier otro punto de código. El prefijo `│ ` cuenta 3, porque U+2502 tiene anchura ambigua y ocupa 2 columnas en un terminal CJK;
|
|
|
- pone `│ ` delante de cada trozo. Así el terminal nunca parte una línea por su cuenta, y ninguna queda sin prefijo;
|
|
|
- repite los veredictos al final, después del comentario y de las rutas que muestre.
|
|
|
|
|
|
### 5. Escritor y lector
|
|
|
|
|
|
**Escritor**, en dos fases (`prepareFiles` y `write`):
|
|
|
|
|
|
- **Fase A, sin red ni secretos:**
|
|
|
- Rutas y mtime, con la casilla marcada por defecto y la regla del apartado 2.
|
|
|
- La CLI recorre con `Lstat` y acepta solo ficheros regulares. Cada directorio de `-in` aporta su nombre como primer segmento, igual que `webkitRelativePath`.
|
|
|
- Offsets y |HEAD| con los hashes a cero. Se rechaza |HEAD| > 16 MiB o L > L_MAX. El tamaño es exacto (`capsuleLength3`) y se comprueba la cuota.
|
|
|
- Pasada 1: SHA-256 en piezas de 64 KiB.
|
|
|
- **Fase B:**
|
|
|
1. §61 y §62 con `VERSION` 3.
|
|
|
2. La sal y `HEAD_CBOR`.
|
|
|
3. `CONTROL_CBOR` de schema 3.
|
|
|
4. Desde la entrega 2, la firma (apartado 10).
|
|
|
5. Desde la entrega 3, el sello (apartado 19).
|
|
|
6. `SECURITY_CBOR`, vacío en la entrega 1, en el área de su versión del spec (512 en las entregas 1 y 2), y la autodecodificación de `CONTROL_CBOR`, `HEAD_CBOR`, `SECURITY_CBOR` y los mapas de sus claves 2 y 3 (MUST).
|
|
|
7. `INNER_ACCESS_AGE` y `OUTER_TIME_AGE`.
|
|
|
8. La escritura, con una pasada 2 que aborta si un tamaño o un hash difieren.
|
|
|
9. La `.dkk` con `capsule_digest`.
|
|
|
|
|
|
**Lector.**
|
|
|
|
|
|
- El paso 2 acepta los formatos 1 a 3; los pasos 12 y 13 esperan 16 stanzas; en el paso 14, la versión de schema debe coincidir con `VERSION`.
|
|
|
- Paso 17:
|
|
|
- **17.1** `PAYLOAD_AGE`.
|
|
|
- **17.2** Trama y área: los rangos de `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`, que quepan en L y que el área termine en ceros → `ERR_INTEGRITY`.
|
|
|
- **17.3** SECURITY, capas 2 y 3, sin código.
|
|
|
- **17.4** HEAD, capas 2 a 4.
|
|
|
- **17.5** end_último = C → si no, `ERR_INTEGRITY`.
|
|
|
- **17.6** Veredictos (apartado 4).
|
|
|
- **17.7** SHA-256 de cada fichero → `ERR_INTEGRITY`.
|
|
|
- **17.8** Relleno → `ERR_INTEGRITY`.
|
|
|
|
|
|
**Precedencia** (§69.1 y final del paso 17):
|
|
|
|
|
|
- Un fallo de `age` después de su cabecera, o un texto en claro de longitud ≠ P, es `ERR_INTEGRITY` y prevalece.
|
|
|
- Si no hay ninguno, decide el primer fallo de 17.2 a 17.8 y, dentro de él, la primera capa.
|
|
|
- Tras un fallo de bloque, el lector deja de entregar y sigue autenticando hasta EOF.
|
|
|
- Solo MAY detenerse antes con `ERR_INTEGRITY`. Los demás códigos del paso 17 MUST NOT informarse antes de autenticar hasta EOF con P bytes, así que el momento de la detección sigue sin cambiar el código.
|
|
|
|
|
|
Ejemplos:
|
|
|
|
|
|
- «..» y un chunk posterior corrupto → `ERR_INTEGRITY`.
|
|
|
- «..» y un byte de relleno ≠ 0 → `ERR_HEAD_INVALID`.
|
|
|
- Corte exacto tras el chunk que termina el head → `ERR_INTEGRITY` en los dos lenguajes.
|
|
|
|
|
|
**Entrega** (§56). Nada se presenta antes del paso 18. El sumidero tiene `Begin(head)`, `Create(i)`, `Commit` y `Abort`.
|
|
|
|
|
|
`Open` con solo `dst` y `open` sin sumidero terminan tras el paso 2 con un error del llamador sin código normativo (`ErrSinkRequired` con `Code() == ""`; `TypeError`), nunca con `ERR_UNSUPPORTED_VERSION`. Una prueba compartida lo fija con `format3_single`.
|
|
|
|
|
|
- **Página.**
|
|
|
- Un fichero de un solo segmento va al fichero fijo de OPFS; sin ficheros, solo se muestra el comentario.
|
|
|
- En los demás casos, un ZIP propio en ese mismo fichero: entradas almacenadas, bit 11, sin descriptores de datos ni entradas de carpeta.
|
|
|
- Como las entradas van sin comprimir, cada fichero ocupa un tramo continuo del ZIP. La página ofrece la descarga suelta de cualquier fichero de la lista como un corte del ZIP, sin copiarlo, y el ZIP entero para descargarlo todo con sus carpetas.
|
|
|
- Con un único fichero dentro de una carpeta, la descarga principal es el fichero, con su nombre, y el ZIP con la carpeta es la segunda opción (decisión 8 del apartado 9).
|
|
|
- El CRC-32 se parchea con escrituras posicionadas (`TempFileHandle` pasa a `FileSystemWritableFileStream`).
|
|
|
- ZIP64 cuando hay 0xFFFF entradas o más, o algún tamaño u offset ≥ 0xFFFFFFFF.
|
|
|
- Tiempos: hora DOS en UTC, recortada a 1980–2107. El extra 0x000A va siempre y es el que manda; después, 0x5455 solo si 0 ≤ t ≤ 2³¹ − 1. Una entrada sin mtime recibe round_time.
|
|
|
- **CLI, `decrypt -out DIR`:**
|
|
|
1. `os.Mkdir(DIR, 0700)` reclama DIR y falla si ya existe. Un `os.Rename` final no serviría: en POSIX sustituiría un DIR vacío.
|
|
|
2. El árbol se escribe en `DIR/.datekeys-*` con `os.OpenRoot`, `O_EXCL`, permisos 0600 y `Root.Chtimes`. R10 impide que una entrada tenga ese nombre.
|
|
|
3. Tras el paso 18, `Root.Rename` lleva cada entrada al primer nivel de DIR; ante cualquier fallo, `RemoveAll(DIR)`.
|
|
|
4. Una cápsula sin ficheros no crea DIR.
|
|
|
5. Los veredictos, el autor y el comentario van a stdout, con la presentación del apartado 4.
|
|
|
|
|
|
### 6. Qué ve cada uno
|
|
|
|
|
|
- **Quien tiene el `.dkc` antes de la fecha:**
|
|
|
- lo mismo que en el formato 2, más `VERSION` = 3;
|
|
|
- no ve si hay firma; del sello, solo lo que delatan el tráfico y un área de 512 (apartado 0.4);
|
|
|
- P acota el número de ficheros y la longitud de los nombres y del comentario. Con un área de 512 y pocos ficheros, n ≤ (P − 579)/45, es decir, 4 con P = 768.
|
|
|
- **Quien abre:** las rutas, los tamaños, los SHA-256 y las mtimes, el comentario y el autor declarado. Con firma y sello, además, lo de los apartados 12 y 20.
|
|
|
|
|
|
### 7. Compatibilidad y versiones
|
|
|
|
|
|
- Un lector v0.9 rechaza `VERSION` 3 en el paso 2 (`framing.go:56`, `framing.ts:44`). Un lector v0.10 acepta 1, 2 y 3; los formatos 1 y 2 no cambian.
|
|
|
- Cambiar la `VERSION` entre 2 y 3 falla en el paso 14, o antes, en el 9.a. Volver a sellar bajo el formato 2 es una reescritura (§70).
|
|
|
- Los escritores v0.10 MUST escribir el formato 3. El escritor de un solo flujo (`Encrypt`, `encrypt()`) queda tras una opción solo para pruebas (§62.1), para los generadores.
|
|
|
- Las entregas 2 y 3 no cambian la trama, el head ni el CDDL de `security`: añaden algoritmos, tipos de sello y veredictos, y la 3, un área mayor que todo lector v0.10 ya acepta.
|
|
|
- **Pruebas que cambian:**
|
|
|
- spec §64;
|
|
|
- `mutations.json` (`[4,1,"03"]` pasa a `"04"`);
|
|
|
- `framing.test.ts`, `inspect.test.ts`, `prefix.test.ts`, `format.ts:40` y `framing_test.go`;
|
|
|
- las del escritor que esperan el formato 2 (`encrypt.test.ts:116`, `encrypt_test.go`, `format2_test.go`).
|
|
|
- **Siguen igual:** `control.test.ts:125` y `framing_test.go:208`.
|
|
|
|
|
|
### 8. Cambios en el spec, el código y las pruebas
|
|
|
|
|
|
**Spec v0.10:**
|
|
|
|
|
|
- §4, objetivo 8, y §55.2: la privacidad de metadatos cubre el formato 3.
|
|
|
- §6: los nombres, los tamaños y las fechas de los ficheros entran en el protocolo, cifrados.
|
|
|
- §22, §23, §27 y §31: formato 3 y `CONTROL_CBOR` de schema 3.
|
|
|
- §29.2 a §29.6 y §29.10: la trama y el área, el contenedor `security`, el head, las rutas, el texto, y los veredictos y su presentación.
|
|
|
- §54 y §72: el head es un objeto nuevo con extensiones, y el registro lo recoge.
|
|
|
- §55.1: el head y `security` en la tabla de confianza por sección.
|
|
|
- §56 y §57: entrega atómica y límites.
|
|
|
- §62.1: reglas nuevas del escritor (formato 3, área y `security` siempre, rutas, texto, mtime y autodecodificación de `CONTROL_CBOR`, `HEAD_CBOR`, `SECURITY_CBOR` y los mapas de sus claves 2 y 3).
|
|
|
- §63, pasos 2, 12 a 14 y 16 a 18.
|
|
|
- §69 y §69.1: `ERR_HEAD_INVALID`, la precedencia y el Alcance.
|
|
|
- §70: `VERSION` de 1 a 3, y los escritores escriben el formato 3.
|
|
|
- `datekeys.cddl`.
|
|
|
|
|
|
**Código:**
|
|
|
|
|
|
- **Go:** `capsule` (`Format3`, `head`, `security`, `pathkey`, el sumidero) y la CLI (`-in` con directorios, `-comment`, `-author`, `-no-mtime` y `decrypt -out DIR`).
|
|
|
- **TypeScript:** los mismos módulos, más `zip.ts` y `crc32.ts`.
|
|
|
- **El generador de tablas,** en `datekeys-go`, que emite código para los dos repos.
|
|
|
|
|
|
**Pruebas:**
|
|
|
|
|
|
- **Fixtures:** `format3_single`, `_tree`, `_comment_only`, `_bloque256`, `_time_and_key_portable`, `_area_1024` (el lector acepta un área mayor), `_security_v2` (veredicto X), `_signature_unsupported`, con alg 1, una clave de 32 bytes y una firma de 64, aleatorias (F1 en la entrega 1 y F2 desde la 2), y `_seal_unsupported`, con esa firma y un sello de `seal_type` 1 con un token aleatorio (F1 y S1 en la entrega 1; S2 desde la 3).
|
|
|
- **Vectores:**
|
|
|
- `paths.json` y `head_schema.json`: U+00A0 y U+3000 en los extremos, con el veredicto que den las tablas, que para U+3000 es el rechazo por R6c; «¿», «§» y «♥», que se aceptan; best-fit; 8.3 con la expresión regular, «~1» incluido; Cn; [«b/..», «a»]; U+206A–206F, las etiquetas y otros ignorables fuera de la lista blanca; «.» seguido de ZWJ y un segmento hecho solo de ZWJ; 127 veces «ΐ»; U+F03A; «.datekeys-x» en el primer nivel y en otro; el orden de U+FF5E y U+1F600;
|
|
|
- `path_fold.json`, con «ab» con y sin ZWNJ;
|
|
|
- `security.json`: el mapa exterior con la clave 2 que no es una cadena de bytes, con una clave 4 o con un byte de más dentro de `SECURITY_LEN` (X); `alg` 0 o una clave vacía dentro de la clave 2 (F1, con el sello intacto);
|
|
|
- `zip.json`: mtime 1, 2³¹, 2040 y 253402300799, y 65535 entradas.
|
|
|
- **Mutaciones:**
|
|
|
- `VERSION` 4, y de 3 a 2 y de 2 a 3;
|
|
|
- la trama y el área: `AREA_LEN` 0, 511, 513 y 66048; `SECURITY_LEN` 0 y mayor que `AREA_LEN`; `HEAD_LEN` 0 y 2²⁴ + 1; 12 + `AREA_LEN` + `HEAD_LEN` = L + 1; L < 12; un byte ≠ 0 en el área;
|
|
|
- cada regla de rutas y de texto;
|
|
|
- head v2 y end_último ≠ C;
|
|
|
- la precedencia, con el corte exacto.
|
|
|
- **Otras:**
|
|
|
- un HEAD de 16 MiB y otro de 16 MiB + 1;
|
|
|
- diferenciales entre Go y TypeScript;
|
|
|
- `archive/zip` y el extra 0x000A;
|
|
|
- la CLI nunca escribe fuera de DIR;
|
|
|
- un lector que informa `ERR_HEAD_INVALID` antes de EOF no es conforme;
|
|
|
- el escritor omite una mtime negativa.
|
|
|
|
|
|
### 9. Decisiones de la entrega 1
|
|
|
|
|
|
El autor cerró el 30-09 las diez preguntas abiertas, todas con la recomendación:
|
|
|
|
|
|
1. Hay un código nuevo, `ERR_HEAD_INVALID`: separa un escritor defectuoso de un CBOR roto.
|
|
|
2. Los escritores de las entregas 1 y 2 usan `AREA_LEN` = 512: cuesta 490 bytes por cápsula y oculta la firma. Se descartó fijar ya un área de 4096.
|
|
|
3. El área la fija la versión del spec, y el mapa `security` sigue en su versión 1 cuando llegue el sello. Así un lector de la entrega 2 sigue comprobando la firma en una cápsula de la entrega 3.
|
|
|
4. Las tablas Unicode son propias, generadas de UCD 18.0.0. El diseño decía 17.0.0, pero la 18.0.0 salió el 16-09-2026, y el autor la eligió ese mismo 30-09: las tablas quedan congeladas con el formato, y no se trabaja con versiones de Unicode ya superadas.
|
|
|
5. El head va siempre en su versión 1. Una versión nueva exige un formato nuevo (§22), así que nunca falla tras la fecha por su versión.
|
|
|
6. Los escritores de la v0.10 solo escriben el formato 3.
|
|
|
7. Los límites quedan como se propusieron, y con el formato 3 quedan congelados: subirlos exigiría un formato nuevo.
|
|
|
|
|
|
| Límite | Valor |
|
|
|
|---|---|
|
|
|
| Comentario | 16 KiB |
|
|
|
| Autor declarado | 256 B |
|
|
|
| Ruta | 1024 B |
|
|
|
| Segmento | 255 B |
|
|
|
| Profundidad | 32 segmentos |
|
|
|
| Ficheros | 65 535 |
|
|
|
| Head | 16 MiB |
|
|
|
| Área | 64 KiB |
|
|
|
|
|
|
8. Un único fichero dentro de una carpeta se descarga directamente, con su nombre, y el ZIP con la carpeta es una opción (apartado 5). No cambia el formato.
|
|
|
9. El lector acepta una cápsula sin ficheros (C = 0); el escritor exige un fichero o un comentario.
|
|
|
10. Un '-' inicial en una ruta solo da un aviso.
|
|
|
11. Todo el texto del creador, sean rutas, comentario o autor, sigue la regla de invisibles: R4b del apartado 3 y la regla del texto del apartado 2. El autor la aprobó el 30-09 por la noche, a raíz del aviso de Unicode 18 sobre los selectores de variante.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Parte 2. Entrega 2: firma y claves de autor
|
|
|
|
|
|
La entrega 2 define `alg` 1, Ed25519 estricto, y las claves de autor. No cambia ningún byte del formato: una cápsula firmada se abre en un lector de la entrega 1, que muestra F1.
|
|
|
|
|
|
### 10. Qué se firma
|
|
|
|
|
|
- `payload_commit = SHA-256("datekeys:dkc3:payload:v1" || 0x00 || I_PAYLOAD)`.
|
|
|
- `CONTROL_PUB` son los bytes de `CONTROL_CBOR` del paso 14, con los 32 de la clave 3 sustituidos por `payload_commit`.
|
|
|
- `control_commit = SHA-256("datekeys:dkc3:control:v1" || 0x00 || CONTROL_PUB)`. Cubre, sin revelar `I_PAYLOAD`:
|
|
|
- `header_binding`: PRELUDE, `capsule_id`, DateKey, política y extensiones públicas;
|
|
|
- L, el relleno y las extensiones de control;
|
|
|
- `I_PAYLOAD`, a través del compromiso.
|
|
|
- `head_digest = SHA-256("datekeys:dkc3:head:v1" || 0x00 || HEAD_CBOR)`. Fija `CONTENT` byte a byte, porque el head lleva el tamaño y el SHA-256 de cada fichero.
|
|
|
- Los compromisos que se firman o se sellan, `payload_commit`, `control_commit`, `head_digest` y `SEAL_SUBJECT`, llevan cada uno su prefijo de dominio (hallazgo 10), así que ninguno sirve en otro contexto. Los hashes que van dentro de ellos, como el de cada fichero, no lo necesitan.
|
|
|
|
|
|
```text
|
|
|
AUTHOR_MESSAGE = "datekeys:dkc3:author-signature:v1" || 0x00 || control_commit || head_digest ; 98 B
|
|
|
```
|
|
|
|
|
|
La firma con `alg` 1 es la Ed25519 de `AUTHOR_MESSAGE`, y el mapa `author-signature`, codificado aparte en la clave 2 de `security`, lleva la clave pública de 32 bytes y la firma de 64. El escritor MUST verificarla, con el perfil estricto, antes de escribir la cápsula. No se cubren los recipients (§55.2), los ciphertexts ni la `.dkk`.
|
|
|
|
|
|
### 11. Ed25519 estricto
|
|
|
|
|
|
Vale para la firma de autor, y en la entrega 3, para el token, el checkpoint y los certificados del sello. Una firma es válida si y solo si:
|
|
|
|
|
|
1. |A| = 32 y |sig| = 64.
|
|
|
2. A es canónica (y < p, y el signo es 0 si y ∈ {1, p−1}), decodifica y no es de orden pequeño.
|
|
|
3. `sig[63] & 0xE0 = 0` y S < ℓ.
|
|
|
4. [S]B − [k]A codifica exactamente R, con k = SHA-512(R‖A‖M) mod ℓ.
|
|
|
|
|
|
En las dos librerías:
|
|
|
|
|
|
- Go 1.26.8 `ed25519.Verify` acepta A = 01 00…00, R = identidad y S = 0. Por eso Go comprueba A con código propio antes de llamarla.
|
|
|
- noble 2.4.0 `ed25519.verify` usa el cofactor y acepta ese mismo caso con `zip215: true`. MUST NOT llamarse, y una prueba de guarda lo impide. Se usa `Point.fromBytes(A, false)`, `isSmallOrder()`, `multiplyUnsafe` y `sha512`.
|
|
|
|
|
|
### 12. Qué prueba una firma
|
|
|
|
|
|
- **Firma válida(K):** quien tiene la clave privada de K firmó este head para este control. No prueba quién tiene K, ni cuándo firmó, ni la autoría, ni el autor declarado, ni que no haya otra cápsula con el mismo `capsule_id` (equivocación).
|
|
|
- **Ataques:** tras la fecha, quien vuelve a sellar el control (§36.1) puede quitar la firma o firmar con K′. Sin sello, una firma no prueba que existiera antes de la fecha. Un trasplante a otro control invalida la firma. Contra la equivocación, se publica `capsule_digest` antes de la fecha.
|
|
|
- **Quien abre** ve la clave pública, que vincula entre sí las cápsulas firmadas con ella.
|
|
|
- Los veredictos F2 a F4 están en el apartado 4.
|
|
|
|
|
|
### 13. Verificación por un tercero
|
|
|
|
|
|
Un tercero recibe un paquete (`datekeys statement export|verify`) con PRELUDE, `PUBLIC_HEADER`, `CONTROL_PUB`, HEAD, SECURITY y los ficheros elegidos, nunca `I_PAYLOAD`.
|
|
|
|
|
|
- Recalcula `header_binding` y lo compara con el que va en `CONTROL_PUB`; después, `control_commit` y `head_digest`.
|
|
|
- Comprueba la firma y, desde la entrega 3, el sello, con los veredictos del apartado 4.
|
|
|
- Comprueba el SHA-256 de cada fichero del paquete contra el head.
|
|
|
- Revelar HEAD muestra los nombres, los tamaños, las mtimes y los SHA-256 de todos los ficheros, también los que no van en el paquete.
|
|
|
|
|
|
### 14. Claves de autor
|
|
|
|
|
|
**Formato.**
|
|
|
|
|
|
- Semilla Ed25519 de 32 bytes.
|
|
|
- Clave pública `dkauthor1…` (bech32, 67 caracteres); clave secreta `DKAUTHOR-SECRET-KEY-1…` (79 caracteres).
|
|
|
- El fichero imita una identidad de `age` y por defecto va envuelto en `age` scrypt con logN = 16 (64 MiB). El `scrypt` de noble es síncrono (`recipients.js:358`), y el 18 por defecto (256 MiB) bloquea o agota un móvil. La página avisa de la pausa.
|
|
|
|
|
|
**Custodia.**
|
|
|
|
|
|
- **Página:** `crypto.getRandomValues` y `ed25519.getPublicKey`. El fichero se ofrece una sola vez y nunca se guarda en el navegador; la semilla se borra tras firmar.
|
|
|
- **CLI:** `author keygen -out FICHERO` (0600, nunca sobrescribe), `author public` y `encrypt -sign`. La passphrase se lee de `-passphrase-file`, o sin eco con código propio sobre `golang.org/x/sys`, que ya está en `go.mod` como indirecta: ningún módulo nuevo.
|
|
|
- La API expone `AUTHOR_MESSAGE` para que firme un dispositivo externo. La sal del head evita que ese dispositivo aprenda algo del contenido (apartado 0.2).
|
|
|
|
|
|
**Cómo se conoce la clave.**
|
|
|
|
|
|
- Por un canal externo: la persona pega la clave `dkauthor1…` completa que le dio el autor, o la CLI la recibe con `-expect-author`.
|
|
|
- La página ofrece además «guardar esta clave como…» desde una cápsula solo si:
|
|
|
- la firma está cubierta por un sello válido, no comprometido y con t < round_time, lo que excluye a quien volvió a firmar tras la fecha;
|
|
|
- la etiqueta no se rellena con el autor declarado;
|
|
|
- la persona confirma la clave completa por otro canal.
|
|
|
|
|
|
Como exige un sello, esta vía solo existe desde la entrega 3.
|
|
|
- Ninguna decisión se basa en una huella truncada.
|
|
|
|
|
|
### 15. Cambios, pruebas y preguntas de la entrega 2
|
|
|
|
|
|
**Spec:**
|
|
|
|
|
|
- §4, objetivo 9: autenticidad opcional.
|
|
|
- §5 y un §7.9 nuevo: la equivocación y la clave de autor robada.
|
|
|
- §29.7 y §29.8: los enunciados de la firma y Ed25519 estricto.
|
|
|
- §62.1: la firma se verifica antes de escribir (MUST).
|
|
|
- §27, §36.1, §55.1, §63 paso 15, §72, §73 y §74: la firma de autor aporta lo que antes solo aportaba una «extensión de firma».
|
|
|
|
|
|
**Código:** `ed25519strict`, `authorkey`, `statement` y la CLI (`-sign`, `-expect-author`, `author keygen|public` y `statement export|verify`); en TypeScript, además, la guarda contra `ed25519.verify`.
|
|
|
|
|
|
**Pruebas:**
|
|
|
|
|
|
- **Fixture:** `format3_signed`.
|
|
|
- **Vectores:** `statements.json`, con los prefijos de dominio y `alg` 1 con claves de 33 bytes y firmas de 63, que dan F1; `ed25519_strict.json`, con los casos de «Taming the many EdDSAs».
|
|
|
- **Mutaciones:** la firma alterada, quitada o rehecha, y el trasplante a otro control.
|
|
|
|
|
|
**Preguntas abiertas:**
|
|
|
|
|
|
1. ¿Fichero de clave cifrado por defecto? Sí, con logN 16.
|
|
|
2. ¿Sin sello, la clave solo se guarda pegándola desde otro canal? Sí: una firma sin sello puede haberse rehecho tras la fecha.
|
|
|
3. ¿Varias firmas, de coautores, de un notario o de un testigo? No hace falta decidirlo ahora. Hay un solo hueco de firma, pero su `alg` permite que una versión posterior defina una lista de firmas sin cambiar el formato. Un lector anterior la mostraría como F1, abriría la cápsula igual y seguiría comprobando el sello, porque la clave 3 va aparte.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Parte 3. Entrega 3: sello de tiempo
|
|
|
|
|
|
La entrega 3 depende del documento «Servicio de sellado DateKeys v1», que definirá la custodia de las claves, los monitores, el hosting y los detalles de esta parte. Aquí queda lo que ya está decidido y lo que ese documento tiene que cerrar. No cambia la trama ni el head: define `seal_type` 1 y un área mayor.
|
|
|
|
|
|
### 16. Decisiones que ya fijan el diseño
|
|
|
|
|
|
1. **Origen (hallazgo 1).** El servicio vive en `seal.datekeys.com`, un origen distinto del de la página, que solo sirve la API y nunca HTML. Responde de todos modos con `X-Content-Type-Options: nosniff` y `Content-Security-Policy: sandbox`. La CSP de `/create` añade `https://seal.datekeys.com` a `connect-src`, solo en esa página; `/inspect` conserva la suya (apartado 19).
|
|
|
2. **La prueba va dentro de la cápsula (hallazgo 3).** El token lleva la prueba de inclusión de su hoja y el checkpoint firmado que la contiene. Una raíz offline, fijada en el software desde el primer perfil, certifica cada clave anual. El sello se verifica sin red para siempre, aunque DateKeys desaparezca; la auditoría del anclaje queda como comprobación online opcional.
|
|
|
3. **Área mayor.** El token con su prueba no cabe en 512 bytes. La entrega 3 fija un área mayor para su versión del spec, que todos sus escritores usan siempre, sellen o no, sin que dependa de `-seal` ni de que haya un `sealer` (apartado 2).
|
|
|
4. **Abuso (hallazgo 4).** Cada petición lleva una prueba de trabajo SHA-256 de un segundo en el navegador. El servicio no guarda estado sobre quién pide.
|
|
|
|
|
|
### 17. Qué se sella
|
|
|
|
|
|
```text
|
|
|
SIG_PART = 0x00 sin firma | 0x01 || SHA-256(contenido de la cadena de bytes de la clave 2 de security, sin su cabecera CBOR)
|
|
|
SEAL_SUBJECT = SHA-256("datekeys:dkc3:seal-subject:v1" || 0x00 || control_commit || head_digest || SIG_PART)
|
|
|
SEAL_MESSAGE = "datekeys:seal:v1" || 0x00 || seal_profile_hash || SHA-256(SEAL_SUBJECT) || u64(t) || u64(índice) ; 97 B
|
|
|
```
|
|
|
|
|
|
- Solo `SEAL_SUBJECT` sale del dispositivo, y el registro publica su hash, nunca los 32 bytes recibidos.
|
|
|
- `seal_profile_hash` es el SHA-256 del contenido de la clave 2 del token, el certificado exacto de la clave anual (apartado 18).
|
|
|
- **Qué prueba un sello válido:** según el servicio, todo existía en t, y si t < round_time, antes de que la cápsula pudiera leerse. Depende del operador; la auditoría acota esa dependencia a la ventana de anclaje.
|
|
|
- **Ataques:** tras la fecha, quien vuelve a sellar el control puede quitar la firma o firmar con K′, pero un sello nuevo tendría t ≥ round_time. Un trasplante a otro control invalida la firma y el sello.
|
|
|
|
|
|
### 18. El token y su verificación sin red
|
|
|
|
|
|
La forma es orientativa; la exacta es del documento del servicio. El certificado va codificado aparte, en una cadena de bytes: así el token se valida sin él, y la raíz firma sus bytes exactos, con un prefijo de dominio, sin reserializar nada (§72).
|
|
|
|
|
|
```text
|
|
|
seal-token = { 0: "datekeys-seal-token", 1: 1,
|
|
|
2: bstr, ; seal-cert, codificado aparte
|
|
|
3: bstr .size 64, ; firma de la raíz sobre el contenido de la clave 2
|
|
|
4: bstr .size 8 (t), 5: bstr .size 8 (índice),
|
|
|
6: bstr .size 64, ; firma de SEAL_MESSAGE con la clave anual
|
|
|
7: [* bstr .size 32], ; prueba de inclusión RFC 9162
|
|
|
8: bstr } ; checkpoint C2SP firmado que incluye la hoja
|
|
|
seal-cert = { 0: "datekeys-seal-cert", 1: 1, 2: "datekeys:seal:v1",
|
|
|
3: tstr .size (1..256), ; log_origin
|
|
|
4: bstr .size 32, ; clave de tokens
|
|
|
5: bstr .size 32, ; clave de checkpoints
|
|
|
6: bstr .size 8, 7: bstr .size 8, ; not_before, not_after
|
|
|
8: bstr .size 8, ; max_anchor_delay
|
|
|
9: bstr .size 32 } ; SHA-256 de la clave pública de la raíz
|
|
|
; u64 big-endian en segundos
|
|
|
```
|
|
|
|
|
|
**Verificación en el paso 17.6, sin red,** después del orden del apartado 4, que da S1 o S2 antes de comprobar nada:
|
|
|
|
|
|
- Se comprueban la firma de la raíz sobre el contenido de la clave 2; que t y el índice no pasan de 2⁵³ − 1 y que t cae en [not_before, not_after]; la firma estricta de `SEAL_MESSAGE`; que el origen del checkpoint es `log_origin`; la prueba de inclusión de la hoja en el índice dado hasta la raíz del checkpoint, con 53 hashes como mucho; y la firma del checkpoint con la clave de checkpoints. Cualquier fallo → S3.
|
|
|
- Si todo cuadra, el sello es válido: S4 si t < round_time y S5 si no. Es S6 si el registro de §71 que trae el lector marca como comprometida esa clave anual.
|
|
|
|
|
|
**Claves.**
|
|
|
|
|
|
- La raíz es offline y se fija en el software desde el primer perfil. Solo firma certificados anuales.
|
|
|
- Cada clave anual tiene una ventana de un año y se destruye de forma documentada en not_after.
|
|
|
- Un compromiso marca **todos** los tokens de esa clave, sea cual sea su t, porque el ladrón elige t; la fecha del compromiso solo sirve a la auditoría. Un lector solo conoce los compromisos publicados antes de su versión.
|
|
|
- Quien robara la raíz podría certificar claves falsas y retrodatar a voluntad. Su custodia es del documento del servicio.
|
|
|
|
|
|
### 19. El servicio
|
|
|
|
|
|
**API.**
|
|
|
|
|
|
- `POST https://seal.datekeys.com/v1/seal` con `application/octet-stream`: los 32 bytes de `SEAL_SUBJECT` y la prueba de trabajo.
|
|
|
- Es una petición entre orígenes, con preflight CORS; el servicio solo admite el origen de la página.
|
|
|
- `fetch` con `credentials:'omit'`, `referrerPolicy:'no-referrer'`, `cache:'no-store'` y `redirect:'error'`, en un único módulo con prueba de guarda.
|
|
|
- Respuestas: 200 con el token; 400, 429 o 503.
|
|
|
- La librería no usa la red: recibe un `sealer(subject)`.
|
|
|
|
|
|
**Página.** Hoy la CSP es global (`kit.csp` en `svelte.config.js`, emitida como `<meta>`), con `connect-src 'self'` y `worker-src 'none'`, y `scripts/check-build.mjs` exige esos valores en todas las páginas. La entrega 3 da a `/create`, y solo a ella, `connect-src 'self' https://seal.datekeys.com` y `worker-src 'self'`, reescribiendo su `<meta>` al prerenderizar o con cabeceras por ruta; `check-build.mjs` pasa a comprobar la política exacta de cada página. El worker de la prueba de trabajo no hereda el `<meta>` (CSP3): su política sería la de su propia respuesta, y el hosting estático no manda cabeceras. Por eso no importa el módulo de red, y una prueba de guarda lo comprueba.
|
|
|
|
|
|
**Escritor.** Con consentimiento, pide el sello después de firmar. MUST verificar el token y MUST cumplirse t < round_time. Si no, descarta el sello, y la persona elige seguir sin él o cancelar, igual que si el servicio no responde; `plan.size` no cambia, porque el área es fija. Avisa antes de pedirlo si faltan menos de 2 min para la fecha de apertura, porque t se redondea al minuto siguiente, y después si |t − reloj| > 10 min.
|
|
|
|
|
|
**Prueba de trabajo.**
|
|
|
|
|
|
- El cliente busca un nonce tal que el SHA-256 de un prefijo de dominio, el subject, la ventana de t y el nonce tenga D bits a cero. D se ajusta para que tarde un segundo en el navegador, en un worker.
|
|
|
- El servicio la comprueba con un solo hash y descarta en memoria las repeticiones de la ventana en curso. No guarda IPs ni nada sobre quién pide.
|
|
|
- Encarece cada petición por igual, pero no frena a quien dedica varios núcleos o una GPU. El registro tiene que aguantar un crecimiento abusivo, con un límite global que no dependa de quién pide.
|
|
|
|
|
|
**Servicio.** Calcula t = 60·⌈ahora/60⌉ con un reloj disciplinado y añade la hoja `SEAL_MESSAGE` de forma duradera y replicada. Después espera al checkpoint que la incluye, firma y responde.
|
|
|
|
|
|
**Registro y anclaje.**
|
|
|
|
|
|
- Árbol Merkle RFC 9162, checkpoints C2SP al menos cada 60 s con su propia clave, y tiles estáticos replicables.
|
|
|
- Cada hora, el último checkpoint va a al menos dos calendarios de OpenTimestamps; la prueba se publica en `https://seal.datekeys.com/v1/anchor/<tamaño>.ots`.
|
|
|
- Invariantes que vigilan los monitores: hay un checkpoint al menos cada 60 s, los tiempos no decrecen, y t − 2 h ≤ t_bloque ≤ t + max_anchor_delay (26 h). La tolerancia inferior absorbe la holgura de los tiempos de bloque de Bitcoin.
|
|
|
- El anclaje **acota** la retrodatación a max_anchor_delay, pero no la impide. Por eso el veredicto auditado «antes de la apertura» exige t_bloque + 2 h < round_time. En una fase 2, testigos tlog-witness la acotarán a minutos.
|
|
|
- La auditoría usa la red, es opcional y queda fuera de §63 (`datekeys seal audit`). Parte del checkpoint que va en el token, y necesita el `.ots` de un checkpoint anclado posterior y la prueba de consistencia entre los dos. La página no puede verificar Bitcoin bajo su CSP, y lo dice.
|
|
|
|
|
|
### 20. Privacidad
|
|
|
|
|
|
- **Antes de la apertura,** el servicio ve la IP, el User-Agent, la hora y 32 bytes que no puede vincular.
|
|
|
- **El tráfico** hacia `seal.datekeys.com` delata a un observador de la red que la cápsula lleva sello y, más o menos, cuándo se creó. Es una fuga aceptada (hallazgo 8): sin sello, la página no hace ninguna petición.
|
|
|
- **Después,** quien abre recalcula el subject y encuentra la hoja: el sello vincula la cápsula con ese POST. Por eso el servicio MUST NOT guardar registros de acceso, tampoco en el hosting ni en el CDN, ni hacer analítica. Lo dicen el texto de consentimiento y §55.2.
|
|
|
- **Quien abre** ve además t al minuto, que aproxima la fecha de creación, como dice el texto de consentimiento, y el índice y el certificado, que vinculan la cápsula con el POST.
|
|
|
|
|
|
### 21. Alternativas descartadas
|
|
|
|
|
|
- **RFC 3161 dentro de la cápsula:** exigiría DER, CMS y X.509 con código propio, tokens de 2 a 6 KB y revocación durante décadas. Queda reservado `seal_type` 2; eIDAS puede llegar después, sobre los checkpoints.
|
|
|
- **OpenTimestamps dentro de la cápsula:** al crear, la prueba está pendiente. Queda reservado `seal_type` 3.
|
|
|
|
|
|
### 22. Cambios y preguntas de la entrega 3
|
|
|
|
|
|
**Spec:** §4, objetivo 9 (fecha opcional); un §7.10 nuevo (servicio de sellado comprometido, clave anual o raíz robadas); §29.9 (el sello); §62.1 (el sello se verifica antes de escribir, MUST); §71 (la raíz y el registro de claves comprometidas); y el documento «Servicio de sellado DateKeys v1».
|
|
|
|
|
|
**Código:** `seal` y `seal-verify`, la guarda contra `fetch` y la CLI (`-seal` y `seal audit`).
|
|
|
|
|
|
**Pruebas:** `format3_signed_sealed` y `format3_sealed_after`; `seal_token.json`, con un token retrodatado bajo una clave anual comprometida y un certificado con otra raíz; mutaciones de t y del trasplante.
|
|
|
|
|
|
**Preguntas abiertas,** para el documento del servicio:
|
|
|
|
|
|
1. ¿Clave con ventana? Decidido: sí, de un año, certificada por una raíz offline desde el primer perfil (tercera decisión sobre la revisión).
|
|
|
2. ¿Hora del sello al minuto? Sí, pero se revisa con la latencia: el token espera al checkpoint que incluye su hoja.
|
|
|
3. ¿Checkpoints cada pocos segundos, o respuesta al cierre de cada minuto? Recomendación: cada pocos segundos, para que la página no espere un minuto.
|
|
|
4. ¿Tamaño del área mayor? Probablemente 4096 bytes. El token, con su certificado, una prueba de inclusión de hasta 40 niveles y el checkpoint, ronda los 2 KB, y la firma ocupa 108 bytes.
|
|
|
5. ¿Cómo se limita el crecimiento del registro sin saber quién pide?
|
|
|
6. ¿Custodia de la raíz, y qué hacer si se compromete?
|
|
|
7. ¿Dónde se conservan a largo plazo los `.ots` y las pruebas de consistencia, para que la auditoría online siga siendo posible sin DateKeys?
|
|
|
8. ¿Cómo se cambia de clave anual? Con t redondeada hacia arriba, una petición cerca de not_after puede recibir una t posterior, y el checkpoint que incluye una hoja de los últimos segundos puede firmarse ya con la clave siguiente. Recomendación: elegir el certificado por t, solapar las ventanas y firmar cada checkpoint del solape con las dos claves, que un signed note admite.
|
|
|
9. ¿Cómo se define la prueba de trabajo? Falta fijar D y de dónde lo obtiene el cliente; la tolerancia de ventana, porque una prueba que empieza en el segundo 59 termina en la ventana siguiente y los relojes se desvían; y un registro de repeticiones que cubra todas las ventanas aceptadas y todas las réplicas.
|
|
|
10. ¿Sello RFC 3161 cualificado? En una segunda fase.
|
|
|
11. ¿Testigos de checkpoints? En la fase 2 del servicio.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Anexo A. Revisión de Fable (30-09)
|
|
|
|
|
|
El texto completo está en [revision_fable.md](revision_fable.md). Veredicto: la parte criptográfica aguanta, y los problemas están en los bordes. Fable comprobó la aritmética de tamaños de entonces, la necesidad del verificador Ed25519 propio y las versiones de Unicode de Go y de Node. No revisó el ZIP byte a byte ni probó HFS+ en una máquina: lo de HFS+ se apoya en la lista de `core.protectHFS` de git.
|
|
|
|
|
|
| # | Gravedad | Hallazgo | Resolución | Dónde |
|
|
|
|---|---|---|---|---|
|
|
|
| 1 | Mayor | Mismo origen para la página y el servicio | `seal.datekeys.com`, solo API, con `nosniff` y `CSP: sandbox` (segunda decisión sobre la revisión) | Apartado 16 |
|
|
|
| 2 | Mayor | Caracteres invisibles en las rutas | R4 por `Default_Ignorable_Code_Point` con lista blanca; R3 y R7 sobre la forma sin la lista blanca; etiquetas prohibidas | Apartado 3 |
|
|
|
| 3 | Mayor | El sello no sobrevive a DateKeys | La prueba dentro de la cápsula y una raíz offline desde el primer perfil (tercera decisión) | Apartados 16 y 18 |
|
|
|
| 4 | Mayor | Abuso frente a «sin registros» | Prueba de trabajo, con estado solo en memoria (cuarta decisión) | Apartado 19 |
|
|
|
| 5 | Menor | El área dependía de `SECURITY_LEN` | `AREA_LEN` en la trama, fijo por versión del spec y siempre reservado | Apartado 2 |
|
|
|
| 6 | Menor | Orden de rutas en TypeScript | R8 compara bytes UTF-8; vector en `paths.json` | Apartados 3 y 8 |
|
|
|
| 7 | Menor | Suplantación en la CLI | Prefijo en cada línea visual, ancho contado por lo alto y veredictos repetidos al final | Apartado 4 |
|
|
|
| 8 | Menor | El POST delata el sello | Fuga aceptada y declarada | Apartados 0.4 y 20 |
|
|
|
| 9 | Menor | Ambigüedades | Tabla de verdad de firma y sello; expresión regular de R6b; mtime omitida fuera de rango | Apartados 4, 3 y 2 |
|
|
|
| 10 | Menor | Sin prefijo de dominio; `.datekeys-*` | Prefijos en `control_commit` y `head_digest`; R10 | Apartados 10 y 3 |
|
|
|
|
|
|
**Sobre el documento:**
|
|
|
|
|
|
- **28 objeciones que eran 24:** el anexo B las funde y marca cada una como corregida o aceptada.
|
|
|
- **Faltaban definiciones:** el apartado 0.2 explica las capas, L_MAX y la sal, y los apartados de este documento se distinguen de los § del spec.
|
|
|
- **Tres ficheros con el mismo texto:** este documento es ahora la única fuente.
|
|
|
- **Un sello que el lector no sabe verificar no mostraba nada:** ahora da S1.
|
|
|
|
|
|
## Anexo B. Revisión adversarial interna: 24 objeciones
|
|
|
|
|
|
Todas eran reales. Cuatro de las 28 originales repetían otras (la 12 a la 3, la 16 a la 4, la 18 a la 7 y la 24 a la 2) y se han fundido. Tres se aceptan en lugar de corregirse: la 2, la 9 y la 20. Entre corchetes, el número original.
|
|
|
|
|
|
1. [1] **`control_digest` revelaba `I_PAYLOAD` (mayor).** Corregida: se firma `control_commit`, con `payload_commit`, y hay un paquete de verificación (apartados 10 y 13). Se usa un hash de `I_PAYLOAD` en vez de `R_PAYLOAD`: con `R_PAYLOAD`, quien recibe el paquete podría cifrar otro `PAYLOAD_AGE` que el control abriría.
|
|
|
2. [2 y 24] **El anclaje deja retrodatar hasta 26 h (mayor).** Aceptada y acotada: el anclaje «acota», el veredicto auditado exige t_bloque + 2 h < round_time y los testigos llegan en la fase 2. La tolerancia inferior del ancla es de 2 h.
|
|
|
3. [3 y 12] **«Comprometido desde T» (mayor).** Corregida: un compromiso marca todos los tokens de la clave, y la clave anual se destruye en not_after. Vale también sin red, con un vector.
|
|
|
4. [4 y 16] **Lista best-fit incompleta (mayor).** Corregida con R6c, generada. Solo se proyecta lo que la tabla lleva a ASCII, para no confundir con sintaxis el '?' por defecto.
|
|
|
5. [5] **TOFU.** Corregida (apartado 14).
|
|
|
6. [6] **Un comentario que imita veredictos.** Corregida: presentación normativa y texto nuevo para la firma no soportada. El hallazgo 7 de Fable la completa en la CLI.
|
|
|
7. [7 y 18] **«Espacio» sin definir.** Corregida: es U+0020.
|
|
|
8. [8] **Alias 8.3.** Corregida con R6b, comprobada en esta máquina y precisada con una expresión regular. Git solo protege `.git`; aquí se generaliza.
|
|
|
9. [9] **Vinculación tras la apertura.** Aceptada y documentada (apartado 20): el servicio no guarda registros de acceso.
|
|
|
10. [10] **Canal de difusión.** Corregida: se publica el hash del subject y `fetch` queda aislado.
|
|
|
11. [11] **MAY de fallo rápido (mayor).** Corregida. Contradecía el Alcance de §69.1 y el final del paso 17.
|
|
|
12. [13] **Tiempos del ZIP (mayor).** Corregida byte a byte.
|
|
|
13. [14] **`Open` sin sumidero.** Corregida: error del llamador, como ya hacen `DecodeControl` con el formato 3 (`framing_test.go:243`) y `open` (`open.ts:139-142`).
|
|
|
14. [15] **Precedencia de R1 y R8.** Corregida.
|
|
|
15. [17] **Tamaños con alg 1.** Corregida: dan «ilegible». Desde la revisión de Fable se comprueban al evaluar la firma, y el mapa de la firma va codificado aparte en la clave 2 de `security`, así que el CDDL exterior es el mismo en las tres entregas y el sello no depende de la firma.
|
|
|
16. [19] **HEAD de más de 16 MiB.** Corregida en la fase A.
|
|
|
17. [20] **Carpetas y ZIP64.** Corregida.
|
|
|
18. [21] **Nombre del fichero en OPFS.** Corregida; comprobado en `isStale` (`tempfile.ts:198`).
|
|
|
19. [22] **Salida de la CLI.** Corregida: `Mkdir`, `Root.Rename` (existe en Go 1.26), stdout, cápsulas sin ficheros y `-in`.
|
|
|
20. [23] **Número de ficheros visible.** Aceptada: P da una cota (apartado 6).
|
|
|
21. [25] **Puntos de código sin asignar.** Corregida: Cn entra en R4, que desde la revisión de Fable prohíbe también los ignorables.
|
|
|
22. [26] **scrypt y passphrase.** Corregida: logN 16; `x/sys` ya es indirecta.
|
|
|
23. [27] **Pruebas del escritor de formato 2.** Corregida: opción solo para pruebas (`encrypt.test.ts:116`).
|
|
|
24. [28] **CDDL del perfil de sello.** Corregida entonces. La tercera decisión sobre la revisión lo sustituye por un certificado de la raíz (apartado 18).
|
|
|
|
|
|
## Anexo C. Revisión adversarial de esta versión (30-09)
|
|
|
|
|
|
Un revisor comprobó que los arreglos de Fable y las decisiones del autor están aplicados, buscó lo que se perdió respecto a la versión anterior y buscó problemas nuevos. No encontró nada bloqueante ni mayor: 15 hallazgos menores, todos resueltos. Una segunda pasada comprobó los arreglos y dejó seis retoques, también aplicados: la definición exacta de `SIG_PART`, el orden del sello con el certificado anidado, dos frases sobre la fuga del área de 512, el paso 6 del escritor, las longitudes de una fixture y un enlace del handoff.
|
|
|
|
|
|
| # | Hallazgo | Resolución |
|
|
|
|---|---|---|
|
|
|
| 1 | Un head de versión 2 dentro del formato 3 solo fallaría tras pedir el release, contra §22 | El head va siempre en su versión 1 (apartados 2 y 9) |
|
|
|
| 2 | El área de la entrega 3 dependía de que el escritor supiera sellar, y un área de 512 delata que no hay sello | El área depende solo de la versión del spec; dos fugas nuevas declaradas; la alternativa de 4096, descartada; la versión del spec de cada entrega (apartados 0.4, 2 y 9) |
|
|
|
| 3 | Faltaban §55.1 y otras secciones en los cambios del spec | Añadidas (apartados 8 y 15) |
|
|
|
| 4 | Los avisos al extraer comparaban el nombre tal cual | Comparan la clave de R7 (apartado 3) |
|
|
|
| 5 | HFS+ solo ignora ZWNJ y ZWJ, y su límite es de 255 unidades UTF-16 tras NFD | R3 corregido y con ese límite (apartado 3) |
|
|
|
| 6 | Un mapa de firma mal formado llevaba a X y arrastraba al sello | Las claves 2 y 3 de `security` van codificadas aparte (apartados 2 y 4) |
|
|
|
| 7 | Faltaban pruebas de S1, de X por la capa 3 y de una trama que no cabe en L | Añadidas (apartado 8) |
|
|
|
| 8 | Una raíz desconocida no se distinguía de una firma de raíz inválida | Identificador de la raíz en el certificado y un orden fijo de S1, S2 y S3 (apartados 4 y 18) |
|
|
|
| 9 | La CSP es global y prohíbe los workers | Requisitos de CSP por página y del worker (apartado 19) |
|
|
|
| 10 | Un sello fallido abortaba la cápsula | Se descarta el sello, y la persona elige seguir o cancelar; aviso si faltan menos de 2 min (apartado 19) |
|
|
|
| 11 | El ancho de línea de la CLI fallaba con TAB, con «│» en un terminal CJK y con terminales estrechos | TAB expandido, trozos de W − 3 y W de al menos 20 (apartado 4) |
|
|
|
| 12 | Faltaban requisitos para el documento del servicio | El origen del checkpoint y la longitud de la prueba en la verificación; el cambio de clave anual y la prueba de trabajo como preguntas (apartados 18, 19 y 22) |
|
|
|
| 13 | El área de uso privado U+F000–U+F0FF confunde en Cygwin, WSL y el SMB de macOS | Prohibida en R4 (apartado 3) |
|
|
|
| 14 | Los ficheros sustituidos seguían en el repo | Borrados; el paquete que revisó Fable sigue en git y en el artefacto publicado |
|
|
|
| 15 | Precisiones | R8 y `localeCompare`, los prefijos de dominio, la cota con área de 512, los checkpoints cada 60 s y Linux en la pregunta 4 |
|
|
|
|
|
|
**Comprobado por el revisor:**
|
|
|
|
|
|
- la aritmética de tamaños, con un codificador CBOR propio;
|
|
|
- que las entregas 2 y 3 no cambian ningún byte de la trama, del head ni del control;
|
|
|
- el paso 17 y sus ejemplos, con el comportamiento de `stream.go` y de `flush`;
|
|
|
- la tabla de verdad, las versiones de Unicode y la lista blanca;
|
|
|
- R6b en Go y en JavaScript, y en el NTFS de esta máquina;
|
|
|
- el caso de Ed25519 en Go 1.26.8 y en noble 2.4.0;
|
|
|
- las referencias al código y las cruzadas.
|
|
|
|
|
|
Quedan sin verificar los datos de Unicode, que desde entonces son los de la 18.0.0, las tablas WindowsBestFit, HFS+ en una máquina y los mapeos de Cygwin, WSL y el SMB de macOS.
|
|
|
|
|
|
## Anexo D. Revisión final del borrador del spec v0.10 (30-09)
|
|
|
|
|
|
El borrador del spec de la entrega 1 (`datekeys-go`, rama `v0.10`) pasó una revisión final, como la de la v0.9. Encontró un problema bloqueante, tres mayores y once menores, todos corregidos en el borrador. La lista completa está en [revision_borrador.md](revision_borrador.md). Estos cambian el diseño:
|
|
|
|
|
|
- **R6c (bloqueante).** Rechazaba cualquier carácter ASCII de R4 en una proyección best-fit, y las tablas llevan ahí caracteres corrientes: bestfit1250 convierte «¿» en '?', y bestfit874 convierte «§», «♥» y las flechas en controles C0. «¿Qué es esto.jpg» no se habría podido guardar. Ahora solo rechaza '/', '\', ':' y U+0000, que son los que cambian el destino con la API ANSI de Windows (apartado 3).
|
|
|
- **Rutas en la salida de la CLI.** El prefijo y la repetición de los veredictos valen también para las rutas que muestre, y para una salida que no es un terminal (apartado 4).
|
|
|
- **Precedencia del paso 17.** Los códigos distintos de `ERR_INTEGRITY` se informan después de leer `PAYLOAD_AGE` hasta EOF, no hasta el final del STREAM: los datos tras el chunk final prevalecen, y `filippo.io/age` los señala en la lectura siguiente.
|
|
|
- **Orden de los veredictos.** `alg` y `seal_type` solo se leen de un contenido que cumple su schema: un `seal` con una clave desconocida es S2, no S1.
|