Sign a PDF using a USB Token for DSC (Digital Signature Certificate)

PDF TIFF SVG JPG C# VB
SignUsbToken.cs
  1. //
  2. // This code is part of Document Solutions for PDF demos.
  3. // Copyright (c) MESCIUS inc. All rights reserved.
  4. //
  5. using System;
  6. using System.IO;
  7. using System.Drawing;
  8. using System.Text;
  9. using System.Collections.Generic;
  10. using System.Security.Cryptography;
  11. using System.Security.Cryptography.X509Certificates;
  12.  
  13. using Org.BouncyCastle.Crypto;
  14. using Org.BouncyCastle.Crypto.Digests;
  15. using Org.BouncyCastle.Asn1;
  16. using Org.BouncyCastle.Asn1.X509;
  17.  
  18. using Net.Pkcs11Interop.Common;
  19. using Net.Pkcs11Interop.HighLevelAPI;
  20.  
  21. using GrapeCity.Documents.Pdf;
  22. using GrapeCity.Documents.Pdf.Security;
  23. using GrapeCity.Documents.Pdf.AcroForms;
  24. using GrapeCity.Documents.Text;
  25.  
  26.  
  27. namespace DsPdfWeb.Demos
  28. {
  29. // This sample shows how to sign an existing PDF file that contains
  30. // an empty signature field with a certificate that is stored
  31. // on a USB Token for DSC (Digital Signature Certificate).
  32. //
  33. // The sample includes a ready to use utility class Pkcs11SignatureGenerator
  34. // that implements the GrapeCity.Documents.Pdf.IPkcs7SignatureGenerator interface,
  35. // and can be used to sign PDFs with certificates stored on a USB Token for DSC.
  36. //
  37. // Please note that when run directly off the DsPdf demo site,
  38. // this sample will NOT sign the PDF, as it passes dummy library name/parameters.
  39. // to the Pkcs11SignatureGenerator's ctor. You will need to download the sample
  40. // and provide your own library and parameters for the sample code to actually sign a PDF.
  41. //
  42. public class SignUsbToken
  43. {
  44. public int CreatePDF(Stream stream)
  45. {
  46. var doc = new GcPdfDocument();
  47. using var s = File.OpenRead(Path.Combine("Resources", "PDFs", "SignUsbToken.pdf"));
  48. doc.Load(s);
  49.  
  50. try
  51. {
  52. // This WILL NOT WORK due to dummy USB Token for DSC library name/parameters.
  53. // Supply valid library name and parameters to actually sign the PDF.
  54. using var sg = new Pkcs11SignatureGenerator(
  55. "path-to-dummy-PKCS11.dll",
  56. null,
  57. null,
  58. Encoding.ASCII.GetBytes("12345"),
  59. null,
  60. null,
  61. OID.HashAlgorithms.SHA512);
  62.  
  63. var sp = new SignatureProperties()
  64. {
  65. SignatureBuilder = new Pkcs7SignatureBuilder()
  66. {
  67. SignatureGenerator = sg,
  68. CertificateChain = new X509Certificate2[] { sg.Certificate },
  69. },
  70. SignatureField = doc.AcroForm.Fields[0]
  71. };
  72. doc.Sign(sp, stream);
  73. }
  74. catch (Exception)
  75. {
  76. var page = doc.Pages[0];
  77. var r = doc.AcroForm.Fields[0].Widgets[0].Rect;
  78. Common.Util.AddNote(
  79. "Signing failed because a dummy USB Token for DSC library name and dummy parameters were used.\n" +
  80. "Provide a valid USB Token library and correct parameters to sign the PDF.",
  81. page,
  82. new RectangleF(r.Left, r.Bottom + 24, page.Size.Width - r.Left * 2, 0));
  83. doc.Save(stream);
  84. }
  85.  
  86. // Done.
  87. return doc.Pages.Count;
  88. }
  89. }
  90.  
  91. /// <summary>
  92. /// Implements <see cref="IPkcs7SignatureGenerator"/>
  93. /// and allows generating a digital signature using a certificate
  94. /// stored on a USB Token for DSC (Digital Signature Certificate).
  95. ///
  96. /// The <b>Pkcs11Interop</b> NuGet package is used to manage the token.
  97. /// </summary>
  98. public class Pkcs11SignatureGenerator : IPkcs7SignatureGenerator, IDisposable
  99. {
  100. public static readonly Pkcs11InteropFactories Factories = new Pkcs11InteropFactories();
  101.  
  102. private IPkcs11Library _pkcs11Library;
  103. private ISlot _slot;
  104. private ISession _session;
  105. private IObjectHandle _privateKeyHandle;
  106. private string _ckaLabel;
  107. private byte[] _ckaId;
  108. private X509Certificate2 _certificate;
  109. private OID _hashAlgorithm;
  110. private IDigest _hashDigest;
  111.  
  112. /// <summary>
  113. /// Initializes a new instance of the <see cref="Pkcs11SignatureGenerator"/> class.
  114. /// The <paramref name="tokenSerial"/> and <paramref name="tokenLabel"/> parameters are used
  115. /// to select the token to use if several tokens are connected.
  116. /// If only one token is connected then both these parameters can be <see langword="null"/>.
  117. /// The <paramref name="ckaLabel"/> and <paramref name="ckaId"/> parameters are used
  118. /// to select the private key to use if the token contains multiple keys.
  119. /// If the token contains a single private key then both these parameters can be <see langword="null"/>.
  120. /// </summary>
  121. /// <param name="libraryPath">Path to the unmanaged PCKS#11 library to use.</param>
  122. /// <param name="tokenSerial">Serial number of the token (smartcard) that contains the signing key.</param>
  123. /// <param name="tokenLabel">Label of the token (smartcard) that contains the signing key.</param>
  124. /// <param name="pin">PIN for the token (smartcard).</param>
  125. /// <param name="ckaLabel">Label (value of CKA_LABEL attribute) of the private key used for signing.</param>
  126. /// <param name="ckaId">Hex encoded string with identifier (value of CKA_ID attribute) of the private key used for signing.</param>
  127. /// <param name="hashAlgorihtm">The hash algorithm to use when creating the signature.</param>
  128. public Pkcs11SignatureGenerator(string libraryPath, string tokenSerial, string tokenLabel, byte[] pin, string ckaLabel, byte[] ckaId, OID hashAlgorihtm)
  129. {
  130. Init(libraryPath, tokenSerial, tokenLabel, pin, ckaLabel, ckaId, hashAlgorihtm);
  131. }
  132.  
  133. ~Pkcs11SignatureGenerator()
  134. {
  135. Dispose(false);
  136. }
  137.  
  138. /// <summary>
  139. /// Releases resources used by this object.
  140. /// </summary>
  141. public void Dispose()
  142. {
  143. Dispose(true);
  144. GC.SuppressFinalize(this);
  145. }
  146.  
  147. protected void Dispose(bool disposing)
  148. {
  149. if (disposing)
  150. {
  151. if (_certificate != null)
  152. {
  153. _certificate.Dispose();
  154. _certificate = null;
  155. }
  156. if (_session != null)
  157. {
  158. _session.Dispose();
  159. _session = null;
  160. }
  161. if (_pkcs11Library != null)
  162. {
  163. _pkcs11Library.Dispose();
  164. _pkcs11Library = null;
  165. }
  166. }
  167. }
  168.  
  169. private ISlot FindSlot(string tokenSerial, string tokenLabel)
  170. {
  171. if (string.IsNullOrEmpty(tokenSerial) && string.IsNullOrEmpty(tokenLabel))
  172. throw new ArgumentException("Token serial and/or label has to be specified");
  173.  
  174. List<ISlot> slots = _pkcs11Library.GetSlotList(SlotsType.WithTokenPresent);
  175. foreach (ISlot slot in slots)
  176. {
  177. ITokenInfo tokenInfo = null;
  178.  
  179. try
  180. {
  181. tokenInfo = slot.GetTokenInfo();
  182. }
  183. catch (Pkcs11Exception ex)
  184. {
  185. if (ex.RV != CKR.CKR_TOKEN_NOT_RECOGNIZED && ex.RV != CKR.CKR_TOKEN_NOT_PRESENT)
  186. throw;
  187. }
  188.  
  189. if (tokenInfo == null)
  190. continue;
  191.  
  192. if (!string.IsNullOrEmpty(tokenSerial))
  193. if (String.Compare(tokenSerial, tokenInfo.SerialNumber, StringComparison.InvariantCultureIgnoreCase) != 0)
  194. continue;
  195.  
  196. if (!string.IsNullOrEmpty(tokenLabel))
  197. if (String.Compare(tokenLabel, tokenInfo.Label, StringComparison.InvariantCultureIgnoreCase) != 0)
  198. continue;
  199.  
  200. return slot;
  201. }
  202. return null;
  203. }
  204.  
  205. protected void Init(string libraryPath, string tokenSerial, string tokenLabel, byte[] pin, string ckaLabel, byte[] ckaId, OID hashAlgorihtm)
  206. {
  207. if (string.IsNullOrEmpty(libraryPath))
  208. throw new ArgumentNullException($"Invalid library path \"{libraryPath}\".");
  209.  
  210. try
  211. {
  212. _pkcs11Library = Factories.Pkcs11LibraryFactory.LoadPkcs11Library(Factories, libraryPath, AppType.SingleThreaded);
  213.  
  214. _slot = FindSlot(tokenSerial, tokenLabel);
  215. if (_slot == null)
  216. throw new Exception(string.Format("Token with serial \"{0}\" and label \"{1}\" was not found", tokenSerial, tokenLabel));
  217.  
  218. _session = _slot.OpenSession(SessionType.ReadOnly);
  219. _session.Login(CKU.CKU_USER, pin);
  220.  
  221. // initialize _privateKeyHandle and _certificate
  222. using (ISession session = _slot.OpenSession(SessionType.ReadOnly))
  223. {
  224. // private key
  225. List<IObjectAttribute> searchTemplate = new List<IObjectAttribute>();
  226. searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_PRIVATE_KEY));
  227. searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_KEY_TYPE, CKK.CKK_RSA));
  228. if (!string.IsNullOrEmpty(ckaLabel))
  229. searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, ckaLabel));
  230. if (ckaId != null)
  231. searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_ID, ckaId));
  232.  
  233. List<IObjectHandle> foundObjects = session.FindAllObjects(searchTemplate);
  234. if (foundObjects.Count < 1)
  235. throw new Exception(string.Format("Private key with label \"{0}\" and id \"{1}\" was not found.", ckaLabel, (ckaId == null) ? null : ConvertUtils.BytesToHexString(ckaId)));
  236. else if (foundObjects.Count > 1)
  237. throw new Exception(string.Format("More than one private key with label \"{0}\" and id \"{1}\" was found.", ckaLabel, (ckaId == null) ? null : ConvertUtils.BytesToHexString(ckaId)));
  238. _privateKeyHandle = foundObjects[0];
  239.  
  240. // certificate
  241. searchTemplate.Clear();
  242. searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_CERTIFICATE));
  243. if (!string.IsNullOrEmpty(ckaLabel))
  244. searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, ckaLabel));
  245. if (ckaId != null)
  246. searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_ID, ckaId));
  247.  
  248. foundObjects = session.FindAllObjects(searchTemplate);
  249. if (foundObjects.Count == 1)
  250. {
  251. List<CKA> attributes = new List<CKA>();
  252. attributes.Add(CKA.CKA_VALUE);
  253.  
  254. List<IObjectAttribute> certificateAttributes = session.GetAttributeValue(foundObjects[0], attributes);
  255. byte[] certificateData = certificateAttributes[0].GetValueAsByteArray();
  256. _certificate = new X509Certificate2(certificateData);
  257. }
  258. }
  259.  
  260. _ckaLabel = ckaLabel;
  261. _ckaId = ckaId;
  262. if (hashAlgorihtm == OID.HashAlgorithms.SHA1)
  263. _hashDigest = new Sha1Digest();
  264. else if (hashAlgorihtm == OID.HashAlgorithms.SHA256)
  265. _hashDigest = new Sha256Digest();
  266. else if (hashAlgorihtm == OID.HashAlgorithms.SHA384)
  267. _hashDigest = new Sha384Digest();
  268. else if (hashAlgorihtm == OID.HashAlgorithms.SHA512)
  269. _hashDigest = new Sha512Digest();
  270. else
  271. throw new Exception($"Unsupported HASH algorithm {hashAlgorihtm}.");
  272. _hashAlgorithm = hashAlgorihtm;
  273. }
  274. catch
  275. {
  276. if (_session != null)
  277. {
  278. _session.Dispose();
  279. _session = null;
  280. }
  281. if (_pkcs11Library != null)
  282. {
  283. _pkcs11Library.Dispose();
  284. _pkcs11Library = null;
  285. }
  286.  
  287. throw;
  288. }
  289. }
  290.  
  291. /// <summary>
  292. /// Gets the <see cref="Sys.X509Certificate2"/> object found on the token
  293. /// with same <b>ckaLabel</b> and <b>ckaId</b> as a private key.
  294. /// </summary>
  295. public X509Certificate2 Certificate
  296. {
  297. get { return _certificate; }
  298. }
  299.  
  300. /// <summary>
  301. /// Gets the ID of the hash algorithm.
  302. /// </summary>
  303. public OID HashAlgorithm => _hashAlgorithm;
  304.  
  305. /// <summary>
  306. /// Gets the ID of the encryption algorithm.
  307. /// </summary>
  308. public OID DigestEncryptionAlgorithm => OID.EncryptionAlgorithms.RSA;
  309.  
  310. /// <summary>
  311. /// Signs data.
  312. /// </summary>
  313. /// <param name="input">The input data to sign.</param>
  314. /// <returns>The signed data.</returns>
  315. public byte[] SignData(byte[] input)
  316. {
  317. using (ISession session = _slot.OpenSession(SessionType.ReadOnly))
  318. using (IMechanism mechanism = Factories.MechanismFactory.Create(CKM.CKM_RSA_PKCS))
  319. {
  320. byte[] hash = new byte[_hashDigest.GetDigestSize()];
  321. _hashDigest.Reset();
  322. _hashDigest.BlockUpdate(input, 0, input.Length);
  323. _hashDigest.DoFinal(hash, 0);
  324.  
  325. var derObjectIdentifier = new DerObjectIdentifier(_hashAlgorithm.ID);
  326. var algorithmIdentifier = new AlgorithmIdentifier(derObjectIdentifier, null);
  327. var digestInfo = new DigestInfo(algorithmIdentifier, hash);
  328. byte[] digestInfoBytes = digestInfo.GetDerEncoded();
  329.  
  330. return session.Sign(mechanism, _privateKeyHandle, digestInfoBytes);
  331. }
  332. }
  333. }
  334. }
  335.