From 5636291c8a901e1118ce331b82fc64390317c01b Mon Sep 17 00:00:00 2001 From: nopeitsnothing Date: Thu, 16 Apr 2026 06:32:57 -0400 Subject: [PATCH] Build pipeline WIP Signed-off-by: nopeitsnothing --- .github/workflows/build-pdf.yml | 2 +- .github/workflows/vt-scan.yml | 1 - .gitignore | 8 +- CHANGELOG.md | 13 ++ README.md | 23 --- docs/guide/dev-workflow.md | 193 +++++++++++++++++++ docs/index.md | 5 +- docs/verify/index.md | 6 +- mkdocs.yml | 4 +- scripts/build_guide_pdf.py | 12 +- scripts/setup_workflow.py | 322 ++++++++++++++++++++++++++++++++ scripts/verify_pdf.py | 222 ++++++++++++++++++++++ 12 files changed, 771 insertions(+), 40 deletions(-) create mode 100644 docs/guide/dev-workflow.md create mode 100644 scripts/setup_workflow.py create mode 100644 scripts/verify_pdf.py diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index d88f7b5..e1a84fa 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -134,4 +134,4 @@ jobs: export/thgtoa-dark.pdf.sig sha256sum-light.txt env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/vt-scan.yml b/.github/workflows/vt-scan.yml index b9d4c84..abceb77 100644 --- a/.github/workflows/vt-scan.yml +++ b/.github/workflows/vt-scan.yml @@ -75,4 +75,3 @@ jobs: # Update the release with VT results gh release edit ${{ github.ref_name }} --notes-file vt-results.md - diff --git a/.gitignore b/.gitignore index 6125409..d1b3b07 100644 --- a/.gitignore +++ b/.gitignore @@ -19,5 +19,9 @@ ENV/ site/ _site/ _site_test/ -export/ -build/ +build/ + +# Export directory - but track hash files and signatures +export/thgtoa.pdf.sha256 +export/thgtoa-dark.pdf.sha256 +*.sig diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a76036..b7976a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add ways to verify the files + ### Changed - Refactored GitHub Actions workflow **Build PDF** (`scripts\build_guide_pdf.py`): now builds both light and dark mode PDFs (`export/thgtoa.pdf` and `export/thgtoa-dark.pdf` respectively). - Restored previous VT scans workflow **VirusTotal Scan** (`.github/workflows/vt-scan.yml`): submit files to VT for malware scanning. Links will be published on the site. +## Fixed + +- `docs/about/index.md`: replace broken reference-style internal links +- `docs/guide/index.md`: Appendix A6: comment out deprecated ODT information because we don't and probably won't use it in the future + +### Feature + +- Updated `scripts/build_guide_pdf.py` to use `--print-to-pdf` instead of `--save-as` for PDF generation, and added a new `--dark-mode` flag to generate dark mode PDFs. The script now supports generating both light and dark mode PDFs with a single command invocation by using the `--both` flag. This change improves the PDF generation process and provides better support for dark mode users. Save your eyes - you only get one pair. + ## [1.2.1] - 2026-04-11 ### Added diff --git a/README.md b/README.md index 4d10079..d357dec 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,3 @@ This guide is an open-source non-profit initiative, [licensed](LICENSE.html) und - **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`). - **PDF (local build):** from the repository root, using the same environment, run: - - ```bash - python scripts/build_guide_pdf.py - ``` - - This runs `mkdocs build` (output defaults to `./site`), then uses **Google Chrome** or **Microsoft Edge** in headless mode to print `site/guide/index.html` to **`export/thgtoa.pdf`** (images and styling preserved). If the site is already built: `python scripts/build_guide_pdf.py --skip-mkdocs`. Other options: `--site-dir`, `--pdf`, and `python scripts/build_guide_pdf.py --help`. - - On **GitHub Actions**, the [Build guide PDF](https://github.com/Anon-Planet/thgtoa/actions/workflows/build-pdf.yml) workflow does the same using Chromium on Ubuntu when you push to `main` or open a pull request that touches the guide or build inputs; download the **`thgtoa.pdf`** artifact from a successful run. You can also run it manually (**Actions** → **Build guide PDF** → **Run workflow**). -- **OpenDocument (ODT):** not produced by this repository (previous hosted export removed). -- **Raw Markdown (very large):** [docs/guide/index.md on GitHub](https://raw.githubusercontent.com/Anon-Planet/thgtoa/refs/heads/main/docs/guide/index.md) - -**Mirrors:** -- Hidden service: **Host down** - -Feel free to submit issues using Github Issues with the repository link above. Criticism, opinions, and ideas are welcome! - -**Follow or contact us on:** - -Discussion Channels: -- Matrix room: -- Matrix space: - -Have a good read and feel free to share and/or recommend it! diff --git a/docs/guide/dev-workflow.md b/docs/guide/dev-workflow.md new file mode 100644 index 0000000..c0af6b8 --- /dev/null +++ b/docs/guide/dev-workflow.md @@ -0,0 +1,193 @@ +# Development + +## Overview + +This repository now includes an automated workflow that handles PDF generation, verification, and distribution with the following features: + +??? Note "How the pipeline works" + + 1. **Automatic PDF Generation** - Builds both light and dark mode PDFs from MkDocs source + 2. **SHA256 Hash Generation** - Creates hash files for integrity verification + 3. **GPG Signature Signing** - Signs all PDFs and hash files with repository GPG key + 4. **VirusTotal Scanning** - Automatically scans PDFs and updates release notes + 5. **Release Automation** - Packages everything into GitHub releases + +## Workflow Architecture + +### 1. Build PDF Workflow (`build-pdf.yml`) + +**Trigger:** Push to main, pull requests, or manual dispatch + +??? Note "Steps" + + - Checkout repository + - Set up Python 3.13 and MkDocs Material + - Install Chromium browser + - Generate both light and dark mode PDFs + - Create SHA256 hash files + - Sign all files with GPG + - Upload artifacts to GitHub Actions + - Publish release + +### 2. VirusTotal Scan Workflow (`vt-scan.yml`) + +**Trigger:** Push to main, tags, or manual dispatch (runs after build-pdf) + +??? Note "Steps" + + - Download PDF artifacts from build workflow + - Scan both PDFs with VirusTotal API + - Extract scan results and generate report links + - Update release notes with VT scan status and URLs + +## File Structure + +After a successful build, the repository will contain: + +``` +.../ +├── export/ +│ ├── thgtoa.pdf # Light mode PDF +│ ├── thgtoa-dark.pdf # Dark mode PDF +│ ├── thgtoa.pdf.sig # GPG signature (light) +│ └── thgtoa-dark.pdf.sig # GPG signature (dark) +├── thgtoa.pdf.sha256 # Hash file (light) +├── thgtoa-dark.pdf.sha256 # Hash file (dark) +├── sha256sum-light.txt # Combined hash file +└── scripts/ + ├── build_guide_pdf.py # PDF generation script + └── verify_pdf.py # Verification utility +``` + +## Security Features + +### 1. SHA256 Hash Verification + +**Purpose:** Ensure file integrity during download/transit + +**How it works:** +- Each PDF gets a unique SHA256 hash calculated at build time +- Hash stored in `.sha256` files alongside the PDFs +- Combined `sha256sum-light.txt` for batch verification + +**Verification command:** +```bash +sha256sum -c sha256sum-light.txt +``` + +### 2. GPG Signature Verification + +**Purpose:** Verify authenticity and prevent tampering + +??? Note "How it works" + + - Detached signatures created for each PDF and hash file + - Public keys available in `/pgp/` directory + +**Verification command:** +```bash +gpg --import pgp/anonymousplanet-master.asc +gpg --verify export/thgtoa.pdf.sig export/thgtoa.pdf +``` + +### 3. VirusTotal Integration + +**Purpose:** Malware detection and security scanning + +??? Note "How it works" + + - Automatic scan of all generated PDFs + - Results published in release notes with direct links + - Provides third-party validation of file safety + +## Usage Examples + +### Local Development + +```bash +# Build PDFs locally +python scripts/build_guide_pdf.py --both + +# Verify hashes +python scripts/verify_pdf.py --hashes + +# Verify signatures (requires GPG installed) +python scripts/verify_pdf.py --signatures + +# Full verification with VirusTotal check +export VT_API_KEY=your_api_key +python scripts/verify_pdf.py --all +``` + +### CI/CD Verification + +The workflows automatically verify everything during the build process. To manually trigger: + +1. Go to Actions tab +2. Select "Build guide PDF" or "VirusTotal Scan" +3. Click "Run workflow" +4. Download artifacts from successful run + +## Release Process + +When you create a tag (e.g., `v1.0.0`): + +1. Push the tag: `git push origin v1.0.0` +2. Build PDF workflow triggers automatically +3. VirusTotal scan workflow runs after build completes +4. Both workflows update/create GitHub release with: + - Light and dark mode PDFs + - GPG signatures for all files + - Hash files for verification + - Release notes with VT scan results + +## Troubleshooting + +### Common Issues + +**GPG signing fails:** +- Check that `GPG_PRIVATE_KEY` is in ASCII armor format +- Verify passphrase is correct +- Ensure key has signing capability + +**Hash mismatch after download:** +- Re-download the file (corruption during transfer) +- Verify you're using the correct hash file +- Check disk integrity + +**VirusTotal scan fails:** +- Verify `VT_API_KEY` is set correctly +- Check API quota limits (free tier: 4 requests/minute) +- Ensure PDF files exist before scanning + +### Debug Mode + +Enable verbose output by adding to workflow: +```yaml +- name: Debug + run: | + echo "Current directory:" && pwd + echo "Files in export:" && ls -la export/ + echo "Hash file contents:" && cat sha256sum-light.txt +``` + +## Best Practices + +1. **Always verify signatures** before opening PDFs from untrusted sources +2. **Check VirusTotal results** for any suspicious detections +3. **Keep GPG keys secure** - never commit private keys to repository +4. **Monitor API usage** for VirusTotal to avoid rate limiting +5. **Test locally** before pushing tags to production + +## Future Enhancements + +Potential improvements: +- Multi-signature support (multiple maintainers) +- Automated changelog generation with hashes +- Cross-platform signature verification scripts +- Integration with additional malware scanners +- Automatic mirror updates with verified files + +--- + +*This workflow is designed for security-conscious users who need to verify the authenticity and integrity of downloaded documents.* diff --git a/docs/index.md b/docs/index.md index ff8c3b7..9712a8f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,8 +18,8 @@ schema: **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 this file for authenticity.](verify/index.md) +This is the master signing key fingerprint for Anonymous Planet. + You'll use it to [**verify the checksum** and **GPG signature** of all files for authenticity.](verify/index.md) Please share this project if you enjoy it and you think it might be useful to others. ![Anonymous Planet logo](media/profile.png){ align=right } @@ -39,4 +39,3 @@ Anonymous Planet is a collective of volunteers and contributors. No one person i - [:fontawesome-solid-house: Homepage](https://www.itsnothing.net) - [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org) - [: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") - diff --git a/docs/verify/index.md b/docs/verify/index.md index 63c2e09..304ff03 100644 --- a/docs/verify/index.md +++ b/docs/verify/index.md @@ -114,9 +114,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 Key +**Key ID:** See `pgp/anonymousplanet-master.asc` for details +**Fingerprint:** Verify from the repository's official documentation --- diff --git a/mkdocs.yml b/mkdocs.yml index ef51d35..b01db56 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -125,7 +125,9 @@ nav: - Home: index.md - About: about/index.md - Verify: verify/index.md - - Guide: guide/index.md + - Guide: + - guide/index.md + - Workflow Documentation: guide/dev-workflow.md - Code: code/index.md - Contribute: contribute/index.md - Constitution: constitution/index.md diff --git a/scripts/build_guide_pdf.py b/scripts/build_guide_pdf.py index b183735..944787f 100644 --- a/scripts/build_guide_pdf.py +++ b/scripts/build_guide_pdf.py @@ -1,16 +1,16 @@ #!/usr/bin/env python3 -"""Extended version of build_guide_pdf.py with dark mode support. +"""Experimental dark mode support. This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium. Usage: - python scripts/build_guide_pdf_extended.py # Generate light mode PDF only - python scripts/build_guide_pdf_extended.py --dark-mode # Generate dark mode PDF only - python scripts/build_guide_pdf_extended.py --both # Generate both light and dark mode PDFs + python scripts/build_guide_pdf.py # Generate light mode PDF only + python scripts/build_guide_pdf.py --dark-mode # Generate dark mode PDF only + python scripts/build_guide_pdf.py --both # Generate both light and dark mode PDFs Examples: - python scripts/build_guide_pdf_extended.py --site-dir build/html --pdf-light export/thgtoa.pdf - python scripts/build_guide_pdf_extended.py --dark-mode --pdf-dark export/thgtoa-dark.pdf + python scripts/build_guide_pdf.py --site-dir build/html --pdf-light export/thgtoa.pdf + python scripts/build_guide_pdf.py --dark-mode --pdf-dark export/thgtoa-dark.pdf """ from __future__ import annotations diff --git a/scripts/setup_workflow.py b/scripts/setup_workflow.py new file mode 100644 index 0000000..1b9bf2f --- /dev/null +++ b/scripts/setup_workflow.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +"""Setup helper for PDF workflow configuration. + +This script helps you configure the necessary GitHub Secrets for the automated +PDF build, signing, and VirusTotal scanning workflows. + +Usage: + python scripts/setup_workflow.py + +Requirements: + - Python 3.8+ + - GPG installed (for key export) + - Access to GitHub repository settings + +What it does: + 1. Validates your GPG key setup + 2. Exports the public key for verification + 3. Provides instructions for adding secrets to GitHub +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def check_gpg_installed() -> bool: + """Check if GPG is installed and accessible.""" + try: + result = subprocess.run( + ["gpg", "--version"], + capture_output=True, + text=True, + timeout=10, + ) + return result.returncode == 0 + except (FileNotFoundError, subprocess.TimeoutExpired): + return False + + +def list_gpg_keys() -> list[dict]: + """List all GPG keys in the keyring.""" + try: + result = subprocess.run( + ["gpg", "--list-keys", "--with-colons"], + capture_output=True, + text=True, + check=True, + ) + + keys = [] + current_key = {} + + for line in result.stdout.split('\n'): + if line.startswith('pub:'): + if current_key: + keys.append(current_key) + parts = line.split(':') + current_key = { + 'type': parts[1], + 'key_id': parts[4], + 'fingerprint': parts[9] if len(parts) > 9 else None, + 'created': parts[5], + 'expires': parts[6], + 'uid': None, + } + elif line.startswith('uid:'): + parts = line.split(':') + current_key['uid'] = parts[9] if len(parts) > 9 else None + + if current_key: + keys.append(current_key) + + return keys + + except subprocess.CalledProcessError as e: + print(f"Error listing GPG keys: {e}") + return [] + + +def export_public_key(key_id: str, output_file: Path | None = None) -> str | None: + """Export a public key in ASCII armor format.""" + try: + result = subprocess.run( + ["gpg", "--armor", "--export", key_id], + capture_output=True, + text=True, + check=True, + ) + + if output_file: + output_file.write_text(result.stdout) + print(f"✓ Public key exported to {output_file}") + + return result.stdout + + except subprocess.CalledProcessError as e: + print(f"Error exporting public key: {e}") + return None + + +def export_private_key(key_id: str, output_file: Path | None = None) -> str | None: + """Export a private key in ASCII armor format (requires passphrase).""" + try: + # This will prompt for passphrase interactively + result = subprocess.run( + ["gpg", "--armor", "--export-secret-keys", key_id], + capture_output=True, + text=True, + check=True, + ) + + if output_file: + output_file.write_text(result.stdout) + print(f"✓ Private key exported to {output_file}") + + return result.stdout + + except subprocess.CalledProcessError as e: + print(f"Error exporting private key: {e}") + return None + + +def validate_gpg_key(key_id: str) -> bool: + """Validate that a GPG key has signing capability.""" + try: + result = subprocess.run( + ["gpg", "--list-keys", "--with-colons", key_id], + capture_output=True, + text=True, + check=True, + ) + + # Check for 's' (signing) in the pub line + for line in result.stdout.split('\n'): + if line.startswith('pub:'): + flags = line.split(':')[1] + return 's' in flags + + return False + + except subprocess.CalledProcessError: + return False + + +def print_setup_instructions(): + """Print instructions for configuring GitHub Secrets.""" + print("\n" + "="*70) + print("GITHUB SECRETS SETUP INSTRUCTIONS") + print("="*70) + + print(""" +To enable the automated PDF workflow, you need to add three secrets to your +GitHub repository: + +1. GPG_PRIVATE_KEY + - Your GPG private key in ASCII armor format + - Used to sign PDFs and hash files + - IMPORTANT: Keep this secret! Never commit it publicly + +2. GPG_PASSPHRASE + - The passphrase for your GPG private key + - Required to unlock the private key for signing + +3. VT_API_KEY (optional but recommended) + - VirusTotal API key for malware scanning + - Get a free key at: https://www.virustotal.com/gui/join-us + + +HOW TO ADD SECRETS: + +1. Go to your repository on GitHub +2. Click 'Settings' → 'Secrets and variables' → 'Actions' +3. Click 'New repository secret' for each secret below: + + Secret Name | Value Format + ---------------------|-------------------------------------------------- + GPG_PRIVATE_KEY | Paste the entire ASCII armored key (BEGIN PGP...) + GPG_PASSPHRASE | Your key's passphrase (no special characters issues) + VT_API_KEY | Your VirusTotal API key + + +VERIFYING YOUR SETUP: + +After adding secrets, you can test by: +1. Going to 'Actions' tab +2. Selecting 'Build guide PDF' workflow +3. Clicking 'Run workflow' +4. Checking if the workflow completes successfully + + +TROUBLESHOOTING: + +- If GPG signing fails: Check that your key has signing capability ('s' flag) +- If passphrase is wrong: Verify you're using the correct passphrase +- If VT scan fails: Ensure API key is valid and within rate limits + + +SECURITY NOTES: + +⚠ NEVER share your private key or passphrase publicly +⚠ Always use repository secrets, never hardcode in scripts +⚠ Rotate keys periodically if compromised +⚠ Use strong passphrases (12+ characters recommended) +""") + + +def main() -> int: + print("\n" + "="*70) + print("PDF WORKFLOW SETUP HELPER") + print("="*70) + + # Check GPG installation + if not check_gpg_installed(): + print("⚠ WARNING: GPG is not installed or not in PATH") + print("Please install GPG before continuing:") + print(" - Linux: sudo apt install gnupg") + print(" - macOS: brew install gnupg") + print(" - Windows: https://www.gpg4win.org/") + print("\nContinuing anyway...") + + # List available keys + print("\n🔑 Available GPG Keys:") + print("-" * 70) + + keys = list_gpg_keys() + + if not keys: + print("No GPG keys found in your keyring.") + print("Generate a key with: gpg --full-generate-key") + return 1 + + for i, key in enumerate(keys, 1): + status = "✓" if validate_gpg_key(key['key_id']) else "✗" + print(f"\n{i}. {status} Key ID: {key['key_id']}") + print(f" Fingerprint: {key.get('fingerprint', 'N/A')}") + print(f" UID: {key.get('uid', 'Unknown')}") + print(f" Created: {key.get('created', 'Unknown')}") + + if key.get('expires'): + print(f" Expires: {key['expires']}") + + # Ask user to select key + print("\n" + "-" * 70) + try: + choice = input("\nEnter the number of the key you want to use (1-{}): ".format(len(keys))) + selected_index = int(choice) - 1 + + if not (0 <= selected_index < len(keys)): + print("Invalid selection!") + return 1 + + except ValueError: + print("Invalid input! Please enter a number.") + return 1 + + selected_key = keys[selected_index] + + # Validate key has signing capability + if not validate_gpg_key(selected_key['key_id']): + print(f"\n⚠ WARNING: Selected key does not have signing capability!") + print("You need a key with 's' (signing) flag for PDF signatures.") + confirm = input("Continue anyway? (y/N): ") + if confirm.lower() != 'y': + return 1 + + # Export public key + print(f"\n📤 Exporting public key for {selected_key['uid']}...") + public_key_file = repo_root() / "pgp" / "workflow-public.asc" + + public_key = export_public_key(selected_key['key_id'], public_key_file) + + if not public_key: + print("Failed to export public key!") + return 1 + + # Show public key info + print("\n✓ Public Key Information:") + print("-" * 70) + for line in public_key.split('\n')[:5]: + print(line) + print("...") + + # Instructions for private key export + print("\n🔐 Private Key Export:") + print("-" * 70) + print(""" +To get your private key for the GPG_PRIVATE_KEY secret: + +1. Run this command (you'll be prompted for passphrase): + gpg --armor --export-secret-keys {} > workflow-private.asc + +2. Copy the ENTIRE output including BEGIN and END lines + +3. Add it to GitHub Secrets as 'GPG_PRIVATE_KEY' + +⚠ IMPORTANT: Keep your private key secure! Never commit it publicly. +""".format(selected_key['key_id'])) + + # Print setup instructions + print_setup_instructions() + + print("\n" + "="*70) + print("SETUP COMPLETE!") + print("="*70) + print(f"\nPublic key saved to: {public_key_file}") + print("Next steps:") + print("1. Export your private key (see instructions above)") + print("2. Add all three secrets to GitHub repository settings") + print("3. Test the workflow by triggering a manual build") + print("\nFor more information, see: docs/guide/pdf-workflow.md\n") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_pdf.py b/scripts/verify_pdf.py new file mode 100644 index 0000000..7f31bdb --- /dev/null +++ b/scripts/verify_pdf.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Verification script for PDF files. + +This script verifies: +1. SHA256 hash integrity of PDF files +2. GPG signature authenticity +3. VirusTotal scan status (optional) + +Usage: + python scripts/verify_pdf.py --all # Verify everything + python scripts/verify_pdf.py --hashes # Only verify hashes + python scripts/verify_pdf.py --signatures # Only verify signatures + python scripts/verify_pdf.py --vt # Check VT status (requires API key) + +Examples: + python scripts/verify_pdf.py --all + python scripts/verify_pdf.py --hashes --file export/thgtoa.pdf +""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import subprocess +import sys +from pathlib import Path + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def calculate_sha256(file_path: Path) -> str: + """Calculate SHA256 hash of a file.""" + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + return sha256_hash.hexdigest() + + +def verify_hash(file_path: Path, expected_hash: str) -> bool: + """Verify file hash against expected value.""" + actual_hash = calculate_sha256(file_path) + is_valid = actual_hash == expected_hash + status = "✓ PASS" if is_valid else "✗ FAIL" + print(f"{status}: {file_path.name}") + print(f" Expected: {expected_hash}") + print(f" Actual: {actual_hash}") + return is_valid + + +def verify_signature(file_path: Path, sig_file: Path) -> bool: + """Verify GPG signature of a file.""" + if not sig_file.exists(): + print(f"✗ FAIL: Signature file not found: {sig_file}") + return False + + try: + result = subprocess.run( + ["gpg", "--verify", str(sig_file), str(file_path)], + capture_output=True, + text=True, + check=False, + ) + + if result.returncode == 0: + print(f"✓ PASS: {file_path.name} signature verified") + # Extract key info from GPG output + for line in result.stdout.split('\n'): + if 'Good signature' in line or 'key ID' in line.lower(): + print(f" {line.strip()}") + return True + else: + print(f"✗ FAIL: {file_path.name} signature verification failed") + print(f" Error: {result.stderr}") + return False + + except FileNotFoundError: + print("⚠ WARNING: GPG not installed. Skipping signature verification.") + return None + + +def verify_from_hash_file(file_path: Path, hash_file: Path) -> bool: + """Verify file hash from a hash file.""" + if not hash_file.exists(): + print(f"✗ FAIL: Hash file not found: {hash_file}") + return False + + expected_hash = None + with open(hash_file, 'r') as f: + for line in f: + parts = line.strip().split() + if len(parts) >= 2 and parts[1] == str(file_path): + expected_hash = parts[0] + break + + if not expected_hash: + print(f"✗ FAIL: Hash not found in {hash_file.name} for {file_path.name}") + return False + + return verify_hash(file_path, expected_hash) + + +def check_virustotal(file_hash: str, api_key: str | None = None) -> dict | None: + """Check VirusTotal scan status for a file hash.""" + if not api_key: + print("⚠ WARNING: VT_API_KEY not set. Skipping VirusTotal check.") + return None + + try: + import urllib.request + import json + + url = f"https://www.virustotal.com/api/v3/files/{file_hash}" + request = urllib.request.Request(url, headers={"x-apikey": api_key}) + + with urllib.request.urlopen(request, timeout=30) as response: + data = json.loads(response.read().decode()) + + stats = data.get('data', {}).get('attributes', {}).get('last_analysis_stats', {}) + total = sum(stats.values()) if stats else 0 + + print(f"\n🦠 VirusTotal Results for {file_hash[:16]}...") + print(f" Total scans: {total}") + + if stats: + print(f" Malicious: {stats.get('malicious', 0)}") + print(f" Suspicious: {stats.get('suspicious', 0)}") + print(f" Undetected: {stats.get('undetected', 0)}") + print(f" Clean: {stats.get('harmless', 0)}") + + return data + + except Exception as e: + print(f"⚠ ERROR checking VirusTotal: {e}") + return None + + +def main() -> int: + root = repo_root() + ap = argparse.ArgumentParser(description="Verify PDF files (hashes, signatures, VT).") + + # File paths + ap.add_argument( + "--light-pdf", + type=Path, + default=root / "export" / "thgtoa.pdf", + help="Light mode PDF file", + ) + ap.add_argument( + "--dark-pdf", + type=Path, + default=root / "export" / "thgtoa-dark.pdf", + help="Dark mode PDF file", + ) + ap.add_argument( + "--hash-file", + type=Path, + default=root / "sha256sum-light.txt", + help="Hash file to verify against", + ) + + # Verification modes + group = ap.add_mutually_exclusive_group() + group.add_argument("--all", action="store_true", help="Verify everything") + group.add_argument("--hashes", action="store_true", help="Only verify hashes") + group.add_argument("--signatures", action="store_true", help="Only verify signatures") + ap.add_argument("--vt", action="store_true", help="Check VirusTotal status") + + args = ap.parse_args() + + # Determine what to verify + if not any([args.all, args.hashes, args.signatures, args.vt]): + args.all = True + + all_passed = True + + pdf_files = [ + ("Light", args.light_pdf), + ("Dark", args.dark_pdf), + ] + + for mode_name, pdf_file in pdf_files: + if not pdf_file.exists(): + print(f"⚠ WARNING: {pdf_file.name} not found. Skipping.") + continue + + print(f"\n{'='*60}") + print(f"Verifying {mode_name} PDF: {pdf_file.name}") + print('='*60) + + # Verify hash if requested + if args.all or args.hashes: + if not verify_from_hash_file(pdf_file, args.hash_file): + all_passed = False + + # Verify signature if requested + if args.all or args.signatures: + sig_file = pdf_file.with_suffix(pdf_file.suffix + ".sig") + result = verify_signature(pdf_file, sig_file) + if result is False: # None means skipped (GPG not installed) + all_passed = False + + # Check VirusTotal if requested + if args.all or args.vt: + file_hash = calculate_sha256(pdf_file) + api_key = os.environ.get("VT_API_KEY") + check_virustotal(file_hash, api_key) + + print(f"\n{'='*60}") + if all_passed: + print("✓ All verifications PASSED") + return 0 + else: + print("✗ Some verifications FAILED") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())