Create digital signatures for PDF documents with PHP

SetaPDF-Signer

Digital sign PDF documents with PHP

Step-Up Authentication With the Swisscom Signing Service

altThis demo shows you how to use on demand signatures with Step-Up Authentication for the Swisscom Signing Service. To issue a certificate and signature this method needs an authentication by an one-time-password (OTP) or the Mobile ID App.

The PHP code of this add-on is available at GitHub and installable using Composer.

<!DOCTYPE html>
<!-- A simple HTML file for the outer layout (preview + signature panel) -->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SetaPDF-Signer meets Swisscom All-in Signing Service</title>
    <link rel="stylesheet" href="style.css">
</head>
<body class="panel">
    <iframe src="/public/path/to/Laboratory-Report.pdf" class="preview"></iframe>
    <iframe src="controller.php?action=init" class="dialog"></iframe>
</body>
</html>
<?php

use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Handler\CurlHandler;
use Http\Factory\Guzzle\RequestFactory;
use Http\Factory\Guzzle\StreamFactory;
use Mjelamanov\GuzzlePsr18\Client as Psr18Wrapper;
use setasign\SetaPDF\Signer\Module\SwisscomAIS\AsyncModule;
use setasign\SetaPDF\Signer\Module\SwisscomAIS\ProcessData;
use setasign\SetaPDF\Signer\Module\SwisscomAIS\SignException;

if (!isset($_GET['action'])) {
    die();
}

require_once('vendor/autoload.php');

$fileToSign = '/path/to/Laboratory-Report.pdf';
$customerId = 'ais-90days-trial:OnDemand-Advanced-EU';
$verify = 'ais-ca-ssl.crt';
$cert = 'your-certificate.pem';
$sslKey = 'your-private-key.pem';

// for demonstration purpose we use a session for state handling
// in a production environment you may use a more reasonable solution
session_start();

