age: don't reorder caller identities

Reported by Joe Doyle of Trail of Bits.
This commit is contained in:
Filippo Valsorda
2026-08-29 19:30:10 +02:00
parent 45445f91cf
commit fb6bef07c3
2 changed files with 80 additions and 0 deletions
+2
View File
@@ -321,6 +321,8 @@ func decryptHdr(hdr *format.Header, identities ...Identity) ([]byte, error) {
if len(identities) == 0 {
return nil, errors.New("no identities specified")
}
// Don't sort the caller's slice in place.
identities = slices.Clone(identities)
slices.SortStableFunc(identities, func(a, b Identity) int {
var aIsNative, bIsNative bool
switch a.(type) {
+78
View File
@@ -15,6 +15,7 @@ import (
"os"
"slices"
"strings"
"sync"
"testing"
"filippo.io/age"
@@ -441,6 +442,83 @@ func (ti *testIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return nil, age.ErrIncorrectIdentity
}
type noMatchIdentity int
func (noMatchIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return nil, age.ErrIncorrectIdentity
}
func TestDecryptDoesNotReorderIdentities(t *testing.T) {
identity, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := age.Encrypt(buf, identity.Recipient())
if err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
encrypted := buf.Bytes()
header, err := age.ExtractHeader(bytes.NewReader(encrypted))
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
decrypt func([]age.Identity) error
}{
{"Decrypt", func(identities []age.Identity) error {
_, err := age.Decrypt(bytes.NewReader(encrypted), identities...)
return err
}},
{"DecryptReaderAt", func(identities []age.Identity) error {
_, _, err := age.DecryptReaderAt(bytes.NewReader(encrypted), int64(len(encrypted)), identities...)
return err
}},
{"DecryptHeader", func(identities []age.Identity) error {
_, err := age.DecryptHeader(header, identities...)
return err
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
identities := []age.Identity{noMatchIdentity(1), identity, noMatchIdentity(2)}
want := slices.Clone(identities)
if err := tt.decrypt(identities); err != nil {
t.Fatal(err)
}
if !slices.Equal(identities, want) {
t.Error("identities were reordered")
}
})
}
t.Run("concurrent", func(t *testing.T) {
identities := []age.Identity{noMatchIdentity(1), identity, noMatchIdentity(2)}
want := slices.Clone(identities)
var wg sync.WaitGroup
for range 4 {
wg.Add(1)
go func() {
defer wg.Done()
for range 20 {
if _, err := age.Decrypt(bytes.NewReader(encrypted), identities...); err != nil {
t.Error(err)
}
}
}()
}
wg.Wait()
if !slices.Equal(identities, want) {
t.Error("identities were reordered")
}
})
}
func TestDecryptNativeIdentitiesFirst(t *testing.T) {
correct, err := age.GenerateX25519Identity()
if err != nil {