Support additional placeholders in ONC identity fields

Support additional placeholders in ONC identity fields such as Identity
or Username:
- CERT_SUBJECT_COMMON_NAME: the ASCII CommonName of the selected client
  certificate (replaced when a CertPattern is used).
- DEVICE_SERIAL_NUMBER: the serial number of the device. Only replaced
  in DeviceOpenNetworkConfiguration.
- DEVICE_ASSET_ID: the Asset ID of the device. Only replaced in
  DeviceOpenNetworkConfiguration.

This CL also changes ONC identity field variable replacement to use
|VariableExpander| instead of custom code.

      chromeos_unittests --gtet_filter=ClientCertResolverTest*

Bug: 833426
Test: unit_tests --gtest_filter=*NetworkConfigurationUpdater* && \
Change-Id: I436792ea0115230554f9726d56288cae09e80d40
Reviewed-on: https://chromium-review.googlesource.com/1016920
Commit-Queue: Pavol Marko <pmarko@chromium.org>
Reviewed-by: Maksim Ivanov <emaxx@chromium.org>
Reviewed-by: Lutz Justen <ljusten@chromium.org>
Reviewed-by: Steven Bennetts <stevenjb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#553087}
diff --git a/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc b/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc
index a7ac918a..2ed36289d 100644
--- a/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc
+++ b/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc
@@ -201,7 +201,8 @@
           chromeos::NetworkHandler::Get()
               ->managed_network_configuration_handler(),
           chromeos::NetworkHandler::Get()->network_device_handler(),
-          chromeos::CrosSettings::Get());
+          chromeos::CrosSettings::Get(),
+          DeviceNetworkConfigurationUpdater::DeviceAssetIDFetcher());
 
   bluetooth_policy_handler_ =
       std::make_unique<BluetoothPolicyHandler>(chromeos::CrosSettings::Get());
diff --git a/chrome/browser/chromeos/policy/device_network_configuration_updater.cc b/chrome/browser/chromeos/policy/device_network_configuration_updater.cc
index 9599abe..23da89c 100644
--- a/chrome/browser/chromeos/policy/device_network_configuration_updater.cc
+++ b/chrome/browser/chromeos/policy/device_network_configuration_updater.cc
@@ -4,21 +4,38 @@
 
 #include "chrome/browser/chromeos/policy/device_network_configuration_updater.h"
 
+#include <map>
+
 #include "base/bind.h"
 #include "base/bind_helpers.h"
 #include "base/command_line.h"
 #include "chrome/browser/browser_process.h"
+#include "chrome/browser/browser_process_platform_part.h"
 #include "chrome/browser/chromeos/policy/browser_policy_connector_chromeos.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chromeos/chromeos_switches.h"
 #include "chromeos/network/managed_network_configuration_handler.h"
 #include "chromeos/network/network_device_handler.h"
+#include "chromeos/network/onc/onc_utils.h"
 #include "chromeos/settings/cros_settings_names.h"
 #include "chromeos/settings/cros_settings_provider.h"
+#include "chromeos/system/statistics_provider.h"
+#include "chromeos/tools/variable_expander.h"
 #include "components/policy/policy_constants.h"
+#include "content/public/browser/browser_thread.h"
 
 namespace policy {
 
+namespace {
+
+std::string GetDeviceAssetID() {
+  return g_browser_process->platform_part()
+      ->browser_policy_connector_chromeos()
+      ->GetDeviceAssetID();
+}
+
+}  // namespace
+
 DeviceNetworkConfigurationUpdater::~DeviceNetworkConfigurationUpdater() {}
 
 // static
@@ -27,11 +44,13 @@
     PolicyService* policy_service,
     chromeos::ManagedNetworkConfigurationHandler* network_config_handler,
     chromeos::NetworkDeviceHandler* network_device_handler,
-    chromeos::CrosSettings* cros_settings) {
+    chromeos::CrosSettings* cros_settings,
+    const DeviceNetworkConfigurationUpdater::DeviceAssetIDFetcher&
+        device_asset_id_fetcher) {
   std::unique_ptr<DeviceNetworkConfigurationUpdater> updater(
       new DeviceNetworkConfigurationUpdater(
           policy_service, network_config_handler, network_device_handler,
-          cros_settings));
+          cros_settings, device_asset_id_fetcher));
   updater->Init();
   return updater;
 }
@@ -40,13 +59,16 @@
     PolicyService* policy_service,
     chromeos::ManagedNetworkConfigurationHandler* network_config_handler,
     chromeos::NetworkDeviceHandler* network_device_handler,
-    chromeos::CrosSettings* cros_settings)
+    chromeos::CrosSettings* cros_settings,
+    const DeviceNetworkConfigurationUpdater::DeviceAssetIDFetcher&
+        device_asset_id_fetcher)
     : NetworkConfigurationUpdater(onc::ONC_SOURCE_DEVICE_POLICY,
                                   key::kDeviceOpenNetworkConfiguration,
                                   policy_service,
                                   network_config_handler),
       network_device_handler_(network_device_handler),
       cros_settings_(cros_settings),
+      device_asset_id_fetcher_(device_asset_id_fetcher),
       weak_factory_(this) {
   DCHECK(network_device_handler_);
   data_roaming_setting_subscription_ = cros_settings->AddSettingsObserver(
@@ -54,6 +76,8 @@
       base::Bind(
           &DeviceNetworkConfigurationUpdater::OnDataRoamingSettingChanged,
           base::Unretained(this)));
+  if (device_asset_id_fetcher_.is_null())
+    device_asset_id_fetcher_ = base::BindRepeating(&GetDeviceAssetID);
 }
 
 std::vector<std::string>
@@ -121,6 +145,25 @@
 void DeviceNetworkConfigurationUpdater::ApplyNetworkPolicy(
     base::ListValue* network_configs_onc,
     base::DictionaryValue* global_network_config) {
+  // Ensure this is runnng on the UI thead because we're accessing global data
+  // to populate the substitutions.
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  // Expand device-specific placeholders. Note: It is OK that if the serial
+  // number or Asset ID are empty, the placeholders will be expanded to an empty
+  // string. This is to be consistent with user policy identity string
+  // expansions.
+  std::map<std::string, std::string> substitutions;
+  substitutions[::onc::substitutes::kDeviceSerialNumber] =
+      chromeos::system::StatisticsProvider::GetInstance()
+          ->GetEnterpriseMachineID();
+  substitutions[::onc::substitutes::kDeviceAssetId] =
+      device_asset_id_fetcher_.Run();
+
+  chromeos::VariableExpander variable_expander(std::move(substitutions));
+  chromeos::onc::ExpandStringsInNetworks(variable_expander,
+                                         network_configs_onc);
+
   network_config_handler_->SetPolicy(onc_source_,
                                      std::string() /* no username hash */,
                                      *network_configs_onc,
diff --git a/chrome/browser/chromeos/policy/device_network_configuration_updater.h b/chrome/browser/chromeos/policy/device_network_configuration_updater.h
index 246f5a74..56161163 100644
--- a/chrome/browser/chromeos/policy/device_network_configuration_updater.h
+++ b/chrome/browser/chromeos/policy/device_network_configuration_updater.h
@@ -36,15 +36,23 @@
  public:
   ~DeviceNetworkConfigurationUpdater() override;
 
+  // Fetches the device's administrator-annotated asset ID.
+  using DeviceAssetIDFetcher = base::RepeatingCallback<std::string()>;
+
   // Creates an updater that applies the ONC device policy from |policy_service|
   // once the policy service is completely initialized and on each policy
-  // change. The argument objects must outlive the returned updater.
+  // change. The argument objects passed as pointers must outlive the returned
+  // updater. |device_assed_id_fetcher| should return the
+  // administrator-annotated asset ID of the device and is used for variable
+  // replacement. If a null callback is passed, the asset ID from device policy
+  // will be used.
   static std::unique_ptr<DeviceNetworkConfigurationUpdater>
   CreateForDevicePolicy(
       PolicyService* policy_service,
       chromeos::ManagedNetworkConfigurationHandler* network_config_handler,
       chromeos::NetworkDeviceHandler* network_device_handler,
-      chromeos::CrosSettings* cros_settings);
+      chromeos::CrosSettings* cros_settings,
+      const DeviceAssetIDFetcher& device_asset_id_fetcher);
 
   // Returns all authority certificates from the currently applied ONC device
   // policy.
@@ -55,7 +63,8 @@
       PolicyService* policy_service,
       chromeos::ManagedNetworkConfigurationHandler* network_config_handler,
       chromeos::NetworkDeviceHandler* network_device_handler,
-      chromeos::CrosSettings* cros_settings);
+      chromeos::CrosSettings* cros_settings,
+      const DeviceAssetIDFetcher& device_asset_id_fetcher);
 
   void Init() override;
   void ImportCertificates(const base::ListValue& certificates_onc) override;
@@ -69,6 +78,9 @@
   std::unique_ptr<base::CallbackList<void(void)>::Subscription>
       data_roaming_setting_subscription_;
 
+  // Returns the device's administrator-set asset id.
+  DeviceAssetIDFetcher device_asset_id_fetcher_;
+
   base::WeakPtrFactory<DeviceNetworkConfigurationUpdater> weak_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(DeviceNetworkConfigurationUpdater);
diff --git a/chrome/browser/chromeos/policy/network_configuration_updater_unittest.cc b/chrome/browser/chromeos/policy/network_configuration_updater_unittest.cc
index eb250d6..053b56d 100644
--- a/chrome/browser/chromeos/policy/network_configuration_updater_unittest.cc
+++ b/chrome/browser/chromeos/policy/network_configuration_updater_unittest.cc
@@ -25,6 +25,8 @@
 #include "chromeos/network/onc/onc_parsed_certificates.h"
 #include "chromeos/network/onc/onc_test_utils.h"
 #include "chromeos/network/onc/onc_utils.h"
+#include "chromeos/system/fake_statistics_provider.h"
+#include "chromeos/system/statistics_provider.h"
 #include "components/onc/onc_constants.h"
 #include "components/policy/core/common/external_data_fetcher.h"
 #include "components/policy/core/common/mock_configuration_policy_provider.h"
@@ -58,6 +60,8 @@
 
 const char kFakeUserEmail[] = "fake email";
 const char kFakeUsernameHash[] = "fake hash";
+const char kFakeSerialNumber[] = "FakeSerial";
+const char kFakeAssetId[] = "FakeAssetId";
 
 class FakeUser : public user_manager::User {
  public:
@@ -167,26 +171,41 @@
   DISALLOW_COPY_AND_ASSIGN(FakeCertificateImporter);
 };
 
-const char kFakeONC[] =
-    "{ \"NetworkConfigurations\": ["
-    "    { \"GUID\": \"{485d6076-dd44-6b6d-69787465725f5040}\","
-    "      \"Type\": \"WiFi\","
-    "      \"Name\": \"My WiFi Network\","
-    "      \"WiFi\": {"
-    "        \"HexSSID\": \"737369642D6E6F6E65\","  // "ssid-none"
-    "        \"Security\": \"None\" }"
-    "    }"
-    "  ],"
-    "  \"GlobalNetworkConfiguration\": {"
-    "    \"AllowOnlyPolicyNetworksToAutoconnect\": true,"
-    "  },"
-    "  \"Certificates\": ["
-    "    { \"GUID\": \"{f998f760-272b-6939-4c2beffe428697ac}\","
-    "      \"PKCS12\": \"YWJj\","
-    "      \"Type\": \"Client\" }"
-    "  ],"
-    "  \"Type\": \"UnencryptedConfiguration\""
-    "}";
+// Note: HexSSID 737369642D6E6F6E65 maps to "ssid-none".
+// HexSSID 7465737431323334 maps to "test1234"
+const char kFakeONC[] = R"(
+    { "NetworkConfigurations": [
+        { "GUID": "{485d6076-dd44-6b6d-69787465725f5040}",
+          "Type": "WiFi",
+          "Name": "My WiFi Network",
+          "WiFi": {
+            "HexSSID": "737369642D6E6F6E65",
+            "Security": "None" }
+        },
+        { "GUID": "{guid-for-wifi-with-device-exp}",
+          "Type": "WiFi",
+          "Name": "My WiFi with device placeholder expansions",
+          "WiFi": {
+            "EAP": {
+              "Outer": "EAP-TLS",
+              "Identity": "${DEVICE_SERIAL_NUMBER}-${DEVICE_ASSET_ID}"
+            },
+            "HexSSID": "7465737431323334",
+            "Security": "WPA-EAP",
+            "SSID": "test1234",
+          }
+        }
+      ],
+      "GlobalNetworkConfiguration": {
+        "AllowOnlyPolicyNetworksToAutoconnect": true,
+      },
+      "Certificates": [
+        { "GUID": "{f998f760-272b-6939-4c2beffe428697ac}",
+          "PKCS12": "YWJj",
+          "Type": "Client" }
+      ],
+      "Type": "UnencryptedConfiguration"
+    })";
 
 std::string ValueToString(const base::Value& value) {
   std::stringstream str;
@@ -194,11 +213,6 @@
   return str.str();
 }
 
