fix: verify SSH host keys, support ssh-agent, fix remote dir walk bugs

- Replace ssh.InsecureIgnoreHostKey() with TOFU verification against
  ~/.ssh/known_hosts (accept-new for unknown hosts, hard reject on a
  changed key) to close a MITM hole on every SFTP connection.
- Actually connect to a running ssh-agent (SSH_AUTH_SOCK) for auth
  instead of only scanning default key files on disk; give a clear
  error when an explicit IdentityFile is passphrase-protected.
- Fix SSHClient.walk()/DirectorySize: the internal filepathSkipDir
  sentinel leaked out as a real error on stat/ReadDir failures instead
  of being swallowed, aborting size calculation on the first
  unreadable subdirectory instead of skipping it.
- Skip symlink-to-directory entries in remote directory copies instead
  of failing the whole transfer trying to Open() them as regular files.
- Consolidate 5 duplicated sudo-aware home-dir lookups into
  internal/homedir, adding a missing os.Geteuid()==0 check before
  trusting SUDO_USER.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
vrubelroman 2026-07-03 15:31:32 +00:00
parent 3cf9582147
commit c15ade247a
16 changed files with 2580 additions and 65 deletions

8
go.mod
View file

@ -4,18 +4,20 @@ go 1.26.0
require ( require (
github.com/alecthomas/chroma/v2 v2.23.1 github.com/alecthomas/chroma/v2 v2.23.1
github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/x/ansi v0.10.1
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/pkg/sftp v1.13.10
golang.org/x/crypto v0.50.0
golang.org/x/sys v0.43.0 golang.org/x/sys v0.43.0
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
@ -28,9 +30,7 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect github.com/muesli/termenv v0.16.0 // indirect
github.com/pkg/sftp v1.13.10 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/text v0.36.0 // indirect golang.org/x/text v0.36.0 // indirect
) )

20
go.sum
View file

