You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/spec/DateKeys_Protocol_Specifica...

366 KiB

DateKeys Protocol Specification

DateKey, DateKeyCap (.dkc) y DateKeys Access Key (.dkk)

Borrador normativo v0.16

Estado: Draft / pre-estándar
Fecha: 7 octubre 2026, borrador
Proyecto: DateKeys
Implementación de referencia: 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;
  • la firma de autor del formato 3, con una clave propia Ed25519 o con certificados X.509 (CMS) de cualquier país, y el sello de tiempo RFC 3161;
  • la llave de palabras, una credencial que se deriva de palabras que elige quien crea la cápsula;
  • la nota pública de una cápsula y la extensión de cápsula de la .dkk;
  • verificación;
  • reglas del escritor;
  • Release API, Release Cache y el objeto release, el release de una ronda como dato (§45, §47, §47.1);
  • recuperación directa contra el proveedor, y a largo plazo con archivos de releases y servicios de caché (§50);
  • extensiones genéricas;
  • consideraciones de privacidad;
  • un anexo informativo para abrir una cápsula sin software de DateKeys (§79).

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, o lo conserve un archivo de releases o un servicio de caché (§50), el ciphertext siga disponible y las credenciales correspondientes existan, un objeto maduro debería poder abrirse sin pasar por la API DateKeys, e incluso sin software de DateKeys (§79).

  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).

  9. Autenticidad opcional Una cápsula de formato 3 puede llevar la firma de uno o varios firmantes y un sello de tiempo. Quien la abre puede comprobar que esos firmantes firmaron este contenido para este control y, con un sello, que la firma existía antes de que la cápsula pudiera abrirse (§29.8 a §29.11). Sin firma, una cápsula no prueba autoría (§36.1).


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, salvo la existencia antes de un instante que prueba un sello de tiempo (§29.11);
  • la validez legal de una firma con certificado: la confianza en su cadena, la revocación y la cualificación las valora un validador externo (§29.10);
  • protección frente a un dispositivo ya comprometido;
  • control del plaintext después de un descifrado legítimo;
  • que una cápsula se abrirá: antes de la fecha nadie, tampoco quien la recibe, puede comprobar que su contenido es el que se dice, que una credencial la abre ni que se escribió para poder abrirse (§36.1). Una cápsula no sirve, por sí sola, para demostrar antes de la fecha una puja, una predicción o un compromiso;
  • caducidad: una cápsula abierta no se cierra, y su contenido no se borra después de la fecha;
  • condiciones distintas de una ronda: un evento, una votación o la falta de una señal (§1);
  • cancelación: quien crea una cápsula no puede impedir que se abra en su fecha, ni retrasarla.

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);
  • poner en la nota pública un texto que imite un veredicto, una autoría o un aviso, o cambiarla después de escribir la cápsula (§24.1).

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

Una cápsula time_only depende de un solo proveedor, Quicknet en V1, y no tiene otra vía de apertura: ninguna clave de DateKeys, ninguna recuperación y ningún servicio la abren antes ni después de la fecha. Del proveedor dependen dos cosas distintas:

  • la confidencialidad hasta la fecha: si tantos miembros de la red de drand como pide su umbral se ponen de acuerdo, o sus partes de la clave caen en las mismas manos, se puede firmar una ronda futura antes de tiempo, y con ella abrir antes de la fecha toda cápsula de esa ronda o anterior;
  • la apertura después de la fecha: si la red deja de firmar antes de la ronda de una cápsula, nadie podrá abrirla nunca; si firma la ronda pero nadie conserva su release, abrirla depende de encontrarlo (§50).

Una cápsula time_and_key añade una credencial que el proveedor no tiene: con ella, una firma anticipada de la ronda no basta para abrir la cápsula, pero la falta de la ronda la deja igual de cerrada (§36.1). El SDK oficial SHOULD recomendarla para horizontes largos o contenidos valiosos.

Un perfil pinneado se identifica por su profile_hash (§11, §12.1): un proveedor que cambie de clave, de periodo o de cadena es otro perfil, y las cápsulas ya escritas siguen dependiendo del anterior.

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.

Tampoco son post-cuánticos los recipients X25519 (§37), las firmas de autor Ed25519, ECDSA y RSA (§29.9, §29.10) ni las firmas de las autoridades de sellado (§29.11). En un horizonte largo, un adversario cuántico podría fabricar después de la fecha una firma y un sello que verifiquen, con una hora anterior a la ronda: un veredicto F3, F4, F6 o S4 de una cápsula leída décadas después prueba menos que el mismo veredicto leído pronto. Conservar los sellos con renovaciones de archivo (RFC 4998) queda fuera de la cápsula (§29.11).

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.

7.9 Firmantes y autoridades de sellado

En formato 3, con firma o sello, además:

  • quien roba la clave secreta de un firmante puede firmar en su nombre, y una firma con clave propia no prueba quién tiene la clave (§29.9);
  • un escritor puede crear dos cápsulas distintas con el mismo capsule_id, firmadas las dos (equivocación). Contra ella, el creador puede publicar capsule_digest antes de la fecha (§43);
  • quien puede abrir una cápsula puede rehacerla tras la fecha con otra área, sin una firma o con otra. Solo un sello anterior a la ronda prueba que una firma existía antes de la fecha (§29.11);
  • una autoridad de sellado deshonesta o comprometida puede fechar mal sus sellos. El protocolo no la audita, y el veredicto la nombra (§29.11);
  • la aplicación de firma externa recibe AUTHOR_MESSAGE, que no revela el contenido (§29.8). Un firmante firma lo que esa aplicación le muestra: una página comprometida o una aplicación de firma falsa pueden obtener su firma para una cápsula que no ha visto, y un organismo que cofirma solo con AUTHOR_MESSAGE nunca ve el contenido. El código de AUTHOR_MESSAGE permite comparar lo que se firma con lo que muestra DateKeys (§62.1, regla 20);
  • un firmante puede firmar coaccionado, y la firma no lo distingue;
  • una clave de autor robada no se puede revocar: un lector que la guardó seguirá mostrando su etiqueta (§29.12);
  • DateKeys no comprueba quién emitió un certificado ni un sello: cualquiera puede fabricar un certificado a nombre de otra persona, o un sello con la fecha que quiera. Lo comprueba un validador oficial, con las pruebas que guarda la cápsula (§29.10).

7.10 Cliente web

Un cliente que el navegador descarga en cada visita, como una página que cifra o abre cápsulas, lo puede sustituir quien sirve la página, quien controla su dominio o quien compromete su construcción. Un cliente sustituido puede copiar el contenido, las credenciales y las palabras de una llave antes de cifrar o después de abrir, o escribir una cápsula que no se abrirá. Ninguna regla del formato lo detecta: la cápsula que escribe es válida. §59 dice cómo reducir ese riesgo.


8. Objetos del protocolo

DateKey
    ↓
condición temporal pública

DateKeyCap (.dkc)
    ↓
objeto protegido

DateKeys Access Key (.dkk)
    ↓
capacidad adicional de acceso

Objeto release
    ↓
firma pública de una ronda, como dato (§47.1)

9. Provider abstraction

Toda condición temporal se expresa como:

provider
profile
condition

Ejemplo Quicknet:

