// 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 }