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/internal/testkit/tlockstepvectors.go

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
}

Powered by TurnKey Linux.