11 releases (5 breaking)
| 0.6.0 | May 4, 2026 |
|---|---|
| 0.5.1 | Apr 29, 2026 |
| 0.4.0 | Apr 27, 2026 |
| 0.3.2 | Apr 21, 2026 |
| 0.1.2 | Apr 16, 2026 |
#365 in Cryptography
465KB
10K
SLoC
tumpa-cli
A command-line tool for OpenPGP operations, SSH agent, and password management, backed by the tumpa keystore.
At a glance, tumpa-cli works as a drop-in for:
- Git — commit and tag signing + verification via
gpg.program - pass — password-store replacement,
either through a native
tpassbinary or by pointingpassattclig - SSH — OpenSSH-compatible agent serving keystore keys and OpenPGP card auth subkeys
- Browserpass — Browserpass
browser extension via the
gpg2→tcligsymlink - PassFF — PassFF Firefox extension,
same
gpg2→tcligpath - OpenPGP smart cards — cards are tried first for sign / decrypt / SSH
auth, with a software-key fallback from
~/.tumpa/keys.db - multiverse/bump-tag — emits
the
[GNUPG:]status lines bump-tag greps for
Three binaries are provided:
tcli-- human-facing key management and SSH agent: import, export, search, fetch, describe, list, delete, card status, agent daemons, signing and verification.tclig-- GnuPG drop-in for programs that invokegpg(git signing,pass, anything with agpg.programhook)tpass-- drop-in replacement for password-store (pass), calling the tumpa keystore directly without GPG
All 3 try hardware OpenPGP cards first, then fall back to software keys
stored in ~/.tumpa/keys.db.
For detailed usage instructions, see the Usage Guide.
Features
- Git commit and tag signing -- use as
gpg.programin git config - Signature verification -- verify commits and tags with keys from the tumpa keystore
- Encryption / decryption -- multi-recipient encryption, card-first decryption with software fallback
- password-store (
pass) support -- works as a drop-in GPG replacement forpass - Browser extension support -- PassFF and Browserpass work via the
gpg2 -> tcligsymlink (ciphertext on stdin via-,--debugflag accepted) tpass-- native password-store replacement, no GPG dependency- OpenPGP card support -- cards are tried first for signing, decryption, and SSH auth. Tested against Yubikey 4/5 (RSA 2048/4096, NIST P-256/P-384; Curve25519 on firmware ≥ 5.2.3) and Nitrokey 3 (RSA and
Cv25519Modern— Ed25519 + X25519 per RFC 9580). Nitrokey rejects legacyCv25519(EdDSALegacy + ECDH/Curve25519) along withEd448andX448keys at upload time - Unified agent -- caches passphrases for GPG operations + optional SSH agent
- Key management -- import, export, search, delete, and fetch keys via WKD
- SSH agent -- serve authentication subkeys from the keystore and connected cards
- Passphrase handling -- agent cache, pinentry,
TUMPA_PASSPHRASEenv var, or terminal prompt - Compatible with multiverse/bump-tag -- produces the
[GNUPG:]status lines git expects
Quickstart
For the common case — a hardware OpenPGP card holding the signing
key, with just the public key imported into the keystore so
tcli knows which card-backed key to use.
1. Install
# macOS (Homebrew)
brew tap tumpaproject/tumpa-cli
brew install tumpa-cli
# Linux / from source
cargo install tumpa-cli
Linux also needs PC/SC libraries for card support — see System dependencies below.
2. Import the public key matching your card
tcli import my-public-key.asc
The secret bits live on the card; the keystore only needs the public
key so tcli can resolve the fingerprint and route signing /
decryption to the card.
3. Verify the key and the card
tcli describe <FINGERPRINT> # details for the imported key
tcli card status # connected cards, serial, PIN retries
4. Configure git
git config --global gpg.program tclig
git config --global user.signingkey <FINGERPRINT>
git config --global commit.gpgsign true
tclig (not tcli) is the GPG drop-in that git talks to.
5. Start the agent at login
macOS (Homebrew):
setup-tumpa-agent
This installs ~/Library/LaunchAgents/in.kushaldas.tumpa.agent.plist
into the Aqua GUI session and bootstraps it. It also stops and removes
any stale homebrew.mxcl.tumpa-cli.plist left over from earlier
versions that suggested brew services.
Do not run brew services start tumpa-cli. Homebrew's service
mechanism loads agents into the Background launchd session, which
cannot reach WindowServer; pinentry-mac will fail to draw its
dialog and signing prompts will appear "unavailable". The
setup-tumpa-agent helper exists specifically to install the agent
into the right session.
Then add to ~/.zshrc so SSH clients find the agent:
export SSH_AUTH_SOCK="$HOME/.tumpa/tcli-ssh.sock"
Linux (systemd user service) — see
contrib/systemd/README.md for the
ready-made unit files. The one-liner for the common case is:
systemctl --user enable --now tumpa-agent.service
Point your shell at the SSH socket via an environment.d drop-in
(systemd reads this into every user session):
mkdir -p ~/.config/environment.d
echo 'SSH_AUTH_SOCK=${XDG_RUNTIME_DIR}/tcli-ssh.sock' \
> ~/.config/environment.d/tumpa-ssh-agent.conf
Installation
From source
cargo install tumpa-cli
Three binaries are installed to ~/.cargo/bin/: tcli, tclig, and tpass.
Homebrew
brew tap tumpaproject/tumpa-cli
brew install tumpa-cli
System dependencies
On Linux, card support requires PC/SC libraries:
- Debian/Ubuntu:
sudo apt install pkg-config libpcsclite-dev pcscd - Fedora/RHEL:
sudo dnf install pkg-config pcsc-lite-devel pcsc-lite - Arch:
sudo pacman -S pkg-config pcsclite
The pcscd service must be running: sudo systemctl start pcscd.socket
On macOS, the PC/SC framework is built in -- no extra packages needed.
Setup
Import your key
The first step is importing your OpenPGP key. The keystore directory
(~/.tumpa/) and database are created automatically on first use:
tcli import my-secret-key.asc
Already have keys in GnuPG? Import the whole keyring in one shot — both process substitution and stdin work, and multi-key streams are split into individual keys:
tcli import <(gpg --export) # all public keys from gpg
gpg --export | tcli import - # same via a pipe
gpg --export-secret-keys | tcli import -
Verify it was imported:
tcli list
If you don't have a key yet, generate one with wecanencrypt, GnuPG, or the tumpa desktop application, then import it.
Shell completions
# Bash
tcli completions bash > ~/.local/share/bash-completion/completions/tcli
tpass --completions bash > ~/.local/share/bash-completion/completions/tpass
# Zsh
mkdir -p ~/.zfunc
tcli completions zsh > ~/.zfunc/_tcli
tpass --completions zsh > ~/.zfunc/_tpass
# Add to .zshrc:
# fpath=(~/.zfunc $fpath)
# autoload -Uz compinit
# compinit
# Fish
tcli completions fish > ~/.config/fish/completions/tcli.fish
tpass --completions fish > ~/.config/fish/completions/tpass.fish
Git
Configure git to use tclig for signing (it's the GPG drop-in; tcli
is the human-facing key manager and doesn't accept gpg flags):
git config --global gpg.program tclig
git config --global user.signingkey <FINGERPRINT>
git config --global commit.gpgsign true
tpass (recommended)
Use tpass directly -- no GPG symlinking needed:
tpass init <FINGERPRINT>
tpass insert email/work
tpass show email/work
tpass -c email/work # copy to clipboard
tpass generate sites/github 20
tpass edit email/work
tpass grep admin
tpass is fully compatible with pass -- they share the same store
format (~/.password-store/). See the
Usage Guide
for full documentation.
password-store (pass) via tclig
Alternatively, use the original pass with tclig as the GPG backend:
mkdir -p ~/bin
ln -s $(which tclig) ~/bin/gpg2
export PATH="$HOME/bin:$PATH"
pass init <FINGERPRINT>
Finding your key fingerprint
tcli list
Keys are managed through the tumpa
desktop application and stored in ~/.tumpa/keys.db.
Optional: run the agent at login
On Linux, ship-ready systemd user units are in
contrib/systemd/. Pick one (combined GPG+SSH,
GPG-only, or SSH-only) — they are mutually exclusive via
Conflicts=, so systemctl --user enable --now tumpa-agent.service
is the one-liner for the common case. Full walkthrough and the
SSH_AUTH_SOCK wiring are covered under
Agent → Starting the agent automatically
below.
Usage
Signing
tclig is normally invoked by git, not directly. When git runs
gpg.program --detach-sign ..., tclig handles it transparently.
If a hardware OpenPGP card is connected and holds the signing key,
tclig uses the card. Otherwise it uses the software key from the
tumpa keystore.
Verification
Git invokes tclig --verify <sigfile> - automatically for commands
like git verify-commit, git verify-tag, and git log --show-signature.
Signing and verifying files with tcli
For ad-hoc signing of arbitrary files at the shell, tcli exposes
three human-friendly commands. Pick a key by fingerprint, key ID, or
email:
# Detached signature (default = ASCII armored, sibling .asc).
tcli sign report.pdf --signer alice@example.com
# -> writes report.pdf.asc
# Binary detached (.sig instead of .asc).
tcli sign report.pdf --signer alice@example.com --binary
# Inline cleartext (-----BEGIN PGP SIGNED MESSAGE-----, software keys only).
tcli sign-inline notice.txt --signer alice@example.com
# Verify a detached signature against the keystore.
tcli verify report.pdf --signature report.pdf.asc
# Verify a cleartext-signed message in place.
tcli verify notice.txt.asc
# Verify against an external public-key file (skips the keystore).
tcli verify report.pdf --signature report.pdf.asc --key-file alice.pub
-o/--output overrides the destination; - reads stdin or writes
stdout. Verify exit codes: 0 good, 1 bad, 2 unknown signer.
See docs/usage.md → Signing and verifying with tcli
for the full surface (email-ambiguity handling, card vs software
backend, parse-time validation, limitations).
Encryption
echo "secret" | tclig -e -r <FINGERPRINT> -o output.gpg
tclig -e -r <FP1> -r <FP2> -o output.gpg input.txt
Multiple recipients are supported. Any recipient can decrypt.
Decryption
tclig -d output.gpg # decrypt to stdout
tclig -d -o plaintext.txt file.gpg # decrypt to file
The secret key is auto-detected from the encrypted message.
Decrypted output files are created with 0600 permissions.
Passphrase / PIN entry
Both tcli and tclig acquire passphrases in this order:
- Agent cache -- if
tcli agentis running, cached passphrases are returned without prompting TUMPA_PASSPHRASEenvironment variable -- useful for scripting and CIpinentryprogram -- same mechanism GnuPG uses- Terminal prompt -- fallback
For card operations, the PIN is requested the same way.
Key management
tcli import mykey.asc # import from file
tcli import /path/to/keys/ -r # import from directory (recursive)
tcli import <(gpg --export) # migrate from gpg in one go
gpg --export | tcli import - # or read keyring bytes from stdin
tcli export <FP> -o key.asc # export (armored is the default)
tcli describe <FP> # detailed info for a keystore key
tcli describe mykey.asc # detailed info for a key file (no import)
tcli search "Kushal" # search by name
tcli search user@example.com --email # search by email (exact match)
tcli fetch user@example.com # fetch via WKD
tcli fetch user@example.com --dry-run # preview without importing
tcli delete <FP> # delete a key
Listing keys
tcli list # human-readable
tclig --list-keys --with-colons # GnuPG colon format (used by pass)
tclig --list-secret-keys --with-colons
Smart card status
tcli card status # status of the connected card
tcli card list # all connected cards (ident + serial)
Shows details of connected OpenPGP cards (manufacturer, serial,
key fingerprints, PIN retry counters), similar to gpg --card-status.
Custom keystore path
tcli --keystore /path/to/keys.db list
Or set TUMPA_KEYSTORE environment variable.
Agent
tcli agent runs a daemon that caches passphrases for GPG operations
(signing, decryption) and optionally serves as an SSH agent.
GPG passphrase caching
tcli agent
This eliminates repeated passphrase prompts. When git calls tcli for
signing, or tpass decrypts a password, the agent provides the cached
passphrase instead of prompting again. The cache expires after 30
minutes by default.
GPG + SSH agent
tcli agent --ssh
tcli agent --ssh -H unix:///tmp/tcli.sock # custom SSH socket
tcli agent --cache-ttl 3600 # custom TTL (1 hour)
Starting the agent automatically
macOS (Homebrew)
setup-tumpa-agent
Add to ~/.zshrc:
export SSH_AUTH_SOCK="$HOME/.tumpa/tcli-ssh.sock"
setup-tumpa-agent is shipped by the tumpa-cli formula. It
installs an Aqua-session LaunchAgent at
~/Library/LaunchAgents/in.kushaldas.tumpa.agent.plist,
bootstraps it via launchctl bootstrap gui/$(id -u), and
verifies the result. Run it again after a brew upgrade to
pick up plist changes (the script is idempotent — it boots out
the prior instance and re-bootstraps).
To check status, restart, or remove, the tumpa-cli repo's
justfile exposes recipes:
just mac-agent-status # show launchd state
just mac-agent-kickstart # force-restart, bypass spawn throttle
just mac-agent-uninstall # bootout + rm plist
Without those recipes, equivalent launchctl invocations are:
launchctl print gui/$(id -u)/in.kushaldas.tumpa.agent
launchctl bootout gui/$(id -u)/in.kushaldas.tumpa.agent
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/in.kushaldas.tumpa.agent.plist
Why not brew services start tumpa-cli? Homebrew's service
mechanism cannot emit LimitLoadToSessionType=Aqua, so it loads
the agent into the Background launchd session — where
pinentry-mac cannot reach WindowServer to draw its dialog.
Symptoms: passphrase / smartcard PIN prompts hang or return
"unavailable". The setup-tumpa-agent helper is the supported
path on macOS; brew services is not.
Linux (systemd user service) -- copy the ready-made units from
contrib/systemd/ and enable one:
mkdir -p ~/.config/systemd/user
cp contrib/systemd/tumpa-*.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now tumpa-agent.service # GPG + SSH
# or
systemctl --user enable --now tumpa-gpg-agent.service # GPG only
# or
systemctl --user enable --now tumpa-ssh-agent.service # SSH only
Point your shell at the SSH socket via an
environment.d drop-in (systemd reads this into every user session):
mkdir -p ~/.config/environment.d
echo 'SSH_AUTH_SOCK=${XDG_RUNTIME_DIR}/tcli-ssh.sock' \
> ~/.config/environment.d/tumpa-ssh-agent.conf
See contrib/systemd/README.md for
customisation (binary path, environment overrides, troubleshooting)
and docs/adr/0004-systemd-user-service.md
for the design rationale.
Without agent
Everything works without the agent -- you just get prompted every time. The agent is purely additive.
Querying socket paths
tcli socket # GPG agent socket (~/.tumpa/agent.sock)
tcli socket ssh # SSH agent socket (/run/user/<UID>/tcli-ssh.sock)
Useful for scripting:
export SSH_AUTH_SOCK=$(tcli socket ssh)
How it works
- Passphrases are cached in memory with a configurable TTL (default 30 min)
- SSH authentication keys from the keystore and connected cards are served
- Ed25519, ECDSA (P-256, P-384, P-521), and RSA keys are supported
- Card-based keys are listed if the card's auth key fingerprint is in the keystore
- The agent socket (
~/.tumpa/agent.sock) is created with 0600 permissions
Supported key types
GPG operations (signing, verification, encryption, decryption)
All cipher suites supported by the wecanencrypt library work:
| Cipher suite | Signing algorithm | Notes |
|---|---|---|
| Cv25519 (default) | EdDSA (Ed25519) | Legacy v4 format, widely compatible |
| Cv25519Modern | Ed25519 | RFC 9580 native format |
| RSA 2048 | RSA | |
| RSA 4096 | RSA | |
| NIST P-256 | ECDSA | |
| NIST P-384 | ECDSA | |
| NIST P-521 | ECDSA |
When a hardware OpenPGP card is connected and holds the signing key, the card performs the operation regardless of algorithm.
SSH agent
The agent serves authentication subkeys as SSH identities:
| OpenPGP algorithm | SSH key type | Software signing |
|---|---|---|
| Ed25519 | ssh-ed25519 |
Supported |
| ECDSA P-256 | ecdsa-sha2-nistp256 |
Supported |
| ECDSA P-384 | ecdsa-sha2-nistp384 |
Supported |
| ECDSA P-521 | ecdsa-sha2-nistp521 |
Supported |
| RSA 2048/4096 | ssh-rsa |
Supported |
Card-based SSH authentication works for all algorithms the card hardware supports.
See ADR 0001 for the full architectural rationale and algorithm details.
How it works with bump-tag
bump-tag verifies that all commits and tags are signed before creating a new signed tag. It relies on:
git pull --verify-signaturesgit verify-tag/git verify-commitgit log --pretty="format:%G?"expectingGfor every commitgit tag -sfor creating the new tag
tcli produces all the [GNUPG:] status lines these commands require
(SIG_CREATED, GOODSIG, VALIDSIG, TRUST_FULLY), so bump-tag
works without changes.
Testing
just test-all
Or run individual test suites:
just test # tpass integration tests (33+ tests)
just test-compat # tpass <-> pass cross-compatibility
just test-pass # tcli + pass integration
just test-keystore # key management (import/export/info/delete/search/fetch)
All tests require TUMPA_PASSPHRASE set and a secret key in ~/.tumpa/keys.db.
License
GPL-3.0-or-later
Dependencies
~58–81MB
~1M SLoC