Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Batch hmac - (#5850) #5875

Merged
merged 7 commits into from
Mar 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions builtin/logical/transit/path_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func TestTransit_ConfigSettings(t *testing.T) {

req.Data = map[string]interface{}{
"plaintext": "abcd",
"input": "abcd",
"context": "abcd",
}

Expand Down
316 changes: 236 additions & 80 deletions builtin/logical/transit/path_hmac.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,36 @@ import (
"strconv"
"strings"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/helper/keysutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
"github.com/mitchellh/mapstructure"
)

// BatchRequestHMACItem represents a request item for batch processing.
// A map type allows us to distinguish between empty and missing values.
type batchRequestHMACItem map[string]string

// BatchResponseItem represents a response item for batch processing
type batchResponseHMACItem struct {
// HMAC for the input present in the corresponding batch request item
HMAC string `json:"hmac,omitempty" structs:"hmac" mapstructure:"hmac"`

// Valid indicates whether signature matches the signature derived from the input string
Valid bool `json:"valid,omitempty" structs:"valid" mapstructure:"valid"`

// Error, if set represents a failure encountered while encrypting a
// corresponding batch request item
Error string `json:"error,omitempty" structs:"error" mapstructure:"error"`

// The return paths in some cases are (nil, err) and others
// (logical.ErrorResponse(..),nil), and others (logical.ErrorResponse(..),err).
// For batch processing to successfully mimic previous handling for simple 'input',
// both output values are needed - though 'err' should never be serialized.
err error
}

func (b *backend) pathHMAC() *framework.Path {
return &framework.Path{
Pattern: "hmac/" + framework.GenericNameRegex("name") + framework.OptionalParamRegex("urlalgorithm"),
Expand Down Expand Up @@ -68,17 +93,12 @@ to the min_encryption_version configured on the key.`,
func (b *backend) pathHMACWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
ver := d.Get("key_version").(int)
inputB64 := d.Get("input").(string)

algorithm := d.Get("urlalgorithm").(string)
if algorithm == "" {
algorithm = d.Get("algorithm").(string)
}

input, err := base64.StdEncoding.DecodeString(inputB64)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("unable to decode input as base64: %s", err)), logical.ErrInvalidRequest
}

// Get the policy
p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Expand Down Expand Up @@ -116,70 +136,105 @@ func (b *backend) pathHMACWrite(ctx context.Context, req *logical.Request, d *fr
return nil, fmt.Errorf("HMAC key value could not be computed")
}

var hf hash.Hash
var hashAlg func() hash.Hash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably use the keysutil.HashTypeMap stuff we are using elsewhere in transit

switch algorithm {
case "sha2-224":
hf = hmac.New(sha256.New224, key)
hashAlg = sha256.New224
case "sha2-256":
hf = hmac.New(sha256.New, key)
hashAlg = sha256.New
case "sha2-384":
hf = hmac.New(sha512.New384, key)
hashAlg = sha512.New384
case "sha2-512":
hf = hmac.New(sha512.New, key)
hashAlg = sha512.New
default:
p.Unlock()
return logical.ErrorResponse(fmt.Sprintf("unsupported algorithm %s", algorithm)), nil
}
hf.Write(input)
retBytes := hf.Sum(nil)

retStr := base64.StdEncoding.EncodeToString(retBytes)
retStr = fmt.Sprintf("vault:v%s:%s", strconv.Itoa(ver), retStr)
batchInputRaw := d.Raw["batch_input"]
var batchInputItems []batchRequestHMACItem
if batchInputRaw != nil {
err = mapstructure.Decode(batchInputRaw, &batchInputItems)
if err != nil {
p.Unlock()
return nil, errwrap.Wrapf("failed to parse batch input: {{err}}", err)
}

if len(batchInputItems) == 0 {
p.Unlock()
return logical.ErrorResponse("missing batch input to process"), logical.ErrInvalidRequest
}
} else {
valueRaw, ok := d.GetOk("input")
if !ok {
p.Unlock()
return logical.ErrorResponse("missing input for HMAC"), logical.ErrInvalidRequest
}

batchInputItems = make([]batchRequestHMACItem, 1)
batchInputItems[0] = batchRequestHMACItem{
"input": valueRaw.(string),
}
}

// Generate the response
resp := &logical.Response{
Data: map[string]interface{}{
"hmac": retStr,
},
response := make([]batchResponseHMACItem, len(batchInputItems))

for i, item := range batchInputItems {
rawInput, ok := item["input"]
if !ok {
response[i].Error = "missing input for HMAC"
response[i].err = logical.ErrInvalidRequest
continue
}

input, err := base64.StdEncoding.DecodeString(rawInput)
if err != nil {
response[i].Error = fmt.Sprintf("unable to decode input as base64: %s", err)
response[i].err = logical.ErrInvalidRequest
continue
}

var hf = hmac.New(hashAlg, key)
hf.Write(input)
retBytes := hf.Sum(nil)

retStr := base64.StdEncoding.EncodeToString(retBytes)
retStr = fmt.Sprintf("vault:v%s:%s", strconv.Itoa(ver), retStr)
response[i].HMAC = retStr
}

p.Unlock()

// Generate the response
resp := &logical.Response{}
if batchInputRaw != nil {
resp.Data = map[string]interface{}{
"batch_results": response,
}
} else {
if response[0].Error != "" || response[0].err != nil {
if response[0].Error != "" {
return logical.ErrorResponse(response[0].Error), response[0].err
} else {
return nil, response[0].err
}
}
resp.Data = map[string]interface{}{
"hmac": response[0].HMAC,
}
}

return resp, nil
}

func (b *backend) pathHMACVerify(ctx context.Context, req *logical.Request, d *framework.FieldData, verificationHMAC string) (*logical.Response, error) {
func (b *backend) pathHMACVerify(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {

name := d.Get("name").(string)
inputB64 := d.Get("input").(string)
algorithm := d.Get("urlalgorithm").(string)
if algorithm == "" {
algorithm = d.Get("algorithm").(string)
}

input, err := base64.StdEncoding.DecodeString(inputB64)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("unable to decode input as base64: %s", err)), logical.ErrInvalidRequest
}

// Verify the prefix
if !strings.HasPrefix(verificationHMAC, "vault:v") {
return logical.ErrorResponse("invalid HMAC to verify: no prefix"), logical.ErrInvalidRequest
}

splitVerificationHMAC := strings.SplitN(strings.TrimPrefix(verificationHMAC, "vault:v"), ":", 2)
if len(splitVerificationHMAC) != 2 {
return logical.ErrorResponse("invalid HMAC: wrong number of fields"), logical.ErrInvalidRequest
}

ver, err := strconv.Atoi(splitVerificationHMAC[0])
if err != nil {
return logical.ErrorResponse("invalid HMAC: version number could not be decoded"), logical.ErrInvalidRequest
}

verBytes, err := base64.StdEncoding.DecodeString(splitVerificationHMAC[1])
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("unable to decode verification HMAC as base64: %s", err)), logical.ErrInvalidRequest
}

// Get the policy
p, _, err := b.lm.GetPolicy(ctx, keysutil.PolicyRequest{
Storage: req.Storage,
Expand All @@ -195,49 +250,150 @@ func (b *backend) pathHMACVerify(ctx context.Context, req *logical.Request, d *f
p.Lock(false)
}

if ver > p.LatestVersion {
p.Unlock()
return logical.ErrorResponse("invalid HMAC: version is too new"), logical.ErrInvalidRequest
}

if p.MinDecryptionVersion > 0 && ver < p.MinDecryptionVersion {
p.Unlock()
return logical.ErrorResponse("cannot verify HMAC: version is too old (disallowed by policy)"), logical.ErrInvalidRequest
}

key, err := p.HMACKey(ver)
if err != nil {
p.Unlock()
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
if key == nil {
p.Unlock()
return nil, fmt.Errorf("HMAC key value could not be computed")
}

var hf hash.Hash
var hashAlg func() hash.Hash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment

switch algorithm {
case "sha2-224":
hf = hmac.New(sha256.New224, key)
hashAlg = sha256.New224
case "sha2-256":
hf = hmac.New(sha256.New, key)
hashAlg = sha256.New
case "sha2-384":
hf = hmac.New(sha512.New384, key)
hashAlg = sha512.New384
case "sha2-512":
hf = hmac.New(sha512.New, key)
hashAlg = sha512.New
default:
p.Unlock()
return logical.ErrorResponse(fmt.Sprintf("unsupported algorithm %s", algorithm)), nil
return logical.ErrorResponse(fmt.Sprintf("unsupported hash algorithm %s", algorithm)), nil
}

batchInputRaw := d.Raw["batch_input"]
var batchInputItems []batchRequestHMACItem
if batchInputRaw != nil {
err := mapstructure.Decode(batchInputRaw, &batchInputItems)
if err != nil {
p.Unlock()
return nil, errwrap.Wrapf("failed to parse batch input: {{err}}", err)
}

if len(batchInputItems) == 0 {
p.Unlock()
return logical.ErrorResponse("missing batch input to process"), logical.ErrInvalidRequest
}
} else {
// use empty string if input is missing - not an error
inputB64 := d.Get("input").(string)
hmac := d.Get("hmac").(string)

batchInputItems = make([]batchRequestHMACItem, 1)
batchInputItems[0] = batchRequestHMACItem{
"input": inputB64,
"hmac": hmac,
}
}

response := make([]batchResponseHMACItem, len(batchInputItems))

for i, item := range batchInputItems {
rawInput, ok := item["input"]
if !ok {
response[i].Error = "missing input"
response[i].err = logical.ErrInvalidRequest
continue
}

input, err := base64.StdEncoding.DecodeString(rawInput)
if err != nil {
response[i].Error = fmt.Sprintf("unable to decode input as base64: %s", err)
response[i].err = logical.ErrInvalidRequest
continue
}

verificationHMAC, ok := item["hmac"]
if !ok {
response[i].Error = "missing hmac"
response[i].err = logical.ErrInvalidRequest
continue
}

// Verify the prefix
if !strings.HasPrefix(verificationHMAC, "vault:v") {
response[i].Error = "invalid HMAC to verify: no prefix"
response[i].err = logical.ErrInvalidRequest
continue
}

splitVerificationHMAC := strings.SplitN(strings.TrimPrefix(verificationHMAC, "vault:v"), ":", 2)
if len(splitVerificationHMAC) != 2 {
response[i].Error = "invalid HMAC: wrong number of fields"
response[i].err = logical.ErrInvalidRequest
continue
}

ver, err := strconv.Atoi(splitVerificationHMAC[0])
if err != nil {
response[i].Error = "invalid HMAC: version number could not be decoded"
response[i].err = logical.ErrInvalidRequest
continue
}

verBytes, err := base64.StdEncoding.DecodeString(splitVerificationHMAC[1])
if err != nil {
response[i].Error = fmt.Sprintf("unable to decode verification HMAC as base64: %s", err)
response[i].err = logical.ErrInvalidRequest
continue
}

if ver > p.LatestVersion {
response[i].Error = "invalid HMAC: version is too new"
response[i].err = logical.ErrInvalidRequest
continue
}

if p.MinDecryptionVersion > 0 && ver < p.MinDecryptionVersion {
response[i].Error = "cannot verify HMAC: version is too old (disallowed by policy)"
response[i].err = logical.ErrInvalidRequest
continue
}

key, err := p.HMACKey(ver)
if err != nil {
response[i].Error = err.Error()
response[i].err = logical.ErrInvalidRequest
continue
}
if key == nil {
response[i].Error = ""
response[i].err = fmt.Errorf("HMAC key value could not be computed")
continue
}

var hf = hmac.New(hashAlg, key)
hf.Write(input)
retBytes := hf.Sum(nil)
response[i].Valid = hmac.Equal(retBytes, verBytes)
}
hf.Write(input)
retBytes := hf.Sum(nil)

p.Unlock()
return &logical.Response{
Data: map[string]interface{}{
"valid": hmac.Equal(retBytes, verBytes),
},
}, nil

// Generate the response
resp := &logical.Response{}
if batchInputRaw != nil {
resp.Data = map[string]interface{}{
"batch_results": response,
}
} else {
if response[0].Error != "" || response[0].err != nil {
if response[0].Error != "" {
return logical.ErrorResponse(response[0].Error), response[0].err
} else {
return nil, response[0].err
}
}
resp.Data = map[string]interface{}{
"valid": response[0].Valid,
}
}

return resp, nil
}

const pathHMACHelpSyn = `Generate an HMAC for input data using the named key`
Expand Down
Loading