diff --git a/go.mod b/go.mod index d5bd69a..2ae4f6a 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ replace apc-p15-tool/cmd/install_only => /cmd/install_only replace apc-p15-tool/cmd/tool => /cmd/tool +replace apc-p15-tool/pkg/apcssh => /pkg/apcssh + replace apc-p15-tool/pkg/app => /pkg/app replace apc-p15-tool/pkg/pkcs15 => /pkg/pkcs15 diff --git a/pkg/apcssh/client.go b/pkg/apcssh/client.go new file mode 100644 index 0000000..33b01d5 --- /dev/null +++ b/pkg/apcssh/client.go @@ -0,0 +1,134 @@ +package apcssh + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "log" + "net" + "runtime" + "strings" + "time" + + "golang.org/x/crypto/ssh" +) + +const ( + apcSSHVer = 1 + + sshTimeout = 90 * time.Second +) + +// APC UPS won't except Go's SSH "Run()" command as the format isn't quite +// the same. Therefore, write a custom implementation instead of relying on +// something like github.com/bramvdbogaerde/go-scp + +type Config struct { + Hostname string + Username string + Password string + ServerFingerprint string + InsecureCipher bool +} + +// Client is an APC UPS SSH client +type Client struct { + hostname string + sshCfg *ssh.ClientConfig +} + +// New creates a new SSH Client for the APC UPS. +func New(cfg *Config) (*Client, error) { + // make host key callback + hk := func(_hostname string, _remote net.Addr, key ssh.PublicKey) error { + // calculate server's key's SHA256 + hasher := sha256.New() + _, err := hasher.Write(key.Marshal()) + if err != nil { + return err + } + actualHash := hasher.Sum(nil) + + // log fingerprint for debugging + actualHashB64 := base64.RawStdEncoding.EncodeToString(actualHash) + actualHashHex := hex.EncodeToString(actualHash) + + // check for fingerprint match (b64 or hex) + if actualHashB64 != cfg.ServerFingerprint && actualHashHex != cfg.ServerFingerprint { + log.Printf("apcssh: remote server key fingerprint (b64): %s", actualHashB64) + log.Printf("apcssh: remote server key fingerprint (hex): %s", actualHashHex) + + return errors.New("apcssh: fingerprint didn't match") + } + + return nil + } + + // kex algos + // see defaults: https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.18.0:ssh/common.go;l=62 + kexAlgos := []string{ + "curve25519-sha256", "curve25519-sha256@libssh.org", + "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", + "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1", + } + // extra for some apc ups + kexAlgos = append(kexAlgos, "diffie-hellman-group-exchange-sha256") + + // ciphers + // see defaults: https://cs.opensource.google/go/x/crypto/+/master:ssh/common.go;l=37 + ciphers := []string{ + "aes128-gcm@openssh.com", "aes256-gcm@openssh.com", + "chacha20-poly1305@openssh.com", + "aes128-ctr", "aes192-ctr", "aes256-ctr", + } + + // insecure cipher options? + if cfg.InsecureCipher { + log.Println("WARNING: insecure ciphers are enabled (--insecurecipher). SSH with an insecure cipher is NOT secure and should NOT be used.") + ciphers = append(ciphers, "aes128-cbc", "3des-cbc") + } + + // install file on UPS + // ssh config + config := &ssh.ClientConfig{ + User: cfg.Username, + Auth: []ssh.AuthMethod{ + ssh.Password(cfg.Password), + }, + // APC seems to require `Client Version` string to start with "SSH-2" and must be at least + // 13 characters long + // working examples from other clients: + // ClientVersion: "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6", + // ClientVersion: "SSH-2.0-PuTTY_Release_0.80", + ClientVersion: fmt.Sprintf("SSH-2.0-apcssh_v%d %s-%s", apcSSHVer, runtime.GOOS, runtime.GOARCH), + Config: ssh.Config{ + KeyExchanges: kexAlgos, + Ciphers: ciphers, + }, + HostKeyCallback: hk, + + // reasonable timeout for file copy + Timeout: sshTimeout, + } + + // if hostname missing a port, add default + if !strings.Contains(cfg.Hostname, ":") { + cfg.Hostname = cfg.Hostname + ":22" + } + + // connect to ups over SSH (to verify everything works) + sshClient, err := ssh.Dial("tcp", cfg.Hostname, config) + if err != nil { + return nil, err + } + _ = sshClient.Close() + + // return Client (note: new ssh Dial will be done for each action as the UPS + // seems to not do well with more than one Session per Dial) + return &Client{ + hostname: cfg.Hostname, + sshCfg: config, + }, nil +} diff --git a/pkg/apcssh/cmd_restartwebui.go b/pkg/apcssh/cmd_restartwebui.go new file mode 100644 index 0000000..4b904c1 --- /dev/null +++ b/pkg/apcssh/cmd_restartwebui.go @@ -0,0 +1,24 @@ +package apcssh + +import ( + "fmt" + "strings" +) + +// RestartWebUI sends the APC command to restart the web ui +// WARNING: Sending a command directly after this one will cause issues. +// This command will cause SSH to also restart after a slight delay, therefore +// any command right after this will start to run but then get stuck / fail +// somewhere in the middle. +func (cli *Client) RestartWebUI() error { + result, err := cli.cmd("reboot -Y") + if err != nil { + return err + } + + if strings.ToLower(result.code) != "e000" { + return fmt.Errorf("apcssh: failed to restart web ui (%s: %s)", result.code, result.codeText) + } + + return nil +} diff --git a/pkg/apcssh/scp.go b/pkg/apcssh/scp.go new file mode 100644 index 0000000..9db77c0 --- /dev/null +++ b/pkg/apcssh/scp.go @@ -0,0 +1,129 @@ +package apcssh + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "path" + + "golang.org/x/crypto/ssh" +) + +// UploadSCP uploads a file to the destination specified (e.g., "/ssl/file.key") +// containing the file content specified. An existing file at the destination +// will be overwritten without warning. +func (cli *Client) UploadSCP(destination string, fileContent []byte, filePermissions fs.FileMode) error { + // connect + sshClient, err := ssh.Dial("tcp", cli.hostname, cli.sshCfg) + if err != nil { + return fmt.Errorf("apcssh: scp: failed to dial session (%w)", err) + } + defer sshClient.Close() + + // make session to use for SCP + session, err := sshClient.NewSession() + if err != nil { + return fmt.Errorf("apcssh: scp: failed to create session (%w)", err) + } + defer session.Close() + + // attach pipes + out, err := session.StdoutPipe() + if err != nil { + return err + } + w, err := session.StdinPipe() + if err != nil { + return err + } + defer w.Close() + + // send execute cmd -- + // build cmd to send as request + // Go implementation sends additional 0x22 bytes when using Run() (as + // compared to putty's scp tool). these additional bytes seem to cause the + // apc ups to fail execution of the command + payload := []byte(fmt.Sprintf("scp -q -t %s", destination)) + payloadLen := uint8(len(payload)) + payload = append([]byte{0, 0, 0, payloadLen}, payload...) + + ok, err := session.SendRequest("exec", true, payload) + if err != nil { + return fmt.Errorf("apcssh: scp: failed to execute scp cmd (%w)", err) + } + if !ok { + return errors.New("apcssh: scp: execute scp cmd not ok") + } + + // check remote response + // Note: File upload may not work if the client doesn't actually read from + // the remote output. + err = scpCheckResponse(out) + if err != nil { + return fmt.Errorf("apcssh: scp: failed to send scp cmd (bad remote response 1) (%w)", err) + } + + // just file name (without path) + filename := path.Base(destination) + + // send file header + _, err = fmt.Fprintln(w, "C"+fmt.Sprintf("%04o", filePermissions.Perm()), len(fileContent), filename) + if err != nil { + return fmt.Errorf("apcssh: scp: failed to send file info (%w)", err) + } + + err = scpCheckResponse(out) + if err != nil { + return fmt.Errorf("apcssh: scp: failed to send file info (bad remote response 2) (%w)", err) + } + + // send actual file + _, err = io.Copy(w, bytes.NewReader(fileContent)) + if err != nil { + return fmt.Errorf("apcssh: scp: failed to send file(%w)", err) + } + + // send file end + _, err = fmt.Fprint(w, "\x00") + if err != nil { + return fmt.Errorf("apcssh: scp: failed to send final 00 byte (%w)", err) + } + + err = scpCheckResponse(out) + if err != nil { + return fmt.Errorf("apcssh: scp: failed to send file (bad remote response 3) (%w)", err) + } + + // done + return nil +} + +// scpCheckResponse reads the output from the remote and returns an error +// if the remote output was not 0 +func scpCheckResponse(remoteOutPipe io.Reader) error { + buffer := make([]uint8, 1) + _, err := remoteOutPipe.Read(buffer) + if err != nil { + return fmt.Errorf("apcssh: failed to read output buffer (%w)", err) + } + + responseType := buffer[0] + message := "" + if responseType > 0 { + bufferedReader := bufio.NewReader(remoteOutPipe) + message, err = bufferedReader.ReadString('\n') + if err != nil { + return fmt.Errorf("apcssh: failed to read output buffer (%w)", err) + } + } + + // if not 0 (aka OK) + if responseType != 0 { + return fmt.Errorf("apcssh: remote returned error (%d: %s)", responseType, message) + } + + return nil +} diff --git a/pkg/apcssh/shell.go b/pkg/apcssh/shell.go new file mode 100644 index 0000000..c6f082c --- /dev/null +++ b/pkg/apcssh/shell.go @@ -0,0 +1,88 @@ +package apcssh + +import ( + "bufio" + "fmt" + "strings" + + "golang.org/x/crypto/ssh" +) + +// upsCmdResponse is a structure that holds all of a shell commands results +type upsCmdResponse struct { + command string + code string + codeText string + resultText string +} + +// cmd creates an interactive shell and executes the specified command +func (cli *Client) cmd(command string) (*upsCmdResponse, error) { + // connect + sshClient, err := ssh.Dial("tcp", cli.hostname, cli.sshCfg) + if err != nil { + return nil, fmt.Errorf("apcssh: failed to dial session (%w)", err) + } + defer sshClient.Close() + + session, err := sshClient.NewSession() + if err != nil { + return nil, fmt.Errorf("apcssh: failed to create session (%w)", err) + } + defer session.Close() + + // pipes to send shell command to; and to receive repsonse + sshInput, err := session.StdinPipe() + if err != nil { + return nil, fmt.Errorf("apcssh: failed to make shell input pipe (%w)", err) + } + sshOutput, err := session.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("apcssh: failed to make shell output pipe (%w)", err) + } + + // make scanner to read shell continuously + scanner := bufio.NewScanner(sshOutput) + scanner.Split(scanAPCShell) + + // start interactive shell + if err := session.Shell(); err != nil { + return nil, fmt.Errorf("apcssh: failed to start shell (%w)", err) + } + // discard the initial shell response (login message(s) / initial shell prompt) + for { + if token := scanner.Scan(); token { + _ = scanner.Bytes() + break + } + } + + // send command + _, err = fmt.Fprint(sshInput, command+"\n") + if err != nil { + return nil, fmt.Errorf("apcssh: failed to send shell command (%w)", err) + } + + res := &upsCmdResponse{} + for { + if tkn := scanner.Scan(); tkn { + result := string(scanner.Bytes()) + + cmdIndx := strings.Index(result, "\n") + res.command = result[:cmdIndx-1] + result = result[cmdIndx+1:] + + codeIndx := strings.Index(result, ": ") + res.code = result[:codeIndx] + result = result[codeIndx+2:] + + codeTxtIndx := strings.Index(result, "\n") + res.codeText = result[:codeTxtIndx-1] + + res.resultText = result[codeTxtIndx+1 : len(result)-2] + break + } + } + + return res, nil +} diff --git a/pkg/apcssh/shell_helpers.go b/pkg/apcssh/shell_helpers.go new file mode 100644 index 0000000..e2084cb --- /dev/null +++ b/pkg/apcssh/shell_helpers.go @@ -0,0 +1,38 @@ +package apcssh + +import ( + "regexp" +) + +// scanAPCShell is a SplitFunc to capture shell output after each interactive +// shell command is run +func scanAPCShell(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + // regex for shell prompt (e.g., `apc@apc>`) + re := regexp.MustCompile(`(\r\n|\r|\n)[A-Za-z0-0.]+@[A-Za-z0-0.]+>`) + + // find match for prompt + if index := re.FindStringIndex(string(data)); index != nil { + // advance starts after the prompt; token is everything before the prompt + return index[1], dropCR(data[0:index[0]]), nil + } + + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), dropCR(data), nil + } + + // Request more data. + return 0, nil, nil +} + +// dropCR drops a terminal \r from the data. +func dropCR(data []byte) []byte { + if len(data) > 0 && data[len(data)-1] == '\r' { + return data[0 : len(data)-1] + } + return data +} diff --git a/pkg/apcssh/ssl.go b/pkg/apcssh/ssl.go new file mode 100644 index 0000000..514545d --- /dev/null +++ b/pkg/apcssh/ssl.go @@ -0,0 +1,15 @@ +package apcssh + +import "fmt" + +// InstallSSLCert installs the specified p15 cert file on the UPS. This +// function currently only works on NMC2. +func (cli *Client) InstallSSLCert(keyCertP15 []byte) error { + // install NMC2 P15 file + err := cli.UploadSCP("/ssl/defaultcert.p15", keyCertP15, 0600) + if err != nil { + return fmt.Errorf("apcssh: ssl cert install: failed to send file to ups over scp (%w)", err) + } + + return nil +} diff --git a/pkg/app/cmd_install.go b/pkg/app/cmd_install.go index 26635e7..f59bd7b 100644 --- a/pkg/app/cmd_install.go +++ b/pkg/app/cmd_install.go @@ -1,16 +1,10 @@ package app import ( + "apc-p15-tool/pkg/apcssh" "context" - "crypto/sha256" - "encoding/base64" - "encoding/hex" "errors" "fmt" - "net" - "runtime" - - "golang.org/x/crypto/ssh" ) // cmdInstall is the app's command to create apc p15 file content from key and cert @@ -52,100 +46,29 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error { // validation done // make p15 file - apcFile, _, err := app.pemToAPCP15s(keyPem, certPem, "install") + keyCertP15, _, err := app.pemToAPCP15s(keyPem, certPem, "install") if err != nil { return err } - // make host key callback - hk := func(hostname string, remote net.Addr, key ssh.PublicKey) error { - // calculate server's key's SHA256 - hasher := sha256.New() - _, err := hasher.Write(key.Marshal()) - if err != nil { - return err - } - actualHash := hasher.Sum(nil) - - // log fingerprint for debugging - actualHashB64 := base64.RawStdEncoding.EncodeToString(actualHash) - actualHashHex := hex.EncodeToString(actualHash) - app.debugLogger.Printf("ssh: remote server key fingerprint (b64): %s", actualHashB64) - app.debugLogger.Printf("ssh: remote server key fingerprint (hex): %s", actualHashHex) - - // allow base64 format - if actualHashB64 == *app.config.install.fingerprint { - return nil - } - - // allow hex format - if actualHashHex == *app.config.install.fingerprint { - return nil - } - - return errors.New("ssh: fingerprint didn't match") + // make APC SSH client + cfg := &apcssh.Config{ + Hostname: *app.config.install.hostAndPort, + Username: *app.config.install.username, + Password: *app.config.install.password, + ServerFingerprint: *app.config.install.fingerprint, + InsecureCipher: *app.config.install.insecureCipher, } - // kex algos - // see defaults: https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.18.0:ssh/common.go;l=62 - kexAlgos := []string{ - "curve25519-sha256", "curve25519-sha256@libssh.org", - "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", - "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1", - } - // extra for some apc ups - kexAlgos = append(kexAlgos, "diffie-hellman-group-exchange-sha256") - - // ciphers - // see defaults: https://cs.opensource.google/go/x/crypto/+/master:ssh/common.go;l=37 - ciphers := []string{ - "aes128-gcm@openssh.com", "aes256-gcm@openssh.com", - "chacha20-poly1305@openssh.com", - "aes128-ctr", "aes192-ctr", "aes256-ctr", - } - - // insecure cipher options? - if app.config.install.insecureCipher != nil && *app.config.install.insecureCipher { - app.stdLogger.Println("WARNING: insecure ciphers are enabled (--insecurecipher). SSH with an insecure cipher is NOT secure and should NOT be used.") - ciphers = append(ciphers, "aes128-cbc", "3des-cbc") - } - - // install file on UPS - // ssh config - config := &ssh.ClientConfig{ - User: *app.config.install.username, - Auth: []ssh.AuthMethod{ - ssh.Password(*app.config.install.password), - }, - // APC seems to require `Client Version` string to start with "SSH-2" and must be at least - // 13 characters long - // working examples from other clients: - // ClientVersion: "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6", - // ClientVersion: "SSH-2.0-PuTTY_Release_0.80", - ClientVersion: fmt.Sprintf("SSH-2.0-apc-p15-tool_v%s %s-%s", appVersion, runtime.GOOS, runtime.GOARCH), - Config: ssh.Config{ - KeyExchanges: kexAlgos, - Ciphers: ciphers, - // MACs: []string{"hmac-sha2-256"}, - }, - // HostKeyAlgorithms: []string{"ssh-rsa"}, - HostKeyCallback: hk, - - // reasonable timeout for file copy - Timeout: sshScpTimeout, - } - - // connect to ups over SSH - client, err := ssh.Dial("tcp", *app.config.install.hostAndPort, config) + client, err := apcssh.New(cfg) if err != nil { return fmt.Errorf("install: failed to connect to host (%w)", err) } - defer client.Close() - // send file to UPS - err = sshScpSendFileToUPS(client, apcFile) + // install SSL Cert + err = client.InstallSSLCert(keyCertP15) if err != nil { - return fmt.Errorf("install: failed to send p15 file to ups over scp (%w)", err) + return fmt.Errorf("install: failed to send file to ups over scp (%w)", err) } // installed @@ -155,16 +78,7 @@ func (app *app) cmdInstall(cmdCtx context.Context, args []string) error { if app.config.install.restartWebUI != nil && *app.config.install.restartWebUI { app.stdLogger.Println("install: sending restart command") - // connect to ups over SSH - // opening a second session doesn't seem to work with my NMC2 for some reason, so make - // a new connection instead - client, err = ssh.Dial("tcp", *app.config.install.hostAndPort, config) - if err != nil { - return fmt.Errorf("install: failed to reconnect to host to send webui restart command (%w)", err) - } - defer client.Close() - - err = sshResetUPSWebUI(client) + err = client.RestartWebUI() if err != nil { return fmt.Errorf("install: failed to send webui restart command (%w)", err) } diff --git a/pkg/app/ssh_resetwebui.go b/pkg/app/ssh_resetwebui.go deleted file mode 100644 index 52f90d9..0000000 --- a/pkg/app/ssh_resetwebui.go +++ /dev/null @@ -1,45 +0,0 @@ -package app - -import ( - "errors" - "fmt" - - "golang.org/x/crypto/ssh" -) - -// sshResetUPSWebUI sends a command to the UPS to restart the WebUI. This -// command is supposed to be required to load the new cert, but that -// doesn't seem to be true (at least it isn't on my UPS). Adding the -// option though, in case other UPS might need it. -func sshResetUPSWebUI(client *ssh.Client) error { - // make session to use for restart command - session, err := client.NewSession() - if err != nil { - return fmt.Errorf("ssh: restart: failed to create session (%w)", err) - } - defer session.Close() - - // start shell - err = session.Shell() - if err != nil { - return fmt.Errorf("ssh: restart: failed to start shell (%w)", err) - } - - // execure reboot via SendRequest - payload := []byte("reboot -Y") - payloadLen := uint8(len(payload)) - payload = append([]byte{0, 0, 0, payloadLen}, payload...) - - ok, err := session.SendRequest("exec", true, payload) - if err != nil { - return fmt.Errorf("ssh: scp: failed to execute scp cmd (%w)", err) - } - if !ok { - return errors.New("ssh: scp: execute scp cmd not ok") - } - - // don't read remote output, as nothing interesting actually outputs - - // done - return nil -} diff --git a/pkg/app/ssh_response.go b/pkg/app/ssh_response.go deleted file mode 100644 index 8068704..0000000 --- a/pkg/app/ssh_response.go +++ /dev/null @@ -1,34 +0,0 @@ -package app - -import ( - "bufio" - "fmt" - "io" -) - -// sshCheckResponse reads the output from the remote and returns an error -// if the remote output was not 0 -func sshCheckResponse(remoteOutPipe io.Reader) error { - buffer := make([]uint8, 1) - _, err := remoteOutPipe.Read(buffer) - if err != nil { - return fmt.Errorf("ssh: failed to read output buffer (%w)", err) - } - - responseType := buffer[0] - message := "" - if responseType > 0 { - bufferedReader := bufio.NewReader(remoteOutPipe) - message, err = bufferedReader.ReadString('\n') - if err != nil { - return fmt.Errorf("ssh: failed to read output buffer (%w)", err) - } - } - - // if not 0 (aka OK) - if responseType != 0 { - return fmt.Errorf("ssh: remote returned error (%d: %s)", responseType, message) - } - - return nil -} diff --git a/pkg/app/ssh_scp.go b/pkg/app/ssh_scp.go deleted file mode 100644 index 54f216c..0000000 --- a/pkg/app/ssh_scp.go +++ /dev/null @@ -1,105 +0,0 @@ -package app - -import ( - "bytes" - "errors" - "fmt" - "io" - "path" - "time" - - "golang.org/x/crypto/ssh" -) - -// APC UPS won't except Go's SSH "Run()" command as the format isn't quite -// the same. Therefore, write a custom implementation to send the desired -// command instead of relying on something like github.com/bramvdbogaerde/go-scp - -const ( - sshScpP15Destination = "/ssl/defaultcert.p15" - sshScpP15PermissionsStr = "0600" - - sshScpTimeout = 90 * time.Second -) - -// sshScpSendFileToUPS sends the p15File to the APC UPS via the SCP protocol. it is -// automatically placed in the correct directory and will overwrite any existing -// file -func sshScpSendFileToUPS(client *ssh.Client, p15File []byte) error { - // make session to use for SCP - session, err := client.NewSession() - if err != nil { - return fmt.Errorf("ssh: scp: failed to create session (%w)", err) - } - defer session.Close() - - // attach pipes - out, err := session.StdoutPipe() - if err != nil { - return err - } - w, err := session.StdinPipe() - if err != nil { - return err - } - defer w.Close() - - // send execute cmd -- - // build cmd to send as request - // Go implementation sends additional 0x22 bytes when using Run() (as - // compared to putty's scp tool). these additional bytes seem to cause the - // apc ups to fail execution of the command - payload := []byte(fmt.Sprintf("scp -q -t %s", sshScpP15Destination)) - payloadLen := uint8(len(payload)) - payload = append([]byte{0, 0, 0, payloadLen}, payload...) - - ok, err := session.SendRequest("exec", true, payload) - if err != nil { - return fmt.Errorf("ssh: scp: failed to execute scp cmd (%w)", err) - } - if !ok { - return errors.New("ssh: scp: execute scp cmd not ok") - } - - // check remote response - // Note: File upload may not work if the client doesn't actually read from - // the remote output. - err = sshCheckResponse(out) - if err != nil { - return fmt.Errorf("ssh: scp: failed to send scp cmd (bad remote response) (%w)", err) - } - - // just file name (without path) - filename := path.Base(sshScpP15Destination) - - // send file header - _, err = fmt.Fprintln(w, "C"+sshScpP15PermissionsStr, len(p15File), filename) - if err != nil { - return fmt.Errorf("ssh: scp: failed to send file info (%w)", err) - } - - err = sshCheckResponse(out) - if err != nil { - return fmt.Errorf("ssh: scp: failed to send file info (bad remote response) (%w)", err) - } - - // send actual file - _, err = io.Copy(w, bytes.NewReader(p15File)) - if err != nil { - return fmt.Errorf("ssh: scp: failed to send file(%w)", err) - } - - // send file end - _, err = fmt.Fprint(w, "\x00") - if err != nil { - return fmt.Errorf("ssh: scp: failed to send final 00 byte (%w)", err) - } - - err = sshCheckResponse(out) - if err != nil { - return fmt.Errorf("ssh: scp: failed to send file (bad remote response) (%w)", err) - } - - // done - return nil -}