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/release_go_vectors.go

385 lines
16 KiB

//go:build ignore
// Prints test/vectors/release_vectors.json: the verdicts, codes and texts of
// the Go reference for lib/src/release.dart and lib/src/tlock.dart.
//
// - verify: provider.Verify (spec §17, §51, §63 step 10) on the published
// releases of the fixtures and on edited ones: other rounds, other
// lengths, re-encodings of the signature (x + p, the point at infinity,
// stray flags, the negation), rounds out of range, and profiles with
// another scheme or another key. The x + p signature of round 1004 is the
// one of the mutation corpus (testdata/vectors/mutations.json).
// - unwrap: agewrap.NewTimeIdentity and its Unwrap (spec §63 step 11) on
// the stanza of OUTER_TIME_AGE of the time_only fixture and on edited
// copies of it: the count and type of the stanzas, their arguments, the
// release, and the body (its length, U re-encoded or the point at
// infinity, V and W edited).
// - recipient: agewrap.NewTimeRecipient on the profile and the round.
//
// Each verdict is "ok", with the file key for an unwrap, or the normative
// code and the text of the error of Go.
//
// 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/release_go_vectors.go \
// ../datekeys-dart/testdata > ../datekeys-dart/test/vectors/release_vectors.json
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"math/big"
"os"
"path/filepath"
"runtime/debug"
"sort"
"strings"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/agewrap"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
bls "github.com/drand/kyber-bls12381"
)
var p, _ = new(big.Int).SetString("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", 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 flip(b []byte, i int, mask byte) []byte {
c := bytes.Clone(b)
c[i] ^= mask
return c
}
func infinity(n int, first byte) []byte {
b := make([]byte, n)
b[0] = first
return b
}
// minusP is the x + p encoding with p subtracted again, flags kept: the
// canonical one.
func minusP(b []byte) []byte {
flags := b[0] & 0xe0
c := bytes.Clone(b)
c[0] &= 0x1f
x := new(big.Int).Sub(new(big.Int).SetBytes(c), p)
out := x.FillBytes(make([]byte, 48))
out[0] |= flags
return out
}
type result struct {
Go string `json:"go"`
Code string `json:"code,omitempty"`
Text string `json:"text,omitempty"`
FileKey string `json:"file_key,omitempty"`
}
func verdict(err error) result {
if err == nil {
return result{Go: "ok"}
}
return result{Go: "reject", Code: datekeys.Code(err), Text: err.Error()}
}
// A profile edit: the Quicknet profile with another scheme or key.
type profileCase struct {
Name string `json:"name"`
Scheme string `json:"scheme"`
PublicKey string `json:"public_key"`
}
type verifyCase struct {
Name string `json:"name"`
Profile string `json:"profile"`
Round uint64 `json:"round"`
Release uint64 `json:"release_round"`
Signature string `json:"signature"`
result
}
type stanza struct {
Type string `json:"type"`
Args []string `json:"args"`
Body string `json:"body"`
}
type unwrapCase struct {
Name string `json:"name"`
Profile string `json:"profile"`
Round uint64 `json:"round"`
Release uint64 `json:"release_round"`
Signature string `json:"signature"`
Stanzas []stanza `json:"stanzas"`
result
}
type recipientCase struct {
Name string `json:"name"`
Profile string `json:"profile"`
Round uint64 `json:"round"`
result
}
func main() {
dir := os.Args[1]
release := func(name string) (uint64, []byte) {
var side struct {
Release struct {
Round uint64 `json:"round"`
Signature string `json:"signature"`
} `json:"release"`
}
if err := json.Unmarshal(must(os.ReadFile(filepath.Join(dir, "fixtures", name+".json"))), &side); err != nil {
panic(err)
}
return side.Release.Round, unhex(side.Release.Signature)
}
_, sig1000 := release("time_only")
_, sig1001 := release("empty_payload")
_, sig2000 := release("time_only_extensions")
var corpus struct {
Cases []struct {
Name string `json:"name"`
Release *struct {
Round uint64 `json:"round"`
Signature string `json:"signature"`
} `json:"release"`
} `json:"cases"`
}
if err := json.Unmarshal(must(os.ReadFile(filepath.Join(dir, "vectors", "mutations.json"))), &corpus); err != nil {
panic(err)
}
var xPlusP []byte
for _, c := range corpus.Cases {
if c.Name == "release signature re-encoded with x + p" {
if c.Release.Round != 1004 {
panic("the x + p case is not of round 1004")
}
xPlusP = unhex(c.Release.Signature)
}
}
if xPlusP == nil {
panic("no x + p case in the mutation corpus")
}
sig1004 := minusP(xPlusP)
quicknet := profile.Quicknet()
key := quicknet.PublicKey
g2 := must(bls.NewBLS12381Suite().G2().Point().Base().MarshalBinary())
profiles := []profileCase{
{"quicknet", quicknet.Scheme, hex.EncodeToString(key)},
{"scheme pedersen-bls-unchained", "pedersen-bls-unchained", hex.EncodeToString(key)},
{"scheme bls-unchained-on-g1", "bls-unchained-on-g1", hex.EncodeToString(key)},
{"scheme not of drand", "datekeys-test", hex.EncodeToString(key)},
{"key the generator of G2", quicknet.Scheme, hex.EncodeToString(g2)},
{"key the point at infinity", quicknet.Scheme, hex.EncodeToString(infinity(96, 0xc0))},
{"key with the compression flag cleared", quicknet.Scheme, hex.EncodeToString(flip(key, 0, 0x80))},
}
profileOf := func(name string) *profile.Profile {
for _, c := range profiles {
if c.Name == name {
q := quicknet.Clone()
q.Scheme, q.PublicKey = c.Scheme, unhex(c.PublicKey)
return q
}
}
panic("no profile " + name)
}
max := quicknet.MaxRound()
out := struct {
Description string `json:"description"`
Generator string `json:"generator"`
Libraries string `json:"libraries"`
MaxRound uint64 `json:"max_round"`
Profiles []profileCase `json:"profiles"`
Verify []verifyCase `json:"verify"`
Unwrap []unwrapCase `json:"unwrap"`
Recipient []recipientCase `json:"recipient"`
}{
Description: "Verdicts, codes and texts of provider.Verify, agewrap.NewTimeIdentity with its Unwrap and " +
"agewrap.NewTimeRecipient of the Go reference; see tool/release_go_vectors.go.",
Generator: "tool/release_go_vectors.go",
Libraries: libraries(),
MaxRound: max,
Profiles: profiles,
}
// provider.Verify.
for _, c := range []struct {
name, profile string
round, release uint64
sig []byte
}{
{"the published release of round 1000", "quicknet", 1000, 1000, sig1000},
{"the published release of round 1001", "quicknet", 1001, 1001, sig1001},
{"the published release of round 1004", "quicknet", 1004, 1004, sig1004},
{"the published release of round 2000", "quicknet", 2000, 2000, sig2000},
{"a valid release of another round", "quicknet", 1000, 1001, sig1001},
{"another round and a short signature", "quicknet", 1000, 1001, sig1001[:47]},
{"the signature of another round relabelled", "quicknet", 1000, 1000, sig1001},
{"the signature of round 1000 for round 999", "quicknet", 999, 999, sig1000},
{"an all-zero signature", "quicknet", 1000, 1000, make([]byte, 48)},
{"a flipped bit", "quicknet", 1000, 1000, flip(sig1000, 47, 1)},
{"a short signature", "quicknet", 1000, 1000, sig1000[:47]},
{"a long signature", "quicknet", 1000, 1000, append(bytes.Clone(sig1000), 0)},
{"a G2-sized signature", "quicknet", 1000, 1000, append(bytes.Clone(sig1000), sig1000...)},
{"an empty signature", "quicknet", 1000, 1000, nil},
{"the signature re-encoded with x + p", "quicknet", 1004, 1004, xPlusP},
{"the signature is the point at infinity", "quicknet", 1000, 1000, infinity(48, 0xc0)},
{"the infinity flag and a payload", "quicknet", 1000, 1000, flip(sig1000, 0, 0x40)},
{"the point at infinity with the sign flag", "quicknet", 1000, 1000, infinity(48, 0xe0)},
{"the compression flag cleared", "quicknet", 1000, 1000, flip(sig1000, 0, 0x80)},
{"the signature negated", "quicknet", 1000, 1000, flip(sig1000, 0, 0x20)},
{"round zero", "quicknet", 0, 0, sig1000},
{"the last round of the profile", "quicknet", max, max, sig1000},
{"a round beyond the profile", "quicknet", max + 1, max + 1, sig1000},
{"another scheme of drand", "scheme pedersen-bls-unchained", 1000, 1000, sig1000},
{"another scheme of drand on G1", "scheme bls-unchained-on-g1", 1000, 1000, sig1000},
{"a scheme that is not of drand", "scheme not of drand", 1000, 1000, sig1000},
{"another scheme and another round", "scheme pedersen-bls-unchained", 1000, 1001, sig1001},
{"another valid key", "key the generator of G2", 1000, 1000, sig1000},
{"the key is the point at infinity", "key the point at infinity", 1000, 1000, sig1000},
{"the key and the signature are the point at infinity", "key the point at infinity", 1000, 1000, infinity(48, 0xc0)},
{"a key that is not a canonical encoding", "key with the compression flag cleared", 1000, 1000, sig1000},
{"a key that is not a canonical encoding and a short signature", "key with the compression flag cleared", 1000, 1000, sig1000[:47]},
} {
err := provider.Verify(profileOf(c.profile), provider.Condition{Round: c.round}, provider.Release{Round: c.release, Signature: c.sig})
out.Verify = append(out.Verify, verifyCase{c.name, c.profile, c.round, c.release, hex.EncodeToString(c.sig), verdict(err)})
}
// agewrap.TimeIdentity on the stanza of the time_only fixture.
file := must(os.ReadFile(filepath.Join(dir, "fixtures", "time_only.dkc")))
pre := must(capsule.ParsePrelude(file))
start := capsule.PreludeSize + int(pre.PublicHeaderLen)
sealed := file[start : start+int(pre.SealedControlLen)]
stanzas := must(agewrap.Stanzas(bytes.NewReader(sealed)))
if len(stanzas) != 1 {
panic("time_only has not one stanza")
}
base := stanzas[0]
body := base.Body
u, v, w := body[:96], body[96:112], body[112:]
c0 := new(big.Int).SetBytes(u[48:])
uc0p := append(bytes.Clone(u[:48]), new(big.Int).Add(c0, p).FillBytes(make([]byte, 48))...)
cat := func(parts ...[]byte) []byte { return bytes.Join(parts, nil) }
with := func(edit func(s *age.Stanza)) []*age.Stanza {
s := &age.Stanza{Type: base.Type, Args: append([]string(nil), base.Args...), Body: bytes.Clone(base.Body)}
edit(s)
return []*age.Stanza{s}
}
same := func(*age.Stanza) {}
other := &age.Stanza{Type: "X25519", Args: []string{"LvR2+5baviJPeIiyw96VfTW1GnzTw3DbWIPGuq9amEw"}, Body: make([]byte, 32)}
for _, c := range []struct {
name, profile string
round, release uint64
sig []byte
stanzas []*age.Stanza
}{
{"the stanza of time_only", "quicknet", 1000, 1000, sig1000, with(same)},
{"no stanza", "quicknet", 1000, 1000, sig1000, nil},
{"two stanzas", "quicknet", 1000, 1000, sig1000, append(with(same), with(same)...)},
{"a stanza of type X25519", "quicknet", 1000, 1000, sig1000, []*age.Stanza{other}},
{"one argument", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Args = s.Args[:1] })},
{"three arguments", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Args = append(s.Args, "x") })},
{"the round of another DateKey", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Args[0] = "1001" })},
{"the round with a leading zero", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Args[0] = "01000" })},
{"the round with a quote, a newline and a letter that is not ASCII", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Args[0] = "1000" + string(rune(34)) + string(rune(10)) + string(rune(233)) })},
{"the chain hash in upper case", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Args[1] = strings.ToUpper(s.Args[1]) })},
{"another chain hash", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Args[1] = strings.Repeat("ab", 32) })},
{"a release of another round", "quicknet", 1000, 1001, sig1001, with(same)},
{"the release signature negated", "quicknet", 1000, 1000, flip(sig1000, 0, 0x20), with(same)},
{"the release signature negated and a body of 127 bytes", "quicknet", 1000, 1000, flip(sig1000, 0, 0x20), with(func(s *age.Stanza) { s.Body = s.Body[:127] })},
{"a body of 127 bytes", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = s.Body[:127] })},
{"a body of 129 bytes", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = append(s.Body, 0) })},
{"an empty body", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = nil })},
{"U re-encoded with c0 + p", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(uc0p, v, w) })},
{"U is the point at infinity", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(infinity(96, 0xc0), v, w) })},
{"U with the infinity flag and a payload", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(flip(u, 0, 0x40), v, w) })},
{"U with the compression flag cleared", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(flip(u, 0, 0x80), v, w) })},
{"U negated", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(flip(u, 0, 0x20), v, w) })},
{"U is the generator of G2", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(unhex(hex.EncodeToString(g2)), v, w) })},
{"V with its first bit flipped", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(u, flip(v, 0, 0x80), w) })},
{"W with its last bit flipped", "quicknet", 1000, 1000, sig1000, with(func(s *age.Stanza) { s.Body = cat(u, v, flip(w, 15, 0x01)) })},
{"another scheme of drand", "scheme pedersen-bls-unchained", 1000, 1000, sig1000, with(same)},
{"a key that is not a canonical encoding", "key with the compression flag cleared", 1000, 1000, sig1000, with(same)},
{"the key is the point at infinity", "key the point at infinity", 1000, 1000, sig1000, with(same)},
{"another valid key", "key the generator of G2", 1000, 1000, sig1000, with(same)},
} {
uc := unwrapCase{Name: c.name, Profile: c.profile, Round: c.round, Release: c.release, Signature: hex.EncodeToString(c.sig), Stanzas: []stanza{}}
for _, s := range c.stanzas {
uc.Stanzas = append(uc.Stanzas, stanza{s.Type, append([]string{}, s.Args...), hex.EncodeToString(s.Body)})
}
id, err := agewrap.NewTimeIdentity(profileOf(c.profile), c.round, provider.Release{Round: c.release, Signature: c.sig})
if err == nil {
var fk []byte
if fk, err = id.Unwrap(c.stanzas); err == nil {
uc.result = result{Go: "ok", FileKey: hex.EncodeToString(fk)}
out.Unwrap = append(out.Unwrap, uc)
continue
}
}
uc.result = verdict(err)
out.Unwrap = append(out.Unwrap, uc)
}
// agewrap.NewTimeRecipient.
for _, c := range []struct {
name, profile string
round uint64
}{
{"round 1000", "quicknet", 1000},
{"the last round", "quicknet", max},
{"round zero", "quicknet", 0},
{"a round beyond the profile", "quicknet", max + 1},
{"another scheme of drand", "scheme pedersen-bls-unchained", 1000},
{"a scheme that is not of drand", "scheme not of drand", 1000},
{"a key that is not a canonical encoding", "key with the compression flag cleared", 1000},
{"the key is the point at infinity", "key the point at infinity", 1000},
{"the key is the point at infinity and round zero", "key the point at infinity", 0},
} {
_, err := agewrap.NewTimeRecipient(profileOf(c.profile), c.round)
out.Recipient = append(out.Recipient, recipientCase{c.name, c.profile, c.round, verdict(err)})
}
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, ", ")
}

Powered by TurnKey Linux.