diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 768ba1a50..07d92f78f 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -10,7 +10,7 @@ jobs:
matrix:
SQL-2019:
Target_SQL: 'HGS-2k19-01'
- Ex_Groups: 'xSQLv15'
+ Ex_Groups: 'xSQLv15, clientCertAuth'
SQL-2012:
Target_SQL: 'SQL-2K12-SP3-1'
Ex_Groups: 'xSQLv12'
diff --git a/pom.xml b/pom.xml
index a467a6723..64ee763b8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,9 +51,10 @@
xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance
NTLM - - - - - - - For tests using NTLM Authentication mode (excluded by default)
reqExternalSetup - For tests requiring external setup (excluded by default)
+ clientCertAuth - - For tests requiring client certificate authentication setup (excluded by default)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Default testing enabled with SQL Server 2019 (SQLv15) -->
- xSQLv15, NTLM, reqExternalSetup
+ xSQLv15, NTLM, reqExternalSetup, clientCertAuth
@@ -66,6 +67,8 @@
5.0.0
4.7.2
2.8.6
+ 1.64
+ 1.64
[1.3.2, 1.5.2]
@@ -119,7 +122,15 @@
org.bouncycastle
bcprov-jdk15on
- 1.64
+ ${bouncycastle.bcprov.version}
+ true
+
+
+
+
+ org.bouncycastle
+ bcpkix-jdk15on
+ ${bouncycastle.bcpkix.version}
true
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index 94e9b07e5..b046a3e0c 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -62,6 +62,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
+import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
@@ -364,6 +365,7 @@ static final String getTokenName(int tdsTokenType) {
static final byte ENCRYPT_ON = 0x01;
static final byte ENCRYPT_NOT_SUP = 0x02;
static final byte ENCRYPT_REQ = 0x03;
+ static final byte ENCRYPT_CLIENT_CERT = (byte) 0x80;
static final byte ENCRYPT_INVALID = (byte) 0xFF;
static final String getEncryptionLevel(int level) {
@@ -1597,9 +1599,16 @@ enum SSLHandhsakeState {
* Server Host Name for SSL Handshake
* @param port
* Server Port for SSL Handshake
+ * @param clientCertificate
+ * Client certificate path
+ * @param clientKey
+ * Private key file path
+ * @param clientKeyPassword
+ * Private key file's password
* @throws SQLServerException
*/
- void enableSSL(String host, int port) throws SQLServerException {
+ void enableSSL(String host, int port, String clientCertificate, String clientKey,
+ String clientKeyPassword) throws SQLServerException {
// If enabling SSL fails, which it can for a number of reasons, the following items
// are used in logging information to the TDS channel logger to help diagnose the problem.
Provider tmfProvider = null; // TrustManagerFactory provider
@@ -1774,13 +1783,16 @@ else if (con.getTrustManagerClass() != null) {
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Getting TLS or better SSL context");
+ KeyManager[] km = (null != clientCertificate && clientCertificate.length() > 0) ? SQLServerCertificateUtils
+ .getKeyManagerFromFile(clientCertificate, clientKey, clientKeyPassword) : null;
+
sslContext = SSLContext.getInstance(sslProtocol);
sslContextProvider = sslContext.getProvider();
if (logger.isLoggable(Level.FINEST))
logger.finest(toString() + " Initializing SSL context");
- sslContext.init(null, tm, null);
+ sslContext.init(km, tm, null);
// Got the SSL context. Now create an SSL socket over our own proxy socket
// which we can toggle between TDS-encapsulated and raw communications.
@@ -6202,7 +6214,8 @@ void writeRPCReaderUnicode(String sName, Reader re, long reLength, boolean bOut,
void sendEnclavePackage(String sql, ArrayList enclaveCEKs) throws SQLServerException {
if (null != con && con.isAEv2()) {
- if (null != sql && !sql.isEmpty() && null != enclaveCEKs && 0 < enclaveCEKs.size() && con.enclaveEstablished()) {
+ if (null != sql && !sql.isEmpty() && null != enclaveCEKs && 0 < enclaveCEKs.size()
+ && con.enclaveEstablished()) {
byte[] b = con.generateEnclavePackage(sql, enclaveCEKs);
if (null != b && 0 != b.length) {
this.writeShort((short) b.length);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index c9e4e1379..7690092a2 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -903,5 +903,43 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
* Enclave attestation protocol.
*/
void setEnclaveAttestationProtocol(String protocol);
+
+ /**
+ * Returns client certificate path for client certificate authentication.
+ *
+ * @return Client certificate path.
+ */
+ String getClientCertificate();
+
+ /**
+ * Sets client certificate path for client certificate authentication.
+ *
+ * @param certPath
+ * Client certificate path.
+ */
+ void setClientCertificate(String certPath);
+
+ /**
+ * Returns Private key file path for client certificate authentication.
+ *
+ * @return Private key file path.
+ */
+ String getClientKey();
+
+ /**
+ * Sets Private key file path for client certificate authentication.
+ *
+ * @param keyPath
+ * Private key file path.
+ */
+ void setClientKey(String keyPath);
+
+ /**
+ * Sets the password to be used for Private key provided by the user for client certificate authentication.
+ *
+ * @param password
+ * Private key password.
+ */
+ void setClientKeyPassword(String password);
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBouncyCastleLoader.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBouncyCastleLoader.java
index 6c13d5af0..c94c40466 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBouncyCastleLoader.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerBouncyCastleLoader.java
@@ -1,12 +1,22 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
package com.microsoft.sqlserver.jdbc;
+import java.security.Provider;
import java.security.Security;
/*
* Class that is meant to statically load the BouncyCastle Provider for JDK 8. Hides the call so JDK 11/13 don't have to include the dependency.
+ * Also loads BouncyCastle provider for PKCS1 private key parsing.
*/
class SQLServerBouncyCastleLoader {
static void loadBouncyCastle() {
- Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+ Provider p = new org.bouncycastle.jce.provider.BouncyCastleProvider();
+ if (null == Security.getProvider(p.getName())) {
+ Security.addProvider(p);
+ }
}
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
new file mode 100644
index 000000000..45d68e884
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
@@ -0,0 +1,295 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.RSAPrivateCrtKeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+
+import org.bouncycastle.openssl.PEMDecryptorProvider;
+import org.bouncycastle.openssl.PEMEncryptedKeyPair;
+import org.bouncycastle.openssl.PEMKeyPair;
+import org.bouncycastle.openssl.PEMParser;
+import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
+import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
+
+
+final class SQLServerCertificateUtils {
+
+ static KeyManager[] getKeyManagerFromFile(String certPath, String keyPath,
+ String keyPassword) throws IOException, GeneralSecurityException, SQLServerException {
+ if (keyPath != null && keyPath.length() > 0) {
+ return readPKCS8Certificate(certPath, keyPath, keyPassword);
+ } else {
+ return readPKCS12Certificate(certPath, keyPassword);
+ }
+ }
+
+ // PKCS#12 format
+ private static final String PKCS12_ALG = "PKCS12";
+ private static final String SUN_X_509 = "SunX509";
+ // PKCS#8 format
+ private static final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----";
+ private static final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----";
+ private static final String JAVA_KEY_STORE = "JKS";
+ private static final String CLIENT_CERT = "client-cert";
+ private static final String CLIENT_KEY = "client-key";
+ // PKCS#1 format
+ private static final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----";
+ // PVK format
+ private static final long PVK_MAGIC = 0xB0B5F11EL;
+ private static final byte[] RSA2_MAGIC = {82, 83, 65, 50};
+ private static final String RC4_ALG = "RC4";
+ private static final String RSA_ALG = "RSA";
+
+ private static KeyManager[] readPKCS12Certificate(String certPath,
+ String keyPassword) throws NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, UnrecoverableKeyException, KeyStoreException, SQLServerException {
+ KeyStore keystore = KeyStore.getInstance(PKCS12_ALG);
+ try {
+ keystore.load(new FileInputStream(certPath), keyPassword.toCharArray());
+ } catch (FileNotFoundException e) {
+ throw new SQLServerException(SQLServerException.getErrString("R_clientCertError"), null, 0, null);
+ }
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(SUN_X_509);
+ keyManagerFactory.init(keystore, keyPassword.toCharArray());
+ return keyManagerFactory.getKeyManagers();
+ }
+
+ private static KeyManager[] readPKCS8Certificate(String certPath, String keyPath,
+ String keyPassword) throws IOException, GeneralSecurityException, SQLServerException {
+ Certificate clientCertificate = loadCertificate(certPath);
+ PrivateKey privateKey = loadPrivateKey(keyPath, keyPassword);
+
+ KeyStore keyStore = KeyStore.getInstance(JAVA_KEY_STORE);
+ keyStore.load(null, null);
+ keyStore.setCertificateEntry(CLIENT_CERT, clientCertificate);
+ keyStore.setKeyEntry(CLIENT_KEY, privateKey, keyPassword.toCharArray(), new Certificate[] {clientCertificate});
+
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(keyStore, keyPassword.toCharArray());
+ return kmf.getKeyManagers();
+ }
+
+ private static PrivateKey loadPrivateKeyFromPKCS8(
+ String key) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+ StringBuilder sb = new StringBuilder(key);
+ deleteFirst(sb, PEM_PRIVATE_START);
+ deleteFirst(sb, PEM_PRIVATE_END);
+ byte[] formattedKey = Base64.getDecoder().decode(sb.toString().replaceAll("\\s", ""));
+
+ KeyFactory factory = KeyFactory.getInstance(RSA_ALG);
+ return factory.generatePrivate(new PKCS8EncodedKeySpec(formattedKey));
+ }
+
+ private static void deleteFirst(StringBuilder sb, String str) {
+ int i = sb.indexOf(str);
+ if (i != -1) {
+ sb.delete(i, i + str.length());
+ }
+ }
+
+ private static PrivateKey loadPrivateKeyFromPKCS1(String key,
+ String keyPass) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+ SQLServerBouncyCastleLoader.loadBouncyCastle();
+ PEMParser pemParser = null;
+ try {
+ pemParser = new PEMParser(new StringReader(key));
+ Object object = pemParser.readObject();
+ JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
+ KeyPair kp;
+ if (object instanceof PEMEncryptedKeyPair && keyPass != null) {
+ PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPass.toCharArray());
+ kp = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv));
+ } else {
+ kp = converter.getKeyPair((PEMKeyPair) object);
+ }
+ return kp.getPrivate();
+ } finally {
+ if (null != pemParser) {
+ pemParser.close();
+ }
+ }
+ }
+
+ private static PrivateKey loadPrivateKeyFromPVK(String keyPath,
+ String keyPass) throws IOException, GeneralSecurityException, SQLServerException {
+ File f = new File(keyPath);
+ ByteBuffer buffer = ByteBuffer.allocate((int) f.length());
+ try (FileInputStream in = new FileInputStream(f)) {
+ in.getChannel().read(buffer);
+ buffer.order(ByteOrder.LITTLE_ENDIAN).rewind();
+
+ long magic = buffer.getInt() & 0xFFFFFFFFL;
+ if (PVK_MAGIC != magic) {
+ SQLServerException.makeFromDriverError(null, magic, SQLServerResource.getResource("R_pvkHeaderError"),
+ "", false);
+ }
+
+ buffer.position(buffer.position() + 8); // skip reserved and keytype
+ boolean encrypted = buffer.getInt() != 0;
+ int saltLength = buffer.getInt();
+ int keyLength = buffer.getInt();
+ byte[] salt = new byte[saltLength];
+ buffer.get(salt);
+
+ buffer.position(buffer.position() + 8); // skip btype(1b), version(1b), reserved(2b), and keyalg(4b)
+
+ byte[] key = new byte[keyLength - 8];
+ buffer.get(key);
+
+ if (encrypted) {
+ MessageDigest digest = MessageDigest.getInstance("SHA1");
+ digest.update(salt);
+ if (null != keyPass) {
+ digest.update(keyPass.getBytes());
+ }
+ byte[] hash = digest.digest();
+ key = getSecretKeyFromHash(key, hash);
+ }
+
+ ByteBuffer buff = ByteBuffer.wrap(key).order(ByteOrder.LITTLE_ENDIAN);
+ buff.position(RSA2_MAGIC.length); // skip the header
+
+ int byteLength = buff.getInt() / 8;
+ BigInteger publicExponent = BigInteger.valueOf(buff.getInt());
+ BigInteger modulus = getBigInteger(buff, byteLength);
+ BigInteger prime1 = getBigInteger(buff, byteLength / 2);
+ BigInteger prime2 = getBigInteger(buff, byteLength / 2);
+ BigInteger primeExponent1 = getBigInteger(buff, byteLength / 2);
+ BigInteger primeExponent2 = getBigInteger(buff, byteLength / 2);
+ BigInteger crtCoefficient = getBigInteger(buff, byteLength / 2);
+ BigInteger privateExponent = getBigInteger(buff, byteLength);
+
+ RSAPrivateCrtKeySpec spec = new RSAPrivateCrtKeySpec(modulus, publicExponent, privateExponent, prime1,
+ prime2, primeExponent1, primeExponent2, crtCoefficient);
+ KeyFactory factory = KeyFactory.getInstance(RSA_ALG);
+ return factory.generatePrivate(spec);
+ }
+ }
+
+ private static Certificate loadCertificate(String certificatePem) throws IOException, GeneralSecurityException, SQLServerException {
+ CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
+ InputStream certstream = fileToStream(certificatePem);
+ return certificateFactory.generateCertificate(certstream);
+ }
+
+ private static PrivateKey loadPrivateKey(String privateKeyPemPath,
+ String privateKeyPassword) throws GeneralSecurityException, IOException, SQLServerException {
+ String privateKeyPem = getStringFromFile(privateKeyPemPath);
+
+ if (privateKeyPem.contains(PEM_PRIVATE_START)) { // PKCS#8 format
+ return loadPrivateKeyFromPKCS8(privateKeyPem);
+ } else if (privateKeyPem.contains(PEM_RSA_PRIVATE_START)) { // PKCS#1 format
+ return loadPrivateKeyFromPKCS1(privateKeyPem, privateKeyPassword);
+ } else {
+ return loadPrivateKeyFromPVK(privateKeyPemPath, privateKeyPassword);
+ }
+ }
+
+ private static boolean startsWithMagic(byte[] b) {
+ for (int i = 0; i < RSA2_MAGIC.length; i++) {
+ if (b[i] != RSA2_MAGIC[i])
+ return false;
+ }
+ return true;
+ }
+
+ private static byte[] getSecretKeyFromHash(byte[] originalKey,
+ byte[] keyHash) throws GeneralSecurityException, SQLServerException {
+ SecretKey key = new SecretKeySpec(keyHash, 0, 16, RC4_ALG);
+ byte[] decrypted = decryptSecretKey(key, originalKey);
+ if (startsWithMagic(decrypted)) {
+ return decrypted;
+ }
+
+ // Couldn't find magic due to padding, trim the key
+ Arrays.fill(keyHash, 5, keyHash.length, (byte) 0);
+ key = new SecretKeySpec(keyHash, 0, 16, RC4_ALG);
+ decrypted = decryptSecretKey(key, originalKey);
+ if (startsWithMagic(decrypted)) {
+ return decrypted;
+ }
+
+ SQLServerException.makeFromDriverError(null, originalKey, SQLServerResource.getResource("R_pvkParseError"), "",
+ false);
+ return null;
+ }
+
+ private static byte[] decryptSecretKey(SecretKey key, byte[] encoded) throws GeneralSecurityException {
+ Cipher cipher = Cipher.getInstance(key.getAlgorithm());
+ cipher.init(Cipher.DECRYPT_MODE, key);
+ return cipher.doFinal(encoded);
+ }
+
+ private static BigInteger getBigInteger(ByteBuffer buffer, int length) {
+ // Add an extra bit for signum
+ byte[] array = new byte[length + 1];
+ // Write in reverse because our buffer was set to Little Endian
+ for (int i = 0; i < length; i++) {
+ array[array.length - 1 - i] = buffer.get();
+ }
+ return new BigInteger(array);
+ }
+
+ private static InputStream fileToStream(String fname) throws IOException, SQLServerException {
+ FileInputStream fis = null;
+ DataInputStream dis = null;
+ try {
+ fis = new FileInputStream(fname);
+ dis = new DataInputStream(fis);
+ byte[] bytes = new byte[dis.available()];
+ dis.readFully(bytes);
+ ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
+ return bais;
+ } catch (FileNotFoundException e) {
+ throw new SQLServerException(SQLServerException.getErrString("R_clientCertError"), null, 0, null);
+ } finally {
+ if (null != dis) {
+ dis.close();
+ }
+ if (null != fis) {
+ fis.close();
+ }
+ }
+ }
+
+ private static String getStringFromFile(String filePath) throws IOException {
+ return new String(Files.readAllBytes(Paths.get(filePath)));
+ }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index 7f7687c38..13f6e913a 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -142,6 +142,10 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
private SqlFedAuthToken fedAuthToken = null;
private String originalHostNameInCertificate = null;
+
+ private String clientCertificate = null;
+ private String clientKey = null;
+ private String clientKeyPassword = "";
final int ENGINE_EDITION_FOR_SQL_AZURE = 5;
final int ENGINE_EDITION_FOR_SQL_AZURE_DW = 6;
@@ -2021,6 +2025,27 @@ else if (0 == requestedPacketSize)
if (null != sPropValue) {
activeConnectionProperties.setProperty(sPropKey, sPropValue);
}
+
+ sPropKey = SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString();
+ sPropValue = activeConnectionProperties.getProperty(sPropKey);
+ if (null != sPropValue) {
+ activeConnectionProperties.setProperty(sPropKey, sPropValue);
+ clientCertificate = sPropValue;
+ }
+
+ sPropKey = SQLServerDriverStringProperty.CLIENT_KEY.toString();
+ sPropValue = activeConnectionProperties.getProperty(sPropKey);
+ if (null != sPropValue) {
+ activeConnectionProperties.setProperty(sPropKey, sPropValue);
+ clientKey = sPropValue;
+ }
+
+ sPropKey = SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString();
+ sPropValue = activeConnectionProperties.getProperty(sPropKey);
+ if (null != sPropValue) {
+ activeConnectionProperties.setProperty(sPropKey, sPropValue);
+ clientKeyPassword = sPropValue;
+ }
FailoverInfo fo = null;
String databaseNameProperty = SQLServerDriverStringProperty.DATABASE_NAME.toString();
@@ -2555,7 +2580,7 @@ private void connectHelper(ServerPortPlaceHolder serverInfo, int timeOutSliceInM
// If prelogin negotiated SSL encryption then, enable it on the TDS channel.
if (TDS.ENCRYPT_NOT_SUP != negotiatedEncryptionLevel) {
- tdsChannel.enableSSL(serverInfo.getServerName(), serverInfo.getPortNumber());
+ tdsChannel.enableSSL(serverInfo.getServerName(), serverInfo.getPortNumber(), clientCertificate, clientKey, clientKeyPassword);
}
// We have successfully connected, now do the login. logon takes seconds timeout
@@ -2629,7 +2654,7 @@ void Prelogin(String serverName, int portNumber) throws SQLServerException {
0, 0, 0, 0, 0, 0,
// - Encryption -
- requestedEncryptionLevel,
+ (null == clientCertificate) ? requestedEncryptionLevel : (byte) (requestedEncryptionLevel | TDS.ENCRYPT_CLIENT_CERT),
// TRACEID Data Session (ClientConnectionId + ActivityId) - Initialize to 0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
@@ -4960,7 +4985,7 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ
+ 4; // AE is always on;
// only add lengths of password and username if not using SSPI or requesting federated authentication info
- if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested)) {
+ if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested) && null == clientCertificate) {
len = len + passwordLen + userBytes.length;
}
@@ -5038,7 +5063,7 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ
tdsWriter.writeShort((short) (tdsLoginRequestBaseLength + dataLen));
tdsWriter.writeShort((short) (0));
- } else if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested)) {
+ } else if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested) && null == clientCertificate) {
// User and Password
tdsWriter.writeShort((short) (tdsLoginRequestBaseLength + dataLen));
tdsWriter.writeShort((short) (sUser == null ? 0 : sUser.length()));
@@ -5130,7 +5155,8 @@ final boolean complete(LogonCommand logonCommand, TDSReader tdsReader) throws SQ
// if we are using NTLM or SSPI or fed auth ADAL, do not send over username/password, since we will use SSPI
// instead
- if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested)) {
+ // Also do not send username or password if user is attempting client certificate authentication.
+ if (!integratedSecurity && !(federatedAuthenticationInfoRequested || federatedAuthenticationRequested) && null == clientCertificate) {
tdsWriter.writeBytes(userBytes); // Username
tdsWriter.writeBytes(passwordBytes); // Password (encrypted)
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index 75397e02f..8f8d39b77 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -943,6 +943,36 @@ public void setEnclaveAttestationProtocol(String protocol) {
setStringProperty(connectionProps, SQLServerDriverStringProperty.ENCLAVE_ATTESTATION_PROTOCOL.toString(),
protocol);
}
+
+ @Override
+ public String getClientCertificate() {
+ return getStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString(),
+ SQLServerDriverStringProperty.CLIENT_CERTIFICATE.getDefaultValue());
+ }
+
+ @Override
+ public void setClientCertificate(String certPath) {
+ setStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString(),
+ certPath);
+ }
+
+ @Override
+ public String getClientKey() {
+ return getStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_KEY.toString(),
+ SQLServerDriverStringProperty.CLIENT_KEY.getDefaultValue());
+ }
+
+ @Override
+ public void setClientKey(String keyPath) {
+ setStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_KEY.toString(),
+ keyPath);
+ }
+
+ @Override
+ public void setClientKeyPassword(String password) {
+ setStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString(),
+ password);
+ }
/**
* Sets a property string value.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index 8ee0047b9..d765d01c2 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -351,7 +351,10 @@ enum SQLServerDriverStringProperty {
SSL_PROTOCOL("sslProtocol", SSLProtocol.TLS.toString()),
MSI_CLIENT_ID("msiClientId", ""),
KEY_VAULT_PROVIDER_CLIENT_ID("keyVaultProviderClientId", ""),
- KEY_VAULT_PROVIDER_CLIENT_KEY("keyVaultProviderClientKey", "");
+ KEY_VAULT_PROVIDER_CLIENT_KEY("keyVaultProviderClientKey", ""),
+ CLIENT_CERTIFICATE("clientCertificate", ""),
+ CLIENT_KEY("clientKey", ""),
+ CLIENT_KEY_PASSWORD("clientKeyPassword", "");
private final String name;
private final String defaultValue;
@@ -598,7 +601,14 @@ public final class SQLServerDriver implements java.sql.Driver {
SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_KEY.getDefaultValue(), false, null),
new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString(),
Boolean.toString(SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue()), false,
- TRUE_FALSE),};
+ TRUE_FALSE),
+ new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.CLIENT_CERTIFICATE.toString(),
+ SQLServerDriverStringProperty.CLIENT_CERTIFICATE.getDefaultValue(), false, null),
+ new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.CLIENT_KEY.toString(),
+ SQLServerDriverStringProperty.CLIENT_KEY.getDefaultValue(), false, null),
+ new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString(),
+ SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.getDefaultValue(), false, null),
+ };
/**
* Properties that can only be set by using Properties. Cannot set in connection string
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 89e8983e1..b9c64eb72 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -254,6 +254,9 @@ protected Object[][] getContents() {
{"R_gsscredentialPropertyDescription", "Impersonated GSS Credential to access SQL Server."},
{"R_msiClientIdPropertyDescription",
"Client Id of User Assigned Managed Identity to be used for generating access token for Azure AD MSI Authentication"},
+ {"R_clientCertificatePropertyDescription", "Client certificate path for client certificate authentication feature."},
+ {"R_clientKeyPropertyDescription", "Private key file path for client certificate authentication feature."},
+ {"R_clientKeyPasswordPropertyDescription", "Password for private key if the private key is password protected."},
{"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""},
{"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."},
{"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."},
@@ -621,5 +624,8 @@ protected Object[][] getContents() {
"Enclave attestation failed, the DH public key signature can't be verified with the enclave public key."},
{"R_AasJWTError", "An error occured when retrieving and validating the JSON web token."},
{"R_AasEhdError", "aas-ehd claim from JWT did not match enclave public key."},
- {"R_VbsRpDataError", "rp_data claim from JWT did not match client nonce."},};
+ {"R_VbsRpDataError", "rp_data claim from JWT did not match client nonce."},
+ {"R_pvkParseError", "Could not read Private Key from PVK, check the password provided."},
+ {"R_pvkHeaderError", "Cannot parse the PVK, PVK file does not contain the correct header."},
+ {"R_clientCertError", "Reading client certificate failed. Please verify the location of the certificate."}};
};
diff --git a/src/test/java/com/microsoft/sqlserver/clientcertauth/ClientCertificateAuthenticationTest.java b/src/test/java/com/microsoft/sqlserver/clientcertauth/ClientCertificateAuthenticationTest.java
new file mode 100644
index 000000000..acf746d0c
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/clientcertauth/ClientCertificateAuthenticationTest.java
@@ -0,0 +1,229 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+package com.microsoft.sqlserver.clientcertauth;
+
+import static org.junit.Assert.assertTrue;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
+
+import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
+import com.microsoft.sqlserver.jdbc.SQLServerException;
+import com.microsoft.sqlserver.jdbc.TestResource;
+import com.microsoft.sqlserver.jdbc.TestUtils;
+import com.microsoft.sqlserver.testframework.AbstractTest;
+import com.microsoft.sqlserver.testframework.Constants;
+
+
+/**
+ * Tests client certificate authentication feature
+ * The feature is only supported against SQL Server Linux CU2 or higher.
+ *
+ */
+@RunWith(JUnitPlatform.class)
+@Tag(Constants.xSQLv12)
+@Tag(Constants.xSQLv14)
+@Tag(Constants.xAzureSQLDW)
+@Tag(Constants.xAzureSQLDB)
+@Tag(Constants.clientCertAuth)
+public class ClientCertificateAuthenticationTest extends AbstractTest {
+
+ static final String PEM_SUFFIX = ".pem;";
+ static final String CER_SUFFIX = ".cer;";
+ static final String PVK_SUFFIX = ".pvk;";
+
+ static final String PKCS1_KEY_SUFFIX = "-pkcs1.key;";
+ static final String ENCRYPTED_PKCS1_KEY_SUFFIX = "-encrypted-pkcs1.key;";
+ static final String PKCS8_KEY_SUFFIX = "-pkcs8.key;";
+ static final String ENCRYPTED_PKCS8_KEY_SUFFIX = "-encrypted-pkcs8.key;";
+ static final String PFX_KEY_SUFFIX = ".pfx;";
+ static final String ENCRYPTED_PFX_KEY_SUFFIX = "-encrypted.pfx;";
+
+ /**
+ * Tests client certificate authentication feature with PKCS1 private key.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void pkcs1Test() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey="
+ + clientKey + PKCS1_KEY_SUFFIX;
+ try (Connection conn = DriverManager.getConnection(conStr)) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with PKCS1 private key that has been encrypted with a password.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void pkcs1EncryptedTest() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey="
+ + clientKey + ENCRYPTED_PKCS1_KEY_SUFFIX + "clientKeyPassword=" + clientKeyPassword + ";";
+ try (Connection conn = DriverManager.getConnection(conStr)) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with PKCS8 private key.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void pkcs8Test() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey="
+ + clientKey + PKCS8_KEY_SUFFIX;
+ try (Connection conn = DriverManager.getConnection(conStr)) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with PKCS8 private key that has been encrypted with a password.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void pkcs8EncryptedTest() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey="
+ + clientKey + ENCRYPTED_PKCS8_KEY_SUFFIX + "clientKeyPassword=" + clientKeyPassword + ";";
+ try (Connection conn = DriverManager.getConnection(conStr)) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with PFX private key.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void pfxTest() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PFX_KEY_SUFFIX;
+ try (Connection conn = DriverManager.getConnection(conStr)) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with PFX private key that has been encrypted with a password.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void pfxEncrytedTest() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + ENCRYPTED_PFX_KEY_SUFFIX
+ + "clientKeyPassword=" + clientKeyPassword + ";";
+ try (Connection conn = DriverManager.getConnection(conStr)) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with PVK private key.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void pvkTest() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + CER_SUFFIX + "clientKey="
+ + clientKey + PVK_SUFFIX + "clientKeyPassword=" + clientKeyPassword + ";";
+ try (Connection conn = DriverManager.getConnection(conStr)) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with invalid certificate provided.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void invalidCert() throws Exception {
+ String conStr = connectionString + ";clientCertificate=invalid_path;" + "clientKeyPassword=" + clientKeyPassword
+ + ";";
+ try (Connection conn = DriverManager.getConnection(conStr)) {} catch (SQLServerException e) {
+ assertTrue(e.getCause().getMessage().matches(TestUtils.formatErrorMsg("R_clientCertError")));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with invalid certificate password provided.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void invalidCertPassword() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PFX_KEY_SUFFIX
+ + "clientKeyPassword=invalid_password;";
+ try (Connection conn = DriverManager.getConnection(conStr)) {} catch (SQLServerException e) {
+ assertTrue(e.getMessage().contains(TestResource.getResource("R_keystorePassword")));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature using a data source.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testDataSource() throws Exception {
+ SQLServerDataSource dsLocal = new SQLServerDataSource();
+ AbstractTest.updateDataSource(connectionString, dsLocal);
+ dsLocal.setClientCertificate(clientCertificate + PEM_SUFFIX.substring(0, PEM_SUFFIX.length() - 1));
+ dsLocal.setClientKey(
+ clientKey + ENCRYPTED_PKCS1_KEY_SUFFIX.substring(0, ENCRYPTED_PKCS1_KEY_SUFFIX.length() - 1));
+ dsLocal.setClientKeyPassword(clientKeyPassword);
+
+ try (Connection conn = dsLocal.getConnection()) {
+ assertTrue(conn.isValid(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with encryption turned on.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testEncryptTrusted() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey="
+ + clientKey + PKCS8_KEY_SUFFIX + "encrypt=true;trustServerCertificate=true;";
+ try (Connection conn = DriverManager.getConnection(conStr); Statement stmt = conn.createStatement()) {
+ ResultSet rs = stmt
+ .executeQuery("SELECT encrypt_option FROM sys.dm_exec_connections WHERE session_id = @@SPID");
+ rs.next();
+ assertTrue(rs.getBoolean(1));
+ }
+ }
+
+ /**
+ * Tests client certificate authentication feature with encryption turned on, untrusted.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testEncryptUntrusted() throws Exception {
+ String conStr = connectionString + ";clientCertificate=" + clientCertificate + PEM_SUFFIX + "clientKey="
+ + clientKey + PKCS8_KEY_SUFFIX + "encrypt=true;trustServerCertificate=false;trustStore="
+ + trustStorePath;
+ try (Connection conn = DriverManager.getConnection(conStr); Statement stmt = conn.createStatement()) {
+ ResultSet rs = stmt
+ .executeQuery("SELECT encrypt_option FROM sys.dm_exec_connections WHERE session_id = @@SPID");
+ rs.next();
+ assertTrue(rs.getBoolean(1));
+ }
+ }
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ActivityIDTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/ActivityIDTest.java
index 9af431ded..0b48bf499 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/ActivityIDTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/ActivityIDTest.java
@@ -1,3 +1,7 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
package com.microsoft.sqlserver.jdbc;
import static org.junit.jupiter.api.Assertions.assertEquals;
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
index 80d641d22..488091591 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
@@ -63,6 +63,7 @@ protected Object[][] getContents() {
{"R_wrongExceptionMessage", "Wrong exception message"},
{"R_parameterNotDefined", "Parameter {0} was not defined"},
{"R_unexpectedExceptionContent", "Unexpected content in exception message"},
+ {"R_connectionClosed", "The connection has been closed"},
{"R_conversionFailed", "Conversion failed when converting {0} to {1} data type"},
{"R_invalidQueryTimeout", "The query timeout value {0} is not valid."},
{"R_skipAzure", "Skipping test case on Azure SQL."},
@@ -183,5 +184,6 @@ protected Object[][] getContents() {
{"R_resultSetEmpty", "Result set is empty."}, {"R_AlterAEv2Error", "Alter Column Encryption failed."},
{"R_RichQueryError", "Rich query failed."}, {"R_reqExternalSetup", "External setup for test required."},
{"R_invalidEnclaveSessionFailed", "invalidate enclave session failed."},
- {"R_invalidEnclaveType", "Invalid enclave type {0}."}};
+ {"R_invalidEnclaveType", "Invalid enclave type {0}."},
+ {"R_keystorePassword", "keystore password was incorrect"}};
}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java
index 83e91bc13..aa12e8fee 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/SSLProtocolTest.java
@@ -45,7 +45,8 @@ public void testWithSupportedProtocols(String sslProtocol) throws Exception {
// Some older versions of SQLServer might not have all the TLS protocol versions enabled.
// Example, if the highest TLS version enabled in the server is TLSv1.1,
// the connection will fail if we enable only TLSv1.2
- assertTrue(e.getMessage().contains(TestResource.getResource("R_noProtocolVersion")));
+ assertTrue(e.getMessage().contains(TestResource.getResource("R_noProtocolVersion"))
+ || e.getCause().getCause().getMessage().contains(TestResource.getResource("R_connectionClosed")));
}
}
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
index 159f382f8..9f2de885d 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java
@@ -63,6 +63,12 @@ public abstract class AbstractTest {
protected static String[] enclaveServer = null;
protected static String[] enclaveAttestationUrl = null;
protected static String[] enclaveAttestationProtocol = null;
+
+ protected static String clientCertificate = null;
+ protected static String clientKey = null;
+ protected static String clientKeyPassword = "";
+
+ protected static String trustStorePath = "";
protected static String javaKeyPath = null;
protected static String javaKeyAliases = null;
@@ -139,6 +145,14 @@ public static void setup() throws Exception {
prop = getConfiguredProperty("enclaveAttestationProtocol", null);
enclaveAttestationProtocol = null != prop ? prop.split(Constants.SEMI_COLON) : null;
+
+ clientCertificate = getConfiguredProperty("clientCertificate", null);
+
+ clientKey = getConfiguredProperty("clientKey", null);
+
+ clientKeyPassword = getConfiguredProperty("clientKeyPassword", "");
+
+ trustStorePath = getConfiguredProperty("trustStore", "");
Map map = new HashMap();
if (null == jksProvider) {
@@ -295,6 +309,15 @@ protected static ISQLServerDataSource updateDataSource(String connectionString,
case Constants.ENCLAVE_ATTESTATIONPROTOCOL:
ds.setEnclaveAttestationProtocol(value);
break;
+ case Constants.CLIENT_CERTIFICATE:
+ ds.setClientCertificate(value);
+ break;
+ case Constants.CLIENT_KEY:
+ ds.setClientKey(value);
+ break;
+ case Constants.CLIENT_KEY_PASSWORD:
+ ds.setClientKeyPassword(value);
+ break;
default:
break;
}
diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
index b7d452bc4..fa809341f 100644
--- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
+++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java
@@ -24,6 +24,7 @@ private Constants() {}
* xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance
* NTLM - - - - - - - For NTLM tests
* reqExternalSetup - For tests requiring external setup
+ * clientCertAuth - - For tests requiring client certificate authentication setup
*
*/
public static final String xJDBC42 = "xJDBC42";
@@ -36,6 +37,7 @@ private Constants() {}
public static final String xAzureSQLMI = "xAzureSQLMI";
public static final String NTLM = "NTLM";
public static final String reqExternalSetup = "reqExternalSetup";
+ public static final String clientCertAuth = "clientCertAuth";
public static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();
public static final Logger LOGGER = Logger.getLogger("AbstractTest");
@@ -139,7 +141,11 @@ private Constants() {}
public static final String ENCLAVE_ATTESTATIONURL = "enclaveAttestationUrl";
public static final String ENCLAVE_ATTESTATIONPROTOCOL = "enclaveAttestationProtocol";
-
+
+ public static final String CLIENT_CERTIFICATE = "CLIENTCERTIFICATE";
+ public static final String CLIENT_KEY = "CLIENTKEY";
+ public static final String CLIENT_KEY_PASSWORD = "CLIENTKEYPASSWORD";
+
public static final String CONFIG_PROPERTIES_FILE = "config.properties";
public enum LOB {