From 9b65bfe47585ae7bd55666e2e4a194324b5f1d02 Mon Sep 17 00:00:00 2001 From: Patrick Deziel Date: Mon, 30 Oct 2023 16:55:59 -0500 Subject: [PATCH 1/2] Add store:password command --- .gitignore | 3 ++ cmd/courier/main.go | 72 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/.gitignore b/.gitignore index a8f876d..0ddd85a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ # Go workspace file go.work + +# macOS +*.DS_Store \ No newline at end of file diff --git a/cmd/courier/main.go b/cmd/courier/main.go index e01cfad..276aa7f 100644 --- a/cmd/courier/main.go +++ b/cmd/courier/main.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "log" "os" "time" @@ -57,6 +58,37 @@ func main() { }, }, }, + { + Name: "store:password", + Usage: "store a pkcs12 password using the courier server", + Category: "client", + Action: storePassword, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Aliases: []string{"u", "endpoint"}, + Usage: "url to connect to the courier server", + EnvVars: []string{"COURIER_CLIENT_URL"}, + Required: true, + }, + &cli.StringFlag{ + Name: "id", + Aliases: []string{"i"}, + Usage: "the id of the certificate to store the password for", + Required: true, + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "the password to store", + }, + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "specify a file to read the password from", + }, + }, + }, { Name: "secrets:get", Usage: "get a secret from the secret manager", @@ -138,6 +170,46 @@ func status(c *cli.Context) (err error) { return printJSON(rep) } +// Store a password using the courier service. +func storePassword(c *cli.Context) (err error) { + var client api.CourierClient + if client, err = api.New(c.String("url")); err != nil { + return cli.Exit(err, 1) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if c.String("password") == "" && c.String("file") == "" { + return cli.Exit("either --password or --file must be specified", 1) + } + + var password string + if password = c.String("password"); password == "" { + var f *os.File + if f, err = os.Open(c.String("file")); err != nil { + return cli.Exit(err, 1) + } + + var data []byte + if data, err = io.ReadAll(f); err != nil { + return cli.Exit(err, 1) + } + + password = string(data) + } + + req := &api.StorePasswordRequest{ + ID: c.String("id"), + Password: password, + } + if err = client.StoreCertificatePassword(ctx, req); err != nil { + return cli.Exit(err, 1) + } + + return nil +} + // Get a secret from the secret manager. func getSecret(c *cli.Context) (err error) { conf := config.SecretsConfig{ From e8865c8b3481cc30017582f94bad8d4a5209ce57 Mon Sep 17 00:00:00 2001 From: Patrick Deziel Date: Tue, 31 Oct 2023 11:03:22 -0500 Subject: [PATCH 2/2] Add store:certificate method --- .gitignore | 5 ++- cmd/courier/main.go | 65 +++++++++++++++++++++++++++++++++++++++ pkg/config/config_test.go | 16 ++++++++++ pkg/server.go | 5 +++ pkg/store/local/store.go | 12 ++++---- 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 0ddd85a..1739cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ go.work # macOS -*.DS_Store \ No newline at end of file +*.DS_Store + +# Google credentials +*.json \ No newline at end of file diff --git a/cmd/courier/main.go b/cmd/courier/main.go index 276aa7f..b0aeab2 100644 --- a/cmd/courier/main.go +++ b/cmd/courier/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -89,6 +90,38 @@ func main() { }, }, }, + { + Name: "store:certificate", + Usage: "store a pkcs12 certificate using the courier server", + Category: "client", + Action: storeCertificate, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Aliases: []string{"u", "endpoint"}, + Usage: "url to connect to the courier server", + EnvVars: []string{"COURIER_CLIENT_URL"}, + Required: true, + }, + &cli.StringFlag{ + Name: "id", + Aliases: []string{"i"}, + Usage: "the id of the certificate, used to lookup the stored password", + Required: true, + }, + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: "path to the certificate file", + Required: true, + }, + &cli.BoolFlag{ + Name: "no-decrypt", + Aliases: []string{"D"}, + Usage: "do not decrypt the certificate before storing it", + }, + }, + }, { Name: "secrets:get", Usage: "get a secret from the secret manager", @@ -210,6 +243,38 @@ func storePassword(c *cli.Context) (err error) { return nil } +// Store a certificate using the courier service. +func storeCertificate(c *cli.Context) (err error) { + var client api.CourierClient + if client, err = api.New(c.String("url")); err != nil { + return cli.Exit(err, 1) + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var f *os.File + if f, err = os.Open(c.String("file")); err != nil { + return cli.Exit(err, 1) + } + + var data []byte + if data, err = io.ReadAll(f); err != nil { + return cli.Exit(err, 1) + } + + req := &api.StoreCertificateRequest{ + ID: c.String("id"), + NoDecrypt: c.Bool("no-decrypt"), + Base64Certificate: base64.StdEncoding.EncodeToString(data), + } + if err = client.StoreCertificate(ctx, req); err != nil { + return cli.Exit(err, 1) + } + + return nil +} + // Get a secret from the secret manager. func getSecret(c *cli.Context) (err error) { conf := config.SecretsConfig{ diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ae8a2cf..0f44bb8 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -84,6 +84,22 @@ func TestValidate(t *testing.T) { require.NoError(t, conf.Validate(), "secure config should be valid") }) + t.Run("ValidSecretManager", func(t *testing.T) { + conf := config.Config{ + BindAddr: ":8080", + Mode: "debug", + MTLS: config.MTLSConfig{ + Insecure: true, + }, + SecretManager: config.SecretsConfig{ + Enabled: true, + Credentials: "test-credentials", + Project: "test-project", + }, + } + require.NoError(t, conf.Validate(), "secret manager config should be valid") + }) + t.Run("MissingBindAddr", func(t *testing.T) { conf := config.Config{ Mode: "debug", diff --git a/pkg/server.go b/pkg/server.go index 417ac4f..959ba2f 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -15,6 +15,7 @@ import ( "github.com/trisacrypto/courier/pkg/api/v1" "github.com/trisacrypto/courier/pkg/config" "github.com/trisacrypto/courier/pkg/store" + "github.com/trisacrypto/courier/pkg/store/gcloud" "github.com/trisacrypto/courier/pkg/store/local" ) @@ -39,6 +40,10 @@ func New(conf config.Config) (s *Server, err error) { if s.store, err = local.Open(conf.LocalStorage); err != nil { return nil, err } + case conf.SecretManager.Enabled: + if s.store, err = gcloud.Open(conf.SecretManager); err != nil { + return nil, err + } default: return nil, errors.New("no storage backend configured") } diff --git a/pkg/store/local/store.go b/pkg/store/local/store.go index be7930f..0743dd4 100644 --- a/pkg/store/local/store.go +++ b/pkg/store/local/store.go @@ -52,7 +52,7 @@ func (s *Store) Close() error { func (s *Store) GetPassword(ctx context.Context, id string) (password []byte, err error) { s.RLock() defer s.RUnlock() - return s.readFile(s.fullPath(store.PasswordPrefix, id)) + return s.readFile(s.fullPath(store.PasswordPrefix, id, archiveExt)) } // UpdatePassword updates a password by id in the local storage backend. If the @@ -60,7 +60,7 @@ func (s *Store) GetPassword(ctx context.Context, id string) (password []byte, er func (s *Store) UpdatePassword(ctx context.Context, id string, password []byte) (err error) { s.Lock() defer s.Unlock() - return s.writeFile(s.fullPath(store.PasswordPrefix, id), password) + return s.writeFile(s.fullPath(store.PasswordPrefix, id, archiveExt), password) } //=========================================================================== @@ -73,7 +73,7 @@ func (s *Store) GetCertificate(ctx context.Context, name string) (cert []byte, e defer s.RUnlock() // Load the certificate archive into bytes - if cert, err = os.ReadFile(s.fullPath(store.CertificatePrefix, name)); err != nil { + if cert, err = os.ReadFile(s.fullPath(store.CertificatePrefix, name, "")); err != nil { if os.IsNotExist(err) { return nil, store.ErrNotFound } @@ -87,7 +87,7 @@ func (s *Store) GetCertificate(ctx context.Context, name string) (cert []byte, e func (s *Store) UpdateCertificate(ctx context.Context, name string, cert []byte) (err error) { s.Lock() defer s.Unlock() - return os.WriteFile(s.fullPath(store.CertificatePrefix, name), cert, 0644) + return os.WriteFile(s.fullPath(store.CertificatePrefix, name, ""), cert, 0644) } //=========================================================================== @@ -95,8 +95,8 @@ func (s *Store) UpdateCertificate(ctx context.Context, name string, cert []byte) //=========================================================================== // fullPath returns the full path to an archive file in the local storage backend. -func (s *Store) fullPath(prefix, name string) string { - return filepath.Join(s.path, prefix+"-"+name+archiveExt) +func (s *Store) fullPath(prefix, name, ext string) string { + return filepath.Join(s.path, prefix+"-"+name+ext) } // read returns file data by archive path from the local storage