Every time a business registers a DSC or files a return with it, the GST
portal asks a program on that person’s own computer to do the signing. The page
opens a WebSocket to 127.0.0.1, and whatever answers there gets to sign on
their behalf.
A friend who files GST for their business asked me for help with exactly that program, on a Mac. There is no Mac native way to do it. The usual path is a Java signer plus a vendor driver held together with workarounds, and when that fails, a Windows virtual machine. So I built a native one. It is called Swakshar, it is Apache 2.0, and its one rule is that nothing gets signed until the person signing has seen what it is and pressed Sign.
This is what the portal actually asks for, how the app stays out of the way of anything that is not you, the bugs that four releases in one evening shook out, and what it was like to build with an AI agent doing the typing.
What the portal asks for
Nothing below comes from vendor software. It is what any browser can see: the portal’s own JavaScript, frames captured from real sessions, and ASN.1 dumps of signatures a real token produced. The full write up is PROTOCOL.md.
The page tries five ports in a fixed order, 1585, 2095, 2568, 2868 and
4587, over TLS only. The signer speaks first:
status = success
port = 1585
version = 2.8
ID = gstnInfy
If version is not the one the portal expects, you get “Please install and use
correct version of emSigner” and nothing else. So the version is a setting, not
a constant.
Then the page sends one request:
action=sign
tobesigned=ABCDE1234F
panNo=ABCDE1234F
signtype=1
The reply is a detached CMS signature over that text, made with the certificate whose PAN matches. That is the whole conversation.
The important part is not the format. It is that the page trusts whoever answers on that port. A signer that signs whatever arrives is a signer that signs for any page that can reach it.
What Swakshar does about that
- It asks, every time. A window shows which site is asking, what will be signed, and which certificate matches the PAN, then waits.
- The PIN is typed every time and never kept. It goes from the window to the signing code, unlocks the token once, and is wiped. Not on disk, not in the Keychain, not in a log. The chip on the token checks it, not Swakshar, and the window warns you before the last try that would lock the token.
- It only answers GST pages, only on
127.0.0.1. The handshake checksOriginandHostbefore the WebSocket even opens. - Signing starts off. A fresh install listens on nothing until you turn it on, and it remembers what you chose.
- The local certificate cannot vouch for anything else. It is valid for
127.0.0.1andlocalhostonly, and the key of the CA that signed it is thrown away the moment it is made. - No telemetry. The only request it makes to the internet is the update check, and that can be turned off.

How it is built
It is a real Mac app: signed and notarized by Apple, one build for Apple silicon and Intel, living in the menu bar and updating itself after asking. Underneath is a Rust workspace behind a Tauri 2 shell, with a React window on top. Six crates, each doing one job: the frames, the CMS signature, the token, the local certificate, the WebSocket server, and a command line tool that can do everything the app does from a terminal.
The token is reached through PKCS#11 on a thread of its own, so a slow driver never freezes the window. Before any driver is installed, Swakshar reads the USB bus directly and can already say “this is a HYP2003, it needs this driver”.
The signatures are checked byte for byte against a reference implementation, using a throwaway test identity. Nothing real is in the repository: no names, no PANs, no token serials.
The house rules are strict on purpose, because this code holds the key to legally binding signatures:
- Tests first. A change lands with the test that would have caught it. There are 81 Rust tests, 46 for the interface and 13 for the release scripts. The Rust ones run on macOS, Windows and Linux on every push.
- No file over 300 lines, no
unwrapoutside tests, no lint suppressions. - No dependency younger than seven days, and every GitHub Action pinned to a commit, not a tag.
The token that would not cooperate
My friend’s token is a HYP2003, which turns out to be a rebadged Feitian ePass2003. The obvious plan was to bundle OpenSC so nobody has to install a vendor driver. OpenSC recognises the chip and then gives up on the layout a certifying authority writes to it:
PKCS#15 binding failed: Unsupported card
So for now Swakshar recognises the token, names the driver it needs, walks you through installing it, and picks it up the moment it appears. A native driver is on the list, but only once there is documentation or permission from the vendor. I will not get there by decompiling theirs.

