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>
This commit is contained in:
Vault Automation 2026-02-03 15:02:48 -05:00 committed by GitHub
parent 375a59c4cd
commit a728a665e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 105 additions and 25 deletions

View file

@ -44,6 +44,12 @@ func (b *backend) pathRandom() *framework.Path {
Default: "platform",
Description: `Which system to source random data from, ether "platform", "seal", or "all".`,
},
"drbg": {
Type: framework.TypeString,
Default: "",
Description: "If set, seed a secure DRBG from the source and use it to generate the bytes. This can be more performant when using the seal source." +
" Possible values are unset (don't use a DRBG), \"auto\" and \"hmacdrbg\" which are equivalent.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{

6
changelog/_12119.txt Normal file
View file

@ -0,0 +1,6 @@
```release-note:improvement
secrets/transit, core: Boost the limit of random bytes retrievable via random byte
APIs. And add the option to get PRNG random bytes seeded by random sources.
Note that requests for large numbers of bytes will increase Vault memory
usage accordingly.
```

View file

@ -11,13 +11,17 @@ import (
"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 = 128 * 1024
const (
APIMaxBytes = 10 * 1024 * 1024
SealMaxBytes = 128 * 1024
)
func HandleRandomAPI(d *framework.FieldData, additionalSource io.Reader) (*logical.Response, error) {
bytes := 0
@ -43,6 +47,16 @@ func HandleRandomAPI(d *framework.FieldData, additionalSource io.Reader) (*logic
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 {
@ -50,7 +64,20 @@ func HandleRandomAPI(d *framework.FieldData, additionalSource io.Reader) (*logic
}
if bytes > APIMaxBytes {
return logical.ErrorResponse(`"bytes" should be less than %d`, APIMaxBytes), nil
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 {
@ -61,33 +88,30 @@ func HandleRandomAPI(d *framework.FieldData, additionalSource io.Reader) (*logic
}
var randBytes []byte
var warning string
switch source {
case "", "platform":
randBytes, err = uuid.GenerateRandomBytes(bytes)
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
}
case "seal":
if rand.Reader == additionalSource {
warning = "no seal/entropy augmentation available, using platform entropy source"
}
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 logical.ErrorResponse("unsupported entropy source %q; must be \"platform\" or \"seal\", or \"all\"", source), nil
}
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 {
@ -109,6 +133,32 @@ func HandleRandomAPI(d *framework.FieldData, additionalSource io.Reader) (*logic
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":

View file

@ -2586,6 +2586,12 @@ func (b *SystemBackend) toolsPaths() []*framework.Path {
Default: "platform",
Description: `Which system to source random data from, ether "platform", "seal", or "all".`,
},
"drbg": {
Type: framework.TypeString,
Default: "",
Description: "If set, seed a secure DRBG from the source and use it to generate the bytes. This can be more performant when using the seal source." +
" Possible values are unset (don't use a DRBG), \"auto\" and \"hmacdrbg\" which are equivalent.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{

View file

@ -4377,7 +4377,7 @@ func TestSystemBackend_ToolsRandom(t *testing.T) {
}
if errExpected {
if !resp.IsError() {
t.Fatalf("bad: got error response: %#v", *resp)
t.Fatalf("bad: got no error response: %#v", *resp)
}
return nil
} else {
@ -4435,6 +4435,13 @@ func TestSystemBackend_ToolsRandom(t *testing.T) {
req.Data["format"] = "hex"
doRequest(req, false, "hex", 24)
// Test PRNG with large output
req.Path = "tools/random/1024768"
req.Data["format"] = "hex"
req.Data["drbg"] = "hmacdrbg"
doRequest(req, false, "hex", 1024768)
delete(req.Data, "drbg")
// Test bad input/format
req.Path = "tools/random"
req.Data["format"] = "base92"
@ -4445,7 +4452,12 @@ func TestSystemBackend_ToolsRandom(t *testing.T) {
doRequest(req, true, "", 0)
req.Data["format"] = "hex"
req.Data["bytes"] = maxBytes + 1
req.Data["bytes"] = random.APIMaxBytes + 1
doRequest(req, true, "", 0)
req.Path = "tools/random/seal"
req.Data["format"] = "hex"
req.Data["bytes"] = random.SealMaxBytes + 1
doRequest(req, true, "", 0)
}