SignUsbToken.vb
''
'' This code is part of Document Solutions for PDF .NET demos.
'' Copyright (c) MESCIUS inc. All rights reserved.
''
Imports System
Imports System.IO
Imports System.Drawing
Imports System.Text
Imports System.Collections.Generic
Imports System.Security.Cryptography.X509Certificates
Imports Org.BouncyCastle.Crypto
Imports Org.BouncyCastle.Crypto.Digests
Imports Org.BouncyCastle.Asn1
Imports Org.BouncyCastle.Asn1.X509
Imports Net.Pkcs11Interop.Common
Imports Net.Pkcs11Interop.HighLevelAPI
Imports GrapeCity.Documents.Pdf
Imports GrapeCity.Documents.Pdf.Security
Imports GrapeCity.Documents.Pdf.AcroForms
Imports GrapeCity.Documents.Text

'' 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
    Function CreatePDF(ByVal stream As Stream) As Integer
        Dim doc = New GcPdfDocument()
        Using 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 sg = New Pkcs11SignatureGenerator(
                    "path-to-dummy-PKCS11.dll",
                    Nothing,
                    Nothing,
                    Encoding.ASCII.GetBytes("12345"),
                    Nothing,
                    Nothing,
                    OID.HashAlgorithms.SHA512)

                Dim sp = New SignatureProperties() With {
                    .SignatureBuilder = New Pkcs7SignatureBuilder() With {
                        .SignatureGenerator = sg,
                        .CertificateChain = New X509Certificate2() {sg.Certificate}
                    },
                    .SignatureField = doc.AcroForm.Fields(0)
                }
                doc.Sign(sp, stream)
            End Using
        Catch e As Exception
            Dim page = doc.Pages(0)
            Dim r = doc.AcroForm.Fields(0).Widgets(0).Rect
            Util.AddNote(
                "Signing failed because a dummy USB Token for DSC library name and dummy parameters were used." & vbLf &
                "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)
        End Try
        End Using

        '' Done.
        Return doc.Pages.Count
    End Function
End Class

''' <summary>
''' Implements IPkcs7SignatureGenerator
''' and allows generating a digital signature using a certificate
''' stored on a USB Token for DSC (Digital Signature Certificate).
'''
''' The Pkcs11Interop NuGet package is used to manage the token.
''' </summary>
Public Class Pkcs11SignatureGenerator
    Implements IPkcs7SignatureGenerator
    Implements IDisposable

    Public Shared ReadOnly Factories As New Pkcs11InteropFactories()

    Private _pkcs11Library As IPkcs11Library
    Private _slot As ISlot
    Private _session As ISession
    Private _privateKeyHandle As IObjectHandle
    Private _ckaLabel As String
    Private _ckaId As Byte()
    Private _certificate As X509Certificate2
    Private _hashAlgorithm As OID
    Private _hashDigest As IDigest

    ''' <summary>
    ''' Initializes a new instance of the Pkcs11SignatureGenerator class.
    ''' </summary>
    Public Sub New(ByVal libraryPath As String, ByVal tokenSerial As String, ByVal tokenLabel As String, ByVal pin As Byte(), ByVal ckaLabel As String, ByVal ckaId As Byte(), ByVal hashAlgorihtm As OID)
        Init(libraryPath, tokenSerial, tokenLabel, pin, ckaLabel, ckaId, hashAlgorihtm)
    End Sub

    Protected Overrides Sub Finalize()
        Dispose(False)
        MyBase.Finalize()
    End Sub

    ''' <summary>
    ''' Releases resources used by this object.
    ''' </summary>
    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

    Protected Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
            If _certificate IsNot Nothing Then
                _certificate.Dispose()
                _certificate = Nothing
            End If
            If _session IsNot Nothing Then
                _session.Dispose()
                _session = Nothing
            End If
            If _pkcs11Library IsNot Nothing Then
                _pkcs11Library.Dispose()
                _pkcs11Library = Nothing
            End If
        End If
    End Sub

    Private Function FindSlot(ByVal tokenSerial As String, ByVal tokenLabel As String) As ISlot
        If String.IsNullOrEmpty(tokenSerial) AndAlso String.IsNullOrEmpty(tokenLabel) Then
            Throw New ArgumentException("Token serial and/or label has to be specified")
        End If

        Dim slots As List(Of ISlot) = _pkcs11Library.GetSlotList(SlotsType.WithTokenPresent)
        For Each slot As ISlot In slots
            Dim tokenInfo As ITokenInfo = Nothing

            Try
                tokenInfo = slot.GetTokenInfo()
            Catch ex As Pkcs11Exception
                If ex.RV <> CKR.CKR_TOKEN_NOT_RECOGNIZED AndAlso ex.RV <> CKR.CKR_TOKEN_NOT_PRESENT Then
                    Throw
                End If
            End Try

            If tokenInfo Is Nothing Then
                Continue For
            End If

            If Not String.IsNullOrEmpty(tokenSerial) Then
                If String.Compare(tokenSerial, tokenInfo.SerialNumber, StringComparison.InvariantCultureIgnoreCase) <> 0 Then
                    Continue For
                End If
            End If

            If Not String.IsNullOrEmpty(tokenLabel) Then
                If String.Compare(tokenLabel, tokenInfo.Label, StringComparison.InvariantCultureIgnoreCase) <> 0 Then
                    Continue For
                End If
            End If

            Return slot
        Next
        Return Nothing
    End Function

    Protected Sub Init(ByVal libraryPath As String, ByVal tokenSerial As String, ByVal tokenLabel As String, ByVal pin As Byte(), ByVal ckaLabel As String, ByVal ckaId As Byte(), ByVal hashAlgorihtm As OID)
        If String.IsNullOrEmpty(libraryPath) Then
            Throw New ArgumentNullException($"Invalid library path ""{libraryPath}"".")
        End If

        Try
            _pkcs11Library = Factories.Pkcs11LibraryFactory.LoadPkcs11Library(Factories, libraryPath, AppType.SingleThreaded)

            _slot = FindSlot(tokenSerial, tokenLabel)
            If _slot Is Nothing Then
                Throw New Exception(String.Format("Token with serial ""{0}"" and label ""{1}"" was not found", tokenSerial, tokenLabel))
            End If

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

            '' initialize _privateKeyHandle and _certificate
            Using session As ISession = _slot.OpenSession(SessionType.ReadOnly)
                '' private key
                Dim searchTemplate As New List(Of 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 Not String.IsNullOrEmpty(ckaLabel) Then
                    searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, ckaLabel))
                End If
                If ckaId IsNot Nothing Then
                    searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_ID, ckaId))
                End If

                Dim foundObjects As List(Of IObjectHandle) = session.FindAllObjects(searchTemplate)
                If foundObjects.Count < 1 Then
                    Throw New Exception(String.Format("Private key with label ""{0}"" and id ""{1}"" was not found.", ckaLabel, If(ckaId Is Nothing, Nothing, ConvertUtils.BytesToHexString(ckaId))))
                ElseIf foundObjects.Count > 1 Then
                    Throw New Exception(String.Format("More than one private key with label ""{0}"" and id ""{1}"" was found.", ckaLabel, If(ckaId Is Nothing, Nothing, ConvertUtils.BytesToHexString(ckaId))))
                End If
                _privateKeyHandle = foundObjects(0)

                '' certificate
                searchTemplate.Clear()
                searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_CERTIFICATE))
                If Not String.IsNullOrEmpty(ckaLabel) Then
                    searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_LABEL, ckaLabel))
                End If
                If ckaId IsNot Nothing Then
                    searchTemplate.Add(Factories.ObjectAttributeFactory.Create(CKA.CKA_ID, ckaId))
                End If

                foundObjects = session.FindAllObjects(searchTemplate)
                If foundObjects.Count = 1 Then
                    Dim attributes As New List(Of CKA)()
                    attributes.Add(CKA.CKA_VALUE)

                    Dim certificateAttributes As List(Of IObjectAttribute) = session.GetAttributeValue(foundObjects(0), attributes)
                    Dim certificateData As Byte() = certificateAttributes(0).GetValueAsByteArray()
                    _certificate = New X509Certificate2(certificateData)
                End If
            End Using

            _ckaLabel = ckaLabel
            _ckaId = ckaId
            If hashAlgorihtm = OID.HashAlgorithms.SHA1 Then
                _hashDigest = New Sha1Digest()
            ElseIf hashAlgorihtm = OID.HashAlgorithms.SHA256 Then
                _hashDigest = New Sha256Digest()
            ElseIf hashAlgorihtm = OID.HashAlgorithms.SHA384 Then
                _hashDigest = New Sha384Digest()
            ElseIf hashAlgorihtm = OID.HashAlgorithms.SHA512 Then
                _hashDigest = New Sha512Digest()
            Else
                Throw New Exception($"Unsupported HASH algorithm {hashAlgorihtm}.")
            End If
            _hashAlgorithm = hashAlgorihtm
        Catch
            If _session IsNot Nothing Then
                _session.Dispose()
                _session = Nothing
            End If
            If _pkcs11Library IsNot Nothing Then
                _pkcs11Library.Dispose()
                _pkcs11Library = Nothing
            End If

            Throw
        End Try
    End Sub

    ''' <summary>
    ''' Gets the X509Certificate2 object found on the token
    ''' with same ckaLabel and ckaId as a private key.
    ''' </summary>
    Public ReadOnly Property Certificate As X509Certificate2
        Get
            Return _certificate
        End Get
    End Property

    ''' <summary>
    ''' Gets the ID of the hash algorithm.
    ''' </summary>
    Public ReadOnly Property HashAlgorithm As OID Implements IPkcs7SignatureGenerator.HashAlgorithm
        Get
            Return _hashAlgorithm
        End Get
    End Property

    ''' <summary>
    ''' Gets the ID of the encryption algorithm.
    ''' </summary>
    Public ReadOnly Property DigestEncryptionAlgorithm As OID Implements IPkcs7SignatureGenerator.DigestEncryptionAlgorithm
        Get
            Return OID.EncryptionAlgorithms.RSA
        End Get
    End Property

    ''' <summary>
    ''' Signs data.
    ''' </summary>
    Public Function SignData(ByVal input As Byte()) As Byte() Implements IPkcs7SignatureGenerator.SignData
        Using session As ISession = _slot.OpenSession(SessionType.ReadOnly)
            Using mechanism As IMechanism = Factories.MechanismFactory.Create(CKM.CKM_RSA_PKCS)
                Dim hash = New Byte(_hashDigest.GetDigestSize() - 1) {}
                _hashDigest.Reset()
                _hashDigest.BlockUpdate(input, 0, input.Length)
                _hashDigest.DoFinal(hash, 0)

                Dim derObjectIdentifier = New DerObjectIdentifier(_hashAlgorithm.ID)
                Dim algorithmIdentifier = New AlgorithmIdentifier(derObjectIdentifier, DerNull.Instance)
                Dim digestInfo = New DigestInfo(algorithmIdentifier, hash)
                Dim digestInfoBytes As Byte() = digestInfo.GetDerEncoded()

                Return session.Sign(mechanism, _privateKeyHandle, digestInfoBytes)
            End Using
        End Using
    End Function
End Class