@ -1,5 +1,9 @@
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@ -18,10 +22,14 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@ -42,9 +50,13 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
@ -53,11 +65,11 @@ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZ
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -9,6 +9,8 @@ import (
"syscall" "syscall"
toml "github.com/pelletier/go-toml/v2" toml "github.com/pelletier/go-toml/v2"
"vcom/internal/homedir"
) )
type Config struct { type Config struct {
@ -221,9 +223,9 @@ func resolvePath(explicitPath string) (string, bool, error) {
if explicitPath != "" { if explicitPath != "" {
candidates = append(candidates, explicitPath) candidates = append(candidates, explicitPath)
} else { } else {
homeDir, err := os.UserHomeDir() homeDir := homedir.Dir()
if err != nil { if homeDir == "" {
return "", false, fmt.Errorf("resolve home dir: %w", err) return "", false, fmt.Errorf("cannot determine home directory")
} }
xdgDir := os.Getenv("XDG_CONFIG_HOME") xdgDir := os.Getenv("XDG_CONFIG_HOME")
if xdgDir == "" { if xdgDir == "" {
@ -261,9 +263,9 @@ func isMissingPathError(err error) bool {
} }
func DefaultUserPath() (string, error) { func DefaultUserPath() (string, error) {
homeDir, err := os.UserHomeDir() homeDir := homedir.Dir()
if err != nil { if homeDir == "" {
return "", fmt.Errorf("resolve home dir: %w", err) return "", fmt.Errorf("cannot determine home directory")
} }
xdgDir := os.Getenv("XDG_CONFIG_HOME") xdgDir := os.Getenv("XDG_CONFIG_HOME")
if xdgDir == "" { if xdgDir == "" {

View file

@ -6,6 +6,8 @@ import (
"path/filepath" "path/filepath"
toml "github.com/pelletier/go-toml/v2" toml "github.com/pelletier/go-toml/v2"
"vcom/internal/homedir"
) )
// SessionState stores the UI state that should be restored on next launch. // SessionState stores the UI state that should be restored on next launch.
@ -24,9 +26,9 @@ type PaneSession struct {
// DefaultSessionPath returns the path to the session file. // DefaultSessionPath returns the path to the session file.
func DefaultSessionPath() (string, error) { func DefaultSessionPath() (string, error) {
homeDir, err := os.UserHomeDir() homeDir := homedir.Dir()
if err != nil { if homeDir == "" {
return "", fmt.Errorf("resolve home dir: %w", err) return "", fmt.Errorf("cannot determine home directory")
} }
xdgDir := os.Getenv("XDG_CONFIG_HOME") xdgDir := os.Getenv("XDG_CONFIG_HOME")
if xdgDir == "" { if xdgDir == "" {

View file

@ -10,6 +10,8 @@ import (
"path/filepath" "path/filepath"
"syscall" "syscall"
"time" "time"
"vcom/internal/homedir"
) )
type TransferStats struct { type TransferStats struct {
@ -261,9 +263,9 @@ func DeletePath(path string) error {
// - A .trashinfo file is written to Trash/info/<basename>.trashinfo // - A .trashinfo file is written to Trash/info/<basename>.trashinfo
// - If <basename> already exists in Trash/files, a numeric suffix is appended. // - If <basename> already exists in Trash/files, a numeric suffix is appended.
func MoveToTrash(path string) error { func MoveToTrash(path string) error {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return fmt.Errorf("cannot determine home directory: %w", err) return fmt.Errorf("cannot determine home directory")
} }
trashDir := filepath.Join(home, ".local", "share", "Trash") trashDir := filepath.Join(home, ".local", "share", "Trash")

View file

@ -3,6 +3,7 @@ package remote
import ( import (
"bufio" "bufio"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -15,6 +16,10 @@ import (
"github.com/pkg/sftp" "github.com/pkg/sftp"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"golang.org/x/crypto/ssh/knownhosts"
"vcom/internal/homedir"
) )
// SSHClient wraps an SSH connection and SFTP client for remote filesystem access. // SSHClient wraps an SSH connection and SFTP client for remote filesystem access.
@ -37,6 +42,11 @@ func Connect(host SSHHost) (*SSHClient, error) {
return nil, fmt.Errorf("ssh auth: %w", err) return nil, fmt.Errorf("ssh auth: %w", err)
} }
hostKeyCB, err := tofuHostKeyCallback()
if err != nil {
return nil, fmt.Errorf("host key verification: %w", err)
}
user := host.User user := host.User
if user == "" { if user == "" {
user = os.Getenv("USER") user = os.Getenv("USER")
@ -45,7 +55,7 @@ func Connect(host SSHHost) (*SSHClient, error) {
config := &ssh.ClientConfig{ config := &ssh.ClientConfig{
User: user, User: user,
Auth: authMethods, Auth: authMethods,
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: support known_hosts verification HostKeyCallback: hostKeyCB,
Timeout: 15 * time.Second, Timeout: 15 * time.Second,
} }
@ -91,23 +101,101 @@ func Connect(host SSHHost) (*SSHClient, error) {
return client, nil return client, nil
} }
// tofuHostKeyCallback returns a HostKeyCallback that verifies server host keys
// against ~/.ssh/known_hosts. Unknown hosts are trusted on first use (TOFU,
// matching `ssh -o StrictHostKeyChecking=accept-new`) and their key is
// recorded for future connections. A host whose key no longer matches a
// previously recorded entry is rejected outright, since that's the signature
// of a man-in-the-middle attack or a legitimately reissued key that the user
// must verify and update manually (e.g. via `ssh-keygen -R <host>`).
func tofuHostKeyCallback() (ssh.HostKeyCallback, error) {
khPath := KnownHostsPath()
if khPath == "" {
return nil, fmt.Errorf("cannot determine known_hosts path")
}
if err := os.MkdirAll(filepath.Dir(khPath), 0o700); err != nil {
return nil, fmt.Errorf("create ssh dir: %w", err)
}
if _, err := os.Stat(khPath); os.IsNotExist(err) {
f, err := os.OpenFile(khPath, os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return nil, fmt.Errorf("create known_hosts: %w", err)
}
f.Close()
}
verify, err := knownhosts.New(khPath)
if err != nil {
return nil, fmt.Errorf("parse known_hosts: %w", err)
}
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
err := verify(hostname, remote, key)
if err == nil {
return nil
}
var keyErr *knownhosts.KeyError
if errors.As(err, &keyErr) && len(keyErr.Want) > 0 {
return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for %s! This may indicate a man-in-the-middle attack, or the server's key was legitimately reissued. Refusing to connect; run `ssh-keygen -R %s` to remove the stale entry once you've verified the new key: %w", hostname, hostname, err)
}
// Unknown host: trust on first use and remember the key.
line := knownhosts.Line([]string{knownhosts.Normalize(hostname)}, key)
f, ferr := os.OpenFile(khPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if ferr != nil {
return fmt.Errorf("cannot save host key for %s: %w", hostname, ferr)
}
defer f.Close()
if _, werr := f.WriteString(line + "\n"); werr != nil {
return fmt.Errorf("cannot save host key for %s: %w", hostname, werr)
}
return nil
}, nil
}
// sshAgentAuth connects to the running ssh-agent (via SSH_AUTH_SOCK) and
// returns an auth method backed by its loaded keys, or false if no agent is
// reachable. The agent connection is intentionally kept open for the life of
// the process rather than closed here: ssh.PublicKeysCallback invokes the
// callback lazily during the handshake, well after this function returns.
func sshAgentAuth() (ssh.AuthMethod, bool) {
sock := os.Getenv("SSH_AUTH_SOCK")
if sock == "" {
return nil, false
}
conn, err := net.Dial("unix", sock)
if err != nil {
return nil, false
}
agentClient := agent.NewClient(conn)
return ssh.PublicKeysCallback(agentClient.Signers), true
}
// authMethodsForHost returns the appropriate SSH auth methods for the given host. // authMethodsForHost returns the appropriate SSH auth methods for the given host.
// For SSH config hosts with IdentityFile, it uses public key authentication. // For SSH config hosts with IdentityFile, it uses public key authentication.
// For custom hosts with a password, it uses password authentication. // For custom hosts with a password, it uses password authentication.
func authMethodsForHost(host SSHHost) ([]ssh.AuthMethod, error) { func authMethodsForHost(host SSHHost) ([]ssh.AuthMethod, error) {
var methods []ssh.AuthMethod var methods []ssh.AuthMethod
var passphraseProtectedKey string
// Try key-based auth if identity file is specified // Try key-based auth if identity file is specified.
if host.IdentityFile != "" { if host.IdentityFile != "" {
key, err := os.ReadFile(host.IdentityFile) key, err := os.ReadFile(host.IdentityFile)
if err == nil { if err == nil {
signer, err := ssh.ParsePrivateKey(key) signer, perr := ssh.ParsePrivateKey(key)
if err == nil { var passphraseErr *ssh.PassphraseMissingError
if perr == nil {
methods = append(methods, ssh.PublicKeys(signer)) methods = append(methods, ssh.PublicKeys(signer))
} else { } else if errors.As(perr, &passphraseErr) {
// If the key is encrypted, try with empty passphrase or common ones // The key is encrypted. We have no interactive prompt for a
// For simplicity, we try without passphrase first // passphrase yet, but the same key may already be unlocked
// In a real implementation, we might prompt for a passphrase // in a running ssh-agent, so fall through and try that too.
passphraseProtectedKey = host.IdentityFile
if authMethod, ok := sshAgentAuth(); ok {
methods = append(methods, authMethod)
}
} }
} }
} }
@ -130,13 +218,16 @@ func authMethodsForHost(host SSHHost) ([]ssh.AuthMethod, error) {
)) ))
} }
// Always try default SSH agent and default keys as a last resort // When no explicit identity file or password is configured, try the
// This covers the case where the user has an SSH agent running with loaded keys // running ssh-agent first (it can supply already-decrypted keys that a
// but no IdentityFile is specified in the config. // bare file scan cannot), then fall back to scanning default key paths.
if host.IdentityFile == "" && host.Password == "" { if host.IdentityFile == "" && host.Password == "" {
// Add default key paths if authMethod, ok := sshAgentAuth(); ok {
home, err := os.UserHomeDir() methods = append(methods, authMethod)
if err == nil { }
home := homedir.Dir()
if home != "" {
defaultKeys := []string{ defaultKeys := []string{
home + "/.ssh/id_rsa", home + "/.ssh/id_rsa",
home + "/.ssh/id_ed25519", home + "/.ssh/id_ed25519",
@ -156,6 +247,9 @@ func authMethodsForHost(host SSHHost) ([]ssh.AuthMethod, error) {
} }
if len(methods) == 0 { if len(methods) == 0 {
if passphraseProtectedKey != "" {
return nil, fmt.Errorf("identity file %q is passphrase-protected; load it into ssh-agent first (ssh-add %s)", passphraseProtectedKey, passphraseProtectedKey)
}
return nil, fmt.Errorf("no authentication methods available for host %q", host.Name) return nil, fmt.Errorf("no authentication methods available for host %q", host.Name)
} }
@ -557,6 +651,12 @@ func (c *SSHClient) copyDirFromRemote(remoteDir, localDir string, onFile func(pa
if info.IsDir() { if info.IsDir() {
return os.MkdirAll(localPath, 0o755) return os.MkdirAll(localPath, 0o755)
} }
if isSymlinkToDir(c, remotePath, info) {
// Copying/dereferencing a symlinked directory isn't supported;
// skip it instead of failing the whole transfer trying to Open()
// what the SFTP server sees as a directory.
return nil
}
if err := c.CopyFileFromRemote(remotePath, localPath); err != nil { if err := c.CopyFileFromRemote(remotePath, localPath); err != nil {
return err return err
} }
@ -628,6 +728,11 @@ func copyDirBetweenRemotes(srcClient, dstClient *SSHClient, srcDir, dstDir strin
if info.IsDir() { if info.IsDir() {
return dstClient.MkdirAll(dstPath) return dstClient.MkdirAll(dstPath)
} }
if isSymlinkToDir(srcClient, remotePath, info) {
// See copyDirFromRemote: a symlink-to-directory can't be opened
// as a regular file, so skip it instead of aborting the transfer.
return nil
}
if err := CopyFileBetweenRemotes(srcClient, dstClient, remotePath, dstPath); err != nil { if err := CopyFileBetweenRemotes(srcClient, dstClient, remotePath, dstPath); err != nil {
return err return err
} }
@ -654,10 +759,13 @@ func (c *SSHClient) Walk(root string, walkFn walkFunc) error {
func (c *SSHClient) walk(dirPath string, walkFn walkFunc, info os.FileInfo) error { func (c *SSHClient) walk(dirPath string, walkFn walkFunc, info os.FileInfo) error {
if info == nil { if info == nil {
var err error var statErr error
info, err = c.sftpCli.Stat(dirPath) info, statErr = c.sftpCli.Stat(dirPath)
if err != nil { if statErr != nil {
return walkFn(dirPath, nil, err) if fnErr := walkFn(dirPath, nil, statErr); fnErr != nil && fnErr != filepathSkipDir {
return fnErr
}
return nil
} }
} }
@ -673,11 +781,22 @@ func (c *SSHClient) walk(dirPath string, walkFn walkFunc, info os.FileInfo) erro
return nil return nil
} }
entries, err := c.sftpCli.ReadDir(dirPath) entries, readErr := c.sftpCli.ReadDir(dirPath)
if err != nil { if readErr != nil {
return walkFn(dirPath, info, err) // Report the error but allow walkFn to skip this directory
// (returns nil, or filepathSkipDir → continue) or abort (returns
// another error → stop). Either way this directory's subtree is done.
if fnErr := walkFn(dirPath, info, readErr); fnErr != nil && fnErr != filepathSkipDir {
return fnErr
}
return nil
} }
// Note: entries for symlinks (including symlinks to directories) come
// back from ReadDir with IsDir()==false, since SFTP reports directory
// entries the way lstat would, without following links. They're handled
// as leaves via walkFn below rather than recursed into, which also means
// cyclic symlinks can't cause unbounded recursion here.
for _, entry := range entries { for _, entry := range entries {
childPath := path.Join(dirPath, entry.Name()) childPath := path.Join(dirPath, entry.Name())
if entry.IsDir() { if entry.IsDir() {
@ -696,12 +815,27 @@ func (c *SSHClient) walk(dirPath string, walkFn walkFunc, info os.FileInfo) erro
// filepathSkipDir is used as a return value from Walk to skip a directory. // filepathSkipDir is used as a return value from Walk to skip a directory.
var filepathSkipDir = fmt.Errorf("skip this directory") var filepathSkipDir = fmt.Errorf("skip this directory")
// isSymlinkToDir reports whether the walk entry at remotePath is a symlink
// whose target is a directory. Such entries can't be copied with a plain
// Open()/Create() file copy (the server rejects opening a directory for
// reading), so callers use this to skip them instead of failing outright.
func isSymlinkToDir(c *SSHClient, remotePath string, info os.FileInfo) bool {
if info.Mode()&os.ModeSymlink == 0 {
return false
}
target, err := c.Stat(remotePath)
return err == nil && target.IsDir()
}
// DirectorySize recursively walks a remote directory and sums up file sizes. // DirectorySize recursively walks a remote directory and sums up file sizes.
// Unreadable subdirectories are skipped gracefully instead of aborting the entire walk.
func (c *SSHClient) DirectorySize(dirPath string) (int64, error) { func (c *SSHClient) DirectorySize(dirPath string) (int64, error) {
var total int64 var total int64
err := c.Walk(dirPath, func(_ string, info os.FileInfo, err error) error { err := c.Walk(dirPath, func(_ string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err // Skip unreadable directories instead of aborting the walk.
// This handles permission-denied and other transient SFTP errors.
return filepathSkipDir
} }
if !info.IsDir() { if !info.IsDir() {
total += info.Size() total += info.Size()

View file

@ -6,13 +6,15 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"vcom/internal/homedir"
) )
// ParseSSHConfig parses ~/.ssh/config and returns a list of SSH hosts. // ParseSSHConfig parses ~/.ssh/config and returns a list of SSH hosts.
// It handles the most common SSH config directives: Host, HostName, Port, User, IdentityFile. // It handles the most common SSH config directives: Host, HostName, Port, User, IdentityFile.
func ParseSSHConfig() []SSHHost { func ParseSSHConfig() []SSHHost {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return nil return nil
} }
@ -129,8 +131,8 @@ func resolveIdentityPath(path string) string {
// Handle ~/ or $HOME/ // Handle ~/ or $HOME/
if strings.HasPrefix(path, "~/") { if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return path return path
} }
path = filepath.Join(home, path[2:]) path = filepath.Join(home, path[2:])
@ -138,8 +140,8 @@ func resolveIdentityPath(path string) string {
// Handle relative paths (relative to ~/.ssh/) // Handle relative paths (relative to ~/.ssh/)
if !filepath.IsAbs(path) { if !filepath.IsAbs(path) {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return path return path
} }
path = filepath.Join(home, ".ssh", path) path = filepath.Join(home, ".ssh", path)
@ -150,8 +152,8 @@ func resolveIdentityPath(path string) string {
// SSHConfigPath returns the path to the user's SSH config file. // SSHConfigPath returns the path to the user's SSH config file.
func SSHConfigPath() string { func SSHConfigPath() string {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return "" return ""
} }
return filepath.Join(home, ".ssh", "config") return filepath.Join(home, ".ssh", "config")
@ -159,8 +161,8 @@ func SSHConfigPath() string {
// HostsFilePath returns the path to the custom hosts data file. // HostsFilePath returns the path to the custom hosts data file.
func HostsFilePath() string { func HostsFilePath() string {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return "" return ""
} }
return filepath.Join(home, ".config", "vcom", "hosts.dat") return filepath.Join(home, ".config", "vcom", "hosts.dat")
@ -168,8 +170,8 @@ func HostsFilePath() string {
// GetSSHDir returns the path to the .ssh directory. // GetSSHDir returns the path to the .ssh directory.
func GetSSHDir() string { func GetSSHDir() string {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return "" return ""
} }
return filepath.Join(home, ".ssh") return filepath.Join(home, ".ssh")
@ -177,8 +179,8 @@ func GetSSHDir() string {
// KnownHostsPath returns the path to known_hosts. // KnownHostsPath returns the path to known_hosts.
func KnownHostsPath() string { func KnownHostsPath() string {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return "" return ""
} }
return filepath.Join(home, ".ssh", "known_hosts") return filepath.Join(home, ".ssh", "known_hosts")

View file

@ -11,6 +11,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"vcom/internal/homedir"
) )
// SSHHost represents a single SSH host configuration. // SSHHost represents a single SSH host configuration.
@ -66,9 +68,9 @@ type HostStore struct {
// NewHostStore creates a new HostStore. // NewHostStore creates a new HostStore.
func NewHostStore() (*HostStore, error) { func NewHostStore() (*HostStore, error) {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return nil, fmt.Errorf("home dir: %w", err) return nil, fmt.Errorf("cannot determine home directory")
} }
store := &HostStore{ store := &HostStore{
@ -77,6 +79,7 @@ func NewHostStore() (*HostStore, error) {
// Load or create encryption key // Load or create encryption key
keyPath := filepath.Join(home, ".config", "vcom", ".hosts-key") keyPath := filepath.Join(home, ".config", "vcom", ".hosts-key")
var err error
store.cipherKey, err = loadOrCreateKey(keyPath) store.cipherKey, err = loadOrCreateKey(keyPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("encryption key: %w", err) return nil, fmt.Errorf("encryption key: %w", err)

View file

@ -0,0 +1,26 @@
// Package homedir resolves the real user's home directory, including when
// the process is running under sudo.
package homedir
import (
"os"
"os/user"
)
// Dir returns the real user's home directory. os.UserHomeDir() resolves to
// /root when the process is running as root via sudo, which points config,
// SSH, and trash paths at the wrong place. When the effective UID is 0 and
// SUDO_USER is set, it resolves the invoking user's home instead.
func Dir() string {
if os.Geteuid() == 0 {
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
if u, err := user.Lookup(sudoUser); err == nil {
return u.HomeDir
}
}
}
if h, err := os.UserHomeDir(); err == nil {
return h
}
return ""
}

View file

@ -24,6 +24,7 @@ import (
"vcom/internal/config" "vcom/internal/config"
vfs "vcom/internal/fs" vfs "vcom/internal/fs"
"vcom/internal/fs/remote" "vcom/internal/fs/remote"
"vcom/internal/homedir"
"vcom/internal/theme" "vcom/internal/theme"
) )
@ -272,9 +273,9 @@ func NewModel(cfg config.Config, configPath string) (Model, error) {
return Model{}, err return Model{}, err
} }
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return Model{}, fmt.Errorf("resolve home dir: %w", err) return Model{}, fmt.Errorf("cannot determine home directory")
} }
leftPath, err := resolveStartPath(cfg.Startup.LeftPath, home) leftPath, err := resolveStartPath(cfg.Startup.LeftPath, home)
@ -5671,9 +5672,9 @@ func resolveStartPath(raw string, fallback string) (string, error) {
return fallback, nil return fallback, nil
} }
if strings.HasPrefix(value, "~/") { if strings.HasPrefix(value, "~/") {
home, err := os.UserHomeDir() home := homedir.Dir()
if err != nil { if home == "" {
return "", err return "", fmt.Errorf("cannot determine home directory")
} }
value = filepath.Join(home, strings.TrimPrefix(value, "~/")) value = filepath.Join(home, strings.TrimPrefix(value, "~/"))
} }
@ -6466,6 +6467,11 @@ func (m *Model) startRemoteCopyJob(kind fileOpKind, sources []string, targetDir
}, ctx) }, ctx)
} else { } else {
log.Printf("[JOB] remoteCopy — local→remote file: job=%d source=%s target=%s size=%d", jobID, sourcePath, targetPath, info.Size()) log.Printf("[JOB] remoteCopy — local→remote file: job=%d source=%s target=%s size=%d", jobID, sourcePath, targetPath, info.Size())
// Check context before single-file transfer
if ctx.Err() != nil {
m.copyProgress <- copyDoneMsg{jobID: jobID, kind: kind, sourcePaths: sources, targetDir: targetDir, err: ctx.Err()}
return
}
err = dstClient.CopyFileToRemote(sourcePath, targetPath) err = dstClient.CopyFileToRemote(sourcePath, targetPath)
} }
} }
@ -6491,6 +6497,11 @@ func (m *Model) startRemoteCopyJob(kind fileOpKind, sources []string, targetDir
}, ctx) }, ctx)
} else { } else {
log.Printf("[JOB] remoteCopy — remote→local file: job=%d source=%s target=%s size=%d", jobID, sourcePath, targetPath, info.Size()) log.Printf("[JOB] remoteCopy — remote→local file: job=%d source=%s target=%s size=%d", jobID, sourcePath, targetPath, info.Size())
// Check context before single-file transfer so cancel works between files
if ctx.Err() != nil {
m.copyProgress <- copyDoneMsg{jobID: jobID, kind: kind, sourcePaths: sources, targetDir: targetDir, err: ctx.Err()}
return
}
err = srcClient.CopyFileFromRemote(sourcePath, targetPath) err = srcClient.CopyFileFromRemote(sourcePath, targetPath)
} }
} }
@ -6570,6 +6581,11 @@ func (m *Model) startRemoteCopyJob(kind fileOpKind, sources []string, targetDir
} }
}, ctx) }, ctx)
} else { } else {
// Check context before remote→remote single-file streaming
if ctx.Err() != nil {
m.copyProgress <- copyDoneMsg{jobID: jobID, kind: kind, sourcePaths: sources, targetDir: targetDir, err: ctx.Err()}
return
}
err = remote.CopyFileBetweenRemotes(srcClient, dstClient, sourcePath, targetPath) err = remote.CopyFileBetweenRemotes(srcClient, dstClient, sourcePath, targetPath)
} }
} }

856
vendor/golang.org/x/crypto/ssh/agent/client.go generated vendored Normal file
View file

@ -0,0 +1,856 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package agent implements the ssh-agent protocol, and provides both
// a client and a server. The client can talk to a standard ssh-agent
// that uses UNIX sockets, and one could implement an alternative
// ssh-agent process using the sample server.
//
// References:
//
// [PROTOCOL.agent]: https://tools.ietf.org/html/draft-miller-ssh-agent-00
package agent
import (
"bytes"
"crypto/dsa"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"sync"
"golang.org/x/crypto/ssh"
)
// SignatureFlags represent additional flags that can be passed to the signature
// requests an defined in [PROTOCOL.agent] section 4.5.1.
type SignatureFlags uint32
// SignatureFlag values as defined in [PROTOCOL.agent] section 5.3.
const (
SignatureFlagReserved SignatureFlags = 1 << iota
SignatureFlagRsaSha256
SignatureFlagRsaSha512
)
// Agent represents the capabilities of an ssh-agent.
type Agent interface {
// List returns the identities known to the agent.
List() ([]*Key, error)
// Sign has the agent sign the data using a protocol 2 key as defined
// in [PROTOCOL.agent] section 2.6.2.
Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error)
// Add adds a private key to the agent.
Add(key AddedKey) error
// Remove removes all identities with the given public key.
Remove(key ssh.PublicKey) error
// RemoveAll removes all identities.
RemoveAll() error
// Lock locks the agent. Sign and Remove will fail, and List will empty an empty list.
Lock(passphrase []byte) error
// Unlock undoes the effect of Lock
Unlock(passphrase []byte) error
// Signers returns signers for all the known keys.
Signers() ([]ssh.Signer, error)
}
type ExtendedAgent interface {
Agent
// SignWithFlags signs like Sign, but allows for additional flags to be sent/received
SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFlags) (*ssh.Signature, error)
// Extension processes a custom extension request. Standard-compliant agents are not
// required to support any extensions, but this method allows agents to implement
// vendor-specific methods or add experimental features. See [PROTOCOL.agent] section 4.7.
// If agent extensions are unsupported entirely this method MUST return an
// ErrExtensionUnsupported error. Similarly, if just the specific extensionType in
// the request is unsupported by the agent then ErrExtensionUnsupported MUST be
// returned.
//
// In the case of success, since [PROTOCOL.agent] section 4.7 specifies that the contents
// of the response are unspecified (including the type of the message), the complete
// response will be returned as a []byte slice, including the "type" byte of the message.
Extension(extensionType string, contents []byte) ([]byte, error)
}
// ConstraintExtension describes an optional constraint defined by users.
type ConstraintExtension struct {
// ExtensionName consist of a UTF-8 string suffixed by the
// implementation domain following the naming scheme defined
// in Section 4.2 of RFC 4251, e.g. "foo@example.com".
ExtensionName string
// ExtensionDetails contains the actual content of the extended
// constraint.
ExtensionDetails []byte
}
// AddedKey describes an SSH key to be added to an Agent.
type AddedKey struct {
// PrivateKey must be a *rsa.PrivateKey, *dsa.PrivateKey,
// ed25519.PrivateKey or *ecdsa.PrivateKey, which will be inserted into the
// agent.
PrivateKey interface{}
// Certificate, if not nil, is communicated to the agent and will be
// stored with the key.
Certificate *ssh.Certificate
// Comment is an optional, free-form string.
Comment string
// LifetimeSecs, if not zero, is the number of seconds that the
// agent will store the key for.
LifetimeSecs uint32
// ConfirmBeforeUse, if true, requests that the agent confirm with the
// user before each use of this key.
ConfirmBeforeUse bool
// ConstraintExtensions are the experimental or private-use constraints
// defined by users.
ConstraintExtensions []ConstraintExtension
}
// See [PROTOCOL.agent], section 3.
const (
agentRequestV1Identities = 1
agentRemoveAllV1Identities = 9
// 3.2 Requests from client to agent for protocol 2 key operations
agentAddIdentity = 17
agentRemoveIdentity = 18
agentRemoveAllIdentities = 19
agentAddIDConstrained = 25
// 3.3 Key-type independent requests from client to agent
agentAddSmartcardKey = 20
agentRemoveSmartcardKey = 21
agentLock = 22
agentUnlock = 23
agentAddSmartcardKeyConstrained = 26
// 3.7 Key constraint identifiers
agentConstrainLifetime = 1
agentConstrainConfirm = 2
// Constraint extension identifier up to version 2 of the protocol. A
// backward incompatible change will be required if we want to add support
// for SSH_AGENT_CONSTRAIN_MAXSIGN which uses the same ID.
agentConstrainExtensionV00 = 3
// Constraint extension identifier in version 3 and later of the protocol.
agentConstrainExtension = 255
)
// maxAgentResponseBytes is the maximum agent reply size that is accepted. This
// is a sanity check, not a limit in the spec.
const maxAgentResponseBytes = 16 << 20
// Agent messages:
// These structures mirror the wire format of the corresponding ssh agent
// messages found in [PROTOCOL.agent].
// 3.4 Generic replies from agent to client
const agentFailure = 5
type failureAgentMsg struct{}
const agentSuccess = 6
type successAgentMsg struct{}
// See [PROTOCOL.agent], section 2.5.2.
const agentRequestIdentities = 11
type requestIdentitiesAgentMsg struct{}
// See [PROTOCOL.agent], section 2.5.2.
const agentIdentitiesAnswer = 12
type identitiesAnswerAgentMsg struct {
NumKeys uint32 `sshtype:"12"`
Keys []byte `ssh:"rest"`
}
// See [PROTOCOL.agent], section 2.6.2.
const agentSignRequest = 13
type signRequestAgentMsg struct {
KeyBlob []byte `sshtype:"13"`
Data []byte
Flags uint32
}
// See [PROTOCOL.agent], section 2.6.2.
// 3.6 Replies from agent to client for protocol 2 key operations
const agentSignResponse = 14
type signResponseAgentMsg struct {
SigBlob []byte `sshtype:"14"`
}
type publicKey struct {
Format string
Rest []byte `ssh:"rest"`
}
// 3.7 Key constraint identifiers
type constrainLifetimeAgentMsg struct {
LifetimeSecs uint32 `sshtype:"1"`
}
type constrainExtensionAgentMsg struct {
ExtensionName string `sshtype:"255|3"`
ExtensionDetails []byte
// Rest is a field used for parsing, not part of message
Rest []byte `ssh:"rest"`
}
// See [PROTOCOL.agent], section 4.7
const agentExtension = 27
const agentExtensionFailure = 28
// ErrExtensionUnsupported indicates that an extension defined in
// [PROTOCOL.agent] section 4.7 is unsupported by the agent. Specifically this
// error indicates that the agent returned a standard SSH_AGENT_FAILURE message
// as the result of a SSH_AGENTC_EXTENSION request. Note that the protocol
// specification (and therefore this error) does not distinguish between a
// specific extension being unsupported and extensions being unsupported entirely.
var ErrExtensionUnsupported = errors.New("agent: extension unsupported")
type extensionAgentMsg struct {
ExtensionType string `sshtype:"27"`
// NOTE: this matches OpenSSH's PROTOCOL.agent, not the IETF draft [PROTOCOL.agent],
// so that it matches what OpenSSH actually implements in the wild.
Contents []byte `ssh:"rest"`
}
// Key represents a protocol 2 public key as defined in
// [PROTOCOL.agent], section 2.5.2.
type Key struct {
Format string
Blob []byte
Comment string
}
func clientErr(err error) error {
return fmt.Errorf("agent: client error: %v", err)
}
// String returns the storage form of an agent key with the format, base64
// encoded serialized key, and the comment if it is not empty.
func (k *Key) String() string {
s := string(k.Format) + " " + base64.StdEncoding.EncodeToString(k.Blob)
if k.Comment != "" {
s += " " + k.Comment
}
return s
}
// Type returns the public key type.
func (k *Key) Type() string {
return k.Format
}
// Marshal returns key blob to satisfy the ssh.PublicKey interface.
func (k *Key) Marshal() []byte {
return k.Blob
}
// Verify satisfies the ssh.PublicKey interface.
func (k *Key) Verify(data []byte, sig *ssh.Signature) error {
pubKey, err := ssh.ParsePublicKey(k.Blob)
if err != nil {
return fmt.Errorf("agent: bad public key: %v", err)
}
return pubKey.Verify(data, sig)
}
type wireKey struct {
Format string
Rest []byte `ssh:"rest"`
}
func parseKey(in []byte) (out *Key, rest []byte, err error) {
var record struct {
Blob []byte
Comment string
Rest []byte `ssh:"rest"`
}
if err := ssh.Unmarshal(in, &record); err != nil {
return nil, nil, err
}
var wk wireKey
if err := ssh.Unmarshal(record.Blob, &wk); err != nil {
return nil, nil, err
}
return &Key{
Format: wk.Format,
Blob: record.Blob,
Comment: record.Comment,
}, record.Rest, nil
}
// client is a client for an ssh-agent process.
type client struct {
// conn is typically a *net.UnixConn
conn io.ReadWriter
// mu is used to prevent concurrent access to the agent
mu sync.Mutex
}
// NewClient returns an Agent that talks to an ssh-agent process over
// the given connection.
func NewClient(rw io.ReadWriter) ExtendedAgent {
return &client{conn: rw}
}
// call sends an RPC to the agent. On success, the reply is
// unmarshaled into reply and replyType is set to the first byte of
// the reply, which contains the type of the message.
func (c *client) call(req []byte) (reply interface{}, err error) {
buf, err := c.callRaw(req)
if err != nil {
return nil, err
}
reply, err = unmarshal(buf)
if err != nil {
return nil, clientErr(err)
}
return reply, nil
}
// callRaw sends an RPC to the agent. On success, the raw
// bytes of the response are returned; no unmarshalling is
// performed on the response.
func (c *client) callRaw(req []byte) (reply []byte, err error) {
c.mu.Lock()
defer c.mu.Unlock()
msg := make([]byte, 4+len(req))
binary.BigEndian.PutUint32(msg, uint32(len(req)))
copy(msg[4:], req)
if _, err = c.conn.Write(msg); err != nil {
return nil, clientErr(err)
}
var respSizeBuf [4]byte
if _, err = io.ReadFull(c.conn, respSizeBuf[:]); err != nil {
return nil, clientErr(err)
}
respSize := binary.BigEndian.Uint32(respSizeBuf[:])
if respSize > maxAgentResponseBytes {
return nil, clientErr(errors.New("response too large"))
}
buf := make([]byte, respSize)
if _, err = io.ReadFull(c.conn, buf); err != nil {
return nil, clientErr(err)
}
return buf, nil
}
func (c *client) simpleCall(req []byte) error {
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
func (c *client) RemoveAll() error {
return c.simpleCall([]byte{agentRemoveAllIdentities})
}
func (c *client) Remove(key ssh.PublicKey) error {
req := ssh.Marshal(&agentRemoveIdentityMsg{
KeyBlob: key.Marshal(),
})
return c.simpleCall(req)
}
func (c *client) Lock(passphrase []byte) error {
req := ssh.Marshal(&agentLockMsg{
Passphrase: passphrase,
})
return c.simpleCall(req)
}
func (c *client) Unlock(passphrase []byte) error {
req := ssh.Marshal(&agentUnlockMsg{
Passphrase: passphrase,
})
return c.simpleCall(req)
}
// List returns the identities known to the agent.
func (c *client) List() ([]*Key, error) {
// see [PROTOCOL.agent] section 2.5.2.
req := []byte{agentRequestIdentities}
msg, err := c.call(req)
if err != nil {
return nil, err
}
switch msg := msg.(type) {
case *identitiesAnswerAgentMsg:
if msg.NumKeys > maxAgentResponseBytes/8 {
return nil, errors.New("agent: too many keys in agent reply")
}
keys := make([]*Key, msg.NumKeys)
data := msg.Keys
for i := uint32(0); i < msg.NumKeys; i++ {
var key *Key
var err error
if key, data, err = parseKey(data); err != nil {
return nil, err
}
keys[i] = key
}
return keys, nil
case *failureAgentMsg:
return nil, errors.New("agent: failed to list keys")
default:
return nil, fmt.Errorf("agent: failed to list keys, unexpected message type %T", msg)
}
}
// Sign has the agent sign the data using a protocol 2 key as defined
// in [PROTOCOL.agent] section 2.6.2.
func (c *client) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
return c.SignWithFlags(key, data, 0)
}
func (c *client) SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFlags) (*ssh.Signature, error) {
req := ssh.Marshal(signRequestAgentMsg{
KeyBlob: key.Marshal(),
Data: data,
Flags: uint32(flags),
})
msg, err := c.call(req)
if err != nil {
return nil, err
}
switch msg := msg.(type) {
case *signResponseAgentMsg:
var sig ssh.Signature
if err := ssh.Unmarshal(msg.SigBlob, &sig); err != nil {
return nil, err
}
return &sig, nil
case *failureAgentMsg:
return nil, errors.New("agent: failed to sign challenge")
default:
return nil, fmt.Errorf("agent: failed to sign challenge, unexpected message type %T", msg)
}
}
// unmarshal parses an agent message in packet, returning the parsed
// form and the message type of packet.
func unmarshal(packet []byte) (interface{}, error) {
if len(packet) < 1 {
return nil, errors.New("agent: empty packet")
}
var msg interface{}
switch packet[0] {
case agentFailure:
return new(failureAgentMsg), nil
case agentSuccess:
return new(successAgentMsg), nil
case agentIdentitiesAnswer:
msg = new(identitiesAnswerAgentMsg)
case agentSignResponse:
msg = new(signResponseAgentMsg)
case agentV1IdentitiesAnswer:
msg = new(agentV1IdentityMsg)
default:
return nil, fmt.Errorf("agent: unknown type tag %d", packet[0])
}
if err := ssh.Unmarshal(packet, msg); err != nil {
return nil, err
}
return msg, nil
}
type rsaKeyMsg struct {
Type string `sshtype:"17|25"`
N *big.Int
E *big.Int
D *big.Int
Iqmp *big.Int // IQMP = Inverse Q Mod P
P *big.Int
Q *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type dsaKeyMsg struct {
Type string `sshtype:"17|25"`
P *big.Int
Q *big.Int
G *big.Int
Y *big.Int
X *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ecdsaKeyMsg struct {
Type string `sshtype:"17|25"`
Curve string
KeyBytes []byte
D *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ed25519KeyMsg struct {
Type string `sshtype:"17|25"`
Pub []byte
Priv []byte
Comments string
Constraints []byte `ssh:"rest"`
}
// Insert adds a private key to the agent.
func (c *client) insertKey(s interface{}, comment string, constraints []byte) error {
var req []byte
switch k := s.(type) {
case *rsa.PrivateKey:
if len(k.Primes) != 2 {
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
}
k.Precompute()
req = ssh.Marshal(rsaKeyMsg{
Type: ssh.KeyAlgoRSA,
N: k.N,
E: big.NewInt(int64(k.E)),
D: k.D,
Iqmp: k.Precomputed.Qinv,
P: k.Primes[0],
Q: k.Primes[1],
Comments: comment,
Constraints: constraints,
})
case *dsa.PrivateKey:
req = ssh.Marshal(dsaKeyMsg{
Type: ssh.InsecureKeyAlgoDSA,
P: k.P,
Q: k.Q,
G: k.G,
Y: k.Y,
X: k.X,
Comments: comment,
Constraints: constraints,
})
case *ecdsa.PrivateKey:
nistID := fmt.Sprintf("nistp%d", k.Params().BitSize)
req = ssh.Marshal(ecdsaKeyMsg{
Type: "ecdsa-sha2-" + nistID,
Curve: nistID,
KeyBytes: elliptic.Marshal(k.Curve, k.X, k.Y),
D: k.D,
Comments: comment,
Constraints: constraints,
})
case ed25519.PrivateKey:
req = ssh.Marshal(ed25519KeyMsg{
Type: ssh.KeyAlgoED25519,
Pub: []byte(k)[32:],
Priv: []byte(k),
Comments: comment,
Constraints: constraints,
})
// This function originally supported only *ed25519.PrivateKey, however the
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
// We still support the pointer variant for backwards compatibility.
case *ed25519.PrivateKey:
req = ssh.Marshal(ed25519KeyMsg{
Type: ssh.KeyAlgoED25519,
Pub: []byte(*k)[32:],
Priv: []byte(*k),
Comments: comment,
Constraints: constraints,
})
default:
return fmt.Errorf("agent: unsupported key type %T", s)
}
// if constraints are present then the message type needs to be changed.
if len(constraints) != 0 {
req[0] = agentAddIDConstrained
}
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
type rsaCertMsg struct {
Type string `sshtype:"17|25"`
CertBytes []byte
D *big.Int
Iqmp *big.Int // IQMP = Inverse Q Mod P
P *big.Int
Q *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type dsaCertMsg struct {
Type string `sshtype:"17|25"`
CertBytes []byte
X *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ecdsaCertMsg struct {
Type string `sshtype:"17|25"`
CertBytes []byte
D *big.Int
Comments string
Constraints []byte `ssh:"rest"`
}
type ed25519CertMsg struct {
Type string `sshtype:"17|25"`
CertBytes []byte
Pub []byte
Priv []byte
Comments string
Constraints []byte `ssh:"rest"`
}
// Add adds a private key to the agent. If a certificate is given,
// that certificate is added instead as public key.
func (c *client) Add(key AddedKey) error {
var constraints []byte
if secs := key.LifetimeSecs; secs != 0 {
constraints = append(constraints, ssh.Marshal(constrainLifetimeAgentMsg{secs})...)
}
if key.ConfirmBeforeUse {
constraints = append(constraints, agentConstrainConfirm)
}
cert := key.Certificate
if cert == nil {
return c.insertKey(key.PrivateKey, key.Comment, constraints)
}
return c.insertCert(key.PrivateKey, cert, key.Comment, constraints)
}
func (c *client) insertCert(s interface{}, cert *ssh.Certificate, comment string, constraints []byte) error {
var req []byte
switch k := s.(type) {
case *rsa.PrivateKey:
if len(k.Primes) != 2 {
return fmt.Errorf("agent: unsupported RSA key with %d primes", len(k.Primes))
}
k.Precompute()
req = ssh.Marshal(rsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
D: k.D,
Iqmp: k.Precomputed.Qinv,
P: k.Primes[0],
Q: k.Primes[1],
Comments: comment,
Constraints: constraints,
})
case *dsa.PrivateKey:
req = ssh.Marshal(dsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
X: k.X,
Comments: comment,
Constraints: constraints,
})
case *ecdsa.PrivateKey:
req = ssh.Marshal(ecdsaCertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
D: k.D,
Comments: comment,
Constraints: constraints,
})
case ed25519.PrivateKey:
req = ssh.Marshal(ed25519CertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
Pub: []byte(k)[32:],
Priv: []byte(k),
Comments: comment,
Constraints: constraints,
})
// This function originally supported only *ed25519.PrivateKey, however the
// general idiom is to pass ed25519.PrivateKey by value, not by pointer.
// We still support the pointer variant for backwards compatibility.
case *ed25519.PrivateKey:
req = ssh.Marshal(ed25519CertMsg{
Type: cert.Type(),
CertBytes: cert.Marshal(),
Pub: []byte(*k)[32:],
Priv: []byte(*k),
Comments: comment,
Constraints: constraints,
})
default:
return fmt.Errorf("agent: unsupported key type %T", s)
}
// if constraints are present then the message type needs to be changed.
if len(constraints) != 0 {
req[0] = agentAddIDConstrained
}
signer, err := ssh.NewSignerFromKey(s)
if err != nil {
return err
}
if !bytes.Equal(cert.Key.Marshal(), signer.PublicKey().Marshal()) {
return errors.New("agent: signer and cert have different public key")
}
resp, err := c.call(req)
if err != nil {
return err
}
if _, ok := resp.(*successAgentMsg); ok {
return nil
}
return errors.New("agent: failure")
}
// Signers provides a callback for client authentication.
func (c *client) Signers() ([]ssh.Signer, error) {
keys, err := c.List()
if err != nil {
return nil, err
}
var result []ssh.Signer
for _, k := range keys {
result = append(result, &agentKeyringSigner{c, k})
}
return result, nil
}
type agentKeyringSigner struct {
agent *client
pub ssh.PublicKey
}
func (s *agentKeyringSigner) PublicKey() ssh.PublicKey {
return s.pub
}
func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) {
// The agent has its own entropy source, so the rand argument is ignored.
return s.agent.Sign(s.pub, data)
}
func (s *agentKeyringSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) {
if algorithm == "" || algorithm == underlyingAlgo(s.pub.Type()) {
return s.Sign(rand, data)
}
var flags SignatureFlags
switch algorithm {
case ssh.KeyAlgoRSASHA256:
flags = SignatureFlagRsaSha256
case ssh.KeyAlgoRSASHA512:
flags = SignatureFlagRsaSha512
default:
return nil, fmt.Errorf("agent: unsupported algorithm %q", algorithm)
}
return s.agent.SignWithFlags(s.pub, data, flags)
}
var _ ssh.AlgorithmSigner = &agentKeyringSigner{}
// certKeyAlgoNames is a mapping from known certificate algorithm names to the
// corresponding public key signature algorithm.
//
// This map must be kept in sync with the one in certs.go.
var certKeyAlgoNames = map[string]string{
ssh.CertAlgoRSAv01: ssh.KeyAlgoRSA,
ssh.CertAlgoRSASHA256v01: ssh.KeyAlgoRSASHA256,
ssh.CertAlgoRSASHA512v01: ssh.KeyAlgoRSASHA512,
ssh.InsecureCertAlgoDSAv01: ssh.InsecureKeyAlgoDSA,
ssh.CertAlgoECDSA256v01: ssh.KeyAlgoECDSA256,
ssh.CertAlgoECDSA384v01: ssh.KeyAlgoECDSA384,
ssh.CertAlgoECDSA521v01: ssh.KeyAlgoECDSA521,
ssh.CertAlgoSKECDSA256v01: ssh.KeyAlgoSKECDSA256,
ssh.CertAlgoED25519v01: ssh.KeyAlgoED25519,
ssh.CertAlgoSKED25519v01: ssh.KeyAlgoSKED25519,
}
// underlyingAlgo returns the signature algorithm associated with algo (which is
// an advertised or negotiated public key or host key algorithm). These are
// usually the same, except for certificate algorithms.
func underlyingAlgo(algo string) string {
if a, ok := certKeyAlgoNames[algo]; ok {
return a
}
return algo
}
// Calls an extension method. It is up to the agent implementation as to whether or not
// any particular extension is supported and may always return an error. Because the
// type of the response is up to the implementation, this returns the bytes of the
// response and does not attempt any type of unmarshalling.
func (c *client) Extension(extensionType string, contents []byte) ([]byte, error) {
req := ssh.Marshal(extensionAgentMsg{
ExtensionType: extensionType,
Contents: contents,
})
buf, err := c.callRaw(req)
if err != nil {
return nil, err
}
if len(buf) == 0 {
return nil, errors.New("agent: failure; empty response")
}
// [PROTOCOL.agent] section 4.7 indicates that an SSH_AGENT_FAILURE message
// represents an agent that does not support the extension
if buf[0] == agentFailure {
return nil, ErrExtensionUnsupported
}
if buf[0] == agentExtensionFailure {
return nil, errors.New("agent: generic extension failure")
}
return buf, nil
}

103
vendor/golang.org/x/crypto/ssh/agent/forward.go generated vendored Normal file
View file

@ -0,0 +1,103 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"errors"
"io"
"net"
"sync"
"golang.org/x/crypto/ssh"
)
// RequestAgentForwarding sets up agent forwarding for the session.
// ForwardToAgent or ForwardToRemote should be called to route
// the authentication requests.
func RequestAgentForwarding(session *ssh.Session) error {
ok, err := session.SendRequest("auth-agent-req@openssh.com", true, nil)
if err != nil {
return err
}
if !ok {
return errors.New("forwarding request denied")
}
return nil
}
// ForwardToAgent routes authentication requests to the given keyring.
func ForwardToAgent(client *ssh.Client, keyring Agent) error {
channels := client.HandleChannelOpen(channelType)
if channels == nil {
return errors.New("agent: already have handler for " + channelType)
}
go func() {
for ch := range channels {
channel, reqs, err := ch.Accept()
if err != nil {
continue
}
go ssh.DiscardRequests(reqs)
go func() {
ServeAgent(keyring, channel)
channel.Close()
}()
}
}()
return nil
}
const channelType = "auth-agent@openssh.com"
// ForwardToRemote routes authentication requests to the ssh-agent
// process serving on the given unix socket.
func ForwardToRemote(client *ssh.Client, addr string) error {
channels := client.HandleChannelOpen(channelType)
if channels == nil {
return errors.New("agent: already have handler for " + channelType)
}
conn, err := net.Dial("unix", addr)
if err != nil {
return err
}
conn.Close()
go func() {
for ch := range channels {
channel, reqs, err := ch.Accept()
if err != nil {
continue
}
go ssh.DiscardRequests(reqs)
go forwardUnixSocket(channel, addr)
}
}()
return nil
}
func forwardUnixSocket(channel ssh.Channel, addr string) {
conn, err := net.Dial("unix", addr)
if err != nil {
return
}
var wg sync.WaitGroup
wg.Add(2)
go func() {
io.Copy(conn, channel)
conn.(*net.UnixConn).CloseWrite()
wg.Done()
}()
go func() {
io.Copy(channel, conn)
channel.CloseWrite()
wg.Done()
}()
wg.Wait()
conn.Close()
channel.Close()
}

250
vendor/golang.org/x/crypto/ssh/agent/keyring.go generated vendored Normal file
View file

@ -0,0 +1,250 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"bytes"
"crypto/rand"
"crypto/subtle"
"errors"
"fmt"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
type privKey struct {
signer ssh.Signer
comment string
expire *time.Time
}
type keyring struct {
mu sync.Mutex
keys []privKey
locked bool
passphrase []byte
}
var errLocked = errors.New("agent: locked")
// NewKeyring returns an Agent that holds keys in memory. It is safe
// for concurrent use by multiple goroutines.
func NewKeyring() Agent {
return &keyring{}
}
// RemoveAll removes all identities.
func (r *keyring) RemoveAll() error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
r.keys = nil
return nil
}
// removeLocked does the actual key removal. The caller must already be holding the
// keyring mutex.
func (r *keyring) removeLocked(want []byte) error {
found := false
for i := 0; i < len(r.keys); {
if bytes.Equal(r.keys[i].signer.PublicKey().Marshal(), want) {
found = true
r.keys[i] = r.keys[len(r.keys)-1]
r.keys = r.keys[:len(r.keys)-1]
continue
} else {
i++
}
}
if !found {
return errors.New("agent: key not found")
}
return nil
}
// Remove removes all identities with the given public key.
func (r *keyring) Remove(key ssh.PublicKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
return r.removeLocked(key.Marshal())
}
// Lock locks the agent. Sign and Remove will fail, and List will return an empty list.
func (r *keyring) Lock(passphrase []byte) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
r.locked = true
r.passphrase = passphrase
return nil
}
// Unlock undoes the effect of Lock
func (r *keyring) Unlock(passphrase []byte) error {
r.mu.Lock()
defer r.mu.Unlock()
if !r.locked {
return errors.New("agent: not locked")
}
if 1 != subtle.ConstantTimeCompare(passphrase, r.passphrase) {
return fmt.Errorf("agent: incorrect passphrase")
}
r.locked = false
r.passphrase = nil
return nil
}
// expireKeysLocked removes expired keys from the keyring. If a key was added
// with a lifetimesecs constraint and seconds >= lifetimesecs seconds have
// elapsed, it is removed. The caller *must* be holding the keyring mutex.
func (r *keyring) expireKeysLocked() {
for _, k := range r.keys {
if k.expire != nil && time.Now().After(*k.expire) {
r.removeLocked(k.signer.PublicKey().Marshal())
}
}
}
// List returns the identities known to the agent.
func (r *keyring) List() ([]*Key, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
// section 2.7: locked agents return empty.
return nil, nil
}
r.expireKeysLocked()
var ids []*Key
for _, k := range r.keys {
pub := k.signer.PublicKey()
ids = append(ids, &Key{
Format: pub.Type(),
Blob: pub.Marshal(),
Comment: k.comment})
}
return ids, nil
}
// Insert adds a private key to the keyring. If a certificate
// is given, that certificate is added as public key. Note that
// any constraints given are ignored.
func (r *keyring) Add(key AddedKey) error {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return errLocked
}
signer, err := ssh.NewSignerFromKey(key.PrivateKey)
if err != nil {
return err
}
if cert := key.Certificate; cert != nil {
signer, err = ssh.NewCertSigner(cert, signer)
if err != nil {
return err
}
}
p := privKey{
signer: signer,
comment: key.Comment,
}
if key.LifetimeSecs > 0 {
t := time.Now().Add(time.Duration(key.LifetimeSecs) * time.Second)
p.expire = &t
}
// If we already have a Signer with the same public key, replace it with the
// new one.
for idx, k := range r.keys {
if bytes.Equal(k.signer.PublicKey().Marshal(), p.signer.PublicKey().Marshal()) {
r.keys[idx] = p
return nil
}
}
r.keys = append(r.keys, p)
return nil
}
// Sign returns a signature for the data.
func (r *keyring) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) {
return r.SignWithFlags(key, data, 0)
}
func (r *keyring) SignWithFlags(key ssh.PublicKey, data []byte, flags SignatureFlags) (*ssh.Signature, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return nil, errLocked
}
r.expireKeysLocked()
wanted := key.Marshal()
for _, k := range r.keys {
if bytes.Equal(k.signer.PublicKey().Marshal(), wanted) {
if flags == 0 {
return k.signer.Sign(rand.Reader, data)
} else {
if algorithmSigner, ok := k.signer.(ssh.AlgorithmSigner); !ok {
return nil, fmt.Errorf("agent: signature does not support non-default signature algorithm: %T", k.signer)
} else {
var algorithm string
switch flags {
case SignatureFlagRsaSha256:
algorithm = ssh.KeyAlgoRSASHA256
case SignatureFlagRsaSha512:
algorithm = ssh.KeyAlgoRSASHA512
default:
return nil, fmt.Errorf("agent: unsupported signature flags: %d", flags)
}
return algorithmSigner.SignWithAlgorithm(rand.Reader, data, algorithm)
}
}
}
}
return nil, errors.New("not found")
}
// Signers returns signers for all the known keys.
func (r *keyring) Signers() ([]ssh.Signer, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.locked {
return nil, errLocked
}
r.expireKeysLocked()
s := make([]ssh.Signer, 0, len(r.keys))
for _, k := range r.keys {
s = append(s, k.signer)
}
return s, nil
}
// The keyring does not support any extensions
func (r *keyring) Extension(extensionType string, contents []byte) ([]byte, error) {
return nil, ErrExtensionUnsupported
}

573
vendor/golang.org/x/crypto/ssh/agent/server.go generated vendored Normal file
View file

@ -0,0 +1,573 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agent
import (
"crypto/dsa"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"math/big"
"golang.org/x/crypto/ssh"
)
// server wraps an Agent and uses it to implement the agent side of
// the SSH-agent, wire protocol.
type server struct {
agent Agent
}
func (s *server) processRequestBytes(reqData []byte) []byte {
rep, err := s.processRequest(reqData)
if err != nil {
if err != errLocked {
// TODO(hanwen): provide better logging interface?
log.Printf("agent %d: %v", reqData[0], err)
}
return []byte{agentFailure}
}
if rep == nil {
return []byte{agentSuccess}
}
return ssh.Marshal(rep)
}
func marshalKey(k *Key) []byte {
var record struct {
Blob []byte
Comment string
}
record.Blob = k.Marshal()
record.Comment = k.Comment
return ssh.Marshal(&record)
}
// See [PROTOCOL.agent], section 2.5.1.
const agentV1IdentitiesAnswer = 2
type agentV1IdentityMsg struct {
Numkeys uint32 `sshtype:"2"`
}
type agentRemoveIdentityMsg struct {
KeyBlob []byte `sshtype:"18"`
}
type agentLockMsg struct {
Passphrase []byte `sshtype:"22"`
}
type agentUnlockMsg struct {
Passphrase []byte `sshtype:"23"`
}
func (s *server) processRequest(data []byte) (interface{}, error) {
switch data[0] {
case agentRequestV1Identities:
return &agentV1IdentityMsg{0}, nil
case agentRemoveAllV1Identities:
return nil, nil
case agentRemoveIdentity:
var req agentRemoveIdentityMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
var wk wireKey
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
return nil, err
}
return nil, s.agent.Remove(&Key{Format: wk.Format, Blob: req.KeyBlob})
case agentRemoveAllIdentities:
return nil, s.agent.RemoveAll()
case agentLock:
var req agentLockMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
return nil, s.agent.Lock(req.Passphrase)
case agentUnlock:
var req agentUnlockMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
return nil, s.agent.Unlock(req.Passphrase)
case agentSignRequest:
var req signRequestAgentMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
var wk wireKey
if err := ssh.Unmarshal(req.KeyBlob, &wk); err != nil {
return nil, err
}
k := &Key{
Format: wk.Format,
Blob: req.KeyBlob,
}
var sig *ssh.Signature
var err error
if extendedAgent, ok := s.agent.(ExtendedAgent); ok {
sig, err = extendedAgent.SignWithFlags(k, req.Data, SignatureFlags(req.Flags))
} else {
sig, err = s.agent.Sign(k, req.Data)
}
if err != nil {
return nil, err
}
return &signResponseAgentMsg{SigBlob: ssh.Marshal(sig)}, nil
case agentRequestIdentities:
keys, err := s.agent.List()
if err != nil {
return nil, err
}
rep := identitiesAnswerAgentMsg{
NumKeys: uint32(len(keys)),
}
for _, k := range keys {
rep.Keys = append(rep.Keys, marshalKey(k)...)
}
return rep, nil
case agentAddIDConstrained, agentAddIdentity:
return nil, s.insertIdentity(data)
case agentExtension:
// Return a stub object where the whole contents of the response gets marshaled.
var responseStub struct {
Rest []byte `ssh:"rest"`
}
if extendedAgent, ok := s.agent.(ExtendedAgent); !ok {
// If this agent doesn't implement extensions, [PROTOCOL.agent] section 4.7
// requires that we return a standard SSH_AGENT_FAILURE message.
responseStub.Rest = []byte{agentFailure}
} else {
var req extensionAgentMsg
if err := ssh.Unmarshal(data, &req); err != nil {
return nil, err
}
res, err := extendedAgent.Extension(req.ExtensionType, req.Contents)
if err != nil {
// If agent extensions are unsupported, return a standard SSH_AGENT_FAILURE
// message as required by [PROTOCOL.agent] section 4.7.
if err == ErrExtensionUnsupported {
responseStub.Rest = []byte{agentFailure}
} else {
// As the result of any other error processing an extension request,
// [PROTOCOL.agent] section 4.7 requires that we return a
// SSH_AGENT_EXTENSION_FAILURE code.
responseStub.Rest = []byte{agentExtensionFailure}
}
} else {
if len(res) == 0 {
return nil, nil
}
responseStub.Rest = res
}
}
return responseStub, nil
}
return nil, fmt.Errorf("unknown opcode %d", data[0])
}
func parseConstraints(constraints []byte) (lifetimeSecs uint32, confirmBeforeUse bool, extensions []ConstraintExtension, err error) {
for len(constraints) != 0 {
switch constraints[0] {
case agentConstrainLifetime:
if len(constraints) < 5 {
return 0, false, nil, io.ErrUnexpectedEOF
}
lifetimeSecs = binary.BigEndian.Uint32(constraints[1:5])
constraints = constraints[5:]
case agentConstrainConfirm:
confirmBeforeUse = true
constraints = constraints[1:]
case agentConstrainExtension, agentConstrainExtensionV00:
var msg constrainExtensionAgentMsg
if err = ssh.Unmarshal(constraints, &msg); err != nil {
return 0, false, nil, err
}
extensions = append(extensions, ConstraintExtension{
ExtensionName: msg.ExtensionName,
ExtensionDetails: msg.ExtensionDetails,
})
constraints = msg.Rest
default:
return 0, false, nil, fmt.Errorf("unknown constraint type: %d", constraints[0])
}
}
return
}
func setConstraints(key *AddedKey, constraintBytes []byte) error {
lifetimeSecs, confirmBeforeUse, constraintExtensions, err := parseConstraints(constraintBytes)
if err != nil {
return err
}
key.LifetimeSecs = lifetimeSecs
key.ConfirmBeforeUse = confirmBeforeUse
key.ConstraintExtensions = constraintExtensions
return nil
}
func parseRSAKey(req []byte) (*AddedKey, error) {
var k rsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
if k.E.BitLen() > 30 {
return nil, errors.New("agent: RSA public exponent too large")
}
priv := &rsa.PrivateKey{
PublicKey: rsa.PublicKey{
E: int(k.E.Int64()),
N: k.N,
},
D: k.D,
Primes: []*big.Int{k.P, k.Q},
}
priv.Precompute()
addedKey := &AddedKey{PrivateKey: priv, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func parseEd25519Key(req []byte) (*AddedKey, error) {
var k ed25519KeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
priv := ed25519.PrivateKey(k.Priv)
addedKey := &AddedKey{PrivateKey: &priv, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func parseDSAKey(req []byte) (*AddedKey, error) {
var k dsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
priv := &dsa.PrivateKey{
PublicKey: dsa.PublicKey{
Parameters: dsa.Parameters{
P: k.P,
Q: k.Q,
G: k.G,
},
Y: k.Y,
},
X: k.X,
}
addedKey := &AddedKey{PrivateKey: priv, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func unmarshalECDSA(curveName string, keyBytes []byte, privScalar *big.Int) (priv *ecdsa.PrivateKey, err error) {
priv = &ecdsa.PrivateKey{
D: privScalar,
}
switch curveName {
case "nistp256":
priv.Curve = elliptic.P256()
case "nistp384":
priv.Curve = elliptic.P384()
case "nistp521":
priv.Curve = elliptic.P521()
default:
return nil, fmt.Errorf("agent: unknown curve %q", curveName)
}
priv.X, priv.Y = elliptic.Unmarshal(priv.Curve, keyBytes)
if priv.X == nil || priv.Y == nil {
return nil, errors.New("agent: point not on curve")
}
return priv, nil
}
func parseEd25519Cert(req []byte) (*AddedKey, error) {
var k ed25519CertMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
if err != nil {
return nil, err
}
priv := ed25519.PrivateKey(k.Priv)
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
return nil, errors.New("agent: bad ED25519 certificate")
}
addedKey := &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func parseECDSAKey(req []byte) (*AddedKey, error) {
var k ecdsaKeyMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
priv, err := unmarshalECDSA(k.Curve, k.KeyBytes, k.D)
if err != nil {
return nil, err
}
addedKey := &AddedKey{PrivateKey: priv, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func parseRSACert(req []byte) (*AddedKey, error) {
var k rsaCertMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
if err != nil {
return nil, err
}
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
return nil, errors.New("agent: bad RSA certificate")
}
// An RSA publickey as marshaled by rsaPublicKey.Marshal() in keys.go
var rsaPub struct {
Name string
E *big.Int
N *big.Int
}
if err := ssh.Unmarshal(cert.Key.Marshal(), &rsaPub); err != nil {
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
}
if rsaPub.E.BitLen() > 30 {
return nil, errors.New("agent: RSA public exponent too large")
}
priv := rsa.PrivateKey{
PublicKey: rsa.PublicKey{
E: int(rsaPub.E.Int64()),
N: rsaPub.N,
},
D: k.D,
Primes: []*big.Int{k.Q, k.P},
}
priv.Precompute()
addedKey := &AddedKey{PrivateKey: &priv, Certificate: cert, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func parseDSACert(req []byte) (*AddedKey, error) {
var k dsaCertMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
if err != nil {
return nil, err
}
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
return nil, errors.New("agent: bad DSA certificate")
}
// A DSA publickey as marshaled by dsaPublicKey.Marshal() in keys.go
var w struct {
Name string
P, Q, G, Y *big.Int
}
if err := ssh.Unmarshal(cert.Key.Marshal(), &w); err != nil {
return nil, fmt.Errorf("agent: Unmarshal failed to parse public key: %v", err)
}
priv := &dsa.PrivateKey{
PublicKey: dsa.PublicKey{
Parameters: dsa.Parameters{
P: w.P,
Q: w.Q,
G: w.G,
},
Y: w.Y,
},
X: k.X,
}
addedKey := &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func parseECDSACert(req []byte) (*AddedKey, error) {
var k ecdsaCertMsg
if err := ssh.Unmarshal(req, &k); err != nil {
return nil, err
}
pubKey, err := ssh.ParsePublicKey(k.CertBytes)
if err != nil {
return nil, err
}
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
return nil, errors.New("agent: bad ECDSA certificate")
}
// An ECDSA publickey as marshaled by ecdsaPublicKey.Marshal() in keys.go
var ecdsaPub struct {
Name string
ID string
Key []byte
}
if err := ssh.Unmarshal(cert.Key.Marshal(), &ecdsaPub); err != nil {
return nil, err
}
priv, err := unmarshalECDSA(ecdsaPub.ID, ecdsaPub.Key, k.D)
if err != nil {
return nil, err
}
addedKey := &AddedKey{PrivateKey: priv, Certificate: cert, Comment: k.Comments}
if err := setConstraints(addedKey, k.Constraints); err != nil {
return nil, err
}
return addedKey, nil
}
func (s *server) insertIdentity(req []byte) error {
var record struct {
Type string `sshtype:"17|25"`
Rest []byte `ssh:"rest"`
}
if err := ssh.Unmarshal(req, &record); err != nil {
return err
}
var addedKey *AddedKey
var err error
switch record.Type {
case ssh.KeyAlgoRSA:
addedKey, err = parseRSAKey(req)
case ssh.InsecureKeyAlgoDSA:
addedKey, err = parseDSAKey(req)
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
addedKey, err = parseECDSAKey(req)
case ssh.KeyAlgoED25519:
addedKey, err = parseEd25519Key(req)
case ssh.CertAlgoRSAv01:
addedKey, err = parseRSACert(req)
case ssh.InsecureCertAlgoDSAv01:
addedKey, err = parseDSACert(req)
case ssh.CertAlgoECDSA256v01, ssh.CertAlgoECDSA384v01, ssh.CertAlgoECDSA521v01:
addedKey, err = parseECDSACert(req)
case ssh.CertAlgoED25519v01:
addedKey, err = parseEd25519Cert(req)
default:
return fmt.Errorf("agent: not implemented: %q", record.Type)
}
if err != nil {
return err
}
return s.agent.Add(*addedKey)
}
// ServeAgent serves the agent protocol on the given connection. It
// returns when an I/O error occurs.
func ServeAgent(agent Agent, c io.ReadWriter) error {
s := &server{agent}
var length [4]byte
for {
if _, err := io.ReadFull(c, length[:]); err != nil {
return err
}
l := binary.BigEndian.Uint32(length[:])
if l == 0 {
return fmt.Errorf("agent: request size is 0")
}
if l > maxAgentResponseBytes {
// We also cap requests.
return fmt.Errorf("agent: request too large: %d", l)
}
req := make([]byte, l)
if _, err := io.ReadFull(c, req); err != nil {
return err
}
repData := s.processRequestBytes(req)
if len(repData) > maxAgentResponseBytes {
return fmt.Errorf("agent: reply too large: %d bytes", len(repData))
}
binary.BigEndian.PutUint32(length[:], uint32(len(repData)))
if _, err := c.Write(length[:]); err != nil {
return err
}
if _, err := c.Write(repData); err != nil {
return err
}
}
}

532
vendor/golang.org/x/crypto/ssh/knownhosts/knownhosts.go generated vendored Normal file
View file

@ -0,0 +1,532 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package knownhosts implements a parser for the OpenSSH known_hosts
// host key database, and provides utility functions for writing
// OpenSSH compliant known_hosts files.
package knownhosts
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"os"
"strings"
"golang.org/x/crypto/ssh"
)
// See the sshd manpage
// (http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) for
// background.
type addr struct{ host, port string }
func (a *addr) String() string {
h := a.host
if strings.Contains(h, ":") {
h = "[" + h + "]"
}
return h + ":" + a.port
}
type matcher interface {
match(addr) bool
}
type hostPattern struct {
negate bool
addr addr
}
func (p *hostPattern) String() string {
n := ""
if p.negate {
n = "!"
}
return n + p.addr.String()
}
type hostPatterns []hostPattern
func (ps hostPatterns) match(a addr) bool {
matched := false
for _, p := range ps {
if !p.match(a) {
continue
}
if p.negate {
return false
}
matched = true
}
return matched
}
// See
// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/addrmatch.c
// The matching of * has no regard for separators, unlike filesystem globs
func wildcardMatch(pat []byte, str []byte) bool {
for {
if len(pat) == 0 {
return len(str) == 0
}
if len(str) == 0 {
return false
}
if pat[0] == '*' {
if len(pat) == 1 {
return true
}
for j := range str {
if wildcardMatch(pat[1:], str[j:]) {
return true
}
}
return false
}
if pat[0] == '?' || pat[0] == str[0] {
pat = pat[1:]
str = str[1:]
} else {
return false
}
}
}
func (p *hostPattern) match(a addr) bool {
return wildcardMatch([]byte(p.addr.host), []byte(a.host)) && p.addr.port == a.port
}
type keyDBLine struct {
cert bool
matcher matcher
knownKey KnownKey
}
func serialize(k ssh.PublicKey) string {
return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal())
}
func (l *keyDBLine) match(a addr) bool {
return l.matcher.match(a)
}
type hostKeyDB struct {
// Serialized version of revoked keys
revoked map[string]*KnownKey
lines []keyDBLine
}
func newHostKeyDB() *hostKeyDB {
db := &hostKeyDB{
revoked: make(map[string]*KnownKey),
}
return db
}
func keyEq(a, b ssh.PublicKey) bool {
return bytes.Equal(a.Marshal(), b.Marshal())
}
// IsHostAuthority can be used as a callback in ssh.CertChecker
func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
h, p, err := net.SplitHostPort(address)
if err != nil {
return false
}
a := addr{host: h, port: p}
for _, l := range db.lines {
if l.cert && keyEq(l.knownKey.Key, remote) && l.match(a) {
return true
}
}
return false
}
// IsRevoked can be used as a callback in ssh.CertChecker
func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
_, ok := db.revoked[string(key.Marshal())]
return ok
}
const markerCert = "@cert-authority"
const markerRevoked = "@revoked"
func nextWord(line []byte) (string, []byte) {
i := bytes.IndexAny(line, "\t ")
if i == -1 {
return string(line), nil
}
return string(line[:i]), bytes.TrimSpace(line[i:])
}
func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
if w, next := nextWord(line); w == markerCert || w == markerRevoked {
marker = w
line = next
}
host, line = nextWord(line)
if len(line) == 0 {
return "", "", nil, errors.New("knownhosts: missing host pattern")
}
// ignore the keytype as it's in the key blob anyway.
_, line = nextWord(line)
if len(line) == 0 {
return "", "", nil, errors.New("knownhosts: missing key type pattern")
}
keyBlob, _ := nextWord(line)
keyBytes, err := base64.StdEncoding.DecodeString(keyBlob)
if err != nil {
return "", "", nil, err
}
key, err = ssh.ParsePublicKey(keyBytes)
if err != nil {
return "", "", nil, err
}
return marker, host, key, nil
}
func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error {
marker, pattern, key, err := parseLine(line)
if err != nil {
return err
}
if marker == markerRevoked {
db.revoked[string(key.Marshal())] = &KnownKey{
Key: key,
Filename: filename,
Line: linenum,
}
return nil
}
entry := keyDBLine{
cert: marker == markerCert,
knownKey: KnownKey{
Filename: filename,
Line: linenum,
Key: key,
},
}
if pattern[0] == '|' {
entry.matcher, err = newHashedHost(pattern)
} else {
entry.matcher, err = newHostnameMatcher(pattern)
}
if err != nil {
return err
}
db.lines = append(db.lines, entry)
return nil
}
func newHostnameMatcher(pattern string) (matcher, error) {
var hps hostPatterns
for _, p := range strings.Split(pattern, ",") {
if len(p) == 0 {
continue
}
var a addr
var negate bool
if p[0] == '!' {
negate = true
p = p[1:]
}
if len(p) == 0 {
return nil, errors.New("knownhosts: negation without following hostname")
}
var err error
if p[0] == '[' {
a.host, a.port, err = net.SplitHostPort(p)
if err != nil {
return nil, err
}
} else {
a.host, a.port, err = net.SplitHostPort(p)
if err != nil {
a.host = p
a.port = "22"
}
}
hps = append(hps, hostPattern{
negate: negate,
addr: a,
})
}
return hps, nil
}
// KnownKey represents a key declared in a known_hosts file.
type KnownKey struct {
Key ssh.PublicKey
Filename string
Line int
}
func (k *KnownKey) String() string {
return fmt.Sprintf("%s:%d: %s", k.Filename, k.Line, serialize(k.Key))
}
// KeyError is returned if we did not find the key in the host key
// database, or there was a mismatch. Typically, in batch
// applications, this should be interpreted as failure. Interactive
// applications can offer an interactive prompt to the user.
type KeyError struct {
// Want holds the accepted host keys. For each key algorithm,
// there can be multiple hostkeys. If Want is empty, the host
// is unknown. If Want is non-empty, there was a mismatch, which
// can signify a MITM attack.
Want []KnownKey
}
func (u *KeyError) Error() string {
if len(u.Want) == 0 {
return "knownhosts: key is unknown"
}
return "knownhosts: key mismatch"
}
// RevokedError is returned if we found a key that was revoked.
type RevokedError struct {
Revoked KnownKey
}
func (r *RevokedError) Error() string {
return "knownhosts: key is revoked"
}
// check checks a key against the host database. This should not be
// used for verifying certificates.
func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil {
return &RevokedError{Revoked: *revoked}
}
host, port, err := net.SplitHostPort(remote.String())
if err != nil {
return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", remote, err)
}
hostToCheck := addr{host, port}
if address != "" {
// Give preference to the hostname if available.
host, port, err := net.SplitHostPort(address)
if err != nil {
return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", address, err)
}
hostToCheck = addr{host, port}
}
return db.checkAddr(hostToCheck, remoteKey)
}
// checkAddr checks if we can find the given public key for the
// given address. If we only find an entry for the IP address,
// or only the hostname, then this still succeeds.
func (db *hostKeyDB) checkAddr(a addr, remoteKey ssh.PublicKey) error {
// TODO(hanwen): are these the right semantics? What if there
// is just a key for the IP address, but not for the
// hostname?
keyErr := &KeyError{}
for _, l := range db.lines {
if !l.match(a) {
continue
}
keyErr.Want = append(keyErr.Want, l.knownKey)
if keyEq(l.knownKey.Key, remoteKey) {
return nil
}
}
return keyErr
}
// The Read function parses file contents.
func (db *hostKeyDB) Read(r io.Reader, filename string) error {
scanner := bufio.NewScanner(r)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' {
continue
}
if err := db.parseLine(line, filename, lineNum); err != nil {
return fmt.Errorf("knownhosts: %s:%d: %v", filename, lineNum, err)
}
}
return scanner.Err()
}
// New creates a host key callback from the given OpenSSH host key
// files. The returned callback is for use in
// ssh.ClientConfig.HostKeyCallback. By preference, the key check
// operates on the hostname if available, i.e. if a server changes its
// IP address, the host key check will still succeed, even though a
// record of the new IP address is not available.
func New(files ...string) (ssh.HostKeyCallback, error) {
db := newHostKeyDB()
for _, fn := range files {
f, err := os.Open(fn)
if err != nil {
return nil, err
}
defer f.Close()
if err := db.Read(f, fn); err != nil {
return nil, err
}
}
var certChecker ssh.CertChecker
certChecker.IsHostAuthority = db.IsHostAuthority
certChecker.IsRevoked = db.IsRevoked
certChecker.HostKeyFallback = db.check
return certChecker.CheckHostKey, nil
}
// Normalize normalizes an address into the form used in known_hosts. Supports
// IPv4, hostnames, bracketed IPv6. Any other non-standard formats are returned
// with minimal transformation.
func Normalize(address string) string {
const defaultSSHPort = "22"
host, port, err := net.SplitHostPort(address)
if err != nil {
host = address
port = defaultSSHPort
}
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = host[1 : len(host)-1]
}
if port == defaultSSHPort {
return host
}
return "[" + host + "]:" + port
}
// Line returns a line to add append to the known_hosts files.
func Line(addresses []string, key ssh.PublicKey) string {
var trimmed []string
for _, a := range addresses {
trimmed = append(trimmed, Normalize(a))
}
return strings.Join(trimmed, ",") + " " + serialize(key)
}
// HashHostname hashes the given hostname. The hostname is not
// normalized before hashing.
func HashHostname(hostname string) string {
// TODO(hanwen): check if we can safely normalize this always.
salt := make([]byte, sha1.Size)
_, err := rand.Read(salt)
if err != nil {
panic(fmt.Sprintf("crypto/rand failure %v", err))
}
hash := hashHost(hostname, salt)
return encodeHash(sha1HashType, salt, hash)
}
func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) {
if len(encoded) == 0 || encoded[0] != '|' {
err = errors.New("knownhosts: hashed host must start with '|'")
return
}
components := strings.Split(encoded, "|")
if len(components) != 4 {
err = fmt.Errorf("knownhosts: got %d components, want 3", len(components))
return
}
hashType = components[1]
if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil {
return
}
if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil {
return
}
return
}
func encodeHash(typ string, salt []byte, hash []byte) string {
return strings.Join([]string{"",
typ,
base64.StdEncoding.EncodeToString(salt),
base64.StdEncoding.EncodeToString(hash),
}, "|")
}
// See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
func hashHost(hostname string, salt []byte) []byte {
mac := hmac.New(sha1.New, salt)
mac.Write([]byte(hostname))
return mac.Sum(nil)
}
type hashedHost struct {
salt []byte
hash []byte
}
const sha1HashType = "1"
func newHashedHost(encoded string) (*hashedHost, error) {
typ, salt, hash, err := decodeHash(encoded)
if err != nil {
return nil, err
}
// The type field seems for future algorithm agility, but it's
// actually hardcoded in openssh currently, see
// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
if typ != sha1HashType {
return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ)
}
return &hashedHost{salt: salt, hash: hash}, nil
}
func (h *hashedHost) match(a addr) bool {
return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
}

2
vendor/modules.txt vendored
View file

@ -96,7 +96,9 @@ golang.org/x/crypto/curve25519
golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/alias
golang.org/x/crypto/internal/poly1305 golang.org/x/crypto/internal/poly1305
golang.org/x/crypto/ssh golang.org/x/crypto/ssh
golang.org/x/crypto/ssh/agent
golang.org/x/crypto/ssh/internal/bcrypt_pbkdf golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/ssh/knownhosts
# golang.org/x/sys v0.43.0 # golang.org/x/sys v0.43.0
## explicit; go 1.25.0 ## explicit; go 1.25.0
golang.org/x/sys/cpu golang.org/x/sys/cpu