Edited at

OpenSSLで作成したRSA暗号鍵をC#で読み込む方法

More than 3 years have passed since last update.

OpenSSLで作成したRSA暗号鍵(.pem)を、C#で読み込むには工夫が必要です。

そのままの内容で読み込むとエラーとなります。

原因はわかったのですがスマートな解決策が見つかっていません。

内容を見て、もし助言いただければ、もれなく私の上司がアイスをプレゼントします。

以下、調査結果です。


1. まず秘密鍵を作成

openssl genrsa > private-key.pem

-----BEGIN RSA PRIVATE KEY-----

MIICXAIBAAKBgQC7Np2qTFhnZD0meg7OqzPMLzQjZLhCjLpmVoeTPAaeTpvQij0r
EG9FmS+Gbj4oGBiV8BaOraWlRTmPh2DnJJv/WmsAmd+WRsbCowGfpmukG7X0bEp7
XIRvbwzTHV34X3JyWdq9eGyr6FjidyiMPnA+sBRuvJhvsf4kmadrHIGXuQIDAQAB
AoGACJHfdcQ458K26eP8eYlsvZQLlvMtXB6FCdo8Kok85FJE3670Tdxau5zfiA/T
2JIBzhtHbRCV3JLmp6NS1EHgwiOVgohrzLj2QME+pN05Kxs+d8snfyl+izmuluV/
4qfOEcq1q3BiKI5jjrF78vVJzkRJNN/xLDxfk+duXYxnHeECQQDoQCcVGRKThyDp
cD/flayKf0ejtfKFZieTkOcKmU5zNdACDu7QgvcgwA2emu/cH/yFIExzPcg6vAXc
cXAg6CLTAkEAzlt6zfaLfGGeWrp+2W+J9svrc060ckeFRzDHiwWA/T3c/F1fjCDs
B2qouZuRWxSQJ5XTro4uKRvd+GjBZxALwwJAC5+qVRP8KgYJT6WejMIg8I2OOFNS
3/pM4MXPymgB1SJMVs4IhccIN0tlYxq+BnZTXRZQvTPKYaYnZPEIe50ioQJBALe0
mrvWu1BsexPcKOcbM+Ago6gQLfX6k7zPwSSTLsTDmQKVgWT3PghQPIqX4qwJv1du
O3ePHar9IvttDpfYog8CQAMy9RiFqJ3K/St4iVZizsoeG/GAa+o/kKiTjK4+wnox
UdkJEZh/7lkKbw9h2CVURIjUitmKDrfjkeJ7GsdKvAM=
-----END RSA PRIVATE KEY-----

中を覗くと以下のようになっています。

Private-Key: (1024 bit)

modulus:
00:bb:36:9d:aa:4c:58:67:64:3d:26:7a:0e:ce:ab:
33:cc:2f:34:23:64:b8:42:8c:ba:66:56:87:93:3c:
06:9e:4e:9b:d0:8a:3d:2b:10:6f:45:99:2f:86:6e:
3e:28:18:18:95:f0:16:8e:ad:a5:a5:45:39:8f:87:
60:e7:24:9b:ff:5a:6b:00:99:df:96:46:c6:c2:a3:
01:9f:a6:6b:a4:1b:b5:f4:6c:4a:7b:5c:84:6f:6f:
0c:d3:1d:5d:f8:5f:72:72:59:da:bd:78:6c:ab:e8:
58:e2:77:28:8c:3e:70:3e:b0:14:6e:bc:98:6f:b1:
fe:24:99:a7:6b:1c:81:97:b9
publicExponent: 65537 (0x10001)
privateExponent:
08:91:df:75:c4:38:e7:c2:b6:e9:e3:fc:79:89:6c:
bd:94:0b:96:f3:2d:5c:1e:85:09:da:3c:2a:89:3c:
e4:52:44:df:ae:f4:4d:dc:5a:bb:9c:df:88:0f:d3:
d8:92:01:ce:1b:47:6d:10:95:dc:92:e6:a7:a3:52:
d4:41:e0:c2:23:95:82:88:6b:cc:b8:f6:40:c1:3e:
a4:dd:39:2b:1b:3e:77:cb:27:7f:29:7e:8b:39:ae:
96:e5:7f:e2:a7:ce:11:ca:b5:ab:70:62:28:8e:63:
8e:b1:7b:f2:f5:49:ce:44:49:34:df:f1:2c:3c:5f:
93:e7:6e:5d:8c:67:1d:e1
prime1:
00:e8:40:27:15:19:12:93:87:20:e9:70:3f:df:95:
ac:8a:7f:47:a3:b5:f2:85:66:27:93:90:e7:0a:99:
4e:73:35:d0:02:0e:ee:d0:82:f7:20:c0:0d:9e:9a:
ef:dc:1f:fc:85:20:4c:73:3d:c8:3a:bc:05:dc:71:
70:20:e8:22:d3
prime2:
00:ce:5b:7a:cd:f6:8b:7c:61:9e:5a:ba:7e:d9:6f:
89:f6:cb:eb:73:4e:b4:72:47:85:47:30:c7:8b:05:
80:fd:3d:dc:fc:5d:5f:8c:20:ec:07:6a:a8:b9:9b:
91:5b:14:90:27:95:d3:ae:8e:2e:29:1b:dd:f8:68:
c1:67:10:0b:c3
exponent1:
0b:9f:aa:55:13:fc:2a:06:09:4f:a5:9e:8c:c2:20:
f0:8d:8e:38:53:52:df:fa:4c:e0:c5:cf:ca:68:01:
d5:22:4c:56:ce:08:85:c7:08:37:4b:65:63:1a:be:
06:76:53:5d:16:50:bd:33:ca:61:a6:27:64:f1:08:
7b:9d:22:a1
exponent2:
00:b7:b4:9a:bb:d6:bb:50:6c:7b:13:dc:28:e7:1b:
33:e0:20:a3:a8:10:2d:f5:fa:93:bc:cf:c1:24:93:
2e:c4:c3:99:02:95:81:64:f7:3e:08:50:3c:8a:97:
e2:ac:09:bf:57:6e:3b:77:8f:1d:aa:fd:22:fb:6d:
0e:97:d8:a2:0f
coefficient:
03:32:f5:18:85:a8:9d:ca:fd:2b:78:89:56:62:ce:
ca:1e:1b:f1:80:6b:ea:3f:90:a8:93:8c:ae:3e:c2:
7a:31:51:d9:09:11:98:7f:ee:59:0a:6f:0f:61:d8:
25:54:44:88:d4:8a:d9:8a:0e:b7:e3:91:e2:7b:1a:
c7:4a:bc:03


2. つづけて公開鍵を作成

openssl rsa -pubout < private-key.pem > public-key.pem

-----BEGIN PUBLIC KEY-----

MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7Np2qTFhnZD0meg7OqzPMLzQj
ZLhCjLpmVoeTPAaeTpvQij0rEG9FmS+Gbj4oGBiV8BaOraWlRTmPh2DnJJv/WmsA
md+WRsbCowGfpmukG7X0bEp7XIRvbwzTHV34X3JyWdq9eGyr6FjidyiMPnA+sBRu
vJhvsf4kmadrHIGXuQIDAQAB
-----END PUBLIC KEY-----

中身は以下の通り

Public-Key: (1024 bit)

Modulus:
00:bb:36:9d:aa:4c:58:67:64:3d:26:7a:0e:ce:ab:
33:cc:2f:34:23:64:b8:42:8c:ba:66:56:87:93:3c:
06:9e:4e:9b:d0:8a:3d:2b:10:6f:45:99:2f:86:6e:
3e:28:18:18:95:f0:16:8e:ad:a5:a5:45:39:8f:87:
60:e7:24:9b:ff:5a:6b:00:99:df:96:46:c6:c2:a3:
01:9f:a6:6b:a4:1b:b5:f4:6c:4a:7b:5c:84:6f:6f:
0c:d3:1d:5d:f8:5f:72:72:59:da:bd:78:6c:ab:e8:
58:e2:77:28:8c:3e:70:3e:b0:14:6e:bc:98:6f:b1:
fe:24:99:a7:6b:1c:81:97:b9
Exponent: 65537 (0x10001)


3. 暗号化を行うコードを用意

