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:
@@ -134,4 +134,4 @@ jobs:
|
||||
export/thgtoa-dark.pdf.sig
|
||||
sha256sum-light.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -75,4 +75,3 @@ jobs:
|
||||
|
||||
# Update the release with VT results
|
||||
gh release edit ${{ github.ref_name }} --notes-file vt-results.md
|
||||
|
||||
|
||||
+6
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:**
|
||||
- <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.*
|
||||
+2
-3
@@ -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.
|
||||
|
||||
{ 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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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