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.
233 lines
9.4 KiB
233 lines
9.4 KiB
package testkit
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
datekeys "github.com/datekeys/datekeys-go"
|
|
"github.com/datekeys/datekeys-go/datekey"
|
|
"github.com/datekeys/datekeys-go/profile"
|
|
)
|
|
|
|
// SpecVersion is the specification the vectors and fixtures implement.
|
|
const SpecVersion = "0.8.1"
|
|
|
|
// RoundVector is one Quicknet resolution vector (spec §65).
|
|
type RoundVector struct {
|
|
Name string `json:"name"`
|
|
Requested string `json:"requested"`
|
|
Round uint64 `json:"round,omitempty"`
|
|
Effective string `json:"effective,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// RoundVectorFile is testdata/vectors/quicknet_rounds.json.
|
|
type RoundVectorFile struct {
|
|
Spec string `json:"spec"`
|
|
Profile string `json:"profile"`
|
|
Description string `json:"description"`
|
|
Vectors []RoundVector `json:"vectors"`
|
|
}
|
|
|
|
// DK1Vector is one dk1_ vector (spec §66). Valid vectors carry the logical
|
|
// object and every intermediate encoding; invalid ones carry the input and the
|
|
// expected error.
|
|
type DK1Vector struct {
|
|
Name string `json:"name"`
|
|
Network string `json:"network,omitempty"`
|
|
Round uint64 `json:"round,omitempty"`
|
|
CanonicalJSON string `json:"canonical_json,omitempty"`
|
|
Base64URL string `json:"base64url,omitempty"`
|
|
Input string `json:"input,omitempty"`
|
|
DK1 string `json:"dk1,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// DK1VectorFile is testdata/vectors/dk1.json.
|
|
type DK1VectorFile struct {
|
|
Spec string `json:"spec"`
|
|
Description string `json:"description"`
|
|
Vectors []DK1Vector `json:"vectors"`
|
|
}
|
|
|
|
// ProfileVector is testdata/vectors/profile_quicknet.json.
|
|
type ProfileVector struct {
|
|
Spec string `json:"spec"`
|
|
Description string `json:"description"`
|
|
ProfileID string `json:"profile_id"`
|
|
Provider string `json:"provider"`
|
|
Network string `json:"network"`
|
|
ChainHash string `json:"chain_hash"`
|
|
PublicKey string `json:"public_key"`
|
|
PeriodSeconds uint64 `json:"period_seconds"`
|
|
GenesisTime int64 `json:"genesis_time"`
|
|
GenesisSeed string `json:"genesis_seed"`
|
|
Scheme string `json:"scheme"`
|
|
CanonicalCBOR string `json:"canonical_cbor"`
|
|
ProfileHash string `json:"profile_hash"`
|
|
}
|
|
|
|
// RoundVectors computes the Quicknet resolution vectors with the implementation.
|
|
func RoundVectors() RoundVectorFile {
|
|
p := profile.Quicknet()
|
|
g := time.Unix(p.GenesisTime, 0).UTC()
|
|
r1000, _ := datekey.RoundTime(p, 1000)
|
|
cases := []struct {
|
|
name string
|
|
at time.Time
|
|
}{
|
|
{"genesis exactly: round 1", g},
|
|
{"genesis + 1ns: next round", g.Add(time.Nanosecond)},
|
|
{"genesis + 1s", g.Add(time.Second)},
|
|
{"genesis + one period: round 2", g.Add(3 * time.Second)},
|
|
{"genesis + one period + 1ns: round 3", g.Add(3*time.Second + time.Nanosecond)},
|
|
{"genesis - 1s: before the profile", g.Add(-time.Second)},
|
|
{"round 1000 boundary exactly", r1000},
|
|
{"one second before the round 1000 boundary", r1000.Add(-time.Second)},
|
|
{"one second after the round 1000 boundary", r1000.Add(time.Second)},
|
|
{"1ns after the round 1000 boundary", r1000.Add(time.Nanosecond)},
|
|
{"half a second after the round 1000 boundary", r1000.Add(500 * time.Millisecond)},
|
|
{"normative vector 2030-01-01 (spec §16)", time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)},
|
|
{"1ns after 2030-01-01", time.Date(2030, 1, 1, 0, 0, 0, 1, time.UTC)},
|
|
{"normative vector round 66432123 (spec §16)", time.Date(2029, 12, 16, 7, 15, 33, 0, time.UTC)},
|
|
{"offset timezone equals UTC instant", time.Date(2026, 10, 22, 19, 0, 0, 1_000_000, time.FixedZone("", 2*3600))},
|
|
{"last representable round time", time.Date(9999, 12, 31, 23, 59, 57, 0, time.UTC)},
|
|
{"after the last representable round", time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)},
|
|
}
|
|
f := RoundVectorFile{
|
|
Spec: SpecVersion,
|
|
Profile: p.ID,
|
|
Description: "Quicknet date to round resolution (spec §15, §16, §65), generated by the reference implementation.",
|
|
}
|
|
for _, c := range cases {
|
|
v := RoundVector{Name: c.name, Requested: c.at.Format(time.RFC3339Nano)}
|
|
d, err := datekey.Resolve(p, c.at)
|
|
if err != nil {
|
|
v.Error = datekeys.Code(err)
|
|
} else {
|
|
v.Round = d.Round
|
|
v.Effective = d.UnlockAt(p).Format(time.RFC3339Nano)
|
|
}
|
|
f.Vectors = append(f.Vectors, v)
|
|
}
|
|
return f
|
|
}
|
|
|
|
// DK1Vectors computes the dk1_ vectors with the implementation.
|
|
func DK1Vectors() DK1VectorFile {
|
|
f := DK1VectorFile{
|
|
Spec: SpecVersion,
|
|
Description: "Canonical dk1_ strings and rejected encodings (spec §18, §19, §66), generated by the reference implementation.",
|
|
}
|
|
for _, v := range []struct {
|
|
name string
|
|
round uint64
|
|
}{
|
|
{"round 1", 1},
|
|
{"round 1000", 1000},
|
|
{"normative 2030-01-01 round", 66884212},
|
|
{"last Quicknet round", profile.Quicknet().MaxRound()},
|
|
} {
|
|
d := datekey.DateKey{ProfileID: profile.QuicknetID, Round: v.round}
|
|
j := d.CanonicalJSON()
|
|
f.Vectors = append(f.Vectors, DK1Vector{
|
|
Name: v.name,
|
|
Network: d.ProfileID,
|
|
Round: d.Round,
|
|
CanonicalJSON: string(j),
|
|
Base64URL: base64.RawURLEncoding.EncodeToString(j),
|
|
DK1: d.Compact(),
|
|
})
|
|
}
|
|
enc := func(s string) string { return datekey.Prefix + base64.RawURLEncoding.EncodeToString([]byte(s)) }
|
|
canon := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 66884212}
|
|
// The canonical JSON of round 1000 is 59 bytes: its Base64 form needs padding
|
|
// and has unused bits, unlike the 63-byte JSON of round 66884212.
|
|
r1000 := datekey.DateKey{ProfileID: profile.QuicknetID, Round: 1000}
|
|
bad := []struct{ name, input string }{
|
|
{"whitespace in JSON", enc(`{"version": 1, "network": "datekeys:quicknet:v1", "round": 66884212}`)},
|
|
{"keys reordered", enc(`{"network":"datekeys:quicknet:v1","version":1,"round":66884212}`)},
|
|
{"trailing whitespace", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":66884212}` + "\n")},
|
|
{"exponent notation", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":6.6884212e7}`)},
|
|
{"fraction notation", enc(`{"version":1.0,"network":"datekeys:quicknet:v1","round":66884212}`)},
|
|
{"escaped character", enc(strings.Replace(string(canon.CanonicalJSON()), "datekeys:", "datekeys\\"+"u003a", 1))},
|
|
{"duplicate key", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":1,"round":66884212}`)},
|
|
{"padded Base64URL", datekey.Prefix + base64.URLEncoding.EncodeToString(r1000.CanonicalJSON())},
|
|
{"non-zero trailing bits", mangleLastChar(r1000.Compact())},
|
|
{"extra field", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":66884212,"public_key":"00"}`)},
|
|
{"missing field", enc(`{"version":1,"network":"datekeys:quicknet:v1"}`)},
|
|
{"version 2", enc(`{"version":2,"network":"datekeys:quicknet:v1","round":66884212}`)},
|
|
{"round 0", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":0}`)},
|
|
{"negative round", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":-1}`)},
|
|
{"fractional round", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":1.5}`)},
|
|
{"round above 2^53-1", enc(fmt.Sprintf(`{"version":1,"network":"datekeys:quicknet:v1","round":%d}`, uint64(datekey.MaxRound)+1))},
|
|
{"round as string", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":"66884212"}`)},
|
|
{"uppercase network", enc(`{"version":1,"network":"DATEKEYS:QUICKNET:V1","round":66884212}`)},
|
|
{"trailing data", enc(`{"version":1,"network":"datekeys:quicknet:v1","round":66884212}x`)},
|
|
{"byte order mark", enc("\ufeff" + `{"version":1,"network":"datekeys:quicknet:v1","round":66884212}`)},
|
|
{"not Base64", datekey.Prefix + "!!!"},
|
|
{"missing prefix", canon.Compact()[len(datekey.Prefix):]},
|
|
{"uppercase prefix", "DK1_" + canon.Compact()[len(datekey.Prefix):]},
|
|
}
|
|
for _, b := range bad {
|
|
_, err := datekey.Parse(b.input)
|
|
code := datekeys.Code(err)
|
|
if err == nil {
|
|
code = "accepted"
|
|
}
|
|
f.Vectors = append(f.Vectors, DK1Vector{Name: b.name, Input: b.input, Error: code})
|
|
}
|
|
return f
|
|
}
|
|
|
|
// mangleLastChar changes the last Base64 character to one that decodes to the
|
|
// same bytes but has non-zero unused bits.
|
|
func mangleLastChar(s string) string {
|
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
last := s[len(s)-1]
|
|
for i := 0; i < len(alphabet); i++ {
|
|
c := alphabet[i]
|
|
if c == last {
|
|
continue
|
|
}
|
|
cand := s[:len(s)-1] + string(c)
|
|
a, errA := base64.RawURLEncoding.DecodeString(s[len(datekey.Prefix):])
|
|
b, errB := base64.RawURLEncoding.DecodeString(cand[len(datekey.Prefix):])
|
|
if errA == nil && errB == nil && string(a) == string(b) {
|
|
return cand
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// QuicknetProfileVector computes the profile vector with the implementation.
|
|
func QuicknetProfileVector() (ProfileVector, error) {
|
|
p := profile.Quicknet()
|
|
b, err := p.CanonicalCBOR()
|
|
if err != nil {
|
|
return ProfileVector{}, err
|
|
}
|
|
h, err := p.Hash()
|
|
if err != nil {
|
|
return ProfileVector{}, err
|
|
}
|
|
return ProfileVector{
|
|
Spec: SpecVersion,
|
|
Description: "Quicknet Provider Profile V1: exact Deterministic CBOR and profile_hash (spec §11, §12, §75 item 2), generated by the reference implementation.",
|
|
ProfileID: p.ID,
|
|
Provider: p.Provider,
|
|
Network: p.Network,
|
|
ChainHash: p.ChainHashHex(),
|
|
PublicKey: hex.EncodeToString(p.PublicKey),
|
|
PeriodSeconds: uint64(p.Period / time.Second),
|
|
GenesisTime: p.GenesisTime,
|
|
GenesisSeed: hex.EncodeToString(p.GenesisSeed[:]),
|
|
Scheme: p.Scheme,
|
|
CanonicalCBOR: hex.EncodeToString(b),
|
|
ProfileHash: hex.EncodeToString(h[:]),
|
|
}, nil
|
|
}
|