Правильно создать RSACryptoServiceProvider из открытого ключа

В настоящее время я пытаюсь создать объект RSACryptoServiceProvider исключительно из декодированного файла PEM. После нескольких дней поисков мне удалось найти работающее решение, но оно не было готово к производству.

В двух словах, чтобы создать объект RSACryptoServiceProvider из байтов, составляющих открытый ключ в файле PEM, я должен создать объект, указав размер ключа (в настоящее время 2048 с использованием SHA256, в частности), а затем импортировать объект RSAParameters с Exponent и Modulus набор. Я делаю это так;

byte[] publicKeyBytes = Convert.FromBase64String(deserializedPublicKey.Replace("-----BEGIN PUBLIC KEY-----", "")
                                                                      .Replace("-----END PUBLIC KEY-----", ""));

// extract the modulus and exponent based on the key data
byte[] exponentData = new byte[3];
byte[] modulusData = new byte[256];
Array.Copy(publicKeyBytes, publicKeyBytes.Length - exponentData.Length, exponentData, 0, exponentData.Length);
Array.Copy(publicKeyBytes, 9, modulusData, 0, modulusData.Length);


// import the public key data (base RSA - works)
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(dwKeySize: 2048);
RSAParameters rsaParam = rsa.ExportParameters(false);
rsaParam.Modulus = modulusData;
rsaParam.Exponent = exponentData;
rsa.ImportParameters(rsaParam);

Хотя это работает, нецелесообразно предполагать, что deserializedPublicKey будет ровно 270 байт и что нужный мне модуль находится в позиции 9 и всегда имеет длину 256 байт.

Как изменить это, чтобы правильно выбрать байт модуля и экспоненты с учетом набора байтов открытого ключа? Я пытался разобраться в стандарте ASN.1, но с небольшой удачей нашел то, что мне нужно от него - стандарт(ы) несколько византийские.

Любая помощь приветствуется.


person DiskJunky    schedule 23.01.2017    source источник


Ответы (3)


Вам не нужно экспортировать существующие параметры, а затем повторно импортировать их поверх них. Это заставляет вашу машину генерировать ключ RSA, а затем выбрасывать его. Таким образом, указание размера ключа для конструктора не имеет значения (если вы не используете ключ, он не будет его генерировать... обычно).

Файл открытого ключа представляет собой большой двоичный объект в кодировке DER.

-----BEGIN PUBLIC KEY-----
MIGgMA0GCSqGSIb3DQEBAQUAA4GOADCBigKBggC8rLGlNJ17NaWArDs5mOsV6/kA
7LMpvx91cXoAshmcihjXkbWSt+xSvVry2w07Y18FlXU9/3unyYctv34yJt70SgfK
Vo0QF5ksK0G/5ew1cIJM8fSxWRn+1RP9pWIEryA0otCP8EwsyknRaPoD+i+jL8zT
SEwV8KLlRnx2/HYLVQkCAwEAAQ==
-----END PUBLIC KEY-----

Если вы возьмете содержимое внутри брони PEM, это массив байтов в кодировке Base64.

30 81 A0 30 0D 06 09 2A 86 48 86 F7 0D 01 01 01 
05 00 03 81 8E 00 30 81 8A 02 81 82 00 BC AC B1 
A5 34 9D 7B 35 A5 80 AC 3B 39 98 EB 15 EB F9 00 
EC B3 29 BF 1F 75 71 7A 00 B2 19 9C 8A 18 D7 91 
B5 92 B7 EC 52 BD 5A F2 DB 0D 3B 63 5F 05 95 75 
3D FF 7B A7 C9 87 2D BF 7E 32 26 DE F4 4A 07 CA 
56 8D 10 17 99 2C 2B 41 BF E5 EC 35 70 82 4C F1 
F4 B1 59 19 FE D5 13 FD A5 62 04 AF 20 34 A2 D0 
8F F0 4C 2C CA 49 D1 68 FA 03 FA 2F A3 2F CC D3 
48 4C 15 F0 A2 E5 46 7C 76 FC 76 0B 55 09 02 03 
01 00 01 

