Everything Tamperlens detects, and exactly what defeats each one

Every signal family against five evasions, derived from the registry the engine actually iterates.

Reference: PDF fraud signals — the field guide

Every vendor in this space publishes what it catches. None of them publish what makes it stop working. This is the second list, for my own engine, in full.

Why publish the manual

The argument against a page like this is that it is a how-to guide for the people I am trying to catch. I do not think that survives contact with reality.

Every command below is on the first page of results for "how to remove pdf metadata". Someone building fake statements for money already owns the tools and already knows which one leaves the cleanest file — that knowledge is what they sell. The people who do not know are the ones on the receiving end: the analyst, the letting agent, the accountant who reads a clean report and hears this document is genuine.

A clean report from a byte-level parser means the bytes are clean. Those are different claims, and the entire risk lives in the gap between them. So here is the gap, drawn to scale.

The five columns

Four of these are the ones people actually run. The fifth is in the table because my own code puts it there.

1. Metadata strip

exiftool -all= statement.pdf

What most people think this does is delete the metadata. What it does to a PDF is append a new revision in which the metadata is blank. The original bytes stay exactly where they were.

Byte-layout diagram: before the strip the file is one revision; after the strip the original bytes are unchanged and a second revision is appended carrying a blanked Info dictionary. before objects + /Info xref trailer /ID %%EOF after objects + /Info xref trailer /ID %%EOF blank /Info xref /Prev trailer, %%EOF revision 1 — byte-identical to the original revision 2 — the strip Any signature signed only what is left of the dashed line. Everything right of it is now uncovered.
A metadata strip on a PDF is an append, not a delete. The revision count goes up by one, and on a signed document the appended bytes fall outside the signed /ByteRange. This is why the tool's own documentation tells you to run a rewriter afterwards.

2. Whole-file rewrite

qpdf --linearize in.pdf out.pdf
gs -sDEVICE=pdfwrite -o out.pdf in.pdf

One revision comes out the other side, with a rebuilt cross-reference table and no history. The two tools are not equivalent, though, and the difference decides five cells in the table below.

A structural rewriter copies through what it does not need to understand — the Info dictionary, the XMP packet, the font objects. A re-renderer regenerates all of it from what it drew. Same column, opposite outcome, and I have not measured which tools sit where.

3. Print-to-PDF

Chromium's Skia backend, macOS Quartz, Microsoft's print driver. The document is rendered and a new file is written from the rendering. Nothing structural crosses over.

Diagram: printing to PDF discards the old file's account of itself — metadata, identifiers, revision history, signature, annotations — and writes a fresh structure from the rendered page. Only the appearance of the page crosses over. the old file's account of itself Info dictionary · XMP packet trailer /ID pair revision chain (/Prev) font subset prefixes signature /ByteRange annotations · JavaScript all discarded render then write anew what the new file says one revision, no /Prev fresh /ID, both halves equal producer = the printer one font subset per face no signature, no annotations coherent, recent timestamps a pristine document What does cross over: the page as it looks. Including, in paint order, the text that somebody hid under a black rectangle.
The reason print-to-PDF beats most of this table is not subtlety. It throws the evidence away and writes a new file that has never been edited, because it genuinely has not been. What it cannot throw away is the picture — which is where the two families in the last column of the matrix survive.

4. Flatten to raster

Every page becomes one image. No text, no fonts, no structure worth reading. The heaviest hammer available and the only one in this list with a real cost, which I come back to at the end.

5. Encrypt with an owner password

Not a bypass anybody markets, and the one column here that is derived purely from my own source. An encrypted PDF has its strings and streams as ciphertext, so five families are skipped by the engine rather than run on noise. The list is a constant called ENCRYPTED_SUPPRESSED_FAMILIES, and it is five names long.

An owner password does not stop anyone opening the file. It still turns off a third of the engine.

The matrix

Rows are the 12 registered families, in registry order. Eleven of them run on every document; issuer-mismatch only runs when the caller supplies a baseline, and it is the last row for that reason.

gone means the family cannot fire. weaker means it fires with less to say. louder means the evasion itself creates the finding. untested means I could not decide it from the code and have not run the experiment.

