# DateKeys Protocol Specification ## DateKey, DateKeyCap (`.dkc`) y DateKeys Access Key (`.dkk`) ### Borrador normativo v0.10 **Estado:** Draft / pre-estándar **Fecha:** 30 septiembre 2026 **Proyecto:** DateKeys **Implementación de referencia prevista:** Go **Proveedor temporal V1:** drand Quicknet --- ## 1. Alcance Este documento define exclusivamente el **protocolo base DateKeys**. Define: - **DateKey**: descriptor público de una condición temporal criptográfica. - **DateKeyCap (`.dkc`)**: contenedor cifrado asociado a una DateKey y a una política de acceso. - **DateKeys Access Key (`.dkk`)**: credencial portable de acceso. - perfiles de proveedor; - Quicknet como proveedor temporal V1; - resolución fecha → ronda; - framing de `.dkc` y `.dkk`; - cifrado y bindings; - políticas `time_only` y `time_and_key`; - recipient X25519 V1; - formatos 1, 2 y 3 de `.dkc` y su compatibilidad; - huecos fijos de `INNER_ACCESS_AGE` y relleno del payload; - contenido del formato 3: varios ficheros con sus rutas y metadatos, y el área `security`; - verificación; - reglas del escritor; - Release API; - Release Cache; - recuperación directa contra el proveedor; - extensiones genéricas; - consideraciones de privacidad. Este documento **no define almacenamiento, descubrimiento, distribución ni aplicaciones construidas sobre DateKeys**. Una aplicación que necesite información de transporte, descubrimiento o integración DEBE expresarla mediante extensiones no críticas, sin modificar el núcleo del protocolo. --- ## 2. Terminología normativa Las palabras **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **MAY** y **OPTIONAL** se interpretan conforme a RFC 2119 / RFC 8174. - **MUST / DEBE**: requisito obligatorio. - **MUST NOT / NO DEBE**: prohibición obligatoria. - **SHOULD / DEBERÍA**: recomendación fuerte. - **MAY / PUEDE**: opcional. --- ## 3. Principio rector > **Nunca confiar en el servidor cuando la misma propiedad puede verificarse criptográficamente en el cliente.** Consecuencias: - fecha → condición se calcula localmente; - Provider Profiles se pinnean localmente; - releases se verifican localmente; - `.dkc`, `.dkk`, APIs y relays se consideran entradas no confiables; - DateKeys no debe necesitar plaintext ni secretos finales de acceso. --- ## 4. Objetivos de seguridad DateKeys persigue: 1. **Confidencialidad temporal** Bajo los supuestos del proveedor, una condición temporal no debe poder satisfacerse antes del momento elegido. 2. **Confidencialidad frente al operador** DateKeys no debe necesitar conocer el plaintext ni la credencial final de acceso. 3. **Verificabilidad local** El SDK debe detectar condiciones, perfiles y releases manipulados. 4. **Integridad** Alteraciones del framing, cabecera, control o payload deben provocar fallo. 5. **Interoperabilidad** Implementaciones independientes deben producir y consumir objetos compatibles. 6. **Recuperación independiente** Siempre que el proveedor conserve o pueda servir el release necesario, el ciphertext siga disponible y las credenciales correspondientes existan, un objeto maduro debería poder abrirse sin pasar por la API DateKeys. 7. **Extensibilidad** Nuevos proveedores y extensiones no deben redefinir objetos antiguos. 8. **Privacidad de metadatos** Antes de la fecha, una cápsula de formato 2 o 3 no debe revelar la longitud exacta de su contenido ni el número de credenciales que la abren. Una de formato 3 tampoco debe revelar las rutas, los tamaños, los hashes ni las fechas de sus ficheros, su comentario ni su autor declarado, ni su número más allá de la cota que da el relleno (§55.2). --- ## 5. No objetivos El protocolo base no garantiza: - existencia perpetua de Quicknet; - conservación perpetua del histórico de releases; - resistencia post-cuántica del timelock V1; - revocación de una copia ya distribuida; - anonimato absoluto; - ocultar la fecha de apertura, la política de acceso, `capsule_id` o el tamaño rellenado del contenido (§55.2); - autoría legal por metadatos; - fecha probatoria de creación; - protección frente a un dispositivo ya comprometido; - control del plaintext después de un descifrado legítimo. --- ## 6. Fuera de alcance El protocolo base NO define: - dónde se almacena un `.dkc`; - cómo se descubre un `.dkc`; - cómo se distribuye un `.dkc`; - políticas de disponibilidad del fichero; - mecanismos de naming externos; - mecanismos de entrega de una `.dkk`. Estas cuestiones pertenecen a capas superiores. El nombre del fichero `.dkc` y sus fechas en el sistema de ficheros tampoco pertenecen al protocolo. Los nombres, los tamaños y las fechas de los ficheros que guarda una cápsula de formato 3 sí: van cifrados en su head (§29.4). --- ## 7. Modelo de amenazas El protocolo asume potencialmente maliciosos: ### 7.1 Servidor DateKeys Puede intentar: - devolver una ronda pasada; - devolver un perfil falso; - devolver releases inválidos; - correlacionar consultas; - servir datos obsoletos; - guardar copias del ciphertext. ### 7.2 Relay / MITM / DNS Puede: - suplantar endpoints; - modificar respuestas; - retrasarlas; - denegar servicio. ### 7.3 Poseedor de `.dkc` Puede: - modificar bytes; - truncar; - reordenar; - intercambiar cabeceras; - intercambiar controles; - intercambiar payloads; - cambiar una política declarada; - intentar downgrade; - fabricar una cápsula de formato 3 con rutas que escapen de la carpeta del lector o que colisionen al extraerlas, nombres que se confundan, un head que agote su memoria o un comentario que imite un veredicto (§29.4 a §29.7). ### 7.4 Poseedor de `.dkk` Debe tratarse como poseedor de una capacidad sensible. ### 7.5 Cadena de suministro Un SDK o dependencia comprometidos pueden: - sustituir la raíz de confianza; - aceptar condiciones falsas; - exfiltrar secretos; - debilitar criptografía. ### 7.6 Provider comprometido Si deja de cumplirse el supuesto de seguridad del provider, puede fallar la confidencialidad temporal. ### 7.7 Adversario cuántico futuro El timelock Quicknet V1 no se considera post-cuántico. Existe riesgo **harvest now, decrypt later** para ciphertexts de larga duración. ### 7.8 Dispositivo del creador Si está comprometido antes o durante el cifrado, el protocolo no puede impedir la copia del plaintext o de secretos. --- ## 8. Objetos del protocolo ```text DateKey ↓ condición temporal pública DateKeyCap (.dkc) ↓ objeto protegido DateKeys Access Key (.dkk) ↓ capacidad adicional de acceso ``` --- ## 9. Provider abstraction Toda condición temporal se expresa como: ```text provider profile condition ``` Ejemplo Quicknet: ```json { "provider": "drand", "profile": "datekeys:quicknet:v1", "condition": { "round": 66884212 } } ``` El protocolo no presupone que todos los providers utilicen rondas. --- ## 10. Provider Profile Un Provider Profile es inmutable. Debe definir: - `profile_id`; - provider; - identificador de red; - parámetros criptográficos; - parámetros de tiempo; - reglas de validación; - `profile_hash`. Cualquier cambio criptográficamente relevante exige otro perfil. --- ## 11. Codificación canónica del Provider Profile V1 usa **Deterministic CBOR** conforme a RFC 8949, con el perfil CBOR del protocolo (§58). Quicknet se serializa como mapa CBOR con claves enteras: ```text 0 → "datekeys-provider-profile" 1 → 1 2 → "datekeys:quicknet:v1" 3 → "drand" 4 → "quicknet" 5 → <32-byte chain hash> 6 → 7 → 3 8 → 1692803367 9 → "bls-unchained-g1-rfc9380" 10 → ``` Rangos de los enteros del perfil: - `period` (clave 7) MUST estar entre 1 y 2⁵³ − 1 (9007199254740991); - `genesis_time` (clave 8) MUST estar entre 0 y 2⁵³ − 1. Estos son los rangos de la codificación. Las reglas que un perfil MUST cumplir para pinnearse, con su código de error, y la autocomprobación de `chain_hash` están en §12.1; el orden en que se aplican sigue §69.1. El hash: ```text profile_hash = SHA-256(exact_deterministic_cbor_bytes) ``` La seguridad NO procede de un `profile_hash` autodeclarado por una entrada remota. La seguridad procede del perfil pinneado/confiado localmente. --- ## 12. Quicknet Provider Profile V1 ```text profile_id: datekeys:quicknet:v1 provider: drand network: quicknet chain_hash: 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971 public_key: 83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a period_seconds: 3 genesis_time: 1692803367 genesis_seed: f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e scheme: bls-unchained-g1-rfc9380 ``` El SDK oficial DEBE pinnear estos parámetros o una representación firmada equivalente. --- ## 12.1 Validación de un Provider Profile Una implementación MUST aplicar estas reglas antes de pinnear un Provider Profile y a todo perfil que decodifique, en este orden; la primera que falla decide el código (§69.1): 1. **Tipo, versión y schema.** El type tag y la versión de schema (claves 0 y 1, §70) y el CDDL `provider-profile` de `datekeys.cddl` (§11): `ERR_NON_CANONICAL_CBOR`, o `ERR_UNSUPPORTED_VERSION` para una versión distinta de 1. Las reglas de nombre y de clave pública de ese CDDL tienen código propio (§57) y se comprueban en el punto 2. Una implementación que aplica el límite de implementación sobre `period` (§74) lo comprueba aquí, con `ERR_NON_CANONICAL_CBOR`. 2. **Campos.** Cada campo, ya con el tipo CBOR de su regla CDDL; cualquier fallo es `ERR_UNKNOWN_PROFILE`: - `profile_id` (clave 2) está formado por caracteres de `[a-z0-9:._-]` y empieza por uno de `[a-z0-9]`; `provider` (3), `network` (4) y `scheme` (9), por caracteres de `[a-z0-9._-]`, y empiezan por uno de `[a-z0-9]`. El `profile_id` aparece sin escapes en el JSON canónico de `dk1_` (§18), que por eso no define ninguno; - una implementación que aplica los límites de implementación de longitud de esos nombres y de `public_key` (§74) los comprueba aquí, con este mismo código; - `period` (clave 7) es como mucho 2³² − 1 (4294967295) segundos, porque el hash del punto 3 lo codifica en 32 bits; - `genesis_time` (clave 8) está entre 1 y 253402300798 (9999-12-31T23:59:58Z): posterior al instante 0 y anterior al último instante representable de §15; - `provider` es `drand`, el único proveedor de V1; - `scheme` es uno de los schemes drand sin encadenar que tlock admite: `pedersen-bls-unchained`, `bls-unchained-on-g1` o `bls-unchained-g1-rfc9380`; - `public_key` es la codificación canónica (§12.2) de un punto del grupo de claves del scheme —G1, 48 bytes, para `pedersen-bls-unchained`; G2, 96 bytes, para los otros dos— distinto del punto en el infinito. 3. **Autocomprobación de `chain_hash`.** Relaciona varias claves y por eso va después de todas las comprobaciones de campo. `chain_hash` (clave 5) MUST ser el hash de la información de cadena de drand (drand Protocol Specification, sección *Root of trust*, §77) de los demás parámetros: ```text chain_hash = SHA-256( uint32_be(period) || int64_be(genesis_time) || public_key || genesis_seed || network ) ``` con `period` en segundos, que el punto 2 acota a 32 bits; `genesis_time`, entero con signo de 64 bits; `public_key` y `genesis_seed`, los bytes exactos de las claves 6 y 10; y `network`, los bytes UTF-8 de la clave 4, que se omiten cuando `network` es `default`. `profile_id`, `provider` y `scheme` no entran en el hash. Una discrepancia es `ERR_PROFILE_MISMATCH`. Para Quicknet el resultado es su `chain_hash` de §12. 4. **Pin.** Al pinnearlo, su `profile_hash` (§11) MUST ser el que el cliente conoce previamente (§13): `ERR_PROFILE_MISMATCH`. Los alfabetos de los nombres valen para todo perfil. Las demás reglas de los puntos 2 y 3 son las del proveedor drand en V1; un proveedor futuro definirá las suyas en su propio perfil (§10). --- ## 12.2 Codificación canónica de un punto La clave pública del Provider Profile (§12.1), la firma de un release (§63, paso 10) y el punto U del stanza tlock (§63, paso 11) son puntos de BLS12-381. Cada punto tiene una única codificación válida, la canónica: la comprimida que produce drand, del formato de serialización de BLS12-381 de ZCash (§77), de 48 bytes para un punto de G1 y de 96 para uno de G2. ```text p = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab r = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001 G1: y² = x³ + 4 sobre Fp G2: y² = x³ + 4·(1 + u) sobre Fp2 = Fp[u]/(u² + 1), con x = c0 + c1·u ``` Los tres bits más significativos del primer byte son flags, y el resto de la cadena es la coordenada x en big-endian: ```text bit 7 (0x80) compresión MUST valer 1 bit 6 (0x40) infinito 1 solo en el punto en el infinito bit 5 (0x20) signo elige y entre y y −y ``` - En G1, x ocupa los 381 bits que siguen a los flags y MUST ser menor que p. - En G2, la cadena es c1 seguido de c0, 48 bytes cada uno, con los flags en el primer byte de c1; c1 y c0 MUST ser menores que p. - El bit de signo vale 1 si y solo si y es lexicográficamente mayor que −y: en G1, y > (p − 1)/2; en G2, con y = y0 + y1·u, y1 > (p − 1)/2, o y1 = 0 e y0 > (p − 1)/2. - El punto MUST estar en la curva y en el subgrupo de orden primo r. - El punto en el infinito se codifica con el primer byte 0xc0, compresión e infinito, y todos los demás bits a cero. Un decodificador MUST rechazar cualquier otra cadena de bytes, aunque represente el mismo punto. En particular: - una longitud distinta de 48 en G1 o de 96 en G2, incluidas las formas sin comprimir, de 96 y 192 bytes; - el bit de compresión a 0; - una coordenada mayor o igual que p, como x + p en G1 o c0 + p y c1 + p en G2, que reducida módulo p daría un punto válido; - el bit de infinito con el de signo o con algún bit de la coordenada a 1: una identidad con signo o con carga; - una x que no es la de un punto de la curva, o un punto fuera del subgrupo de orden primo. Es la regla del decodificador que usan drand y la implementación de referencia (`kilic/bls12-381`). Una librería que reduce las coordenadas módulo p o ignora la carga del infinito no la cumple por sí sola, y abriría cápsulas que la referencia rechaza (§76). La clave pública, la firma y U MUST además ser distintos del punto en el infinito (§12.1, §63 pasos 10 y 11): su codificación canónica existe, pero ninguno de los tres usos la admite. --- ## 13. Root of trust El cliente NO DEBE aceptar como raíz de confianza una clave pública o un perfil suministrados por el mismo endpoint que entrega el release. Debe conocer previamente: - chain hash; - public key; - scheme; - genesis time; - genesis seed; - period; - profile hash. --- ## 14. DateKey Una DateKey: - es pública; - no es una clave simétrica; - no es una private key; - no es una `.dkk`; - no es un secreto. Representa una condición temporal verificable. --- ## 15. Resolución Quicknet fecha → ronda Para Quicknet: ```text round_time(r) = genesis_time + (r - 1) * period ``` El SDK MUST elegir la primera ronda cuyo: ```text round_time >= requested_unlock_at ``` Algoritmo: ```text candidate = floor((timestamp - genesis_time) / period) + 1 if round_time(candidate) < requested_unlock_at: candidate++ ``` `requested_unlock_at` MAY tener precisión inferior al segundo. La comparación `round_time(candidate) < requested_unlock_at` MUST realizarse a la precisión completa del instante solicitado; truncar o redondear el instante antes de comparar está prohibido. Nunca se redondea hacia atrás. Al escribir una cápsula, el instante pedido MUST ser posterior al instante actual del reloj del escritor (§62.1). Cotas de la condición temporal: - El último instante representable es 9999-12-31T23:59:59Z (253402300799 en segundos Unix). La ronda de una DateKey MUST cumplir `round_time(round) <= 253402300799`; una ronda posterior es `ERR_DATEKEY_INVALID`. Para Quicknet la última ronda válida es 83903165811, con `round_time` 9999-12-31T23:59:57Z. - Al resolver una fecha, un `requested_unlock_at` anterior a `genesis_time` MUST rechazarse con `ERR_DATEKEY_INVALID`, en lugar de resolverse a la ronda 1, y también uno cuya primera ronda tenga un `round_time` posterior a 9999-12-31T23:59:59Z (para Quicknet, todo instante posterior a 9999-12-31T23:59:57Z). - Al leer una DateKey, la sintaxis `dk1_` (§19) admite rondas de 1 a 2⁵³ − 1 sin consultar el perfil. La cota de `round_time` depende del perfil y se comprueba en el paso 7 de §63, antes de cualquier petición de red. --- ## 16. Vector normativo de ronda Con: ```text genesis_time = 1692803367 period = 3 requested_unlock_at = 2030-01-01T00:00:00Z ``` resultado: ```text round = 66884212 round_time = 2030-01-01T00:00:00Z ``` La ronda: ```text 66432123 ``` corresponde a: ```text 2029-12-16T07:15:33Z ``` Los vectores definitivos MUST generarse desde la implementación de referencia y congelarse antes de `v1.0`. --- ## 17. Ataque de ronda pasada Verificar únicamente una firma BLS válida NO basta. El SDK MUST exigir, cuando disponga de la fecha solicitada: ```text received_round == locally_resolved_round ``` y: ```text round_time(received_round) >= requested_unlock_at ``` Al descifrar, el paso 10 de §63 compara la ronda del release con `DateKey.round` antes de verificar su firma: una ronda distinta es `ERR_ROUND_MISMATCH`, aunque su firma sea válida para esa otra ronda. Ese código es el de un release que el llamador suministra directamente: el que llega de una fuente de red con otra ronda lo descarta su fuente en el paso 9, y si ninguna fuente entrega un release válido el código es `ERR_RELEASE_UNAVAILABLE`. --- ## 18. Representación `dk1_` Por compatibilidad con el prototipo V1 conserva: ```text dk1_ ``` con JSON UTF-8. Payload canónico: ```json {"version":1,"network":"datekeys:quicknet:v1","round":66884212} ``` `network` es el `profile_id` del perfil, cuyo alfabeto (§12.1) nunca necesita escapes JSON: el JSON canónico no escapa ningún carácter. Representación: ```text dk1_ ``` sin padding. --- ## 19. Canonicalidad `dk1_` Existe una única cadena válida para una DateKey V1. El parser MUST: 1. Base64URL-decodificar. 2. Parsear JSON. 3. Validar campos. 4. Reemitir el JSON canónico exactamente. 5. Recrear `dk1_...`. 6. Comparar byte a byte con la entrada. Si no coincide: ```text ERR_DATEKEY_NON_CANONICAL ``` Un fallo de los pasos 1 a 3 es `ERR_DATEKEY_INVALID`; una entrada que supera los pasos 1 a 3 y no coincide en el paso 6 es `ERR_DATEKEY_NON_CANONICAL`. Los pasos 1 a 3 aplican estas reglas: - el paso 1 exige el prefijo `dk1_` y decodifica el resto con uno de los dos alfabetos, Base64URL o Base64 estándar, sin mezclarlos, sin padding o con el padding completo, y admite bits finales distintos de cero; el paso 6 rechaza después esas formas. Cualquier otro carácter, incluidos el espacio, CR (0x0D) y LF (0x0A), hace fallar el paso 1; - el paso 2 exige un único texto JSON de RFC 8259 cuyo valor es un objeto, con espacio en blanco JSON opcional antes y después y nada más, y sin marca de orden de bytes: un U+FEFF inicial, un segundo valor, UTF-8 inválido o cualquier otra cosa fuera de la gramática de RFC 8259 hace fallar el paso 2; - el paso 3 exige exactamente los campos `version`, `network` y `round` (un nombre repetido cuenta una vez, con su último valor); `version` igual a 1; `network` un `profile_id` válido: el alfabeto de §12.1 y, si la implementación lo aplica, el límite de longitud de §74; y `round` un entero entre 1 y 2⁵³ − 1; - los números JSON se leen por su valor decimal exacto, nunca por su aproximación a un double IEEE 754: otra grafía del mismo valor, como `1.0`, `1e0` o `100e-2` para 1, supera el paso 3 y la rechaza el paso 6, pero `1.0000000000000001` no es 1 y falla el paso 3. Esta validación no consulta el perfil: la cota de §15 se comprueba en el paso 7 de §63. --- ## 20. Extensiones de archivo ```text .dkc → DateKeyCap .dkk → DateKeys Access Key ``` La extensión no sustituye los magic bytes. --- ## 21. `capsule_id` Cada `.dkc` MUST contener un `capsule_id`. Debe ser: - aleatorio; - opaco; - independiente de identidad, fecha o servicio; - de al menos 128 bits de entropía. En V1, `capsule_id` MUST ser exactamente: ```text 16 random bytes ``` generados por un CSPRNG. §24 y `datekeys.cddl` fijan esa longitud (§57). --- ## 22. Framing `.dkc` V1 V1 fija el siguiente prelude: ```text offset size field 0 4 MAGIC = "DKC1" 4 1 VERSION = 1, 2 o 3 (formato) 5 1 FLAGS = 0 6 2 RESERVED = 0 8 4 PUBLIC_HEADER_LEN (uint32 BE) 12 4 SEALED_CONTROL_LEN (uint32 BE) 16 ... PUBLIC_HEADER ... ... SEALED_CONTROL ... EOF PAYLOAD_AGE ``` No existe `PAYLOAD_LEN`. El payload es el resto del fichero hasta EOF. `VERSION` es el **formato** de la cápsula. La trama es la misma en los tres formatos; cambian `INNER_ACCESS_AGE`, el plaintext de `PAYLOAD_AGE` y la versión de schema de `CONTROL_CBOR`: | Formato | `VERSION` | `INNER_ACCESS_AGE` (`time_and_key`) | Plaintext de `PAYLOAD_AGE` | Versión de schema de `CONTROL_CBOR` | |---|---|---|---|---| | 1 | 1 | uno o más stanzas | el contenido | 1 | | 2 | 2 | exactamente 16 stanzas (§39) | el contenido con relleno (§29.1) | 2 (§31) | | 3 | 3 | exactamente 16 stanzas (§39) | `BODY`, con los ficheros y su head, con relleno (§29.2) | 3 (§31) | El formato 1 es el de la v0.8.2; el 2, el de la v0.9; el 3, el de esta versión. `PUBLIC_HEADER` y la `.dkk` tienen la versión de schema 1 en los tres formatos (§24, §41). Un escritor MUST escribir el formato 3 (§62.1). Un lector MUST aceptar los tres (§70). Un cambio de la semántica de la cápsula que un lector anterior solo detectaría después de pedir el release, como un código de relleno nuevo (§29.1), una versión nueva del head o un límite mayor del formato 3 (§29.4), exige un formato nuevo, con su fila en esta tabla: así un lector que no lo conoce lo rechaza en el paso 2 de §63, sin red. V1 MUST exigir: ```text 1 <= VERSION <= 3 FLAGS == 0 RESERVED == 0 1 <= PUBLIC_HEADER_LEN <= 1048576 (1 MiB) 1 <= SEALED_CONTROL_LEN <= 67108864 (64 MiB) ``` Una longitud 0 queda fuera de rango: ninguna trama vacía contiene un objeto válido (§57). `PAYLOAD_AGE` no tiene campo de longitud: ocupa desde `16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN` hasta EOF y MUST ser un fichero `age` completo (§28.1); un `PAYLOAD_AGE` vacío es un fichero `age` mal formado. --- ## 23. PRELUDE `PRELUDE` son exactamente los primeros 16 bytes: ```text MAGIC VERSION FLAGS RESERVED PUBLIC_HEADER_LEN SEALED_CONTROL_LEN ``` El binding criptográfico de la cabecera utiliza estos bytes exactos. Los pasos 1 y 2 de §63 lo validan en este orden, y el primer fallo decide el código (§69.1): 1. menos de 4 bytes, o `MAGIC` distinto de `DKC1` → `ERR_INVALID_MAGIC` (paso 1); 2. `DKC1` seguido de menos de 12 bytes, un PRELUDE truncado → `ERR_INTEGRITY` (paso 1); 3. `VERSION` fuera de 1 a 3 → `ERR_UNSUPPORTED_VERSION` (paso 2); 4. `FLAGS` o `RESERVED` distintos de 0 → `ERR_INVALID_FLAGS` (paso 2); 5. `PUBLIC_HEADER_LEN` y después `SEALED_CONTROL_LEN` fuera de su rango (§22) → `ERR_INTEGRITY` (paso 2). Que el fichero contenga los bytes que declara cada longitud se comprueba al leer cada sección: `PUBLIC_HEADER` en el paso 3 y `SEALED_CONTROL` en el paso 5, ambos con `ERR_INTEGRITY`. Un lector que solo conoce el formato 1, como los de la v0.8.2, rechaza aquí una cápsula de formato 2, y uno de la v0.9, que conoce los formatos 1 y 2, una de formato 3, antes de cualquier petición de red (§70). --- ## 24. PUBLIC_HEADER V1 almacena la cabecera directamente como **Deterministic CBOR**. Schema: ```text 0 → "datekeycap" 1 → 1 2 → capsule_id (16 bytes) 3 → compact_datekey (text, canonical dk1_) 4 → access_policy 5 → critical_extensions 6 → noncritical_extensions ``` Las claves 5 y 6 son opcionales y se omiten cuando no hay extensiones (§58.1). Cada una, cuando existe, contiene entre 1 y 64 extensiones (§54). `PUBLIC_HEADER` es la misma en los tres formatos, con la versión de schema 1. No contiene nada del relleno, del número de credenciales ni del contenido del formato 3 (§55.2). La `data` de las extensiones de `PUBLIC_HEADER` es pública y el protocolo base no la vincula al control hasta que se verifica `header_binding` (§63, paso 15). Esa verificación aporta coherencia interna, no autoría (§36.1, §55.1, §72). No se almacena `profile_id` por separado. El perfil se obtiene de la DateKey, evitando dos fuentes de verdad. --- ## 25. Política de acceso declarada Valores V1: ```text 0 → time_only 1 → time_and_key ``` El parser MUST comprobar que la estructura criptográfica real de `SEALED_CONTROL` coincide con la política declarada. Una discrepancia: ```text ERR_POLICY_STRUCTURE_MISMATCH ``` --- ## 26. Header binding `CONTROL_CBOR` MUST contener: ```text header_binding = SHA-256( PRELUDE || PUBLIC_HEADER_BYTES ) ``` donde `PUBLIC_HEADER_BYTES` son exactamente los bytes CBOR almacenados. La implementación NO DEBE reserializar la cabecera para calcular el binding. El PRELUDE incluye `VERSION`: una cápsula a la que solo se cambia el formato falla, como tarde, en el paso 15. Antes falla en el paso 12 o en el 14 (§31, §39, §63), o en el 9.a, con `ERR_ACCESS_INVALID`, si se ofrece una `.dkk` con `capsule_digest` y la implementación lo comprueba (§43, §69.1). Quien puede sellar otro control sí puede cambiar el formato: es una reescritura (§55.1, §70). --- ## 27. Validación pre-unlock Antes de abrir el control pueden verificarse: - magic; - versión, que es el formato (1, 2 o 3); - flags; - reserved; - longitudes; - CBOR canónico; - `capsule_id`; - DateKey canónica; - provider profile pinneado; - coherencia fecha/ronda cuando la fecha solicitada esté disponible; - extensions críticas conocidas. La implementación SHOULD inspeccionar, antes de utilizar secretos o realizar una petición de red, la estructura visible de los ficheros `age` accesibles en ese momento: - `OUTER_TIME_AGE`: número y tipo de stanzas; - `PAYLOAD_AGE`: número y tipo de stanzas; - argumentos visibles del stanza `tlock`, cuando la implementación pueda inspeccionarlos de forma segura. Esta inspección previa permite rechazar una cápsula mal formada antes de solicitar un release. Además de reducir trabajo innecesario, evita que una cápsula inválida genere una consulta observable en la Release API o en los relays. Estas comprobaciones de stanzas previas al descifrado son estructurales. Su autenticidad frente a modificaciones de terceros queda confirmada únicamente cuando la cabecera `age` correspondiente supera la verificación del MAC durante la apertura, y solo frente a quien no conoce la file key (§55.1): una vez publicada la ronda, cualquiera puede calcular `FK_TIME` y recalcular el MAC de `OUTER_TIME_AGE` (§64). Frente a un creador que incluya vías alternativas de descifrado mediante stanzas adicionales, el MAC es válido por construcción y la comprobación estructural es la defensa del protocolo. La inspección pre-unlock es una optimización de validación y privacidad y, por tanto, es un SHOULD. La aplicación de las reglas de cardinalidad de las secciones 29, 32, 33 y 39 es un MUST y DEBE realizarse, como mínimo, en el momento de abrir cada fichero `age`: la identity que desenvuelve la file key MUST rechazar el fichero si el conjunto completo de stanzas recibido viola la regla correspondiente. `header_binding` vincula `PUBLIC_HEADER` al control abierto (§63, paso 15): aporta coherencia interna, no autoría ni fecha (§55.1). Solo una extensión de firma puede aportar autenticidad del creador (§36.1, §72). --- ## 28. Tres ficheros `age` La construcción `time_and_key` V1 utiliza tres ficheros `age` estándar e independientes: ```text 1. PAYLOAD_AGE 2. INNER_ACCESS_AGE 3. OUTER_TIME_AGE ``` Cada fichero `age` genera su propia file key aleatoria de 16 bytes. Se nombran: ```text FK_PAYLOAD FK_ACCESS FK_TIME ``` No se reutiliza ninguna de ellas. --- ## 28.1 Formato de los ficheros `age` Cada uno de los tres ficheros MUST ser un fichero age v1 completo conforme a la especificación `age` de C2SP (§77). Su cabecera sigue la gramática de esa especificación: ```text header = v1-line 1*stanza end v1-line = %s"age-encryption.org/v1" LF stanza = arg-line *full-line final-line arg-line = "-> " argument *(SP argument) LF ; el primer argumento es el tipo argument = 1*VCHAR ; bytes 0x21 a 0x7E full-line = 64base64char LF ; 48 bytes final-line = *63base64char LF ; menos de 48 bytes, quizá ninguno end = "--- " 43base64char LF ; MAC de 32 bytes base64char = ALPHA / DIGIT / "+" / "/" ``` - Base64 usa el alfabeto estándar sin padding y se decodifica de forma estricta: un `=`, una longitud que ninguna codificación sin padding produce o unos bits finales distintos de cero hacen mal formada la línea. - Las líneas terminan en LF: un CR forma parte de la línea y la hace mal formada. - Los argumentos se separan por exactamente un espacio. Una cabecera que no sigue esta gramática hace mal formado el fichero. Eso incluye una cabecera sin stanzas, porque la gramática exige al menos uno (`1*stanza`), y una cabecera que supera los límites del parser de la implementación (§74). Correspondencia de errores: | Fichero | Cabecera mal formada | Cabecera bien formada con un número o un tipo de stanzas que viola §29, §32, §33 o §39 | |---|---|---| | `OUTER_TIME_AGE` | `ERR_INTEGRITY` en el paso 5, o en el 11 si no se hizo la inspección previa | `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 5, y como MUST en el 11 | | `PAYLOAD_AGE` | `ERR_INTEGRITY` en el paso 6, o en el 17 | `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 6, y como MUST en el 17 | | `INNER_ACCESS_AGE` | `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12 (§36) | `ERR_POLICY_STRUCTURE_MISMATCH` en los pasos 12 y 13 | `INNER_ACCESS_AGE` es el plaintext autenticado de `OUTER_TIME_AGE`: si no es un fichero `age` bien formado, lo que falla es la estructura que declara `access_policy` (§36), no la integridad de la cápsula. La cabecera de `OUTER_TIME_AGE` se analiza dentro de los `SEALED_CONTROL_LEN` bytes de `SEALED_CONTROL`; la de `PAYLOAD_AGE`, desde su offset hasta EOF. Los pasos 5 y 6 analizan solo la cabecera `age` y no verifican su MAC (§27). Los demás fallos de un fichero `age` —MAC de la cabecera, argumentos o cuerpo de un stanza, STREAM, truncado, datos tras el final— aparecen al abrirlo, en los pasos 11, 13 y 17, y son `ERR_INTEGRITY`, salvo los códigos que esos pasos de §63 asignan a las reglas de stanzas y a las identities ofrecidas. En los formatos 2 y 3 también es `ERR_INTEGRITY`, en el paso 17, un plaintext de `PAYLOAD_AGE` cuya longitud no es la P de su L o cuyo relleno no es nulo (§29.1), y en formato 3, además, una trama del contenido inválida, un último fichero que no termina en C o un fichero con otro SHA-256 (§29.2, §29.4). --- ## 29. PAYLOAD_AGE El contenido del usuario se cifra como un **fichero age v1 estándar completo**. Durante la creación se genera: ```text I_PAYLOAD = X25519 identity aleatoria de 32 bytes R_PAYLOAD = X25519 public recipient correspondiente ``` `I_PAYLOAD` MUST salir de un CSPRNG, nueva para cada cápsula. MUST NOT reutilizarse en otra cápsula ni derivarse de otro valor, como el contenido, `capsule_id`, `I_ACCESS` o un secreto maestro. La razón es la confidencialidad: la cabecera de `PAYLOAD_AGE` es pública desde la creación, y `I_PAYLOAD` viaja en el control, que se abre en la fecha de su cápsula: - compartida con otra cápsula, abrir una de ellas, lo que en `time_only` puede hacer cualquiera en su fecha, abre el payload de la otra antes de la suya; - derivada del contenido, cualquiera puede comprobar un contenido que adivina, y descifrarlo, antes de la fecha; - derivada de un secreto maestro, ese secreto abre todos los payloads, sea cual sea su fecha. Además, el binding de §30.1 solo separa dos cápsulas si sus `I_PAYLOAD` son distintas. Entonces: ```text PAYLOAD_AGE = age.Encrypt( recipient = R_PAYLOAD, plaintext = PAYLOAD_PLAINTEXT ) ``` donde L es la longitud del contenido en bytes, y en formato 3, la de `BODY` (§29.2): ```text formato 1: PAYLOAD_PLAINTEXT = contenido formato 2: PAYLOAD_PLAINTEXT = contenido || 0x00^(P − L), P = regla(L) (§29.1) formato 3: PAYLOAD_PLAINTEXT = BODY || 0x00^(P − L), P = regla(L) (§29.2) ``` `PAYLOAD_AGE` MUST contener exactamente un stanza, de tipo X25519, para `R_PAYLOAD`. `age` genera internamente: ```text FK_PAYLOAD = 16 random bytes ``` y la protege para `R_PAYLOAD`. `I_PAYLOAD` se almacena dentro de `CONTROL_CBOR`; en los formatos 2 y 3, también L y el código de la regla de relleno (§31). --- ## 29.1 Relleno del payload En los formatos 2 y 3 el plaintext de `PAYLOAD_AGE` es el contenido seguido de ceros. En formato 3, el contenido de esta sección es `BODY` (§29.2): ```text PAYLOAD_PLAINTEXT = contenido || 0x00^(P − L) P = regla(L) ``` donde L es la longitud del contenido en bytes. La longitud de `PAYLOAD_AGE`, visible para cualquiera, revela P y no L (§55.2). El escritor elige la regla. Su código viaja sellado en `CONTROL_CBOR`, junto a L (claves 6 y 7, §31); nada del relleno va en `PUBLIC_HEADER`. | Código | Nombre | P | |---|---|---| | 1 | `bloque256` | el menor múltiplo de 256 mayor o igual que L, y como mínimo 256 | | 2 | `reforzado` | el mayor de `bloque256(L)` y `Padme(L)` | No existe una regla sin relleno. Un lector MUST aceptar los dos códigos. El SDK oficial SHOULD usar el código 2 por defecto. Un código nuevo exige un formato nuevo (§22). Definición exacta, para 0 ≤ L ≤ L_MAX: ```text L_MAX = 2^53 − 2^46 = 8936830510563328 si L <= 256: bloque256(L) = 256 reforzado(L) = 256 si L > 256: bloque256(L) = 256 · ceil(L / 256) E = bitlen(L) − 1 ; floor(log2 L), de 8 a 52 S = bitlen(E) ; floor(log2 E) + 1, de 4 a 6 lastBits = E − S mask = 2^lastBits − 1 Padme(L) = (L + mask) AND NOT mask reforzado(L) = max(bloque256(L), Padme(L)) ``` `bitlen(x)` es el número de bits de x sin ceros a la izquierda: `bitlen(1) = 1`, `bitlen(256) = 9`. `Padme` es la función Padmé de Nikitin et al. (§77), con E = ⌊log2 L⌋, S = ⌊log2 E⌋ + 1 y lastBits = E − S: conserva los S + 1 bits más significativos de L y redondea hacia arriba los demás. `(L + mask) AND NOT mask` es igual a `2^lastBits · ceil(L / 2^lastBits)`. Tratar aparte L ≤ 256 evita el logaritmo de 0 y garantiza E ≥ 8 y S ≥ 4 donde se usa `Padme`. Aritmética: - todos los valores son enteros exactos; para L ≤ L_MAX, ningún resultado intermedio supera 2⁵³ − 1; - E MUST calcularse con enteros, como `bitlen(L) − 1`, nunca con un logaritmo en coma flotante: en IEEE 754, `Math.log2(2^49 − 1)` da 49, y el resultado correcto es 48. Es el primer L del rango en que falla. Con el `Math.log2` de Node, el error solo aparece justo por debajo de una potencia de dos y no cambia P ni S, pero sí E y lastBits, que fijan los vectores; - `bitlen` y `Padme` MUST NOT calcularse con operaciones de 32 bits, como los operadores de bits de JavaScript o `Math.clz32`: dan una P incorrecta por primera vez en L = 2 113 929 217 con los operadores con signo (2 113 929 472 en lugar de 2 147 483 648), y en L = 4 227 858 433 con `>>> 0` (4 227 858 688 en lugar de 4 294 967 296). Es el riesgo real para P: con el código 2, un escritor así rellenaría como `bloque256`, y los lectores correctos rechazarían la cápsula en el paso 17, cuando ya no puede repararse. Una implementación en JavaScript usa BigInt o la forma con `ceil`, exacta en doubles porque divide y multiplica por potencias de dos. L_MAX es el mayor L para el que las dos reglas dan una P de como mucho 2⁵³ − 1 (§58): `reforzado(L_MAX + 1) = 2^53`. Un escritor MUST NOT sellar un L mayor (§62.1), y un lector lo rechaza en `CONTROL_CBOR` (§31). Propiedades, para todo L de 0 a L_MAX (informativo): - P es múltiplo de 256, P ≥ 256 y P ≥ L; - las dos reglas coinciden para L ≤ 8192; la primera diferencia está en L = 8193: 8448 con `bloque256` y 8704 con `reforzado`; - `bloque256` añade como mucho 256 bytes; por encima de L = 8192, `reforzado` añade menos de L / 2^S: menos del 6,25 %, del 3,125 % desde L = 65 536 y del 1,5625 % desde L = 2³²; - entre 2^E y 2^(E+1), `reforzado` admite como mucho 2^S valores de P (16, 32 o 64), y `bloque256`, 2^(E − 8). Vectores (`testdata/vectors/padding.json`), con la longitud de `PAYLOAD_AGE`, `184 + P + 16·max(1, ⌈P / 65536⌉)` (§62.1): | L | P, código 1 | P, código 2 | `PAYLOAD_AGE`, código 1 / 2 | |---|---|---|---| | 0, 1, 40, 255, 256 | 256 | 256 | 456 / 456 | | 257 | 512 | 512 | 712 / 712 | | 1000 | 1024 | 1024 | 1224 / 1224 | | 8192 | 8192 | 8192 | 8392 / 8392 | | 8193 | 8448 | 8704 | 8648 / 8904 | | 65 537 | 65 792 | 67 584 | 66 008 / 67 800 | | 78 000 | 78 080 | 79 872 | 78 296 / 80 088 | | 3 000 000 | 3 000 064 | 3 014 656 | 3 000 984 / 3 015 576 | | 600 000 000 | 600 000 000 | 603 979 776 | 600 146 680 / 604 127 416 | | 2 113 929 217 | 2 113 929 472 | 2 147 483 648 | 2 114 445 768 / 2 148 008 120 | | 4 227 858 433 | 4 227 858 688 | 4 294 967 296 | 4 228 891 080 / 4 296 016 056 | | 2³² − 1 | 4 294 967 296 | 4 294 967 296 | 4 296 016 056 / 4 296 016 056 | | 2³² + 1 | 4 294 967 552 | 4 362 076 160 | 4 296 016 328 / 4 363 141 304 | | 5 000 000 000 | 5 000 000 000 | 5 033 164 800 | 5 001 220 888 / 5 034 393 784 | | 10¹² | 1 000 000 000 000 | 1 005 022 347 264 | 1 000 244 140 824 / 1 005 267 714 232 | | 2⁵² + 1 | 4 503 599 627 370 752 | 4 573 968 371 548 160 | 4 504 699 138 998 728 / 4 575 085 063 045 304 | | L_MAX | L_MAX | L_MAX | 8 939 012 353 949 880 / 8 939 012 353 949 880 | Las filas 2 113 929 217 y 4 227 858 433 son los primeros L en que fallan las operaciones de 32 bits, con signo y con `>>> 0`; en la fila 2³² − 1 aciertan por coincidencia. Las filas por encima de 2³² fijan la aritmética de más de 32 bits: en ellas, salvo L_MAX, `Padme` supera a `bloque256`. El código va sellado, pero P lo delata a veces (§55.2). --- ## 29.2 Contenido del formato 3 En formato 3 el contenido es `BODY`: una trama de 12 bytes, el área de `security` (§29.3), el head (§29.4) y los ficheros. L es la longitud de `BODY`, y el relleno de §29.1 se aplica a `BODY`: ```text PAYLOAD_PLAINTEXT = BODY || 0x00^(P − L), L = |BODY|, P = regla(L) BODY = AREA_LEN (uint32 BE) || SECURITY_LEN (uint32 BE) || HEAD_LEN (uint32 BE) || SECURITY_CBOR || 0x00^(AREA_LEN − SECURITY_LEN) || HEAD_CBOR || CONTENT ``` La trama MUST cumplir: ```text AREA_LEN = 512·k, con 1 <= k <= 128 (de 512 a 65536) 1 <= SECURITY_LEN <= AREA_LEN 1 <= HEAD_LEN <= 16777216 (16 MiB) 12 + AREA_LEN + HEAD_LEN <= L ``` y los `AREA_LEN − SECURITY_LEN` bytes que siguen a `SECURITY_CBOR` MUST valer 0x00. Cualquier violación de la trama, incluido un L menor que 12, es `ERR_INTEGRITY` en el paso 17 (§63). `SECURITY_CBOR` y `HEAD_CBOR` ocupan exactamente `SECURITY_LEN` y `HEAD_LEN` bytes: un objeto CBOR que termina antes o que no cabe en ellos es inválido, con las consecuencias de su objeto (§29.3, §29.4). `CONTENT` son los C = L − 12 − `AREA_LEN` − `HEAD_LEN` bytes restantes: los ficheros del head concatenados en su orden (§29.4). Sin ficheros, C MUST ser 0. El área: - su tamaño lo fija la versión del spec con la que se escribe, no lo que contiene ni ninguna opción de cada cápsula: un escritor de esta versión MUST escribir `AREA_LEN` = 512, y MUST escribir `SECURITY_CBOR` siempre, aunque vaya vacío (§62.1, regla 13); - un lector MUST aceptar cualquier `AREA_LEN` que cumpla la trama, sin compararlo con la versión del spec: así lee el área, quizá mayor, de un escritor de una versión posterior (§70); - por eso P no depende de lo que lleve `security` (§55.2). `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN` van dentro del plaintext de `PAYLOAD_AGE`: solo se conocen al descifrarlo y los autentica `age`. No son longitudes de trama del `.dkc`, y un lector MUST NOT reservar memoria según ellas antes de recibir los bytes que declaran (§57). Ejemplos, con el área de 512 bytes y el código 2 (informativo): | Contenido | `HEAD_LEN` | L | P | |---|---|---|---| | sin ficheros ni comentario | 53 | 577 | 768 | | un comentario de 1 byte | 56 | 580 | 768 | | un fichero `nota.txt` de 1000 bytes, con mtime | 117 | 1641 | 1792 | `SECURITY_CBOR` vacío mide 22 bytes (§29.3), así que el área lleva 490 bytes a cero. --- ## 29.3 `security` `SECURITY_CBOR` es un mapa CBOR con el perfil de §58: ```text 0 → "datekeys-security" 1 → 1 2 → author_signature (cadena de bytes con el mapa author-signature, codificado aparte) 3 → seal (cadena de bytes con el mapa seal, codificado aparte) ``` Las claves 2 y 3 son opcionales (§58.1). Cada una es una cadena de bytes de 1 a 65 536 bytes cuyo contenido es otro objeto CBOR con el perfil de §58: ```text author-signature: 0 → alg (entero sin signo, de 1 a 2³² − 1) 1 → clave pública (cadena de bytes) 2 → firma (cadena de bytes) seal: 0 → seal_type (entero sin signo, de 1 a 2³² − 1) 1 → token (cadena de bytes) ``` `security` nunca decide la apertura: ningún fallo suyo tiene código de error, detiene el paso 17 o cambia el código de otro fallo (§69.1). Su único resultado son los veredictos de §29.7. Es el objetivo 6 de §4: abrir una cápsula nunca depende de poder comprobar una firma o un sello. Esta versión no define ningún `alg` ni ningún `seal_type`. La firma de autor y el sello de tiempo llegarán en versiones posteriores, que no cambiarán la trama, el head ni el mapa exterior de `security` (§70). Quedan reservados `alg` 1 (Ed25519) y `seal_type` 1 (servicio de sellado de DateKeys), 2 (RFC 3161) y 3 (OpenTimestamps). En esta versión: - un escritor MUST escribir `security` vacío, `{0: "datekeys-security", 1: 1}`, de 22 bytes (§62.1, regla 13); - un lector MUST aceptar las claves 2 y 3 y evaluarlas como dice §29.7, lo que con esta versión da siempre «no soportado» o «ilegible». El contenido de las claves 2 y 3 se codifica aparte para que los lectores de todas las versiones validen igual el mapa exterior. Así, un `author-signature` o un `seal` mal formado solo cambia su propio veredicto, nunca el del otro, y un `alg` o un `seal_type` futuros pueden tener el tamaño que quepa en el área. Validación, en el paso 17.3, con las capas de §69.1 y sin código: 1. **Capa 2:** el mapa exterior empieza por el type tag `datekeys-security` y la versión 1. 2. **Capa 3:** el perfil de §58, la igualdad con la recodificación, el schema de arriba y que `SECURITY_CBOR` ocupe exactamente `SECURITY_LEN` bytes. Un fallo en cualquiera de las dos da el veredicto X (§29.7). El contenido de las claves 2 y 3 se valida después, al evaluar cada veredicto. --- ## 29.4 Head `HEAD_CBOR` describe el contenido de una cápsula de formato 3. Es un mapa CBOR con el perfil de §58: ```text 0 → "datekeys-head" 1 → 1 2 → salt (32 bytes) 3 → comment (texto) 4 → declared_author (texto) 5 → files (array de entradas) 6 → critical_extensions 7 → noncritical_extensions ``` Las claves 3 a 7 son opcionales (§58.1). Cada entrada de `files` es un mapa: ```text 0 → path (texto: la ruta relativa del fichero, §29.5) 1 → size (entero sin signo) 2 → start (entero sin signo) 3 → end (entero sin signo, exclusivo) 4 → sha256 (32 bytes: el SHA-256 del fichero) 5 → mtime (entero sin signo: segundos UTC, informativa) ``` En una entrada, la clave 5 es opcional y las demás, obligatorias. Reglas: - **Versión.** En formato 3 el head MUST tener la versión 1. Una versión nueva del head exige un formato nuevo (§22), que un lector anterior rechaza en el paso 2, sin red. La versión se sigue comprobando como defensa: otra es `ERR_UNSUPPORTED_VERSION` en el paso 17, capa 2, igual que la versión de `CONTROL_CBOR` en el paso 14. - **Sal.** `salt` son 32 bytes de un CSPRNG, nuevos en cada cápsula (§62.1, regla 14). Hacen de `SHA-256(HEAD_CBOR)` un compromiso que oculta: quien ve ese hash sin el head 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 puede compararlos con los de sus ficheros. - **Comentario y autor declarado.** `comment` mide de 1 a 16 384 bytes, y `declared_author`, de 1 a 256; sus caracteres siguen §29.6. El autor declarado es un texto del creador y no prueba nada (§36.1, §55.1). - **Ficheros.** `files` tiene de 1 a 65 535 entradas, en orden ascendente estricto de los bytes UTF-8 de `path` (§29.5, R8). Sin ficheros, la clave 5 se omite. - **Maquetación.** Con C la longitud de `CONTENT` (§29.2) y las entradas en su orden: start₀ = 0; startᵢ = endᵢ₋₁; y endᵢ − startᵢ = sizeᵢ. `size`, `start` y `end` son como mucho L_MAX. Un lector comprueba las igualdades restando, sin sumar valores que no ha comprobado. El fichero i son los bytes startᵢ a endᵢ − 1 de `CONTENT`, y su SHA-256 MUST ser su `sha256`. Que el `end` de la última entrada sea C, o que C sea 0 sin ficheros, se comprueba en el paso 17.5 (§63). - **mtime.** La fecha de modificación del fichero en su origen, en segundos UTC desde 1970-01-01, de 0 a 253402300799 (9999-12-31T23:59:59Z). Es informativa: no prueba nada, y ningún lector decide nada por ella (§55.1). - **Extensiones.** Las claves 6 y 7 siguen §54. Una extensión solo vale en el head si está registrada para él (§72). - **Tamaño.** `HEAD_CBOR` mide como mucho 16 MiB (§29.2). El head no lleva tipo de contenido, hash global, permisos ni fecha de creación. Los límites de esta sección y de §29.2, §29.5 y §29.6 quedan fijados con el formato 3: subirlos exige un formato nuevo (§22), porque un lector de esta versión rechazaría la cápsula después de pedir el release. Validación, en el paso 17.4, con las capas de §69.1: 1. **Capa 2:** type tag y versión. Un type tag ajeno es `ERR_NON_CANONICAL_CBOR`; una versión distinta de 1, `ERR_UNSUPPORTED_VERSION`. 2. **Capa 3:** el perfil de §58, la igualdad con la recodificación, el schema de arriba con sus tamaños y rangos, que `HEAD_CBOR` ocupe exactamente `HEAD_LEN` bytes, R1 y R8 de §29.5 y las reglas de §54 para las extensiones → `ERR_NON_CANONICAL_CBOR`. 3. **Capa 4,** en orden de clave: - claves 3 y 4: los caracteres de §29.6 → `ERR_HEAD_INVALID`; - clave 5: las reglas de §29.5 y la maquetación, en el orden de §29.5 → `ERR_HEAD_INVALID`; - clave 6: las extensiones críticas → `ERR_EXTENSION_CRITICAL_UNKNOWN`, y si todas son conocidas, `ERR_EXTENSION_DATA_INVALID` (§54). `ERR_HEAD_INVALID` separa un head bien codificado cuyos campos incumplen sus reglas de un CBOR roto. --- ## 29.5 Rutas `path` es la ruta relativa de un fichero dentro de la cápsula. Estas reglas hacen que ningún lector escriba fuera de su carpeta, y que dos entradas no puedan confundirse ni colisionar al extraerlas en Windows, macOS o Linux, ni al abrir el ZIP que las lleva (§56). R1 y R8 son de la capa 3 y se comprueban sobre todo el array; las demás, de la capa 4. Así, `["b/..", "a"]` es `ERR_NON_CANONICAL_CBOR` por R8, aunque «..» incumpla también R3. - **R1.** `path` mide de 1 a 1024 bytes UTF-8. - **R2.** Se divide en segmentos por '/' (U+002F): de 1 a 32 segmentos, ninguno vacío. No hay rutas absolutas ni '//'. - **R3.** Cada segmento mide de 1 a 255 bytes y no es «.» ni «..». Al quitarle los puntos de la lista blanca de R4, tampoco queda vacío, ni queda «.» ni «..»: HFS+ ignora ZWNJ y ZWJ al comparar nombres, así que allí un «.» seguido de ZWJ se vería y se compararía como «.». Además, el NFD del segmento (§29.5.1) 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.** Ningún punto de código de un segmento es: - de C0 (U+0000 a U+001F) ni de U+007F a U+009F; - `"`, `*`, `:`, `<`, `>`, `?`, `\` ni `|`; - U+2028 ni U+2029; - de la propiedad `Default_Ignorable_Code_Point`, salvo los de la lista blanca: U+200C (ZWNJ), U+200D (ZWJ), U+FE0E (VS15) y U+FE0F (VS16), que hacen falta en emoji y en varias escrituras. La propiedad incluye los controles bidi (U+061C, U+200E, U+200F, U+202A a U+202E y U+2066 a U+2069), U+00AD, U+034F, U+200B, U+2060 a U+206F, U+3164, U+FEFF, los demás selectores de variante y las etiquetas U+E0000 a U+E007F, que permiten nombres visualmente idénticos; - de U+F000 a 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»; - de la categoría general Cn, que incluye los no-caracteres y los puntos sin asignar: una versión futura de Unicode podría plegarlos o volverlos ignorables. - **R4b.** Los puntos de la lista blanca de R4 solo van donde su uso es conforme: - VS15 y VS16, justo detrás de un carácter para el que `emoji-variation-sequences.txt` (§29.5.1) define la secuencia con ese selector; - ZWJ y ZWNJ, nunca al principio ni al final del segmento, ni justo detrás de otro ZWJ o ZWNJ. Así un segmento no puede llevar secuencias invisibles, que esconden datos: Unicode 18.0.0 advierte de que las secuencias de selectores de variante invisibles sirven para atacar aplicaciones de IA. Los emoji compuestos siguen valiendo, como la bandera arcoíris, U+1F3F3 U+FE0F U+200D U+1F308. - **R5.** Ningún segmento empieza por U+0020 ni termina en U+0020 o en '.'. - **R6.** La parte del segmento anterior a su primer '.', o todo el segmento si no lo tiene, sin los U+0020 finales y sin distinguir mayúsculas ASCII, no es CON, PRN, AUX, NUL, CONIN$, CONOUT$, de COM0 a COM9, de LPT0 a LPT9, COM¹, COM², COM³, LPT¹, LPT² ni LPT³. - **R6b.** Contando puntos de código, no se cumplen a la vez estas tres condiciones, que describen un alias 8.3 de NTFS: - el segmento tiene como mucho un '.'; - la parte anterior al '.', o todo el segmento si no lo tiene, mide de 1 a 8 y cumple la expresión regular `^[^.]*~[0-9]{1,6}$`; - la parte posterior al '.' mide de 0 a 3. En NTFS, «ABCDEF~1» puede resolver a «ABCDEFGHIJ», y extraer las dos fusionaría dos carpetas distintas. La expresión regular no usa búsquedas anticipadas, que el paquete `regexp` de Go no admite; en JavaScript lleva la bandera `u`. - **R6c.** Para cada tabla best-fit de §29.5.1, la proyección del segmento, que sustituye cada punto no ASCII que la tabla lleva a un byte ASCII por ese byte y deja los demás como están, no contiene '/', '\', ':' ni U+0000, y cumple R3, R5, R6 y R6b. Cubre, entre otros, ∖, ∶, ¥ en cp932, ₩ en cp949, ´ en cp1253, las formas de ancho completo de '/', '\', ':' y '.', «CON.txt», y U+3000 al principio o al final de un segmento, que bestfit1252 lleva a U+0020. Solo se proyecta lo que la tabla lleva a ASCII: el '?' que Windows pone por defecto a lo que no convierte no es sintaxis de ruta. 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. - **R7.** La clave de un segmento s es NFD(pliegue(NFD(s′))), con s′ el segmento sin los puntos de la lista blanca de R4 y el pliegue de las entradas C y F de CaseFolding más U+0131 → U+0069, como NTFS (§29.5.1). Los puntos de la lista blanca se quitan antes de normalizar porque tienen clase de combinación 0: entre dos marcas combinantes, cambiarían su orden canónico. Sobre el árbol cuyos nodos son las rutas y sus prefijos por segmentos: - dos hermanos no tienen la misma clave. Colisionan, por ejemplo, «A.txt» y «a.txt», un nombre en NFC y en NFD, «Fotos/a» y «fotos/b», «Straße» y «STRASSE», «K» y el signo de Kelvin, y «ab» con y sin ZWNJ; - ninguna ruta es a la vez un fichero y el prefijo de otra, comparando claves, como «A» y «a/b». - **R8.** Las rutas están en orden estrictamente ascendente de sus bytes UTF-8, comparados como en §54. Una implementación MUST NOT comparar unidades UTF-16 ni usar una colación: con el operador `<` de JavaScript, U+FF5E y U+1F600 quedan al revés que en UTF-8, y `localeCompare` ordena por colación. - **R9.** Hay como mucho 65 535 carpetas implícitas, es decir, prefijos propios distintos de las rutas. - **R10.** La clave de R7 del primer segmento no empieza por «.datekeys-». La implementación de referencia escribe el árbol en `DIR/.datekeys-*` antes de moverlo a su destino (§56), y una entrada con ese nombre chocaría con él. Orden de las comprobaciones de la capa 4 en la clave 5: para cada entrada, en su orden, R2, R3, R4, R4b, R5, R6, R6b, R6c, R10 y la maquetación de §29.4; después de todas las entradas, R7 y R9. Todas dan `ERR_HEAD_INVALID`, y la primera que falla decide. ### 29.5.1 Tablas R3, R4, R4b, R6c, R7 y la regla de invisibles de §29.6 usan datos fijos, no los de la plataforma: - **Unicode 18.0.0:** `UnicodeData.txt`, con la categoría general, la clase de combinación canónica y las descomposiciones canónicas; `DerivedCoreProperties.txt`, con `Default_Ignorable_Code_Point`; `CaseFolding.txt`, con sus entradas C y F; `emoji/emoji-variation-sequences.txt`, con los caracteres que admiten VS15 y VS16 (R4b); y NFD según UAX #15, con la descomposición algorítmica de Hangul. - **WindowsBestFit,** de unicode.org: los quince ficheros `bestfit874.txt`, `bestfit932.txt`, `bestfit936.txt`, `bestfit949.txt`, `bestfit950.txt`, de `bestfit1250.txt` a `bestfit1258.txt` y `bestfit1361.txt`, con la conversión de Unicode a su código de página de su sección `WCTABLE`. Una implementación MUST aplicar las tablas generadas de esos ficheros, fijados por el SHA-256 de la tabla de abajo, y MUST NOT usar las funciones de Unicode de su plataforma para estas reglas: `normalize`, `toLowerCase`, las clases `\p{…}` de las expresiones regulares, el paquete `unicode` de Go o `golang.org/x/text`. Su versión de Unicode cambia con cada motor: Go 1.26.8 trae la 15.0.0, y Node 24.9, la 16.0. La implementación de referencia genera desde esos ficheros el código de las dos implementaciones, y sus pruebas comprueban los digests. Las rutas de la tabla son las de los ficheros bajo `https://www.unicode.org/Public/18.0.0/ucd/` y `https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/`. | Fichero | SHA-256 | |---|---| | `unicode/18.0.0/UnicodeData.txt` | `0736451de439ae7baf1425136617da495e09ee5afbe6e394374db7009ea08950` | | `unicode/18.0.0/DerivedCoreProperties.txt` | `09c928886a178fcafd93c29e4bd59073a058e5a100b716d425cb563ab50f68c9` | | `unicode/18.0.0/CaseFolding.txt` | `a004797658a457bec4dc11683e39f69249ea3b595b752dbea6721c4c9f587b0d` | | `unicode/18.0.0/emoji/emoji-variation-sequences.txt` | `ff1707564aa1f1b2fcf4ec92d609d4cb26940bc0d4dcf07f5328ad6879e84da3` | | `bestfit/bestfit874.txt` | `663f43ca662e037c4534cb16298b560f29ce29c27b49b3589601ec3d97dd89fd` | | `bestfit/bestfit932.txt` | `2614cfea35c3c86c41d33198793a84ca44edee3cf0ee0013a61a43fba4ece331` | | `bestfit/bestfit936.txt` | `e5070a2d6ad26619f5872ddbe64d3381c11620af5adbb04cda0f0abb1a91fdae` | | `bestfit/bestfit949.txt` | `50e13b60ea8fda66a8223ecc85270e0f182303222244e2345d3d57f3e839d20a` | | `bestfit/bestfit950.txt` | `cf8c23389a42a226ea707f7ec32c665556d1fc3364db25bd765ce64d54eaee2a` | | `bestfit/bestfit1250.txt` | `cef9f171e67b09445bcb3f9ffccdc89418250ff825f1bd2d29a92d2074d7a53b` | | `bestfit/bestfit1251.txt` | `59ec85612ff908d9da0e877893c935941e56b13a2882b4fb9c9599be3d1ce4e7` | | `bestfit/bestfit1252.txt` | `72ea23c939c5b26fae7aded0207b327e2f3902d7d3c168d7087f5cfc38ee76a9` | | `bestfit/bestfit1253.txt` | `ea80c442aff7f09b36da6335f85f8e527f51c146beeb9825ec00d1b6ca99a99e` | | `bestfit/bestfit1254.txt` | `3d02512087634dc493b720992b590277736ffb2d5b0b665d69b6b9727e2c361a` | | `bestfit/bestfit1255.txt` | `fdd4bdda74f6571d89171b0070ac052cd3714c395dc3d1799bcd5e4a4da6f83a` | | `bestfit/bestfit1256.txt` | `745c447ada04a838da8bea406c13f446c7453b6371e8c6c7863a632443d56007` | | `bestfit/bestfit1257.txt` | `b8c5d7f3b8c25c3d5625d44dd3d6ee7a06e652ddf77373d050282c1cb7517366` | | `bestfit/bestfit1258.txt` | `5d52a9357b7d6b5b5014ed5a51be0ff9809b0c33625793d2a4feaf502e0682f1` | | `bestfit/bestfit1361.txt` | `7dcda2d5d2cfc5ddf43757d589a0106e020ef0d9b84de47f08e18297ae0fe1ec` | Un punto de código posterior a Unicode 18.0.0 es Cn para estas tablas: R4 lo rechaza, y el escritor lo dice con un mensaje que nombra el carácter (§62.1, regla 15). La versión de Unicode queda fijada con el formato 3. Pasar a otra cambiaría qué rutas se aceptan, algo que un lector de esta versión solo notaría después de pedir el release, así que exige un formato nuevo (§22). --- ## 29.6 Texto El comentario y el autor declarado del head (§29.4) son UTF-8 válido. «Espacio» es U+0020. Una implementación MUST NOT usar `trim`, `\s`, `TrimSpace` ni `unicode.IsSpace` para estas reglas. - **Comentario.** Ningún punto de código es de U+0000 a U+0008, de U+000B a U+001F, de U+007F a U+009F, de U+202A a U+202E, de U+2066 a U+2069, U+2028, U+2029, U+FEFF ni un no-carácter. Se admiten TAB y LF; el escritor convierte CR LF, y un CR suelto, en LF. - **Autor declarado.** Lo mismo que el comentario, y además ni TAB, ni LF, ni U+061C, U+200E ni U+200F. No empieza ni termina en U+0020. - **Invisibles.** Ni el comentario ni el autor declarado llevan puntos de la propiedad `Default_Ignorable_Code_Point` salvo los de la lista blanca de R4, y esos van solo donde R4b los admite, con cada línea en el papel del segmento. Quedan fuera las etiquetas U+E0000 a U+E007F y los selectores de variante fuera de una secuencia de emoji, con los que se esconde un texto que no se ve en pantalla pero que un modelo de IA sí lee. Por eso quedan fuera también las banderas de subdivisión, como la de Escocia, que usan etiquetas. Así, ningún texto ni ruta puede llevar ESC ni un control C1 a un terminal, cambiar la dirección del texto que lo sigue ni esconder un texto invisible. --- ## 29.7 Veredictos y presentación Un lector evalúa `security` en el paso 17.6 y presenta el resultado, tras el paso 18, como veredictos. Un veredicto nunca impide abrir la cápsula. 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 (§29.3) | X | X | | Sin clave 2 | F0 | no cambia | | El contenido de la clave 2 no decodifica o incumple el schema de `author-signature` | F1 | no cambia | | `alg` que el lector no implementa | F1 | no cambia | | Sin clave 3 | no cambia | S0 | | El contenido de la clave 3 no decodifica o incumple el schema de `seal` | no cambia | S2 | | `seal_type` que el lector no implementa | no cambia | S1 | Para la firma y para el sello por separado, decide la primera fila de la tabla que se cumple, de arriba abajo: `alg` y `seal_type` solo se leen de un contenido que decodifica y cumple su schema. Así, un `seal` con una clave desconocida, o con `seal_type` 0, es S2 y no S1. X es una sola línea, en lugar de la de la firma y la del sello. Esta versión no implementa ningún `alg` ni ningún `seal_type` (§29.3), así que un lector solo llega a X, F0, F1, S0, S1 y S2. Las versiones que definan la firma y el sello añadirán sus propios veredictos sin cambiar los de esta tabla. | Veredicto | Significado | Texto | |---|---|---| | X | `security` ilegible | «No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada.» | | F0 | sin firma | «Sin firma de autor.» | | F1 | firma ilegible o no soportada | «No se ha comprobado ninguna firma: trátala como no firmada.» | | S0 | sin sello | ninguno | | S1 | sello no soportado | «Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada.» | | S2 | sello ilegible | «El sello de tiempo es ilegible: no prueba nada.» | El SDK oficial MUST usar los textos de esta tabla. Otra implementación MUST usar esos textos o una traducción que no afirme más que ellos. Un lector que muestra el contenido de una cápsula de formato 3: - MUST mostrar primero los veredictos, y después el autor declarado y el comentario, marcados como texto del creador que nadie ha comprobado. El SDK oficial MUST usar «autor declarado (texto del creador, sin comprobar)» y un recuadro titulado «Comentario del creador (sin comprobar)»; - MUST mostrar las rutas solo como texto, aisladas de la dirección del texto que las rodea; - en una salida de texto, como la de una CLI, sea o no un terminal, MUST: - expandir cada TAB del comentario a espacios, hasta la siguiente columna múltiplo de 8; - partir 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; - contar el ancho por lo alto: 1 por carácter ASCII imprimible y 2 por cualquier otro punto de código; - poner delante de cada trozo `│ ` (U+2502 y U+0020), que cuenta 3 columnas porque U+2502 tiene anchura ambigua y ocupa 2 en un terminal CJK; - repetir los veredictos al final, después del comentario y de las rutas que muestre. Así el terminal nunca parte una línea por su cuenta, y ninguna línea del creador aparece sin prefijo ni puede imitar un veredicto; - SHOULD avisar de los ficheros `.lnk`, `.url`, `.library-ms`, `.searchConnector-ms` y `desktop.ini`, de una carpeta `.git`, de los ejecutables y de un '-' inicial, comparando la clave de R7 del segmento y de su extensión, así que «.GIT» o «Informe.LNK» también avisan. --- ## 30. Por qué PAYLOAD_AGE es un fichero age completo V1 NO expone ni reimplementa STREAM como un subformato DateKeys. El STREAM de ChaCha20-Poly1305, chunks de 64 KiB, nonce y final-chunk marker forman parte interna del formato `age`. DateKeys trata `PAYLOAD_AGE` como bytes de un fichero `age` estándar. Esto permite reutilizar: - header MAC; - recipient stanza; - file key de 16 bytes; - STREAM; - detección de truncado; - vectores y librerías existentes. El relleno de §29.1 forma parte del plaintext de `age`: no existe un subformato DateKeys para él. --- ## 30.1 Binding `CONTROL_CBOR` ↔ `PAYLOAD_AGE` El binding entre control y payload se obtiene mediante `I_PAYLOAD`. `CONTROL_CBOR` contiene exactamente la identity privada `I_PAYLOAD` generada para esa cápsula. Esa identity solo abre el `PAYLOAD_AGE` cifrado para su recipient correspondiente. Por tanto: ```text CONTROL_A + PAYLOAD_AGE_B ``` MUST fallar porque `I_PAYLOAD_A` no puede desenvolver la file key de `PAYLOAD_AGE_B`. Este binding criptográfico es independiente de cualquier digest auxiliar del fichero. El binding solo separa dos cápsulas si sus `I_PAYLOAD` son distintas: `I_PAYLOAD` MUST ser nueva para cada cápsula (§29). En los formatos 2 y 3 el control fija, además, la longitud y el relleno del plaintext de `PAYLOAD_AGE`: su L y su código dan P (§29.1), y el paso 17 rechaza un plaintext de otra longitud o con un byte de relleno distinto de 0x00 (`ERR_INTEGRITY`). Esta comprobación complementa el binding de `I_PAYLOAD`, no lo sustituye. Aporta determinismo: un solo plaintext válido para cada control y cada payload, en el que todos los lectores coinciden. No añade autenticidad: quien conoce `FK_TIME` o `FK_ACCESS` puede sellar otro control con otra L de la misma P, y quien conoce `I_PAYLOAD` puede cifrar otro plaintext (§55.1). En formato 3, además, el head fija el tamaño, la posición y el SHA-256 de cada fichero dentro de `BODY` (§29.4), y el paso 17 lo comprueba todo. Tampoco añade autenticidad: quien conoce `I_PAYLOAD` puede cifrar otro `BODY` coherente con su propio head. --- ## 31. CONTROL_CBOR Una vez abierto, el control es **siempre Deterministic CBOR**. Una codificación no canónica MUST rechazarse aunque `CONTROL_CBOR` no participe directamente en un hash. Schema base: ```text 0 → "datekeys-control" 1 → versión de schema: 1 en formato 1, 2 en formato 2 y 3 en formato 3 2 → header_binding (32 bytes) 3 → payload_identity (32 raw bytes, I_PAYLOAD) 4 → critical_extensions 5 → noncritical_extensions 6 → payload_length (8 bytes: L, entero sin signo big-endian) 7 → padding (código de relleno: 1 = bloque256, 2 = reforzado) ``` Reglas del schema: - la versión de schema (clave 1) MUST ser la que la tabla de §22 asigna al formato de la cápsula, el `VERSION` de su PRELUDE: 1 en formato 1, 2 en formato 2 y 3 en formato 3. Otra es `ERR_UNSUPPORTED_VERSION` en el paso 14 (§69.1, capa 2); - las claves 4 y 5 son opcionales y se omiten cuando no hay extensiones (§58.1); significan lo mismo en las tres versiones; - las claves 6 y 7 MUST existir en las versiones 2 y 3 y MUST NOT existir en la versión 1. Las versiones 2 y 3 tienen las mismas claves; en la 3, L es la longitud de `BODY` (§29.2); - `payload_length` MUST ser una cadena de bytes de exactamente 8 bytes con L, la longitud del contenido, como entero sin signo big-endian, con ceros a la izquierda; su valor MUST ser como mucho L_MAX = 2⁵³ − 2⁴⁶ (8936830510563328, §29.1). L = 0 se codifica `48 0000000000000000`, y L = 2³² + 1, `48 0000000100000001`; - `padding` MUST ser el entero sin signo 1 o 2 (§29.1); - cualquier violación de las reglas de las claves 6 y 7 es `ERR_NON_CANONICAL_CBOR` (§57). `payload_length` tiene tamaño fijo para que la longitud de `CONTROL_CBOR`, visible en `SEALED_CONTROL_LEN`, no dependa de L (§55.2): un entero CBOR en su forma más corta ocuparía 1, 2, 3, 5 o 9 bytes según L. Un `CONTROL_CBOR` de versión 2 o 3 sin extensiones mide 103 bytes: ```text a6 00 70 "datekeys-control" 01 <02 o 03> 02 58 20 03 58 20 06 48 07 ``` L y P no son longitudes de trama: un lector MUST NOT reservar memoria ni disco según L o P antes de recibir el plaintext que los respalda (§57). No contiene el payload grande. DateKeys V1 utiliza **un único mecanismo de extensión**. No existe un campo core separado para aplicaciones o semánticas superiores. Cualquier semántica adicional se registra como una extensión identificada por namespace. Las claves 6 y 7 son campos del protocolo, no semántica de aplicación. Una extensión usa conceptualmente: ```text 0 → extension_id 1 → extension_version 2 → data (opcional) ``` Reglas: - `extension_id` MUST ser una cadena de texto UTF-8 válida de al menos 1 byte; - `extension_version` MUST ser un entero sin signo entre 0 y 2³² − 1; - `(extension_id, extension_version)` identifica el schema; - `data`, cuando existe, MUST ser una cadena de bytes (`bstr`) no vacía, opaca para el protocolo base (§54); - una extensión sin datos MUST omitir la clave 2; - cada array de extensiones MUST contener entre 1 y 64 extensiones; - los elementos de cada array MUST estar en orden estrictamente ascendente de los bytes UTF-8 de `extension_id`, según la comparación de §54, lo que también prohíbe repetir un `extension_id` dentro del array; el orden no tiene semántica; - un mismo `extension_id` MUST NOT aparecer simultáneamente en `critical_extensions` y `noncritical_extensions` dentro del mismo objeto; - por esas dos reglas, un `extension_id` aparece como mucho una vez en cada objeto, aunque cambie `extension_version`; si un schema necesita varios valores, los lleva dentro de su `data`; - una extensión crítica desconocida MUST provocar rechazo; - una extensión no crítica desconocida MAY ignorarse; - una extensión conocida que aparece en un objeto o array para el que no está registrada se trata allí como desconocida (§54, §72). El protocolo base no decodifica ni valida el contenido de `data` (§54). --- ## 32. Política `time_only` Construcción: ```text CONTROL_CBOR ↓ OUTER_TIME_AGE ``` donde: ```text OUTER_TIME_AGE = age file recipient = tlock(DateKey) plaintext = CONTROL_CBOR ``` `OUTER_TIME_AGE` MUST contener exactamente un stanza, de tipo tlock. `age` genera: ```text FK_TIME = 16 random bytes ``` y tlock envuelve `FK_TIME` para la ronda futura. No existe `INNER_ACCESS_AGE`. --- ## 33. Política `time_and_key` Construcción: ```text CONTROL_CBOR ↓ INNER_ACCESS_AGE ↓ OUTER_TIME_AGE ``` Primero: ```text INNER_ACCESS_AGE = age file recipients = X25519: en los formatos 2 y 3, 16, credenciales y señuelos (§39) plaintext = CONTROL_CBOR ``` `INNER_ACCESS_AGE` MUST contener stanzas de tipo X25519, exactamente uno por recipient: uno o más en formato 1, y exactamente 16 en los formatos 2 y 3 (§39). `age` genera: ```text FK_ACCESS = 16 random bytes ``` Después: ```text OUTER_TIME_AGE = age file recipient = tlock(DateKey) plaintext = exact bytes of INNER_ACCESS_AGE ``` `age` genera: ```text FK_TIME = 16 random bytes ``` La política implementa: ```text TIME AND KEY ``` No dos envolturas paralelas. --- ## 34. `SEALED_CONTROL` En `.dkc`: ```text SEALED_CONTROL = exact bytes of OUTER_TIME_AGE ``` Debe ser siempre un fichero age completo. Para `time_only`, el plaintext interno es `CONTROL_CBOR`. Para `time_and_key`, el plaintext interno es `INNER_ACCESS_AGE`. --- ## 35. tlock strict mode El recipient/identity tlock V1 MUST usar: - chain hash pinneado; - public key pinneada; - scheme pinneado; - round esperado. DateKeys MUST desactivar confianza automática en chain hash suministrado por el ciphertext. La root of trust procede del Provider Profile local. El stanza tlock tiene exactamente dos argumentos tras el tipo, la ronda y el chain hash, que se comparan como cadenas con las reglas del paso 8 de §63. --- ## 36. Comprobación política ↔ estructura Tras abrir `OUTER_TIME_AGE`: ### Si `access_policy = time_only` el resultado MUST ser directamente un `CONTROL_CBOR` canónico válido. Un resultado que empieza por la línea de versión de `age` (`age-encryption.org/v1` seguida de LF) es `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12; cualquier otro se decodifica como `CONTROL_CBOR` en el paso 14, con los códigos de §69.1. ### Si `access_policy = time_and_key` el resultado MUST ser un fichero age v1 válido cuyo header contenga uno o más stanzas en formato 1, o exactamente 16 en los formatos 2 y 3 (§39), todos ellos de tipo X25519, y sin dos stanzas de un solo argumento tras el tipo con el mismo argumento, que en un stanza X25519 es su share efímero. Un resultado cuya cabecera no sigue la gramática de §28.1 no lo es. `age` genera un share efímero nuevo para cada stanza X25519: dos stanzas con el mismo share no los produce ningún encoder conforme y, para un mismo recipient, serían dos stanzas para él (§33). Es la parte de «exactamente un stanza por recipient» que se comprueba sin secretos; el resto, y la forma de cada stanza X25519, los comprueba el paso 13 de §63 con las identities ofrecidas. En los formatos 2 y 3 el límite de 1024 stanzas del parser de la implementación de referencia (§74) no cambia ningún código: una cabecera que lo supera también tiene más de 16 stanzas, y las dos causas dan `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12. Si la estructura no coincide: ```text ERR_POLICY_STRUCTURE_MISMATCH ``` --- ## 36.1 Semántica de autenticidad `time_only` **NO proporciona autenticidad del creador**, ni antes ni después de madurar la condición temporal, salvo que una extensión de firma explícita añada esa propiedad. La falsificación es posible desde el momento de creación: cifrar hacia una ronda futura solo requiere la clave pública del Provider Profile y la condición temporal pública. Además, `header_binding` se calcula únicamente a partir de bytes públicos (`PRELUDE` y `PUBLIC_HEADER`). Un tercero puede construir otro `CONTROL_CBOR` con un `header_binding` correcto, generar su propio `I_PAYLOAD`, crear su propio `PAYLOAD_AGE` y sellar ese control hacia la misma DateKey sin esperar a que la ronda madure. Por tanto `capsule_id`, DateKey y `header_binding` proporcionan **coherencia interna**, no autoría (§55.1). `time_and_key` añade una barrera adicional de acceso: un tercero que no posea la identity/capacidad requerida no puede abrir el `INNER_ACCESS_AGE`. Esta propiedad **NO sustituye una firma del creador** y no debe presentarse como prueba de autoría. Los señuelos de §39 no añaden ninguna barrera de acceso ni ninguna autenticidad. En formato 3, el autor declarado del head (§29.4) es un texto del creador y no prueba autoría, y en esta versión `security` no prueba nada (§29.3). --- ## 37. X25519 recipient V1 V1 adopta el recipient X25519 estándar de `age`. Una identity X25519: ```text I = 32 random bytes from CSPRNG ``` El recipient correspondiente se deriva según la especificación `age`. No se define un KEM propio. Un recipient X25519 son 32 bytes: la coordenada u de RFC 7748 en little-endian. El escritor MUST rechazar (§62.1): - un recipient no canónico: con el bit 255 a 1, o con u ≥ p = 2²⁵⁵ − 19; - un recipient de orden bajo: aquel para el que X25519(k, u) es la cadena de 32 ceros. Con el recorte de escalares de RFC 7748, el resultado no depende de k. X25519 ignora el bit 255 y reduce u módulo p, pero `age` pone los 32 bytes recibidos en el salt de HKDF, y la identity, su clave pública canónica: el stanza de un recipient no canónico no lo abre nadie. Con un recipient de orden bajo, el secreto compartido es cero: `age` se niega a cifrar, y una librería que no lo comprobara dejaría abrir ese stanza a cualquiera. Las reglas de §63 no detectan ninguno de los dos casos: el stanza no contiene el recipient. La lista no es exhaustiva. Una u canónica que no está en la curva, sino en su torsión cuadrática (twist), tampoco es la clave pública de ninguna identity, y su stanza tampoco lo abre nadie. Un escritor MAY rechazarla comprobando que u³ + 486662·u² + u es un cuadrado módulo p (símbolo de Legendre). --- ## 38. Portable Access Key Cuando el creador quiere generar una credencial portable, genera: ```text I_ACCESS = 32 random bytes R_ACCESS = corresponding X25519 recipient ``` `R_ACCESS` se usa como recipient de `INNER_ACCESS_AGE`. En los formatos 2 y 3 cuenta como una de sus credenciales, como mucho 16, y ocupa una posición aleatoria entre los 16 stanzas (§39). `I_ACCESS` se almacena como **32 bytes crudos** dentro del `.dkk`. Una `I_ACCESS` portable MUST generarse específicamente para **una única `capsule_id`** y MUST NOT reutilizarse en otra DateKeyCap. La reutilización convertiría una misma `.dkk` en capacidad válida para varias cápsulas y rompería el aislamiento esperado entre objetos. La representación Bech32 `AGE-SECRET-KEY-...` MAY mostrarse/exportarse para interoperabilidad humana, pero NO es la representación binaria canónica del campo. --- ## 39. Recipients de `INNER_ACCESS_AGE` Una **credencial** es un par X25519 que abre `INNER_ACCESS_AGE`: el escritor recibe su recipient, o genera `R_ACCESS` (§38), y el lector ofrece su identity, suelta o dentro de una `.dkk` (§63, paso 9). Contar credenciales es contar recipients distintos. En los formatos 2 y 3: - `INNER_ACCESS_AGE` MUST contener exactamente 16 stanzas X25519, uno por hueco; - una cápsula `time_and_key` MUST tener entre 1 y 16 credenciales, sin repetir ninguna; - cada hueco que no ocupa una credencial lleva un **señuelo**: la clave pública de una identity X25519 nueva que el escritor genera con un CSPRNG y descarta en el acto. La clave privada de un señuelo MUST NOT almacenarse, registrarse ni entregarse; ningún señuelo se reutiliza ni se deriva de otro valor; - el orden de los 16 stanzas en la cabecera MUST ser una permutación uniformemente aleatoria, obtenida de un CSPRNG sin sesgo —por ejemplo, Fisher–Yates con muestreo por rechazo—, independiente de qué huecos son credenciales y del orden en que el escritor las recibe. La regla vale para la cabecera escrita, no para la llamada a `age`: `filippo.io/age` y `age-encryption` escriben los stanzas en el orden de sus recipients, y una librería que los ordenara o agrupara la rompería; - la permutación y qué huecos son señuelos MUST NOT almacenarse, registrarse ni entregarse, salvo en los vectores oficiales de prueba (§67): revelan el número de credenciales; - toda identity real abre exactamente un stanza, y ninguna abre el de un señuelo. En formato 1, `INNER_ACCESS_AGE` contiene (MUST, §33) uno o más stanzas X25519, uno por recipient, sin señuelos ni orden prescrito: es la regla de la v0.8.2. En todos los formatos, todos los stanzas envuelven la misma: ```text FK_ACCESS ``` incluidos los de los señuelos. Por tanto el `CONTROL_CBOR` y `PAYLOAD_AGE` no se duplican. Un señuelo envuelve la misma `FK_ACCESS`: su clave privada abre la cápsula como una credencial mientras exista. Por eso se descarta en el acto. Nota informativa: las APIs de `age` no permiten borrarla. `generateX25519Identity` de `age-encryption` devuelve una cadena `AGE-SECRET-KEY-1…` inmutable; `age.X25519Identity` de Go guarda el escalar sin forma de borrarlo, y la implementación de referencia obtiene sus bytes a través de esa cadena. Un escritor puede generar el escalar en un buffer que pueda borrar, calcular X25519(k, 9), borrar el buffer y dar a `age` solo el recipient. Un señuelo es indistinguible de una credencial. Un stanza X25519 de `age` contiene un share efímero nuevo y la file key cifrada con una clave derivada del secreto compartido con el recipient (C2SP age, §77); el recipient no aparece en él, y los dos tienen la misma forma y la misma longitud, 98 bytes. Es el anonimato de recipient de `age` X25519, bajo el supuesto de Diffie–Hellman en X25519: saber si un stanza es para una clave pública dada exige el secreto compartido, es decir, la clave privada del recipient o el escalar efímero, que `age` descarta. Ni siquiera quien conoce `FK_ACCESS` puede comprobarlo. Quien tiene una credencial sabe cuál es su stanza y nada de los otros 15; varias credenciales juntas solo saben cuántos stanzas abren entre todas. §55.2 enumera lo que los formatos 2 y 3 ocultan y lo que impiden comprobar. --- ## 40. Framing `.dkk` V1 ```text offset size field 0 4 MAGIC = "DKK1" 4 1 VERSION = 1 5 1 FLAGS = 0 6 2 RESERVED = 0 8 4 BODY_LEN (uint32 BE) 12 ... BODY_CBOR ``` V1 MUST exigir: ```text FLAGS == 0 RESERVED == 0 1 <= BODY_LEN <= 16777216 (16 MiB) ``` La `.dkk` conserva `VERSION` 1 con los tres formatos de `.dkc`: no contiene nada que dependa del número de credenciales ni del relleno. `BODY_CBOR` son exactamente los `BODY_LEN` bytes que siguen al prelude, y el fichero termina con ellos. La trama se valida en este orden, y el primer fallo decide el código (§69.1): 1. menos de 4 bytes, o `MAGIC` distinto de `DKK1` → `ERR_INVALID_MAGIC`; 2. `DKK1` seguido de menos de 8 bytes, un prelude truncado → `ERR_INTEGRITY`; 3. `VERSION` distinto de 1 → `ERR_UNSUPPORTED_VERSION`; 4. `FLAGS` o `RESERVED` distintos de 0 → `ERR_INVALID_FLAGS`; 5. `BODY_LEN` fuera de su rango; una longitud 0 queda fuera, como en §22 → `ERR_INTEGRITY`; 6. menos de `BODY_LEN` bytes tras el prelude, o algún byte después de `BODY_CBOR` → `ERR_INTEGRITY`. Solo después se decodifica `BODY_CBOR`, con las capas 2 a 4 de §69.1. --- ## 41. `.dkk` BODY_CBOR Schema: ```text 0 → "datekeys-access-key" 1 → 1 2 → credential_id (16 random bytes) 3 → capsule_id (16 bytes) 4 → access_type 5 → access_material 6 → verification_metadata 7 → critical_extensions 8 → noncritical_extensions ``` Las claves 6, 7 y 8 son opcionales y se omiten cuando están ausentes (§43, §58.1). Las claves 7 y 8, cuando existen, contienen entre 1 y 64 extensiones (§54). Para V1 portable X25519: ```text access_type = "x25519" access_material = 32 raw identity bytes ``` --- ## 42. `credential_id` Debe ser: ```text 16 random bytes ``` generados por CSPRNG. Es un identificador opaco. No deriva de la key ni de identidad personal. --- ## 43. `verification_metadata` V1 define opcionalmente: ```text 0 → capsule_digest ``` donde: ```text capsule_digest = SHA-256(exact .dkc bytes) ``` Su función es: - fallo rápido; - detección de fichero equivocado; - UX; - deduplicación. NO constituye una propiedad de seguridad necesaria para el acceso. Una identity X25519 solo podrá abrir el `INNER_ACCESS_AGE` para el que fue utilizada; el digest no sustituye esa propiedad criptográfica. Si no existe metadata de verificación, la clave `verification_metadata` MUST omitirse. Un mapa vacío no es una representación canónica válida de ausencia en V1. --- ## 44. Extensiones de aplicación en `.dkk` Información de integración que no pertenezca al protocolo base DEBE ir en: ```text key 8 → noncritical_extensions ``` salvo que una futura especificación DateKeys registre una extensión crítica concreta. De este modo las implementaciones no inventan campos core incompatibles. La `data` de esas extensiones viaja en claro junto a la credencial y `header_binding` no la cubre: el protocolo base no la autentica, y solo informa a quien posee la `.dkk` (§55.1, §72). --- ## 45. Release API La API recomendada se indexa por condición: ```text GET /v1/releases/{profile}/{condition} ``` Ejemplo Quicknet: ```text GET /v1/releases/datekeys:quicknet:v1/66884212 ``` No requiere `capsule_id`. --- ## 46. Release Queue La unidad de trabajo es: ```text profile + condition ``` No cápsula. Muchas cápsulas de una misma ronda comparten un único release. --- ## 47. Release Cache Un release publicado puede almacenarse como: ```text profile condition release_material verified verified_at ``` El servidor MUST verificarlo antes de marcarlo como válido. El SDK MUST verificarlo de nuevo. --- ## 48. Multi-relay La implementación SHOULD soportar varios relays independientes por disponibilidad. La autenticidad procede de la verificación BLS. No del hostname. --- ## 49. Recuperación directa contra el proveedor Una implementación conforme SHOULD poder obtener un release directamente del proveedor temporal, sin pasar por la API DateKeys. Para Quicknet: ```text .dkc + Provider Profile pinneado + release obtenido de un relay drand + .dkk si la política la exige ``` debe ser suficiente para ejecutar el flujo de apertura. La API DateKeys es una capa de conveniencia, disponibilidad y caché, no una autoridad criptográfica obligatoria. --- ## 50. Dependencia del histórico de releases La recuperación años después depende de que el release histórico necesario siga disponible. Para Quicknet, esto puede provenir de: - un relay drand que conserve/entregue rondas históricas; o - una Release Cache válida conservada por otra fuente. El protocolo NO debe asumir silenciosamente que cualquier proveedor conservará histórico indefinidamente. Una aplicación que prometa horizontes largos SHOULD documentar esta dependencia. --- ## 51. Verificación de release Quicknet El SDK MUST comprobar: ```text expected Provider Profile expected chain hash expected round valid BLS signature ``` Al descifrar, el paso 10 de §63 fija el orden de estas comprobaciones y sus códigos: primero la ronda (`ERR_ROUND_MISMATCH`), después la firma (`ERR_RELEASE_INVALID`). Son los códigos de un release que el llamador suministra directamente: el que llega de una fuente de red y no supera estas comprobaciones lo descarta su fuente en el paso 9, y si ninguna fuente entrega uno que las supere el código es `ERR_RELEASE_UNAVAILABLE`. El perfil y el chain hash esperados son los del perfil pinneado, cuya clave pública verifica la firma. Un campo remoto: ```text verified = true ``` no tiene valor de seguridad. --- ## 52. DNS / MITM Controlar DNS, TLS termination o un relay no debe permitir fabricar un release válido mientras: - la raíz de confianza esté pinneada; - la condición esperada se calcule localmente; - la firma se verifique. --- ## 53. Harvest now, decrypt later El SDK oficial SHOULD advertir al usuario en horizontes temporales largos. Debe explicar: - Quicknet V1 no es post-cuántico; - el ciphertext puede permanecer disponible durante años; - la seguridad futura depende del provider y de la criptografía subyacente. El umbral temporal de la advertencia es política de producto, no parte de la semántica criptográfica del protocolo. --- ## 54. Extensiones `PUBLIC_HEADER`, `CONTROL_CBOR`, `.dkk` y, en formato 3, el head (§29.4) pueden incluir extensiones mediante el mismo mecanismo. Cada extensión declara: ```text 0 → extension_id 1 → extension_version 2 → data (opcional) ``` El par: ```text (extension_id, extension_version) ``` identifica el schema de la extensión. `extension_id` es una cadena de texto UTF-8 de al menos 1 byte (§31), y `extension_version` un entero sin signo entre 0 y 2³² − 1 (4294967295). La posición del array determina si la extensión es: ```text critical ``` o: ```text noncritical ``` Reglas: - una extensión crítica desconocida → MUST reject (`ERR_EXTENSION_CRITICAL_UNKNOWN`); - una extensión no crítica desconocida → MAY ignore; - una extensión conocida que aparece en un objeto o array para el que no está registrada (§72) se trata allí como desconocida: crítica → MUST reject (`ERR_EXTENSION_CRITICAL_UNKNOWN`); no crítica → MAY ignore, y una implementación MUST NOT interpretar su `data`; - los elementos de cada array MUST estar en orden estrictamente ascendente de los bytes UTF-8 de `extension_id` (ver abajo), lo que también prohíbe repetir un `extension_id` dentro del array; - un mismo `extension_id` MUST NOT aparecer simultáneamente en `critical_extensions` y `noncritical_extensions` dentro del mismo objeto; - por esas dos reglas, un mismo `extension_id` no aparece más de una vez en el mismo objeto, aunque cambie `extension_version`; la multiplicidad, si un schema la necesita, va dentro de su `data`; - cada array de extensiones contiene entre 1 y 64 extensiones: un encoder MUST NOT producir más de 64 y un decoder MUST rechazar un array de más de 64. Orden de `extension_id`: dos identificadores se comparan byte a byte sobre su codificación UTF-8, con cada byte como entero sin signo (0x00 a 0xFF); el primer byte distinto decide, y si uno es prefijo propio del otro, el más corto va primero. Nunca se comparan unidades de código UTF-16 ni se aplica la colación de una locale, un orden sin distinción de mayúsculas o una normalización Unicode. Para UTF-8 válido este orden coincide con el de los puntos de código, y difiere del de UTF-16: U+FF61 (`ef bd a1`) va antes que U+10000 (`f0 90 80 80`). Un encoder MUST escribir cada array en este orden y un decoder MUST rechazar cualquier otro, o un `extension_id` repetido, con `ERR_NON_CANONICAL_CBOR`. `data` (clave 2): - MUST ser una cadena de bytes CBOR (`bstr`, tipo mayor 2) de al menos 1 byte; - una extensión sin datos MUST omitir la clave 2; `h''` (`0x40`) es inválido; - cualquier otro tipo CBOR en la clave 2 es inválido; - su longitud está acotada por la trama del objeto que la contiene (§57); - su contenido es opaco: el protocolo base nunca lo decodifica ni lo valida, y la validez base de `PUBLIC_HEADER`, `CONTROL_CBOR`, `.dkk` y el head nunca depende de él. Una `data` vacía o de tipo distinto de `bstr` y un array de más de 64 extensiones son violaciones del CDDL: MUST rechazarse con `ERR_NON_CANONICAL_CBOR` (§57). Solo una implementación que conoce `(extension_id, extension_version)` en el objeto y el array en que aparece interpreta su `data`, conforme a su schema registrado (§72): - una extensión crítica conocida cuya `data` no cumple su schema registrado → MUST reject (`ERR_EXTENSION_DATA_INVALID`); - una extensión no crítica conocida cuya `data` no cumple su schema registrado no invalida el objeto: la implementación MUST tratarla como inutilizable, sin usar su `data`, y MUST notificarlo al llamador. La información específica de una aplicación que no pertenezca al núcleo DateKeys —incluidos datos de transporte o descubrimiento si una aplicación los necesita— MUST ir en `noncritical_extensions` y no en campos core del protocolo. --- ## 55. Integridad auxiliar Hashes públicos MAY utilizarse para: - identificación; - caché; - deduplicación; - auditoría; - UX. No sustituyen: - header MAC de age; - X25519 wrapping; - tlock; - STREAM authentication; - `header_binding`. --- ## 55.1 Modelo de confianza por sección La tabla resume, para cada sección de un `.dkc` y para una `.dkk`, quién puede escribirla, desde qué paso de §63 queda vinculada al resto de la cápsula y mediante qué, y qué no prueba nunca. Ninguna fila prueba autoría: solo una extensión de firma puede aportarla (§36.1, §72). La tabla es normativa: una implementación MUST NOT presentar una sección como prueba de algo que su columna «Nunca prueba» excluye, salvo que lo cubra una extensión de firma. | Sección | Quién puede escribirla | Vinculada desde el paso, mediante | Nunca prueba | |---|---|---|---| | `PRELUDE` y `PUBLIC_HEADER` | Cualquiera que tenga el `.dkc`: viajan en claro y ninguna clave los protege. | Paso 15: `header_binding`, dentro de `CONTROL_CBOR`, cubre sus bytes exactos (§26). Los pasos 1 a 8 solo comprueban su estructura. | Autoría ni fecha de creación: `header_binding` se calcula con bytes públicos (§36.1). Antes del paso 15, ni siquiera su coherencia con el control. | | `CONTROL_CBOR` | Cualquiera puede sellar un control hacia la DateKey, porque basta la clave pública del perfil (§36.1). En `time_and_key`, uno que abra una credencial dada solo quien conozca la clave pública de su recipient. | Paso 11: el MAC de la cabecera `age` y STREAM de `OUTER_TIME_AGE`, y en `time_and_key` los de `INNER_ACCESS_AGE` en el paso 13, autentican sus bytes frente a quien no conoce la file key. Paso 15: `header_binding` lo vincula a `PUBLIC_HEADER`. | Que lo escribiera el creador de `PUBLIC_HEADER`: un tercero puede sellar otro control con un `header_binding` correcto. Tras la apertura, quien conoce `FK_TIME` o `FK_ACCESS` puede reescribirlo. | | `PAYLOAD_AGE` | Quien conoce `R_PAYLOAD`: antes de la apertura, solo quien selló el control; después, cualquiera que haya abierto `CONTROL_CBOR` y obtenido `I_PAYLOAD`. | Paso 17: `I_PAYLOAD` desenvuelve `FK_PAYLOAD`, y el MAC de la cabecera y STREAM autentican cada byte (§30.1). En los formatos 2 y 3, L y el código de `CONTROL_CBOR` fijan además su longitud y su relleno (§29.1). | Autoría; tampoco que siga siendo el payload original después de que alguien haya abierto la cápsula, porque puede cifrar otro para `R_PAYLOAD`. Tampoco que L sea la longitud original: quien puede reescribir el control puede declarar otra L de la misma P (§30.1). | | Contenido de formato 3: `security`, head y ficheros (§29.2) | Quien escribe `PAYLOAD_AGE`, porque es su plaintext. | Paso 17: los autentican el MAC de la cabecera y STREAM de `PAYLOAD_AGE`, y el head fija el tamaño y el SHA-256 de cada fichero (§29.4). | Autoría ni fecha: el autor declarado, el comentario, las rutas y las mtimes son texto del creador. En esta versión, `security` tampoco prueba nada (§29.3, §29.7). | | Cuerpo de la `.dkk` | Cualquiera que tenga la `.dkk`: no lleva MAC ni firma. | Paso 9.a: su `capsule_id` debe ser el de `PUBLIC_HEADER`, un valor público, y su `capsule_digest`, cuando existe y se comprueba, la ata a los bytes exactos de un `.dkc` (§43). Paso 13: `access_material` abre `INNER_ACCESS_AGE` solo si es la identity de uno de sus recipients. | Que la emitiera el creador de la cápsula. La `data` de sus extensiones no queda vinculada a nada. | En consecuencia, una afirmación de la que dependa una decisión de seguridad del lector —autorización, identidad, integridad o fecha— no puede apoyarse en `PUBLIC_HEADER` ni en una `.dkk` sin una extensión de firma, y la `data` de las extensiones de una `.dkk` solo informa a quien la posee (§72). --- ## 55.2 Consideraciones de privacidad Esta sección enumera qué revela una cápsula y a quién. No añade reglas de lectura: resume las consecuencias de §22, §24, §29.1, §31 y §39. Describe los formatos 2 y 3; el formato 1 revela más (al final). ### Visible para quien tenga el `.dkc`, antes y después de la fecha - `VERSION`, que es el formato, `PUBLIC_HEADER_LEN` y `SEALED_CONTROL_LEN`. `VERSION` 1 revela un escritor anterior a la v0.9, y `VERSION` 2, uno de la v0.9 (§62.1); - `capsule_id`, que permite reconocer copias de la misma cápsula; - la DateKey: el perfil y la ronda, y por tanto el instante de apertura; - `access_policy`: si hace falta una credencial. Sigue visible para que el paso 9 pueda fallar sin red (`ERR_ACCESS_REQUIRED`); - las extensiones de `PUBLIC_HEADER`, con su `data`; - el stanza tlock de `OUTER_TIME_AGE`: la ronda y el chain hash; - la cabecera de `PAYLOAD_AGE` y su longitud, que da P exactamente: `184 + P + 16·max(1, ⌈P / 65536⌉)`. P acota L: entre 0 y 256 si P = 256; si no, a un intervalo de 256 bytes con `bloque256`, y con `reforzado`, de 256 bytes hasta L = 8192 y de como mucho P / 16 por encima. Con `bloque256` el intervalo es de 256 bytes a cualquier tamaño: un fichero público conocido puede reconocerse por su tamaño. En formato 3, L es la longitud de `BODY`, así que P acota además el número de ficheros y la longitud de sus rutas y del comentario: con el área de 512 bytes y pocos ficheros, n ≤ (P − 579)/45, es decir, como mucho 4 ficheros con P = 768. El tamaño del área depende de la versión del escritor, no de lo que lleve `security` (§29.2); - la longitud de `CONTROL_CBOR`, que se deduce de `SEALED_CONTROL_LEN` (§62.1): 103 bytes más los de sus extensiones. Revela el tamaño de las extensiones de control, no su contenido. No depende de L, que ocupa siempre 8 bytes, ni del número de credenciales, porque `INNER_ACCESS_AGE` tiene siempre 16 stanzas. El código de relleno va sellado, pero P lo delata a veces: los dos códigos dan la misma P para L ≤ 8192, y por encima una P que `reforzado` no produce revela `bloque256`, y con él un escritor que no usa el código por defecto (§29.1). ### Oculto hasta la fecha - el contenido y su longitud exacta L; - el código de relleno, salvo lo que delata P; - `I_PAYLOAD` y todo `CONTROL_CBOR`, incluida la `data` de sus extensiones; - en formato 3, `security`, el head y los ficheros: sus rutas, sus tamaños, sus hashes y sus mtimes, el comentario y el autor declarado, y su número, salvo la cota que da P; - en `time_and_key`, el número de credenciales y la cabecera de `INNER_ACCESS_AGE`. En `time_only` no hay credenciales, y `access_policy` lo dice. ### Después de la fecha Publicada la ronda, cualquiera puede abrir `OUTER_TIME_AGE`: - `time_only`: cualquiera lee `CONTROL_CBOR` y el contenido, con L y el código, y en formato 3, el head y todos los ficheros. Nada queda oculto; - `time_and_key`: cualquiera ve la cabecera de `INNER_ACCESS_AGE`, siempre con 16 stanzas; `CONTROL_CBOR` y el contenido siguen ocultos para quien no tiene una credencial. Quien tiene una lee L, el código, el contenido y las extensiones de control, y sabe cuál es su stanza; no sabe cuáles de los otros 15 son señuelos. Varias credenciales juntas solo saben cuántos stanzas abren entre todas: una cota inferior del número de credenciales. ### Nunca visible Bajo el supuesto de Diffie–Hellman en X25519 (§39): - los recipients: un stanza X25519 no contiene la clave pública de su recipient, y comprobar si un stanza es para una clave pública dada exige su clave privada o el escalar efímero; - si un stanza es de un señuelo; - si existe una `.dkk` portable y cuál es su stanza, para quien no la tiene; - el número de credenciales, salvo la cota inferior de arriba. Ese supuesto no resiste a un adversario cuántico futuro (§7.7): quien conserve el `.dkc` y conozca claves públicas candidatas podría recuperar los escalares efímeros y comprobar para cuáles hay un stanza, e identificar así a los recipients reales. ### Lo que los formatos 2 y 3 impiden comprobar - En formato 1, quien esperaba n recipients podía contar los stanzas. En los formatos 2 y 3 nadie salvo el escritor puede saber cuántas partes pueden abrir la cápsula. - Nadie puede comprobar que el escritor descartó las claves privadas de los señuelos (§39). Un SDK comprometido (§7.5) o un dispositivo del creador comprometido (§7.8) puede conservar una como credencial oculta, y nadie lo notaría. ### Fuera del `.dkc` - una `.dkk` lleva en claro la credencial, `access_material`, y además `capsule_id`, `credential_id`, el `capsule_digest` opcional, que la ata a los bytes exactos de un `.dkc`, y la `data` de sus extensiones (§41, §43, §44); - pedir el release revela a la Release API y a los relays el perfil, la ronda y la dirección de quien pregunta, pero no `capsule_id` (§45, §48); - el nombre del fichero `.dkc`, sus fechas en el sistema de ficheros y el canal de entrega no pertenecen al protocolo (§6). En formato 3, los nombres y las mtimes de los ficheros que guarda la cápsula sí van dentro, cifrados en el head (§29.4). ### Formato 1 Una cápsula de formato 1 no oculta ni el número de credenciales ni L: - `SEALED_CONTROL_LEN` crece 98 bytes por stanza de `INNER_ACCESS_AGE`: en los fixtures de la v0.8.2, 646 bytes con una credencial y 842 con tres; - la longitud de `PAYLOAD_AGE`, `184 + L + 16·max(1, ⌈L / 65536⌉)`, da L exacta. El protocolo no rellena la `data` de las extensiones. Una extensión registrada que necesite ocultar su longitud define su propio relleno (§72). --- ## 56. Atomic plaintext output El descifrado SHOULD usar: - fichero temporal; - stream transaccional; - mecanismo equivalente. No debe presentar plaintext parcial como válido si falla cualquier autenticación posterior. En formato 2, un lector MUST NOT presentar como válidos los L primeros bytes, el contenido, antes de que el paso 17 termine sin error, con la comprobación de la longitud y del relleno (§63, paso 18). Un lector que escribe el contenido en streaming MUST NOT escribir el relleno y MUST señalar el error del paso 17, para que se descarte lo escrito como en el resto de esta sección. Esa comprobación solo termina después del último byte del contenido: un lector no transaccional, o un llamador que ignora el error, tendría un contenido de aspecto completo de un objeto inválido. En formato 3, un lector MUST NOT presentar como válidos ningún fichero, el head ni ningún veredicto antes de que el paso 17 termine sin error (paso 18). Un lector que escribe los ficheros en un sistema de ficheros mientras los descifra: - MUST escribirlos en un lugar temporal y moverlos a su destino solo en el paso 18, y ante cualquier fallo MUST borrarlos; - MUST crearlos dentro de un directorio que él mismo crea y que no existía, con las rutas del head (§29.5), sin seguir enlaces simbólicos, sin sobrescribir nada y sin escribir fuera de él; - no crea nada para una cápsula sin ficheros. Un lector que entrega los ficheros en un ZIP MUST usar como nombre de cada entrada su ruta del head, en UTF-8 y con el bit 11 del campo de flags, y MUST NOT añadir entradas que el head no tiene. El ZIP se entrega como un fichero único, con las reglas de esta sección. Nota informativa: la implementación de referencia reclama el directorio de destino con `os.Mkdir`, que falla si ya existe; escribe el árbol en `DIR/.datekeys-*` con `os.OpenRoot`, `O_EXCL` y permisos 0600; tras el paso 18 lleva cada entrada al primer nivel con `Root.Rename`; y ante cualquier fallo borra el directorio que creó. La página de `datekeys-ts` escribe entradas almacenadas, sin compresión, descriptores de datos ni entradas de carpeta; pasa a ZIP64 con 65 535 entradas o con algún tamaño u offset de 2³² − 1 o más; y guarda la mtime como hora DOS en UTC, recortada a 1980–2107, con el campo extra 0x000A siempre y el 0x5455 si cabe en 32 bits con signo. Como las entradas van sin comprimir, cada fichero es un tramo continuo del ZIP y puede descargarse suelto. --- ## 57. Límites del parser Encoders y decoders MUST aplicar estos límites de trama: ```text 1 byte <= PUBLIC_HEADER <= 1 MiB 1 byte <= SEALED_CONTROL <= 64 MiB 1 byte <= DKK BODY <= 16 MiB ``` - un encoder MUST NOT producir un objeto que los supere; - un decoder MUST rechazar un objeto que los supere; - una trama vacía no contiene ningún objeto válido: una longitud 0 queda fuera de rango (§22, §40); - cada trama acota todo lo que contiene, incluida la `data` de las extensiones (§54); `CONTROL_CBOR` queda acotado por `SEALED_CONTROL`. `PAYLOAD_AGE` no tiene trama propia: va hasta EOF, se procesa en streaming y es, como mínimo, una cabecera `age` bien formada (§22, §28.1). En los formatos 2 y 3 su plaintext mide exactamente P = regla(L) (§29.1). Los tamaños se validan antes de reservar memoria. L (clave 6 de `CONTROL_CBOR`) y P no son longitudes de trama. Cualquiera puede sellar un control `time_only` que declara L = L_MAX junto a un `PAYLOAD_AGE` de 456 bytes (§36.1). Un lector MUST NOT reservar memoria ni disco según L o P antes de recibir el plaintext que los respalda: los acota el ciphertext. En formato 3 (§29.2): - `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN` van en el plaintext de `PAYLOAD_AGE`, no en la trama del `.dkc`: se comprueban en el paso 17, con `ERR_INTEGRITY`. Un lector MUST NOT reservar memoria según ellas, ni según `size`, `start` o `end`, antes de recibir los bytes que declaran; - el área mide como mucho 64 KiB y el head, 16 MiB: un lector puede tener los dos enteros en memoria; - los límites del head (§29.4 a §29.6) son normativos y quedan fijados con el formato 3 (§22). Límites del escritor que el lector no comprueba como trama (§62.1): - `time_and_key` admite entre 1 y 16 credenciales (§39); un lector de los formatos 2 y 3 solo ve 16 stanzas; - L es como mucho L_MAX = 2⁵³ − 2⁴⁶ (§29.1); un lector lo comprueba en `CONTROL_CBOR` (§31), con `ERR_NON_CANONICAL_CBOR`. Correspondencia de errores: - una longitud de trama (`PUBLIC_HEADER_LEN`, `SEALED_CONTROL_LEN` y el `BODY_LEN` de la `.dkk`) fuera de estos límites, incluida una longitud 0, o un objeto que supera el límite de su trama → `ERR_INTEGRITY`; - cualquier violación de una regla normativa de `datekeys.cddl`, incluidas las restricciones de tipo, de tamaño y de rango, el orden de los `extension_id`, el máximo de 64 extensiones por array, y las de las claves 6 y 7 de `CONTROL_CBOR` (§31) → `ERR_NON_CANONICAL_CBOR`, salvo los casos con código propio: versión de schema (clave 1) que el objeto no admite —para `CONTROL_CBOR`, la que no corresponde al formato de su cápsula (§22, §31)— → `ERR_UNSUPPORTED_VERSION` (§70); `compact_datekey` → `ERR_DATEKEY_INVALID` o `ERR_DATEKEY_NON_CANONICAL` (§19); `access_type` no soportado o `access_material` de longitud incorrecta → `ERR_ACCESS_INVALID`; en el Provider Profile, un `profile_id`, `provider`, `network` o `scheme` que no cumple su regla, o una clave pública (clave 6) fuera de la suya → `ERR_UNKNOWN_PROFILE` (§12.1, §13); - en formato 3, una violación de la trama del contenido (§29.2) → `ERR_INTEGRITY`; una de las reglas del head con código propio (§29.4 a §29.6, capa 4 de §69.1) → `ERR_HEAD_INVALID`; y las de `security`, ningún código (§29.3); - un código propio se aplica a un campo que ya tiene el tipo CBOR de su regla: un `compact_datekey`, `access_type` o nombre del perfil que no es una cadena de texto, o un `access_material` o una clave pública que no es una cadena de bytes, es `ERR_NON_CANONICAL_CBOR`; - las reglas que `datekeys.cddl` marca como límites de la implementación de referencia no son normativas mientras §74 deje abiertos los límites definitivos de campos; quien las aplica usa esta misma correspondencia (§74 las enumera). Cuando unos bytes violan varias reglas, el código es el de la primera capa que falla, en el orden de §69.1. --- ## 58. Canonical CBOR Todas las estructuras CBOR del protocolo MUST: - usar Deterministic CBOR con el perfil CBOR del protocolo definido en esta sección; - rechazar siempre codificaciones no canónicas; - usar enteros de clave según los schemas normativos. Perfil CBOR del protocolo: Deterministic CBOR (RFC 8949 §4.2.1) restringido a: - tipos mayores 0 (entero sin signo), 2 (cadena de bytes), 3 (cadena de texto), 4 (array) y 5 (mapa); - claves de mapa que son enteros sin signo; - longitudes definidas; - enteros y longitudes en su forma más corta; - claves de mapa en orden ascendente estricto, sin duplicados; - mapas cerrados: una clave no prevista por el schema MUST rechazarse; - cadenas de texto en UTF-8 válido. Con claves enteras sin signo en su forma más corta, el orden por bytes de RFC 8949 §4.2.1 coincide con el orden numérico. Se rechazan expresamente, con `ERR_NON_CANONICAL_CBOR`: - enteros negativos (tipo mayor 1); - tags (tipo mayor 6); - flotantes y valores simples, incluidos `false`, `true`, `null` y `undefined` (tipo mayor 7); - longitudes indefinidas; - claves de mapa que no son enteros sin signo. Los schemas del protocolo acotan todo entero sin signo a 2⁵³ − 1 (9007199254740991) como máximo (§11, §54), de modo que cada uno se representa exactamente como double IEEE 754. L (clave 6 de `CONTROL_CBOR`) no es un entero CBOR: es una cadena de 8 bytes con un entero sin signo big-endian de como mucho 2⁵³ − 2⁴⁶ (§31), también exacto como double. Un decodificador lee los 8 bytes: uno que solo leyera los 4 últimos tomaría `48 0000000100000001`, L = 2³² + 1, por L = 1. En la `data` de una extensión, el perfil se aplica a la cabecera de la cadena de bytes (tipo mayor 2 y longitud en su forma más corta), nunca a su contenido (§54). Las extensiones registradas cuya `data` sea CBOR siguen además §72. En formato 3, `SECURITY_CBOR` y `HEAD_CBOR` siguen este perfil, y también el contenido de las claves 2 y 3 de `security`, que se valida aparte (§29.3). En `SECURITY_CBOR` y en ese contenido, lo que este perfil rechaza no tiene código ni detiene la apertura: solo cambia los veredictos (§29.7). --- ## 58.1 Regla global para campos opcionales V1 usa una única convención: > **Un campo opcional semánticamente ausente MUST omitirse.** No se debe representar ausencia mediante: ```text {} [] null "" h'' ``` salvo que el schema de ese campo defina expresamente uno de esos valores como dato real. `null` no puede serlo nunca: el perfil de §58 excluye los valores simples. Por tanto: - metadata opcional ausente → clave omitida; - arrays opcionales sin elementos → clave omitida; - mapas opcionales sin entradas → clave omitida; - listas de extensiones vacías → clave omitida; - extensión sin `data` → clave 2 omitida; `h''` es inválido (§54); - las claves 6 y 7 de `CONTROL_CBOR` de las versiones 2 y 3 son obligatorias y nunca se omiten; L = 0 es un dato real (`48 0000000000000000`), no una ausencia, y ningún código de relleno representa «sin relleno» (§29.1, §31); - en formato 3, un head sin comentario, sin autor o sin ficheros omite su clave 3, 4 o 5; una entrada sin mtime omite su clave 5; y `security` sin firma ni sello omite sus claves 2 y 3 (§29.3, §29.4). Esto reduce representaciones equivalentes y simplifica vectores canónicos. --- ## 59. Supply-chain security Implementaciones oficiales SHOULD: - pinnear dependencias criptográficas; - publicar SBOM; - firmar releases; - publicar hashes; - publicar perfiles firmados; - usar builds reproducibles cuando sea viable; - fuzzear parsers; - publicar vectores; - someter v1.0 a revisión criptográfica externa. --- ## 60. Interfaces Go conceptuales ```go type Condition any type Release any type TimeProvider interface { ProfileID() string Resolve(time.Time) (Condition, error) EffectiveTime(Condition) (time.Time, error) FetchRelease( context.Context, Condition, ) (Release, error) VerifyRelease( Condition, Release, ) error } ``` --- ## 61. Flujo de cifrado `time_only` Un orden válido para escribir una cápsula de formato 3. Un escritor MAY seguir otro que produzca un objeto equivalente y cumpla §62.1. ```text 1. Validar las opciones (§62.1): ninguna credencial; instante pedido posterior al reloj del escritor; al menos un fichero o un comentario; rutas (§29.5) y textos (§29.6) válidos; código de relleno 1 o 2; extensiones conformes a §54 y §72. 2. Medir el contenido (§29.2, §29.4): el tamaño y, si se incluye, la mtime de cada fichero; HEAD_CBOR con sus SHA-256 y su sal a cero, que mide lo mismo que el definitivo, de como mucho 16 MiB; y L = 12 + 512 + |HEAD_CBOR| + C, como mucho L_MAX. 3. Primera pasada: el SHA-256 de cada fichero. 4. Resolver DateKey localmente (§15). 5. Generar capsule_id (CSPRNG, §21). 6. Generar I_PAYLOAD X25519 nueva (CSPRNG, §29). 7. Construir PUBLIC_HEADER (versión de schema 1). 8. Medir SEALED_CONTROL_LEN (§62.1): la longitud del OUTER_TIME_AGE del paso 13 con un CONTROL_CBOR de 103 bytes más sus extensiones. Más de 64 MiB → error (§57). 9. Construir PRELUDE: VERSION = 3 y las longitudes de los pasos 7 y 8. 10. Calcular header_binding = SHA-256(PRELUDE || PUBLIC_HEADER). 11. Crear CONTROL_CBOR, versión de schema 3: header_binding I_PAYLOAD critical_extensions noncritical_extensions payload_length = L padding = código 12. Construir HEAD_CBOR, con una sal nueva (CSPRNG) y los SHA-256 del paso 3, y SECURITY_CBOR vacío (§29.3). Decodificar CONTROL_CBOR, HEAD_CBOR y SECURITY_CBOR con las reglas del lector (§62.1, regla 17). 13. Crear OUTER_TIME_AGE: age genera FK_TIME (16 bytes) recipient = tlock(DateKey) plaintext = CONTROL_CBOR Su longitud MUST ser la del paso 8. 14. SEALED_CONTROL = OUTER_TIME_AGE. 15. Escribir PRELUDE || PUBLIC_HEADER || SEALED_CONTROL. 16. Escribir PAYLOAD_AGE a continuación: age genera FK_PAYLOAD (16 bytes) recipient = R_PAYLOAD plaintext = BODY || 0x00^(P − L), P = regla(L) (§29.1), con BODY = 512 || |SECURITY_CBOR| || |HEAD_CBOR| (uint32 BE) || SECURITY_CBOR || ceros hasta 512 bytes || HEAD_CBOR || los ficheros en el orden del head Segunda pasada: si un fichero no mide lo que declara su entrada o su SHA-256 cambia → error, y lo escrito se descarta (§62.1, reglas 9 y 18). ``` File keys utilizadas: ```text FK_PAYLOAD FK_TIME ``` --- ## 62. Flujo de cifrado `time_and_key` Un orden válido para escribir una cápsula de formato 3, como en §61. ```text 1. Validar las opciones (§62.1): entre 1 y 16 credenciales —los recipients X25519 dados y, si se pide, una clave portable—, sin repetir ninguna, canónicas y no de orden bajo (§37); el resto, como en §61. 2. Medir el contenido y hacer la primera pasada, como en §61, pasos 2 y 3. 3. Resolver DateKey localmente. 4. Generar capsule_id. 5. Generar I_PAYLOAD X25519 nueva. 6. Si se pide una clave portable: generar I_ACCESS (§38). 7. Completar 16 recipients con señuelos (§39): por cada hueco libre, generar un escalar X25519 nuevo (CSPRNG), calcular su clave pública y borrar el escalar. Permutar los 16 al azar (CSPRNG, sin sesgo). 8. Construir PUBLIC_HEADER. 9. Medir SEALED_CONTROL_LEN (§62.1): la del OUTER_TIME_AGE del paso 14, que contiene un INNER_ACCESS_AGE de 16 stanzas. Más de 64 MiB → error (§57). 10. Construir PRELUDE (VERSION = 3). 11. Calcular header_binding. 12. Crear CONTROL_CBOR, versión de schema 3, con los campos de §61, paso 11, y construir HEAD_CBOR y SECURITY_CBOR como en §61, paso 12. 13. Crear INNER_ACCESS_AGE: age genera FK_ACCESS recipients = los 16 del paso 7, en ese orden plaintext = CONTROL_CBOR Sus 16 stanzas MUST quedar en el orden del paso 7 (§39). 14. Crear OUTER_TIME_AGE: age genera FK_TIME recipient = tlock(DateKey) plaintext = exact INNER_ACCESS_AGE bytes Su longitud MUST ser la del paso 9. 15. SEALED_CONTROL = OUTER_TIME_AGE. 16. Escribir PRELUDE || PUBLIC_HEADER || SEALED_CONTROL. 17. Escribir PAYLOAD_AGE como en §61, paso 16. 18. Si se generó I_ACCESS: escribir I_ACCESS cruda en una .dkk (§40 a §43), con el capsule_digest del .dkc completo si se incluye. ``` Las tres file keys son: ```text FK_PAYLOAD FK_ACCESS FK_TIME ``` y MUST ser independientes. --- ## 62.1 Reglas del escritor Un escritor conforme con esta versión cumple estas reglas, siga o no el orden de §61 y §62. MUST: 1. **Formato.** Escribir el formato 3: `VERSION` = 3 (§22) y `CONTROL_CBOR` de versión 3 (§31). MUST NOT escribir los formatos 1 ni 2; solo un generador de vectores de prueba MAY hacerlo (§70). 2. **Instante.** Rechazar un `requested_unlock_at` que no sea posterior al instante actual de su reloj. La ronda se resuelve después, con §15. Esta regla solo evita un error del usuario: con el reloj retrasado, un escritor puede sellar hacia una ronda ya publicada. El SDK oficial SHOULD mostrar el instante efectivo de la ronda (§15) y MAY comparar la ronda con la última publicada cuando tiene red. 3. **Credenciales.** Con `time_only`, rechazar cualquier credencial. Con `time_and_key`, exigir entre 1 y 16 credenciales —recipients X25519 y, si se pide, la clave portable (§38)—, rechazar una clave pública repetida y rechazar todo recipient que no sea X25519, que no sea canónico o que sea de orden bajo (§37). Sin credenciales nadie abriría la cápsula; una repetida daría a su identity dos stanzas, y la cápsula fallaría para ella en el paso 13. 4. **Huecos.** Completar los 16 recipients con señuelos y dejar los 16 stanzas en un orden uniformemente aleatorio (§39). Las claves privadas de los señuelos MUST NOT almacenarse, registrarse ni entregarse; la permutación y qué huecos son señuelos, tampoco, salvo en los vectores oficiales de prueba (§67). 5. **Aleatoriedad.** Obtener de un CSPRNG, nuevos para cada cápsula, `capsule_id` (§21), `I_PAYLOAD` (§29), `I_ACCESS` (§38), `credential_id` (§42), los señuelos y el orden de los stanzas. `I_PAYLOAD` y los señuelos MUST NOT reutilizarse ni derivarse de otro valor: una `I_PAYLOAD` compartida o derivada rompe la confidencialidad de §29, y la clave privada de un señuelo abre la cápsula (§39). 6. **Contenido.** Conocer L antes de sellar `CONTROL_CBOR`, con L ≤ L_MAX, y usar el código 1 o 2 (§29.1). En formato 3, L es la longitud de `BODY` (§29.2): la dan los tamaños de los ficheros y la longitud del head, que no depende de sus SHA-256 ni de su sal. Un escritor cuya fuente no declara su longitud MAY volcarla antes a un fichero temporal para medirla; MUST NOT suponer L. Si el contenido no mide exactamente L bytes, la cápsula fallaría en el paso 17, cuando ya no puede repararse: se aplica la regla 9. 7. **Longitud del control.** Escribir en `SEALED_CONTROL_LEN` la longitud exacta de `SEALED_CONTROL`. `header_binding` cubre el PRELUDE y va dentro de `CONTROL_CBOR`, así que esa longitud debe conocerse antes de calcular `header_binding`. La de un fichero `age` depende solo de la longitud de su plaintext y de la forma de sus stanzas, y la de `CONTROL_CBOR` de versión 2 o 3 no depende de `header_binding`, `I_PAYLOAD`, L ni el código, todos de tamaño fijo. El escritor MAY medirla sellando antes un `CONTROL_CBOR` provisional de la misma longitud, con `header_binding` e `I_PAYLOAD` a cero, o calcularla con la nota de abajo. En los dos casos MUST comprobar que el sellado real mide lo mismo. 8. **Límites.** Respetar los límites de §57 y las reglas de extensiones de §54 y §72. 9. **Errores.** Ante cualquier error, informar de él y no presentar lo escrito como una cápsula. Un escritor en streaming no puede recuperar lo que ya escribió: quien recibe su salida MUST descartarla, como en §56. SHOULD: 10. **Código por defecto.** El SDK oficial SHOULD usar el código 2. 11. **Autocomprobación.** Antes de sellar, el escritor SHOULD decodificar su `PUBLIC_HEADER` y su `CONTROL_CBOR` con las reglas del lector (§63, pasos 4 y 14). Después SHOULD comprobar: - que `INNER_ACCESS_AGE` tiene 16 stanzas X25519 con shares distintos, y que cada identity que genera y conserva, como `I_ACCESS`, abre exactamente uno; - que `I_PAYLOAD` abre la cabecera de `PAYLOAD_AGE`; - que entregó a `age` exactamente P bytes de plaintext y que `PAYLOAD_AGE` mide `184 + P + 16·max(1, ⌈P / 65536⌉)`. Sin esta comprobación, un relleno omitido revelaría L exacta y la cápsula fallaría en el paso 17. Una cápsula que el lector rechaza solo se descubre tras la fecha. 12. **Borrado.** El escritor SHOULD borrar de la memoria, en cuanto dejan de necesitarse, `I_PAYLOAD`, `CONTROL_CBOR` y sus copias, las copias de `I_ACCESS` que no entrega en la `.dkk` y los buffers del contenido; los que usa la autocomprobación, después de ella. Los escalares de los señuelos se descartan en el acto (regla 4, §39). En formato 3, además, MUST: 13. **Área.** Escribir `AREA_LEN` = 512 y `SECURITY_CBOR` en todas las cápsulas, sin que dependa de ninguna opción ni de lo que el escritor sepa hacer (§29.2). En esta versión, `SECURITY_CBOR` va vacío: `{0: "datekeys-security", 1: 1}` (§29.3). Solo un generador de vectores de prueba MAY escribir otra área u otro `SECURITY_CBOR`, como los de `format3_area_1024`, `format3_security_v2`, `format3_signature_unsupported` y `format3_seal_unsupported` (§67). 14. **Head.** Escribir el head de versión 1 (§29.4): con una sal de un CSPRNG, nueva para cada cápsula; con al menos un fichero o un comentario; con las entradas en el orden de R8, con sus tamaños, su maquetación y el SHA-256 de los bytes que escribe; y de como mucho 16 MiB. 15. **Rutas y texto.** Rechazar una ruta o un texto que incumpla §29.5 o §29.6, con un mensaje que nombre la regla y el carácter, en lugar de corregirlo sin avisar. Guardar las rutas tal como llegan, rechazar un UTF-16 mal formado y no conservar carpetas vacías. El SDK oficial SHOULD excluir por defecto `.DS_Store`, `Thumbs.db`, `desktop.ini`, `._*` y `__MACOSX/`, y dejar ver y editar la lista antes de cifrar. 16. **mtime.** Si incluye la mtime de un fichero, tomarla de su fuente, al cargarlo: ⌊lastModified / 1000⌋ en un navegador, o `ModTime().Unix()` en Go; y omitirla si no la conoce o si cae fuera de 0 a 253402300799, sin recortarla. El SDK oficial SHOULD incluirla por defecto, con una opción para quitarla. 17. **Autodecodificación.** Decodificar `CONTROL_CBOR`, `HEAD_CBOR` y `SECURITY_CBOR` con las reglas del lector (§63, pasos 14 y 17) antes de escribir la cápsula. A diferencia de la regla 11, es un MUST: un head que el lector rechaza solo se descubre tras la fecha. 18. **Ficheros estables.** Escribir cada fichero con el tamaño y el SHA-256 de su entrada. Si un fichero cambia entre la medida y la escritura, abortar, como en la regla 9. Nota informativa: longitudes en Quicknet. Con `age` estándar, un stanza X25519 mide 98 bytes, y el stanza tlock, 249 + d, con d el número de dígitos decimales de la ronda. Con c(n) = max(1, ⌈n / 65536⌉) y k stanzas X25519 (16 en los formatos 2 y 3): ```text PAYLOAD_AGE = 184 + P + 16·c(P) ; P = L en formato 1 INNER_ACCESS_AGE = 86 + 98·k + C + 16·c(C) ; C = |CONTROL_CBOR| OUTER_TIME_AGE = 335 + d + n + 16·c(n) ; n = C en time_only, ; |INNER_ACCESS_AGE| en time_and_key ``` Sin extensiones de control, C = 103: en la ronda 1000, `SEALED_CONTROL_LEN` vale 458 en `time_only` y 2128 en `time_and_key`. Las fórmulas dan también las longitudes de los cinco fixtures de formato 1 (§67), con C = 91 (127 en `time_only_extensions`) y k = 1 o 3 en los dos `time_and_key`. --- ## 63. Flujo de descifrado ```text 1. Parsear DKC1: MAGIC y PRELUDE completo (§23). 2. Validar PRELUDE: version, que es el formato (1, 2 o 3), flags, reserved, longitudes y límites (§22, §23). Los pasos siguientes aplican las reglas de ese formato. 3. Leer PUBLIC_HEADER exacto. 4. Validar PUBLIC_HEADER con las capas de §69.1: tipo y versión de schema (§70), la versión 1 en los tres formatos, CBOR canónico y CDDL; después, en orden de clave, DateKey canónica (clave 3), Provider Profile pinneado y extensiones críticas (clave 5, §54): alguna desconocida → ERR_EXTENSION_CRITICAL_UNKNOWN; todas conocidas, alguna con data inválida → ERR_EXTENSION_DATA_INVALID. 5. Leer SEALED_CONTROL exacto (faltan bytes → ERR_INTEGRITY). SHOULD: inspeccionar su cabecera age, la de OUTER_TIME_AGE (§28.1), antes de usar red o secretos: exactamente un stanza; de tipo tlock. 6. SHOULD: localizar PAYLOAD_AGE mediante las longitudes del PRELUDE e inspeccionar únicamente su cabecera age (§28.1): exactamente un stanza; de tipo X25519. 7. Resolver/verificar localmente la condición temporal, incluida la cota de round_time de §15 (ERR_DATEKEY_INVALID). 8. SHOULD, cuando se haya realizado la inspección previa, comprobar los argumentos del stanza tlock, en este orden: exactamente dos argumentos tras el tipo → si no, ERR_POLICY_STRUCTURE_MISMATCH; el primero es la representación decimal canónica de DateKey.round: solo dígitos ASCII 0-9, sin signo y sin ceros a la izquierda → si no, ERR_ROUND_MISMATCH; el segundo es el chain_hash del Provider Profile pinneado en hexadecimal en minúsculas, 64 caracteres → si no, ERR_PROFILE_MISMATCH. Los argumentos se comparan como cadenas de bytes exactas: nunca se interpretan como números ni se normalizan mayúsculas o ceros. 9. Antes de pedir el release, y sin ninguna petición de red: a. Si access_policy = time_and_key y se ofrece una .dkk: primero la .dkk como objeto, con las capas de §69.1: su trama (§40) y su schema (§41), con los códigos de §57; access_type y access_material (claves 4 y 5) → ERR_ACCESS_INVALID; después sus extensiones críticas (clave 7), como en el paso 4; después, su vínculo con esta cápsula (§55.1): capsule_id (clave 3) distinto del de PUBLIC_HEADER → ERR_ACCESS_INVALID; capsule_digest (clave 6), si existe y la implementación lo comprueba, lo que es opcional (§43), distinto del SHA-256 del .dkc → ERR_ACCESS_INVALID. b. Si access_policy = time_and_key y no se ofrece ninguna credencial, .dkk o identity → ERR_ACCESS_REQUIRED, sin consultar el reloj. c. Si el instante actual es anterior al round_time de la DateKey → ERR_RELEASE_UNAVAILABLE, sin pedir el release. Con time_only, las credenciales ofrecidas no intervienen: no se validan ni se usan, y ningún error suyo se informa. Solo entonces, obtener el release. Una fuente que obtiene releases por red (relay, Release API o caché) MUST verificar cada respuesta con las reglas del paso 10 y descartar la que no las cumple. Si ninguna fuente entrega un release, por cualquier causa, incluida la de haber descartado todas sus respuestas → ERR_RELEASE_UNAVAILABLE. 10. Verificar el release localmente (§17, §51), en este orden: ronda del release distinta de DateKey.round → ERR_ROUND_MISMATCH, aunque su firma sea válida para esa otra ronda; firma que no es la codificación canónica (§12.2) de un punto del grupo de firmas del scheme —G1, 48 bytes, en Quicknet—, que es el punto en el infinito o que no verifica con la clave pública pinneada como firma de la ronda según el scheme de drand → ERR_RELEASE_INVALID. Estos códigos se informan para un release que el llamador suministra directamente, como en los vectores oficiales: el que llega por red sin cumplir estas reglas lo descarta su fuente en el paso 9. 11. Abrir OUTER_TIME_AGE. La identity tlock MUST recibir y validar el conjunto completo de stanzas y MUST rechazar el fichero salvo que contenga exactamente un stanza tlock (→ ERR_POLICY_STRUCTURE_MISMATCH). La ronda y el chain hash MUST coincidir con la DateKey y con el Provider Profile pinneado, con las reglas y los códigos del paso 8. El cuerpo del stanza tlock es el ciphertext IBE-CCA con el que tlock (drand/tlock, §77) envuelve la file key para la ronda: U || V || W con |U| el tamaño de punto del grupo de claves del scheme —96 bytes, G2, en Quicknet; 48, G1, en pedersen-bls-unchained— y |V| = |W| = 16: 128 bytes en Quicknet. U MUST ser la codificación canónica (§12.2) de un punto de ese grupo distinto del punto en el infinito. El descifrado es el de tlock con la firma verificada en el paso 10; en Quicknet, con e el pairing de G1 × G2: sigma = V XOR H2(e(firma, U)) FK_TIME = W XOR H4(sigma) r = H3(sigma, FK_TIME) y MUST comprobar r·G == U, con G el generador del grupo de U. H2, H3 y H4 son las del paquete encrypt/ibe de drand/kyber, que tlock importa (§77), sobre SHA-256 con las etiquetas IBE-H2, IBE-H3 e IBE-H4. H2 resume el elemento de GT serializado como fija «Serialización de GT en H2», tras este flujo. Un cuerpo de otra longitud, un U que no cumple §12.2 o que es el punto en el infinito, una comprobación r·G == U que falla o una file key que no mide 16 bytes → ERR_INTEGRITY. La apertura autentica además la cabecera age mediante su MAC. 12. Verificar que la estructura resultante coincide con access_policy (§36). En los formatos 2 y 3 con time_and_key, la cabecera de INNER_ACCESS_AGE MUST tener exactamente 16 stanzas (§39) → si no, ERR_POLICY_STRUCTURE_MISMATCH. 13. Si time_and_key: la identity X25519 que abre INNER_ACCESS_AGE MUST recibir y validar el conjunto completo de stanzas; MUST existir uno o más stanzas en formato 1, y exactamente 16 en los formatos 2 y 3; todos MUST ser X25519; debe existir exactamente un stanza por recipient; después, abrir INNER_ACCESS_AGE con la identity adecuada. Se prueba cada identity ofrecida con cada stanza, y los códigos van en este orden: las reglas de stanzas del paso 12 → ERR_POLICY_STRUCTURE_MISMATCH; un stanza X25519 mal formado según la especificación age, que exige un único argumento tras el tipo, el share efímero en Base64 sin padding, de 32 bytes y que no es de orden bajo, y un cuerpo de 32 bytes → ERR_INTEGRITY; alguna identity desenvuelve más de un stanza, aunque otra desenvuelva exactamente uno → ERR_POLICY_STRUCTURE_MISMATCH; ninguna identity desenvuelve ningún stanza → ERR_ACCESS_INVALID; el MAC de la cabecera, STREAM, truncado o datos tras el final → ERR_INTEGRITY. 14. Parsear CONTROL_CBOR canónico, con las capas de §69.1. En la capa 2, su versión de schema (clave 1) MUST ser la que §22 asigna al formato de la cápsula, el VERSION del paso 2 (§31) → si no, ERR_UNSUPPORTED_VERSION. En la capa 3, las reglas de las claves 6 y 7 (§31): presentes en las versiones 2 y 3 y ausentes en la 1, L de 8 bytes y como mucho 2⁵³ − 2⁴⁶, código 1 o 2 → si no, ERR_NON_CANONICAL_CBOR. Validar sus extensiones críticas (clave 4) como en el paso 4. 15. Verificar header_binding (§26) → ERR_HEADER_BINDING. Desde aquí la data de las extensiones de PUBLIC_HEADER queda vinculada al control abierto: coherencia interna (§36.1), no autoría, salvo que la cubra una extensión de firma (§72). 16. Recuperar I_PAYLOAD. En los formatos 2 y 3, recuperar también L y el código de relleno (claves 6 y 7) y calcular P = regla(L) (§29.1). Este paso no falla: el paso 14 ya validó las dos claves. L y P no son longitudes de trama: no se reserva memoria ni disco según ellas (§57). 17. Abrir PAYLOAD_AGE usando I_PAYLOAD. La identity de payload MUST recibir y validar el conjunto completo de stanzas y MUST rechazar el fichero salvo que contenga exactamente un stanza X25519 (→ ERR_POLICY_STRUCTURE_MISMATCH) para R_PAYLOAD. Un stanza mal formado o que I_PAYLOAD no desenvuelve, porque no es para R_PAYLOAD (§30.1), el MAC de la cabecera, STREAM, truncado o datos tras el final → ERR_INTEGRITY. En los formatos 2 y 3, además (§29.1): el plaintext MUST medir exactamente P bytes; sus bytes L a P − 1 MUST valer 0x00; → si no, ERR_INTEGRITY. Un lector en streaming conoce P antes de descifrar el primer byte (paso 16): MAY fallar en cuanto el plaintext supera P bytes o, en formato 2, aparece un byte de relleno distinto de 0x00; un plaintext de menos de P bytes se detecta al final del STREAM. Un lector que conoce la longitud de PAYLOAD_AGE MAY comprobar, tras las reglas de stanzas, que es 184 + P + 16·max(1, ⌈P / 65536⌉) (§62.1). Todos estos fallos tienen el mismo código, así que el momento en que se detectan no lo cambia. En formato 3, los L primeros bytes del plaintext son BODY (§29.2), y el paso se divide en estos subpasos, en este orden: 17.1 descifrar PAYLOAD_AGE hasta EOF, con las comprobaciones de arriba salvo la del relleno, que es 17.8; 17.2 la trama del contenido y el área (§29.2) → ERR_INTEGRITY; 17.3 SECURITY_CBOR, capas 2 y 3 (§29.3), sin código; 17.4 HEAD_CBOR, capas 2 a 4 (§29.4 a §29.6) → ERR_UNSUPPORTED_VERSION, ERR_NON_CANONICAL_CBOR, ERR_HEAD_INVALID o los de sus extensiones críticas; 17.5 el end del último fichero es C, o C es 0 sin ficheros → si no, ERR_INTEGRITY; 17.6 los veredictos de security (§29.7), sin código; 17.7 el SHA-256 de cada fichero (§29.4) → ERR_INTEGRITY; 17.8 el relleno, como arriba → ERR_INTEGRITY. Precedencia en formato 3: un fallo de age después de su cabecera —MAC, STREAM, truncado o datos tras el final— o un plaintext de longitud distinta de P es ERR_INTEGRITY y prevalece sobre todo lo demás. Si no hay ninguno, decide el primer subpaso que falla de 17.2, 17.4, 17.5, 17.7 y 17.8, y dentro de él la primera capa (§69.1); 17.3 y 17.6 nunca fallan: solo fijan los veredictos (§29.3). Tras un fallo, el lector deja de entregar ficheros y sigue leyendo PAYLOAD_AGE hasta EOF. Solo MAY detenerse antes con ERR_INTEGRITY, y solo cuando ese ya es el código final: por un fallo de age o de la longitud del plaintext, o por un subpaso de ERR_INTEGRITY sin un fallo anterior de otro código. Cualquier otro código del paso 17 MUST informarse solo después de leer PAYLOAD_AGE hasta EOF, sin ningún fallo de age y con un plaintext de exactamente P bytes. 18. Commit del resultado solo si el paso 17 termina sin error, con todas sus comprobaciones: formato 1 → el plaintext completo de PAYLOAD_AGE; formato 2 → sus L primeros bytes, el contenido. El relleno nunca se entrega (§56). formato 3 → los ficheros, cada uno con su ruta, y el comentario, el autor declarado y los veredictos (§29.7); nunca la trama, el área ni el relleno (§56). ``` La inspección de los pasos 5, 6 y 8 es un SHOULD de fail-fast; la lectura de `SEALED_CONTROL` en el paso 5 es obligatoria (§23). La aplicación de las reglas de cardinalidad durante los pasos 11, 13 y 17 es un MUST. Una implementación no puede considerar válido un `.dkc` únicamente porque `age` haya podido desenvolver una file key: debe verificar también que el conjunto completo de stanzas cumple la política DateKeys V1 y, en los formatos 2 y 3, que `INNER_ACCESS_AGE` tiene 16 stanzas y que el plaintext de `PAYLOAD_AGE` tiene la longitud y el relleno de su L (§29.1); en formato 3, también la trama, el head y el SHA-256 de cada fichero (§29.2, §29.4). Precedencia: el código que se informa es el del primer paso que falla y, dentro de un objeto, el de la primera capa que falla (§69.1). Una implementación que omite la inspección de los pasos 5, 6 u 8 detecta los mismos fallos, con el mismo código, en los pasos 11 y 17, salvo que antes falle otro paso; §69.1 delimita lo que eso cambia, y los vectores oficiales suponen que esa inspección se realiza. La `.dkk` es una entrada distinta del `.dkc`. Una implementación MAY decodificarla al recibirla, antes del paso 1, pero MUST informar de cualquier error suyo —de trama, de schema o de sus campos— solo en el paso 9.a, en el orden de ese paso, y nunca si `access_policy` es `time_only`. `PAYLOAD_AGE` comienza exactamente en: ```text 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN ``` Una implementación MAY saltar directamente a ese offset y leer solo la cabecera `age` necesaria para la inspección previa. No es necesario leer ni descifrar el payload completo y esta comprobación no rompe el procesamiento en streaming. Siempre que sea viable, una implementación SHOULD validar todo lo verificable localmente antes de realizar una petición de red o utilizar un secreto. Además de fallar antes, esta regla evita que cápsulas inválidas generen consultas observables en relays o en la Release API. Serialización de GT en H2. H2, del paso 11, es SHA-256 de la etiqueta `IBE-H2` seguida de los 576 bytes del elemento de GT, truncado a los 16 bytes de V. Esos bytes MUST ser la serialización de kilic/bls12-381, la que usa drand/kyber (§77), sobre la torre Fp2 = Fp[u]/(u² + 1), Fp6 = Fp2[v]/(v³ − (u + 1)) y Fp12 = Fp6[w]/(w² − v): en cada nivel, los coeficientes de mayor a menor grado, y cada elemento de Fp, entre 0 y p − 1, en 48 bytes big-endian: ```text Fp12 = c0 + c1·w → c1 || c0 (288 bytes cada uno) Fp6 = c0 + c1·v + c2·v² → c2 || c1 || c0 (96 bytes cada uno) Fp2 = c0 + c1·u → c1 || c0 (48 bytes cada uno) ``` Es el orden de las coordenadas de G2 en §12.2, c1 antes que c0, en cada nivel de la torre. Una serialización que empieza por c0 en cada nivel, como `Fp12.toBytes` de noble, da otro H2 y otra file key. Vector (`testdata/vectors/tlock_ibe.json`): con G1 y G2 los generadores de sus grupos, H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`; fija a la vez el pairing e y la serialización. --- ## 64. Mutation tests obligatorios Las listas valen para los tres formatos: el corpus oficial las aplica a fixtures de formato 3 y conserva las de los formatos 1 y 2 como casos de compatibilidad (§70). Antes de v1.0 deben fallar, como mínimo: ```text PUBLIC_HEADER_A + SEALED_CONTROL_B SEALED_CONTROL_A + PAYLOAD_AGE_B DateKey A + release de round B chain hash cambiado version cambiada a 4 flags != 0 reserved != 0 payload truncado payload age modificado control modificado dk1_ JSON no canónico perfil desconocido release de otra ronda access_policy=time_only con estructura time_and_key access_policy=time_and_key con estructura time_only stanza adicional en OUTER_TIME_AGE stanza adicional en PAYLOAD_AGE stanza de tipo distinto de X25519 en INNER_ACCESS_AGE ronda del stanza tlock distinta de DateKey.round chain hash del stanza tlock distinto del Provider Profile pinneado data de extensión de tipo distinto de bstr data de extensión vacía (h'') 65 extensiones en un mismo array ``` y, con el código y el paso de §63 en que fallan, las de la codificación canónica de puntos (§12.2): ```text U del stanza tlock con c0 + p ERR_INTEGRITY, paso 11 U del stanza tlock en el infinito ERR_INTEGRITY, paso 11 U con el bit de infinito y carga ERR_INTEGRITY, paso 11 cuerpo del stanza tlock de 127 bytes ERR_INTEGRITY, paso 11 cuerpo del stanza tlock de 129 bytes ERR_INTEGRITY, paso 11 firma del release con x + p ERR_RELEASE_INVALID, paso 10 firma del release en el infinito ERR_RELEASE_INVALID, paso 10 firma del release con el bit de infinito y carga ERR_RELEASE_INVALID, paso 10 firma del release negada ERR_RELEASE_INVALID, paso 10 firma negada y U con c0 + p ERR_RELEASE_INVALID, paso 10 ``` En las mutaciones de U y del cuerpo, la cabecera `age` de `OUTER_TIME_AGE` conserva un MAC válido, que el creador, o cualquiera una vez publicada la ronda, puede calcular: solo las reglas del paso 11 las rechazan. La firma con x + p necesita un release publicado cuya x cumpla x + p < 2³⁸¹. La firma negada es una codificación canónica que no verifica; junto a un U con c0 + p, fija que el paso 10 termina antes de que el paso 11 lea U. y, con el código y el paso de §63 en que fallan, las del formato 2 (§22, §29.1, §31, §39), sobre fixtures de formato 2 salvo donde se indica: ```text VERSION 4 ERR_UNSUPPORTED_VERSION, paso 2 time_only con VERSION 3 ERR_UNSUPPORTED_VERSION, paso 14 time_only de formato 1 con VERSION 2 ERR_UNSUPPORTED_VERSION, paso 14 time_and_key de formato 1, un stanza, con VERSION 2, y la identity ERR_POLICY_STRUCTURE_MISMATCH, paso 12 time_only con VERSION 1 ERR_UNSUPPORTED_VERSION, paso 14 time_and_key con VERSION 1 y la identity ERR_UNSUPPORTED_VERSION, paso 14 INNER_ACCESS_AGE con 15 stanzas ERR_POLICY_STRUCTURE_MISMATCH, paso 12 INNER_ACCESS_AGE con 17 stanzas ERR_POLICY_STRUCTURE_MISMATCH, paso 12 16 stanzas, dos para un mismo recipient, y la identity de ese recipient ERR_POLICY_STRUCTURE_MISMATCH, paso 13 identity que no es recipient de ninguno de los 16 ERR_ACCESS_INVALID, paso 13 CONTROL_CBOR de versión 2 sin la clave 6 ERR_NON_CANONICAL_CBOR, paso 14 CONTROL_CBOR de versión 2 sin la clave 7 ERR_NON_CANONICAL_CBOR, paso 14 código de relleno 0 ERR_NON_CANONICAL_CBOR, paso 14 código de relleno 3 ERR_NON_CANONICAL_CBOR, paso 14 L = 2⁵³ − 2⁴⁶ + 1 ERR_NON_CANONICAL_CBOR, paso 14 L de 7 bytes ERR_NON_CANONICAL_CBOR, paso 14 L de 9 bytes ERR_NON_CANONICAL_CBOR, paso 14 L como entero CBOR ERR_NON_CANONICAL_CBOR, paso 14 último byte de relleno distinto de 0x00 ERR_INTEGRITY, paso 17 plaintext de P − 1 bytes ERR_INTEGRITY, paso 17 plaintext de P + 256 bytes ERR_INTEGRITY, paso 17 plaintext sin relleno, de L bytes ERR_INTEGRITY, paso 17 código 2 cambiado a 1, con L = 78000 ERR_INTEGRITY, paso 17 L − 1, con el último byte del contenido distinto de 0 ERR_INTEGRITY, paso 17 ``` Las mutaciones de `VERSION` editan un byte de un fixture. Todas las de `time_and_key` ofrecen la identity, `access_material` en su forma `AGE-SECRET-KEY-1…`, y no la `.dkk`: cualquier cambio del `.dkc` rompe el `capsule_digest` de una `.dkk` que lo lleva, como las que escribe la implementación de referencia, y la mutación fallaría antes, en el paso 9.a, con `ERR_ACCESS_INVALID` (§43, §69.1). Las de `INNER_ACCESS_AGE` las construye quien conoce `FK_ACCESS`, como el creador. Las de `CONTROL_CBOR` sellan otro control, lo que en `time_only` puede hacer cualquiera (§36.1). Las que cambian la longitud de `SEALED_CONTROL`, también las de 15 y 17 stanzas, recalculan el PRELUDE y `header_binding`. Las del plaintext las construye quien conoce `I_PAYLOAD`. Todas conservan MAC válidos: solo las reglas de su paso las rechazan. y, con el código y el paso de §63 en que fallan, las del formato 3 (§29.2 a §29.6), sobre fixtures de formato 3: ```text VERSION 4 ERR_UNSUPPORTED_VERSION, paso 2 time_only con VERSION 2 ERR_UNSUPPORTED_VERSION, paso 14 AREA_LEN 0, 511, 513 o 66048 ERR_INTEGRITY, paso 17 SECURITY_LEN 0, o mayor que AREA_LEN ERR_INTEGRITY, paso 17 HEAD_LEN 0, o 2²⁴ + 1 ERR_INTEGRITY, paso 17 12 + AREA_LEN + HEAD_LEN = L + 1 ERR_INTEGRITY, paso 17 L < 12 ERR_INTEGRITY, paso 17 un byte distinto de 0x00 en el área ERR_INTEGRITY, paso 17 head de versión 2 ERR_UNSUPPORTED_VERSION, paso 17 head con otro type tag ERR_NON_CANONICAL_CBOR, paso 17 head con un byte de más dentro de HEAD_LEN ERR_NON_CANONICAL_CBOR, paso 17 rutas "b" y "a", en ese orden ERR_NON_CANONICAL_CBOR, paso 17 rutas "b/.." y "a", en ese orden ERR_NON_CANONICAL_CBOR, paso 17 ruta ".." ERR_HEAD_INVALID, paso 17 ruta "/a" ERR_HEAD_INVALID, paso 17 rutas "A.txt" y "a.txt" ERR_HEAD_INVALID, paso 17 rutas "ab" y "a", U+200C, "b" ERR_HEAD_INVALID, paso 17 ruta "CON.txt" ERR_HEAD_INVALID, paso 17 ruta "CON.txt" en formas de ancho completo ERR_HEAD_INVALID, paso 17 ruta "ABCDEF~1" ERR_HEAD_INVALID, paso 17 ruta con U+202E ERR_HEAD_INVALID, paso 17 ruta ".datekeys-x" ERR_HEAD_INVALID, paso 17 comentario con U+202E ERR_HEAD_INVALID, paso 17 ruta de 1025 bytes ERR_NON_CANONICAL_CBOR, paso 17 ruta "a." ERR_HEAD_INVALID, paso 17 rutas "A" y "a/b" ERR_HEAD_INVALID, paso 17 65 536 carpetas implícitas ERR_HEAD_INVALID, paso 17 autor declarado con LF ERR_HEAD_INVALID, paso 17 start de la primera entrada distinto de 0 ERR_HEAD_INVALID, paso 17 end − start distinto de size ERR_HEAD_INVALID, paso 17 ruta "a" seguida de VS16 ERR_HEAD_INVALID, paso 17 ruta con dos ZWJ seguidos ERR_HEAD_INVALID, paso 17 comentario con la etiqueta U+E0041 ERR_HEAD_INVALID, paso 17 comentario con el selector de variante VS17 ERR_HEAD_INVALID, paso 17 start de una entrada distinto del end de la anterior ERR_HEAD_INVALID, paso 17 end del último fichero distinto de C ERR_INTEGRITY, paso 17 un byte de un fichero cambiado ERR_INTEGRITY, paso 17 ruta ".." y un byte de relleno distinto de 0x00 ERR_HEAD_INVALID, paso 17 ruta ".." y el chunk siguiente de STREAM corrupto ERR_INTEGRITY, paso 17 ruta ".." y corte exacto tras el chunk que la contiene ERR_INTEGRITY, paso 17 ``` Las mutaciones del formato 3 cambian el plaintext de `PAYLOAD_AGE`: las construye quien conoce `I_PAYLOAD`, sellan otro control cuando cambia L y conservan MAC válidos, salvo el chunk corrupto y el corte, que rompen STREAM. La forma de ancho completo de «CON.txt» falla por R6c. Tres casos más se abren, sin código, con su veredicto: `security` de versión 2 (X), una firma de `alg` 1 (F1) y un sello de `seal_type` 1 (S1). --- ## 65. Test vectors Quicknet Deben incluir: - timestamp exactamente en frontera; - un segundo antes; - un segundo después; - timestamp con fracción de segundo inmediatamente posterior a una frontera de ronda; - fecha 2030-01-01; - fechas próximas al genesis. Cada vector: ```text requested timestamp expected round effective timestamp ``` --- ## 66. Test vectors `dk1_` Cada vector: ```text logical object exact canonical JSON bytes exact Base64URL bytes final dk1_ string ``` La cadena textual debe ser única. --- ## 67. Test vectors `.dkc` Los vectores `.dkc` V1 son **fixtures de descifrado y validación**, no pruebas que exijan reproducir byte a byte una llamada pública a `age.Encrypt`. La implementación de `age` genera internamente randomness que su API pública no permite inyectar de forma estable. El protocolo NO exige parchear `age` ni `tlock` para controlar: - file keys internas; - efímeros X25519; - nonces internos. Cada vector oficial incluirá: - bytes `.dkc` fijos previamente generados; - `PUBLIC_HEADER` esperado; - PRELUDE esperado; - DateKey esperada; - `header_binding` esperado; - estructura esperada (`time_only` o `time_and_key`); - identity `.dkk` cuando corresponda; - `CONTROL_CBOR` esperado después de abrir; - `I_PAYLOAD` esperado; - plaintext final esperado; - resultado esperado de cada etapa de verificación; - el formato (1, 2 o 3); - en los formatos 2 y 3: L, el código de relleno y P esperados, y en `time_and_key`, los 16 stanzas de `INNER_ACCESS_AGE`, con el que abre cada credencial; - en formato 2, el plaintext final esperado son los L primeros bytes del plaintext de `PAYLOAD_AGE`; - en formato 3, el head y `security` esperados, cada fichero con su ruta, su tamaño, su SHA-256, su mtime y sus bytes, y los veredictos. Qué stanza abre cada credencial es un dato de prueba: fuera de los vectores, un escritor no lo entrega (§39). Los fixtures de la v0.8.2 se conservan como fixtures de compatibilidad de formato 1: un lector MUST abrirlos con la semántica de la v0.8.2 (§70). Los de la v0.9 se conservan como fixtures de compatibilidad de formato 2, con la semántica de la v0.9. Los de formato 2 cubren, como mínimo: - las dos políticas; - los dos códigos de relleno, con un mismo L ≥ 8193 para el que dan P distintas; - L = 0 (P = 256); - una credencial y 15 señuelos; - varias credenciales, entre ellas una clave portable; - 16 credenciales, sin señuelos; - extensiones en `PUBLIC_HEADER` y en `CONTROL_CBOR`. Son `format2_time_only.dkc` (código 2, L = 78 000, P = 79 872), `format2_time_only_bloque256.dkc` (el mismo contenido con el código 1, P = 78 080), `format2_empty_payload.dkc`, `format2_time_only_extensions.dkc`, `format2_time_and_key_portable.dkc` con su `.dkk`, `format2_time_and_key_recipients.dkc` con su `.dkk` (tres recipients y una clave portable) y `format2_time_and_key_sixteen.dkc`. Un relleno que ocupa varios chunks de STREAM (L = 5 000 000 con el código 2, P = 5 111 808) se prueba mejor con un test que genera la cápsula al ejecutarse que con un fixture de 5 MB. Los de formato 3 cubren, como mínimo: - un solo fichero; - un árbol con carpetas; - un comentario sin ficheros; - los dos códigos de relleno; - `time_and_key` con clave portable; - un área de 1024 bytes, que el lector acepta; - `security` de versión 2, con el veredicto X; - una firma de `alg` 1 con una clave de 32 bytes y una firma de 64, aleatorias, con el veredicto F1; - esa firma y un sello de `seal_type` 1 con un token aleatorio, con F1 y S1. Son `format3_single.dkc`, `format3_tree.dkc`, `format3_comment_only.dkc`, `format3_bloque256.dkc`, `format3_time_and_key_portable.dkc` con su `.dkk`, `format3_area_1024.dkc`, `format3_security_v2.dkc`, `format3_signature_unsupported.dkc` y `format3_seal_unsupported.dkc`. Los vectores de rutas y del head van en `testdata/vectors/paths.json`, `head_schema.json` y `path_fold.json`, y los de `security`, en `security.json`. Los vectores de rutas y del head cubren, como mínimo: U+00A0 y U+3000 al principio y al final de un segmento, con el veredicto que dan las tablas, que para U+3000 es el rechazo por R6c; best-fit; alias 8.3, «~1» incluido; Cn; `["b/..", "a"]`; U+206A a U+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; «ab» con y sin ZWNJ; «¿», «§» y «♥», que se aceptan; VS16 tras un emoji que lo admite, que se acepta, y tras «a», que no; ZWJ al principio, al final y dos seguidos; y la bandera arcoíris, que se acepta, y la de Escocia, que no. Los de `head_schema.json` cubren además un comentario con etiquetas y con selectores de variante sueltos. Los de `security.json` cubren: 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); y un `seal` que incumple su schema con un `seal_type` desconocido (S2). Los vectores de las reglas de relleno (§29.1) van en un fichero propio, `testdata/vectors/padding.json`, con L, código, P, la longitud de `PAYLOAD_AGE`, L_MAX y, como dato informativo, E, S y lastBits. La conformidad se demuestra descifrando/verificando el fixture y comparando resultados intermedios y plaintext. --- ## 68. Test vectors `.dkk` Los vectores `.dkk` son igualmente fixtures de parseo, validación y uso. Cada vector incluirá: - bytes `.dkk` fijos; - `credential_id` esperado; - `capsule_id` esperado; - `access_type` esperado; - 32 bytes crudos esperados de X25519 identity; - `verification_metadata` esperado cuando exista; - extensiones esperadas, con los bytes exactos de su `data`; - resultado esperado al utilizar la identity contra el `INNER_ACCESS_AGE` asociado. Al menos un vector oficial `.dkk` MUST incluir una extensión con `data`. Al menos un vector oficial `.dkk` MUST acompañar a una cápsula de formato 2, y otro, a una de formato 3. El formato de la `.dkk` no cambia (§40). No se exige reproducir los bytes de una `.dkk` partiendo de una generación aleatoria nueva. --- ## 69. Errores normativos ```text ERR_INVALID_MAGIC ERR_UNSUPPORTED_VERSION ERR_INVALID_FLAGS ERR_NON_CANONICAL_CBOR ERR_UNKNOWN_PROFILE ERR_PROFILE_MISMATCH ERR_DATEKEY_INVALID ERR_DATEKEY_NON_CANONICAL ERR_ROUND_MISMATCH ERR_RELEASE_UNAVAILABLE ERR_RELEASE_INVALID ERR_ACCESS_REQUIRED ERR_ACCESS_INVALID ERR_POLICY_STRUCTURE_MISMATCH ERR_HEADER_BINDING ERR_INTEGRITY ERR_EXTENSION_CRITICAL_UNKNOWN ERR_EXTENSION_DATA_INVALID ERR_HEAD_INVALID ``` `ERR_HEAD_INVALID` es nuevo en la v0.10: un head de formato 3 bien codificado cuyos campos incumplen sus reglas (§29.4 a §29.6). --- ## 69.1 Precedencia de errores Cuando unos bytes violan varias reglas, una implementación MUST informar del código de la primera capa que falla, de modo que el código no dependa de su estrategia interna de validación: validar el schema entero antes que los campos o al revés, detenerse en el primer error o recorrer todo el objeto. §57 dice qué código corresponde a cada regla; esta sección, cuál se informa cuando fallan varias. Cada objeto —Provider Profile, `PUBLIC_HEADER`, `CONTROL_CBOR`, `.dkk` y, en formato 3, el head y `SECURITY_CBOR`— se valida en cuatro capas, en este orden. `SECURITY_CBOR` nunca da código: sus fallos solo cambian los veredictos (§29.3). 1. **Trama.** La del `.dkc` para `PUBLIC_HEADER` (§22, §23) y la de la `.dkk` para su cuerpo (§40): magic, prelude completo, versión de trama —el formato, 1, 2 o 3, en el `.dkc` (§23), y 1 en la `.dkk` (§40)—, `FLAGS`, `RESERVED`, longitudes y límites de §57, y la presencia de los bytes que declara cada longitud, en el orden de esas secciones: `ERR_INVALID_MAGIC`; un prelude truncado, `ERR_INTEGRITY`, antes que la versión; después `ERR_UNSUPPORTED_VERSION`, `ERR_INVALID_FLAGS` y `ERR_INTEGRITY`. Un objeto que supera el límite de su trama es `ERR_INTEGRITY`. El Provider Profile y `CONTROL_CBOR` no tienen trama propia; `CONTROL_CBOR` queda acotado por `SEALED_CONTROL`. En formato 3, la trama del head y de `SECURITY_CBOR` es la del contenido (§29.2): sus tres longitudes y sus rangos, que quepan en L y los ceros del área, con `ERR_INTEGRITY` en el paso 17. 2. **Tipo y versión de schema**, leídos antes que nada de las claves 0 y 1 (§70). El objeto MUST empezar, con cada cabecera en su forma más corta y dentro del perfil de §58, por: una cabecera de mapa de longitud definida que anuncia al menos dos entradas y no más de la mitad de los bytes que la siguen, porque cada entrada ocupa al menos dos; la clave 0; una cadena de texto, el type tag; la clave 1; y un entero sin signo de como mucho 2⁵³ − 1, la versión. Cualquier otra cosa ahí, incluida una versión de 2⁵³ o más, es `ERR_NON_CANONICAL_CBOR`. Después, un type tag distinto del propio del schema es `ERR_NON_CANONICAL_CBOR`, sea cual sea la versión. Solo entonces una versión que el objeto no admite es `ERR_UNSUPPORTED_VERSION`, sea lo que sea lo que la sigue: claves desconocidas, elementos fuera del perfil, truncado o bytes sobrantes. El Provider Profile, `PUBLIC_HEADER`, la `.dkk`, el head y `SECURITY_CBOR` admiten la versión 1; `CONTROL_CBOR`, la que §22 asigna al formato de su cápsula (§31). Es la única regla de esta capa que depende de otro objeto. 3. **Codificación y schema.** El perfil de §58, la igualdad con la recodificación y toda regla normativa de `datekeys.cddl` para el objeto, incluidos tipos, tamaños, rangos, el máximo de 64 extensiones, el orden y la unicidad de `extension_id` (§54), en el head R1 y R8 (§29.5), en `CONTROL_CBOR` las reglas de las claves 6 y 7 —presentes en las versiones 2 y 3 y ausentes en la 1, L de 8 bytes y como mucho L_MAX, código 1 o 2 (§31)— y los límites de implementación que se apliquen con ese código (§74): `ERR_NON_CANONICAL_CBOR`. Quedan fuera las reglas a las que §57 asigna código propio —la sintaxis de `compact_datekey`, `access_type`, la longitud de `access_material`, los nombres y la clave pública del Provider Profile—: de ellas, esta capa solo comprueba el tipo CBOR del campo, y el resto pasa a la capa 4. 4. **Campos semánticos** con código propio (§57), cada uno ya con el tipo CBOR de su regla, en orden ascendente de clave: - Provider Profile: los campos del punto 2 de §12.1 → `ERR_UNKNOWN_PROFILE`; después, porque relaciona varias claves, la autocomprobación de `chain_hash` → `ERR_PROFILE_MISMATCH`; - `PUBLIC_HEADER`, en el paso 4 de §63: la DateKey de la clave 3 (§19) → `ERR_DATEKEY_INVALID` o `ERR_DATEKEY_NON_CANONICAL`, y su perfil pinneado → `ERR_UNKNOWN_PROFILE`; después, las extensiones críticas de la clave 5; - `CONTROL_CBOR`, en el paso 14: las extensiones críticas de la clave 4; - `.dkk`, en el paso 9.a: `access_type` y `access_material`, claves 4 y 5 → `ERR_ACCESS_INVALID`; después, las extensiones críticas de la clave 7; - el head, en el paso 17: el comentario y el autor declarado, claves 3 y 4 (§29.6), y los ficheros de la clave 5, con las reglas y el orden de §29.5 → `ERR_HEAD_INVALID`; después, las extensiones críticas de la clave 6. En un array de extensiones críticas, primero cualquier extensión desconocida, también una conocida que no está registrada para ese objeto y ese array → `ERR_EXTENSION_CRITICAL_UNKNOWN`; solo si todas son conocidas, una con `data` inválida → `ERR_EXTENSION_DATA_INVALID` (§54). En esta capa, las extensiones no críticas nunca producen error. Entre objetos y entre pasos decide el orden de §63: el primer paso que falla determina el código. Las comprobaciones que relacionan un objeto con otro, o con el perfil resuelto, pertenecen a su paso y no a la capa 4 del objeto: la cota de `round_time` (paso 7, §15), los argumentos del stanza tlock (paso 8), el vínculo de una `.dkk` con su `.dkc` (paso 9.a), la estructura frente a `access_policy` y, en los formatos 2 y 3, los 16 stanzas de `INNER_ACCESS_AGE` (paso 12), la versión de `CONTROL_CBOR` frente al formato (paso 14, en su capa 2), `header_binding` (paso 15), y la longitud y el relleno del plaintext de `PAYLOAD_AGE` frente a L y el código (paso 17), y en formato 3, el `end` del último fichero frente a C y el SHA-256 de cada fichero (paso 17). En formato 3, dentro del paso 17 decide el orden de sus subpasos: un fallo de `age` o de la longitud del plaintext prevalece, y si no lo hay, decide el primer subpaso que falla (§63). La presencia de los bytes de `SEALED_CONTROL` se comprueba en el paso 5, después de validar `PUBLIC_HEADER` en el paso 4. Alcance. La garantía vale para un mismo conjunto de comprobaciones. §63 deja opcionales la inspección previa de los pasos 5, 6 y 8 (SHOULD) y la comprobación de `capsule_digest` en el paso 9.a (§43). Una implementación que omite alguna detecta esos fallos más tarde, o no los detecta, y puede informar antes el código de otro paso; los vectores oficiales y los ejemplos de abajo suponen que se realizan todas. La lectura de `SEALED_CONTROL` en el paso 5 no es opcional: si faltan sus bytes, el código es `ERR_INTEGRITY` en ese paso (§23). Los códigos del paso 10 son los de un release que el llamador suministra directamente, como en los vectores oficiales: una fuente que obtiene releases por red descarta el que no cumple las reglas del paso 10, y si ninguna entrega uno que las cumpla el código es `ERR_RELEASE_UNAVAILABLE`, en el paso 9 (§63). Ninguna otra elección de la implementación puede cambiar el código: ni el orden de sus comprobaciones dentro de una capa, ni decodificar la `.dkk` antes del paso 1 (§63), ni el orden en que prueba las identities del paso 13, ni el momento en que un lector en streaming detecta un fallo del paso 17, ni la comprobación opcional de la longitud de `PAYLOAD_AGE` en ese paso, que tiene el mismo código. En formato 3, eso exige que un lector solo se detenga antes de EOF con `ERR_INTEGRITY`, y solo cuando ese ya es el código final (§63, paso 17). Ejemplos, reproducibles con los vectores oficiales o con los tests de la implementación de referencia (§76): | Entrada | Código | |---|---| | Type tag de otro schema y versión 2 | `ERR_NON_CANONICAL_CBOR` | | Versión 2, una clave desconocida y una DateKey inválida | `ERR_UNSUPPORTED_VERSION` | | `access_policy` 2 y la DateKey `dk1_x` | `ERR_NON_CANONICAL_CBOR` | | DateKey `dk1_x` y una extensión crítica desconocida | `ERR_DATEKEY_INVALID` | | Extensión crítica con `data` inválida seguida, en el array, de otra desconocida | `ERR_EXTENSION_CRITICAL_UNKNOWN` | | Extensión crítica con `data` inválida seguida, en el array, de una conocida registrada solo como no crítica | `ERR_EXTENSION_CRITICAL_UNKNOWN` | | Provider Profile con un `provider` inválido y un `chain_hash` que no corresponde | `ERR_UNKNOWN_PROFILE` | | `.dkk` de otra cápsula con una extensión crítica desconocida | `ERR_EXTENSION_CRITICAL_UNKNOWN`, paso 9 | | Cápsula `time_and_key` con `FLAGS` 1 y una `.dkk` sin magic `DKK1` | `ERR_INVALID_FLAGS`, paso 2 | | Cápsula `time_only` válida y una `.dkk` sin magic `DKK1` | se abre: la `.dkk` no interviene | | Cápsula `time_and_key` sin credenciales y con el reloj antes de `round_time` | `ERR_ACCESS_REQUIRED`, paso 9 | | Release de otra ronda con una firma válida para esa ronda, suministrado directamente | `ERR_ROUND_MISMATCH`, paso 10 | | El mismo release, única respuesta de un relay | `ERR_RELEASE_UNAVAILABLE`, paso 9 | | Firma del release negada y U del stanza tlock con c0 + p | `ERR_RELEASE_INVALID`, paso 10 | | Dos identities: una desenvuelve un stanza de `INNER_ACCESS_AGE` y la otra dos | `ERR_POLICY_STRUCTURE_MISMATCH`, paso 13 | | `CONTROL_CBOR` con una extensión crítica desconocida y el `header_binding` de otra cabecera | `ERR_EXTENSION_CRITICAL_UNKNOWN`, paso 14 | | Ronda fuera del perfil y cabecera de `PAYLOAD_AGE` mal formada | `ERR_INTEGRITY`, paso 6 | | Cápsula de formato 2 con 15 stanzas en `INNER_ACCESS_AGE` y una identity que abre uno | `ERR_POLICY_STRUCTURE_MISMATCH`, paso 12 | | `CONTROL_CBOR` de versión 1 en una cápsula de formato 2, con una clave desconocida | `ERR_UNSUPPORTED_VERSION`, paso 14 | | Código de relleno 3 y una extensión crítica desconocida en `CONTROL_CBOR` | `ERR_NON_CANONICAL_CBOR`, paso 14 | | Código de relleno 3 y el `header_binding` de otra cabecera | `ERR_NON_CANONICAL_CBOR`, paso 14 | | Extensión crítica desconocida en `CONTROL_CBOR` y un byte de relleno distinto de 0x00 | `ERR_EXTENSION_CRITICAL_UNKNOWN`, paso 14 | | Byte de relleno distinto de 0x00 y un stanza adicional en `PAYLOAD_AGE` | `ERR_POLICY_STRUCTURE_MISMATCH`, paso 6 | | Cápsula `time_and_key` de formato 1 con `VERSION` 2 y su `.dkk`, con `capsule_digest` | `ERR_ACCESS_INVALID`, paso 9 | | `CONTROL_CBOR` de versión 2 en una cápsula de formato 3 | `ERR_UNSUPPORTED_VERSION`, paso 14 | | Cápsula de formato 3 con una ruta «..» y el chunk siguiente de `PAYLOAD_AGE` corrupto | `ERR_INTEGRITY`, paso 17 | | Una ruta «..» y el fichero cortado justo después del chunk completo que la contiene | `ERR_INTEGRITY`, paso 17 | | Una ruta «..» y un byte de relleno distinto de 0x00 | `ERR_HEAD_INVALID`, paso 17 | | Una ruta «..» y un fichero con otro SHA-256 | `ERR_HEAD_INVALID`, paso 17 | | Rutas «b/..» y «a», en ese orden | `ERR_NON_CANONICAL_CBOR`, paso 17 | | Head de versión 2 con una ruta «..» | `ERR_UNSUPPORTED_VERSION`, paso 17 | | `AREA_LEN` 513 y un head de versión 2 | `ERR_INTEGRITY`, paso 17 | | `security` ilegible y una ruta «..» | `ERR_HEAD_INVALID`, paso 17 | | `security` ilegible y todo lo demás válido | se abre, con el veredicto X | --- ## 70. Compatibilidad Una implementación V1: - MUST aceptar `DKC1` con `VERSION` 1, 2 o 3, y `DKK1` con `VERSION` 1; - MUST rechazar cualquier otra versión de trama (§23, §40) y toda versión de schema que el objeto no admita (§69.1), salvo la de `SECURITY_CBOR`, que solo da el veredicto X (§29.3); - MUST abrir todo objeto válido de formato 1 con la semántica de la v0.8.2: uno o más stanzas en `INNER_ACCESS_AGE` y el plaintext completo de `PAYLOAD_AGE`, sin relleno; - MUST abrir todo objeto válido de formato 2 con la semántica de la v0.9: 16 stanzas en `INNER_ACCESS_AGE` y, como contenido, los L primeros bytes del plaintext de `PAYLOAD_AGE`, sin nombre ni metadatos; - MUST abrir todo objeto válido de formato 3 (§29.2 a §29.7, §56). Una interfaz que solo sabe entregar un flujo de bytes y recibe una cápsula de formato 3 MUST terminar tras el paso 2, sin red, con un error del llamador sin código de §69, y MUST NOT informarlo como `ERR_UNSUPPORTED_VERSION`: la cápsula es válida; - si escribe cápsulas, MUST escribir el formato 3 y MUST NOT escribir los formatos 1 ni 2 (§62.1); solo un generador de vectores de prueba MAY escribirlos; - SHOULD indicar al llamador el formato de una cápsula abierta o inspeccionada: el formato 1 no oculta el número de credenciales ni la longitud exacta del contenido (§55.2); - MUST rechazar critical extensions desconocidas; - MAY ignorar noncritical extensions desconocidas; - MUST mantener inmutable la interpretación de perfiles publicados. Un lector que solo conoce el formato 1, como los de la v0.8.2, rechaza una cápsula de formato 2 en el paso 2, con `ERR_UNSUPPORTED_VERSION` y sin ninguna petición de red. Si alguien cambia su `VERSION` a 1 sin volver a sellar el control, falla en el paso 14, también con `ERR_UNSUPPORTED_VERSION`, porque su `CONTROL_CBOR` tiene la versión 2; en `time_and_key` con una `.dkk` que lleva `capsule_digest`, antes, en el paso 9.a, con `ERR_ACCESS_INVALID`, si se comprueba (§43). Una cápsula de formato 2 cuyo control no se ha vuelto a sellar nunca entrega el contenido con su relleno ni lo corta. Del mismo modo, un lector de la v0.9 rechaza una cápsula de formato 3 en el paso 2, sin red. Cambiar la `VERSION` de una cápsula entre 2 y 3 sin volver a sellar su control falla en el paso 14, con `ERR_UNSUPPORTED_VERSION`, o antes, en el paso 9.a, con una `.dkk` que lleva `capsule_digest`. Una cápsula de formato 3 cuyo control no se ha vuelto a sellar nunca entrega su `BODY` como si fuera un contenido de formato 2. Las versiones que definan la firma de autor y el sello de tiempo no cambiarán la trama del contenido, el head ni el mapa exterior de `security`: añadirán valores de `alg` y de `seal_type` con sus veredictos, y quizá un área mayor, que todo lector de esta versión acepta (§29.2, §29.3). Un lector de esta versión muestra lo nuevo como no soportado y abre la cápsula. `VERSION` no está autenticado hasta los pasos 14 y 15: lo que impide el downgrade es la versión de `CONTROL_CBOR`, que autentican los MAC de `age` frente a quien no conoce la file key, y `header_binding`. Quien conoce `I_PAYLOAD` y puede sellar un control —cualquiera tras la fecha en `time_only` (§36.1), quien tiene una credencial en `time_and_key`— puede construir una cápsula de formato 1 con el mismo `PAYLOAD_AGE`, que todo lector abre como el contenido seguido de ceros, o, con el de una cápsula de formato 3, una de formato 2 que entrega `BODY` entero como contenido. Es una reescritura (§55.1), no un downgrade: esa misma persona ya puede cifrar cualquier otro plaintext para `R_PAYLOAD`. --- ## 71. Registro de perfiles DateKeys SHOULD publicar un registro de Provider Profiles. Cada entrada debería incluir: - exact canonical CBOR; - SHA-256; - firma offline; - fecha de publicación; - estado. --- ## 72. Registro de extensiones DateKeys MAY publicar un registro de `extension_id`. Registrar una extensión no cambia el núcleo del protocolo. Cada extensión registrada, identificada por `(extension_id, extension_version)`, MUST declarar: - la codificación de su `data`, o que no lleva `data`; - su forma canónica; - su longitud máxima, que no puede superar la trama del objeto que la contiene (§57); - los objetos (`PUBLIC_HEADER`, `CONTROL_CBOR`, `.dkk` o el head de formato 3) y el array (crítico, no crítico o ambos) en que puede aparecer; en otro objeto o array se trata como desconocida (§54); - sus vectores de prueba. Una extensión registrada en `CONTROL_CBOR` vale en todas sus versiones de schema (§31). Un encoder MUST NOT escribir una extensión registrada en un objeto o array para el que no está registrada: allí un lector la trata como desconocida (§54). Si la codificación de `data` es CBOR: - MUST usar el perfil de §58 de forma recursiva, con enteros sin signo de como mucho 2⁵³ − 1, arrays y mapas no vacíos y sin `null`; - los lectores que implementan la extensión MUST rechazar una codificación interna no canónica o fuera de ese perfil como `data` inválida (§54); - su encoder MUST decodificar su propia salida, con las reglas de esos lectores, antes de sellar o escribir el objeto. Una extensión de firma que cubra la `data` de otras extensiones MUST firmar los bytes exactos del contenido de esos `bstr`, nunca una reserialización de su contenido decodificado. Ubicación según el modelo de confianza (§55.1): - una extensión que lleva afirmaciones relevantes para la seguridad —aquellas de las que depende una decisión de seguridad de quien abre la cápsula: autorización, identidad, integridad o fecha— MUST registrarse en `CONTROL_CBOR` o estar firmada por una extensión de firma; ni siquiera en `CONTROL_CBOR` prueba autoría (§36.1); - sin una extensión de firma que la cubra, la `data` de las extensiones de una `.dkk` es solo informativa para quien la posee: una implementación MUST NOT basar en ella una decisión de seguridad sobre la cápsula. Nota: la `data` de `PUBLIC_HEADER` es pública. El protocolo base no la vincula al control hasta que se verifica `header_binding` (§63, paso 15), y esa verificación aporta coherencia interna, no autoría: solo una extensión de firma puede aportar autenticidad del creador (§36.1, §55.1). La de `.dkk` viaja en claro y `header_binding` no la cubre, así que el protocolo base no la autentica en ningún paso. --- ## 73. Decisiones canónicas v0.10 ```text DateKeys = plataforma / protocolo DateKey = condición temporal pública DateKeyCap = .dkc DateKeys Access Key = .dkk Quicknet = provider V1 dk1_ = JSON canónico heredado del prototipo Provider Profile = Deterministic CBOR + genesis_seed capsule_id = 16 bytes aleatorios y opacos DKC framing = sin PAYLOAD_LEN; VERSION es el formato de la cápsula: 1 (v0.8.2), 2 (v0.9) o 3 (v0.10); se escribe solo el 3 PUBLIC_HEADER = Deterministic CBOR exacto profile_id duplicado en header = eliminado; la DateKey es la fuente única header binding = SHA-256(PRELUDE || exact PUBLIC_HEADER bytes) payload = fichero age v1 estándar completo; en los formatos 2 y 3, su plaintext es el contenido seguido de ceros hasta P = regla(L); en formato 3, el contenido es BODY payload access = X25519 identity I_PAYLOAD dentro de CONTROL_CBOR, nueva de un CSPRNG para cada cápsula time_only = age(tlock → CONTROL_CBOR) time_and_key = age(tlock → age(X25519 recipient(s) → CONTROL_CBOR)); en los formatos 2 y 3, exactamente 16 recipients: de 1 a 16 credenciales y señuelos, en orden uniformemente aleatorio portable .dkk = X25519 identity de 32 bytes crudos extensions = único mecanismo genérico de extensibilidad en PUBLIC_HEADER, CONTROL_CBOR, .dkk y el head de formato 3 extension arrays = 1 a 64 extensiones en orden estrictamente ascendente de los bytes UTF-8 de extension_id; extension_id único en cada objeto extension data = bstr no vacío y opaco; el protocolo base nunca lo decodifica CBOR profile = RFC 8949 §4.2.1 con tipos mayores 0, 2, 3, 4 y 5 y claves enteras sin signo parser limits = MUST para encoders y decoders; ninguna trama vacía error precedence = primera capa que falla en cada objeto (trama, tipo y versión, schema, campos); entre objetos, el orden de pasos de §63; los errores de una .dkk, solo en el paso 9.a; en formato 3, dentro del paso 17, prevalece un fallo de age y después decide el primer subpaso trust model = §55.1; autoría solo mediante una extensión de firma BLS12-381 points = una sola codificación válida, la comprimida canónica de drand (§12.2); clave pública, firma y U distintos del infinito tlock stanza body = U || V || W, 128 bytes en Quicknet (§63 paso 11) GT en H2 = orden de kilic/kyber: Fp12 c1 || c0, Fp6 c2 || c1 || c0, Fp2 c1 || c0, cada Fp en 48 bytes big-endian (§63 paso 11) release sources = una fuente de red verifica cada respuesta; sin release, por cualquier causa, ERR_RELEASE_UNAVAILABLE en el paso 9 y ningún otro código; los códigos del paso 10, para un release suministrado directamente extension placement = un encoder no escribe una extensión fuera de los objetos y arrays de su registro, y un lector la trata allí como desconocida (§54, §72) payload padding = códigos 1 (bloque256) y 2 (reforzado, con Padmé), sin opción sin relleno; L y el código en las claves 6 y 7 de CONTROL_CBOR versiones 2 y 3, L en 8 bytes y como mucho 2^53 − 2^46; comprobado en el paso 17 (ERR_INTEGRITY); el SDK usa el 2 por defecto capsule formats = VERSION 1, 2 o 3; la versión de schema de CONTROL_CBOR es la del formato; PUBLIC_HEADER y .dkk, versión 1 en los tres; un lector v0.10 abre los formatos 1 y 2 con la semántica de sus versiones; un lector v0.9 rechaza el formato 3 en el paso 2, sin red privacy = §55.2: visibles la fecha, access_policy, capsule_id y P; ocultos L, el número de credenciales, los recipients y, en formato 3, las rutas, los tamaños, los hashes y las fechas de los ficheros writer rules = §62.1: MUST para formato, credenciales, señuelos, aleatoriedad, L, SEALED_CONTROL_LEN y errores, y en formato 3, para el área, el head, las rutas, el texto, la mtime, la autodecodificación y los ficheros; SHOULD para autocomprobación y borrado content format 3 = BODY: AREA_LEN, SECURITY_LEN y HEAD_LEN en 12 bytes; security en un área de tamaño fijo por versión del spec, 512 bytes en la v0.10; el head; los ficheros concatenados head = versión 1 siempre en formato 3; sal de 32 bytes; comentario y autor declarado opcionales; por fichero, ruta, size, start, end, SHA-256 y mtime opcional; hasta 65 535 ficheros y 16 MiB; ERR_HEAD_INVALID paths = R1 a R10 y R4b, con tablas propias de Unicode 18.0.0 y WindowsBestFit; orden por bytes UTF-8; ningún invisible en el texto del creador salvo ZWJ, ZWNJ, VS15 y VS16 donde su uso es conforme security = mapa de versión 1 con la firma y el sello codificados aparte; nunca decide la apertura; en la v0.10, vacío al escribir y sin ningún alg ni seal_type; veredictos X, F0, F1, S0, S1 y S2 recovery = puede obtener release directamente del provider historical release availability = dependencia explícita del horizonte de recuperación ``` --- ## 74. Aspectos todavía provisionales Antes de v1.0 quedan por cerrar: - schema CBOR final byte-a-byte de `PUBLIC_HEADER`; - schema CBOR final byte-a-byte de `CONTROL_CBOR`; - schema CBOR final byte-a-byte de `.dkk`; - límites definitivos de campos; - vectores definitivos de Provider Profile; - suite exacta de tests interoperables. El framing base, la ausencia de `PAYLOAD_LEN`, el uso de age files estándar, la identity X25519 cruda de `.dkk` y el formato de extensiones (§54) dejan de considerarse provisionales en este borrador. Tampoco lo son los formatos 1, 2 y 3 (§22), los 16 huecos de `INNER_ACCESS_AGE` (§39), las reglas de relleno 1 y 2 (§29.1), ni la trama del contenido, el head y las reglas de rutas y de texto del formato 3 (§29.2 a §29.6). El schema byte a byte de `CONTROL_CBOR`, claves 6 y 7 incluidas, sigue abierto como el de los demás objetos; su versión final conservará una codificación de L de longitud fija (§31, §55.2). Mientras los límites definitivos de campos sigan abiertos, la implementación de referencia aplica los siguientes límites de implementación. No son normativos; se enumeran para que otra implementación pueda reproducir los vectores oficiales, que los aplican. Los cinco primeros son las reglas que `datekeys.cddl` marca como límites de implementación, y quien los aplica usa la correspondencia de errores de §57 y los comprueba en la capa de §69.1 que corresponde a ese código: | Límite de la implementación de referencia | Regla normativa | Código | |---|---|---| | `profile_id` de como mucho 128 bytes, también como `network` de `dk1_` (§19) | el alfabeto de §12.1, sin longitud máxima | `ERR_UNKNOWN_PROFILE` en el Provider Profile; `ERR_DATEKEY_INVALID` en `dk1_` | | `provider`, `network` y `scheme` de como mucho 64 bytes | el alfabeto de §12.1, sin longitud máxima | `ERR_UNKNOWN_PROFILE` | | `public_key` del Provider Profile de 1 a 1024 bytes | 48 o 96 bytes según el scheme (§12.1) | `ERR_UNKNOWN_PROFILE` | | `period` del Provider Profile de 1 a 86400 segundos (un día) | de 1 a 2⁵³ − 1 en la codificación (§11), y como mucho 2³² − 1 para pinnear el perfil (§12.1) | `ERR_NON_CANONICAL_CBOR` | | `extension_id` de como mucho 256 bytes | cadena de texto UTF-8 de al menos 1 byte (§31) | `ERR_NON_CANONICAL_CBOR` | | cadena `dk1_` de como mucho 256 bytes | sin límite | `ERR_DATEKEY_INVALID` | | cabecera `age` de como mucho 1024 stanzas, 128 argumentos tras el tipo de cada stanza y 2 MiB desde la línea de versión hasta la del MAC (los del parser de `filippo.io/age`) | sin límite (§28.1) | `ERR_INTEGRITY` en `OUTER_TIME_AGE` y `PAYLOAD_AGE`; `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12 en `INNER_ACCESS_AGE` (§28.1) | Una entrada que solo supera uno de estos límites no falla por esa causa en una implementación que no lo aplica; en los vectores de schema, los que dependen de un límite lo dicen en su nombre (`testdata/README.md`). Los alfabetos de los nombres, la longitud de la clave pública de cada scheme, la cota de `period` para pinnear y el mínimo de 1 byte de `extension_id` son normativos (§12.1, §31): no dependen de estos límites. El máximo de 16 credenciales (§39), L_MAX (§29.1) y los límites del formato 3 (§29.2 a §29.6) no son límites de implementación: son normativos y no figuran en esta tabla. ### Trabajo futuro Quedan fuera de la v0.10, como trabajo futuro que esta versión no especifica: - el formato de un objeto de release y de su fuente de archivo; - una nueva redacción del modelo de amenazas del proveedor (§7.6); - una extensión de firma, y la firma de autor y el sello de tiempo del área `security` (§29.3), que definirán las versiones siguientes, con el documento «Servicio de sellado DateKeys v1»; - el uso del reloj local en el paso 9.c de §63; - un tipo de acceso post-cuántico. --- ## 75. Requisitos bloqueantes antes de v1.0 1. Schemas CBOR congelados. 2. `profile_hash` vector oficial. 3. Quicknet vectors oficiales. 4. `dk1_` vectors canónicos. 5. fixtures oficiales `.dkc` de descifrado/validación, de formato 3 y de compatibilidad de los formatos 1 y 2. 6. fixtures oficiales `.dkk` de parseo/uso. 7. mutation tests completos, en los tres formatos. 8. strict tlock tests. 9. parser fuzzing. 10. revisión criptográfica externa. 11. vectores de las reglas de relleno (§29.1). 12. tablas de Unicode 18.0.0 y WindowsBestFit fijadas por su SHA-256 (§29.5.1), con sus vectores de rutas. --- ## 76. Política de cambios del borrador v0.8.1 La v0.8.1 congela el diseño normativo del borrador para la fase de implementación e interoperabilidad. A partir de esta versión, un cambio normativo posterior SHOULD responder a un caso reproducible descubierto mediante al menos una de estas fuentes: - implementación de referencia; - schema `datekeys.cddl`; - fixture oficial; - mutation test; - prueba de interoperabilidad; - fuzzing; - segunda implementación independiente; - revisión criptográfica o técnica externa. Nuevas ideas, preferencias editoriales o posibilidades futuras que no estén respaldadas por uno de esos casos SHOULD documentarse fuera del núcleo normativo hasta que exista evidencia suficiente para modificarlo. Esta política no impide correcciones editoriales que no alteren la semántica normativa. ### Cambios normativos de la v0.8.2 La v0.8.2 cierra el formato de las extensiones (§74) con este cambio normativo: - `data` (clave 2) pasa de `any` a `bstr` no vacío y opaco, sin más límite de longitud que la trama de su contenedor; el protocolo base nunca decodifica ni valida su contenido (§31, §54, §57, §58.1); - cada array admite como máximo 64 extensiones (§31, §54); - `extension_version` queda acotado a 2³² − 1, y `period` y `genesis_time` a 2⁵³ − 1 (§11, §54, §58); - se elimina la excepción de multiplicidad: un `extension_id` aparece una sola vez por objeto (§31, §54); - nuevo error `ERR_EXTENSION_DATA_INVALID` para una extensión crítica conocida con `data` inválida (§54, §63, §69); - los límites de §57 pasan de recomendación a MUST para encoders y decoders, con su correspondencia de errores; - §58 nombra el perfil CBOR del protocolo; - §72 fija las reglas de registro de extensiones; - nuevas mutaciones (§64) y un vector `.dkk` con extensión (§68). Antes de publicarse lo completan los refinamientos, la enmienda y las correcciones de los bloques siguientes. Casos reproducibles que lo justifican, obtenidos con la implementación de referencia, sus fixtures y `datekeys.cddl` de la v0.8.1: 1. El CDDL se contradecía: sus líneas 12 a 14 establecen que `null` y los valores vacíos nunca representan ausencia, y su línea 82 declaraba `? 2 => any`. 2. `extension.New(id, v, nil)` emitía `f6` (`null`) como `data`. 3. La misma cápsula de 810 bytes, con `data` de cabecera `{NaN:0, NaN:1}` (`a2f97e0000f97e0001`), se aceptaba de forma no determinista: `Inspect` la aceptó 437 veces de 500 y `Open` 82 de 100. 4. El fixture oficial `time_only_extensions` usaba claves de texto y `true` en `data`. 5. `Encrypt` sellaba una `data` de `CONTROL_CBOR` con 14 arrays anidados que `Open` rechazaba en el paso 14, ya desbloqueada (`exceeded max nested level 16`): la cápsula quedaba irrecuperable. 6. Sin máximo de extensiones, una `PUBLIC_HEADER` de 880 KB con 40 000 + 40 000 extensiones hacía que `DecodeHeader` tardara 8,3 s, por una comprobación de disjunción cuadrática. Las versiones de framing y de schema (clave 1) no cambian. Un objeto v0.8.1 deja de ser válido si la `data` de una extensión no es un `bstr` no vacío, si un array supera 64 extensiones, si repite un `extension_id` al amparo de la antigua excepción de multiplicidad o si un `extension_version` supera 2³² − 1; también un Provider Profile con `period` o `genesis_time` por encima de 2⁵³ − 1. El fixture `time_only_extensions` se regenera. #### Refinamientos de la v0.8.2 La v0.8.2 no se ha publicado todavía, así que estos refinamientos la modifican sin cambiar de versión. Ninguno cambia la codificación de un objeto válido ni el veredicto de un vector o fixture oficial existente: pasan a texto normativo reglas que solo estaban en la implementación de referencia o en `testdata/README.md`, y fijan el código cuando fallan varias reglas a la vez. `dk1.json` gana cuatro vectores, que fijan reglas de lectura de §19. Proceden de la implementación de referencia, de los vectores oficiales y de la revisión de una segunda implementación independiente, que encontró en `testdata/README.md` reglas que la especificación no fijaba. 1. **Precedencia de errores por capas** (§57, §63, §69.1). Casos: los vectores de `cbor.json` «type tag of PUBLIC_HEADER and schema version 2: the type tag is checked first» (`ERR_NON_CANONICAL_CBOR`) y «schema version 2 and an unknown key 11: the version is read first» (`ERR_UNSUPPORTED_VERSION`); una `PUBLIC_HEADER` con `access_policy` 2 y la DateKey `dk1_x`, que la implementación de referencia informaba como `ERR_DATEKEY_INVALID` con su antigua librería CBOR y como `ERR_NON_CANONICAL_CBOR` con su codec propio, sin que la especificación decidiera entre ambos; y una `.dkk` de otra cápsula con una extensión crítica desconocida, que el paso 9 de la referencia informaba como `ERR_ACCESS_INVALID` porque comprobaba `capsule_id` antes que la `.dkk` como objeto, y que ahora es `ERR_EXTENSION_CRITICAL_UNKNOWN`. Las reglas con código propio de §57 no pertenecen a la capa 3: un `dk1_` con padding en `PUBLIC_HEADER` es `ERR_DATEKEY_NON_CANONICAL`, no `ERR_NON_CANONICAL_CBOR`, aunque rompa la expresión regular de `compact-datekey`. Las comprobaciones opcionales —la inspección de los pasos 5, 6 y 8, y la de `capsule_digest`— pueden cambiar el código: la referencia comprueba el digest solo con un `.dkc` que puede releer, y la mutación «capsule_digest of the .dkk does not match» supone que se comprueba. No eran lo único que podía cambiarlo: el código de un release inválido obtenido por red dependía también de si su fuente lo verificaba, hasta que lo fijaron las correcciones de la revisión formal (abajo). Los errores de una `.dkk` se informan en el paso 9.a aunque se decodifique antes: la CLI de referencia la decodificaba antes del paso 1, así que `datekeys decrypt -in time_only.dkc -dkk` con una `.dkk` de `access_type` `mlkem768` fallaba con `ERR_ACCESS_INVALID`, mientras `capsule.Open` abría la misma cápsula con la misma credencial. 2. **Modelo de confianza** (§55.1, §72). Caso: con los bytes públicos de `time_only.dkc` cualquiera construye otra cápsula con el mismo PRELUDE, la misma `PUBLIC_HEADER` y otro plaintext, que supera los 18 pasos; y otra `data` en la extensión de `time_and_key_portable_extension.dkk` abre la cápsula igual. La especificación solo lo decía de `time_only` (§36.1) y de la `data` de las extensiones (§72), sin una regla sobre dónde registrar afirmaciones de seguridad. 3. **Orden de `extension_id`** (§31, §54). Caso: el vector de `cbor.json` con U+FF61 y U+10000 en el orden de UTF-16 (`ERR_NON_CANONICAL_CBOR`); §31 solo pedía al encoder «ordenar por bytes UTF-8», sin definir la comparación ni lo que hace el decoder. Además, `extension_id` tiene al menos 1 byte: el vector «empty extension_id» de `cbor.json` (`ERR_NON_CANONICAL_CBOR`) dependía de ese mínimo, que §31 no decía: el CDDL lo incluía en su bloque de límites de la implementación de referencia (`extension-id = tstr .size (1..256)`), y la tabla de límites de `testdata/README.md` lo daba como rango normativo («1 byte or more») sin citar ninguna sección. 4. **Huecos que la especificación dejaba a la implementación**, con el comportamiento que ya tenía la referencia salvo donde se indica: 1. cabeceras `age` mal formadas, incluida una sin stanzas, que la gramática de C2SP excluye (`1*stanza`) (§28.1, §36): 10 casos de `inspect_differential.json` fallan por una cabecera sin stanzas con `ERR_INTEGRITY`, 5 en el paso 5 y 5 en el paso 6; 2. cota de `round_time` en 9999-12-31T23:59:59Z y rechazo de los instantes anteriores a `genesis_time` (§15): los vectores «after the last representable round» y «genesis - 1s: before the profile» de `quicknet_rounds.json`, «last Quicknet round» de `dk1.json` y 6 casos del paso 7 de `inspect_differential.json`; 3. reglas de lectura de `dk1_` (§19): de ellas dependen «padded Base64URL», «non-zero trailing bits», «fraction notation», «duplicate key» y «byte order mark» en `dk1.json`; esta última dependía de que el parser JSON de la referencia rechaza el BOM, que RFC 8259 permite ignorar. La referencia aceptaba CR y LF dentro del Base64, porque los decodificadores de Go los omiten, e informaba `ERR_DATEKEY_NON_CANONICAL`; ahora falla el paso 1 con `ERR_DATEKEY_INVALID`, como en una implementación que sigue el texto, en la que la revisión diferencial de la segunda implementación encontró 799 discrepancias de esta clase en 20 000 casos. Un número JSON se lee por su valor decimal exacto: con un double, `1.0000000000000001` sería 1. Vectores nuevos de `dk1.json`: «line feed inside the Base64», «carriage return and line feed after the Base64», «version 1.0000000000000001: its exact value, not a double» e «invalid UTF-8 in a member a repeated name overwrites». Este último lo encontró el diferencial de la segunda implementación: `encoding/json` sustituía el UTF-8 inválido por U+FFFD, así que un miembro que un nombre repetido sobrescribe superaba los pasos 2 y 3, y la referencia informaba `ERR_DATEKEY_NON_CANONICAL` en el paso 6; §19 lo hace fallar en el paso 2, con `ERR_DATEKEY_INVALID`; 4. cotas inferiores de las longitudes (§22, §23, §40, §57): un caso de `inspect_differential.json` con `PUBLIC_HEADER_LEN` 0 da `ERR_INTEGRITY` en el paso 2, y otros tres con `FLAGS` distinto de 0 dan `ERR_INVALID_FLAGS`; `BODY_LEN` 0 en una `.dkk`, que la referencia informaba como `ERR_NON_CANONICAL_CBOR` al decodificar un cuerpo vacío, pasa a `ERR_INTEGRITY`, como en §22; 5. comparación de los argumentos del stanza tlock (§35, §63 paso 8): la mutación «tlock round edited by a third party» y los 69 casos del paso 8 de `inspect_differential.json`; `01000`, `+1000` o un chain hash en mayúsculas no coinciden; 6. validación del Provider Profile y fórmula de `chain_hash` (§12.1): el bloque `provider_profile` de `cbor.json`, con «network default, left out of the chain hash» y «genesis_time 253402300799, 9999-12-31T23:59:59Z». Los alfabetos de los nombres son normativos, porque el JSON canónico de `dk1_` no define escapes (§18): de ellos dependen «invalid profile_id» en `cbor.json`, «uppercase network» en `dk1.json` y las mutaciones 46, 746, 1138 y 1754 de `inspect_differential.json` (`ERR_DATEKEY_INVALID` en el paso 4); sus longitudes siguen siendo límites de implementación. `period` es como mucho 2³² − 1 para pinnear el perfil: drand escribe `uint32(period)` en el hash de la información de cadena, y para un valor mayor el hash no está definido; ningún vector lo alcanza, porque el límite de 86400 segundos de la referencia falla antes. Al pinnear un perfil, la referencia aplicaba las reglas de campo antes que el schema: un `period` de 86401 segundos era `ERR_UNKNOWN_PROFILE` en `profile.NewRegistry` y `ERR_NON_CANONICAL_CBOR` en `profile.Decode`; ahora los dos dan el segundo. Un punto de G1 que está en la curva pero fuera del subgrupo de orden primo es `ERR_UNKNOWN_PROFILE`; 7. límites de implementación (§74): el vector «period of one day and one second, above the implementation limit» de `cbor.json`; en `INNER_ACCESS_AGE`, una cabecera que supera los del parser es `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12, como toda cabecera mal formada ahí (§28.1); 8. credenciales y reloj en el paso 9 (§63): las mutaciones «time_and_key without credentials» (`ERR_ACCESS_REQUIRED`, paso 9, sin red), «round not reached yet» (`ERR_RELEASE_UNAVAILABLE`, paso 9, sin red) y «access_policy=time_only with time_and_key structure», que ofrece una `.dkk` que no interviene y falla en el paso 12 por la regla de §36 para `time_only`, que tampoco estaba escrita: un resultado que empieza por la línea de versión de `age` es `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12, y cualquier otro se decodifica como `CONTROL_CBOR` en el paso 14. Sin credenciales, `ERR_ACCESS_REQUIRED` va antes que el reloj; la referencia contaba una identity nula como credencial y, con el reloj antes de `round_time`, informaba `ERR_RELEASE_UNAVAILABLE`; 9. verificación del release en el paso 10 (§17, §51, §63): las mutaciones de §64 «DateKey A + release of round B» (`ERR_ROUND_MISMATCH`, con una firma válida de la ronda 1001) y «release of another round» (`ERR_RELEASE_INVALID`, con la firma de la ronda 1001 presentada como de la ronda 1000) dependen de comparar la ronda antes que la firma, algo que solo decía `testdata/README.md`; 10. códigos de las identities en los pasos 11, 13 y 17 (§28.1, §36, §63): §27 y esos pasos exigían que la identity rechazara el fichero sin asignar código, y los códigos solo estaban en los vectores de `mutations.json` y, para una identity que no es recipient (`ERR_ACCESS_INVALID`, paso 13), en `testdata/README.md`. Casos: las mutaciones «identity that is not a recipient» (`ERR_ACCESS_INVALID`, paso 13), «two INNER_ACCESS_AGE stanzas for one recipient» (`ERR_POLICY_STRUCTURE_MISMATCH`, paso 13) y «SEALED_CONTROL_A + PAYLOAD_AGE_B» (`ERR_INTEGRITY`, paso 17); y el share efímero repetido en `INNER_ACCESS_AGE`, que la referencia rechazaba en el paso 12 sin regla en §36. La referencia probaba las identities en orden y aceptaba el fichero con la primera que desenvolvía un stanza, aunque otra desenvolviera dos; ahora es `ERR_POLICY_STRUCTURE_MISMATCH` en cualquier orden. Estos refinamientos cambian el código de la implementación de referencia en entradas que ningún vector oficial existente recoge: una `.dkk` con fallos a la vez en el objeto y en su vínculo con la cápsula, y una `.dkk` que la CLI no puede decodificar, ahora en el paso 9.a (punto 1); CR o LF en un `dk1_`, y UTF-8 inválido en un miembro que un nombre repetido sobrescribe (punto 4.3); una `.dkk` con `BODY_LEN` 0 (punto 4.4); los códigos de `profile.NewRegistry` (punto 4.6); una identity nula (punto 4.8); y dos identities de las que una desenvuelve dos stanzas (punto 4.10). Reproducen cada caso los tests `capsule.TestPrecedenceWithinPublicHeader`, `TestPrecedenceAcrossSteps`, `TestAccessKeyCheckOrder`, `TestAccessKeyFileAtStep9`, `TestControlCriticalBeforeHeaderBinding`, `TestFrameLengthLowerBounds`, `TestMalformedAgeHeaders`, `TestTlockStanzaArgumentComparison` y `TestTrustModel`; `accesskey.TestDecodePrecedence`; `agewrap.TestAccessIdentityStrictness` y `TestMalformedX25519Stanzas`; `datekey.TestReadingRules`; `profile.TestDecodePrecedence`, `TestChainHashFormula` y `TestPinPathMatchesDecode`; `provider.TestVerifyRejects`; `extension.TestOrderIsUnsignedBytewise`; y `cmd/datekeys.TestDecryptAccessKeyOrder`. #### Enmienda de la v0.8.2: canonicidad de puntos La v0.8.2 sigue sin publicarse, así que esta enmienda también la modifica sin cambiar de versión. Fija la codificación de los puntos de BLS12-381 y el contenido del stanza tlock, que la especificación dejaba a las librerías: - nuevo §12.2: la codificación canónica de un punto, la comprimida de drand, única para cada punto; un decodificador rechaza cualquier otra cadena, como x + p, una identidad con carga o con signo, la forma sin comprimir o una longitud distinta; - §12.1, punto 2: `public_key` es una codificación canónica distinta del punto en el infinito, con el mismo código, `ERR_UNKNOWN_PROFILE`; - §63, paso 10: la firma es la codificación canónica de un punto del grupo de firmas del scheme distinto del infinito y verifica como firma de la ronda, o `ERR_RELEASE_INVALID`; `ERR_ROUND_MISMATCH` conserva su precedencia; - §63, paso 11: el cuerpo del stanza tlock es `U || V || W`, con |U| el tamaño de punto del grupo de claves del scheme y |V| = |W| = 16; U es una codificación canónica distinta del infinito, y el descifrado IBE-CCA comprueba r·G == U; cualquier fallo, incluida una longitud distinta de 128 bytes en Quicknet, es `ERR_INTEGRITY`; - §64: diez mutaciones nuevas, con su código y su paso; §69.1 gana su ejemplo de precedencia, §73 resume las dos decisiones y §77 cita el formato de serialización. Caso reproducible que la justifica, de una segunda implementación independiente: su investigación de la fase 2 encontró que `tlock-js` 0.9.0 sobre `@noble/curves` 1.9.7 acepta un U recodificado como c0 + p y una firma recodificada como x + p, y devuelve la misma file key que con las codificaciones canónicas, mientras la implementación de referencia rechaza las dos, con `ERR_INTEGRITY` en el paso 11 y `ERR_RELEASE_INVALID` en el paso 10: una cápsula así se abriría en una implementación sobre esa librería y fallaría en la referencia. noble 1.9.7 discrepaba del decodificador de la referencia en 5 615 de 41 686 codificaciones de punto: 5 612 coordenadas no canónicas (x + p, c0 o c1 + k·p) que reducía al punto correcto y 3 identidades con flags o carga no nula. La especificación callaba: §12.1 pedía «la codificación comprimida de BLS12-381 que usa drand» sin exigir x < p, y el paso 11 no definía el cuerpo del stanza. La implementación de referencia ya rechazaba todas esas entradas con el código y el paso que fija el texto: ningún código cambia. Solo cambia dónde rechaza un U en el infinito, antes de descifrar en lugar de en la comprobación r·G == U, con el mismo `ERR_INTEGRITY`. Ningún fixture ni vector existente cambia de bytes ni de veredicto: `mutations.json` gana los diez casos de §64, en los que la cabecera `age` de los U y cuerpos editados conserva un MAC válido. La firma con x + p usa la ronda 1004 de Quicknet, la primera después de la 1000 cuya firma lo permite: ninguna de las rondas de los fixtures (1000, 1001 y 2000) tiene una x menor que 2³⁸¹ − p. Reproducen cada caso los tests `capsule.TestExportedMutationCorpus` y `TestPointMutationsChangeOnlyTheEncoding`; `profile.TestDrandPointDecodersAreCanonical` y `TestPublicKeyEncodingIsCanonical`; `provider.TestVerifyRejects`; y `agewrap.TestTimeIdentityStrictness` y `TestTimeIdentityRelease`. #### Correcciones de la revisión formal La v0.8.2 sigue sin publicarse, así que estas correcciones también la modifican sin cambiar de versión. Proceden de la revisión formal e independiente de esta especificación, una de las fuentes que admite esta política («revisión criptográfica o técnica externa»). Tres de sus observaciones cambian reglas normativas: 1. **Release inválido obtenido por red** (§63 pasos 9 y 10, §69.1). Una fuente que obtiene releases por red MUST verificar cada respuesta con las reglas del paso 10 y descartar la que no las cumple; si ninguna entrega un release que las cumpla, el código es `ERR_RELEASE_UNAVAILABLE`, en el paso 9. Los códigos del paso 10 son los de un release que el llamador suministra directamente, como en los vectores oficiales. Caso: el texto no decía dónde verifica una fuente de red, así que un release de otra ronda firmado para esa ronda, única respuesta de un relay, daba `ERR_RELEASE_UNAVAILABLE` en el paso 9 con la referencia, cuyo cliente drand descarta toda respuesta que no verifica, y `ERR_ROUND_MISMATCH` en el paso 10 con una fuente que lo devolviera sin verificar, aunque §69.1 afirmaba que, fuera de las comprobaciones opcionales, ninguna elección de la implementación podía cambiar el código. 2. **Objetos y arrays de cada extensión** (§31, §54, §69.1, §72). Cada extensión registrada MUST declarar los objetos y el array en que puede aparecer, y una extensión conocida que aparece en un objeto o array para el que no está registrada se trata allí como desconocida. Caso: §72 exige registrar en `CONTROL_CBOR` una extensión con afirmaciones relevantes para la seguridad, pero un lector que la conocía la aceptaba en cualquier objeto: copiada en las extensiones críticas de `PUBLIC_HEADER`, que cualquiera puede escribir (§55.1), o de una `.dkk`, superaba el paso 4 o el 9. 3. **Serialización de GT en H2** (§63 paso 11, §77). H2, H3 y H4 son las de `encrypt/ibe` de drand/kyber, y H2 resume el elemento de GT en el orden de kilic/bls12-381, c1 antes que c0 en cada nivel de la torre, con el vector H2(e(G1, G2)) = `cb87319f24560b5231579a09ad79f12e`, nuevo en `testdata/vectors/tlock_ibe.json`. Caso: el paso 11 citaba H2 sin fijar la serialización, y en el orden de `Fp12.toBytes` de noble, c0 antes que c1, H2(e(G1, G2)) es `0118eea9d5971745f71e3c94926f1717`: una implementación sobre esa librería que siguiera el texto derivaría otra `FK_TIME` y no abriría ninguna cápsula. Las demás observaciones no cambian ninguna regla ni ningún código: §27 remite la autenticidad de `PUBLIC_HEADER` al modelo de confianza (§55.1) y precisa que el MAC de `age` solo protege frente a quien no conoce la file key; el paso 5 separa la lectura de `SEALED_CONTROL`, obligatoria como ya fijaban §23 y §69.1, de la inspección de su cabecera; el paso 15 nombra su código, `ERR_HEADER_BINDING`; §21 hace MUST los 16 bytes de `capsule_id` que ya exigían §24, `datekeys.cddl` y §57; §77 gana RFC 8259, RFC 8610 y RFC 4648; y hay correcciones de redacción en §5, §36, §55.1, el paso 13, la capa 1 de §69.1 y §72. En este registro, los refinamientos cuentan cuatro vectores nuevos de `dk1.json`, no tres, con el de UTF-8 inválido (punto 4.3); el punto 4.8 recoge la regla de §36 para `time_only`; el punto 1 ya no dice que solo las comprobaciones opcionales pueden cambiar el código; y los casos 3 y 4.10 citan los textos que existían, en lugar de otros que ninguna versión anterior contenía. La implementación de referencia ya seguía las correcciones 1 y 3: ningún código cambia. Para la 2 gana la interfaz opcional `extension.Placement`, que consultan las comprobaciones de extensiones de los pasos 4, 9.a y 14; un registro que no la implementa conoce sus extensiones en todos los objetos y arrays, como antes. Ningún fixture ni vector existente cambia. Reproducen cada caso los tests `capsule.TestReleaseFromANetworkSource` y `TestExtensionPlacement`; `extension.TestPlacement`; y `agewrap.TestTlockH2Vector`. Una segunda ronda de la misma revisión confirmó estas correcciones y pidió otras tres, que también modifican la v0.8.2 sin cambiar de versión: 4. **Extensiones fuera de su registro, al escribir** (§54, §72). Un encoder MUST NOT escribir una extensión registrada en un objeto o array para el que no está registrada, y un lector que ignora por esa causa una extensión no crítica MUST NOT interpretar su `data`. Caso: la corrección 2 solo obligaba al lector, así que un escritor podía sellar en las extensiones críticas de `CONTROL_CBOR` una extensión registrada allí solo como no crítica, y un lector que conoce su registro la rechaza en el paso 14, con la cápsula ya desbloqueada (`ERR_EXTENSION_CRITICAL_UNKNOWN`): nadie que siga el registro puede abrirla. 5. **Códigos del paso 10 en §17 y §51.** §17 asignaba `ERR_ROUND_MISMATCH`, y §51 los dos códigos del paso 10, a todo release; ahora dicen, como el paso 10, que son los de un release que el llamador suministra directamente, y que el que llega de una fuente de red sin cumplir esas reglas lo descarta su fuente en el paso 9. Caso: tras la corrección 1, §17 contradecía el ejemplo de §69.1 «El mismo release, única respuesta de un relay», que da `ERR_RELEASE_UNAVAILABLE` en el paso 9, y no `ERR_ROUND_MISMATCH`. 6. **Un solo código en el paso 9** (§63 paso 9, §69.1). Si ninguna fuente entrega un release, por cualquier causa, el código es `ERR_RELEASE_UNAVAILABLE`, aunque el fallo de la fuente lleve el código de otra regla. Caso: la referencia informaba en el paso 9 el código que trajera el error de la fuente, así que una fuente del llamador que fallaba con `ERR_RELEASE_INVALID` daba `ERR_RELEASE_INVALID` en el paso 9; y el error de su cliente drand envolvía, además de `ERR_RELEASE_UNAVAILABLE`, el código del fallo de cada relay: con un único relay que devuelve la ronda 1001, el código era `ERR_RELEASE_UNAVAILABLE`, pero el error también era `ERR_ROUND_MISMATCH` para `errors.Is`, contra la regla de la referencia de que cada error envuelve exactamente un código. Las demás observaciones de la segunda ronda son de redacción: §28.1 dice que los pasos 5 y 6 analizan solo la cabecera `age`, porque el paso 5 lee `SEALED_CONTROL` entero; el paso 9 une sus dos flechas hacia `ERR_RELEASE_UNAVAILABLE`; y la introducción de este apartado termina en su lista y nombra después los bloques que la completan, y la de las correcciones dice que tres de ellas cambian reglas normativas, no el texto normativo, que también cambian las demás. Fuera de la especificación, `testdata/README.md` dice dónde están registradas las extensiones del corpus de mutaciones: en los dos arrays de todos los objetos, así que sus casos de `data` inválida no dependen de la ubicación. La corrección 6 cambia el código de la implementación de referencia en entradas que ningún vector oficial recoge: el error de una fuente que lleva otro código normativo se informa en el paso 9 con `ERR_RELEASE_UNAVAILABLE` como único código y conserva solo su texto, y `provider/drand.Client` guarda el fallo de cada relay solo como texto. Para la 4, `capsule.Encrypt` y `accesskey.Encode` no reciben un registro de extensiones: escriben las que se les dan, y la aplicación, que conoce el registro, aplica la regla. Ningún fixture ni vector existente cambia. Reproducen cada caso los tests `capsule.TestExtensionPlacement`, que ahora cubre también el array no crítico de una `.dkk`, y `TestReleaseSourceErrorsAtStep9`; `provider/drand.TestFetchErrorHasOneCode` y `TestUnavailabilityAndCancellation`; y `datekeys.TestCode`. ### Cambios normativos de la v0.9 La v0.9 añade el formato 2 de `.dkc`, que oculta hasta la fecha la longitud exacta del contenido y el número de credenciales, y pasa a texto normativo las reglas del escritor. Los casos se obtuvieron con la implementación de referencia v0.8.2, sus fixtures y las librerías `age` de Go (`filippo.io/age` 1.3.2) y TypeScript (`age-encryption` 0.3.1). Los tests son los de la implementación de referencia: los marcados «ampliado» existían en la v0.8.2, y los demás se añadieron al implementar la v0.9. 1. **Formato de la cápsula** (§22, §23, §24, §26, §31, §40, §63 pasos 2, 4 y 14, §69.1, §70). - Cambio: `VERSION` del PRELUDE es el formato, 1 (el de la v0.8.2) o 2. `CONTROL_CBOR` tiene la versión de schema que §22 asigna a su formato, 1 o 2; otra es `ERR_UNSUPPORTED_VERSION` en el paso 14, capa 2. `PUBLIC_HEADER` y la `.dkk` conservan la versión 1. Un escritor MUST escribir el formato 2; un lector MUST abrir los dos, el 1 con la semántica de la v0.8.2. Un código de relleno nuevo exige un formato nuevo. - Motivo: un lector v0.8.2 debe rechazar una cápsula v0.9 antes de pedir el release. Sin un cambio visible de versión, la abriría hasta el paso 14 y fallaría allí, tras la petición de red, por la versión 2 de su control o, si el control conservara la versión 1, por las claves 6 y 7, desconocidas en su mapa cerrado (§58). - Caso: la mutación «version changed» de `mutations.json` (`[4,1,"02"]` sobre `time_only.dkc`) da `ERR_UNSUPPORTED_VERSION` en el paso 2, sin red, con la implementación de referencia v0.8.2. Se cambia `VERSION` y no la versión de `PUBLIC_HEADER` porque es la primera comprobación, porque el cambio es de toda la cápsula y la cabecera no cambia, y porque así siguen valiendo los vectores «schema version 2» de `PUBLIC_HEADER` en `cbor.json`, la mutación «header schema version changed» y el ejemplo de §69.1 «Versión 2, una clave desconocida y una DateKey inválida». Un magic `DKC2` daría en un lector v0.8.2 `ERR_INVALID_MAGIC`, que dice «no es una cápsula». Una cápsula de formato 2 con `VERSION` 1 falla en el paso 14 en los dos lectores, por la versión 2 de su control, salvo que antes falle el `capsule_digest` de una `.dkk` en el paso 9.a. - Tests: `capsule.TestFormatDispatch`, `TestFormatRelabel` y `TestFormat1Compatibility`; `capsule.TestExportedMutationCorpus` y `TestDecodeControlRejects`, ampliados. 2. **16 huecos en `INNER_ACCESS_AGE`** (§33, §36, §36.1, §38, §39, §57, §63 pasos 12 y 13). - Cambio: en formato 2, `time_and_key` lleva exactamente 16 stanzas X25519: de 1 a 16 credenciales y, en los huecos libres, señuelos cuya clave privada se descarta en el acto. Otro número es `ERR_POLICY_STRUCTURE_MISMATCH` en el paso 12. - Motivo: el número de credenciales era visible para cualquiera. - Caso: `SEALED_CONTROL_LEN` (bytes 12 a 15 del PRELUDE) vale 646 en `time_and_key_portable.dkc`, con una credencial, y 842 = 646 + 2·98 en `time_and_key_recipients.dkc`, con tres: cualquiera cuenta las credenciales antes de la fecha, y tras el release en la cabecera de `INNER_ACCESS_AGE`. En formato 2, con la ronda 1000 y sin extensiones de control, vale siempre 2128 en `time_and_key` y 458 en `time_only`. Un señuelo envuelve la misma `FK_ACCESS`: una cápsula sellada conservando la clave privada de un señuelo se abre con ella. - Tests: `capsule.TestInnerHasSixteenStanzas` y `TestDummyRecipients`; `agewrap.TestAccessSlots`. 3. **Orden aleatorio de los stanzas** (§39, §62 paso 6, §62.1 regla 4). - Cambio: el orden de los 16 stanzas en la cabecera es una permutación uniformemente aleatoria, sin sesgo; la permutación y qué huecos son señuelos no se almacenan ni se entregan. - Motivo: sin esta regla, los señuelos no ocultan el número de credenciales a quien tiene una. - Caso: `filippo.io/age` y `age-encryption` escriben los stanzas en el orden de sus recipients, y `capsule.Encrypt` añade `R_ACCESS` al final. Con los señuelos detrás de las credenciales, quien abre el stanza k sabría que hay al menos k credenciales, y quien tiene la clave portable, la última, sabría su número exacto. - Tests: `capsule.TestStanzaOrderIsUniform`. 4. **Relleno del payload** (§29, §29.1, §30, §30.1, §55.1, §56, §63 pasos 16 a 18). - Cambio: en formato 2 el plaintext de `PAYLOAD_AGE` es el contenido seguido de ceros hasta P = regla(L), con los códigos 1 (`bloque256`) y 2 (`reforzado`, con Padmé), sin regla sin relleno. Un plaintext de otra longitud o con un byte de relleno distinto de 0x00 es `ERR_INTEGRITY` en el paso 17. El lector entrega solo los L primeros bytes y no los presenta como válidos antes de que el paso 17 termine; si los escribe en streaming, no escribe el relleno y señala el error para que se descarte lo escrito. La aritmética es entera y de más de 32 bits. - Motivo: la longitud exacta del contenido era visible para cualquiera. Se elige `ERR_INTEGRITY` porque el fallo es del binding control ↔ payload de §30.1, cuyos fallos ya son `ERR_INTEGRITY` en el paso 17, como «SEALED_CONTROL_A + PAYLOAD_AGE_B»; `ERR_POLICY_STRUCTURE_MISMATCH` es de stanzas, `ERR_HEADER_BINDING` solo cubre la cabecera, y un código nuevo cambiaría §69. - Caso: `time_only.dkc` mide 78 799 bytes y su `PAYLOAD_AGE`, 78 216 = 184 + 78 000 + 2·16: cualquiera lee L = 78 000. El `PAYLOAD_AGE` de `empty_payload.dkc`, de 200 bytes, da L = 0. En formato 2 con el código 2, un `PAYLOAD_AGE` de 80 088 bytes corresponde a cualquier L entre 77 825 y 79 872. Aritmética: un `Padme` con los operadores de 32 bits de JavaScript da una P incorrecta por primera vez en L = 2 113 929 217 con los operadores con signo (2 113 929 472 en lugar de 2 147 483 648), y en L = 4 227 858 433 con `>>> 0` (4 227 858 688 en lugar de 4 294 967 296). Sin las filas de §29.1 para esos dos L pasaría todas las demás hasta 2³² − 1, y también la de L_MAX, estas dos por coincidencia, porque en ellas `Padme` no supera a `bloque256`; solo fallaría por encima de 2³², como en L = 2³² + 1 (4 294 967 552 en lugar de 4 362 076 160); `Math.log2(2^49 − 1)` da 49 en Node, y el valor correcto de E es 48. - Tests: `capsule.TestPaddingRules`, `TestPaddingChecksAtStep17` y `TestPaddingAcrossChunks`; `capsule.TestTrustModel`, ampliado. 5. **L de longitud fija y L_MAX** (§31, §57, §58, §58.1, §69.1 capa 3). - Cambio: L y el código van sellados en las claves 6 y 7 de `CONTROL_CBOR` versión 2, obligatorias en ella y prohibidas en la versión 1. L es una cadena de 8 bytes big-endian de como mucho L_MAX = 2⁵³ − 2⁴⁶. Toda violación es `ERR_NON_CANONICAL_CBOR` en el paso 14, capa 3. - Motivo: la longitud de `CONTROL_CBOR` es visible en `SEALED_CONTROL_LEN`, y no debe depender de L. L_MAX es el mayor L para el que las dos reglas dan una P de como mucho 2⁵³ − 1 (§58): `reforzado(L_MAX + 1) = 2^53`. - Caso: con L como entero CBOR en su forma más corta (§58), su valor ocuparía 1, 2, 3, 5 o 9 bytes, y `SEALED_CONTROL_LEN` lo revelaría: en el hueco P = 256, el de los mensajes cortos, distinguiría L < 24, 24 ≤ L ≤ 255 y L = 256; en P = 65 536, L = 65 536 del resto. Con 8 bytes fijos, un `CONTROL_CBOR` de versión 2 sin extensiones mide siempre 103 bytes. - Tests: `capsule.TestControlLengthIsConstant`; `capsule.TestDecodeControlRejects`, ampliado. 6. **Lectura de L y P** (§31, §57, §63 pasos 16 y 17, §69.1). - Cambio: un lector MUST NOT reservar memoria ni disco según L o P antes de recibir el plaintext; MAY comprobar en el paso 17 la longitud de `PAYLOAD_AGE`, con el mismo código. - Motivo: L es un dato del control, no una longitud de trama, y cualquiera puede sellar un control `time_only` (§36.1). - Caso: un control `time_only` que declara L = L_MAX junto a un `PAYLOAD_AGE` de 456 bytes; un lector que reservara según L pediría más de 8·10¹⁵ bytes. - Tests: `capsule.TestDeclaredLengthIsNotAllocated`. 7. **Consideraciones de privacidad** (§4, §5, §55.2, §70). - Cambio: nueva §55.2, con lo que revela una cápsula antes y después de la fecha, a quién, y lo que el formato 2 impide comprobar. `access_policy` sigue visible. Una implementación SHOULD indicar al llamador el formato de una cápsula. - Motivo: el texto no decía qué metadatos protege el protocolo. - Caso: los de los cambios 2 y 4. `access_policy` sigue visible porque de ella depende la mutación «time_and_key without credentials» (`ERR_ACCESS_REQUIRED`, paso 9, sin red). - Tests: los de los cambios 2, 4 y 5. 8. **Orden de escritura** (§61, §62, §62.1 regla 7). - Cambio: `SEALED_CONTROL_LEN` es la longitud exacta de `SEALED_CONTROL`, conocida antes de calcular `header_binding`, medida con un sellado provisional o calculada, y comprobada después. - Motivo: el orden de la v0.8.2 no se podía seguir. - Caso: §61 y §62 construían el PRELUDE (paso 6) antes que `SEALED_CONTROL` (pasos 9 y 10 de §61, 9 a 11 de §62), cuya longitud lleva el PRELUDE, y `header_binding`, que cubre el PRELUDE, va dentro del control sellado: el orden era circular. `capsule.Encrypt` lo resuelve sellando antes un control provisional de la misma longitud, algo que el texto no decía. - Tests: `capsule.TestSealedControlLength`. 9. **`I_PAYLOAD` nueva** (§29, §30.1, §62.1 regla 5). - Cambio: `I_PAYLOAD` sale de un CSPRNG para cada cápsula y no se reutiliza ni se deriva de otro valor. - Motivo: confidencialidad. §29 solo decía «aleatoria». - Caso: si dos cápsulas `time_only` comparten `I_PAYLOAD`, abrir la primera en su fecha, lo que puede hacer cualquiera, abre el payload de la segunda antes de la suya; y «SEALED_CONTROL_A + PAYLOAD_AGE_B» (§64) se abriría en lugar de fallar en el paso 17. Derivada del contenido, cualquiera comprueba un contenido que adivina antes de la fecha. - Tests: `capsule.TestPayloadIdentityReuse`. 10. **Recipients no canónicos y de orden bajo** (§37, §62.1 regla 3). - Cambio: el escritor MUST rechazar un recipient con el bit 255 a 1, con u ≥ p o de orden bajo, y MAY rechazar uno de la torsión cuadrática. - Motivo: su stanza no lo abre nadie, o lo abriría cualquiera, y las reglas del lector (§63) no lo detectan. - Caso: `filippo.io/age` 1.3.2 y `age-encryption` 0.3.1 cifran para un recipient con el bit 255 a 1 un stanza que su identity no abre («incorrect identity for recipient block» y «no identity matched any of the file's recipients»): el salt de HKDF lleva los 32 bytes recibidos, y la identity usa su clave pública canónica. Con la u cero, de orden bajo, las dos librerías fallan al cifrar. - Tests: `agewrap.TestNonCanonicalRecipients`. 11. **Credenciales y reloj del escritor** (§15, §39, §62.1 reglas 2 y 3). - Cambio: el instante pedido es posterior al reloj del escritor; `time_only` no admite credenciales; `time_and_key` admite de 1 a 16, sin repetir. - Motivo: la referencia ya aplicaba estas reglas sin texto normativo que las exigiera, y el máximo de 16 procede del cambio 2. - Caso: `capsule.TestEncryptRejectsInvalidOptions` rechaza un instante que no es posterior al reloj, `time_only` con credenciales, `time_and_key` sin ellas y un recipient repetido. Un recipient repetido daría a su identity dos stanzas, y la cápsula fallaría para ella en el paso 13 (mutación «two INNER_ACCESS_AGE stanzas for one recipient»). - Tests: `capsule.TestEncryptRejectsInvalidOptions`, ampliado, y `TestCredentialBounds`. 12. **L conocida antes de sellar y errores del escritor** (§61, §62.1 reglas 6 y 9). - Cambio: el escritor conoce L antes de sellar el control, MAY volcar la fuente a un fichero temporal y MUST NOT suponer L; ante un error lo informa, y quien recibe la salida la descarta. - Motivo: en formato 2, L se sella antes que el payload. - Caso: `capsule.Encrypt` escribe el PRELUDE, la cabecera y el control en `dst` y después cifra en streaming una fuente de longitud desconocida; su documentación ya dice que, ante un error, `dst` puede tener una cápsula parcial que hay que descartar. Si la fuente entregara otra longitud que la sellada, la cápsula fallaría en el paso 17, cuando ya no puede repararse. - Tests: `capsule.TestEncryptSourceLength`. 13. **Autocomprobación y borrado** (§62.1 reglas 11 y 12, SHOULD). - Cambio: el escritor comprueba su propia salida, también la longitud del plaintext entregado a `age` y la de `PAYLOAD_AGE`, y borra los secretos después. - Motivo: un error del escritor solo se descubre tras la fecha. - Caso: el caso 5 de la v0.8.2: `Encrypt` selló un `CONTROL_CBOR` que el lector rechazaba en el paso 14, con la cápsula ya desbloqueada. Un relleno omitido superaría la comprobación de que `I_PAYLOAD` abre la cabecera de `PAYLOAD_AGE`, revelaría L exacta y fallaría en el paso 17. - Tests: `capsule.TestEncryptSelfCheck`. Ningún código de error es nuevo (§69). Ningún objeto de formato 1 cambia de veredicto ni de código: solo cambian las entradas con `VERSION` 2, que la v0.8.2 rechazaba en el paso 2. Los cinco fixtures y las mutaciones construidas por el creador, todos de formato 1, quedan como casos de compatibilidad (§70), y ninguno de los 1 825 casos de `inspect_differential.json` produce `VERSION` 2: sus ediciones del byte 4 dan a7, 05, e8 y 20. Cambian estos datos de prueba: - la mutación «version changed» pasa a `VERSION` 3 (`[4,1,"03"]`): con `VERSION` 2, ese `time_only.dkc` de formato 1 llega al paso 14 (`ERR_UNSUPPORTED_VERSION`), un caso que pasa a la lista nueva de §64; - las mutaciones de `time_and_key` de esa lista ofrecen la identity, sin `.dkk`, y cada una tiene un caso compañero con la `.dkk`, fuera de §64, que falla en el paso 9.a con `ERR_ACCESS_INVALID`; - los vectores `control_cbor` de `cbor.json` dependen del formato y ganan el campo `format`, 1 por omisión: «schema version 2» es `ERR_UNSUPPORTED_VERSION` en formato 1 y `ERR_NON_CANONICAL_CBOR` en formato 2, donde le faltan las claves 6 y 7. Entre los nuevos de formato 2, `payload_length` = `48 0000000100000001` se acepta como 4 294 967 297 (§58), y uno de 7 o de 9 bytes, o codificado como entero CBOR, es `ERR_NON_CANONICAL_CBOR`. «unknown key 6» conserva su veredicto en formato 1, y pasa a llamarse «key 6, defined only in schema version 2», porque la clave 6 ya existe; - «código de relleno 3 y una extensión crítica desconocida» no puede ser un vector de `cbor.json`, cuyos vectores de schema no comprueban extensiones críticas: es un caso de `capsule.TestPrecedenceAcrossSteps`, ampliado, como los demás ejemplos nuevos de §69.1; - `testdata/README.md` cita la mutación «version changed», la «framing version» y el número de mutaciones de §64; - las 33 mutaciones de las dos primeras listas de §64 se aplican también a fixtures de formato 2, con el prefijo «format 2: », y `inspect_differential.json` conserva sus 1 825 casos y añade un bloque por cada fixture de formato 2; - `capsule.Open` no registraba el éxito del paso 17, así que las etapas de los fixtures pasaban del 16 al 18; con las comprobaciones nuevas de ese paso, que §67 necesita, lo registra, y los registros de los fixtures de formato 1 ganan esa etapa y el campo `format`. ### Cambios normativos de la v0.10 La v0.10 añade el formato 3 de `.dkc`, que guarda varios ficheros con sus rutas, tamaños, hashes y fechas, cifrados, y reserva el área `security` para una firma de autor y un sello de tiempo que definirán versiones posteriores sin cambiar el formato. Es la primera de tres entregas del formato 3; la segunda definirá la firma, y la tercera, el sello. El diseño pasó una revisión de seguridad externa y dos revisiones adversariales. Los casos se obtuvieron con la implementación de referencia v0.9, con `datekeys-ts` 0.2.0-dev, con las librerías `age` de Go (`filippo.io/age` 1.3.2) y TypeScript (`age-encryption` 0.3.1), y en la máquina de desarrollo. Las pruebas son las previstas: se nombrarán al implementar la v0.10. 1. **Formato 3** (§1, §6, §22, §23, §24, §27, §31, §33, §36, §38, §39, §63 pasos 2, 12 a 14 y 16 a 18, §69.1, §70). - Cambio: `VERSION` 3. `CONTROL_CBOR` de versión 3, con las claves de la 2; en formato 3, L es la longitud de `BODY`. Un escritor MUST escribir el formato 3; un lector MUST abrir los tres, el 1 y el 2 con la semántica de sus versiones. - Motivo: hasta la v0.9 una cápsula guarda un solo fichero, y su nombre y sus fechas quedan fuera del protocolo (§6, §55.2). Guardar varios ficheros con sus metadatos cambia la semántica del plaintext, y un lector v0.9 solo lo notaría después de pedir el release: §22 exige un formato nuevo. - Caso: la página `/inspect` de `datekeys-ts` 0.2.0-dev descarga el contenido de una cápsula con un nombre que deduce de su tipo, porque la cápsula no guarda el suyo. Un lector v0.9 que abriera un `BODY` lo entregaría entero, trama y head incluidos, como si fuera el contenido. Con `VERSION` 3, la implementación de referencia v0.9 rechaza la cápsula en el paso 2 (`framing.go:56`), sin red. - Pruebas previstas: las mutaciones de `VERSION` de §64 y los fixtures `format3_*` de §67. 2. **Trama del contenido y área** (§29.2, §55.2, §57, §62.1 regla 13). - Cambio: `BODY` empieza por `AREA_LEN`, `SECURITY_LEN` y `HEAD_LEN`. El área tiene un tamaño fijo por versión del spec, 512 bytes en esta, y va siempre. El lector acepta cualquier área válida. - Motivo: el tamaño de lo que lleve `security` no debe cambiar L, porque P revelaría si una cápsula lleva firma o sello. - Caso: con un fichero `nota.txt` de 1100 bytes, con mtime, y un área que midiera lo que `SECURITY_CBOR`, L sería 1251 sin firma y 1359 con una firma Ed25519, que ocupa 108 bytes, y P valdría 1280 y 1536: P delataría la firma. Con el área fija, L = 1741 en los dos casos. - Pruebas previstas: `format3_area_1024` y las mutaciones de la trama de §64. 3. **`security`** (§29.3, §29.7, §69.1). - Cambio: mapa de versión 1 cuyas claves 2 y 3 son cadenas de bytes con la firma y el sello codificados aparte. Nunca decide la apertura. En esta versión se escribe vacío, y ningún `alg` ni `seal_type` está definido. - Motivo: el formato se congela en esta versión, y la firma y el sello llegarán después (§70). Con la firma y el sello como mapas dentro del mapa exterior, uno mal formado dejaría a los dos sin comprobar, y un límite de tamaño de la clave y la firma en ese schema, como los 8192 bytes de un borrador anterior del diseño, dejaría fuera algoritmos futuros en todos los lectores anteriores. - Caso: un `author-signature` con `alg` 0 incumpliría el schema del mapa exterior, y un sello válido a su lado dejaría de evaluarse. SLH-DSA (FIPS 205) tiene firmas de 7856 bytes en SLH-DSA-SHA2-128s y SLH-DSA-SHAKE-128s, y de 16 224 a 49 856 en sus otras diez variantes: con un límite de 8192 bytes, solo esas dos cabrían. - Pruebas previstas: `format3_security_v2`, `format3_signature_unsupported`, `format3_seal_unsupported` y `security.json`. 4. **Head** (§29.4, §58.1, §62.1 reglas 14 y 17). - Cambio: el head de versión 1, siempre en formato 3, con una sal, un comentario y un autor declarado opcionales y, por fichero, la ruta, `size`, `start`, `end`, el SHA-256 y la mtime opcional. Sus límites quedan fijados con el formato. - Motivo: los datos que el formato 2 deja fuera. La versión propia del head solo es una defensa: por §22, una versión nueva exige un formato nuevo. - Caso: el del cambio 1. Con una versión nueva del head dentro del formato 3, un lector de esta versión pediría el release y fallaría en el paso 17, que es lo que §22 prohíbe. - Pruebas previstas: `format3_single`, `format3_tree`, `format3_comment_only` y `head_schema.json`. 5. **Rutas** (§7.3, §29.5, §29.5.1). - Cambio: las reglas R1 a R10, con tablas propias de WindowsBestFit y de Unicode 18.0.0, la versión vigente al congelar el formato, publicada el 16-09-2026. - Motivo: cualquiera puede fabricar una cápsula (§36.1). Sus rutas no deben escapar de la carpeta del lector, ni confundirse, ni colisionar al extraerlas. - Caso: comprobado en la máquina de desarrollo, en NTFS, «ABCDEF~1» resuelve a «ABCDEFGHIJ» (R6b). HFS+ ignora ZWNJ y ZWJ al comparar nombres, como recoge la lista de `core.protectHFS` de git (R3, R4 y R7). En Node 24.9, `"~" < "\u{1F600}"` es falso, y en UTF-8 U+FF5E va antes (R8). Go 1.26.8 trae Unicode 15.0.0 y Node 24.9 el 16.0, así que sus funciones de Unicode no coinciden (§29.5.1). 127 veces «ΐ» (U+0390) miden 254 bytes y 381 unidades UTF-16 tras NFD (R3). En WindowsBestFit, bestfit1250 lleva U+00BF («¿») a 0x3F, y bestfit874 lleva U+2665 («♥») a 0x03 y U+00A7 («§») a 0x15: si R6c rechazara cualquier carácter ASCII de R4 en una proyección, esas rutas no se podrían guardar. - Pruebas previstas: `paths.json`, `path_fold.json` y las mutaciones de rutas de §64. 6. **Texto** (§29.6). - Cambio: los caracteres prohibidos en el comentario y en el autor declarado. - Motivo: los dos se muestran junto a los veredictos. - Caso: un comentario con U+202E invierte el orden visual de lo que le sigue, y uno con ESC puede mover el cursor de un terminal. - Pruebas previstas: `head_schema.json` y las mutaciones de texto de §64. 7. **Veredictos y presentación** (§29.7). - Cambio: los veredictos X, F0, F1, S0, S1 y S2, con su texto. Primero se muestran los veredictos y después el autor y el comentario, marcados. En un terminal, cada línea visual del creador lleva un prefijo, y los veredictos se repiten al final. - Motivo: un creador no debe poder hacer pasar su texto por un veredicto. - Caso: una línea de comentario más ancha que el terminal, partida por él, mostraría su resto sin prefijo. U+2502 ocupa 2 columnas en un terminal CJK. - Pruebas previstas: las de presentación de la CLI de la referencia. 8. **Paso 17 del formato 3** (§63 pasos 17 y 18, §69, §69.1). - Cambio: los subpasos 17.1 a 17.8. Un fallo de `age` prevalece; después decide el primer subpaso que falla. Un lector solo para antes de EOF con `ERR_INTEGRITY`, y solo cuando ese es ya el código final. Código nuevo, `ERR_HEAD_INVALID`. - Motivo: si decidiera el momento de la detección, dos lectores correctos darían códigos distintos. - Caso: con el fichero cortado justo después de un chunk completo, `filippo.io/age` 1.3.2 lo entrega como no final y después devuelve `ErrUnexpectedEOF` (`stream.go:117-156`), y `age-encryption` 0.3.1 lo retiene y, en `flush`, lo prueba como final y falla sin entregarlo. Con una ruta «..» en ese chunk, el lector en Go vería antes el head, y el de TypeScript, el fallo de STREAM. Además, `filippo.io/age` detecta unos datos tras el chunk final al leerlo, pero devuelve el error en la lectura siguiente: un lector que leyera exactamente P bytes no lo vería. - Pruebas previstas: los ejemplos de §69.1 y las mutaciones de precedencia de §64, con una ruta «..» y el corte exacto tras el chunk completo que la contiene. 9. **Entrega de varios ficheros** (§56). - Cambio: nada se presenta antes del paso 18. Un lector que escribe los ficheros lo hace en un lugar temporal, dentro de un directorio nuevo, sin seguir enlaces ni sobrescribir. Un ZIP lleva las rutas del head en UTF-8. - Motivo: extraer una cápsula no debe escribir fuera de su carpeta ni dejar los ficheros de una cápsula inválida. - Caso: en POSIX, `os.Rename` de un directorio temporal sobre un destino vacío que ya existe lo sustituye, así que el lector reclama primero su directorio con `os.Mkdir`, que falla si existe. - Pruebas previstas: la CLI de la referencia nunca escribe fuera de su directorio, con las rutas de `paths.json`. 10. **Privacidad** (§4, §55.1, §55.2). - Cambio: antes de la fecha, el formato 3 oculta las rutas, los tamaños, los hashes, las fechas, el comentario y el autor, y el número de ficheros salvo la cota que da P. - Motivo: son los metadatos nuevos del formato 3. - Caso: con el área de 512 bytes y P = 768 caben como mucho 4 ficheros, de nombre de un byte y vacíos: n ≤ (P − 579)/45. - Pruebas previstas: las de los cambios 2 y 4. 11. **Reglas del escritor** (§61, §62, §62.1 reglas 1, 6 y 13 a 18). - Cambio: escribir el formato 3; el área y `security` siempre; el head con una sal nueva; las rutas y el texto válidos; la mtime omitida si no está en su rango; la autodecodificación de los tres CBOR como MUST; y los ficheros iguales en las dos pasadas. - Motivo: un head que el lector rechaza solo se descubre tras la fecha, y un fichero que cambia mientras se cifra daría un SHA-256 que no le corresponde. - Caso: el `lastModified` de un `File` del navegador y `ModTime().Unix()` en Go pueden ser negativos, para una fecha anterior a 1970, y el schema del head solo admite enteros sin signo. - Pruebas previstas: las del escritor de la referencia y de `datekeys-ts`. 12. **Invisibles** (§29.5 R4b, §29.5.1, §29.6). - Cambio: ningún texto del creador, sean rutas, comentario o autor declarado, lleva puntos de `Default_Ignorable_Code_Point` salvo ZWJ, ZWNJ, VS15 y VS16, y estos solo donde su uso es conforme: VS15 y VS16 tras un carácter que los admite según `emoji-variation-sequences.txt`, y ZWJ y ZWNJ nunca al principio ni al final, ni dos seguidos. - Motivo: Unicode 18.0.0 aclara qué usos de los selectores de variante son conformes y advierte de que sus secuencias invisibles sirven para atacar aplicaciones de IA. Un comentario o un nombre con texto oculto se vería normal en pantalla, y lo leería un modelo al que se pasara la cápsula abierta. - Caso: cada etiqueta de U+E0020 a U+E007E es un carácter ASCII invisible: «Hola» seguida de U+E0049, U+E0047, U+E004E, U+E004F, U+E0052 y U+E0041 se muestra como «Hola» y lleva escrito IGNORA. Cada selector de VS1 a VS256 detrás de un emoji codifica un byte que tampoco se ve. El borrador anterior admitía los dos en el comentario, y en las rutas admitía secuencias de ZWJ, ZWNJ, VS15 y VS16 de cualquier longitud. - Pruebas previstas: las mutaciones de invisibles de §64 y los vectores de `paths.json` y `head_schema.json`. `ERR_HEAD_INVALID` es el único código nuevo (§69). Ningún objeto de formato 1 ni de formato 2 cambia de veredicto ni de código, salvo que `VERSION` 3 deja de dar `ERR_UNSUPPORTED_VERSION` en el paso 2. Producen `VERSION` 3 solo dos de los 125 casos de `mutations.json`, «version changed» y «format 2: version changed», y ninguno de los 4 380 de `inspect_differential.json`. Cambian estos datos de prueba: - las mutaciones «version changed» y «format 2: version changed» pasan a `VERSION` 4 (`[4,1,"04"]`). La segunda es también la entrada «VERSION 3» de la lista del formato 2 de §64, que pasa a «VERSION 4», y se añade `time_only` de formato 2 con `VERSION` 3, que falla en el paso 14; - las 33 mutaciones de las dos primeras listas de §64 se aplican también a fixtures de formato 3, y se añade la lista del formato 3; - en `cbor.json`, los vectores de formato 1 «key 6, defined only in schema version 2» y «keys 6 and 7 of schema version 2» conservan su veredicto, pero su nombre deja de ser exacto: las claves 6 y 7 existen también en la versión 3; - el campo `spec` de los ficheros de `testdata` y de los registros de los fixtures pasa a «0.10»; - `testdata/README.md`, que dice que `format` vale 1 o 2, que un escritor solo produce el formato 2, que `cbor.json` cubre `CONTROL_CBOR` en los dos formatos y que §64 tiene 88 mutaciones, y que pone como ejemplo `[[4, 1, "03"]]` y «VERSION 1 or 2» en su tabla de pasos; - los fixtures de formato 2 pasan a ser casos de compatibilidad (§67, §70); - en las implementaciones, las pruebas que esperan `VERSION` 3 rechazada en el paso 2 (`framing.test.ts`, `inspect.test.ts` y `prefix.test.ts`), las que tratan el formato 3 como un error del llamador en `EncodeControl` y `DecodeControl` (`framing_test.go`), el texto del paso 2 de `/inspect`, que dice que `VERSION` vale 1 o 2 (`format.ts`), y las del escritor que esperan el formato 2 (`encrypt.test.ts`, `encrypt_test.go` y `format2_test.go`). --- ## 77. Referencias - drand Protocol Specification https://docs.drand.love/docs/specification/ - drand/tlock https://github.com/drand/tlock - drand/kyber — paquete `encrypt/ibe`, que tlock importa: H2, H3 y H4 del IBE-CCA; el pairing y la serialización de GT son los de drand/kyber-bls12381, sobre kilic/bls12-381 https://github.com/drand/kyber - age specification — C2SP https://github.com/C2SP/C2SP/blob/main/age.md - BLS12-381 serialization — zkcrypto `bls12_381`, formato de ZCash https://docs.rs/bls12_381/latest/bls12_381/notes/serialization/index.html - K. Nikitin, L. Barman, W. Lueks, M. Underwood, J.-P. Hubaux, B. Ford — «Reducing Metadata Leakage from Encrypted Files and Communication with PURBs», Proceedings on Privacy Enhancing Technologies 2019(4), pp. 6–33: función de relleno Padmé (§29.1) https://doi.org/10.2478/popets-2019-0056 - The Unicode Standard, Version 18.0.0 — Unicode Character Database: `UnicodeData.txt`, `DerivedCoreProperties.txt`, `CaseFolding.txt` y `emoji/emoji-variation-sequences.txt` (§29.5.1) https://www.unicode.org/Public/18.0.0/ucd/ - UAX #15 — Unicode Normalization Forms: NFD (§29.5.1) https://www.unicode.org/reports/tr15/ - WindowsBestFit — tablas best-fit de Windows, en unicode.org (§29.5.1) https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/ - Microsoft — «Naming Files, Paths, and Namespaces»: nombres reservados y alias 8.3 (§29.5) https://learn.microsoft.com/windows/win32/fileio/naming-a-file - PKWARE — APPNOTE.TXT, .ZIP File Format Specification: bit 11, ZIP64 y campos extra (§56) https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT - NIST FIPS 205 — Stateless Hash-Based Digital Signature Standard (SLH-DSA): tamaños de firma (§76) - RFC 7748 — X25519: codificación de u, recorte de escalares y puntos de orden bajo (§37) - RFC 8949 — CBOR - RFC 8610 — CDDL - RFC 8259 — JSON - RFC 4648 — Base64 y Base64URL - RFC 2119 / RFC 8174 — normative terminology --- ## 78. Principio final > **DateKey define cuándo.** > **DateKeyCap protege qué.** > **`.dkk` transporta la capacidad adicional de acceso cuando la política la exige.** > **DateKeys facilita, registra y acelera; el cliente verifica.**