diff --git a/README.md b/README.md index 170c67f..d7211a4 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,13 @@ Lo que hay hoy (pasos 1 a 8) no importa ninguna dependencia. Funciona en navegad | `extension.ts` | Arrays de extensiones, leídos con su objeto (capa 3 de §69.1). Registros con ubicación opcional (`registeredIn`): una extensión conocida fuera de los objetos y arrays de su registro cuenta allí como desconocida (§54, §72), como `extension.Placement` en Go. Reglas del array: de 1 a 64, en orden estrictamente ascendente de los bytes UTF-8 de `extension_id` (nunca por unidades UTF-16), `extension_version` hasta 2³² − 1, `data` ausente o `bstr` no vacío, ningún id en los dos arrays; registros, críticas y no críticas (capa 4) | `extension` | | `profile.ts` | Provider Profile: CBOR exacto, `profile_hash`, reglas 1 a 4 de §12.1 en su orden (el límite de `period` de §74 en la capa del esquema; alfabetos, clave pública del grupo del scheme y fórmula de `chain_hash`), registro pinneado; Quicknet fijado por su CBOR y su hash | `profile` | | `bls12381.ts` | Pertenencia de claves públicas BLS12-381 comprimidas (G1 y G2) al subgrupo, como `FromCompressed` de kilic | `kyber-bls12381` | +| `ibe.ts` | IBE-CCA de tlock sobre G2 para Quicknet (§63 paso 11): `decryptOnG2`, con la puerta de codificación canónica de `bls12381.ts` sobre la firma y U; H2 sobre GT serializado en el orden de kilic (nunca `Fp12.toBytes` de noble), H3 y H4; `roundIdentity`; el cuerpo `U ‖ V ‖ W` de 128 bytes del stanza. Errores `IbeError` con motivo (`length`, `encoding`, `identity`, `proof`) y texto fijos, sin ningún valor del cálculo; borra sigma y los hashes derivados. Sobre `@noble/curves` 2.4.0; lleva el aviso MIT de `tlock-js`, cuya estructura sigue. Aún no lo reexporta `index.ts`: lo usará la apertura (paso 5 de la fase 2) | `encrypt/ibe` de drand/kyber (`DecryptCCAonG2`), `tlock.BytesToCiphertext` y `TimeUnlock` | | `datekey.ts` | `dk1_` canónico con las reglas de lectura de §19 (CR, LF y todo carácter fuera del alfabeto fallan el paso 1; números JSON por su valor decimal exacto), ronda desde una fecha con precisión de nanosegundos y cota de 9999-12-31T23:59:59Z (§15), parser RFC 3339 equivalente a `time.Parse(time.RFC3339Nano, …)` | `datekey` | | `header.ts`, `control.ts`, `accesskey.ts` | PUBLIC_HEADER, CONTROL_CBOR y `.dkk` (cuerpo y trama), decodificar y codificar, con las capas de §69.1 | `capsule`, `accesskey` | | `framing.ts` | Prelude DKC1 (16 bytes) y DKK1 (12 bytes) en el orden de §23 y §40, longitudes de 1 byte hasta los límites de §57, y troceo de secciones | `capsule/framing.go` | | `age.ts` | Parser estricto de la cabecera `age` v1 (§28.1) sobre los ficheros binarios, con los textos de error de `age`; reglas de stanzas; `MAX_AGE_HEADER_LEN` (2 MiB), el límite que usa la página para leer solo el prefijo de un `.dkc` grande | `agewrap`, `filippo.io/age/internal/format` | | `inspect.ts` | Pasos 1 a 8 de §63 y la vista JSON de `datekeys inspect -json` (`inspectView`, `inspectJSON`) | `capsule/inspect.go`, `internal/inspectview` | -| `index.ts` | Reexporta todo | | +| `index.ts` | Reexporta todo, salvo `ibe.ts` por ahora | | | `testing/` | Solo para tests: lectura de `testdata/` y de sus formatos (`vectors.ts`: ediciones, vectores), constructores de CBOR en hex, cirugía de cápsulas | | Los tests (`*.test.ts`) están junto a cada fichero. @@ -126,7 +127,7 @@ npm run build:check # solo la comprobación del sitio ya construido npm run verify # check, typecheck, coverage y build (con su comprobación) ``` -Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` al 100 % en líneas, ramas, funciones y sentencias; el conjunto de `src/lib/dkc` al 95/90/95/95, y el de `src/lib/inspector` también. +Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` e `ibe.ts` al 100 % en líneas, ramas, funciones y sentencias; el conjunto de `src/lib/dkc` al 95/90/95/95, y el de `src/lib/inspector` también. `vitest.config.ts` es la configuración de los tests; `vite.config.ts`, la del sitio con el plugin de SvelteKit. Vitest prefiere la primera, así que los tests de `src/lib` corren sin SvelteKit, y `src/lib/inspector` importa la librería por rutas relativas, sin el alias `$lib`. `tsconfig.json` extiende el que genera `svelte-kit sync` (por eso `typecheck` y `check` lo ejecutan antes, y `npm install` también, con `prepare`). @@ -141,12 +142,21 @@ Umbrales de cobertura (`vitest.config.ts`): `cbor.ts` al 100 % en líneas, ramas - `testdata/vectors/tlock_ibe.json`: el vector de H2 del IBE de tlock (§63 paso 11). Hasta que llegue `ibe.ts` (fase 2), el test lo recalcula con `@noble/curves` 2.4.0: los puntos son canónicos para `bls12381.ts`, el pairing serializado en el orden de kilic es el GT del vector y su H2 coincide; el orden propio de noble (`Fp12.toBytes`) da otro hash. - `testdata/vectors/mutations.json`: se leen los 65 casos enteros (ediciones sobre un fixture o hex congelado, release, reloj, registro, extensiones, `.dkk` e identidades). Los 31 de los pasos 1 a 8 pasan por `inspect` con su registro y sus extensiones, y dan el mismo código y el mismo paso; los 34 de los pasos 9 a 18 (entre ellos las 10 mutaciones de la enmienda de canonicidad de puntos, en los pasos 10 y 11) necesitan `open` (fase 2) y se saltan uno a uno con ese motivo, y un test fija los dos recuentos. Las `.dkk` ofrecidas se decodifican. - `testdata/vectors/inspect_differential.json`: las 1 825 mutaciones dan el mismo veredicto, código y paso que Go; los `bases` se comprueban por su SHA-256. +- `src/lib/dkc/testing/ibe-vectors.json`: los valores de referencia de `ibe.ts`. Los escribe `scripts/ibe-go-vectors.go` con kyber, tlock y `age`, las librerías de la referencia Go, y `ibe.test.ts` los comprueba todos: + - el GT de e(G1, G2) y de su cuadrado, con H2 de 16 y 32 bytes; + - H3 y H4 sobre entradas fijas, entre ellas una H3 aceptada en la segunda iteración y otra en la tercera; + - la identidad de varias rondas; + - para el stanza tlock de cada fixture oficial, el pairing, sigma, r y la file key. La file key es la que devuelve `tlock.TimeUnlock`, y con ella `age` abre el `OUTER_TIME_AGE`. El test lo repite con `age-encryption`: el MAC de la cabecera y STREAM verifican, y el contenido es el `control_cbor` del registro o un `INNER_ACCESS_AGE`; + - mensajes de 0, 1, 16 y 32 bytes cifrados por `EncryptCCAonG2` de kyber; + - el veredicto de `DecryptCCAonG2` sobre copias editadas del stanza de `time_only`: U con c0 + p, en el infinito o negado, V o W alterados, la firma de otra ronda, negada o en el infinito, y longitudes erróneas. + + H2, H3 y H4 no son públicas en kyber: el script las reescribe con sus etiquetas y las comprueba en cada fixture contra la file key de tlock y contra U = r·G2. Los cifrados de kyber usan un sigma aleatorio, así que el fichero se genera una vez y se congela. Para regenerarlo, desde un módulo Go temporal que requiera la referencia (`replace g.activething.com/go/DateKeys => ../datekeys-go`, `GOFLAGS=-mod=mod`): `go run ibe-go-vectors.go ../App/testdata/fixtures > ibe-vectors.json`. - Todo se lee con los formatos de `testdata/README.md` (`testing/vectors.ts`): una clave desconocida o que falta, un valor de otro tipo, un código que no es de §69 o una edición fuera de su base hacen fallar el fichero con su motivo; nada se salta en silencio. - Todo fichero de `testdata/` tiene que ejecutarlo algún test: un nombre nuevo exportado por Go (otro `vectors/*.json`, un fichero de fixture que ningún JSON nombra) hace fallar `testdata/ holds no file that no test runs` hasta que se le añade su bloque. ## Dependencias de ejecución -Aprobadas en el plan de la fase 2 (sección 3 y decisión 5) e instaladas con su versión exacta. Todavía ningún fichero de `src/` las importa, así que el sitio no cambia hasta que llegue el código que las usa. +Aprobadas en el plan de la fase 2 (sección 3 y decisión 5) e instaladas con su versión exacta. `ibe.ts` importa noble desde el paso 3, pero la página todavía no lo usa, así que el sitio no cambia hasta que llegue la apertura. | Paquete | Versión | Licencia | Uso | |---|---|---|---| diff --git a/docs/PLAN_fase2_ibe_noble2.md b/docs/PLAN_fase2_ibe_noble2.md index c8ffb87..fa190ec 100644 --- a/docs/PLAN_fase2_ibe_noble2.md +++ b/docs/PLAN_fase2_ibe_noble2.md @@ -159,7 +159,7 @@ La ruta `/inspect` gana una acción "abrir": con un fixture o un `.dkc` arrastra | 0 | Confirmar las decisiones de la sección 2 y aprobar las dependencias de la sección 3 | hecho el 26-09-2026 | | 1 | Precondición: `main` verde con `testdata` sincronizado al último commit de `datekeys-go` | cumplida en `d5e3236` (`692cf87`) y de nuevo en `71ab8fb`, con `testdata` en `9ac9cd9` (`spec-v0.8.2`) | | 2 | Dependencias y guardas | instaladas con versiones exactas; `npm audit --omit=dev` sin avisos; guardas en verde; en el lockfile, un solo noble 2.4.0 en la raíz, la copia 2.0.1 solo bajo `@noble/post-quantum` y ningún 1.x. Hecho el 28-09-2026: el README de `App` recoge las guardas, la medida del bundle y el resultado de `npm audit` | -| 3 | `ibe.ts` de descifrado desde la semilla, `roundIdentity`, escritura del stanza en `age.ts`, `ibe.test.ts` sin la parte de cifrado | los cinco fixtures dan la file key correcta; U no canónico e identidad rechazados; cobertura 100 % | +| 3 | `ibe.ts` de descifrado desde la semilla, `roundIdentity`, escritura del stanza en `age.ts`, `ibe.test.ts` sin la parte de cifrado | los cinco fixtures dan la file key correcta; U no canónico e identidad rechazados; cobertura 100 %. Hecho el 28-09-2026: vectores de `scripts/ibe-go-vectors.go` en `src/lib/dkc/testing/ibe-vectors.json`; `ibe.ts` al 100 %, fijado como umbral. `ibe.ts` también pasa el cuerpo `U ‖ V ‖ W` a bytes; los argumentos del stanza y su paso al `Stanza` de `age-encryption`, que guarda el tipo en `args[0]`, van al paso 7, con el `Recipient` que los usa | | 4 | `release.ts` y `release.test.ts` | ronda real válida, alias rechazados | | 5 | `open.ts` con la `Identity` propia, pasos 9 a 18, `open.test.ts` | los cinco fixtures se abren y el plaintext coincide con el sidecar; el corpus de mutaciones existente reproduce código y paso | | 6 | Spec, mutaciones en Go, `testdata:sync`, reproducción en TypeScript (sección 7) | texto aprobado; vectores congelados en ambos repositorios; commits en Gitea | diff --git a/scripts/ibe-go-vectors.go b/scripts/ibe-go-vectors.go new file mode 100644 index 0000000..9c4c344 --- /dev/null +++ b/scripts/ibe-go-vectors.go @@ -0,0 +1,418 @@ +//go:build ignore + +// Prints the Go reference values of src/lib/dkc/ibe.ts (plan of phase 2, +// section 4) as the JSON of src/lib/dkc/testing/ibe-vectors.json. Everything +// comes from the libraries that tlock decrypts with for Quicknet +// (bls-unchained-g1-rfc9380): drand/kyber encrypt/ibe on +// kyber-bls12381.NewBLS12381Suite(), over kilic/bls12-381. +// +// - gt: e(G1, G2) and e(2·G1, G2), serialized by kyber-bls12381 (the order +// of kilic: c1 before c0 at every level of the tower), with H2 truncated +// to 16 and 32 bytes. +// - h3 and h4: H3 and H4 on fixed inputs, among them inputs whose first +// candidates for r are rejected. +// - round_identities: the identity of a round, scheme.DigestBeacon. +// - fixtures: for the tlock stanza of every official .dkc, the pairing of +// the release signature with U, sigma, r and the file key. The file key +// is the one tlock.TimeUnlock unwraps, and age.Decrypt opens +// OUTER_TIME_AGE with it: the header MAC and the STREAM verify. +// - kyber: messages of 0, 1, 16 and 32 bytes encrypted for round 1000 by +// ibe.EncryptCCAonG2 itself, with its random sigma, and decrypted back by +// ibe.DecryptCCAonG2. +// - decrypt: the verdict of ibe.DecryptCCAonG2, after decoding the points +// as the scheme does, on edited copies of the time_only stanza. +// +// H2, H3 and H4 are unexported in kyber: this file restates them with +// kyber's exported tags, and checks them on every fixture against what +// tlock.TimeUnlock unwraps and against U = r·G2. A mismatch panics. +// +// The kyber vectors use a random sigma, so the output is not +// byte-reproducible: the file is generated once and frozen, like the .dkc +// fixtures of the reference. +// +// Run it from a scratch module that requires the reference implementation +// (replace g.activething.com/go/DateKeys => ../datekeys-go and +// GOFLAGS=-mod=mod), passing the directory of the official fixtures: +// +// go run ibe-go-vectors.go path/to/testdata/fixtures > ibe-vectors.json +package main + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math/big" + "os" + "path/filepath" + "runtime/debug" + "sort" + "strconv" + "strings" + + "filippo.io/age" + "g.activething.com/go/DateKeys/capsule" + "g.activething.com/go/DateKeys/profile" + "github.com/drand/drand/v2/common" + "github.com/drand/drand/v2/crypto" + "github.com/drand/kyber" + bls "github.com/drand/kyber-bls12381" + "github.com/drand/kyber/encrypt/ibe" + "github.com/drand/tlock" +) + +// The published Quicknet signature of round 1001, as in the mutation corpus. +const sig1001 = "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41" + +var ( + suite = bls.NewBLS12381Suite() + // The field and the scalar orders of BLS12-381. + p, _ = new(big.Int).SetString("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", 16) + order, _ = new(big.Int).SetString("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", 16) +) + +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} + +func unhex(s string) []byte { return must(hex.DecodeString(s)) } + +func marshal(m interface{ MarshalBinary() ([]byte, error) }) string { + return hex.EncodeToString(must(m.MarshalBinary())) +} + +func concat(parts ...[]byte) []byte { return bytes.Join(parts, nil) } + +func xor(a, b []byte) []byte { + out := make([]byte, len(a)) + for i := range a { + out[i] = a[i] ^ b[i] + } + return out +} + +// H2, H3 and H4 of kyber encrypt/ibe (gtToHash, h3, h4), restated. +func h2(gt []byte, n int) []byte { + sum := sha256.Sum256(concat(ibe.H2Tag(), gt)) + return sum[:n] +} + +func h4(sigma []byte, n int) []byte { + sum := sha256.Sum256(concat(ibe.H4Tag(), sigma)) + return sum[:n] +} + +// h3 returns r as 32 big-endian bytes and the iteration that accepted it. +func h3(sigma, msg []byte) ([]byte, int) { + base := sha256.Sum256(concat(ibe.H3Tag(), sigma, msg)) + for i := uint16(1); i < 65535; i++ { + d := sha256.Sum256(concat(binary.LittleEndian.AppendUint16(nil, i), base[:])) + d[0] >>= 1 + if new(big.Int).SetBytes(d[:]).Cmp(order) < 0 { + return d[:], int(i) + } + } + panic("h3: rejection sampling failed") +} + +// rG2 is r·G2 through kyber, with r decoded as kyber's h3 decodes it. +func rG2(r []byte) kyber.Point { + s := suite.G2().Scalar() + if err := s.UnmarshalBinary(r); err != nil { + panic(err) + } + return suite.G2().Point().Mul(s, nil) +} + +type gtVector struct { + Name string `json:"name"` + G1 string `json:"g1"` + G2 string `json:"g2"` + GT string `json:"gt"` + H2 string `json:"h2_16"` + H232 string `json:"h2_32"` +} + +type h3Vector struct { + Name string `json:"name"` + Sigma string `json:"sigma"` + Msg string `json:"msg"` + R string `json:"r"` + Iterations int `json:"iterations"` +} + +type h4Vector struct { + Sigma string `json:"sigma"` + H416 string `json:"h4_16"` + H432 string `json:"h4_32"` +} + +type roundIdentity struct { + Round uint64 `json:"round"` + ID string `json:"id"` +} + +type fixtureVector struct { + Name string `json:"name"` + Round uint64 `json:"round"` + Signature string `json:"signature"` + Body string `json:"body"` + GT string `json:"gt"` + Sigma string `json:"sigma"` + R string `json:"r"` + FileKey string `json:"file_key"` +} + +type ciphertextVector struct { + Name string `json:"name"` + Round uint64 `json:"round"` + Signature string `json:"signature"` + U string `json:"u"` + V string `json:"v"` + W string `json:"w"` + Go string `json:"go"` + Msg string `json:"msg,omitempty"` +} + +func main() { + scheme := must(crypto.SchemeFromName(crypto.SigsOnG1ID)) + quicknet := profile.Quicknet() + key := scheme.KeyGroup.Point() + if err := key.UnmarshalBinary(quicknet.PublicKey); err != nil { + panic(err) + } + out := struct { + Description string `json:"description"` + Generator string `json:"generator"` + Libraries string `json:"libraries"` + Scheme string `json:"scheme"` + PublicKey string `json:"public_key"` + GT []gtVector `json:"gt"` + H3 []h3Vector `json:"h3"` + H4 []h4Vector `json:"h4"` + RoundIdentities []roundIdentity `json:"round_identities"` + Fixtures []fixtureVector `json:"fixtures"` + Kyber []ciphertextVector `json:"kyber"` + Decrypt []ciphertextVector `json:"decrypt"` + }{ + Description: "Go reference values of the tlock IBE-CCA on G2 (spec §63 step 11) for src/lib/dkc/ibe.ts; " + + "see scripts/ibe-go-vectors.go for how each block is obtained.", + Generator: "scripts/ibe-go-vectors.go", + Libraries: libraries(), + Scheme: scheme.Name, + PublicKey: hex.EncodeToString(quicknet.PublicKey), + } + + // GT and H2. + g1, g2 := suite.G1().Point().Base(), suite.G2().Point().Base() + two := suite.G1().Point().Mul(suite.G1().Scalar().SetInt64(2), g1) + square := suite.GT().Point().Add(suite.Pair(g1, g2), suite.Pair(g1, g2)) + if !square.Equal(suite.Pair(two, g2)) { + panic("e(2·G1, G2) is not e(G1, G2) squared") + } + for _, c := range []struct { + name string + a, b kyber.Point + }{{"e(G1, G2)", g1, g2}, {"e(2·G1, G2), the square of e(G1, G2)", two, g2}} { + gt := must(suite.Pair(c.a, c.b).MarshalBinary()) + out.GT = append(out.GT, gtVector{c.name, marshal(c.a), marshal(c.b), hex.EncodeToString(gt), + hex.EncodeToString(h2(gt, 16)), hex.EncodeToString(h2(gt, 32))}) + } + + // H3: fixed inputs, then the first inputs of a deterministic sequence + // whose r is accepted at the second and at the third iteration. + for _, c := range []struct{ name, sigma, msg string }{ + {"16 zero bytes each", strings.Repeat("00", 16), strings.Repeat("00", 16)}, + {"32 bytes each", strings.Repeat("ab", 32), strings.Repeat("cd", 32)}, + {"empty", "", ""}, + } { + r, it := h3(unhex(c.sigma), unhex(c.msg)) + out.H3 = append(out.H3, h3Vector{c.name, c.sigma, c.msg, hex.EncodeToString(r), it}) + } + for want := 2; want <= 3; want++ { + for i := uint32(0); ; i++ { + seed := sha256.Sum256(binary.BigEndian.AppendUint32([]byte("DateKeys H3 vector "), i)) + sigma, msg := seed[:16], seed[16:] + if r, it := h3(sigma, msg); it == want { + out.H3 = append(out.H3, h3Vector{fmt.Sprintf("accepted at iteration %d (sequence item %d)", want, i), + hex.EncodeToString(sigma), hex.EncodeToString(msg), hex.EncodeToString(r), it}) + break + } + } + } + + // H4. + for _, sigma := range []string{strings.Repeat("00", 16), strings.Repeat("5a", 32)} { + out.H4 = append(out.H4, h4Vector{sigma, hex.EncodeToString(h4(unhex(sigma), 16)), hex.EncodeToString(h4(unhex(sigma), 32))}) + } + + // Round identities. + for _, round := range []uint64{1, 1000, 1001, 83903165811, 1<<53 - 1} { + out.RoundIdentities = append(out.RoundIdentities, roundIdentity{round, hex.EncodeToString(scheme.DigestBeacon(&common.Beacon{Round: round}))}) + } + + // The fixtures. + dir := os.Args[1] + names := must(filepath.Glob(filepath.Join(dir, "*.dkc"))) + sort.Strings(names) + var timeOnly fixtureVector + for _, name := range names { + v := fixture(scheme, key, name) + out.Fixtures = append(out.Fixtures, v) + if v.Name == "time_only" { + timeOnly = v + } + } + if timeOnly.Name == "" { + panic("no time_only fixture") + } + + // Encryptions by kyber, for round 1000. + id1000 := scheme.DigestBeacon(&common.Beacon{Round: 1000}) + sig1000 := unhex(timeOnly.Signature) + for _, n := range []int{0, 1, 16, 32} { + msg := sha256.Sum256([]byte("DateKeys IBE vector message")) + ct := must(ibe.EncryptCCAonG2(suite, key, id1000, msg[:n])) + v := verdict(scheme, fmt.Sprintf("a %d-byte message", n), 1000, sig1000, unhex(marshal(ct.U)), ct.V, ct.W) + if v.Go != "ok" || v.Msg != hex.EncodeToString(msg[:n]) { + panic("kyber does not decrypt its own ciphertext") + } + out.Kyber = append(out.Kyber, v) + } + + // Edited copies of the time_only stanza. + body := unhex(timeOnly.Body) + u, vv, w := body[:96], body[96:112], body[112:] + flip := func(b []byte, i int, mask byte) []byte { + c := bytes.Clone(b) + c[i] ^= mask + return c + } + // c0 is the second coordinate of the compressed encoding of G2. + c0 := new(big.Int).SetBytes(u[48:]) + uc0p := concat(u[:48], new(big.Int).Add(c0, p).FillBytes(make([]byte, 48))) + infinityG2 := concat([]byte{0xc0}, make([]byte, 95)) + infinityG1 := concat([]byte{0xc0}, make([]byte, 47)) + for _, c := range []struct { + name string + sig, u, v, w []byte + }{ + {"the time_only stanza", sig1000, u, vv, w}, + {"U with p added to c0", sig1000, uc0p, vv, w}, + {"U is the point at infinity", sig1000, infinityG2, vv, w}, + {"U negated", sig1000, flip(u, 0, 0x20), vv, w}, + {"V with its first bit flipped", sig1000, u, flip(vv, 0, 0x80), w}, + {"W with its last bit flipped", sig1000, u, vv, flip(w, 15, 0x01)}, + {"the signature of round 1001", unhex(sig1001), u, vv, w}, + {"the signature negated", flip(sig1000, 0, 0x20), u, vv, w}, + {"the signature is the point at infinity", infinityG1, u, vv, w}, + {"W one byte shorter than V", sig1000, u, vv, w[:15]}, + {"V and W of 33 bytes", sig1000, u, concat(vv, vv, []byte{0}), concat(w, w, []byte{0})}, + } { + out.Decrypt = append(out.Decrypt, verdict(scheme, c.name, 1000, c.sig, c.u, c.v, c.w)) + } + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + enc.SetEscapeHTML(false) + if err := enc.Encode(out); err != nil { + panic(err) + } +} + +// fixture opens the OUTER_TIME_AGE of a .dkc with the release of its sidecar, +// through tlock.TimeUnlock inside an age identity, and restates the IBE. +func fixture(scheme *crypto.Scheme, key kyber.Point, path string) fixtureVector { + file := must(os.ReadFile(path)) + var side struct { + Release struct { + Round uint64 `json:"round"` + Signature string `json:"signature"` + } `json:"release"` + } + if err := json.Unmarshal(must(os.ReadFile(strings.TrimSuffix(path, ".dkc")+".json")), &side); err != nil { + panic(err) + } + sig := unhex(side.Release.Signature) + pre := must(capsule.ParsePrelude(file)) + start := capsule.PreludeSize + int(pre.PublicHeaderLen) + sealed := file[start : start+int(pre.SealedControlLen)] + + var body, fileKey []byte + id := unwrap(func(stanzas []*age.Stanza) ([]byte, error) { + if len(stanzas) != 1 || stanzas[0].Type != "tlock" || len(stanzas[0].Args) != 2 || stanzas[0].Args[0] != strconv.FormatUint(side.Release.Round, 10) { + panic("not one tlock stanza for the round of the release") + } + body = stanzas[0].Body + ct := must(tlock.BytesToCiphertext(*scheme, body)) + fileKey = must(tlock.TimeUnlock(*scheme, key, common.Beacon{Round: side.Release.Round, Signature: sig}, ct)) + return bytes.Clone(fileKey), nil + }) + r := must(age.Decrypt(bytes.NewReader(sealed), id)) + if _, err := io.Copy(io.Discard, r); err != nil { + panic(fmt.Sprintf("%s: the age payload does not open: %v", path, err)) + } + + // The IBE restated, checked against tlock. + u, v, w := body[:96], body[96:112], body[112:] + sp, up := scheme.SigGroup.Point(), scheme.KeyGroup.Point() + if sp.UnmarshalBinary(sig) != nil || up.UnmarshalBinary(u) != nil { + panic("points do not decode") + } + gt := must(suite.Pair(sp, up).MarshalBinary()) + sigma := xor(v, h2(gt, len(w))) + msg := xor(w, h4(sigma, len(w))) + if !bytes.Equal(msg, fileKey) { + panic(path + ": the restated H2 and H4 disagree with tlock") + } + rb, _ := h3(sigma, msg) + if !rG2(rb).Equal(up) { + panic(path + ": the restated H3 disagrees with U") + } + return fixtureVector{strings.TrimSuffix(filepath.Base(path), ".dkc"), side.Release.Round, side.Release.Signature, + hex.EncodeToString(body), hex.EncodeToString(gt), hex.EncodeToString(sigma), hex.EncodeToString(rb), hex.EncodeToString(fileKey)} +} + +// verdict decodes the points as the scheme does and runs ibe.DecryptCCAonG2, +// the decryption of tlock.TimeUnlock for Quicknet after its beacon check. +func verdict(scheme *crypto.Scheme, name string, round uint64, sig, u, v, w []byte) ciphertextVector { + out := ciphertextVector{Name: name, Round: round, Signature: hex.EncodeToString(sig), U: hex.EncodeToString(u), + V: hex.EncodeToString(v), W: hex.EncodeToString(w), Go: "reject"} + sp, up := scheme.SigGroup.Point(), scheme.KeyGroup.Point() + if sp.UnmarshalBinary(sig) != nil || up.UnmarshalBinary(u) != nil { + return out + } + msg, err := ibe.DecryptCCAonG2(suite, sp, &ibe.Ciphertext{U: up, V: v, W: w}) + if err != nil { + return out + } + out.Go, out.Msg = "ok", hex.EncodeToString(msg) + return out +} + +type unwrap func([]*age.Stanza) ([]byte, error) + +func (f unwrap) Unwrap(stanzas []*age.Stanza) ([]byte, error) { return f(stanzas) } + +// libraries names the versions of the libraries this program ran with. +func libraries() string { + info, ok := debug.ReadBuildInfo() + if !ok { + panic("no build info") + } + var out []string + for _, d := range info.Deps { + switch d.Path { + case "filippo.io/age", "github.com/drand/tlock", "github.com/drand/kyber", "github.com/drand/kyber-bls12381", "github.com/drand/drand/v2": + out = append(out, d.Path+" "+d.Version) + } + } + sort.Strings(out) + return strings.Join(out, ", ") +} diff --git a/src/lib/dkc/ibe.failure.test.ts b/src/lib/dkc/ibe.failure.test.ts new file mode 100644 index 0000000..a3abf9d --- /dev/null +++ b/src/lib/dkc/ibe.failure.test.ts @@ -0,0 +1,39 @@ +// A failure of noble after the canonical-encoding gate of ibe.ts, which no +// input reaches today: the gate admits only points that noble decodes too. +// The pairing is replaced by one that throws, in a file of its own because +// vi.mock replaces the module for the whole file. + +import { readFileSync } from 'node:fs'; +import { describe, expect, it, vi } from 'vitest'; +import { ciphertextFromBody, decryptOnG2, IbeError } from './ibe.ts'; +import { h } from './testing/testdata.ts'; + +vi.mock('@noble/curves/bls12-381.js', async (importOriginal) => { + const noble = await importOriginal(); + return { + ...noble, + bls12_381: { + ...noble.bls12_381, + pairing: () => { + throw new Error('pairing failed: 0123456789abcdef0123456789abcdef'); + }, + }, + }; +}); + +describe('ibe.ts when noble fails', () => { + it('reports a proof failure with a fixed message, never the text of noble', () => { + const vectors = JSON.parse(readFileSync(new URL('./testing/ibe-vectors.json', import.meta.url), 'utf8')) as { + fixtures: { name: string; signature: string; body: string }[]; + }; + const f = vectors.fixtures.find((x) => x.name === 'time_only')!; + let err: unknown; + try { + decryptOnG2(h(f.signature), ciphertextFromBody(h(f.body))); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(IbeError); + expect([(err as IbeError).reason, (err as IbeError).message]).toEqual(['proof', 'ibe: the computation failed']); + }); +}); diff --git a/src/lib/dkc/ibe.test.ts b/src/lib/dkc/ibe.test.ts new file mode 100644 index 0000000..d753b96 --- /dev/null +++ b/src/lib/dkc/ibe.test.ts @@ -0,0 +1,235 @@ +// Tests of ibe.ts against the Go reference: src/lib/dkc/testing/ibe-vectors.json, +// written by scripts/ibe-go-vectors.go with drand/kyber encrypt/ibe, tlock and +// age, the libraries of the reference implementation. + +import { bls12_381 } from '@noble/curves/bls12-381.js'; +import { Decrypter, type Identity, type Stanza as AgeStanza } from 'age-encryption'; +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { parseAgeHeader } from './age.ts'; +import { splitCapsule } from './framing.ts'; +import { + ciphertextFromBody, + ciphertextToBody, + decryptOnG2, + gtBytes, + h2, + h3, + h4, + IbeError, + proofHolds, + roundIdentity, + TLOCK_BODY_LEN, + type Ciphertext, + type IbeReason, +} from './ibe.ts'; +import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts'; + +const { G1, G2, fields, pairing } = bls12_381; + +type Named = { name: string }; +type CiphertextVector = Named & { round: number; signature: string; u: string; v: string; w: string; go: 'ok' | 'reject'; msg?: string }; +const V = JSON.parse(readFileSync(new URL('./testing/ibe-vectors.json', import.meta.url), 'utf8')) as { + scheme: string; + public_key: string; + gt: (Named & { g1: string; g2: string; gt: string; h2_16: string; h2_32: string })[]; + h3: (Named & { sigma: string; msg: string; r: string; iterations: number })[]; + h4: { sigma: string; h4_16: string; h4_32: string }[]; + round_identities: { round: number; id: string }[]; + fixtures: (Named & { round: number; signature: string; body: string; gt: string; sigma: string; r: string; file_key: string })[]; + kyber: CiphertextVector[]; + decrypt: CiphertextVector[]; +}; +const BLS = JSON.parse(readFileSync(new URL('./testing/bls12381-vectors.json', import.meta.url), 'utf8')) as { + vectors: { label: string; group: 'G1' | 'G2'; hex: string; go: 'point' | 'identity' | 'invalid' }[]; +}; + +const ct = (v: CiphertextVector): Ciphertext => ({ U: h(v.u), V: h(v.v), W: h(v.w) }); +const scalarHex = (r: bigint): string => r.toString(16).padStart(64, '0'); +const xor = (a: Uint8Array, b: Uint8Array): Uint8Array => a.map((x, i) => x ^ b[i]!); +const timeOnly = V.fixtures.find((f) => f.name === 'time_only')!; +const PROOF = 'ibe: U is not r·G2: the ciphertext does not decrypt under this signature'; + +// Runs fn and returns the IbeError it throws. +function ibeError(fn: () => unknown, label: string): IbeError { + try { + fn(); + } catch (e) { + expect(e, label).toBeInstanceOf(IbeError); + return e as IbeError; + } + throw new Error(`${label}: no error`); +} + +describe('ibe.ts against the Go reference', () => { + it('reads the vectors of the Quicknet scheme and key', () => { + expect(V.scheme).toBe('bls-unchained-g1-rfc9380'); + expect(V.public_key).toBe(readJSON<{ public_key: string }>('vectors/profile_quicknet.json').public_key); + }); + + it('serializes GT in the order of kilic, which H2 hashes, and never in the order of noble', () => { + for (const v of V.gt) { + const gt = pairing(G1.Point.fromBytes(h(v.g1)), G2.Point.fromBytes(h(v.g2))); + expect(hx(gtBytes(gt)), v.name).toBe(v.gt); + expect(hx(h2(gt, 16)), v.name).toBe(v.h2_16); + expect(hx(h2(gt, 32)), v.name).toBe(v.h2_32); + // noble's Fp12.toBytes writes c0 first: another serialization. + expect(hx(fields.Fp12.toBytes(gt)), v.name).not.toBe(v.gt); + } + // The first vector is the one every implementation shares. + const shared = readJSON<{ vectors: { gt: string; h2: string }[] }>('vectors/tlock_ibe.json').vectors[0]!; + expect([V.gt[0]!.gt, V.gt[0]!.h2_16]).toEqual([shared.gt, shared.h2]); + }); + + it('computes H3 through its rejection sampling, and H4', () => { + for (const v of V.h3) { + const [sigma, msg] = [h(v.sigma), h(v.msg)]; + expect(scalarHex(h3(sigma, msg)), v.name).toBe(v.r); + expect(scalarHex(h3(sigma, msg, v.iterations)), v.name).toBe(v.r); + if (v.iterations > 1) { + const e = ibeError(() => h3(sigma, msg, v.iterations - 1), v.name); + expect([e.reason, e.message]).toEqual(['proof', 'ibe: no scalar r below the order of the group (rejection sampling failed)']); + } + } + expect(V.h3.map((v) => v.iterations)).toEqual(expect.arrayContaining([1, 2, 3])); + for (const v of V.h4) { + expect(hx(h4(h(v.sigma), 16))).toBe(v.h4_16); + expect(hx(h4(h(v.sigma), 32))).toBe(v.h4_32); + } + }); + + it('derives the identity of a round as drand does', () => { + for (const v of V.round_identities) expect(hx(roundIdentity(v.round)), String(v.round)).toBe(v.id); + for (const bad of [-1, 1.5, 2 ** 53, Number.NaN]) expect(() => roundIdentity(bad), String(bad)).toThrow(RangeError); + }); + + it('opens the tlock stanza of every official fixture with the file key of the reference', () => { + const names = listTestdata('fixtures', '.dkc').map((p) => p.replace(/^.*\//, '').replace(/\.dkc$/, '')); + expect(V.fixtures.map((f) => f.name).sort()).toEqual(names.sort()); + for (const f of V.fixtures) { + const stanza = parseAgeHeader(splitCapsule(readBytes(`fixtures/${f.name}.dkc`)).sealedControl!).stanzas[0]!; + expect([stanza.type, stanza.args[0], hx(stanza.body)], f.name).toEqual(['tlock', String(f.round), f.body]); + expect(readJSON<{ release: unknown }>(`fixtures/${f.name}.json`).release).toEqual({ round: f.round, signature: f.signature }); + const sig = h(f.signature); + const c = ciphertextFromBody(stanza.body); + expect(hx(decryptOnG2(sig, c)), f.name).toBe(f.file_key); + // The values in between, as the reference computes them. + const gt = pairing(G1.Point.fromBytes(sig), G2.Point.fromBytes(c.U)); + expect(hx(gtBytes(gt)), f.name).toBe(f.gt); + const sigma = xor(c.V, h2(gt, 16)); + expect(hx(sigma), f.name).toBe(f.sigma); + expect(xor(c.W, h4(sigma, 16)), f.name).toEqual(h(f.file_key)); + expect(scalarHex(h3(sigma, h(f.file_key))), f.name).toBe(f.r); + expect(hx(ciphertextToBody(c)), f.name).toBe(f.body); + } + }); + + it('opens OUTER_TIME_AGE of every fixture through age-encryption with that file key: header MAC and STREAM', async () => { + for (const f of V.fixtures) { + const sealed = splitCapsule(readBytes(`fixtures/${f.name}.dkc`)).sealedControl!; + const side = readJSON<{ access_policy: string; control_cbor: string }>(`fixtures/${f.name}.json`); + const open = async (edit: (key: Uint8Array) => Uint8Array): Promise => { + const identity: Identity = { + unwrapFileKey(stanzas: AgeStanza[]) { + const s = stanzas.find((x) => x.args[0] === 'tlock'); + return s === undefined ? null : edit(decryptOnG2(h(f.signature), ciphertextFromBody(s.body))); + }, + }; + const d = new Decrypter(); + d.addIdentity(identity); + return d.decrypt(sealed); + }; + const out = await open((key) => key); + // time_only seals CONTROL_CBOR; time_and_key, INNER_ACCESS_AGE. + if (side.access_policy === 'time_only') expect(hx(out), f.name).toBe(side.control_cbor); + else expect(new TextDecoder().decode(out.subarray(0, 22)), f.name).toBe('age-encryption.org/v1\n'); + // Another file key fails the header MAC. + await expect(open((key) => xor(key, new Uint8Array(16).fill(1))), f.name).rejects.toThrow(); + } + }); + + it('decrypts what kyber encrypts, for messages of 0 to 32 bytes', () => { + expect(V.kyber.map((v) => h(v.v).length)).toEqual([0, 1, 16, 32]); + for (const v of V.kyber) expect(hx(decryptOnG2(h(v.signature), ct(v))), v.name).toBe(v.msg ?? ''); + }); + + it('rejects what the reference rejects, with the reason of the first failing check', () => { + const want: Record = { + 'the time_only stanza': 'ok', + 'U with p added to c0': 'encoding', + 'U is the point at infinity': 'identity', + 'U negated': 'proof', + 'V with its first bit flipped': 'proof', + 'W with its last bit flipped': 'proof', + 'the signature of round 1001': 'proof', + 'the signature negated': 'proof', + 'the signature is the point at infinity': 'identity', + 'W one byte shorter than V': 'length', + 'V and W of 33 bytes': 'length', + }; + expect(V.decrypt.map((v) => v.name).sort()).toEqual(Object.keys(want).sort()); + for (const v of V.decrypt) { + expect(v.go === 'ok', v.name).toBe(want[v.name] === 'ok'); + if (v.go === 'ok') { + expect(hx(decryptOnG2(h(v.signature), ct(v))), v.name).toBe(v.msg); + continue; + } + const e = ibeError(() => decryptOnG2(h(v.signature), ct(v)), v.name); + expect(e.reason, v.name).toBe(want[v.name]); + if (e.reason === 'proof') expect(e.message, v.name).toBe(PROOF); + } + }); + + it('gates every encoding of the signature and of U as the Go reference decodes it', () => { + const c = ciphertextFromBody(h(timeOnly.body)); + const sig = h(timeOnly.signature); + const reason = { invalid: 'encoding', identity: 'identity', point: 'proof' } as const; + const seen = new Set(); + for (const v of BLS.vectors) { + const bytes = h(v.hex); + const e = ibeError(() => (v.group === 'G1' ? decryptOnG2(bytes, c) : decryptOnG2(sig, { ...c, U: bytes })), v.label); + // An encoding of another length is invalid for Go too; ibe.ts says so first. + const want = bytes.length === (v.group === 'G1' ? 48 : 96) ? reason[v.go] : 'length'; + expect(e.reason, v.label).toBe(want); + seen.add(e.reason); + } + expect([...seen].sort()).toEqual(['encoding', 'identity', 'length', 'proof']); + }); + + it('checks the lengths first', () => { + const c = ciphertextFromBody(h(timeOnly.body)); + const sig = h(timeOnly.signature); + const cases: [string, () => unknown, string][] = [ + ['a signature of 47 bytes', () => decryptOnG2(sig.subarray(1), c), 'ibe: the signature of 47 bytes, want 48'], + ['a signature of 49 bytes', () => decryptOnG2(new Uint8Array([...sig, 0]), c), 'ibe: the signature of 49 bytes, want 48'], + ['U of 95 bytes', () => decryptOnG2(sig, { ...c, U: c.U.subarray(1) }), 'ibe: U of 95 bytes, want 96'], + ['V longer than W', () => decryptOnG2(sig, { ...c, V: new Uint8Array(17) }), 'ibe: V of 17 bytes and W of 16, want equal lengths of at most 32'], + ['a body of 127 bytes', () => ciphertextFromBody(new Uint8Array(TLOCK_BODY_LEN - 1)), 'ibe: tlock stanza body of 127 bytes, want 128'], + ['a body of 129 bytes', () => ciphertextFromBody(new Uint8Array(TLOCK_BODY_LEN + 1)), 'ibe: tlock stanza body of 129 bytes, want 128'], + ['a body with V of 15 bytes', () => ciphertextToBody({ ...c, V: c.V.subarray(1) }), 'ibe: a tlock stanza body holds U of 96 bytes and V and W of 16'], + ['a body with W of 17 bytes', () => ciphertextToBody({ ...c, W: new Uint8Array(17) }), 'ibe: a tlock stanza body holds U of 96 bytes and V and W of 16'], + ['a body with U of 95 bytes', () => ciphertextToBody({ ...c, U: c.U.subarray(1) }), 'ibe: a tlock stanza body holds U of 96 bytes and V and W of 16'], + ]; + for (const [label, fn, message] of cases) { + const e = ibeError(fn, label); + expect([e.name, e.reason, e.message], label).toEqual(['IbeError', 'length', message]); + } + }); + + it('never proves with r = 0, which noble cannot multiply by', () => { + const u = G2.Point.fromBytes(h(timeOnly.body).subarray(0, 96)); + expect(proofHolds(0n, u)).toBe(false); + expect(proofHolds(BigInt(`0x${timeOnly.r}`), u)).toBe(true); + }); + + it('never puts a value of the computation in an error', () => { + // The messages are fixed by the reason and the lengths: the file key, + // sigma and r of the stanzas the edits start from appear in none. + const secrets = [timeOnly.file_key, timeOnly.sigma, timeOnly.r, timeOnly.signature, timeOnly.body.slice(0, 32)]; + for (const v of V.decrypt.filter((d) => d.go === 'reject')) { + const e = ibeError(() => decryptOnG2(h(v.signature), ct(v)), v.name); + for (const s of secrets) expect(e.message.toLowerCase(), v.name).not.toContain(s.slice(0, 16)); + expect(e.message, v.name).not.toMatch(/[0-9a-f]{16}/i); + } + }); +}); diff --git a/src/lib/dkc/ibe.ts b/src/lib/dkc/ibe.ts new file mode 100644 index 0000000..5fdcbb6 --- /dev/null +++ b/src/lib/dkc/ibe.ts @@ -0,0 +1,262 @@ +// The IBE-CCA of tlock on G2 (spec §63 step 11), for Quicknet +// (bls-unchained-g1-rfc9380): U lies on G2 and the decryption key of a round +// is its release signature on G1. It is DecryptCCAonG2 of drand/kyber +// encrypt/ibe, which tlock.TimeUnlock calls for this scheme, on +// @noble/curves 2.4.0 (plan of phase 2, section 4). +// +// Differences with DecryptCCAonG2, none of which accepts anything kyber +// rejects: +// - the signature and U must be the canonical encoding of a point of their +// subgroup, other than the point at infinity (spec §12.2), checked by +// bls12381.ts before noble decodes them; +// - a tlock stanza body is exactly U || V || W of 96 + 16 + 16 bytes (spec +// §63 step 11), as tlock.BytesToCiphertext requires; +// - the errors are IbeError, with a fixed reason and message: none carries +// sigma, the message, r or any input byte. kyber's error carries the +// candidate message and r, from which whoever edited a stanza learns the +// file key. +// +// H2 hashes the element of GT serialized in the order of kilic/bls12-381, c1 +// before c0 at every level of the tower, never with noble's Fp12.toBytes +// (testdata/vectors/tlock_ibe.json). sigma and the hashes derived from it are +// wiped once used, on every path. The caller wipes the message it gets, a +// file key. Values that noble keeps as bigints, such as r and the pairing, +// cannot be wiped. +// +// The structure follows crypto/ibe.ts of tlock-js 0.9.0 +// (https://github.com/drand/tlock-js, gitHead 17d817e), licensed under +// Apache-2.0 OR MIT and used here under the MIT License: +// +// Copyright (c) 2022 drand team +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { bls12_381 } from '@noble/curves/bls12-381.js'; +import { bytesToNumberBE } from '@noble/curves/utils.js'; +import { sha256 } from '@noble/hashes/sha2.js'; +import { checkCompressedPoint, type Group } from './bls12381.ts'; + +const { G1, G2, fields, pairing } = bls12_381; +const { Fp, Fr } = fields; + +/** Sizes of the compressed signature (G1) and of U (G2), and of V and W in a tlock stanza. */ +export const SIGNATURE_LEN = 48; +export const U_LEN = 96; +export const TLOCK_BLOCK_LEN = 16; +/** The tlock stanza body: U || V || W (spec §63 step 11). */ +export const TLOCK_BODY_LEN = U_LEN + 2 * TLOCK_BLOCK_LEN; +/** The longest message: the output of SHA-256, as kyber checks. */ +export const MAX_MESSAGE_LEN = 32; + +const tag = (s: string): Uint8Array => new TextEncoder().encode(s); +const H2_TAG = tag('IBE-H2'); +const H3_TAG = tag('IBE-H3'); +const H4_TAG = tag('IBE-H4'); +// The iterations of H3 end before 65535, as in kyber (a uint16 counter). +const H3_ITERATIONS = 65534; + +/** A tlock ciphertext: U compressed on G2; V and W as long as the message. */ +export interface Ciphertext { + readonly U: Uint8Array; + readonly V: Uint8Array; + readonly W: Uint8Array; +} + +/** Why a ciphertext does not decrypt. */ +export type IbeReason = 'length' | 'encoding' | 'identity' | 'proof'; + +/** A failure of the IBE. Its message is fixed by its reason and the value at fault. */ +export class IbeError extends Error { + readonly reason: IbeReason; + + constructor(reason: IbeReason, message: string) { + super(`ibe: ${message}`); + this.name = 'IbeError'; + this.reason = reason; + } +} + +type Fp12 = ReturnType; + +/** + * The 576 bytes of an element of GT in the order of kilic/bls12-381, which + * kyber-bls12381 marshals and H2 hashes: c1 before c0 in Fp12, c2, c1, c0 in + * each Fp6, c1 before c0 in each Fp2, each Fp in 48 big-endian bytes. + */ +export function gtBytes(gt: Fp12): Uint8Array { + const out = new Uint8Array(576); + let i = 0; + for (const a of [gt.c1, gt.c0]) { + for (const b of [a.c2, a.c1, a.c0]) { + for (const c of [b.c1, b.c0]) { + out.set(Fp.toBytes(c), i); + i += 48; + } + } + } + return out; +} + +// SHA-256 of the parts, truncated to n bytes; the digest is wiped. +function hashTo(n: number, ...parts: Uint8Array[]): Uint8Array { + const h = sha256.create(); + for (const p of parts) h.update(p); + const d = h.digest(); + const out = d.slice(0, n); + d.fill(0); + return out; +} + +/** H2: SHA-256("IBE-H2" || GT) truncated to n bytes, n at most 32. */ +export function h2(gt: Fp12, n: number): Uint8Array { + const b = gtBytes(gt); + try { + return hashTo(n, H2_TAG, b); + } finally { + b.fill(0); + } +} + +/** H4: SHA-256("IBE-H4" || sigma) truncated to n bytes, n at most 32. */ +export function h4(sigma: Uint8Array, n: number): Uint8Array { + return hashTo(n, H4_TAG, sigma); +} + +/** + * H3, the scalar r of sigma and the message: with base = SHA-256("IBE-H3" || + * sigma || msg), the first d = SHA-256(uint16le(i) || base), i = 1, 2, ..., + * whose top bit cleared makes it a big-endian integer below the order of + * the scalar field. `iterations` bounds i, for tests; after it, the proof + * fails, as in kyber. + */ +export function h3(sigma: Uint8Array, msg: Uint8Array, iterations = H3_ITERATIONS): bigint { + const base = hashTo(32, H3_TAG, sigma, msg); + try { + for (let i = 1; i <= iterations; i++) { + const d = hashTo(32, new Uint8Array([i & 0xff, i >> 8]), base); + d[0] = d[0]! >> 1; + const r = bytesToNumberBE(d); + d.fill(0); + if (r < Fr.ORDER) return r; + } + } finally { + base.fill(0); + } + throw new IbeError('proof', 'no scalar r below the order of the group (rejection sampling failed)'); +} + +/** + * Reports whether U = r·G2, the proof of the IBE-CCA. r = 0 never holds: U + * is never the point at infinity. noble's constant-time multiplication + * rejects 0, so it is checked first. + */ +export function proofHolds(r: bigint, u: InstanceType): boolean { + return r !== 0n && G2.Point.BASE.multiply(r).equals(u); +} + +/** The identity of a round for tlock: SHA-256 of its 8 big-endian bytes (drand DigestBeacon). */ +export function roundIdentity(round: number): Uint8Array { + if (!Number.isSafeInteger(round) || round < 0) throw new RangeError(`ibe: round ${round} is not a safe non-negative integer`); + const b = new Uint8Array(8); + new DataView(b.buffer).setBigUint64(0, BigInt(round)); + return sha256(b); +} + +/** Splits a tlock stanza body into U, V and W; its length must be TLOCK_BODY_LEN. */ +export function ciphertextFromBody(body: Uint8Array): Ciphertext { + if (body.length !== TLOCK_BODY_LEN) { + throw new IbeError('length', `tlock stanza body of ${body.length} bytes, want ${TLOCK_BODY_LEN}`); + } + return { + U: body.slice(0, U_LEN), + V: body.slice(U_LEN, U_LEN + TLOCK_BLOCK_LEN), + W: body.slice(U_LEN + TLOCK_BLOCK_LEN), + }; +} + +/** The tlock stanza body U || V || W of a ciphertext of a 16-byte message. */ +export function ciphertextToBody(ct: Ciphertext): Uint8Array { + if (ct.U.length !== U_LEN || ct.V.length !== TLOCK_BLOCK_LEN || ct.W.length !== TLOCK_BLOCK_LEN) { + throw new IbeError('length', `a tlock stanza body holds U of ${U_LEN} bytes and V and W of ${TLOCK_BLOCK_LEN}`); + } + const body = new Uint8Array(TLOCK_BODY_LEN); + body.set(ct.U); + body.set(ct.V, U_LEN); + body.set(ct.W, U_LEN + TLOCK_BLOCK_LEN); + return body; +} + +// Checks the canonical-encoding gate of a point; `name` is "the signature" +// or "U". +function gate(group: Group, bytes: Uint8Array, name: string): void { + const size = group === 'G1' ? SIGNATURE_LEN : U_LEN; + if (bytes.length !== size) throw new IbeError('length', `${name} of ${bytes.length} bytes, want ${size}`); + const verdict = checkCompressedPoint(group, bytes); + if (verdict === 'identity') throw new IbeError('identity', `${name} is the point at infinity`); + if (verdict === 'invalid') { + throw new IbeError('encoding', `${name} is not the canonical encoding of a point of the prime-order subgroup of ${group}`); + } +} + +const xor = (a: Uint8Array, b: Uint8Array): Uint8Array => a.map((x, i) => x ^ b[i]!); + +/** + * Decrypts `ct` with the round's signature, a compressed point of G1, and + * returns the message; the caller wipes it. Throws IbeError: `length` for a + * signature, U, V or W of the wrong length (V and W as long as each other, + * at most 32 bytes), `encoding` or `identity` for a signature or a U that is + * not the canonical encoding of a point of its subgroup other than the + * point at infinity, and `proof` when U is not r·G2, which covers a wrong + * signature and any edit of U, V or W. + */ +export function decryptOnG2(signature: Uint8Array, ct: Ciphertext): Uint8Array { + const { U, V, W } = ct; + gate('G1', signature, 'the signature'); + gate('G2', U, 'U'); + if (V.length !== W.length || W.length > MAX_MESSAGE_LEN) { + throw new IbeError('length', `V of ${V.length} bytes and W of ${W.length}, want equal lengths of at most ${MAX_MESSAGE_LEN}`); + } + let sigma: Uint8Array | undefined; + let mask: Uint8Array | undefined; + let msg: Uint8Array | undefined; + try { + const sig = G1.Point.fromBytes(signature); + sig.assertValidity(); + const u = G2.Point.fromBytes(U); + u.assertValidity(); + mask = h2(pairing(sig, u), W.length); + sigma = xor(V, mask); + mask.fill(0); + mask = h4(sigma, W.length); + msg = xor(W, mask); + if (!proofHolds(h3(sigma, msg), u)) throw new IbeError('proof', 'U is not r·G2: the ciphertext does not decrypt under this signature'); + const out = msg; + msg = undefined; + return out; + } catch (e) { + // The gate admits only points that noble decodes too; anything noble + // throws past it is a failure to decrypt, never a message. + throw e instanceof IbeError ? e : new IbeError('proof', 'the computation failed'); + } finally { + sigma?.fill(0); + mask?.fill(0); + msg?.fill(0); + } +} diff --git a/src/lib/dkc/testing/ibe-vectors.json b/src/lib/dkc/testing/ibe-vectors.json new file mode 100644 index 0000000..22633d5 --- /dev/null +++ b/src/lib/dkc/testing/ibe-vectors.json @@ -0,0 +1,291 @@ +{ + "description": "Go reference values of the tlock IBE-CCA on G2 (spec §63 step 11) for src/lib/dkc/ibe.ts; see scripts/ibe-go-vectors.go for how each block is obtained.", + "generator": "scripts/ibe-go-vectors.go", + "libraries": "filippo.io/age v1.3.2, github.com/drand/drand/v2 v2.1.7, github.com/drand/kyber v1.3.2, github.com/drand/kyber-bls12381 v0.3.4, github.com/drand/tlock v1.2.0", + "scheme": "bls-unchained-g1-rfc9380", + "public_key": "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a", + "gt": [ + { + "name": "e(G1, G2)", + "g1": "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb", + "g2": "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8", + "gt": "0f41e58663bf08cf068672cbd01a7ec73baca4d72ca93544deff686bfd6df543d48eaa24afe47e1efde449383b67663104c581234d086a9902249b64728ffd21a189e87935a954051c7cdba7b3872629a4fafc05066245cb9108f0242d0fe3ef03350f55a7aefcd3c31b4fcb6ce5771cc6a0e9786ab5973320c806ad360829107ba810c5a09ffdd9be2291a0c25a99a211b8b424cd48bf38fcef68083b0b0ec5c81a93b330ee1a677d0d15ff7b984e8978ef48881e32fac91b93b47333e2ba5706fba23eb7c5af0d9f80940ca771b6ffd5857baaf222eb95a7d2809d61bfe02e1bfd1b68ff02f0b8102ae1c2d5d5ab1a19f26337d205fb469cd6bd15c3d5a04dc88784fbb3d0b2dbdea54d43b2b73f2cbb12d58386a8703e0f948226e47ee89d018107154f25a764bd3c79937a45b84546da634b8f6be14a8061e55cceba478b23f7dacaa35c8ca78beae9624045b4b601b2f522473d171391125ba84dc4007cfbf2f8da752f7c74185203fcca589ac719c34dffbbaad8431dad1c1fb597aaa5193502b86edb8857c273fa075a50512937e0794e1e65a7617c90d8bd66065b1fffe51d7a579973b1315021ec3c19934f1368bb445c7c2d209703f239689ce34c0378a68e72a6b3b216da0e22a5031b54ddff57309396b38c881c4c849ec23e87089a1c5b46e5110b86750ec6a532348868a84045483c92b7af5af689452eafabf1a8943e50439f1d59882a98eaa0170f1250ebd871fc0a92a7b2d83168d0d727272d441befa15c503dd8e90ce98db3e7b6d194f60839c508a84305aaca1789b6", + "h2_16": "cb87319f24560b5231579a09ad79f12e", + "h2_32": "cb87319f24560b5231579a09ad79f12eb60956e693ebb0102a4fb12324c7f789" + }, + { + "name": "e(2·G1, G2), the square of e(G1, G2)", + "g1": "a572cbea904d67468808c8eb50a9450c9721db309128012543902d0ac358a62ae28f75bb8f1c7c42c39a8c5529bf0f4e", + "g2": "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8", + "gt": "079ab7b345eb23c944c957a36a6b74c37537163d4cbf73bad9751de1dd9c68ef72cb21447e259880f72a871c3eda1b0c017f1c95cf79b22b459599ea57e613e00cb75e35de1f837814a93b443c54241015ac9761f8fb20a44512ff5cfc04ac7f0f6b8b52b2b5d0661cbf232820a257b8c5594309c01c2a45e64c6a7142301e4fb36e6e16b5a85bd2e437599d103c3ace06d8046c6b3424c4cd2d72ce98d279f2290a28a87e8664cb0040580d0c485f34df45267f8c215dcbcd862787ab555c7e113286dee21c9c63a458898beb35914dc8daaac453441e7114b21af7b5f47d559879d477cf2a9cbd5b40c86becd071280900410bb2751d0a6af0fe175dcf9d864ecaac463c6218745b543f9e06289922434ee446030923a3e4c4473b4e3b1914081abd33a78d31eb8d4c1bb3baab0529bb7baf1103d848b4cead1a8e0aa7a7b260fbe79c67dbe41ca4d65ba8a54a72b61692a61ce5f4d7a093b2c46aa4bca6c4a66cf873d405ebc9c35d8aa639763720177b23beffaf522d5e41d3c5310ea3331409cebef9ef393aa00f2ac64673675521e8fc8fddaf90976e607e62a740ac59c3dddf95a6de4fba15beb30c43d4e3f803a3734dbeb064bf4bc4a03f945a4921e49d04ab8d45fd753a28b8fa082616b4b17bbcb685e455ff3bf8f60c3bd32a0c185ef728cf41a1b7b700b7e445f0b372bc29e370bc227d443c70ae9dbcf73fee8acedbd317a286a53266562d817269c004fb0f149dd925d2c590a960936763e519c2b62e14c7759f96672cd852194325904197b0b19c6b528ab33566946af39b", + "h2_16": "73e3dbd40bbe59ac85644aba27a08fc1", + "h2_32": "73e3dbd40bbe59ac85644aba27a08fc19183f393b830fab221565a43c65708af" + } + ], + "h3": [ + { + "name": "16 zero bytes each", + "sigma": "00000000000000000000000000000000", + "msg": "00000000000000000000000000000000", + "r": "1095e2f350a30d8b57ab5d35948ef05e1c8e32508d3f66873ed8ed95e965b64e", + "iterations": 1 + }, + { + "name": "32 bytes each", + "sigma": "abababababababababababababababababababababababababababababababab", + "msg": "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + "r": "0a1ddfa03dd187c9020bbbc712f9182472e7b0714a56c06707cdbca883a85180", + "iterations": 1 + }, + { + "name": "empty", + "sigma": "", + "msg": "", + "r": "70138cd56c5b0e6b00942408356c0b7c26b9893d0c2eb33510e0d2b9db78739d", + "iterations": 1 + }, + { + "name": "accepted at iteration 2 (sequence item 24)", + "sigma": "2a0e1f7caa8b0767d33890a394c460e3", + "msg": "72a72eda2b46bad46345cae2b80798ca", + "r": "7232567f27f5e7867a3382ff7d84a0490a4dcdd6a570f420bd51353e3f811326", + "iterations": 2 + }, + { + "name": "accepted at iteration 3 (sequence item 67)", + "sigma": "fdaa01708026990ff60ed74e8875f1b9", + "msg": "a2eefa73eaa7028f29dfddf86cb2a2bb", + "r": "6773be5dbb0ac8cbd3aebe463ff5911eee6d6d7f7b012de9f55382a3e99572da", + "iterations": 3 + } + ], + "h4": [ + { + "sigma": "00000000000000000000000000000000", + "h4_16": "e98934fb796adfa42b207a1b701a473d", + "h4_32": "e98934fb796adfa42b207a1b701a473dc4843617fe8bfa0c766c0fd767c3bd98" + }, + { + "sigma": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a", + "h4_16": "66e2e5ff21703ab44b70ad3b9368c406", + "h4_32": "66e2e5ff21703ab44b70ad3b9368c406cbed15e958df8f527895bf9abf9940f2" + } + ], + "round_identities": [ + { + "round": 1, + "id": "cd2662154e6d76b2b2b92e70c0cac3ccf534f9b74eb5b89819ec509083d00a50" + }, + { + "round": 1000, + "id": "f652498d092acd949bad74e40683bf3824fb817980504a0c7e6722cfc5a9c0a3" + }, + { + "round": 1001, + "id": "ce43c3353a7ad7aac3408cad0bf921b6a7dda89be75d9cb2b3b5a152cefc8afd" + }, + { + "round": 83903165811, + "id": "d2f715a9a98312047535c17e4822f1630cb75940d956840e711853b57cf222d7" + }, + { + "round": 9007199254740991, + "id": "6ebb1f681bf37ab86a120d042a9feab2875e0513104f6ca9676d6faa0570cedc" + } + ], + "fixtures": [ + { + "name": "empty_payload", + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "body": "85ab07e5665d20f28d1837af3986dd863299ceee1ad78abe6be8757f058f1048ad7eb28ff7600a1ff3208aec9ee2da6d0009dddbefaf0011d4127cc6e33e48921fafb2a04e7a4fe8c065d91509ea55409de0205b945671c68830e013aa83e3aed473119aec86a8961f991d1effb418075a5381f66948ce236989f8666bc087fd", + "gt": "033a5320b36bf6e32e6706a94adf2beded51adf74403c8e26787b79971d7378514f775510faa183303398a3dc7e53b5716bf0ddf3fb184df18061b9fd1739ce7d69d6a733be5898f2f3fab2886c017add04ab845af7ac27854eb9a4f146a155e10e23ca6b607107105ad0c68d803fb55f0f9c7bf4aa6dd036a93315bd83d4f00f8ea5971125bd984203f8de491287973158bcff089f4ef1638124eff025dd541f37ca52b5cfc12f54896067c4654a2efef3721466e50e63e208eba444dd2a7ba148716e56c75052dd0da1b545575fc2c18b7398e0596f083d46800f671d606a0a71348d17b6092143bb583d3e2f8e73b148df56784d4b3130ccc717e7ccfa13f6ffe66bf057ba1db61038c2089db273c45a90bf2bcf240d22640ecca4c0bb2de1788ccf0be8dda75ecfe520ea206460ee63e0d2c6b6ce02998e2acdca57b71aa6a2ae2abee9d765820f9595d04b5dca2097d2bcb09074af723bf0d9ac014279dfbf9e90a24c0730a38e5eec87b83fadcffd3d8209b088f23415a9285562733620dc68c956f3570309f004a9be7aca24c8566eb6b6a49489776b4921c053a1ef35a84c1ab5f27ffb5030ba10113afa763045460ba90bfbd7ad035b374a95834c29b83ddda4a413bc06d68cdaea11a2a4d0b791a026aba1c7edc776e716014443819e85989d503f82706aefed5901e69ca3101a597ca8bde764fc7b5067e972173584c8008cd1e6264cdd45793024a3ab7127eb8bf85564618ee589b25f78879200e48b977313798cb7b4664a3c626512a34cb29a796b7c8ad580179b49cc7ef5b", + "sigma": "67b8e06b25bfae89804daa7ace0e0ff5", + "r": "015c0a49507b1208268feac750c97a12b3d08bdd59e367f86b7579c9f07046e4", + "file_key": "6cce480b39dcea2918359a8e77cdedae" + }, + { + "name": "time_and_key_portable", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "body": "a37b9156ffa34b6618b2161a60dbf9a761b5206d5af3f941c73b722bb73c5359fc2da5b781ab84db0f36c2b71510a9e1067f4c287c408b4a0b14f754d5fd3b0509d161888a46c440edb6e99818f0068148d9fb4f50f99b1f8a2f4b567d9fabc055cc003e9d535125c0c2d350b47fa778dc09ac8e8f67d0b30cb4e97c12450179", + "gt": "0889bbf5f48555ce86a8383e97c37cd59d1e25509cd05d5fe716c266016640d558cc2d974bec714f6e510ef914f3c148043e4ea16243afef90094bef83a848b453be67a9dcb58727175a9dc780afda04ab2febd70f4ea6ca8db88f8032a4ec6d19292cd399722067232eadc7ce1d835afad52f6f4211693289fc5b1e0bab29afe8d297bbd60d4d3b18c854f8b6dfc249092e8e9387f62f7707ffbe5c92a83869d6caf4d748660e6fc0c037ef85595984cedf95fe053bcdb885d7302b73f62f3316c0d93eb768471df130e364e2c0ecb0a013587633cce561b188441cc5ed4d79cb9cada8bb0b36fc26fbac6be012dae707361ab567574500724213d211c3b3ba540415ece53a06a918ea3c34ee24b28463293cd604a676061532aedb73efdfd706331afd5dae971e2c16ed7a55ea2fea3ae7fc19f729b73b01f04a69e407d38643ae710c710dddd19ccf3fbb02a9f7500b6969d08cbce9c1e5dd8625f6bcb2a096c85bf2197a8972f66ce1a7170615f417f14bce2d3cce5ea13c7041e1f3c7a709a6724b9391abb6a12039798c73b971d5ab15f14143e60797e1f2cd9a3dd76087070256d0e7a927076ec5d54ecc565219bfca4b84f43fb9ed20e8293ab9ca93860a58b691862418d923dae9dcb88ffc83c6a97d34000b6f696d2ce0cf544695072ad7a618e84de85811bfe26e082da2a614bd201413edae8b6f0b5c42a217a4816c0c803d237a6de7855b3dc38917f4063e45b40b5dd2412fc77aa05f582bd9e61713f985b447096f8ee4fab2da3bc787dc9450230265b84425a8d6a62dbd07", + "sigma": "8558300932ade6715f84c5f476d6f10f", + "r": "00f39949c78198542ecb3e6d0017e984ef548faab2ddffdc960597cf7fb75898", + "file_key": "bd1c70394ef8c69e0660aa2e517ed1ca" + }, + { + "name": "time_and_key_recipients", + "round": 1001, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "body": "b81e4e55a134184eb2121c9fed89a854e5821740767332c0662cb70866c9ff322ad4ebc9e4ee54609eee211d6669ce2e112e85d4d8db4764cfc18de7cc554678ebef0c09bc029496d283745b69a24f93dab5aeb98a55ae6e184fb5fb52f2297da52eed2b01c7c25a7af85fe8aa908ccf0c5da33a8ba3d75868bb3f0f14253fdf", + "gt": "10a0bdb058ed5207eeff0c0b2f5772b182e5c2fdd24953666a62e2de5bde0905c9d11fe73a2dafdf3bc8f527a1373919164277af5ba63c30df08ddb7ab1c90200e2975d3502ebbcceca43291f918378f4688258fced78d67f764bb6b9f894e7c0bfb12c2f6e3b14cf7ccccda5cdb28a574f9c79d982c30e492049482cd306be027641a6761f7a4b32c30b6945d3630aa100fe90d3b0ed998c91369c272f0c14eef6cb63434d4773906368dd6b69660332114ef3ca7ac5f9c245f9d45b76505f707b9946dc1da0f05a4de0d972eed0c4eefba67a92fe5869221d11e002bb824038ec96201505aa8fc92bb2df8722030ad0319b77c4fb91b847efe3b926d5d8d692d786f5ddb55dd2a269a153a931371176fa5f3e2a4cc63531ee8acc7b96f3e600e5817019150eb393ac5f246ca198d8023712859d6a6d3d87917fbbf2a3b679c98fc7581ea4fcfba5a732ab5c1b833e80ccba7e3b0905c55fbd71fc1871bb027085e11898711345e2b034578faa2b5970373c7540a9ee9b3133b3e5b531a51d60fa3bb6d05482d06792b76cf6824d9e551e739800f7ee5f1e0c01529db5a5f957ef527b42a32b557f98a467581d9ef49013e01fb37f9fd9bef1005c80e9c7239fa235796c01e41c2a90659571c16a617862ba935a32668b03e297f866c010c7d1917a11deead3d281cf594472c9d37572c31215f734244319cbf419dda60c0b4c60789a91caa6710c2186ec0ad5e16600a2758ab73765cea974e3fd86455e126f16e748f297a0aebe2145359de1ef052a304e8e67ab809fd34c944cb41f6e498", + "sigma": "871f6a3d5028e727597eddd1a306adbd", + "r": "2b8ab95abc6b92c497650b4a87f499d252b64eacab5f0388233e589046920c3f", + "file_key": "d0a706c871a65f4690e3737227720f7f" + }, + { + "name": "time_only", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "body": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c6c26076e7a50eae51ac174e025205dc073ebd97ccd56cdb81065c8fbabfb502c", + "gt": "123f4da429bf059c0f67d365508c23a211c91887ef8e12300653ca69b041141c782c917079fc6aaaab00c23411affb06192b9a2d994cf4fb35b95ed10730e91d30ad89155bcca5f314bf02a0e9a966e4b4cbbfb5f0c1ad6a40a7e8be25bc47cd12baed915725d78041253c84d626ee149fb4eb4d5fed6c043ddc18a840bf79fcda71e5fb23efa0ebf886f5a45f1e0377121da9c8c199b9cfed2a20c867bc15ecbf1058b2a194b44920cd27f069254dd961a8b88d69d7c093362fe7d98b210b7a0c5e704227d27ae6683009b0de88fa5ea4e1259eff5a05017c3703f65b656a05ca3a237efbbcd29eb55a2a5a1d658d6c1035e54af149271a1638ae68dcf34699ac850b555952718b7e852a3a5433b5b8c5bb0a3f4f9df09b43069e6854e51558015af9892ef2be85ce8e7c4695ac83eb8fb1a83de80969f155efd81b72361667dbfc552c1c94183b28920ca38d193ed814f4dfaaa3319dc91c2723be7a10dabc1f45e905b4693c100a3203644f7ae95ec1e1100c05729072d61f38ccd7d3db2510f4eeafefda4be8cf2094657faf87e4bed175c6f9f660bc6bd3897563b5891393ae5fe5a87f83bdd6139716110262fb0392758e8be6d3f74315e376c9248fc70ada39aecd2c8120ee6e70b1a9d784564236916bca0881214a1b34b0b259c17918521964b4307335323d8cb6fabdec708c52e42f6990042d3a3a95fce7bb6c48d24d0732eced009e672176fbc9380eff129f664c8634923c1f4b2973eb909f36012a3029122b3733edc2265b6389de56fe4ee3d7534b9acdc4aa166f3e8e94e1", + "sigma": "0028db16b378ebdaf1a95acd61877783", + "r": "520e6f605ac31bfc613f8ba7e35a4ee89652d9ecae2b905f17caa7c47d788869", + "file_key": "684bfc20912fb50a1a7f1b644e1f6f52" + }, + { + "name": "time_only_extensions", + "round": 2000, + "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e", + "body": "b14321735db0542f13770d3743e6bcf20b7489184276242f292d927e57cd635c11d44a2a62e3c4e573b4cd693ac977de1880ea98a63e082b33f0dd0a3dd9d332da04c54221451e9f8990ae0ecc01a6deff4e3d6df23499bcf794e33ae33dd5ee62dc47d9cabdc60591c294e4d6ca2fccff19da312848f77b71eb796045cfc660", + "gt": "0903f246d6751204b3c909175f9eea50d1c6ab235ee4c8493a84855b0fdf8d411b809acc3fbb210b651be539d8df11280e1184d9cc173c537921d970eb89db71dc492322108864f4ad0aef003b33e7ab93490caa83d6127392050237596de8ce0d8fb93bcddd04e6f99e0f64c135eb2b8e54b453aa04082f697ce64aefbc5c3c4bfe6d670708d67662874b3f7e4137b805df11bf21fccbc8340406a872936574ab378e9b8e362d091bef2a8013dedd6e520ef4bac5a9369b4a7d095d86b2d574043124283fa54e6be3c7a2b035f7cd61ee077ea5667554850ebce9a1e87f228e01da13dd73edb353c107c50bc6f8496d15680b646ab8fcde36e5b0900286eab861b596dffae7b46e5707d649f27a3d5212e942a83d6c186b4c24139163dccec2156e5a809557c229a529152360114846b1a58e2cb58aa7483a7d016cbace88eed14d4ad0130fb68f3d1f25a50417237c18d5a2596d851c3ed57c6c6dada06feefb9c359d3131c41108032ae55f599ab4da82d5017f483f439a26ce88ef0e96eb0c527b1d67fa0e31c815558572ba66709399443e19bfbcd1d0b6aa111525b49f955debd6a089aabfdf58a89d7c2436c512ab24d4a0bc3be1f0ce2f382b155e42bb1a3465e890f3009e73750e66bd84cfcc79c1ca1cfed695cb9fbf3432db92841897a510bd8dd9369132d953609705494d7cf2f69be2831764c751b2d089e5df6001ff9c43f65cc257fab5230ccc310d04b52d428e5073a378228f2be742def69f7e8d52560f20d5c6a7507340346bdac4cbc2bf7ce3d117ef08e9ee1c0b7533", + "sigma": "eecb8e723ec29600088e162d5b27c5ee", + "r": "5b6076213aeeaa8eb014c104171f6641c844788766b734fb15f2c5a4be3419cf", + "file_key": "5be87729206e4f7b5f344287a377fd7b" + } + ], + "kyber": [ + { + "name": "a 0-byte message", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "8e5323386dde85946358148a71f4471948cd8e77b34750138a3cbad5a74cf5c7ff124a3e95c98ed34ec0ad5db411b0d20ab6d1638c51a62f7e68bdb8326dc5786a7baeb80c47d3701914a77af36b3d882d4746184d78589f51feacca232bed95", + "v": "", + "w": "", + "go": "ok" + }, + { + "name": "a 1-byte message", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "83a4ab1705c1765a1cd1ea44bcc4bfaf08f39912cf21f985a76d0e3e5abcd2ccfaaa740350c6efd684ec1955b1e7086e0f3f4ef5e6569dc5804bf2129d56ff5384df0b9bdb73f991b3b28bfd78b02a442ccd919abd9dfb0f7b62c7794bef2092", + "v": "37", + "w": "7d", + "go": "ok", + "msg": "6c" + }, + { + "name": "a 16-byte message", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "b5fc4ddbbbb4f055d996ee177c3bd8f2ab3b752a86882b2f971376a76ac9b0bbff9fc395e8be090972a20eef7a42301f0958429c32746ac5a27f3b9b7f6f036cd0ef26e17f42e5f2dddbb551b0ed2eb9a0c276ead0abdcb6c5b2418d1c7c9020", + "v": "f12464d1b9867b1add515f800e40770a", + "w": "50fdebae579e4e4d44c1f2db4f523629", + "go": "ok", + "msg": "6c8908a8bca467ce306844af509353fe" + }, + { + "name": "a 32-byte message", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "a0966fbdd8e1a3a68f03a0aebfdb85968cfd56041261d5e317bb3fd5b01961f68aa54d9d0fa2057e2d48d8a3e25e02cb0e6d622d6e73963d227d58521475279106f37d17897bcb9a03c69656b3f9520c6317e29b0b068b0b58f79e5809d9dc4a", + "v": "24e5ed8f36a1b9bcca2404432fb20b61b799b5dbd764a016b220fd75dff9e554", + "w": "e8e4b501c753b12fdde90155124e8a4e6366752109b5c77cf99b2d522bcf6d9e", + "go": "ok", + "msg": "6c8908a8bca467ce306844af509353fec59698e0870aa80cea87ea7fb2c1c3d0" + } + ], + "decrypt": [ + { + "name": "the time_only stanza", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "ok", + "msg": "684bfc20912fb50a1a7f1b644e1f6f52" + }, + { + "name": "U with p added to c0", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe309482bdb770815acd72eacc4f8bd69654af04c8f29afb29206e0909171fd9bf7a9fb5eb7378a3b3f114875717605ff9fb3e7", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "reject" + }, + { + "name": "U is the point at infinity", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "reject" + }, + { + "name": "U negated", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "a08f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "reject" + }, + { + "name": "V with its first bit flipped", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "ec26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "reject" + }, + { + "name": "W with its last bit flipped", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502d", + "go": "reject" + }, + { + "name": "the signature of round 1001", + "round": 1000, + "signature": "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "reject" + }, + { + "name": "the signature negated", + "round": 1000, + "signature": "944679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "reject" + }, + { + "name": "the signature is the point at infinity", + "round": 1000, + "signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb502c", + "go": "reject" + }, + { + "name": "W one byte shorter than V", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc0", + "w": "73ebd97ccd56cdb81065c8fbabfb50", + "go": "reject" + }, + { + "name": "V and W of 33 bytes", + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39", + "u": "808f8eead90f3eebee2e6e15d490a2ab973214350d179d9c29ffc804e612f8fdd45bc3d2acf8552c2b3bb6666fe3094811da651ddc2cf09461a9510726199e18e817de2abf0cf42129609f5ca546b3d7400b378b89eb1148bb727605ffa0093c", + "v": "6c26076e7a50eae51ac174e025205dc06c26076e7a50eae51ac174e025205dc000", + "w": "73ebd97ccd56cdb81065c8fbabfb502c73ebd97ccd56cdb81065c8fbabfb502c00", + "go": "reject" + } + ] +} diff --git a/vitest.config.ts b/vitest.config.ts index f6caf2b..754e724 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,8 +15,10 @@ export default defineConfig({ exclude: ['src/lib/**/*.test.ts', 'src/lib/dkc/testing/**', 'src/lib/dkc/index.ts'], reporter: ['text', 'html', 'json-summary'], thresholds: { - // The codec is covered completely (plan §7). + // The codec is covered completely (plan §7), and so is the IBE (plan + // of phase 2, section 10, step 3). 'src/lib/dkc/cbor.ts': { 100: true }, + 'src/lib/dkc/ibe.ts': { 100: true }, 'src/lib/dkc/**/*.ts': { statements: 95, branches: 90, functions: 95, lines: 95 }, // The page model and helpers of the inspector (plan §8, phase 1). 'src/lib/inspector/**/*.ts': { statements: 95, branches: 90, functions: 95, lines: 95 },