Skip to content

Commit

Permalink
[v13] fix forwarding a SSH agent in a Cygwin environment (#30582)
Browse files Browse the repository at this point in the history
* fix forwarding a SSH agent in a Cygwin env

* added more comments

* use os.Getenv

* add examples to regex vars, make formatting of guid more clear

* parse strings as uints, improve error messages

* make it more clear uint32s are used for UIDs

* turns out calling "bash" isn't needed
  • Loading branch information
capnspacehook committed Aug 16, 2023
1 parent cb26396 commit 39edf3e
Show file tree
Hide file tree
Showing 2 changed files with 222 additions and 2 deletions.
2 changes: 1 addition & 1 deletion lib/client/api.go
Expand Up @@ -4304,7 +4304,7 @@ func connectToSSHAgent() agent.ExtendedAgent {
socketPath := os.Getenv(teleport.SSHAuthSock)
conn, err := agentconn.Dial(socketPath)
if err != nil {
log.Errorf("[KEY AGENT] Unable to connect to SSH agent on socket: %q.", socketPath)
log.WithError(err).Errorf("[KEY AGENT] Unable to connect to SSH agent on socket: %q.", socketPath)
return nil
}

Expand Down
222 changes: 221 additions & 1 deletion lib/utils/agentconn/agent_windows.go
Expand Up @@ -20,20 +20,240 @@ limitations under the License.
package agentconn

import (
"encoding/binary"
"encoding/hex"
"net"
"os"
"os/exec"
"regexp"
"strconv"
"strings"

"github.com/Microsoft/go-winio"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"

apiutils "github.com/gravitational/teleport/api/utils"
)

const namedPipe = `\\.\pipe\openssh-ssh-agent`

// Dial creates net.Conn to a SSH agent listening on a Windows named pipe.
// This is behind a build flag because winio.DialPipe is only available on
// Windows.
// Windows. If connecting to a named pipe fails and we're in a Cygwin
// environment, a connection to a Cygwin SSH agent will be attempted.
func Dial(socket string) (net.Conn, error) {
conn, err := winio.DialPipe(namedPipe, nil)
if err != nil {
// MSYSTEM is used to specify what Cygwin environment is used;
// if it exists, there's a very good chance we're in a Cygwin
// environment
if msys := os.Getenv("MSYSTEM"); msys != "" {
conn, err := dialCygwin(socket)
if err != nil {
return nil, trace.Wrap(err)
}
return conn, nil
}

return nil, trace.Wrap(err)
}

return conn, nil
}

// SIDs that are computer or domain SIDs start with this prefix.
const wellKnownSIDPrefix = "S-1-5-"

var (
// Format of the contents of a file created by Cygwin 'ssh-agent'.
// After '!<socket >', the listening port is specified, followed by
// an optional 's ' that is sometimes set depending on the implementation,
// ending with a GUID which is used as a shared secret when handshaking
// with the SSH agent.
// example:
// !<socket >51463 s 043B28B0-30D7E90E-027C556A-314067F9
cygwinSocket = regexp.MustCompile(`!<socket >(\d+) (s )?([A-Fa-f0-9-]+)`)
// format of an output line from Cygwin 'ps'
// example:
// PID PPID PGID WINPID TTY UID STIME COMMAND
// 1634 1540 1634 7356 ? 197608 14:31:52 /usr/bin/ps
psLine = regexp.MustCompile(`(?m)^\s+\d+\s+\d+\s+\d+\s+\d+\s+\?\s+(\d+)`)
)

// attempt to connect a Cygwin SSH agent socket. Some code adapted from
// https://github.com/abourget/secrets-bridge/blob/master/pkg/agentfwd/agentconn_windows.go
func dialCygwin(socket string) (net.Conn, error) {
// the "socket" is actually a file Cygwin uses to communicate what the
// actual socket is and the parameters for the handshake
contents, err := os.ReadFile(socket)
if err != nil {
return nil, trace.Wrap(err)
}

sockMatches := cygwinSocket.FindStringSubmatch(string(contents))
if len(sockMatches) != 4 {
return nil, trace.Errorf("could not find necessary information in Cygwin socket file")
}
port := sockMatches[1]
if sockMatches[2] != "s " {
return nil, trace.NotImplemented("dialing mysysgit ssh-agent sockets is not supported")
}
key := sockMatches[3]

u, err := apiutils.CurrentUser()
if err != nil {
return nil, trace.Wrap(err)
}

var uid uint32
var unsureOfUID bool
if !strings.HasPrefix(u.Uid, wellKnownSIDPrefix) {
// the format of the SID isn't supported, fallback to getting
// the UID from 'ps'
uid, err = getCygwinUIDFromPS()
if err != nil {
return nil, trace.Wrap(err)
}
} else {
// Attempt to get a Cygwin UID from a Windows SID. Details of
// UID -> SID mapping here: https://cygwin.com/cygwin-ug-net/ntsec.html
sidParts := strings.Split(u.Uid, "-")
if len(sidParts) == 4 {
// well-known SIDs in the NT_AUTHORITY domain of the S-1-5-RID type
u, err := strconv.ParseUint(sidParts[3], 10, 32)
if err != nil {
return nil, trace.Wrap(err)
}
uid = uint32(u)
} else if len(sidParts) == 5 {
// other well-known SIDs that aren't groups
x, err := strconv.ParseUint(sidParts[3], 10, 32)
if err != nil {
return nil, trace.Wrap(err)
}
rid, err := strconv.ParseUint(sidParts[4], 10, 32)
if err != nil {
return nil, trace.Wrap(err)
}
uid = uint32(0x1000*x + rid)
} else if len(sidParts) == 8 {
// SIDs from the local machine's SAM, the machine's primary
// domain, or a trusted domain of the machine's primary domain
u, err := strconv.ParseUint(sidParts[7], 10, 32)
if err != nil {
return nil, trace.Wrap(err)
}
uid = uint32(u)
unsureOfUID = true
} else {
// the format of the SID isn't supported, fallback to getting
// the UID from 'ps'
uid, err = getCygwinUIDFromPS()
if err != nil {
return nil, trace.Wrap(err)
}
}
}

// dial socket and complete handshake
var conn net.Conn
if !unsureOfUID {
// we're confident in what the Cygwin UID is, only make one attempt
// at establishing a connection
conn, err = attemptCygwinHandshake(port, key, uid)
if err == nil {
return conn, nil
}
} else {
// the Cygwin UID could be built a few different ways; attempt
// with all UIDs until one succeeds
cygwinRIDNums := []uint32{0x30000, 0x100000, 0x80000000}
for _, num := range cygwinRIDNums {
conn, err = attemptCygwinHandshake(port, key, num+uid)
if err == nil {
return conn, nil
}
}

// none of those UIDs worked, fallback to getting UID from 'ps'
uid, err = getCygwinUIDFromPS()
if err != nil {
return nil, trace.Wrap(err)
}
conn, err = attemptCygwinHandshake(port, key, uid)
if err != nil {
return nil, trace.Wrap(err)
}
}

return conn, nil
}

// use Cygwin 'ps' binary to get the Cygwin UID of the current user
func getCygwinUIDFromPS() (uint32, error) {
psOutput, err := exec.Command("ps").Output()
if err != nil {
return 0, trace.Wrap(err)
}
psMatches := psLine.FindStringSubmatch(string(psOutput))
if len(psMatches) != 2 {
return 0, trace.Errorf("UID not found in Cygwin ps output")
}
uid, err := strconv.ParseUint(psMatches[1], 10, 32)
if err != nil {
return 0, trace.Wrap(err)
}

return uint32(uid), nil
}

// connect to a listening socket of a Cygwin SSH agent and attempt to
// preform a successful handshake with it. Handshake details here:
// https://stackoverflow.com/questions/23086038/what-mechanism-is-used-by-msys-cygwin-to-emulate-unix-domain-sockets
func attemptCygwinHandshake(port, key string, uid uint32) (net.Conn, error) {
logrus.Debugf("[KEY AGENT] attempting a handshake with Cygwin ssh-agent socket; port=%s uid=%d", port, uid)

conn, err := net.Dial("tcp", "localhost:"+port)
if err != nil {
return nil, trace.Wrap(err)
}

// 1. send hex-decoded GUID in little endian
keyBuf := make([]byte, 0, 16)
dst := make([]byte, 4)
// handle each part of the GUID in order
for i := 8; i <= len(key); i += 9 {
_, err := hex.Decode(dst, []byte(key)[i-8:i])
if err != nil {
return nil, trace.Wrap(err)
}
dst[0], dst[1], dst[2], dst[3] = dst[3], dst[2], dst[1], dst[0]
keyBuf = append(keyBuf, dst...)
}

if _, err = conn.Write(keyBuf); err != nil {
return nil, trace.Wrap(err)
}

// 2. server echoes the same bytes, read them
if _, err = conn.Read(keyBuf); err != nil {
return nil, trace.Wrap(err)
}

// 3. send PID, Cygwin UID and Cygwin GID of the calling process
pidsUids := make([]byte, 12)
pid := os.Getpid()
gid := pid // for cygwin's AF_UNIX -> AF_INET, pid = gid
binary.LittleEndian.PutUint32(pidsUids, uint32(pid))
binary.LittleEndian.PutUint32(pidsUids[4:], uid)
binary.LittleEndian.PutUint32(pidsUids[8:], uint32(gid))
if _, err = conn.Write(pidsUids); err != nil {
return nil, trace.Wrap(err)
}

// 4. server echoes the same bytes, read them
if _, err = conn.Read(pidsUids); err != nil {
return nil, trace.Wrap(err)
}

Expand Down

0 comments on commit 39edf3e

Please sign in to comment.