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

99 KiB

DateKeys Protocol Specification

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

Borrador normativo v0.8.2

Estado: Draft / pre-estándar
Fecha: 26 septiembre 2026
Proyecto: DateKeys
Implementación de referencia prevista: Go
Proveedor temporal V1: drand Quicknet


1. Alcance

Este documento define exclusivamente el protocolo base DateKeys.

Define:

  • DateKey: descriptor público de una condición temporal criptográfica.
  • DateKeyCap (.dkc): contenedor cifrado asociado a una DateKey y a una política de acceso.
  • DateKeys Access Key (.dkk): credencial portable de acceso.
  • perfiles de proveedor;
  • Quicknet como proveedor temporal V1;
  • resolución fecha → ronda;
  • framing de .dkc y .dkk;
  • cifrado y bindings;
  • políticas time_only y time_and_key;
  • recipient X25519 V1;
  • verificación;
  • Release API;
  • Release Cache;
  • recuperación directa contra el proveedor;
  • extensiones genéricas.

Este documento no define almacenamiento, descubrimiento, distribución ni aplicaciones construidas sobre DateKeys.

Una aplicación que necesite información de transporte, descubrimiento o integración DEBE expresarla mediante extensiones no críticas, sin modificar el núcleo del protocolo.


2. Terminología normativa

Las palabras MUST, MUST NOT, SHOULD, SHOULD NOT, MAY y OPTIONAL se interpretan conforme a RFC 2119 / RFC 8174.

  • MUST / DEBE: requisito obligatorio.
  • MUST NOT / NO DEBE: prohibición obligatoria.
  • SHOULD / DEBERÍA: recomendación fuerte.
  • MAY / PUEDE: opcional.

3. Principio rector

Nunca confiar en el servidor cuando la misma propiedad puede verificarse criptográficamente en el cliente.

Consecuencias:

  • fecha → condición se calcula localmente;
  • Provider Profiles se pinnean localmente;
  • releases se verifican localmente;
  • .dkc, .dkk, APIs y relays se consideran entradas no confiables;
  • DateKeys no debe necesitar plaintext ni secretos finales de acceso.

4. Objetivos de seguridad

DateKeys persigue:

  1. Confidencialidad temporal
    Bajo los supuestos del proveedor, una condición temporal no debe poder satisfacerse antes del momento elegido.

  2. Confidencialidad frente al operador
    DateKeys no debe necesitar conocer el plaintext ni la credencial final de acceso.

  3. Verificabilidad local
    El SDK debe detectar condiciones, perfiles y releases manipulados.

  4. Integridad
    Alteraciones del framing, cabecera, control o payload deben provocar fallo.

  5. Interoperabilidad
    Implementaciones independientes deben producir y consumir objetos compatibles.

  6. Recuperación independiente
    Siempre que el proveedor conserve o pueda servir el release necesario, el ciphertext siga disponible y las credenciales correspondientes existan, un objeto maduro debería poder abrirse sin pasar por la API DateKeys.

  7. Extensibilidad
    Nuevos proveedores y extensiones no deben redefinir objetos antiguos.


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;
  • autoría legal por metadatos;
  • fecha probatoria solo por created_at;
  • protección frente a un dispositivo ya comprometido;
  • control del plaintext después de un descifrado legítimo.

6. Fuera de alcance

El protocolo base NO define:

  • dónde se almacena un .dkc;
  • cómo se descubre un .dkc;
  • cómo se distribuye un .dkc;
  • políticas de disponibilidad del fichero;
  • mecanismos de naming externos;
  • mecanismos de entrega de una .dkk.

Estas cuestiones pertenecen a capas superiores.


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.

7.4 Poseedor de .dkk

Debe tratarse como poseedor de una capacidad sensible.

7.5 Cadena de suministro

Un SDK o dependencia comprometidos pueden:

  • sustituir la raíz de confianza;
  • aceptar condiciones falsas;
  • exfiltrar secretos;
  • debilitar criptografía.

7.6 Provider comprometido

Si deja de cumplirse el supuesto de seguridad del provider, puede fallar la confidencialidad temporal.

7.7 Adversario cuántico futuro

El timelock Quicknet V1 no se considera post-cuántico.

Existe riesgo harvest now, decrypt later para ciphertexts de larga duración.

7.8 Dispositivo del creador

Si está comprometido antes o durante el cifrado, el protocolo no puede impedir la copia del plaintext o de secretos.


