فصل ۲۰: رمزنگاری در .NET؛ Hashing، AES، RSA و امضای دیجیتال
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
نشان تصویری آغاز فصل در منبع
فصل ۲۰: رمزنگاری (Cryptography)
در این فصل APIهای اصلی رمزنگاری در .NET بررسی میشوند:
- Windows Data Protection API یا DPAPI
- Hashing
- رمزنگاری متقارن (Symmetric Encryption)
- رمزنگاری کلید عمومی و امضای دیجیتال
نوعهای این فصل عمدتاً در فضاهای نام زیر قرار دارند:
System.Security;
System.Security.Cryptography;
مرور کلی
جدول 20-1 گزینههای رمزنگاری و hashing در .NET را خلاصه میکند.
جدول 20-1. گزینههای رمزنگاری و Hashing در .NET| گزینه | تعداد کلید قابل مدیریت | سرعت | قدرت | یادداشت |
File.Encrypt | 0 | سریع | وابسته به گذرواژهٔ کاربر | فایلها را با پشتیبانی فایلسیستم بهصورت شفاف محافظت میکند. کلید بهطور ضمنی از اطلاعات ورود کاربر مشتق میشود. فقط Windows. |
| Windows Data Protection | 0 | سریع | وابسته به گذرواژهٔ کاربر | آرایههای byte را با کلیدی که ضمنی مشتق میشود رمز و رمزگشایی میکند. |
| Hashing | 0 | سریع | بالا | تبدیل یکطرفه و برگشتناپذیر؛ برای ذخیره گذرواژه، مقایسه فایل و بررسی خرابی داده. |
| Symmetric Encryption | 1 | سریع | بالا | برای رمزگذاری/رمزگشایی عمومی؛ همان کلید برای هر دو جهت بهکار میرود و میتواند پیام در حال انتقال را امن کند. |
| Public Key Encryption | 2 | کند | بالا | رمزگذاری و رمزگشایی از کلیدهای متفاوت استفاده میکنند؛ برای تبادل یک کلید متقارن و نیز امضای دیجیتال فایلها. |
.NET همچنین پشتیبانی تخصصیتری برای ساخت و اعتبارسنجی امضاهای مبتنی بر XML در System.Security.Cryptography.Xml و برای کار با گواهیهای دیجیتال در System.Security.Cryptography.X509Certificates دارد.
Windows Data Protection
در بخش عملیات فایل و دایرکتوری در صفحه 723، استفاده از File.Encrypt برای درخواست از سیستمعامل جهت رمزگذاری شفاف یک فایل توضیح داده شد:
File.WriteAllText ("myfile.txt", "");
File.Encrypt ("myfile.txt");
File.AppendAllText ("myfile.txt", "sensitive data");
این رمزگذاری از کلیدی استفاده میکند که از گذرواژهٔ کاربر واردشده به سیستم مشتق میشود. همان کلید ضمنی را میتوان با DPAPI برای رمزگذاری یک آرایهٔ byte نیز به کار برد. DPAPI از طریق کلاس سادهٔ ProtectedData با دو متد static عرضه میشود:
public static byte[] Protect
(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope);
public static byte[] Unprotect
(byte[] encryptedData, byte[] optionalEntropy, DataProtectionScope scope);
هر دادهای که در optionalEntropy قرار داده شود به کلید اضافه میشود و امنیت را افزایش میدهد. آرگومان enum از نوع DataProtectionScope دو گزینه دارد: CurrentUser و LocalMachine. در حالت CurrentUser کلید از اعتبارنامهٔ کاربر فعلی مشتق میشود، بنابراین دادهٔ رمزشده توسط یک کاربر توسط کاربر دیگر قابل رمزگشایی نیست. در LocalMachine کلیدی در سطح ماشین و مشترک میان کاربران استفاده میشود؛ این حالت حفاظت کمتری دارد، ولی برای Windows Service یا برنامهای که باید زیر حسابهای کاربری گوناگون کار کند مناسب است.
byte[] original = {1, 2, 3, 4, 5};
DataProtectionScope scope = DataProtectionScope.CurrentUser;
byte[] encrypted = ProtectedData.Protect (original, null, scope);
byte[] decrypted = ProtectedData.Unprotect (encrypted, null, scope);
// decrypted is now {1, 2, 3, 4, 5}
Windows Data Protection در برابر مهاجمی که دسترسی کامل به رایانه دارد، بسته به قدرت گذرواژهٔ کاربر، حفاظت متوسطی فراهم میکند. در حالت LocalMachine فقط در برابر افرادی که دسترسی فیزیکی و الکترونیکی محدود دارند مؤثر است.
Hashing
الگوریتم hashing حجم بالقوه بزرگی از byteها را به hashcode کوچکی با طول ثابت تقلیل میدهد. الگوریتمهای hash طوری طراحی میشوند که تغییر فقط یک bit در هر نقطه از دادهٔ منبع، hashcode کاملاً متفاوتی ایجاد کند. به همین علت برای مقایسهٔ فایلها یا تشخیص خرابی تصادفی یا عمدی در فایل و جریان داده مناسباند.
Hashing نوعی تبدیل یکطرفه نیز محسوب میشود، زیرا بازگرداندن hashcode به دادهٔ اصلی بسیار دشوار یا عملاً ناممکن است. این ویژگی برای ذخیره گذرواژه در پایگاه داده مفید است: اگر database افشا شود، مهاجم نباید گذرواژههای plain text را به دست آورد. برای احراز هویت، ورودی کاربر hash میشود و با hash ذخیرهشده مقایسه میگردد.
برای ساخت hash، متد ComputeHash را روی یکی از subclassهای HashAlgorithm مانند SHA1 یا SHA256 فراخوانی میکنید:
byte[] hash;
using (Stream fs = File.OpenRead ("checkme.doc"))
hash = SHA1.Create().ComputeHash (fs); // SHA1 hash is 20 bytes long
ComputeHash آرایهٔ byte را نیز میپذیرد، که برای hashکردن گذرواژه مناسب است:
byte[] data = System.Text.Encoding.UTF8.GetBytes ("stRhong%pword");
byte[] hash = SHA256.Create().ComputeHash (data);
الگوریتمهای Hash در .NET
| کلاس | الگوریتم | طول hash به byte | قدرت |
| MD5 | MD5 | 16 | بسیار ضعیف |
| SHA1 | SHA-1 | 20 | ضعیف |
| SHA256 | SHA-2 | 32 | خوب |
| SHA384 | SHA-2 | 48 | خوب |
| SHA512 | SHA-2 | 64 | خوب |
در پیادهسازیهای فعلی، هر پنج الگوریتم تقریباً با سرعت مشابه اجرا میشوند، بهجز SHA256 که حدود 2 تا 3 برابر سریعتر است؛ این موضوع میتواند با سختافزار و سیستمعامل تغییر کند. روی یک desktop یا server متعلق به حوالی 2024 میتوان انتظار حداقل حدود 500 MB در ثانیه را داشت. hashهای طولانیتر احتمال collision، یعنی تولید hash یکسان برای دو فایل متفاوت، را کاهش میدهند.
Hashکردن گذرواژهها
الگوریتمهای SHA طولانیتر میتوانند مبنای hash گذرواژه باشند، به شرط آنکه سیاست گذرواژهٔ قوی برای کاهش خطر dictionary attack اعمال شود؛ در چنین حملهای مهاجم با hashکردن واژههای یک فرهنگ لغت، جدول جستوجوی گذرواژه میسازد.
روش استاندارد، افزودن salt است: دنبالهای بلند از byteها که ابتدا با مولد عدد تصادفی ساخته میشود و قبل از hashing با هر گذرواژه ترکیب میگردد. این کار مهاجم را از دو جهت دشوارتر میکند:
- باید byteهای salt را نیز بداند.
- نمیتواند از rainbow tableهای عمومی و از پیش محاسبهشده استفاده کند؛ با توان پردازشی کافی، dictionary attack همچنان ممکن است.
امنیت را میتوان با «stretching» نیز تقویت کرد: hash را بارها دوباره hash کنید تا محاسبهٔ آن پرهزینهتر شود. متن منبع مثال میزند که اگر 100 بار rehash انجام شود، حملهای که در حالت عادی یک ماه طول میکشد میتواند هشت سال طول بکشد. کلاسهای KeyDerivation، Rfc2898DeriveBytes و PasswordDeriveBytes این نوع stretching را همراه با salting انجام میدهند. از میان آنها، متن منبع KeyDerivation.Pbkdf2 را بهترین گزینه معرفی میکند:
byte[] encrypted = KeyDerivation.Pbkdf2 (
password: "stRhong%pword",
salt: Encoding.UTF8.GetBytes ("j78Y#p)/saREN!y3@"),
prf: KeyDerivationPrf.HMACSHA512,
iterationCount: 100,
numBytesRequested: 64);
رمزنگاری متقارن
در رمزنگاری متقارن، همان کلید برای encryption و decryption استفاده میشود. BCL در .NET چهار الگوریتم متقارن ارائه میکند که در متن منبع Rijndael گزینهٔ اصلی معرفی شده و سایر الگوریتمها عمدتاً برای سازگاری با برنامههای قدیمی هستند. Rijndael سریع و امن است و دو پیادهسازی دارد: کلاس Rijndael و کلاس Aes. این دو تقریباً یکساناند، جز آنکه AES اجازه نمیدهد با تغییر block size cipher را تضعیف کنید؛ تیم امنیت CLR استفاده از AES را توصیه میکند.
Rijndael و AES کلیدهایی با طول 16، 24 یا 32 byte میپذیرند و همهٔ این اندازهها در متن منبع امن تلقی شدهاند. نمونهٔ زیر با کلید 16-byte هنگام نوشتن در فایل دادهها را رمز میکند:
byte[] key = {145,12,32,245,98,132,98,214,6,77,131,44,221,3,9,50};
byte[] iv = {15,122,132,5,93,198,44,31,9,39,241,49,250,188,80,7};
byte[] data = { 1, 2, 3, 4, 5 }; // This is what we're encrypting.
using (SymmetricAlgorithm algorithm = Aes.Create())
using (ICryptoTransform encryptor = algorithm.CreateEncryptor (key, iv))
using (Stream f = File.Create ("encrypted.bin"))
using (Stream c = new CryptoStream (f, encryptor, CryptoStreamMode.Write))
c.Write (data, 0, data.Length);
کد زیر فایل را رمزگشایی میکند:
byte[] key = {145,12,32,245,98,132,98,214,6,77,131,44,221,3,9,50};
byte[] iv = {15,122,132,5,93,198,44,31,9,39,241,49,250,188,80,7};
byte[] decrypted = new byte[5];
using (SymmetricAlgorithm algorithm = Aes.Create())
using (ICryptoTransform decryptor = algorithm.CreateDecryptor (key, iv))
using (Stream f = File.OpenRead ("encrypted.bin"))
using (Stream c = new CryptoStream (f, decryptor, CryptoStreamMode.Read))
for (int b; (b = c.ReadByte()) > -1;)
Console.Write (b + " "); // 1 2 3 4 5
در مثال، کلیدی 16-byte از byteهای تصادفی فرضی ساخته شد. اگر کلید اشتباه برای رمزگشایی استفاده شود، CryptoStream یک CryptographicException پرتاب میکند و گرفتن همین exception تنها راه آزمون درستبودن کلید است.
علاوه بر کلید، یک IV یا Initialization Vector نیز وجود دارد. این دنبالهٔ 16-byte مانند کلید بخشی از cipher است، اما محرمانه محسوب نمیشود. هنگام انتقال پیام رمزشده میتوان IV را بهصورت plain text، مثلاً در header، ارسال کرد و برای هر پیام تغییر داد. در نتیجه حتی اگر دو پیام رمزنشده مشابه یا یکسان باشند، ciphertext آنها از یکدیگر قابل تشخیص نخواهد بود.
کار رمزنگاری میان کلاسها تقسیم شده است: Aes نقش «ریاضیدان» را دارد و الگوریتم cipher و transformهای encryptor/decryptor را اعمال میکند؛ CryptoStream نقش «لولهکش» را دارد و اتصال streamها را مدیریت میکند. میتوان AES را با الگوریتم متقارن دیگری جایگزین کرد و همچنان از CryptoStream استفاده نمود.
CryptoStream دوطرفه است و با توجه به انتخاب CryptoStreamMode.Read یا Write میتوان از آن خواند یا در آن نوشت. encryptor و decryptor هر دو با read و write کار میکنند و چهار ترکیب ایجاد میشود. برای تصور سادهتر، خواندن را «pull» و نوشتن را «push» در نظر بگیرید؛ در صورت تردید، برای encryption از Write و برای decryption از Read شروع کنید.
برای تولید key یا IV تصادفی از RandomNumberGenerator در فضای نام cryptography استفاده کنید. اعداد آن از نظر رمزنگاری غیرقابلپیشبینیاند؛ System.Random چنین تضمینی ندارد:
byte[] key = new byte [16];
byte[] iv = new byte [16];
RandomNumberGenerator rand = RandomNumberGenerator.Create();
rand.GetBytes (key);
rand.GetBytes (iv);
از .NET 6 به بعد:
byte[] key = RandomNumberGenerator.GetBytes (16);
byte[] iv = RandomNumberGenerator.GetBytes (16);
اگر key و IV را مشخص نکنید، مقادیر تصادفی cryptographically strong بهطور خودکار ساخته میشوند و از propertyهای Key و IV شیء AES قابل خواندن هستند.
رمزگذاری در حافظه
از .NET 6 میتوان برای کوتاهکردن فرایند رمز و رمزگشایی آرایههای byte از EncryptCbc و DecryptCbc استفاده کرد:
public static byte[] Encrypt (byte[] data, byte[] key, byte[] iv)
{
using Aes algorithm = Aes.Create();
algorithm.Key = key;
return algorithm.EncryptCbc (data, iv);
}
public static byte[] Decrypt (byte[] data, byte[] key, byte[] iv)
{
using Aes algorithm = Aes.Create();
algorithm.Key = key;
return algorithm.DecryptCbc (data, iv);
}
معادل سازگار با همهٔ نسخههای .NET:
public static byte[] Encrypt (byte[] data, byte[] key, byte[] iv)
{
using (Aes algorithm = Aes.Create())
using (ICryptoTransform encryptor = algorithm.CreateEncryptor (key, iv))
return Crypt (data, encryptor);
}
public static byte[] Decrypt (byte[] data, byte[] key, byte[] iv)
{
using (Aes algorithm = Aes.Create())
using (ICryptoTransform decryptor = algorithm.CreateDecryptor (key, iv))
return Crypt (data, decryptor);
}
static byte[] Crypt (byte[] data, ICryptoTransform cryptor)
{
MemoryStream m = new MemoryStream();
using (Stream c = new CryptoStream (m, cryptor, CryptoStreamMode.Write))
c.Write (data, 0, data.Length);
return m.ToArray();
}
در اینجا CryptoStreamMode.Write هم برای encryption و هم decryption مناسب است، زیرا در هر دو حالت داده را به یک MemoryStream تازه «push» میکنیم.
overloadهای رشتهای:
public static string Encrypt (string data, byte[] key, byte[] iv)
{
return Convert.ToBase64String (
Encrypt (Encoding.UTF8.GetBytes (data), key, iv));
}
public static string Decrypt (string data, byte[] key, byte[] iv)
{
return Encoding.UTF8.GetString (
Decrypt (Convert.FromBase64String (data), key, iv));
}
byte[] key = new byte[16];
byte[] iv = new byte[16];
var cryptoRng = RandomNumberGenerator.Create();
cryptoRng.GetBytes (key);
cryptoRng.GetBytes (iv);
string encrypted = Encrypt ("Yeah!", key, iv);
Console.WriteLine (encrypted); // R1/5gYvcxyR2vzPjnT7yaQ==
string decrypted = Decrypt (encrypted, key, iv);
Console.WriteLine (decrypted); // Yeah!
زنجیرهکردن Streamهای رمزنگاری
CryptoStream یک decorator است و میتوان آن را با streamهای دیگر زنجیره کرد. مثال زیر متن فشرده و رمزشده را در فایل مینویسد و دوباره میخواند:
byte[] key = new byte [16];
byte[] iv = new byte [16];
var cryptoRng = RandomNumberGenerator.Create();
cryptoRng.GetBytes (key);
cryptoRng.GetBytes (iv);
using (Aes algorithm = Aes.Create())
{
using (ICryptoTransform encryptor = algorithm.CreateEncryptor(key, iv))
using (Stream f = File.Create ("serious.bin"))
using (Stream c = new CryptoStream (f, encryptor, CryptoStreamMode.Write))
using (Stream d = new DeflateStream (c, CompressionMode.Compress))
using (StreamWriter w = new StreamWriter (d))
await w.WriteLineAsync ("Small and secure!");
using (ICryptoTransform decryptor = algorithm.CreateDecryptor(key, iv))
using (Stream f = File.OpenRead ("serious.bin"))
using (Stream c = new CryptoStream (f, decryptor, CryptoStreamMode.Read))
using (Stream d = new DeflateStream (c, CompressionMode.Decompress))
using (StreamReader r = new StreamReader (d))
Console.WriteLine (await r.ReadLineAsync()); // Small and secure!
}
در پایان برنامه با WriteLineAsync و ReadLineAsync و await کردن نتیجه asynchronous شده است. متغیرهای یکحرفی زنجیرهٔ stream را میسازند و algorithm/encryptor/decryptor به CryptoStream برای عملیات cipher کمک میکنند. این زنجیرهسازی فارغ از اندازهٔ نهایی stream حافظهٔ کمی مصرف میکند.
شکل 20-1. زنجیرهکردن streamهای رمزنگاری و فشردهسازی
Dispose کردن اشیای رمزنگاری
Dispose کردن CryptoStream تضمین میکند cache داخلی داده به stream زیرین flush شود. این cache لازم است چون الگوریتمهای رمزنگاری داده را بهصورت block پردازش میکنند نه byte به byte.
CryptoStream.Flush برخلاف انتظار کاری انجام نمیدهد. برای flush بدون dispose باید FlushFinalBlock فراخوانی شود؛ این متد فقط یک بار قابل فراخوانی است و پس از آن دادهٔ بیشتری نمیتوان نوشت.
اشیای Aes و ICryptoTransform نیز dispose میشوند. هنگام dispose شدن transformهای Rijndael، کلید متقارن و دادههای مرتبط از حافظه پاک میشوند تا نرمافزار دیگر، از جمله malware، نتواند بعداً آنها را کشف کند. Garbage Collector صرفاً حافظه را آزاد علامت میزند و همهٔ byteها را صفر نمیکند، بنابراین برای این کار کافی نیست.
خارج از using، سادهترین راه پاکسازی شیء AES فراخوانی Clear است. Dispose آن بهصورت explicit implementation پنهان شده است تا semantics غیرمعمول پاککردن حافظه را نشان دهد.
مدیریت کلید
مدیریت کلید بخش حیاتی امنیت است: اگر key افشا شود، داده نیز افشا شده است. باید تعیین کنید چه کسانی به کلید دسترسی داشته باشند، در خرابی سختافزار چگونه backup بگیرید و آن را چگونه طوری ذخیره کنید که دسترسی غیرمجاز ممکن نباشد.
hard-code کردن کلید رمزنگاری توصیه نمیشود، زیرا ابزارهای رایج میتوانند assembly را با تخصص اندکی decompile کنند. در Windows گزینهٔ بهتر ساخت key تصادفی برای هر نصب و نگهداری امن آن با Windows Data Protection است.
برای برنامههای cloud، Microsoft Azure و Amazon Web Services یا AWS سامانههای مدیریت کلید با امکانات سازمانی مانند audit trail ارائه میکنند. برای رمزگذاری stream پیام، رمزنگاری کلید عمومی همچنان گزینهای مهم است.
رمزنگاری کلید عمومی و امضای دیجیتال
رمزنگاری کلید عمومی نامتقارن است؛ یعنی encryption و decryption از کلیدهای متفاوت استفاده میکنند. برخلاف رمزنگاری متقارن که هر دنبالهٔ byte با طول مناسب میتواند key باشد، رمزنگاری نامتقارن به جفتکلیدهایی با ساختار خاص نیاز دارد. این جفت شامل public key و private key است:
- Public key پیامها را encrypt میکند.
- Private key پیامها را decrypt میکند.
سازندهٔ key pair کلید خصوصی را محرمانه نگه میدارد و کلید عمومی را آزادانه توزیع میکند. ویژگی مهم این است که private key از public key قابل محاسبه نیست. اگر کلید خصوصی گم شود، دادهٔ رمزشده قابل بازیابی نیست؛ اگر private key نشت کند، سیستم رمزنگاری عملاً بیاثر میشود.
یک handshake مبتنی بر public key به دو رایانه اجازه میدهد بدون تماس قبلی و بدون secret مشترک از پیش موجود، روی شبکهٔ عمومی امن ارتباط برقرار کنند. فرض کنید Origin میخواهد پیام محرمانهای برای Target بفرستد:
- Target یک جفت public/private key میسازد و public key را برای Origin میفرستد.
- Origin پیام محرمانه را با public keyِ Target رمز میکند و برای Target میفرستد.
- Target پیام را با private key خود رمزگشایی میکند.
استراقسمعکننده public keyِ Target و پیام رمزشده با آن را میبیند، اما بدون private key نمیتواند پیام را decrypt کند.
چون public-key encryption نسبتاً کند و اندازهٔ پیام آن محدود است، پیام ابتدایی معمولاً یک کلید تازه برای رمزنگاری متقارن بعدی حمل میکند. پس از تبادل این کلید، ادامهٔ session با الگوریتم متقارن سریعتر و مناسب پیامهای بزرگ انجام میشود. اگر برای هر session جفتکلید عمومی/خصوصی تازه ساخته شود، پروتکل امنتر است چون نیازی به ذخیرهٔ کلید روی هیچیک از دو رایانه نیست.
کلاس RSA
.NET چند الگوریتم نامتقارن دارد که RSA از رایجترین آنهاست:
byte[] data = { 1, 2, 3, 4, 5 }; // This is what we're encrypting.
using (var rsa = new RSACryptoServiceProvider())
{
byte[] encrypted = rsa.Encrypt (data, true);
byte[] decrypted = rsa.Decrypt (encrypted, true);
}
چون public/private key مشخص نشده، provider بهطور خودکار key pair میسازد. در متن منبع طول پیشفرض 1,024 bit ذکر شده و میتوان از constructor کلیدهای بلندتر در گامهای هشت byte خواست. برای برنامههای امنیتی، 2,048 bit توصیه شده است:
var rsa = new RSACryptoServiceProvider (2048);
ساخت key pair از نظر محاسباتی پرهزینه است و ممکن است حدود 10 ms طول بکشد؛ به همین دلیل RSA آن را تا اولین نیاز واقعی، مثلاً Encrypt، به تعویق میاندازد. این فرصت وجود دارد که پیش از ساخت جفتکلید جدید، کلید موجود بارگذاری شود.
ImportCspBlob و ExportCspBlob keyها را با قالب byte array بارگذاری و ذخیره میکنند. FromXmlString و ToXmlString همین کار را با fragment XML انجام میدهند. آرگومان bool تعیین میکند private key هنگام ذخیره شامل شود یا نه:
using (var rsa = new RSACryptoServiceProvider())
{
File.WriteAllText ("PublicKeyOnly.xml", rsa.ToXmlString (false));
File.WriteAllText ("PublicPrivate.xml", rsa.ToXmlString (true));
}
در مثال بعد public key فقط برای encryption و جفت public/private برای decryption استفاده میشود:
byte[] data = Encoding.UTF8.GetBytes ("Message to encrypt");
string publicKeyOnly = File.ReadAllText ("PublicKeyOnly.xml");
string publicPrivate = File.ReadAllText ("PublicPrivate.xml");
byte[] encrypted, decrypted;
using (var rsaPublicOnly = new RSACryptoServiceProvider())
{
rsaPublicOnly.FromXmlString (publicKeyOnly);
encrypted = rsaPublicOnly.Encrypt (data, true);
// decrypted = rsaPublicOnly.Decrypt (encrypted, true); // needs private key
}
using (var rsaPublicPrivate = new RSACryptoServiceProvider())
{
rsaPublicPrivate.FromXmlString (publicPrivate);
decrypted = rsaPublicPrivate.Decrypt (encrypted, true);
}
امضای دیجیتال
الگوریتمهای public key برای امضای دیجیتال پیام و سند نیز کاربرد دارند. signature شبیه hash است، با این تفاوت که تولید آن private key میخواهد و بنابراین قابل جعل نیست؛ public key برای verifyکردن signature استفاده میشود:
byte[] data = Encoding.UTF8.GetBytes ("Message to sign");
byte[] publicKey;
byte[] signature;
object hasher = SHA1.Create(); // Our chosen hashing algorithm.
using (var publicPrivate = new RSACryptoServiceProvider())
{
signature = publicPrivate.SignData (data, hasher);
publicKey = publicPrivate.ExportCspBlob (false); // get public key
}
using (var publicOnly = new RSACryptoServiceProvider())
{
publicOnly.ImportCspBlob (publicKey);
Console.Write (publicOnly.VerifyData (data, hasher, signature)); // True
data[0] = 0;
Console.Write (publicOnly.VerifyData (data, hasher, signature)); // False
signature = publicOnly.SignData (data, hasher); // throws: no private key
}
برای signing ابتدا داده hash و سپس الگوریتم نامتقارن روی hash اعمال میشود. چون hash اندازهٔ ثابت و کوچکی دارد، سندهای بزرگ نسبتاً سریع امضا میشوند؛ public-key encryption از hashing بسیار CPU-intensiveتر است. میتوانید hashing را خودتان انجام دهید و SignHash را بهجای SignData فراخوانی کنید:
using (var rsa = new RSACryptoServiceProvider())
{
byte[] hash = SHA1.Create().ComputeHash (data);
signature = rsa.SignHash (hash, CryptoConfig.MapNameToOID ("SHA1"));
...
}
SignHash همچنان باید الگوریتم hash را بداند و CryptoConfig.MapNameToOID نامی مانند SHA1 را به قالب لازم تبدیل میکند. RSACryptoServiceProvider signatureای هماندازهٔ key میسازد؛ متن منبع میگوید در حال حاضر الگوریتم mainstream امنی که signature بسیار کوچکتر از 128 byte تولید کند وجود ندارد.