Individual Signature Process

Introduction

It is possible to create own signature modules that may use other signature backends to create the signature. This way it may be possible to e.g. extend the OpenSSL CMS module, and implement the handling of an OpenSSL engine or use a webservice in an individual module for the signature process.

Also some existing modules (CMS and PAdES) allow you to get the signature data and pass the signature value back. With this you can use nearly every signature backend without much effort.

Individual Module

You can extend an existing module or implement your own module class by implementing the SetaPDF_Signer_Signature_Module_ModuleInterface interface.  

Additional interfaces are available which allow modifications of the signature dictionary or the document instance: SetaPDF_Signer_Signature_DictionaryInterface and SetaPDF_Signer_Signature_DocumentInterface.

Example

PHP
<?php
class MySignatureModule implements 
    SetaPDF_Signer_Signature_Module_ModuleInterface,
    SetaPDF_Signer_Signature_DictionaryInterface.
    SetaPDF_Signer_Signature_DocumentInterface
{
    /**
     * Create a signature for the file in the given $tmpPath.
     *
     * @param SetaPDF_Core_Reader_FilePath $tmpPath
     * @return string
     */
    public function createSignature(SetaPDF_Core_Reader_FilePath $tmpPath)
    {
        // get hash
        $hashAlgorithm = 'sha256';
        $hashValue = hash_file($hashAlgorithm, $tmpPath);

        // e.g. call an external webservice to create the signature
        $signature = callWebserviceToCreateSignature($hashValue, $hashAlgorithm);

        // return the signature value
        return $signature;
    }

    /**
     * Method to update the signature dictionary.
     *
     * @param SetaPDF_Core_Type_Dictionary $dictionary
     */
    public function updateSignatureDictionary(SetaPDF_Core_Type_Dictionary $dictionary)
    {
        // update the signature dictionary if needed
    }

    /**
     * Method to allow updates onto the document instance.
     *
     * @param SetaPDF_Core_Document $document
     */
    public function updateDocument(SetaPDF_Core_Document $document)
    {
        // make changes or check to the document instance
    }
}

External Implementation

Because the CMS module creates the whole signature structure in PHP code it is possible to receive this data container from this module through the getDataToSign() method.

The signature value can be created in any foreign process then and passed back to the setSignatureValue() method.

This allows independent implementations also with external signature components without writing a complete new module class. Following some examples of possible implementations which can be used with both the CMS or PAdES module.

OpenSSL CLI commands

OpenSSL offer various command line tools, which can be used to create the signature value:

dgst

The dgst command generate the digital signature using message digests:

PHP
<?php
// Define the path to the OpenSSL executable
// $opensslPath = 'C:\\OpenSSL-Win32\\bin\\';
$opensslPath = '/usr/bin/';

// require SetaPDF
require_once('library/SetaPDF/Autoload.php');

date_default_timezone_set('Europe/Berlin');

// the file to sign
$fileToSign = 'files/pdfs/tektown/Laboratory-Report.pdf';
// create a temporary path
$tempFile = SetaPDF_Core_Writer_TempFile::createTempPath();

// create a writer instance
$writer = new SetaPDF_Core_Writer_Http('signed-with-dgst.pdf', true);
// create the document instance
$document = SetaPDF_Core_Document::loadByFilename($fileToSign, $writer);

// create the signer instance
$signer = new SetaPDF_Signer($document);

// let's use the PAdES modul and configure it
$module = new SetaPDF_Signer_Signature_Module_Pades();
$module->setDigest(SetaPDF_Signer_Digest::SHA_256);
$module->setCertificate('file://files/certificates/setapdf-no-pw.pem');

// create a temporary version which represents the data which should get signed
$tmpDocument = $signer->preSign(new SetaPDF_Core_Writer_File($tempFile), $module);

// get the hash data from the module
$hashData = $module->getDataToSign($tmpDocument->getHashFile());

// define some variables related to the private key
$privateKey = realpath('files/certificates/setapdf-no-pw.pem');
$privateKeyPass = '';

// create a temporary file with the data to sign
$tmpFileIn = SetaPDF_Core_Writer_TempFile::createTempFile($hashData);
// prepare a temporary file for the final signature
$tmpFileOut = SetaPDF_Core_Writer_TempFile::createTempPath();

// build the command
$cmd = $opensslPath . 'openssl dgst '
    . '-' . $module->getDigest() . ' '
    . '-binary '
    . "-sign " . escapeshellarg($privateKey) . ' '
    . '-passin pass:' . escapeshellarg($privateKeyPass) . ' '
    . '-out ' . escapeshellarg($tmpFileOut) . ' '
    . escapeshellarg($tmpFileIn);

