agessh: don't cache mismatched encrypted 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 86c6cc24c0
commit e0ffaca260
2 changed files with 71 additions and 3 deletions
+66
View File
@@ -9,6 +9,7 @@ import (
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"encoding/pem"
"reflect"
"testing"
@@ -185,3 +186,68 @@ func TestSSHEd25519FingerprintCollision(t *testing.T) {
t.Errorf("invalid output: %x, expected %x", out, fileKey)
}
}
func TestEncryptedSSHIdentityMismatch(t *testing.T) {
announcedPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
embeddedPub, embeddedPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
announcedKey, err := ssh.NewPublicKey(announcedPub)
if err != nil {
t.Fatal(err)
}
embeddedKey, err := ssh.NewPublicKey(embeddedPub)
if err != nil {
t.Fatal(err)
}
passphrase := []byte("passphrase")
block, err := ssh.MarshalPrivateKeyWithPassphrase(embeddedPriv, "", passphrase)
if err != nil {
t.Fatal(err)
}
prompts := 0
i, err := agessh.NewEncryptedSSHIdentity(announcedKey, pem.EncodeToMemory(block), func() ([]byte, error) {
prompts++
return passphrase, nil
})
if err != nil {
t.Fatal(err)
}
announcedRecipient, err := agessh.NewEd25519Recipient(announcedKey)
if err != nil {
t.Fatal(err)
}
embeddedRecipient, err := agessh.NewEd25519Recipient(embeddedKey)
if err != nil {
t.Fatal(err)
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
}
stanzas, err := announcedRecipient.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
embeddedStanzas, err := embeddedRecipient.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
stanzas = append(stanzas, embeddedStanzas...)
for attempt := 1; attempt <= 2; attempt++ {
_, err := i.Unwrap(stanzas)
if err == nil || err.Error() != "mismatched private and public SSH key" {
t.Fatalf("attempt %d: unexpected error: %v", attempt, err)
}
if prompts != attempt {
t.Fatalf("attempt %d: passphrase callback invoked %d times", attempt, prompts)
}
}
}
+5 -3
View File
@@ -109,16 +109,17 @@ func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, er
var pubKey interface {
Equal(x crypto.PublicKey) bool
}
var decrypted age.Identity
switch k := k.(type) {
case *ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(*k)
decrypted, err = NewEd25519Identity(*k)
pubKey = k.Public().(ed25519.PublicKey)
// ParseRawPrivateKey returns inconsistent types. See Issue 429.
case ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(k)
decrypted, err = NewEd25519Identity(k)
pubKey = k.Public().(ed25519.PublicKey)
case *rsa.PrivateKey:
i.decrypted, err = NewRSAIdentity(k)
decrypted, err = NewRSAIdentity(k)
pubKey = &k.PublicKey
default:
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
@@ -131,5 +132,6 @@ func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, er
return nil, fmt.Errorf("mismatched private and public SSH key")
}
i.decrypted = decrypted
return i.decrypted.Unwrap(stanzas)
}