Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Optional convenience for the GhostNet signer: when a PDF is committed, re-sign
# it so its .asc never goes stale, and stage the signature in the same commit.
#
# Install (signer only): git config core.hooksPath hooks
#
# ponytail: convenience only, not enforcement. Git does not distribute hooks to
# people who clone, and this is bypassable with `git commit --no-verify`. The
# real mechanism is signing/sign-ghostnet.sh; this just runs it automatically.
set -euo pipefail

cd "$(git rev-parse --show-toplevel)"
mapfile -t pdfs < <(git diff --cached --name-only --diff-filter=ACM -- '*.pdf')
[ "${#pdfs[@]}" -gt 0 ] || exit 0

signing/sign-ghostnet.sh "${pdfs[@]}"
git add "${pdfs[@]/%/.asc}"
74 changes: 74 additions & 0 deletions signing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
GhostNet PDF Signing
====================

**Status: Proposed.** Opened in response to
[issue #13](https://github.com/s2underground/GhostNet/issues/13). Signing is not
active until S2 Underground publishes a signing key (see "For the signer").

Why
---
The concern in issue #13 is forged copies of the GhostNet manual circulating
with altered information, especially once a PDF leaves GitHub and travels by
radio, sneakernet, or reposts. A detached GPG signature lets anyone confirm a
PDF is the genuine S2 Underground release, wherever they got it.

The signature (`<file>.pdf.asc`) is committed next to each PDF and travels with
the file. This is deliberately not git commit/tag signing: that would only help
people who obtained the file through GitHub, who are the ones least at risk.

The trust model (read this)
---------------------------
A valid signature only proves the PDF matches **the key in this repository**.
Anyone can fork the repo, swap in their own key, and re-sign a forged PDF. The
signature is therefore only meaningful if you verify the key's **fingerprint
against a copy published out of band**, somewhere an attacker cannot quietly
change:

- read aloud / sent during a live net,
- printed inside the manual PDF itself,
- posted on QRZ and on more than one independent platform,
- anywhere the real S2 Underground identity is already established.

If the fingerprint the verify script prints does not match a source you trust,
the signature means nothing. Do not skip this step.

For anyone: verify a PDF
------------------------
Requires `gpg`.

```sh
signing/verify-ghostnet.sh # verify all signed PDFs
signing/verify-ghostnet.sh GhostNet_Version_1.5.pdf
```

It imports the bundled key into a throwaway keyring (your own keyring is never
touched), prints the fingerprint to cross-check, and reports `OK` or `FAIL` per
file. A non-zero exit means at least one file failed.

For the signer (S2 Underground)
-------------------------------
1. Publish your public key and its fingerprint out of band (see trust model).
2. Add the public key to this repo as `signing/GHOSTNET-signing-key.asc`:
```sh
gpg --armor --export <your-key-id> > signing/GHOSTNET-signing-key.asc
```
3. Sign the release PDFs and commit the `.asc` files:
```sh
signing/sign-ghostnet.sh # signs all tracked PDFs
git add *.pdf.asc signing/GHOSTNET-signing-key.asc
```
Pick a specific key with `-u <key-id>` or `GHOSTNET_SIGNING_KEY=<key-id>`.

Optional: sign automatically on commit:
```sh
git config core.hooksPath hooks
```
The `hooks/pre-commit` hook then re-signs any staged PDF and stages its
signature. This is a convenience for the signer only; git does not distribute
hooks to people who clone.

Checking the scripts
--------------------
`signing/selftest.sh` generates a throwaway key, signs a sample, and asserts a
good signature verifies while a tampered file is rejected. No network, no
changes to your keyring.
41 changes: 41 additions & 0 deletions signing/selftest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Self-test for sign-ghostnet.sh / verify-ghostnet.sh.
# Generates a throwaway key in a temp keyring (no network, your real keyring is
# untouched), signs a dummy file, and asserts: good signature verifies, a
# tampered file does NOT. Exercises both scripts end to end.
#
# Run: signing/selftest.sh
set -euo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
work="$(mktemp -d)"
export GNUPGHOME="$work/gnupg"
mkdir -p "$GNUPGHOME"; chmod 700 "$GNUPGHOME"
trap 'rm -rf "$work"' EXIT

echo "1. generating throwaway signing key"
gpg --homedir "$GNUPGHOME" --batch --quiet \
--quick-generate-key "GhostNet Selftest <selftest@example.invalid>" \
default default none
fpr="$(gpg --homedir "$GNUPGHOME" --batch --with-colons --fingerprint \
| awk -F: '/^fpr:/{print $10; exit}')"
gpg --homedir "$GNUPGHOME" --batch --armor --export "$fpr" > "$work/pub.asc"

cd "$work"
printf 'GhostNet selftest payload\n' > sample.pdf

echo "2. signing with sign-ghostnet.sh"
GHOSTNET_SIGNING_KEY="$fpr" "$here/sign-ghostnet.sh" sample.pdf
[ -f sample.pdf.asc ] || { echo "FAIL: no signature produced"; exit 1; }

echo "3. verifying a good signature (expect OK)"
GHOSTNET_KEY_FILE="$work/pub.asc" "$here/verify-ghostnet.sh" sample.pdf \
|| { echo "FAIL: good signature did not verify"; exit 1; }

echo "4. tampering the file (expect FAIL)"
printf 'forged content\n' > sample.pdf
if GHOSTNET_KEY_FILE="$work/pub.asc" "$here/verify-ghostnet.sh" sample.pdf; then
echo "FAIL: tampered file verified as good"; exit 1
fi

echo "PASS: good signature verifies, tampered file rejected"
37 changes: 37 additions & 0 deletions signing/sign-ghostnet.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Sign GhostNet release PDFs with a detached, armored GPG signature.
# Intended for the S2 Underground signer only. Produces "<file>.asc" next to
# each PDF, which travels with the file even when it leaves GitHub.
#
# Usage:
# signing/sign-ghostnet.sh [file ...]
# With no arguments, signs every *.pdf tracked in the repository root.
#
# Key selection (first match wins):
# -u / --local-user KEYID explicit key
# $GHOSTNET_SIGNING_KEY environment override
# gpg default key otherwise
set -euo pipefail

command -v gpg >/dev/null || { echo "error: gpg not found" >&2; exit 127; }

key="${GHOSTNET_SIGNING_KEY:-}"
case "${1:-}" in
-u|--local-user) key="$2"; shift 2 ;;
esac

