45 Commits

Author SHA1 Message Date
nopeitsnothing f3cb57230f Submit actual develop page
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-25 17:40:12 -04:00
nopeitsnothing 5e8057bb1f Fix copy information in website footer
And also move the develop workflow information to docs/code to cleanup
the guide documents, preventing accidental addition to the PDF.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-25 17:37:09 -04:00
nopeitsnothing ac3d2ceb37 Fix some broken YAML references
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-24 15:54:30 -04:00
nopeitsnothing 5eded0af38 Delete stale information
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-24 00:50:53 -04:00
nopeitsnothing 1bb0acc3e8 Sign local copy
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:50:31 -04:00
nopeitsnothing 25bc901ece Tweaking some of the build to function
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:43:52 -04:00
nopeitsnothing 78a0a37ee8 Tweaking some of the build to function
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:38:27 -04:00
nopeitsnothing aeb63cd7ba Tweaking some of the build to function
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:34:47 -04:00
nopeitsnothing 64ddd18535 Tweaking some of the build to function
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:29:54 -04:00
nopeitsnothing 7c9847e7d1 Tweaking some of the build to function
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:23:15 -04:00
nopeitsnothing 1e8c90513f Tweaking some of the build to function
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:21:00 -04:00
nopeitsnothing 2d09d7c01c Tweaking some of the build to function pt6
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:15:19 -04:00
nopeitsnothing 1938e031ee Tweaking some of the build to function pt5
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:13:28 -04:00
nopeitsnothing 8483d6336b Tweaking some of the build to function pt4
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:08:28 -04:00
nopeitsnothing 1c168691c5 Tweaking some of the build to function pt3
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 03:04:39 -04:00
nopeitsnothing ae50911375 Tweaking some of the build to function pt2
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 02:58:56 -04:00
nopeitsnothing df2dd61676 Tweaking some of the build to function
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 02:50:05 -04:00
nopeitsnothing 904fa24478 Moving some things around
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-20 02:45:06 -04:00
nopeitsnothing 28556c016c One job to rule them all
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-19 00:46:00 -04:00
nopeitsnothing 7bc3ed6bb6 Forgot to add flag
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-19 00:36:07 -04:00
nopeitsnothing 9a58ca1b7c The GPG bit fails, let's try again pt2
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-19 00:27:35 -04:00
nopeitsnothing 655e47fb8d The GPG bit fails, let's try again
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-19 00:25:57 -04:00
nopeitsnothing c0eb8aa6f3 Move out some scripts
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-19 00:17:39 -04:00
nopeitsnothing 90aa8b5442 Combined actions refactor
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-19 00:05:27 -04:00
nopeitsnothing 85912692d2 Combined actions into one file for less overhead
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-18 23:45:03 -04:00
nopeitsnothing 6305e1fbbb Overhaul the Hashing, scanning, release management
tool :) pt 2

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-18 22:05:52 -04:00
nopeitsnothing 0b71c3f49a Overhaul the Hashing, scanning, release management
...tool :)

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-18 22:03:40 -04:00
nopeitsnothing 468ff8f4a1 Refactor build action
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-17 13:44:04 -04:00
nopeitsnothing 11c2882ba5 Slightly refactor the workflow task
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 17:10:39 -04:00
nopeitsnothing 4c3ca7bfd7 Fix README
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 06:56:12 -04:00
nopeitsnothing 5636291c8a Build pipeline WIP
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 06:44:59 -04:00
nopeitsnothing 41ac52de0a Add: How to verify the authenticity of our files and check virus scans
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 06:03:23 -04:00
nopeitsnothing f100633632 Refactoring the VT job
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 05:58:25 -04:00
nopeitsnothing 062128732e Downgrade to working versions to fix broken jobs
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 05:55:35 -04:00
nopeitsnothing e0d16797ed Appendix A6: comment out deprecated ODT information
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 01:36:06 -04:00
nopeitsnothing d5659af3f7 Replace broken internal link with correct rel path
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 01:34:20 -04:00
nopeitsnothing a2dbdd10e9 Add nav in mkdocs.yml config
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-16 01:27:59 -04:00
nopeitsnothing cd00fa79fd Fix broken link to page
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-15 18:04:19 -04:00
nopeitsnothing c49cc87390 Update VT scan workflow
- Use v6.x for latest stable actions/checkout
- Use v6.x for latest stable Go version
- Add release line
- Scan on push using rules

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-14 04:50:59 -04:00
nopeitsnothing a14191bc7b Use VT v5.x
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-14 04:35:26 -04:00
nopeitsnothing a47e02939d Add VirusTotal scans for submitted PDFs
Submit locally crafted PDFs (currently, dark mode is broken and I'm
refactoring the CSS and HTML to fix this). First runs likely will fail
due to how runners work on GitHub Actions.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-14 04:21:12 -04:00
nopeitsnothing 52a38f5deb Fix path
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-14 03:34:45 -04:00
nopeitsnothing 206a6ff6b7 Refactor PDF build in CI, add dark mode PDF (pt 4)
Refactored GitHub Actions workflow **Build guide PDF**
(`scripts\build_guide_pdf.py`): now builds both light and dark mode PDFs
(`export/thgtoa.pdf` and `export/thgtoa-dark.pdf` respectively).

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-14 03:22:13 -04:00
nopeitsnothing 0e8de6ccc0 Fix PDF build in CI
Added workflow for building PDF. Progress.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-12 05:01:54 -04:00
nopeitsnothing d0dfec95db Fix PDF build in CI
Added workflow for building PDF. Progress.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-04-12 04:21:50 -04:00
28 changed files with 1451 additions and 173 deletions
+105
View File
@@ -0,0 +1,105 @@
name: 📖 Build & Sign PDFs
on:
workflow_dispatch:
inputs:
build_mode:
description: 'PDF build mode'
required: true
default: 'both'
type: choice
options:
- light
- dark
- both
push:
branches:
- main
paths:
- "docs/**"
- "mkdocs.yml"
- "scripts/**"
- ".github/workflows/**"
permissions:
contents: write
id-token: write
jobs:
build-sign-release:
name: Build, Sign & Release PDFs
runs-on: ubuntu-latest
steps:
- name: 🛠️ Checkout
uses: actions/checkout@v4
- name: 🐍 Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: 📦 Install MkDocs Material
run: pip install mkdocs-material
- name: Setup Chrome
uses: browser-actions/setup-chrome@v2
with:
chrome-version: 120
install-dependencies: true
install-chromedriver: true
- name: 🔑 Install GPG tools
run: |
sudo apt-get update
sudo apt-get install gnupg
- name: 🖨️ Build & Hash PDFs
env:
CI: true
run: |
python scripts/build_guide_pdf.py --${{ inputs.build_mode || 'both' }}
for f in ./export/*.pdf; do
echo "sha256sums: $f"; sha256sum "$f" >> export/sha256sums.txt; done
for f in ./export/*.pdf; do
echo "b2sums: $f"; b2sum "$f" >> export/b2sums.txt; done
- name: 🦠 Upload PDFs to VirusTotal
uses: crazy-max/ghaction-virustotal@v5
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
export/thgtoa.pdf
export/thgtoa-dark.pdf
- name: 📊 Extract VT scan results
id: vt-scan
run: |
echo "status=completed" >> $GITHUB_OUTPUT
- name: 🔗 Generate VT report links
run: |
# Create a markdown file with VT scan results and links
cat > export/virus-total-results.md << EOF
## VirusTotal Scan Results
**Scan Date:** \$(date -u +"%Y-%m-%d %H:%M UTC")
### thgtoa.pdf (Light Mode)
- **VT Report:** https://www.virustotal.com/gui/file/\$(sha256sum export/thgtoa.pdf | cut -d' ' -f1)
### thgtoa-dark.pdf (Dark Mode) (currently broken)
- **VT Report:** https://www.virustotal.com/gui/file/\$(sha256sum export/thgtoa-dark.pdf | cut -d' ' -f1)
---
*Scan performed automatically by GitHub Actions*
EOF
- name: 📤 Upload export directory as artifact
uses: actions/upload-artifact@v4
with:
name: upload pdf artifact
path: |
export/*
if-no-files-found: error
retention-days: 90
compression-level: 0
+18 -121
View File
@@ -1,130 +1,27 @@
# Byte-compiled / optimized / DLL files
# Visual Studio (Windows) solution metadata
.vs/
.vscode/
# Python (MkDocs, scripts/build_guide_pdf.py)
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
.venv/
venv/
env/
ENV/
env.bak/
venv.bak/
.env
# Spyder project settings
.spyderproject
.spyproject
# Cache
.cache/
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# MkDocs build output and local PDF export
site/
_site/
_site_test/
build/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Export directory - but track hash files and signatures
export/thgtoa.pdf.sha256
export/thgtoa-dark.pdf.sha256
*.sig
+4 -20
View File
@@ -1,28 +1,12 @@
Welcome.
**[IMPORTANT RECOMMENDATION FOR UKRAINIANS. ВАЖЛИВА РЕКОМЕНДАЦІЯ ДЛЯ УКРАЇНЦІВ](briar.html)**
This is a maintained guide with the aim of providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. <span style="color: red">**It is written with hope for activists, journalists, scientists, lawyers, whistle-blowers, and good people being oppressed, censored, harassed anywhere!**</span> This guide has no affiliation with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> collective/movement.
This is a guide with the aim of providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. <span style="color: red">**It is written with hope for activists, journalists, scientists, lawyers, whistle-blowers, and good people being oppressed, censored, harassed anywhere!**</span> This guide has no affiliation with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.com/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> collective/movement.
This guide is an open-source non-profit initiative, [licensed](LICENSE.html) under **Creative Commons Attribution-NonCommercial 4.0 International** ([cc-by-nc-4.0](https://creativecommons.org/licenses/by-nc/4.0/) <sup>[[Archive.org]](https://web.archive.org/web/https://creativecommons.org/licenses/by-nc/4.0/)</sup>) and is **not sponsored/endorsed by any commercial/governmental entity**. This means that you are free to use our guide for pretty much any purpose **excluding commercially** as long as you do attribute it. There are no ads or any affiliate links.
**If you would like to make a donation to help this project, you can do so from [here](donations.html) where you will also find the project goals. All the donations will be strictly used within the context of this project. All donations and spendings are logged on the donations page.**
**View the guide:**
- [In your browser](guide.html)
- [PDF](export/guide.pdf)
- [OpenDocument (ODT)](export/guide.odt)
- Raw [Markdown](https://raw.githubusercontent.com/Anon-Planet/thgtoa/main/guide.md)
**Ways to read or export the guide**
**Mirrors:**
- Hidden service: <del><http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/></del> **Host down**
Feel free to submit issues using Github Issues with the repository link above. Criticism, opinions, and ideas are welcome!
Follow or contact us on:
Discussion Channels:
- Matrix room: <https://matrix.to/#/#anonymity:anonymousplanet.net>
- Matrix space: <https://matrix.to/#/#psa:anonymousplanet.net>
Have a good read and feel free to share and/or recommend it!
- **In your browser:** [Hitchhiker's Guide](https://www.anonymousplanet.org/guide/) (hosted site). After a local build you can also open `site/guide/index.html` directly.
- **Local HTML preview:** from the repository root, with Python 3 and [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/) installed (`pip install mkdocs-material`), run `mkdocs serve` and open the URL printed in the terminal (for example `http://127.0.0.1:8000`).
+3 -3
View File
@@ -1,5 +1,5 @@
---
title: "About Anonymous Planet"
title: "Anonymous Planet"
description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
schema:
"@context": https://schema.org
@@ -7,7 +7,7 @@ schema:
"@id": https://www.anonymousplanet.org/
name: Anonymous Planet
url: https://www.anonymousplanet.org/about/
logo: ../media/favicon.png
logo: ../media/profile.png
sameAs:
- https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg
@@ -15,7 +15,7 @@ schema:
---
![Anonymous Planet logo](../media/profile.png){ align=right }
**Anonymous Planet** are the maintainers of the [_Hitchhiker's Guide_](https://anonymousplanet.org/guide.html) and the [_PSA Community_](https://psa.anonymousplanet.org). It is responsible for maintaining the projects and code repositories.
**Anonymous Planet** are the maintainers of the [_Hitchhiker's Guide_](../guide/index.md) and the [_PSA Community_](https://psa.anonymousplanet.org). It is responsible for maintaining the projects and code repositories. This project is part of our ongoing efforts to provide open-source tools and resources for the community, with regular updates and improvements added to the changelog.
The purpose: providing an introduction to various online tracking techniques, online ID verification techniques, and detailed guidance to creating and maintaining (truly) anonymous online identities. It is written with the hopes that good people (e.g., activists, journalists, scientists, lawyers, whistle-blowers, etc.) will be able to fight oppression, censorship and harassment! The website and projects are free (as in freedom) and not affiliated with any donor or projects discussed.
+69
View File
@@ -0,0 +1,69 @@
---
title: "Release Notes"
description: "Release Notes"
schema:
"@context": https://schema.org
"@type": Organization
"@id": https://www.anonymousplanet.org/
name: Anonymous Planet
url: https://www.anonymousplanet.org/authors/
logo: ../media/profile.png
sameAs:
- https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg
- https://mastodon.social/@anonymousplanet
---
# Release Notes
All notable changes to this project will be documented in this file.
## [Unreleased]
!!! Note "Added"
- This changelog page
- Add ways to verify the files
!!! Note "Changed"
- Refactored GitHub Actions workflow **Build PDF** (`scripts\build_guide_pdf.py`): now builds both light and dark mode PDFs
- Restored previous VT scans
!!! Note "Fixed"
- `docs/about/index.md`: replace broken reference-style internal links
- `docs/guide/index.md`: Appendix A6: comment out deprecated ODT information because we don't and probably won't use it in the future
!!! Note "Feature"
- Updated `scripts/build_guide_pdf.py` to use `--print-to-pdf` instead of `--save-as` for PDF generation
- Added a new `--dark-mode` flag to generate dark mode PDFs. Save your eyes - you only get one pair.
## [v1.2.1]
!!! Note "Added"
- GitHub Actions workflow **Build PDF** (`.github/workflows/build-pdf.yml`): installs Chromium on `ubuntu-latest`, runs `scripts/build_guide_pdf.py`, uploads `export/guide.pdf` as the `guide-pdf` artifact. Runs on `workflow_dispatch`, on pushes to `main` that touch docs or build inputs, and on matching pull requests.
- `scripts/build_guide_pdf.py` to build the MkDocs site and render the guide to a single PDF (`export/guide.pdf` by default) using a Chromium-based browser (Chrome or Edge) headless print-to-PDF.
- `docs/stylesheets/extra.css` and `extra_css` in `mkdocs.yml` for shared site styling.
- This `CHANGELOG.md`.
!!! Note "Changed"
- `README.md` “Ways to read or export the guide”: hosted link, local `mkdocs serve`, PDF build via the script, ODT note, raw Markdown link.
- Guide landing layout: wrap the opening block in `docs/guide/index.md` with a `guide-intro-lead` container so the logo and first sections share one layout context for web and print.
- `.gitignore` to exclude local build outputs `export/`, `site/`, and `_site_test/`.
- `scripts/build_guide_pdf.py`: when the `CI` environment variable is set, pass Chromium flags (`--no-sandbox`, `--disable-setuid-sandbox`, `--disable-dev-shm-usage`) so headless print works on typical CI images.
- `README.md`: note the **Build PDF** GitHub Actions workflow and the `guide-pdf` artifact.
!!! Note "Fixed"
- `docs/guide/index.md`: replace broken reference-style internal links (`[label][label:]`) with working same-page fragment links to the correct headings; correct the mismatched “Real-Name System” cross-reference; fix a broken footnote marker on the “free (unallocated) space of your hard drive” list item.
[Unreleased]: https://github.com/Anon-Planet/thgtoa/compare/v1.2.1...HEAD
[v1.2.1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.1
***The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),***
***and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).***
+50
View File
@@ -0,0 +1,50 @@
# Development
??? Note "How the pipeline works"
**Automatic PDF Generation:** - Builds both light and dark mode PDFs from MkDocs source
**SHA256 Hash Generation:** - Creates hash files for integrity verification
**GPG Signature Signing:** - Signs all PDFs and hash files with repository GPG key
**VirusTotal Scanning:** - Automatically scans PDFs and updates release notes
**Release Automation:** - Packages everything into GitHub releases
## Architecture
### Build PDF Workflow (`build-sign-release.yml`)
!!! Note "Steps"
- Checkout repository
- Set up Python and MkDocs Material
- Install Chromium browser
- Generate both light and dark mode PDFs with `scripts\build_guide_pdf.py`
- Create SHA256 and blake2 hash files in `export/`
- Sign all files with GPG in `export/`
- Upload artifacts to GitHub Actions **manually**
### SHA256 Hash Verification
!!! Note "**How it works**"
- Each PDF gets a unique SHA256 hash calculated at build time
- Hash stored in `.sha256` files alongside the PDFs
- Combined `sha256sum.txt` for batch verification
### GPG Signature Verification
**Purpose:** Verify authenticity and prevent tampering
!!! Note "How it works"
- Detached signatures created for each PDF and hash file
- Public keys available in `/pgp/` directory
**Verification command:**
```bash
gpg --import pgp/anonymousplanet-master.asc
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
```
---
*This workflow is designed for security-conscious users who need to verify the authenticity and integrity of downloaded documents.*
+31 -23
View File
@@ -7,12 +7,18 @@ schema:
"@id": https://www.anonymousplanet.org/
name: Anonymous Planet
url: https://www.anonymousplanet.org/guide/
logo: ../media/favicon.ico
logo: ../media/profile.png
sameAs:
- https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg
---
![Anonymous Planet logo](../media/profile.png){ align=right }
<div class="pdf-title-page" aria-hidden="true">
<p class="pdf-title-page__title">The Hitchhiker's Guide to Online Anonymity</p>
<p class="pdf-title-page__subtitle"><em>(Or "How I learned to start worrying and love privacy anonymity")</em></p>
<p class="pdf-title-page__meta">Version 1.2.1, April 2026 by Anonymous Planet</p>
</div>
<div class="guide-intro-lead" markdown="1">
![Anonymous Planet logo](../media/profile.png)
There are several ways you could read this guide:
@@ -45,6 +51,8 @@ You could also install the [LibRedirect](https://libredirect.github.io/) extensi
Finally note that this guide does mention and even recommends various commercial services (such as VPNs, CDNs, e-mail providers, hosting providers...) **but is not endorsed or sponsored by any of them in any way. There are no referral links and no commercial ties with any of these providers. This project is 100% non-profit and only relying on donations.**
</div>
## Requirements & Limitations
- Understanding of the English language (in this case American English).
@@ -719,7 +727,7 @@ As well as those interesting podcasts:
<https://www.inteltechniques.com/podcast.html>
You should never share real individual experiences/details using your anonymous identities that could later lead to finding your real identity. You will see more details about this in the [Creating new identities][Creating new identities:] section.
You should never share real individual experiences/details using your anonymous identities that could later lead to finding your real identity. You will see more details about this in the [Creating new identities](#creating-new-identities) section.
### Your Face, Voice, Biometrics, and Pictures
@@ -1966,7 +1974,7 @@ Remember that encryption with or without plausible deniability is not a silver b
**See <https://en.wikipedia.org/wiki/Rubber-hose_cryptanalysis>** <sup>[[Wikiless]](https://wikiless.com/wiki/Rubber-hose_cryptanalysis)</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Rubber-hose_cryptanalysis)</sup>
CAUTION: Please see [**Appendix K: Considerations for using external SSD drives**](#appendix-k-considerations-for-using-external-ssd-drives) and [**Understanding HDD vs SSD**][Understanding HDD vs SSD:] sections if you consider storing such hidden VMs on an external SSD drive:
CAUTION: Please see [**Appendix K: Considerations for using external SSD drives**](#appendix-k-considerations-for-using-external-ssd-drives) and [**Understanding HDD vs SSD**](#understanding-hdd-vs-ssd) sections if you consider storing such hidden VMs on an external SSD drive:
- **Do not use hidden volumes on SSD drives as this is not supported/recommended by Veracrypt**[^303]**.**
@@ -2226,7 +2234,7 @@ You can mitigate this attack by doing the following (as recommended earlier):
- Set up BIOS/UEFI/Firmware passwords to prevent any unauthorized boot of an unauthorized device.
- Some OSes and Encryption software have the [Anti Evil Maid (AEM)][Anti Evil Maid (AEM):] protection that can be enabled. This is the case with Windows/Veracrypt and QubeOS (only on Intel CPUs).
- Some OSes and Encryption software have the [Anti Evil Maid (AEM)](#anti-evil-maid-aem) protection that can be enabled. This is the case with Windows/Veracrypt and QubeOS (only on Intel CPUs).
##### Cold-Boot Attack
@@ -3323,7 +3331,7 @@ Unfortunately, using Tor alone will raise the suspicion of many destinations' pl
- If you intend to create persistent shared and authenticated identities on various services where access from Tor is hard, we recommend the **VPN over Tor** and **VPS VPN/Proxy over Tor** options (or VPN over Tor over VPN if needed). It might be a bit less secure against correlation attacks due to breaking Tor Stream isolation but provides much better convenience in accessing online resources than just using Tor. It is an "acceptable" trade-off IMHP if you are careful enough with your identity.
- **Note: It is becoming more common that mainstream services and CDNS are also blocking or hindering VPN users with captchas and other various obstacles**. **In that case, a self-hosted VPS with a VPN/Proxy over Tor is the best solution for this as having your own dedicated VPS guarantees you are the sole user of your IP and encounter little to no obstacles.** Consider a [Self-hosted VPN/Proxy on a Monero/Cash-paid VPS (for users more familiar with Linux)][Self-hosted VPN/Proxy on a Monero/Cash-paid VPS (for users more familiar with Linux):] if you want the least amount of issues (this will be explained in the next section in more details).
- **Note: It is becoming more common that mainstream services and CDNS are also blocking or hindering VPN users with captchas and other various obstacles**. **In that case, a self-hosted VPS with a VPN/Proxy over Tor is the best solution for this as having your own dedicated VPS guarantees you are the sole user of your IP and encounter little to no obstacles.** Consider a [Self-hosted VPN/Proxy on a Monero/Cash-paid VPS (for users more familiar with Linux)](#self-hosted-vpnproxy-on-a-monerocash-paid-vps-for-users-more-familiar-with-linux) if you want the least amount of issues (this will be explained in the next section in more details).
- If your intent however is just to browse random services anonymously without creating specific shared identities, using tor friendly services; or if you do not want to accept that trade-off in the earlier option. **Then we recommend using the Tor Only route to keep the full benefits of Stream Isolation (or Tor over VPN if you need to).**
@@ -4371,7 +4379,7 @@ All the VMs behind the Whonix Gateway should now work fine without additional co
**Take a post-install VirtualBox snapshot of your VMs.**
You are done and can now skip the rest to go to the [Getting Online][Getting Online:] part.
You are done and can now skip the rest to go to the [Getting Online](#getting-online) part.
## The Qubes Route
@@ -4560,7 +4568,7 @@ Unfortunately, using Tor alone will raise the suspicion of many destinations' pl
- If you intend to create persistent shared and authenticated identities on various services where access from Tor is hard, we recommend the **VPN over Tor** and **VPS VPN/Proxy over Tor** options (or VPN over Tor over VPN if needed). It might be a bit less secure against correlation attacks due to breaking Tor Stream isolation but provides much better convenience in accessing online resources than just using Tor. It is an "acceptable" trade-off IMHP if you are careful enough with your identity.
- **Note: It is becoming more common that mainstream services and CDNS are also blocking or hindering VPN users with captchas and other various obstacles**. **In that case, a self-hosted VPS with a VPN/Proxy over Tor is the best solution for this as having your own dedicated VPS guarantees you are the sole user of your IP and encounter little to no obstacles.** Consider a [Self-hosted VPN/Proxy on a Monero/Cash-paid VPS (for users more familiar with Linux)][Self-hosted VPN/Proxy on a Monero/Cash-paid VPS (for users more familiar with Linux):] if you want the least amount of issues (this will be explained in the next section in more details).
- **Note: It is becoming more common that mainstream services and CDNS are also blocking or hindering VPN users with captchas and other various obstacles**. **In that case, a self-hosted VPS with a VPN/Proxy over Tor is the best solution for this as having your own dedicated VPS guarantees you are the sole user of your IP and encounter little to no obstacles.** Consider a [Self-hosted VPN/Proxy on a Monero/Cash-paid VPS (for users more familiar with Linux)](#self-hosted-vpnproxy-on-a-monerocash-paid-vps-for-users-more-familiar-with-linux) if you want the least amount of issues (this will be explained in the next section in more details).
- If your intent however is just to browse random services anonymously without creating specific shared identities, using tor friendly services; or if you do not want to accept that trade-off in the earlier option. **Then we recommend using the Tor Only route to keep the full benefits of Stream Isolation (or Tor over VPN if you need to).**
@@ -5223,7 +5231,7 @@ Phone verification is advertised by most platforms to verify you are human. But
Most platforms (including the privacy-oriented ones such as Signal/Telegram/Proton will require a phone number to register, and most countries now make it mandatory to submit a proof of ID to register[^381].
Fortunately, this guide explained earlier how to get a number for these cases: [Getting an anonymous Phone number][Getting an anonymous Phone number:].
Fortunately, this guide explained earlier how to get a number for these cases: [Getting an anonymous Phone number](#getting-an-anonymous-phone-number).
### E-Mail verification
@@ -5731,7 +5739,7 @@ Legend:
- "Indirectly": This means they do require something but indirectly through a third-party system (Financial KYC for example).
- **See [The Real-Name System][Checking if your Tor Exit Node is terrible:] for essential information. See below for details.**
- **See [The Real-Name System](#the-real-name-system) for essential information. See below for details.**
**Below you'll find a list of "problematic services". If they're not below, it means there are no issues at all with anything (like Briar for example)**
@@ -6215,7 +6223,7 @@ You are going to have to find a separate way to post there using at least seven
**Crypto Wallets**
Use any crypto wallet app within the Windows Virtual Machine. But be careful not to transfer anything toward an Exchange or a known Wallet. Crypto is in most cases NOT anonymous and can be traced back to you when you buy/sell any (remember the [Your Cryptocurrencies transactions][Your Cryptocurrencies transactions:] section).
Use any crypto wallet app within the Windows Virtual Machine. But be careful not to transfer anything toward an Exchange or a known Wallet. Crypto is in most cases NOT anonymous and can be traced back to you when you buy/sell any (remember the [Your Crypto Transactions](#your-crypto-transactions) section).
**If you really want to use Crypto, use Monero which is the only one with reasonable privacy/anonymity.**
@@ -6269,7 +6277,7 @@ For these reasons, it is always important to check the claims of various apps. O
#### Roll your own crypto
See the [Bad Cryptography][Bad Cryptography:] section at the start of this guide.
See the [Bad Cryptography](#bad-cryptography) section at the start of this guide.
**Always be cautious of apps rolling their own crypto until it has been reviewed by many in the crypto community (or even better published and peer-reviewed academically)**. Again, this is harder to verify with closed-source proprietary apps.
@@ -6293,7 +6301,7 @@ Zero-Access Encryption[^411] at rest is used when you store data at some provide
Zero-Access encryption is an added feature/companion to e2ee but is applied mainly to data at rest and not communications.
Examples of this issue would be iMessage and WhatsApp, see the [Your Cloud backups/sync services][Your Cloud backups/sync services:] at the start of this guide.
Examples of this issue would be iMessage and WhatsApp, see the [Your Cloud Backup & Sync Services](#your-cloud-backup-sync-services) at the start of this guide.
So again, it is best to prefer Apps/Providers that do offer Zero-Access Encryption at rest and cannot read/access any of your data/metadata even at rest and not only limited to communications.
@@ -6301,7 +6309,7 @@ Such a feature would have prevented important hacks such as the Cambridge Analyt
#### Metadata Protection
Remember the [Your Metadata including your Geo-Location][Your Metadata including your Geo-Location:] section. End-to-end Encryption is one thing, but it does not necessarily protect your metadata.
Remember the [Your Metadata](#your-metadata) section (including geo-location). End-to-end Encryption is one thing, but it does not necessarily protect your metadata.
For Instance, WhatsApp might not know what you are saying but they might know who you are talking to, how long and when you have been talking to someone, who else is in groups with you, and if you transferred data with them (such as large files).
@@ -7087,7 +7095,7 @@ Here is a comparative table of recommended/included software compiled from vario
**Legend:** * Not recommended but mentioned. N/A = Not Included or absence of recommendation for that software type. (L)= Linux Only but can maybe be used on Windows/macOS through other means (HomeBrew, Virtualization, Cygwin). (?)= Not tested but open-source and could be considered.
**In all cases, we strongly recommend only using such applications from within a VM or Tails to prevent as much leaking as possible. If you do not, you will have to sanitize those documents carefully before publishing (See [Removing Metadata from Files/Documents/Pictures][Removing Metadata from Files/Documents/Pictures:]).**
**In all cases, we strongly recommend only using such applications from within a VM or Tails to prevent as much leaking as possible. If you do not, you will have to sanitize those documents carefully before publishing (See [Removing Metadata from Files/Documents/Pictures](#removing-metadata-from-filesdocumentspictures)).**
### Communicating sensitive information
@@ -7123,7 +7131,7 @@ Without SecureDrop you could consider:
What you should avoid:
- Do not send physical materials using the post due to the risk of leaving DNA/Fingerprints or other traceable information (see [Cash-Paid VPN (preferred)][Cash/Monero-Paid VPN:]).
- Do not send physical materials using the post due to the risk of leaving DNA/Fingerprints or other traceable information (see [Cash-Paid VPN (preferred)](#cashmonero-paid-vpn)).
- Do not use methods linked to a phone number (even a burner one) such as Signal/WhatsApp/Telegram.
@@ -7207,7 +7215,7 @@ To do this, when mounting the Decoy Volume, select Mount Options and Check the "
- If you are mounting the hidden volume from your Host OS (**not recommended**), you should erase all traces of this hidden volume everywhere after use. There could be traces in various places (system logs, file systems journaling, recent documents in your applications, indexing, registry entries...). Refer to the [Some additional measures against forensics](#some-additional-measures-against-forensics) section of this guide to remove such artifacts. Especially on Windows. Instead, you should mount them on your Guest VMs. With Virtualbox for instance, you could take a snapshot of the VM before opening/working the hidden volume and then restore the snapshot before opening/working on it after use. This should erase the traces of its presence and mitigate the issue. Your Host OS might keep logs of the USB key being inserted but not of the hidden volume usage. Therefore, we do not recommend using these from your host OS.
- Do not store these on external SSD drives if you are not sure you can use Trim on them (see the [Understanding HDD vs SSD][Understanding HDD vs SSD:] section).
- Do not store these on external SSD drives if you are not sure you can use Trim on them (see the [Understanding HDD vs SSD](#understanding-hdd-vs-ssd) section).
### Full Disk/System Backups
@@ -8599,7 +8607,7 @@ Please keep thinking for yourself, use critical thinking, and keep an open mind.
**"In the end the Party would announce that two and two made five, and you would have to believe it" -- George Orwell, 1984, Book One, Chapter Seven.**
Consider helping others (see [Helping others staying anonymous][Helping others staying anonymous:])
Consider helping others (see [Helping others staying anonymous](#helping-others-staying-anonymous))
# Donations
@@ -9259,7 +9267,7 @@ Both these tools can be used for cleaning many things such as:
- Various logs
- The free (unallocated) space of your hard drive][^462].
- The free (unallocated) space of your hard drive[^462].
- Secure deletion of files
@@ -10109,7 +10117,7 @@ Even the most basic controls (supervision or parental) will send out detailed ap
This method is the one we would recommend in those cases.
It is relatively easy for your adversary to prevent this by setting up firmware BIOS/UEFI (see [Bios/UEFI/Firmware Settings of your laptop][Bios/UEFI/Firmware Settings of your laptop:]) controls but usually most adversaries will overlook this possibility which requires more technical knowledge than just relying on Software.
It is relatively easy for your adversary to prevent this by setting up firmware BIOS/UEFI (see [Bios/UEFI/Firmware Settings of your laptop](#biosuefifirmware-settings-of-your-laptop)) controls but usually most adversaries will overlook this possibility which requires more technical knowledge than just relying on Software.
This method could even decrease suspicion and increase your plausible deniability as your adversaries think they have things under control and that everything appears normal in their reports.
@@ -10998,7 +11006,7 @@ As mentioned before in this guide multiple times, we strongly recommend the use
- But what if the service you want does not accept Monero but does accept a more mainstream cryptocurrency such as Bitcoin (BTC) or Ethereum (ETH)?
**Bitcoin and other "mainstream cryptocurrencies" are not anonymous at all (Remember [Your Cryptocurrencies transactions][Your Cryptocurrencies transactions:]) and you should never ever purchase, for example, Bitcoin from an exchange and then use these directly for purchasing services anonymously. This will not work, and the transaction can be traced easily.**
**Bitcoin and other "mainstream cryptocurrencies" are not anonymous at all (Remember [Your Crypto Transactions](#your-crypto-transactions)) and you should never ever purchase, for example, Bitcoin from an exchange and then use these directly for purchasing services anonymously. This will not work, and the transaction can be traced easily.**
- **Stay away from so-called "private" mixers, tumblers and coinjoiners.** You might think this is a good idea, but not only are they useless with cryptocurrencies such as BTC/ETH/LTC, they are also dangerous. They take custody of your coins. Use Monero to anonymize your crypto. Do not use a normal KYC-enabled exchange to buy/sell your Monero (such as Kraken), since this information on your purchases and withdrawals (for intended use) are retained in the exchange. Instead, use a P2P exchange that doesn't require KYC such as what can be found on <https://kycnot.me/>.
@@ -11336,7 +11344,7 @@ If you want to compare an older version of the PDF with a newer version, conside
- <https://draftable.com/compare>
If you want to compare the older version of the ODT format with a newer version, use the LibreWriter compare features as explained here: <https://help.libreoffice.org/7.1/en-US/text/shared/guide/redlining_doccompare.html> <sup>[[Archive.org]](https://web.archive.org/web/https://help.libreoffice.org/7.1/en-US/text/shared/guide/redlining_doccompare.html)</sup>
<!-- If you want to compare the older version of the ODT format with a newer version, use the LibreWriter compare features as explained here: <https://help.libreoffice.org/7.1/en-US/text/shared/guide/redlining_doccompare.html> <sup>[[Archive.org]](https://web.archive.org/web/https://help.libreoffice.org/7.1/en-US/text/shared/guide/redlining_doccompare.html)</sup> -->
# Appendix A7: Crypto Swapping Services without Registration and KYC
@@ -11438,7 +11446,7 @@ Remember this should only be done on a secure environment such as VM behind the
Here is a checklist of things to verify before sharing information to anyone:
- Check the files for any metadata: see [Removing Metadata from Files/Documents/Pictures][Removing Metadata from Files/Documents/Pictures:]
- Check the files for any metadata: see [Removing Metadata from Files/Documents/Pictures](#removing-metadata-from-filesdocumentspictures)
- Check the files for anything malicious: see [Appendix T: Checking files for malware](#appendix-t-checking-files-for-malware)
+8 -4
View File
@@ -7,19 +7,23 @@ schema:
"@id": https://www.anonymousplanet.org/
name: Anonymous Planet
url: https://www.anonymousplanet.org/authors/
logo: ../media/favicon.png
logo: ../media/profile.png
sameAs:
- https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg
- https://mastodon.social/@anonymousplanet
---
![Anonymous Planet logo](media/profile.png){ align=right }
**Welcome to the Hitchhiker's Guide.**
# **Hello, and welcome to the Hitchhiker's Guide.**
**9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6**
You'll use it to [**verify the checksum** and **GPG signature** of all files for authenticity.](verify/index.md)
Please share this project if you enjoy it and you think it might be useful to others.
Anonymous Planet is a collective of volunteers and contributors. No one person is considered more valuable than another, and no one person should be viewed as having "more impact" on Anonymous Planet.
![Anonymous Planet logo](media/profile.png){ align=right }
Anonymous Planet is a collective of volunteers.
??? person "Das Kolburn"
+7 -1
View File
@@ -7,7 +7,7 @@ schema:
"@id": https://www.anonymousplanet.org/
name: Anonymous Planet
url: https://www.anonymousplanet.org/mirrors/
logo: ../media/favicon.png
logo: ../media/profile.png
sameAs:
- https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg
@@ -25,6 +25,12 @@ schema:
- [Archive.today](https://archive.fo/anonymousplanet.org)
- [Archive.today over Tor](http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/anonymousplanet.org)
!!! Note "PDF export (single file)"
The guide is also available as a **PDF** (images and layout preserved). It is built automatically in GitHub Actions: open [**Build guide PDF**](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-sign-release.yml) on the [**source repository**](https://github.com/Anon-Planet/thgtoa), pick a successful run, and download the **`thgtoa`** and **`thgtoa-dark`** artifacts. You can start a fresh build anytime (**Actions** → **Build guide PDF****Run workflow**).
To produce the same file locally, clone the repository and run `python3 scripts/build_guide_pdf.py --both` (Python, [MkDocs Material](https://squidfunk.github.io/mkdocs-material/getting-started/), and **Google Chrome** or **Microsoft Edge** required). More detail is in the [repository README](https://github.com/Anon-Planet/thgtoa#ways-to-read-or-export-the-guide).
!!! Note "Our official git mirrors"
- [Github](https://github.com/anon-planet)
+130
View File
@@ -0,0 +1,130 @@
/* Generate dark mode PDF of the HTML at guide/index.html */
/*
DARK_MODE_PDF.CSS
Use this stylesheet when generating a PDF from HTML.
*/
:root {
/* Color Palette */
--bg-color: #121212; /* Deep dark grey (easier on eyes than pure black) */
--text-primary: #e0e0e0; /* Off-white for readability */
--text-secondary: #a0a0a0; /* Grey for captions/metadata */
--accent-color: #bb86fc; /* Light purple accent (optional) */
--border-color: #333333; /* Subtle borders */
/* Fonts - System fonts ensure best rendering across PDF engines */
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* --- RESET & BASE STYLES --- */
* {
box-sizing: border-box;
}
body {
background-color: var(--bg-color);
color: var(--text-primary);
font-family: var(--font-main);
line-height: 1.6;
margin: 0;
padding: 0;
}
/* --- TYPOGRAPHY & HEADINGS --- */
h1, h2, h3, h4, h5, h6 {
color: var(--text-primary);
font-weight: 700;
margin-top: 1.5em;
margin-bottom: 0.5em;
}
p {
margin-bottom: 1rem;
color: var(--text-secondary); /* Slightly dimmer text for body copy */
}
a {
color: var(--accent-color);
text-decoration: underline;
}
/* --- CONTAINER & LAYOUT --- */
.container {
max-width: 800px;
margin: 40px auto;
padding: 20px;
}
/* Cards / Sections with dark backgrounds */
.card {
background-color: #1e1e1e;
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
/* --- TABLES (Common in PDFs) --- */
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
color: var(--text-primary);
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background-color: #2c2c2c;
font-weight: bold;
}
tr:last-child td {
border-bottom: none;
}
/* --- IMAGES --- */
img {
max-width: 100%;
height: auto;
display: block;
margin: 20px 0;
/* This ensures high contrast images don't get washed out */
filter: brightness(1.1);
}
/* --- CRITICAL FOR PDF GENERATORS --- */
/* Forces the browser/PDF engine to print background colors and graphics */
@media print {
@page {
size: A4; /* Change to 'Letter' if preferred */
margin: 20mm;
}
body {
background-color: var(--bg-color) !important;
color: var(--text-primary) !important;
}
.card, table th {
-webkit-print-color-adjust: exact !important; /* Chrome/Safari */
print-color-adjust: exact !important; /* Firefox/Standard */
background-color: #1e1e1e !important;
color: var(--text-primary) !important;
}
/* Prevent page breaks in the middle of a sentence or card if possible */
.card {
break-inside: avoid;
}
/* Hide elements you don't want in PDF (like navigation bars) */
nav, footer, button {
display: none !important;
}
}
+55
View File
@@ -0,0 +1,55 @@
/* Title sheet: visible only when printing / generating PDF (not on screen). */
.pdf-title-page {
display: none;
}
@media print {
.pdf-title-page {
display: block;
box-sizing: border-box;
text-align: center;
padding: 5rem 2rem 4rem;
page-break-after: always;
}
.pdf-title-page__title {
font-size: 1.65rem;
font-weight: 700;
line-height: 1.25;
margin: 0 0 1.25rem;
}
.pdf-title-page__subtitle {
font-size: 1.05rem;
line-height: 1.4;
margin: 0 0 2rem;
}
.pdf-title-page__meta {
font-size: 0.95rem;
font-style: normal;
margin: 0;
}
}
/* Guide landing: small floating logo so opening copy flows beside it (HTML + PDF). */
.guide-intro-lead {
overflow: auto;
}
.guide-intro-lead > p:first-child {
margin-top: 0;
}
.guide-intro-lead > p:first-child img {
float: right;
max-width: 6.5rem;
height: auto;
margin: 0 0 0.5rem 1rem;
}
@media print {
.guide-intro-lead > p:first-child img {
max-width: 5rem;
}
}
+126
View File
@@ -0,0 +1,126 @@
---
title: "Verify"
description: How to verify the authenticity of our files and check virus scans
---
# PDF Verification Guide
## Files Provided
For each PDF release, you'll receive:
- **PDF file** (`thgtoa.pdf` or `thgtoa-dark.pdf`) - The actual document
- **Signature file** (`.sig`) - GPG detached signature for authenticity verification
- **Hash file** (`.sha256`) - SHA256 checksum for integrity verification
## Quick Verification
### Using Python Script (Recommended)
```bash
# Verify everything (hashes, signatures, and optionally VirusTotal)
python scripts/verify_pdf.py --all
# Only verify hashes
python scripts/verify_pdf.py --hashes
# Only verify GPG signatures
python scripts/verify_pdf.py --signatures
# Check VirusTotal scan status (requires VT_API_KEY environment variable)
python scripts/verify_pdf.py --vt
```
### Manual Verification
#### 1. Verify SHA256 Hash
**Linux/macOS:**
```bash
cd /path/to/repo
sha256sum -c sha256sum-light.txt
```
**Windows (PowerShell):**
```powershell
Get-FileHash -Algorithm SHA256 export\thgtoa.pdf | Select-Object Hash
# Compare with the hash in thgtoa.pdf.sha256
```
#### 2. Verify GPG Signature
First, import the public key:
```bash
gpg --import pgp/anonymousplanet-master.asc
```
Then verify the signature:
```bash
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
gpg --verify export/thgtoa-dark.pdf.sig export/thgtoa-dark.pdf
```
Expected output for successful verification:
```
gpg: Signature made Mon 20 Apr 2026 01:46:40 AM EDT
gpg: using EDDSA key 9FA5436D0EE360985157382517ECA05F768DEDF6
gpg: Good signature from "Anonymous Planet Master Signing Key" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6
```
#### 3. Check VirusTotal Status
Visit the VirusTotal report links (automatically generated in release notes):
- Light mode: `https://www.virustotal.com/gui/file/[hash]`
- Dark mode: `https://www.virustotal.com/gui/file/[hash]`
Or use the Python script with API key:
```bash
export VT_API_KEY=your_vt_api_key
python scripts/verify_pdf.py --vt
```
## Automated Verification in CI/CD
The GitHub Actions workflows automatically:
1. **Build PDFs** from MkDocs source
2. **Generate SHA256 hashes** and save to root directory
3. **Sign files with GPG** using the repository's private key
4. **Scan with VirusTotal** and update release notes
5. **Create releases** with all verification artifacts
## Security Best Practices
1. **Always verify signatures** before opening PDFs from untrusted sources
2. **Check hashes** to ensure files weren't corrupted during download
3. **Review VirusTotal results** for any suspicious detections
4. **Import keys securely** - verify key fingerprints with the project maintainers
5. **Keep verification scripts updated** to match current security standards
## Troubleshooting
### "Good signature" but wrong owner?
- Ensure you imported the correct public key
- Check the key fingerprint matches the official one from the repository
### Hash mismatch?
- Re-download the file (corruption during transfer)
- Verify you're checking against the correct hash file
- Check for disk errors on your system
### GPG not found?
- Install GPG: `sudo apt install gnupg` (Debian/Ubuntu) or `brew install gnupg` (macOS)
- On Windows, use [Gpg4win](https://www.gpg4win.org/)
## Key Information
**Signing Key:** Anonymous Planet Master Signing Key ("MSK")
**Key ID:** See `pgp/anonymousplanet-master.asc` for details
**Fingerprint:** Verify from the repository's official documentation
---
*For questions or issues with verification, please open an issue on GitHub.*
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN PGP SIGNATURE-----
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fY6QAD/YCGJqs9HiRllFrF9EluE
Ga4XUEQ/R6Q2zc+X6lX856sBAJIpxeMxUmMUXyr3xBAHxUf5eV+nQYkQQMKI81L1
x8gL
=VX6l
-----END PGP SIGNATURE-----
+1
View File
@@ -0,0 +1 @@
f212d0425b38d5cd10da6dc804b60f143da23d4b07051aae31d0966082519b300af0e1c423683e0223738b33b138c687232b1c8bd68cf643777bbc5b588152bd ./export/thgtoa-dark.pdf
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN PGP SIGNATURE-----
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fbdDgEAoSslLR47ydW/3r1wJOPY
X/waLkVbkGZpHqwd4RjywwcA/3B7Ci+jUg+yP5TRsuChagEhwyO5vw2DxSlUGoB4
+ksH
=2ja9
-----END PGP SIGNATURE-----
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN PGP SIGNATURE-----
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7faErgD/Svj1G+B7gmrZQ6AsLZ5J
HfeldxjmrXE99dig1iHtl5IBAMndZZb+95TO03IZ9eLGfYuyTz4GCUanmftsY9yv
LAIN
=MEd0
-----END PGP SIGNATURE-----
BIN
View File
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN PGP SIGNATURE-----
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7favvgEAvFFSB5NrsrKMYvGG5ZYB
iLIyt8Sn1rZmlVkibssMPq0BAImpZe8S7hWNkbukyEC4sLbKiOYvjbVipQHnrIUV
xPMH
=0hnj
-----END PGP SIGNATURE-----
+1
View File
@@ -0,0 +1 @@
436ed0df78c299f95b8d5ff94f43f26ec2e7825d92d843fc15419630d55ed5e0c98485e738c12715a2b6242633faae38e8a98935b361d44ddde97a1692cb01a1 ./export/thgtoa.pdf
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN PGP SIGNATURE-----
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fatsgEAixDzH+zTnKYMEx3sikWp
dsNTiHTU6wJY/brVJIU879UBAJntBIq72vqwKtMb/ZlVvomdDvKVllZw8ZsYBz1n
aTkM
=vkgy
-----END PGP SIGNATURE-----
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN PGP SIGNATURE-----
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7faAGQEAyEhVKrRoXIsV3E5f1FZg
8fcsmbxCnKBqxichCkf0dWYBAIvbI146mQLHaNqLDaTIqCUQbkq1aE/YMFDGykUG
ngsJ
=/0RY
-----END PGP SIGNATURE-----
+16
View File
@@ -0,0 +1,16 @@
## VirusTotal Scan Results
**Scan Date:** 2026-04-19 01:48 UTC
---
### thgtoa.pdf
- **SHA256 Hash:** `f82f6f53319315568fc2524b4eaf01126fe52356a20363cd358ad5977388ba28`
- **VirusTotal Report:** VT_API_KEY not configured, scan skipped
### thgtoa-dark.pdf
- **SHA256 Hash:** `94a0c8e3b81b0aeeb921029a41713d81b836da893a9bc9f905ca7296e82bd70f`
- **VirusTotal Report:** VT_API_KEY not configured, scan skipped
---
*Scan performed automatically by GitHub Actions*
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN PGP SIGNATURE-----
iJEEABYKADkWIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCaeXaqxsUgAAAAAAEAA5t
YW51MiwyLjUrMS4xMiwyLDIACgkQF+ygX3aN7fYpCgEA209U3QewChp7mdrrFjH1
CaBMIk2sCHwRMCcmbMDkNTAA/RIchAKex13ZjZWC9xsJpZEktvBENFsQLsNPReqR
UZ8C
=TYsa
-----END PGP SIGNATURE-----
+22 -1
View File
@@ -9,6 +9,7 @@ repo_name: ""
#edit_uri: ""
theme:
name: material
locale: en
favicon: media/profile.png
icon:
logo: material/bird
@@ -17,6 +18,8 @@ theme:
text: Public Sans
code: Liberation Mono
features:
- navigation.instant
- navigation.instant.prefetch
- navigation.tabs
- navigation.sections
- toc.integrate
@@ -52,6 +55,9 @@ plugins:
# - git-authors: {}
# - git-latest-release: {}
extra_css:
- stylesheets/extra.css
extra:
social:
- icon: simple/mastodon
@@ -118,5 +124,20 @@ markdown_extensions:
permalink: true
toc_depth: 3
nav:
- Welcome: index.md
- About: about/index.md
- Verify: verify/index.md
- Guide:
- guide/index.md
- Code:
- code/index.md
- Develop: code/develop.md
- Contribute: contribute/index.md
- Constitution: constitution/index.md
- Mirrors: mirrors/index.md
- Twitter: twitter/index.md
- Releases: changelog/index.md
copyright: |
&copy; 2023-2025 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
<a href="https://anonymousplanet.org/">The Hitchhiker's Guide</a> ©2023-2026 by <a href="https://psa.anonymousplanet.org/">Anonymous Planet</a> is licensed under <a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC 4.0</a><img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"></a>
+247
View File
@@ -0,0 +1,247 @@
#!/usr/bin/env python3
"""Experimental dark mode support.
This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium.
Usage:
python scripts/build_guide_pdf.py # Generate light mode PDF only
python scripts/build_guide_pdf.py --dark-mode # Generate dark mode PDF only
python scripts/build_guide_pdf.py --both # Generate both light and dark mode PDFs
Examples:
python scripts/build_guide_pdf.py --site-dir build/html --pdf-light export/thgtoa.pdf
python scripts/build_guide_pdf.py --dark-mode --pdf-dark export/thgtoa-dark.pdf
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
import time
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def find_chromium_executable() -> Path | None:
if sys.platform == "win32":
paths = [
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Microsoft/Edge/Application/msedge.exe",
Path(os.environ.get("LOCALAPPDATA", "")) / "Microsoft/Edge/Application/msedge.exe",
Path(os.environ.get("PROGRAMFILES", "")) / "Google/Chrome/Application/chrome.exe",
Path(os.environ.get("PROGRAMFILES(X86)", "")) / "Google/Chrome/Application/chrome.exe",
Path(os.environ.get("LOCALAPPDATA", "")) / "Google/Chrome/Application/chrome.exe",
]
for p in paths:
if p.is_file():
return p
for name in ("chrome", "msedge"):
w = shutil.which(name)
if w:
return Path(w)
elif sys.platform == "darwin":
for p in (
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
):
if os.path.isfile(p):
return Path(p)
for name in ("google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome"):
w = shutil.which(name)
if w:
return Path(w)
return None
def run_mkdocs(site_dir: Path) -> None:
site_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(
[sys.executable, "-m", "mkdocs", "build", "-d", str(site_dir)],
cwd=repo_root(),
check=True,
)
def print_to_pdf(browser: Path, html_file: Path, pdf_out: Path, dark_mode: bool = False) -> Path:
"""Write PDF to ``pdf_out``. Uses a temp file first so an open ``guide.pdf`` on Windows
does not block the build: if the final path is locked, writes ``guide-new.pdf`` instead.
Args:
browser: Path to Chromium executable
html_file: Path to HTML file to convert
pdf_out: Output PDF path
dark_mode: If True, use dark mode color scheme via --prefers-color-scheme flag
"""
pdf_out.parent.mkdir(parents=True, exist_ok=True)
partial = pdf_out.parent / f".{pdf_out.name}.writing"
partial.unlink(missing_ok=True)
uri = html_file.resolve().as_uri()
# Chromium headless print; allow time for fonts/images on very large pages.
cmd = [str(browser)]
if os.environ.get("CI"):
# GitHub Actions / other CI runners often need these for Chromium to start.
cmd += [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
]
cmd += [
"--headless=new",
"--disable-gpu",
"--no-pdf-header-footer",
]
# Add dark mode preference if requested
if dark_mode:
cmd.append("--prefers-color-scheme=dark")
cmd += [
f"--print-to-pdf={partial.resolve()}",
uri,
]
subprocess.run(cmd, check=True, timeout=600)
deadline = time.time() + 120
while time.time() < deadline:
if partial.exists() and partial.stat().st_size > 0:
break
time.sleep(0.25)
else:
partial.unlink(missing_ok=True)
raise RuntimeError(f"PDF was not written to {partial}")
try:
if pdf_out.exists():
pdf_out.unlink()
except PermissionError:
fallback = pdf_out.with_name(f"{pdf_out.stem}-new{pdf_out.suffix}")
fallback.unlink(missing_ok=True)
partial.replace(fallback)
return fallback
partial.replace(pdf_out)
return pdf_out
def generate_dark_mode_html(html_file: Path, output_file: Path, dark_css_path: Path) -> None:
"""Create a temporary HTML file with dark mode stylesheet applied.
This is used when we need to force dark mode rendering via CSS rather than browser flags.
"""
try:
from bs4 import BeautifulSoup
# Read the original HTML
html_content = html_file.read_text(encoding='utf-8')
soup = BeautifulSoup(html_content, 'html.parser')
# Add dark mode stylesheet link if not present
existing_links = [link.get('href', '') for link in soup.find_all('link', rel='stylesheet')]
if not any(dark_css_path.name in link for link in existing_links):
head = soup.head or soup.new_tag('head')
link_tag = soup.new_tag('link', rel='stylesheet', href=str(dark_css_path))
if soup.head:
soup.head.append(link_tag)
else:
# Create a new head section
new_head = soup.new_tag('head')
new_head.append(link_tag)
soup.insert(0, new_head)
# Write the modified HTML
output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(str(soup), encoding='utf-8')
except ImportError:
print("BeautifulSoup not available. Skipping CSS injection.")
def main() -> int:
root = repo_root()
ap = argparse.ArgumentParser(description="Build MkDocs + single-page guide PDF (light and/or dark mode).")
ap.add_argument(
"--site-dir",
type=Path,
default=root / "build" / "html",
help="MkDocs output directory (default: ./build/html)",
)
ap.add_argument(
"--pdf-light",
type=Path,
default=root / "export" / "thgtoa.pdf",
help="Output PDF path for light mode (default: ./export/thgtoa.pdf)",
)
ap.add_argument(
"--pdf-dark",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
help="Output PDF path for dark mode (default: ./export/thgtoa-dark.pdf)",
)
ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.")
ap.add_argument("--dark-mode", action="store_true", help="Generate dark mode PDF only")
ap.add_argument("--both", action="store_true", help="Generate both light and dark mode PDFs")
args = ap.parse_args()
# Determine which modes to generate
if args.dark_mode:
modes = ["dark"]
elif args.both:
modes = ["light", "dark"]
else:
modes = ["light"]
guide_html = args.site_dir / "guide" / "index.html"
if not args.skip_mkdocs or any(mode == "light" for mode in modes):
run_mkdocs(args.site_dir)
if not guide_html.is_file():
print(f"Missing {guide_html}; run without --skip-mkdocs first.", file=sys.stderr)
return 1
browser = find_chromium_executable()
if not browser:
print(
"No Chromium-based browser found (Chrome, Edge, or Chromium). "
"Install Google Chrome or Microsoft Edge, or add Chromium to PATH.",
file=sys.stderr,
)
return 1
dark_css_path = root / "docs" / "stylesheets" / "dark-extra.css"
# Generate light mode PDF (default)
if "light" in modes:
out_light = print_to_pdf(browser, guide_html, args.pdf_light, dark_mode=False)
size_kb = out_light.stat().st_size // 1024
print(f"Wrote {out_light.resolve()} ({size_kb} KiB) [Light Mode]")
if out_light.resolve() != args.pdf_light.resolve():
print(
f"Note: {args.pdf_light.name} was in use; close it and rename or replace with the file above.",
file=sys.stderr,
)
# Generate dark mode PDF
if "dark" in modes:
out_dark = print_to_pdf(browser, guide_html, args.pdf_dark, dark_mode=True)
size_kb = out_dark.stat().st_size // 1024
print(f"Wrote {out_dark.resolve()} ({size_kb} KiB) [Dark Mode]")
if out_dark.resolve() != args.pdf_dark.resolve():
print(
f"Note: {args.pdf_dark.name} was in use; close it and rename or replace with the file above.",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+288
View File
@@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""Setup helper for PDF workflow configuration.
This script helps you configure the necessary GitHub Secrets for the automated
PDF build, signing, and VirusTotal scanning workflows.
Usage:
python scripts/setup_workflow.py
Requirements:
- Python 3.8+
- GPG installed (for key export)
- Access to GitHub repository settings
What it does:
1. Validates your GPG key setup
2. Exports the public key for verification
3. Provides instructions for adding secrets to GitHub
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def check_gpg_installed() -> bool:
"""Check if GPG is installed and accessible."""
try:
result = subprocess.run(
["gpg", "--version"],
capture_output=True,
text=True,
timeout=10,
)
return result.returncode == 0
except (FileNotFoundError, subprocess.TimeoutExpired):
return False
def list_gpg_keys() -> list[dict]:
"""List all GPG keys in the keyring."""
try:
result = subprocess.run(
["gpg", "--list-keys", "--with-colons"],
capture_output=True,
text=True,
check=True,
)
keys = []
current_key = {}
for line in result.stdout.split('\n'):
if line.startswith('pub:'):
if current_key:
keys.append(current_key)
parts = line.split(':')
current_key = {
'type': parts[1],
'key_id': parts[4],
'fingerprint': parts[9] if len(parts) > 9 else None,
'created': parts[5],
'expires': parts[6],
'uid': None,
}
elif line.startswith('uid:'):
parts = line.split(':')
current_key['uid'] = parts[9] if len(parts) > 9 else None
if current_key:
keys.append(current_key)
return keys
except subprocess.CalledProcessError as e:
print(f"Error listing GPG keys: {e}")
return []
def export_public_key(key_id: str, output_file: Path | None = None) -> str | None:
"""Export a public key in ASCII armor format."""
try:
result = subprocess.run(
["gpg", "--armor", "--export", key_id],
capture_output=True,
text=True,
check=True,
)
if output_file:
output_file.write_text(result.stdout)
print(f"✓ Public key exported to {output_file}")
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Error exporting public key: {e}")
return None
def export_private_key(key_id: str, output_file: Path | None = None) -> str | None:
"""Export a private key in ASCII armor format (requires passphrase)."""
try:
# This will prompt for passphrase interactively
result = subprocess.run(
["gpg", "--armor", "--export-secret-keys", key_id],
capture_output=True,
text=True,
check=True,
)
if output_file:
output_file.write_text(result.stdout)
print(f"✓ Private key exported to {output_file}")
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Error exporting private key: {e}")
return None
def validate_gpg_key(key_id: str) -> bool:
"""Validate that a GPG key has signing capability."""
try:
result = subprocess.run(
["gpg", "--list-keys", "--with-colons", key_id],
capture_output=True,
text=True,
check=True,
)
# Check for 's' (signing) in the pub line
for line in result.stdout.split('\n'):
if line.startswith('pub:'):
flags = line.split(':')[1]
return 's' in flags
return False
except subprocess.CalledProcessError:
return False
def print_setup_instructions():
"""Print instructions for configuring GitHub Secrets."""
print("\n" + "="*70)
print("GITHUB SECRETS SETUP INSTRUCTIONS")
print("="*70)
print("""
To enable the automated PDF workflow, you need to add three secrets to your
GitHub repository:
1. GPG_PRIVATE_KEY
- Your GPG private key in ASCII armor format
- Used to sign PDFs and hash files
- IMPORTANT: Keep this secret! Never commit it publicly
2. GPG_PASSPHRASE
- The passphrase for your GPG private key
- Required to unlock the private key for signing
3. VT_API_KEY (optional but recommended)
- VirusTotal API key for malware scanning
TROUBLESHOOTING:
- If GPG signing fails: Check that your key has signing capability ('s' flag)
- If passphrase is wrong: Verify you're using the correct passphrase
- If VT scan fails: Ensure API key is valid and within rate limits
""")
def main() -> int:
print("\n" + "="*70)
print("PDF WORKFLOW SETUP HELPER")
print("="*70)
# Check GPG installation
if not check_gpg_installed():
print("⚠ WARNING: GPG is not installed or not in PATH")
print("Please install GPG before continuing:")
print(" - Linux: sudo apt install gnupg")
print("\nContinuing anyway...")
# List available keys
print("\n🔑 Available GPG Keys:")
print("-" * 70)
keys = list_gpg_keys()
if not keys:
print("No GPG keys found in your keyring.")
print("Generate a key with: gpg --full-generate-key")
return 1
for i, key in enumerate(keys, 1):
status = "" if validate_gpg_key(key['key_id']) else ""
print(f"\n{i}. {status} Key ID: {key['key_id']}")
print(f" Fingerprint: {key.get('fingerprint', 'N/A')}")
print(f" UID: {key.get('uid', 'Unknown')}")
print(f" Created: {key.get('created', 'Unknown')}")
if key.get('expires'):
print(f" Expires: {key['expires']}")
# Ask user to select key
print("\n" + "-" * 70)
try:
choice = input("\nEnter the number of the key you want to use (1-{}): ".format(len(keys)))
selected_index = int(choice) - 1
if not (0 <= selected_index < len(keys)):
print("Invalid selection!")
return 1
except ValueError:
print("Invalid input! Please enter a number.")
return 1
selected_key = keys[selected_index]
# Validate key has signing capability
if not validate_gpg_key(selected_key['key_id']):
print(f"\n⚠ WARNING: Selected key does not have signing capability!")
print("You need a key with 's' (signing) flag for PDF signatures.")
confirm = input("Continue anyway? (y/N): ")
if confirm.lower() != 'y':
return 1
# Export public key
print(f"\n📤 Exporting public key for {selected_key['uid']}...")
public_key_file = repo_root() / "pgp" / "workflow-public.asc"
public_key = export_public_key(selected_key['key_id'], public_key_file)
if not public_key:
print("Failed to export public key!")
return 1
# Show public key info
print("\n✓ Public Key Information:")
print("-" * 70)
for line in public_key.split('\n')[:5]:
print(line)
print("...")
# Instructions for private key export
print("\n🔐 Private Key Export:")
print("-" * 70)
print("""
To get your private key for the GPG_PRIVATE_KEY secret:
1. Run this command (you'll be prompted for passphrase):
gpg --armor --export-secret-keys {} > workflow-private.asc
2. Copy the ENTIRE output including BEGIN and END lines
3. Add it to GitHub Secrets as 'GPG_PRIVATE_KEY'
⚠ IMPORTANT: Keep your private key secure! Never commit it publicly.
""".format(selected_key['key_id']))
# Print setup instructions
print_setup_instructions()
print("\n" + "="*70)
print("SETUP COMPLETE!")
print("="*70)
print(f"\nPublic key saved to: {public_key_file}")
print("Next steps:")
print("1. Export your private key (see instructions above)")
print("2. Add all three secrets to GitHub repository settings")
print("3. Test the workflow by triggering a manual build")
print("\nFor more information, see: docs/guide/dev-workflow.md\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""Verification script for PDF files.
This script verifies:
1. SHA256 hash integrity of PDF files
2. GPG signature authenticity
3. VirusTotal scan status (optional)
Usage:
python scripts/verify_pdf.py --all # Verify everything
python scripts/verify_pdf.py --hashes # Only verify hashes
python scripts/verify_pdf.py --signatures # Only verify signatures
python scripts/verify_pdf.py --vt # Check VT status (requires API key)
Examples:
python scripts/verify_pdf.py --all
python scripts/verify_pdf.py --hashes --file export/thgtoa.pdf
"""
from __future__ import annotations
import argparse
import hashlib
import os
import subprocess
import sys
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def calculate_sha256(file_path: Path) -> str:
"""Calculate SHA256 hash of a file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def verify_hash(file_path: Path, expected_hash: str) -> bool:
"""Verify file hash against expected value."""
actual_hash = calculate_sha256(file_path)
is_valid = actual_hash == expected_hash
status = "✓ PASS" if is_valid else "✗ FAIL"
print(f"{status}: {file_path.name}")
print(f" Expected: {expected_hash}")
print(f" Actual: {actual_hash}")
return is_valid
def verify_signature(file_path: Path, sig_file: Path) -> bool:
"""Verify GPG signature of a file."""
if not sig_file.exists():
print(f"✗ FAIL: Signature file not found: {sig_file}")
return False
try:
result = subprocess.run(
["gpg", "--verify", str(sig_file), str(file_path)],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
print(f"✓ PASS: {file_path.name} signature verified")
# Extract key info from GPG output
for line in result.stdout.split('\n'):
if 'Good signature' in line or 'key ID' in line.lower():
print(f" {line.strip()}")
return True
else:
print(f"✗ FAIL: {file_path.name} signature verification failed")
print(f" Error: {result.stderr}")
return False
except FileNotFoundError:
print("⚠ WARNING: GPG not installed. Skipping signature verification.")
return None
def verify_from_hash_file(file_path: Path, hash_file: Path) -> bool:
"""Verify file hash from a hash file."""
if not hash_file.exists():
print(f"✗ FAIL: Hash file not found: {hash_file}")
return False
expected_hash = None
with open(hash_file, 'r') as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2 and parts[1] == str(file_path):
expected_hash = parts[0]
break
if not expected_hash:
print(f"✗ FAIL: Hash not found in {hash_file.name} for {file_path.name}")
return False
return verify_hash(file_path, expected_hash)
def check_virustotal(file_hash: str, api_key: str | None = None) -> dict | None:
"""Check VirusTotal scan status for a file hash."""
if not api_key:
print("⚠ WARNING: VT_API_KEY not set. Skipping VirusTotal check.")
return None
try:
import urllib.request
import json
url = f"https://www.virustotal.com/api/v3/files/{file_hash}"
request = urllib.request.Request(url, headers={"x-apikey": api_key})
with urllib.request.urlopen(request, timeout=30) as response:
data = json.loads(response.read().decode())
stats = data.get('data', {}).get('attributes', {}).get('last_analysis_stats', {})
total = sum(stats.values()) if stats else 0
print(f"\n🦠 VirusTotal Results for {file_hash[:16]}...")
print(f" Total scans: {total}")
if stats:
print(f" Malicious: {stats.get('malicious', 0)}")
print(f" Suspicious: {stats.get('suspicious', 0)}")
print(f" Undetected: {stats.get('undetected', 0)}")
print(f" Clean: {stats.get('harmless', 0)}")
return data
except Exception as e:
print(f"⚠ ERROR checking VirusTotal: {e}")
return None
def main() -> int:
root = repo_root()
ap = argparse.ArgumentParser(description="Verify PDF files (hashes, signatures, VT).")
# File paths
ap.add_argument(
"--light-pdf",
type=Path,
default=root / "export" / "thgtoa.pdf",
help="Light mode PDF file",
)
ap.add_argument(
"--dark-pdf",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
help="Dark mode PDF file",
)
ap.add_argument(
"--hash-file",
type=Path,
default=root / "export" / "thgtoa.pdf.sha256",
help="Hash file to verify against",
)
# Verification modes
group = ap.add_mutually_exclusive_group()
group.add_argument("--all", action="store_true", help="Verify everything")
group.add_argument("--hashes", action="store_true", help="Only verify hashes")
group.add_argument("--signatures", action="store_true", help="Only verify signatures")
ap.add_argument("--vt", action="store_true", help="Check VirusTotal status")
args = ap.parse_args()
# Determine what to verify
if not any([args.all, args.hashes, args.signatures, args.vt]):
args.all = True
all_passed = True
pdf_files = [
("Light", args.light_pdf),
("Dark", args.dark_pdf),
]
for mode_name, pdf_file in pdf_files:
if not pdf_file.exists():
print(f"⚠ WARNING: {pdf_file.name} not found. Skipping.")
continue
print(f"\n{'='*60}")
print(f"Verifying {mode_name} PDF: {pdf_file.name}")
print('='*60)
# Verify hash if requested
if args.all or args.hashes:
if not verify_from_hash_file(pdf_file, args.hash_file):
all_passed = False
# Verify signature if requested
if args.all or args.signatures:
sig_file = pdf_file.with_suffix(pdf_file.suffix + ".sig")
result = verify_signature(pdf_file, sig_file)
if result is False: # None means skipped (GPG not installed)
all_passed = False
# Check VirusTotal if requested
if args.all or args.vt:
file_hash = calculate_sha256(pdf_file)
api_key = os.environ.get("VT_API_KEY")
check_virustotal(file_hash, api_key)
print(f"\n{'='*60}")
if all_passed:
print("✓ All verifications PASSED")
return 0
else:
print("✗ Some verifications FAILED")
return 1
if __name__ == "__main__":
raise SystemExit(main())