vault/helper/random/random_api.go
Vault Automation a728a665e1
Random API improvements (#12119) (#12143)
* In the random APIs, add a 'prng' param that causes a DRBG seeded from the selected source(s) to be the source of the returned bytes

* fixes, unit test next

* unit tests

* changelog

* memory ramifications

* switch to using a string called drbg

* Update helper/random/random_api.go



* wrong changelog

---------

Co-authored-by: Scott Miller <smiller@hashicorp.com>
Co-authored-by: Steven Clark <steven.clark@hashicorp.com>
2026-02-03 20:02:48 +00:00

168 lines
4.5 KiB
Go

// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: BUSL-1.1
package random
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"strconv"
"github.com/hashicorp/go-hmac-drbg/hmacdrbg"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/xor"
"github.com/hashicorp/vault/sdk/logical"
)
const (
APIMaxBytes = 10 * 1024 * 1024
SealMaxBytes = 128 * 1024
)
func HandleRandomAPI(d *framework.FieldData, additionalSource io.Reader) (*logical.Response, error) {
bytes := 0
// Parsing is convoluted here, but allows operators to ACL both source and byte count
maybeUrlBytes := d.Raw["urlbytes"]
maybeSource := d.Raw["source"]
source := "platform"
var err error
if maybeSource == "" {
bytes = d.Get("bytes").(int)
} else if maybeUrlBytes == "" && isValidSource(maybeSource.(string)) {
source = maybeSource.(string)
bytes = d.Get("bytes").(int)
} else if maybeUrlBytes == "" {
bytes, err = strconv.Atoi(maybeSource.(string))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("error parsing url-set byte count: %s", err)), nil
}
} else {
source = maybeSource.(string)
bytes, err = strconv.Atoi(maybeUrlBytes.(string))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("error parsing url-set byte count: %s", err)), nil
}
}
maybeDrbg, ok := d.GetOk("drbg")
var drbg string
if ok {
drbg = maybeDrbg.(string)
switch drbg {
case "", "auto", "hmacdrbg":
default:
return logical.ErrorResponse("invalid setting for drbg, must absent, auto, or hmacdrbg"), nil
}
}
format := d.Get("format").(string)
if bytes < 1 {
return logical.ErrorResponse(`"bytes" cannot be less than 1`), nil
}
if bytes > APIMaxBytes {
return logical.ErrorResponse(`"bytes" must be less than %d`, APIMaxBytes), nil
} else if (source == "seal" || source == "all") && bytes > SealMaxBytes && len(drbg) == 0 {
return logical.ErrorResponse("bytes cannot be more than %d if sourced from seal. Use drbg mode instead", SealMaxBytes), nil
}
var warning string
switch source {
case "seal":
if rand.Reader == additionalSource {
warning = "no seal/entropy augmentation available, using platform entropy source"
}
case "platform", "all", "":
default:
return logical.ErrorResponse("unsupported entropy source %q; must be \"platform\" or \"seal\", or \"all\"", source), nil
}
switch format {
case "hex":
case "base64":
default:
return logical.ErrorResponse("unsupported encoding format %q; must be \"hex\" or \"base64\"", format), nil
}
var randBytes []byte
if len(drbg) > 0 {
// right now only one value for drbg besides unset is possible, so we assume it was validated
// above
// Seed the drbg from source, but use it to generate byes
var seed []byte
// HMACDRBG wants a seed at least 3x the security level
seed, err = getEntropy(source, (256*3)/8, additionalSource)
if err != nil {
return nil, err
}
drbg := hmacdrbg.NewHmacDrbg(256, seed, []byte("api-random-with-hmac-drbg"))
reader := hmacdrbg.NewHmacDrbgReader(drbg)
randBytes = make([]byte, bytes)
_, err = io.ReadFull(reader, randBytes)
if err != nil {
return nil, err
}
} else {
randBytes, err = getEntropy(source, bytes, additionalSource)
if err != nil {
return nil, err
}
}
var retStr string
switch format {
case "hex":
retStr = hex.EncodeToString(randBytes)
case "base64":
retStr = base64.StdEncoding.EncodeToString(randBytes)
}
// Generate the response
resp := &logical.Response{
Data: map[string]interface{}{
"random_bytes": retStr,
},
}
if warning != "" {
resp.Warnings = []string{warning}
}
return resp, nil
}
func getEntropy(source string, bytes int, additionalSource io.Reader) ([]byte, error) {
var randBytes []byte
var err error
switch source {
case "", "platform":
randBytes, err = uuid.GenerateRandomBytes(bytes)
if err != nil {
return nil, err
}
case "seal":
randBytes, err = uuid.GenerateRandomBytesWithReader(bytes, additionalSource)
case "all":
randBytes, err = uuid.GenerateRandomBytes(bytes)
if err == nil && rand.Reader != additionalSource {
var sealBytes []byte
sealBytes, err = uuid.GenerateRandomBytesWithReader(bytes, additionalSource)
if err == nil {
randBytes, err = xor.XORBytes(sealBytes, randBytes)
}
}
default:
return nil, fmt.Errorf("unsupported entropy source %q; must be \"platform\" or \"seal\", or \"all\"", source)
}
return randBytes, err
}
func isValidSource(s string) bool {
switch s {
case "", "platform", "seal", "all":
return true
}
return false
}