dart:io | Support connection renegotiation (rehandshake) on SecureSocket.

BUG=dartbug.com/11384
R=ajohnsen@google.com

Review URL: https://codereview.chromium.org//18984008

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@24916 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/runtime/bin/io_natives.cc b/runtime/bin/io_natives.cc
index c2a3a96..97b3ad7 100644
--- a/runtime/bin/io_natives.cc
+++ b/runtime/bin/io_natives.cc
@@ -48,6 +48,7 @@
   V(SecureSocket_PeerCertificate, 1)                                           \
   V(SecureSocket_RegisterBadCertificateCallback, 2)                            \
   V(SecureSocket_RegisterHandshakeCompleteCallback, 2)                         \
+  V(SecureSocket_Renegotiate, 4)                                               \
   V(SecureSocket_InitializeLibrary, 3)                                         \
   V(SecureSocket_NewServicePort, 0)                                            \
   V(SecureSocket_FilterPointer, 1)                                             \
diff --git a/runtime/bin/secure_socket.cc b/runtime/bin/secure_socket.cc
index d308b3b..7b5cd37 100644
--- a/runtime/bin/secure_socket.cc
+++ b/runtime/bin/secure_socket.cc
@@ -189,6 +189,21 @@
 }
 
 
+void FUNCTION_NAME(SecureSocket_Renegotiate)(Dart_NativeArguments args) {
+  Dart_EnterScope();
+  bool use_session_cache =
+      DartUtils::GetBooleanValue(Dart_GetNativeArgument(args, 1));
+  bool request_client_certificate =
+      DartUtils::GetBooleanValue(Dart_GetNativeArgument(args, 2));
+  bool require_client_certificate =
+      DartUtils::GetBooleanValue(Dart_GetNativeArgument(args, 3));
+  GetFilter(args)->Renegotiate(use_session_cache,
+                               request_client_certificate,
+                               require_client_certificate);
+  Dart_ExitScope();
+}
+
+
 void FUNCTION_NAME(SecureSocket_RegisterHandshakeCompleteCallback)(
     Dart_NativeArguments args) {
   Dart_EnterScope();
@@ -732,8 +747,9 @@
         ThrowPRException("TlsException",
                          "Failed SSL_OptionSet(REQUEST_CERTIFICATE) call");
       }
-      PRBool require_cert = require_client_certificate ? PR_TRUE : PR_FALSE;
-      status = SSL_OptionSet(filter_, SSL_REQUIRE_CERTIFICATE, require_cert);
+      status = SSL_OptionSet(filter_,
+                             SSL_REQUIRE_CERTIFICATE,
+                             require_client_certificate);
       if (status != SECSuccess) {
         ThrowPRException("TlsException",
                          "Failed SSL_OptionSet(REQUIRE_CERTIFICATE) call");
@@ -772,8 +788,7 @@
                            BadCertificateCallback,
                            static_cast<void*>(this));
 
-  PRBool as_server = is_server ? PR_TRUE : PR_FALSE;
-  status = SSL_ResetHandshake(filter_, as_server);
+  status = SSL_ResetHandshake(filter_, is_server);
   if (status != SECSuccess) {
     ThrowPRException("TlsException",
                      "Failed SSL_ResetHandshake call");
@@ -827,6 +842,43 @@
 }
 
 
