Captured during the attempt to measure ECDSA-P384 handshake wall-clock per the
"ECDSA P-384 verify wall-clock" subsection of CLAUDE.md. The dispatcher and
overlay swap (Phase 5) work — the regression is upstream of where the
P-384 verify dispatcher fires.
Symptom
After a freshly-bootstrapped UCI build (make BACKEND=uci USE_OVERLAY_P384_EMBED=0
then plain make BACKEND=uci), running tools/uci/test_https_local_p384.py
against a U64E at 48 MHz turbo:
- C64 sends ClientHello, receives + parses ServerHello (key share),
derives handshake keys (HK1 + KEYS screen markers print) - Enters
tls_recv_encrypted for EncryptedExtensions (ENC1 + RX markers print) - Never prints
GOT2 — meaning tls_record_recv_and_decrypt never
returns C=0 once HS keys are derived - After
enc_timeout exhausts (16-bit retry counter, ≈565 s wall-clock at
48 MHz given the TOD-bounded 5 s waits), tls_recv_encrypted returns C=1 tls_connect goes to @error, sets tls_state=0xFF, tls_last_state=0x03
(ENCRYPTED_EXT), net_last_error=0x86 (UCI_ERR_READ_FAIL)
Reproduces deterministically across 2 retries with DEBUG_CAPTURE=0 (which
rules out the orthogonal c64-test-harness FPGA-wedge issue #129 in
that repo).
P-256 baseline (tools/uci/test_https_local.py) passes cleanly in 82.7 s
on the same hardware ~5 min apart from the P-384 failures, so this is not a
device or environment issue.
Concrete evidence
Reference passing run (P-256, artifact /tmp/uci_https_debug/20260519_210656/):
- Screen:
... HK1 KEYS ENC1 RX GOT2 GOT DEC PROC CERT ENC1 RX GOT2 GOT DEC PROC CV ... FIN CFIN http_status=200, http_resp_buf="HELLO FROM TLS SERVER"tls_recv_sub_progress=0x0A (success path in tls_record_recv_and_decrypt)- Wall-clock 82.7 s
Failing run (P-384, /tmp/p384_v4.log):
- Screen:
... HK1 KEYS ENC1 RX — stops at RX, NO GOT2 net_last_error=0x86, tls_state=0xFF, tls_last_state=0x03,
tls_recv_progress=0x05, tls_recv_sub_progress=0x02tcp_recv_head = tcp_recv_tail = $032F (815 bytes consumed and drained from ring)- The ring's stored bytes show ServerHello (95 B) + ChangeCipherSpec (6 B) +
first encrypted record (5 + 50 B body) + second encrypted record
(5 + 463 B body, plausible Certificate size for P-384 cert) + ~191 B of next.
The records were physically pulled from the ring, but decrypt never
succeeded.
Where the regression lives
CLAUDE.md says P-384 reached read_seq=4 on 2026-05-16 (artifact
/tmp/uci_https_debug/20260516_152824/). Five commits since:
4c6d4a3 test(uci): harness lock/health pattern — TEST onlye52216e refactor(tools/uci): P-384 arbiter helper — TEST only3c60550 fix(uci): bounded uci_wait_not_busy — code (memory says cleared
by bisect, but the bisect criterion was likely "dispatcher reaches
ecdsa_verify_384" which is downstream of THIS failure)8f28941 fix(uci): bound uci_drain_resp + uci_drain_status — code (same
bisect caveat as above)28e042e build(make): cc65 -g/--dbgfile — build only0ae3ee5 test(uci): bump SENTINEL_POLL_TIMEOUT — TEST onlyf9f746d docs(CLAUDE.md) — docs only
Worth re-bisecting with the new criterion: "does GOT2 appear during EE
recv?"
ClientHello sig_algs verified
src/tls_handshake.s:599-604 correctly advertises both:
sig_algs_ext_data:
.byte $00, $0d
.byte $00, $06
.byte $00, $04
.byte $04, $03 ; ecdsa_secp256r1_sha256
.byte $05, $03 ; ecdsa_secp384r1_sha384
So the server's choice of 0x0503 is the C64-side intent. ServerHello on
the failing run is 86 B body — same shape as the P-256 case. The failure is
clearly downstream of negotiation.
What an agent researching this could look at
- HKDF / key-schedule output divergence. Capture
tls_hs_read_key,
tls_hs_read_iv, and the 50 B ciphertext + 16 B AEAD tag of the first
encrypted record. Run an off-C64 reference AEAD decrypt against them with
the same nonce-construction (IV XOR seq=0). If the reference also fails,
the C64's keys are wrong. If reference succeeds, the C64's aead_decrypt
path is the bug. - Re-bisect against the new pass criterion. The 5-commit window above
has two code commits (3c60550, 8f28941) whose bcs-on-bounded-drain
callers in net.s might have introduced a subtle SOCKET_READ post-error
path that drops or corrupts buffered bytes. Specifically: does the new
bcs @pe_drain_to skip-the-ack-on-timeout shape leave the firmware in a
state where the NEXT SOCKET_READ returns READ_FAIL spuriously? - Inspect the ring-consumption order vs the @Retry CCS skip.
tls_record_recv_and_decrypt consumes a CCS record (6 B) via @Retry,
then reads the next record's header. With the new TOD-bounded drain
semantics, is there a scenario where uci_drain_status sets C=1 spuriously
and the caller forces tcp_state=ERROR mid-flight, even though there was
no actual firmware error? - Compare AEAD nonce construction across P-256 PASS vs P-384 FAIL —
should be IV XOR (read_seq=0) in both cases on the first record.
Repro
After git checkout master (HEAD f9f746d), bootstrap build:
cd /Users/someone/Documents/c64-https
make clean
make BACKEND=uci USE_OVERLAY_P384_EMBED=0 # produce labels.txt
make BACKEND=uci # embed overlays
Then on a host with U64E LAN access:
U64_HOST=10.43.23.81 C64_SKIP_BUILD=1 TURBO_MHZ=48 \
DEBUG_CAPTURE=0 \
/Users/someone/Documents/c64-ChaCha20-Poly1305/.venv/bin/python3 \
tools/uci/test_https_local_p384.py
DEBUG_CAPTURE=0 is intentional — with capture on, the U64E FPGA hits a
sustained-workload wedge that produces a noisy second failure mode
(Ultimate64TimeoutError on REST read_memory) that hides the underlying
TLS issue. That harness-side wedge is filed separately as
c64-test-harness#129.
Cross-refs
- Memory
p384-session-handoff-2026-05-20 (this session's full notes) - Memory
p384-session-handoff-2026-05-17 (prior session, dispatcher OK) - CLAUDE.md "ECDSA P-384 verify wall-clock" — still says "Not yet measured
end-to-end"; this issue blocks closing that out
Captured during the attempt to measure ECDSA-P384 handshake wall-clock per the
"ECDSA P-384 verify wall-clock" subsection of CLAUDE.md. The dispatcher and
overlay swap (Phase 5) work — the regression is upstream of where the
P-384 verify dispatcher fires.
Symptom
After a freshly-bootstrapped UCI build (
make BACKEND=uci USE_OVERLAY_P384_EMBED=0then plain
make BACKEND=uci), runningtools/uci/test_https_local_p384.pyagainst a U64E at 48 MHz turbo:
derives handshake keys (HK1 + KEYS screen markers print)
tls_recv_encryptedfor EncryptedExtensions (ENC1 + RX markers print)GOT2— meaningtls_record_recv_and_decryptneverreturns C=0 once HS keys are derived
enc_timeoutexhausts (16-bit retry counter, ≈565 s wall-clock at48 MHz given the TOD-bounded 5 s waits),
tls_recv_encryptedreturns C=1tls_connectgoes to@error, setstls_state=0xFF,tls_last_state=0x03(ENCRYPTED_EXT),
net_last_error=0x86(UCI_ERR_READ_FAIL)Reproduces deterministically across 2 retries with DEBUG_CAPTURE=0 (which
rules out the orthogonal c64-test-harness FPGA-wedge issue #129 in
that repo).
P-256 baseline (
tools/uci/test_https_local.py) passes cleanly in 82.7 son the same hardware ~5 min apart from the P-384 failures, so this is not a
device or environment issue.
Concrete evidence
Reference passing run (P-256, artifact
/tmp/uci_https_debug/20260519_210656/):... HK1 KEYS ENC1 RX GOT2 GOT DEC PROC CERT ENC1 RX GOT2 GOT DEC PROC CV ... FIN CFINhttp_status=200,http_resp_buf="HELLO FROM TLS SERVER"tls_recv_sub_progress=0x0A(success path intls_record_recv_and_decrypt)Failing run (P-384,
/tmp/p384_v4.log):... HK1 KEYS ENC1 RX— stops at RX, NO GOT2net_last_error=0x86,tls_state=0xFF,tls_last_state=0x03,tls_recv_progress=0x05,tls_recv_sub_progress=0x02tcp_recv_head = tcp_recv_tail = $032F(815 bytes consumed and drained from ring)first encrypted record (5 + 50 B body) + second encrypted record
(5 + 463 B body, plausible Certificate size for P-384 cert) + ~191 B of next.
The records were physically pulled from the ring, but decrypt never
succeeded.
Where the regression lives
CLAUDE.md says P-384 reached
read_seq=4on 2026-05-16 (artifact/tmp/uci_https_debug/20260516_152824/). Five commits since:4c6d4a3test(uci): harness lock/health pattern — TEST onlye52216erefactor(tools/uci): P-384 arbiter helper — TEST only3c60550fix(uci): bounded uci_wait_not_busy — code (memory says clearedby bisect, but the bisect criterion was likely "dispatcher reaches
ecdsa_verify_384" which is downstream of THIS failure)
8f28941fix(uci): bound uci_drain_resp + uci_drain_status — code (samebisect caveat as above)
28e042ebuild(make): cc65 -g/--dbgfile — build only0ae3ee5test(uci): bump SENTINEL_POLL_TIMEOUT — TEST onlyf9f746ddocs(CLAUDE.md) — docs onlyWorth re-bisecting with the new criterion: "does GOT2 appear during EE
recv?"
ClientHello sig_algs verified
src/tls_handshake.s:599-604correctly advertises both:So the server's choice of
0x0503is the C64-side intent. ServerHello onthe failing run is 86 B body — same shape as the P-256 case. The failure is
clearly downstream of negotiation.
What an agent researching this could look at
tls_hs_read_key,tls_hs_read_iv, and the 50 B ciphertext + 16 B AEAD tag of the firstencrypted record. Run an off-C64 reference AEAD decrypt against them with
the same nonce-construction (IV XOR seq=0). If the reference also fails,
the C64's keys are wrong. If reference succeeds, the C64's
aead_decryptpath is the bug.
has two code commits (3c60550, 8f28941) whose
bcs-on-bounded-draincallers in
net.smight have introduced a subtle SOCKET_READ post-errorpath that drops or corrupts buffered bytes. Specifically: does the new
bcs @pe_drain_toskip-the-ack-on-timeout shape leave the firmware in astate where the NEXT SOCKET_READ returns READ_FAIL spuriously?
tls_record_recv_and_decryptconsumes a CCS record (6 B) via @Retry,then reads the next record's header. With the new TOD-bounded drain
semantics, is there a scenario where uci_drain_status sets C=1 spuriously
and the caller forces tcp_state=ERROR mid-flight, even though there was
no actual firmware error?
should be
IV XOR (read_seq=0)in both cases on the first record.Repro
After
git checkout master(HEADf9f746d), bootstrap build:Then on a host with U64E LAN access:
DEBUG_CAPTURE=0is intentional — with capture on, the U64E FPGA hits asustained-workload wedge that produces a noisy second failure mode
(
Ultimate64TimeoutErroron RESTread_memory) that hides the underlyingTLS issue. That harness-side wedge is filed separately as
c64-test-harness#129.
Cross-refs
p384-session-handoff-2026-05-20(this session's full notes)p384-session-handoff-2026-05-17(prior session, dispatcher OK)end-to-end"; this issue blocks closing that out