You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							598 lines
						
					
					
						
							15 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							598 lines
						
					
					
						
							15 KiB
						
					
					
				| package kms_test | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"fmt" | |
| 	"os" | |
| 	"os/exec" | |
| 	"strings" | |
| 	"testing" | |
| 	"time" | |
| 
 | |
| 	"github.com/hashicorp/vault/api" | |
| 	"github.com/stretchr/testify/assert" | |
| 	"github.com/stretchr/testify/require" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/kms" | |
| 	_ "github.com/seaweedfs/seaweedfs/weed/kms/openbao" | |
| ) | |
| 
 | |
| const ( | |
| 	OpenBaoAddress = "http://127.0.0.1:8200" | |
| 	OpenBaoToken   = "root-token-for-testing" | |
| 	TransitPath    = "transit" | |
| ) | |
| 
 | |
| // Test configuration for OpenBao KMS provider | |
| type testConfig struct { | |
| 	config map[string]interface{} | |
| } | |
| 
 | |
| func (c *testConfig) GetString(key string) string { | |
| 	if val, ok := c.config[key]; ok { | |
| 		if str, ok := val.(string); ok { | |
| 			return str | |
| 		} | |
| 	} | |
| 	return "" | |
| } | |
| 
 | |
| func (c *testConfig) GetBool(key string) bool { | |
| 	if val, ok := c.config[key]; ok { | |
| 		if b, ok := val.(bool); ok { | |
| 			return b | |
| 		} | |
| 	} | |
| 	return false | |
| } | |
| 
 | |
| func (c *testConfig) GetInt(key string) int { | |
| 	if val, ok := c.config[key]; ok { | |
| 		if i, ok := val.(int); ok { | |
| 			return i | |
| 		} | |
| 		if f, ok := val.(float64); ok { | |
| 			return int(f) | |
| 		} | |
| 	} | |
| 	return 0 | |
| } | |
| 
 | |
| func (c *testConfig) GetStringSlice(key string) []string { | |
| 	if val, ok := c.config[key]; ok { | |
| 		if slice, ok := val.([]string); ok { | |
| 			return slice | |
| 		} | |
| 	} | |
| 	return nil | |
| } | |
| 
 | |
| func (c *testConfig) SetDefault(key string, value interface{}) { | |
| 	if c.config == nil { | |
| 		c.config = make(map[string]interface{}) | |
| 	} | |
| 	if _, exists := c.config[key]; !exists { | |
| 		c.config[key] = value | |
| 	} | |
| } | |
| 
 | |
| // setupOpenBao starts OpenBao in development mode for testing | |
| func setupOpenBao(t *testing.T) (*exec.Cmd, func()) { | |
| 	// Check if OpenBao is running in Docker (via make dev-openbao) | |
| 	client, err := api.NewClient(&api.Config{Address: OpenBaoAddress}) | |
| 	if err == nil { | |
| 		client.SetToken(OpenBaoToken) | |
| 		_, err = client.Sys().Health() | |
| 		if err == nil { | |
| 			glog.V(1).Infof("Using existing OpenBao server at %s", OpenBaoAddress) | |
| 			// Return dummy command and cleanup function for existing server | |
| 			return nil, func() {} | |
| 		} | |
| 	} | |
| 
 | |
| 	// Check if OpenBao binary is available for starting locally | |
| 	_, err = exec.LookPath("bao") | |
| 	if err != nil { | |
| 		t.Skip("OpenBao not running and bao binary not found. Run 'cd test/kms && make dev-openbao' first") | |
| 	} | |
| 
 | |
| 	// Start OpenBao in dev mode | |
| 	cmd := exec.Command("bao", "server", "-dev", "-dev-root-token-id="+OpenBaoToken, "-dev-listen-address=127.0.0.1:8200") | |
| 	cmd.Env = append(os.Environ(), "BAO_DEV_ROOT_TOKEN_ID="+OpenBaoToken) | |
| 
 | |
| 	// Capture output for debugging | |
| 	cmd.Stdout = os.Stdout | |
| 	cmd.Stderr = os.Stderr | |
| 
 | |
| 	err = cmd.Start() | |
| 	require.NoError(t, err, "Failed to start OpenBao server") | |
| 
 | |
| 	// Wait for OpenBao to be ready | |
| 	client, err = api.NewClient(&api.Config{Address: OpenBaoAddress}) | |
| 	require.NoError(t, err) | |
| 	client.SetToken(OpenBaoToken) | |
| 
 | |
| 	// Wait up to 30 seconds for OpenBao to be ready | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | |
| 	defer cancel() | |
| 
 | |
| 	for { | |
| 		select { | |
| 		case <-ctx.Done(): | |
| 			cmd.Process.Kill() | |
| 			t.Fatal("Timeout waiting for OpenBao to start") | |
| 		default: | |
| 			// Try to check health | |
| 			resp, err := client.Sys().Health() | |
| 			if err == nil && resp.Initialized { | |
| 				glog.V(1).Infof("OpenBao server ready") | |
| 				goto ready | |
| 			} | |
| 			time.Sleep(500 * time.Millisecond) | |
| 		} | |
| 	} | |
| 
 | |
| ready: | |
| 	// Setup cleanup function | |
| 	cleanup := func() { | |
| 		if cmd != nil && cmd.Process != nil { | |
| 			glog.V(1).Infof("Stopping OpenBao server") | |
| 			cmd.Process.Kill() | |
| 			cmd.Wait() | |
| 		} | |
| 	} | |
| 
 | |
| 	return cmd, cleanup | |
| } | |
| 
 | |
| // setupTransitEngine enables and configures the transit secrets engine | |
| func setupTransitEngine(t *testing.T) { | |
| 	client, err := api.NewClient(&api.Config{Address: OpenBaoAddress}) | |
| 	require.NoError(t, err) | |
| 	client.SetToken(OpenBaoToken) | |
| 
 | |
| 	// Enable transit secrets engine | |
| 	err = client.Sys().Mount(TransitPath, &api.MountInput{ | |
| 		Type:        "transit", | |
| 		Description: "Transit engine for KMS testing", | |
| 	}) | |
| 	if err != nil && !strings.Contains(err.Error(), "path is already in use") { | |
| 		require.NoError(t, err, "Failed to enable transit engine") | |
| 	} | |
| 
 | |
| 	// Create test encryption keys | |
| 	testKeys := []string{"test-key-1", "test-key-2", "seaweedfs-test-key"} | |
| 
 | |
| 	for _, keyName := range testKeys { | |
| 		keyData := map[string]interface{}{ | |
| 			"type": "aes256-gcm96", | |
| 		} | |
| 
 | |
| 		path := fmt.Sprintf("%s/keys/%s", TransitPath, keyName) | |
| 		_, err = client.Logical().Write(path, keyData) | |
| 		if err != nil && !strings.Contains(err.Error(), "key already exists") { | |
| 			require.NoError(t, err, "Failed to create test key %s", keyName) | |
| 		} | |
| 
 | |
| 		glog.V(2).Infof("Created/verified test key: %s", keyName) | |
| 	} | |
| } | |
| 
 | |
