If you have ever used Nostr on Android, you have probably encountered Amber, the excellent signer app that keeps your private keys safe while letting other apps request signatures. It is a beautiful thing. Your keys stay locked away, apps ask nicely for what they need, and everyone is happy.
Now look at your Linux desktop. Where is your Amber equivalent? That is what I thought.
So I built one. Welcome to Pleb Signer.
The Problem With Keys Everywhere
The Nostr ecosystem has a key management problem on desktop Linux. Most clients want you to paste your nsec directly into them. Every. Single. Time. This is roughly equivalent to giving your house key to every pizza delivery person and hoping for the best.
What we need is a dedicated signer application that:
- Stores private keys securely in the OS keyring
- Runs quietly in the system tray
- Provides a standard interface for any app to request signatures
- Never, ever exposes the private key to those requesting apps
On Android, NIP-55 solves this using Intents and Content Providers. On the web, NIP-07 uses the browser's window.nostr API. But on Linux desktop? Nothing. Until now.
Enter D-Bus: The Unsung Hero of Linux IPC
If you have used Linux for any length of time, you have interacted with D-Bus whether you knew it or not. It is the inter-process communication system that lets applications talk to each other. Your notification daemon uses it. Your media players use it. Your screen brightness controls use it. It is everywhere, and it is criminally underutilized for Nostr.
D-Bus provides exactly what we need:
- A well-known service name that any application can connect to
- Method calls with typed parameters and return values
- Session bus isolation so your signer only responds to your user session
- A battle-tested protocol that has been running on Linux systems for decades
For Pleb Signer, I registered the service name com.plebsigner.Signer with an object at path /com/plebsigner/Signer. Any Nostr client can now connect to this service and request signatures without ever touching your private key.
The D-Bus Interface
The interface is intentionally simple. Here is what clients can call:
Interface: com.plebsigner.Signer1
Methods:
Version() -> String
IsReady() -> Boolean
GetPublicKey() -> String
ListKeys() -> String
SignEvent(event_json: String, app_id: String) -> String
Nip04Encrypt(plaintext: String, pubkey: String, app_id: String) -> String
Nip04Decrypt(ciphertext: String, pubkey: String, app_id: String) -> String
Nip44Encrypt(plaintext: String, pubkey: String, app_id: String) -> String
Nip44Decrypt(ciphertext: String, pubkey: String, app_id: String) -> String
The response format is JSON with a consistent structure:
{
"success": true,
"id": "req_abc123",
"result": "<the data you asked for>",
"error": null
}
On failure, success is false and error contains a human-readable message. No exceptions thrown across process boundaries, no cryptic error codes. Just straightforward JSON.
Implementation Details
The D-Bus service is implemented using the zbus crate, which provides a clean Rust interface for D-Bus operations. Here is the core of how it works:
#[interface(name = "com.plebsigner.Signer1")]
impl SignerInterface {
async fn get_public_key(&self) -> String {
let id = Self::generate_request_id();
if let Err(e) = self.check_ready().await {
return DbusResponse::error(id, e);
}
match self.signing_engine.get_public_key().await {
Ok(result) => DbusResponse::success(id, result),
Err(e) => DbusResponse::error(id, e),
}
}
async fn sign_event(&self, event_json: &str, _app_id: &str) -> String {
// Parse the unsigned event, sign it, return the signature
// The private key never leaves this process
}
}
The service starts when Pleb Signer launches and registers itself on the session bus. From that point forward, any application can call these methods using standard D-Bus tooling.
You can even test it from the command line:
dbus-send --session --print-reply --dest=com.plebsigner.Signer \
/com/plebsigner/Signer com.plebsigner.Signer1.GetPublicKey
If you get back a JSON response with your public key, everything is working.
Key Storage: Using What the OS Already Provides
Rather than rolling my own encrypted key storage (a terrible idea for anyone not specifically employed to do cryptography), Pleb Signer uses the nostr-keyring crate which integrates with the Linux Secret Service API. This is the same API that GNOME Keyring and KWallet implement.
When you add a key to Pleb Signer, it gets stored in your existing keyring alongside your WiFi passwords and SSH keys. The keyring handles encryption, unlocking, and all the other security details that I am not qualified to implement correctly.
The System Tray: Because Good Apps Stay Out of Your Way
A signer needs to be running all the time, but it should not demand attention. Pleb Signer lives in your system tray using the StatusNotifierItem protocol (via the ksni crate), which works on KDE Plasma, GNOME with extensions, and modern desktops like Cosmic.
The tray icon provides quick access to:
- Current status (locked or ready)
- Opening the main window
- Quitting the application
For the icon itself, I embedded a small golden key rendered as raw ARGB pixel data. No external icon files to lose, no theme compatibility issues. Just 484 bytes of hardcoded pixels that look like a key.
Bunker Mode: Because Sometimes You Need Remote Signing
What if you want to sign events from your phone while your keys stay safely on your desktop? That is where NIP-46 comes in, and Pleb Signer supports it through what I call Bunker Mode.
When enabled, Pleb Signer generates a bunker:// URI containing your public key and relay information. You paste this URI into a remote client, and that client can then send signing requests through Nostr relays. The requests arrive at your desktop, get signed locally, and the responses go back through the relays.
Your private key never leaves your machine. The remote client only sees signatures, never secrets.
The Architecture
The application has three main components running simultaneously:
- The system tray, which runs in a dedicated thread using ksni
- The D-Bus service, which runs on a Tokio runtime handling async requests
- The GUI, which spawns as a separate subprocess using iced
The subprocess approach for the GUI deserves explanation. The iced framework uses winit for window management, and winit has a limitation: you cannot recreate the event loop in the same process. If the user closes the window and later wants to reopen it from the tray, we need a fresh process. So each time the window opens, it spawns a new subprocess that loads the configuration and key metadata, displays the UI, and exits cleanly when closed.
The main process stays running, keeping the tray icon active and the D-Bus service available.
What Nostr Clients Need to Integrate
For client developers wanting to add Pleb Signer support, here is the minimal integration:
- Connect to the session D-Bus
- Check if
com.plebsigner.Signeris available - Call
IsReady()to verify the signer is unlocked - Call
GetPublicKey()to identify the user - Call
SignEvent()whenever you need a signature
In Python with pydbus:
from pydbus import SessionBus
bus = SessionBus()
signer = bus.get("com.plebsigner.Signer", "/com/plebsigner/Signer")
if signer.IsReady():
pubkey_response = signer.GetPublicKey()
# Parse JSON, use the pubkey
In Rust with zbus, you can use the proxy macro to generate type-safe bindings. The CLIENT_INTEGRATION.md file in the repository has complete examples in Python, Rust, and JavaScript.
What This Means for Linux Nostr Users
You can now use multiple Nostr clients on your Linux desktop without pasting your nsec into each one. Set up Pleb Signer once, import or generate your key, and any compatible client can request signatures through D-Bus.
Your key is stored in your OS keyring, protected by whatever authentication your system uses. The D-Bus service only runs in your user session. Remote clients can use Bunker mode without ever touching your private key.
It is not NIP-55 because that specification is Android-specific. But it accomplishes the same goals using the appropriate Linux primitives. Perhaps someday there will be a NIP specifically for D-Bus signers, and Pleb Signer can serve as a reference implementation.
Try It Out
The code is available and builds with a simple cargo build --release. You need a system with D-Bus (so, basically any Linux desktop), a Secret Service provider like GNOME Keyring, and a system tray that supports StatusNotifierItem.
Generate a key, try the D-Bus commands from your terminal, and see if your favorite Nostr client can be modified to use it. The signer is ready. Now we just need the clients to catch up.
Pleb Signer is open source and welcomes contributions. If you build something with it, I would love to hear about it.