{
  "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:

0  → "datekeys-provider-profile"
1  → 1
2  → "datekeys:quicknet:v1"
3  → "drand"
4  → "quicknet"
5  → <32-byte chain hash>
6  → <public key bytes>
7  → 3
8  → 1692803367
9  → "bls-unchained-g1-rfc9380"
10 → <genesis seed bytes>

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:

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

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 MUST pinnear estos parámetros, byte a byte. V1 pinnea un único perfil, Quicknet, con el scheme bls-unchained-g1-rfc9380: rondas sin encadenar, firmas en G1 de 48 bytes y clave pública en G2 de 96 bytes. Es el único scheme cuya verificación de release y cuyo descifrado tlock fija este documento byte a byte (§63, pasos 10 y 11, y los párrafos que siguen al flujo), con sus vectores en testdata/vectors/tlock_steps.json.


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 bls-unchained-g1-rfc9380, el único scheme de drand cuya verificación y cuyo descifrado tlock fija este documento byte a byte (§63, pasos 10 y 11). Los otros schemes sin encadenar que tlock admite, pedersen-bls-unchained y bls-unchained-on-g1, no son de V1;
    • public_key es la codificación canónica (§12.2) de un punto de G2, 96 bytes, 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:

    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.

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:

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:

round_time(r) =
genesis_time + (r - 1) * period

El SDK MUST elegir la primera ronda cuyo:

round_time >= requested_unlock_at

Algoritmo:

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:

genesis_time = 1692803367
period = 3
requested_unlock_at = 2030-01-01T00:00:00Z

resultado:

round = 66884212
round_time = 2030-01-01T00:00:00Z

La ronda:

66432123

corresponde a:

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:

received_round == locally_resolved_round

y:

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 en la mano (§49): 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:

dk1_

con JSON UTF-8.

Payload canónico:

{"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:

dk1_<base64url(JSON_CANONICO)>

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:

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

.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:

16 random bytes

generados por un CSPRNG. §24 y datekeys.cddl fijan esa longitud (§57).


22. Framing .dkc V1

V1 fija el siguiente prelude:

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:

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:

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:

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.


24.1 Nota pública

La nota pública es un texto corto que quien crea la cápsula deja a la vista, para que se sepa qué es antes de la fecha, como «Cartas del viaje a Lisboa». Es la extensión datekeys.note, versión 1, registrada para el array no crítico de PUBLIC_HEADER (§72):

  • su data es el texto en UTF-8, de 1 a 1024 bytes, sin CBOR alrededor, y MUST cumplir las reglas de texto del autor declarado de §29.6: una sola línea, sin tabuladores ni espacios en los extremos;
  • un lector que no la conoce la ignora y abre la cápsula igual (§54);
  • un lector que la conoce y encuentra una data que incumple esas reglas la trata como inutilizable, no la muestra y lo notifica (§54).

Es pública: la lee cualquiera que tenga el .dkc, también antes de la fecha, y no se puede borrar de las copias que circulen (§55.2). Junto con la fecha puede identificar a alguien: «Para Lucía, en su 18 cumpleaños» da la fecha de nacimiento de una menor. El escritor SHOULD avisarlo al pedirla.

No está comprobada antes de la fecha: cifrar hacia una ronda no exige ningún secreto, así que cualquiera puede fabricar una cápsula con la nota que quiera (§36.1). El paso 15 la ata al control, porque header_binding cubre los bytes exactos de PUBLIC_HEADER (§26), y una firma la cubre a través de control_commit (§29.8). Una nota que alguien cambia después de escribir la cápsula hace fallar el paso 15 con ERR_HEADER_BINDING, y nadie lo nota antes de la fecha.

Un lector que la muestra:

  • MUST presentarla como texto del creador que nadie ha comprobado, con la presentación de §29.7 para ese texto. El SDK oficial MUST titularla «Nota pública del creador (sin comprobar)»;
  • antes de la fecha, MUST mostrar junto a ella «Nadie puede comprobar antes de la fecha quién creó la cápsula ni si va firmada.»;
  • MUST NOT convertirla en un enlace ni ofrecer editarla.

25. Política de acceso declarada

Valores V1:

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:

ERR_POLICY_STRUCTURE_MISMATCH

26. Header binding

CONTROL_CBOR MUST contener:

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 firma de security (§29.8) o una extensión de firma pueden aportar autenticidad (§36.1, §72).


28. Tres ficheros age

La construcción time_and_key V1 utiliza tres ficheros age estándar e independientes:

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:

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:

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:

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:

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):

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:

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):

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:

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:

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:

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: un escritor de esta versión MUST escribir AREA_LEN = 32768 (32 KiB), lleve o no firma o sello, salvo que quien crea la cápsula la amplíe expresamente porque sus firmas no caben, y entonces MUST escribir 65536 (§62.1, regla 13). MUST escribir SECURITY_CBOR siempre, aunque vaya vacío. Los escritores de la v0.10 escriben 512;
  • 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, salvo en un área ampliada (§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 32 KiB y el código 2 (informativo):

Contenido HEAD_LEN L P
sin ficheros ni comentario 53 32833 34816
un comentario de 1 byte 56 32836 34816
un fichero nota.txt de 1000 bytes, con mtime 117 33897 34816

SECURITY_CBOR vacío mide 22 bytes (§29.3), así que el área lleva 32 746 bytes a cero. Con el área de 512 bytes de la v0.10, los mismos contenidos dan L = 577, 580 y 1641, y P = 768, 768 y 1792.


29.3 security

SECURITY_CBOR es un mapa CBOR con el perfil de §58:

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:

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 define:

  • alg 1, una firma Ed25519 estricta con una clave propia (§29.9);
  • alg 2, una firma CMS con uno o varios certificados X.509 (§29.10). Su clave 1 lleva la lista de firmantes exigidos en lugar de una clave pública;
  • seal_type 2, un sello de tiempo RFC 3161 (§29.11).

Quedan reservados seal_type 1 (servicio de sellado de DateKeys) y 3 (OpenTimestamps). alg y seal_type 4294967295 quedan reservados para pruebas: ninguna versión los definirá, así que un lector siempre los da por no soportados. En esta versión:

  • un escritor MUST escribir security vacío, {0: "datekeys-security", 1: 1}, de 22 bytes, en una cápsula sin firma ni sello (§62.1, regla 13);
  • con alg 2, el sello va dentro de la firma (§29.10), y la clave 3 MUST NOT existir;
  • un lector MUST aceptar las claves 2 y 3 y evaluarlas como dice §29.7.

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:

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:

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
alg conocido cuyo contenido incumple su perfil (§29.9, §29.10) F1 no cambia
Una firma presente que no verifica F2 no cambia
alg 1, firma válida F3 o F4 no cambia
alg 2, un firmante exigido ausente, no verificable, sin sello, con el sello inválido o con el certificado fuera de validez, o hay clave 3 (§29.10) F5 no cambia
alg 2, todos los firmantes exigidos válidos, cada uno con su sello F6 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
seal_type 2 cuyo token incumple su forma o usa un algoritmo fuera de la tabla, en el orden de §29.11 no cambia S2 o S1
Sello que no verifica no cambia S3
Sello válido, con accuracy y t + precisión < round_time no cambia S4
Sello válido, en otro caso no cambia S5

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. Un lector de la v0.10 solo llega a X, F0, F1, S0, S1 y S2; esta versión añade F2 a F6 y S3 a S5 sin cambiar los anteriores.

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.»
F2 firma inválida «La firma no corresponde a este contenido.»
F3 alg 1, válida con una clave guardada «Firmado con la clave que guardaste como ‹etiqueta›.»
F4 alg 1, válida con otra clave «Firmado con la clave ‹dkauthor1… completa›. No prueba quién la tiene.»
F5 alg 2, incompleta «Faltan firmas o sellos que la propia cápsula exige: trátala como no firmada.»
F6 alg 2, completa «Firmado con un certificado a nombre de ‹titulares›. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.»
S3 sello inválido «El sello no corresponde a este contenido.»
S4 válido, con accuracy y t + precisión < round_time «Según un sello a nombre de ‹TSA›, existía el ‹t›, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.»
S5 válido, sin acreditar que sea anterior a round_time «No acredita que se sellara antes de la fecha de apertura: ‹motivo›.»

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.

En los textos, ‹etiqueta› es la que la persona dio a una clave guardada (§29.12), y ‹t›, el instante del sello en UTC, en la forma de RFC 3339, con su fracción de segundo si la tiene. ‹titulares› son los nombres de los titulares de los certificados de los firmantes exigidos, y ‹TSA›, el del titular del certificado de la autoridad de sellado, cada uno entre « y » (U+00AB y U+00BB), y varios, separados por una coma y un espacio: así se ve dónde empieza y dónde acaba lo que escribió quien hizo el certificado.

‹motivo› es el primero que se cumple: «se selló después de esa fecha o demasiado cerca de ella», si t + precisión ≥ round_time, con una precisión de 0 si el token no lleva accuracy; «el sello no dice la precisión que exige su política», si el token no lleva accuracy y su policy es la BTSP de ETSI (§29.11); y «el sello no dice su precisión», en otro caso.

El nombre de un certificado se toma de su subject con el perfil de §29.10: su givenName y su surname, separados por un espacio, si tiene uno de cada y los dos tienen texto (§29.10) no vacío, y si no, su commonName; nunca su serialNumber, y el commonName va detrás porque puede llevar el identificador nacional de la persona. Se muestra si cumple las reglas del autor declarado de §29.6, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos; si no, o si el certificado no da un nombre, se muestra el SHA-256 del certificado en hexadecimal.

Con F6, el lector MUST mostrar además, tras su línea, una por cada firmante exigido, en el orden de SIGNERS:

  «‹titular›» (emisor según su certificado: «‹emisor›»), sellado por «‹TSA›» el ‹t›, antes de la fecha de apertura.

con «sin acreditar que fuera antes de la fecha de apertura: ‹motivo›» en lugar de «antes de la fecha de apertura» si su sello no lleva accuracy o t más su precisión no es anterior a round_time, con el ‹motivo› de S5. ‹emisor› es el commonName del emisor que dice el certificado, o si no tiene uno con texto (§29.10), su organizationName, con las reglas de un titular; si no hay, o las incumple, el SHA-256 de su nombre, el DER de su Name, en hexadecimal. Si alguna de esas líneas dice «antes de la fecha de apertura», el lector MUST añadir después la línea DateKeys no comprueba quién emitió los sellos.. Un SignerInfo cuyo certificado no está entre los exigidos se muestra aparte, con su resultado, y no cuenta (§29.10):

  Otro firmante, «‹titular›»: ‹resultado›. No cuenta.

con ‹resultado› «válida», «inválida», «no verificable», «sin sello», «con el sello inválido» o «con el certificado fuera de validez».

Un lector MUST NOT decir que una cápsula se firmó antes de la fecha salvo por un sello válido que lleva accuracy, con t + precisión < round_time, y entonces MUST decir que no comprueba quién emitió el sello: quien puede abrirla puede rehacer el área (§7.9). Con un sello válido, una mtime posterior a t SHOULD mostrarse como incoherencia.

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;
    • partir cada línea de los veredictos en filas de como mucho W − 3 columnas, contadas igual, por el último espacio que cabe tras algún texto, que se omite, o, dentro de una palabra más larga que la fila, tras el último punto de código que cabe; y empezar cada fila después de la primera con ↳ (dos espacios, U+21B3 y un espacio), que cuenta 5 columnas.

    Así el terminal nunca parte una línea por su cuenta, ninguna línea del creador aparece sin prefijo ni puede imitar un veredicto, y ningún nombre de un certificado empieza una fila;

  • 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.


29.8 Firma de autor: qué se firma

Una firma de autor cubre el control, sin I_PAYLOAD ni L, el head y la lista de firmantes exigidos. Nunca cubre el área ni el relleno: el tamaño del área no cambia lo firmado, así que un área puede ampliarse después de firmar sin volver a firmar (§62.1, regla 13).

payload_commit = SHA-256("datekeys:dkc3:payload:v1" || 0x00 || I_PAYLOAD)
CONTROL_SIG    = CONTROL_CBOR con los 32 bytes de payload_identity (clave 3)
                 sustituidos por payload_commit, y los 8 de payload_length
                 (clave 6) a cero
control_commit = SHA-256("datekeys:dkc3:control:v1" || 0x00 || CONTROL_SIG)
head_digest    = SHA-256("datekeys:dkc3:head:v1" || 0x00 || HEAD_CBOR)
signers_digest = SHA-256("datekeys:dkc3:signers:v1" || 0x00 || u32(alg) || SIGNERS)
D              = SHA-256(control_commit || head_digest || signers_digest)
AUTHOR_MESSAGE = "datekeys:dkc3:author-signature:v1" || 0x0A || hex(D) || 0x0A  ; 99 bytes
código         = los 8 primeros caracteres de hex(D), en dos grupos de 4: «xxxx-xxxx»
  • CONTROL_SIG tiene la longitud y la estructura de CONTROL_CBOR: solo cambian esos 40 bytes. Cubre header_binding, y con él PRELUDE y PUBLIC_HEADER (§26), la regla de relleno y las extensiones de control, y oculta I_PAYLOAD.
  • L no entra. Ocupa siempre 8 bytes (§31), así que el tamaño del área no cambia SEALED_CONTROL_LEN ni header_binding. Los tamaños de los ficheros sí entran, por el head.
  • head_digest fija CONTENT byte a byte, porque el head lleva el tamaño y el SHA-256 de cada fichero (§29.4). La sal del head lo hace un compromiso que oculta: quien ve AUTHOR_MESSAGE sin el head, como la aplicación de firma externa, no puede confirmar una conjetura sobre el contenido.
  • u32(alg) son 4 bytes big-endian. SIGNERS son los bytes exactos del contenido de la clave 1 del mapa author-signature con alg 2 (§29.10), y ninguno con alg 1.
  • Cada prefijo de dominio es una cadena ASCII. En las fórmulas lo sigue un byte 0x00, y en AUTHOR_MESSAGE, un salto de línea (0x0A). hex escribe en minúsculas.
  • AUTHOR_MESSAGE es texto ASCII: quien firma puede abrirlo, y antes de firmar compara su código con el que muestra DateKeys (§62.1, regla 20).

Todos estos valores se recalculan desde la cápsula abierta; ninguno se almacena.

Qué prueba una firma válida: que sus firmantes firmaron este head para este control y esta lista de firmantes. Cada uno firma lo que le muestra su aplicación: si conocía el contenido depende de ella, y un cofirmante que solo recibe AUTHOR_MESSAGE no lo conoce (§7.9). No prueba cuándo firmaron, salvo con un sello (§29.11), ni la autoría del contenido, ni el autor declarado, ni que no exista otra cápsula con el mismo capsule_id (§7.9).

Trasplantes: una firma no vale en otra cápsula, porque control_commit cubre capsule_id, la DateKey y payload_commit, ni en otro contexto, porque cada valor firmado lleva su prefijo de dominio.


29.9 Firma con clave propia (alg 1)

alg 1 es una firma Ed25519 (RFC 8032) de AUTHOR_MESSAGE con una clave de autor (§29.12):

author-signature:
0 → 1
1 → clave pública A (32 bytes)
2 → firma (64 bytes): R (32 bytes) || S (32 bytes)

Una clave o una firma de otra longitud incumple el perfil (F1). La firma es válida si y solo si se cumplen las cuatro condiciones siguientes; si falla alguna, no verifica (F2):

  1. A es la codificación canónica de un punto: su y es menor que p, y si y vale 1 o p − 1, el bit de signo es 0;
  2. A decodifica a un punto que no es de orden pequeño;
  3. sig[63] & 0xE0 = 0, y S, como entero little-endian, es menor que ℓ;
  4. [S]B − [k]A codifica exactamente R, con k = SHA-512(R ‖ A ‖ AUTHOR_MESSAGE) mod ℓ.

Es la verificación sin cofactor de RFC 8032 con A y R canónicas. Una implementación MUST NOT aceptar más. Go 1.26 ed25519.Verify acepta A = 01 00…00 con R igual a la identidad y S = 0, y verify de noble 2.4 usa siempre la ecuación con cofactor: las dos necesitan comprobaciones propias. ed25519_strict.json da el resultado esperado de cada caso de «Taming the many EdDSAs» (§64).

Con alg 1, la lista de firmantes exigidos va vacía: la propia clave identifica al firmante. Una firma válida prueba que alguien con la clave secreta de A firmó, no quién la tiene; quien abre la reconoce si la recibió antes por otro canal (§29.12).


29.10 Firma con certificado (alg 2)

alg 2 es una firma CMS (RFC 5652) separada de AUTHOR_MESSAGE, con el perfil CAdES (ETSI EN 319 122-1) que producen las aplicaciones de firma con certificados X.509 de cualquier país: una tarjeta de identidad, un certificado cualificado, el de una empresa o el del almacén del sistema, con AutoFirma, DigiDoc, una tarjeta por PKCS#11 u otra aplicación. Lleva uno o varios firmantes, que firman el mismo AUTHOR_MESSAGE (cofirma).

author-signature:
0 → 2
1 → SIGNERS: la lista de firmantes exigidos (cadena de bytes)
2 → la firma: un ContentInfo de tipo SignedData, en DER (cadena de bytes)

Firmantes exigidos. SIGNERS es un array CBOR con el perfil de §58, de 1 a 16 cadenas de 32 bytes: el SHA-256 del certificado de cada firmante, sobre sus bytes tal cual, en orden estrictamente ascendente de bytes y sin repeticiones. Están todos, también el autor, porque CMS no ordena a los firmantes y una lista parcial dejaría retirar sin rastro a los que no estuvieran.

  • La lista se cierra antes de la primera firma, así que exige conocer antes los certificados (§62.1, regla 20).
  • Va en el área y no en el control: SEALED_CONTROL_LEN es visible (§55.2), y una extensión de control que solo existiera con firmantes los delataría. Entra en lo firmado por signers_digest (§29.8).
  • Un SignerInfo de un certificado que no está en SIGNERS es un firmante ajeno: se verifica igual, se muestra aparte con su resultado y nunca cuenta para el veredicto.

Forma. La firma MUST cumplir, y un lector MUST comprobar, en este orden; un fallo es F1:

  1. DER de X.690 en todo el ContentInfo, con los SET OF ordenados y sin bytes de más, y contentType id-signedData (1.2.840.113549.1.7.2). X.690 admite que un SET OF repita un elemento, y un lector MUST aceptarlo: dos copias de un certificado cuentan como una, y cuántas veces puede ir un atributo lo dice la regla 4. Los certificados se leen con el perfil de abajo y los tokens con el de §29.11; las respuestas OCSP no se analizan.
  2. Firma separada: encapContentInfo con eContentType id-data (1.2.840.113549.1.7.1) y sin eContent.
  3. Cada SignerInfo identifica con su sid, por issuerAndSerialNumber, comparando byte a byte el DER del emisor y el contenido del número de serie, o por subjectKeyIdentifier, exactamente un certificado de certificates que cumple el perfil de abajo, y ningún certificado tiene dos SignerInfo. Un certificado que incumple el perfil, o que ningún SignerInfo identifica, no decide nada: un validador externo puede usarlo como intermedio. En crls solo van respuestas OCSP (OtherRevocationInfoFormat con id-ri-ocsp-response, RFC 5940).
  4. Cada SignerInfo lleva signedAttrs con exactamente un content-type (id-data), un message-digest y un signing-certificate-v2 (RFC 5035) cuyo primer ESSCertIDv2 da el hash de su certificado; y como mucho un atributo no firmado signature-time-stamp (id-aa-signatureTimeStampToken, 1.2.840.113549.1.9.16.2.14), con un solo valor. Los demás atributos, firmados o no, no deciden nada.

Sobre esa forma, un lector MUST aplicar además:

  • el ContentInfo y cada SignerInfo son SEQUENCE; la version de un SignerInfo es 1 con issuerAndSerialNumber y 3 con subjectKeyIdentifier (RFC 5652, §5.3);
  • un atributo tiene al menos un valor, y un atributo de un tipo que esta sección pide una sola vez se cuenta por atributo y no por valor: dos atributos content-type, uno de ellos con un conjunto de valores vacío, incumplen la forma;
  • un signing-certificate (RFC 2634) junto al signing-certificate-v2 no decide nada: cuenta el v2;
  • un ESSCertIDv2 con el hashAlgorithm SHA-256 escrito de forma explícita se acepta, aunque sea su valor por defecto y DER no lo escriba, porque algunas aplicaciones de firma lo escriben; su hash MUST ser uno de la tabla de abajo, y SHA-1 incumple la forma;
  • los parámetros de RSASSA-PSS van en el orden de sus etiquetas, sin repetir ninguno y sin escribir el trailerField que vale 1 por defecto: de otro modo, la firma no es verificable;
  • los identificadores de objeto se comparan por los bytes de su DER: un arco de cualquier tamaño es solo un identificador que la tabla no tiene, y un atributo de un tipo que esta sección no nombra no decide nada aunque lleve uno.

Algoritmos. Una tabla cerrada; el hash de la firma es el de digestAlgorithm:

Algoritmo OID
SHA-256, SHA-384 y SHA-512 2.16.840.1.101.3.4.2.1, .2 y .3, con los parámetros ausentes o NULL
RSASSA-PKCS1-v1_5 rsaEncryption (1.2.840.113549.1.1.1), o sha256, sha384 y sha512WithRSAEncryption (1.2.840.113549.1.1.11, .12 y .13)
RSASSA-PSS 1.2.840.113549.1.1.10, con el hash de digestAlgorithm, MGF1 con ese hash, una sal de su longitud y trailerField 1
ECDSA ecdsa-with-SHA256, SHA384 y SHA512 (1.2.840.10045.4.3.2, .3 y .4), con P-256 (1.2.840.10045.3.1.7), P-384 (1.3.132.0.34) o P-521 (1.3.132.0.35)

Las claves RSA miden de 2048 a 4096 bits, con un módulo impar y un exponente impar de 3 a 2³¹ − 1. Esta versión no admite las curvas brainpool, que usan algunas tarjetas europeas, ni algoritmos nacionales como GOST o SM2: ni Go ni Web Crypto los traen, y verificarlos exigiría código propio en las dos implementaciones.

Perfil del certificado. Un lector lee de un certificado solo lo que usa, con estas reglas, y uno que las incumple no cumple el perfil:

  • un SEQUENCE de tbsCertificate, signatureAlgorithm y signature, sin nada más;
  • tbsCertificate: version [0], que vale 2 (la versión 3), serialNumber, signature, issuer, validity, subject y subjectPublicKeyInfo, y después, si están, [1], [2] y [3], en ese orden y sin nada detrás;
  • un nombre: un SEQUENCE de SET no vacíos de AttributeTypeAndValue, cada uno un identificador de objeto y un valor; el orden de cada SET no se comprueba;
  • validity: dos instantes, UTCTime o GeneralizedTime en DER y sin fracción de segundo (RFC 5280, 4.1.2.5), con los años 50 a 99 de un UTCTime en el siglo XX;
  • las extensiones: [3] lleva un SEQUENCE de una o más Extension, cada una un identificador de objeto, un BOOLEAN opcional y un OCTET STRING, sin repetir ninguna; subjectKeyIdentifier lleva un OCTET STRING no vacío.

La firma del certificado, su algoritmo y las demás extensiones no deciden nada. La clave de subjectPublicKeyInfo está en la tabla si es rsaEncryption con parámetros NULL y un RSAPublicKey de exactamente un módulo y un exponente, o id-ecPublicKey (1.2.840.10045.2.1) con una curva con nombre de la tabla y el punto sin comprimir, en la curva; cualquier otra es una clave fuera de la tabla.

El texto de un atributo de un nombre sale solo de un UTF8String en UTF-8 válido, un PrintableString en su alfabeto, un IA5String o un TeletexString en ASCII, o un BMPString en UTF-16BE sin sustitutos; un valor de otro tipo, o con bytes que incumplen su tipo, no es texto. Del texto no se quita nada: una marca de orden de bytes o un terminador se quedan, y deciden las reglas de §29.6. Un tipo de atributo que aparece más de una vez en un nombre no da texto. Un nombre sin texto se muestra por su SHA-256 (§29.7).

Verificación. Para cada hash de SIGNERS, el resultado es el primero que se cumple:

  1. ningún SignerInfo es de ese certificado → ausente;
  2. un algoritmo, una clave o una curva fuera de la tabla → no verificable;
  3. el message-digest no es el hash de AUTHOR_MESSAGE, o la firma de signedAttrs no verifica con la clave del certificado, también cuando la clave es de otro esquema que el algoritmo → inválido;
  4. sin signature-time-stamp → sin sello;
  5. el token, con el perfil de §29.11, sobre los bytes del campo signature de ese SignerInfo: S2, S1 o S3 → sello inválido;
  6. el certificado no es válido en t, el instante del sello → fuera de validez;
  7. en otro caso → válido, con t.

El veredicto es el primero que se cumple: F2 si algún firmante exigido es inválido; F5 si alguno está ausente, no verificable, sin sello, con el sello inválido o fuera de validez, o si la cápsula lleva clave 3; y F6 si todos son válidos.

Lo que DateKeys no comprueba: quién emitió cada certificado, si se revocó y si el certificado o la firma son cualificados. Lo hace un validador oficial, el del país que corresponda, cuando haga falta y con las listas de confianza de ese momento. Para que pueda hacerlo años después, la cápsula guarda las pruebas del momento de la firma que caben en el área: siempre la firma, el certificado de cada firmante y su sello, que prueba que se firmó cuando el certificado era válido; y, si caben, las cadenas de los firmantes y de las autoridades de sellado sin sus raíces y las respuestas OCSP (§62.1, regla 21). Lo que no cupo lo dijo el escritor al crearla, y solo existe donde se exportó: un validador puede necesitarlo cuando quizá ya no pueda pedirlo. El SDK oficial SHOULD poder exportar AUTHOR_MESSAGE y la firma, avisando de que así entrega a un tercero el certificado de otra persona (§55.2). El texto de F6 lo dice (§29.7).

Lo que prueba F6: que quien tenía la clave de cada certificado exigido firmó este AUTHOR_MESSAGE, y que esa firma existía en t según un sello. A nombre de quién está de verdad el certificado, y quién emitió el sello, lo dice un validador oficial. Cada firmante firma lo que le muestra su aplicación: si conocía el contenido depende de ella (§7.9). Si alguien retira todas las firmas tras abrir la cápsula, nada interno lo detecta: hace falta que quien la recibe espere una firma.


29.11 Sello de tiempo

Un sello de tiempo prueba que unos bytes existían en un instante t, según una autoridad de sellado (TSA). Esta versión define el sello RFC 3161 (con RFC 5816) en dos sitios, nunca en los dos en la misma cápsula:

Cápsula Sello Qué sella
Con alg 2 CAdES-T dentro de la firma, uno por firmante (§29.10) el valor de la firma de ese firmante
Sin firma o con alg 1 opcional, en la clave 3, con seal_type 2 SEAL_SUBJECT
SIG_PART     = 0x00                                             ; sin clave 2
             | 0x01 || SHA-256("datekeys:dkc3:sig-part:v1" || 0x00
                               || el contenido de la clave 2)       ; con clave 2
SEAL_SUBJECT = SHA-256("datekeys:dkc3:seal-subject:v1" || 0x00
                       || control_commit || head_digest || SIG_PART)

seal:
0 → 2
1 → token: un ContentInfo de tipo SignedData con un TSTInfo, en DER (cadena de bytes)

SIG_PART usa el contenido exacto de la cadena de bytes de la clave 2, sin su cabecera CBOR, sea cual sea su alg y su veredicto: un sello se verifica igual junto a una firma ilegible, no soportada o inválida, y prueba que esos bytes existían en t. Con seal_type 2, el messageImprint del token es SHA-256(SEAL_SUBJECT), con el algoritmo SHA-256: la TSA nunca ve SEAL_SUBJECT. El sello se pide después de la firma.

Perfil del token, en los dos sitios. Un lector comprueba, en este orden, y el resultado es el del primer fallo:

  1. Forma (S2): DER de X.690; un SignedData con eContentType id-ct-TSTInfo (1.2.840.113549.1.9.16.1.4), su eContent y un solo SignerInfo; en él, signedAttrs con content-type id-ct-TSTInfo, message-digest y un signing-certificate (ESSCertID, cuyo SHA-1 solo identifica) o signing-certificate-v2 que identifica el certificado de la TSA en certificates, con el perfil de §29.10; y un TSTInfo de versión 1, en DER aunque vaya dentro de una cadena de bytes: genTime en UTC con la letra Z y una fracción sin cero final, accuracy con segundos de 0 a 2³¹ − 1 y milisegundos y microsegundos de 1 a 999, cada uno un INTEGER mínimo, ordering solo si es TRUE, los campos en su orden y ninguno después del último. En un token, crls no decide nada.
  2. Algoritmos (S1): los de la tabla de §29.10, también la clave del certificado de la TSA, incluido el de messageImprint, que con seal_type 2 es SHA-256.
  3. Verificación (S3): el message-digest es el hash del eContent, la firma de la TSA verifica, messageImprint es el hash de lo sellado, y no lo es uno de otra longitud, y el certificado de la TSA es válido en genTime.

t es genTime, y su precisión, la de accuracy, con 0 en los campos que no lleva (RFC 3161, §2.4.2). Un sello que pasa los tres da S4 si lleva accuracy y t + precisión < round_time, y S5 en otro caso, con el motivo de §29.7. Dentro de alg 2, el resultado se traduce como dice §29.10.

Sin accuracy. RFC 3161 deja entonces la precisión a la política de la TSA, que el token nombra en policy y el lector no conoce, y ningún margen fijo vale para toda TSA. Un lector MUST NOT suponer una precisión que el token no dice, tampoco por su política. La política de buenas prácticas de ETSI EN 319 421 V1.3.1 (BTSP, 0.4.0.2023.1.1, §5.2) exige una precisión declarada de 1 s o mejor (§5.1 y TIS-7.7.2-03) y tokens con el perfil de ETSI EN 319 422 (TIS-7.7.1-01), cuyo §5.2.2 exige accuracy: un token de esa política sin accuracy la incumple, y su motivo de S5 lo dice.

Lo que prueba S4: según la autoridad que dice el sello, lo sellado existía en t, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién es esa autoridad, ni su revocación, ni si es cualificada: lo hace un validador oficial. Un sello no cambia el veredicto de la firma.

Fuera de la cápsula pueden ir otros sellos: OpenTimestamps, un sello cualificado del .dkc o resellados de archivo (RFC 4998). Esta versión no los define. Sellan ese fichero cifrado, no que un firmante conociera su contenido, y un lector MUST NOT presentarlos como una firma interna.


29.12 Claves de autor (alg 1)

Una clave de autor es una semilla Ed25519 de 32 bytes:

  • la pública, A, se escribe en bech32 (BIP 173) con el prefijo dkauthor, en minúsculas: dkauthor1…, 67 caracteres;
  • la secreta, la semilla, en bech32 con el prefijo DKAUTHOR-SECRET-KEY-, en mayúsculas: 79 caracteres.

Una implementación MUST rechazar una clave en otra caja, con otra longitud, con otro prefijo o con los bits de relleno distintos de cero. Un fichero de clave secreta lleva una línea con ella y, por defecto, va cifrado con age y una contraseña, con scrypt de logN = 16.

Cómo conoce la clave quien abre: por un canal externo, porque una firma alg 1 no dice quién tiene la clave, y un sello no prueba quién lo emitió. Un lector MAY guardar una clave que la persona recibió así, con la etiqueta que ella le dé, y dar después F3, y MUST permitir borrarla. MUST NOT ofrecer guardarla desde una cápsula.

Una clave robada no se puede revocar: un lector que la guardó seguirá dando F3 a las firmas de quien la robó (§7.9). Ninguna decisión se basa en una huella truncada de la clave.


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:

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:

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:

a6 00 70 "datekeys-control" 01 <02 o 03> 02 58 20 <header_binding>
03 58 20 <I_PAYLOAD> 06 48 <L, 8 bytes> 07 <código, 01 o 02>

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:

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:

CONTROL_CBOR
    ↓
OUTER_TIME_AGE

donde:

OUTER_TIME_AGE =
age file
recipient = tlock(DateKey)
plaintext = CONTROL_CBOR

OUTER_TIME_AGE MUST contener exactamente un stanza, de tipo tlock.

age genera:

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:

CONTROL_CBOR
    ↓
INNER_ACCESS_AGE
    ↓
OUTER_TIME_AGE

Primero:

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:

FK_ACCESS = 16 random bytes

Después:

OUTER_TIME_AGE =
age file
recipient = tlock(DateKey)
plaintext = exact bytes of INNER_ACCESS_AGE

age genera:

FK_TIME = 16 random bytes

La política implementa:

TIME AND KEY

No dos envolturas paralelas.


34. SEALED_CONTROL

En .dkc:

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.

En Quicknet, la identity tlock:

  1. verifica el release con la ecuación del paso 10 de §63, e(H(M), clave pública) == e(firma, G2), con M y H los de «Mensaje de ronda y hash a G1», tras el flujo de §63;
  2. descifra el cuerpo del stanza, U ‖ V ‖ W, con la firma de ese release, como fija el paso 11: sigma = V XOR H2(e(firma, U)), FK_TIME = W XOR H4(sigma) y r = H3(sigma, FK_TIME), con H2, H3 y H4 los de «Serialización de GT en H2» y «H3 y H4», tras el mismo flujo;
  3. comprueba r·G2 == U, con G2 el generador de G2;
  4. devuelve FK_TIME, de 16 bytes, como file key de OUTER_TIME_AGE. Con ella age verifica el MAC de la cabecera y deriva la clave del payload, como fija la especificación age (§28.1, §77); DateKeys no la transforma.

Ningún dato del stanza elige la clave pública, el scheme, el DST ni las etiquetas: salen del perfil pinneado y de este documento.


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:

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 firma de autor (§29.8) o 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, tampoco con una firma anticipada de la ronda por un proveedor comprometido (§7.6). 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. Una firma de autor válida (§29.8 a §29.10) prueba que sus firmantes firmaron este contenido para este control, no quién creó el .dkc: tras la fecha, quien puede abrir la cápsula puede rehacerla sin la firma o con otra. Solo un sello anterior a la ronda prueba que la firma existía antes de la fecha (§29.11).


37. X25519 recipient V1

V1 adopta el recipient X25519 estándar de age.

Una identity X25519:

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:

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.


38.1 Llave de palabras

Una llave de palabras es una credencial X25519 de time_and_key que se deriva de palabras que elige quien crea la cápsula, en lugar de guardarse en una .dkk. Es un recipient más de INNER_ACCESS_AGE y cuenta entre las 16 credenciales (§39): el formato no cambia, y la identity que se deriva abre la cápsula en cualquier lector.

Normalización. Las palabras de un texto son:

  1. su NFD, con las tablas de Unicode 18.0.0 de §29.5.1;
  2. sin los puntos de código U+0300 a U+036F;
  3. cada punto de código en minúscula, uno a uno, con su correspondencia simple de Unicode 18.0.0 (campo 13 de UnicodeData.txt), de una tabla que las implementaciones generan como las de §29.5.1;
  4. partido por los espacios U+0009 a U+000D, U+0020, U+0085, U+00A0, U+1680, U+2000 a U+200A, U+2028, U+2029, U+202F, U+205F y U+3000, sin palabras vacías.

Así, «Ábaco ÁRBOL», «abaco arbol» y «ábaco árbol» son las mismas palabras. La puntuación cuenta: «perro,» no es «perro».

Derivación.

P  = las palabras en UTF-8, separadas por un U+0020
S  = "DateKeys llave de palabras v2|" || hex(chain_hash) || "|" || decimal(ronda)
     || "|" || hex(capsule_id)
id = PBKDF2-HMAC-SHA256(P, S, 600000 iteraciones, 32 bytes)      ; RFC 8018

hex escribe en minúsculas el chain hash del perfil de la DateKey y los 16 bytes de capsule_id (§21), y decimal(ronda) es la ronda sin ceros a la izquierda. id es una identity X25519 cruda, como el access_material de una .dkk (§41), y su clave pública es el recipient. Con capsule_id en la sal, las mismas palabras dan una llave distinta en cada cápsula: cada intento de un atacante prueba una sola cápsula, aunque muchas compartan ronda.

Reglas del escritor. MUST:

  • exigir al menos 6 palabras tras la normalización;
  • rechazar un texto con controles distintos de los espacios de arriba, o con puntos de código Default_Ignorable o sin asignar (Cn) en Unicode 18.0.0: uno pegado sin querer dejaría la cápsula sin abrir.

Y SHOULD:

  • contar para ese mínimo solo las palabras distintas de 3 caracteres o más, que es lo que hace el SDK oficial, y recomendar más, o unas al azar;
  • mostrar las palabras normalizadas y pedir que se escriban de nuevo;
  • avisar de que no se reutilice una contraseña: tras la fecha, la cápsula sirve para probarla;
  • ofrecer por defecto palabras generadas al azar de una lista pública, al menos 6 de una lista de 2048 o más, y decir que unas palabras elegidas por una persona no son adecuadas para un contenido valioso: su fuerza es la de su entropía, y PBKDF2 solo la multiplica por las iteraciones.

Lector. MAY pedir las palabras en lugar de una .dkk o de una identity, y derivar id con la DateKey y el capsule_id de la cápsula, que dan los pasos 1 a 8 de §63.

Tras la fecha, quien tenga el .dkc puede probar palabras sin conexión: unas palabras elegidas por una persona son más débiles que unas al azar.

Vector: con «perro luna casa verde tren mar», el chain hash de Quicknet, la ronda 1000 y capsule_id = 000102030405060708090a0b0c0d0e0f, id = fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41.


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:

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

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:

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:

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:

access_type = "x25519"
access_material = 32 raw identity bytes

42. credential_id

Debe ser:

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:

0 → capsule_digest

donde:

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.

Con un localizador (§44.1), capsule_digest en claro no delata dónde está la cápsula: lo que se guarda fuera es el resto de un sobre cifrado, con otro hash.


44. Extensiones de aplicación en .dkk

Información de integración que no pertenezca al protocolo base DEBE ir en:

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).


