Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dart:io WebSocket client cannot connect to WebSocket server with pinned, self-signed certificate #34284

Open
CryptUser opened this issue Aug 28, 2018 · 23 comments
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-io P2 A bug or feature request we're likely to work on type-enhancement A request for a change that isn't a bug

Comments

@CryptUser
Copy link

I have a WebSocket server at ip address 192.168.0.11 with external port 9000, and a self-signed certificate.
The server is tested to work ok with an ios client using SocketRocket.
When I try to connect with my flutter app using

socket = await WebSocket.connect('wss://192.168.0.11:9000');

I get the following error message:
[VERBOSE-2:dart_error.cc(16)] Unhandled exception:
HandshakeException: Handshake error in client (OS Error:
CERTIFICATE_VERIFY_FAILED: ok(handshake.cc:363))
#0 _WebsocketPlaygroundHomeState.connectWebsocket (package:quano_flutter/ui/debug/websocket_playground.dart:92:14)

#1 _WebsocketPlaygroundHomeState.build. (package:quano_flutter/ui/debug/websocket_playground.dart:138:19)
#2 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:494:14)
#3 _InkResponseState.build. (package:flutter/src/material/ink_well.dart:549:30)
#4 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:102:24)
#5 TapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:161:9)
#6 TapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:94:7)
#7 PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:315:9)
#8 <…>

Note that I cannot just replace the server certificate with one signed by a CA: The reason I want to use a pinned, self-signed certificate is security. The public part of the certificate will be saved in the app, and only connections to a server with the matching certificate will be allowed, thus preventing man-in-the-middle attacks. Pinning a self-signed certificate has the advantage, that we as app developers do not need to trust any CA.

My idea then was to make the app aware of the certificate using a SecurityContext, but WebSocket.connect(...) does not seem to take a SecurityContext. Having a String "cert" that contains the certificate, I instead tried the following code that makes use of the fromUpgradedSocket constructor:

var securityContext = SecurityContext();
var bytesList = utf8.encode(cert);
var bytes = bytesList is Uint8List ? bytesList : Uint8List.fromList(bytesList);
securityContext.setTrustedCertificatesBytes(bytes);
SecureSocket secureSocket = await SecureSocket.connect('192.168.0.11', 9000, context: securityContext);
socket = WebSocket.fromUpgradedSocket(secureSocket);

However, I got the following error:

[VERBOSE-2:dart_error.cc(16)] Unhandled exception:
HandshakeException: Handshake error in client (OS Error:
CERTIFICATE_VERIFY_FAILED: ok(handshake.cc:363))
#0 _SecureFilterImpl.handshake (dart:io/runtime/binsecure_socket_patch.dart:96:51)
#1 _RawSecureSocket._secureHandshake (dart:io/secure_socket.dart:779:21)
#2 _RawSecureSocket._tryFilter. (dart:io/secure_socket.dart:900:13)
#3 _RootZone.runUnary (dart:async/zone.dart:1381:54)
#4 _FutureListener.handleValue (dart:async/future_impl.dart:129:18)
#5 Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:633:45)
#6 Future._propagateToListeners (dart:async/future_impl.dart:662:32)
#7 Future._completeWithValue (dart:async/future_impl.dart:477:5)
#8 Future._asyncComplete. (dart:async/future_impl.dart:507:7)
#9 _microtaskLoop (dart:async/schedule_microtask.dart:41:21)
#10 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5)

My version details are:
Dart VM version: 2.0.0-dev.66.0 (Fri Jun 29 11:19:05 2018 +0200) on "macos_x64"

The question is, how to connect to a WebSocket server with a self-signed certificate, and how to only allow the connection if the certificate matches the one saved in the app (certificate pinning)?

@CryptUser
Copy link
Author

I got a step further after finding the onBadCertificate parameter. Using:

SecureSocket secureSocket = await SecureSocket.connect('192.168.0.11', 9000,
onBadCertificate: (X509Certificate cert) => true);

I did no longer get an error. However, even after the line

socket = WebSocket.fromUpgradedSocket(secureSocket, serverSide: false);

I did not get any web socket connection. The server-log showed after some seconds:

dropping connection to peer tcp4:192.168.0.11:51735 with abort=True: WebSocket opening handshake timeout (peer did not finish the opening handshake in time)

