Remove context-as-nonce, add docs, and properly support datakey

This commit is contained in:
Jeff Mitchell 2016-08-07 15:53:40 -04:00
parent b5858e2237
commit 84cd3c20b3
11 changed files with 100 additions and 207 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

@ -230,7 +230,6 @@ func testAccStepReadPolicy(t *testing.T, name string, expectNone, derived bool)
KDFMode string `mapstructure:"kdf_mode"`
DeletionAllowed bool `mapstructure:"deletion_allowed"`
ConvergentEncryption bool `mapstructure:"convergent_encryption"`
ContextAsNonce bool `mapstructure:"context_as_nonce"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
@ -608,6 +607,16 @@ 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"
@ -702,126 +711,6 @@ func TestConvergentEncryption(t *testing.T) {
}
}
func TestConvergentEncryptionContextAsNonce(t *testing.T) {
var b *backend
sysView := logical.TestSystemView()
storage := &logical.InmemStorage{}
b = Backend(&logical.BackendConfig{
StorageView: storage,
System: sysView,
})
req := &logical.Request{
Storage: storage,
Operation: logical.UpdateOperation,
Path: "keys/testkeynonderived",
Data: map[string]interface{}{
"derived": false,
"convergent_encryption": true,
"context_as_nonce": true,
},
}
resp, err := b.HandleRequest(req)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected non-nil response")
}
if !resp.IsError() {
t.Fatalf("bad: expected error response, got %#v", *resp)
}
req.Path = "keys/testkey"
req.Data = map[string]interface{}{
"derived": true,
"convergent_encryption": true,
"context_as_nonce": true,
}
resp, err = b.HandleRequest(req)
if err != nil {
t.Fatal(err)
}
if resp != nil {
t.Fatalf("bad: got resp %#v", *resp)
}
// First, test using an invalid length of nonce
req.Path = "encrypt/testkey"
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "Zm9vIGJhcg==", // "foo bar"
}
resp, err = b.HandleRequest(req)
if resp == nil {
t.Fatal("expected non-nil response")
}
if !resp.IsError() {
t.Fatalf("expected error response, got %#v", *resp)
}
// Now test encrypting the same value twice
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "b25ldHdvdGhyZWVl", // "onetwothreee"
}
resp, err = b.HandleRequest(req)
if resp == nil {
t.Fatal("expected non-nil response")
}
if resp.IsError() {
t.Fatalf("got error response: %#v", *resp)
}
ciphertext1 := 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)
}
ciphertext2 := resp.Data["ciphertext"].(string)
if ciphertext1 != ciphertext2 {
t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext1, ciphertext2)
}
// For sanity, also check a different value
req.Data = map[string]interface{}{
"plaintext": "emlwIHphcA==", // "zip zap"
"context": "dHdvdGhyZWVmb3Vy", // "twothreefour"
}
resp, err = b.HandleRequest(req)
if resp == nil {
t.Fatal("expected non-nil response")
}
if resp.IsError() {
t.Fatalf("got error response: %#v", *resp)
}
ciphertext3 := 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)
}
ciphertext4 := resp.Data["ciphertext"].(string)
if ciphertext3 != ciphertext4 {
t.Fatalf("expected the same ciphertext but got %s and %s", ciphertext3, ciphertext4)
}
if ciphertext1 == ciphertext3 {
t.Fatalf("expected different ciphertexts")
}
}
func TestPolicyFuzzing(t *testing.T) {
var be *backend
sysView := logical.TestSystemView()

View File

@ -105,42 +105,42 @@ func (lm *lockManager) UnlockPolicy(lock *sync.RWMutex, lockType bool) {
// is needed (for instance, for an upgrade/migration), give up the read lock,
// call again with an exclusive lock, then swap back out for a read lock.
func (lm *lockManager) GetPolicyShared(storage logical.Storage, name string) (*Policy, *sync.RWMutex, error) {
p, lock, _, err := lm.getPolicyCommon(storage, name, false, false, false, false, shared)
p, lock, _, err := lm.getPolicyCommon(storage, name, false, false, false, shared)
if err == nil ||
(err != nil && err != errNeedExclusiveLock) {
return p, lock, err
}
// Try again while asking for an exlusive lock
p, lock, _, err = lm.getPolicyCommon(storage, name, false, false, false, false, exclusive)
p, lock, _, err = lm.getPolicyCommon(storage, name, false, false, false, exclusive)
if err != nil || p == nil || lock == nil {
return p, lock, err
}
lock.Unlock()
p, lock, _, err = lm.getPolicyCommon(storage, name, false, false, false, false, shared)
p, lock, _, err = lm.getPolicyCommon(storage, name, false, false, false, shared)
return p, lock, err
}
// Get the policy with an exclusive lock
func (lm *lockManager) GetPolicyExclusive(storage logical.Storage, name string) (*Policy, *sync.RWMutex, error) {
p, lock, _, err := lm.getPolicyCommon(storage, name, false, false, false, false, exclusive)
p, lock, _, err := lm.getPolicyCommon(storage, name, false, false, false, exclusive)
return p, lock, err
}
// 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, convergent, contextAsNonce bool) (*Policy, *sync.RWMutex, bool, error) {
p, lock, _, err := lm.getPolicyCommon(storage, name, true, derived, convergent, contextAsNonce, shared)
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) {
return p, lock, false, err
}
// Try again while asking for an exlusive lock
p, lock, upserted, err := lm.getPolicyCommon(storage, name, true, derived, convergent, contextAsNonce, exclusive)
p, lock, upserted, err := lm.getPolicyCommon(storage, name, true, derived, convergent, exclusive)
if err != nil || p == nil || lock == nil {
return p, lock, upserted, err
}
@ -148,14 +148,14 @@ func (lm *lockManager) GetPolicyUpsert(storage logical.Storage, name string, der
lock.Unlock()
// Now get a shared lock for the return, but preserve the value of upsert
p, lock, _, err = lm.getPolicyCommon(storage, name, true, derived, convergent, contextAsNonce, shared)
p, lock, _, err = lm.getPolicyCommon(storage, name, true, derived, convergent, shared)
return p, lock, upserted, err
}
// When the function returns, a lock will be held on the policy if err == nil.
// It is the caller's responsibility to unlock.
func (lm *lockManager) getPolicyCommon(storage logical.Storage, name string, upsert, derived, convergent, contextAsNonce, lockType bool) (*Policy, *sync.RWMutex, bool, error) {
func (lm *lockManager) getPolicyCommon(storage logical.Storage, name string, upsert, derived, convergent, lockType bool) (*Policy, *sync.RWMutex, bool, error) {
lock := lm.policyLock(name, lockType)
var p *Policy
@ -204,8 +204,6 @@ func (lm *lockManager) getPolicyCommon(storage logical.Storage, name string, ups
if derived {
p.KDFMode = kdfMode
p.ConvergentEncryption = convergent
p.ContextAsNonce = new(bool)
*p.ContextAsNonce = contextAsNonce
}
err = p.rotate(storage)

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, nil, 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

@ -30,7 +30,7 @@ func (b *backend) pathDecrypt() *framework.Path {
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Nonce for when convergent encryption is used and the context is not used as the nonce",
Description: "Nonce for when convergent encryption is used",
},
},

View File

@ -31,7 +31,7 @@ func (b *backend) pathEncrypt() *framework.Path {
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Nonce for when convergent encryption is used and the context is not used as the nonce",
Description: "Nonce for when convergent encryption is used",
},
},
@ -95,7 +95,7 @@ func (b *backend) pathEncryptWrite(
var lock *sync.RWMutex
var upserted bool
if req.Operation == logical.CreateOperation {
p, lock, upserted, err = b.lm.GetPolicyUpsert(req.Storage, name, len(context) != 0, false, false)
p, lock, upserted, err = b.lm.GetPolicyUpsert(req.Storage, name, len(context) != 0, false)
} else {
p, lock, err = b.lm.GetPolicyShared(req.Storage, name)
}

View File

@ -29,31 +29,14 @@ allows for per-transaction unique keys.`,
This is only supported when using a key with
key derivation enabled and will require all
requests to carry both a context and 96-bit
(12-byte) nonce, unless the "context_as_nonce"
feature is also enabled. The given nonce will
be used in place of a randomly generated nonce.
As a result, when the same context and nonce
(or context, if "context_as_nonce" is enabled)
are supplied, the same ciphertext is emitted
from the encryption function. It is *very
important* when using this mode that you ensure
that all nonces are unique for a given context,
or, when using "context_as_nonce", that all
contexts are unique for a given key. Failing to
do so will severely impact the ciphertext's
security.`,
},
"context_as_nonce": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `Whether to use the context value as the
nonce in the convergent encryption operation
mode. If set true, the user will have to
supply a 96-bit (12-byte) context value.
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.`,
(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.`,
},
},
@ -73,13 +56,12 @@ func (b *backend) pathPolicyWrite(
name := d.Get("name").(string)
derived := d.Get("derived").(bool)
convergent := d.Get("convergent_encryption").(bool)
contextAsNonce := d.Get("context_as_nonce").(bool)
if !derived && convergent {
return logical.ErrorResponse("convergent encryption requires derivation to be enabled"), nil
}
p, lock, upserted, err := b.lm.GetPolicyUpsert(req.Storage, name, derived, convergent, contextAsNonce)
p, lock, upserted, err := b.lm.GetPolicyUpsert(req.Storage, name, derived, convergent)
if lock != nil {
defer lock.RUnlock()
}
@ -127,9 +109,6 @@ func (b *backend) pathPolicyRead(
if p.Derived {
resp.Data["kdf_mode"] = p.KDFMode
resp.Data["convergent_encryption"] = p.ConvergentEncryption
if p.ContextAsNonce != nil {
resp.Data["context_as_nonce"] = *p.ContextAsNonce
}
}
retKeys := map[string]int64{}

View File

@ -30,7 +30,7 @@ func (b *backend) pathRewrap() *framework.Path {
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Nonce for when convergent encryption is used and the context is not used as the nonce",
Description: "Nonce for when convergent encryption is used",
},
},

View File

@ -72,7 +72,6 @@ type Policy struct {
Derived bool `json:"derived"`
KDFMode string `json:"kdf_mode"`
ConvergentEncryption bool `json:"convergent_encryption"`
ContextAsNonce *bool `json:"context_as_nonce"`
// The minimum version of the key allowed to be used
// for decryption
@ -260,10 +259,6 @@ func (p *Policy) needsUpgrade() bool {
return true
}
if p.ConvergentEncryption && p.ContextAsNonce == nil {
return true
}
return false
}
@ -293,14 +288,6 @@ func (p *Policy) upgrade(storage logical.Storage) error {
persistNeeded = true
}
// Originally the context-as-nonce mode was the only mode, so keep that
// behavior if convergent encryption is already in use
if p.ConvergentEncryption && p.ContextAsNonce == nil {
p.ContextAsNonce = new(bool)
*p.ContextAsNonce = true
persistNeeded = true
}
if persistNeeded {
err := p.Persist(storage)
if err != nil {
@ -377,17 +364,9 @@ func (p *Policy) Encrypt(context, nonce []byte, value string) (string, error) {
}
if p.ConvergentEncryption {
if *p.ContextAsNonce {
if len(context) != gcm.NonceSize() {
return "", errutil.UserError{Err: fmt.Sprintf("base64-decoded context must be %d bytes long when using convergent encryption with context-as-nonce with this key", gcm.NonceSize())}
}
nonce = context
} else if len(nonce) != gcm.NonceSize() {
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())
@ -421,7 +400,7 @@ func (p *Policy) Decrypt(context, nonce []byte, value string) (string, error) {
return "", errutil.UserError{Err: "invalid ciphertext: no prefix"}
}
if p.ConvergentEncryption && !*p.ContextAsNonce && (nonce == nil || len(nonce) == 0) {
if p.ConvergentEncryption && (nonce == nil || len(nonce) == 0) {
return "", errutil.UserError{Err: "invalid convergent nonce supplied"}
}
@ -483,9 +462,6 @@ func (p *Policy) Decrypt(context, nonce []byte, value string) (string, error) {
// Extract the nonce and ciphertext
var ciphertext []byte
if p.ConvergentEncryption {
if *p.ContextAsNonce {
nonce = context
}
ciphertext = decoded
} else {
nonce = decoded[:gcm.NonceSize()]

View File

@ -22,7 +22,7 @@ func Test_KeyUpgrade(t *testing.T) {
func testKeyUpgradeCommon(t *testing.T, lm *lockManager) {
storage := &logical.InmemStorage{}
p, lock, upserted, err := lm.GetPolicyUpsert(storage, "test", false, false, false)
p, lock, upserted, err := lm.GetPolicyUpsert(storage, "test", false, false)
if lock != nil {
defer lock.RUnlock()
}
@ -68,7 +68,7 @@ func testArchivingUpgradeCommon(t *testing.T, lm *lockManager) {
storage := &logical.InmemStorage{}
p, lock, _, err := lm.GetPolicyUpsert(storage, "test", false, false, false)
p, lock, _, err := lm.GetPolicyUpsert(storage, "test", false, false)
if err != nil {
t.Fatal(err)
}
@ -198,7 +198,7 @@ func testArchivingCommon(t *testing.T, lm *lockManager) {
storage := &logical.InmemStorage{}
p, lock, _, err := lm.GetPolicyUpsert(storage, "test", false, false, false)
p, lock, _, err := lm.GetPolicyUpsert(storage, "test", false, false)
if lock != nil {
defer lock.RUnlock()
}

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>