forked from getsops/sops
/
keysource.go
382 lines (349 loc) · 11.5 KB
/
keysource.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
/*
Package kms contains an implementation of the go.mozilla.org/sops/v3.MasterKey
interface that encrypts and decrypts the data key using AWS KMS with the SDK
for Go V2.
*/
package kms //import "go.mozilla.org/sops/v3/kms"
import (
"context"
"encoding/base64"
"fmt"
"os"
"regexp"
"strings"
"time"
"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"
"go.mozilla.org/sops/v3/logging"
)
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
)
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
)
func init() {
log = logging.NewLogger("AWSKMS")
}
// MasterKey is an AWS KMS key used to encrypt and decrypt SOPS' data key using
// AWS SDK for Go V2.
type MasterKey struct {
// 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
// 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
// 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
// epResolver can be used to override the endpoint the AWS client resolves
// 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.
epResolver aws.EndpointResolverWithOptions
}
// 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(),
}
}
// NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that
// ARN.
func NewMasterKeyFromArn(arn string, context map[string]*string, awsProfile string) *MasterKey {
key := &MasterKey{}
arn = strings.Replace(arn, " ", "", -1)
key.Arn = arn
roleIndex := strings.Index(arn, "+arn:aws:iam::")
if roleIndex > 0 {
// Overwrite ARN
key.Arn = arn[:roleIndex]
key.Role = arn[roleIndex+1:]
}
key.EncryptionContext = context
key.CreationDate = time.Now().UTC()
key.AwsProfile = awsProfile
return key
}
// MasterKeysFromArnString takes a comma separated list of AWS KMS ARNs, and
// returns a slice of new MasterKeys for those ARNs.
func MasterKeysFromArnString(arn string, context map[string]*string, awsProfile string) []*MasterKey {
var keys []*MasterKey
if arn == "" {
return keys
}
for _, s := range strings.Split(arn, ",") {
keys = append(keys, NewMasterKeyFromArn(s, context, awsProfile))
}
return keys
}
// 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 {
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 {
value, ok := v.(string)
if !ok {
log.Warn(nonStringValueWarning)
return nil
}
out[k] = &value
}
case map[interface{}]interface{}:
if len(in) == 0 {
return nil
}
for k, v := range in {
key, ok := k.(string)
if !ok {
log.Warn(nonStringValueWarning)
return nil
}
value, ok := v.(string)
if !ok {
log.Warn(nonStringValueWarning)
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]
}
}
return out
}
// 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
}
// Encrypt takes a SOPS data key, encrypts it with KMS and stores the result
// in the EncryptedKey field.
func (key *MasterKey) Encrypt(dataKey []byte) error {
cfg, err := key.createKMSConfig()
if err != nil {
log.WithError(err).WithField("arn", key.Arn).Error("Encryption failed")
return err
}
client := kms.NewFromConfig(*cfg)
input := &kms.EncryptInput{
KeyId: &key.Arn,
Plaintext: dataKey,
EncryptionContext: stringPointerToStringMap(key.EncryptionContext),
}
out, err := client.Encrypt(context.TODO(), input)
if err != nil {
log.WithError(err).WithField("arn", key.Arn).Error("Encryption failed")
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.
func (key *MasterKey) Decrypt() ([]byte, error) {
k, err := base64.StdEncoding.DecodeString(key.EncryptedKey)
if err != nil {
log.WithError(err).WithField("arn", key.Arn).Error("Decryption failed")
return nil, fmt.Errorf("error base64-decoding encrypted data key: %s", err)
}
cfg, err := key.createKMSConfig()
if err != nil {
log.WithError(err).WithField("arn", key.Arn).Error("Decryption failed")
return nil, err
}
client := kms.NewFromConfig(*cfg)
input := &kms.DecryptInput{
KeyId: &key.Arn,
CiphertextBlob: k,
EncryptionContext: stringPointerToStringMap(key.EncryptionContext),
}
decrypted, err := client.Decrypt(context.TODO(), input)
if err != nil {
log.WithError(err).WithField("arn", key.Arn).Error("Decryption failed")
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 {
return key.Arn
}
// 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
}
// createKMSConfig returns an AWS config with the credentialsProvider of the
// MasterKey, or the default configuration sources.
func (key MasterKey) createKMSConfig() (*aws.Config, error) {
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(context.TODO(), func(lo *config.LoadOptions) error {
// 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
// Set the epResolver, if present. Used ONLY for tests.
if key.epResolver != nil {
lo.EndpointResolverWithOptions = key.epResolver
}
return nil
})
if err != nil {
return nil, fmt.Errorf("could not load AWS config: %w", err)
}
if key.Role != "" {
return key.createSTSConfig(&cfg)
}
return &cfg, nil
}
// 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(config *aws.Config) (*aws.Config, error) {
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(context.TODO(), input)
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
}