SetaPDF Demos

There seems to be a problem loading the components. Please check your PHP error logs for details!

Common issues could be that you missed to install the trial license or that you are using a trial version on an unsupported PHP version.

Fortify Web-component Batch Signatures

This demo shows an implementation with the web-component of Fortify for signing multiple documents at once.

PHP
<?php

if ($_SERVER['SERVER_NAME'] !== 'localhost' && (empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] === 'off')) {
    throw new Exception('This demo must run on localhost or HTTPS.');
}

$path = substr($_SERVER['PHP_SELF'], 0, -strlen(basename(__FILE__)));
$controllerPath = 'https://' . $_SERVER['HTTP_HOST'] . $path . 'controller.php';

?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>SetaPDF-Signer meets Fortify</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pure-css-loader@3.3.3/dist/css-loader.css">
    <link rel="stylesheet" href="https://unpkg.com/@peculiar/fortify-webcomponents@^1.0.0/dist/peculiar/peculiar.css">
    <style>
        * {
            box-sizing: border-box;
        }

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

            /* adjust some colors in the Fortify webcomponent */
            --peculiar-color-footer-rgb: 255, 255, 255;
            --peculiar-color-footer-text-rgb: 0, 0, 0;
        }

        #signatureControlsPanel, #previewContainer, #signButtonContainer, #fortifyContainer, #downloadButtonContainer {
            height:100%;
        }

        #previewContainer, #signButtonContainer, #fortifyContainer, #downloadButtonContainer {
            border: 0;
            float: left;
            width: 50%;
        }

        #signButtonContainer, #fortifyContainer, #downloadButtonContainer {
            border-right: 1px solid rgb(234, 237, 242);
            border-top: 1px solid rgb(234, 237, 242);
            border-bottom: 1px solid rgb(234, 237, 242);
        }

        #signButtonContainer, #downloadButtonContainer {
            padding: 34px 50px 46px;
        }

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

        button.btnContinue, button.btnCancel {
            justify-content: center;
            border-radius: 3px;
            padding: 0 26px;
            height: 40px;
            float: right;
            cursor: pointer;
            transition: color 200ms;
        }

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

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

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

        button.btnCancel:hover {
            color: rgb(182, 195, 204);
        }

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

        label.checkbox {
            display: block;
            margin: 5px 0;
            margin-left: 20px;
        }

        label.checkbox input[type=checkbox] {
            position: absolute;
            margin-left: -20px;
        }

        .loader-default::after {
            border-color: rgb(13, 132, 255);
            border-left-color: transparent;
        }
    </style>
</head>
<body>
<div id="loader" class="loader loader-default is-active" data-text="Loading..."></div>
<div id="outdated" style="display:none;">Your browser is outdated.</div>

<div id="signatureControlsPanel" style="display:none;">
    <div id="previewContainer"></div>
    <div id="signButtonContainer">
        <h4>Signature Settings</h4>
        <label class="checkbox" for="useAIA">
            <input type="checkbox" name="useAIA" id="useAIA" checked="checked"/>
            Embedded certificates fetched from the <a href="http://www.pkiglobe.org/auth_info_access.html" target="_blank">AIA extension</a>.
        </label>

        <label class="checkbox" for="useTimestamp">
            <input type="checkbox" name="useTimestamp" id="useTimestamp" checked="checked" />
            Embedded timestamp if adobe timestamp extension is available in certificate.
        </label>

        <button id="signBtn" class="btnContinue">Start and choose certificate</button>
    </div>
    <div id="fortifyContainer" style="display: none;height:100%;"></div>
    <div id="downloadButtonContainer" style="display: none;">
        <p>The documents were successfully signed.</p>
        <p id="extraCerts"></p>
        <p id="tsUrl"></p>
        <button id="resetBtn" class="btnCancel">Restart</button>
    </div>
</div>

