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. ![Anonymous Planet logo](media/profile.png){ align=right } -Anonymous Planet is a collective of volunteers and contributors. No one person is considered more valuable than another, and no one person should be viewed as having "more impact" on Anonymous Planet. +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())