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);