# Plan de implementación: `datekeys-go` Implementación de referencia en Go de la **DateKeys Protocol Specification v0.8.1**. Estado: borrador para revisión. Fecha: 25 de septiembre de 2026. --- ## 1. Objetivo y principios Construir desde cero una librería Go que implemente el protocolo base tal como lo fija la v0.8.1, con estas reglas: 1. **La especificación manda.** El código no añade semántica. Si el código descubre un problema en el spec, se abre un caso reproducible según la política de cambios de la sección 76. 2. **Cero criptografía propia.** Solo `age`, `tlock` y la verificación BLS de `drand`. La librería aporta framing, CBOR, bindings, reglas de verificación y flujo. 3. **Validar todo lo verificable localmente antes de tocar red o secretos.** Sección 63. 4. **Fallar cerrado.** Ningún plaintext parcial, ningún error enmascarado, ningún "verified: true" ajeno. 5. **Superficie pequeña y auditable.** Pocas dependencias, todas pinneadas, builds reproducibles, y un mapa de trazabilidad spec ↔ código para la revisión externa. 6. **Nueva librería, no refactor.** Lo aprovechable del prototipo se copia y se adapta; el repositorio actual no se modifica. --- ## 2. Alcance de la primera versión de la librería (v0.1.0) Implementa: - DateKey: resolución fecha → ronda, `dk1_` canónico, vectores (secciones 14 a 19). - Provider Profile Quicknet: struct, CBOR canónico, `profile_hash`, registro pinneado (secciones 10 a 13). - Adaptador drand: obtención de releases por relays, verificación BLS local, modo estricto (secciones 35, 45 a 52). - `.dkc`: prelude, PUBLIC_HEADER, CONTROL_CBOR, `header_binding`, `time_only` y `time_and_key`, flujo de cifrado y descifrado completo con SHOULD y MUST (secciones 20 a 39, 61 a 63). - `.dkk`: framing, body, identity portable de un solo uso (secciones 40 a 44). - Extensiones genéricas con reglas de duplicados y orden canónico (sección 54). - Catálogo de errores (sección 69). Límites de parser (57). Canonicidad CBOR y omisión de opcionales (58, 58.1). - Fixtures, mutaciones, fuzzing y CLI mínima. No implementa, a propósito: - Servidor de Release API ni Release Queue. La librería define la interfaz de fuente de releases; el servidor actual podrá consumirla más adelante. - Almacenamiento, entrega de `.dkk`, servicios, extensiones concretas. - Cliente TypeScript. Vendrá después con la misma suite de fixtures. --- ## 3. Decisiones de diseño ### 3.1 Módulo y layout Repositorio: Gitea propio, `https://g.activething.com/go/DateKeys.git`. Módulo: `g.activething.com/go/DateKeys`, la ruta que el propio Gitea anuncia en su etiqueta `go-import`. Si más adelante se quiere una ruta en `datekeys.com` que no dependa del servidor, es un cambio de una línea en `go.mod` y un `sed` en las importaciones, siempre antes de publicar. Go 1.26, con `go.mod` fijando la última versión de parche de la serie. No se usa GitHub para nada del proyecto; las dependencias alojadas allí se obtienen como módulos Go, igual que cualquier otra. ```text datekeys-go/ go.mod LICENSE Apache-2.0 (código) README.md SECURITY.md CONTRIBUTING.md TRADEMARKS.md CHANGELOG.md spec/ DateKeys_Protocol_Specification_v0.8.1.md copia congelada datekeys.cddl schema normativo de todos los CBOR datekey/ DateKey, resolución, dk1_ canónico profile/ Provider Profile, CBOR canónico, hash, registro pinneado, quicknet.go provider/ interfaces Condition, Release, ReleaseSource, Verify provider/drand/ relays HTTP, verificación BLS, modo estricto codec/ CBOR determinista: Encode, Decode con comprobación por reencodificación, límites codec/bech32/ codificación Bech32 para la forma humana de identities (ver 4.3) agewrap/ identities y recipients envolventes de age: cardinalidad, sonda de inspección, stanza tlock extension/ tipos y reglas de extensiones capsule/ .dkc: framing, cabecera, control, binding, Encrypt, Inspect, Open accesskey/ .dkk: framing, body, identity errors.go sentinel errors del catálogo de la sección 69 internal/testkit/ generación de fixtures, escritor de cabeceras age malformadas, mutadores testdata/fixtures/ fixtures oficiales y sus valores esperados docs/traceability.md mapa sección del spec → paquete, función, test cmd/datekeys/ CLI: encrypt, decrypt, inspect, datekey, profile ``` ### 3.2 API pública, deliberadamente pequeña ```go // datekey type DateKey struct { ProfileID string; Round uint64 } func Resolve(p *profile.Profile, at time.Time) (DateKey, error) // §15, precisión completa func Parse(s string) (DateKey, error) // §19, solo forma canónica func (d DateKey) Compact() string // dk1_... func (d DateKey) UnlockAt(p *profile.Profile) time.Time // profile type Profile struct { ID, Provider, Network string; ChainHash [32]byte; PublicKey []byte; Period time.Duration; GenesisTime int64; Scheme string; GenesisSeed [32]byte } func (p *Profile) CanonicalCBOR() []byte func (p *Profile) Hash() [32]byte // §11 var Quicknet *Profile // §12, pinneado en el binario type Registry interface { Lookup(id string) (*Profile, bool) } // provider type Condition struct { Round uint64 } type Release struct { Round uint64; Signature []byte } type ReleaseSource interface { Fetch(ctx context.Context, p *profile.Profile, c Condition) (Release, error) } func Verify(p *profile.Profile, c Condition, r Release) error // §51, BLS local siempre // capsule type Policy uint8 // TimeOnly = 0, TimeAndKey = 1 type EncryptOptions struct { Profile *profile.Profile UnlockAt time.Time Policy Policy Recipients []age.Recipient // X25519 de destinatarios conocidos NewPortableKey bool // genera I_ACCESS y devuelve la .dkk Critical, Noncritical []extension.Extension } type Result struct { DateKey datekey.DateKey; CapsuleID [16]byte; PortableKey *accesskey.AccessKey } func Encrypt(dst io.Writer, src io.Reader, opts EncryptOptions) (*Result, error) type Inspection struct { /* prelude, cabecera, política, ronda, unlock, resultado de cada comprobación */ } func Inspect(r io.ReadSeeker, reg profile.Registry) (*Inspection, error) // pasos 1 a 8, sin red ni secretos type OpenOptions struct { Registry profile.Registry Source provider.ReleaseSource Identities []age.Identity // X25519 propias del destinatario AccessKey *accesskey.AccessKey // .dkk portable Now func() time.Time // inyectable para tests } func Open(dst io.Writer, r io.ReadSeeker, opts OpenOptions) error // pasos 9 a 18 // accesskey type AccessKey struct { CredentialID, CapsuleID [16]byte; Type string; Material []byte; Verification *Verification; Critical, Noncritical []extension.Extension } func Encode(w io.Writer, k *AccessKey) error func Decode(r io.Reader) (*AccessKey, error) func (k *AccessKey) Identity() (age.Identity, error) ``` Reglas de la API: - `io.Reader` y `io.Writer` en todo; el payload nunca se carga entero en memoria. - `Open` recibe `io.ReadSeeker` para poder inspeccionar la cabecera de PAYLOAD_AGE en el offset `16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN` antes de pedir el release. Si el lector no permite seek, se omite el SHOULD y se aplica el MUST al abrir. - El reloj se inyecta. Ningún paquete llama a `time.Now` directamente salvo la CLI. - Ningún tipo imprime secretos en `String()`. Las identities se pasan como tipos de `age`, no como bytes sueltos, salvo en `accesskey`. ### 3.3 Cómo se usan `age` y `tlock` - **PAYLOAD_AGE e INNER_ACCESS_AGE:** `age.Encrypt` y `age.Decrypt` con recipients e identities X25519 de la API pública de `age`. - **OUTER_TIME_AGE:** `age.Encrypt` con un recipient propio en `agewrap` que llama a `tlock.TimeLock` y `tlock.CiphertextToBytes`, ambos exportados, y emite el stanza `tlock ` con el mismo formato que `tlock`. Para abrir, una identity propia que comprueba el conjunto completo de stanzas, la ronda y el chain hash contra la DateKey y el perfil, y llama a `tlock.TimeUnlock`, que verifica el beacon antes de descifrar. - Motivo: `tlock.New(...).Decrypt` usa una identity no exportada en la que no podemos imponer la cardinalidad ni evitar que todo error se convierta en `ErrTooEarly`. Usando su núcleo exportado se mantiene la compatibilidad de stanza con `tle` y se gana control del flujo. - **Cardinalidad MUST:** en las tres identities envolventes, dentro de `Unwrap`, que recibe todos los stanzas del fichero. Sin APIs internas. - **Inspección SHOULD:** `age.ExtractHeader` lee únicamente la cabecera del fichero, y `age.DecryptHeader` con una identity sonda, cuyo `Unwrap` registra los stanzas recibidos y devuelve un error centinela distinto de `ErrIncorrectIdentity`, entrega los stanzas parseados por `age` sin descifrar nada ni usar secretos. Ambas funciones son API pública de `age` 1.3. Para PAYLOAD_AGE se aplica en el offset del prelude. No hace falta parser propio. --- ## 4. Dependencias | Dependencia | Versión | Uso | Nota | |---|---|---|---| | `filippo.io/age` | v1.3.2 | ficheros age, X25519, `Stanza`, `Recipient`, `Identity` | única implementación de STREAM; no se reimplementa | | `github.com/drand/tlock` | v1.2.0 | `TimeLock`, `TimeUnlock`, `CiphertextToBytes`, `BytesToCiphertext` | no se usa `tlock.New`, `Encrypt` ni `Decrypt` | | `github.com/drand/drand/v2` | v2.1.7 | `crypto.Scheme`, `VerifyBeacon`, `chain.Info` | arrastra gRPC y otros; `govulncheck` ya confirmó que no son alcanzables | | `github.com/drand/kyber` y `kyber-bls12381` | transitivas | pairing BLS12-381 | sobre `kilic/bls12-381`, archivado; vigilar y documentar en SECURITY.md | | `github.com/fxamacker/cbor/v2` | v2.9.4 | CBOR determinista | `CoreDetEncOptions` para codificar; límites en decodificación | Sin otras dependencias en la librería. Tests con la biblioteca estándar. CLI con `flag`. HTTP con `net/http`. ### 4.1 Política de versiones `go.mod` pinneado, `go.sum` verificado, `go mod verify` en CI, `govulncheck` en cada PR, Renovate (compatible con Gitea) solo para parches, con revisión manual de cualquier cambio en `age`, `tlock`, `drand` o `kyber`. ### 4.2 Canonicidad CBOR Codificar con Core Deterministic Encoding. Al decodificar, además de los límites de `fxamacker` (sin longitudes indefinidas, sin tags, sin claves duplicadas, profundidad y tamaños acotados), se **reencodifica y se compara byte a byte** con la entrada. Cualquier diferencia es `ErrNonCanonicalCBOR`. Es el mismo principio que `dk1_` y no depende de que una librería prometa rechazar todas las formas no canónicas. ### 4.3 Bech32 `age` solo acepta identities y recipients X25519 en Bech32 por su API pública, y su paquete Bech32 es `internal`. El `.dkk` guarda 32 bytes crudos por spec. Se necesita un codificador Bech32 de unas cien líneas para pasar de bytes a `AGE-SECRET-KEY-1...` y de `age1...` a bytes. Es codificación, no criptografía. Opciones: copiar el paquete interno de `age` con su licencia BSD y atribución, o reimplementar BIP-173 con vectores de prueba. Propuesta: copiar, para no divergir. --- ## 5. Qué se copia del prototipo | Origen | Destino | Cambios | |---|---|---| | `pkg/datekeys/key.go`: `RoundForTime`, `TimeForRound`, `ParseDateKey`, `FromRound` | `datekey/` | quitar campos de API y el alias `EncryptionKey`; el struct de respuesta HTTP se queda en el servidor | | `pkg/datekeys/key_test.go`, `FuzzRecipient` | `datekey/` | renombrar, ampliar con vectores de la sección 65 | | `internal/quicknet/trust.go` | `profile/quicknet.go` | añadir CBOR canónico y hash; conservar la autocomprobación de chain hash | | `internal/drand/client.go` y tests | `provider/drand/` | carrera entre relays, respuestas malformadas, cancelación; adaptar a `ReleaseSource` | | `pkg/datekeys/crypto.go` | no se copia | sustituido por `agewrap` sobre el núcleo exportado de `tlock`; se conserva la doble verificación BLS | | `tests/integration/quicknet_test.go` | `provider/drand/`, build tag `integration` | prueba en vivo contra Quicknet | | `cmd/datekeys` | reescrito | misma ergonomía, nueva API | | `internal/api`, `web-client` | no se tocan | consumirán la librería más adelante | --- ## 6. Estilo de código - Identificadores, comentarios de código, mensajes de error y commits en **inglés**. Documentación de usuario en inglés con versión en español. Motivo: la revisión externa y la comunidad de `age` y drand. - `gofmt`, `goimports`, `go vet`, `staticcheck`, `golangci-lint` con configuración corta: `errcheck`, `govet`, `staticcheck`, `ineffassign`, `unparam`, `gosec` en modo aviso. - Errores: un sentinel por entrada del catálogo de la sección 69, envueltos con `%w` y contexto; nunca se sustituye un error por otro más cómodo. Los tests de mutación comprueban `errors.Is`. - Parsers: límites antes de reservar memoria, `io.LimitReader`, ningún `panic` alcanzable desde la entrada. Todo parser tiene un objetivo de fuzzing. - Sin estado global mutable. El registro de perfiles se pasa explícitamente; el registro por defecto contiene solo Quicknet. - Comentarios de paquete y de función citan la sección del spec que implementan, por ejemplo `// Spec §26`. - Secretos: no se registran en logs, no aparecen en `String()`, se borran de buffers propios al terminar, sin prometer más de lo que Go garantiza. - Salida atómica en la CLI: fichero temporal en el mismo directorio y renombrado al final; nunca sobrescribir destinos existentes. - Versionado semántico. `v0.x` hasta que el spec sea v1.0. Sin promesa de estabilidad de API antes de `v1.0.0`. --- ## 7. Estrategia de tests 1. **Unitarios** por paquete, tabla-driven, biblioteca estándar. 2. **Vectores de spec**, generados por el código y guardados como golden files versionados: ronda (frontera, un segundo antes y después, fracción de segundo tras frontera, 2030-01-01, cercanos al genesis), `dk1_` (objeto, JSON canónico, base64url, cadena), `profile_hash` de Quicknet. 3. **Fixtures `.dkc` y `.dkk`** (secciones 67 y 68): generados una vez con `internal/testkit` sobre una **ronda ya publicada**, con la firma BLS embebida en el fixture. Los tests verifican la firma contra la clave pinneada y descifran **sin red**. Se comparan todos los valores intermedios: cabecera, prelude, binding, control, `I_PAYLOAD`, plaintext y el error esperado en cada etapa. 4. **Corpus de mutaciones** (sección 64): las veinte mutaciones como funciones sobre un fixture válido; cada una afirma su sentinel error concreto. 5. **Escritor de cabeceras age malformadas** en `testkit`: añade stanzas o cambia tipos manteniendo el MAC válido, porque en el test se conoce la file key. Demuestra que la comprobación estructural rechaza lo que el MAC aceptaría. Es la prueba directa de la amenaza del creador. Implementación: según la especificación C2SP, el MAC de cabecera es HMAC-SHA-256 con clave HKDF-SHA-256 de la file key e info `header`, sobre el texto de la cabecera; `testkit` lo recalcula con `golang.org/x/crypto/hkdf`, ya presente en el grafo por `age`. Para abrir ficheros de test con una file key conocida se usa `age.NewInjectedFileKeyIdentity`, también API pública. 6. **Propiedades:** `Encode(Decode(x)) == x` para todo fixture; toda reordenación de claves o ensanchamiento de enteros se rechaza como no canónico; `Parse(Compact(d)) == d`. 7. **Fuzzing** nativo de Go: prelude, PUBLIC_HEADER, CONTROL_CBOR, `.dkk`, `dk1_`, cabecera age vía sonda. Corpus sembrado con fixtures y mutaciones. Presupuesto corto en cada PR, largo en nightly. 8. **Integración en vivo** con build tag: cifrar hacia ahora más treinta segundos, esperar, abrir por relays reales. Nightly, no en cada PR. 9. **Interoperabilidad con herramientas ajenas:** extraer SEALED_CONTROL de un fixture `time_only` y abrirlo con la CLI `tle` oficial; extraer PAYLOAD_AGE y abrirlo con la CLI `age` usando `I_PAYLOAD`. Si ambas pasan, la afirmación "son ficheros age estándar" queda demostrada por terceros. 10. **Cobertura:** mínimo 90 % en `codec`, `capsule`, `accesskey`, `datekey`, `agewrap`; `-race` siempre. 11. **CDDL:** `spec/datekeys.cddl` valida los fixtures en un job opcional de CI con la herramienta `cddl`; si no se quiere otra toolchain, se mantiene como documento normativo y se comprueba a mano en cada cambio de schema. --- ## 8. CI, releases y cadena de suministro - Gitea Actions, con la misma sintaxis que GitHub Actions: `test` en Linux y Windows con la versión estable de Go y la anterior, macOS cuando haya runner; `lint`; `vuln` con `govulncheck`; `fuzz-short`; `integration` nightly por cron; `sbom` con `cyclonedx-gomod`. - Releases con `goreleaser`, que publica en Gitea: `-trimpath`, `CGO_ENABLED=0`, checksums publicados, firma con `cosign` con clave propia, ya que la firma sin clave depende de un proveedor OIDC que Gitea no ofrece. - `SECURITY.md` con canal de reporte, versiones soportadas y la dependencia de `kilic/bls12-381` declarada como riesgo conocido y vigilado. - `TRADEMARKS.md`: "DateKeys" reservado; "implements the DateKey protocol" permitido. --- ## 9. Hitos y criterios de aceptación **M0. Cimientos.** Esqueleto, `go.mod` pinneado, CI con lint y `govulncheck`, `codec` con canonicidad por reencodificación y límites, `spec/datekeys.cddl` inicial, `profile` con Quicknet, su CBOR y su hash como primer vector oficial. Hecho cuando: el hash del perfil está congelado en un golden file y CI está verde en tres sistemas. **M1. Objetos.** `datekey` copiado y ampliado; framing DKC1 y DKK1; codecs de PUBLIC_HEADER, CONTROL_CBOR y `.dkk`; `header_binding`; `extension`; catálogo de errores; fuzzing de todos los parsers. Hecho cuando: los vectores de ronda y `dk1_` están generados y versionados y el fuzzing lleva una noche sin fallos. **M2. `time_only`.** `agewrap` con recipient e identity tlock estrictos, sonda de inspección, PAYLOAD_AGE, `Encrypt`, `Inspect`, `Open` con los 18 pasos; `provider/drand` copiado. Hecho cuando: un fixture `time_only` sobre ronda pasada se abre offline, la prueba con `tle` y `age` externas pasa, y las mutaciones aplicables fallan con su error exacto. **M3. `time_and_key`.** INNER_ACCESS_AGE con varios recipients, `.dkk` portable de un solo uso, `Identities` y `AccessKey` en `Open`. Hecho cuando: fixtures `time_and_key` con uno y varios recipients se abren offline, la reutilización de `I_ACCESS` está prohibida en `Encrypt` y probada, y las veinte mutaciones están cubiertas. **M4. Conformidad.** Suite de fixtures oficial, golden files, `docs/traceability.md` completo, integración nightly, SBOM y release firmada `v0.1.0`. Hecho cuando: cada sección normativa de la 14 a la 69 apunta a al menos un test, y `v0.1.0` se construye de forma reproducible. **M5. CLI.** `datekeys encrypt`, `decrypt`, `inspect`, `datekey resolve`, `profile hash`. `inspect` ejecuta solo los pasos 1 a 8 y nunca pide un release ni usa secretos. Hecho cuando: la CLI abre los fixtures y reproduce la ergonomía del prototipo con salida atómica. Fuera de estos hitos: SDK TypeScript, migración del cliente web, servidor de Release API sobre la librería, segundo perfil `evmnet`. --- ## 10. Trazabilidad `docs/traceability.md` mantiene una tabla sección → paquete → función → test. Se actualiza en el mismo PR que cualquier cambio de código normativo. Es el documento que se entrega al revisor externo junto con el spec, los fixtures y el corpus de mutaciones. --- ## 11. Decisiones a revisar 1. Decidida: ruta `g.activething.com/go/DateKeys`. Queda abierto si se pasa a una ruta en `datekeys.com` antes de publicar. Nota: el servidor ejecuta Gitea 1.18.3, sin Actions; los workflows de `.gitea/workflows` quedan listos para Gitea 1.21 o superior con un `act_runner`, y hasta entonces `scripts/check.sh` es el control obligatorio antes de cada push. 2. Inglés para código, errores y commits. 3. Licencia Apache-2.0 para el código, CC-BY-4.0 para el spec. 4. Copiar el Bech32 interno de `age` frente a reimplementarlo. 5. Depender del módulo `drand/v2` completo o extraer solo la verificación BLS a un paquete propio sobre `kyber-bls12381`. La primera opción es más simple; la segunda reduce el grafo y el ruido de `govulncheck`. 6. Incluir en la librería un cliente de la Release API de DateKeys además del de relays drand, o dejarlo para el servidor. 7. Validación CDDL en CI con toolchain externa, o solo como documento. 8. Si `Open` exige `io.ReadSeeker` o acepta `io.Reader` y degrada el SHOULD. --- ## 12. Riesgos conocidos - `kilic/bls12-381` archivado bajo `drand` y `tlock`. Mitigación: pinneado, vigilancia, plan de fork o de migración documentado en `SECURITY.md`. - `tlock` sin etiqueta desde agosto de 2024. Mitigación: pinneado por versión; solo se usa su núcleo exportado. - Fixtures sobre rondas pasadas dependen de que la firma embebida sea auténtica. Mitigación: se verifica BLS en cada ejecución contra la clave pinneada. - Desviación spec ↔ código. Mitigación: trazabilidad, política de cambios de la sección 76, y una segunda implementación en TypeScript como prueba de interoperabilidad.