Skip to content

feat(ocpp): TLS and mutual TLS for the embedded OCPP servers - #1342

Merged
woopstar merged 5 commits into
mainfrom
feat/1341-ocpp-tls
Oct 29, 2026
Merged

woopstar merged 5 commits into
mainfrom
feat/1341-ocpp-tls

Conversation

@woopstar

@woopstar woopstar commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds TLS and mutual TLS to the embedded OCPP 1.6 servers. The certificates can be supplied by the user or generated by HSEM in Home Assistant's SSL folder.

Fixes #1341

Branch: feat/1341-ocpp-tls

What changed and why

New setting hsem_ocpp_tls_mode (OCPP step, shared by both servers): off (default, unchanged ws://), tls (wss:// with a server certificate), mtls (wss:// plus a required client certificate). It comes with three optional paths: server certificate, server key, and client CA. Empty paths mean HSEM uses the certificates it generates.

Generated certificates (utils/ocpp_tls.py, new):

  • Stored in /ssl/hsem/ocpp/ when /ssl exists (HA OS and Supervised). Otherwise stored in <config>/ssl/hsem/ocpp/. HSEM only writes inside that subfolder, never directly into /ssl.
  • A private CA (RSA 3072, 10 years) that is never overwritten. A half-present CA (only ca.crt or only ca.key) is an error, not something HSEM silently replaces.
  • A server certificate (RSA 2048, 1 year) signed by the CA. Its SANs are HA's LAN IPs (network integration), the host name and the internal URL host. It is reissued on start-up when it expires within 30 days, when the names change, or when it wasn't signed by the current CA.
  • Keys are written atomically with mode 0600 (the temporary file already has 0600). The folders get 0700.
  • All key generation and file I/O runs in hass.async_add_executor_job, including the /ssl existence check.

Server (ocpp_server.py): OCPPServer takes an optional ssl_context, built on homeassistant.util.ssl.server_context_modern() (TLS 1.2+). In mtls mode the context uses CERT_REQUIRED with the client CA. The upgrade is also refused with HTTP 403 when the peer certificate's CN doesn't match the last segment of the CPID path. That check runs before the reconnect handling, so a charger can't use its own valid certificate to take over another charger's session.

CN mismatch setting hsem_ocpp_tls_allow_cn_mismatch (default off, mtls only): for chargers whose certificate is named after their serial number. When on, a certificate whose CN differs from the CPID is accepted, and a warning is logged on every such connection. The certificate must still chain to the client CA, and a client certificate is still required. Only the name binding is dropped, so the docs warn that any certificate from that CA can then connect under any CPID.

No fallback to ws:// (coordinator_lifecycle.py): TLS is prepared once and shared by both servers. If it fails, neither server starts, and HSEM logs OCPP TLS setup failed, OCPP server(s) not started.

TLS changes reload the entry (__init__.py): option changes are normally applied in place without restarting the OCPP servers. Without this change, switching TLS on would have kept plain ws:// running until a restart. The TLS settings are now part of the snapshot that decides when the entry reloads.

Service hsem.ocpp_generate_client_certificate (services_ocpp_tls.py, new): signs a client key and certificate (CN=<cpid>) with HSEM's CA and returns certificate_path, key_path and ca_certificate_path (paths only). The CPID must match ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ because it becomes a file name. The service refuses to overwrite an existing certificate without replace: true. It also refuses when a custom client CA is configured, since HSEM can't sign with a CA whose key it doesn't hold.

Diagnostics: ocpp_tls block with mode, the CN mismatch setting, certificate source, server certificate expiry, SANs and the CA's SHA-256 fingerprint, or the setup error. It never contains key material. build_diagnostics_dump only accepts a real dict there.

OCPP status sensor: the url attribute now advertises wss:// when TLS is on.

Manifest: network added to after_dependencies (used for the SANs). No new requirement: cryptography already ships with Home Assistant.

Files

  • New: utils/ocpp_tls.py, services_ocpp_tls.py
  • Changed: custom_sensors/ocpp_server.py, custom_sensors/ocpp_sensors.py, coordinator_lifecycle.py, coordinator.py, coordinator_state.py, __init__.py, flows/ocpp.py, custom_sensors/config_reader.py, models/sensor_config.py, const.py, services.py, services.yaml, diagnostics.py, utils/diagnostics.py, manifest.json, translations/{en,da,de,es}.json
  • Docs: docs/ev-charge-plan-setup.md (new section "Securing the OCPP connection (TLS)"), docs/config-flow-reference.md, docs/services-reference.md, docs/sensors-reference.md, .github/memories.md (module map)

Tests

  • tests/custom_sensors/test_ocpp_server_tls.py: real wss:// connections against a running OCPPServer with generated certificates. Covered: server-only TLS; plain ws:// refused; valid client certificate; /ocpp/<id> base-URL path; missing client certificate; certificate from another CA; CN naming another charger (403); and a mismatching certificate not displacing the connected charger. With the CN check removed, the two CN tests fail. With the mismatch setting on: CP1's certificate connects as CP2 (warning logged), a certificate from another CA is still refused, and a missing client certificate is still refused.
  • tests/utils/test_ocpp_tls.py: folder resolution, CA generation/reuse/permissions/half-CA/expiry, server certificate SANs/usage/rotation (expiry, names, replaced CA), client signing, unsafe CPIDs, context building, config-flow file checks, CN parsing and matching, SAN discovery, and the executor hand-off.
  • tests/test_ocpp_tls_wiring.py: flow schema and validation, config reader, server start-up (off / shared context / failure starts nothing / no generation with OCPP off), the service, and diagnostics.
  • tests/test_init_options_reload.py, tests/test_init_setup_order.py: each TLS setting change schedules a reload, and the reload snapshot includes the TLS settings.
  • tests/sensors/test_ocpp_sensors_state.py: wss:// URL.

Quality gates

  • ./scripts/quality.sh all: passes (7538 passed, 1 skipped, run on the branch rebased onto current main; new modules at 97–100 % coverage; mypy, pyright, translations and file-size checks clean)
  • Pinned mypy 2.4.0 (the devcontainer has 2.3.1): no issues in 610 files
  • scripts/validate_translations.py: 0 missing, 0 stale, 0 placeholder mismatches

Known limitations

  • HTTP Basic auth is not included, as the issue scoped. That leaves security profile 1 out, and profile 2 here covers only the TLS side.
  • Not tested against a real charger. The TLS behaviour is covered by real loopback connections with Python's TLS stack only. Embedded charger TLS stacks may differ (cipher suites: the modern list is ECDHE + AES-GCM/ChaCha20 on TLS 1.2).
  • Port changes still apply in place without restarting the servers. This was already true before this change and is not addressed here.

🤖 Generated with Claude Code

…1341

Add hsem_ocpp_tls_mode (off/tls/mtls, default off) with optional server
certificate, server key and client CA paths, shared by both OCPP servers.
With no paths, HSEM generates a private CA and a CA-signed server
certificate in /ssl/hsem/ocpp/ (or <config>/ssl/hsem/ocpp/ without /ssl),
keys 0600, CA never overwritten, server certificate reissued within 30
days of expiry or when the host names change. All crypto and file I/O
runs in the executor.

In mtls mode the server requires a client certificate from the client CA
and refuses the upgrade (HTTP 403) when its CN does not match the CPID
path, before any existing session is touched. If TLS cannot be set up,
neither server starts: there is no fallback to plain ws://. TLS setting
changes reload the entry so the servers restart with them.

New service hsem.ocpp_generate_client_certificate signs a client key and
certificate (CN = CPID) and returns paths only. Diagnostics gain an
ocpp_tls block (mode, expiry, SANs, CA fingerprint or the setup error),
and the OCPP status sensor's url attribute uses wss:// with TLS on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the enhancement New feature or request label Oct 28, 2026
…riptions - Fixes #1341

Hassfest rejects translation strings that look like HTML.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the bug Something isn't working label Oct 28, 2026
@codecov-commenter

codecov-commenter commented Oct 28, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 98.10811% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.21%. Comparing base (9e08d44) to head (12c31ad).

Files with missing lines Patch % Lines
custom_components/hsem/utils/ocpp_tls.py 97.48% 7 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1342      +/-   ##
==========================================
- Coverage   99.23%   99.21%   -0.03%     
==========================================
  Files         260      262       +2     
  Lines       19424    19787     +363     
==========================================
+ Hits        19275    19631     +356     
- Misses        149      156       +7     
Flag Coverage Δ
unittests 99.21% <98.10%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

woopstar and others added 3 commits October 29, 2026 13:28
…CPID - Fixes #1341

Add hsem_ocpp_tls_allow_cn_mismatch (default off, mtls only) for chargers
whose certificate is named after their serial number. When on, the CN
check is skipped and a warning is logged on every such connection; the
certificate must still chain to the client CA and is still required.
The setting is part of the reload snapshot and the diagnostics block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #1341

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@woopstar
woopstar merged commit 90a05fb into main Oct 29, 2026
19 checks passed
@woopstar
woopstar deleted the feat/1341-ocpp-tls branch October 29, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(ocpp): support TLS server certificates and client certificates (mTLS), with generated certificates as fallback

2 participants