// execute it
exec($cmd, $out, $retValue);

if ($retValue !== 0) {
    throw new SetaPDF_Signer_Exception(
        sprintf(
            'An error occurs while calling OpenSSL through CLI (exit code %s).',
            $retValue
        )
    );
}

// get the signature data
$signatureValue = file_get_contents($tmpFileOut);

// pass it to the module
$module->setSignatureValue($signatureValue);

// get the final cms container
$cms = $module->getCms();
// and pass it to the main signer instance
$signer->saveSignature($tmpDocument, $cms);
            
pkeyutl

Following demo uses the pkeyutil command to generate the digital signature:

PHP
<?php
// Define the path to the OpenSSL executable
// $opensslPath = 'C:\\OpenSSL-Win32\\bin\\';
$opensslPath = '/usr/bin/';

// require SetaPDF
require_once('library/SetaPDF/Autoload.php');

date_default_timezone_set('Europe/Berlin');

// the file to sign
$fileToSign = 'files/pdfs/tektown/Laboratory-Report.pdf';
// create a temporary path
$tempFile = SetaPDF_Core_Writer_TempFile::createTempPath();

// create a writer instance
$writer = new SetaPDF_Core_Writer_Http('signed-with-pkeyutl.pdf', true);
// create the document instance
$document = SetaPDF_Core_Document::loadByFilename($fileToSign, $writer);

// create the signer instance
$signer = new SetaPDF_Signer($document);

// let's use the PAdES modul and configure it
$module = new SetaPDF_Signer_Signature_Module_Pades();
$module->setDigest(SetaPDF_Signer_Digest::SHA_256);
$module->setCertificate('file://files/certificates/setapdf-no-pw.pem');

// create a temporary version which represents the data which should get signed
$tmpDocument = $signer->preSign(new SetaPDF_Core_Writer_File($tempFile), $module);

// get the hash data from the module
$hashData = $module->getDataToSign($tmpDocument->getHashFile());

// define some variables related to the private key
$privateKey = realpath('files/certificates/setapdf-no-pw.pem');
$privateKeyPass = '';

// with pkeyutl we only need to pass the hash value, so get it
$hash = hash($module->getDigest(), $hashData, true);

// and write it to a temporary file
$tmpFileIn = SetaPDF_Core_Writer_TempFile::createTempFile($hash);
// prepare a temporary file for the final signature
$tmpFileOut = SetaPDF_Core_Writer_TempFile::createTempPath();

// build the command
$cmd = $opensslPath . "openssl pkeyutl -sign "
    . "-inkey " . escapeshellarg($privateKey) . ' '
    . "-pkeyopt digest:" . $module->getDigest() . ' ' // this will allow us to sign the hash only
    . '-passin pass:' . escapeshellarg($privateKeyPass) . ' '
    . '-in ' . escapeshellarg($tmpFileIn) . ' '
    . '-out ' . escapeshellarg($tmpFileOut);

// execute it
exec($cmd, $out, $retValue);

if ($retValue !== 0) {
    throw new SetaPDF_Signer_Exception(
        sprintf('An error occurs while calling OpenSSL through CLI (exit code %s).', $retValue)
    );
}

// get the signature data
$signatureValue = file_get_contents($tmpFileOut);

// pass it to the module
$module->setSignatureValue($signatureValue);

// get the final cms container
$cms = $module->getCms();
// and pass it to the main signer instance
$signer->saveSignature($tmpDocument, $cms);
            
rsautl

Following demo uses the rsautl command to generate the digital signature. It requires some more than only hashing but it also requires us to encode the hash in an ASN.1 structure:

PHP
<?php
// Define the path to the OpenSSL executable
// $opensslPath = 'C:\\OpenSSL-Win32\\bin\\';
$opensslPath = '/usr/bin/';

// require SetaPDF
require_once('library/SetaPDF/Autoload.php');

// the file to sign
$fileToSign = 'files/pdfs/tektown/Laboratory-Report.pdf';
// create a temporary path
$tempFile = SetaPDF_Core_Writer_TempFile::createTempPath();

// create a writer instance
$writer = new SetaPDF_Core_Writer_Http('signed-with-rsautl.pdf', true);
// create the document instance
$document = SetaPDF_Core_Document::loadByFilename($fileToSign, $writer);

// create the signer instance
$signer = new SetaPDF_Signer($document);

// let's use the CMS modul and configure it
$module = new SetaPDF_Signer_Signature_Module_Cms();
$module->setDigest(SetaPDF_Signer_Digest::SHA_256);
$module->setCertificate('file://files/certificates/setapdf-no-pw.pem');

// create a temporary version which represents the data which should get signed
$tmpDocument = $signer->preSign(new SetaPDF_Core_Writer_File($tempFile), $module);