-void AppendAll(const base::ListValue& from, base::ListValue* to) {
-  for (const auto& value : from)
-    to->Append(value.CreateDeepCopy());
-}
-
 // Matcher to match base::Value.
 MATCHER_P(IsEqualTo,
           value,
@@ -224,6 +238,9 @@
   NetworkConfigurationUpdaterTest() : certificate_importer_(NULL) {}
 
   void SetUp() override {
+    fake_statistics_provider_.SetMachineStatistic(
+        chromeos::system::kSerialNumberKey, kFakeSerialNumber);
+
     EXPECT_CALL(provider_, IsInitializationComplete(_))
         .WillRepeatedly(Return(false));
     provider_.Init();
@@ -234,11 +251,6 @@
     std::unique_ptr<base::DictionaryValue> fake_toplevel_onc =
         chromeos::onc::ReadDictionaryFromJson(kFakeONC);
 
-    base::ListValue* network_configs = NULL;
-    fake_toplevel_onc->GetListWithoutPathExpansion(
-        onc::toplevel_config::kNetworkConfigurations, &network_configs);
-    AppendAll(*network_configs, &fake_network_configs_);
-
     base::DictionaryValue* global_config = NULL;
     fake_toplevel_onc->GetDictionaryWithoutPathExpansion(
         onc::toplevel_config::kGlobalNetworkConfiguration, &global_config);
@@ -254,6 +266,26 @@
     certificate_importer_owned_.reset(certificate_importer_);
   }
 
+  base::Value* GetExpectedFakeNetworkConfigs(::onc::ONCSource source) {
+    std::unique_ptr<base::DictionaryValue> fake_toplevel_onc =
+        chromeos::onc::ReadDictionaryFromJson(kFakeONC);
+    fake_network_configs_ =
+        fake_toplevel_onc->FindKey(onc::toplevel_config::kNetworkConfigurations)
+            ->Clone();
+    if (source == ::onc::ONC_SOURCE_DEVICE_POLICY) {
+      std::string expected_identity =
+          std::string(kFakeSerialNumber) + "-" + std::string(kFakeAssetId);
+      SetExpectedValueInNetworkConfig(
+          &fake_network_configs_, "{guid-for-wifi-with-device-exp}",
+          {"WiFi", "EAP", "Identity"}, base::Value(expected_identity));
+    }
+    return &fake_network_configs_;
+  }
+
+  base::Value* GetExpectedFakeGlobalNetworkConfig() {
+    return &fake_global_network_config_;
+  }
+
   void TearDown() override {
     network_configuration_updater_.reset();
     provider_.Shutdown();
@@ -295,23 +327,23 @@
   }
 
   void CreateNetworkConfigurationUpdaterForDevicePolicy() {
+    auto testing_device_asset_id_getter =
+        base::BindRepeating([] { return std::string(kFakeAssetId); });
     network_configuration_updater_ =
         DeviceNetworkConfigurationUpdater::CreateForDevicePolicy(
-            policy_service_.get(),
-            &network_config_handler_,
-            &network_device_handler_,
-            chromeos::CrosSettings::Get());
+            policy_service_.get(), &network_config_handler_,
+            &network_device_handler_, chromeos::CrosSettings::Get(),
+            testing_device_asset_id_getter);
   }
 
   content::TestBrowserThreadBundle thread_bundle_;
 
-  base::ListValue fake_network_configs_;
-  base::DictionaryValue fake_global_network_config_;
   std::unique_ptr<chromeos::onc::OncParsedCertificates> fake_certificates_;
   StrictMock<chromeos::MockManagedNetworkConfigurationHandler>
       network_config_handler_;
   FakeNetworkDeviceHandler network_device_handler_;
   chromeos::ScopedCrosSettingsTestHelper settings_helper_;
+  chromeos::system::ScopedFakeStatisticsProvider fake_statistics_provider_;
 
   // Ownership of certificate_importer_owned_ is passed to the
   // NetworkConfigurationUpdater. When that happens, |certificate_importer_|
@@ -328,6 +360,25 @@
   TestingProfile profile_;
 
   std::unique_ptr<NetworkConfigurationUpdater> network_configuration_updater_;
+
+ private:
+  void SetExpectedValueInNetworkConfig(
+      base::Value* network_configs,
+      base::StringPiece guid,
+      std::initializer_list<base::StringPiece> path,
+      base::Value value) {
+    for (base::Value& network_config : network_configs->GetList()) {
+      const base::Value* guid_value =
+          network_config.FindKey(::onc::network_config::kGUID);
+      if (!guid_value || guid_value->GetString() != guid)
+        continue;
+      network_config.SetPath(path, std::move(value));
+      break;
+    }
+  }
+
+  base::Value fake_network_configs_;
+  base::DictionaryValue fake_global_network_config_;
 };
 
 TEST_F(NetworkConfigurationUpdaterTest, CellularAllowRoaming) {
@@ -510,11 +561,11 @@
              std::make_unique<base::Value>(kFakeONC), nullptr);
   UpdateProviderPolicy(policy);
 
+  ::onc::ONCSource source = onc::ONC_SOURCE_USER_POLICY;
   EXPECT_CALL(network_config_handler_,
-              SetPolicy(onc::ONC_SOURCE_USER_POLICY,
-                        kFakeUsernameHash,
-                        IsEqualTo(&fake_network_configs_),
-                        IsEqualTo(&fake_global_network_config_)));
+              SetPolicy(source, kFakeUsernameHash,
+                        IsEqualTo(GetExpectedFakeNetworkConfigs(source)),
+                        IsEqualTo(GetExpectedFakeGlobalNetworkConfig())));
 
   UserNetworkConfigurationUpdater* updater =
       CreateNetworkConfigurationUpdaterForUserPolicy(
@@ -535,6 +586,23 @@
   EXPECT_EQ(1u, certificate_importer_->GetAndResetImportCount());
 }
 
+TEST_F(NetworkConfigurationUpdaterTest, ReplaceDeviceOncPlaceholders) {
+  PolicyMap policy;
+  policy.Set(key::kDeviceOpenNetworkConfiguration, POLICY_LEVEL_MANDATORY,
+             POLICY_SCOPE_MACHINE, POLICY_SOURCE_CLOUD,
+             std::make_unique<base::Value>(kFakeONC), nullptr);
+  UpdateProviderPolicy(policy);
+
+  ::onc::ONCSource source = onc::ONC_SOURCE_DEVICE_POLICY;
+  EXPECT_CALL(network_config_handler_,
+              SetPolicy(source, std::string(),
+                        IsEqualTo(GetExpectedFakeNetworkConfigs(source)),
+                        IsEqualTo(GetExpectedFakeGlobalNetworkConfig())));
+
+  CreateNetworkConfigurationUpdaterForDevicePolicy();
+  MarkPolicyProviderInitialized();
+}
+
 class NetworkConfigurationUpdaterTestWithParam
     : public NetworkConfigurationUpdaterTest,
       public testing::WithParamInterface<const char*> {
@@ -579,11 +647,11 @@
              nullptr);
   UpdateProviderPolicy(policy);
 
-  EXPECT_CALL(network_config_handler_,
-              SetPolicy(CurrentONCSource(),
-                        ExpectedUsernameHash(),
-                        IsEqualTo(&fake_network_configs_),
-                        IsEqualTo(&fake_global_network_config_)));
+  EXPECT_CALL(
+      network_config_handler_,
+      SetPolicy(CurrentONCSource(), ExpectedUsernameHash(),
+                IsEqualTo(GetExpectedFakeNetworkConfigs(CurrentONCSource())),
+                IsEqualTo(GetExpectedFakeGlobalNetworkConfig())));
   certificate_importer_->SetExpectedONCCertificates(
       std::move(fake_certificates_));
   certificate_importer_->SetExpectedONCSource(CurrentONCSource());
@@ -607,11 +675,11 @@
   Mock::VerifyAndClearExpectations(&network_config_handler_);
   EXPECT_EQ(0u, certificate_importer_->GetAndResetImportCount());
 
-  EXPECT_CALL(network_config_handler_,
-              SetPolicy(CurrentONCSource(),
-                        ExpectedUsernameHash(),
-                        IsEqualTo(&fake_network_configs_),
-                        IsEqualTo(&fake_global_network_config_)));
+  EXPECT_CALL(
+      network_config_handler_,
+      SetPolicy(CurrentONCSource(), ExpectedUsernameHash(),
+                IsEqualTo(GetExpectedFakeNetworkConfigs(CurrentONCSource())),
+                IsEqualTo(GetExpectedFakeGlobalNetworkConfig())));
   certificate_importer_->SetExpectedONCSource(CurrentONCSource());
   certificate_importer_->SetExpectedONCCertificates(
       std::move(fake_certificates_));
@@ -631,11 +699,11 @@
              nullptr);
   UpdateProviderPolicy(policy);
 
-  EXPECT_CALL(network_config_handler_,
-              SetPolicy(CurrentONCSource(),
-                        ExpectedUsernameHash(),
-                        IsEqualTo(&fake_network_configs_),
-                        IsEqualTo(&fake_global_network_config_)));
+  EXPECT_CALL(
+      network_config_handler_,
+      SetPolicy(CurrentONCSource(), ExpectedUsernameHash(),
+                IsEqualTo(GetExpectedFakeNetworkConfigs(CurrentONCSource())),
+                IsEqualTo(GetExpectedFakeGlobalNetworkConfig())));
   certificate_importer_->SetExpectedONCSource(CurrentONCSource());
   certificate_importer_->SetExpectedONCCertificates(
       std::move(fake_certificates_));
@@ -658,11 +726,11 @@
             certificate_importer_->GetAndResetImportCount());
 
   // The Updater should update if policy changes.
-  EXPECT_CALL(network_config_handler_,
-              SetPolicy(CurrentONCSource(),
-                        _,
-                        IsEqualTo(&fake_network_configs_),
-                        IsEqualTo(&fake_global_network_config_)));
+  EXPECT_CALL(
+      network_config_handler_,
+      SetPolicy(CurrentONCSource(), _,
+                IsEqualTo(GetExpectedFakeNetworkConfigs(CurrentONCSource())),
+                IsEqualTo(GetExpectedFakeGlobalNetworkConfig())));
   certificate_importer_->SetExpectedONCSource(CurrentONCSource());
   certificate_importer_->SetExpectedONCCertificates(
       std::move(fake_certificates_));
diff --git a/chromeos/network/certificate_helper.cc b/chromeos/network/certificate_helper.cc
index b72cb86..b82193b 100644
--- a/chromeos/network/certificate_helper.cc
+++ b/chromeos/network/certificate_helper.cc
@@ -83,6 +83,10 @@
   return name;
 }
 