8. Objetos del protocolo

DateKey
    ↓
condición temporal pública

DateKeyCap (.dkc)
    ↓
objeto protegido

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

9. Provider abstraction

Toda condición temporal se expresa como:

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 DEBE pinnear estos parámetros o una representación firmada equivalente.


12.1 Validación de un Provider Profile

Una implementación MUST aplicar estas reglas antes de pinnear un Provider Profile y a todo perfil que decodifique, en este orden; la primera que falla decide el código (§69.1):

  1. Tipo, versión y schema. El type tag y la versión de schema (claves 0 y 1, §70) y el CDDL provider-profile de datekeys.cddl (§11): ERR_NON_CANONICAL_CBOR, o ERR_UNSUPPORTED_VERSION para una versión distinta de 1. Las reglas de nombre y de clave pública de ese CDDL tienen código propio (§57) y se comprueban en el punto 2. Una implementación que aplica el límite de implementación sobre period (§74) lo comprueba aquí, con ERR_NON_CANONICAL_CBOR.

  2. Campos. Cada campo, ya con el tipo CBOR de su regla CDDL; cualquier fallo es ERR_UNKNOWN_PROFILE:

    • profile_id (clave 2) está formado por caracteres de [a-z0-9:._-] y empieza por uno de [a-z0-9]; provider (3), network (4) y scheme (9), por caracteres de [a-z0-9._-], y empiezan por uno de [a-z0-9]. El profile_id aparece sin escapes en el JSON canónico de dk1_ (§18), que por eso no define ninguno;
    • una implementación que aplica los límites de implementación de longitud de esos nombres y de public_key (§74) los comprueba aquí, con este mismo código;
    • period (clave 7) es como mucho 2³² − 1 (4294967295) segundos, porque el hash del punto 3 lo codifica en 32 bits;
    • genesis_time (clave 8) está entre 1 y 253402300798 (9999-12-31T23:59:58Z): posterior al instante 0 y anterior al último instante representable de §15;
    • provider es drand, el único proveedor de V1;
    • scheme es uno de los schemes drand sin encadenar que tlock admite: pedersen-bls-unchained, bls-unchained-on-g1 o bls-unchained-g1-rfc9380;
    • public_key es la codificación canónica (§12.2) de un punto del grupo de claves del scheme —G1, 48 bytes, para pedersen-bls-unchained; G2, 96 bytes, para los otros dos— distinto del punto en el infinito.
  3. Autocomprobación de chain_hash. Relaciona varias claves y por eso va después de todas las comprobaciones de campo. chain_hash (clave 5) MUST ser el hash de la información de cadena de drand (drand Protocol Specification, sección Root of trust, §77) de los demás parámetros:

    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.

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.


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.

V1 recomienda exactamente:

16 random bytes

generados por CSPRNG.


22. Framing .dkc V1

V1 fija el siguiente prelude:

offset  size  field
0       4     MAGIC = "DKC1"
4       1     VERSION = 1
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.

V1 MUST exigir:

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 distinto de 1 → 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.


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

La data de las extensiones de PUBLIC_HEADER es pública y el protocolo base no la vincula al control hasta que se verifica header_binding (§63, paso 15). Esa verificación aporta coherencia interna, no autoría (§36.1, §55.1, §72).

No se almacena profile_id por separado.

El perfil se obtiene de la DateKey, evitando dos fuentes de verdad.


25. Política de acceso declarada

Valores V1:

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.


27. Validación pre-unlock

Antes de abrir el control pueden verificarse:

  • magic;
  • version;
  • 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. 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 y 33 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.

La autenticidad criptográfica definitiva de PUBLIC_HEADER se comprueba mediante header_binding tras abrir el control, salvo que una extensión adicional aporte autenticidad previa.


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 o §33
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 leen solo la cabecera 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.


29. PAYLOAD_AGE

El payload 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

Entonces:

PAYLOAD_AGE =
age.Encrypt(
    recipient = R_PAYLOAD,
    plaintext = user payload
)

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.


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.

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.


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 → 1
2 → header_binding (32 bytes)
3 → payload_identity (32 raw bytes, I_PAYLOAD)
4 → critical_extensions
5 → noncritical_extensions

Las claves 4 y 5 son opcionales y se omiten cuando no hay extensiones (§58.1).

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.

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.

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
recipient(s) = X25519
plaintext = CONTROL_CBOR

