LoginSignup
5

More than 3 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#のドキュメントが全く無いので、リンクは張らないでおきます。

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
What you can do with signing up
5