diff --git a/ssh/knownhosts/knownhosts.go b/ssh/knownhosts/knownhosts.go index e57cf5b..1ef1c35 100644 --- a/ssh/knownhosts/knownhosts.go +++ b/ssh/knownhosts/knownhosts.go @@ -178,7 +178,7 @@ func nextWord(line []byte) (string, []byte) { return string(line), nil } - return string(line[:i]), bytes.TrimSpace(line[i:]) + return string(line[:i]), trimSpace(line[i:]) } func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) { @@ -387,7 +387,7 @@ func (db *hostKeyDB) Read(r io.Reader, filename string) error { for scanner.Scan() { lineNum++ line := scanner.Bytes() - line = bytes.TrimSpace(line) + line = trimSpace(line) if len(line) == 0 || line[0] == '#' { continue } @@ -535,3 +535,10 @@ func newHashedHost(encoded string) (*hashedHost, error) { func (h *hashedHost) match(a addr) bool { return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash) } + +// trimSpace removes leading and trailing ASCII whitespace (space and tab). It +// is used instead of bytes.TrimSpace to match OpenSSH behavior, which strictly +// parses only ASCII space (0x20) and tab (0x09) as whitespace. +func trimSpace(in []byte) []byte { + return bytes.Trim(in, " \t") +} diff --git a/ssh/knownhosts/knownhosts_test.go b/ssh/knownhosts/knownhosts_test.go index 4d3ca59..8e1fba2 100644 --- a/ssh/knownhosts/knownhosts_test.go +++ b/ssh/knownhosts/knownhosts_test.go @@ -8,6 +8,7 @@ import ( "bytes" "crypto/ed25519" "crypto/rand" + "encoding/base64" "fmt" "net" "reflect" @@ -407,3 +408,41 @@ func TestIssue36126(t *testing.T) { t.Errorf("should have passed the check, got %v", err) } } + +func TestUnicodeSpace(t *testing.T) { + line := fmt.Sprintf("server.org %s  %s", edKey.Type(), base64.StdEncoding.EncodeToString(edKey.Marshal())) + + db := newHostKeyDB() + err := db.Read(bytes.NewBufferString(line), "testdb") + + if err == nil { + t.Fatal("Read succeeded on line with Unicode space, expected error due to strict ASCII parsing") + } +} + +func TestUnicodeSpaceTrimming(t *testing.T) { + const unicodeSpace = " " + line := fmt.Sprintf("%sserver.org %s", unicodeSpace, edKeyStr) + + db := newHostKeyDB() + err := db.Read(bytes.NewBufferString(line), "testdb") + + if err != nil { + t.Fatalf("Read failed: %v", err) + } + + lines := db.lines + if len(lines) != 1 { + t.Fatalf("Expected 1 line, got %d", len(lines)) + } + + cleanAddr := addr{host: "server.org", port: "22"} + dirtyAddr := addr{host: unicodeSpace + "server.org", port: "22"} + + if lines[0].match(cleanAddr) { + t.Errorf("Matched clean host 'server.org', implying Unicode space was trimmed") + } + if !lines[0].match(dirtyAddr) { + t.Errorf("Did not match dirty host, implying parsing issue") + } +}