// a simple "controller":
switch ($_GET['action']) {
    // Display the step-up authentication from
    case 'init':
        $html = <<<HTML
<h1>Step-Up Authentication</h1>
    <p>To start the signature workflow, fill in the following form fields.<br/>
        You have to approve the signature with an OTP send to your mobile or with your Mobile ID app.</p>
    <form action="controller.php?action=start" method="post">
        <label for="lastname">Lastname</label><br/>
        <input type="text" id="lastname" name="lastname" value="Tester">
        <br/><br/>
        <label for="firstname">Firstname</label><br/>
        <input type="text" id="firstname" name="firstname" value="Joe">
        <br/><br/>
        <label for="mobile">Mobile Number (incl. country code - without any extra character - e.g. 4912345678)</label><br/>
        <input type="text" id="mobile" name="mobile">
        <br/><br/>
        <button class="btnContinue">Sign document</button>
    </form>
HTML;
        $hideRestart = true;
        include __DIR__ . '/dialog.php';
        break;

    // prepare the PDF document, and start the signature process
    case 'start':
        // reset the current state
        unset($_SESSION[__FILE__]);

        $guzzleOptions = [
            'handler' => new CurlHandler(),
            'http_errors' => false,
            'verify' => $verify,
            'cert' => $cert,
            'ssl_key' => $sslKey,
        ];

        $httpClient = new GuzzleClient($guzzleOptions);
        // only required if you are using guzzle < 7
        $httpClient = new Psr18Wrapper($httpClient);

        // let's get the document
        $document = SetaPDF_Core_Document::loadByFilename($fileToSign);

        // now let's create a signer instance
        $signer = new SetaPDF_Signer($document);
        // create a Swisscom AIS module instance
        $swisscomModule = new AsyncModule($customerId, $httpClient, new RequestFactory(), new StreamFactory());

        $signer->setAllowSignatureContentLengthChange(false);
        $signer->setSignatureContentLength(30000);

        // set some signature properties
        $signer->setLocation('www.setasign.com');
        $signer->setContactInfo($_POST['mobile']);
        $signer->setReason('Testing Swisscom AIS');

        $fieldName = $signer->addSignatureField()->getQualifiedName();
        $signer->setSignatureFieldName($fieldName);

        // additionally, the signature should include a timestamp
        $swisscomModule->setAddTimestamp(true);

        $lastname = str_replace(',', '', $_POST['lastname']);
        $firstname = str_replace(',', '', $_POST['firstname']);

        // set on-demand options
        $swisscomModule->setOnDemandCertificate(
            'cn=TEST ' . $firstname . ' ' . $lastname . ',givenname=' . $firstname . ',surname=' . $lastname . ', c=de, emailaddress=demos@setasign.com'
        );

        try {
            $swisscomModule->setStepUpAuthorisation(
                $_POST['mobile'],
                'Please confirm to sign "' . basename($fileToSign) . '"',
                'en'
            );
        } catch (Throwable $e) {
            $html = '<h1>Invalid mobile number</h1><p>Error: ' . htmlspecialchars($e->getMessage()) . '</p>';
            include __DIR__ . '/dialog.php';
            break;
        }

        // you may use an own temporary file handler
        $tempPath = SetaPDF_Core_Writer_TempFile::createTempPath();

        try {
            // prepare the PDF
            $tmpDocument = $signer->preSign(
                new SetaPDF_Core_Writer_File($tempPath),
                $swisscomModule
            );

            $processData = $swisscomModule->initSignature($tmpDocument, $fieldName);
        } catch (Throwable $e) {
            $html = '<h1>Error on presign</h1><p>' . htmlspecialchars($e->getMessage()) . '</p>' .
                '<p>Error: '. htmlspecialchars($e->getMessage()) . '</p>';
            include __DIR__ . '/dialog.php';
            break;
        }

        // inject individual metadata into the process data
        $processData->setMetadata(['filename' => 'Swisscom-with-step-up-authentication.pdf']);

        // For the purpose of this demo we just serialize the processData into the session.
        // You could use e.g. a database or a dedicated directory on your server.
        $_SESSION[__FILE__]['processData'] = $processData;

        $response = $swisscomModule->getLastResponseData();
        // if your mobile number isn't registered for mobile id authentication, you'll fall back to sms authentication
        if (isset($response['SignResponse']['OptionalOutputs']['sc.StepUpAuthorisationInfo']['sc.Result']['sc.ConsentURL'])) {
            // The content of the website pointed by the consent URL can change over time and therefore the page
            // must be displayed as it is. The recommended methods for showing the content hosted under the consent
            // URL are:
            // • To embed an iFrame in the application (see [IFR - https://github.com/SwisscomTrustServices/AIS/wiki/SAS-iFrame-Embedding-Guide] for guidelines)
            // • To send an SMS to the user with the consent URL, so the user can open it directly on his phone browser

            $url = json_encode($response['SignResponse']['OptionalOutputs']['sc.StepUpAuthorisationInfo']['sc.Result']['sc.ConsentURL']);
            $html = '<h1>Signing process started...</h1>' .
                '<p><a href="#" onclick="openLink()">Please give your consent via mobile number</a>. (popups must be allowed)</p>';
            $html .= <<<HTML
<script type="text/javascript">
function openLink () {
    window.open({$url}, '_blank', 'location=yes,height=570,width=520,scrollbars=yes,status=yes');
    window.setTimeout(function () {window.location = "controller.php?action=complete";}, 5000);
}
</script>
HTML;
            include __DIR__ . '/dialog.php';
            break;
        }

        $html = '<h1>Signing process started (via mobile id)</h1>' .
            '<p>Waiting for authorisation via mobile number (page will reload automatically).</p>' .
            '<script type="text/javascript">window.setTimeout(function () {window.location = "controller.php?action=complete";}, 5000);</script>';
        include __DIR__ . '/dialog.php';
        break;

    // check if the signature was created and embed it into the PDF document
    case 'complete':
        if (!isset($_SESSION[__FILE__]['processData'])) {
            $html = '<p>No process data found in session</p>';
            include __DIR__ . '/dialog.php';
            break;
        }
        /** @var ProcessData $processData */
        $processData = $_SESSION[__FILE__]['processData'];

        $guzzleOptions = [
            'handler' => new CurlHandler(),
            'http_errors' => false,
            'verify' => $verify,
            'cert' => $cert,
            'ssl_key' => $sslKey,
        ];

        $httpClient = new GuzzleClient($guzzleOptions);
        // only required if you are using guzzle < 7
        $httpClient = new Psr18Wrapper($httpClient);

        // create a Swisscom AIS module instance
        $swisscomModule = new AsyncModule($customerId, $httpClient, new RequestFactory(), new StreamFactory());

        $swisscomModule->setProcessData($processData);

        try {
            $signResult = $swisscomModule->processPendingSignature();
        } catch (SignException $e) {
            $minorResult = $e->getResultMinor();
            if ($minorResult === 'http://ais.swisscom.ch/1.1/resultminor/subsystem/StepUp/timeout') {
                $html = '<h1>StepUp authentification timeout</h1>';
            } elseif ($minorResult === 'http://ais.swisscom.ch/1.1/resultminor/subsystem/StepUp/cancel') {
                $html = '<h1>StepUp authentification was canceled</h1>';
            } else {
                $html = '<h1>An error occurred</h1><p>' . htmlspecialchars($e->getMessage()) . '</p>' .
                    '<p>ResultMajor: ' . $e->getResultMajor() . '</p>' .
                    '<p>ResultMinor:' . $e->getResultMinor() . '</p>';
            }

            include __DIR__ . '/dialog.php';
            // clean up temporary file
            unlink($processData->getTmpDocument()->getWriter()->getPath());
            unset($_SESSION[__FILE__]);
            break;
        } catch (Throwable $e) {
            $html = '<h1>Error on signing</h1><p>Error: '. $e->getMessage() . '</p>';
            include __DIR__ . '/dialog.php';
            unlink($processData->getTmpDocument()->getWriter()->getPath());
            unset($_SESSION[__FILE__]);
            break;
        }

        if ($signResult === false) {
            $html = '<h1>Still pending!</h1>' .
                '<p>Waiting for authorisation via mobile number (page will reload automatically).</p>' .
                '<script type="text/javascript">window.setTimeout(function () {window.location = "controller.php?action=complete";}, 5000);</script>';
            include __DIR__ . '/dialog.php';
            break;
        }

        // let's get the document
        $document = SetaPDF_Core_Document::loadByFilename($fileToSign);

        // now let's create a signer instance
        $signer = new SetaPDF_Signer($document);

        try {
            $tmpWriter = new SetaPDF_Core_Writer_TempFile();
            $document->setWriter($tmpWriter);

            // save the signature to the temporary document
            $signer->saveSignature($processData->getTmpDocument(), $signResult);

            // add DSS
            $document = SetaPDF_Core_Document::loadByFilename($tmpWriter->getPath());

            // optional: validate the technical integrity of the signature
            $result = SetaPDF_Signer_ValidationRelatedInfo_IntegrityResult::create($document, $processData->getFieldName());
            if (!$result->isValid()) {
                throw new Exception('Signature integrity is not valid!');
            }

            // use the filename stored in the process data metadata to create a writer instance
            $writer = new SetaPDF_Core_Writer_String();
            $document->setWriter($writer);
            $swisscomModule->updateDss($document, $processData->getFieldName());
            $document->save()->finish();

            // clean up temporary file
            $_SESSION[__FILE__]['pdf'] = [
                'name' => $processData->getMetadata()['filename'],
                'data' => $writer->getBuffer()
            ];
            $html = '<h1>The document was signed successfully</h1>' .
                '<p><a href="controller.php?action=download" class="downloadBtn">Download</a></p>';

        } catch (Throwable $e) {
            $html = '<h1>Error on saving the signature</h1><p>Error: ' . htmlspecialchars($e->getMessage()) . '</p>';
        } finally {
            unlink($processData->getTmpDocument()->getWriter()->getPath());
            unset($_SESSION[__FILE__]['processData']);
        }

        include __DIR__ . '/dialog.php';
        break;

    // a download action for the final signed document
    case 'download':
        if (!isset($_SESSION[__FILE__]['pdf'])) {
            $html = '<h1>No pdf found in session</h1>';
            include __DIR__ . '/dialog.php';
            break;
        }

        $doc = $_SESSION[__FILE__]['pdf'];

        header('Content-Type: application/pdf');
        header('Content-Disposition: attachment; ' . SetaPDF_Core_Writer_Http::encodeFilenameForHttpHeader($doc['name']));
        header('Expires: 0');
        header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
        header('Pragma: public');
        header('Content-Length: ' . strlen($doc['data']));
        echo $doc['data'];
        flush();
        break;

    default:
        $html = '<h1>Unknown action</h1>';
        include __DIR__ . '/dialog.php';
        break;
}
<?php
/* This file is used as a kind of template for the dialog steps.
 * It is required in the controller.php file if an output is done.
 */
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SetaPDF-Signer meets Swisscom All-in Signing Service</title>
    <link rel="stylesheet" href="style.css">
