diff --git a/.github/workflows/build-sign-release.yml b/.github/workflows/build-sign-release.yml
index 7b8789f..664f24a 100644
--- a/.github/workflows/build-sign-release.yml
+++ b/.github/workflows/build-sign-release.yml
@@ -6,7 +6,7 @@ on:
build_mode:
description: 'PDF build mode'
required: true
- default: 'both'
+ default: 'light'
type: choice
options:
- light
@@ -57,57 +57,19 @@ jobs:
env:
CI: true
run: python scripts/build_guide_pdf.py --${{ inputs.build_mode || 'both' }}
-
- - name: 🔒 Sign SHA256 hash file with GPG
- env:
- GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
- GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
+
+ - name: 🛡️ Sign PDFs
run: |
- cd ${{ github.workspace }}
-
- # Import GPG key
- export GPG_TTY=$(tty)
- echo "$GPG_KEY" | gpg --batch --import 2>/dev/null || true
-
-
-
- - name: 🔒 Sign PDF files with GPG
- env:
- GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
- GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
- run: |
- cd ${{ github.workspace }}
-
- # Import GPG key if not already imported
- export GPG_TTY=$(tty)
- echo "$GPG_KEY" | gpg --batch --import 2>/dev/null || true
-
- # Create combined hash file with all PDFs
- sha256sum export/thgtoa.pdf > export/checksums.sha256
- sha256sum export/thgtoa-dark.pdf >> export/checksums.sha256
-
- # Sign the checksum file
- gpg --batch --yes --armor --detach-sign --output export/checksums.sha256.sig export/checksums.sha256 2>/dev/null || true
-
- # Sign each PDF file individually with detached signature
- for pdf_file in export/*.pdf; do
- if [ -f "$pdf_file" ]; then
- base_name=$(basename "$pdf_file")
- echo "Signing $base_name..."
- gpg --default-key 17ECA05F768DEDF6 --batch --yes --armor --detach-sign --output "export/${pdf_file}.sig" "$pdf_file" 2>/dev/null || true
- fi
- done
-
- # Verify signatures were created
- ls -la export/*.sig 2>/dev/null || echo "No signature files found in export/"
+ chmod +x scripts/sign-pdfs.sh
+ ./scripts/sign-pdfs.sh
- name: 🦠 Upload PDFs to VirusTotal
uses: crazy-max/ghaction-virustotal@v5
with:
vt_api_key: ${{ secrets.VT_API_KEY }}
files: |
- ./export/thgtoa.pdf
- ./export/thgtoa-dark.pdf
+ export/thgtoa.pdf
+ export/thgtoa-dark.pdf
- name: 📊 Extract VT scan results
id: vt-scan
@@ -125,7 +87,7 @@ jobs:
### thgtoa.pdf (Light Mode)
- **VT Report:** https://www.virustotal.com/gui/file/\$(sha256sum export/thgtoa.pdf | cut -d' ' -f1)
- ### thgtoa-dark.pdf (Dark Mode)
+ ### thgtoa-dark.pdf (Dark Mode) (currently broken)
- **VT Report:** https://www.virustotal.com/gui/file/\$(sha256sum export/thgtoa-dark.pdf | cut -d' ' -f1)
---
@@ -135,12 +97,9 @@ jobs:
- name: 📤 Upload export directory as artifact
uses: actions/upload-artifact@v4
with:
- name: pdf-export-${{ inputs.build_mode || 'both' }}
+ name: export
path: |
- export/*.pdf
- export/*.sig
- export/*.sha256
- export/virus-total-results.md
+ export/*
if-no-files-found: error
retention-days: 90
compression-level: 0
diff --git a/CHANGELOG.md b/docs/changelog/index.md
similarity index 80%
rename from CHANGELOG.md
rename to docs/changelog/index.md
index b7976a3..701683c 100644
--- a/CHANGELOG.md
+++ b/docs/changelog/index.md
@@ -1,20 +1,34 @@
+---
+title: "TBA"
+description: ""
+schema:
+ "@context": https://schema.org
+ "@type": Organization
+ "@id": https://www.anonymousplanet.org/
+ name: Anonymous Planet
+ url: https://www.anonymousplanet.org/authors/
+ logo: ../media/favicon.png
+ sameAs:
+ - https://github.com/Anon-Planet
+ - https://opencollective.com/anonymousplanetorg
+ - https://mastodon.social/@anonymousplanet
+---
+
# Changelog
All notable changes to this project will be documented in this file.
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
## [Unreleased]
### Added
+- This changelog page
- Add ways to verify the files
### Changed
- Refactored GitHub Actions workflow **Build PDF** (`scripts\build_guide_pdf.py`): now builds both light and dark mode PDFs (`export/thgtoa.pdf` and `export/thgtoa-dark.pdf` respectively).
-- Restored previous VT scans workflow **VirusTotal Scan** (`.github/workflows/vt-scan.yml`): submit files to VT for malware scanning. Links will be published on the site.
+- Restored previous VT scans
## Fixed
@@ -49,3 +63,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[Unreleased]: https://github.com/Anon-Planet/thgtoa/compare/v1.2.1...HEAD
[1.2.1]: https://github.com/Anon-Planet/thgtoa/releases/tag/v1.2.1
+
+***The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),***
+***and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).***
\ No newline at end of file
diff --git a/docs/guide/dev-workflow.md b/docs/guide/dev-workflow.md
index c0af6b8..4ef37e9 100644
--- a/docs/guide/dev-workflow.md
+++ b/docs/guide/dev-workflow.md
@@ -1,81 +1,35 @@
# Development
-## Overview
-
-This repository now includes an automated workflow that handles PDF generation, verification, and distribution with the following features:
-
??? Note "How the pipeline works"
- 1. **Automatic PDF Generation** - Builds both light and dark mode PDFs from MkDocs source
- 2. **SHA256 Hash Generation** - Creates hash files for integrity verification
- 3. **GPG Signature Signing** - Signs all PDFs and hash files with repository GPG key
- 4. **VirusTotal Scanning** - Automatically scans PDFs and updates release notes
- 5. **Release Automation** - Packages everything into GitHub releases
+ **Automatic PDF Generation:** - Builds both light and dark mode PDFs from MkDocs source
+ **SHA256 Hash Generation:** - Creates hash files for integrity verification
+ **GPG Signature Signing:** - Signs all PDFs and hash files with repository GPG key
+ **VirusTotal Scanning:** - Automatically scans PDFs and updates release notes
+ **Release Automation:** - Packages everything into GitHub releases
## Workflow Architecture
-### 1. Build PDF Workflow (`build-pdf.yml`)
-
-**Trigger:** Push to main, pull requests, or manual dispatch
+### Build PDF Workflow (`build-sign-release.yml`)
??? Note "Steps"
- Checkout repository
- - Set up Python 3.13 and MkDocs Material
+ - Set up Python and MkDocs Material
- Install Chromium browser
- - Generate both light and dark mode PDFs
- - Create SHA256 hash files
- - Sign all files with GPG
- - Upload artifacts to GitHub Actions
- - Publish release
+ - Generate both light and dark mode PDFs with `scripts\build_guide_pdf.py`
+ - Create SHA256 and blake2 hash files in `export/`
+ - Sign all files with GPG in `export/`
+ - Upload artifacts to GitHub Actions **manually**
-### 2. VirusTotal Scan Workflow (`vt-scan.yml`)
+### SHA256 Hash Verification
-**Trigger:** Push to main, tags, or manual dispatch (runs after build-pdf)
+!!! Note "**How it works**"
-??? Note "Steps"
-
- - Download PDF artifacts from build workflow
- - Scan both PDFs with VirusTotal API
- - Extract scan results and generate report links
- - Update release notes with VT scan status and URLs
-
-## File Structure
-
-After a successful build, the repository will contain:
-
-```
-.../
-├── export/
-│ ├── thgtoa.pdf # Light mode PDF
-│ ├── thgtoa-dark.pdf # Dark mode PDF
-│ ├── thgtoa.pdf.sig # GPG signature (light)
-│ └── thgtoa-dark.pdf.sig # GPG signature (dark)
-├── thgtoa.pdf.sha256 # Hash file (light)
-├── thgtoa-dark.pdf.sha256 # Hash file (dark)
-├── sha256sum-light.txt # Combined hash file
-└── scripts/
- ├── build_guide_pdf.py # PDF generation script
- └── verify_pdf.py # Verification utility
-```
-
-## Security Features
-
-### 1. SHA256 Hash Verification
-
-**Purpose:** Ensure file integrity during download/transit
-
-**How it works:**
-- Each PDF gets a unique SHA256 hash calculated at build time
-- Hash stored in `.sha256` files alongside the PDFs
-- Combined `sha256sum-light.txt` for batch verification
-
-**Verification command:**
-```bash
-sha256sum -c sha256sum-light.txt
-```
-
-### 2. GPG Signature Verification
+ - Each PDF gets a unique SHA256 hash calculated at build time
+ - Hash stored in `.sha256` files alongside the PDFs
+ - Combined `sha256sum.txt` for batch verification
+### GPG Signature Verification
**Purpose:** Verify authenticity and prevent tampering
@@ -90,104 +44,6 @@ gpg --import pgp/anonymousplanet-master.asc
gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf
```
-### 3. VirusTotal Integration
-
-**Purpose:** Malware detection and security scanning
-
-??? Note "How it works"
-
- - Automatic scan of all generated PDFs
- - Results published in release notes with direct links
- - Provides third-party validation of file safety
-
-## Usage Examples
-
-### Local Development
-
-```bash
-# Build PDFs locally
-python scripts/build_guide_pdf.py --both
-
-# Verify hashes
-python scripts/verify_pdf.py --hashes
-
-# Verify signatures (requires GPG installed)
-python scripts/verify_pdf.py --signatures
-
-# Full verification with VirusTotal check
-export VT_API_KEY=your_api_key
-python scripts/verify_pdf.py --all
-```
-
-### CI/CD Verification
-
-The workflows automatically verify everything during the build process. To manually trigger:
-
-1. Go to Actions tab
-2. Select "Build guide PDF" or "VirusTotal Scan"
-3. Click "Run workflow"
-4. Download artifacts from successful run
-
-## Release Process
-
-When you create a tag (e.g., `v1.0.0`):
-
-1. Push the tag: `git push origin v1.0.0`
-2. Build PDF workflow triggers automatically
-3. VirusTotal scan workflow runs after build completes
-4. Both workflows update/create GitHub release with:
- - Light and dark mode PDFs
- - GPG signatures for all files
- - Hash files for verification
- - Release notes with VT scan results
-
-## Troubleshooting
-
-### Common Issues
-
-**GPG signing fails:**
-- Check that `GPG_PRIVATE_KEY` is in ASCII armor format
-- Verify passphrase is correct
-- Ensure key has signing capability
-
-**Hash mismatch after download:**
-- Re-download the file (corruption during transfer)
-- Verify you're using the correct hash file
-- Check disk integrity
-
-**VirusTotal scan fails:**
-- Verify `VT_API_KEY` is set correctly
-- Check API quota limits (free tier: 4 requests/minute)
-- Ensure PDF files exist before scanning
-
-### Debug Mode
-
-Enable verbose output by adding to workflow:
-```yaml
-- name: Debug
- run: |
- echo "Current directory:" && pwd
- echo "Files in export:" && ls -la export/
- echo "Hash file contents:" && cat sha256sum-light.txt
-```
-
-## Best Practices
-
-1. **Always verify signatures** before opening PDFs from untrusted sources
-2. **Check VirusTotal results** for any suspicious detections
-3. **Keep GPG keys secure** - never commit private keys to repository
-4. **Monitor API usage** for VirusTotal to avoid rate limiting
-5. **Test locally** before pushing tags to production
-
-## Future Enhancements
-
-Potential improvements:
-- Multi-signature support (multiple maintainers)
-- Automated changelog generation with hashes
-- Cross-platform signature verification scripts
-- Integration with additional malware scanners
-- Automatic mirror updates with verified files
-
---
*This workflow is designed for security-conscious users who need to verify the authenticity and integrity of downloaded documents.*
diff --git a/docs/index.md b/docs/index.md
index 9712a8f..17495b0 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -17,14 +17,13 @@ schema:
# **Hello, and welcome to the Hitchhiker's Guide.**
**9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6**
-
-This is the master signing key fingerprint for Anonymous Planet.
- You'll use it to [**verify the checksum** and **GPG signature** of all files for authenticity.](verify/index.md)
- Please share this project if you enjoy it and you think it might be useful to others.
+
+You'll use it to [**verify the checksum** and **GPG signature** of all files for authenticity.](verify/index.md)
+Please share this project if you enjoy it and you think it might be useful to others.
{ align=right }
-Anonymous Planet is a collective of volunteers and contributors. No one person is considered more valuable than another, and no one person should be viewed as having "more impact" on Anonymous Planet.
+Anonymous Planet is a collective of volunteers.
??? person "Das Kolburn"
diff --git a/docs/verify/index.md b/docs/verify/index.md
index 304ff03..9922442 100644
--- a/docs/verify/index.md
+++ b/docs/verify/index.md
@@ -62,9 +62,12 @@ gpg --verify export/thgtoa-dark.pdf.sig export/thgtoa-dark.pdf
Expected output for successful verification:
```
-gpg: Signature made [date]
-gpg: using RSA key [key-id]
-gpg: Good signature from "[owner]"
+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
@@ -114,9 +117,9 @@ The GitHub Actions workflows automatically:
## Key Information
-**Signing Key:** Anonymous Planet Master Key
-**Key ID:** See `pgp/anonymousplanet-master.asc` for details
-**Fingerprint:** Verify from the repository's official documentation
+**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
---
diff --git a/export/thgtoa-dark.pdf b/export/thgtoa-dark.pdf
index 8b607de..7f2fabf 100644
Binary files a/export/thgtoa-dark.pdf and b/export/thgtoa-dark.pdf differ
diff --git a/export/thgtoa-dark.pdf.b2sum b/export/thgtoa-dark.pdf.b2sum
new file mode 100644
index 0000000..e2af623
--- /dev/null
+++ b/export/thgtoa-dark.pdf.b2sum
@@ -0,0 +1 @@
+f212d0425b38d5cd10da6dc804b60f143da23d4b07051aae31d0966082519b300af0e1c423683e0223738b33b138c687232b1c8bd68cf643777bbc5b588152bd ./export/thgtoa-dark.pdf
diff --git a/export/thgtoa.pdf b/export/thgtoa.pdf
index dabe4f8..e27ada0 100644
Binary files a/export/thgtoa.pdf and b/export/thgtoa.pdf differ
diff --git a/export/thgtoa.pdf.b2sum b/export/thgtoa.pdf.b2sum
new file mode 100644
index 0000000..1046edd
--- /dev/null
+++ b/export/thgtoa.pdf.b2sum
@@ -0,0 +1 @@
+436ed0df78c299f95b8d5ff94f43f26ec2e7825d92d843fc15419630d55ed5e0c98485e738c12715a2b6242633faae38e8a98935b361d44ddde97a1692cb01a1 ./export/thgtoa.pdf
diff --git a/export/virus-total-results.md b/export/virus-total-results.md
new file mode 100644
index 0000000..85dbbfa
--- /dev/null
+++ b/export/virus-total-results.md
@@ -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*
diff --git a/mkdocs.yml b/mkdocs.yml
index b01db56..470c179 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -133,6 +133,7 @@ nav:
- Constitution: constitution/index.md
- Mirrors: mirrors/index.md
- Twitter: twitter/index.md
+ - TBA: changelog/index.md
copyright: |
© 2023-2026 Anonymous Planet
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..4c9497b
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,132 @@
+# PDF Build, Scan & Release Scripts
+
+This directory contains scripts for building PDFs from MkDocs documentation, scanning them with VirusTotal, generating hashes, and uploading artifacts to GitHub releases.
+
+## Scripts
+
+### `pdf_release.sh` (v2 - Recommended)
+The main script that handles:
+- SHA256 hash generation for PDF files
+- VirusTotal scanning of PDFs
+- Release creation/update on GitHub
+- GPG signature verification support
+
+**Usage:**
+```bash
+./scripts/pdf_release.sh --build --release [--vt-api-key VT_KEY] [--github-token TOKEN]
+```
+
+**Options:**
+- `--build`: PDF build mode (`light`, `dark`, or `both`) - Required
+- `--release`: Release update mode (`tag` for tagged releases, `latest` to always update) - Default: `tag`
+- `--vt-api-key`: VirusTotal API key (optional)
+- `--github-token`: GitHub token for release operations (optional)
+
+### `build_guide_pdf.py`
+Python script that builds MkDocs documentation and converts it to PDF using Chromium/Chrome.
+
+**Usage:**
+```bash
+python scripts/build_guide_pdf.py --both # Build both light and dark mode
+python scripts/build_guide_pdf.py --dark-mode # Dark mode only
+python scripts/build_guide_pdf.py --skip-mkdocs # Skip MkDocs build, use existing site
+```
+
+## GitHub Actions Workflow
+
+The workflow `.github/workflows/build-pdf-combined.yml` combines all operations:
+
+1. **Build PDFs** - Generates light/dark mode PDFs with GPG signatures
+2. **Scan & Release** - Scans with VirusTotal and updates/releases artifacts
+
+### Required Secrets
+
+Add these to your repository settings under **Settings > Secrets and variables > Actions**:
+
+- `GPG_PRIVATE_KEY`: Your GPG private key for signing
+- `GPG_PASSPHRASE`: Passphrase for the GPG key (if any)
+- `VT_API_KEY`: VirusTotal API key for malware scanning
+- `GITHUB_TOKEN`: Automatically available, but can be manually added
+
+### Workflow Triggers
+
+The workflow runs on:
+- Manual dispatch (`workflow_dispatch`) with customizable options
+- Push to main branch when docs, mkdocs.yml, or scripts change
+
+## Output Files
+
+After running the build and release process, you'll get:
+
+```
+export/
+├── thgtoa.pdf # Light mode PDF
+├── thgtoa-dark.pdf # Dark mode PDF
+├── thgtoa.pdf.sig # GPG signature for light PDF
+├── thgtoa-dark.pdf.sig # GPG signature for dark PDF
+├── thgtoa.pdf.sha256 # SHA256 hash for light PDF
+├── thgtoa-dark.pdf.sha256 # SHA256 hash for dark PDF
+├── sha256sum-combined.txt # Combined hash file
+├── sha256sum-combined.txt.sig # GPG signature for combined hashes
+└── virus-total-results.md # VirusTotal scan results
+```
+
+## Hash Verification
+
+To verify the integrity of downloaded PDFs:
+
+```bash
+# Verify against individual hash file
+sha256sum -c thgtoa.pdf.sha256
+
+# Or verify against combined hash file
+sha256sum -c sha256sum-combined.txt
+```
+
+## VirusTotal Integration
+
+When a `VT_API_KEY` is provided, the script will:
+1. Upload each PDF to VirusTotal's API
+2. Generate individual scan reports
+3. Include VT report links in release notes and artifacts
+
+The VT results file (`virus-total-results.md`) contains:
+- Scan timestamp
+- SHA256 hashes for each PDF
+- Direct links to VirusTotal GUI reports
+
+## Release Management
+
+The script supports two release modes:
+
+1. **Tag mode** (`--release tag`): Updates the release matching the current git tag
+2. **Latest mode** (`--release latest`): Always updates the most recent release (useful for continuous deployment)
+
+When running in a GitHub Actions workflow with a tag push, it will automatically create or update the corresponding release.
+
+## Troubleshooting
+
+### PDF Build Fails
+- Ensure Chrome/Chromium is installed: `sudo apt install chromium-browser`
+- Check MkDocs configuration is valid: `mkdocs build --strict`
+- Verify all documentation files are present and properly formatted
+
+### VirusTotal Scan Fails
+- Check VT_API_KEY secret is correctly set in repository settings
+- Verify the API key has sufficient quota (free tier allows 4 requests/minute)
+- Check network connectivity to VirusTotal API
+
+### Release Upload Fails
+- Ensure GITHUB_TOKEN has appropriate permissions (repo scope)
+- For existing releases, use `--release latest` instead of `tag`
+- Check that the release tag format matches GitHub's requirements (e.g., `v1.0.0`)
+
+## Security Notes
+
+- **GPG Keys**: Never commit private keys to version control. Use GitHub Secrets.
+- **VT API Key**: Keep your VirusTotal API key secret and rotate periodically.
+- **Release Artifacts**: All uploaded artifacts are publicly visible on your releases page.
+
+## License
+
+These scripts are part of the "The How-To Guide To Anonymity" project and follow the same licensing as the main repository.
diff --git a/scripts/build_guide_pdf.py b/scripts/build_guide_pdf.py
index 944787f..c276e2b 100644
--- a/scripts/build_guide_pdf.py
+++ b/scripts/build_guide_pdf.py
@@ -177,13 +177,13 @@ def main() -> int:
"--pdf-light",
type=Path,
default=root / "export" / "thgtoa.pdf",
- help="Output PDF path for light mode (default: ./export/guide.pdf)",
+ help="Output PDF path for light mode (default: ./export/thgtoa.pdf)",
)
ap.add_argument(
"--pdf-dark",
type=Path,
default=root / "export" / "thgtoa-dark.pdf",
- help="Output PDF path for dark mode (default: ./export/guide-dark.pdf)",
+ help="Output PDF path for dark mode (default: ./export/thgtoa-dark.pdf)",
)
ap.add_argument("--skip-mkdocs", action="store_true", help="Reuse existing site dir; only run print-to-pdf.")
ap.add_argument("--dark-mode", action="store_true", help="Generate dark mode PDF only")
diff --git a/scripts/pdf_release.sh b/scripts/pdf_release.sh
deleted file mode 100644
index f5fc452..0000000
--- a/scripts/pdf_release.sh
+++ /dev/null
@@ -1,365 +0,0 @@
-!/bin/bash
-set -e
-
-# PDF Hashing, Scanning, Release and Management script (hSCRAM)
-# Usage: ./pdf_release.sh --build --release [--vt-api-key]
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-ROOT_DIR="$(dirname "$SCRIPT_DIR")"
-EXPORT_DIR="$ROOT_DIR/export"
-
-# Default values
-MODE="both" # light, dark, or both
-RELEASE_MODE="tag" # tag (e.g. "v2.1.2") or "latest"
-VT_API_KEY=""
-GITHUB_TOKEN=""
-
-while [[ $# -gt 0 ]]; do
- case $1 in
- --build)
- MODE="$2"
- shift 2
- ;;
- --release)
- RELEASE_MODE="$2"
- shift 2
- ;;
- --vt-api-key)
- VT_API_KEY="$2"
- shift 2
- ;;
- --github-token)
- GITHUB_TOKEN="$2"
- shift 2
- ;;
- *)
- echo "Unknown option: $1"
- exit 1
- ;;
- esac
-done
-
-if [[ ! "$MODE" =~ ^(light|dark|both)$ ]]; then
- echo "Error: Invalid build mode '$MODE'. Must be 'light', 'dark', or 'both'."
- exit 1
-fi
-
-echo "Hashing, Scanning, Release and Management script (hSCRAM)"
-echo "Mode: $MODE"
-echo "Release Mode: $RELEASE_MODE"
-
-generate_hash() {
- local file="$1"
- sha256sum "$file" | cut -d' ' -f1
-}
-
-create_hash_file() {
- local pdf_path="$1"
- local base_name=$(basename "$pdf_path")
- local hash_file="${EXPORT_DIR}/${base_name}.sha256"
-
- (cd "$EXPORT_DIR" && sha256sum "$base_name") > "$hash_file"
- echo "Created: $hash_file"
-}
-
-scan_with_virustotal() {
- local pdf_path="$1"
- local base_name=$(basename "$pdf_path")
-
- if [[ -z "$VT_API_KEY" ]]; then
- echo "Warning: VT_API_KEY not provided, skipping VirusTotal scan for $base_name"
- return 1
- fi
-
- echo "Scanning $base_name with VirusTotal..."
-
- local upload_response=$(curl -s -X POST \
- -H "x-apikey: $VT_API_KEY" \
- -F "file=@$pdf_path" \
- https://www.virustotal.com/api/v3/files)
-
- if [[ $? -ne 0 ]]; then
- echo "Error uploading $base_name to VirusTotal"
- return 1
- fi
-
- local file_id=$(echo "$upload_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['data']['id'])" 2>/dev/null || echo "")
-
- if [[ -z "$file_id" ]]; then
- echo "Error: Could not extract file ID from VirusTotal response for $base_name"
- echo "Response: $upload_response"
- return 1
- fi
-
- local vt_url="https://www.virustotal.com/gui/file/$file_id"
- echo "$vt_url"
-}
-
-scan_all_pdfs() {
- local results_file="$EXPORT_DIR/virus-total-results.md"
-
- cat > "$results_file" << 'HEADER'
-## VirusTotal Scan Results
-
-**Scan Date:** TIMESTAMP
-
----
-HEADER
-
- sed -i "s/TIMESTAMP/$(date -u +"%Y-%m-%d %H:%M UTC")/" "$results_file"
-
- local pdf_files=()
- if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then
- pdf_files+=("$EXPORT_DIR/thgtoa.pdf")
- fi
- if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then
- pdf_files+=("$EXPORT_DIR/thgtoa-dark.pdf")
- fi
-
- for pdf in "${pdf_files[@]}"; do
- if [[ -f "$pdf" ]]; then
- local base_name=$(basename "$pdf")
- local hash=$(generate_hash "$pdf")
-
- echo "" >> "$results_file"
- echo "### $base_name" >> "$results_file"
- echo "- **SHA256 Hash:** \`$hash\`" >> "$results_file"
-
- if [[ -n "$VT_API_KEY" ]]; then
- local vt_url=$(scan_with_virustotal "$pdf")
- if [[ $? -eq 0 && -n "$vt_url" ]]; then
- echo "- **VirusTotal Report:** [$vt_url]($vt_url)" >> "$results_file"
- else
- echo "- **VirusTotal Report:** Scan failed or API key not provided" >> "$results_file"
- fi
- else
- echo "- **VirusTotal Report:** VT_API_KEY not configured, scan skipped" >> "$results_file"
- fi
-
- create_hash_file "$pdf"
- else
- echo "Warning: $pdf does not exist, skipping..."
- fi
- done
-
- cat >> "$results_file" << 'FOOTER'
-
----
-*Scan performed automatically by GitHub Actions*
-FOOTER
-
- echo "VirusTotal results saved to: $results_file"
-}
-
-update_release() {
- local tag="${1:-}"
- local release_notes="$EXPORT_DIR/release-notes.md"
-
- if [[ "$RELEASE_MODE" == "tag" && -z "$tag" ]]; then
- tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
- fi
-
- if [[ -z "$tag" ]]; then
- echo "Warning: No release tag found, skipping release update."
- return 1
- fi
-
- echo "Updating release for tag: $tag"
-
- cat > "$release_notes" << EOF
-# Release Notes - $tag
-
-**Release Date:** $(date -u +"%Y-%m-%d %H:%M UTC")
-
-## PDF Files
-
-EOF
-
- if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then
- local hash=$(generate_hash "$EXPORT_DIR/thgtoa.pdf")
- echo "- **thgtoa.pdf (Light Mode)**" >> "$release_notes"
- echo " - SHA256: \`$hash\`" >> "$release_notes"
- if [[ -f "$EXPORT_DIR/thgtoa.pdf.sig" ]]; then
- echo " - Signature: \`thgtoa.pdf.sig\` (GPG signed)" >> "$release_notes"
- fi
- fi
-
- if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then
- local hash=$(generate_hash "$EXPORT_DIR/thgtoa-dark.pdf")
- echo "- **thgtoa-dark.pdf (Dark Mode)**" >> "$release_notes"
- echo " - SHA256: \`$hash\`" >> "$release_notes"
- if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf.sig" ]]; then
- echo " - Signature: \`thgtoa-dark.pdf.sig\` (GPG signed)" >> "$release_notes"
- fi
- fi
-
- echo "" >> "$release_notes"
- echo "---" >> "$release_notes"
-
- if [[ -f "$EXPORT_DIR/virus-total-results.md" ]]; then
- echo "## VirusTotal Scan Results" >> "$release_notes"
- echo "" >> "$release_notes"
- cat "$EXPORT_DIR/virus-total-results.md" >> "$release_notes"
- echo "" >> "$release_notes"
- fi
-
- local files_to_upload=""
-
- if [[ -f "$EXPORT_DIR/thgtoa.pdf" ]]; then
- files_to_upload+="$EXPORT_DIR/thgtoa.pdf "
- fi
- if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf" ]]; then
- files_to_upload+="$EXPORT_DIR/thgtoa-dark.pdf "
- fi
- if [[ -f "$EXPORT_DIR/thgtoa.pdf.sig" ]]; then
- files_to_upload+="$EXPORT_DIR/thgtoa.pdf.sig "
- fi
- if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf.sig" ]]; then
- files_to_upload+="$EXPORT_DIR/thgtoa-dark.pdf.sig "
- fi
-
- local combined_hash_file="$EXPORT_DIR/sha256sum-combined.txt"
-
- if [[ -f "$EXPORT_DIR/thgtoa.pdf.sha256" ]]; then
- cat "$EXPORT_DIR/thgtoa.pdf.sha256" >> "$combined_hash_file" 2>/dev/null || true
- fi
- if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf.sha256" ]]; then
- echo "" >> "$combined_hash_file"
- cat "$EXPORT_DIR/thgtoa-dark.pdf.sha256" >> "$combined_hash_file"
- fi
-
- files_to_upload+="$combined_hash_file "
-
- if [[ -n "${GPG_PRIVATE_KEY:-}" && -n "${GPG_PASSPHRASE:-}" ]]; then
- echo "$GPG_PRIVATE_KEY" | gpg --batch --import 2>/dev/null || true
- gpg --batch --yes --armor --detach-sign --output "$combined_hash_file.sig" "$combined_hash_file" 2>/dev/null || true
- if [[ -f "$combined_hash_file.sig" ]]; then
- files_to_upload+="$combined_hash_file.sig "
- fi
- fi
-
- if command -v gh &> /dev/null && [[ -n "$GITHUB_TOKEN" ]]; then
- echo "Uploading release with GitHub CLI..."
-
- local release_exists=$(gh release view "$tag" 2>/dev/null && echo "yes" || echo "no")
-
- if [[ "$release_exists" == "yes" ]]; then
- gh release edit "$tag" --notes-file "$release_notes" 2>/dev/null || {
- echo "Warning: Failed to update release notes"
- }
-
- for file in $files_to_upload; do
- if [[ -f "$file" ]]; then
- local file_name=$(basename "$file")
- gh release upload "$tag" "$file" 2>/dev/null || {
- echo "Warning: Failed to upload $file_name"
- }
- fi
- done
- else
- gh release create "$tag" \
- --title "Release $tag" \
- --notes-file "$release_notes" \
- $files_to_upload 2>/dev/null || {
- echo "Error: Failed to create release"
- return 1
- }
- fi
-
- else
- if [[ -n "$GITHUB_TOKEN" && -n "$tag" ]]; then
- echo "Using GitHub API to upload release..."
-
- local repo="${GITHUB_REPOSITORY:-}"
- local api_url="https://api.github.com/repos/$repo/releases"
-
- local existing_release=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
- "$api_url/tags/$tag")
-
- if [[ $(echo "$existing_release" | grep -c '"id":') -gt 0 ]]; then
- echo "Release already exists, updating..."
- local release_id=$(echo "$existing_release" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
-
- curl -X PATCH \
- -H "Authorization: token $GITHUB_TOKEN" \
- -H "Accept: application/vnd.github.v3+json" \
- "$api_url/$release_id" \
- -d "{\"body\":\"$(cat "$release_notes")\"}" 2>/dev/null || true
-
- for file in $files_to_upload; do
- if [[ -f "$file" ]]; then
- local file_name=$(basename "$file")
- local mime_type=$(file --mime-type -b "$file")
-
- curl -X POST \
- -H "Authorization: token $GITHUB_TOKEN" \
- -H "Content-Type: $mime_type" \
- --data-binary @"$file" \
- "https://uploads.github.com/repos/$repo/releases/$release_id/assets?name=$file_name" 2>/dev/null || {
- echo "Warning: Failed to upload $file_name"
- }
- fi
- done
- else
- echo "Creating new release..."
-
- local create_response=$(curl -s -X POST \
- -H "Authorization: token $GITHUB_TOKEN" \
- -H "Accept: application/vnd.github.v3+json" \
- "$api_url" \
- -d "{\"tag_name\":\"$tag\",\"name\":\"Release $tag\",\"body\":\"$(cat "$release_notes")\"}")
-
- local release_id=$(echo "$create_response" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "")
-
- for file in $files_to_upload; do
- if [[ -f "$file" ]]; then
- local file_name=$(basename "$file")
- local mime_type=$(file --mime-type -b "$file")
-
- curl -X POST \
- -H "Authorization: token $GITHUB_TOKEN" \
- -H "Content-Type: $mime_type" \
- --data-binary @"$file" \
- "https://uploads.github.com/repos/$repo/releases/$release_id/assets?name=$file_name" 2>/dev/null || {
- echo "Warning: Failed to upload $file_name"
- }
- fi
- done
- fi
- else
- echo "Error: GITHUB_TOKEN required for release upload."
- return 1
- fi
- fi
-
- echo "Release update complete!"
-}
-
-echo ""
-echo "Step 1: Generating hashes..."
-if [[ "$MODE" == "light" || "$MODE" == "both" ]]; then
- if [[ -f "$EXPORT_DIR/thgtoa.pdf" ]]; then
- create_hash_file "$EXPORT_DIR/thgtoa.pdf"
- else
- echo "Warning: $EXPORT_DIR/thgtoa.pdf not found. Ensure PDF is built first."
- fi
-fi
-
-if [[ "$MODE" == "dark" || "$MODE" == "both" ]]; then
- if [[ -f "$EXPORT_DIR/thgtoa-dark.pdf" ]]; then
- create_hash_file "$EXPORT_DIR/thgtoa-dark.pdf"
- else
- echo "Warning: $EXPORT_DIR/thgtoa-dark.pdf not found. Ensure PDF is built first."
- fi
-fi
-
-echo ""
-echo "Step 2: Scanning with VirusTotal..."
-scan_all_pdfs
-
-echo ""
-echo "Step 3: Updating release..."
-update_release "$GITHUB_REF_NAME"
-
-echo ""
-echo "PDF Release Script Complete!"
diff --git a/scripts/setup_workflow.py b/scripts/setup_workflow.py
index 1b9bf2f..e67bdd6 100644
--- a/scripts/setup_workflow.py
+++ b/scripts/setup_workflow.py
@@ -169,44 +169,12 @@ GitHub repository:
3. VT_API_KEY (optional but recommended)
- VirusTotal API key for malware scanning
- - Get a free key at: https://www.virustotal.com/gui/join-us
-
-
-HOW TO ADD SECRETS:
-
-1. Go to your repository on GitHub
-2. Click 'Settings' → 'Secrets and variables' → 'Actions'
-3. Click 'New repository secret' for each secret below:
-
- Secret Name | Value Format
- ---------------------|--------------------------------------------------
- GPG_PRIVATE_KEY | Paste the entire ASCII armored key (BEGIN PGP...)
- GPG_PASSPHRASE | Your key's passphrase (no special characters issues)
- VT_API_KEY | Your VirusTotal API key
-
-
-VERIFYING YOUR SETUP:
-
-After adding secrets, you can test by:
-1. Going to 'Actions' tab
-2. Selecting 'Build guide PDF' workflow
-3. Clicking 'Run workflow'
-4. Checking if the workflow completes successfully
-
TROUBLESHOOTING:
- If GPG signing fails: Check that your key has signing capability ('s' flag)
- If passphrase is wrong: Verify you're using the correct passphrase
- If VT scan fails: Ensure API key is valid and within rate limits
-
-
-SECURITY NOTES:
-
-⚠ NEVER share your private key or passphrase publicly
-⚠ Always use repository secrets, never hardcode in scripts
-⚠ Rotate keys periodically if compromised
-⚠ Use strong passphrases (12+ characters recommended)
""")
@@ -220,8 +188,6 @@ def main() -> int:
print("⚠ WARNING: GPG is not installed or not in PATH")
print("Please install GPG before continuing:")
print(" - Linux: sudo apt install gnupg")
- print(" - macOS: brew install gnupg")
- print(" - Windows: https://www.gpg4win.org/")
print("\nContinuing anyway...")
# List available keys
@@ -313,7 +279,7 @@ To get your private key for the GPG_PRIVATE_KEY secret:
print("1. Export your private key (see instructions above)")
print("2. Add all three secrets to GitHub repository settings")
print("3. Test the workflow by triggering a manual build")
- print("\nFor more information, see: docs/guide/pdf-workflow.md\n")
+ print("\nFor more information, see: docs/guide/dev-workflow.md\n")
return 0
diff --git a/scripts/sign-pdfs.sh b/scripts/sign-pdfs.sh
new file mode 100644
index 0000000..05b6aa3
--- /dev/null
+++ b/scripts/sign-pdfs.sh
@@ -0,0 +1,178 @@
+#!/bin/bash
+
+# Script to generate checksums (SHA256, B2SUM) and GPG sign PDF files
+# Usage: ./sign-pdfs.sh [input_directory] [output_directory]
+# If directories are not provided, defaults will be used
+
+set -e # Exit on error
+
+# Configuration
+INPUT_DIR="${1:-./export}" # Default: build-output directory
+OUTPUT_DIR="${2:-./export}" # Default: signed-pdfs directory
+CHECKSUMS_DIR="${3:-./export}" # Default: checksums directory
+GPG_KEY_ID="9FA5436D0EE360985157382517ECA05F768DEDF6"
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+# Function to print colored messages
+print_info() {
+ echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+print_warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Check if required tools are available
+check_dependencies() {
+ print_info "Checking dependencies..."
+
+ for cmd in sha256sum b2sum gpg; do
+ if ! command -v "$cmd" &> /dev/null; then
+ print_error "$cmd is not installed. Please install it and try again."
+ exit 1
+ fi
+ done
+
+ # Check GPG key availability
+ if [ -z "$GPG_KEY_ID" ]; then
+ GPG_KEY_ID="${SIGN_PDF_GPG_KEY:-}"
+ fi
+
+ if [ -n "$GPG_KEY_ID" ]; then
+ if ! gpg --list-keys "$GPG_KEY_ID" &> /dev/null; then
+ print_error "GPG key '$GPG_KEY_ID' not found in your keyring."
+ exit 1
+ fi
+ else
+ # List available keys and prompt user
+ print_warn "No GPG key ID specified. Listing available secret keys:"
+ gpg --list-secret-keys --keyid-format LONG
+
+ read -p "Enter the GPG key ID to use for signing (or press Enter to skip): " GPG_KEY_ID
+ if [ -n "$GPG_KEY_ID" ]; then
+ if ! gpg --list-keys "$GPG_KEY_ID" &> /dev/null; then
+ print_error "GPG key '$GPG_KEY_ID' not found in your keyring."
+ exit 1
+ fi
+ else
+ print_warn "No GPG signing will be performed. Set SIGN_PDF_GPG_KEY environment variable or pass key ID as argument."
+ fi
+ fi
+
+ print_info "All dependencies checked successfully!"
+}
+
+# Create output directories
+setup_directories() {
+ print_info "Setting up directories..."
+
+ if [ ! -d "$INPUT_DIR" ]; then
+ print_error "Input directory '$INPUT_DIR' does not exist."
+ exit 1
+ fi
+
+ mkdir -p "$OUTPUT_DIR"
+ mkdir -p "$CHECKSUMS_DIR"
+
+ print_info "Input directory: $INPUT_DIR"
+ print_info "Output directory: $OUTPUT_DIR"
+ print_info "Checksums directory: $CHECKSUMS_DIR"
+}
+
+# Generate SHA256 checksum for a file
+generate_sha256() {
+ local file="$1"
+ local filename=$(basename "$file")
+ local output_file="${CHECKSUMS_DIR}/${filename}.sha256"
+
+ sha256sum "$file" > "$output_file"
+ print_info "SHA256 checksum generated: $output_file"
+}
+
+# Generate B2SUM checksum for a file
+generate_b2sum() {
+ local file="$1"
+ local filename=$(basename "$file")
+ local output_file="${CHECKSUMS_DIR}/${filename}.b2sum"
+
+ b2sum "$file" > "$output_file"
+ print_info "B2SUM checksum generated: $output_file"
+}
+
+# GPG sign a file
+gpg_sign() {
+ local file="$1"
+ local filename=$(basename "$file")
+
+ if [ -z "$GPG_KEY_ID" ]; then
+ print_warn "Skipping GPG signing for '$filename' (no key ID provided)"
+ return 0
+ fi
+
+ # Sign the file in detached mode with ASCII armor
+ gpg --batch --yes --detach-sign --armor --local-user "$GPG_KEY_ID" \
+ --output "${file}.sig" "$file"
+
+ print_info "GPG signature generated: ${file}.sig"
+}
+
+# Process a single PDF file
+process_pdf() {
+ local pdf_file="$1"
+ local filename=$(basename "$pdf_file")
+
+ print_info "Processing: $filename"
+
+ # Generate checksums
+ generate_sha256 "$pdf_file"
+ generate_b2sum "$pdf_file"
+
+ # GPG sign if key is available
+ gpg_sign "$pdf_file"
+}
+
+# Main function
+main() {
+ echo ""
+ check_dependencies
+ setup_directories
+
+ # Find all PDF files in input directory (recursively)
+ pdf_files=($(find "$INPUT_DIR" -type f -name "*.pdf"))
+
+ if [ ${#pdf_files[@]} -eq 0 ]; then
+ print_error "No PDF files found in '$INPUT_DIR'"
+ exit 1
+ fi
+
+ print_info "Found ${#pdf_files[@]} PDF file(s) to process"
+
+ # Process each PDF file
+ for pdf_file in "${pdf_files[@]}"; do
+ process_pdf "$pdf_file"
+ done
+
+ print_info "=========================================="
+ print_info "Processing Complete!"
+ print_info "=========================================="
+ print_info "Checksums saved to: $CHECKSUMS_DIR"
+ print_info "Signed files and signatures in: $(dirname "$INPUT_DIR")"
+
+ # Display summary of checksums
+ print_info "SHA256 Checksums:"
+ cat "${CHECKSUMS_DIR}"/*.sha256 2>/dev/null || true
+ print_info "B2SUM Checksums:"
+ cat "${CHECKSUMS_DIR}"/*.b2sum 2>/dev/null || true
+}
+
+# Run main function
+main "$@"
diff --git a/scripts/verify_pdf.py b/scripts/verify_pdf.py
index 7f31bdb..deb807b 100644
--- a/scripts/verify_pdf.py
+++ b/scripts/verify_pdf.py
@@ -26,11 +26,9 @@ import subprocess
import sys
from pathlib import Path
-
def repo_root() -> Path:
return Path(__file__).resolve().parent.parent
-
def calculate_sha256(file_path: Path) -> str:
"""Calculate SHA256 hash of a file."""
sha256_hash = hashlib.sha256()
@@ -39,7 +37,6 @@ def calculate_sha256(file_path: Path) -> str:
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
-
def verify_hash(file_path: Path, expected_hash: str) -> bool:
"""Verify file hash against expected value."""
actual_hash = calculate_sha256(file_path)
@@ -50,7 +47,6 @@ def verify_hash(file_path: Path, expected_hash: str) -> bool:
print(f" Actual: {actual_hash}")
return is_valid
-
def verify_signature(file_path: Path, sig_file: Path) -> bool:
"""Verify GPG signature of a file."""
if not sig_file.exists():
@@ -81,7 +77,6 @@ def verify_signature(file_path: Path, sig_file: Path) -> bool:
print("⚠ WARNING: GPG not installed. Skipping signature verification.")
return None
-
def verify_from_hash_file(file_path: Path, hash_file: Path) -> bool:
"""Verify file hash from a hash file."""
if not hash_file.exists():
@@ -102,7 +97,6 @@ def verify_from_hash_file(file_path: Path, hash_file: Path) -> bool:
return verify_hash(file_path, expected_hash)
-
def check_virustotal(file_hash: str, api_key: str | None = None) -> dict | None:
"""Check VirusTotal scan status for a file hash."""
if not api_key:
@@ -127,9 +121,9 @@ def check_virustotal(file_hash: str, api_key: str | None = None) -> dict | None:
if stats:
print(f" Malicious: {stats.get('malicious', 0)}")
- print(f" Suspicious: {stats.get('suspicious', 0)}")
- print(f" Undetected: {stats.get('undetected', 0)}")
- print(f" Clean: {stats.get('harmless', 0)}")
+ print(f" Suspicious: {stats.get('suspicious', 0)}")
+ print(f" Undetected: {stats.get('undetected', 0)}")
+ print(f" Clean: {stats.get('harmless', 0)}")
return data
@@ -137,7 +131,6 @@ def check_virustotal(file_hash: str, api_key: str | None = None) -> dict | None:
print(f"⚠ ERROR checking VirusTotal: {e}")
return None
-
def main() -> int:
root = repo_root()
ap = argparse.ArgumentParser(description="Verify PDF files (hashes, signatures, VT).")
@@ -158,7 +151,7 @@ def main() -> int:
ap.add_argument(
"--hash-file",
type=Path,
- default=root / "sha256sum-light.txt",
+ default=root / "export" / "thgtoa.pdf.sha256",
help="Hash file to verify against",
)
@@ -217,6 +210,5 @@ def main() -> int:
print("✗ Some verifications FAILED")
return 1
-
if __name__ == "__main__":
raise SystemExit(main())