44.1 Extensión datekeys.capsule

La extensión datekeys.capsule, versión 1, registrada para el array no crítico de la .dkk (§72), dice qué es la cápsula de una llave y, si se quiere, dónde está, para quien solo tiene la llave. Su data es un mapa CBOR con el perfil de §58:

0 → nota (texto, opcional)
1 → compact_datekey (texto)
2 → localizador (cadena de bytes, opcional)
  • La nota es la copia de la nota pública de la cápsula (§24.1), con sus mismas reglas.

  • compact_datekey es la DateKey de la cápsula en su forma canónica (§19): dice cuándo se abre.

  • El localizador es un fichero age con un único stanza tlock para la ronda y la cadena de esa DateKey (§28.1, §35). Su plaintext es otro mapa CBOR con el perfil de §58:

    0 → direcciones (array de 1 a 8 mapas de dirección)
    1 → I_SOBRE (32 bytes: la identity X25519 cruda del sobre)
    2 → cabecera del sobre (cadena de bytes de 1 a 1024)
    3 → resto_digest (32 bytes: el SHA-256 del resto del sobre)
    4 → resto_size (entero: la longitud del resto en bytes)
    5 → capsule_digest (32 bytes, §43)
    6 → relleno (cadena de bytes a cero, opcional)
    
    dirección:
    0 → URI (texto de 1 a 1024 bytes)
    1 → desplazamiento (entero, opcional: 0 si falta)
    

    El plaintext mide exactamente 4096 bytes, o el menor múltiplo de 4096 en el que quepa: la clave 6, de al menos un byte, completa lo que falte, así que, dentro de los primeros 4096 bytes, su longitud no delata cuántas direcciones hay ni de qué tipo. Si ninguna longitud de la clave 6 completa un múltiplo exactamente, porque la clave 6 ocupa al menos 3 bytes y la cabecera CBOR de su cadena crece a los 24 y a los 256 bytes, se usa el múltiplo siguiente: le pasa a una base, la longitud sin la clave 6, a la que le faltan 1, 2, 26 o 259 bytes para un múltiplo, como 4070 o 3837, que dan 8192. locator.json da los casos en torno a esos límites.

El sobre. Quien crea la cápsula cifra el .dkc con age para la pública de I_SOBRE, una identity nueva de un CSPRNG, y parte ese fichero age en dos: la cabecera, hasta el salto de línea de su MAC incluido, que va en el localizador; y el resto, el nonce y los chunks de STREAM, que es lo único que se guarda fuera. El resto no lleva ninguna marca: sus bytes no se distinguen del azar, así que quien lo encuentra no sabe que es un fichero age ni una cápsula. Nadie, tampoco quien tiene la llave, lee el localizador antes de la fecha. En la fecha, el mismo release que abre la cápsula lo descifra, y el lector une la cabecera y el resto y descifra el .dkc con I_SOBRE.

Dentro de otro fichero. El resto puede guardarse solo o dentro de otro fichero, el huésped: una imagen, un vídeo, un documento o un fichero de cualquier tipo, normalmente añadido al final, donde casi todos los programas lo ignoran. Su dirección dice entonces en qué byte del huésped empieza; su longitud es resto_size.

  • Es ocultación, no esteganografía: quien analice el huésped puede ver que lleva bytes de más, pero no qué son ni de qué cápsula.
  • Solo sirve un almacenamiento que conserva el fichero byte a byte, como un disco en la nube, IPFS o un servidor propio. Una red social o una aplicación de mensajería recomprimen las imágenes y los vídeos, o quitan lo que sobra, y el resto se pierde.

Direcciones. Cada URI es ASCII, de RFC 3986, con el esquema https o ipfs en minúsculas, sin userinfo. Un lector lee la dirección tal como está escrita, sin decodificar nada, y MUST rechazarla:

  • si lleva un carácter que RFC 3986 no admite, o un % sin dos dígitos hexadecimales detrás;
  • si su autoridad lleva %, @ o una barra invertida, o su puerto no es un número de 1 a 65535 sin ceros a la izquierda;
  • si su ruta tiene un segmento «.» o «..», escrito o con %2e: un cliente o una pasarela que lo resolviera pediría otra cosa que la que se muestra;
  • si el host de un https no son segmentos de 1 a 63 letras, dígitos y guiones, separados por puntos, que no empiezan ni acaban en guion, ni una dirección IP pública: una IPv4 fuera de los bloques 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4 y 240.0.0.0/4 de los registros de direcciones de uso especial de IANA, o una IPv6 de 2000::/3 fuera de 2001::/23, 2001:db8::/32, 2002::/16 y 3fff::/20, lo que deja fuera las que llevan dentro una IPv4, mapeadas, compatibles, de NAT64, 6to4 o Teredo;
  • si un nombre cuyo último segmento es numérico o empieza por 0x o 0X no es una dirección IPv4 pública en notación decimal con puntos, sin ceros a la izquierda;
  • si el nombre tiene un solo segmento, o es localhost, local, home.arpa, internal, invalid, test, example u onion, o termina en uno de ellos tras un punto, sin distinguir mayúsculas de minúsculas: solo los resuelve una máquina o una red local;
  • si un ipfs no lleva un CID v1 de 128 caracteres como mucho en base32 canónica, «b» y el alfabeto de RFC 4648 en minúsculas, sin relleno y con los bits sobrantes a cero; y, decodificado, con varint mínimos de 9 bytes como mucho, la versión 1, un códec y un multihash con un resumen de al menos un byte y de la longitud que dice, sin nada después.

Un nombre con letras que no son ASCII se escribe en su forma punycode. El host que muestra a la persona es ese texto, no el que daría una decodificación. Un lector rechaza cada dirección que incumple estas reglas y usa las demás; un escritor no escribe ninguna así. Un lector:

  • MUST NOT descargar sin que la persona lo pida, y MUST mostrar antes el host o el CID: la descarga revela a quien controla la dirección cuándo y desde dónde se usa la llave, y en IPFS la ven la pasarela y los pares;
  • MUST pedir solo los bytes del resto, con un rango de HTTP si el servidor lo admite; si no, MUST dejar de leer al llegar al desplazamiento más resto_size;
  • MUST NOT seguir una redirección a otro esquema ni a un host que estas reglas rechazan, y MUST comprobar en cada conexión que la dirección IP a la que resuelve un nombre es pública. En una red solo IPv6 con DNS64 y NAT64, un nombre que solo tiene IPv4 resuelve a una IPv6 que lleva dentro esa IPv4 (RFC 6052): una del prefijo bien conocido 64:ff9b::/96 cuenta como pública si la IPv4 de sus últimos 32 bits lo es. El lector MAY aceptar igual una del prefijo de NAT64 de su red, el que descubre con RFC 7050 o le da el sistema, si ese prefijo tiene una de las longitudes de RFC 6052, 32, 40, 48, 56, 64 o 96 bits, está en 64:ff9b::/16 o es una IPv6 pública, y la IPv4 que lleva dentro, en las posiciones de RFC 6052, es pública; con ese prefijo, una dirección suya cuenta como pública solo si lo es esa IPv4, aunque el prefijo sea una IPv6 pública. La regla es solo para la dirección a la que resuelve un nombre: una dirección escrita en el localizador con una IPv6 de NAT64 se sigue rechazando;
  • MUST comprobar que el SHA-256 del resto es resto_digest, descifrar el sobre con I_SOBRE y comprobar que el SHA-256 del .dkc es capsule_digest antes de usarlo, y SHOULD guardar una copia;
  • MUST tratar como inutilizable un localizador cuya ronda o cuya cadena no son las de compact_datekey.

resto_digest y capsule_digest protegen frente a quien guarda el resto o el huésped, no frente a quien escribió la .dkk.

Como toda extensión de la .dkk, es informativa: nada la ata a la cápsula (§44). Un lector:

  • MUST emparejar la .dkk con su cápsula por capsule_id y, cuando lo tiene, por capsule_digest, nunca por la nota ni por la DateKey de la extensión;
  • MUST mostrar la nota de la cápsula si sus bytes difieren de los de la nota de la llave, y SHOULD avisar de la diferencia.

Un escritor:

  • MUST guardar fuera solo el resto del sobre, nunca el .dkc ni la cabecera del sobre, y conocer las direcciones antes de escribir la .dkk: primero guarda el resto, solo o en su huésped, también en IPFS (§62.1, regla 24);
  • con un huésped, SHOULD añadir el resto al final, y MUST comprobar antes de escribir la .dkk que el fichero que guarda tiene el resto en ese desplazamiento;
  • SHOULD avisar de que con un localizador la .dkk basta para encontrar la cápsula y abrirla en la fecha; de que quien solo tiene la llave no sabe dónde está la cápsula hasta la fecha; de que una red social o una aplicación de mensajería estropean un huésped; y de que una dirección que deja de existir pierde la cápsula para quien no tenga otra copia.

45. Release API

La Release API entrega el release de una condición, sin capsule_id. Su respuesta MUST ser el objeto release de §47.1, el mismo que guarda una Release Cache (§47). Quien la usa es una fuente de red (§49): verifica cada respuesta con las reglas del paso 10 de §63 y descarta la que no las cumple, como con un relay de drand.

Informativo: la forma HTTP de la petición no es normativa. La recomendada se indexa por condición y responde con el objeto, con el tipo application/cbor:

GET /v1/releases/{profile}/{condition}

Ejemplo Quicknet:

GET /v1/releases/datekeys:quicknet:v1/66884212

Hoy ningún servicio de DateKeys la ofrece: las implementaciones de referencia piden el release a los relays de drand (§49).


46. Release Queue

La unidad de trabajo es:

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:

profile
condition
release_material
verified
verified_at

release_material es el objeto release de §47.1.

El servidor MUST verificarlo antes de marcarlo como válido.

El SDK MUST verificarlo de nuevo.


47.1 Objeto release

El objeto release es el release de una ronda como dato: la respuesta de la Release API (§45), una entrada de una Release Cache (§47) y un release que el llamador da desde un fichero o desde un archivo de releases (§49, §50). El protocolo no le da extensión de fichero propia (§20): lo identifica su type tag, la clave 0 de su objeto. Es Deterministic CBOR con el perfil de §58, sin trama:

0 → "datekeys-release"
1 → 1
2 → chain_hash (32 bytes)
3 → round (entero sin signo, de 1 a 2⁵³ − 1)
4 → signature (cadena de bytes, de 1 a 96 bytes; 48 en Quicknet)

Las cinco claves son obligatorias. La cadena se identifica por su chain_hash, el identificador de drand: el mismo del stanza tlock (§63, paso 8) y de cualquier archivo de releases ajeno a DateKeys. El objeto no lleva el profile_hash, que nadie fuera de DateKeys conoce, ni el profile_id (§24).

El objeto de la ronda 1000 de Quicknet mide 111 bytes (testdata/releases/1000.cbor):

a5
00 70 "datekeys-release"
01 01
02 58 20 <chain_hash, 32 bytes>
03 19 03e8
04 58 30 <firma, 48 bytes>

Tamaño. El objeto no tiene trama propia: lo acota el fichero o la entrada que lo contiene. Un lector MUST rechazar, antes de decodificarla, una entrada vacía o de más de 1024 bytes, con ERR_NON_CANONICAL_CBOR. Ninguna codificación válida mide más de 165 bytes, así que el límite no excluye ningún objeto válido; existe para que un lector no lea un fichero arbitrario entero y para que el código no dependa de cuánto lee.

Validación. El objeto se valida con las capas de §69.1, en el paso 10 de §63: su tamaño; su tipo y su versión (otro type tag → ERR_NON_CANONICAL_CBOR; una versión distinta de 1 → ERR_UNSUPPORTED_VERSION); y el perfil de §58 con el schema de arriba y de datekeys.cddl → ERR_NON_CANONICAL_CBOR. No tiene campos con código propio en la capa 4: su chain_hash, su ronda y su firma se comparan con el perfil pinneado y con la DateKey en el paso 10, en ese orden, con ERR_PROFILE_MISMATCH, ERR_ROUND_MISMATCH y ERR_RELEASE_INVALID. Una firma de una longitud distinta de la del grupo de firmas del scheme, 48 bytes en Quicknet, cumple el schema y es ERR_RELEASE_INVALID.

