Support for Azure Key Vault

This commit is contained in:
David Puglielli 2018-05-05 17:08:01 -07:00
parent 440636aff8
commit 977e5309dd
13 changed files with 1755 additions and 25 deletions

View file

@ -40,17 +40,20 @@ const char Server[] = "Server";
const char APP[] = "APP";
const char ApplicationIntent[] = "ApplicationIntent";
const char AttachDBFileName[] = "AttachDbFileName";
const char ConnectionPooling[] = "ConnectionPooling";
const char Authentication[] = "Authentication";
const char Driver[] = "Driver";
#ifdef _WIN32
const char ColumnEncryption[] = "ColumnEncryption";
const char ConnectionPooling[] = "ConnectionPooling";
#ifdef _WIN32
const char ConnectRetryCount[] = "ConnectRetryCount";
const char ConnectRetryInterval[] = "ConnectRetryInterval";
#endif // _WIN32
const char Database[] = "Database";
const char Driver[] = "Driver";
const char Encrypt[] = "Encrypt";
const char Failover_Partner[] = "Failover_Partner";
const char KeyStoreAuthentication[] = "KeyStoreAuthentication";
const char KeyStorePrincipalId[] = "KeyStorePrincipalId";
const char KeyStoreSecret[] = "KeyStoreSecret";
const char LoginTimeout[] = "LoginTimeout";
const char MARS_Option[] = "MultipleActiveResultSets";
const char MultiSubnetFailover[] = "MultiSubnetFailover";
@ -231,7 +234,6 @@ const connection_option PDO_CONN_OPTS[] = {
CONN_ATTR_STRING,
driver_set_func::func
},
#ifdef _WIN32
{
PDOConnOptionNames::ColumnEncryption,
sizeof(PDOConnOptionNames::ColumnEncryption),
@ -241,6 +243,7 @@ const connection_option PDO_CONN_OPTS[] = {
CONN_ATTR_STRING,
column_encryption_set_func::func
},
#ifdef _WIN32
{
PDOConnOptionNames::ConnectRetryCount,
sizeof( PDOConnOptionNames::ConnectRetryCount ),
@ -287,6 +290,33 @@ const connection_option PDO_CONN_OPTS[] = {
CONN_ATTR_STRING,
conn_str_append_func::func
},
{
PDOConnOptionNames::KeyStoreAuthentication,
sizeof( PDOConnOptionNames::KeyStoreAuthentication ),
SQLSRV_CONN_OPTION_KEYSTORE_AUTHENTICATION,
ODBCConnOptions::KeyStoreAuthentication,
sizeof( ODBCConnOptions::KeyStoreAuthentication ),
CONN_ATTR_STRING,
ce_akv_str_set_func::func
},
{
PDOConnOptionNames::KeyStorePrincipalId,
sizeof( PDOConnOptionNames::KeyStorePrincipalId ),
SQLSRV_CONN_OPTION_KEYSTORE_PRINCIPAL_ID,
ODBCConnOptions::KeyStorePrincipalId,
sizeof( ODBCConnOptions::KeyStorePrincipalId ),
CONN_ATTR_STRING,
ce_akv_str_set_func::func
},
{
PDOConnOptionNames::KeyStoreSecret,
sizeof( PDOConnOptionNames::KeyStoreSecret ),
SQLSRV_CONN_OPTION_KEYSTORE_SECRET,
ODBCConnOptions::KeyStoreSecret,
sizeof( ODBCConnOptions::KeyStoreSecret ),
CONN_ATTR_STRING,
ce_akv_str_set_func::func
},
{
PDOConnOptionNames::LoginTimeout,
sizeof( PDOConnOptionNames::LoginTimeout ),
@ -362,7 +392,7 @@ const connection_option PDO_CONN_OPTS[] = {
{
PDOConnOptionNames::TransparentNetworkIPResolution,
sizeof(PDOConnOptionNames::TransparentNetworkIPResolution),
SQLSRV_CONN_OPTION_TRANSPARANT_NETWORK_IP_RESOLUTION,
SQLSRV_CONN_OPTION_TRANSPARENT_NETWORK_IP_RESOLUTION,
ODBCConnOptions::TransparentNetworkIPResolution,
sizeof(ODBCConnOptions::TransparentNetworkIPResolution),
CONN_ATTR_STRING,

View file

@ -189,7 +189,7 @@ void conn_string_parser::add_key_value_pair( _In_reads_(len) const char* value,
if( !valid ) {
THROW_PDO_ERROR( this->ctx, PDO_SQLSRV_ERROR_INVALID_AUTHENTICATION_OPTION, this->current_key_name );
}
string_parser::add_key_value_pair( value, len );
}

View file

@ -409,6 +409,22 @@ pdo_error PDO_ERRORS[] = {
SQLSRV_ERROR_ENCRYPTED_STREAM_FETCH,
{ IMSSP, (SQLCHAR*) "Connection with Column Encryption enabled does not support fetching stream. Please fetch the data as a string.", -84, false }
},
{
SQLSRV_ERROR_INVALID_AKV_AUTHENTICATION_OPTION,
{ IMSSP, (SQLCHAR*) "Invalid option for the KeyStoreAuthentication keyword. Only KeyVaultPassword or KeyVaultClientSecret is allowed.", -85, false }
},
{
SQLSRV_ERROR_AKV_AUTH_MISSING,
{ IMSSP, (SQLCHAR*) "Authentication method for Azure Key Vault is missing. KeyStoreAuthentication must be set to KeyVaultPassword or KeyVaultClientSecret.", -86, false }
},
{
SQLSRV_ERROR_AKV_NAME_MISSING,
{ IMSSP, (SQLCHAR*) "ID for Azure Key Vault is missing. A username or client Id is required.", -87, false }
},
{
SQLSRV_ERROR_AKV_SECRET_MISSING,
{ IMSSP, (SQLCHAR*) "Secret for Azure Key Vault is missing. A password or client secret is required.", -88, false }
},
{ UINT_MAX, {} }
};

View file

@ -71,6 +71,9 @@ const char* get_processor_arch( void );
void get_server_version( _Inout_ sqlsrv_conn* conn, _Outptr_result_buffer_(len) char** server_version, _Out_ SQLSMALLINT& len TSRMLS_DC );
connection_option const* get_connection_option( sqlsrv_conn* conn, _In_ const char* key, _In_ SQLULEN key_len TSRMLS_DC );
void common_conn_str_append_func( _In_z_ const char* odbc_name, _In_reads_(val_len) const char* val, _Inout_ size_t val_len, _Inout_ std::string& conn_str TSRMLS_DC );
void load_azure_key_vault( _Inout_ sqlsrv_conn* conn TSRMLS_DC );
void configure_azure_key_vault( sqlsrv_conn* conn, BYTE config_attr, const DWORD config_value, size_t key_size);
void configure_azure_key_vault( sqlsrv_conn* conn, BYTE config_attr, const char* config_value, size_t key_size);
}
// core_sqlsrv_connect
@ -128,13 +131,13 @@ sqlsrv_conn* core_sqlsrv_connect( _In_ sqlsrv_context& henv_cp, _In_ sqlsrv_cont
// we do this earlier because we have to allocate the connection handle prior to setting attributes on
// it in build_connection_string_and_set_conn_attr.
if( options_ht && zend_hash_num_elements( options_ht ) > 0 ) {
zval* option_z = NULL;
option_z = zend_hash_index_find( options_ht, SQLSRV_CONN_OPTION_CONN_POOLING );
if ( option_z ) {
// if the option was found and it's not true, then use the non pooled environment handle
if(( Z_TYPE_P( option_z ) == IS_STRING && !core_str_zval_is_true( option_z )) || !zend_is_true( option_z ) ) {
if( options_ht && zend_hash_num_elements( options_ht ) > 0 ) {
zval* option_z = NULL;
option_z = zend_hash_index_find( options_ht, SQLSRV_CONN_OPTION_CONN_POOLING );
if ( option_z ) {
// if the option was found and it's not true, then use the non pooled environment handle
if(( Z_TYPE_P( option_z ) == IS_STRING && !core_str_zval_is_true( option_z )) || !zend_is_true( option_z ) ) {
henv = &henv_ncp;
is_pooled = false;
}
@ -245,6 +248,8 @@ sqlsrv_conn* core_sqlsrv_connect( _In_ sqlsrv_context& henv_cp, _In_ sqlsrv_cont
throw core::CoreException();
}
load_azure_key_vault( conn );
// determine the version of the server we're connected to. The server version is left in the
// connection upon return.
//
@ -932,6 +937,90 @@ void determine_server_version( _Inout_ sqlsrv_conn* conn TSRMLS_DC )
conn->server_version = version_major;
}
void load_azure_key_vault( _Inout_ sqlsrv_conn* conn TSRMLS_DC )
{
// If column encryption is not enabled simply do nothing. Otherwise, check if a custom keystore provider
// is required for encryption or decryption. Note, in order to load and configure a custom keystore provider,
// all KSP fields in conn->ce_option must be defined.
if ( ! conn->ce_option.enabled || ! conn->ce_option.akv_required )
return;
CHECK_CUSTOM_ERROR( conn->ce_option.akv_auth == NULL || Z_STRLEN_P(conn->ce_option.akv_auth) <= 0, conn, SQLSRV_ERROR_AKV_AUTH_MISSING) {
throw core::CoreException();
}
CHECK_CUSTOM_ERROR( conn->ce_option.akv_id == NULL || Z_STRLEN_P(conn->ce_option.akv_id) <= 0, conn, SQLSRV_ERROR_AKV_NAME_MISSING) {
throw core::CoreException();
}
CHECK_CUSTOM_ERROR( conn->ce_option.akv_secret == NULL || Z_STRLEN_P(conn->ce_option.akv_secret) <= 0, conn, SQLSRV_ERROR_AKV_SECRET_MISSING) {
throw core::CoreException();
}
char *akv_auth = Z_STRVAL_P( conn->ce_option.akv_auth );
char *akv_id = Z_STRVAL_P( conn->ce_option.akv_id );
char *akv_secret = Z_STRVAL_P( conn->ce_option.akv_secret );
unsigned int id_len = static_cast<unsigned int>( Z_STRLEN_P( conn->ce_option.akv_id ));
unsigned int key_size = static_cast<unsigned int>( Z_STRLEN_P( conn->ce_option.akv_secret ));
//sqlsrv_malloc_auto_ptr<unsigned char> akv_data;
//akv_data = reinterpret_cast<unsigned char*>( sqlsrv_malloc( sizeof( CEKEYSTOREDATA ) + key_size ));
//CEKEYSTOREDATA *pAKV = reinterpret_cast<CEKEYSTOREDATA*>( akv_data.get() );
//pAKV->dataSize = key_size;
// unsigned int wid_len = 0;
// sqlsrv_malloc_auto_ptr<SQLWCHAR> wakv_id;
// wakv_id = utf16_string_from_mbcs_string( SQLSRV_ENCODING_UTF8, akv_id, id_len, &wid_len );
// CHECK_CUSTOM_ERROR( wakv_id == 0, conn, SQLSRV_ERROR_CONNECT_STRING_ENCODING_TRANSLATE ) {
// throw core::CoreException();
// }
//pAKV->name = L"AZURE_KEY_VAULT";(wchar_t *) wakv_id.get();
// Next, extract the character string from conn->ce_option.ksp_encrypt_key into encrypt_key
//char* akv_secret = Z_STRVAL_P( conn->ce_option.akv_secret );
//memcpy_s( pAKV->data, key_size * sizeof( char ) , encrypt_key, key_size );
if ( !stricmp(akv_auth, "KeyVaultPassword") )
{
configure_azure_key_vault( conn, AKV_CONFIG_FLAGS, AKVCFG_AUTHMODE_PASSWORD, 0 );
}
else if ( !stricmp(akv_auth, "KeyVaultClientSecret") )
{
configure_azure_key_vault( conn, AKV_CONFIG_FLAGS, AKVCFG_AUTHMODE_CLIENTKEY, 0 );
}
configure_azure_key_vault( conn, AKV_CONFIG_PRINCIPALID, akv_id, id_len );
configure_azure_key_vault( conn, AKV_CONFIG_AUTHSECRET, akv_secret, key_size );
}
void configure_azure_key_vault( sqlsrv_conn* conn, BYTE config_attr, const DWORD config_value, size_t key_size )
{
BYTE akv_data[sizeof( CEKEYSTOREDATA ) + sizeof(DWORD) + 1 ];
CEKEYSTOREDATA *pData = reinterpret_cast<CEKEYSTOREDATA*>( akv_data );
pData->name = L"AZURE_KEY_VAULT";
pData->data[0] = config_attr;
pData->dataSize = sizeof(config_attr) + sizeof(config_value);
*reinterpret_cast<DWORD*>(&pData->data[1]) = config_value;
core::SQLSetConnectAttr( conn, SQL_COPT_SS_CEKEYSTOREDATA, reinterpret_cast<SQLPOINTER>(pData), SQL_IS_POINTER );
}
void configure_azure_key_vault( sqlsrv_conn* conn, BYTE config_attr, const char* config_value, size_t key_size )
{
BYTE akv_data[sizeof( CEKEYSTOREDATA ) + 2048 ];
CEKEYSTOREDATA *pData = reinterpret_cast<CEKEYSTOREDATA*>( akv_data );
pData->name = L"AZURE_KEY_VAULT";
pData->data[0] = config_attr;
pData->dataSize = 1+key_size;
//pData->data[1] = config_value;
memcpy_s( pData->data+1, key_size * sizeof( char ) , config_value, key_size );
core::SQLSetConnectAttr( conn, SQL_COPT_SS_CEKEYSTOREDATA, reinterpret_cast<SQLPOINTER>(pData), SQL_IS_POINTER );
}
void common_conn_str_append_func( _In_z_ const char* odbc_name, _In_reads_(val_len) const char* val, _Inout_ size_t val_len, _Inout_ std::string& conn_str TSRMLS_DC )
{
// wrap a connection option in a quote. It is presumed that any character that need to be escaped will
@ -1005,6 +1094,45 @@ void column_encryption_set_func::func( _In_ connection_option const* option, _In
conn_str += ";";
}
void ce_akv_str_set_func::func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC )
{
SQLSRV_ASSERT( Z_TYPE_P( value ) == IS_STRING, "Azure Key Vault keywords accept only strings." );
//size_t value_len = Z_STRLEN_P( value );
switch( option->conn_option_key )
{
case SQLSRV_CONN_OPTION_KEYSTORE_AUTHENTICATION:
{
char *value_str = Z_STRVAL_P( value );
CHECK_CUSTOM_ERROR( stricmp( value_str, "KeyVaultPassword" ) && stricmp( value_str, "KeyVaultClientSecret" ), conn, SQLSRV_ERROR_INVALID_AKV_AUTHENTICATION_OPTION )
{
throw core::CoreException();
}
conn->ce_option.akv_auth = value;
conn->ce_option.akv_required = true;
break;
}
case SQLSRV_CONN_OPTION_KEYSTORE_PRINCIPAL_ID:
{
conn->ce_option.akv_id = value;
//conn->ce_option.akv_id_len = value_len;
conn->ce_option.akv_required = true;
break;
}
case SQLSRV_CONN_OPTION_KEYSTORE_SECRET:
{
conn->ce_option.akv_secret = value;
//conn->ce_option.akv_secret_len = value_len;
conn->ce_option.akv_required = true;
break;
}
default:
SQLSRV_ASSERT( false, "ce_akv_str_set_func: Invalid AKV option!" );
break;
}
}
// helper function to evaluate whether a string value is true or false.
// Values = ("true" or "1") are treated as true values. Everything else is treated as false.
// Returns 1 for true and 0 for false.

View file

@ -1055,8 +1055,12 @@ struct stmt_option;
// This holds the various details of column encryption.
struct col_encryption_option {
bool enabled; // column encryption enabled, false by default
zval_auto_ptr akv_auth;
zval_auto_ptr akv_id;
zval_auto_ptr akv_secret;
bool akv_required;
col_encryption_option() : enabled( false )
col_encryption_option() : enabled( false ), akv_required( false )
{
}
};
@ -1106,14 +1110,17 @@ const char Authentication[] = "Authentication";
const char Driver[] = "Driver";
const char CharacterSet[] = "CharacterSet";
const char ConnectionPooling[] = "ConnectionPooling";
#ifdef _WIN32
const char ColumnEncryption[] = "ColumnEncryption";
#ifdef _WIN32
const char ConnectRetryCount[] = "ConnectRetryCount";
const char ConnectRetryInterval[] = "ConnectRetryInterval";
#endif // _WIN32
const char Database[] = "Database";
const char Encrypt[] = "Encrypt";
const char Failover_Partner[] = "Failover_Partner";
const char KeyStoreAuthentication[] = "KeyStoreAuthentication";
const char KeyStorePrincipalId[] = "KeyStorePrincipalId";
const char KeyStoreSecret[] = "KeyStoreSecret";
const char LoginTimeout[] = "LoginTimeout";
const char MARS_ODBC[] = "MARS_Connection";
const char MultiSubnetFailover[] = "MultiSubnetFailover";
@ -1156,7 +1163,10 @@ enum SQLSRV_CONN_OPTIONS {
SQLSRV_CONN_OPTION_CEKEYSTORE_PROVIDER,
SQLSRV_CONN_OPTION_CEKEYSTORE_NAME,
SQLSRV_CONN_OPTION_CEKEYSTORE_ENCRYPT_KEY,
SQLSRV_CONN_OPTION_TRANSPARANT_NETWORK_IP_RESOLUTION,
SQLSRV_CONN_OPTION_KEYSTORE_AUTHENTICATION,
SQLSRV_CONN_OPTION_KEYSTORE_PRINCIPAL_ID,
SQLSRV_CONN_OPTION_KEYSTORE_SECRET,
SQLSRV_CONN_OPTION_TRANSPARENT_NETWORK_IP_RESOLUTION,
#ifdef _WIN32
SQLSRV_CONN_OPTION_CONN_RETRY_COUNT,
SQLSRV_CONN_OPTION_CONN_RETRY_INTERVAL,
@ -1219,6 +1229,10 @@ struct ce_ksp_provider_set_func {
static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC );
};
struct ce_akv_str_set_func {
static void func( _In_ connection_option const* option, _In_ zval* value, _Inout_ sqlsrv_conn* conn, _Inout_ std::string& conn_str TSRMLS_DC );
};
// factory to create a connection (since they are subclassed to instantiate statements)
typedef sqlsrv_conn* (*driver_conn_factory)( _In_ SQLHANDLE h, _In_ error_callback e, _In_ void* drv TSRMLS_DC );
@ -1699,6 +1713,10 @@ enum SQLSRV_ERROR_CODES {
SQLSRV_ERROR_BUFFER_LIMIT_EXCEEDED,
SQLSRV_ERROR_INVALID_BUFFER_LIMIT,
SQLSRV_ERROR_OUTPUT_PARAM_TYPES_NOT_SUPPORTED,
SQLSRV_ERROR_INVALID_AKV_AUTHENTICATION_OPTION,
SQLSRV_ERROR_AKV_AUTH_MISSING,
SQLSRV_ERROR_AKV_NAME_MISSING,
SQLSRV_ERROR_AKV_SECRET_MISSING,
SQLSRV_ERROR_ENCRYPTED_STREAM_FETCH,
// Driver specific error codes starts from here.
@ -1916,7 +1934,7 @@ namespace core {
}
inline void SQLAllocHandle( _In_ SQLSMALLINT HandleType, _Inout_ sqlsrv_context& InputHandle,
_Out_ SQLHANDLE* OutputHandlePtr TSRMLS_DC )
_Out_ SQLHANDLE* OutputHandlePtr TSRMLS_DC )
{
SQLRETURN r;
r = ::SQLAllocHandle( HandleType, InputHandle.handle(), OutputHandlePtr );

View file

@ -184,19 +184,22 @@ namespace SSConnOptionNames {
const char APP[] = "APP";
const char ApplicationIntent[] = "ApplicationIntent";
const char AttachDBFileName[] = "AttachDbFileName";
const char CharacterSet[] = "CharacterSet";
const char Authentication[] = "Authentication";
const char ConnectionPooling[] = "ConnectionPooling";
const char Driver[] = "Driver";
#ifdef _WIN32
const char CharacterSet[] = "CharacterSet";
const char ColumnEncryption[] = "ColumnEncryption";
const char ConnectionPooling[] = "ConnectionPooling";
#ifdef _WIN32
const char ConnectRetryCount[] = "ConnectRetryCount";
const char ConnectRetryInterval[] = "ConnectRetryInterval";
#endif // _WIN32
const char Database[] = "Database";
const char DateAsString[] = "ReturnDatesAsStrings";
const char Driver[] = "Driver";
const char Encrypt[] = "Encrypt";
const char Failover_Partner[] = "Failover_Partner";
const char KeyStoreAuthentication[] = "KeyStoreAuthentication";
const char KeyStorePrincipalId[] = "KeyStorePrincipalId";
const char KeyStoreSecret[] = "KeyStoreSecret";
const char LoginTimeout[] = "LoginTimeout";
const char MARS_Option[] = "MultipleActiveResultSets";
const char MultiSubnetFailover[] = "MultiSubnetFailover";
@ -312,7 +315,6 @@ const connection_option SS_CONN_OPTS[] = {
CONN_ATTR_STRING,
driver_set_func::func
},
#ifdef _WIN32
{
SSConnOptionNames::ColumnEncryption,
sizeof(SSConnOptionNames::ColumnEncryption),
@ -322,6 +324,7 @@ const connection_option SS_CONN_OPTS[] = {
CONN_ATTR_STRING,
column_encryption_set_func::func
},
#ifdef _WIN32
{
SSConnOptionNames::ConnectRetryCount,
sizeof( SSConnOptionNames::ConnectRetryCount ),
@ -368,6 +371,33 @@ const connection_option SS_CONN_OPTS[] = {
CONN_ATTR_STRING,
conn_str_append_func::func
},
{
SSConnOptionNames::KeyStoreAuthentication,
sizeof( SSConnOptionNames::KeyStoreAuthentication ),
SQLSRV_CONN_OPTION_KEYSTORE_AUTHENTICATION,
ODBCConnOptions::KeyStoreAuthentication,
sizeof( ODBCConnOptions::KeyStoreAuthentication ),
CONN_ATTR_STRING,
ce_akv_str_set_func::func
},
{
SSConnOptionNames::KeyStorePrincipalId,
sizeof( SSConnOptionNames::KeyStorePrincipalId ),
SQLSRV_CONN_OPTION_KEYSTORE_PRINCIPAL_ID,
ODBCConnOptions::KeyStorePrincipalId,
sizeof( ODBCConnOptions::KeyStorePrincipalId ),
CONN_ATTR_STRING,
ce_akv_str_set_func::func
},
{
SSConnOptionNames::KeyStoreSecret,
sizeof( SSConnOptionNames::KeyStoreSecret ),
SQLSRV_CONN_OPTION_KEYSTORE_SECRET,
ODBCConnOptions::KeyStoreSecret,
sizeof( ODBCConnOptions::KeyStoreSecret ),
CONN_ATTR_STRING,
ce_akv_str_set_func::func
},
{
SSConnOptionNames::LoginTimeout,
sizeof( SSConnOptionNames::LoginTimeout ),
@ -443,7 +473,7 @@ const connection_option SS_CONN_OPTS[] = {
{
SSConnOptionNames::TransparentNetworkIPResolution,
sizeof(SSConnOptionNames::TransparentNetworkIPResolution),
SQLSRV_CONN_OPTION_TRANSPARANT_NETWORK_IP_RESOLUTION,
SQLSRV_CONN_OPTION_TRANSPARENT_NETWORK_IP_RESOLUTION,
ODBCConnOptions::TransparentNetworkIPResolution,
sizeof(ODBCConnOptions::TransparentNetworkIPResolution),
CONN_ATTR_STRING,
@ -525,7 +555,7 @@ PHP_FUNCTION ( sqlsrv_connect )
core::sqlsrv_zend_hash_init( *g_ss_henv_cp, ss_conn_options_ht, 10 /* # of buckets */,
ZVAL_PTR_DTOR, 0 /*persistent*/ TSRMLS_CC );
// Either of g_ss_henv_cp or g_ss_henv_ncp can be used to propogate the error.
// Either of g_ss_henv_cp or g_ss_henv_ncp can be used to propagate the error.
::validate_conn_options( *g_ss_henv_cp, options_z, &uid, &pwd, ss_conn_options_ht TSRMLS_CC );
// call the core connect function

View file

@ -400,6 +400,22 @@ ss_error SS_ERRORS[] = {
SQLSRV_ERROR_ENCRYPTED_STREAM_FETCH,
{ IMSSP, (SQLCHAR*) "Connection with Column Encryption enabled does not support fetching stream. Please fetch the data as a string.", -109, false }
},
{
SQLSRV_ERROR_INVALID_AKV_AUTHENTICATION_OPTION,
{ IMSSP, (SQLCHAR*) "Invalid option for the KeyStoreAuthentication keyword. Only KeyVaultPassword or KeyVaultClientSecret is allowed.", -110, false }
},
{
SQLSRV_ERROR_AKV_AUTH_MISSING,
{ IMSSP, (SQLCHAR*) "Authentication method for Azure Key Vault is missing. KeyStoreAuthentication must be set to KeyVaultPassword or KeyVaultClientSecret.", -111, false }
},
{
SQLSRV_ERROR_AKV_NAME_MISSING,
{ IMSSP, (SQLCHAR*) "ID for Azure Key Vault is missing. A username or client Id is required.", -112, false }
},
{
SQLSRV_ERROR_AKV_SECRET_MISSING,
{ IMSSP, (SQLCHAR*) "Secret for Azure Key Vault is missing. A password or client secret is required.", -113, false }
},
// terminate the list of errors/warnings
{ UINT_MAX, {} }

View file

@ -44,4 +44,9 @@ $traceEnabled = false;
$keystore = "none"; // key store provider, acceptable values are none, win, ksp, akv
$dataEncrypted = false; // whether data is to be encrypted
$principalName = 'name';
$clientID = 'clientid';
$AKVPassword = 'password';
$AKVSecret = 'secret';
?>

View file

@ -0,0 +1,337 @@
--TEST--
Test connection keywords and credentials for Azure Key Vault for Always Encrypted.
--SKIPIF--
<?php require('skipif_mid-refactor.inc'); ?>
--FILE--
<?php
require_once("MsCommon_mid-refactor.inc");
require_once("MsSetup.inc");
require_once('values.php');
// We will test the direct product (set of all possible combinations) of the following
$columnEncryption = ['enabled', 'disabled', 'notvalid', ''];
$keyStoreAuthentication = ['KeyVaultPassword', 'KeyVaultClientSecret', 'KeyVaultNothing', ''];
$keyStorePrincipalId = [$principalName, $clientID, 'notaname', ''];
$keyStoreSecret = [$AKVPassword, $AKVSecret, 'notasecret', ''];
function checkErrors($errors, ...$codes)
{
$errSize = empty($errors) ? 0 : sizeof($errors);
if (2*$errSize < sizeof($codes)) fatalError("Errors and input codes do not match.\n");
$i=0;
foreach($codes as $code)
{
if ($i%2==0) {
if ($errors[0] != $code)
{
echo "Error: ";
print_r($errors[$i/2][0]);
echo "\nExpected: ";
print_r($code);
echo "\n";
fatalError("Error codes do not match.\n");
}
} else if ($i%2==1) {
if ($errors[1] != $code)
{
echo "Error: ";
print_r($errors[$i/2][1]);
echo "\nExpected: ";
print_r($code);
echo "\n";
fatalError("Error codes do not match.\n");
}
}
++$i;
}
}
// Set up the columns and build the insert query. Each data type has an
// AE-encrypted and a non-encrypted column side by side in the table.
function FormulateSetupQuery($tableName, &$dataTypes, &$columns, &$insertQuery)
{
$columns = array();
$queryTypes = "(";
$queryTypesAE = "(";
$valuesString = "VALUES (";
$numTypes = sizeof($dataTypes);
for ($i = 0; $i < $numTypes; ++$i) {
// Replace parentheses for column names
$colname = str_replace(array("(", ",", ")"), array("_", "_", ""), $dataTypes[$i]);
$columns[] = new ColumnMeta($dataTypes[$i], "c_".$colname."_AE", null, "deterministic", false);
$columns[] = new ColumnMeta($dataTypes[$i], "c_".$colname, null, "none", false);
$queryTypes .= "c_"."$colname, ";
$queryTypes .= "c_"."$colname"."_AE, ";
$valuesString .= "?, ?, ";
}
$queryTypes = substr($queryTypes, 0, -2).")";
$valuesString = substr($valuesString, 0, -2).")";
$insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString;
}
$strsize = 64;
$dataTypes = array ("char($strsize)", "varchar($strsize)", "nvarchar($strsize)",
"decimal", "float", "real", "bigint", "int", "bit"
);
// Test every combination of the keywords above
// Leave good credentials to the end to avoid caching influencing the results.
// The cache timeout can only be changed with SQLSetConnectAttr, so we can't
// run a PHP test without caching, and if we started with good credentials
// then subsequent calls with bad credentials can work, which would muddle
// the results of this test.
for ($i=0; $i < sizeof($columnEncryption); ++$i) {
for ($j=0; $j < sizeof($keyStoreAuthentication); ++$j) {
for ($k=0; $k < sizeof($keyStorePrincipalId); ++$k) {
for ($m=0; $m < sizeof($keyStoreSecret); ++$m) {
$connectionOptions = "sqlsrv:Server=$server;Database=$databaseName";
if (!empty($columnEncryption[$i]))
$connectionOptions .= ";ColumnEncryption=".$columnEncryption[$i];
if (!empty($keyStoreAuthentication[$j]))
$connectionOptions .= ";KeyStoreAuthentication=".$keyStoreAuthentication[$j];
if (!empty($keyStorePrincipalId[$k]))
$connectionOptions .= ";KeyStorePrincipalId=".$keyStorePrincipalId[$k];
if (!empty($keyStoreSecret[$m]))
$connectionOptions .= ";KeyStoreSecret=".$keyStoreSecret[$m];
// Valid credentials getting skipped
if (($i==0 and $j==0 and $k==0 and $m==0) or
($i==0 and $j==1 and $k==1 and $m==1)) {
continue;
}
$connectionOptions .= ";";
try {
// Connect to the AE-enabled database
$conn = new PDO($connectionOptions, $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$tableName = "type_conversion_table";
$columns = array();
$insertQuery = "";
// Generate the INSERT query
FormulateSetupQuery($tableName, $dataTypes, $columns, $insertQuery);
createTable($conn, $tableName, $columns);
// Duplicate all values for insertion - one is encrypted, one is not
$testValues = array();
for ($n=0; $n<sizeof($small_values); ++$n) {
$testValues[] = $small_values[$n];
$testValues[] = $small_values[$n];
}
// Prepare the INSERT query
// This is never expected to fail
$stmt = $conn->prepare($insertQuery);
if ($stmt == false) {
print_r($conn->errorInfo());
fatalError("sqlsrv_prepare failed\n");
}
// Execute the INSERT query
// This is where we expect failure if the credentials are incorrect
if ($stmt->execute($testValues) == false) {
print_r($stmt->errorInfo());
$stmt = null;
} else {
// The INSERT query succeeded with bad credentials
fatalError( "Successful insertion with bad credentials\n");
}
// Free the statement and close the connection
$stmt = null;
$conn = null;
} catch(Exception $e) {
$errors = $e->errorInfo;
if ($i==0 and $j==3 and $k==3 and $m==3)
checkErrors($errors, 'CE258', '0');
else if ($j==2)
checkErrors($errors, 'IMSSP', '-85');
else if ($i==2)
checkErrors($errors, '08001', '0');
else if ($i==1 or $i==3)
checkErrors($errors, '22018', '206');
else if ($j==3)
checkErrors($errors, 'IMSSP', '-86');
else if ($k==3)
checkErrors($errors, 'IMSSP', '-87');
else if ($m==3)
checkErrors($errors, 'IMSSP', '-88');
else
checkErrors($errors, 'CE275', '0');
}
}
}
}
}
// Now test the good credentials, where ($i, $j, $k, $m) == (0, 0, 0, 0)
// and ($i, $j, $k, $m) == (0, 1, 1, 1)
$connectionOptions = "sqlsrv:Server=$server;Database=$databaseName";
$connectionOptions .= ";ColumnEncryption=".$columnEncryption[0];
$connectionOptions .= ";KeyStoreAuthentication=".$keyStoreAuthentication[0];
$connectionOptions .= ";KeyStorePrincipalId=".$keyStorePrincipalId[0];
$connectionOptions .= ";KeyStoreSecret=".$keyStoreSecret[0];
$connectionOptions .= ";";
try {
// Connect to the AE-enabled database
$conn = new PDO($connectionOptions, $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$tableName = "type_conversion_table";
$columns = array();
$insertQuery = "";
// Generate the INSERT query
FormulateSetupQuery($tableName, $dataTypes, $columns, $insertQuery);
createTable($conn, $tableName, $columns);
// Duplicate all values for insertion - one is encrypted, one is not
$testValues = array();
for ($n=0; $n<sizeof($small_values); ++$n) {
$testValues[] = $small_values[$n];
$testValues[] = $small_values[$n];
}
// Prepare the INSERT query
// This is never expected to fail
$stmt = $conn->prepare($insertQuery);
if ($stmt == false) {
print_r($conn->errorInfo());
fatalError("sqlsrv_prepare failed\n");
}
// Execute the INSERT query
// This should not fail since our credentials are correct
if ($stmt->execute($testValues) == false) {
print_r($stmt->errorInfo());
fatalError("INSERT query execution failed with good credentials.\n");
} else {
echo "Successful insertion with username/password.\n";
$selectQuery = "SELECT * FROM $tableName";
$stmt1 = $conn->query($selectQuery);
$data = $stmt1->fetchAll(PDO::FETCH_NUM);
$data = $data[0];
if (sizeof($data) != 2*sizeof($dataTypes)) {
fatalError("Incorrect number of fields returned.\n");
}
for ($n=0; $n<sizeof($data); $n+=2) {
if ($data[$n] != $data[$n+1]) {
echo "Failed on field $n: ".$data[$n]." ".$data[$n+1]."\n";
fatalError("AE and non-AE values do not match.\n");
}
}
$stmt = null;
$stmt1 = null;
}
// Free the statement and close the connection
$stmt = null;
$conn = null;
} catch(Exception $e) {
echo "Unexpected error.\n";
print_r($e->errorInfo);
}
$connectionOptions = "sqlsrv:Server=$server;Database=$databaseName";
$connectionOptions .= ";ColumnEncryption=".$columnEncryption[0];
$connectionOptions .= ";KeyStoreAuthentication=".$keyStoreAuthentication[1];
$connectionOptions .= ";KeyStorePrincipalId=".$keyStorePrincipalId[1];
$connectionOptions .= ";KeyStoreSecret=".$keyStoreSecret[1];
$connectionOptions .= ";";
try {
// Connect to the AE-enabled database
$conn = new PDO($connectionOptions, $uid, $pwd);
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$tableName = "type_conversion_table";
$columns = array();
$insertQuery = "";
// Generate the INSERT query
FormulateSetupQuery($tableName, $dataTypes, $columns, $insertQuery);
createTable($conn, $tableName, $columns);
// Duplicate all values for insertion - one is encrypted, one is not
$testValues = array();
for ($n=0; $n<sizeof($small_values); ++$n) {
$testValues[] = $small_values[$n];
$testValues[] = $small_values[$n];
}
// Prepare the INSERT query
// This is never expected to fail
$stmt = $conn->prepare($insertQuery);
if ($stmt == false) {
print_r($conn->errorInfo());
fatalError("sqlsrv_prepare failed\n");
}
// Execute the INSERT query
// This should not fail since our credentials are correct
if ($stmt->execute($testValues) == false) {
print_r($stmt->errorInfo());
fatalError("INSERT query execution failed with good credentials.\n");
} else {
echo "Successful insertion with client ID/secret.\n";
$selectQuery = "SELECT * FROM $tableName";
$stmt1 = $conn->query($selectQuery);
$data = $stmt1->fetchAll(PDO::FETCH_NUM);
$data = $data[0];
if (sizeof($data) != 2*sizeof($dataTypes)) {
fatalError("Incorrect number of fields returned.\n");
}
for ($n=0; $n<sizeof($data); $n+=2) {
if ($data[$n] != $data[$n+1]) {
echo "Failed on field $n: ".$data[$n]." ".$data[$n+1]."\n";
fatalError("AE and non-AE values do not match.\n");
}
}
$stmt = null;
$stmt1 = null;
}
// Free the statement and close the connection
$stmt = null;
$conn = null;
} catch(Exception $e) {
echo "Unexpected error2.\n";
print_r($e->errorInfo);
}
?>
--EXPECT--
Successful insertion with username/password.
Successful insertion with clinet ID/secret.

File diff suppressed because one or more lines are too long

View file

@ -44,4 +44,9 @@ if (isset($_ENV['MSSQL_SERVER']) || isset($_ENV['MSSQL_USER']) || isset($_ENV['M
$keystore = "none"; // key store provider, acceptable values are none, win, ksp, akv
$dataEncrypted = false; // whether data is to be encrypted
$principalName = 'name';
$clientID = 'clientid';
$AKVPassword = 'password';
$AKVSecret = 'secret';
?>

View file

@ -0,0 +1,343 @@
--TEST--
Test connection keywords nad credentials for Azure Key Vault for Always Encrypted.
--SKIPIF--
<?php require('skipif.inc'); ?>
--FILE--
<?php
require_once('MsCommon.inc');
require_once('tools.inc');
require_once('values.php');
// We will test the direct product (set of all possible combinations) of the following
$columnEncryption = ['enabled', 'disabled', 'notvalid', ''];
$keyStoreAuthentication = ['KeyVaultPassword', 'KeyVaultClientSecret', 'KeyVaultNothing', ''];
$keyStorePrincipalId = [$principalName, $clientID, 'notaname', ''];
$keyStoreSecret = [$AKVPassword, $AKVSecret, 'notasecret', ''];
function checkErrors($errors, ...$codes)
{
$errSize = empty($errors) ? 0 : sizeof($errors);
if (2*$errSize < sizeof($codes)) fatalError("Errors and input codes do not match.\n");
$i=0;
foreach($codes as $code)
{
if ($i%2==0) {
if ($errors[$i/2][0] != $code)
{
echo "Error: ";
print_r($errors[$i/2][0]);
echo "\nExpected: ";
print_r($code);
echo "\n";
fatalError("Error codes do not match.\n");
}
} else if ($i%2==1) {
if ($errors[$i/2][1] != $code)
{
echo "Error: ";
print_r($errors[$i/2][1]);
echo "\nExpected: ";
print_r($code);
echo "\n";
fatalError("Error codes do not match.\n");
}
}
++$i;
}
}
// Set up the columns and build the insert query. Each data type has an
// AE-encrypted and a non-encrypted column side by side in the table.
function FormulateSetupQuery($tableName, &$dataTypes, &$columns, &$insertQuery, $strsize)
{
$columns = array();
$queryTypes = "(";
$queryTypesAE = "(";
$valuesString = "VALUES (";
$numTypes = sizeof($dataTypes);
for ($i = 0; $i < $numTypes; ++$i) {
// Replace parentheses for column names
$colname = str_replace(array("(", ",", ")"), array("_", "_", ""), $dataTypes[$i]);
$columns[] = new AE\ColumnMeta($dataTypes[$i], "c_".$colname."_AE");
$columns[] = new AE\ColumnMeta($dataTypes[$i], "c_".$colname, null, true, true);
$queryTypes .= "c_"."$colname, ";
$queryTypes .= "c_"."$colname"."_AE, ";
$valuesString .= "?, ?, ";
}
$queryTypes = substr($queryTypes, 0, -2).")";
$valuesString = substr($valuesString, 0, -2).")";
$insertQuery = "INSERT INTO $tableName ".$queryTypes." ".$valuesString;
}
$strsize = 64;
$dataTypes = array ("char($strsize)", "varchar($strsize)", "nvarchar($strsize)",
"decimal", "float", "real", "bigint", "int", "bit"
);
// Test every combination of the keywords above
// Leave good credentials to the end to avoid caching influencing the results.
// The cache timeout can only be changed with SQLSetConnectAttr, so we can't
// run a PHP test without caching, and if we started with good credentials
// then subsequent calls with bad credentials can work, which would muddle
// the results of this test.
for ($i=0; $i < sizeof($columnEncryption); ++$i) {
for ($j=0; $j < sizeof($keyStoreAuthentication); ++$j) {
for ($k=0; $k < sizeof($keyStorePrincipalId); ++$k) {
for ($m=0; $m < sizeof($keyStoreSecret); ++$m) {
$connectionOptions = array("CharacterSet"=>"UTF-8",
"database"=>$databaseName,
"uid"=>$uid,
"pwd"=>$pwd,
"ConnectionPooling"=>0);
if (!empty($columnEncryption[$i]))
$connectionOptions['ColumnEncryption'] = $columnEncryption[$i];
if (!empty($keyStoreAuthentication[$j]))
$connectionOptions['KeyStoreAuthentication'] = $keyStoreAuthentication[$j];
if (!empty($keyStorePrincipalId[$k]))
$connectionOptions['KeyStorePrincipalId'] = $keyStorePrincipalId[$k];
if (!empty($keyStoreSecret[$m]))
$connectionOptions['KeyStoreSecret'] = $keyStoreSecret[$m];
// Valid credentials getting skipped
if (($i==0 and $j==0 and $k==0 and $m==0) or
($i==0 and $j==1 and $k==1 and $m==1)) {
continue;
}
// Connect to the AE-enabled database
// Failure is expected when the keyword combination is wrong
$conn = sqlsrv_connect($server, $connectionOptions);
if (!$conn) {
$errors = sqlsrv_errors();
if ($j==2)
checkErrors($errors, 'IMSSP', '-110');
else if ($i==2)
checkErrors($errors, '08001', '0');
else if ($j==3)
checkErrors($errors, 'IMSSP', '-111');
else if ($k==3)
checkErrors($errors, 'IMSSP', '-112');
else if ($m==3)
checkErrors($errors, 'IMSSP', '-113');
else
fatalError("Connection failed, unexpected connection string.\n");
} else {
$tableName = "type_conversion_table";
$columns = array();
$insertQuery = "";
// Generate the INSERT query
FormulateSetupQuery($tableName, $dataTypes, $columns, $insertQuery, $strsize);
$stmt = AE\createTable($conn, $tableName, $columns);
if (!$stmt) {
fatalError("Failed to create table $tableName.\n");
}
// Duplicate all values for insertion - one is encrypted, one is not
$testValues = array();
for ($n=0; $n<sizeof($small_values); ++$n) {
$testValues[] = $small_values[$n];
$testValues[] = $small_values[$n];
}
// Prepare the INSERT query
// This is never expected to fail
$stmt = sqlsrv_prepare($conn, $insertQuery, $testValues);
if ($stmt == false) {
print_r(sqlsrv_errors());
fatalError("sqlsrv_prepare failed.\n");
}
// Execute the INSERT query
// This is where we expect failure if the credentials are incorrect
if (sqlsrv_execute($stmt) == false) {
$errors = sqlsrv_errors();
if ($i==0 and $j==3 and $k==3 and $m==3)
checkErrors($errors, 'CE258', '0', 'CE202', '0');
if ($i==0 and $j==3)
checkErrors($errors, 'CE258', '0', 'CE202', '0');
else if ($i==1 or $i==3)
checkErrors($errors, '22018', '206', '42000', '33514','42000', '8180');
else
checkErrors($errors, 'CE275', '0', 'CE275', '0', 'CE258', '0', 'CE202', '0');
sqlsrv_free_stmt($stmt);
} else {
// The INSERT query succeeded with bad credentials
fatalError( "Successful insertion with bad credentials\n");
}
// Free the statement and close the connection
sqlsrv_close($conn);
}
}
}
}
}
// Now test the good credentials, where ($i, $j, $k, $m) == (0, 0, 0, 0)
// and ($i, $j, $k, $m) == (0, 1, 1, 1)
$connectionOptions = array("CharacterSet"=>"UTF-8",
"database"=>$databaseName,
"uid"=>$uid,
"pwd"=>$pwd,
"ConnectionPooling"=>0);
$connectionOptions['ColumnEncryption'] = $columnEncryption[0];
$connectionOptions['KeyStoreAuthentication'] = $keyStoreAuthentication[0];
$connectionOptions['KeyStorePrincipalId'] = $keyStorePrincipalId[0];
$connectionOptions['KeyStoreSecret'] = $keyStoreSecret[0];
// Connect to the AE-enabled database
$conn = sqlsrv_connect($server, $connectionOptions);
if (!$conn) {
$errors = sqlsrv_errors();
fatalError("Connection failed while testing good credentials.\n");
} else {
$tableName = "type_conversion_table";
$columns = array();
$insertQuery = "";
// Generate the INSERT query
FormulateSetupQuery($tableName, $dataTypes, $columns, $insertQuery, $strsize);
$stmt = AE\createTable($conn, $tableName, $columns);
if (!$stmt) {
fatalError("Failed to create table $tableName\n");
}
// Duplicate all values for insertion - one is encrypted, one is not
$testValues = array();
for ($n=0; $n<sizeof($small_values); ++$n) {
$testValues[] = $small_values[$n];
$testValues[] = $small_values[$n];
}
// Prepare the INSERT query
// This is never expected to fail
$stmt = sqlsrv_prepare($conn, $insertQuery, $testValues);
if ($stmt == false) {
print_r(sqlsrv_errors());
fatalError("sqlsrv_prepare failed\n");
}
// Execute the INSERT query
// This should not fail since our credentials are correct
if (sqlsrv_execute($stmt) == false) {
$errors = sqlsrv_errors();
fatalError("INSERT query failed with good credentials.\n");
} else {
echo "Successful insertion with username/password.\n";
// Now get the data back and display it
$selectQuery = "SELECT * FROM $tableName";
$stmt1 = sqlsrv_query($conn, $selectQuery);
$data = sqlsrv_fetch_array($stmt1, SQLSRV_FETCH_NUMERIC);
if (sizeof($data) != 2*sizeof($dataTypes)) {
fatalError("Incorrect number of fields returned.\n");
}
for ($n=0; $n<sizeof($data); $n+=2) {
if ($data[$n] != $data[$n+1]) {
echo "Failed on field $n: ".$data[$n]." ".$data[$n+1]."\n";
fatalError("AE and non-AE values do not match.\n");
}
}
sqlsrv_free_stmt($stmt);
sqlsrv_free_stmt($stmt1);
}
// Free the statement and close the connection
sqlsrv_close($conn);
}
$connectionOptions['ColumnEncryption'] = $columnEncryption[0];
$connectionOptions['KeyStoreAuthentication'] = $keyStoreAuthentication[1];
$connectionOptions['KeyStorePrincipalId'] = $keyStorePrincipalId[1];
$connectionOptions['KeyStoreSecret'] = $keyStoreSecret[1];
// Connect to the AE-enabled database
$conn = sqlsrv_connect($server, $connectionOptions);
if (!$conn) {
$errors = sqlsrv_errors();
fatalError("Connection failed while testing good credentials.\n");
} else {
$tableName = "type_conversion_table";
$columns = array();
$insertQuery = "";
// Generate the INSERT query
FormulateSetupQuery($tableName, $dataTypes, $columns, $insertQuery, $strsize);
$stmt = AE\createTable($conn, $tableName, $columns);
if (!$stmt) {
fatalError("Failed to create table $tableName\n");
}
// Duplicate all values for insertion - one is encrypted, one is not
$testValues = array();
for ($n=0; $n<sizeof($small_values); ++$n) {
$testValues[] = $small_values[$n];
$testValues[] = $small_values[$n];
}
// Prepare the INSERT query
// This is never expected to fail
$stmt = sqlsrv_prepare($conn, $insertQuery, $testValues);
if ($stmt == false) {
print_r(sqlsrv_errors());
fatalError("sqlsrv_prepare failed\n");
}
// Execute the INSERT query
// This should not fail since our credentials are correct
if (sqlsrv_execute($stmt) == false) {
$errors = sqlsrv_errors();
fatalError("INSERT query execution failed with good credentials.\n");
} else {
echo "Successful insertion with client ID/secret.\n";
// Now get the data back and display it
$selectQuery = "SELECT * FROM $tableName";
$stmt1 = sqlsrv_query($conn, $selectQuery);
$data = sqlsrv_fetch_array($stmt1, SQLSRV_FETCH_NUMERIC);
if (sizeof($data) != 2*sizeof($dataTypes)) {
fatalError("Incorrect number of fields returned.\n");
}
for ($n=0; $n<sizeof($data); $n+=2) {
if ($data[$n] != $data[$n+1]) {
echo "Failed on field $n: ".$data[$n]." ".$data[$n+1]."\n";
fatalError("AE and non-AE values do not match.\n");
}
}
sqlsrv_free_stmt($stmt);
sqlsrv_free_stmt($stmt1);
}
// Free the statement and close the connection
sqlsrv_close($conn);
}
?>
--EXPECT--
Successful insertion with username/password.
Successful insertion with clinet ID/secret.

View file

@ -775,4 +775,16 @@ $values[] = array(array(("C0A0B025C680B0A23D7885F7C203AD211F679679F97F910F0F1A36
"2002-01-31 23:59:59.0498764",
null,
null,
);
);
$small_values = array("qwerty",
"wertyu",
"ϕƆǀđIΩͰDZζ±Áɔd͋ǻĆÅũμ",
52.7878,
-1.79E+308,
-3.4E+38,
-987654321987654321,
987654321,
1,
);