Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 7 min read

How I built a DSC PDF signer in Rust and Tauri

I'm a company director, and in India that means signing board resolutions and financial statements with a Digital Signature Certificate (DSC). The DSC is a Class 3 certificate on a USB token, the same one used for MCA an

I'm a company director, and in India that means signing board resolutions and financial statements with a Digital Signature Certificate (DSC). The DSC is a Class 3 certificate on a USB token, the same one used for MCA and GST filings.

For a while, signing a PDF with that token meant paying for and installing a large commercial PDF suite. A lot of software for one click and a PIN. I wanted something small, free and offline that would never break a signature already on the PDF.

So I built TrackMy Sign, a free, open-source (MIT) desktop app for Windows, macOS and Linux. This post is about how it works underneath. The code is at github.com/rahuldk1105/trackmysign.

The stack

  • Rust for everything that matters: talking to the token, parsing PDFs, building signatures.
  • Tauri 2 for the desktop shell, so the app ships as a native binary with a webview rather than a bundled browser.
  • React for the interface.

The interesting parts are all on the Rust side, so that's where the rest of this goes.

Part 1: talking to the token with PKCS#11

An Indian DSC token is a smart card in a USB stick. The private key is generated on it and can't be exported. You never get the key; you ask the token to sign something with it.

The standard way to ask is PKCS#11, a C API that token vendors implement in a driver library: a .dll on Windows, a .dylib on macOS, a .so on Linux. Every vendor ships its own. The flow is the same for all of them:

  1. Load the vendor's library and call C_Initialize.
  2. Find a slot with a token in it and open a session.
  3. C_Login with the user's PIN.
  4. Find the certificate object and the private key that matches it.
  5. Call C_Sign with that key over the data to be signed.

Steps 3 and 5 are the ones that matter for security. The PIN goes to the token's driver, and the signing happens on the token. The private key never leaves it. TrackMy Sign doesn't store the PIN.

The awkward part is step 1. There's no standard place for the driver. Each vendor installs its library somewhere different, under a different name, on each platform. So the app keeps a list of known driver locations for common Indian tokens (HyperPKI, ePass2003, ProxKey, mToken, SafeNet and others), loads every one it finds, and lists the certificates from all of them together. A driver that fails to load is skipped rather than taking the app down with it. You can also point it at a driver in an odd location.

Part 2: what a PDF signature actually is

A PDF signature is a CMS (PKCS#7) SignedData structure stored inside the PDF. TrackMy Sign uses the standard adbe.pkcs7.detached sub-filter with SHA-256. "Detached" means the CMS blob doesn't contain the content it signs; it contains a hash of it, the signer's certificate chain, and the signature over those signed attributes.

The question is: which content? The answer is the ByteRange.

When you add a signature, you add a signature dictionary to the PDF. It has a /Contents entry, which is a hex string reserved for the CMS blob, and a /ByteRange entry, which is four numbers:

/ByteRange [0 840 960 240]
/Contents <000000...000000>

That reads as: "this signature covers bytes 0 to 840, and bytes 960 to 1200". The gap in the middle is exactly the /Contents hex string. The signature covers the whole file except the hole where the signature itself goes.

So signing is roughly:

  1. Write the new PDF content with a /Contents placeholder big enough for the signature, filled with zeros.
  2. Work out the byte offsets and write the real /ByteRange numbers.
  3. Hash the bytes in the two ranges with SHA-256.
  4. Build the signed attributes, and send them to the token via C_Sign.
  5. Assemble the CMS SignedData with the certificate chain, hex-encode it, and write it into the placeholder.

Nothing outside the placeholder changes after step 3. If it did, the hash would no longer match.

Part 3: incremental updates, and why the auditor's signature survives

This was the requirement I cared most about. Financial statements can arrive already signed by an auditor. If they signed before you, your signature has to go on without breaking theirs.

Look at what the auditor's ByteRange covers: specific bytes of the file as it was when they signed. If any of those bytes move or change, their signature is invalid. A tool that loads a PDF into an object model, adds a signature and serialises the whole thing again will shift offsets, renumber or recompress objects, and break the auditor's signature.

PDF has a mechanism for this, called an incremental update. You don't rewrite anything. You append to the end of the file:

  • the new and changed objects (the signature dictionary, the signature field, its visible appearance, the updated page and form objects);
  • a new cross-reference section listing where those objects are;
  • a new trailer whose /Prev points back to the previous cross-reference section.

A reader follows the chain from the newest trailer backwards and sees the latest version of each object. The original bytes are still there, byte for byte. The auditor's ByteRange still covers exactly what it covered before, and those bytes are untouched. Their signature stays valid. Yours covers the whole file including their signature, which is what you want.

TrackMy Sign has its own PDF parser and incremental writer, written in Rust. The writer only ever appends. It never rewrites the original bytes.

The same mechanism handles the other features that touch a signed file: signing into an existing empty signature field in a bank or government form, putting one signature's visible box on every page, and certifying (locking) a document.

Part 4: testing without a physical token

You can't run CI against my USB token, so the tests use SoftHSM, a software implementation of a PKCS#11 token. To the app it looks like any other token driver: load the library, log in, find the key, sign.

The end-to-end tests generate PDFs, sign them through SoftHSM, and then check the output with two independent validators:

  • pyHanko, a Python PDF signing library, validates the signatures.
  • qpdf checks that the file structure is sound.

Using validators I didn't write matters. My parser agreeing with my writer proves very little. Two unrelated tools agreeing with both proves rather more.

Then the real-world checks, done by hand with my own Hypersecu HyperPKI HYP2003 token: Adobe Acrobat Reader shows "Signature is VALID" on the output.

Part 5: the GST portal's local signer

The GST portal doesn't sign in the browser. Its JavaScript connects to a signer service running locally on your computer, over a secure WebSocket to 127.0.0.1, and asks it to sign. Normally that service is a separate utility you install.

TrackMy Sign can act as that local signer. At a high level:

  • It listens only on 127.0.0.1, only while the app is running, and only answers requests from the GST portal's domain.
  • Because the browser needs a trusted TLS certificate even for localhost, the app sets up a certificate for 127.0.0.1 on that computer.
  • Every request is shown to you. You approve it and enter your PIN; the token signs.

It's an independent implementation of the local protocol the portal's JavaScript uses. A GST portal DSC registration signed this way was accepted by the portal.

What it can't do: MCA21 V3 and the Income Tax portal use eMudhra's emBridge with licensed encryption, so TrackMy Sign can't sign on those. Keep using emBridge there.

Part 6: releases on three platforms

Every release is built by GitHub Actions from the public source, for Windows, macOS and Linux. Nobody builds a release on my laptop, and you can see what each release was built from.

macOS. The Mac app is signed and notarised by Apple. Notarisation means Apple scans the signed app and issues a ticket, so Gatekeeper opens it without the "unidentified developer" dance.

Windows. The installer isn't code-signed yet, so Windows SmartScreen shows a warning the first time you run it. I'd rather tell you that here than have you find out. A Microsoft Store version is in review, and a winget package is waiting for approval.

Linux. AppImage and .deb.

What's in v0.6.0

Beyond signing: a signature designer (company seal or logo, "For Company β€” Director" lines), batch signing with one PIN, a watch folder, optional trusted timestamps and LTV, signature verification with PDF reports and whole-folder checks, password-protected PDFs, page tools (merge, rotate, reorder), signing any file as .p7s or .p7m, a private signing history, a DSC expiry reminder, in-app updates, and English, Hindi and Tamil. It works offline, with no account and no telemetry. The features page has the full list.

It isn't affiliated with Adobe, eMudhra, GSTN, MCA or the CCA.

Try it, break it

I've only tested with my own token. If you have a different one, I'd like to hear whether it works, and especially if it doesn't. Issues and pull requests welcome.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.