62 Commits

Author SHA1 Message Date
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
nopeitsnothing 783f02f404 services: archive Matrix database and shutdown
We are temporarily disabling the Matrix homeserver at anonymousplanet
dot net due to the inability to keep up with infrastructure costs. This
is the last attempt we will make until we know the services will be more
reliable and we apologize for the inconvenience and instability. This is
a joint decision caused by late payments to our hosting provider and we
decided it best to simply leave things down and focus on other matters.
We never really wanted to make a big deal about servers and services on
them, it was mostly about community and chatting with like-minded
individuals. We'll be around on Matrix still, just reach out to us if
you have questions or comments.

(Matrix) nope: thehidden at tchncs dot de
(Matrix) daskolburn: daskolburn at thomcat dot rocks

Our email remains the same: contact at anonymousplanet dot org

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-01-18 21:52:46 -05:00
nopeitsnothing 6c8dba5d5f The Tor onion v3 address works
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-29 23:30:21 -05:00
nopeitsnothing ee32450516 Merge branch 'fix-broken-markdown-links'
5745d29b - amend previous commit, fixing another found markdown link
that was broken.
2025-12-29 22:37:34 -05:00
nopeitsnothing c7452ea796 Fix
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-29 22:02:53 -05:00
nopeitsnothing 3621967517 Revert "Fix some metadata"
This reverts commit 6448e2b786.
2025-12-29 21:50:00 -05:00
nopeitsnothing 88896a4f15 File endings
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-29 21:47:32 -05:00
nopeitsnothing 14de26d77d Missing parenthesis
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 01:45:28 -05:00
nopeitsnothing 3b430dc96a Extra parenthesis
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 01:43:57 -05:00
nopeitsnothing 565f3b8516 Creating your anonymous online identities
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 01:42:23 -05:00
nopeitsnothing fd60ef8460 Traffic anonymization
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 01:39:44 -05:00
nopeitsnothing fb5e1fca74 OPSEC thoughts
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 01:31:47 -05:00
nopeitsnothing 02764539f2 Watermarking
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 01:30:33 -05:00
nopeitsnothing 2e0b7a9716 Browser and device fingerprinting
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 01:28:29 -05:00
nopeitsnothing 239d1c632f Requirements refs
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:24:53 -05:00
nopeitsnothing c76ccd3e43 Local data leaks, forensics
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:21:07 -05:00
nopeitsnothing 37068765cc Whonix virtual machines
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:19:45 -05:00
nopeitsnothing 2c3dea5f41 Some more
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:18:16 -05:00
nopeitsnothing d623dda610 Adversarial considerations (threats)
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:15:40 -05:00
nopeitsnothing 8e386addb8 Some more
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:13:35 -05:00
nopeitsnothing 7b3599df63 Some Tails refs
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:09:11 -05:00
nopeitsnothing ac870b1497 Some additional measures against forensics refs
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:03:46 -05:00
nopeitsnothing 88cee5a3c0 Comparing versions ref
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-22 00:00:58 -05:00
nopeitsnothing 56678b3567 Persistent plausible deniability
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-21 23:57:18 -05:00
nopeitsnothing 3b1c5946ae All refs I have time for at the moment
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-21 22:03:26 -05:00
nopeitsnothing 6448e2b786 Fix some metadata
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-16 12:51:03 -05:00
nopeitsnothing 957da7e2af Update admin info in Matrix listing
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-12-03 16:48:02 -05:00
nopeitsnothing 6c4fd4a5c0 Fix some metadata
- remove some old references to "files" we don't host.
- the guide is not available in PDF in favor of a modern website.
- the website can be cloned and redistributed using various tools.

