From 71b9d40711773001b5bf485c9cf0745723d9323b Mon Sep 17 00:00:00 2001 From: David Puglielli Date: Mon, 3 Feb 2020 14:39:28 -0800 Subject: [PATCH] Ae v2 extended tests (#1077) * Added extended AE v2 tests * Added binary types to error check * Updated test descriptions * Added the test matrix * Refactored tests * Added else check for keystore * Debugging connection failures * Debugging connection failures * Debugging connection failures * Addressed review comments * Fixed parse error * Fixed parse error * Fixed parse error * Addressed review comments --- test/extended/AE_v2_values.inc | 220 ++++ test/extended/MsSetup.inc | 58 + test/extended/pdo_AE_functions.inc | 911 ++++++++++++++++ .../pdo_aev2_plaintext_nonstring.phpt | 41 + test/extended/pdo_aev2_plaintext_string.phpt | 41 + ...do_aev2_reencrypt_encrypted_nonstring.phpt | 39 + .../pdo_aev2_reencrypt_encrypted_string.phpt | 39 + test/extended/skipif_not_hgs.inc | 36 + test/extended/sqlsrv_AE_functions.inc | 991 ++++++++++++++++++ .../sqlsrv_aev2_plaintext_nonstring.phpt | 41 + .../sqlsrv_aev2_plaintext_string.phpt | 41 + ...rv_aev2_reencrypt_encrypted_nonstring.phpt | 39 + ...qlsrv_aev2_reencrypt_encrypted_string.phpt | 39 + test/functional/setup/setup_dbs.py | 5 + 14 files changed, 2541 insertions(+) create mode 100644 test/extended/AE_v2_values.inc create mode 100644 test/extended/MsSetup.inc create mode 100644 test/extended/pdo_AE_functions.inc create mode 100644 test/extended/pdo_aev2_plaintext_nonstring.phpt create mode 100644 test/extended/pdo_aev2_plaintext_string.phpt create mode 100644 test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt create mode 100644 test/extended/pdo_aev2_reencrypt_encrypted_string.phpt create mode 100644 test/extended/skipif_not_hgs.inc create mode 100644 test/extended/sqlsrv_AE_functions.inc create mode 100644 test/extended/sqlsrv_aev2_plaintext_nonstring.phpt create mode 100644 test/extended/sqlsrv_aev2_plaintext_string.phpt create mode 100644 test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt create mode 100644 test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt diff --git a/test/extended/AE_v2_values.inc b/test/extended/AE_v2_values.inc new file mode 100644 index 00000000..d4f32638 --- /dev/null +++ b/test/extended/AE_v2_values.inc @@ -0,0 +1,220 @@ +$attestation, + 'enabled' =>'enabled', + 'disabled'=>'disabled', + 'invalid' =>$wrongProtocol, + 'wrongurl'=>$wrongAttestation, + ); + +$targetCeValues = array('correct' =>$attestation, + 'enabled' =>'enabled', + 'disabled'=>'disabled', + 'invalid' =>$wrongProtocol, + 'wrongurl'=>$wrongAttestation, + ); + +// Names of the encryption keys, depending on whether we are using Windows +// or AKV authentication (defined in MsSetup.inc). -enclave keys are enclave +// enabled, -noenclave keys are not enclave enabled. +// $targetKeys are the keys used for re-encrypting encrypted columns +if ($keystore == 'win') { + $keys = array("CEK-win-enclave", + "CEK-win-noenclave" + ); + $targetKeys = array("CEK-win-enclave", + "CEK-win-noenclave", + "CEK-win-enclave2", + "CEK-win-noenclave2" + ); +} elseif ($keystore == 'akv') { + $keys = array("CEK-akv-enclave", + "CEK-akv-noenclave" + ); + $targetKeys = array("CEK-akv-enclave", + "CEK-akv-noenclave", + "CEK-akv-enclave2", + "CEK-akv-noenclave2" + ); +} else { + die("No keystore specified! Aborting...\n"); +} + +// $targetTypes are the encryption types used for re-encrypting encrypted columns +$encryptionTypes = array("Deterministic", + "Randomized", + ); +$targetTypes = array("Deterministic", + "Randomized", + ); + + +// Length of the string-type columns. $slength is length as a string instead of integer +$length = 64; +$slength = '64'; + +// Testing the following data types, split into two arrays because if we try one array, +// at some point we get a CE405 error for no clear reason (might be a memory issue?). +// TODO: Follow up and see if we can use a single type array. +$dataTypes1 = array('integer', + 'bigint', + 'smallint', + 'tinyint', + 'bit', + 'float', + 'real', + 'numeric', + 'date', + 'time', + 'datetime', + 'datetime2', + 'datetimeoffset', + 'smalldatetime', + ); + +$dataTypes2 = array('char', + 'nchar', + 'varchar', + 'nvarchar', + 'varchar(max)', + 'nvarchar(max)', + 'binary', + 'varbinary', + 'varbinary(max)', + ); + +// Construct the array of column names. Two columns for each data type, +// one encrypted (suffixed _AE) and one not encrypted. +$colNames1 = array(); +$colNamesAE1 = array(); +$colNames2 = array(); +$colNamesAE2 = array(); + +foreach ($dataTypes1 as $type) { + $column = str_replace(array("(", ",", ")"), array("_", "_", ""), $type); + $colNames1[$type] = "c_".$column; + $colNamesAE1[$type] = "c_".$column."_AE"; +} + +foreach ($dataTypes2 as $type) { + $column = str_replace(array("(", ",", ")"), array("_", "_", ""), $type); + $colNames2[$type] = "c_".$column; + $colNamesAE2[$type] = "c_".$column."_AE"; +} + +// The test data below is a mixture of random data and edge cases +$testValues = array(); + +// integers +$testValues['integer'] = array(0,-1,1,2147483647,-2147483648,65536,-100000000,128,9); +$testValues['bigint'] = array(9223372036854775807,-40,0,1,2147483647,-2147483648,65536,-100000000000000); +$testValues['smallint'] = array(4,-4,-32768,-99,32767,-30000,-12,-1); +$testValues['tinyint'] = array(2,0,255,254,99,101,100,32); +$testValues['bit'] = array(1,1,0,0,0,0,1,0); + +// floating point +$testValues['float'] = array(3.14159,2.3e+12,-2.3e+12,2.23e-308,1,-1.79e+308,892.3098234234001,1.2); +$testValues['real'] = array(3.14159,2.3e+12,-2.3e+12,1.18e-38,1,-3.4e+38,892.3098234234001,1.2); +$testValues['numeric'] = array(-3.14159,1.003456789,45.6789,0.0001,987987.12345,-987987.12345,100000000000,-100000000000); + +// dates and times +$testValues['date'] = array('2010-01-31','0485-03-31','7825-07-23','9999-12-31','1956-02-27','2018-09-01','5401-11-02','1031-10-04'); +$testValues['time'] = array('12:40:40','08:14:54.3096','23:59:59.9999','01:00:34.0101','21:45:45.4545','00:23:45.6','17:48:00.0000','20:31:49.0001'); +$testValues['datetime2'] = array('9801-01-29 11:45:23.5092856','2384-12-31 12:40:12.5434323','1984-09-25 10:40:20.0909111','9999-12-31 23:59:59.999999', + '1259-04-29 23:59:59.9999999','1748-09-21 17:48:54.723','3125-05-31 05:00:32.4','0001-01-01 00:00:00'); +$testValues['datetimeoffset'] = array('9801-01-29 11:45:23.5092856-12:45','0001-01-01 00:00:00-02:30','1984-09-25 10:40:20.0909111+03:00','1748-09-21 17:48:54.723-09:21', + '4896-05-18 23:17:58.3-02:00','1657-08-04 18:14:27.4','2022-03-17 07:31:45.890342+09:30','1987-10-25 14:27:34.6320945-06:00'); +$testValues['datetime'] = array('9801-01-29 11:45:23.509','2384-12-31 12:40:12.543','1984-09-25 10:40:20.090','9999-12-31 23:59:59.997', + '2753-04-29 23:59:59.997','1948-09-21 17:48:54.723','3125-05-31 05:00:32.4','2001-01-01 00:00:00'); +$testValues['smalldatetime'] = array('1998-06-13 04:00:00','1985-03-31 12:40:00','2025-07-23 05:00:00','1999-12-31 00:00:00', + '1956-02-27 23:59:00','2018-09-01 14:35:00','2079-06-06 23:59:00','1931-10-04 19:52:00'); + +// strings, ascii and unicode +$testValues['char'] = array('wvyxz', 'tposw', '%c@kj>5', 'fd4$_w@q^@!coe$7', 'abcd', 'ev72#x*fv=u$', '4rfg3sw', 'voi%###i<@@'); +$testValues['nchar'] = array('⽧㘎ⷅ㪋','af㋮ᶄḉㇼ៌ӗඣ','ኁ㵮ഖᅥ㪮ኸ⮊ߒᙵꇕ⯐គꉟफ़⻦ꈔꇼŞ','ꐷꬕ','㐯㩧㖃⺵㴰ڇལᧆ겴ꕕ겑וֹꔄ若㌉ᒵȅ㗉ꗅᡉ','ʭḪぅᾔᎀ㍏겶ꅫၞ㴉ᴳ㜞҂','','בּŬḛʼꃺꌖ㓵ꗛ᧽ഭწ社⾯㬄౧ຸฬ㐯ꋛ㗾'); +$testValues['varchar'] = array('gop093','*#$@@)%*$@!%','cio4*3do*$','zzz$a#l',' ','v#x%n!k&r@p$f^','6$gt?je#~','0x3dK#?'); +$testValues['nvarchar'] = array('ᾁẴ㔮㖖ୱܝ㐗㴴៸ழ᷂ᵄ葉អ㺓節','ӕᏵ൴ꔓὀ⾼','Ὡ','璉Džꖭ갪ụ⺭','Ӿϰᇬ㭡㇑ᵈᔆ⽹hᙎ՞ꦣ㧼ለͭ','Ĕ㬚㔈♠既','ꁈ ݫ','ꍆફⷌ㏽̗ૣܯ¢⽳㌭ゴᔓᅄѓⷉꘊⶮᏏᴗஈԋ≡ㄊହꂈ꓂ꑽრꖾŞ⽉걹ꩰോఫ㒧㒾㑷藍㵀ဲ更ꧥ'); +$testValues['varchar(max)'] = array('Q0H4@4E%v+ 3*Trx#>*r86-&d$VgjZ','AjEvVABur(A&Q@eG,A$3u"xAzl','z#dFd4z', + '9Dvsg9B?7oktB@|OIqy<\K^\e|*7Y&yH31E-<.hQ:)g Jl`MQV>rdOhjG;B4wQ(WR[`l(pELt0FYu._T3+8tns!}Nqrc1%n@|N|ik C@ 03a/ +H9mBq', + 'SSs$Ie*{:D4;S]',' ','<\K^\e|*7Y&yH31E-<.hQ:','@Kg1Z6XTOgbt?CEJ|M^rkR_L4{1?l', '<=', '>=', '<>', '!<', '!>'); + +// Thresholds against which to use the comparison operators +$thresholds = array('integer' => 0, + 'bigint' => 0, + 'smallint' => 1000, + 'tinyint' => 100, + 'bit' => 0, + 'float' => 1.2, + 'real' => -1.2, + 'numeric' => 45.6789, + 'char' => 'rstuv', + 'nchar' => '㊃ᾞਲ㨴꧶ꁚꅍ', + 'varchar' => '6$gt?je#~', + 'nvarchar' => 'ӕᏵ൴ꔓὀ⾼', + 'varchar(max)' => 'hijkl', + 'nvarchar(max)' => 'xᐕᛙᘡ', + 'binary' => '44EE4A', + 'varbinary' => 'E43004FF', + 'varbinary(max)' => 'D3EA762C78FC', + 'date' => '2010-01-31', + 'time' => '21:45:45.4545', + 'datetime' => '3125-05-31 05:00:32.4', + 'datetime2' => '2384-12-31 12:40:12.5434323', + 'datetimeoffset' => '1984-09-25 10:40:20.0909111+03:00', + 'smalldatetime' => '1998-06-13 04:00:00', + ); + +// String patterns to test with LIKE +// For AE, LIKE only works with string types for now. Additional types +// are listed here because eventually the type conversions required for +// pattern matching non-string types should be supported. +$patterns = array('integer' => array('8', '48', '123'), + 'bigint' => array('000','7', '65536'), + 'smallint' => array('4','768','abc'), + 'tinyint' => array('9','0','25'), + 'bit' => array('0','1','100'), + 'float' => array('14159','.','E+','2.3','308'), + 'real' => array('30','.','e-','2.3','38'), + 'numeric' => array('0','0000','12345','abc','.'), + 'char' => array('w','@','x*fv=u$','e3'), + 'nchar' => array('af㋮','㐯ꋛ㗾','ꦣ㧼ለͭ','123'), + 'varchar' => array(' ','a','#','@@)'), + 'nvarchar' => array(' ','Ӿϰᇬ㭡','璉Džꖭ갪ụ⺭','更ꧥ','ꈔꇼŞ'), + 'varchar(max)' => array('A','|*7Y&','4z','@!@','AjE'), + 'nvarchar(max)' => array('t','㧶ᐁቴƯɋ','ᘷ㬡',' ','ꐾɔᡧ㝚'), + 'binary' => array('44EE4A'), + 'varbinary' => array('E43004FF'), + 'varbinary(max)' => array('D3EA762C78FC'), + 'date' => array('20','%','9-','04'), + 'time' => array('4545','.0','20:','12345',':'), + 'datetime' => array('997','12',':5','9999'), + 'datetime2' => array('3125-05-31 05:','.45','$f#','-29 ','0001'), + 'datetimeoffset' => array('+02','96',' ','5092856',':00'), + 'smalldatetime' => array('00','1999','abc',':','06'), + ); +?> diff --git a/test/extended/MsSetup.inc b/test/extended/MsSetup.inc new file mode 100644 index 00000000..36be4308 --- /dev/null +++ b/test/extended/MsSetup.inc @@ -0,0 +1,58 @@ + $database, "UID" => $userName, "PWD" => $userPassword, "TraceOn" => false, "Driver" => $driver); +$daasMode = false; +$marsMode = true; + +$traceEnabled = false; + +$adServer = 'TARGET_AD_SERVER'; +$adDatabase = 'TARGET_AD_DATABASE'; +$adUser = 'TARGET_AD_USERNAME'; +$adPassword = 'TARGET_AD_PASSWORD'; + +if (isset($_ENV['MSSQL_SERVER']) || isset($_ENV['MSSQL_USER']) || isset($_ENV['MSSQL_PASSWORD'])) { + $server = $_ENV['MSSQL_SERVER']; + $uid = $_ENV['MSSQL_USER']; + $pwd = $_ENV['MSSQL_PASSWORD']; + $databaseName = $_ENV['MSSQL_DATABASE_NAME']; +} else { + $uid = $userName; + $pwd = $userPassword; + $databaseName = $database; +} + +// column encryption variables +$keystore = "none"; // key store provider, acceptable values are none, win, ksp, akv +$dataEncrypted = false; // whether data is to be encrypted + +// for Azure Key Vault +$AKVKeyStoreAuthentication = 'TARGET_AKV_AUTH'; // can be KeyVaultPassword or KeyVaultClientSecret +$AKVPrincipalName = 'TARGET_AKV_PRINCIPAL_NAME'; // for use with KeyVaultPassword +$AKVPassword = 'TARGET_AKV_PASSWORD'; // for use with KeyVaultPassword +$AKVClientID = 'TARGET_AKV_CLIENT_ID'; // for use with KeyVaultClientSecret +$AKVSecret = 'TARGET_AKV_CLIENT_SECRET'; // for use with KeyVaultClientSecret + +// for enclave computations +$attestation = 'TARGET_ATTESTATION'; +?> \ No newline at end of file diff --git a/test/extended/pdo_AE_functions.inc b/test/extended/pdo_AE_functions.inc new file mode 100644 index 00000000..7f52c939 --- /dev/null +++ b/test/extended/pdo_AE_functions.inc @@ -0,0 +1,911 @@ +$ceValue) { + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->query("DBCC FREEPROCCACHE"); + + // Create and populate a non-encrypted table + $createQuery = constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating a plaintext table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + // Encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + $isEncrypted = encryptTable($conn, $alterQuery, $key, $encryptionType, $attestationType); + } + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// runEncryptedTest is the main function that cycles through the +// ColumnEncryption keywords, keys, and encryption types, testing +// in-place re-encryption and rich computations. The arguments +// all come from AE_v2_values.inc. +// Arguments: +// array $ceValues: ColumnEncryption keywords/attestation URLs +// array $keys: Encryption keys +// array $encryptionTypes: Encryption types (Deterministic, Randomized) +// array $targetCeValues: ColumnEncryption keywords/attestation URLs on reconnection +// array $targetKeys: Encryption keys on reconnection +// array $targetTypes: Encryption types on reconnection +// string $tableName: Name of table used for testing +// array $dataTypes: Data types going into the table +// array $colNames: Plaintext column names +// array $colNamesAE: Encrypted column names +// integer $length: Size of string columns +// string $slength: $length as a string +// array $testValues: Data to be inserted into the table +// array $comparisons: The comparison operators +// array $patterns: Values to pattern match against +// array $thresholds: Values to use comparison operators against +function runEncryptedTest($ceValues, $keys, $encryptionTypes, + $targetCeValues, $targetKeys, $targetTypes, + $tableName, $dataTypes, $colNames, $colNamesAE, + $length, $slength, $testValues, + $comparisons, $patterns, $thresholds) +{ + // Create a table for each key and encryption type, re-encrypt using each + // combination of target key and target encryption + foreach ($ceValues as $attestationType=>$ceValue) { + + // Cannot create a table with encrypted data if CE is disabled + // TODO: Since we can create an empty encrypted table with + // CE disabled, account for the case where CE is disabled. + if ($ceValue == 'disabled') continue; + + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->query("DBCC FREEPROCCACHE"); + + // Create and populate an encrypted table + $createQuery = constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + try { + $stmt = $conn->query("DROP TABLE IF EXISTS $tableName"); + $stmt = $conn->query($createQuery); + } catch(Exception $error) { + print_r($error); + die("Creating an encrypted table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + $ceDisabled = ($attestationType == 'disabled') ? true : false; + insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled); + + $isEncrypted = true; + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + $initiallyEnclaveEncryption = isEnclaveEnabled($key); + + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType, true, $initiallyEnclaveEncryption); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// Connect and clear the procedure cache +function connect($attestationInfo) +{ + require("MsSetup.inc"); + $options = "sqlsrv:Server=$server;Database=$databaseName;ColumnEncryption=$attestationInfo"; + + if ($keystore == 'akv') { + + $securityInfo = ''; + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $securityInfo .= ";KeyStoreAuthentication=$AKVKeyStoreAuthentication"; + $securityInfo .= ";KeyStorePrincipalId=$AKVPrincipalName"; + $securityInfo .= ";KeyStoreSecret=$AKVPassword"; + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $securityInfo .= ";KeyStoreAuthentication=$AKVKeyStoreAuthentication"; + $securityInfo .= ";KeyStorePrincipalId=$AKVClientID"; + $securityInfo .= ";KeyStoreSecret=$AKVSecret"; + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options .= $securityInfo; + } + + try { + $conn = new PDO($options, $uid, $pwd); + } catch (PDOException $error) { + $e = $error->errorInfo; + checkErrors($e, array('CE400', '0')); + return false; + } + + $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $conn->setAttribute(PDO::SQLSRV_ATTR_FETCHES_DATETIME_TYPE, true); + + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + + $stmt = $conn->query($query); + if (!$stmt) { + print_r($conn->errorInfo()); + die("Error when checking if enclave computations are enabled. This should never happen! Non-HGS servers should have been skipped.\n"); + } else { + $info = $stmt->fetch(); + if (empty($info) or ($info['value'] != 1) or ($info['value_in_use'] != 1)) { + die("Error: enclave computations are not enabled on the server!"); + } + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + $conn->exec("DBCC FREEPROCCACHE"); + + unset($stmt); + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + + foreach ($dataTypes as $dataType) { + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled=false) +{ + if (empty($testValues)) { + die("$testValues is empty or non-existent. Please check the required values file.\n"); + } + + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // Insert the data using PDO::prepare() + try { + $stmt = $conn->prepare($insertQuery); + $i=1; + foreach ($dataTypes as $type) { + $PDOType = getPDOType($type); + if (!dataTypeIsBinary($type)) { + $stmt->bindParam($i, $testValues[$type][$v], $PDOType); + $stmt->bindParam($i+1, $testValues[$type][$v], $PDOType); + } else { + // unset() is necessary because otherwise the same data may be + // inserted into multiple binary columns. + unset($val); + $val=pack('H*', $testValues[$type][$v]); + $stmt->bindParam($i, $val, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->bindParam($i+1, $val, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + } + $i+=2; + } + $stmt->execute(); + } catch (PDOException $error) { + if (!$ceDisabled) { + print_r($error); + die("Inserting values in encrypted table failed\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('22018', '206')); + } + } + } + + unset($stmt); +} + +// encryptTable attempts to encrypt the table in place and verifies +// if it works given the attestation info and key type. +// Arguments: +// resource $conn: The connection +// string $alterQuery: The query to encrypt the table +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $sameKeyAndType: Whether the key and encryption type are same for re-encrypting +// as for initial encryption. +// bool $initialEncryption: Whether we are testing with table initially encrypted, instead +// of plaintext being encrypted after creation +// bool $initiallyEnclaveEncrypted: Whether the table was initally encrypted with an +// enclave-enabled key +function encryptTable($conn, $alterQuery, $key, $encryptionType, $attestation, $sameKeyAndType=false, $initialEncryption=false, $initallyEnclaveEncrypted=false) +{ + try { + $stmt = $conn->query($alterQuery); + if ((!isEnclaveEnabled($key) or $attestation != 'correct') and !$sameKeyAndType) { + die("Encrypting should have failed with attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } catch (PDOException $error) { + if ($sameKeyAndType) { + print_r($error); + die("Encrypting table should not fail when target encryption key and type are the same as source: attestation $attestation, key $key and encryption type $encryptionType\n"); + } elseif ($initialEncryption and !$initallyEnclaveEncrypted) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } elseif ($attestation == 'correct') { + if (isEnclaveEnabled($key)) { + print_r($error); + die("Encrypting with correct attestation failed when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'enabled' or $attestation == 'disabled') { + if (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'wrongurl') { + if (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'invalid') { + die("Encrypting table with invalid protocol! Should not get here!\n"); + } else { + die("Error! This is no-man's-land\n"); + } + + return false; + } + + unset($stmt); + + return true; +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison='', $type='') +{ + try { + $nonAEstmt->execute(); + } catch(Exception $error) { + print_r($error); + die("Executing non-AE computation statement failed!\n"); + } + + try { + $AEstmt->execute(); + } catch(Exception $error) { + if (!$isEncrypted) { + die("Computation statement execution should not have failed for an unencrypted table: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + $e = $error->errorInfo; + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } else { + print_r($error); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } else { + print_r($error); + die("Comparison failed for correct attestation when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } elseif ($attestation == 'disabled') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } elseif ($comparison == '=' or $comparison == '<>' or $encryptionType == 'Randomized') { + $e = $error->errorInfo; + checkErrors($e, array('22018', '206')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33277')); + } + } else { + print_r($error); + die("Unexpected error occurred in compareResults: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + return; + } + + $AEres = $AEstmt->fetchAll(PDO::FETCH_NUM); + $nonAEres = $nonAEstmt->fetchAll(PDO::FETCH_NUM); + $AEcount = count($AEres); + $nonAEcount = count($nonAEres); + + if ($type == 'char' or $type == 'nchar' or $type == 'binary') { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + return; + } elseif ($AEcount > $nonAEcount) { + print_r("Too many AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } elseif ($AEcount < $nonAEcount) { + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + print_r($AEres); + print_r($nonAEres); + } else { + if ($AEcount != 0) { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr[0] != $nonAEres[$i][0]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i][0]." and non-AE result ".$nonAEres[$i][0]."\n"); + } + ++$i; + } + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// integer $length: Length of the string types, from AE_v2_values.inc +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the Latin1_General_BIN2 + // collation. If the COLLATE clause is left out, we get different + // results between the encrypted and non-encrypted columns (probably + // because the collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $unicode = dataTypeIsUnicode($type); + $PDOType = getPDOType($type); + unset($threshold); + $threshold = dataTypeIsBinary($type) ? pack('H*', $thresholds[$type]) : $thresholds[$type]; + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + try { + $AEstmt = $conn->prepare($AEQuery); + $nonAEstmt = $conn->prepare($nonAEQuery); + + if (!dataTypeIsBinary($type)) { + $AEstmt->bindParam(1, $threshold, $PDOType); + $nonAEstmt->bindParam(1, $threshold, $PDOType); + } else { + $AEstmt->bindParam(1, $threshold, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + $nonAEstmt->bindParam(1, $threshold, $PDOType, 0, PDO::SQLSRV_ENCODING_BINARY); + } + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed! Comparison $comparison, type $type"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $patterns: Strings to pattern match, from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types. + // This is for security reasons, follow up on it. + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + + $patternArray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternArray as $spattern) { + + // Unicode operations with AE require the PHPTYPE to be specified as + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + // We must pass the length of the pattern matching string + // to the SQLTYPE instead of the field size, as we usually would, + // because otherwise we would get an empty result set. + // We need iconv_strlen to return the number of characters + // for unicode strings, since strlen returns the number of bytes. + $unicode = dataTypeIsUnicode($type); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $PDOType = getPDOType($type); + + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + // TODO: Add binary type support below. May need to use unset() + // as in insertValues(). + try { + $AEstmt = $conn->prepare($AEQuery); + $AEstmt->bindParam(1, $spattern, $PDOType); + $nonAEstmt = $conn->prepare($nonAEQuery); + $nonAEstmt->bindParam(1, $spattern, $PDOType); + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $pattern, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } + } +} + +// Check that the expected errors ($codes) is found in the PDOException ($errors) +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0] and $code[1]==$errors[1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function dataTypeIsBinary($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "varbinary(max)"])); +} + +function getPDOType($type) +{ + switch($type) { + case "bigint": + case "integer": + case "smallint": + case "tinyint": + return PDO::PARAM_INT; + case "bit": + return PDO::PARAM_BOOL; + case "real": + case "float": + case "double": + case "numeric": + case "time": + case "date": + case "datetime2": + case "datetime": + case "datetimeoffset": + case "smalldatetime": + case "money": + case "smallmoney"; + case "xml": + case "uniqueidentifier": + case "char": + case "varchar": + case "varchar(max)": + case "nchar": + case "nvarchar": + case "nvarchar(max)": + return PDO::PARAM_STR; + case "binary": + case "varbinary": + case "varbinary(max)": + return PDO::PARAM_LOB; + default: + die("Case is missing for $type type in getPDOType.\n"); + } +} + +?> diff --git a/test/extended/pdo_aev2_plaintext_nonstring.phpt b/test/extended/pdo_aev2_plaintext_nonstring.phpt new file mode 100644 index 00000000..81e47198 --- /dev/null +++ b/test/extended/pdo_aev2_plaintext_nonstring.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_plaintext_string.phpt b/test/extended/pdo_aev2_plaintext_string.phpt new file mode 100644 index 00000000..14bc276f --- /dev/null +++ b/test/extended/pdo_aev2_plaintext_string.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt b/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt new file mode 100644 index 00000000..0821e8e3 --- /dev/null +++ b/test/extended/pdo_aev2_reencrypt_encrypted_nonstring.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt b/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt new file mode 100644 index 00000000..e016fbfd --- /dev/null +++ b/test/extended/pdo_aev2_reencrypt_encrypted_string.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/skipif_not_hgs.inc b/test/extended/skipif_not_hgs.inc new file mode 100644 index 00000000..dd4614de --- /dev/null +++ b/test/extended/skipif_not_hgs.inc @@ -0,0 +1,36 @@ +$uid, "PWD"=>$pwd, "Driver" => $driver); + +$conn = sqlsrv_connect( $server, $connectionInfo ); +if ($conn === false) { + die( "skip Could not connect during SKIPIF." ); +} + +$msodbcsql_ver = sqlsrv_client_info($conn)["DriverVer"]; +$msodbcsql_maj = explode(".", $msodbcsql_ver)[0]; +$msodbcsql_min = explode(".", $msodbcsql_ver)[1]; + +if ($msodbcsql_maj < 17) { + die("skip Unsupported ODBC driver version"); +} + +if ($msodbcsql_min < 4 and $msodbcsql_maj == 17) { + die("skip Unsupported ODBC driver version"); +} + +// Get SQL Server +$server_info = sqlsrv_server_info($conn); +if (strpos($server_info['SQLServerName'], 'PHPHGS') === false) { + die("skip Server is not HGS enabled"); +} +?> diff --git a/test/extended/sqlsrv_AE_functions.inc b/test/extended/sqlsrv_AE_functions.inc new file mode 100644 index 00000000..5760b65f --- /dev/null +++ b/test/extended/sqlsrv_AE_functions.inc @@ -0,0 +1,991 @@ +$ceValue) { + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + + // Create and populate a non-encrypted table + $createQuery = constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + $stmt = sqlsrv_query($conn, "DROP TABLE IF EXISTS $tableName"); + $stmt = sqlsrv_query($conn, $createQuery); + if(!$stmt) { + print_r(sqlsrv_errors()); + die("Creating a plaintext table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + // Encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $key, $encryptionType, $slength); + $isEncrypted = encryptTable($conn, $alterQuery, $key, $encryptionType, $attestationType); + } + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if ($key == $targetKey and $encryptionType == $targetType and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// runEncryptedTest is the main function that cycles through the +// ColumnEncryption keywords, keys, and encryption types, testing +// in-place re-encryption and rich computations. The arguments +// all come from AE_v2_values.inc. +// Arguments: +// array $ceValues: ColumnEncryption keywords/attestation URLs +// array $keys: Encryption keys +// array $encryptionTypes: Encryption types (Deterministic, Randomized) +// array $targetCeValues: ColumnEncryption keywords/attestation URLs on reconnection +// array $targetKeys: Encryption keys on reconnection +// array $targetTypes: Encryption types on reconnection +// string $tableName: Name of table used for testing +// array $dataTypes: Data types going into the table +// array $colNames: Plaintext column names +// array $colNamesAE: Encrypted column names +// integer $length: Size of string columns +// string $slength: $length as a string +// array $testValues: Data to be inserted into the table +// array $comparisons: The comparison operators +// array $patterns: Values to pattern match against +// array $thresholds: Values to use comparison operators against +function runEncryptedTest($ceValues, $keys, $encryptionTypes, + $targetCeValues, $targetKeys, $targetTypes, + $tableName, $dataTypes, $colNames, $colNamesAE, + $length, $slength, $testValues, + $comparisons, $patterns, $thresholds) +{ + // Create a table for each key and encryption type, re-encrypt using each + // combination of target key and target encryption + foreach ($ceValues as $attestationType=>$ceValue) { + + // Cannot create a table with encrypted data if CE is disabled + // TODO: Since we can create an empty encrypted table with + // CE disabled, account for the case where CE is disabled. + if ($ceValue == 'disabled') continue; + + foreach ($keys as $key) { + foreach ($encryptionTypes as $encryptionType) { + + // $count is used to ensure we only run testCompare and + // testPatternMatch once for the initial table + $count = 0; + + foreach ($targetCeValues as $targetAttestationType=>$targetCeValue) { + foreach ($targetKeys as $targetKey) { + foreach ($targetTypes as $targetType) { + + $conn = connect($ceValue); + if (!$conn) { + if ($attestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($attestationType == 'invalid') { + die("Connection should have failed for invalid protocol at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + + // Create and populate an encrypted table + $createQuery = constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType); + $insertQuery = constructInsertQuery($tableName, $dataTypes, $colNames, $colNamesAE); + + $stmt = sqlsrv_query($conn, "DROP TABLE IF EXISTS $tableName"); + $stmt = sqlsrv_query($conn, $createQuery); + if(!$stmt) { + print_r(sqlsrv_errors()); + die("Creating an encrypted table failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + + $ceDisabled = $attestationType == 'disabled' ? true : false; + insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled); + + $isEncrypted = true; + + // Test rich computations + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestationType, $isEncrypted); + } + ++$count; + + // $sameKeyAndType is used when checking re-encryption, because no error is returned + $sameKeyAndType = false; + if (($key == $targetKey) and ($encryptionType == $targetType) and $isEncrypted) { + $sameKeyAndType = true; + } + + // Disconnect and reconnect with the target ColumnEncryption keyword value + unset($conn); + + $conn = connect($targetCeValue); + if (!$conn) { + if ($targetAttestationType == 'invalid') { + continue; + } else { + print_r(sqlsrv_errors()); + die("Connection failed when it shouldn't have at ColumnEncryption = $ceValue, key = $key, type = $encryptionType, targets $targetCeValue, $targetKey, $targetType\n"); + } + } elseif ($targetAttestationType == 'invalid') { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + + // Re-encrypt the table + $initiallyEnclaveEncryption = isEnclaveEnabled($key); + + // Split the data type array, because for some reason we get an error + // if the query is too long (>2000 characters) + // TODO: This is a known issue, follow up on it. + $splitdataTypes = array_chunk($dataTypes, 5); + foreach ($splitdataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionSucceeded = encryptTable($conn, $alterQuery, $targetKey, $targetType, $targetAttestationType, $sameKeyAndType, true, $initiallyEnclaveEncryption); + } + + // Test rich computations + if ($encryptionSucceeded) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, $targetAttestationType,true); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, $targetAttestationType, true); + } else { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $targetAttestationType, $isEncrypted); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $targetAttestationType, $isEncrypted); + } + + unset($conn); + } + } + } + } + } + } +} + +// Connect and clear the procedure cache +function connect($attestationInfo) +{ + require("MsSetup.inc"); + $options = array('database'=>$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'CharacterSet'=>'UTF-8', + 'ColumnEncryption'=>$attestationInfo, + 'TraceOn'=>true, + 'TraceOn'=>'c:\Users\davidp\Documents\SQL.LOG', + ); + + if ($keystore == 'akv') { + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $securityInfo = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVPrincipalName, + 'KeyStoreSecret'=>$AKVPassword, + ); + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $securityInfo = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVClientID, + 'KeyStoreSecret'=>$AKVSecret, + ); + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options = array_merge($options, $securityInfo); + } + + $conn = sqlsrv_connect($server, $options); + if (!$conn) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE400', '0')); + return false; + } + else + { + // Check that enclave computations are enabled + // See https://docs.microsoft.com/en-us/sql/relational-databases/security/encryption/configure-always-encrypted-enclaves?view=sqlallproducts-allversions#configure-a-secure-enclave + $query = "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';"; + $stmt = sqlsrv_query($conn, $query); + if (!$stmt) { + print_r(sqlsrv_errors()); + die("Error when checking if enclave computations are enabled. This should never happen! Non-HGS servers should have been skipped.\n"); + } else { + $info = sqlsrv_fetch_array($stmt); + if (empty($info) or ($info['value'] != 1) or ($info['value_in_use'] != 1)) { + die("Error: enclave computations are not enabled on the server!"); + } + } + + // Enable rich computations + sqlsrv_query($conn, "DBCC traceon(127,-1);"); + + // Free the encryption cache to avoid spurious 'operand type clash' errors + sqlsrv_query($conn, "DBCC FREEPROCCACHE"); + } + + return $conn; +} + +// This CREATE TABLE query simply creates a non-encrypted table with +// two columns for each data type side by side +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer, +// c_integer_AE integer +// ) +function constructCreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength."), \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength."), \n "; + } else { + $query = $query.$colNames[$type]." ".$type.", \n "; + $query = $query.$colNamesAE[$type]." ".$type.", \n "; + } + } + + // Remove the ", \n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -7)."\n)"; + + return $query; +} + +// The ALTER TABLE query encrypts columns. Each ALTER COLUMN directive must +// be preceded by ALTER TABLE +// This produces a query that looks like +// ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_integer_AE] integer +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER TABLE [dbo].[aev2test2] +// ALTER COLUMN [c_bigint_AE] bigint +// ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL +// WITH +// (ONLINE = ON); ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE; +function constructAlterQuery($tableName, $colNames, $dataTypes, $key, $encryptionType, $slength) +{ + $query = ''; + + foreach ($dataTypes as $dataType) { + $plength = dataTypeIsString($dataType) ? "(".$slength.")" : ""; + $collate = dataTypeNeedsCollate($dataType) ? " COLLATE Latin1_General_BIN2" : ""; + $query = $query." ALTER TABLE [dbo].[".$tableName."] + ALTER COLUMN [".$colNames[$dataType]."] ".$dataType.$plength." ".$collate." + ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NOT NULL + WITH + (ONLINE = ON);"; + } + + $query = $query." ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + return $query; +} + +// This CREATE TABLE query creates a table with two columns for +// each data type side by side, one plaintext and one encrypted +// This produces a query that looks like +// CREATE TABLE aev2test2 ( +// c_integer integer NULL, +// c_integer_AE integer +// COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [CEK-win-enclave], ENCRYPTION_TYPE = Randomized, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL +// ) +function constructAECreateQuery($tableName, $dataTypes, $colNames, $colNamesAE, $slength, $key, $encryptionType) +{ + $query = "CREATE TABLE ".$tableName." (\n "; + + foreach ($dataTypes as $type) { + $collate = dataTypeNeedsCollate($type) ? " COLLATE Latin1_General_BIN2" : ""; + + if (dataTypeIsString($type)) { + $query = $query.$colNames[$type]." ".$type."(".$slength.") NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type."(".$slength.") \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } else { + $query = $query.$colNames[$type]." ".$type." NULL, \n "; + $query = $query.$colNamesAE[$type]." ".$type." \n "; + $query = $query." ".$collate." ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [".$key."], ENCRYPTION_TYPE = ".$encryptionType.", ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256') NULL,\n "; + } + } + + // Remove the ",\n " from the end of the query or the comma will cause a syntax error + $query = substr($query, 0, -6)."\n)"; + + return $query; +} + +// The INSERT query for the table +function constructInsertQuery($tableName, &$dataTypes, &$colNames, &$colNamesAE) +{ + $queryTypes = "("; + $valuesString = "VALUES ("; + + foreach ($dataTypes as $type) { + $colName1 = $colNames[$type].", "; + $colName2 = $colNamesAE[$type].", "; + $queryTypes .= $colName1; + $queryTypes .= $colName2; + $valuesString .= "?, ?, "; + } + + // Remove the ", " from the end of the query or the comma will cause a syntax error + $queryTypes = substr($queryTypes, 0, -2).")"; + $valuesString = substr($valuesString, 0, -2).")"; + + $insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString; + + return $insertQuery; +} + +function insertValues($conn, $insertQuery, $dataTypes, $testValues, $ceDisabled=false) +{ + global $length; + + if (empty($testValues)) { + die("$testValues is empty or non-existent. Please check the required values file.\n"); + } + + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // Use pack() on binary data + $params = array(); + foreach ($dataTypes as $type) { + $SQLType = getSQLType($type, $length); + $PHPType = getPHPType($type); + $val = dataTypeIsBinary($type) ? pack('H*', $testValues[$type][$v]) : $testValues[$type][$v]; + $params[] = array($val, SQLSRV_PARAM_IN, $PHPType, $SQLType); + $params[] = array($val, SQLSRV_PARAM_IN, $PHPType, $SQLType); + } + + // Insert the data using sqlsrv_prepare() + $stmt = sqlsrv_prepare($conn, $insertQuery, $params); + if ($stmt == false) { + if (!$ceDisabled) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at prepare\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } + } + + if (sqlsrv_execute($stmt) == false) { + if (!$ceDisabled) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at execute\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } + } + + sqlsrv_free_stmt($stmt); + } +} + +// encryptTable attempts to encrypt the table in place and verifies +// if it works given the attestation info and key type. +// Arguments: +// resource $conn: The connection +// string $alterQuery: The query to encrypt the table +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $sameKeyAndType: Whether the key and encryption type are same for re-encrypting +// as for initial encryption. +// bool $initialEncryption: Whether we are testing with table initially encrypted, instead +// of plaintext being encrypted after creation +// bool $initiallyEnclaveEncrypted: Whether the table was initally encrypted with an +// enclave-enabled key +function encryptTable($conn, $alterQuery, $key, $encryptionType, $attestation, $sameKeyAndType=false, $initialEncryption=false, $initallyEnclaveEncrypted=false) +{ + $stmt = sqlsrv_query($conn, $alterQuery); + + if(!$stmt) { + if ($sameKeyAndType) { + print_r(sqlsrv_errors()); + die("Encrypting table should not fail when target encryption key and type are the same as source: attestation $attestation, key $key and encryption type $encryptionType\n"); + } elseif ($initialEncryption and !$initallyEnclaveEncrypted) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } elseif ($attestation == 'correct') { + if (isEnclaveEnabled($key)) { + print_r(sqlsrv_errors()); + die("Encrypting with correct attestation failed when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'enabled' or $attestation == 'disabled') { + if (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'wrongurl') { + if (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE405', '0')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + } + } elseif ($attestation == 'invalid') { + die("Encrypting table with invalid protocol! Should not get here!\n"); + } else { + die("Error! This is no-man's-land\n"); + } + + return false; + } else { + if ((!isEnclaveEnabled($key) or $attestation != 'correct') and !$sameKeyAndType) { + die("Encrypting should have failed with attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + unset($stmt); + + return true; + } +} + +// compareResults checks that the results between the encrypted and non-encrypted +// columns are identical if statement execution succeeds. If statement execution +// fails, this function checks for the correct error. +// Arguments: +// statement $AEstmt: Prepared statement fetching encrypted data +// statement $nonAEstmt: Prepared statement fetching non-encrypted data +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// string $comparison: Comparison operator +// string $type: Data type the comparison is operating on +function compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison='', $type='') +{ + if (!sqlsrv_execute($nonAEstmt)) { + print_r(sqlsrv_errors()); + die("Executing non-AE computation statement failed!\n"); + } + + if(!sqlsrv_execute($AEstmt)) { + if (!$isEncrypted) { + die("Computation statement execution should not have failed for an unencrypted table: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } else { + print_r(sqlsrv_errors()); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } elseif (isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE405', '0')); + } elseif (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } else { + print_r(sqlsrv_errors()); + die("AE statement execution failed when it shouldn't: attestation $attestation, key $key and encryption type $encryptionType"); + } + } elseif ($attestation == 'correct') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } elseif ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption: attestation $attestation, key $key and encryption type $encryptionType\n"); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } else { + print_r(sqlsrv_errors()); + die("Comparison failed for correct attestation when it shouldn't have: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } elseif ($attestation == 'disabled') { + if (!isEnclaveEnabled($key) and $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } elseif ($comparison == '=' or $comparison == '<>' or $encryptionType == 'Randomized') { + $e = sqlsrv_errors(); + checkErrors($e, array('22018', '206')); + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33277')); + } + } else { + print_r(sqlsrv_errors()); + die("Unexpected error occurred in compareResults: attestation $attestation, key $key and encryption type $encryptionType\n"); + } + } else { + // char and nchar may not return the same results - at this point + // we've verified that statement execution works so just return + // TODO: Check if this bug is fixed and if so, remove this if block + if ($type == 'char' or $type == 'nchar' or $type == 'binary') { + return; + } + + while($AEres = sqlsrv_fetch_array($AEstmt, SQLSRV_FETCH_NUMERIC)) { + $nonAEres = sqlsrv_fetch_array($nonAEstmt, SQLSRV_FETCH_NUMERIC); + if (!$nonAEres) { + print_r($AEres); + print_r(sqlsrv_errors()); + print_r("Too many AE results for operation $comparison and data type $type!\n"); + } else { + $i = 0; + foreach ($AEres as $AEr) { + if ($AEr != $nonAEres[$i]) { + print_r("AE and non-AE results are different for operation $comparison and data type $type! For field $i, got AE result ".$AEres[$i]." and non-AE result ".$nonAEres[$i]."\n"); + print_r(sqlsrv_errors()); + } + ++$i; + } + } + } + + if ($rr = sqlsrv_fetch_array($nonAEstmt)) { + print_r($rr); + print_r(sqlsrv_errors()); + print_r("Too many non-AE results for operation $comparison and data type $type!\n"); + } + } +} + +// testCompare selects based on a comparison in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $comparisons: Comparison operations from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// array $thresholds: Values to use comparison operators against, from AE_v2_values.inc +// string $key: Name of the encryption key +// integer $length: Length of the string types, from AE_v2_values.inc +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($comparisons as $comparison) { + foreach ($dataTypes as $type) { + + // Unicode operations with AE require the PHPTYPE to be specified to + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + $string = dataTypeIsStringMax($type); + $unicode = dataTypeIsUnicode($type); + $collate = $string ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = getPHPType($type); + $threshold = dataTypeIsBinary($type) ? pack('H*', $thresholds[$type]) : $thresholds[$type]; + + $param = array(array($threshold, SQLSRV_PARAM_IN, $phptype, getSQLType($type, $length))); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE ".$comparison." ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." ".$comparison." ?".$collate; + + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $comparison, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } +} + +// testPatternMatch selects based on a pattern in the WHERE clause and compares +// the results between encrypted and non-encrypted columns, checking that the +// results are identical +// Arguments: +// resource $conn: The connection +// string $tableName: Table name +// array $patterns: Strings to pattern match, from AE_v2_values.inc +// array $dataTypes: Data types from AE_v2_values.inc +// array $colNames: Column names +// string $key: Name of the encryption key +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', 'disabled', or 'wrongurl' +// bool $isEncrypted: Whether the table is encrypted +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation, $isEncrypted) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types. + // This is for security reasons, follow up on it. + if (!dataTypeIsStringMax($type)) { + continue; + } + + foreach ($patterns[$type] as $pattern) { + $patternarray = array($pattern, + $pattern."%", + "%".$pattern, + "%".$pattern."%", + ); + + foreach ($patternarray as $spattern) { + + // Unicode operations with AE require the PHPTYPE to be specified as + // UTF-8 and the Latin1_General_BIN2 collation. If the COLLATE + // clause is left out, we get different results between the + // encrypted and non-encrypted columns (probably because the + // collation was only changed in the encryption query). + // We must pass the length of the pattern matching string + // to the SQLTYPE instead of the field size, as we usually would, + // because otherwise we would get an empty result set. + // We need iconv_strlen to return the number of characters + // for unicode strings, since strlen returns the number of bytes. + $unicode = dataTypeIsUnicode($type); + $slength = $unicode ? iconv_strlen($spattern) : strlen($spattern); + $collate = $unicode ? " COLLATE Latin1_General_BIN2" : ""; + $phptype = $unicode ? SQLSRV_PHPTYPE_STRING('UTF-8') : null; + $sqltype = $unicode ? SQLSRV_SQLTYPE_NCHAR($slength) : SQLSRV_SQLTYPE_CHAR($slength); + + $param = array(array($spattern, SQLSRV_PARAM_IN, $phptype, $sqltype)); + $AEQuery = "SELECT ".$colNames[$type]."_AE FROM $tableName WHERE ".$colNames[$type]."_AE LIKE ?".$collate; + $nonAEQuery = "SELECT ".$colNames[$type]." FROM $tableName WHERE ".$colNames[$type]." LIKE ?".$collate; + + // TODO: Add binary type support below. May need to use unset() + // as in insertValues(). + $AEstmt = sqlsrv_prepare($conn, $AEQuery, $param); + if (!$AEstmt) { + print_r(sqlsrv_errors()); + die("Preparing AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + $nonAEstmt = sqlsrv_prepare($conn, $nonAEQuery, $param); + if (!$nonAEstmt) { + print_r(sqlsrv_errors()); + die("Preparing non-AE statement for comparison failed! Comparison $comparison, type $type\n"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $isEncrypted, $pattern, $type); + + unset($AEstmt); + unset($nonAEstmt); + } + } + } +} + +// Check that the expected errors ($codes) is found in the output of sqlsrv_errors() ($errors) +function checkErrors($errors, ...$codes) +{ + $codeFound = false; + + foreach ($codes as $code) { + if ($code[0]==$errors[0][0] and $code[1]==$errors[0][1]) { + $codeFound = true; + break; + } + } + + if ($codeFound == false) { + echo "Error: "; + print_r($errors); + echo "\nExpected: "; + print_r($codes); + echo "\n"; + die("Error code not found.\n"); + } +} + +function isEnclaveEnabled($key) +{ + return (strpos($key, '-enclave') !== false); +} + +function dataTypeIsString($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "char", "nchar", "varchar", "nvarchar"])); +} + +function dataTypeIsStringMax($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeNeedsCollate($dataType) +{ + return (in_array($dataType, ["char", "nchar", "varchar", "nvarchar", "varchar(max)", "nvarchar(max)"])); +} + +function dataTypeIsUnicode($dataType) +{ + return (in_array($dataType, ["nchar", "nvarchar", "nvarchar(max)"])); +} + +function dataTypeIsBinary($dataType) +{ + return (in_array($dataType, ["binary", "varbinary", "varbinary(max)"])); +} + +function getPHPType($type) +{ + switch($type) { + case "bigint": + case "integer": + case "smallint": + case "tinyint": + case "bit": + return SQLSRV_PHPTYPE_INT; + break; + case "real": + case "float": + case "double": + return SQLSRV_PHPTYPE_FLOAT; + break; + case "numeric": + case "money": + case "smallmoney": + case "time": + case "date": + case "datetime": + case "datetime2": + case "datetimeoffset": + case "smalldatetime": + case "xml": + case "uniqueidentifier": + case "char": + case "varchar": + case "varchar(max)": + return SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR); + break; + case "nchar": + case "nvarchar": + case "nvarchar(max)": + return SQLSRV_PHPTYPE_STRING('UTF-8'); + break; + case "binary": + case "varbinary": + case "varbinary(max)": + return SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY); + break; + default: + die("Case is missing for $type type in GetPHPType.\n"); + } +} + +function getSQLType($type, $length) +{ + switch($type) { + case "bigint": + return SQLSRV_SQLTYPE_BIGINT; + case "integer": + return SQLSRV_SQLTYPE_INT; + case "smallint": + return SQLSRV_SQLTYPE_SMALLINT; + case "tinyint": + return SQLSRV_SQLTYPE_TINYINT; + case "bit": + return SQLSRV_SQLTYPE_BIT; + case "real": + return SQLSRV_SQLTYPE_REAL; + case "float": + case "double": + return SQLSRV_SQLTYPE_FLOAT; + case "numeric": + return SQLSRV_SQLTYPE_NUMERIC(18,0); + case "time": + return SQLSRV_SQLTYPE_TIME; + case "date": + return SQLSRV_SQLTYPE_DATE; + case "datetime": + return SQLSRV_SQLTYPE_DATETIME; + case "datetime2": + return SQLSRV_SQLTYPE_DATETIME2; + case "datetimeoffset": + return SQLSRV_SQLTYPE_DATETIMEOFFSET; + case "smalldatetime": + return SQLSRV_SQLTYPE_SMALLDATETIME; + case "money": + return SQLSRV_SQLTYPE_MONEY; + case "smallmoney": + return SQLSRV_SQLTYPE_SMALLMONEY; + case "xml": + return SQLSRV_SQLTYPE_XML; + case "uniqueidentifier": + return SQLSRV_SQLTYPE_UNIQUEIDENTIFIER; + case "char": + return SQLSRV_SQLTYPE_CHAR($length); + case "varchar": + return SQLSRV_SQLTYPE_VARCHAR($length); + case "varchar(max)": + return SQLSRV_SQLTYPE_VARCHAR('max'); + case "nchar": + return SQLSRV_SQLTYPE_NCHAR($length); + case "nvarchar": + return SQLSRV_SQLTYPE_NVARCHAR($length); + case "nvarchar(max)": + return SQLSRV_SQLTYPE_NVARCHAR('max'); + case "binary": + return SQLSRV_SQLTYPE_BINARY($length); + break; + case "varbinary": + return SQLSRV_SQLTYPE_VARBINARY($length); + break; + case "varbinary(max)": + return SQLSRV_SQLTYPE_VARBINARY('max'); + break; + default: + die("Case is missing for $type type in getSQLType.\n"); + } +} + +?> diff --git a/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt b/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt new file mode 100644 index 00000000..a4f520f9 --- /dev/null +++ b/test/extended/sqlsrv_aev2_plaintext_nonstring.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_plaintext_string.phpt b/test/extended/sqlsrv_aev2_plaintext_string.phpt new file mode 100644 index 00000000..b258e534 --- /dev/null +++ b/test/extended/sqlsrv_aev2_plaintext_string.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating a +plaintext table each time, then trying to encrypt it with different combinations +of enclave-enabled and non-enclave keys and encryption types. It then reconnects +and cycles through $targetCeValues, $targetTypes and $targetKeys to try re-encrypting +the table with different target combinations of enclave-enabled and non-enclave keys +and encryption types. +The sequence of operations is the following: +1. Create a table in plaintext with two columns for each AE-supported data type. +2. Insert some data in plaintext. +3. Encrypt one column for each data type. +4. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +5. Ensure the two results are the same. +6. Disconnect and reconnect with a new value for ColumnEncryption. +7. Compare computations as in 4. above. +8. Re-encrypt the table using a new key and/or encryption type. +9. Compare computations as in 4. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt b/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt new file mode 100644 index 00000000..5574b425 --- /dev/null +++ b/test/extended/sqlsrv_aev2_reencrypt_encrypted_nonstring.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests nonstring types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt b/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt new file mode 100644 index 00000000..6d2635eb --- /dev/null +++ b/test/extended/sqlsrv_aev2_reencrypt_encrypted_string.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $ceValues, $encryptionTypes, and $keys, creating +an encrypted table each time, then cycles through $targetCeValues, $targetTypes, +and $targetKeys to try re-encrypting the table with different combinations of +enclave-enabled and non-enclave keys and encryption types. +The sequence of operations is the following: +1. Create an encrypted table with two columns for each AE-supported data type, + one encrypted and one not encrypted. +2. Insert some data. +3. Perform rich computations on each AE-enabled column (comparisons and pattern matching) + and compare the result to the same query on the corresponding non-AE column for each data type. +4. Ensure the two results are the same. +5. Disconnect and reconnect with a new value for ColumnEncryption. +6. Compare computations as in 3. above. +7. Re-encrypt the table using a new key and/or encryption type. +8. Compare computations as in 3. above. +This test only tests string types, because if we try to tests all types at +once, eventually a CE405 error is returned. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/functional/setup/setup_dbs.py b/test/functional/setup/setup_dbs.py index ead94a30..69844169 100644 --- a/test/functional/setup/setup_dbs.py +++ b/test/functional/setup/setup_dbs.py @@ -42,6 +42,8 @@ if __name__ == '__main__': parser.add_argument('-dbname', '--DBNAME', required=True) parser.add_argument('-azure', '--AZURE', required=False, default='no') args = parser.parse_args() + + print("Start\n") try: server = os.environ['TEST_PHP_SQL_SERVER'] @@ -61,10 +63,13 @@ if __name__ == '__main__': if (args.AZURE.lower() == 'no'): manageTestDB('create_db.sql', conn_options, args.DBNAME) + print("About to set up databases...\n") # create tables in the new database setupTestDatabase(conn_options, args.DBNAME, args.AZURE) + print("About to populate tables...\n") # populate these tables populateTables(conn_options, args.DBNAME) + print("About to set up encryption...\n") # setup AE (certificate, column master key and column encryption key) setupAE(conn_options, args.DBNAME)