#!/usr/bin/env bash set +x set -euo pipefail cd "$(dirname "$0")/.." umask 077 tmp=$(mktemp -d "${TMPDIR:-/tmp}/spindle-mill.XXXXXXXX") name="spindle-mill-smoke-${tmp##*.}" volume="$name-data" cleanup() { docker rm -f "$name" >/dev/null 2>&1 || true docker volume rm "$volume" >/dev/null 2>&1 || true rm -rf "$tmp" } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' TERM openssl ecparam -name prime256v1 -genkey -noout -out "$tmp/key.pem" openssl ec -in "$tmp/key.pem" -pubout -conv_form compressed -outform DER \ -out "$tmp/public.der" 2>/dev/null export SPINDLE_SIGNING_KEY_PEM SPINDLE_SIGNING_KEY_PEM=$(cat "$tmp/key.pem") docker run --detach --platform linux/amd64 --name "$name" \ --publish 127.0.0.1::6555 \ --mount "type=volume,source=$volume,target=/data,volume-nocopy=true" \ --tmpfs /run/spindle:rw,noexec,nosuid,size=64k,uid=10001,gid=10001,mode=0700 \ --env SPINDLE_SIGNING_KEY_PEM \ --env SPINDLE_ROLE=mill \ --env SPINDLE_SERVER_DEV=true \ --env SPINDLE_SERVER_HOSTNAME=localhost \ --env SPINDLE_SERVER_OWNER=did:plc:zld4i3c3iacwpxsuwcko5yuz \ --env SPINDLE_CONTACT_EMAIL=smoke@example.invalid \ --env SPINDLE_ARTIFACT_STORES_DISK_DIR=/data/artifacts \ --env SPINDLE_MILL_ARTIFACT_STORE=disk \ spindle-mill:v1.20.0 >/dev/null unset SPINDLE_SIGNING_KEY_PEM port=$(docker port "$name" 6555/tcp) url="http://$port" ready=false for ((attempt = 0; attempt < 60; attempt++)); do if [ "$(curl --silent --output "$tmp/health.json" --write-out '%{http_code}' \ --max-time 2 "$url/_health")" = 200 ]; then ready=true break fi if [ "$(docker inspect --format '{{.State.Running}}' "$name")" != true ]; then break fi sleep 1 done if [ "$ready" != true ]; then echo 'mill: /_health did not return HTTP 200' >&2 exit 1 fi echo 'GET /_health: 200' curl --fail --silent --show-error --max-time 5 \ "$url/.well-known/did.json" --output "$tmp/did.json" python3 - "$tmp/did.json" "$tmp/public.der" <<'PY' import json import pathlib import sys with open(sys.argv[1], encoding="utf-8") as f: doc = json.load(f) # P-256 public multicodec (0x1200 as a varint), then the compressed EC point. public = b"\x80\x24" + pathlib.Path(sys.argv[2]).read_bytes()[-33:] alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" value = int.from_bytes(public, "big") encoded = "" while value: value, digit = divmod(value, 58) encoded = alphabet[digit] + encoded expected = "z" + encoded valid = doc.get("id") == "did:web:localhost" and any( method.get("type") == "Multikey" and method.get("controller") == "did:web:localhost" and method.get("publicKeyMultibase") == expected for method in doc.get("verificationMethod", []) ) if not valid: sys.exit("mill: DID document does not contain did:web:localhost and the generated P-256 public key") print("GET /.well-known/did.json: did:web:localhost, matching P-256 public key") PY mode=$(docker exec "$name" stat -c '%a' /run/spindle/key.pem) if [ "$mode" != 600 ]; then echo "mill: signing key mode is $mode, expected 600" >&2 exit 1 fi echo 'Signing key permissions: 600' uid=$(docker exec "$name" awk '/^Uid:/ { print $2, $3, $4, $5 }' /proc/1/status) if [ "$uid" != '10001 10001 10001 10001' ]; then echo "mill: PID 1 UIDs are $uid, expected 10001 for all four IDs" >&2 exit 1 fi echo 'PID 1 UIDs: 10001 (real, effective, saved, filesystem)' docker stop --signal SIGTERM --timeout 30 "$name" >/dev/null status=$(docker inspect --format '{{.State.ExitCode}}' "$name") if [ "$status" != 0 ]; then echo "mill: SIGTERM exit code is $status, expected 0" >&2 exit 1 fi echo 'SIGTERM: clean exit (0)' docker logs "$name" > "$tmp/container.log" 2>&1 leaks=$(grep -c 'BEGIN EC PRIVATE KEY' "$tmp/container.log" || true) echo "Private-key markers in container output: $leaks" if [ "$leaks" != 0 ]; then exit 1 fi echo 'mill: smoke passed'