Verify RSA-SHA256 signatures on all PIA API responses
All checks were successful
Check Flake / check-flake (push) Successful in 3m18s

Every PIA API response includes a trailing RSA-SHA256 signature
(line 1 = JSON, lines 3+ = base64-encoded signature) which was
previously ignored entirely. Add verifyPIAResponse() that checks
each response against PIA's public signing key before trusting
the data. On verification failure the service aborts and systemd
restarts it.

Also bump RestartSec to 5m to avoid hammering PIA servers on
repeated failures.
This commit is contained in:
2026-02-26 22:06:35 -08:00
parent 1dd1b420d5
commit 9fdadd321c
3 changed files with 52 additions and 17 deletions

View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzLYHwX5Ug/oUObZ5eH5P
rEwmfj4E/YEfSKLgFSsyRGGsVmmjiXBmSbX2s3xbj/ofuvYtkMkP/VPFHy9E/8ox
Y+cRjPzydxz46LPY7jpEw1NHZjOyTeUero5e1nkLhiQqO/cMVYmUnuVcuFfZyZvc
8Apx5fBrIp2oWpF/G9tpUZfUUJaaHiXDtuYP8o8VhYtyjuUu3h7rkQFoMxvuoOFH
6nkc0VQmBsHvCfq4T9v8gyiBtQRy543leapTBMT34mxVIQ4ReGLPVit/6sNLoGLb
gSnGe9Bk/a5V/5vlqeemWF0hgoRtUxMtU1hFbe7e8tSq1j+mu0SHMyKHiHd+OsmU
IQIDAQAB
-----END PUBLIC KEY-----

View File

