Skip to content

Commit

Permalink
signing: KMS signature client (#33)
Browse files Browse the repository at this point in the history
What
This PR adds the AWS KMS support for managing the Distribution Account Private Key. We encrypt the Private Key using KMS and store it in the database. Then, when a signing is needed, we decrypt using KMS.

Why
Security request/improvement.
  • Loading branch information
CaioTeixeira95 authored Aug 21, 2024
1 parent 5cf53fa commit 415470e
Show file tree
Hide file tree
Showing 35 changed files with 1,605 additions and 83 deletions.
13 changes: 13 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
WALLET_SIGNING_KEY=

# Generate a new keypair for the distribution account.
DISTRIBUTION_ACCOUNT_PUBLIC_KEY=
DISTRIBUTION_ACCOUNT_SIGNATURE_PROVIDER=ENV

# Env Signature Client
DISTRIBUTION_ACCOUNT_PRIVATE_KEY=

# KMS Signature Client
KMS_KEY_ARN=
AWS_REGION=
# Using KMS locally is necessary to inject the AWS credentials envs inside the container.
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=

# Channel Accounts
CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.vscode
captive-core*/
.env
.DS_Store
19 changes: 12 additions & 7 deletions cmd/channel_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
"github.com/stellar/wallet-backend/cmd/utils"
"github.com/stellar/wallet-backend/internal/db"
"github.com/stellar/wallet-backend/internal/services"
"github.com/stellar/wallet-backend/internal/signing"
"github.com/stellar/wallet-backend/internal/signing/channelaccounts"
"github.com/stellar/wallet-backend/internal/signing/store"
signingutils "github.com/stellar/wallet-backend/internal/signing/utils"
)

type channelAccountCmdConfigOptions struct {
Expand All @@ -37,10 +37,13 @@ func (c *channelAccountCmd) Command() *cobra.Command {
utils.NetworkPassphraseOption(&cfg.NetworkPassphrase),
utils.BaseFeeOption(&cfg.BaseFee),
utils.HorizonClientURLOption(&cfg.HorizonClientURL),
utils.DistributionAccountPrivateKeyOption(&cfg.DistributionAccountPrivateKey),
utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase),
}

// Distribution Account Signature Client options
signatureClientOpts := utils.SignatureClientOptions{}
cfgOpts = append(cfgOpts, utils.DistributionAccountSignatureProviderOption(&signatureClientOpts)...)

cmd := &cobra.Command{
Use: "channel-account",
Short: "Manage channel accounts",
Expand All @@ -65,13 +68,15 @@ func (c *channelAccountCmd) Command() *cobra.Command {
return fmt.Errorf("opening connection pool: %w", err)
}

signatureClient, err := signing.NewEnvSignatureClient(cfg.DistributionAccountPrivateKey, cfg.NetworkPassphrase)
signatureClientOpts.DBConnectionPool = dbConnectionPool
signatureClientOpts.NetworkPassphrase = cfg.NetworkPassphrase
signatureClient, err := utils.SignatureClientResolver(&signatureClientOpts)
if err != nil {
return fmt.Errorf("instantiating distribution account signature client: %w", err)
return fmt.Errorf("resolving distribution account signature client: %w", err)
}

channelAccountModel := channelaccounts.ChannelAccountModel{DB: dbConnectionPool}
privateKeyEncrypter := channelaccounts.DefaultPrivateKeyEncrypter{}
channelAccountModel := store.ChannelAccountModel{DB: dbConnectionPool}
privateKeyEncrypter := signingutils.DefaultPrivateKeyEncrypter{}
c.channelAccountService, err = services.NewChannelAccountService(services.ChannelAccountServiceOptions{
DB: dbConnectionPool,
HorizonClient: &horizonclient.Client{
Expand Down
116 changes: 116 additions & 0 deletions cmd/distribution_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package cmd

import (
"errors"
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/stellar/go/support/config"
"github.com/stellar/go/support/log"
"github.com/stellar/wallet-backend/cmd/utils"
"github.com/stellar/wallet-backend/internal/db"
"github.com/stellar/wallet-backend/internal/services"
"github.com/stellar/wallet-backend/internal/signing/awskms"
"github.com/stellar/wallet-backend/internal/signing/store"
)

type kmsCommandConfig struct {
databaseURL string
kmsKeyARN string
awsRegion string
distributionAccountPublicKey string
}

type distributionAccountCmd struct{}

func (c *distributionAccountCmd) Command() *cobra.Command {
cmd := cobra.Command{
Use: "distribution-account",
Short: "Distribution Account Private Key management.",
}

cmd.AddCommand(kmsCommand())

return &cmd
}

func kmsCommand() *cobra.Command {
cfg := kmsCommandConfig{}
cfgOpts := config.ConfigOptions{
utils.DatabaseURLOption(&cfg.databaseURL),
utils.DistributionAccountPublicKeyOption(&cfg.distributionAccountPublicKey),
}
cfgOpts = append(cfgOpts, utils.AWSOptions(&cfg.awsRegion, &cfg.kmsKeyARN, true)...)

cmd := &cobra.Command{
Use: "kms",
Short: "Manage the Distribution Account private key using KMS.",
}

var kmsImportService services.KMSImportService
importCmd := &cobra.Command{
Use: "import",
Short: "Import your Distribution Account Private Key. This command encrypts and stores the encrypted private key.",
Args: cobra.NoArgs,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := cfgOpts.RequireE(); err != nil {
return fmt.Errorf("requiring values of config options: %w", err)
}

if err := cfgOpts.SetValues(); err != nil {
return fmt.Errorf("setting values of config options: %w", err)
}

dbConnectionPool, err := db.OpenDBConnectionPool(cfg.databaseURL)
if err != nil {
return fmt.Errorf("opening connection pool: %w", err)
}

kmsClient, err := awskms.GetKMSClient(cfg.awsRegion)
if err != nil {
return fmt.Errorf("getting kms client: %w", err)
}

kmsImportService, err = services.NewKMSImportService(kmsClient, cfg.kmsKeyARN, store.NewKeypairModel(dbConnectionPool), cfg.distributionAccountPublicKey)
if err != nil {
return fmt.Errorf("instantiating kms import service: %w", err)
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

passwordPrompter, err := utils.NewDefaultPasswordPrompter(
"🔑 Input your Distribution Account Private Key (key will be hidden):", os.Stdin, os.Stdout)
if err != nil {
return fmt.Errorf("instantiating password prompter: %w", err)
}

distributionAccountSeed, err := passwordPrompter.Run()
if err != nil {
return fmt.Errorf("getting distribution account seed input: %w", err)
}

err = kmsImportService.ImportDistributionAccountKey(ctx, distributionAccountSeed)
if err != nil {
if errors.Is(err, services.ErrMismatchDistributionAccount) {
return fmt.Errorf("the private key provided doesn't belong to the configured distribution account public key")
}
return fmt.Errorf("importing distribution account seed: %w", err)
}

log.Ctx(ctx).Info("Successfully imported and encrypted the Distribution Account Private Key")
return nil
},
}

cmd.AddCommand(importCmd)

if err := cfgOpts.Init(cmd); err != nil {
log.Fatalf("Error initializing a config option: %s", err.Error())
}

return cmd
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ func init() {
rootCmd.AddCommand((&ingestCmd{}).Command())
rootCmd.AddCommand((&migrateCmd{}).Command())
rootCmd.AddCommand((&channelAccountCmd{}).Command())
rootCmd.AddCommand((&distributionAccountCmd{}).Command())
}
17 changes: 11 additions & 6 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,20 @@ import (
"github.com/stellar/wallet-backend/internal/db"
"github.com/stellar/wallet-backend/internal/serve"
"github.com/stellar/wallet-backend/internal/signing"
"github.com/stellar/wallet-backend/internal/signing/channelaccounts"
signingutils "github.com/stellar/wallet-backend/internal/signing/utils"
)

type serveCmd struct{}

func (c *serveCmd) Command() *cobra.Command {
cfg := serve.Configs{}

var distributionAccountPrivateKey string
cfgOpts := config.ConfigOptions{
utils.DatabaseURLOption(&cfg.DatabaseURL),
utils.LogLevelOption(&cfg.LogLevel),
utils.NetworkPassphraseOption(&cfg.NetworkPassphrase),
utils.BaseFeeOption(&cfg.BaseFee),
utils.HorizonClientURLOption(&cfg.HorizonClientURL),
utils.DistributionAccountPrivateKeyOption(&distributionAccountPrivateKey),
utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase),
{
Name: "port",
Expand Down Expand Up @@ -79,6 +77,11 @@ func (c *serveCmd) Command() *cobra.Command {
Required: true,
},
}

// Distribution Account Signature Client options
signatureClientOpts := utils.SignatureClientOptions{}
cfgOpts = append(cfgOpts, utils.DistributionAccountSignatureProviderOption(&signatureClientOpts)...)

cmd := &cobra.Command{
Use: "serve",
Short: "Run Wallet Backend server",
Expand All @@ -95,13 +98,15 @@ func (c *serveCmd) Command() *cobra.Command {
return fmt.Errorf("opening connection pool: %w", err)
}

signatureClient, err := signing.NewEnvSignatureClient(distributionAccountPrivateKey, cfg.NetworkPassphrase)
signatureClientOpts.DBConnectionPool = dbConnectionPool
signatureClientOpts.NetworkPassphrase = cfg.NetworkPassphrase
signatureClient, err := utils.SignatureClientResolver(&signatureClientOpts)
if err != nil {
return fmt.Errorf("instantiating env signature client: %w", err)
return fmt.Errorf("resolving distribution account signature client: %w", err)
}
cfg.DistributionAccountSignatureClient = signatureClient

channelAccountSignatureClient, err := signing.NewChannelAccountDBSignatureClient(dbConnectionPool, cfg.NetworkPassphrase, &channelaccounts.DefaultPrivateKeyEncrypter{}, cfg.EncryptionPassphrase)
channelAccountSignatureClient, err := signing.NewChannelAccountDBSignatureClient(dbConnectionPool, cfg.NetworkPassphrase, &signingutils.DefaultPrivateKeyEncrypter{}, cfg.EncryptionPassphrase)
if err != nil {
return fmt.Errorf("instantiating channel account db signature client: %w", err)
}
Expand Down
27 changes: 27 additions & 0 deletions cmd/utils/custom_set_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/stellar/go/support/config"
"github.com/stellar/go/support/log"
"github.com/stellar/wallet-backend/internal/entities"
"github.com/stellar/wallet-backend/internal/signing"
)

func unexpectedTypeError(key any, co *config.ConfigOption) error {
Expand Down Expand Up @@ -64,6 +65,10 @@ func SetConfigOptionStellarPublicKey(co *config.ConfigOption) error {
func SetConfigOptionStellarPrivateKey(co *config.ConfigOption) error {
privateKey := viper.GetString(co.Name)

if privateKey == "" && !co.Required {
return nil
}

isValid := strkey.IsValidEd25519SecretSeed(privateKey)
if !isValid {
return fmt.Errorf("invalid private key provided in %s", co.Name)
Expand Down Expand Up @@ -141,3 +146,25 @@ func SetConfigOptionAssets(co *config.ConfigOption) error {

return nil
}

func SetConfigOptionSignatureClientProvider(co *config.ConfigOption) error {
scType := viper.GetString(co.Name)

scType = strings.TrimSpace(scType)
if scType == "" {
return fmt.Errorf("%s cannot be empty", co.Name)
}

t := signing.SignatureClientType(scType)
if !t.IsValid() {
return fmt.Errorf("invalid %s value provided. expected: ENV or KMS", co.Name)
}

key, ok := co.ConfigKey.(*signing.SignatureClientType)
if !ok {
return unexpectedTypeError(key, co)
}
*key = t

return nil
}
70 changes: 62 additions & 8 deletions cmd/utils/global_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/stellar/go/network"
"github.com/stellar/go/support/config"
"github.com/stellar/go/txnbuild"
"github.com/stellar/wallet-backend/internal/signing"
)

func DatabaseURLOption(configKey *string) *config.ConfigOption {
Expand Down Expand Up @@ -66,23 +67,76 @@ func HorizonClientURLOption(configKey *string) *config.ConfigOption {
}
}

func ChannelAccountEncryptionPassphraseOption(configKey *string) *config.ConfigOption {
return &config.ConfigOption{
Name: "channel-account-encryption-passphrase",
Usage: "The Encryption Passphrase used to encrypt the channel accounts private key.",
OptType: types.String,
ConfigKey: configKey,
Required: true,
}
}

func DistributionAccountPublicKeyOption(configKey *string) *config.ConfigOption {
return &config.ConfigOption{
Name: "distribution-account-public-key",
Usage: "The Distribution Account public key.",
OptType: types.String,
CustomSetValue: SetConfigOptionStellarPublicKey,
ConfigKey: configKey,
Required: true,
}
}

func DistributionAccountPrivateKeyOption(configKey *string) *config.ConfigOption {
return &config.ConfigOption{
Name: "distribution-account-private-key",
Usage: "The Distribution Account private key.",
Usage: `The Distribution Account private key. It's required if the configured signature client is "ENV"`,
OptType: types.String,
CustomSetValue: SetConfigOptionStellarPrivateKey,
ConfigKey: configKey,
Required: true,
Required: false,
}
}

func ChannelAccountEncryptionPassphraseOption(configKey *string) *config.ConfigOption {
func DistributionAccountSignatureClientProviderOption(configKey *signing.SignatureClientType) *config.ConfigOption {
return &config.ConfigOption{
Name: "channel-account-encryption-passphrase",
Usage: "The Encryption Passphrase used to encrypt the channel accounts private key.",
OptType: types.String,
ConfigKey: configKey,
Required: true,
Name: "distribution-account-signature-provider",
Usage: "The Distribution Account Signature Client Provider. Options: ENV, KMS",
OptType: types.String,
CustomSetValue: SetConfigOptionSignatureClientProvider,
ConfigKey: configKey,
FlagDefault: string(signing.EnvSignatureClientType),
Required: true,
}
}

func AWSOptions(awsRegionConfigKey *string, kmsKeyARN *string, required bool) config.ConfigOptions {
awsOpts := config.ConfigOptions{
{
Name: "aws-region",
Usage: `The AWS region. It's required if the configured signature client is "KMS"`,
OptType: types.String,
ConfigKey: awsRegionConfigKey,
FlagDefault: "us-east-2",
Required: required,
},
{
Name: "kms-key-arn",
Usage: `The KMS Key ARN. It's required if the configured signature client is "KMS"`,
OptType: types.String,
ConfigKey: kmsKeyARN,
Required: required,
},
}
return awsOpts
}

func DistributionAccountSignatureProviderOption(scOpts *SignatureClientOptions) config.ConfigOptions {
opts := config.ConfigOptions{}
opts = append(opts, DistributionAccountPublicKeyOption(&scOpts.DistributionAccountPublicKey))
opts = append(opts, DistributionAccountSignatureClientProviderOption(&scOpts.Type))
opts = append(opts, DistributionAccountPrivateKeyOption(&scOpts.DistributionAccountSecretKey))
opts = append(opts, AWSOptions(&scOpts.AWSRegion, &scOpts.KMSKeyARN, false)...)
return opts
}
Loading

0 comments on commit 415470e

Please sign in to comment.