add early validation for enforced encryption methods (#1711)

Signed-off-by: ollevche <ollevche@gmail.com>
This commit is contained in:
Oleksandr Levchenkov 2024-06-12 21:06:06 +03:00 committed by GitHub
parent ed0c761b0e
commit 568ff66bef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 212 additions and 8 deletions

View File

@ -13,6 +13,7 @@ ENHANCEMENTS:
* Allow to reference variable inside the `variables` block of a test file. ([1488](https://github.com/opentofu/opentofu/pull/1488))
BUG FIXES:
* Fixed validation for `enforced` flag in encryption configuration. ([#1711](https://github.com/opentofu/opentofu/pull/1711))
* Fixed crash in gcs backend when using certain commands. ([#1618](https://github.com/opentofu/opentofu/pull/1618))
* Fixed inmem backend crash due to missing struct field. ([#1619](https://github.com/opentofu/opentofu/pull/1619))
* Added a check in the `tofu test` to validate that the names of test run blocks do not contain spaces. ([#1489](https://github.com/opentofu/opentofu/pull/1489))

View File

@ -95,10 +95,6 @@ func (s *baseEncryption) encrypt(data []byte, enhance func(basedata) interface{}
encryptor := s.encMethods[0]
if unencrypted.Is(encryptor) {
// ensure that the method is defined when Enforced is true
if s.enforced {
return nil, fmt.Errorf("unable to use unencrypted method for %q when enforced = true", s.name)
}
return data, nil
}
@ -143,9 +139,6 @@ func (s *baseEncryption) decrypt(data []byte, validator func([]byte) error) ([]b
// Yep, it's already decrypted
for _, method := range s.encMethods {
if unencrypted.Is(method) {
if s.enforced {
return nil, fmt.Errorf("unable to use unencrypted method when enforced = true")
}
return data, nil
}
}

View File

@ -123,3 +123,8 @@ func (a aesgcm) getGCM(key []byte) (cipher.AEAD, error) {
}
return gcm, nil
}
func Is(m method.Method) bool {
_, ok := m.(*aesgcm)
return ok
}

View File

@ -13,6 +13,7 @@ import (
"github.com/opentofu/opentofu/internal/encryption/config"
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
"github.com/opentofu/opentofu/internal/encryption/method"
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
"github.com/opentofu/opentofu/internal/encryption/registry"
"github.com/zclconf/go-cty/cty"
)
@ -58,7 +59,21 @@ func (base *baseEncryption) buildTargetMethods(meta map[keyprovider.Addr][]byte)
}
methods, targetDiags := builder.build(base.target, base.name)
return methods, append(diags, targetDiags...)
diags = append(diags, targetDiags...)
if base.enforced {
for _, m := range methods {
if unencrypted.Is(m) {
return nil, append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unencrypted method is forbidden",
Detail: "Unable to use `unencrypted` method since the `enforced` flag is used.",
})
}
}
}
return methods, diags
}
// build sets up a single target for encryption. It returns the primary and fallback methods for the target, as well

View File

@ -0,0 +1,190 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package encryption
import (
"reflect"
"testing"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/encryption/config"
"github.com/opentofu/opentofu/internal/encryption/keyprovider"
"github.com/opentofu/opentofu/internal/encryption/keyprovider/static"
"github.com/opentofu/opentofu/internal/encryption/method"
"github.com/opentofu/opentofu/internal/encryption/method/aesgcm"
"github.com/opentofu/opentofu/internal/encryption/method/unencrypted"
"github.com/opentofu/opentofu/internal/encryption/registry"
"github.com/opentofu/opentofu/internal/encryption/registry/lockingencryptionregistry"
)
func TestBaseEncryption_buildTargetMethods(t *testing.T) {
t.Parallel()
tests := map[string]btmTestCase{
"simple": {
rawConfig: `
key_provider "static" "basic" {
key = "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169"
}
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
state {
method = method.aes_gcm.example
}
`,
wantMethods: []func(method.Method) bool{
aesgcm.Is,
},
},
"no-key-provider": {
rawConfig: `
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
state {
method = method.aes_gcm.example
}
`,
wantErr: `Test Config Source:3,25-32: Unsupported attribute; This object does not have an attribute named "static".`,
},
"fallback": {
rawConfig: `
key_provider "static" "basic" {
key = "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169"
}
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
method "unencrypted" "example" {
}
state {
method = method.aes_gcm.example
fallback {
method = method.unencrypted.example
}
}
`,
wantMethods: []func(method.Method) bool{
aesgcm.Is,
unencrypted.Is,
},
},
"enforced": {
rawConfig: `
key_provider "static" "basic" {
key = "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169"
}
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
method "unencrypted" "example" {
}
state {
enforced = true
method = method.aes_gcm.example
}
`,
wantMethods: []func(method.Method) bool{
aesgcm.Is,
},
},
"enforced-with-unencrypted": {
rawConfig: `
key_provider "static" "basic" {
key = "6f6f706830656f67686f6834616872756f3751756165686565796f6f72653169"
}
method "aes_gcm" "example" {
keys = key_provider.static.basic
}
method "unencrypted" "example" {
}
state {
enforced = true
method = method.aes_gcm.example
fallback {
method = method.unencrypted.example
}
}
`,
wantErr: "<nil>: Unencrypted method is forbidden; Unable to use `unencrypted` method since the `enforced` flag is used.",
},
}
reg := lockingencryptionregistry.New()
if err := reg.RegisterKeyProvider(static.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(aesgcm.New()); err != nil {
panic(err)
}
if err := reg.RegisterMethod(unencrypted.New()); err != nil {
panic(err)
}
for name, test := range tests {
t.Run(name, test.newTestRun(reg))
}
}
type btmTestCase struct {
rawConfig string // must contain state target
wantMethods []func(method.Method) bool
wantErr string
}
func (testCase btmTestCase) newTestRun(reg registry.Registry) func(t *testing.T) {
return func(t *testing.T) {
t.Parallel()
cfg, diags := config.LoadConfigFromString("Test Config Source", testCase.rawConfig)
if diags.HasErrors() {
panic(diags.Error())
}
base := &baseEncryption{
enc: &encryption{
cfg: cfg,
reg: reg,
},
target: cfg.State.AsTargetConfig(),
enforced: cfg.State.Enforced,
name: "test",
encMeta: make(map[keyprovider.Addr][]byte),
}
methods, diags := base.buildTargetMethods(base.encMeta)
if diags.HasErrors() {
if !hasDiagWithMsg(diags, testCase.wantErr) {
t.Fatalf("Got unexpected error: %v", diags.Error())
}
}
if !diags.HasErrors() && testCase.wantErr != "" {
t.Fatalf("Expected error (got none): %v", testCase.wantErr)
}
if len(methods) != len(testCase.wantMethods) {
t.Fatalf("Expected %d method(s), got %d", len(testCase.wantMethods), len(methods))
}
for i, m := range methods {
if !testCase.wantMethods[i](m) {
t.Fatalf("Got unexpected method: %v", reflect.TypeOf(m).String())
}
}
}
}
func hasDiagWithMsg(diags hcl.Diagnostics, msg string) bool {
for _, d := range diags {
if d.Error() == msg {
return true
}
}
return false
}