INNER_ACCESS_AGE MUST contener uno o más stanzas, todos ellos de tipo X25519, exactamente uno por recipient.

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.


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, todos ellos de tipo X25519, y sin dos stanzas de un solo argumento 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.

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 extensión de firma explícita añada esa propiedad.

La falsificación es posible desde el momento de creación: cifrar hacia una ronda futura solo requiere la clave pública del Provider Profile y la condición temporal pública. Además, header_binding se calcula únicamente a partir de bytes públicos (PRELUDE y PUBLIC_HEADER). Un tercero puede construir otro CONTROL_CBOR con un header_binding correcto, generar su propio I_PAYLOAD, crear su propio PAYLOAD_AGE y sellar ese control hacia la misma DateKey sin esperar a que la ronda madure.

Por tanto capsule_id, DateKey y header_binding proporcionan coherencia interna, no autoría (§55.1).

time_and_key añade una barrera adicional de acceso: un tercero que no posea la identity/capacidad requerida no puede abrir el INNER_ACCESS_AGE. Esta propiedad NO sustituye una firma del creador y no debe presentarse como prueba de autoría.


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.


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.

I_ACCESS se almacena como 32 bytes crudos dentro del .dkk.

Una I_ACCESS portable MUST generarse específicamente para una única capsule_id y MUST NOT reutilizarse en otra DateKeyCap. La reutilización convertiría una misma .dkk en capacidad válida para varias cápsulas y rompería el aislamiento esperado entre objetos.

La representación Bech32 AGE-SECRET-KEY-... MAY mostrarse/exportarse para interoperabilidad humana, pero NO es la representación binaria canónica del campo.


39. Múltiples recipients

INNER_ACCESS_AGE MAY contener múltiples stanzas X25519.

Todas envuelven la misma:

FK_ACCESS

Por tanto el CONTROL_CBOR y PAYLOAD_AGE no se duplican.


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)

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.


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


45. Release API

La API recomendada se indexa por condición:

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

Ejemplo Quicknet:

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

No requiere capsule_id.


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

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

El SDK MUST verificarlo de nuevo.


48. Multi-relay

La implementación SHOULD soportar varios relays independientes por disponibilidad.

La autenticidad procede de la verificación BLS.

No del hostname.


49. Recuperación directa contra el proveedor

Una implementación conforme SHOULD poder obtener un release directamente del proveedor temporal, sin pasar por la API DateKeys.

Para Quicknet:

.dkc
+
Provider Profile pinneado
+
release obtenido de un relay drand
+
.dkk si la política la exige

debe ser suficiente para ejecutar el flujo de apertura.

La API DateKeys es una capa de conveniencia, disponibilidad y caché, no una autoridad criptográfica obligatoria.


50. Dependencia del histórico de releases

La recuperación años después depende de que el release histórico necesario siga disponible.

Para Quicknet, esto puede provenir de:

  • un relay drand que conserve/entregue rondas históricas; o
  • una Release Cache válida conservada por otra fuente.

El protocolo NO debe asumir silenciosamente que cualquier proveedor conservará histórico indefinidamente.

Una aplicación que prometa horizontes largos SHOULD documentar esta dependencia.


51. Verificación de release Quicknet

El SDK MUST comprobar:

expected Provider Profile
expected chain hash
expected round
valid BLS signature

Al descifrar, el paso 10 de §63 fija el orden de estas comprobaciones y sus códigos: primero la ronda (ERR_ROUND_MISMATCH), después la firma (ERR_RELEASE_INVALID). 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;
  • el ciphertext puede permanecer disponible durante años;
  • la seguridad futura depende del provider y de la criptografía subyacente.

El umbral temporal de la advertencia es política de producto, no parte de la semántica criptográfica del protocolo.


54. Extensiones

PUBLIC_HEADER, CONTROL_CBOR y .dkk 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;
  • 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 y .dkk 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) interpreta su data, conforme a su schema registrado (§72):

  • una extensión crítica conocida cuya data no cumple su schema registrado → MUST reject (ERR_EXTENSION_DATA_INVALID);
  • una extensión no crítica conocida cuya data no cumple su schema registrado no invalida el objeto: la implementación MUST tratarla como inutilizable, sin usar su data, y MUST notificarlo al llamador.

La información específica de una aplicación que no pertenezca al núcleo DateKeys —incluidos datos de transporte o descubrimiento si una aplicación los necesita— MUST ir en noncritical_extensions y no en campos core del protocolo.


