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.
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
TLS 1.2 only. No TLS 1.3. No static-RSA key exchange.
SslAesTransport offers only static-RSA cipher suites
kasa/transports/sslaestransport.py: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
Result:
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=2with
EE certificate key too weak. However this is not what breaksSslAesTransport, which sets
verify_mode = ssl.CERT_NONEand nevervalidates the certificate. The reported error is
SSLV3_ALERT_HANDSHAKE_FAILURE,not
CERTIFICATE_VERIFY_FAILED— the failure happens during ciphernegotiation, before any certificate handling.
Suggested fix
Add the ECDHE suites to
CIPHERSinSslAesTransport(and likely thesame for
KlapTransport/LinkieTransport, which use the same pattern):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.