Un lector MAY decodificar el objeto al recibirlo, antes del paso 1, pero MUST informar de sus errores solo en el paso 10, como los de la .dkk en el paso 9.a (§63).

Confianza. El objeto no se firma ni dice si se verificó: un campo así no tendría valor (§51). Para una ronda y una clave pública solo hay una firma BLS válida, así que cualquier copia que verifique en el paso 10 es el release, venga de donde venga.

El JSON de drand como entrada. Un lector SHOULD aceptar además, como entrada del llamador, el JSON de un relay de drand, {"round": …, "signature": "…"}. Una entrada cuyo primer byte que no es un espacio de JSON (U+0020, U+0009, U+000A o U+000D) es { se lee como ese JSON; cualquier otra, como objeto release. No es un formato de DateKeys:

  • la entrada es un JSON de RFC 8259, en UTF-8, cuyo valor es un objeto. Ningún objeto del JSON repite un nombre, y los nombres se comparan exactos, punto de código a punto de código, tras decodificar sus escapes: "\u0072ound" es round, y Round es otro nombre. Un escape de un sustituto (U+D800 a U+DFFF) que no forma un par con el siguiente hace el JSON mal formado;
  • round es un número sin signo, sin fracción ni exponente, de 1 a 2⁵³ − 1, y signature, una cadena con la firma en hexadecimal, en minúsculas o en mayúsculas; randomness, si existe, MUST ser una cadena con el SHA-256 de la firma en hexadecimal, y los demás campos se ignoran;
  • no nombra su cadena, así que el paso 10 no compara ningún chain_hash;
  • una entrada de más de 8192 bytes; un JSON mal formado, que no es un objeto o que repite un nombre; sin round o sin signature; un round escrito de otra forma o fuera de ese rango, como 1000.0, 1e3, -1000 o 0; otro tipo en ellos, también null; una firma que no es hexadecimal; o un randomness que no es una cadena o no corresponde → ERR_RELEASE_INVALID, en el paso 10, antes que la ronda.

Las reglas son estrictas porque un lector de JSON corriente se queda con el último de dos nombres repetidos, o no distingue mayúsculas en los nombres: dos lectores verían rondas distintas en la misma entrada.

Una Release Cache (§47) y la Release API (§45) MUST guardar y servir el release como objeto release, nunca como ese JSON.

Vectores: testdata/vectors/release.json, con objetos válidos e inválidos, el JSON de drand y su resultado en el paso 10, y testdata/releases/<ronda>.cbor, el objeto de cada ronda publicada de los fixtures.


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:

.dkc
+
Provider Profile pinneado
+
release obtenido de un relay drand, de un servicio de caché o de un archivo
+
.dkk si la política la exige

debe ser suficiente para ejecutar el flujo de apertura.

Un release llega de una de estas dos clases de fuente, y el paso 9 de §63 las trata de forma distinta:

  • fuente de red: un relay de drand, la Release API, un servicio de caché o un archivo de releases remoto (§50). Cada petición es observable y revela la ronda a quien la sirve. La fuente verifica cada respuesta y descarta la que no cumple el paso 10, y no se le pide nada antes de round_time (paso 9.c);
  • release en la mano: un release que el llamador suministra sin red: un objeto release (§47.1) o el JSON de drand en un fichero, o la entrada de un archivo de releases local (§50). No se compara con el reloj (paso 9.c), y un release que no cumple el paso 10 da los códigos de ese paso.

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;
  • un archivo de releases con todas las rondas de la cadena, local o remoto (abajo);
  • un servicio de caché, de DateKeys o de otros, que guarde de forma continua los releases de todas las rondas y los sirva; 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. La misma vale para el .dkc, que el protocolo no guarda (§6), y para el resto de un sobre guardado fuera (§44.1): una cápsula a décadas necesita que alguien conserve las tres cosas.

El release es la única de ellas que no existe al sellar: se publica en round_time, así que quien crea la cápsula no puede guardarlo con ella. Y llegada la fecha, la cápsula ya se puede abrir: un release guardado entonces junto a ella solo sirve para volver a abrirla. El caso que importa es otro: quien abre la cápsula décadas después de su fecha, cuando drand quizá ya no exista y nadie guardó nada para ella. Por eso la recuperación a largo plazo descansa en archivos de releases y en servicios de caché que guardan, de forma continua, los releases de todas las rondas de la cadena, y no en el release de una cápsula concreta: guardar solo las rondas de cápsulas conocidas revelaría sus fechas y no se recomienda.

Quien lee pide la ronda de su DateKey y verifica la firma con la clave pública del perfil pinneado, en el paso 10 de §63. No necesita confiar en quien la sirve: para una ronda solo hay una firma válida (§47.1). Un servicio de caché que se consulta por red es una fuente de red y conoce la ronda que se le pide (§49); un archivo leído en local, no. Un servicio de caché de DateKeys responde con el objeto release, como la Release API (§45); uno ajeno puede servir el JSON de drand, que un lector también acepta (§47.1). Este documento no promete que exista un archivo publicado ni un servicio de caché, ni dice dónde se alojan.

Archivo de releases (informativo). Un archivo guarda las firmas de un tramo de rondas consecutivas de una cadena. Su formato no es normativo ni tiene códigos de error propios: una cabecera en Deterministic CBOR con el perfil de §58,

{0: "datekeys-release-archive", 1: 1, 2: chain_hash (32 bytes),
 3: primera ronda, 4: número de rondas}

seguida de las firmas, una tras otra, cada una de n bytes, la longitud de una firma de la cadena: 48 en Quicknet. La de la ronda r empieza en |cabecera| + (r − primera ronda)·n, y el fichero mide exactamente |cabecera| + número·n. Una ronda que falta se escribe con n ceros. La entrada de una ronda es el objeto release con el chain_hash de la cabecera, la ronda y esa firma, y se verifica en el paso 10 como cualquier otro objeto release. Un archivo de otra cadena, de otra longitud o que no tiene la ronda no entrega ningún release: ERR_RELEASE_UNAVAILABLE en el paso 9.

Leído en local, el archivo es un release en la mano (§49). Leído en remoto, por ejemplo con un rango de HTTP, es una fuente de red: revela la ronda a quien lo sirve. Quicknet publica una ronda cada 3 segundos, 10 512 000 al año: unos 505 MB de firmas al año, que no se comprimen, y unos 42 MB al mes. Un paquete por mes, publicado con su SHA-256, es un tamaño razonable. Vector: testdata/releases/archive_1000_1004.bin, de las rondas 1000 a 1004, con la 1002 y la 1003 a cero.


51. Verificación de release Quicknet

El SDK MUST comprobar:

expected Provider Profile
expected chain hash
expected round
valid BLS signature

En Quicknet, la firma BLS válida es la del paso 10 de §63: e(H(M), clave pública) == e(firma, G2), con M el SHA-256 de la ronda en 8 bytes big-endian y H el hash a G1 de RFC 9380 con el DST BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_ («Mensaje de ronda y hash a G1», tras el flujo de §63).

Al descifrar, el paso 10 de §63 fija el orden de estas comprobaciones y sus códigos: primero, si el release es un objeto release (§47.1), sus capas y su chain hash (ERR_PROFILE_MISMATCH); después la ronda (ERR_ROUND_MISMATCH), y por último la firma (ERR_RELEASE_INVALID). Son los códigos de un release en la mano (§49): 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:

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, ni lo son las firmas y los sellos que lleve la cápsula (§7.7);
  • el ciphertext puede permanecer disponible durante años;
  • la seguridad futura depende del provider y de la criptografía subyacente;
  • abrir la cápsula dentro de años exigirá el .dkc, el release de su ronda, que solo existe después de la fecha y habrá que obtener de drand, de un archivo de releases o de un servicio de caché, y en time_and_key la .dkk (§50, §62.1).

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:

0 → extension_id
1 → extension_version
2 → data (opcional)

El par:

(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:

critical

o:

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 la aportan una firma de security (§29.8 a §29.10) o una extensión de firma (§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 cubran una firma o un sello válidos de security, con el alcance de §29.8 a §29.12, o 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. security prueba solo lo que dicen sus veredictos (§29.7 a §29.12).
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 firma que los cubra, 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, incluida la nota pública (§24.1);
  • 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 32 KiB y pocos ficheros, n ≤ (P − 32835)/45, es decir, como mucho 44 ficheros con P = 34 816. El tamaño del área depende de la versión del escritor, no de lo que lleve security, salvo en un área ampliada (§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.

Firma y sello

  • Quien tiene el .dkc antes de la fecha no puede saber si lleva firma o sello mientras la cápsula use el área común de 32 KiB. Un área ampliada a 64 KiB lo delata, porque P crece y solo se amplía para que quepan firmas. Una cápsula de un escritor de la v0.10, con 512 bytes, delata que no lleva sello cuando P es pequeña.
  • Las firmas, sus certificados y los sellos van en el área, cifrados: los titulares y la autoridad de sellado solo se ven tras la fecha.
  • La aplicación de firma recibe AUTHOR_MESSAGE, que no revela el contenido (§29.8). En la web, ese mensaje y la firma quedan como ficheros en el dispositivo, y la firma lleva el certificado del firmante.
  • La autoridad de sellado recibe el hash de lo que sella y, como cualquier servicio, la dirección de quien pregunta y el instante.
  • El certificado de un firmante lleva su nombre y su NIF, y el de un representante, la entidad: tras la fecha los ve quien pueda abrir la cápsula, y en time_only, cualquiera. Un lector muestra el nombre sin el serialNumber (§29.7).
  • Pedir una respuesta OCSP, a menudo por HTTP sin cifrar, revela a la red el número de serie del certificado y a la autoridad el momento de la firma. El escritor SHOULD pedir el OCSP y el sello desde el dispositivo; si usa un intermediario, MUST declararlo, porque vería la dirección, el instante y el hash de cada cápsula firmada.
  • Exportar una firma a un validador externo le entrega el certificado de otra persona.
  • Esto cubre el .dkc, no los sellos ni las firmas que van fuera (§29.11), ni el tráfico hacia esos servicios.

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);
  • la extensión datekeys.capsule de una .dkk lleva en claro la nota y la fecha, y cifrado para la fecha el localizador. Lo que se guarda fuera es el resto de un sobre cifrado, sin cabecera, solo o dentro de otro fichero: quien lo aloja ve bytes al azar, su tamaño, que da la longitud del .dkc con unos bytes de más, y quién los sube o los baja, pero nada de lo que contiene (§44.1);
  • 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:

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);
  • una violación de las reglas de la data de una extensión registrada (note-data, capsule-data y capsule-locator de datekeys.cddl), ningún código: la comprueba solo quien conoce la extensión, que la trata como dice §54;
  • un objeto release (§47.1) vacío o de más de 1024 bytes → ERR_NON_CANONICAL_CBOR, antes de decodificarlo: el objeto no tiene trama, y ese límite es su capa 1 (§69.1);
  • 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:

{}
[]
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;
  • servir un cliente web desde builds reproducibles cuyos hashes se publican, con Subresource Integrity en cada script y una política de contenido que no cargue código de otro origen, y ofrecer el mismo cliente para usarlo sin conexión (§7.10).

60. Interfaces Go conceptuales

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.

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 + AREA_LEN + |HEAD_CBOR| + C, como mucho L_MAX, con
   AREA_LEN = 32768, o 65536 si se amplía (§62.1, regla 13).
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 sin firma ni sello (§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 = AREA_LEN || |SECURITY_CBOR| || |HEAD_CBOR| (uint32 BE)
            || SECURITY_CBOR || ceros hasta AREA_LEN 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:

FK_PAYLOAD
FK_TIME

Con firma o sello, entre los pasos 12 y 13: calcular AUTHOR_MESSAGE con CONTROL_SIG (§29.8), que no depende de L; obtener las firmas y los sellos y verificarlos (§62.1, regla 19); y construir SECURITY_CBOR con ellos. Si no caben en el área, ampliarla solo con el permiso expreso de quien crea la cápsula (regla 13) y escribir en CONTROL_CBOR la L del área ampliada: la firma no cambia. Después, decodificar de nuevo CONTROL_CBOR y SECURITY_CBOR definitivos con las reglas del lector (regla 17). Mientras espera las firmas, el escritor guarda I_PAYLOAD y el contenido solo en memoria (regla 25).


62. Flujo de cifrado time_and_key

Un orden válido para escribir una cápsula de formato 3, como en §61.

1. Validar las opciones (§62.1): entre 1 y 16 credenciales —los
   recipients X25519 dados y, si se piden, una clave portable y una llave de palabras (§38.1)—, 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). Si se pide una
   llave de palabras: derivar su identity con la DateKey resuelta (§38.1).
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:

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:

  1. Código por defecto. El SDK oficial SHOULD usar el código 2.

  2. 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.

  3. 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:

  1. Área. Escribir AREA_LEN = 32768 y SECURITY_CBOR en todas las cápsulas, lleve o no firma o sello, sin que dependa de lo que el escritor sepa hacer (§29.2). La única excepción es la ampliación expresa: si las firmas y sus evidencias no caben y quien crea la cápsula elige ampliar, AREA_LEN = 65536. MUST NOT ampliar por su cuenta ni descartar nada en silencio, y si no cabe en 65536, MUST rechazar la cápsula. El escritor decide el área después de hacer las firmas y los sellos, con lo que ocupan, y quien firmó no firma de nuevo por eso. Sin firma ni sello, 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

Con firma o sello, además, MUST:

  1. Verificación. Verificar cada firma y cada sello con las reglas del lector (§29.9 a §29.11) antes de escribir la cápsula, y no escribir una que daría F1, F2 o F5, ni S1, S2 o S3. Avisar de un sello sin accuracy, que no acreditará que se selló antes de la fecha (§29.11), y SHOULD ofrecer pedirlo a otra autoridad.
  2. Firmantes exigidos y lo que se firma. Entregar AUTHOR_MESSAGE como texto y mostrar su código (§29.8) antes de pedir cada firma. Con alg 2, cerrar SIGNERS antes de la primera firma con el certificado de cada firmante: seleccionado sin firmar, importado o tomado de una firma de prueba. Una firma de prueba es sobre TEST_MESSAGE = "datekeys:dkc3:test-signature:v1" || 0x0A || hex(32 bytes de un CSPRNG) || 0x0A, nunca sobre un AUTHOR_MESSAGE, y su tamaño solo es una estimación. El certificado se procesa en el dispositivo y no va a registros. Si un firmante firma con otro certificado, rehacer la lista y volver a firmar.
  3. Firma con certificado. Con alg 2, añadir a cada SignerInfo su sello CAdES-T. SHOULD guardar además lo que un validador necesitará años después, cuando quizá ya no pueda pedirlo (§29.10): la cadena de cada firmante sin su raíz, en certificates; la de la autoridad de cada sello sin su raíz, en el token o en certificates; y en crls, la respuesta OCSP del momento de la firma de cada certificado de esas cadenas que la tenga. Una aplicación de firma puede entregar una firma implícita, o con más certificados: el escritor MAY quitar su eContent, las raíces y los certificados ajenos a esas cadenas, lo que no invalida la firma. Si la autoridad de sellado falla, reintentar sin volver a firmar, y no dar por terminada una cápsula sin sellos. Antes de pedir la primera firma, SHOULD estimar si todo cabe en el área y, si no, preguntar si ampliarla o quitar firmantes. Si aun así no cabe todo, MUST decir qué deja fuera, qué certificados de qué cadena y qué respuestas OCSP, y SHOULD ofrecer exportarlo junto a la cápsula. Nunca deja fuera una firma, el certificado de un firmante ni su sello.
  4. Sello sin certificado. Sin firma o con alg 1, el sello es opcional: seal_type 2 sobre SEAL_SUBJECT, pedido después de la firma (§29.11). SHOULD guardar en el token la cadena de su autoridad sin la raíz y, si no cabe, decirlo, como en la regla 21.

Con nota pública, llave de palabras o extensión de cápsula, MUST:

  1. Nota pública. Escribir la extensión datekeys.note solo si la persona la pide, con un texto que cumpla las reglas del autor declarado de §29.6, y SHOULD avisar de que es pública y de que, con la fecha, puede identificar a alguien (§24.1).
  2. Llaves. Cumplir las reglas del escritor de §38.1 para una llave de palabras. Para una .dkk con localizador, crear el sobre, guardar fuera su resto, solo o en un huésped, y escribir después la .dkk con sus direcciones; nunca guardar fuera el .dkc ni la cabecera del sobre (§44.1).

Con firma o sello, además, MUST:

  1. Secretos en reposo. Mientras espera una firma o un sello, no guardar en disco I_PAYLOAD, CONTROL_CBOR ni el contenido: si la espera se interrumpe, se empieza de nuevo.

Para la recuperación a largo plazo (v0.15), el SDK oficial SHOULD:

  1. Aviso. Al sellar, decir que dentro de años harán falta el .dkc, en time_and_key la .dkk, y el release de su ronda, que solo existe después de la fecha: abrirla años después de esa fecha dependerá de que drand siga sirviendo la ronda o de que la conserve un archivo de releases o un servicio de caché (§50, §53). MAY programar un aviso local para round_time.
  2. Anexo. Guardar junto al .dkc el texto del anexo de recuperación (§79), igual para toda cápsula y sin ningún dato de ella: no revela nada que no revele ya el magic DKC1.

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):

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

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 release se pide a una fuente de red (§49) y el instante
      actual es anterior al round_time de la DateKey
      → ERR_RELEASE_UNAVAILABLE, sin pedir el release.
      Un release en la mano (§49) no se compara con el reloj: su
      firma prueba que la ronda se publicó. Si su round_time es
      posterior al instante actual, el lector MAY avisar de que su
      reloj puede ir atrasado. Con una fuente de red, la persona MAY
      pedir expresamente la petición antes de round_time, porque su
      reloj puede estar mal; el lector la hace entonces sin 9.c.
   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, caché o archivo remoto) 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, o si un release en la mano no se puede obtener
      → ERR_RELEASE_UNAVAILABLE.

10. Verificar el release localmente (§17, §51), en este orden:
       un release en la mano que es un objeto release (§47.1): sus
       capas (§69.1), su tamaño, su tipo y versión, y su codificación
       y schema
           → ERR_NON_CANONICAL_CBOR o ERR_UNSUPPORTED_VERSION;
       un release en la mano que es el JSON de drand y no se puede
       leer (§47.1)
           → ERR_RELEASE_INVALID;
       chain_hash del objeto release distinto del chain_hash del
       Provider Profile pinneado
           → ERR_PROFILE_MISMATCH;
       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; en Quicknet:
           M = SHA-256(uint64_be(ronda))
           e(H(M), clave_pública) == e(firma, G2)
       con H el hash a G1 de RFC 9380 con el DST de «Mensaje de ronda
       y hash a G1», tras este flujo
           → ERR_RELEASE_INVALID.
    Estos códigos se informan para un release en la mano (§49), 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| = 96, un punto de G2, y |V| = |W| = 16: 128 bytes. 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, G2 en
    Quicknet. H2, H3 y H4 usan SHA-256 con las etiquetas IBE-H2,
    IBE-H3 e IBE-H4:
        H2(x)        = 16 primeros bytes de SHA-256("IBE-H2" || x), con
                       x el elemento de GT serializado como fija
                       «Serialización de GT en H2»
        H4(sigma)    = 16 primeros bytes de SHA-256("IBE-H4" || sigma)
        H3(sigma, m) = el escalar que fija «H3 y H4»
    los dos párrafos, tras este flujo. Son las del paquete encrypt/ibe
    de drand/kyber, que tlock importa (§77). FK_TIME es la file key de
    OUTER_TIME_AGE (§35).
    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 firma de security (§29.8) o
    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.