55. Integridad auxiliar

Hashes públicos MAY utilizarse para:

  • identificación;
  • caché;
  • deduplicación;
  • auditoría;
  • UX.

No sustituyen:

  • header MAC de age;
  • X25519 wrapping;
  • tlock;
  • STREAM authentication;
  • header_binding.

55.1 Modelo de confianza por sección

La tabla resume, para cada sección de un .dkc y para una .dkk, quién puede escribirla, desde qué paso de §63 queda vinculada al resto de la cápsula y mediante qué, y qué no prueba nunca. Ninguna fila prueba autoría: solo una extensión de firma puede aportarla (§36.1, §72). La tabla es normativa: una implementación MUST NOT presentar una sección como prueba de algo que su columna «Nunca prueba» excluye, salvo que lo cubra una extensión de firma.

Sección Quién puede escribirla Vinculada desde el paso, mediante Nunca prueba
PRELUDE y PUBLIC_HEADER Cualquiera que tenga el .dkc: viajan en claro y ninguna clave los protege. Paso 15: header_binding, dentro de CONTROL_CBOR, cubre sus bytes exactos (§26). Los pasos 1 a 8 solo comprueban su estructura. Autoría ni fecha de creación: header_binding se calcula con bytes públicos (§36.1). Antes del paso 15, ni siquiera su coherencia con el control.
CONTROL_CBOR Cualquiera puede sellar un control hacia la DateKey, porque basta la clave pública del perfil (§36.1). En time_and_key, uno que abra una credencial dada solo quien conozca la clave pública de su recipient. Paso 11: el MAC de la cabecera age y STREAM de OUTER_TIME_AGE, y en time_and_key los de INNER_ACCESS_AGE en el paso 13, autentican sus bytes frente a quien no conoce la file key. Paso 15: header_binding lo vincula a PUBLIC_HEADER. Que lo escribiera el creador de PUBLIC_HEADER: un tercero puede sellar otro control con un header_binding correcto. Tras la apertura, quien conoce FK_TIME o FK_ACCESS puede reescribirlo.
PAYLOAD_AGE Quien conoce R_PAYLOAD: antes de la apertura, solo quien selló el control; después, cualquiera que haya abierto CONTROL_CBOR y obtenido I_PAYLOAD. Paso 17: I_PAYLOAD desenvuelve FK_PAYLOAD, y el MAC de la cabecera y STREAM autentican cada byte (§30.1). 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.
Cuerpo de la .dkk Cualquiera que tenga la .dkk: no lleva MAC ni firma. Paso 9: su capsule_id debe ser el de PUBLIC_HEADER, un valor público, y su capsule_digest, cuando existe y se comprueba, la ata a los bytes exactos de un .dkc (§43). Paso 13: access_material abre INNER_ACCESS_AGE solo si es la identity de uno de sus recipients. Que la emitiera el creador de la cápsula. La data de sus extensiones no queda vinculada a nada.

En consecuencia, una afirmación de la que dependa una decisión de seguridad del lector —autorización, identidad, integridad o fecha— no puede apoyarse en PUBLIC_HEADER ni en una .dkk sin una extensión de firma, y la data de las extensiones de una .dkk solo informa a quien la posee (§72).


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.


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

Los tamaños se validan antes de reservar memoria.

Correspondencia de errores:

  • una longitud de trama (PUBLIC_HEADER_LEN, SEALED_CONTROL_LEN, BODY_LEN) 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 y el máximo de 64 extensiones por array → ERR_NON_CANONICAL_CBOR, salvo los casos con código propio: versión de schema (clave 1) no soportada → 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);
  • 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.

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.


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

Esto reduce representaciones equivalentes y simplifica vectores canónicos.


59. Supply-chain security

Implementaciones oficiales SHOULD:

  • pinnear dependencias criptográficas;
  • publicar SBOM;
  • firmar releases;
  • publicar hashes;
  • publicar perfiles firmados;
  • usar builds reproducibles cuando sea viable;
  • fuzzear parsers;
  • publicar vectores;
  • someter v1.0 a revisión criptográfica externa.

60. Interfaces Go conceptuales

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

1. Resolver DateKey localmente.
2. Generar capsule_id.
3. Generar I_PAYLOAD X25519 (32 bytes).
4. Crear PAYLOAD_AGE:
      age genera FK_PAYLOAD (16 bytes)
      recipient = R_PAYLOAD
      plaintext = payload
