Add packaged DLP broker and protected Windows service foundation

This commit is contained in:
Karl committed 2026-10-14 14:19:22 +02:00
1 parent 95d23f4e0b
commit 7b5057901a
140 files changed
+11215 -60

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
# Native Windows service host
This component uses Windows SCM, named pipes and Job objects around the stock
Node broker. It does not rebuild Electron/Chromium or modify installed Chrome.
It has not been installed or proven as the production network boundary.
Run `node scripts/build-windows-service-host.mjs --self-test` on Windows with
Visual Studio C++ Build Tools and SDK 10.0.26100.0. The builder uses explicit
compiler/include/library paths, /W4 /WX, static runtime, CFG, ASLR and DEP. Output
and fixed metadata evidence remain under ignored `artifacts/native-service-host`.
It changes no execution policy, service, firewall, DNS setting or user profile.
The builder accepts `--self-test` or `--compile-only`. Compile-only never launches
the executable and writes separate evidence. It bakes a disposable public key, retains
private test keys only in memory, and signs synthetic inventory fixtures after
compilation. It never creates a production release key or registers a service.
The actual negative tests verify: missing SCM service identity, unauthorized
client image, oversized frame, wrong file hash, second pipe instance denied by
its DACL, and a child plus grandchild retained in a non-breakaway kill-on-close
Job and both observed terminal after closing the Job. These are meaningful
native component tests; they do not prove an installed SCM or WFP deployment.
The fixed bridge is `\\.\pipe\SafetyNet.Broker.v1`, local-only, one authenticated
browser connection at a time. An enabled SCM service SID, protected code and
independently pinned deployment material are required before broker launch. The
client is the protected installed helper launched by the protected installed
SafetyNet.exe. Both must have matching medium/high, non-AppContainer user
tokens and sessions. Its process token and impersonated pipe token must agree
on user SID and session. The process handle remains retained and monitored;
client exit terminates the broker Job. The ACL excludes FILE_CREATE_PIPE_INSTANCE
from employee rights. No trust arises from a pipe name or claimed PID in JSON.
Frames are uint32 little-endian length followed by UTF-8 JSON. The first
parent-to-child frame is bounded to 2 MiB; subsequent frames and child responses
are 64 KiB. The constant child receives only inherited stdin/stdout handles and
`--service-stdio`; stderr goes to NUL. Seven Windows profile/runtime variables
and one fixed OS-derived `SAFETYNET_SERVICE_CLIENT_SID` are passed. No caller
environment supplies that SID. The Node adapter must validate the existing
v1 generation, monotonic sequence, typed audit/certificate receipts and pinned
service deployment authority. Framing does not authenticate policy or allow a
caller to replace its public key, device identity or version floor.
The `--client-stdio` mode connects once and authenticates the actual connected
handle's retained server process against signed release hashes, the exact
installed path and an enabled SCM service SID. It never probes and reconnects.
`--trust-snapshot` requires a service-token helper and a retained signed Node
parent. It returns bounded framed public anchor/SID/registry-floor metadata,
without accepting a registry path or trust key from its caller.
The fixed child entry is `resources/broker/apps/network-broker/service-entry.cjs`.
The prepared `--topology-snapshot` helper uses the compiled broker port 43120,
fixed installed browser/broker paths and the real SCM service SID. Service-token,
signed Node-parent and signed user-assignment checks precede its read-only BFE
enumeration. It verifies all eight owned filter keys, actions, conditions,
weights and service descriptor bytes, checks equal-priority collisions and BFE
state, and reports bounded metadata. It never accepts a plan, port or visibility
flag from IPC. The new topology source compiled, but its latest self-test launch
failed with `UNKNOWN:spawn`; compile-only evidence is separate from earlier
successful native tests. This helper has no installed execution proof yet.
Windows enumerators omit unreadable objects and notifications are asynchronous
and unreliable. Therefore the helper reports `allObjectsReadable:false` even at
successful EOF. Production admission requires an independently reviewed,
protected deployment prerequisite; token elevation or an ordinary JSON flag
cannot establish it. Ordinary-user enforcement assumes trusted administrators;
user-mode monitoring cannot promise zero packets during hostile privileged
kernel/BFE changes. See Microsoft's [access control](https://learn.microsoft.com/en-us/windows/win32/fwp/access-control)
and [notification contract](https://learn.microsoft.com/en-us/windows/win32/fwp/change-notifications).
The fixed 64-bit HKLM key `SOFTWARE\SafetyNet\Trust` contains one canonical
`AcceptanceState` REG_BINARY value, including release/deployment digests and
sorted per-user policy floors. Missing or corrupt state fails closed. The
snapshot exposes `{version, authenticatedSid, releaseAnchor, acceptanceState}`.
`--commit-acceptance` accepts only `{expectedPreviousSHA, candidateState}`,
checks a full monotonic transition under an authenticated fixed mutex, writes
one value, flushes and verifies readback before returning
`{persisted:true,stateSha256}`. State bytes are capped at 2 MiB minus 1024 bytes;
all stdio frames have a single bounded deadline. The trusted Node gate must
verify the actual signed policy before requesting a commit. Policy digests hash
the original base64url payload string; manifest digests hash canonical decoded
JSON bytes.
The prepared `--management-credential` command accepts exactly device ID,
canonical HTTPS origin, policy version, policy-key DER SHA256 and the accepted
policy-payload digest. It binds them to the native service-minted user SID,
signed v2 deployment and exact protected acceptance record. Its private reply
contains only the same fields and the token, bounded to 8 KiB. The separate fixed
key `SOFTWARE\SafetyNet\ManagementCredentials` requires a protected DACL owned
by SYSTEM/administrators, granting access only to them and the exact service SID.
Encrypted records use LocalMachine DPAPI with canonical metadata as entropy;
this is Windows encryption, not a post-quantum storage claim.
Each read first validates the exact current accepted policy request. It then
selects the unique highest stored version at or below that request for the same
SID, device, origin and policy key. Same-version digest conflicts, ambiguous
duplicates and future versions fail closed. Decryption uses the record's own
original metadata; the reply echoes the verified current request. Advancing a
policy therefore does not require re-encrypting an otherwise valid credential.
Provisioning replaces all records for that stable identity and preserves other
identities, preventing a new record for every policy revision.
Internal `--provision-management-credential` additionally requires the previous
encrypted-store SHA256 and token. It uses the same authenticated service role,
serialized mutex, bounded canonical store (256 records/1 MiB), CAS, flush and
readback before returning a receipt. It accepts no registry path, user SID or
key selector. Neither command creates keys or supplies an initial empty store.
No renderer/enrollment provisioning endpoint is exposed. Initial protected
provisioning and actual service-token DPAPI/registry execution remain unproven;
the latest native source has compile-only evidence, not execution evidence.
The mutex uses an OWNER_RIGHTS read-control ACE rather than assuming the service
SID can own objects. Actual native tests show owner WRITE_DAC denial while
synchronization and modification remain allowed. Commit source is prepared,
but no registry write, installed service positive or durable commit has been
performed. Current uninstalled snapshot/commit/client tests require exit 5.
`signed-manifest.h` verifies canonical manifests using Windows CNG P-256/SHA256
and fixed 64-byte IEEE-P1363 signatures. The baked vendor key verifies release
inventory; its delegated customer key verifies deployment and user assignment.
This is a classical release signature, not a post-quantum signature. Actual
native tests verify positive delegation and reject tampering, the wrong customer
key, expiry and equal-sequence digest mismatch. A larger signed inventory tests
real envelope parsing. These fixture tests do not attest installed resource ACLs.
Before deployment, implement and independently verify these remaining gates:
- Actual vendor signing/provisioning and complete final inventory, including
every broker, inspector, model and browser resource. The external signature
avoids circular browser/host hashes; synthetic fixtures are not a release.
- Installed positive client/server authentication and lifecycle tests; current
native negatives cannot prove a correctly provisioned SCM service.
- Protected initial acceptance-state provisioning and an actual service-token
CAS/flush/readback proof before listen. Prepared source and uninstalled
negative tests do not establish durable installed behavior.
- Real SCM provisioning under a least-privileged service identity, protected
resource/state ACLs, actual service-start/stop and bounded I/O timeout tests.
- Per-user signed assignment binding and typed management/inspector/OTLP RPC.
Interactive DPAPI audit persistence stays in the authenticated user parent.
- Transactional WFP outbound filters and receiver service-descriptor soft-permit
plus lower block in the same owned sublayer, preserving corporate veto.
- Native readback and physical IPv4/IPv6/UDP/DNS/port-takeover tests, including
another instance of the same stock Node executable and service crashes.
- Effective WFP hard-permit topology validation and BFE change monitoring;
a low-priority sublayer or a static rule inventory cannot prove enforcement.
Browser inbound-first IPv4/IPv6 TCP/UDP denies need physical receiver tests.
Until those gates pass, retain existing browser quarantine and enforcement.
@@ -0,0 +1,114 @@
#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
@@ -0,0 +1,276 @@
#pragma once
#include "acceptance-state.h"
#include <wincrypt.h>
#pragma comment(lib, "crypt32.lib")
namespace credential_contract {
using release_contract::Json;
static constexpr DWORD STORE_LIMIT = 1048576;
struct Wipe {
std::vector<BYTE> bytes;
~Wipe() {
if (!bytes.empty())
SecureZeroMemory(bytes.data(), bytes.size());
}
};
struct WipeText {
std::string &text;
~WipeText() {
if (!text.empty())
SecureZeroMemory(text.data(), text.size());
}
};
static Json parse(const std::string &text) {
release_contract::Parser parser{text};
auto result = parser.parse();
if (parser.offset != text.size())
throw 1;
return result;
}
static bool tokenValid(const std::string &token) {
if (token.size() < 16 || token.size() > 4096)
return false;
for (char c : token)
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '~' ||
c == '-'))
return false;
return true;
}
static Json binding(const Json &request,
const release_contract::Assignment &assignment,
const Json &state) {
if (assignment.managementOrigin.empty() ||
release_contract::str(request.object.at("deviceId")) !=
assignment.deviceId ||
release_contract::str(request.object.at("origin")) !=
assignment.managementOrigin)
throw 1;
auto der = release_contract::pemDer(assignment.policyPem);
const std::string keyBytes(der.begin(), der.end());
if (release_contract::str(request.object.at("policyKeySha256")) !=
release_contract::digest(keyBytes))
throw 1;
const auto version =
release_contract::number(request.object.at("policyVersion"));
const auto digest = release_contract::str(request.object.at("policyDigest"));
if (!acceptance_contract::sha(digest) || version < assignment.minimumVersion)
throw 1;
bool matched = false;
for (const auto &record : state.object.at("policies").array) {
if (release_contract::str(record.object.at("sid")) == assignment.sid &&
release_contract::str(record.object.at("deviceId")) ==
assignment.deviceId &&
release_contract::str(record.object.at("policyKeySha256")) ==
release_contract::str(request.object.at("policyKeySha256"))) {
matched =
release_contract::number(record.object.at("minimumVersion")) ==
version &&
release_contract::str(record.object.at("acceptedDigest")) == digest;
}
}
if (!matched)
throw 1;
Json result = request;
result.object.erase("token");
result.object.erase("expectedPreviousSHA");
Json sid;
sid.kind = Json::String;
sid.string = assignment.sid;
result.object["sid"] = sid;
return result;
}
static Json validateStore(const std::string &text) {
if (text.empty() || text.size() > STORE_LIMIT)
throw 1;
auto value = parse(text);
if (release_contract::canonical(value) != text ||
!release_contract::exact(value, {"format", "records"}) ||
release_contract::str(value.object.at("format")) !=
"safetynet-management-credentials-v1" ||
value.object.at("records").kind != Json::Array ||
value.object.at("records").array.size() > 256)
throw 1;
std::string previous;
for (const auto &record : value.object.at("records").array) {
if (!release_contract::exact(record, {"sid", "deviceId", "origin",
"policyVersion", "policyKeySha256",
"policyDigest", "protectedToken"}) ||
!acceptance_contract::userSid(
release_contract::str(record.object.at("sid"))) ||
!release_contract::managementOrigin(
release_contract::str(record.object.at("origin"))) ||
!acceptance_contract::sha(
release_contract::str(record.object.at("policyKeySha256"))) ||
!acceptance_contract::sha(
release_contract::str(record.object.at("policyDigest"))))
throw 1;
release_contract::number(record.object.at("policyVersion"));
const auto device = release_contract::str(record.object.at("deviceId"));
if (device.empty() || device.size() > 80 ||
!isalnum(static_cast<unsigned char>(device[0])))
throw 1;
for (char c : device)
if (!isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '_' &&
c != '-')
throw 1;
const auto cipher =
release_contract::str(record.object.at("protectedToken"));
if (cipher.empty() || cipher.size() > 16384)
throw 1;
release_contract::decode(cipher);
Json identity = record;
identity.object.erase("protectedToken");
const auto key = release_contract::canonical(identity);
if (!previous.empty() && key <= previous)
throw 1;
previous = key;
}
return value;
}
static std::string stableIdentity(Json metadata) {
metadata.object.erase("policyVersion");
metadata.object.erase("policyDigest");
metadata.object.erase("protectedToken");
return release_contract::canonical(metadata);
}
static const Json *selectRecord(const Json &metadata,
const std::vector<Json> &records) {
const auto identity = stableIdentity(metadata);
const auto current =
release_contract::number(metadata.object.at("policyVersion"));
const Json *selected = nullptr;
ULONGLONG highest = 0;
std::set<ULONGLONG> versions;
for (const auto &record : records) {
if (stableIdentity(record) != identity)
continue;
const auto version =
release_contract::number(record.object.at("policyVersion"));
if (!versions.insert(version).second)
throw 1;
if (version > current)
throw 1;
if (version == current &&
release_contract::str(record.object.at("policyDigest")) !=
release_contract::str(metadata.object.at("policyDigest")))
throw 1;
if (version > highest) {
highest = version;
selected = &record;
}
}
return selected;
}
static void replaceRecord(std::vector<Json> &records, const Json &record) {
const auto identity = stableIdentity(record);
records.erase(std::remove_if(records.begin(), records.end(),
[&](const Json &old) {
return stableIdentity(old) == identity;
}),
records.end());
records.push_back(record);
std::sort(records.begin(), records.end(), [](Json a, Json b) {
a.object.erase("protectedToken");
b.object.erase("protectedToken");
return release_contract::canonical(a) < release_contract::canonical(b);
});
}
#ifdef SAFETYNET_NATIVE_SELF_TEST
static bool selectionSelfTest() {
auto current =
parse("{\"deviceId\":\"synthetic\",\"origin\":\"https://"
"management.example\",\"policyDigest\":\"" +
std::string(64, 'b') + "\",\"policyKeySha256\":\"" +
std::string(64, 'a') +
"\",\"policyVersion\":8,\"sid\":\"S-1-5-21-1-2-3-1001\"}");
auto old = current;
old.object.at("policyVersion").integer = 7;
old.object.at("policyDigest").string = std::string(64, 'c');
auto oldest = old;
oldest.object.at("policyVersion").integer = 6;
std::vector<Json> records{oldest, old};
if (selectRecord(current, records) != &records[1])
return false;
auto other = old;
other.object.at("origin").string = "https://other.example";
records.push_back(other);
replaceRecord(records, current);
if (records.size() != 2 || !selectRecord(current, records))
return false;
auto fails = [&](std::vector<Json> rows) {
try {
selectRecord(current, rows);
return false;
} catch (...) {
return true;
}
};
auto conflicting = current;
conflicting.object.at("policyDigest").string = std::string(64, 'd');
auto future = current;
future.object.at("policyVersion").integer = 9;
if (!fails({old, old}) || !fails({conflicting}) || !fails({future}))
return false;
auto wrongKey = old;
wrongKey.object.at("policyKeySha256").string = std::string(64, 'e');
return !selectRecord(current, {other, wrongKey});
}
#endif
static std::string load(HKEY key) {
DWORD type = 0, size = 0;
if (RegQueryValueExW(key, L"ManagementCredentials", nullptr, &type, nullptr,
&size) ||
type != REG_BINARY || !size || size > STORE_LIMIT)
throw 1;
std::string value(size, '\0');
if (RegQueryValueExW(key, L"ManagementCredentials", nullptr, &type,
reinterpret_cast<BYTE *>(value.data()), &size) ||
type != REG_BINARY || size != value.size())
throw 1;
validateStore(value);
return value;
}
static std::vector<BYTE> entropy(const Json &metadata) {
const auto value =
std::string("SafetyNet management credential v1\0",
sizeof("SafetyNet management credential v1\0") - 1) +
release_contract::canonical(metadata);
auto digest = release_contract::unhex(release_contract::digest(value));
return digest;
}
static std::string protect(const std::string &token, const Json &metadata) {
if (!tokenValid(token))
throw 1;
auto bindingBytes = entropy(metadata);
Wipe plain;
plain.bytes.assign(token.begin(), token.end());
DATA_BLOB input{static_cast<DWORD>(plain.bytes.size()), plain.bytes.data()},
additional{static_cast<DWORD>(bindingBytes.size()), bindingBytes.data()},
output{};
if (!CryptProtectData(
&input, L"SafetyNet credential", &additional, nullptr, nullptr,
CRYPTPROTECT_LOCAL_MACHINE | CRYPTPROTECT_UI_FORBIDDEN, &output))
throw 1;
std::vector<BYTE> cipher(output.pbData, output.pbData + output.cbData);
LocalFree(output.pbData);
return release_contract::base64url(cipher);
}
static std::string unprotect(const std::string &cipher, const Json &metadata) {
auto encrypted = release_contract::decode(cipher),
bindingBytes = entropy(metadata);
DATA_BLOB input{static_cast<DWORD>(encrypted.size()), encrypted.data()},
additional{static_cast<DWORD>(bindingBytes.size()), bindingBytes.data()},
output{};
if (!CryptUnprotectData(&input, nullptr, &additional, nullptr, nullptr,
CRYPTPROTECT_UI_FORBIDDEN, &output))
throw 1;
std::string token(reinterpret_cast<char *>(output.pbData), output.cbData);
SecureZeroMemory(output.pbData, output.cbData);
LocalFree(output.pbData);
if (!tokenValid(token)) {
SecureZeroMemory(token.data(), token.size());
throw 1;
}
return token;
}
} // namespace credential_contract
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,790 @@
// 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
+378
View File
@@ -0,0 +1,378 @@
#pragma once
// clang-format off
#include <windows.h>
#include <sddl.h>
#include <objbase.h>
#include <fwpmu.h>
// clang-format on
#include <string>
#include <vector>
#include <windows.h>
#pragma comment(lib, "fwpuclnt.lib")
#pragma comment(lib, "ole32.lib")
namespace topology_contract {
static constexpr UINT16 BROKER_PORT = 43120;
static GUID guid(const wchar_t *text) {
GUID value{};
if (FAILED(CLSIDFromString(text, &value)))
throw 1;
return value;
}
struct Memory {
void *p = nullptr;
~Memory() {
if (p)
FwpmFreeMemory0(&p);
}
};
struct Engine {
HANDLE h = nullptr;
~Engine() {
if (h)
FwpmEngineClose0(h);
}
};
struct Enumeration {
HANDLE engine, h = nullptr;
explicit Enumeration(HANDLE e) : engine(e) {}
~Enumeration() {
if (h)
FwpmFilterDestroyEnumHandle0(engine, h);
}
};
struct Result {
DWORD code = ERROR_ACCESS_DENIED;
UINT32 accessibleFilters = 0;
UINT32 ownedFilters = 0;
bool ownedVerified = false;
bool collision = false;
bool bfeRunning = false;
// Enumeration omits unreadable objects even at successful EOF. Never infer
// this from token elevation, caller JSON or the number of returned rows.
bool allObjectsReadable = false;
};
static bool blobEqual(const FWP_BYTE_BLOB *actual,
const std::vector<BYTE> &expected) {
return actual && actual->data && actual->size == expected.size() &&
memcmp(actual->data, expected.data(), expected.size()) == 0;
}
struct ExpectedCondition {
GUID field;
FWP_MATCH_TYPE match;
FWP_DATA_TYPE type;
UINT32 value;
std::vector<BYTE> bytes;
};
static bool marker(const FWP_BYTE_BLOB &value) {
static constexpr char expected[] = "SafetyNet.NetworkBoundary.v1";
return value.data && value.size == sizeof(expected) - 1 &&
memcmp(value.data, expected, sizeof(expected) - 1) == 0;
}
using SecurityQuery = DWORD(WINAPI *)(HANDLE, const GUID *,
SECURITY_INFORMATION, PSID *, PSID *,
PACL *, PACL *, PSECURITY_DESCRIPTOR *);
static bool protectedObject(HANDLE engine, const GUID &key,
SecurityQuery query) {
PSECURITY_DESCRIPTOR descriptor = nullptr;
PACL dacl = nullptr;
PSID owner = nullptr;
DWORD code = query(engine, &key,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&owner, nullptr, &dacl, nullptr, &descriptor);
BYTE system[SECURITY_MAX_SID_SIZE], admins[SECURITY_MAX_SID_SIZE];
DWORD ss = sizeof(system), as = sizeof(admins);
CreateWellKnownSid(WinLocalSystemSid, nullptr, system, &ss);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admins, &as);
SECURITY_DESCRIPTOR_CONTROL control{};
DWORD revision = 0;
bool okay = !code && dacl && owner &&
(EqualSid(owner, system) || EqualSid(owner, admins)) &&
GetSecurityDescriptorControl(descriptor, &control, &revision) &&
(control & SE_DACL_PROTECTED);
const DWORD write = FWPM_ACTRL_ADD | FWPM_ACTRL_ADD_LINK |
FWPM_ACTRL_BEGIN_WRITE_TXN | FWPM_ACTRL_WRITE | DELETE |
WRITE_DAC | WRITE_OWNER | GENERIC_ALL | GENERIC_WRITE;
if (okay)
for (DWORD i = 0; i < dacl->AceCount; ++i) {
void *raw = nullptr;
if (!GetAce(dacl, i, &raw)) {
okay = false;
break;
}
auto header = static_cast<ACE_HEADER *>(raw);
if (header->AceFlags & INHERIT_ONLY_ACE)
continue;
if (header->AceType == ACCESS_ALLOWED_ACE_TYPE) {
auto ace = static_cast<ACCESS_ALLOWED_ACE *>(raw);
if ((ace->Mask & write) && !EqualSid(&ace->SidStart, system) &&
!EqualSid(&ace->SidStart, admins)) {
okay = false;
break;
}
} else if (header->AceType != ACCESS_DENIED_ACE_TYPE) {
okay = false;
break;
}
}
if (descriptor)
FwpmFreeMemory0(reinterpret_cast<void **>(&descriptor));
return okay;
}
struct Rule {
GUID key, layer;
FWP_ACTION_TYPE action;
UINT64 weight;
std::vector<ExpectedCondition> conditions;
};
static bool matches(const FWPM_FILTER0 &actual, const Rule &rule,
const GUID &provider, const GUID &sublayer) {
if (!actual.providerKey || !IsEqualGUID(*actual.providerKey, provider) ||
!IsEqualGUID(actual.subLayerKey, sublayer) ||
!IsEqualGUID(actual.layerKey, rule.layer) ||
actual.action.type != rule.action ||
actual.flags != FWPM_FILTER_FLAG_PERSISTENT ||
actual.numFilterConditions != rule.conditions.size() ||
!actual.filterCondition)
return false;
if (rule.weight) {
if (actual.weight.type != FWP_UINT64 || !actual.weight.uint64 ||
*actual.weight.uint64 != rule.weight)
return false;
} else if (actual.weight.type != FWP_EMPTY)
return false;
for (size_t i = 0; i < rule.conditions.size(); ++i) {
const auto &e = rule.conditions[i];
const auto &a = actual.filterCondition[i];
if (!IsEqualGUID(a.fieldKey, e.field) || a.matchType != e.match ||
a.conditionValue.type != e.type)
return false;
switch (e.type) {
case FWP_UINT8:
if (a.conditionValue.uint8 != e.value)
return false;
break;
case FWP_UINT16:
if (a.conditionValue.uint16 != e.value)
return false;
break;
case FWP_UINT32:
if (a.conditionValue.uint32 != e.value)
return false;
break;
case FWP_BYTE_BLOB_TYPE:
if (!blobEqual(a.conditionValue.byteBlob, e.bytes))
return false;
break;
case FWP_SECURITY_DESCRIPTOR_TYPE:
if (!blobEqual(a.conditionValue.sd, e.bytes))
return false;
break;
default:
return false;
}
}
return true;
}
static std::vector<BYTE> appId(const std::wstring &path) {
Memory memory;
FWP_BYTE_BLOB *blob = nullptr;
if (FwpmGetAppIdFromFileName0(path.c_str(), &blob))
throw 1;
memory.p = blob;
if (!blob || !blob->data || blob->size < 8 || blob->size > 65536)
throw 1;
return {blob->data, blob->data + blob->size};
}
static Result inspect(const std::wstring &root,
const std::wstring &serviceSid) {
Result result;
try {
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
if (!manager) {
result.code = GetLastError();
return result;
}
SC_HANDLE bfe = OpenServiceW(manager, L"BFE", SERVICE_QUERY_STATUS);
SERVICE_STATUS_PROCESS status{};
DWORD needed = 0;
result.bfeRunning = bfe &&
QueryServiceStatusEx(bfe, SC_STATUS_PROCESS_INFO,
reinterpret_cast<BYTE *>(&status),
sizeof(status), &needed) &&
status.dwCurrentState == SERVICE_RUNNING;
if (bfe)
CloseServiceHandle(bfe);
CloseServiceHandle(manager);
if (!result.bfeRunning)
return result;
const GUID provider = guid(L"{2a7bcbf2-e4f5-4e94-bd33-d5442a3bef50}"),
sub = guid(L"{403a69b8-fc7c-460a-a247-72e0d72e6bf8}");
const GUID c4 = FWPM_LAYER_ALE_AUTH_CONNECT_V4,
c6 = FWPM_LAYER_ALE_AUTH_CONNECT_V6,
r4 = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V4,
r6 = FWPM_LAYER_ALE_AUTH_RECV_ACCEPT_V6;
const auto browser = appId(root + L"\\SafetyNet.exe"),
broker = appId(root + L"\\resources\\broker\\node.exe");
PSECURITY_DESCRIPTOR descriptor = nullptr;
ULONG size = 0;
std::wstring sddl = L"D:(A;;CC;;;" + serviceSid + L")";
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
sddl.c_str(), SDDL_REVISION_1, &descriptor, &size))
throw 1;
std::vector<BYTE> sd(reinterpret_cast<BYTE *>(descriptor),
reinterpret_cast<BYTE *>(descriptor) + size);
LocalFree(descriptor);
auto scalar = [](GUID field, FWP_MATCH_TYPE match, FWP_DATA_TYPE type,
UINT32 value) {
return ExpectedCondition{field, match, type, value, {}};
};
auto bytes = [](GUID field, FWP_DATA_TYPE type,
const std::vector<BYTE> &value) {
return ExpectedCondition{field, FWP_MATCH_EQUAL, type, 0, value};
};
const auto app = bytes(FWPM_CONDITION_ALE_APP_ID, FWP_BYTE_BLOB_TYPE,
browser),
tcp = scalar(FWPM_CONDITION_IP_PROTOCOL, FWP_MATCH_EQUAL,
FWP_UINT8, 6),
local = scalar(FWPM_CONDITION_IP_REMOTE_ADDRESS, FWP_MATCH_EQUAL,
FWP_UINT32, 0x7f000001);
const std::vector<ExpectedCondition> tuple = {
tcp,
scalar(FWPM_CONDITION_IP_LOCAL_ADDRESS, FWP_MATCH_EQUAL, FWP_UINT32,
0x7f000001),
scalar(FWPM_CONDITION_IP_LOCAL_PORT, FWP_MATCH_EQUAL, FWP_UINT16,
BROKER_PORT)};
auto permit = tuple;
permit.push_back(
bytes(FWPM_CONDITION_ALE_APP_ID, FWP_BYTE_BLOB_TYPE, broker));
permit.push_back(
bytes(FWPM_CONDITION_ALE_USER_ID, FWP_SECURITY_DESCRIPTOR_TYPE, sd));
const std::vector<Rule> rules = {
{guid(L"{4ac4e1d1-f08b-50fe-83fe-1872672c42b9}"),
c4,
FWP_ACTION_BLOCK,
0,
{app, scalar(FWPM_CONDITION_IP_PROTOCOL, FWP_MATCH_NOT_EQUAL,
FWP_UINT8, 6)}},
{guid(L"{07130743-8828-58b5-9476-3a0c2a001796}"),
c4,
FWP_ACTION_BLOCK,
0,
{app, tcp,
scalar(FWPM_CONDITION_IP_REMOTE_ADDRESS, FWP_MATCH_NOT_EQUAL,
FWP_UINT32, 0x7f000001)}},
{guid(L"{51401be2-30ce-5808-878f-7e181d0f6fde}"),
c4,
FWP_ACTION_BLOCK,
0,
{app, tcp, local,
scalar(FWPM_CONDITION_IP_REMOTE_PORT, FWP_MATCH_NOT_EQUAL, FWP_UINT16,
BROKER_PORT)}},
{guid(L"{1ee3c54d-84f8-5461-bd50-b74f32335ff3}"),
c6,
FWP_ACTION_BLOCK,
0,
{app}},
{guid(L"{a71f6123-ef64-5836-90ee-f624c6841f18}"),
r4,
FWP_ACTION_BLOCK,
0,
{app}},
{guid(L"{e537682c-948d-5bbf-a231-5db3ba896eda}"),
r6,
FWP_ACTION_BLOCK,
0,
{app}},
{guid(L"{dcbbb5f1-5936-5978-8349-21a2e791ffbf}"), r4, FWP_ACTION_PERMIT,
2, permit},
{guid(L"{7c4dba0f-904a-5685-a3ed-ae774a45136a}"), r4, FWP_ACTION_BLOCK,
1, tuple}};
Engine engine;
result.code = FwpmEngineOpen0(nullptr, RPC_C_AUTHN_WINNT, nullptr, nullptr,
&engine.h);
if (result.code)
return result;
Enumeration enumeration(engine.h);
Memory providerMemory;
FWPM_PROVIDER0 *p = nullptr;
result.code = FwpmProviderGetByKey0(engine.h, &provider, &p);
providerMemory.p = p;
if (result.code)
return result;
if (p->flags != FWPM_PROVIDER_FLAG_PERSISTENT || !p->serviceName ||
wcscmp(p->serviceName, L"SafetyNetNetworkBroker") ||
!marker(p->providerData) ||
!protectedObject(engine.h, provider,
FwpmProviderGetSecurityInfoByKey0)) {
result.code = ERROR_INVALID_DATA;
return result;
}
result.code =
FwpmFilterCreateEnumHandle0(engine.h, nullptr, &enumeration.h);
if (result.code)
return result;
std::vector<GUID> seen;
for (;;) {
Memory memory;
FWPM_FILTER0 **entries = nullptr;
UINT32 count = 0;
result.code =
FwpmFilterEnum0(engine.h, enumeration.h, 128, &entries, &count);
memory.p = entries;
if (result.code)
return result;
if (count > 128 || result.accessibleFilters + count > 65536) {
result.code = ERROR_MORE_DATA;
return result;
}
result.accessibleFilters += count;
for (UINT32 i = 0; i < count; ++i) {
const auto &f = *entries[i];
if (IsEqualGUID(f.subLayerKey, sub) ||
(f.providerKey && IsEqualGUID(*f.providerKey, provider))) {
bool okay = false;
for (const auto &rule : rules)
if (IsEqualGUID(f.filterKey, rule.key) &&
matches(f, rule, provider, sub))
okay = true;
if (!okay || !protectedObject(engine.h, f.filterKey,
FwpmFilterGetSecurityInfoByKey0)) {
result.code = ERROR_INVALID_DATA;
return result;
}
seen.push_back(f.filterKey);
++result.ownedFilters;
} else if (IsEqualGUID(f.layerKey, c4) || IsEqualGUID(f.layerKey, c6) ||
IsEqualGUID(f.layerKey, r4) || IsEqualGUID(f.layerKey, r6)) {
Memory sm;
FWPM_SUBLAYER0 *s = nullptr;
result.code = FwpmSubLayerGetByKey0(engine.h, &f.subLayerKey, &s);
sm.p = s;
if (result.code)
return result;
if (s->weight == 65535)
result.collision = true;
}
}
if (count < 128)
break;
}
Memory sm;
FWPM_SUBLAYER0 *s = nullptr;
result.code = FwpmSubLayerGetByKey0(engine.h, &sub, &s);
sm.p = s;
if (result.code)
return result;
result.ownedVerified =
marker(s->providerData) &&
protectedObject(engine.h, sub, FwpmSubLayerGetSecurityInfoByKey0) &&
result.ownedFilters == 8 && !result.collision && s->weight == 65535 &&
s->providerKey && IsEqualGUID(*s->providerKey, provider) &&
s->flags == FWPM_SUBLAYER_FLAG_PERSISTENT;
result.code = result.ownedVerified ? ERROR_SUCCESS : ERROR_INVALID_DATA;
} catch (...) {
result.code = ERROR_INVALID_DATA;
}
return result;
}
} // namespace topology_contract