Files
safetynet/apps/network-broker/native/acceptance-state.h
T

115 lines
5.0 KiB
C++

#pragma once
#include "signed-manifest.h"
// Canonical monotonic state contract. No registry operations in this header.
namespace acceptance_contract {
using release_contract::Json;
static bool sha(const std::string &value) {
if (value.size() != 64)
return false;
for (char c : value)
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')))
return false;
return true;
}
static bool userSid(const std::string &value) {
if ((value.rfind("S-1-5-21-", 0) != 0 && value.rfind("S-1-12-1-", 0) != 0) ||
std::count(value.begin(), value.end(), '-') != 7)
return false;
PSID sid = nullptr;
if (!ConvertStringSidToSidA(value.c_str(), &sid))
return false;
LPSTR text = nullptr;
bool valid = ConvertSidToStringSidA(sid, &text) && text && value == text;
if (text)
LocalFree(text);
LocalFree(sid);
return valid;
}
static std::string identity(const Json &record) {
return release_contract::str(record.object.at("sid")) + "|" +
release_contract::str(record.object.at("deviceId")) + "|" +
release_contract::str(record.object.at("policyKeySha256"));
}
static void floor(const Json &value, bool deployment) {
if (!release_contract::exact(
value,
deployment
? std::initializer_list<const char *>{"minimumSequence",
"acceptedDigest", "tenant"}
: std::initializer_list<const char *>{"minimumSequence",
"acceptedDigest"}) ||
!sha(release_contract::str(value.object.at("acceptedDigest"))) ||
!release_contract::number(value.object.at("minimumSequence")) ||
(deployment && !release_contract::uuid(
release_contract::str(value.object.at("tenant")))))
throw std::runtime_error("fixed-state-invalid");
}
static Json validate(const std::string &text) {
if (text.size() > 2096128)
throw std::runtime_error("fixed-state-invalid");
Json state = release_contract::json(text);
if (!release_contract::exact(
state, {"format", "release", "deployment", "policies"}) ||
release_contract::str(state.object.at("format")) !=
"safetynet-service-floors-v1")
throw std::runtime_error("fixed-state-invalid");
floor(state.object.at("release"), false);
floor(state.object.at("deployment"), true);
const auto &policies = state.object.at("policies");
if (policies.kind != Json::Array || policies.array.size() > 8192)
throw std::runtime_error("fixed-state-invalid");
std::string previous;
for (const auto &record : policies.array) {
if (!release_contract::exact(record,
{"sid", "deviceId", "policyKeySha256",
"minimumVersion", "acceptedDigest"}) ||
!userSid(release_contract::str(record.object.at("sid"))) ||
!sha(release_contract::str(record.object.at("policyKeySha256"))) ||
!sha(release_contract::str(record.object.at("acceptedDigest"))) ||
!release_contract::number(record.object.at("minimumVersion")))
throw std::runtime_error("fixed-state-invalid");
auto device = release_contract::str(record.object.at("deviceId"));
if (device.empty() || device.size() > 80 ||
!isalnum(static_cast<unsigned char>(device[0])))
throw std::runtime_error("fixed-state-invalid");
for (unsigned char c : device)
if (!isalnum(c) && c != '.' && c != '_' && c != '-')
throw std::runtime_error("fixed-state-invalid");
auto id = identity(record);
if (!previous.empty() && id <= previous)
throw std::runtime_error("fixed-state-invalid");
previous = id;
}
return state;
}
static bool nondecreasing(const Json &previous, const Json &candidate,
const char *field) {
auto old = release_contract::number(previous.object.at(field)),
next = release_contract::number(candidate.object.at(field));
return next >= old &&
(next != old ||
release_contract::str(previous.object.at("acceptedDigest")) ==
release_contract::str(candidate.object.at("acceptedDigest")));
}
static bool transition(const Json &old, const Json &candidate) {
if (!nondecreasing(old.object.at("release"), candidate.object.at("release"),
"minimumSequence") ||
!nondecreasing(old.object.at("deployment"),
candidate.object.at("deployment"), "minimumSequence") ||
release_contract::str(old.object.at("deployment").object.at("tenant")) !=
release_contract::str(
candidate.object.at("deployment").object.at("tenant")))
return false;
std::map<std::string, const Json *> records;
for (const auto &record : candidate.object.at("policies").array)
records.emplace(identity(record), &record);
for (const auto &record : old.object.at("policies").array) {
auto match = records.find(identity(record));
if (match == records.end() ||
!nondecreasing(record, *match->second, "minimumVersion"))
return false;
}
return true;
}
} // namespace acceptance_contract