diff --git a/builtin/logical/transit/path_random.go b/builtin/logical/transit/path_random.go index aa9dc25923..f84abc00e4 100644 --- a/builtin/logical/transit/path_random.go +++ b/builtin/logical/transit/path_random.go @@ -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{ diff --git a/changelog/_12119.txt b/changelog/_12119.txt new file mode 100644 index 0000000000..51bc87f578 --- /dev/null +++ b/changelog/_12119.txt @@ -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. +``` \ No newline at end of file diff --git a/helper/random/random_api.go b/helper/random/random_api.go index fc8dbc919e..e9055dce0b 100644 --- a/helper/random/random_api.go +++ b/helper/random/random_api.go @@ -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,32 +88,29 @@ 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" + 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 } - 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) - } + } else { + randBytes, err = getEntropy(source, bytes, additionalSource) + if err != nil { + return nil, err } - default: - return logical.ErrorResponse("unsupported entropy source %q; must be \"platform\" or \"seal\", or \"all\"", source), nil - } - if err != nil { - return nil, err } var retStr string @@ -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": diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index baf14c8937..32f4405d65 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -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{ diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 198770fe19..42b2a80533 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -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) }