Криптографски пъзел Импортиране на WebMoney ключ към доставчик на крипто услуги
Частните ключове в система Windows обикновено се съхраняват в специално хранилище за ключове. Работата с тези ключове се извършва чрез извикване на функциите на криптографския доставчик (наричан по-долу CSP). Когато използвате стандартния CSP (Microsoft Base Cryptographic Provider), потребителските ключове се съхраняват в папката C:\Users\[Vasia]\AppData\Roaming\Microsoft\Crypto. При използване на специални устройства, ключовете се съхраняват в паметта на самото устройство.
За да се подобри сигурността, беше решено да се импортира ключът WebMoney (същият .kwm, използван за подписване на заявки към интерфейси) в CSP.Обикновено тези, които използват ключа за подписване на заявки към WM интерфейси, го съхраняват или като .kwm файл във файловата система, или като xml представяне - и двете опции не са много сигурни.
Оказа се, че не е толкова лесно.
Така. Нека започнем с файловия формат kwm. Файлът има неусложнен формат (липсват малки неща):
1. Хеш за проверка на целостта. 2. Криптиран частен RSA експонент (D). З. Криптиран RSA модул (Modulus).
Проблем № 1:след дешифриране на ключовия файл, получаваме само 2 от 8-те параметъра, от които се нуждаем.
Имаме: D и модул, имаме нужда от: експонента, модул, P, Q, DP, DQ, обратен Q, D (за подробности относно структурата, към която трябва да пренесете ключа за импортиране в CSP, вижте MSDN msdn.microsoft.com/en-us/library/Aa387401).
Ако знаехте E (отворен експонент), нямаше да е трудно да намерите всички тези параметри. Обикновено E приема едно от числата на Ферма: 17, 257, 65537 или 4294967297. Проверява се много лесно:
1. Ние шифроваме произволно съобщение с нашите параметри D и Modulus:
шифровано = съобщение^D % Модул
2.Опит за дешифриране с експонента (приема се) и модул:
съобщение = криптирано^Експонент % Модул
Ако съобщението, получено след дешифрирането, съвпада с оригиналното, тогава експонентът е избран правилно.
Забележка: по-нататък операторът "^" е степенуване, а операторът "%" е остатъкът от делението.
Проблем №2:Не знаем отворения показател, а той е доста голям.
За съжаление нито едно от тези числа на Ферма не се появи като отворен показател. Това е малко разочароващо. Освен това, надявайки се, че това число все още не е твърде голямо, всички числа под 4 байта бяха изпробвани (чрез груба сила). Никой от тях не пасва. Изглежда задънена улица и би било възможно да забравим за тази идея ...
Това може да е краят на всичко. Не беше възможно да се получи отворен показател: груба сила за няколко десетки (или дори стотици) години.
Историята обаче продължава. Имало едно време човек катоMichael J. Wiener, който измислил начин да разбие системата RSA в случай на малка стойност на d. Имаме малко обратното: малка стойност на отворения показател, която не знаем.
Наистина, като приложите атаката на Wiener към RSA, можете почти моментално да намерите публичния експонент на всеки ключ (ако не е много дълъг).
Ето какво се случи:
Оригинални стойности на D и модул (получени от kwm файла):
Отворен експонент, който беше незабавно възстановен с помощта на атаката на Wiener срещу RSA:
Изглежда, че въпросът е малък: пренесете получените параметри в структурата на BLOB на частния ключ и ги импортирайте във всеки доставчик на крипто. Но не е толкова просто...
Проблем #3:публичният показател е по-дълъг от 4 байта, а форматът BLOB на частния ключ позволява само показатели до 4 байтадължина (4 байта включително).
Тук е бедата! Изглежда, че проблемът няма решение и можете да забравите за него (за втори път).
Въпреки че ... В крайна сметка имаме параметрите на криптосистемата (по-специално простите числа p и q). Следователно можете да направите това:
1. Взимаме първоначалните параметри P и Q.
2. Изберете подходящия публичен експонент Exponent2 (не повече от 4 байта). Вземете числото на Ферма 65537.
3. Изчислете затворената експонента D2 (умножено обратно на числото e по модул (P-1)* (Q-1)). D2 ще бъде различен от оригиналния D от kwm файла.
4. Изчислете параметрите: DP2, DQ2, InverseQ2. Те са необходими за бързо получаване на подпис, като се използва китайската теорема за остатъка.
5. Най-важното! Ние изчисляваме разликата между оригиналния D (който е в оригиналния kwm ключ) и D2 на нашата модифицирана криптосистема (т.е. D2 - D).
Сега модифицираният ключ може да се съхранява в хранилището на CSP (например eToken PRO) като RSA частен ключ, а отклонението може да се съхранява като обект PKCS#11. Когато използвате Microsoft CSP, отклонението може да се съхранява в хранилището на потребителя.
По принцип стойността на отклонението не е тайна, нашият модифициран ключ (който се съхранява в CSP) вече е секретен.
Ето пример за C#, който отговаря за импортирането на модифицирания ключ в CSP:
public void ImportPrivateKey( byte [] modulusBytes, byte [] privateExponentBytes) // Пропускане на проверката на аргумент
var modulus = new BigInteger(modulusBytes); var d = new BigInteger(privateExponentBytes);
var p = Wiener.Calculate(d, модул); // прилагане на атаката на Wiener към RSA и намиране на P (по затворен експонент и модул) var q = modulus/p; вар. f = (p - 1)*(q - 1); // Функция на Ойлер var e = new BigInteger( new byte [] ); // 65537
var d2 = e.modInverse(f); // d2 -- модифициран таен експонент
vardp2 = d2%(p - 1); vardq2 = d2%(q - 1); var iq = q.modInverse(p);
var rsaParameters = new RSAParameters D = toByteArray(d2), DP = toByteArray(dp2), DQ = toByteArray(dq2), Exponent = toByteArray(e), InverseQ = toByteArray(iq), Modulus = toByteArray(modulus), P = toByteArray(p), Q = toByteArray(q) >;
BigInteger отклонение; байтов флаг; // ако d > d2, тогава флагът е зададен
if (d > d2) отклонение = d - d2; флаг = 0; > друго отклонение = d2 - d; флаг = 1; >
// Импортирайте нашия ключ в CSP (за да съхраните в eToken, посочете името на CSP „eToken Base Cryptographic Provider“) var cspParameters = new CspParameters Flags = CspProviderFlags.UseNonExportableKey CspProviderFlags.UseUserProtectedKey, KeyNumber = ( int )KeyNumber .Exchange, KeyContainerName = _име на контейнер >;
// Използване на потребителско хранилище RSACryptoServiceProv >false ;
използване ( var cryptoServiceProv > new RSACryptoServiceProvider(cspParameters)) // импортиране на модифицирания ключ cryptoServiceProvider.ImportParameters(rsaParameters); >
// Съхранявайте отклонението в потребителското хранилище (не е тайно) Storage.SaveDataInStorage(_containerName, toByteArray(deviation)); Storage.SaveDataInStorage(_containerName + "_flag" , new [] ); >
* Този изходен код беше подчертан с инструмента за открояване на изходния код.
Как сега да получа подписа на съобщението?
Оригиналният подпис изглежда така:
подпис = хеш ^ D % Модул
Сега нямаме D, но имаме D2 и отклонение и D2 + отклонение = D. Директен достъп до D2липсва, защото той се поддържа от CSP: човек може да получи подпис от този D2 само чрез извикване на стандартни функции. Да си припомним алгебрата:
Този закон работи и в модулната алгебра. Така че нашият оригинален подпис (без да знаем D) е такъв:
част1 = хеш ^ D2 % модул
part2 = хеш ^ отклонение % модул
подпис = част1 * част2 % модул
печалба! Сега нашият ключ се поддържа от CSP (в случай на хардуерно устройство това е доста надеждно). Малко неудобство - цялото „не се вписваше“, все още имаше „парче“ под формата на отклонение. Но това отклонение не е тайна, без познаване на D2 то не дава почти никаква представа за оригиналния D.
Но не бързайте да се отпуснете.
Проблем #4:Структурата на подписаните данни се различава от общоприетата.
Ако дешифрираме подписа, получен чрез Win CAPI (можете да го дешифрирате с публичен изложител и модул), ще видим нещо като тази структура:
в началото е стандартен хедър, а в края е 32 байта от хеша на подписано съобщение (водено за SHA-256).
Сигнатурата на WebMoney Signer е различна: първите байтове се пълнят с произволни числа, след това 16 байта от MD4 хеша, след това заглавка от 2 байта.
Проблемът е, че CSP не поддържа директната функция на модулно степенуване с частен ключ (ако беше възможно, всичко щеше да се получи). Само:
1. Подпишете съобщението. В ясна форма не ни устройва по никакъв начин, т.к WebMoney няма да разпознае нашия подпис - все пак структурата е различна.
2. Шифровайте съобщението. Отново - криптирането е странно (оригиналното съобщение е модифицирано, допълнено с произволни числа) - то не съвпада с нашия формат по никакъв начин.
Отново безизходица. Въпреки че ... И какво ще стане, ако не изпуснем нашия CSPреален хеш и псевдохеш, в който ще поберем данните във формата, от който се нуждаем? Хеш функцията не е обратима, така че няма начин да се провери дали хешът е реален.
За да направим това, трябва да изберем алгоритъм, който има достатъчно дълъг хеш: така че нашият 16-байтов MD4 да може да се побере, и 2-байтов хедър, и би било хубаво, за сигурност, да допълним данните с произволни числа.
SHA-512 се оказа твърде голям, но SHA-256 е точно.
Ето получената функция за генериране на подпис:
public string Sign( string value ) if ( string .IsNullOrEmpty( value )) throw new ArgumentNullException( "value" );
var toSign = нов байт[32]; // Псевдо хеш SHA256
var random = нов байт[14]; // произволни числа var hash = getHash( value ); // хеш на нашето MD4 съобщение var prefix = нов байт [] ; // Заглавие на WebMoney
var rngCryptoServiceProv >нов RNGCryptoServiceProvider(); rngCryptoServiceProvider.GetBytes(произволен);
Buffer.BlockCopy(random, 0, toSign, 0, random.Length); Buffer.BlockCopy(hash, 0, toSign, random.Length, hash.Length); Buffer.BlockCopy(prefix, 0, toSign, random.Length + hash.Length, prefix.Length);
var cspParameters = new CspParameters Flags = CspProviderFlags.UseNonExportableKey CspProviderFlags.UseUserProtectedKey, KeyNumber = ( int ) KeyNumber.Exchange, KeyContainerName = _containerName >;
байт [] подпис1; Модул BigInteger;
с помощта на ( var rsaCryptoServiceProv >new RSACryptoServiceProvider(528, cspParameters)) signature1 = rsaCryptoServiceProv >"SHA256" )); modulus = new BigInteger(rsaCryptoServiceProv >false).Modulus); >
var deviation = new BigInteger(Storage.LoadDataFromStorage(_containerName));
// стандартный заголовок SHA-256 var header = нов байт [] 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x30, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 >;
var toEncrypt = нов байт [65];
Buffer.BlockCopy(header, 0, toEncrypt, 0, header.Length); Buffer.BlockCopy(random, 0, toEncrypt, header.Length, random.Length); Buffer.BlockCopy(hash, 0, toEncrypt, header.Length + random.Length, hash.Length); Buffer.BlockCopy(prefix, 0, toEncrypt, header.Length + random.Length + hash.Length, prefix.Length);
байт флаг = Storage.LoadDataFromStorage(_containerName + "_flag" )[0];
var part1 = new BigInteger(signature1); BigInteger part2;
if (1 == флаг) // Инверсия -- в проекции на училищната алгебра -- това деление part2 = new BigInteger(toEncrypt).modInverse(modulus).modPow(deviation, modulus); > друго part2 = нов BigInteger(toEncrypt).modPow(отклонение, модул);
променлив подпис = toByteArray((part1*part2)%modulus);
// Превеждам в little-endian Array.Reverse(подпис);
// Привеждам към строковия вид var uResult = new ushort [KeyBytesLength/2];
Buffer.BlockCopy(подпис, 0, uResult, 0, подпис.Дължина);
var stringBuilder = нов StringBuilder ();
for ( int pos = 0; pos string .Format(CultureInfo.InvariantCulture, "", uResult[pos]));
* Този изходен код беше подчертан с инструмента за открояване на изходния код.
Сега всичко работи като часове: ключът е съвместим с всяко криптографско устройство, работещо в системата на Windows (известен още като eToken, ruToken — което е удобно), подписът е валиден.
Възможно кому-то сложно ще бъде понят, защо яне избра лесния път. Да поясня: обичам криптографски пъзели. Някой решава кръстословици, но аз предпочитам пъзели в тази форма.
И тук можете да получите грант за тестов период на Yandex.Cloud. Необходимо е само да въведете "Habr" в полето "секретна парола".