/* * 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); var retVal = "xref\n"; var last = -2; for (var i in xrefEntries) { i = parseInt(i); if (typeof xrefEntries[i].offset === "undefined") { continue; } retVal += calcFlow(i, last, xrefEntries); var 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 ""; } var 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) { var retVal = "trailer <<\n"; retVal += " /Size " + size + "\n"; var refRoot = topDict.getRaw("Root"); if (typeof refRoot !== "undefined") { retVal += " /Root " + refRoot.num + " " + refRoot.gen + " R\n"; } var 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); var retVal = "xref\n"; var last = -2; for (var i in xrefEntries) { i = parseInt(i); if (typeof xrefEntries[i].offset === "undefined") { continue; } retVal += calcFlow(i, last, xrefEntries); var 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) { var sorted = []; for (var key in dict) { sorted[sorted.length] = key; } sorted.sort(); var tempDict = {}; for (var i = 0; i < sorted.length; i++) { tempDict[sorted[i]] = dict[sorted[i]]; } return tempDict; } function removeFromArray(array, from, to) { var cutlen = to - from; var buf = new Uint8Array(array.length - cutlen); for (var i = 0; i < from; i++) { buf[i] = array[i]; } for (var i = to, len = array.length; i < len; i++) { buf[i - cutlen] = array[i]; } return buf; } function findXrefBlocks(xrefBlocks) { var num = xrefBlocks.length / 2; var retVal = []; for (var i = 0; i < num; i++) { retVal.push({ start: xrefBlocks[i], end: xrefBlocks[i + num] }); } return retVal; } function convertUint8ArrayToBinaryString(u8Array) { var i, len = u8Array.length, b_str = ""; for (i = 0; i < len; i++) { b_str += String.fromCharCode(u8Array[i]); } return b_str; } function arrayObjectIndexOf(array, start, end, orig) { for (var 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) { var s = "000000000" + num; return s.substr(s.length - 10); } function pad5(num) { var s = "0000" + num; return s.substr(s.length - 5); } function pad2(num) { var s = "0" + num; return s.substr(s.length - 2); } function findRootEntry(xref) { var rootNr = xref.root.objId.substring(0, xref.root.objId.length - 1); return xref.entries[rootNr]; } function findSuccessorEntry(xrefEntries, current) { //find it first var currentOffset = current.offset; var currentMin = Number.MAX_SAFE_INTEGER; var currentMinIndex = -1; for (var 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) { var upd = stringToUint8Array(str); for (var i = 0, len = upd.length; i < len; i++) { array[i + pos] = upd[i]; } return array; } function copyToEnd(array, from, to) { var buf = new Uint8Array(array.length + (to - from)); for (var i = 0, len = array.length; i < len; i++) { buf[i] = array[i]; } for (var i = 0, len = to - from; i < len; i++) { buf[array.length + i] = array[from + i]; } return buf; } function insertIntoArray(array, pos, str) { var ins = stringToUint8Array(str); var buf = new Uint8Array(array.length + ins.length); for (var i = 0; i < pos; i++) { buf[i] = array[i]; } for (var i = 0; i < ins.length; i++) { buf[pos + i] = ins[i]; } for (var i = pos; i < array.length; i++) { buf[ins.length + i] = array[i]; } return buf; } function stringToUint8Array(str) { var buf = new Uint8Array(str.length); for (var 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") { var s = ""; for (var 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 : []; var inc = used.length; for (var i = 1; i < xrefEntries.length; i++) { var index = used.indexOf(i); var 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; var search = stringToUint8Array(needle); var match = 0; for (var 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; var search = stringToUint8Array(needle); var match = search.length - 1; for (var 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) { var a = ""; for (var 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; } if (!pdf.acroForm) { return false; } return pdf.acroForm.get("SigFlags") === 3; } function updateXrefOffset(xref, offset, offsetDelta) { for (var i in xref.entries) { if (xref.entries[i].offset >= offset) { xref.entries[i].offset += offsetDelta; } } for (var i in xref.xrefBlocks) { if (xref.xrefBlocks[i] >= offset) { xref.xrefBlocks[i] += offsetDelta; } } } function updateXrefBlocks(xrefBlocks, offset, offsetDelta) { for (var 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(); var yyyy = date.getFullYear().toString(); var MM = pad2(date.getMonth() + 1); var dd = pad2(date.getDate()); var hh = pad2(date.getHours()); var mm = pad2(date.getMinutes()); var ss = pad2(date.getSeconds()); return yyyy + MM + dd + hh + mm + ss + createOffset(date); } function createOffset(date) { var sign = date.getTimezoneOffset() > 0 ? "-" : "+"; var offset = Math.abs(date.getTimezoneOffset()); var hours = pad2(Math.floor(offset / 60)); var minutes = pad2(offset % 60); return sign + hours + "'" + minutes; } async function newSig( pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey ) { try { // {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 var annotEntry = findFreeXrefNr(pdf.xref.entries); // we'll store all the modifications we make, as we need to adjust the offset in the PDF var 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>> // var 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[ var pages = pdf.catalog.catDict.get("Pages"); //get first page, we have hidden sig, so don't bother var ref = pages.get("Kids")[0]; var xref = pdf.xref.fetch(ref); var offsetContentEnd = xref.get("#Contents_offset"); //we now search backwards, this is safe as we don't expect user content here var offsetContent = findBackwards( pdf.stream.bytes, "/Contents", offsetContentEnd ); var appendAnnots = "/Annots[" + annotEntry + " 0 R]\n "; //now insert string into stream var 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 ); var 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 var 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 // var 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 var blocks = findXrefBlocks(pdf.xref.xrefBlocks); var 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 var 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. var start = sigEntry + " 0 obj\n<</Contents <"; var dummy = await sign_pki( signingCert, certificateChain, privateKey, stringToUint8Array("A"), date ); //TODO: Adobe thinks its important to have the right size, no idea why this is the case var crypto = new Array(round256(dummy.length * 2)).join("0"); var middle = ">\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:" + now(date) + "')\n/ByteRange "; var byteRange = "[0000000000 0000000000 0000000000 0000000000]"; var end = "/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n"; //all together var append2 = start + crypto + middle + byteRange + end; var 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 var xrefBlocks = findXrefBlocks(pdf.xref.xrefBlocks); for (var i in xrefBlocks) { var oldSize = array.length; array = removeFromArray(array, xrefBlocks[i].start, xrefBlocks[i].end); var length = array.length - oldSize; updateXrefOffset(pdf.xref, xrefBlocks[i].start, length); //check for %%EOF and remove it as well var offsetEOF = find( array, "%%EOF", xrefBlocks[i].start, xrefBlocks[i].start + 20 ); if (offsetEOF > 0) { var 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); } var 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 }; var 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] var from1 = 0; var to1 = offsetSig + start.length; var from2 = to1 + crypto.length; var to2 = array.length - from2 - 1; var byteRange = "[" + pad10(from1) + " " + pad10(to1 - 1) + " " + pad10(from2 + 1) + " " + pad10(to2) + "]"; array = updateArray(array, offsetSig + offsetByteRange, byteRange); var data = removeFromArray(array, to1 - 1, from2 + 1); var crypto2 = await sign_pki( signingCert, certificateChain, privateKey, data.buffer, date ); array = updateArray(array, to1, crypto2); return array; } catch (err) { throw new Error("Error creating new signature in PDF: " + err); } } async function appendSig( pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey ) { try { //copy root and the entry with contents to the end var startRoot = pdf.stream.bytes.length + 1; var 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: var offsetAcroForm = find(array, "/AcroForm<</Fields", startRoot); var endOffsetAcroForm = find(array, "]", offsetAcroForm); var annotEntry = findFreeXrefNr(pdf.xref.entries); var sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]); var 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 var pages = pdf.catalog.catDict.get("Pages"); //get first page, we have hidden sig, so don't bother var contentRef = pages.get("Kids")[0]; var xref = pdf.xref.fetch(contentRef); var offsetAnnotEnd = xref.get("#Annots_offset"); //we now search ], this is safe as we signed it previously var endOffsetAnnot = find(array, "]", offsetAnnotEnd); var xrefEntry = pdf.xref.getEntry(contentRef.num); var xrefEntrySuccosser = findSuccessorEntry(pdf.xref.entries, xrefEntry); var offsetAnnotRelative = endOffsetAnnot - xrefEntrySuccosser.offset; var startContent = array.length; array = copyToEnd(array, xrefEntry.offset, xrefEntrySuccosser.offset); array = insertIntoArray( array, array.length + offsetAnnotRelative, appendAnnot ); var startAnnot = array.length; var 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); var startSig = array.length; var start = sigEntry + " 0 obj\n<</Contents <"; var dummy = await sign_pki( signingCert, certificateChain, privateKey, stringToUint8Array("A"), date ); //TODO: Adobe thinks its important to have the right size, no idea why this is the case var crypto = new Array(round256(dummy.length * 2)).join("0"); var middle = ">\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:" + now(date) + "')\n/ByteRange "; var byteRange = "[0000000000 0000000000 0000000000 0000000000]"; var end = "/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n"; //all together var append2 = start + crypto + middle + byteRange + end; array = insertIntoArray(array, startSig, append2); var sha256Hex = await sha256(array); var prev = pdf.xref.xrefBlocks[0]; var startxref = array.length; var 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 }; var xrefTable = createXrefTableAppend(xrefEntries); xrefTable += createTrailer( pdf.xref.topDict, startxref, sha256Hex, xrefEntries.length, prev ); array = insertIntoArray(array, array.length, xrefTable); var from1 = 0; var to1 = startSig + start.length; var from2 = to1 + crypto.length; var to2 = array.length - from2 - 1; var 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 var data = removeFromArray(array, to1 - 1, from2 + 1); var crypto2 = await sign_pki( signingCert, certificateChain, privateKey, data.buffer, date ); array = updateArray(array, to1, crypto2); return array; } catch (err) { throw new Error("Error appending signature in PDF: " + err); } } function loadPdf(pdfArray) { try { var pdf = new pdfjsCoreDocument.PDFDocument(false, pdfArray, ""); pdf.parseStartXRef(); pdf.parse(); return pdf; } catch (err) { throw new Error("Error parsing PDF: " + err); } } //data must be Uint8Array async function sign_pki(signingCert, certificateChain, privateKey, data, date) { console.log("Calling sign_pki"); 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 }); const signatureBuffer = 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 let 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 ) { console.log("Calling signPdfObjects"); date = typeof date !== "undefined" && date !== null ? date : new Date(); if (pdfRaw instanceof Buffer) { pdfRaw = new Uint8Array(pdfRaw); } else if (pdfRaw instanceof ArrayBuffer) { pdfRaw = new Uint8Array(pdfRaw); } console.log("Calling loadPdf"); var pdf = loadPdf(pdfRaw); console.log("Calling findRootEntry"); var root = findRootEntry(pdf.xref); console.log("Calling findSuccessorEntry"); var rootSuccessor = findSuccessorEntry(pdf.xref.entries, root); console.log("Calling isSigInRoot"); if (!isSigInRoot(pdf)) { console.log("Calling newSig"); return await newSig( pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey ); } else { console.log("Calling appendSig"); return await appendSig( pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey ); } } export async function signPdf( pdfRaw, signingCert, certificateChain, privateKey ) { console.log("Calling signPdf"); const signingCertObj = parseCertificate(signingCert); const certificateChainObj = []; certificateChainObj[0] = parseCertificate(signingCert); for (let i = 0; i < certificateChain.length; i++) { certificateChainObj[i + 1] = parseCertificate(certificateChain[i]); } let privateKeyDecoded; try { console.log("Calling parsePrivateKey"); privateKeyDecoded = await parsePrivateKey(privateKey); } catch (e) { console.log("Error decoding private key: ", e); throw new Error("Error decoding private key: " + e); } return await signPdfObjects( pdfRaw, signingCertObj, certificateChainObj, privateKeyDecoded ); }