From a9308cf7e82e53b1eaece977ad76483759eac88e Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Thu, 7 Aug 2025 20:37:15 +0100
Subject: [PATCH 1/6] Introduce low-allocation AlwaysEncrypted primitives
---
.editorconfig | 3 +
.../src/Microsoft.Data.SqlClient.csproj | 6 +
.../netfx/src/Microsoft.Data.SqlClient.csproj | 6 +
.../ColumnMasterKeyMetadata.cs | 114 +++++++++
.../EncryptedColumnEncryptionKeyParameters.cs | 220 ++++++++++++++++++
...ryptionCertificateStoreProvider.Windows.cs | 14 +-
...onCertificateStoreProvider.netcore.Unix.cs | 10 +
.../SqlColumnEncryptionCngProvider.Windows.cs | 12 +-
...olumnEncryptionCngProvider.netcore.Unix.cs | 10 +
.../SqlColumnEncryptionCspProvider.Windows.cs | 12 +-
...olumnEncryptionCspProvider.netcore.Unix.cs | 10 +
.../src/Microsoft/Data/SqlClient/SqlUtil.cs | 27 ++-
.../src/Resources/Strings.Designer.cs | 39 +++-
.../src/Resources/Strings.resx | 11 +-
14 files changed, 477 insertions(+), 17 deletions(-)
create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
diff --git a/.editorconfig b/.editorconfig
index f0ea20ec32..a17e1b47d9 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -43,6 +43,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
diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
index d1dd14f6b7..10889a69c9 100644
--- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj
@@ -99,6 +99,12 @@
Microsoft\Data\ProviderBase\DbConnectionClosed.cs
+
+ Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs
+
+
+ Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs
+
Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs
diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
index 0c2fe65f93..4f519e1aff 100644
--- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
+++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj
@@ -288,6 +288,12 @@
Microsoft\Data\ProviderBase\DbConnectionInternal.cs
+
+ Microsoft\Data\SqlClient\AlwaysEncrypted\ColumnMasterKeyMetadata.cs
+
+
+ Microsoft\Data\SqlClient\AlwaysEncrypted\EncryptedColumnEncryptionKeyParameters.cs
+
Microsoft\Data\SqlClient\ConnectionPool\ChannelDbConnectionPool.cs
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
new file mode 100644
index 0000000000..182748b783
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
@@ -0,0 +1,114 @@
+// 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.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient.AlwaysEncrypted;
+
+///
+/// Represents metadata about the column master key, to be signed or verified by an enclave.
+///
+///
+/// This metadata is a lower-case string which is laid out in the following format:
+///
+///
+/// Provider name. This always .
+///
+///
+/// Master key path. This will be in the format [LocalMachine|CurrentUser]/My/[SHA1 thumbprint].
+///
+///
+/// Boolean to indicate whether the CMK supports enclave computations. This is either true or false.
+///
+///
+///
+/// This takes ownership of the RSA instance supplied to it, disposing of it when Dispose is called.
+///
+///
+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.
+ 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 the format:
+ // [LocalMachine|CurrentUser]/My/[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.
+ ReadOnlySpan 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 masterKeyMetadata = masterKeyMetadataLength <= CharStackAllocationThreshold
+ ? stackalloc char[CharStackAllocationThreshold].Slice(0, masterKeyMetadataLength)
+ : new char[masterKeyMetadataLength];
+ Span masterKeyMetadataSpan = masterKeyMetadata;
+#else
+ char[] masterKeyMetadata = new char[masterKeyMetadataLength];
+ Span masterKeyMetadataSpan = masterKeyMetadata.AsSpan();
+#endif
+
+ providerName.AsSpan().ToLowerInvariant(masterKeyMetadataSpan);
+ masterKeyPath.AsSpan().ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length));
+ enclaveComputationSpan.ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length + masterKeyPath.Length));
+ byteCount = Encoding.Unicode.GetByteCount(masterKeyMetadata);
+
+#if NET
+ Span 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;
+ }
+
+ public byte[] Sign() =>
+ _rsa.SignHash(_hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
+
+ public bool Verify(byte[] signature) =>
+ _rsa.VerifyHash(_hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
+
+ public void Dispose() =>
+ _rsa.Dispose();
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
new file mode 100644
index 0000000000..a69f1e6b4b
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
@@ -0,0 +1,220 @@
+// 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.Buffers.Binary;
+using System.Diagnostics;
+using System.Security.Cryptography;
+using System.Text;
+
+#nullable enable
+
+namespace Microsoft.Data.SqlClient.AlwaysEncrypted;
+
+///
+/// Represents the parameters used to construct an encrypted column encryption key, used to encrypt and decrypt data in SQL Server Always Encrypted columns.
+///
+///
+/// An encrypted CEK is a byte array that contains the following structure:
+///
+///
+/// Version: 1 byte, always 0x01
+///
+///
+/// Key path length: 2 bytes, length of the key path in bytes
+///
+///
+/// Ciphertext length: 2 bytes, length of the ciphertext in bytes
+///
+///
+/// Key path: variable length, Unicode-encoded string representing the key path
+///
+///
+/// Ciphertext: variable length, encrypted data. Length determined by size of the RSA key used for encryption
+///
+///
+/// Signature: variable length, digital signature of the encrypted CEK's SHA256 hash. Length determined by the size of the RSA key used for signing
+///
+///
+///
+/// This takes ownership of the RSA instance supplied to it, disposing of it when Dispose is called.
+///
+///
+internal readonly ref struct EncryptedColumnEncryptionKeyParameters // : IDisposable
+{
+ private const byte AlgorithmVersion = 0x01;
+
+ private const int AlgorithmOffset = 0;
+ private const int KeyPathLengthOffset = AlgorithmOffset + sizeof(byte);
+ private const int CiphertextLengthOffset = KeyPathLengthOffset + sizeof(ushort);
+ private const int KeyPathOffset = CiphertextLengthOffset + sizeof(ushort);
+
+#if NET
+ private const int HashSize = SHA256.HashSizeInBytes;
+#else
+ private const int HashSize = 32;
+#endif
+
+ private static readonly HashAlgorithmName s_hashAlgorithm = HashAlgorithmName.SHA256;
+
+ private readonly RSA _rsa;
+ private readonly int _rsaKeySize;
+ private readonly string _keyPath;
+ private readonly string _keyType;
+ private readonly string _keyPathReference;
+
+ // @TODO: SqlColumnEncryptionCertificateStoreProvider, SqlColumnEncryptionCngProvider and SqlColumnEncryptionCspProvider should use this type.
+ public EncryptedColumnEncryptionKeyParameters(RSA rsa, string keyPath, string keyType, string keyPathReference)
+ {
+ _rsa = rsa;
+ _rsaKeySize = rsa.KeySize / 8;
+ _keyPath = keyPath;
+
+ Debug.Assert(keyType is SqlColumnEncryptionCertificateStoreProvider.MasterKeyType
+ or SqlColumnEncryptionCngProvider.MasterKeyType or SqlColumnEncryptionCspProvider.MasterKeyType);
+ Debug.Assert(keyPathReference is SqlColumnEncryptionCertificateStoreProvider.KeyPathReference
+ or SqlColumnEncryptionCngProvider.KeyPathReference or SqlColumnEncryptionCspProvider.KeyPathReference);
+ _keyType = keyType;
+ _keyPathReference = keyPathReference;
+ }
+
+ public byte[] Encrypt(byte[] columnEncryptionKey)
+ {
+ ushort keyPathSize = (ushort)Encoding.Unicode.GetByteCount(_keyPath);
+ int cekSize = sizeof(byte) + sizeof(ushort) + sizeof(ushort) + keyPathSize + _rsaKeySize + _rsaKeySize;
+ byte[] encryptedColumnEncryptionKey = new byte[cekSize];
+ int bytesWritten;
+ int cipherTextOffset = KeyPathOffset + keyPathSize;
+ int signatureOffset = cipherTextOffset + _rsaKeySize;
+
+ // We currently only support one version
+ encryptedColumnEncryptionKey[AlgorithmOffset] = AlgorithmVersion;
+
+ // Write the key path length and the ciphertext length
+ BinaryPrimitives.WriteUInt16LittleEndian(encryptedColumnEncryptionKey.AsSpan(KeyPathLengthOffset), keyPathSize);
+ BinaryPrimitives.WriteUInt16LittleEndian(encryptedColumnEncryptionKey.AsSpan(CiphertextLengthOffset), (ushort)_rsaKeySize);
+
+ // Write the unicode encoded bytes of the key path
+ bytesWritten = Encoding.Unicode.GetBytes(_keyPath, 0, _keyPath.Length, encryptedColumnEncryptionKey, KeyPathOffset);
+ Debug.Assert(bytesWritten == keyPathSize, @"Key path length does not match the expected length.");
+
+ // Encrypt the column encryption key using RSA with OAEP padding.
+ // In .NET Core, we can encrypt directly into the byte array, while in .NET Framework we need to allocate an intermediary and copy.
+#if NET
+ // CodeQL [SM03796] Required for an external standard: Always Encrypted only supports encrypting column encryption keys with RSA_OAEP(SHA1) (https://learn.microsoft.com/en-us/sql/t-sql/statements/create-column-encryption-key-transact-sql?view=sql-server-ver16)
+ bytesWritten = _rsa.Encrypt(columnEncryptionKey, encryptedColumnEncryptionKey.AsSpan(cipherTextOffset), RSAEncryptionPadding.OaepSHA1);
+#else
+ // CodeQL [SM03796] Required for an external standard: Always Encrypted only supports encrypting column encryption keys with RSA_OAEP(SHA1) (https://learn.microsoft.com/en-us/sql/t-sql/statements/create-column-encryption-key-transact-sql?view=sql-server-ver16)
+ byte[] cipherText = _rsa.Encrypt(columnEncryptionKey, RSAEncryptionPadding.OaepSHA1);
+ bytesWritten = cipherText.Length;
+
+ Buffer.BlockCopy(cipherText, 0, encryptedColumnEncryptionKey, cipherTextOffset, bytesWritten);
+#endif
+ Debug.Assert(bytesWritten == _rsaKeySize, @"Ciphertext length does not match the RSA key size.");
+
+ // Compute the SHA256 hash of the encrypted CEK, (up to this point) then sign it and write the signature
+ // In .NET Core, we can use a stack-allocated span for the hash, while in .NET Framework we need to allocate a byte array.
+#if NET
+ Span hash = stackalloc byte[HashSize];
+ bytesWritten = SHA256.HashData(encryptedColumnEncryptionKey.AsSpan(0, signatureOffset), hash);
+ Debug.Assert(bytesWritten == HashSize, @"Hash size does not match the expected size.");
+
+ bytesWritten = _keyType == SqlColumnEncryptionCertificateStoreProvider.MasterKeyType
+ ? _rsa.SignHash(hash, encryptedColumnEncryptionKey.AsSpan(signatureOffset), s_hashAlgorithm, RSASignaturePadding.Pkcs1)
+ : _rsa.SignData(hash, encryptedColumnEncryptionKey.AsSpan(signatureOffset), s_hashAlgorithm, RSASignaturePadding.Pkcs1);
+ Debug.Assert(bytesWritten == _rsaKeySize, @"Signature length does not match the RSA key size.");
+
+#else
+ byte[] hash;
+ using (SHA256 sha256 = SHA256.Create())
+ {
+ sha256.TransformFinalBlock(encryptedColumnEncryptionKey, 0, signatureOffset);
+ hash = sha256.Hash;
+ }
+ bytesWritten = hash.Length;
+ Debug.Assert(bytesWritten == HashSize, @"Hash size does not match the expected size.");
+
+ byte[] signedHash = _keyType == SqlColumnEncryptionCertificateStoreProvider.MasterKeyType
+ ? _rsa.SignHash(hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1)
+ : _rsa.SignData(hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
+ bytesWritten = signedHash.Length;
+ Debug.Assert(bytesWritten == _rsaKeySize, @"Signature length does not match the RSA key size.");
+
+ Buffer.BlockCopy(signedHash, 0, encryptedColumnEncryptionKey, signatureOffset, bytesWritten);
+#endif
+
+ return encryptedColumnEncryptionKey;
+ }
+
+ public byte[] Decrypt(byte[] encryptedCek)
+ {
+ // Validate the version byte
+ if (encryptedCek[0] != AlgorithmVersion)
+ {
+ throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedCek[0], AlgorithmVersion);
+ }
+
+ // Get key path length, but skip reading it. It exists only for troubleshooting purposes and doesn't need validation.
+ ushort keyPathLength = BinaryPrimitives.ReadUInt16LittleEndian(encryptedCek.AsSpan(KeyPathLengthOffset));
+
+ // Get ciphertext length, then validate it against the RSA key size
+ ushort cipherTextLength = BinaryPrimitives.ReadUInt16LittleEndian(encryptedCek.AsSpan(CiphertextLengthOffset));
+
+ if (cipherTextLength != _rsaKeySize)
+ {
+ throw SQL.InvalidCiphertextLengthInEncryptedCEK(_keyType, _keyPathReference, cipherTextLength, _rsaKeySize, _keyPath);
+ }
+
+ // Validate the signature length
+ int cipherTextOffset = KeyPathOffset + keyPathLength;
+ int signatureOffset = cipherTextOffset + cipherTextLength;
+ int signatureLength = encryptedCek.Length - signatureOffset;
+
+ if (signatureLength != _rsaKeySize)
+ {
+ throw SQL.InvalidSignatureInEncryptedCEK(_keyType, _keyPathReference, signatureLength, _rsaKeySize, _keyPath);
+ }
+
+ // Get the ciphertext and signature, then calculate the hash of the encrypted CEK.
+ // In .NET Core most of these operations can be done with spans, while in .NET Framework we need to allocate byte arrays.
+#if NET
+ Span cipherText = encryptedCek.AsSpan(cipherTextOffset, cipherTextLength);
+ Span signature = encryptedCek.AsSpan(signatureOffset);
+
+ Span hash = stackalloc byte[HashSize];
+ SHA256.HashData(encryptedCek.AsSpan(0, signatureOffset), hash);
+#else
+ byte[] cipherText = new byte[cipherTextLength];
+ Buffer.BlockCopy(encryptedCek, cipherTextOffset, cipherText, 0, cipherText.Length);
+
+ byte[] signature = new byte[signatureLength];
+ Buffer.BlockCopy(encryptedCek, signatureOffset, signature, 0, signature.Length);
+
+ byte[] hash;
+ using (SHA256 sha256 = SHA256.Create())
+ {
+ sha256.TransformFinalBlock(encryptedCek, 0, signatureOffset);
+ hash = sha256.Hash;
+ }
+ Debug.Assert(hash.Length == HashSize, @"hash length should be same as the signature length while decrypting encrypted column encryption key.");
+#endif
+
+ bool dataVerified = _keyType == SqlColumnEncryptionCertificateStoreProvider.MasterKeyType
+ ? _rsa.VerifyHash(hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1)
+ : _rsa.VerifyData(hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
+
+ // Validate the signature
+ if (!dataVerified)
+ {
+ throw SQL.InvalidSignature(_keyPath, _keyType);
+ }
+
+ // Decrypt the CEK
+ // CodeQL [SM03796] Required for an external standard: Always Encrypted only supports encrypting column encryption keys with RSA_OAEP(SHA1) (https://learn.microsoft.com/en-us/sql/t-sql/statements/create-column-encryption-key-transact-sql?view=sql-server-ver16)
+ return _rsa.Decrypt(cipherText, RSAEncryptionPadding.OaepSHA1);
+ }
+
+ public void Dispose() =>
+ _rsa.Dispose();
+}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.Windows.cs
index 9960447465..21dcd534e6 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.Windows.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.Windows.cs
@@ -21,6 +21,16 @@ public class SqlColumnEncryptionCertificateStoreProvider : SqlColumnEncryptionKe
///
public const string ProviderName = @"MSSQL_CERTIFICATE_STORE";
+ ///
+ /// This encryption keystore uses a certificate as the column master key.
+ ///
+ internal const string MasterKeyType = @"certificate";
+
+ ///
+ /// This encryption keystore uses the master key path to reference a specific certificate.
+ ///
+ internal const string KeyPathReference = @"certificate";
+
///
/// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys.
///
@@ -107,7 +117,7 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e
// validate the ciphertext length
if (cipherTextLength != keySizeInBytes)
{
- throw SQL.InvalidCiphertextLengthInEncryptedCEK(cipherTextLength, keySizeInBytes, masterKeyPath);
+ throw SQL.InvalidCiphertextLengthInEncryptedCEKCertificate(cipherTextLength, keySizeInBytes, masterKeyPath);
}
// Validate the signature length
@@ -115,7 +125,7 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e
int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength;
if (signatureLength != keySizeInBytes)
{
- throw SQL.InvalidSignatureInEncryptedCEK(signatureLength, keySizeInBytes, masterKeyPath);
+ throw SQL.InvalidSignatureInEncryptedCEKCertificate(signatureLength, keySizeInBytes, masterKeyPath);
}
// Get ciphertext
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.netcore.Unix.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.netcore.Unix.cs
index 598385851c..0b5ba9ea36 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.netcore.Unix.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCertificateStoreProvider.netcore.Unix.cs
@@ -16,6 +16,16 @@ public class SqlColumnEncryptionCertificateStoreProvider : SqlColumnEncryptionKe
///
public const string ProviderName = @"MSSQL_CERTIFICATE_STORE";
+ ///
+ /// This encryption keystore uses a certificate as the column master key.
+ ///
+ internal const string MasterKeyType = @"certificate";
+
+ ///
+ /// This encryption keystore uses the master key path to reference a specific certificate.
+ ///
+ internal const string KeyPathReference = @"certificate";
+
///
/// This function uses a certificate specified by the key path
/// and decrypts an encrypted CEK with RSA encryption algorithm.
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs
index c47173c33c..2a6c86357c 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.Windows.cs
@@ -15,6 +15,16 @@ public class SqlColumnEncryptionCngProvider : SqlColumnEncryptionKeyStoreProvide
///
public const string ProviderName = @"MSSQL_CNG_STORE";
+ ///
+ /// This encryption keystore uses an asymmetric key as the column master key.
+ ///
+ internal const string MasterKeyType = @"asymmetric key";
+
+ ///
+ /// This encryption keystore uses the master key path to reference a CNG provider.
+ ///
+ internal const string KeyPathReference = @"Microsoft Cryptography API: Next Generation (CNG) provider";
+
///
/// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys using this provider.
/// For now, we are keeping all the providers in sync.
@@ -113,7 +123,7 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e
// Validate the signature
if (!RSAVerifySignature(hash, signature, rsaCngProvider))
{
- throw SQL.InvalidSignature(masterKeyPath);
+ throw SQL.InvalidAsymmetricKeySignature(masterKeyPath);
}
// Decrypt the CEK
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.netcore.Unix.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.netcore.Unix.cs
index 038b43df2d..5f75192562 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.netcore.Unix.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCngProvider.netcore.Unix.cs
@@ -28,6 +28,16 @@ public class SqlColumnEncryptionCngProvider : SqlColumnEncryptionKeyStoreProvide
///
public const string ProviderName = @"MSSQL_CNG_STORE";
+ ///
+ /// This encryption keystore uses an asymmetric key as the column master key.
+ ///
+ internal const string MasterKeyType = @"asymmetric key";
+
+ ///
+ /// This encryption keystore uses the master key path to reference a CNG provider.
+ ///
+ internal const string KeyPathReference = @"Microsoft Cryptography API: Next Generation (CNG) provider";
+
///
/// This function uses the asymmetric key specified by the key path
/// and decrypts an encrypted CEK with RSA encryption algorithm.
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.Windows.cs
index 6a5227d039..db659a1784 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.Windows.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.Windows.cs
@@ -18,6 +18,16 @@ public class SqlColumnEncryptionCspProvider : SqlColumnEncryptionKeyStoreProvide
///
public const string ProviderName = @"MSSQL_CSP_PROVIDER";
+ ///
+ /// This encryption keystore uses an asymmetric key as the column master key.
+ ///
+ internal const string MasterKeyType = @"asymmetric key";
+
+ ///
+ /// This encryption keystore uses the master key path to reference a CSP.
+ ///
+ internal const string KeyPathReference = @"Microsoft Cryptographic Service Provider (CSP)";
+
///
/// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys using this provider.
/// For now, we are keeping all the providers in sync.
@@ -121,7 +131,7 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e
// Validate the signature
if (!RSAVerifySignature(hash, signature, rsaProvider))
{
- throw SQL.InvalidSignature(masterKeyPath);
+ throw SQL.InvalidAsymmetricKeySignature(masterKeyPath);
}
// Decrypt the CEK
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.netcore.Unix.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.netcore.Unix.cs
index 5fe9980788..4e2836c020 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.netcore.Unix.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionCspProvider.netcore.Unix.cs
@@ -28,6 +28,16 @@ public class SqlColumnEncryptionCspProvider : SqlColumnEncryptionKeyStoreProvide
///
public const string ProviderName = @"MSSQL_CSP_PROVIDER";
+ ///
+ /// This encryption keystore uses an asymmetric key as the column master key.
+ ///
+ internal const string MasterKeyType = @"asymmetric key";
+
+ ///
+ /// This encryption keystore uses the master key path to reference a CSP.
+ ///
+ internal const string KeyPathReference = @"Microsoft Cryptographic Service Provider (CSP)";
+
///
/// This function uses the asymmetric key specified by the key path
/// and decrypts an encrypted CEK with RSA encryption algorithm.
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
index 94df331f22..8524250365 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs
@@ -1816,9 +1816,14 @@ internal static Exception InvalidAlgorithmVersionInEncryptedCEK(byte actual, byt
return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidAlgorithmVersionInEncryptedCEK, actual.ToString(@"X2"), expected.ToString(@"X2")), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
}
- internal static Exception InvalidCiphertextLengthInEncryptedCEK(int actual, int expected, string certificateName)
+ internal static Exception InvalidCiphertextLengthInEncryptedCEK(string keyType, string keyPathReference, int actual, int expected, string masterKeyPath)
{
- return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidCiphertextLengthInEncryptedCEK, actual, expected, certificateName), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
+ return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidCiphertextLengthInEncryptedCEK, actual, expected, keyType, masterKeyPath, keyPathReference), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
+ }
+
+ internal static Exception InvalidCiphertextLengthInEncryptedCEKCertificate(int actual, int expected, string certificateName)
+ {
+ return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidCiphertextLengthInEncryptedCEKCertificate, actual, expected, certificateName), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
}
internal static Exception InvalidCiphertextLengthInEncryptedCEKCsp(int actual, int expected, string masterKeyPath)
@@ -1831,9 +1836,14 @@ internal static Exception InvalidCiphertextLengthInEncryptedCEKCng(int actual, i
return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidCiphertextLengthInEncryptedCEKCng, actual, expected, masterKeyPath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
}
- internal static Exception InvalidSignatureInEncryptedCEK(int actual, int expected, string masterKeyPath)
+ internal static Exception InvalidSignatureInEncryptedCEK(string keyType, string keyPathReference, int actual, int expected, string masterKeyPath)
{
- return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidSignatureInEncryptedCEK, actual, expected, masterKeyPath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
+ return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidSignatureInEncryptedCEK, actual, expected, keyType, masterKeyPath, keyPathReference), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
+ }
+
+ internal static Exception InvalidSignatureInEncryptedCEKCertificate(int actual, int expected, string masterKeyPath)
+ {
+ return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidSignatureInEncryptedCEKCertificate, actual, expected, masterKeyPath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
}
internal static Exception InvalidSignatureInEncryptedCEKCsp(int actual, int expected, string masterKeyPath)
@@ -1846,14 +1856,19 @@ internal static Exception InvalidSignatureInEncryptedCEKCng(int actual, int expe
return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidSignatureInEncryptedCEKCng, actual, expected, masterKeyPath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
}
+ internal static Exception InvalidSignature(string masterKeyPath, string keyType)
+ {
+ return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidSignature, keyType, masterKeyPath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
+ }
+
internal static Exception InvalidCertificateSignature(string certificatePath)
{
return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidCertificateSignature, certificatePath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
}
- internal static Exception InvalidSignature(string masterKeyPath)
+ internal static Exception InvalidAsymmetricKeySignature(string masterKeyPath)
{
- return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidSignature, masterKeyPath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
+ return ADP.Argument(StringsHelper.GetString(Strings.TCE_InvalidAsymmetricKeySignature, masterKeyPath), TdsEnums.TCE_PARAM_ENCRYPTED_CEK);
}
internal static Exception CertificateWithNoPrivateKey(string keyPath, bool isSystemOp)
diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
index dc8684f1f3..695217f13d 100644
--- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
+++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.Designer.cs
@@ -12639,15 +12639,24 @@ internal static string TCE_InvalidCertificateStoreSysErr {
return ResourceManager.GetString("TCE_InvalidCertificateStoreSysErr", resourceCulture);
}
}
-
+
///
- /// Looks up a localized string similar to The specified encrypted column encryption key's ciphertext length: {0} does not match the ciphertext length: {1} when using column master key (certificate) in '{2}'. The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect..
+ /// Looks up a localized string similar to The specified encrypted column encryption key's ciphertext length: {0} does not match the ciphertext length: {1} when using column master key ({2}) in '{3}'. The encrypted column encryption key may be corrupt, or the specified {4} path may be incorrect..
///
internal static string TCE_InvalidCiphertextLengthInEncryptedCEK {
get {
return ResourceManager.GetString("TCE_InvalidCiphertextLengthInEncryptedCEK", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to The specified encrypted column encryption key's ciphertext length: {0} does not match the ciphertext length: {1} when using column master key (certificate) in '{2}'. The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect..
+ ///
+ internal static string TCE_InvalidCiphertextLengthInEncryptedCEKCertificate {
+ get {
+ return ResourceManager.GetString("TCE_InvalidCiphertextLengthInEncryptedCEKCertificate", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to The specified encrypted column encryption key's ciphertext length: {0} does not match the ciphertext length: {1} when using column master key (asymmetric key) in '{2}'. The encrypted column encryption key may be corrupt, or the specified Microsoft Cryptography API: Next Generation (CNG) provider path may be incorrect..
@@ -12855,24 +12864,42 @@ internal static string TCE_InvalidKeyStoreProviderName {
return ResourceManager.GetString("TCE_InvalidKeyStoreProviderName", resourceCulture);
}
}
-
+
///
- /// Looks up a localized string similar to The specified encrypted column encryption key signature does not match the signature computed with the column master key (asymmetric key) in '{0}'. The encrypted column encryption key may be corrupt, or the specified path may be incorrect..
+ /// Looks up a localized string similar to The specified encrypted column encryption key signature does not match the signature computed with the column master key ({0}) in '{1}'. The encrypted column encryption key may be corrupt, or the specified path may be incorrect..
///
internal static string TCE_InvalidSignature {
get {
return ResourceManager.GetString("TCE_InvalidSignature", resourceCulture);
}
}
-
+
///
- /// Looks up a localized string similar to The specified encrypted column encryption key's signature length: {0} does not match the signature length: {1} when using column master key (certificate) in '{2}'. The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect..
+ /// Looks up a localized string similar to The specified encrypted column encryption key signature does not match the signature computed with the column master key (asymmetric key) in '{0}'. The encrypted column encryption key may be corrupt, or the specified path may be incorrect..
+ ///
+ internal static string TCE_InvalidAsymmetricKeySignature {
+ get {
+ return ResourceManager.GetString("TCE_InvalidAsymmetricKeySignature", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The specified encrypted column encryption key's signature length: {0} does not match the signature length: {1} when using column master key ({2}) in '{3}'. The encrypted column encryption key may be corrupt, or the specified {4} path may be incorrect..
///
internal static string TCE_InvalidSignatureInEncryptedCEK {
get {
return ResourceManager.GetString("TCE_InvalidSignatureInEncryptedCEK", resourceCulture);
}
}
+
+ ///
+ /// Looks up a localized string similar to The specified encrypted column encryption key's signature length: {0} does not match the signature length: {1} when using column master key (certificate) in '{2}'. The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect..
+ ///
+ internal static string TCE_InvalidSignatureInEncryptedCEKCertificate {
+ get {
+ return ResourceManager.GetString("TCE_InvalidSignatureInEncryptedCEKCertificate", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to The specified encrypted column encryption key's signature length: {0} does not match the signature length: {1} when using column master key (asymmetric key) in '{2}'. The encrypted column encryption key may be corrupt, or the specified Microsoft Cryptography API: Next Generation (CNG) provider path may be incorrect..
diff --git a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
index 8d59dd7ae4..8628dc6324 100644
--- a/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
+++ b/src/Microsoft.Data.SqlClient/src/Resources/Strings.resx
@@ -4057,6 +4057,9 @@
Specified encrypted column encryption key contains an invalid encryption algorithm version '{0}'. Expected version is '{1}'.
+ The specified encrypted column encryption key's ciphertext length: {0} does not match the ciphertext length: {1} when using column master key ({2}) in '{3}'. The encrypted column encryption key may be corrupt, or the specified {4} path may be incorrect.
+
+
The specified encrypted column encryption key's ciphertext length: {0} does not match the ciphertext length: {1} when using column master key (certificate) in '{2}'. The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect.
@@ -4066,6 +4069,9 @@
The specified encrypted column encryption key's ciphertext length: {0} does not match the ciphertext length: {1} when using column master key (asymmetric key) in '{2}'. The encrypted column encryption key may be corrupt, or the specified Microsoft Cryptography API: Next Generation (CNG) provider path may be incorrect.
+ The specified encrypted column encryption key's signature length: {0} does not match the signature length: {1} when using column master key ({2}) in '{3}'. The encrypted column encryption key may be corrupt, or the specified {4} path may be incorrect.
+
+
The specified encrypted column encryption key's signature length: {0} does not match the signature length: {1} when using column master key (certificate) in '{2}'. The encrypted column encryption key may be corrupt, or the specified certificate path may be incorrect.
@@ -4074,10 +4080,13 @@
The specified encrypted column encryption key's signature length: {0} does not match the signature length: {1} when using column master key (asymmetric key) in '{2}'. The encrypted column encryption key may be corrupt, or the specified Microsoft Cryptography API: Next Generation (CNG) provider path may be incorrect.
+
+ The specified encrypted column encryption key signature does not match the signature computed with the column master key ({0}) in '{1}'. The encrypted column encryption key may be corrupt, or the specified path may be incorrect.
+
The specified encrypted column encryption key signature does not match the signature computed with the column master key (certificate) in '{0}'. The encrypted column encryption key may be corrupt, or the specified path may be incorrect.
-
+
The specified encrypted column encryption key signature does not match the signature computed with the column master key (asymmetric key) in '{0}'. The encrypted column encryption key may be corrupt, or the specified path may be incorrect.
From e30ccae099908e9f17572d6abf9e5e76d7ea3162 Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Tue, 12 Aug 2025 21:35:01 +0100
Subject: [PATCH 2/6] Respond to code review
Add a comment and a debug assertion to make it clear that the invariant lowercase value of masterKeyPath should be the same length as the original string.
---
.../AlwaysEncrypted/ColumnMasterKeyMetadata.cs | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
index 182748b783..c3b323a1b5 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
@@ -3,6 +3,7 @@
// 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;
@@ -55,11 +56,17 @@ public ColumnMasterKeyMetadata(RSA rsa, string masterKeyPath, string providerNam
// .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 the format:
- // [LocalMachine|CurrentUser]/My/[40 character SHA1 thumbprint]
+ // 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 enclaveComputationSpan = (allowEnclaveComputations ? bool.TrueString : bool.FalseString).AsSpan();
int masterKeyMetadataLength = providerName.Length + masterKeyPath.Length + enclaveComputationSpan.Length;
int byteCount;
From ba8bdea41c67debfcddfc22566d51a536c706701 Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Wed, 27 Aug 2025 21:12:06 +0100
Subject: [PATCH 3/6] Subsequent code review response
Add/updated XML documentation to the public APIs.
Add intermediary variable containing the signature size for clarity.
Use AlgorithmOffset rather than hardcoded offset in EncryptedColumnEncryptionKeyParameters.
Specifically define using block in ColumnMasterKeyMetadata.
---
.../ColumnMasterKeyMetadata.cs | 51 +++++++++++++++--
.../EncryptedColumnEncryptionKeyParameters.cs | 57 +++++++++++++++++--
2 files changed, 96 insertions(+), 12 deletions(-)
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
index c3b323a1b5..c1b5e1065a 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
@@ -19,7 +19,7 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted;
/// This metadata is a lower-case string which is laid out in the following format:
///
///
-/// Provider name. This always .
+/// Provider name. This is always .
///
///
/// Master key path. This will be in the format [LocalMachine|CurrentUser]/My/[SHA1 thumbprint].
@@ -50,6 +50,25 @@ private struct Sha256Hash
private readonly RSA _rsa;
// @TODO: SqlColumnEncryptionCertificateStoreProvider.SignMasterKeyMetadata and .VerifyMasterKeyMetadata should use this type.
+ ///
+ /// Represents metadata associated with a column master key, including its cryptographic hash, path, provider name,
+ /// and enclave computation settings.
+ ///
+ ///
+ /// This class 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.
+ ///
+ /// The RSA cryptographic provider used for signing or verifying the metadata.
+ /// The path to the column master key. This must be a valid path in one of the following formats:
+ ///
+ /// [LocalMachine|CurrentUser]/My/[40-character SHA1 thumbprint]
+ /// My/[40-character SHA1 thumbprint]
+ /// [40-character SHA1 thumbprint]
+ ///
+ /// The path is case-insensitive and will be converted to lowercase for processing.
+ /// The name of the provider associated with the column master key.
+ /// A value indicating whether enclave computations are allowed for this column master key.
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.
@@ -100,22 +119,42 @@ public ColumnMasterKeyMetadata(RSA rsa, string masterKeyPath, string providerNam
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;
+ using (SHA256 sha256 = SHA256.Create())
+ {
+ // Compute hash
+ sha256.TransformFinalBlock(masterKeyMetadataBytes, 0, masterKeyMetadataBytes.Length);
+ _hash = sha256.Hash;
+ }
#endif
_rsa = rsa;
}
+ ///
+ /// Signs the current master key metadata using the RSA key associated with this instance.
+ ///
+ ///
+ /// A byte array containing the digital signature of the master key metadata.
+ ///
public byte[] Sign() =>
_rsa.SignHash(_hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
+ ///
+ /// Verifies the specified master key metadata signature against the computed hash using the RSA key associated with this instance.
+ ///
+ /// The digital signature to verify. This must be a valid signature generated by .
+ ///
+ /// if the signature is valid and matches the computed hash; otherwise, .
+ ///
public bool Verify(byte[] signature) =>
_rsa.VerifyHash(_hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
+ ///
+ /// Releases all resources used by this .
+ ///
+ ///
+ /// This method disposes the instance used to construct this instance.
+ ///
public void Dispose() =>
_rsa.Dispose();
}
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
index a69f1e6b4b..434bf185b0 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
@@ -22,13 +22,13 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted;
/// Version: 1 byte, always 0x01
///
///
-/// Key path length: 2 bytes, length of the key path in bytes
+/// Key path length: 2 bytes, length of the key path in bytes. Written in little-endian byte order.
///
///
-/// Ciphertext length: 2 bytes, length of the ciphertext in bytes
+/// Ciphertext length: 2 bytes, length of the ciphertext in bytes. Written in little-endian byte order.
///
///
-/// Key path: variable length, Unicode-encoded string representing the key path
+/// Key path: variable length, string representing the key path. Encoded with UTF-16.
///
///
/// Ciphertext: variable length, encrypted data. Length determined by size of the RSA key used for encryption
@@ -65,6 +65,23 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted;
private readonly string _keyPathReference;
// @TODO: SqlColumnEncryptionCertificateStoreProvider, SqlColumnEncryptionCngProvider and SqlColumnEncryptionCspProvider should use this type.
+ ///
+ /// Initializes a new instance of the class with the specified
+ /// RSA key, key path, key type, and key path reference.
+ ///
+ ///
+ /// This constructor is used to initialize the parameters required for encrypting a column encryption key. The
+ /// and must correspond to the supported types and
+ /// references defined by the specific encryption provider being used.
+ ///
+ /// The object representing the RSA key used for encryption.
+ /// The path initially used to locate .
+ /// The type of the encryption key. This must be one of the supported key types, such
+ /// as , ,
+ /// or .
+ /// The type of object which contains the RSA key referenced by the
+ /// parameter. This must be one of the supported object types, such as ,
+ /// , or .
public EncryptedColumnEncryptionKeyParameters(RSA rsa, string keyPath, string keyType, string keyPathReference)
{
_rsa = rsa;
@@ -79,10 +96,20 @@ public EncryptedColumnEncryptionKeyParameters(RSA rsa, string keyPath, string ke
_keyPathReference = keyPathReference;
}
+ ///
+ /// Encrypts the specified column encryption key using the RSA key associated with this instance.
+ ///
+ /// The plaintext column encryption key to encrypt.
+ ///
+ /// The encrypted column encryption key, including metadata such as the key path, ciphertext, and a digital signature
+ /// for integrity verification.
+ ///
public byte[] Encrypt(byte[] columnEncryptionKey)
{
ushort keyPathSize = (ushort)Encoding.Unicode.GetByteCount(_keyPath);
- int cekSize = sizeof(byte) + sizeof(ushort) + sizeof(ushort) + keyPathSize + _rsaKeySize + _rsaKeySize;
+ // The signature size is always the same as the RSA key size
+ int signatureSize = _rsaKeySize;
+ int cekSize = sizeof(byte) + sizeof(ushort) + sizeof(ushort) + keyPathSize + _rsaKeySize + signatureSize;
byte[] encryptedColumnEncryptionKey = new byte[cekSize];
int bytesWritten;
int cipherTextOffset = KeyPathOffset + keyPathSize;
@@ -147,12 +174,24 @@ public byte[] Encrypt(byte[] columnEncryptionKey)
return encryptedColumnEncryptionKey;
}
+ ///
+ /// Decrypts an encrypted column encryption key (CEK) using the RSA key associated with this instance.
+ ///
+ ///
+ /// This method validates the algorithm version, ciphertext length, and signature length before
+ /// decrypting the CEK. It also verifies the integrity of the encrypted CEK using the provided signature.
+ ///
+ /// A byte array containing the encrypted column encryption key. The array must include
+ /// the algorithm version, key path length, ciphertext, and signature.
+ ///
+ /// The decrypted column encryption key.
+ ///
public byte[] Decrypt(byte[] encryptedCek)
{
// Validate the version byte
- if (encryptedCek[0] != AlgorithmVersion)
+ if (encryptedCek[AlgorithmOffset] != AlgorithmVersion)
{
- throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedCek[0], AlgorithmVersion);
+ throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedCek[AlgorithmOffset], AlgorithmVersion);
}
// Get key path length, but skip reading it. It exists only for troubleshooting purposes and doesn't need validation.
@@ -215,6 +254,12 @@ public byte[] Decrypt(byte[] encryptedCek)
return _rsa.Decrypt(cipherText, RSAEncryptionPadding.OaepSHA1);
}
+ ///
+ /// Releases all resources used by this .
+ ///
+ ///
+ /// This method disposes the instance used to construct this instance.
+ ///
public void Dispose() =>
_rsa.Dispose();
}
From 7defd2f0c6caff2da1944ca00c57bf61ccbfbae8 Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Wed, 27 Aug 2025 21:31:51 +0100
Subject: [PATCH 4/6] Typo correction
class -> struct
---
.../Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs | 2 +-
.../AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
index c1b5e1065a..1c1228e579 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
@@ -55,7 +55,7 @@ private struct Sha256Hash
/// and enclave computation settings.
///
///
- /// This class is used to encapsulate the metadata required for signing or verifying a column master key. The metadata includes
+ /// 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.
///
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
index 434bf185b0..82080d71cc 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
@@ -66,7 +66,7 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted;
// @TODO: SqlColumnEncryptionCertificateStoreProvider, SqlColumnEncryptionCngProvider and SqlColumnEncryptionCspProvider should use this type.
///
- /// Initializes a new instance of the class with the specified
+ /// Initializes a new instance of the struct with the specified
/// RSA key, key path, key type, and key path reference.
///
///
From a311495a8afb9a002532c63932b2cb8bf0ea0c9a Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Thu, 28 Aug 2025 19:13:23 +0100
Subject: [PATCH 5/6] Review and add exceptions to XML documentation
---
.../SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs | 1 +
.../EncryptedColumnEncryptionKeyParameters.cs | 5 +++++
2 files changed, 6 insertions(+)
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
index 1c1228e579..beeaea5bd4 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
@@ -146,6 +146,7 @@ public byte[] Sign() =>
///
/// if the signature is valid and matches the computed hash; otherwise, .
///
+ /// Thrown when is ."
public bool Verify(byte[] signature) =>
_rsa.VerifyHash(_hash, signature, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
index 82080d71cc..33cdf85226 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
@@ -104,6 +104,8 @@ public EncryptedColumnEncryptionKeyParameters(RSA rsa, string keyPath, string ke
/// The encrypted column encryption key, including metadata such as the key path, ciphertext, and a digital signature
/// for integrity verification.
///
+ /// Thrown when is null.
+ /// Thrown when is longer than the RSA key size.
public byte[] Encrypt(byte[] columnEncryptionKey)
{
ushort keyPathSize = (ushort)Encoding.Unicode.GetByteCount(_keyPath);
@@ -186,6 +188,9 @@ public byte[] Encrypt(byte[] columnEncryptionKey)
///
/// The decrypted column encryption key.
///
+ /// Thrown when is null.
+ /// Thrown when the contents of are malformed or its signature fails to verify.
+ /// Thrown when decryption fails.
public byte[] Decrypt(byte[] encryptedCek)
{
// Validate the version byte
From cb143aa21a979de106f9e50ddf22af28a8e265ea Mon Sep 17 00:00:00 2001
From: Edward Neal <55035479+edwardneal@users.noreply.github.com>
Date: Thu, 28 Aug 2025 23:46:29 +0100
Subject: [PATCH 6/6] Add additional instances where CryptographicException can
be thrown
---
.../Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs | 1 +
.../AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
index beeaea5bd4..32663eb1a1 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/ColumnMasterKeyMetadata.cs
@@ -136,6 +136,7 @@ public ColumnMasterKeyMetadata(RSA rsa, string masterKeyPath, string providerNam
///
/// A byte array containing the digital signature of the master key metadata.
///
+ /// Thrown when the signing operation fails.
public byte[] Sign() =>
_rsa.SignHash(_hash, s_hashAlgorithm, RSASignaturePadding.Pkcs1);
diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
index 33cdf85226..4abfa62abd 100644
--- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
+++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptedColumnEncryptionKeyParameters.cs
@@ -105,7 +105,9 @@ public EncryptedColumnEncryptionKeyParameters(RSA rsa, string keyPath, string ke
/// for integrity verification.
///
/// Thrown when is null.
- /// Thrown when is longer than the RSA key size.
+ /// Thrown when is longer than the RSA
+ /// key size, when an error occurs encrypting the column encryption key, or when signing the encrypted column
+ /// encryption key fails.
public byte[] Encrypt(byte[] columnEncryptionKey)
{
ushort keyPathSize = (ushort)Encoding.Unicode.GetByteCount(_keyPath);