shill: vpn: Implement NSS database interface class.

Use the NSS class to lookup NSS certificates for OpenVPN.

BUG=chromium-os:28792
TEST=unit tests

Change-Id: I2e0c7924d664f375f5b01bc73506e2b91e6f8720
Reviewed-on: https://gerrit.chromium.org/gerrit/19457
Tested-by: Darin Petkov <petkov@chromium.org>
Reviewed-by: Paul Stewart <pstew@chromium.org>
Commit-Ready: Darin Petkov <petkov@chromium.org>
diff --git a/Makefile b/Makefile
index 95018ab..df43445 100644
--- a/Makefile
+++ b/Makefile
@@ -161,6 +161,7 @@
 	modem_manager_proxy.o \
 	modem_proxy.o \
 	modem_simple_proxy.o \
+	nss.o \
 	openvpn_driver.o \
 	openvpn_management_server.o \
 	portal_detector.o \
@@ -262,6 +263,7 @@
 	mock_modem_manager_proxy.o \
 	mock_modem_proxy.o \
 	mock_modem_simple_proxy.o \
+	mock_nss.o \
 	mock_openvpn_driver.o \
 	mock_openvpn_management_server.o \
 	mock_portal_detector.o \
@@ -289,6 +291,7 @@
 	modem_manager_unittest.o \
 	modem_unittest.o \
 	nice_mock_control.o \
+	nss_unittest.o \
 	openvpn_driver_unittest.o \
 	openvpn_management_server_unittest.o \
 	portal_detector_unittest.o \
diff --git a/glib.cc b/glib.cc
index a2e9e07..a462ac7 100644
--- a/glib.cc
+++ b/glib.cc
@@ -224,6 +224,28 @@
   g_spawn_close_pid(pid);
 }
 
+gboolean GLib::SpawnSync(const gchar *working_directory,
+                         gchar **argv,
+                         gchar **envp,
+                         GSpawnFlags flags,
+                         GSpawnChildSetupFunc child_setup,
+                         gpointer user_data,
+                         gchar **standard_output,
+                         gchar **standard_error,
+                         gint *exit_status,
+                         GError **error) {
+  return g_spawn_sync(working_directory,
+                      argv,
+                      envp,
+                      flags,
+                      child_setup,
+                      user_data,
+                      standard_output,
+                      standard_error,
+                      exit_status,
+                      error);
+}
+
 void GLib::Strfreev(gchar **str_array) {
   g_strfreev(str_array);
 }
diff --git a/glib.h b/glib.h
index 6f75048..079b244 100644
--- a/glib.h
+++ b/glib.h
@@ -145,6 +145,17 @@
                                           GError **error);
   // g_spawn_close_pid
   virtual void SpawnClosePID(GPid pid);
+  // g_spawn_sync
+  virtual gboolean SpawnSync(const gchar *working_directory,
+                             gchar **argv,
+                             gchar **envp,
+                             GSpawnFlags flags,
+                             GSpawnChildSetupFunc child_setup,
+                             gpointer user_data,
+                             gchar **standard_output,
+                             gchar **standard_error,
+                             gint *exit_status,
+                             GError **error);
   // g_strfreev
   virtual void Strfreev(gchar **str_array);
   // g_type_init
diff --git a/mock_glib.h b/mock_glib.h
index 60e3ff5..b9b10e9 100644
--- a/mock_glib.h
+++ b/mock_glib.h
@@ -116,6 +116,16 @@
                          gint *standard_error,
                          GError **error));
   MOCK_METHOD1(SpawnClosePID, void(GPid pid));
+  MOCK_METHOD10(SpawnSync, gboolean(const gchar *working_directory,
+                                    gchar **argv,
+                                    gchar **envp,
+                                    GSpawnFlags flags,
+                                    GSpawnChildSetupFunc child_setup,
+                                    gpointer user_data,
+                                    gchar **standard_output,
+                                    gchar **standard_error,
+                                    gint *exit_status,
+                                    GError **error));
   MOCK_METHOD1(Strfreev, void(gchar **str_array));
   MOCK_METHOD0(TypeInit, void());
 
