FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

SslAesTransport CIPHERS lacks ECDHE suites — handshake_failure with recent Tapo camera firmware (C120 1.9.2) · Issue #1724 · python-kasa/python-kasa · GitHub

Repository navigation

SslAesTransport CIPHERS lacks ECDHE suites — handshake_failure with recent Tapo camera firmware (C120 1.9.2) #1724

Closed

Description

Root cause: cipher suite mismatch in SslAesTransport (not the weak certificate)

Device: Tapo C120, firmware 1.9.2
python-kasa: 0.10.2

The camera only accepts ECDHE cipher suites

$ nmap --script ssl-enum-ciphers -p 443 <camera-ip>
|   TLSv1.2:
|     ciphers:
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp521r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp521r1) - A
|     cipher preference: server
|_  least strength: A

TLS 1.2 only. No TLS 1.3. No static-RSA key exchange.

SslAesTransport offers only static-RSA cipher suites

kasa/transports/sslaestransport.py:

CIPHERS = ":".join([
    "AES256-GCM-SHA384",
    "AES256-SHA256",
    "AES128-GCM-SHA256",
    "AES128-SHA256",
    "AES256-SHA",
])

All five are static-RSA key exchange. The intersection with what the
camera offers is empty, so the device replies with a TLS handshake_failure.

Isolated reproduction

import socket, ssl
KASA = "AES256-GCM-SHA384:AES256-SHA256:AES128-GCM-SHA256:AES128-SHA256:AES256-SHA"
FIX  = KASA + ":ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384"

def t(label, ciphers):
    c = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    c.set_ciphers(ciphers); c.check_hostname = False; c.verify_mode = ssl.CERT_NONE
    try:
        with socket.create_connection(("<camera-ip>", 443), timeout=5) as s:
            with c.wrap_socket(s) as ss:
                print(f"[OK]   {label} -> {ss.cipher()[0]}")
    except Exception as e:
        print(f"[FAIL] {label} -> {e}")

t("kasa CIPHERS", KASA)
t("kasa + ECDHE", FIX)

Result:

[FAIL] kasa CIPHERS -> [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] ssl/tls alert handshake failure
[OK]   kasa + ECDHE -> ECDHE-RSA-AES128-GCM-SHA256

Note on the "weak certificate" theory

The camera does present a self-signed 1024-bit RSA certificate
(CN=TPRI-DEVICE), and OpenSSL rejects it under the default SECLEVEL=2
with EE certificate key too weak. However this is not what breaks
SslAesTransport, which sets verify_mode = ssl.CERT_NONE and never
validates the certificate. The reported error is SSLV3_ALERT_HANDSHAKE_FAILURE,
not CERTIFICATE_VERIFY_FAILED — the failure happens during cipher
negotiation, before any certificate handling.

Suggested fix

Add the ECDHE suites to CIPHERS in SslAesTransport (and likely the
same for KlapTransport / LinkieTransport, which use the same pattern):

CIPHERS = ":".join([
    "ECDHE-RSA-AES256-GCM-SHA384",
    "ECDHE-RSA-AES128-GCM-SHA256",
    "AES256-GCM-SHA384",
    "AES256-SHA256",
    "AES128-GCM-SHA256",
    "AES128-SHA256",
    "AES256-SHA",
])

This keeps backward compatibility with older firmware while supporting
the newer ones. Likely affects C120/C121 (1.9.2), C210 (1.5.2),
C230 (1.4.2), C260 (1.1.3), C500 — all recently reported.

Activity

  1. Nino6689 commented on Aug 6, 2026

    Independent confirmation on a second model — Tapo C225 (PTZ), SMART.IPCAMERA, login_version: 3 — python-kasa 0.10.2, Python 3.14, OpenSSL 3.5.7, HA 2026.7.4 (HAOS). Same failure, same fix.

    Adding two data points that go slightly beyond the original report.

    1. Each shipped cipher tested individually, rather than as a list — every one is rejected, so it is not one bad entry:

    Offered Result
    AES256-GCM-SHA384 FAIL handshake_failure
    AES256-SHA256 FAIL handshake_failure
    AES128-GCM-SHA256 FAIL handshake_failure
    AES128-SHA256 FAIL handshake_failure
    AES256-SHA FAIL handshake_failure
    shipped list + any ECDHE OK — TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
    DEFAULT OK — ECDHE-RSA-AES128-GCM-SHA256

    2. Explicitly ruling out the security-level explanation. This one keeps resurfacing in the downstream HA issues, so it is worth nailing down: appending @SECLEVEL=0 and @SECLEVEL=1 to the shipped list still fails identically. verify_mode=CERT_NONE is already set in _create_ssl_context, so the 1024-bit certificate is never validated. Security level is not the blocker — the cipher intersection is empty.

    A useful diagnostic trap for anyone triaging this: a plain ssl.create_default_context() connects to the camera fine from the very same container, because it offers ECDHE. "TLS works when I test it manually" therefore does not clear the library — the reproduction has to use SslAesTransport.CIPHERS itself.

    Applying exactly the fix proposed here (appending ECDHE-RSA-AES128-GCM-SHA256 and ECDHE-RSA-AES256-GCM-SHA384, keeping the static-RSA entries for older firmware) takes the config entry from a permanent setup_retry loop to loaded, with all 13 PTZ/detection entities populating. Cipher selection is server-preference, so older devices are unaffected.

    Happy to raise this as a PR if that would help — it is a one-line change to the CIPHERS list.

    Related downstream reports, all open and all the same root cause: home-assistant/core#165645, #164045, #167714, #156926, #162498. #1727 here is the same thing on C225 V2 firmware 1.3.1.

  2. Aho-K commented on Aug 30, 2026

    Another affected model: Tapo C200, HW 3.0, FW 1.9.1 Build 260326 Rel.26771n (SMART.IPCAMERA, login_version: 3), python-kasa 0.10.2, Home Assistant 2026.8.3 (Container), Python 3.14.

    Same root cause and same fix. Two additions that may help scope it.

    1. Every shipped cipher tested individually against the C200

    Not one bad entry — the whole list is rejected, and both ECDHE suites are accepted:

    Cipher offered C200 (FW 1.9.1)
    AES256-GCM-SHA384 FAIL handshake_failure
    AES256-SHA256 FAIL handshake_failure
    AES128-GCM-SHA256 FAIL handshake_failure
    AES128-SHA256 FAIL handshake_failure
    AES256-SHA FAIL handshake_failure
    ECDHE-RSA-AES128-GCM-SHA256 OK, TLSv1.2
    ECDHE-RSA-AES256-GCM-SHA384 OK, TLSv1.2

    2. The discriminator is the key exchange, not the certificate type

    I have a C210 (HW 3.0, FW 1.2.6) on the same LAN that still connects fine with the current CIPHERS. Probing it shows it serves two different certificates depending on the negotiated suite:

    # C210, restricted to a static-RSA suite  -> handshake succeeds
    $ openssl s_client -connect <c210>:443 -cipher 'AES256-GCM-SHA384'
      Signature Algorithm: sha256WithRSAEncryption
      Public-Key: (1024 bit)
    
    # C210, default cipher list  -> ECDHE-ECDSA-AES128-GCM-SHA256
    $ openssl s_client -connect <c210>:443
      Signature Algorithm: ecdsa-with-SHA256
      Public-Key: (256 bit)
    

    The C200 presents only the RSA-1024 certificate and offers only ECDHE-RSA-*.

    So "works" vs "handshake_failure" is not about whether the device has an ECDSA certificate — it is about whether the firmware still offers static-RSA key exchange at all. Firmware that has dropped it fails with the current list regardless of model, which fits the spread reported here and in #1736.

    3. SECLEVEL is not involved

    The RSA-1024 certificate would normally trip OpenSSL's default SECLEVEL=2, so that is a tempting explanation, but it is not the cause here. _create_ssl_context() already sets verify_mode = ssl.CERT_NONE, and appending the ECDHE suites works with SECLEVEL left at the default 2. No security-level downgrade is needed.

    Confirmed stopgap

    Appending rather than replacing:

    CIPHERS = ":".join([
        "AES256-GCM-SHA384",
        "AES256-SHA256",
        "AES128-GCM-SHA256",
        "AES128-SHA256",
        "AES256-SHA",
        "ECDHE-RSA-AES128-GCM-SHA256",
        "ECDHE-RSA-AES256-GCM-SHA384",
    ])

    With this the C200 pairs and exposes the full feature set including PanTilt, while the C210 continues to negotiate AES256-GCM-SHA384 exactly as before — so appending does not disturb devices that currently work.

    Models reported affected so far across this issue and #1736: C120 1.9.2, C200 1.9.1, C201, C220 1.5.4, C225, TC70.

  3. Aho-K commented on Aug 30, 2026

    Correction / follow-up to my previous comment: this is already fixed on main, and has been for six months.

    ECDHE-RSA-AES128-GCM-SHA256 was added to SslAesTransport.CIPHERS in b22917a ("Allow SSL connections to older devices", #1654), merged 2026-02-21:

    # main today
    CIPHERS = ":".join(
        [
            "ECDHE-RSA-AES128-GCM-SHA256",
            "AES256-GCM-SHA384",
            "AES256-SHA256",
            "AES128-GCM-SHA256",
            "AES128-SHA256",
            "AES256-SHA",
        ]
    )
    $ git tag --contains b22917a
    (no output)
    

    The commit is in no release tag. The newest release is 0.10.2 from 2025-02-12, which predates it, and that is the version Home Assistant pins (python-kasa[speedups]==0.10.2). So every affected user here is hitting a bug that was fixed half a year ago but has never shipped.

    That reframes this issue and #1736: no code change is needed — the blocker is purely that no release has been cut. Anyone reading this thread and considering a PR can skip it.

    For anyone stuck in the meantime, the runtime workaround that avoids editing site-packages (which is wiped on every Home Assistant image update) is to append the cipher from a small custom integration, since CIPHERS is read as self.CIPHERS inside _create_ssl_context() on every connection:

    from kasa.transports.sslaestransport import SslAesTransport
    
    if "ECDHE" not in SslAesTransport.CIPHERS:
        SslAesTransport.CIPHERS += ":ECDHE-RSA-AES128-GCM-SHA256"

    This is version-agnostic and becomes a no-op automatically once a release containing #1654 lands. Verified against a Tapo C200 (HW 3.0, FW 1.9.1) on python-kasa 0.10.2 / Home Assistant 2026.8.3: the camera pairs and exposes the full feature set including PanTilt, and an existing C210 on the same install continues to negotiate AES256-GCM-SHA384 unchanged.

  4. rytilahti commented on Oct 12, 2026

    Member

    This should already be fixed by the newest release, but if not, feel free to reopen. #1798 aims to make the transports more robust also for potential future firmware updates, please feel free to test and let me know if you have any issues!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL