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

959 lines
35 KiB

//go:build ignore
//go:debug cryptocustomrand=1
// Writes src/lib/dkc/testing/authorkey-vectors.json, the vectors of the
// author keys of authorkey.ts and of the signatures of ed25519sign.ts: the
// Ed25519 signatures of Go's crypto/ed25519 and the package authorkey of
// datekeys-go, with the texts of its errors. It is the generator of the Dart
// port (datekeys-dart, tool/authorkey_go_vectors.go), with the output that
// the tests of this library read:
//
// - 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 a
// ChaCha20 keystream under SHA-256(seed), zero nonce, with each draw in
// hexadecimal, which the tests hand to authorkey.ts in the same order;
// 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 with the draws that age made, which the tests
// write again with age-encryption and those draws. Arrays of numbers are
// written on one line.
//
// It imports only public packages, so it runs in the module of the
// reference implementation, in an export of datekeys-go at the tag
// spec-v0.12 made with git archive, which it does not change. From the root
// of this repository:
//
// tmp=$(mktemp -d)
// git -C ../datekeys-go archive spec-v0.12 | tar -x -C "$tmp"
// cp scripts/authorkey-go-vectors.go "$tmp/"
// src=$(git -C ../datekeys-go rev-parse 'spec-v0.12^{commit}')
// (cd "$tmp" && go run authorkey-go-vectors.go -source "$src" \
// -testdata "$OLDPWD/testdata" \
// -out "$OLDPWD/src/lib/dkc/testing/authorkey-vectors.json")
// rm -rf "$tmp"
//
// The output is the same on every run.
package main
import (
"bufio"
"bytes"
"encoding/base64"
"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-ts authorkey: " + 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)}
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())})
}
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, d := sealedDraws(seed, pass, wf, join(plain))
return part{"sealed": obj{"seed": seed, "passphrase": pass, "work_factor": wf, "plain": plain, "draws": drawsOf(d)}, "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)}
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 {
f, _ := sealedDraws(seed, pass, wf, plain)
return f
}
// sealedDraws is sealed with the draws of crypto/rand.
func sealedDraws(seed, pass string, wf int, plain []byte) ([]byte, [][]byte) {
var buf bytes.Buffer
d := 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(), d
}
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)
plain("a stray continuation byte alone on a line", "\x80\n"+s1)
plain("two stray continuation bytes before a key", "\x80\x80"+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))
// The stanza of a file of work factor 1 edited, which age reads before
// scrypt, or the bytes after its header, which it reads after the MAC.
hdrEnd := bytes.Index(f, []byte("\n--- ")) + 1
hdrEnd += bytes.IndexByte(f[hdrEnd:], '\n') + 1
lines := strings.SplitN(string(f[:hdrEnd]), "\n", 4) // intro, stanza, body, MAC and the rest
stanza, body := lines[1], lines[2]
args := strings.Fields(stanza) // "->", "scrypt", salt, work factor
edited := func(name, st, bd string, after []byte) {
e := []byte(lines[0] + "\n" + st + "\n" + bd + "\n" + lines[3])
out = append(out, readCase("encrypted, "+name, []part{hx(append(e, after...))}, "p", true))
}
rest := f[hdrEnd:]
edited("an X25519 stanza besides scrypt", stanza, body+"\n-> X25519 "+args[2]+"\n"+body, rest)
edited("a second scrypt stanza", stanza, body+"\n"+stanza+"\n"+body, rest)
edited("scrypt with one argument", "-> scrypt "+args[2], body, rest)
edited("scrypt with three arguments", stanza+" 1", body, rest)
edited("a salt that is not Base64", "-> scrypt !"+args[2][1:]+" 1", body, rest)
edited("a salt with bits after its end", "-> scrypt "+args[2][:21]+"B 1", body, rest)
edited("a salt of 15 bytes", "-> scrypt "+b64.EncodeToString(make([]byte, 15))+" 1", body, rest)
edited("a salt of 17 bytes", "-> scrypt "+b64.EncodeToString(make([]byte, 17))+" 1", body, rest)
edited("a work factor beyond 64 bits", "-> scrypt "+args[2]+" 99999999999999999999", body, rest)
edited("a work factor of 2^63", "-> scrypt "+args[2]+" 9223372036854775808", body, rest)
edited("a work factor of 2^63 - 1", "-> scrypt "+args[2]+" 9223372036854775807", body, rest)
raw, err := b64.DecodeString(body)
check(err)
edited("a body of 31 bytes", stanza, b64.EncodeToString(raw[:31]), rest)
edited("a body of 33 bytes", stanza, b64.EncodeToString(append(bytes.Clone(raw), 0)), rest)
out = append(out, readCase("encrypted, the header alone", []part{hx(f[:hdrEnd])}, "p", true))
out = append(out, readCase("encrypted, five bytes of the nonce", []part{hx(f[:hdrEnd+5])}, "p", true))
out = append(out, readCase("encrypted, the header and the nonce", []part{hx(f[:hdrEnd+16])}, "p", true))
out = append(out, readCase("encrypted, the header, the nonce and a byte", []part{hx(f[:hdrEnd+17])}, "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
}
// b64 is the Base64 of the stanzas of age: standard, without padding.
var b64 = base64.RawStdEncoding.Strict()
var testdata = flag.String("testdata", "", "the testdata of this repository")
func main() {
out := flag.String("out", "", "the JSON file to write")
src := flag.String("source", "", "the commit of datekeys-go")
flag.Parse()
if *out == "" || *src == "" || *testdata == "" {
log.Fatal("usage: -source <commit> -testdata <dir> -out <file>")
}
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 scripts/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, draws}, length, sha256}, the age file of plain, a list of parts, for a scrypt recipient with the draws of crypto/rand in their order. A message_pattern of n is n bytes with (31·i + 7) mod 256 as byte i. Draws are those of crypto/rand, in their order.",
"sign": signSection(),
"scalars": scalarSection(),
"keys": keySection(),
"generate": generateSection(),
"encrypt": encryptSection(),
"public": publicSection(pub),
"secret": secretSection(sec),
"read": readSection(),
"runes": runeSection(1),
}
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) })
check(os.WriteFile(*out, b, 0o644))
fmt.Printf("wrote %s, %d bytes\n", *out, len(b))
}
// 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.