Skip to content

Commit

Permalink
Reject supplied nonces for non-convergent encryption operations (#22852
Browse files Browse the repository at this point in the history
…) (#22902)

Backport to 1.12.x
  • Loading branch information
sgmiller committed Sep 8, 2023
1 parent d653525 commit ac9b65f
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 13 deletions.
25 changes: 16 additions & 9 deletions builtin/logical/transit/backend_test.go
Expand Up @@ -1111,10 +1111,12 @@ func testConvergentEncryptionCommon(t *testing.T, ver int, keyType keysutil.KeyT

// Now test encrypting the same value twice
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"nonce": "b25ldHdvdGhyZWVl", // "onetwothreee"
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
if ver == 0 {
req.Data["nonce"] = "b25ldHdvdGhyZWVl" // "onetwothreee"
}
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1145,11 +1147,10 @@ func testConvergentEncryptionCommon(t *testing.T, ver int, keyType keysutil.KeyT

// For sanity, also check a different nonce value...
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"nonce": "dHdvdGhyZWVmb3Vy", // "twothreefour"
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
if ver < 2 {
if ver == 0 {
req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour"
} else {
req.Data["context"] = "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOldandSdd7S"
Expand Down Expand Up @@ -1188,10 +1189,12 @@ func testConvergentEncryptionCommon(t *testing.T, ver int, keyType keysutil.KeyT

// ...and a different context value
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"nonce": "dHdvdGhyZWVmb3Vy", // "twothreefour"
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "qV4h9iQyvn+raODOer4JNAsOhkXBwdT4HZ677Ql4KLqXSU+Jk4C/fXBWbv6xkSYT",
}
if ver == 0 {
req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour"
}
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -1303,9 +1306,11 @@ func testConvergentEncryptionCommon(t *testing.T, ver int, keyType keysutil.KeyT
// Finally, check operations on empty values
// First, check without setting a plaintext at all
req.Data = map[string]interface{}{
"nonce": "b25ldHdvdGhyZWVl", // "onetwothreee"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
if ver == 0 {
req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour"
}
resp, err = b.HandleRequest(context.Background(), req)
if err == nil {
t.Fatal("expected error, got nil")
Expand All @@ -1320,9 +1325,11 @@ func testConvergentEncryptionCommon(t *testing.T, ver int, keyType keysutil.KeyT
// Now set plaintext to empty
req.Data = map[string]interface{}{
"plaintext": "",
"nonce": "b25ldHdvdGhyZWVl", // "onetwothreee"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
if ver == 0 {
req.Data["nonce"] = "dHdvdGhyZWVmb3Vy" // "twothreefour"
}
resp, err = b.HandleRequest(context.Background(), req)
if err != nil {
t.Fatal(err)
Expand Down
4 changes: 4 additions & 0 deletions builtin/logical/transit/path_datakey.go
Expand Up @@ -160,6 +160,10 @@ func (b *backend) pathDatakeyWrite(ctx context.Context, req *logical.Request, d
},
}

if len(nonce) > 0 && !nonceAllowed(p) {
return nil, ErrNonceNotAllowed
}

if constants.IsFIPS() && shouldWarnAboutNonceUsage(p, nonce) {
resp.AddWarning("A provided nonce value was used within FIPS mode, this violates FIPS 140 compliance.")
}
Expand Down
26 changes: 26 additions & 0 deletions builtin/logical/transit/path_encrypt.go
Expand Up @@ -395,6 +395,13 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d
successesInBatch := false
for i, item := range batchInputItems {
if batchResponseItems[i].Error != "" {
userErrorInBatch = true
continue
}

if item.Nonce != "" && !nonceAllowed(p) {
userErrorInBatch = true
batchResponseItems[i].Error = ErrNonceNotAllowed.Error()
continue
}

Expand Down Expand Up @@ -465,6 +472,25 @@ func (b *backend) pathEncryptWrite(ctx context.Context, req *logical.Request, d
return batchRequestResponse(d, resp, req, successesInBatch, userErrorInBatch, internalErrorInBatch)
}

func nonceAllowed(p *keysutil.Policy) bool {
var supportedKeyType bool
switch p.Type {
case keysutil.KeyType_MANAGED_KEY:
return true
case keysutil.KeyType_AES128_GCM96, keysutil.KeyType_AES256_GCM96, keysutil.KeyType_ChaCha20_Poly1305:
supportedKeyType = true
default:
supportedKeyType = false
}

if supportedKeyType && p.ConvergentEncryption && p.ConvergentVersion == 1 {
// We only use the user supplied nonce for v1 convergent encryption keys
return true
}

return false
}

// Depending on the errors in the batch, different status codes should be returned. User errors
// will return a 400 and precede internal errors which return a 500. The reasoning behind this is
// that user errors are non-retryable without making changes to the request, and should be surfaced
Expand Down
81 changes: 78 additions & 3 deletions builtin/logical/transit/path_encrypt_test.go
Expand Up @@ -3,6 +3,7 @@ package transit
import (
"context"
"encoding/json"
"net/http"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -645,13 +646,26 @@ func TestTransit_BatchEncryptionCase12(t *testing.T) {
}

// Case13: Incorrect input for nonce when we aren't in convergent encryption should fail the operation
func TestTransit_BatchEncryptionCase13(t *testing.T) {
func TestTransit_EncryptionCase13(t *testing.T) {
var err error

b, s := createBackendWithStorage(t)

// Non-batch first
data := map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "nonce": "R80hr9eNUIuFV52e"}
req := &logical.Request{
Operation: logical.CreateOperation,
Path: "encrypt/my-key",
Storage: s,
Data: data,
}
resp, err := b.HandleRequest(context.Background(), req)
if err == nil {
t.Fatal("expected invalid request")
}

batchInput := []interface{}{
map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "nonce": "YmFkbm9uY2U="},
map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "nonce": "R80hr9eNUIuFV52e"},
}

batchData := map[string]interface{}{
Expand All @@ -663,10 +677,71 @@ func TestTransit_BatchEncryptionCase13(t *testing.T) {
Storage: s,
Data: batchData,
}
_, err = b.HandleRequest(context.Background(), batchReq)
resp, err = b.HandleRequest(context.Background(), batchReq)
if err != nil {
t.Fatal(err)
}

if v, ok := resp.Data["http_status_code"]; !ok || v.(int) != http.StatusBadRequest {
t.Fatal("expected request error")
}
}

// Case14: Incorrect input for nonce when we are in convergent version 3 should fail
func TestTransit_EncryptionCase14(t *testing.T) {
var err error

b, s := createBackendWithStorage(t)

cReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "keys/my-key",
Storage: s,
Data: map[string]interface{}{
"convergent_encryption": "true",
"derived": "true",
},
}
resp, err := b.HandleRequest(context.Background(), cReq)
if err != nil {
t.Fatal(err)
}

// Non-batch first
data := map[string]interface{}{"plaintext": "bXkgc2VjcmV0IGRhdGE=", "context": "SGVsbG8sIFdvcmxkCg==", "nonce": "R80hr9eNUIuFV52e"}
req := &logical.Request{
Operation: logical.CreateOperation,
Path: "encrypt/my-key",
Storage: s,
Data: data,
}

resp, err = b.HandleRequest(context.Background(), req)
if err == nil {
t.Fatal("expected invalid request")
}

batchInput := []interface{}{
data,
}

batchData := map[string]interface{}{
"batch_input": batchInput,
}
batchReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "encrypt/my-key",
Storage: s,
Data: batchData,
}
resp, err = b.HandleRequest(context.Background(), batchReq)
if err != nil {
t.Fatal(err)
}

if v, ok := resp.Data["http_status_code"]; !ok || v.(int) != http.StatusBadRequest {
t.Fatal("expected request error")
}
}

// Test that the fast path function decodeBatchRequestItems behave like mapstructure.Decode() to decode []BatchRequestItem.
Expand Down
8 changes: 8 additions & 0 deletions builtin/logical/transit/path_rewrap.go
Expand Up @@ -3,6 +3,7 @@ package transit
import (
"context"
"encoding/base64"
"errors"
"fmt"

"github.com/hashicorp/vault/helper/constants"
Expand All @@ -13,6 +14,8 @@ import (
"github.com/mitchellh/mapstructure"
)

var ErrNonceNotAllowed = errors.New("provided nonce not allowed for this key")

func (b *backend) pathRewrap() *framework.Path {
return &framework.Path{
Pattern: "rewrap/" + framework.GenericNameRegex("name"),
Expand Down Expand Up @@ -143,6 +146,11 @@ func (b *backend) pathRewrapWrite(ctx context.Context, req *logical.Request, d *
continue
}

if item.Nonce != "" && !nonceAllowed(p) {
batchResponseItems[i].Error = ErrNonceNotAllowed.Error()
continue
}

plaintext, err := p.Decrypt(item.DecodedContext, item.DecodedNonce, item.Ciphertext)
if err != nil {
switch err.(type) {
Expand Down
3 changes: 3 additions & 0 deletions changelog/22852.txt
@@ -0,0 +1,3 @@
```release-note:security
secrets/transit: fix a regression that was honoring nonces provided in non-convergent modes during encryption.
```
8 changes: 7 additions & 1 deletion sdk/helper/keysutil/policy.go
Expand Up @@ -1873,9 +1873,15 @@ func (p *Policy) EncryptWithFactory(ver int, context []byte, nonce []byte, value

encBytes := 32
hmacBytes := 0
if p.convergentVersion(ver) > 2 {
convergentVersion := p.convergentVersion(ver)
if convergentVersion > 2 {
deriveHMAC = true
hmacBytes = 32
if len(nonce) > 0 {
return "", errutil.UserError{Err: "nonce provided when not allowed"}
}
} else if len(nonce) > 0 && (!p.ConvergentEncryption || convergentVersion != 1) {
return "", errutil.UserError{Err: "nonce provided when not allowed"}
}
if p.Type == KeyType_AES128_GCM96 {
encBytes = 16
Expand Down

0 comments on commit ac9b65f

Please sign in to comment.