diff --git a/mock_nss.cc b/mock_nss.cc
new file mode 100644
index 0000000..78d5603
--- /dev/null
+++ b/mock_nss.cc
@@ -0,0 +1,13 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shill/mock_nss.h"
+
+namespace shill {
+
+MockNSS::MockNSS() {}
+
+MockNSS::~MockNSS() {}
+
+}  // namespace shill
diff --git a/mock_nss.h b/mock_nss.h
new file mode 100644
index 0000000..f1b3049
--- /dev/null
+++ b/mock_nss.h
@@ -0,0 +1,30 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SHILL_MOCK_NSS_
+#define SHILL_MOCK_NSS_
+
+#include <gmock/gmock.h>
+
+#include "shill/nss.h"
+
+namespace shill {
+
+class MockNSS : public NSS {
+ public:
+  MockNSS();
+  virtual ~MockNSS();
+
+  MOCK_METHOD2(GetPEMCertfile, FilePath(const std::string &nickname,
+                                        const std::vector<char> &id));
+  MOCK_METHOD2(GetDERCertfile, FilePath(const std::string &nickname,
+                                        const std::vector<char> &id));
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(MockNSS);
+};
+
+}  // namespace shill
+
+#endif  // SHILL_MOCK_NSS_
diff --git a/nss.cc b/nss.cc
new file mode 100644
index 0000000..2f370ca
--- /dev/null
+++ b/nss.cc
@@ -0,0 +1,91 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shill/nss.h"
+
+#include <base/logging.h>
+#include <base/string_number_conversions.h>
+#include <base/string_util.h>
+#include <base/stringprintf.h>
+
+#include "shill/glib.h"
+
+using base::HexEncode;
+using base::StringPrintf;
+using std::string;
+using std::vector;
+
+namespace shill {
+
+namespace {
+const char kCertfileBasename[] = "/tmp/nss-cert.";
+const char kNSSGetCertScript[] = SCRIPTDIR "/nss-get-cert";
+}  // namespace
+
+// TODO(ers): not using LAZY_INSTANCE_INITIALIZER
+// because of http://crbug.com/114828
+static base::LazyInstance<NSS> g_nss = { 0, {{0}} };
+
+NSS::NSS()
+    : glib_(NULL) {
+  VLOG(2) << __func__;
+}
+
+NSS::~NSS() {
+  VLOG(2) << __func__;
+}
+
+// static
+NSS *NSS::GetInstance() {
+  return g_nss.Pointer();
+}
+
+void NSS::Init(GLib *glib) {
+  glib_ = glib;
+}
+
+FilePath NSS::GetPEMCertfile(const string &nickname, const vector<char> &id) {
+  return GetCertfile(nickname, id, "pem");
+}
+
+FilePath NSS::GetDERCertfile(const string &nickname, const vector<char> &id) {
+  return GetCertfile(nickname, id, "der");
+}
+
+FilePath NSS::GetCertfile(
+    const string &nickname, const vector<char> &id, const string &type) {
+  CHECK(glib_);
+  string filename =
+      kCertfileBasename + StringToLowerASCII(HexEncode(&id[0], id.size()));
+  char *argv[] = {
+    const_cast<char *>(kNSSGetCertScript),
+    const_cast<char *>(nickname.c_str()),
+    const_cast<char *>(type.c_str()),
+    const_cast<char *>(filename.c_str()),
+    NULL
+  };
+  int status = 0;
+  GError *error = NULL;
+  if (!glib_->SpawnSync(NULL,
+                        argv,
+                        NULL,
+                        static_cast<GSpawnFlags>(0),
+                        NULL,
+                        NULL,
+                        NULL,
+                        NULL,
+                        &status,
+                        &error)) {
+    LOG(ERROR) << "nss-get-cert failed: "
+               << glib_->ConvertErrorToMessage(error);
+    return FilePath();
+  }
+  if (!WIFEXITED(status) || WEXITSTATUS(status)) {
+    LOG(ERROR) << "nss-get-cert failed, status=" << status;
+    return FilePath();
+  }
+  return FilePath(filename);
+}
+
+}  // namespace shill
diff --git a/nss.h b/nss.h
new file mode 100644
index 0000000..4c02321
--- /dev/null
+++ b/nss.h
@@ -0,0 +1,55 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SHILL_NSS_
+#define SHILL_NSS_
+
+#include <string>
+#include <vector>
+
+#include <base/file_path.h>
+#include <base/lazy_instance.h>
+#include <gtest/gtest_prod.h>  // for FRIEND_TEST
+
+namespace shill {
+
+class GLib;
+
+class NSS {
+ public:
+  virtual ~NSS();
+
+  // This is a singleton -- use NSS::GetInstance()->Foo()
+  static NSS *GetInstance();
+
+  void Init(GLib *glib);
+
+  // Returns an empty path on failure.
+  virtual FilePath GetPEMCertfile(const std::string &nickname,
+                                  const std::vector<char> &id);
+
+  // Returns an empty path on failure.
+  virtual FilePath GetDERCertfile(const std::string &nickname,
+                                  const std::vector<char> &id);
+
+ protected:
+  NSS();
+
+ private:
+  friend struct base::DefaultLazyInstanceTraits<NSS>;
+  friend class NSSTest;
+  FRIEND_TEST(NSSTest, GetCertfile);
+
+  FilePath GetCertfile(const std::string &nickname,
+                       const std::vector<char> &id,
+                       const std::string &type);
+
+  GLib *glib_;
+
+  DISALLOW_COPY_AND_ASSIGN(NSS);
+};
+
+}  // namespace shill
+
+#endif  // SHILL_NSS_
diff --git a/nss_unittest.cc b/nss_unittest.cc
new file mode 100644
index 0000000..57163f7
--- /dev/null
+++ b/nss_unittest.cc
@@ -0,0 +1,72 @@
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "shill/nss.h"
+
+#include <gtest/gtest.h>
+
+#include "shill/mock_glib.h"
+
+using std::vector;
+using testing::_;
+using testing::Return;
+using testing::SetArgumentPointee;
+
+namespace shill {
+
+class NSSTest : public testing::Test {
+ public:
+  NSSTest() : nss_(NSS::GetInstance()) {
+    nss_->glib_ = &glib_;
+    test_id_.push_back(0x1a);
+    test_id_.push_back(0x2b);
+  }
+
+ protected:
+  vector<char> test_id_;
+  MockGLib glib_;
+  NSS *nss_;
+};
+
+namespace {
+MATCHER_P(GetCertfileArgv, type, "") {
+  if (!arg || !arg[0] || !arg[1] || !arg[2] || !arg[3] || arg[4]) {
+    return false;
+  }
+  if (strcmp(type, arg[2])) {
+    return false;
+  }
+  if (strcmp(arg[3], "/tmp/nss-cert.1a2b")) {
+    return false;
+  }
+  return true;
+}
+}  // namespace
+
+TEST_F(NSSTest, GetCertfile) {
+  EXPECT_CALL(glib_,
+              SpawnSync(_, GetCertfileArgv("pem"), _, _, _, _, _, _, _, _))
+      .WillOnce(Return(false))
+      .WillOnce(DoAll(SetArgumentPointee<8>(1), Return(true)))
+      .WillOnce(DoAll(SetArgumentPointee<8>(0), Return(true)));
+  EXPECT_TRUE(nss_->GetCertfile("foo", test_id_, "pem").empty());
+  EXPECT_TRUE(nss_->GetCertfile("foo", test_id_, "pem").empty());
+  EXPECT_FALSE(nss_->GetCertfile("foo", test_id_, "pem").empty());
+}
+
+TEST_F(NSSTest, GetPEMCertfile) {
+  EXPECT_CALL(glib_,
+              SpawnSync(_, GetCertfileArgv("pem"), _, _, _, _, _, _, _, _))
+      .WillOnce(DoAll(SetArgumentPointee<8>(0), Return(true)));
+  EXPECT_FALSE(nss_->GetPEMCertfile("foo", test_id_).empty());
+}
+
+TEST_F(NSSTest, GetDERCertfile) {
+  EXPECT_CALL(glib_,
+              SpawnSync(_, GetCertfileArgv("der"), _, _, _, _, _, _, _, _))
+      .WillOnce(DoAll(SetArgumentPointee<8>(0), Return(true)));
+  EXPECT_FALSE(nss_->GetDERCertfile("foo", test_id_).empty());
+}
+
+}  // namespace shill
diff --git a/openvpn_driver.cc b/openvpn_driver.cc
index 6373d87..a012c22 100644
--- a/openvpn_driver.cc
+++ b/openvpn_driver.cc
@@ -18,6 +18,7 @@
 #include "shill/dhcp_config.h"
 #include "shill/error.h"
 #include "shill/manager.h"
