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.
282 lines
9.2 KiB
282 lines
9.2 KiB
//go:build ignore
|
|
|
|
// Prints test/vectors/tlock_vectors.json: the Go reference values for the
|
|
// tlock encryption of lib/src/ibe.dart and lib/src/tlock.dart, a port of
|
|
// scripts/tlock-go-vectors.go of datekeys-ts.
|
|
//
|
|
// - encrypt: EncryptCCAonG2 of drand/kyber with the suite of tlock for
|
|
// Quicknet, restated with a fixed sigma, for messages of 0, 1, 16 and 32
|
|
// bytes to rounds 1000 and 1001. kyber draws sigma from crypto/rand, so
|
|
// the restatement is checked: ibe.DecryptCCAonG2 opens every ciphertext
|
|
// with the published signature of its round, and tlock.BytesToCiphertext
|
|
// with tlock.TimeUnlock opens the 16-byte ones, as a tlock stanza body.
|
|
// - interop: the ciphertexts that scripts/tlock-ts-samples.mjs of
|
|
// datekeys-ts made with the TypeScript library, frozen in its
|
|
// src/lib/dkc/testing/tlock-vectors.json at 289fe71, each opened again by
|
|
// the reference here. An IBE body goes through tlock.BytesToCiphertext and
|
|
// tlock.TimeUnlock; an age file through age.Decrypt with
|
|
// agewrap.NewTimeIdentity, the identity of capsule.Open at step 11. Each
|
|
// sample gets the verdict "ok" with what Go recovered, or "reject".
|
|
//
|
|
// H2, H3 and H4 are unexported in kyber; they are restated with its tags, as
|
|
// in tool/ibe_go_vectors.go, and the decryptions above check them.
|
|
//
|
|
// Run it from a scratch module that requires the reference implementation at
|
|
// spec-v0.11 (a module scratch whose go.mod has require
|
|
// g.activething.com/go/DateKeys v0.0.0 and replace
|
|
// g.activething.com/go/DateKeys => an export of that tag, with its go.sum,
|
|
// and GOFLAGS=-mod=mod), copied into it, passing the frozen file of
|
|
// datekeys-ts:
|
|
//
|
|
// go run tlock_go_vectors.go path/to/tlock-vectors.json > tlock_vectors.json
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"os"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
|
|
"filippo.io/age"
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
"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 signatures of rounds 1000 and 1001, as in the
|
|
// fixtures and the mutation corpus; provider.Verify checks them below.
|
|
var published = map[uint64]string{
|
|
1000: "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39",
|
|
1001: "b33bf3667cbd5a82de3a24b4e0e9fe5513cc1a0e840368c6e31f5fcfa79bea03f73896b25883abf2853d10337fb8fa41",
|
|
}
|
|
|
|
var (
|
|
suite = bls.NewBLS12381Suite()
|
|
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 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
|
|
}
|
|
|
|
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]
|
|
}
|
|
|
|
func h3(sigma, msg []byte) kyber.Scalar {
|
|
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 {
|
|
r := suite.G1().Scalar()
|
|
if err := r.UnmarshalBinary(d[:]); err != nil {
|
|
panic(err)
|
|
}
|
|
return r
|
|
}
|
|
}
|
|
panic("h3: rejection sampling failed")
|
|
}
|
|
|
|
// encrypt is EncryptCCAonG2 of kyber with the given sigma.
|
|
func encrypt(key kyber.Point, id, msg, sigma []byte) *ibe.Ciphertext {
|
|
qid := suite.G1().Point().(kyber.HashablePoint).Hash(id)
|
|
gid := suite.Pair(qid, key)
|
|
r := h3(sigma, msg)
|
|
gt := must(suite.GT().Point().Mul(r, gid).MarshalBinary())
|
|
return &ibe.Ciphertext{U: suite.G2().Point().Mul(r, nil), V: xor(sigma, h2(gt, len(msg))), W: xor(msg, h4(sigma, len(msg)))}
|
|
}
|
|
|
|
type encryptVector struct {
|
|
Name string `json:"name"`
|
|
Round uint64 `json:"round"`
|
|
ID string `json:"id"`
|
|
Msg string `json:"msg"`
|
|
Sigma string `json:"sigma"`
|
|
U string `json:"u"`
|
|
V string `json:"v"`
|
|
W string `json:"w"`
|
|
Signature string `json:"signature"`
|
|
}
|
|
|
|
type sample struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Round uint64 `json:"round"`
|
|
FileKey string `json:"file_key,omitempty"`
|
|
Body string `json:"body,omitempty"`
|
|
Plaintext string `json:"plaintext,omitempty"`
|
|
File string `json:"file,omitempty"`
|
|
Go string `json:"go"`
|
|
GoResult string `json:"go_result,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)
|
|
}
|
|
release := func(round uint64) provider.Release {
|
|
r := provider.Release{Round: round, Signature: unhex(published[round])}
|
|
if err := provider.Verify(quicknet, provider.Condition{Round: round}, r); err != nil {
|
|
panic(err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
var vectors []encryptVector
|
|
for i, c := range []struct {
|
|
round uint64
|
|
n int
|
|
}{{1000, 16}, {1001, 16}, {1000, 1}, {1000, 32}, {1000, 0}} {
|
|
seed := sha256.Sum256(binary.BigEndian.AppendUint32([]byte("DateKeys tlock vector "), uint32(i)))
|
|
msgSeed := sha256.Sum256(seed[:])
|
|
msg, sigma := msgSeed[:c.n], seed[:c.n]
|
|
id := scheme.DigestBeacon(&common.Beacon{Round: c.round})
|
|
ct := encrypt(key, id, msg, sigma)
|
|
rel := release(c.round)
|
|
sig := scheme.SigGroup.Point()
|
|
if err := sig.UnmarshalBinary(rel.Signature); err != nil {
|
|
panic(err)
|
|
}
|
|
if got, err := ibe.DecryptCCAonG2(suite, sig, ct); err != nil || !bytes.Equal(got, msg) {
|
|
panic(fmt.Sprintf("kyber does not decrypt the restated encryption %d: %v", i, err))
|
|
}
|
|
u := must(ct.U.MarshalBinary())
|
|
if c.n == 16 {
|
|
body := concat(u, ct.V, ct.W)
|
|
got := must(tlock.TimeUnlock(*scheme, key, common.Beacon{Round: rel.Round, Signature: rel.Signature}, must(tlock.BytesToCiphertext(*scheme, body))))
|
|
if !bytes.Equal(got, msg) {
|
|
panic("tlock does not decrypt the restated encryption")
|
|
}
|
|
}
|
|
vectors = append(vectors, encryptVector{
|
|
Name: fmt.Sprintf("a %d-byte message for round %d", c.n, c.round), Round: c.round, ID: hex.EncodeToString(id),
|
|
Msg: hex.EncodeToString(msg), Sigma: hex.EncodeToString(sigma), U: hex.EncodeToString(u),
|
|
V: hex.EncodeToString(ct.V), W: hex.EncodeToString(ct.W), Signature: published[c.round],
|
|
})
|
|
}
|
|
|
|
var frozen struct {
|
|
Interop struct {
|
|
Generator string `json:"generator"`
|
|
Samples []sample `json:"samples"`
|
|
} `json:"interop"`
|
|
}
|
|
if err := json.Unmarshal(must(os.ReadFile(os.Args[1])), &frozen); err != nil {
|
|
panic(err)
|
|
}
|
|
in := frozen.Interop
|
|
if len(in.Samples) != 4 {
|
|
panic("want the 4 frozen samples of datekeys-ts")
|
|
}
|
|
for i := range in.Samples {
|
|
s := &in.Samples[i]
|
|
rel := release(s.Round)
|
|
s.Go = "reject"
|
|
switch s.Kind {
|
|
case "ibe":
|
|
ct, err := tlock.BytesToCiphertext(*scheme, unhex(s.Body))
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", s.Name, err)
|
|
continue
|
|
}
|
|
fk, err := tlock.TimeUnlock(*scheme, key, common.Beacon{Round: rel.Round, Signature: rel.Signature}, ct)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", s.Name, err)
|
|
continue
|
|
}
|
|
s.Go, s.GoResult = "ok", hex.EncodeToString(fk)
|
|
case "age":
|
|
id := must(agewrap.NewTimeIdentity(quicknet, s.Round, rel))
|
|
r, err := age.Decrypt(bytes.NewReader(unhex(s.File)), id)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", s.Name, err)
|
|
continue
|
|
}
|
|
pt, err := io.ReadAll(r)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%s: %v\n", s.Name, err)
|
|
continue
|
|
}
|
|
s.Go, s.GoResult = "ok", hex.EncodeToString(pt)
|
|
default:
|
|
panic("unknown sample kind " + s.Kind)
|
|
}
|
|
}
|
|
|
|
out := map[string]any{
|
|
"description": "Go reference values for the tlock encryption of lib/src/ibe.dart and lib/src/tlock.dart; " +
|
|
"see tool/tlock_go_vectors.go for how each block is obtained.",
|
|
"generator": "tool/tlock_go_vectors.go",
|
|
"interop_from": "the interop samples of src/lib/dkc/testing/tlock-vectors.json of datekeys-ts at 289fe71, " +
|
|
"opened again by this generator",
|
|
"libraries": libraries(),
|
|
"scheme": scheme.Name,
|
|
"public_key": hex.EncodeToString(quicknet.PublicKey),
|
|
"encrypt": vectors,
|
|
"interop": map[string]any{"generator": in.Generator, "samples": in.Samples},
|
|
}
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
enc.SetEscapeHTML(false)
|
|
if err := enc.Encode(out); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// 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, ", ")
|
|
}
|