Fortify Web-component
This demo shows an implementation with the web-component of Fortify.
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 <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 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\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\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 \setasign\SetaPDF2\Signer\X509\Extension\TimeStamp $ts */
$ts = $certificate->getExtensions()->get(\setasign\SetaPDF2\Signer\X509\Extension\TimeStamp::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);
// 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;
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 '
+ '<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.';
}
const message = fromHex(startJson.dataToSign);
const alg = {
name: privateKey.algorithm.name,
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)})
);
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();
})();