<script type="text/javascript">
    var controllerPath = '<?=$controllerPath?>';
    document.addEventListener("DOMContentLoaded", function () {
        function loadScript(src, module) {
            return new Promise(function (resolve, reject) {
                var script = document.createElement('script');
                script.src = src;
                script.type = 'text/javascript';
                script.onload = resolve.bind(null, true);
                script.onerror = reject;
                if (typeof module !== 'undefined') {
                    if (module) {
                        script.type = 'module';
                    } else {
                        script.noModule = true;
                    }
                }
                document.head.appendChild(script);
            });
        }

        try {
            loadScript('https://unpkg.com/@peculiar/fortify-webcomponents@^1.0.0/dist/peculiar/peculiar.esm.js', true)
                .then(function () {return loadScript('https://verify.ink/webcomponent/index.js', true)})
                .then(function () {return loadScript('js/main.js')})
                .catch(function (e) {
                    document.getElementById('loader').style.display = 'none';
                    document.getElementById('outdated').style.display = '';
                    console.error(e);
                });
        } catch (e) {
            document.getElementById('loader').style.display = 'none';
            document.getElementById('outdated').style.display = '';
            console.error(e);
        }
    });
</script>
</body>
</html>
PHP
<?php
if (!isset($_GET['action'])) {
    die();
}

// load and register the autoload function
require_once __DIR__ . '/../../../../../../bootstrap.php';

$filesToSign = [
    'tektown' => $assetsDirectory . '/pdfs/tektown/Laboratory-Report.pdf',
    'camtown' => $assetsDirectory . '/pdfs/camtown/Laboratory-Report.pdf',
    'lenstown' => $assetsDirectory . '/pdfs/lenstown/Laboratory-Report.pdf',
];

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