+std::string GetCertAsciiSubjectCommonName(CERTCertificate* cert_handle) {
+  return Stringize(CERT_GetCommonName(&cert_handle->subject), std::string());
+}
+
 std::string GetCertAsciiNameOrNickname(CERTCertificate* cert_handle) {
   std::string alternative_text = GetNickname(cert_handle);
   return Stringize(CERT_GetCommonName(&cert_handle->subject), alternative_text);
diff --git a/chromeos/network/certificate_helper.h b/chromeos/network/certificate_helper.h
index 28368118..f3228249 100644
--- a/chromeos/network/certificate_helper.h
+++ b/chromeos/network/certificate_helper.h
@@ -35,7 +35,14 @@
 // value (see GetCertAsciiNameOrNickname instead).
 CHROMEOS_EXPORT std::string GetCertNameOrNickname(CERTCertificate* cert_handle);
 
-// Returns the unformated ASCII common name for |cert_handle|->subject.
+// Returns the unformated ASCII common name for |cert_handle|->subject. Returns
+// an empty string if the subject name is unavailable or empty.
+CHROMEOS_EXPORT std::string GetCertAsciiSubjectCommonName(
+    CERTCertificate* cert_handle);
+
+// Returns the unformatted ASCII common name for |cert_handle|->subject or for
+// |cert_handle|->nickname if the subject name is unavailable or empty. If both
+// are not available, returns an empty string.
 CHROMEOS_EXPORT std::string GetCertAsciiNameOrNickname(
     CERTCertificate* cert_handle);
 
diff --git a/chromeos/network/client_cert_resolver.cc b/chromeos/network/client_cert_resolver.cc
index d350134..ddd329e 100644
--- a/chromeos/network/client_cert_resolver.cc
+++ b/chromeos/network/client_cert_resolver.cc
@@ -17,14 +17,15 @@
 #include "base/logging.h"
 #include "base/optional.h"
 #include "base/stl_util.h"
-#include "base/strings/string_util.h"
 #include "base/task_scheduler/post_task.h"
 #include "base/time/clock.h"
 #include "chromeos/dbus/dbus_thread_manager.h"
 #include "chromeos/dbus/shill_service_client.h"
+#include "chromeos/network/certificate_helper.h"
 #include "chromeos/network/managed_network_configuration_handler.h"
 #include "chromeos/network/network_event_log.h"
 #include "chromeos/network/network_state.h"
+#include "chromeos/tools/variable_expander.h"
 #include "components/onc/onc_constants.h"
 #include "dbus/object_path.h"
 #include "net/cert/scoped_nss_types.h"
@@ -80,6 +81,39 @@
 // pattern resolution.
 enum class ResolveStatus { kResolving, kResolved };
 
+// Returns substitutions based on |cert|'s contents to be used in a
+// VariableExpander.
+std::map<std::string, std::string> GetSubstitutionsForCert(
+    CERTCertificate* cert) {
+  std::map<std::string, std::string> substitutions;
+
+  {
+    std::vector<std::string> names;
+    net::x509_util::GetRFC822SubjectAltNames(cert, &names);
+    // Currently, we only use the first specified RFC8222
+    // SubjectAlternativeName.
+    std::string firstSANEmail;
+    if (!names.empty())
+      firstSANEmail = names[0];
+    substitutions[onc::substitutes::kCertSANEmail] = firstSANEmail;
+  }
+
+  {
+    std::vector<std::string> names;
+    net::x509_util::GetUPNSubjectAltNames(cert, &names);
+    // Currently, we only use the first specified UPN SubjectAlternativeName.
+    std::string firstSANUPN;
+    if (!names.empty())
+      firstSANUPN = names[0];
+    substitutions[onc::substitutes::kCertSANUPN] = firstSANUPN;
+  }
+
+  substitutions[onc::substitutes::kCertSubjectCommonName] =
+      certificate::GetCertAsciiSubjectCommonName(cert);
+
+  return substitutions;
+}
+
 }  // namespace
 
 namespace internal {
@@ -286,7 +320,6 @@
 
     std::string pkcs11_id;
     int slot_id = -1;
-    std::string identity;
 
     pkcs11_id =
         CertLoader::GetPkcs11IdAndSlotForCert(cert_it->cert.get(), &slot_id);
@@ -297,31 +330,14 @@
       continue;
     }
 
-    // If the policy specifies an identity containing ${CERT_SAN_xxx},
-    // see if the cert contains a suitable subjectAltName that can be
-    // stuffed into the shill properties.
-    identity = network_and_pattern.cert_config.policy_identity;
-    std::vector<std::string> names;
-
-    size_t offset = identity.find(onc::substitutes::kCertSANEmail, 0);
-    if (offset != std::string::npos) {
-      std::vector<std::string> names;
-      net::x509_util::GetRFC822SubjectAltNames(cert_it->cert.get(), &names);
-      if (!names.empty()) {
-        base::ReplaceSubstringsAfterOffset(
-            &identity, offset, onc::substitutes::kCertSANEmail, names[0]);
-      }
-    }
-
-    offset = identity.find(onc::substitutes::kCertSANUPN, 0);
-    if (offset != std::string::npos) {
-      std::vector<std::string> names;
-      net::x509_util::GetUPNSubjectAltNames(cert_it->cert.get(), &names);
-      if (!names.empty()) {
-        base::ReplaceSubstringsAfterOffset(
-            &identity, offset, onc::substitutes::kCertSANUPN, names[0]);
-      }
-    }
+    // Expand placeholders in the identity string that are specific to the
+    // client certificate.
+    VariableExpander variable_expander(
+        GetSubstitutionsForCert(cert_it->cert.get()));
+    std::string identity = network_and_pattern.cert_config.policy_identity;
+    const bool success = variable_expander.ExpandString(&identity);
+    LOG_IF(ERROR, !success)
+        << "Error during variable expansion in ONC-configured identity";
 
     matches.push_back(NetworkAndMatchingCert(
         network_and_pattern, MatchingCert(pkcs11_id, slot_id, identity)));
diff --git a/chromeos/network/client_cert_resolver_unittest.cc b/chromeos/network/client_cert_resolver_unittest.cc
index b41edc92..d69b055f 100644
--- a/chromeos/network/client_cert_resolver_unittest.cc
+++ b/chromeos/network/client_cert_resolver_unittest.cc
@@ -731,6 +731,16 @@
   GetServiceProperty(shill::kEapIdentityProperty, &identity);
   EXPECT_EQ("upn-santest@ad.corp.example.com-suffix", identity);
   EXPECT_EQ(2, network_properties_changed_count_);
+
+  // Verify that after changing the ONC policy to request the subject CommonName
+  // field, the correct value is substituted into the shill service entry.
+  SetupPolicyMatchingIssuerPEM(onc::ONC_SOURCE_USER_POLICY,
+                               "subject-cn-${CERT_SUBJECT_COMMON_NAME}-suffix");
+  scoped_task_environment_.RunUntilIdle();
+
+  GetServiceProperty(shill::kEapIdentityProperty, &identity);
+  EXPECT_EQ("subject-cn-Client Cert F-suffix", identity);
+  EXPECT_EQ(3, network_properties_changed_count_);
 }
 
 // Test for crbug.com/781276. A notification which results in no networks to be
diff --git a/chromeos/network/onc/onc_translator_onc_to_shill.cc b/chromeos/network/onc/onc_translator_onc_to_shill.cc
index 7cb3bfe..7a1e315 100644
--- a/chromeos/network/onc/onc_translator_onc_to_shill.cc
+++ b/chromeos/network/onc/onc_translator_onc_to_shill.cc
@@ -323,8 +323,8 @@
   // password substitution variable is set.
   const base::Value* password_field =
       onc_object_->FindKey(::onc::eap::kPassword);
-  if (password_field &&
-      password_field->GetString() == ::onc::substitutes::kPasswordField) {
+  if (password_field && password_field->GetString() ==
+                            ::onc::substitutes::kPasswordPlaceholderVerbatim) {
     shill_dictionary_->SetKey(shill::kEapUseLoginPasswordProperty,
                               base::Value(true));
   }
diff --git a/chromeos/network/onc/onc_translator_shill_to_onc.cc b/chromeos/network/onc/onc_translator_shill_to_onc.cc
index faf2104..f9baff2 100644
--- a/chromeos/network/onc/onc_translator_shill_to_onc.cc
+++ b/chromeos/network/onc/onc_translator_shill_to_onc.cc
@@ -709,8 +709,9 @@
   if (shill_dictionary_->GetBooleanWithoutPathExpansion(
           shill::kEapUseLoginPasswordProperty, &use_login_password) &&
       use_login_password) {
-    onc_object_->SetKey(::onc::eap::kPassword,
-                        base::Value(::onc::substitutes::kPasswordField));
+    onc_object_->SetKey(
+        ::onc::eap::kPassword,
+        base::Value(::onc::substitutes::kPasswordPlaceholderVerbatim));
   }
 }
 
diff --git a/chromeos/network/onc/onc_utils.cc b/chromeos/network/onc/onc_utils.cc
index c086a7ee..4baca3a 100644
--- a/chromeos/network/onc/onc_utils.cc
+++ b/chromeos/network/onc/onc_utils.cc
@@ -65,277 +65,33 @@
 
 namespace {
 
-const char kUnableToDecrypt[] = "Unable to decrypt encrypted ONC";
-const char kUnableToDecode[] = "Unable to decode encrypted ONC";
+// Error messages that can be reported when decrypting encrypted ONC.
+constexpr char kUnableToDecrypt[] = "Unable to decrypt encrypted ONC";
+constexpr char kUnableToDecode[] = "Unable to decode encrypted ONC";
 
-// This class defines which string placeholders of ONC are replaced by which
-// user attribute.
-class UserStringSubstitution : public chromeos::onc::StringSubstitution {
- public:
-  explicit UserStringSubstitution(const user_manager::User* user)
-      : user_(user) {}
-  ~UserStringSubstitution() override = default;
+// Scheme strings for supported |net::ProxyServer::SCHEME_*| enum values.
+constexpr char kDirectScheme[] = "direct";
+constexpr char kQuicScheme[] = "quic";
+constexpr char kSocksScheme[] = "socks";
+constexpr char kSocks4Scheme[] = "socks4";
+constexpr char kSocks5Scheme[] = "socks5";
 
-  bool GetSubstitute(const std::string& placeholder,
-                     std::string* substitute) const override {
-    if (placeholder == ::onc::substitutes::kLoginIDField)
-      *substitute = user_->GetAccountName(false);
-    else if (placeholder == ::onc::substitutes::kEmailField)
-      *substitute = user_->GetAccountId().GetUserEmail();
-    else
-      return false;
-    return true;
-  }
-
- private:
-  const user_manager::User* user_;
-
-  DISALLOW_COPY_AND_ASSIGN(UserStringSubstitution);
-};
-
-}  // namespace
-
-const char kEmptyUnencryptedConfiguration[] =
-    "{\"Type\":\"UnencryptedConfiguration\",\"NetworkConfigurations\":[],"
-    "\"Certificates\":[]}";
-
-std::unique_ptr<base::DictionaryValue> ReadDictionaryFromJson(
-    const std::string& json) {
-  std::string error;
-  std::unique_ptr<base::Value> root = base::JSONReader::ReadAndReturnError(
-      json, base::JSON_ALLOW_TRAILING_COMMAS, nullptr, &error);
-
-  base::DictionaryValue* dict_ptr = nullptr;
-  if (!root || !root->GetAsDictionary(&dict_ptr)) {
-    NET_LOG(ERROR) << "Invalid JSON Dictionary: " << error;
-    return nullptr;
-  }
-  ignore_result(root.release());
-  return base::WrapUnique(dict_ptr);
-}
-
-std::unique_ptr<base::DictionaryValue> Decrypt(
-    const std::string& passphrase,
-    const base::DictionaryValue& root) {
-  const int kKeySizeInBits = 256;
-  const int kMaxIterationCount = 500000;
-  std::string onc_type;
-  std::string initial_vector;
-  std::string salt;
-  std::string cipher;
-  std::string stretch_method;
-  std::string hmac_method;
-  std::string hmac;
-  int iterations;
-  std::string ciphertext;
-
-  if (!root.GetString(encrypted::kCiphertext, &ciphertext) ||
-      !root.GetString(encrypted::kCipher, &cipher) ||
-      !root.GetString(encrypted::kHMAC, &hmac) ||
-      !root.GetString(encrypted::kHMACMethod, &hmac_method) ||
-      !root.GetString(encrypted::kIV, &initial_vector) ||
-      !root.GetInteger(encrypted::kIterations, &iterations) ||
-      !root.GetString(encrypted::kSalt, &salt) ||
-      !root.GetString(encrypted::kStretch, &stretch_method) ||
-      !root.GetString(toplevel_config::kType, &onc_type) ||
-      onc_type != toplevel_config::kEncryptedConfiguration) {
-    NET_LOG(ERROR) << "Encrypted ONC malformed.";
-    return nullptr;
-  }
-
-  if (hmac_method != encrypted::kSHA1 ||
-      cipher != encrypted::kAES256 ||
-      stretch_method != encrypted::kPBKDF2) {
-    NET_LOG(ERROR) << "Encrypted ONC unsupported encryption scheme.";
-    return nullptr;
-  }
-
-  // Make sure iterations != 0, since that's not valid.
-  if (iterations == 0) {
-    NET_LOG(ERROR) << kUnableToDecrypt;
-    return nullptr;
-  }
-
-  // Simply a sanity check to make sure we can't lock up the machine
-  // for too long with a huge number (or a negative number).
-  if (iterations < 0 || iterations > kMaxIterationCount) {
-    NET_LOG(ERROR) << "Too many iterations in encrypted ONC";
-    return nullptr;
-  }
-
-  if (!base::Base64Decode(salt, &salt)) {
-    NET_LOG(ERROR) << kUnableToDecode;
-    return nullptr;
-  }
-
-  std::unique_ptr<crypto::SymmetricKey> key(
-      crypto::SymmetricKey::DeriveKeyFromPassword(crypto::SymmetricKey::AES,
-                                                  passphrase, salt, iterations,
-                                                  kKeySizeInBits));
-
-  if (!base::Base64Decode(initial_vector, &initial_vector)) {
-    NET_LOG(ERROR) << kUnableToDecode;
-    return nullptr;
-  }
-  if (!base::Base64Decode(ciphertext, &ciphertext)) {
-    NET_LOG(ERROR) << kUnableToDecode;
-    return nullptr;
-  }
-  if (!base::Base64Decode(hmac, &hmac)) {
-    NET_LOG(ERROR) << kUnableToDecode;
-    return nullptr;
-  }
-
-  crypto::HMAC hmac_verifier(crypto::HMAC::SHA1);
-  if (!hmac_verifier.Init(key.get()) ||
-      !hmac_verifier.Verify(ciphertext, hmac)) {
-    NET_LOG(ERROR) << kUnableToDecrypt;
-    return nullptr;
-  }
-
-  crypto::Encryptor decryptor;
-  if (!decryptor.Init(key.get(), crypto::Encryptor::CBC, initial_vector))  {
-    NET_LOG(ERROR) << kUnableToDecrypt;
-    return nullptr;
-  }
-
-  std::string plaintext;
-  if (!decryptor.Decrypt(ciphertext, &plaintext)) {
-    NET_LOG(ERROR) << kUnableToDecrypt;
-    return nullptr;
-  }
-
-  std::unique_ptr<base::DictionaryValue> new_root =
-      ReadDictionaryFromJson(plaintext);
-  if (!new_root) {
-    NET_LOG(ERROR) << "Property dictionary malformed.";
-    return nullptr;
-  }
-
-  return new_root;
-}
-
-std::string GetSourceAsString(ONCSource source) {
-  switch (source) {
-    case ONC_SOURCE_UNKNOWN:
-      return "unknown";
-    case ONC_SOURCE_NONE:
-      return "none";
-    case ONC_SOURCE_DEVICE_POLICY:
-      return "device policy";
-    case ONC_SOURCE_USER_POLICY:
-      return "user policy";
-    case ONC_SOURCE_USER_IMPORT:
-      return "user import";
-  }
-  NOTREACHED() << "unknown ONC source " << source;
-  return "unknown";
-}
-
+// Runs |variable_expander.ExpandString| on the field |fieldname| in
+// |onc_object|.
 void ExpandField(const std::string& fieldname,
-                 const StringSubstitution& substitution,
+                 const VariableExpander& variable_expander,
                  base::DictionaryValue* onc_object) {
-  std::string user_string;
-  if (!onc_object->GetStringWithoutPathExpansion(fieldname, &user_string))
+  std::string field_value;
+  if (!onc_object->GetStringWithoutPathExpansion(fieldname, &field_value))
     return;
 
-  std::string login_id;
-  if (substitution.GetSubstitute(substitutes::kLoginIDField, &login_id)) {
-    base::ReplaceSubstringsAfterOffset(&user_string, 0,
-                                       substitutes::kLoginIDField,
-                                       login_id);
-  }
+  variable_expander.ExpandString(&field_value);
 
-  std::string email;
-  if (substitution.GetSubstitute(substitutes::kEmailField, &email)) {
-    base::ReplaceSubstringsAfterOffset(&user_string, 0,
-                                       substitutes::kEmailField,
-                                       email);
-  }
-
-  onc_object->SetKey(fieldname, base::Value(user_string));
+  onc_object->SetKey(fieldname, base::Value(field_value));
 }
 
