mirror of
https://github.com/gregtwallace/apc-p15-tool.git
synced 2025-01-22 00:04:09 +00:00
add install function
install pem files directly to an apc ups
This commit is contained in:
parent
4c154b2f27
commit
a089d12c87
9 changed files with 392 additions and 51 deletions
5
go.mod
5
go.mod
|
@ -9,7 +9,10 @@ require (
|
|||
golang.org/x/crypto v0.18.0
|
||||
)
|
||||
|
||||
require go.uber.org/multierr v1.11.0 // indirect
|
||||
require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
)
|
||||
|
||||
replace apc-p15-tool/cmd => /cmd
|
||||
|
||||
|
|
4
go.sum
4
go.sum
|
@ -18,6 +18,10 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
|||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
|
@ -31,27 +31,21 @@ func Start() {
|
|||
logger: makeZapLogger(&initLogLevel),
|
||||
}
|
||||
|
||||
// get config
|
||||
app.getConfig()
|
||||
// log start
|
||||
app.logger.Infof("apc-p15-tool v%s", appVersion)
|
||||
|
||||
// get & parse config
|
||||
err := app.getConfig()
|
||||
|
||||
// re-init logger with configured log level
|
||||
app.logger = makeZapLogger(app.config.logLevel)
|
||||
|
||||
// log start
|
||||
app.logger.Infof("apc-p15-tool v%s", appVersion)
|
||||
|
||||
// get config
|
||||
app.getConfig()
|
||||
|
||||
// run it
|
||||
exitCode := 0
|
||||
err := app.cmd.ParseAndRun(context.Background(), os.Args[1:], ff.WithEnvVarPrefix(environmentVarPrefix))
|
||||
// deal with config err (after logger re-init)
|
||||
if err != nil {
|
||||
exitCode = 1
|
||||
exitCode := 0
|
||||
|
||||
if errors.Is(err, ff.ErrHelp) {
|
||||
// help explicitly requested
|
||||
exitCode = 0
|
||||
app.logger.Info("\n\n", ffhelp.Command(app.cmd))
|
||||
|
||||
} else if errors.Is(err, ff.ErrDuplicateFlag) ||
|
||||
|
@ -59,13 +53,28 @@ func Start() {
|
|||
errors.Is(err, ff.ErrNoExec) ||
|
||||
errors.Is(err, ErrExtraArgs) {
|
||||
// other error that suggests user needs to see help
|
||||
exitCode = 1
|
||||
app.logger.Error(err)
|
||||
app.logger.Info("\n\n", ffhelp.Command(app.cmd))
|
||||
|
||||
} else {
|
||||
// any other error
|
||||
exitCode = 1
|
||||
app.logger.Error(err)
|
||||
}
|
||||
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
// get config
|
||||
app.getConfig()
|
||||
|
||||
// run it
|
||||
exitCode := 0
|
||||
err = app.cmd.Run(context.Background())
|
||||
if err != nil {
|
||||
exitCode = 1
|
||||
app.logger.Error(err)
|
||||
}
|
||||
|
||||
app.logger.Info("apc-p15-tool done")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"apc-p15-tool/pkg/pkcs15"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -29,42 +28,13 @@ func (app *app) cmdCreate(_ context.Context, args []string) error {
|
|||
}
|
||||
|
||||
// validation done
|
||||
app.logger.Infof("create: making apc p15 file from pem files")
|
||||
|
||||
// Read in PEM files
|
||||
keyPem, err := os.ReadFile(*app.config.create.keyPemFilePath)
|
||||
// make p15 file
|
||||
apcFile, err := app.pemToAPCP15(*app.config.create.keyPemFilePath, *app.config.create.certPemFilePath, "create")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create: failed to read key file (%s)", err)
|
||||
return err
|
||||
}
|
||||
|
||||
certPem, err := os.ReadFile(*app.config.create.certPemFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create: failed to read cert file (%s)", err)
|
||||
}
|
||||
|
||||
// make p15 struct
|
||||
p15, err := pkcs15.ParsePEMToPKCS15(keyPem, certPem)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create: failed to parse pem files (%s)", err)
|
||||
}
|
||||
|
||||
app.logger.Infof("create: successfully loaded pem files")
|
||||
|
||||
// make file bytes
|
||||
p15File, err := p15.ToP15File()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create: failed to make p15 file (%s)", err)
|
||||
}
|
||||
|
||||
// make header for file bytes
|
||||
apcHeader, err := makeFileHeader(p15File)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create: failed to make p15 file header (%s)", err)
|
||||
}
|
||||
|
||||
// combine header with file
|
||||
apcFile := append(apcHeader, p15File...)
|
||||
|
||||
// determine file name (should already be done by flag parsing, but avoid nil just in case)
|
||||
fileName := createDefaultOutFilePath
|
||||
if app.config.create.outFilePath != nil && *app.config.create.outFilePath != "" {
|
||||
|
|
131
pkg/app/cmd_install.go
Normal file
131
pkg/app/cmd_install.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"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
|
||||
// pem files and upload the p15 to the specified APC UPS
|
||||
func (app *app) cmdInstall(cmdCtx context.Context, args []string) error {
|
||||
// extra args == error
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("install: failed, %w (%d)", ErrExtraArgs, len(args))
|
||||
}
|
||||
|
||||
// must have username
|
||||
if app.config.install.username == nil || *app.config.install.username == "" {
|
||||
return errors.New("install: failed, username not specified")
|
||||
}
|
||||
|
||||
// must have password
|
||||
if app.config.install.password == nil || *app.config.install.password == "" {
|
||||
return errors.New("install: failed, password not specified")
|
||||
}
|
||||
|
||||
// must have fingerprint
|
||||
if app.config.install.fingerprint == nil || *app.config.install.fingerprint == "" {
|
||||
return errors.New("install: failed, fingerprint not specified")
|
||||
}
|
||||
|
||||
// key must be specified
|
||||
if app.config.install.keyPemFilePath == nil || *app.config.install.keyPemFilePath == "" {
|
||||
return errors.New("install: failed, key not specified")
|
||||
}
|
||||
|
||||
// cert must be specified
|
||||
if app.config.install.certPemFilePath == nil || *app.config.install.certPemFilePath == "" {
|
||||
return errors.New("install: failed, cert not specified")
|
||||
}
|
||||
|
||||
// host to install on must be specified
|
||||
if app.config.install.hostAndPort == nil || *app.config.install.hostAndPort == "" {
|
||||
return errors.New("install: failed, apc host not specified")
|
||||
}
|
||||
|
||||
// validation done
|
||||
|
||||
// make p15 file
|
||||
apcFile, err := app.pemToAPCP15(*app.config.install.keyPemFilePath, *app.config.install.certPemFilePath, "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.logger.Debugf("ssh: remote server key fingerprint (b64): %s", actualHashB64)
|
||||
app.logger.Debugf("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")
|
||||
}
|
||||
|
||||
// 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
|
||||
// e.g. working from Ubuntu ssh: 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: []string{"ecdh-sha2-nistp256"},
|
||||
// Ciphers: []string{"aes128-ctr"},
|
||||
// MACs: []string{"hmac-sha2-256"},
|
||||
},
|
||||
// HostKeyAlgorithms: []string{"ssh-rsa"},
|
||||
HostKeyCallback: hk,
|
||||
|
||||
// reasonable timeout for file copy
|
||||
Timeout: scpTimeout,
|
||||
}
|
||||
|
||||
// connect to ups over SSH
|
||||
client, err := ssh.Dial("tcp", *app.config.install.hostAndPort, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("install: failed to connect to host (%w)", err)
|
||||
}
|
||||
|
||||
// send file to UPS
|
||||
err = scpSendFileToUPS(client, apcFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("install: failed to send p15 file to ups over scp (%w)", err)
|
||||
}
|
||||
|
||||
// done
|
||||
app.logger.Infof("install: apc p15 file installed on %s", *app.config.install.hostAndPort)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -2,6 +2,7 @@ package app
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v4"
|
||||
)
|
||||
|
@ -18,18 +19,26 @@ type config struct {
|
|||
certPemFilePath *string
|
||||
outFilePath *string
|
||||
}
|
||||
install struct {
|
||||
keyPemFilePath *string
|
||||
certPemFilePath *string
|
||||
hostAndPort *string
|
||||
fingerprint *string
|
||||
username *string
|
||||
password *string
|
||||
}
|
||||
}
|
||||
|
||||
// getConfig returns the app's configuration from either command line args,
|
||||
// or environment variables
|
||||
func (app *app) getConfig() {
|
||||
func (app *app) getConfig() error {
|
||||
// make config
|
||||
cfg := &config{}
|
||||
|
||||
// commands:
|
||||
// create
|
||||
// install
|
||||
// TODO:
|
||||
// upload
|
||||
// unpack (both key & key+cert)
|
||||
|
||||
// apc-p15-tool -- root command
|
||||
|
@ -61,7 +70,33 @@ func (app *app) getConfig() {
|
|||
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, createCmd)
|
||||
|
||||
// set app cmd & cfg
|
||||
app.cmd = rootCmd
|
||||
// install -- subcommand
|
||||
installFlags := ff.NewFlagSet("install").SetParent(rootFlags)
|
||||
|
||||
cfg.install.keyPemFilePath = installFlags.StringLong("keyfile", "", "path and filename of the rsa-2048 key in pem format")
|
||||
cfg.install.certPemFilePath = installFlags.StringLong("certfile", "", "path and filename of the certificate in pem format")
|
||||
cfg.install.hostAndPort = installFlags.StringLong("apchost", "", "hostname:port of the apc ups to install the certificate on")
|
||||
cfg.install.fingerprint = installFlags.StringLong("fingerprint", "", "the SHA256 fingerprint value of the ups' ssh server")
|
||||
cfg.install.username = installFlags.StringLong("username", "", "username to login to the apc ups")
|
||||
cfg.install.password = installFlags.StringLong("password", "", "password to login to the apc ups")
|
||||
|
||||
installCmd := &ff.Command{
|
||||
Name: "install",
|
||||
Usage: "apc-p15-tool upload --keyfile key.pem --certfile cert.pem --apchost example.com:22 --fingerprint 123abc --username apc --password test",
|
||||
ShortHelp: "install the specified key and cert pem files on an apc ups (they will be converted to a comaptible p15 file)",
|
||||
Flags: installFlags,
|
||||
Exec: app.cmdInstall,
|
||||
}
|
||||
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, installCmd)
|
||||
|
||||
// set cfg & parse
|
||||
app.config = cfg
|
||||
app.cmd = rootCmd
|
||||
err := app.cmd.Parse(os.Args[1:], ff.WithEnvVarPrefix(environmentVarPrefix))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
50
pkg/app/pem_to_p15.go
Normal file
50
pkg/app/pem_to_p15.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"apc-p15-tool/pkg/pkcs15"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// pemToAPCP15 reads the specified pem files and returns the apc p15 bytes
|
||||
func (app *app) pemToAPCP15(keyFileName, certFileName, parentCmdName string) ([]byte, error) {
|
||||
app.logger.Infof("%s: making apc p15 file from pem files", parentCmdName)
|
||||
|
||||
// Read in PEM files
|
||||
keyPem, err := os.ReadFile(keyFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to read key file (%w)", parentCmdName, err)
|
||||
}
|
||||
|
||||
certPem, err := os.ReadFile(certFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to read cert file (%w)", parentCmdName, err)
|
||||
}
|
||||
|
||||
// make p15 struct
|
||||
p15, err := pkcs15.ParsePEMToPKCS15(keyPem, certPem)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to parse pem files (%w)", parentCmdName, err)
|
||||
}
|
||||
|
||||
app.logger.Infof("%s: successfully loaded pem files", parentCmdName)
|
||||
|
||||
// make file bytes
|
||||
p15File, err := p15.ToP15File()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to make p15 file (%w)", parentCmdName, err)
|
||||
}
|
||||
|
||||
// make header for file bytes
|
||||
apcHeader, err := makeFileHeader(p15File)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: failed to make p15 file header (%w)", parentCmdName, err)
|
||||
}
|
||||
|
||||
// combine header with file
|
||||
apcFile := append(apcHeader, p15File...)
|
||||
|
||||
app.logger.Infof("%s: apc p15 file data succesfully generated", parentCmdName)
|
||||
|
||||
return apcFile, nil
|
||||
}
|
105
pkg/app/scp.go
Normal file
105
pkg/app/scp.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
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 (
|
||||
scpP15Destination = "/ssl/defaultcert.p15"
|
||||
scpP15PermissionsStr = "0600"
|
||||
|
||||
scpTimeout = 90 * time.Second
|
||||
)
|
||||
|
||||
// scpSendFileToUPS 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 scpSendFileToUPS(client *ssh.Client, p15File []byte) error {
|
||||
// make session to use for SCP
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("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 -v -t %s", scpP15Destination))
|
||||
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("scp: failed to execute scp cmd (%w)", err)
|
||||
}
|
||||
if !ok {
|
||||
return errors.New("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("scp: failed to send scp cmd (bad remote response) (%w)", err)
|
||||
}
|
||||
|
||||
// just file name (without path)
|
||||
filename := path.Base(scpP15Destination)
|
||||
|
||||
// send file header
|
||||
_, err = fmt.Fprintln(w, "C"+scpP15PermissionsStr, len(p15File), filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scp: failed to send file info (%w)", err)
|
||||
}
|
||||
|
||||
err = scpCheckResponse(out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("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("scp: failed to send file(%w)", err)
|
||||
}
|
||||
|
||||
// send file end
|
||||
_, err = fmt.Fprint(w, "\x00")
|
||||
if err != nil {
|
||||
return fmt.Errorf("scp: failed to send final 00 byte (%w)", err)
|
||||
}
|
||||
|
||||
err = scpCheckResponse(out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scp: failed to send file (bad remote response) (%w)", err)
|
||||
}
|
||||
|
||||
// done
|
||||
return nil
|
||||
}
|
34
pkg/app/scp_response.go
Normal file
34
pkg/app/scp_response.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 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("scp: failed to make 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("scp: failed to read output buffer (%w)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// if not 0 (aka OK)
|
||||
if responseType != 0 {
|
||||
return fmt.Errorf("scp: remote returned error (%d: %s)", responseType, message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue