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 With Vanilla JavaScript

This is a demo using vanilla JavaScript to communicate with the Fortify client.

PHP
<?php

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

?>
<!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>
    <style>
        body {
            font-family: Tahoma,Verdana,Segoe,sans-serif;
            font-size: 14px;
        }

        #signatureControlsPanel {
            margin-top: 1em;
        }
    </style>
</head>
<body>

<div id="loading">Loading...</div>
<div id="outdated" style="display:none;">Your browser is outdated.</div>
<div id="fortifyNotReachable" style="display:none;">Fortify is not running. <a href="https://fortifyapp.com/" target="_blank">Install</a> and start the app on your computer and reload this demo. Note: Most browsers need to run this script in https.</div>
<div id="challengeExchange" style="display:none;">Please compare this pin <b>{pin}</b> with the one that Fortify displays and confirm.</div>
<div id="notLoggedIn" style="display: none;">You're not logged in with Fortify.</div>

<div id="signatureControlsPanel" style="display: none;">

    <select id="providersSelect"><option>Loading...</option></select>
    <select id="certificatesSelect"><option>Loading...</option></select>
    <br />

    <input type="checkbox" name="useAIA" id="useAIA" checked="checked"/><label for="useAIA">Embedded certificates fetched from the <a href="http://www.pkiglobe.org/auth_info_access.html" target="_blank">AIA extension</a> (only HTTP, .cer/.der (no .p7c support), no validation is done).</label><br />
    <input type="checkbox" name="useTimestamp" id="useTimestamp" checked="checked" /><label for="useTimestamp">Embedded timestamp if adobe timestamp extension is available in certificate.</label><br />

    <button id="signBtn" disabled="disabled">Sign Dummy File</button>
    <button id="downloadBtn" disabled="disabled">Download</button>

    <script type="text/javascript">
        if (!window.Promise) {
            document.getElementById('outdated').style.display = '';
        } else {
            document.addEventListener("DOMContentLoaded", function(event) {
                function loadScript(src) {
                    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;
                        document.head.appendChild(script);
                    });
                }

                loadScript('https://cdn.jsdelivr.net/npm/@babel/polyfill@7.8.3/dist/polyfill.min.js')
                    .then(function () {return loadScript('https://cdn.jsdelivr.net/npm/asmcrypto.js@2.3.2/asmcrypto.all.es5.min.js')})
                    .then(function () {return loadScript('https://rawcdn.githack.com/indutny/elliptic/60489415e545efdfd3010ae74b9726facbf08ca8/dist/elliptic.min.js')})
                    .then(function () {return loadScript('https://cdn.jsdelivr.net/npm/webcrypto-liner@1.2.2/build/webcrypto-liner.shim.min.js')})
                    .then(function () {return loadScript('https://cdn.jsdelivr.net/npm/protobufjs@6.8.8/dist/protobuf.min.js')})
                    .then(function () {return loadScript('https://cdn.jsdelivr.net/npm/@webcrypto-local/client@1.1.0/build/webcrypto-socket.min.js')})
                    .then(function () {return loadScript('js/main.js')})
                    .catch(function(e) {
                        document.getElementById('loading').style.display = 'none';
                        document.getElementById('outdated').style.display = '';
                        console.error(e);
                    });
            });
        }
    </script>
</div>

</body>
</html>
PHP
<?php
if (!isset($_GET['action'])) {
    die();
}

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