-void ExpandStringsInOncObject(
-    const OncValueSignature& signature,
-    const StringSubstitution& substitution,
-    base::DictionaryValue* onc_object) {
-  if (&signature == &kEAPSignature) {
-    ExpandField(eap::kAnonymousIdentity, substitution, onc_object);
-    ExpandField(eap::kIdentity, substitution, onc_object);
-  } else if (&signature == &kL2TPSignature ||
-             &signature == &kOpenVPNSignature) {
-    ExpandField(vpn::kUsername, substitution, onc_object);
-  }
-
-  // Recurse into nested objects.
-  for (base::DictionaryValue::Iterator it(*onc_object); !it.IsAtEnd();
-       it.Advance()) {
-    base::DictionaryValue* inner_object = nullptr;
-    if (!onc_object->GetDictionaryWithoutPathExpansion(it.key(), &inner_object))
-      continue;
-
-    const OncFieldSignature* field_signature =
-        GetFieldSignature(signature, it.key());
-    if (!field_signature)
-      continue;
-
-    ExpandStringsInOncObject(*field_signature->value_signature,
-                             substitution, inner_object);
-  }
-}
-
-void ExpandStringsInNetworks(const StringSubstitution& substitution,
-                             base::ListValue* network_configs) {
-  for (auto& entry : *network_configs) {
-    base::DictionaryValue* network = nullptr;
-    entry.GetAsDictionary(&network);
-    DCHECK(network);
-    ExpandStringsInOncObject(
-        kNetworkConfigurationSignature, substitution, network);
-  }
-}
-
-void FillInHexSSIDFieldsInOncObject(const OncValueSignature& signature,
-                                    base::DictionaryValue* onc_object) {
-  if (&signature == &kWiFiSignature)
-    FillInHexSSIDField(onc_object);
-
-  // Recurse into nested objects.
-  for (base::DictionaryValue::Iterator it(*onc_object); !it.IsAtEnd();
-       it.Advance()) {
-    base::DictionaryValue* inner_object = nullptr;
-    if (!onc_object->GetDictionaryWithoutPathExpansion(it.key(), &inner_object))
-      continue;
-
-    const OncFieldSignature* field_signature =
-        GetFieldSignature(signature, it.key());
-    if (!field_signature)
-      continue;
-
-    FillInHexSSIDFieldsInOncObject(*field_signature->value_signature,
-                                   inner_object);
-  }
-}
-
-void FillInHexSSIDField(base::DictionaryValue* wifi_fields) {
-  std::string ssid_string;
-  if (wifi_fields->HasKey(::onc::wifi::kHexSSID) ||
-      !wifi_fields->GetStringWithoutPathExpansion(::onc::wifi::kSSID,
-                                                  &ssid_string)) {
-    return;
-  }
-  if (ssid_string.empty()) {
-    NET_LOG(ERROR) << "Found empty SSID field.";
-    return;
-  }
-  wifi_fields->SetKey(
-      ::onc::wifi::kHexSSID,
-      base::Value(base::HexEncode(ssid_string.c_str(), ssid_string.size())));
-}
-
-namespace {
-
+// A |Mapper| for masking sensitive fields (e.g. credentials such as
+// passphrases) in ONC.
 class OncMaskValues : public Mapper {
  public:
   static std::unique_ptr<base::DictionaryValue> Mask(
@@ -362,7 +118,7 @@
       // If it's the password field and the substitution string is used, don't
       // mask it.
       if (&object_signature == &kEAPSignature && field_name == eap::kPassword &&
-          onc_value.GetString() == substitutes::kPasswordField) {
+          onc_value.GetString() == substitutes::kPasswordPlaceholderVerbatim) {
         return Mapper::MapField(field_name, object_signature, onc_value,
                                 found_unknown_field, error);
       }
@@ -377,44 +133,8 @@
   std::string mask_;
 };
 
-}  // namespace
-
-std::unique_ptr<base::DictionaryValue> MaskCredentialsInOncObject(
-    const OncValueSignature& signature,
-    const base::DictionaryValue& onc_object,
-    const std::string& mask) {
-  return OncMaskValues::Mask(signature, onc_object, mask);
-}
-
-std::string DecodePEM(const std::string& pem_encoded) {
-  // The PEM block header used for DER certificates
-  const char kCertificateHeader[] = "CERTIFICATE";
-
-  // This is an older PEM marker for DER certificates.
-  const char kX509CertificateHeader[] = "X509 CERTIFICATE";
-
-  std::vector<std::string> pem_headers;
-  pem_headers.push_back(kCertificateHeader);
-  pem_headers.push_back(kX509CertificateHeader);
-
-  net::PEMTokenizer pem_tokenizer(pem_encoded, pem_headers);
-  std::string decoded;
-  if (pem_tokenizer.GetNext()) {
-    decoded = pem_tokenizer.data();
-  } else {
-    // If we failed to read the data as a PEM file, then try plain base64 decode
-    // in case the PEM marker strings are missing. For this to work, there has
-    // to be no white space, and it has to only contain the base64-encoded data.
-    if (!base::Base64Decode(pem_encoded, &decoded)) {
-      LOG(ERROR) << "Unable to base64 decode X509 data: " << pem_encoded;
-      return std::string();
-    }
-  }
-  return decoded;
-}
-
-namespace {
-
+// Returns a map GUID->PEM of all server and authority certificates defined in
+// the Certificates section of ONC, which is passed in as |certificates|.
 CertPEMsByGUIDMap GetServerAndCACertsByGUID(
     const base::ListValue& certificates) {
   CertPEMsByGUIDMap certs_by_guid;
@@ -447,6 +167,7 @@
   return certs_by_guid;
 }
 
+// Fills HexSSID fields in all entries in the |network_configs| list.
 void FillInHexSSIDFieldsInNetworks(base::ListValue* network_configs) {
   for (auto& entry : *network_configs) {
     base::DictionaryValue* network = nullptr;
@@ -456,129 +177,10 @@
   }
 }
 
-}  // namespace
-
-bool ParseAndValidateOncForImport(const std::string& onc_blob,
-                                  ONCSource onc_source,
-                                  const std::string& passphrase,
-                                  base::ListValue* network_configs,
-                                  base::DictionaryValue* global_network_config,
-                                  base::ListValue* certificates) {
-  if (network_configs)
-    network_configs->Clear();
-  if (global_network_config)
-    global_network_config->Clear();
-  if (certificates)
-    certificates->Clear();
-  if (onc_blob.empty())
-    return true;
-
-  std::unique_ptr<base::DictionaryValue> toplevel_onc =
-      ReadDictionaryFromJson(onc_blob);
-  if (!toplevel_onc) {
-    LOG(ERROR) << "ONC loaded from " << GetSourceAsString(onc_source)
-               << " is not a valid JSON dictionary.";
-    return false;
-  }
-
-  // Check and see if this is an encrypted ONC file. If so, decrypt it.
-  std::string onc_type;
-  toplevel_onc->GetStringWithoutPathExpansion(toplevel_config::kType,
-                                              &onc_type);
-  if (onc_type == toplevel_config::kEncryptedConfiguration) {
-    toplevel_onc = Decrypt(passphrase, *toplevel_onc);
-    if (!toplevel_onc) {
-      LOG(ERROR) << "Couldn't decrypt the ONC from "
-                 << GetSourceAsString(onc_source);
-      return false;
-    }
-  }
-
-  bool from_policy = (onc_source == ONC_SOURCE_USER_POLICY ||
-                      onc_source == ONC_SOURCE_DEVICE_POLICY);
-
-  // Validate the ONC dictionary. We are liberal and ignore unknown field
-  // names and ignore invalid field names in kRecommended arrays.
-  Validator validator(false,  // Ignore unknown fields.
-                      false,  // Ignore invalid recommended field names.
-                      true,   // Fail on missing fields.
-                      from_policy);
-  validator.SetOncSource(onc_source);
-
-  Validator::Result validation_result;
-  toplevel_onc = validator.ValidateAndRepairObject(
-      &kToplevelConfigurationSignature,
-      *toplevel_onc,
-      &validation_result);
-
-  if (from_policy) {
-    UMA_HISTOGRAM_BOOLEAN("Enterprise.ONC.PolicyValidation",
-                          validation_result == Validator::VALID);
-  }
-
-  bool success = true;
-  if (validation_result == Validator::VALID_WITH_WARNINGS) {
-    LOG(WARNING) << "ONC from " << GetSourceAsString(onc_source)
-                 << " produced warnings.";
-    success = false;
-  } else if (validation_result == Validator::INVALID || !toplevel_onc) {
-    LOG(ERROR) << "ONC from " << GetSourceAsString(onc_source)
-               << " is invalid and couldn't be repaired.";
-    return false;
-  }
-
-  base::ListValue* validated_certs = nullptr;
-  if (certificates && toplevel_onc->GetListWithoutPathExpansion(
-                          toplevel_config::kCertificates, &validated_certs)) {
-    certificates->Swap(validated_certs);
-  }
-
-  base::ListValue* validated_networks = nullptr;
-  // Note that this processing is performed even if |network_configs| is
-  // nullptr, because ResolveServerCertRefsInNetworks could affect the return
-  // value of the function (which is supposed to aggregate validation issues in
-  // all segments of the ONC blob).
-  if (toplevel_onc->GetListWithoutPathExpansion(
-          toplevel_config::kNetworkConfigurations, &validated_networks)) {
-    FillInHexSSIDFieldsInNetworks(validated_networks);
-
-    CertPEMsByGUIDMap server_and_ca_certs =
-        GetServerAndCACertsByGUID(*certificates);
-
-    if (!ResolveServerCertRefsInNetworks(server_and_ca_certs,
-                                         validated_networks)) {
-      LOG(ERROR) << "Some certificate references in the ONC policy for source "
-                 << GetSourceAsString(onc_source) << " could not be resolved.";
-      success = false;
-    }
-
-    if (network_configs)
-      network_configs->Swap(validated_networks);
-  }
-
-  base::DictionaryValue* validated_global_config = nullptr;
-  if (global_network_config && toplevel_onc->GetDictionaryWithoutPathExpansion(
-                                   toplevel_config::kGlobalNetworkConfiguration,
-                                   &validated_global_config)) {
-    global_network_config->Swap(validated_global_config);
-  }
-
-  return success;
-}
-
-net::ScopedCERTCertificate DecodePEMCertificate(
-    const std::string& pem_encoded) {
-  std::string decoded = DecodePEM(pem_encoded);
-  net::ScopedCERTCertificate cert =
-      net::x509_util::CreateCERTCertificateFromBytes(
-          reinterpret_cast<const uint8_t*>(decoded.data()), decoded.size());
-  LOG_IF(ERROR, !cert.get()) << "Couldn't create certificate from X509 data: "
-                             << decoded;
-  return cert;
-}
-
-namespace {
-
+// Given a GUID->PEM certificate mapping |certs_by_guid|, looks up the PEM
+// encoded certificate referenced by |guid_ref|. If a match is found, sets
+// |*pem_encoded| to the PEM encoded certificate and returns true. Otherwise,
+// returns false.
 bool GUIDRefToPEMEncoding(const CertPEMsByGUIDMap& certs_by_guid,
                           const std::string& guid_ref,
                           std::string* pem_encoded) {
@@ -595,6 +197,12 @@
   return true;
 }
 
+// Given a GUID-> PM certificate mapping |certs_by_guid|, attempts to resolve
+// the certificate referenced by the |key_guid_ref| field in |onc_object|.
+// * If |onc_object| has no |key_guid_ref| field, returns true.
+// * If no matching certificate is found in |certs_by_guid|, returns false.
+// * If a matching certificate is found, removes the |key_guid_ref| field,
+//   fills the |key_pem| field in |onc_object| and returns true.
 bool ResolveSingleCertRef(const CertPEMsByGUIDMap& certs_by_guid,
                           const std::string& key_guid_ref,
                           const std::string& key_pem,
@@ -612,6 +220,16 @@
   return true;
 }
 
+// Given a GUID-> PM certificate mapping |certs_by_guid|, attempts to resolve
+// the certificates referenced by the list-of-strings field |key_guid_ref_list|
+// in |onc_object|.
+// * If |key_guid_ref_list| does not exist in |onc_object|, returns true.
+// * If any element |key_guid_ref_list| can not be found in |certs_by_guid|,
+//   aborts processing and returns false. |onc_object| is unchanged in this
+//   case.
+// * Otherwise, sets |key_pem_list| to be a list-of-strings field in
+//   |onc_object|, containing all PEM encoded resolved certificates in order and
+//   returns true.
 bool ResolveCertRefList(const CertPEMsByGUIDMap& certs_by_guid,
                         const std::string& key_guid_ref_list,
                         const std::string& key_pem_list,
@@ -640,6 +258,8 @@
   return true;
 }
 
+// Same as |ResolveSingleCertRef|, but the output |key_pem_list| will be set to
+// a list with exactly one value when resolution succeeds.
 bool ResolveSingleCertRefToList(const CertPEMsByGUIDMap& certs_by_guid,
                                 const std::string& key_guid_ref,
                                 const std::string& key_pem_list,
@@ -682,6 +302,8 @@
       certs_by_guid, key_guid_ref, key_pem_list, onc_object);
 }
 
+// Resolve known server and authority certiifcate reference fields in
+// |onc_object|.
 bool ResolveServerCertRefsInObject(const CertPEMsByGUIDMap& certs_by_guid,
                                    const OncValueSignature& signature,
                                    base::DictionaryValue* onc_object) {
@@ -744,88 +366,6 @@
   return true;
 }
 
-}  // namespace
-
-bool ResolveServerCertRefsInNetworks(const CertPEMsByGUIDMap& certs_by_guid,
-                                     base::ListValue* network_configs) {
-  bool success = true;
-  for (base::ListValue::iterator it = network_configs->begin();
-       it != network_configs->end(); ) {
-    base::DictionaryValue* network = nullptr;
-    it->GetAsDictionary(&network);
-    if (!ResolveServerCertRefsInNetwork(certs_by_guid, network)) {
-      std::string guid;
-      network->GetStringWithoutPathExpansion(network_config::kGUID, &guid);
-      // This might happen even with correct validation, if the referenced
-      // certificate couldn't be imported.
-      LOG(ERROR) << "Couldn't resolve some certificate reference of network "
-                 << guid;
-      it = network_configs->Erase(it, nullptr);
-      success = false;
-      continue;
-    }
-    ++it;
-  }
-  return success;
-}
-
-bool ResolveServerCertRefsInNetwork(const CertPEMsByGUIDMap& certs_by_guid,
-                                    base::DictionaryValue* network_config) {
-  return ResolveServerCertRefsInObject(certs_by_guid,
-                                       kNetworkConfigurationSignature,
-                                       network_config);
-}
-
-NetworkTypePattern NetworkTypePatternFromOncType(const std::string& type) {
-  if (type == ::onc::network_type::kAllTypes)
-    return NetworkTypePattern::Default();
-  if (type == ::onc::network_type::kCellular)
-    return NetworkTypePattern::Cellular();
-  if (type == ::onc::network_type::kEthernet)
-    return NetworkTypePattern::Ethernet();
-  if (type == ::onc::network_type::kTether)
-    return NetworkTypePattern::Tether();
-  if (type == ::onc::network_type::kVPN)
-    return NetworkTypePattern::VPN();
-  if (type == ::onc::network_type::kWiFi)
-    return NetworkTypePattern::WiFi();
-  if (type == ::onc::network_type::kWimax)
-    return NetworkTypePattern::Wimax();
-  if (type == ::onc::network_type::kWireless)
-    return NetworkTypePattern::Wireless();
-  NOTREACHED() << "Unrecognized ONC type: " << type;
-  return NetworkTypePattern::Default();
-}
-
-bool IsRecommendedValue(const base::DictionaryValue* onc,
-                        const std::string& property_key) {
-  std::string property_basename, recommended_property_key;
-  size_t pos = property_key.find_last_of('.');
-  if (pos != std::string::npos) {
-    // 'WiFi.AutoConnect' -> 'AutoConnect', 'WiFi.Recommended'
-    property_basename = property_key.substr(pos + 1);
-    recommended_property_key =
-        property_key.substr(0, pos + 1) + ::onc::kRecommended;
-  } else {
-    // 'Name' -> 'Name', 'Recommended'
-    property_basename = property_key;
-    recommended_property_key = ::onc::kRecommended;
-  }
-
-  const base::ListValue* recommended_keys = nullptr;
-  return (onc->GetList(recommended_property_key, &recommended_keys) &&
-          recommended_keys->Find(base::Value(property_basename)) !=
-              recommended_keys->end());
-}
-
-namespace {
-
-const char kDirectScheme[] = "direct";
-const char kQuicScheme[] = "quic";
-const char kSocksScheme[] = "socks";
-const char kSocks4Scheme[] = "socks4";
-const char kSocks5Scheme[] = "socks5";
-
 net::ProxyServer ConvertOncProxyLocationToHostPort(
     net::ProxyServer::Scheme default_proxy_scheme,
     const base::DictionaryValue& onc_proxy_location) {
@@ -939,8 +479,591 @@
   dict->SetWithoutPathExpansion(onc_scheme, std::move(url_dict));
 }
 
+// Returns the NetworkConfiugration with |guid| from |network_configs|, or
+// nullptr if no such NetworkConfiguration is found.
+const base::DictionaryValue* GetNetworkConfigByGUID(
+    const base::ListValue& network_configs,
+    const std::string& guid) {
+  for (base::ListValue::const_iterator it = network_configs.begin();
+       it != network_configs.end(); ++it) {
+    const base::DictionaryValue* network = NULL;
+    it->GetAsDictionary(&network);
+    DCHECK(network);
+
+    std::string current_guid;
+    network->GetStringWithoutPathExpansion(::onc::network_config::kGUID,
+                                           &current_guid);
+    if (current_guid == guid)
+      return network;
+  }
+  return NULL;
+}
+
+// Returns the first Ethernet NetworkConfiguration from |network_configs| with
+// "Authentication: None", or nullptr if no such NetworkConfiguration is found.
+const base::DictionaryValue* GetNetworkConfigForEthernetWithoutEAP(
+    const base::ListValue& network_configs) {
+  VLOG(2) << "Search for ethernet policy without EAP.";
+  for (base::ListValue::const_iterator it = network_configs.begin();
+       it != network_configs.end(); ++it) {
+    const base::DictionaryValue* network = NULL;
+    it->GetAsDictionary(&network);
+    DCHECK(network);
+
+    std::string type;
+    network->GetStringWithoutPathExpansion(::onc::network_config::kType, &type);
+    if (type != ::onc::network_type::kEthernet)
+      continue;
+
+    const base::DictionaryValue* ethernet = NULL;
+    network->GetDictionaryWithoutPathExpansion(::onc::network_config::kEthernet,
+                                               &ethernet);
+
+    std::string auth;
+    ethernet->GetStringWithoutPathExpansion(::onc::ethernet::kAuthentication,
+                                            &auth);
+    if (auth == ::onc::ethernet::kAuthenticationNone)
+      return network;
+  }
+  return NULL;
+}
+
+// Returns the NetworkConfiguration object for |network| from
+// |network_configs| or nullptr if no matching NetworkConfiguration is found. If
+// |network| is a non-Ethernet network, performs a lookup by GUID. If |network|
+// is an Ethernet network, tries lookup of the GUID of the shared EthernetEAP
+// service, or otherwise returns the first Ethernet NetworkConfiguration with
+// "Authentication: None".
+const base::DictionaryValue* GetNetworkConfigForNetworkFromOnc(
+    const base::ListValue& network_configs,
+    const NetworkState& network) {
+  // In all cases except Ethernet, we use the GUID of |network|.
+  if (!network.Matches(NetworkTypePattern::Ethernet()))
+    return GetNetworkConfigByGUID(network_configs, network.guid());
+
+  // Ethernet is always shared and thus cannot store a GUID per user. Thus we
+  // search for any Ethernet policy intead of a matching GUID.
+  // EthernetEAP service contains only the EAP parameters and stores the GUID of
+  // the respective ONC policy. The EthernetEAP service itself is however never
+  // in state "connected". An EthernetEAP policy must be applied, if an Ethernet
+  // service is connected using the EAP parameters.
+  const NetworkState* ethernet_eap = NULL;
+  if (NetworkHandler::IsInitialized()) {
+    ethernet_eap =
+        NetworkHandler::Get()->network_state_handler()->GetEAPForEthernet(
+            network.path());
+  }
+
+  // The GUID associated with the EthernetEAP service refers to the ONC policy
+  // with "Authentication: 8021X".
+  if (ethernet_eap)
+    return GetNetworkConfigByGUID(network_configs, ethernet_eap->guid());
+
+  // Otherwise, EAP is not used and instead the Ethernet policy with
+  // "Authentication: None" applies.
+  return GetNetworkConfigForEthernetWithoutEAP(network_configs);
+}
+
+// Expects |pref_name| in |pref_service| to be a pref holding an ONC blob.
+// Returns the NetworkConfiguration ONC object for |network| from this ONC, or
+// nullptr if no configuration is found. See |GetNetworkConfigForNetworkFromOnc|
+// for the NetworkConfiguration lookup rules.
+const base::DictionaryValue* GetPolicyForNetworkFromPref(
+    const PrefService* pref_service,
+    const char* pref_name,
+    const NetworkState& network) {
+  if (!pref_service) {
+    VLOG(2) << "No pref service";
+    return NULL;
+  }
+
+  const PrefService::Preference* preference =
+      pref_service->FindPreference(pref_name);
+  if (!preference) {
+    VLOG(2) << "No preference " << pref_name;
+    // The preference may not exist in tests.
+    return NULL;
+  }
+
+  // User prefs are not stored in this Preference yet but only the policy.
+  //
+  // The policy server incorrectly configures the OpenNetworkConfiguration user
+  // policy as Recommended. To work around that, we handle the Recommended and
+  // the Mandatory value in the same way.
+  // TODO(pneubeck): Remove this workaround, once the server is fixed. See
+  // http://crbug.com/280553 .
+  if (preference->IsDefaultValue()) {
+    VLOG(2) << "Preference has no recommended or mandatory value.";
+    // No policy set.
+    return NULL;
+  }
+  VLOG(2) << "Preference with policy found.";
+  const base::Value* onc_policy_value = preference->GetValue();
+  DCHECK(onc_policy_value);
+
+  const base::ListValue* onc_policy = NULL;
+  onc_policy_value->GetAsList(&onc_policy);
+  DCHECK(onc_policy);
+
+  return GetNetworkConfigForNetworkFromOnc(*onc_policy, network);
+}
+
+// Returns the global network configuration dictionary from the ONC policy of
+// the active user if |for_active_user| is true, or from device policy if it is
+// false.
+const base::DictionaryValue* GetGlobalConfigFromPolicy(bool for_active_user) {
+  std::string username_hash;
+  if (for_active_user) {
+    const user_manager::User* user =
+        user_manager::UserManager::Get()->GetActiveUser();
+    if (!user) {
+      LOG(ERROR) << "No user logged in yet.";
+      return NULL;
+    }
+    username_hash = user->username_hash();
+  }
+  return NetworkHandler::Get()
+      ->managed_network_configuration_handler()
+      ->GetGlobalConfigFromPolicy(username_hash);
+}
+
 }  // namespace
 
