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

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
}

Powered by TurnKey Linux.