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}."}}; }