Four releases in one evening
The first release went out on the evening of 27 September. A little after midnight there were four, and every one of them fixed something real.
Before 0.1.0: the disk image Gatekeeper refused. Tauri signs and notarizes the app, then wraps it in a disk image that is neither. macOS checks the image first, so a perfectly notarized app arrived inside something Gatekeeper rejected. The release workflow now notarizes and staples the image too.
0.1.1: no Dock icon. A menu bar app sets macOS’s accessory activation policy, which removes the Dock icon, the Command-Tab entry and the app’s own menu bar. That is right while the app sits in the menu bar and wrong while its window is open. Swakshar now becomes a regular app just before a window opens and goes back to the menu bar when the last one closes. Settings can keep it in the Dock for people who prefer that.

0.1.2: Restart did not restart. The update installed, I pressed Restart,
and nothing came back. The Restart button ran on the main thread, where
Tauri’s restart() relaunches immediately and skips the exit event. The
single instance lock is only cleaned up in that exit event, so the new copy
found the old one still alive, handed itself over to it, and quit. Then the
old one exited too.
The fix is request_restart(), which exits properly first. The catch is that
an update is installed by the version you already have, so the fix could only
be proven by the next update. It was.
0.1.2, again: the update links. The manifest the updater reads pointed at GitHub API links. Those work, but anonymous API calls are limited to 60 an hour per network address, so an office full of Macs updating together could be turned away. The release now rewrites them to plain download links, and fails if any link points at a file the release does not have.
0.1.3: quitting that dropped the answer. ⌘Q and Quit in the Dock go through macOS directly and skipped Swakshar’s own shutdown. Looking closer, even the menu bar’s Quit killed open connections before a waiting page got its “signing canceled”. Now every way out ends in one shutdown: the port closes, a waiting request is answered, idle pages get a proper close frame, and anything still open after a second is cut off. The same release stopped the update card from breaking release notes mid-sentence.
A test caught one more case while I wrote that. If a request had just
arrived when the stop came, the page got a close frame and no answer. The fix
is one word, biased, so frames already waiting are read before the stop is:
let next = tokio::select! {
biased;
next = timeout(IDLE_TIMEOUT, stream.next()) => next,
() = stop.stopped() => {
sink.close().await?;
return Ok(());
}
};
And one thing that was not a bug. Right after publishing 0.1.3, Check now said the app was up to date. GitHub’s “latest release” link takes a minute or two to move after a release is published. A minute later it found 0.1.3.
Built with an AI agent
Almost none of Swakshar’s code was typed by me. It was written by Claude Code, running in Conductor, while I decided what it should do, set the rules, and checked every release by hand with a real token on the real portal.
That split shaped the repository. The rules in AGENTS.md exist for the agent
as much as for any person: tests first, one small commit per change, 300 lines
per file, no lint suppressions, no dependency younger than a week. An agent
that has to write the failing test before the fix leaves a trail that is easy
to check, and the stop race above was caught exactly that way.
It was good at the slow, careful parts. It found why Restart did not restart by reading Tauri’s own source rather than guessing, and it tried the update link rewrite against the real 0.1.1 manifest before trusting it. It also kept the provenance clean: the protocol came only from public sources, and the native driver waits for documentation or permission rather than a decompiler.
It could not do the parts that need hands. It never ran the app, never touched the token and never saw the portal. The missing Dock icon, the Restart that did not restart and the release notes broken mid-sentence were all found by using the app, and no release shipped until someone had.
What is next
A native driver for the ePass2003 family, if the vendor agrees. A sandboxed build for the Mac App Store, which means signing through macOS’s own smart card support instead of PKCS#11. Signed Windows builds.
The code, the releases and every decision with its reasoning are at github.com/swakshar-app/swakshar. If it does not work with your token, the issue forms ask for exactly what I need to fix it.
Swakshar is unofficial. It is not affiliated with GSTN, the GST Council, Infosys, eMudhra or any certifying authority. It signs only with your own DSC, only after you approve, and what you sign is your responsibility.