</head>
<body class="dialog">
<?=$html?>
<?php if(!isset($hideRestart)): ?>
<a href="controller.php?action=init" class="restartBtn">Restart</a>
<?php endif;?>
</body>
</html>
html {
    margin: 0;
    padding: 0;
    height: 100%;
    width: 100%;
}

body {
    font-family: "Open Sans", "Arial", sans-serif;
    font-size: 14px;
    color: rgb(64, 72, 79);
    margin: 0;
    padding: 0;
}

body.panel {
    display: flex;
    height: 100%;
}

iframe.preview, iframe.dialog {
    width: 50%;
    border: 1px solid rgb(234, 237, 242);
}

iframe.dialog {
    border-left: 0;
}

body.dialog {
    padding: 34px 50px 46px;
}

h1 {
    font-size: 17px;
    margin-top: 0;
    padding-top: 0;
}

a {
    color: rgb(53, 132, 247);
}

input {
    border-radius: 3px;
    transition: color 200ms;
    border: 1px solid rgb(182, 195, 204);
    padding: 5px;
}

button.btnContinue, a.restartBtn, a.downloadBtn {
    justify-content: center;
    border-radius: 3px;
    padding: 10px 26px;
    float: right;
    cursor: pointer;
    transition: color 200ms;
}

a.restartBtn, a.downloadBtn {
    display: inline-block;
    text-decoration: none;
    padding: 10px 26px;
}

