OpenPGP detached-signature verification for WordPress posts β clickable lock icon, tamper-evident audit log, pure PHP, no server GPG required.
ArchivioID is an add-on plugin for ArchivioMD that brings cryptographic author proof to WordPress. Authors sign posts locally with their own GPG key, upload the .asc signature file through the post editor, and ArchivioID verifies it, displays a clickable π badge, and logs every event to a tamper-evident audit trail.
- Why ArchivioID
- Requirements
- Installation
- Quick Start
- How Signing Works
- Features
- Architecture
- Security
- Audit Log Reference
- Configuration
- Troubleshooting
- Changelog
- License
WordPress has no native mechanism to prove who wrote a post or that it has not been altered since publication. Existing PGP plugins address email encryption or contact forms β none integrate detached OpenPGP signature verification into the post editing workflow.
ArchivioID fills that gap. A detached GPG signature:
- Proves authorship β the signature can only be created by the holder of the private key
- Proves integrity β any change to the post content breaks the signature
- Is independently verifiable β any reader can verify using standard GPG tools, without needing WordPress, ArchivioID, or any proprietary software
Relevant use cases: investigative journalism, security disclosures, legal and compliance documentation, academic publishing, open-source release notes, whistleblower platforms, DevOps runbooks.
| Requirement | Minimum |
|---|---|
| WordPress | 5.0 |
| PHP | 7.4 |
| MySQL / MariaDB | 5.7 / 10.3 |
| ArchivioMD | 1.5.0 (required parent plugin) |
| Server GPG | Not required β pure PHP crypto |
ArchivioID will not activate without ArchivioMD β₯ 1.5.0.
Via WordPress admin:
Plugins β Add New β Upload Plugin β archivio-id-v1.2.0.zip β Install β Activate
Via FTP / SSH:
unzip archivio-id-v1.2.0.zip -d /path/to/wp-content/plugins/Then activate through Plugins β Installed Plugins.
ArchivioID β Key Management β Add Key
Paste your ASCII-armored public key block and give it a label. The plugin extracts and stores the full 40-character fingerprint. Only public keys are accepted β private key material is rejected at import.
1. Install ArchivioMD + ArchivioID
2. Upload GPG public key β ArchivioID β Key Management
3. Write and publish a post in WordPress
4. Copy the hash from the ArchivioID meta box
5. Strip the algorithm prefix (see below) and sign the hex hash locally
6. Upload the .asc file in the meta box β click Verify
7. π badge appears on the published post
ArchivioID does not sign raw post content. It signs the hex hash that ArchivioMD generates and stores in the _archivio_post_hash post meta field.
When you open the post editor, the ArchivioID meta box displays:
Hash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
This packed format includes the algorithm name followed by a colon, then the hex digest.
You must sign only the hex portion of the hash β not the full string including the algorithm prefix.
The meta box displays the full packed string for reference:
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Before signing, remove everything up to and including the colon. You sign only:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Why? ArchivioID's verification engine internally unpacks the stored hash using MDSM_Hash_Helper::unpack(), which strips the algorithm prefix before passing the raw hex digest to phpseclib for verification. If you sign the full sha256:hex string, the bytes will not match what the verifier checks, and verification will always fail.
| What you see in meta box | What you sign |
|---|---|
sha256:e3b0c44298... |
e3b0c44298... |
sha512:cf83e1357e... |
cf83e1357e... |
sha3-256:a7ffc6f8... |
a7ffc6f8... |
Rule: Copy from the first character after the colon to the end of the string. Never include the prefix.
Open your post editor. In the ArchivioID sidebar panel you will see:
Hash: sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Strip everything up to and including the colon. Your working value is:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Save it without a trailing newline:
echo -n "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" > hash.txt
echo -nis required. A trailing newline changes the byte sequence and will cause verification to fail. Standardechowithout-nappends\n.
Match the --digest-algo flag to the algorithm shown in the meta box prefix:
# SHA-256 (ArchivioMD default)
gpg --digest-algo SHA256 --detach-sign --armor hash.txtThis creates hash.txt.asc. That is the file you upload.
head -1 hash.txt.asc
# Must output: -----BEGIN PGP SIGNATURE-----
tail -1 hash.txt.asc
# Must output: -----END PGP SIGNATURE-----If you see -----BEGIN PGP MESSAGE----- you used --encrypt instead of --detach-sign.
If you see -----BEGIN PGP SIGNED MESSAGE----- you used --clearsign instead of --detach-sign.
- Edit the post
- In the ArchivioID meta box, select your key from the dropdown
- Upload
hash.txt.asc - Click Update (or Publish)
- Click Verify Signature
- The page reloads and shows Verified β
# One-liner: pipe directly to gpg
echo -n "YOUR_HEX_HASH_HERE" | gpg --digest-algo SHA256 --detach-sign --armor > signature.ascReplace YOUR_HEX_HASH_HERE with the hex string only (no sha256: prefix).
For other algorithms:
# SHA-512
echo -n "YOUR_HEX_HASH" | gpg --digest-algo SHA512 --detach-sign --armor > signature.asc
# SHA-384
echo -n "YOUR_HEX_HASH" | gpg --digest-algo SHA384 --detach-sign --armor > signature.ascTo sign with a specific key when you have multiple:
echo -n "YOUR_HEX_HASH" | gpg --local-user YOUR_KEY_ID --digest-algo SHA256 --detach-sign --armor > signature.asc- Copy the hex hash from the meta box (without the
sha256:prefix) - Open TextEdit β Format β Make Plain Text
- Paste the hex hash
- Save as
hash.txt - Right-click
hash.txtβ Services β OpenPGP: Sign File - Select your key
- Output:
hash.txt.ascβ upload this file
Note: GPG Suite defaults to the key's preferred digest algorithm. If verification fails, use the command line and specify
--digest-algoexplicitly to match the ArchivioMD algorithm.
- Copy the hex hash from the meta box (without the
sha256:prefix) - Open Notepad, paste the hex, save as
hash.txt(ANSI or UTF-8, no BOM) - Open Kleopatra
- File β Sign/Encrypt Files β select
hash.txt - Check Create detached signature
- Check ASCII armor
- Uncheck Encrypt
- Select your signing key β Sign
- Output:
hash.txt.ascβ upload this file
Hardware tokens work with standard GPG. The signing process is identical to the command-line method β GPG handles communication with the token transparently:
echo -n "YOUR_HEX_HASH" | gpg --digest-algo SHA256 --detach-sign --armor > signature.ascGPG will prompt for your PIN. The private key never leaves the hardware token.
| Algorithm | Meta box prefix | GPG flag | Notes |
|---|---|---|---|
| SHA-256 | sha256: |
--digest-algo SHA256 |
Default, recommended |
| SHA-512 | sha512: |
--digest-algo SHA512 |
High security |
| SHA-384 | sha384: |
--digest-algo SHA384 |
|
| SHA-224 | sha224: |
--digest-algo SHA224 |
|
| SHA3-256 | sha3-256: |
--digest-algo SHA3-256 |
Requires GnuPG 2.2.12+ |
| BLAKE2b | blake2b: |
Not directly supported | Use CLI workaround |
| SHA-1 | sha1: |
--digest-algo SHA1 |
Not recommended |
The algorithm is always shown in the meta box prefix. Match --digest-algo to whatever prefix you see.
Cause: You signed sha256:abc123... instead of abc123...
Fix:
# β Wrong β includes prefix
echo -n "sha256:e3b0c44298..." | gpg --detach-sign --armor > sig.asc
# β
Correct β hex only
echo -n "e3b0c44298..." | gpg --detach-sign --armor > sig.ascCause: Used echo without -n
Fix:
# β Wrong β echo adds \n
echo "e3b0c44298..." > hash.txt
# β
Correct β no newline
echo -n "e3b0c44298..." > hash.txtVerify byte count:
wc -c hash.txt
# SHA-256 hex = 64 bytes, not 65Cause: GPG used a different digest algorithm than ArchivioMD
Fix: Explicitly specify --digest-algo matching the prefix in the meta box. Without this flag, GPG uses the algorithm specified in your key preferences, which may differ.
Cause: Used --encrypt instead of --detach-sign
Fix:
# β Wrong
gpg --encrypt --armor hash.txt
# β
Correct
gpg --detach-sign --armor hash.txtCause: Used --clearsign instead of --detach-sign
Fix:
# β Wrong
gpg --clearsign hash.txt
# β
Correct
gpg --detach-sign --armor hash.txtCause: Editing post content causes ArchivioMD to regenerate the hash. The old signature no longer matches.
Fix: After editing, the meta box will show a new hash. Delete the old signature, re-sign the new hash, and re-upload.
- Upload ASCII-armored OpenPGP public keys via admin UI
- Plugin extracts and stores the full 40-character RFC 4880 fingerprint automatically
- Label keys by author, role, or device (e.g. "Alice β YubiKey 5")
- Supports multiple keys for multi-author sites
- Soft-delete (deactivate) keys without losing signature history
- Private key detection at import β any block containing private key material is rejected immediately
- 4 KB size limit per key
- Integrated meta box on every post type (posts, pages, custom post types)
- Upload
.ascdetached signature files directly from the post editor - AJAX verification runs immediately on upload
- Replace or delete signatures at any time
- 4 KB maximum file size (sufficient for any standard detached signature)
- Structural validation: file must contain
-----BEGIN PGP SIGNATURE-----/-----END PGP SIGNATURE-----
Pure PHP β no system GPG installation required, works on shared hosting.
- phpseclib v3 β cryptographic operations
- OpenPGP-PHP β OpenPGP packet parsing and decoding
- RSA, DSA, and EdDSA (Ed25519) key types supported
- Verification result stored in database:
verified,invalid, orerror - Failure reason recorded when verification fails
- 30-second per-user rate limit to prevent verification spam
- π lock icon appended to post title on verified posts
- Clicking the lock icon downloads the
.ascfile directly β no page navigation - Download filename:
<post-slug>-<fingerprint-short>.asc - Assets (CSS + JS) loaded only on singular posts with verified signatures β zero overhead on non-signed content
- Download event logged to audit trail
- Shortcode
[archivio_id_badge]for manual placement - WCAG 2.1 AA accessible:
focus-visiblering,aria-label,aria-busyduring download
.
archivio-id/
βββ archivio-id.php Main plugin file, bootstrap, singleton
βββ includes/
β βββ class-archivio-id-db.php Database schema, table name helpers
β βββ class-archivio-id-audit-log.php Event logging, CSV export, retention
β βββ class-archivio-id-key-manager.php Public key CRUD, fingerprint extraction
β βββ class-archivio-id-signature-store.php Per-post signature CRUD, cache invalidation
β βββ class-archivio-id-openpgp-verifier.php OpenPGP packet parsing layer
β βββ class-archivio-id-verifier.php Orchestrates verification flow
β βββ class-archivio-id-post-integration.php Meta box, AJAX handlers, admin assets
β βββ class-archivio-id-frontend-badge.php Title filter, asset enqueueing
β βββ class-archivio-id-signature-download.php Public AJAX download endpoint
β βββ autoloader.php Vendor library loader
βββ admin/
β βββ class-archivio-id-admin.php Admin menu registration
β βββ class-archivio-id-key-admin.php Key management UI
β βββ class-archivio-id-settings-admin.php Settings page
β βββ class-archivio-id-audit-log-admin.php Audit log UI + CSV export
β βββ views/ PHP view templates
βββ assets/
β βββ js/
β β βββ archivio-id-post.js Admin meta box interactions
β β βββ archivio-id-frontend.js Frontend lock icon download handler
β βββ css/
β βββ archivio-id-post.css Admin meta box styles
β βββ archivio-id-frontend.css Frontend badge + lock styles
βββ vendor/
β βββ phpseclib/ Pure PHP crypto (RSA, DSA, EdDSA)
β βββ openpgp-php/ OpenPGP packet parsing
βββ uninstall.php Clean database tables on uninstall
{prefix}archivio_id_keys
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
label |
varchar(255) | Human-readable key label |
fingerprint |
char(40) | 40-char uppercase hex (UNIQUE) |
key_id |
char(16) | Last 16 chars of fingerprint (long key ID) |
armored_key |
longtext | Full ASCII-armored public key block |
created_at |
datetime | UTC insert timestamp |
added_by |
bigint | WordPress user ID |
is_active |
tinyint(1) | Soft-delete flag |
{prefix}archivio_id_signatures
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
post_id |
bigint | WordPress post ID (UNIQUE β one signature per post) |
key_id |
bigint | FK to archivio_id_keys.id |
archivio_hash |
varchar(255) | Packed hash from ArchivioMD (algorithm:hex) |
hash_algorithm |
varchar(20) | Extracted algorithm name |
hash_mode |
varchar(10) | standard or hmac |
signature_asc |
text | ASCII-armored detached signature |
status |
varchar(20) | uploaded, verified, invalid, error |
verified_at |
datetime | UTC timestamp of successful verification |
failure_reason |
varchar(512) | Error detail if status is invalid or error |
uploaded_at |
datetime | UTC upload timestamp |
uploaded_by |
bigint | WordPress user ID |
{prefix}archivio_id_audit_log
See Audit Log Reference for schema.
Private keys are never uploaded, stored, or transmitted to the server. The signing workflow is entirely local. The server receives only the detached .asc output β a blob of data that can verify the signature but cannot be used to produce new ones.
- All admin form inputs pass through
sanitize_text_field(),sanitize_textarea_field(),sanitize_key(), andabsint()as appropriate - Uploaded
.ascfiles: extension check,is_uploaded_file()guard, 4 KB size cap, structural regex validation - Uploaded public keys: armor header check, private key material rejection, 4 KB cap, fingerprint parse validation
- All database values are escaped with
esc_html(),esc_attr(),wp_kses()before output - JavaScript dynamic content uses a local
escapeHtml()function to prevent XSS - Download endpoint outputs raw ASCII armor without HTML processing (correct for the content type)
| Action | Required capability |
|---|---|
| View admin pages | manage_options |
| Add / deactivate keys | manage_options |
| Upload signature to post | edit_post on that post |
| Verify / delete signature | edit_post on that post |
| View audit log | manage_options |
| Export audit log CSV | manage_options |
Download .asc (frontend) |
None β public endpoint, nonce only |
All admin AJAX actions use check_ajax_referer(). The frontend download uses check_ajax_referer() with a per-post nonce (archivio_id_download_<post_id>) so a token for one post cannot be replayed for another.
All database queries use $wpdb->prepare() with typed placeholders. No raw string interpolation of user data into SQL.
Content-Type: application/pgp-signature; charset=utf-8
Content-Disposition: attachment; filename="<slug>-<fp>.asc"
X-Content-Type-Options: nosniff
X-Robots-Tag: noindex
{prefix}archivio_id_audit_log
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
post_id |
bigint | WordPress post ID |
post_type |
varchar(20) | Post type (post, page, custom) |
event_type |
varchar(20) | upload, verify, delete, download |
timestamp_utc |
datetime | UTC event time |
key_fingerprint |
char(40) | 40-char public key fingerprint |
hash_algorithm |
varchar(20) | Algorithm name (sha256, sha512, etc.) |
signature_status |
varchar(20) | unverified, verified, invalid, error |
user_id |
bigint | WordPress user ID (0 for anonymous downloads) |
notes |
varchar(512) | Context or error message |
| Event | When it fires | Status logged |
|---|---|---|
upload |
.asc file attached to a post |
unverified |
verify |
Verification button clicked | verified, invalid, or error |
delete |
Signature removed | Previous status at time of deletion |
download |
Visitor clicks the π icon | verified |
- Private keys
- Raw signature bytes
- Post content or the full hash value (only the algorithm name is logged)
- Passphrases or credentials
Access via ArchivioID β Audit Logs β Export CSV.
- Filter by date range and/or status before export
- Maximum 10,000 rows per export
- Filename:
archivio-id-audit-log-YYYY-MM-DD-HHmmss.csv - CSV injection prevention: fields beginning with
=,+,-,@are prefixed with' - UTF-8 encoding
Default retention period is 90 days. Configure at ArchivioID β Settings β Audit Log Retention (1β365 days). Old entries are removed automatically via WP-Cron or on manual trigger from the admin interface.
All settings at ArchivioID β Settings.
| Setting | Default | Description |
|---|---|---|
| Audit log retention | 90 days | How long to keep log entries (1β365) |
| Default hash algorithm | sha256 | Hash algorithm for new signatures |
| Badge position | In title | Where the π icon appears |
// wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );All ArchivioID log messages are prefixed [ArchivioID] in wp-content/debug.log.
[ArchivioID] Verification SUCCESS for post 42 (key: Alice, fp: ABCD...1234)
[ArchivioID] Verification status recorded for post 42: status="verified"
[ArchivioID] Cache invalidated for post 42 after verification (status: verified)
[ArchivioID] Verification INVALID for post 42: Signature does not match the stored hash
[ArchivioID] Signature upload rejected: Uploaded format is PGP MESSAGE (expected PGP SIGNATURE)
[ArchivioID] Download blocked: stored signature for post 42 failed structural validation.
-
Did you include the algorithm prefix?
Signe3b0c44298...notsha256:e3b0c44298... -
Did you use
echo -n?
A trailing newline changes the byte sequence. -
Does
--digest-algomatch the meta box prefix?
sha256:β--digest-algo SHA256,sha512:β--digest-algo SHA512 -
Is the key in Key Management active?
Go to ArchivioID β Key Management and verify the key is not deactivated. -
Was the post edited after signing?
Editing regenerates the ArchivioMD hash. Re-sign with the new hash. -
Correct key?
gpg --verify hash.txt.asc hash.txtshows which private key was used for signing. Cross-check the fingerprint with what is stored in Key Management.
The download endpoint serves only status = verified signatures. If the signature was uploaded but never verified (status = uploaded), the download will fail. Click Verify Signature in the post editor first.
ArchivioID invalidates WordPress post cache and object cache (Redis/Memcached) after every write. If you use a full-page cache (WP Rocket, W3 Total Cache, etc.) and the lock icon does not appear after verification, manually clear that cache or wait for it to expire.
Feature: Clickable lock icon with signature download
- The π badge in the post title is now a clickable anchor
- Clicking triggers a browser download of the verified
.ascfile - Filename format:
<post-slug>-<fingerprint-short>.asc - New AJAX endpoint
archivio_id_download_sig(public, nopriv) with per-post nonce - Download event logged to audit log (
event_type = 'download') - Frontend JS and CSS enqueued only on singular posts with verified signatures
- Lock icon shows pulse animation during download, blocks double-clicks
New files:
includes/class-archivio-id-signature-download.php
Modified files:
includes/class-archivio-id-frontend-badge.phpβ lock is now<a>withdata-post-idassets/js/archivio-id-frontend.jsβ click handler, hidden-anchor download patternassets/css/archivio-id-frontend.cssβ hover, focus-visible, downloading pulse styles
Bugfix: UI sync after verification
- Added
clean_post_cache()andwp_cache_delete()after all verification writes - Auto page-reload (2-second delay) after successful verification to synchronize UI
- Enhanced AJAX response with fresh signature data and badge metadata
- Post-update verification check in debug mode to detect status mismatches
nocache_headers()on AJAX verification responses
Complete UI layer
- Admin interfaces: key management, audit log viewer, settings
- Post meta box with upload, verify, and delete actions
- Front-end π badge via
the_titlefilter - AJAX handlers for verify and delete
- Audit log database table and CSV export
Initial release
- Core verification engine (phpseclib v3 + OpenPGP-PHP)
- Public key storage with fingerprint extraction
GPL v2 or later β see LICENSE or https://www.gnu.org/licenses/gpl-2.0.html
Built by Mountain View Provisions LLC
Plugin home: mountainviewprovisions.com/ArchivioID
Support: info@mountainviewprovisions.com
Parent plugin: ArchivioMD