19 Commits
Author SHA1 Message Date
Jason Morcos 2a6fc627f2 feat(protocol): add PSK authentication provider 2026-08-08 16:06:53 -07:00
Jason Morcos 8fb37ca2ed refactor(protocol): add certificate authentication provider 2026-08-08 14:29:30 -07:00
Jason Morcos dd453ebdfb feat(protocol): bound DTLS endpoint probing 2026-08-03 12:45:14 -07:00
Jason Morcos d677c72f89 feat(protocol): add resolved connected UDP endpoints 2026-08-03 12:45:04 -07:00
Jason Morcos c7e15a7dd3 feat(errors): add redacted typed failures 2026-08-03 12:44:51 -07:00
Quite Yellow 597a88ff25 Merge pull request #21 from Moballo-LLC/codex/py-01-ci-foundation
ci: add validation and package smoke tests
2026-08-03 19:33:28 +01:00
Jason Morcos fc6240b72e test(safety): allow public GitHub attachments 2026-08-02 16:31:15 -07:00
Jason Morcos 6dc9dca339 fix(setup-cert): keep SHA-1 retry compatible with LibreSSL 2026-08-02 10:43:40 -07:00
Jason Morcos 2c93cb3097 test: make worker cleanup checks deterministic 2026-08-02 10:13:56 -07:00
Jason Morcos 119c114daa ci: add pull request validation 2026-08-02 10:02:52 -07:00
Quite Yellow a494e73e89 fix(setup_cert): surface openssl errors and work around SHA-1 crypto policy (#19)
* fix(setup_cert): surface openssl errors and work around SHA-1 crypto policy

The signing step forces -sha1 (the AC14K_M chain requires SHA-1-signed
leaves), which Fedora/RHEL's default crypto policy rejects on OpenSSL
3.x. run() also swallowed stderr, so the failure surfaced as an opaque
non-zero-exit traceback with no diagnostic.

- run() now raises CommandError carrying the command and openssl stderr
- mint_cert retries signing with a scoped OPENSSL_CONF enabling
  rh-allow-sha1-signatures when the first attempt fails
- main() prints the update-crypto-policies fallback on failure

Fixes #15

* test(setup_cert): cover SHA-1 signing, error surfacing, and crypto-policy retry

Regression tests for the #15 fix:
- full mint_cert flow (SHA-1 leaf, UUID SAN, custom OIDs, chain assembly)
- CommandError surfaces openssl stderr on a genuine signing failure
- signing retries via the SHA-1 override when the plain attempt is blocked
- run() raises CommandError with detail
2026-08-01 11:47:09 +01:00
Jack Nagy 1a35cd59a1 feat(protocol): add DTLS ClientHello liveness probe + wire it into the bridge
A stateless-by-default DTLS ClientHello probe that classifies a host:port
as DEAD/LIVE/COMPLETED/REJECTED in ~1 RTT off the server's first flight,
sitting in front of the full handshake.

Probe (smartthings_local/protocol/dtls_probe.py):
- Stateless liveness mode (default): stops at HelloVerifyRequest and never
  sends the cookie'd second ClientHello, so by RFC 6347 §4.2.1 it leaves
  no association on the device — safe to run before a real connect.
- Diagnostic mode (stateless=False): drives the handshake further to
  capture cipher/cert-chain/CertificateRequest or a fatal Alert, for
  OCF-PKI-wall characterization (#16). Kept out of hot reconnect paths.
- Retransmit + retries: services OpenSSL's DTLS retransmit timer so a
  single dropped ClientHello no longer reads as a false DEAD.

MQTT bridge (mqtt_demo):
- Stateless pre-flight gate in session_once() rejects a silent/rebooting
  device or wrong port in ~3s (retries=1) instead of eating the 12s
  HANDSHAKE_TIMEOUT_S per reconnect.
- OCF-band port autodiscovery when OCF_PORT is unset: races the band in
  parallel and returns on the first port to answer LIVE (~1 RTT, abandoning
  the dead-port probes), cached across reconnects; the stateless gate
  leaves no orphan, preserving the fixed-source-port §4.2.8 invariant.

Validated on real hardware (dryer 49155 / oven 49154): parallel discovery
resolves both ports in <1s, connect with no orphan cooldown, and a wrong
pinned port rejected in ~3s.

Tests: probe behaviour (retransmit recovery, stateless single-flight
guard, silent-port flight budget, diagnostic continuation) and bridge
port-resolution (pinned gate, parallel discovery early-exit, cache).
2026-08-01 10:57:54 +01:00
Marc Billow a2dc524c0b feat: support in-memory PEM cert/key alongside file paths in DtlsCoapSession
localthings mints its client cert at runtime through the HA config flow
and never writes it to disk. DtlsCoapSession only accepted cert_path/
key_path (file-based), which would have forced localthings to write its
in-memory cert/key to disk on every connect just to migrate off its
vendored copy of this transport layer.

Adds an alternate cert_pem/key_pem constructor path (ported from
localthings' own _load_pem_chain), validated so exactly one cert source
(file pair or PEM pair) is required. Existing file-path callers
(mqtt_demo, setup_cert.py) are unaffected — verified against both real
appliances with each constructor path.
2026-07-06 15:04:20 -05:00
Jack Nagy 3fdc735141 feat(packaging): nest protocol/ + ocf/ under smartthings_local, add PyPI packaging
Nest the two library packages under a single import namespace so they
can ship as one distribution:

  protocol/ -> smartthings_local/protocol/
  ocf/      -> smartthings_local/ocf/   (git mv, history preserved)

- Rewrite all imports protocol.* -> smartthings_local.protocol.*,
  ocf.* -> smartthings_local.ocf.* across the ocf modules, mqtt_demo/
  (bridge, descriptor, samples), and tests.
- Add pyproject.toml: dist name `smartthings-local`, hatch-vcs versioning
  from v* tags, wheel ships only smartthings_local/.
- Add .github/workflows/publish.yml: build + PyPI Trusted Publishing on
  v* tags (OIDC, no stored token).
- Force-include protocol/ocf_root_ca.pem via [tool.hatch.build] artifacts:
  it is tracked but matches .gitignore's *.pem, so hatchling's VCS file
  selection would drop it — and dtls_session.py loads it at runtime.
- Update mqtt_demo Dockerfile COPY and deploy.sh tar allowlist to the
  single smartthings_local/ package.
- .gitignore: build artifacts (_version.py, dist/, *.egg-info/).

Validated: pytest tests/ (11 passed), python -m build produces sdist +
wheel with the pem bundled, fresh pip install resolves all nested imports
with the pem readable from site-packages.
2026-07-06 20:29:19 +01:00
Marc Billow c7232b3df6 fix: isolate subprocess PYTHONPATH in import-isolation test to prevent false pass 2026-07-04 17:29:22 -05:00
Marc Billow 8a6bc8d594 test: verify protocol/ + ocf/ import in isolation from mqtt_demo/
- Add test_import_isolation.py: isolation test that copies protocol/ and ocf/
  to a temp directory without mqtt_demo/ and verifies all modules import
- Add conftest.py: pytest configuration to add repo root to sys.path,
  enabling tests to import protocol/ and ocf/ packages
2026-07-04 17:29:22 -05:00
Marc Billow 24fc197a9e refactor: extract ocf/ — fold sensors.index_links into StateCache.index_device_tree 2026-07-04 16:02:08 -05:00
Marc Billow 10773bb8c8 refactor: extract protocol/ — split CoAP wire helpers from DtlsCoapSession 2026-07-04 15:55:12 -05:00
Marc Billow d33b46171a test: characterize CoAP wire encode/decode before extracting protocol/ 2026-07-04 15:50:38 -05:00