SignUsbToken.cs
// 
// このコードは、DioDocs for PDF のサンプルの一部として提供されています。
// © MESCIUS inc. All rights reserved.
// 
using System;
using System.IO;
using System.Drawing;
using System.Text;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;

using Net.Pkcs11Interop.Common;
using Net.Pkcs11Interop.HighLevelAPI;

using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Pdf.Security;
using GrapeCity.Documents.Pdf.AcroForms;
using GrapeCity.Documents.Text;


namespace DsPdfWeb.Demos
{
    // このサンプルでは、空の署名フィールドを含む既存の PDF ファイルに、
    // DSC(デジタル署名証明書、Digital Signature Certificate)用の USB トークンに
    // 格納された証明書を使用して署名する方法を示しています。
    //
    // サンプルには、GrapeCity.Documents.Pdf.IPkcs7SignatureGenerator インターフェースを実装した、
    // 実用的な Pkcs11SignatureGenerator クラスが含まれており、DSC 用 の USB トークンに
    // 格納された証明書を使用して PDF に署名するために利用することができます。
    //  
    // なお、デモサイトで実行する場合、このサンプルはダミーのライブラリ名やパラメーターを
    // Pkcs11SignatureGenerator クラスのコンストラクターに渡すため、PDF には署名しません。
    // 実際に PDF に署名するためには、サンプルをダウンロードし、サンプルコードに
    // 独自のライブラリとパラメータを指定する必要があります。
    //
    public class SignUsbToken
    {
        public int CreatePDF(Stream stream)
        {
            var doc = new GcPdfDocument();
            using var s = File.OpenRead(Path.Combine("Resources", "PDFs", "SignUsbToken.pdf"));
            doc.Load(s);

            try
            {
                // DSC のライブラリ名やパラメーターがダミーの USB トークンであるため、署名はしません。
                // 実際に PDF に署名するためには、有効なライブラリ名とパラメーターを指定してください。
                using var sg = new Pkcs11SignatureGenerator(
                    "path-to-dummy-PKCS11.dll",
                    null,
                    null,
                    Encoding.ASCII.GetBytes("12345"),
                    null,
                    null,
                    OID.HashAlgorithms.SHA512);

                var sp = new SignatureProperties()
                {
                    SignatureBuilder = new Pkcs7SignatureBuilder()
                    {
                        SignatureGenerator = sg,
                        CertificateChain = new X509Certificate2[] { sg.Certificate },
                    },
                    SignatureField = doc.AcroForm.Fields[0]
                };
                doc.Sign(sp, stream);
            }
            catch (Exception)
            {
                var page = doc.Pages[0];
                var r = doc.AcroForm.Fields[0].Widgets[0].Rect;
                Common.Util.AddNote(
                    "DSC のライブラリ名やパラメーターがダミーの USB トークンが使用されたため、署名に失敗しました。\n" +
                    "PDF に署名するには、有効な USB トークンのライブラリと正しいパラメーターを指定してください。",
                    page,
                    new RectangleF(72, r.Bottom + 24, page.Size.Width - 72 * 2, 100));
                doc.Save(stream);
            }

            // 終了
            return doc.Pages.Count;
        }
    }

    /// <summary>
    /// <see cref="IPkcs7SignatureGenerator"/> を実装し、
    /// DSC(デジタル署名証明書)用の USB トークンに格納された
    /// 証明書を使用してデジタル署名を生成できるようにします。
    /// 
    /// トークンの管理には、<b>Pkcs11Interop</b> NuGet パッケージが使用されています。
    /// </summary>
    public class Pkcs11SignatureGenerator : IPkcs7SignatureGenerator, IDisposable
    {
        public static readonly Pkcs11InteropFactories Factories = new Pkcs11InteropFactories();

        private IPkcs11Library _pkcs11Library;
        private ISlot _slot;
        private ISession _session;
        private IObjectHandle _privateKeyHandle;
        private string _ckaLabel;
        private byte[] _ckaId;
        private X509Certificate2 _certificate;
        private OID _hashAlgorithm;
        private IDigest _hashDigest;

        /// <summary>
        /// <see cref="Pkcs11SignatureGenerator"/> クラスの新しいインスタンスを初期化します。
        /// <paramref name="tokenSerial"/> と <paramref name="tokenLabel"/> パラメーターは、
        /// 複数のトークンが接続されている場合に、使用するトークンを選択するために使用されます。 
        /// トークンが1つのみ接続されている場合、これらのパラメーターは両方とも <see langword="null"/> にできます。
        /// <paramref name="ckaLabel"/> と <paramref name="ckaId"/> パラメーターは、
        /// トークンが複数の鍵を含んでいる場合に、使用する秘密鍵を選択するために使用されます。
        /// トークンが1つのみの秘密鍵を含む場合、これらのパラメーターは両方とも <see langword="null"/> にできます。
        /// </summary>
        /// <param name="libraryPath">使用するアンマネージドの PCKS #11 ライブラリへのパス</param>
        /// <param name="tokenSerial">署名キーを含むトークン(スマートカード)のシリアル番号</param>
        /// <param name="tokenLabel">署名キーを含むトークン(スマートカード)のラベル</param>
        /// <param name="pin">トークン(スマートカード)のPIN</param>
        /// <param name="ckaLabel">署名に使用される秘密鍵のラベル(CKA_LABEL 属性の値)</param>
        /// <param name="ckaId">署名に使用される秘密鍵の識別子(CKA_ID 属性の値)を16進数に変換した文字列</param>
        /// <param name="hashAlgorihtm">署名を作成する際に使用するハッシュアルゴリズム</param>
        public Pkcs11SignatureGenerator(string libraryPath, string tokenSerial, string tokenLabel, byte[] pin, string ckaLabel, byte[] ckaId, OID hashAlgorihtm)
        {
            Init(libraryPath, tokenSerial, tokenLabel, pin, ckaLabel, ckaId, hashAlgorihtm);
        }

        ~Pkcs11SignatureGenerator()
        {
            Dispose(false);
        }

