diff --git a/pom.xml b/pom.xml
index 64b65ca79..8c319b81f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,6 +65,7 @@
6.0.0
5.0.0
4.7.2
+ 2.8.6
[1.3.2, 1.4.2]
@@ -107,6 +108,14 @@
true
+
+
+ com.google.code.gson
+ gson
+ ${google.gson.version}
+ true
+
+
org.osgi
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerEnclaveProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerEnclaveProvider.java
index 51f75ec6a..4576e7840 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerEnclaveProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerEnclaveProvider.java
@@ -5,28 +5,195 @@
package com.microsoft.sqlserver.jdbc;
+import static java.nio.charset.StandardCharsets.UTF_16LE;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECGenParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.ECPublicKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.text.MessageFormat;
+import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
+import javax.crypto.KeyAgreement;
+
+
/**
*
* Provides an interface to create an Enclave Session
*
*/
public interface ISQLServerEnclaveProvider {
- byte[] getEnclavePackage(String userSQL, ArrayList enclaveCEKs) throws SQLServerException;
+ static final String proc = "EXEC sp_describe_parameter_encryption ?,?,?";
+
+ default byte[] getEnclavePackage(String userSQL, ArrayList enclaveCEKs) throws SQLServerException {
+ EnclaveSession enclaveSession = getEnclaveSession();
+ if (null != enclaveSession) {
+ try {
+ ByteArrayOutputStream enclavePackage = new ByteArrayOutputStream();
+ enclavePackage.writeBytes(enclaveSession.getSessionID());
+ ByteArrayOutputStream keys = new ByteArrayOutputStream();
+ byte[] randomGUID = new byte[16];
+ SecureRandom.getInstanceStrong().nextBytes(randomGUID);
+ keys.writeBytes(randomGUID);
+ keys.writeBytes(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN)
+ .putLong(enclaveSession.getCounter()).array());
+ keys.writeBytes(MessageDigest.getInstance("SHA-256").digest((userSQL).getBytes(UTF_16LE)));
+ for (byte[] b : enclaveCEKs) {
+ keys.writeBytes(b);
+ }
+ enclaveCEKs.clear();
+ SQLServerAeadAes256CbcHmac256EncryptionKey encryptedKey = new SQLServerAeadAes256CbcHmac256EncryptionKey(
+ enclaveSession.getSessionSecret(), SQLServerAeadAes256CbcHmac256Algorithm.algorithmName);
+ SQLServerAeadAes256CbcHmac256Algorithm algo = new SQLServerAeadAes256CbcHmac256Algorithm(encryptedKey,
+ SQLServerEncryptionType.Randomized, (byte) 0x1);
+ enclavePackage.writeBytes(algo.encryptData(keys.toByteArray()));
+ return enclavePackage.toByteArray();
+ } catch (GeneralSecurityException | SQLServerException e) {
+ SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
+ }
+ }
+ return null;
+ }
+
+ default ResultSet executeProc(PreparedStatement stmt, String userSql, String preparedTypeDefinitions,
+ BaseAttestationRequest req) throws SQLException {
+ ((SQLServerPreparedStatement) stmt).isInternalEncryptionQuery = true;
+ stmt.setNString(1, userSql);
+ if (preparedTypeDefinitions != null && preparedTypeDefinitions.length() != 0) {
+ stmt.setNString(2, preparedTypeDefinitions);
+ } else {
+ stmt.setNString(2, "");
+ }
+ stmt.setBytes(3, req.getBytes());
+ return ((SQLServerPreparedStatement) stmt).executeQueryInternal();
+ }
+
+ default void processAev1SPDE(String userSql, String preparedTypeDefinitions, Parameter[] params,
+ ArrayList parameterNames, SQLServerConnection connection, PreparedStatement stmt, ResultSet rs,
+ ArrayList enclaveRequestedCEKs) throws SQLException {
+ Map cekList = new HashMap<>();
+ CekTableEntry cekEntry = null;
+ boolean isRequestedByEnclave = false;
+ while (rs.next()) {
+ int currentOrdinal = rs.getInt(DescribeParameterEncryptionResultSet1.KeyOrdinal.value());
+ if (!cekList.containsKey(currentOrdinal)) {
+ cekEntry = new CekTableEntry(currentOrdinal);
+ cekList.put(cekEntry.ordinal, cekEntry);
+ } else {
+ cekEntry = cekList.get(currentOrdinal);
+ }
+
+ String keyStoreName = rs.getString(DescribeParameterEncryptionResultSet1.ProviderName.value());
+ String algo = rs.getString(DescribeParameterEncryptionResultSet1.KeyEncryptionAlgorithm.value());
+ String keyPath = rs.getString(DescribeParameterEncryptionResultSet1.KeyPath.value());
+
+ int dbID = rs.getInt(DescribeParameterEncryptionResultSet1.DbId.value());
+ byte[] mdVer = rs.getBytes(DescribeParameterEncryptionResultSet1.KeyMdVersion.value());
+ int keyID = rs.getInt(DescribeParameterEncryptionResultSet1.KeyId.value());
+ byte[] encryptedKey = rs.getBytes(DescribeParameterEncryptionResultSet1.EncryptedKey.value());
+
+ cekEntry.add(encryptedKey, dbID, keyID, rs.getInt(DescribeParameterEncryptionResultSet1.KeyVersion.value()),
+ mdVer, keyPath, keyStoreName, algo);
+
+ // servers supporting enclave computations should always return a boolean indicating whether the key
+ // is
+ // required by enclave or not.
+ if (ColumnEncryptionVersion.AE_v2.value() <= connection.getServerColumnEncryptionVersion().value()) {
+ isRequestedByEnclave = rs
+ .getBoolean(DescribeParameterEncryptionResultSet1.IsRequestedByEnclave.value());
+ }
+
+ if (isRequestedByEnclave) {
+ byte[] keySignature = rs.getBytes(DescribeParameterEncryptionResultSet1.EnclaveCMKSignature.value());
+ String serverName = connection.getTrustedServerNameAE();
+ SQLServerSecurityUtility.verifyColumnMasterKeyMetadata(connection, keyStoreName, keyPath, serverName,
+ isRequestedByEnclave, keySignature);
+
+ // DBID(4) + MDVER(8) + KEYID(2) + CEK(32) = 46
+ ByteBuffer aev2CekEntry = ByteBuffer.allocate(46);
+ aev2CekEntry.order(ByteOrder.LITTLE_ENDIAN).putInt(dbID);
+ aev2CekEntry.put(mdVer);
+ aev2CekEntry.putShort((short) keyID);
+ aev2CekEntry.put(connection.getColumnEncryptionKeyStoreProvider(keyStoreName)
+ .decryptColumnEncryptionKey(keyPath, algo, encryptedKey));
+ enclaveRequestedCEKs.add(aev2CekEntry.array());
+ }
+ }
+
+ // Process the second resultset.
+ if (!stmt.getMoreResults()) {
+ throw new SQLServerException(this, SQLServerException.getErrString("R_UnexpectedDescribeParamFormat"), null,
+ 0, false);
+ }
+
+ rs = (SQLServerResultSet) stmt.getResultSet();
+ while (rs.next() && null != params) {
+ String paramName = rs.getString(DescribeParameterEncryptionResultSet2.ParameterName.value());
+ int paramIndex = parameterNames.indexOf(paramName);
+ int cekOrdinal = rs.getInt(DescribeParameterEncryptionResultSet2.ColumnEncryptionKeyOrdinal.value());
+ cekEntry = cekList.get(cekOrdinal);
+
+ // cekEntry will be null if none of the parameters are encrypted.
+ if ((null != cekEntry) && (cekList.size() < cekOrdinal)) {
+ MessageFormat form = new MessageFormat(
+ SQLServerException.getErrString("R_InvalidEncryptionKeyOrdinal"));
+ Object[] msgArgs = {cekOrdinal, cekEntry.getSize()};
+ throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
+ }
+ SQLServerEncryptionType encType = SQLServerEncryptionType
+ .of((byte) rs.getInt(DescribeParameterEncryptionResultSet2.ColumnEncrytionType.value()));
+ if (SQLServerEncryptionType.PlainText != encType) {
+ params[paramIndex].cryptoMeta = new CryptoMetadata(cekEntry, (short) cekOrdinal,
+ (byte) rs.getInt(DescribeParameterEncryptionResultSet2.ColumnEncryptionAlgorithm.value()), null,
+ encType.value,
+ (byte) rs.getInt(DescribeParameterEncryptionResultSet2.NormalizationRuleVersion.value()));
+ // Decrypt the symmetric key.(This will also validate and throw if needed).
+ SQLServerSecurityUtility.decryptSymmetricKey(params[paramIndex].cryptoMeta, connection);
+ } else {
+ if (params[paramIndex].getForceEncryption()) {
+ MessageFormat form = new MessageFormat(
+ SQLServerException.getErrString("R_ForceEncryptionTrue_HonorAETrue_UnencryptedColumn"));
+ Object[] msgArgs = {userSql, paramIndex + 1};
+ SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), "0", true);
+ }
+ }
+ }
+ }
/**
* Returns the attestation parameters
- * @param createNewParameters
- * indicates whether to create new parameters
+ *
* @param url
* attestation url
* @throws SQLServerException
* when an error occurs.
*/
- void getAttestationParameters(boolean createNewParameters, String url) throws SQLServerException;
+ void getAttestationParameters(String url) throws SQLServerException;
/**
* Creates the enclave session
@@ -41,8 +208,7 @@ public interface ISQLServerEnclaveProvider {
* params
* @param parameterNames
* parameterNames
- * @return
- * list of enclave requested CEKs
+ * @return list of enclave requested CEKs
* @throws SQLServerException
* when an error occurs.
*/
@@ -57,19 +223,152 @@ ArrayList createEnclaveSession(SQLServerConnection connection, String us
/**
* Returns the enclave session
- * @return
- * the enclave session
+ *
+ * @return the enclave session
*/
EnclaveSession getEnclaveSession();
}
abstract class BaseAttestationRequest {
+ protected static final byte[] ECDH_MAGIC = {0x45, 0x43, 0x4b, 0x33, 0x30, 0x00, 0x00, 0x00};
+ protected static final int ENCLAVE_LENGTH = 104;
+
protected PrivateKey privateKey;
+ protected byte[] enclaveChallenge;
+ protected byte[] x;
+ protected byte[] y;
byte[] getBytes() {
return null;
};
+
+ byte[] createSessionSecret(byte[] serverResponse) throws GeneralSecurityException, SQLServerException {
+ if (serverResponse == null || serverResponse.length != ENCLAVE_LENGTH) {
+ SQLServerException.makeFromDriverError(null, this,
+ SQLServerResource.getResource("R_MalformedECDHPublicKey"), "0", false);
+ }
+ ByteBuffer sr = ByteBuffer.wrap(serverResponse);
+ byte[] magic = new byte[8];
+ sr.get(magic);
+ if (!Arrays.equals(magic, ECDH_MAGIC)) {
+ SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_MalformedECDHHeader"),
+ "0", false);
+ }
+ byte[] x = new byte[48];
+ byte[] y = new byte[48];
+ sr.get(x);
+ sr.get(y);
+ /*
+ * Server returns X and Y coordinates, create a key using the point of the server and our key parameters.
+ * Public/Private key parameters are the same.
+ */
+ ECPublicKeySpec keySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(1, x), new BigInteger(1, y)),
+ ((ECPrivateKey) privateKey).getParams());
+ KeyAgreement ka = KeyAgreement.getInstance("ECDH");
+ ka.init(privateKey);
+ // Generate a PublicKey from the above key specifications and do an agreement with our PrivateKey
+ ka.doPhase(KeyFactory.getInstance("EC").generatePublic(keySpec), true);
+ // Generate a Secret from the agreement and hash with SHA-256 to create Session Secret
+ return MessageDigest.getInstance("SHA-256").digest(ka.generateSecret());
+ }
+
+ void initBcryptECDH() throws SQLServerException {
+ /*
+ * Create our BCRYPT_ECCKEY_BLOB
+ */
+ KeyPairGenerator kpg = null;
+ try {
+ kpg = KeyPairGenerator.getInstance("EC");
+ kpg.initialize(new ECGenParameterSpec("secp384r1"));
+ } catch (GeneralSecurityException e) {
+ SQLServerException.makeFromDriverError(null, kpg, e.getLocalizedMessage(), "0", false);
+ }
+ KeyPair kp = kpg.generateKeyPair();
+ ECPublicKey publicKey = (ECPublicKey) kp.getPublic();
+ privateKey = kp.getPrivate();
+ ECPoint w = publicKey.getW();
+ x = w.getAffineX().toByteArray();
+ y = w.getAffineY().toByteArray();
+
+ /*
+ * For some reason toByteArray doesn't have an Signum option like the constructor. Manually remove leading 00
+ * byte if it exists.
+ */
+ if (0 == x[0] && 48 != x.length) {
+ x = Arrays.copyOfRange(x, 1, x.length);
+ }
+ if (0 == y[0] && 48 != y.length) {
+ y = Arrays.copyOfRange(y, 1, y.length);
+ }
+ }
+}
+
+
+abstract class BaseAttestationResponse {
+ protected int totalSize;
+ protected int identitySize;
+ protected int attestationTokenSize;
+ protected int enclaveType;
+
+ protected byte[] enclavePK;
+ protected int sessionInfoSize;
+ protected byte[] sessionID = new byte[8];
+ protected int DHPKsize;
+ protected int DHPKSsize;
+ protected byte[] DHpublicKey;
+ protected byte[] publicKeySig;
+
+ @SuppressWarnings("unused")
+ void validateDHPublicKey() throws SQLServerException, GeneralSecurityException {
+ /*-
+ * Java doesn't directly support PKCS1 padding for RSA keys. Parse the key bytes and create a RSAPublicKeySpec
+ * with the exponent and modulus.
+ *
+ * Static string "RSA1" - 4B (Unused)
+ * Bit count - 4B (Unused)
+ * Public Exponent Length - 4B
+ * Public Modulus Length - 4B
+ * Prime 1 - 4B (Unused)
+ * Prime 2 - 4B (Unused)
+ * Exponent - publicExponentLength bytes
+ * Modulus - publicModulusLength bytes
+ */
+ ByteBuffer enclavePKBuffer = ByteBuffer.wrap(enclavePK).order(ByteOrder.LITTLE_ENDIAN);
+ byte[] rsa1 = new byte[4];
+ enclavePKBuffer.get(rsa1);
+ int bitCount = enclavePKBuffer.getInt();
+ int publicExponentLength = enclavePKBuffer.getInt();
+ int publicModulusLength = enclavePKBuffer.getInt();
+ int prime1 = enclavePKBuffer.getInt();
+ int prime2 = enclavePKBuffer.getInt();
+ byte[] exponent = new byte[publicExponentLength];
+ enclavePKBuffer.get(exponent);
+ byte[] modulus = new byte[publicModulusLength];
+ enclavePKBuffer.get(modulus);
+ if (enclavePKBuffer.remaining() != 0) {
+ SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_EnclavePKLengthError"),
+ "0", false);
+ }
+ RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(1, modulus), new BigInteger(1, exponent));
+ KeyFactory factory = KeyFactory.getInstance("RSA");
+ PublicKey pub = factory.generatePublic(spec);
+ Signature sig = Signature.getInstance("SHA256withRSA");
+ sig.initVerify(pub);
+ sig.update(DHpublicKey);
+ if (!sig.verify(publicKeySig)) {
+ SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_InvalidDHKeySignature"),
+ "0", false);
+ }
+ }
+
+ byte[] getDHpublicKey() {
+ return DHpublicKey;
+ }
+
+ byte[] getSessionID() {
+ return sessionID;
+ }
}
@@ -92,7 +391,65 @@ byte[] getSessionSecret() {
return sessionSecret;
}
- long getCounter() {
+ synchronized long getCounter() {
return counter.getAndIncrement();
}
}
+
+
+final class EnclaveSessionCache {
+ private Hashtable sessionCache;
+
+ EnclaveSessionCache() {
+ sessionCache = new Hashtable<>(0);
+ }
+
+ void addEntry(String servername, String attestationUrl, BaseAttestationRequest b, EnclaveSession e) {
+ sessionCache.put(servername + attestationUrl, new EnclaveCacheEntry(b, e));
+ }
+
+ void removeEntry(EnclaveSession e) {
+ for (Entry entry : sessionCache.entrySet()) {
+ EnclaveCacheEntry ece = entry.getValue();
+ if (Arrays.equals(ece.getEnclaveSession().getSessionID(), e.getSessionID())) {
+ sessionCache.remove(entry.getKey());
+ }
+ }
+ }
+
+ EnclaveCacheEntry getSession(String key) {
+ EnclaveCacheEntry e = sessionCache.get(key);
+ if (null != e && e.expired()) {
+ sessionCache.remove(key);
+ return null;
+ }
+ return e;
+ }
+}
+
+
+class EnclaveCacheEntry {
+ private static final long EIGHT_HOURS_IN_SECONDS = 28800;
+
+ private BaseAttestationRequest bar;
+ private EnclaveSession es;
+ private long timeCreatedInSeconds;
+
+ EnclaveCacheEntry(BaseAttestationRequest b, EnclaveSession e) {
+ bar = b;
+ es = e;
+ timeCreatedInSeconds = Instant.now().getEpochSecond();
+ }
+
+ boolean expired() {
+ return (Instant.now().getEpochSecond() - timeCreatedInSeconds) > EIGHT_HOURS_IN_SECONDS;
+ }
+
+ BaseAttestationRequest getBaseAttestationRequest() {
+ return bar;
+ }
+
+ EnclaveSession getEnclaveSession() {
+ return es;
+ }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAASEnclaveProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAASEnclaveProvider.java
new file mode 100644
index 000000000..0fffc9bae
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerAASEnclaveProvider.java
@@ -0,0 +1,364 @@
+/*
+ * 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.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Hashtable;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+
+/**
+ *
+ * Provides the implementation of the AAS Enclave Provider. The enclave provider encapsulates the client-side
+ * implementation details of the enclave attestation protocol.
+ *
+ */
+public class SQLServerAASEnclaveProvider implements ISQLServerEnclaveProvider {
+
+ private static EnclaveSessionCache enclaveCache = new EnclaveSessionCache();
+
+ private AASAttestationParameters aasParams = null;
+ private AASAttestationResponse hgsResponse = null;
+ private String attestationURL = null;
+ private EnclaveSession enclaveSession = null;
+
+ @Override
+ public void getAttestationParameters(String url) throws SQLServerException {
+ if (null == aasParams) {
+ attestationURL = url;
+ try {
+ aasParams = new AASAttestationParameters(attestationURL);
+ } catch (IOException e) {
+ SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
+ }
+ }
+ }
+
+ @Override
+ public ArrayList createEnclaveSession(SQLServerConnection connection, String userSql,
+ String preparedTypeDefinitions, Parameter[] params,
+ ArrayList parameterNames) throws SQLServerException {
+ ArrayList b = describeParameterEncryption(connection, userSql, preparedTypeDefinitions, params,
+ parameterNames);
+ if (null != hgsResponse && !connection.enclaveEstablished()) {
+ // Check if the session exists in our cache
+ EnclaveCacheEntry entry = enclaveCache.getSession(connection.getServerName() + attestationURL);
+ if (null != entry) {
+ this.enclaveSession = entry.getEnclaveSession();
+ this.aasParams = (AASAttestationParameters) entry.getBaseAttestationRequest();
+ return b;
+ }
+ try {
+ enclaveSession = new EnclaveSession(hgsResponse.getSessionID(),
+ aasParams.createSessionSecret(hgsResponse.getDHpublicKey()));
+ enclaveCache.addEntry(connection.getServerName(), connection.enclaveAttestationUrl, aasParams,
+ 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;
+ aasParams = null;
+ attestationURL = null;
+ }
+
+ @Override
+ public EnclaveSession getEnclaveSession() {
+ return enclaveSession;
+ }
+
+ private AASAttestationResponse validateAttestationResponse(AASAttestationResponse ar) throws SQLServerException {
+ try {
+ ar.validateToken(attestationURL, aasParams.getNonce());
+ ar.validateDHPublicKey(aasParams.getNonce());
+ } catch (GeneralSecurityException e) {
+ SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
+ }
+ return ar;
+ }
+
+ private ArrayList describeParameterEncryption(SQLServerConnection connection, String userSql,
+ String preparedTypeDefinitions, Parameter[] params,
+ ArrayList parameterNames) throws SQLServerException {
+ ArrayList enclaveRequestedCEKs = new ArrayList<>();
+ ResultSet rs = null;
+ try (PreparedStatement stmt = connection.prepareStatement(proc)) {
+ rs = executeProc(stmt, userSql, preparedTypeDefinitions, aasParams);
+ if (null == rs) {
+ // No results. Meaning no parameter.
+ // Should never happen.
+ return enclaveRequestedCEKs;
+ }
+ processAev1SPDE(userSql, preparedTypeDefinitions, params, parameterNames, connection, stmt, rs,
+ enclaveRequestedCEKs);
+ // Process the third resultset.
+ if (connection.isAEv2() && stmt.getMoreResults()) {
+ rs = (SQLServerResultSet) stmt.getResultSet();
+ while (rs.next()) {
+ hgsResponse = new AASAttestationResponse(rs.getBytes(1));
+ // This validates and establishes the enclave session if valid
+ if (!connection.enclaveEstablished()) {
+ hgsResponse = validateAttestationResponse(hgsResponse);
+ }
+ }
+ }
+ // Null check for rs is done already.
+ rs.close();
+ } catch (SQLException e) {
+ if (e instanceof SQLServerException) {
+ throw (SQLServerException) e;
+ } else {
+ throw new SQLServerException(SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"), null,
+ 0, e);
+ }
+ }
+ return enclaveRequestedCEKs;
+ }
+}
+
+
+class AASAttestationParameters extends BaseAttestationRequest {
+
+ // Type 1 is AAS, sent as Little Endian 0x10000000
+ private static final byte[] ENCLAVE_TYPE = new byte[] {0x1, 0x0, 0x0, 0x0};
+ // Nonce length is always 256
+ private static byte[] NONCE_LENGTH = new byte[] {0x0, 0x1, 0x0, 0x0};
+ private byte[] nonce = new byte[256];
+
+ AASAttestationParameters(String attestationUrl) throws SQLServerException, IOException {
+ byte[] attestationUrlBytes = (attestationUrl + '\0').getBytes(UTF_16LE);
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(attestationUrlBytes.length).array());
+ os.writeBytes(attestationUrlBytes);
+ os.writeBytes(NONCE_LENGTH);
+ new SecureRandom().nextBytes(nonce);
+ os.writeBytes(nonce);
+ enclaveChallenge = os.toByteArray();
+
+ initBcryptECDH();
+ }
+
+ @Override
+ byte[] getBytes() {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ os.writeBytes(ENCLAVE_TYPE);
+ os.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(enclaveChallenge.length).array());
+ os.writeBytes(enclaveChallenge);
+ os.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ENCLAVE_LENGTH).array());
+ os.writeBytes(ECDH_MAGIC);
+ os.writeBytes(x);
+ os.writeBytes(y);
+ return os.toByteArray();
+ }
+
+ byte[] getNonce() {
+ return nonce;
+ }
+}
+
+
+class JWTCertificateEntry {
+ private static final long TWENTY_FOUR_HOUR_IN_SECONDS = 86400;
+
+ private JsonArray certificates;
+ private long timeCreatedInSeconds;
+
+ JWTCertificateEntry(JsonArray j) {
+ certificates = j;
+ timeCreatedInSeconds = Instant.now().getEpochSecond();
+ }
+
+ boolean expired() {
+ return (Instant.now().getEpochSecond() - timeCreatedInSeconds) > TWENTY_FOUR_HOUR_IN_SECONDS;
+ }
+
+ JsonArray getCertificates() {
+ return certificates;
+ }
+}
+
+
+@SuppressWarnings("unused")
+class AASAttestationResponse extends BaseAttestationResponse {
+
+ private byte[] attestationToken;
+ private static Hashtable certificateCache = new Hashtable<>();
+
+ AASAttestationResponse(byte[] b) throws SQLServerException {
+ /*-
+ * A model class representing the deserialization of the byte payload the client
+ * receives from SQL Server while setting up a session.
+ * 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];
+ 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);
+ }
+ }
+
+ void validateToken(String attestationUrl, byte[] nonce) throws SQLServerException {
+ try {
+ /*
+ * 3 parts of our JWT token: Header, Body, and Signature. Broken up via '.'
+ */
+ String jwtToken = (new String(attestationToken)).trim();
+ if (jwtToken.startsWith("\"") && jwtToken.endsWith("\"")) {
+ jwtToken = jwtToken.substring(1, jwtToken.length() - 1);
+ }
+ String[] splitString = jwtToken.split("\\.");
+ java.util.Base64.Decoder decoder = Base64.getUrlDecoder();
+ String header = new String(decoder.decode(splitString[0]));
+ String body = new String(decoder.decode(splitString[1]));
+ byte[] stmtSig = decoder.decode(splitString[2]);
+
+ JsonArray keys = null;
+ JWTCertificateEntry cacheEntry = certificateCache.get(attestationUrl);
+ if (null != cacheEntry && !cacheEntry.expired()) {
+ keys = cacheEntry.getCertificates();
+ } else if (null != cacheEntry && cacheEntry.expired()) {
+ certificateCache.remove(attestationUrl);
+ }
+
+ if (null == keys) {
+ // Use the attestation URL to find where our keys are
+ String authorityUrl = new URL(attestationUrl).getAuthority();
+ URL wellKnownUrl = new URL("https://" + authorityUrl + "/.well-known/openid-configuration");
+ URLConnection con = wellKnownUrl.openConnection();
+ String wellKnownUrlJson = new String(con.getInputStream().readAllBytes());
+ JsonObject attestationJson = JsonParser.parseString(wellKnownUrlJson).getAsJsonObject();
+ // Get our Keys
+ URL jwksUrl = new URL(attestationJson.get("jwks_uri").getAsString());
+ URLConnection jwksCon = jwksUrl.openConnection();
+ String jwksUrlJson = new String(jwksCon.getInputStream().readAllBytes());
+ JsonObject jwksJson = JsonParser.parseString(jwksUrlJson).getAsJsonObject();
+ keys = jwksJson.get("keys").getAsJsonArray();
+ certificateCache.put(attestationUrl, new JWTCertificateEntry(keys));
+ }
+ // Find the specific keyID we need from our header
+
+ JsonObject headerJsonObject = JsonParser.parseString(header).getAsJsonObject();
+ String keyID = headerJsonObject.get("kid").getAsString();
+ // Iterate through our list of keys and find the one with the same keyID
+ for (JsonElement key : keys) {
+ JsonObject keyObj = key.getAsJsonObject();
+ String kId = keyObj.get("kid").getAsString();
+ if (kId.equals(keyID)) {
+ JsonArray certsFromServer = keyObj.get("x5c").getAsJsonArray();
+ /*
+ * To create the signature part you have to take the encoded header, the encoded payload, a secret,
+ * the algorithm specified in the header, and sign that.
+ */
+ byte[] signatureBytes = (splitString[0] + "." + splitString[1]).getBytes();
+ for (JsonElement jsonCert : certsFromServer) {
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509Certificate cert = (X509Certificate) cf.generateCertificate(
+ new ByteArrayInputStream(java.util.Base64.getDecoder().decode(jsonCert.getAsString())));
+ Signature sig = Signature.getInstance("SHA256withRSA");
+ sig.initVerify(cert.getPublicKey());
+ sig.update(signatureBytes);
+ if (sig.verify(stmtSig)) {
+ // Token is verified, now check the aas-ehd
+ JsonObject bodyJsonObject = JsonParser.parseString(body).getAsJsonObject();
+ String aasEhd = bodyJsonObject.get("aas-ehd").getAsString();
+ if (!Arrays.equals(Base64.getUrlDecoder().decode(aasEhd), enclavePK)) {
+ SQLServerException.makeFromDriverError(null, this,
+ SQLServerResource.getResource("R_AasEhdError"), "0", false);
+ }
+ if (this.enclaveType == 1) {
+ // Verify rp_data claim as well if VBS
+ String rpData = bodyJsonObject.get("rp_data").getAsString();
+ if (!Arrays.equals(Base64.getUrlDecoder().decode(rpData), nonce)) {
+ SQLServerException.makeFromDriverError(null, this,
+ SQLServerResource.getResource("R_VbsRpDataError"), "0", false);
+ }
+ }
+ return;
+ }
+ }
+ }
+ }
+ SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_AasJWTError"), "0",
+ false);
+ } catch (IOException | GeneralSecurityException e) {
+ SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "", false);
+ }
+ }
+
+ void validateDHPublicKey(byte[] nonce) throws SQLServerException, GeneralSecurityException {
+ if (this.enclaveType == 2) {
+ for (int i = 0; i < enclavePK.length; i++) {
+ enclavePK[i] = (byte) (enclavePK[i] ^ nonce[i % nonce.length]);
+ }
+ }
+ validateDHPublicKey();
+ }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index b81a655b1..3701de78b 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -1476,6 +1476,13 @@ Connection connectInternal(Properties propsIn,
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
+ this.enclaveProvider = new SQLServerAASEnclaveProvider();
+ }
}
// enclave requires columnEncryption=enabled, enclaveAttestationUrl and enclaveAttestationProtocol
@@ -6472,12 +6479,12 @@ boolean isAEv2() {
return (aeVersion >= TDS.COLUMNENCRYPTION_VERSION2);
}
- ISQLServerEnclaveProvider enclaveProvider = new SQLServerVSMEnclaveProvider();
+ private ISQLServerEnclaveProvider enclaveProvider;
ArrayList initEnclaveParameters(String userSql, String preparedTypeDefinitions, Parameter[] params,
ArrayList parameterNames) throws SQLServerException {
if (!this.enclaveEstablished()) {
- enclaveProvider.getAttestationParameters(false, this.enclaveAttestationUrl);
+ enclaveProvider.getAttestationParameters(this.enclaveAttestationUrl);
}
return enclaveProvider.createEnclaveSession(this, userSql, preparedTypeDefinitions, params, parameterNames);
}
@@ -6489,6 +6496,10 @@ boolean enclaveEstablished() {
byte[] generateEnclavePackage(String userSQL, ArrayList enclaveCEKs) throws SQLServerException {
return (enclaveCEKs.size() > 0) ? enclaveProvider.getEnclavePackage(userSQL, enclaveCEKs) : null;
}
+
+ String getServerName() {
+ return this.trustedServerNameAE;
+ }
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index f638c55de..8ee0047b9 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -114,7 +114,8 @@ static ColumnEncryptionSetting valueOfString(String value) throws SQLServerExcep
enum AttestationProtocol {
- HGS("HGS"); // only protocol supported currently
+ HGS("HGS"),
+ AAS("AAS");
private final String protocol;
@@ -130,11 +131,17 @@ static boolean isValidAttestationProtocol(String protocol) {
}
return false;
}
+
+ @Override
+ public String toString() {
+ return protocol;
+ }
}
enum EnclaveType {
- VBS("VBS"); // only VBS type supported
+ VBS("VBS"),
+ SGX("SGX");
private final String type;
@@ -142,6 +149,10 @@ enum EnclaveType {
this.type = type;
}
+ public int getValue() {
+ return ordinal() + 1;
+ }
+
static boolean isValidEnclaveType(String type) {
for (EnclaveType t : EnclaveType.values()) {
if (type.equalsIgnoreCase(t.toString())) {
@@ -150,6 +161,11 @@ static boolean isValidEnclaveType(String type) {
}
return false;
}
+
+ @Override
+ public String toString() {
+ return type;
+ }
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index cbbbf955d..c73b00902 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -618,5 +618,8 @@ protected Object[][] getContents() {
{"R_InvalidSignedStatement",
" Enclave Attestation failed, the statement bytes were not signed by the health certificate."},
{"R_InvalidDHKeySignature",
- "Enclave Attestation failed, the DH Public Key signature can't be verified with the enclave PK."},};
+ "Enclave Attestation failed, the DH Public Key signature can't be verified with the enclave PK."},
+ {"R_AasJWTError", "An error occured when retrieving and validating the JSON Web Token."},
+ {"R_AasEhdError", "aas-ehd claim from JWT did not match enclavePK."},
+ {"R_VbsRpDataError", "rp_data claim from JWT did not match client nonce."},};
};
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java
index 873ff1d5e..416a6a8a7 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerVSMEnclaveProvider.java
@@ -5,45 +5,27 @@
package com.microsoft.sqlserver.jdbc;
-import static java.nio.charset.StandardCharsets.UTF_16LE;
-
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.GeneralSecurityException;
-import java.security.KeyFactory;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.MessageDigest;
-import java.security.PublicKey;
-import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
-import java.security.interfaces.ECPrivateKey;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECGenParameterSpec;
-import java.security.spec.ECPoint;
-import java.security.spec.ECPublicKeySpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
-import java.security.spec.RSAPublicKeySpec;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.MessageFormat;
+import java.time.Instant;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.crypto.KeyAgreement;
+import java.util.Hashtable;
/**
@@ -54,15 +36,17 @@
*/
public class SQLServerVSMEnclaveProvider implements ISQLServerEnclaveProvider {
+ private static EnclaveSessionCache enclaveCache = new EnclaveSessionCache();
+
private VSMAttestationParameters vsmParams = null;
- private AttestationResponse hgsResponse = null;
- private String attestationURL = null;
+ private VSMAttestationResponse hgsResponse = null;
+ private String attestationUrl = null;
private EnclaveSession enclaveSession = null;
@Override
- public void getAttestationParameters(boolean createNewParameters, String url) throws SQLServerException {
- if (null == vsmParams || createNewParameters) {
- attestationURL = url;
+ public void getAttestationParameters(String url) throws SQLServerException {
+ if (null == vsmParams) {
+ attestationUrl = url;
vsmParams = new VSMAttestationParameters();
}
}
@@ -74,9 +58,19 @@ public ArrayList createEnclaveSession(SQLServerConnection connection, St
ArrayList b = describeParameterEncryption(connection, userSql, preparedTypeDefinitions, params,
parameterNames);
if (null != hgsResponse && !connection.enclaveEstablished()) {
+ // Check if the session exists in our cache
+ EnclaveCacheEntry entry = enclaveCache.getSession(connection.getServerName() + attestationUrl);
+ if (null != entry) {
+ this.enclaveSession = entry.getEnclaveSession();
+ this.vsmParams = (VSMAttestationParameters) entry.getBaseAttestationRequest();
+ return b;
+ }
+ // If not, set it up
try {
enclaveSession = new EnclaveSession(hgsResponse.getSessionID(),
vsmParams.createSessionSecret(hgsResponse.getDHpublicKey()));
+ enclaveCache.addEntry(connection.getServerName(), connection.enclaveAttestationUrl, vsmParams,
+ enclaveSession);
} catch (GeneralSecurityException e) {
SQLServerException.makeFromDriverError(connection, this, e.getLocalizedMessage(), "0", false);
}
@@ -86,9 +80,12 @@ public ArrayList createEnclaveSession(SQLServerConnection connection, St
@Override
public void invalidateEnclaveSession() {
+ if (null != enclaveSession) {
+ enclaveCache.removeEntry(enclaveSession);
+ }
enclaveSession = null;
vsmParams = null;
- attestationURL = null;
+ attestationUrl = null;
}
@Override
@@ -96,36 +93,7 @@ public EnclaveSession getEnclaveSession() {
return enclaveSession;
}
- @Override
- public byte[] getEnclavePackage(String userSQL, ArrayList enclaveCEKs) throws SQLServerException {
- if (null != enclaveSession) {
- try {
- ByteArrayOutputStream enclavePackage = new ByteArrayOutputStream();
- enclavePackage.writeBytes(enclaveSession.getSessionID());
- ByteArrayOutputStream keys = new ByteArrayOutputStream();
- byte[] randomGUID = new byte[16];
- SecureRandom.getInstanceStrong().nextBytes(randomGUID);
- keys.writeBytes(randomGUID);
- keys.writeBytes(ByteBuffer.allocate(8).putLong(enclaveSession.getCounter()).array());
- keys.writeBytes(MessageDigest.getInstance("SHA-256").digest((userSQL).getBytes(UTF_16LE)));
- for (byte[] b : enclaveCEKs) {
- keys.writeBytes(b);
- }
- enclaveCEKs.clear();
- SQLServerAeadAes256CbcHmac256EncryptionKey encryptedKey = new SQLServerAeadAes256CbcHmac256EncryptionKey(
- enclaveSession.getSessionSecret(), SQLServerAeadAes256CbcHmac256Algorithm.algorithmName);
- SQLServerAeadAes256CbcHmac256Algorithm algo = new SQLServerAeadAes256CbcHmac256Algorithm(encryptedKey,
- SQLServerEncryptionType.Randomized, (byte) 0x1);
- enclavePackage.writeBytes(algo.encryptData(keys.toByteArray()));
- return enclavePackage.toByteArray();
- } catch (GeneralSecurityException e) {
- SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
- }
- }
- return null;
- }
-
- private AttestationResponse validateAttestationResponse(AttestationResponse ar) throws SQLServerException {
+ private VSMAttestationResponse validateAttestationResponse(VSMAttestationResponse ar) throws SQLServerException {
try {
byte[] attestationCerts = getAttestationCertificates();
ar.validateCert(attestationCerts);
@@ -137,15 +105,28 @@ private AttestationResponse validateAttestationResponse(AttestationResponse ar)
return ar;
}
+ private static Hashtable certificateCache = new Hashtable<>();
+
private byte[] getAttestationCertificates() throws IOException {
- java.net.URL url = new java.net.URL(attestationURL + "/attestationservice.svc/v2.0/signingCertificates/");
- java.net.URLConnection con = url.openConnection();
- String s = new String(con.getInputStream().readAllBytes());
- // omit the square brackets that come with the JSON
- String[] bytesString = s.substring(1, s.length() - 1).split(",");
- byte[] certData = new byte[bytesString.length];
- for (int i = 0; i < certData.length; i++) {
- certData[i] = (byte) (Integer.parseInt(bytesString[i]));
+ byte[] certData = null;
+ X509CertificateEntry cacheEntry = certificateCache.get(attestationUrl);
+ if (null != cacheEntry && !cacheEntry.expired()) {
+ certData = cacheEntry.getCertificates();
+ } else if (null != cacheEntry && cacheEntry.expired()) {
+ certificateCache.remove(attestationUrl);
+ }
+
+ if (null == certData) {
+ java.net.URL url = new java.net.URL(attestationUrl + "/attestationservice.svc/v2.0/signingCertificates/");
+ java.net.URLConnection con = url.openConnection();
+ String s = new String(con.getInputStream().readAllBytes());
+ // omit the square brackets that come with the JSON
+ String[] bytesString = s.substring(1, s.length() - 1).split(",");
+ certData = new byte[bytesString.length];
+ for (int i = 0; i < certData.length; i++) {
+ certData[i] = (byte) (Integer.parseInt(bytesString[i]));
+ }
+ certificateCache.put(attestationUrl, new X509CertificateEntry(certData));
}
return certData;
}
@@ -155,146 +136,26 @@ private ArrayList describeParameterEncryption(SQLServerConnection connec
ArrayList parameterNames) throws SQLServerException {
ArrayList enclaveRequestedCEKs = new ArrayList<>();
ResultSet rs = null;
- try (PreparedStatement stmt = connection.prepareStatement("EXEC sp_describe_parameter_encryption ?,?,?")) {
- ((SQLServerPreparedStatement) stmt).isInternalEncryptionQuery = true;
- stmt.setNString(1, userSql);
- if (preparedTypeDefinitions != null && preparedTypeDefinitions.length() != 0) {
- stmt.setNString(2, preparedTypeDefinitions);
- } else {
- stmt.setNString(2, "");
- }
- stmt.setBytes(3, vsmParams.getBytes());
- rs = ((SQLServerPreparedStatement) stmt).executeQueryInternal();
-
+ try (PreparedStatement stmt = connection.prepareStatement(proc)) {
+ rs = executeProc(stmt, userSql, preparedTypeDefinitions, vsmParams);
if (null == rs) {
// No results. Meaning no parameter.
// Should never happen.
return enclaveRequestedCEKs;
}
-
- Map cekList = new HashMap<>();
- CekTableEntry cekEntry = null;
- boolean isRequestedByEnclave = false;
- try {
- while (rs.next()) {
- int currentOrdinal = rs.getInt(DescribeParameterEncryptionResultSet1.KeyOrdinal.value());
- if (!cekList.containsKey(currentOrdinal)) {
- cekEntry = new CekTableEntry(currentOrdinal);
- cekList.put(cekEntry.ordinal, cekEntry);
- } else {
- cekEntry = cekList.get(currentOrdinal);
- }
-
- String keyStoreName = rs.getString(DescribeParameterEncryptionResultSet1.ProviderName.value());
- String algo = rs.getString(DescribeParameterEncryptionResultSet1.KeyEncryptionAlgorithm.value());
- String keyPath = rs.getString(DescribeParameterEncryptionResultSet1.KeyPath.value());
-
- int dbID = rs.getInt(DescribeParameterEncryptionResultSet1.DbId.value());
- byte[] mdVer = rs.getBytes(DescribeParameterEncryptionResultSet1.KeyMdVersion.value());
- int keyID = rs.getInt(DescribeParameterEncryptionResultSet1.KeyId.value());
- byte[] encryptedKey = rs.getBytes(DescribeParameterEncryptionResultSet1.EncryptedKey.value());
-
- cekEntry.add(encryptedKey, dbID, keyID,
- rs.getInt(DescribeParameterEncryptionResultSet1.KeyVersion.value()), mdVer, keyPath,
- keyStoreName, algo);
-
- // servers supporting enclave computations should always return a boolean indicating whether the key
- // is
- // required by enclave or not.
- if (ColumnEncryptionVersion.AE_v2.value() <= connection.getServerColumnEncryptionVersion()
- .value()) {
- isRequestedByEnclave = rs
- .getBoolean(DescribeParameterEncryptionResultSet1.IsRequestedByEnclave.value());
- }
-
- if (isRequestedByEnclave) {
- byte[] keySignature = rs
- .getBytes(DescribeParameterEncryptionResultSet1.EnclaveCMKSignature.value());
- String serverName = connection.getTrustedServerNameAE();
- SQLServerSecurityUtility.verifyColumnMasterKeyMetadata(connection, keyStoreName, keyPath,
- serverName, isRequestedByEnclave, keySignature);
-
- // DBID(4) + MDVER(8) + KEYID(2) + CEK(32) = 46
- ByteBuffer aev2CekEntry = ByteBuffer.allocate(46);
- aev2CekEntry.order(ByteOrder.LITTLE_ENDIAN).putInt(dbID);
- aev2CekEntry.put(mdVer);
- aev2CekEntry.putShort((short) keyID);
- aev2CekEntry.put(connection.getColumnEncryptionKeyStoreProvider(keyStoreName)
- .decryptColumnEncryptionKey(keyPath, algo, encryptedKey));
- enclaveRequestedCEKs.add(aev2CekEntry.array());
- }
- }
- } catch (SQLException e) {
- if (e instanceof SQLServerException) {
- throw (SQLServerException) e;
- } else {
- throw new SQLServerException(SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"),
- null, 0, e);
- }
- }
-
- // Process the second resultset.
- if (!stmt.getMoreResults()) {
- throw new SQLServerException(this, SQLServerException.getErrString("R_UnexpectedDescribeParamFormat"),
- null, 0, false);
- }
-
- try {
- rs = (SQLServerResultSet) stmt.getResultSet();
- while (rs.next() && null != params) {
- String paramName = rs.getString(DescribeParameterEncryptionResultSet2.ParameterName.value());
- int paramIndex = parameterNames.indexOf(paramName);
- int cekOrdinal = rs
- .getInt(DescribeParameterEncryptionResultSet2.ColumnEncryptionKeyOrdinal.value());
- cekEntry = cekList.get(cekOrdinal);
-
- // cekEntry will be null if none of the parameters are encrypted.
- if ((null != cekEntry) && (cekList.size() < cekOrdinal)) {
- MessageFormat form = new MessageFormat(
- SQLServerException.getErrString("R_InvalidEncryptionKeyOrdinal"));
- Object[] msgArgs = {cekOrdinal, cekEntry.getSize()};
- throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
- }
- SQLServerEncryptionType encType = SQLServerEncryptionType
- .of((byte) rs.getInt(DescribeParameterEncryptionResultSet2.ColumnEncrytionType.value()));
- if (SQLServerEncryptionType.PlainText != encType) {
- params[paramIndex].cryptoMeta = new CryptoMetadata(cekEntry, (short) cekOrdinal,
- (byte) rs.getInt(
- DescribeParameterEncryptionResultSet2.ColumnEncryptionAlgorithm.value()),
- null, encType.value, (byte) rs.getInt(
- DescribeParameterEncryptionResultSet2.NormalizationRuleVersion.value()));
- // Decrypt the symmetric key.(This will also validate and throw if needed).
- SQLServerSecurityUtility.decryptSymmetricKey(params[paramIndex].cryptoMeta, connection);
- } else {
- if (params[paramIndex].getForceEncryption()) {
- MessageFormat form = new MessageFormat(SQLServerException
- .getErrString("R_ForceEncryptionTrue_HonorAETrue_UnencryptedColumn"));
- Object[] msgArgs = {userSql, paramIndex + 1};
- SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), "0", true);
- }
- }
- }
- } catch (SQLException e) {
- if (e instanceof SQLServerException) {
- throw (SQLServerException) e;
- } else {
- throw new SQLServerException(SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"),
- null, 0, e);
- }
- }
-
+ processAev1SPDE(userSql, preparedTypeDefinitions, params, parameterNames, connection, stmt, rs,
+ enclaveRequestedCEKs);
// Process the third resultset.
if (connection.isAEv2() && stmt.getMoreResults()) {
rs = (SQLServerResultSet) stmt.getResultSet();
while (rs.next()) {
- hgsResponse = new AttestationResponse(rs.getBytes(1));
+ hgsResponse = new VSMAttestationResponse(rs.getBytes(1));
// This validates and establishes the enclave session if valid
if (!connection.enclaveEstablished()) {
hgsResponse = validateAttestationResponse(hgsResponse);
}
}
}
-
// Null check for rs is done already.
rs.close();
} catch (SQLException e) {
@@ -311,111 +172,35 @@ private ArrayList describeParameterEncryption(SQLServerConnection connec
class VSMAttestationParameters extends BaseAttestationRequest {
-
- // Static byte[] for VSM ECDH
- private static byte ECDH_MAGIC[] = {0x45, 0x43, 0x4b, 0x33, 0x30, 0x00, 0x00, 0x00};
// Type 3 is VSM, sent as Little Endian 0x30000000
private static byte ENCLAVE_TYPE[] = new byte[] {0x3, 0x0, 0x0, 0x0};
- // VSM doesn't have a challenge
- private static byte ENCLAVE_CHALLENGE[] = new byte[] {0x0, 0x0, 0x0, 0x0};
- private static int ENCLAVE_LENGTH = 104;
- private byte[] x;
- private byte[] y;
VSMAttestationParameters() throws SQLServerException {
- KeyPairGenerator kpg = null;
- try {
- kpg = KeyPairGenerator.getInstance("EC");
- kpg.initialize(new ECGenParameterSpec("secp384r1"));
- } catch (GeneralSecurityException e) {
- SQLServerException.makeFromDriverError(null, kpg, e.getLocalizedMessage(), "0", false);
- }
- KeyPair kp = kpg.generateKeyPair();
- ECPublicKey publicKey = (ECPublicKey) kp.getPublic();
- privateKey = kp.getPrivate();
- ECPoint w = publicKey.getW();
- x = w.getAffineX().toByteArray();
- y = w.getAffineY().toByteArray();
-
- /*
- * For some reason toByteArray doesn't have an Signum option like the constructor. Manually remove leading 00
- * byte if it exists.
- */
- if (x[0] == 0 && x.length != 48) {
- x = Arrays.copyOfRange(x, 1, x.length);
- }
- if (y[0] == 0 && y.length != 48) {
- y = Arrays.copyOfRange(y, 1, y.length);
- }
+ enclaveChallenge = new byte[] {0x0, 0x0, 0x0, 0x0};
+ initBcryptECDH();
}
@Override
byte[] getBytes() {
ByteArrayOutputStream os = new ByteArrayOutputStream();
os.writeBytes(ENCLAVE_TYPE);
- os.writeBytes(ENCLAVE_CHALLENGE);
+ os.writeBytes(enclaveChallenge);
os.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ENCLAVE_LENGTH).array());
os.writeBytes(ECDH_MAGIC);
os.writeBytes(x);
os.writeBytes(y);
return os.toByteArray();
}
-
- byte[] createSessionSecret(byte[] serverResponse) throws GeneralSecurityException, SQLServerException {
- if (null == serverResponse || serverResponse.length != ENCLAVE_LENGTH) {
- SQLServerException.makeFromDriverError(null, this,
- SQLServerResource.getResource("R_MalformedECDHPublicKey"), "0", false);
- }
- ByteBuffer sr = ByteBuffer.wrap(serverResponse);
- byte[] magic = new byte[8];
- sr.get(magic);
-
- if (!Arrays.equals(magic, ECDH_MAGIC)) {
- SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_MalformedECDHHeader"),
- "0", false);
- }
-
- byte[] x = new byte[48];
- byte[] y = new byte[48];
- sr.get(x);
- sr.get(y);
- /*
- * Server returns X and Y coordinates, create a key using the point of the server and our key parameters.
- * Public/Private key parameters are the same.
- */
- ECPublicKeySpec keySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(1, x), new BigInteger(1, y)),
- ((ECPrivateKey) privateKey).getParams());
- KeyAgreement ka = KeyAgreement.getInstance("ECDH");
- ka.init(privateKey);
- // Generate a PublicKey from the above key specifications and do an agreement with our PrivateKey
- ka.doPhase(KeyFactory.getInstance("EC").generatePublic(keySpec), true);
- // Generate a Secret from the agreement and hash with SHA-256 to create Session Secret
- return MessageDigest.getInstance("SHA-256").digest(ka.generateSecret());
- }
}
@SuppressWarnings("unused")
-class AttestationResponse {
- private int totalSize;
- private int identitySize;
- private int healthReportSize;
- private int enclaveReportSize;
-
- private byte[] enclavePK;
+class VSMAttestationResponse extends BaseAttestationResponse {
private byte[] healthReportCertificate;
private byte[] enclaveReportPackage;
-
- private int sessionInfoSize;
- private byte[] sessionID = new byte[8];
- private int DHPKsize;
- private int DHPKSsize;
- private byte[] DHpublicKey;
- private byte[] publicKeySig;
-
private X509Certificate healthCert;
- AttestationResponse(byte[] b) throws SQLServerException {
+ VSMAttestationResponse(byte[] b) throws SQLServerException {
/*-
* Parse the attestation response.
*
@@ -437,8 +222,8 @@ class AttestationResponse {
if (null != response) {
this.totalSize = response.getInt();
this.identitySize = response.getInt();
- this.healthReportSize = response.getInt();
- this.enclaveReportSize = response.getInt();
+ int healthReportSize = response.getInt();
+ int enclaveReportSize = response.getInt();
enclavePK = new byte[identitySize];
healthReportCertificate = new byte[healthReportSize];
@@ -464,7 +249,6 @@ class AttestationResponse {
SQLServerException.makeFromDriverError(null, this,
SQLServerResource.getResource("R_EnclaveResponseLengthError"), "0", false);
}
-
// Create a X.509 certificate from the bytes
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
@@ -495,7 +279,6 @@ void validateCert(byte[] b) throws SQLServerException {
SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
}
}
-
SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_InvalidHealthCert"), "0",
false);
}
@@ -546,54 +329,25 @@ void validateStatementSignature() throws SQLServerException, GeneralSecurityExce
SQLServerResource.getResource("R_InvalidSignedStatement"), "0", false);
}
}
+}
- void validateDHPublicKey() throws SQLServerException, GeneralSecurityException {
- /*-
- * Java doesn't directly support PKCS1 padding for RSA keys. Parse the key bytes and create a RSAPublicKeySpec
- * with the exponent and modulus.
- *
- * Static string "RSA1" - 4B (Unused)
- * Bit count - 4B (Unused)
- * Public Exponent Length - 4B
- * Public Modulus Length - 4B
- * Prime 1 - 4B (Unused)
- * Prime 2 - 4B (Unused)
- * Exponent - publicExponentLength bytes
- * Modulus - publicModulusLength bytes
- */
- ByteBuffer enclavePKBuffer = ByteBuffer.wrap(enclavePK).order(ByteOrder.LITTLE_ENDIAN);
- byte[] rsa1 = new byte[4];
- enclavePKBuffer.get(rsa1);
- int bitCount = enclavePKBuffer.getInt();
- int publicExponentLength = enclavePKBuffer.getInt();
- int publicModulusLength = enclavePKBuffer.getInt();
- int prime1 = enclavePKBuffer.getInt();
- int prime2 = enclavePKBuffer.getInt();
- byte[] exponent = new byte[publicExponentLength];
- enclavePKBuffer.get(exponent);
- byte[] modulus = new byte[publicModulusLength];
- enclavePKBuffer.get(modulus);
- if (enclavePKBuffer.remaining() != 0) {
- SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_EnclavePKLengthError"),
- "0", false);
- }
- RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(1, modulus), new BigInteger(1, exponent));
- KeyFactory factory = KeyFactory.getInstance("RSA");
- PublicKey pub = factory.generatePublic(spec);
- Signature sig = Signature.getInstance("SHA256withRSA");
- sig.initVerify(pub);
- sig.update(DHpublicKey);
- if (!sig.verify(publicKeySig)) {
- SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_InvalidDHKeySignature"),
- "0", false);
- }
+
+class X509CertificateEntry {
+ private static final long EIGHT_HOUR_IN_SECONDS = 28800;
+
+ private byte[] certificates;
+ private long timeCreatedInSeconds;
+
+ X509CertificateEntry(byte[] b) {
+ certificates = b;
+ timeCreatedInSeconds = Instant.now().getEpochSecond();
}
- byte[] getDHpublicKey() {
- return DHpublicKey;
+ boolean expired() {
+ return (Instant.now().getEpochSecond() - timeCreatedInSeconds) > EIGHT_HOUR_IN_SECONDS;
}
- byte[] getSessionID() {
- return sessionID;
+ byte[] getCertificates() {
+ return certificates;
}
}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java
index 2a75721ab..ee92ce495 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java
@@ -1248,6 +1248,7 @@ private void testAlterColumnEncryption(SQLServerStatement stmt, String tableName
if (!TestUtils.isAEv2(con)) {
fail(TestResource.getResource("R_expectedExceptionNotThrown"));
} else {
+ e.printStackTrace();
fail(TestResource.getResource("R_AlterAEv2Error") + e.getMessage() + "Query: " + sql);
}
}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
index 8d4a21f51..b9c7a5ce8 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/EnclavePackageTest.java
@@ -10,6 +10,7 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
+import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -198,7 +199,7 @@ public static void setupEnclave() throws Exception {
String enclaveAttestationProtocol = TestUtils.getConfiguredProperty("enclaveAttestationProtocol");
connectionStringEnclave = TestUtils.addOrOverrideProperty(connectionStringEnclave, "enclaveAttestationProtocol",
- (null != enclaveAttestationProtocol) ? enclaveAttestationProtocol : AttestationProtocol.HGS.toString());
+ (null != enclaveAttestationProtocol) ? enclaveAttestationProtocol : "HGS");
// reset logging to avoid severe logs due to negative testing
LogManager.getLogManager().reset();
@@ -372,7 +373,7 @@ public static void testBadSessionSecret() throws SQLServerException {
@SuppressWarnings("unused")
public static void testNullAttestationResponse() throws SQLServerException {
try {
- AttestationResponse resp = new AttestationResponse(null);
+ VSMAttestationResponse resp = new VSMAttestationResponse(null);
} catch (SQLServerException e) {
assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_EnclaveResponseLengthError")));
} catch (Exception e) {
@@ -387,7 +388,7 @@ public static void testNullAttestationResponse() throws SQLServerException {
public static void testBadAttestationResponse() throws SQLServerException {
try {
byte[] responseBytes = new byte[36];
- AttestationResponse resp = new AttestationResponse(responseBytes);
+ VSMAttestationResponse resp = new VSMAttestationResponse(responseBytes);
} catch (SQLServerException e) {
assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_HealthCertError")));
} catch (Exception e) {
@@ -400,7 +401,7 @@ public static void testBadAttestationResponse() throws SQLServerException {
*/
public static void testBadCertSignature() throws SQLServerException, CertificateException {
try {
- AttestationResponse resp = new AttestationResponse(healthReportCertificate);
+ VSMAttestationResponse resp = new VSMAttestationResponse(healthReportCertificate);
resp.validateCert(null);
} catch (SQLServerException e) {
assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidHealthCert")));
@@ -440,7 +441,17 @@ private static void verifyEnclaveEnabled(Connection con) throws SQLException {
try (Statement stmt = con.createStatement(); ResultSet rs = stmt.executeQuery(
"SELECT [name], [value], [value_in_use] FROM sys.configurations WHERE [name] = 'column encryption enclave type';")) {
while (rs.next()) {
- assertEquals("1", rs.getString(2));
+ String enclaveType = rs.getString(2);
+ String enclaveAttestationProtocol = getConfiguredProperty("enclaveAttestationProtocol");
+ if (String.valueOf(AttestationProtocol.HGS).equals(enclaveAttestationProtocol)) {
+ assertEquals(EnclaveType.VBS.getValue(), Integer.parseInt(enclaveType));
+ } else if (String.valueOf(AttestationProtocol.AAS).equals(enclaveAttestationProtocol)) {
+ assertEquals(EnclaveType.SGX.getValue(), Integer.parseInt(enclaveType));
+ } else {
+ MessageFormat form = new MessageFormat(TestResource.getResource("R_invalidEnclaveType"));
+ Object[] msgArgs = {enclaveType};
+ fail(form.format(msgArgs));
+ }
}
}
}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
index de1a912ba..80d641d22 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
@@ -182,5 +182,6 @@ protected Object[][] getContents() {
{"R_NoPrivilege", "The EXECUTE permission was denied on the object {0}"},
{"R_resultSetEmpty", "Result set is empty."}, {"R_AlterAEv2Error", "Alter Column Encryption failed."},
{"R_RichQueryError", "Rich query failed."}, {"R_reqExternalSetup", "External setup for test required."},
- {"R_invalidEnclaveSessionFailed", "invalidate enclave session failed."}};
+ {"R_invalidEnclaveSessionFailed", "invalidate enclave session failed."},
+ {"R_invalidEnclaveType", "Invalid enclave type {0}."}};
}