apc-p15-tool/pkg/apcssh/client.go

135 lines
3.6 KiB
Go
Raw Normal View History

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
}