はじめに
以前はRSA暗号について書きましたが、今回はOpenSSLで作った鍵を使って楕円曲線暗号で署名と検証をします。
準備するもの
C#の標準ライブラリが楕円曲線に対応したのは、.NET 4.7(C# 7.0, Visutal Studio 2017)以降のようであり、しかもこれを使ってもプログラムが難しそうなので、楕円曲線暗号のライブラリとしてBouncyCastleを使うことにしました。
BouncyCastleのインストールは、いつもどおりNuGetから行えばよいです。
使用バージョンを整理しておくと、今回の記事では次のバージョンを使用しました。ちなみに BouncyCastleのライセンスは MIT Licence となっています。
バージョン | |
---|---|
Visual Studio | 2015 (.NET 4.5.2) |
BouncyCastle | 1.8.3 (2018年10月時点での最新) |
OpenSSL | OpenSSL 1.0.2k-fips (多分CentOS7.5のyumで入れたもの) |
OpenSSLで秘密鍵と公開鍵を生成する
使用できる楕円曲線の一覧を出力する
OpenSSLのバージョンによって、使用できる楕円曲線の種類が違うので、最初に確認しておきます。今回使ったOpenSSLのバージョンでは、4種類の曲線が使えるようです。
$ openssl version
OpenSSL 1.0.2k-fips 26 Jan 2017
$ openssl ecparam -list_curves
secp256k1 : SECG curve over a 256 bit prime field
secp384r1 : NIST/SECG curve over a 384 bit prime field
secp521r1 : NIST/SECG curve over a 521 bit prime field
prime256v1: X9.62/SECG curve over a 256 bit prime field
秘密鍵と公開鍵をPEMで生成する
楕円曲線は何を指定しても処理方法は同じなので、この記事では適当に secp256k1
を指定しておきます。
$ openssl ecparam -genkey -name secp256k1 -out secp256k1.keypair
$ openssl ec -in secp256k1.keypair -outform PEM -out secp256k1.privatekey
read EC key
writing EC key
$ openssl ec -in secp256k1.keypair -outform PEM -pubout -out secp256k1.publickey
read EC key
writing EC key
できたもの
secp256k1.privatekey:秘密鍵(鍵ペア)
secp256k1.publickey:公開鍵
秘密鍵(鍵ペア)について補足しておくと、公開鍵暗号で「鍵ペア」と言うと「公開鍵」+「秘密鍵」をイメージしている人が多いと思しますし、大雑把にはその理解でいいのですが、実際の中身は、秘密鍵は鍵全部のデータで、公開鍵は秘密鍵の(公開できる)一部のデータです。そのため、秘密鍵は公開鍵を含んでいるので、公開鍵としても使うことができますし、秘密鍵から公開鍵を取り出すこともできます。(もちろん、技術的に秘密鍵を公開鍵としても使用できるといっても、秘密鍵を相手に渡してよいという意味ではありません。念のため)
PEMの中身
PEMの中身を少しだけ見ておきます。
- OpenSSLで生成した鍵ペアのPEM(secp256k1.keypair)
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBR9JEhnC2N/QRu+ZOMeQ6VpzJylPR/EUyPNjrujtFEIoAcGBSuBBAAK
oUQDQgAE5LL1tgcqAJFuPuGTlTem7/Z2LR4asnJHjGjDyDOcuWOzZYamoVsenCaG
Bqa/unCYihYYV4f5G83grTY9OMz6bQ==
-----END EC PRIVATE KEY-----
OpenSSLで楕円曲線の鍵を生成すると、2パートに分かれています。楕円曲線暗号はRSA暗号よりパラメータ数が多いはずなのですが、PEMのサイズはすごく小さいです。これは、パラメータとして、楕円曲線名が書かれているだけだからです。楕円曲線名から各パラメータの値(AとかQとか)を知りたい場合は、参考サイトに載せた「楕円曲線のパラメータ一覧」から調べる必要があります。
ちなみに、BouncyCastleは、このPEMファイルを読み込めて欲しいのですが、現在のバージョンでは読み込めませんでした。UnitTestのコードには、TODOとなっていたので、将来のバージョンでは対応されるかもしれません(しかし、C#版のBouncyCastleはほとんど更新がないため、望みはないかも)。
仕方が無いので、BoucyCastleで読み込めるようにするため、秘密鍵(secp256k1.privatekey)と公開鍵(secp256k1.publickey)を取り出します。
- 秘密鍵のPEM(secp256k1.privatekey)
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBR9JEhnC2N/QRu+ZOMeQ6VpzJylPR/EUyPNjrujtFEIoAcGBSuBBAAK
oUQDQgAE5LL1tgcqAJFuPuGTlTem7/Z2LR4asnJHjGjDyDOcuWOzZYamoVsenCaG
Bqa/unCYihYYV4f5G83grTY9OMz6bQ==
-----END EC PRIVATE KEY-----
秘密鍵を取り出す、と言っても、2パートあるうちの下のパートの部分になるだけです。opensslのコマンドを使わなくても、テキストエディタでコピペしてもOKです。実はこの中にも楕円曲線名が入っているため、これだけでも十分だったりします。
ちなみに、PEMの中身を見るアプリケーションを使うと、こんな感じになっています。Object Identifier(OID)に楕円曲線のOIDが入っています。
C#で署名と検証をする
using文
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Crypto.Parameters;
今回使うのは、この4つだけでよさそうでした。System.Security.Cryptography
は不要でした。
PEMファイルの読み込み
PEMファイルなので、Org.BouncyCastle.OpenSsl.PemReader
で TextReader
から読み込みます(①)。鍵情報は、ReadObject()
で取り出せますが、戻り型が object
となっているため、非対称鍵ペア(AsymmetricCipherKeyPair
)にダウンキャストしておきます(②)。
AsymmetricCipherKeyPair pair = null;
using (var stream = new StreamReader(@"..\..\secp256k1.privatekey"))
{
var reader = new PemReader(stream); // ①
pair = reader.ReadObject() as AsymmetricCipherKeyPair; // ②
}
署名する
Org.BouncyCastle.Crypto.Signers.ECDsaSigner#Init()
で署名インスタンスを初期化します(①)。第1引数は署名するので true
、第2引数は署名用の秘密鍵を指定します。署名インスタンスが初期化したら、後は GenerateSignature()
で署名するだけです(②)。
// 署名元データ
var clearText = "あいうえお12345平文";
var plain = Encoding.UTF8.GetBytes(clearText); // 署名元データをbyte配列にしておく
// 署名する
var signer = new ECDsaSigner();
signer.Init(true, pair.Private); // ①
BigInteger[] sign = signer.GenerateSignature(plain); // ②
var sign1 = sign[0].ToByteArray().SkipWhile(b => b == 0x00).Reverse(); // ③(R)
var sign2 = sign[1].ToByteArray().SkipWhile(b => b == 0x00).Reverse(); // ③(S)
byte[] signature = sign1.Concat(sign2).ToArray(); // ④
なお、楕円曲線の署名の値は、整数値が2つ(RとS)あるため、戻り値は BigInteger
の2要素の配列となっています。ただ、実際に署名として使いたい値は、(用途にもよりますが)byte配列だと思いますので、もう少し加工します。
JWTの署名として使う場合、この2つの値をオクテット(byte配列)にして連結するだけなのですが、オクテットはビッグエンディアンで、先頭の値がゼロのバイトはstripする必要があります。そのため、署名の長さは必ず64オクテットになります。C#は、というかWindowsはリトルエンディアンであるため、BigInteger#ToByteArray()
でbyte配列を得るだけではダメで、反転させなければなりません。うへぇ。
②で得た署名の値(RとS)を、ToByteArray()
でbyte[]
にし、SkipWhile(b => b == 0x00)
で先頭にあるzeroバイトを削除し、Reverse()
で反転します(③)。ここまでできれば、後は結合するだけです(④)。
署名を検証する
基本的には署名と反対のことを行えばよいです。署名をbyte配列で受け取ったら、まず32byteずつに分割し、RとSに分けます(その前に、署名の長さが 64byte であることをチェックします)。その後、それぞれをBigInteger
にしたいのですが、C#はリトルエンディアンなので Reverse()
で反転する必要があります(①)。さらに最初のbyteの最上位bitが1である場合、マイナスになってしまうので、先頭にzeroのバイトを追加してやらないとなりません(②)。まじすか。
BigInteger
にできれば、あとは署名と同様に、Org.BouncyCastle.Crypto.Signers.ECDsaSigner#Init()
で署名インスタンスを初期化します(③)。第1引数は検証なので false
、第2引数は署名検証用の公開鍵を指定します。署名の検証は VerifySignature()
メソッドで行い、第1引数に署名元データ、第2・3引数に署名の値、RとSを指定します(④)。
// 署名
byte[] signature = ...
// 署名検証元データ
var clearText = "あいうえお12345平文";
var plain = Encoding.UTF8.GetBytes(clearText); // 署名検証元データをbyte配列にしておく
// 検証
var sign1 = signature.Take(32).Reverse().ToArray(); // ①
if ((sign1[0] & 0x80) == 0x80) sign1 = new byte[] { 0x00 }.Concat(sign1).ToArray(); // ②(R)
var sign2 = signature.Skip(32).Reverse().ToArray(); // ①
if ((sign2[0] & 0x80) == 0x80) sign2 = new byte[] { 0x00 }.Concat(sign2).ToArray(); // ②(S)
var sign = new BigInteger[] { new BigInteger(sign1), new BigInteger(sign2) };
var signer = new ECDsaSigner();
signer.Init(false, pair.Public); // ③
bool result = signer.VerifySignature(plain, sign[0], sign[1]); // ④
全部まとめる
- OpenSSL
$ openssl ecparam -list_curves
secp256k1 : SECG curve over a 256 bit prime field
secp384r1 : NIST/SECG curve over a 384 bit prime field
secp521r1 : NIST/SECG curve over a 521 bit prime field
prime256v1: X9.62/SECG curve over a 256 bit prime field
$ openssl ecparam -genkey -name secp256k1 -out secp256k1.keypair
$ openssl ec -in secp256k1.keypair -outform PEM -out secp256k1.privatekey
read EC key
writing EC key
$ openssl ec -in secp256k1.keypair -outform PEM -pubout -out secp256k1.publickey
read EC key
writing EC key
- プログラム
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Crypto.Parameters;
namespace Sample
{
class Program
{
static void Main(string[] args)
{
var clearText = @"あいうえお12345";
// 署名
byte[] signature = Sign(clearText, @"..\..\secp256k1.privatekey");
Console.WriteLine(string.Join("", signature.Select(b => $"{b:x02}")));
// 検証
bool result = Verify(clearText, signature, @"..\..\secp256k1.privatekey");
Console.WriteLine($"verify: {result}");
Console.ReadLine();
}
/// <summary>
/// 署名する
/// </summary>
static byte[] Sign(string clearText, string key)
{
var plain = Encoding.UTF8.GetBytes(clearText);
// 鍵を読み込む
AsymmetricCipherKeyPair pair = null;
using (var stream = new StreamReader(key))
{
var reader = new PemReader(stream);
pair = reader.ReadObject() as AsymmetricCipherKeyPair;
}
// 署名インスタンスを生成&署名
var signer = new ECDsaSigner();
signer.Init(true, pair.Private);
var sign = signer.GenerateSignature(plain);
// 署名の値をbyte[]にしておく
var sign1 = sign[0].ToByteArray().SkipWhile(b => b == 0x00).Reverse();
var sign2 = sign[1].ToByteArray().SkipWhile(b => b == 0x00).Reverse();
byte[] signature = sign1.Concat(sign2).ToArray();
return signature;
}
/// <summary>
/// 署名を検証する
/// </summary>
static bool Verify(string clearText, byte[] signature, string key)
{
var plain = Encoding.UTF8.GetBytes(clearText);
// 鍵を読み込む
AsymmetricCipherKeyPair pair = null;
using (var stream = new StreamReader(key))
{
var reader = new PemReader(stream);
pair = reader.ReadObject() as AsymmetricCipherKeyPair;
}
// 署名の値をBigIntegerに変換する
var sign1 = signature.Take(32).Reverse().ToArray();
if ((sign1[0] & 0x80) == 0x80) sign1 = new byte[] { 0x00 }.Concat(sign1).ToArray();
var sign2 = signature.Skip(32).Reverse().ToArray();
if ((sign2[0] & 0x80) == 0x80) sign2 = new byte[] { 0x00 }.Concat(sign2).ToArray();
var sign = new BigInteger[] { new BigInteger(sign1), new BigInteger(sign2) };
// 検証する
var signer = new ECDsaSigner();
signer.Init(false, pair.Public);
var result = signer.VerifySignature(plain, sign[0], sign[1]);
return result;
}
}
}
まとめ
C#標準ライブラリで楕円曲線暗号を使うのは難しそうだったので、BouncyCastleを使いましたが、どういう訳かBouncyCastleにはC#のドキュメントが全くありません。C#版の更新頻度もそんなに高くないので、正直コレを使っても大丈夫か、という一抹の不安はあります(ライセンス的には問題なさそうですが)。さらにドキュメントが無いので、暗号/復号の方法が分からず、今回は触れることができませんでした。どうもUnitTestを見るしか情報源がなさそうなのが、BouncyCastleを使うのが厳しいところです。
参考文献・サイト
アルゴリズムの話は全くしなかったのですが、楕円曲線をざっくり知るには、このスライドショーがいいと思います。
楕円曲線のパラメータ(AとかGとかいうやつ)一覧です。今回はライブラリを使ったので、自分で楕円曲線(ECCurve
)を実装する、という必要は無かったのですが、そうでない場合は、楕円曲線名からここで各パラメータをコピペする必要があります。
楕円曲線の署名って数値2つなのに、JWTの署名の値はどうするんだ、と思ったのですが、ちゃんと仕様書に書いてありました。
ちなみに、BoucyCastleにはC#のドキュメントが全く無いので、リンクは張らないでおきます。