blob: eac6bf10979d0e9010b88f78114e3c4abddcda05 [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shill/wifi/passpoint_credentials.h"
#include <string>
#include <vector>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <chromeos/dbus/shill/dbus-constants.h>
#include <uuid/uuid.h>
#include "shill/data_types.h"
#include "shill/dbus/dbus_control.h"
#include "shill/eap_credentials.h"
#include "shill/error.h"
#include "shill/profile.h"
#include "shill/refptr_types.h"
#include "shill/store/key_value_store.h"
#include "shill/store/store_interface.h"
#include "shill/supplicant/wpa_supplicant.h"
namespace shill {
namespace {
// Retrieve the list of OIs encoded as decimal strings from the given DBus
// property dictionary |args| (as a shill's KeyValueStore), convert them to
// uint64 values and add them to |parsed_ois|. If a string-to-number conversion
// error happens, populate |error| and return false.
bool ParsePasspointOiList(const KeyValueStore& args,
const std::string& property,
std::vector<uint64_t>* parsed_ois,
Error* error) {
const auto raw_ois = args.Lookup<std::vector<std::string>>(property, {});
for (const auto& raw_oi : raw_ois) {
uint64_t oi;
if (!base::StringToUint64(raw_oi, &oi)) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"invalid " + property + " list: \"" + raw_oi +
"\" was not a valid decimal string");
parsed_ois->clear();
return false;
}
parsed_ois->push_back(oi);
}
return true;
}
} // namespace
// Size of an UUID string.
constexpr size_t kUUIDStringLength = 37;
PasspointCredentials::PasspointCredentials(
const std::string& id,
const std::vector<std::string>& domains,
const std::string& realm,
const std::vector<uint64_t>& home_ois,
const std::vector<uint64_t>& required_home_ois,
const std::vector<uint64_t>& roaming_consortia,
bool metered_override,
const std::string& android_package_name)
: domains_(domains),
realm_(realm),
home_ois_(home_ois),
required_home_ois_(required_home_ois),
roaming_consortia_(roaming_consortia),
metered_override_(metered_override),
android_package_name_(android_package_name),
id_(id),
profile_(nullptr),
supplicant_id_(DBusControl::NullRpcIdentifier()) {}
bool PasspointCredentials::ToSupplicantProperties(
KeyValueStore* properties) const {
CHECK(properties);
// A set of passpoint credentials is validated at insertion time in Shill,
// it is expected to be valid now.
CHECK(!domains_.empty() && !domains_[0].empty());
CHECK(!realm_.empty());
if (domains_.size() > 1) {
// TODO(b/162105998) add support for multiple domains in wpa_supplicant
// D-Bus interface.
LOG(WARNING) << "Passpoint credentials does not support multiple domains "
<< "yet, only the first one will be used.";
}
properties->Set<std::string>(WPASupplicant::kCredentialsPropertyDomain,
domains_[0]);
properties->Set<std::string>(WPASupplicant::kCredentialsPropertyRealm,
realm_);
// As supplicant lacks the support for matching multiple Home Organization
// Identifiers (Home OIs), we need to handle them carefully. It leads to two
// different situations:
// - "required" Home OIs: only one OI is supported. If there's more, we
// can't use the credentials or we would take the risk to match with
// networks we're not supposed to (see ยง9.1.2 from the specification).
// - there's no "required" Home OIs: we take the first one of the OIs
// list, but we may miss some matches.
// The full OIs lists are stored, so we'll be able add the support to
// supplicant later without breaking the credentials providers (apps, ...).
if (!required_home_ois_.empty()) {
if (required_home_ois_.size() > 1) {
// TODO(b/162105998) add support for multiple Home OIs in wpa_supplicant.
LOG(ERROR) << "Passpoint credentials does not support multiple "
<< "required Home OIs yet (" << required_home_ois_.size()
<< " found).";
properties->Clear();
return false;
}
properties->Set<std::string>(
WPASupplicant::kCredentialsPropertyRequiredRoamingConsortium,
EncodeOI(required_home_ois_[0]));
} else if (!home_ois_.empty()) {
if (home_ois_.size() > 1) {
// TODO(b/162105998) add support for multiple Home OIs in wpa_supplicant.
LOG(WARNING) << "Passpoint credentials does not support multiple "
<< "Home OIs yet, only the first one will be used.";
}
properties->Set<std::string>(
WPASupplicant::kCredentialsPropertyRoamingConsortium,
EncodeOI(home_ois_[0]));
}
if (!roaming_consortia_.empty()) {
properties->Set<std::string>(
WPASupplicant::kCredentialsPropertyRoamingConsortiums,
EncodeOIList(roaming_consortia_));
}
// Supplicant requires the EAP method for interworking selection.
properties->Set<std::string>(WPASupplicant::kNetworkPropertyEapEap,
eap_.method());
// Supplicant requires the credentials to perform matches using the realm
// (see b/225170348).
if (eap_.method() == kEapMethodTLS) {
properties->Set<std::string>(WPASupplicant::kNetworkPropertyEapCertId,
eap_.cert_id());
properties->Set<std::string>(WPASupplicant::kNetworkPropertyEapKeyId,
eap_.key_id());
} else if (eap_.method() == kEapMethodTTLS) {
properties->Set<std::string>(WPASupplicant::kCredentialsPropertyUsername,
eap_.identity());
properties->Set<std::string>(WPASupplicant::kCredentialsPropertyPassword,
eap_.password());
} else {
LOG(ERROR) << "Passpoint credentials does not support EAP method '"
<< eap_.method() << "'";
properties->Clear();
return false;
}
return true;
}
void PasspointCredentials::Load(const StoreInterface* storage) {
CHECK(storage);
CHECK(!id_.empty());
storage->GetStringList(id_, kStorageDomains, &domains_);
storage->GetString(id_, kStorageRealm, &realm_);
storage->GetUint64List(id_, kStorageHomeOIs, &home_ois_);
storage->GetUint64List(id_, kStorageRequiredHomeOIs, &required_home_ois_);
storage->GetUint64List(id_, kStorageRoamingConsortia, &roaming_consortia_);
storage->GetBool(id_, kStorageMeteredOverride, &metered_override_);
storage->GetString(id_, kStorageAndroidPackageName, &android_package_name_);
eap_.Load(storage, id_);
}
bool PasspointCredentials::Save(StoreInterface* storage) {
CHECK(storage);
CHECK(!id_.empty());
// The credentials identifier is unique, we can use it as storage identifier.
storage->SetString(id_, kStorageType, kTypePasspoint);
storage->SetStringList(id_, kStorageDomains, domains_);
storage->SetString(id_, kStorageRealm, realm_);
storage->SetUint64List(id_, kStorageHomeOIs, home_ois_);
storage->SetUint64List(id_, kStorageRequiredHomeOIs, required_home_ois_);
storage->SetUint64List(id_, kStorageRoamingConsortia, roaming_consortia_);
storage->SetBool(id_, kStorageMeteredOverride, metered_override_);
storage->SetString(id_, kStorageAndroidPackageName, android_package_name_);
eap_.Save(storage, id_, /*save_credentials=*/true);
return true;
}
std::string PasspointCredentials::GenerateIdentifier() {
uuid_t uuid_bytes;
uuid_generate_random(uuid_bytes);
std::string uuid(kUUIDStringLength, '\0');
uuid_unparse(uuid_bytes, &uuid[0]);
// Remove the null terminator from the string.
uuid.resize(kUUIDStringLength - 1);
return uuid;
}
PasspointCredentialsRefPtr PasspointCredentials::CreatePasspointCredentials(
const KeyValueStore& args, Error* error) {
std::vector<std::string> domains;
std::string realm;
std::vector<uint64_t> home_ois, required_home_ois, roaming_consortia;
bool metered_override;
std::string android_package_name;
domains = args.Lookup<std::vector<std::string>>(
kPasspointCredentialsDomainsProperty, std::vector<std::string>());
if (domains.empty()) {
Error::PopulateAndLog(
FROM_HERE, error, Error::kInvalidArguments,
"at least one FQDN is required in " +
std::string(kPasspointCredentialsDomainsProperty));
return nullptr;
}
for (const auto& domain : domains) {
if (!EapCredentials::ValidDomainSuffixMatch(domain)) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"domain '" + domain + "' is not a valid FQDN");
return nullptr;
}
}
if (!args.Contains<std::string>(kPasspointCredentialsRealmProperty)) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
std::string(kPasspointCredentialsRealmProperty) +
" property is mandatory");
return nullptr;
}
realm = args.Get<std::string>(kPasspointCredentialsRealmProperty);
if (!EapCredentials::ValidDomainSuffixMatch(realm)) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"realm '" + realm + "' is not a valid FQDN");
return nullptr;
}
if (!ParsePasspointOiList(args, kPasspointCredentialsHomeOIsProperty,
&home_ois, error)) {
return nullptr;
}
if (!ParsePasspointOiList(args, kPasspointCredentialsRequiredHomeOIsProperty,
&required_home_ois, error)) {
return nullptr;
}
if (!ParsePasspointOiList(args, kPasspointCredentialsRoamingConsortiaProperty,
&roaming_consortia, error)) {
return nullptr;
}
metered_override =
args.Lookup<bool>(kPasspointCredentialsMeteredOverrideProperty, false);
android_package_name = args.Lookup<std::string>(
kPasspointCredentialsAndroidPackageNameProperty, std::string());
// Create the set of credentials with a unique identifier.
std::string id = GenerateIdentifier();
PasspointCredentialsRefPtr creds = new PasspointCredentials(
id, domains, realm, home_ois, required_home_ois, roaming_consortia,
metered_override, android_package_name);
// Load EAP credentials from the set of properties.
creds->eap_.Load(args);
// Server authentication: if the caller specify a CA certificate, disable
// system CAs. Otherwise, verify that with the trusted system CAs an
// alternative name match list is specified or that a subject name match and a
// domain suffix match list are specified.
if (!creds->eap_.ca_cert_pem().empty()) {
creds->eap_.set_use_system_cas(false);
} else {
creds->eap_.set_use_system_cas(true);
bool noNameMatch = creds->eap_.subject_match().empty();
bool noAltnameMatchList =
creds->eap_.subject_alternative_name_match_list().empty();
bool noSuffixMatchList = creds->eap_.domain_suffix_match_list().empty();
if (noAltnameMatchList && (noNameMatch || noSuffixMatchList)) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"EAP credentials with no CA certificate must have "
"a Subject Alternative Name match list");
return nullptr;
}
}
// Check the set of credentials is consistent.
if (!creds->eap().IsConnectable()) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"EAP credendials not connectable");
return nullptr;
}
// Our Passpoint implementation only supports EAP TLS or TTLS. SIM based EAP
// methods are not supported on ChromeOS yet.
std::string method = creds->eap().method();
if (method != kEapMethodTLS && method != kEapMethodTTLS) {
Error::PopulateAndLog(
FROM_HERE, error, Error::kInvalidArguments,
"EAP method '" + method + "' is not supported by Passpoint");
return nullptr;
}
// The only valid inner EAP method for TTLS is MSCHAPv2
std::string inner_method = creds->eap().inner_method();
if (method == kEapMethodTTLS && inner_method != kEapPhase2AuthTTLSMSCHAPV2) {
Error::PopulateAndLog(FROM_HERE, error, Error::kInvalidArguments,
"TTLS inner EAP method '" + inner_method +
"' is not supported by Passpoint");
return nullptr;
}
return creds;
}
std::string PasspointCredentials::GetFQDN() {
if (domains_.empty())
return std::string();
return domains_[0];
}
std::string PasspointCredentials::GetOrigin() {
return android_package_name_;
}
// static
std::string PasspointCredentials::EncodeOI(uint64_t oi) {
static const char kHexChars[] = "0123456789ABCDEF";
// Each input byte creates two output hex characters.
static const size_t size = sizeof(uint64_t) * 2;
std::string ret(size, '\0');
size_t i = size;
// wpa_supplicant expects an even number of char as a byte is filled by two
// of them.
do {
ret[--i] = kHexChars[oi & 0x0f];
ret[--i] = kHexChars[(oi & 0xf0) >> 4];
oi = oi >> 8;
} while (oi > 0);
return ret.substr(i);
}
// static
std::string PasspointCredentials::EncodeOIList(
const std::vector<uint64_t>& ois) {
std::vector<std::string> strings;
for (const auto& oi : ois) {
strings.push_back(EncodeOI(oi));
}
return base::JoinString(strings, ",");
}
} // namespace shill