SignUsbToken.cs
// // This code is part of Document Solutions for PDF demos. // Copyright (c) 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 { // This sample shows how to sign an existing PDF file that contains // an empty signature field with a certificate that is stored // on a USB Token for DSC (Digital Signature Certificate). // // The sample includes a ready to use utility class Pkcs11SignatureGenerator // that implements the GrapeCity.Documents.Pdf.IPkcs7SignatureGenerator interface, // and can be used to sign PDFs with certificates stored on a USB Token for DSC. // // Please note that when run directly off the DsPdf demo site, // this sample will NOT sign the PDF, as it passes dummy library name/parameters. // to the Pkcs11SignatureGenerator's ctor. You will need to download the sample // and provide your own library and parameters for the sample code to actually sign a 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 { // This WILL NOT WORK due to dummy USB Token for DSC library name/parameters. // Supply valid library name and parameters to actually sign the 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( "Signing failed because a dummy USB Token for DSC library name and dummy parameters were used.\n" + "Provide a valid USB Token library and correct parameters to sign the PDF.", page, new RectangleF(r.Left, r.Bottom + 24, page.Size.Width - r.Left * 2, 0)); doc.Save(stream); } // Done. return doc.Pages.Count; } } /// <summary> /// Implements <see cref="IPkcs7SignatureGenerator"/> /// and allows generating a digital signature using a certificate /// stored on a USB Token for DSC (Digital Signature Certificate). /// /// The <b>Pkcs11Interop</b> NuGet package is used to manage the token. /// </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> /// Initializes a new instance of the <see cref="Pkcs11SignatureGenerator"/> class. /// The <paramref name="tokenSerial"/> and <paramref name="tokenLabel"/> parameters are used /// to select the token to use if several tokens are connected. /// If only one token is connected then both these parameters can be <see langword="null"/>. /// The <paramref name="ckaLabel"/> and <paramref name="ckaId"/> parameters are used /// to select the private key to use if the token contains multiple keys. /// If the token contains a single private key then both these parameters can be <see langword="null"/>. /// </summary> /// <param name="libraryPath">Path to the unmanaged PCKS#11 library to use.</param> /// <param name="tokenSerial">Serial number of the token (smartcard) that contains the signing key.</param> /// <param name="tokenLabel">Label of the token (smartcard) that contains the signing key.</param> /// <param name="pin">PIN for the token (smartcard).</param> /// <param name="ckaLabel">Label (value of CKA_LABEL attribute) of the private key used for signing.</param> /// <param name="ckaId">Hex encoded string with identifier (value of CKA_ID attribute) of the private key used for signing.</param> /// <param name="hashAlgorihtm">The hash algorithm to use when creating the signature.</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> /// Releases resources used by this object. /// </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("Token serial and/or label has to be specified"); 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($"Invalid library path \"{libraryPath}\"."); try { _pkcs11Library = Factories.Pkcs11LibraryFactory.LoadPkcs11Library(Factories, libraryPath, AppType.SingleThreaded); _slot = FindSlot(tokenSerial, tokenLabel); if (_slot == null) throw new Exception(string.Format("Token with serial \"{0}\" and label \"{1}\" was not found", tokenSerial, tokenLabel)); _session = _slot.OpenSession(SessionType.ReadOnly); _session.Login(CKU.CKU_USER, pin); // initialize _privateKeyHandle and _certificate using (ISession session = _slot.OpenSession(SessionType.ReadOnly)) { // private key 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("Private key with label \"{0}\" and id \"{1}\" was not found.", ckaLabel, (ckaId == null) ? null : ConvertUtils.BytesToHexString(ckaId))); else if (foundObjects.Count > 1) 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))); _privateKeyHandle = foundObjects[0]; // certificate 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($"Unsupported HASH algorithm {hashAlgorihtm}."); _hashAlgorithm = hashAlgorihtm; } catch { if (_session != null) { _session.Dispose(); _session = null; } if (_pkcs11Library != null) { _pkcs11Library.Dispose(); _pkcs11Library = null; } throw; } } /// <summary> /// Gets the <see cref="Sys.X509Certificate2"/> object found on the token /// with same <b>ckaLabel</b> and <b>ckaId</b> as a private key. /// </summary> public X509Certificate2 Certificate { get { return _certificate; } } /// <summary> /// Gets the ID of the hash algorithm. /// </summary> public OID HashAlgorithm => _hashAlgorithm; /// <summary> /// Gets the ID of the encryption algorithm. /// </summary> public OID DigestEncryptionAlgorithm => OID.EncryptionAlgorithms.RSA; /// <summary> /// Signs data. /// </summary> /// <param name="input">The input data to sign.</param> /// <returns>The signed data.</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, DerNull.Instance); var digestInfo = new DigestInfo(algorithmIdentifier, hash); byte[] digestInfoBytes = digestInfo.GetDerEncoded(); return session.Sign(mechanism, _privateKeyHandle, digestInfoBytes); } } } }