Files
safetynet/apps/network-broker/native/management-credential.h
T

277 lines
10 KiB
C++

#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