b34f4e1464e1f30fee960b50895b85ecb9e5eb6e2e33d97f1b3da730553176ad0248f437220dcb9628cd5a93ec809b9625e8e60eba4dab13efa002f7f4c6273a

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-10-30 09:06:23 -04:00
nopeitsnothing 2f5254a9fb Fix discussion channels
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-10-07 19:04:29 -04:00
nopeitsnothing 23f2acb788 Fix nope's Matrix pushed to only one repo
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-10-03 16:14:29 -04:00
nopeitsnothing 2bfa84de28 Fix Das' Matrix pushed to only one repo
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-10-03 16:10:10 -04:00
nopeitsnothing 9b98497ed3 chore(commit): remove commitizen requirement
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-10-03 16:05:58 -04:00
nopeitsnothing d212e86012 chore(crypto): remove than/nope signed canary
I will no longer include my canary here, please see:
https://itsnothing.net/crypto.txt and https://itsnothing.net/pgp.txt

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2025-10-03 15:43:29 -04:00
nopeitsnothing 621a6ae3d0 chore(pre-commit): upgrade pre-commit hooks 2025-10-03 15:39:16 -04:00
nopeitsnothing 377e8ecad7 docs(link): Wikiless.org --> Wikiless.com 2025-08-05 03:35:19 -04:00
nopeitsnothing 2c35d7ced8 gpg(than): minor updated information
Cryptographic, canary, etc.
2025-07-19 03:18:19 -04:00
25 changed files with 2560 additions and 900 deletions
-10
View File
@@ -1,10 +0,0 @@
[tool.commitizen]
bump_message = "release $current_version → $new_version []"
annotated_tag = true
version = "1.2.1"
update_changelog_on_bump = false
gpg_sign = true
allow_abort = true
version_files = [
"_config.yml:version"
]
+146
View File
@@ -0,0 +1,146 @@
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 PDFs
env:
CI: true
run: python scripts/build_guide_pdf.py --${{ inputs.build_mode || 'both' }}
- name: 🔒 Sign SHA256 hash file with GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
cd ${{ github.workspace }}
# Import GPG key
export GPG_TTY=$(tty)
echo "$GPG_KEY" | gpg --batch --import 2>/dev/null || true
- name: 🔒 Sign PDF files with GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
cd ${{ github.workspace }}
# Import GPG key if not already imported
export GPG_TTY=$(tty)
echo "$GPG_KEY" | gpg --batch --import 2>/dev/null || true
# Create combined hash file with all PDFs
sha256sum export/thgtoa.pdf > export/checksums.sha256
sha256sum export/thgtoa-dark.pdf >> export/checksums.sha256
# Sign the checksum file
gpg --batch --yes --armor --detach-sign --output export/checksums.sha256.sig export/checksums.sha256 2>/dev/null || true
# Sign each PDF file individually with detached signature
for pdf_file in export/*.pdf; do
if [ -f "$pdf_file" ]; then
base_name=$(basename "$pdf_file")
echo "Signing $base_name..."
gpg --default-key 17ECA05F768DEDF6 --batch --yes --armor --detach-sign --output "export/${pdf_file}.sig" "$pdf_file" 2>/dev/null || true
fi
done
# Verify signatures were created
ls -la export/*.sig 2>/dev/null || echo "No signature files found in export/"
- 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)
- **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: pdf-export-${{ inputs.build_mode || 'both' }}
path: |
export/*.pdf
export/*.sig
export/*.sha256
export/virus-total-results.md
if-no-files-found: error
retention-days: 90
compression-level: 0
+1 -1
View File
@@ -1,4 +1,4 @@
name: Publish docs via GitHub Pages
name: 🚀 Publish docs via GitHub Pages
on:
push:
branches:
+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
+1 -5
View File
@@ -5,7 +5,7 @@ default_install_hook_types: [pre-commit, commit-msg]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-merge-conflict
@@ -21,7 +21,3 @@ repos:
hooks:
- id: markdownlint
- id: markdownlint-fix
- hooks:
- id: commitizen
repo: https://github.com/commitizen-tools/commitizen
rev: v3.28.0
+51
View File
@@ -0,0 +1,51 @@
# Changelog
All notable changes to this project will be documented in this file.
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).
## [Unreleased]
### Added
- Add ways to verify the files
### Changed
- Refactored GitHub Actions workflow **Build PDF** (`scripts\build_guide_pdf.py`): now builds both light and dark mode PDFs (`export/thgtoa.pdf` and `export/thgtoa-dark.pdf` respectively).
- Restored previous VT scans workflow **VirusTotal Scan** (`.github/workflows/vt-scan.yml`): submit files to VT for malware scanning. Links will be published on the site.
## 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
### Feature
- Updated `scripts/build_guide_pdf.py` to use `--print-to-pdf` instead of `--save-as` for PDF generation, and added a new `--dark-mode` flag to generate dark mode PDFs. The script now supports generating both light and dark mode PDFs with a single command invocation by using the `--both` flag. This change improves the PDF generation process and provides better support for dark mode users. Save your eyes - you only get one pair.
## [1.2.1] - 2026-04-11
### 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`.
### 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.
### 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
[1.2.1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.1
+4 -38
View File
@@ -2,47 +2,13 @@ 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.org/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 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 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:
- Tor Onion Mirror: <http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/>
The guide and all the files are also readily available on Archive.org and Archive.today:
- Archive.org: <https://web.archive.org/web/https://anonymousplanet.org/>
- Archive.today: <https://archive.ph/anonymousplanet.org/>
- Archive.today over Tor: <http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/anonymousplanet.org/>
If you want to access/see the original/legacy project, please see the [legacy resources](legacy.html) page.
If you want to see the changes between your PDF and the latest PDF, you could use one of these tools (we do not endorse those):
- <https://tools.pdf24.org/en/compare-pdf>
- <https://products.aspose.app/pdf/comparison>
- <https://draftable.com/compare>
If you want to compare an older ODT file with a newer one, 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/wen/https://help.libreoffice.org/7.1/en-US/text/shared/guide/redlining_doccompare.html)</sup>)
**If you want to check the files for integrity, safety, authenticity, please refer to this ["How To"](verify.html).**
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: `#anonymity:matrix.org` <https://matrix.to/#/#anonymity:matrix.org>
- Matrix space: `#privacy-security-anonymity:matrix.org` <https://matrix.to/#/#privacy-security-anonymity:matrix.org>
- Twitter at https://twitter.com/AnonyPla
- Mastodon at https://mastodon.social/@anonymousplanet
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`).
+9 -8
View File
@@ -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.
@@ -35,7 +35,7 @@ This guide is a non-profit open-source initiative, licensed under Creative Commo
- For mirrors see [Mirrors](../mirrors/index.md) and the links at the bottom right of the page. You should see these on every page.
- For help in comparing versions see [Comparing versions](../guide/index.md#appendix-a7-comparing-versions)
- For help in comparing versions see [Comparing versions](../guide/index.md#appendix-a6-comparing-versions)
Feel free to submit issues **(please do report anything wrong)** using GitHub Issues at: <https://github.com/Anon-Planet/thgtoa/issues>. We also accept Merge Requests (MR) from our Gitlab and many other places. Do not hesitate to report issues and suggestions!
@@ -43,9 +43,10 @@ Feel free to submit issues **(please do report anything wrong)** using GitHub Is
We offer a Matrix.org hosted space of our own. Check it out!
- Read [the rules](https://anonymousplanet.org/chatrooms-rules.html), please
- Matrix Room: [#anonymity:matrix.org](https://matrix.to/#/#anonymity:matrix.org)
- Matrix Space: [#privacy-security-anonymity:matrix.org](https://matrix.to/#/#privacy-security-anonymity:matrix.org)
- Read [the rules](https://psa.anonymousplanet.org/), please
- Matrix Room: https://matrix.to/#/#nth:anonymousplanet.net
- Matrix Space: https://matrix.to/#/#psa:anonymousplanet.net
- Admins: @daskolburn:thomcat.rocks and @thehidden:tchncs.de
Follow us on:
@@ -94,9 +95,9 @@ _Anonymous Planet_ **does not** participate in any sponsoring, endorsement, adve
??? Note "Useful resources"
- KYC? Not me: <https://kycnot.me/>
- Library Genesis: <https://en.wikipedia.org/wiki/Library_Genesis> <sup>[[Wikiless]](https://wikiless.org/wiki/Library_Genesis)</sup> (see their latest known URL in the Wikipedia article)
- Library Genesis: <https://en.wikipedia.org/wiki/Library_Genesis> <sup>[[Wikiless]](https://wikiless.com/wiki/Library_Genesis)</sup> (see their latest known URL in the Wikipedia article)
- Real World Onion Sites: <https://github.com/alecmuffett/real-world-onion-sites>
- Sci-Hub <https://en.wikipedia.org/wiki/Sci-Hub> <sup>[[Wikiless]](https://wikiless.org/wiki/Sci-Hub)</sup> (see their latest known URL in the main Wikipedia article)
- Sci-Hub <https://en.wikipedia.org/wiki/Sci-Hub> <sup>[[Wikiless]](https://wikiless.com/wiki/Sci-Hub)</sup> (see their latest known URL in the main Wikipedia article)
- Terms of Service, Didn't Read: <https://tosdr.org>
- Whonix Documentation: <https://www.whonix.org/wiki/Documentation>
@@ -104,7 +105,7 @@ _Anonymous Planet_ **does not** participate in any sponsoring, endorsement, adve
One or two of our community members uses or has used the resources of Riseup. We are not affiliated with Riseup in any manner.
We also hold **no affiliation** with the [Anonymous](https://en.wikipedia.org/wiki/Anonymous_(hacker_group)) <sup>[[Wikiless]](https://wikiless.org/wiki/Anonymous_(hacker_group))</sup> <sup>[[Archive.org]](https://web.archive.org/web/https://en.wikipedia.org/wiki/Anonymous_(hacker_group))</sup> hacker collective.
We also hold **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> hacker collective.
## License
+193
View File
@@ -0,0 +1,193 @@
# Development
## Overview
This repository now includes an automated workflow that handles PDF generation, verification, and distribution with the following features:
??? Note "How the pipeline works"
1. **Automatic PDF Generation** - Builds both light and dark mode PDFs from MkDocs source
2. **SHA256 Hash Generation** - Creates hash files for integrity verification
3. **GPG Signature Signing** - Signs all PDFs and hash files with repository GPG key
4. **VirusTotal Scanning** - Automatically scans PDFs and updates release notes
5. **Release Automation** - Packages everything into GitHub releases
## Workflow Architecture
### 1. Build PDF Workflow (`build-pdf.yml`)
**Trigger:** Push to main, pull requests, or manual dispatch
??? Note "Steps"
- Checkout repository
- Set up Python 3.13 and MkDocs Material
- Install Chromium browser
- Generate both light and dark mode PDFs
- Create SHA256 hash files
- Sign all files with GPG
- Upload artifacts to GitHub Actions
- Publish release
### 2. VirusTotal Scan Workflow (`vt-scan.yml`)
**Trigger:** Push to main, tags, or manual dispatch (runs after build-pdf)
??? Note "Steps"
- Download PDF artifacts from build workflow
- Scan both PDFs with VirusTotal API
- Extract scan results and generate report links
- Update release notes with VT scan status and URLs
## File Structure
After a successful build, the repository will contain:
```
.../
├── export/
│ ├── thgtoa.pdf # Light mode PDF
│ ├── thgtoa-dark.pdf # Dark mode PDF
│ ├── thgtoa.pdf.sig # GPG signature (light)
│ └── thgtoa-dark.pdf.sig # GPG signature (dark)
├── thgtoa.pdf.sha256 # Hash file (light)
├── thgtoa-dark.pdf.sha256 # Hash file (dark)
├── sha256sum-light.txt # Combined hash file
└── scripts/
├── build_guide_pdf.py # PDF generation script
└── verify_pdf.py # Verification utility
```
## Security Features
### 1. SHA256 Hash Verification
**Purpose:** Ensure file integrity during download/transit
**How it works:**
- Each PDF gets a unique SHA256 hash calculated at build time
- Hash stored in `.sha256` files alongside the PDFs
- Combined `sha256sum-light.txt` for batch verification
**Verification command:**
```bash
sha256sum -c sha256sum-light.txt
```
### 2. 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
```
### 3. VirusTotal Integration
**Purpose:** Malware detection and security scanning
??? Note "How it works"
- Automatic scan of all generated PDFs
- Results published in release notes with direct links
- Provides third-party validation of file safety
## Usage Examples
### Local Development
```bash
# Build PDFs locally
python scripts/build_guide_pdf.py --both
# Verify hashes
python scripts/verify_pdf.py --hashes
# Verify signatures (requires GPG installed)
python scripts/verify_pdf.py --signatures
# Full verification with VirusTotal check
export VT_API_KEY=your_api_key
python scripts/verify_pdf.py --all
```
### CI/CD Verification
The workflows automatically verify everything during the build process. To manually trigger:
1. Go to Actions tab
2. Select "Build guide PDF" or "VirusTotal Scan"
3. Click "Run workflow"
4. Download artifacts from successful run
## Release Process
When you create a tag (e.g., `v1.0.0`):
1. Push the tag: `git push origin v1.0.0`
2. Build PDF workflow triggers automatically
3. VirusTotal scan workflow runs after build completes
4. Both workflows update/create GitHub release with:
- Light and dark mode PDFs
- GPG signatures for all files
- Hash files for verification
- Release notes with VT scan results
## Troubleshooting
### Common Issues
**GPG signing fails:**
- Check that `GPG_PRIVATE_KEY` is in ASCII armor format
- Verify passphrase is correct
- Ensure key has signing capability
**Hash mismatch after download:**
- Re-download the file (corruption during transfer)
- Verify you're using the correct hash file
- Check disk integrity
**VirusTotal scan fails:**
- Verify `VT_API_KEY` is set correctly
- Check API quota limits (free tier: 4 requests/minute)
- Ensure PDF files exist before scanning
### Debug Mode
Enable verbose output by adding to workflow:
```yaml
- name: Debug
run: |
echo "Current directory:" && pwd
echo "Files in export:" && ls -la export/
echo "Hash file contents:" && cat sha256sum-light.txt
```
## Best Practices
1. **Always verify signatures** before opening PDFs from untrusted sources
2. **Check VirusTotal results** for any suspicious detections
3. **Keep GPG keys secure** - never commit private keys to repository
4. **Monitor API usage** for VirusTotal to avoid rate limiting
5. **Test locally** before pushing tags to production
## Future Enhancements
Potential improvements:
- Multi-signature support (multiple maintainers)
- Automated changelog generation with hashes
- Cross-platform signature verification scripts
- Integration with additional malware scanners
- Automatic mirror updates with verified files
---
*This workflow is designed for security-conscious users who need to verify the authenticity and integrity of downloaded documents.*
+404 -399
View File
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -13,19 +13,24 @@ schema:
- 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**
This is the master signing key fingerprint for Anonymous Planet.
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. We have moved away from Matrix. Please avoid it as it contains harmful (illegal) spam. We are not moderating the rooms in PSA at present. It is likely that we will see a complete move away from Matrix in the future.
![Anonymous Planet logo](media/profile.png){ align=right }
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.
??? person "Das Kolburn"
- [:simple-github: GitHub](https://github.com/NobodySpecial256 "@NobodySpecial256")
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
- [:simple-matrix: Matrix](https://matrix.to/#/@memorysafetybelike:envs.net)
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@daskolburn:thomcat.rocks "@daskolburn:thomcat.rocks"), [:simple-matrix: Org Matrix](https://matrix.to/#/@daskolburn:anonymousplanet.net "@daskolburn:anonymousplanet.net")
??? person "Nope"
@@ -33,5 +38,4 @@ Anonymous Planet is a collective of volunteers and contributors. No one person i
- [:simple-mastodon: Mastodon](https://ioc.exchange/@unknown "@unknown@ioc.exchange"){rel=me}
- [:fontawesome-solid-house: Homepage](https://www.itsnothing.net)
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
- [:simple-matrix: Matrix](https://matrix.to/#/@thehidden:tchncs.de)
- [:fontawesome-solid-shield: Canary](https://itsnothing.net/canary.txt)
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@thehidden:tchncs.de "@thehidden:tchncs.de"), [:simple-matrix: Org Matrix](https://matrix.to/#/@nope:anonymousplanet.net "@nope:anonymousplanet.net")
+7 -3
View File
@@ -20,12 +20,16 @@ schema:
!!! Note "Where to find the Hitchhiker's Guide"
- [Original](https://anonymousplanet.org)
<!-- - (offline) [Tor Onion Mirror](http://thgtoa27ujspeqxasrfvcf5aozqdczvgmwgorrmblh6jn4nino3spcqd.onion) -->
- [Tor v3](http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion) **Down**
- [Archive.org](https://web.archive.org/web/https://anonymousplanet.org)
- [Archive.today](https://archive.fo/anonymousplanet.org)
- [Archive.today over Tor](http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/anonymousplanet.org)
<!-- - (n/a) [PDF](https://anonymousplanet.org/export/guide.pdf) <sup>[[Archive.org]](https://web.archive.org/web/https://anonymousplanet.org/export/guide.pdf)</sup> <sup>[[Tor Mirror]](http://thgtoa27ujspeqxasrfvcf5aozqdczvgmwgorrmblh6jn4nino3spcqd.onion/export/guide.pdf)</sup> -->
<!-- - (n/a) [ODT](https://anonymousplanet.org/export/guide.odt) <sup>[[Archive.org]](https://web.archive.org/web/https://anonymousplanet.org/export/guide.odt)</sup> <sup>[[Tor Mirror]](http://thgtoa27ujspeqxasrfvcf5aozqdczvgmwgorrmblh6jn4nino3spcqd.onion/export/guide.odt)</sup> -->
!!! 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-pdf.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"
+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;
}
}
+123
View File
@@ -0,0 +1,123 @@
---
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 [date]
gpg: using RSA key [key-id]
gpg: Good signature from "[owner]"
```
#### 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 Key
**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.
BIN
View File
Binary file not shown.
+19 -6
View File
@@ -52,14 +52,14 @@ plugins:
# - git-authors: {}
# - git-latest-release: {}
extra_css:
- stylesheets/extra.css
extra:
social:
- icon: simple/mastodon
link: https://mastodon.social/@anonymousplanet
name: Mastodon
# - icon: simple/matrix
# link: https://matrix.to/#/#p-s-a:matrix.org
# name: Matrix Space
- icon: simple/gitlab
link: http://wmj5kiic7b6kjplpbvwadnht2nh2qnkbnqtcv3dyvpqtz7ssbssftxid.onion/
name: "0xacab"
@@ -76,7 +76,7 @@ extra:
link: https://codeberg.org/anonymousplanet
name: Codeberg
# - icon: simple/torbrowser
# link: TODO
# link: http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/
# name: Hidden service
markdown_extensions:
@@ -98,7 +98,7 @@ markdown_extensions:
custom_fences:
- name: mermaid
class: mermaid
format: materialx.superfences.fence_code_format
format: !!python/name:pymdownx.superfences.fence_code_format
- pymdownx.tabbed:
alternate_style: true
@@ -121,5 +121,18 @@ markdown_extensions:
permalink: true
toc_depth: 3
nav:
- Home: index.md
- About: about/index.md
- Verify: verify/index.md
- Guide:
- guide/index.md
- Workflow Documentation: guide/dev-workflow.md
- Code: code/index.md
- Contribute: contribute/index.md
- Constitution: constitution/index.md
- Mirrors: mirrors/index.md
- Twitter: twitter/index.md
copyright: |
&copy; 2023-2025 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
&copy; 2023-2026 <a href="https://anonymousplanet.org/" target="_blank" rel="noopener">Anonymous Planet</a>
+13 -3
View File
@@ -1,13 +1,23 @@
# Import
```
```bash
$ gpg --import pgp/core-devs/*
```
# Verify
TODO
```bash
$ gpg --verify pgp/core-devs/than/than-crypto.txt
### All signing keys are signed by the Master Signing Key
gpg: Signature made Sat 19 Jul 2025 02:04:10 AM EDT
gpg: using EDDSA key 8B3A74890536BAD50D9376EBF1CB32F67E3302A1
gpg: Good signature from "nopenothinghere@proton.me <nopenothinghere@proton.me>" [ultimate]
gpg: aka "Nope Nothing (Anonymous Planet Contact) <no@anonymousplanet.org>" [ultimate]
gpg: aka "Nope Nothing (Systems Administrator) <admin@itsnothing.net>" [ultimate]
Primary key fingerprint: 8B3A 7489 0536 BAD5 0D93 76EB F1CB 32F6 7E33 02A1
```
## All signing keys are signed by the Master Signing Key
TODO
-80
View File
@@ -1,80 +0,0 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
Tue Jul 25 14:51:36 EDT 2023
I am the admin of itsnothing.net (@Unknown@ioc.exchange) and co-admin of THGTOA.
I will update this canary within 1 month.
Latest bitcoin block hash:
00000000000000000000d9330bf8a03ce70cbe5542bddd16558693a43ea32fd3
I am in complete control of all my key material.
All previous keys have been revoked as part of standard OPSEC key rotation procedures.
Do not encrypt communications to my old keys, I will not read them.
The key currently published on my website https://itsnothing.net/pgp.txt with a fingerprint
of C87D87466FD205945CF10A3821AB6B6A6CB2C337, is my only PGP key for public communication.
Permanent record of old and new PGP keys:
the old key was:
pub rsa4096/0xB208C4084A2C582D 2022-11-04 [SC] [expires: 2027-11-03]
Key fingerprint = D793 9998 F78B ADB5 18C1 B600 B208 C408 4A2C 582D
uid [ultimate] Nope <no@anonymousplanet.org>
And the new key is:
pub ed25519/0x21AB6B6A6CB2C337 2023-07-14 [SC]
Key fingerprint = C87D 8746 6FD2 0594 5CF1 0A38 21AB 6B6A 6CB2 C337
uid [ultimate] nopenothinghere@proton.me <nopenothinghere@proton.me>
To fetch the full key, you can simply do:
gpg --keyserver keys.openpgp.org --recv-key 0x21AB6B6A6CB2C337
**
Note: this keyserver is experimental.[0] I still have yet to add this key to
the I2P keyserver pool, and I don't know if I will. If you have previously
signed my key but did a local-only signature (lsign), you will not want to
issue the following, instead you will want to use --lsign-key, and not send
the signatures to the keyserver.
**
gpg --sign-key 0x21AB6B6A6CB2C337
I'd like to receive your signatures on my key. You can either send me an e-mail
with the new signatures (if you have a functional MTA on your system):
gpg --export 0x21AB6B6A6CB2C337 | gpg --encrypt -r 0x21AB6B6A6CB2C337 --armor \
| mail -s 'OpenPGP Signatures' <nopenothinghere@proton.me>
Additionally, I highly recommend that you implement a mechanism to keep your key
material up-to-date so that you obtain the latest revocations, and other updates
in a timely manner. You can do regular key updates by using parcimonie[1] to
refresh your keyring. Parcimonie is a daemon that slowly refreshes your keyring
from a keyserver over Tor. It uses a randomized sleep, and fresh tor circuits
for each key. The purpose is to make it hard for an attacker to correlate the
key updates with your keyring.
I also highly recommend checking out the excellent Riseup GPG best practices
doc, from which I stole most of the text for this transition message ;-)
https://we.riseup.net/riseuplabs+paow/openpgp-best-practices
Please let me know if you have any questions, or problems, and sorry for the
inconvenience.
Nope (Anonymous Planet) <no@anonymousplanet.org>
0. https://gist.github.com/rjhansen/67ab921ffb4084c865b3618d6955275f
1. https://directory.fsf.org/wiki/Parcimonie
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQTIfYdGb9IFlFzxCjghq2tqbLLDNwUCZMAZwAAKCRAhq2tqbLLD
N3l3AQC28SZK5HHU1o7K36ifOd/OKj97urrMZF+NUkaRmAwQxgEAlIa2y9g0JoQW
epEpViXFDwyWIUfNhVaJwUWjn/DLoAI=
=A72C
-----END PGP SIGNATURE-----
+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/guide.pdf)",
)
ap.add_argument(
"--pdf-dark",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
help="Output PDF path for dark mode (default: ./export/guide-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())
+365
View File
@@ -0,0 +1,365 @@
!/bin/bash
set -e
# PDF Hashing, Scanning, Release and Management script (hSCRAM)
# Usage: ./pdf_release.sh --build <light|dark|both> --release <tag|latest> [--vt-api-key]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
EXPORT_DIR="$ROOT_DIR/export"
# Default values
MODE="both" # light, dark, or both
RELEASE_MODE="tag" # tag (e.g. "v2.1.2") or "latest"
VT_API_KEY=""
GITHUB_TOKEN=""
while [[ $# -gt 0 ]]; do
case $1 in
--build)
MODE="$2"
shift 2
;;
--release)
RELEASE_MODE="$2"
shift 2
;;
--vt-api-key)
VT_API_KEY="$2"
shift 2
;;
--github-token)
GITHUB_TOKEN="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
if [[ ! "$MODE" =~ ^(light|dark|both)$ ]]; then
echo "Error: Invalid build mode '$MODE'. Must be 'light', 'dark', or 'both'."
exit 1
fi
echo "Hashing, Scanning, Release and Management script (hSCRAM)"
echo "Mode: $MODE"
echo "Release Mode: $RELEASE_MODE"
generate_hash() {
local file="$1"
sha256sum "$file" | cut -d' ' -f1
}
create_hash_file() {
local pdf_path="$1"
local base_name=$(basename "$pdf_path")
local hash_file="${EXPORT_DIR}/${base_name}.sha256"
(cd "$EXPORT_DIR" && sha256sum "$base_name") > "$hash_file"
echo "Created: $hash_file"
}
scan_with_virustotal() {
local pdf_path="$1"
local base_name=$(basename "$pdf_path")
if [[ -z "$VT_API_KEY" ]]; then
echo "Warning: VT_API_KEY not provided, skipping VirusTotal scan for $base_name"
return 1
fi
echo "Scanning $base_name with VirusTotal..."
local upload_response=$(curl -s -X POST \
-H "x-apikey: $VT_API_KEY" \
-F "file=@$pdf_path" \
https://www.virustotal.com/api/v3/files)
if [[ $? -ne 0 ]]; then
echo "Error uploading $base_name to VirusTotal"
return 1
fi
local file_id=$(echo "$upload_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['id'])" 2>/dev/null || echo "")
if [[ -z "$file_id" ]]; then
echo "Error: Could not extract file ID from VirusTotal response for $base_name"
echo "Response: $upload_response"
return 1
fi
local vt_url="https://www.virustotal.com/gui/file/$file_id"
echo "$vt_url"
}
scan_all_pdfs() {
local results_file="$EXPORT_DIR/virus-total-results.md"
cat > "$results_file" << 'HEADER'
## VirusTotal Scan Results
**Scan Date:** TIMESTAMP
---
HEADER
sed -i "s/TIMESTAMP/$(date -u +"%Y-%m-%d %H:%M UTC")/" "$results_file"
local pdf_files=()
if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then
pdf_files+=("$EXPORT_DIR/thgtoa.pdf")
fi
if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then
pdf_files+=("$EXPORT_DIR/thgtoa-dark.pdf")
fi
for pdf in "${pdf_files[@]}"; do
if [[ -f "$pdf" ]]; then
local base_name=$(basename "$pdf")
local hash=$(generate_hash "$pdf")
echo "" >> "$results_file"
echo "### $base_name" >> "$results_file"
echo "- **SHA256 Hash:** \`$hash\`" >> "$results_file"
if [[ -n "$VT_API_KEY" ]]; then
local vt_url=$(scan_with_virustotal "$pdf")
if [[ $? -eq 0 && -n "$vt_url" ]]; then
echo "- **VirusTotal Report:** [$vt_url]($vt_url)" >> "$results_file"
else
echo "- **VirusTotal Report:** Scan failed or API key not provided" >> "$results_file"
fi
else
echo "- **VirusTotal Report:** VT_API_KEY not configured, scan skipped" >> "$results_file"
fi
create_hash_file "$pdf"
else
echo "Warning: $pdf does not exist, skipping..."
fi
done
cat >> "$results_file" << 'FOOTER'
---
*Scan performed automatically by GitHub Actions*
FOOTER
echo "VirusTotal results saved to: $results_file"
}
update_release() {
local tag="${1:-}"
local release_notes="$EXPORT_DIR/release-notes.md"
if [[ "$RELEASE_MODE" == "tag" && -z "$tag" ]]; then
tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
fi
if [[ -z "$tag" ]]; then
echo "Warning: No release tag found, skipping release update."
return 1
fi
echo "Updating release for tag: $tag"
cat > "$release_notes" << EOF
# Release Notes - $tag
**Release Date:** $(date -u +"%Y-%m-%d %H:%M UTC")
## PDF Files
EOF
if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then
local hash=$(generate_hash "$EXPORT_DIR/thgtoa.pdf")
echo "- **thgtoa.pdf (Light Mode)**" >> "$release_notes"
echo " - SHA256: \`$hash\`" >> "$release_notes"
if [[ -f "$EXPORT_DIR/thgtoa.pdf.sig" ]]; then
echo " - Signature: \`thgtoa.pdf.sig\` (GPG signed)" >> "$release_notes"
fi
fi
if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then
local hash=$(generate_hash "$EXPORT_DIR/thgtoa-dark.pdf")
echo "- **thgtoa-dark.pdf (Dark Mode)**" >> "$release_notes"
echo " - SHA256: \`$hash\`" >> "$release_notes"
if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf.sig" ]]; then
echo " - Signature: \`thgtoa-dark.pdf.sig\` (GPG signed)" >> "$release_notes"
fi
fi
echo "" >> "$release_notes"
echo "---" >> "$release_notes"
if [[ -f "$EXPORT_DIR/virus-total-results.md" ]]; then
echo "## VirusTotal Scan Results" >> "$release_notes"
echo "" >> "$release_notes"
cat "$EXPORT_DIR/virus-total-results.md" >> "$release_notes"
echo "" >> "$release_notes"
fi
local files_to_upload=""
if [[ -f "$EXPORT_DIR/thgtoa.pdf" ]]; then
files_to_upload+="$EXPORT_DIR/thgtoa.pdf "
fi
if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf" ]]; then
files_to_upload+="$EXPORT_DIR/thgtoa-dark.pdf "
fi
if [[ -f "$EXPORT_DIR/thgtoa.pdf.sig" ]]; then
files_to_upload+="$EXPORT_DIR/thgtoa.pdf.sig "
fi
if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf.sig" ]]; then
files_to_upload+="$EXPORT_DIR/thgtoa-dark.pdf.sig "
fi
local combined_hash_file="$EXPORT_DIR/sha256sum-combined.txt"
if [[ -f "$EXPORT_DIR/thgtoa.pdf.sha256" ]]; then
cat "$EXPORT_DIR/thgtoa.pdf.sha256" >> "$combined_hash_file" 2>/dev/null || true
fi
if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf.sha256" ]]; then
echo "" >> "$combined_hash_file"
cat "$EXPORT_DIR/thgtoa-dark.pdf.sha256" >> "$combined_hash_file"
fi
files_to_upload+="$combined_hash_file "
if [[ -n "${GPG_PRIVATE_KEY:-}" && -n "${GPG_PASSPHRASE:-}" ]]; then
echo "$GPG_PRIVATE_KEY" | gpg --batch --import 2>/dev/null || true
gpg --batch --yes --armor --detach-sign --output "$combined_hash_file.sig" "$combined_hash_file" 2>/dev/null || true
if [[ -f "$combined_hash_file.sig" ]]; then
files_to_upload+="$combined_hash_file.sig "
fi
fi
if command -v gh &> /dev/null && [[ -n "$GITHUB_TOKEN" ]]; then
echo "Uploading release with GitHub CLI..."
local release_exists=$(gh release view "$tag" 2>/dev/null && echo "yes" || echo "no")
if [[ "$release_exists" == "yes" ]]; then
gh release edit "$tag" --notes-file "$release_notes" 2>/dev/null || {
echo "Warning: Failed to update release notes"
}
for file in $files_to_upload; do
if [[ -f "$file" ]]; then
local file_name=$(basename "$file")
gh release upload "$tag" "$file" 2>/dev/null || {
echo "Warning: Failed to upload $file_name"
}
fi
done
else
gh release create "$tag" \
--title "Release $tag" \
--notes-file "$release_notes" \
$files_to_upload 2>/dev/null || {
echo "Error: Failed to create release"
return 1
}
fi
else
if [[ -n "$GITHUB_TOKEN" && -n "$tag" ]]; then
echo "Using GitHub API to upload release..."
local repo="${GITHUB_REPOSITORY:-}"
local api_url="https://api.github.com/repos/$repo/releases"
local existing_release=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"$api_url/tags/$tag")
if [[ $(echo "$existing_release" | grep -c '"id":') -gt 0 ]]; then
echo "Release already exists, updating..."
local release_id=$(echo "$existing_release" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
curl -X PATCH \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"$api_url/$release_id" \
-d "{\"body\":\"$(cat "$release_notes")\"}" 2>/dev/null || true
for file in $files_to_upload; do
if [[ -f "$file" ]]; then
local file_name=$(basename "$file")
local mime_type=$(file --mime-type -b "$file")
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: $mime_type" \
--data-binary @"$file" \
"https://uploads.github.com/repos/$repo/releases/$release_id/assets?name=$file_name" 2>/dev/null || {
echo "Warning: Failed to upload $file_name"
}
fi
done
else
echo "Creating new release..."
local create_response=$(curl -s -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Accept: application/vnd.github.v3+json" \
"$api_url" \
-d "{\"tag_name\":\"$tag\",\"name\":\"Release $tag\",\"body\":\"$(cat "$release_notes")\"}")
local release_id=$(echo "$create_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
for file in $files_to_upload; do
if [[ -f "$file" ]]; then
local file_name=$(basename "$file")
local mime_type=$(file --mime-type -b "$file")
curl -X POST \
-H "Authorization: token $GITHUB_TOKEN" \
-H "Content-Type: $mime_type" \
--data-binary @"$file" \
"https://uploads.github.com/repos/$repo/releases/$release_id/assets?name=$file_name" 2>/dev/null || {
echo "Warning: Failed to upload $file_name"
}
fi
done
fi
else
echo "Error: GITHUB_TOKEN required for release upload."
return 1
fi
fi
echo "Release update complete!"
}
echo ""
echo "Step 1: Generating hashes..."
if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then
if [[ -f "$EXPORT_DIR/thgtoa.pdf" ]]; then
create_hash_file "$EXPORT_DIR/thgtoa.pdf"
else
echo "Warning: $EXPORT_DIR/thgtoa.pdf not found. Ensure PDF is built first."
fi
fi
if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then
if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf" ]]; then
create_hash_file "$EXPORT_DIR/thgtoa-dark.pdf"
else
echo "Warning: $EXPORT_DIR/thgtoa-dark.pdf not found. Ensure PDF is built first."
fi
fi
echo ""
echo "Step 2: Scanning with VirusTotal..."
scan_all_pdfs
echo ""
echo "Step 3: Updating release..."
update_release "$GITHUB_REF_NAME"
echo ""
echo "PDF Release Script Complete!"
+322
View File
@@ -0,0 +1,322 @@
#!/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
- Get a free key at: https://www.virustotal.com/gui/join-us
HOW TO ADD SECRETS:
1. Go to your repository on GitHub
2. Click 'Settings''Secrets and variables''Actions'
3. Click 'New repository secret' for each secret below:
Secret Name | Value Format
---------------------|--------------------------------------------------
GPG_PRIVATE_KEY | Paste the entire ASCII armored key (BEGIN PGP...)
GPG_PASSPHRASE | Your key's passphrase (no special characters issues)
VT_API_KEY | Your VirusTotal API key
VERIFYING YOUR SETUP:
After adding secrets, you can test by:
1. Going to 'Actions' tab
2. Selecting 'Build guide PDF' workflow
3. Clicking 'Run workflow'
4. Checking if the workflow completes successfully
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
SECURITY NOTES:
⚠ NEVER share your private key or passphrase publicly
⚠ Always use repository secrets, never hardcode in scripts
⚠ Rotate keys periodically if compromised
⚠ Use strong passphrases (12+ characters recommended)
""")
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(" - macOS: brew install gnupg")
print(" - Windows: https://www.gpg4win.org/")
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/pdf-workflow.md\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+222
View File
@@ -0,0 +1,222 @@
#!/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 / "sha256sum-light.txt",
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())