110 Commits

Author SHA1 Message Date
github-actions[bot] a2724af904 chore(export): update PDFs, hashes and signatures [skip ci] 2026-05-31 08:54:02 +00:00
nopeitsnothing cc5ad371a8 ci(darktea): change the repo URL for our tor mirror
The repo is defunct - please use the new one.
2026-05-31 03:42:30 -04:00
nopeitsnothing 84a7ccbdd9 fix: fix inline reference
The ref should be [link](#target)
2026-05-30 11:49:26 -04:00
nopeitsnothing 4eaca49a1c style(docs): fix recommended reading admonition
- docs/about/index.md: convert "Recommended Reading" to collapsible
  admonition
2026-05-30 11:01:51 -04:00
nopeitsnothing c5e5ae48e1 ci: refactoring some things and removing others
Lots of source additions here from long-standing notes over the past few
months. Squashed to make it neater than 219 commits.

- bump version to v1.2.4, Jun 2026
- expand Tor section with new "Traffic analysis and the limits of Tor" subsection
  guard node persistence, website fingerprinting, and a practical breakdown of
  when Tor is and is not sufficient
- expand hardware/firmware threat section with new subsections on firmware
  implants, USB attack hardware (O.MG Cable, Rubber Ducky), Evil Maid attacks,
  supply chain compromise, and a physical inspection checklist
- rename "Removing Metadata from Files/Documents/Pictures" section to "Metadata
  auditing"; add reference table of tools by file type; expand EXIF/XMP coverage,
  PDF metadata (font fingerprinting), and DOCX revision history with real-world
  source identification cases; restructure subsections
- add introductory paragraph to "Your Metadata" section
- add new appendix B8: operational security failure case studies with common
  threads
- add new appendix B9: post-quantum cryptography covering HNDL threat, NIST PQC
  standards, Signal's PQXDH, browser hybrid KEM, PGP limitations, VPN guidance,
  and Monero note
- add new appendix C1: stylometric analysis and writing style covering features
  measured, deployed tools, real cases (J.K. Rowling), effective and ineffective
  countermeasures including AI rewriting
- fix Dangerzone GitHub URL (firstlook -> freedomofpress)
- Remove duplicate footnote [^500]; minor wording fixes ("users" -> "people",
  passive voice tweaks, cross-reference updates)

- docs/index.md: both MSK and RSK GPG fingerprints in a collapsible tip admonition
  instead of bare text
- docs/about/index.md: convert Note admonitions to tip; reformat social media
  links into collapsible tip block
- docs/mirrors/index.md: simplify PDF download instructions to point to Releases;
- README.md: add star history chart
- mkdocs.yml: rename site to "The Hitchhiker's Guide"; update site description
  with hashtags

- sign.yml: remove commented-out workflow_run trigger and if: condition; add
  verify job that runs after sign, downloads artifacts, runs verify_pdf.py, and
  writes a full job summary with hashes; update artifact upload description; minor
  comment and whitespace cleanup
- release.yml, changelog.yml: replace decorative banner comments with single-line
  comments; fix trailing-space style in permissions block
- publish.yml: remove stale comment about nomaterial theme
- verify_pdf.py: full rewrite: replace single-hash-file lookup with flexible
  resolver that checks both bare hash files (.sha256, .b2sum) and two-column
  sumfiles (sha256sums.txt, b2sums.txt); add BLAKE2b verification alongside
  SHA-256; fix signature extension (.asc not .sig); improve CLI (--file,
  --export-dir flags; remove --all; default runs all checks); improve VirusTotal
  output with direct link; cleaner output formatting with ruled separators
2026-05-30 09:32:16 -04:00
nopeitsnothing d1817e9049 ci(pipeline): more meta changes to the pipeline
pre-commit install --install-hooks
2026-05-27 23:49:19 -04:00
nopeitsnothing ede2a53437 ci(pipeline): replace semver tagging with timestamp tags, drop tag_release.py
- release.yml now generates release-YYYYMMDD-<sha> tags automatically
- changelog.yml requires explicit version input, no auto-increment from tags
- sign.yml normalises extensions to .asc and .b2sum
- build-sign-release.yml neutered to a no-op with descriptive error
- tag_release.py archived to scripts/archived/
- update_changelog.py: version_from_changelog() is now primary version source
- .gitignore: fix export/ tracking to match actual file extensions
- docs/code/develop.md: fully rewritten to reflect new manual four-step flow
2026-05-27 23:49:19 -04:00
github-actions[bot] 91a77ed552 chore(export): update PDFs, hashes and signatures [skip ci] 2026-05-26 00:14:20 +00:00
nopeitsnothing 1c3cf75cf0 ci(github): Manual only
automatic triggering is disabled to prevent version mismatches
2026-05-25 19:34:04 -04:00
github-actions[bot] 121be79cd8 docs: update changelog [skip ci] 2026-05-24 12:03:20 +00:00
nopeitsnothing 3b550119a8 chore(lint): commitizen passes
Passed a couple times through the automatic linter to fix some markdown

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-24 07:58:11 -04:00
nopeitsnothing c19389ce49 change(changelog): v1.2.3
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-24 07:57:59 -04:00
nopeitsnothing aabcbac3d9 fix(develop): we use the Anonymous Planet RSK for releases
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-24 01:16:29 -04:00
nopeitsnothing e11a1eb1ce fix(release): sign using RSK instead
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-24 01:05:31 -04:00
nopeitsnothing df6cfbc94b ci(release): auto-increment using [vX.X.X]
Keep it clean, simple, only include the semver tag:

LATEST=$(git tag --list 'v*' --sort=-version:refname \
  | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-24 00:35:59 -04:00
github-actions[bot] 095bb0d8be docs: update changelog [skip ci] 2026-05-24 04:12:03 +00:00
nopeitsnothing 8b81081089 change(changelog): only use "vX.X.X" in version tags
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-24 00:10:06 -04:00
nopeitsnothing ccc97461c9 add(changelog): explain missing v1.2.2 tag
v1.2.2 contained broken Python and other additions that were not meant for release

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-24 00:07:25 -04:00
github-actions[bot] 8d74635d49 docs: update changelog [skip ci] 2026-05-24 03:59:47 +00:00
nopeitsnothing f71e5e2a28 fix(changelog): prevent history dump and filter noise commits
commits_since(): when no prior tag exists, scope to commits not yet on
origin/main via merge-base instead of walking the entire history. This
is what caused the v2.0.1 entry to contain every commit back to project
inception.

categorise(): replace the minimal skip pattern with a compiled NOISE
regex that also drops:
  - numbered series commits (3/8, 7/8, etc.)
  - vague WIP messages (Tweaking, Moving some, Still broken, pt2...)
  - one-word infrastructure fixes (Fix workflow, Fix path, Fix README)
  - oops commits (Forgot to, Revert "...")
  - joke messages (One job to rule them all)

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-23 23:55:34 -04:00
nopeitsnothing 3e28ec19ad fix(convert): actually save per-page PDFs for qpdf, not PNGs
We ignore this for the guide

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-23 23:21:14 -04:00
github-actions[bot] 192da89138 docs: update changelog [skip ci] 2026-05-24 03:05:09 +00:00
nopeitsnothing c658c354ee fix(convert): actually save per-page PDFs for qpdf, not PNGs
Previous filesystem edits to _save_images_as_pdf did not persist to
disk. Rewrote the function: quantize each dark-themed RGB image to
palette mode (256 colours, FASTOCTREE) so Pillow uses zlib/deflate
instead of JPEG (no libjpeg needed), save each as a single-page PDF,
then merge with qpdf. qpdf only accepts PDF inputs to --pages.

Also restores the orphaned footnote citations [^536] and [^537] in
docs/guide/index.md at the key disclosure law paragraph (line 8586).
Previous edit also did not persist to disk.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-23 22:57:26 -04:00
nopeitsnothing 343ad7f037 fix(convert): fail fast with helpful message if pdftoppm or qpdf missing
Previously the script crashed with a FileNotFoundError traceback when
system tools were absent. Now _check_dependencies() runs before any
work begins and prints install instructions for Linux/WSL, macOS, and
a pointer to develop.md for Windows.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-23 22:53:28 -04:00
nopeitsnothing 85ea1fee66 docs(develop): rewrite developer guide for current pipeline
Replaces the thin stub describing the old monolithic workflow with a
full developer reference covering:

- Prerequisites (Linux/macOS/Windows tabs)
- Repository layout
- Local build instructions for both PDFs and the MkDocs site
- Pipeline flow diagram (build → sign → release → changelog)
- What to check before pushing
- Every GitHub Secret: what it is, how to generate it, what breaks
  without it, and a summary table
- Step-by-step release process using tag_release.py
- Release verification instructions (GPG + hash checks)
- Troubleshooting section for every known CI failure mode

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-23 22:48:13 -04:00
nopeitsnothing cdc54d8b3b feat(scripts): add tag_release.py — guided signed release tagger
Interactive script for maintainers to create GPG-signed annotated tags.
Checks clean tree and branch, auto-increments version from latest tag,
pulls the message from the matching changelog entry, resolves the release
signing key (default: 9FA5436D0EE360985157382517ECA05F768DEDF6),
creates the tag, verifies the signature, then prints the push command.

Usage:
  python scripts/tag_release.py                   # auto version
  python scripts/tag_release.py --version v1.2.4
  python scripts/tag_release.py --dry-run         # preview only

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 17:46:37 -04:00
nopeitsnothing 823edbf4af fix(ci): resolve Pillow JPEG KeyError and cairosvg missing dep
convert.py: Pillow's PDF writer requires libjpeg for RGB images, which
is not available in the CI Python environment. Replace all Pillow PDF
saves with _save_images_as_pdf(), which writes pages as lossless PNGs
and assembles them with qpdf — no JPEG dependency needed.

build.yml: install mkdocs-material[imaging] instead of mkdocs-material
to satisfy the cairosvg dependency required by the social plugin.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 17:23:15 -04:00
nopeitsnothing f9d4c17ac6 8/8 chore(bump): v1.2.3
Some whitespace failed checks

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 17:23:15 -04:00
nopeitsnothing 4fd0413f4b 8/8 chore(scripts): minor cleanup to setup_workflow.py
Some whitespace failed checks

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:28:43 -04:00
nopeitsnothing bb005772af 7/8 docs(guide): bump version string to v1.2.3
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:27:07 -04:00
nopeitsnothing 77840d7d5c 6/8 chore: track .b2 hash files in .gitignore
Adds export/thgtoa.pdf.b2 and export/thgtoa-dark.pdf.b2 alongside
the existing .sha256 and .sig entries.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:27:07 -04:00
nopeitsnothing f02a15a07c 5/8 docs(changelog): rewrite for v1.2.3 — consolidate and clean up
Merges the v1.2.2 and v1.2.3 draft entries into a single clean
release. Removes duplicate bullets, internal implementation noise,
and half-finished notes. Switches admonitions to success/warning/bug
types for better visual scanning. Adds a plain-English summary line
per version.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:27:06 -04:00
nopeitsnothing c0aa6b8814 4/8 ci: add automated changelog update workflow
update_changelog.py reads git log since the last version tag,
categorises commits by conventional-commit prefix, and prepends a
new ## [vX.Y.Z] entry to docs/changelog/index.md. changelog.yml
runs after build.yml succeeds and commits the result back to main
with [skip ci]. Supports dry_run and manual_version dispatch inputs.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:27:06 -04:00
nopeitsnothing c6fd2891e0 3/8 ci: split monolithic workflow into build, sign, release stages
build.yml   — builds PDFs, uploads artifact, no secrets required
sign.yml    — hashes (SHA-256 + BLAKE2b) and GPG-signs, triggered via
              workflow_run after build or manually with a build_run_id
release.yml — downloads artifacts, uploads to VirusTotal, publishes
              tagged GitHub Release with all 12 assets attached

All three chain automatically on push to main. Each can be re-run or
triggered independently against any historical run.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:27:05 -04:00
nopeitsnothing 11f88859bf 2/8 refactor(pdf): wire dark mode through convert.py
Removes the dead Chromium dark mode path and BeautifulSoup CSS
injection code. Dark PDF is now produced by calling convert.py on the
finished light PDF. --both builds light then dark; --dark alone works
if the light PDF already exists.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:27:05 -04:00
nopeitsnothing 68b2687b6b 1/8 feat(pdf): add pixel-based dark mode PDF converter
Replaces the broken --prefers-color-scheme=dark Chromium flag with a
pixel-level converter. Rasterizes pages via pdftoppm, remaps colors to
the hacker theme (#1f1f31 bg, #e0e0e0 text, #5e8bde links), and
reassembles with qpdf. Processes in batches of 50 pages to avoid OOM
on large documents like the 414-page guide.

Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:27:04 -04:00
nopeitsnothing 3184181fa8 ci: refactor pipeline into independent build/sign/release/changelog workflows
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
2026-05-22 16:20:57 -04:00
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
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
46 changed files with 73374 additions and 652 deletions
+9
View File
@@ -0,0 +1,9 @@
[tool.commitizen]
name = "cz_conventional_commits"
# enforce sign-off below as well
extra_arguments = ["-S", "--signoff"]
# harmless redundancy
[tool.commitizen.customize]
schema_pattern = '^(feat|add|fix|bugfix|revert|security|perf|refactor|change|chore|ci|docs|style|test|build)(\(.+\))?(!)?: .{1,72}(\n.*)*\nSigned-off-by: .+ <.+@.+>'
+25
View File
@@ -0,0 +1,25 @@
name: 🗑️ DEPRECATED — Build & Sign & Release (combined)
# DEPRECATED — replaced by build.yml, sign.yml, and release.yml
# This workflow is disabled. It is kept only as a reference until the
# split workflows have been confirmed stable in production.
# Do not trigger this workflow.
on:
workflow_dispatch:
inputs:
_disabled:
description: 'This workflow is deprecated. Use build.yml → sign.yml → release.yml instead.'
required: false
type: string
jobs:
noop:
name: Deprecated — no-op
runs-on: ubuntu-latest
steps:
- name: ❌ Workflow is deprecated
run: |
echo "This workflow is deprecated."
echo "Use build.yml → sign.yml → release.yml instead."
exit 1
+85
View File
@@ -0,0 +1,85 @@
# 1. Push to main → build.yml runs automatically → note the run ID
# 2. Manually trigger sign.yml with that build run ID → note the sign run ID
# 3. Manually trigger release.yml with: version=v1.2.5, sign_run_id=<id>
# 4. Manually trigger changelog.yml with: version=v1.2.5
name: 📖 Build 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/build.yml"
permissions:
contents: read
jobs:
build:
name: Build PDFs
runs-on: ubuntu-latest
outputs:
build_mode: ${{ steps.mode.outputs.build_mode }}
run_id: ${{ github.run_id }}
steps:
- name: 🛠️ Checkout
uses: actions/checkout@v4
- name: 🐍 Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: 📦 Install Python dependencies
run: pip install "mkdocs-material[imaging]" pillow numpy
- name: 🖼️ Install poppler and qpdf
run: |
sudo apt-get update -qq
sudo apt-get install -y poppler-utils qpdf
- name: Setup Chrome
uses: browser-actions/setup-chrome@v2
with:
chrome-version: 120
install-dependencies: true
install-chromedriver: true
- name: 🖨️ Resolve build mode
id: mode
run: |
MODE="${{ inputs.build_mode || 'both' }}"
echo "build_mode=$MODE" >> $GITHUB_OUTPUT
echo "Building in mode: $MODE"
- name: 🖨️ Build PDFs
env:
CI: true
run: python scripts/build_guide_pdf.py --${{ steps.mode.outputs.build_mode }}
- name: 📤 Upload PDF artifacts
uses: actions/upload-artifact@v4
with:
name: pdfs
path: |
export/thgtoa.pdf
export/thgtoa-dark.pdf
if-no-files-found: warn
retention-days: 90
compression-level: 0
+58
View File
@@ -0,0 +1,58 @@
name: 📝 Update Changelog
# Manual only — run after a release is published. Provide the exact version
# string (e.g. v1.2.4) to prepend to the changelog. Version is required to
# prevent silent auto-increment drift from release tags.
on:
workflow_dispatch:
inputs:
version:
description: 'Version string to record (e.g. v1.2.4) — required'
required: true
type: string
dry_run:
description: 'Dry run — print entry without committing'
required: false
default: false
type: boolean
permissions:
contents: write # commit changelog back to main
jobs:
changelog:
name: Prepend changelog entry
runs-on: ubuntu-latest
steps:
- name: 🛠️ Checkout
uses: actions/checkout@v4
with:
# Use a PAT so the commit triggers downstream workflows (GITHUB_TOKEN won't)
token: ${{ secrets.CHANGELOG_PAT || secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: 🐍 Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: 📝 Generate and prepend changelog entry
env:
DRY_RUN: ${{ inputs.dry_run || 'false' }}
MANUAL_VERSION: ${{ inputs.version }}
GH_SHA: ${{ github.sha }}
GH_REF: ${{ github.ref_name }}
TRIGGERING_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
run: python scripts/update_changelog.py
- name: 📤 Commit changelog
if: ${{ inputs.dry_run != 'true' }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add docs/changelog/index.md
# Only commit if there's actually a change
git diff --cached --quiet && echo "No changelog change to commit." || \
git commit -m "docs: update changelog [skip ci]"
git push
-1
View File
@@ -14,7 +14,6 @@ jobs:
- name: Deploy docs - name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master uses: mhausenblas/mkdocs-deploy-gh-pages@master
# Or use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to build without the mkdocs-material theme
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: anonymousplanet.org CUSTOM_DOMAIN: anonymousplanet.org
+188
View File
@@ -0,0 +1,188 @@
name: 🚀 Release
# Manual only — run this deliberately after build and sign are confirmed good.
# Provide the sign.yml run ID to pull artifacts from. The release tag is
# generated automatically as release-YYYYMMDD-<short-sha> — no version input
# needed, no semver drift possible.
on:
workflow_dispatch:
inputs:
sign_run_id:
description: 'sign.yml run ID to pull signatures and PDFs from'
required: true
type: string
prerelease:
description: 'Mark as pre-release?'
required: false
default: false
type: boolean
permissions:
contents: write # create releases and tags
actions: read # download artifacts from other runs
jobs:
release:
name: Publish GitHub Release
runs-on: ubuntu-latest
steps:
- name: 🛠️ Checkout (for pgp/)
uses: actions/checkout@v4
with:
fetch-depth: 0
sparse-checkout: pgp
# Download artifacts from the specified sign run
- name: 📥 Download signatures artifact
uses: actions/download-artifact@v4
with:
name: signatures
path: release/
run-id: ${{ inputs.sign_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: 📥 Download signed PDFs artifact
uses: actions/download-artifact@v4
with:
name: pdfs-signed
path: release/
run-id: ${{ inputs.sign_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: 📋 List release assets
run: ls -lh release/
# Read hashes for the release body
- name: "#️⃣ Read hashes"
id: hashes
run: |
read_hash() { cat "release/$1" 2>/dev/null || echo "(not built)"; }
echo "light_sha256=$(read_hash thgtoa.pdf.sha256)" >> $GITHUB_OUTPUT
echo "dark_sha256=$(read_hash thgtoa-dark.pdf.sha256)" >> $GITHUB_OUTPUT
echo "light_b2=$(read_hash thgtoa.pdf.b2sum)" >> $GITHUB_OUTPUT
echo "dark_b2=$(read_hash thgtoa-dark.pdf.b2sum)" >> $GITHUB_OUTPUT
# VirusTotal
- name: 🦠 Upload PDFs to VirusTotal
id: vt
uses: crazy-max/ghaction-virustotal@v5
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
release/thgtoa.pdf
release/thgtoa-dark.pdf
- name: 🔗 Build VT report URLs
id: vt_urls
run: |
light_hash=$(cat release/thgtoa.pdf.sha256 2>/dev/null || echo "")
dark_hash=$(cat release/thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
if [ -n "$light_hash" ]; then
echo "light_vt=https://www.virustotal.com/gui/file/${light_hash}" >> $GITHUB_OUTPUT
else
echo "light_vt=(not built)" >> $GITHUB_OUTPUT
fi
if [ -n "$dark_hash" ]; then
echo "dark_vt=https://www.virustotal.com/gui/file/${dark_hash}" >> $GITHUB_OUTPUT
else
echo "dark_vt=(not built)" >> $GITHUB_OUTPUT
fi
# Generate release tag — timestamp + short SHA, always unique
- name: 🏷️ Generate release tag
id: tag
run: |
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
DATE=$(date -u +'%Y%m%d')
TAG="release-${DATE}-${SHORT_SHA}"
NAME="Release ${DATE} (${SHORT_SHA})"
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "name=$NAME" >> $GITHUB_OUTPUT
echo "Tag: $TAG"
# Create GitHub Release
- name: 🚀 Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.name }}
prerelease: ${{ inputs.prerelease || false }}
draft: false
fail_on_unmatched_files: false
body: |
## 📖 The Hitchhiker's Guide to Online Anonymity
Built from [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) on `${{ github.ref_name }}`.
---
### 📄 Release assets
| File | Description |
|------|-------------|
| `thgtoa.pdf` | Light mode PDF |
| `thgtoa-dark.pdf` | Dark mode PDF (hacker theme) |
| `sha256sums.txt` | SHA-256 checksums (both files) |
| `b2sums.txt` | BLAKE2b checksums (both files) |
| `thgtoa.pdf.sha256` | SHA-256 — light PDF |
| `thgtoa-dark.pdf.sha256` | SHA-256 — dark PDF |
| `thgtoa.pdf.b2sum` | BLAKE2b — light PDF |
| `thgtoa-dark.pdf.b2sum` | BLAKE2b — dark PDF |
| `*.asc` | GPG detached signatures (ASCII armor) |
---
### #️⃣ Hashes
**thgtoa.pdf** (light)
```text
SHA-256 ${{ steps.hashes.outputs.light_sha256 }}
BLAKE2b ${{ steps.hashes.outputs.light_b2 }}
```
**thgtoa-dark.pdf** (dark)
```text
SHA-256 ${{ steps.hashes.outputs.dark_sha256 }}
BLAKE2b ${{ steps.hashes.outputs.dark_b2 }}
```
---
### 🔏 Verifying GPG signatures
```bash
# Import the release signing key
gpg --import pgp/anonymousplanet-release.asc
# Verify PDFs
gpg --verify thgtoa.pdf.asc thgtoa.pdf
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
# Verify hash files
gpg --verify sha256sums.txt.asc sha256sums.txt
gpg --verify b2sums.txt.asc b2sums.txt
```
---
### 🦠 VirusTotal scans
| File | Report |
|------|--------|
| `thgtoa.pdf` | ${{ steps.vt_urls.outputs.light_vt }} |
| `thgtoa-dark.pdf` | ${{ steps.vt_urls.outputs.dark_vt }} |
files: |
release/thgtoa.pdf
release/thgtoa-dark.pdf
release/sha256sums.txt
release/b2sums.txt
release/thgtoa.pdf.sha256
release/thgtoa-dark.pdf.sha256
release/thgtoa.pdf.b2sum
release/thgtoa-dark.pdf.b2sum
release/thgtoa.pdf.asc
release/thgtoa-dark.pdf.asc
release/sha256sums.txt.asc
release/b2sums.txt.asc
+256
View File
@@ -0,0 +1,256 @@
name: 🔏 Sign PDFs
# Can be triggered:
# 1. Automatically after build.yml completes on main
# 2. Manually, pointing at a specific build run to pull PDFs from
on:
workflow_dispatch:
inputs:
build_run_id:
description: 'build.yml run ID to download PDFs from'
required: true
type: string
# Download artifacts from other runs + commit export/ files back to the repo
permissions:
actions: read
contents: write
jobs:
sign:
name: Hash & Sign PDFs
runs-on: ubuntu-latest
outputs:
light_sha256: ${{ steps.hashes.outputs.light_sha256 }}
dark_sha256: ${{ steps.hashes.outputs.dark_sha256 }}
light_b2: ${{ steps.hashes.outputs.light_b2 }}
dark_b2: ${{ steps.hashes.outputs.dark_b2 }}
steps:
- name: 🛠️ Checkout (for pgp/ key reference only)
uses: actions/checkout@v4
with:
sparse-checkout: pgp
- name: 📥 Download PDF artifacts
uses: actions/download-artifact@v4
with:
name: pdfs
path: export/
run-id: ${{ inputs.build_run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: 📋 List downloaded files
run: ls -lh export/
# Hash - extensions match export/ conventions: .sha256, .b2sum
- name: "#️⃣ Hash PDFs"
id: hashes
run: |
cd export
for f in thgtoa.pdf thgtoa-dark.pdf; do
[ -f "$f" ] || continue
sha256sum "$f" | awk '{print $1}' > "${f}.sha256"
b2sum "$f" | awk '{print $1}' > "${f}.b2sum"
done
# Combined summary files
sha256sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > sha256sums.txt || \
sha256sum thgtoa.pdf 2>/dev/null > sha256sums.txt
b2sum thgtoa.pdf thgtoa-dark.pdf 2>/dev/null > b2sums.txt || \
b2sum thgtoa.pdf 2>/dev/null > b2sums.txt
light_sha256=$(cat thgtoa.pdf.sha256 2>/dev/null || echo "")
dark_sha256=$(cat thgtoa-dark.pdf.sha256 2>/dev/null || echo "")
light_b2=$(cat thgtoa.pdf.b2sum 2>/dev/null || echo "")
dark_b2=$(cat thgtoa-dark.pdf.b2sum 2>/dev/null || echo "")
echo "light_sha256=$light_sha256" >> $GITHUB_OUTPUT
echo "dark_sha256=$dark_sha256" >> $GITHUB_OUTPUT
echo "light_b2=$light_b2" >> $GITHUB_OUTPUT
echo "dark_b2=$dark_b2" >> $GITHUB_OUTPUT
echo "--- SHA-256 ---"
cat sha256sums.txt
echo "--- BLAKE2b ---"
cat b2sums.txt
# GPG sign — detached ASCII-armor signatures use .asc extension
- name: 🔑 Install GPG
run: |
sudo apt-get update -qq
sudo apt-get install -y gnupg
- name: 🔏 Import GPG signing key
env:
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
--pinentry-mode loopback --list-secret-keys
- name: 🔏 Sign PDFs and hash files
env:
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: |
sign() {
local file="$1"
[ -f "$file" ] || return 0
echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 \
--pinentry-mode loopback \
--detach-sign --armor --output "${file}.asc" "$file"
echo "Signed: $file → ${file}.asc"
}
sign export/thgtoa.pdf
sign export/thgtoa-dark.pdf
sign export/sha256sums.txt
sign export/b2sums.txt
# Commit export/ back to main
- name: 📦 Checkout full repo for commit
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
path: repo
- name: 📂 Copy export files into repo
run: cp -v export/* repo/export/
- name: 🔏 Configure SSH commit signing
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ACTIONS_SSH_SIGNING_KEY }}" > ~/.ssh/signing_key
chmod 600 ~/.ssh/signing_key
git config --global gpg.format ssh
git config --global user.signingKey ~/.ssh/signing_key
git config --global commit.gpgSign true
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
# If no change in git diff, do nothing
- name: 📤 Commit and push export/ to main
working-directory: repo
run: |
git add export/
if git diff --cached --quiet; then
echo "Nothing to commit — export/ is already up to date."
else
git commit -S -m "chore(export): update PDFs, hashes and signatures [skip ci]"
git push origin main
fi
# Upload artifacts for release.yml and verify job to consume
- name: 📤 Upload signatures artifact
uses: actions/upload-artifact@v4
with:
name: signatures
path: |
export/sha256sums.txt
export/b2sums.txt
export/thgtoa.pdf.sha256
export/thgtoa-dark.pdf.sha256
export/thgtoa.pdf.b2sum
export/thgtoa-dark.pdf.b2sum
export/thgtoa.pdf.asc
export/thgtoa-dark.pdf.asc
export/sha256sums.txt.asc
export/b2sums.txt.asc
if-no-files-found: error
retention-days: 90
compression-level: 0
- name: 📤 Upload signed PDFs artifact
uses: actions/upload-artifact@v4
with:
name: pdfs-signed
path: |
export/thgtoa.pdf
export/thgtoa-dark.pdf
if-no-files-found: warn
retention-days: 90
compression-level: 0
# Verify — runs after sign, surfaces results as a job summary
verify:
name: Verify hashes & signatures
runs-on: ubuntu-latest
needs: sign
# Always run so the summary is visible even if sign partially failed
if: always()
steps:
- name: 🛠️ Checkout scripts and public key
uses: actions/checkout@v4
with:
sparse-checkout: |
scripts/verify_pdf.py
pgp
- name: 📥 Download signatures artifact
uses: actions/download-artifact@v4
with:
name: signatures
path: export/
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: 📥 Download signed PDFs artifact
uses: actions/download-artifact@v4
with:
name: pdfs-signed
path: export/
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: 🔑 Install GPG and import public key
run: |
sudo apt-get update -qq
sudo apt-get install -y gnupg
gpg --import pgp/anonymousplanet-release.asc
- name: 🐍 Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: 🔍 Run verify_pdf.py
id: verify
run: |
# Capture output and exit code separately so we can write the
# summary regardless of whether verification passed or failed.
set +e
output=$(python scripts/verify_pdf.py --hashes --signatures --export-dir export 2>&1)
exit_code=$?
set -e
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
# ── Job summary ──────────────────────────────────────────────
{
if [ "$exit_code" -eq 0 ]; then
echo "## ✅ Verification passed"
else
echo "## ❌ Verification failed"
fi
echo ""
echo "**Run:** [\`${{ github.run_id }}\`](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) &nbsp;·&nbsp; **Commit:** [\`${GITHUB_SHA::7}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) &nbsp;·&nbsp; **By:** \`${{ github.actor }}\`"
echo ""
echo "### Script output"
echo '```'
echo "$output"
echo '```'
echo ""
echo "### Hashes"
echo '```'
echo "── SHA-256 ──────────────────────────────────────────────────────"
cat export/sha256sums.txt 2>/dev/null || echo "(not found)"
echo ""
echo "── BLAKE2b ──────────────────────────────────────────────────────"
cat export/b2sums.txt 2>/dev/null || echo "(not found)"
echo '```'
} >> $GITHUB_STEP_SUMMARY
# Propagate failure so the job is marked red if verification fails
exit $exit_code
+20 -121
View File
@@ -1,130 +1,29 @@
# Byte-compiled / optimized / DLL files # Visual Studio (Windows) solution metadata
.vs/
.vscode/
# Python (MkDocs, scripts/build_guide_pdf.py)
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
.venv/
# 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/
env.bak/ .env
venv.bak/
# Spyder project settings # Cache
.spyderproject .cache/
.spyproject
# Rope project settings # MkDocs build output and local PDF export
.ropeproject site/
# mkdocs documentation
/site
_site/ _site/
_site_test/
build/
# mypy # Export directory — track only hashes and signatures, not the PDFs themselves
.mypy_cache/ export/thgtoa.pdf.sha256
.dmypy.json export/thgtoa-dark.pdf.sha256
dmypy.json export/thgtoa.pdf.b2sum
export/thgtoa-dark.pdf.b2sum
# Pyre type checker export/*.asc
.pyre/
+1 -18
View File
@@ -62,24 +62,7 @@ MD012:
# Consecutive blank lines # Consecutive blank lines
maximum: 1 maximum: 1
# MD013/line-length - Line length # MD013/line-length - Line length
# MD013: false
MD013:
# Number of characters
line_length: 80
# Number of characters for headings
heading_line_length: 80
# Number of characters for code blocks
code_block_line_length: 160
# Include code blocks
code_blocks: false
# Include tables
tables: false
# Include headings
headings: true
# Strict length checking (e.g. allow for longer URLs)
strict: false
# Stern length checking
stern: false
# MD014/commands-show-output - Dollar signs used before commands without showing output # MD014/commands-show-output - Dollar signs used before commands without showing output
# TODO: set false for now but we should consider enabling it # TODO: set false for now but we should consider enabling it
+12 -5
View File
@@ -10,14 +10,21 @@ repos:
- id: check-added-large-files - id: check-added-large-files
- id: check-merge-conflict - id: check-merge-conflict
- id: check-symlinks - id: check-symlinks
- id: detect-private-key
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- id: mixed-line-ending - id: mixed-line-ending
args: [--fix=lf] args: [--fix=lf]
- repo: https://github.com/igorshubovych/markdownlint-cli - repo: https://github.com/commitizen-tools/commitizen
rev: v0.41.0 rev: v4.8.3
hooks: hooks:
- id: markdownlint - id: commitizen
- id: markdownlint-fix stages: [commit-msg]
- repo: local
hooks:
- id: require-commit-body
name: Avoid bad commits
language: script
entry: scripts/hooks/require-commit-body.sh
stages: [commit-msg]
+7 -39
View File
@@ -1,48 +1,16 @@
Welcome. Welcome.
**[IMPORTANT RECOMMENDATION FOR UKRAINIANS. ВАЖЛИВА РЕКОМЕНДАЦІЯ ДЛЯ УКРАЇНЦІВ](briar.html)** ## Star History
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. [![Star History Chart](https://api.star-history.com/chart?repos=Anon-Planet/thgtoa&type=date&logscale&legend=top-left)](https://www.star-history.com/?repos=Anon-Planet%2Fthgtoa&type=date&logscale=&legend=bottom-right)
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. 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.** **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). -->
<!-- Mirrors: **Ways to read or export the guide**
- Tor Onion Mirror: <http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/> -->
The guide and all the files are also readily available on Archive.org and Archive.today: - **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`).
- 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!
+24 -24
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. description: We are the maintainers of the Hitchhiker's Guide and the PSA Matrix space.
schema: schema:
"@context": https://schema.org "@context": https://schema.org
@@ -7,7 +7,7 @@ schema:
"@id": https://www.anonymousplanet.org/ "@id": https://www.anonymousplanet.org/
name: Anonymous Planet name: Anonymous Planet
url: https://www.anonymousplanet.org/about/ url: https://www.anonymousplanet.org/about/
logo: ../media/favicon.png logo: ../media/profile.png
sameAs: sameAs:
- https://github.com/Anon-Planet - https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg - https://opencollective.com/anonymousplanetorg
@@ -15,7 +15,7 @@ schema:
--- ---
![Anonymous Planet logo](../media/profile.png){ align=right } ![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. 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,38 +35,38 @@ 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 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! 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!
??? Note "Discuss ideas on Matrix for real-time chat" ??? tip "Discuss ideas on Matrix for real-time chat"
We offer a Matrix.org hosted space of our own. Check it out! We offer a Matrix.org hosted space of our own. Check it out!
- Read [the rules](https://anonymousplanet.org/chatrooms-rules.html), please - Read [the rules](https://psa.anonymousplanet.org/), please
- Matrix Room: [#anonymity:matrix.org](https://matrix.to/#/#anonymity:matrix.org) - Matrix Room: https://matrix.to/#/#nth:anonymousplanet.net
- Matrix Space: [#privacy-security-anonymity:matrix.org](https://matrix.to/#/#privacy-security-anonymity:matrix.org) - Matrix Space: https://matrix.to/#/#psa:anonymousplanet.net
- Admins: @daskolburn:thomcat.rocks and @thehidden:tchncs.de
Follow us on: ???+ tip "Follow us on"
- Twitter at <https://twitter.com/AnonyPla> - Twitter at <https://twitter.com/AnonyPla>
- Mastodon at <https://mastodon.social/@anonymousplanet>
- Mastodon at <https://mastodon.social/@anonymousplanet>
To contact me, see the updated information on the website or send an e-mail to <contact@anonymousplanet.org> To contact me, see the updated information on the website or send an e-mail to <contact@anonymousplanet.org>
**Please consider [donating](../guide/index.md#donations) if you enjoy the project and want to support the hosting fees or support the funding of initiatives like the hosting of Tor Exit Nodes.** **Please consider [donating](../guide/index.md#donations) if you enjoy the project and want to support the hosting fees or support the funding of initiatives like the hosting of Tor Exit Nodes.**
### Recommended Reading ???+ example "Recommended Reading"
Some of those resources may, in order to sustain their project, contain or propose: Some of those resources may, in order to sustain their project, contain or propose:
- Sponsored commercial content - Sponsored commercial content
- Monetized content through third party platforms (such as YouTube) - Monetized content through third party platforms (such as YouTube)
- Affiliate links to commercial services - Affiliate links to commercial services
- Paid Services such as consultancy - Paid Services such as consultancy
- Premium content such as ad-free content or updated content - Premium content such as ad-free content or updated content
- Merchandising - Merchandising
_Note that these websites could contain affiliate/sponsored content and/or merchandising. This guide does not endorse and is not sponsored by any commercial entity in any way._ _Note that these websites could contain affiliate/sponsored content and/or merchandising. This guide does not endorse and is not sponsored by any commercial entity in any way._
@@ -74,7 +74,7 @@ If you skipped those, you should really still consider viewing this YouTube play
_Anonymous Planet_ **does not** participate in any sponsoring, endorsement, advertising, or other affiliate programs for any entity. We only rely on anonymous donations in a closed, transparent loop system. _Anonymous Planet_ **does not** participate in any sponsoring, endorsement, advertising, or other affiliate programs for any entity. We only rely on anonymous donations in a closed, transparent loop system.
??? Note "Privacy related" ??? tip "Privacy related"
- AnarSec: <https://www.anarsec.guide/> - AnarSec: <https://www.anarsec.guide/>
- EFF Surveillance Self-Defense: <https://ssd.eff.org/> - EFF Surveillance Self-Defense: <https://ssd.eff.org/>
@@ -84,14 +84,14 @@ _Anonymous Planet_ **does not** participate in any sponsoring, endorsement, adve
- The New Oil: <https://thenewoil.org> - The New Oil: <https://thenewoil.org>
- PrivacyTools.io: <https://privacytools.io> - PrivacyTools.io: <https://privacytools.io>
??? Note "Blogs and personal websites" ??? tip "Blogs and personal websites"
- CIA Officer's Blog: <https://officercia.mirror.xyz/> - CIA Officer's Blog: <https://officercia.mirror.xyz/>
- Continuing Ed: <https://edwardsnowden.substack.com/> - Continuing Ed: <https://edwardsnowden.substack.com/>
- Madaidan's Insecurities: <https://madaidans-insecurities.github.io/> - Madaidan's Insecurities: <https://madaidans-insecurities.github.io/>
- Seirdy's Home: <https://seirdy.one/> - Seirdy's Home: <https://seirdy.one/>
??? Note "Useful resources" ??? tip "Useful resources"
- KYC? Not me: <https://kycnot.me/> - KYC? Not me: <https://kycnot.me/>
- 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) - 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)
@@ -100,7 +100,7 @@ _Anonymous Planet_ **does not** participate in any sponsoring, endorsement, adve
- Terms of Service, Didn't Read: <https://tosdr.org> - Terms of Service, Didn't Read: <https://tosdr.org>
- Whonix Documentation: <https://www.whonix.org/wiki/Documentation> - Whonix Documentation: <https://www.whonix.org/wiki/Documentation>
??? Note "We are not affiliated with Anonymous or Riseup" ??? note "We are not affiliated with Anonymous or Riseup"
One or two of our community members uses or has used the resources of Riseup. We are not affiliated with Riseup in any manner. One or two of our community members uses or has used the resources of Riseup. We are not affiliated with Riseup in any manner.
+83
View File
@@ -0,0 +1,83 @@
---
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
Notable changes to the guide and its tooling. Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [v1.2.3]
CI/CD pipeline split into independent stages, dark PDF quality improved, release signing automated, and the changelog now updates itself on every build. Skipping v1.2.2 which was a placeholder and contained broken Python unsuitable for a tag/release.
???+ tip "Added"
- **Dark mode PDF** (`scripts/convert.py`): pixel-level converter replaces the broken `--prefers-color-scheme=dark` Chromium flag. Produces a 200 DPI hacker-themed PDF (`#1f1f31` background, `#e0e0e0` text, `#5e8bde` links) with batched page processing to avoid OOM on large documents.
- **Three independent CI workflows** replacing the old monolithic `build-sign-release.yml`:
- `build.yml`: builds PDFs and uploads them as an artifact; no secrets required, can be re-run freely.
- `sign.yml`: downloads the PDF artifact, computes SHA-256 and BLAKE2b hashes, GPG-signs all outputs, and uploads a `signatures` artifact. Can be re-run against any historical build.
- `release.yml`: downloads both artifacts, uploads to VirusTotal, and publishes a tagged GitHub Release with all 12 assets attached. Can be triggered manually against any previous sign run.
- **`scripts/update_changelog.py`**: reads `git log` since the last version tag, categorises commits by conventional-commit prefix, and prepends a new entry to this file automatically after each successful build.
- **`changelog.yml`** workflow: commits the auto-generated changelog entry back to `main` after every build, with `dry_run` and `manual_version` dispatch inputs for safe local testing.
- **`scripts/tag_release.py`**: interactive guided helper for maintainers to create GPG-signed annotated tags. Checks clean tree and branch, auto-increments the version, pulls the message from the changelog, resolves the release signing key, creates and verifies the tag, then prints the push command.
- **`docs/code/develop.md`**: full developer reference covering prerequisites, local build instructions, the pipeline flow, all required GitHub Secrets, the release process, verification steps, and a troubleshooting section for every known CI failure mode.
!!! warning "Changed"
- `build-sign-release.yml` deprecated - push triggers removed, manual dispatch only. Will be deleted once in-flight runs complete.
- The full pipeline (build → sign → release → changelog) now chains automatically via `workflow_run` on every push to `main`.
- GPG signing uses `--pinentry-mode loopback` and `--passphrase-fd 0` to avoid interactive prompts on headless runners.
- VirusTotal scans moved to the release stage so they run once per release, not once per build.
- `.gitignore` updated to track `.b2` per-file hash files alongside existing `.sha256` and `.sig` entries.
- Stale information removed from the guide; deprecated ODT section in Appendix A6 commented out.
- Footer copyright information corrected.
!!! bug "Fixed"
- `_save_images_as_pdf` in `convert.py` was passing raw PNG files to `qpdf --pages`, which only accepts PDF inputs. Fixed by quantizing each page to palette mode (256 colours, FASTOCTREE) and saving as a single-page PDF before merging.
- `convert.py` now fails immediately with install instructions if `pdftoppm` or `qpdf` are missing, instead of crashing with an unhelpful `FileNotFoundError`.
- Pillow `KeyError: 'JPEG'` on CI resolved by installing `mkdocs-material[imaging]` and using palette-mode PDF encoding instead of RGB+JPEG.
- Orphaned footnote citations `[^536]` and `[^537]` (Australian privacy law and the Identify and Disrupt Act) restored at the key disclosure law paragraph in the guide.
- Broken internal links and mismatched cross-references throughout the guide corrected.
---
## [v1.2.1]
First automated PDF build and the start of the CI pipeline.
???+ tip "Added"
- `scripts/build_guide_pdf.py`: builds the MkDocs site and renders the full guide to a single PDF via headless Chromium (Chrome or Edge). Supports `--dark`, `--light`, and `--both` modes.
- GitHub Actions workflow that installs Chromium, runs the build script, and uploads `export/thgtoa.pdf` as an artifact on every push to `main` or manual dispatch.
- `docs/stylesheets/extra.css` for shared site styling.
- This changelog.
!!! warning "Changed"
- `README.md` updated with instructions for local PDF export and a note about the GitHub Actions artifact.
- `.gitignore` updated to exclude local build outputs (`export/`, `site/`, `_site_test/`).
!!! bug "Fixed"
- Broken reference-style internal links throughout `docs/guide/index.md` replaced with correct fragment links.
- Broken footnote marker on the "free (unallocated) space" list item in the guide.
---
[v1.2.3]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.3
[v1.2.1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.1
+387
View File
@@ -0,0 +1,387 @@
# Developer Guide
This page covers everything you need to contribute to the project, run the build pipeline locally, configure GitHub Secrets, and publish a release.
---
## Prerequisites
Install these before anything else.
=== "Linux / macOS"
```bash
# Python 3.11+
python3 --version
# poppler (pdftoppm) and qpdf
sudo apt install poppler-utils qpdf # Debian/Ubuntu
brew install poppler qpdf # macOS
# GPG
sudo apt install gnupg # Debian/Ubuntu
brew install gnupg # macOS
# Python dependencies
pip install "mkdocs-material[imaging]" pillow numpy
```
=== "Windows"
```powershell
# Python 3.11+ from https://python.org
# poppler: download from https://github.com/oschwartz10612/poppler-windows/releases
# Extract and add the bin\ folder to PATH
# qpdf: download from https://github.com/qpdf/qpdf/releases
# Extract and add the bin\ folder to PATH
# GPG: download Gpg4win from https://gpg4win.org
# Python dependencies
pip install "mkdocs-material[imaging]" pillow numpy
```
You also need **Google Chrome** or **Microsoft Edge** installed for the light-mode PDF build (headless Chromium).
---
## Repository layout
```
.github/
workflows/
build.yml # builds PDFs, uploads artifact
sign.yml # hashes + GPG signs, uploads signatures artifact
release.yml # publishes GitHub Release with all assets
changelog.yml # prepends a new entry to docs/changelog/index.md
publish.yml # deploys MkDocs site to GitHub Pages
build-sign-release.yml # DEPRECATED - fails on trigger, kept for reference
docs/
guide/index.md # the guide (single Markdown file)
changelog/ # release notes
code/ # this page
export/ # PDF output (PDFs gitignored; .sha256, .b2sum, .asc tracked)
pgp/ # public signing keys
scripts/
build_guide_pdf.py # MkDocs + Chromium PDF builder
convert.py # pixel-based dark mode PDF converter
update_changelog.py # auto-generates changelog entries from git log
setup_workflow.py # GitHub Secrets setup assistant
verify_pdf.py # signature verification helper
archived/
tag_release.py # ARCHIVED - GPG tag helper (not used in current flow)
```
---
## Building locally
### Build both PDFs
```bash
python scripts/build_guide_pdf.py --both
```
This builds the MkDocs site, renders it to `export/thgtoa.pdf` via headless Chromium, then calls `scripts/convert.py` to produce `export/thgtoa-dark.pdf`.
| Flag | Effect |
|------|--------|
| `--both` | Light PDF then dark PDF |
| (no flag) | Light PDF only |
| `--dark` | Dark PDF only (light PDF must already exist) |
### Build only the dark PDF from an existing light PDF
```bash
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
```
Options:
| Flag | Default | Description |
|------|---------|-------------|
| `--dpi` | `200` | Rasterization DPI. 150 = smaller file, 300 = sharper but slow |
| `--batch-size` | `50` | Pages per batch. Reduce if you hit OOM |
| `--bg` | `1f1f31` | Background colour (hex) |
| `--text` | `e0e0e0` | Body text colour (hex) |
| `--link` | `5e8bde` | Link / blue element colour (hex) |
### Preview the MkDocs site
```bash
mkdocs serve
```
Opens at `http://127.0.0.1:8000`.
---
## CI/CD pipeline overview
The pipeline is fully manual after the initial build - no step automatically triggers the next. This prevents version mismatches between what was built, what was signed, and what gets released.
```
push to main (or manual trigger)
build.yml
Builds thgtoa.pdf + thgtoa-dark.pdf.
Uploads artifact: pdfs
Note the run ID.
│ # manually trigger sign.yml with the build run ID
sign.yml
Downloads pdfs artifact. Hashes (SHA-256 + BLAKE2b) and GPG-signs
all files. Commits export/ back to main. Uploads artifacts:
signatures, pdfs-signed
Note the run ID.
│ # manually trigger release.yml with the sign run ID
release.yml
Downloads signatures + pdfs-signed artifacts. Runs VirusTotal.
Creates GitHub Release tagged release-YYYYMMDD-<short-sha>.
│ # manually trigger changelog.yml with the version string
changelog.yml
Runs update_changelog.py, prepends a new ## [vX.Y.Z] entry,
commits back to main.
```
Each stage is independent. If signing fails (e.g. an expired key), re-run only `sign.yml` pointing at the existing build artifact - no need to rebuild the PDFs.
!!! warning "Before you push"
- Make sure the working tree is clean (`git status`)
- Run `mkdocs build` locally if you changed `docs/` to catch broken links before CI does
- If you added new footnotes, verify they have both a definition `[^N]:` and at least one inline citation `[^N]`
---
## Release process (step by step)
### 1. Trigger a build
Push to `main` - `build.yml` runs automatically when `docs/`, `mkdocs.yml`, or `scripts/` change. You can also trigger it manually from **Actions → Build PDFs → Run workflow**.
Once it completes successfully, **note the run ID** from the URL or the Actions list.
---
### 2. Sign the PDFs
Go to **Actions → Sign PDFs → Run workflow**.
| Input | Value |
|-------|-------|
| `build_run_id` | The run ID from step 1 |
`sign.yml` will:
- Download the PDFs artifact from the build run
- Compute SHA-256 and BLAKE2b hashes, writing `thgtoa.pdf.sha256`, `thgtoa.pdf.b2sum`, `sha256sums.txt`, `b2sums.txt`, and the dark equivalents
- GPG-sign all PDFs and hash files, writing `.asc` detached signature files
- Commit the updated `export/` directory back to `main`
- Upload two artifacts: `signatures` and `pdfs-signed`
Once it completes successfully, **note the run ID**.
---
### 3. Publish the release
Go to **Actions → Release → Run workflow**.
| Input | Value |
|-------|-------|
| `sign_run_id` | The run ID from step 2 |
| `prerelease` | `false` for a normal release |
`release.yml` will:
- Download `signatures` and `pdfs-signed` artifacts from the sign run
- Upload both PDFs to VirusTotal
- Auto-generate a release tag in the format `release-YYYYMMDD-<short-sha>` (e.g. `release-20260527-abc1234`)
- Create a GitHub Release with all PDFs, hash files, and signatures attached, and the VirusTotal report URLs in the body
No version number needs to be chosen at this step - the tag is derived from the date and commit SHA, so it is always unique and always traceable.
---
### 4. Update the changelog
Go to **Actions → Update Changelog → Run workflow**.
| Input | Value |
|-------|-------|
| `version` | The human-readable version string, e.g. `v1.2.4` |
| `dry_run` | `true` to preview without committing |
`changelog.yml` runs `scripts/update_changelog.py`, which:
- Reads git log since the last `## [vX.Y.Z]` heading in the changelog
- Categorises commits into Added / Changed / Fixed using conventional-commit prefixes
- Prepends a new `## [version]` admonition block to `docs/changelog/index.md`
- Commits the result back to `main`
The version string is the only human decision in the release process. It goes into the changelog only - it does not affect the release tag.
!!! tip "Previewing the changelog entry"
Run with `dry_run: true` first to review the generated entry before it is committed.
---
## Release tag format
Release tags use the format `release-YYYYMMDD-<short-sha>`, for example:
```
release-20260527-abc1234
```
This format is always unique, requires no version decision at release time, and is directly traceable to the commit that was built. The version string (e.g. `v1.2.4`) is a separate, human-assigned label that lives only in the changelog.
---
## Commit message format
All commits must follow the [Conventional Commits](https://www.conventionalcommits.org) format. This is enforced by the `commitizen` pre-commit hook.
```
<type>(<scope>): <description>
```
Accepted types and their changelog bucket:
| Type | Bucket |
|------|--------|
| `feat`, `feature`, `add` | Added |
| `fix`, `bugfix`, `revert`, `security` | Fixed |
| `perf`, `refactor`, `change`, `chore`, `ci`, `docs`, `style`, `test`, `build` | Changed |
Examples:
```bash
feat: add dark-mode PDF export
fix(scripts): handle locked PDF on Windows
docs: update developer workflow guide
chore(ci): pin Chrome version to 120
```
---
## GitHub Secrets
Configure these in **Settings → Secrets and variables → Actions** before the pipeline will fully work. The build step requires no secrets; signing and releasing require all of them.
### `GPG_PRIVATE_KEY`
The ASCII-armored private key used to sign PDFs and hash files.
```bash
gpg --armor --export-secret-keys C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2
```
Copy the entire output (including `-----BEGIN PGP PRIVATE KEY BLOCK-----` and the closing line) and paste it as the secret value.
!!! danger "Key security"
This is the release signing key. Only repository admins should have access to it. Never commit it to the repository or share it outside of GitHub Secrets.
### `GPG_PASSPHRASE`
The passphrase protecting the private key above. Must match exactly - no trailing newline.
### `ACTIONS_SSH_SIGNING_KEY`
An SSH private key used by `sign.yml` to sign the commit that pushes `export/` back to `main`. Generate a dedicated key for this:
```bash
ssh-keygen -t ed25519 -C "github-actions signing key" -f actions_signing_key
```
Add the **private key** as the `ACTIONS_SSH_SIGNING_KEY` secret, and the **public key** to the repository's Deploy Keys (Settings → Deploy Keys) with write access.
### `VT_API_KEY`
A [VirusTotal](https://www.virustotal.com) API key with file upload permissions. Used by `release.yml` to scan both PDFs before publishing. Get one by creating a free account at `virustotal.com` → API key under your profile. The free tier (4 lookups/minute, 500/day) is sufficient.
### `CHANGELOG_PAT`
A GitHub Personal Access Token with `contents: write` scope on this repository. Needed because `changelog.yml` commits back to `main` - commits made with the default `GITHUB_TOKEN` do not trigger further workflow runs (GitHub loop-prevention). A PAT bypasses this. If absent, falls back to `GITHUB_TOKEN` - the commit still happens, it just won't trigger downstream workflows.
**Creating one:** GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens → set Contents to Read and write for this repo only.
### Secrets summary
| Secret | Required by | What happens if missing |
|--------|------------|------------------------|
| `GPG_PRIVATE_KEY` | `sign.yml` | Signing step fails - no `.asc` files produced |
| `GPG_PASSPHRASE` | `sign.yml` | GPG import succeeds but signing fails |
| `ACTIONS_SSH_SIGNING_KEY` | `sign.yml` | Export commit is unsigned (may fail if branch protection requires signed commits) |
| `VT_API_KEY` | `release.yml` | VirusTotal step fails - release is not published |
| `CHANGELOG_PAT` | `changelog.yml` | Falls back to `GITHUB_TOKEN` - changelog updates but commit won't trigger downstream workflows |
---
## Verifying a release
Anyone can verify the authenticity of a release download.
```bash
# Import the release signing key
gpg --import pgp/anonymousplanet-release.asc
# Verify the PDFs
gpg --verify thgtoa.pdf.asc thgtoa.pdf
gpg --verify thgtoa-dark.pdf.asc thgtoa-dark.pdf
# Verify the hash files
gpg --verify sha256sums.txt.asc sha256sums.txt
gpg --verify b2sums.txt.asc b2sums.txt
# Check the PDF hashes match
sha256sum -c sha256sums.txt
b2sum -c b2sums.txt
```
A successful verify looks like:
```
gpg: Signature made ...
gpg: Good signature from "Anonymous Planet (Release) ..."
```
---
## Troubleshooting
**`cairosvg` missing during MkDocs build**
Install the imaging extras: `pip install "mkdocs-material[imaging]"`. Required by the `social` plugin.
**`KeyError: 'JPEG'` in convert.py**
Pillow needs libjpeg. Reinstall after installing the system lib: `sudo apt install libjpeg-dev && pip install --force-reinstall pillow`.
**`qpdf: can't find PDF header`**
Ensure you are on the current version of `convert.py` - qpdf only accepts PDF inputs, not PNG.
**GPG signing fails on CI with `No secret key`**
The `GPG_PRIVATE_KEY` secret is missing or malformed. Re-export with `gpg --armor --export-secret-keys <fingerprint>` and paste the full block including header and footer lines.
**GPG signing fails with `Bad passphrase`**
The `GPG_PASSPHRASE` secret has a trailing space or newline. Paste it again with no surrounding whitespace.
**`release.yml` fails on VirusTotal**
The `VT_API_KEY` is missing, invalid, or over the rate limit (500 requests/day on the free tier). Check the secret and re-run after a few minutes.
**`sign.yml` fails downloading PDF artifact**
The `build_run_id` is wrong, or the artifact has expired (90-day retention). Trigger a new build and use the fresh run ID.
**Changelog already contains version X**
`update_changelog.py` will error if `MANUAL_VERSION` is set to a version already in the changelog. Choose the next version string.
**Footnote warnings from MkDocs (`link '#fnref:N' has no anchor`)**
A footnote definition `[^N]:` exists without a matching inline citation. Add the citation or remove the orphaned definition.
+549 -264
View File
File diff suppressed because it is too large Load Diff
+20 -6
View File
@@ -7,25 +7,39 @@ schema:
"@id": https://www.anonymousplanet.org/ "@id": https://www.anonymousplanet.org/
name: Anonymous Planet name: Anonymous Planet
url: https://www.anonymousplanet.org/authors/ url: https://www.anonymousplanet.org/authors/
logo: ../media/favicon.png logo: ../media/profile.png
sameAs: sameAs:
- https://github.com/Anon-Planet - https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg - https://opencollective.com/anonymousplanetorg
- https://mastodon.social/@anonymousplanet - 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.**
You'll use these keys 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. 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 }
??? tip "GPG Signing Keys for Verification"
<div style="padding: 1em; border-radius: 0.3em;">
<strong>Anonymous Planet Master Signing Key (MSK):</strong>
9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6
<strong>Anonymous Planet Release Signing Key (RSK):</strong>
C302 3DBE A3FB 38C4 38BA 1EED CEC6 0AED E8B9 92A2
</div>
Anonymous Planet is a collective of volunteers.
??? person "Das Kolburn" ??? person "Das Kolburn"
- [:simple-github: GitHub](https://github.com/NobodySpecial256 "@NobodySpecial256") - [:simple-github: GitHub](https://github.com/NobodySpecial256 "@NobodySpecial256")
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org) - [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
- [:simple-matrix: Matrix](https://matrix.to/#/@daskolburn:thomcat.rocks "@daskolburn:thomcat.rocks") - [: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" ??? person "Nope"
@@ -33,4 +47,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} - [:simple-mastodon: Mastodon](https://ioc.exchange/@unknown "@unknown@ioc.exchange"){rel=me}
- [:fontawesome-solid-house: Homepage](https://www.itsnothing.net) - [:fontawesome-solid-house: Homepage](https://www.itsnothing.net)
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org) - [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
- [:simple-matrix: Matrix](https://matrix.to/#/@nope:anonymousplanet.net) - [: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 -5
View File
@@ -7,7 +7,7 @@ schema:
"@id": https://www.anonymousplanet.org/ "@id": https://www.anonymousplanet.org/
name: Anonymous Planet name: Anonymous Planet
url: https://www.anonymousplanet.org/mirrors/ url: https://www.anonymousplanet.org/mirrors/
logo: ../media/favicon.png logo: ../media/profile.png
sameAs: sameAs:
- https://github.com/Anon-Planet - https://github.com/Anon-Planet
- https://opencollective.com/anonymousplanetorg - https://opencollective.com/anonymousplanetorg
@@ -17,15 +17,17 @@ schema:
--- ---
!!! Note "Where to find the Hitchhiker's Guide" ???+ tip "Where to find the Hitchhiker's Guide"
- [Original](https://anonymousplanet.org) - [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.org](https://web.archive.org/web/https://anonymousplanet.org)
- [Archive.today](https://archive.fo/anonymousplanet.org) - [Archive.today](https://archive.fo/anonymousplanet.org)
- [Archive.today over Tor](http://archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion/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. See the [Releases](https://github.com/Anon-Planet/thgtoa/releases). 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" !!! 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;
}
}
+135
View File
@@ -0,0 +1,135 @@
---
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:
```text
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._
+2
View File
@@ -0,0 +1,2 @@
7b9549543555e0da64175a3de010f50720a1d39d63d85da325433b03897ff0a1212b6c10467fec7c5cbe91dc7cab4f3ce17421be1e3dda8853d42ab5f1c34070 thgtoa.pdf
f0125984a62fb3776086230375d546e35459208cdccf1978007f92752b61274d66ef66694842668894db72bc3a39fd746b0579f264319f0b68ffdcfbe9e1ae1f thgtoa-dark.pdf
+2
View File
@@ -0,0 +1,2 @@
54f44ad46d844bdfddb2579371059ef88cb56ff5eeba42644998e386a18d70ae thgtoa.pdf
c9a05d9fc09baa745099f029eab568dd1ed8fe36a9316a8919665629991a078c thgtoa-dark.pdf
+69308
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahv3FQAKCRDOxgrt6LmS
orf3AP9xFUMGyki3135dZ9gm953aWNvRrLHjdcBz7pyh3e41awD/WFem/9WCLH1C
2Q91cqnL1ysnnf1y6wpp0XtewWMnBQA=
=TLm3
-----END PGP SIGNATURE-----
+1
View File
@@ -0,0 +1 @@
f0125984a62fb3776086230375d546e35459208cdccf1978007f92752b61274d66ef66694842668894db72bc3a39fd746b0579f264319f0b68ffdcfbe9e1ae1f
+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.
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN PGP SIGNATURE-----
iHUEABYKAB0WIQTDAj2+o/s4xDi6Hu3Oxgrt6LmSogUCahv3FAAKCRDOxgrt6LmS
otN3AQCAT+U5gzR11x2OiWq9xx0bCYYQc973rtabc/OB1RkwHgD+IUys8A9pAyf7
5Npq+4SiPjhduZ7w82tMRffxdKAhWAU=
=K+IA
-----END PGP SIGNATURE-----
+1
View File
@@ -0,0 +1 @@
7b9549543555e0da64175a3de010f50720a1d39d63d85da325433b03897ff0a1212b6c10467fec7c5cbe91dc7cab4f3ce17421be1e3dda8853d42ab5f1c34070
+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-----
+28 -10
View File
@@ -1,6 +1,6 @@
site_name: Hitchhiker's Guide site_name: The Hitchhiker's Guide
site_author: Anonymous Planet site_author: Anonymous Planet
site_description: "The comprehensive guide for online anonymity and OpSec." site_description: "The comprehensive guide for online #anonymity and #opsec."
site_dir: '/site/' site_dir: '/site/'
docs_dir: 'docs/' docs_dir: 'docs/'
site_url: "https://www.anonymousplanet.org/" site_url: "https://www.anonymousplanet.org/"
@@ -9,6 +9,7 @@ repo_name: ""
#edit_uri: "" #edit_uri: ""
theme: theme:
name: material name: material
locale: en
favicon: media/profile.png favicon: media/profile.png
icon: icon:
logo: material/bird logo: material/bird
@@ -17,6 +18,8 @@ theme:
text: Public Sans text: Public Sans
code: Liberation Mono code: Liberation Mono
features: features:
- navigation.instant
- navigation.instant.prefetch
- navigation.tabs - navigation.tabs
- navigation.sections - navigation.sections
- toc.integrate - toc.integrate
@@ -52,19 +55,19 @@ plugins:
# - git-authors: {} # - git-authors: {}
# - git-latest-release: {} # - git-latest-release: {}
extra_css:
- stylesheets/extra.css
extra: extra:
social: social:
- icon: simple/mastodon - icon: simple/mastodon
link: https://mastodon.social/@anonymousplanet link: https://mastodon.social/@anonymousplanet
name: Mastodon name: Mastodon
# - icon: simple/matrix
# link: https://matrix.to/#/#p-s-a:matrix.org
# name: Matrix Space
- icon: simple/gitlab - icon: simple/gitlab
link: http://wmj5kiic7b6kjplpbvwadnht2nh2qnkbnqtcv3dyvpqtz7ssbssftxid.onion/ link: http://wmj5kiic7b6kjplpbvwadnht2nh2qnkbnqtcv3dyvpqtz7ssbssftxid.onion/
name: "0xacab" name: "0xacab"
- icon: simple/gitea - icon: simple/gitea
link: http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonymousplanetorg link: http://it7otdanqu7ktntxzm427cba6i53w6wlanlh23v5i3siqmos47pzhvyd.onion/anonypla
name: Darktea name: Darktea
- icon: simple/github - icon: simple/github
link: https://github.com/anon-planet link: https://github.com/anon-planet
@@ -75,9 +78,9 @@ extra:
- icon: simple/codeberg - icon: simple/codeberg
link: https://codeberg.org/anonymousplanet link: https://codeberg.org/anonymousplanet
name: Codeberg name: Codeberg
# - icon: simple/torbrowser # - icon: simple/torbrowser
# link: TODO # link: http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/
# name: Hidden service # name: Hidden service
markdown_extensions: markdown_extensions:
- pymdownx.highlight: - pymdownx.highlight:
@@ -121,5 +124,20 @@ markdown_extensions:
permalink: true permalink: true
toc_depth: 3 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: | 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>
+15 -13
View File
@@ -1,16 +1,18 @@
-----BEGIN PGP PUBLIC KEY BLOCK----- -----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEY2cNGBYJKwYBBAHaRw8BAQdAbKn/ExAQ+aq6/o2yc04B9jx5PMloaxux1eoT mDMEZc0J8xYJKwYBBAHaRw8BAQdAWIpOKf8GnTINRH7uW4oeGW4D4vfmK9xeQrnq
iKwQgX60JEFub255bW91cyBQbGFuZXQgUmVsZWFzZSBTaWduaW5nIEtleYiQBBMW n/TMIMe0JEFub255bW91cyBQbGFuZXQgUmVsZWFzZSBTaWduaW5nIEtleYiTBBMW
CgA4FiEEg6bPnvV6wltcf10pKF5gSKEjIbIFAmNnDRgCGwMFCwkIBwMFFQoJCAsF CgA7FiEEwwI9vqP7OMQ4uh7tzsYK7ei5kqIFAmXNCfMCGwMFCwkIBwICIgIGFQoJ
FgIDAQACHgECF4AACgkQKF5gSKEjIbI5+QD/YSQ5E+LW4YJEAQQ+D3LFsGtGGRf3 CAsCBBYCAwECHgcCF4AACgkQzsYK7ei5kqJJVgD+NKdW7U/uMWl6Ov1Ye9PPy6Mb
qQRD5plsUvTtBfsA/15EJaIjzSwrsf/3wsW48zSYKCer/nrhGY9y5yd0m2gBiHUE IyyCYd2j5snO60e7msQA/0rxLaeLwzraevcE+WpdPMadxP2M8MxIKrKeAkKAe+IJ
EBYKAB0WIQSeqYJ4Y58c2FPglsv/lFB1h6apuQUCY2cNxAAKCRD/lFB1h6apuXun iHUEEBYKAB0WIQSfpUNtDuNgmFFXOCUX7KBfdo3t9gUCZqRFIAAKCRAX7KBfdo3t
AQCSNwZBNybUZzN/K4Zl1j6uhCqqnvbUlO80wvbHDMXpywD/dpabqjmpfxfJC20n 9o9LAP426yx71EP9sLKKpkkdAT19HJgsNBeA7SdR/DtMzWEbegD/f2oQYwVz3O1w
t3OFxKSeIbfJ0VHvoHKpwcaGuwC4OARjZw0YEgorBgEEAZdVAQUBAQdAE7WMDHTx 7xuUqJMHS6/bN1E8B78JSi576up9rA2IdQQQFgoAHRYhBJ+lQ20O42CYUVc4JRfs
zWp542lXGLxSsiE4gtMvVxkEneKmZWwzbDcDAQgHiHgEGBYKACAWIQSDps+e9XrC oF92je32BQJp508bAAoJEBfsoF92je32TM8A/2j51Jc3owAx9STceeamG5GG7inq
W1x/XSkoXmBIoSMhsgUCY2cNGAIbDAAKCRAoXmBIoSMhsowLAP42HbiJIsIodWwn 5jRMyKlMG4Kw1y1lAQD2kKSR9tz/l4Yhvy96WOuQYb+uG0W78T12l2c61F/xBrg4
C3yBzwGrd1xRtf/91MpQUgFpCx7xuAD9G0F3l04hKkjxiHK+wJ27LnYcigaTVdje BGXNCfMSCisGAQQBl1UBBQEBB0DOf/mxiZClX/sJqtj7Ob+pCHbsMp9Wd4SHW7/P
6d7bt7TerwE= FaUKHwMBCAeIeAQYFgoAIBYhBMMCPb6j+zjEOLoe7c7GCu3ouZKiBQJlzQnzAhsM
=Hgos AAoJEM7GCu3ouZKie1EBAL5P2th3moOj4IDdXrP6KgdBB0kYweAHix0djG1jV/1+
AQDrgVyMPBbTEztpvc4cyyGAmI42SLM/jKbqO2yWqwVoAg==
=UoL3
-----END PGP PUBLIC KEY BLOCK----- -----END PGP PUBLIC KEY BLOCK-----
+7
View File
@@ -0,0 +1,7 @@
# scripts/archived
Scripts kept for reference but no longer part of the active pipeline.
| Script | Why archived |
|--------|-------------|
| `tag_release.py` | Created GPG-signed `vX.Y.Z` annotated tags. Superseded by the `release-YYYYMMDD-<sha>` timestamp tagging built into `release.yml`. Re-enable if semver release tagging is reintroduced. |
+407
View File
@@ -0,0 +1,407 @@
#!/usr/bin/env python3
"""Release tagging helper for Anonymous Planet maintainers.
Creates a GPG-signed annotated git tag using the release key, with a
message derived from the changelog entry for that version.
Usage:
python scripts/tag_release.py # auto-detect next version
python scripts/tag_release.py --version v1.2.4
python scripts/tag_release.py --version v1.2.4 --key <fingerprint>
python scripts/tag_release.py --version v1.2.4 --dry-run
What it does:
1. Checks the working tree is clean and the branch is main
2. Resolves the version (auto-increments patch if not given)
3. Finds the matching ## [vX.Y.Z] entry in the changelog
4. Lists available signing keys and selects the release key
5. Creates a signed annotated tag: git tag -s vX.Y.Z -u <key> -m <message>
6. Verifies the tag signature
7. Prints the push command
Requirements:
- git
- GPG with the release signing key imported
"""
# The Hitchhiker's Guide to Online Anonymity © 2026 by Anonymous Planet is licensed under Creative
# Commons Attribution-NonCommercial 4.0 International
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
# Default release signing key fingerprint.
# Maintainers with a different key can pass --key on the CLI.
DEFAULT_SIGNING_KEY = "C3023DBEA3FB38C438BA1EEDCEC60AEDE8B992A2"
CHANGELOG = Path(__file__).resolve().parent.parent / "docs" / "changelog" / "index.md"
# --------------------------------------------------------------------------- #
# Helpers
# --------------------------------------------------------------------------- #
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
return subprocess.run(cmd, capture_output=True, text=True, check=check)
def git(*args: str, check: bool = True) -> str:
return run(["git", *args], check=check).stdout.strip()
def check_gpg_installed() -> bool:
try:
return run(["gpg", "--version"], check=False).returncode == 0
except FileNotFoundError:
return False
# --------------------------------------------------------------------------- #
# Git state checks
# --------------------------------------------------------------------------- #
def check_clean_tree() -> bool:
"""Return True if the working tree and index are clean."""
status = git("status", "--porcelain")
return status == ""
def current_branch() -> str:
return git("rev-parse", "--abbrev-ref", "HEAD")
def latest_tag() -> str | None:
"""Return the most recent vX.Y.Z tag, or None."""
out = run(["git", "tag", "--sort=-version:refname", "--list", "v*"], check=False).stdout
for line in out.splitlines():
if re.match(r"^v\d+\.\d+\.\d+", line.strip()):
return line.strip()
return None
def tag_exists(version: str) -> bool:
out = run(["git", "tag", "--list", version], check=False).stdout.strip()
return bool(out)
def head_sha() -> str:
return git("rev-parse", "HEAD")
def short_sha() -> str:
return git("rev-parse", "--short", "HEAD")
# --------------------------------------------------------------------------- #
# Version logic
# --------------------------------------------------------------------------- #
def auto_increment(tag: str | None) -> str:
"""Bump the patch number of tag, or return v1.0.0 if no tag exists."""
if not tag:
return "v1.0.0"
m = re.match(r"^v?(\d+)\.(\d+)\.(\d+)", tag)
if not m:
return "v1.0.0"
return f"v{m.group(1)}.{m.group(2)}.{int(m.group(3)) + 1}"
def normalise_version(v: str) -> str:
return v if v.startswith("v") else f"v{v}"
# --------------------------------------------------------------------------- #
# Changelog parsing
# --------------------------------------------------------------------------- #
def extract_changelog_entry(version: str) -> str | None:
"""Return the raw text of the ## [version] section, or None if not found."""
if not CHANGELOG.exists():
return None
content = CHANGELOG.read_text(encoding="utf-8")
# Find the heading for this version
pattern = re.compile(
rf"^(## \[{re.escape(version)}\].*?)(?=^## \[|\Z)",
re.MULTILINE | re.DOTALL,
)
m = pattern.search(content)
if not m:
return None
return m.group(1).strip()
def changelog_to_tag_message(version: str, entry: str | None, sha: str) -> str:
"""Convert a changelog entry into a clean tag message."""
if not entry:
return f"{version}\n\nNo changelog entry found for {version}.\nCommit: {sha}"
lines = []
current_bucket = None
for line in entry.splitlines():
# Skip the ## [vX.Y.Z] heading — we'll add our own first line
if re.match(r"^## \[", line):
continue
# Skip admonition headers like !!! Note "Added" → use bucket name as section
m = re.match(r'^!!!\s+\w+\s+"(.+)"', line)
if m:
current_bucket = m.group(1)
lines.append(f"\n{current_bucket}:")
continue
# Skip Meta bucket entirely
if current_bucket == "Meta":
continue
# Strip 4-space admonition indent and leading bullet dash
stripped = re.sub(r"^ {4}", "", line)
stripped = re.sub(r"^- ", " - ", stripped)
# Skip blank lines inside admonitions (keep structure clean)
if stripped.strip():
lines.append(stripped)
body = "\n".join(lines).strip()
return f"{version}\n\n{body}\n\nCommit: {sha}"
# --------------------------------------------------------------------------- #
# GPG key selection
# --------------------------------------------------------------------------- #
def list_secret_keys() -> list[dict]:
"""Return a list of available secret keys with fingerprint and uid."""
result = run(["gpg", "--list-secret-keys", "--with-colons"], check=False)
keys = []
current: dict = {}
for line in result.stdout.splitlines():
parts = line.split(":")
if parts[0] == "sec":
if current:
keys.append(current)
current = {"fingerprint": None, "uid": None, "key_id": parts[4] if len(parts) > 4 else ""}
elif parts[0] == "fpr" and current is not None:
if not current.get("fingerprint"):
current["fingerprint"] = parts[9] if len(parts) > 9 else ""
elif parts[0] == "uid" and current is not None:
if not current.get("uid"):
current["uid"] = parts[9] if len(parts) > 9 else ""
if current:
keys.append(current)
return keys
def resolve_signing_key(preferred: str) -> str | None:
"""
Return the fingerprint to use for signing.
Prefers the key matching `preferred` (full fingerprint or key ID suffix).
Falls back to interactive selection if not found.
"""
keys = list_secret_keys()
if not keys:
return None
# Try to match preferred fingerprint/key ID
preferred_upper = preferred.upper()
for k in keys:
fp = (k.get("fingerprint") or "").upper()
kid = (k.get("key_id") or "").upper()
if preferred_upper in (fp, kid) or fp.endswith(preferred_upper) or kid.endswith(preferred_upper):
return k["fingerprint"]
# Not found — show what's available and ask
print("\n⚠ Preferred key not found in keyring. Available keys:\n")
for i, k in enumerate(keys, 1):
print(f" {i}. {k.get('fingerprint', 'N/A')}")
print(f" {k.get('uid', 'Unknown')}\n")
try:
choice = input(f"Select key (1{len(keys)}) or press Enter to abort: ").strip()
if not choice:
return None
idx = int(choice) - 1
if 0 <= idx < len(keys):
return keys[idx]["fingerprint"]
except (ValueError, EOFError):
pass
return None
# --------------------------------------------------------------------------- #
# Tag creation and verification
# --------------------------------------------------------------------------- #
def create_signed_tag(version: str, key_fingerprint: str, message: str, dry_run: bool) -> bool:
"""Create a GPG-signed annotated tag. Returns True on success."""
sha = head_sha()
cmd = [
"git", "tag",
"-s", version,
sha,
"-u", key_fingerprint,
"-m", message,
]
print(f"\n{'' * 70}")
print(f" Tag: {version}")
print(f" Commit: {sha[:12]}")
print(f" Key: {key_fingerprint}")
print(f"{'' * 70}")
print("\nTag message:\n")
for line in message.splitlines():
print(f" {line}")
print()
if dry_run:
print("── DRY RUN — tag not created. Remove --dry-run to proceed.\n")
return True
confirm = input("Create this tag? [y/N] ").strip().lower()
if confirm != "y":
print("Aborted.")
return False
result = run(cmd, check=False)
if result.returncode != 0:
print(f"\n✗ Tag creation failed:\n{result.stderr}")
return False
print(f"\n✓ Tag {version} created.")
return True
def verify_tag(version: str) -> bool:
"""Verify the GPG signature on the tag."""
result = run(["git", "tag", "-v", version], check=False)
output = result.stdout + result.stderr
if result.returncode == 0:
print("\n✓ Tag signature verified:")
for line in output.splitlines():
if any(k in line for k in ("gpg:", "Primary", "using", "issuer", "Good")):
print(f" {line.strip()}")
return True
else:
print(f"\n✗ Tag signature verification failed:\n{output}")
return False
# --------------------------------------------------------------------------- #
# Main
# --------------------------------------------------------------------------- #
def main() -> int:
ap = argparse.ArgumentParser(
description="Create a GPG-signed release tag from the changelog."
)
ap.add_argument(
"--version", "-v",
help="Version to tag, e.g. v1.2.4 (default: auto-increment from latest tag)",
)
ap.add_argument(
"--key", "-k",
default=DEFAULT_SIGNING_KEY,
help=f"GPG key fingerprint to sign with (default: {DEFAULT_SIGNING_KEY})",
)
ap.add_argument(
"--dry-run", "-n",
action="store_true",
help="Print the tag message and exit without creating the tag",
)
args = ap.parse_args()
root = repo_root()
print("\n" + "=" * 70)
print(" RELEASE TAGGER — Anonymous Planet")
print("=" * 70)
# 1. GPG available?
if not check_gpg_installed():
print("\n✗ GPG is not installed or not in PATH.")
print(" Install with: sudo apt install gnupg | brew install gnupg")
return 1
# 2. Working tree clean?
if not check_clean_tree():
print("\n✗ Working tree is not clean. Commit or stash changes first.")
result = run(["git", "status", "--short"], check=False)
print(result.stdout)
return 1
# 3. On main?
branch = current_branch()
if branch != "main":
print(f"\n⚠ You are on branch '{branch}', not 'main'.")
confirm = input(" Tag anyway? [y/N] ").strip().lower()
if confirm != "y":
return 1
# 4. Resolve version
last_tag = latest_tag()
version = normalise_version(args.version) if args.version else auto_increment(last_tag)
print(f"\n Latest tag: {last_tag or '(none)'}")
print(f" New version: {version}")
print(f" Branch: {branch}")
print(f" HEAD: {short_sha()}")
if tag_exists(version) and not args.dry_run:
print(f"\n✗ Tag {version} already exists.")
print(" Use --version to specify a different version, or delete the tag first:")
print(f" git tag -d {version}")
return 1
# 5. Build tag message from changelog
entry = extract_changelog_entry(version)
if not entry:
print(f"\n⚠ No changelog entry found for {version}.")
print(f" Add a '## [{version}]' section to {CHANGELOG.relative_to(root)}")
print(" and re-run, or proceed with a minimal message.")
confirm = input(" Proceed with minimal message? [y/N] ").strip().lower()
if confirm != "y":
return 1
message = changelog_to_tag_message(version, entry, head_sha())
# 6. Resolve signing key
print(f"\n Signing key: {args.key}")
key = resolve_signing_key(args.key)
if not key:
print("\n✗ Could not resolve a signing key. Aborting.")
print(" Import the release key with:")
print(" gpg --import pgp/anonymousplanet-release.asc")
return 1
print(f" Resolved to: {key}")
# 7. Create tag
if not create_signed_tag(version, key, message, args.dry_run):
return 1
if args.dry_run:
return 0
# 8. Verify
if not verify_tag(version):
return 1
# 9. Push instructions
print("\n" + "=" * 70)
print(" ✓ All done. Push the tag with:")
print(f"\n git push origin {version}\n")
print(" The release.yml workflow can then be triggered manually from")
print(" GitHub Actions to publish the GitHub Release for this tag.")
print("=" * 70 + "\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Build light-mode PDF with MkDocs + Chromium, then produce dark-mode PDF via convert.py.
Usage:
python scripts/build_guide_pdf.py # Light PDF only
python scripts/build_guide_pdf.py --dark # Dark PDF only (requires light PDF to exist)
python scripts/build_guide_pdf.py --both # Light PDF, then dark PDF
Examples:
python scripts/build_guide_pdf.py --site-dir build/html --pdf-light export/thgtoa.pdf
python scripts/build_guide_pdf.py --both --pdf-light export/thgtoa.pdf --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) -> Path:
"""Render html_file to pdf_out via headless Chromium.
Uses a temp file first so an open guide.pdf on Windows does not block the
build; if the final path is still locked, writes guide-new.pdf instead.
"""
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()
cmd = [str(browser)]
if os.environ.get("CI"):
cmd += [
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
]
cmd += [
"--headless=new",
"--disable-gpu",
"--no-pdf-header-footer",
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
# Use scripts/convert.py in place of broken dark-mode hack
def build_dark_pdf(light_pdf: Path, dark_pdf: Path) -> Path:
"""Convert the light PDF to dark mode using scripts/convert.py."""
convert_script = repo_root() / "scripts" / "convert.py"
print(f"Converting {light_pdf.name}{dark_pdf.name} (dark mode)…")
subprocess.run(
[sys.executable, str(convert_script), str(light_pdf), str(dark_pdf)],
check=True,
)
return dark_pdf
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 path for light PDF (default: ./export/thgtoa.pdf)",
)
ap.add_argument(
"--pdf-dark",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
help="Output path for dark PDF (default: ./export/thgtoa-dark.pdf)",
)
ap.add_argument(
"--skip-mkdocs",
action="store_true",
help="Reuse existing site dir; skip MkDocs build.",
)
mode = ap.add_mutually_exclusive_group()
mode.add_argument("--dark", action="store_true", help="Dark PDF only (light PDF must already exist)")
mode.add_argument("--both", action="store_true", help="Build light PDF, then dark PDF")
# default (no flag) = light only
args = ap.parse_args()
build_light = not args.dark
build_dark = args.dark or args.both
# --- Light PDF (Chromium) ---
if build_light:
guide_html = args.site_dir / "guide" / "index.html"
if not args.skip_mkdocs:
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
out_light = print_to_pdf(browser, guide_html, args.pdf_light)
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,
)
# --- Dark PDF (pixel converter) ---
if build_dark:
if not args.pdf_light.exists():
print(
f"Light PDF not found at {args.pdf_light}. "
"Run without --dark first, or use --both.",
file=sys.stderr,
)
return 1
out_dark = build_dark_pdf(args.pdf_light, args.pdf_dark)
size_kb = out_dark.stat().st_size // 1024
print(f"Wrote {out_dark.resolve()} ({size_kb} KiB) [Dark Mode]")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+270
View File
@@ -0,0 +1,270 @@
#!/usr/bin/env python3
"""
Dark-mode PDF converter (pixel-based, batch-safe).
Rasterizes each page with pdftoppm, applies the hacker theme palette
pixel-by-pixel, then reassembles into a PDF. Processes in batches of 50
pages to stay within memory limits on large documents, then merges with qpdf.
Usage:
python scripts/convert.py INPUT.pdf [OUTPUT.pdf]
python scripts/convert.py INPUT.pdf [OUTPUT.pdf] [--dpi 200]
[--bg 1f1f31] [--text e0e0e0] [--link 5e8bde]
[--batch-size 50]
Examples:
python scripts/convert.py export/thgtoa.pdf export/thgtoa-dark.pdf
python scripts/convert.py export/thgtoa.pdf --dpi 150 --bg 0d1117
"""
from __future__ import annotations
import argparse
import glob
import os
import subprocess
import sys
import tempfile
from pathlib import Path
import numpy as np
from PIL import Image
# --------------------------------------------------------------------------- #
# Defaults (Hacker theme)
# --------------------------------------------------------------------------- #
DEFAULT_BG = (0x1f, 0x1f, 0x31)
DEFAULT_TEXT = (0xe0, 0xe0, 0xe0)
DEFAULT_LINK = (0x5e, 0x8b, 0xde)
DEFAULT_DPI = 200
DEFAULT_BATCH = 50
def hex_to_rgb(h: str) -> tuple:
h = h.lstrip('#')
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
def apply_dark_theme(
img: Image.Image,
bg=DEFAULT_BG,
text=DEFAULT_TEXT,
link=DEFAULT_LINK,
) -> Image.Image:
"""
Remap a white-background page image to a dark theme.
- Near-white pixels → bg color
- Dark pixels (ink/text) → text color
- Blue-ish pixels → link color
"""
arr = np.array(img.convert('RGB'), dtype=np.float32)
orig = arr.copy()
norm = arr / 255.0
lightness = (
0.299 * norm[:, :, 0]
+ 0.587 * norm[:, :, 1]
+ 0.114 * norm[:, :, 2]
)
r, g, b = orig[:, :, 0], orig[:, :, 1], orig[:, :, 2]
link_mask = (
(b > 100)
& (b > r * 1.3)
& (b > g * 0.9)
& (lightness < 0.85)
)
content_mask = (lightness < 0.85) & ~link_mask
blend = ((1.0 - lightness) / 0.85).clip(0, 1)
bg_f = [c / 255.0 for c in bg]
text_f = [c / 255.0 for c in text]
link_f = [c / 255.0 for c in link]
out = np.zeros_like(norm)
for i, (b_c, t, lc) in enumerate(zip(bg_f, text_f, link_f)):
channel = np.full(lightness.shape, b_c)
channel = np.where(content_mask, b_c + blend * (t - b_c), channel)
channel = np.where(link_mask, b_c + blend * (lc - b_c), channel)
out[:, :, i] = channel
return Image.fromarray((out * 255).clip(0, 255).astype('uint8'))
def _save_images_as_pdf(images: list, output_path: str) -> None:
"""Save a list of RGB PIL images as a PDF without requiring libjpeg.
Pillow's PDF writer defaults to JPEG encoding for RGB images, which
fails when libjpeg is absent in the environment. Fix: quantize each
image to palette mode (256 colours, FASTOCTREE) so Pillow uses
zlib/deflate instead of JPEG, save each as an individual single-page
PDF, then merge the page PDFs with qpdf.
Colour fidelity is preserved — the hacker theme uses only a handful
of distinct colours so 256-colour quantization is visually lossless.
"""
import tempfile as _tempfile
with _tempfile.TemporaryDirectory() as staging:
page_pdfs = []
for i, img in enumerate(images):
page_path = os.path.join(staging, f'p{i:05d}.pdf')
img.quantize(colors=256, method=Image.Quantize.FASTOCTREE).save(
page_path, format='PDF'
)
page_pdfs.append(page_path)
subprocess.run(
['qpdf', '--empty', '--pages'] + page_pdfs + ['--', output_path],
check=True,
)
def _check_qpdf() -> bool:
return subprocess.run(
['qpdf', '--version'], capture_output=True
).returncode == 0
def _check_dependencies() -> None:
"""Verify required system tools are available before doing any work."""
missing = []
for tool in ('pdftoppm', 'qpdf'):
if subprocess.run(['which', tool], capture_output=True).returncode != 0:
missing.append(tool)
if missing:
tools = ', '.join(missing)
instructions = (
f"Install with:\n"
f" Linux/WSL: sudo apt install poppler-utils qpdf\n"
f" macOS: brew install poppler qpdf\n"
f" Windows: see docs/code/develop.md"
)
raise RuntimeError(
f"Missing required system tool(s): {tools}\n{instructions}"
)
def convert_pdf_to_dark(
input_path: str | Path,
output_path: str | Path,
dpi: int = DEFAULT_DPI,
bg=DEFAULT_BG,
text=DEFAULT_TEXT,
link=DEFAULT_LINK,
batch_size: int = DEFAULT_BATCH,
) -> None:
"""
Full pipeline: rasterize → apply dark theme → reassemble as PDF.
For large documents, pages are processed in batches of `batch_size` to
avoid OOM, then merged with qpdf. Falls back to single-pass Pillow save
if qpdf is not available (fine for small documents).
"""
input_path = str(input_path)
output_path = str(output_path)
_check_dependencies()
with tempfile.TemporaryDirectory() as tmp:
# 1. Rasterize all pages
prefix = os.path.join(tmp, 'page')
result = subprocess.run(
['pdftoppm', '-r', str(dpi), '-png', input_path, prefix],
capture_output=True,
)
if result.returncode != 0:
raise RuntimeError(
f"pdftoppm failed:\n{result.stderr.decode()}"
)
pages = sorted(glob.glob(prefix + '-*.png'))
if not pages:
raise RuntimeError(
"pdftoppm produced no output pages — "
"is the PDF valid and not password-protected?"
)
total = len(pages)
print(f" Converting {total} page(s) at {dpi} DPI…", flush=True)
out_dir = os.path.dirname(output_path)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
# 2. Process in batches
use_batches = total > batch_size and _check_qpdf()
if use_batches:
batch_dir = os.path.join(tmp, 'batches')
os.makedirs(batch_dir)
batch_files = []
for start in range(0, total, batch_size):
batch = pages[start:start + batch_size]
batch_num = start // batch_size + 1
batch_path = os.path.join(batch_dir, f'batch_{batch_num:04d}.pdf')
print(
f" Batch {batch_num}/{(total + batch_size - 1) // batch_size}: "
f"pages {start + 1}{start + len(batch)}",
flush=True,
)
dark = [apply_dark_theme(Image.open(p), bg, text, link) for p in batch]
_save_images_as_pdf(dark, batch_path)
batch_files.append(batch_path)
del dark
# 3. Merge batches with qpdf
print(" Merging batches…", flush=True)
subprocess.run(
['qpdf', '--empty', '--pages'] + batch_files + ['--', output_path],
check=True,
)
else:
# Single-pass for small documents or when qpdf is unavailable
dark_pages = []
for i, p in enumerate(pages, 1):
if i % 50 == 0 or i == 1:
print(f" Page {i}/{total}", flush=True)
dark_pages.append(apply_dark_theme(Image.open(p), bg, text, link))
_save_images_as_pdf(dark_pages, output_path)
size_mb = os.path.getsize(output_path) / 1024 / 1024
print(f" Saved → {output_path} ({size_mb:.1f} MB)")
# --------------------------------------------------------------------------- #
# CLI
# --------------------------------------------------------------------------- #
def main() -> int:
parser = argparse.ArgumentParser(description='Convert a PDF to dark mode.')
parser.add_argument('input', help='Input PDF path')
parser.add_argument('output', nargs='?', help='Output PDF path (optional)')
parser.add_argument('--dpi', type=int, default=DEFAULT_DPI, help='Rasterization DPI (default: 200)')
parser.add_argument('--batch-size', type=int, default=DEFAULT_BATCH, help='Pages per batch (default: 50)')
parser.add_argument('--bg', default='1f1f31', help='Background hex color (default: 1f1f31)')
parser.add_argument('--text', default='e0e0e0', help='Body text hex color (default: e0e0e0)')
parser.add_argument('--link', default='5e8bde', help='Link/blue hex color (default: 5e8bde)')
args = parser.parse_args()
if not args.output:
base = Path(args.input).stem
args.output = str(Path(args.input).parent / f"{base}-dark.pdf")
convert_pdf_to_dark(
args.input,
args.output,
dpi=args.dpi,
bg=hex_to_rgb(args.bg),
text=hex_to_rgb(args.text),
link=hex_to_rgb(args.link),
batch_size=args.batch_size,
)
return 0
if __name__ == '__main__':
raise SystemExit(main())
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
COMMIT_MSG_FILE="$1"
if [ -z "$COMMIT_MSG_FILE" ]; then
echo "require-commit-body: no commit message file supplied" >&2
exit 1
fi
stripped=$(grep -v '^\s*#' "$COMMIT_MSG_FILE" | sed 's/[[:space:]]*$//')
subject=$(echo "$stripped" | sed -n '1p')
separator=$(echo "$stripped" | sed -n '2p')
body=$(echo "$stripped" | tail -n +3 | grep -v '^\s*$')
if [ -z "$subject" ]; then
echo ""
echo " COMMIT REJECTED: subject line is empty." >&2
echo ""
exit 1
fi
if [ -z "$separator" ] && [ -z "$body" ]; then
echo ""
echo " COMMIT REJECTED: commit has no body." >&2
echo ""
echo " Every commit must explain *why*, not just *what*." >&2
echo " Format:" >&2
echo ""
echo " type(scope): short subject" >&2
echo ""
echo " Explain the motivation for this change. What problem does" >&2
echo " it solve? Why is this the right approach? Reference any" >&2
echo " relevant issues, prior art, or context." >&2
echo ""
exit 1
fi
if [ -n "$separator" ]; then
echo ""
echo " COMMIT REJECTED: no blank line between subject and body." >&2
echo ""
echo " The second line of a commit message must be blank." >&2
echo ""
exit 1
fi
if [ -z "$body" ]; then
echo ""
echo " COMMIT REJECTED: commit body is empty." >&2
echo ""
echo " Add at least one line after the blank separator explaining" >&2
echo " why this change was made." >&2
echo ""
exit 1
fi
exit 0
+291
View File
@@ -0,0 +1,291 @@
#!/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
"""
# The Hitchhiker's Guide to Online Anonymity © 2026 by Anonymous Planet is licensed under Creative
# Commons Attribution-NonCommercial 4.0 International
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
# Computes the root directory of the repository based on the current files location
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
# Determines whether the GPG command-line tool is available on the current system
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
# Checks whether the GPG tool is installed and accessible on the system
# Export public keys, parse, and read selection of keys from the GPG keyring
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
# Exporting the given private key in ASCII armor format (requires passphrase)
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
# return None if export fails (e.g. wrong passphrase, key not found)
except subprocess.CalledProcessError as e:
print(f"Error exporting private key: {e}")
return None
# Validate that the selected GPG key has signing capability
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
# Print instructions for configuring GitHub Secrets
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())
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""Auto-generate and prepend a changelog entry to docs/changelog/index.md.
Called by .github/workflows/changelog.yml. Reads git log since the last
changelog version, categorises commits by conventional-commit prefix,
and prepends a new ## [vX.Y.Z] section in the MkDocs admonition format used
by the rest of the file.
Environment variables:
MANUAL_VERSION Version string to record (required when run from CI).
Falls back to auto-increment from the changelog for local runs.
TRIGGERING_SHA The commit SHA that triggered this run (used as range end).
DRY_RUN If "true", print the entry and exit without writing.
Note: version is sourced from the changelog file, not from git tags. Git tags
are no longer used as the version authority. The changelog is the source of truth.
"""
from __future__ import annotations
import os
import re
import subprocess
from datetime import datetime, timezone
from pathlib import Path
CHANGELOG = Path(__file__).resolve().parent.parent / "docs" / "changelog" / "index.md"
# Conventional-commit prefixes → changelog bucket
BUCKET_MAP: dict[str, str] = {
"feat": "Added",
"feature": "Added",
"add": "Added",
"fix": "Fixed",
"bugfix": "Fixed",
"perf": "Changed",
"refactor": "Changed",
"change": "Changed",
"chore": "Changed",
"ci": "Changed",
"docs": "Changed",
"style": "Changed",
"test": "Changed",
"revert": "Fixed",
"security": "Fixed",
"build": "Changed",
}
BUCKET_ORDER = ["Added", "Changed", "Fixed"]
def run(cmd: list[str]) -> str:
result = subprocess.run(cmd, capture_output=True, text=True)
return result.stdout.strip()
def auto_increment_version(current: str | None) -> str:
"""Bump the patch number of the current version, or default to v1.0.0."""
if not current:
return "v1.0.0"
m = re.match(r"^v?(\d+)\.(\d+)\.(\d+)", current)
if not m:
return "v1.0.0"
major, minor, patch = int(m.group(1)), int(m.group(2)), int(m.group(3))
return f"v{major}.{minor}.{patch + 1}"
def version_from_changelog() -> str | None:
"""Parse the most recent ## [vX.Y.Z] heading from the changelog file.
This is the primary version source — the changelog is the authority,
not git tags.
"""
if not CHANGELOG.exists():
return None
for line in CHANGELOG.read_text(encoding="utf-8").splitlines():
m = re.match(r"^## \[(v\d+\.\d+\.\d+)\]", line)
if m:
return m.group(1)
return None
def commits_since(ref: str | None, until: str) -> list[str]:
"""Return one-line commit messages between ref and until (exclusive/inclusive).
When no ref is given we fall back to the merge-base between HEAD and
origin/main to avoid dumping the entire history into the changelog.
"""
if ref:
log_range = f"{ref}..{until}"
else:
merge_base = run(["git", "merge-base", "HEAD", "origin/main"])
if merge_base:
log_range = f"{merge_base}..{until}"
else:
# Truly brand new repo with no remote — limit to last 50 commits
log_range = f"-50 {until}"
out = run(["git", "log", "--pretty=format:%s", log_range])
return [line.strip() for line in out.splitlines() if line.strip()]
def categorise(messages: list[str]) -> dict[str, list[str]]:
"""Sort commit messages into Added / Changed / Fixed buckets."""
buckets: dict[str, list[str]] = {b: [] for b in BUCKET_ORDER}
NOISE = re.compile(
r"""
\[skip\ ci\] # CI skip marker
| ^Merge\ (pull\ request|branch) # merge commits
| ^chore:\ bump # version bump chores
| update\ changelog # self-referential
| ^\d+/\d+ # numbered commit series (e.g. 3/8)
| ^Tweaking # vague WIP messages
| ^Moving\ some # vague WIP messages
| \ pt\d+$ # "...pt2", "...pt3" suffixes
| ^Fix\ (workflow|path|README)$ # one-word infrastructure fixes
| ^Still\ broken # embarrassing mid-fix notes
| ^WIP\b # work in progress
| ^Forgot\ to # oops commits
| ^Revert\ " # reverts (surface the original instead)
| ^One\ job\ to\ rule # joke commit messages
""",
re.VERBOSE | re.IGNORECASE,
)
for msg in messages:
if NOISE.search(msg):
continue
m = re.match(r"^(\w+)(?:\([^)]+\))?!?:\s*(.+)$", msg)
if m:
prefix = m.group(1).lower()
description = m.group(2).strip()
bucket = BUCKET_MAP.get(prefix, "Changed")
else:
description = msg
bucket = "Changed"
description = description[0].upper() + description[1:] if description else description
buckets[bucket].append(description)
return buckets
def format_admonition(bucket: str, items: list[str]) -> str:
lines = [f'!!! Note "{bucket}"', ""]
for item in items:
lines.append(f" - {item}")
lines.append("")
return "\n".join(lines)
def build_entry(version: str, buckets: dict[str, list[str]], sha: str) -> str:
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
lines = [f"## [{version}]", ""]
lines.append('!!! Note "Meta"')
lines.append("")
lines.append(f" - Released {date} from [`{sha[:7]}`](https://github.com/Anon-Planet/thgtoa/commit/{sha})")
lines.append("")
for bucket in BUCKET_ORDER:
if buckets.get(bucket):
lines.append(format_admonition(bucket, buckets[bucket]))
if not any(buckets[b] for b in BUCKET_ORDER):
lines.append('!!! Note "Changed"')
lines.append("")
lines.append(" - Minor updates and maintenance")
lines.append("")
return "\n".join(lines)
def prepend_entry(entry: str) -> None:
"""Insert the new entry after the # Release Notes heading."""
content = CHANGELOG.read_text(encoding="utf-8")
insert_at = content.find("\n## ")
if insert_at == -1:
content = content.rstrip() + "\n\n" + entry + "\n"
else:
content = content[: insert_at + 1] + entry + "\n" + content[insert_at + 1 :]
CHANGELOG.write_text(content, encoding="utf-8")
def already_has_version(version: str) -> bool:
if not CHANGELOG.exists():
return False
return f"## [{version}]" in CHANGELOG.read_text(encoding="utf-8")
def main() -> int:
dry_run = os.environ.get("DRY_RUN", "false").lower() == "true"
manual_version = os.environ.get("MANUAL_VERSION", "").strip()
triggering_sha = os.environ.get("TRIGGERING_SHA", "HEAD").strip() or "HEAD"
# Version authority: MANUAL_VERSION (required in CI) → changelog → auto-increment.
# Git tags are intentionally not consulted.
last_cl_ver = version_from_changelog()
new_version = manual_version or auto_increment_version(last_cl_ver)
print(f"Last CL version: {last_cl_ver or '(none)'}")
print(f"New version: {new_version}")
print(f"Triggering SHA: {triggering_sha}")
if already_has_version(new_version) and not manual_version:
print(f"Changelog already contains {new_version} — nothing to do.")
return 0
if already_has_version(new_version) and manual_version:
print(f"::error::Changelog already contains {new_version}. Choose a different version.")
return 1
# Collect commits since the last changelog version (using it as a git ref
# is a best-effort — if the tag doesn't exist, commits_since handles it gracefully).
messages = commits_since(last_cl_ver, triggering_sha)
print(f"Commits found: {len(messages)}")
for m in messages:
print(f" {m}")
buckets = categorise(messages)
entry = build_entry(new_version, buckets, triggering_sha)
print("\n--- Generated entry ---")
print(entry)
print("-----------------------\n")
if dry_run:
print("DRY RUN — not writing to file.")
return 0
prepend_entry(entry)
print(f"Prepended {new_version} to {CHANGELOG}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+260
View File
@@ -0,0 +1,260 @@
#!/usr/bin/env python3
"""Verification script for thgtoa PDF releases.
Verifies SHA-256 hashes, BLAKE2b hashes, and GPG signatures (.asc) for
the light and dark PDFs. Optionally checks VirusTotal scan status.
Usage:
python scripts/verify_pdf.py
python scripts/verify_pdf.py --hashes
python scripts/verify_pdf.py --signatures
python scripts/verify_pdf.py --vt
python scripts/verify_pdf.py --file export/thgtoa.pdf --hashes
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import subprocess
import sys
import urllib.request
from pathlib import Path
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
def _read_bare_hash(hash_file: Path) -> str | None:
"""Read a bare hex digest from a single-value hash file."""
try:
return hash_file.read_text(encoding="utf-8").strip().split()[0]
except (OSError, IndexError):
return None
def _read_hash_from_sumfile(sum_file: Path, pdf_path: Path) -> str | None:
"""Read a hash from a two-column sumfile (sha256sum / b2sum format).
Matches on the filename only (not the full path) so the file can be used
regardless of where the PDFs sit on disk.
"""
if not sum_file.exists():
return None
target = pdf_path.name
try:
for line in sum_file.read_text(encoding="utf-8").splitlines():
parts = line.strip().split(None, 1)
if len(parts) == 2 and Path(parts[1].lstrip("*")).name == target:
return parts[0]
except OSError:
return None
return None
# Hash verification
def _sha256(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def _blake2b(path: Path) -> str:
h = hashlib.blake2b()
with path.open("rb") as fh:
for chunk in iter(lambda: fh.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def verify_hashes(pdf: Path, export_dir: Path) -> bool:
"""Verify all available hash files for a PDF. Returns True if all pass."""
stem = pdf.name # e.g. "thgtoa.pdf" or "thgtoa-dark.pdf"
results: list[bool] = []
checks = [
("SHA-256", _sha256, export_dir / f"{stem}.sha256", export_dir / "sha256sums.txt"),
("BLAKE2b", _blake2b, export_dir / f"{stem}.b2sum", export_dir / "b2sums.txt"),
]
for algo, fn, bare_file, sum_file in checks:
# Resolve expected hash — prefer bare file, fall back to sumfile
expected = _read_bare_hash(bare_file) if bare_file.exists() else None
if expected is None:
expected = _read_hash_from_sumfile(sum_file, pdf)
if expected is None:
print(f"{algo}: no hash file found (checked {bare_file.name}, {sum_file.name})")
continue
actual = fn(pdf)
ok = actual == expected
results.append(ok)
mark = "" if ok else ""
print(f" {mark} {algo}")
if not ok:
print(f" expected: {expected}")
print(f" actual: {actual}")
return all(results) if results else False
# Signature verification
def verify_signature(pdf: Path) -> bool | None:
"""Verify the .asc detached signature for a PDF.
Returns True on success, False on failure, None if GPG is not installed
or the signature file is missing.
"""
sig = pdf.with_suffix(pdf.suffix + ".asc")
if not sig.exists():
print(f" ⚠ Signature file not found: {sig.name}")
return None
try:
result = subprocess.run(
["gpg", "--verify", str(sig), str(pdf)],
capture_output=True,
text=True,
check=False,
)
except FileNotFoundError:
print(" ⚠ GPG not installed — skipping signature verification")
return None
if result.returncode == 0:
print(f" ✓ GPG signature valid")
# Surface the key info line from stderr (that's where gpg writes it)
for line in result.stderr.splitlines():
if any(kw in line for kw in ("Good signature", "key ID", "fingerprint", "using")):
print(f" {line.strip()}")
return True
else:
print(f" ✗ GPG signature INVALID")
for line in result.stderr.splitlines():
if line.strip():
print(f" {line.strip()}")
return False
# VirusTotal
def check_virustotal(pdf: Path, api_key: str) -> bool:
"""Query VirusTotal for the SHA-256 of a PDF. Returns True if clean."""
file_hash = _sha256(pdf)
url = f"https://www.virustotal.com/api/v3/files/{file_hash}"
req = urllib.request.Request(url, headers={"x-apikey": api_key})
try:
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
if e.code == 404:
print(f" ⚠ Not yet scanned on VirusTotal (hash: {file_hash[:16]}…)")
else:
print(f" ⚠ VirusTotal HTTP error: {e.code}")
return False
except Exception as e:
print(f" ⚠ VirusTotal error: {e}")
return False
stats = data.get("data", {}).get("attributes", {}).get("last_analysis_stats", {})
malicious = stats.get("malicious", 0)
suspicious = stats.get("suspicious", 0)
undetected = stats.get("undetected", 0)
harmless = stats.get("harmless", 0)
total = malicious + suspicious + undetected + harmless
clean = malicious == 0 and suspicious == 0
mark = "" if clean else ""
print(f" {mark} VirusTotal ({malicious} malicious, {suspicious} suspicious, "
f"{harmless} clean / {total} engines)")
print(f" https://www.virustotal.com/gui/file/{file_hash}")
return clean
def main() -> int:
root = repo_root()
export = root / "export"
ap = argparse.ArgumentParser(
description="Verify thgtoa PDF hashes, signatures, and VirusTotal status.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
ap.add_argument(
"--file",
type=Path,
default=None,
metavar="PDF",
help="Verify a single PDF instead of both light and dark",
)
ap.add_argument(
"--export-dir",
type=Path,
default=export,
metavar="DIR",
help=f"Directory containing hash and signature files (default: {export})",
)
ap.add_argument("--hashes", action="store_true", help="Verify hashes only")
ap.add_argument("--signatures", action="store_true", help="Verify signatures only")
ap.add_argument("--vt", action="store_true", help="Check VirusTotal status")
args = ap.parse_args()
# Default: verify everything
do_hashes = args.hashes or not any([args.hashes, args.signatures, args.vt])
do_sigs = args.signatures or not any([args.hashes, args.signatures, args.vt])
do_vt = args.vt or not any([args.hashes, args.signatures, args.vt])
# Resolve PDFs to check
if args.file:
pdfs = [args.file]
else:
pdfs = [export / "thgtoa.pdf", export / "thgtoa-dark.pdf"]
vt_api_key = os.environ.get("VT_API_KEY", "")
overall_pass = True
for pdf in pdfs:
bar = "" * 60
print(f"\n{bar}")
print(f" {pdf.name}")
print(bar)
if not pdf.exists():
print(f" ⚠ File not found: {pdf} — skipping")
overall_pass = False
continue
if do_hashes:
ok = verify_hashes(pdf, args.export_dir)
if not ok:
overall_pass = False
if do_sigs:
result = verify_signature(pdf)
if result is False:
overall_pass = False
if do_vt:
if not vt_api_key:
print(" ⚠ VT_API_KEY not set — skipping VirusTotal check")
else:
ok = check_virustotal(pdf, vt_api_key)
if not ok:
overall_pass = False
print(f"\n{'' * 60}")
if overall_pass:
print(" ✓ All checks passed")
else:
print(" ✗ One or more checks failed")
print()
return 0 if overall_pass else 1
if __name__ == "__main__":
raise SystemExit(main())