Merge pull request #1696 from hashicorp/transit-convergent-specify-nonce

Require nonce specification for more flexibility
This commit is contained in:
Jeff Mitchell 2016-08-08 11:41:10 -04:00 committed by GitHub
commit 0a67bcb5bd
10 changed files with 204 additions and 89 deletions

View File

@ -29,9 +29,10 @@ FEATURES:
deprecates App-ID. [GH-1426]
* **Convergent Encryption in `Transit`**: The `transit` backend now supports a
convergent encryption mode where the same plaintext will produce the same
ciphertext. Although very useful in some situations, this has security
implications, which are mostly mitigated by requiring the use of key
derivation when convergent encryption is enabled. See [the `transit`
ciphertext. Although very useful in some situations, this has potential
security implications, which are mostly mitigated by requiring the use of
key derivation when convergent encryption is enabled. See [the `transit`
backend
documentation](https://www.vaultproject.io/docs/secrets/transit/index.html)
for more details. [GH-1537]
* **Improved LDAP Group Filters**: The `ldap` auth backend now uses templates

View File

@ -56,19 +56,6 @@ func TestBackend_upsert(t *testing.T) {
})
}
func TestBackend_upsert_convergent(t *testing.T) {
decryptData := make(map[string]interface{})
logicaltest.Test(t, logicaltest.TestCase{
Factory: Factory,
Steps: []logicaltest.TestStep{
testAccStepReadPolicy(t, "test", true, false),
testAccStepEncryptUpsertConvergent(t, "test", testPlaintext, decryptData),
testAccStepReadPolicy(t, "test", false, false),
testAccStepDecrypt(t, "test", testPlaintext, decryptData),
},
})
}
func TestBackend_datakey(t *testing.T) {
dataKeyInfo := make(map[string]interface{})
logicaltest.Test(t, logicaltest.TestCase{
@ -323,30 +310,6 @@ func testAccStepEncryptUpsert(
}
}
func testAccStepEncryptUpsertConvergent(
t *testing.T, name, plaintext string, decryptData map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.CreateOperation,
Path: "encrypt/" + name,
Data: map[string]interface{}{
"plaintext": base64.StdEncoding.EncodeToString([]byte(plaintext)),
},
Check: func(resp *logical.Response) error {
var d struct {
Ciphertext string `mapstructure:"ciphertext"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
if d.Ciphertext == "" {
return fmt.Errorf("missing ciphertext")
}
decryptData["ciphertext"] = d.Ciphertext
return nil
},
}
}
func testAccStepEncryptContext(
t *testing.T, name, plaintext, context string, decryptData map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
@ -633,7 +596,8 @@ func TestConvergentEncryption(t *testing.T) {
req.Path = "encrypt/testkey"
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "Zm9vIGJhcg==", // "foo bar"
"nonce": "Zm9vIGJhcg==", // "foo bar"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
resp, err = b.HandleRequest(req)
if resp == nil {
@ -643,10 +607,21 @@ func TestConvergentEncryption(t *testing.T) {
t.Fatalf("expected error response, got %#v", *resp)
}
// Ensure we fail if we do not provide a nonce
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
resp, err = b.HandleRequest(req)
if err == nil && (resp == nil || !resp.IsError()) {
t.Fatal("expected error response")
}
// Now test encrypting the same value twice
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "b25ldHdvdGhyZWVl", // "onetwothreee"
"nonce": "b25ldHdvdGhyZWVl", // "onetwothreee"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
resp, err = b.HandleRequest(req)
if resp == nil {
@ -670,10 +645,11 @@ func TestConvergentEncryption(t *testing.T) {
t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext1, ciphertext2)
}
// For sanity, also check a different value
// For sanity, also check a different nonce value...
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "dHdvdGhyZWVmb3Vy", // "twothreefour"
"nonce": "dHdvdGhyZWVmb3Vy", // "twothreefour"
"context": "pWZ6t/im3AORd0lVYE0zBdKpX6Bl3/SvFtoVTPWbdkzjG788XmMAnOlxandSdd7S",
}
resp, err = b.HandleRequest(req)
if resp == nil {
@ -694,12 +670,45 @@ func TestConvergentEncryption(t *testing.T) {
ciphertext4 := resp.Data["ciphertext"].(string)
if ciphertext3 != ciphertext4 {
t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext1, ciphertext2)
t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext3, ciphertext4)
}
if ciphertext1 == ciphertext3 {
t.Fatalf("expected different ciphertexts")
}
// ...and a different context value
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"nonce": "dHdvdGhyZWVmb3Vy", // "twothreefour"
"context": "qV4h9iQyvn+raODOer4JNAsOhkXBwdT4HZ677Ql4KLqXSU+Jk4C/fXBWbv6xkSYT",
}
resp, err = b.HandleRequest(req)
if resp == nil {
t.Fatal("expected non-nil response")
}
if resp.IsError() {
t.Fatalf("got error response: %#v", *resp)
}
ciphertext5 := resp.Data["ciphertext"].(string)
resp, err = b.HandleRequest(req)
if resp == nil {
t.Fatal("expected non-nil response")
}
if resp.IsError() {
t.Fatalf("got error response: %#v", *resp)
}
ciphertext6 := resp.Data["ciphertext"].(string)
if ciphertext5 != ciphertext6 {
t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext5, ciphertext6)
}
if ciphertext1 == ciphertext5 {
t.Fatalf("expected different ciphertexts")
}
if ciphertext3 == ciphertext5 {
t.Fatalf("expected different ciphertexts")
}
}
func TestPolicyFuzzing(t *testing.T) {

View File

@ -132,7 +132,7 @@ func (lm *lockManager) GetPolicyExclusive(storage logical.Storage, name string)
// Get the policy with a read lock; if it returns that an exclusive lock is
// needed, retry. If successful, call one more time to get a read lock and
// return the value.
func (lm *lockManager) GetPolicyUpsert(storage logical.Storage, name string, derived bool, convergent bool) (*Policy, *sync.RWMutex, bool, error) {
func (lm *lockManager) GetPolicyUpsert(storage logical.Storage, name string, derived, convergent bool) (*Policy, *sync.RWMutex, bool, error) {
p, lock, _, err := lm.getPolicyCommon(storage, name, true, derived, convergent, shared)
if err == nil ||
(err != nil && err != errNeedExclusiveLock) {

View File

@ -30,6 +30,11 @@ ciphertext; "wrapped" will return the ciphertext only.`,
Description: "Context for key derivation. Required for derived keys.",
},
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Nonce for when convergent encryption is used",
},
"bits": &framework.FieldSchema{
Type: framework.TypeInt,
Description: `Number of bits for the key; currently 128, 256,
@ -61,17 +66,28 @@ func (b *backend) pathDatakeyWrite(
return logical.ErrorResponse("Invalid path, must be 'plaintext' or 'wrapped'"), logical.ErrInvalidRequest
}
var err error
// Decode the context if any
contextRaw := d.Get("context").(string)
var context []byte
if len(contextRaw) != 0 {
var err error
context, err = base64.StdEncoding.DecodeString(contextRaw)
if err != nil {
return logical.ErrorResponse("failed to decode context as base64"), logical.ErrInvalidRequest
}
}
// Decode the nonce if any
nonceRaw := d.Get("nonce").(string)
var nonce []byte
if len(nonceRaw) != 0 {
nonce, err = base64.StdEncoding.DecodeString(nonceRaw)
if err != nil {
return logical.ErrorResponse("failed to decode nonce as base64"), logical.ErrInvalidRequest
}
}
// Get the policy
p, lock, err := b.lm.GetPolicyShared(req.Storage, name)
if lock != nil {
@ -100,7 +116,7 @@ func (b *backend) pathDatakeyWrite(
return nil, err
}
ciphertext, err := p.Encrypt(context, base64.StdEncoding.EncodeToString(newKey))
ciphertext, err := p.Encrypt(context, nonce, base64.StdEncoding.EncodeToString(newKey))
if err != nil {
switch err.(type) {
case errutil.UserError:

View File

@ -27,6 +27,11 @@ func (b *backend) pathDecrypt() *framework.Path {
Type: framework.TypeString,
Description: "Context for key derivation. Required for derived keys.",
},
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Nonce for when convergent encryption is used",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -46,17 +51,28 @@ func (b *backend) pathDecryptWrite(
return logical.ErrorResponse("missing ciphertext to decrypt"), logical.ErrInvalidRequest
}
var err error
// Decode the context if any
contextRaw := d.Get("context").(string)
var context []byte
if len(contextRaw) != 0 {
var err error
context, err = base64.StdEncoding.DecodeString(contextRaw)
if err != nil {
return logical.ErrorResponse("failed to decode context as base64"), logical.ErrInvalidRequest
}
}
// Decode the nonce if any
nonceRaw := d.Get("nonce").(string)
var nonce []byte
if len(nonceRaw) != 0 {
nonce, err = base64.StdEncoding.DecodeString(nonceRaw)
if err != nil {
return logical.ErrorResponse("failed to decode nonce as base64"), logical.ErrInvalidRequest
}
}
// Get the policy
p, lock, err := b.lm.GetPolicyShared(req.Storage, name)
if lock != nil {
@ -69,7 +85,7 @@ func (b *backend) pathDecryptWrite(
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
}
plaintext, err := p.Decrypt(context, ciphertext)
plaintext, err := p.Decrypt(context, nonce, ciphertext)
if err != nil {
switch err.(type) {
case errutil.UserError:

View File

@ -28,6 +28,11 @@ func (b *backend) pathEncrypt() *framework.Path {
Type: framework.TypeString,
Description: "Context for key derivation. Required for derived keys.",
},
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Nonce for when convergent encryption is used",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -63,10 +68,11 @@ func (b *backend) pathEncryptWrite(
return logical.ErrorResponse("missing plaintext to encrypt"), logical.ErrInvalidRequest
}
var err error
// Decode the context if any
contextRaw := d.Get("context").(string)
var context []byte
var err error
if len(contextRaw) != 0 {
context, err = base64.StdEncoding.DecodeString(contextRaw)
if err != nil {
@ -74,6 +80,16 @@ func (b *backend) pathEncryptWrite(
}
}
// Decode the nonce if any
nonceRaw := d.Get("nonce").(string)
var nonce []byte
if len(nonceRaw) != 0 {
nonce, err = base64.StdEncoding.DecodeString(nonceRaw)
if err != nil {
return logical.ErrorResponse("failed to decode nonce as base64"), logical.ErrInvalidRequest
}
}
// Get the policy
var p *Policy
var lock *sync.RWMutex
@ -93,7 +109,7 @@ func (b *backend) pathEncryptWrite(
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
}
ciphertext, err := p.Encrypt(context, value)
ciphertext, err := p.Encrypt(context, nonce, value)
if err != nil {
switch err.(type) {
case errutil.UserError:

View File

@ -18,23 +18,25 @@ func (b *backend) pathKeys() *framework.Path {
},
"derived": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Enables key derivation mode. This allows for per-transaction unique keys",
Type: framework.TypeBool,
Description: `Enables key derivation mode. This
allows for per-transaction unique keys.`,
},
"convergent_encryption": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `Whether to use convergent encryption.
Description: `Whether to support convergent encryption.
This is only supported when using a key with
key derivation enabled and will require all
context values to be 96 bits (12 bytes) when
base64-decoded. This mode ensures that when
the same context is supplied, the same
ciphertext is emitted from the encryption
function. It is *very important* when using
this mode that you ensure that all contexts
are *globally unique*. Failing to do so will
severely impact the security of the key.`,
requests to carry both a context and 96-bit
(12-byte) nonce. The given nonce will be used
in place of a randomly generated nonce. As a
result, when the same context and nonce are
supplied, the same ciphertext is generated. It
is *very important* when using this mode that
you ensure that all nonces are unique for a
given context. Failing to do so will severely
impact the ciphertext's security.`,
},
},

View File

@ -27,6 +27,11 @@ func (b *backend) pathRewrap() *framework.Path {
Type: framework.TypeString,
Description: "Context for key derivation. Required for derived keys.",
},
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Nonce for when convergent encryption is used",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -47,17 +52,28 @@ func (b *backend) pathRewrapWrite(
return logical.ErrorResponse("missing ciphertext to decrypt"), logical.ErrInvalidRequest
}
var err error
// Decode the context if any
contextRaw := d.Get("context").(string)
var context []byte
if len(contextRaw) != 0 {
var err error
context, err = base64.StdEncoding.DecodeString(contextRaw)
if err != nil {
return logical.ErrorResponse("failed to decode context as base64"), logical.ErrInvalidRequest
}
}
// Decode the nonce if any
nonceRaw := d.Get("nonce").(string)
var nonce []byte
if len(nonceRaw) != 0 {
nonce, err = base64.StdEncoding.DecodeString(nonceRaw)
if err != nil {
return logical.ErrorResponse("failed to decode nonce as base64"), logical.ErrInvalidRequest
}
}
// Get the policy
p, lock, err := b.lm.GetPolicyShared(req.Storage, name)
if lock != nil {
@ -71,7 +87,7 @@ func (b *backend) pathRewrapWrite(
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
}
plaintext, err := p.Decrypt(context, value)
plaintext, err := p.Decrypt(context, nonce, value)
if err != nil {
switch err.(type) {
case errutil.UserError:
@ -87,7 +103,7 @@ func (b *backend) pathRewrapWrite(
return nil, fmt.Errorf("empty plaintext returned during rewrap")
}
ciphertext, err := p.Encrypt(context, plaintext)
ciphertext, err := p.Encrypt(context, nonce, plaintext)
if err != nil {
switch err.(type) {
case errutil.UserError:

View File

@ -307,10 +307,6 @@ func (p *Policy) DeriveKey(context []byte, ver int) ([]byte, error) {
return nil, errutil.InternalError{Err: "unable to access the key; no key versions found"}
}
if p.LatestVersion == 0 {
return nil, errutil.InternalError{Err: "unable to access the key; no key versions found"}
}
if ver <= 0 || ver > p.LatestVersion {
return nil, errutil.UserError{Err: "invalid key version"}
}
@ -335,7 +331,7 @@ func (p *Policy) DeriveKey(context []byte, ver int) ([]byte, error) {
}
}
func (p *Policy) Encrypt(context []byte, value string) (string, error) {
func (p *Policy) Encrypt(context, nonce []byte, value string) (string, error) {
// Decode the plaintext value
plaintext, err := base64.StdEncoding.DecodeString(value)
if err != nil {
@ -367,15 +363,12 @@ func (p *Policy) Encrypt(context []byte, value string) (string, error) {
return "", errutil.InternalError{Err: err.Error()}
}
if p.ConvergentEncryption && len(context) != gcm.NonceSize() {
return "", errutil.UserError{Err: fmt.Sprintf("base64-decoded context must be %d bytes long when using convergent encryption with this key", gcm.NonceSize())}
}
// Compute random nonce
var nonce []byte
if p.ConvergentEncryption {
nonce = context
if len(nonce) != gcm.NonceSize() {
return "", errutil.UserError{Err: fmt.Sprintf("base64-decoded nonce must be %d bytes long when using convergent encryption with this key", gcm.NonceSize())}
}
} else {
// Compute random nonce
nonce = make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
if err != nil {
@ -387,7 +380,10 @@ func (p *Policy) Encrypt(context []byte, value string) (string, error) {
out := gcm.Seal(nil, nonce, plaintext, nil)
// Place the encrypted data after the nonce
full := append(nonce, out...)
full := out
if !p.ConvergentEncryption {
full = append(nonce, out...)
}
// Convert to base64
encoded := base64.StdEncoding.EncodeToString(full)
@ -398,12 +394,16 @@ func (p *Policy) Encrypt(context []byte, value string) (string, error) {
return encoded, nil
}
func (p *Policy) Decrypt(context []byte, value string) (string, error) {
func (p *Policy) Decrypt(context, nonce []byte, value string) (string, error) {
// Verify the prefix
if !strings.HasPrefix(value, "vault:v") {
return "", errutil.UserError{Err: "invalid ciphertext: no prefix"}
}
if p.ConvergentEncryption && (nonce == nil || len(nonce) == 0) {
return "", errutil.UserError{Err: "invalid convergent nonce supplied"}
}
splitVerCiphertext := strings.SplitN(strings.TrimPrefix(value, "vault:v"), ":", 2)
if len(splitVerCiphertext) != 2 {
return "", errutil.UserError{Err: "invalid ciphertext: wrong number of fields"}
@ -460,8 +460,13 @@ func (p *Policy) Decrypt(context []byte, value string) (string, error) {
}
// Extract the nonce and ciphertext
nonce := decoded[:gcm.NonceSize()]
ciphertext := decoded[gcm.NonceSize():]
var ciphertext []byte
if p.ConvergentEncryption {
ciphertext = decoded
} else {
nonce = decoded[:gcm.NonceSize()]
ciphertext = decoded[gcm.NonceSize():]
}
// Verify and Decrypt
plain, err := gcm.Open(nil, nonce, ciphertext, nil)

View File

@ -33,7 +33,7 @@ data's attack surface.
Key derivation is supported, which allows the same key to be used for multiple
purposes by deriving a new key based on a user-supplied context value. In this
mode, convergent encryption can optionally be supported, which allows the same
context and plaintext to produce the same ciphertext.
input values to produce the same ciphertext.
The backend also supports key rotation, which allows a new version of the named
key to be generated. All data encrypted with the key will use the newest
@ -156,12 +156,16 @@ only encrypt or decrypt using the named keys they need access to.
<span class="param-flags">optional</span>
If set, the key will support convergent encryption, where the same
plaintext creates the same ciphertext. This requires _derived_ to be
set to `true`. When enabled, the context value must be exactly 12 bytes
(96 bits) and will both be used to derive the key and as the nonce for
the encryption operation. Note that while this is useful for particular
situations, it also has security implications. In particular, you must
ensure that you do **not** use the same context value for more than one
plaintext value. Defaults to false.
set to `true`. When enabled, each
encryption(/decryption/rewrap/datakey) operation will require a `nonce`
value to be specified. Note that while this is useful for particular
situations, all nonce values used with a given context value **must be
unique** or it will compromise the security of your key. A common way
to use this will be to generate a unique identifier for the given data
(for instance, a SHA-512 sum), then separate the bytes so that twelve
bytes are used as the nonce and the remaining as the context, ensuring
that all bits of unique identity are used as a part of the encryption
operation. Defaults to false.
</li>
</ul>
</dd>
@ -347,6 +351,15 @@ only encrypt or decrypt using the named keys they need access to.
The key derivation context, provided as base64 encoded.
Must be provided if derivation is enabled.
</li>
<li>
<span class="param">nonce</span>
<span class="param-flags">optional</span>
The nonce value, provided as base64 encoded. Must be provided if
convergent encryption is enabled for this key. The value must be
exactly 96 bits (12 bytes) long and the user must ensure that for any
given context (and thus, any given encryption key) this nonce value is
**never reused**.
</li>
</ul>
</dd>
@ -393,6 +406,12 @@ only encrypt or decrypt using the named keys they need access to.
The key derivation context, provided as base64 encoded.
Must be provided if derivation is enabled.
</li>
<li>
<span class="param">nonce</span>
<span class="param-flags">optional</span>
The nonce value used during encryption, provided as base64 encoded.
Must be provided if convergent encryption is enabled for this key.
</li>
</ul>
</dd>
@ -441,6 +460,12 @@ only encrypt or decrypt using the named keys they need access to.
The key derivation context, provided as base64 encoded.
Must be provided if derivation is enabled.
</li>
<li>
<span class="param">nonce</span>
<span class="param-flags">optional</span>
The nonce value used during encryption, provided as base64 encoded.
Must be provided if convergent encryption is enabled for this key.
</li>
</ul>
</dd>
@ -493,6 +518,15 @@ only encrypt or decrypt using the named keys they need access to.
The key derivation context, provided as base64 encoded.
Must be provided if derivation is enabled.
</li>
<li>
<span class="param">nonce</span>
<span class="param-flags">optional</span>
The nonce value, provided as base64 encoded. Must be provided if
convergent encryption is enabled for this key. The value must be
exactly 96 bits (12 bytes) long and the user must ensure that for any
given context (and thus, any given encryption key) this nonce value is
**never reused**.
</li>
<li>
<span class="param">bits</span>
<span class="param-flags">optional</span>