enrollment: the server mints the §2.1 bootstrap link, the app consumes it
server-release / image (push) Successful in 15s
server-test / test (push) Successful in 29s
server-release / release (push) Successful in 31s

POST /admin/enroll-tokens now returns the whole link, not just the token:

  echolot://enroll?v=1&u=<control URL>&p=pin-sha256:<b64>&t=<token>

The server is the only party that knows all three parts at once, and the part
an operator gets wrong by hand is the base64 pin — which does not fail loudly,
it just never matches, surfacing days later as an inscrutable TLS error. The
app takes the link from a paste or from an echolot:// deep link (QR scan), and
writes URL, pin and credential together or not at all.

One trap the tests pin: an unencoded "+" in a query string decodes to a space,
so a hand-assembled link arrives with a pin wrong by one character. Base64 has
no spaces, so they are restored — unambiguous, and it cannot damage a correctly
encoded pin.

Also fixes a spec divergence: §2.1 names the field device_credential and the
first implementation shipped "credential". Both are sent now and the client
prefers the spec's; the alias goes once nothing reads it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 12:06:22 +02:00
co-authored by Claude Fable 5
parent 8166611af1
commit ad85f3bfcd
15 changed files with 533 additions and 17 deletions
+17 -6
View File
@@ -19,13 +19,23 @@ UDP_PORT="${ECHOLOT_UDP_PORT:-8442}"
CTL_URL="https://${CTL_HOST}:${CTL_PORT}"
echo "· minting enrollment token on ${SSH_HOST} ..."
TOKEN=$(ssh -o BatchMode=yes "$SSH_HOST" \
'curl -s -X POST http://127.0.0.1:8444/admin/enroll-tokens' \
| python -c 'import json,sys;print(json.load(sys.stdin)["token"])')
MINTED=$(ssh -o BatchMode=yes "$SSH_HOST" \
'curl -s -X POST http://127.0.0.1:8444/admin/enroll-tokens')
TOKEN=$(printf '%s' "$MINTED" | python -c 'import json,sys;print(json.load(sys.stdin)["token"])')
# The server also returns the whole §2.1 bootstrap link. LiveEnrollmentTest redeems that link,
# which is what proves the Go side and the Kotlin side agree on its encoding — a disagreement
# there yields a pin wrong by one character, which fails much later and looks like anything but.
ENROLL_URI=$(printf '%s' "$MINTED" \
| python -c 'import json,sys;print(json.load(sys.stdin).get("enroll_uri",""))')
echo "· enrolling over ${CTL_URL} ..."
CRED=$(curl -sk -X POST "${CTL_URL}/v1/enroll" -H "Authorization: Bearer ${TOKEN}" \
| python -c 'import json,sys;print(json.load(sys.stdin)["credential"])')
# A second token, because the one above is single-use and may be spent by LiveEnrollmentTest.
TOKEN2=$(ssh -o BatchMode=yes "$SSH_HOST" \
'curl -s -X POST http://127.0.0.1:8444/admin/enroll-tokens' \
| python -c 'import json,sys;print(json.load(sys.stdin)["token"])')
CRED=$(curl -sk -X POST "${CTL_URL}/v1/enroll" -H "Authorization: Bearer ${TOKEN2}" \
-H "X-Echolot-App-Version: 0.2.0" \
| python -c 'import json,sys;d=json.load(sys.stdin);print(d.get("device_credential") or d["credential"])')
echo "· computing SPKI pin from served cert ..."
PIN=$(echo | openssl s_client -connect "${CTL_HOST}:${CTL_PORT}" 2>/dev/null \
@@ -43,5 +53,6 @@ ECHOLOT_LIVE_PIN="$PIN" \
ECHOLOT_LIVE_CRED="$CRED" \
ECHOLOT_LIVE_UDP="${CTL_HOST}:${UDP_PORT}" \
ECHOLOT_LIVE_TARGET="${ECHOLOT_LIVE_TARGET:-fmr}" \
ECHOLOT_ENROLL_URI="$ENROLL_URI" \
./gradlew "$TASK" --tests "$FILTER" --info --rerun-tasks --console=plain \
2>&1 | grep -E "profile:|capabilities:|session:|echo |primed|mtu probe|downtrain|big_send|largest|observations bytes|Live[A-Za-z]*Test|BUILD|FAIL|PASS|^e:" || true
2>&1 | grep -E "profile:|capabilities:|session:|echo |primed|mtu probe|downtrain|big_send|largest|observations bytes|link:|parsed:|enrolled:|refused|Live[A-Za-z]*Test|BUILD|FAIL|PASS|^e:" || true