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:
parent
3cf9582147
commit
c15ade247a
16 changed files with 2580 additions and 65 deletions
8
go.mod
8
go.mod
|
|
@ -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
20
go.sum
|
|
@ -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=
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
|
|
@ -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 == "" {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
26
internal/homedir/homedir.go
Normal file
26
internal/homedir/homedir.go
Normal 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 ""
|
||||||
|
}
|
||||||
|
|
@ -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
856
vendor/golang.org/x/crypto/ssh/agent/client.go
generated
vendored
Normal 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
103
vendor/golang.org/x/crypto/ssh/agent/forward.go
generated
vendored
Normal 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
250
vendor/golang.org/x/crypto/ssh/agent/keyring.go
generated
vendored
Normal 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
573
vendor/golang.org/x/crypto/ssh/agent/server.go
generated
vendored
Normal 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
532
vendor/golang.org/x/crypto/ssh/knownhosts/knownhosts.go
generated
vendored
Normal 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
2
vendor/modules.txt
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue