UNPKG

node-signpdf

Version:
188 lines (160 loc) 6.92 kB
import forge from 'node-forge'; import SignPdfError from './SignPdfError'; import {removeTrailingNewLine, findByteRange} from './helpers'; import {DEFAULT_BYTE_RANGE_PLACEHOLDER} from './helpers/const'; export {default as SignPdfError} from './SignPdfError'; export * from './helpers'; export * from './helpers/const'; export class SignPdf { constructor() { this.byteRangePlaceholder = DEFAULT_BYTE_RANGE_PLACEHOLDER; this.lastSignature = null; } sign( pdfBuffer, p12Buffer, additionalOptions = {}, ) { const options = { asn1StrictParsing: false, passphrase: '', ...additionalOptions, }; if (!(pdfBuffer instanceof Buffer)) { throw new SignPdfError( 'PDF expected as Buffer.', SignPdfError.TYPE_INPUT, ); } if (!(p12Buffer instanceof Buffer)) { throw new SignPdfError( 'p12 certificate expected as Buffer.', SignPdfError.TYPE_INPUT, ); } let pdf = removeTrailingNewLine(pdfBuffer); // Find the ByteRange placeholder. const {byteRangePlaceholder} = findByteRange(pdf); if (!byteRangePlaceholder) { throw new SignPdfError( `Could not find empty ByteRange placeholder: ${byteRangePlaceholder}`, SignPdfError.TYPE_PARSE, ); } const byteRangePos = pdf.indexOf(byteRangePlaceholder); // Calculate the actual ByteRange that needs to replace the placeholder. const byteRangeEnd = byteRangePos + byteRangePlaceholder.length; const contentsTagPos = pdf.indexOf('/Contents ', byteRangeEnd); const placeholderPos = pdf.indexOf('<', contentsTagPos); const placeholderEnd = pdf.indexOf('>', placeholderPos); const placeholderLengthWithBrackets = (placeholderEnd + 1) - placeholderPos; const placeholderLength = placeholderLengthWithBrackets - 2; const byteRange = [0, 0, 0, 0]; byteRange[1] = placeholderPos; byteRange[2] = byteRange[1] + placeholderLengthWithBrackets; byteRange[3] = pdf.length - byteRange[2]; let actualByteRange = `/ByteRange [${byteRange.join(' ')}]`; actualByteRange += ' '.repeat(byteRangePlaceholder.length - actualByteRange.length); // Replace the /ByteRange placeholder with the actual ByteRange pdf = Buffer.concat([ pdf.slice(0, byteRangePos), Buffer.from(actualByteRange), pdf.slice(byteRangeEnd), ]); // Remove the placeholder signature pdf = Buffer.concat([ pdf.slice(0, byteRange[1]), pdf.slice(byteRange[2], byteRange[2] + byteRange[3]), ]); // Convert Buffer P12 to a forge implementation. const forgeCert = forge.util.createBuffer(p12Buffer.toString('binary')); const p12Asn1 = forge.asn1.fromDer(forgeCert); const p12 = forge.pkcs12.pkcs12FromAsn1( p12Asn1, options.asn1StrictParsing, options.passphrase, ); // Extract safe bags by type. // We will need all the certificates and the private key. const certBags = p12.getBags({ bagType: forge.pki.oids.certBag, })[forge.pki.oids.certBag]; const keyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag, })[forge.pki.oids.pkcs8ShroudedKeyBag]; const privateKey = keyBags[0].key; // Here comes the actual PKCS#7 signing. const p7 = forge.pkcs7.createSignedData(); // Start off by setting the content. p7.content = forge.util.createBuffer(pdf.toString('binary')); // Then add all the certificates (-cacerts & -clcerts) // Keep track of the last found client certificate. // This will be the public key that will be bundled in the signature. let certificate; Object.keys(certBags).forEach((i) => { const {publicKey} = certBags[i].cert; p7.addCertificate(certBags[i].cert); // Try to find the certificate that matches the private key. if (privateKey.n.compareTo(publicKey.n) === 0 && privateKey.e.compareTo(publicKey.e) === 0 ) { certificate = certBags[i].cert; } }); if (typeof certificate === 'undefined') { throw new SignPdfError( 'Failed to find a certificate that matches the private key.', SignPdfError.TYPE_INPUT, ); } // Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects. p7.addSigner({ key: privateKey, certificate, digestAlgorithm: forge.pki.oids.sha256, authenticatedAttributes: [ { type: forge.pki.oids.contentType, value: forge.pki.oids.data, }, { type: forge.pki.oids.messageDigest, // value will be auto-populated at signing time }, { type: forge.pki.oids.signingTime, // value can also be auto-populated at signing time // We may also support passing this as an option to sign(). // Would be useful to match the creation time of the document for example. value: new Date(), }, ], }); // Sign in detached mode. p7.sign({detached: true}); // Check if the PDF has a good enough placeholder to fit the signature. const raw = forge.asn1.toDer(p7.toAsn1()).getBytes(); // placeholderLength represents the length of the HEXified symbols but we're // checking the actual lengths. if ((raw.length * 2) > placeholderLength) { throw new SignPdfError( `Signature exceeds placeholder length: ${raw.length * 2} > ${placeholderLength}`, SignPdfError.TYPE_INPUT, ); } let signature = Buffer.from(raw, 'binary').toString('hex'); // Store the HEXified signature. At least useful in tests. this.lastSignature = signature; // Pad the signature with zeroes so the it is the same length as the placeholder signature += Buffer .from(String.fromCharCode(0).repeat((placeholderLength / 2) - raw.length)) .toString('hex'); // Place it in the document. pdf = Buffer.concat([ pdf.slice(0, byteRange[1]), Buffer.from(`<${signature}>`), pdf.slice(byteRange[1]), ]); // Magic. Done. return pdf; } } export default new SignPdf();