diff --git a/builtin/logical/transit/path_hmac_test.go b/builtin/logical/transit/path_hmac_test.go index c60b6ef46..16dbb1a49 100644 --- a/builtin/logical/transit/path_hmac_test.go +++ b/builtin/logical/transit/path_hmac_test.go @@ -14,71 +14,167 @@ import ( func TestTransit_HMAC(t *testing.T) { b, storage := createBackendWithSysView(t) - // First create a key - req := &logical.Request{ - Storage: storage, - Operation: logical.UpdateOperation, - Path: "keys/foo", - } - _, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) + cases := []struct { + name string + typ string + }{ + { + name: "foo", + typ: "", + }, + { + name: "dedicated", + typ: "hmac", + }, } - // Now, change the key value to something we control - p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ - Storage: storage, - Name: "foo", - }, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - // We don't care as we're the only one using this - latestVersion := strconv.Itoa(p.LatestVersion) - keyEntry := p.Keys[latestVersion] - keyEntry.HMACKey = []byte("01234567890123456789012345678901") - p.Keys[latestVersion] = keyEntry - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - req.Path = "hmac/foo" - req.Data = map[string]interface{}{ - "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", - } - - doRequest := func(req *logical.Request, errExpected bool, expected string) { - path := req.Path - defer func() { req.Path = path }() - - resp, err := b.HandleRequest(context.Background(), req) - if err != nil && !errExpected { - panic(fmt.Sprintf("%v", err)) + for _, c := range cases { + req := &logical.Request{ + Storage: storage, + Operation: logical.UpdateOperation, + Path: "keys/" + c.name, } - if resp == nil { - t.Fatal("expected non-nil response") + _, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) } - if errExpected { - if !resp.IsError() { + + // Now, change the key value to something we control + p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ + Storage: storage, + Name: c.name, + }, b.GetRandomReader()) + if err != nil { + t.Fatal(err) + } + // We don't care as we're the only one using this + latestVersion := strconv.Itoa(p.LatestVersion) + keyEntry := p.Keys[latestVersion] + keyEntry.HMACKey = []byte("01234567890123456789012345678901") + keyEntry.Key = []byte("01234567890123456789012345678901") + p.Keys[latestVersion] = keyEntry + if err = p.Persist(context.Background(), storage); err != nil { + t.Fatal(err) + } + + req.Path = "hmac/" + c.name + req.Data = map[string]interface{}{ + "input": "dGhlIHF1aWNrIGJyb3duIGZveA==", + } + + doRequest := func(req *logical.Request, errExpected bool, expected string) { + path := req.Path + defer func() { req.Path = path }() + + resp, err := b.HandleRequest(context.Background(), req) + if err != nil && !errExpected { + panic(fmt.Sprintf("%v", err)) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if errExpected { + if !resp.IsError() { + t.Fatalf("bad: got error response: %#v", *resp) + } + return + } + if resp.IsError() { t.Fatalf("bad: got error response: %#v", *resp) } - return - } - if resp.IsError() { - t.Fatalf("bad: got error response: %#v", *resp) - } - value, ok := resp.Data["hmac"] - if !ok { - t.Fatalf("no hmac key found in returned data, got resp data %#v", resp.Data) - } - if value.(string) != expected { - panic(fmt.Sprintf("mismatched hashes; expected %s, got resp data %#v", expected, resp.Data)) + value, ok := resp.Data["hmac"] + if !ok { + t.Fatalf("no hmac key found in returned data, got resp data %#v", resp.Data) + } + if value.(string) != expected { + panic(fmt.Sprintf("mismatched hashes; expected %s, got resp data %#v", expected, resp.Data)) + } + + // Now verify + req.Path = strings.ReplaceAll(req.Path, "hmac", "verify") + req.Data["hmac"] = value.(string) + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("%v: %v", err, resp) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if resp.Data["valid"].(bool) == false { + panic(fmt.Sprintf("error validating hmac;\nreq:\n%#v\nresp:\n%#v", *req, *resp)) + } } - // Now verify - req.Path = strings.ReplaceAll(req.Path, "hmac", "verify") - req.Data["hmac"] = value.(string) - resp, err = b.HandleRequest(context.Background(), req) + // Comparisons are against values generated via openssl + + // Test defaults -- sha2-256 + doRequest(req, false, "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=") + + // Test algorithm selection in the path + req.Path = "hmac/" + c.name + "/sha2-224" + doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") + + // Reset and test algorithm selection in the data + req.Path = "hmac/" + c.name + req.Data["algorithm"] = "sha2-224" + doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") + + req.Data["algorithm"] = "sha2-384" + doRequest(req, false, "vault:v1:jDB9YXdPjpmr29b1JCIEJO93IydlKVfD9mA2EO9OmJtJQg3QAV5tcRRRb7IQGW9p") + + req.Data["algorithm"] = "sha2-512" + doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==") + + // Test returning as base64 + req.Data["format"] = "base64" + doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==") + + // Test SHA3 + req.Path = "hmac/" + c.name + req.Data["algorithm"] = "sha3-224" + doRequest(req, false, "vault:v1:TGipmKH8LR/BkMolYpDYy0BJCIhTtGPDhV2VkQ==") + + req.Data["algorithm"] = "sha3-256" + doRequest(req, false, "vault:v1:+px9V/7QYLfdK808zPESC2T/L33uFf4Blzsn9Jy838o=") + + req.Data["algorithm"] = "sha3-384" + doRequest(req, false, "vault:v1:YGoRwN4UdTRYZeOER86jsQOB8piWenzLDzJ2wJQK/Jq59rAsY8lh7SCdqqCyFg70") + + req.Data["algorithm"] = "sha3-512" + doRequest(req, false, "vault:v1:GrNA8sU88naMPEQ7UZGj9EJl7YJhl03AFHfxcEURFrtvnobdea9ZlZHePpxAx/oCaC7R2HkrAO+Tu3uXPIl3lg==") + + // Test returning SHA3 as base64 + req.Data["format"] = "base64" + doRequest(req, false, "vault:v1:GrNA8sU88naMPEQ7UZGj9EJl7YJhl03AFHfxcEURFrtvnobdea9ZlZHePpxAx/oCaC7R2HkrAO+Tu3uXPIl3lg==") + + req.Data["algorithm"] = "foobar" + doRequest(req, true, "") + + req.Data["algorithm"] = "sha2-256" + req.Data["input"] = "foobar" + doRequest(req, true, "") + req.Data["input"] = "dGhlIHF1aWNrIGJyb3duIGZveA==" + + // Rotate + err = p.Rotate(context.Background(), storage, b.GetRandomReader()) + if err != nil { + t.Fatal(err) + } + keyEntry = p.Keys["2"] + // Set to another value we control + keyEntry.HMACKey = []byte("12345678901234567890123456789012") + p.Keys["2"] = keyEntry + if err = p.Persist(context.Background(), storage); err != nil { + t.Fatal(err) + } + + doRequest(req, false, "vault:v2:Dt+mO/B93kuWUbGMMobwUNX5Wodr6dL3JH4DMfpQ0kw=") + + // Verify a previous version + req.Path = "verify/" + c.name + + req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" + resp, err := b.HandleRequest(context.Background(), req) if err != nil { t.Fatalf("%v: %v", err, resp) } @@ -86,116 +182,36 @@ func TestTransit_HMAC(t *testing.T) { t.Fatal("expected non-nil response") } if resp.Data["valid"].(bool) == false { - panic(fmt.Sprintf("error validating hmac;\nreq:\n%#v\nresp:\n%#v", *req, *resp)) + t.Fatalf("error validating hmac\nreq\n%#v\nresp\n%#v", *req, *resp) } - } - // Comparisons are against values generated via openssl + // Try a bad value + req.Data["hmac"] = "vault:v1:UcBvm4VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("%v: %v", err, resp) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if resp.Data["valid"].(bool) { + t.Fatalf("expected error validating hmac") + } - // Test defaults -- sha2-256 - doRequest(req, false, "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=") + // Set min decryption version, attempt to verify + p.MinDecryptionVersion = 2 + if err = p.Persist(context.Background(), storage); err != nil { + t.Fatal(err) + } - // Test algorithm selection in the path - req.Path = "hmac/foo/sha2-224" - doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") - - // Reset and test algorithm selection in the data - req.Path = "hmac/foo" - req.Data["algorithm"] = "sha2-224" - doRequest(req, false, "vault:v1:3p+ZWVquYDvu2dSTCa65Y3fgoMfIAc6fNaBbtg==") - - req.Data["algorithm"] = "sha2-384" - doRequest(req, false, "vault:v1:jDB9YXdPjpmr29b1JCIEJO93IydlKVfD9mA2EO9OmJtJQg3QAV5tcRRRb7IQGW9p") - - req.Data["algorithm"] = "sha2-512" - doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==") - - // Test returning as base64 - req.Data["format"] = "base64" - doRequest(req, false, "vault:v1:PSXLXvkvKF4CpU65e2bK1tGBZQpcpCEM32fq2iUoiTyQQCfBcGJJItQ+60tMwWXAPQrC290AzTrNJucGrr4GFA==") - - // Test SHA3 - req.Path = "hmac/foo" - req.Data["algorithm"] = "sha3-224" - doRequest(req, false, "vault:v1:TGipmKH8LR/BkMolYpDYy0BJCIhTtGPDhV2VkQ==") - - req.Data["algorithm"] = "sha3-256" - doRequest(req, false, "vault:v1:+px9V/7QYLfdK808zPESC2T/L33uFf4Blzsn9Jy838o=") - - req.Data["algorithm"] = "sha3-384" - doRequest(req, false, "vault:v1:YGoRwN4UdTRYZeOER86jsQOB8piWenzLDzJ2wJQK/Jq59rAsY8lh7SCdqqCyFg70") - - req.Data["algorithm"] = "sha3-512" - doRequest(req, false, "vault:v1:GrNA8sU88naMPEQ7UZGj9EJl7YJhl03AFHfxcEURFrtvnobdea9ZlZHePpxAx/oCaC7R2HkrAO+Tu3uXPIl3lg==") - - // Test returning SHA3 as base64 - req.Data["format"] = "base64" - doRequest(req, false, "vault:v1:GrNA8sU88naMPEQ7UZGj9EJl7YJhl03AFHfxcEURFrtvnobdea9ZlZHePpxAx/oCaC7R2HkrAO+Tu3uXPIl3lg==") - - req.Data["algorithm"] = "foobar" - doRequest(req, true, "") - - req.Data["algorithm"] = "sha2-256" - req.Data["input"] = "foobar" - doRequest(req, true, "") - req.Data["input"] = "dGhlIHF1aWNrIGJyb3duIGZveA==" - - // Rotate - err = p.Rotate(context.Background(), storage, b.GetRandomReader()) - if err != nil { - t.Fatal(err) - } - keyEntry = p.Keys["2"] - // Set to another value we control - keyEntry.HMACKey = []byte("12345678901234567890123456789012") - p.Keys["2"] = keyEntry - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - doRequest(req, false, "vault:v2:Dt+mO/B93kuWUbGMMobwUNX5Wodr6dL3JH4DMfpQ0kw=") - - // Verify a previous version - req.Path = "verify/foo" - - req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["valid"].(bool) == false { - t.Fatalf("error validating hmac\nreq\n%#v\nresp\n%#v", *req, *resp) - } - - // Try a bad value - req.Data["hmac"] = "vault:v1:UcBvm4VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatalf("%v: %v", err, resp) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if resp.Data["valid"].(bool) { - t.Fatalf("expected error validating hmac") - } - - // Set min decryption version, attempt to verify - p.MinDecryptionVersion = 2 - if err = p.Persist(context.Background(), storage); err != nil { - t.Fatal(err) - } - - req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" - resp, err = b.HandleRequest(context.Background(), req) - if err == nil { - t.Fatalf("expected an error, got response %#v", resp) - } - if err != logical.ErrInvalidRequest { - t.Fatalf("expected invalid request error, got %v", err) + req.Data["hmac"] = "vault:v1:UcBvm5VskkukzZHlPgm3p5P/Yr/PV6xpuOGZISya3A4=" + resp, err = b.HandleRequest(context.Background(), req) + if err == nil { + t.Fatalf("expected an error, got response %#v", resp) + } + if err != logical.ErrInvalidRequest { + t.Fatalf("expected invalid request error, got %v", err) + } } } diff --git a/builtin/logical/transit/path_import.go b/builtin/logical/transit/path_import.go index c9e983751..47997e0aa 100644 --- a/builtin/logical/transit/path_import.go +++ b/builtin/logical/transit/path_import.go @@ -177,6 +177,8 @@ func (b *backend) pathImportWrite(ctx context.Context, req *logical.Request, d * polReq.KeyType = keysutil.KeyType_RSA3072 case "rsa-4096": polReq.KeyType = keysutil.KeyType_RSA4096 + case "hmac": + polReq.KeyType = keysutil.KeyType_HMAC default: return logical.ErrorResponse(fmt.Sprintf("unknown key type: %v", keyType)), logical.ErrInvalidRequest } diff --git a/builtin/logical/transit/path_import_test.go b/builtin/logical/transit/path_import_test.go index a532dfdbe..d31b12b45 100644 --- a/builtin/logical/transit/path_import_test.go +++ b/builtin/logical/transit/path_import_test.go @@ -30,6 +30,7 @@ var keyTypes = []string{ "rsa-2048", "rsa-3072", "rsa-4096", + "hmac", } var hashFns = []string{ @@ -543,7 +544,7 @@ func wrapTargetKeyForImport(t *testing.T, wrappingKey *rsa.PublicKey, targetKey var ok bool var err error switch targetKeyType { - case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305": + case "aes128-gcm96", "aes256-gcm96", "chacha20-poly1305", "hmac": preppedTargetKey, ok = targetKey.([]byte) if !ok { 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 { case "aes128-gcm96": return uuid.GenerateRandomBytes(16) - case "aes256-gcm96": + case "aes256-gcm96", "hmac": return uuid.GenerateRandomBytes(32) case "chacha20-poly1305": return uuid.GenerateRandomBytes(32) diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index e64adb24a..e8edabc17 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -103,6 +103,11 @@ being automatically rotated. A value of 0 (default) disables automatic rotation for the 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{ @@ -130,6 +135,7 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d * derived := d.Get("derived").(bool) convergent := d.Get("convergent_encryption").(bool) keyType := d.Get("type").(string) + keySize := d.Get("key_size").(int) exportable := d.Get("exportable").(bool) allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool) 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, AutoRotatePeriod: autoRotatePeriod, } + switch keyType { case "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 case "rsa-4096": polReq.KeyType = keysutil.KeyType_RSA4096 + case "hmac": + polReq.KeyType = keysutil.KeyType_HMAC default: 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()) if err != nil { @@ -242,6 +260,9 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f "imported_key": p.Imported, }, } + if p.KeySize != 0 { + resp.Data["key_size"] = p.KeySize + } if p.Imported { resp.Data["imported_key_allow_rotation"] = p.AllowImportedKeyRotation diff --git a/changelog/16668.txt b/changelog/16668.txt new file mode 100644 index 000000000..745cf0beb --- /dev/null +++ b/changelog/16668.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/transit: Add a dedicated HMAC key type, which can be used with key import. +``` \ No newline at end of file diff --git a/sdk/helper/keysutil/lock_manager.go b/sdk/helper/keysutil/lock_manager.go index df855c93f..a60cf69d5 100644 --- a/sdk/helper/keysutil/lock_manager.go +++ b/sdk/helper/keysutil/lock_manager.go @@ -36,6 +36,9 @@ type PolicyRequest struct { // The key type KeyType KeyType + // The key size for variable key size algorithms + KeySize int + // Whether it should be derived Derived bool @@ -373,6 +376,11 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io cleanup() 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: cleanup() @@ -387,6 +395,7 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io Exportable: req.Exportable, AllowPlaintextBackup: req.AllowPlaintextBackup, AutoRotatePeriod: req.AutoRotatePeriod, + KeySize: req.KeySize, } if req.Derived { diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 86b5e0e49..d92aa3738 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -45,6 +45,9 @@ import ( const ( Kdf_hmac_sha256_counter = iota // built-in helper 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 @@ -59,6 +62,7 @@ const ( KeyType_ECDSA_P521 KeyType_AES128_GCM96 KeyType_RSA3072 + KeyType_HMAC ) const ( @@ -160,6 +164,8 @@ func (kt KeyType) String() string { return "rsa-3072" case KeyType_RSA4096: return "rsa-4096" + case KeyType_HMAC: + return "hmac" } return "[unknown]" @@ -318,9 +324,10 @@ type Policy struct { // served after a delete. deleted uint32 - Name string `json:"name"` - Key []byte `json:"key,omitempty"` // DEPRECATED - Keys keyEntryMap `json:"keys"` + Name string `json:"name"` + Key []byte `json:"key,omitempty"` // DEPRECATED + KeySize int `json:"key_size,omitempty"` // For algorithms with variable key sizes + Keys keyEntryMap `json:"keys"` // Derived keys MUST provide a context and the master underlying key is // never used. If convergent encryption is true, the context will be used @@ -1025,10 +1032,13 @@ func (p *Policy) HMACKey(version int) ([]byte, error) { if err != nil { return nil, err } + + if p.Type == KeyType_HMAC { + return keyEntry.Key, nil + } if keyEntry.HMACKey == nil { return nil, fmt.Errorf("no HMAC key exists for that key version") } - return keyEntry.HMACKey, nil } @@ -1386,19 +1396,25 @@ func (p *Policy) Import(ctx context.Context, storage logical.Storage, key []byte DeprecatedCreationTime: now.Unix(), } - hmacKey, err := uuid.GenerateRandomBytesWithReader(32, randReader) - if err != nil { - return err + if p.Type != KeyType_HMAC { + hmacKey, err := uuid.GenerateRandomBytesWithReader(32, randReader) + if err != nil { + return err + } + entry.HMACKey = hmacKey } - entry.HMACKey = hmacKey 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) } - 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 + if p.Type == KeyType_HMAC { + p.KeySize = len(key) + } } else { parsedPrivateKey, err := x509.ParsePKCS8PrivateKey(key) if err != nil { @@ -1549,11 +1565,16 @@ func (p *Policy) RotateInMemory(randReader io.Reader) (retErr error) { entry.HMACKey = hmacKey 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 numBytes := 32 if p.Type == KeyType_AES128_GCM96 { 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) if err != nil { diff --git a/website/content/api-docs/secret/transit.mdx b/website/content/api-docs/secret/transit.mdx index 65c49612e..cc103bcb0 100644 --- a/website/content/api-docs/secret/transit.mdx +++ b/website/content/api-docs/secret/transit.mdx @@ -63,10 +63,19 @@ values set here cannot be changed after key creation. - `rsa-2048` - RSA with bit size of 2048 (asymmetric) - `rsa-3072` - RSA with bit size of 3072 (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 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 this key should be rotated automatically. Setting this to "0" (the default) 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 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 `allow_rotation` field was set to `true` on import. Once an imported key is rotated within Vault, it will not support further import operations. diff --git a/website/content/docs/secrets/transit/index.mdx b/website/content/docs/secrets/transit/index.mdx index 9dcf37717..b78f4c5b4 100644 --- a/website/content/docs/secrets/transit/index.mdx +++ b/website/content/docs/secrets/transit/index.mdx @@ -85,10 +85,17 @@ types also generate separate HMAC keys): signature verification - `rsa-4096`: 4096-bit RSA key; supports encryption, decryption, signing, and signature verification +- `hmac`: HMAC; supporting HMAC generation and verification. ~> **Note**: In FIPS 140-2 mode, the following algorithms are not certified 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 is a mode where the same set of plaintext+context always