mandrewcito / signalrcore

SignalR Core python client
https://mandrewcito.github.io/signalrcore/
MIT License
119 stars 57 forks source link

Custom client cert #105

Open Toemsel opened 1 year ago

Toemsel commented 1 year ago

Is your feature request related to a problem? Please describe. Due to cert authentication, we require to attach a custom certificate for the client.

Describe the solution you'd like An option to provide a custom certificate. E.g.:

  options={
                "access_token_factory": login_function,
                "cert": cert_path # or binary, str,... whatever

Describe alternatives you've considered I took a look at the implementation details of this project. I did encounter

  1. The request does not implement client certs. StackOverflow example
  2. You do not allow to attach any CA. (SSLContext reference) You also may find a ref impl. within this StackOverflow post

Additional context Add any other context or screenshots about the feature request here.

Toemsel commented 1 year ago

Solution 1 (hacky)

I did hack something together to get the job done. However, I don't want to create a PR for that, since it is.... too hacky for the public. But if someone else has a similar requirement:

    appSrv = HubConnectionBuilder()\
        .with_url(AppSrvUrl + ':' + str(AppSrvPort) + '/' + AppSrvHub,
                          options={"verify_ssl": False,
                                "ssl": {
                                       "serverCert": pathlib.Path("C:/cert/localhost.crt"),
                                        "cert": pathlib.Path("C:/cert/localhost.crt"),
                                        "key": pathlib.Path("C:/cert/localhost.key"),
                                        "secret": ""
                                        },
                                "headers": {
                                        "Certificate": clientCertHeader
                                        }})\
        .configure_logging(logging.DEBUG)\

websocket_transport.py (replace the start method)

 def start(self):
        if not self.skip_negotiation:
            self.negotiate()

        if self.state == ConnectionState.connected:
            self.logger.warning("Already connected unable to start")
            return False

        self.state = ConnectionState.connecting
        self.logger.debug("start url:" + self.url)

        self._ws = websocket.WebSocketApp(
            self.url,
            header=self.headers,
            on_message=self.on_message,
            on_error=self.on_socket_error,
            on_close=self.on_close,
            on_open=self.on_open,
            )

        # sslOptions={"cert_reqs": ssl.CERT_NONE if self.verify_ssl else {}}
        sslOptions= { }

        # https://docs.python.org/3/library/ssl.html#ssl.VerifyMode
        if self._ssl is not None:

            context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            context.load_cert_chain(certfile=self._ssl["cert"], keyfile=self._ssl["key"])
            context.load_verify_locations(self._ssl["cert"])

            sslOptions = {
                "cert_reqs": ssl.CERT_REQUIRED,
                "ca_certs": self._ssl["serverCert"],
                "ssl_version": ssl.PROTOCOL_TLSv1_2,
                "certfile": self._ssl["cert"],
                "keyfile": self._ssl["key"],
                "password": self._ssl["secret"],
                "ssl_context": context
            }

        print(sslOptions)

        self._thread = threading.Thread(
            target=lambda: self._ws.run_forever(
                sslopt=sslOptions
            ))
        self._thread.daemon = True
        self._thread.start()
        return True

    def negotiate(self):
        negotiate_url = Helpers.get_negotiate_url(self.url)
        self.logger.debug("Negotiate url:{0}".format(negotiate_url))

        response = requests.post(
            # negotiate_url, headers=self.headers, verify=self.verify_ssl)
            negotiate_url, headers=self.headers, verify=self._ssl["serverCert"])
        self.logger.debug(
            "Response status code{0}".format(response.status_code))

        if response.status_code != 200:
            raise HubError(response.status_code)\
                if response.status_code != 401 else UnAuthorizedHubError()

        data = response.json()

        if "connectionId" in data.keys():
            self.url = Helpers.encode_connection_id(
                self.url, data["connectionId"])

        # Azure
        if 'url' in data.keys() and 'accessToken' in data.keys():
            Helpers.get_logger().debug(
                "Azure url, reformat headers, token and url {0}".format(data))
            self.url = data["url"]\
                if data["url"].startswith("ws") else\
                Helpers.http_to_websocket(data["url"])
            self.token = data["accessToken"]
            self.headers = {"Authorization": "Bearer " + self.token}

ofc you have to add the ssl parameter here and there. But that got our job done.

Solution 2 (less hacky, but still)

In addition, if you use an ASP.NET backend, you might use the following config:

services.AddCertificateForwarding(c => c.CertificateHeader = "Certificate");
...
app.UseCertificateForwarding();

In that case, you don't need to extend the library. Instead, you can make use of the optional header:

    clientCertFileHandler = open('C:/cert/localhost.crt')
    clientCert = clientCertFileHandler.read()
    clientCertAscii = clientCert.encode('ascii')
    clientCertBase64 = base64.b64encode(clientCertAscii)
    clientCertHeader = clientCertBase64.decode()
    clientCertFileHandler.close()

    print(clientCertHeader)

    appSrv = HubConnectionBuilder()\
        .with_url(AppSrvUrl + ':' + str(AppSrvPort) + '/' + AppSrvHub,
                          options={"verify_ssl": False,
                                "headers": {
                                        "Certificate": clientCertHeader
                                        }})\
        .configure_logging(logging.DEBUG)\