Skip to content

Commit 5d48a1e

Browse files
authored
PHPLIB-820: Key Management API spec tests (#957)
* Unified spec and prose tests for CSFLE key management API Support runOnRequirement.csfle, clientEncryption entity/operations, and $$placeholder syntax Synced with mongodb/specifications@10b4a41 * Note ClientEncryption's parent client to ensure events are observed * Set ClientEncryption tlsOptions when KMIP endpoint is used * PHPLIB-929: Prose test for rewrapManyDataKey * Do not consider SkippedTest exceptions as errors for valid-fail tests This is particularly relevant for kmsProviders tests, which may need to be skipped according to runOnRequirements.csfle
1 parent ece69fb commit 5d48a1e

34 files changed

+6609
-28
lines changed

.evergreen/config.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -621,22 +621,22 @@ axes:
621621
- id: driver-versions
622622
display_name: Driver Version
623623
values:
624-
# TODO: Update to "1.14.0" once PHPC 1.14.0 is released
624+
# TODO: Update to "1.15.0" once PHPC 1.15.0 is released
625625
- id: "oldest-supported"
626-
# display_name: "1.14.0"
627-
display_name: "PHPC 1.14-dev (master)"
626+
# display_name: "1.15.0"
627+
display_name: "PHPC 1.15-dev (master)"
628628
variables:
629-
# EXTENSION_VERSION: "1.14.0"
629+
# EXTENSION_VERSION: "1.15.0"
630630
EXTENSION_BRANCH: "master"
631-
# TODO: Update to "1.14.x"/"stable" once PHPC 1.14.0 is released
631+
# TODO: Update to "1.15.x"/"stable" once PHPC 1.15.0 is released
632632
- id: "latest-stable"
633-
# display_name: "1.14.x"
634-
display_name: "PHPC 1.14-dev (master)"
633+
# display_name: "1.15.x"
634+
display_name: "PHPC 1.15-dev (master)"
635635
variables:
636636
# EXTENSION_VERSION: "stable"
637637
EXTENSION_BRANCH: "master"
638638
- id: "latest-dev"
639-
display_name: "PHPC 1.14-dev (master)"
639+
display_name: "PHPC 1.15-dev (master)"
640640
variables:
641641
EXTENSION_BRANCH: "master"
642642

tests/FunctionalTestCase.php

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,21 @@ public function configureFailPoint($command, ?Server $server = null): void
284284
$this->configuredFailPoints[] = [$command->configureFailPoint, $failPointServer];
285285
}
286286