| func TestOpenBaoKMSProvider_Integration(t *testing.T) { | |
| 	// Start OpenBao server | |
| 	_, cleanup := setupOpenBao(t) | |
| 	defer cleanup() | |
| 
 | |
| 	// Setup transit engine and keys | |
| 	setupTransitEngine(t) | |
| 
 | |
| 	t.Run("CreateProvider", func(t *testing.T) { | |
| 		config := &testConfig{ | |
| 			config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        OpenBaoToken, | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 		} | |
| 
 | |
| 		provider, err := kms.GetProvider("openbao", config) | |
| 		require.NoError(t, err) | |
| 		require.NotNil(t, provider) | |
| 
 | |
| 		defer provider.Close() | |
| 	}) | |
| 
 | |
| 	t.Run("ProviderRegistration", func(t *testing.T) { | |
| 		// Test that the provider is registered | |
| 		providers := kms.ListProviders() | |
| 		assert.Contains(t, providers, "openbao") | |
| 		assert.Contains(t, providers, "vault") // Compatibility alias | |
| 	}) | |
| 
 | |
| 	t.Run("GenerateDataKey", func(t *testing.T) { | |
| 		config := &testConfig{ | |
| 			config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        OpenBaoToken, | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 		} | |
| 
 | |
| 		provider, err := kms.GetProvider("openbao", config) | |
| 		require.NoError(t, err) | |
| 		defer provider.Close() | |
| 
 | |
| 		ctx := context.Background() | |
| 		req := &kms.GenerateDataKeyRequest{ | |
| 			KeyID:   "test-key-1", | |
| 			KeySpec: kms.KeySpecAES256, | |
| 			EncryptionContext: map[string]string{ | |
| 				"test": "context", | |
| 				"env":  "integration", | |
| 			}, | |
| 		} | |
| 
 | |
| 		resp, err := provider.GenerateDataKey(ctx, req) | |
| 		require.NoError(t, err) | |
| 		require.NotNil(t, resp) | |
| 
 | |
| 		assert.Equal(t, "test-key-1", resp.KeyID) | |
| 		assert.Len(t, resp.Plaintext, 32) // 256 bits | |
| 		assert.NotEmpty(t, resp.CiphertextBlob) | |
| 
 | |
| 		// Verify the response is in standardized envelope format | |
| 		envelope, err := kms.ParseEnvelope(resp.CiphertextBlob) | |
| 		assert.NoError(t, err) | |
| 		assert.Equal(t, "openbao", envelope.Provider) | |
| 		assert.Equal(t, "test-key-1", envelope.KeyID) | |
| 		assert.True(t, strings.HasPrefix(envelope.Ciphertext, "vault:")) // Raw OpenBao format inside envelope | |
| 	}) | |
| 
 | |
| 	t.Run("DecryptDataKey", func(t *testing.T) { | |
| 		config := &testConfig{ | |
| 			config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        OpenBaoToken, | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 		} | |
| 
 | |
| 		provider, err := kms.GetProvider("openbao", config) | |
| 		require.NoError(t, err) | |
| 		defer provider.Close() | |
| 
 | |
| 		ctx := context.Background() | |
| 
 | |
| 		// First generate a data key | |
| 		genReq := &kms.GenerateDataKeyRequest{ | |
| 			KeyID:   "test-key-1", | |
| 			KeySpec: kms.KeySpecAES256, | |
| 			EncryptionContext: map[string]string{ | |
| 				"test": "decrypt", | |
| 				"env":  "integration", | |
| 			}, | |
| 		} | |
| 
 | |
| 		genResp, err := provider.GenerateDataKey(ctx, genReq) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		// Now decrypt it | |
| 		decReq := &kms.DecryptRequest{ | |
| 			CiphertextBlob: genResp.CiphertextBlob, | |
| 			EncryptionContext: map[string]string{ | |
| 				"openbao:key:name": "test-key-1", | |
| 				"test":             "decrypt", | |
| 				"env":              "integration", | |
| 			}, | |
| 		} | |
| 
 | |
| 		decResp, err := provider.Decrypt(ctx, decReq) | |
| 		require.NoError(t, err) | |
| 		require.NotNil(t, decResp) | |
| 
 | |
| 		assert.Equal(t, "test-key-1", decResp.KeyID) | |
| 		assert.Equal(t, genResp.Plaintext, decResp.Plaintext) | |
| 	}) | |
| 
 | |
| 	t.Run("DescribeKey", func(t *testing.T) { | |
| 		config := &testConfig{ | |
| 			config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        OpenBaoToken, | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 		} | |
| 
 | |
| 		provider, err := kms.GetProvider("openbao", config) | |
| 		require.NoError(t, err) | |
| 		defer provider.Close() | |
| 
 | |
| 		ctx := context.Background() | |
| 		req := &kms.DescribeKeyRequest{ | |
| 			KeyID: "test-key-1", | |
| 		} | |
| 
 | |
| 		resp, err := provider.DescribeKey(ctx, req) | |
| 		require.NoError(t, err) | |
| 		require.NotNil(t, resp) | |
| 
 | |
| 		assert.Equal(t, "test-key-1", resp.KeyID) | |
| 		assert.Contains(t, resp.ARN, "openbao:") | |
| 		assert.Equal(t, kms.KeyStateEnabled, resp.KeyState) | |
| 		assert.Equal(t, kms.KeyUsageEncryptDecrypt, resp.KeyUsage) | |
| 	}) | |
| 
 | |
| 	t.Run("NonExistentKey", func(t *testing.T) { | |
| 		config := &testConfig{ | |
| 			config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        OpenBaoToken, | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 		} | |
| 
 | |
| 		provider, err := kms.GetProvider("openbao", config) | |
| 		require.NoError(t, err) | |
| 		defer provider.Close() | |
| 
 | |
| 		ctx := context.Background() | |
| 		req := &kms.DescribeKeyRequest{ | |
| 			KeyID: "non-existent-key", | |
| 		} | |
| 
 | |
| 		_, err = provider.DescribeKey(ctx, req) | |
| 		require.Error(t, err) | |
| 
 | |
| 		kmsErr, ok := err.(*kms.KMSError) | |
| 		require.True(t, ok) | |
| 		assert.Equal(t, kms.ErrCodeNotFoundException, kmsErr.Code) | |
| 	}) | |
| 
 | |
| 	t.Run("MultipleKeys", func(t *testing.T) { | |
| 		config := &testConfig{ | |
| 			config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        OpenBaoToken, | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 		} | |
| 
 | |
| 		provider, err := kms.GetProvider("openbao", config) | |
| 		require.NoError(t, err) | |
| 		defer provider.Close() | |
| 
 | |
| 		ctx := context.Background() | |
| 
 | |
| 		// Test with multiple keys | |
| 		testKeys := []string{"test-key-1", "test-key-2", "seaweedfs-test-key"} | |
| 
 | |
| 		for _, keyName := range testKeys { | |
| 			t.Run(fmt.Sprintf("Key_%s", keyName), func(t *testing.T) { | |
| 				// Generate data key | |
| 				genReq := &kms.GenerateDataKeyRequest{ | |
| 					KeyID:   keyName, | |
| 					KeySpec: kms.KeySpecAES256, | |
| 					EncryptionContext: map[string]string{ | |
| 						"key": keyName, | |
| 					}, | |
| 				} | |
| 
 | |
| 				genResp, err := provider.GenerateDataKey(ctx, genReq) | |
| 				require.NoError(t, err) | |
| 				assert.Equal(t, keyName, genResp.KeyID) | |
| 
 | |
| 				// Decrypt data key | |
| 				decReq := &kms.DecryptRequest{ | |
| 					CiphertextBlob: genResp.CiphertextBlob, | |
| 					EncryptionContext: map[string]string{ | |
| 						"openbao:key:name": keyName, | |
| 						"key":              keyName, | |
| 					}, | |
| 				} | |
| 
 | |
| 				decResp, err := provider.Decrypt(ctx, decReq) | |
| 				require.NoError(t, err) | |
| 				assert.Equal(t, genResp.Plaintext, decResp.Plaintext) | |
| 			}) | |
| 		} | |
| 	}) | |
| } | |
| 
 | |
