Skip to content

P-384 TLS handshake regression: first encrypted record (EncryptedExtensions) fails decrypt on hw, blocks wall-clock measurement #45

Description

@JC-000

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=0x02
  • tcp_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 only
  • e52216e refactor(tools/uci): P-384 arbiter helper — TEST only
  • 3c60550 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 only
  • 0ae3ee5 test(uci): bump SENTINEL_POLL_TIMEOUT — TEST only
  • f9f746d 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

  1. 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.
  2. 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?
  3. 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?
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions