npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@scanmate/seal

v0.24.6

Published

Is a signed PDF still the document that was signed? Verifies each signature's own bytes: the digest, the signature, what it covers, and who signed - without claiming legal validity.

Readme

@scanmate/seal

Is a signed PDF still the document that was signed?

The rest of this suite compares a returned document with the one that was issued: pixels, words, fields, boxes. This asks the other question, of a document that came back born-digital and signed: has the file changed since someone signed it? A signature answers that in its own bytes, and nothing else here can.

Install

npm install @scanmate/seal
import { readFile } from 'node:fs/promises'
import { verifySignatures } from '@scanmate/seal'

const report = await verifySignatures(await readFile('agreement-signed.pdf'))

report.signed                          // the file carries a signature at all
report.unbroken                        // every signature intact, and the last covers the whole file
report.signatures[0].signer?.subject   // CN=A Person, O=A Company
report.signatures[0].problems          // why not, when not

What it checks

| | | |---|---| | intact | The bytes this signature covers digest to what the signature says, and that digest is signed by the key of the certificate it carries. In short: the document is, byte for byte, what was signed. | | whole | The signature covers the whole file. A PDF may be signed and then added to - a second signature, a form filled - so intact and not whole means "this much is vouched for, and there is more". uncovered counts the bytes. | | signer | What the certificate says about itself: subject, issuer, serial, validity, and whether it signed itself. | | signedAt | /M, what the signing software recorded. Evidence of nothing on its own - a clock the signer controls - but reported, and compared against the certificate's validity. |

Each problem is named rather than lumped into a boolean: digest-mismatch (the document changed, or is not what was signed), signature-invalid, not-covered (bytes appended after signing), certificate-expired, unreadable.

What it does not check, and will not claim

This does not tell you a signature is legally valid. That question is about trust, and trust is not in the file:

  • No trust list. Whether the signing certificate chains to a root anyone recognises - Adobe's AATL, the EU trusted lists, your own organisation's - is not checked. A self-signed certificate made five minutes ago verifies here exactly as a qualified one does, and signer.selfSigned is how you tell them apart.
  • No revocation. A certificate revoked the day after signing still verifies. OCSP and CRL are network protocols with their own failure modes, and answering them badly is worse than not answering.
  • No timestamp validation. A signature timestamp, where one exists, is not checked against a time authority; signedAt is only what the file says.
  • It does not sign. This reads and verifies; it never writes a signature.

What is left is the part that is in the file, and it is the part that catches a document edited after signing - which is the question this suite exists to ask. Treat unbroken: true as "the bytes are as signed", and answer "should this signer be trusted?" with your own policy, or with a qualified validation service.

How it reads a PDF

By its bytes, with no PDF library. That is not a shortcut: /Contents holds a signature over the file's own bytes, so a signature dictionary cannot be compressed into an object stream or encrypted without the offsets in /ByteRange ceasing to mean anything. Every signed PDF therefore carries its signatures in plain sight - and reading them this way still works on a file too damaged for a parser to open, which is exactly the file whose signature you most want to check. A spec covers that case: a wrecked cross-reference table, a signature still read, and the tampering reported.

The cryptography is pkijs over Node's own WebCrypto. The tests sign for real: a key, a self-signed certificate and a PDF signed over its /ByteRange, made in the test rather than committed as a fixture, because a fixture cannot prove a verifier works.

What it has been tried on

Signatures this package makes itself, in its tests: RSA with SHA-256, one signer, a detached CMS in adbe.pkcs7.detached, with the certificate carried in the signature. Those cover the paths that matter - digest, signature, coverage, dates, and picking the signer out of a carried chain - and each failure is provoked rather than imagined: a byte changed, bytes appended, a certificate out of date, a wrecked cross-reference table.

And three real signed documents, run locally and not committed - private papers, which is the only kind there is: a property reservation signed through Adobe Sign in ETSI.CAdES.detached, and two adbe.pkcs7.detached signatures, one from a commercial signing service and one from a named individual. All three verified as intact, and one of them was found to carry 59,945 bytes appended after signing, reported as not-covered with the earlier bytes still vouched for. That is not a pass mark for every producer, but it is a measurement rather than a hope, and two of the three exercised paths the tests here cannot make: a CAdES signature, and a full certificate chain.

Those real files also found two defects that no self-made fixture could:

  • A signature dictionary was read by taking the nearest << before /ByteRange, which on real signatures lands inside /Prop_Build <</App<<…>>>> - the nested dictionary naming the signing software. /SubFilter and everything else in the real dictionary went unread. Brackets are now balanced, and a run of >>>> is counted as two closers rather than three.
  • The signer was read as the first certificate carried. A real signature carries a chain, and the first of it is as often the authority as the signer; one document reported a certificate authority where a person had signed. The signer is now matched by the issuer and serial that SignerInfo names.

Still unmeasured: DocuSign, a qualified European provider, ECDSA or RSA-PSS keys, signature timestamps, and a document with more than one signature. pkijs implements all of them, so they are expected to work - expected, not measured. A file of yours is how any of those stops being a guess.

Alongside the rest

const report = await verifySignatures(returned)
if (!report.unbroken) {
  // The file changed after it was signed: no comparison of pixels is needed to know that.
}

// And for what the paper says, rather than what the bytes do:
const audit = await new Scanmate(issued, returned).audit()

A born-digital return with an unbroken signature still needs the audit: a signature proves nobody edited the file, not that the document says what was agreed.