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.
341 lines
13 KiB
341 lines
13 KiB
package testkit
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"slices"
|
|
"strconv"
|
|
|
|
"filippo.io/age"
|
|
"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/kyber/pairing"
|
|
"github.com/drand/tlock"
|
|
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
)
|
|
|
|
// TlockStepsFile is testdata/vectors/tlock_steps.json: the intermediate
|
|
// values of steps 10 and 11 of spec §63 for Quicknet, over published
|
|
// releases. Step 10: the message of a round, its hash to G1 and the pairing
|
|
// check of the signature. Step 11: the decryption of a tlock stanza, H2, H4,
|
|
// the file key and H3 with each of its tries.
|
|
type TlockStepsFile struct {
|
|
Spec string `json:"spec"`
|
|
Description string `json:"description"`
|
|
Profile string `json:"profile"`
|
|
Scheme string `json:"scheme"`
|
|
ChainHash string `json:"chain_hash"`
|
|
PublicKey string `json:"public_key"`
|
|
DST string `json:"dst"`
|
|
Tags TlockStepTags `json:"tags"`
|
|
Vectors []TlockStepVector `json:"vectors"`
|
|
}
|
|
|
|
// TlockStepTags are the domain separation tags of H2, H3 and H4, in hex.
|
|
type TlockStepTags struct {
|
|
H2 string `json:"h2"`
|
|
H3 string `json:"h3"`
|
|
H4 string `json:"h4"`
|
|
}
|
|
|
|
// TlockStepVector is one round and one stanza of it. Every byte string is
|
|
// hex. The writer chose sigma and file_key; the rest follows from them, the
|
|
// pinned public key and the signature of the round.
|
|
type TlockStepVector struct {
|
|
Name string `json:"name"`
|
|
Round uint64 `json:"round"`
|
|
Signature string `json:"signature"` // the release, compressed G1
|
|
Message string `json:"message"` // M = SHA-256(uint64_be(round))
|
|
HashToG1 string `json:"hash_to_g1"` // H(M), compressed G1
|
|
Body string `json:"body"` // U || V || W, the stanza body
|
|
U string `json:"u"` // compressed G2
|
|
V string `json:"v"` // 16 bytes
|
|
W string `json:"w"` // 16 bytes
|
|
Pairing string `json:"pairing"` // e(signature, U), 576 bytes
|
|
H2 string `json:"h2"` // H2(pairing), 16 bytes
|
|
Sigma string `json:"sigma"` // V XOR H2
|
|
H4 string `json:"h4"` // H4(sigma), 16 bytes
|
|
FileKey string `json:"file_key"` // W XOR H4: FK_TIME
|
|
H3Base string `json:"h3_base"` // SHA-256("IBE-H3" || sigma || file_key)
|
|
H3Tries []H3Try `json:"h3_tries"` // the tries of H3, the last one accepted
|
|
R string `json:"r"` // H3(sigma, file_key), 32 bytes big-endian
|
|
}
|
|
|
|
// H3Try is one try of H3: the counter i, d = SHA-256(uint16_le(i) ||
|
|
// h3_base), d with its first byte shifted one bit to the right, and whether
|
|
// that is below the order of the group.
|
|
type H3Try struct {
|
|
I int `json:"i"`
|
|
Digest string `json:"digest"`
|
|
Shifted string `json:"shifted"`
|
|
Accepted bool `json:"accepted"`
|
|
}
|
|
|
|
// The order of the groups of BLS12-381 (spec §12.2), written out here
|
|
// rather than taken from kyber, against which this file checks itself.
|
|
var tlockStepOrder, _ = new(big.Int).SetString("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001", 16)
|
|
|
|
// The tags of H2, H3 and H4 and the DST of Quicknet, written out here.
|
|
const (
|
|
tlockStepH2Tag = "IBE-H2"
|
|
tlockStepH3Tag = "IBE-H3"
|
|
tlockStepH4Tag = "IBE-H4"
|
|
tlockStepDST = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"
|
|
)
|
|
|
|
// tlockStepMessage is the message a Quicknet signature signs.
|
|
func tlockStepMessage(round uint64) []byte {
|
|
var b [8]byte
|
|
binary.BigEndian.PutUint64(b[:], round)
|
|
m := sha256.Sum256(b[:])
|
|
return m[:]
|
|
}
|
|
|
|
func tlockStepHash(n int, parts ...[]byte) []byte {
|
|
h := sha256.New()
|
|
for _, p := range parts {
|
|
h.Write(p)
|
|
}
|
|
return h.Sum(nil)[:n]
|
|
}
|
|
|
|
// tlockStepH3 is H3 as spec §63 writes it, independently of kyber.
|
|
func tlockStepH3(sigma, fileKey []byte) (base []byte, tries []H3Try, r []byte, err error) {
|
|
base = tlockStepHash(32, []byte(tlockStepH3Tag), sigma, fileKey)
|
|
for i := 1; i <= 65534; i++ {
|
|
d := tlockStepHash(32, []byte{byte(i), byte(i >> 8)}, base)
|
|
s := bytes.Clone(d)
|
|
s[0] >>= 1
|
|
ok := new(big.Int).SetBytes(s).Cmp(tlockStepOrder) < 0
|
|
tries = append(tries, H3Try{I: i, Digest: hex.EncodeToString(d), Shifted: hex.EncodeToString(s), Accepted: ok})
|
|
if ok {
|
|
return base, tries, s, nil
|
|
}
|
|
}
|
|
return nil, nil, nil, errors.New("H3: no try accepted")
|
|
}
|
|
|
|
func tlockStepXOR(a, b []byte) []byte {
|
|
out := make([]byte, len(a))
|
|
for i := range a {
|
|
out[i] = a[i] ^ b[i]
|
|
}
|
|
return out
|
|
}
|
|
|
|
func tlockStepBytes(p interface{ MarshalBinary() ([]byte, error) }) []byte {
|
|
b, err := p.MarshalBinary()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// tlockStepInput derives the sigma and the file key of stanza n of a round,
|
|
// deterministically.
|
|
func tlockStepInput(round uint64, n int) (sigma, fileKey []byte) {
|
|
var b [9]byte
|
|
binary.BigEndian.PutUint64(b[:8], round)
|
|
b[8] = byte(n)
|
|
return tlockStepHash(16, []byte("datekeys tlock_steps sigma"), b[:]),
|
|
tlockStepHash(16, []byte("datekeys tlock_steps file key"), b[:])
|
|
}
|
|
|
|
// TlockStepVectors computes testdata/vectors/tlock_steps.json. It builds
|
|
// each stanza with its own H2, H3 and H4 and checks every value against
|
|
// drand, kyber, tlock and agewrap: the message against DigestBeacon of the
|
|
// scheme, the hash to G1 against the pairing equation with the published
|
|
// signature, the body against tlock.TimeUnlock, kyber's DecryptCCAonG2 and
|
|
// the TimeIdentity of the reference, which recover the file key only if
|
|
// their H2, H3 and H4 are the ones written here. It also checks that the
|
|
// vectors tell the rules from their usual misreadings: the signature does
|
|
// not verify with the DST of G2 or with the round itself as the message, and
|
|
// clearing the top bit of a digest of H3 instead of shifting its first byte
|
|
// gives another r.
|
|
func TlockStepVectors() (TlockStepsFile, error) {
|
|
p := profile.Quicknet()
|
|
scheme, err := p.DrandScheme()
|
|
if err != nil {
|
|
return TlockStepsFile{}, err
|
|
}
|
|
if !bytes.Equal(ibe.H2Tag(), []byte(tlockStepH2Tag)) || !bytes.Equal(ibe.H3Tag(), []byte(tlockStepH3Tag)) || !bytes.Equal(ibe.H4Tag(), []byte(tlockStepH4Tag)) {
|
|
return TlockStepsFile{}, errors.New("the tags of kyber differ from the ones of spec §63")
|
|
}
|
|
if !bytes.Equal(bls.DefaultDomainG1(), []byte(tlockStepDST)) {
|
|
return TlockStepsFile{}, errors.New("the G1 DST of kyber-bls12381 differs from the one of spec §63")
|
|
}
|
|
suite := bls.NewBLS12381Suite()
|
|
key := scheme.KeyGroup.Point()
|
|
if err := key.UnmarshalBinary(p.PublicKey); err != nil {
|
|
return TlockStepsFile{}, err
|
|
}
|
|
g2 := suite.G2().Point().Base()
|
|
f := TlockStepsFile{
|
|
Spec: SpecVersion,
|
|
Description: "Steps 10 and 11 of spec §63 for Quicknet, value by value, over published releases. Step 10: M = SHA-256(uint64_be(round)), " +
|
|
"H(M) the hash to G1 of RFC 9380 with the suite BLS12381G1_XMD:SHA-256_SSWU_RO_ and the DST dst, and e(H(M), public_key) = e(signature, G2). " +
|
|
"Step 11: a stanza body U || V || W built with the sigma and the file key of the vector; H2 = SHA-256(\"IBE-H2\" || e(signature, U))[:16], " +
|
|
"sigma = V XOR H2, H4 = SHA-256(\"IBE-H4\" || sigma)[:16], file_key = W XOR H4, and r = H3(sigma, file_key): h3_base = SHA-256(\"IBE-H3\" || sigma || file_key), " +
|
|
"then for i = 1, 2, ... d = SHA-256(uint16_le(i) || h3_base), its first byte shifted one bit to the right, until it is below the order of the group; " +
|
|
"r·G2 = U. Generated by the reference implementation and checked against drand, kyber, tlock and agewrap. See testdata/README.md.",
|
|
Profile: p.ID,
|
|
Scheme: p.Scheme,
|
|
ChainHash: p.ChainHashHex(),
|
|
PublicKey: hex.EncodeToString(p.PublicKey),
|
|
DST: tlockStepDST,
|
|
Tags: TlockStepTags{
|
|
H2: hex.EncodeToString([]byte(tlockStepH2Tag)),
|
|
H3: hex.EncodeToString([]byte(tlockStepH3Tag)),
|
|
H4: hex.EncodeToString([]byte(tlockStepH4Tag)),
|
|
},
|
|
}
|
|
|
|
type input struct {
|
|
name string
|
|
round uint64
|
|
n int
|
|
}
|
|
inputs := []input{}
|
|
for _, round := range Rounds {
|
|
inputs = append(inputs, input{fmt.Sprintf("round %d, stanza 0", round), round, 0})
|
|
}
|
|
// The first stanza of round 1000 whose H3 needs at least three tries.
|
|
for n := 1; n < 256; n++ {
|
|
sigma, fk := tlockStepInput(1000, n)
|
|
if _, tries, _, err := tlockStepH3(sigma, fk); err == nil && len(tries) >= 3 {
|
|
inputs = append(inputs, input{fmt.Sprintf("round 1000, stanza %d: H3 accepts its try %d", n, len(tries)), 1000, n})
|
|
break
|
|
}
|
|
}
|
|
|
|
for _, in := range inputs {
|
|
v, err := tlockStepVector(scheme, suite, key, g2, p, in.round, in.n)
|
|
if err != nil {
|
|
return TlockStepsFile{}, fmt.Errorf("%s: %w", in.name, err)
|
|
}
|
|
v.Name = in.name
|
|
f.Vectors = append(f.Vectors, v)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func tlockStepVector(scheme *crypto.Scheme, suite pairing.Suite, key, g2 kyber.Point, p *profile.Profile, round uint64, n int) (TlockStepVector, error) {
|
|
release := Release(round)
|
|
m := tlockStepMessage(round)
|
|
if !bytes.Equal(m, scheme.DigestBeacon(&common.Beacon{Round: round})) {
|
|
return TlockStepVector{}, errors.New("M differs from DigestBeacon of the scheme")
|
|
}
|
|
// Step 10.
|
|
if err := provider.Verify(p, provider.Condition{Round: round}, release); err != nil {
|
|
return TlockStepVector{}, err
|
|
}
|
|
hm := suite.G1().Point().(kyber.HashablePoint).Hash(m)
|
|
sig := suite.G1().Point()
|
|
if err := sig.UnmarshalBinary(release.Signature); err != nil {
|
|
return TlockStepVector{}, err
|
|
}
|
|
if !suite.ValidatePairing(hm, key, sig, g2) {
|
|
return TlockStepVector{}, errors.New("e(H(M), public key) differs from e(signature, G2)")
|
|
}
|
|
// Another DST, the one of G2 that bls-unchained-on-g1 uses on G1, or the
|
|
// round without SHA-256, does not verify.
|
|
other := bls.NewBLS12381SuiteWithDST(bls.DefaultDomainG2(), nil).G1().Point().(kyber.HashablePoint).Hash(m)
|
|
raw := suite.G1().Point().(kyber.HashablePoint).Hash(binary.BigEndian.AppendUint64(nil, round))
|
|
if suite.ValidatePairing(other, key, sig, g2) || suite.ValidatePairing(raw, key, sig, g2) {
|
|
return TlockStepVector{}, errors.New("the signature verifies with another DST or another message")
|
|
}
|
|
|
|
// Step 11: the writer's side, with the H2, H3 and H4 of this file.
|
|
sigma, fk := tlockStepInput(round, n)
|
|
base, tries, rb, err := tlockStepH3(sigma, fk)
|
|
if err != nil {
|
|
return TlockStepVector{}, err
|
|
}
|
|
// The vector tells the shift of the first byte from clearing its top
|
|
// bit: that rule gives another r.
|
|
for _, t := range tries {
|
|
d, _ := hex.DecodeString(t.Digest)
|
|
d[0] &= 0x7f
|
|
if new(big.Int).SetBytes(d).Cmp(tlockStepOrder) < 0 {
|
|
if bytes.Equal(d, rb) {
|
|
return TlockStepVector{}, errors.New("clearing the top bit of H3 gives the same r")
|
|
}
|
|
break
|
|
}
|
|
}
|
|
r := suite.G1().Scalar()
|
|
if err := r.UnmarshalBinary(rb); err != nil {
|
|
return TlockStepVector{}, err
|
|
}
|
|
u := suite.G2().Point().Mul(r, nil)
|
|
gid := suite.Pair(hm, key)
|
|
gidr := tlockStepBytes(gid.Mul(r, gid))
|
|
h4 := tlockStepHash(16, []byte(tlockStepH4Tag), sigma)
|
|
ub := tlockStepBytes(u)
|
|
vb := tlockStepXOR(sigma, tlockStepHash(16, []byte(tlockStepH2Tag), gidr))
|
|
wb := tlockStepXOR(fk, h4)
|
|
body := slices.Concat(ub, vb, wb)
|
|
|
|
// The reader's side, recomputed.
|
|
gt := tlockStepBytes(suite.Pair(sig, u))
|
|
if !bytes.Equal(gt, gidr) {
|
|
return TlockStepVector{}, errors.New("e(signature, U) differs from e(H(M), public key)^r")
|
|
}
|
|
h2 := tlockStepHash(16, []byte(tlockStepH2Tag), gt)
|
|
if !bytes.Equal(tlockStepXOR(vb, h2), sigma) || !bytes.Equal(tlockStepXOR(wb, h4), fk) {
|
|
return TlockStepVector{}, errors.New("the decryption does not give back sigma and the file key")
|
|
}
|
|
|
|
// drand, kyber, tlock and agewrap decrypt it.
|
|
ct, err := tlock.BytesToCiphertext(*scheme, body)
|
|
if err != nil {
|
|
return TlockStepVector{}, err
|
|
}
|
|
got, err := tlock.TimeUnlock(*scheme, key, common.Beacon{Round: round, Signature: release.Signature}, ct)
|
|
if err != nil || !bytes.Equal(got, fk) {
|
|
return TlockStepVector{}, fmt.Errorf("tlock.TimeUnlock does not give back the file key: %v", err)
|
|
}
|
|
got, err = ibe.DecryptCCAonG2(bls.NewBLS12381Suite(), sig, ct)
|
|
if err != nil || !bytes.Equal(got, fk) {
|
|
return TlockStepVector{}, errors.New("kyber's DecryptCCAonG2 does not give back the file key")
|
|
}
|
|
id, err := agewrap.NewTimeIdentity(p, round, release)
|
|
if err != nil {
|
|
return TlockStepVector{}, err
|
|
}
|
|
got, err = id.Unwrap([]*age.Stanza{{Type: agewrap.StanzaTLock, Args: []string{strconv.FormatUint(round, 10), p.ChainHashHex()}, Body: body}})
|
|
if err != nil || !bytes.Equal(got, fk) {
|
|
return TlockStepVector{}, fmt.Errorf("agewrap.TimeIdentity does not give back the file key: %v", err)
|
|
}
|
|
|
|
return TlockStepVector{
|
|
Round: round,
|
|
Signature: hex.EncodeToString(release.Signature),
|
|
Message: hex.EncodeToString(m),
|
|
HashToG1: hex.EncodeToString(tlockStepBytes(hm)),
|
|
Body: hex.EncodeToString(body),
|
|
U: hex.EncodeToString(ub),
|
|
V: hex.EncodeToString(vb),
|
|
W: hex.EncodeToString(wb),
|
|
Pairing: hex.EncodeToString(gt),
|
|
H2: hex.EncodeToString(h2),
|
|
Sigma: hex.EncodeToString(sigma),
|
|
H4: hex.EncodeToString(h4),
|
|
FileKey: hex.EncodeToString(fk),
|
|
H3Base: hex.EncodeToString(base),
|
|
H3Tries: tries,
|
|
R: hex.EncodeToString(rb),
|
|
}, nil
|
|
}
|