Files
ai-app/gen-dev-cert.sh
T
irisandClaude Fable 5 95d389e2b8 Phase 2 core: ClaudeDriver over stream-json, permissions and questions on the phone
The second driver behind the same trait: claude -p with stream-json both
ways, the hidden --permission-prompt-tool stdio flag (without which no
permission ever reaches a client), text deltas streamed from raw API
events, tool_use/tool_result mapped to tool events, and can_use_tool
control requests surfaced as Question events -- plain permissions as
Allow/Deny, AskUserQuestion as one Question per sub-question with the
chosen labels sent back in updatedInput.answers keyed by question text
(wire shapes pinned by live probes against CLI 2.1.237, recorded in the
module doc). The CLI session id is persisted per session dir, so a
backend restart respawns with --resume and loses nothing. set_model
rides the control protocol and persists through the manager; the spawn
screen grows model/cwd/permission-mode fields.

Also: the dev CA now carries proper keyUsage/basicConstraints
extensions (strict verifiers reject it otherwise) -- regenerated and
re-pinned before any real phone has installed the app.

Verified: 20 unit tests + clippy clean; scripted end-to-end over the
HTTP API (AskUserQuestion round trip, Bash permission allow, streaming,
restart with --resume remembering earlier work, delete); and on the
emulator, a live haiku session asking Tea-or-coffee and acknowledging
the tapped answer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xn8nHw1tw1R6PtiY1eEtw
2026-08-24 21:31:50 -04:00

114 lines
5.2 KiB
Bash
Executable File

#!/bin/sh
# Generates the self-signed dev CA and leaf certificate `server/` serves its
# TLS listener with. Run once before the first `cargo run`; the server exits
# with a clear message if `certs/` is 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.
#
# 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="$SCRIPT_DIR/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}"
mkdir -p "$CERTS_DIR"
cd "$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 <<EOF
subjectAltName = IP:$SERVER_IP,IP:$LOOPBACK_IP,IP:$EMULATOR_HOST_IP,IP:$LAN_IP
basicConstraints = CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = serverAuth
EOF
openssl x509 -req -in leaf.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out leaf.pem -days 3650 -extfile leaf.ext
rm -f leaf.csr leaf.ext ca.srl
echo "==> 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. certs/ca.pem"
echo " did not already exist): the app embeds PINNED_CA_PEM and needs the"
echo " new certs/ca.pem contents pasted in, or it silently stops being able"
echo " to reach this server. The app installs via Local Updater, so"
echo " recovery is a reinstall through that -- but it's still a one-way"
echo " door for the installed copy."
echo
echo " app/androidApp/src/main/kotlin/com/example/aiapp/PinnedCert.kt"