Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support AES encryption of the seda_keys.json file #468

Merged
merged 5 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/abci/vote_extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ func TestExtendVerifyVoteHandlers(t *testing.T) {
tmpDir := t.TempDir()

valAddr := sdk.ValAddress(simtestutil.CreateRandomAccounts(1)[0])
pubKeys, err := utils.GenerateSEDAKeys(valAddr, tmpDir)
pubKeys, err := utils.GenerateSEDAKeys(valAddr, tmpDir, "", false)
require.NoError(t, err)
signer, err := utils.LoadSEDASigner(filepath.Join(tmpDir, utils.SEDAKeyFileName))
signer, err := utils.LoadSEDASigner(filepath.Join(tmpDir, utils.SEDAKeyFileName), true)
require.NoError(t, err)

secp256k1PubKey := pubKeys[utils.SEDAKeyIndexSecp256k1].PubKey
Expand Down
2 changes: 1 addition & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,7 @@ func NewApp(

// Register ABCI handlers for batch signing.
pvKeyFile := filepath.Join(homePath, cast.ToString(appOpts.Get("priv_validator_key_file")))
signer, err := utils.LoadSEDASigner(filepath.Join(filepath.Dir(pvKeyFile), utils.SEDAKeyFileName))
signer, err := utils.LoadSEDASigner(filepath.Join(filepath.Dir(pvKeyFile), utils.SEDAKeyFileName), utils.ShouldAllowUnencryptedSedaKeys(appOpts))
if err != nil {
app.Logger().Error("error loading SEDA signer", "err", err)
}
Expand Down
150 changes: 133 additions & 17 deletions app/utils/seda_keys.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package utils

import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
Expand All @@ -11,14 +14,48 @@ import (
"sort"

ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/spf13/cast"

cmtos "github.com/cometbft/cometbft/libs/os"

servertypes "github.com/cosmos/cosmos-sdk/server/types"
sdk "github.com/cosmos/cosmos-sdk/types"

pubkeytypes "github.com/sedaprotocol/seda-chain/x/pubkey/types"
)

const (
// FlagAllowUnencryptedSedaKeys is a flag that allows unencrypted SEDA keys.
FlagAllowUnencryptedSedaKeys = "allow-unencrypted-seda-keys"
// EnvAllowUnencryptedSedaKeys is an environment variable that allows unencrypted SEDA keys.
EnvAllowUnencryptedSedaKeys = "SEDA_ALLOW_UNENCRYPTED_KEYS"
// SEDAKeyEncryptionKeyEnvVar is the environment variable that should contain the SEDA key encryption key.
SEDAKeyEncryptionKeyEnvVar = "SEDA_KEYS_ENCRYPTION_KEY"
)

func ShouldAllowUnencryptedSedaKeys(appOpts servertypes.AppOptions) bool {
allowUnencryptedFlag := cast.ToBool(appOpts.Get(FlagAllowUnencryptedSedaKeys))
_, allowUnencryptedInEnv := os.LookupEnv(EnvAllowUnencryptedSedaKeys)

return allowUnencryptedFlag || allowUnencryptedInEnv
}

// ReadSEDAKeyEncryptionKeyFromEnv reads the SEDA key encryption key from
// the environment variable. Returns an empty string if the environment
// variable is not set.
func ReadSEDAKeyEncryptionKeyFromEnv() string {
return os.Getenv(SEDAKeyEncryptionKeyEnvVar)
}

func GenerateSEDAKeyEncryptionKey() (string, error) {
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(key), nil
}

// SEDAKeyIndex enumerates the SEDA key indices.
type SEDAKeyIndex uint32

Expand Down Expand Up @@ -106,10 +143,11 @@ func (k *indexedPrivKey) UnmarshalJSON(data []byte) error {
}

// saveSEDAKeyFile saves a given list of indexedPrivKey in the directory
// at dirPath.
func saveSEDAKeyFile(keys []indexedPrivKey, valAddr sdk.ValAddress, dirPath string) error {
// at dirPath. When encryptionKey is not empty, the file is encrypted
// using the provided key and stored as base64 encoded.
func saveSEDAKeyFile(keys []indexedPrivKey, valAddr sdk.ValAddress, dirPath string, encryptionKey string, forceKeyFile bool) error {
savePath := filepath.Join(dirPath, SEDAKeyFileName)
if cmtos.FileExists(savePath) {
if SEDAKeyFileExists(dirPath) && !forceKeyFile {
return fmt.Errorf("SEDA key file already exists at %s", savePath)
}
err := cmtos.EnsureDir(filepath.Dir(savePath), 0o700)
Expand All @@ -125,19 +163,46 @@ func saveSEDAKeyFile(keys []indexedPrivKey, valAddr sdk.ValAddress, dirPath stri
return fmt.Errorf("failed to marshal SEDA keys: %v", err)
}

if encryptionKey != "" {
encryptedData, err := encryptBytes(jsonBytes, encryptionKey)
if err != nil {
return fmt.Errorf("failed to encrypt SEDA keys: %v", err)
}
jsonBytes = []byte(base64.StdEncoding.EncodeToString(encryptedData))
}

err = os.WriteFile(savePath, jsonBytes, 0o600)
if err != nil {
return fmt.Errorf("failed to write SEDA key file: %v", err)
}
return nil
}

// loadSEDAKeyFile loads the SEDA key file from the given path.
func loadSEDAKeyFile(loadPath string) (sedaKeyFile, error) {
func SEDAKeyFileExists(dirPath string) bool {
return cmtos.FileExists(filepath.Join(dirPath, SEDAKeyFileName))
}

// loadSEDAKeyFile loads the SEDA key file from the given path. When
// encryptionKey is not empty, the file is processed as base64 encoded
// and then decrypted using the provided key.
func loadSEDAKeyFile(loadPath string, encryptionKey string) (sedaKeyFile, error) {
keysJSONBytes, err := os.ReadFile(loadPath)
if err != nil {
return sedaKeyFile{}, fmt.Errorf("failed to read SEDA keys from %v: %v", loadPath, err)
}

if encryptionKey != "" {
decodedBytes, err := base64.StdEncoding.DecodeString(string(keysJSONBytes))
if err != nil {
return sedaKeyFile{}, fmt.Errorf("failed to base64 decode SEDA keys: %v", err)
}
decryptedData, err := decryptBytes(decodedBytes, encryptionKey)
if err != nil {
return sedaKeyFile{}, fmt.Errorf("failed to decrypt SEDA keys: %v", err)
}
keysJSONBytes = decryptedData
}

var keyFile sedaKeyFile
err = json.Unmarshal(keysJSONBytes, &keyFile)
if err != nil {
Expand All @@ -147,16 +212,13 @@ func loadSEDAKeyFile(loadPath string) (sedaKeyFile, error) {
}

// LoadSEDAPubKeys loads the SEDA key file from the given path and
// returns a list of index-public key pairs.
func LoadSEDAPubKeys(loadPath string) ([]pubkeytypes.IndexedPubKey, error) {
keysJSONBytes, err := os.ReadFile(loadPath)
// returns a list of index-public key pairs. When encryptionKey is not
// empty, the file is processed as base64 encoded and then decrypted
// using the provided key.
func LoadSEDAPubKeys(loadPath string, encryptionKey string) ([]pubkeytypes.IndexedPubKey, error) {
keyFile, err := loadSEDAKeyFile(loadPath, encryptionKey)
if err != nil {
return nil, fmt.Errorf("failed to read SEDA keys from %v: %v", loadPath, err)
}
var keyFile sedaKeyFile
err = json.Unmarshal(keysJSONBytes, &keyFile)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal SEDA keys from %v: %v", loadPath, err)
return nil, err
}

result := make([]pubkeytypes.IndexedPubKey, len(keyFile.Keys))
Expand All @@ -174,8 +236,11 @@ func LoadSEDAPubKeys(loadPath string) ([]pubkeytypes.IndexedPubKey, error) {
// GenerateSEDAKeys generates a new set of SEDA keys and saves them to
// the SEDA key file, along with the provided validator address. It
// returns the resulting index-public key pairs. The key file is stored
// in the directory given by dirPath.
func GenerateSEDAKeys(valAddr sdk.ValAddress, dirPath string) ([]pubkeytypes.IndexedPubKey, error) {
// in the directory given by dirPath. When encryptionKey is not empty,
// the file is encrypted using the provided key and stored as base64
// encoded. If forceKeyFile is true, the key file is overwritten if it
// already exists.
func GenerateSEDAKeys(valAddr sdk.ValAddress, dirPath string, encryptionKey string, forceKeyFile bool) ([]pubkeytypes.IndexedPubKey, error) {
privKeys := make([]indexedPrivKey, 0, len(sedaKeyGenerators))
pubKeys := make([]pubkeytypes.IndexedPubKey, 0, len(sedaKeyGenerators))
for keyIndex, generator := range sedaKeyGenerators {
Expand All @@ -194,7 +259,7 @@ func GenerateSEDAKeys(valAddr sdk.ValAddress, dirPath string) ([]pubkeytypes.Ind
}

// The key file is placed in the same directory as the validator key file.
err := saveSEDAKeyFile(privKeys, valAddr, dirPath)
err := saveSEDAKeyFile(privKeys, valAddr, dirPath, encryptionKey, forceKeyFile)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -235,3 +300,54 @@ func PubKeyToEthAddress(uncompressed []byte) ([]byte, error) {
}
return ethcrypto.Keccak256(uncompressed[1:])[12:], nil
}

func encryptBytes(data []byte, key string) ([]byte, error) {
keyBytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, err
}

aes, err := aes.NewCipher(keyBytes)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(aes)
if err != nil {
return nil, err
}

nonce := make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
if err != nil {
return nil, err
}

return gcm.Seal(nonce, nonce, data, nil), nil
}

func decryptBytes(data []byte, key string) ([]byte, error) {
keyBytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return nil, err
}

aes, err := aes.NewCipher(keyBytes)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(aes)
if err != nil {
return nil, err
}

nonceSize := gcm.NonceSize()
nonce, encryptedData := data[:nonceSize], data[nonceSize:]

decryptedData, err := gcm.Open(nil, nonce, encryptedData, nil)
if err != nil {
return nil, err
}
return decryptedData, nil
}
107 changes: 107 additions & 0 deletions app/utils/seda_keys_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package utils_test

import (
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/suite"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/stretchr/testify/require"

"github.com/sedaprotocol/seda-chain/app/params"
"github.com/sedaprotocol/seda-chain/app/utils"
)

type SEDAKeysTestSuite struct {
suite.Suite
}

func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(SEDAKeysTestSuite))
}

func (s *SEDAKeysTestSuite) SetupSuite() {
config := sdk.GetConfig()
config.SetBech32PrefixForAccount(params.Bech32PrefixAccAddr, params.Bech32PrefixAccPub)
config.SetBech32PrefixForValidator(params.Bech32PrefixValAddr, params.Bech32PrefixValPub)
config.SetBech32PrefixForConsensusNode(params.Bech32PrefixConsAddr, params.Bech32PrefixConsPub)
config.Seal()
}

func (s *SEDAKeysTestSuite) TestSEDAKeyEncryptionDecryption() {
valAddr, err := sdk.ValAddressFromBech32("sedavaloper12rype4zl8wxcgqwl237fll6hvufkgcj8act8xw")
require.NoError(s.T(), err)

encryptionKey, err := utils.GenerateSEDAKeyEncryptionKey()
require.NoError(s.T(), err)

tempDir := s.T().TempDir()
generatedKeys, err := utils.GenerateSEDAKeys(valAddr, tempDir, encryptionKey, false)

s.Require().NoError(err)
s.Require().Equal(utils.SEDAKeyIndex(generatedKeys[0].Index), utils.SEDAKeyIndexSecp256k1)
s.Require().NotEmpty(generatedKeys[0].PubKey, "public key should not be empty")

keyfilePath := filepath.Join(tempDir, utils.SEDAKeyFileName)

invalidKey, err := utils.GenerateSEDAKeyEncryptionKey()
s.Require().NoError(err)

_, err = utils.LoadSEDAPubKeys(keyfilePath, invalidKey)
s.Require().ErrorContains(err, "cipher: message authentication failed")

loadedKeys, err := utils.LoadSEDAPubKeys(keyfilePath, encryptionKey)
s.Require().NoError(err)
s.Require().Equal(generatedKeys[0].PubKey, loadedKeys[0].PubKey)
}

func (s *SEDAKeysTestSuite) TestSEDAKeyDecryptionExistingFile() {
tempDir := s.T().TempDir()
err := os.WriteFile(filepath.Join(tempDir, utils.SEDAKeyFileName), []byte("kNYhCAjfN9BhJ46iYzJWCUXn9efOAGf30D81UjF5tRlRtdiziW1zGVK+6ehxeJXKcPAmWjQkTxAKcJv7ozAA0xdleR4yO6HakROtFRXlOBy3K9Fv6rkDfCmbIUUjOH9oGP2F5+ldKeE5030MOdNORWUKW7fIlnKUyBWTZfLSmsKi+iCaIyZ/bFh2+NDiESPHAYl+X8t+SKKy6MgAwarrW9W1/6enNLoVmF8dAJ1dhxeKyXF/aXWKR7HaMRwe7V1NjfnaFcI09CeibpWud9rYKhbjV3K0/RdBobjPTIHAnLd5erh/3eVo9RGm8bC8a97obKm68lDernSN9HvjoTO3QlvI0k7cVDAhiuphS4qlgjOVW+eWm+S5dlD2gpCExcmrqxbggLOtjoZbQyrKhQFmfn5UGonoDTSbwtbZZtvY1N48AVT4eueReBWumcipO0ViWnkxLNIJ8vFA"), 0o600)
s.Require().NoError(err)

_, err = utils.LoadSEDAPubKeys(filepath.Join(tempDir, utils.SEDAKeyFileName), "xmp1EDn7ndgZIdgwupJ9yfDWlSssubKpgo2ZHqjx+4w=")
s.Require().ErrorContains(err, "cipher: message authentication failed")

keys, err := utils.LoadSEDAPubKeys(filepath.Join(tempDir, utils.SEDAKeyFileName), "La1PSNwUBZXEoIQ1CM0VF+kRr9vqforxE97afYdTF+c=")
s.Require().NoError(err)
s.Require().Equal(hex.EncodeToString(keys[0].PubKey), "04be41e55492d9d823c435b6b6801413223b31fdfa0318d2dea51e1886215e8664e234c34afa7af32ec02a1d0289ce656bab3ed106646836c9d26ce35968b2ff68")
}

func (s *SEDAKeysTestSuite) TestSEDAKeyWithoutEncryption() {
valAddr, err := sdk.ValAddressFromBech32("sedavaloper12rype4zl8wxcgqwl237fll6hvufkgcj8act8xw")
require.NoError(s.T(), err)

tempDir := s.T().TempDir()
generatedKeys, err := utils.GenerateSEDAKeys(valAddr, tempDir, "", false)

s.Require().NoError(err)
s.Require().Equal(utils.SEDAKeyIndex(generatedKeys[0].Index), utils.SEDAKeyIndexSecp256k1)
s.Require().NotEmpty(generatedKeys[0].PubKey, "public key should not be empty")

keyfilePath := filepath.Join(tempDir, utils.SEDAKeyFileName)
keys, err := os.ReadFile(keyfilePath)
s.Require().NoError(err)

// We only verify the top level of the JSON schema, this should be enough
// to ensure that the file was not encrypted.
type jsonSchema struct {
ValidatorAddr sdk.ValAddress `json:"validator_addr"`
Keys []interface{} `json:"keys"`
}

var sedaKeyFile jsonSchema
s.Require().NoError(json.Unmarshal(keys, &sedaKeyFile))
s.Require().Equal(sedaKeyFile.ValidatorAddr, valAddr)
s.Require().Equal(len(sedaKeyFile.Keys), 1)

// Test that the file can be loaded without encryption.
loadedKeys, err := utils.LoadSEDAPubKeys(keyfilePath, "")
s.Require().NoError(err)
s.Require().Equal(generatedKeys[0].PubKey, loadedKeys[0].PubKey)
}
20 changes: 15 additions & 5 deletions app/utils/seda_signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ type sedaKeys struct {

// LoadSEDASigner loads the SEDA keys from a given file path and
// returns a SEDASigner interface.
func LoadSEDASigner(keyFilePath string) (SEDASigner, error) {
keys, err := loadSEDAKeys(keyFilePath)
func LoadSEDASigner(keyFilePath string, allowUnencrypted bool) (SEDASigner, error) {
keys, err := loadSEDAKeys(keyFilePath, allowUnencrypted)
if err != nil {
keys.keyPath = keyFilePath
keys.isLoaded = false
Expand All @@ -41,8 +41,15 @@ func LoadSEDASigner(keyFilePath string) (SEDASigner, error) {
return &keys, nil
}

func loadSEDAKeys(keyFilePath string) (keys sedaKeys, err error) {
keyFile, err := loadSEDAKeyFile(keyFilePath)
func loadSEDAKeys(keyFilePath string, allowUnencrypted bool) (keys sedaKeys, err error) {
encryptionKey := ReadSEDAKeyEncryptionKeyFromEnv()
if encryptionKey == "" && !allowUnencrypted {
panic(fmt.Sprintf("SEDA key encryption key is not set, set the %s environment variable or run with --%s", SEDAKeyEncryptionKeyEnvVar, FlagAllowUnencryptedSedaKeys))
} else if encryptionKey != "" && allowUnencrypted {
panic(fmt.Sprintf("SEDA key encryption key is set, but --%s flag is also set", FlagAllowUnencryptedSedaKeys))
}

keyFile, err := loadSEDAKeyFile(keyFilePath, encryptionKey)
if err != nil {
return keys, err
}
Expand Down Expand Up @@ -124,7 +131,10 @@ func (s *sedaKeys) IsLoaded() bool {

// Reload reloads the signer from the key file.
func (s *sedaKeys) reload() error {
keys, err := loadSEDAKeys(s.keyPath)
// Reload should run from the same process as the one that loaded the signer,
// so the check for the encryption key should already have passed if we're
// hitting this function.
keys, err := loadSEDAKeys(s.keyPath, true)
if err != nil {
s.valAddr = nil
s.keys = nil
Expand Down
Loading
Loading