Криптографски пъзел Импортиране на 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" в полето "секретна парола".