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

938 lines
33 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//go:build ignore
//go:debug cryptocustomrand=1
// Writes test/vectors/authorkey.json and authorkey.g.dart, the vectors of
// the author keys of datekeys-dart, stage 7b of docs/PLAN_dart.md: the
// Ed25519 signatures of Go's crypto/ed25519 and the package authorkey of
// datekeys-go, with the texts of its errors.
//
// - sign: crypto/ed25519.Sign over seeds and messages. The first 64 lines
// of sign.input of Go's crypto/ed25519 (SUPERCOP), whose lines 0, 1 and
// 2 are tests 1 to 3 of RFC 8032, 7.1, every 64th line after them, and
// line 1023, whose message of 1023 bytes is the one of its TEST 1024;
// TEST SHA(abc), the message SHA-512("abc") under the key of
// TestSignVerifyHashed of Go; seeds of a fixed seed with messages of 0
// bytes to 1 MiB; and private keys whose second half is another public
// key, which Go hashes as it is given;
// - scalars: x mod ℓ of 64-byte numbers and (a·b + c) mod ℓ of 32-byte
// ones, little-endian, with math/big, in the corners and at random;
// - keys: NewFromSeed, Public, PublicString, Secret, Marshal and String,
// and the errors of NewFromSeed and PublicString;
// - generate and encrypt: Generate and Encrypt while crypto/rand reads
// the keystream of SeededRandomSource of lib/src/random.dart (ChaCha20
// under SHA-256(seed), zero nonce), with each draw; Generate reads it
// through the GODEBUG cryptocustomrand=1 of this file;
// - public and secret: ParsePublic and ParseSecret over strings, as bytes:
// valid, in the other case or mixed, of other lengths, with each Bech32
// error, other prefixes, data of other lengths and paddings, keys that
// are not canonical, not on the curve or of small order, and bytes that
// are not UTF-8;
// - runes: the same over a valid string where one character is replaced
// by a rune of as many bytes, in the prefix and in the data, for the
// code points at each edge of the sets of unicode.ToLower,
// unicode.ToUpper and unicode.IsSpace of Go: [kind, position, rune,
// text], kind 0 for ParsePublic and 1 for ParseSecret;
// - read: Read of plain and encrypted files, with the result or the text
// of the error.
//
// Every expected value is what Go gives; none is written by hand. A text is
// an index into texts, whose first entry, "", stands for no error. Binary
// values are lower-case hexadecimal. A file is a list of parts, each
// {"hex": …}, {"byte": b, "n": count} or {"sealed": …, "length", "sha256"},
// the age file of a recipe, which the tests write again with
// SeededRandomSource. authorkey.g.dart is the same document with only the
// signatures marked node and one in eight of the runes, as a Dart
// constant, for the tests that also run compiled to JavaScript. Arrays of
// numbers are written on one line.
//
// It imports only public packages, so it runs in the module of the
// reference implementation, without changing anything there, from the
// datekeys-go next to this repository, on the branch v0.12 at c531e93:
//
// cd ../datekeys-go && go run ../datekeys-dart/tool/authorkey_go_vectors.go \
// -source $(git rev-parse v0.12) -out ../datekeys-dart/test/vectors
//
// The output is the same on every run.
package main
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/ed25519"
cryptorand "crypto/rand"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"math/big"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"unicode"
"unicode/utf8"
_ "unsafe"
"filippo.io/age"
"golang.org/x/crypto/chacha20"
"g.activething.com/go/DateKeys/authorkey"
_ "g.activething.com/go/DateKeys/codec/bech32"
)
//go:linkname createChecksum g.activething.com/go/DateKeys/codec/bech32.createChecksum
func createChecksum(hrp string, data []byte) []byte
type obj = map[string]any
func h(b []byte) string { return hex.EncodeToString(b) }
func check(err error) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
log.Fatalf("%s:%d: %v", filepath.Base(file), line, err)
}
}
func mustHex(s string) []byte {
b, err := hex.DecodeString(s)
check(err)
return b
}
func label(s string) []byte {
b := sha256.Sum256([]byte("datekeys-dart stage 7b: " + s))
return b[:]
}
// pattern is a plaintext of n bytes: byte i is (31·i + 7) mod 256.
func pattern(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(31*i + 7)
}
return b
}
// texts are the error texts, indexed; 0 is no error.
var texts = []string{""}
var textIndex = map[string]int{"": 0}
func t(err error) int {
if err == nil {
return 0
}
s := err.Error()
if i, ok := textIndex[s]; ok {
return i
}
texts = append(texts, s)
textIndex[s] = len(texts) - 1
return len(texts) - 1
}
// ---------------------------------------------------------------------------
// crypto/rand from a seed, as in tool/age_writer_go_vectors.go
type seeded struct {
c *chacha20.Cipher
draws [][]byte
}
func (s *seeded) Read(p []byte) (int, error) {
clear(p)
s.c.XORKeyStream(p, p)
s.draws = append(s.draws, bytes.Clone(p))
return len(p), nil
}
func with(seed string, f func()) [][]byte {
key := sha256.Sum256([]byte(seed))
c, err := chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize))
check(err)
s := &seeded{c: c}
old := cryptorand.Reader
cryptorand.Reader = s
defer func() { cryptorand.Reader = old }()
f()
return s.draws
}
func drawsOf(d [][]byte) []obj {
out := []obj{}
for _, b := range d {
out = append(out, obj{"n": len(b), "hex": h(b)})
}
return out
}
// ---------------------------------------------------------------------------
// Signatures
func signCase(name string, seed, pub, msg []byte, node bool) obj {
priv := append(bytes.Clone(seed), pub...)
sig := ed25519.Sign(priv, msg)
c := obj{"name": name, "seed": h(seed), "public_key": h(pub), "signature": h(sig), "node": node}
if len(msg) > 4096 {
if !bytes.Equal(msg, pattern(len(msg))) {
log.Fatal("a long message must be a pattern")
}
c["message_pattern"] = len(msg)
} else {
c["message"] = h(msg)
}
ownPub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
c["valid"] = ed25519.Verify(ownPub, msg, sig)
return c
}
func signSection() []obj {
out := []obj{}
f, err := os.Open(filepath.Join(runtime.GOROOT(), "src", "crypto", "ed25519", "testdata", "sign.input.gz"))
check(err)
defer f.Close()
gz, err := gzip.NewReader(f)
check(err)
sc := bufio.NewScanner(gz)
sc.Buffer(nil, 1<<20)
for line := 0; sc.Scan(); line++ {
if line >= 64 && line%64 != 0 && line != 1023 {
continue
}
parts := strings.Split(sc.Text(), ":")
seed := mustHex(parts[0])[:32]
pub := mustHex(parts[1])
msg := mustHex(parts[2])
sig := mustHex(parts[3])[:64]
if !bytes.Equal(ed25519.Sign(append(bytes.Clone(seed), pub...), msg), sig) {
log.Fatalf("sign.input line %d", line)
}
out = append(out, signCase(fmt.Sprintf("sign.input line %d", line), seed, pub, msg, line < 4 || line%16 == 0))
}
check(sc.Err())
// TEST SHA(abc): the key of TestSignVerifyHashed of Go, the private key
// of RFC 8032, 7.3, which 7.1 signs SHA-512("abc") with.
src, err := os.ReadFile(filepath.Join(runtime.GOROOT(), "src", "crypto", "ed25519", "ed25519_test.go"))
check(err)
m := regexp.MustCompile(`func TestSignVerifyHashed[^{]*\{[^"]*key, _ := hex\.DecodeString\("([0-9a-f]{128})"\)`).FindSubmatch(src)
if m == nil {
log.Fatal("no key in TestSignVerifyHashed")
}
key := mustHex(string(m[1]))
abc := sha512.Sum512([]byte("abc"))
out = append(out, signCase("RFC 8032 TEST SHA(abc)", key[:32], key[32:], abc[:], true))
lengths := []int{0, 1, 2, 31, 32, 33, 63, 64, 65, 99, 111, 112, 113, 127, 128, 129, 200, 255, 256, 1000, 4096}
for i := 0; i < 160; i++ {
seed := label(fmt.Sprintf("sign seed %d", i))
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
n := lengths[i%len(lengths)]
msg := label(fmt.Sprintf("sign message %d", i))
for len(msg) < n {
msg = append(msg, label(fmt.Sprintf("sign message %d %d", i, len(msg)))...)
}
out = append(out, signCase(fmt.Sprintf("seeded %d, %d bytes", i, n), seed, pub, msg[:n], i%8 == 0))
}
for _, n := range []int{64 << 10, 1 << 20} {
seed := label(fmt.Sprintf("sign long %d", n))
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
out = append(out, signCase(fmt.Sprintf("a message of %d bytes", n), seed, pub, pattern(n), n < 1<<20))
}
for _, b := range []byte{0, 0xff} {
seed := bytes.Repeat([]byte{b}, 32)
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
out = append(out, signCase(fmt.Sprintf("seed of 0x%02x", b), seed, pub, []byte("DateKeys"), true))
}
// Go hashes the second half of the private key as the public key,
// whatever it is.
for i := 0; i < 4; i++ {
seed := label(fmt.Sprintf("other key seed %d", i))
other := ed25519.NewKeyFromSeed(label(fmt.Sprintf("other key %d", i))).Public().(ed25519.PublicKey)
if i == 3 {
other = make([]byte, 32)
}
out = append(out, signCase(fmt.Sprintf("the public key of another seed, %d", i), seed, other, []byte("message"), true))
}
return out
}
// ---------------------------------------------------------------------------
// Scalars
var order, _ = new(big.Int).SetString("7237005577332262213973186563042994240857116359379907606001950938285454250989", 10)
func le(x *big.Int, n int) []byte {
b := x.FillBytes(make([]byte, n))
for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return b
}
func fromLE(b []byte) *big.Int {
r := bytes.Clone(b)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return new(big.Int).SetBytes(r)
}
func scalarSection() obj {
// ℓ is checked against the order of crypto/ed25519: [ℓ]B is the
// identity, through a signature whose S is ℓ - 1 + 1.
two := big.NewInt(2)
if new(big.Int).Sub(order, new(big.Int).Exp(two, big.NewInt(252), nil)).String() != "27742317777372353535851937790883648493" {
log.Fatal("ℓ")
}
max512 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 512), big.NewInt(1))
top := new(big.Int).Mul(new(big.Int).Div(max512, order), order)
reduceIn := []*big.Int{
big.NewInt(0), big.NewInt(1), new(big.Int).Sub(order, big.NewInt(1)), order,
new(big.Int).Add(order, big.NewInt(1)), new(big.Int).Mul(order, two),
new(big.Int).Lsh(big.NewInt(1), 252), new(big.Int).Lsh(big.NewInt(1), 253),
new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)),
new(big.Int).Lsh(big.NewInt(1), 511), max512, top, new(big.Int).Sub(top, big.NewInt(1)),
new(big.Int).Add(top, big.NewInt(1)),
}
for i := 0; i < 200; i++ {
x := new(big.Int).SetBytes(append(label(fmt.Sprintf("reduce %d a", i)), label(fmt.Sprintf("reduce %d b", i))...))
if i%4 == 1 {
x.Rsh(x, uint(i%512))
}
if i%4 == 2 {
x.Add(x.Mul(new(big.Int).Rsh(x, 260), order), big.NewInt(int64(i%3)-1))
x.And(x, max512)
}
reduceIn = append(reduceIn, x)
}
reduce := []obj{}
for _, x := range reduceIn {
reduce = append(reduce, obj{"in": h(le(x, 64)), "out": h(le(new(big.Int).Mod(x, order), 32))})
}
max256 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
corner := []*big.Int{big.NewInt(0), big.NewInt(1), new(big.Int).Sub(order, big.NewInt(1)), order, max256, new(big.Int).Lsh(big.NewInt(1), 255)}
muladd := []obj{}
add := func(a, b, c *big.Int) {
r := new(big.Int).Mul(a, b)
r.Add(r, c).Mod(r, order)
muladd = append(muladd, obj{"a": h(le(a, 32)), "b": h(le(b, 32)), "c": h(le(c, 32)), "out": h(le(r, 32))})
}
for _, a := range corner {
for _, b := range corner {
add(a, b, corner[(len(muladd))%len(corner)])
}
}
for i := 0; i < 100; i++ {
a := new(big.Int).SetBytes(label(fmt.Sprintf("muladd %d a", i)))
b := new(big.Int).SetBytes(label(fmt.Sprintf("muladd %d b", i)))
c := new(big.Int).SetBytes(label(fmt.Sprintf("muladd %d c", i)))
add(a, b, c)
}
return obj{"reduce": reduce, "muladd": muladd, "order": h(le(order, 32))}
}
// ---------------------------------------------------------------------------
// Keys
func keySection() obj {
keys := []obj{}
for i := 0; i < 24; i++ {
seed := label(fmt.Sprintf("key %d", i))
if i == 0 {
seed = make([]byte, 32)
}
k, err := authorkey.NewFromSeed(seed)
check(err)
ps, err := authorkey.PublicString(k.Public())
check(err)
keys = append(keys, obj{"seed": h(seed), "public_key": h(k.Public()), "public": ps, "secret": k.Secret(), "marshal": string(authorkey.Marshal(k)), "string": k.String(), "gostring": fmt.Sprintf("%#v", k)})
}
seedErrors := []obj{}
for _, n := range []int{0, 31, 33, 64} {
_, err := authorkey.NewFromSeed(make([]byte, n))
seedErrors = append(seedErrors, obj{"length": n, "error": err.Error()})
}
publicErrors := []obj{}
for _, n := range []int{0, 31, 33, 64} {
_, err := authorkey.PublicString(make([]byte, n))
publicErrors = append(publicErrors, obj{"length": n, "error": err.Error()})
}
return obj{"keys": keys, "seed_errors": seedErrors, "public_errors": publicErrors}
}
func generateSection() []obj {
out := []obj{}
for i := 0; i < 3; i++ {
seed := fmt.Sprintf("authorkey generate %d", i)
var k *authorkey.Key
d := with(seed, func() {
var err error
k, err = authorkey.Generate()
check(err)
})
out = append(out, obj{"seed": seed, "draws": drawsOf(d), "secret": k.Secret(), "public_key": h(k.Public())})
}
return out
}
func encryptSection() []obj {
out := []obj{}
for i, pass := range []string{"correct horse battery staple", "contraseña ñ €", "x"} {
k, err := authorkey.NewFromSeed(label(fmt.Sprintf("encrypt key %d", i)))
check(err)
seed := fmt.Sprintf("authorkey encrypt %d", i)
var buf bytes.Buffer
d := with(seed, func() { check(authorkey.Encrypt(&buf, k, pass)) })
back, err := authorkey.Read(bytes.NewReader(buf.Bytes()), pass)
check(err)
if back.Secret() != k.Secret() {
log.Fatal("Read does not give the key back")
}
out = append(out, obj{"seed": seed, "key_seed": h(label(fmt.Sprintf("encrypt key %d", i))), "passphrase": pass, "draws": drawsOf(d), "file": h(buf.Bytes()), "node": i == 0})
}
k, err := authorkey.NewFromSeed(label("encrypt key 0"))
check(err)
err = authorkey.Encrypt(&bytes.Buffer{}, k, "")
out = append(out, obj{"passphrase": "", "error": err.Error()})
return out
}
// ---------------------------------------------------------------------------
// Strings
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
// encode5 writes hrp and the 5-bit values with a valid checksum, in lower
// case: a Bech32 string whose data part need not be 8-bit data.
func encode5(hrp string, values []byte) string {
var b strings.Builder
b.WriteString(hrp)
b.WriteString("1")
for _, v := range values {
b.WriteByte(charset[v])
}
for _, v := range createChecksum(hrp, values) {
b.WriteByte(charset[v])
}
return b.String()
}
func to5(data []byte) []byte {
var out []byte
acc, bits := 0, 0
for _, v := range data {
acc = acc<<8 | int(v)
bits += 8
for bits >= 5 {
bits -= 5
out = append(out, byte(acc>>bits)&31)
}
}
if bits > 0 {
out = append(out, byte(acc<<(5-bits))&31)
}
return out
}
func enc(hrp string, data []byte) string {
v := to5(data)
s := encode5(strings.ToLower(hrp), v)
if strings.ToUpper(hrp) == hrp {
return strings.ToUpper(s)
}
return s
}
// variants are the strings of a valid key string s, of the prefix hrp and
// the data data: other cases, lengths, characters, prefixes, paddings.
func variants(s, hrp string, data []byte, full bool) []string {
out := []string{s, strings.ToUpper(s), strings.ToLower(s), s[:1] + strings.ToLower(s[1:]), s[:1] + strings.ToUpper(s[1:]),
s[:len(s)-1] + strings.ToUpper(s[len(s)-1:]), s[:len(s)-1] + strings.ToLower(s[len(s)-1:]),
"", s[:1], s[:len(s)-1], s + "q", s + s, " " + s[1:], s[:len(s)-1] + " ", s[:len(s)-1] + "\n"}
// Each position changed to another character of the charset, to one
// out of it and to the separator.
for i := 0; full && i < len(s); i++ {
for _, c := range []byte{'q', 'p', 'b', 'i', 'o', '1', '0', 'Z', ' ', 0, 0x7f, '"', '\\'} {
if s[i] == c {
continue
}
if c != 'q' && c != 'p' && i%5 != 0 && c != 'b' {
continue
}
out = append(out, s[:i]+string([]byte{c})+s[i+1:])
}
}
lower := strings.ToLower(hrp) == hrp
casing := func(x string) string {
if lower {
return strings.ToLower(x)
}
return strings.ToUpper(x)
}
n := len(hrp)
// Other prefixes of the same length and of a length one less or more,
// with data of the length that keeps the string length.
out = append(out, enc(casing(hrp[:n-1]+"q"), data))
out = append(out, enc(casing(hrp[:n-1]+"Q"), data))
out = append(out, enc(casing("x"+hrp[1:]), data))
v := to5(data)
out = append(out, casing(encode5(strings.ToLower(hrp[:n-1]), append(bytes.Clone(v), 0))))
out = append(out, casing(encode5(strings.ToLower(hrp[:n-1]), append(bytes.Clone(v), 1))))
out = append(out, casing(encode5(strings.ToLower(hrp+"x"), v[:len(v)-1])))
out = append(out, casing(encode5(strings.ToLower(hrp), append(bytes.Clone(v[:len(v)-1]), v[len(v)-1]|1))))
out = append(out, casing(encode5(strings.ToLower(hrp), append(bytes.Clone(v[:len(v)-1]), 31))))
out = append(out, casing(encode5(strings.ToLower(hrp+"x"), v[:len(v)-2])))
out = append(out, casing(encode5(strings.ToLower(hrp[:n-2]), append(bytes.Clone(v), 0, 0))))
out = append(out, casing(encode5(strings.ToLower(hrp[:n-1]+"1"), v[:len(v)-1])))
out = append(out, casing(encode5("", append(bytes.Clone(v), bytes.Repeat([]byte{0}, n+1)...))))
// A byte that is not ASCII and a separator 6, 7 or 8 bytes before the
// end: the position of the separator is checked first.
for _, bad := range []string{"\xff", "é"} {
for _, back := range []int{6, 7, 8} {
b := []byte(s[:3] + bad + s[3+len(bad):])
b[len(b)-back] = '1'
out = append(out, string(b))
}
}
// Bytes that are not ASCII or not UTF-8, in place of as many bytes.
for _, bad := range []string{"\xff", "\x80", "\xc0\x80", "\xe0\x80\x80", "\xed\xa0\x80", "\xf4\x90\x80\x80", "\xc3", "é", "€", "İ", "ß", "Dž", " ", "<22>", "\U0001f600"} {
for _, at := range []int{0, 3, n, n + 1, len(s) - len(bad)} {
if at+len(bad) <= len(s) {
out = append(out, s[:at]+bad+s[at+len(bad):])
}
}
}
return out
}
func keyStrings() ([]string, []string) {
var pub, sec []string
for i := 0; i < 6; i++ {
k, err := authorkey.NewFromSeed(label(fmt.Sprintf("strings %d", i)))
check(err)
ps, err := authorkey.PublicString(k.Public())
check(err)
if i < 2 {
pub = append(pub, variants(ps, authorkey.PublicPrefix, k.Public(), i == 0)...)
seed := label(fmt.Sprintf("strings %d", i))
sec = append(sec, variants(k.Secret(), authorkey.SecretPrefix, seed, i == 0)...)
} else {
pub = append(pub, ps)
sec = append(sec, k.Secret())
}
}
// Keys that the strict profile rejects: the public keys of
// ed25519_strict.json, encodings that are not canonical, and random
// encodings, about half of them off the curve.
var raws [][]byte
var strict struct {
Vectors []struct {
PublicKey string `json:"public_key"`
} `json:"vectors"`
}
check(json.Unmarshal(mustRead(filepath.Join(*testdata, "vectors", "ed25519_strict.json")), &strict))
for _, v := range strict.Vectors {
raws = append(raws, mustHex(v.PublicKey))
}
p := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))
for d := int64(-1); d <= 19; d++ {
y := le(new(big.Int).Add(p, big.NewInt(d)), 32)
raws = append(raws, bytes.Clone(y))
y[31] |= 0x80
raws = append(raws, y)
}
for _, y := range []*big.Int{big.NewInt(0), big.NewInt(1), new(big.Int).Sub(p, big.NewInt(1))} {
b := le(y, 32)
raws = append(raws, bytes.Clone(b))
b[31] |= 0x80
raws = append(raws, b)
}
for i := 0; i < 48; i++ {
raws = append(raws, label(fmt.Sprintf("random key %d", i)))
}
for _, r := range raws {
s, err := authorkey.PublicString(r)
check(err)
pub = append(pub, s)
}
return pub, sec
}
func mustRead(path string) []byte {
b, err := os.ReadFile(path)
check(err)
return b
}
func publicSection(in []string) []obj {
out := []obj{}
for _, s := range in {
k, err := authorkey.ParsePublic(s)
c := obj{"in": h([]byte(s)), "text": t(err)}
if err == nil {
c["public_key"] = h(k)
}
out = append(out, c)
}
return out
}
func secretSection(in []string) []obj {
out := []obj{}
for _, s := range in {
k, err := authorkey.ParseSecret(s)
c := obj{"in": h([]byte(s)), "text": t(err)}
if err == nil {
c["public_key"] = h(k.Public())
}
out = append(out, c)
}
return out
}
// edges returns the code points at each edge of a set: the last one out
// and the first one in, the last one in and the first one out.
func edges(in func(rune) bool, add func(rune)) {
prev := in(0)
for r := rune(1); r <= unicode.MaxRune; r++ {
if r >= 0xd800 && r <= 0xdfff {
continue
}
cur := in(r)
if cur != prev {
add(r - 1)
add(r)
}
prev = cur
}
}
func runeSection(n int) obj {
seen := map[rune]bool{}
var runes []rune
add := func(r rune) {
if r < 0x80 || (r >= 0xd800 && r <= 0xdfff) || seen[r] {
return
}
seen[r] = true
runes = append(runes, r)
}
edges(func(r rune) bool { return unicode.ToLower(r) != r }, add)
edges(func(r rune) bool { return unicode.ToUpper(r) != r }, add)
edges(unicode.IsSpace, add)
add(utf8.MaxRune)
add(0xfffd)
k, err := authorkey.NewFromSeed(label("runes"))
check(err)
ps, err := authorkey.PublicString(k.Public())
check(err)
sec := k.Secret()
cases := [][]any{}
for i, r := range runes {
e := string(r)
for kind, s := range []string{ps, sec} {
for _, at := range []int{[]int{3, len(s) - 9}[i%2]} {
in := s[:at] + e + s[at+len(e):]
var err error
if kind == 0 {
_, err = authorkey.ParsePublic(in)
} else {
_, err = authorkey.ParseSecret(in)
}
if err == nil {
log.Fatalf("U+%04X passes", r)
}
cases = append(cases, []any{kind, at, r, t(err)})
}
}
}
if n > 1 {
var some [][]any
for i := 0; i < len(cases); i += n * 2 {
some = append(some, cases[i:i+2]...)
}
cases = some
}
return obj{"public": ps, "secret": sec, "cases": cases}
}
// ---------------------------------------------------------------------------
// Files
type part = obj
func sum(b []byte) string {
s := sha256.Sum256(b)
return h(s[:])
}
// sealedPart is the file of sealed(seed, pass, wf, plain), written as its
// recipe, its length and its SHA-256: the tests write it again with
// SeededRandomSource, as age writes it here.
func sealedPart(seed, pass string, wf int, plain []part) part {
f := sealed(seed, pass, wf, join(plain))
return part{"sealed": obj{"seed": seed, "passphrase": pass, "work_factor": wf, "plain": plain}, "length": len(f), "sha256": sum(f)}
}
func hx(b []byte) part { return part{"hex": h(b)} }
func rep(b byte, n int) part { return part{"byte": int(b), "n": n} }
func join(parts []part) []byte {
var out []byte
for _, p := range parts {
if x, ok := p["hex"]; ok {
out = append(out, mustHex(x.(string))...)
} else if s, ok := p["sealed"]; ok {
r := s.(obj)
f := sealed(r["seed"].(string), r["passphrase"].(string), r["work_factor"].(int), join(r["plain"].([]part)))
if len(f) != p["length"].(int) || sum(f) != p["sha256"].(string) {
log.Fatal("a sealed part")
}
out = append(out, f...)
} else {
out = append(out, bytes.Repeat([]byte{byte(p["byte"].(int))}, p["n"].(int))...)
}
}
return out
}
func readCase(name string, parts []part, pass string, node bool) obj {
k, err := authorkey.Read(bytes.NewReader(join(parts)), pass)
c := obj{"name": name, "file": parts, "passphrase": pass, "text": t(err), "node": node}
if err == nil {
c["public_key"] = h(k.Public())
c["secret"] = k.Secret()
}
return c
}
// sealed encrypts plain with a scrypt recipient of work factor wf while
// crypto/rand reads the keystream of seed.
func sealed(seed, pass string, wf int, plain []byte) []byte {
var buf bytes.Buffer
with(seed, func() {
r, err := age.NewScryptRecipient(pass)
check(err)
r.SetWorkFactor(wf)
w, err := age.Encrypt(&buf, r)
check(err)
_, err = w.Write(plain)
check(err)
check(w.Close())
})
return buf.Bytes()
}
func readSection() []obj {
k1, err := authorkey.NewFromSeed(label("read 1"))
check(err)
k2, err := authorkey.NewFromSeed(label("read 2"))
check(err)
s1, s2 := k1.Secret(), k2.Secret()
p1, err := authorkey.PublicString(k1.Public())
check(err)
out := []obj{}
plain := func(name, s string) {
out = append(out, readCase(name, []part{hx([]byte(s))}, "", true))
}
plain("Marshal", string(authorkey.Marshal(k1)))
plain("the line alone", s1)
plain("the line and LF", s1+"\n")
plain("CR LF", "# c\r\n"+s1+"\r\n\r\n")
plain("CR alone", s1+"\r")
plain("CR inside", s1[:10]+"\r"+s1[10:])
plain("spaces and tabs", " \t "+s1+" \t\v\f\r\n")
plain("comments and empty lines", "\n\n# one\n # two\n\n"+s1+"\n# three\n\n")
plain("a comment without the space", "#"+s1+"\n"+s1+"\n")
plain("a comment that is not UTF-8", "# \xff\xfe\n"+s1)
plain("two keys", s1+"\n"+s2+"\n")
plain("the same key twice", s1+"\n"+s1+"\n")
plain("a key and something else", s1+"\nsomething\n")
plain("something else and a key", "something\n"+s1+"\n")
plain("a key in lower case", strings.ToLower(s1))
plain("a public key", p1)
plain("an age identity", "AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ")
plain("empty", "")
plain("LF", "\n")
plain("only comments", "# a\n# b\n")
plain("only spaces", " \n\t\n\v\f\n")
plain("NUL before the key", "\x00"+s1)
plain("a BOM before the key", bom+s1)
plain("a byte that is not UTF-8 before the key", "\xff"+s1)
plain("NEL alone, not UTF-8", "\x85"+s1)
plain("NEL in UTF-8", "\u0085"+s1+"\u0085")
// The ends of a line that are not quite a space: utf8.DecodeLastRune
// and DecodeRune give U+FFFD for them, which TrimSpace keeps.
plain("a space and a stray continuation byte at the end", s1+ideographicSpace+"\x80")
plain("a stray continuation byte and a space at the start", "\x80"+ideographicSpace+s1)
plain("a space cut at the end", s1+ideographicSpace[:2])
plain("a space cut at the start", ideographicSpace[1:]+s1)
plain("four continuation bytes after a space", s1+ideographicSpace+"\x80\x80\x80\x80")
plain("a space after the key and a stray byte", s1+" \x80")
plain("a key cut", s1[:78])
plain("a key and a byte", s1+"x")
plain("age-encryption.org/v1 without LF", "age-encryption.org/v1")
plain("age-encryption.org/v1 and a key", "age-encryption.org/v1 \n"+s1)
// Every space of Go, and its neighbours, around the line.
seen := map[rune]bool{}
for r := rune(0); r <= 0x3001; r++ {
if !unicode.IsSpace(r) {
continue
}
for _, x := range []rune{r - 1, r, r + 1} {
if seen[x] || x == '\n' || (x >= 0x21 && x < 0x7f && x != r) {
continue
}
seen[x] = true
e := string(x)
out = append(out, readCase(fmt.Sprintf("U+%04X around the line", x), []part{hx([]byte(e + e + s1 + e + "\n"))}, "", true))
}
}
// The limits: 64 KiB, the bufio.Scanner and its token of 64 KiB.
out = append(out, readCase("64 KiB of comment without LF", []part{hx([]byte("#")), rep('x', 65535)}, "", true))
out = append(out, readCase("64 KiB of comment with LF", []part{hx([]byte("#")), rep('x', 65534), hx([]byte("\n"))}, "", true))
out = append(out, readCase("a key, then a comment, 64 KiB in all", []part{hx([]byte(s1 + "\n#")), rep('x', 65536-82), hx([]byte("\n"))}, "", true))
out = append(out, readCase("64 KiB of spaces without LF", []part{rep(' ', 65536)}, "", true))
out = append(out, readCase("64 KiB and a byte", []part{hx([]byte(s1 + "\n#")), rep('x', 65536-80)}, "", true))
out = append(out, readCase("128 KiB", []part{rep('#', 128<<10)}, "", true))
out = append(out, readCase("64 KiB and a byte, encrypted", []part{hx([]byte("age-encryption.org/v1\n")), rep('x', 65536-21)}, "p", true))
// Encrypted files, with work factors of 1 and 2, cheap for the tests.
enc := func(name, seed, pass string, wf int, plain []byte, read string, node bool) {
out = append(out, readCase(name, []part{hx(sealed(seed, pass, wf, plain))}, read, node))
}
m1 := authorkey.Marshal(k1)
enc("encrypted, work factor 1", "read enc 1", "p", 1, m1, "p", true)
enc("encrypted, work factor 2, UTF-8 passphrase", "read enc 2", "pässwörd €", 2, m1, "pässwörd €", true)
enc("encrypted, wrong passphrase", "read enc 3", "p", 1, m1, "q", true)
enc("encrypted, no passphrase", "read enc 4", "p", 1, m1, "", true)
enc("encrypted, two keys", "read enc 5", "p", 1, []byte(s1+"\n"+s2+"\n"), "p", true)
enc("encrypted, no key", "read enc 6", "p", 1, []byte("# nothing\n"), "p", true)
enc("encrypted, empty", "read enc 7", "p", 1, nil, "p", true)
enc("encrypted, a key in lower case", "read enc 8", "p", 1, []byte(strings.ToLower(s1)), "p", true)
enc("encrypted, spaces around", "read enc 9", "p", 1, []byte(ideographicSpace+s1+paragraphSeparator+"\r"+lf), "p", true)
enc("encrypted, work factor 17", "read enc 10", "p", 17, m1, "p", false)
// Other work factors, edited into a file of work factor 1: age reads
// the work factor before it runs scrypt, and its MAC after.
w1 := sealed("read enc 11", "p", 1, m1)
for _, wf := range []string{"22", "0", "01", "31", "-1", "1 ", "16"} {
e := bytes.Replace(w1, []byte(" 1"+lf), []byte(" "+wf+lf), 1)
out = append(out, readCase("encrypted, work factor edited to "+wf, []part{hx(e)}, "p", wf != "16"))
}
large := sealedPart("read enc 12", "p", 1, []part{hx([]byte(s1 + "\n#")), rep('x', 65000), hx([]byte("\n"))})
out = append(out, readCase("encrypted, 64 KiB of plaintext", []part{large}, "p", true))
tooBig := sealedPart("read enc 13", "p", 1, []part{hx([]byte(s1 + "\n#")), rep('x', 65536)})
out = append(out, readCase("encrypted, more than 64 KiB", []part{tooBig}, "p", true))
f := sealed("read enc 14", "p", 1, m1)
out = append(out, readCase("encrypted, cut", []part{hx(f[:len(f)-1])}, "p", true))
out = append(out, readCase("encrypted, header only", []part{hx(f[:bytes.Index(f, []byte("\n--- "))+1])}, "p", true))
g := bytes.Clone(f)
g[len(g)-1] ^= 1
out = append(out, readCase("encrypted, last byte changed", []part{hx(g)}, "p", true))
g = bytes.Clone(f)
i := bytes.Index(g, []byte("\n--- ")) + 6
g[i] ^= 1
out = append(out, readCase("encrypted, MAC changed", []part{hx(g)}, "p", true))
out = append(out, readCase("encrypted, garbage", []part{hx([]byte("age-encryption.org/v1\n-> what\n"))}, "p", true))
// An X25519 recipient instead of scrypt.
var xbuf bytes.Buffer
with("read enc x25519", func() {
id, err := age.GenerateX25519Identity()
check(err)
w, err := age.Encrypt(&xbuf, id.Recipient())
check(err)
_, err = w.Write(m1)
check(err)
check(w.Close())
})
out = append(out, readCase("encrypted for X25519", []part{hx(xbuf.Bytes())}, "p", true))
return out
}
var testdata = flag.String("testdata", "", "the testdata of this repository")
func main() {
out := flag.String("out", "", "where the vectors go")
src := flag.String("source", "", "the commit of datekeys-go")
flag.Parse()
if *out == "" || *src == "" || *testdata == "" {
log.Fatal("usage: -source <commit> -testdata <dir> -out <dir>")
}
pub, sec := keyStrings()
doc := obj{
"source": *src,
"go": runtime.Version(),
"unicode": unicode.Version,
"description": "The author keys of package authorkey of datekeys-go and the signatures of crypto/ed25519, by tool/authorkey_go_vectors.go. A text is an index into texts. A file is a list of parts: {hex}, {byte, n}, or {sealed: {seed, passphrase, work_factor, plain}, length, sha256}, the age file of plain, a list of parts, for a scrypt recipient while crypto/rand reads the keystream of the seed. A message_pattern of n is n bytes with (31·i + 7) mod 256 as byte i. Draws are those of crypto/rand reading the keystream of SeededRandomSource. A case marked node false is left out compiled to JavaScript.",
"sign": signSection(),
"scalars": scalarSection(),
"keys": keySection(),
"generate": generateSection(),
"encrypt": encryptSection(),
"public": publicSection(pub),
"secret": secretSection(sec),
"read": readSection(),
}
write := func(runes int) []byte {
doc["runes"] = runeSection(runes)
doc["texts"] = texts
var buf bytes.Buffer
e := json.NewEncoder(&buf)
e.SetEscapeHTML(false)
e.SetIndent("", " ")
check(e.Encode(doc))
// Arrays of numbers on one line each.
b := numbers.ReplaceAllFunc(buf.Bytes(), func(m []byte) []byte { return spaces.ReplaceAll(m, nil) })
buf.Reset()
buf.Write(b)
if bytes.Contains(buf.Bytes(), []byte("'''")) {
log.Fatal("the JSON holds three quotes")
}
return buf.Bytes()
}
full := write(1)
path := filepath.Join(*out, "authorkey.json")
check(os.WriteFile(path, full, 0o644))
fmt.Printf("wrote %s, %d bytes\n", path, len(full))
// The Dart constant: one in eight of the seeded signatures and of the
// rune cases, the long messages left out.
signs := doc["sign"].([]obj)
var some []obj
for _, c := range signs {
if c["node"].(bool) {
some = append(some, c)
}
}
doc["sign"] = some
part := write(8)
dart := "// Generated by tool/authorkey_go_vectors.go: authorkey.json with a part of\n" +
"// its signatures and rune cases, for the tests that also run compiled to\n" +
"// JavaScript, where no file can be read. Do not edit.\n\n" +
"/// A part of test/vectors/authorkey.json.\n" +
"const authorKeyJson = r'''\n" + string(part) + "''';\n"
dpath := filepath.Join(*out, "authorkey.g.dart")
check(os.WriteFile(dpath, []byte(dart), 0o644))
fmt.Printf("wrote %s, %d bytes\n", dpath, len(dart))
}
// Characters written by their code points, so that the source stays ASCII
// where they matter.
var (
lf = string(rune(0x0a))
bom = string(rune(0xfeff))
ideographicSpace = string(rune(0x3000))
paragraphSeparator = string(rune(0x2029))
)
var (
numbers = regexp.MustCompile(`\[\s*-?[0-9]+(,\s*-?[0-9]+)*\s*\]`)
spaces = regexp.MustCompile(`\s+`)
)

Powered by TurnKey Linux.