Skip to content

Commit 3ddd756

Browse files
authored
Performance | Introduce lower-allocation AlwaysEncrypted primitives (#3554)
1 parent 0809419 commit 3ddd756

13 files changed

+567
-17
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ csharp_style_var_for_built_in_types = false:none
4646
csharp_style_var_when_type_is_apparent = false:none
4747
csharp_style_var_elsewhere = false:suggestion
4848

49+
# don't prefer the range operator, netfx doesn't have these types
50+
csharp_style_prefer_range_operator = false
51+
4952
# use language keywords instead of BCL types
5053
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
5154
dotnet_style_predefined_type_for_member_access = true:suggestion

src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@
9999
<Compile Include="$(CommonSourceRoot)\Microsoft\Data\ProviderBase\DbConnectionClosed.cs">
100100
<Link>Microsoft\Data\ProviderBase\DbConnectionClosed.cs</Link>
101101
</Compile>
102+
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs">
103+
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs</Link>
104+
</Compile>
105+
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs">
106+
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs</Link>
107+
</Compile>
102108
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs">
103109
<Link>Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs</Link>
104110
</Compile>

src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,12 @@
288288
<Compile Include="$(CommonSourceRoot)Microsoft\Data\ProviderBase\DbConnectionInternal.cs">
289289
<Link>Microsoft\Data\ProviderBase\DbConnectionInternal.cs</Link>
290290
</Compile>
291+
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs">
292+
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs</Link>
293+
</Compile>
294+
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs">
295+
<Link>Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs</Link>
296+
</Compile>
291297
<Compile Include="$(CommonSourceRoot)Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs">
292298
<Link>Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs</Link>
293299
</Compile>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Diagnostics;
7+
using System.Runtime.CompilerServices;
8+
using System.Security.Cryptography;
9+
using System.Text;
10+
11+
#nullable enable
12+
13+
namespace Microsoft.Data.SqlClient.AlwaysEncrypted;
14+
15+
/// <summary>
16+
/// Represents metadata about the column master key, to be signed or verified by an enclave.
17+
/// </summary>
18+
/// <remarks>
19+
/// This metadata is a lower-case string which is laid out in the following format:
20+
/// <list type="number">
21+
/// <item>
22+
/// Provider name. This is always <see cref="SqlColumnEncryptionCertificateStoreProvider.ProviderName"/>.
23+
/// </item>
24+
/// <item>
25+
/// Master key path. This will be in the format [LocalMachine|CurrentUser]/My/[SHA1 thumbprint].
26+
/// </item>
27+
/// <item>
28+
/// Boolean to indicate whether the CMK supports enclave computations. This is either <c>true</c> or <c>false</c>.
29+
/// </item>
30+
/// </list>
31+
/// <para>
32+
/// This takes ownership of the RSA instance supplied to it, disposing of it when Dispose is called.
33+
/// </para>
34+
/// </remarks>
35+
internal readonly ref struct ColumnMasterKeyMetadata // : IDisposable
36+
{
37+
private static readonly HashAlgorithmName s_hashAlgorithm = HashAlgorithmName.SHA256;
38+
39+
#if NET
40+
[InlineArray(SHA256.HashSizeInBytes)]
41+
private struct Sha256Hash
42+
{
43+
private byte _elementTemplate;
44+
}
45+
46+
private readonly Sha256Hash _hash;
47+
#else
48+
private readonly byte[] _hash;
49+
#endif
50+
private readonly RSA _rsa;
51+
52+
// @TODO: SqlColumnEncryptionCertificateStoreProvider.SignMasterKeyMetadata and .VerifyMasterKeyMetadata should use this type.
53+
/// <summary>
54+
/// Represents metadata associated with a column master key, including its cryptographic hash, path, provider name,
55+
/// and enclave computation settings.
56+
/// </summary>
57+
/// <remarks>
58+
/// This struct is used to encapsulate the metadata required for signing or verifying a column master key. The metadata includes
59+
/// the provider name, the master key path, and whether enclave computations are allowed. The metadata is hashed using SHA-256
60+
/// to ensure integrity.
61+
/// </remarks>
62+
/// <param name="rsa">The RSA cryptographic provider used for signing or verifying the metadata.</param>
63+
/// <param name="masterKeyPath">The path to the column master key. This must be a valid path in one of the following formats:
64+
/// <list type="bullet">
65+
/// <item>[LocalMachine|CurrentUser]/My/[40-character SHA1 thumbprint]</item>
66+
/// <item>My/[40-character SHA1 thumbprint]</item>
67+
/// <item>[40-character SHA1 thumbprint]</item>
68+
/// </list>
69+
/// The path is case-insensitive and will be converted to lowercase for processing.</param>
70+
/// <param name="providerName">The name of the provider associated with the column master key.</param>
71+
/// <param name="allowEnclaveComputations">A value indicating whether enclave computations are allowed for this column master key.</param>
72+
public ColumnMasterKeyMetadata(RSA rsa, string masterKeyPath, string providerName, bool allowEnclaveComputations)
73+
{
74+
// Lay the column master key metadata out in memory. Then, calculate the hash of this metadata ready for signature or verification.
75+
// .NET Core supports Spans in more places, allowing us to allocate on the stack for better performance. It also supports the
76+
// SHA256.HashData method, which saves allocations compared to instantiating a SHA256 object and calling TransformFinalBlock.
77+
78+
// 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:
79+
// * [LocalMachine|CurrentUser]/My/[40 character SHA1 thumbprint]
80+
// * My/[40 character SHA1 thumbprint]
81+
// * [40 character SHA1 thumbprint]
82+
// ProviderName is a constant string of length 23 characters, and allowEnclaveComputations' longest value is 5 characters long. This
83+
// implies a maximum length of 84 characters for the masterKeyMetadata string - and by extension, 168 bytes for the Unicode-encoded
84+
// 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.
85+
// It also implies that when masterKeyPath is converted to its invariant lowercase value, it will be the same length (because it's
86+
// an ASCII string.)
87+
Debug.Assert(masterKeyPath.Length == masterKeyPath.ToLowerInvariant().Length);
88+
89+
ReadOnlySpan<char> enclaveComputationSpan = (allowEnclaveComputations ? bool.TrueString : bool.FalseString).AsSpan();
90+
int masterKeyMetadataLength = providerName.Length + masterKeyPath.Length + enclaveComputationSpan.Length;
91+
int byteCount;
92+
93+
#if NET
94+
const int CharStackAllocationThreshold = 128;
95+
const int ByteStackAllocationThreshold = CharStackAllocationThreshold * sizeof(char);
96+
97+
Span<char> masterKeyMetadata = masterKeyMetadataLength <= CharStackAllocationThreshold
98+
? stackalloc char[CharStackAllocationThreshold].Slice(0, masterKeyMetadataLength)
99+
: new char[masterKeyMetadataLength];
100+
Span<char> masterKeyMetadataSpan = masterKeyMetadata;
101+
#else
102+
char[] masterKeyMetadata = new char[masterKeyMetadataLength];
103+
Span<char> masterKeyMetadataSpan = masterKeyMetadata.AsSpan();
104+
#endif
105+
106+
providerName.AsSpan().ToLowerInvariant(masterKeyMetadataSpan);
107+
masterKeyPath.AsSpan().ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length));
108+
enclaveComputationSpan.ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length + masterKeyPath.Length));
109+
byteCount = Encoding.Unicode.GetByteCount(masterKeyMetadata);
110+
111+
#if NET
112+
Span<byte> masterKeyMetadataBytes = byteCount <= ByteStackAllocationThreshold
113+
? stackalloc byte[ByteStackAllocationThreshold].Slice(0, byteCount)
114+
: new byte[byteCount];
115+
116+
Encoding.Unicode.GetBytes(masterKeyMetadata, masterKeyMetadataBytes);
117+
118+
// Compute hash
119+
SHA256.HashData(masterKeyMetadataBytes, _hash);
120+
#else
121+
byte[] masterKeyMetadataBytes = Encoding.Unicode.GetBytes(masterKeyMetadata);
122+
using (SHA256 sha256 = SHA256.Create())
123+
{
124+
// Compute hash
125+
sha256.TransformFinalBlock(masterKeyMetadataBytes, 0, masterKeyMetadataBytes.Length);
126+
_hash = sha256.Hash;
127+
}
128+
#endif
129+
130+
_rsa = rsa;
131+
}
132+
133+
/// <summary>
134+
/// Signs the current master key metadata using the RSA key associated with this instance.
135+
/// </summary>
136+
/// <returns>
137+
/// A byte array containing the digital signature of the master key metadata.
138+
/// </returns>
139+
/// <exception cref="CryptographicException">Thrown when the signing operation fails.</exception>
140+
public byte[] Sign() =>
141+
_rsa.SignHash(_hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
142+
143+
/// <summary>
144+
/// Verifies the specified master key metadata signature against the computed hash using the RSA key associated with this instance.
145+
/// </summary>
146+
/// <param name="signature">The digital signature to verify. This must be a valid signature generated by <see cref="Sign"/>.</param>
147+
/// <returns>
148+
/// <see langword="true"/> if the signature is valid and matches the computed hash; otherwise, <see langword="false"/>.
149+
/// </returns>
150+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="signature"/> is <see langword="null"/>.</exception>"
151+
public bool Verify(byte[] signature) =>
152+
_rsa.VerifyHash(_hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
153+
154+
/// <summary>
155+
/// Releases all resources used by this <see cref="ColumnMasterKeyMetadata"/>.
156+
/// </summary>
157+
/// <remarks>
158+
/// This method disposes the <see cref="RSA"/> instance used to construct this <see cref="ColumnMasterKeyMetadata" /> instance.
159+
/// </remarks>
160+
public void Dispose() =>
161+
_rsa.Dispose();
162+
}

0 commit comments

Comments
 (0)