Note that the web socket server is written in Python and running well with native Android code.
What else can I do?

In case it is not possible to use the WebSocket.fromUpgradedSocket(...) constructor with non-dart servers, I would suggest to add the onBadCertificate parameter directly to the Websocket.connect(...) constructor.

@zoechi
Copy link
Contributor

zoechi commented Sep 8, 2018

Similar to #31948
dart-lang/pub#1882 (comment) might help

@CryptUser
Copy link
Author

@zoechi Is that hint only aimed at command line apps?

Since I'm running flutter, I tried with
DART_VM_OPTIONS=--root-certs-file=mycertpath.pem flutter packages get
which ran without problems.

Running the app when using the constructor
socket = await WebSocket.connect('wss://192.168.0.11:9000');
still resulted in the error
[ERROR:flutter/shell/common/shell.cc(181)] Dart Error: Unhandled exception:
E/flutter ( 2929): HandshakeException: Handshake error in client (OS Error:
E/flutter ( 2929): CERTIFICATE_VERIFY_FAILED: self signed certificate(handshake.cc:363)) ...

Running with the WebSocket.fromUpgradedSocket(...) constructor gave the same result as described above.

@zoechi
Copy link
Contributor

zoechi commented Sep 8, 2018

hint only aimed at command line apps

What other kind of apps do you have in mind?
In the browser this is out of Dart's reach anyway.
This would leave Flutter. I guess there it's a bit tricky to get that environment variable set for the process.

@CryptUser
Copy link
Author

I am trying to implement an Android/ios app with flutter.
For that, the Dart Websocket class in principle works, but as far as I could find out not with a self-signed certificate.

As pointed out above, my idea was to use a SecureSocket which would allow a self-signed certificate, and then use the WebSocket.fromUpgradedSocket(...) constructor. Which I however did not get to work, as shown above.

If this strategy does not work, my question is, wouldn't it be best if the WebSocket.connect constructor would also get the additional parameter "onBadCertificate" to allow self-signed and pinned certificates?

@vsmenon vsmenon added the area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. label Sep 19, 2018
@jamespet77
Copy link

I am in the same boat as @CryptUser The platform I develop for allows users to upload and use self signed certs. It would be very beneficial to have an additional callback or param to allow selfsigned certs.

@barrylapthorn
Copy link

barrylapthorn commented Mar 18, 2019

Another 'me too' comment, I'm afraid.

Also in the same situation as @CryptUser and @linuxjet - I need to open a secure websocket Flutter in our development environment to our development server, and despite importing the certificate into the Android emulator, I see:

I/flutter ( 8460): 2019-03-18 10:38:58.729594: SEVERE:  HandshakeException: Handshake error in client (OS Error: 
I/flutter ( 8460): 	CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate(handshake.cc:352))

The WebSocket.fromUpgradedSocket(...) seem feasible, but again, I also haven't had any success with getting that working...

(edit)
...because I have to either rewrite or cut'n'paste all the http request and upgrading code from websocket_impl.dart all because I want to do something as simple as construct my own secure socket with its own security context etc. It's certainly not what I would want to ship to production code either.

Something like var ws = WebSocket.fromSecureSocket(mySecureSocket) would solve the problem.
(/edit)

@fusion44
Copy link

I'm having the same problem. In a nodejs environment it is actually pretty easy

var ws = new WebSocket('wss://127.0.0.1:8334/ws', {
  headers: {
    'Authorization': 'Basic '+new Buffer(user+':'+password).toString('base64')
  },
  cert: cert,
  ca: [cert]
});

@mzimbres
Copy link

I also have this problem. Are there any plans to fix this in flutter?

@nailgilaziev
Copy link

I do some investigation to a problem. Searching possible workarounds and trying to reimplement some part of dart:io(http) websocket_impl class on my own. This led me to understand the root of the problem:
this line from sdk/lib/_http/websocket_impl.dart

HttpClient initialized statically with default factory constructor without any access to it and ability to control this step:
static final HttpClient _httpClient = new HttpClient();

Problems:

  • Not possible to initialize it with required SecurityContext
    factory HttpClient({SecurityContext context}) { ... }
  • Not possible to specify .badCertificateCallback for HttpClient

