diff --git a/source/shared/core_conn.cpp b/source/shared/core_conn.cpp index cea5a3ddc..10545e42c 100644 --- a/source/shared/core_conn.cpp +++ b/source/shared/core_conn.cpp @@ -718,14 +718,14 @@ bool core_is_conn_opt_value_escaped( _Inout_ const char* value, _Inout_ size_t v const char *pch = strchr(pstr, '}'); size_t i = 0; - + while (pch != NULL && i < value_len) { i = pch - pstr + 1; - + if (i == value_len || (i < value_len && pstr[i] != '}')) { return false; } - + i++; // skip the brace pch = strchr(pch + 2, '}'); // continue searching } @@ -783,7 +783,7 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou try { // Since connection options access token and authentication cannot coexist, check if both of them are used. - // If access token is specified, check UID and�PWD as well. + // If access token is specified, check UID and�PWD as well. // No need to check the keyword Trusted_Connection�because it is not among the acceptable options for SQLSRV drivers if (zend_hash_index_exists(options, SQLSRV_CONN_OPTION_ACCESS_TOKEN)) { bool invalidOptions = false; @@ -801,7 +801,7 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou access_token_used = true; } - // Check if Authentication is ActiveDirectoryMSI + // Check if Authentication is ActiveDirectoryMSI // https://docs.microsoft.com/en-ca/azure/active-directory/managed-identities-azure-resources/overview bool activeDirectoryMSI = false; if (authentication_option_used) { @@ -813,7 +813,7 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou if (!stricmp(option, AzureADOptions::AZURE_AUTH_AD_MSI)) { activeDirectoryMSI = true; - // There are two types of managed identities: + // There are two types of managed identities: // (1) A system-assigned managed identity: UID must be NULL // (2) A user-assigned managed identity: UID defined but must not be an empty string // In both cases, PWD must be NULL @@ -832,11 +832,11 @@ void build_connection_string_and_set_conn_attr( _Inout_ sqlsrv_conn* conn, _Inou } } } - + // Add the server name common_conn_str_append_func( ODBCConnOptions::SERVER, server, strnlen_s( server ), connection_string TSRMLS_CC ); - // If uid is not present then we use trusted connection -- but not when access token or ActiveDirectoryMSI is used, + // If uid is not present then we use trusted connection -- but not when access token or ActiveDirectoryMSI is used, // because they are incompatible if (!access_token_used && !activeDirectoryMSI) { if (uid == NULL || strnlen_s(uid) == 0) { @@ -1153,9 +1153,12 @@ void column_encryption_set_func::func( _In_ connection_option const* option, _In convert_to_string( value ); const char* value_str = Z_STRVAL_P( value ); - // Column Encryption is disabled by default unless it is explicitly 'Enabled' + // Column Encryption is disabled by default, but if it is present and not + // explicitly set to disabled or enabled, the ODBC driver will assume the + // user is providing an attestation protocol and URL for enclave support. + // For our purposes we need only set ce_option.enabled to true if not disabled. conn->ce_option.enabled = false; - if ( !stricmp(value_str, "enabled" )) { + if ( stricmp(value_str, "disabled" )) { conn->ce_option.enabled = true; } @@ -1200,7 +1203,7 @@ void ce_akv_str_set_func::func(_In_ connection_option const* option, _In_ zval* char *pValue = static_cast(sqlsrv_malloc(value_len + 1)); memcpy_s(pValue, value_len + 1, value_str, value_len); pValue[value_len] = '\0'; // this makes sure there will be no trailing garbage - + // This will free the existing memory block before assigning the new pointer -- the user might set the value(s) more than once if (option->conn_option_key == SQLSRV_CONN_OPTION_KEYSTORE_PRINCIPAL_ID) { conn->ce_option.akv_id = pValue; @@ -1262,10 +1265,10 @@ void access_token_set_func::func( _In_ connection_option const* option, _In_ zva } const char* value_str = Z_STRVAL_P( value ); - - // The SQL_COPT_SS_ACCESS_TOKEN pre-connection attribute allows the use of an access token (in the format extracted from - // an OAuth JSON response), obtained from Azure AD for authentication instead of username and password, and also - // bypasses the negotiation and obtaining of an access token by the driver. To use an access token, set the + + // The SQL_COPT_SS_ACCESS_TOKEN pre-connection attribute allows the use of an access token (in the format extracted from + // an OAuth JSON response), obtained from Azure AD for authentication instead of username and password, and also + // bypasses the negotiation and obtaining of an access token by the driver. To use an access token, set the // SQL_COPT_SS_ACCESS_TOKEN connection attribute to a pointer to an ACCESSTOKEN structure // // typedef struct AccessToken @@ -1276,30 +1279,30 @@ void access_token_set_func::func( _In_ connection_option const* option, _In_ zva // // NOTE: The ODBC Driver version 13.1 only supports this authentication on Windows. // - // A valid access token byte string must be expanded so that each byte is followed by a 0 padding byte, + // A valid access token byte string must be expanded so that each byte is followed by a 0 padding byte, // similar to a UCS-2 string containing only ASCII characters // // See https://docs.microsoft.com/sql/connect/odbc/using-azure-active-directory#authenticating-with-an-access-token size_t dataSize = 2 * value_len; - - sqlsrv_malloc_auto_ptr accToken; + + sqlsrv_malloc_auto_ptr accToken; accToken = reinterpret_cast(sqlsrv_malloc(sizeof(ACCESSTOKEN) + dataSize)); ACCESSTOKEN *pAccToken = accToken.get(); SQLSRV_ASSERT(pAccToken != NULL, "Something went wrong when trying to allocate memory for the access token."); pAccToken->dataSize = dataSize; - + // Expand access token with padding bytes for (size_t i = 0, j = 0; i < dataSize; i += 2, j++) { pAccToken->data[i] = value_str[j]; pAccToken->data[i+1] = 0; } - + core::SQLSetConnectAttr(conn, SQL_COPT_SS_ACCESS_TOKEN, reinterpret_cast(pAccToken), SQL_IS_POINTER); - - // Save the pointer because SQLDriverConnect() will use it to make connection to the server + + // Save the pointer because SQLDriverConnect() will use it to make connection to the server conn->azure_ad_access_token = pAccToken; accToken.transferred(); } diff --git a/test/functional/pdo_sqlsrv/AE_v2_values.inc b/test/functional/pdo_sqlsrv/AE_v2_values.inc new file mode 100644 index 000000000..721295b40 --- /dev/null +++ b/test/functional/pdo_sqlsrv/AE_v2_values.inc @@ -0,0 +1,163 @@ +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' => 0x44E4A, + 'varbinary' => 0xE4300FF, + 'varbinary(max)' => 0xD3EA762C78F, + '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:30', + ); + +// String patterns to test with LIKE +$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('0x44E4A'), + 'varbinary' => array('0xE4300FF'), + 'varbinary(max)' => array('0xD3EA762C78F'), + '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/functional/pdo_sqlsrv/MsSetup.inc b/test/functional/pdo_sqlsrv/MsSetup.inc index 823f283cc..1895f5aad 100644 --- a/test/functional/pdo_sqlsrv/MsSetup.inc +++ b/test/functional/pdo_sqlsrv/MsSetup.inc @@ -49,4 +49,6 @@ $AKVPassword = 'TARGET_AKV_PASSWORD'; // for use with KeyVaultPasswo $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/functional/pdo_sqlsrv/pdo_AE_functions.inc b/test/functional/pdo_sqlsrv/pdo_AE_functions.inc new file mode 100644 index 000000000..393554d95 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_AE_functions.inc @@ -0,0 +1,488 @@ +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); + $info = $stmt->fetch(); + if ($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"); + + 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 query can be used to both encrypt plaintext +// columns and to re-encrypt encrypted columns. +// 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) +{ + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + foreach ($dataTypes as $type) { + $insertValues[] = $testValues[$type][$v]; + $insertValues[] = $testValues[$type][$v]; + } + + // Insert the data using PDO::prepare() + try { + $stmt = $conn->prepare($insertQuery); + $stmt->execute($insertValues); + } catch (PDOException $error) { + print_r($error); + die("Inserting values in encrypted table failed\n"); + } + } +} + +// 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, $comparison='', $type='') +{ + try { + $nonAEstmt->execute(); + } catch(Exception $error) { + print_r($error); + die("Executing non-AE statement failed!\n"); + } + + try { + $AEstmt->execute(); + } catch(Exception $error) { + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r($error); + die("Equality comparison failed for deterministic encryption!\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!"); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + $e = $error->errorInfo; + die("Equality comparison failed for deterministic encryption!\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!"); + } + } 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!\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!\n"); + } + } else { + print_r($error); + die("Unexpected error occurred in compareResults!\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') { + // 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 +// string $encryptionType: Type of encryption, randomized or deterministic +// string $attestation: Type of attestation - 'correct', 'enabled', or 'wrongurl' +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, $attestation) +{ + 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); + + $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); + $AEstmt->bindParam(1, $thresholds[$type], $PDOType); + $nonAEstmt = $conn->prepare($nonAEQuery); + $nonAEstmt->bindParam(1, $thresholds[$type], $PDOType); + } catch (PDOException $error) { + print_r($error); + die("Preparing/binding statements for comparison failed"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $comparison, $type); + } + } +} + +// 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: Patterns to match against, 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', or 'wrongurl' +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation) +{ + foreach ($dataTypes as $type) { + + // TODO: Pattern matching doesn't work in AE for non-string types + // without an explicit cast + 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 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). + $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; + + 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"); + } + + compareResults($AEstmt, $nonAEstmt, $key, $encryptionType, $attestation, $pattern, $type); + } + } + } +} + +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, ["binary", "varbinary", "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 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/functional/pdo_sqlsrv/pdo_aev2_ce_enabled.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_ce_enabled.phpt new file mode 100644 index 000000000..2b603fe14 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_ce_enabled.phpt @@ -0,0 +1,93 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to 'enabled', which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $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. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with ColumnEncryption set to 'enabled'. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- +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!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + unset($conn); + + // Reconnect with ColumnEncryption set to 'enabled' + $newAttestation = 'enabled'; + $conn = connect($server, $newAttestation); + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'enabled'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'enabled'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $dataTypes, $targetKey, $targetType, $slength); + + try { + $stmt = $conn->query($alterQuery); + + // Query should fail and trigger catch block before getting here + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } catch (PDOException $error) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33546')); + } + } + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_encrypt_plaintext.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_encrypt_plaintext.phpt new file mode 100644 index 000000000..59d545d38 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_encrypt_plaintext.phpt @@ -0,0 +1,136 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $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 cycles through $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. Re-encrypt the table using new key and/or encryption type. +7. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +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 table failed when it shouldn't have!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + if ($count == 0) { + // 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); + $encryptionFailed = false; + + try { + $stmt = $conn->query($alterQuery); + if (!isEnclaveEnabled($key)) { + die("Encrypting should have failed with key $key and encryption type $encryptionType\n"); + } + } catch (PDOException $error) { + if (!isEnclaveEnabled($key)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r($error); + die("Encrypting failed when it shouldn't have!\n"); + } + } + } + } + + if ($encryptionFailed) continue; + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'correct'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + // Try re-encrypting the table + foreach ($splitDataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $encryptionFailed = false; + + try { + $stmt = $conn->query($alterQuery); + if (!isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } + } catch (Exception $error) { + if (!isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r($error); + die("Encrypting failed when it shouldn't have!\n"); + } + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_keywords.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_keywords.phpt new file mode 100644 index 000000000..c2fa226e3 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_keywords.phpt @@ -0,0 +1,60 @@ +--TEST-- +Test various settings for the ColumnEncryption keyword. +--DESCRIPTION-- +For AE v2, the Column Encryption keyword must be set to [protocol]/[attestation URL]. +If [protocol] is wrong, connection should fail; if the URL is wrong, connection +should succeed. This test sets ColumnEncryption to three values: +1. Random nonsense, which is interpreted as an incorrect protocol + so connection should fail. +2. Incorrect protocol with a correct attestation URL, connection should fail. +3. Correct protocol and incorrect URL, connection should succeed. +--SKIPIF-- + +--FILE-- +errorInfo; + checkErrors($e, array('CE400', '0')); +} + +// Test with incorrect protocol and good attestation URL. Connection should fail. +// Insert a rogue 'x' into the protocol part of the attestation. +$comma = strpos($attestation, ','); +$badProtocol = substr_replace($attestation, 'x', $comma, 0); +$options = "sqlsrv:Server=$server;database=$databaseName;ColumnEncryption=$badProtocol"; + +try { + $conn = new PDO($options, $uid, $pwd); + die("Connection should have failed!\n"); +} catch(Exception $error) { + $e = $error->errorInfo; + checkErrors($e, array('CE400', '0')); +} + +// Test with good protocol and incorrect attestation URL. Connection should succeed +// because the URL is only checked when an enclave computation is attempted. +$badURL = substr_replace($attestation, 'x', $comma+1, 0); +$options = "sqlsrv:Server=$server;database=$databaseName;ColumnEncryption=$badURL"; + +try { + $conn = new PDO($options, $uid, $pwd); +} catch(Exception $error) { + print_r($error); + die("Connecting with a bad attestation URL should have succeeded!\n"); +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_reencrypt_encrypted.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_reencrypt_encrypted.phpt new file mode 100644 index 000000000..c962ba3e6 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_reencrypt_encrypted.phpt @@ -0,0 +1,109 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $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. Re-encrypt the table using new key and/or encryption type. +6. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +query("DBCC FREEPROCCACHE"); + + // Create 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!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'correct'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + // 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); + $encryptionFailed = false; + foreach ($splitDataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + + try { + $stmt = $conn->query($alterQuery); + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $encryptionType\n"); + } + } catch (PDOException $error) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r($error); + die("Encrypting failed when it shouldn't have! key = $targetKey and type = $targetType\n"); + } + + continue; + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/pdo_aev2_wrong_attestation.phpt b/test/functional/pdo_sqlsrv/pdo_aev2_wrong_attestation.phpt new file mode 100644 index 000000000..da6708f20 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdo_aev2_wrong_attestation.phpt @@ -0,0 +1,95 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to the wrong attestation URL, which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $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. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with a faulty attestation URL. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- +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!\n"); + } + + insertValues($conn, $insertQuery, $dataTypes, $testValues); + unset($conn); + + // Reconnect with a faulty attestation URL + $comma = strpos($attestation, ','); + $newAttestation = substr_replace($attestation, 'x', $comma+1, 0); + + $conn = connect($server, $newAttestation); + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $key, $encryptionType, 'wrongurl'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'wrongurl'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $dataTypes, $targetKey, $targetType, $slength); + + try { + $stmt = $conn->query($alterQuery); + + // Query should fail and trigger catch block before getting here + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } catch(Exception $error) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = $error->errorInfo; + checkErrors($e, array('42000', '33543')); + } else { + $e = $error->errorInfo; + checkErrors($e, array('CE405', '0')); + } + } + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/pdo_sqlsrv/skipif_not_hgs.inc b/test/functional/pdo_sqlsrv/skipif_not_hgs.inc new file mode 100644 index 000000000..dd4614de8 --- /dev/null +++ b/test/functional/pdo_sqlsrv/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/functional/setup/AEV2Cert.pfx b/test/functional/setup/AEV2Cert.pfx new file mode 100644 index 000000000..4a9fc5bb8 Binary files /dev/null and b/test/functional/setup/AEV2Cert.pfx differ diff --git a/test/functional/setup/ae_keys.sql b/test/functional/setup/ae_keys.sql index 35c877209..d352a6f83 100644 --- a/test/functional/setup/ae_keys.sql +++ b/test/functional/setup/ae_keys.sql @@ -1,35 +1,98 @@ -/* DROP Column Encryption Key first, Column Master Key cannot be dropped until no encryption depends on it */ -IF EXISTS (SELECT * FROM sys.column_encryption_keys WHERE [name] LIKE '%AEColumnKey%') - +/* DROP Column Encryption Keys first, Column Master Keys cannot be dropped until no CEKs depend on them */ +IF EXISTS (SELECT * FROM sys.column_encryption_keys WHERE [name] LIKE '%AEColumnKey%' OR [name] LIKE '%-win-%') BEGIN DROP COLUMN ENCRYPTION KEY [AEColumnKey] +DROP COLUMN ENCRYPTION KEY [CEK-win-enclave] +DROP COLUMN ENCRYPTION KEY [CEK-win-enclave2] +DROP COLUMN ENCRYPTION KEY [CEK-win-noenclave] +DROP COLUMN ENCRYPTION KEY [CEK-win-noenclave2] END GO -/* Can finally drop Column Master Key after the Encryption Key is dropped */ -IF EXISTS (SELECT * FROM sys.column_master_keys WHERE [name] LIKE '%AEMasterKey%') - +/* Can finally drop Column Master Keys after the Column Encryption Keys are dropped */ +IF EXISTS (SELECT * FROM sys.column_master_keys WHERE [name] LIKE '%AEMasterKey%' OR [name] LIKE '%-win-%') BEGIN DROP COLUMN MASTER KEY [AEMasterKey] +DROP COLUMN MASTER KEY [CMK-win-enclave] +DROP COLUMN MASTER KEY [CMK-win-noenclave] END GO -/* Recreate the Column Master Key */ +/* Create the Column Master Keys */ +/* AKVMasterKey is a non-enclave enabled key for AE v1 testing */ +/* The enclave-enabled master key requires an ENCLAVE_COMPUTATIONS clause */ CREATE COLUMN MASTER KEY [AEMasterKey] WITH ( - KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', - KEY_PATH = N'CurrentUser/my/237F94738E7F5214D8588006C2269DBC6B370816' + KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', + KEY_PATH = N'CurrentUser/my/237F94738E7F5214D8588006C2269DBC6B370816' ) GO -/* Create Column Encryption Key using the Column Master Key */ +/* The enclave-enabled master key requires an ENCLAVE_COMPUTATIONS clause */ +CREATE COLUMN MASTER KEY [CMK-win-enclave] +WITH +( + KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', + KEY_PATH = N'CurrentUser/My/D9C0572FA54B221D6591C473BAEA53FE61AAC854', + ENCLAVE_COMPUTATIONS (SIGNATURE = 0xA1150DE565E9C132D2AAB8FF8B228EAA8DA804F250B5B422874CB608A3B274DDE523E71B655A3EFC6C3018B632701E9205BAD80C178614E1FE821C6807B0E70BCF11168FC4B202638905C5F016EDBADACA23C696B79772C56825F36EB8C0366B130C91D85362E560C9D2FDD20DCAE99619256045CA2725DEC9E0C115CAEB9EA686CCB0DE0D53D2056C01752B17B634FC6DBB51EA043F607349489722DB8A086CBC876649284A8352822DD22B328E7BA3D671CCDF54CDAAF61DFD6AF2EAAC14E03897324234AB103C45AB48131C1CD19040782359FC920A0AF61BA9842ADFB76C3196CBC6EB9C0A679926ED63E092B7C8643232C97A64C7F918104C210787A56F) +) +GO + +CREATE COLUMN MASTER KEY [CMK-win-noenclave] +WITH +( + KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE', + KEY_PATH = N'CurrentUser/My/D9C0572FA54B221D6591C473BAEA53FE61AAC854' +) +GO + +/* Now we can create the Column Encryption Keys */ /* ENCRYPTED_VALUE is generated by SSMS and it is always the same if the same Certificate is imported */ CREATE COLUMN ENCRYPTION KEY [AEColumnKey] WITH VALUES ( - COLUMN_MASTER_KEY = [AEMasterKey], - ALGORITHM = 'RSA_OAEP', - ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F00320033003700660039003400370033003800650037006600350032003100340064003800350038003800300030003600630032003200360039006400620063003600620033003700300038003100360039DE2397A08F6313E7820D75382D8469BE1C8F3CD47E3240A5A6D6F82D322F6EB1B103C9C47999A69FFB164D37E7891F60FFDB04ADEADEB990BE88AE488CAFB8774442DF909D2EF8BB5961A5C11B85BA7903E0E453B27B49CE0A30D14FF4F412B5737850A4C564B44C744E690E78FAECF007F9005E3E0FB4F8D6C13B016A6393B84BB3F83FEED397C4E003FF8C5BBDDC1F6156349A8B40EDC26398C9A03920DD81B9197BC83A7378F79ECB430A04B4CFDF3878B0219BB629F5B5BF3C2359A7498AD9A6F5D63EF15E060CDB10A65E6BF059C7A32237F0D9E00C8AC632CCDD68230774477D4F2E411A0E4D9B351E8BAA87793E64456370D91D4420B5FD9A252F6D9178AE3DD02E1ED57B7F7008114272419F505CBCEB109715A6C4331DEEB73653990A7140D7F83089B445C59E4858809D139658DC8B2781CB27A749F1CE349DC43238E1FBEAE0155BF2DBFEF6AFD9FD2BD1D14CEF9AC125523FD1120488F24416679A6041184A2719B0FC32B6C393FF64D353A3FA9BC4FA23DFDD999B0771A547B561D72B92A0B2BB8B266BC25191F2A0E2F8D93648F8750308DCD79BE55A2F8D5FBE9285265BEA66173CD5F5F21C22CC933AE2147F46D22BFF329F6A712B3D19A6488DDEB6FDAA5B136B29ADB0BA6B6D1FD6FBA5D6A14F76491CB000FEE4769D5B268A3BF50EA3FBA713040944558EDE99D38A5828E07B05236A4475DA27915E + COLUMN_MASTER_KEY = [AEMasterKey], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F00320033003700660039003400370033003800650037006600350032003100340064003800350038003800300030003600630032003200360039006400620063003600620033003700300038003100360039DE2397A08F6313E7820D75382D8469BE1C8F3CD47E3240A5A6D6F82D322F6EB1B103C9C47999A69FFB164D37E7891F60FFDB04ADEADEB990BE88AE488CAFB8774442DF909D2EF8BB5961A5C11B85BA7903E0E453B27B49CE0A30D14FF4F412B5737850A4C564B44C744E690E78FAECF007F9005E3E0FB4F8D6C13B016A6393B84BB3F83FEED397C4E003FF8C5BBDDC1F6156349A8B40EDC26398C9A03920DD81B9197BC83A7378F79ECB430A04B4CFDF3878B0219BB629F5B5BF3C2359A7498AD9A6F5D63EF15E060CDB10A65E6BF059C7A32237F0D9E00C8AC632CCDD68230774477D4F2E411A0E4D9B351E8BAA87793E64456370D91D4420B5FD9A252F6D9178AE3DD02E1ED57B7F7008114272419F505CBCEB109715A6C4331DEEB73653990A7140D7F83089B445C59E4858809D139658DC8B2781CB27A749F1CE349DC43238E1FBEAE0155BF2DBFEF6AFD9FD2BD1D14CEF9AC125523FD1120488F24416679A6041184A2719B0FC32B6C393FF64D353A3FA9BC4FA23DFDD999B0771A547B561D72B92A0B2BB8B266BC25191F2A0E2F8D93648F8750308DCD79BE55A2F8D5FBE9285265BEA66173CD5F5F21C22CC933AE2147F46D22BFF329F6A712B3D19A6488DDEB6FDAA5B136B29ADB0BA6B6D1FD6FBA5D6A14F76491CB000FEE4769D5B268A3BF50EA3FBA713040944558EDE99D38A5828E07B05236A4475DA27915E +) +GO + +/* There are two enclave enabled keys and two non-enclave enabled keys to test the case where a user + tries to reencrypt a table from one enclave enabled key to another enclave enabled key, or from a + non-enclave key to another non-enclave key */ +CREATE COLUMN ENCRYPTION KEY [CEK-win-enclave] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-enclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F0064003900630030003500370032006600610035003400620032003200310064003600350039003100630034003700330062006100650061003500330066006500360031006100610063003800350034007382EDDDE3FFCE076D5715B6BBBD22EA64E665899BEFAAD5B329F218EE30BE9F789EB98717B6FD9E50AE496AC9FEED962B23442D4FD3FBFEC9C9B65F40A3BCEC7CFAC198F4CAEE8A255F67988289EF050F9F75D0287F3DF9A9FDA0C674E48DF2CB13298AAAD039930DD909EEE71682CC8A90202D3F2A1F1037BB20B1954C8B6A11F05D104CA9DAF1561C6B2F9DBB08BCE17244157B751C02FC1730E387F372C31327F2834D19AF626D0B46B152615F05FA2F3566350312CDE6DE1160B3C1D0FD35FAF13891C04711DF184DA501AA51D16BF009EA71A2D28E201804C6F8F9100E90234923B2713EA7988861FBA4E292E5518FFC02CCBD2513EDA871F6E03ECDDD309619557277C10A07906E55BA3F59A6A18834B4CD5185DA4B4574A18B8B1AC53A2C36B033D7A72443F1438E76E37306A1F92AC30BC751F6D7ED1633FEE807440E1D6096C53C5E3E33828C9C59E8761E5BAD341C6D9E2BD1F2B5C3992666620CAA38C4645C154976EF62AE80161A9F7700C96875A72995E1C585918B28F65060F1B8B96417328F6DEDFCA79ED9F01EAB19FF4E3163F9963BA26E9B58031A04320CC73702A6ED438513E0F8ABA1966B53114038CC587050F90D9CD0F9E26CA9749723ABA85CF31F963A5E85E04993B2B2869725E734BE8FCFD30A801825582730B49C00A2058C02D3312D6D8E82078FF4F77C5FF9CE6E9D140F1A4517635AB784 ) -GO \ No newline at end of file +GO + +CREATE COLUMN ENCRYPTION KEY [CEK-win-enclave2] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-enclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F0064003900630030003500370032006600610035003400620032003200310064003600350039003100630034003700330062006100650061003500330066006500360031006100610063003800350034006B4D40ABF0975AF7C5CA7D1F4345DE437318556F5A2380DCFE4AB792DC3A424EABC80EA24EE850FACD94F04809C8B32674C6FF2D966FA7F9F9E522990E5F5011515BA4B7EF3603619D8A4BF46AA9B769A8A4417462C4B0303F995F04964A2E328A503D87CD1AB85ECFCB8241D0C815540989DC33E58EDCCBAFF0753E196813E3FCCC5A3C9E4277DD528AE276F1F795973A4DF8D1BB3B1F405B5F35A6A583F0BB86BAD7FCADC1FCF6B14B602890109360FAB67D6A27DE542AE87784C40FEB9071AC34C4C40C92A6C153A4A38B6DA3AD48ED39E32D6D161ACE7EFE516B414139A831D878C13FF178649823C4EFDC8E5DB4C02F2147CC76965C01C2F3624EB809FD4F5C2E291056077B1ABEFF1F5001C1F4248704C7C70CF63DA1EBC2FEC4A3DF919BA4F6B465819BC4587599C2E7499CDE62D7C335CE7BBCFC72242A8F41C1B5C94DEB0A9AF49B723759A8CD9751EE70DDEBAFA1957382287F621790543841EBCCA0007BA030CAF29E9FBF8CEB4FEC88673F47B5EC3B5F759BBDD8ED2EAF572711D78286E4294B89FF6EBFEE4968B4596AF3B5C34985F28E886F6C211F385326F10ED62602007589FC494372902FB32B0E3D67A8C64F43A87B06EE9F2CF074EB6F3EC7A431733EDA8745051B7A4AA4C020797A9492E6A3BA643D031E491497BF17539993871085AC249D0AD82203CD442F69D6C686D26F4D17BA46B69D3CB7E395 +) +GO + +CREATE COLUMN ENCRYPTION KEY [CEK-win-noenclave] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-noenclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F00640039006300300035003700320066006100350034006200320032003100640036003500390031006300340037003300620061006500610035003300660065003600310061006100630038003500340042DC7A3AAAD184E01288C0913EFB6FEC6167CD8EA08A5F46ADCCC34D3AA6A1BDDDA15EA3DD219ED8795AB05C0111E48EA35A82ADDF2A206FACBBF4FD73D01C004DF627012D3950FEBCA4BBEDBDF97BA77033728D8873BA81E1C7BDCBE04BB3AA7EB42A1EDDBEF9B1CA9477ADA33F76711FEDF782CA1BD3C0104FDEB9E0D66DFCEC7D3C236906481B44F04457549658635322447742FB00B6D6F36A7CFCC56BB39F7280736BC25FD499F9CBA2F63CE11D53E536FD4A266929E06CF2BDBAF229894A77EDE140323B674ECF28C58C3E0B6C2E9407AD1A26776CB55D68B8286F64787CE5A468CFA27295D6069EFA5D65CD9A04602E861F4504F2611AAE6A8ADE33038A2BECE8BD7CF5B48567C217E324F11935C552FD25FE1FEFB152684BD1B3F8EB70EC9F6439340CE82CD8E74DD5986A6C4F9E8336ED4AC804FAD800A3EA324F78DCE37832035C3DC92782A06150916D01322A80767D1A36D7A8D9BCF6727DCE6AC67A168FA8B8B5032E60DCB178B21A860F2D98BE09DA9BA5DCCBD0D339369FF3C50C7993463372CF5B1DA9FAA12CD16E76F5961C01EADC5804C7F22227E2095BAD0F90A47B6330B1B43407E01DE5B61CEBD542A93797428AD84376E9362EADE6DDD103B9EC96E616A2ECED7D1D665B5B872E77FC024AD92AB4A8335D12D41BDD152790E87590798C1005956F9F92D4DD0C1C9852D147F7CB55B3224DE8EF593F +) +GO + +CREATE COLUMN ENCRYPTION KEY [CEK-win-noenclave2] +WITH VALUES +( + COLUMN_MASTER_KEY = [CMK-win-noenclave], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F0064003900630030003500370032006600610035003400620032003200310064003600350039003100630034003700330062006100650061003500330066006500360031006100610063003800350034009014CD16FC878CEA2DE91C8C681AE86C7C062D8BD88C4CEE501A89FEAC47356D7181644A350F72B5F6023DA2B9E26C5A2522C08B1910D390068CF26794F4BA7B0298A6676B4DC6DED913E3B077B56224D2E1A3FE4EF33F58FE44CFC3DD67E54FB15BE8E29ABAF8357F378FBEDA3EBF9868A54746074D5E0E798047867E1ABD39AD0645BB8E071C72BFC37C007CBFC58F5690A5253F444E77169B2FE92FD95897A412B2078DA3804A00723D6DF824FCA527208A1DFB377B5BA16B620213F8252E10E7D7A3719A3FBB2F7A8189792B0BCF737236963C7DDCA6366F7B04F127925A1F8DDBB1B5A01D280BD300ECA3B1F31F24C8A0D517AE7BCBC3233A24E83B70A334754098DE373A1C027A4D09BB1D26C930E7501EB02464C519D19CFA0B296238AF11638C2E0688C7599E3DB1714AACF4EBFCEF63E1EE521A8E38E3BEFD4EF4991A15E8DD5CFD94E58E68754F3E90BC117025C01562F6440417A42612BE9C8871A18108CBE3E96DA7E35C45171C03E1DFBB3CA1E35A6D322F2D5B79E2BF2A07F14136DA4A768E08E2A7F1A42E04B717CB6AE3D1A3FA0EACCFC9CEC27DB53761E13DE1F55B410A65FB441D50CF8B2153B64925B1CEBDE062B5CAF4C99C41FED6836327037C46515710F16DC611305A0EBA1943A9BA5CC6889626990879713E9C95BB54D6A8A3C1C05A10AFE142B2487A1F0A07B57841E940CC9816E3F43CAE3CB7 +) +GO diff --git a/test/functional/setup/setup_dbs.py b/test/functional/setup/setup_dbs.py index 58900526d..ead94a30d 100644 --- a/test/functional/setup/setup_dbs.py +++ b/test/functional/setup/setup_dbs.py @@ -31,6 +31,8 @@ def setupAE(conn_options, dbname): # import self signed certificate inst_command = "certutil -user -p '' -importPFX My PHPcert.pfx NoRoot" executeCommmand(inst_command) + inst_command = "certutil -user -p '' -importPFX My AEV2Cert.pfx NoRoot" + executeCommmand(inst_command) # create Column Master Key and Column Encryption Key script_command = 'sqlcmd -I ' + conn_options + ' -i ae_keys.sql -d ' + dbname executeCommmand(script_command) diff --git a/test/functional/sqlsrv/AE_v2_values.inc b/test/functional/sqlsrv/AE_v2_values.inc new file mode 100644 index 000000000..721295b40 --- /dev/null +++ b/test/functional/sqlsrv/AE_v2_values.inc @@ -0,0 +1,163 @@ +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' => 0x44E4A, + 'varbinary' => 0xE4300FF, + 'varbinary(max)' => 0xD3EA762C78F, + '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:30', + ); + +// String patterns to test with LIKE +$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('0x44E4A'), + 'varbinary' => array('0xE4300FF'), + 'varbinary(max)' => array('0xD3EA762C78F'), + '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/functional/sqlsrv/MsSetup.inc b/test/functional/sqlsrv/MsSetup.inc index aec2b4bb1..8335c13b7 100644 --- a/test/functional/sqlsrv/MsSetup.inc +++ b/test/functional/sqlsrv/MsSetup.inc @@ -53,4 +53,6 @@ $AKVPassword = 'TARGET_AKV_PASSWORD'; // for use with KeyVaultPasswo $AKVClientID = 'TARGET_AKV_CLIENT_ID'; // for use with KeyVaultClientSecret $AKVSecret = 'TARGET_AKV_CLIENT_SECRET'; // for use with KeyVaultClientSecret +// for enclave computations +$attestation = 'TARGET_ATTESTATION'; ?> diff --git a/test/functional/sqlsrv/skipif_not_hgs.inc b/test/functional/sqlsrv/skipif_not_hgs.inc new file mode 100644 index 000000000..7d7b3ca1d --- /dev/null +++ b/test/functional/sqlsrv/skipif_not_hgs.inc @@ -0,0 +1,36 @@ +$userName, "PWD"=>$userPassword, "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/functional/sqlsrv/sqlsrv_AE_functions.inc b/test/functional/sqlsrv/sqlsrv_AE_functions.inc new file mode 100644 index 000000000..f7ef4e89f --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_AE_functions.inc @@ -0,0 +1,518 @@ +$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'CharacterSet'=>'UTF-8', + 'ColumnEncryption'=>$attestation_info, + ); + + if ($keystore == 'akv') { + if ($AKVKeyStoreAuthentication == 'KeyVaultPassword') { + $security_info = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVPrincipalName, + 'KeyStoreSecret'=>$AKVPassword, + ); + } elseif ($AKVKeyStoreAuthentication == 'KeyVaultClientSecret') { + $security_info = array('KeyStoreAuthentication'=>$AKVKeyStoreAuthentication, + 'KeyStorePrincipalId'=>$AKVClientID, + 'KeyStoreSecret'=>$AKVSecret, + ); + } else { + die("Incorrect value for KeyStoreAuthentication keyword!\n"); + } + + $options = array_merge($options, $security_info); + } + + $conn = sqlsrv_connect($server, $options); + if (!$conn) { + echo "Connection failed\n"; + print_r(sqlsrv_errors()); + } + + // 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); + $info = sqlsrv_fetch_array($stmt); + if ($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) +{ + for ($v = 0; $v < sizeof($testValues['bigint']); ++$v) { + $insertValues = array(); + + // two copies of each value for the two columns for each data type + foreach ($dataTypes as $type) { + $insertValues[] = $testValues[$type][$v]; + $insertValues[] = $testValues[$type][$v]; + } + + // Insert the data using sqlsrv_prepare() + $stmt = sqlsrv_prepare($conn, $insertQuery, $insertValues); + if ($stmt == false) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at prepare\n"); + } + + if (sqlsrv_execute($stmt) == false) { + print_r(sqlsrv_errors()); + die("Inserting values in encrypted table failed at execute\n"); + } + } +} + +// 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, $comparison='', $type='') +{ + if (!sqlsrv_execute($nonAEstmt)) { + print_r(sqlsrv_errors()); + die("Executing non-AE statement failed!\n"); + } + + if(!sqlsrv_execute($AEstmt)) { + if ($attestation == 'enabled') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption!\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')); + } + } elseif ($attestation == 'wrongurl') { + if ($encryptionType == 'Deterministic') { + if ($comparison == '=') { + print_r(sqlsrv_errors()); + die("Equality comparison failed for deterministic encryption!\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')); + } + } 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!\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!\n"); + } + } else { + print_r(sqlsrv_errors()); + die("Unexpected error occurred in compareResults!\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') { + 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: Thable 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' +function testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, $attestation) +{ + 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 = $unicode ? SQLSRV_PHPTYPE_STRING('UTF-8') : null; + + $param = array(array($thresholds[$type], 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, $comparison, $type); + } + } +} + +// 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 +function testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, $attestation) +{ + // TODO: Pattern matching doesn't work in AE for non-string types + // without an explicit cast + foreach ($dataTypes as $type) { + 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; + + $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, $pattern, $type); + } + } + } +} + +// 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, ["binary", "varbinary", "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 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": + case "varbinary": + case "varbinary(max)": + // Using a binary type here produces a 'Restricted data type attribute violation' + return SQLSRV_SQLTYPE_BIGINT; + default: + die("Case is missing for $type type in getSQLType.\n"); + } +} + +?> diff --git a/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt b/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt index 59e184478..c492a961b 100644 --- a/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt +++ b/test/functional/sqlsrv/sqlsrv_ae_fetch_phptypes.phpt @@ -15,7 +15,6 @@ function formulateSetupQuery($tableName, &$dataTypes, &$columns, &$insertQuery) { $columns = array(); $queryTypes = "("; - $queryTypesAE = "("; $valuesString = "VALUES ("; $numTypes = sizeof($dataTypes); diff --git a/test/functional/sqlsrv/sqlsrv_aev2_ce_enabled.phpt b/test/functional/sqlsrv/sqlsrv_aev2_ce_enabled.phpt new file mode 100644 index 000000000..186e93492 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_ce_enabled.phpt @@ -0,0 +1,113 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to 'enabled', which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $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. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with ColumnEncryption set to 'enabled'. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- +2000 characters) + $splitDataTypes = array_chunk($dataTypes, 5); + $encryptionFailed = false; + + foreach ($splitDataTypes as $split) { + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $stmt = sqlsrv_query($conn, $alterQuery); + + if(!$stmt) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33546')); + $encryptionFailed = true; + continue; + } + + continue; + } else { + die("Encrypting should have failed with key $targetKey and encryption type $encryptionType!\n"); + } + } + + if ($encryptionFailed) { + continue; + } + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt b/test/functional/sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt new file mode 100644 index 000000000..b8daaacf3 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_encrypt_plaintext.phpt @@ -0,0 +1,138 @@ +--TEST-- +Test rich computations and in-place encryption of plaintext with AE v2. +--DESCRIPTION-- +This test cycles through $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 cycles through $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. Re-encrypt the table using new key and/or encryption type. +7. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +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); + + $stmt = sqlsrv_query($conn, $alterQuery); + $encryptionFailed = false; + + if(!$stmt) { + if (!isEnclaveEnabled($key)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r(sqlsrv_errors()); + die("Encrypting failed when it shouldn't have!\n"); + } + } else { + if (!isEnclaveEnabled($key)) { + die("Encrypting should have failed with key $key and encryption type $encryptionType\n"); + } + } + } + } + + if ($encryptionFailed) continue; + + if ($count == 0) { + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $key, $encryptionType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $key, $encryptionType, 'correct'); + } + ++$count; + + if ($key == $targetKey and $encryptionType == $targetType) { + continue; + } + + // Try re-encrypting the table + $encryptionFailed = false; + foreach ($splitDataTypes as $split) { + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + + $stmt = sqlsrv_query($conn, $alterQuery); + if(!$stmt) { + if (!isEnclaveEnabled($targetKey)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r(sqlsrv_errors()); + die("Encrypting failed when it shouldn't have!\n"); + } + } else { + if (!isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $targetType\n"); + } + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_keywords.phpt b/test/functional/sqlsrv/sqlsrv_aev2_keywords.phpt new file mode 100644 index 000000000..d236a2a4f --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_keywords.phpt @@ -0,0 +1,71 @@ +--TEST-- +Test various settings for the ColumnEncryption keyword. +--DESCRIPTION-- +For AE v2, the Column Encryption keyword must be set to [protocol]/[attestation URL]. +If [protocol] is wrong, connection should fail; if the URL is wrong, connection +should succeed. This test sets ColumnEncryption to three values: +1. Random nonsense, which is interpreted as an incorrect protocol + so connection should fail. +2. Incorrect protocol with a correct attestation URL, connection should fail. +3. Correct protocol and incorrect URL, connection should succeed. +--SKIPIF-- + +--FILE-- +$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'ColumnEncryption'=>"xyz", + ); + +$conn = sqlsrv_connect($server, $options); +if (!$conn) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE400', '0')); +} else { + die("Connecting with nonsense should have failed!\n"); +} + +// Test with incorrect protocol and good attestation URL. Connection should fail. +// Insert a rogue 'x' into the protocol part of the attestation. +$comma = strpos($attestation, ','); +$badProtocol = substr_replace($attestation, 'x', $comma, 0); +$options = array('database'=>$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'ColumnEncryption'=>$badProtocol, + ); + +$conn = sqlsrv_connect($server, $options); +if (!$conn) { + $e = sqlsrv_errors(); + checkErrors($e, array('CE400', '0')); +} else { + die("Connecting with a bad attestation protocol should have failed!\n"); +} + +// Test with good protocol and incorrect attestation URL. Connection should succeed +// because the URL is only checked when an enclave computation is attempted. +$badURL = substr_replace($attestation, 'x', $comma+1, 0); +$options = array('database'=>$database, + 'uid'=>$userName, + 'pwd'=>$userPassword, + 'ColumnEncryption'=>$badURL, + ); + +$conn = sqlsrv_connect($server, $options); +if (!$conn) { + print_r(sqlsrv_errors()); + die("Connecting with a bad attestation URL should have succeeded!\n"); +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_reencrypt_encrypted.phpt b/test/functional/sqlsrv/sqlsrv_aev2_reencrypt_encrypted.phpt new file mode 100644 index 000000000..002f61ac7 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_reencrypt_encrypted.phpt @@ -0,0 +1,110 @@ +--TEST-- +Test rich computations and in place re-encryption with AE v2. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $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. Re-encrypt the table using new key and/or encryption type. +6. Compare computations as in 4. above. +--SKIPIF-- + +--FILE-- +2000 characters) + // TODO: This is a known issue, follow up on it. + $splitDataTypes = array_chunk($dataTypes, 5); + $encryptionFailed = false; + + foreach ($splitDataTypes as $split) { + + $alterQuery = constructAlterQuery($tableName, $colNamesAE, $split, $targetKey, $targetType, $slength); + $stmt = sqlsrv_query($conn, $alterQuery); + + if(!$stmt) { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + $e = sqlsrv_errors(); + checkErrors($e, array('42000', '33543')); + $encryptionFailed = true; + continue; + } else { + print_r(sqlsrv_errors()); + die("Encrypting failed when it shouldn't have! key = $targetKey and type = $targetType\n"); + } + + continue; + } else { + if (!isEnclaveEnabled($key) or !isEnclaveEnabled($targetKey)) { + die("Encrypting should have failed with key $targetKey and encryption type $encryptionType\n"); + } + } + } + + if ($encryptionFailed) { + continue; + } + + testCompare($conn, $tableName, $comparisons, $dataTypes, $colNames, $thresholds, $length, $targetKey, $targetType, 'correct'); + testPatternMatch($conn, $tableName, $patterns, $dataTypes, $colNames, $targetKey, $targetType, 'correct'); + } + } + } +} + +echo "Done.\n"; + +?> +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/sqlsrv_aev2_wrong_attestation.phpt b/test/functional/sqlsrv/sqlsrv_aev2_wrong_attestation.phpt new file mode 100644 index 000000000..a42cabaf4 --- /dev/null +++ b/test/functional/sqlsrv/sqlsrv_aev2_wrong_attestation.phpt @@ -0,0 +1,93 @@ +--TEST-- +Try re-encrypting a table with ColumnEncryption set to the wrong attestation URL, which should fail. +--DESCRIPTION-- +This test cycles through $encryptionTypes and $keys, creating an encrypted table +each time, then cycles through $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. Connect with correct attestation information. +2. Create an encrypted table with two columns for each AE-supported data type, one encrypted and one not encrypted. +3. Insert some data. +4. Disconnect and reconnect with a faulty attestation URL. +5. Test comparison and pattern matching by comparing the results for the encrypted and non-encrypted columns. + Equality should work with deterministic encryption as in AE v1, but other computations should fail. +6. Try re-encrypting the table. This should fail. +--SKIPIF-- + +--FILE-- + +--EXPECT-- +Done. diff --git a/test/functional/sqlsrv/test_ae_keys_setup.phpt b/test/functional/sqlsrv/test_ae_keys_setup.phpt index 53a2c0fa6..d2e3850d7 100644 --- a/test/functional/sqlsrv/test_ae_keys_setup.phpt +++ b/test/functional/sqlsrv/test_ae_keys_setup.phpt @@ -1,8 +1,8 @@ --TEST-- Test the existence of Windows Always Encrypted keys generated in the database setup --DESCRIPTION-- -This test iterates through the rows of sys.column_master_keys and/or -sys.column_encryption_keys to look for the specific column master key and +This test iterates through the rows of sys.column_master_keys and/or +sys.column_encryption_keys to look for the specific column master key and column encryption key generated in the database setup --SKIPIF-- @@ -44,8 +44,8 @@ if (AE\IsQualified($conn)) { sqlsrv_free_stmt($stmt); } -echo "Test Successfully done.\n"; +echo "Test successfully done.\n"; sqlsrv_close($conn); ?> --EXPECT-- -Test Successfully done. +Test successfully done.