Skip to content

Commit 415470e

Browse files
signing: KMS signature client (stellar#33)
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.
1 parent 5cf53fa commit 415470e

35 files changed

+1605
-83
lines changed

.env.example

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
WALLET_SIGNING_KEY=
33

44
# Generate a new keypair for the distribution account.
5+
DISTRIBUTION_ACCOUNT_PUBLIC_KEY=
6+
DISTRIBUTION_ACCOUNT_SIGNATURE_PROVIDER=ENV
7+
8+
# Env Signature Client
59
DISTRIBUTION_ACCOUNT_PRIVATE_KEY=
610

11+
# KMS Signature Client
12+
KMS_KEY_ARN=
13+
AWS_REGION=
14+
# Using KMS locally is necessary to inject the AWS credentials envs inside the container.
15+
AWS_ACCESS_KEY_ID=
16+
AWS_SECRET_ACCESS_KEY=
17+
AWS_SESSION_TOKEN=
18+
19+
# Channel Accounts
720
CHANNEL_ACCOUNT_ENCRYPTION_PASSPHRASE=

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
.vscode
33
captive-core*/
44
.env
5+
.DS_Store

cmd/channel_account.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313
"github.com/stellar/wallet-backend/cmd/utils"
1414
"github.com/stellar/wallet-backend/internal/db"
1515
"github.com/stellar/wallet-backend/internal/services"
16-
"github.com/stellar/wallet-backend/internal/signing"
17-
"github.com/stellar/wallet-backend/internal/signing/channelaccounts"
16+
"github.com/stellar/wallet-backend/internal/signing/store"
17+
signingutils "github.com/stellar/wallet-backend/internal/signing/utils"
1818
)
1919

2020
type channelAccountCmdConfigOptions struct {
@@ -37,10 +37,13 @@ func (c *channelAccountCmd) Command() *cobra.Command {
3737
utils.NetworkPassphraseOption(&cfg.NetworkPassphrase),
3838
utils.BaseFeeOption(&cfg.BaseFee),
3939
utils.HorizonClientURLOption(&cfg.HorizonClientURL),
40-
utils.DistributionAccountPrivateKeyOption(&cfg.DistributionAccountPrivateKey),
4140
utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase),
4241
}
4342

