#!/bin/bash # Mastodon install hooks. # Web Push identity. # --------------------------------------------------------------------------- # The two VAPID values are not independent secrets — they are the two halves of # one P-256 keypair, and the browser verifies that the push request is signed by # the private key matching the public key it subscribed with. The framework's # RANDOMIZED* generators mint each placeholder on its own, so they can produce # two well-formed strings but never a *pair*: what shipped before was two # unrelated random values, and web push could not work with them. Stoat hits the # same wall and solves it the same way, in its own install hook. # # Mastodon's webpush gem expects unpadded URL-safe base64 of the raw key # material: the 32-byte private scalar (43 chars) and the 65-byte uncompressed # public point (87 chars). Both are sliced out of the SEC1 DER, whose layout for # prime256v1 is a fixed 121 bytes — a 2-byte SEQUENCE header, INTEGER 1, then # `04 20` introducing the private scalar at offset 7, and the public point as # the trailing field. Both slices were cross-checked against `openssl ec -text`. # # Runs at install_post_setup: the deployed config exists by then (so there is # something to write to) and the compose has not been templated yet (so the pair # reaches the compose on this same install). # # Generated once, then left alone. Rotating the pair invalidates every push # subscription clients are holding, so an existing well-formed pair is never # replaced — including across reinstalls. mastodon_install_post_setup() { local app_name="${1:-mastodon}" local config_file="$(appDir "$app_name")/${app_name}.config" [[ -f "$config_file" ]] || return 0 # Keep a pair that already has the right shape. The length test is also what # retires the old values: two independent RANDOMIZEDVAPID strings are the # wrong length for the public half, so an install carrying them regenerates # once and is correct from then on. if [[ "$CFG_MASTODON_VAPID_PRIVATE_KEY_1" =~ ^[A-Za-z0-9_-]{43}$ \ && "$CFG_MASTODON_VAPID_PUBLIC_KEY_1" =~ ^[A-Za-z0-9_-]{87}$ ]]; then return 0 fi local pem der pem=$(mktemp) || return 0 der=$(mktemp) || { rm -f "$pem"; return 0; } if ! openssl ecparam -name prime256v1 -genkey -noout -out "$pem" 2>/dev/null \ || ! openssl ec -in "$pem" -outform DER -out "$der" 2>/dev/null; then rm -f "$pem" "$der" isError "Could not generate a VAPID keypair for $app_name — web push will not work." return 0 fi local vapid_private vapid_public vapid_private=$(head -c 39 "$der" | tail -c 32 | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=') vapid_public=$(tail -c 65 "$der" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=') rm -f "$pem" "$der" # Refuse to write a malformed pair over a working one — a short read or an # openssl build with a different DER layout would otherwise quietly break # push instead of leaving it as it was. if [[ ! "$vapid_private" =~ ^[A-Za-z0-9_-]{43}$ || ! "$vapid_public" =~ ^[A-Za-z0-9_-]{87}$ ]]; then isError "Generated VAPID keypair had an unexpected shape — leaving $app_name's push keys alone." return 0 fi updateConfigOption "CFG_MASTODON_VAPID_PRIVATE_KEY_1" "$vapid_private" "$config_file" updateConfigOption "CFG_MASTODON_VAPID_PUBLIC_KEY_1" "$vapid_public" "$config_file" isSuccessful "Generated a VAPID keypair for $app_name web push." }