diff --git a/builtin/logical/transit/path_random.go b/builtin/logical/transit/path_random.go index 93810bec7..8d9b2c8b7 100644 --- a/builtin/logical/transit/path_random.go +++ b/builtin/logical/transit/path_random.go @@ -2,21 +2,14 @@ package transit import ( "context" - "encoding/base64" - "encoding/hex" - "fmt" - "strconv" - - uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/random" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) -const maxBytes = 128 * 1024 - func (b *backend) pathRandom() *framework.Path { return &framework.Path{ - Pattern: "random" + framework.OptionalParamRegex("urlbytes"), + Pattern: "random(/" + framework.GenericNameRegex("source") + ")?" + framework.OptionalParamRegex("urlbytes"), Fields: map[string]*framework.FieldSchema{ "urlbytes": { Type: framework.TypeString, @@ -34,6 +27,12 @@ func (b *backend) pathRandom() *framework.Path { Default: "base64", Description: `Encoding format to use. Can be "hex" or "base64". Defaults to "base64".`, }, + + "source": { + Type: framework.TypeString, + Default: "platform", + Description: `Which system to source random data from, ether "platform", "seal", or "all".`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -45,55 +44,8 @@ func (b *backend) pathRandom() *framework.Path { } } -func (b *backend) pathRandomWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - bytes := 0 - var err error - strBytes := d.Get("urlbytes").(string) - if strBytes != "" { - bytes, err = strconv.Atoi(strBytes) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("error parsing url-set byte count: %s", err)), nil - } - } else { - bytes = d.Get("bytes").(int) - } - format := d.Get("format").(string) - - if bytes < 1 { - return logical.ErrorResponse(`"bytes" cannot be less than 1`), nil - } - - if bytes > maxBytes { - return logical.ErrorResponse(`"bytes" should be less than %d`, maxBytes), nil - } - - switch format { - case "hex": - case "base64": - default: - return logical.ErrorResponse("unsupported encoding format %q; must be \"hex\" or \"base64\"", format), nil - } - - randBytes, err := uuid.GenerateRandomBytes(bytes) - 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, - }, - } - return resp, nil +func (b *backend) pathRandomWrite(_ context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return random.HandleRandomAPI(d, b.GetRandomReader()) } const pathRandomHelpSyn = `Generate random bytes` diff --git a/builtin/logical/transit/path_random_test.go b/builtin/logical/transit/path_random_test.go index 994345394..037a00b55 100644 --- a/builtin/logical/transit/path_random_test.go +++ b/builtin/logical/transit/path_random_test.go @@ -4,9 +4,11 @@ import ( "context" "encoding/base64" "encoding/hex" + "fmt" "reflect" "testing" + "github.com/hashicorp/vault/helper/random" "github.com/hashicorp/vault/sdk/logical" ) @@ -81,24 +83,42 @@ func TestTransit_Random(t *testing.T) { } } - // Test defaults - doRequest(req, false, "base64", 32) + for _, source := range []string{"", "platform", "seal", "all"} { + req.Data["source"] = source + req.Data["bytes"] = 32 + req.Data["format"] = "base64" + req.Path = "random" + // Test defaults + doRequest(req, false, "base64", 32) - // Test size selection in the path - req.Path = "random/24" - req.Data["format"] = "hex" - doRequest(req, false, "hex", 24) + // Test size selection in the path + req.Path = "random/24" + req.Data["format"] = "hex" + doRequest(req, false, "hex", 24) - // Test bad input/format - req.Path = "random" - req.Data["format"] = "base92" - doRequest(req, true, "", 0) + if source != "" { + // Test source selection in the path + req.Path = fmt.Sprintf("random/%s", source) + req.Data["format"] = "hex" + doRequest(req, false, "hex", 32) - req.Data["format"] = "hex" - req.Data["bytes"] = -1 - doRequest(req, true, "", 0) + req.Path = fmt.Sprintf("random/%s/24", source) + req.Data["format"] = "hex" + doRequest(req, false, "hex", 24) + } - req.Data["format"] = "hex" - req.Data["bytes"] = maxBytes + 1 - doRequest(req, true, "", 0) + // Test bad input/format + req.Path = "random" + req.Data["format"] = "base92" + doRequest(req, true, "", 0) + + req.Data["format"] = "hex" + req.Data["bytes"] = -1 + doRequest(req, true, "", 0) + + req.Data["format"] = "hex" + req.Data["bytes"] = random.APIMaxBytes + 1 + + doRequest(req, true, "", 0) + } } diff --git a/changelog/15213.txt b/changelog/15213.txt new file mode 100644 index 000000000..b5774b2d2 --- /dev/null +++ b/changelog/15213.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core,transit: Allow callers to choose random byte source including entropy augmentation sources for the sys/tools/random and transit/random endpoints. +``` \ No newline at end of file diff --git a/helper/random/random_api.go b/helper/random/random_api.go new file mode 100644 index 000000000..1cee54272 --- /dev/null +++ b/helper/random/random_api.go @@ -0,0 +1,115 @@ +package random + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "io" + "strconv" + + "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 + +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 + } + } + 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" should be less than %d`, APIMaxBytes), 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 + var warning string + switch source { + case "", "platform": + randBytes, err = uuid.GenerateRandomBytes(bytes) + 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": + var sealBytes []byte + sealBytes, err = uuid.GenerateRandomBytesWithReader(bytes, additionalSource) + if err == nil { + randBytes, err = uuid.GenerateRandomBytes(bytes) + 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 + } + 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 isValidSource(s string) bool { + switch s { + case "", "platform", "seal", "all": + return true + } + return false +} diff --git a/vault/logical_system.go b/vault/logical_system.go index 777296216..7af2a737f 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -29,7 +29,6 @@ import ( "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/strutil" - "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/hostutil" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/metricsutil" @@ -3610,55 +3609,8 @@ func (b *SystemBackend) pathHashWrite(ctx context.Context, req *logical.Request, return resp, nil } -func (b *SystemBackend) pathRandomWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - bytes := 0 - var err error - strBytes := d.Get("urlbytes").(string) - if strBytes != "" { - bytes, err = strconv.Atoi(strBytes) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf("error parsing url-set byte count: %s", err)), nil - } - } else { - bytes = d.Get("bytes").(int) - } - format := d.Get("format").(string) - - if bytes < 1 { - return logical.ErrorResponse(`"bytes" cannot be less than 1`), nil - } - - if bytes > maxBytes { - return logical.ErrorResponse(`"bytes" should be less than %d`, maxBytes), nil - } - - switch format { - case "hex": - case "base64": - default: - return logical.ErrorResponse("unsupported encoding format %q; must be \"hex\" or \"base64\"", format), nil - } - - randBytes, err := uuid.GenerateRandomBytes(bytes) - 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, - }, - } - return resp, nil +func (b *SystemBackend) pathRandomWrite(_ context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return random.HandleRandomAPI(d, b.Core.secureRandomReader) } func hasMountAccess(ctx context.Context, acl *ACL, path string) bool { diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index 751c27fde..c6d36b431 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -855,7 +855,7 @@ func (b *SystemBackend) toolsPaths() []*framework.Path { }, { - Pattern: "tools/random" + framework.OptionalParamRegex("urlbytes"), + Pattern: "tools/random(/" + framework.GenericNameRegex("source") + ")?" + framework.OptionalParamRegex("urlbytes"), Fields: map[string]*framework.FieldSchema{ "urlbytes": { Type: framework.TypeString, @@ -873,6 +873,12 @@ func (b *SystemBackend) toolsPaths() []*framework.Path { Default: "base64", Description: `Encoding format to use. Can be "hex" or "base64". Defaults to "base64".`, }, + + "source": { + Type: framework.TypeString, + Default: "platform", + Description: `Which system to source random data from, ether "platform", "seal", or "all".`, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index 80269a135..0e2036480 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -652,9 +652,9 @@ $ curl \ This endpoint returns high-quality random bytes of the specified length. -| Method | Path | -| :----- | :------------------------- | -| `POST` | `/transit/random(/:bytes)` | +| Method | Path | +| :----- | :----------------------------------- | +| `POST` | `/transit/random(/:source)(/:bytes)` | ### Parameters @@ -664,6 +664,11 @@ This endpoint returns high-quality random bytes of the specified length. - `format` `(string: "base64")` – Specifies the output encoding. Valid options are `hex` or `base64`. +- `source` `(string: "platform")` - Specifies the source of the requested bytes. + `platform`, the default, sources bytes from the platform's entropy source. + `seal` sources from entropy augmentation (enterprise only). + `all` mixes bytes from all available sources. + ### Sample Payload ```json diff --git a/website/content/api-docs/system/tools.mdx b/website/content/api-docs/system/tools.mdx index a38ff80d0..ee7b6d333 100644 --- a/website/content/api-docs/system/tools.mdx +++ b/website/content/api-docs/system/tools.mdx @@ -12,9 +12,9 @@ The `/sys/tools` endpoints are a general set of tools. This endpoint returns high-quality random bytes of the specified length. -| Method | Path | -| :----- | :--------------------------- | -| `POST` | `/sys/tools/random(/:bytes)` | +| Method | Path | +| :----- | :------------------------------------- | +| `POST` | `/sys/tools/random(/:source)(/:bytes)` | ### Parameters @@ -24,6 +24,11 @@ This endpoint returns high-quality random bytes of the specified length. - `format` `(string: "base64")` – Specifies the output encoding. Valid options are `hex` or `base64`. +- `source` `(string: "platform")` - Specifies the source of the requested bytes. + `platform`, the default, sources bytes from the platform's entropy source. + `seal` sources from entropy augmentation (enterprise only). + `all` mixes bytes from all available sources. + ### Sample Payload ```json