        /// <summary>
        /// オブジェクトが使用しているリソースを解放します。
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_certificate != null)
                {
                    _certificate.Dispose();
                    _certificate = null;
                }
                if (_session != null)
                {
                    _session.Dispose();
                    _session = null;
                }
                if (_pkcs11Library != null)
                {
                    _pkcs11Library.Dispose();
                    _pkcs11Library = null;
                }
            }
        }

        private ISlot FindSlot(string tokenSerial, string tokenLabel)
        {
            if (string.IsNullOrEmpty(tokenSerial) && string.IsNullOrEmpty(tokenLabel))
                throw new ArgumentException("トークンのシリアルやラベルを指定する必要があります。");

            List<ISlot> slots = _pkcs11Library.GetSlotList(SlotsType.WithTokenPresent);
            foreach (ISlot slot in slots)
            {
                ITokenInfo tokenInfo = null;

                try
                {
                    tokenInfo = slot.GetTokenInfo();
                }
                catch (Pkcs11Exception ex)
                {
                    if (ex.RV != CKR.CKR_TOKEN_NOT_RECOGNIZED && ex.RV != CKR.CKR_TOKEN_NOT_PRESENT)
                        throw;
                }

                if (tokenInfo == null)
                    continue;

                if (!string.IsNullOrEmpty(tokenSerial))
                    if (String.Compare(tokenSerial, tokenInfo.SerialNumber, StringComparison.InvariantCultureIgnoreCase) != 0)
                        continue;

                if (!string.IsNullOrEmpty(tokenLabel))
                    if (String.Compare(tokenLabel, tokenInfo.Label, StringComparison.InvariantCultureIgnoreCase) != 0)
                        continue;

                return slot;
            }
            return null;
        }

        protected void Init(string libraryPath, string tokenSerial, string tokenLabel, byte[] pin, string ckaLabel, byte[] ckaId, OID hashAlgorihtm)
        {
            if (string.IsNullOrEmpty(libraryPath))
                throw new ArgumentNullException($"\"{libraryPath}\" は無効なライブラリパスです。");

            try
            {
                _pkcs11Library = Factories.Pkcs11LibraryFactory.LoadPkcs11Library(Factories, libraryPath, AppType.SingleThreaded);

                _slot = FindSlot(tokenSerial, tokenLabel);
                if (_slot == null)
                    throw new Exception(string.Format("シリアル \"{0}\" とラベル \"{1}\" を持つトークンが見つかりませんでした。", tokenSerial, tokenLabel));

                _session = _slot.OpenSession(SessionType.ReadOnly);
                _session.Login(CKU.CKU_USER, pin);

                // _privateKeyHandle と _certificate を初期化します。
                using (ISession session = _slot.OpenSession(SessionType.ReadOnly))
                {
                    // 秘密鍵
                    List<IObjectAttribute> searchTemplate = new List<IObjectAttribute>();
                    searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_PRIVATE_KEY));
                    searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_KEY_TYPE, CKK.CKK_RSA));
                    if (!string.IsNullOrEmpty(ckaLabel))
                        searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, ckaLabel));
                    if (ckaId != null)
                        searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_ID, ckaId));

                    List<IObjectHandle> foundObjects = session.FindAllObjects(searchTemplate);
                    if (foundObjects.Count < 1)
                        throw new Exception(string.Format("ラベル \"{0}\" と ID \"{1}\" を持つ秘密鍵が見つかりませんでした。", ckaLabel, (ckaId == null) ? null : ConvertUtils.BytesToHexString(ckaId)));
                    else if (foundObjects.Count > 1)
                        throw new Exception(string.Format("ラベル \"{0}\" と ID \"{1}\" を持つ秘密鍵が複数見つかりました。", ckaLabel, (ckaId == null) ? null : ConvertUtils.BytesToHexString(ckaId)));
                    _privateKeyHandle = foundObjects[0];

                    // 証明書
                    searchTemplate.Clear();
                    searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_CERTIFICATE));
                    if (!string.IsNullOrEmpty(ckaLabel))
                        searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, ckaLabel));
                    if (ckaId != null)
                        searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_ID, ckaId));

                    foundObjects = session.FindAllObjects(searchTemplate);
                    if (foundObjects.Count == 1)
                    {
                        List<CKA> attributes = new List<CKA>();
                        attributes.Add(CKA.CKA_VALUE);

                        List<IObjectAttribute> certificateAttributes = session.GetAttributeValue(foundObjects[0], attributes);
                        byte[] certificateData = certificateAttributes[0].GetValueAsByteArray();
                        _certificate = new X509Certificate2(certificateData);
                    }
                }

                _ckaLabel = ckaLabel;
                _ckaId = ckaId;
                if (hashAlgorihtm == OID.HashAlgorithms.SHA1)
                    _hashDigest = new Sha1Digest();
                else if (hashAlgorihtm == OID.HashAlgorithms.SHA256)
                    _hashDigest = new Sha256Digest();
                else if (hashAlgorihtm == OID.HashAlgorithms.SHA384)
                    _hashDigest = new Sha384Digest();
                else if (hashAlgorihtm == OID.HashAlgorithms.SHA512)
                    _hashDigest = new Sha512Digest();
                else
                    throw new Exception($"{hashAlgorihtm} はサポートされていないハッシュアルゴリズムです。");
                _hashAlgorithm = hashAlgorihtm;
            }
            catch
            {
                if (_session != null)
                {
                    _session.Dispose();
                    _session = null;
                }
                if (_pkcs11Library != null)
                {
                    _pkcs11Library.Dispose();
                    _pkcs11Library = null;
                }

                throw;
            }
        }

        /// <summary>
        /// 秘密鍵と同じ <b>ckaLabel</b> と <b>ckaId</b> を持つトークンに存在する
        /// <see cref="Sys.X509Certificate2"/> オブジェクトを取得します。
        /// </summary>
        public X509Certificate2 Certificate
        {
            get { return _certificate; }
        }

        /// <summary>
        /// ハッシュアルゴリズムの ID を取得します。
        /// </summary>
        public OID HashAlgorithm => _hashAlgorithm;

        /// <summary>
        /// 暗号化アルゴリズムの ID を取得します。
        /// </summary>
        public OID DigestEncryptionAlgorithm => OID.EncryptionAlgorithms.RSA;

        /// <summary>
        /// 署名データです。
        /// </summary>
        /// <param name="input">署名する入力データ</param>
        /// <returns>署名されたデータ</returns>
        public byte[] SignData(byte[] input)
        {
            using (ISession session = _slot.OpenSession(SessionType.ReadOnly))
            using (IMechanism mechanism = Factories.MechanismFactory.Create(CKM.CKM_RSA_PKCS))
            {
                byte[] hash = new byte[_hashDigest.GetDigestSize()];
                _hashDigest.Reset();
                _hashDigest.BlockUpdate(input, 0, input.Length);
                _hashDigest.DoFinal(hash, 0);

                var derObjectIdentifier = new DerObjectIdentifier(_hashAlgorithm.ID);
                var algorithmIdentifier = new AlgorithmIdentifier(derObjectIdentifier, null);
                var digestInfo = new DigestInfo(algorithmIdentifier, hash);
                byte[] digestInfoBytes = digestInfo.GetDerEncoded();

                return session.Sign(mechanism, _privateKeyHandle, digestInfoBytes);
            }
        }
    }
}