43+
// Distribution Account Signature Client options
44+
signatureClientOpts := utils.SignatureClientOptions{}
45+
cfgOpts = append(cfgOpts, utils.DistributionAccountSignatureProviderOption(&signatureClientOpts)...)
46+
4447
cmd := &cobra.Command{
4548
Use: "channel-account",
4649
Short: "Manage channel accounts",
@@ -65,13 +68,15 @@ func (c *channelAccountCmd) Command() *cobra.Command {
6568
return fmt.Errorf("opening connection pool: %w", err)
6669
}
6770

68-
signatureClient, err := signing.NewEnvSignatureClient(cfg.DistributionAccountPrivateKey, cfg.NetworkPassphrase)
71+
signatureClientOpts.DBConnectionPool = dbConnectionPool
72+
signatureClientOpts.NetworkPassphrase = cfg.NetworkPassphrase
73+
signatureClient, err := utils.SignatureClientResolver(&signatureClientOpts)
6974
if err != nil {
70-
return fmt.Errorf("instantiating distribution account signature client: %w", err)
75+
return fmt.Errorf("resolving distribution account signature client: %w", err)
7176
}
7277

73-
channelAccountModel := channelaccounts.ChannelAccountModel{DB: dbConnectionPool}
74-
privateKeyEncrypter := channelaccounts.DefaultPrivateKeyEncrypter{}
78+
channelAccountModel := store.ChannelAccountModel{DB: dbConnectionPool}
79+
privateKeyEncrypter := signingutils.DefaultPrivateKeyEncrypter{}
7580
c.channelAccountService, err = services.NewChannelAccountService(services.ChannelAccountServiceOptions{
7681
DB: dbConnectionPool,
7782
HorizonClient: &horizonclient.Client{

cmd/distribution_account.go

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/stellar/go/support/config"
10+
"github.com/stellar/go/support/log"
11+
"github.com/stellar/wallet-backend/cmd/utils"
12+
"github.com/stellar/wallet-backend/internal/db"
13+
"github.com/stellar/wallet-backend/internal/services"
14+
"github.com/stellar/wallet-backend/internal/signing/awskms"
15+
"github.com/stellar/wallet-backend/internal/signing/store"
16+
)
17+
18+
type kmsCommandConfig struct {
19+
databaseURL string
20+
kmsKeyARN string
21+
awsRegion string
22+
distributionAccountPublicKey string
23+
}
24+
25+
type distributionAccountCmd struct{}
26+
27+
func (c *distributionAccountCmd) Command() *cobra.Command {
28+
cmd := cobra.Command{
29+
Use: "distribution-account",
30+
Short: "Distribution Account Private Key management.",
31+
}
32+
33+
cmd.AddCommand(kmsCommand())
34+
35+
return &cmd
36+
}
37+
38+
func kmsCommand() *cobra.Command {
39+
cfg := kmsCommandConfig{}
40+
cfgOpts := config.ConfigOptions{
41+
utils.DatabaseURLOption(&cfg.databaseURL),
42+
utils.DistributionAccountPublicKeyOption(&cfg.distributionAccountPublicKey),
43+
}
44+
cfgOpts = append(cfgOpts, utils.AWSOptions(&cfg.awsRegion, &cfg.kmsKeyARN, true)...)
45+
46+
cmd := &cobra.Command{
47+
Use: "kms",
48+
Short: "Manage the Distribution Account private key using KMS.",
49+
}
50+
51+
var kmsImportService services.KMSImportService
52+
importCmd := &cobra.Command{
53+
Use: "import",
54+
Short: "Import your Distribution Account Private Key. This command encrypts and stores the encrypted private key.",
55+
Args: cobra.NoArgs,
56+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
57+
if err := cfgOpts.RequireE(); err != nil {
58+
return fmt.Errorf("requiring values of config options: %w", err)
59+
}
60+
61+
if err := cfgOpts.SetValues(); err != nil {
62+
return fmt.Errorf("setting values of config options: %w", err)
63+
}
64+
65+
dbConnectionPool, err := db.OpenDBConnectionPool(cfg.databaseURL)
66+
if err != nil {
67+
return fmt.Errorf("opening connection pool: %w", err)
68+
}
69+
70+
kmsClient, err := awskms.GetKMSClient(cfg.awsRegion)
71+
if err != nil {
72+
return fmt.Errorf("getting kms client: %w", err)
73+
}
74+
75+
kmsImportService, err = services.NewKMSImportService(kmsClient, cfg.kmsKeyARN, store.NewKeypairModel(dbConnectionPool), cfg.distributionAccountPublicKey)
76+
if err != nil {
77+
return fmt.Errorf("instantiating kms import service: %w", err)
78+
}
79+
80+
return nil
81+
},
82+
RunE: func(cmd *cobra.Command, args []string) error {
83+
ctx := cmd.Context()
84+
85+
passwordPrompter, err := utils.NewDefaultPasswordPrompter(
86+
"🔑 Input your Distribution Account Private Key (key will be hidden):", os.Stdin, os.Stdout)
87+
if err != nil {
88+
return fmt.Errorf("instantiating password prompter: %w", err)
89+
}
90+
91+
distributionAccountSeed, err := passwordPrompter.Run()
92+
if err != nil {
93+
return fmt.Errorf("getting distribution account seed input: %w", err)
94+
}
95+
96+
err = kmsImportService.ImportDistributionAccountKey(ctx, distributionAccountSeed)
97+
if err != nil {
98+
if errors.Is(err, services.ErrMismatchDistributionAccount) {
99+
return fmt.Errorf("the private key provided doesn't belong to the configured distribution account public key")
100+
}
101+
return fmt.Errorf("importing distribution account seed: %w", err)
102+
}
103+
104+
log.Ctx(ctx).Info("Successfully imported and encrypted the Distribution Account Private Key")
105+
return nil
106+
},
107+
}
108+
109+
cmd.AddCommand(importCmd)
110+
111+
if err := cfgOpts.Init(cmd); err != nil {
112+
log.Fatalf("Error initializing a config option: %s", err.Error())
113+
}
114+
115+
return cmd
116+
}

cmd/root.go

+1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ func init() {
3434
rootCmd.AddCommand((&ingestCmd{}).Command())
3535
rootCmd.AddCommand((&migrateCmd{}).Command())
3636
rootCmd.AddCommand((&channelAccountCmd{}).Command())
37+
rootCmd.AddCommand((&distributionAccountCmd{}).Command())
3738
}

cmd/serve.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,20 @@ import (
1212
"github.com/stellar/wallet-backend/internal/db"
1313
"github.com/stellar/wallet-backend/internal/serve"
1414
"github.com/stellar/wallet-backend/internal/signing"
15-
"github.com/stellar/wallet-backend/internal/signing/channelaccounts"
15+
signingutils "github.com/stellar/wallet-backend/internal/signing/utils"
1616
)
1717

1818
type serveCmd struct{}
1919

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

23-
var distributionAccountPrivateKey string
2423
cfgOpts := config.ConfigOptions{
2524
utils.DatabaseURLOption(&cfg.DatabaseURL),
2625
utils.LogLevelOption(&cfg.LogLevel),
2726
utils.NetworkPassphraseOption(&cfg.NetworkPassphrase),
2827
utils.BaseFeeOption(&cfg.BaseFee),
2928
utils.HorizonClientURLOption(&cfg.HorizonClientURL),
30-
utils.DistributionAccountPrivateKeyOption(&distributionAccountPrivateKey),
3129
utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase),
3230
{
3331
Name: "port",
@@ -79,6 +77,11 @@ func (c *serveCmd) Command() *cobra.Command {
7977
Required: true,
8078
},
8179
}
80+
81+
// Distribution Account Signature Client options
82+
signatureClientOpts := utils.SignatureClientOptions{}
83+
cfgOpts = append(cfgOpts, utils.DistributionAccountSignatureProviderOption(&signatureClientOpts)...)
84+
8285
cmd := &cobra.Command{
8386
Use: "serve",
8487
Short: "Run Wallet Backend server",
@@ -95,13 +98,15 @@ func (c *serveCmd) Command() *cobra.Command {
9598
return fmt.Errorf("opening connection pool: %w", err)
9699
}
97100

98-
signatureClient, err := signing.NewEnvSignatureClient(distributionAccountPrivateKey, cfg.NetworkPassphrase)
101+
signatureClientOpts.DBConnectionPool = dbConnectionPool
102+
signatureClientOpts.NetworkPassphrase = cfg.NetworkPassphrase
103+
signatureClient, err := utils.SignatureClientResolver(&signatureClientOpts)
99104
if err != nil {
100-
return fmt.Errorf("instantiating env signature client: %w", err)
105+
return fmt.Errorf("resolving distribution account signature client: %w", err)
101106
}
102107
cfg.DistributionAccountSignatureClient = signatureClient
103108

104-
channelAccountSignatureClient, err := signing.NewChannelAccountDBSignatureClient(dbConnectionPool, cfg.NetworkPassphrase, &channelaccounts.DefaultPrivateKeyEncrypter{}, cfg.EncryptionPassphrase)
109+
channelAccountSignatureClient, err := signing.NewChannelAccountDBSignatureClient(dbConnectionPool, cfg.NetworkPassphrase, &signingutils.DefaultPrivateKeyEncrypter{}, cfg.EncryptionPassphrase)
105110
if err != nil {
106111
return fmt.Errorf("instantiating channel account db signature client: %w", err)
107112
}

cmd/utils/custom_set_value.go

+27
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/stellar/go/support/config"
1515
"github.com/stellar/go/support/log"
1616
"github.com/stellar/wallet-backend/internal/entities"
17+
"github.com/stellar/wallet-backend/internal/signing"
1718
)
1819

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

68+
if privateKey == "" && !co.Required {
69+
return nil
70+
}
71+
6772
isValid := strkey.IsValidEd25519SecretSeed(privateKey)
6873
if !isValid {
6974
return fmt.Errorf("invalid private key provided in %s", co.Name)
@@ -141,3 +146,25 @@ func SetConfigOptionAssets(co *config.ConfigOption) error {
141146

142147
return nil
143148
}
149+
150+
func SetConfigOptionSignatureClientProvider(co *config.ConfigOption) error {
151+
scType := viper.GetString(co.Name)
152+
153+
scType = strings.TrimSpace(scType)
154+
if scType == "" {
155+
return fmt.Errorf("%s cannot be empty", co.Name)
156+
}
157+
158+
t := signing.SignatureClientType(scType)
159+
if !t.IsValid() {
160+
return fmt.Errorf("invalid %s value provided. expected: ENV or KMS", co.Name)
161+
}
162+
163+
key, ok := co.ConfigKey.(*signing.SignatureClientType)
164+
if !ok {
165+
return unexpectedTypeError(key, co)
166+
}
167+
*key = t
168+
169+
return nil
170+
}

cmd/utils/global_options.go

+62-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/stellar/go/network"
99
"github.com/stellar/go/support/config"
1010
"github.com/stellar/go/txnbuild"
11+
"github.com/stellar/wallet-backend/internal/signing"
1112
)
1213

1314
func DatabaseURLOption(configKey *string) *config.ConfigOption {
@@ -66,23 +67,76 @@ func HorizonClientURLOption(configKey *string) *config.ConfigOption {
6667
}
6768
}
6869

70+
func ChannelAccountEncryptionPassphraseOption(configKey *string) *config.ConfigOption {
71+
return &config.ConfigOption{
72+
Name: "channel-account-encryption-passphrase",
73+
Usage: "The Encryption Passphrase used to encrypt the channel accounts private key.",
74+
OptType: types.String,
75+
ConfigKey: configKey,
76+
Required: true,
77+
}
78+
}
79+
80+
func DistributionAccountPublicKeyOption(configKey *string) *config.ConfigOption {
81+
return &config.ConfigOption{
82+
Name: "distribution-account-public-key",
83+
Usage: "The Distribution Account public key.",
84+
OptType: types.String,
85+
CustomSetValue: SetConfigOptionStellarPublicKey,
86+
ConfigKey: configKey,
87+
Required: true,
88+
}
89+
}
90+
6991
func DistributionAccountPrivateKeyOption(configKey *string) *config.ConfigOption {
7092
return &config.ConfigOption{
7193
Name: "distribution-account-private-key",
72-
Usage: "The Distribution Account private key.",
94+
Usage: `The Distribution Account private key. It's required if the configured signature client is "ENV"`,
7395
OptType: types.String,
7496
CustomSetValue: SetConfigOptionStellarPrivateKey,
7597
ConfigKey: configKey,
76-
Required: true,
98+
Required: false,
7799
}
78100
}
79101

80-
func ChannelAccountEncryptionPassphraseOption(configKey *string) *config.ConfigOption {
102+
func DistributionAccountSignatureClientProviderOption(configKey *signing.SignatureClientType) *config.ConfigOption {
81103
return &config.ConfigOption{
82-
Name: "channel-account-encryption-passphrase",
83-
Usage: "The Encryption Passphrase used to encrypt the channel accounts private key.",
84-
OptType: types.String,
85-
ConfigKey: configKey,
86-
Required: true,
104+
Name: "distribution-account-signature-provider",
105+
Usage: "The Distribution Account Signature Client Provider. Options: ENV, KMS",
106+
OptType: types.String,
107+
CustomSetValue: SetConfigOptionSignatureClientProvider,
108+
ConfigKey: configKey,
109+
FlagDefault: string(signing.EnvSignatureClientType),
110+
Required: true,
111+
}
112+
}
113+
114+
func AWSOptions(awsRegionConfigKey *string, kmsKeyARN *string, required bool) config.ConfigOptions {
115+
awsOpts := config.ConfigOptions{
116+
{
117+
Name: "aws-region",
118+
Usage: `The AWS region. It's required if the configured signature client is "KMS"`,
119+
OptType: types.String,
120+
ConfigKey: awsRegionConfigKey,
121+
FlagDefault: "us-east-2",
122+
Required: required,
123+
},
124+
{
125+
Name: "kms-key-arn",
126+
Usage: `The KMS Key ARN. It's required if the configured signature client is "KMS"`,
127+
OptType: types.String,
128+
ConfigKey: kmsKeyARN,
129+
Required: required,
130+
},
87131
}
132+
return awsOpts
133+
}
134+
135+
func DistributionAccountSignatureProviderOption(scOpts *SignatureClientOptions) config.ConfigOptions {
136+
opts := config.ConfigOptions{}
137+
opts = append(opts, DistributionAccountPublicKeyOption(&scOpts.DistributionAccountPublicKey))
138+
opts = append(opts, DistributionAccountSignatureClientProviderOption(&scOpts.Type))
139+
opts = append(opts, DistributionAccountPrivateKeyOption(&scOpts.DistributionAccountSecretKey))
140+
opts = append(opts, AWSOptions(&scOpts.AWSRegion, &scOpts.KMSKeyARN, false)...)
141+
return opts
88142
}

0 commit comments

Comments
 (0)