791 lines
28 KiB
C++
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
|