ITU-T X.690 определяет, как читать элементы, закодированные в соответствии с базовыми Правила кодирования (BER), правила канонического кодирования (CER, которые я никогда не видел в явном виде) и особые правила кодирования (DER). По большей части CER ограничивает BER, а DER ограничивает CER, что делает DER наиболее простым для чтения. (ITU-T X.680 описывает абстрактную синтаксическую нотацию один (ASN .1), которая представляет собой грамматику, для которой DER является двоичным кодированием)

Теперь мы можем сделать небольшой разбор:

30

Это идентифицирует ПОСЛЕДОВАТЕЛЬНОСТЬ (0x10) с установленным битом CONSTRUCTED (0x20), что означает, что она содержит другие значения DER/помеченные. (ПОСЛЕДОВАТЕЛЬНОСТЬ всегда КОНСТРУКТИРУЕТСЯ в DER)

81 A0

Эта следующая часть является длиной. Поскольку у него установлен старший бит (> 0x7F), первый байт является значением «длина длины». Это указывает на то, что истинная длина закодирована в следующем 1 байте (байтах) (lengthLength & 0x7F). Таким образом, содержимое этой ПОСЛЕДОВАТЕЛЬНОСТИ составляет всего 160 байтов. (В данном случае «остальные данные», но ПОСЛЕДОВАТЕЛЬНОСТЬ могла содержаться в чем-то другом). Итак, читаем содержимое:

30 0D

Мы снова видим нашу CONSTRUCTED SEQUENCE (0x30) со значением длины 0x0D, поэтому у нас есть полезная нагрузка размером 13 байт.

06 09 2A 86 48 86 F7 0D 01 01 01 05 00 