// get the hash data from the module
$hashData = $module->getDataToSign($tmpDocument->getHashFile());

// define some variables related to the private key
$privateKey = realpath('files/certificates/setapdf-no-pw.pem');
$privateKeyPass = '';

// encode the hash in an ASN.1 structure
$digestInfo = new SetaPDF_Signer_Asn1_Element(
    SetaPDF_Signer_Asn1_Element::SEQUENCE | SetaPDF_Signer_Asn1_Element::IS_CONSTRUCTED, '',
    array(
        new SetaPDF_Signer_Asn1_Element(
            SetaPDF_Signer_Asn1_Element::SEQUENCE | SetaPDF_Signer_Asn1_Element::IS_CONSTRUCTED, '',
            array(
                new SetaPDF_Signer_Asn1_Element(
                    SetaPDF_Signer_Asn1_Element::OBJECT_IDENTIFIER,
                    SetaPDF_Signer_Asn1_Oid::encode(
                        SetaPDF_Signer_Digest::getOid($module->getDigest())
                    )
                ),
                new SetaPDF_Signer_Asn1_Element(SetaPDF_Signer_Asn1_Element::NULL)
            )
        ),
        new SetaPDF_Signer_Asn1_Element(
            SetaPDF_Signer_Asn1_Element::OCTET_STRING,
            hash($module->getDigest(), $hashData, true)
        )
    )
);

// and write it to a temporary file
$tmpFileIn = SetaPDF_Core_Writer_TempFile::createTempFile($digestInfo);
// prepare a temporary file for the final signature
$tmpFileOut = SetaPDF_Core_Writer_TempFile::createTempPath();

// build the command
$cmd = $opensslPath . "openssl rsautl -sign -pkcs "
    . "-inkey " . escapeshellarg($privateKey) . ' '
    . '-passin pass:' . escapeshellarg($privateKeyPass) . ' '
    . '-in ' . escapeshellarg($tmpFileIn) . ' '
    . '-out ' . escapeshellarg($tmpFileOut);

// execute it
exec($cmd, $out, $retValue);

if ($retValue !== 0) {
    throw new SetaPDF_Signer_Exception(
        sprintf(
            'An error occurs while calling OpenSSL through CLI (exit code %s).',
            $retValue
        )
    );
}

// get the signature data
$signatureValue = file_get_contents($tmpFileOut);

// pass it to the module
$module->setSignatureValue($signatureValue);

// get the final cms container
$cms = $module->getCms();
// and pass it to the main signer instance
$signer->saveSignature($tmpDocument, $cms);
            
Engines

All of the above commands can be used with an engine which e.g. allows you to access a smart card or usb token.

CAPI

You can use for example the CAPI engine on a windows environment this way:

PHP
// the subject
$privateKey = 'SetaPDF-Demo';
// the password
$privateKeyPass = '';

// build the command
$cmd = $opensslPath . " pkeyutl -sign "
    . '-keyform ENGINE -engine capi -inkey ' . escapeshellarg($privateKey) . ' '
    . "-pkeyopt digest:" . $module->getDigest() . ' ' // this will allow us to sign the hash only
    . '-passin pass:' . escapeshellarg($privateKeyPass) . ' '
    . '-in ' . escapeshellarg($tmpFileIn) . ' '
    . '-out ' . escapeshellarg($tmpFileOut) .  ' 2>&1';
PKCS11

If you have an USB token with PKCS11 support installed on your operating system and the engine is configured properly in OpenSSL you can access it this way:

PHP
$pkcs11Uri = 'pkcs11:model=ePass2003;manufacturer=EnterSafe%00;serial=XXXXXXX;token=XXXX;id=XXXX;object=XXX;type=private;pin-value=11111111';

// build the command
$cmd = $opensslPath . " pkeyutl -sign "
    . '-keyform ENGINE -engine pkcs11 -inkey ' . escapeshellarg($pkcs11Uri) . ' '
    . "-pkeyopt digest:" . $module->getDigest() . ' ' // this will allow us to sign the hash only
    . '-in ' . escapeshellarg($tmpFileIn) . ' '
    . '-out ' . escapeshellarg($tmpFileOut) .  ' 2>&1';

The $pkcs11Uri format is defined in RFC 7512.

To list all available objects on your token, you can e.g. use the p11tool (replace the so file with the one of your token):

p11tool --provider=/usr/lib/libcastle.so.1.0.0 --list-all --login

To export a certificate from your USB token (which is required as the certificate parameter) you also can use the p11tool: 

p11tool –provider=/usr/lib/libcastle.so.1.0.0 –login –export-chain "PKCS-11 URI OF THE CERTIFICATE"