azure_kms.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. //go:build azurekms
  2. package azure
  3. import (
  4. "context"
  5. "crypto/rand"
  6. "encoding/json"
  7. "fmt"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "github.com/Azure/azure-sdk-for-go/sdk/azcore"
  12. "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
  13. "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
  14. "github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys"
  15. "github.com/seaweedfs/seaweedfs/weed/glog"
  16. seaweedkms "github.com/seaweedfs/seaweedfs/weed/kms"
  17. "github.com/seaweedfs/seaweedfs/weed/util"
  18. )
  19. func init() {
  20. // Register the Azure Key Vault provider
  21. seaweedkms.RegisterProvider("azure", NewAzureKMSProvider)
  22. }
  23. // AzureKMSProvider implements the KMSProvider interface using Azure Key Vault
  24. type AzureKMSProvider struct {
  25. client *azkeys.Client
  26. vaultURL string
  27. tenantID string
  28. clientID string
  29. clientSecret string
  30. }
  31. // AzureKMSConfig contains configuration for the Azure Key Vault provider
  32. type AzureKMSConfig struct {
  33. VaultURL string `json:"vault_url"` // Azure Key Vault URL (e.g., "https://myvault.vault.azure.net/")
  34. TenantID string `json:"tenant_id"` // Azure AD tenant ID
  35. ClientID string `json:"client_id"` // Service principal client ID
  36. ClientSecret string `json:"client_secret"` // Service principal client secret
  37. Certificate string `json:"certificate"` // Certificate path for cert-based auth (alternative to client secret)
  38. UseDefaultCreds bool `json:"use_default_creds"` // Use default Azure credentials (managed identity)
  39. RequestTimeout int `json:"request_timeout"` // Request timeout in seconds (default: 30)
  40. }
  41. // NewAzureKMSProvider creates a new Azure Key Vault provider
  42. func NewAzureKMSProvider(config util.Configuration) (seaweedkms.KMSProvider, error) {
  43. if config == nil {
  44. return nil, fmt.Errorf("Azure Key Vault configuration is required")
  45. }
  46. // Extract configuration
  47. vaultURL := config.GetString("vault_url")
  48. if vaultURL == "" {
  49. return nil, fmt.Errorf("vault_url is required for Azure Key Vault provider")
  50. }
  51. tenantID := config.GetString("tenant_id")
  52. clientID := config.GetString("client_id")
  53. clientSecret := config.GetString("client_secret")
  54. useDefaultCreds := config.GetBool("use_default_creds")
  55. requestTimeout := config.GetInt("request_timeout")
  56. if requestTimeout == 0 {
  57. requestTimeout = 30 // Default 30 seconds
  58. }
  59. // Create credential based on configuration
  60. var credential azcore.TokenCredential
  61. var err error
  62. if useDefaultCreds {
  63. // Use default Azure credentials (managed identity, Azure CLI, etc.)
  64. credential, err = azidentity.NewDefaultAzureCredential(nil)
  65. if err != nil {
  66. return nil, fmt.Errorf("failed to create default Azure credentials: %w", err)
  67. }
  68. glog.V(1).Infof("Azure KMS: Using default Azure credentials")
  69. } else if clientID != "" && clientSecret != "" {
  70. // Use service principal credentials
  71. if tenantID == "" {
  72. return nil, fmt.Errorf("tenant_id is required when using client credentials")
  73. }
  74. credential, err = azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, nil)
  75. if err != nil {
  76. return nil, fmt.Errorf("failed to create Azure client secret credential: %w", err)
  77. }
  78. glog.V(1).Infof("Azure KMS: Using client secret credentials for client ID %s", clientID)
  79. } else {
  80. return nil, fmt.Errorf("either use_default_creds=true or client_id+client_secret must be provided")
  81. }
  82. // Create Key Vault client
  83. clientOptions := &azkeys.ClientOptions{
  84. ClientOptions: azcore.ClientOptions{
  85. PerCallPolicies: []policy.Policy{},
  86. Transport: &http.Client{
  87. Timeout: time.Duration(requestTimeout) * time.Second,
  88. },
  89. },
  90. }
  91. client, err := azkeys.NewClient(vaultURL, credential, clientOptions)
  92. if err != nil {
  93. return nil, fmt.Errorf("failed to create Azure Key Vault client: %w", err)
  94. }
  95. provider := &AzureKMSProvider{
  96. client: client,
  97. vaultURL: vaultURL,
  98. tenantID: tenantID,
  99. clientID: clientID,
  100. clientSecret: clientSecret,
  101. }
  102. glog.V(1).Infof("Azure Key Vault provider initialized for vault %s", vaultURL)
  103. return provider, nil
  104. }
  105. // GenerateDataKey generates a new data encryption key using Azure Key Vault
  106. func (p *AzureKMSProvider) GenerateDataKey(ctx context.Context, req *seaweedkms.GenerateDataKeyRequest) (*seaweedkms.GenerateDataKeyResponse, error) {
  107. if req == nil {
  108. return nil, fmt.Errorf("GenerateDataKeyRequest cannot be nil")
  109. }
  110. if req.KeyID == "" {
  111. return nil, fmt.Errorf("KeyID is required")
  112. }
  113. // Validate key spec
  114. var keySize int
  115. switch req.KeySpec {
  116. case seaweedkms.KeySpecAES256:
  117. keySize = 32 // 256 bits
  118. default:
  119. return nil, fmt.Errorf("unsupported key spec: %s", req.KeySpec)
  120. }
  121. // Generate data key locally (Azure Key Vault doesn't have GenerateDataKey like AWS)
  122. dataKey := make([]byte, keySize)
  123. if _, err := rand.Read(dataKey); err != nil {
  124. return nil, fmt.Errorf("failed to generate random data key: %w", err)
  125. }
  126. // Encrypt the data key using Azure Key Vault
  127. glog.V(4).Infof("Azure KMS: Encrypting data key using key %s", req.KeyID)
  128. // Prepare encryption parameters
  129. algorithm := azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256
  130. encryptParams := azkeys.KeyOperationsParameters{
  131. Algorithm: &algorithm, // Default encryption algorithm
  132. Value: dataKey,
  133. }
  134. // Add encryption context as Additional Authenticated Data (AAD) if provided
  135. if len(req.EncryptionContext) > 0 {
  136. // Marshal encryption context to JSON for deterministic AAD
  137. aadBytes, err := json.Marshal(req.EncryptionContext)
  138. if err != nil {
  139. return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
  140. }
  141. encryptParams.AAD = aadBytes
  142. glog.V(4).Infof("Azure KMS: Using encryption context as AAD for key %s", req.KeyID)
  143. }
  144. // Call Azure Key Vault to encrypt the data key
  145. encryptResult, err := p.client.Encrypt(ctx, req.KeyID, "", encryptParams, nil)
  146. if err != nil {
  147. return nil, p.convertAzureError(err, req.KeyID)
  148. }
  149. // Get the actual key ID from the response
  150. actualKeyID := req.KeyID
  151. if encryptResult.KID != nil {
  152. actualKeyID = string(*encryptResult.KID)
  153. }
  154. // Create standardized envelope format for consistent API behavior
  155. envelopeBlob, err := seaweedkms.CreateEnvelope("azure", actualKeyID, string(encryptResult.Result), nil)
  156. if err != nil {
  157. return nil, fmt.Errorf("failed to create ciphertext envelope: %w", err)
  158. }
  159. response := &seaweedkms.GenerateDataKeyResponse{
  160. KeyID: actualKeyID,
  161. Plaintext: dataKey,
  162. CiphertextBlob: envelopeBlob, // Store in standardized envelope format
  163. }
  164. glog.V(4).Infof("Azure KMS: Generated and encrypted data key using key %s", actualKeyID)
  165. return response, nil
  166. }
  167. // Decrypt decrypts an encrypted data key using Azure Key Vault
  168. func (p *AzureKMSProvider) Decrypt(ctx context.Context, req *seaweedkms.DecryptRequest) (*seaweedkms.DecryptResponse, error) {
  169. if req == nil {
  170. return nil, fmt.Errorf("DecryptRequest cannot be nil")
  171. }
  172. if len(req.CiphertextBlob) == 0 {
  173. return nil, fmt.Errorf("CiphertextBlob cannot be empty")
  174. }
  175. // Parse the ciphertext envelope to extract key information
  176. envelope, err := seaweedkms.ParseEnvelope(req.CiphertextBlob)
  177. if err != nil {
  178. return nil, fmt.Errorf("failed to parse ciphertext envelope: %w", err)
  179. }
  180. keyID := envelope.KeyID
  181. if keyID == "" {
  182. return nil, fmt.Errorf("envelope missing key ID")
  183. }
  184. // Convert string back to bytes
  185. ciphertext := []byte(envelope.Ciphertext)
  186. // Prepare decryption parameters
  187. decryptAlgorithm := azkeys.JSONWebKeyEncryptionAlgorithmRSAOAEP256
  188. decryptParams := azkeys.KeyOperationsParameters{
  189. Algorithm: &decryptAlgorithm, // Must match encryption algorithm
  190. Value: ciphertext,
  191. }
  192. // Add encryption context as Additional Authenticated Data (AAD) if provided
  193. if len(req.EncryptionContext) > 0 {
  194. // Marshal encryption context to JSON for deterministic AAD (must match encryption)
  195. aadBytes, err := json.Marshal(req.EncryptionContext)
  196. if err != nil {
  197. return nil, fmt.Errorf("failed to marshal encryption context: %w", err)
  198. }
  199. decryptParams.AAD = aadBytes
  200. glog.V(4).Infof("Azure KMS: Using encryption context as AAD for decryption of key %s", keyID)
  201. }
  202. // Call Azure Key Vault to decrypt the data key
  203. glog.V(4).Infof("Azure KMS: Decrypting data key using key %s", keyID)
  204. decryptResult, err := p.client.Decrypt(ctx, keyID, "", decryptParams, nil)
  205. if err != nil {
  206. return nil, p.convertAzureError(err, keyID)
  207. }
  208. // Get the actual key ID from the response
  209. actualKeyID := keyID
  210. if decryptResult.KID != nil {
  211. actualKeyID = string(*decryptResult.KID)
  212. }
  213. response := &seaweedkms.DecryptResponse{
  214. KeyID: actualKeyID,
  215. Plaintext: decryptResult.Result,
  216. }
  217. glog.V(4).Infof("Azure KMS: Decrypted data key using key %s", actualKeyID)
  218. return response, nil
  219. }
  220. // DescribeKey validates that a key exists and returns its metadata
  221. func (p *AzureKMSProvider) DescribeKey(ctx context.Context, req *seaweedkms.DescribeKeyRequest) (*seaweedkms.DescribeKeyResponse, error) {
  222. if req == nil {
  223. return nil, fmt.Errorf("DescribeKeyRequest cannot be nil")
  224. }
  225. if req.KeyID == "" {
  226. return nil, fmt.Errorf("KeyID is required")
  227. }
  228. // Get key from Azure Key Vault
  229. glog.V(4).Infof("Azure KMS: Describing key %s", req.KeyID)
  230. result, err := p.client.GetKey(ctx, req.KeyID, "", nil)
  231. if err != nil {
  232. return nil, p.convertAzureError(err, req.KeyID)
  233. }
  234. if result.Key == nil {
  235. return nil, fmt.Errorf("no key returned from Azure Key Vault")
  236. }
  237. key := result.Key
  238. response := &seaweedkms.DescribeKeyResponse{
  239. KeyID: req.KeyID,
  240. Description: "Azure Key Vault key", // Azure doesn't provide description in the same way
  241. }
  242. // Set ARN-like identifier for Azure
  243. if key.KID != nil {
  244. response.ARN = string(*key.KID)
  245. response.KeyID = string(*key.KID)
  246. }
  247. // Set key usage based on key operations
  248. if key.KeyOps != nil && len(key.KeyOps) > 0 {
  249. // Azure keys can have multiple operations, check if encrypt/decrypt are supported
  250. for _, op := range key.KeyOps {
  251. if op != nil && (*op == string(azkeys.JSONWebKeyOperationEncrypt) || *op == string(azkeys.JSONWebKeyOperationDecrypt)) {
  252. response.KeyUsage = seaweedkms.KeyUsageEncryptDecrypt
  253. break
  254. }
  255. }
  256. }
  257. // Set key state based on enabled status
  258. if result.Attributes != nil {
  259. if result.Attributes.Enabled != nil && *result.Attributes.Enabled {
  260. response.KeyState = seaweedkms.KeyStateEnabled
  261. } else {
  262. response.KeyState = seaweedkms.KeyStateDisabled
  263. }
  264. }
  265. // Azure Key Vault keys are managed by Azure
  266. response.Origin = seaweedkms.KeyOriginAzure
  267. glog.V(4).Infof("Azure KMS: Described key %s (state: %s)", req.KeyID, response.KeyState)
  268. return response, nil
  269. }
  270. // GetKeyID resolves a key name to the full key identifier
  271. func (p *AzureKMSProvider) GetKeyID(ctx context.Context, keyIdentifier string) (string, error) {
  272. if keyIdentifier == "" {
  273. return "", fmt.Errorf("key identifier cannot be empty")
  274. }
  275. // Use DescribeKey to resolve and validate the key identifier
  276. descReq := &seaweedkms.DescribeKeyRequest{KeyID: keyIdentifier}
  277. descResp, err := p.DescribeKey(ctx, descReq)
  278. if err != nil {
  279. return "", fmt.Errorf("failed to resolve key identifier %s: %w", keyIdentifier, err)
  280. }
  281. return descResp.KeyID, nil
  282. }
  283. // Close cleans up any resources used by the provider
  284. func (p *AzureKMSProvider) Close() error {
  285. // Azure SDK clients don't require explicit cleanup
  286. glog.V(2).Infof("Azure Key Vault provider closed")
  287. return nil
  288. }
  289. // convertAzureError converts Azure Key Vault errors to our standard KMS errors
  290. func (p *AzureKMSProvider) convertAzureError(err error, keyID string) error {
  291. // Azure SDK uses different error types, need to check for specific conditions
  292. errMsg := err.Error()
  293. if strings.Contains(errMsg, "not found") || strings.Contains(errMsg, "NotFound") {
  294. return &seaweedkms.KMSError{
  295. Code: seaweedkms.ErrCodeNotFoundException,
  296. Message: fmt.Sprintf("Key not found in Azure Key Vault: %v", err),
  297. KeyID: keyID,
  298. }
  299. }
  300. if strings.Contains(errMsg, "access") || strings.Contains(errMsg, "Forbidden") || strings.Contains(errMsg, "Unauthorized") {
  301. return &seaweedkms.KMSError{
  302. Code: seaweedkms.ErrCodeAccessDenied,
  303. Message: fmt.Sprintf("Access denied to Azure Key Vault: %v", err),
  304. KeyID: keyID,
  305. }
  306. }
  307. if strings.Contains(errMsg, "disabled") || strings.Contains(errMsg, "unavailable") {
  308. return &seaweedkms.KMSError{
  309. Code: seaweedkms.ErrCodeKeyUnavailable,
  310. Message: fmt.Sprintf("Key unavailable in Azure Key Vault: %v", err),
  311. KeyID: keyID,
  312. }
  313. }
  314. // For unknown errors, wrap as internal failure
  315. return &seaweedkms.KMSError{
  316. Code: seaweedkms.ErrCodeKMSInternalFailure,
  317. Message: fmt.Sprintf("Azure Key Vault error: %v", err),
  318. KeyID: keyID,
  319. }
  320. }