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);
}
}
}
}