06 – это ОБЪЕКТНЫЙ ИДЕНТИФИКАТОР с полезной нагрузкой _11 байт. OID имеет несколько неинтуитивную кодировку, но она эквивалентна текстовому представлению 1.2.840.113549.1.1.1, которое равно id-rsaEncryption (http://www.oid-info.com/get/1.2.840.113549.1.1.1).

Это по-прежнему оставляет нам два байта (05 00), которые, как мы видим, являются NULL (с ​​полезной нагрузкой 0 байтов, потому что это NULL).

Так что пока у нас есть

SEQUENCE
  SEQUENCE
    OID 1.2.840.113549.1.1.1
    NULL
  143 more bytes.

Продолжение:

03 81 8E 00

03 означает БИТОВАЯ СТРОКА. BIT STRING кодируется как [тег] [длина] [количество неиспользуемых битов]. Неиспользуемые биты по существу всегда равны нулю. Итак, это последовательность битов длиной 0x8E байт, и все они используются.

Технически мы должны остановиться на этом, потому что CONSTRUCTED не был установлен. Но поскольку нам известен формат этой структуры, мы обрабатываем значение так, как если бы бит CONSTRUCTED был установлен в любом случае:

30 81 8A

Вот наш друг CONSTRUCTED SEQUENCE снова, 0x8A байт полезной нагрузки, что удобно соответствует «всему, что осталось».

02 81 82

02 идентифицирует INTEGER, и у этого есть 0x82 байт полезной нагрузки:

00 BC AC B1 A5 34 9D 7B 35 A5 80 AC 3B 39 98 EB 
15 EB F9 00 EC B3 29 BF 1F 75 71 7A 00 B2 19 9C 
8A 18 D7 91 B5 92 B7 EC 52 BD 5A F2 DB 0D 3B 63 
5F 05 95 75 3D FF 7B A7 C9 87 2D BF 7E 32 26 DE 
F4 4A 07 CA 56 8D 10 17 99 2C 2B 41 BF E5 EC 35 
70 82 4C F1 F4 B1 59 19 FE D5 13 FD A5 62 04 AF 
20 34 A2 D0 8F F0 4C 2C CA 49 D1 68 FA 03 FA 2F 
A3 2F CC D3 48 4C 15 F0 A2 E5 46 7C 76 FC 76 0B 
55 09 

Начальный 0x00 будет нарушением DER, за исключением того, что в следующем байте установлен старший бит. Это означает, что 0x00 был там, чтобы предотвратить установку бита знака, что делает это число положительным.

02 03 01 00 01

Другой INTEGER, 3 байта, значение 01 00 01. И мы закончили.

SEQUENCE
  SEQUENCE
    OID 1.2.840.113549.1.1.1
    NULL
  BIT STRING
    SEQUENCE
      INTEGER 00 BC AC ... 0B 55 09
      INTEGER 01 00 01

Собрав https://tools.ietf.org/html/rfc5280, мы видим, что это очень похоже на структуру SubjectPublicKeyInfo:

SubjectPublicKeyInfo  ::=  SEQUENCE  {
  algorithm            AlgorithmIdentifier,
  subjectPublicKey     BIT STRING  }

AlgorithmIdentifier  ::=  SEQUENCE  {
  algorithm               OBJECT IDENTIFIER,
  parameters              ANY DEFINED BY algorithm OPTIONAL  }
                            -- contains a value of the type
                            -- registered for use with the
                            -- algorithm object identifier value

Конечно, он не знает, что такое формат открытого ключа RSA. Но сайт oid-info посоветовал нам проверить RFC 2313, где мы видим

An RSA public key shall have ASN.1 type RSAPublicKey:

RSAPublicKey ::= SEQUENCE {
  modulus INTEGER, -- n
  publicExponent INTEGER -- e }

Таким образом, это говорит о том, что первое INTEGER, которое мы читаем, является значением модуля, а второе — (общедоступным) показателем степени.

Кодировка DER имеет обратный порядок байтов, что также является кодировкой RSAParameters, но для RSAParameters вам необходимо удалить начальные значения 0x00 из модуля.

Хотя это не так просто, как дать вам код для этого, должно быть довольно просто написать синтаксический анализатор для ключей RSA с учетом этой информации. Я бы порекомендовал вам написать его как internal static RSAParameters ReadRsaPublicKey(...), а затем вам просто нужно сделать

RSAParameters rsaParameters = ReadRsaPublicKey(...);

using (RSA rsa = RSA.Create())
{
    rsa.ImportParameters(rsaParameters);
    // things you want to do with the key go here
}
person bartonjs    schedule 24.01.2017
comment
github.com/sevenTiny/ Метод SevenTiny.Bantina/blob/ =› CreateRsaProviderFromPublicKey выполняет свою работу! - person Mathieu Momal; 21.06.2018
comment
В популярной библиотеке Bouncy Castle также есть реализация для получения RSAParameters. DotNetUtilities.ToRSAParameters(... - person MattWazEre; 24.09.2020
comment
За 4 года, прошедших с момента этого ответа, поддержка этого также была только что встроена: key.ImportSubjectPublicKeyInfo(derBytes, out int bytesRead). Но ответ по-прежнему объясняет, что делает этот метод. - person bartonjs; 24.09.2020

После долгого поиска и выдающегося ответа bartonjs код для этого на самом деле является прямым в конце хотя и немного неинтуитивно для тех, кто не знаком со структурой открытого ключа.

TL;DR По сути, если ваш открытый ключ исходит из источника, отличного от .NET, этот ответ не поможет, поскольку .NET не предоставляет встроенного способа анализа правильно сформированного PEM. Однако, если код, сгенерировавший PEM, основан на .NET, то в этом ответе описывается создание PEM только с открытым ключом и как загрузить его обратно.

Открытый ключ PEM может описывать различные типы ключей, а не только RSA, поэтому вместо чего-то вроде new RSACryptoServiceProvider(pemBytes) мы должны анализировать PEM на основе его структуры/синтаксиса, ASN.1, и затем он сообщает нам, является ли это ключом RSA ( это может быть ряд других). Знаю это;

const string rsaOid = "1.2.840.113549.1.1.1";   // found under System.Security.Cryptography.CngLightup.RsaOid but it's marked as private
Oid oid = new Oid(rsaOid);
AsnEncodedData keyValue = new AsnEncodedData(publicKeyBytes);           // see question
AsnEncodedData keyParam = new AsnEncodedData(new byte[] { 05, 00 });    // ASN.1 code for NULL
PublicKey pubKeyRdr = new PublicKey(oid, keyParam, keyValue);
var rsaCryptoServiceProvider = (RSACryptoServiceProvider)pubKeyRdr.Key;

ПРИМЕЧАНИЕ. Приведенный выше код не готов к работе! Вам нужно будет поставить соответствующие охранники вокруг создания объекта (например, открытый ключ может не быть RSA), приведения к RSACryptoServiceProvider и т. д. Пример кода здесь короткий, чтобы проиллюстрировать, что это можно сделать достаточно чисто.

Как я это понял? Просматривая пространство имен Cryptographic в ILSpy, я заметил AsnEncodedData, который звонил в колокол с описанием bartonjs. Проводя дополнительные исследования, я наткнулся на этот пост (выглядит знакомо?). Это было попыткой определить размер ключа конкретно, но по пути он создает необходимые RSACryptoServiceProvider.

Я оставляю ответ bartonjs принятым, и это правильно. Приведенный выше код является результатом этого исследования, и я оставлю его здесь, чтобы другие, желающие сделать то же самое, могли сделать это чисто без каких-либо хаков с копированием массива, как это было у меня в моем ОП.

Кроме того, в целях декодирования и тестирования вы можете проверить, поддается ли анализу ваш открытый ключ с помощью декодера ASN.1 здесь.

ОБНОВЛЕНИЕ

Планируется, что .NET упростит этот процесс с помощью Разбор ASN.1 для ядра >2.1.0.

ОБНОВЛЕНИЕ 2

Теперь в Core .NET 2.1.1 есть частная реализация. MS проводит тестирование до тех пор, пока не убедится, что все в порядке, и мы (надеемся) увидим общедоступный API в следующей версии.

ОБНОВЛЕНИЕ 3

Как я узнал из вопроса здесь, приведенная выше информация неполная. Чего не хватает, так это того, что открытый ключ, загружаемый с помощью этого решения, был сгенерирован программно из загруженной пары открытого и закрытого ключей. После создания RSACryptoServiceProvider из пары ключей (а не только открытого ключа) вы можете экспортировать только открытые байты и закодировать их как открытый ключ PEM. Это будет совместимо с решением здесь. Что с этим?

Загрузите пару открытый + закрытый ключ в RSACryptoServiceProvider, а затем экспортируйте ее следующим образом;

var cert = new X509Certificate2(keypairBytes, password,
                                X509KeyStorageFlags.Exportable 
                                | X509KeyStorageFlags.MachineKeySet);
var partialAsnBlockWithPublicKey = cert.GetPublicKey();

// export bytes to PEM format
var base64Encoded = Convert.ToBase64String(partialAsnBlockWithPublicKey, Base64FormattingOptions.InsertLineBreaks);
var pemHeader = "-----BEGIN PUBLIC KEY-----";
var pemFooter = "-----END PUBLIC KEY-----";
var pemFull = string.Format("{0}\r\n{1}\r\n{2}", pemHeader, base64Encoded, pemFooter);

Если вы создадите PEM из этого ключа, вы сможете загрузить его обратно, используя метод, описанный ранее. Почему это другое? Вызов cert.GetPublicKey() фактически вернет блочную структуру ASN.1;

SEQUENCE(2 elem)
  INTEGER (2048 bit)
  INTEGER 65537

На самом деле это неполный большой двоичный объект DER, но тот, который .NET может декодировать (полный анализ и генерация ASN.1 не поддерживается .NET на момент написания — https://github.com/dotnet/designs/Issues/11).

Правильно закодированные байты открытого ключа DER (ASN.1) имеют следующую структуру;

SEQUENCE(2 elem)
  SEQUENCE(2 elem)
     OBJECT IDENTIFIER   "1.2.840.113549.1.1.1" - rsaEncryption(PKCS #1)
     NULL
BIT STRING(1 elem)
  SEQUENCE(2 elem)
    INTEGER (2048 bit)
    INTEGER 65537

Итак, приведенное выше дает вам открытый ключ (вроде), который вы можете загрузить. Это уродливо и технически неполно, но действительно использует собственные выходные данные .NET из метода RSACryptoServiceProvider.GetPublicCert(). Конструктор может использовать те же самые байты при последующей загрузке только открытого ключа. К сожалению, это не настоящий, полностью сформированный PEM. Мы все еще ожидаем синтаксический анализатор ASN.1 от MS в .NET Core 3.0>.

person DiskJunky    schedule 21.03.2018
comment
Приведенный выше код можно упростить, сократив первые 2 строки следующим образом: Oid oid = new Oid(RSA); - person markf78; 19.11.2019
comment
Это действительно сработало? Я получаю исключение, см. stackoverflow.com/questions/58940913/ для более подробной информации. - person markf78; 19.11.2019
comment
@ markf78, да, хотя я вижу, что у Резы была похожая проблема, но я пропустил комментарий до сих пор. Я посмотрю на ваш связанный вопрос - person DiskJunky; 20.11.2019

Файлы PEM представляют собой просто набор файлов DER с кодировкой base64, а .net позволяет напрямую импортировать файлы DER, поэтому вы можете сделать что-то вроде этого (я предполагаю, что вы используете только открытый ключ, поскольку вы заявляете, что используете только его):

byte[] certBytes = Convert.FromBase64String(deserializedPublicKey
    .Replace("-----BEGIN PUBLIC KEY-----", "")
    .Replace("-----END PUBLIC KEY-----", ""));

X509Certificate2 cert =  new X509Certificate2(certBytes);
RSACryptoServiceProvider publicKeyProvider = 
(RSACryptoServiceProvider)cert.PublicKey.Key;
person Gusman    schedule 23.01.2017
comment
если бы это было так просто :-) Если я передам открытый ключ в конструктор, это приведет к CryptographicException из Cannot find the requested object - person DiskJunky; 23.01.2017
comment
Это должно быть так просто, но кажется, что X509Certificate2 требует, чтобы файл DER включал закрытый ключ... - person Gusman; 23.01.2017
comment
В качестве альтернативы используйте Bouncy Castle, он имеет встроенную поддержку для импорта файлов PEM. - person Gusman; 23.01.2017
comment
именно поэтому я использовал подход грубой силы выше. Удивительно сложно создать необходимые объекты только из открытого ключа. У Java хорошая реализация, а у C# есть старая библиотека под названием BouncyCastle, но текущая документация не существует (буквально, это пустая вики), и мне неудобно ее использовать, учитывая ее текущий юридический статус благотворительности. Все это означало низкоуровневый подход к синтаксическому анализу. - person DiskJunky; 23.01.2017
comment
На самом деле я тоже не смог сделать это с помощью BouncyCastle. Существующие сообщения и информация очень устарели, а текущей документации не существует. - person DiskJunky; 23.01.2017
comment
BouncyCastle хорошо поддерживается, последний толчок был 4 дня назад, это правда, что документацию трудно найти, потому что она такая же, как и для Java, попробуйте, я использую это во всех своих проектах, чтобы избежать проблем с моно, и работает как Шарм. - person Gusman; 23.01.2017
comment
Давайте продолжим это обсуждение в чате. - person DiskJunky; 23.01.2017
comment
у вас есть рабочий образец кода для чтения в PEM и создания объекта RSACryptoServiceProvider? Это должно быть все, что мне нужно тогда - person DiskJunky; 23.01.2017
comment
Кто-нибудь смог успешно использовать это решение? Я получаю Cannot find the requested object исключение в new X509Certificate2(certBytes); - person Reza; 02.11.2018
comment
@ Реза, извини, я только сейчас это увидел. Да, это решение находится в производстве уже несколько лет. Удалось ли вам найти ответ на исключение? - person DiskJunky; 20.11.2019