デジタル署名

デジタル署名と署名検証の取り扱いに興味が向いたので、コンソールアプリケーションで作ってみました。デジタル署名の対象は「文字列」「XML 形式の文字列」「ファイル」の3種類を記述しています(2種類はコメントアウトしてある)。

デジタル署名を行うと次の XML 形式の文字列が得られるようになっています。


<SignedData>
  <Data>デジタル署名対象のBase64 エンコーディングしたデータ</Data>
  <Algorithm>ハッシュ計算のアルゴリズム</Algorithm>
  <Signature>BASE64エンコードしたデジタル署名</Signature>
</SignedData>

XML 形式の文字列を得るようにしていますが、XML 署名にはしていません。署名対象のデータも BASE64 エンコードしているので、XML 正規化などは考慮外となっています。

デジタル署名は BASE64 エンコードしたあとに行っています。
デジタル署名に関連するデータ(対象データ、ハッシュ値計算アルゴリズムの種類、デジタル署名)は、SignedData クラスが保持していて、次のプロパティとメソッドを持っています。

byte[] Data { get; set; }
デジタル署名対象の Byte 配列
string Base64Data { get; }
デジタル署名対象のBase64 エンコーディングしたデータ
HashKind Algorithm { get; set; }
ハッシュ計算のアルゴリズム
string Signature { get; set; }
Base64 エンコーディングしたデジタル署名
void FromXmlString(string xmlString)
XML 文字列からデータを取り出します。
string ToXmlString()
XML 文字列を生成します。

実行すると、コンソールに次のようなメッセージが表示されます。

コンソールへの表示
コンソールへの表示

それではプログラムです。
コンソール アプリケーションでプロジェクト名は「DigitalSignature01」としています。

SignedData クラスを作成します。SignedData.cs ファイルにはハッシュ計算アルゴリズムの種類を示す HashKind 列挙体も記述しています。

using System;
using System.Xml.Linq;

namespace DigitalSignature01
{
    public class SignedData
    {
        public SignedData() { }
        public SignedData(byte[] data, HashKind algorithm)
        {
            Data = data;
            Algorithm = algorithm;
        }

        // デジタル署名対象のBase64 エンコーディングしたデータ
        private string _data;
        /// <summary>
        /// デジタル署名対象の Byte 配列
        /// </summary>
        public byte[] Data
        {
            get
            {
                return Convert.FromBase64String(_data);
            }
            set
            {
                _data = Convert.ToBase64String(value);
            }
        }

        /// <summary>
        /// デジタル署名対象のBase64 エンコーディングしたデータ
        /// </summary>
        public string Base64Data
        {
            get { return _data; }
            private set { _data = value; }
        }

        /// <summary>
        /// ハッシュ計算のアルゴリズム
        /// </summary>
        public HashKind Algorithm { get; set; }

        /// <summary>
        /// デジタル署名
        /// </summary>
        public string Signature { get; set; }

        /// <summary>
        /// XML 文字列からデータを取り出します。
        /// </summary>
        /// <param name="xmlString"></param>
        public void FromXmlString(string xmlString)
        {
            var xml = XDocument.Parse(xmlString);
            if (xml == null)
            {
                throw new ArgumentException("SignedData XML の形式が誤っています。");
            }
            XElement element = null;
            element = xml.Element("SignedData").Element("Data");
            if (element == null)
            {
                throw new ArgumentException("SignedData XML に Data タグがありません。");
            }
            Base64Data = element.Value;
            element = xml.Element("SignedData").Element("Algorithm");
            if (element == null)
            {
                throw new ArgumentException("SignedData XML に Algorithm タグがありません。");
            }
            Algorithm = (HashKind)Enum.Parse(typeof(HashKind), element.Value);
            element = xml.Element("SignedData").Element("Signature");
            if (element == null)
            {
                throw new ArgumentException("SignedData XML に Signature タグがありません。。");
            }
            Signature = element.Value;
        }

        /// <summary>
        /// XML 文字列を生成します。
        /// </summary>
        /// <returns></returns>
        public string ToXmlString()
        {
            var xml = 
                new XElement("SignedData",
                    new XElement("Data", Base64Data),
                    new XElement("Algorithm", Algorithm),
                    new XElement("Signature", Signature)
                );
            return xml.ToString();
        }
    }

