kinnay / NintendoClients

Python package to communicate with Switch, Wii U and 3DS servers
MIT License
537 stars 63 forks source link

SSL Handshake Failure #123

Open pccavar opened 6 months ago

pccavar commented 6 months ago

I have encountered an issue with a script that was functioning correctly until recently. The error has started occurring both locally and on the server simultaneously. I suspect there might have been some changes on the host side that could be causing this problem. Can you provide insights into any modifications that might have been made recently?

Error Traceback:

Traceback (most recent call last):
  File "<download.py>", line 278, in <module>
    anyio.run(main)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anyio/_core/_eventloop.py", line 66, in run
    return async_backend.run(func, args, {}, backend_options)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 1968, in run
    return native_run(wrapper(), debug=debug)
  File "/Users/admin/miniforge3/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/Users/admin/miniforge3/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
    return future.result()
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 1961, in wrapper
    return await func(*args)
  File "<download.py>", line 217, in main
    async with backend.connect(s, HOST, PORT) as be:
  File "/Users/admin/miniforge3/lib/python3.10/contextlib.py", line 199, in __aenter__
    return await anext(self.gen)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/nintendo/nex/backend.py", line 137, in connect
    async with rmc.connect(settings, host, port, context=context) as client:
  File "/Users/admin/miniforge3/lib/python3.10/contextlib.py", line 199, in __aenter__
    return await anext(self.gen)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/nintendo/nex/rmc.py", line 286, in connect
    async with prudp.connect(settings, host, port, vport, 10, context, credentials) as client:
  File "/Users/admin/miniforge3/lib/python3.10/contextlib.py", line 199, in __aenter__
    return await anext(self.gen)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/nintendo/nex/prudp.py", line 1556, in connect
    async with connect_transport(settings, host, port, context) as transport:
  File "/Users/admin/miniforge3/lib/python3.10/contextlib.py", line 199, in __aenter__
    return await anext(self.gen)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/nintendo/nex/prudp.py", line 1533, in connect_transport
    async with connect_transport_socket(settings, host, port, context) as socket:
  File "/Users/admin/miniforge3/lib/python3.10/contextlib.py", line 199, in __aenter__
    return await anext(self.gen)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anynet/websocket.py", line 336, in connect
    async with http.connect(server, context) as client:
  File "/Users/admin/miniforge3/lib/python3.10/contextlib.py", line 199, in __aenter__
    return await anext(self.gen)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anynet/http.py", line 766, in connect
    async with tls.connect(host, port, context) as client:
  File "/Users/admin/miniforge3/lib/python3.10/contextlib.py", line 199, in __aenter__
    return await anext(self.gen)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anynet/tls.py", line 247, in connect
    async with await anyio.connect_tcp(host, port, ssl_context=context, tls_standard_compatible=False) as stream:
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anyio/_core/_sockets.py", line 234, in connect_tcp
    return await TLSStream.wrap(
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anyio/streams/tls.py", line 125, in wrap
    await wrapper._call_sslobject_method(ssl_object.do_handshake)
  File "/Users/admin/miniforge3/lib/python3.10/site-packages/anyio/streams/tls.py", line 133, in _call_sslobject_method
    result = func(*args)
  File "/Users/admin/miniforge3/lib/python3.10/ssl.py", line 975, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:997)

Additionally, I would like to inquire if anyone else has experienced a similar error recently. Any information or assistance regarding this issue would be greatly appreciated.

kinnay commented 6 months ago

I just tested one of my own scripts and did not get any errors. I would need a bit more information to help you.

Which server are you trying to connect with? Do you want to share the script that causes the error?

I actually changed something related to TLS a while ago, because the PROTOCOL_TLS constant was deprecated in Python 3.10: https://github.com/kinnay/anynet/commit/e8efe0064726cc40a81e2f2eb2286f4b55481c70. This should not cause any issues with NEX however.

pccavar commented 6 months ago

Thank you for your prompt response and willingness to help. Here is the excerpt from my script where the connection is attempted to Game Builder Garage's server:

# coding:utf-8
# https://github.com/kinnay/NintendoClients/blob/master/examples/switch/gamebuilder.py

from nintendo.switch import dauth, aauth, baas, dragons
from nintendo import switch
from nintendo.nex import backend, settings, authentication, datastore, streams, common
from anynet import http
import anyio
from anynet import tls
from pprint import pprint
import sys

class GBG:
    TITLE_ID = 0x0100FA5010788000
    LATEST_VERSION = 0x10000

    GAME_SERVER_ID = 0x2E99DD00
    ACCESS_KEY = "97b08aad"
    NEX_VERSION = 40610
    CLIENT_VERSION = 2

