Allow callers to choose the entropy source for the random endpoints. (#15213)
* Allow callers to choose the entropy source for the random endpoints * Put source in the URL for sys as well * changelog * docs * Fix unit tests, and add coverage * refactor to use a single common implementation * Update documentation * one more tweak * more cleanup * Readd lost test expected code * fmt
This commit is contained in:
parent
6d9888e09b
commit
bef350c916
|
@ -2,21 +2,14 @@ package transit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"github.com/hashicorp/vault/helper/random"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
uuid "github.com/hashicorp/go-uuid"
|
|
||||||
"github.com/hashicorp/vault/sdk/framework"
|
"github.com/hashicorp/vault/sdk/framework"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxBytes = 128 * 1024
|
|
||||||
|
|
||||||
func (b *backend) pathRandom() *framework.Path {
|
func (b *backend) pathRandom() *framework.Path {
|
||||||
return &framework.Path{
|
return &framework.Path{
|
||||||
Pattern: "random" + framework.OptionalParamRegex("urlbytes"),
|
Pattern: "random(/" + framework.GenericNameRegex("source") + ")?" + framework.OptionalParamRegex("urlbytes"),
|
||||||
Fields: map[string]*framework.FieldSchema{
|
Fields: map[string]*framework.FieldSchema{
|
||||||
"urlbytes": {
|
"urlbytes": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
|
@ -34,6 +27,12 @@ func (b *backend) pathRandom() *framework.Path {
|
||||||
Default: "base64",
|
Default: "base64",
|
||||||
Description: `Encoding format to use. Can be "hex" or "base64". Defaults to "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{
|
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) {
|
func (b *backend) pathRandomWrite(_ context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
bytes := 0
|
return random.HandleRandomAPI(d, b.GetRandomReader())
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathRandomHelpSyn = `Generate random bytes`
|
const pathRandomHelpSyn = `Generate random bytes`
|
||||||
|
|
|
@ -4,9 +4,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/random"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -81,24 +83,42 @@ func TestTransit_Random(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test defaults
|
for _, source := range []string{"", "platform", "seal", "all"} {
|
||||||
doRequest(req, false, "base64", 32)
|
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
|
// Test size selection in the path
|
||||||
req.Path = "random/24"
|
req.Path = "random/24"
|
||||||
req.Data["format"] = "hex"
|
req.Data["format"] = "hex"
|
||||||
doRequest(req, false, "hex", 24)
|
doRequest(req, false, "hex", 24)
|
||||||
|
|
||||||
// Test bad input/format
|
if source != "" {
|
||||||
req.Path = "random"
|
// Test source selection in the path
|
||||||
req.Data["format"] = "base92"
|
req.Path = fmt.Sprintf("random/%s", source)
|
||||||
doRequest(req, true, "", 0)
|
req.Data["format"] = "hex"
|
||||||
|
doRequest(req, false, "hex", 32)
|
||||||
|
|
||||||
req.Data["format"] = "hex"
|
req.Path = fmt.Sprintf("random/%s/24", source)
|
||||||
req.Data["bytes"] = -1
|
req.Data["format"] = "hex"
|
||||||
doRequest(req, true, "", 0)
|
doRequest(req, false, "hex", 24)
|
||||||
|
}
|
||||||
|
|
||||||
req.Data["format"] = "hex"
|
// Test bad input/format
|
||||||
req.Data["bytes"] = maxBytes + 1
|
req.Path = "random"
|
||||||
doRequest(req, true, "", 0)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
```
|
|
@ -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
|
||||||
|
}
|
|
@ -29,7 +29,6 @@ import (
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||||
"github.com/hashicorp/go-uuid"
|
|
||||||
"github.com/hashicorp/vault/helper/hostutil"
|
"github.com/hashicorp/vault/helper/hostutil"
|
||||||
"github.com/hashicorp/vault/helper/identity"
|
"github.com/hashicorp/vault/helper/identity"
|
||||||
"github.com/hashicorp/vault/helper/metricsutil"
|
"github.com/hashicorp/vault/helper/metricsutil"
|
||||||
|
@ -3610,55 +3609,8 @@ func (b *SystemBackend) pathHashWrite(ctx context.Context, req *logical.Request,
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SystemBackend) pathRandomWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
func (b *SystemBackend) pathRandomWrite(_ context.Context, _ *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||||
bytes := 0
|
return random.HandleRandomAPI(d, b.Core.secureRandomReader)
|
||||||
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 hasMountAccess(ctx context.Context, acl *ACL, path string) bool {
|
func hasMountAccess(ctx context.Context, acl *ACL, path string) bool {
|
||||||
|
|
|
@ -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{
|
Fields: map[string]*framework.FieldSchema{
|
||||||
"urlbytes": {
|
"urlbytes": {
|
||||||
Type: framework.TypeString,
|
Type: framework.TypeString,
|
||||||
|
@ -873,6 +873,12 @@ func (b *SystemBackend) toolsPaths() []*framework.Path {
|
||||||
Default: "base64",
|
Default: "base64",
|
||||||
Description: `Encoding format to use. Can be "hex" or "base64". Defaults to "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{
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||||
|
|
|
@ -652,9 +652,9 @@ $ curl \
|
||||||
|
|
||||||
This endpoint returns high-quality random bytes of the specified length.
|
This endpoint returns high-quality random bytes of the specified length.
|
||||||
|
|
||||||
| Method | Path |
|
| Method | Path |
|
||||||
| :----- | :------------------------- |
|
| :----- | :----------------------------------- |
|
||||||
| `POST` | `/transit/random(/:bytes)` |
|
| `POST` | `/transit/random(/:source)(/:bytes)` |
|
||||||
|
|
||||||
### Parameters
|
### 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
|
- `format` `(string: "base64")` – Specifies the output encoding. Valid options
|
||||||
are `hex` or `base64`.
|
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
|
### Sample Payload
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
|
@ -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.
|
This endpoint returns high-quality random bytes of the specified length.
|
||||||
|
|
||||||
| Method | Path |
|
| Method | Path |
|
||||||
| :----- | :--------------------------- |
|
| :----- | :------------------------------------- |
|
||||||
| `POST` | `/sys/tools/random(/:bytes)` |
|
| `POST` | `/sys/tools/random(/:source)(/:bytes)` |
|
||||||
|
|
||||||
### Parameters
|
### 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
|
- `format` `(string: "base64")` – Specifies the output encoding. Valid options
|
||||||
are `hex` or `base64`.
|
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
|
### Sample Payload
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
Loading…
Reference in New Issue