Repository: microsoft/mssql-jdbc
Pull request #1773 (2022-03-21 19:40:56)

Add serverCertificate connection property for encrypt=strict


Patch:
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index d242a44b..cd1bed31 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -35,8 +35,6 @@ import java.nio.charset.Charset;
 import java.security.KeyStore;
 import java.security.Provider;
 import java.security.Security;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
 import java.sql.Timestamp;
 import java.text.MessageFormat;
 import java.time.LocalDate;
@@ -45,11 +43,9 @@ import java.time.OffsetTime;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
-import java.util.Collection;
 import java.util.GregorianCalendar;
 import java.util.Iterator;
 import java.util.LinkedList;
-import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -1538,274 +1534,6 @@ final class TDSChannel implements Serializable {
         }
     }
 
-    /**
-     * This class implements an X509TrustManager that always accepts the X509Certificate chain offered to it.
-     *
-     * A PermissiveX509TrustManager is used to "verify" the authenticity of the server when the trustServerCertificate
-     * connection property is set to true.
-     */
-    private final class PermissiveX509TrustManager implements X509TrustManager {
-        private final TDSChannel tdsChannel;
-        private final Logger logger;
-        private final String logContext;
-
-        PermissiveX509TrustManager(TDSChannel tdsChannel) {
-            this.tdsChannel = tdsChannel;
-            this.logger = tdsChannel.getLogger();
-            this.logContext = tdsChannel.toString() + " (PermissiveX509TrustManager):";
-        }
-
-        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINER))
-                logger.finer(logContext + " Trusting client certificate (!)");
-        }
-
-        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINER))
-                logger.finer(logContext + " Trusting server certificate");
-        }
-
-        public X509Certificate[] getAcceptedIssuers() {
-            return new X509Certificate[0];
-        }
-    }
-
-    /**
-     * This class implements an X509TrustManager that hostname for validation.
-     *
-     * This validates the subject name in the certificate with the host name
-     */
-    private final class HostNameOverrideX509TrustManager implements X509TrustManager {
-        private final Logger logger;
-        private final String logContext;
-        private final X509TrustManager defaultTrustManager;
-        private String hostName;
-
-        HostNameOverrideX509TrustManager(TDSChannel tdsChannel, X509TrustManager tm, String hostName) {
-            this.logger = tdsChannel.getLogger();
-            this.logContext = tdsChannel.toString() + " (HostNameOverrideX509TrustManager):";
-            defaultTrustManager = tm;
-            // canonical name is in lower case so convert this to lowercase too.
-            this.hostName = hostName.toLowerCase(Locale.ENGLISH);
-        }
-
-        // Parse name in RFC 2253 format
-        // Returns the common name if successful, null if failed to find the common name.
-        // The parser tuned to be safe than sorry so if it sees something it cant parse correctly it returns null
-        private String parseCommonName(String distinguishedName) {
-            int index;
-            // canonical name converts entire name to lowercase
-            index = distinguishedName.indexOf("cn=");
-            if (index == -1) {
-                return null;
-            }
-            distinguishedName = distinguishedName.substring(index + 3);
-            // Parse until a comma or end is reached
-            // Note the parser will handle gracefully (essentially will return empty string) , inside the quotes (e.g
-            // cn="Foo, bar") however
-            // RFC 952 says that the hostName cant have commas however the parser should not (and will not) crash if it
-            // sees a , within quotes.
-            for (index = 0; index < distinguishedName.length(); index++) {
-                if (distinguishedName.charAt(index) == ',') {
-                    break;
-                }
-            }
-            String commonName = distinguishedName.substring(0, index);
-            // strip any quotes
-            if (commonName.length() > 1 && ('\"' == commonName.charAt(0))) {
-                if ('\"' == commonName.charAt(commonName.length() - 1))
-                    commonName = commonName.substring(1, commonName.length() - 1);
-                else {
-                    // Be safe the name is not ended in " return null so the common Name wont match
-                    commonName = null;
-                }
-            }
-            return commonName;
-        }
-
-        private boolean validateServerName(String nameInCert) {
-            // Failed to get the common name from DN or empty CN
-            if (null == nameInCert) {
-                if (logger.isLoggable(Level.FINER)) {
-                    logger.finer(logContext + " Failed to parse the name from the certificate or name is empty.");
-                }
-                return false;
-            }
-            // We do not allow wildcards in IDNs (xn--).
-            if (!nameInCert.startsWith("xn--") && nameInCert.contains("*")) {
-                int hostIndex = 0, certIndex = 0, match = 0, startIndex = -1, periodCount = 0;
-                while (hostIndex < hostName.length()) {
-                    if ('.' == hostName.charAt(hostIndex)) {
-                        periodCount++;
-                    }
-                    if (certIndex < nameInCert.length() && hostName.charAt(hostIndex) == nameInCert.charAt(certIndex)) {
-                        hostIndex++;
-                        certIndex++;
-                    } else if (certIndex < nameInCert.length() && '*' == nameInCert.charAt(certIndex)) {
-                        startIndex = certIndex;
-                        match = hostIndex;
-                        certIndex++;
-                    } else if (startIndex != -1 && 0 == periodCount) {
-                        certIndex = startIndex + 1;
-                        match++;
-                        hostIndex = match;
-                    } else {
-                        logFailMessage(nameInCert);
-                        return false;
-                    }
-                }
-                if (nameInCert.length() == certIndex && periodCount > 1) {
-                    logSuccessMessage(nameInCert);
-                    return true;
-                } else {
-                    logFailMessage(nameInCert);
-                    return false;
-                }
-            }
-            // Verify that the name in certificate matches exactly with the host name
-            if (!nameInCert.equals(hostName)) {
-                logFailMessage(nameInCert);
-                return false;
-            }
-            logSuccessMessage(nameInCert);
-            return true;
-        }
-
-        private void logFailMessage(String nameInCert) {
-            if (logger.isLoggable(Level.FINER)) {
-                logger.finer(logContext + " The name in certificate " + nameInCert
-                        + " does not match with the server name " + hostName + ".");
-            }
-        }
-
-        private void logSuccessMessage(String nameInCert) {
-            if (logger.isLoggable(Level.FINER)) {
-                logger.finer(logContext + " The name in certificate:" + nameInCert + " validated against server name "
-                        + hostName + ".");
-            }
-        }
-
-        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(logContext + " Forwarding ClientTrusted.");
-            defaultTrustManager.checkClientTrusted(chain, authType);
-            // Explicitly validate the expiry dates
-            for (X509Certificate cert : chain) {
-                cert.checkValidity();
-            }
-        }
-
-        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(logContext + " Forwarding Trusting server certificate");
-            defaultTrustManager.checkServerTrusted(chain, authType);
-            // Explicitly validate the expiry dates
-            for (X509Certificate cert : chain) {
-                cert.checkValidity();
-            }
-            if (logger.isLoggable(Level.FINEST))
-                logger.finest(logContext + " default serverTrusted succeeded proceeding with server name validation");
-
-            validateServerNameInCertificate(chain[0]);
-        }
-
-        private void validateServerNameInCertificate(X509Certificate cert) throws CertificateException {
-            String nameInCertDN = cert.getSubjectX500Principal().getName("canonical");
-            if (logger.isLoggable(Level.FINER)) {
-                logger.finer(logContext + " Validating the server name:" + hostName);
-                logger.finer(logContext + " The DN name in certificate:" + nameInCertDN);
-            }
-
-            boolean isServerNameValidated;
-            String dnsNameInSANCert = "";
-
-            // the name in cert is in RFC2253 format parse it to get the actual subject name
-            String subjectCN = parseCommonName(nameInCertDN);
-
-            isServerNameValidated = validateServerName(subjectCN);
-
-            if (!isServerNameValidated) {
-
-                Collection<List<?>> sanCollection = cert.getSubjectAlternativeNames();
-
-                if (sanCollection != null) {
-                    // find a subjectAlternateName entry corresponding to DNS Name
-                    for (List<?> sanEntry : sanCollection) {
-
-                        if (sanEntry != null && sanEntry.size() >= 2) {
-                            Object key = sanEntry.get(0);
-                            Object value = sanEntry.get(1);
-
-                            if (logger.isLoggable(Level.FINER)) {
-                                logger.finer(logContext + "Key: " + key + "; KeyClass:"
-                                        + (key != null ? key.getClass() : null) + ";value: " + value + "; valueClass:"
-                                        + (value != null ? value.getClass() : null));
-
-                            }
-
-                            // From
-                            // Documentation(http://download.oracle.com/javase/6/docs/api/java/security/cert/X509Certificate.html):
-                            // "Note that the Collection returned may contain
-                            // more than one name of the same type."
-                            // So, more than one entry of dnsNameType can be present.
-                            // Java docs guarantee that the first entry in the list will be an integer.
-                            // 2 is the sequence no of a dnsName
-                            if ((key != null) && (key instanceof Integer) && ((Integer) key == 2)) {
-                                // As per RFC2459, the DNSName will be in the
-                                // "preferred name syntax" as specified by RFC
-                                // 1034 and the name can be in upper or lower case.
-                                // And no significance is attached to case.
-                                // Java docs guarantee that the second entry in the list
-                                // will be a string for dnsName
-                                if (value != null && value instanceof String) {
-                                    dnsNameInSANCert = (String) value;
-
-                                    // Use English locale to avoid Turkish i issues.
-                                    // Note that, this conversion was not necessary for
-                                    // cert.getSubjectX500Principal().getName("canonical");
-                                    // as the above API already does this by default as per documentation.
-                                    dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);
-
-                                    isServerNameValidated = validateServerName(dnsNameInSANCert);
-
-                                    if (isServerNameValidated) {
-                                        if (logger.isLoggable(Level.FINER)) {
-                                            logger.finer(logContext + " found a valid name in certificate: "
-                                                    + dnsNameInSANCert);
-                                        }
-                                        break;
-                                    }
-                                }
-
-                                if (logger.isLoggable(Level.FINER)) {
-                                    logger.finer(logContext
-                                            + " the following name in certificate does not match the serverName: "
-                                            + value);
-                                }
-                            }
-
-                        } else {
-                            if (logger.isLoggable(Level.FINER)) {
-                                logger.finer(logContext + " found an invalid san entry: " + sanEntry);
-                            }
-                        }
-                    }
-
-                }
-            }
-
-            if (!isServerNameValidated) {
-                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_certNameFailed"));
-                Object[] msgArgs = {hostName, dnsNameInSANCert};
-                throw new CertificateException(form.format(msgArgs));
-            }
-        }
-
-        public X509Certificate[] getAcceptedIssuers() {
-            return defaultTrustManager.getAcceptedIssuers();
-        }
-    }
-
     enum SSLHandhsakeState {
         SSL_HANDHSAKE_NOT_STARTED,
         SSL_HANDHSAKE_STARTED,
@@ -1860,15 +1588,18 @@ final class TDSChannel implements Serializable {
                 trustStoreType = SQLServerDriverStringProperty.TRUST_STORE_TYPE.getDefaultValue();
             }
 
+            String serverCert = con.activeConnectionProperties
+                    .getProperty(SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString());
+
             isFips = Boolean.valueOf(
                     con.activeConnectionProperties.getProperty(SQLServerDriverBooleanProperty.FIPS.toString()));
-            sslProtocol = con.activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.SSL_PROTOCOL.toString());
-
             if (isFips) {
                 validateFips(trustStoreType, trustStoreFileName);
             }
 
+            sslProtocol = con.activeConnectionProperties
+                    .getProperty(SQLServerDriverStringProperty.SSL_PROTOCOL.toString());
+
             byte requestedEncryptLevel = con.getRequestedEncryptionLevel();
             assert TDS.ENCRYPT_OFF == requestedEncryptLevel || // Login only SSL
                     TDS.ENCRYPT_ON == requestedEncryptLevel || // Full SSL
@@ -1895,88 +1626,101 @@ final class TDSChannel implements Serializable {
             // Otherwise, we'll validate the certificate using a real TrustManager obtained
             // from the a security provider that is capable of validating X.509 certificates.
             else {
-                if (logger.isLoggable(Level.FINER))
-                    logger.finer(toString() + " SSL handshake will validate server certificate");
-
-                KeyStore ks = null;
+                if (isTDSS) {
+                    if (logger.isLoggable(Level.FINEST))
+                        logger.finest(toString() + " Verify server certificate for TDSS");
 
-                // If we are using the system default trustStore and trustStorePassword
-                // then we can skip all of the KeyStore loading logic below.
-                // The security provider's implementation takes care of everything for us.
-                if (null == trustStoreFileName && null == trustStorePassword) {
+                    if (null != hostNameInCertificate) {
+                        tm = new TrustManager[] {
+                                new ServerCertificateX509TrustManager(this, serverCert, hostNameInCertificate)};
+                    } else {
+                        tm = new TrustManager[] {new ServerCertificateX509TrustManager(this, serverCert, host)};
+                    }
+                } else {
                     if (logger.isLoggable(Level.FINER))
-                        logger.finer(toString() + " Using system default trust store and password");
-                }
+                        logger.finer(toString() + " SSL handshake will validate server certificate");
 
-                // Otherwise either the trustStore, trustStorePassword, or both was specified.
-                // In that case, we need to load up a KeyStore ourselves.
-                else {
-                    // First, obtain an interface to a KeyStore that can load trust material
-                    // stored in Java Key Store (JKS) format.
-                    if (logger.isLoggable(Level.FINEST))
-                        logger.finest(toString() + " Finding key store interface");
+                    KeyStore ks = null;
 
-                    ks = KeyStore.getInstance(trustStoreType);
-                    ksProvider = ks.getProvider();
+                    // If we are using the system default trustStore and trustStorePassword
+                    // then we can skip all of the KeyStore loading logic below.
+                    // The security provider's implementation takes care of everything for us.
+                    if (null == trustStoreFileName && null == trustStorePassword && !isTDSS) {
+                        if (logger.isLoggable(Level.FINER)) {
+                            logger.finer(toString() + " Using system default trust store and password");
+                        }
+                    } else {
+                        // Otherwise either the trustStore, trustStorePassword, or both was specified.
+                        // In that case, we need to load up a KeyStore ourselves.
 
-                    // Next, load up the trust store file from the specified location.
-                    // Note: This function returns a null InputStream if the trust store cannot
-                    // be loaded. This is by design. See the method comment and documentation
-                    // for KeyStore.load for details.
-                    InputStream is = loadTrustStore(trustStoreFileName);
+                        // First, obtain an interface to a KeyStore that can load trust material
+                        // stored in Java Key Store (JKS) format.
+                        if (logger.isLoggable(Level.FINEST))
+                            logger.finest(toString() + " Finding key store interface");
 
-                    // Finally, load the KeyStore with the trust material (if any) from the
-                    // InputStream and close the stream.
-                    if (logger.isLoggable(Level.FINEST))
-                        logger.finest(toString() + " Loading key store");
+                        ks = KeyStore.getInstance(trustStoreType);
+                        ksProvider = ks.getProvider();
 
-                    try {
-                        ks.load(is, (null == trustStorePassword) ? null : trustStorePassword.toCharArray());
-                    } finally {
-                        // We are also done with the trust store input stream.
-                        if (null != is) {
-                            try {
-                                is.close();
-                            } catch (IOException e) {
-                                if (logger.isLoggable(Level.FINE))
-                                    logger.fine(toString() + " Ignoring error closing trust material InputStream...");
+                        // Next, load up the trust store file from the specified location.
+                        // Note: This function returns a null InputStream if the trust store cannot
+                        // be loaded. This is by design. See the method comment and documentation
+                        // for KeyStore.load for details.
+                        InputStream is = loadTrustStore(trustStoreFileName);
+
+                        // Finally, load the KeyStore with the trust material (if any) from the
+                        // InputStream and close the stream.
+                        if (logger.isLoggable(Level.FINEST))
+                            logger.finest(toString() + " Loading key store");
+
+                        try {
+                            ks.load(is, (null == trustStorePassword) ? null : trustStorePassword.toCharArray());
+                        } finally {
+                            // We are also done with the trust store input stream.
+                            if (null != is) {
+                                try {
+                                    is.close();
+                                } catch (IOException e) {
+                                    if (logger.isLoggable(Level.FINE))
+                                        logger.fine(
+                                                toString() + " Ignoring error closing trust material InputStream...");
+                                }
                             }
                         }
                     }
-                }
 
-                // Either we now have a KeyStore populated with trust material or we are using the
-                // default source of trust material (cacerts). Either way, we are now ready to
-                // use a TrustManagerFactory to create a TrustManager that uses the trust material
-                // to validate the server certificate.
+                    // Either we now have a KeyStore populated with trust material or we are using the
+                    // default source of trust material (cacerts). Either way, we are now ready to
+                    // use a TrustManagerFactory to create a TrustManager that uses the trust material
+                    // to validate the server certificate.
 
-                // Next step is to get a TrustManagerFactory that can produce TrustManagers
-                // that understands X.509 certificates.
-                TrustManagerFactory tmf = null;
+                    // Next step is to get a TrustManagerFactory that can produce TrustManagers
+                    // that understands X.509 certificates.
+                    TrustManagerFactory tmf = null;
 
-                if (logger.isLoggable(Level.FINEST))
-                    logger.finest(toString() + " Locating X.509 trust manager factory");
+                    if (logger.isLoggable(Level.FINEST))
+                        logger.finest(toString() + " Locating X.509 trust manager factory");
 
-                tmfDefaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
-                tmf = TrustManagerFactory.getInstance(tmfDefaultAlgorithm);
-                tmfProvider = tmf.getProvider();
+                    tmfDefaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+                    tmf = TrustManagerFactory.getInstance(tmfDefaultAlgorithm);
+                    tmfProvider = tmf.getProvider();
 
-                // Tell the TrustManagerFactory to give us TrustManagers that we can use to
-                // validate the server certificate using the trust material in the KeyStore.
-                if (logger.isLoggable(Level.FINEST))
-                    logger.finest(toString() + " Getting trust manager");
+                    // Tell the TrustManagerFactory to give us TrustManagers that we can use to
+                    // validate the server certificate using the trust material in the KeyStore.
+                    if (logger.isLoggable(Level.FINEST))
+                        logger.finest(toString() + " Getting trust manager");
 
-                tmf.init(ks);
-                tm = tmf.getTrustManagers();
+                    tmf.init(ks);
+                    tm = tmf.getTrustManagers();
 
-                // if the host name in cert provided use it or use the host name Only if it is not FIPS
-                if (!isFips) {
-                    if (null != hostNameInCertificate) {
-                        tm = new TrustManager[] {new HostNameOverrideX509TrustManager(this, (X509TrustManager) tm[0],
-                                hostNameInCertificate)};
-                    } else {
-                        tm = new TrustManager[] {
-                                new HostNameOverrideX509TrustManager(this, (X509TrustManager) tm[0], host)};
+                    // if the host name in cert provided use it or use the host name Only if it is not FIPS
+                    if (!isFips) {
+                        if (null != hostNameInCertificate) {
+                            tm = new TrustManager[] {new HostNameOverrideX509TrustManager(this,
+                                    (X509TrustManager) tm[0], hostNameInCertificate)};
+                        } else {
+                            tm = new TrustManager[] {
+                                    new HostNameOverrideX509TrustManager(this, (X509TrustManager) tm[0], host)};
+                        }
                     }
                 }
             } // end if (!con.trustServerCertificate())
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index 0102e6ea..a6d27b80 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -125,6 +125,21 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
      */
     String getEncrypt();
 
+    /**
+     * Returns the path to the server certificate.
+     *
+     * @return serverCertificate property value
+     */
+    String getServerCertificate();
+
+    /**
+     * Sets the connection property 'serverCertificate' on the connection.
+     *
+     * @param cert
+     *        The path to the server certificate.
+     */
+    void setServerCertificate(String cert);
+
     /**
      * Sets the value to enable/disable Transparent Network IP Resolution (TNIR). Beginning in version 6.0 of the
      * Microsoft JDBC Driver for SQL Server, a new connection property transparentNetworkIPResolution (TNIR) is added
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
index fd7134e7..162d592f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java
@@ -17,6 +17,7 @@ import java.math.BigInteger;
 import java.nio.Buffer;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.security.GeneralSecurityException;
@@ -35,8 +36,14 @@ import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.RSAPrivateCrtKeySpec;
+import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Base64;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import javax.crypto.Cipher;
 import javax.crypto.SecretKey;
@@ -54,6 +61,9 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
 
 final class SQLServerCertificateUtils {
 
+    private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.SQLServerCertificateUtils");
+    private static final String logContext = Thread.currentThread().getStackTrace()[1].getClassName() + ": ";
+
     static KeyManager[] getKeyManagerFromFile(String certPath, String keyPath,
             String keyPassword) throws IOException, GeneralSecurityException, SQLServerException {
         if (keyPath != null && keyPath.length() > 0) {
@@ -63,6 +73,237 @@ final class SQLServerCertificateUtils {
         }
     }
 
+    /**
+     * Parse name in RFC 2253 format Returns the common name if successful, null if failed to find the common name. The
+     * parser tuned to be safe than sorry so if it sees something it can't parse correctly it returns null
+     * 
+     * @param distinguishedName
+     *        server name to parse
+     * @return subject name
+     */
+    static String parseCommonName(String distinguishedName) {
+        int index;
+        // canonical name converts entire name to lowercase
+        index = distinguishedName.indexOf("cn=");
+        if (index == -1) {
+            return null;
+        }
+        distinguishedName = distinguishedName.substring(index + 3);
+        // Parse until a comma or end is reached
+        // Note the parser will handle gracefully (essentially will return empty string) , inside the quotes (e.g
+        // cn="Foo, bar") however
+        // RFC 952 says that the hostName cant have commas however the parser should not (and will not) crash if it
+        // sees a , within quotes.
+        for (index = 0; index < distinguishedName.length(); index++) {
+            if (distinguishedName.charAt(index) == ',') {
+                break;
+            }
+        }
+        String commonName = distinguishedName.substring(0, index);
+        // strip any quotes
+        if (commonName.length() > 1 && ('\"' == commonName.charAt(0))) {
+            if ('\"' == commonName.charAt(commonName.length() - 1))
+                commonName = commonName.substring(1, commonName.length() - 1);
+            else {
+                // Be safe the name is not ended in " return null so the common Name wont match
+                commonName = null;
+            }
+        }
+        return commonName;
+    }
+
+    /**
+     * Validate server name in certificate matches hostname
+     * 
+     * @param nameInCert
+     *        server name in certificate
+     * @param hostName
+     *        hostname
+     * @return if the server name is valid and matches hostname
+     */
+    static boolean validateServerName(String nameInCert, String hostName) {
+        // Failed to get the common name from DN or empty CN
+        if (null == nameInCert) {
+            if (logger.isLoggable(Level.FINER)) {
+                logger.finer(logContext + " Failed to parse the name from the certificate or name is empty.");
+            }
+            return false;
+        }
+        // We do not allow wildcards in IDNs (xn--).
+        if (!nameInCert.startsWith("xn--") && nameInCert.contains("*")) {
+            int hostIndex = 0, certIndex = 0, match = 0, startIndex = -1, periodCount = 0;
+            while (hostIndex < hostName.length()) {
+                if ('.' == hostName.charAt(hostIndex)) {
+                    periodCount++;
+                }
+                if (certIndex < nameInCert.length() && hostName.charAt(hostIndex) == nameInCert.charAt(certIndex)) {
+                    hostIndex++;
+                    certIndex++;
+                } else if (certIndex < nameInCert.length() && '*' == nameInCert.charAt(certIndex)) {
+                    startIndex = certIndex;
+                    match = hostIndex;
+                    certIndex++;
+                } else if (startIndex != -1 && 0 == periodCount) {
+                    certIndex = startIndex + 1;
+                    match++;
+                    hostIndex = match;
+                } else {
+                    logFailMessage(nameInCert, hostName);
+                    return false;
+                }
+            }
+            if (nameInCert.length() == certIndex && periodCount > 1) {
+                logSuccessMessage(nameInCert, hostName);
+                return true;
+            } else {
+                logFailMessage(nameInCert, hostName);
+                return false;
+            }
+        }
+        // Verify that the name in certificate matches exactly with the host name
+        if (!nameInCert.equals(hostName)) {
+            logFailMessage(nameInCert, hostName);
+            return false;
+        }
+        logSuccessMessage(nameInCert, hostName);
+        return true;
+    }
+
+    /**
+     * Validate server name in certificate
+     * 
+     * @param cert
+     *        X509 certificate
+     * @param hostName
+     *        hostname
+     * @throws CertificateException
+     */
+    static void validateServerNameInCertificate(X509Certificate cert, String hostName) throws CertificateException {
+        String nameInCertDN = cert.getSubjectX500Principal().getName("canonical");
+
+        if (logger.isLoggable(Level.FINER)) {
+            logger.finer(logContext + " Validating the server name:" + hostName);
+            logger.finer(logContext + " The DN name in certificate:" + nameInCertDN);
+        }
+
+        boolean isServerNameValidated;
+        String dnsNameInSANCert = "";
+
+        // the name in cert is in RFC2253 format parse it to get the actual subject name
+        String subjectCN = parseCommonName(nameInCertDN);
+
+        isServerNameValidated = validateServerName(subjectCN, hostName);
+
+        if (!isServerNameValidated) {
+            Collection<List<?>> sanCollection = cert.getSubjectAlternativeNames();
+
+            if (sanCollection != null) {
+                // find a subjectAlternateName entry corresponding to DNS Name
+                for (List<?> sanEntry : sanCollection) {
+
+                    if (sanEntry != null && sanEntry.size() >= 2) {
+                        Object key = sanEntry.get(0);
+                        Object value = sanEntry.get(1);
+
+                        if (logger.isLoggable(Level.FINER)) {
+                            logger.finer(logContext + "Key: " + key + "; KeyClass:"
+                                    + (key != null ? key.getClass() : null) + ";value: " + value + "; valueClass:"
+                                    + (value != null ? value.getClass() : null));
+                        }
+
+                        // From
+                        // Documentation(http://download.oracle.com/javase/6/docs/api/java/security/cert/X509Certificate.html):
+                        // "Note that the Collection returned may contain
+                        // more than one name of the same type."
+                        // So, more than one entry of dnsNameType can be present.
+                        // Java docs guarantee that the first entry in the list will be an integer.
+                        // 2 is the sequence no of a dnsName
+                        if ((key != null) && (key instanceof Integer) && ((Integer) key == 2)) {
+                            // As per RFC2459, the DNSName will be in the
+                            // "preferred name syntax" as specified by RFC
+                            // 1034 and the name can be in upper or lower case.
+                            // And no significance is attached to case.
+                            // Java docs guarantee that the second entry in the list
+                            // will be a string for dnsName
+                            if (value != null && value instanceof String) {
+                                dnsNameInSANCert = (String) value;
+
+                                // Use English locale to avoid Turkish i issues.
+                                // Note that, this conversion was not necessary for
+                                // cert.getSubjectX500Principal().getName("canonical");
+                                // as the above API already does this by default as per documentation.
+                                dnsNameInSANCert = dnsNameInSANCert.toLowerCase(Locale.ENGLISH);
+
+                                isServerNameValidated = validateServerName(dnsNameInSANCert, hostName);
+                                if (isServerNameValidated) {
+                                    if (logger.isLoggable(Level.FINER)) {
+                                        logger.finer(
+                                                logContext + " found a valid name in certificate: " + dnsNameInSANCert);
+                                    }
+                                    break;
+                                }
+                            }
+
+                            if (logger.isLoggable(Level.FINER)) {
+                                logger.finer(logContext
+                                        + " the following name in certificate does not match the serverName: " + value);
+                            }
+                        }
+
+                    } else {
+                        if (logger.isLoggable(Level.FINER)) {
+                            logger.finer(logContext + " found an invalid san entry: " + sanEntry);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!isServerNameValidated) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_certNameFailed"));
+            Object[] msgArgs = {hostName, dnsNameInSANCert};
+            throw new CertificateException(form.format(msgArgs));
+        }
+    }
+
+    /**
+     * Validate certificate provided in path against server X509 certificate
+     * 
+     * @param cert
+     *        X509 certificate
+     * @param certFile
+     *        path to certificate file to validate
+     * @throws CertificateException
+     */
+    static void validateServerCerticate(X509Certificate cert, String certFile) throws CertificateException {
+        try (InputStream is = fileToStream(certFile)) {
+            if (!CertificateFactory.getInstance("X509").generateCertificate(is).getPublicKey()
+                    .equals(cert.getPublicKey())) {
+                MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverCertError"));
+                Object[] msgArgs = {certFile};
+                throw new CertificateException(form.format(msgArgs));
+            }
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverCertError"));
+            Object[] msgArgs = {certFile, e.getMessage()};
+            throw new CertificateException(form.format(msgArgs));
+        }
+    }
+
+    private static void logFailMessage(String nameInCert, String hostName) {
+        if (logger.isLoggable(Level.FINER)) {
+            logger.finer(logContext + " The name in certificate " + nameInCert + " does not match with the server name "
+                    + hostName + ".");
+        }
+    }
+
+    private static void logSuccessMessage(String nameInCert, String hostName) {
+        if (logger.isLoggable(Level.FINER)) {
+            logger.finer(logContext + " The name in certificate:" + nameInCert + " validated against server name "
+                    + hostName + ".");
+        }
+    }
+
     // PKCS#12 format
     private static final String PKCS12_ALG = "PKCS12";
     private static final String SUN_X_509 = "SunX509";
@@ -148,8 +389,8 @@ final class SQLServerCertificateUtils {
             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);
+
+        try (FileInputStream in = new FileInputStream(f); FileChannel channel = in.getChannel()) {
             ((Buffer) buffer.order(ByteOrder.LITTLE_ENDIAN)).rewind();
 
             long magic = buffer.getInt() & 0xFFFFFFFFL;
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index acc07ae9..c025ef72 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -754,6 +754,13 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         return trustServerCertificate;
     }
 
+    /** server certificate for encrypt=strict */
+    private String serverCertificate = null;
+
+    final String getServerCertificate() {
+        return serverCertificate;
+    }
+
     /** negotiated encryption level */
     private byte negotiatedEncryptionLevel = TDS.ENCRYPT_INVALID;
 
@@ -2094,6 +2101,14 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                     // do not trust server cert for strict
                     trustServerCertificate = false;
 
+                    sPropKey = SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString();
+                    sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                    if (null == sPropValue) {
+                        sPropValue = SQLServerDriverStringProperty.SERVER_CERTIFICATE.getDefaultValue();
+                    }
+                    serverCertificate = activeConnectionProperties
+                            .getProperty(SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString());
+
                     // prelogin TLS handshake is required
                     isTDSS = true;
                 } else {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index 669a190e..f7ea7668 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -323,6 +323,16 @@ public class SQLServerDataSource
                 SQLServerDriverStringProperty.ENCRYPT.getDefaultValue());
     }
 
+    @Override
+    public void setServerCertificate(String cert) {
+        setStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString(), cert);
+    }
+
+    @Override
+    public String getServerCertificate() {
+        return getStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString(), null);
+    }
+
     @Override
     public void setTransparentNetworkIPResolution(boolean tnir) {
         setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString(),
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index ff123be9..67b7e604 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -453,7 +453,8 @@ enum SQLServerDriverStringProperty {
     AAD_SECURE_PRINCIPAL_ID("AADSecurePrincipalId", ""),
     AAD_SECURE_PRINCIPAL_SECRET("AADSecurePrincipalSecret", ""),
     MAX_RESULT_BUFFER("maxResultBuffer", "-1"),
-    ENCRYPT("encrypt", EncryptOption.True.toString());
+    ENCRYPT("encrypt", EncryptOption.True.toString()),
+    SERVER_CERTIFICATE("serverCertificate", "");
 
     private final String name;
     private final String defaultValue;
@@ -595,6 +596,8 @@ public final class SQLServerDriver implements java.sql.Driver {
                     new String[] {EncryptOption.False.toString(), EncryptOption.No.toString(),
                             EncryptOption.Optional.toString(), EncryptOption.True.toString(),
                             EncryptOption.Mandatory.toString(), EncryptOption.Strict.toString()}),
+            new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_CERTIFICATE.toString(),
+                    SQLServerDriverStringProperty.SERVER_CERTIFICATE.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.PREPARE_METHOD.toString(),
                     SQLServerDriverStringProperty.PREPARE_METHOD.getDefaultValue(), false,
                     new String[] {PrepareMethod.PREPEXEC.toString(), PrepareMethod.PREPARE.toString()}),
@@ -698,7 +701,6 @@ public final class SQLServerDriver implements java.sql.Driver {
                             SqlAuthentication.ActiveDirectoryMSI.toString(),
                             SqlAuthentication.ActiveDirectoryServicePrincipal.toString(),
                             SqlAuthentication.ActiveDirectoryInteractive.toString()}),
-
             new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(),
                     Integer.toString(SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue()), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.FIPS.toString(),
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 94cde8fc..f1186553 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -192,6 +192,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_failoverPartnerPropertyDescription", "The name of the failover server used in a database mirroring configuration."},
         {"R_packetSizePropertyDescription", "The network packet size used to communicate with SQL Server."},
         {"R_encryptPropertyDescription", "Determines if Secure Sockets Layer (SSL) encryption should be used between the client and the server."},
+        {"R_serverCertificatePropertyDescription", "The path to the server certificate file."},
         {"R_prepareMethodPropertyDescription", "Determines the prepare method used in the driver."},
         {"R_socketFactoryClassPropertyDescription", "The class to instantiate as the SocketFactory for connections"},
         {"R_socketFactoryConstructorArgPropertyDescription", "The optional argument to pass to the constructor specified by socketFactoryClass"},
@@ -285,8 +286,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_PlainTextNullAE", "Internal error. Plaintext value cannot be null."},
         {"R_StreamingDataTypeAE", "Data of length greater than {0} is not supported in encrypted {1} column."},
         {"R_AE_NotSupportedByServer", "SQL Server in use does not support column encryption."},
-        {"R_InvalidAEVersionNumber", "Received invalid version number \"{0}\" for Always Encrypted."}, // From
-                                                                                                       // Server
+        {"R_InvalidAEVersionNumber", "Received invalid version number \"{0}\" for Always Encrypted."}, // From server
         {"R_NullEncryptedColumnEncryptionKey", "Internal error. Encrypted column encryption key cannot be null."},
         {"R_EmptyEncryptedColumnEncryptionKey", "Internal error. Empty encrypted column encryption key specified."},
         {"R_InvalidMasterKeyDetails", "Invalid master key details specified."},
@@ -503,6 +503,8 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_crClientSSLStateNotRecoverable", "The server did not preserve SSL encryption during a recovery attempt, connection recovery is not possible."},
         {"R_crCommandCannotTimeOut", "Request failed to time out and SQLServerConnection does not exist"},
         {"R_UnableLoadAuthDll", "Unable to load authentication DLL {0}"},
+        {"R_illegalArgumentTrustManager", "Interal error. Peer certificate chain or key exchange algorithem can not be null or empty."},
+        {"R_serverCertError", "Error validating Server Certificate: {0}."},
     };
 }
 // @formatter:on
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerTrustManager.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerTrustManager.java
new file mode 100644
index 00000000..7598af75
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerTrustManager.java
@@ -0,0 +1,166 @@
+package com.microsoft.sqlserver.jdbc;
+
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.X509TrustManager;
+
+
+/**
+ * This class implements an X509TrustManager that always accepts the X509Certificate chain offered to it.
+ *
+ * A PermissiveX509TrustManager is used to "verify" the authenticity of the server when the trustServerCertificate
+ * connection property is set to true.
+ */
+final class PermissiveX509TrustManager implements X509TrustManager {
+    private final Logger logger;
+    private final String logContext;
+
+    PermissiveX509TrustManager(TDSChannel tdsChannel) {
+        this.logger = tdsChannel.getLogger();
+        this.logContext = tdsChannel.toString() + " (PermissiveX509TrustManager):";
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINER))
+            logger.finer(logContext + " Trusting client certificate (!)");
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINER))
+            logger.finer(logContext + " Trusting server certificate");
+    }
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+}
+
+
+/**
+ * This class implements an X509TrustManager that validates hostname.
+ *
+ * This validates the subject name in the certificate with the host name
+ */
+final class HostNameOverrideX509TrustManager implements X509TrustManager {
+    private final Logger logger;
+    private final String logContext;
+    private final X509TrustManager defaultTrustManager;
+    private String hostName;
+
+    HostNameOverrideX509TrustManager(TDSChannel tdsChannel, X509TrustManager tm, String hostName) {
+        this.logger = tdsChannel.getLogger();
+        this.logContext = tdsChannel.toString() + " (HostNameOverrideX509TrustManager):";
+        defaultTrustManager = tm;
+
+        // canonical name is in lower case so convert this to lowercase too.
+        this.hostName = hostName.toLowerCase(Locale.ENGLISH);
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Forwarding ClientTrusted.");
+        }
+
+        defaultTrustManager.checkClientTrusted(chain, authType);
+
+        // Explicitly validate the expiry dates
+        for (X509Certificate cert : chain) {
+            cert.checkValidity();
+        }
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Forwarding Trusting server certificate");
+        }
+
+        defaultTrustManager.checkServerTrusted(chain, authType);
+
+        // Explicitly validate the expiry dates
+        for (X509Certificate cert : chain) {
+            cert.checkValidity();
+        }
+
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Default serverTrusted succeeded proceeding with server name validation");
+        }
+
+        SQLServerCertificateUtils.validateServerNameInCertificate(chain[0], hostName);
+    }
+
+    public X509Certificate[] getAcceptedIssuers() {
+        return defaultTrustManager.getAcceptedIssuers();
+    }
+}
+
+
+/**
+ * This class implements an X509TrustManager that validates the server certificate provided. This is applicable when
+ * encrypt connection property is set to "strict"
+ *
+ */
+final class ServerCertificateX509TrustManager implements X509TrustManager {
+    private final Logger logger;
+    private final String logContext;
+    private String hostName;
+    private String serverCert;
+
+    ServerCertificateX509TrustManager(TDSChannel tdsChannel, String cert, String hostName) {
+        this.logger = tdsChannel.getLogger();
+        this.logContext = tdsChannel.toString() + " (ServerCertificateX509TrustManager):";
+        // canonical name is in lower case so convert this to lowercase too.
+        this.hostName = hostName.toLowerCase(Locale.ENGLISH);
+        this.serverCert = cert;
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST))
+            logger.finest(logContext + " Trusting client certificate (!)");
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+        if (logger.isLoggable(Level.FINEST)) {
+            logger.finest(logContext + " Check if server trusted.");
+        }
+
+        if (null == chain || 0 == chain.length || null == authType || authType.isEmpty()) {
+            throw new IllegalArgumentException(SQLServerException.getErrString("R_illegalArgumentTrustManager"));
+        }
+
+        try {
+            // validate expiry dates
+            for (X509Certificate c : chain) {
+                c.checkValidity();
+            }
+
+            // 1st element in the certificate chain is the server cert
+            // 2nd element in the certificate chain is the trusted root cert
+            if (null == serverCert) {
+                SQLServerCertificateUtils.validateServerNameInCertificate(chain[0], hostName);
+            } else {
+                SQLServerCertificateUtils.validateServerCerticate(chain[0], serverCert);
+            }
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverCertError"));
+            Object[] msgArgs = {serverCert, e.getMessage()};
+            throw new CertificateExpiredException(form.format(msgArgs));
+        }
+    }
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+}


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
index 0f206df8..d94100d4 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
@@ -166,6 +166,9 @@ public class SQLServerConnectionTest extends AbstractTest {
         assertEquals(Boolean.toString(booleanPropValue), ds.getEncrypt(),
                 TestResource.getResource("R_valuesAreDifferent"));
 
+        ds.setServerCertificate(stringPropValue);
+        assertEquals(stringPropValue, ds.getServerCertificate(), TestResource.getResource("R_valuesAreDifferent"));
+
         ds.setPrepareMethod(stringPropValue);
         assertEquals(stringPropValue, ds.getPrepareMethod(), TestResource.getResource("R_valuesAreDifferent"));
 
@@ -906,4 +909,39 @@ public class SQLServerConnectionTest extends AbstractTest {
         persistentTokenAspect.beforeCacheAccess(tokenCacheAccessContext);
         PersistentTokenCacheAccessAspect.clearUserTokenCache();
     }
+
+    /**
+     * test bad serverCertificate property
+     * 
+     * @throws SQLException
+     */
+    @Test
+    public void testBadServerCert() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setServerCertificate("badCert");
+        ds.setEncrypt(Constants.STRICT);
+        ds.setTrustServerCertificate(false);
+
+        // test using datasource
+        try (Connection con = ds.getConnection()) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (SQLException e) {
+            // TODO: servers which do not support TDSS will return SSL failed error, test should be updated once server
+            // available
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_serverCertError"))
+                    || e.getMessage().matches(TestUtils.formatErrorMsg("R_sslFailed")), e.getMessage());
+        }
+
+        // test connection string
+        try (Connection con = PrepUtil.getConnection(
+                connectionString + ";encrypt=strict;trustServerCertificate=false;serverCertificate=badCert")) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (SQLException e) {
+            // TODO: servers which do not support TDSS will return SSL failed error, test should be updated once server
+            // available
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_serverCertError"))
+                    || e.getMessage().matches(TestUtils.formatErrorMsg("R_sslFailed")), e.getMessage());
+        }
+    }
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java
index 9c09d736..5309780c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerDriverTest.java
@@ -15,10 +15,8 @@ import java.sql.Statement;
 import java.util.Enumeration;
 import java.util.Properties;
 import java.util.logging.Logger;
-import java.util.stream.Stream;
 
 import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
@@ -35,7 +33,7 @@ public class SQLServerDriverTest extends AbstractTest {
 
     @BeforeAll
     public static void setupTests() throws Exception {
-        connectionString = TestUtils.addOrOverrideProperty(connectionString,"trustServerCertificate", "true");
+        connectionString = TestUtils.addOrOverrideProperty(connectionString, "trustServerCertificate", "true");
         setConnection();
     }