115 lines
5.0 KiB
C++
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
|