Fortify Web-component
This demo shows an implementation with the web-component of Fortify and the SetaPDF-Signer component.
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://cdn.jsdelivr.net/npm/@peculiar/fortify-webcomponents@4/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 AIA extension. </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 document was 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://cdn.jsdelivr.net/npm/@peculiar/fortify-webcomponents@4/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 use setasign\SetaPDF2\Core\Document; use setasign\SetaPDF2\Core\Type\PdfHexString; use setasign\SetaPDF2\Core\Writer\FileWriter; use setasign\SetaPDF2\Core\Writer\StringWriter; use setasign\SetaPDF2\Core\Writer\TempFileWriter; use setasign\SetaPDF2\Signer\Asn1\Element as Asn1Element; use setasign\SetaPDF2\Signer\Cms\SignedData; use setasign\SetaPDF2\Signer\InformationResolver\HttpCurlResolver; use setasign\SetaPDF2\Signer\InformationResolver\Manager as InformationResolverManager; use setasign\SetaPDF2\Signer\Signature\Appearance\Dynamic; use setasign\SetaPDF2\Signer\Signature\Module\Pades as PadesModule; use setasign\SetaPDF2\Signer\SignatureField; use setasign\SetaPDF2\Signer\Signer; use setasign\SetaPDF2\Signer\Timestamp\Module\Rfc3161\Curl as CurlTimestampModule; use setasign\SetaPDF2\Signer\X509\Certificate; use setasign\SetaPDF2\Signer\X509\Collection; use setasign\SetaPDF2\Signer\X509\Extension\AuthorityInformationAccess; use setasign\SetaPDF2\Signer\X509\Extension\TimeStamp as TimeStampExtension; use setasign\SetaPDF2\Signer\X509\Format; 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']) { case 'preview': $doc = file_get_contents($fileToSign); // 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: inline; filename="' . basename($fileToSign, '.pdf') . '.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['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 = Document::loadByFilename($fileToSign); // create a signer instance $signer = new Signer($document); // create a module instance $module = new PadesModule(); // create a certificate instance $certificate = new Certificate($data->certificate); // pass the user certificate to the module $module->setCertificate($certificate); // setup information resolver manager $informationResolverManager = new InformationResolverManager(); $informationResolverManager->addResolver(new HttpCurlResolver([ \CURLOPT_FOLLOWLOCATION => true, \CURLOPT_MAXREDIRS => 5 ])); $extraCerts = new Collection(); // get issuer certificates if (isset($data->useAIA) && $data->useAIA) { $certificates = [$certificate]; while (count($certificates) > 0) { /** @var Certificate $currentCertificate */ $currentCertificate = array_pop($certificates); /** @var AuthorityInformationAccess $aia */ $aia = $currentCertificate->getExtensions()->get(AuthorityInformationAccess::OID); if ($aia instanceof 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(Format::DER)) * 2); } $signer->setSignatureContentLength($signatureContentLength); unset($_SESSION['tsUrl']); // get timestamp information and use it if (isset($data->useTimestamp) && $data->useTimestamp) { /** @var TimeStampExtension $ts */ $ts = $certificate->getExtensions()->get(TimeStampExtension::OID); if ($ts && $ts->getVersion() === 1 && $ts->requiresAuth() === false) { $_SESSION['tsUrl'] = $ts->getLocation(); $signer->setSignatureContentLength($signatureContentLength + 6000); } } // A simple example to add a visible signature. $field = $signer->addSignatureField( 'Signature', 1, SignatureField::POSITION_LEFT_TOP, ['x' => 20, 'y' => -20], 180, 60 ); $signer->setSignatureFieldName($field->getQualifiedName()); $appearance = new Dynamic($module); $signer->setAppearance($appearance); // you may use an own temporary file handler $tempPath = TempFileWriter::createTempPath(); // prepare the PDF $tmpDocument = $signer->preSign( new FileWriter($tempPath), $module ); // prepare the response $response = [ 'dataToSign' => PdfHexString::str2hex( $module->getDataToSign($tmpDocument->getHashFile()) ), 'extraCerts' => array_map(function (Certificate $cert) { return $cert->get(Format::PEM); }, $extraCerts->getAll()), 'tsUrl' => $_SESSION['tsUrl'] ?? false ]; $_SESSION['tmpDocument'] = $tmpDocument; $_SESSION['module'] = $module; // send it header('Content-Type: application/json; charset=utf-8'); echo json_encode($response); break; // This action embeds the signature in the CMS container // and optionally requests and embeds the timestamp case 'complete': $data = json_decode(file_get_contents('php://input')); if (!isset($data->signature)) { die(); } $data->signature = PdfHexString::hex2str($data->signature); // create the document instance $writer = new StringWriter(); $document = Document::loadByFilename($fileToSign, $writer); $signer = new Signer($document); if (isset($data->algorithmName) && $data->algorithmName === 'ECDSA') { $len = \strlen($data->signature); // prohibit 'Uninitialized string offset 0' error if ($len < 2) { throw new Exception('Signature value is faulty!'); } $s = \substr($data->signature, 0, $len / 2); if (\ord($s[0]) & 0x80) { // ensure positive integers $s = "\0" . $s; } $r = \substr($data->signature, $len / 2); if (\ord($r[0]) & 0x80) { // ensure positive integers $r = "\0" . $r; } $signatureValueElm = new Asn1Element( Asn1Element::SEQUENCE | Asn1Element::IS_CONSTRUCTED, '', [ new Asn1Element(Asn1Element::INTEGER, $s), new Asn1Element(Asn1Element::INTEGER, $r), ] ); $data->signature = (string)$signatureValueElm; } // 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 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 CurlTimestampModule($_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]; // 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="' . 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) {
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(url) {
document.getElementById('previewContainer').innerHTML = '<verify-viewer'
+ ' url="' + url + '"'
+ ' show-signature-if-present="false"'
+ ' notify-if-not-signed="false"'
+ ' sign="false"'
+ ' search="false"'
+ ' download="true"'
+ ' style="height: 100%;"'
+ ' ></verify-viewer>';
}
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('selectionCancel', function () {
hide('fortifyContainer');
show('signButtonContainer');
});
fortifyComp.addEventListener('selectionSuccess', async function (event) {
let signature, algorithmName;
try {
show('loader');
document.getElementById('loader').setAttribute('data-text', 'Signing document');
let provider = await event.detail.socketProvider.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 AIA extension.';
} 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.';
}
const message = fromHex(startJson.dataToSign);
algorithmName = privateKey.algorithm.name;
const alg = {
name: algorithmName,
hash: "SHA-256",
};
signature = await provider.subtle.sign(alg, privateKey, message);
} catch (error) {
hide('loader');
console.info(error);
alert('An error occured: ' + error);
return;
}
try {
let completeResponseText = await postRequest(
controllerPath + '?action=complete',
JSON.stringify({signature: toHex(signature), algorithmName: algorithmName})
);
let completeJson = JSON.parse(completeResponseText);
lastId = completeJson.id;
initVerify(controllerPath + '?action=download&id=' + lastId);
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');
initVerify(controllerPath + '?action=preview');
show('signButtonContainer');
});
initVerify(controllerPath + '?action=preview');
show('signatureControlsPanel');
hide('loader');
}
//noinspection JSIgnoredPromiseFromCall
main();
})();
