Repository: microsoft/mssql-jdbc
Pull request #1783 (2022-03-31 22:20:48)

Changelog for 11.1.0


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c828549d..d51a4591 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,27 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 
+## [11.1.0] Preview Release
+### Added
+- Added new connection string property prepareMethod to toggle use of sp_prepare [1719](https://github.com/microsoft/mssql-jdbc/pull/1719)
+- Added Azure Active Directory tests for Azure Data Explorer using user/password/applicationName [1755](https://github.com/microsoft/mssql-jdbc/pull/1755)
+- Added check for MSAL library when attempting ActiveDirectoryServicePrincipal authentication [1759](https://github.com/microsoft/mssql-jdbc/pull/1759)
+- Added new encrypt options for TDSS support [1757](https://github.com/microsoft/mssql-jdbc/pull/1757)
+- Added Configurable IPv6 Support [1766](https://github.com/microsoft/mssql-jdbc/pull/1766)
+- Added serverCertificate connection property for encrypt=strict [1773](https://github.com/microsoft/mssql-jdbc/pull/1773)
+- Added encrypt utility to obfuscate password strings [1780](https://github.com/microsoft/mssql-jdbc/pull/1780)
+- Added option for NONE attestation protocol [1779](https://github.com/microsoft/mssql-jdbc/pull/1779)
+
+### Changed
+- Simplified traceID creation in DataSource and PooledConnection [1747](https://github.com/microsoft/mssql-jdbc/pull/1747)
+- Refactored SQLServerColumnEncryptionAzureKeyVaultProvider usage in SQLServerConnection [1774](https://github.com/microsoft/mssql-jdbc/pull/1774)
+### Fixed issues
+- Removed extra call to executeCommand() within connectionCommand() [1754](https://github.com/microsoft/mssql-jdbc/pull/1754)
+- Fixed warnings for Implicit narrowing conversion in compound assignment [1758](https://github.com/microsoft/mssql-jdbc/pull/1758)
+- Added check for MSAL library when attempting ActiveDirectoryServicePrincipal authentication [1759](https://github.com/microsoft/mssql-jdbc/pull/1759)
+- Fixed Managed Identity retry interval to exponential backoff properly [1770](https://github.com/microsoft/mssql-jdbc/pull/1770)
+
+
 ## [10.2.0] Stable Release
 ### Added
 - Support for datetimeoffset with sql_variant [1673](https://github.com/microsoft/mssql-jdbc/pull/1673)
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index 802e9c12..cefc9005 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -123,6 +123,7 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
     /** Current limit for this particular connection. */
     private Boolean enablePrepareOnFirstPreparedStatementCall = null;
 
+    /** Used for toggling use of sp_prepare */
     private String prepareMethod = null;
 
     /** Handle the actual queue of discarded prepared statements. */
@@ -2000,24 +2001,31 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
                 sPropValue = activeConnectionProperties.getProperty(sPropKey);
                 if (null != sPropValue) {
                     enclaveAttestationProtocol = sPropValue;
-                    if (!AttestationProtocol.isValidAttestationProtocol(enclaveAttestationProtocol)) {
-                        throw new SQLServerException(
-                                SQLServerException.getErrString("R_enclaveInvalidAttestationProtocol"), null);
-                    }
 
                     if (enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.HGS.toString())) {
                         this.enclaveProvider = new SQLServerVSMEnclaveProvider();
-                    } else {
-                        // If it's a valid Provider & not HGS, then it has to be AAS
+                    } else if (enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.NONE.toString())) {
+                        this.enclaveProvider = new SQLServerNoneEnclaveProvider();
+                    } else if (enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.AAS.toString())) {
                         this.enclaveProvider = new SQLServerAASEnclaveProvider();
+                    } else {
+                        throw new SQLServerException(
+                                SQLServerException.getErrString("R_enclaveInvalidAttestationProtocol"), null);
                     }
                 }
 
                 // enclave requires columnEncryption=enabled, enclaveAttestationUrl and enclaveAttestationProtocol
-                if ((null != enclaveAttestationUrl && !enclaveAttestationUrl.isEmpty()
+                if (
+                        // An attestation URL requires a protocol
+                        (null != enclaveAttestationUrl && !enclaveAttestationUrl.isEmpty()
                         && (null == enclaveAttestationProtocol || enclaveAttestationProtocol.isEmpty()))
+
+                        // An attestation protocol that is not NONE requires a URL
                         || (null != enclaveAttestationProtocol && !enclaveAttestationProtocol.isEmpty()
+                                && !enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.NONE.toString())
                                 && (null == enclaveAttestationUrl || enclaveAttestationUrl.isEmpty()))
+
+                        // An attestation protocol also requires column encryption
                         || (null != enclaveAttestationUrl && !enclaveAttestationUrl.isEmpty()
                                 && (null != enclaveAttestationProtocol || !enclaveAttestationProtocol.isEmpty())
                                 && (null == columnEncryptionSetting || !isColumnEncryptionSettingEnabled()))) {
@@ -4550,7 +4558,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
         if (write) {
             tdsWriter.writeByte(TDS.TDS_FEATURE_EXT_AE); // FEATUREEXT_TC
             tdsWriter.writeInt(1); // length of version
-            if (null == enclaveAttestationUrl || enclaveAttestationUrl.isEmpty()) {
+            if (null == enclaveAttestationUrl || enclaveAttestationUrl.isEmpty() || (enclaveAttestationProtocol != null 
+               && !enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.NONE.toString()))) {
                 tdsWriter.writeByte(TDS.COLUMNENCRYPTION_VERSION1);
             } else {
                 tdsWriter.writeByte(TDS.COLUMNENCRYPTION_VERSION2);
@@ -5669,7 +5678,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
 
                 serverColumnEncryptionVersion = ColumnEncryptionVersion.AE_V1;
 
-                if (null != enclaveAttestationUrl) {
+                if (null != enclaveAttestationUrl || (enclaveAttestationProtocol != null 
+                && enclaveAttestationProtocol.equalsIgnoreCase(AttestationProtocol.NONE.toString()))) {
                     if (aeVersion < TDS.COLUMNENCRYPTION_VERSION2) {
                         throw new SQLServerException(SQLServerException.getErrString("R_enclaveNotSupported"), null);
                     } else {
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index 43209c8f..3e1bfca8 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -162,7 +162,8 @@ enum EncryptOption {
 
 enum AttestationProtocol {
     HGS("HGS"),
-    AAS("AAS");
+    AAS("AAS"),
+    NONE("NONE");
 
     private final String protocol;
 
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNoneEnclaveProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNoneEnclaveProvider.java
new file mode 100644
index 00000000..30b76b90
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerNoneEnclaveProvider.java
@@ -0,0 +1,246 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+
+
+/**
+ * 
+ * Provides the implementation of the NONE Enclave Provider. This enclave provider does not use attestation.
+ *
+ */
+public class SQLServerNoneEnclaveProvider implements ISQLServerEnclaveProvider {
+
+    private static final EnclaveSessionCache enclaveCache = new EnclaveSessionCache();
+
+    private NoneAttestationParameters noneParams = null;
+    private NoneAttestationResponse noneResponse = null;
+    private String attestationUrl = null;
+    private EnclaveSession enclaveSession = null;
+    
+    @Override
+    public void getAttestationParameters(String url) throws SQLServerException {
+        if (null == noneParams) {
+            attestationUrl = url;
+            try {
+                noneParams = new NoneAttestationParameters(attestationUrl);
+            } catch (IOException e) {
+                SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
+            }
+        }
+    }
+    
+    @Override
+    public ArrayList<byte[]> createEnclaveSession(SQLServerConnection connection, SQLServerStatement statement,
+            String userSql, String preparedTypeDefinitions, Parameter[] params,
+            ArrayList<String> parameterNames) throws SQLServerException {
+
+        /*
+         * for None attestation: enclave does not send public key, and sends an empty attestation info. The only
+         * non-trivial content it sends is the session setup info (DH pubkey of enclave).
+         */
+
+        // Check if the session exists in our cache
+        StringBuilder keyLookup = new StringBuilder(connection.getServerName()).append(connection.getCatalog())
+                .append(attestationUrl);
+        EnclaveCacheEntry entry = enclaveCache.getSession(keyLookup.toString());
+        if (null != entry) {
+            this.enclaveSession = entry.getEnclaveSession();
+            this.noneParams = (NoneAttestationParameters) entry.getBaseAttestationRequest();
+        }
+        ArrayList<byte[]> b = describeParameterEncryption(connection, statement, userSql, preparedTypeDefinitions,
+                params, parameterNames);
+        if (connection.enclaveEstablished()) {
+            return b;
+        } else if (null != noneResponse && !connection.enclaveEstablished()) {
+            try {
+                enclaveSession = new EnclaveSession(noneResponse.getSessionID(),
+                        noneParams.createSessionSecret(noneResponse.getDHpublicKey()));
+                enclaveCache.addEntry(connection.getServerName(), connection.getCatalog(),
+                        connection.enclaveAttestationUrl, noneParams, enclaveSession);
+            } catch (GeneralSecurityException e) {
+                SQLServerException.makeFromDriverError(connection, this, e.getLocalizedMessage(), "0", false);
+            }
+        }
+        return b;
+    }
+
+    @Override
+    public void invalidateEnclaveSession() {
+        if (null != enclaveSession) {
+            enclaveCache.removeEntry(enclaveSession);
+        }
+        enclaveSession = null;
+        noneParams = null;
+        attestationUrl = null;
+    }
+
+    @Override
+    public EnclaveSession getEnclaveSession() {
+        return enclaveSession;
+    }
+
+    private void validateAttestationResponse() throws SQLServerException {
+        if (null != noneResponse) {
+            try {
+                noneResponse.validateDHPublicKey();
+            } catch (GeneralSecurityException e) {
+                SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
+            }
+        }
+    }
+    
+    private ArrayList<byte[]> describeParameterEncryption(SQLServerConnection connection, SQLServerStatement statement,
+            String userSql, String preparedTypeDefinitions, Parameter[] params,
+            ArrayList<String> parameterNames) throws SQLServerException {
+        ArrayList<byte[]> enclaveRequestedCEKs = new ArrayList<>();
+        try (PreparedStatement stmt = connection.prepareStatement(connection.enclaveEstablished() ? SDPE1 : SDPE2)) {
+            try (ResultSet rs = connection.enclaveEstablished() ? executeSDPEv1(stmt, userSql,
+                    preparedTypeDefinitions) : executeSDPEv2(stmt, userSql, preparedTypeDefinitions, noneParams)) {
+                if (null == rs) {
+                    // No results. Meaning no parameter.
+                    // Should never happen.
+                    return enclaveRequestedCEKs;
+                }
+                processSDPEv1(userSql, preparedTypeDefinitions, params, parameterNames, connection, statement, stmt, rs,
+                        enclaveRequestedCEKs);
+                // Process the third result set.
+                if (connection.isAEv2() && stmt.getMoreResults()) {
+                    try (ResultSet hgsRs = stmt.getResultSet()) {
+                        if (hgsRs.next()) {
+                            noneResponse = new NoneAttestationResponse(hgsRs.getBytes(1));
+                            // This validates and establishes the enclave session if valid
+                            validateAttestationResponse();
+                        } else {
+                            SQLServerException.makeFromDriverError(null, this,
+                                    SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"), "0", false);
+                        }
+                    }
+                }
+            }
+        } catch (SQLException | IOException e) {
+            if (e instanceof SQLServerException) {
+                throw (SQLServerException) e;
+            } else {
+                throw new SQLServerException(SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"), null,
+                        0, e);
+            }
+        }
+        return enclaveRequestedCEKs;
+    }
+}
+
+
+/**
+ * 
+ * Represents the serialization of the request the client sends to the 
+ * SQL Server while setting up a session.
+ *
+ */
+class NoneAttestationParameters extends BaseAttestationRequest {
+
+    // Type 2 is NONE, sent as Little Endian 0x20000000
+    private static final byte[] ENCLAVE_TYPE = new byte[] {0x2, 0x0, 0x0, 0x0};
+    // Nonce length is always 256
+    private static final byte[] NONCE_LENGTH = new byte[] {0x0, 0x1, 0x0, 0x0};
+    private final byte[] nonce = new byte[256];
+
+    NoneAttestationParameters(String attestationUrl) throws SQLServerException, IOException {
+        byte[] attestationUrlBytes = (attestationUrl + '\0').getBytes(UTF_16LE);
+
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(attestationUrlBytes.length).array());
+        os.write(attestationUrlBytes);
+        os.write(NONCE_LENGTH);
+        new SecureRandom().nextBytes(nonce);
+        os.write(nonce);
+        enclaveChallenge = os.toByteArray();
+
+        initBcryptECDH();
+    }
+
+    @Override
+    byte[] getBytes() throws IOException {
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(ENCLAVE_TYPE);
+        os.write(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(enclaveChallenge.length).array());
+        os.write(enclaveChallenge);
+        os.write(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ENCLAVE_LENGTH).array());
+        os.write(ECDH_MAGIC);
+        os.write(x);
+        os.write(y);
+        return os.toByteArray();
+    }
+
+    byte[] getNonce() {
+        return nonce;
+    }
+}
+
+
+/**
+ * 
+ * Represents the deserialization of the byte payload the client receives from the
+ * SQL Server while setting up a session.
+ *
+ */
+class NoneAttestationResponse extends BaseAttestationResponse {
+    
+    NoneAttestationResponse(byte[] b) throws SQLServerException {
+        /*-
+         * Protocol format:
+         * 1. Total Size of the attestation blob as UINT
+         * 2. Size of Enclave RSA public key as UINT
+         * 3. Size of Attestation token as UINT
+         * 4. Enclave Type as UINT
+         * 5. Enclave RSA public key (raw key, of length #2)
+         * 6. Attestation token (of length #3)
+         * 7. Size of Session ID was UINT
+         * 8. Session id value
+         * 9. Size of enclave ECDH public key
+         * 10. Enclave ECDH public key (of length #9)
+        */
+        ByteBuffer response = ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN);
+        this.totalSize = response.getInt();
+        this.identitySize = response.getInt();
+        this.attestationTokenSize = response.getInt();
+        this.enclaveType = response.getInt(); // 1 for VBS, 2 for SGX
+
+        enclavePK = new byte[identitySize];
+        byte[] attestationToken = new byte[attestationTokenSize];
+
+        response.get(enclavePK, 0, identitySize);
+        response.get(attestationToken, 0, attestationTokenSize);
+
+        this.sessionInfoSize = response.getInt();
+        response.get(sessionID, 0, 8);
+        this.DHPKsize = response.getInt();
+        this.DHPKSsize = response.getInt();
+
+        DHpublicKey = new byte[DHPKsize];
+        publicKeySig = new byte[DHPKSsize];
+
+        response.get(DHpublicKey, 0, DHPKsize);
+        response.get(publicKeySig, 0, DHPKSsize);
+
+        if (0 != response.remaining()) {
+            SQLServerException.makeFromDriverError(null, this,
+                    SQLServerResource.getResource("R_EnclaveResponseLengthError"), "0", false);
+        }
+    }
+}


Test patch:
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
index c9ee28dc..68eceb53 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
@@ -208,6 +208,7 @@ public class EnclavePackageTest extends AbstractTest {
         connectionStringEnclave = TestUtils.addOrOverrideProperty(connectionStringEnclave, "enclaveAttestationUrl",
                 (null != url) ? url : "http://blah");
 
+        // NONE protocol does not need a URL and will not work properly with tests (false negative)
         connectionStringEnclave = TestUtils.addOrOverrideProperty(connectionStringEnclave, "enclaveAttestationProtocol",
                 (null != url) ? protocol : "HGS");
     }
@@ -482,7 +483,10 @@ public class EnclavePackageTest extends AbstractTest {
                 "SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';")) {
             while (rs.next()) {
                 String enclaveType = rs.getString(2);
-                if (String.valueOf(AttestationProtocol.HGS).equals(protocol)) {
+
+                // HGS/NONE use only VBS, AAS can use either VBS or SGX
+                if (String.valueOf(AttestationProtocol.HGS).equals(protocol)
+                        || String.valueOf(AttestationProtocol.NONE).equals(protocol)) {
                     assertEquals(EnclaveType.VBS.getValue(), Integer.parseInt(enclaveType));
                 } else if (String.valueOf(AttestationProtocol.AAS).equals(protocol)) {
                     assertTrue(Integer.parseInt(enclaveType) == EnclaveType.VBS.getValue()