From f4900bf0f062bb0a0474b20534674c72d78abea4 Mon Sep 17 00:00:00 2001 From: Ben <0x620x64@protonmail.com> Date: Mon, 29 Aug 2022 17:41:24 +0100 Subject: [PATCH] #1 Initial version --- .goreleaser.yml | 40 +++++++++++++++++++ Makefile | 17 ++++++++ cmd/init.go | 38 ++++++++++++++++++ cmd/root.go | 19 +++++++++ cmd/sign.go | 91 ++++++++++++++++++++++++++++++++++++++++++ config/configReader.go | 43 ++++++++++++++++++++ go.mod | 13 ++++-- go.sum | 29 ++++++++++++++ helpers/keyHelper.go | 47 ++++++++++++++++++++++ helpers/pathHelper.go | 28 +++++++++++++ main.go | 7 ++++ 11 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 .goreleaser.yml create mode 100644 Makefile create mode 100644 cmd/init.go create mode 100644 cmd/root.go create mode 100644 cmd/sign.go create mode 100644 config/configReader.go create mode 100644 go.sum create mode 100644 helpers/keyHelper.go create mode 100644 helpers/pathHelper.go create mode 100644 main.go diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..afbf1e8 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,40 @@ +# This is an example .goreleaser.yml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + # you may remove this if you don't need go generate + - go generate ./... +builds: + - env: + goos: + - linux +archives: + - replacements: + linux: Linux + 386: i386 + amd64: x86_64 + files: + - README.md + - LICENSE +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' + +signs: + - id: signify + signature: ${artifact}.sig + cmd: signify + args: ["-S", "-s", "{{ .Env.HOME }}/.signify/ssh-sentinel.sec", "-m", "${artifact}", "-x", "${artifact}.sig"] + artifacts: checksum +release: + prerelease: auto + mode: keep-existing diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ec5a393 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +test: + go test ./... + +test_coverage: + go test ./... -coverprofile .testCoverage.txt + +doc: + godoc -http=:6060 + +release: + $(shell goreleaser release --rm-dist) + +release-dry: + $(shell goreleaser release --skip-publish) + +snapshot: + $(shell goreleaser release --snapshot --skip-publish --rm-dist) diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..490df6a --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "encoding/json" + "github.com/spf13/cobra" + "github.com/st2projects/ssh-sentinel-client/config" + "os" + "path/filepath" +) + +var initCmd = &cobra.Command{ + Use: "init", + Short: "Initialise the client", + Run: func(cmd *cobra.Command, args []string) { + + configBytes, err := json.MarshalIndent(&config.ConfigType{}, "", " ") + + if err != nil { + panic(err) + } + + userHome, err := os.UserHomeDir() + + if err != nil { + panic(err) + } + + configPath := filepath.Join(userHome, ".ssh-sentinel.json") + err = os.WriteFile(configPath, configBytes, os.FileMode(0600)) + if err != nil { + panic(err) + } + }, +} + +func init() { + rootCmd.AddCommand(initCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..aa3d1cb --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,19 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "os" +) + +var rootCmd = &cobra.Command{ + Use: "ssh-sentinel-client ", + Short: "A simple ssh-sentinel client", +} + +func Execute() { + err := rootCmd.Execute() + + if err != nil { + os.Exit(1) + } +} diff --git a/cmd/sign.go b/cmd/sign.go new file mode 100644 index 0000000..13623c1 --- /dev/null +++ b/cmd/sign.go @@ -0,0 +1,91 @@ +package cmd + +import ( + "bytes" + "encoding/json" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/st2projects/ssh-sentinel-client/config" + "github.com/st2projects/ssh-sentinel-client/helpers" + "github.com/st2projects/ssh-sentinel-core/model" + "io" + "net/http" + "os" +) + +var configPath string + +var signCmd = &cobra.Command{ + Use: "sign", + Short: "Sign a new key", + Run: func(cmd *cobra.Command, args []string) { + config.MakeConfig(configPath) + + conf := config.Config + + certValid, expDate := helpers.IsCertValid(conf.GetCertFile()) + if certValid { + log.Infof("Existing cert valid until %s", expDate) + } else { + log.Info("Creating new cert") + signNewKey(conf) + } + }, +} + +func signNewKey(conf *config.ConfigType) { + pubKey := conf.GetPublicKey() + + if !helpers.PathExists(pubKey) { + panic("Key " + conf.PublicKey + " does not exist") + } + + key, err := os.ReadFile(pubKey) + + if err != nil { + panic(err) + } + + signReq := &model.KeySignRequest{ + Username: conf.Username, + APIKey: conf.APIKey, + Principals: conf.Principals, + Key: string(key), + } + + signReqBytes, err := json.Marshal(signReq) + + if err != nil { + panic(err) + } + + resp, err := http.Post(conf.EndPoint, "application/json", bytes.NewBuffer(signReqBytes)) + + body, err := io.ReadAll(resp.Body) + + if err != nil { + panic(err) + } + + signResp := &model.KeySignResponse{} + + json.Unmarshal(body, signResp) + + if !signResp.Success { + log.Errorf("Sign request failed with err: %s", signResp.Message) + } else { + err := os.WriteFile(helpers.ExpandPath(conf.GetCertFile()), []byte(signResp.SignedKey), os.FileMode(0600)) + if err != nil { + log.Errorf("Failed to write new cert %s", err.Error()) + } else { + log.Info("Signed new key") + } + } +} + +func init() { + rootCmd.AddCommand(signCmd) + signCmd.Flags().StringVarP(&configPath, "config", "c", "", "Config file") + + signCmd.MarkFlagRequired("config") +} diff --git a/config/configReader.go b/config/configReader.go new file mode 100644 index 0000000..a171c16 --- /dev/null +++ b/config/configReader.go @@ -0,0 +1,43 @@ +package config + +import ( + "encoding/json" + "github.com/st2projects/ssh-sentinel-client/helpers" + "os" +) + +type ConfigType struct { + EndPoint string `json:"endPoint"` + APIKey string `json:"apiKey"` + Username string `json:"username"` + Principals []string `json:"principals"` + PublicKey string `json:"publicKey"` + CertFile string `json:"certFile"` +} + +var Config *ConfigType + +func MakeConfig(configFile string) { + if !helpers.PathExists(configFile) { + panic("config file " + configFile + " does not exits") + } + + configString, err := os.ReadFile(configFile) + if err != nil { + panic(err) + } + + err = json.Unmarshal(configString, &Config) + if err != nil { + panic(err) + } + +} + +func (c *ConfigType) GetPublicKey() string { + return helpers.ExpandPath(c.PublicKey) +} + +func (c *ConfigType) GetCertFile() string { + return helpers.ExpandPath(c.CertFile) +} diff --git a/go.mod b/go.mod index 7d59bbc..8c845c2 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,14 @@ module github.com/st2projects/ssh-sentinel-client go 1.18 require ( - github.com/st2projects/ssh-sentinel-core v1.0.0 github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.5.0 -) \ No newline at end of file + github.com/spf13/cobra v1.5.0 + github.com/st2projects/ssh-sentinel-core v1.0.0 + golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d +) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0704847 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/st2projects/ssh-sentinel-core v1.0.0 h1:9MKquOBeExd660PWkJ221pIa6qw11Bvec9TfA90W1os= +github.com/st2projects/ssh-sentinel-core v1.0.0/go.mod h1:x7Lj7JO1u4BT0iWnj66eIbn9fqQD1qB2ollTFsVzzHg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo= +golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers/keyHelper.go b/helpers/keyHelper.go new file mode 100644 index 0000000..7e88400 --- /dev/null +++ b/helpers/keyHelper.go @@ -0,0 +1,47 @@ +package helpers + +import ( + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh" + "os" + "time" +) + +func IsCertValid(certPath string) (bool, string) { + + certValid := PathExists(certPath) + + if certValid { + certBytes, err := os.ReadFile(certPath) + + if err != nil { + log.Errorf("%s - cert does not exist or cannot be read", certPath) + } + + pub, _, _, _, err := ssh.ParseAuthorizedKey(certBytes) + + if err != nil { + log.Errorf("Error when parsing cert: %s", err.Error()) + } + + cert, ok := pub.(*ssh.Certificate) + + if !ok { + log.Errorf("Failed to cast to cert") + } + + now := time.Now().UTC() + validBefore := time.Unix(int64(cert.ValidBefore), 0).UTC() + validAfter := time.Unix(int64(cert.ValidAfter), 0).UTC() + + validBeforeString := validBefore.Format("2006-01-01 15:04:05.5 -0700") + + if now.After(validAfter) && now.Before(validBefore) { + return true, validBeforeString + } else { + return false, validBeforeString + } + } + + return false, "Cert not found" +} diff --git a/helpers/pathHelper.go b/helpers/pathHelper.go new file mode 100644 index 0000000..ff5df7b --- /dev/null +++ b/helpers/pathHelper.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "errors" + "os" + "os/user" + "path/filepath" + "strings" +) + +func ExpandPath(path string) string { + usr, _ := user.Current() + homeDir := usr.HomeDir + + if path == "~" { + path = homeDir + } else if strings.HasPrefix(path, "~/") { + path = filepath.Join(homeDir, path[2:]) + } + + return path +} + +func PathExists(path string) bool { + _, err := os.Stat(path) + + return !(err != nil && errors.Is(err, os.ErrNotExist)) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fe33a24 --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/st2projects/ssh-sentinel-client/cmd" + +func main() { + cmd.Execute() +}