Skip to content

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

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.

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