#!/bin/sh # Generates the self-signed dev CA and leaf certificate `server/` serves its # TLS listener with. Run it once, ON THE MACHINE THAT RUNS THE BACKEND, before # the first start; the server exits with a clear message if the certificates # are missing. # # Same scheme as ../local-updater's: nothing on a device trusts this # automatically -- the app embeds the CA certificate verbatim and pins to it # (`PinnedCert.kt`), rather than relying on the device's system trust store. # This server's API *is* remote code execution (it spawns AI sessions on # request), so a MITM on it would be as bad as it gets -- hence pinning. # # WHERE THE KEYS LIVE, AND WHY NOT IN THE REPO # # Output goes to $XDG_CONFIG_HOME/ai-app/certs (0700), deliberately *not* # beside this script. The repo is a virtiofs mount shared with the dev VM, # and that VM is treated as untrusted -- a machine that isn't malicious but # could become so. A CA private key it can read is a CA private key it can # sign with, and a leaf signed by this CA is one the phone's pinned app # accepts without question. Keeping the key off the shared mount is what # makes pinning mean anything. # # The same reasoning says the CA key doesn't belong on the backend either, # strictly: the server only ever reads leaf.pem and leaf-key.pem, and the CA # key is needed solely to reissue a leaf. Moving ca-key.pem somewhere offline # once the setup is stable costs nothing but having it to hand at reissue. # # Running this inside the VM is fine and expected for emulator work -- it # just produces a *different*, throwaway CA there. Never install a build # pinning that dev CA on the real phone. # # The CA is idempotent -- skipped if `certs/ca.pem` already exists, so # re-running this doesn't invalidate the certificate the installed app has # pinned against without a reason to. The leaf is cheap and reissued on # every run (still signed by that same, unchanged CA), so adding another SAN # entry only means rerunning this script, not touching anything pinned. # # The leaf's SANs must cover every address a device reaches this server at. # In production that is exactly one: the backend's WireGuard address, which # the phone uses from everywhere (see PLAN.md's off-network section). # Override with SERVER_IP=... if your wg0 address differs. # # Outputs into `certs/` (gitignored -- private key material, and the whole # thing is trivially regeneratable anyway): # ca.pem the CA certificate (not its private key) -- what the # app embeds and pins against. # ca-key.pem the CA's private key -- only this script needs it, to # sign the leaf below. Never shipped anywhere. # leaf.pem the server's own certificate (CA-signed), presented on # every TLS handshake. # leaf-key.pem the leaf's private key -- what the server loads to # terminate TLS. # ca-sha256.txt the CA certificate's SPKI SHA-256 fingerprint, printed # below too. Informational -- the app embeds the whole # `ca.pem`, not this digest. # leaf-sha256.txt the leaf's SPKI SHA-256 fingerprint. Informational -- # nothing pins the leaf; the app pins the CA and # validates the chain. set -eu SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) CERTS_DIR="${AI_APP_CERTS:-${XDG_CONFIG_HOME:-$HOME/.config}/ai-app/certs}" # The backend's WireGuard address -- the one address the phone ever dials in # production (PLAN.md: single-path addressing, no home/away distinction). SERVER_IP="${SERVER_IP:-10.66.0.1}" # Also covered so development before the tunnel exists can complete a real # handshake against the same pinned CA: # 127.0.0.1 curl from the machine itself, and tests binding loopback # 10.0.2.2 the Android emulator's alias for the host's loopback # LAN_IP a real phone on the same LAN, pre-WireGuard LOOPBACK_IP="127.0.0.1" EMULATOR_HOST_IP="10.0.2.2" LAN_IP="${LAN_IP:-192.168.1.168}" # Private key material: owner-only from the moment it exists, rather than # created world-readable and chmod'ed a beat later. umask 077 mkdir -p "$CERTS_DIR" chmod 700 "$CERTS_DIR" cd "$CERTS_DIR" echo "==> Writing certificates to $CERTS_DIR" if [ -f ca.pem ]; then echo "==> ca.pem already exists, reusing existing CA." else echo "==> Generating CA key + self-signed CA certificate" openssl ecparam -name prime256v1 -genkey -noout -out ca-key.pem # Explicit keyUsage: strict verifiers (e.g. Python 3.14's ssl) reject a # CA without it, and Android could follow -- cheap to be proper now, # expensive to regenerate after phones have pinned it. openssl req -new -x509 -key ca-key.pem -out ca.pem -days 3650 \ -subj "/O=ai-app dev/CN=ai-app dev CA" \ -addext "basicConstraints=critical,CA:TRUE" \ -addext "keyUsage=critical,keyCertSign,cRLSign" fi echo "==> Generating leaf key + CSR for $SERVER_IP (+ dev addresses)" openssl ecparam -name prime256v1 -genkey -noout -out leaf-key.pem openssl req -new -key leaf-key.pem -out leaf.csr \ -subj "/O=ai-app dev/CN=$SERVER_IP" echo "==> Signing leaf certificate with the dev CA" cat > leaf.ext < Computing certificate fingerprints (SPKI SHA-256)" CA_SHA256=$(openssl x509 -in ca.pem -pubkey -noout \ | openssl pkey -pubin -outform der \ | openssl dgst -sha256 -binary \ | openssl base64) echo "$CA_SHA256" > ca-sha256.txt LEAF_SHA256=$(openssl x509 -in leaf.pem -pubkey -noout \ | openssl pkey -pubin -outform der \ | openssl dgst -sha256 -binary \ | openssl base64) echo "$LEAF_SHA256" > leaf-sha256.txt echo echo "==> Done." echo " CA fingerprint (base64): $CA_SHA256" echo " Leaf fingerprint (base64): $LEAF_SHA256" echo echo " Only relevant if the CA was regenerated just now (i.e. ca.pem did" echo " not already exist): the app embeds PINNED_CA_PEM and needs the new" echo " $CERTS_DIR/ca.pem contents pasted in, or it silently" echo " stops being able to reach this server. The app installs via Local" echo " Updater, so recovery is a reinstall through that -- but it's still" echo " a one-way door for the installed copy." echo echo " app/androidApp/src/main/kotlin/com/example/aiapp/PinnedCert.kt" echo echo " Only ca.pem is meant to leave this machine. Copy it by hand; do not" echo " put the certs directory back in the repo, which is shared with the" echo " VM (see this script's header)."