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