Skip to content

fix(cas): anchor the legacy CAS certificate chain so non-EU regions work - #12

Open
rtammekivi wants to merge 1 commit into
mainfrom
fix/cas-cert-fallback
Open

fix(cas): anchor the legacy CAS certificate chain so non-EU regions work#12
rtammekivi wants to merge 1 commit into
mainfrom
fix/cas-cert-fallback

Conversation

@rtammekivi

Copy link
Copy Markdown
Owner

Live video fails for every non-EU account with:

no live feed for Q4361xxxx ch2 (...) at 192.168.x.x: CAS key fetch failed for Q4361xxxx: CAS TLS handshake failed

Reported in #9 (Australia). Reproduced here without the reporter's account.

Not a station, network or clock problem

The station IP in that message is context from camera.py:266, not the endpoint that failed. _send_all — the first byte of any CAS request — runs after wrap_socket, so the connection dies before the station key is ever requested. The socket that failed is to a cloud host on port 6500.

CAS is resolved from sysConf[15]:[16], which follows the account's home region. "Global (auto-route)" does not change that — it logs in with meta 200 and still returns the home region's sysConf — and a deliberate cross-region login is refused with meta 1100 and no session. There is no config-level workaround.

Only EU was migrated

CAS endpoint issuer expires strict verify
eucas GlobalSign RSA OV SSL CA 2018 2026-12-14 OK
uscas Entrust CA - L1K 2019-06-17 fail
sgpcas Entrust CA - L1K 2019-06-17 fail
sacas Entrust CA - L1K 2019-06-17 fail

The three failing endpoints serve one identical certificate — same serial, same 2019 expiry, chaining L1K → G2 → Entrust Root (2006).

This is rot, not a latent bug

The expiry was never the problem. _is_certificate_expired_error exists precisely because the vendor app tolerates an expired CAS certificate, OpenSSL reported verify_code 10, the fallback fired, and video worked.

The 2024 Entrust distrust removed those roots from public trust stores. The HA 2026.8.1 image carries 119 roots, zero Entrust. The chain can no longer anchor, so verification now fails as verify_code 19 before expiry is considered, and the fallback's trigger condition never occurs.

Proven by changing only the trust store, against an unchanged sgpcas:

today's container trust store:                   verify_code=19 (self-signed certificate in certificate chain)
                                                 -> expiry fallback fires? False
with the Entrust root trusted (pre-2024 world):  verify_code=10 (certificate has expired)
                                                 -> expiry fallback fires? True

The fix

Carry the Entrust G2 root and trust it for CAS connections only, restoring the chain so the single tolerated defect is expiry again. _is_certificate_expired_error and the expiry assertion are untouched.

Verified against the live endpoints, patched code, inside the HA container:

  eucas.ezvizlife.com:6500    verified handshake OK
  sgpcas.ezvizlife.com:6500   expiry fallback OK (verify_code=10, chain anchored)
  uscas.ezvizlife.com:6500    expiry fallback OK (verify_code=10, chain anchored)
  sacas.ezvizlife.com:6500    expiry fallback OK (verify_code=10, chain anchored)

guards still hold:
  hostname mismatch rejected: CAS TLS certificate is not valid for attacker.example.com
  non-expired cert refused by fallback: CAS TLS expiry fallback received a non-expired certificate

EU is unaffected — it verifies outright and never reaches the fallback.

Also included: cas.py no longer discards the underlying SSLError (that omission is what made this undiagnosable from the log), and get_control_key keeps the CAS error visible when the 0.10.5 re-login hold-off suppresses the retry.

Alternatives rejected

  • Widen the fallback to any verification failure. The fallback handshake is itself CERT_NONE, so the only surviving check is the SAN — any self-signed certificate carrying *.ezvizlife.com would be accepted. No real security.
  • Pin the leaf or SPKI. eucas and sgpcas serve unrelated certificates from different CAs, so a pin needs an entry per region, and the eucas certificate expires 2026-12-14.

Worth a deliberate decision

This ships a publicly-distrusted CA root in the repo. It is scoped to the CAS context — never added to the system store, used for one hostname pattern on one port, and the peer certificate's SAN is still checked by hand on the fallback path. That is a real trade-off and should be an explicit choice, not a detail discovered later.

Refs: #9 rather than Fixes: the handshake is confirmed fixed, but the key fetch end-to-end can only be confirmed by the reporter.

Upstream ezviz_hp7 carries the same rot at pylocalapi/cas.py#L127-L133 — every non-EU user of that integration is broken identically.

🤖 Generated with Claude Code

Live video fails outside the EU with "CAS TLS handshake failed". CAS is
reached at sysConf[15]:[16], which is the account's home region — Global
(auto-route) does not change it, and a cross-region login is refused with
meta 1100, so no config choice avoids it.

Only eucas was migrated to GlobalSign. uscas, sgpcas and sacas all still
serve one Entrust-issued *.ezvizlife.com certificate that expired
2019-06-17, chaining through Entrust L1K and G2. The expiry alone was
never a problem: _is_certificate_expired_error exists precisely because
the vendor app tolerates it, and OpenSSL reported verify_code 10, so the
fallback fired and video worked.

The 2024 Entrust distrust removed those roots from public trust stores —
the HA 2026.8.1 image carries 119 roots and no Entrust. The chain can no
longer anchor, so verification now fails as verify_code 19 (self-signed
certificate in certificate chain) before expiry is ever considered, the
fallback's trigger never occurs, and the handshake is refused. Proven by
swapping only the trust store against an unchanged sgpcas: code 19 with
the image's roots, code 10 with the Entrust root trusted.

Carry the Entrust G2 root (notAfter 2030-12-07) and trust it for CAS
connections only, restoring the chain so the one tolerated defect is
expiry again. Verified against the live endpoints from inside the HA
container: eucas verifies outright, sgpcas/uscas/sacas reach the expiry
fallback with the chain anchored, a hostname mismatch is still rejected,
and the fallback still refuses a non-expired certificate.

Bundling the CA is deliberately preferred over widening the fallback to
any verification failure: that would accept any self-signed certificate
carrying a *.ezvizlife.com SAN, since the fallback handshake itself is
unverified. Pinning the leaf is not viable either — eucas and sgpcas
serve unrelated certificates and the eucas one expires 2026-12-14.

Also stop discarding the underlying SSLError, which is what made this
undiagnosable from the log, and keep the CAS error visible when the
0.10.5 re-login hold-off suppresses the retry in get_control_key.

Upstream ezviz_hp7 has the same rot at pylocalapi/cas.py#L127-L133.

Refs: #9

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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