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
```text
DateKey
↓
condición temporal pública
DateKeyCap (.dkc)
↓
objeto protegido
DateKeys Access Key (.dkk)
↓
capacidad adicional de acceso
```
---
## 9. Provider abstraction
Toda condición temporal se expresa como:
```text
provider
profile
condition
```
Ejemplo Quicknet:
```json
{
"provider": "drand",
"profile": "datekeys:quicknet:v1",
"condition": {
"round": 66884212
}
}
```
El protocolo no presupone que todos los providers utilicen rondas.
---
## 10. Provider Profile
Un Provider Profile es inmutable.
Debe definir:
-`profile_id`;
- provider;
- identificador de red;
- parámetros criptográficos;
- parámetros de tiempo;
- reglas de validación;
-`profile_hash`.
Cualquier cambio criptográficamente relevante exige otro perfil.
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.
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 comprimida de BLS12-381 que usa drand de un punto del grupo de claves del scheme —G1, 48 bytes, para `pedersen-bls-unchained`; G2, 96 bytes, para los otros dos—, en el subgrupo de orden primo y distinto del punto en el infinito.
3.**Autocomprobación de `chain_hash`.** Relaciona varias claves y por eso va después de todas las comprobaciones de campo. `chain_hash` (clave 5) MUST ser el hash de la información de cadena de drand (drand Protocol Specification, sección *Root of trust*, §77) de los demás parámetros:
```text
chain_hash = SHA-256(
uint32_be(period) ||
int64_be(genesis_time) ||
public_key ||
genesis_seed ||
network
)
```
con `period` en segundos, que el punto 2 acota a 32 bits; `genesis_time`, entero con signo de 64 bits; `public_key` y `genesis_seed`, los bytes exactos de las claves 6 y 10; y `network`, los bytes UTF-8 de la clave 4, que se omiten cuando `network` es `default`. `profile_id`, `provider` y `scheme` no entran en el hash. Una discrepancia es `ERR_PROFILE_MISMATCH`. Para Quicknet el resultado es su `chain_hash` de §12.
4.**Pin.** Al pinnearlo, su `profile_hash` (§11) MUST ser el que el cliente conoce previamente (§13): `ERR_PROFILE_MISMATCH`.
Los alfabetos de los nombres valen para todo perfil. Las demás reglas de los puntos 2 y 3 son las del proveedor drand en V1; un proveedor futuro definirá las suyas en su propio perfil (§10).
- 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.
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.
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.
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.
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`.
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).
El perfil se obtiene de la DateKey, evitando dos fuentes de verdad.
---
## 25. Política de acceso declarada
Valores V1:
```text
0 → time_only
1 → time_and_key
```
El parser MUST comprobar que la estructura criptográfica real de `SEALED_CONTROL` coincide con la política declarada.
Una discrepancia:
```text
ERR_POLICY_STRUCTURE_MISMATCH
```
---
## 26. Header binding
`CONTROL_CBOR` MUST contener:
```text
header_binding =
SHA-256(
PRELUDE ||
PUBLIC_HEADER_BYTES
)
```
donde `PUBLIC_HEADER_BYTES` son exactamente los bytes CBOR almacenados.
La implementación NO DEBE reserializar la cabecera para calcular el binding.
---
## 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:
```text
1. PAYLOAD_AGE
2. INNER_ACCESS_AGE
3. OUTER_TIME_AGE
```
Cada fichero `age` genera su propia file key aleatoria de 16 bytes.
Cada uno de los tres ficheros MUST ser un fichero age v1 completo conforme a la especificación `age` de C2SP (§77). Su cabecera sigue la gramática de esa especificación:
```text
header = v1-line 1*stanza end
v1-line = %s"age-encryption.org/v1" LF
stanza = arg-line *full-line final-line
arg-line = "-> " argument *(SP argument) LF ; el primer argumento es el tipo
argument = 1*VCHAR ; bytes 0x21 a 0x7E
full-line = 64base64char LF ; 48 bytes
final-line = *63base64char LF ; menos de 48 bytes, quizá ninguno
end = "--- " 43base64char LF ; MAC de 32 bytes
base64char = ALPHA / DIGIT / "+" / "/"
```
- Base64 usa el alfabeto estándar sin padding y se decodifica de forma estricta: un `=`, una longitud que ninguna codificación sin padding produce o unos bits finales distintos de cero hacen mal formada la línea.
- Las líneas terminan en LF: un CR forma parte de la línea y la hace mal formada.
- Los argumentos se separan por exactamente un espacio.
Una cabecera que no sigue esta gramática hace mal formado el fichero. Eso incluye una cabecera sin stanzas, porque la gramática exige al menos uno (`1*stanza`), y una cabecera que supera los límites del parser de la implementación (§74).
Correspondencia de errores:
| Fichero | Cabecera mal formada | Cabecera bien formada con un número o un tipo de stanzas que viola §29, §32 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.
El payload del usuario se cifra como un **fichero age v1 estándar completo**.
Durante la creación se genera:
```text
I_PAYLOAD = X25519 identity aleatoria de 32 bytes
R_PAYLOAD = X25519 public recipient correspondiente
```
Entonces:
```text
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:
```text
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:
```text
CONTROL_A + PAYLOAD_AGE_B
```
MUST fallar porque `I_PAYLOAD_A` no puede desenvolver la file key de `PAYLOAD_AGE_B`.
Este binding criptográfico es independiente de cualquier digest auxiliar del fichero.
---
## 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.
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.
- 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;
- 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`;
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.
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.
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.
`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.
`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:
```text
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:
```text
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:
```text
FK_ACCESS
```
Por tanto el `CONTROL_CBOR` y `PAYLOAD_AGE` no se duplican.
`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.
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).
NO constituye una propiedad de seguridad necesaria para el acceso.
Una identity X25519 solo podrá abrir el `INNER_ACCESS_AGE` para el que fue utilizada; el digest no sustituye esa propiedad criptográfica.
Si no existe metadata de verificación, la clave `verification_metadata` MUST omitirse. Un mapa vacío no es una representación canónica válida de ausencia en V1.
---
## 44. Extensiones de aplicación en `.dkk`
Información de integración que no pertenezca al protocolo base DEBE ir en:
```text
key 8 → noncritical_extensions
```
salvo que una futura especificación DateKeys registre una extensión crítica concreta.
De este modo las implementaciones no inventan campos core incompatibles.
La `data` de esas extensiones viaja en claro junto a la credencial y `header_binding` no la cubre: el protocolo base no la autentica, y solo informa a quien posee la `.dkk` (§55.1, §72).
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.
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).
- 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;
- 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`.
- 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.
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).
- 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.
- 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.
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.
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`.
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:
```text
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
- timestamp con fracción de segundo inmediatamente posterior a una frontera de ronda;
- fecha 2030-01-01;
- fechas próximas al genesis.
Cada vector:
```text
requested timestamp
expected round
effective timestamp
```
---
## 66. Test vectors `dk1_`
Cada vector:
```text
logical object
exact canonical JSON bytes
exact Base64URL bytes
final dk1_ string
```
La cadena textual debe ser única.
---
## 67. Test vectors `.dkc`
Los vectores `.dkc` V1 son **fixtures de descifrado y validación**, no pruebas que exijan reproducir byte a byte una llamada pública a `age.Encrypt`.
La implementación de `age` genera internamente randomness que su API pública no permite inyectar de forma estable. El protocolo NO exige parchear `age` ni `tlock` para controlar:
- file keys internas;
- efímeros X25519;
- nonces internos.
Cada vector oficial incluirá:
- bytes `.dkc` fijos previamente generados;
-`PUBLIC_HEADER` esperado;
- PRELUDE esperado;
- DateKey esperada;
-`header_binding` esperado;
- estructura esperada (`time_only` o `time_and_key`);
- identity `.dkk` cuando corresponda;
-`CONTROL_CBOR` esperado después de abrir;
-`I_PAYLOAD` esperado;
- plaintext final esperado;
- resultado esperado de cada etapa de verificación.
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.
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 |
| 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 |
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.
- 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.
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.
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.
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.
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`.