Heat grid: every signal family against every evasion. Most cells go quiet; the issuer-mismatch row gets louder in all five columns. strip rewrite print raster encrypt incremental-updates louder gone gone gone fires metadata-mismatch gone untested gone gone gone date-anomalies gone untested gone gone gone producer-fingerprint weaker untested weaker louder gone id-inconsistency untested gone gone gone fires font-anomalies fires untested gone gone gone hybrid-page fires fires untested gone gone active-content fires untested gone gone fires signature-coverage louder gone gone gone fires structure-warnings fires gone gone gone louder redaction-exposure fires fires untested gone weaker issuer-mismatch louder louder louder louder louder gone weaker fires louder untested
The shape, without the reading. Two columns are nearly solid: raster silences almost everything, and the bottom row survives everything. The detail — and the reasons — are in the table below.
Family Metadata strip Whole-file rewrite Print-to-PDF Flatten to raster Encrypt
incremental-updates louder — the strip is the second revision gone — one revision out gone gone fires — the xref chain is not encrypted
metadata-mismatch gone — needs both stores present untested — copied through, or regenerated? gone — one writer fills both gone gone — suppressed by the engine
date-anomalies gone — no timestamps left untested — same question gone — fresh, coherent dates gone gone — suppressed
producer-fingerprint weaker — drops to "declares no producing tool", severity low untested — qpdf keeps the string, a re-render replaces it weaker — Quartz and Microsoft are on the list, Skia is not louder — a full-page image escalates the finding to high gone — suppressed
id-inconsistency untested — does the appended trailer touch /ID[1]? gone — one save, both halves equal gone gone fires — /ID lives in the trailer
font-anomalies fires — fonts are not metadata untested — a re-subset would merge the duplicates gone — one subset per face gone — no fonts at all gone — suppressed
hybrid-page fires fires — the image and the text both copy through untested — depends whether the printer keeps one image per page gone — no text layer left to be suspicious about gone — suppressed
active-content fires — a strip touches metadata only untested — structural copy keeps it, a re-render drops it gone — actions are not printed gone fires — read from dictionary keys, and the finding says so
signature-coverage louder — appended bytes land outside the signed range gone — signature destroyed, and nothing reports its absence gone gone fires — /ByteRange is plain
structure-warnings fires — an append repairs nothing gone — repairing is what a rewriter is for gone gone louder — carries the disclosure and names what was skipped
redaction-exposure fires — the words under the box are content fires — paint order copies through untested — the marks half is gone, the covered-text half probably survives gone — and here the evasion is also the correct fix weaker — unapplied marks still counted, covered text unreadable
issuer-mismatch (needs a baseline) louder — an absent producer mismatches too louder louder — two dimensions move, which is what reaches high louder — producer and font set both move louder — suppressed fields read as absent

Three cells worth reading twice

A metadata strip makes two families louder

Because it is an append. The revision count goes from one to two, which is exactly what incremental-updates looks for. And on a signed document the appended region sits past the end of the signed /ByteRange, which is the highest-severity finding the engine has.

Someone tidying a PDF before sending it to me makes the report worse, not better. That is not clever detection on my part. It is the file format.

Removing the producer string does not silence the producer family

With no producing tool declared anywhere, producer-fingerprint still returns a finding: document declares no producing tool, at severity low.

Low is the right place for it, and our own corpus is the reason. Six genuine Huntington account-analysis statements carry no producer and no creator string at all — high-volume composition engines often emit none. An absent toolchain is weakly suspicious and weakly normal at the same time, so it sits at the bottom of the ladder and stays there.

Chromium's print-to-PDF trips nothing, on purpose

The consumer-editor list has around fifty entries. Quartz PDFContext is on it. Microsoft Print to PDF is on it. Skia/PDF, which is what Chrome writes when you print a page, is not.

I know how to close that. I am not going to. When we parsed 213 PDFs published by US banks, two Bank of America online-banking agreements came back as Skia/PDF m130 and Skia/PDF m135 — printed straight out of a browser by whoever maintained the page. Adding Skia to that list turns a real bank's real document into a finding, on a page that is genuinely from the bank.

So printing from Chrome is the cheapest evasion in this table. It costs nothing, takes one keystroke, and my engine says nothing about the tool that did it.

What survives everything

Read the matrix down the columns and the answer is uncomfortable: nothing in the unconditional set survives all five.

Bar chart: how many of the eleven unconditional signal families each evasion silences, and what the evasion costs the person sending the document. families silenced, of 11 what it costs the sender metadata strip 2 costs nothing — and appends a revision whole-file rewrite 4 costs any signature encrypt (owner password) 5 costs nothing — the report says so out loud print-to-PDF 8 costs the whole revision history flatten to raster 10 costs the text layer
Counting only cells marked gone; the untested ones are left out, so three of these bars are floors rather than measurements. Cost is what the evasion takes away from the person sending the document, which is the column that decides whether anyone actually does it.

The one row that survives is issuer-mismatch, and it is the row that needs something the document cannot supply. Every other family reads the file's own account of itself, so rewriting the file rewrites the evidence. That one compares the file to documents from the institution it claims to come from — and all five evasions replace the toolchain a real issuer used.

That is the whole asymmetry, in one sentence: self-description is defeated by regeneration, and comparison to an outside reference is not. The price is that you have to hold the reference. Public specimens mostly do not work for this; documents you already know are genuine do.

The raster trade-off, honestly

Flattening every page to an image silences ten of the eleven. It is the strongest move on the board and I have nothing that beats it.

What it costs is the text layer. No selection, no copy-paste, no extraction, no screen reader. If the document is going into any pipeline that reads the text — an income parser, an accounting import, a reconciliation step — a raster fails on arrival, and it fails before anyone gets to the question of whether it is real.

But that cost lands on the workflow, not on my parser. If a human is going to open the file and look at it, a flattened page sails past everything on this site.

The concession

A full-page raster is not itself reported as a finding, because scanners exist and a scanned document looks exactly like that. On a flattened file the engine is mostly silent, and silence here means I could not look, not I looked and it was fine. If your process accepts raster PDFs, assume this engine is close to blind on them and put the control somewhere else.

One more thing the columns hide: they compose. The strip tool's own documentation tells you to run a rewriter afterwards, precisely because the strip alone leaves the original bytes on disk. Run both and you have removed the metadata and the history in one go. That pair is the realistic attack, and it is cheaper than either of the last two columns.

The cells I could not fill in

Eight cells say untested. They are not hedging — they are the cells where the answer depends on which tool ran, and I have derived this table from source rather than from a bench full of files.

  • id-inconsistency against a metadata strip — whether the appended trailer rewrites the second half of /ID.
  • metadata-mismatch, date-anomalies, producer-fingerprint, font-anomalies and active-content against a whole-file rewrite — all five turn on the same unmeasured question, whether the rewriter copies those objects through or regenerates them.
  • hybrid-page against print-to-PDF — whether the print path emits one image per page or tiles it, which decides whether the page still reads as a full-page image at all.
  • redaction-exposure against print-to-PDF — the unapplied-marks half is certainly gone; whether the covered text is re-emitted underneath the box is the part I have not run.

Each of those is a morning's work with a fixture set, and when I run it this post gets superseded by a dated one rather than quietly corrected.

The number I will not give you

The obvious next question is what fraction of real document fraud this catches. I do not know, and neither does anyone else selling this.

Every academic benchmark I could find is pixel-level image forensics; there is no PDF-structure equivalent. No vendor publishes a false-positive rate on legitimate documents. The one public figure I have seen is an unmethodologised 80% on a competitor's site.

So the claim is high precision, unknown recall. When a family fires, the report shows the bytes it fired on and you can check them yourself. When nothing fires, you have learned that this particular set of tests came back quiet — which, after reading the table above, you now know how little that can mean.

A finding is a statement about bytes. "This document is fraudulent" is a statement about a person, and a parser does not get to make it.

Run it on a file of your own

The free checker takes a PDF and gives back the same report this table describes, with the raw evidence under every finding. No account, nothing stored.

If you want to see the table hold up, run a document twice: once as you received it, once after printing it to PDF. The difference is the point of this post.