//
// 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, null);
var digestInfo = new DigestInfo(algorithmIdentifier, hash);
byte[] digestInfoBytes = digestInfo.GetDerEncoded();
return session.Sign(mechanism, _privateKeyHandle, digestInfoBytes);
}
}
}
}