| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- //------------------------------------------------------------------------------
- // <copyright file="SqlColumnEncryptionCertificateStoreProvider.cs" company="Microsoft">
- // Copyright (c) Microsoft Corporation. All rights reserved.
- // </copyright>
- // <owner current="true" primary="true">balnee</owner>
- // <owner current="true" primary="false">krishnib</owner>
- //------------------------------------------------------------------------------
- namespace System.Data.SqlClient
- {
- using System;
- using System.Text;
- using System.Data.Common;
- using System.Diagnostics;
- using System.Globalization;
- using System.Security;
- using System.Security.Cryptography;
- using System.Security.Cryptography.X509Certificates;
- /// <summary>
- /// Certificate Key Store Provider class
- /// </summary>
- public class SqlColumnEncryptionCertificateStoreProvider : SqlColumnEncryptionKeyStoreProvider
- {
- // Constants
- //
- // Assumption: Certificate Locations (LocalMachine & CurrentUser), Certificate Store name "My"
- // Certificate provider name (CertificateStore) dont need to be localized.
- /// <summary>
- /// Name for the certificate key store provider.
- /// </summary>
- public const string ProviderName = @"MSSQL_CERTIFICATE_STORE";
- /// <summary>
- /// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys.
- /// </summary>
- internal const string RSAEncryptionAlgorithmWithOAEP = @"RSA_OAEP";
- /// <summary>
- /// LocalMachine certificate store location. Valid certificate locations are LocalMachine and CurrentUser.
- /// </summary>
- private const string _certLocationLocalMachine = @"LocalMachine";
- /// <summary>
- /// CurrentUser certificate store location. Valid certificate locations are LocalMachine and CurrentUser.
- /// </summary>
- private const string _certLocationCurrentUser = @"CurrentUser";
- /// <summary>
- /// Valid certificate store
- /// </summary>
- private const string _myCertificateStore = @"My";
- /// <summary>
- /// Certificate path format. This is a custom format.
- /// </summary>
- private const string _certificatePathFormat = @"[LocalMachine|CurrentUser]/My/[Thumbprint]";
- /// <summary>
- /// Hashig algoirthm used for signing
- /// </summary>
- private const string _hashingAlgorithm = @"SHA256";
- /// <summary>
- /// Algorithm version
- /// </summary>
- private readonly byte[] _version = new byte[] { 0x01 };
- /// <summary>
- /// This function uses a certificate specified by the key path
- /// and decrypts an encrypted CEK with RSA encryption algorithm.
- /// </summary>
- /// <param name="masterKeyPath">Complete path of a certificate</param>
- /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
- /// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key</param>
- /// <returns>Plain text column encryption key</returns>
- public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey)
- {
- // Validate the input parameters
- ValidateNonEmptyCertificatePath(masterKeyPath, isSystemOp: true);
- if (null == encryptedColumnEncryptionKey)
- {
- throw SQL.NullEncryptedColumnEncryptionKey();
- }
- else if (0 == encryptedColumnEncryptionKey.Length)
- {
- throw SQL.EmptyEncryptedColumnEncryptionKey();
- }
- // Validate encryptionAlgorithm
- ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true);
- // Validate key path length
- ValidateCertificatePathLength(masterKeyPath, isSystemOp: true);
- // Parse the path and get the X509 cert
- X509Certificate2 certificate = GetCertificateByPath(masterKeyPath, isSystemOp: true);
- int keySizeInBytes = certificate.PublicKey.Key.KeySize / 8;
- // Validate and decrypt the EncryptedColumnEncryptionKey
- // Format is
- // version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
- //
- // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and
- // we will not validate it against the data contained in the CMK metadata (masterKeyPath).
- // Validate the version byte
- if (encryptedColumnEncryptionKey[0] != _version[0])
- {
- throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedColumnEncryptionKey[0], _version[0]);
- }
- // Get key path length
- int currentIndex = _version.Length;
- Int16 keyPathLength = BitConverter.ToInt16(encryptedColumnEncryptionKey, currentIndex);
- currentIndex += sizeof(Int16);
- // Get ciphertext length
- int cipherTextLength = BitConverter.ToInt16(encryptedColumnEncryptionKey, currentIndex);
- currentIndex += sizeof(Int16);
- // Skip KeyPath
- // KeyPath exists only for troubleshooting purposes and doesnt need validation.
- currentIndex += keyPathLength;
- // validate the ciphertext length
- if (cipherTextLength != keySizeInBytes)
- {
- throw SQL.InvalidCiphertextLengthInEncryptedCEK(cipherTextLength, keySizeInBytes, masterKeyPath);
- }
- // Validate the signature length
- // Signature length should be same as the key side for RSA PKCSv1.5
- int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength;
- if (signatureLength != keySizeInBytes)
- {
- throw SQL.InvalidSignatureInEncryptedCEK(signatureLength, keySizeInBytes, masterKeyPath);
- }
- // Get ciphertext
- byte[] cipherText = new byte[cipherTextLength];
- Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherText.Length);
- currentIndex += cipherTextLength;
- // Get signature
- byte[] signature = new byte[signatureLength];
- Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, signature, 0, signature.Length);
- // Compute the hash to validate the signature
- byte[] hash;
- using (SHA256Cng sha256 = new SHA256Cng())
- {
- sha256.TransformFinalBlock(encryptedColumnEncryptionKey, 0, encryptedColumnEncryptionKey.Length - signature.Length);
- hash = sha256.Hash;
- }
- Debug.Assert(hash != null, @"hash should not be null while decrypting encrypted column encryption key.");
- // Validate the signature
- if (!RSAVerifySignature(hash, signature, certificate))
- {
- throw SQL.InvalidCertificateSignature(masterKeyPath);
- }
- // Decrypt the CEK
- return RSADecrypt(cipherText, certificate);
- }
- /// <summary>
- /// This function uses a certificate specified by the key path
- /// and encrypts CEK with RSA encryption algorithm.
- /// </summary>
- /// <param name="keyPath">Complete path of a certificate</param>
- /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
- /// <param name="columnEncryptionKey">Plain text column encryption key</param>
- /// <returns>Encrypted column encryption key</returns>
- public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey)
- {
- // Validate the input parameters
- ValidateNonEmptyCertificatePath(masterKeyPath, isSystemOp: false);
- if (null == columnEncryptionKey)
- {
- throw SQL.NullColumnEncryptionKey();
- }
- else if (0 == columnEncryptionKey.Length)
- {
- throw SQL.EmptyColumnEncryptionKey();
- }
- // Validate encryptionAlgorithm
- ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: false);
- // Validate masterKeyPath Length
- ValidateCertificatePathLength(masterKeyPath, isSystemOp: false);
- // Parse the certificate path and get the X509 cert
- X509Certificate2 certificate = GetCertificateByPath(masterKeyPath, isSystemOp: false);
- int keySizeInBytes = certificate.PublicKey.Key.KeySize / 8;
- // Construct the encryptedColumnEncryptionKey
- // Format is
- // version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature
- //
- // We currently only support one version
- byte[] version = new byte[] { _version[0] };
- // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath
- byte[] masterKeyPathBytes = Encoding.Unicode.GetBytes(masterKeyPath.ToLowerInvariant());
- byte[] keyPathLength = BitConverter.GetBytes((Int16)masterKeyPathBytes.Length);
- // Encrypt the plain text
- byte[] cipherText = RSAEncrypt(columnEncryptionKey, certificate);
- byte[] cipherTextLength = BitConverter.GetBytes((Int16)cipherText.Length);
- Debug.Assert(cipherText.Length == keySizeInBytes, @"cipherText length does not match the RSA key size");
- // Compute hash
- // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext)
- byte[] hash;
- using (SHA256Cng sha256 = new SHA256Cng())
- {
- sha256.TransformBlock(version, 0, version.Length, version, 0);
- sha256.TransformBlock(keyPathLength, 0, keyPathLength.Length, keyPathLength, 0);
- sha256.TransformBlock(cipherTextLength, 0, cipherTextLength.Length, cipherTextLength, 0);
- sha256.TransformBlock(masterKeyPathBytes, 0, masterKeyPathBytes.Length, masterKeyPathBytes, 0);
- sha256.TransformFinalBlock(cipherText, 0, cipherText.Length);
- hash = sha256.Hash;
- }
- // Sign the hash
- byte[] signedHash = RSASignHashedData(hash, certificate);
- Debug.Assert(signedHash.Length == keySizeInBytes, @"signed hash length does not match the RSA key size");
- Debug.Assert(RSAVerifySignature(hash, signedHash, certificate), @"Invalid signature of the encrypted column encryption key computed.");
- // Construct the encrypted column encryption key
- // EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
- int encryptedColumnEncryptionKeyLength = version.Length + cipherTextLength.Length + keyPathLength.Length + cipherText.Length + masterKeyPathBytes.Length + signedHash.Length;
- byte[] encryptedColumnEncryptionKey = new byte[encryptedColumnEncryptionKeyLength];
- // Copy version byte
- int currentIndex = 0;
- Buffer.BlockCopy(version, 0, encryptedColumnEncryptionKey, currentIndex, version.Length);
- currentIndex += version.Length;
- // Copy key path length
- Buffer.BlockCopy(keyPathLength, 0, encryptedColumnEncryptionKey, currentIndex, keyPathLength.Length);
- currentIndex += keyPathLength.Length;
- // Copy ciphertext length
- Buffer.BlockCopy(cipherTextLength, 0, encryptedColumnEncryptionKey, currentIndex, cipherTextLength.Length);
- currentIndex += cipherTextLength.Length;
- // Copy key path
- Buffer.BlockCopy(masterKeyPathBytes, 0, encryptedColumnEncryptionKey, currentIndex, masterKeyPathBytes.Length);
- currentIndex += masterKeyPathBytes.Length;
- // Copy ciphertext
- Buffer.BlockCopy(cipherText, 0, encryptedColumnEncryptionKey, currentIndex, cipherText.Length);
- currentIndex += cipherText.Length;
- // copy the signature
- Buffer.BlockCopy(signedHash, 0, encryptedColumnEncryptionKey, currentIndex, signedHash.Length);
- return encryptedColumnEncryptionKey;
- }
- /// <summary>
- /// This function validates that the encryption algorithm is RSA_OAEP and if it is not,
- /// then throws an exception
- /// </summary>
- /// <param name="encryptionAlgorithm">Asymmetric key encryptio algorithm</param>
- private void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp)
- {
- // This validates that the encryption algorithm is RSA_OAEP
- if (null == encryptionAlgorithm)
- {
- throw SQL.NullKeyEncryptionAlgorithm(isSystemOp);
- }
- if (string.Equals(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, StringComparison.OrdinalIgnoreCase) != true)
- {
- throw SQL.InvalidKeyEncryptionAlgorithm(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, isSystemOp);
- }
- }
- /// <summary>
- /// Certificate path length has to fit in two bytes, so check its value against Int16.MaxValue
- /// </summary>
- /// <param name="masterKeyPath"></param>
- /// <param name="isSystemOp"></param>
- private void ValidateCertificatePathLength(string masterKeyPath, bool isSystemOp)
- {
- if (masterKeyPath.Length >= Int16.MaxValue)
- {
- throw SQL.LargeCertificatePathLength(masterKeyPath.Length, Int16.MaxValue, isSystemOp);
- }
- }
- /// <summary>
- /// Gets a string array containing Valid certificate locations.
- /// </summary>
- private string[] GetValidCertificateLocations()
- {
- return new string[2] {_certLocationLocalMachine, _certLocationCurrentUser};
- }
- /// <summary>
- /// Checks if the certificate path is Empty or Null (and raises exception if they are).
- /// </summary>
- private void ValidateNonEmptyCertificatePath(string masterKeyPath, bool isSystemOp)
- {
- if (string.IsNullOrWhiteSpace(masterKeyPath))
- {
- if (null == masterKeyPath)
- {
- throw SQL.NullCertificatePath(GetValidCertificateLocations(), isSystemOp);
- }
- else
- {
- throw SQL.InvalidCertificatePath(masterKeyPath, GetValidCertificateLocations(), isSystemOp);
- }
- }
- }
- /// <summary>
- /// Parses the given certificate path, searches in certificate store and returns a matching certificate
- /// </summary>
- /// <param name="keyPath">
- /// Certificate key path. Format of the path is [LocalMachine|CurrentUser]/[storename]/thumbprint
- /// </param>
- /// <returns>Returns the certificate identified by the certificate path</returns>
- private X509Certificate2 GetCertificateByPath(string keyPath, bool isSystemOp)
- {
- Debug.Assert(!string.IsNullOrEmpty(keyPath));
- // Assign default values for omitted fields
- StoreLocation storeLocation = StoreLocation.LocalMachine; // Default to Local Machine
- StoreName storeName = StoreName.My;
- string[] certParts = keyPath.Split('/');
- // Validate certificate path
- // Certificate path should only contain 3 parts (Certificate Location, Certificate Store Name and Thumbprint)
- if (certParts.Length > 3)
- {
- throw SQL.InvalidCertificatePath(keyPath, GetValidCertificateLocations(), isSystemOp);
- }
- // Extract the store location where the cert is stored
- if (certParts.Length > 2)
- {
- if (string.Equals(certParts[0], _certLocationLocalMachine, StringComparison.OrdinalIgnoreCase) == true)
- {
- storeLocation = StoreLocation.LocalMachine;
- }
- else if (string.Equals(certParts[0], _certLocationCurrentUser, StringComparison.OrdinalIgnoreCase) == true)
- {
- storeLocation = StoreLocation.CurrentUser;
- }
- else
- {
- // throw an invalid certificate location exception
- throw SQL.InvalidCertificateLocation(certParts[0], keyPath, GetValidCertificateLocations(), isSystemOp);
- }
- }
- // Parse the certificate store name
- if (certParts.Length > 1)
- {
- if (string.Equals(certParts[certParts.Length - 2], _myCertificateStore, StringComparison.OrdinalIgnoreCase) == true)
- {
- storeName = StoreName.My;
- }
- else
- {
- // We only support storing them in My certificate store
- throw SQL.InvalidCertificateStore(certParts[certParts.Length - 2], keyPath, _myCertificateStore, isSystemOp);
- }
- }
- // Get thumpbrint
- string thumbprint = certParts[certParts.Length - 1];
- if (string.IsNullOrEmpty(thumbprint))
- {
- // An empty thumbprint specified
- throw SQL.EmptyCertificateThumbprint(keyPath, isSystemOp);
- }
- // Find the certificate and return
- return GetCertificate(storeLocation, storeName, keyPath, thumbprint, isSystemOp);
- }
- /// <summary>
- /// Searches for a certificate in certificate store and returns the matching certificate
- /// </summary>
- /// <param name="storeLocation">Store Location: This can be one of LocalMachine or UserName</param>
- /// <param name="storeName">Store Location: Currently this can only be My store.</param>
- /// <param name="thumbprint">Certificate thumbprint</param>
- /// <returns>Matching certificate</returns>
- private X509Certificate2 GetCertificate(StoreLocation storeLocation, StoreName storeName, string masterKeyPath, string thumbprint, bool isSystemOp)
- {
- // Open specified certificate store
- X509Store certificateStore = null;
- try
- {
- certificateStore = new X509Store(storeName, storeLocation);
- certificateStore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
- // Search for the specified certificate
- X509Certificate2Collection matchingCertificates =
- certificateStore.Certificates.Find(X509FindType.FindByThumbprint,
- thumbprint,
- false);
- // Throw an exception if a cert with the specified thumbprint is not found
- if (matchingCertificates == null || matchingCertificates.Count == 0)
- {
- throw SQL.CertificateNotFound(thumbprint, storeName.ToString(), storeLocation.ToString(), isSystemOp);
- }
- X509Certificate2 certificate = matchingCertificates[0];
- if (!certificate.HasPrivateKey)
- {
- // ensure the certificate has private key
- throw SQL.CertificateWithNoPrivateKey(masterKeyPath, isSystemOp);
- }
- // return the matching certificate
- return certificate;
- }
- finally
- {
- // Close the certificate store
- if (certificateStore != null)
- {
- certificateStore.Close();
- }
- }
- }
- /// <summary>
- /// Encrypt the text using specified certificate.
- /// </summary>
- /// <param name="plaintext">Text to encrypt.</param>
- /// <param name="certificate">Certificate object.</param>
- /// <param name="masterKeyPath">Master key path that was used.</param>
- /// <returns>Returns an encrypted blob or throws an exception if there are any errors.</returns>
- private byte[] RSAEncrypt(byte[] plainText, X509Certificate2 certificate)
- {
- Debug.Assert(plainText != null);
- Debug.Assert(certificate != null);
- Debug.Assert(certificate.HasPrivateKey, "Attempting to encrypt with cert without privatekey");
- RSACryptoServiceProvider rscp = (RSACryptoServiceProvider)certificate.PublicKey.Key;
- return rscp.Encrypt(plainText, fOAEP: true);
- }
- /// <summary>
- /// Encrypt the text using specified certificate.
- /// </summary>
- /// <param name="plaintext">Text to decrypt.</param>
- /// <param name="certificate">Certificate object.</param>
- /// <param name="masterKeyPath">Master key path that was used.</param>
- private byte[] RSADecrypt(byte[] cipherText, X509Certificate2 certificate)
- {
- Debug.Assert((cipherText != null) && (cipherText.Length != 0));
- Debug.Assert(certificate != null);
- Debug.Assert(certificate.HasPrivateKey, "Attempting to decrypt with cert without privatekey");
- RSACryptoServiceProvider rscp = (RSACryptoServiceProvider)certificate.PrivateKey;
- return rscp.Decrypt(cipherText, fOAEP: true);
- }
- /// <summary>
- /// Generates signature based on RSA PKCS#v1.5 scheme using a specified certificate.
- /// </summary>
- /// <param name="dataToSign">Text to sign.</param>
- /// <param name="certificate">Certificate object.</param>
- /// <returns>Signature</returns>
- private byte[] RSASignHashedData(byte[] dataToSign, X509Certificate2 certificate)
- {
- Debug.Assert((dataToSign != null) && (dataToSign.Length != 0));
- Debug.Assert(certificate != null);
- Debug.Assert(certificate.HasPrivateKey, "Attempting to sign with cert without privatekey");
- // Prepare RSACryptoServiceProvider from certificate's private key
- RSACryptoServiceProvider rscp = GetCSPFromCertificatePrivateKey(certificate);
- // Prepare RSAPKCS1SignatureFormatter for signing the passed in hash
- RSAPKCS1SignatureFormatter rsaFormatter = new RSAPKCS1SignatureFormatter(rscp);
- //Set the hash algorithm to SHA256.
- rsaFormatter.SetHashAlgorithm(_hashingAlgorithm);
- //Create a signature for HashValue and return it.
- return rsaFormatter.CreateSignature(dataToSign);
- }
- /// <summary>
- /// Verifies the given RSA PKCSv1.5 signature.
- /// </summary>
- /// <param name="dataToVerify"></param>
- /// <param name="signature"></param>
- /// <param name="certificate"></param>
- /// <returns>true if signature is valid, false if it is not valid</returns>
- private bool RSAVerifySignature(byte[] dataToVerify, byte[] signature, X509Certificate2 certificate)
- {
- Debug.Assert((dataToVerify != null) && (dataToVerify.Length != 0));
- Debug.Assert((signature != null) && (signature.Length != 0));
- Debug.Assert(certificate != null);
- Debug.Assert(certificate.HasPrivateKey, "Attempting to sign with cert without privatekey");
- // Prepare RSACryptoServiceProvider from certificate's private key
- RSACryptoServiceProvider rscp = GetCSPFromCertificatePrivateKey(certificate);
- // Prepare RSAPKCS1SignatureFormatter for signing the passed in hash
- RSAPKCS1SignatureDeformatter rsaDeFormatter = new RSAPKCS1SignatureDeformatter(rscp);
- //Set the hash algorithm to SHA256.
- rsaDeFormatter.SetHashAlgorithm(_hashingAlgorithm);
- //Create a signature for HashValue and return it.
- return rsaDeFormatter.VerifySignature(dataToVerify, signature);
- }
- /// <summary>
- /// Prepares RSACryptoServiceProvider from a given certificate's private key
- /// </summary>
- /// <param name="certificate"></param>
- /// <returns></returns>
- private RSACryptoServiceProvider GetCSPFromCertificatePrivateKey(X509Certificate2 certificate)
- {
- const int rsaAesProviderType = 24;
- CspParameters privateKeyParams = new CspParameters();
- privateKeyParams = new CspParameters();
- privateKeyParams.KeyContainerName = ((RSACryptoServiceProvider)certificate.PrivateKey).CspKeyContainerInfo.KeyContainerName;
- privateKeyParams.ProviderType = rsaAesProviderType /*PROV_RSA_AES*/;
- privateKeyParams.KeyNumber = (int)((RSACryptoServiceProvider)certificate.PrivateKey).CspKeyContainerInfo.KeyNumber;
- // For CurrentUser store, use UseExistingKey
- // For LocalMachine store, use UseMachineKeyStore
- // CspKeyContainerInfo.MachineKeyStore already contains the appropriate information so just use it.
- if (((RSACryptoServiceProvider)certificate.PrivateKey).CspKeyContainerInfo.MachineKeyStore)
- {
- privateKeyParams.Flags = CspProviderFlags.UseMachineKeyStore;
- }
- else
- {
- privateKeyParams.Flags = CspProviderFlags.UseExistingKey;
- }
- return new RSACryptoServiceProvider(privateKeyParams);
- }
- }
- }
|