5. Construir PUBLIC_HEADER.
6. Construir PRELUDE.
7. Calcular header_binding.
8. Crear CONTROL_CBOR:
      header_binding
      I_PAYLOAD
      critical_extensions
      noncritical_extensions
9. Crear OUTER_TIME_AGE:
      age genera FK_TIME (16 bytes)
      recipient = tlock(DateKey)
      plaintext = CONTROL_CBOR
10. SEALED_CONTROL = OUTER_TIME_AGE.
11. Escribir PRELUDE || PUBLIC_HEADER || SEALED_CONTROL || PAYLOAD_AGE.

File keys utilizadas:

FK_PAYLOAD
FK_TIME

62. Flujo de cifrado time_and_key

1. Resolver DateKey localmente.
2. Generar capsule_id.
3. Generar I_PAYLOAD X25519.
4. Crear PAYLOAD_AGE:
      age genera FK_PAYLOAD
      recipient = R_PAYLOAD
      plaintext = payload
5. Construir PUBLIC_HEADER.
6. Construir PRELUDE.
7. Calcular header_binding.
8. Crear CONTROL_CBOR.
9. Crear INNER_ACCESS_AGE:
      age genera FK_ACCESS
      recipients = R_ACCESS[...]
      plaintext = CONTROL_CBOR
10. Crear OUTER_TIME_AGE:
      age genera FK_TIME
      recipient = tlock(DateKey)
      plaintext = exact INNER_ACCESS_AGE bytes
11. SEALED_CONTROL = OUTER_TIME_AGE.
12. Escribir .dkc.
13. Si se generó una identity portable:
      escribir I_ACCESS cruda en .dkk.

Las tres file keys son:

FK_PAYLOAD
FK_ACCESS
FK_TIME

y MUST ser independientes.


63. Flujo de descifrado

1. Parsear DKC1: MAGIC y PRELUDE completo (§23).

2. Validar PRELUDE, version, flags, reserved, longitudes y límites
   (§22, §23).

3. Leer PUBLIC_HEADER exacto.

4. Validar PUBLIC_HEADER con las capas de §69.1: tipo y versión de
   schema (§70), 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. SHOULD: inspeccionar la cabecera age de OUTER_TIME_AGE (§28.1)
   antes de usar red o secretos:
       exactamente un stanza;
       de tipo tlock.

6. SHOULD: localizar PAYLOAD_AGE mediante las longitudes del PRELUDE
   e inspeccionar únicamente su cabecera age (§28.1):
       exactamente un stanza;
       de tipo X25519.

7. Resolver/verificar localmente la condición temporal, incluida la
   cota de round_time de §15 (ERR_DATEKEY_INVALID).

8. SHOULD, cuando se haya realizado la inspección previa, comprobar los
   argumentos del stanza tlock, en este orden:
       exactamente dos argumentos tras el tipo
           → si no, ERR_POLICY_STRUCTURE_MISMATCH;
       el primero es la representación decimal canónica de
       DateKey.round: solo dígitos ASCII 0-9, sin signo y sin ceros a
       la izquierda
           → si no, ERR_ROUND_MISMATCH;
       el segundo es el chain_hash del Provider Profile pinneado en
       hexadecimal en minúsculas, 64 caracteres
           → si no, ERR_PROFILE_MISMATCH.
   Los argumentos se comparan como cadenas de bytes exactas: nunca se
   interpretan como números ni se normalizan mayúsculas o ceros.

9. Antes de pedir el release, y sin ninguna petición de red:
   a. Si access_policy = time_and_key y se ofrece una .dkk:
        primero la .dkk como objeto, con las capas de §69.1: su trama
        (§40) y su schema (§41), con los códigos de §57;
        access_type y access_material (claves 4 y 5)
        → ERR_ACCESS_INVALID; después sus extensiones críticas
        (clave 7), como en el paso 4;
        después, su vínculo con esta cápsula (§55.1): capsule_id
        (clave 3) distinto del de PUBLIC_HEADER → ERR_ACCESS_INVALID;
        capsule_digest (clave 6), si existe y la implementación lo
        comprueba, lo que es opcional (§43), distinto del SHA-256 del
        .dkc → ERR_ACCESS_INVALID.
   b. Si access_policy = time_and_key y no se ofrece ninguna
      credencial, .dkk o identity → ERR_ACCESS_REQUIRED, sin consultar
      el reloj.
   c. Si el instante actual es anterior al round_time de la DateKey
      → ERR_RELEASE_UNAVAILABLE, sin pedir el release.
   Con time_only, las credenciales ofrecidas no intervienen: no se
   validan ni se usan, y ningún error suyo se informa.
   Solo entonces, obtener el release; si ninguna fuente lo entrega
      → ERR_RELEASE_UNAVAILABLE.

