Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -150,6 +153,68 @@ internal override void InvalidateEnclaveSession(EnclaveSessionParameters enclave

#region Private helpers

/// <summary>
/// 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
/// <see cref="VerifyAttestationInfo"/> 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.
/// </summary>
/// <param name="enclaveReportPackage">
/// The signature-verified enclave report package. Its <c>Report.EnclaveData</c> supplies the committed
/// key hash.
/// </param>
/// <param name="enclavePublicKey">
/// The enclave public key that will be used to derive the session secret.
/// </param>
/// <exception cref="ArgumentException">
/// Thrown when the report's EnclaveData does not match SHA-256 of <paramref name="enclavePublicKey"/>, or
/// when the required report or key data is missing. In either case attestation is rejected.
/// </exception>
/// <remarks>
/// This is internal to allow for targeted unit testing without resorting to reflection.
/// </remarks>
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<byte> 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)
{
Expand Down Expand Up @@ -241,7 +306,7 @@ private bool AnyCertificatesExpired(X509Certificate2Collection certificates)
/// <summary>
/// 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.
/// </summary>
/// <param name="signingCerts"></param>
Expand Down Expand Up @@ -340,6 +405,26 @@ private void VerifyEnclaveReportSignature(EnclaveReportPackage enclaveReportPack
}
}

#if !NET
// CryptographicOperations.FixedTimeEquals is unavailable on .NET Framework, so hand-roll an equivalent
Comment thread
benrr101 marked this conversation as resolved.
// constant-time comparison of the first <paramref name="length"/> 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)
{
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1968,6 +1968,9 @@
<data name="VerifyEnclaveReportFailed" xml:space="preserve">
<value>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.</value>
</data>
<data name="VerifyEnclaveKeyBindingFailed" xml:space="preserve">
<value>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.</value>
</data>
<data name="VerifyEnclaveReportFormatFailed" xml:space="preserve">
<value>The enclave report received from SQL Server is not in the correct format. Contact Customer Support Services.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
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

/// <summary>
/// A report whose EnclaveData commits to the key used for the session passes the binding check.
/// </summary>
[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);
}

/// <summary>
/// A report whose committed data does not match the session's enclave public key is rejected.
/// </summary>
[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<ArgumentException>(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);
}
}
Loading