diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs index a3b4545d03..46a8e28cf8 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs @@ -122,6 +122,9 @@ internal override void CreateEnclaveSession(byte[] attestationInfo, ECDiffieHell // Perform Attestation per VSM protocol VerifyAttestationInfo(enclaveSessionParameters.AttestationUrl, info.HealthReport, info.EnclaveReportPackage); + // Verify the enclave public key is bound to the signed report, before it is used for key exchange + VerifyEnclavePublicKeyBinding(info.EnclaveReportPackage, info.Identity); + // Set up shared secret and validate signature byte[] sharedSecret = GetSharedSecret(info.Identity, info.EnclaveDHInfo, clientDHKey); @@ -150,6 +153,68 @@ internal override void InvalidateEnclaveSession(EnclaveSessionParameters enclave #region Private helpers + /// + /// Verifies that the enclave's Diffie-Hellman public key is the one committed to by the signed + /// attestation report. A genuine VBS enclave writes SHA-256(public key) into the first 32 bytes of the + /// report's EnclaveData, and that EnclaveData is covered by the report signature that + /// has already validated. Confirming this binding ensures the key used + /// to derive the session secret is the exact key the attested enclave committed to. This mirrors the + /// aas-ehd key binding performed on the AAS attestation path. + /// + /// + /// The signature-verified enclave report package. Its Report.EnclaveData supplies the committed + /// key hash. + /// + /// + /// The enclave public key that will be used to derive the session secret. + /// + /// + /// Thrown when the report's EnclaveData does not match SHA-256 of , or + /// when the required report or key data is missing. In either case attestation is rejected. + /// + /// + /// This is internal to allow for targeted unit testing without resorting to reflection. + /// + internal static void VerifyEnclavePublicKeyBinding(EnclaveReportPackage enclaveReportPackage, EnclavePublicKey enclavePublicKey) + { + const int ReportDataLength = 32; // SHA-256 digest length + + // The first 32 bytes of EnclaveData must equal SHA-256 of the key we will use to derive the session + // secret. Read both inputs defensively so missing data results in a clean rejection rather than a + // NullReferenceException (SHA256 hashing also throws on a null input). + byte[] reportData = enclaveReportPackage?.Report?.EnclaveData; + byte[] publicKey = enclavePublicKey?.PublicKey; + + if (reportData == null || reportData.Length < ReportDataLength || publicKey == null || publicKey.Length == 0) + { + throw new ArgumentException(Strings.VerifyEnclaveKeyBindingFailed); + } + +#if NET + // Hash directly into a stack buffer to avoid a heap allocation for the digest. + Span expectedBinding = stackalloc byte[ReportDataLength]; + SHA256.HashData(publicKey, expectedBinding); + + // Use a fixed-time comparison in this security-sensitive path so the check does not leak a timing + // signal about how many leading bytes matched. + bool bound = CryptographicOperations.FixedTimeEquals( + reportData.AsSpan(0, ReportDataLength), expectedBinding); +#else + byte[] expectedBinding; + using (SHA256 sha256 = SHA256.Create()) + { + expectedBinding = sha256.ComputeHash(publicKey); + } + + bool bound = FixedTimeEquals(reportData, expectedBinding, ReportDataLength); +#endif + + if (!bound) + { + throw new ArgumentException(Strings.VerifyEnclaveKeyBindingFailed); + } + } + // Performs Attestation per the protocol used by Virtual Secure Modules. private void VerifyAttestationInfo(string attestationUrl, HealthReport healthReport, EnclaveReportPackage enclaveReportPackage) { @@ -241,7 +306,7 @@ private bool AnyCertificatesExpired(X509Certificate2Collection certificates) /// /// Verifies that a chain of trust can be built from the health report provided /// by SQL Server and the attestation service's root signing certificate(s). - /// + /// /// If the method returns false, the value of chainStatus doesn't matter. The chain could not be validated. /// /// @@ -340,6 +405,26 @@ private void VerifyEnclaveReportSignature(EnclaveReportPackage enclaveReportPack } } +#if !NET + // CryptographicOperations.FixedTimeEquals is unavailable on .NET Framework, so hand-roll an equivalent + // constant-time comparison of the first bytes for the key-binding check above. + private static bool FixedTimeEquals(byte[] left, byte[] right, int length) + { + if (left == null || right == null || left.Length < length || right.Length < length) + { + return false; + } + + int accumulator = 0; + for (int index = 0; index < length; index++) + { + accumulator |= left[index] ^ right[index]; + } + + return accumulator == 0; + } +#endif + // Verifies the enclave policy matches expected policy. private void VerifyEnclavePolicy(EnclaveReportPackage enclaveReportPackage) { diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs index 75ee7d7b82..1fadc67306 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs @@ -6342,6 +6342,15 @@ internal static string VerifyEnclaveReportFailed { } } + /// + /// Looks up a localized string similar to Enclave attestation failed because the signed enclave report did not bind to the enclave public key used to establish the session. The enclave public key must match the value committed to by the signed report - see https://go.microsoft.com/fwlink/?linkid=2160553 for more details. If correct, contact Customer Support Services.. + /// + internal static string VerifyEnclaveKeyBindingFailed { + get { + return ResourceManager.GetString("VerifyEnclaveKeyBindingFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to The enclave report received from SQL Server is not in the correct format. Contact Customer Support Services.. /// diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx index 4d65d00a71..bd30563594 100644 --- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx @@ -1968,6 +1968,9 @@ Signature verification of the enclave report failed. The report signature does not match the signature computed using the HGS root certificate. Verify the DNS mapping for the endpoint - see https://go.microsoft.com/fwlink/?linkid=2160553 for more details. If correct, contact Customer Support Services. + + Enclave attestation failed because the signed enclave report did not bind to the enclave public key used to establish the session. The enclave public key must match the value committed to by the signed report - see https://go.microsoft.com/fwlink/?linkid=2160553 for more details. If correct, contact Customer Support Services. + The enclave report received from SQL Server is not in the correct format. Contact Customer Support Services. diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderTest.cs new file mode 100644 index 0000000000..08de59fd91 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderTest.cs @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Security.Cryptography; +using System.Text; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// +/// Tests the enclave public key binding check on the VSM/HGS attestation path. A genuine VBS enclave +/// commits to its public key by placing SHA-256(public key) in the first 32 bytes of the report's +/// signature-covered EnclaveData, so the binding must accept a matching key and reject any other. +/// +public class VirtualSecureModeEnclaveProviderTest +{ + private const int EnclaveReportPackageHeaderSize = 6 * sizeof(uint); // 24 + private const int EnclaveReportSize = (sizeof(uint) * 2) + 64 + 152; // ReportSize + ReportVersion + EnclaveData + EnclaveIdentity = 224 + + /// + /// A report whose EnclaveData commits to the key used for the session passes the binding check. + /// + [Fact] + public void VerifyEnclavePublicKeyBinding_GenuineKey_Succeeds() + { + // Arrange + byte[] enclaveKey = Encoding.UTF8.GetBytes("genuine-enclave-public-key-blob"); + EnclaveReportPackage testPackage = BuildReportPackage(Sha256(enclaveKey)); + EnclavePublicKey testKey = new EnclavePublicKey(enclaveKey); + + // Act / Assert + // Report commits to enclaveKey and the session uses enclaveKey: the binding holds, so no exception. + VirtualizationBasedSecurityEnclaveProviderBase.VerifyEnclavePublicKeyBinding(testPackage, testKey); + } + + /// + /// A report whose committed data does not match the session's enclave public key is rejected. + /// + [Fact] + public void VerifyEnclavePublicKeyBinding_SwappedKey_Throws() + { + // Arrange + byte[] committedKeyBytes = Encoding.UTF8.GetBytes("committed-enclave-public-key-blob"); + + // The signed report commits to committedKey... + EnclaveReportPackage testPackage = BuildReportPackage(Sha256(committedKeyBytes)); + + // ...but a different enclave public key is offered for the session. + byte[] substitutedKeyBytes = Encoding.UTF8.GetBytes("substituted-enclave-public-key"); + EnclavePublicKey substitutedKey = new EnclavePublicKey(substitutedKeyBytes); + + + // Act + Action action = () => VirtualizationBasedSecurityEnclaveProviderBase.VerifyEnclavePublicKeyBinding( + testPackage, + substitutedKey); + + // Assert + ArgumentException ex = Assert.Throws(action); + Assert.Equal(Strings.VerifyEnclaveKeyBindingFailed, ex.Message); + } + + // Builds a minimal EnclaveReportPackage whose report EnclaveData begins with the given 32-byte + // binding value. The signature is empty because this targets the binding, not the report signature. + private static EnclaveReportPackage BuildReportPackage(byte[] enclaveDataFirst32) + { + byte[] payload = new byte[EnclaveReportPackageHeaderSize + EnclaveReportSize]; + int offset = 0; + + void WriteUInt(uint value) + { + BitConverter.GetBytes(value).CopyTo(payload, offset); + offset += sizeof(uint); + } + + // EnclaveReportPackageHeader + WriteUInt((uint)payload.Length); // PackageSize + WriteUInt(1); // Version + WriteUInt(1); // SignatureScheme + WriteUInt(EnclaveReportSize); // SignedStatementSize + WriteUInt(0); // SignatureSize + WriteUInt(0); // Reserved + + // EnclaveReport + WriteUInt(EnclaveReportSize); // ReportSize + WriteUInt(1); // ReportVersion + Array.Copy(enclaveDataFirst32, 0, payload, offset, 32); // EnclaveData: first 32 bytes = SHA-256(public key) + + return new EnclaveReportPackage(payload); + } + + private static byte[] Sha256(byte[] data) + { + using SHA256 sha256 = SHA256.Create(); + return sha256.ComputeHash(data); + } +}