$fileToSign = $assetsDirectory . '/pdfs/tektown/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']) {
        // This action expects the certificate of the signer.
        // It prepares the PDF document accordingly.
        case 'start':
            if (isset($_SESSION['tmpDocument'])) {
                @unlink($_SESSION['tmpDocument']->getWriter()->getPath());
            }

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

            // 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();
            $module->setDigest(\SetaPDF_Signer_Digest::SHA_256);

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

            // pass the user certificate to the module
            $module->setCertificate($certificate);

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

            $extraCerts = new \SetaPDF_Signer_X509_Collection();

            // 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;
                        }
                    }
                }
            }

            $module->setExtraCertificates($extraCerts);

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

            $signer->setSignatureContentLength($signatureContentLength);

            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();
                    $signer->setSignatureContentLength($signatureContentLength + 6000);
                }
            }

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

            // prepare the PDF
            $_SESSION['tmpDocument'] = $signer->preSign(
                new \SetaPDF_Core_Writer_File($tempPath),
                $module
            );

            $_SESSION['module'] = $module;

            // prepare the response
            $response = [
                'dataToSign' => \SetaPDF_Core_Type_HexString::str2hex(
                    $module->getDataToSign($_SESSION['tmpDocument']->getHashFile())
                )
            ];

            // 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 time stamp
        case 'complete':
            $data = json_decode(file_get_contents('php://input'));
            if (!isset($data->signature)) {
                die();
            }

            $data->signature = \SetaPDF_Core_Type_HexString::hex2str($data->signature);

            // 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['module']->setSignatureValue($data->signature);

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

            // verify that the received signature matches to the CMS package and document.
            $signedData = new \SetaPDF_Signer_Cms_SignedData($cms);
            $signedData->setDetachedSignedData($_SESSION['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['tmpDocument']);
            }

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

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

            $id = $_SESSION['pdfs']['currentId']++;
            $_SESSION['pdfs']['docs']['id-' . $id] = $writer;
            // send the response
            header('Content-Type: application/json; charset=utf-8');
            echo json_encode(['id' => $id]);
            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];

            header('Content-Type: application/pdf');
            header('Content-Disposition: attachment; filename="' . basename($fileToSign, '.pdf') . '-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, replacements) {
        replacements = replacements || {};
        let e = document.getElementById(id);
        if (!e.htmlTpl) {
            e.htmlTpl = e.innerHTML;
        }

        if (Object.keys(replacements).length > 0) {
            let tpl = e.htmlTpl;
            for (let k in replacements) {
                if (!replacements.hasOwnProperty(k)) {
                    continue;
                }

                let v = replacements[k];
                tpl = tpl.replace('{' + k + '}', v);
            }
            e.innerHTML = tpl;
        }

        e.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 ws = new WebcryptoSocket.SocketProvider({
            storage: await WebcryptoSocket.BrowserStorage.create(),
        });

        // Checks if end-to-end session is approved
        let handleChallenge = async () => {
            if (!await ws.isLoggedIn()) {
                const pin = await ws.challenge();
                // show PIN
                show('challengeExchange', {pin: pin});
                // ask to approve session
                try {
                    await ws.login();
                } catch (e) {
                    if (confirm('Challenge was not accepted. Retry?')) {
                        await handleChallenge();
                    }
                }
                hide('challengeExchange');
            }
        };

        let providersSelect = document.getElementById('providersSelect'),
            certificatesSelect = document.getElementById('certificatesSelect'),
            signBtn = document.getElementById('signBtn'),
            downloadBtn = document.getElementById('downloadBtn');

        signBtn.disabled = true;
        downloadBtn.disabled = true;

        let init = () => {
            show('signatureControlsPanel');
            ws.cardReader
                .on("insert", () => updateProviders())
                .on("remove", () => updateProviders());
            updateProviders();
        };

        let updateProviders = async () => {
            const info = await ws.info();
            let selected = false;
            let currentProviderId = providersSelect.value;

            providersSelect.length = 0;

            if (!info.providers.length) {
                const option = document.createElement("option");
                option.textContent = "No providers";
                option.setAttribute("value", "");
                option.disabled = true;
                providersSelect.appendChild(option);
                providersSelect.dispatchEvent(new Event('change'));
                return;
            }

            for (const provider of info.providers) {
                const option = document.createElement("option");
                option.setAttribute("value", provider.id);
                option.textContent = provider.name;
                if (currentProviderId === provider.id) {
                    option.setAttribute("selected", "selected");
                    selected = true;
                }
                providersSelect.appendChild(option);
            }

            if (!selected) {
                providersSelect.firstElementChild.setAttribute("selected", "selected");
            }

            providersSelect.dispatchEvent(new Event('change'));
        };

        providersSelect.addEventListener('change', () => updateCertificates());

        let certs, provider;
        let updateCertificates = async () => {
            if (providersSelect.value === '') {
                certificatesSelect.length = 0;
                const option = document.createElement("option");
                option.textContent = "No certificates";
                option.setAttribute("value", "");
                option.disabled = true;
                certificatesSelect.appendChild(option);

                return;
            }

            provider = await ws.getCrypto(providersSelect.value);
            if (!(await provider.isLoggedIn())) {
                try {
                    await provider.login();
                } catch (e) {
                    // you may map e.code to a more meaningful message. A list of codes is available
                    // here: https://github.com/PeculiarVentures/fortify-web/blob/master/src/sagas/error.js
                    alert(e.message);
                    providersSelect.length = 0;
                    await updateProviders();
                    return;
                }
            }

            certs = [];
            let certIds = await provider.certStorage.keys();
            certIds = certIds.filter((id) => {
                const parts = id.split("-");
                return parts[0] === "x509";
            });

            let keyIds = await provider.keyStorage.keys();
            keyIds = keyIds.filter((id) => (id.split("-")[0] === "private"));

            const extractCommonName = (name) => {
                let reg = /CN=([^,]+),?/i,
                    res = reg.exec(name);
                return res ? res[1] : "Unknown";
            };

            for (const certId of certIds) {
                for (const keyId of keyIds) {
                    if (keyId.split("-")[2] === certId.split("-")[2]) {
                        try {
                            const cert = await provider.certStorage.getItem(certId);
                            certs.push({
                                id: certId,
                                item: cert,
                                name: extractCommonName(cert.subjectName),
                                pem: await provider.certStorage.exportCert('pem', cert),
                                privateKey: await provider.keyStorage.getItem(keyId)
                            });
                        } catch (e) {
                            console.error(`Cannot get certificate ${certId} from CertificateStorage. ${e.message}`);
                        }
                    }
                }
            }

            const now = new Date();
            certs = certs
                .filter((cert) => (cert.item.notBefore < now && now < cert.item.notAfter))
                .sort((a, b) => (a.name.localeCompare(b.name, undefined, {sensitivity: 'base'})));

            certificatesSelect.length = 0;
            certs.forEach((cert, index) => {
                const option = document.createElement("option"),
                    issuer = extractCommonName(cert.item.issuerName);

                option.setAttribute("value", index);
                option.textContent = cert.name + ' (' + issuer + '; '
                    + ' not before:' + cert.item.notBefore.toLocaleString() + '; '
                    + ' not after:' + cert.item.notAfter.toLocaleString() + ')';
                certificatesSelect.appendChild(option);
            });

            signBtn.disabled = (certs.length === 0);
        };

        let lastId = null;
        let sign = async () => {
            let cert = certs[certificatesSelect.value];

            try {
                let startResponseText = await postRequest(
                    'controller.php?action=start',
                    JSON.stringify({
                        certificate: cert.pem,
                        useAIA: document.getElementById('useAIA').checked,
                        useTimestamp: document.getElementById('useTimestamp').checked
                    })
                );
                let startJson = JSON.parse(startResponseText),
                    privateKey = cert.privateKey;

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

                let signature = await provider.subtle.sign(alg, privateKey, message);
                let completeResponseText = await postRequest(
                    'controller.php?action=complete',
                    JSON.stringify({signature: toHex(signature)})
                );
                let completeJson = JSON.parse(completeResponseText);
                lastId = completeJson.id;
                downloadBtn.disabled = false;
                window.open('controller.php?action=download&id=' + lastId);
            } catch (error) {
                console.info(error);
                alert('An error occured: ' + error.responseText);
            }
        };

        downloadBtn.addEventListener('click', () => window.open('controller.php?action=download&id=' + lastId));
        signBtn.addEventListener('click', () => sign());

        ws.connect("127.0.0.1:31337")
            .on("error", (e) => {
                hide('loading');
                show('fortifyNotReachable');
                console.error(e);
            })
            .on("listening", (e) => {
                hide('loading');

                handleChallenge()
                    .then(() => {
                        return ws.isLoggedIn();
                    })
                    .then((isLoggedIn) => {
                        // was it successfully?
                        if (!isLoggedIn) {
                            show('notLoggedIn');
                            return;
                        }

                        init();
                    }, (error) => {
                        console.error(error)
                    });
            });
    }

    //noinspection JSIgnoredPromiseFromCall
    main();
})();