/* * Based on PDFSign v1.0.0 * https://github.com/Communication-Systems-Group/pdfsign.js * * Copyright 2015, Thomas Bocek, University of Zurich * * Licensed under the MIT license: * http://www.opensource.org/licenses/MIT */ import { ObjectIdentifier, UTCTime, OctetString } from 'asn1js'; import { ContentInfo, SignedData, Attribute, SignerInfo, IssuerAndSerialNumber, SignedAndUnsignedAttributes, EncapsulatedContentInfo, getCrypto } from 'pkijs'; import { parseCertificate, parsePrivateKey } from './signingUtilities'; //keep this import to include the patched version of pdfjs library import {PDFJS} from "../lib/pdfjs.parser.js"; function createXrefTable(xrefEntries) { xrefEntries = sortOnKeys(xrefEntries); let retVal = 'xref\n'; let last = -2; for (let i in xrefEntries) { i = parseInt(i); if (typeof xrefEntries[i].offset === 'undefined') { continue; } retVal += calcFlow(i, last, xrefEntries); const offset = xrefEntries[i].offset; retVal += pad10(offset) + ' ' + pad5(xrefEntries[i].gen) + ' ' + (xrefEntries[i].free ? 'f' : 'n') + ' \n'; last = i; } return retVal; } function calcFlow(i, last, xrefEntries) { if (last + 1 === i) { return ''; } let count = 1; while (typeof xrefEntries[i + count] !== 'undefined' && typeof xrefEntries[i + count].offset !== 'undefined') { count++; } return i + ' ' + count + '\n'; } function createTrailer(topDict, startxref, sha256Hex, size, prev) { let retVal = 'trailer <<\n'; retVal += ' /Size ' + size + '\n'; const refRoot = topDict.getRaw('Root'); if (typeof refRoot !== 'undefined') { retVal += ' /Root ' + refRoot.num + ' ' + refRoot.gen + ' R\n'; } const refInfo = topDict.getRaw('Info'); if (typeof refInfo !== 'undefined') { retVal += ' /Info ' + refInfo.num + ' ' + refInfo.gen + ' R\n'; } retVal += ' /ID [<' + sha256Hex.substring(0,32) + '><' + sha256Hex.substring(32,64) + '>]\n'; if (typeof prev !== 'undefined') { retVal += ' /Prev ' + prev + '\n'; } retVal += '>>\n'; retVal += 'startxref\n'; retVal += startxref + '\n'; retVal += '%%EOF\n'; return retVal; } function createXrefTableAppend(xrefEntries) { xrefEntries = sortOnKeys(xrefEntries); let retVal = 'xref\n'; let last = -2; for (let i in xrefEntries) { i = parseInt(i); if (typeof xrefEntries[i].offset === 'undefined') { continue; } retVal += calcFlow(i, last, xrefEntries); const offset = xrefEntries[i].offset; retVal += pad10(offset) + ' ' + pad5(xrefEntries[i].gen) + ' ' + (xrefEntries[i].free ? 'f' : 'n') + ' \n'; last = i; } return retVal; } //http://stackoverflow.com/questions/10946880/sort-a-dictionary-or-whatever-key-value-data-structure-in-js-on-word-number-ke function sortOnKeys(dict) { const sorted = []; for (const key in dict) { sorted[sorted.length] = key; } sorted.sort(); const tempDict = {}; for (let i = 0; i < sorted.length; i++) { tempDict[sorted[i]] = dict[sorted[i]]; } return tempDict; } function removeFromArray(array, from, to) { const cutlen = to - from; const buf = new Uint8Array(array.length - cutlen); for (let i = 0; i < from; i++) { buf[i] = array[i]; } for (let i = to, len = array.length; i < len; i++) { buf[i - cutlen] = array[i]; } return buf; } function findXrefBlocks(xrefBlocks) { const num = xrefBlocks.length / 2; const retVal = []; for (let i = 0; i < num; i++) { retVal.push({start: xrefBlocks[i], end: xrefBlocks[i + num]}); } return retVal; } function convertUint8ArrayToBinaryString(u8Array) { let i, len = u8Array.length, bStr = ""; for (i = 0; i < len; i++) { bStr += String.fromCharCode(u8Array[i]); } return bStr; } function arrayObjectIndexOf(array, start, end, orig) { for (let i = 0, len = array.length; i < len; i++) { if ((array[i].start === start) && (array[i].end === end) && (array[i].orig === orig)) { return i; } } return -1; } function pad10(num) { const s = "000000000" + num; return s.substr(s.length - 10); } function pad5(num) { const s = "0000" + num; return s.substr(s.length - 5); } function pad2(num) { const s = "0" + num; return s.substr(s.length - 2); } function findRootEntry(xref) { const rootNr = xref.root.objId.substring(0, xref.root.objId.length - 1); return xref.entries[rootNr]; } function findSuccessorEntry(xrefEntries, current) { //find it first const currentOffset = current.offset; let currentMin = Number.MAX_SAFE_INTEGER; let currentMinIndex = -1; for (const i in xrefEntries) { if (xrefEntries[i].offset > currentOffset) { if (xrefEntries[i].offset < currentMin) { currentMin = xrefEntries[i].offset; currentMinIndex = i; } } } if (currentMinIndex === -1) { return current; } return xrefEntries[currentMinIndex]; } function updateArray(array, pos, str) { const upd = stringToUint8Array(str); for (let i = 0, len = upd.length; i < len; i++) { array[i + pos] = upd[i]; } return array; } function copyToEnd(array, from, to) { const buf = new Uint8Array(array.length + (to - from)); for (let i = 0, len = array.length; i < len; i++) { buf[i] = array[i]; } for (let i = 0, len = to - from; i < len; i++) { buf[array.length + i] = array[from + i]; } return buf; } function insertIntoArray(array, pos, str) { const ins = stringToUint8Array(str); const buf = new Uint8Array(array.length + ins.length); for (let i = 0; i < pos; i++) { buf[i] = array[i]; } for (let i = 0; i < ins.length; i++) { buf[pos + i] = ins[i]; } for (let i = pos; i < array.length; i++) { buf[ins.length + i] = array[i]; } return buf; } function stringToUint8Array(str) { const buf = new Uint8Array(str.length); for (let i = 0, strLen = str.length; i < strLen; i++) { buf[i] = str.charCodeAt(i); } return buf; } function uint8ArrayToString(buf, from, to) { if (typeof from !== 'undefined' && typeof to !== 'undefined') { let s = ''; for (let i = from; i < to; i++) { s = s + String.fromCharCode(buf[i]); } return s; } return String.fromCharCode.apply(null, buf); } function findFreeXrefNr(xrefEntries, used) { used = typeof used !== 'undefined' ? used : []; let inc = used.length; for (let i = 1; i < xrefEntries.length; i++) { const index = used.indexOf(i); const entry = xrefEntries["" + i]; if (index === -1 && (typeof entry === 'undefined' || entry.free)) { return i; } if (index !== -1) { inc--; } } return xrefEntries.length + inc; } function find(uint8, needle, start, limit) { start = typeof start !== 'undefined' ? start : 0; limit = typeof limit !== 'undefined' ? limit : Number.MAX_SAFE_INTEGER; const search = stringToUint8Array(needle); let match = 0; for (let i = start; i < uint8.length && i < limit; i++) { if (uint8[i] === search[match]) { match++; } else { match = 0; if (uint8[i] === search[match]) { match++; } } if (match === search.length) { return (i + 1) - match; } } return -1; } function findBackwards(uint8, needle, start, limit) { start = typeof start !== 'undefined' ? start : uint8.length; limit = typeof limit !== 'undefined' ? limit : Number.MAX_SAFE_INTEGER; const search = stringToUint8Array(needle); let match = search.length - 1; for (let i = start; i >= 0 && i < limit; i--) { if (uint8[i] === search[match]) { match--; } else { match = search.length - 1; if (uint8[i] === search[match]) { match--; } } if (match === 0) { return i - 1; } } return -1; } function strHex(s) { let a = ""; for (let i = 0; i < s.length; i++) { a = a + pad2(s.charCodeAt(i).toString(16)); } return a; } async function sha256(array) { const cryptoLib = getCrypto(); const digestTmpBuf = await cryptoLib.digest({ name: "SHA-256" }, array); const digestTmpArray = new Uint8Array(digestTmpBuf); const digestTmpStr = uint8ArrayToString(digestTmpArray); const sha256Hex = strHex(digestTmpStr); return sha256Hex; } function isSigInRoot(pdf) { if (typeof pdf.acroForm === 'undefined') { return false; } return pdf.acroForm.get('SigFlags') === 3; } function updateXrefOffset(xref, offset, offsetDelta) { for (const i in xref.entries) { if (xref.entries[i].offset >= offset) { xref.entries[i].offset += offsetDelta; } } for (const i in xref.xrefBlocks) { if (xref.xrefBlocks[i] >= offset) { xref.xrefBlocks[i] += offsetDelta; } } } function updateXrefBlocks(xrefBlocks, offset, offsetDelta) { for (const i in xrefBlocks) { if (xrefBlocks[i].start >= offset) { xrefBlocks[i].start += offsetDelta; } if (xrefBlocks[i].end >= offset) { xrefBlocks[i].end += offsetDelta; } } } function updateOffset(pos, offset, offsetDelta) { if (pos >= offset) { return pos + offsetDelta; } return pos; } function round256(x) { return (Math.ceil(x / 256) * 256) - 1; } /** * (D:YYYYMMDDHHmmSSOHH'mm) * e.g. (D:20151210164400+01'00') * where: * YYYY shall be the year * MM shall be the month (01–12) * DD shall be the day (01–31) * HH shall be the hour (00–23) * mm shall be the minute (00–59) * SS shall be the second (00–59) * O shall be the relationship of local time to Universal Time (UT), and shall be denoted by one of the characters PLUS SIGN (U+002B) (+), HYPHEN-MINUS (U+002D) (-), or LATIN CAPITAL LETTER Z (U+005A) (Z) (see below) * HH followed by APOSTROPHE (U+0027) (') shall be the absolute value of the offset from UT in hours (00–23) * mm shall be the absolute value of the offset from UT in minutes (00–59) */ function now(date) { //date = typeof date !== 'undefined' ? date : new Date(); const yyyy = date.getFullYear().toString(); const MM = pad2(date.getMonth() + 1); const dd = pad2(date.getDate()); const hh = pad2(date.getHours()); const mm = pad2(date.getMinutes()); const ss = pad2(date.getSeconds()); return yyyy + MM + dd + hh + mm + ss + createOffset(date); } function createOffset(date) { const sign = date.getTimezoneOffset() > 0 ? "-" : "+"; const offset = Math.abs(date.getTimezoneOffset()); const hours = pad2(Math.floor(offset / 60)); const minutes = pad2(offset % 60); return sign + hours + "'" + minutes; } async function newSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey) { // {annotEntry} is the ref to the annot widget. If we enlarge the array, make sure all the offsets // after the modification will be updated -> xref table and startxref const annotEntry = findFreeXrefNr(pdf.xref.entries); // we'll store all the modifications we make, as we need to adjust the offset in the PDF const offsetForm = find(pdf.stream.bytes, '<<', root.offset, rootSuccessor.offset) + 2; //first we need to find the root element and add the following: // // /AcroForm<</Fields[{annotEntry} 0 R] /SigFlags 3>> // const appendAcroForm = '/AcroForm<</Fields[' + annotEntry + ' 0 R] /SigFlags 3>>'; //before we insert the acroform, we find the right place for annotentry //we need to add Annots [x y R] to the /Type /Page section. We can do that by searching /Contents[ const pages = pdf.catalog.catDict.get('Pages'); //get first page, we have hidden sig, so don't bother const ref = pages.get('Kids')[0]; const xref = pdf.xref.fetch(ref); const offsetContentEnd = xref.get('#Contents_offset'); //we now search backwards, this is safe as we don't expect user content here let offsetContent = findBackwards(pdf.stream.bytes, '/Contents', offsetContentEnd); const appendAnnots = '/Annots[' + annotEntry + ' 0 R]\n '; //now insert string into stream let array = insertIntoArray(pdf.stream.bytes, offsetForm, appendAcroForm); //recalculate the offsets in the xref table, only update those that are affected updateXrefOffset(pdf.xref, offsetForm, appendAcroForm.length); offsetContent = updateOffset(offsetContent, offsetForm, appendAcroForm.length); array = insertIntoArray(array, offsetContent, appendAnnots); updateXrefOffset(pdf.xref, offsetContent, appendAnnots.length); offsetContent = -1; //not needed anymore, don't update when offset changes //Then add to the next free object (annotEntry) //add right before the xref table or stream //if its a table, place element before the xref table // // sigEntry is the ref to the signature content. Next we need the signature object const sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]); // // {annotEntry} 0 obj // <</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature)/V Y 0 R>> // endobj // const append = annotEntry + ' 0 obj\n<</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature' + annotEntry + ')/V ' + sigEntry + ' 0 R>>\nendobj\n\n'; // we want the offset just before the last xref table or entry const blocks = findXrefBlocks(pdf.xref.xrefBlocks); let offsetAnnot = blocks[0].start; array = insertIntoArray(array, offsetAnnot, append); //no updateXrefOffset, as the next entry will be following // // {sigEntry} 0 obj // <</Contents <0481801e6d931d561563fb254e27c846e08325570847ed63d6f9e35 ... b2c8788a5> // /Type/Sig/SubFilter/adbe.pkcs7.detached/Location(Ghent)/M(D:20120928104114+02'00') // /ByteRange [A B C D]/Filter/Adobe.PPKLite/Reason(Test)/ContactInfo()>> // endobj // //the next entry goes below the above let offsetSig = offsetAnnot + append.length; // Both {annotEntry} and {sigEntry} objects need to be added to the last xref table. The byte range needs // to be adjusted. Since the signature will always be in a gap, use first an empty sig // to check the size, add ~25% size, then calculate the signature and place in the empty // space. const start = sigEntry + ' 0 obj\n<</Contents <'; const dummy = await signPki(signingCert, certificateChain, privateKey, stringToUint8Array('A'), date); //TODO: Adobe thinks its important to have the right size, no idea why this is the case const crypto = new Array(round256(dummy.length * 2)).join('0'); const middle = '>\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:' + now(date) + '\')\n/ByteRange '; let byteRange = '[0000000000 0000000000 0000000000 0000000000]'; const end = '/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n'; //all together const append2 = start + crypto + middle + byteRange + end; const offsetByteRange = start.length + crypto.length + middle.length; array = insertIntoArray(array, offsetSig, append2); updateXrefOffset(pdf.xref, offsetAnnot, append2.length + append.length); //find the xref tables, remove them and also the EOF, as we'll write a new table const xrefBlocks = findXrefBlocks(pdf.xref.xrefBlocks); for (const i in xrefBlocks) { const oldSize = array.length; array = removeFromArray(array, xrefBlocks[i].start, xrefBlocks[i].end); const length = array.length - oldSize; updateXrefOffset(pdf.xref, xrefBlocks[i].start, length); //check for %%EOF and remove it as well const offsetEOF = find(array, '%%EOF', xrefBlocks[i].start, xrefBlocks[i].start + 20); if (offsetEOF > 0) { const lengthEOF = '%%EOF'.length; array = removeFromArray(array, offsetEOF, offsetEOF + lengthEOF); updateXrefOffset(pdf.xref, offsetEOF, -lengthEOF); updateXrefBlocks(xrefBlocks, offsetEOF, -lengthEOF); offsetAnnot = updateOffset(offsetAnnot, offsetEOF, -lengthEOF); offsetSig = updateOffset(offsetSig, offsetEOF, -lengthEOF); } updateXrefBlocks(xrefBlocks, xrefBlocks[i].start, length); offsetAnnot = updateOffset(offsetAnnot, xrefBlocks[i].start, length); offsetSig = updateOffset(offsetSig, xrefBlocks[i].start, length); } const sha256Hex = await sha256(array); //add the new entries to the xref pdf.xref.entries[annotEntry] = {offset: offsetAnnot, gen: 0, free: false}; pdf.xref.entries[sigEntry] = {offset: offsetSig, gen: 0, free: false}; let xrefTable = createXrefTable(pdf.xref.entries); //also empty entries count as in the PDF spec, page 720 (example) xrefTable += createTrailer(pdf.xref.topDict, array.length, sha256Hex, pdf.xref.entries.length); array = insertIntoArray(array, array.length, xrefTable); //since we consolidate, no prev! [adjust /Prev -> rawparsing + offset] const from1 = 0; const to1 = offsetSig + start.length; const from2 = to1 + crypto.length; const to2 = (array.length - from2) - 1; byteRange = '[' + pad10(from1) + ' ' + pad10(to1 - 1) + ' ' + pad10(from2 + 1) + ' ' + pad10(to2) + ']'; array = updateArray(array, offsetSig + offsetByteRange, byteRange); const data = removeFromArray(array, to1 - 1, from2 + 1); const crypto2 = await signPki(signingCert, certificateChain, privateKey, data.buffer, date); array = updateArray(array, to1, crypto2); return array; } async function appendSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey) { //copy root and the entry with contents to the end const startRoot = pdf.stream.bytes.length + 1; let array = copyToEnd(pdf.stream.bytes, root.offset - 1, rootSuccessor.offset); //since we signed the first one, we know how the pdf has to look like: const offsetAcroForm = find(array, '/AcroForm<</Fields', startRoot); const endOffsetAcroForm = find(array, ']', offsetAcroForm); const annotEntry = findFreeXrefNr(pdf.xref.entries); const sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]); const appendAnnot = ' ' + annotEntry + ' 0 R'; array = insertIntoArray(array, endOffsetAcroForm, appendAnnot); //we need to add Annots [x y R] to the /Type /Page section. We can do that by searching /Annots const pages = pdf.catalog.catDict.get('Pages'); //get first page, we have hidden sig, so don't bother const contentRef = pages.get('Kids')[0]; const xref = pdf.xref.fetch(contentRef); const offsetAnnotEnd = xref.get('#Annots_offset'); //we now search ], this is safe as we signed it previously const endOffsetAnnot = find(array, ']', offsetAnnotEnd); const xrefEntry = pdf.xref.getEntry(contentRef.num); const xrefEntrySuccosser = findSuccessorEntry(pdf.xref.entries, xrefEntry); const offsetAnnotRelative = endOffsetAnnot - xrefEntrySuccosser.offset; const startContent = array.length; array = copyToEnd(array, xrefEntry.offset, xrefEntrySuccosser.offset); array = insertIntoArray(array, array.length + offsetAnnotRelative, appendAnnot); const startAnnot = array.length; const append = annotEntry + ' 0 obj\n<</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature' + annotEntry + ')/V ' + sigEntry + ' 0 R>>\nendobj\n\n'; array = insertIntoArray(array, startAnnot, append); const startSig = array.length; const start = sigEntry + ' 0 obj\n<</Contents <'; const dummy = await signPki(signingCert, certificateChain, privateKey, stringToUint8Array('A'), date); //TODO: Adobe thinks its important to have the right size, no idea why this is the case const crypto = new Array(round256(dummy.length * 2)).join('0'); const middle = '>\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:' + now(date) + '\')\n/ByteRange '; let byteRange = '[0000000000 0000000000 0000000000 0000000000]'; const end = '/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n'; //all together const append2 = start + crypto + middle + byteRange + end; array = insertIntoArray(array, startSig, append2); const sha256Hex = await sha256(array); const prev = pdf.xref.xrefBlocks[0]; const startxref = array.length; const xrefEntries = []; xrefEntries[0] = {offset: 0, gen: 65535, free: true}; xrefEntries[pdf.xref.topDict.getRaw('Root').num] = {offset: startRoot, gen: 0, free: false}; xrefEntries[contentRef.num] = {offset: startContent, gen: 0, free: false}; xrefEntries[annotEntry] = {offset: startAnnot, gen: 0, free: false}; xrefEntries[sigEntry] = {offset: startSig, gen: 0, free: false}; let xrefTable = createXrefTableAppend(xrefEntries); xrefTable += createTrailer(pdf.xref.topDict, startxref, sha256Hex, xrefEntries.length, prev); array = insertIntoArray(array, array.length, xrefTable); const from1 = 0; const to1 = startSig + start.length; const from2 = to1 + crypto.length; const to2 = (array.length - from2) - 1; byteRange = '[' + pad10(from1) + ' ' + pad10(to1 - 1) + ' ' + pad10(from2 + 1) + ' ' + pad10(to2) + ']'; array = updateArray(array, from2 + middle.length, byteRange); //now sign from1-to1 / from2-to2 and update byterange const data = removeFromArray(array, to1 - 1, from2 + 1); const crypto2 = await signPki(signingCert, certificateChain, privateKey, data.buffer, date); array = updateArray(array, to1, crypto2); return array; } function loadPdf(pdfArray) { const pdf = new pdfjsCoreDocument.PDFDocument(false, pdfArray, ''); pdf.parseStartXRef(); pdf.parse(); return pdf; } //data must be Uint8Array async function signPki(signingCert, certificateChain, privateKey, data, date) { const crypto = getCrypto(); //date = typeof date !== 'undefined' ? date : new Date(); const hashAlg = "SHA-256"; const digest = await crypto.digest({ name: hashAlg }, data); const signedAttr = []; signedAttr.push(new Attribute({ type: "1.2.840.113549.1.9.3", values: [ new ObjectIdentifier({ value: "1.2.840.113549.1.7.1" }) ] })); // contentType signedAttr.push(new Attribute({ type: "1.2.840.113549.1.9.4", values: [ new OctetString({ valueHex: digest }) ] })); // messageDigest signedAttr.push(new Attribute({ type: "1.2.840.113549.1.9.5", values: [ new UTCTime({ valueDate: date }) ] })); // signingTime const cmsSignedSimpl = new SignedData({ version: 1, encapContentInfo: new EncapsulatedContentInfo({ eContentType: "1.2.840.113549.1.7.1" // "data" content type }), signerInfos: [ new SignerInfo({ version: 1, sid: new IssuerAndSerialNumber({ issuer: signingCert.issuer, serialNumber: signingCert.serialNumber }), signedAttrs: new SignedAndUnsignedAttributes({ type: 0, attributes: signedAttr }) }) ], certificates: certificateChain }); await cmsSignedSimpl.sign(privateKey, 0, hashAlg, data.buffer); const cmsSignedSchema = cmsSignedSimpl.toSchema(true); const cmsContentSimp = new ContentInfo({ contentType: "1.2.840.113549.1.7.2", content: cmsSignedSchema }); const _cmsSignedSchema = cmsContentSimp.toSchema(); //region Make length of some elements in "indefinite form" _cmsSignedSchema.lenBlock.isIndefiniteForm = true; const block1 = _cmsSignedSchema.valueBlock.value[1]; block1.lenBlock.isIndefiniteForm = true; const block2 = block1.valueBlock.value[0]; block2.lenBlock.isIndefiniteForm = true; //endregion const cmsSignedBuffer = _cmsSignedSchema.toBER(false); const cmsSignedArray = new Uint8Array(cmsSignedBuffer); const cmsSignedString = uint8ArrayToString(cmsSignedArray); const hex = strHex(cmsSignedString); return hex; } async function signPdfObjects(pdfRaw, signingCert, certificateChain, privateKey, date) { date = typeof date !== 'undefined' ? date : new Date(); if (pdfRaw instanceof ArrayBuffer) { pdfRaw = new Uint8Array(pdfRaw); } const pdf = loadPdf(pdfRaw); const root = findRootEntry(pdf.xref); const rootSuccessor = findSuccessorEntry(pdf.xref.entries, root); if (!isSigInRoot(pdf)) { return await newSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey); } else { return await appendSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey); } } export function signPdf(pdfRaw, signingCert, certificateChain, privateKey) { const signingCertObj = parseCertificate(signingCert); const certificateChainObj = []; certificateChainObj[0] = parseCertificate(signingCert); for (let i = 0; i < certificateChain.length; i++) { certificateChainObj[i + 1] = parseCertificate(certificateChain[i]); } return parsePrivateKey(privateKey).then(privateKeyDecoded => { return signPdfObjects(pdfRaw, signingCertObj, certificateChainObj, privateKeyDecoded); }); }