A Tor onion service that serves static files over HTTP, built with Arti.
cd garner
cargo install --path .
garner --helpPlace files in a public/ directory and start the server:
mkdir -p public
echo "Hello from Garner via Tor!" > public/index.txt
garner serverGarner connects to the Tor network, publishes a hidden-service descriptor, and prints the .onion URL once it is reachable. It also prints the public key in UR format (ur:signing-public-key/…) so clients can use it with garner get --key. In a separate terminal, fetch a file with the built-in client:
garner get http://<onion-address>.onion/Each run generates a new ephemeral address.
Supply an Ed25519 key to get the same .onion address across restarts.
garner generate keypairThis prints two lines: the private key UR (ur:signing-private-key/…) on line 1 and the public key UR (ur:signing-public-key/…) on line 2. Save them to files:
garner generate keypair | { read -r priv; read -r pub; echo "$priv" > key.ur; echo "$pub" > pubkey.ur; }Keep key.ur secret. You can share pubkey.ur with anyone who needs to connect to your server.
Alternatively, the Gordian Envelope CLI (envelope) can generate key bundles that garner also accepts:
envelope generate prvkeys --signing ed25519 > key.ur
envelope generate pubkeys < key.ur > pubkey.urgarner server --key "$(cat key.ur)"The server derives a deterministic .onion address from the Ed25519 private key. Restarting with the same key produces the same address.
A client with the corresponding public key can connect without knowing the full .onion URL:
garner get --key "$(cat pubkey.ur)" /index.txtThe --key flag derives the .onion host from the public key, so the positional arguments become just paths. You can fetch multiple paths in a single invocation:
garner get --key "$(cat pubkey.ur)" / /index.txtIf you already know the .onion address, use --address instead of --key:
garner get --address <onion-address>.onion /index.txtLike --key, --address lets you pass one or more paths as positional arguments.
You can still use a full URL without --key or --address:
garner get http://<onion-address>.onion/index.txtBoth subcommands read GARNER_KEY as a fallback for --key. The get subcommand also reads GARNER_ADDRESS as a fallback for --address.
export GARNER_KEY="$(cat key.ur)"
garner server # uses GARNER_KEY as private keyexport GARNER_KEY="$(cat pubkey.ur)"
garner get /index.txt # uses GARNER_KEY as public keyexport GARNER_ADDRESS="<onion-address>.onion"
garner get / /index.txt # uses GARNER_ADDRESSMultiple garner processes can run at the same time — for example, a long-running garner server alongside one or more garner get requests, or several parallel fetches. Each invocation creates its own ephemeral Tor state directory, so there is no lock contention between processes. All invocations share a single Tor network cache directory, which is safe for concurrent access. No private key material is ever written to disk — garner uses an in-memory keystore exclusively.
Garner accepts two UR key formats:
| Format | UR type | Produced by |
|---|---|---|
| Key bundle (default) | ur:crypto-prvkeys / ur:crypto-pubkeys |
envelope generate prvkeys / pubkeys |
| Signing key only | ur:signing-private-key / ur:signing-public-key |
direct CBOR construction |
When a key bundle is provided, garner extracts the Ed25519 signing key and ignores the encapsulation key.
The server exposes a fixed set of paths from the document root directory (default public/, configurable with --docroot):
| URL path | File |
|---|---|
/ |
<docroot>/index.html, falling back to <docroot>/index.txt |
/index.html |
<docroot>/index.html |
/index.txt |
<docroot>/index.txt |
A request to / serves index.html if it exists, otherwise index.txt. All other paths return 404. The Content-Type header is set from the file extension (text/html for .html, text/plain for .txt). The server exits immediately if the document root directory does not exist.
garner generate keypair
Generate a random Ed25519 keypair. Prints the private key UR on line 1 and the public key UR on line 2.
garner server [--key <UR>] [--docroot <DIR>]
Start a Tor onion service serving files from the given document root (default public/). Prints the .onion URL and the public key UR to stderr on startup.
| Option | Description |
|---|---|
--key <UR> |
Ed25519 private key in UR format for a deterministic .onion address. Also reads GARNER_KEY env var. |
--docroot <DIR> |
Directory to serve files from. Defaults to public. |
garner get [--key <UR>] [--address <ADDR>] <URL>...
Fetch one or more documents from a .onion address over Tor.
| Option / Arg | Description |
|---|---|
<URL>... |
Full .onion URL(s), or path(s) when --key or --address is set. |
--key <UR> |
Ed25519 public key in UR format to derive the .onion host. Also reads GARNER_KEY env var. |
--address <ADDR> |
.onion address to connect to directly. Also reads GARNER_ADDRESS env var. |
- Tor onion service server (
garner server) that serves static files from a configurable docroot over HTTP. - Tor client (
garner get) that fetches documents from .onion URLs with 120s connect timeout and END MISC workaround. - Ed25519 keypair generation (
garner generate keypair) for deterministic .onion addresses. - UR-encoded key support: accepts
ur:signing-private-key,ur:signing-public-key,ur:crypto-prvkeys, andur:crypto-pubkeysformats. - Deterministic onion addresses via
--keyflag (server) or--key/--addressflags (get). - Ephemeral in-memory Arti keystore with per-invocation temp state dirs for concurrent operation.
- Interactive terminal UI with spinners and elapsed-time counters; structured log output for non-interactive use.
- Common Log Format request logging for served requests.
- Path traversal protection and MIME type detection for served files.
- Fallback from
index.htmltoindex.txtfor root path requests.
Garner is currently in community review. We appreciate your testing and feedback. Comments can be posted to the Gordian Developer Community.
Because this tool is still in community review, it should not be used for production tasks until it has received further testing and auditing.
See Blockchain Commons' Development Phases.
We encourage public contributions through issues and pull requests! Please review CONTRIBUTING.md for details on our development process. All contributions to this repository require a GPG signed Contributor License Agreement.
Garner is a project of Blockchain Commons, a "not-for-profit" social benefit corporation committed to open source & open development. Our work is funded entirely by donations and collaborative partnerships with people like you. Every contribution will be spent on building open tools, technologies, and techniques that sustain and advance blockchain and internet security infrastructure and promote an open web.
To financially support further development of Garner and other projects, please consider becoming a Patron of Blockchain Commons through ongoing monthly patronage as a GitHub Sponsor. You can also support Blockchain Commons with bitcoins at our BTCPay Server.
The best place to talk about Blockchain Commons and its projects is in our GitHub Discussions areas:
- Gordian Developer Community: For developers working with Gordian specifications
- Blockchain Commons Discussions: For general Blockchain Commons topics
The following people directly contributed to this repository:
| Name | Role | Github | GPG Fingerprint | |
|---|---|---|---|---|
| Christopher Allen | Principal Architect | @ChristopherA | ChristopherA@LifeWithAlacrity.com | FDFE 14A5 4ECB 30FC 5D22 74EF F8D3 6C91 3574 05ED |
| Wolf McNally | Contributor | @WolfMcNally | Wolf@WolfMcNally.com | 9436 52EE 3844 1760 C3DC 3536 4B6C 2FCF 8947 80AE |
We want to keep all our software safe for everyone. If you have discovered a security vulnerability, we appreciate your help in disclosing it to us in a responsible manner. Please see our security policy for details.
Garner is licensed under the BSD-2-Clause-Patent license. See LICENSE for details.