+#include "shill/nss.h"
 #include "shill/openvpn_management_server.h"
 #include "shill/property_accessor.h"
 #include "shill/rpc_task.h"
@@ -117,6 +118,7 @@
       glib_(glib),
       args_(args),
       management_server_(new OpenVPNManagementServer(this, glib)),
+      nss_(NSS::GetInstance()),
       pid_(0),
       child_watch_tag_(0) {}
 
@@ -441,9 +443,24 @@
   AppendValueOption(flimflam::kOpenVPNServerPollTimeoutProperty,
                     "--server-poll-timeout", options);
 
-  // TODO(petkov): Implement this.
-  LOG_IF(ERROR, args_.ContainsString(flimflam::kOpenVPNCaCertNSSProperty))
-      << "Support for NSS CA not implemented yet.";
+  string ca_cert_nss =
+      args_.LookupString(flimflam::kOpenVPNCaCertNSSProperty, "");
+  if (!ca_cert_nss.empty()) {
+    if (!args_.LookupString(flimflam::kOpenVPNCaCertProperty, "").empty()) {
+      Error::PopulateAndLog(error,
+                            Error::kInvalidArguments,
+                            "Can't specify both CACert and CACertNSS.");
+      return;
+    }
+    vector<char> id(vpnhost.begin(), vpnhost.end());
+    FilePath certfile = nss_->GetPEMCertfile(ca_cert_nss, id);
+    if (certfile.empty()) {
+      LOG(ERROR) << "Unable to extract certificate: " << ca_cert_nss;
+    } else {
+      options->push_back("--ca");
+      options->push_back(certfile.value());
+    }
+  }
 
   // Client-side ping support.
   AppendValueOption(kOpenVPNPingProperty, "--ping", options);