If SDK expose this HttpClient to outside, a lot of things can be available for websocket (SSL pinning, etc)
A lot of examples @mleonhard writes here

one of the straightforward solution will be accept HttpClient like a one of the parameters in connect function:

static Future<WebSocket> connect(
  String url, 
  Iterable<String> protocols, 
  Map<String, dynamic> headers,
  {CompressionOptions compression: CompressionOptions.compressionDefault,
  HttpClient httpClient}            //<--optional parametr
) 
{...}

if param is specified - it will be used instead static field.
Code behaviour will be almost the same as current implementation but now is configurable;
Users can configure

  • SecurityContext
  • .badCertificateCallback

who thinks what?

@estevez-dev
Copy link

Hi @nailgilaziev
I'm happy to see someone want to fix this issue. I think the better way to get feedback on your solution is to create a pull request with your changes.

@sortie sortie added type-enhancement A request for a change that isn't a bug P2 A bug or feature request we're likely to work on labels Dec 9, 2019
@matheust3
Copy link

same problem here. I'm using a self-signed certificate for development and I can't connect to the server.

@jamespet77
Copy link

I finally gave up and used RawSocket. Something like this:

  bool badCert(X509Certificate cert) {
  //Do stuff here
    return false;
  }

  Future<RawSocket> connectSocket(String host, String port, bool ssl) async {
    if (ssl) {
      return RawSecureSocket.connect(host, port, onBadCertificate: badCert);
    } else {
      return RawSocket.connect(host, port);
    }
  }

@andersonmendesdev
Copy link

I still have the problem, unfortunately RawSocket does not satisfy my problem, RawSocket runs an nslookup and cannot find the server and this breaks my connection since my socket server url carries a "/ params" parameter and the websocket can handle this problem, but I need the onBadcertificate parameter.
I am waiting for a possible solution.

@jamespet77
Copy link

I see. So just connect the socket to the host and make a connection to the endpoint in socket write.

here is one of my connection functions:

  Future<RawSocket> connectSocket() async {
    if (_authObject.isSSL) {
      return RawSecureSocket.connect(_authObject.hostName, _authObject.port, onBadCertificate: badCert);
    } else {
      return RawSocket.connect(_authObject.hostName, _authObject.port);
    }
  }

void subscribe() async {
    var envelope = '''<?xml version="1.0" encoding="utf-8"?>
<s:Envelope></s:Envelope>''';  //confidential - removed

    var sb = new StringBuffer();
    sb.write("POST /services HTTP/1.1\n");
    sb.write("Host: ${_authObject.hostName} \n");
    sb.write("Content-Type: text/xml; charset=utf-8\n");
    sb.write("Authorization: Basic ${_authObject.getEncodedAuth()}\n");
    sb.write("Content-Length: ${envelope.length}");
    sb.write("\r\n");
    sb.write("\r\n");
    sb.write(envelope);
    sb.write("\r\n");

    connectSocket().then((RawSocket sock) {
      rsocket = sock;
      rsocket.listen(dataHandler, onError: errorHandler, onDone: doneHandler, cancelOnError: false);
      rsocket.write(sb
          .toString()
          .codeUnits);
    }).catchError(errorHandler);
  }

Let me know if this helps you out.

@akkolyasnikov
Copy link

I see. So just connect the socket to the host and make a connection to the endpoint in socket write.

here is one of my connection functions:

  Future<RawSocket> connectSocket() async {
    if (_authObject.isSSL) {
      return RawSecureSocket.connect(_authObject.hostName, _authObject.port, onBadCertificate: badCert);
    } else {
      return RawSocket.connect(_authObject.hostName, _authObject.port);
    }
  }

void subscribe() async {
    var envelope = '''<?xml version="1.0" encoding="utf-8"?>
<s:Envelope></s:Envelope>''';  //confidential - removed

    var sb = new StringBuffer();
    sb.write("POST /services HTTP/1.1\n");
    sb.write("Host: ${_authObject.hostName} \n");
    sb.write("Content-Type: text/xml; charset=utf-8\n");
    sb.write("Authorization: Basic ${_authObject.getEncodedAuth()}\n");
    sb.write("Content-Length: ${envelope.length}");
    sb.write("\r\n");
    sb.write("\r\n");
    sb.write(envelope);
    sb.write("\r\n");

    connectSocket().then((RawSocket sock) {
      rsocket = sock;
      rsocket.listen(dataHandler, onError: errorHandler, onDone: doneHandler, cancelOnError: false);
      rsocket.write(sb
          .toString()
          .codeUnits);
    }).catchError(errorHandler);
  }