+const char kEmptyUnencryptedConfiguration[] =
+    "{\"Type\":\"UnencryptedConfiguration\",\"NetworkConfigurations\":[],"
+    "\"Certificates\":[]}";
+
+std::unique_ptr<base::DictionaryValue> ReadDictionaryFromJson(
+    const std::string& json) {
+  std::string error;
+  std::unique_ptr<base::Value> root = base::JSONReader::ReadAndReturnError(
+      json, base::JSON_ALLOW_TRAILING_COMMAS, nullptr, &error);
+
+  base::DictionaryValue* dict_ptr = nullptr;
+  if (!root || !root->GetAsDictionary(&dict_ptr)) {
+    NET_LOG(ERROR) << "Invalid JSON Dictionary: " << error;
+    return nullptr;
+  }
+  ignore_result(root.release());
+  return base::WrapUnique(dict_ptr);
+}
+
+std::unique_ptr<base::DictionaryValue> Decrypt(
+    const std::string& passphrase,
+    const base::DictionaryValue& root) {
+  const int kKeySizeInBits = 256;
+  const int kMaxIterationCount = 500000;
+  std::string onc_type;
+  std::string initial_vector;
+  std::string salt;
+  std::string cipher;
+  std::string stretch_method;
+  std::string hmac_method;
+  std::string hmac;
+  int iterations;
+  std::string ciphertext;
+
+  if (!root.GetString(encrypted::kCiphertext, &ciphertext) ||
+      !root.GetString(encrypted::kCipher, &cipher) ||
+      !root.GetString(encrypted::kHMAC, &hmac) ||
+      !root.GetString(encrypted::kHMACMethod, &hmac_method) ||
+      !root.GetString(encrypted::kIV, &initial_vector) ||
+      !root.GetInteger(encrypted::kIterations, &iterations) ||
+      !root.GetString(encrypted::kSalt, &salt) ||
+      !root.GetString(encrypted::kStretch, &stretch_method) ||
+      !root.GetString(toplevel_config::kType, &onc_type) ||
+      onc_type != toplevel_config::kEncryptedConfiguration) {
+    NET_LOG(ERROR) << "Encrypted ONC malformed.";
+    return nullptr;
+  }
+
+  if (hmac_method != encrypted::kSHA1 || cipher != encrypted::kAES256 ||
+      stretch_method != encrypted::kPBKDF2) {
+    NET_LOG(ERROR) << "Encrypted ONC unsupported encryption scheme.";
+    return nullptr;
+  }
+
+  // Make sure iterations != 0, since that's not valid.
+  if (iterations == 0) {
+    NET_LOG(ERROR) << kUnableToDecrypt;
+    return nullptr;
+  }
+
+  // Simply a sanity check to make sure we can't lock up the machine
+  // for too long with a huge number (or a negative number).
+  if (iterations < 0 || iterations > kMaxIterationCount) {
+    NET_LOG(ERROR) << "Too many iterations in encrypted ONC";
+    return nullptr;
+  }
+
+  if (!base::Base64Decode(salt, &salt)) {
+    NET_LOG(ERROR) << kUnableToDecode;
+    return nullptr;
+  }
+
+  std::unique_ptr<crypto::SymmetricKey> key(
+      crypto::SymmetricKey::DeriveKeyFromPassword(crypto::SymmetricKey::AES,
+                                                  passphrase, salt, iterations,
+                                                  kKeySizeInBits));
+
+  if (!base::Base64Decode(initial_vector, &initial_vector)) {
+    NET_LOG(ERROR) << kUnableToDecode;
+    return nullptr;
+  }
+  if (!base::Base64Decode(ciphertext, &ciphertext)) {
+    NET_LOG(ERROR) << kUnableToDecode;
+    return nullptr;
+  }
+  if (!base::Base64Decode(hmac, &hmac)) {
+    NET_LOG(ERROR) << kUnableToDecode;
+    return nullptr;
+  }
+
+  crypto::HMAC hmac_verifier(crypto::HMAC::SHA1);
+  if (!hmac_verifier.Init(key.get()) ||
+      !hmac_verifier.Verify(ciphertext, hmac)) {
+    NET_LOG(ERROR) << kUnableToDecrypt;
+    return nullptr;
+  }
+
+  crypto::Encryptor decryptor;
+  if (!decryptor.Init(key.get(), crypto::Encryptor::CBC, initial_vector)) {
+    NET_LOG(ERROR) << kUnableToDecrypt;
+    return nullptr;
+  }
+
+  std::string plaintext;
+  if (!decryptor.Decrypt(ciphertext, &plaintext)) {
+    NET_LOG(ERROR) << kUnableToDecrypt;
+    return nullptr;
+  }
+
+  std::unique_ptr<base::DictionaryValue> new_root =
+      ReadDictionaryFromJson(plaintext);
+  if (!new_root) {
+    NET_LOG(ERROR) << "Property dictionary malformed.";
+    return nullptr;
+  }
+
+  return new_root;
+}
+
+std::string GetSourceAsString(ONCSource source) {
+  switch (source) {
+    case ONC_SOURCE_UNKNOWN:
+      return "unknown";
+    case ONC_SOURCE_NONE:
+      return "none";
+    case ONC_SOURCE_DEVICE_POLICY:
+      return "device policy";
+    case ONC_SOURCE_USER_POLICY:
+      return "user policy";
+    case ONC_SOURCE_USER_IMPORT:
+      return "user import";
+  }
+  NOTREACHED() << "unknown ONC source " << source;
+  return "unknown";
+}
+
+void ExpandStringsInOncObject(const OncValueSignature& signature,
+                              const VariableExpander& variable_expander,
+                              base::DictionaryValue* onc_object) {
+  if (&signature == &kEAPSignature) {
+    ExpandField(eap::kAnonymousIdentity, variable_expander, onc_object);
+    ExpandField(eap::kIdentity, variable_expander, onc_object);
+  } else if (&signature == &kL2TPSignature ||
+             &signature == &kOpenVPNSignature) {
+    ExpandField(vpn::kUsername, variable_expander, onc_object);
+  }
+
+  // Recurse into nested objects.
+  for (base::DictionaryValue::Iterator it(*onc_object); !it.IsAtEnd();
+       it.Advance()) {
+    base::DictionaryValue* inner_object = nullptr;
+    if (!onc_object->GetDictionaryWithoutPathExpansion(it.key(), &inner_object))
+      continue;
+
+    const OncFieldSignature* field_signature =
+        GetFieldSignature(signature, it.key());
+    if (!field_signature)
+      continue;
+
+    ExpandStringsInOncObject(*field_signature->value_signature,
+                             variable_expander, inner_object);
+  }
+}
+
+void ExpandStringsInNetworks(const VariableExpander& variable_expander,
+                             base::ListValue* network_configs) {
+  for (auto& entry : *network_configs) {
+    base::DictionaryValue* network = nullptr;
+    entry.GetAsDictionary(&network);
+    DCHECK(network);
+    ExpandStringsInOncObject(kNetworkConfigurationSignature, variable_expander,
+                             network);
+  }
+}
+
+void FillInHexSSIDFieldsInOncObject(const OncValueSignature& signature,
+                                    base::DictionaryValue* onc_object) {
+  if (&signature == &kWiFiSignature)
+    FillInHexSSIDField(onc_object);
+
+  // Recurse into nested objects.
+  for (base::DictionaryValue::Iterator it(*onc_object); !it.IsAtEnd();
+       it.Advance()) {
+    base::DictionaryValue* inner_object = nullptr;
+    if (!onc_object->GetDictionaryWithoutPathExpansion(it.key(), &inner_object))
+      continue;
+
+    const OncFieldSignature* field_signature =
+        GetFieldSignature(signature, it.key());
+    if (!field_signature)
+      continue;
+
+    FillInHexSSIDFieldsInOncObject(*field_signature->value_signature,
+                                   inner_object);
+  }
+}
+
+void FillInHexSSIDField(base::DictionaryValue* wifi_fields) {
+  std::string ssid_string;
+  if (wifi_fields->HasKey(::onc::wifi::kHexSSID) ||
+      !wifi_fields->GetStringWithoutPathExpansion(::onc::wifi::kSSID,
+                                                  &ssid_string)) {
+    return;
+  }
+  if (ssid_string.empty()) {
+    NET_LOG(ERROR) << "Found empty SSID field.";
+    return;
+  }
+  wifi_fields->SetKey(
+      ::onc::wifi::kHexSSID,
+      base::Value(base::HexEncode(ssid_string.c_str(), ssid_string.size())));
+}
+
+std::unique_ptr<base::DictionaryValue> MaskCredentialsInOncObject(
+    const OncValueSignature& signature,
+    const base::DictionaryValue& onc_object,
+    const std::string& mask) {
+  return OncMaskValues::Mask(signature, onc_object, mask);
+}
+
+std::string DecodePEM(const std::string& pem_encoded) {
+  // The PEM block header used for DER certificates
+  const char kCertificateHeader[] = "CERTIFICATE";
+
+  // This is an older PEM marker for DER certificates.
+  const char kX509CertificateHeader[] = "X509 CERTIFICATE";
+
+  std::vector<std::string> pem_headers;
+  pem_headers.push_back(kCertificateHeader);
+  pem_headers.push_back(kX509CertificateHeader);
+
+  net::PEMTokenizer pem_tokenizer(pem_encoded, pem_headers);
+  std::string decoded;
+  if (pem_tokenizer.GetNext()) {
+    decoded = pem_tokenizer.data();
+  } else {
+    // If we failed to read the data as a PEM file, then try plain base64 decode
+    // in case the PEM marker strings are missing. For this to work, there has
+    // to be no white space, and it has to only contain the base64-encoded data.
+    if (!base::Base64Decode(pem_encoded, &decoded)) {
+      LOG(ERROR) << "Unable to base64 decode X509 data: " << pem_encoded;
+      return std::string();
+    }
+  }
+  return decoded;
+}
+
+bool ParseAndValidateOncForImport(const std::string& onc_blob,
+                                  ONCSource onc_source,
+                                  const std::string& passphrase,
+                                  base::ListValue* network_configs,
+                                  base::DictionaryValue* global_network_config,
+                                  base::ListValue* certificates) {
+  if (network_configs)
+    network_configs->Clear();
+  if (global_network_config)
+    global_network_config->Clear();
+  if (certificates)
+    certificates->Clear();
+  if (onc_blob.empty())
+    return true;
+
+  std::unique_ptr<base::DictionaryValue> toplevel_onc =
+      ReadDictionaryFromJson(onc_blob);
+  if (!toplevel_onc) {
+    LOG(ERROR) << "ONC loaded from " << GetSourceAsString(onc_source)
+               << " is not a valid JSON dictionary.";
+    return false;
+  }
+
+  // Check and see if this is an encrypted ONC file. If so, decrypt it.
+  std::string onc_type;
+  toplevel_onc->GetStringWithoutPathExpansion(toplevel_config::kType,
+                                              &onc_type);
+  if (onc_type == toplevel_config::kEncryptedConfiguration) {
+    toplevel_onc = Decrypt(passphrase, *toplevel_onc);
+    if (!toplevel_onc) {
+      LOG(ERROR) << "Couldn't decrypt the ONC from "
+                 << GetSourceAsString(onc_source);
+      return false;
+    }
+  }
+
+  bool from_policy = (onc_source == ONC_SOURCE_USER_POLICY ||
+                      onc_source == ONC_SOURCE_DEVICE_POLICY);
+
+  // Validate the ONC dictionary. We are liberal and ignore unknown field
+  // names and ignore invalid field names in kRecommended arrays.
+  Validator validator(false,  // Ignore unknown fields.
+                      false,  // Ignore invalid recommended field names.
+                      true,   // Fail on missing fields.
+                      from_policy);
+  validator.SetOncSource(onc_source);
+
+  Validator::Result validation_result;
+  toplevel_onc = validator.ValidateAndRepairObject(
+      &kToplevelConfigurationSignature, *toplevel_onc, &validation_result);
+
+  if (from_policy) {
+    UMA_HISTOGRAM_BOOLEAN("Enterprise.ONC.PolicyValidation",
+                          validation_result == Validator::VALID);
+  }
+
+  bool success = true;
+  if (validation_result == Validator::VALID_WITH_WARNINGS) {
+    LOG(WARNING) << "ONC from " << GetSourceAsString(onc_source)
+                 << " produced warnings.";
+    success = false;
+  } else if (validation_result == Validator::INVALID || !toplevel_onc) {
+    LOG(ERROR) << "ONC from " << GetSourceAsString(onc_source)
+               << " is invalid and couldn't be repaired.";
+    return false;
+  }
+
+  base::ListValue* validated_certs = nullptr;
+  if (certificates && toplevel_onc->GetListWithoutPathExpansion(
+                          toplevel_config::kCertificates, &validated_certs)) {
+    certificates->Swap(validated_certs);
+  }
+
+  base::ListValue* validated_networks = nullptr;
+  // Note that this processing is performed even if |network_configs| is
+  // nullptr, because ResolveServerCertRefsInNetworks could affect the return
+  // value of the function (which is supposed to aggregate validation issues in
+  // all segments of the ONC blob).
+  if (toplevel_onc->GetListWithoutPathExpansion(
+          toplevel_config::kNetworkConfigurations, &validated_networks)) {
+    FillInHexSSIDFieldsInNetworks(validated_networks);
+
+    CertPEMsByGUIDMap server_and_ca_certs =
+        GetServerAndCACertsByGUID(*certificates);
+
+    if (!ResolveServerCertRefsInNetworks(server_and_ca_certs,
+                                         validated_networks)) {
+      LOG(ERROR) << "Some certificate references in the ONC policy for source "
+                 << GetSourceAsString(onc_source) << " could not be resolved.";
+      success = false;
+    }
+
+    if (network_configs)
+      network_configs->Swap(validated_networks);
+  }
+
+  base::DictionaryValue* validated_global_config = nullptr;
+  if (global_network_config && toplevel_onc->GetDictionaryWithoutPathExpansion(
+                                   toplevel_config::kGlobalNetworkConfiguration,
+                                   &validated_global_config)) {
+    global_network_config->Swap(validated_global_config);
+  }
+
+  return success;
+}
+
+net::ScopedCERTCertificate DecodePEMCertificate(
+    const std::string& pem_encoded) {
+  std::string decoded = DecodePEM(pem_encoded);
+  net::ScopedCERTCertificate cert =
+      net::x509_util::CreateCERTCertificateFromBytes(
+          reinterpret_cast<const uint8_t*>(decoded.data()), decoded.size());
+  LOG_IF(ERROR, !cert.get())
+      << "Couldn't create certificate from X509 data: " << decoded;
+  return cert;
+}
+
+bool ResolveServerCertRefsInNetworks(const CertPEMsByGUIDMap& certs_by_guid,
+                                     base::ListValue* network_configs) {
+  bool success = true;
+  for (base::ListValue::iterator it = network_configs->begin();
+       it != network_configs->end();) {
+    base::DictionaryValue* network = nullptr;
+    it->GetAsDictionary(&network);
+    if (!ResolveServerCertRefsInNetwork(certs_by_guid, network)) {
+      std::string guid;
+      network->GetStringWithoutPathExpansion(network_config::kGUID, &guid);
+      // This might happen even with correct validation, if the referenced
+      // certificate couldn't be imported.
+      LOG(ERROR) << "Couldn't resolve some certificate reference of network "
+                 << guid;
+      it = network_configs->Erase(it, nullptr);
+      success = false;
+      continue;
+    }
+    ++it;
+  }
+  return success;
+}
+
+bool ResolveServerCertRefsInNetwork(const CertPEMsByGUIDMap& certs_by_guid,
+                                    base::DictionaryValue* network_config) {
+  return ResolveServerCertRefsInObject(
+      certs_by_guid, kNetworkConfigurationSignature, network_config);
+}
+
+NetworkTypePattern NetworkTypePatternFromOncType(const std::string& type) {
+  if (type == ::onc::network_type::kAllTypes)
+    return NetworkTypePattern::Default();
+  if (type == ::onc::network_type::kCellular)
+    return NetworkTypePattern::Cellular();
+  if (type == ::onc::network_type::kEthernet)
+    return NetworkTypePattern::Ethernet();
+  if (type == ::onc::network_type::kTether)
+    return NetworkTypePattern::Tether();
+  if (type == ::onc::network_type::kVPN)
+    return NetworkTypePattern::VPN();
+  if (type == ::onc::network_type::kWiFi)
+    return NetworkTypePattern::WiFi();
+  if (type == ::onc::network_type::kWimax)
+    return NetworkTypePattern::Wimax();
+  if (type == ::onc::network_type::kWireless)
+    return NetworkTypePattern::Wireless();
+  NOTREACHED() << "Unrecognized ONC type: " << type;
+  return NetworkTypePattern::Default();
+}
+
+bool IsRecommendedValue(const base::DictionaryValue* onc,
+                        const std::string& property_key) {
+  std::string property_basename, recommended_property_key;
+  size_t pos = property_key.find_last_of('.');
+  if (pos != std::string::npos) {
+    // 'WiFi.AutoConnect' -> 'AutoConnect', 'WiFi.Recommended'
+    property_basename = property_key.substr(pos + 1);
+    recommended_property_key =
+        property_key.substr(0, pos + 1) + ::onc::kRecommended;
+  } else {
+    // 'Name' -> 'Name', 'Recommended'
+    property_basename = property_key;
+    recommended_property_key = ::onc::kRecommended;
+  }
+
+  const base::ListValue* recommended_keys = nullptr;
+  return (onc->GetList(recommended_property_key, &recommended_keys) &&
+          recommended_keys->Find(base::Value(property_basename)) !=
+              recommended_keys->end());
+}
+
 std::unique_ptr<base::DictionaryValue> ConvertOncProxySettingsToProxyConfig(
     const base::DictionaryValue& onc_proxy_settings) {
   std::string type;
@@ -1058,125 +1181,6 @@
   return proxy_settings;
 }
 
-namespace {
-
-const base::DictionaryValue* GetNetworkConfigByGUID(
-    const base::ListValue& network_configs,
-    const std::string& guid) {
-  for (base::ListValue::const_iterator it = network_configs.begin();
-       it != network_configs.end(); ++it) {
-    const base::DictionaryValue* network = NULL;
-    it->GetAsDictionary(&network);
-    DCHECK(network);
-
-    std::string current_guid;
-    network->GetStringWithoutPathExpansion(::onc::network_config::kGUID,
-                                           &current_guid);
-    if (current_guid == guid)
-      return network;
-  }
-  return NULL;
-}
-
-const base::DictionaryValue* GetNetworkConfigForEthernetWithoutEAP(
-    const base::ListValue& network_configs) {
-  VLOG(2) << "Search for ethernet policy without EAP.";
-  for (base::ListValue::const_iterator it = network_configs.begin();
-       it != network_configs.end(); ++it) {
-    const base::DictionaryValue* network = NULL;
-    it->GetAsDictionary(&network);
-    DCHECK(network);
-
-    std::string type;
-    network->GetStringWithoutPathExpansion(::onc::network_config::kType, &type);
-    if (type != ::onc::network_type::kEthernet)
-      continue;
-
-    const base::DictionaryValue* ethernet = NULL;
-    network->GetDictionaryWithoutPathExpansion(::onc::network_config::kEthernet,
-                                               &ethernet);
-
-    std::string auth;
-    ethernet->GetStringWithoutPathExpansion(::onc::ethernet::kAuthentication,
-                                            &auth);
-    if (auth == ::onc::ethernet::kAuthenticationNone)
-      return network;
-  }
-  return NULL;
-}
-
-const base::DictionaryValue* GetNetworkConfigForNetworkFromOnc(
-    const base::ListValue& network_configs,
-    const NetworkState& network) {
-  // In all cases except Ethernet, we use the GUID of |network|.
-  if (!network.Matches(NetworkTypePattern::Ethernet()))
-    return GetNetworkConfigByGUID(network_configs, network.guid());
-
-  // Ethernet is always shared and thus cannot store a GUID per user. Thus we
-  // search for any Ethernet policy intead of a matching GUID.
-  // EthernetEAP service contains only the EAP parameters and stores the GUID of
-  // the respective ONC policy. The EthernetEAP service itself is however never
-  // in state "connected". An EthernetEAP policy must be applied, if an Ethernet
-  // service is connected using the EAP parameters.
-  const NetworkState* ethernet_eap = NULL;
-  if (NetworkHandler::IsInitialized()) {
-    ethernet_eap =
-        NetworkHandler::Get()->network_state_handler()->GetEAPForEthernet(
-            network.path());
-  }
-
-  // The GUID associated with the EthernetEAP service refers to the ONC policy
-  // with "Authentication: 8021X".
-  if (ethernet_eap)
-    return GetNetworkConfigByGUID(network_configs, ethernet_eap->guid());
-
-  // Otherwise, EAP is not used and instead the Ethernet policy with
-  // "Authentication: None" applies.
-  return GetNetworkConfigForEthernetWithoutEAP(network_configs);
-}
-
-const base::DictionaryValue* GetPolicyForNetworkFromPref(
-    const PrefService* pref_service,
-    const char* pref_name,
-    const NetworkState& network) {
-  if (!pref_service) {
-    VLOG(2) << "No pref service";
-    return NULL;
-  }
-
-  const PrefService::Preference* preference =
-      pref_service->FindPreference(pref_name);
-  if (!preference) {
-    VLOG(2) << "No preference " << pref_name;
-    // The preference may not exist in tests.
-    return NULL;
-  }
-
-  // User prefs are not stored in this Preference yet but only the policy.
-  //
-  // The policy server incorrectly configures the OpenNetworkConfiguration user
-  // policy as Recommended. To work around that, we handle the Recommended and
-  // the Mandatory value in the same way.
-  // TODO(pneubeck): Remove this workaround, once the server is fixed. See
-  // http://crbug.com/280553 .
-  if (preference->IsDefaultValue()) {
-    VLOG(2) << "Preference has no recommended or mandatory value.";
-    // No policy set.
-    return NULL;
-  }
-  VLOG(2) << "Preference with policy found.";
-  const base::Value* onc_policy_value = preference->GetValue();
-  DCHECK(onc_policy_value);
-
-  const base::ListValue* onc_policy = NULL;
-  onc_policy_value->GetAsList(&onc_policy);
-  DCHECK(onc_policy);
-
-  return GetNetworkConfigForNetworkFromOnc(*onc_policy, network);
-}
-
-}  // namespace
-
 void ExpandStringPlaceholdersInNetworksForUser(
     const user_manager::User* user,
     base::ListValue* network_configs) {
@@ -1185,8 +1189,15 @@
     // expand the strings.
     return;
   }