    /// <summary>
    /// HashKind 列挙体
    /// </summary>
    public enum HashKind
    {
        SHA256,
        SHA384,
        SHA512
    }
}

デジタル署名対象データは、内部的には BASE64 エンコーディグしたものを保持していて、byte 配列の Data プロパティでは BASE64 デコード・エンコード メソッドを通して取得・設定を行っています。

XML 文字列からデータを取り出してセットする FromXmlString メソッドでは LINQ to XML を用いて操作を行っています。

次に Program.cs です。

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace DigitalSignature01
{
    class Program
    {
        static void Main(string[] args)
        {
            // 文字列
            var target = "abcdefg 0123456 ABCDEFG";
            // XML 形式文字列
            //var target = "<test>\n  <item1>testItem1</item1>\n  <item2>testItem2</item2>\n</test>";
            // ファイル
             /*
            var filePath = @".\DigitalSignature01.exe";
            byte[] target = null;
            using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                target = new byte[fs.Length];
                fs.Read(target, 0, target.Length);
            }
             */
            var algorithm = HashKind.SHA256; // ハッシュ計算のアルゴリズム
            var containerName = "TestRsa";   // 鍵ペア保存のコンテナ名

            try
            {
                // 鍵ペアの生成及び格納
                GetPublickey(containerName);
                Console.WriteLine("公開鍵の XML データ");
                Console.WriteLine(GetPublickey(containerName));

                // デジタル署名
                var xml = sign(target, containerName, algorithm);

                // XML からのデータの取得
                var signedData = new SignedData();
                signedData.FromXmlString(xml);

                // 公開鍵の取得
                var publicKey = GetPublickey(containerName);
                // デジタル署名の検証
                Console.WriteLine("---");
                Console.Write("デジタル署名の検証: ");
                var condition = VerifyDigitalSignature(publicKey, signedData.Base64Data, signedData.Signature, signedData.Algorithm);
                Console.WriteLine("{0}", condition);

                // 格納された鍵ペアの削除
                DeleteKeyFromContainer(containerName);
                Console.WriteLine("---");
                Console.WriteLine("格納した鍵ペアを削除しました。");
            }
            catch (ArgumentException e)
            {   // デジタル署名の XML データが不正な場合など
                Console.WriteLine("引数が不正です。");
                Console.WriteLine(e.Message);
            }
            catch (FormatException e)
            {   // デジタル署名の BASE64 エンコーディング データが不正な場合など
                Console.WriteLine("引数の形式が不正です。");
                Console.WriteLine(e.Message);
            }
            catch (OutOfMemoryException e)
            {   // メモリ不足な場合
                Console.WriteLine("メモリが足りません。");
                Console.WriteLine(e.Message);
            }
            catch (Exception e)
            {
                Console.WriteLine("その他のエラーです。");
                Console.WriteLine(e.Message);
            }

            Console.WriteLine("Press any key...");
            Console.ReadKey();
            
        }

        /// <summary>
        /// デジタル署名を行います。
        /// (BASE64 変換を行ってから署名を行います。)
        /// </summary>
        /// <param name="target">デジタル署名対象の UTF-8 文字列</param>
        /// <param name="containerName">鍵ペアを保存しているコンテナの名称</param>
        /// <param name="algorithm">ハッシュ計算アルゴリズム</param>
        /// <returns></returns>
        static string sign(string target, string containerName, HashKind algorithm)
        {
            var bytes = Encoding.UTF8.GetBytes(target);
            return sign(bytes, containerName, algorithm);
        }
        /// <summary>
        /// デジタル署名を行います。
        /// (BASE64 変換を行ってから署名を行います。)
        /// </summary>
        /// <param name="target">デジタル署名対象の byte 配列</param>
        /// <param name="containerName">鍵ペアを保存しているコンテナの名称</param>
        /// <param name="algorithm">ハッシュ計算アルゴリズム</param>
        /// <returns></returns>
        static string sign(byte[] target, string containerName, HashKind algorithm)
        {
            // データ長のチェック
            if (target.Length > 1614648000)
            {
                throw new ArgumentException("署名対象が大きすぎます。");
            }

            // SignedData 生成
            var signedData = new SignedData(target, algorithm);

            Console.WriteLine("署名対象");
            Console.WriteLine(signedData.Base64Data);
            var hash = GetHash(signedData.Base64Data, algorithm);

            // デジタル署名
            string signature = null;
            using (var rsa = GetRsaProvider(containerName))
            {
                signature = GetDigitalSignature(rsa, hash, algorithm);
            }
            Console.WriteLine("---");
            Console.WriteLine("デジタル署名(BASE64)");
            Console.WriteLine(signature);

            // 署名を格納
            signedData.Signature = signature;
            // SignedData を文字列化
            var xml = signedData.ToXmlString();
            Console.WriteLine("---");
            Console.WriteLine("XML データ");
            Console.WriteLine(xml);

            return xml;
        }

        /// <summary>
        /// RSA サービスプロバイダを取得します。
        /// 指定された鍵ペアのコンテナが無い場合には、鍵ペアの生成と生成した鍵ペアの保存を行います。
        /// 保存先は %AppData%\Microsoft\Crypto\RSA\S-1-5-21-2207887259-3328070093-2314057241-1000 フォルダ
        /// CspProviderFlags.UseMachineKeyStoreを指定した場合 %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys フォルダ
        /// </summary>
        /// <param name="containerName">保存コンテナ名</param>
        /// <returns></returns>
        static RSACryptoServiceProvider GetRsaProvider(string containerName)
        {
            var csp = new CspParameters();
            csp.KeyContainerName = containerName;

            return new RSACryptoServiceProvider(csp);
        }

        /// <summary>
        /// 公開鍵だけを格納する XML 文字列を取得します。
        /// </summary>
        /// <param name="containerName">保存コンテナ名</param>
        /// <returns></returns>
        static string GetPublickey(string containerName)
        {
            string xmlString = null;
            using (var rsa = GetRsaProvider(containerName))
            {
                xmlString = rsa.ToXmlString(false);
            }

            return xmlString;
        }

        /// <summary>
        /// 鍵ペアの保存コンテナを削除します。
        /// </summary>
        /// <param name="containerName">保存コンテナ名</param>
        static void DeleteKeyFromContainer(string containerName)
        {
            var csp = new CspParameters();
            csp.KeyContainerName = containerName;

            using (var rsa = new RSACryptoServiceProvider(csp))
            {
                rsa.PersistKeyInCsp = false;
            }
        }

        /// <summary>
        /// ターゲットのハッシュ値を取得します。
        /// </summary>
        /// <param name="target">ハッシュ値を取得する UTF-8 文字列</param>
        /// <returns></returns>
        static byte[] GetHash(string target, HashKind algorithm)
        {
            var bytes = Encoding.UTF8.GetBytes(target);
            return GetHash(bytes, algorithm);
        }
        /// <summary>
        /// ターゲットのハッシュ値を取得します。
        /// </summary>
        /// <param name="target">ハッシュ値を取得するバイト配列</param>
        /// <returns></returns>
        static byte[] GetHash(byte[] target, HashKind algorithm)
        {
            byte[] hash = null;
            using (var sha = GetHashAlgorithm(algorithm))
            {
                hash = sha.ComputeHash(target);
            }

            return hash;
        }

        /// <summary>
        /// 指定されたハッシュ計算アルゴリズムのインスタンスを取得します。
        /// </summary>
        /// <param name="hashKind"></param>
        /// <returns></returns>
        static HashAlgorithm GetHashAlgorithm(HashKind hashKind)
        {
            HashAlgorithm result = null;
            switch (hashKind)
            {
                case HashKind.SHA256:
                    result = new SHA256Managed();
                    break;
                case HashKind.SHA384:
                    result = new SHA384Managed();
                    break;
                case HashKind.SHA512:
                    result = new SHA512Managed();
                    break;
            }

            return result;
        }

        /// <summary>
        /// BASE64 エンコードしたデジタル署名を取得します。
        /// </summary>
        /// <param name="rsa"></param>
        /// <param name="hash"></param>
        /// <param name="algorithm"></param>
        /// <returns></returns>
        static string GetDigitalSignature(RSACryptoServiceProvider rsa, byte[] hash, HashKind algorithm)
        {
            // デジタル署名用のクラスの作成
            var rsaFormatter = new RSAPKCS1SignatureFormatter(rsa);
            rsaFormatter.SetHashAlgorithm(algorithm.ToString());
            var bytes = rsaFormatter.CreateSignature(hash);

            return Convert.ToBase64String(bytes);
        }

        /// <summary>
        /// デジタル署名を検証します。
        /// </summary>
        /// <param name="xmlPublicKey">公開鍵だけを格納する XML 文字列</param>
        /// <param name="target">ハッシュ値を計算する UTF8 文字列</param>
        /// <param name="signature">BASE64 エンコーディングされたデジタル署名</param>
        /// <param name="algorithm">ハッシュ計算アルゴリズム</param>
        /// <returns></returns>
        static bool VerifyDigitalSignature(string xmlPublicKey, string target, string signature, HashKind algorithm)
        {
            var bytes = Encoding.UTF8.GetBytes(target);
            return VerifyDigitalSignature(xmlPublicKey, bytes, signature, algorithm);
        }
        /// <summary>
        /// デジタル署名を検証します。
        /// </summary>
        /// <param name="xmlPublicKey">公開鍵だけを格納する XML 文字列</param>
        /// <param name="target">ハッシュ値を計算する バイト配列</param>
        /// <param name="signature">BASE64 エンコーディングされたデジタル署名</param>
        /// <param name="algorithm">ハッシュ計算アルゴリズム</param>
        /// <returns></returns>
        static bool VerifyDigitalSignature(string xmlPublicKey, byte[] target, string signature, HashKind algorithm)
        {
            var result = false;
            var hash = GetHash(target, algorithm);
            var sigBytes = Convert.FromBase64String(signature);
            using (var rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(xmlPublicKey);
                var rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
                rsaDeformatter.SetHashAlgorithm(algorithm.ToString());
                result = rsaDeformatter.VerifySignature(hash, sigBytes);
            }

            return result;
        }
    }
}

Main メソッドの先頭で「文字列」「XML 形式の文字列」「ファイル」をデジタル署名対象とするコードがあり、後ろ2つのコードはコメント化してあります(ファイルは読み込み操作があるので複数行になっています)。

ファイルの後ろに「ハッシュ計算アルゴリズムの設定」と「鍵ペア保存コンテナ名の設定」があります。テスト用の鍵ペアを格納したコンテナは、最後に削除しています。
鍵ペアの保存については、DOBON.NET プログラミング道 さんの「秘密鍵をCSPキーコンテナに保存する」や MSDN ライブラリの「方法 : キー コンテナーに非対称キーを格納する」が参考になります。

あとはコメントを入れているのでやっていることは分かるかと思いますが、補足事項を3つだけ。

このプログラムでは、署名対象の選択部分は実装していません。そのため、署名対象の属性情報(種類や名称等)の保持も行っていません。別解としては、署名対象がファイルの場合には、ファイル自体は保持せずに署名対象のファイルパスと署名対象がファイルであることを示すフラグを持つことも考えられます。

このプログラムでは公開キーをコンテナから取得していますが、実際の場面では鍵ペアのコンテナのある環境で署名検証を行うのはレアケースになると思います。実際の場面では「公開鍵の安全な渡し方(逆に言うと「その公開鍵が検証に使って大丈夫なことの確証の確保」)」と「検証に使う公開鍵の選択」が必要になるケースが出てくることもあると思います(必要ないケースとしては、プログラムに公開鍵を仕込んでおいて、プログラムのアクセス先データの正当性の検証に使う場合などがあるかと(厳密なセキュリティ対策が必要な場合にはプログラム自体の改ざん防止あるいは検出方策が必要になりますが))。検証に使う公開鍵の選択はシステム的な仕掛けが必要になりますが、このプログラムでは公開鍵の選択機能は実装していません。

鍵ペアの保存はキーコンテナを用いてコンピュータ上に行っていますが、マシントラブル等で失われてしまうことへの対策も必要です(コンテナの保存先フォルダを含むバックアップを取っておくか、個別に保管しておくか)。個別に保管する場合には、Program クラスの GetPublickey メソッドで使用している RSA クラスの ToXmlString メソッドの引数を true にすることで秘密鍵を含む XML 文字列を取得することができますが、秘密鍵を含んでいるので安全な保管状態を維持し続ける必要があります。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です