Let me know if this helps you out.

Can you post your websocket with this code ?

@tusharsadhwani
Copy link

tusharsadhwani commented Jan 25, 2021

For my case HttpOverrides was all I needed.

class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          (X509Certificate cert, String host, int port) => true; // add your localhost detection logic here if you want
  }
}

void main() {
  HttpOverrides.global = new MyHttpOverrides();
  runApp(MyApp());
}

@rnewquist
Copy link
Contributor

I created a PR that solves the problem, @sortie anything more I need to do? I have this running on my local machine without an issue.

@lmint1
Copy link

lmint1 commented Jun 20, 2021

After two days struggling with this problem, I gave up and started using WebSocket. It's more low level, but is easy to implement and solved my problem.
Here is the code, I hope it can help you guys:

  void startListening() async {
    _socket = await WebSocket.connect("wss://localhost");
    _socket.listen(
      (event) => print('Server: $event'),
      onError: (error) => print(error),
      onDone: () => print("Done"),
    );

    await sendMessage('Hello World! 1');
    await sendMessage('Hello World! 2');
    await sendMessage('Hello World! 3');
  }

  Future<void> sendMessage(String message) async {
    print('Client: $message');
    _socket.add(message);
    await Future.delayed(Duration(seconds: 2));
  }

@satyajitghana
Copy link

This is a great temporary fix ! it works on local ip with self signed certificate. (Please modify the badCertificateCallback to your needs)

class MyHttpOverrides extends HttpOverrides {
  @override
  HttpClient createHttpClient(SecurityContext? context) {
    return super.createHttpClient(context)
      ..badCertificateCallback =
          (X509Certificate cert, String host, int port) => true; // add your localhost detection logic here if you want
  }
}

void main() {
  HttpOverrides.global = MyHttpOverrides();
  runApp(MaterialApp(home: MyApp()));
}

@erlangparasu
Copy link

After two days struggling with this problem, I gave up and started using WebSocket. It's more low level, but is easy to implement and solved my problem.
Here is the code, I hope it can help you guys:

  void startListening() async {
    _socket = await WebSocket.connect("wss://localhost");
    _socket.listen(
      (event) => print('Server: $event'),
      onError: (error) => print(error),
      onDone: () => print("Done"),
    );

    await sendMessage('Hello World! 1');
    await sendMessage('Hello World! 2');
    await sendMessage('Hello World! 3');
  }

  Future<void> sendMessage(String message) async {
    print('Client: $message');
    _socket.add(message);
    await Future.delayed(Duration(seconds: 2));
  }

Thanks @lmint1 !

dart-bot pushed a commit that referenced this issue Sep 15, 2021
… custom HTTP Client for web socket connections.

The WebSocket abstract class was changed to allow an optional parameter called customClient that takes in a HTTPClient and passes it to the WebSocket Implementation.
The WebSocket implementation takes the customClient, checks if its null, if its not null, it uses the customClient in place of the static HTTPClient that the WebSocket Implementation offers.
This custom client does not override the static HTTPClient, so all previous functionality remains the same when the customClient is not present.

TEST=testStaticClientUserAgentStaysTheSame() in web_socket_test.dart in standalone_2/standalone
TEST=new SecurityConfiguration(secure: true).runTests(); in web_socket_error_test.dart and web_socket_test.dart in standalone_2/standalone

Bug: #34284

Closes #46040
#46040

GitOrigin-RevId: 58fed38
Change-Id: I042b1e3fa7a4effed076c0deeec1f86af0dfe26d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/200262
Reviewed-by: Alexander Aprelev <aam@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
Commit-Queue: Alexander Aprelev <aam@google.com>
@rnewquist
Copy link
Contributor

Just letting you all know, this issue has been fixed, within WebSocket.connect() you can add a custom HTTP Client to it now.

@TheGlorySaint
Copy link

Is there any Update on this Issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. library-io P2 A bug or feature request we're likely to work on type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests