-
Notifications
You must be signed in to change notification settings - Fork 314
Performance | Introduce lower-allocation AlwaysEncrypted primitives #3554
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
paulmedynski
merged 8 commits into
dotnet:main
from
edwardneal:perf/alwaysencrypted-primitives
Sep 10, 2025
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a9308cf
Introduce low-allocation AlwaysEncrypted primitives
edwardneal e30ccae
Respond to code review
edwardneal ba8bdea
Subsequent code review response
edwardneal 7defd2f
Typo correction
edwardneal a311495
Review and add exceptions to XML documentation
edwardneal 7990d0a
Merge main
edwardneal cb143aa
Add additional instances where CryptographicException can be thrown
edwardneal dc50d06
Merge branch 'main' into perf/alwaysencrypted-primitives
edwardneal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
162 changes: 162 additions & 0 deletions
162
...ft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
paulmedynski marked this conversation as resolved.
Show resolved
Hide resolved
paulmedynski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
// 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)); | ||
paulmedynski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
enclaveComputationSpan.ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length + masterKeyPath.Length)); | ||
byteCount = Encoding.Unicode.GetByteCount(masterKeyMetadata); | ||
paulmedynski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#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); | ||
paulmedynski marked this conversation as resolved.
Show resolved
Hide resolved
paulmedynski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <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(); | ||
paulmedynski marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.