External Implementation

Introduction

The CMS module creates the whole signature structure in PHP. It also allows you to access the data that should be signed through the getDataToSign() method.

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

This allows you to delegate the signature creation to any external program or service without writing a complete new module class. Following some examples of possible implementations which can be used with both the CMS or PAdES module.

Notice that currently the CMS and PAdES modules only support the RSA schema RSASSA-PKCS1-v1_5 in the signature value.

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 with the pkeyutl 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

A more reliable solution to access PKCS11 compatible devices is the PKCS11 module.

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 with the pkeyutl 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"

OpenSC pkcs11-tool

A more reliable solution to access PKCS11 compatible devices is the PKCS11 module.

Another way to access an attached USB token or smart card which offers a PKCS11 interface is by using the command line tool pkcs11-tool from the OpenSC project:

PHP
<?php

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

$pkcs11ToolPathBin = '/usr/local/bin/pkcs11-tool';
$pkcs11Path = '/usr/local/lib/libetpkcs11.so'; // path to your PKCS11 module
$slot = 0;
// List objects: pkcs11-tool --module {$pkcs11Path} --slot {$slot} -O
$id = '<the-id-of-your-key>';
$pin = '<your-pin>';

$writer = new \SetaPDF_Core_Writer_File('signed.pdf');
$document = \SetaPDF_Core_Document::loadByFilename('Laboratory-Report.pdf', $writer);

$signer = new \SetaPDF_Signer($document);
$signer->setSignatureContentLength(18000);

$module = new \SetaPDF_Signer_Signature_Module_Pades();
$module->setCertificate('setapdf.crt');

$tempFile = \SetaPDF_Core_Writer_TempFile::createTempPath();

// 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());

// 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, '',
    [
        new \SetaPDF_Signer_Asn1_Element(
            \SetaPDF_Signer_Asn1_Element::SEQUENCE | \SetaPDF_Signer_Asn1_Element::IS_CONSTRUCTED, '',
            [
                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();

$cmd = $pkcs11ToolPathBin . ' '
    . '--module ' . escapeshellarg($pkcs11Path) . ' '
    . '--pin ' . escapeshellarg($pin) . ' '
    . '--slot ' . escapeshellarg($slot) . ' '
    . '--id ' . escapeshellarg($id) . ' '
    . '-m RSA-PKCS '
    . '--sign '
    . '--input-file ' . escapeshellarg($tmpFileIn) . ' '
    . '--output-file ' . escapeshellarg($tmpFileOut) . ' '
    . '2>&1';

exec($cmd, $out, $retValue);

if ($retValue !== 0) {
    throw new SetaPDF_Signer_Exception(
        sprintf('An error occurs while calling pkcs11-tool 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);

unlink($tmpFileIn);
unlink($tmpFileOut);