Files
safetynet/apps/network-broker/native/signed-manifest.h
T

791 lines
28 KiB
C++

// Strict bounded native signed-release verification. No signer key in
// envelopes.
#pragma once
#include <algorithm>
#include <cctype>
#include <map>
#include <set>
#include <stdexcept>
#include <wincrypt.h>
#include <ws2tcpip.h>
#pragma comment(lib, "crypt32.lib")
#pragma comment(lib, "ws2_32.lib")
namespace release_contract {
struct Json {
enum Kind { String, Integer, Object, Array } kind;
std::string string;
ULONGLONG integer = 0;
std::map<std::string, Json> object;
std::vector<Json> array;
};
struct Parser {
const std::string &text;
size_t offset = 0, nodes = 0, stringCap = 16384;
[[noreturn]] void fail() {
throw std::runtime_error("fixed-manifest-invalid");
}
char take() {
if (offset >= text.size())
fail();
return text[offset++];
}
std::string string() {
if (take() != '"')
fail();
std::string value;
while (true) {
char c = take();
if (c == '"')
break;
if (static_cast<unsigned char>(c) < 32)
fail();
if (c == '\\') {
char escaped = take();
switch (escaped) {
case '"':
c = '"';
break;
case '\\':
c = '\\';
break;
case 'b':
c = '\b';
break;
case 'f':
c = '\f';
break;
case 'n':
c = '\n';
break;
case 'r':
c = '\r';
break;
case 't':
c = '\t';
break;
case 'u': {
unsigned code = 0;
for (int i = 0; i < 4; i++) {
char h = take();
if (h >= '0' && h <= '9')
code = code * 16 + unsigned(h - '0');
else if (h >= 'a' && h <= 'f')
code = code * 16 + unsigned(h - 'a' + 10);
else
fail();
}
if (code > 127)
fail();
c = char(code);
break;
}
default:
fail();
}
}
if (static_cast<unsigned char>(c) > 127 || value.size() >= stringCap)
fail();
value += c;
}
return value;
}
Json parse(int depth = 0) {
if (depth > 8 || ++nodes > 100000 || offset >= text.size())
fail();
Json result;
char c = text[offset];
if (c == '"') {
result.kind = Json::String;
result.string = string();
return result;
}
if (c >= '0' && c <= '9') {
result.kind = Json::Integer;
size_t start = offset;
while (offset < text.size() && text[offset] >= '0' &&
text[offset] <= '9') {
result.integer = result.integer * 10 + ULONGLONG(text[offset++] - '0');
if (result.integer > 9007199254740991ULL)
fail();
}
if (offset - start > 1 && text[start] == '0')
fail();
return result;
}
if (c == '{') {
result.kind = Json::Object;
offset++;
if (offset < text.size() && text[offset] == '}') {
offset++;
return result;
}
while (true) {
std::string key = string();
if (take() != ':' || result.object.count(key))
fail();
result.object.emplace(key, parse(depth + 1));
char delimiter = take();
if (delimiter == '}')
break;
if (delimiter != ',')
fail();
}
return result;
}
if (c == '[') {
result.kind = Json::Array;
offset++;
if (offset < text.size() && text[offset] == ']') {
offset++;
return result;
}
while (true) {
result.array.push_back(parse(depth + 1));
char delimiter = take();
if (delimiter == ']')
break;
if (delimiter != ',')
fail();
}
return result;
}
fail();
}
};
static std::string escaped(const std::string &input) {
std::string output = "\"";
for (unsigned char c : input) {
switch (c) {
case '"':
output += "\\\"";
break;
case '\\':
output += "\\\\";
break;
case '\b':
output += "\\b";
break;
case '\f':
output += "\\f";
break;
case '\n':
output += "\\n";
break;
case '\r':
output += "\\r";
break;
case '\t':
output += "\\t";
break;
default:
if (c < 32) {
char bytes[7]{};
sprintf_s(bytes, 7, "\\u%04x", unsigned(c));
output += bytes;
} else
output += char(c);
}
}
return output + '"';
}
static std::string canonical(const Json &value) {
if (value.kind == Json::String)
return escaped(value.string);
if (value.kind == Json::Integer)
return std::to_string(value.integer);
std::string result;
bool first = true;
if (value.kind == Json::Object) {
result = "{";
for (const auto &pair : value.object) {
if (!first)
result += ',';
first = false;
result += escaped(pair.first) + ':' + canonical(pair.second);
}
return result + '}';
}
result = "[";
for (const auto &item : value.array) {
if (!first)
result += ',';
first = false;
result += canonical(item);
}
return result + ']';
}
static Json json(const std::string &input, size_t maximum = 2097152,
size_t stringCap = 16384) {
if (input.empty() || input.size() > maximum)
throw std::runtime_error("fixed-manifest-invalid");
Parser parser{input, 0, 0, stringCap};
Json value = parser.parse();
if (parser.offset != input.size() || canonical(value) != input)
parser.fail();
return value;
}
static bool exact(const Json &value,
std::initializer_list<const char *> names) {
if (value.kind != Json::Object || value.object.size() != names.size())
return false;
for (auto name : names)
if (!value.object.count(name))
return false;
return true;
}
static const std::string &str(const Json &value) {
if (value.kind != Json::String)
throw std::runtime_error("fixed-manifest-invalid");
return value.string;
}
static ULONGLONG number(const Json &value) {
if (value.kind != Json::Integer || !value.integer)
throw std::runtime_error("fixed-manifest-invalid");
return value.integer;
}
static std::vector<BYTE> unhex(const std::string &input) {
if (input.empty() || input.size() % 2)
throw std::runtime_error("fixed-manifest-invalid");
std::vector<BYTE> result;
auto digit = [](char c) {
if (c >= '0' && c <= '9')
return int(c - '0');
if (c >= 'a' && c <= 'f')
return int(c - 'a' + 10);
throw std::runtime_error("fixed-manifest-invalid");
};
for (size_t i = 0; i < input.size(); i += 2)
result.push_back(BYTE(digit(input[i]) * 16 + digit(input[i + 1])));
return result;
}
static std::string digest(const std::string &text) {
BCRYPT_ALG_HANDLE alg = nullptr;
BCRYPT_HASH_HANDLE hash = nullptr;
std::array<BYTE, 32> bytes{};
bool good = false;
if (BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, nullptr, 0) >=
0 &&
BCryptCreateHash(alg, &hash, nullptr, 0, nullptr, 0, 0) >= 0)
good = BCryptHashData(
hash, reinterpret_cast<PUCHAR>(const_cast<char *>(text.data())),
ULONG(text.size()), 0) >= 0 &&
BCryptFinishHash(hash, bytes.data(), 32, 0) >= 0;
if (hash)
BCryptDestroyHash(hash);
if (alg)
BCryptCloseAlgorithmProvider(alg, 0);
if (!good)
throw std::runtime_error("fixed-manifest-invalid");
char output[65]{};
for (size_t i = 0; i < 32; i++)
sprintf_s(output + i * 2, 3, "%02x", bytes[i]);
return output;
}
static std::string base64url(const std::vector<BYTE> &bytes) {
DWORD size = 0;
if (!CryptBinaryToStringA(bytes.data(), DWORD(bytes.size()),
CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr,
&size))
throw std::runtime_error("fixed-manifest-invalid");
std::string text(size, '\0');
if (!CryptBinaryToStringA(bytes.data(), DWORD(bytes.size()),
CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF,
text.data(), &size))
throw std::runtime_error("fixed-manifest-invalid");
text.resize(size);
while (!text.empty() && (text.back() == '\0' || text.back() == '='))
text.pop_back();
std::replace(text.begin(), text.end(), '+', '-');
std::replace(text.begin(), text.end(), '/', '_');
return text;
}
static std::vector<BYTE> decode(const std::string &input) {
if (input.empty() || input.size() > 3000000)
throw std::runtime_error("fixed-manifest-invalid");
std::string text = input;
for (char &c : text) {
if (c == '-')
c = '+';
else if (c == '_')
c = '/';
else if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') &&
!(c >= '0' && c <= '9'))
throw std::runtime_error("fixed-manifest-invalid");
}
while (text.size() % 4)
text += '=';
DWORD size = 0;
if (!CryptStringToBinaryA(text.c_str(), DWORD(text.size()),
CRYPT_STRING_BASE64, nullptr, &size, nullptr,
nullptr) ||
size > 2097152)
throw std::runtime_error("fixed-manifest-invalid");
std::vector<BYTE> result(size);
if (!CryptStringToBinaryA(text.c_str(), DWORD(text.size()),
CRYPT_STRING_BASE64, result.data(), &size, nullptr,
nullptr) ||
base64url(result) != input)
throw std::runtime_error("fixed-manifest-invalid");
return result;
}
static bool verify(const std::vector<BYTE> &xy, const std::string &payload,
const std::vector<BYTE> &signature,
const std::string &domain) {
if (xy.size() != 64 || signature.size() != 64)
return false;
BCRYPT_ALG_HANDLE algorithm = nullptr, sha = nullptr;
BCRYPT_KEY_HANDLE key = nullptr;
BCRYPT_HASH_HANDLE hash = nullptr;
bool okay = false;
if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_ECDSA_P256_ALGORITHM,
nullptr, 0) < 0)
return false;
BCRYPT_ECCKEY_BLOB header{BCRYPT_ECDSA_PUBLIC_P256_MAGIC, 32};
std::vector<BYTE> blob(sizeof(header) + 64);
memcpy(blob.data(), &header, sizeof(header));
memcpy(blob.data() + sizeof(header), xy.data(), 64);
if (BCryptImportKeyPair(algorithm, nullptr, BCRYPT_ECCPUBLIC_BLOB, &key,
blob.data(), DWORD(blob.size()), 0) >= 0 &&
BCryptOpenAlgorithmProvider(&sha, BCRYPT_SHA256_ALGORITHM, nullptr, 0) >=
0 &&
BCryptCreateHash(sha, &hash, nullptr, 0, nullptr, 0, 0) >= 0) {
std::array<BYTE, 32> digest{};
if (BCryptHashData(
hash, reinterpret_cast<PUCHAR>(const_cast<char *>(domain.data())),
ULONG(domain.size()), 0) >= 0 &&
BCryptHashData(
hash, reinterpret_cast<PUCHAR>(const_cast<char *>(payload.data())),
ULONG(payload.size()), 0) >= 0 &&
BCryptFinishHash(hash, digest.data(), 32, 0) >= 0)
okay = BCryptVerifySignature(key, nullptr, digest.data(), 32,
const_cast<PUCHAR>(signature.data()), 64,
0) >= 0;
}
if (hash)
BCryptDestroyHash(hash);
if (sha)
BCryptCloseAlgorithmProvider(sha, 0);
if (key)
BCryptDestroyKey(key);
BCryptCloseAlgorithmProvider(algorithm, 0);
return okay;
}
static std::string read(const std::wstring &file) {
Handle input(CreateFileW(file.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT,
nullptr));
if (input.h == INVALID_HANDLE_VALUE)
throw std::runtime_error("fixed-manifest-invalid");
FILE_ATTRIBUTE_TAG_INFO info{};
LARGE_INTEGER size{};
if (!GetFileInformationByHandleEx(input, FileAttributeTagInfo, &info,
sizeof(info)) ||
(info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) ||
!GetFileSizeEx(input, &size) || size.QuadPart < 1 ||
size.QuadPart > 3000000)
throw std::runtime_error("fixed-manifest-invalid");
std::string content(size_t(size.QuadPart), '\0');
DWORD bytes = 0;
if (!ReadFile(input, content.data(), DWORD(content.size()), &bytes,
nullptr) ||
bytes != content.size())
throw std::runtime_error("fixed-manifest-invalid");
return content;
}
static Json signedPayload(const std::wstring &file, const std::vector<BYTE> &xy,
const std::string &domain, size_t maximum = 2097152) {
Json envelope = json(read(file), 3000000, 3000000);
if (!exact(envelope, {"payload", "signature"}))
throw std::runtime_error("fixed-manifest-invalid");
auto bytes = decode(str(envelope.object.at("payload"))),
signature = decode(str(envelope.object.at("signature")));
if (bytes.size() > maximum)
throw std::runtime_error("fixed-manifest-invalid");
std::string payload(bytes.begin(), bytes.end());
if (!verify(xy, payload, signature, domain))
throw std::runtime_error("fixed-manifest-invalid");
return json(payload);
}
static bool uuid(const std::string &value) {
if (value.size() != 36)
return false;
for (size_t i = 0; i < value.size(); i++) {
if (i == 8 || i == 13 || i == 18 || i == 23) {
if (value[i] != '-')
return false;
} else if (!((value[i] >= 'a' && value[i] <= 'f') ||
(value[i] >= '0' && value[i] <= '9')))
return false;
}
return true;
}
static bool relative(const std::string &value) {
if (value.empty() || value.size() > 240 || value.front() == '/' ||
value.back() == '/')
return false;
size_t start = 0;
for (size_t i = 0; i <= value.size(); i++) {
if (i < value.size() && value[i] != '/') {
char c = value[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '.' || c == '-'))
return false;
continue;
}
std::string part = value.substr(start, i - start);
if (part.empty() || part == "." || part == ".." || part.back() == '.')
return false;
std::transform(part.begin(), part.end(), part.begin(),
[](unsigned char c) { return char(toupper(c)); });
part = part.substr(0, part.find('.'));
if (part == "CON" || part == "PRN" || part == "AUX" || part == "NUL" ||
(part.size() == 4 &&
(part.substr(0, 3) == "COM" || part.substr(0, 3) == "LPT") &&
part[3] >= '1' && part[3] <= '9'))
return false;
start = i + 1;
}
return true;
}
struct Release {
std::string releaseId, tenant, customerPem;
ULONGLONG sequence;
std::map<std::string, std::string> hashes;
};
static Release release(const std::wstring &root,
const std::vector<BYTE> &anchor, ULONGLONG floor,
const std::string &acceptedDigest = "") {
Json value =
signedPayload(root + L"\\release-envelope.json", anchor,
std::string("SafetyNet Windows release v1\0",
sizeof("SafetyNet Windows release v1\0") - 1));
if (!exact(value, {"format", "releaseId", "releaseSequence", "serviceName",
"files", "deploymentTenant", "deploymentPublicKey"}))
throw std::runtime_error("fixed-manifest-invalid");
if (str(value.object.at("format")) != "safetynet-windows-release-v1" ||
str(value.object.at("serviceName")) != "SafetyNetNetworkBroker" ||
!uuid(str(value.object.at("releaseId"))) ||
!uuid(str(value.object.at("deploymentTenant"))))
throw std::runtime_error("fixed-manifest-invalid");
Release result{str(value.object.at("releaseId")),
str(value.object.at("deploymentTenant")),
str(value.object.at("deploymentPublicKey")),
number(value.object.at("releaseSequence")),
{}};
if (!floor || result.sequence < floor ||
(result.sequence == floor && !acceptedDigest.empty() &&
digest(canonical(value)) != acceptedDigest))
throw std::runtime_error("fixed-manifest-invalid");
const Json &files = value.object.at("files");
if (files.kind != Json::Array || files.array.empty() ||
files.array.size() > 10000)
throw std::runtime_error("fixed-manifest-invalid");
std::string previous;
std::set<std::string> folded;
for (const auto &item : files.array) {
if (!exact(item, {"path", "bytes", "sha256"}))
throw std::runtime_error("fixed-manifest-invalid");
auto name = str(item.object.at("path")),
digest = str(item.object.at("sha256"));
if (!relative(name) || (!previous.empty() && name <= previous) ||
digest.size() != 64 || unhex(digest).size() != 32 ||
item.object.at("bytes").kind != Json::Integer)
throw std::runtime_error("fixed-manifest-invalid");
previous = name;
std::string lower = name;
std::transform(lower.begin(), lower.end(), lower.begin(),
[](unsigned char c) { return char(tolower(c)); });
if (!folded.insert(lower).second)
throw std::runtime_error("fixed-manifest-invalid");
result.hashes.emplace(name, digest);
}
for (const char *required :
{"SafetyNet.exe", "safetynet-service-host.exe", "resources/app.asar",
"resources/broker/node.exe", "resources/python/python.exe",
"resources/inspector/server.py"})
if (!result.hashes.count(required))
throw std::runtime_error("fixed-manifest-invalid");
bool semantic = false;
for (const auto &item : result.hashes) {
if (item.first.rfind("resources/semantic/", 0) == 0)
semantic = true;
if (item.first == "release-envelope.json" ||
item.first == "deployment-envelope.json")
throw std::runtime_error("fixed-manifest-invalid");
}
if (!semantic)
throw std::runtime_error("fixed-manifest-invalid");
return result;
}
static std::vector<BYTE> pemDer(const std::string &pem, size_t pemLimit = 4096,
DWORD derLimit = 1024) {
if (pem.size() > pemLimit)
throw std::runtime_error("fixed-manifest-invalid");
DWORD size = 0;
if (!CryptStringToBinaryA(pem.c_str(), DWORD(pem.size()),
CRYPT_STRING_BASE64HEADER, nullptr, &size, nullptr,
nullptr) ||
size > derLimit)
throw std::runtime_error("fixed-manifest-invalid");
std::vector<BYTE> bytes(size);
if (!CryptStringToBinaryA(pem.c_str(), DWORD(pem.size()),
CRYPT_STRING_BASE64HEADER, bytes.data(), &size,
nullptr, nullptr))
throw std::runtime_error("fixed-manifest-invalid");
std::string encoded = base64url(bytes);
std::replace(encoded.begin(), encoded.end(), '-', '+');
std::replace(encoded.begin(), encoded.end(), '_', '/');
while (encoded.size() % 4)
encoded += '=';
std::string canonicalPem = "-----BEGIN PUBLIC KEY-----\n";
for (size_t offset = 0; offset < encoded.size(); offset += 64)
canonicalPem += encoded.substr(offset, 64) + '\n';
canonicalPem += "-----END PUBLIC KEY-----\n";
if (canonicalPem != pem)
throw std::runtime_error("fixed-manifest-invalid");
return bytes;
}
static std::vector<BYTE> customerKey(const std::string &pem) {
auto bytes = pemDer(pem),
prefix = unhex("3059301306072a8648ce3d020106082a8648ce3d03010703420004");
if (bytes.size() != 91 ||
!std::equal(prefix.begin(), prefix.end(), bytes.begin()))
throw std::runtime_error("fixed-manifest-invalid");
return std::vector<BYTE>(bytes.begin() + 27, bytes.end());
}
static bool policyKey(const std::string &pem) {
auto bytes = pemDer(pem), prefix = unhex("302a300506032b6570032100");
return bytes.size() == 44 &&
std::equal(prefix.begin(), prefix.end(), bytes.begin());
}
// SPKI metadata binding only; actual ML-DSA signatures use the existing Node
// policy channel. No unknown algorithm or opaque-key fallback is accepted.
static bool managementKey(const std::string &pem) {
auto bytes = pemDer(pem, 20000, 4096),
prefix = unhex("30820a32300b060960864801650304031303820a2100");
return bytes.size() == 2614 &&
std::equal(prefix.begin(), prefix.end(), bytes.begin());
}
static std::string ipv6Canonical(const IN6_ADDR &address) {
std::array<unsigned, 8> groups{};
for (size_t i = 0; i < 8; ++i)
groups[i] =
unsigned(address.u.Byte[i * 2]) * 256 + address.u.Byte[i * 2 + 1];
size_t best = 8, longest = 1;
for (size_t i = 0; i < 8;) {
if (groups[i]) {
++i;
continue;
}
size_t start = i;
while (i < 8 && !groups[i])
++i;
if (i - start > longest) {
best = start;
longest = i - start;
}
}
std::string output;
for (size_t i = 0; i < 8;) {
if (i == best) {
output += "::";
i += longest;
continue;
}
if (!output.empty() && output.back() != ':')
output += ':';
char hex[5]{};
sprintf_s(hex, "%x", groups[i]);
output += hex;
++i;
}
return output;
}
static bool managementOrigin(const std::string &origin) {
if (origin.size() > 2048 || origin.rfind("https://", 0) != 0)
return false;
std::string authority = origin.substr(8), host, port;
for (unsigned char c : authority)
if (c <= 32 || c >= 127 || c == '/' || c == '?' || c == '#' || c == '@' ||
c == '\\' || c == '%')
return false;
if (authority.empty())
return false;
if (authority[0] == '[') {
auto end = authority.find(']');
if (end == std::string::npos)
return false;
host = authority.substr(1, end - 1);
IN6_ADDR address{};
if (InetPtonA(AF_INET6, host.c_str(), &address) != 1 ||
ipv6Canonical(address) != host)
return false;
if (end + 1 < authority.size()) {
if (authority[end + 1] != ':')
return false;
port = authority.substr(end + 2);
if (port.empty())
return false;
}
} else {
auto colon = authority.find(':');
host = authority.substr(0, colon);
if (colon != std::string::npos) {
port = authority.substr(colon + 1);
if (port.empty())
return false;
}
if (host.empty() || host.size() > 253)
return false;
IN_ADDR address{};
if (InetPtonA(AF_INET, host.c_str(), &address) == 1) {
const BYTE *bytes = reinterpret_cast<const BYTE *>(&address);
std::string ip =
std::to_string(bytes[0]) + "." + std::to_string(bytes[1]) + "." +
std::to_string(bytes[2]) + "." + std::to_string(bytes[3]);
if (ip != host)
return false;
} else {
bool numeric = true;
for (char c : host)
if (!(c >= '0' && c <= '9') && c != '.')
numeric = false;
if (numeric)
return false;
size_t start = 0;
while (start < host.size()) {
auto end = host.find('.', start);
if (end == std::string::npos)
end = host.size();
auto label = host.substr(start, end - start);
if (label.empty() || label.size() > 63 || label.front() == '-' ||
label.back() == '-')
return false;
for (char c : label)
if (!(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '-')
return false;
if (label.rfind("0x", 0) == 0 && label.size() > 2 &&
std::all_of(label.begin() + 2, label.end(), [](char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
}))
return false;
if (end == host.size())
break;
start = end + 1;
if (start == host.size())
return false;
}
}
}
if (!port.empty()) {
if (port.size() > 5 || port[0] == '0')
return false;
unsigned number = 0;
for (char c : port) {
if (c < '0' || c > '9')
return false;
number = number * 10 + unsigned(c - '0');
}
if (!number || number > 65535 || number == 443)
return false;
}
return true;
}
struct Assignment {
std::string sid, deviceId, policyPem;
ULONGLONG minimumVersion, deploymentSequence;
std::string managementOrigin;
};
static Assignment deployment(const std::wstring &root, const Release &release,
const std::string &sid, ULONGLONG floor,
const std::string &acceptedDigest = "") {
Json value = signedPayload(
root + L"\\deployment-envelope.json", customerKey(release.customerPem),
std::string("SafetyNet Windows deployment v1\0",
sizeof("SafetyNet Windows deployment v1\0") - 1),
262144);
const auto format = str(value.object.at("format"));
const bool v2 = format == "safetynet-windows-deployment-v2";
if ((!v2 && format != "safetynet-windows-deployment-v1") ||
(v2 ? !exact(value, {"format", "releaseId", "deploymentTenant",
"deploymentSequence", "policyPublicKey", "expiresAt",
"users", "management"})
: !exact(value, {"format", "releaseId", "deploymentTenant",
"deploymentSequence", "policyPublicKey", "expiresAt",
"users"})) ||
str(value.object.at("releaseId")) != release.releaseId ||
str(value.object.at("deploymentTenant")) != release.tenant || !floor ||
number(value.object.at("deploymentSequence")) < floor)
throw std::runtime_error("fixed-manifest-invalid");
if (number(value.object.at("deploymentSequence")) == floor &&
!acceptedDigest.empty() && digest(canonical(value)) != acceptedDigest)
throw std::runtime_error("fixed-manifest-invalid");
if (v2) {
const auto &management = value.object.at("management");
if (!exact(management, {"origin", "signatureKey", "protocol"}) ||
!managementOrigin(str(management.object.at("origin"))) ||
str(management.object.at("protocol")) !=
"safetynet-policy-mlkem1024-mldsa87-v1" ||
!managementKey(str(management.object.at("signatureKey"))))
throw std::runtime_error("fixed-manifest-invalid");
}
FILETIME now;
GetSystemTimeAsFileTime(&now);
ULARGE_INTEGER time{};
time.LowPart = now.dwLowDateTime;
time.HighPart = now.dwHighDateTime;
ULONGLONG millis = time.QuadPart / 10000 - 11644473600000ULL;
if (number(value.object.at("expiresAt")) <= millis)
throw std::runtime_error("fixed-manifest-invalid");
const auto &users = value.object.at("users");
if (users.kind != Json::Array || users.array.empty() ||
users.array.size() > 1024)
throw std::runtime_error("fixed-manifest-invalid");
Assignment result{};
std::string previous;
std::set<std::string> devices;
for (const auto &user : users.array) {
if (!exact(user, {"sid", "deviceId", "minimumPolicyVersion"}))
throw std::runtime_error("fixed-manifest-invalid");
auto userSid = str(user.object.at("sid")),
device = str(user.object.at("deviceId"));
PSID parsed = nullptr;
if (!ConvertStringSidToSidA(userSid.c_str(), &parsed))
throw std::runtime_error("fixed-manifest-invalid");
LPSTR canonicalSid = nullptr;
bool valid = ConvertSidToStringSidA(parsed, &canonicalSid) &&
canonicalSid && userSid == canonicalSid;
if (canonicalSid)
LocalFree(canonicalSid);
LocalFree(parsed);
bool sidFamily = (userSid.rfind("S-1-5-21-", 0) == 0 ||
userSid.rfind("S-1-12-1-", 0) == 0) &&
std::count(userSid.begin(), userSid.end(), '-') == 7;
if (!valid || !sidFamily || (!previous.empty() && userSid <= previous) ||
device.empty() || device.size() > 80 ||
!isalnum(static_cast<unsigned char>(device[0])) ||
!devices.insert(device).second)
throw std::runtime_error("fixed-manifest-invalid");
for (char c : device)
if (!isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '_' &&
c != '-')
throw std::runtime_error("fixed-manifest-invalid");
previous = userSid;
ULONGLONG minimum = number(user.object.at("minimumPolicyVersion"));
if (userSid == sid)
result = {sid,
device,
str(value.object.at("policyPublicKey")),
minimum,
number(value.object.at("deploymentSequence")),
v2 ? str(value.object.at("management").object.at("origin"))
: ""};
}
if (result.sid.empty() || result.policyPem.empty() ||
!policyKey(result.policyPem))
throw std::runtime_error("fixed-manifest-invalid");
return result;
}
} // namespace release_contract