※鍵の読み込みにはBouncyCastleを使っています。


error.cs

void Main()

{
//暗号化
var publicParameters = CreatePublicKeyParameters(PublicKey);
var cipherText = Encrypt(publicParameters, "topsecret");

//復号化
var privateParameters = CreatePrivateKeyParameters(PrivateKey);
var plainText = Decrypt(privateParameters, cipherText);
}

string Encrypt(RSAParameters parameters, string plainText)
{
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var cipherBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.Pkcs1);

return Convert.ToBase64String(cipherBytes);
}
}

string Decrypt(RSAParameters parameters, string cipherText)
{
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
var cipherBytes = Convert.FromBase64String(cipherText);
var plainBytes = rsa.Decrypt(cipherBytes, RSAEncryptionPadding.Pkcs1);

return Encoding.UTF8.GetString(plainBytes);
}
}

RSAParameters CreatePublicKeyParameters(string publicKey)
{
using (var reader = new StringReader(publicKey))
{
var pem = new Org.BouncyCastle.Utilities.IO.Pem.PemReader(reader).ReadPemObject();

using (var stream = new MemoryStream(pem.Content, false))
{
var asn1 = new Asn1InputStream(stream, pem.Content.Length).ReadObject();
var info = SubjectPublicKeyInfo.GetInstance(asn1);
var parameters = PublicKeyFactory.CreateKey(pem.Content) as RsaKeyParameters;

return new RSAParameters
{
Exponent = parameters?.Exponent?.ToByteArray(),
Modulus = parameters?.Modulus?.ToByteArray(),
};
}
}
}

RSAParameters CreatePrivateKeyParameters(string privateKey)
{
using (var reader = new StringReader(privateKey))
{
var pem = new Org.BouncyCastle.OpenSsl.PemReader(reader);
var keyPair = pem.ReadObject() as AsymmetricCipherKeyPair;
var parameters = keyPair?.Private as RsaPrivateCrtKeyParameters;

return new RSAParameters
{
D = parameters?.Exponent?.ToByteArray(),
DP = parameters?.DP?.ToByteArray(),
DQ = parameters?.DQ?.ToByteArray(),
Exponent = parameters?.PublicExponent?.ToByteArray(),
InverseQ = parameters?.QInv?.ToByteArray(),
Modulus = parameters?.Modulus?.ToByteArray(),
P = parameters?.P?.ToByteArray(),
Q = arameters?.Q?.ToByteArray(),
};
}
}

const string PrivateKey = @"-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC7Np2qTFhnZD0meg7OqzPMLzQjZLhCjLpmVoeTPAaeTpvQij0r
EG9FmS+Gbj4oGBiV8BaOraWlRTmPh2DnJJv/WmsAmd+WRsbCowGfpmukG7X0bEp7
XIRvbwzTHV34X3JyWdq9eGyr6FjidyiMPnA+sBRuvJhvsf4kmadrHIGXuQIDAQAB
AoGACJHfdcQ458K26eP8eYlsvZQLlvMtXB6FCdo8Kok85FJE3670Tdxau5zfiA/T
2JIBzhtHbRCV3JLmp6NS1EHgwiOVgohrzLj2QME+pN05Kxs+d8snfyl+izmuluV/
4qfOEcq1q3BiKI5jjrF78vVJzkRJNN/xLDxfk+duXYxnHeECQQDoQCcVGRKThyDp
cD/flayKf0ejtfKFZieTkOcKmU5zNdACDu7QgvcgwA2emu/cH/yFIExzPcg6vAXc
cXAg6CLTAkEAzlt6zfaLfGGeWrp+2W+J9svrc060ckeFRzDHiwWA/T3c/F1fjCDs
B2qouZuRWxSQJ5XTro4uKRvd+GjBZxALwwJAC5+qVRP8KgYJT6WejMIg8I2OOFNS
3/pM4MXPymgB1SJMVs4IhccIN0tlYxq+BnZTXRZQvTPKYaYnZPEIe50ioQJBALe0
mrvWu1BsexPcKOcbM+Ago6gQLfX6k7zPwSSTLsTDmQKVgWT3PghQPIqX4qwJv1du
O3ePHar9IvttDpfYog8CQAMy9RiFqJ3K/St4iVZizsoeG/GAa+o/kKiTjK4+wnox
UdkJEZh/7lkKbw9h2CVURIjUitmKDrfjkeJ7GsdKvAM=
-----END RSA PRIVATE KEY-----"
;

