UNPKG

node-signpdf

Version:
355 lines (321 loc) 13.8 kB
import PDFDocument from 'pdfkit'; import forge from 'node-forge'; import fs from 'fs'; import signer from './signpdf'; import {pdfkitAddPlaceholder, extractSignature, plainAddPlaceholder} from './helpers'; import SignPdfError from './SignPdfError'; /** * Creates a Buffer containing a PDF. * Returns a Promise that is resolved with the resulting Buffer of the PDFDocument. * @returns {Promise<Buffer>} */ const createPdf = (params) => new Promise((resolve) => { const requestParams = { placeholder: {}, text: 'node-signpdf', addSignaturePlaceholder: true, pages: 1, ...params, }; const pdf = new PDFDocument({ autoFirstPage: false, size: 'A4', layout: 'portrait', bufferPages: true, }); pdf.info.CreationDate = ''; if (requestParams.pages < 1) { requestParams.pages = 1; } // Add some content to the page(s) for (let i = 0; i < requestParams.pages; i += 1) { pdf .addPage() .fillColor('#333') .fontSize(25) .moveDown() .text(requestParams.text) .save(); } // Collect the ouput PDF // and, when done, resolve with it stored in a Buffer const pdfChunks = []; pdf.on('data', (data) => { pdfChunks.push(data); }); pdf.on('end', () => { resolve(Buffer.concat(pdfChunks)); }); if (requestParams.addSignaturePlaceholder) { // Externally (to PDFKit) add the signature placeholder. const refs = pdfkitAddPlaceholder({ pdf, pdfBuffer: Buffer.from([pdf]), reason: 'I am the author', ...requestParams.placeholder, }); // Externally end the streams of the created objects. // PDFKit doesn't know much about them, so it won't .end() them. Object.keys(refs).forEach((key) => refs[key].end()); } // Also end the PDFDocument stream. // See pdf.on('end'... on how it is then converted to Buffer. pdf.end(); }); describe('Test signing', () => { it('expects PDF to be Buffer', () => { try { signer.sign('non-buffer', Buffer.from('')); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_INPUT); expect(e.message).toMatchSnapshot(); } }); it('expects P12 certificate to be Buffer', () => { try { signer.sign(Buffer.from(''), 'non-buffer'); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_INPUT); expect(e.message).toMatchSnapshot(); } }); it('expects PDF to contain a ByteRange placeholder', () => { try { signer.sign(Buffer.from('No BR placeholder\n%%EOF'), Buffer.from('')); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_PARSE); expect(e.message).toMatchSnapshot(); } }); it('expects a reasonably sized placeholder', async () => { try { const pdfBuffer = await createPdf({ placeholder: { signatureLength: 2, }, }); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); signer.sign(pdfBuffer, p12Buffer); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_INPUT); expect(e.message).toMatchSnapshot(); } }); it('signs input PDF', async () => { let pdfBuffer = await createPdf(); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); pdfBuffer = signer.sign(pdfBuffer, p12Buffer); expect(pdfBuffer instanceof Buffer).toBe(true); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('signs detached', async () => { const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); let pdfBuffer = await createPdf({text: 'Some text'}); signer.sign(pdfBuffer, p12Buffer); const signature1 = signer.lastSignature; pdfBuffer = await createPdf({text: 'some other text '.repeat(30)}); signer.sign(pdfBuffer, p12Buffer); const signature2 = signer.lastSignature; expect(signature1).not.toBe(signature2); expect(signature1).toHaveLength(signature2.length); }); it('signs a ready pdf', async () => { const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); let pdfBuffer = fs.readFileSync(`${__dirname}/../resources/w3dummy.pdf`); pdfBuffer = plainAddPlaceholder({ pdfBuffer, reason: 'I have reviewed it.', signatureLength: 1612, }); pdfBuffer = signer.sign(pdfBuffer, p12Buffer); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('signs a ready pdf that does not have Annots', async () => { const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); let pdfBuffer = fs.readFileSync(`${__dirname}/../resources/no-annotations.pdf`); pdfBuffer = plainAddPlaceholder({ pdfBuffer, reason: 'I have reviewed it.', signatureLength: 1612, }); pdfBuffer = signer.sign(pdfBuffer, p12Buffer); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('signs a ready pdf that does not have metadata', async () => { const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); let pdfBuffer = fs.readFileSync(`${__dirname}/../resources/no-metadata.pdf`); pdfBuffer = plainAddPlaceholder({ pdfBuffer, reason: 'I have reviewed it.', signatureLength: 1612, }); pdfBuffer = signer.sign(pdfBuffer, p12Buffer); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('signs a ready pdf two times', async () => { const secondP12Buffer = fs.readFileSync(`${__dirname}/../resources/withpass.p12`); let signedPdfBuffer = fs.readFileSync(`${__dirname}/../resources/signed-once.pdf`); signedPdfBuffer = plainAddPlaceholder({ pdfBuffer: signedPdfBuffer, reason: 'second', location: 'test location', signatureLength: 1592, }); signedPdfBuffer = signer.sign(signedPdfBuffer, secondP12Buffer, {passphrase: 'node-signpdf'}); const {signature, signedData} = extractSignature(signedPdfBuffer, 2); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('signs big PDF twice producing big AcroForm ID on the first time', async () => { let pdfBuffer = await createPdf({ pages: 100, }); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); pdfBuffer = signer.sign(pdfBuffer, p12Buffer); expect(pdfBuffer instanceof Buffer).toBe(true); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); const secondP12Buffer = fs.readFileSync(`${__dirname}/../resources/withpass.p12`); pdfBuffer = plainAddPlaceholder({ pdfBuffer, reason: 'second', location: 'test location', signatureLength: 1592, }); pdfBuffer = signer.sign(pdfBuffer, secondP12Buffer, {passphrase: 'node-signpdf'}); expect(pdfBuffer instanceof Buffer).toBe(true); const { signature: secondSignature, signedData: secondSignatureData, } = extractSignature(pdfBuffer, 2); expect(typeof secondSignature === 'string').toBe(true); expect(secondSignatureData instanceof Buffer).toBe(true); }); it('signs a ready pdf containing a link', async () => { const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); let pdfBuffer = fs.readFileSync(`${__dirname}/../resources/including-a-link.pdf`); pdfBuffer = plainAddPlaceholder({ pdfBuffer, reason: 'I have reviewed it.', location: 'some city', signatureLength: 1612, }); pdfBuffer = signer.sign(pdfBuffer, p12Buffer); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('signs with ca, intermediate and multiple certificates bundle', async () => { let pdfBuffer = await createPdf(); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/bundle.p12`); pdfBuffer = signer.sign(pdfBuffer, p12Buffer); expect(pdfBuffer instanceof Buffer).toBe(true); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('signs with passphrased certificate', async () => { let pdfBuffer = await createPdf(); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/withpass.p12`); pdfBuffer = signer.sign( pdfBuffer, p12Buffer, {passphrase: 'node-signpdf'}, ); expect(pdfBuffer instanceof Buffer).toBe(true); const {signature, signedData} = extractSignature(pdfBuffer); expect(typeof signature === 'string').toBe(true); expect(signedData instanceof Buffer).toBe(true); }); it('errors on wrong certificate passphrase', async () => { const pdfBuffer = await createPdf(); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/withpass.p12`); try { signer.sign( pdfBuffer, p12Buffer, {passphrase: 'Wrong passphrase'}, ); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof Error).toBe(true); expect(e.message).toMatchSnapshot(); } }); it('errors when no matching certificate is found in bags', async () => { const pdfBuffer = await createPdf(); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/bundle.p12`); // Monkey-patch pkcs12 to return no matching certificates although bundle.p12 is correct. const originalPkcs12FromAsn1 = forge.pkcs12.pkcs12FromAsn1; let p12Instance; forge.pkcs12.pkcs12FromAsn1 = (...params) => { // This instance will be used for all non-mocked code. p12Instance = originalPkcs12FromAsn1(...params); return { ...p12Instance, getBags: ({bagType}) => { if (bagType === forge.pki.oids.certBag) { // Only mock this case. // Make sure there will be no matching certificate. return { [forge.pki.oids.certBag]: [], }; } return p12Instance.getBags({bagType}); }, }; }; try { signer.sign( pdfBuffer, p12Buffer, ); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_INPUT); expect(e.message).toMatchSnapshot(); } finally { forge.pkcs12.pkcs12FromAsn1 = originalPkcs12FromAsn1; } }); it('error when final key inside trailer dictionary is /Root', async () => { let pdfBuffer = fs.readFileSync(`${__dirname}/../resources/w3dummy-different-trailer.pdf`); pdfBuffer = plainAddPlaceholder({ pdfBuffer, reason: 'I have reviewed it.', signatureLength: 1612, }); const trailer = pdfBuffer.slice(pdfBuffer.lastIndexOf('trailer')).toString(); // the trailer should contain only one startxref expect(trailer.match(/startxref/g).length).toBe(1); }); it('expects siging to fail because of no byteRangePlaceholder available to sign', async () => { try { const pdfBuffer = fs.readFileSync(`${__dirname}/../resources/signed.pdf`); const p12Buffer = fs.readFileSync(`${__dirname}/../resources/certificate.p12`); signer.sign(pdfBuffer, p12Buffer); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_PARSE); } }); });