blob: a405caaad0a432d360be21ce32d3c56f46292841 [file] [log] [blame]
// Copyright 2021 The Chromium 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 "device/fido/filter.h"
#include "base/feature_list.h"
#include "base/json/json_reader.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
#include "base/strings/pattern.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "components/device_event_log/device_event_log.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
namespace device {
namespace fido_filter {
namespace {
const base::Feature kFilter{"WebAuthenticationFilter",
base::FEATURE_DISABLED_BY_DEFAULT};
const base::FeatureParam<std::string> kFilterJSON{
&kFilter,
"json",
"",
};
struct FilterStep {
absl::optional<std::string> operation;
std::vector<std::string> rp_id;
absl::optional<std::string> device;
absl::optional<std::string> id_type;
std::vector<std::string> id;
absl::optional<size_t> id_min_size;
absl::optional<size_t> id_max_size;
Action action;
};
bool IsString(const base::Value& v) {
return v.is_string();
}
bool IsNonEmptyString(const base::Value& v) {
return v.is_string() && !v.GetString().empty();
}
bool IsListOf(const base::Value* v, bool (*predicate)(const base::Value&)) {
if (!v->is_list()) {
return false;
}
auto contents = v->GetListDeprecated();
return !contents.empty() &&
std::all_of(contents.begin(), contents.end(), predicate);
}
std::vector<std::string> GetStringOrListOfStrings(const base::Value* v) {
if (v->is_string()) {
return {v->GetString()};
}
std::vector<std::string> ret;
for (const auto& elem : v->GetListDeprecated()) {
ret.push_back(elem.GetString());
}
return ret;
}
absl::optional<std::vector<FilterStep>> ParseJSON(base::StringPiece json) {
absl::optional<base::Value> v =
base::JSONReader::Read(json, base::JSON_ALLOW_TRAILING_COMMAS);
if (!v || !v->is_dict()) {
return absl::nullopt;
}
const base::Value* filters = v->FindKey("filters");
if (!filters || !filters->is_list()) {
return absl::nullopt;
}
std::vector<FilterStep> ret;
const auto filter_list = filters->GetListDeprecated();
for (const auto& filter : filter_list) {
if (!filter.is_dict()) {
return absl::nullopt;
}
// These are the keys that are extracted from the JSON:
const base::Value* operation = nullptr;
const base::Value* rp_id = nullptr;
const base::Value* device = nullptr;
const base::Value* id_type = nullptr;
const base::Value* id = nullptr;
const base::Value* id_min_size = nullptr;
const base::Value* id_max_size = nullptr;
const base::Value* action = nullptr;
// DictItems is used so that unknown keys in the dictionary can be rejected.
for (auto pair : filter.DictItems()) {
if (pair.first == "operation") {
operation = &pair.second;
} else if (pair.first == "rp_id") {
rp_id = &pair.second;
} else if (pair.first == "device") {
device = &pair.second;
} else if (pair.first == "id_type") {
id_type = &pair.second;
} else if (pair.first == "id") {
id = &pair.second;
} else if (pair.first == "id_min_size") {
id_min_size = &pair.second;
} else if (pair.first == "id_max_size") {
id_max_size = &pair.second;
} else if (pair.first == "action") {
action = &pair.second;
} else {
// Unknown keys are an error.
return absl::nullopt;
}
}
if (!action || !IsNonEmptyString(*action) ||
(operation && !IsNonEmptyString(*operation)) ||
(rp_id && !IsNonEmptyString(*rp_id) &&
!IsListOf(rp_id, IsNonEmptyString)) ||
(device && !IsNonEmptyString(*device)) ||
(id_type && !IsNonEmptyString(*id_type)) ||
(id && !IsString(*id) && !IsListOf(id, IsString)) ||
(id_min_size && !id_min_size->is_int()) ||
(id_max_size && !id_max_size->is_int())) {
return absl::nullopt;
}
if ((id_min_size || id_max_size || id) && !id_type) {
// If matches on the contents or size of an ID are given then the type
// must also be matched.
return absl::nullopt;
}
if (!rp_id && !device) {
// Filter is too broad. For safety this is disallowed, although one can
// still explicitly use a wildcard.
return absl::nullopt;
}
FilterStep step;
const std::string& action_str = action->GetString();
if (action_str == "allow") {
step.action = Action::ALLOW;
} else if (action_str == "block") {
step.action = Action::BLOCK;
} else if (action_str == "no-attestation") {
step.action = Action::NO_ATTESTATION;
} else {
return absl::nullopt;
}
if (operation) {
step.operation = operation->GetString();
}
if (rp_id) {
step.rp_id = GetStringOrListOfStrings(rp_id);
}
if (device) {
step.device = device->GetString();
}
if (id_type) {
step.id_type = id_type->GetString();
}
if (id) {
step.id = GetStringOrListOfStrings(id);
}
if (id_min_size) {
const int min_size_int = id_min_size->GetInt();
if (min_size_int < 0) {
return absl::nullopt;
}
step.id_min_size = min_size_int;
}
if (id_max_size) {
const int max_size_int = id_max_size->GetInt();
if (max_size_int < 0) {
return absl::nullopt;
}
step.id_max_size = max_size_int;
}
ret.emplace_back(std::move(step));
}
return ret;
}
const char* OperationToString(Operation op) {
switch (op) {
case Operation::MAKE_CREDENTIAL:
return "mc";
case Operation::GET_ASSERTION:
return "ga";
}
}
const char* IDTypeToString(IDType id_type) {
switch (id_type) {
case IDType::CREDENTIAL_ID:
return "cred";
case IDType::USER_ID:
return "user";
}
}
size_t g_testing_depth = 0;
struct CurrentFilter {
absl::optional<std::vector<FilterStep>> steps;
absl::optional<std::string> json;
};
CurrentFilter* GetCurrentFilter() {
static base::NoDestructor<CurrentFilter> current_filter;
return current_filter.get();
}
bool MaybeParseFilter(base::StringPiece json) {
CurrentFilter* const current_filter = GetCurrentFilter();
if (current_filter->json && json == *current_filter->json) {
return true;
}
if (json.size() == 0) {
current_filter->steps.reset();
current_filter->json = "";
return true;
}
current_filter->steps = ParseJSON(json);
if (!current_filter->steps) {
current_filter->json.reset();
return false;
}
current_filter->json = std::string(json);
return true;
}
} // namespace
void MaybeInitialize() {
if (g_testing_depth != 0) {
return;
}
const std::string& json = kFilterJSON.Get();
if (!MaybeParseFilter(json)) {
FIDO_LOG(ERROR) << "Failed to parse filter JSON. Failing open.";
}
}
Action Evaluate(
Operation op,
base::StringPiece rp_id,
absl::optional<base::StringPiece> device,
absl::optional<std::pair<IDType, base::span<const uint8_t>>> id) {
CurrentFilter* const current_filter = GetCurrentFilter();
if (!current_filter->steps) {
return Action::ALLOW;
}
absl::optional<std::string> id_hex;
if (id) {
id_hex = base::HexEncode(id->second);
}
for (const auto& filter : *current_filter->steps) {
if ((!filter.operation ||
base::MatchPattern(OperationToString(op), *filter.operation)) &&
(filter.rp_id.empty() ||
std::any_of(filter.rp_id.begin(), filter.rp_id.end(),
[rp_id](const std::string& pattern) -> bool {
return base::MatchPattern(rp_id, pattern);
})) &&
(!filter.device ||
base::MatchPattern(device.value_or(""), *filter.device)) &&
(!filter.id_type || (id && base::MatchPattern(IDTypeToString(id->first),
*filter.id_type))) &&
(!filter.id_min_size ||
(id && *filter.id_min_size <= id->second.size())) &&
(!filter.id_max_size ||
(id && *filter.id_max_size >= id->second.size())) &&
(filter.id.empty() ||
(id_hex && std::any_of(filter.id.begin(), filter.id.end(),
[&id_hex](const std::string& pattern) -> bool {
return base::MatchPattern(*id_hex, pattern);
})))) {
return filter.action;
}
}
return Action::ALLOW;
}
ScopedFilterForTesting::ScopedFilterForTesting(base::StringPiece json)
: previous_json_(GetCurrentFilter()->json) {
g_testing_depth++;
CHECK(g_testing_depth != 0);
CHECK(MaybeParseFilter(json)) << json;
}
ScopedFilterForTesting::ScopedFilterForTesting(
base::StringPiece json,
ScopedFilterForTesting::PermitInvalidJSON)
: previous_json_(GetCurrentFilter()->json) {
g_testing_depth++;
CHECK(g_testing_depth != 0);
MaybeParseFilter(json);
}
ScopedFilterForTesting::~ScopedFilterForTesting() {
CurrentFilter* const current_filter = GetCurrentFilter();
current_filter->steps.reset();
current_filter->json.reset();
g_testing_depth--;
if (previous_json_) {
CHECK(MaybeParseFilter(*previous_json_));
}
}
bool ParseForTesting(base::StringPiece json) {
CHECK(base::JSONReader::Read(json, base::JSON_ALLOW_TRAILING_COMMAS)) << json;
return MaybeParseFilter(json);
}
} // namespace fido_filter
} // namespace device