@@ -1,5 +1,6 @@
let let
caPath = ./ca.rsa.4096.crt; caPath = ./ca.rsa.4096.crt;
pubKeyPath = ./pubkey.pem;
in in
# Bash function library for PIA VPN WireGuard operations. # Bash function library for PIA VPN WireGuard operations.
@@ -21,14 +22,33 @@ in
fi fi
} }
# Verify a raw PIA API response (line 1 = JSON, lines 3+ = base64 RSA-SHA256 signature).
verifyPIAResponse() {
local raw=$1 label=$2
local sig_file
sig_file=$(mktemp)
echo "$raw" | tail -n +3 | base64 -d > "$sig_file"
if ! echo -n "$(echo "$raw" | head -n 1 | tr -d '\n')" | \
openssl dgst -sha256 -verify "${pubKeyPath}" \
-signature "$sig_file"; then
echo "ERROR: $label signature verification failed" >&2
rm -f "$sig_file"
return 1
fi
echo "$label signature verified"
rm -f "$sig_file"
}
fetchPIAToken() { fetchPIAToken() {
local PIA_USER PIA_PASS resp local PIA_USER PIA_PASS raw resp
echo "Reading PIA credentials..." echo "Reading PIA credentials..."
PIA_USER=$(sed '1q;d' /run/agenix/pia-login.conf) PIA_USER=$(sed '1q;d' /run/agenix/pia-login.conf)
PIA_PASS=$(sed '2q;d' /run/agenix/pia-login.conf) PIA_PASS=$(sed '2q;d' /run/agenix/pia-login.conf)
echo "Requesting PIA authentication token..." echo "Requesting PIA authentication token..."
resp=$(curl -s $(proxy_args) -u "$PIA_USER:$PIA_PASS" \ raw=$(curl -s $(proxy_args) -u "$PIA_USER:$PIA_PASS" \
"https://www.privateinternetaccess.com/gtoken/generateToken") "https://www.privateinternetaccess.com/gtoken/generateToken")
verifyPIAResponse "$raw" "generateToken"
resp=$(echo "$raw" | head -n 1)
PIA_TOKEN=$(echo "$resp" | jq -r '.token') PIA_TOKEN=$(echo "$resp" | jq -r '.token')
if [[ -z "$PIA_TOKEN" || "$PIA_TOKEN" == "null" ]]; then if [[ -z "$PIA_TOKEN" || "$PIA_TOKEN" == "null" ]]; then
echo "ERROR: Failed to fetch PIA token: $resp" >&2 echo "ERROR: Failed to fetch PIA token: $resp" >&2
@@ -39,20 +59,20 @@ in
choosePIAServer() { choosePIAServer() {
local serverLocation=$1 local serverLocation=$1
local servers servers_json totalservers serverindex local raw servers_json totalservers serverindex
servers=$(mktemp)
servers_json=$(mktemp) servers_json=$(mktemp)
echo "Fetching PIA server list..." echo "Fetching PIA server list..."
curl -s $(proxy_args) \ raw=$(curl -s $(proxy_args) \
"https://serverlist.piaservers.net/vpninfo/servers/v6" > "$servers" "https://serverlist.piaservers.net/vpninfo/servers/v6")
head -n 1 "$servers" | tr -d '\n' > "$servers_json" verifyPIAResponse "$raw" "serverList"
echo "$raw" | head -n 1 | tr -d '\n' > "$servers_json"
totalservers=$(jq -r \ totalservers=$(jq -r \
'.regions | .[] | select(.id=="'"$serverLocation"'") | .servers.wg | length' \ '.regions | .[] | select(.id=="'"$serverLocation"'") | .servers.wg | length' \
"$servers_json") "$servers_json")
if ! [[ "$totalservers" =~ ^[0-9]+$ ]] || [ "$totalservers" -eq 0 ] 2>/dev/null; then if ! [[ "$totalservers" =~ ^[0-9]+$ ]] || [ "$totalservers" -eq 0 ] 2>/dev/null; then
echo "ERROR: Location \"$serverLocation\" not found." >&2 echo "ERROR: Location \"$serverLocation\" not found." >&2
rm -f "$servers_json" "$servers" rm -f "$servers_json"
return 1 return 1
fi fi
echo "Found $totalservers WireGuard servers in region '$serverLocation'" echo "Found $totalservers WireGuard servers in region '$serverLocation'"
@@ -66,7 +86,7 @@ in
"$servers_json") "$servers_json")
WG_SERVER_PORT=$(jq -r '.groups.wg | .[0] | .ports | .[0]' "$servers_json") WG_SERVER_PORT=$(jq -r '.groups.wg | .[0] | .ports | .[0]' "$servers_json")
rm -f "$servers_json" "$servers" rm -f "$servers_json"
echo "Selected server $serverindex/$totalservers: $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)" echo "Selected server $serverindex/$totalservers: $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)"
} }
@@ -77,14 +97,16 @@ in
} }
authorizeKeyWithPIAServer() { authorizeKeyWithPIAServer() {
local addKeyResponse local raw addKeyResponse
echo "Sending addKey request to $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)..." echo "Sending addKey request to $WG_HOSTNAME ($WG_SERVER_IP:$WG_SERVER_PORT)..."
addKeyResponse=$(curl -s -G $(proxy_args) \ raw=$(curl -s -G $(proxy_args) \
--connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \ --connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \
--cacert "${caPath}" \ --cacert "${caPath}" \
--data-urlencode "pt=$PIA_TOKEN" \ --data-urlencode "pt=$PIA_TOKEN" \
--data-urlencode "pubkey=$PUBLIC_KEY" \ --data-urlencode "pubkey=$PUBLIC_KEY" \
"https://$WG_HOSTNAME:$WG_SERVER_PORT/addKey") "https://$WG_HOSTNAME:$WG_SERVER_PORT/addKey")
verifyPIAResponse "$raw" "addKey"
addKeyResponse=$(echo "$raw" | head -n 1)
local status local status
status=$(echo "$addKeyResponse" | jq -r '.status') status=$(echo "$addKeyResponse" | jq -r '.status')
if [[ "$status" != "OK" ]]; then if [[ "$status" != "OK" ]]; then
@@ -158,13 +180,15 @@ in
} }
reservePortForward() { reservePortForward() {
local payload_and_signature local raw payload_and_signature
echo "Requesting port forward signature from $WG_HOSTNAME..." echo "Requesting port forward signature from $WG_HOSTNAME..."
payload_and_signature=$(curl -s -m 5 \ raw=$(curl -s -m 5 \
--connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \ --connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \
--cacert "${caPath}" \ --cacert "${caPath}" \
-G --data-urlencode "token=$PIA_TOKEN" \ -G --data-urlencode "token=$PIA_TOKEN" \
"https://$WG_HOSTNAME:19999/getSignature") "https://$WG_HOSTNAME:19999/getSignature")
verifyPIAResponse "$raw" "getSignature"
payload_and_signature=$(echo "$raw" | head -n 1)
local status local status
status=$(echo "$payload_and_signature" | jq -r '.status') status=$(echo "$payload_and_signature" | jq -r '.status')
if [[ "$status" != "OK" ]]; then if [[ "$status" != "OK" ]]; then
@@ -195,14 +219,16 @@ in
} }
refreshPIAPort() { refreshPIAPort() {
local bindPortResponse local raw bindPortResponse
echo "Refreshing port forward binding with $WG_HOSTNAME..." echo "Refreshing port forward binding with $WG_HOSTNAME..."
bindPortResponse=$(curl -Gs -m 5 \ raw=$(curl -Gs -m 5 \
--connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \ --connect-to "$WG_HOSTNAME::$WG_SERVER_IP:" \
--cacert "${caPath}" \ --cacert "${caPath}" \
--data-urlencode "payload=$PORT_PAYLOAD" \ --data-urlencode "payload=$PORT_PAYLOAD" \
--data-urlencode "signature=$PORT_SIGNATURE" \ --data-urlencode "signature=$PORT_SIGNATURE" \
"https://$WG_HOSTNAME:19999/bindPort") "https://$WG_HOSTNAME:19999/bindPort")
verifyPIAResponse "$raw" "bindPort"
bindPortResponse=$(echo "$raw" | head -n 1)
echo "bindPort response: $bindPortResponse" echo "bindPort response: $bindPortResponse"
} }
''; '';

View File

@@ -73,7 +73,7 @@ in
config = { config, pkgs, lib, ... }: config = { config, pkgs, lib, ... }:
let let
scriptPkgs = with pkgs; [ wireguard-tools iproute2 curl jq iptables coreutils ]; scriptPkgs = with pkgs; [ wireguard-tools iproute2 curl jq iptables coreutils openssl ];
in in
{ {
imports = allModules; imports = allModules;
@@ -133,7 +133,7 @@ in
serviceConfig = { serviceConfig = {
Type = "simple"; Type = "simple";
Restart = "always"; Restart = "always";
RestartSec = "10s"; RestartSec = "5m";
RuntimeMaxSec = "30d"; RuntimeMaxSec = "30d";
}; };