mirror of
https://github.com/Anon-Planet/thgtoa.git
synced 2026-05-06 11:34:18 +02:00
Build pipeline WIP
Signed-off-by: nopeitsnothing <no@anonymousplanet.org>
This commit is contained in:
@@ -75,4 +75,3 @@ jobs:
|
|||||||
|
|
||||||
# Update the release with VT results
|
# Update the release with VT results
|
||||||
gh release edit ${{ github.ref_name }} --notes-file vt-results.md
|
gh release edit ${{ github.ref_name }} --notes-file vt-results.md
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -19,5 +19,9 @@ ENV/
|
|||||||
site/
|
site/
|
||||||
_site/
|
_site/
|
||||||
_site_test/
|
_site_test/
|
||||||
export/
|
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# Export directory - but track hash files and signatures
|
||||||
|
export/thgtoa.pdf.sha256
|
||||||
|
export/thgtoa-dark.pdf.sha256
|
||||||
|
*.sig
|
||||||
|
|||||||
@@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add ways to verify the files
|
||||||
|
|
||||||
### Changed
|
### 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).
|
- 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 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
|
## [1.2.1] - 2026-04-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -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.
|
- **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`).
|
- **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:
|
- **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:**
|
|
||||||
- <del>Hidden service: <http://thgtoa3jzy3doku7hkna32htpghjijefscwvh4dyjgfydbbjkeiohgid.onion/></del> **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: <https://matrix.to/#/#anonymity:anonymousplanet.net>
|
|
||||||
- Matrix space: <https://matrix.to/#/#psa:anonymousplanet.net>
|
|
||||||
|
|
||||||
Have a good read and feel free to share and/or recommend it!
|
|
||||||
|
|||||||
@@ -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.*
|
||||||
+1
-2
@@ -19,7 +19,7 @@ schema:
|
|||||||
**9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6**
|
**9FA5 436D 0EE3 6098 5157 3825 17EC A05F 768D EDF6**
|
||||||
|
|
||||||
This is the master signing key fingerprint for Anonymous Planet.
|
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)
|
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.
|
Please share this project if you enjoy it and you think it might be useful to others.
|
||||||
|
|
||||||
{ align=right }
|
{ 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-house: Homepage](https://www.itsnothing.net)
|
||||||
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
|
- [:fontawesome-solid-envelope: E-mail](mailto:contact@anonymousplanet.org)
|
||||||
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@thehidden:tchncs.de "@thehidden:tchncs.de"), [:simple-matrix: Org Matrix](https://matrix.to/#/@nope:anonymousplanet.net "@nope:anonymousplanet.net")
|
- [:simple-matrix: Personal Matrix](https://matrix.to/#/@thehidden:tchncs.de "@thehidden:tchncs.de"), [:simple-matrix: Org Matrix](https://matrix.to/#/@nope:anonymousplanet.net "@nope:anonymousplanet.net")
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -125,7 +125,9 @@ nav:
|
|||||||
- Home: index.md
|
- Home: index.md
|
||||||
- About: about/index.md
|
- About: about/index.md
|
||||||
- Verify: verify/index.md
|
- Verify: verify/index.md
|
||||||
- Guide: guide/index.md
|
- Guide:
|
||||||
|
- guide/index.md
|
||||||
|
- Workflow Documentation: guide/dev-workflow.md
|
||||||
- Code: code/index.md
|
- Code: code/index.md
|
||||||
- Contribute: contribute/index.md
|
- Contribute: contribute/index.md
|
||||||
- Constitution: constitution/index.md
|
- Constitution: constitution/index.md
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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.
|
This script builds both light and dark mode MkDocs site, then renders docs/guide/ to single PDFs via Chromium.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python scripts/build_guide_pdf_extended.py # Generate light mode PDF only
|
python scripts/build_guide_pdf.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.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 --both # Generate both light and dark mode PDFs
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
python scripts/build_guide_pdf_extended.py --site-dir build/html --pdf-light export/thgtoa.pdf
|
python scripts/build_guide_pdf.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 --dark-mode --pdf-dark export/thgtoa-dark.pdf
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user