Add packaged DLP broker and protected Windows service foundation
This commit is contained in:
1 parent
95d23f4e0b
commit
7b5057901a
140 files changed
+11215
-60
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user