#!/bin/bash # HKP keyserver CGI # Implements: GET /pks/lookup?op=get|index|vindex and POST /pks/add # Keys live in /var/pgp/keys/.asc KEYS_DIR=/var/pgp/keys FAVICON="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDAiIGhlaWdodD0iMTQwIiBzdHJva2U9IiMwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iMTIyIiBzdHJva2UtZGFzaGFycmF5PSIyLDM4IiBkPSJtOSw3MGgxMjJNNzAsOXYxMjIiLz48cGF0aCBzdHJva2Utd2lkdGg9IjMzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIGQ9Im03MCwzMGgwbTQwLDQwaDBtLTgwLDQwdjBtNDAsMGgwbTQwLDBoMCIvPjwvc3ZnPg==" CSS='@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Regular.woff2); font-weight: normal; } @font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Bold.woff2); font-weight: bold; } * { unicode-bidi: plaintext; box-sizing: border-box; } html { color: black; background-color: white; } body { font-family: "Kawkab Mono"; font-size: 16px; line-height: 1.4; margin: 0; padding: 4rem 0; min-height: 100%; overflow-wrap: break-word; } main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; } h1, header, footer { text-align: center; } main { text-align: justify; } p, h2, h3, h4 { margin: 1em 0 0 0; } table { margin: 1em auto; border-collapse: collapse; } th, td { border: 1px solid; padding: 0.3em 0.8em; } code { font-size: 85%; } hr { border: none; border-top: thin solid; margin: 1.25rem 0; } header { margin-bottom: 1em; } footer { margin-top: 3em; } @media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } } @media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } } @media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }' FOOTER='' qs_get() { echo "$QUERY_STRING" \ | tr '&' '\n' \ | grep -E "^$1=" \ | head -1 \ | cut -d= -f2- \ | sed 's/+/ /g; s/%20/ /g' } op=$(qs_get op) search=$(qs_get search | sed 's/^0x//i; s/^0X//i') case "$REQUEST_METHOD" in GET) case "$op" in get) printf "Content-Type: text/plain\r\n\r\n" FP="${search^^}" keyfile="$KEYS_DIR/$FP.asc" if [[ -f "$keyfile" ]]; then cat "$keyfile" else printf "Error: No key found for %s\n" "$search" fi ;; index) printf "Content-Type: text/plain\r\n\r\n" total=$(find "$KEYS_DIR" -name '*.asc' 2>/dev/null | wc -l) echo "info:1:$total" for f in "$KEYS_DIR"/*.asc; do [[ -f "$f" ]] || continue gpg --with-colons --import-options show-only \ --import < "$f" 2>/dev/null \ | awk -F: '/^pub/{print "pub:" $5 ":" $6 ":::" $10 "::"}' done ;; vindex) printf "Content-Type: text/html\r\n\r\n" printf '\n\n\n' printf '\n' printf '\n' "$FAVICON" printf '\n' printf 'pgp.gumx.cc / keys\n' printf '\n' "$CSS" printf '\n\n' printf '
\n

gumx / pgp / keys

\n
\n' printf '
\n\n\n' found=0 for f in "$KEYS_DIR"/*.asc; do [[ -f "$f" ]] || continue found=1 FP=$(basename "$f" .asc) KEY_UID=$(gpg --with-colons --import-options show-only --import < "$f" 2>/dev/null \ | awk -F: '/^uid/{print $10; exit}') printf '\n' \ "$FP" "$FP" "$KEY_UID" done [[ $found -eq 0 ]] && printf '\n' printf '
fingerprintuid
%s%s
no keys
\n
\n' printf '%s\n' "$FOOTER" printf '\n\n' ;; *) printf "Content-Type: text/plain\r\n\r\n" echo "Error: unknown op" ;; esac ;; POST) printf "Content-Type: text/plain\r\n\r\n" read -r -d '' -n "${CONTENT_LENGTH:-4096}" SUBMITTED_KEY TMPDIR_GPG=$(mktemp -d) FINGERPRINT=$(echo "$SUBMITTED_KEY" \ | gpg --homedir "$TMPDIR_GPG" \ --with-colons --import-options show-only \ --import 2>/dev/null \ | awk -F: '/^fpr/{print $10; exit}') rm -rf "$TMPDIR_GPG" if [[ -z "$FINGERPRINT" ]]; then echo "Error: Could not parse submitted key" exit 0 fi KEYFILE="$KEYS_DIR/${FINGERPRINT^^}.asc" if [[ -f "$KEYFILE" ]]; then echo "$SUBMITTED_KEY" > "$KEYFILE" echo "Key updated successfully" else echo "Error: Fingerprint not registered." echo "Contact ahmed@gumx.cc to arrange a key signing meeting." fi ;; esac