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.
385 lines
16 KiB
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, ", ")
|
|
}
|