1package awsauth
2
3import (
4	"context"
5	"crypto/hmac"
6	"crypto/sha256"
7	"crypto/subtle"
8	"encoding/base64"
9	"fmt"
10	"strconv"
11	"strings"
12	"time"
13
14	uuid "github.com/hashicorp/go-uuid"
15	"github.com/hashicorp/vault/helper/policyutil"
16	"github.com/hashicorp/vault/helper/strutil"
17	"github.com/hashicorp/vault/logical"
18	"github.com/hashicorp/vault/logical/framework"
19)
20
21const roleTagVersion = "v1"
22
23func pathRoleTag(b *backend) *framework.Path {
24	return &framework.Path{
25		Pattern: "role/" + framework.GenericNameRegex("role") + "/tag$",
26		Fields: map[string]*framework.FieldSchema{
27			"role": &framework.FieldSchema{
28				Type:        framework.TypeString,
29				Description: "Name of the role.",
30			},
31
32			"instance_id": &framework.FieldSchema{
33				Type: framework.TypeString,
34				Description: `Instance ID for which this tag is intended for.
35If set, the created tag can only be used by the instance with the given ID.`,
36			},
37
38			"policies": &framework.FieldSchema{
39				Type:        framework.TypeCommaStringSlice,
40				Description: "Policies to be associated with the tag. If set, must be a subset of the role's policies. If set, but set to an empty value, only the 'default' policy will be given to issued tokens.",
41			},
42
43			"max_ttl": &framework.FieldSchema{
44				Type:        framework.TypeDurationSecond,
45				Default:     0,
46				Description: "If set, specifies the maximum allowed token lifetime.",
47			},
48
49			"allow_instance_migration": &framework.FieldSchema{
50				Type:        framework.TypeBool,
51				Default:     false,
52				Description: "If set, allows migration of the underlying instance where the client resides. This keys off of pendingTime in the metadata document, so essentially, this disables the client nonce check whenever the instance is migrated to a new host and pendingTime is newer than the previously-remembered time. Use with caution.",
53			},
54
55			"disallow_reauthentication": &framework.FieldSchema{
56				Type:        framework.TypeBool,
57				Default:     false,
58				Description: "If set, only allows a single token to be granted per instance ID. In order to perform a fresh login, the entry in whitelist for the instance ID needs to be cleared using the 'auth/aws-ec2/identity-whitelist/<instance_id>' endpoint.",
59			},
60		},
61
62		Callbacks: map[logical.Operation]framework.OperationFunc{
63			logical.UpdateOperation: b.pathRoleTagUpdate,
64		},
65
66		HelpSynopsis:    pathRoleTagSyn,
67		HelpDescription: pathRoleTagDesc,
68	}
69}
70
71// pathRoleTagUpdate is used to create an EC2 instance tag which will
72// identify the Vault resources that the instance will be authorized for.
73func (b *backend) pathRoleTagUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
74	roleName := strings.ToLower(data.Get("role").(string))
75	if roleName == "" {
76		return logical.ErrorResponse("missing role"), nil
77	}
78
79	// Fetch the role entry
80	roleEntry, err := b.lockedAWSRole(ctx, req.Storage, roleName)
81	if err != nil {
82		return nil, err
83	}
84	if roleEntry == nil {
85		return logical.ErrorResponse(fmt.Sprintf("entry not found for role %s", roleName)), nil
86	}
87
88	// If RoleTag is empty, disallow creation of tag.
89	if roleEntry.RoleTag == "" {
90		return logical.ErrorResponse("tag creation is not enabled for this role"), nil
91	}
92
93	// There should be a HMAC key present in the role entry
94	if roleEntry.HMACKey == "" {
95		// Not being able to find the HMACKey is an internal error
96		return nil, fmt.Errorf("failed to find the HMAC key")
97	}
98
99	resp := &logical.Response{}
100
101	// Instance ID is an optional field.
102	instanceID := strings.ToLower(data.Get("instance_id").(string))
103
104	// If no policies field was not supplied, then the tag should inherit all the policies
105	// on the role. But, it was provided, but set to empty explicitly, only "default" policy
106	// should be inherited. So, by leaving the policies var unset to anything when it is not
107	// supplied, we ensure that it inherits all the policies on the role.
108	var policies []string
109	policiesRaw, ok := data.GetOk("policies")
110	if ok {
111		policies = policyutil.ParsePolicies(policiesRaw)
112	}
113	if !strutil.StrListSubset(roleEntry.Policies, policies) {
114		resp.AddWarning("Policies on the tag are not a subset of the policies set on the role. Login will not be allowed with this tag unless the role policies are updated.")
115	}
116
117	// This is an optional field.
118	disallowReauthentication := data.Get("disallow_reauthentication").(bool)
119
120	// This is an optional field.
121	allowInstanceMigration := data.Get("allow_instance_migration").(bool)
122	if allowInstanceMigration && !roleEntry.AllowInstanceMigration {
123		resp.AddWarning("Role does not allow instance migration. Login will not be allowed with this tag unless the role value is updated.")
124	}
125
126	if disallowReauthentication && allowInstanceMigration {
127		return logical.ErrorResponse("cannot set both disallow_reauthentication and allow_instance_migration"), nil
128	}
129
130	// max_ttl for the role tag should be less than the max_ttl set on the role.
131	maxTTL := time.Duration(data.Get("max_ttl").(int)) * time.Second
132
133	// max_ttl on the tag should not be greater than the system view's max_ttl value.
134	if maxTTL > b.System().MaxLeaseTTL() {
135		resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the mount maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second))
136	}
137	// If max_ttl is set for the role, check the bounds for tag's max_ttl value using that.
138	if roleEntry.MaxTTL != time.Duration(0) && maxTTL > roleEntry.MaxTTL {
139		resp.AddWarning(fmt.Sprintf("Given max TTL of %d is greater than the role maximum of %d seconds, and will be capped at login time.", maxTTL/time.Second, roleEntry.MaxTTL/time.Second))
140	}
141
142	if maxTTL < time.Duration(0) {
143		return logical.ErrorResponse("max_ttl cannot be negative"), nil
144	}
145
146	// Create a random nonce.
147	nonce, err := createRoleTagNonce()
148	if err != nil {
149		return nil, err
150	}
151
152	// Create a role tag out of all the information provided.
153	rTagValue, err := createRoleTagValue(&roleTag{
154		Version:                  roleTagVersion,
155		Role:                     roleName,
156		Nonce:                    nonce,
157		Policies:                 policies,
158		MaxTTL:                   maxTTL,
159		InstanceID:               instanceID,
160		DisallowReauthentication: disallowReauthentication,
161		AllowInstanceMigration:   allowInstanceMigration,
162	}, roleEntry)
163	if err != nil {
164		return nil, err
165	}
166
167	// Return the key to be used for the tag and the value to be used for that tag key.
168	// This key value pair should be set on the EC2 instance.
169	resp.Data = map[string]interface{}{
170		"tag_key":   roleEntry.RoleTag,
171		"tag_value": rTagValue,
172	}
173
174	return resp, nil
175}
176
177// createRoleTagValue prepares the plaintext version of the role tag,
178// and appends a HMAC of the plaintext value to it, before returning.
179func createRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (string, error) {
180	if rTag == nil {
181		return "", fmt.Errorf("nil role tag")
182	}
183
184	if roleEntry == nil {
185		return "", fmt.Errorf("nil role entry")
186	}
187
188	// Attach version, nonce, policies and maxTTL to the role tag value.
189	rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag)
190	if err != nil {
191		return "", err
192	}
193
194	// Attach HMAC to tag's plaintext and return.
195	return appendHMAC(rTagPlaintext, roleEntry)
196}
197
198// Takes in the plaintext part of the role tag, creates a HMAC of it and returns
199// a role tag value containing both the plaintext part and the HMAC part.
200func appendHMAC(rTagPlaintext string, roleEntry *awsRoleEntry) (string, error) {
201	if rTagPlaintext == "" {
202		return "", fmt.Errorf("empty role tag plaintext string")
203	}
204
205	if roleEntry == nil {
206		return "", fmt.Errorf("nil role entry")
207	}
208
209	// Create the HMAC of the value
210	hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext)
211	if err != nil {
212		return "", err
213	}
214
215	// attach the HMAC to the value
216	rTagValue := fmt.Sprintf("%s:%s", rTagPlaintext, hmacB64)
217
218	// This limit of 255 is enforced on the EC2 instance. Hence complying to that here.
219	if len(rTagValue) > 255 {
220		return "", fmt.Errorf("role tag 'value' exceeding the limit of 255 characters")
221	}
222
223	return rTagValue, nil
224}
225
226// verifyRoleTagValue rebuilds the role tag's plaintext part, computes the HMAC
227// from it using the role specific HMAC key and compares it with the received HMAC.
228func verifyRoleTagValue(rTag *roleTag, roleEntry *awsRoleEntry) (bool, error) {
229	if rTag == nil {
230		return false, fmt.Errorf("nil role tag")
231	}
232
233	if roleEntry == nil {
234		return false, fmt.Errorf("nil role entry")
235	}
236
237	// Fetch the plaintext part of role tag
238	rTagPlaintext, err := prepareRoleTagPlaintextValue(rTag)
239	if err != nil {
240		return false, err
241	}
242
243	// Compute the HMAC of the plaintext
244	hmacB64, err := createRoleTagHMACBase64(roleEntry.HMACKey, rTagPlaintext)
245	if err != nil {
246		return false, err
247	}
248
249	return subtle.ConstantTimeCompare([]byte(rTag.HMAC), []byte(hmacB64)) == 1, nil
250}
251
252// prepareRoleTagPlaintextValue builds the role tag value without the HMAC in it.
253func prepareRoleTagPlaintextValue(rTag *roleTag) (string, error) {
254	if rTag == nil {
255		return "", fmt.Errorf("nil role tag")
256	}
257	if rTag.Version == "" {
258		return "", fmt.Errorf("missing version")
259	}
260	if rTag.Nonce == "" {
261		return "", fmt.Errorf("missing nonce")
262	}
263	if rTag.Role == "" {
264		return "", fmt.Errorf("missing role")
265	}
266
267	// Attach Version, Nonce, Role, DisallowReauthentication and AllowInstanceMigration
268	// fields to the role tag.
269	value := fmt.Sprintf("%s:%s:r=%s:d=%s:m=%s", rTag.Version, rTag.Nonce, rTag.Role, strconv.FormatBool(rTag.DisallowReauthentication), strconv.FormatBool(rTag.AllowInstanceMigration))
270
271	// Attach the policies only if they are specified.
272	if len(rTag.Policies) != 0 {
273		value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ","))
274	}
275
276	// Attach instance_id if set.
277	if rTag.InstanceID != "" {
278		value = fmt.Sprintf("%s:i=%s", value, rTag.InstanceID)
279	}
280
281	// Attach max_ttl if it is provided.
282	if int(rTag.MaxTTL.Seconds()) > 0 {
283		value = fmt.Sprintf("%s:t=%d", value, int(rTag.MaxTTL.Seconds()))
284	}
285
286	return value, nil
287}
288
289// Parses the tag from string form into a struct form. This method
290// also verifies the correctness of the parsed role tag.
291func (b *backend) parseAndVerifyRoleTagValue(ctx context.Context, s logical.Storage, tag string) (*roleTag, error) {
292	tagItems := strings.Split(tag, ":")
293
294	// Tag must contain version, nonce, policies and HMAC
295	if len(tagItems) < 4 {
296		return nil, fmt.Errorf("invalid tag")
297	}
298
299	rTag := &roleTag{}
300
301	// Cache the HMAC value. The last item in the collection.
302	rTag.HMAC = tagItems[len(tagItems)-1]
303
304	// Remove the HMAC from the list.
305	tagItems = tagItems[:len(tagItems)-1]
306
307	// Version will be the first element.
308	rTag.Version = tagItems[0]
309	if rTag.Version != roleTagVersion {
310		return nil, fmt.Errorf("invalid role tag version")
311	}
312
313	// Nonce will be the second element.
314	rTag.Nonce = tagItems[1]
315
316	// Delete the version and nonce from the list.
317	tagItems = tagItems[2:]
318
319	for _, tagItem := range tagItems {
320		var err error
321		switch {
322		case strings.HasPrefix(tagItem, "i="):
323			rTag.InstanceID = strings.TrimPrefix(tagItem, "i=")
324		case strings.HasPrefix(tagItem, "r="):
325			rTag.Role = strings.TrimPrefix(tagItem, "r=")
326		case strings.HasPrefix(tagItem, "p="):
327			rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",")
328		case strings.HasPrefix(tagItem, "d="):
329			rTag.DisallowReauthentication, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "d="))
330			if err != nil {
331				return nil, err
332			}
333		case strings.HasPrefix(tagItem, "m="):
334			rTag.AllowInstanceMigration, err = strconv.ParseBool(strings.TrimPrefix(tagItem, "m="))
335			if err != nil {
336				return nil, err
337			}
338		case strings.HasPrefix(tagItem, "t="):
339			rTag.MaxTTL, err = time.ParseDuration(fmt.Sprintf("%ss", strings.TrimPrefix(tagItem, "t=")))
340			if err != nil {
341				return nil, err
342			}
343		default:
344			return nil, fmt.Errorf("unrecognized item %q in tag", tagItem)
345		}
346	}
347
348	if rTag.Role == "" {
349		return nil, fmt.Errorf("missing role name")
350	}
351
352	roleEntry, err := b.lockedAWSRole(ctx, s, rTag.Role)
353	if err != nil {
354		return nil, err
355	}
356	if roleEntry == nil {
357		return nil, fmt.Errorf("entry not found for %q", rTag.Role)
358	}
359
360	// Create a HMAC of the plaintext value of role tag and compare it with the given value.
361	verified, err := verifyRoleTagValue(rTag, roleEntry)
362	if err != nil {
363		return nil, err
364	}
365	if !verified {
366		return nil, fmt.Errorf("role tag signature verification failed")
367	}
368
369	return rTag, nil
370}
371
372// Creates base64 encoded HMAC using a per-role key.
373func createRoleTagHMACBase64(key, value string) (string, error) {
374	if key == "" {
375		return "", fmt.Errorf("invalid HMAC key")
376	}
377	hm := hmac.New(sha256.New, []byte(key))
378	hm.Write([]byte(value))
379
380	// base64 encode the hmac bytes.
381	return base64.StdEncoding.EncodeToString(hm.Sum(nil)), nil
382}
383
384// Creates a base64 encoded random nonce.
385func createRoleTagNonce() (string, error) {
386	if uuidBytes, err := uuid.GenerateRandomBytes(8); err != nil {
387		return "", err
388	} else {
389		return base64.StdEncoding.EncodeToString(uuidBytes), nil
390	}
391}
392
393// Struct roleTag represents a role tag in a struct form.
394type roleTag struct {
395	Version                  string        `json:"version"`
396	InstanceID               string        `json:"instance_id"`
397	Nonce                    string        `json:"nonce"`
398	Policies                 []string      `json:"policies"`
399	MaxTTL                   time.Duration `json:"max_ttl"`
400	Role                     string        `json:"role"`
401	HMAC                     string        `json:"hmac"`
402	DisallowReauthentication bool          `json:"disallow_reauthentication"`
403	AllowInstanceMigration   bool          `json:"allow_instance_migration"`
404}
405
406func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool {
407	return rTag1 != nil &&
408		rTag2 != nil &&
409		rTag1.Version == rTag2.Version &&
410		rTag1.Nonce == rTag2.Nonce &&
411		policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) &&
412		rTag1.MaxTTL == rTag2.MaxTTL &&
413		rTag1.Role == rTag2.Role &&
414		rTag1.HMAC == rTag2.HMAC &&
415		rTag1.InstanceID == rTag2.InstanceID &&
416		rTag1.DisallowReauthentication == rTag2.DisallowReauthentication &&
417		rTag1.AllowInstanceMigration == rTag2.AllowInstanceMigration
418}
419
420const pathRoleTagSyn = `
421Create a tag on a role in order to be able to further restrict the capabilities of a role.
422`
423
424const pathRoleTagDesc = `
425If there are needs to apply only a subset of role's capabilities to any specific
426instance, create a role tag using this endpoint and attach the tag on the instance
427before performing login.
428
429To be able to create a role tag, the 'role_tag' option on the role should be
430enabled via the endpoint 'role/<role>'. Also, the policies to be associated
431with the tag should be a subset of the policies associated with the registered role.
432
433This endpoint will return both the 'key' and the 'value' of the tag to be set
434on the EC2 instance.
435`
436