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

Implement KMS AWS master key loading #552

Merged
merged 6 commits into from
Jul 13, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 3 additions & 3 deletions configs/acra-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ keystore_cache_size: 1000
# KMS credentials JSON file path
kms_credentials_path:

# KMS type for using: <aws>
kms_type:

# Log to stderr if true
log_to_console: true

Expand All @@ -95,9 +98,6 @@ log_to_file:
# Logging format: plaintext, json or CEF
logging_format: plaintext

# KMS Key identifier in Tink's format
master_key_encryption_key_uri:

# Handle MySQL connections
mysql_enable: false

Expand Down
2 changes: 1 addition & 1 deletion keystore/keyloader/kms/aws_enable.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ import (
)

func init() {
kms.RegisterEncryptorCreator(aws.KeyIdentifierPrefix, aws.NewEncryptor)
kms.RegisterEncryptorCreator(kms.TypeAWS, aws.NewEncryptor)
}
19 changes: 10 additions & 9 deletions keystore/keyloader/kms/kms_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ package kms

import (
"flag"
"fmt"
"github.com/cossacklabs/acra/keystore/kms"
"strings"

"github.com/cossacklabs/acra/keystore/keyloader"
log "github.com/sirupsen/logrus"
)

const (
kmsKeyURIFlag = "master_key_encryption_key_uri"
)
const kmsTypeFlag = "kms_type"

// CLIOptions keep command-line options related to KMS ACRA_MASTER_KEY loading.
type CLIOptions struct {
KeyIdentifierURI string
CredentialsPath string
KMSType string
CredentialsPath string
}