# Default target list: tracked PDFs in the repo root.
if [ "$#" -gt 0 ]; then
pdfs=("$@")
else
mapfile -t pdfs < <(git ls-files -- '*.pdf' 2>/dev/null || true)
fi
[ "${#pdfs[@]}" -gt 0 ] || { echo "no PDFs to sign" >&2; exit 0; }

for pdf in "${pdfs[@]}"; do
[ -f "$pdf" ] || { echo "skip (missing): $pdf" >&2; continue; }
gpg --batch --yes --armor --detach-sign \
${key:+--local-user "$key"} \
--output "$pdf.asc" "$pdf"
echo "signed: $pdf -> $pdf.asc"
done
60 changes: 60 additions & 0 deletions signing/verify-ghostnet.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Verify GhostNet release PDFs against their detached GPG signatures.
# Anyone can run this. It imports the bundled GhostNet signing key into a
# throwaway keyring (your own keyring is never touched) and checks each PDF.
#
# IMPORTANT: a good signature only proves the file matches the key in this
# repo. Confirm the printed fingerprint against a copy published OUT OF BAND
# (read during a net, printed inside the PDF, posted on QRZ / multiple sites)
# before you trust it. See signing/README.md.
#
# Usage:
# signing/verify-ghostnet.sh [file ...]
# With no arguments, verifies every *.pdf that has a matching *.asc.
#
# Key file: signing/GHOSTNET-signing-key.asc (override with $GHOSTNET_KEY_FILE).
set -euo pipefail

command -v gpg >/dev/null || { echo "error: gpg not found" >&2; exit 127; }

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
keyfile="${GHOSTNET_KEY_FILE:-$here/GHOSTNET-signing-key.asc}"
if [ ! -f "$keyfile" ]; then
echo "error: signing key not found: $keyfile" >&2
echo "The S2 Underground public key has not been added yet. Once published," >&2
echo "save it as signing/GHOSTNET-signing-key.asc." >&2
exit 2
fi

# Throwaway keyring so we never touch the user's real GnuPG home.
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
chmod 700 "$tmp"
gpg --homedir "$tmp" --batch --quiet --import "$keyfile"
echo "Imported signing key. Cross-check this fingerprint out of band:"
gpg --homedir "$tmp" --batch --fingerprint --with-colons \
| awk -F: '/^fpr:/{print " " $10; exit}'
echo

# Target list: explicit args, or every PDF that has a signature.
if [ "$#" -gt 0 ]; then
pdfs=("$@")
else
pdfs=()
for sig in *.asc; do
[ -e "$sig" ] || continue
pdfs+=("${sig%.asc}")
done
fi
[ "${#pdfs[@]}" -gt 0 ] || { echo "no signed PDFs found" >&2; exit 0; }

failed=0
for pdf in "${pdfs[@]}"; do
if gpg --homedir "$tmp" --batch --verify "$pdf.asc" "$pdf" 2>/dev/null; then
echo "OK: $pdf"
else
echo "FAIL: $pdf"
failed=1
fi
done
exit "$failed"