mirror of
https://github.com/FiloSottile/age.git
synced 2025-12-23 05:25:14 +00:00
internal/testkit: new test framework
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
*.age binary
|
||||
*.test binary
|
||||
|
||||
@@ -75,7 +75,7 @@ func ExampleDecrypt() {
|
||||
}
|
||||
|
||||
func ExampleParseIdentities() {
|
||||
keyFile, err := os.Open("testdata/keys.txt")
|
||||
keyFile, err := os.Open("testdata/example_keys.txt")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open private keys file: %v", err)
|
||||
}
|
||||
|
||||
178
internal/testkit/testkit.go
Normal file
178
internal/testkit/testkit.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright 2022 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package testkit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age/internal/bech32"
|
||||
"golang.org/x/crypto/chacha20"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
var TestFileKey = []byte("YELLOW SUBMARINE")
|
||||
|
||||
var _, TestX25519Identity, _ = bech32.Decode(
|
||||
"AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0")
|
||||
|
||||
var TestX25519Recipient, _ = curve25519.X25519(TestX25519Identity, curve25519.Basepoint)
|
||||
|
||||
type TestFile struct {
|
||||
Buf bytes.Buffer
|
||||
rand io.Reader
|
||||
|
||||
streamKey []byte
|
||||
nonce [12]byte
|
||||
payload bytes.Buffer
|
||||
expect string
|
||||
comment string
|
||||
identities []string
|
||||
}
|
||||
|
||||
type zeroReader struct{}
|
||||
|
||||
func (zeroReader) Read(p []byte) (int, error) {
|
||||
for n := range p {
|
||||
p[n] = 0
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func NewTestFile() *TestFile {
|
||||
c, _ := chacha20.NewUnauthenticatedCipher(
|
||||
[]byte("TEST RANDOMNESS TEST RANDOMNESS!"), make([]byte, chacha20.NonceSize))
|
||||
return &TestFile{rand: cipher.StreamReader{c, zeroReader{}}, expect: "success"}
|
||||
}
|
||||
|
||||
func (f *TestFile) TextLine(s string) {
|
||||
f.Buf.WriteString(s)
|
||||
f.Buf.WriteString("\n")
|
||||
}
|
||||
|
||||
func (f *TestFile) VersionLine(v string) {
|
||||
f.TextLine("age-encryption.org/" + v)
|
||||
}
|
||||
|
||||
func (f *TestFile) ArgsLine(args ...string) {
|
||||
f.TextLine(strings.Join(append([]string{"->"}, args...), " "))
|
||||
}
|
||||
|
||||
var b64 = base64.RawStdEncoding.EncodeToString
|
||||
|
||||
func (f *TestFile) Body(body []byte) {
|
||||
for {
|
||||
line := body
|
||||
if len(line) > 48 {
|
||||
line = line[:48]
|
||||
}
|
||||
f.TextLine(b64(line))
|
||||
body = body[len(line):]
|
||||
if len(line) < 48 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *TestFile) Stanza(args []string, body []byte) {
|
||||
f.ArgsLine(args...)
|
||||
f.Body(body)
|
||||
}
|
||||
|
||||
func (f *TestFile) AEADBody(key, body []byte) {
|
||||
aead, _ := chacha20poly1305.New(key)
|
||||
f.Body(aead.Seal(nil, make([]byte, chacha20poly1305.NonceSize), body, nil))
|
||||
}
|
||||
|
||||
func (f *TestFile) X25519(identity []byte) {
|
||||
id, _ := bech32.Encode("AGE-SECRET-KEY-", identity)
|
||||
f.identities = append(f.identities, id)
|
||||
recipient, _ := curve25519.X25519(identity, curve25519.Basepoint)
|
||||
ephemeral := make([]byte, 32)
|
||||
f.rand.Read(ephemeral)
|
||||
share, _ := curve25519.X25519(ephemeral, curve25519.Basepoint)
|
||||
f.ArgsLine("X25519", b64(share))
|
||||
secret, _ := curve25519.X25519(ephemeral, recipient)
|
||||
key := make([]byte, 32)
|
||||
hkdf.New(sha256.New, secret, append(share, recipient...),
|
||||
[]byte("age-encryption.org/v1/X25519")).Read(key)
|
||||
f.AEADBody(key, TestFileKey)
|
||||
}
|
||||
|
||||
func (f *TestFile) HMACLine(h []byte) {
|
||||
f.TextLine("--- " + b64(h))
|
||||
}
|
||||
|
||||
func (f *TestFile) HMAC() {
|
||||
key := make([]byte, 32)
|
||||
hkdf.New(sha256.New, TestFileKey, nil, []byte("header")).Read(key)
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write(f.Buf.Bytes())
|
||||
h.Write([]byte("---"))
|
||||
f.HMACLine(h.Sum(nil))
|
||||
}
|
||||
|
||||
func (f *TestFile) Nonce() {
|
||||
nonce := make([]byte, 16)
|
||||
f.rand.Read(nonce)
|
||||
f.streamKey = make([]byte, 32)
|
||||
hkdf.New(sha256.New, TestFileKey, nonce, []byte("payload")).Read(f.streamKey)
|
||||
f.Buf.Write(nonce)
|
||||
}
|
||||
|
||||
func (f *TestFile) PayloadChunk(plaintext []byte) {
|
||||
f.payload.Write(plaintext)
|
||||
aead, _ := chacha20poly1305.New(f.streamKey)
|
||||
f.Buf.Write(aead.Seal(nil, f.nonce[:], plaintext, nil))
|
||||
f.nonce[10]++
|
||||
}
|
||||
|
||||
func (f *TestFile) PayloadChunkFinal(plaintext []byte) {
|
||||
f.payload.Write(plaintext)
|
||||
f.nonce[11] = 1
|
||||
aead, _ := chacha20poly1305.New(f.streamKey)
|
||||
f.Buf.Write(aead.Seal(nil, f.nonce[:], plaintext, nil))
|
||||
}
|
||||
|
||||
func (f *TestFile) Payload(plaintext string) {
|
||||
f.Nonce()
|
||||
f.PayloadChunkFinal([]byte(plaintext))
|
||||
}
|
||||
|
||||
func (f *TestFile) ExpectHeaderFailure() {
|
||||
f.expect = "header failure"
|
||||
}
|
||||
|
||||
func (f *TestFile) ExpectPayloadFailure() {
|
||||
f.expect = "payload failure"
|
||||
}
|
||||
|
||||
func (f *TestFile) Comment(c string) {
|
||||
f.comment = c
|
||||
}
|
||||
|
||||
func (f *TestFile) Generate() {
|
||||
fmt.Printf("expect: %s\n", f.expect)
|
||||
if f.expect == "success" {
|
||||
fmt.Printf("payload: %x\n", sha256.Sum256(f.payload.Bytes()))
|
||||
}
|
||||
for _, id := range f.identities {
|
||||
fmt.Printf("identity: %s\n", id)
|
||||
}
|
||||
if f.comment != "" {
|
||||
fmt.Printf("comment: %s\n", f.comment)
|
||||
}
|
||||
fmt.Println()
|
||||
io.Copy(os.Stdout, &f.Buf)
|
||||
}
|
||||
18
testdata/x25519.go
vendored
Normal file
18
testdata/x25519.go
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright 2022 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import "filippo.io/age/internal/testkit"
|
||||
|
||||
func main() {
|
||||
f := testkit.NewTestFile()
|
||||
f.VersionLine("v1")
|
||||
f.X25519(testkit.TestX25519Recipient)
|
||||
f.HMAC()
|
||||
f.Payload("age")
|
||||
f.Generate()
|
||||
}
|
||||
9
testdata/x25519.test
vendored
Normal file
9
testdata/x25519.test
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
expect: success
|
||||
payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab
|
||||
identity: AGE-SECRET-KEY-1XMWWC06LY3EE5RYTXM9MFLAZ2U56JJJ36S0MYPDRWSVLUL66MV4QX3S7F6
|
||||
|
||||
age-encryption.org/v1
|
||||
-> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc
|
||||
EmECAEcKN+n/Vs9SbWiV+Hu0r+E8R77DdWYyd83nw7U
|
||||
--- Vn+54jqiiUCE+WZcEVY3f1sqHjlu/z1LCQ/T7Xm7qI0
|
||||
îÏbÇΑ´3'NhÔòùL·L[þ÷¾ªRÈð¼™,ƒ1ûf
|
||||
135
testkit_test.go
Normal file
135
testkit_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2022 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package age_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
//go:generate go test -generate -run ^$
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
genFlag := flag.Bool("generate", false, "regenerate test files")
|
||||
flag.Parse()
|
||||
if *genFlag {
|
||||
generators, err := filepath.Glob("testdata/*.go")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, generator := range generators {
|
||||
vector := strings.TrimSuffix(generator, ".go") + ".test"
|
||||
fmt.Fprintf(os.Stderr, "%s -> %s\n", generator, vector)
|
||||
out, err := exec.Command("go", "run", generator).Output()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.WriteFile(vector, out, 0664)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestVectors(t *testing.T) {
|
||||
tests, err := filepath.Glob("testdata/*.test")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, test := range tests {
|
||||
contents, err := os.ReadFile(test)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := strings.TrimPrefix(test, "testdata/")
|
||||
name = strings.TrimSuffix(name, ".test")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testVector(t, contents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testVector(t *testing.T, test []byte) {
|
||||
var (
|
||||
expectHeaderFailure bool
|
||||
expectPayloadFailure bool
|
||||
payloadHash *[32]byte
|
||||
identities []age.Identity
|
||||
)
|
||||
|
||||
for {
|
||||
line, rest, ok := bytes.Cut(test, []byte("\n"))
|
||||
if !ok {
|
||||
t.Fatal("invalid test file: no payload")
|
||||
}
|
||||
test = rest
|
||||
if len(line) == 0 {
|
||||
break
|
||||
}
|
||||
key, value, _ := strings.Cut(string(line), ": ")
|
||||
switch key {
|
||||
case "expect":
|
||||
switch value {
|
||||
case "success":
|
||||
case "header failure":
|
||||
expectHeaderFailure = true
|
||||
case "payload failure":
|
||||
expectPayloadFailure = true
|
||||
default:
|
||||
t.Fatal("invalid test file: unknown expect value:", value)
|
||||
}
|
||||
case "payload":
|
||||
h, err := hex.DecodeString(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payloadHash = (*[32]byte)(h)
|
||||
case "identity":
|
||||
i, err := age.ParseX25519Identity(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
identities = append(identities, i)
|
||||
case "comment":
|
||||
t.Log(value)
|
||||
default:
|
||||
t.Fatal("invalid test file: unknown header key:", key)
|
||||
}
|
||||
}
|
||||
|
||||
r, err := age.Decrypt(bytes.NewReader(test), identities...)
|
||||
if err != nil {
|
||||
if expectHeaderFailure {
|
||||
return
|
||||
}
|
||||
t.Fatal("unexpected header error:", err)
|
||||
} else if expectHeaderFailure {
|
||||
t.Fatal("expected header error")
|
||||
}
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
if expectPayloadFailure {
|
||||
return
|
||||
}
|
||||
t.Fatal("unexpected payload error:", err)
|
||||
} else if expectPayloadFailure {
|
||||
t.Fatal("expected payload error")
|
||||
}
|
||||
if sha256.Sum256(out) != *payloadHash {
|
||||
t.Error("payload hash mismatch")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user