Un release en la mano también es una entrada distinta. Una implementación MAY decodificarlo al recibirlo, pero MUST informar de sus errores solo en el paso 10, después de los pasos 1 a 9: una cápsula inválida o sin credenciales da su propio código aunque el release en la mano también sea inválido.

PAYLOAD_AGE comienza exactamente en:

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:

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.

Mensaje de ronda y hash a G1. Quicknet (bls-unchained-g1-rfc9380) no encadena sus rondas: la firma de la ronda n es una firma BLS del mensaje

M = SHA-256(uint64_be(n))

los 32 bytes del SHA-256 de la ronda en 8 bytes big-endian. Ni la firma de la ronda anterior ni otro dato de la cadena entran en él. La firma es un punto de G1, la clave pública uno de G2, y la firma verifica si y solo si

e(H(M), clave_pública) == e(firma, G2)

con e el pairing de G1 × G2 del paso 11, G2 el generador de G2 y H el hash_to_curve de RFC 9380 (§77) a G1, con la suite BLS12381G1_XMD:SHA-256_SSWU_RO_ y el DST de 43 bytes ASCII

BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_

Es decir: expand_message_xmd con SHA-256, hash_to_field a dos elementos de Fp con L = 64, el map SSWU simplificado a la curva 11-isógena, la isogenia y la multiplicación por h_eff = 0xd201000000010001. La firma y la clave pública se decodifican con §12.2 antes de la ecuación, y ninguna puede ser el punto en el infinito (§12.1, paso 10). Es la verificación de drand para ese scheme: la de NewPedersenBLSUnchainedG1 de drand, con el esquema BLS sobre G1 de drand/kyber (§77). El mismo H(M) es la identidad de la ronda n para la que tlock cifra (paso 11): sin él, e(firma, U) no da el elemento de GT con el que se cifró V. Vectores (testdata/vectors/tlock_steps.json): M, H(M) y la firma de cuatro rondas publicadas de Quicknet.

H3 y H4. Las etiquetas son los bytes ASCII de IBE-H2 (49 42 45 2d 48 32), IBE-H3 (49 42 45 2d 48 33) e IBE-H4 (49 42 45 2d 48 34), sin longitud ni terminador. sigma y FK_TIME miden 16 bytes. H4(sigma) son los 16 primeros bytes de SHA-256 de IBE-H4 seguido de sigma. H3(sigma, FK_TIME) es el escalar r de la comprobación r·G == U:

q    = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001
base = SHA-256("IBE-H3" || sigma || FK_TIME)
para i = 1, 2, …, 65534:
    d    = SHA-256(uint16_le(i) || base)      32 bytes
    d[0] = d[0] >> 1
    si int_be(d) < q: r = int_be(d) y fin
si ningún i lo cumple, la comprobación r·G == U falla

con q el orden de los grupos (§12.2), uint16_le(i) el contador en 2 bytes little-endian, que empieza en 1, e int_be(d) los 32 bytes de d como entero big-endian. d[0] >> 1 desplaza un bit a la derecha el primer byte de d, y solo ese byte: su bit menos significativo se pierde y los otros 31 bytes no cambian. No equivale a poner a cero el bit más significativo de d, que daría otro r y ninguna cápsula se abriría. Tras el desplazamiento, int_be(d) < 2²⁵⁵, y como q es algo más de 0,9 · 2²⁵⁵, un intento se rechaza con una probabilidad cercana a 0,094. Que se rechacen los 65534 no ocurre en la práctica, pero el descifrado lo trata como una comprobación fallida, ERR_INTEGRITY en el paso 11. r = 0 tampoco la cumple, porque U no es el punto en el infinito.

Es h3 del paquete encrypt/ibe de drand/kyber con la suite de tlock, drand/kyber-bls12381 (§77): su escalar es un entero módulo q de 32 bytes big-endian, y el desplazamiento es su máscara de 256 − 255 = 1 bit sobre el primer byte. Vectores (testdata/vectors/tlock_steps.json): para cinco stanzas sobre cuatro rondas publicadas de Quicknet, el cuerpo U ‖ V ‖ W, e(firma, U), H2, sigma, H4, FK_TIME, base, cada intento de H3 con d antes y después del desplazamiento, y r. Uno de ellos acepta su cuarto intento, donde poner a cero el bit más significativo aceptaría el segundo.


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:

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):

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:

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:

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. Cuatro casos más se abren, sin código, con su veredicto: security de versión 2 (X), una firma de alg 4294967295 (F1), una firma de alg 1 que no verifica (F2) y un sello de seal_type 4294967295 (S1).

Firma, sello, nota y llave de palabras (v0.11), además:

  • alg 1: la firma alterada, quitada, rehecha con otra clave o trasplantada a otra cápsula; una clave o una firma de otra longitud (F1); los casos de «Taming the many EdDSAs» (A de orden pequeño, A o R no canónicas, S ≥ ℓ, la ecuación con cofactor), con el veredicto de cada uno en ed25519_strict.json;
  • alg 2: un firmante exigido retirado (F5); el autor retirado y los demás intactos (F5); una firma retirada y SIGNERS cambiado para ocultarlo (F2); el CAdES-T de un firmante retirado (F5); un certificado fuera de validez en t (F5); un firmante ajeno (aparte, sin contar); message-digest de otro mensaje (F2); BER, o los signerInfos desordenados (F1); dos SignerInfo del mismo certificado (F1); un algoritmo fuera de la tabla (F5); la clave 3 con alg 2 (F5); un firmante cuyo sello no lleva accuracy (F6, y su línea no dice «antes de la fecha de apertura»);
  • el área ampliada de 32 KiB a 64 KiB después de firmar: la firma sigue valiendo y AUTHOR_MESSAGE no cambia;
  • seal_type 2: messageImprint de otro SEAL_SUBJECT (S3); un token sin el message-digest de su eContent (S2); t + precisión ≥ round_time (S5); un token sin accuracy sellado años antes de round_time (S5, «el sello no dice su precisión»), y otro de la política BTSP (S5, con su motivo); un accuracy de 0 segundos (S4); un token en BER (S2); un sello junto a una clave 2 de un alg desconocido, que se verifica igual;
  • alg y seal_type 4294967295: F1 y S1 en cualquier versión;
  • el mismo contenido con firma y sin ella, en el área común: la misma P;
  • la nota pública cambiada en PUBLIC_HEADER: ERR_HEADER_BINDING en el paso 15; con un salto de línea o un carácter que §29.6 prohíbe: inutilizable y sin mostrarse;
  • la llave de palabras: el vector de §38.1; textos con mayúsculas, acentos y espacios que dan las mismas palabras; y un texto con U+200B, que el escritor rechaza;
  • la extensión datekeys.capsule: un localizador para otra ronda (inutilizable); una dirección file: o http: (no se descarga); un resto cuyo SHA-256 no es resto_digest, porque alguien recomprimió el huésped o el desplazamiento no es el suyo (no se usa); un recurso que sigue tras el resto, del que se lee solo hasta el desplazamiento más resto_size.

Release y su fuente (v0.15), además:

  • release.json: el objeto release de las rondas 1000, 1001, 1004 y 2000; uno vacío, de 1025 bytes, también de versión 2, con un byte de más, con el type tag del Provider Profile o en mayúsculas, con las claves 0 y 1 en otro orden, de versión 2 o 0, sin firma, con una clave 5, con el chain_hash de 31 bytes o como texto, con la ronda 0, 2⁵³ o en cuatro bytes, con una firma vacía o de 97 bytes, y un mapa de longitud indefinida (ERR_NON_CANONICAL_CBOR o ERR_UNSUPPORTED_VERSION); uno de otra cadena, también de otra ronda (ERR_PROFILE_MISMATCH); uno de otra ronda (ERR_ROUND_MISMATCH); y la firma de otra ronda, de 47 o de 96 bytes, a cero, en el infinito, con el bit de infinito y carga, negada y con x + p (ERR_RELEASE_INVALID); el JSON de drand, válido, con randomness, con espacios y en mayúsculas, y mal formado de siete formas; y con round repetido, también como "\u0072ound" o con null, con Round en lugar de round, con round 1000.0, 1e3, -1000, 0 o 2⁵³, con signature repetido, con un nombre ajeno repetido, también dentro de un objeto anidado, y con un sustituto suelto (ERR_RELEASE_INVALID, v0.16); y las rondas que un archivo local tiene, le faltan o no cubre;
  • mutations.json, cuyos casos dicen su fuente (source): un release en la mano y el reloj 1 ns antes de round_time, que abre; lo mismo con una fuente de red (ERR_RELEASE_UNAVAILABLE, paso 9, sin petición); un release de otra ronda de una fuente de red (ERR_RELEASE_UNAVAILABLE, paso 9), que en la mano da ERR_ROUND_MISMATCH; y un objeto release de otra cadena (ERR_PROFILE_MISMATCH, paso 10), también de otra ronda y con el reloj atrasado;
  • el anexo de recuperación (§79): un fixture time_only y uno time_and_key de formato 3 abiertos solo con lo que dice el anexo, sin código de DateKeys; y desde la v0.16, uno abierto con una llave de palabras, escritas con mayúsculas, tildes, marcas sueltas y varios espacios, y uno cuyo PAYLOAD_AGE acaba en un bloque completo.

Raíz de confianza (v0.14), además:

  • tlock_steps.json: M, H(M) y la ecuación del paso 10 con las firmas publicadas de las rondas 1000, 1001, 1004 y 2000; el descifrado del paso 11 de cinco stanzas, con e(firma, U), H2, sigma, H4, FK_TIME, cada intento de H3 y r, uno de ellos aceptado en su cuarto intento, donde poner a cero el bit más significativo aceptaría el segundo; y la misma firma, que no verifica con el DST de G2 ni con la ronda sin SHA-256 como mensaje.

Localizador (v0.13), además:

  • la IP a la que resuelve un nombre: una de 64:ff9b::/96 con una IPv4 pública dentro, que se usa, y con una IPv4 de cada bloque que no es público, que no; una de NAT64 de otro prefijo, que no se usa sin el prefijo de la red; y con el prefijo de la red, una de cada longitud de RFC 6052, un prefijo de otra longitud o fuera de 64:ff9b::/16 y de las IPv6 públicas, que no vale, y una IPv4 que no es pública dentro de él.

Firma, sello, nota y localizador (v0.12), además:

  • alg 2: el certificado de un firmante fuera de validez con una TSA válida (F5, «fuera de validez»); SIGNERS desordenado, vacío, de 17 entradas o con un elemento de 31 bytes, junto a una firma CMS válida (F1); la versión de un SignerInfo que no casa con su sid (F1); dos atributos content-type (F1); un signing-certificate junto al v2 (F6); un ESSCertIDv2 con SHA-256 escrito (F6), con SHA-1 o con el hash de otro certificado (F1); una firma RSASSA-PSS (F6), y con trailerField escrito (F5); un atributo desconocido con un arco de 2³¹ o más (no decide nada); un certificado dos veces en certificates (F6); una clave RSA con un algoritmo ECDSA (F2); una clave EC comprimida (F5); un titular con dos espacios seguidos, con más de 64 puntos de código, con ESC, U+202E o una marca de orden de bytes (su SHA-256); un titular con givenName, surname y un commonName con su NIF (el nombre, sin el NIF); un emisor sin texto (el SHA-256 de su nombre);
  • seal_type 2: una accuracy negativa o con un INTEGER no mínimo (S2); un genTime sin Z o con un cero final (S2); ordering FALSE escrito (S2); un campo tras el último (S2); un messageImprint de otra longitud (S3); t + precisión igual a round_time (S5); el certificado de la TSA dos veces (S4);
  • la nota pública: su codificación, y una nota con un salto de línea o con un sustituto suelto, que el escritor rechaza;
  • datekeys.capsule: una dirección de cada bloque que no es público, los nombres locales, también en mayúsculas, los caracteres fuera de RFC 3986, los segmentos «..», un segmento de 64 caracteres o con un guion en un extremo, el esquema en mayúsculas, una IPv4 y un puerto con ceros a la izquierda y un CID con los bits sobrantes distintos de cero; un localizador con una dirección rechazada y otra válida, que se usa; y las bases 3837, 4070, 4094 y 4095 del relleno y las del múltiplo siguiente.

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:

requested timestamp
expected round
effective timestamp

66. Test vectors dk1_

Cada vector:

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 4294967295, reservado para pruebas (§29.3), con una clave de 32 bytes y una firma de 64, aleatorias, con el veredicto F1;
  • esa firma y un sello de seal_type 4294967295 con un token aleatorio, con F1 y S1;
  • el área de 32 KiB sin firma;
  • una firma de alg 1 (F4, o F3 con su clave guardada);
  • esa firma con un sello de seal_type 2 (S4), y un fichero cuya mtime es posterior al sello, que se muestra como incoherencia;
  • una firma de alg 2 de dos firmantes, con ECDSA P-256 y con RSA de 2048 bits, cada uno con su sello CAdES-T (F6);
  • desde la v0.16, una cápsula time_and_key cuya única credencial es una llave de palabras, la del segundo vector de 79.7, y una cuyo PAYLOAD_AGE acaba en un bloque completo de 65 536 bytes de texto (79.5).

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, format3_seal_unsupported.dkc, format3_unsigned.dkc, format3_signed.dkc, format3_sealed.dkc, format3_signed_cms.dkc, format3_time_and_key_words.dkc, con el texto de sus palabras en su registro, y format3_full_chunk.dkc. Los vectores de rutas y del head van en testdata/vectors/paths.json, head_schema.json y path_fold.json; los de security, en security.json y security_cms.json; los de Ed25519 estricto, en ed25519_strict.json; los de la nota, en note.json; y los de datekeys.capsule, en locator.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

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, el objeto release (§47.1) 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. El objeto release tampoco: su capa 1 es su tamaño, de 1 a 1024 bytes, y fuera de él es ERR_NON_CANONICAL_CBOR (§47.1). 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, SECURITY_CBOR y el objeto release 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;
    • el objeto release, en el paso 10: ninguno; su chain_hash, su ronda y su firma relacionan el objeto con el perfil y con la DateKey, y pertenecen al paso 10.

    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), el chain_hash, la ronda y la firma de un release, en ese orden (paso 10), 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 en la mano (§49), 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, en la mano ERR_ROUND_MISMATCH, paso 10
El mismo release, única respuesta de un relay ERR_RELEASE_UNAVAILABLE, paso 9
Release válido en la mano y el reloj antes de round_time se abre: el paso 9.c no se aplica
El mismo release de una fuente de red, con ese reloj ERR_RELEASE_UNAVAILABLE, paso 9, sin petición
Objeto release de otra cadena y de otra ronda, en la mano ERR_PROFILE_MISMATCH, paso 10
Objeto release de versión 2 y de 1025 bytes ERR_NON_CANONICAL_CBOR, paso 10
Objeto release de versión 2 con una clave desconocida y otra cadena ERR_UNSUPPORTED_VERSION, paso 10
Cápsula time_and_key sin credenciales y un objeto release vacío en la mano ERR_ACCESS_REQUIRED, 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.

Un lector de la v0.10 abre las cápsulas de un escritor de la v0.11: acepta el área de 32 KiB y la de 64 KiB (§29.2), da F1 a alg 1 y 2 y S1 a seal_type 2 (§29.7), e ignora la nota pública y la extensión datekeys.capsule (§54). La llave de palabras es un recipient X25519 más, así que la identity que se deriva abre la cápsula en cualquier lector (§38.1). Un lector de la v0.11 abre las cápsulas de la v0.10, con su área de 512 bytes.

La v0.16 no cambia ningún formato. Cambian dos veredictos: un sello válido sin accuracy da S5 donde la v0.15 daba S4, y en F6 la línea de su firmante deja de decir «antes de la fecha de apertura» (§29.7, §29.11); y el JSON de drand con un nombre repetido, con Round por round o con la ronda 0 da ERR_RELEASE_INVALID, donde un lector de la v0.15 podía leer otra ronda o abrir la cápsula (§47.1). Un objeto release no cambia.

La v0.15 no cambia ningún formato de .dkc ni de .dkk: añade el objeto release (§47.1), que sirve a toda cápsula de los tres formatos, también a las escritas antes. Cambia un veredicto: un release válido en la mano abre la cápsula aunque el reloj del lector sea anterior a round_time, donde la v0.14 daba ERR_RELEASE_UNAVAILABLE en el paso 9 (§63, paso 9.c). Con una fuente de red nada cambia. Un lector de la v0.14 no lee un objeto release, pero sí el JSON de drand de la misma ronda; un objeto release cuyo chain_hash no es el del perfil pinneado da ERR_PROFILE_MISMATCH en el paso 10.

La v0.14 no cambia ningún formato ni ningún veredicto de una cápsula de Quicknet. Un Provider Profile de un scheme de drand distinto de bls-unchained-g1-rfc9380 deja de pasar §12.1, con ERR_UNKNOWN_PROFILE: V1 no pinnea ninguno.

La v0.13 no cambia ningún formato ni ningún veredicto: solo deja a un lector descargar el resto de un sobre desde una red solo IPv6 con NAT64 (§44.1).

La v0.12 no cambia ningún formato: un lector de la v0.11 abre las cápsulas de la v0.12, y uno de la v0.12, las de la v0.11. Cambian algunos textos de los veredictos (§29.7) y cómo se leen un certificado y un token (§29.10, §29.11), y con ellos los veredictos de algunas firmas y algunos sellos cuyo caso la v0.11 no fijaba.


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.

El estado es uno de estos:

  • activo: se escriben y se abren cápsulas con él;
  • solo lectura: no se escriben cápsulas nuevas con él, porque el proveedor anuncia su fin o se sospecha de él, pero se siguen abriendo las que existen;
  • comprometido: hay pruebas de que su confidencialidad falló; se siguen abriendo las cápsulas que existen, y el SDK oficial SHOULD avisar de que su contenido pudo leerse antes de la fecha.

