| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package approle |
| |
| import ( |
| "context" |
| "crypto/hmac" |
| "crypto/sha256" |
| "encoding/hex" |
| "fmt" |
| "strings" |
| "time" |
| |
| uuid "github.com/hashicorp/go-uuid" |
| "github.com/hashicorp/vault/helper/parseip" |
| "github.com/hashicorp/vault/sdk/helper/cidrutil" |
| "github.com/hashicorp/vault/sdk/helper/locksutil" |
| "github.com/hashicorp/vault/sdk/logical" |
| ) |
| |
| // secretIDStorageEntry represents the information stored in storage |
| // when a SecretID is created. The structure of the SecretID storage |
| // entry is the same for all the types of SecretIDs generated. |
| type secretIDStorageEntry struct { |
| // Accessor for the SecretID. It is a random UUID serving as |
| // a secondary index for the SecretID. This uniquely identifies |
| // the SecretID it belongs to, and hence can be used for listing |
| // and deleting SecretIDs. Accessors cannot be used as valid |
| // SecretIDs during login. |
| SecretIDAccessor string `json:"secret_id_accessor" mapstructure:"secret_id_accessor"` |
| |
| // Number of times this SecretID can be used to perform the login |
| // operation |
| SecretIDNumUses int `json:"secret_id_num_uses" mapstructure:"secret_id_num_uses"` |
| |
| // Duration after which this SecretID should expire. This is capped by |
| // the backend mount's max TTL value. |
| SecretIDTTL time.Duration `json:"secret_id_ttl" mapstructure:"secret_id_ttl"` |
| |
| // The time when the SecretID was created |
| CreationTime time.Time `json:"creation_time" mapstructure:"creation_time"` |
| |
| // The time when the SecretID becomes eligible for tidy operation. |
| // Tidying is performed by the PeriodicFunc of the backend which is 1 |
| // minute apart. |
| ExpirationTime time.Time `json:"expiration_time" mapstructure:"expiration_time"` |
| |
| // The time representing the last time this storage entry was modified |
| LastUpdatedTime time.Time `json:"last_updated_time" mapstructure:"last_updated_time"` |
| |
| // Metadata that belongs to the SecretID |
| Metadata map[string]string `json:"metadata" mapstructure:"metadata"` |
| |
| // CIDRList is a set of CIDR blocks that impose source address |
| // restrictions on the usage of SecretID |
| CIDRList []string `json:"cidr_list" mapstructure:"cidr_list"` |
| |
| // TokenBoundCIDRs is a set of CIDR blocks that impose source address |
| // restrictions on the usage of the token generated by this SecretID |
| TokenBoundCIDRs []string `json:"token_cidr_list" mapstructure:"token_bound_cidrs"` |
| |
| // This is a deprecated field |
| SecretIDNumUsesDeprecated int `json:"SecretIDNumUses" mapstructure:"SecretIDNumUses"` |
| } |
| |
| // Represents the payload of the storage entry of the accessor that maps to a |
| // unique SecretID. Note that SecretIDs should never be stored in plaintext |
| // anywhere in the backend. SecretIDHMAC will be used as an index to fetch the |
| // properties of the SecretID and to delete the SecretID. |
| type secretIDAccessorStorageEntry struct { |
| // Hash of the SecretID which can be used to find the storage index at which |
| // properties of SecretID is stored. |
| SecretIDHMAC string `json:"secret_id_hmac" mapstructure:"secret_id_hmac"` |
| } |
| |
| // verifyCIDRRoleSecretIDSubset checks if the CIDR blocks set on the secret ID |
| // are a subset of CIDR blocks set on the role |
| func verifyCIDRRoleSecretIDSubset(secretIDCIDRs []string, roleBoundCIDRList []string) error { |
| if len(secretIDCIDRs) != 0 { |
| // If there are no CIDR blocks on the role, then the subset |
| // requirement would be satisfied |
| if len(roleBoundCIDRList) != 0 { |
| // Address blocks with /32 mask do not get stored with the CIDR mask |
| // Check if there are any /32 addresses and append CIDR mask |
| for i, block := range roleBoundCIDRList { |
| if !strings.Contains(block, "/") { |
| roleBoundCIDRList[i] = fmt.Sprint(block, "/32") |
| } |
| } |
| |
| subset, err := cidrutil.SubsetBlocks(roleBoundCIDRList, secretIDCIDRs) |
| if !subset || err != nil { |
| return fmt.Errorf( |
| "failed to verify subset relationship between CIDR blocks on the role %q and CIDR blocks on the secret ID %q: %w", |
| roleBoundCIDRList, |
| secretIDCIDRs, |
| err, |
| ) |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| const maxHmacInputLength = 4096 |
| |
| // Creates a SHA256 HMAC of the given 'value' using the given 'key' and returns |
| // a hex encoded string. |
| func createHMAC(key, value string) (string, error) { |
| if key == "" { |
| return "", fmt.Errorf("invalid HMAC key") |
| } |
| |
| if len(value) > maxHmacInputLength { |
| return "", fmt.Errorf("value is longer than maximum of %d bytes", maxHmacInputLength) |
| } |
| |
| hm := hmac.New(sha256.New, []byte(key)) |
| hm.Write([]byte(value)) |
| return hex.EncodeToString(hm.Sum(nil)), nil |
| } |
| |
| func (b *backend) secretIDLock(secretIDHMAC string) *locksutil.LockEntry { |
| return locksutil.LockForKey(b.secretIDLocks, secretIDHMAC) |
| } |
| |
| func (b *backend) secretIDAccessorLock(secretIDAccessor string) *locksutil.LockEntry { |
| return locksutil.LockForKey(b.secretIDAccessorLocks, secretIDAccessor) |
| } |
| |
| func decodeSecretIDStorageEntry(entry *logical.StorageEntry) (*secretIDStorageEntry, error) { |
| result := secretIDStorageEntry{} |
| if err := entry.DecodeJSON(&result); err != nil { |
| return nil, err |
| } |
| |
| cleanup := func(in []string) []string { |
| if len(in) == 0 { |
| // Don't change unnecessarily, if it was empty list leave as empty list |
| // instead of making it nil. |
| return in |
| } |
| var out []string |
| for _, s := range in { |
| out = append(out, parseip.TrimLeadingZeroesCIDR(s)) |
| } |
| return out |
| } |
| |
| result.CIDRList = cleanup(result.CIDRList) |
| result.TokenBoundCIDRs = cleanup(result.TokenBoundCIDRs) |
| return &result, nil |
| } |
| |
| // nonLockedSecretIDStorageEntry fetches the secret ID properties from physical |
| // storage. The entry will be indexed based on the given HMACs of both role |
| // name and the secret ID. This method will not acquire secret ID lock to fetch |
| // the storage entry. Locks need to be acquired before calling this method. |
| func (b *backend) nonLockedSecretIDStorageEntry(ctx context.Context, s logical.Storage, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC string) (*secretIDStorageEntry, error) { |
| if secretIDHMAC == "" { |
| return nil, fmt.Errorf("missing secret ID HMAC") |
| } |
| |
| if roleNameHMAC == "" { |
| return nil, fmt.Errorf("missing role name HMAC") |
| } |
| |
| // Prepare the storage index at which the secret ID will be stored |
| entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC) |
| |
| entry, err := s.Get(ctx, entryIndex) |
| if err != nil { |
| return nil, err |
| } |
| if entry == nil { |
| return nil, nil |
| } |
| |
| result, err := decodeSecretIDStorageEntry(entry) |
| if err != nil { |
| return nil, err |
| } |
| |
| // TODO: Remove this upgrade bit in future releases |
| persistNeeded := false |
| if result.SecretIDNumUsesDeprecated != 0 { |
| if result.SecretIDNumUses == 0 || |
| result.SecretIDNumUsesDeprecated < result.SecretIDNumUses { |
| result.SecretIDNumUses = result.SecretIDNumUsesDeprecated |
| persistNeeded = true |
| } |
| if result.SecretIDNumUses < result.SecretIDNumUsesDeprecated { |
| result.SecretIDNumUsesDeprecated = result.SecretIDNumUses |
| persistNeeded = true |
| } |
| } |
| |
| if persistNeeded { |
| if err := b.nonLockedSetSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC, result); err != nil { |
| return nil, fmt.Errorf("failed to upgrade role storage entry %w", err) |
| } |
| } |
| |
| return result, nil |
| } |
| |
| // nonLockedSetSecretIDStorageEntry creates or updates a secret ID entry at the |
| // physical storage. The entry will be indexed based on the given HMACs of both |
| // role name and the secret ID. This method will not acquire secret ID lock to |
| // create/update the storage entry. Locks need to be acquired before calling |
| // this method. |
| func (b *backend) nonLockedSetSecretIDStorageEntry(ctx context.Context, s logical.Storage, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC string, secretEntry *secretIDStorageEntry) error { |
| if roleSecretIDPrefix == "" { |
| return fmt.Errorf("missing secret ID prefix") |
| } |
| if secretIDHMAC == "" { |
| return fmt.Errorf("missing secret ID HMAC") |
| } |
| |
| if roleNameHMAC == "" { |
| return fmt.Errorf("missing role name HMAC") |
| } |
| |
| if secretEntry == nil { |
| return fmt.Errorf("nil secret entry") |
| } |
| |
| entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC) |
| |
| if entry, err := logical.StorageEntryJSON(entryIndex, secretEntry); err != nil { |
| return err |
| } else if err = s.Put(ctx, entry); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // registerSecretIDEntry creates a new storage entry for the given SecretID. |
| func (b *backend) registerSecretIDEntry(ctx context.Context, s logical.Storage, roleName, secretID, hmacKey, roleSecretIDPrefix string, secretEntry *secretIDStorageEntry) (*secretIDStorageEntry, error) { |
| secretIDHMAC, err := createHMAC(hmacKey, secretID) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create HMAC of secret ID: %w", err) |
| } |
| roleNameHMAC, err := createHMAC(hmacKey, roleName) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create HMAC of role_name: %w", err) |
| } |
| |
| lock := b.secretIDLock(secretIDHMAC) |
| lock.RLock() |
| |
| entry, err := b.nonLockedSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC) |
| if err != nil { |
| lock.RUnlock() |
| return nil, err |
| } |
| if entry != nil { |
| lock.RUnlock() |
| return nil, fmt.Errorf("SecretID is already registered") |
| } |
| |
| // If there isn't an entry for the secretID already, switch the read lock |
| // with a write lock and create an entry. |
| lock.RUnlock() |
| lock.Lock() |
| defer lock.Unlock() |
| |
| // But before saving a new entry, check if the secretID entry was created during the lock switch. |
| entry, err = b.nonLockedSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC) |
| if err != nil { |
| return nil, err |
| } |
| if entry != nil { |
| return nil, fmt.Errorf("SecretID is already registered") |
| } |
| |
| // |
| // Create a new entry for the SecretID |
| // |
| |
| // Set the creation time for the SecretID |
| currentTime := time.Now() |
| secretEntry.CreationTime = currentTime |
| secretEntry.LastUpdatedTime = currentTime |
| |
| if ttl := b.deriveSecretIDTTL(secretEntry.SecretIDTTL); ttl != time.Duration(0) { |
| secretEntry.ExpirationTime = currentTime.Add(ttl) |
| } |
| |
| // Before storing the SecretID, store its accessor. |
| if err := b.createSecretIDAccessorEntry(ctx, s, secretEntry, secretIDHMAC, roleSecretIDPrefix); err != nil { |
| return nil, err |
| } |
| |
| if err := b.nonLockedSetSecretIDStorageEntry(ctx, s, roleSecretIDPrefix, roleNameHMAC, secretIDHMAC, secretEntry); err != nil { |
| return nil, err |
| } |
| |
| return secretEntry, nil |
| } |
| |
| // deriveSecretIDTTL determines the secret ID TTL to use based on the system's |
| // max lease TTL. |
| // |
| // If SecretIDTTL is negative or if it crosses the backend mount's limit, |
| // return to backend's max lease TTL. Otherwise, return the provided secretIDTTL |
| // value. |
| func (b *backend) deriveSecretIDTTL(secretIDTTL time.Duration) time.Duration { |
| if secretIDTTL < time.Duration(0) || secretIDTTL > b.System().MaxLeaseTTL() { |
| return b.System().MaxLeaseTTL() |
| } |
| |
| return secretIDTTL |
| } |
| |
| // secretIDAccessorEntry is used to read the storage entry that maps an |
| // accessor to a secret_id. |
| func (b *backend) secretIDAccessorEntry(ctx context.Context, s logical.Storage, secretIDAccessor, roleSecretIDPrefix string) (*secretIDAccessorStorageEntry, error) { |
| if secretIDAccessor == "" { |
| return nil, fmt.Errorf("missing secretIDAccessor") |
| } |
| |
| var result secretIDAccessorStorageEntry |
| |
| // Create index entry, mapping the accessor to the token ID |
| salt, err := b.Salt(ctx) |
| if err != nil { |
| return nil, err |
| } |
| accessorPrefix := secretIDAccessorPrefix |
| if roleSecretIDPrefix == secretIDLocalPrefix { |
| accessorPrefix = secretIDAccessorLocalPrefix |
| } |
| entryIndex := accessorPrefix + salt.SaltID(secretIDAccessor) |
| |
| accessorLock := b.secretIDAccessorLock(secretIDAccessor) |
| accessorLock.RLock() |
| defer accessorLock.RUnlock() |
| |
| if entry, err := s.Get(ctx, entryIndex); err != nil { |
| return nil, err |
| } else if entry == nil { |
| return nil, nil |
| } else if err := entry.DecodeJSON(&result); err != nil { |
| return nil, err |
| } |
| |
| return &result, nil |
| } |
| |
| // createSecretIDAccessorEntry creates an identifier for the SecretID. A storage index, |
| // mapping the accessor to the SecretID is also created. This method should |
| // be called when the lock for the corresponding SecretID is held. |
| func (b *backend) createSecretIDAccessorEntry(ctx context.Context, s logical.Storage, entry *secretIDStorageEntry, secretIDHMAC, roleSecretIDPrefix string) error { |
| // Create a random accessor |
| accessorUUID, err := uuid.GenerateUUID() |
| if err != nil { |
| return err |
| } |
| entry.SecretIDAccessor = accessorUUID |
| |
| // Create index entry, mapping the accessor to the token ID |
| salt, err := b.Salt(ctx) |
| if err != nil { |
| return err |
| } |
| |
| accessorPrefix := secretIDAccessorPrefix |
| if roleSecretIDPrefix == secretIDLocalPrefix { |
| accessorPrefix = secretIDAccessorLocalPrefix |
| } |
| entryIndex := accessorPrefix + salt.SaltID(entry.SecretIDAccessor) |
| |
| accessorLock := b.secretIDAccessorLock(accessorUUID) |
| accessorLock.Lock() |
| defer accessorLock.Unlock() |
| |
| if entry, err := logical.StorageEntryJSON(entryIndex, &secretIDAccessorStorageEntry{ |
| SecretIDHMAC: secretIDHMAC, |
| }); err != nil { |
| return err |
| } else if err = s.Put(ctx, entry); err != nil { |
| return fmt.Errorf("failed to persist accessor index entry: %w", err) |
| } |
| |
| return nil |
| } |
| |
| // deleteSecretIDAccessorEntry deletes the storage index mapping the accessor to a SecretID. |
| func (b *backend) deleteSecretIDAccessorEntry(ctx context.Context, s logical.Storage, secretIDAccessor, roleSecretIDPrefix string) error { |
| salt, err := b.Salt(ctx) |
| if err != nil { |
| return err |
| } |
| |
| accessorPrefix := secretIDAccessorPrefix |
| if roleSecretIDPrefix == secretIDLocalPrefix { |
| accessorPrefix = secretIDAccessorLocalPrefix |
| } |
| entryIndex := accessorPrefix + salt.SaltID(secretIDAccessor) |
| |
| accessorLock := b.secretIDAccessorLock(secretIDAccessor) |
| accessorLock.Lock() |
| defer accessorLock.Unlock() |
| |
| // Delete the accessor of the SecretID first |
| if err := s.Delete(ctx, entryIndex); err != nil { |
| return fmt.Errorf("failed to delete accessor storage entry: %w", err) |
| } |
| |
| return nil |
| } |
| |
| // flushRoleSecrets deletes all the SecretIDs that belong to the given |
| // RoleID. |
| func (b *backend) flushRoleSecrets(ctx context.Context, s logical.Storage, roleName, hmacKey, roleSecretIDPrefix string) error { |
| roleNameHMAC, err := createHMAC(hmacKey, roleName) |
| if err != nil { |
| return fmt.Errorf("failed to create HMAC of role_name: %w", err) |
| } |
| |
| // Acquire the custom lock to perform listing of SecretIDs |
| b.secretIDListingLock.RLock() |
| defer b.secretIDListingLock.RUnlock() |
| |
| secretIDHMACs, err := s.List(ctx, fmt.Sprintf("%s%s/", roleSecretIDPrefix, roleNameHMAC)) |
| if err != nil { |
| return err |
| } |
| for _, secretIDHMAC := range secretIDHMACs { |
| // Acquire the lock belonging to the SecretID |
| lock := b.secretIDLock(secretIDHMAC) |
| lock.Lock() |
| entryIndex := fmt.Sprintf("%s%s/%s", roleSecretIDPrefix, roleNameHMAC, secretIDHMAC) |
| if err := s.Delete(ctx, entryIndex); err != nil { |
| lock.Unlock() |
| return fmt.Errorf("error deleting SecretID %q from storage: %w", secretIDHMAC, err) |
| } |
| lock.Unlock() |
| } |
| return nil |
| } |