10. Verificar el release localmente (§17, §51), en este orden:
       ronda del release distinta de DateKey.round
           → ERR_ROUND_MISMATCH, aunque su firma sea válida para esa
             otra ronda;
       firma que no es la codificación canónica (§12.2) de un punto
       del grupo de firmas del scheme —G1, 48 bytes, en Quicknet—, que
       es el punto en el infinito o que no verifica con la clave
       pública pinneada como firma de la ronda según el scheme de drand
           → ERR_RELEASE_INVALID.

11. Abrir OUTER_TIME_AGE.
    La identity tlock MUST recibir y validar el conjunto completo de stanzas
    y MUST rechazar el fichero salvo que contenga exactamente un stanza tlock
    (→ ERR_POLICY_STRUCTURE_MISMATCH).
    La ronda y el chain hash MUST coincidir con la DateKey y con el
    Provider Profile pinneado, con las reglas y los códigos del paso 8.
    El cuerpo del stanza tlock es el ciphertext IBE-CCA con el que tlock
    (drand/tlock, §77) envuelve la file key para la ronda:
        U || V || W
    con |U| el tamaño de punto del grupo de claves del scheme —96 bytes,
    G2, en Quicknet; 48, G1, en pedersen-bls-unchained— y
    |V| = |W| = 16: 128 bytes en Quicknet. U MUST ser la codificación
    canónica (§12.2) de un punto de ese grupo distinto del punto en el
    infinito. El descifrado es el de tlock con la firma verificada en el
    paso 10; en Quicknet, con e el pairing de G1 × G2:
        sigma   = V XOR H2(e(firma, U))
        FK_TIME = W XOR H4(sigma)
        r       = H3(sigma, FK_TIME)
    y MUST comprobar r·G == U, con G el generador del grupo de U. H2, H3
    y H4 son las funciones de drand/tlock (§77), sobre SHA-256 con las
    etiquetas IBE-H2, IBE-H3 e IBE-H4.
    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).

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;
       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, 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.
    Validar sus extensiones críticas (clave 4) como en el paso 4.

15. Verificar header_binding.
    Desde aquí la data de las extensiones de PUBLIC_HEADER queda
    vinculada al control abierto: coherencia interna (§36.1), no
    autoría, salvo que la cubra una extensión de firma (§72).

16. Recuperar I_PAYLOAD.

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.

18. Commit del plaintext solo si age completa sin error.

La inspección de los pasos 5, 6 y 8 es un SHOULD de fail-fast. 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.

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 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 esos pasos se realizan.

La .dkk es una entrada distinta del .dkc. Una implementación MAY decodificarla al recibirla, antes del paso 1, pero MUST informar de cualquier error suyo —de trama, de schema o de sus campos— solo en el paso 9.a, en el orden de ese paso, y nunca si access_policy es time_only.

PAYLOAD_AGE comienza exactamente en:

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.


64. Mutation tests obligatorios

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


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.

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.

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

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— se valida en cuatro capas, en este orden:

  1. Trama. La del .dkc para PUBLIC_HEADER (§22, §23) y la de la .dkk para su cuerpo (§40): magic, versión de framing, 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, ERR_UNSUPPORTED_VERSION, ERR_INVALID_FLAGS, 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.

  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 distinta de 1 es ERR_UNSUPPORTED_VERSION, sea lo que sea lo que la sigue: claves desconocidas, elementos fuera del perfil, truncado o bytes sobrantes.

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

    En un array de extensiones críticas, primero cualquier extensión desconocida → ERR_EXTENSION_CRITICAL_UNKNOWN; solo si todas son conocidas, una con data inválida → ERR_EXTENSION_DATA_INVALID (§54). En esta capa, las extensiones no críticas nunca producen error.

Entre objetos y entre pasos decide el orden de §63: el primer paso que falla determina el código. Las comprobaciones que relacionan un objeto con otro, o con el perfil resuelto, pertenecen a su paso y no a la capa 4 del objeto: la cota de round_time (paso 7, §15), los argumentos del stanza tlock (paso 8), el vínculo de una .dkk con su .dkc (paso 9.a), la estructura frente a access_policy (paso 12) y header_binding (paso 15). 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. 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.

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
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 ERR_ROUND_MISMATCH, paso 10
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

