Add support for a dedicated HMAC type in Transit. (#16668)

* Get import correct

* limits, docs

* changelog

* unit tests

* And fix import for hmac unit test

* typo

* Update website/content/api-docs/secret/transit.mdx

Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com>

* Update builtin/logical/transit/path_keys.go

Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com>

* Validate key sizes a bit more carefully

* Update sdk/helper/keysutil/policy.go

Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com>

Co-authored-by: Matt Schultz <975680+schultz-is@users.noreply.github.com>
This commit is contained in:
Scott Miller
2022-09-06 10:17:58 -05:00
committed by GitHub
parent e0be62caf1
commit d6a1ce2e7b
9 changed files with 268 additions and 176 deletions

View File

@@ -14,11 +14,25 @@ import (
func TestTransit_HMAC(t *testing.T) { func TestTransit_HMAC(t *testing.T) {
b, storage := createBackendWithSysView(t) b, storage := createBackendWithSysView(t)
// First create a key cases := []struct {
name string
typ string
}{
{
name: "foo",
typ: "",
},
{
name: "dedicated",
typ: "hmac",
},
}
for _, c := range cases {
req := &logical.Request{ req := &logical.Request{
Storage: storage, Storage: storage,
Operation: logical.UpdateOperation, Operation: logical.UpdateOperation,
Path: "keys/foo", Path: "keys/" + c.name,
} }
_, err := b.HandleRequest(context.Background(), req) _, err := b.HandleRequest(context.Background(), req)
if err != nil { if err != nil {
@@ -28,7 +42,7 @@ func TestTransit_HMAC(t *testing.T) {
// Now, change the key value to something we control // Now, change the key value to something we control
p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{
Storage: storage, Storage: storage,
Name: "foo", Name: c.name,
}, b.GetRandomReader()) }, b.GetRandomReader())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -37,12 +51,13 @@ func TestTransit_HMAC(t *testing.T) {
latestVersion := strconv.Itoa(p.LatestVersion) latestVersion := strconv.Itoa(p.LatestVersion)
keyEntry := p.Keys[latestVersion] keyEntry := p.Keys[latestVersion]
keyEntry.HMACKey = []byte("01234567890123456789012345678901") keyEntry.HMACKey = []byte("01234567890123456789012345678901")
keyEntry.Key = []byte("01234567890123456789012345678901")
p.Keys[latestVersion] = keyEntry p.Keys[latestVersion] = keyEntry
if err = p.Persist(context.Background(), storage); err != nil { if err = p.Persist(context.Background(), storage); err != nil {
t.Fatal(err) t.Fatal(err)
} }
req.Path = "hmac/foo" req.Path = "hmac/" + c.name
req.Data = map[string]interface{}{ req.Data = map[string]interface{}{
"input": "dGhlIHF1aWNrIGJyb3duIGZveA==", "input": "dGhlIHF1aWNrIGJyb3duIGZveA==",
} }
@@ -96,11 +111,11 @@ func TestTransit_HMAC(t *testing.T) {
doRequest(req, false, "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=") doRequest(req, false, "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=")
// Test algorithm selection in the path // Test algorithm selection in the path
req.Path = "hmac/foo/sha2-224" req.Path = "hmac/" + c.name + "/sha2-224"
doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==")
// Reset and test algorithm selection in the data // Reset and test algorithm selection in the data
req.Path = "hmac/foo" req.Path = "hmac/" + c.name
req.Data["algorithm"] = "sha2-224" req.Data["algorithm"] = "sha2-224"
doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==")
@@ -115,7 +130,7 @@ func TestTransit_HMAC(t *testing.T) {
doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==") doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==")
// Test SHA3 // Test SHA3
req.Path = "hmac/foo" req.Path = "hmac/" + c.name
req.Data["algorithm"] = "sha3-224" req.Data["algorithm"] = "sha3-224"
doRequest(req, false, "vault:v1:TGipmKH8LR/BkMolYpDYy0BJCIhTtGPDhV2VkQ==") doRequest(req, false, "vault:v1:TGipmKH8LR/BkMolYpDYy0BJCIhTtGPDhV2VkQ==")
@@ -156,7 +171,7 @@ func TestTransit_HMAC(t *testing.T) {
doRequest(req, false, "vault:v2:Dt+mO/B93kuWUbGMMobwUNX5Wodr6dL3JH4DMfpQ0kw=") doRequest(req, false, "vault:v2:Dt+mO/B93kuWUbGMMobwUNX5Wodr6dL3JH4DMfpQ0kw=")
// Verify a previous version // Verify a previous version
req.Path = "verify/foo" req.Path = "verify/" + c.name
req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4="
resp, err := b.HandleRequest(context.Background(), req) resp, err := b.HandleRequest(context.Background(), req)
@@ -197,6 +212,7 @@ func TestTransit_HMAC(t *testing.T) {
if err != logical.ErrInvalidRequest { if err != logical.ErrInvalidRequest {
t.Fatalf("expected invalid request error, got %v", err) t.Fatalf("expected invalid request error, got %v", err)
} }
}
} }
func TestTransit_batchHMAC(t *testing.T) { func TestTransit_batchHMAC(t *testing.T) {

View File

@@ -177,6 +177,8 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d *
polReq.KeyType = keysutil.KeyType_RSA3072 polReq.KeyType = keysutil.KeyType_RSA3072
case "rsa-4096": case "rsa-4096":
polReq.KeyType = keysutil.KeyType_RSA4096 polReq.KeyType = keysutil.KeyType_RSA4096
case "hmac":
polReq.KeyType = keysutil.KeyType_HMAC
default: default:
return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest
} }

View File

@@ -30,6 +30,7 @@ var keyTypes = []string{
"rsa-2048", "rsa-2048",
"rsa-3072", "rsa-3072",
"rsa-4096", "rsa-4096",
"hmac",
} }
var hashFns = []string{ var hashFns = []string{
@@ -543,7 +544,7 @@ func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey
var ok bool var ok bool
var err error var err error
switch targetKeyType { switch targetKeyType {
case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305": case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305", "hmac":
preppedTargetKey, ok = targetKey.([]byte) preppedTargetKey, ok = targetKey.([]byte)
if !ok { if !ok {
t.Fatal("failed to wrap target key for import: symmetric key not provided in byte format") t.Fatal("failed to wrap target key for import: symmetric key not provided in byte format")
@@ -600,7 +601,7 @@ func generateKey(keyType string) (interface{}, error) {
switch keyType { switch keyType {
case "aes128-gcm96": case "aes128-gcm96":
return uuid.GenerateRandomBytes(16) return uuid.GenerateRandomBytes(16)
case "aes256-gcm96": case "aes256-gcm96", "hmac":
return uuid.GenerateRandomBytes(32) return uuid.GenerateRandomBytes(32)
case "chacha20-poly1305": case "chacha20-poly1305":
return uuid.GenerateRandomBytes(32) return uuid.GenerateRandomBytes(32)

View File

@@ -103,6 +103,11 @@ being automatically rotated. A value of 0
(default) disables automatic rotation for the (default) disables automatic rotation for the
key.`, key.`,
}, },
"key_size": {
Type: framework.TypeInt,
Default: 0,
Description: fmt.Sprintf("The key size in bytes for the algorithm. Only applies to HMAC and must be no fewer than %d bytes and no more than %d", keysutil.HmacMinKeySize, keysutil.HmacMaxKeySize),
},
}, },
Callbacks: map[logical.Operation]framework.OperationFunc{ Callbacks: map[logical.Operation]framework.OperationFunc{
@@ -130,6 +135,7 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d *
derived := d.Get("derived").(bool) derived := d.Get("derived").(bool)
convergent := d.Get("convergent_encryption").(bool) convergent := d.Get("convergent_encryption").(bool)
keyType := d.Get("type").(string) keyType := d.Get("type").(string)
keySize := d.Get("key_size").(int)
exportable := d.Get("exportable").(bool) exportable := d.Get("exportable").(bool)
allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool) allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool)
autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int)) autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int))
@@ -152,6 +158,7 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d *
AllowPlaintextBackup: allowPlaintextBackup, AllowPlaintextBackup: allowPlaintextBackup,
AutoRotatePeriod: autoRotatePeriod, AutoRotatePeriod: autoRotatePeriod,
} }
switch keyType { switch keyType {
case "aes128-gcm96": case "aes128-gcm96":
polReq.KeyType = keysutil.KeyType_AES128_GCM96 polReq.KeyType = keysutil.KeyType_AES128_GCM96
@@ -173,9 +180,20 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d *
polReq.KeyType = keysutil.KeyType_RSA3072 polReq.KeyType = keysutil.KeyType_RSA3072
case "rsa-4096": case "rsa-4096":
polReq.KeyType = keysutil.KeyType_RSA4096 polReq.KeyType = keysutil.KeyType_RSA4096
case "hmac":
polReq.KeyType = keysutil.KeyType_HMAC
default: default:
return logical.ErrorResponse(fmt.Sprintf("unknown key type %v", keyType)), logical.ErrInvalidRequest return logical.ErrorResponse(fmt.Sprintf("unknown key type %v", keyType)), logical.ErrInvalidRequest
} }
if keySize != 0 {
if polReq.KeyType != keysutil.KeyType_HMAC {
return logical.ErrorResponse(fmt.Sprintf("key_size is not valid for algorithm %v", polReq.KeyType)), logical.ErrInvalidRequest
}
if keySize < keysutil.HmacMinKeySize || keySize > keysutil.HmacMaxKeySize {
return logical.ErrorResponse(fmt.Sprintf("invalid key_size %d", keySize)), logical.ErrInvalidRequest
}
polReq.KeySize = keySize
}
p, upserted, err := b.GetPolicy(ctx, polReq, b.GetRandomReader()) p, upserted, err := b.GetPolicy(ctx, polReq, b.GetRandomReader())
if err != nil { if err != nil {
@@ -242,6 +260,9 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f
"imported_key": p.Imported, "imported_key": p.Imported,
}, },
} }
if p.KeySize != 0 {
resp.Data["key_size"] = p.KeySize
}
if p.Imported { if p.Imported {
resp.Data["imported_key_allow_rotation"] = p.AllowImportedKeyRotation resp.Data["imported_key_allow_rotation"] = p.AllowImportedKeyRotation

3
changelog/16668.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
secrets/transit: Add a dedicated HMAC key type, which can be used with key import.
```

View File

@@ -36,6 +36,9 @@ type PolicyRequest struct {
// The key type // The key type
KeyType KeyType KeyType KeyType
// The key size for variable key size algorithms
KeySize int
// Whether it should be derived // Whether it should be derived
Derived bool Derived bool
@@ -373,6 +376,11 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io
cleanup() cleanup()
return nil, false, fmt.Errorf("key derivation and convergent encryption not supported for keys of type %v", req.KeyType) return nil, false, fmt.Errorf("key derivation and convergent encryption not supported for keys of type %v", req.KeyType)
} }
case KeyType_HMAC:
if req.Derived || req.Convergent {
cleanup()
return nil, false, fmt.Errorf("key derivation and convergent encryption not supported for keys of type %v", req.KeyType)
}
default: default:
cleanup() cleanup()
@@ -387,6 +395,7 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io
Exportable: req.Exportable, Exportable: req.Exportable,
AllowPlaintextBackup: req.AllowPlaintextBackup, AllowPlaintextBackup: req.AllowPlaintextBackup,
AutoRotatePeriod: req.AutoRotatePeriod, AutoRotatePeriod: req.AutoRotatePeriod,
KeySize: req.KeySize,
} }
if req.Derived { if req.Derived {

View File

@@ -45,6 +45,9 @@ import (
const ( const (
Kdf_hmac_sha256_counter = iota // built-in helper Kdf_hmac_sha256_counter = iota // built-in helper
Kdf_hkdf_sha256 // golang.org/x/crypto/hkdf Kdf_hkdf_sha256 // golang.org/x/crypto/hkdf
HmacMinKeySize = 256 / 8
HmacMaxKeySize = 4096 / 8
) )
// Or this one...we need the default of zero to be the original AES256-GCM96 // Or this one...we need the default of zero to be the original AES256-GCM96
@@ -59,6 +62,7 @@ const (
KeyType_ECDSA_P521 KeyType_ECDSA_P521
KeyType_AES128_GCM96 KeyType_AES128_GCM96
KeyType_RSA3072 KeyType_RSA3072
KeyType_HMAC
) )
const ( const (
@@ -160,6 +164,8 @@ func (kt KeyType) String() string {
return "rsa-3072" return "rsa-3072"
case KeyType_RSA4096: case KeyType_RSA4096:
return "rsa-4096" return "rsa-4096"
case KeyType_HMAC:
return "hmac"
} }
return "[unknown]" return "[unknown]"
@@ -320,6 +326,7 @@ type Policy struct {
Name string `json:"name"` Name string `json:"name"`
Key []byte `json:"key,omitempty"` // DEPRECATED Key []byte `json:"key,omitempty"` // DEPRECATED
KeySize int `json:"key_size,omitempty"` // For algorithms with variable key sizes
Keys keyEntryMap `json:"keys"` Keys keyEntryMap `json:"keys"`
// Derived keys MUST provide a context and the master underlying key is // Derived keys MUST provide a context and the master underlying key is
@@ -1025,10 +1032,13 @@ func (p *Policy) HMACKey(version int) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if p.Type == KeyType_HMAC {
return keyEntry.Key, nil
}
if keyEntry.HMACKey == nil { if keyEntry.HMACKey == nil {
return nil, fmt.Errorf("no HMAC key exists for that key version") return nil, fmt.Errorf("no HMAC key exists for that key version")
} }
return keyEntry.HMACKey, nil return keyEntry.HMACKey, nil
} }
@@ -1386,19 +1396,25 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte
DeprecatedCreationTime: now.Unix(), DeprecatedCreationTime: now.Unix(),
} }
if p.Type != KeyType_HMAC {
hmacKey, err := uuid.GenerateRandomBytesWithReader(32, randReader) hmacKey, err := uuid.GenerateRandomBytesWithReader(32, randReader)
if err != nil { if err != nil {
return err return err
} }
entry.HMACKey = hmacKey entry.HMACKey = hmacKey
}
if (p.Type == KeyType_AES128_GCM96 && len(key) != 16) || if (p.Type == KeyType_AES128_GCM96 && len(key) != 16) ||
((p.Type == KeyType_AES256_GCM96 || p.Type == KeyType_ChaCha20_Poly1305) && len(key) != 32) { ((p.Type == KeyType_AES256_GCM96 || p.Type == KeyType_ChaCha20_Poly1305) && len(key) != 32) ||
(p.Type == KeyType_HMAC && (len(key) < HmacMinKeySize || len(key) > HmacMaxKeySize)) {
return fmt.Errorf("invalid key size %d bytes for key type %s", len(key), p.Type) return fmt.Errorf("invalid key size %d bytes for key type %s", len(key), p.Type)
} }
if p.Type == KeyType_AES128_GCM96 || p.Type == KeyType_AES256_GCM96 || p.Type == KeyType_ChaCha20_Poly1305 { if p.Type == KeyType_AES128_GCM96 || p.Type == KeyType_AES256_GCM96 || p.Type == KeyType_ChaCha20_Poly1305 || p.Type == KeyType_HMAC {
entry.Key = key entry.Key = key
if p.Type == KeyType_HMAC {
p.KeySize = len(key)
}
} else { } else {
parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key) parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key)
if err != nil { if err != nil {
@@ -1549,11 +1565,16 @@ func (p *Policy) RotateInMemory(randReader io.Reader) (retErr error) {
entry.HMACKey = hmacKey entry.HMACKey = hmacKey
switch p.Type { switch p.Type {
case KeyType_AES128_GCM96, KeyType_AES256_GCM96, KeyType_ChaCha20_Poly1305: case KeyType_AES128_GCM96, KeyType_AES256_GCM96, KeyType_ChaCha20_Poly1305, KeyType_HMAC:
// Default to 256 bit key // Default to 256 bit key
numBytes := 32 numBytes := 32
if p.Type == KeyType_AES128_GCM96 { if p.Type == KeyType_AES128_GCM96 {
numBytes = 16 numBytes = 16
} else if p.Type == KeyType_HMAC {
numBytes := p.KeySize
if numBytes < HmacMinKeySize || numBytes > HmacMaxKeySize {
return fmt.Errorf("invalid key size for HMAC key, must be between %d and %d bytes", HmacMinKeySize, HmacMaxKeySize)
}
} }
newKey, err := uuid.GenerateRandomBytesWithReader(numBytes, randReader) newKey, err := uuid.GenerateRandomBytesWithReader(numBytes, randReader)
if err != nil { if err != nil {

View File

@@ -63,10 +63,19 @@ values set here cannot be changed after key creation.
- `rsa-2048` - RSA with bit size of 2048 (asymmetric) - `rsa-2048` - RSA with bit size of 2048 (asymmetric)
- `rsa-3072` - RSA with bit size of 3072 (asymmetric) - `rsa-3072` - RSA with bit size of 3072 (asymmetric)
- `rsa-4096` - RSA with bit size of 4096 (asymmetric) - `rsa-4096` - RSA with bit size of 4096 (asymmetric)
- `hmac` - HMAC (HMAC generation, verification)
~> **Note**: In FIPS 140-2 mode, the following algorithms are not certified ~> **Note**: In FIPS 140-2 mode, the following algorithms are not certified
and thus should not be used: `chacha20-poly1305` and `ed25519`. and thus should not be used: `chacha20-poly1305` and `ed25519`.
~> **Note**: All key types support HMAC through the use of a second randomly
generated key created key creation time or rotation. The HMAC key type only
supports HMAC, and behaves identically to other algorithms with
respect to the HMAC operations but supports key import. By default,
the HMAC key type uses a 256-bit key.
- `key_size` `(int: "0", optional)` - The key size in bytes for algorithms
that allow variable key sizes. Currently only applicable to HMAC, where
it must be between 16 and 512 bytes.
- `auto_rotate_period` `(duration: "0", optional)` The period at which - `auto_rotate_period` `(duration: "0", optional)` The period at which
this key should be rotated automatically. Setting this to "0" (the default) this key should be rotated automatically. Setting this to "0" (the default)
will disable automatic key rotation. This value cannot be shorter than one will disable automatic key rotation. This value cannot be shorter than one
@@ -431,6 +440,9 @@ ciphertext to be encrypted with the latest version of the key, use the `rewrap`
endpoint. This is only supported with keys that support encryption and endpoint. This is only supported with keys that support encryption and
decryption operations. decryption operations.
For algorithms with a configurable key size, the rotated key will use the same
key size as the previous version.
~> **Note**: For imported keys, rotation is only supported if the ~> **Note**: For imported keys, rotation is only supported if the
`allow_rotation` field was set to `true` on import. Once an imported key is `allow_rotation` field was set to `true` on import. Once an imported key is
rotated within Vault, it will not support further import operations. rotated within Vault, it will not support further import operations.

View File

@@ -85,10 +85,17 @@ types also generate separate HMAC keys):
signature verification signature verification
- `rsa-4096`: 4096-bit RSA key; supports encryption, decryption, signing, and - `rsa-4096`: 4096-bit RSA key; supports encryption, decryption, signing, and
signature verification signature verification
- `hmac`: HMAC; supporting HMAC generation and verification.
~> **Note**: In FIPS 140-2 mode, the following algorithms are not certified ~> **Note**: In FIPS 140-2 mode, the following algorithms are not certified
and thus should not be used: `chacha20-poly1305` and `ed25519`. and thus should not be used: `chacha20-poly1305` and `ed25519`.
~> **Note**: All key types support HMAC operations through the use of a second randomly
generated key created key creation time or rotation. The HMAC key type only
supports HMAC, and behaves identically to other algorithms with
respect to the HMAC operations but supports key import. By default,
the HMAC key type uses a 256-bit key.
## Convergent Encryption ## Convergent Encryption
Convergent encryption is a mode where the same set of plaintext+context always Convergent encryption is a mode where the same set of plaintext+context always