golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/coordinator/remote/ssh.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build linux || darwin
     6  
     7  package remote
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"crypto/ecdsa"
    13  	"crypto/elliptic"
    14  	"crypto/rand"
    15  	"crypto/x509"
    16  	"encoding/pem"
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"log"
    21  	"net"
    22  	"os"
    23  	"os/exec"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"syscall"
    28  	"time"
    29  	"unsafe"
    30  
    31  	"github.com/creack/pty"
    32  	gssh "github.com/gliderlabs/ssh"
    33  	"golang.org/x/build/dashboard"
    34  	"golang.org/x/build/internal/envutil"
    35  	"golang.org/x/crypto/ssh"
    36  )
    37  
    38  // SignPublicSSHKey signs a public SSH key using the certificate authority. These keys are intended for use with the specified gomote and owner.
    39  // The public SSH are intended to be used in OpenSSH certificate authentication with the gomote SSH server.
    40  func SignPublicSSHKey(ctx context.Context, caPriKey ssh.Signer, rawPubKey []byte, sessionID, ownerID string, d time.Duration) ([]byte, error) {
    41  	pubKey, _, _, _, err := ssh.ParseAuthorizedKey(rawPubKey)
    42  	if err != nil {
    43  		return nil, fmt.Errorf("unable to parse public key=%w", err)
    44  	}
    45  	cert := &ssh.Certificate{
    46  		Key:             pubKey,
    47  		Serial:          1,
    48  		CertType:        ssh.UserCert,
    49  		KeyId:           "go_build",
    50  		ValidPrincipals: []string{fmt.Sprintf("%s@farmer.golang.org", sessionID), ownerID},
    51  		ValidAfter:      uint64(time.Now().Unix()),
    52  		ValidBefore:     uint64(time.Now().Add(d).Unix()),
    53  		Permissions: ssh.Permissions{
    54  			Extensions: map[string]string{
    55  				"permit-X11-forwarding":   "",
    56  				"permit-agent-forwarding": "",
    57  				"permit-port-forwarding":  "",
    58  				"permit-pty":              "",
    59  				"permit-user-rc":          "",
    60  			},
    61  		},
    62  	}
    63  	if err := cert.SignCert(rand.Reader, caPriKey); err != nil {
    64  		return nil, fmt.Errorf("cerificate.SignCert() = %w", err)
    65  	}
    66  	mCert := ssh.MarshalAuthorizedKey(cert)
    67  	return mCert, nil
    68  }
    69  
    70  // SSHKeyPair generates a set of ecdsa256 SSH Keys. The public key is serialized for inclusion in
    71  // an OpenSSH authorized_keys file. The private key is PEM encoded.
    72  func SSHKeyPair() (privateKey []byte, publicKey []byte, err error) {
    73  	private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    74  	if err != nil {
    75  		return nil, nil, err
    76  	}
    77  	public, err := ssh.NewPublicKey(&private.PublicKey)
    78  	if err != nil {
    79  		return nil, nil, err
    80  	}
    81  	publicKey = ssh.MarshalAuthorizedKey(public)
    82  	priKeyByt, err := x509.MarshalECPrivateKey(private)
    83  	if err != nil {
    84  		return nil, nil, fmt.Errorf("unable to marshal private key=%w", err)
    85  	}
    86  	privateKey = pem.EncodeToMemory(&pem.Block{
    87  		Type:  "EC PRIVATE KEY",
    88  		Bytes: priKeyByt,
    89  	})
    90  	return
    91  }
    92  
    93  // SSHOption are options to set for the SSH server.
    94  type SSHOption func(*SSHServer)
    95  
    96  // EnableLUCIOption sets the configuration needed for swarming bots to connect to the
    97  // SSH server.
    98  func EnableLUCIOption() SSHOption {
    99  	return func(s *SSHServer) {
   100  		s.server.Handler = s.HandleIncomingSSHPostAuthSwarming
   101  	}
   102  }
   103  
   104  // SSHServer is the SSH server that the coordinator provides.
   105  type SSHServer struct {
   106  	gomotePublicKey    string
   107  	privateHostKeyFile string
   108  	server             *gssh.Server
   109  	sessionPool        *SessionPool
   110  }
   111  
   112  // NewSSHServer creates an SSH server used to access remote buildlet sessions.
   113  func NewSSHServer(addr string, hostPrivateKey, gomotePublicKey, caPrivateKey []byte, sp *SessionPool, opts ...SSHOption) (*SSHServer, error) {
   114  	hostSigner, err := ssh.ParsePrivateKey(hostPrivateKey)
   115  	if err != nil {
   116  		return nil, fmt.Errorf("failed to parse SSH host key: %v; not configuring SSH server", err)
   117  	}
   118  	CASigner, err := ssh.ParsePrivateKey(caPrivateKey)
   119  	if err != nil {
   120  		return nil, fmt.Errorf("failed to parse SSH host key: %v; not configuring SSH server", err)
   121  	}
   122  	privateHostKeyFile, err := WriteSSHPrivateKeyToTempFile(hostPrivateKey)
   123  	if err != nil {
   124  		return nil, fmt.Errorf("error writing ssh private key to temp file: %v; not configuring SSH server", err)
   125  	}
   126  	if len(gomotePublicKey) == 0 {
   127  		return nil, errors.New("invalid gomote public key")
   128  	}
   129  	s := &SSHServer{
   130  		gomotePublicKey:    string(gomotePublicKey),
   131  		privateHostKeyFile: privateHostKeyFile,
   132  		sessionPool:        sp,
   133  		server: &gssh.Server{
   134  			Addr:             addr,
   135  			PublicKeyHandler: handleCertificateAuthFunc(sp, CASigner),
   136  			HostSigners:      []gssh.Signer{hostSigner},
   137  		},
   138  	}
   139  	s.server.Handler = s.HandleIncomingSSHPostAuth
   140  	for _, opt := range opts {
   141  		opt(s)
   142  	}
   143  	return s, nil
   144  }
   145  
   146  // ListenAndServe attempts to start the SSH server. This blocks until the server stops.
   147  func (ss *SSHServer) ListenAndServe() error {
   148  	return ss.server.ListenAndServe()
   149  }
   150  
   151  // Close immediately closes all active listeners and connections.
   152  func (ss *SSHServer) Close() error {
   153  	return ss.server.Close()
   154  }
   155  
   156  // serve attempts to start the SSH server and listens with the passed in net.Listener. This blocks
   157  // until the server stops. This should be used while testing the server.
   158  func (ss *SSHServer) serve(l net.Listener) error {
   159  	return ss.server.Serve(l)
   160  }
   161  
   162  // HandleIncomingSSHPostAuth handles post-authentication requests for the SSH server. This handler uses
   163  // Sessions for session management.
   164  func (ss *SSHServer) HandleIncomingSSHPostAuth(s gssh.Session) {
   165  	inst := s.User()
   166  	ptyReq, winCh, isPty := s.Pty()
   167  	if !isPty {
   168  		fmt.Fprintf(s, "scp etc not yet supported; https://golang.org/issue/21140\n")
   169  		return
   170  	}
   171  	rs, err := ss.sessionPool.Session(inst)
   172  	if err != nil {
   173  		fmt.Fprintf(s, "unknown instance %q", inst)
   174  		return
   175  	}
   176  	hostConf, ok := dashboard.Hosts[rs.HostType]
   177  	if !ok {
   178  		fmt.Fprintf(s, "instance %q has unknown host type %q\n", inst, rs.HostType)
   179  		return
   180  	}
   181  	bconf, ok := dashboard.Builders[rs.BuilderType]
   182  	if !ok {
   183  		fmt.Fprintf(s, "instance %q has unknown builder type %q\n", inst, rs.BuilderType)
   184  		return
   185  	}
   186  
   187  	ctx, cancel := context.WithCancel(s.Context())
   188  	defer cancel()
   189  	if err := ss.sessionPool.KeepAlive(ctx, inst); err != nil {
   190  		log.Printf("ssh: KeepAlive on session=%s failed: %s", inst, err)
   191  	}
   192  
   193  	sshUser := hostConf.SSHUsername
   194  	useLocalSSHProxy := bconf.GOOS() != "plan9"
   195  	if sshUser == "" && useLocalSSHProxy {
   196  		fmt.Fprintf(s, "instance %q host type %q does not have SSH configured\n", inst, rs.HostType)
   197  		return
   198  	}
   199  	if !hostConf.IsHermetic() {
   200  		fmt.Fprintf(s, "WARNING: instance %q host type %q is not currently\n", inst, rs.HostType)
   201  		fmt.Fprintf(s, "configured to have a hermetic filesystem per boot.\n")
   202  		fmt.Fprintf(s, "You must be careful not to modify machine state\n")
   203  		fmt.Fprintf(s, "that will affect future builds.\n")
   204  	}
   205  	log.Printf("connecting to ssh to instance %q ...", inst)
   206  	fmt.Fprint(s, "# Welcome to the gomote ssh proxy.\n")
   207  	fmt.Fprint(s, "# Connecting to/starting remote ssh...\n")
   208  	fmt.Fprint(s, "#\n")
   209  
   210  	var localProxyPort int
   211  	bc, err := ss.sessionPool.BuildletClient(inst)
   212  	if err != nil {
   213  		fmt.Fprintf(s, "failed to connect to ssh on %s: %v\n", inst, err)
   214  		return
   215  	}
   216  	if useLocalSSHProxy {
   217  		sshConn, err := bc.ConnectSSH(sshUser, ss.gomotePublicKey)
   218  		log.Printf("buildlet(%q).ConnectSSH = %T, %v", inst, sshConn, err)
   219  		if err != nil {
   220  			fmt.Fprintf(s, "failed to connect to ssh on %s: %v\n", inst, err)
   221  			return
   222  		}
   223  		defer sshConn.Close()
   224  
   225  		// Now listen on some localhost port that we'll proxy to sshConn.
   226  		// The openssh ssh command line tool will connect to this IP.
   227  		ln, err := net.Listen("tcp", "localhost:0")
   228  		if err != nil {
   229  			fmt.Fprintf(s, "local listen error: %v\n", err)
   230  			return
   231  		}
   232  		localProxyPort = ln.Addr().(*net.TCPAddr).Port
   233  		log.Printf("ssh local proxy port for %s: %v", inst, localProxyPort)
   234  		var lnCloseOnce sync.Once
   235  		lnClose := func() { lnCloseOnce.Do(func() { ln.Close() }) }
   236  		defer lnClose()
   237  
   238  		// Accept at most one connection from localProxyPort and proxy
   239  		// it to sshConn.
   240  		go func() {
   241  			c, err := ln.Accept()
   242  			lnClose()
   243  			if err != nil {
   244  				return
   245  			}
   246  			defer c.Close()
   247  			errc := make(chan error, 1)
   248  			go func() {
   249  				_, err := io.Copy(c, sshConn)
   250  				errc <- err
   251  			}()
   252  			go func() {
   253  				_, err := io.Copy(sshConn, c)
   254  				errc <- err
   255  			}()
   256  			err = <-errc
   257  		}()
   258  	}
   259  	workDir, err := bc.WorkDir(ctx)
   260  	if err != nil {
   261  		fmt.Fprintf(s, "Error getting WorkDir: %v\n", err)
   262  		return
   263  	}
   264  	ip, _, ipErr := net.SplitHostPort(bc.IPPort())
   265  
   266  	fmt.Fprint(s, "# `gomote push` and the builders use:\n")
   267  	fmt.Fprintf(s, "# - workdir: %s\n", workDir)
   268  	fmt.Fprintf(s, "# - GOROOT: %s/go\n", workDir)
   269  	fmt.Fprintf(s, "# - GOPATH: %s/gopath\n", workDir)
   270  	fmt.Fprintf(s, "# - env: %s\n", strings.Join(bconf.Env(), " ")) // TODO: shell quote?
   271  	fmt.Fprint(s, "# Happy debugging.\n")
   272  
   273  	log.Printf("ssh to %s: starting ssh -p %d for %s@localhost", inst, localProxyPort, sshUser)
   274  	var cmd *exec.Cmd
   275  	switch bconf.GOOS() {
   276  	default:
   277  		cmd = exec.Command("ssh",
   278  			"-p", strconv.Itoa(localProxyPort),
   279  			"-o", "UserKnownHostsFile=/dev/null",
   280  			"-o", "StrictHostKeyChecking=no",
   281  			"-i", ss.privateHostKeyFile,
   282  			sshUser+"@localhost")
   283  	case "plan9":
   284  		fmt.Fprintf(s, "# Plan9 user/pass: glenda/glenda123\n")
   285  		if ipErr != nil {
   286  			fmt.Fprintf(s, "# Failed to get IP out of %q: %v\n", bc.IPPort(), ipErr)
   287  			return
   288  		}
   289  		cmd = exec.Command("/usr/local/bin/drawterm",
   290  			"-a", ip, "-c", ip, "-u", "glenda", "-k", "user=glenda")
   291  	}
   292  	envutil.SetEnv(cmd, "TERM="+ptyReq.Term)
   293  	f, err := pty.Start(cmd)
   294  	if err != nil {
   295  		log.Printf("running ssh client to %s: %v", inst, err)
   296  		return
   297  	}
   298  	defer f.Close()
   299  	go func() {
   300  		for win := range winCh {
   301  			setWinsize(f, win.Width, win.Height)
   302  		}
   303  	}()
   304  	go func() {
   305  		ss.setupRemoteSSHEnv(bconf, workDir, f)
   306  		io.Copy(f, s) // stdin
   307  	}()
   308  	io.Copy(s, f) // stdout
   309  	cmd.Process.Kill()
   310  	cmd.Wait()
   311  }
   312  
   313  // HandleIncomingSSHPostAuthSwarming handles post-authentication requests for the SSH server. This handler uses
   314  // Sessions for session management.
   315  func (ss *SSHServer) HandleIncomingSSHPostAuthSwarming(s gssh.Session) {
   316  	inst := s.User()
   317  	ptyReq, winCh, isPty := s.Pty()
   318  	if !isPty {
   319  		fmt.Fprintf(s, "scp etc not yet supported; https://go.dev/issue/21140\n")
   320  		return
   321  	}
   322  	rs, err := ss.sessionPool.Session(inst)
   323  	if err != nil {
   324  		fmt.Fprintf(s, "unknown instance %q", inst)
   325  		return
   326  	}
   327  	ctx, cancel := context.WithCancel(s.Context())
   328  	defer cancel()
   329  	if err := ss.sessionPool.KeepAlive(ctx, inst); err != nil {
   330  		log.Printf("ssh: KeepAlive on session=%s failed: %s", inst, err)
   331  	}
   332  
   333  	sshUser := "swarming"
   334  	isPlan9 := strings.Contains(rs.HostType, "plan9")
   335  	useLocalSSHProxy := !isPlan9
   336  	if sshUser == "" && useLocalSSHProxy {
   337  		fmt.Fprintf(s, "instance %q host type %q does not have SSH configured\n", inst, rs.HostType)
   338  		return
   339  	}
   340  	// TODO(go.dev/issue/64064) do we still need hermetic checks?
   341  	log.Printf("connecting to ssh to instance %q ...", inst)
   342  	fmt.Fprint(s, "# Welcome to the gomote ssh proxy.\n")
   343  	fmt.Fprint(s, "# Connecting to/starting remote ssh...\n")
   344  	fmt.Fprint(s, "#\n")
   345  
   346  	var localProxyPort int
   347  	bc, err := ss.sessionPool.BuildletClient(inst)
   348  	if err != nil {
   349  		fmt.Fprintf(s, "failed to connect to ssh on %s: %v\n", inst, err)
   350  		return
   351  	}
   352  	if useLocalSSHProxy {
   353  		sshConn, err := bc.ConnectSSH(sshUser, ss.gomotePublicKey)
   354  		log.Printf("buildlet(%q).ConnectSSH = %T, %v", inst, sshConn, err)
   355  		if err != nil {
   356  			fmt.Fprintf(s, "failed to connect to ssh on %s: %v\n", inst, err)
   357  			return
   358  		}
   359  		defer sshConn.Close()
   360  
   361  		// Now listen on some localhost port that we'll proxy to sshConn.
   362  		// The openssh ssh command line tool will connect to this IP.
   363  		ln, err := net.Listen("tcp", "localhost:0")
   364  		if err != nil {
   365  			fmt.Fprintf(s, "local listen error: %v\n", err)
   366  			return
   367  		}
   368  		localProxyPort = ln.Addr().(*net.TCPAddr).Port
   369  		log.Printf("ssh local proxy port for %s: %v", inst, localProxyPort)
   370  		var lnCloseOnce sync.Once
   371  		lnClose := func() { lnCloseOnce.Do(func() { ln.Close() }) }
   372  		defer lnClose()
   373  
   374  		// Accept at most one connection from localProxyPort and proxy
   375  		// it to sshConn.
   376  		go func() {
   377  			c, err := ln.Accept()
   378  			lnClose()
   379  			if err != nil {
   380  				return
   381  			}
   382  			defer c.Close()
   383  			errc := make(chan error, 1)
   384  			go func() {
   385  				_, err := io.Copy(c, sshConn)
   386  				errc <- err
   387  			}()
   388  			go func() {
   389  				_, err := io.Copy(sshConn, c)
   390  				errc <- err
   391  			}()
   392  			err = <-errc
   393  		}()
   394  	}
   395  	workDir, err := bc.WorkDir(ctx)
   396  	if err != nil {
   397  		fmt.Fprintf(s, "Error getting WorkDir: %v\n", err)
   398  		return
   399  	}
   400  	ip, _, ipErr := net.SplitHostPort(bc.IPPort())
   401  
   402  	fmt.Fprint(s, "# `gomote push` and the builders use:\n")
   403  	fmt.Fprintf(s, "# - workdir: %s\n", workDir)
   404  	fmt.Fprintf(s, "# - GOROOT: %s/go\n", workDir)
   405  	fmt.Fprintf(s, "# - GOPATH: %s/gopath\n", workDir)
   406  	fmt.Fprint(s, "# Happy debugging.\n")
   407  
   408  	log.Printf("ssh to %s: starting ssh -p %d for %s@localhost", inst, localProxyPort, sshUser)
   409  	cmd := exec.Command("ssh",
   410  		"-p", strconv.Itoa(localProxyPort),
   411  		"-o", "UserKnownHostsFile=/dev/null",
   412  		"-o", "StrictHostKeyChecking=no",
   413  		"-i", ss.privateHostKeyFile,
   414  		sshUser+"@localhost")
   415  	if isPlan9 {
   416  		fmt.Fprintf(s, "# Plan9 user/pass: glenda/glenda123\n")
   417  		if ipErr != nil {
   418  			fmt.Fprintf(s, "# Failed to get IP out of %q: %v\n", bc.IPPort(), ipErr)
   419  			return
   420  		}
   421  		cmd = exec.Command("/usr/local/bin/drawterm",
   422  			"-a", ip, "-c", ip, "-u", "glenda", "-k", "user=glenda")
   423  	}
   424  
   425  	envutil.SetEnv(cmd, "TERM="+ptyReq.Term)
   426  	f, err := pty.Start(cmd)
   427  	if err != nil {
   428  		log.Printf("running ssh client to %s: %v", inst, err)
   429  		return
   430  	}
   431  	defer f.Close()
   432  	go func() {
   433  		for win := range winCh {
   434  			setWinsize(f, win.Width, win.Height)
   435  		}
   436  	}()
   437  	go func() {
   438  		ss.setupRemoteSSHEnvSwarm(rs.BuilderType, workDir, f)
   439  		io.Copy(f, s) // stdin
   440  	}()
   441  	io.Copy(s, f) // stdout
   442  	cmd.Process.Kill()
   443  	cmd.Wait()
   444  }
   445  
   446  // setupRemoteSSHEnvSwarm prints environmental details to the writer.
   447  // This makes the new SSH session easier to use for Go testing.
   448  func (ss *SSHServer) setupRemoteSSHEnvSwarm(builderType, workDir string, f io.Writer) {
   449  	if strings.Contains(builderType, "windows") {
   450  		// TODO(65826) find a universal way of setting the working directory.
   451  		fmt.Fprintf(f, `cd %s`+"\r\n", workDir)
   452  		return
   453  	}
   454  	fmt.Fprintf(f, "cd %s\n", workDir)
   455  }
   456  
   457  // setupRemoteSSHEnv sets up environment variables on the remote system.
   458  // This makes the new SSH session easier to use for Go testing.
   459  func (ss *SSHServer) setupRemoteSSHEnv(bconf *dashboard.BuildConfig, workDir string, f io.Writer) {
   460  	switch bconf.GOOS() {
   461  	default:
   462  		// A Unix system.
   463  		for _, env := range bconf.Env() {
   464  			fmt.Fprintln(f, env)
   465  			if idx := strings.Index(env, "="); idx > 0 {
   466  				fmt.Fprintf(f, "export %s\n", env[:idx])
   467  			}
   468  		}
   469  		fmt.Fprintf(f, "GOPATH=%s/gopath\n", workDir)
   470  		fmt.Fprintf(f, "PATH=$PATH:%s/go/bin\n", workDir)
   471  		fmt.Fprintf(f, "export GOPATH PATH\n")
   472  		fmt.Fprintf(f, "cd %s/go/src\n", workDir)
   473  	case "windows":
   474  		for _, env := range bconf.Env() {
   475  			fmt.Fprintf(f, "set %s\n", env)
   476  		}
   477  		fmt.Fprintf(f, `set GOPATH=%s\gopath`+"\n", workDir)
   478  		fmt.Fprintf(f, `set PATH=%%PATH%%;%s\go\bin`+"\n", workDir)
   479  		fmt.Fprintf(f, `cd %s\go\src`+"\n", workDir)
   480  	case "plan9":
   481  		// TODO
   482  	}
   483  }
   484  
   485  // WriteSSHPrivateKeyToTempFile writes a key to a temporary file on the local file system. It also
   486  // sets the permissions on the file to what is expected by OpenSSH implementations of SSH.
   487  func WriteSSHPrivateKeyToTempFile(key []byte) (path string, err error) {
   488  	tf, err := os.CreateTemp("", "ssh-priv-key")
   489  	if err != nil {
   490  		return "", err
   491  	}
   492  	if err := tf.Chmod(0600); err != nil {
   493  		return "", err
   494  	}
   495  	if _, err := tf.Write(key); err != nil {
   496  		return "", err
   497  	}
   498  	return tf.Name(), tf.Close()
   499  }
   500  
   501  // handleCertificateAuthFunc creates a function that authenticates the session using OpenSSH certificate
   502  // authentication. The passed in certificate is tested to ensure it is valid, signed by the CA and
   503  // corresponds to an existing session.
   504  func handleCertificateAuthFunc(sp *SessionPool, caKeySigner ssh.Signer) gssh.PublicKeyHandler {
   505  	return func(ctx gssh.Context, key gssh.PublicKey) bool {
   506  		sessionID := ctx.User()
   507  		cert, ok := key.(*ssh.Certificate)
   508  		if !ok {
   509  			log.Printf("public key is not a certificate session=%s", sessionID)
   510  			return false
   511  		}
   512  		if cert.CertType != ssh.UserCert {
   513  			log.Printf("certificate not user cert session=%s", sessionID)
   514  			return false
   515  		}
   516  		if !bytes.Equal(cert.SignatureKey.Marshal(), caKeySigner.PublicKey().Marshal()) {
   517  			log.Printf("certificate is not signed by recognized Certificate Authority session=%s", sessionID)
   518  			return false
   519  		}
   520  
   521  		ses, err := sp.Session(sessionID)
   522  		if err != nil {
   523  			log.Printf("HandleCertificateAuth: unable to retrieve session=%s: %s", sessionID, err)
   524  			return false
   525  		}
   526  		certChecker := &ssh.CertChecker{}
   527  		wantPrincipal := fmt.Sprintf("%s@farmer.golang.org", sessionID)
   528  		if err := certChecker.CheckCert(wantPrincipal, cert); err != nil {
   529  			log.Printf("certChecker.CheckCert(%s, user_certificate) = %s", wantPrincipal, err)
   530  			return false
   531  		}
   532  		for _, principal := range cert.ValidPrincipals {
   533  			if principal == ses.OwnerID {
   534  				return true
   535  			}
   536  		}
   537  		log.Printf("HandleCertificateAuth: unable to verify ownerID in certificate principals")
   538  		return false
   539  	}
   540  }
   541  
   542  // authorizedKey is a Github user's SSH authorized key, in both string and parsed format.
   543  type authorizedKey struct {
   544  	AuthorizedLine string // e.g. "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILj8HGIG9NsT34PHxO8IBq0riSBv7snp30JM8AanBGoV"
   545  	PublicKey      ssh.PublicKey
   546  }
   547  
   548  func setWinsize(f *os.File, w, h int) {
   549  	syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
   550  		uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
   551  }