try {
    // a simple "controller":
    switch ($_GET['action']) {
        case 'preview':
            if (!array_key_exists($_GET['file'], $filesToSign)) {
                http_response_code(404);
                die();
            }

            $doc = file_get_contents($filesToSign[$_GET['file']]);

            // Note: these lines are only required for the Verify.ink pdf viewer because of CORS
            header('Access-Control-Allow-Origin: https://verify.ink');
            header('Access-Control-Allow-Credentials: true');
            header('Access-Control-Expose-Headers: Content-Disposition');

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

        // This action expects the certificate of the signer.
        // It prepares the PDF document accordingly.
        case 'start':
            if (isset($_SESSION['tmpDocuments'])) {
                foreach ($_SESSION['tmpDocuments'] as $tmpDocument) {
                    @unlink($tmpDocument['tmpDocument']->getWriter()->getPath());
                }
            }

            $data = json_decode(file_get_contents('php://input'));
            if (!isset($data->certificate)) {
                throw new Exception('Missing certificate!');
            }

            // create a certificate instance
            $certificate = new \SetaPDF_Signer_X509_Certificate($data->certificate);
            $extraCerts = new \SetaPDF_Signer_X509_Collection();

            // setup information resolver manager
            $informationResolverManager = new \SetaPDF_Signer_InformationResolver_Manager();
            $informationResolverManager->addResolver(new \SetaPDF_Signer_InformationResolver_HttpCurlResolver([
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_MAXREDIRS => 5
            ]));

            // get issuer certificates
            if (isset($data->useAIA) && $data->useAIA) {
                $certificates = [$certificate];
                while (count($certificates) > 0) {
                    /** @var \SetaPDF_Signer_X509_Certificate $currentCertificate */
                    $currentCertificate = array_pop($certificates);
                    /** @var \SetaPDF_Signer_X509_Extension_AuthorityInformationAccess $aia */
                    $aia = $currentCertificate->getExtensions()->get(\SetaPDF_Signer_X509_Extension_AuthorityInformationAccess::OID);
                    if ($aia instanceof \SetaPDF_Signer_X509_Extension_AuthorityInformationAccess) {
                        foreach ($aia->fetchIssuers($informationResolverManager)->getAll() as $issuer) {
                            $extraCerts->add($issuer);
                            $certificates[] = $issuer;
                        }
                    }
                }
            }

            $signatureContentLength = 10000;
            foreach ($extraCerts->getAll() as $extraCert) {
                $signatureContentLength += (strlen($extraCert->get(\SetaPDF_Signer_X509_Format::DER)) * 2);
            }


            unset($_SESSION['tsUrl']);
            // get timestamp information and use it
            if (isset($data->useTimestamp) && $data->useTimestamp) {
                /** @var \SetaPDF_Signer_X509_Extension_TimeStamp $ts */
                $ts = $certificate->getExtensions()->get(\SetaPDF_Signer_X509_Extension_TimeStamp::OID);
                if ($ts && $ts->getVersion() === 1 && $ts->requiresAuth() === false) {
                    $_SESSION['tsUrl'] = $ts->getLocation();
                    $signatureContentLength += 6000;
                }
            }

            $tmpDocuments = [];
            foreach ($filesToSign as $k => $fileToSign) {
                // load the PDF document
                $document = \SetaPDF_Core_Document::loadByFilename($fileToSign);
                // create a signer instance
                $signer = new \SetaPDF_Signer($document);
                // create a module instance
                $module = new \SetaPDF_Signer_Signature_Module_Pades();


                // pass the user certificate to the module
                $module->setCertificate(clone $certificate);
                $module->setExtraCertificates(clone $extraCerts);
                $signer->setSignatureContentLength($signatureContentLength);

                // A simple example to add a visible signature.
                //        $field = $signer->addSignatureField(
                //            'Signature', 1, \SetaPDF_Signer_SignatureField::POSITION_LEFT_TOP, ['x' => 20, 'y' => -20], 180, 60
                //        );
                //        $signer->setSignatureFieldName($field->getQualifiedName());
                //
                //        $appearance = new \SetaPDF_Signer_Signature_Appearance_Dynamic($module);
                //        $signer->setAppearance($appearance);

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

                $tmpDocuments[$k] = [
                    'tmpDocument' => $signer->preSign(
                        new \SetaPDF_Core_Writer_File($tempPath),
                        $module
                    ),
                    'module' => $module
                ];
            }

            // prepare the response
            $response = [
                'dataToSign' => array_map(function ($tmpDocument) {
                    return \SetaPDF_Core_Type_HexString::str2hex(
                        $tmpDocument['module']->getDataToSign($tmpDocument['tmpDocument']->getHashFile())
                    );
                }, $tmpDocuments),
                'extraCerts' => array_map(function (\SetaPDF_Signer_X509_Certificate $cert) {
                    return $cert->get(\SetaPDF_Signer_X509_Format::PEM);
                }, $extraCerts->getAll()),
                'tsUrl' => isset($_SESSION['tsUrl']) ? $_SESSION['tsUrl'] : false
            ];

            $_SESSION['tmpDocuments'] = $tmpDocuments;

            // send it
            header('Content-Type: application/json; charset=utf-8');
            echo json_encode($response);
            break;

        // This action embeddeds the signature in the CMS container
        // and optionally requests and embeds the timestamp
        case 'complete':
            $data = json_decode(file_get_contents('php://input'), true);
            if (!isset($data['signatures'])) {
                die();
            }

            $data['signatures'] = array_map([\SetaPDF_Core_Type_HexString::class, 'hex2str'], $data['signatures']);

            $resultIds = [];
            foreach ($filesToSign as $key => $fileToSign) {
                // create the document instance
                $writer = new \SetaPDF_Core_Writer_String();
                $document = \SetaPDF_Core_Document::loadByFilename($fileToSign, $writer);
                $signer = new \SetaPDF_Signer($document);

                // pass the signature to the signature modul
                $_SESSION['tmpDocuments'][$key]['module']->setSignatureValue($data['signatures'][$key]);

                // get the CMS structur from the signature module
                $cms = (string)$_SESSION['tmpDocuments'][$key]['module']->getCms();

                // verify that the received signature matches to the CMS package and document.
                $signedData = new \SetaPDF_Signer_Cms_SignedData($cms);
                $signedData->setDetachedSignedData($_SESSION['tmpDocuments'][$key]['tmpDocument']->getHashFile());
                if (!$signedData->verify($signedData->getSigningCertificate())) {
                    throw new Exception('Signature cannot be verified!');
                }

                // add the timestamp (if available)
                if (isset($_SESSION['tsUrl'])) {
                    $tsModule = new \SetaPDF_Signer_Timestamp_Module_Rfc3161_Curl($_SESSION['tsUrl']);
                    $signer->setTimestampModule($tsModule);
                    $cms = $signer->addTimeStamp($cms, $_SESSION['tmpDocuments'][$key]['tmpDocument']);
                }

                // save the signature to the temporary document
                $signer->saveSignature($_SESSION['tmpDocuments'][$key]['tmpDocument'], $cms);
                // clean up temporary file
                unlink($_SESSION['tmpDocuments'][$key]['tmpDocument']->getWriter()->getPath());

                if (!isset($_SESSION['pdfs']['currentId'])) {
                    $_SESSION['pdfs'] = ['currentId' => 0, 'docs' => []];
                } else {
                    // reduce the session data to 6 signed files only
                    while (count($_SESSION['pdfs']['docs']) > 6) {
                        array_shift($_SESSION['pdfs']['docs']);
                    }
                }

                $id = $_SESSION['pdfs']['currentId']++;
                $_SESSION['pdfs']['docs']['id-' . $id] = $writer;
                $resultIds[$key] = $id;
            }

            // send the response
            header('Content-Type: application/json; charset=utf-8');
            echo json_encode([
                'ids' => $resultIds
            ]);
            break;

        // a download action
        case 'download':
            $key = 'id-' . (isset($_GET['id']) ? $_GET['id'] : '');
            if (!isset($_SESSION['pdfs']['docs'][$key])) {
                die();
            }

            $doc = $_SESSION['pdfs']['docs'][$key];

            // Note: these lines are only required for the Verify.ink pdf viewer because of CORS
            header('Access-Control-Allow-Origin: https://verify.ink');
            header('Access-Control-Allow-Credentials: true');
            header('Access-Control-Expose-Headers: Content-Disposition');

            header('Content-Type: application/pdf');
            header('Content-Disposition: attachment; filename="' .  (isset($_GET['name']) ? $_GET['name'] : 'document') . '-signed.pdf"');
            header('Expires: 0');
            header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
            header('Pragma: public');
            header('Content-Length: ' . strlen($doc));
            echo $doc;
            flush();
            break;
    }
} catch (\Exception $e) {
    header('Content-Type: application/json; charset=utf-8', true, 500);
    echo json_encode(['error' => $e->getMessage()]);
}
(function() {
    // Some helper functions:
    function show(id) {
        document.getElementById(id).style.display = '';
    }

    function hide(id) {
        document.getElementById(id).style.display = 'none';
    }

    // some helper functions to work with typed arrays
    function toHex(buffer) {
        let buf = new Uint8Array(buffer),
            splitter = "",
            res = [],
            len = buf.length;

        for (let i = 0; i < len; i++) {
            let char = buf[i].toString(16);
            res.push(char.length === 1 ? "0" + char : char);
        }
        return res.join(splitter);
    }

    function fromHex(hexString) {
        let res = new Uint8Array(hexString.length / 2);
        for (let i = 0; i < hexString.length; i = i + 2) {
            let c = hexString.slice(i, i + 2);
            res[i / 2] = parseInt(c, 16);
        }
        return res.buffer;
    }

    // we need some ajax
    function postRequest(url, params) {
        return new Promise(function(resolve, reject) {
            let xhr = new XMLHttpRequest();
            xhr.open("POST", url, true);
            xhr.onreadystatechange = () => {
                if (xhr.readyState === 4) {
                    if (xhr.status === 200) {
                        resolve(xhr.responseText);
                    } else {
                        reject(xhr, xhr.status);
                    }
                }
            };

            xhr.onerror = (() => reject(xhr, xhr.status));
            xhr.send(params);
        });
    }

    // the main function
    async function main() {
        let lastId = null,
            fortifyComp = null;

        function initVerify(urls) {
            document.getElementById('previewContainer').innerHTML = '<verify-viewer'
                + ' url="' + urls.join(',') + '"'
                + ' show-signature-if-present="true"'
                + ' notify-if-not-signed="false"'
                + ' sign="false"'
                + ' search="false"'
                + ' download="true"'
                + ' style="height: 100%;"'
                + ' ></verify-viewer>';
        }

        function initPreviewFiles() {
            initVerify([
                controllerPath + '?action=preview&file=tektown',
                controllerPath + '?action=preview&file=camtown',
                controllerPath + '?action=preview&file=lenstown'
            ]);
        }

        function initCompletedFiles(ids) {
            let files = [];
            for (let filename in ids) {
                if (!ids.hasOwnProperty(filename)) {
                    continue;
                }
                files.push(controllerPath + '?action=download&id=' + ids[filename] + '&name=' + filename);
            }

            initVerify(files);
        }

        function initFortify () {
            // https://fortifyapp.com/developers/examples/certificate-management
            fortifyComp = document.createElement('peculiar-fortify-certificates');
            fortifyComp.style.height = '100%';
            fortifyComp.language = 'en';
            fortifyComp.filters = {
                //   onlySmartcards: false,
                expired: false,
                //   subjectDNMatch: 'apple',
                //   subjectDNMatch: new RegExp(/apple/),
                //   issuerDNMatch: 'demo',
                //   issuerDNMatch: new RegExp(/demo/),
                // keyUsage: ['digitalSignature'],
                onlyWithPrivateKey: true,
                ca: true
            };

            fortifyComp.addEventListener('cancel', function () {
                hide('fortifyContainer');
                show('signButtonContainer');
            });
            fortifyComp.addEventListener('continue', async function (event) {
                try {
                    show('loader');
                    document.getElementById('loader').setAttribute('data-text', 'Signing document');
                    let provider = await event.detail.server.getCrypto(event.detail.providerId);

                    let cert = await provider.certStorage.getItem(event.detail.certificateId);
                    let certPem = await provider.certStorage.exportCert('pem', cert);
                    let privateKey = await provider.keyStorage.getItem(event.detail.privateKeyId);

                    let startResponseText = await postRequest(
                        controllerPath + '?action=start',
                        JSON.stringify({
                            certificate: certPem,
                            useAIA: document.getElementById('useAIA').checked,
                            useTimestamp: document.getElementById('useTimestamp').checked
                        })
                    );
                    let startJson = JSON.parse(startResponseText);

                    if (startJson.extraCerts.length > 0) {
                        document.getElementById('extraCerts').innerHTML = startJson.extraCerts.length
                            + ' extra certificate(s) resolved and embedded through the '
                            + '<a href="http://www.pkiglobe.org/auth_info_access.html" target="_blank">AIA extension</a>.';
                    } else {
                        document.getElementById('extraCerts').innerHTML = 'No extra certificates were resolved.';
                    }

                    if (startJson.tsUrl) {
                        document.getElementById('tsUrl').innerHTML = 'Timestamp server located at <i>' + startJson.tsUrl
                            + '</i> was used.';
                    } else {
                        document.getElementById('tsUrl').innerHTML = 'No timestamp server found.';
                    }

                    let signatures = {};
                    for (const key in startJson.dataToSign) {
                        if (!startJson.dataToSign.hasOwnProperty(key)) {
                            continue;
                        }

                        const message = fromHex(startJson.dataToSign[key]);
                        const alg = {
                            name: privateKey.algorithm.name,
                            hash: "SHA-256",
                        };

                        let signature = await provider.subtle.sign(alg, privateKey, message);
                        signatures[key] = toHex(signature);
                    }

                    let completeResponseText = await postRequest(
                        controllerPath + '?action=complete',
                        JSON.stringify({signatures})
                    );
                    let completeJson = JSON.parse(completeResponseText);

                    initCompletedFiles(completeJson.ids)
                    hide('fortifyContainer');
                    hide('loader');
                    show('downloadButtonContainer');
                } catch (error) {
                    hide('loader');
                    console.info(error);
                    alert('An error occured: ' + error.responseText);
                }
            });

            document.getElementById('fortifyContainer').appendChild(fortifyComp);
        }

        document.getElementById('signBtn').addEventListener('click', () => {
            if (!fortifyComp) {
                initFortify();
            }

            hide('signButtonContainer');
            show('fortifyContainer');
        });

        document.getElementById('resetBtn').addEventListener('click', () => {
            hide('downloadButtonContainer');
            initPreviewFiles();
            show('signButtonContainer');
        });

        initPreviewFiles();
        show('signatureControlsPanel');
        hide('loader');
    }

    //noinspection JSIgnoredPromiseFromCall
    main();

})();