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.
647 lines
25 KiB
647 lines
25 KiB
package agewrap_test
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"filippo.io/age"
|
|
"github.com/drand/drand/v2/crypto"
|
|
"github.com/drand/kyber"
|
|
bls "github.com/drand/kyber-bls12381"
|
|
"github.com/drand/kyber/encrypt/ibe"
|
|
"github.com/drand/tlock"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/agewrap"
|
|
"g.activething.com/go/DateKeys/codec/bech32"
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
"g.activething.com/go/DateKeys/provider"
|
|
)
|
|
|
|
// tlockNetwork adapts the pinned profile to tlock.Network, to run the
|
|
// official tlock code path against our stanzas.
|
|
type tlockNetwork struct {
|
|
p *profile.Profile
|
|
release provider.Release
|
|
}
|
|
|
|
func (n tlockNetwork) ChainHash() string { return n.p.ChainHashHex() }
|
|
func (n tlockNetwork) Current(time.Time) uint64 { return 1 << 40 }
|
|
func (n tlockNetwork) SwitchChainHash(string) error { return errors.New("forbidden") }
|
|
func (n tlockNetwork) Scheme() crypto.Scheme {
|
|
s, _ := n.p.DrandScheme()
|
|
return *s
|
|
}
|
|
func (n tlockNetwork) PublicKey() kyber.Point {
|
|
s, _ := n.p.DrandScheme()
|
|
k := s.KeyGroup.Point()
|
|
_ = k.UnmarshalBinary(n.p.PublicKey)
|
|
return k
|
|
}
|
|
func (n tlockNetwork) Signature(round uint64) ([]byte, error) {
|
|
if round != n.release.Round {
|
|
return nil, errors.New("unknown round")
|
|
}
|
|
return n.release.Signature, nil
|
|
}
|
|
|
|
func encrypt(t *testing.T, plaintext []byte, r ...age.Recipient) []byte {
|
|
t.Helper()
|
|
var b bytes.Buffer
|
|
w, err := age.Encrypt(&b, r...)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Write(plaintext)
|
|
if err := w.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b.Bytes()
|
|
}
|
|
|
|
func decrypt(file []byte, id age.Identity) ([]byte, error) {
|
|
r, err := age.Decrypt(bytes.NewReader(file), id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return io.ReadAll(r)
|
|
}
|
|
|
|
func TestTimeRecipientStanzaAndRoundTrip(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
rec, err := agewrap.NewTimeRecipient(p, 1000)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file := encrypt(t, []byte("control"), rec)
|
|
st, err := agewrap.Stanzas(bytes.NewReader(file))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(st) != 1 || st[0].Type != "tlock" || len(st[0].Args) != 2 || st[0].Args[0] != "1000" || st[0].Args[1] != p.ChainHashHex() || len(st[0].Body) != 128 {
|
|
t.Fatalf("stanza %+v", st)
|
|
}
|
|
id, _ := agewrap.NewTimeIdentity(p, 1000, testkit.Release(1000))
|
|
if got, err := decrypt(file, id); err != nil || string(got) != "control" {
|
|
t.Fatalf("round trip: %q %v", got, err)
|
|
}
|
|
}
|
|
|
|
// The stanza is the one of the tlock library and the tle CLI, in both
|
|
// directions (spec §32, plan §3.3).
|
|
func TestInteroperabilityWithTlockLibrary(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
net := tlockNetwork{p: p, release: testkit.Release(1000)}
|
|
|
|
rec, _ := agewrap.NewTimeRecipient(p, 1000)
|
|
ours := encrypt(t, []byte("from datekeys"), rec)
|
|
var out bytes.Buffer
|
|
if err := tlock.New(net).Strict().Decrypt(&out, bytes.NewReader(ours)); err != nil || out.String() != "from datekeys" {
|
|
t.Fatalf("tlock cannot open our file: %v", err)
|
|
}
|
|
|
|
var theirs bytes.Buffer
|
|
if err := tlock.New(net).Strict().Encrypt(&theirs, strings.NewReader("from tlock"), 1000); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, _ := agewrap.NewTimeIdentity(p, 1000, testkit.Release(1000))
|
|
if got, err := decrypt(theirs.Bytes(), id); err != nil || string(got) != "from tlock" {
|
|
t.Fatalf("we cannot open a tlock file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTimeRecipientCannotBeMixed(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
a, _ := agewrap.NewTimeRecipient(p, 1000)
|
|
b, _ := agewrap.NewTimeRecipient(p, 1000)
|
|
x, _ := age.GenerateX25519Identity()
|
|
for _, rs := range [][]age.Recipient{{a, x.Recipient()}, {a, b}} {
|
|
if _, err := age.Encrypt(io.Discard, rs...); err == nil {
|
|
t.Fatal("tlock recipient mixed with another recipient")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every rule is enforced in Unwrap even when the header MAC is valid, which
|
|
// is what a malicious creator produces (spec §27, §63).
|
|
func TestTimeIdentityStrictness(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
rec, _ := agewrap.NewTimeRecipient(p, 1000)
|
|
file, fk, err := testkit.Encrypt([]byte("control"), rec)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rewrite := func(edit func([]*age.Stanza) []*age.Stanza) []byte {
|
|
out, err := testkit.RewriteAge(file, fk, edit)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The rewritten header is authentic for age: the injected file key opens it.
|
|
if _, err := decrypt(out, age.NewInjectedFileKeyIdentity(fk)); err != nil {
|
|
t.Fatalf("rewritten file is not a valid age file: %v", err)
|
|
}
|
|
return out
|
|
}
|
|
backdoor, backdoorID, _ := testkit.X25519Stanza(fk)
|
|
cases := []struct {
|
|
name string
|
|
file []byte
|
|
want error
|
|
}{
|
|
{"extra X25519 stanza", rewrite(func(s []*age.Stanza) []*age.Stanza { return append(s, backdoor) }), datekeys.ErrPolicyStructureMismatch},
|
|
{"stanza type changed", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Type = "tlock2"; return s }), datekeys.ErrPolicyStructureMismatch},
|
|
{"third argument", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Args = append(s[0].Args, "x"); return s }), datekeys.ErrPolicyStructureMismatch},
|
|
{"other round", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Args[0] = "1001"; return s }), datekeys.ErrRoundMismatch},
|
|
{"round with leading zero", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Args[0] = "01000"; return s }), datekeys.ErrRoundMismatch},
|
|
{"other chain hash", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Args[1] = strings.Repeat("0", 64); return s }), datekeys.ErrProfileMismatch},
|
|
{"uppercase chain hash", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Args[1] = strings.ToUpper(s[0].Args[1]); return s }), datekeys.ErrProfileMismatch},
|
|
{"corrupted tlock body", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Body[100] ^= 1; return s }), datekeys.ErrIntegrity},
|
|
{"truncated tlock body", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Body = s[0].Body[:127]; return s }), datekeys.ErrIntegrity},
|
|
// Spec §12.2, §63 step 11: U || V || W with |U| = 96, and U the
|
|
// canonical encoding of a point of G2 other than the point at
|
|
// infinity. For a decoder that reduces c0 modulo p, c0 + p is U.
|
|
{"tlock body of 129 bytes", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Body = append(s[0].Body, 0); return s }), datekeys.ErrIntegrity},
|
|
{"U re-encoded with c0 + p", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Body = editU(t, s[0].Body, c0PlusP); return s }), datekeys.ErrIntegrity},
|
|
{"U the point at infinity", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Body = editU(t, s[0].Body, infinity); return s }), datekeys.ErrIntegrity},
|
|
{"U with the infinity flag and a payload", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Body = editU(t, s[0].Body, infinityWithPayload); return s }), datekeys.ErrIntegrity},
|
|
{"U negated", rewrite(func(s []*age.Stanza) []*age.Stanza { s[0].Body = editU(t, s[0].Body, negated); return s }), datekeys.ErrIntegrity},
|
|
}
|
|
id, _ := agewrap.NewTimeIdentity(p, 1000, testkit.Release(1000))
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if _, err := decrypt(tc.file, id); !errors.Is(err, tc.want) {
|
|
t.Fatalf("got %v, want %v", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
// Without the DateKeys rule, the backdoor stanza would open the file.
|
|
if got, err := decrypt(cases[0].file, backdoorID); err != nil || string(got) != "control" {
|
|
t.Fatalf("backdoor model broken: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTimeIdentityRelease(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
rec, _ := agewrap.NewTimeRecipient(p, 1000)
|
|
file := encrypt(t, []byte("control"), rec)
|
|
for _, tc := range []struct {
|
|
name string
|
|
rel provider.Release
|
|
want error
|
|
}{
|
|
{"release of another round", testkit.Release(1001), datekeys.ErrRoundMismatch},
|
|
{"relabelled signature", provider.Release{Round: 1000, Signature: testkit.Release(1001).Signature}, datekeys.ErrReleaseInvalid},
|
|
{"empty signature", provider.Release{Round: 1000}, datekeys.ErrReleaseInvalid},
|
|
{"negated signature", provider.Release{Round: 1000, Signature: testkit.Negated(testkit.Release(1000).Signature)}, datekeys.ErrReleaseInvalid},
|
|
{"signature the point at infinity", provider.Release{Round: 1000, Signature: testkit.Infinity(48)}, datekeys.ErrReleaseInvalid},
|
|
} {
|
|
id, _ := agewrap.NewTimeIdentity(p, 1000, tc.rel)
|
|
if _, err := decrypt(file, id); !errors.Is(err, tc.want) {
|
|
t.Errorf("%s: got %v, want %v", tc.name, err, tc.want)
|
|
}
|
|
}
|
|
// An identity for another round refuses the stanza before using the release.
|
|
id, _ := agewrap.NewTimeIdentity(p, 1001, testkit.Release(1001))
|
|
if _, err := decrypt(file, id); !errors.Is(err, datekeys.ErrRoundMismatch) {
|
|
t.Fatalf("identity for round 1001: %v", err)
|
|
}
|
|
// Spec §12.2: the published signature of a round re-encoded with x + p
|
|
// is refused, although it is the same point for a decoder that reduces
|
|
// x modulo p; the canonical one opens the file.
|
|
rec, _ = agewrap.NewTimeRecipient(p, testkit.XPlusPRound)
|
|
file = encrypt(t, []byte("control"), rec)
|
|
canonical := testkit.Release(testkit.XPlusPRound)
|
|
xPlusP, err := testkit.AddModulus(canonical.Signature, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, _ = agewrap.NewTimeIdentity(p, testkit.XPlusPRound, provider.Release{Round: testkit.XPlusPRound, Signature: xPlusP})
|
|
if _, err := decrypt(file, id); !errors.Is(err, datekeys.ErrReleaseInvalid) {
|
|
t.Errorf("signature re-encoded with x + p: %v", err)
|
|
}
|
|
id, _ = agewrap.NewTimeIdentity(p, testkit.XPlusPRound, canonical)
|
|
if got, err := decrypt(file, id); err != nil || string(got) != "control" {
|
|
t.Errorf("canonical signature of round %d: %q %v", testkit.XPlusPRound, got, err)
|
|
}
|
|
}
|
|
|
|
// Spec §63 step 11: H2 hashes the element of GT in the order of
|
|
// kilic/bls12-381, c1 before c0 at every level of the tower. The frozen
|
|
// vector H2(e(G1, G2)) of testdata/vectors/tlock_ibe.json pins the pairing and
|
|
// that serialization, and step 11 computed with them, sigma = V XOR
|
|
// H2(e(signature, U)) and FK_TIME = W XOR H4(sigma), recovers the file key
|
|
// that tlock unwraps. The reverse order, c0 first at every level as in the
|
|
// Fp12.toBytes of noble, gives another H2 and another key.
|
|
func TestTlockH2Vector(t *testing.T) {
|
|
var golden testkit.IBEVectorFile
|
|
if err := testkit.ReadJSON("../testdata/vectors/tlock_ibe.json", &golden); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := testkit.IBEVectors(); err != nil || !reflect.DeepEqual(got, golden) {
|
|
t.Fatalf("testdata/vectors/tlock_ibe.json is stale: run genfixtures (%v)", err)
|
|
}
|
|
const (
|
|
g1 = "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb"
|
|
g2 = "93e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e" +
|
|
"024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb8"
|
|
)
|
|
vec := golden.Vectors[0]
|
|
if len(golden.Vectors) != 1 || vec.G1 != g1 || vec.G2 != g2 || vec.H2 != "cb87319f24560b5231579a09ad79f12e" {
|
|
t.Fatalf("the frozen vector changed: %+v", golden.Vectors)
|
|
}
|
|
gt, err := hex.DecodeString(vec.GT)
|
|
if err != nil || len(gt) != testkit.GTLen {
|
|
t.Fatalf("gt of %d bytes: %v", len(gt), err)
|
|
}
|
|
if sum := sha256.Sum256(append([]byte("IBE-H2"), gt...)); hex.EncodeToString(sum[:testkit.H2Len]) != vec.H2 {
|
|
t.Fatal("h2 is not SHA-256(\"IBE-H2\" || gt) truncated to 16 bytes")
|
|
}
|
|
if h := hex.EncodeToString(testkit.H2(reversed(gt))); h != "0118eea9d5971745f71e3c94926f1717" {
|
|
t.Fatalf("H2 in the reverse order: %s", h)
|
|
}
|
|
|
|
// Step 11 on a stanza of round 1000 with the published release.
|
|
p := profile.Quicknet()
|
|
rec, _ := agewrap.NewTimeRecipient(p, 1000)
|
|
stanzas, err := agewrap.Stanzas(bytes.NewReader(encrypt(t, []byte("control"), rec)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
release := testkit.Release(1000)
|
|
id, _ := agewrap.NewTimeIdentity(p, 1000, release)
|
|
fileKey, err := id.Unwrap(stanzas)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scheme, err := p.DrandScheme()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body := stanzas[0].Body
|
|
u, v, w := body[:len(body)-2*testkit.H2Len], body[len(body)-2*testkit.H2Len:len(body)-testkit.H2Len], body[len(body)-testkit.H2Len:]
|
|
sig, point := scheme.SigGroup.Point(), scheme.KeyGroup.Point()
|
|
if err := sig.UnmarshalBinary(release.Signature); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := point.UnmarshalBinary(u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pairing, err := bls.NewBLS12381Suite().Pair(sig, point).MarshalBinary()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
gt []byte
|
|
opens bool
|
|
}{
|
|
{"the order of kilic/bls12-381", pairing, true},
|
|
{"the reverse order", reversed(pairing), false},
|
|
} {
|
|
sigma := xor(v, testkit.H2(tc.gt))
|
|
h4 := sha256.Sum256(append(ibe.H4Tag(), sigma...))
|
|
if got := xor(w, h4[:testkit.H2Len]); bytes.Equal(got, fileKey) != tc.opens {
|
|
t.Errorf("%s: FK_TIME %x, tlock unwraps %x", tc.name, got, fileKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
// reversed returns the twelve 48-byte coordinates of a serialization of GT in
|
|
// reverse order: c0 before c1 at every level of the tower.
|
|
func reversed(gt []byte) []byte {
|
|
out := make([]byte, 0, len(gt))
|
|
for i := len(gt) - testkit.CoordinateLen; i >= 0; i -= testkit.CoordinateLen {
|
|
out = append(out, gt[i:i+testkit.CoordinateLen]...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func xor(a, b []byte) []byte {
|
|
out := make([]byte, len(a))
|
|
for i := range a {
|
|
out[i] = a[i] ^ b[i]
|
|
}
|
|
return out
|
|
}
|
|
|
|
// U edits of a Quicknet tlock stanza body U || V || W (spec §12.2, §63 step
|
|
// 11).
|
|
var (
|
|
c0PlusP = func(u []byte) ([]byte, error) { return testkit.AddModulus(u, testkit.CoordinateLen) }
|
|
infinity = func(u []byte) ([]byte, error) { return testkit.Infinity(len(u)), nil }
|
|
infinityWithPayload = func(u []byte) ([]byte, error) { return testkit.InfinityWithPayload(u), nil }
|
|
negated = func(u []byte) ([]byte, error) { return testkit.Negated(u), nil }
|
|
)
|
|
|
|
func editU(t *testing.T, body []byte, edit func(u []byte) ([]byte, error)) []byte {
|
|
t.Helper()
|
|
out, err := testkit.EditU(edit)(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestPayloadIdentityStrictness(t *testing.T) {
|
|
iPayload, _ := age.GenerateX25519Identity()
|
|
raw, err := agewrap.RawX25519Identity(iPayload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, err := agewrap.NewPayloadIdentity(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
file, fk, _ := testkit.Encrypt([]byte("payload"), iPayload.Recipient())
|
|
if got, err := decrypt(file, id); err != nil || string(got) != "payload" {
|
|
t.Fatalf("round trip: %v", err)
|
|
}
|
|
backdoor, _, _ := testkit.X25519Stanza(fk)
|
|
extra, _ := testkit.RewriteAge(file, fk, func(s []*age.Stanza) []*age.Stanza { return append(s, backdoor) })
|
|
// Plain age accepts the file with I_PAYLOAD: the MAC is valid.
|
|
if _, err := decrypt(extra, iPayload); err != nil {
|
|
t.Fatalf("model broken: %v", err)
|
|
}
|
|
if _, err := decrypt(extra, id); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("extra stanza in PAYLOAD_AGE: %v", err)
|
|
}
|
|
other, _ := age.GenerateX25519Identity()
|
|
if _, err := decrypt(encrypt(t, []byte("x"), other.Recipient()), id); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
t.Fatalf("payload of another control: %v", err)
|
|
}
|
|
pw, _ := age.NewScryptRecipient("password")
|
|
pw.SetWorkFactor(10)
|
|
if _, err := decrypt(encrypt(t, []byte("x"), pw), id); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("scrypt payload: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAccessIdentityStrictness(t *testing.T) {
|
|
a, _ := age.GenerateX25519Identity()
|
|
b, _ := age.GenerateX25519Identity()
|
|
file, fk, _ := testkit.Encrypt([]byte("control"), a.Recipient(), b.Recipient())
|
|
for _, id := range []*age.X25519Identity{a, b} {
|
|
acc, _ := agewrap.NewAccessIdentity(0, id)
|
|
if got, err := decrypt(file, acc); err != nil || string(got) != "control" {
|
|
t.Fatalf("recipient cannot open: %v", err)
|
|
}
|
|
}
|
|
// A non-X25519 stanza is rejected although plain age would accept the file.
|
|
odd := &age.Stanza{Type: "scrypt", Args: []string{"c2FsdHNhbHRzYWx0c2FsdA", "10"}, Body: make([]byte, 32)}
|
|
withOdd, _ := testkit.RewriteAge(file, fk, func(s []*age.Stanza) []*age.Stanza { return append(s, odd) })
|
|
if _, err := decrypt(withOdd, a); err != nil {
|
|
t.Fatalf("model broken: %v", err)
|
|
}
|
|
acc, _ := agewrap.NewAccessIdentity(0, a)
|
|
if _, err := decrypt(withOdd, acc); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("non-X25519 stanza: %v", err)
|
|
}
|
|
// Two stanzas for the same recipient.
|
|
dup, _ := a.Recipient().Wrap(fk)
|
|
withDup, _ := testkit.RewriteAge(file, fk, func(s []*age.Stanza) []*age.Stanza { return append(s, dup[0]) })
|
|
if _, err := decrypt(withDup, acc); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("two stanzas for one recipient: %v", err)
|
|
}
|
|
// Spec §63 step 13, §69.1: the verdict does not depend on the order of
|
|
// the identities. b unwraps exactly one stanza of withDup and a two, in
|
|
// either order.
|
|
for i, ids := range [][]age.Identity{{a, b}, {b, a}} {
|
|
both, _ := agewrap.NewAccessIdentity(0, ids...)
|
|
if _, err := decrypt(withDup, both); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("two stanzas for a, order %d: %v", i, err)
|
|
}
|
|
}
|
|
stranger, _ := age.GenerateX25519Identity()
|
|
for _, ids := range [][]age.Identity{{stranger, b}, {b, stranger}} {
|
|
mixed, _ := agewrap.NewAccessIdentity(0, ids...)
|
|
if got, err := decrypt(file, mixed); err != nil || string(got) != "control" {
|
|
t.Fatalf("a stranger and a recipient: %v", err)
|
|
}
|
|
}
|
|
accS, _ := agewrap.NewAccessIdentity(0, stranger)
|
|
if _, err := decrypt(file, accS); !errors.Is(err, datekeys.ErrAccessInvalid) {
|
|
t.Fatalf("stranger: %v", err)
|
|
}
|
|
if _, err := agewrap.NewAccessIdentity(0); !errors.Is(err, datekeys.ErrAccessRequired) {
|
|
t.Fatalf("no identity: %v", err)
|
|
}
|
|
}
|
|
|
|
// Spec §39, §63 steps 12 and 13: in format 2 INNER_ACCESS_AGE holds exactly
|
|
// AccessSlots stanzas, a rule checked on its own and again by the identity;
|
|
// format 1 takes one or more.
|
|
func TestAccessSlots(t *testing.T) {
|
|
a, _ := age.GenerateX25519Identity()
|
|
for _, n := range []int{1, 15, 16, 17} {
|
|
recipients := []age.Recipient{a.Recipient()}
|
|
for len(recipients) < n {
|
|
id, _ := age.GenerateX25519Identity()
|
|
recipients = append(recipients, id.Recipient())
|
|
}
|
|
file := encrypt(t, []byte("control"), recipients...)
|
|
st, err := agewrap.Stanzas(bytes.NewReader(file))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
acc, _ := agewrap.NewAccessIdentity(agewrap.AccessSlots, a)
|
|
_, openErr := decrypt(file, acc)
|
|
for _, err := range []error{agewrap.CheckAccessStanzas(st, agewrap.AccessSlots), openErr} {
|
|
if (n == agewrap.AccessSlots) != (err == nil) || (err != nil && !errors.Is(err, datekeys.ErrPolicyStructureMismatch)) {
|
|
t.Fatalf("%d stanzas in format 2: %v", n, err)
|
|
}
|
|
}
|
|
acc1, _ := agewrap.NewAccessIdentity(0, a)
|
|
if err := agewrap.CheckAccessStanzas(st, 0); err != nil {
|
|
t.Fatalf("%d stanzas in format 1: %v", n, err)
|
|
}
|
|
if got, err := decrypt(file, acc1); err != nil || string(got) != "control" {
|
|
t.Fatalf("%d stanzas in format 1: %v", n, err)
|
|
}
|
|
}
|
|
if _, err := agewrap.NewAccessIdentity(-1, a); err == nil {
|
|
t.Fatal("negative slots accepted")
|
|
}
|
|
}
|
|
|
|
// Spec §37, §62.1 rule 3, §76 change 10: the X25519 recipients a writer must
|
|
// reject, non-canonical or of low order, whose stanza nobody or anybody
|
|
// opens; the rules of the reader cannot tell, because the stanza does not
|
|
// hold the recipient.
|
|
func TestNonCanonicalRecipients(t *testing.T) {
|
|
x, _ := age.GenerateX25519Identity()
|
|
if err := agewrap.CheckX25519Recipient(x.Recipient()); err != nil {
|
|
t.Fatalf("a generated recipient: %v", err)
|
|
}
|
|
fromRaw := func(b []byte) *age.X25519Recipient {
|
|
t.Helper()
|
|
s, err := bech32.Encode("age", b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := age.ParseX25519Recipient(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return r
|
|
}
|
|
ff := func(first, last byte) []byte {
|
|
return append(append([]byte{first}, bytes.Repeat([]byte{0xff}, 30)...), last)
|
|
}
|
|
raw, _ := agewrap.RawX25519Recipient(x.Recipient())
|
|
high := bytes.Clone(raw)
|
|
high[31] |= 0x80
|
|
mustHex := func(s string) []byte {
|
|
b, err := hex.DecodeString(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
for name, u := range map[string][]byte{
|
|
"bit 255 set": high,
|
|
"u = p": ff(0xed, 0x7f),
|
|
"u = p + 1": ff(0xee, 0x7f),
|
|
"u = 2^255 - 1": ff(0xff, 0x7f),
|
|
"low order, 0": make([]byte, 32),
|
|
"low order, 1": append([]byte{1}, make([]byte, 31)...),
|
|
"low order, p-1": ff(0xec, 0x7f),
|
|
"low order, 8 #1": mustHex("e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800"),
|
|
"low order, 8 #2": mustHex("5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157"),
|
|
} {
|
|
if err := agewrap.CheckX25519Recipient(fromRaw(u)); err == nil {
|
|
t.Errorf("%s accepted", name)
|
|
}
|
|
}
|
|
// The cases of spec §76: age encrypts to the recipient with bit 255 set
|
|
// a stanza that the identity cannot open, and refuses to encrypt to the
|
|
// zero point.
|
|
if _, err := decrypt(encrypt(t, []byte("x"), fromRaw(high)), x); err == nil {
|
|
t.Fatal("the identity opens the stanza of its non-canonical recipient")
|
|
}
|
|
w, err := age.Encrypt(io.Discard, fromRaw(make([]byte, 32)))
|
|
if err == nil {
|
|
err = w.Close()
|
|
}
|
|
if err == nil {
|
|
t.Fatal("age encrypts to a point of low order")
|
|
}
|
|
}
|
|
|
|
func TestStanzasProbe(t *testing.T) {
|
|
if _, err := agewrap.Stanzas(strings.NewReader("not age")); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
t.Fatalf("garbage: %v", err)
|
|
}
|
|
x, _ := age.GenerateX25519Identity()
|
|
file := encrypt(t, []byte("x"), x.Recipient())
|
|
st, err := agewrap.Stanzas(bytes.NewReader(file))
|
|
if err != nil || len(st) != 1 || st[0].Type != "X25519" {
|
|
t.Fatalf("%+v %v", st, err)
|
|
}
|
|
// Probing never needs a secret and leaves the stanzas untouched for age.
|
|
if _, err := decrypt(file, x); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRawKeys(t *testing.T) {
|
|
id, _ := age.GenerateX25519Identity()
|
|
raw, err := agewrap.RawX25519Identity(id)
|
|
if err != nil || len(raw) != 32 {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := agewrap.X25519IdentityFromRaw(raw)
|
|
if err != nil || back.String() != id.String() {
|
|
t.Fatal("identity round trip")
|
|
}
|
|
pub, err := agewrap.RawX25519Recipient(id.Recipient())
|
|
if err != nil || len(pub) != 32 {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := agewrap.X25519IdentityFromRaw(raw[:31]); err == nil {
|
|
t.Fatal("31-byte identity accepted")
|
|
}
|
|
}
|
|
|
|
func TestConstructorsRejectInvalidInput(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
for _, round := range []uint64{0, p.MaxRound() + 1} {
|
|
if _, err := agewrap.NewTimeRecipient(p, round); !errors.Is(err, datekeys.ErrDateKeyInvalid) {
|
|
t.Errorf("round %d: %v", round, err)
|
|
}
|
|
}
|
|
for name, edit := range map[string]func(p *profile.Profile){
|
|
"unknown scheme": func(p *profile.Profile) { p.Scheme = "nope" },
|
|
"public key not a point": func(p *profile.Profile) { p.PublicKey = bytes.Repeat([]byte{0xff}, 96) },
|
|
"identity element": func(p *profile.Profile) { p.PublicKey = append([]byte{0xc0}, make([]byte, 95)...) },
|
|
} {
|
|
bad := profile.Quicknet()
|
|
edit(bad)
|
|
if _, err := agewrap.NewTimeRecipient(bad, 1000); !errors.Is(err, datekeys.ErrUnknownProfile) {
|
|
t.Errorf("recipient, %s: %v", name, err)
|
|
}
|
|
if _, err := agewrap.NewTimeIdentity(bad, 1000, testkit.Release(1000)); !errors.Is(err, datekeys.ErrUnknownProfile) {
|
|
t.Errorf("identity, %s: %v", name, err)
|
|
}
|
|
}
|
|
if _, err := agewrap.NewPayloadIdentity(make([]byte, 31)); err == nil {
|
|
t.Fatal("31-byte I_PAYLOAD accepted")
|
|
}
|
|
}
|
|
|
|
func TestMalformedX25519Stanzas(t *testing.T) {
|
|
x, _ := age.GenerateX25519Identity()
|
|
file, fk, _ := testkit.Encrypt([]byte("data"), x.Recipient())
|
|
malformed, err := testkit.RewriteAge(file, fk, func(s []*age.Stanza) []*age.Stanza {
|
|
s[0].Args = append(s[0].Args, "extra")
|
|
return s
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := agewrap.RawX25519Identity(x)
|
|
pid, _ := agewrap.NewPayloadIdentity(raw)
|
|
if _, err := decrypt(malformed, pid); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
t.Fatalf("payload: %v", err)
|
|
}
|
|
acc, _ := agewrap.NewAccessIdentity(0, x)
|
|
if _, err := decrypt(malformed, acc); !errors.Is(err, datekeys.ErrIntegrity) {
|
|
t.Fatalf("access: %v", err)
|
|
}
|
|
st, _ := agewrap.Stanzas(bytes.NewReader(file))
|
|
if err := agewrap.CheckAccessStanzas(append(st, st[0]), 0); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("repeated stanza: %v", err)
|
|
}
|
|
if err := agewrap.CheckAccessStanzas(nil, 0); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("no stanza: %v", err)
|
|
}
|
|
if err := agewrap.CheckPayloadStanzas(append(st, st[0])); !errors.Is(err, datekeys.ErrPolicyStructureMismatch) {
|
|
t.Fatalf("two payload stanzas: %v", err)
|
|
}
|
|
}
|
|
|
|
func FuzzStanzas(f *testing.F) {
|
|
x, _ := age.GenerateX25519Identity()
|
|
var b bytes.Buffer
|
|
w, _ := age.Encrypt(&b, x.Recipient())
|
|
w.Close()
|
|
f.Add(b.Bytes())
|
|
f.Add([]byte("age-encryption.org/v1\n-> X25519 a\n\n--- AAAA\n"))
|
|
f.Fuzz(func(t *testing.T, in []byte) {
|
|
st, err := agewrap.Stanzas(bytes.NewReader(in))
|
|
if err != nil && !errors.Is(err, datekeys.ErrIntegrity) {
|
|
t.Fatalf("unexpected error class: %v", err)
|
|
}
|
|
_ = agewrap.CheckPayloadStanzas(st)
|
|
_ = agewrap.CheckAccessStanzas(st, 0)
|
|
_ = agewrap.CheckAccessStanzas(st, agewrap.AccessSlots)
|
|
})
|
|
}
|