5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

C#で楕円曲線暗号を使って署名する

Posted at

はじめに

以前はRSA暗号について書きましたが、今回はOpenSSLで作った鍵を使って楕円曲線暗号で署名と検証をします。

準備するもの

C#の標準ライブラリが楕円曲線に対応したのは、.NET 4.7(C# 7.0, Visutal Studio 2017)以降のようであり、しかもこれを使ってもプログラムが難しそうなので、楕円曲線暗号のライブラリとしてBouncyCastleを使うことにしました。

BouncyCastleのインストールは、いつもどおりNuGetから行えばよいです。

2018-10-02_101927.png

使用バージョンを整理しておくと、今回の記事では次のバージョンを使用しました。ちなみに 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が入っています。

2018-10-10_180917.png

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.PemReaderTextReader から読み込みます(①)。鍵情報は、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#のドキュメントが全く無いので、リンクは張らないでおきます。

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?