var cliOptions CLIOptions
Expand All @@ -30,8 +31,8 @@ func (options *CLIOptions) RegisterCLIParameters(flags *flag.FlagSet, prefix str
if description != "" {
description = " (" + description + ")"
}
if flags.Lookup(prefix+kmsKeyURIFlag) == nil {
flags.StringVar(&options.KeyIdentifierURI, prefix+kmsKeyURIFlag, "", "KMS Key identifier in Tink's format"+description)
if flags.Lookup(prefix+kmsTypeFlag) == nil {
flags.StringVar(&options.KMSType, prefix+kmsTypeFlag, "", fmt.Sprintf("KMS type for using: <%s>", strings.Join(kms.SupportedTypes, "|")+description))
// TODO: how to better provide an example of configuration files for different providers
flags.StringVar(&options.CredentialsPath, prefix+"kms_credentials_path", "", "KMS credentials JSON file path"+description)
}
Expand All @@ -44,10 +45,10 @@ func GetCLIParameters() *CLIOptions {

// New create MasterKeyLoader from kms.CLIOptions - implementation of keyloader.CliMasterKeyLoaderCreator interface
func (options *CLIOptions) New() (keyloader.MasterKeyLoader, error) {
if options.KeyIdentifierURI == "" {
if options.KMSType == "" {
return nil, nil
}

log.Infoln("Using KMS for ACRA_MASTER_KEY loading...")
return NewLoader(options.CredentialsPath, options.KeyIdentifierURI)
return NewLoader(options.CredentialsPath, options.KMSType)
}
26 changes: 10 additions & 16 deletions keystore/keyloader/kms/kms_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kms
import (
"context"
"crypto/subtle"

"github.com/cossacklabs/acra/keystore"
keystoreCE "github.com/cossacklabs/acra/keystore"
"github.com/cossacklabs/acra/keystore/kms"
Expand All @@ -13,20 +14,14 @@ import (

// Loader is implementation of MasterKeyLoader for kms
type Loader struct {
keyID kms.KeyIdentifier
encryptor kms.Encryptor
}

// NewLoader create new kms MasterKeyLoader
func NewLoader(credentialPath, keyIdentifierURI string) (*Loader, error) {
keyID, err := kms.NewKeyIdentifierFromURI(keyIdentifierURI)
if err != nil {
return nil, err
}

createEncryptor, ok := kms.GetEncryptorCreator(keyID.Prefix())
func NewLoader(credentialPath, kmsType string) (*Loader, error) {
createEncryptor, ok := kms.GetEncryptorCreator(kmsType)
if !ok {
log.Errorln("Unknown key ID provided")
log.Errorf("Unknown KMS type provided %s", kmsType)
return nil, nil
}

Expand All @@ -38,16 +33,15 @@ func NewLoader(credentialPath, keyIdentifierURI string) (*Loader, error) {

log.Infof("Initialized %s MasterKeyLoader", encryptor.ID())
return &Loader{
keyID: keyID,
encryptor: encryptor,
}, nil
}

// LoadMasterKey implementation kms MasterKeyLoader for loading AcraMasterKey for keystore v1
func (loader *Loader) LoadMasterKey() ([]byte, error) {
rawKey, err := loader.decryptWithKMSKey(loader.keyID)
rawKey, err := loader.decryptWithKMSKey(kms.AcraMasterKeyKEKID)
if err != nil {
log.WithError(err).Warnf("Failed to decrypt ACRA_MASTER_KEY with KMS keyID %s", loader.keyID.ID())
log.WithError(err).Warnf("Failed to decrypt ACRA_MASTER_KEY with KMS keyID %s", kms.AcraMasterKeyKEKID)
return nil, err
}

Expand All @@ -61,9 +55,9 @@ func (loader *Loader) LoadMasterKey() ([]byte, error) {

// LoadMasterKeys implementation kms MasterKeyLoader for loading AcraMasterKey for keystore v2
func (loader *Loader) LoadMasterKeys() (encryption []byte, signature []byte, err error) {
rawKey, err := loader.decryptWithKMSKey(loader.keyID)
rawKey, err := loader.decryptWithKMSKey(kms.AcraMasterKeyKEKID)
if err != nil {
log.WithError(err).Warnf("Failed to decrypt ACRA_MASTER_KEY with KMS keyID %s", loader.keyID.ID())
log.WithError(err).Warnf("Failed to decrypt ACRA_MASTER_KEY with KMS keyID %s", kms.AcraMasterKeyKEKID)
return nil, nil, err
}

Expand Down Expand Up @@ -93,14 +87,14 @@ func (loader *Loader) LoadMasterKeys() (encryption []byte, signature []byte, err
return keys.Encryption, keys.Signature, nil
}

func (loader *Loader) decryptWithKMSKey(keyID kms.KeyIdentifier) ([]byte, error) {
func (loader *Loader) decryptWithKMSKey(keyID string) ([]byte, error) {
Lagovas marked this conversation as resolved.
Show resolved Hide resolved
cipherMasterKey, err := keystore.GetMasterKeyFromEnvironmentVariable(keystore.AcraMasterKeyVarName)
if err != nil {
return nil, err
}

ctx, _ := context.WithTimeout(context.Background(), network.DefaultNetworkTimeout)
masterKey, err := loader.encryptor.Decrypt(ctx, keyID.ID(), cipherMasterKey)
masterKey, err := loader.encryptor.Decrypt(ctx, keyID, cipherMasterKey)
if err != nil {
return nil, err
}
Expand Down
8 changes: 3 additions & 5 deletions keystore/keyloader/kms/kms_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import (

"github.com/cossacklabs/acra/keystore"
"github.com/cossacklabs/acra/keystore/kms"
"github.com/cossacklabs/acra/keystore/kms/aws"
"github.com/cossacklabs/acra/keystore/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestSuccessMasterKeyLoading(t *testing.T) {
kmsEncryptor := &mocks.Encryptor{}
keyID := "aws-kms://arn:aws:kms:eu-west-2:account:key/f1cc9aa8-a8fd-49a3-a123-15e7b10127d4"

key := make([]byte, 64)
_, err := rand.Read(key)
Expand All @@ -28,14 +26,14 @@ func TestSuccessMasterKeyLoading(t *testing.T) {
defer os.Unsetenv(keystore.AcraMasterKeyVarName)

kmsEncryptor.On("ID").Return("mocked KMS encryptor")
kmsEncryptor.On("Decrypt", mock.Anything, "arn:aws:kms:eu-west-2:account:key/f1cc9aa8-a8fd-49a3-a123-15e7b10127d4", key).Return([]byte(masterKey), nil)
kmsEncryptor.On("Decrypt", mock.Anything, kms.AcraMasterKeyKEKID, key).Return([]byte(masterKey), nil)

encryptorCreator := func(path string) (kms.Encryptor, error) {
return kmsEncryptor, nil
}
kms.RegisterEncryptorCreator(aws.KeyIdentifierPrefix, encryptorCreator)
kms.RegisterEncryptorCreator(kms.TypeAWS, encryptorCreator)

kmsLoader, err := NewLoader("config.json", keyID)
kmsLoader, err := NewLoader("config.json", kms.TypeAWS)
assert.NoError(t, err)

loadedMasterKey, err := kmsLoader.LoadMasterKey()
Expand Down
6 changes: 2 additions & 4 deletions keystore/kms/aws/encryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package aws
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"

"github.com/aws/aws-sdk-go-v2/aws"
Expand All @@ -20,9 +21,6 @@ type Configuration struct {
Endpoint string `json:"endpoint"`
}

// KeyIdentifierPrefix describe prefix used for AWS KMS KeyID
const KeyIdentifierPrefix = "aws-kms:"

// Encryptor implementation of AWS Encryptor
type Encryptor struct {
client *kms.Client
Expand Down Expand Up @@ -67,7 +65,7 @@ func (e *Encryptor) Encrypt(ctx context.Context, keyID string, data []byte) ([]b
func (e *Encryptor) Decrypt(ctx context.Context, keyID string, blob []byte) ([]byte, error) {
input := &kms.DecryptInput{
CiphertextBlob: blob,
KeyId: aws.String(keyID),
KeyId: aws.String(fmt.Sprintf("alias/%s", keyID)),
Lagovas marked this conversation as resolved.
Show resolved Hide resolved
}

result, err := e.client.Decrypt(ctx, input)
Expand Down
46 changes: 7 additions & 39 deletions keystore/kms/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package kms

import (
"context"
"errors"
"strings"
"sync"

log "github.com/sirupsen/logrus"
Expand All @@ -29,12 +27,6 @@ func GetEncryptorCreator(encryptorID string) (EncryptorCreateFunc, bool) {
return creator, ok
}

// KeyID validation related errors
var (
ErrInvalidKeyIDFormat = errors.New("invalid keyID URI format")
ErrUnsupportedKeyIDFormat = errors.New("unsupported keyID URI format")
)

//go:generate mockery --name Encryptor --output ../mocks --filename KmsEncryptor.go

// Encryptor is main kms encryptor interface
Expand All @@ -44,37 +36,13 @@ type Encryptor interface {
Decrypt(ctx context.Context, keyID string, data []byte) ([]byte, error)
}

// KeyIdentifier represent KMS KeyID in Tink format
// https://developers.google.com/tink/get-key-uri
type KeyIdentifier struct {
id, prefix string
}

// NewKeyIdentifierFromURI create new KeyIdentifier from provided value
// expected value: `aws-kms://arn:aws:kms:<region>:<account-id>:key/<key-id>`
func NewKeyIdentifierFromURI(value string) (KeyIdentifier, error) {
splits := strings.Split(value, "//")
if len(splits) != 2 {
return KeyIdentifier{}, ErrInvalidKeyIDFormat
}
prefix := splits[0]
_, ok := GetEncryptorCreator(splits[0])
if !ok {
return KeyIdentifier{}, ErrUnsupportedKeyIDFormat
}
// AcraMasterKeyKEKID represent ID/alias of encryption key used for MasterKey loading
const AcraMasterKeyKEKID = "acra_master_key"

return KeyIdentifier{
prefix: prefix,
id: splits[1],
}, nil
}

// ID return keyID without prefix
func (k KeyIdentifier) ID() string {
return k.id
}
// TypeAWS supported KMS type AWS
const TypeAWS = "aws"

// Prefix return keyID prefix
func (k KeyIdentifier) Prefix() string {
return k.prefix
// SupportedTypes contains all possible values for flag `--kms_type`
var SupportedTypes = []string{
TypeAWS,
}
19 changes: 18 additions & 1 deletion tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,20 @@ def create_key(self):
response = self.kms_client.create_key(Description='AcraMasterKey KEK')
return response['KeyMetadata']['Arn']

def create_alias(self, keyId, alias_name):
self.kms_client.create_alias(
AliasName=alias_name,
TargetKeyId=keyId
)

def delete_alias(self, alias_name):
self.kms_client.delete_alias(
AliasName=alias_name,
)

def close(self):
self.kms_client.close()

def disable_key(self, keyId):
self.kms_client.disable_key(KeyId=keyId)

Expand Down Expand Up @@ -3085,6 +3099,7 @@ def setUp(self):

self.kms_client = AWSKMSClient()
self.master_key_kek_uri = self.kms_client.create_key()
self.kms_client.create_alias(keyId=self.master_key_kek_uri, alias_name='alias/acra_master_key')

master_key = b64decode(get_master_key())
response = self.kms_client.encrypt(keyId=self.master_key_kek_uri, data=master_key)
Expand All @@ -3111,15 +3126,17 @@ def create_configuration_file(self):
def fork_acra(self, popen_kwargs: dict = None, **acra_kwargs: dict):
args = {
'kms_credentials_path': self.config_file,
'master_key_encryption_key_uri': '{0}//{1}'.format('aws-kms:', self.master_key_kek_uri)
'kms_type': 'aws'
}
os.environ[ACRA_MASTER_KEY_VAR_NAME] = self.master_key_ciphertext
acra_kwargs.update(args)
return super(AWSKMSMasterKeyLoaderMixin, self).fork_acra(popen_kwargs, **acra_kwargs)

def tearDown(self):
super().tearDown()
self.kms_client.delete_alias(alias_name='alias/acra_master_key')
self.kms_client.disable_key(keyId=self.master_key_kek_uri)
self.kms_client.close()
os.environ[ACRA_MASTER_KEY_VAR_NAME] = get_master_key()


Expand Down