From 99433d9f5cda1a04c76a86c40654585e6fcf5b04 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 23:32:41 +0000 Subject: [PATCH] test(marmot): pin the broker's certificate in the stream tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests 18 and 19 failed inside TLS before a single frame was written: CRYPTO_ERROR (TLS alert 42): certificate chain validation failed: PKIX path building failed: unable to find valid certification path The reference broker generates a self-signed certificate — its startup JSON says so, `"tls":"generated_self_signed"` — and there is no CA anywhere in this picture, so chaining it to the JDK trust store could never have worked. The binding anticipates exactly this: a client MAY pin the endpoint certificate by SHA-256 instead of chaining, which is what `--pin-sha256` and `PinnedCertificateValidator` are for. The broker prints the fingerprint the pin needs, in the same JSON line the harness already waits on; the harness just never read it. So `start_quic_broker` now captures `server_cert_sha256_fingerprint` and fails loudly if it is absent, and the three `amy marmot stream send|watch` calls pass it. `wn` reaches the same place with `--insecure-local`; pinning is the better half of that trade, since the peer still has to sign the TLS transcript with the pinned certificate's private key. 25 passed, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016kCuA6tc4JQzHPCDd39GHq --- cli/tests/marmot/marmot-interop-headless.sh | 2 ++ cli/tests/marmot/setup.sh | 13 ++++++++++++- cli/tests/marmot/tests-extras.sh | 8 +++++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cli/tests/marmot/marmot-interop-headless.sh b/cli/tests/marmot/marmot-interop-headless.sh index b754ec64b0..4dc78eb538 100755 --- a/cli/tests/marmot/marmot-interop-headless.sh +++ b/cli/tests/marmot/marmot-interop-headless.sh @@ -61,6 +61,8 @@ BROKER_HOST="${BROKER_HOST:-127.0.0.1}" BROKER_PORT="${BROKER_PORT:-4455}" BROKER_URI="quic://$BROKER_HOST:$BROKER_PORT" BROKER_PID="" +# SHA-256 of the broker's self-signed leaf, read from its startup JSON. +BROKER_PIN="" # A loopback Blossom blob store for the encrypted-media tests. Ciphertext only: # the file key comes from each group's MLS exporter and never reaches it. diff --git a/cli/tests/marmot/setup.sh b/cli/tests/marmot/setup.sh index 88aa4150f6..c5ebb35b36 100644 --- a/cli/tests/marmot/setup.sh +++ b/cli/tests/marmot/setup.sh @@ -143,7 +143,18 @@ start_quic_broker() { local deadline=$(( $(date +%s) + 15 )) while [[ $(date +%s) -lt $deadline ]]; do if grep -q '"local_addr"' "$STATE_DIR/broker/stdout.log" 2>/dev/null; then - info "broker pid $BROKER_PID ready" + # The broker generates a self-signed certificate and prints its + # fingerprint. `amy` pins that exact leaf rather than trusting a chain — + # there is no CA in this picture, and without the pin every stream test + # fails inside TLS before a single frame is written. + BROKER_PIN=$(sed -n 's/.*"server_cert_sha256_fingerprint":"\([0-9a-f]*\)".*/\1/p' \ + "$STATE_DIR/broker/stdout.log" | head -1) + if [[ -z "$BROKER_PIN" ]]; then + fail_msg "broker printed no server_cert_sha256_fingerprint — cannot pin it" + BROKER_PID="" + return 1 + fi + info "broker pid $BROKER_PID ready (cert ${BROKER_PIN:0:16}…)" return 0 fi if ! kill -0 "$BROKER_PID" 2>/dev/null; then break; fi diff --git a/cli/tests/marmot/tests-extras.sh b/cli/tests/marmot/tests-extras.sh index 9df6cb7a34..10dbcb04ee 100644 --- a/cli/tests/marmot/tests-extras.sh +++ b/cli/tests/marmot/tests-extras.sh @@ -456,7 +456,7 @@ test_18_agent_stream_amy_publishes() { local send_json thash chunks send_json=$(amy_json marmot stream send "$gid" --stream-id "$sid" --start-event-id "$seid" \ - --broker "$BROKER_URI" "Hello " "from " "amethyst") || { + --broker "$BROKER_URI" --pin-sha256 "$BROKER_PIN" "Hello " "from " "amethyst") || { record_result "$id" fail "amy stream send failed"; return } thash=$(printf '%s' "$send_json" | jq -r '.transcript_hash // empty') @@ -466,7 +466,8 @@ test_18_agent_stream_amy_publishes() { # Our own subscriber must recover the stream from the broker's replay window # and fold it to the same transcript the publisher computed. local watch_json - watch_json=$(amy_json marmot stream watch "$gid" --stream-id "$sid" --timeout 15) || { + watch_json=$(amy_json marmot stream watch "$gid" --stream-id "$sid" --timeout 15 \ + --pin-sha256 "$BROKER_PIN") || { record_result "$id" fail "amy stream watch failed"; return } printf 'stream18 watch=%s\n' "$watch_json" >>"$LOG_FILE" @@ -535,7 +536,8 @@ test_19_agent_stream_wn_publishes() { fi local watch_out="$STATE_DIR/stream-19-watch.json" - ( amy_a marmot stream watch "$gid" --stream-id "$wsid" --timeout 25 >"$watch_out" 2>>"$LOG_FILE" ) & + ( amy_a marmot stream watch "$gid" --stream-id "$wsid" --timeout 25 \ + --pin-sha256 "$BROKER_PIN" >"$watch_out" 2>>"$LOG_FILE" ) & local watch_pid=$! sleep 4