// Strict bounded native signed-release verification. No signer key in // envelopes. #pragma once #include #include #include #include #include #include #include #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 object; std::vector 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(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(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 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 unhex(const std::string &input) { if (input.empty() || input.size() % 2) throw std::runtime_error("fixed-manifest-invalid"); std::vector 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 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(const_cast(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 &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 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 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 &xy, const std::string &payload, const std::vector &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 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 digest{}; if (BCryptHashData( hash, reinterpret_cast(const_cast(domain.data())), ULONG(domain.size()), 0) >= 0 && BCryptHashData( hash, reinterpret_cast(const_cast(payload.data())), ULONG(payload.size()), 0) >= 0 && BCryptFinishHash(hash, digest.data(), 32, 0) >= 0) okay = BCryptVerifySignature(key, nullptr, digest.data(), 32, const_cast(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 &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 hashes; }; static Release release(const std::wstring &root, const std::vector &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 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 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 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 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(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 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(&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 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(device[0])) || !devices.insert(device).second) throw std::runtime_error("fixed-manifest-invalid"); for (char c : device) if (!isalnum(static_cast(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