287+
public static function getModuleInfo(string $row): ?string
288+
{
289+
ob_start();
290+
phpinfo(INFO_MODULES);
291+
$info = ob_get_clean();
292+
293+
$pattern = sprintf('/^%s([\w ]+)$/m', preg_quote($row . ' => '));
294+
295+
if (preg_match($pattern, $info, $matches) !== 1) {
296+
return null;
297+
}
298+
299+
return $matches[1];
300+
}
301+
287302
/**
288303
* Creates the test collection with the specified options.
289304
*
@@ -512,7 +527,7 @@ protected function skipIfClientSideEncryptionIsNotSupported(): void
512527
$this->markTestSkipped('Client Side Encryption only supported on FCV 4.2 or higher');
513528
}
514529

515-
if ($this->getModuleInfo('libmongocrypt') === 'disabled') {
530+
if (static::getModuleInfo('libmongocrypt') === 'disabled') {
516531
$this->markTestSkipped('Client Side Encryption is not enabled in the MongoDB extension');
517532
}
518533
}
@@ -598,21 +613,6 @@ private function disableFailPoints(): void
598613
}
599614
}
600615

601-
private function getModuleInfo(string $row): ?string
602-
{
603-
ob_start();
604-
phpinfo(INFO_MODULES);
605-
$info = ob_get_clean();
606-
607-
$pattern = sprintf('/^%s([\w ]+)$/m', preg_quote($row . ' => '));
608-
609-
if (preg_match($pattern, $info, $matches) !== 1) {
610-
return null;
611-
}
612-
613-
return $matches[1];
614-
}
615-
616616
/**
617617
* Checks if the failCommand command is supported on this server version
618618
*

tests/SpecTests/ClientSideEncryptionSpecTest.php

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use MongoDB\Driver\Exception\ConnectionTimeoutException;
1616
use MongoDB\Driver\Exception\EncryptionException;
1717
use MongoDB\Driver\Exception\RuntimeException;
18+
use MongoDB\Driver\Exception\ServerException;
1819
use MongoDB\Driver\Monitoring\CommandFailedEvent;
1920
use MongoDB\Driver\Monitoring\CommandStartedEvent;
2021
use MongoDB\Driver\Monitoring\CommandSubscriber;
@@ -1403,6 +1404,93 @@ static function (self $test, ClientEncryption $clientEncryption, Client $encrypt
14031404
];
14041405
}
14051406

1407+
/**
1408+
* Prose test 13: Unique Index on keyAltNames
1409+
*
1410+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.rst#unique-index-on-keyaltnames
1411+
* @dataProvider provideUniqueIndexOnKeyAltNamesTests
1412+
*/
1413+
public function testUniqueIndexOnKeyAltNames(Closure $test): void
1414+
{
1415+
// Test setup
1416+
$client = static::createTestClient();
1417+
1418+
// Ensure that the key vault is dropped with a majority write concern
1419+
self::insertKeyVaultData($client, []);
1420+
1421+
$client->selectCollection('keyvault', 'datakeys')->createIndex(
1422+
['keyAltNames' => 1],
1423+
[
1424+
'unique' => true,
1425+
'partialFilterExpression' => ['keyAltNames' => ['$exists' => true]],
1426+
'writeConcern' => new WriteConcern(WriteConcern::MAJORITY),
1427+
]
1428+
);
1429+
1430+
$clientEncryption = new ClientEncryption([
1431+
'keyVaultClient' => $client->getManager(),
1432+
'keyVaultNamespace' => 'keyvault.datakeys',
1433+
'kmsProviders' => ['local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY), 0)]],
1434+
]);
1435+
1436+
$clientEncryption->createDataKey('local', ['keyAltNames' => ['def']]);
1437+
1438+
$test($this, $client, $clientEncryption);
1439+
}
1440+
1441+
public static function provideUniqueIndexOnKeyAltNamesTests()
1442+
{
1443+
// See: https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.rst#case-1-createdatakey
1444+
yield 'Case 1: createDataKey()' => [
1445+
static function (self $test, Client $client, ClientEncryption $clientEncryption): void {
1446+
$clientEncryption->createDataKey('local', ['keyAltNames' => ['abc']]);
1447+
1448+
try {
1449+
$clientEncryption->createDataKey('local', ['keyAltNames' => ['abc']]);
1450+
$test->fail('Expected exception to be thrown');
1451+
} catch (ServerException $e) {
1452+
$test->assertSame(11000 /* DuplicateKey */, $e->getCode());
1453+
}
1454+
1455+
try {
1456+
$clientEncryption->createDataKey('local', ['keyAltNames' => ['def']]);
1457+
$test->fail('Expected exception to be thrown');
1458+
} catch (ServerException $e) {
1459+
$test->assertSame(11000 /* DuplicateKey */, $e->getCode());
1460+
}
1461+
},
1462+
];
1463+
1464+
// See: https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.rst#case-2-addkeyaltname
1465+
yield 'Case 2: addKeyAltName()' => [
1466+
static function (self $test, Client $client, ClientEncryption $clientEncryption): void {
1467+
$keyId = $clientEncryption->createDataKey('local');
1468+
1469+
$keyBeforeUpdate = $clientEncryption->addKeyAltName($keyId, 'abc');
1470+
$test->assertObjectNotHasAttribute('keyAltNames', $keyBeforeUpdate);
1471+
1472+
$keyBeforeUpdate = $clientEncryption->addKeyAltName($keyId, 'abc');
1473+
$test->assertObjectHasAttribute('keyAltNames', $keyBeforeUpdate);
1474+
$test->assertIsArray($keyBeforeUpdate->keyAltNames);
1475+
$test->assertContains('abc', $keyBeforeUpdate->keyAltNames);
1476+
1477+
try {
1478+
$clientEncryption->addKeyAltName($keyId, 'def');
1479+
$test->fail('Expected exception to be thrown');
1480+
} catch (ServerException $e) {
1481+
$test->assertSame(11000 /* DuplicateKey */, $e->getCode());
1482+
}
1483+
1484+
$originalKeyId = $clientEncryption->getKeyByAltName('def')->_id;
1485+
1486+
$originalKeyBeforeUpdate = $clientEncryption->addKeyAltName($originalKeyId, 'def');
1487+
$test->assertObjectHasAttribute('keyAltNames', $originalKeyBeforeUpdate);
1488+
$test->assertIsArray($originalKeyBeforeUpdate->keyAltNames);
1489+
$test->assertContains('def', $originalKeyBeforeUpdate->keyAltNames);
1490+
},
1491+
];
1492+
}
1493+
14061494
/**
14071495
* Prose test 14: Decryption Events
14081496
*
@@ -1552,6 +1640,84 @@ static function (self $test, Client $setupClient, ClientEncryption $clientEncryp
15521640
];
15531641
}
15541642

1643+
/**
1644+
* Prose test 16: RewrapManyDataKey
1645+
*
1646+
* @see https://github.com/mongodb/specifications/blob/master/source/client-side-encryption/tests/README.rst#rewrap
1647+
* @dataProvider provideRewrapManyDataKeySrcAndDstProviders
1648+
*/
1649+
public function testRewrapManyDataKey(string $srcProvider, string $dstProvider): void
1650+
{
1651+
$providerMasterKeys = [
1652+
'aws' => ['region' => 'us-east-1', 'key' => 'arn:aws:kms:us-east-1:579766882180:key/89fcc2c4-08b0-4bd9-9f25-e30687b580d0'],
1653+
'azure' => ['keyVaultEndpoint' => 'key-vault-csfle.vault.azure.net', 'keyName' => 'key-name-csfle'],
1654+
'gcp' => ['projectId' => 'devprod-drivers', 'location' => 'global', 'keyRing' => 'key-ring-csfle', 'keyName' => 'key-name-csfle'],
1655+
'kmip' => [],
1656+
];
1657+
1658+
// Test setup
1659+
$client = static::createTestClient();
1660+
1661+
// Ensure that the key vault is dropped with a majority write concern
1662+
self::insertKeyVaultData($client, []);
1663+
1664+
$clientEncryptionOpts = [
1665+
'keyVaultNamespace' => 'keyvault.datakeys',
1666+
'kmsProviders' => [
1667+
'aws' => Context::getAWSCredentials(),
1668+
'azure' => Context::getAzureCredentials(),
1669+
'gcp' => Context::getGCPCredentials(),
1670+
'kmip' => ['endpoint' => Context::getKmipEndpoint()],
1671+
'local' => ['key' => new Binary(base64_decode(self::LOCAL_MASTERKEY), 0)],
1672+
],
1673+
'tlsOptions' => [
1674+
'kmip' => Context::getKmsTlsOptions(),
1675+
],
1676+
];
1677+
1678+
$clientEncryption1 = $client->createClientEncryption($clientEncryptionOpts);
1679+
1680+
$createDataKeyOpts = [];
1681+
1682+
if (isset($providerMasterKeys[$srcProvider])) {
1683+
$createDataKeyOpts['masterKey'] = $providerMasterKeys[$srcProvider];
1684+
}
1685+
1686+
$keyId = $clientEncryption1->createDataKey($srcProvider, $createDataKeyOpts);
1687+
1688+
$ciphertext = $clientEncryption1->encrypt('test', ['algorithm' => ClientEncryption::AEAD_AES_256_CBC_HMAC_SHA_512_DETERMINISTIC, 'keyId' => $keyId]);
1689+
1690+
$clientEncryption2 = $client->createClientEncryption($clientEncryptionOpts);
1691+
1692+
$rewrapManyDataKeyOpts = ['provider' => $dstProvider];
1693+
1694+
if (isset($providerMasterKeys[$dstProvider])) {
1695+
$rewrapManyDataKeyOpts['masterKey'] = $providerMasterKeys[$dstProvider];
1696+
}
1697+
1698+
$result = $clientEncryption2->rewrapManyDataKey([], $rewrapManyDataKeyOpts);
1699+
1700+
$this->assertObjectHasAttribute('bulkWriteResult', $result);
1701+
$this->assertIsObject($result->bulkWriteResult);
1702+
// libmongoc uses different field names for its BulkWriteResult
1703+
$this->assertObjectHasAttribute('nModified', $result->bulkWriteResult);
1704+
$this->assertSame(1, $result->bulkWriteResult->nModified);
1705+
1706+
$this->assertSame('test', $clientEncryption1->decrypt($ciphertext));
1707+
$this->assertSame('test', $clientEncryption2->decrypt($ciphertext));
1708+
}
1709+
1710+
public static function provideRewrapManyDataKeySrcAndDstProviders()
1711+
{
1712+
$providers = ['aws', 'azure', 'gcp', 'kmip', 'local'];
1713+
1714+
foreach ($providers as $srcProvider) {
1715+
foreach ($providers as $dstProvider) {
1716+
yield [$srcProvider, $dstProvider];
1717+
}
1718+
}
1719+
}
1720+
15551721
private function createInt64(string $value): Int64
15561722
{
15571723
$array = sprintf('a:1:{s:7:"integer";s:%d:"%s";}', strlen($value), $value);

0 commit comments

Comments
 (0)