Una entrada publicada no se retira del registro: un perfil cambia de estado, y un lector sigue pudiendo abrir las cápsulas escritas con él.


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 firma de security (§29.8) o una extensión de firma pueden aportar autenticidad (§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.

Extensiones registradas

extension_id Versión Objeto y array data Longitud máxima Definición
datekeys.note 1 PUBLIC_HEADER, no crítico texto UTF-8 que cumple §29.6, sin CBOR 1024 bytes §24.1
datekeys.capsule 1 .dkk, no crítico mapa CBOR de §44.1 la de la trama de la .dkk (§57) §44.1

Las dos son informativas: ninguna decisión de seguridad se basa en ellas (§55.1).


73. Decisiones canónicas v0.16

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 firma de security o una
  extensión de firma; quién emite un certificado o un sello lo
  comprueba un validador oficial

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 y no se le pide nada
  antes de round_time; un release en la mano (objeto release, JSON de
  drand, archivo local) no se compara con el reloj; 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 en la mano

release object
= Deterministic CBOR sin trama, de 1 a 1024 bytes,
  {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round,
  4: signature}, sin extensión de fichero; la cadena por su
  chain_hash; el JSON de drand, solo como entrada y estricto: sin
  nombres repetidos, nombres exactos y la ronda un entero; en el paso
  10, capas, chain_hash, ronda y firma

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, 32 KiB en la v0.11, o 64 KiB
  si se amplía expresamente, y 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; alg 1 (Ed25519 estricto) y 2 (CMS con certificados
  y un CAdES-T por firmante), seal_type 2 (RFC 3161); veredictos X, F0
  a F6 y S0 a S5; S4 solo con accuracy

author signature
= AUTHOR_MESSAGE, un texto de 99 bytes con el hash de control_commit,
  sin I_PAYLOAD ni L, de head_digest y de signers_digest, y un código de
  8 caracteres; nunca cubre el área; los firmantes exigidos de alg 2 en
  su clave 1; DateKeys no comprueba quién emite los certificados ni los
  sellos

word key
= recipient X25519 derivado de 6 o más palabras normalizadas, con
  PBKDF2-HMAC-SHA256 de 600 000 iteraciones y la cadena, la ronda y
  capsule_id como sal

public note
= datekeys.note en PUBLIC_HEADER, pública y atada en el paso 15; copia
  informativa en datekeys.capsule de la .dkk, con la fecha y un
  localizador cifrado para la fecha, que apunta al resto de un sobre
  opaco, solo o dentro de otro fichero

recovery
= puede obtener release directamente del provider; a largo plazo,
  archivos de releases de todas las rondas y servicios de caché que
  los sirven, sin promesa de alojamiento; el formato del archivo es
  informativo; anexo informativo para abrir sin software de
  DateKeys (§79)

historical release availability
= dependencia explícita del horizonte de recuperación; el SDK avisa
  al sellar y guarda el anexo junto al .dkc (§62.1)

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.

En la v0.16 son además provisionales:

  • los 32 KiB del área, hasta medir firmas CAdES de varios países, con y sin cadena, sellos de dos autoridades cualificadas y respuestas OCSP. Se confirman si un firmante completo cabe con un 25 % de margen; si dos firmantes completos no caben, usan la ampliación expresa; solo se bajan a 16 KiB si dos firmantes completos miden menos de 12 KiB;
  • la tabla de algoritmos y el perfil del certificado de §29.10, hasta probarlos con firmas y sellos reales de varios países.

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.16, como trabajo futuro que esta versión no especifica:

  • un archivo de releases o un servicio de caché del proyecto, y su alojamiento (§50);
  • el sello del servicio de DateKeys (seal_type 1) y OpenTimestamps dentro de la cápsula (seal_type 3);
  • listas generales de firmas y de sellos, y roles y umbrales de firmantes;
  • borradores cifrados en disco, para que un organismo firme días después, y anexos cifrados para la misma ronda;
  • la verificación por un tercero sin I_PAYLOAD;
  • una tabla de políticas de sellado con su precisión, para acreditar con S4 un sello sin accuracy (§29.11);
  • una lista de autoridades que el lector reconozca, si algún día se quiere que la propia aplicación compruebe quién emitió un certificado o un sello;
  • las curvas brainpool y los algoritmos nacionales, como GOST o SM2;
  • los ficheros guardados fuera de la cápsula, con sus direcciones en el head;
  • 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.
  13. el tamaño del área medido con firmas, sellos y respuestas OCSP reales (§29.2, §74).

76. Política de cambios

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:

  1. 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.
  2. 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.
  3. 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).

Cambios normativos de la v0.11

La v0.11 define la firma de autor y el sello de tiempo del área security, que la v0.10 reservó, y añade la llave de palabras, la nota pública y la extensión de cápsula de la .dkk. No cambia la trama, el head, el mapa exterior de security ni ningún formato: un lector de la v0.10 abre sus cápsulas (§70). El diseño de la firma y del sello pasó la revisión cruzada de Fable y Astra el 1-10-2026, comprobada después contra la v0.10 y sus implementaciones.

  1. Área de 32 KiB (§29.2, §55.2, §62.1 regla 13).
    • Cambio: un escritor escribe AREA_LEN = 32768, firme o no, y 65536 solo si se amplía expresamente.
    • Motivo: una firma con certificado, su sello y su respuesta OCSP no caben en 512 bytes, y un área que creciera con ellos revelaría en P si la cápsula va firmada, y de qué tipo, a quien conoce su contenido o compara cápsulas del mismo autor.
    • Caso: con un head de 100 bytes, una carta de 2 000 bytes da P = 2 816 sin firma, y P = 8 704 con una firma que ocupe un área ajustada de 6 144 bytes, la misma P que una carta de 8 000 bytes sin firma: quien sabe qué carta va dentro deduce la firma. Con el área de 32 KiB, P = 36 864 en los dos casos.
    • Pruebas previstas: la misma P con firma y sin ella (§64).
  2. Qué se firma (§29.8).
    • Cambio: AUTHOR_MESSAGE, un texto de 99 bytes con el hash de control_commit de CONTROL_SIG, sin I_PAYLOAD ni L, de head_digest y de signers_digest, y un código de 8 caracteres para compararlo antes de firmar.
    • Motivo: con L dentro, el área tendría que fijarse antes de firmar, y una firma que no cupiera obligaría a firmar de nuevo; sin L, el área se amplía después sin un segundo PIN. L ocupa siempre 8 bytes (§31), así que header_binding no depende del área. En texto, quien firma puede leer lo que firma.
    • Caso: el diseño de la entrega 2 firmaba control_commit con L. Una firma CAdES no tiene tamaño conocido hasta hacerla, así que el escritor no podría conocer L antes de pedir el PIN.
    • Pruebas previstas: el área ampliada después de firmar, con la misma AUTHOR_MESSAGE (§64).
  3. Firma con clave propia, alg 1 (§29.9, §29.12).
    • Cambio: Ed25519 estricto y claves dkauthor1….
    • Motivo: la firma de autor que reservó la v0.10.
    • Caso: Go 1.26 ed25519.Verify acepta A = 01 00…00 con R igual a la identidad y S = 0, y verify de noble 2.4 usa siempre la ecuación con cofactor: sin un perfil estricto, las dos implementaciones darían veredictos distintos.
    • Pruebas previstas: ed25519_strict.json (§64).
  4. Firma con certificado, alg 2 (§29.10, §62.1 reglas 19 a 21).
    • Cambio: CMS separada con uno o varios firmantes, la lista de firmantes exigidos en la clave 1 y un CAdES-T por firmante.
    • Motivo: el autor quiere que se sepa quién firma, con un certificado de su país, como los de la FNMT o el DNIe en España, y la aplicación de firma de cada plataforma.
    • Alcance: DateKeys comprueba la criptografía y guarda las pruebas; quién emitió los certificados y los sellos lo comprueba un validador oficial. El autor descartó una lista de autoridades en el lector: nunca cubriría todos los países y no añade valor legal.
    • Caso: la lista va en la clave 1 de author-signature y no en una clave nueva del mapa exterior, porque un lector de la v0.10 da X a una clave desconocida de ese mapa (security.json) y F1 a un alg desconocido.
    • Pruebas previstas: las mutaciones de alg 2 de §64.
  5. Sello RFC 3161, seal_type 2 (§29.11).
    • Cambio: un sello opcional sobre SEAL_SUBJECT sin firma o con alg 1; con alg 2, el sello va dentro de la firma.
    • Motivo: el autor quiere sellos de una autoridad cualificada, en lugar de un servicio propio.
    • Caso: sin sello, quien abre una cápsula puede rehacer su área tras la fecha, así que una firma no prueba que existiera antes (§7.9).
    • Pruebas previstas: las de seal_type 2 de §64.
  6. Llave de palabras (§38.1).
    • Cambio: una credencial X25519 derivada de 6 o más palabras, con la cadena, la ronda y capsule_id como sal.
    • Motivo: pocas personas saben guardar una clave, y una .dkk se pierde.
    • Caso: la página /create de datekeys-ts y la CLI de Go la implementan, y la CLI abre una cápsula hecha en la página. Su primera versión no llevaba capsule_id en la sal: las mismas palabras daban la misma llave en todas las cápsulas de una ronda, y un diccionario las atacaba a la vez (revisión del borrador).
    • Pruebas previstas: el vector y la normalización (§64).
  7. Nota pública y extensión de cápsula (§24.1, §43, §44.1, §72).
    • Cambio: datekeys.note en PUBLIC_HEADER, y datekeys.capsule en la .dkk, con la nota, la fecha y un localizador cifrado para la fecha que apunta al resto de un sobre opaco, sin cabecera, solo o dentro de otro fichero.
    • Motivo: una aplicación que lista cápsulas y llaves solo puede distinguirlas hoy por capsule_id, y una llave suelta no dice de qué cápsula es ni dónde está.
    • Caso: una .dkk de la v0.10 lleva capsule_id y capsule_digest, nada legible para una persona. Y en IPFS un .dkc es público: un rastreador indexaría capsule_id, la nota y la fecha, y quien tiene la .dkk encontraría la cápsula antes de la fecha; por eso se guarda un sobre.
    • Pruebas previstas: la nota cambiada y la nota inválida, y los casos de datekeys.capsule (§64).
  8. Aclaraciones de la revisión adversarial (§29.7, §29.10, §29.11, §44.1, §62.1 regla 13).
    • Cambio: la forma de la firma CMS y del TSTInfo queda fijada campo a campo (versión del SignerInfo según su sid, atributos contados por atributo, signing-certificate junto al v2, ESSCertIDv2 con SHA-256 explícito, parámetros de PSS, accuracy sin negativos, genTime en UTC con Z); el emisor de un certificado se muestra con las reglas del titular; las direcciones del localizador se leen sin decodificar y se rechazan los hosts que no son letras, dígitos y guiones y las IP que no son públicas; el relleno del localizador pasa al múltiplo siguiente cuando ninguna longitud encaja; y el área se decide después de firmar.
    • Motivo: tres revisiones independientes, del lector CMS, de la lógica de firma y del escritor y del localizador, encontraron estas ambigüedades y fallos, todos con un caso que los reproduce.
    • Caso: un sello con una accuracy negativa daba S4 a una cápsula sellada después de la fecha; un certificado con un nombre de emisor que lleva ESC y U+202E ponía líneas falsas en los veredictos; https://%D0%B0pple.com/ se mostraba como un host con una «а» cirílica.
    • Pruebas previstas: security_cms.json y locator.json, y los fixtures format3_signed_cms y format3_sealed (§64, §67).

Con un lector de la v0.11 cambian de veredicto cinco vectores de la v0.10 que usaban alg 1 y seal_type 2 como no soportados: en security.json, «a signature of alg 1» y «a signature and a seal» pasan de F1 a F2, y «a seal of seal_type 2», de S1 a S2; y los fixtures format3_signature_unsupported y format3_seal_unsupported, de F1 a F2. Los casos no soportados se rehacen con alg y seal_type 4294967295 (§29.3), y los actuales quedan como casos de F2 y S2. Ningún otro objeto cambia de veredicto ni de código. Los vectores de la firma, del sello y de la extensión de cápsula, y los registros de control_commit, signers_digest, AUTHOR_MESSAGE y SEAL_SUBJECT de format3_signed, format3_sealed y format3_signed_cms, están en testdata (§67). En la v0.11, security.json seguía dando los veredictos de un lector de la v0.10 y format3_seal_unsupported usaba seal_type 1: los corrige la v0.12 (cambio 9).

Cambios normativos de la v0.12

La v0.12 corrige lo que encontró la revisión de la implementación de la v0.11, el 2 de octubre de 2026: huecos del texto, lecturas que dejaba a cada implementación y erratas. No cambia ningún formato (§70).

  1. Los nombres de un certificado en los veredictos (§29.7).
    • Cambio: cada nombre de un certificado va entre « y »; se muestra si cumple §29.6, tiene como mucho 64 puntos de código y no lleva dos espacios seguidos, y si no, se muestra su SHA-256; en una salida de texto, las líneas de los veredictos se parten como el texto del creador, con ↳ delante de cada fila después de la primera.
    • Motivo: el nombre de un certificado lo escribe quien lo hace, y DateKeys no comprueba quién lo emitió. Con espacios, un nombre colocaba un texto suyo donde el terminal partía la línea.
    • Caso: el certificado de una TSA con el commonName «TSA», 50 espacios y «Firmado con la clave que guardaste como Banco» mostraba, en un terminal de 80 columnas, una fila igual que la de F3.
    • Pruebas previstas: security_cms.json (§64).
  2. El sello en las líneas de F6 (§29.7).
    • Cambio: la línea de cada firmante nombra la autoridad de su sello, y si alguna dice «antes de la fecha de apertura», la sigue la línea «DateKeys no comprueba quién emitió los sellos.»; el resultado de un firmante ajeno se escribe en español.
    • Motivo: §29.7 exige ese aviso siempre que se dice que una cápsula se firmó antes de la fecha, y su tabla no lo daba con F6.
    • Caso: quien abre una cápsula puede rehacer el área con su certificado y su propia autoridad de sellado (§7.9), y la línea de F6 decía «antes de la fecha de apertura» sin más.
    • Pruebas previstas: format3_signed_cms (§67).
  3. El titular sin su identificador (§29.7, §55.2).
    • Cambio: el titular es su givenName y su surname antes que su commonName, si tiene uno de cada y los dos tienen texto no vacío; el emisor es su commonName, o si no tiene uno con texto, su organizationName.
    • Motivo: el commonName de un certificado de persona física de la FNMT es «APELLIDOS NOMBRE - NIF», y §55.2 promete el nombre sin el serialNumber. El texto no decía qué pasaba con un atributo sin texto: la implementación de referencia lo trataba como ausente, y una que siguiera el texto podía mostrar otro nombre.
    • Caso: un certificado con el givenName «JUAN», el surname «ESPAÑOL ESPAÑOL» y el commonName «ESPAÑOL ESPAÑOL JUAN - 12345678Z» se mostraba con el NIF. Con un givenName vacío, repetido o en un VisibleString, la referencia muestra el commonName.
    • Pruebas previstas: security_cms.json y las del titular y el emisor de internal/cms (cert_test.go).
  4. El perfil del certificado (§29.10).
    • Cambio: un lector lee un certificado campo a campo, con el perfil de §29.10, y no con lo que acepte su biblioteca; un certificado que lo incumple no decide nada salvo que lo nombre un SignerInfo; la clave EC va sin comprimir, y la RSA con un módulo impar.
    • Motivo: «con sus propias reglas (RFC 5280)» dejaba a cada implementación lo que aceptaba: la de referencia y la de TypeScript daban veredictos distintos a unos 1 600 certificados de un diferencial.
    • Caso: un commonName en PrintableString con «_» daba F1 en una y F6 en la otra; una clave EC comprimida, F5 en una y F6 en la otra.
    • Pruebas previstas: security_cms.json.
  5. Identificadores, repeticiones y casos límite (§29.10, §29.11).
    • Cambio: los identificadores de objeto se comparan por los bytes de su DER; un SET OF puede repetir un elemento, y dos copias de un certificado cuentan como una; una clave de otro esquema que el algoritmo es inválida (F2); un messageImprint de otra longitud es S3; accuracy lleva segundos de 0 a 2³¹ − 1, y milisegundos y microsegundos en INTEGER mínimos; los crls de un token no deciden nada.
    • Motivo: el texto lo decía o lo dejaba abierto, y la implementación de referencia hacía otra cosa: daba F1 a un atributo desconocido con un arco de 2³¹ o más, S2 a una TSA que repetía su certificado y F5 a una clave RSA con un algoritmo ECDSA.
    • Caso: una TSA de OpenSSL que lleva su certificado dos veces en certificates daba S2.
    • Pruebas previstas: security_cms.json.
  6. Las direcciones del localizador (§44.1).
    • Cambio: «pública» se define con los registros de uso especial de IANA; se rechazan los nombres locales, los caracteres que RFC 3986 no admite, los segmentos «.» y «..» y un CID que no decodifica; un lector rechaza la dirección y usa las demás, y comprueba en cada conexión la IP a la que resuelve un nombre. El texto fija además lo que la implementación de referencia exigía sin que él lo dijera: segmentos de 1 a 63 caracteres que no empiezan ni acaban en guion, el esquema en minúsculas, 0x y los nombres locales sin distinguir mayúsculas de minúsculas, una IPv4 sin ceros a la izquierda y un CID en su forma canónica; y el puerto, que ella aceptaba con ceros a la izquierda hasta cinco dígitos, deja de llevarlos.
    • Motivo: «privada» no estaba definida, y la implementación de referencia aceptaba 100.64.0.1, una dirección de NAT64 que lleva a 127.0.0.1, localhost y nombres de un segmento; una dirección inválida inutilizaba todo el localizador. Una implementación que siguiera el texto aceptaba además direcciones que la de referencia rechazaba.
    • Caso: https://[64:ff9b::7f00:1]/ llega a 127.0.0.1. El texto aceptaba https://01.2.3.4/, que algunos clientes leen en octal, y un CID con los bits sobrantes distintos de cero, que la implementación de referencia rechazaba; y esta aceptaba el puerto 0443 y rechazaba 000443, el mismo número.
    • Pruebas previstas: locator.json.
  7. El relleno del localizador (§44.1, CDDL).
    • Cambio: la clave 6 lleva al menos un byte; el texto da todos los huecos, a 1, 2, 26 y 259 bytes de un múltiplo; el CDDL fija los tamaños y los enteros seguros.
    • Motivo: el CDDL admitía h'' y un desplazamiento 0 escrito, que la implementación de referencia rechazaba, y el texto solo explicaba el hueco de 26.
    • Caso: con una base de 4094 bytes, 06 40 completaba 4096 según el CDDL, y la implementación pedía 8192.
    • Pruebas previstas: locator.json.
  8. Erratas (§44.1, §55.2, §64, §67, §76).
    • Cambio: §64 da F2 a una firma de alg 1 que no verifica; §67 lista los fixtures y los vectores de la v0.11; §76 deja de anunciar vectores que ya existen; §55.2 dice que el tamaño del resto da la longitud del .dkc; y §44.1, que la longitud del localizador solo oculta las direcciones dentro de los primeros 4096 bytes.
    • Motivo: textos que la v0.11 no puso al día.
    • Caso: una firma de alg 1 con datos al azar da F2 (§29.9), y §64 decía F1.
    • Pruebas previstas: ninguna nueva.
  9. Los vectores de la v0.11 (§67).
    • Cambio: security.json da los veredictos de un lector de esta versión, con un contexto, como security_cms.json, y format3_seal_unsupported usa seal_type 4294967295.
    • Motivo: el cierre de los cambios de la v0.11 los anunciaba y no se hicieron: security.json seguía dando los veredictos de la v0.10, y el fixture usaba seal_type 1, que está reservado.
    • Caso: «a signature of alg 1» daba F1, y su firma incumple la condición 3 de §29.9: es F2.
    • Pruebas previstas: security.json y format3_seal_unsupported.

