Skip to content

feat(protocol): add bounded DTLS endpoint probing - #25

Open
Jason-Morcos wants to merge 3 commits into
QuiteYellow:mainfrom
Moballo-LLC:codex/py-04-bounded-probe
Open

feat(protocol): add bounded DTLS endpoint probing#25
Jason-Morcos wants to merge 3 commits into
QuiteYellow:mainfrom
Moballo-LLC:codex/py-04-bounded-probe

Conversation

@Jason-Morcos

@Jason-Morcos Jason-Morcos commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a frozen, stateless DTLS first-flight API for one port and for a bounded concurrent port set;
  • discover both standard secure OCF port 5684 and Samsung's dynamic 49152-49160 range without first-responder guessing;
  • preserve IPv4, IPv6, and scoped-IPv6 endpoint behavior through connected UDP sockets;
  • make the bridge fail closed when more than one unconfigured listener answers; and
  • document the exact newer OCF-PKI laundry findings behind issues Newer Samsung firmware uses standard OCF ports (5683/5684) + non-AC14K_M trust anchor #16 and Can't connect to BespokeAI Washer/Dryer #20, including how I connected the two WD53 appliances and what remains unsafe or unsupported.

This addresses the endpoint-discovery portion of #16 and #20. It deliberately does not claim that either report's authentication/setup path is solved.

Context

I now have several Samsung appliances working locally with code I have maintained in-house, including two Bespoke AI Laundry Combo units. I would like to contribute the reusable, sanitized lessons upstream as small, independently tested contracts. This PR is the endpoint/probe contract; authentication providers, PSK transport, and reviewed setup policy remain separate follow-ups.

Why these two reports matter

Issue #16 is the exact WD53 profile I tested

Issue #16 reports WD53DBA900HZ with primary software 20260416.215549. My two units identify as the full SKU WD53DBA900HZA1, family AWM-US-M64-24-WD80, with the same software version and the same protocol behavior:

  • public OCF on UDP 5683;
  • standard/dynamically advertised secure endpoints, including IPv4, IPv6 ULA, and scoped IPv6 link-local forms;
  • AC14K_M authentication reaches DTLS but receives fatal unknown_ca;
  • manufacturer-certificate OTM methods 2 and 0xFF02 are advertised; and
  • the provisioning nonce rotates per read and additional authorization is required.

That makes #16 the same appliance/software profile for this library's purposes. This PR fixes the immediate fixed-port/IPv4-only discovery assumptions, but not its authentication boundary.

Issue #20 is related evidence on different hardware

Issue #20 reports a WW11BB534DAWS6 washer and DV90BB5245AWS6 dryer. The washer evidence shows:

  • public OCF on 5683;
  • a DTLS listener on 49154;
  • owned:false and isop:false;
  • only Samsung manufacturer OTM 0xFF02; and
  • handshake_failure when the existing certificate path is attempted.

That makes this PR's 5684-plus-dynamic-range discovery and explicit alert classification directly useful. It does not prove that the WD53 authorization profile applies: the TLS alert differs, the model-specific proof/timing/security payloads have not been validated, and the dryer has not supplied equivalent protocol evidence.

How the WD53 appliances were actually connected

The complete sanitized record is in docs/ocf-pki-laundry.md. The important sequence was:

  1. Discover public OCF first. GET /oic/res on UDP 5683 exposed a 72-href directory and the current secure endpoint. The secure endpoint can move after sleep/restart, so it must be rediscovered rather than fixed to one 4915x port.

  2. Classify AC14K_M failure correctly. RSA and ECDSA AC14K_M client chains both reached the server and were rejected with fatal unknown_ca. Re-signing only the leaf, adding ciphers, or disabling verification cannot turn an untrusted authentication profile into authorization.

  3. Read public DOXM/PSTAT state. The units advertised OTM 2 (oic.sec.doxm.mfgcert) and 0xFF02 (x.org.iotivity.conmfgcert), a per-read rotating four-byte nonce represented as eight hex characters, and additional-authorization-required state.

  4. Open the manufacturer window through the model's authorized path. During one-time idle research, the signed-in SmartThings Android path invoked the same-account transition. The installed 5.0.47 stack checked the proofs in the appliance's order, which is the inverse of a newer helper's method naming:

    • TriggerSerialHashRequest: SHA256(serial_hash_ascii || nonce_raw) with no account value;
    • rotate the nonce;
    • TriggerAutoResetHashRequest: SHA256(serial_hash_ascii || SHA256(user_id_ascii) || fresh_nonce_raw) using the raw inner digest and the ten-character same-account user ID.

    serial_hash_ascii is the 128-character lowercase hex SHA-512 digest of the ASCII registration serial. There are no delimiters. Only a fresh public DOXM/PSTAT read—not the missed application callback—was treated as proof that the unit entered manufacturer RFOTM.

  5. Use the correct manufacturer carrier. Inside that confirmed window, server-authenticated manufacturer DTLS succeeded with exactly ECDHE-ECDSA-AES128-GCM-SHA256 and no AC14K_M, TEST, or OneApp client identity leaf.

  6. Derive the per-appliance OwnerPSK exactly. IoTivity first expands the 48-byte master secret with TLS 1.2 SHA-256 P_hash using key expansion || server_random || client_random to the GCM suite's 120-byte key block. It then expands that key block with the selected method label, raw 16-byte owner UUID, and raw 16-byte appliance UUID to a 16-byte OwnerPSK.

  7. Stage, prove, then finalize. The credential was staged before security mutation, the reviewed CRED/ACL/DOXM/PSTAT transaction was applied, and a fresh ECDHE-PSK session had to prove the candidate before publication/finalization. The resulting runtime path returned 39 complete protected representations per unit. Low-risk setting/power changes had exact protected readback and were restored; the same changes remained visible in SmartThings for the tested transaction.

