@@ -4,13 +4,15 @@ import (
44 "bytes"
55 "context"
66 "io"
7+ "log/slog"
78 "os"
89 "slices"
910 "strings"
1011 "testing"
1112 "time"
1213
1314 "github.com/superfly/ltx"
15+ "golang.org/x/crypto/ssh"
1416
1517 "github.com/benbjohnson/litestream"
1618 "github.com/benbjohnson/litestream/internal/testingutil"
@@ -417,3 +419,79 @@ func TestReplicaClient_S3_BucketValidation(t *testing.T) {
417419 t .Errorf ("expected bucket validation error, got: %v" , err )
418420 }
419421}
422+
423+ func TestReplicaClient_SFTP_HostKeyValidation (t * testing.T ) {
424+ testHostKeyPEM := `-----BEGIN OPENSSH PRIVATE KEY-----
425+ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
426+ QyNTUxOQAAACAJytPhncDnpV5QF3ai8f6r0u1hzK96x+81tvtA7ZiuawAAAJAIcGGVCHBh
427+ lQAAAAtzc2gtZWQyNTUxOQAAACAJytPhncDnpV5QF3ai8f6r0u1hzK96x+81tvtA7Ziuaw
428+ AAAEDzV1D6COyvFGhSiZa6ll9aXZ2IMWED3KGrvCNjEEtYHwnK0+GdwOelXlAXdqLx/qvS
429+ 7WHMr3rH7zW2+0DtmK5rAAAADGZlbGl4QGJvcmVhcwE=
430+ -----END OPENSSH PRIVATE KEY-----`
431+ privateKey , err := ssh .ParsePrivateKey ([]byte (testHostKeyPEM ))
432+ if err != nil {
433+ t .Fatal (err )
434+ }
435+
436+ t .Run ("ValidHostKey" , func (t * testing.T ) {
437+ addr := testingutil .MockSFTPServer (t , privateKey )
438+ expectedHostKey := string (ssh .MarshalAuthorizedKey (privateKey .PublicKey ()))
439+
440+ c := testingutil .NewSFTPReplicaClient (t )
441+ c .User = "foo"
442+ c .Host = addr
443+ c .HostKey = expectedHostKey
444+
445+ _ , err = c .Init (context .Background ())
446+ if err != nil {
447+ t .Fatalf ("SFTP connection failed: %v" , err )
448+ }
449+ })
450+ t .Run ("InvalidHostKey" , func (t * testing.T ) {
451+ addr := testingutil .MockSFTPServer (t , privateKey )
452+ invalidHostKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEqM2NkGvKKhR1oiKO0E72L3tOsYk+aX7H8Xn4bbZKsa"
453+
454+ c := testingutil .NewSFTPReplicaClient (t )
455+ c .User = "foo"
456+ c .Host = addr
457+ c .HostKey = invalidHostKey
458+
459+ _ , err = c .Init (context .Background ())
460+ if err == nil {
461+ t .Fatalf ("SFTP connection established despite invalid host key" )
462+ }
463+ if ! strings .Contains (err .Error (), "ssh: host key mismatch" ) {
464+ t .Errorf ("expected host key validation error, got: %v" , err )
465+ }
466+ })
467+ t .Run ("IgnoreHostKey" , func (t * testing.T ) {
468+ var captured []string
469+ slog .SetDefault (slog .New (slog .NewTextHandler (io .Discard , & slog.HandlerOptions {
470+ Level : slog .LevelWarn ,
471+ ReplaceAttr : func (groups []string , a slog.Attr ) slog.Attr {
472+ if a .Key == slog .MessageKey {
473+ captured = append (captured , a .Value .String ())
474+ }
475+ return a
476+ },
477+ })))
478+
479+ addr := testingutil .MockSFTPServer (t , privateKey )
480+
481+ c := testingutil .NewSFTPReplicaClient (t )
482+ c .User = "foo"
483+ c .Host = addr
484+
485+ _ , err = c .Init (context .Background ())
486+ if err != nil {
487+ t .Fatalf ("SFTP connection failed: %v" , err )
488+ }
489+
490+ if ! slices .ContainsFunc (captured , func (msg string ) bool {
491+ return strings .Contains (msg , "sftp host key not verified" )
492+ }) {
493+ t .Errorf ("Expected warning not found" )
494+ }
495+
496+ })
497+ }
0 commit comments