| func TestOpenBaoKMSProvider_ErrorHandling(t *testing.T) { | |
| 	// Start OpenBao server | |
| 	_, cleanup := setupOpenBao(t) | |
| 	defer cleanup() | |
| 
 | |
| 	setupTransitEngine(t) | |
| 
 | |
| 	t.Run("InvalidToken", func(t *testing.T) { | |
| 		t.Skip("Skipping invalid token test - OpenBao dev mode may be too permissive") | |
| 
 | |
| 		config := &testConfig{ | |
| 			config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        "invalid-token", | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 		} | |
| 
 | |
| 		provider, err := kms.GetProvider("openbao", config) | |
| 		require.NoError(t, err) // Provider creation doesn't validate token | |
| 		defer provider.Close() | |
| 
 | |
| 		ctx := context.Background() | |
| 		req := &kms.GenerateDataKeyRequest{ | |
| 			KeyID:   "test-key-1", | |
| 			KeySpec: kms.KeySpecAES256, | |
| 		} | |
| 
 | |
| 		_, err = provider.GenerateDataKey(ctx, req) | |
| 		require.Error(t, err) | |
| 
 | |
| 		// Check that it's a KMS error (could be access denied or other auth error) | |
| 		kmsErr, ok := err.(*kms.KMSError) | |
| 		require.True(t, ok, "Expected KMSError but got: %T", err) | |
| 		// OpenBao might return different error codes for invalid tokens | |
| 		assert.Contains(t, []string{kms.ErrCodeAccessDenied, kms.ErrCodeKMSInternalFailure}, kmsErr.Code) | |
| 	}) | |
| 
 | |
| } | |
| 
 | |