This was an explicit OCF ownership-changing setup operation, not a harmless discovery trick. SmartThings pairing survived on the two tested units, but the signed request authority and private account value are not public onboarding inputs. This PR contains no OTM implementation, credentials, security-resource writes, reset/takeover behavior, hardware I/O, or captured identifiers. Runtime after setup was LAN OwnerPSK only; Android was not a startup, polling, command, wake, or recovery dependency.

Probe contract

The production probe now:

  • generates one ClientHello through the same OpenSSL cipher/MTU profile as a real session;
  • freezes that first flight and retransmits only byte-identical records inside one timeout budget;
  • uses connected UDP sockets so replies are bound to the resolved IPv4/IPv6 peer;
  • accepts only structurally complete epoch-zero HelloVerifyRequest, ServerHello, or Alert responses, with warning alerts treated as liveness and only fatal alerts as rejection;
  • ignores unrelated/malformed appliance datagrams without consuming a retransmission;
  • probes a de-duplicated set concurrently, with a maximum of 32 candidates;
  • waits for bounded post-resolution worker cleanup and preserves caller order;
  • documents that the timeout bounds socket I/O after synchronous platform resolution, whose timing remains operating-system-controlled;
  • returns ambiguous if multiple listeners answer unless a previously proven/configured port is explicitly preferred; and
  • exposes fixed error codes without retaining host names or raw packets.

The old probe() result shape remains as a compatibility adapter. Stateful handshake research now has the explicit diagnose_dtls_handshake() name and CLI --diagnostic opt-in; discovery and reconnect never feed the cookie response back to OpenSSL or send a cookie-bearing second ClientHello.

Stack and scope

This branch is one commit on top of the small prerequisite branches:

Once #23 and #24 land, rebasing leaves only the single bounded-probe commit. The CI/package and LibreSSL fixes from #21 and #22 are now in upstream main.

Not included here: authenticated endpoint validation, certificate-provider abstraction, PSK sessions, Samsung server-profile verification, OCF security payload codecs, OwnerPSK code, or any automatic ownership decision.

Validation

  • PATH="/opt/homebrew/opt/openssl@3/bin:$PATH" python -m pytest -q116 passed
  • focused probe/endpoint/bridge suite — 53 passed
  • system LibreSSL 3.3.6 — 116 passed
  • Ruff F, I, and E checks for the new probe/tests — passed
  • wheel and sdist build — passed
  • clean-directory wheel import of the new APIs — passed
  • introduced-content/staged share-safety scan — passed
  • git diff --check — passed

All network fixtures are synthetic or documentation-reserved; no appliance, household, account, credential, UUID, address, packet capture, or raw exception data is included.

@Jason-Morcos

Copy link
Copy Markdown
Contributor Author

Adversarial follow-up complete for exact head f014b068cb781c47a84a30b7531239a09bd96e4a. The liveness parser now couples alert detail to the validated epoch-zero alert record, treats warning alerts as live and only fatal alerts as rejected, validates address family before worker launch, enforces the post-resolution diagnostic I/O deadline, rejects short sends, and applies the same 32-port/deduplication/certificate-pair limits in the CLI. The public contract now explicitly documents that synchronous platform resolution remains OS-timed.

Validation: 101 passed on Python 3.14 / OpenSSL 3.6.3; 53 focused probe/endpoint/bridge tests passed; the system LibreSSL baseline is 99 passed with the 2 setup-certificate failures isolated to #22; build/import, Ruff, diff, and introduced-content safety checks passed.

@codex review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant