Repository: microsoft/mssql-jdbc
Pull request #1774 (2022-03-22 19:07:26)

Refactor SQLServerColumnEncryptionAzureKeyVaultProvider usage in SQLServerConnection
This refactors the usage of `SQLServerColumnEncryptionAzureKeyVaultProvider` into private methods.

This is useful for the native-image compilation GraalVM's optimizations, allowing these methods to be dynamically substituted when the Azure classes are not in the classpath

See https://github.com/quarkusio/quarkus/pull/24010 for more information

lilgreenbird: hi @gastaldi 

thank you for this the team will take a look.


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..f44d36c3 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -18,6 +18,8 @@ import java.io.UnsupportedEncodingException;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.math.RoundingMode;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.Socket;
@@ -35,8 +37,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 +45,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;
@@ -749,17 +747,21 @@ final class TDSChannel implements Serializable {
 
     /**
      * Opens the physical communications channel (TCP/IP socket and I/O streams) to the SQL Server.
+     * 
+     * @param iPAddressPreference
+     *        Preferred type of IP address to use first
      *
      * @return InetSocketAddress of the connection socket.
      */
     final InetSocketAddress open(String host, int port, int timeoutMillis, boolean useParallel, boolean useTnir,
-            boolean isTnirFirstAttempt, int timeoutMillisForFullTimeout) throws SQLServerException {
+            boolean isTnirFirstAttempt, int timeoutMillisForFullTimeout,
+            String iPAddressPreference) throws SQLServerException {
         if (logger.isLoggable(Level.FINER))
             logger.finer(this.toString() + ": Opening TCP socket...");
 
         SocketFinder socketFinder = new SocketFinder(traceID, con);
         channelSocket = tcpSocket = socketFinder.findSocket(host, port, timeoutMillis, useParallel, useTnir,
-                isTnirFirstAttempt, timeoutMillisForFullTimeout);
+                isTnirFirstAttempt, timeoutMillisForFullTimeout, iPAddressPreference);
         try {
             // Set socket options
             tcpSocket.setTcpNoDelay(true);
@@ -1538,274 +1540,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,
@@ -1848,8 +1582,12 @@ final class TDSChannel implements Serializable {
 
             String trustStoreFileName = con.activeConnectionProperties
                     .getProperty(SQLServerDriverStringProperty.TRUST_STORE.toString());
-            String trustStorePassword = con.activeConnectionProperties
-                    .getProperty(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
+
+            String trustStorePassword = null;
+            if (con.encryptedTrustStorePassword != null) {
+                trustStorePassword = SecureStringUtil.getInstance().getDecryptedString(con.encryptedTrustStorePassword);
+            }
+
             String hostNameInCertificate = con.activeConnectionProperties
                     .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
 
@@ -1860,15 +1598,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 +1636,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())
@@ -2579,6 +2333,9 @@ final class SocketFinder {
     // necessary for raising exceptions so that the connection pool can be notified
     private final SQLServerConnection conn;
 
+    // list of addresses for ip selection by type preference
+    private static ArrayList<InetAddress> addressList = new ArrayList<>();
+
     /**
      * Constructs a new SocketFinder object with appropriate traceId
      * 
@@ -2598,11 +2355,13 @@ final class SocketFinder {
      * @param hostName
      * @param portNumber
      * @param timeoutInMilliSeconds
+     * @param iPAddressPreference
      * @return connected socket
      * @throws IOException
      */
     Socket findSocket(String hostName, int portNumber, int timeoutInMilliSeconds, boolean useParallel, boolean useTnir,
-            boolean isTnirFirstAttempt, int timeoutInMilliSecondsForFullTimeout) throws SQLServerException {
+            boolean isTnirFirstAttempt, int timeoutInMilliSecondsForFullTimeout,
+            String iPAddressPreference) throws SQLServerException {
         assert timeoutInMilliSeconds != 0 : "The driver does not allow a time out of 0";
 
         try {
@@ -2612,9 +2371,10 @@ final class SocketFinder {
                 // MSF is false. TNIR could be true or false. DBMirroring could be true or false.
                 // For TNIR first attempt, we should do existing behavior including how host name is resolved.
                 if (useTnir && isTnirFirstAttempt) {
-                    return getDefaultSocket(hostName, portNumber, SQLServerConnection.TnirFirstAttemptTimeoutMs);
+                    return getSocketByIPPreference(hostName, portNumber, SQLServerConnection.TnirFirstAttemptTimeoutMs,
+                            iPAddressPreference);
                 } else if (!useTnir) {
-                    return getDefaultSocket(hostName, portNumber, timeoutInMilliSeconds);
+                    return getSocketByIPPreference(hostName, portNumber, timeoutInMilliSeconds, iPAddressPreference);
                 }
             }
 
@@ -2907,19 +2667,85 @@ final class SocketFinder {
         return socketFactory;
     }
 
-    // This method contains the old logic of connecting to
-    // a socket of one of the IPs corresponding to a given host name.
-    // In the old code below, the logic around 0 timeout has been removed as
-    // 0 timeout is not allowed. The code has been re-factored so that the logic
-    // is common for hostName or InetAddress.
-    private Socket getDefaultSocket(String hostName, int portNumber, int timeoutInMilliSeconds) throws IOException {
-        // Open the socket, with or without a timeout, throwing an UnknownHostException
-        // if there is a failure to resolve the host name to an InetSocketAddress.
-        //
+    /**
+     * Helper function which traverses through list of InetAddresses to find a resolved one
+     * @param hostName 
+     * 
+     * @param portNumber
+     *        Port Number
+     * @return First resolved address or unresolved address if none found
+     * @throws IOException
+     * @throws SQLServerException
+     */
+    private InetSocketAddress getInetAddressByIPPreference(String hostName, int portNumber) throws IOException, SQLServerException {
+        InetSocketAddress addr = InetSocketAddress.createUnresolved(hostName, portNumber);
+        for (int i = 0; i < addressList.size(); i++) {
+            addr = new InetSocketAddress(addressList.get(i), portNumber);
+            if (!addr.isUnresolved())
+                return addr;
+        }
+        return addr;
+    }
+
+    /**
+     * This method makes a connected socket given a hostname or IP address.
+     * 
+     * @param hostName
+     *        Hostname or IP Address
+     * @param portNumber
+     *        Port number
+     * @param timeoutInMilliSeconds
+     *        Timeout
+     * @param iPAddressPreference
+     *        Preferred IP address type
+     * @return Connected Socket
+     * @throws IOException
+     * @throws SQLServerException
+     */
+    private Socket getSocketByIPPreference(String hostName, int portNumber, int timeoutInMilliSeconds,
+            String iPAddressPreference) throws IOException, SQLServerException {
+        InetSocketAddress addr = null;
+        InetAddress addresses[] = InetAddress.getAllByName(hostName);
+        IPAddressPreference pref = IPAddressPreference.valueOfString(iPAddressPreference);
+        switch (pref) {
+            case IPv6First:
+                // Try to connect to first choice of IP address type
+                fillAddressList(addresses, true);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                // No unresolved addresses of preferred type, try the other
+                fillAddressList(addresses, false);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                break;
+            case IPv4First:
+                // Try to connect to first choice of IP address type
+                fillAddressList(addresses, false);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                // No unresolved addresses of preferred type, try the other
+                fillAddressList(addresses, true);
+                addr = getInetAddressByIPPreference(hostName, portNumber);
+                if (!addr.isUnresolved())
+                    return getConnectedSocket(addr, timeoutInMilliSeconds);
+                break;
+            case UsePlatformDefault:
+                for (InetAddress address : addresses) {
+                    addr = new InetSocketAddress(address, portNumber);
+                    if (!addr.isUnresolved())
+                        return getConnectedSocket(addr, timeoutInMilliSeconds);
+                }
+                break;
+            default:
+                break;
+        }
+
         // Note that Socket(host, port) throws an UnknownHostException if the host name
         // cannot be resolved, but that InetSocketAddress(host, port) does not - it sets
         // the returned InetSocketAddress as unresolved.
-        InetSocketAddress addr = new InetSocketAddress(hostName, portNumber);
         if (addr.isUnresolved()) {
             if (logger.isLoggable(Level.FINER)) {
                 logger.finer(this.toString() + "Failed to resolve host name: " + hostName
@@ -2928,9 +2754,32 @@ final class SocketFinder {
             InetSocketAddress cacheEntry = SQLServerConnection.getDNSEntry(hostName);
             addr = (null != cacheEntry) ? cacheEntry : addr;
         }
+
         return getConnectedSocket(addr, timeoutInMilliSeconds);
     }
 
+    /**
+     * Fills static array of IP Addresses with addresses of the preferred protocol version.
+     * @param addresses Array of all addresses
+     * @param ipv6first Boolean switch for IPv6 first
+     */
+    private void fillAddressList(InetAddress[] addresses, boolean ipv6first) {
+        addressList.clear();
+        if (ipv6first) {
+            for (InetAddress addr : addresses) {
+                if (addr instanceof Inet6Address) {
+                    addressList.add(addr);
+                }
+            }
+        } else {
+            for (InetAddress addr : addresses) {
+                if (addr instanceof Inet4Address) {
+                    addressList.add(addr);
+                }
+            }
+        }
+    }
+
     private Socket getConnectedSocket(InetAddress inetAddr, int portNumber,
             int timeoutInMilliSeconds) throws IOException {
         InetSocketAddress addr = new InetSocketAddress(inetAddr, portNumber);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
index a74a3bd1..a224eb7f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java
@@ -391,4 +391,20 @@ public interface ISQLServerConnection extends java.sql.Connection {
      *        boolean value for 'delayLoadingLobs'.
      */
     void setDelayLoadingLobs(boolean delayLoadingLobs);
+    
+    /**
+     * Sets the name of the preferred type of IP Address.
+     * 
+     * @param iPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    void setIPAddressPreference(String iPAddressPreference);
+    
+    /**
+     * Gets the name of the preferred type of IP Address.
+     * 
+     * @return IPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    String getIPAddressPreference();
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index 0102e6ea..238c4e51 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
@@ -372,6 +387,22 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
      * @return A String that contains the server name or null if no value is set.
      */
     String getServerName();
+    
+    /**
+     * Sets the name of the preferred type of IP Address.
+     * 
+     * @param iPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    void setIPAddressPreference(String iPAddressPreference);
+    
+    /**
+     * Gets the name of the preferred type of IP Address.
+     * 
+     * @return IPAddressPreference
+     *        A String that contains the preferred type of IP Address.
+     */
+    String getIPAddressPreference();
 
     /**
      * Sets the name of the failover server that is used in a database mirroring configuration.
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..802e9c12 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -217,6 +217,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     /** flag indicating whether prelogin TLS handshake is required */
     private boolean isTDSS = false;
 
+    String encryptedTrustStorePassword = null;
+
     /**
      * Return an existing cached SharedTimer associated with this Connection or create a new one.
      *
@@ -391,7 +393,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         private Instant lastNetworkActivity = Instant.now();
 
         /**
-         * An “idle” connection will only ever get its socket disconnected by a keepalive packet after a connection has
+         * An "idle" connection will only ever get its socket disconnected by a keepalive packet after a connection has
          * been severed. KeepAlive packets are only sent on idle sockets. Default setting by the driver (on platforms
          * that have Java support for setting it) and the recommended setting is 30s (and OS default for those that
          * don't set it is 2 hrs).
@@ -754,6 +756,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;
 
@@ -1740,34 +1749,13 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
             KeyStoreAuthentication keyStoreAuthentication = KeyStoreAuthentication.valueOfString(keyStoreAuth);
             switch (keyStoreAuthentication) {
                 case JavaKeyStorePassword:
-                    // both secret and location must be set for JKS.
-                    if ((null == keyStoreSecret) || (null == keyStoreLocation)) {
-                        throw new SQLServerException(
-                                SQLServerException.getErrString("R_keyStoreSecretOrLocationNotSet"), null);
-                    } else {
-                        SQLServerColumnEncryptionJavaKeyStoreProvider provider = new SQLServerColumnEncryptionJavaKeyStoreProvider(
-                                keyStoreLocation, keyStoreSecret.toCharArray());
-                        systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
-                    }
+                    setKeyStoreSecretAndLocation(keyStoreSecret, keyStoreLocation);
                     break;
                 case KeyVaultClientSecret:
-                    // need a secret to use the secret method
-                    if (null == keyStoreSecret) {
-                        throw new SQLServerException(SQLServerException.getErrString("R_keyStoreSecretNotSet"), null);
-                    } else {
-                        SQLServerColumnEncryptionAzureKeyVaultProvider provider = new SQLServerColumnEncryptionAzureKeyVaultProvider(
-                                keyStorePrincipalId, keyStoreSecret);
-                        systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
-                    }
+                    this.setKeyVaultProvider(keyStorePrincipalId, keyStoreSecret);
                     break;
                 case KeyVaultManagedIdentity:
-                    SQLServerColumnEncryptionAzureKeyVaultProvider provider;
-                    if (null != keyStorePrincipalId) {
-                        provider = new SQLServerColumnEncryptionAzureKeyVaultProvider(keyStorePrincipalId);
-                    } else {
-                        provider = new SQLServerColumnEncryptionAzureKeyVaultProvider();
-                    }
-                    systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
+                    setKeyVaultProvider(keyStorePrincipalId);
                     break;
                 default:
                     // valueOfString would throw an exception if the keyStoreAuthentication is not valid.
@@ -1776,6 +1764,39 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         }
     }
 
+    private void setKeyStoreSecretAndLocation(String keyStoreSecret, String keyStoreLocation) throws SQLServerException {
+        // both secret and location must be set for JKS.
+        if ((null == keyStoreSecret) || (null == keyStoreLocation)) {
+            throw new SQLServerException(
+                    SQLServerException.getErrString("R_keyStoreSecretOrLocationNotSet"), null);
+        } else {
+            SQLServerColumnEncryptionJavaKeyStoreProvider provider = new SQLServerColumnEncryptionJavaKeyStoreProvider(
+                     keyStoreLocation, keyStoreSecret.toCharArray());
+            systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
+        }
+    }
+
+    private void setKeyVaultProvider(String keyStorePrincipalId) throws SQLServerException {
+        SQLServerColumnEncryptionAzureKeyVaultProvider provider;
+        if (null != keyStorePrincipalId) {
+            provider = new SQLServerColumnEncryptionAzureKeyVaultProvider(keyStorePrincipalId);
+        } else {
+            provider = new SQLServerColumnEncryptionAzureKeyVaultProvider();
+        }
+        systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
+    }
+
+    private void setKeyVaultProvider(String keyStorePrincipalId, String keyStoreSecret) throws SQLServerException {
+        // need a secret to use the secret method
+        if (null == keyStoreSecret) {
+            throw new SQLServerException(SQLServerException.getErrString("R_keyStoreSecretNotSet"), null);
+        } else {
+            SQLServerColumnEncryptionAzureKeyVaultProvider provider = new SQLServerColumnEncryptionAzureKeyVaultProvider(
+                    keyStorePrincipalId, keyStoreSecret);
+            systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
+        }
+    }
+
     // Helper to check if timeout value is valid
     int validateTimeout(SQLServerDriverIntProperty property) throws SQLServerException {
         int timeout = property.getDefaultValue();
@@ -1819,6 +1840,13 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
                 pooledConnectionParent = pooledConnection;
 
+                String trustStorePassword = activeConnectionProperties
+                        .getProperty(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
+                if (trustStorePassword != null) {
+                    encryptedTrustStorePassword = SecureStringUtil.getInstance().getEncryptedString(trustStorePassword);
+                    activeConnectionProperties.remove(SQLServerDriverStringProperty.TRUST_STORE_PASSWORD.toString());
+                }
+
                 String hostNameInCertificate = activeConnectionProperties
                         .getProperty(SQLServerDriverStringProperty.HOSTNAME_IN_CERTIFICATE.toString());
 
@@ -1930,6 +1958,15 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 if (null != sPropValuePort) {
                     trustedServerNameAE += ":" + sPropValuePort;
                 }
+                
+                sPropKey = SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString();
+                sPropValue = activeConnectionProperties.getProperty(sPropKey);
+                if (null == sPropValue) {
+                    sPropValue = SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue();
+                    activeConnectionProperties.setProperty(sPropKey, sPropValue);
+                } else {
+                    activeConnectionProperties.setProperty(sPropKey, IPAddressPreference.valueOfString(sPropValue).toString());
+                }
 
                 sPropKey = SQLServerDriverStringProperty.APPLICATION_NAME.toString();
                 sPropValue = activeConnectionProperties.getProperty(sPropKey);
@@ -2028,9 +2065,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                                 SQLServerException.getErrString("R_keyVaultProviderClientKeyNotSet"), null);
                     }
                     String keyVaultColumnEncryptionProviderClientKey = sPropValue;
-                    SQLServerColumnEncryptionAzureKeyVaultProvider provider = new SQLServerColumnEncryptionAzureKeyVaultProvider(
-                            keyVaultColumnEncryptionProviderClientId, keyVaultColumnEncryptionProviderClientKey);
-                    systemColumnEncryptionKeyStoreProvider.put(provider.getName(), provider);
+                    setKeyVaultProvider(keyVaultColumnEncryptionProviderClientId, keyVaultColumnEncryptionProviderClientKey);
                 }
 
                 sPropKey = SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString();
@@ -2094,6 +2129,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 {
@@ -3214,10 +3257,11 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
         // if the timeout is infinite slices are infinite too.
         tdsChannel = new TDSChannel(this);
+        String iPAddressPreference = activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
 
         InetSocketAddress inetSocketAddress = tdsChannel.open(serverInfo.getParsedServerName(),
                 serverInfo.getPortNumber(), (0 == timeOutFullInSeconds) ? 0 : timeOutSliceInMillis, useParallel,
-                useTnir, isTnirFirstAttempt, timeOutsliceInMillisForFullTimeout);
+                useTnir, isTnirFirstAttempt, timeOutsliceInMillisForFullTimeout, iPAddressPreference);
 
         setState(State.Connected);
 
@@ -7609,6 +7653,17 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     String getServerName() {
         return this.trustedServerNameAE;
     }
+
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        activeConnectionProperties.setProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), iPAddressPreference);
+        
+    }
+
+    @Override
+    public String getIPAddressPreference() {
+        return activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString());
+    }
 }
 
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
index 91678f39..364f549d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java
@@ -619,4 +619,15 @@ class SQLServerConnectionPoolProxy implements ISQLServerConnection, java.io.Seri
     public void setDelayLoadingLobs(boolean delayLoadingLobs) {
         wrappedConnection.setDelayLoadingLobs(delayLoadingLobs);
     }
+
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        wrappedConnection.setIPAddressPreference(iPAddressPreference);
+        
+    }
+
+    @Override
+    public String getIPAddressPreference() {
+        return wrappedConnection.getIPAddressPreference();
+    }
 }
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index 669a190e..c30d6b15 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(),
@@ -584,6 +594,24 @@ public class SQLServerDataSource
     public String getServerName() {
         return getStringProperty(connectionProps, SQLServerDriverStringProperty.SERVER_NAME.toString(), null);
     }
+    
+    /**
+     * Set the preferred type of IP Address
+     * 
+     * @param iPAddressPreference Preferred IP Address type
+     */
+    @Override
+    public void setIPAddressPreference(String iPAddressPreference) {
+        setStringProperty(connectionProps, SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), iPAddressPreference);
+    }
+    
+    /**
+     * Gets the preferred type of IP Address
+     */
+    @Override
+    public String getIPAddressPreference() {
+        return getStringProperty(connectionProps, SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(), SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue());
+    }
 
     /**
      * Sets the realm for Kerberos authentication.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index ff123be9..43209c8f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -257,6 +257,43 @@ enum SSLProtocol {
 }
 
 
+enum IPAddressPreference {
+    IPv4First("IPv4First"),
+    IPv6First("IPv6First"),
+    UsePlatformDefault("UsePlatformDefault");
+
+    private final String name;
+
+    IPAddressPreference(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return name;
+    }
+
+    static IPAddressPreference valueOfString(String value) throws SQLServerException {
+        IPAddressPreference iptype = null;
+
+        if (value.toLowerCase(Locale.US).equalsIgnoreCase(IPAddressPreference.IPv4First.toString())) {
+            iptype = IPAddressPreference.IPv4First;
+        } else if (value.toLowerCase(Locale.US).equalsIgnoreCase(IPAddressPreference.IPv6First.toString())) {
+            iptype = IPAddressPreference.IPv6First;
+        } else if (value.toLowerCase(Locale.US)
+                .equalsIgnoreCase(IPAddressPreference.UsePlatformDefault.toString())) {
+            iptype = IPAddressPreference.UsePlatformDefault;
+
+        } else {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidIPAddressPreference"));
+            Object[] msgArgs = {value};
+            throw new SQLServerException(form.format(msgArgs), null);
+        }
+        return iptype;
+    }
+}
+
+
 enum KeyStoreAuthentication {
     JavaKeyStorePassword,
     KeyVaultClientSecret,
@@ -422,6 +459,7 @@ enum SQLServerDriverStringProperty {
     SELECT_METHOD("selectMethod", "direct"),
     DOMAIN("domain", ""),
     SERVER_NAME("serverName", ""),
+    IPADDRESS_PREFERENCE("iPAddressPreference", IPAddressPreference.IPv4First.toString()),
     SERVER_SPN("serverSpn", ""),
     REALM("realm", ""),
     SOCKET_FACTORY_CLASS("socketFactoryClass", ""),
@@ -453,7 +491,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 +634,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()}),
@@ -649,6 +690,11 @@ public final class SQLServerDriver implements java.sql.Driver {
                     SQLServerDriverStringProperty.DOMAIN.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_NAME.toString(),
                     SQLServerDriverStringProperty.SERVER_NAME.getDefaultValue(), false, null),
+            new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString(),
+                    SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.getDefaultValue(), false,
+                    new String[] {IPAddressPreference.IPv4First.toString(),
+                            IPAddressPreference.IPv6First.toString(),
+                            IPAddressPreference.UsePlatformDefault.toString()}),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.SERVER_SPN.toString(),
                     SQLServerDriverStringProperty.SERVER_SPN.getDefaultValue(), false, null),
             new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.REALM.toString(),
@@ -698,7 +744,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..bb98b34d 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -165,6 +165,7 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_databaseNamePropertyDescription", "The name of the database to connect to."},
         {"R_domainPropertyDescription", "The Windows domain to authenticate in using NTLM."},
         {"R_serverNamePropertyDescription", "The computer running SQL Server."},
+        {"R_iPAddressPreferencePropertyDescription", "The preferred type of IP address."},
         {"R_portNumberPropertyDescription", "The TCP port where an instance of SQL Server is listening."},
         {"R_realmPropertyDescription", "The realm for Kerberos authentication."},
         {"R_serverSpnPropertyDescription", "SQL Server SPN."},
@@ -192,6 +193,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"},
@@ -266,8 +268,8 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_InvalidCipherTextSize", "Specified ciphertext has an invalid size of {0} bytes, which is below the minimum {1} bytes required for decryption."},
         {"R_InvalidAlgorithmVersion", "The specified ciphertext''s encryption algorithm version {0} does not match the expected encryption algorithm version {1} ."},
         {"R_InvalidAuthenticationTag", "Specified ciphertext has an invalid authentication tag. "},
-        {"R_EncryptionFailed", "Internal error while encryption:  {0} "},
-        {"R_DecryptionFailed", "Internal error while decryption:  {0} "},
+        {"R_EncryptionFailed", "Internal error during encryption:  {0} "},
+        {"R_DecryptionFailed", "Internal error during decryption:  {0} "},
         {"R_InvalidKeySize", "The column encryption key has been successfully decrypted but it''s length: {0} does not match the length: {1} for algorithm \"{2}\". Verify the encrypted value of the column encryption key in the database."},
         {"R_InvalidEncryptionType", "Encryption type {0} specified for the column in the database is either invalid or corrupted. Valid encryption types for algorithm {1} are: {2}."},
         {"R_UnknownColumnEncryptionAlgorithm", "The Algorithm {0} does not exist. Algorithms registered in the factory are {1}."},
@@ -285,8 +287,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."},
@@ -502,7 +503,11 @@ public final class SQLServerResource extends ListResourceBundle {
         {"R_crClientUnrecoverable","The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection."},
         {"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_InvalidIPAddressPreference", "IP address preference {0} is not valid."},
         {"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}."},
+        {"R_SecureStringInitFailed", "Failed to initialize SecureStringUtil to store secure strings"},
     };
 }
 // @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];
+    }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
new file mode 100644
index 00000000..c34979d1
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SecureStringUtil.java
@@ -0,0 +1,139 @@
+package com.microsoft.sqlserver.jdbc;
+
+import java.security.SecureRandom;
+import java.text.MessageFormat;
+import java.util.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+
+/**
+ * 
+ * This is an utility class to encrypt/decrypt strings. This is used to obfuscate passwords so they won't be visible as
+ * plaintext.
+ */
+final class SecureStringUtil {
+    /* cipher transformation in the form of algorithm/mode/padding */
+    static final String CIPHER_TRANSFORMATION = "AES/GCM/NoPadding";
+
+    /* key generator algorithm */
+    static final String KEYGEN_ALGORITHEM = "AES";
+
+    /* length of initialization vector buffer */
+    static final int IV_LENGTH = 12;
+
+    /* key size */
+    static final int KEY_SIZE = 256;
+
+    /* authentication tag length in bits */
+    static final int TAG_LENGTH = 16;
+
+    /* initialization vector */
+    byte[] iv;
+
+    /** secret key for encryption/decryption */
+    SecretKeySpec secretKey;
+
+    /* cryptographic cipher for encryption */
+    private Cipher encryptCipher;
+
+    /* cryptographic cipher for decryption */
+    private Cipher decryptCipher;
+
+    /* singleton instance */
+    private static SecureStringUtil instance;
+
+    /**
+     * Get reference to SecureStringUtil instance
+     * 
+     * @return the SecureStringUtil instance
+     * 
+     * @throws SQLServerException
+     *         if error
+     */
+    static SecureStringUtil getInstance() throws SQLServerException {
+        if (instance == null) {
+            instance = new SecureStringUtil();
+        }
+        return instance;
+    }
+
+    /**
+     * Creates an instance of the SecureStringUtil object and initialize values to encrypt/decrypt strings
+     * 
+     * @throws SQLServerException
+     *         if error
+     */
+    private SecureStringUtil() throws SQLServerException {
+        iv = new byte[IV_LENGTH];
+        try {
+            // generate key */
+            KeyGenerator keygen = KeyGenerator.getInstance(KEYGEN_ALGORITHEM);
+            keygen.init(KEY_SIZE);
+            secretKey = new SecretKeySpec(keygen.generateKey().getEncoded(), "AES");
+
+            // get ciphers for encryption/decryption
+            encryptCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+            decryptCipher = Cipher.getInstance(CIPHER_TRANSFORMATION);
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_SecureStringInitFailed"));
+            Object[] msgArgs = {e.getMessage()};
+            throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
+        }
+    }
+
+    /**
+     * Get encrypted value of given string
+     * 
+     * @param str
+     *        string to encrypt
+     * 
+     * @return encrypted string
+     * 
+     * @throws SQLServerException
+     *         if error
+     */
+    String getEncryptedString(String str) throws SQLServerException {
+        SecureRandom random = new SecureRandom();
+        random.nextBytes(iv);
+        GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
+
+        try {
+            encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParamSpec);
+
+            byte[] cipherText = encryptCipher.doFinal(str.getBytes());
+            return Base64.getEncoder().encodeToString(cipherText);
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_EncryptionFailed"));
+            Object[] msgArgs = {e.getMessage()};
+            throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
+        }
+    }
+
+    /**
+     * Get decrypted value of an encrypted string
+     * 
+     * @param str
+     * 
+     * @return decrypted string
+     * 
+     * @throws SQLServerException
+     */
+    String getDecryptedString(String str) throws SQLServerException {
+        GCMParameterSpec ivParamSpec = new GCMParameterSpec(TAG_LENGTH * 8, iv);
+
+        try {
+            decryptCipher.init(Cipher.DECRYPT_MODE, secretKey, ivParamSpec);
+
+            byte[] plainText = decryptCipher.doFinal(Base64.getDecoder().decode(str));
+            return new String(plainText);
+        } catch (Exception e) {
+            MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_DecryptionFailed"));
+            Object[] msgArgs = {e.getMessage()};
+            throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
+        }
+    }
+}


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..f4139075 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java
@@ -82,6 +82,9 @@ public class SQLServerConnectionTest extends AbstractTest {
 
         ds.setPortNumber(intPropValue);
         assertEquals(intPropValue, ds.getPortNumber(), TestResource.getResource("R_valuesAreDifferent"));
+        
+        ds.setIPAddressPreference(stringPropValue);
+        assertEquals(stringPropValue, ds.getIPAddressPreference(), TestResource.getResource("R_valuesAreDifferent"));
 
         ds.setURL(stringPropValue);
         assertEquals(stringPropValue, ds.getURL(), TestResource.getResource("R_valuesAreDifferent"));
@@ -166,6 +169,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"));
 
@@ -655,7 +661,7 @@ public class SQLServerConnectionTest extends AbstractTest {
                 assertTrue(timeDiff <= milsecs, form.format(msgArgs));
             }
         } catch (Exception e) {
-            assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")));
+            assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage());
             timerEnd = System.currentTimeMillis();
         }
     }
@@ -906,4 +912,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();
     }
 
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
index 40110c6a..0f28b61f 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/ConnectionTest.java
@@ -12,10 +12,14 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.platform.runner.JUnitPlatform;
 import org.junit.runner.RunWith;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
 
 import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
+import com.microsoft.sqlserver.jdbc.TestResource;
 import com.microsoft.sqlserver.testframework.AbstractTest;
 
+
 /*
  * This test is for testing various connection options
  */
@@ -36,10 +40,33 @@ public class ConnectionTest extends AbstractTest {
         ds.setKeyStoreSecret("placeholder");
 
         // Multiple, successive connections should not fail
-        try (Connection con = ds.getConnection()) {
-        }
+        try (Connection con = ds.getConnection()) {}
+
+        try (Connection con = ds.getConnection()) {}
+    }
+
+    @Test
+    public void testConnectWithIPAddressPreference() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setIPAddressPreference("IPv4First");
+        try (Connection con = ds.getConnection()) {}
+        ds.setIPAddressPreference("IPv6First");
+        try (Connection con = ds.getConnection()) {}
+        ds.setIPAddressPreference("UsePlatformDefault");
+        try (Connection con = ds.getConnection()) {}
+    }
 
+    @Test
+    public void testInvalidConnectWithIPAddressPreference() throws SQLException {
+        SQLServerDataSource ds = new SQLServerDataSource();
+        ds.setURL(connectionString);
+        ds.setIPAddressPreference("Bogus");
         try (Connection con = ds.getConnection()) {
+            fail(TestResource.getResource("R_expectedFailPassed"));
+        } catch (Exception e) {
+            assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidIPAddressPreference")));
         }
     }
+
 }
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
index ee768ccf..5ab5428c 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java
@@ -497,6 +497,8 @@ public class RequestBoundaryMethodsTest extends AbstractTest {
         verifiedMethodNames.add("registerColumnEncryptionKeyStoreProvidersOnConnection");
         verifiedMethodNames.add("getPrepareMethod");
         verifiedMethodNames.add("setPrepareMethod");
+        verifiedMethodNames.add("getIPAddressPreference");
+        verifiedMethodNames.add("setIPAddressPreference");
 
         return verifiedMethodNames;
     }