You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dateKeys-dart/tool/ibe_go_vectors.go

437 lines
16 KiB

//go:build ignore
// Prints test/vectors/ibe_vectors.json: the Go reference values of the tlock
// IBE-CCA on G2 of lib/src/ibe.dart (spec §63 step 11), a port of
// scripts/ibe-go-vectors.go of datekeys-ts. 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 of testdata/
// (the branch v0.12 at c531e93), 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. They are not
// reproducible: they are the frozen ones of
// src/lib/dkc/testing/ibe-vectors.json of datekeys-ts at 289fe71, which
// this program reads and decrypts again with ibe.DecryptCCAonG2; a
// mismatch panics.
// - 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.
//
// Run it in the module of the reference implementation, which it imports,
// without changing anything there, from the datekeys-go next to this
// repository: at the tag spec-v0.11 or at the draft v0.12, whose packages
// that it uses are the same, and so is the output.
//
// cd ../datekeys-go && go run ../datekeys-dart/tool/ibe_go_vectors.go \
// ../datekeys-dart/testdata/fixtures \
// ../datekeys-ts/src/lib/dkc/testing/ibe-vectors.json \
// > ../datekeys-dart/test/vectors/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"`
KyberFrom string `json:"kyber_from"`
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 lib/src/ibe.dart; " +
"see tool/ibe_go_vectors.go for how each block is obtained.",
Generator: "tool/ibe_go_vectors.go",
KyberFrom: "the kyber section of src/lib/dkc/testing/ibe-vectors.json of datekeys-ts at 289fe71, decrypted again by this generator",
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: the frozen ones of datekeys-ts,
// decrypted again.
sig1000 := unhex(timeOnly.Signature)
var frozen struct {
Kyber []ciphertextVector `json:"kyber"`
}
if err := json.Unmarshal(must(os.ReadFile(os.Args[2])), &frozen); err != nil {
panic(err)
}
for i, f := range frozen.Kyber {
msg := sha256.Sum256([]byte("DateKeys IBE vector message"))
n := []int{0, 1, 16, 32}[i]
v := verdict(scheme, f.Name, f.Round, unhex(f.Signature), unhex(f.U), unhex(f.V), unhex(f.W))
if f.Round != 1000 || f.Signature != timeOnly.Signature || f.Go != "ok" || v.Go != "ok" || v.Msg != f.Msg || v.Msg != hex.EncodeToString(msg[:n]) {
panic("kyber does not decrypt the frozen ciphertext " + f.Name)
}
out.Kyber = append(out.Kyber, v)
}
if len(out.Kyber) != 4 {
panic("want the 4 frozen kyber ciphertexts")
}
// 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})},
{"V and W empty", sig1000, u, nil, nil},
{"U is the generator of G2", sig1000, unhex(marshal(suite.G2().Point().Base())), vv, w},
} {
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, ", ")
}

Powered by TurnKey Linux.