70. Compatibilidad

Una implementación V1:

  • MUST aceptar DKC1 y DKK1;
  • MUST rechazar major versions desconocidas;
  • MUST rechazar critical extensions desconocidas;
  • MAY ignorar noncritical extensions desconocidas;
  • MUST mantener inmutable la interpretación de perfiles publicados.

71. Registro de perfiles

DateKeys SHOULD publicar un registro de Provider Profiles.

Cada entrada debería incluir:

  • exact canonical CBOR;
  • SHA-256;
  • firma offline;
  • fecha de publicación;
  • estado.

72. Registro de extensiones

DateKeys MAY publicar un registro de extension_id.

Registrar una extensión no cambia el núcleo del protocolo.

Cada extensión registrada, identificada por (extension_id, extension_version), MUST declarar:

  • la codificación de su data, o que no lleva data;
  • su forma canónica;
  • su longitud máxima, que no puede superar la trama del objeto que la contiene (§57);
  • sus vectores de prueba.

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 (§36.1): solo una extensión de firma puede aportar autenticidad del creador (§27). La de .dkk viaja en claro y header_binding no la cubre, así que el protocolo base no la autentica en ningún paso.


73. Decisiones canónicas v0.8.2

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

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

payload access
= X25519 identity I_PAYLOAD dentro de CONTROL_CBOR

time_only
= age(tlock → CONTROL_CBOR)

time_and_key
= age(tlock → age(X25519 recipient(s) → CONTROL_CBOR))

portable .dkk
= X25519 identity de 32 bytes crudos

extensions
= único mecanismo genérico de extensibilidad en PUBLIC_HEADER, CONTROL_CBOR y .dkk

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

trust model
= §55.1; autoría solo mediante una extensión de firma

BLS12-381 points
= una sola codificación válida, la comprimida canónica de drand
  (§12.2); clave pública, firma y U distintos del infinito

tlock stanza body
= U || V || W, 128 bytes en Quicknet (§63 paso 11)

recovery
= puede obtener release directamente del provider

historical release availability
= dependencia explícita del horizonte de recuperación

74. Aspectos todavía provisionales

Antes de v1.0 quedan por cerrar:

  • schema CBOR final byte-a-byte de PUBLIC_HEADER;
  • schema CBOR final byte-a-byte de CONTROL_CBOR;
  • schema CBOR final byte-a-byte de .dkk;
  • límites definitivos de campos;
  • vectores definitivos de Provider Profile;
  • suite exacta de tests interoperables.

El framing base, la ausencia de PAYLOAD_LEN, el uso de age files estándar, la identity X25519 cruda de .dkk y el formato de extensiones (§54) dejan de considerarse provisionales en este borrador.

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.


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.
  6. fixtures oficiales .dkk de parseo/uso.
  7. mutation tests completos.
  8. strict tlock tests.
  9. parser fuzzing.
  10. revisión criptográfica externa.

76. Política de cambios del borrador v0.8.1

La v0.8.1 congela el diseño normativo del borrador para la fase de implementación e interoperabilidad.

A partir de esta versión, un cambio normativo posterior SHOULD responder a un caso reproducible descubierto mediante al menos una de estas fuentes:

  • implementación de referencia;
  • schema datekeys.cddl;
  • fixture oficial;
  • mutation test;
  • prueba de interoperabilidad;
  • fuzzing;
  • segunda implementación independiente;
  • revisión criptográfica o técnica externa.

Nuevas ideas, preferencias editoriales o posibilidades futuras que no estén respaldadas por uno de esos casos SHOULD documentarse fuera del núcleo normativo hasta que exista evidencia suficiente para modificarlo.

Esta política no impide correcciones editoriales que no alteren la semántica normativa.

Cambio normativo v0.8.2: extensiones

La v0.8.2 cierra el formato de las extensiones (§74) en un único 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).

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 tres 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. Solo los pasos opcionales —5, 6 y 8, y la comprobación 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. 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 §74 atribuía a §31 y que §31 no decía.
  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» y «version 1.0000000000000001: its exact value, not a double»;
    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. 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): §28.1 remitía a unos «códigos propios de cada identity» que §63 no definía. 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_ (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.


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.

Powered by TurnKey Linux.