Phase 2, step 3: the tlock IBE on noble 2, checked against Go

src/lib/dkc/ibe.ts is DecryptCCAonG2 of drand/kyber encrypt/ibe, the
decryption of tlock.TimeUnlock for Quicknet, on @noble/curves 2.4.0
(plan of phase 2, section 4). It adds nothing that kyber would reject:
- the signature and U pass the canonical-encoding gate of bls12381.ts,
  which rejects the point at infinity too;
- H2 hashes GT in the order of kilic, never with noble's Fp12.toBytes;
- H3 and H4 follow kyber, including the rejection sampling of r, and
  r = 0 never proves;
- roundIdentity is drand's DigestBeacon;
- the stanza body is exactly U || V || W, 128 bytes, as in tlock.
Errors are IbeError with a fixed reason (length, encoding, identity,
proof) and message: none carries sigma, the message, r or input bytes.
Anything noble throws past the gate is a proof failure. sigma and the
hashes derived from it are wiped on every path. The file keeps the MIT
notice of tlock-js, whose structure it follows. index.ts does not
re-export it yet; the opening of step 5 will use it.

scripts/ibe-go-vectors.go writes src/lib/dkc/testing/ibe-vectors.json
with kyber, tlock and age:
- the GT of e(G1, G2) and of its square, with H2;
- H3, including inputs accepted at the second and third iteration, and
  H4;
- round identities;
- for the tlock stanza of every official fixture, the pairing, sigma, r
  and the file key. tlock.TimeUnlock unwraps that file key, and age
  opens OUTER_TIME_AGE with it;
- messages of 0, 1, 16 and 32 bytes encrypted by EncryptCCAonG2;
- kyber's verdict on eleven edited copies of the time_only stanza.
It restates the unexported H2, H3 and H4 and checks them on every
fixture against tlock and U = r·G2.

ibe.test.ts replays every vector and opens OUTER_TIME_AGE of each
fixture through age-encryption with a custom Identity: the header MAC
and STREAM verify, and a time_only fixture yields its control_cbor. It
also gates all 157 BLS edge encodings as the Go reference decodes them
and checks the fixed error texts. ibe.failure.test.ts mocks a noble
failure. Coverage of ibe.ts is 100 % and is now a threshold. The site
does not change.

npm run verify is green: 2,397 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 1 week ago
parent ea6dc77d35
commit 35be27cf92

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

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

@ -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, ", ")
}

@ -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<typeof import('@noble/curves/bls12-381.js')>();
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']);
});
});

@ -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<Uint8Array> => {
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<string, IbeReason | 'ok'> = {
'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<IbeReason>();
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);
}
});
});

@ -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<typeof pairing>;
/**
* 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<typeof G2.Point>): 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);
}
}

@ -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"
}
]
}

@ -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 },

Loading…
Cancel
Save

Powered by TurnKey Linux.