const string PublicKey = @"-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7Np2qTFhnZD0meg7OqzPMLzQj
ZLhCjLpmVoeTPAaeTpvQij0rEG9FmS+Gbj4oGBiV8BaOraWlRTmPh2DnJJv/WmsA
md+WRsbCowGfpmukG7X0bEp7XIRvbwzTHV34X3JyWdq9eGyr6FjidyiMPnA+sBRu
vJhvsf4kmadrHIGXuQIDAQAB
-----END PUBLIC KEY-----"
;



4. 実行結果

復号化のタイミングでエラーとなってしまいました。

鍵の読み込みで問題が発生しているのがわかります。

err.PNG


5. 原因を探る

コチラを使ってCryptoAPIで鍵を読み込んで確認してみるとmodulusが128byteなのがわかります。(CryptoAPIで読み込んだ鍵の暗号化復号化は動作OKでした)

check3.PNG

先ほどの実行結果と比べると

check1.PNG

modullusが1byte多いのがわかります。他にも1byte多いパラメータがあるのがわかります。

それぞれを比較すると頭1byteに0x00が含まれているのが差異であることがわかりました。

試しにコレを削ったら動作するようになりました。

success.PNG

stack overflowに似たようなケースの対応方法がありましたが、やはりmodulusを1byte削っており、私のアプローチと同じです。


5. 整理すると


  1. OpenSSLの鍵の一部のパラメータは頭1byteに0x00が入っている

  2. RSACryptoServiceProviderは0x00が入っているとエラーを吐く

  3. CryptoAPIで鍵を読み込むと0x00が除去されている

  4. BouncyCastleで鍵を読み込むと0x00が含まれたまま

今回使用したOpenSSLの鍵は負値の場合、頭に0x00の追加が必要で、これはバイナリ変換規則ASN.1の仕様のようです。

修正したコードは以下で、first byteを調整しています。

現在の.NET Frameworkは、CryptoAPIのようなASN.1をデコードする機能が存在しないんでしょうかね?


success.cs

void Main()

{
//暗号化
var publicParameters = CreatePublicKeyParameters(PublicKey);
var cipherText = Encrypt(publicParameters, "topsecret");
$"encrypted:{cipherText}".Dump();

//復号化
var privateParameters = CreatePrivateKeyParameters(PrivateKey);
var plainText = Decrypt(privateParameters, cipherText);
$"decrypted:{plainText}".Dump();
}

bool RSA暗号鍵の各種パラメータの頭1バイトが0x00だったら除外する { get; set; } = true;

string Encrypt(RSAParameters parameters, string plainText)
{
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var cipherBytes = rsa.Encrypt(plainBytes, RSAEncryptionPadding.Pkcs1);

return Convert.ToBase64String(cipherBytes);
}
}

string Decrypt(RSAParameters parameters, string cipherText)
{
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(parameters);
var cipherBytes = Convert.FromBase64String(cipherText);
var plainBytes = rsa.Decrypt(cipherBytes, RSAEncryptionPadding.Pkcs1);

return Encoding.UTF8.GetString(plainBytes);
}
}

RSAParameters CreatePublicKeyParameters(string publicKey)
{
using (var reader = new StringReader(publicKey))
{
var pem = new Org.BouncyCastle.Utilities.IO.Pem.PemReader(reader).ReadPemObject();

using (var stream = new MemoryStream(pem.Content, false))
{
var asn1 = new Asn1InputStream(stream, pem.Content.Length).ReadObject();
var info = SubjectPublicKeyInfo.GetInstance(asn1);
var parameters = PublicKeyFactory.CreateKey(pem.Content) as RsaKeyParameters;

//$"OID:{info.AlgorithmID.Algorithm.Id}".Dump();

return new RSAParameters
{
Exponent = Adjustment(parameters?.Exponent?.ToByteArray()),
Modulus = Adjustment(parameters?.Modulus?.ToByteArray())
};
}
}
}

