dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.3k stars 1.59k forks source link

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

Open CryptUser opened 6 years ago

CryptUser commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

Similar to #31948 https://github.com/dart-lang/pub/issues/1882#issuecomment-415588527 might help

CryptUser commented 6 years ago

@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 commented 6 years ago

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 commented 6 years ago

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?

jamespet77 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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

nailgilaziev commented 4 years ago

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:

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

who thinks what?

estevez-dev commented 4 years ago

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.

matheust3 commented 4 years ago

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

jamespet77 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 !

rnewquist commented 2 years ago

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

TheGlorySaint commented 1 year ago

Is there any Update on this Issue?