+void SSLFilter::Renegotiate(bool use_session_cache,
+                            bool request_client_certificate,
+                            bool require_client_certificate) {
+  SECStatus status;
+  // The SSL_REQUIRE_CERTIFICATE option only takes effect if the
+  // SSL_REQUEST_CERTIFICATE option is also set, so set it.
+  request_client_certificate =
+      request_client_certificate || require_client_certificate;
+
+  status = SSL_OptionSet(filter_,
+                         SSL_REQUEST_CERTIFICATE,
+                         request_client_certificate);
+  if (status != SECSuccess) {
+    ThrowPRException("TlsException",
+       "Failure in (Raw)SecureSocket.renegotiate request_client_certificate");
+  }
+  status = SSL_OptionSet(filter_,
+                         SSL_REQUIRE_CERTIFICATE,
+                         require_client_certificate);
+  if (status != SECSuccess) {
+    ThrowPRException("TlsException",
+       "Failure in (Raw)SecureSocket.renegotiate require_client_certificate");
+  }
+  bool flush_cache = !use_session_cache;
+  status = SSL_ReHandshake(filter_, flush_cache);
+  if (status != SECSuccess) {
+    if (is_server_) {
+      ThrowPRException("HandshakeException",
+                       "Failure in (Raw)SecureSocket.renegotiate in server");
+    } else {
+      ThrowPRException("HandshakeException",
+                       "Failure in (Raw)SecureSocket.renegotiate in client");
+    }
+  }
+}
+
+
 void SSLFilter::Destroy() {
   for (int i = 0; i < kNumBuffers; ++i) {
     Dart_DeletePersistentHandle(dart_buffer_objects_[i]);
diff --git a/runtime/bin/secure_socket.h b/runtime/bin/secure_socket.h
index ce011b6..817be4a 100644
--- a/runtime/bin/secure_socket.h
+++ b/runtime/bin/secure_socket.h
@@ -66,6 +66,9 @@
                bool send_client_certificate);
   void Destroy();
   void Handshake();
+  void Renegotiate(bool use_session_cache,
+                   bool request_client_certificate,
+                   bool require_client_certificate);
   void RegisterHandshakeCompleteCallback(Dart_Handle handshake_complete);
   void RegisterBadCertificateCallback(Dart_Handle callback);
   Dart_Handle bad_certificate_callback() {
diff --git a/runtime/bin/secure_socket_patch.dart b/runtime/bin/secure_socket_patch.dart
index 4b7c5ab..104f562 100644
--- a/runtime/bin/secure_socket_patch.dart
+++ b/runtime/bin/secure_socket_patch.dart
@@ -31,6 +31,14 @@
     _raw.onBadCertificate = callback;
   }
 
+  void renegotiate({bool useSessionCache: true,
+                    bool requestClientCertificate: false,
+                    bool requireClientCertificate: false}) {
+    _raw.renegotiate(useSessionCache: useSessionCache,
+                     requestClientCertificate: requestClientCertificate,
+                     requireClientCertificate: requireClientCertificate);
+  }
+
   X509Certificate get peerCertificate {
     if (_raw == null) {
      throw new StateError("peerCertificate called on destroyed SecureSocket");
@@ -84,6 +92,10 @@
 
   void handshake() native "SecureSocket_Handshake";
 
+  void renegotiate(bool useSessionCache,
+                   bool requestClientCertificate,
+                   bool requireClientCertificate)
+      native "SecureSocket_Renegotiate";
   void init() native "SecureSocket_Init";
 
   X509Certificate get peerCertificate native "SecureSocket_PeerCertificate";
diff --git a/sdk/lib/io/secure_socket.dart b/sdk/lib/io/secure_socket.dart
index 417534a..6da8ec6 100644
--- a/sdk/lib/io/secure_socket.dart
+++ b/sdk/lib/io/secure_socket.dart
@@ -150,6 +150,17 @@
   X509Certificate get peerCertificate;
 
   /**
+   * Renegotiate an existing secure connection, renewing the session keys
+   * and possibly changing the connection properties.
+   *
+   * This repeats the SSL or TLS handshake, with options that allow clearing
+   * the session cache and requesting a client certificate.
+   */
+  void renegotiate({bool useSessionCache: true,
+                    bool requestClientCertificate: false,
+                    bool requireClientCertificate: false});
+
+  /**
    * Initializes the NSS library. If [initialize] is not called, the library
    * is automatically initialized as if [initialize] were called with no
    * arguments. If [initialize] is called more than once, or called after
@@ -334,6 +345,17 @@
   }
 
   /**
+   * Renegotiate an existing secure connection, renewing the session keys
+   * and possibly changing the connection properties.
+   *
+   * This repeats the SSL or TLS handshake, with options that allow clearing
+   * the session cache and requesting a client certificate.
+   */
+  void renegotiate({bool useSessionCache: true,
+                    bool requestClientCertificate: false,
+                    bool requireClientCertificate: false});
+
+  /**
    * Get the peer certificate for a connected RawSecureSocket.  If this
    * RawSecureSocket is the server end of a secure socket connection,
    * [peerCertificate] will return the client certificate, or null, if no
@@ -785,6 +807,21 @@
     }
   }
 
+  void renegotiate({bool useSessionCache: true,
+                    bool requestClientCertificate: false,
+                    bool requireClientCertificate: false}) {
+    if (_status != CONNECTED) {
+      throw new HandshakeException(
+          "Called renegotiate on a non-connected socket");
+    }
+    _secureFilter.renegotiate(useSessionCache,
+                              requestClientCertificate,
+                              requireClientCertificate);
+    _status = HANDSHAKE;
+    _filterStatus.writeEmpty = false;
+    _scheduleFilter();
+  }
+
   void _secureHandshakeCompleteHandler() {
     _status = CONNECTED;
     if (_connectPending) {
@@ -1158,6 +1195,10 @@
                bool sendClientCertificate);
   void destroy();
   void handshake();
+  void rehandshake();
+  void renegotiate(bool useSessionCache,
+                   bool requestClientCertificate,
+                   bool requireClientCertificate);
   void init();
   X509Certificate get peerCertificate;
   int processBuffer(int bufferIndex);
diff --git a/tests/standalone/io/secure_socket_renegotiate_client.dart b/tests/standalone/io/secure_socket_renegotiate_client.dart
new file mode 100644
index 0000000..9ac2192
--- /dev/null
+++ b/tests/standalone/io/secure_socket_renegotiate_client.dart
@@ -0,0 +1,79 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Client for secure_socket_renegotiate_test, that runs in a subprocess.
+// The test verifies that client certificates work, if the client and server
+// are in separate processes, and that connection renegotiation can request
+// a client certificate to be sent.
+
+import "dart:async";
+import "dart:io";
+
+const HOST_NAME = "localhost";
+const CERTIFICATE = "localhost_cert";
+
+
+class ExpectException implements Exception {
+  ExpectException(this.message);
+  String toString() => message;
+  String message;
+}
+
+
+void expectEquals(expected, actual) {
+  if (actual != expected) {
+    throw new ExpectException('Expected $expected, found $actual');
+  }
+}
+
+
+void expect(condition) {
+  if (!condition) {
+    throw new ExpectException('');
+  }
+}
+
+
+void runClient(int port) {
+  SecureSocket.connect(HOST_NAME, port, sendClientCertificate: true)
+    .then((SecureSocket socket) {
+      X509Certificate certificate = socket.peerCertificate;
+      expect(certificate != null);
+      expectEquals('CN=localhost', certificate.subject);
+      expectEquals('CN=myauthority', certificate.issuer);
+      StreamIterator<String> input = new StreamIterator(socket
+          .transform(new StringDecoder())
+          .transform(new LineTransformer()));
+      socket.writeln('first');
+      input.moveNext()
+        .then((success) {
+          expect(success);
+          expectEquals('first reply', input.current);
+          socket.renegotiate();
+          socket.writeln('renegotiated');
+          return input.moveNext();
+        })
+        .then((success) {
+          expect(success);
+          expectEquals('server renegotiated', input.current);
+          X509Certificate certificate = socket.peerCertificate;
+          expect(certificate != null);
+          expectEquals("CN=localhost", certificate.subject);
+          expectEquals("CN=myauthority", certificate.issuer);
+          socket.writeln('second');
+          return input.moveNext();
+        })
+        .then((success) {
+          expect(success != true);
+          socket.close();
+        });
+    });
+}
+
+
+void main() {
+  final args = new Options().arguments;
+  SecureSocket.initialize(database: args[1], password: 'dartdart');
+  runClient(int.parse(args[0]));
+}
diff --git a/tests/standalone/io/secure_socket_renegotiate_test.dart b/tests/standalone/io/secure_socket_renegotiate_test.dart
new file mode 100644
index 0000000..49c1644
--- /dev/null
+++ b/tests/standalone/io/secure_socket_renegotiate_test.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// This test verifies that client certificates work, if the client and server
+// are in separate processes, and that connection renegotiation works, and
+// can request a client certificate to be sent.
+
+import "package:expect/expect.dart";
+import "package:pathos/path.dart" as path;
+import "dart:async";
+import "dart:io";
+
+const HOST_NAME = "localhost";
+const CERTIFICATE = "localhost_cert";
+
+
+String certificateDatabase() =>
+    path.join(path.dirname(new Options().script), 'pkcert', '');
+
+
+Future<SecureServerSocket> runServer() {
+  SecureSocket.initialize(database: certificateDatabase(),
+      password: 'dartdart');
+
+  return SecureServerSocket.bind(HOST_NAME, 0, CERTIFICATE)
+    .then((SecureServerSocket server) {
+      server.listen((SecureSocket socket) {
+        Expect.isNull(socket.peerCertificate);
+
+        StreamIterator<String> input =
+            new StreamIterator(socket.transform(new StringDecoder())
+                                     .transform(new LineTransformer()));
+        input.moveNext().then((success) {
+          Expect.isTrue(success);
+          Expect.equals('first', input.current);
+          socket.writeln('first reply');
+          return input.moveNext();
+        }).then((success) {
+          Expect.isTrue(success);
+          Expect.equals('renegotiated', input.current);
+          Expect.isNull(socket.peerCertificate);
+          socket.renegotiate(requestClientCertificate: true,
+                             requireClientCertificate: true,
+                             useSessionCache: false);
+          socket.writeln('server renegotiated');
+          return input.moveNext();
+        }).then((success) {
+          Expect.isTrue(success);
+          Expect.equals('second', input.current);
+          X509Certificate certificate = socket.peerCertificate;
+          Expect.isNotNull(certificate);
+          Expect.equals("CN=localhost", certificate.subject);
+          Expect.equals("CN=myauthority", certificate.issuer);
+          server.close();
+          socket.close();
+        });
+      });
+      return server;
+    });
+}
+
+
+void main() {
+  runServer()
+    .then((SecureServerSocket server) {
+      final options = new Options();
+      var clientScript =
+          options.script.replaceFirst("_test.dart", "_client.dart");
+      Expect.isTrue(clientScript.endsWith("_client.dart"));
+      Process.run(options.executable,
+                  [clientScript,
+                   server.port.toString(),
+                   certificateDatabase()])
+        .then((ProcessResult result) {
+          if (result.exitCode != 0) {
+            print("Client failed, stdout:");
+            print(result.stdout);
+            print("  stderr:");
+            print(result.stderr);
+            Expect.fail('Client subprocess exit code: ${result.exitCode}');
+          }
+        });
+    });
+}