RSAParameters CreatePrivateKeyParameters(string privateKey)
{
using (var reader = new StringReader(privateKey))
{
var pem = new Org.BouncyCastle.OpenSsl.PemReader(reader);
var keyPair = pem.ReadObject() as AsymmetricCipherKeyPair;
var parameters = keyPair?.Private as RsaPrivateCrtKeyParameters;

return new RSAParameters
{
D = Adjustment(parameters?.Exponent?.ToByteArray()),
DP = Adjustment(parameters?.DP?.ToByteArray()),
DQ = Adjustment(parameters?.DQ?.ToByteArray()),
Exponent = Adjustment(parameters?.PublicExponent?.ToByteArray()),
InverseQ = Adjustment(parameters?.QInv?.ToByteArray()),
Modulus = Adjustment(parameters?.Modulus?.ToByteArray()),
P = Adjustment(parameters?.P?.ToByteArray()),
Q = Adjustment(parameters?.Q?.ToByteArray()),
};
}
}

byte[] Adjustment(byte[] bytes)
{
if (!RSA暗号鍵の各種パラメータの頭1バイトが0x00だったら除外する)
{
return bytes;
}

if (bytes == null) return bytes;

if (bytes.Length > 0 && bytes[0] == 0x00)
{
var offset = 1;
var size = bytes.Length - offset;
var buffer = new byte[size];
Buffer.BlockCopy(bytes, offset, buffer, 0, size);
return buffer;
}

return bytes;
}

//CMD:openssl genrsa > private-key.pem
const string PrivateKey = @"-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQC7Np2qTFhnZD0meg7OqzPMLzQjZLhCjLpmVoeTPAaeTpvQij0r
EG9FmS+Gbj4oGBiV8BaOraWlRTmPh2DnJJv/WmsAmd+WRsbCowGfpmukG7X0bEp7
XIRvbwzTHV34X3JyWdq9eGyr6FjidyiMPnA+sBRuvJhvsf4kmadrHIGXuQIDAQAB
AoGACJHfdcQ458K26eP8eYlsvZQLlvMtXB6FCdo8Kok85FJE3670Tdxau5zfiA/T
2JIBzhtHbRCV3JLmp6NS1EHgwiOVgohrzLj2QME+pN05Kxs+d8snfyl+izmuluV/
4qfOEcq1q3BiKI5jjrF78vVJzkRJNN/xLDxfk+duXYxnHeECQQDoQCcVGRKThyDp
cD/flayKf0ejtfKFZieTkOcKmU5zNdACDu7QgvcgwA2emu/cH/yFIExzPcg6vAXc
cXAg6CLTAkEAzlt6zfaLfGGeWrp+2W+J9svrc060ckeFRzDHiwWA/T3c/F1fjCDs
B2qouZuRWxSQJ5XTro4uKRvd+GjBZxALwwJAC5+qVRP8KgYJT6WejMIg8I2OOFNS
3/pM4MXPymgB1SJMVs4IhccIN0tlYxq+BnZTXRZQvTPKYaYnZPEIe50ioQJBALe0
mrvWu1BsexPcKOcbM+Ago6gQLfX6k7zPwSSTLsTDmQKVgWT3PghQPIqX4qwJv1du
O3ePHar9IvttDpfYog8CQAMy9RiFqJ3K/St4iVZizsoeG/GAa+o/kKiTjK4+wnox
UdkJEZh/7lkKbw9h2CVURIjUitmKDrfjkeJ7GsdKvAM=
-----END RSA PRIVATE KEY-----"
;

//CMD:openssl rsa -pubout < private-key.pem > public-key.pem
const string PublicKey = @"-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7Np2qTFhnZD0meg7OqzPMLzQj
ZLhCjLpmVoeTPAaeTpvQij0rEG9FmS+Gbj4oGBiV8BaOraWlRTmPh2DnJJv/WmsA
md+WRsbCowGfpmukG7X0bEp7XIRvbwzTHV34X3JyWdq9eGyr6FjidyiMPnA+sBRu
vJhvsf4kmadrHIGXuQIDAQAB
-----END PUBLIC KEY-----"
;