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

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 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.