LibrePortal/containers/mastodon/scripts/mastodon_install_hooks.sh
librelad 4685320353 feat(secrets): real VAPID keypair for mastodon, slot-numbered DB passwords
VAPID: the two values are the halves of one P-256 keypair, not independent
secrets — the browser verifies that a push is signed by the private key matching
the public key it subscribed with. The RANDOMIZED* generators mint each
placeholder on its own, so they produced two unrelated strings and web push could
never have worked. Generate the pair in mastodon_install_post_setup the way stoat
already does, encoded as Mastodon's webpush gem expects: unpadded URL-safe base64
of the 32-byte private scalar and the 65-byte uncompressed public point, sliced
out of the SEC1 DER. Verified by rebuilding the key from the emitted private half
and re-deriving the public point — openssl accepts it and the point matches.

Generated once and never rotated (rotation would invalidate every subscription),
but a pair of the wrong shape is replaced, so an install carrying the old
unrelated strings heals itself on next install — their public half is 42 chars
where a real point is 87.

Slots: CFG_<APP>_DB_PASSWORD -> CFG_<APP>_DB_PASSWORD_1 and likewise for
DB_ROOT_PASSWORD, across mastodon, owncloud, mattermost, matrix, nextcloud and
bookstack, so a database credential is always a numbered slot and a second one is
just _2. Renaming a key means reconciliation drops the old and adds the new
holding its placeholder, so an existing install regenerates unless the value is
carried over first — documented, including that the old file survives as
.<app>.config.bak.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:42:50 +01:00

72 lines
3.4 KiB
Bash

#!/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="${containers_dir}${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" =~ ^[A-Za-z0-9_-]{43}$ \
&& "$CFG_MASTODON_VAPID_PUBLIC_KEY" =~ ^[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" "$vapid_private" "$config_file"
updateConfigOption "CFG_MASTODON_VAPID_PUBLIC_KEY" "$vapid_public" "$config_file"
isSuccessful "Generated a VAPID keypair for $app_name web push."
}