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.
		
		
		
		
		
			
		
			
				
					
					
						
							403 lines
						
					
					
						
							13 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							403 lines
						
					
					
						
							13 KiB
						
					
					
				
								package openbao
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"crypto/rand"
							 | 
						|
									"encoding/base64"
							 | 
						|
									"encoding/json"
							 | 
						|
									"fmt"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									vault "github.com/hashicorp/vault/api"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/util"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								func init() {
							 | 
						|
									// Register the OpenBao/Vault KMS provider
							 | 
						|
									seaweedkms.RegisterProvider("openbao", NewOpenBaoKMSProvider)
							 | 
						|
									seaweedkms.RegisterProvider("vault", NewOpenBaoKMSProvider) // Alias for compatibility
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// OpenBaoKMSProvider implements the KMSProvider interface using OpenBao/Vault Transit engine
							 | 
						|
								type OpenBaoKMSProvider struct {
							 | 
						|
									client      *vault.Client
							 | 
						|
									transitPath string // Transit engine mount path (default: "transit")
							 | 
						|
									address     string
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// OpenBaoKMSConfig contains configuration for the OpenBao/Vault KMS provider
							 | 
						|
								type OpenBaoKMSConfig struct {
							 | 
						|
									Address        string `json:"address"`         // Vault address (e.g., "http://localhost:8200")
							 | 
						|
									Token          string `json:"token"`           // Vault token for authentication
							 | 
						|
									RoleID         string `json:"role_id"`         // AppRole role ID (alternative to token)
							 | 
						|
									SecretID       string `json:"secret_id"`       // AppRole secret ID (alternative to token)
							 | 
						|
									TransitPath    string `json:"transit_path"`    // Transit engine mount path (default: "transit")
							 | 
						|
									TLSSkipVerify  bool   `json:"tls_skip_verify"` // Skip TLS verification (for testing)
							 | 
						|
									CACert         string `json:"ca_cert"`         // Path to CA certificate
							 | 
						|
									ClientCert     string `json:"client_cert"`     // Path to client certificate
							 | 
						|
									ClientKey      string `json:"client_key"`      // Path to client private key
							 | 
						|
									RequestTimeout int    `json:"request_timeout"` // Request timeout in seconds (default: 30)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewOpenBaoKMSProvider creates a new OpenBao/Vault KMS provider
							 | 
						|
								func NewOpenBaoKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
							 | 
						|
									if config == nil {
							 | 
						|
										return nil, fmt.Errorf("OpenBao/Vault KMS configuration is required")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Extract configuration
							 | 
						|
									address := config.GetString("address")
							 | 
						|
									if address == "" {
							 | 
						|
										address = "http://localhost:8200" // Default OpenBao address
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									token := config.GetString("token")
							 | 
						|
									roleID := config.GetString("role_id")
							 | 
						|
									secretID := config.GetString("secret_id")
							 | 
						|
									transitPath := config.GetString("transit_path")
							 | 
						|
									if transitPath == "" {
							 | 
						|
										transitPath = "transit" // Default transit path
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									tlsSkipVerify := config.GetBool("tls_skip_verify")
							 | 
						|
									caCert := config.GetString("ca_cert")
							 | 
						|
									clientCert := config.GetString("client_cert")
							 | 
						|
									clientKey := config.GetString("client_key")
							 | 
						|
								
							 | 
						|
									requestTimeout := config.GetInt("request_timeout")
							 | 
						|
									if requestTimeout == 0 {
							 | 
						|
										requestTimeout = 30 // Default 30 seconds
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create Vault client configuration
							 | 
						|
									vaultConfig := vault.DefaultConfig()
							 | 
						|
									vaultConfig.Address = address
							 | 
						|
									vaultConfig.Timeout = time.Duration(requestTimeout) * time.Second
							 | 
						|
								
							 | 
						|
									// Configure TLS
							 | 
						|
									if tlsSkipVerify || caCert != "" || (clientCert != "" && clientKey != "") {
							 | 
						|
										tlsConfig := &vault.TLSConfig{
							 | 
						|
											Insecure: tlsSkipVerify,
							 | 
						|
										}
							 | 
						|
										if caCert != "" {
							 | 
						|
											tlsConfig.CACert = caCert
							 | 
						|
										}
							 | 
						|
										if clientCert != "" && clientKey != "" {
							 | 
						|
											tlsConfig.ClientCert = clientCert
							 | 
						|
											tlsConfig.ClientKey = clientKey
							 | 
						|
										}
							 | 
						|
								
							 | 
						|
										if err := vaultConfig.ConfigureTLS(tlsConfig); err != nil {
							 | 
						|
											return nil, fmt.Errorf("failed to configure TLS: %w", err)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create Vault client
							 | 
						|
									client, err := vault.NewClient(vaultConfig)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to create OpenBao/Vault client: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Authenticate
							 | 
						|
									if token != "" {
							 | 
						|
										client.SetToken(token)
							 | 
						|
										glog.V(1).Infof("OpenBao KMS: Using token authentication")
							 | 
						|
									} else if roleID != "" && secretID != "" {
							 | 
						|
										if err := authenticateAppRole(client, roleID, secretID); err != nil {
							 | 
						|
											return nil, fmt.Errorf("failed to authenticate with AppRole: %w", err)
							 | 
						|
										}
							 | 
						|
										glog.V(1).Infof("OpenBao KMS: Using AppRole authentication")
							 | 
						|
									} else {
							 | 
						|
										return nil, fmt.Errorf("either token or role_id+secret_id must be provided")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									provider := &OpenBaoKMSProvider{
							 | 
						|
										client:      client,
							 | 
						|
										transitPath: transitPath,
							 | 
						|
										address:     address,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(1).Infof("OpenBao/Vault KMS provider initialized at %s", address)
							 | 
						|
									return provider, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// authenticateAppRole authenticates using AppRole method
							 | 
						|
								func authenticateAppRole(client *vault.Client, roleID, secretID string) error {
							 | 
						|
									data := map[string]interface{}{
							 | 
						|
										"role_id":   roleID,
							 | 
						|
										"secret_id": secretID,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									secret, err := client.Logical().Write("auth/approle/login", data)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("AppRole authentication failed: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if secret == nil || secret.Auth == nil {
							 | 
						|
										return fmt.Errorf("AppRole authentication returned empty token")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									client.SetToken(secret.Auth.ClientToken)
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GenerateDataKey generates a new data encryption key using OpenBao/Vault Transit
							 | 
						|
								func (p *OpenBaoKMSProvider) GenerateDataKey(ctx context.Context, req *seaweedkms.GenerateDataKeyRequest) (*seaweedkms.GenerateDataKeyResponse, error) {
							 | 
						|
									if req == nil {
							 | 
						|
										return nil, fmt.Errorf("GenerateDataKeyRequest cannot be nil")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if req.KeyID == "" {
							 | 
						|
										return nil, fmt.Errorf("KeyID is required")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Validate key spec
							 | 
						|
									var keySize int
							 | 
						|
									switch req.KeySpec {
							 | 
						|
									case seaweedkms.KeySpecAES256:
							 | 
						|
										keySize = 32 // 256 bits
							 | 
						|
									default:
							 | 
						|
										return nil, fmt.Errorf("unsupported key spec: %s", req.KeySpec)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Generate data key locally (similar to Azure/GCP approach)
							 | 
						|
									dataKey := make([]byte, keySize)
							 | 
						|
									if _, err := rand.Read(dataKey); err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to generate random data key: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Encrypt the data key using OpenBao/Vault Transit
							 | 
						|
									glog.V(4).Infof("OpenBao KMS: Encrypting data key using key %s", req.KeyID)
							 | 
						|
								
							 | 
						|
									// Prepare encryption data
							 | 
						|
									encryptData := map[string]interface{}{
							 | 
						|
										"plaintext": base64.StdEncoding.EncodeToString(dataKey),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Add encryption context if provided
							 | 
						|
									if len(req.EncryptionContext) > 0 {
							 | 
						|
										contextJSON, err := json.Marshal(req.EncryptionContext)
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
							 | 
						|
										}
							 | 
						|
										encryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Call OpenBao/Vault Transit encrypt endpoint
							 | 
						|
									path := fmt.Sprintf("%s/encrypt/%s", p.transitPath, req.KeyID)
							 | 
						|
									secret, err := p.client.Logical().WriteWithContext(ctx, path, encryptData)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, p.convertVaultError(err, req.KeyID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if secret == nil || secret.Data == nil {
							 | 
						|
										return nil, fmt.Errorf("no data returned from OpenBao/Vault encrypt operation")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									ciphertext, ok := secret.Data["ciphertext"].(string)
							 | 
						|
									if !ok {
							 | 
						|
										return nil, fmt.Errorf("invalid ciphertext format from OpenBao/Vault")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create standardized envelope format for consistent API behavior
							 | 
						|
									envelopeBlob, err := seaweedkms.CreateEnvelope("openbao", req.KeyID, ciphertext, nil)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									response := &seaweedkms.GenerateDataKeyResponse{
							 | 
						|
										KeyID:          req.KeyID,
							 | 
						|
										Plaintext:      dataKey,
							 | 
						|
										CiphertextBlob: envelopeBlob, // Store in standardized envelope format
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(4).Infof("OpenBao KMS: Generated and encrypted data key using key %s", req.KeyID)
							 | 
						|
									return response, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Decrypt decrypts an encrypted data key using OpenBao/Vault Transit
							 | 
						|
								func (p *OpenBaoKMSProvider) Decrypt(ctx context.Context, req *seaweedkms.DecryptRequest) (*seaweedkms.DecryptResponse, error) {
							 | 
						|
									if req == nil {
							 | 
						|
										return nil, fmt.Errorf("DecryptRequest cannot be nil")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if len(req.CiphertextBlob) == 0 {
							 | 
						|
										return nil, fmt.Errorf("CiphertextBlob cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Parse the ciphertext envelope to extract key information
							 | 
						|
									envelope, err := seaweedkms.ParseEnvelope(req.CiphertextBlob)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to parse ciphertext envelope: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									keyID := envelope.KeyID
							 | 
						|
									if keyID == "" {
							 | 
						|
										return nil, fmt.Errorf("envelope missing key ID")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Use the ciphertext from envelope
							 | 
						|
									ciphertext := envelope.Ciphertext
							 | 
						|
								
							 | 
						|
									// Prepare decryption data
							 | 
						|
									decryptData := map[string]interface{}{
							 | 
						|
										"ciphertext": ciphertext,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Add encryption context if provided
							 | 
						|
									if len(req.EncryptionContext) > 0 {
							 | 
						|
										contextJSON, err := json.Marshal(req.EncryptionContext)
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
							 | 
						|
										}
							 | 
						|
										decryptData["context"] = base64.StdEncoding.EncodeToString(contextJSON)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Call OpenBao/Vault Transit decrypt endpoint
							 | 
						|
									path := fmt.Sprintf("%s/decrypt/%s", p.transitPath, keyID)
							 | 
						|
									glog.V(4).Infof("OpenBao KMS: Decrypting data key using key %s", keyID)
							 | 
						|
									secret, err := p.client.Logical().WriteWithContext(ctx, path, decryptData)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, p.convertVaultError(err, keyID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if secret == nil || secret.Data == nil {
							 | 
						|
										return nil, fmt.Errorf("no data returned from OpenBao/Vault decrypt operation")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									plaintextB64, ok := secret.Data["plaintext"].(string)
							 | 
						|
									if !ok {
							 | 
						|
										return nil, fmt.Errorf("invalid plaintext format from OpenBao/Vault")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									plaintext, err := base64.StdEncoding.DecodeString(plaintextB64)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to decode plaintext from OpenBao/Vault: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									response := &seaweedkms.DecryptResponse{
							 | 
						|
										KeyID:     keyID,
							 | 
						|
										Plaintext: plaintext,
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									glog.V(4).Infof("OpenBao KMS: Decrypted data key using key %s", keyID)
							 | 
						|
									return response, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// DescribeKey validates that a key exists and returns its metadata
							 | 
						|
								func (p *OpenBaoKMSProvider) DescribeKey(ctx context.Context, req *seaweedkms.DescribeKeyRequest) (*seaweedkms.DescribeKeyResponse, error) {
							 | 
						|
									if req == nil {
							 | 
						|
										return nil, fmt.Errorf("DescribeKeyRequest cannot be nil")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if req.KeyID == "" {
							 | 
						|
										return nil, fmt.Errorf("KeyID is required")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Get key information from OpenBao/Vault
							 | 
						|
									path := fmt.Sprintf("%s/keys/%s", p.transitPath, req.KeyID)
							 | 
						|
									glog.V(4).Infof("OpenBao KMS: Describing key %s", req.KeyID)
							 | 
						|
									secret, err := p.client.Logical().ReadWithContext(ctx, path)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, p.convertVaultError(err, req.KeyID)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if secret == nil || secret.Data == nil {
							 | 
						|
										return nil, &seaweedkms.KMSError{
							 | 
						|
											Code:    seaweedkms.ErrCodeNotFoundException,
							 | 
						|
											Message: fmt.Sprintf("Key not found: %s", req.KeyID),
							 | 
						|
											KeyID:   req.KeyID,
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									response := &seaweedkms.DescribeKeyResponse{
							 | 
						|
										KeyID:       req.KeyID,
							 | 
						|
										ARN:         fmt.Sprintf("openbao:%s:key:%s", p.address, req.KeyID),
							 | 
						|
										Description: "OpenBao/Vault Transit engine key",
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check key type and set usage
							 | 
						|
									if keyType, ok := secret.Data["type"].(string); ok {
							 | 
						|
										if keyType == "aes256-gcm96" || keyType == "aes128-gcm96" || keyType == "chacha20-poly1305" {
							 | 
						|
											response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
							 | 
						|
										} else {
							 | 
						|
											// Default to data key generation if not an encrypt/decrypt type
							 | 
						|
											response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
							 | 
						|
										}
							 | 
						|
									} else {
							 | 
						|
										// If type is missing, default to data key generation
							 | 
						|
										response.KeyUsage = seaweedkms.KeyUsageGenerateDataKey
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// OpenBao/Vault keys are enabled by default (no disabled state in transit)
							 | 
						|
									response.KeyState = seaweedkms.KeyStateEnabled
							 | 
						|
								
							 | 
						|
									// Keys in OpenBao/Vault transit are service-managed
							 | 
						|
									response.Origin = seaweedkms.KeyOriginOpenBao
							 | 
						|
								
							 | 
						|
									glog.V(4).Infof("OpenBao KMS: Described key %s (state: %s)", req.KeyID, response.KeyState)
							 | 
						|
									return response, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetKeyID resolves a key name (already the full key ID in OpenBao/Vault)
							 | 
						|
								func (p *OpenBaoKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
							 | 
						|
									if keyIdentifier == "" {
							 | 
						|
										return "", fmt.Errorf("key identifier cannot be empty")
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Use DescribeKey to validate the key exists
							 | 
						|
									descReq := &seaweedkms.DescribeKeyRequest{KeyID: keyIdentifier}
							 | 
						|
									descResp, err := p.DescribeKey(ctx, descReq)
							 | 
						|
									if err != nil {
							 | 
						|
										return "", fmt.Errorf("failed to resolve key identifier %s: %w", keyIdentifier, err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return descResp.KeyID, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Close cleans up any resources used by the provider
							 | 
						|
								func (p *OpenBaoKMSProvider) Close() error {
							 | 
						|
									// OpenBao/Vault client doesn't require explicit cleanup
							 | 
						|
									glog.V(2).Infof("OpenBao/Vault KMS provider closed")
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// convertVaultError converts OpenBao/Vault errors to our standard KMS errors
							 | 
						|
								func (p *OpenBaoKMSProvider) convertVaultError(err error, keyID string) error {
							 | 
						|
									errMsg := err.Error()
							 | 
						|
								
							 | 
						|
									if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "no handler") {
							 | 
						|
										return &seaweedkms.KMSError{
							 | 
						|
											Code:    seaweedkms.ErrCodeNotFoundException,
							 | 
						|
											Message: fmt.Sprintf("Key not found in OpenBao/Vault: %v", err),
							 | 
						|
											KeyID:   keyID,
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if strings.Contains(errMsg, "permission") || strings.Contains(errMsg, "denied") || strings.Contains(errMsg, "forbidden") {
							 | 
						|
										return &seaweedkms.KMSError{
							 | 
						|
											Code:    seaweedkms.ErrCodeAccessDenied,
							 | 
						|
											Message: fmt.Sprintf("Access denied to OpenBao/Vault: %v", err),
							 | 
						|
											KeyID:   keyID,
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if strings.Contains(errMsg, "disabled") || strings.Contains(errMsg, "unavailable") {
							 | 
						|
										return &seaweedkms.KMSError{
							 | 
						|
											Code:    seaweedkms.ErrCodeKeyUnavailable,
							 | 
						|
											Message: fmt.Sprintf("Key unavailable in OpenBao/Vault: %v", err),
							 | 
						|
											KeyID:   keyID,
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// For unknown errors, wrap as internal failure
							 | 
						|
									return &seaweedkms.KMSError{
							 | 
						|
										Code:    seaweedkms.ErrCodeKMSInternalFailure,
							 | 
						|
										Message: fmt.Sprintf("OpenBao/Vault error: %v", err),
							 | 
						|
										KeyID:   keyID,
							 | 
						|
									}
							 | 
						|
								}
							 |