//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ž", " ", "�", "\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 -testdata -out ") } 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+`) )