Cambios normativos de la v0.13

La v0.13 recoge lo que encontró la implementación del localizador en Dart, el 6 de octubre de 2026, para la aplicación móvil. No cambia ningún formato (§70).

  1. La IP de un nombre en una red con NAT64 (§44.1).
    • Cambio: la IP a la que resuelve un nombre puede ser una IPv6 de 64:ff9b::/96, o del prefijo de NAT64 de la red si el lector lo conoce, que cuenta como pública si la IPv4 que lleva dentro lo es. Una dirección escrita en el localizador con una IPv6 de NAT64 se sigue rechazando.
    • Motivo: en una red solo IPv6, como las de muchos operadores móviles, DNS64 da a un host que solo tiene IPv4 una dirección de NAT64, y la v0.12 la rechazaba siempre, así que la aplicación no podía descargar el resto de un sobre guardado en él. La IPv4 de dentro es la que alcanza la conexión: comprobarla mantiene lo que la regla protege, que una dirección no lleve a la red local de quien abre.
    • Caso: un servidor https con solo IPv4 en 203.0.114.5 resuelve, en una red con DNS64, a 64:ff9b::cb00:7205, y un lector de la v0.12 no descargaba de él. Con un registro A de 192.168.1.10, resuelve a 64:ff9b::c0a8:10a, que sigue rechazándose.
    • Pruebas previstas: resolved_ip.json (§64).

Cambios normativos de la v0.14

La v0.14 escribe lo que la revisión de completitud del 6 de octubre de 2026 encontró sin escribir, de cara a la v1.0, y la raíz de confianza byte a byte, como ya la aplican las tres implementaciones. No cambia ningún formato ni ningún veredicto de una cápsula de Quicknet; solo deja de admitir un Provider Profile de otro scheme de drand (§70).

  1. Lo que no se garantiza (§5).
    • Cambio: §5 dice que nadie puede comprobar antes de la fecha que una cápsula se abrirá, y añade la caducidad, las condiciones distintas de una ronda y la cancelación.
    • Motivo: el texto no lo decía, y una cápsula podía presentarse como prueba de una puja o una predicción.
    • Caso: un creador escribe una cápsula time_and_key para una credencial que no existe; antes de la fecha, nadie distingue esa cápsula de una que se abrirá.
    • Pruebas previstas: ninguna; es texto.
  2. El proveedor (§7.6, §36.1, §71).
    • Cambio: §7.6 separa la confidencialidad hasta la fecha de la apertura después, dice que una cápsula no tiene otra vía de apertura y que time_and_key protege la primera, no la segunda; §71 da los estados de un perfil y dice que una entrada no se retira.
    • Motivo: §7.6 era una frase, y §74 lo dejaba como trabajo futuro, siendo el mayor riesgo real del sistema.
    • Caso: si drand deja de firmar antes de la ronda de una cápsula, el texto anterior no decía que nadie podrá abrirla.
    • Pruebas previstas: ninguna; es texto.
  3. Las firmas y los sellos frente a un adversario cuántico (§7.7, §53).
    • Cambio: §7.7 dice que las firmas de autor, las de las autoridades de sellado y los recipients X25519 tampoco son post-cuánticos, y lo que eso quita a un veredicto leído décadas después.
    • Motivo: §7.7 solo hablaba del timelock.
    • Caso: una cápsula time_only con firma y sello, leída en un horizonte en que se pueden falsificar firmas Ed25519, da F4 y S4 a una firma fabricada tras la fecha.
    • Pruebas previstas: ninguna; es texto.
  4. El cliente web (§7.10, §59).
    • Cambio: §7.10 cuenta con un cliente web sustituido, y §59 pide builds reproducibles, Subresource Integrity y un cliente sin conexión.
    • Motivo: el modelo de amenazas no tenía en cuenta la página que cifra y abre, que es como la usa la mayoría.
    • Caso: quien controla el dominio sirve una página que copia las palabras de una llave antes de cifrar, y la cápsula que escribe es válida.
    • Pruebas previstas: ninguna; es texto.
  5. La entropía de una llave de palabras (§38.1).
    • Cambio: el SDK oficial SHOULD ofrecer por defecto palabras al azar de una lista pública, y decir que unas palabras elegidas por una persona no son adecuadas para un contenido valioso.
    • Motivo: tras la fecha, cualquiera con el .dkc prueba palabras sin conexión, y seis palabras elegidas pueden tener poca entropía.
    • Caso: «perro luna casa verde tren mar» cumple las reglas del escritor.
    • Pruebas previstas: ninguna; es texto.
  6. La raíz de confianza, byte a byte (§12, §35, §51, §63 pasos 10 y 11).
    • Cambio: el paso 10 escribe el mensaje que firma una ronda de Quicknet, M = SHA-256 de la ronda en 8 bytes big-endian, su hash a G1 con la suite BLS12381G1_XMD:SHA-256_SSWU_RO_ de RFC 9380 y el DST BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_, y la ecuación e(H(M), clave pública) == e(firma, G2); el paso 11 escribe H2, H3 y H4 con sus etiquetas en bytes, y H3 con su contador de 2 bytes little-endian, su límite de 65534 intentos y el desplazamiento de un bit de su primer byte; §35 dice qué hace la identity tlock y que FK_TIME es, sin transformar, la file key de OUTER_TIME_AGE; §51 remite a la ecuación del paso 10; §12 dice que V1 pinnea Quicknet con bls-unchained-g1-rfc9380 y deja de admitir «una representación firmada equivalente», que ninguna implementación usaba.
    • Motivo: el paso 10 verificaba «según el scheme de drand», y H3 y H4 eran «las de drand/kyber»: verificar un release o abrir un stanza tlock exigía leer drand, kyber y kilic (revisión de completitud de la v0.13, punto 2.5).
    • Caso: H3 desplaza un bit a la derecha el primer byte de cada hash; una implementación que lo lee como una máscara y pone a cero el bit más significativo obtiene otro r, y la comprobación r·G2 == U falla en todas las cápsulas. En el stanza 111 de la ronda 1000 de tlock_steps.json, esa regla acepta el segundo intento en vez del cuarto. Con el DST de G2 sobre G1, el de bls-unchained-on-g1, o con la ronda sin SHA-256 como mensaje, ninguna firma de Quicknet verifica.
    • Pruebas previstas: tlock_steps.json (§64), nuevo.
  7. Un solo scheme de drand (§12.1).
    • Cambio: un Provider Profile V1 solo admite el scheme bls-unchained-g1-rfc9380, con la clave pública en G2; pedersen-bls-unchained y bls-unchained-on-g1 dan ERR_UNKNOWN_PROFILE en el punto 2 de §12.1.
    • Motivo: con el cambio 6, el texto fija byte a byte la verificación y el descifrado de ese scheme y de ningún otro; y las implementaciones divergían: la de referencia verificaba los tres, y las de TypeScript y Dart, solo el de Quicknet. V1 pinnea solo Quicknet, así que ninguna cápsula válida cambia.
    • Caso: un perfil de bls-unchained-on-g1 con su clave y su chain_hash válidos pasaba §12.1 y la referencia verificaba sus releases con el DST de G2 sobre G1, mientras que TypeScript y Dart los rechazaban en el paso 10.
    • Pruebas previstas: profile.TestDecodePrecedence, con un perfil de cada uno de los otros dos schemes.
  8. Erratas (§50, §76).
    • Cambio: §50 recuerda que el .dkc y el resto de un sobre son la misma dependencia de largo plazo que el release; el título de §76 ya no dice «del borrador v0.8.1»; y los cambios de cada versión van en orden.
    • Motivo: el bloque de la v0.13 iba antes que el de la v0.12.
    • Pruebas previstas: ninguna.

Cambios normativos de la v0.15

La v0.15 cierra los dos puntos de trabajo futuro de §74 sobre la recuperación a largo plazo: el formato de un objeto de release y de su fuente de archivo, y el uso del reloj local en el paso 9.c de §63. Sigue el diseño del 6 de octubre de 2026, las ocho decisiones de su autor y su corrección del 7 de octubre, que quitó el fichero .dkr (cambio 4). No cambia ningún formato de .dkc ni de .dkk; añade el objeto release, y cambia un veredicto: un release en la mano abre la cápsula aunque el reloj vaya atrasado (§70).

  1. El objeto release (§8, §45, §47, §47.1, §57, §69.1, datekeys.cddl).
    • Cambio: el release de una ronda tiene un formato, el objeto release: Deterministic CBOR sin trama, de 1 a 1024 bytes, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}. Es la respuesta de la Release API, el release_material de una Release Cache (§47) y la forma de un release que el llamador da desde un fichero o desde un archivo de releases; no tiene extensión de fichero propia. Un lector SHOULD aceptar además el JSON de drand como entrada, pero una Release Cache y la Release API no lo guardan ni lo sirven así.
    • Motivo: §47 dejaba release_material sin definir, y §74 dejaba su formato como trabajo futuro. Una caché, un archivo o un servicio no tenían un formato común para guardar y servir el release de una ronda, y sin ellos una cápsula depende de que un relay conserve la ronda (§50). El chain_hash identifica la cadena como la identifica drand; CBOR sin trama, como el Provider Profile, usa el codec que ya tiene cada implementación.
    • Caso: testdata/releases/1000.cbor, de 111 bytes, abre time_only.dkc con datekeys decrypt -release, sin red; el JSON de drand de la ronda 1000 da el mismo release sin nombrar la cadena.
    • Pruebas previstas: release.json (§64), nuevo, y los cuatro objetos de testdata/releases.
  2. La cadena del release en el paso 10 (§51, §63 paso 10, §69.1).
    • Cambio: el paso 10 empieza por las capas del objeto release y compara su chain_hash con el del perfil pinneado, con ERR_PROFILE_MISMATCH, antes que la ronda y la firma. No hay ningún código nuevo.
    • Motivo: un objeto release dice de qué cadena es, y un objeto que se contradice no se acepta, como el chain hash del stanza tlock en el paso 8.
    • Caso: el objeto de la ronda 1000 con un bit de su chain_hash cambiado y la firma publicada de Quicknet: sin la comparación abriría time_only.dkc, y con ella da ERR_PROFILE_MISMATCH en el paso 10.
    • Pruebas previstas: mutations.json, «release object of another chain» y «release object of another chain and another round, with a clock behind»; release.json.
  3. El paso 9.c y el release en la mano (§17, §49, §51, §63 paso 9, §69.1, §70, §73).
    • Cambio: 9.c solo se aplica antes de una petición de red. Un release en la mano —un objeto release o el JSON de drand en un fichero, o la entrada de un archivo local— no se compara con el reloj, y el lector MAY avisar de que su reloj va atrasado. Con una fuente de red, la persona MAY pedir la petición antes de round_time.
    • Motivo: las razones de 9.c son de red: no hacer peticiones inútiles ni observables y fallar pronto. Para un release en la mano no vale ninguna: su firma prueba que la ronda se publicó, y una firma válida de una ronda futura solo es posible si drand está comprometido (§7.6), cuando el reloj ya no protege la confidencialidad. El reloj vetaba un release válido por un dato local que nadie puede verificar, lo contrario de §3: una pila del CMOS gastada o una máquina virtual dejaban la cápsula cerrada con el release en la mano.
    • Caso: «round not reached yet» de mutations.json: time_only.dkc, el release publicado de la ronda 1000 en la mano y el reloj 1 ns antes de su round_time. La v0.14 daba ERR_RELEASE_UNAVAILABLE en el paso 9; ahora se abre. Con una fuente de red sigue dando ERR_RELEASE_UNAVAILABLE en el paso 9, sin petición.
    • Pruebas previstas: mutations.json, con el campo nuevo source en cada caso, «round not reached yet», que pasa a abrir, y «round not reached yet, from a network source» y «release of another round, from a network source», nuevos.
  4. La recuperación a largo plazo (§1, §4, §50, §53, §62.1, §79).
    • Cambio: §50 basa la recuperación a largo plazo en archivos de releases con todas las rondas de la cadena y en servicios de caché, de DateKeys o de otros, que los guardan de forma continua y los sirven; quien lee pide su ronda y verifica la firma con la clave pinneada, sin confiar en quien la sirve. §50 no promete ningún alojamiento y describe el formato del archivo como informativo; §53 avisa de que hará falta el release; §62.1 añade las reglas 26 y 27, SHOULD del SDK oficial: avisar al sellar y guardar el anexo junto al .dkc; §79, nuevo, es un anexo informativo para abrir una cápsula sin software de DateKeys, con el release tomado de un archivo, de un servicio de caché o de cualquier copia.
    • Motivo: una cápsula a veinte años depende de que alguien conserve el release de su ronda, y quizá de un software que ya no exista (revisión de completitud, punto 6; revisión de la v0.13, punto 2.6). tle no acepta un release dado y age no acepta una file key: sin el anexo, abrir una cápsula exigía leer drand, kyber, tlock y age.
    • Caso: scripts/recovery_check.sh abre format3_single (time_only) y format3_time_and_key_portable (time_and_key) solo con lo que dice el anexo, una librería BLS12-381 y age, sin código de DateKeys, tlock ni drand, y obtiene sus ficheros.
    • Pruebas previstas: scripts/recovery y su test, que prohíbe importar esos módulos, sobre ocho fixtures de los tres formatos; scripts/check.sh ejecuta la comprobación.
    • Descartado: el borrador del 6 de octubre guardaba el release en un fichero .dkr junto a la cápsula, y junto a una .dkk con localizador, con una extensión de fichero propia, la regla 28 de §62.1 (el SDK lo obtenía y lo guardaba tras la fecha) y las opciones decrypt -save-release y datekeys release de la CLI. El autor lo quitó el 7 de octubre: al crear la cápsula el release no existe, y llegada la fecha la cápsula ya se puede abrir, así que un release guardado junto a ella solo sirve para volver a abrirla y no cubre el caso real, el de quien la abre décadas después, cuando drand ya no existe y nadie guardó nada. El objeto release y el release en la mano se quedan.
  5. La Release API (§45).
    • Cambio: §45 fija que la respuesta es el objeto release y que quien la usa es una fuente de red; la forma HTTP de la petición pasa a ser informativa.
    • Motivo: ningún servicio la ofrece, y la forma de la URL no decide nada de seguridad: el release se verifica siempre (§51). La revisión sugería pasarla a un anexo; el autor prefirió mantenerla con el objeto.
    • Caso: las implementaciones de referencia piden el release a los relays de drand (provider/drand), nunca a la Release API, así que su forma HTTP no tenía ningún caso que la fijara.
    • Pruebas previstas: ninguna; es texto.
  6. Trabajo futuro (§74).
    • Cambio: salen los dos puntos de arriba; entra un archivo de releases o un servicio de caché del proyecto, con su alojamiento.
    • Motivo: el autor deja el alojamiento del archivo y del servicio para cuando haya un sitio propio. La extensión datekeys.release de la .dkk, que el borrador del 6 de octubre dejaba como trabajo futuro para guardar el release dentro de una llave con localizador, sale con el .dkr: tampoco existe el release al escribir la llave, y añadirlo después de la fecha no ayuda a quien abre décadas más tarde.
    • Pruebas previstas: ninguna.

Cambios normativos de la v0.16