| func TestKMSManager_WithOpenBao(t *testing.T) { | |
| 	// Start OpenBao server | |
| 	_, cleanup := setupOpenBao(t) | |
| 	defer cleanup() | |
| 
 | |
| 	setupTransitEngine(t) | |
| 
 | |
| 	t.Run("KMSManagerIntegration", func(t *testing.T) { | |
| 		manager := kms.InitializeKMSManager() | |
| 
 | |
| 		// Add OpenBao provider to manager | |
| 		kmsConfig := &kms.KMSConfig{ | |
| 			Provider: "openbao", | |
| 			Config: map[string]interface{}{ | |
| 				"address":      OpenBaoAddress, | |
| 				"token":        OpenBaoToken, | |
| 				"transit_path": TransitPath, | |
| 			}, | |
| 			CacheEnabled: true, | |
| 			CacheTTL:     time.Hour, | |
| 		} | |
| 
 | |
| 		err := manager.AddKMSProvider("openbao-test", kmsConfig) | |
| 		require.NoError(t, err) | |
| 
 | |
| 		// Set as default provider | |
| 		err = manager.SetDefaultKMSProvider("openbao-test") | |
| 		require.NoError(t, err) | |
| 
 | |
| 		// Test bucket-specific assignment | |
| 		err = manager.SetBucketKMSProvider("test-bucket", "openbao-test") | |
| 		require.NoError(t, err) | |
| 
 | |
| 		// Test key operations through manager | |
| 		ctx := context.Background() | |
| 		resp, err := manager.GenerateDataKeyForBucket(ctx, "test-bucket", "test-key-1", kms.KeySpecAES256, map[string]string{ | |
| 			"bucket": "test-bucket", | |
| 		}) | |
| 		require.NoError(t, err) | |
| 		require.NotNil(t, resp) | |
| 
 | |
| 		assert.Equal(t, "test-key-1", resp.KeyID) | |
| 		assert.Len(t, resp.Plaintext, 32) | |
| 
 | |
| 		// Test decryption through manager | |
| 		decResp, err := manager.DecryptForBucket(ctx, "test-bucket", resp.CiphertextBlob, map[string]string{ | |
| 			"bucket": "test-bucket", | |
| 		}) | |
| 		require.NoError(t, err) | |
| 		assert.Equal(t, resp.Plaintext, decResp.Plaintext) | |
| 
 | |
| 		// Test health check | |
| 		health := manager.GetKMSHealth(ctx) | |
| 		assert.Contains(t, health, "openbao-test") | |
| 		assert.NoError(t, health["openbao-test"]) // Should be healthy | |
|  | |
| 		// Cleanup | |
| 		manager.Close() | |
| 	}) | |
| } | |
| 
 | |
