plugins: work around x/crypto rejecting ssh-mldsa44-ed25519 known_hosts entries

fixes #4029
port 2.0
This commit is contained in:
bbedward committed 2026-10-30 10:35:55 -04:00
1 parent a0b74b2acf
commit 7ad1b7ef1d
4 files changed
+143 -10

No files matched your search

+2 -2
View File
@@ -15,6 +15,7 @@ require (
github.com/go-git/go-git/v6 v6.0.0-beta.1
github.com/godbus/dbus/v5 v5.2.4
github.com/holoplot/go-evdev v0.0.0-20260909133534-d873ed210bda
github.com/kevinburke/ssh_config v1.6.0
github.com/klauspost/compress v1.20.3
github.com/lucasb-eyer/go-colorful v1.4.1
github.com/mattn/go-isatty v0.0.24
@@ -31,6 +32,7 @@ require (
github.com/yuin/goldmark v1.8.6
go.etcd.io/bbolt v1.5.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
golang.org/x/crypto v0.58.0
golang.org/x/image v0.47.0
golang.org/x/sys v0.49.0
golang.org/x/text v0.43.0
@@ -71,7 +73,6 @@ require (
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.31 // indirect
@@ -97,7 +98,6 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb // indirect
golang.org/x/crypto v0.58.0 // indirect
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect
golang.org/x/net v0.61.0 // indirect
golang.org/x/sync v0.24.0 // indirect
+129
View File
@@ -0,0 +1,129 @@
package gitssh
import (
"bytes"
"context"
"errors"
"fmt"
"io/fs"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"github.com/go-git/go-git/v6/plumbing/client"
"github.com/go-git/go-git/v6/plumbing/transport"
xssh "github.com/go-git/go-git/v6/plumbing/transport/ssh"
"github.com/go-git/go-git/v6/plumbing/transport/ssh/knownhosts"
"github.com/kevinburke/ssh_config"
gossh "golang.org/x/crypto/ssh"
)
func ClientOptions() []client.Option {
return []client.Option{client.WithSSHAuth(agentAuth{})}
}
type agentAuth struct{}
func (agentAuth) ClientConfig(ctx context.Context, req *transport.Request) (*gossh.ClientConfig, error) {
auth, err := xssh.NewSSHAgentAuth(username(req.URL))
if err != nil {
return nil, err
}
db, err := knownHostsDB()
if err != nil {
return nil, err
}
auth.HostKeyCallback = db.HostKeyCallback()
cfg, err := auth.ClientConfig(ctx, req)
if err != nil {
return nil, err
}
cfg.HostKeyAlgorithms = db.HostKeyAlgorithms(hostWithPort(req.URL))
if len(cfg.HostKeyAlgorithms) == 0 {
cfg.HostKeyAlgorithms = gossh.SupportedAlgorithms().HostKeys
}
return cfg, nil
}
func username(u *url.URL) string {
if u.User == nil || u.User.Username() == "" {
return xssh.DefaultUsername
}
return u.User.Username()
}
func hostWithPort(u *url.URL) string {
hostname := u.Hostname()
port := u.Port()
if configHost := ssh_config.Get(hostname, "Hostname"); configHost != "" {
hostname = configHost
}
if port == "" {
if configPort := ssh_config.Get(u.Hostname(), "Port"); configPort != "" {
if _, err := strconv.Atoi(configPort); err == nil {
port = configPort
}
}
}
if port == "" {
port = strconv.Itoa(xssh.DefaultPort)
}
return net.JoinHostPort(hostname, port)
}
// x/crypto rejects a whole known_hosts file over one key type it cannot parse,
// so entries like ssh-mldsa44-ed25519 are blanked before the file is loaded.
func knownHostsDB() (*knownhosts.HostKeyDB, error) {
dir, err := os.MkdirTemp("", "dms-known-hosts-")
if err != nil {
return nil, err
}
defer os.RemoveAll(dir)
var filtered []string
for i, file := range knownHostsFiles() {
content, err := os.ReadFile(file)
if errors.Is(err, fs.ErrNotExist) {
continue
}
if err != nil {
return nil, err
}
path := filepath.Join(dir, fmt.Sprintf("%d-%s", i, filepath.Base(file)))
if err := os.WriteFile(path, blankUnparseableLines(content), 0o600); err != nil {
return nil, err
}
filtered = append(filtered, path)
}
if len(filtered) == 0 {
return nil, errors.New("unable to find any valid known_hosts file, set SSH_KNOWN_HOSTS env variable")
}
return knownhosts.NewDB(filtered...)
}
func knownHostsFiles() []string {
if files := filepath.SplitList(os.Getenv("SSH_KNOWN_HOSTS")); len(files) != 0 {
return files
}
home, err := os.UserHomeDir()
if err != nil {
return []string{"/etc/ssh/ssh_known_hosts"}
}
return []string{filepath.Join(home, ".ssh", "known_hosts"), "/etc/ssh/ssh_known_hosts"}
}
func blankUnparseableLines(content []byte) []byte {
lines := bytes.Split(content, []byte("\n"))
for i, line := range lines {
trimmed := bytes.TrimSpace(line)
if len(trimmed) == 0 || trimmed[0] == '#' {
continue
}
if _, _, _, _, _, err := gossh.ParseKnownHosts(trimmed); err != nil {
lines[i] = nil
}
}
return bytes.Join(lines, []byte("\n"))
}
+7 -5
View File
@@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/duxlabs-dev/dux-shell/core/internal/gitssh"
"github.com/duxlabs-dev/dux-shell/core/internal/registries"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
@@ -44,8 +45,9 @@ type realGitClient struct{}
func (g *realGitClient) PlainClone(path string, url string) error {
_, err := git.PlainClone(path, &git.CloneOptions{
URL: url,
Progress: os.Stdout,
URL: url,
Progress: os.Stdout,
ClientOptions: gitssh.ClientOptions(),
})
return err
}
@@ -61,7 +63,7 @@ func (g *realGitClient) Pull(path string) error {
return err
}
err = worktree.Pull(&git.PullOptions{})
err = worktree.Pull(&git.PullOptions{ClientOptions: gitssh.ClientOptions()})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
@@ -101,7 +103,7 @@ func (g *realGitClient) HasUpdates(path string) (bool, string, string, error) {
return false, "", "", err
}
refs, err := remote.List(&git.ListOptions{})
refs, err := remote.List(&git.ListOptions{ClientOptions: gitssh.ClientOptions()})
if err != nil {
return false, "", "", err
}
@@ -168,7 +170,7 @@ func (g *realGitClient) CheckoutRevision(path string, revision string) error {
if _, remoteErr := repo.Remote("origin"); remoteErr != nil {
return fmt.Errorf("revision %s is not available in repository: %w", revision, objectErr)
}
fetchErr := repo.Fetch(&git.FetchOptions{})
fetchErr := repo.Fetch(&git.FetchOptions{ClientOptions: gitssh.ClientOptions()})
if fetchErr != nil && fetchErr != git.NoErrAlreadyUpToDate {
return fmt.Errorf("failed to fetch locked revision: %w", fetchErr)
}
+5 -3
View File
@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"github.com/duxlabs-dev/dux-shell/core/internal/gitssh"
"github.com/duxlabs-dev/dux-shell/core/internal/registries"
"github.com/go-git/go-git/v6"
"github.com/spf13/afero"
@@ -157,8 +158,9 @@ type realGitClient struct{}
func (g *realGitClient) PlainClone(path string, url string) error {
_, err := git.PlainClone(path, &git.CloneOptions{
URL: url,
Progress: os.Stdout,
URL: url,
Progress: os.Stdout,
ClientOptions: gitssh.ClientOptions(),
})
return err
}
@@ -174,7 +176,7 @@ func (g *realGitClient) Pull(path string) error {
return err
}
err = worktree.Pull(&git.PullOptions{})
err = worktree.Pull(&git.PullOptions{ClientOptions: gitssh.ClientOptions()})
if err != nil && err.Error() != "already up-to-date" {
return err
}