La v0.16 corrige lo que encontró la revisión de Astra de la v0.15, el 7 de octubre de 2026, con la dirección que se acordó en su segunda y su tercera respuesta: cinco hallazgos, todos comprobados. No cambia ningún formato; cambian el veredicto de un sello sin accuracy y la lectura del JSON de drand (§70).

  1. El sello sin accuracy (§29.7, §29.10, §29.11, §62.1 regla 19, §64, §70).
    • Cambio: S4 exige que el token lleve accuracy y que t + precisión < round_time; sin accuracy, un sello válido es S5. S5 pasa a «No acredita que se sellara antes de la fecha de apertura: ‹motivo›.», con tres motivos, uno de ellos para un token de la política BTSP de ETSI, que exige accuracy. En F6, la línea de un firmante cuyo sello no la lleva no dice «antes de la fecha de apertura». El escritor avisa de un sello sin accuracy.
    • Motivo: con «0 si no viene», un sello sin accuracy acreditaba anterioridad con una precisión que nadie había declarado. RFC 3161 (§2.4.2) deja entonces la precisión a la política de la TSA, que el lector no conoce, y ningún margen fijo vale para toda TSA. El texto de S5, «Sellado después de la fecha de apertura», era además falso para un sello anterior a round_time cuya precisión llegaba a ella (revisión de Astra, punto 1).
    • Caso: «seal: before the round time: S4» de security_cms.json, un token sin accuracy sellado el 30 de septiembre de 2026 en una cápsula de 2030, daba S4 con una precisión de 0; ahora da S5, «el sello no dice su precisión». format3_sealed lleva una accuracy de 1 s y no cambia.
    • Pruebas: security_cms.json, hecho de nuevo, 143 casos: los sellos de los casos que prueban otra cosa llevan una accuracy de 1 s, y hay casos nuevos sin accuracy, años antes de la fecha y después, de la política BTSP con ella y sin ella, con una accuracy de 0 segundos y vacía, y de un firmante de alg 2 cuyo sello no la lleva, también BTSP, cada uno con su seal_reason; capsule.TestEvaluateSeal y TestEncryptFilesCMSAndSeal, que comprueba que el escritor devuelve el veredicto S5 de un sello sin accuracy para avisar de él.
    • Descartado: un margen fijo para el sello sin accuracy, y una tabla de políticas con su precisión, que queda como trabajo futuro (§74).
  2. La llave de palabras en el anexo (§79, 79.6, 79.7).
    • Cambio: el anexo trae la derivación de §38.1 y su normalización: una receta sin tablas, exacta para el texto que dan las listas de DateKeys (ASCII, á, é, í, ó, ú, ü, ñ y sus mayúsculas, y las marcas de U+0300 a U+036F), y para cualquier otro texto, la normalización completa con UnicodeData.txt de Unicode 18.0.0, que nombra por su SHA-256; y dos vectores. Lo que hace falta incluye PBKDF2-HMAC-SHA256.
    • Motivo: el anexo remitía a §38.1, que no contiene, y §38.1 a las tablas de §29.5.1: sin software de DateKeys no se podía abrir una cápsula de una llave de palabras (revisión de Astra, punto 2).
    • Caso: el texto «Ñandú», dos espacios, «PINGÜINO», un tabulador y «camión árbol Éter ola» da id = 273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7 en la cápsula del vector de §38.1, lo mismo que «nandu pinguino camion arbol eter ola» y que el texto con sus marcas sueltas.
    • Pruebas: scripts/recovery abre con -words y, si hace falta, -unicodedata; TestAnnexWordVectors comprueba los dos vectores, TestNormalizeVectors, que las dos normalizaciones dan las palabras de cada caso de wordkey.json, y TestRecoverWithWords, el fixture nuevo format3_time_and_key_words, que scripts/recovery_check.sh abre también; wordkey.json gana el texto del vector, también con sus marcas sueltas, y su llave.
  3. El último bloque de age (79.5).
    • Cambio: «el último más corto» pasa a «el último puede ser más corto, o estar completo»: un plaintext de 65 536 bytes es un solo bloque completo, marcado como último. La última línea del cuerpo de un stanza puede estar vacía.
    • Motivo: lo dice la especificación age de C2SP (§77). Quien siguiera el anexo rechazaría un fichero age cuyo plaintext mide un múltiplo de 65 536 bytes (revisión de Astra, punto 3). scripts/recovery ya lo hacía bien.
    • Caso: ningún fixture oficial tiene un PAYLOAD_AGE que acabe en un bloque completo, así que scripts/recovery_check.sh no lo comprobaba.
    • Pruebas: format3_full_chunk, cuyo BODY y cuya P miden 65 536 bytes, abierto por scripts/recovery_check.sh y por TestRecoverFixtures.
  4. Las evidencias de la firma con certificado (§29.10, §62.1 reglas 21 y 22).
    • Cambio: la regla 21 deja de recomendar podar la cadena a un certificado por firmante: el escritor SHOULD guardar las cadenas de los firmantes y de las autoridades de sellado sin sus raíces, y las respuestas OCSP; si no caben, MUST decir qué deja fuera y SHOULD ofrecer exportarlo. La regla 22 pide lo mismo para la cadena de la autoridad de un sello de la clave 3. §29.10 promete solo lo que la cápsula conserva.
    • Motivo: un validador de largo plazo necesita los certificados intermedios y la revocación del momento de la firma, y años después quizá no pueda pedirlos. Podar la cadena los perdía en silencio, y §29.10 prometía la respuesta OCSP aunque no cupiera (revisión de Astra, punto 4).
    • Caso: con la regla 21 de la v0.15, la firma de una tarjeta cuya cadena tiene un certificado intermedio quedaba sin él, y un validador solo podía obtenerlo si su emisor lo seguía publicando.
    • Pruebas previstas: ninguna en el lector, cuyos veredictos no cambian: los certificados de más no deciden nada (§29.10). En el escritor, cuando firme con certificados, la lista de lo que deja fuera. La medida del área (§74, §75) incluirá las cadenas.
  5. El JSON de drand, estricto (§47.1, §64, §70).
    • Cambio: ningún objeto del JSON repite un nombre; los nombres se comparan exactos, tras decodificar sus escapes; un sustituto suelto hace el JSON mal formado; y round es un número sin signo, sin fracción ni exponente, de 1 a 2⁵³ − 1. Lo demás es ERR_RELEASE_INVALID.
    • Motivo: «round es un número entero» dejaba la lectura a cada librería de JSON. Las tres implementaciones leen como encoding/json de Go, que se queda con el último de dos nombres repetidos y no distingue mayúsculas, y otro lector con JSON.parse o con el json de Python distingue mayúsculas: la misma entrada daba dos rondas (revisión de Astra, punto 5).
    • Caso: {"round":1000,"ROUND":1001,"signature":…}, con la firma de la ronda 1000, da ERR_ROUND_MISMATCH con time_only.dkc en las tres implementaciones, que leen la ronda 1001, y abre la cápsula con un lector que distingue mayúsculas; y {"round":1000,"round":1001,…} da la ronda 1001 en las tres, y la 1000 en un lector que se queda con el primero.
    • Pruebas: release.json, con 25 casos nuevos del JSON de drand (§64), y provider.TestStrictJSON, con los bordes de la gramática de RFC 8259 y UTF-8 inválido. provider/drand lee las respuestas de los relays con el mismo lector.
  6. Erratas (§1, §74, §77, §79).
    • Cambio: §1 junta en una línea la Release API, la Release Cache y el objeto release; la lista de provisionales de §74 acaba en punto; §77 añade ETSI EN 319 421 y 319 422; §79 nombra las interfaces de drand/kyber, que scripts/recovery también importa; y la cabecera ya no dice «prevista» de la implementación de referencia.
    • Pruebas previstas: ninguna.

77. Referencias


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.


79. Anexo informativo: recuperación sin software DateKeys

Este anexo no es normativo. Dice cómo abrir una cápsula de Quicknet sin ningún software de DateKeys, por si dentro de décadas no existe. Repite lo que fijan las secciones que cita, que deciden en caso de duda.

La regla 27 de §62.1 recomienda al SDK oficial guardar este anexo junto al .dkc. No contiene ningún dato de una cápsula.

Hace falta:

  • el .dkc;
  • el release de su ronda, de cualquier fuente: un relay de drand, un archivo de releases, un servicio de caché (§50) o cualquier copia. No hace falta confiar en quien lo da: se verifica con la clave pública de 79.1 (79.3);
  • en time_and_key, una credencial: la .dkk, la identity age de un recipient o las palabras de una llave de palabras (§38.1);
  • una librería de BLS12-381 con pairing y con el hash a G1 de RFC 9380, SHA-256, HMAC-SHA256, HKDF-SHA256 (RFC 5869), ChaCha20-Poly1305 (RFC 8439), un decodificador de CBOR y la herramienta age (§77) o una librería compatible;
  • con una llave de palabras, PBKDF2-HMAC-SHA256 (RFC 8018) y, si las palabras llevan otras letras que las de 79.7, UnicodeData.txt de Unicode 18.0.0.

No sirven las herramientas de drand: tle pide el release a la red y no acepta uno dado, y age no acepta una file key, que es lo que da el stanza tlock (79.4). Por eso este anexo describe esos dos pasos enteros (79.4 y 79.5).

La implementación de referencia lo sigue en scripts/recovery, un programa que no importa ningún paquete de DateKeys, tlock ni drand: solo la librería estándar de Go, golang.org/x/crypto, filippo.io/age y la librería BLS12-381 drand/kyber-bls12381, con las interfaces de drand/kyber. scripts/recovery_check.sh abre con él una cápsula time_only, otra time_and_key con su .dkk, otra con una llave de palabras y otra cuyo PAYLOAD_AGE acaba en un bloque completo, de los fixtures oficiales. Las palabras se le dan en un fichero de texto, y UnicodeData.txt, si hace falta, en otro.

79.1 Parámetros de Quicknet

Son los de §12, y pueden no estar ya en ningún otro sitio:

chain_hash   52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
clave pública (G2, 96 bytes)
             83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c
             8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb
             5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a
genesis_time 1692803367 (segundos Unix de la ronda 1)
period       3 segundos
round_time(r) = genesis_time + (r − 1)·3
q            0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001
DST          BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_

Los puntos se codifican comprimidos, en el formato de ZCash (§12.2): 48 bytes en G1, la firma, y 96 en G2, la clave pública y U, con la coordenada c1 antes que c0.

79.2 La cápsula

El .dkc empieza por 16 bytes (§22):

0   4  "DKC1"
4   1  VERSION: el formato, 1, 2 o 3
5   1  0
6   2  0
8   4  PUBLIC_HEADER_LEN, entero big-endian
12  4  SEALED_CONTROL_LEN, entero big-endian

Le siguen PUBLIC_HEADER, de PUBLIC_HEADER_LEN bytes; SEALED_CONTROL, de SEALED_CONTROL_LEN bytes; y PAYLOAD_AGE, desde el byte 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN hasta el final del fichero.

PUBLIC_HEADER es un mapa CBOR (§24). Su clave 2 es capsule_id, 16 bytes; su clave 4, la política, 0 para time_only y 1 para time_and_key; y su clave 3, la DateKey, un texto dk1_ seguido del Base64URL sin relleno de un JSON (§18):

{"version":1,"network":"datekeys:quicknet:v1","round":1000}

round es la ronda de la cápsula.

79.3 El release

Un objeto release es un mapa CBOR (§47.1): la clave 0 es "datekeys-release"; la 2, el chain_hash, que ha de ser el de 79.1; la 3, la ronda, que ha de ser la de la DateKey; y la 4, la firma, de 48 bytes. Así lo sirven la Release API y un servicio de caché. En un archivo de releases (§50), la firma de la ronda r son los 48 bytes que empiezan en |cabecera| + (r − primera ronda)·48, y 48 ceros si el archivo no la tiene. Un relay de drand la entrega como JSON, {"round": …, "signature": "…"}, con la firma en hexadecimal. Hoy se pide así, aunque las direcciones pueden cambiar:

GET https://api.drand.sh/v2/chains/<chain_hash>/rounds/<ronda>

La firma no necesita confianza: se verifica (§63, paso 10). Con M el SHA-256 de la ronda en 8 bytes big-endian, y H el hash a G1 de RFC 9380 con la suite BLS12381G1_XMD:SHA-256_SSWU_RO_ y el DST de 79.1:

e(H(M), clave_pública) == e(firma, G2)

con e el pairing de G1 × G2, el primer argumento en G1 y el segundo en G2, y G2 el generador de G2. La firma y la clave pública se decodifican como puntos comprimidos válidos del subgrupo, distintos del punto en el infinito. Para una ronda solo hay una firma válida: cualquier copia que verifique es el release.

79.4 El stanza tlock y FK_TIME

SEALED_CONTROL es un fichero age (79.5) cuya cabecera tiene un solo stanza:

-> tlock <ronda en decimal> <chain_hash en hexadecimal en minúsculas>
<cuerpo en Base64 estándar sin relleno, en líneas de 64 caracteres>

El cuerpo mide 128 bytes, U ‖ V ‖ W: U, de 96 bytes, un punto de G2, y V y W, de 16 bytes. Con la firma del release (§63, paso 11):

sigma   = V XOR H2(e(firma, U))
FK_TIME = W XOR H4(sigma)
r       = H3(sigma, FK_TIME)
comprobar r·G2 == U
  • H2(x) son los 16 primeros bytes de SHA-256(IBE-H2 ‖ x), con x los 576 bytes del elemento de GT en el orden de §63, «Serialización de GT en H2»: c1 antes que c0 en cada nivel de la torre, y c2, c1, c0 en Fp6, cada elemento de Fp en 48 bytes big-endian. Una librería que serializa con c0 primero da otro H2. El vector de testdata/vectors/tlock_ibe.json lo comprueba: H2(e(G1, G2)) = cb87319f24560b5231579a09ad79f12e, con G1 y G2 los generadores.
  • H4(sigma) son los 16 primeros bytes de SHA-256(IBE-H4 ‖ sigma).
  • H3(sigma, FK_TIME): base = SHA-256(IBE-H3 ‖ sigma ‖ FK_TIME); para i = 1, 2, … hasta 65534, d = SHA-256(uint16_le(i) ‖ base), con el contador en 2 bytes little-endian delante de base; se desplaza un bit a la derecha el primer byte de d, solo ese byte; y si d, como entero big-endian de 32 bytes, es menor que q, r = d.

Las etiquetas son los bytes ASCII, sin longitud ni terminador. FK_TIME, de 16 bytes, es la file key del fichero age de SEALED_CONTROL. testdata/vectors/tlock_steps.json da cada valor intermedio de cinco stanzas.

79.5 Abrir un fichero age con su file key

Es la especificación age v1 de C2SP (§77), resumida. Un fichero age es una cabecera de texto y un payload binario:

age-encryption.org/v1
-> <tipo> <argumentos…>
<cuerpo del stanza en Base64 sin relleno, líneas de 64 caracteres, la última más corta, quizá vacía>
--- <MAC en Base64 sin relleno, 43 caracteres>
<payload>

Con la file key FK, de 16 bytes:

  1. La cabecera: clave_mac = HKDF-SHA256(ikm = FK, salt = vacío, info = header), 32 bytes. El MAC es HMAC-SHA256(clave_mac, la cabecera desde age-encryption.org/v1 hasta --- inclusive, sin el espacio que lo sigue). Si no coincide con el de la línea ---, la file key o la cabecera son otras.
  2. El payload empieza tras el salto de línea del MAC por un nonce de 16 bytes. clave = HKDF-SHA256(ikm = FK, salt = nonce, info = payload), 32 bytes.
  3. Lo demás son bloques de ChaCha20-Poly1305 de 65 536 bytes de texto, 65 552 cifrados; el último puede ser más corto, o estar completo: un plaintext de 65 536 bytes es un solo bloque, completo y marcado como último. El nonce de 12 bytes del bloque n, desde 0, es n en 11 bytes big-endian seguido de 0x01 en el último bloque y de 0x00 en los demás. No hay datos asociados. El último bloque solo puede estar vacío si es el único, y nada sigue al último bloque.

79.6 Las capas siguientes

El plaintext de SEALED_CONTROL es:

  • en time_only, CONTROL_CBOR;
  • en time_and_key, otro fichero age, INNER_ACCESS_AGE, con stanzas X25519, uno por credencial y señuelos hasta 16 en los formatos 2 y 3. Se abre con age -d -i clave.txt, con la identity de la credencial en clave.txt. La de una .dkk es su access_material. La .dkk empieza por 12 bytes, DKK1, 01, 00, 00 00 y BODY_LEN en 4 bytes big-endian (§40), y le sigue un mapa CBOR cuya clave 5 es ese access_material, 32 bytes (§41), y cuya clave 3 es el capsule_id de su cápsula. Una llave de palabras da la identity con 79.7.

CONTROL_CBOR es un mapa CBOR (§31). Su clave 3 es I_PAYLOAD, otra identity X25519 de 32 bytes, y en los formatos 2 y 3 su clave 6 es L, una cadena de 8 bytes con un entero big-endian, no un entero CBOR. Con I_PAYLOAD, age -d -i payload.txt abre PAYLOAD_AGE.

Una identity X25519 de 32 bytes se escribe para age en Bech32 (BIP 173, §77), no Bech32m: el prefijo age-secret-key-, los 32 bytes reagrupados de 8 en 5 bits con ceros al final, que dan 52 caracteres, y la suma de comprobación de BIP 173, calculada con el prefijo en minúsculas. Después, todo en mayúsculas: AGE-SECRET-KEY-1….

79.7 La llave de palabras

Unas palabras dan la identity X25519 de una credencial (§38.1):

P  = las palabras normalizadas, en UTF-8, separadas por un espacio (0x20)
S  = "DateKeys llave de palabras v2|" || chain_hash || "|" || ronda || "|" || capsule_id
id = PBKDF2-HMAC-SHA256(P, S, 600000 iteraciones, 32 bytes)      ; RFC 8018

En S, chain_hash es el de 79.1 y capsule_id el de 79.2, en hexadecimal en minúsculas, y la ronda, la de la DateKey en decimal, sin ceros a la izquierda. id es la identity, que se escribe para age como dice 79.6.

Normalización sin tablas. Si el texto solo lleva caracteres ASCII imprimibles (U+0021 a U+007E), los espacios U+0009 a U+000D y U+0020, las letras á, é, í, ó, ú, ü y ñ y sus mayúsculas, y marcas de U+0300 a U+036F, que es lo que da cualquier palabra de las listas de DateKeys, las palabras normalizadas salen así:

  1. se quitan las marcas de U+0300 a U+036F;
  2. á y Á pasan a a; é y É, a e; í e Í, a i; ó y Ó, a o; ú, ü, Ú y Ü, a u; ñ y Ñ, a n;
  3. A a Z pasan a a a z;
  4. se parte el texto por los espacios, sin palabras vacías.

El resto se queda: la puntuación y las cifras cuentan, y «perro,» no es «perro».

Normalización completa. Para cualquier otro texto, en este orden: la NFD de UAX #15, con la descomposición canónica de cada punto de código (el campo 5 de UnicodeData.txt, sin las que llevan una etiqueta entre < y >, aplicada hasta el final), la de las sílabas Hangul, que se calcula, y la reordenación canónica por la clase de combinación (campo 3); se quitan los puntos de código de U+0300 a U+036F; cada punto de código pasa a su minúscula simple (campo 13), si la tiene; y se parte por los espacios U+0009 a U+000D, U+0020, U+0085, U+00A0, U+1680, U+2000 a U+200A, U+2028, U+2029, U+202F, U+205F y U+3000, sin palabras vacías. Sirve cualquier copia de UnicodeData.txt de Unicode 18.0.0 cuyo SHA-256 sea 0736451de439ae7baf1425136617da495e09ee5afbe6e394374db7009ea08950; unicode.org la publica en https://www.unicode.org/Public/18.0.0/ucd/UnicodeData.txt. Otra versión de Unicode puede dar otras palabras: una que asigne un punto de código nuevo, o que cambie una descomposición o una minúscula.

Vectores, con el chain hash de Quicknet, la ronda 1000 y capsule_id = 000102030405060708090a0b0c0d0e0f:

  • «perro luna casa verde tren mar» da id = fceec4d8ca8de86c85a1f26ed49f82a2b38431bd0ce36db995ae7dfd49b96e41;
  • el texto «Ñandú», dos espacios, «PINGÜINO», un tabulador (U+0009) y «camión árbol Éter ola» da «nandu pinguino camion arbol eter ola» e id = 273295d29370126a3be50b743132718d3cd9137fb3bb4cb20aa23163d2e19bb7, lo mismo que ese texto con las tildes, la diéresis y la tilde de la eñe escritas como marcas sueltas detrás de su letra (U+0301, U+0308 y U+0303).

79.8 El contenido

El plaintext de PAYLOAD_AGE es:

  • en formato 1, el contenido entero;
  • en formato 2, el contenido en sus L primeros bytes, seguido de ceros;
  • en formato 3, BODY en sus L primeros bytes, seguido de ceros (§29.2).

BODY empieza por tres enteros de 4 bytes big-endian: AREA_LEN, SECURITY_LEN y HEAD_LEN. Siguen el área de security, de AREA_LEN bytes, que se puede saltar: solo da los veredictos de la firma y del sello (§29.7); el head, un mapa CBOR de HEAD_LEN bytes que empieza en 12 + AREA_LEN; y los ficheros, desde 12 + AREA_LEN + HEAD_LEN, el origen de sus desplazamientos.

La clave 5 del head es la lista de ficheros (§29.4). Cada uno es un mapa:

0 → ruta, con "/" entre carpetas
1 → tamaño
2 → start
3 → end
4 → SHA-256 de sus bytes
5 → mtime, en segundos Unix, opcional

Sus bytes van de start a end, sin incluir end, contados desde el origen. Las claves 3 y 4 del head son el comentario y el autor declarado: textos del creador que no prueban nada (§29.7). Una ruta que saldría de la carpeta de destino no se escribe.

79.9 Lo que el anexo no comprueba

Este anexo comprueba lo que decide que el resultado es el correcto: la firma del release, r·G2 == U, los MAC de cada fichero age y el SHA-256 de cada fichero. No comprueba, entre otras cosas, la codificación canónica de cada objeto, header_binding (§26), los 16 stanzas de INNER_ACCESS_AGE (§39), los ceros del relleno (§29.1) ni las reglas de las rutas (§29.5). Una cápsula que el lector de §63 rechazaría puede abrirse siguiendo este anexo; su contenido es el que sellaron las MAC de age, pero no tiene la garantía de un lector conforme.

Powered by TurnKey Linux.