From 343dda9aa49869524609b20a7b61f40557ce6569 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sat, 29 Aug 2026 16:30:44 +0200 Subject: [PATCH] cmd/age: reject malformed supported SSH recipients Reported by Joe Doyle of Trail of Bits. --- cmd/age/age_test.go | 46 +++++++++++++++++++++++++++++++++++++++++++++ cmd/age/parse.go | 26 ++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/cmd/age/age_test.go b/cmd/age/age_test.go index 1d9420f..f091e94 100644 --- a/cmd/age/age_test.go +++ b/cmd/age/age_test.go @@ -117,6 +117,52 @@ func TestParseFileSizeLimit(t *testing.T) { } } +func TestUnsupportedSSHKey(t *testing.T) { + tests := []struct { + name, key, want string + ok bool + }{ + { + "truncated Ed25519", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FG", + "", false, + }, + { + "type-only RSA", + "ssh-rsa AAAAB3NzaC1yc2E=", + "", false, + }, + { + "small RSA", + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCxlfoYpG04TTnmjocggQZI5l0fjvaUMky1ZD5zwktDKAgBj441OAtFj/3m7Gujpxx/8w3jjJoUXVtU+NFbcC972ROpPI4aPq3OR5SmuuR5bCr6efeZlyAEMY/DmbkeUe1TrvdU3VMDGJAvEEzT1wzokKt9PiwbV2jNkdPjaG5LDQ==", + "ssh-rsa", true, + }, + { + "invalid Ed25519 point", + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIC", + "", false, + }, + { + "unknown type", + "totally-not-a-key AAAAEXRvdGFsbHktbm90LWEta2V5", + "totally-not-a-key", true, + }, + { + "ECDSA", + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOSh0Qo5zaBE33EiJnxwIBuZml9Nt9rCfvGi++FniWSk8X/10EO4E7KMgqGLlkCTDNIcbuKcsgfMw1/tifB8aTc=", + "ecdsa-sha2-nistp256", true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := unsupportedSSHKey(tt.key) + if got != tt.want || ok != tt.ok { + t.Errorf("unsupportedSSHKey() = %q, %v; want %q, %v", got, ok, tt.want, tt.ok) + } + }) + } +} + var buildExtraCommands = sync.OnceValue(func() error { bindir := filepath.SplitList(os.Getenv("PATH"))[0] // Build age-keygen and age-plugin-pq into the test binary directory. diff --git a/cmd/age/parse.go b/cmd/age/parse.go index f209630..d83a0cc 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -6,6 +6,7 @@ package main import ( "bufio" + "crypto/rsa" "encoding/base64" "fmt" "io" @@ -88,7 +89,7 @@ func parseRecipientsFile(name string) ([]age.Recipient, error) { } r, err := parseRecipient(line) if err != nil { - if t, ok := sshKeyType(line); ok { + if t, unsupported := unsupportedSSHKey(line); unsupported { // Skip unsupported but valid SSH public keys with a warning. warningf("recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n) continue @@ -114,7 +115,7 @@ func parseRecipientsFile(name string) ([]age.Recipient, error) { return recs, nil } -func sshKeyType(s string) (string, bool) { +func unsupportedSSHKey(s string) (string, bool) { // TODO: also ignore options? And maybe support multiple spaces and tabs as // field separators like OpenSSH? fields := strings.Split(s, " ") @@ -132,7 +133,26 @@ func sshKeyType(s string) (string, bool) { return "", false } if t := fields[0]; t == string(typeBytes) { - return t, true + switch t { + case "ssh-ed25519": + return "", false + case "ssh-rsa": + out, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s)) + if err != nil { + return "", false + } + cryptoKey, ok := out.(ssh.CryptoPublicKey) + if !ok { + return "", false + } + rsaKey, ok := cryptoKey.CryptoPublicKey().(*rsa.PublicKey) + if !ok { + return "", false + } + return t, rsaKey.N.BitLen() < 2048 + default: + return t, true + } } return "", false }