1
0
mirror of https://github.com/getsops/sops.git synced 2026-02-05 12:45:21 +01:00
Files
sops/kms/keysource.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

485 lines
14 KiB
Go
Raw Permalink Normal View History

2017-09-12 20:01:12 -07:00
/*
Package kms contains an implementation of the github.com/getsops/sops/v3.MasterKey
2022-05-27 22:35:50 +02:00
interface that encrypts and decrypts the data key using AWS KMS with the SDK
for Go V2.
2017-09-12 20:01:12 -07:00
*/
package kms // import "github.com/getsops/sops/v3/kms"
import (
2022-05-27 22:35:50 +02:00
"context"
"encoding/base64"
"fmt"
"net/http"
"os"
"regexp"
"sort"
"strings"
"time"
2022-05-27 22:35:50 +02:00
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/sirupsen/logrus"
"github.com/getsops/sops/v3/logging"
2022-05-27 22:35:50 +02:00
)
2017-09-07 10:49:27 -07:00
2022-05-27 22:35:50 +02:00
const (
// arnRegex matches an AWS ARN, for example:
// "arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48".
arnRegex = `^arn:aws[\w-]*:kms:(.+):[0-9]+:(key|alias)/.+$`
// stsSessionRegex matches an AWS STS session name, for example:
// "john_s", "sops@42WQm042".
stsSessionRegex = "[^a-zA-Z0-9=,.@-_]+"
// roleSessionNameLengthLimit is the AWS role session name length limit.
roleSessionNameLengthLimit = 64
// kmsTTL is the duration after which a MasterKey requires rotation.
kmsTTL = time.Hour * 24 * 30 * 6
// KeyTypeIdentifier is the string used to identify an AWS KMS MasterKey.
KeyTypeIdentifier = "kms"
)
2022-05-27 22:35:50 +02:00
var (
// log is the global logger for any AWS KMS MasterKey.
log *logrus.Logger
// osHostname returns the hostname as reported by the kernel.
osHostname = os.Hostname
)
2017-09-06 17:36:39 -07:00
func init() {
2017-09-25 10:05:40 -07:00
log = logging.NewLogger("AWSKMS")
2017-09-06 17:36:39 -07:00
}
2022-05-27 22:35:50 +02:00
// MasterKey is an AWS KMS key used to encrypt and decrypt SOPS' data key using
// AWS SDK for Go V2.
type MasterKey struct {
2022-05-27 22:35:50 +02:00
// Arn associated with the AWS KMS key.
Arn string
// Role ARN used to assume a role through AWS STS.
Role string
// EncryptedKey stores the data key in it's encrypted form.
EncryptedKey string
// CreationDate is when this MasterKey was created.
CreationDate time.Time
// EncryptionContext provides additional context about the data key.
// Ref: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context
EncryptionContext map[string]*string
2022-05-27 22:35:50 +02:00
// AwsProfile is the profile to use for loading configuration and credentials.
// Ref: https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-profiles
AwsProfile string
2022-05-27 22:35:50 +02:00
// credentialsProvider is used to configure the AWS client config with
// credentials. It can be injected by a (local) keyservice.KeyServiceServer
// using CredentialsProvider.ApplyToMasterKey. If nil, the default client is used
// which utilizes runtime environmental values.
credentialsProvider aws.CredentialsProvider
// baseEndpoint can be used to override the endpoint the AWS client resolves
2022-05-27 22:35:50 +02:00
// to by default. This is mostly used for testing purposes as it can not be
// injected using e.g. an environment variable. The field is not publicly
// exposed, nor configurable.
baseEndpoint string
// httpClient is used to override the default HTTP client used by the AWS client.
httpClient *http.Client
}
2022-05-27 22:35:50 +02:00
// NewMasterKey creates a new MasterKey from an ARN, role and context, setting
// the creation date to the current date.
func NewMasterKey(arn string, role string, context map[string]*string) *MasterKey {
return &MasterKey{
Arn: arn,
Role: role,
EncryptionContext: context,
CreationDate: time.Now().UTC(),
}
}
// NewMasterKeyWithProfile creates a new MasterKey from an ARN, role, context
// and awsProfile, setting the creation date to the current date.
func NewMasterKeyWithProfile(arn string, role string, context map[string]*string, awsProfile string) *MasterKey {
k := NewMasterKey(arn, role, context)
k.AwsProfile = awsProfile
return k
}
2022-05-27 22:35:50 +02:00
// NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that
// ARN.
2019-01-25 12:42:41 +00:00
func NewMasterKeyFromArn(arn string, context map[string]*string, awsProfile string) *MasterKey {
2022-05-27 22:35:50 +02:00
key := &MasterKey{}
arn = strings.Replace(arn, " ", "", -1)
2022-05-27 22:35:50 +02:00
key.Arn = arn
roleIndex := strings.Index(arn, "+arn:aws:iam::")
if roleIndex > 0 {
2022-05-27 22:35:50 +02:00
// Overwrite ARN
key.Arn = arn[:roleIndex]
key.Role = arn[roleIndex+1:]
}
2022-05-27 22:35:50 +02:00
key.EncryptionContext = context
key.CreationDate = time.Now().UTC()
key.AwsProfile = awsProfile
return key
}
2022-05-27 22:35:50 +02:00
// MasterKeysFromArnString takes a comma separated list of AWS KMS ARNs, and
// returns a slice of new MasterKeys for those ARNs.
2019-01-25 12:42:41 +00:00
func MasterKeysFromArnString(arn string, context map[string]*string, awsProfile string) []*MasterKey {
var keys []*MasterKey
if arn == "" {
return keys
}
for _, s := range strings.Split(arn, ",") {
2019-01-25 12:42:41 +00:00
keys = append(keys, NewMasterKeyFromArn(s, context, awsProfile))
}
return keys
}
2022-05-27 22:35:50 +02:00
// ParseKMSContext takes either a KMS context map or a comma-separated list of
// KMS context key:value pairs, and returns a map.
func ParseKMSContext(in interface{}) map[string]*string {
2022-05-27 22:35:50 +02:00
const nonStringValueWarning = "Encryption context contains a non-string value, context will not be used"
out := make(map[string]*string)
switch in := in.(type) {
case map[string]interface{}:
if len(in) == 0 {
return nil
}
for k, v := range in {
2017-03-22 10:26:22 -07:00
value, ok := v.(string)
if !ok {
log.Warn(nonStringValueWarning)
2017-03-22 10:26:22 -07:00
return nil
}
out[k] = &value
}
case map[interface{}]interface{}:
if len(in) == 0 {
2016-10-31 17:28:59 +01:00
return nil
}
for k, v := range in {
2017-03-22 10:26:22 -07:00
key, ok := k.(string)
if !ok {
log.Warn(nonStringValueWarning)
2017-03-22 10:26:22 -07:00
return nil
}
value, ok := v.(string)
if !ok {
log.Warn(nonStringValueWarning)
2017-03-22 10:26:22 -07:00
return nil
}
out[key] = &value
}
case string:
if in == "" {
return nil
}
for _, kv := range strings.Split(in, ",") {
kv := strings.Split(kv, ":")
if len(kv) != 2 {
log.Warn(nonStringValueWarning)
return nil
}
out[kv[0]] = &kv[1]
}
}
2016-08-18 15:49:27 -07:00
return out
}
2022-05-27 22:35:50 +02:00
// kmsContextToString converts a dictionary into a string that can be parsed
// again with ParseKMSContext().
func kmsContextToString(in map[string]*string) string {
if len(in) == 0 {
return ""
}
// Collect the keys in a slice and compute the expected length
keys := make([]string, 0, len(in))
length := 0
for key := range in {
keys = append(keys, key)
length += len(key) + len(*in[key]) + 2
}
// Sort the keys
sort.Strings(keys)
// Compose a comma-separated string of key-vale pairs
var builder strings.Builder
builder.Grow(length)
for index, key := range keys {
if index > 0 {
builder.WriteString(",")
}
builder.WriteString(key)
builder.WriteByte(':')
builder.WriteString(*in[key])
}
return builder.String()
}
2022-05-27 22:35:50 +02:00
// CredentialsProvider is a wrapper around aws.CredentialsProvider used for
// authentication towards AWS KMS.
type CredentialsProvider struct {
provider aws.CredentialsProvider
}
// NewCredentialsProvider returns a CredentialsProvider object with the provided
// aws.CredentialsProvider.
func NewCredentialsProvider(cp aws.CredentialsProvider) *CredentialsProvider {
return &CredentialsProvider{
provider: cp,
}
}
// ApplyToMasterKey configures the credentials on the provided key.
func (c CredentialsProvider) ApplyToMasterKey(key *MasterKey) {
key.credentialsProvider = c.provider
}
// HTTPClient is a wrapper around http.Client used for configuring the
// AWS KMS client.
type HTTPClient struct {
hc *http.Client
}
// NewHTTPClient creates a new HTTPClient with the provided http.Client.
func NewHTTPClient(hc *http.Client) *HTTPClient {
return &HTTPClient{hc: hc}
}
// ApplyToMasterKey configures the HTTP client on the provided key.
func (h HTTPClient) ApplyToMasterKey(key *MasterKey) {
key.httpClient = h.hc
}
2022-05-27 22:35:50 +02:00
// Encrypt takes a SOPS data key, encrypts it with KMS and stores the result
// in the EncryptedKey field.
//
// Consider using EncryptContext instead.
2022-05-27 22:35:50 +02:00
func (key *MasterKey) Encrypt(dataKey []byte) error {
return key.EncryptContext(context.Background(), dataKey)
}
// EncryptContext takes a SOPS data key, encrypts it with KMS and stores the result
// in the EncryptedKey field.
func (key *MasterKey) EncryptContext(ctx context.Context, dataKey []byte) error {
cfg, err := key.createKMSConfig(ctx)
2022-05-27 22:35:50 +02:00
if err != nil {
log.WithField("arn", key.Arn).Info("Encryption failed")
2022-05-27 22:35:50 +02:00
return err
}
client := key.createClient(cfg)
2022-05-27 22:35:50 +02:00
input := &kms.EncryptInput{
KeyId: &key.Arn,
Plaintext: dataKey,
EncryptionContext: stringPointerToStringMap(key.EncryptionContext),
}
out, err := client.Encrypt(ctx, input)
2022-05-27 22:35:50 +02:00
if err != nil {
log.WithField("arn", key.Arn).Info("Encryption failed")
2022-05-27 22:35:50 +02:00
return fmt.Errorf("failed to encrypt sops data key with AWS KMS: %w", err)
}
key.EncryptedKey = base64.StdEncoding.EncodeToString(out.CiphertextBlob)
log.WithField("arn", key.Arn).Info("Encryption succeeded")
return nil
}
// EncryptIfNeeded encrypts the provided SOPS data key, if it has not been
// encrypted yet.
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
}
return nil
}
// EncryptedDataKey returns the encrypted data key this master key holds.
func (key *MasterKey) EncryptedDataKey() []byte {
return []byte(key.EncryptedKey)
}
// SetEncryptedDataKey sets the encrypted data key for this master key.
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
key.EncryptedKey = string(enc)
}
// Decrypt decrypts the EncryptedKey with a newly created AWS KMS config, and
// returns the result.
//
// Consider using DecryptContext instead.
2022-05-27 22:35:50 +02:00
func (key *MasterKey) Decrypt() ([]byte, error) {
return key.DecryptContext(context.Background())
}
// DecryptContext decrypts the EncryptedKey with a newly created AWS KMS config, and
// returns the result.
func (key *MasterKey) DecryptContext(ctx context.Context) ([]byte, error) {
2022-05-27 22:35:50 +02:00
k, err := base64.StdEncoding.DecodeString(key.EncryptedKey)
if err != nil {
log.WithField("arn", key.Arn).Info("Decryption failed")
2022-05-27 22:35:50 +02:00
return nil, fmt.Errorf("error base64-decoding encrypted data key: %s", err)
}
cfg, err := key.createKMSConfig(ctx)
2022-05-27 22:35:50 +02:00
if err != nil {
log.WithField("arn", key.Arn).Info("Decryption failed")
2022-05-27 22:35:50 +02:00
return nil, err
}
client := key.createClient(cfg)
2022-05-27 22:35:50 +02:00
input := &kms.DecryptInput{
KeyId: &key.Arn,
CiphertextBlob: k,
EncryptionContext: stringPointerToStringMap(key.EncryptionContext),
}
decrypted, err := client.Decrypt(ctx, input)
2022-05-27 22:35:50 +02:00
if err != nil {
log.WithField("arn", key.Arn).Info("Decryption failed")
2022-05-27 22:35:50 +02:00
return nil, fmt.Errorf("failed to decrypt sops data key with AWS KMS: %w", err)
}
log.WithField("arn", key.Arn).Info("Decryption succeeded")
return decrypted.Plaintext, nil
}
// NeedsRotation returns whether the data key needs to be rotated or not.
func (key *MasterKey) NeedsRotation() bool {
return time.Since(key.CreationDate) > kmsTTL
}
// ToString converts the key to a string representation.
func (key *MasterKey) ToString() string {
arnRole := key.Arn
if key.Role != "" {
arnRole = fmt.Sprintf("%s+%s", key.Arn, key.Role)
}
context := kmsContextToString(key.EncryptionContext)
if key.AwsProfile != "" {
return fmt.Sprintf("%s|%s|%s", arnRole, context, key.AwsProfile)
}
if context != "" {
return fmt.Sprintf("%s|%s", arnRole, context)
}
return arnRole
2022-05-27 22:35:50 +02:00
}
// ToMap converts the MasterKey to a map for serialization purposes.
func (key MasterKey) ToMap() map[string]interface{} {
out := make(map[string]interface{})
out["arn"] = key.Arn
if key.Role != "" {
out["role"] = key.Role
}
out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
out["enc"] = key.EncryptedKey
if key.EncryptionContext != nil {
outcontext := make(map[string]string)
for k, v := range key.EncryptionContext {
outcontext[k] = *v
}
out["context"] = outcontext
}
return out
}
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
2022-05-27 22:35:50 +02:00
// createKMSConfig returns an AWS config with the credentialsProvider of the
// MasterKey, or the default configuration sources.
func (key MasterKey) createKMSConfig(ctx context.Context) (*aws.Config, error) {
2022-05-27 22:35:50 +02:00
re := regexp.MustCompile(arnRegex)
matches := re.FindStringSubmatch(key.Arn)
if matches == nil {
return nil, fmt.Errorf("no valid ARN found in '%s'", key.Arn)
}
region := matches[1]
cfg, err := config.LoadDefaultConfig(ctx, func(lo *config.LoadOptions) error {
2022-05-27 22:35:50 +02:00
// Use the credentialsProvider if present, otherwise default to reading credentials
// from the environment.
if key.credentialsProvider != nil {
lo.Credentials = key.credentialsProvider
}
if key.AwsProfile != "" {
lo.SharedConfigProfile = key.AwsProfile
}
lo.Region = region
if key.httpClient != nil {
lo.HTTPClient = key.httpClient
}
2022-05-27 22:35:50 +02:00
return nil
})
if err != nil {
return nil, fmt.Errorf("could not load AWS config: %w", err)
}
if key.Role != "" {
return key.createSTSConfig(ctx, &cfg)
2022-05-27 22:35:50 +02:00
}
return &cfg, nil
}
// createClient creates a new AWS KMS client with the provided config.
func (key MasterKey) createClient(config *aws.Config) *kms.Client {
return kms.NewFromConfig(*config, func(o *kms.Options) {
if key.baseEndpoint != "" {
o.BaseEndpoint = aws.String(key.baseEndpoint)
}
})
}
2022-05-27 22:35:50 +02:00
// createSTSConfig uses AWS STS to assume a role and returns a config
// configured with that role's credentials. It returns an error if
// it fails to construct a session name, or assume the role.
func (key MasterKey) createSTSConfig(ctx context.Context, config *aws.Config) (*aws.Config, error) {
2022-05-27 22:35:50 +02:00
name, err := stsSessionName()
if err != nil {
return nil, err
}
input := &sts.AssumeRoleInput{
RoleArn: &key.Role,
RoleSessionName: &name,
}
client := sts.NewFromConfig(*config)
out, err := client.AssumeRole(ctx, input)
2022-05-27 22:35:50 +02:00
if err != nil {
return nil, fmt.Errorf("failed to assume role '%s': %w", key.Role, err)
}
config.Credentials = credentials.NewStaticCredentialsProvider(*out.Credentials.AccessKeyId,
*out.Credentials.SecretAccessKey, *out.Credentials.SessionToken,
)
return config, nil
}
// stsSessionName returns the name for the STS session in the format of
// `sops@<hostname>`. It sanitizes the hostname with stsSessionRegex, and
// truncates to roleSessionNameLengthLimit when it exceeds the limit.
func stsSessionName() (string, error) {
hostname, err := osHostname()
if err != nil {
return "", fmt.Errorf("failed to construct STS session name: %w", err)
}
re := regexp.MustCompile(stsSessionRegex)
sanitizedHostname := re.ReplaceAllString(hostname, "")
name := "sops@" + sanitizedHostname
if len(name) >= roleSessionNameLengthLimit {
name = name[:roleSessionNameLengthLimit]
}
return name, nil
}
func stringPointerToStringMap(in map[string]*string) map[string]string {
var out = make(map[string]string)
for k, v := range in {
if v == nil {
continue
}
out[k] = *v
}
return out
}