From ba09500b55212a5637a0b7a691e0ea3778ab3c39 Mon Sep 17 00:00:00 2001 From: ralph-infrawatch Date: Mon, 22 Jun 2026 15:12:23 +0200 Subject: [PATCH] fix: detect RSA public keys against modern OpenSSH (issue #48) --- auth/halfauth_research.go | 5 +++++ auth/results.go | 21 +++++++++++++++------ auth/results_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 auth/results_test.go diff --git a/auth/halfauth_research.go b/auth/halfauth_research.go index 9b9c61d..9ed39f9 100644 --- a/auth/halfauth_research.go +++ b/auth/halfauth_research.go @@ -29,3 +29,8 @@ func (s *HalfSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { copy(s.SignData, data) return nil, ErrHalfAuth } + +// SignWithAlgorithm makes HalfSigner an ssh.AlgorithmSigner so RSA keys are probed with rsa-sha2-* instead of SHA-1 ssh-rsa. +func (s *HalfSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) { + return s.Sign(rand, data) +} diff --git a/auth/results.go b/auth/results.go index 5b86216..3fbf066 100644 --- a/auth/results.go +++ b/auth/results.go @@ -70,22 +70,31 @@ func (r *AuthResult) SupportsHostKey(t string) bool { } func (r *AuthResult) SupportsPubKeyType(t string) bool { - // Example: ssh-ed25519,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-256,rsa-sha2-512,ssh-rsa,ssh-dss okTypes, ok := r.Extensions["server-sig-algs"] - if !ok { - // Assume all types are supported unless the server - // has told us otherwise via the extension. + if !ok || strings.TrimSpace(okTypes) == "" { return true } + // ssh-rsa keys are usable when the server advertises any RSA signature algo; + // modern OpenSSH (>= 8.8) only offers rsa-sha2-256/rsa-sha2-512, not ssh-rsa. + wanted := sigAlgosForKeyType(t) for kt := range strings.SplitSeq(okTypes, ",") { kt = strings.TrimSpace(kt) - if strings.EqualFold(kt, t) { - return true + for _, w := range wanted { + if strings.EqualFold(kt, w) { + return true + } } } return false } +func sigAlgosForKeyType(t string) []string { + if t == "ssh-rsa" { + return []string{"ssh-rsa", "rsa-sha2-256", "rsa-sha2-512"} + } + return []string{t} +} + func (r *AuthResult) AddVuln(v VulnResult) { r.Vulns = append(r.Vulns, v) } diff --git a/auth/results_test.go b/auth/results_test.go new file mode 100644 index 0000000..f2aa294 --- /dev/null +++ b/auth/results_test.go @@ -0,0 +1,27 @@ +package auth + +import "testing" + +func TestSupportsPubKeyType(t *testing.T) { + const modern = "ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256" + const legacy = "ssh-ed25519,rsa-sha2-256,rsa-sha2-512,ssh-rsa" + + cases := []struct { + sigAlgs string + keyType string + want bool + }{ + {modern, "ssh-rsa", true}, + {modern, "ssh-ed25519", true}, + {legacy, "ssh-rsa", true}, + {"ssh-ed25519", "ssh-rsa", false}, + {modern, "ecdsa-sha2-nistp521", false}, + } + + for _, tc := range cases { + r := &AuthResult{Extensions: map[string]string{"server-sig-algs": tc.sigAlgs}} + if got := r.SupportsPubKeyType(tc.keyType); got != tc.want { + t.Errorf("SupportsPubKeyType(%q) with %q = %v, want %v", tc.keyType, tc.sigAlgs, got, tc.want) + } + } +}