У меня есть программа С#, использующая System.Security.Cryptography (стандартный провайдер), которая должна генерировать ключи RSA определенного битового размера и экспоненты для взаимодействия с другой давней системой. Этот код кажется мне разумным:
for (int trix = 0; trix < 1000; trix++)
{
using (var rsa2 = new RSACryptoServiceProvider(1024)) // public key length in bits
{ // PROBLEM: MS seems stuck on the big exponent
RSAParameters key2 = rsa2.ExportParameters(true);
key2.Exponent = new byte[1] { 3 }; // public key exponent
rsa2.ImportParameters(key2);
PrintToFeedback(rsa2.ToXmlString(true));
byte[] bm0 = Utilities.HexStringToByteArray("1002030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
byte[] bm1 = rsa2.Encrypt(bm0, false);
byte[] bm2 = rsa2.Decrypt(bm1, false);
string szbm0 = Utilities.ByteArrayToHexString(bm0);
string szbm2 = Utilities.ByteArrayToHexString(bm2);
if (szbm0 != szbm2)
{
PrintToFeedback("RSA module test FAILED with MS RSA keys with small exponent, bm0, bm1, bm2 follow:");
PrintToFeedback(szbm0);
PrintToFeedback(Utilities.ByteArrayToHexString(bm1));
PrintToFeedback(szbm2);
ok = false;
break;
}
}
}
В большинстве случаев, но не всегда, я получаю исключение Bad Parameter для rsa2.ImportParameters с показателем 3. Иногда это работает, и у меня были запуски, когда rsa2.ToXmlString показывает экспоненту 3:
<Exponent>Aw==</Exponent>
>base64 -d | xxd
Aw==
00000000: 03
Тестовый цикл иногда дает сбой с ненулевым триксом, поэтому работает немного. Смотрите скриншот и этот пост в социальной сети MSDN от 2019 года
Как правильно получить 1024-битный ключ с показателем степени 3 из System.Security.Cryptography?
(отредактировано для добавления ссылки MSDN)
@Topaco, спасибо за это предложение. Программа уже использует много кода 3DES и Hash из .Net. Я думаю, что импорт BouncyCastle или OpenSSL заставит сильно переписать, потому что между этими библиотеками и System.Security.Cryptography возникает конфликт имен. Я не думаю, что библиотеки могут сосуществовать. Что касается согласованности данных, я полагаю, что ключ должен обновляться с новым показателем. В тех немногих случаях, когда для ImportParameters нет исключений, тест шифрования-дешифрования работает.
Анализ успешно сгенерированного ключа в синтаксическом анализаторе ASN.1 (например, lapo.it/asn1js) показывает, что был изменен только общедоступный показатель степени, а другие параметры не были скорректированы, поэтому данные в целом противоречивы. Проверка OpenSSL (openssl rsa -check -in <ключ>) этого ключа вернула Ошибка ключа RSA: d e не соответствует 1, подтверждая это предположение. Вы можете убедиться в этом самостоятельно с помощью упомянутых инструментов.
BouncyCastle обычно без проблем используется параллельно с нативными криптографическими реализациями .NET, но, конечно, я не могу сказать это точно для вашего приложения, так как я не знаю вашего приложения в деталях. Что касается перезаписи, вы можете использовать BC исключительно для генерации ключей. Нет смысла мигрировать остальные в БК.
Спасибо, Топако. Буду внимательно смотреть БК. Для протокола я поместил тест в блок try и записал все значения ключей, которые не вызывали исключений с показателем степени 3. Из 100 тестов я получил 26 ключей. Перебирая эти значения N, P, Q с показателем степени 65537, все, кроме одного, прошли тест шифрования-дешифрования. Так что, возможно, проблема не в «периодической», а в том, что «MS выдает исключение для ImportParameters, когда новый показатель степени не будет работать со старыми значениями ключа».
После изменения публичной экспоненты остальные зависимые компоненты (а именно P
, Q
, Modulus
, D
, DP
, DQ
, InverseQ
) ключа также должны быть скорректированы. Для этого определенно лучше использовать специализированные инструменты, например. БаунсиКасл.
В чистом С# вы можете сделать что-то вроде:
public static void Main()
{
using (var rsa = new RSACryptoServiceProvider(1024))
{
RSAParameters key = rsa.ExportParameters(true);
// new public exponent
BigInteger e = 3;
// update public exponent and adjust dependent components
UpdatePublicExponent(ref key, e);
rsa.ImportParameters(key);
Console.WriteLine(rsa.ToXmlString(true));
byte[] bm0 =
HexStringToByteArray(
"1002030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
byte[] bm1 = rsa.Encrypt(bm0, false);
byte[] bm2 = rsa.Decrypt(bm1, false);
Console.WriteLine(bm0.SequenceEqual(bm2));
}
}
Вот UpdatePublicExponent
с другими вспомогательными функциями:
private static void UpdatePublicExponent(ref RSAParameters key, BigInteger e)
{
int keyBytes = key.Modulus?.Length ?? 128;
int keyBitLength = 8 * keyBytes;
int pBitLength = (keyBitLength + 1) / 2;
int qBitLength = keyBitLength - pBitLength;
int minDiffBits = keyBitLength / 3;
for (;;)
{
BigInteger p = GetRandomPrime(pBitLength, e);
BigInteger q, n;
for (;;)
{
q = GetRandomPrime(qBitLength, e);
// p and q should not be too close together (or equal!)
BigInteger diff = BigInteger.Abs(q - p);
if (diff.GetBitLength() < minDiffBits)
{
continue;
}
// calculate the modulus
n = p * q;
if (n.GetBitLength() != keyBitLength)
{
// if we get here our primes aren't big enough, make the largest
// of the two p and try again
p = BigInteger.Max(p, q);
continue;
}
break;
}
if (p < q)
{
BigInteger tmp = p;
p = q;
q = tmp;
}
BigInteger pSub1 = p - 1;
BigInteger qSub1 = q - 1;
BigInteger gcd = BigInteger.GreatestCommonDivisor(pSub1, qSub1);
BigInteger lcm = pSub1 / gcd * qSub1;
// calculate the private exponent
BigInteger d = ModInverse(e, lcm);
if (d.GetBitLength() <= qBitLength)
{
continue;
}
// calculate the CRT factors
BigInteger dP = d % pSub1;
BigInteger dQ = d % qSub1;
BigInteger invQ = ModInverse(p, q);
int halfBytes = (keyBytes + 1) / 2;
// update key components
key.P = GetBytes(p, halfBytes);
key.Q = GetBytes(q, halfBytes);
key.Modulus = GetBytes(n, keyBytes);
key.Exponent = GetBytes(e, -1);
key.D = GetBytes(d, keyBytes);
key.DP = GetBytes(dP, halfBytes);
key.DQ = GetBytes(dQ, halfBytes);
key.InverseQ = GetBytes(invQ, halfBytes);
break;
}
}
private static BigInteger ModInverse(BigInteger a, BigInteger n)
{
BigInteger i = n, v = 0, d = 1;
while (a > 0)
{
BigInteger t = i / a, x = a;
a = i % x;
i = x;
x = d;
d = v - t * x;
v = x;
}
v %= n;
if (v < 0) v = (v + n) % n;
return v;
}
private static BigInteger GetRandomPrime(int bitCount, BigInteger e)
{
BigInteger prime;
RandomNumberGenerator rng = RandomNumberGenerator.Create();
int byteLength = (bitCount + 7) / 8;
do
{
byte[] bytes = new byte[byteLength];
rng.GetBytes(bytes);
prime = new BigInteger(bytes, true);
} while (prime.GetBitLength() != bitCount || prime % e == BigInteger.One || !IsProbablePrime(prime, 40));
rng.Dispose();
return prime;
}
// Miller-Rabin primality test as an extension method on the BigInteger type.
// http://rosettacode.org/wiki/Miller%E2%80%93Rabin_primality_test#C.23
private static bool IsProbablePrime(BigInteger source, int certainty)
{
if (source == 2 || source == 3)
return true;
if (source < 2 || source % 2 == 0)
return false;
BigInteger d = source - 1;
int s = 0;
while (d % 2 == 0)
{
d /= 2;
s += 1;
}
// There is no built-in method for generating random BigInteger values.
// Instead, random BigIntegers are constructed from randomly generated
// byte arrays of the same length as the source.
RandomNumberGenerator rng = RandomNumberGenerator.Create();
byte[] bytes = new byte[source.ToByteArray().LongLength];
BigInteger a;
for (int i = 0; i < certainty; i++)
{
do
{
// This may raise an exception in Mono 2.10.8 and earlier.
// http://bugzilla.xamarin.com/show_bug.cgi?id=2761
rng.GetBytes(bytes);
a = new BigInteger(bytes);
}
while (a < 2 || a >= source - 2);
BigInteger x = BigInteger.ModPow(a, d, source);
if (x == 1 || x == source - 1)
continue;
for (int r = 1; r < s; r++)
{
x = BigInteger.ModPow(x, 2, source);
if (x == 1)
return false;
if (x == source - 1)
break;
}
if (x != source - 1)
return false;
}
return true;
}
private static byte[] GetBytes(BigInteger value, int size = -1)
{
byte[] bytes = value.ToByteArray();
if (size == -1)
{
size = bytes.Length;
}
if (bytes.Length > size + 1)
{
throw new InvalidOperationException($"Cannot squeeze value {value} to {size} bytes from {bytes.Length}.");
}
if (bytes.Length == size + 1 && bytes[bytes.Length - 1] != 0)
{
throw new InvalidOperationException($"Cannot squeeze value {value} to {size} bytes from {bytes.Length}.");
}
Array.Resize(ref bytes, size);
Array.Reverse(bytes);
return bytes;
}
public static byte[] HexStringToByteArray(string hex)
{
byte[] bytes = new byte[hex.Length / 2];
int[] hexValue = new int[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F };
for (int x = 0, i = 0; i < hex.Length; i += 2, x += 1)
{
bytes[x] = (byte) (hexValue[char.ToUpper(hex[i + 0]) - '0'] << 4 | hexValue[char.ToUpper(hex[i + 1]) - '0']);
}
return bytes;
}
руслан, какую реализацию BigInteger вы используете? Спасибо за код!
Я только что понял, что исходный код не работает. Обновил, но оказалось, что работает только в 1/4 случаях. Я использую стандартный System.Numerics.BigInteger
Я все еще получаю исключения, вероятно, из-за моего переноса вашего кода в мою систему. Я использую старый BigInteger из CodeProject с другой семантикой. Сейчас я буду перебирать блок try-catch, пока .Net не даст мне ключ, который работает с показателем степени 3. Удар по скорости пока не проблема.
ну теперь я вижу проблему. Дело в том, что публичный показатель e
должен быть выбран таким образом, чтобы выполнялись следующие условия: 1 < e < ϕ
и gcd(e, ϕ) = 1
, где ϕ = (p-1)(q-1)
. Поэтому, когда вы меняете e
, вам также нужно настроить p
и q
. Подробнее см. в di-mgt.com.au/rsa_alg.html#keygen.
Я отредактировал код. Теперь он пересчитывает все ключевые компоненты.
Я не могу найти в документации никаких указаний на то, что спецификация общедоступной экспоненты поддерживается. И простой экспорт параметров, изменение общедоступной экспоненты и импорт параметров обычно должны приводить к противоречивым данным. Я бы использовал BouncyCastle для этой задачи. Эта библиотека позволяет вам сгенерировать ключ RSA, указав размер и общедоступную экспоненту.