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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ csharp_style_var_for_built_in_types = false:none
csharp_style_var_when_type_is_apparent = false:none
csharp_style_var_elsewhere = false:suggestion

# don't prefer the range operator, netfx doesn't have these types
csharp_style_prefer_range_operator = false

# use language keywords instead of BCL types
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@
<Compile Include="$(CommonSourceRoot)\Microsoft\Data\ProviderBase\DbConnectionClosed.cs">
<Link>Microsoft\Data\ProviderBase\DbConnectionClosed.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs">
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs">
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs">
<Link>Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@
<Compile Include="$(CommonSourceRoot)Microsoft\Data\ProviderBase\DbConnectionInternal.cs">
<Link>Microsoft\Data\ProviderBase\DbConnectionInternal.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs">
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs">
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs</Link>
</Compile>
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs">
<Link>Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;

#nullable enable

namespace Microsoft.Data.SqlClient.AlwaysEncrypted;

/// <summary>
/// Represents metadata about the column master key, to be signed or verified by an enclave.
/// </summary>
/// <remarks>
/// This metadata is a lower-case string which is laid out in the following format:
/// <list type="number">
/// <item>
/// Provider name. This is always <see cref="SqlColumnEncryptionCertificateStoreProvider.ProviderName"/>.
/// </item>
/// <item>
/// Master key path. This will be in the format [LocalMachine|CurrentUser]/My/[SHA1 thumbprint].
/// </item>
/// <item>
/// Boolean to indicate whether the CMK supports enclave computations. This is either <c>true</c> or <c>false</c>.
/// </item>
/// </list>
/// <para>
/// This takes ownership of the RSA instance supplied to it, disposing of it when Dispose is called.
/// </para>
/// </remarks>
internal readonly ref struct ColumnMasterKeyMetadata // : IDisposable
{
private static readonly HashAlgorithmName s_hashAlgorithm = HashAlgorithmName.SHA256;

#if NET
[InlineArray(SHA256.HashSizeInBytes)]
private struct Sha256Hash
{
private byte _elementTemplate;
}

private readonly Sha256Hash _hash;
#else
private readonly byte[] _hash;
#endif
private readonly RSA _rsa;

// @TODO: SqlColumnEncryptionCertificateStoreProvider.SignMasterKeyMetadata and .VerifyMasterKeyMetadata should use this type.
/// <summary>
/// Represents metadata associated with a column master key, including its cryptographic hash, path, provider name,
/// and enclave computation settings.
/// </summary>
/// <remarks>
/// This struct is used to encapsulate the metadata required for signing or verifying a column master key. The metadata includes
/// the provider name, the master key path, and whether enclave computations are allowed. The metadata is hashed using SHA-256
/// to ensure integrity.
/// </remarks>
/// <param name="rsa">The RSA cryptographic provider used for signing or verifying the metadata.</param>
/// <param name="masterKeyPath">The path to the column master key. This must be a valid path in one of the following formats:
/// <list type="bullet">
/// <item>[LocalMachine|CurrentUser]/My/[40-character SHA1 thumbprint]</item>
/// <item>My/[40-character SHA1 thumbprint]</item>
/// <item>[40-character SHA1 thumbprint]</item>
/// </list>
/// The path is case-insensitive and will be converted to lowercase for processing.</param>
/// <param name="providerName">The name of the provider associated with the column master key.</param>
/// <param name="allowEnclaveComputations">A value indicating whether enclave computations are allowed for this column master key.</param>
public ColumnMasterKeyMetadata(RSA rsa, string masterKeyPath, string providerName, bool allowEnclaveComputations)
{
// Lay the column master key metadata out in memory. Then, calculate the hash of this metadata ready for signature or verification.
// .NET Core supports Spans in more places, allowing us to allocate on the stack for better performance. It also supports the
// SHA256.HashData method, which saves allocations compared to instantiating a SHA256 object and calling TransformFinalBlock.

// By this point, we know that we have a valid certificate, so the path is valid. The longest valid masterKeyPath is in one of the formats:
// * [LocalMachine|CurrentUser]/My/[40 character SHA1 thumbprint]
// * My/[40 character SHA1 thumbprint]
// * [40 character SHA1 thumbprint]
// ProviderName is a constant string of length 23 characters, and allowEnclaveComputations' longest value is 5 characters long. This
// implies a maximum length of 84 characters for the masterKeyMetadata string - and by extension, 168 bytes for the Unicode-encoded
// byte array. This is small enough to allocate on the stack, but we fall back to allocating a new char/byte array in case those assumptions fail.
// It also implies that when masterKeyPath is converted to its invariant lowercase value, it will be the same length (because it's
// an ASCII string.)
Debug.Assert(masterKeyPath.Length == masterKeyPath.ToLowerInvariant().Length);

ReadOnlySpan<char> enclaveComputationSpan = (allowEnclaveComputations ? bool.TrueString : bool.FalseString).AsSpan();
int masterKeyMetadataLength = providerName.Length + masterKeyPath.Length + enclaveComputationSpan.Length;
int byteCount;

#if NET
const int CharStackAllocationThreshold = 128;
const int ByteStackAllocationThreshold = CharStackAllocationThreshold * sizeof(char);

Span<char> masterKeyMetadata = masterKeyMetadataLength <= CharStackAllocationThreshold
? stackalloc char[CharStackAllocationThreshold].Slice(0, masterKeyMetadataLength)
: new char[masterKeyMetadataLength];
Span<char> masterKeyMetadataSpan = masterKeyMetadata;
#else
char[] masterKeyMetadata = new char[masterKeyMetadataLength];
Span<char> masterKeyMetadataSpan = masterKeyMetadata.AsSpan();
#endif

providerName.AsSpan().ToLowerInvariant(masterKeyMetadataSpan);
masterKeyPath.AsSpan().ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length));
Comment on lines +106 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the characters in the input variable restricted such that it is certain that the output lower invariant replacement version is certain to be of exactly the same length?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, indirectly. This is due to be used by the certificate store provider, and the constructor takes an RSA instance. The only way to get this instance is by using the master key path to select a valid certificate, and the only valid master key paths are ASCII text in a few formats:

  • [LocalMachine|CurrentUser]/My/[40 character SHA1 thumbprint]
  • My/[40 character SHA1 thumbprint]
  • [40 character SHA1 thumbprint]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent. It might be worth leaving a comment in the code somewhere so anyone who reads the code in future knows that there are input restrictions.

