web-dev-qa-db-ja.com

公開鍵からRSACryptoServiceProviderを正しく作成する

現在、デコードされたPEMファイルのみからRSACryptoServiceProviderオブジェクトを作成しようとしています。数日間の検索の後、私はなんとかして実用的な解決策を練り上げましたが、それは生産準備ができているものではありません。

簡単に言えば、PEMファイルの公開鍵を構成するバイトからRSACryptoServiceProviderオブジェクトを作成するには、キーサイズを指定するオブジェクト(現在は特にSHA256を使用する2048)を作成してから、 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標準を理解しようとしましたが、私はそこから必要なものを見つけるのにほとんど運がありませんでした-標準は多少ビザンチンです。

どんな助けでもありがたいです。

10
DiskJunky

既存のパラメーターをエクスポートしてからそれらの上に再インポートする必要はありません。これにより、マシンはRSAキーを生成して強制的に破棄します。したがって、コンストラクタにキーサイズを指定することは重要ではありません(キーを使用しない場合、キーは生成されません...通常)。

公開鍵ファイルは、DERエンコードされたblobです。

_-----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.69 は、Basic Encoding Rules(BER)、Canonical Encoding Rules(CER、私が明示的に使用したことはない)、Distinguished Encoding Rules(DER)でエンコードされたものを読み取る方法を定義します。ほとんどの場合、CERはBERを制限し、DERはCERを制限するため、DERが最も読みやすくなります。 ( ITU-T X.68 は、DERがバイナリエンコーディングである文法である抽象構文記法1(ASN.1)を説明します)

これで少し解析できます:

_30
_

これは、CONSTRUCTEDビットが設定されたシーケンス(0x10)(0x20)を識別します。これは、他のDER /タグ付き値が含まれていることを意味します。 (SEQUENCEは常にDERで構築されます)

_81 A0
_

この次の部分は長さです。上位ビットセット(> 0x7F)があるため、最初のバイトは「長さの長さ」の値です。これは、実際の長さが次の1バイトでエンコードされることを示します(_lengthLength & 0x7F_)。したがって、このシーケンスの内容は合計160バイトです。 (この場合、「残りのデータ」ですが、SEQUENCEは他の何かの中に含まれている可能性があります)。内容を読みましょう:

_30 0D
_

長さの値が_0x30_のCONSTRUCTED SEQUENCE(_0x0D_)が再び表示されるので、13バイトのペイロードがあります。

_06 09 2A 86 48 86 F7 0D 01 01 01 05 00 
_

_06_はOBJECT IDENTIFIERで、_0x09_バイトのペイロードがあります。 OIDは少し直感的でないエンコーディングを持っていますが、これは_1.2.840.113549.1.1.1_であるテキスト表現_id-rsaEncryption_と同等です( http:// www .oid-info.com/get/1.2.840.113549.1.1.1 )。

これでも、2バイト(_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を意味します。 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/rfc528 これは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 231 、私たちが見るところ

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

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

つまり、読み取る最初のINTEGERはModulus値であり、2番目は(public)Exponentです。

DERエンコーディングはビッグエンディアンであり、これはRSAParametersエンコーディングでもありますが、RSAParametersの場合、先頭の_0x00_値をModulusから削除する必要があります。

それを行うためのコードを提供するほど簡単ではありませんが、この情報が与えられれば、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
}
_
24
bartonjs

長い時間をかけて、検索と bartonjs の優れた応答を行うと、公開鍵の構造に慣れていない人には少し直感的ではありませんが、これを行うコードは実際には単純です。

公開鍵PEMは、new RSACryptoServiceProvider(pemBytes)のようなものではなく、RSAだけでなく、さまざまな鍵タイプを記述することができます。構造/構文、ASN.1に基づいてPEMを解析する必要があります。それが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;

注:上記のコードはnot生産準備完了です!オブジェクトの作成(公開鍵はRSAではない場合があります)、RSACryptoServiceProviderへのキャストなどに適切なガードを配置する必要があります。ここのコードサンプルは、合理的にクリーンに実行できることを示すために短くなっています。

どうやってこれを手に入れましたか? ILSpyの暗号化名前空間を調べてみると、AsnEncodedDatabartonjs の説明でベルを鳴らしていることに気付きました。さらに調査を行ったところ、 this の投稿に遭遇しました(見覚えがありますか?)。これはキーのサイズを特定することを試みていましたが、途中で必要なRSACryptoServiceProviderが作成されます。

bartonjs の回答をAcceptedのままにします。上記のコードはその研究の結果であり、私がここに残しているのは、同じことをしようとしている他の人が、私のOPのように配列をコピーするハックなしに、きれいに実行できるようにするためです。

また、デコードとテストの目的で、ASN.1デコーダー here を使用して、公開鍵が解析可能かどうかを確認できます。

[〜#〜]更新[〜#〜]

これを作成するのは.NETロードマップにあります easier with ASN.1 parsing for Core> 2.1.0。

更新2

現在、Core .NET 2.1.1にはプライベート実装があります。 MSは、すべてが満足するまでドッグフーディングしており、(できれば)後続のバージョンでパブリックAPIを見る予定です。

6
DiskJunky

PEMファイルは、base64でエンコードされたDERファイルの単なるシリーズであり、.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;
1
Gusman