-  UserStringSubstitution substitution(user);
-  chromeos::onc::ExpandStringsInNetworks(substitution, network_configs);
+
+  // Note: It is OK for the placeholders to be replaced with empty strings if
+  // that is what the getters on |user| provide.
+  std::map<std::string, std::string> substitutions;
+  substitutions[::onc::substitutes::kLoginID] = user->GetAccountName(false);
+  substitutions[::onc::substitutes::kLoginEmail] =
+      user->GetAccountId().GetUserEmail();
+  VariableExpander variable_expander(std::move(substitutions));
+  chromeos::onc::ExpandStringsInNetworks(variable_expander, network_configs);
 }
 
 void ImportNetworksForUser(const user_manager::User* user,
@@ -1276,26 +1287,6 @@
       ->FindPolicyByGUID(username_hash, guid, onc_source);
 }
 
-namespace {
-
-const base::DictionaryValue* GetGlobalConfigFromPolicy(bool for_active_user) {
-  std::string username_hash;
-  if (for_active_user) {
-    const user_manager::User* user =
-        user_manager::UserManager::Get()->GetActiveUser();
-    if (!user) {
-      LOG(ERROR) << "No user logged in yet.";
-      return NULL;
-    }
-    username_hash = user->username_hash();
-  }
-  return NetworkHandler::Get()
-      ->managed_network_configuration_handler()
-      ->GetGlobalConfigFromPolicy(username_hash);
-}
-
-}  // namespace
-
 bool PolicyAllowsOnlyPolicyNetworksToAutoconnect(bool for_active_user) {
   const base::DictionaryValue* global_config =
       GetGlobalConfigFromPolicy(for_active_user);
@@ -1354,7 +1345,7 @@
       return false;
     }
 
-    if (password_field == ::onc::substitutes::kPasswordField) {
+    if (password_field == ::onc::substitutes::kPasswordPlaceholderVerbatim) {
       return true;
     }
   }
diff --git a/chromeos/network/onc/onc_utils.h b/chromeos/network/onc/onc_utils.h
index 800d7686..540e5aa 100644
--- a/chromeos/network/onc/onc_utils.h
+++ b/chromeos/network/onc/onc_utils.h
@@ -10,10 +10,10 @@
 #include <string>
 #include <vector>
 
-#include "base/macros.h"
 #include "base/memory/ref_counted.h"
 #include "chromeos/chromeos_export.h"
 #include "chromeos/network/network_type_pattern.h"
+#include "chromeos/tools/variable_expander.h"
 #include "components/onc/onc_constants.h"
 #include "net/cert/scoped_nss_types.h"
 
@@ -58,35 +58,19 @@
 // For logging only: strings not user facing.
 CHROMEOS_EXPORT std::string GetSourceAsString(::onc::ONCSource source);
 
-// Used for string expansion with function ExpandStringInOncObject(...).
-class CHROMEOS_EXPORT StringSubstitution {
- public:
-  StringSubstitution() {}
-  virtual ~StringSubstitution() {}
-
-  // Returns the replacement string for |placeholder| in
-  // |substitute|. Currently, substitutes::kLoginIDField and
-  // substitutes::kEmailField are supported.
-  virtual bool GetSubstitute(const std::string& placeholder,
-                             std::string* substitute) const = 0;
-
- private:
-  DISALLOW_COPY_AND_ASSIGN(StringSubstitution);
-};
-
 // Replaces all expandable fields that are mentioned in the ONC
-// specification. The object of |onc_object| is modified in place. Currently
-// substitutes::kLoginIDField and substitutes::kEmailField are expanded. The
-// replacement strings are obtained from |substitution|.
+// specification. The object of |onc_object| is modified in place.
+// The substitution is performed using the passed |variable_expander|, which
+// defines the placeholder-value mapping.
 CHROMEOS_EXPORT void ExpandStringsInOncObject(
     const OncValueSignature& signature,
-    const StringSubstitution& substitution,
+    const VariableExpander& variable_expander,
     base::DictionaryValue* onc_object);
 
 // Replaces expandable fields in the networks of |network_configs|, which must
 // be a list of ONC NetworkConfigurations. See ExpandStringsInOncObject above.
 CHROMEOS_EXPORT void ExpandStringsInNetworks(
-    const StringSubstitution& substitution,
+    const VariableExpander& variable_expander,
     base::ListValue* network_configs);
 
 // Fills in all missing HexSSID fields that are mentioned in the ONC
@@ -170,9 +154,9 @@
 ConvertProxyConfigToOncProxySettings(
     std::unique_ptr<base::DictionaryValue> proxy_config_value);
 
-// Replaces string placeholders in |network_configs|, which must be a list of
-// ONC NetworkConfigurations. Currently only user name placeholders are
-// implemented, which are replaced by attributes from |user|.
+// Replaces user-specific string placeholders in |network_configs|, which must
+// be a list of ONC NetworkConfigurations. Currently only user name placeholders
+// are implemented, which are replaced by attributes from |user|.
 CHROMEOS_EXPORT void ExpandStringPlaceholdersInNetworksForUser(
     const user_manager::User* user,
     base::ListValue* network_configs);
diff --git a/chromeos/network/onc/onc_utils_unittest.cc b/chromeos/network/onc/onc_utils_unittest.cc
index 548877d..a65da7cf 100644
--- a/chromeos/network/onc/onc_utils_unittest.cc
+++ b/chromeos/network/onc/onc_utils_unittest.cc
@@ -16,6 +16,7 @@
 #include "chromeos/network/network_ui_data.h"
 #include "chromeos/network/onc/onc_signature.h"
 #include "chromeos/network/onc/onc_test_utils.h"
+#include "chromeos/tools/variable_expander.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace chromeos {
@@ -83,22 +84,12 @@
 const char* kLoginId = "hans";
 const char* kLoginEmail = "hans@my.domain.com";
 
-class StringSubstitutionStub : public StringSubstitution {
- public:
-  StringSubstitutionStub() = default;
-  bool GetSubstitute(const std::string& placeholder,
-                     std::string* substitute) const override {
-    if (placeholder == ::onc::substitutes::kLoginIDField)
-      *substitute = kLoginId;
-    else if (placeholder ==::onc::substitutes::kEmailField)
-      *substitute = kLoginEmail;
-    else
-      return false;
-    return true;
-  }
- private:
-  DISALLOW_COPY_AND_ASSIGN(StringSubstitutionStub);
-};
+std::map<std::string, std::string> GetTestStringSubstutions() {
+  std::map<std::string, std::string> substitutions;
+  substitutions[::onc::substitutes::kLoginID] = kLoginId;
+  substitutions[::onc::substitutes::kLoginEmail] = kLoginEmail;
+  return substitutions;
+}
 
 }  // namespace
 
@@ -106,8 +97,8 @@
   std::unique_ptr<base::DictionaryValue> vpn_onc =
       test_utils::ReadTestDictionary("valid_openvpn.onc");
 
-  StringSubstitutionStub substitution;
-  ExpandStringsInOncObject(kNetworkConfigurationSignature, substitution,
+  VariableExpander variable_expander(GetTestStringSubstutions());
+  ExpandStringsInOncObject(kNetworkConfigurationSignature, variable_expander,
                            vpn_onc.get());
 
   std::string actual_expanded;
@@ -119,8 +110,8 @@
   std::unique_ptr<base::DictionaryValue> wifi_onc =
       test_utils::ReadTestDictionary("wifi_clientcert_with_cert_pems.onc");
 
-  StringSubstitutionStub substitution;
-  ExpandStringsInOncObject(kNetworkConfigurationSignature, substitution,
+  VariableExpander variable_expander(GetTestStringSubstutions());
+  ExpandStringsInOncObject(kNetworkConfigurationSignature, variable_expander,
                            wifi_onc.get());
 
   std::string actual_expanded;
diff --git a/chromeos/tools/variable_expander.cc b/chromeos/tools/variable_expander.cc
index b6c6357..4fe7ceedc 100644
--- a/chromeos/tools/variable_expander.cc
+++ b/chromeos/tools/variable_expander.cc
@@ -108,14 +108,14 @@
 
 VariableExpander::~VariableExpander() = default;
 
-bool VariableExpander::ExpandString(std::string* str) {
+bool VariableExpander::ExpandString(std::string* str) const {
   bool no_error = true;
   for (const auto& kv : variables_)
     no_error &= Expand(kv.first, kv.second, str);
   return no_error;
 }
 
-bool VariableExpander::ExpandValue(base::Value* value) {
+bool VariableExpander::ExpandValue(base::Value* value) const {
   bool no_error = true;
   switch (value->type()) {
     case base::Value::Type::STRING: {
diff --git a/chromeos/tools/variable_expander.h b/chromeos/tools/variable_expander.h
index 4c45e55..fe3c007 100644
--- a/chromeos/tools/variable_expander.h
+++ b/chromeos/tools/variable_expander.h
@@ -41,13 +41,13 @@
   // Expands all variables in |str|. Returns true if no error has occurred.
   // Returns false if at least one variable was malformed and could not be
   // expanded (the good ones are still expanded).
-  bool ExpandString(std::string* str);
+  bool ExpandString(std::string* str) const;
 
   // Calls ExpandString on every string contained in |value|. Recursively
   // handles all hierarchy levels. Returns true if no error has occurred.
   // Returns false if at least one variable was malformed and could not be
   // expanded (the good ones are still expanded).
-  bool ExpandValue(base::Value* value);
+  bool ExpandValue(base::Value* value) const;
 
  private:
   // Maps variable -> value.
diff --git a/components/onc/docs/onc_spec.md b/components/onc/docs/onc_spec.md
index 116b3057..8fec180 100644
--- a/components/onc/docs/onc_spec.md
+++ b/components/onc/docs/onc_spec.md
@@ -1741,29 +1741,45 @@
 
 ### The expansions are:
 
-* ${LOGIN_ID} - expands to the email address of the user, but before the '@'.
+* Placeholders that will only be replaced in user-specific ONC:
+    * ${LOGIN\_ID} - expands to the email address of the user, but before
+      the '@'.
+    * ${LOGIN\_EMAIL} - expands to the email address of the user.
 
-* ${LOGIN_EMAIL} - expands to the email address of the user.
+* Placeholders that will only be replaced in device-wide ONC:
+    * ${DEVICE\_SERIAL\_NUMBER} - expands to the serial number of the device.
+    * ${DEVICE\_ASSET\_ID} - expands to the administrator-set asset ID of the
+      device.
+
+* Placeholders that will only be replaced when a client certificate has been
+  matched by a [CertificatePattern](#CertificatePattern-type):
+    * ${CERT\_SAN\_EMAIL} - expands to the first RFC822 SubjectAlternativeName
+      extracted from the client certificate.
+    * ${CERT\_SAN\_UPN} - expands to the first OtherName SubjectAlternativeName
+      with OID 1.3.6.1.4.1.311.20.2.3 (UserPrincipalName) extracted from the
+      client certificate.
+    * ${CERT\_SUBJECT\_COMMON\_NAME} - expands to the ASCII value of the Subject
+      CommonName extracted from the client certificate.
 
 ### The following SED would properly handle resolution.
 
-* s/\$\{LOGIN_ID\}/bobquail$1/g
+* s/\$\{LOGIN\_ID\}/bobquail$1/g
 
-* s/\$\{LOGIN_EMAIL\}/bobquail@example.com$1/g
+* s/\$\{LOGIN\_EMAIL\}/bobquail@example.com$1/g
 
 ### Example expansions, assuming the user was bobquail@example.com:
 
-* "${LOGIN_ID}" -> "bobquail"
+* "${LOGIN\_ID}" -> "bobquail"
 
-* "${LOGIN_ID}@corp.example.com" -> "bobquail@corp.example.com"
+* "${LOGIN\_ID}@corp.example.com" -> "bobquail@corp.example.com"
 
-* "${LOGIN_EMAIL}" -> "bobquail@example.com"
+* "${LOGIN\_EMAIL}" -> "bobquail@example.com"
 
-* "${LOGIN_ID}X" -> "bobquailX"
+* "${LOGIN\_ID}X" -> "bobquailX"
 
-* "${LOGIN_IDX}" -> "${LOGIN_IDX}"
+* "${LOGIN\_IDX}" -> "${LOGIN\_IDX}"
 
-* "X${LOGIN_ID}" -> "Xbobquail"
+* "X${LOGIN\_ID}" -> "Xbobquail"
 
 
 ## String Substitutions
diff --git a/components/onc/onc_constants.cc b/components/onc/onc_constants.cc
index 22d2c85a..681a255 100644
--- a/components/onc/onc_constants.cc
+++ b/components/onc/onc_constants.cc
@@ -440,11 +440,20 @@
 }  // namespace proxy
 
 namespace substitutes {
-const char kLoginIDField[] = "${LOGIN_ID}";
-const char kPasswordField[] = "${PASSWORD}";
-const char kEmailField[] = "${LOGIN_EMAIL}";
-const char kCertSANEmail[] = "${CERT_SAN_EMAIL}";
-const char kCertSANUPN[] = "${CERT_SAN_UPN}";
+const char kLoginID[] = "LOGIN_ID";
+const char kLoginEmail[] = "LOGIN_EMAIL";
+const char kCertSANEmail[] = "CERT_SAN_EMAIL";
+const char kCertSANUPN[] = "CERT_SAN_UPN";
+const char kCertSubjectCommonName[] = "CERT_SUBJECT_COMMON_NAME";
+const char kDeviceSerialNumber[] = "DEVICE_SERIAL_NUMBER";
+const char kDeviceAssetId[] = "DEVICE_ASSET_ID";
+// The password placeholder is defined as ${PASSWORD} because it's compared
+// verbatim against the policy-specified password field, and if it matches,
+// another bool (|shill::kEapUseLoginPasswordProperty|) is set, which makes
+// shill replace the whole password field.
+// The other placeholders above on the other hand are replaced using
+// VariableExpander.
+const char kPasswordPlaceholderVerbatim[] = "${PASSWORD}";
 }  // namespace substitutes
 
 namespace global_network_config {
diff --git a/components/onc/onc_constants.h b/components/onc/onc_constants.h
index 281345b..a662bc4 100644
--- a/components/onc/onc_constants.h
+++ b/components/onc/onc_constants.h
@@ -438,11 +438,14 @@
 }  // namespace verify_x509
 
 namespace substitutes {
-ONC_EXPORT extern const char kEmailField[];
-ONC_EXPORT extern const char kPasswordField[];
-ONC_EXPORT extern const char kLoginIDField[];
+ONC_EXPORT extern const char kLoginEmail[];
+ONC_EXPORT extern const char kLoginID[];
 ONC_EXPORT extern const char kCertSANEmail[];
 ONC_EXPORT extern const char kCertSANUPN[];
+ONC_EXPORT extern const char kCertSubjectCommonName[];
+ONC_EXPORT extern const char kDeviceSerialNumber[];
+ONC_EXPORT extern const char kDeviceAssetId[];
+ONC_EXPORT extern const char kPasswordPlaceholderVerbatim[];
 }  // namespace substitutes
 
 namespace proxy {