enclaveComputationSpan.ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length + masterKeyPath.Length));
byteCount = Encoding.Unicode.GetByteCount(masterKeyMetadata);

#if NET
Span<byte> masterKeyMetadataBytes = byteCount <= ByteStackAllocationThreshold
? stackalloc byte[ByteStackAllocationThreshold].Slice(0, byteCount)
: new byte[byteCount];

Encoding.Unicode.GetBytes(masterKeyMetadata, masterKeyMetadataBytes);

// Compute hash
SHA256.HashData(masterKeyMetadataBytes, _hash);
#else
byte[] masterKeyMetadataBytes = Encoding.Unicode.GetBytes(masterKeyMetadata);
using (SHA256 sha256 = SHA256.Create())
{
// Compute hash
sha256.TransformFinalBlock(masterKeyMetadataBytes, 0, masterKeyMetadataBytes.Length);
_hash = sha256.Hash;
}
#endif

_rsa = rsa;
}

/// <summary>
/// Signs the current master key metadata using the RSA key associated with this instance.
/// </summary>
/// <returns>
/// A byte array containing the digital signature of the master key metadata.
/// </returns>
/// <exception cref="CryptographicException">Thrown when the signing operation fails.</exception>
public byte[] Sign() =>
_rsa.SignHash(_hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1);

/// <summary>
/// Verifies the specified master key metadata signature against the computed hash using the RSA key associated with this instance.
/// </summary>
/// <param name="signature">The digital signature to verify. This must be a valid signature generated by <see cref="Sign"/>.</param>
/// <returns>
/// <see langword="true"/> if the signature is valid and matches the computed hash; otherwise, <see langword="false"/>.
/// </returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="signature"/> is <see langword="null"/>.</exception>"
public bool Verify(byte[] signature) =>
_rsa.VerifyHash(_hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1);

/// <summary>
/// Releases all resources used by this <see cref="ColumnMasterKeyMetadata"/>.
/// </summary>
/// <remarks>
/// This method disposes the <see cref="RSA"/> instance used to construct this <see cref="ColumnMasterKeyMetadata" /> instance.
/// </remarks>
public void Dispose() =>
_rsa.Dispose();
}
Loading
Loading