| // Benchmark tests for performance | |
| func BenchmarkOpenBaoKMS_GenerateDataKey(b *testing.B) { | |
| 	if testing.Short() { | |
| 		b.Skip("Skipping benchmark in short mode") | |
| 	} | |
| 
 | |
| 	// Start OpenBao server | |
| 	_, cleanup := setupOpenBao(&testing.T{}) | |
| 	defer cleanup() | |
| 
 | |
| 	setupTransitEngine(&testing.T{}) | |
| 
 | |
| 	config := &testConfig{ | |
| 		config: map[string]interface{}{ | |
| 			"address":      OpenBaoAddress, | |
| 			"token":        OpenBaoToken, | |
| 			"transit_path": TransitPath, | |
| 		}, | |
| 	} | |
| 
 | |
| 	provider, err := kms.GetProvider("openbao", config) | |
| 	if err != nil { | |
| 		b.Fatal(err) | |
| 	} | |
| 	defer provider.Close() | |
| 
 | |
| 	ctx := context.Background() | |
| 	req := &kms.GenerateDataKeyRequest{ | |
| 		KeyID:   "test-key-1", | |
| 		KeySpec: kms.KeySpecAES256, | |
| 	} | |
| 
 | |
| 	b.ResetTimer() | |
| 	b.RunParallel(func(pb *testing.PB) { | |
| 		for pb.Next() { | |
| 			_, err := provider.GenerateDataKey(ctx, req) | |
| 			if err != nil { | |
| 				b.Fatal(err) | |
| 			} | |
| 		} | |
| 	}) | |
| } | |
| 
 | |
| func BenchmarkOpenBaoKMS_Decrypt(b *testing.B) { | |
| 	if testing.Short() { | |
| 		b.Skip("Skipping benchmark in short mode") | |
| 	} | |
| 
 | |
| 	// Start OpenBao server | |
| 	_, cleanup := setupOpenBao(&testing.T{}) | |
| 	defer cleanup() | |
| 
 | |
| 	setupTransitEngine(&testing.T{}) | |
| 
 | |
| 	config := &testConfig{ | |
| 		config: map[string]interface{}{ | |
| 			"address":      OpenBaoAddress, | |
| 			"token":        OpenBaoToken, | |
| 			"transit_path": TransitPath, | |
| 		}, | |
| 	} | |
| 
 | |
| 	provider, err := kms.GetProvider("openbao", config) | |
| 	if err != nil { | |
| 		b.Fatal(err) | |
| 	} | |
| 	defer provider.Close() | |
| 
 | |
| 	ctx := context.Background() | |
| 
 | |
| 	// Generate a data key for decryption testing | |
| 	genResp, err := provider.GenerateDataKey(ctx, &kms.GenerateDataKeyRequest{ | |
| 		KeyID:   "test-key-1", | |
| 		KeySpec: kms.KeySpecAES256, | |
| 	}) | |
| 	if err != nil { | |
| 		b.Fatal(err) | |
| 	} | |
| 
 | |
| 	decReq := &kms.DecryptRequest{ | |
| 		CiphertextBlob: genResp.CiphertextBlob, | |
| 		EncryptionContext: map[string]string{ | |
| 			"openbao:key:name": "test-key-1", | |
| 		}, | |
| 	} | |
| 
 | |
| 	b.ResetTimer() | |
| 	b.RunParallel(func(pb *testing.PB) { | |
| 		for pb.Next() { | |
| 			_, err := provider.Decrypt(ctx, decReq) | |
| 			if err != nil { | |
| 				b.Fatal(err) | |
| 			} | |
| 		} | |
| 	}) | |
| }
 |