HOST = "g%08x-lp1.s.n.srv.nintendo.net" %GBG.GAME_SERVER_ID
PORT = 443

async def main():

    s = settings.load("switch")
    s.configure(GBG.ACCESS_KEY, GBG.NEX_VERSION, GBG.CLIENT_VERSION)

    async with backend.connect(s, HOST, PORT) as be:
        print("Connected")
        sys.exit()

anyio.run(main)

In my environment, executing this code results in an SSLV3_ALERT_HANDSHAKE_FAILURE error.

kinnay commented 6 months ago

I get the same error. One thing that's interesting is that the certificate of the GBG server is signed by Nintendo Class 2 CA - G3 instead of Amazon.

Here is a minimal reproducible example:

>>> import socket
>>> import ssl
>>> s = ssl.wrap_socket(socket.socket())
>>> s.connect(("g2e99dd00-lp1.s.n.srv.nintendo.net", 443))
Traceback (most recent call last):
  File "<pyshell#33>", line 1, in <module>
    s.connect(("g2e99dd00-lp1.s.n.srv.nintendo.net", 443))
  File "/usr/lib/python3.10/ssl.py", line 1375, in connect
    self._real_connect(addr, False)
  File "/usr/lib/python3.10/ssl.py", line 1366, in _real_connect
    self.do_handshake()
  File "/usr/lib/python3.10/ssl.py", line 1342, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLError: [SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure (_ssl.c:1007)

The issue seems to affect the GBG server specifically. If you replace the game server id by g22306d00 (the AC:NH server) then it works fine.

kinnay commented 6 months ago

My guess is that Nintendo has put a proxy in front of the GBG server, which might have an outdated TLS implementation. I found out that it works if we manually set the cipher to AES256-GCM-SHA384 on the TLS context.

I'm not sure how we can implement this in NintendoClients properly. Maybe we should add a parameter that specifies the TLS context to backend.connect.

For now, here is a workaround:


from nintendo.nex import backend, settings
from anynet import tls
import anyio
import contextlib
import pkg_resources
import ssl
import sys

class GBG:
    TITLE_ID = 0x0100FA5010788000
    LATEST_VERSION = 0x10000

    GAME_SERVER_ID = 0x2E99DD00
    ACCESS_KEY = "97b08aad"
    NEX_VERSION = 40610
    CLIENT_VERSION = 2

HOST = "g%08x-lp1.s.n.srv.nintendo.net" %GBG.GAME_SERVER_ID
PORT = 443

CA = pkg_resources.resource_filename("nintendo", "files/cert/CACERT_NINTENDO_CLASS2_CA_G3.der")

@contextlib.asynccontextmanager
async def connect_patched(host, port, context=None):
    with open(CA, "rb") as f:
        ca = f.read()

    context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
    context.load_verify_locations(cadata=ca)
    context.set_ciphers("AES256-GCM-SHA384")
    async with await anyio.connect_tcp(host, port, ssl_context=context, tls_standard_compatible=False) as stream:
        yield tls.TLSClient(stream)
tls.connect = connect_patched

async def main():
    s = settings.load("switch")
    s.configure(GBG.ACCESS_KEY, GBG.NEX_VERSION, GBG.CLIENT_VERSION)

    async with backend.connect(s, HOST, PORT) as be:
        print("Connected")

anyio.run(main)

On my machine, this works.

pccavar commented 6 months ago

Thank you so much for your invaluable help. Your provided workaround has successfully resolved the SSL handshake issue, and I greatly appreciate your quick and effective support.

To ensure that the changes made for the workaround don't affect other sessions, I have implemented a host-specific separation in the connection code. Here is the modified part of the code:

N_CA = pkg_resources.resource_filename("nintendo", "files/cert/CACERT_NINTENDO_CLASS2_CA_G3.der")

@contextlib.asynccontextmanager
async def connect_patched(host, port, context=None):
    if host != HOST:
        # Existing code for other hosts
        logger.debug("Connecting TCP/TLS client to %s:%s", host, port)
        if context:
            context = context.get(False)
        async with await anyio.connect_tcp(host, port, ssl_context=context, tls_standard_compatible=False) as stream:
            yield tls.TLSClient(stream)
    else:
        # Workaround for the GBG server
        with open(N_CA, "rb") as f:
            ca = f.read()

        context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        context.load_verify_locations(cadata=ca)
        context.set_ciphers("AES256-GCM-SHA384")
        async with await anyio.connect_tcp(host, port, ssl_context=context, tls_standard_compatible=False) as stream:
            yield tls.TLSClient(stream)

tls.connect = connect_patched