ArchivioTOR
Ed25519 post signing for WordPress. Keys never leave your browser. Ephemeral signing, Strict Containment Mode for Tor Browser Safest level, and onion-aware rate limiting. Built for journalists, activists, and anonymous publishers.
by ArchivioTOR Project · github.com/mountainviewprovisions/archiviotor · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/mountainviewprovisions/archiviotor/archive/refs/heads/main.zipArchivioTOR — Ed25519 Post Signing for WordPress
ArchivioTOR is a privacy-first cryptographic signing plugin for WordPress. Authors sign posts with Ed25519 keys generated and held entirely in the browser — the private key never touches the server, not even once. Signatures prove that post content has not been altered since signing and can be verified independently by any reader using standard tools, no WordPress required.
Designed for deployment on Tor hidden services: no external dependencies, bundled noble-ed25519 with integrity verification, Onion Mode rate limiting that works correctly when all clients arrive from 127.0.0.1, WordPress identity signal suppression (generator tag, version strings, pingback, author enumeration, REST user endpoint, oEmbed), and Strict Containment Mode for fully server-rendered badges at Tor Browser Safest level with JavaScript disabled. Ephemeral signing generates a one-time keypair per post and discards the private key immediately signatures prove integrity without linking posts to a persistent identity.
Contents
- What It Does
- What It Does Not Do
- Requirements
- Installation
- Quick Start
- Signing Posts
- Verifying Signatures
- Ephemeral Keys
- Offline Signing
- Tor Deployment
- Settings Reference
- Canonical Message Format
- REST API Reference
- Troubleshooting
- Uninstalling
- Security Notes
- Changelog
- License
What It Does
ArchivioTOR attaches a cryptographic proof to each WordPress post you sign. The proof lets any reader confirm two things independently, without special software and without contacting any external service:
- Integrity — the post content has not been altered since it was signed.
- Continuity — this post was signed by the same key holder as other posts sharing that public key.
Signing happens entirely in your browser. The server stores only the resulting public key and signature — never the private key, never intermediate values. The signature covers a deterministic canonical representation of the post: its ID, title, slug, stripped body text, and GMT publication date. Any change to any of those fields invalidates the signature and triggers a stale warning on the verification badge.
What It Does Not Do
Understanding the limits is as important as understanding the capabilities.
ArchivioTOR does not provide anonymity or network privacy. It is a content-integrity tool. It does not route traffic through Tor, does not hide who visits your site, and does not protect against traffic analysis. For network-level protection, deploy your site as a Tor hidden service — that is outside this plugin's scope.
ArchivioTOR does not protect against a fully compromised server. If an adversary controls this server, they can serve a manipulated canonical message and your browser will sign it without any visible indication that something is wrong. The standard signing workflow inherently trusts the server for message construction. See Offline Signing for how to remove that dependency for high-risk situations.
A signature does not prove when you published. It proves that the key holder signed that exact content at some point, not when. For cryptographic timestamping, submit a hash of your post to a public transparency log at publication time.
A signature does not prove your personal identity. It proves that whoever holds your private key approved that text. Identity depends on anchoring your public key fingerprint to independent out-of-band channels — a DNS TXT record, a Nostr profile, a Keybase proof, a second server you control.
Requirements
| Component | Minimum | Recommended |
|---|---|---|
| WordPress | 5.8 | 6.4+ |
| PHP | 7.4 | 8.1+ |
| PHP ext-sodium | Optional | Recommended |
| Browser | Chrome 67+ / Firefox 57+ | Any modern browser |
| Tor Browser | 7.5+ | Latest stable |
PHP ext-sodium is required only for server-side signature verification. Client-side verification (the default) works without it.
Installation
- Download the plugin
.zipfile. - In WordPress admin go to Plugins → Add New Plugin → Upload Plugin.
- Upload the
.zip, click Install Now, then Activate. - Alternatively, unzip and upload the
archiviotor/folder towp-content/plugins/via FTP or SSH. - Navigate to ArchivioTOR in the left sidebar.
On activation, Onion Mode and WordPress identity signal suppression are enabled automatically for new installations. See Settings Reference for what these do. Existing installations that have already saved settings are not affected.
Quick Start
- Go to ArchivioTOR → Key Generator and click ⚡ Generate New Key Pair.
- Export a backup immediately: 📤 Export Key File → save the
.jsonto an encrypted password manager or offline vault. If you skip this and clear browser data, the key is gone permanently. - Go to ArchivioTOR → Sign Posts, load your key, select posts, click Sign Selected.
- Go to ArchivioTOR → Verify Posts and click 🔍 Verify (Client) to confirm signatures are valid.
- Publish your public key fingerprint in at least two independent places so readers can confirm it has not been substituted.
Signing Posts
Standard workflow
Go to ArchivioTOR → Sign Posts.
- Load your key: 🔄 Load from Storage if you saved it to this browser, or 📂 Import Key File to load from a
.jsonexport. - Your active public key and fingerprint appear in the Active Key panel.
- Select posts by checkbox, or use Select all.
- Click Sign Selected, or click Sign on an individual row.
What happens:
- Your browser fetches the post's canonical message from the server.
- Ed25519 signing is performed locally using your private key.
- The browser sends only the public key and signature to the server.
- The server stores them in post meta (
_archiviotor_signature,_archiviotor_pubkey).
Re-sign a post after editing its title, slug, or body content. The existing signature covers the content at signing time — any change invalidates it and triggers a stale warning.
Server-trust caveat
The standard workflow fetches the canonical message from this server before signing it. A compromised server could serve a manipulated message, and your signature would cover content you did not intend to approve with no visible indication. For situations where the server itself is inside your threat model, use Offline Signing instead.
Verifying Signatures
From the admin panel
Go to ArchivioTOR → Verify Posts and click 🔍 Verify (Client) on any row. The browser fetches the canonical message and verifies the signature locally — no private key required, no external network contact. This works over Tor and on .onion addresses.
If PHP ext-sodium is available on the server, a 🖥 Verify (Server) button also appears. Client-side verification is preferred when using Tor.
From the front end
When the signature badge is enabled in Settings, a badge appears on each signed post. Any reader — without an account or any special software — can click it to verify the signature in their own browser.
Independent verification
Because the canonical message format is documented and the signing library is standard Ed25519, any reader can verify a signature entirely independently using command-line tools. See Canonical Message Format.
Ephemeral Keys
An ephemeral key is a one-time key pair generated fresh for a single post and immediately discarded. The private key exists in browser memory only for the milliseconds needed to sign and is never saved anywhere.
Use ephemeral keys when:
- You want to sign posts without linking them to a persistent author identity.
- You are publishing anonymously or pseudonymously.
- You want readers to be able to verify that content is unchanged, without establishing authorship continuity across posts.
How to use them:
- Single post: click the Ephemeral button on any row in the Sign Posts table.
- Bulk: check Ephemeral keys in the bulk-select bar before clicking Sign Selected.
The trade-off: Ephemeral signatures prove a post has not been altered. They do not prove authorship by a persistent identity, and two posts signed with different ephemeral keys cannot be linked to each other — this is intentional.
Offline Signing
The offline signing page removes the server from the signing process entirely. Instead of fetching the canonical message from the server, you supply the post field values yourself. The canonical message is constructed locally in your browser, signed there, and only the resulting signature is submitted to the server.
Use offline signing when:
- The server itself is inside your threat model.
- You are in a high-risk situation where a compromised server must not be able to influence what you sign.
- You want to sign on an air-gapped machine and transfer only the signature.
Go to ArchivioTOR → Offline Signing.
Air-gap workflow:
- Copy
keygen/keygen.htmlfrom the plugin folder to an offline machine. - Open it in Tor Browser (or any browser) with no network connection.
- Generate your key pair, export the
.jsonfile. - Transfer the
.jsonto your online machine via encrypted USB. - Import the key in Key Generator → 📂 Import Key File.
- Use Offline Signing to sign posts — the private key is never present in a networked browser.
Tor Deployment
ArchivioTOR is designed to be safe by default on .onion addresses.
No external dependencies. noble-ed25519.min.js is bundled with the plugin. No scripts, fonts, images, or analytics are fetched from external servers. The CDN fetch option is off by default.
Same-origin only. All AJAX and REST API calls go to your server's own origin. A .onion address works identically to a clearweb domain.
Onion Mode (on by default for new installs) switches the rate limiter from IP-based bucketing to per-session token bucketing. On a Tor hidden service every client arrives at PHP from 127.0.0.1, so IP-based rate limiting collapses all visitors into one bucket. Onion Mode issues each anonymous visitor a short-lived random cookie (SameSite=Strict, HttpOnly, Secure, host-scoped) that carries no identity. X-Forwarded-For is ignored entirely — Tor does not set it and trusting it would allow IP spoofing.
WordPress identity signal suppression (on by default for new installs) closes several WordPress-default information leaks:
| Signal | What it exposes | Action |
|---|---|---|
<meta name="generator"> |
WordPress version | Removed from <head> |
?ver=X.Y.Z on asset URLs |
WP core, plugin, and theme versions | Stripped from all enqueued assets |
X-Pingback header |
xmlrpc.php URL | Removed from all responses |
pingback.ping XML-RPC method |
Confirms xmlrpc.php; DDoS abuse vector | Disabled |
/?author=N redirect |
All registered usernames in sequence | Blocked |
/wp-json/wp/v2/users |
All registered users, names, slugs | Removed (returns 404) |
oEmbed <link> tags and Link: header |
Confirms WordPress; exposes REST API root | Removed |
Strict Containment Mode is available for deployments where readers use Tor Browser at the Safest security level (JavaScript disabled). When active, the signature badge is rendered entirely server-side with no JavaScript, the plugin's frontend assets are not enqueued, and all shortcode output passes through a link-stripper before being returned. Enable in Settings.
WebCrypto requirement. Signing uses window.crypto.subtle, which requires a secure context — HTTPS on clearweb or a .onion address over Tor. Plain HTTP admin access will not work.
Settings Reference
| Setting | Default | Description |
|---|---|---|
| Signature badge on posts | On | Append a verification badge to signed posts on the front end |
| Server-side verification | On | Enable the PHP Sodium REST verification endpoint |
| Strict Containment Mode | Off | Fully server-rendered badges; no frontend JS; for Tor Browser Safest level |
| Onion Mode | On | Per-session rate limiting; X-Forwarded-For ignored |
| WordPress identity signal suppression | On | Suppresses version strings, pingback, author enumeration, REST user list, oEmbed |
| Manifest endpoint | Off | Enable the authenticated JSON manifest for archival pipelines |
| CDN library fetch | Off | Fetch noble-ed25519 from jsDelivr instead of the bundled copy |
Settings marked On are enabled by default on fresh installations.
Manifest endpoint
The manifest endpoint (/wp-json/archiviotor/v1/manifest) returns a JSON document listing every signed post with its public key, canonical hash, signature prefix, and staleness status. It is intended for external archival and independent verification pipelines.
The endpoint always requires the Editor role or higher, regardless of the Settings toggle. It is never accessible to anonymous visitors. If you need a genuinely public manifest, reverse-proxy it behind your own access control layer.
When enabled, the manifest exposes post titles, slugs, publication dates, public keys, and modification history for every signed post in a single authenticated request. On a Tor deployment this creates a complete index of your published corpus. Disable it unless you have a specific archival need.
Canonical Message Format
The message signed for each post is a deterministic UTF-8 string:
archiviotor-v1
{post_id}
{post_title}
{post_slug}
{post_content_with_html_stripped}
{post_date_gmt}
Fields are separated by newline characters (\n). The format is versioned — future releases can introduce archiviotor-v2 without breaking existing signatures.
Retrieve the canonical message for any post:
GET /wp-json/archiviotor/v1/message/{post_id}
Independent command-line verification:
# Fetch the canonical message
curl -s https://your-site.onion/wp-json/archiviotor/v1/message/42 | jq -r '.message' > message.txt
# Fetch the stored public key and signature
curl -s https://your-site.onion/wp-json/archiviotor/v1/meta/42 | jq -r '.pubkey, .signature'
# Verify with Python (cryptography library)
python3 - <<'EOF'
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
import binascii
pubkey_hex = "YOUR_64_HEX_PUBKEY"
sig_hex = "YOUR_128_HEX_SIGNATURE"
message = open("message.txt", "rb").read()
key = Ed25519PublicKey.from_public_bytes(binascii.unhexlify(pubkey_hex))
key.verify(binascii.unhexlify(sig_hex), message)
print("Signature valid")
EOF
REST API Reference
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/wp-json/archiviotor/v1/message/{id} |
Public | Canonical signable message for a post |
GET |
/wp-json/archiviotor/v1/meta/{id} |
Public | Signature, public key, ephemeral flag, staleness |
POST |
/wp-json/archiviotor/v1/verify/{id} |
Public | Server-side verification (requires ext-sodium) |
POST |
/wp-json/archiviotor/v1/sign/{id} |
Editor+ | Save a signature (REST alternative to AJAX) |
GET |
/wp-json/archiviotor/v1/manifest |
Editor+ | Signed-post manifest for archival pipelines |
GET |
/wp-json/archiviotor/v1/sig-log/{id} |
Editor+ | Signature audit log for a post |
Public endpoints are rate-limited at 60 requests per minute per client. On Tor deployments rate limiting uses per-session token bucketing rather than IP hashing.
Troubleshooting
"noble-ed25519 not loaded" on the Dashboard
Confirm that js/noble-ed25519.min.js exists in the plugin folder, that no security plugin or server rule blocks .js files from wp-content/plugins/, and that JavaScript is enabled.
Signature shows "INVALID" or "Stale" after editing
Expected behaviour. The signature covers the content at signing time. Any change to the title, slug, or body invalidates it. Re-sign the post.
"WebCrypto not available"
Signing requires a secure context — HTTPS on clearweb, or a .onion address over Tor. Plain HTTP will not work.
Server-side verify button is missing
PHP ext-sodium is not available. Client-side verification still works. Ask your host to enable the sodium extension, or use PHP 7.2+ where it is bundled.
Key disappeared after clearing browser data
Keys are stored in sessionStorage and are cleared when the tab or browser closes — this is intentional. Import your .json backup on the Key Generator page, or generate a new key pair and re-sign your posts.
Bulk signing stops partway through
Large batches may hit PHP execution time limits. Sign in batches of 20–30. Each signature is saved immediately, so completed posts retain their signatures.
Author archive pages behave unexpectedly after enabling signal suppression
Signal suppression blocks the /?author=N numeric enumeration redirect only. Direct navigation to /author/username/ is unaffected.
The manifest endpoint returns 404
The manifest is disabled by default. Enable it in Settings. Even when enabled, it requires the Editor role — anonymous requests always receive 404.
Uninstalling
Deactivating and deleting the plugin removes all plugin files. Signatures and public keys stored in post meta remain in the database. To remove them:
DELETE FROM wp_postmeta WHERE meta_key IN (
'_archiviotor_signature',
'_archiviotor_pubkey',
'_archiviotor_ephemeral',
'_archiviotor_canonical_hash'
);
Replace wp_ with your actual table prefix if different.
Security Notes
Protect your private key. Anyone who obtains it can sign arbitrary content as you. Never paste it into a browser on a machine you do not fully trust. Store your .json export in an encrypted password manager or offline vault. For high-risk situations, generate keys on an air-gapped machine using the standalone keygen/keygen.html file.
Publish your public key fingerprint independently. A signature is only as trustworthy as the channel through which you distribute your public key. If an adversary controls your server they can substitute a different public key and sign fraudulent content. Publish your fingerprint in a DNS TXT record, Nostr profile, GitHub Gist, or on a second server you control so readers have a way to detect substitution.
Revoke compromised keys promptly. If a private key is stolen or exposed, mark it as revoked in ArchivioTOR → Key Registry immediately. Revocation signals to readers that signatures from that key should not be trusted, and prompts re-signing with a new key.
Sign close to the moment of writing. Signing protects content from that point forward. If your server was compromised before you signed, the version you signed may already be the tampered version.
Security disclosures. Contact the maintainer privately before opening a public issue for any security vulnerability.
Changelog
1.4.6
- Added dismissible server-trust warning on the Sign Posts page with a direct link to Offline Signing, so operators in high-risk situations find the right workflow without hunting for it.
1.4.5
- Author enumeration via
/?author=Nblocked; REST/wp-json/wp/v2/usersendpoints removed (returns 404, not 401); oEmbed discovery links,wp-embed.js, and RESTLink:header removed. All under the existing WordPress identity signal suppression option. - Threat model page updated with a dedicated section and summary table rows for all three paths.
1.4.4
- Rate-limit cookie
domainattribute now set to the exact request host, preventing subdomain leakage on multisite installs.
1.4.3
site_urlremoved from manifest response — it served no verification purpose and would have linked an onion address to a clearweb origin if the endpoint were ever proxied publicly.
1.4.2
- Onion Mode and WordPress identity signal suppression enabled by default on fresh installations.
- Manifest endpoint now requires Editor role or higher regardless of the Settings toggle.
1.4.1
- Initial public release.
License
ArchivioTOR is released under the GNU General Public License v2.0 or later.
noble-ed25519 is bundled under the MIT License.