diff --git a/openvpn_driver.h b/openvpn_driver.h
index 74a156f..7d657de 100644
--- a/openvpn_driver.h
+++ b/openvpn_driver.h
@@ -30,6 +30,7 @@
 class EventDispatcher;
 class Manager;
 class Metrics;
+class NSS;
 class OpenVPNManagementServer;
 
 class OpenVPNDriver : public VPNDriver,
@@ -157,6 +158,7 @@
   KeyValueStore args_;
   Sockets sockets_;
   scoped_ptr<OpenVPNManagementServer> management_server_;
+  NSS *nss_;
 
   VPNServiceRefPtr service_;
   scoped_ptr<RPCTask> rpc_task_;
diff --git a/openvpn_driver_unittest.cc b/openvpn_driver_unittest.cc
index 15a7ed0..30f854a 100644
--- a/openvpn_driver_unittest.cc
+++ b/openvpn_driver_unittest.cc
@@ -21,6 +21,7 @@
 #include "shill/mock_glib.h"
 #include "shill/mock_manager.h"
 #include "shill/mock_metrics.h"
+#include "shill/mock_nss.h"
 #include "shill/mock_openvpn_management_server.h"
 #include "shill/mock_service.h"
 #include "shill/mock_store.h"
@@ -37,6 +38,7 @@
 using testing::_;
 using testing::AnyNumber;
 using testing::DoAll;
+using testing::ElementsAreArray;
 using testing::Ne;
 using testing::NiceMock;
 using testing::Return;
@@ -59,6 +61,7 @@
                             kInterfaceName, kInterfaceIndex)),
         management_server_(new NiceMock<MockOpenVPNManagementServer>()) {
     driver_->management_server_.reset(management_server_);
+    driver_->nss_ = &nss_;
   }
 
   virtual ~OpenVPNDriverTest() {}
@@ -116,6 +119,7 @@
   OpenVPNDriver *driver_;  // Owned by |service_|.
   scoped_refptr<MockVPNService> service_;
   scoped_refptr<MockVPN> device_;
+  MockNSS nss_;
 
   // Owned by |driver_|.
   NiceMock<MockOpenVPNManagementServer> *management_server_;
@@ -353,19 +357,29 @@
 TEST_F(OpenVPNDriverTest, InitOptions) {
   static const char kHost[] = "192.168.2.254";
   static const char kTLSAuthContents[] = "SOME-RANDOM-CONTENTS\n";
+  static const char kCaCertNSS[] = "{1234}";
+  static const char kNSSCertfile[] = "/tmp/nss-cert";
+  FilePath nss_cert(kNSSCertfile);
   args_.SetString(flimflam::kProviderHostProperty, kHost);
   args_.SetString(flimflam::kOpenVPNTLSAuthContentsProperty, kTLSAuthContents);
+  args_.SetString(flimflam::kOpenVPNCaCertNSSProperty, kCaCertNSS);
   SetArgs();
   driver_->rpc_task_.reset(new RPCTask(&control_, this));
   driver_->tunnel_interface_ = kInterfaceName;
   EXPECT_CALL(*management_server_, Start(&dispatcher_, &driver_->sockets_, _))
       .WillOnce(Return(false))
       .WillOnce(Return(true));
+  EXPECT_CALL(nss_,
+              GetPEMCertfile(kCaCertNSS,
+                             ElementsAreArray(kHost, arraysize(kHost) - 1)))
+      .Times(2)
+      .WillRepeatedly(Return(nss_cert));
   {
     Error error;
     vector<string> options;
     driver_->InitOptions(&options, &error);
-    EXPECT_FALSE(error.IsSuccess());
+    EXPECT_EQ(Error::kInternalError, error.type());
+    EXPECT_EQ("Unable to setup management channel.", error.message());
   }
   {
     Error error;
@@ -384,6 +398,7 @@
     EXPECT_TRUE(
         file_util::ReadFileToString(driver_->tls_auth_file_, &contents));
     EXPECT_EQ(kTLSAuthContents, contents);
+    ExpectInFlags(options, "--ca", kNSSCertfile);
   }
 }
 
diff --git a/shill_daemon.cc b/shill_daemon.cc
index d4782be..5549327 100644
--- a/shill_daemon.cc
+++ b/shill_daemon.cc
@@ -14,6 +14,7 @@
 
 #include "shill/dhcp_provider.h"
 #include "shill/error.h"
+#include "shill/nss.h"
 #include "shill/proxy_factory.h"
 #include "shill/routing_table.h"
 #include "shill/rtnl_handler.h"
@@ -27,6 +28,7 @@
 Daemon::Daemon(Config *config, ControlInterface *control)
     : config_(config),
       control_(control),
+      nss_(NSS::GetInstance()),
       proxy_factory_(ProxyFactory::GetInstance()),
       rtnl_handler_(RTNLHandler::GetInstance()),
       routing_table_(RoutingTable::GetInstance()),
@@ -64,6 +66,7 @@
 
 void Daemon::Start() {
   glib_.TypeInit();
+  nss_->Init(&glib_);
   proxy_factory_->Init();
   rtnl_handler_->Start(&dispatcher_, &sockets_);
   routing_table_->Start();
diff --git a/shill_daemon.h b/shill_daemon.h
index 76737bc..48a35ed 100644
--- a/shill_daemon.h
+++ b/shill_daemon.h
@@ -21,6 +21,7 @@
 class ControlInterface;
 class DHCPProvider;
 class GLib;
+class NSS;
 class ProxyFactory;
 class RoutingTable;
 class RTNLHandler;
@@ -32,8 +33,7 @@
 
   void AddDeviceToBlackList(const std::string &device_name);
   void SetStartupProfiles(const std::vector<std::string> &profile_path);
-  // Main for connection manager.  Starts main process and holds event
-  // loop.
+  // Main for connection manager.  Starts main process and holds event loop.
   void Run();
   void Quit();
 
@@ -46,6 +46,7 @@
   Config *config_;
   ControlInterface *control_;
   Metrics metrics_;
+  NSS *nss_;
   ProxyFactory *proxy_factory_;
   RTNLHandler *rtnl_handler_;
   RoutingTable *routing_table_;