chromium/chromeos/ash/components/network/client_cert_util.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/network/client_cert_util.h"

#include <cert.h>
#include <pk11pub.h>
#include <stddef.h>

#include <list>
#include <optional>

#include "base/check.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "components/onc/onc_constants.h"
#include "net/base/net_errors.h"
#include "net/cert/cert_database.h"
#include "net/cert/nss_cert_database.h"
#include "net/cert/scoped_nss_types.h"
#include "net/cert/x509_certificate.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "third_party/cros_system_api/dbus/service_constants.h"

namespace ash::client_cert {

const char kDefaultTPMPin[] = "111111";

namespace {

// Extracts the type and descriptor (referenced GUID or client cert pattern
// or provisioning profile id) of a ONC-specified client certificate
// specification for a network
// (|dict_with_client_cert|) and stores it in |cert_config|.
void GetClientCertTypeAndDescriptor(
    onc::ONCSource onc_source,
    const base::Value::Dict& dict_with_client_cert,
    ClientCertConfig* cert_config) {
  cert_config->onc_source = onc_source;

  const std::string* identity =
      dict_with_client_cert.FindString(::onc::eap::kIdentity);
  if (identity)
    cert_config->policy_identity = *identity;

  const std::string* client_cert_type =
      dict_with_client_cert.FindString(::onc::client_cert::kClientCertType);
  if (client_cert_type)
    cert_config->client_cert_type = *client_cert_type;

  if (cert_config->client_cert_type == ::onc::client_cert::kPattern) {
    const base::Value::Dict* pattern_value =
        dict_with_client_cert.FindDict(::onc::client_cert::kClientCertPattern);
    if (pattern_value) {
      std::optional<OncCertificatePattern> pattern =
          OncCertificatePattern::ReadFromONCDictionary(*pattern_value);
      if (!pattern.has_value()) {
        LOG(ERROR) << "ClientCertPattern invalid";
        return;
      }
      cert_config->pattern = pattern.value();
    }
  } else if (cert_config->client_cert_type == ::onc::client_cert::kRef) {
    const std::string* client_cert_ref_key =
        dict_with_client_cert.FindString(::onc::client_cert::kClientCertRef);
    if (client_cert_ref_key)
      cert_config->guid = *client_cert_ref_key;
  } else if (cert_config->client_cert_type ==
             ::onc::client_cert::kProvisioningProfileId) {
    const std::string* provisioning_profile_id =
        dict_with_client_cert.FindString(
            ::onc::client_cert::kClientCertProvisioningProfileId);
    if (!provisioning_profile_id) {
      LOG(ERROR) << "ProvisioningProfileId missing";
      return;
    }
    cert_config->provisioning_profile_id = *provisioning_profile_id;
  }
}

}  // namespace

std::string GetPkcs11AndSlotIdFromEapCertId(const std::string& cert_id,
                                            int* slot_id) {
  *slot_id = -1;
  if (cert_id.empty())
    return std::string();

  size_t delimiter_pos = cert_id.find(':');
  if (delimiter_pos == std::string::npos) {
    // No delimiter found, so |cert_id| only contains the PKCS11 id.
    return cert_id;
  }
  if (delimiter_pos + 1 >= cert_id.size()) {
    LOG(ERROR) << "Empty PKCS11 id in cert id.";
    return std::string();
  }
  int parsed_slot_id;
  if (base::StringToInt(cert_id.substr(0, delimiter_pos), &parsed_slot_id))
    *slot_id = parsed_slot_id;
  else
    LOG(ERROR) << "Slot ID is not an integer. Cert ID is: " << cert_id << ".";
  return cert_id.substr(delimiter_pos + 1);
}

void GetClientCertFromShillProperties(const base::Value::Dict& shill_properties,
                                      ConfigType* cert_config_type,
                                      int* tpm_slot,
                                      std::string* pkcs11_id) {
  *cert_config_type = ConfigType::kNone;
  *tpm_slot = -1;
  pkcs11_id->clear();

  // Look for VPN specific client certificate properties.
  //
  // VPN Provider values are read from the "Provider" dictionary, not the
  // "Provider.Type", etc keys (which are used only to set the values).
  const base::Value::Dict* provider_properties =
      shill_properties.FindDict(shill::kProviderProperty);
  if (provider_properties) {
    const std::string* pkcs11_id_str = nullptr;

    // Look for OpenVPN specific properties.
    // Note: OpenVPN does not have a slot property, see crbug.com/769550.
    pkcs11_id_str =
        provider_properties->FindString(shill::kOpenVPNClientCertIdProperty);
    if (pkcs11_id_str) {
      *pkcs11_id = *pkcs11_id_str;
      *cert_config_type = ConfigType::kOpenVpn;
      return;
    }

    // Look for L2TP-IPsec specific properties.
    pkcs11_id_str =
        provider_properties->FindString(shill::kL2TPIPsecClientCertIdProperty);
    if (pkcs11_id_str) {
      *pkcs11_id = *pkcs11_id_str;

      const std::string* cert_slot = provider_properties->FindString(
          shill::kL2TPIPsecClientCertSlotProperty);
      if (cert_slot && !cert_slot->empty() &&
          !base::StringToInt(*cert_slot, tpm_slot)) {
        LOG(ERROR) << "Cert slot is not an integer: " << *cert_slot << ".";
        return;
      }

      *cert_config_type = ConfigType::kL2tpIpsec;
    }

    // Look for IKEv2 specific properties.
    pkcs11_id_str =
        provider_properties->FindString(shill::kIKEv2ClientCertIdProperty);
    if (pkcs11_id_str) {
      *pkcs11_id = *pkcs11_id_str;

      const std::string* cert_slot =
          provider_properties->FindString(shill::kIKEv2ClientCertSlotProperty);
      if (cert_slot && !cert_slot->empty() &&
          !base::StringToInt(*cert_slot, tpm_slot)) {
        LOG(ERROR) << "Cert slot is not an integer: " << *cert_slot << ".";
        return;
      }

      *cert_config_type = ConfigType::kIkev2;
    }
    return;
  }

  // Look for EAP specific client certificate properties, which can either be
  // part of a WiFi or EthernetEAP configuration.
  const std::string* cert_id =
      shill_properties.FindString(shill::kEapCertIdProperty);
  if (cert_id) {
    // Shill requires both CertID and KeyID for TLS connections, despite the
    // fact that by convention they are the same ID, because one identifies
    // the certificate and the other the private key.
    std::string key_id;
    const std::string* key_id_str =
        shill_properties.FindString(shill::kEapKeyIdProperty);
    if (key_id_str)
      key_id = *key_id_str;
    // Assume the configuration to be invalid, if the two IDs are not identical.
    if (*cert_id != key_id) {
      LOG(ERROR) << "EAP CertID differs from KeyID";
      return;
    }
    *pkcs11_id = GetPkcs11AndSlotIdFromEapCertId(*cert_id, tpm_slot);
    *cert_config_type = ConfigType::kEap;
  }
}

void SetShillProperties(const ConfigType cert_config_type,
                        const int tpm_slot,
                        const std::string& pkcs11_id,
                        base::Value::Dict& properties) {
  switch (cert_config_type) {
    case ConfigType::kNone: {
      return;
    }
    case ConfigType::kOpenVpn: {
      properties.Set(shill::kOpenVPNPinProperty, kDefaultTPMPin);
      // Note: OpenVPN does not have a slot property, see crbug.com/769550.
      properties.Set(shill::kOpenVPNClientCertIdProperty, pkcs11_id);
      break;
    }
    case ConfigType::kL2tpIpsec: {
      properties.Set(shill::kL2TPIPsecPinProperty, kDefaultTPMPin);
      properties.Set(shill::kL2TPIPsecClientCertSlotProperty,
                     base::NumberToString(tpm_slot));
      properties.Set(shill::kL2TPIPsecClientCertIdProperty, pkcs11_id);
      break;
    }
    case ConfigType::kIkev2: {
      // PIN property is not used by shill for a IKEv2 service since it is a
      // fixed value.
      properties.Set(shill::kIKEv2ClientCertSlotProperty,
                     base::NumberToString(tpm_slot));
      properties.Set(shill::kIKEv2ClientCertIdProperty, pkcs11_id);
      break;
    }
    case ConfigType::kEap: {
      properties.Set(shill::kEapPinProperty, kDefaultTPMPin);
      std::string key_id =
          base::StringPrintf("%i:%s", tpm_slot, pkcs11_id.c_str());

      // Shill requires both CertID and KeyID for TLS connections, despite the
      // fact that by convention they are the same ID, because one identifies
      // the certificate and the other the private key.
      properties.Set(shill::kEapCertIdProperty, key_id);
      properties.Set(shill::kEapKeyIdProperty, key_id);
      break;
    }
  }
}

void SetEmptyShillProperties(const ConfigType cert_config_type,
                             base::Value::Dict& properties) {
  switch (cert_config_type) {
    case ConfigType::kNone: {
      return;
    }
    case ConfigType::kOpenVpn: {
      properties.Set(shill::kOpenVPNPinProperty, std::string());
      properties.Set(shill::kOpenVPNClientCertIdProperty, std::string());
      break;
    }
    case ConfigType::kL2tpIpsec: {
      properties.Set(shill::kL2TPIPsecPinProperty, std::string());
      properties.Set(shill::kL2TPIPsecClientCertSlotProperty, std::string());
      properties.Set(shill::kL2TPIPsecClientCertIdProperty, std::string());
      break;
    }
    case ConfigType::kIkev2: {
      // PIN property is not used by shill for a IKEv2 service since it is a
      // fixed value.
      properties.Set(shill::kIKEv2ClientCertSlotProperty, std::string());
      properties.Set(shill::kIKEv2ClientCertIdProperty, std::string());
      break;
    }
    case ConfigType::kEap: {
      properties.Set(shill::kEapPinProperty, std::string());
      // Shill requires both CertID and KeyID for TLS connections, despite the
      // fact that by convention they are the same ID, because one identifies
      // the certificate and the other the private key.
      properties.Set(shill::kEapCertIdProperty, std::string());
      properties.Set(shill::kEapKeyIdProperty, std::string());
      break;
    }
  }
}

ClientCertConfig::ClientCertConfig()
    : location(ConfigType::kNone),
      client_cert_type(onc::client_cert::kClientCertTypeNone) {}

ClientCertConfig::ClientCertConfig(const ClientCertConfig& other) = default;

ClientCertConfig::~ClientCertConfig() = default;

ResolvedCert::ResolvedCert(
    Status status,
    int slot_id,
    const std::string& pkcs11_id,
    base::flat_map<std::string, std::string> variable_expansions)
    : status_(status),
      slot_id_(slot_id),
      pkcs11_id_(pkcs11_id),
      variable_expansions_(variable_expansions) {}

ResolvedCert::~ResolvedCert() = default;

ResolvedCert::ResolvedCert(ResolvedCert&& other) = default;

ResolvedCert& ResolvedCert::operator=(ResolvedCert&& other) = default;

// static
ResolvedCert ResolvedCert::NotKnownYet() {
  return ResolvedCert(Status::kNotKnownYet, -1, std::string(), {});
}

// static
ResolvedCert ResolvedCert::NothingMatched() {
  return ResolvedCert(Status::kNothingMatched, -1, std::string(), {});
}

// static
ResolvedCert ResolvedCert::CertMatched(
    int slot_id,
    const std::string& pkcs11_id,
    base::flat_map<std::string, std::string> variable_expansions) {
  return ResolvedCert(Status::kCertMatched, slot_id, pkcs11_id,
                      std::move(variable_expansions));
}

ResolvedCert::Status ResolvedCert::status() const {
  return status_;
}

int ResolvedCert::slot_id() const {
  DCHECK_EQ(status(), Status::kCertMatched);
  return slot_id_;
}

const std::string& ResolvedCert::pkcs11_id() const {
  DCHECK_EQ(status(), Status::kCertMatched);
  return pkcs11_id_;
}

const base::flat_map<std::string, std::string>&
ResolvedCert::variable_expansions() const {
  DCHECK_EQ(status(), Status::kCertMatched);
  return variable_expansions_;
}

bool operator==(const ResolvedCert& lhs, const ResolvedCert& rhs) {
  if (lhs.status() != rhs.status())
    return false;

  if (lhs.status() == ResolvedCert::Status::kCertMatched) {
    // Compare attributes of the matched certificate.
    return lhs.slot_id() == rhs.slot_id() &&
           lhs.pkcs11_id() == rhs.pkcs11_id() &&
           lhs.variable_expansions() == rhs.variable_expansions();
  }

  return true;
}

// Uses a template type to easily implement a const and a non-const version.
template <typename DictType>
DictType* GetOncClientCertConfigDict(DictType& network_config,
                                     ConfigType* out_config_type) {
  DictType* wifi = network_config.FindDict(::onc::network_config::kWiFi);
  if (wifi) {
    DictType* eap = wifi->FindDict(::onc::wifi::kEAP);
    if (!eap)
      return nullptr;
    if (out_config_type)
      *out_config_type = ConfigType::kEap;
    return eap;
  }

  DictType* ethernet =
      network_config.FindDict(::onc::network_config::kEthernet);
  if (ethernet) {
    DictType* eap = ethernet->FindDict(::onc::wifi::kEAP);
    if (!eap)
      return nullptr;
    if (out_config_type)
      *out_config_type = ConfigType::kEap;
    return eap;
  }

  DictType* vpn = network_config.FindDict(::onc::network_config::kVPN);
  if (vpn) {
    DictType* openvpn = vpn->FindDict(::onc::vpn::kOpenVPN);
    DictType* ipsec = vpn->FindDict(::onc::vpn::kIPsec);
    DictType* l2tp = vpn->FindDict(::onc::vpn::kL2TP);
    if (openvpn) {
      if (out_config_type)
        *out_config_type = ConfigType::kOpenVpn;
      return openvpn;
    }
    if (ipsec) {
      // Currently we support two kinds of IPsec-based VPN:
      // - L2TP/IPsec: IKE version is 1 and |l2tp| is set;
      // - IKEv2: IKE version is 2 and |l2tp| is not set.
      // Thus we only use |l2tp| to distinguish between these two cases.
      if (out_config_type)
        *out_config_type = l2tp ? ConfigType::kL2tpIpsec : ConfigType::kIkev2;
      return ipsec;
    }
  }

  return nullptr;
}

void OncToClientCertConfig(::onc::ONCSource onc_source,
                           const base::Value::Dict& network_config,
                           ClientCertConfig* cert_config) {
  *cert_config = ClientCertConfig();

  const base::Value::Dict* dict_with_client_cert =
      GetOncClientCertConfigDict(network_config, &(cert_config->location));
  if (dict_with_client_cert) {
    GetClientCertTypeAndDescriptor(onc_source, *dict_with_client_cert,
                                   cert_config);
  }
}

void SetResolvedCertInOnc(const ResolvedCert& resolved_cert,
                          base::Value::Dict& network_config) {
  if (resolved_cert.status() == ResolvedCert::Status::kNotKnownYet)
    return;

  base::Value::Dict* dict_with_client_cert =
      GetOncClientCertConfigDict(network_config, /*out_config_type=*/nullptr);
  if (!dict_with_client_cert)
    return;
  dict_with_client_cert->Set(::onc::client_cert::kClientCertType,
                             ::onc::client_cert::kPKCS11Id);
  if (resolved_cert.status() == ResolvedCert::Status::kNothingMatched) {
    // Empty PKCS11Id means that no certificate has been selected and it
    // should be cleared in shill.
    dict_with_client_cert->Set(::onc::client_cert::kClientCertPKCS11Id,
                               std::string());
  } else {
    dict_with_client_cert->Set(
        ::onc::client_cert::kClientCertPKCS11Id,
        base::StringPrintf("%i:%s", resolved_cert.slot_id(),
                           resolved_cert.pkcs11_id().c_str()));
  }
  dict_with_client_cert->Remove(::onc::client_cert::kClientCertPattern);
}

}  // namespace ash::client_cert