button.btnContinue, a.downloadBtn {
    color: #ffffff;
    border: 1px solid rgb(10, 190, 101);
    background-color: rgb(10, 190, 101);
}

button.btnContinue:hover, a.downloadBtn:hover {
    color: #9ddd97;
}

a.restartBtn {
    color: rgb(109, 125, 135);
    background-color: #ffffff;
    border: 1px solid rgb(182, 195, 204);
    float: none;
}

a.restartBtn:hover {
    color: rgb(182, 195, 204);
}

button.btnContinue:focus, a.restartBtn:focus, a.downloadBtn:focus {
    box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.1);
    outline:none;
}

Swisscom Signing Service

The Swisscom Signing Service is a cloud service for electronic signatures and timestamps for documents and files. It is offered as Swiss managed service to service providers, public authorities and companies.

The signing service allows documents and files to be signed in a legally compliant manner in the scope of the eIDAS (EU) regulation and the Swiss Signature Law (ZertES). Electronic signatures ensure the integrity and/or authenticity of such files for the relevant contract partner or legislator.

Combined with Swiss SIM based Mobile ID, international Mobile ID App from the Google/iOS Store or a combination of password and one-time-code via SMS, the KPMG-audited Signing Service enables binding signatures to be authorized by mobile phones. You also benefit from the expertise of Swisscom as a legally recognised certificate service provider (CSP) in Switzerland and Austria.

More information: https://trustservices.swisscom.com/