|
32 | 32 | #include "arrow/util/logging.h" |
33 | 33 |
|
34 | 34 | #include "parquet/encryption/crypto_factory.h" |
| 35 | +#include "parquet/encryption/file_system_key_material_store.h" |
| 36 | +#include "parquet/encryption/key_material.h" |
35 | 37 | #include "parquet/encryption/key_toolkit.h" |
36 | 38 | #include "parquet/encryption/test_encryption_util.h" |
37 | 39 | #include "parquet/encryption/test_in_memory_kms.h" |
@@ -194,6 +196,72 @@ class TestEncryptionKeyManagement : public ::testing::Test { |
194 | 196 | crypto_factory_.RemoveCacheEntriesForAllTokens(); |
195 | 197 | } |
196 | 198 |
|
| 199 | + // Write a file that uses external key material and records the KMS |
| 200 | + // instance ID and instance URL in its key material. |
| 201 | + std::string WriteExternalMaterialFileWithKmsConfig(const std::string& instance_id, |
| 202 | + const std::string& instance_url) { |
| 203 | + kms_connection_config_.kms_instance_id = instance_id; |
| 204 | + kms_connection_config_.kms_instance_url = instance_url; |
| 205 | + TestOnlyInServerWrapKms::InitializeMasterKeys(key_list_); |
| 206 | + constexpr bool double_wrapping = true; |
| 207 | + constexpr int encryption_no = 0; |
| 208 | + this->WriteEncryptedParquetFile(double_wrapping, /*internal_key_material=*/false, |
| 209 | + encryption_no); |
| 210 | + return temp_dir_->path().ToString() + GetFileName(double_wrapping, wrap_locally_, |
| 211 | + /*internal_key_material=*/false, |
| 212 | + encryption_no); |
| 213 | + } |
| 214 | + |
| 215 | + // Rotate the keys of a file written with the KMS ID and URL configured, |
| 216 | + // and return the KMS connection configurations used to create clients |
| 217 | + // during key rotation. |
| 218 | + std::vector<KmsConnectionConfig> RotateKeysWithKmsConfig( |
| 219 | + const KmsConnectionConfig& rotation_config, const bool read_kms_url) { |
| 220 | + const auto file_system = std::make_shared<::arrow::fs::LocalFileSystem>(); |
| 221 | + this->SetupCryptoFactory(false); |
| 222 | + |
| 223 | + const std::string file_path = |
| 224 | + this->WriteExternalMaterialFileWithKmsConfig("123", "https://example.com/kms"); |
| 225 | + |
| 226 | + auto kms_client_factory = std::make_shared<TestOnlyInMemoryKmsClientFactory>( |
| 227 | + /*wrap_locally=*/false, key_list_); |
| 228 | + auto crypto_factory = std::make_shared<CryptoFactory>(); |
| 229 | + crypto_factory->RegisterKmsClientFactory(kms_client_factory); |
| 230 | + |
| 231 | + TestOnlyInServerWrapKms::StartKeyRotation(new_key_list_); |
| 232 | + crypto_factory->RotateMasterKeys(rotation_config, file_path, file_system, |
| 233 | + /*double_wrapping=*/true, |
| 234 | + kDefaultCacheLifetimeSeconds, read_kms_url); |
| 235 | + TestOnlyInServerWrapKms::FinishKeyRotation(); |
| 236 | + |
| 237 | + std::vector<KmsConnectionConfig> creation_requests = |
| 238 | + kms_client_factory->CreationRequests(); |
| 239 | + |
| 240 | + // The new key material always uses the KMS connection configuration provided, |
| 241 | + // not the config from the previous key material. |
| 242 | + // If it's empty, default values are written. |
| 243 | + const auto key_material_store = |
| 244 | + FileSystemKeyMaterialStore::Make(file_path, file_system, |
| 245 | + /*use_tmp_prefix=*/false); |
| 246 | + const KeyMaterial rotated_key_material = KeyMaterial::Parse( |
| 247 | + key_material_store->GetKeyMaterial(std::string(KeyMaterial::kFooterKeyIdInFile))); |
| 248 | + const auto& expected_id = rotation_config.kms_instance_id.empty() |
| 249 | + ? KmsClient::kKmsInstanceIdDefault |
| 250 | + : rotation_config.kms_instance_id; |
| 251 | + const auto& expected_url = rotation_config.kms_instance_url.empty() |
| 252 | + ? KmsClient::kKmsInstanceUrlDefault |
| 253 | + : rotation_config.kms_instance_url; |
| 254 | + EXPECT_EQ(rotated_key_material.kms_instance_id(), expected_id); |
| 255 | + EXPECT_EQ(rotated_key_material.kms_instance_url(), expected_url); |
| 256 | + |
| 257 | + // Check the rotated file is readable |
| 258 | + const auto file_decryption_properties = crypto_factory->GetFileDecryptionProperties( |
| 259 | + rotation_config, GetDecryptionConfiguration(), file_path, file_system); |
| 260 | + decryptor_.DecryptFile(file_path, file_decryption_properties); |
| 261 | + |
| 262 | + return creation_requests; |
| 263 | + } |
| 264 | + |
197 | 265 | // Create encryption properties without keeping the creating CryptoFactory alive |
198 | 266 | std::shared_ptr<FileEncryptionProperties> GetOrphanedFileEncryptionProperties( |
199 | 267 | std::shared_ptr<KmsClientFactory> kms_client_factory, |
@@ -441,4 +509,100 @@ TEST_F(TestEncryptionKeyManagement, ReadParquetMRExternalKeyMaterialFile) { |
441 | 509 | } |
442 | 510 | } |
443 | 511 |
|
| 512 | +TEST_F(TestEncryptionKeyManagement, ReadKmsUrlFromFile) { |
| 513 | + this->SetupCryptoFactory(true); |
| 514 | + |
| 515 | + constexpr bool internal_key_material = true; |
| 516 | + constexpr bool double_wrapping = true; |
| 517 | + constexpr int encryption_no = 0; |
| 518 | + |
| 519 | + std::string file_name = "kms-config-test-file.parquet.encrypted"; |
| 520 | + std::string file_path = temp_dir_->path().ToString() + file_name; |
| 521 | + |
| 522 | + auto encryption_config = |
| 523 | + GetEncryptionConfiguration(double_wrapping, internal_key_material, encryption_no); |
| 524 | + |
| 525 | + KmsConnectionConfig write_config; |
| 526 | + write_config.kms_instance_id = "123"; |
| 527 | + write_config.kms_instance_url = "https://example.com/kms"; |
| 528 | + |
| 529 | + auto file_encryption_properties = |
| 530 | + crypto_factory_.GetFileEncryptionProperties(write_config, encryption_config); |
| 531 | + encryptor_.EncryptFile(file_path, file_encryption_properties); |
| 532 | + |
| 533 | + for (const auto& enable_kms_url_read : {false, true}) { |
| 534 | + // Create a fresh crypto factory and client factory for each read |
| 535 | + // to avoid re-using cached clients. |
| 536 | + CryptoFactory read_crypto_factory; |
| 537 | + auto kms_client_factory = |
| 538 | + std::make_shared<TestOnlyInMemoryKmsClientFactory>(true, key_list_); |
| 539 | + read_crypto_factory.RegisterKmsClientFactory(kms_client_factory); |
| 540 | + |
| 541 | + auto decryption_config = DecryptionConfiguration(); |
| 542 | + decryption_config.read_kms_url = enable_kms_url_read; |
| 543 | + |
| 544 | + KmsConnectionConfig read_config; |
| 545 | + |
| 546 | + auto file_decryption_properties = |
| 547 | + read_crypto_factory.GetFileDecryptionProperties(read_config, decryption_config); |
| 548 | + |
| 549 | + decryptor_.DecryptFile(file_path, file_decryption_properties); |
| 550 | + |
| 551 | + ASSERT_EQ(kms_client_factory->CreationRequests().size(), 1); |
| 552 | + const auto& request = kms_client_factory->CreationRequests()[0]; |
| 553 | + EXPECT_EQ(request.kms_instance_id, "123"); |
| 554 | + if (enable_kms_url_read) { |
| 555 | + EXPECT_EQ(request.kms_instance_url, "https://example.com/kms"); |
| 556 | + } else { |
| 557 | + EXPECT_EQ(request.kms_instance_url, "DEFAULT"); |
| 558 | + } |
| 559 | + } |
| 560 | +} |
| 561 | + |
| 562 | +TEST_F(TestEncryptionKeyManagement, ReadKmsUrlFromFileDuringKeyRotation) { |
| 563 | + // Use an empty config for rotation |
| 564 | + const KmsConnectionConfig rotation_config; |
| 565 | + const auto requests = RotateKeysWithKmsConfig(rotation_config, /*read_kms_url=*/true); |
| 566 | + |
| 567 | + ASSERT_EQ(requests.size(), 2); |
| 568 | + // The first KMS creation request is for wrapping new keys. |
| 569 | + // This uses the empty config provided. |
| 570 | + EXPECT_EQ(requests[0].kms_instance_id, ""); |
| 571 | + EXPECT_EQ(requests[0].kms_instance_url, ""); |
| 572 | + // The KMS client used to unwrap the previous keys should be configured |
| 573 | + // with the instance ID and url provided at write time. |
| 574 | + EXPECT_EQ(requests[1].kms_instance_id, "123"); |
| 575 | + EXPECT_EQ(requests[1].kms_instance_url, "https://example.com/kms"); |
| 576 | +} |
| 577 | + |
| 578 | +TEST_F(TestEncryptionKeyManagement, KeyRotationWithoutReadingKmsUrl) { |
| 579 | + // Use an empty config for rotation |
| 580 | + const KmsConnectionConfig rotation_config; |
| 581 | + const auto requests = RotateKeysWithKmsConfig(rotation_config, /*read_kms_url=*/false); |
| 582 | + |
| 583 | + ASSERT_EQ(requests.size(), 2); |
| 584 | + // The first KMS creation request is for wrapping new keys. |
| 585 | + // This uses the empty config provided. |
| 586 | + EXPECT_EQ(requests[0].kms_instance_id, ""); |
| 587 | + EXPECT_EQ(requests[0].kms_instance_url, ""); |
| 588 | + // When unwrapping the existing keys, the URL in the key material is |
| 589 | + // ignored and the default used. |
| 590 | + EXPECT_EQ(requests[1].kms_instance_id, "123"); |
| 591 | + EXPECT_EQ(requests[1].kms_instance_url, KmsClient::kKmsInstanceUrlDefault); |
| 592 | +} |
| 593 | + |
| 594 | +TEST_F(TestEncryptionKeyManagement, KeyRotationUsesProvidedKmsConfig) { |
| 595 | + KmsConnectionConfig rotation_config; |
| 596 | + rotation_config.kms_instance_id = "456"; |
| 597 | + rotation_config.kms_instance_url = "https://example.com/kms2"; |
| 598 | + const auto requests = RotateKeysWithKmsConfig(rotation_config, /*read_kms_url=*/true); |
| 599 | + |
| 600 | + ASSERT_EQ(requests.size(), 1); |
| 601 | + // Wrap and unwrap both use the same configuration. |
| 602 | + // The instance id and url in the existing key material is ignored even though |
| 603 | + // read_kms_url is enabled. The provided config takes precedence. |
| 604 | + EXPECT_EQ(requests[0].kms_instance_id, "456"); |
| 605 | + EXPECT_EQ(requests[0].kms_instance_url, "https://example.com/kms2"); |
| 606 | +} |
| 607 | + |
444 | 608 | } // namespace parquet::encryption::test |
0 commit comments