js-pdf-signer
v1.0.0
Published
PDF 自签名证书 + PKCS#7 分离数字签名库(浏览器/Node 通用,依赖 WebCrypto,基于 pdf-lib)
Maintainers
Readme
js-pdf-signer
Add certificate-based PKCS#7 detached digital signatures (adbe.pkcs7.detached) to PDF files in the browser or Node.js. It generates a self-signed X.509 certificate on the fly, so no external certificate files are needed — the resulting signature is recognized by Adobe Acrobat, UPDF and other viewers as a real digital signature.
Built on top of pdf-lib and designed to work with it.
Features
- 🔐 Real digital signature — produces a standard PKCS#7/CMS
SignedDatathat can be verified by openssl, Adobe Acrobat, UPDF, etc. - 📜 Built-in self-signed certificate — generates an RSA-2048 key pair + X.509 self-signed certificate in-browser using WebCrypto, zero setup.
- 🌐 Browser & Node compatible — relies only on the global WebCrypto (
window.crypto/globalThis.crypto), no native Node dependencies. - 🖋 ByteRange detached signature — computes the
/ByteRangeper the PDF spec and signs only the digest, leaving the rest of the document untouched. - 📦 Build to an IIFE bundle —
npm run buildproducespdfsigner.bundle.js; include it with a<script>tag to get the globalPDFSigner. - 🧩 Works seamlessly with pdf-lib —
addSignaturePlaceholderoperates directly on a pdf-lib signature field.
Install
npm install js-pdf-signerThe publish registry is pinned to the official source:
publishConfig.registry = https://registry.npmjs.org/
Quick Start
Browser (built bundle)
The pre-built bundle is published as dist/pdfsigner.bundle.js. You can either load it from the installed package, or rebuild it yourself:
npm run build # generates dist/pdfsigner.bundle.js (and copies it to ../pdfsigner.bundle.js for the companion app in this repo)<script src="pdf-lib.min.js"></script>
<script src="node_modules/js-pdf-signer/dist/pdfsigner.bundle.js"></script>
<script>
// 1) Load/create a PDF with pdf-lib and get the signature field
const pdfDoc = await PDFLib.PDFDocument.load(bytes);
const field = pdfDoc.getForm().getField('Signature');
// 2) Add a signature placeholder (field /V → standalone signature dict)
window.PDFSigner.addSignaturePlaceholder({
pdfLib: PDFLib,
pdfDocLib: pdfDoc,
field,
});
// 3) IMPORTANT: save without object streams, otherwise the placeholder
// gets compressed and can no longer be located
const withPlaceholder = await pdfDoc.save({ useObjectStreams: false });
// 4) Sign (a self-signed certificate is generated and cached automatically)
const signed = await window.PDFSigner.signPdfBytes(withPlaceholder);
</script>Node.js (direct API)
const PDFLib = require('pdf-lib');
const { addSignaturePlaceholder, signPdfBytes } = require('js-pdf-signer');
const pdfDoc = await PDFLib.PDFDocument.load(bytes);
const field = pdfDoc.getForm().getField('Signature');
addSignaturePlaceholder({ pdfLib: PDFLib, pdfDocLib: pdfDoc, field });
const withPlaceholder = await pdfDoc.save({ useObjectStreams: false });
const signed = await signPdfBytes(withPlaceholder); // Buffer / Uint8ArrayAPI
addSignaturePlaceholder({ pdfLib, pdfDocLib, field, placeholderHexLen? })
Adds a signature placeholder to a pdf-lib signature field.
pdfLib— the pdf-lib namespace (pass your own instance to avoid bundling it twice)pdfDocLib— aPDFDocumentinstancefield— the signature field wrapper (field.acroField.dictmust exist)placeholderHexLen— length of the/Contentsplaceholder hex string; defaults toPLACEHOLDER_HEX_LEN(8192)
Per what Acrobat requires, the signature dictionary is created as a standalone object referenced by the field's /V (containing /Type /Sig, /ByteRange, /Contents, /Filter, /SubFilter), rather than being merged directly into the field dict.
signPdfBytes(pdfBytes, signer?)
Performs the ByteRange detached signature on PDF bytes that already contain a placeholder.
pdfBytes—Uint8Array/Buffer(must be the bytes saved withuseObjectStreams: falseafteraddSignaturePlaceholder)signer— optional{ cert, privateKey }; defaults to the session-cached self-signed signer- Returns — the signed
Uint8Array
Internally: locate the /ByteRange and /Contents placeholders → write the real ByteRange numbers → remove the placeholder → compute the SHA-256 digest → build the CMS SignedData (with contentType / signingTime / messageDigest signed attributes) → RSA-PKCS#1 v1.5 signature → write it back into /Contents.
getOrCreateSigner()
Returns the session-cached self-signed signer { cert, privateKey } (generates RSA-2048 + X.509 on first call, reuses afterwards).
makeSelfSignedCert()
Generates a brand-new self-signed certificate + key pair on every call.
buildCmsSignature(cert, privateKey, digestBytes, signingTime)
Low-level helper that builds the CMS ContentInfo DER bytes (usually not needed directly).
Utilities
bytesToLatin1 / latin1ToBytes / bytesToHex — PDF bytes ↔ latin1 string conversion (used by the ByteRange handling).
Constants
PLACEHOLDER_HEX_LEN — default /Contents placeholder hex length (8192, enough for an RSA-2048 signature + certificate).
How It Works
PDF digital signatures (ISO 32000-1 §12.8):
- The signature field's
Vpoints to a signature dictionary containing:/ByteRange [0 <len1> <start2> <len2>]— two byte ranges/Contents <...>— the signature data (DER-encoded PKCS#7/CMS)
- Compute a SHA-256 digest over everything except
/Contents(i.e. the two ByteRange spans). - The digest, together with
contentTypeandsigningTime, form the CMSsignedAttrs, which are signed with the private key (PKCS#1 v1.5). - Write the DER-encoded CMS (including the signer certificate) back into the
/Contentsplaceholder.
This library uses pkijs to generate the X.509 certificate and asn1js to build the CMS by hand (avoiding a SignedData serialization bug in some pkijs versions). All signing operations go through WebCrypto.
Development
npm install
npm run build # generates dist/pdfsigner.bundle.js (IIFE, exposes global PDFSigner)
npm test # end-to-end: create → sign → openssl cms -verifyDirectory layout:
src/
index.js # entry, exports the full API + pkijs/asn1js
utils.js # bytes ↔ latin1 ↔ hex
cert.js # self-signed X.509 + RSA-2048
cms.js # PKCS#7/CMS SignedData construction & signing
placeholder.js # add the /V signature placeholder to a pdf-lib field
sign-pdf.js # ByteRange detached signing + signer cache
build.mjs # esbuild build script (outputs dist/pdfsigner.bundle.js)
dist/ # built IIFE bundle (published to npm)
test/test.js # end-to-end testLimitations
- Self-signed certificate: Acrobat / UPDF will warn that the signer identity cannot be verified — normal for self-signed certs. The signature itself is cryptographically valid and can be checked with
openssl cms -verify. To show "verified", use a certificate from a trusted CA. - Single signature: the current implementation targets a single signature field; multiple sequential signatures (each covering the previous signature's
/Contents) are not yet implemented. - Zero padding in
/Contents: the placeholder is fixed-length and shorter signatures are padded with00bytes (same approach as signpdf), which mainstream validators tolerate.
License
MIT
