1package approle
2
3import (
4	"context"
5	"crypto/hmac"
6	"crypto/sha256"
7	"encoding/hex"
8	"fmt"
9	"time"
10
11	uuid "github.com/hashicorp/go-uuid"
12	"github.com/hashicorp/vault/sdk/helper/cidrutil"
13	"github.com/hashicorp/vault/sdk/helper/locksutil"
14	"github.com/hashicorp/vault/sdk/logical"
15)
16
17// secretIDStorageEntry represents the information stored in storage
18// when a SecretID is created. The structure of the SecretID storage
19// entry is the same for all the types of SecretIDs generated.
20type secretIDStorageEntry struct {
21	// Accessor for the SecretID. It is a random UUID serving as
22	// a secondary index for the SecretID. This uniquely identifies
23	// the SecretID it belongs to, and hence can be used for listing
24	// and deleting SecretIDs. Accessors cannot be used as valid
25	// SecretIDs during login.
26	SecretIDAccessor string `json:"secret_id_accessor" mapstructure:"secret_id_accessor"`
27
28	// Number of times this SecretID can be used to perform the login
29	// operation
30	SecretIDNumUses int `json:"secret_id_num_uses" mapstructure:"secret_id_num_uses"`
31
32	// Duration after which this SecretID should expire. This is capped by
33	// the backend mount's max TTL value.
34	SecretIDTTL time.Duration `json:"secret_id_ttl" mapstructure:"secret_id_ttl"`
35
36	// The time when the SecretID was created
37	CreationTime time.Time `json:"creation_time" mapstructure:"creation_time"`
38
39	// The time when the SecretID becomes eligible for tidy operation.
40	// Tidying is performed by the PeriodicFunc of the backend which is 1
41	// minute apart.
42	ExpirationTime time.Time `json:"expiration_time" mapstructure:"expiration_time"`
43
44	// The time representing the last time this storage entry was modified
45	LastUpdatedTime time.Time `json:"last_updated_time" mapstructure:"last_updated_time"`
46
47	// Metadata that belongs to the SecretID
48	Metadata map[string]string `json:"metadata" mapstructure:"metadata"`
49
50	// CIDRList is a set of CIDR blocks that impose source address
51	// restrictions on the usage of SecretID
52	CIDRList []string `json:"cidr_list" mapstructure:"cidr_list"`
53
54	// TokenBoundCIDRs is a set of CIDR blocks that impose source address
55	// restrictions on the usage of the token generated by this SecretID
56	TokenBoundCIDRs []string `json:"token_cidr_list" mapstructure:"token_bound_cidrs"`
57
58	// This is a deprecated field
59	SecretIDNumUsesDeprecated int `json:"SecretIDNumUses" mapstructure:"SecretIDNumUses"`
60}
61
62// Represents the payload of the storage entry of the accessor that maps to a
63// unique SecretID. Note that SecretIDs should never be stored in plaintext
64// anywhere in the backend. SecretIDHMAC will be used as an index to fetch the
65// properties of the SecretID and to delete the SecretID.
66type secretIDAccessorStorageEntry struct {
67	// Hash of the SecretID which can be used to find the storage index at which
68	// properties of SecretID is stored.
69	SecretIDHMAC string `json:"secret_id_hmac" mapstructure:"secret_id_hmac"`
70}
71
72// verifyCIDRRoleSecretIDSubset checks if the CIDR blocks set on the secret ID
73// are a subset of CIDR blocks set on the role
74func verifyCIDRRoleSecretIDSubset(secretIDCIDRs []string, roleBoundCIDRList []string) error {
75	if len(secretIDCIDRs) != 0 {
76		// If there are no CIDR blocks on the role, then the subset
77		// requirement would be satisfied
78		if len(roleBoundCIDRList) != 0 {
79			subset, err := cidrutil.SubsetBlocks(roleBoundCIDRList, secretIDCIDRs)
80			if !subset || err != nil {
81				return fmt.Errorf(
82					"failed to verify subset relationship between CIDR blocks on the role %q and CIDR blocks on the secret ID %q: %w",
83					roleBoundCIDRList,
84					secretIDCIDRs,
85					err,
86				)
87			}
88		}
89	}
90
91	return nil
92}
93
94// Creates a SHA256 HMAC of the given 'value' using the given 'key' and returns
95// a hex encoded string.
96func createHMAC(key, value string) (string, error) {
97	if key == "" {
98		return "", fmt.Errorf("invalid HMAC key")
99	}
100	hm := hmac.New(sha256.New, []byte(key))
101	hm.Write([]byte(value))
102	return hex.EncodeToString(hm.Sum(nil)), nil
103}
104
105func (b *backend) secretIDLock(secretIDHMAC string) *locksutil.LockEntry {
106	return locksutil.LockForKey(b.secretIDLocks, secretIDHMAC)
107}
108
109func (b *backend) secretIDAccessorLock(secretIDAccessor string) *locksutil.LockEntry {
110	return locksutil.LockForKey(b.secretIDAccessorLocks, secretIDAccessor)
111}
112
113// nonLockedSecretIDStorageEntry fetches the secret ID properties from physical
114// storage. The entry will be indexed based on the given HMACs of both role
115// name and the secret ID. This method will not acquire secret ID lock to fetch
116// the storage entry. Locks need to be acquired before calling this method.
117func (b *backend) nonLockedSecretIDStorageEntry(ctx context.Context, s logical.Storage, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC string) (*secretIDStorageEntry, error) {
118	if secretIDHMAC == "" {
119		return nil, fmt.Errorf("missing secret ID HMAC")
120	}
121
122	if roleNameHMAC == "" {
123		return nil, fmt.Errorf("missing role name HMAC")
124	}
125
126	// Prepare the storage index at which the secret ID will be stored
127	entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
128
129	entry, err := s.Get(ctx, entryIndex)
130	if err != nil {
131		return nil, err
132	}
133	if entry == nil {
134		return nil, nil
135	}
136
137	result := secretIDStorageEntry{}
138	if err := entry.DecodeJSON(&result); err != nil {
139		return nil, err
140	}
141
142	// TODO: Remove this upgrade bit in future releases
143	persistNeeded := false
144	if result.SecretIDNumUsesDeprecated != 0 {
145		if result.SecretIDNumUses == 0 ||
146			result.SecretIDNumUsesDeprecated < result.SecretIDNumUses {
147			result.SecretIDNumUses = result.SecretIDNumUsesDeprecated
148			persistNeeded = true
149		}
150		if result.SecretIDNumUses < result.SecretIDNumUsesDeprecated {
151			result.SecretIDNumUsesDeprecated = result.SecretIDNumUses
152			persistNeeded = true
153		}
154	}
155
156	if persistNeeded {
157		if err := b.nonLockedSetSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC, &result); err != nil {
158			return nil, fmt.Errorf("failed to upgrade role storage entry %w", err)
159		}
160	}
161
162	return &result, nil
163}
164
165// nonLockedSetSecretIDStorageEntry creates or updates a secret ID entry at the
166// physical storage. The entry will be indexed based on the given HMACs of both
167// role name and the secret ID. This method will not acquire secret ID lock to
168// create/update the storage entry. Locks need to be acquired before calling
169// this method.
170func (b *backend) nonLockedSetSecretIDStorageEntry(ctx context.Context, s logical.Storage, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC string, secretEntry *secretIDStorageEntry) error {
171	if roleSecretIDPrefix == "" {
172		return fmt.Errorf("missing secret ID prefix")
173	}
174	if secretIDHMAC == "" {
175		return fmt.Errorf("missing secret ID HMAC")
176	}
177
178	if roleNameHMAC == "" {
179		return fmt.Errorf("missing role name HMAC")
180	}
181
182	if secretEntry == nil {
183		return fmt.Errorf("nil secret entry")
184	}
185
186	entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
187
188	if entry, err := logical.StorageEntryJSON(entryIndex, secretEntry); err != nil {
189		return err
190	} else if err = s.Put(ctx, entry); err != nil {
191		return err
192	}
193
194	return nil
195}
196
197// registerSecretIDEntry creates a new storage entry for the given SecretID.
198func (b *backend) registerSecretIDEntry(ctx context.Context, s logical.Storage, roleName, secretID, hmacKey, roleSecretIDPrefix string, secretEntry *secretIDStorageEntry) (*secretIDStorageEntry, error) {
199	secretIDHMAC, err := createHMAC(hmacKey, secretID)
200	if err != nil {
201		return nil, fmt.Errorf("failed to create HMAC of secret ID: %w", err)
202	}
203	roleNameHMAC, err := createHMAC(hmacKey, roleName)
204	if err != nil {
205		return nil, fmt.Errorf("failed to create HMAC of role_name: %w", err)
206	}
207
208	lock := b.secretIDLock(secretIDHMAC)
209	lock.RLock()
210
211	entry, err := b.nonLockedSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
212	if err != nil {
213		lock.RUnlock()
214		return nil, err
215	}
216	if entry != nil {
217		lock.RUnlock()
218		return nil, fmt.Errorf("SecretID is already registered")
219	}
220
221	// If there isn't an entry for the secretID already, switch the read lock
222	// with a write lock and create an entry.
223	lock.RUnlock()
224	lock.Lock()
225	defer lock.Unlock()
226
227	// But before saving a new entry, check if the secretID entry was created during the lock switch.
228	entry, err = b.nonLockedSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
229	if err != nil {
230		return nil, err
231	}
232	if entry != nil {
233		return nil, fmt.Errorf("SecretID is already registered")
234	}
235
236	//
237	// Create a new entry for the SecretID
238	//
239
240	// Set the creation time for the SecretID
241	currentTime := time.Now()
242	secretEntry.CreationTime = currentTime
243	secretEntry.LastUpdatedTime = currentTime
244
245	if ttl := b.deriveSecretIDTTL(secretEntry.SecretIDTTL); ttl != time.Duration(0) {
246		secretEntry.ExpirationTime = currentTime.Add(ttl)
247	}
248
249	// Before storing the SecretID, store its accessor.
250	if err := b.createSecretIDAccessorEntry(ctx, s, secretEntry, secretIDHMAC, roleSecretIDPrefix); err != nil {
251		return nil, err
252	}
253
254	if err := b.nonLockedSetSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC, secretEntry); err != nil {
255		return nil, err
256	}
257
258	return secretEntry, nil
259}
260
261// deriveSecretIDTTL determines the secret ID TTL to use based on the system's
262// max lease TTL.
263//
264// If SecretIDTTL is negative or if it crosses the backend mount's limit,
265// return to backend's max lease TTL. Otherwise, return the provided secretIDTTL
266// value.
267func (b *backend) deriveSecretIDTTL(secretIDTTL time.Duration) time.Duration {
268	if secretIDTTL < time.Duration(0) || secretIDTTL > b.System().MaxLeaseTTL() {
269		return b.System().MaxLeaseTTL()
270	}
271
272	return secretIDTTL
273}
274
275// secretIDAccessorEntry is used to read the storage entry that maps an
276// accessor to a secret_id.
277func (b *backend) secretIDAccessorEntry(ctx context.Context, s logical.Storage, secretIDAccessor, roleSecretIDPrefix string) (*secretIDAccessorStorageEntry, error) {
278	if secretIDAccessor == "" {
279		return nil, fmt.Errorf("missing secretIDAccessor")
280	}
281
282	var result secretIDAccessorStorageEntry
283
284	// Create index entry, mapping the accessor to the token ID
285	salt, err := b.Salt(ctx)
286	if err != nil {
287		return nil, err
288	}
289	accessorPrefix := secretIDAccessorPrefix
290	if roleSecretIDPrefix == secretIDLocalPrefix {
291		accessorPrefix = secretIDAccessorLocalPrefix
292	}
293	entryIndex := accessorPrefix + salt.SaltID(secretIDAccessor)
294
295	accessorLock := b.secretIDAccessorLock(secretIDAccessor)
296	accessorLock.RLock()
297	defer accessorLock.RUnlock()
298
299	if entry, err := s.Get(ctx, entryIndex); err != nil {
300		return nil, err
301	} else if entry == nil {
302		return nil, nil
303	} else if err := entry.DecodeJSON(&result); err != nil {
304		return nil, err
305	}
306
307	return &result, nil
308}
309
310// createSecretIDAccessorEntry creates an identifier for the SecretID. A storage index,
311// mapping the accessor to the SecretID is also created. This method should
312// be called when the lock for the corresponding SecretID is held.
313func (b *backend) createSecretIDAccessorEntry(ctx context.Context, s logical.Storage, entry *secretIDStorageEntry, secretIDHMAC, roleSecretIDPrefix string) error {
314	// Create a random accessor
315	accessorUUID, err := uuid.GenerateUUID()
316	if err != nil {
317		return err
318	}
319	entry.SecretIDAccessor = accessorUUID
320
321	// Create index entry, mapping the accessor to the token ID
322	salt, err := b.Salt(ctx)
323	if err != nil {
324		return err
325	}
326
327	accessorPrefix := secretIDAccessorPrefix
328	if roleSecretIDPrefix == secretIDLocalPrefix {
329		accessorPrefix = secretIDAccessorLocalPrefix
330	}
331	entryIndex := accessorPrefix + salt.SaltID(entry.SecretIDAccessor)
332
333	accessorLock := b.secretIDAccessorLock(accessorUUID)
334	accessorLock.Lock()
335	defer accessorLock.Unlock()
336
337	if entry, err := logical.StorageEntryJSON(entryIndex, &secretIDAccessorStorageEntry{
338		SecretIDHMAC: secretIDHMAC,
339	}); err != nil {
340		return err
341	} else if err = s.Put(ctx, entry); err != nil {
342		return fmt.Errorf("failed to persist accessor index entry: %w", err)
343	}
344
345	return nil
346}
347
348// deleteSecretIDAccessorEntry deletes the storage index mapping the accessor to a SecretID.
349func (b *backend) deleteSecretIDAccessorEntry(ctx context.Context, s logical.Storage, secretIDAccessor, roleSecretIDPrefix string) error {
350	salt, err := b.Salt(ctx)
351	if err != nil {
352		return err
353	}
354
355	accessorPrefix := secretIDAccessorPrefix
356	if roleSecretIDPrefix == secretIDLocalPrefix {
357		accessorPrefix = secretIDAccessorLocalPrefix
358	}
359	entryIndex := accessorPrefix + salt.SaltID(secretIDAccessor)
360
361	accessorLock := b.secretIDAccessorLock(secretIDAccessor)
362	accessorLock.Lock()
363	defer accessorLock.Unlock()
364
365	// Delete the accessor of the SecretID first
366	if err := s.Delete(ctx, entryIndex); err != nil {
367		return fmt.Errorf("failed to delete accessor storage entry: %w", err)
368	}
369
370	return nil
371}
372
373// flushRoleSecrets deletes all the SecretIDs that belong to the given
374// RoleID.
375func (b *backend) flushRoleSecrets(ctx context.Context, s logical.Storage, roleName, hmacKey, roleSecretIDPrefix string) error {
376	roleNameHMAC, err := createHMAC(hmacKey, roleName)
377	if err != nil {
378		return fmt.Errorf("failed to create HMAC of role_name: %w", err)
379	}
380
381	// Acquire the custom lock to perform listing of SecretIDs
382	b.secretIDListingLock.RLock()
383	defer b.secretIDListingLock.RUnlock()
384
385	secretIDHMACs, err := s.List(ctx, fmt.Sprintf("%s%s/", roleSecretIDPrefix, roleNameHMAC))
386	if err != nil {
387		return err
388	}
389	for _, secretIDHMAC := range secretIDHMACs {
390		// Acquire the lock belonging to the SecretID
391		lock := b.secretIDLock(secretIDHMAC)
392		lock.Lock()
393		entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC)
394		if err := s.Delete(ctx, entryIndex); err != nil {
395			lock.Unlock()
396			return fmt.Errorf("error deleting SecretID %q from storage: %w", secretIDHMAC, err)
397		}
398		lock.Unlock()
399	}
400	return nil
401}
402