Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdk_v2/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ find_package(nlohmann_json CONFIG REQUIRED)
find_package(azure-storage-blobs-cpp CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(Microsoft.GSL CONFIG REQUIRED)
find_package(OpenSSL CONFIG REQUIRED)
Comment thread
prathikr marked this conversation as resolved.

if(FOUNDRY_LOCAL_BUILD_SERVICE)
find_package(oatpp CONFIG REQUIRED)
Expand Down Expand Up @@ -219,6 +220,7 @@ function(foundry_local_configure_target TARGET LINK_SCOPE)
Azure::azure-core
Azure::azure-storage-blobs
spdlog::spdlog
OpenSSL::Crypto
)

if(TARGET OnnxRuntimeGenAI::OnnxRuntimeGenAI)
Expand Down
76 changes: 76 additions & 0 deletions sdk_v2/cpp/src/ep_detection/ep_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@

#include <fmt/format.h>

#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/pem.h>

#include <algorithm>
#include <cctype>
#include <string>
#include <vector>

namespace fl {

Expand Down Expand Up @@ -40,4 +45,75 @@ bool VerifyEpPackage(
return true;
}

bool VerifyRsaSha256Signature(
std::string_view data,
std::string_view base64_sig,
std::string_view public_key_pem,
ILogger& logger) {
// Load the RSA public key from PEM.
BIO* key_bio = BIO_new_mem_buf(public_key_pem.data(), static_cast<int>(public_key_pem.size()));
if (!key_bio) {
logger.Log(LogLevel::Warning, "manifest signature: failed to allocate BIO for public key");
return false;
}
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(key_bio, nullptr, nullptr, nullptr);
BIO_free(key_bio);
if (!pkey) {
logger.Log(LogLevel::Warning, "manifest signature: failed to parse public key PEM");
return false;
}

// Decode the base64 signature (single-line, no newlines).
BIO* b64 = BIO_new(BIO_f_base64());
if (!b64) {
logger.Log(LogLevel::Warning, "manifest signature: failed to allocate BIO for base64");
return false;
}

BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO* mem = BIO_new_mem_buf(base64_sig.data(), static_cast<int>(base64_sig.size()));
if (!mem) {
BIO_free(b64);
logger.Log(LogLevel::Warning, "manifest signature: failed to allocate BIO for signature buffer");
return false;
}

BIO_push(b64, mem);

// Upper bound: base64 expands by ~4/3.
std::vector<unsigned char> sig_bytes(base64_sig.size());
int sig_len = BIO_read(b64, sig_bytes.data(), static_cast<int>(sig_bytes.size()));
BIO_free_all(b64);

if (sig_len <= 0) {
EVP_PKEY_free(pkey);
logger.Log(LogLevel::Warning, "manifest signature: failed to decode base64 signature");
return false;
}
sig_bytes.resize(static_cast<size_t>(sig_len));

// Verify RSA-SHA256-PKCS1v15.
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
if (!ctx) {
EVP_PKEY_free(pkey);
logger.Log(LogLevel::Warning, "manifest signature: failed to allocate EVP_MD_CTX");
return false;
}

bool ok = false;
if (EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, pkey) == 1 &&
EVP_DigestVerifyUpdate(ctx, data.data(), data.size()) == 1) {
ok = (EVP_DigestVerifyFinal(ctx, sig_bytes.data(), sig_bytes.size()) == 1);
}

EVP_MD_CTX_free(ctx);
EVP_PKEY_free(pkey);

if (!ok) {
logger.Log(LogLevel::Warning, "manifest signature: RSA-SHA256 verification failed");
}

return ok;
}

} // namespace fl
13 changes: 13 additions & 0 deletions sdk_v2/cpp/src/ep_detection/ep_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,17 @@ bool VerifyEpPackage(
std::string_view ep_name,
ILogger& logger);

/// Verify an RSA-SHA256-PKCS1v15 detached signature over @p data.
///
/// @param data The signed data bytes (e.g. raw manifest JSON text).
/// @param base64_sig Base64-encoded RSA signature (single line, no newlines).
/// @param public_key_pem PEM-encoded RSA public key (-----BEGIN PUBLIC KEY----- block).
/// @param logger Logger for diagnostic output.
/// @return true if the signature is valid; false otherwise.
bool VerifyRsaSha256Signature(
std::string_view data,
std::string_view base64_sig,
std::string_view public_key_pem,
ILogger& logger);

} // namespace fl
125 changes: 112 additions & 13 deletions sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
#include <atomic>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <string>
#include <unordered_map>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
Expand All @@ -32,9 +34,28 @@ constexpr const char* kStagingDirName = "webgpu-ep-staging";
constexpr const char* kUserAgent = "FoundryLocal";
constexpr int kMaxInstallAttempts = 5;

// Manifest URL — always uses prod.
constexpr const char* kManifestUrl =
"https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_ep_prod.json";
// Manifest zip URL — atomically contains manifest.json and manifest.json.sig.
constexpr const char* kManifestZipUrl =
"https://foundrypackages-ffhrdhbxb7gpdreh.b02.azurefd.net/webgpu_manifest_prod.zip";

// RSA-4096 public key used to verify the manifest signature.
// Corresponds to the private key used by official WebGPU Plugin EP Publishing Pipeline.
constexpr const char* kManifestSigningPublicKey = R"PEM(
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1YwPWIQ7UJZ0EOVfRIeU
AiI6G9nwmQ+0RGmBKKNPeuTt8To7EUBfs2yjHs1nS159oEbI9wmN+SRhTx72fyo7
EEbQ2kYB/d+/znqrpTinHiyfrn6dEzqJzj5diTfXkVbm5+uueqxoxN6TAUwZqsdO
wveft1DiSU8G0NRx3QPxBACZx199ObiQgqDQycTbc7qaRUy9rkcrMimvXKIaui3z
fmxQtzF6WkRnN4Xf+jkzxgua0xSHkcdYpDu+M39iynqEkSChzv+h0NIE/B05z9/y
+6/EjFETYB2LuSr7N3EOMj1eTff/oFqwBk1gBuLxNxHjTtH1+DxpygIxz9Dy2OY5
jG46Io9Eg8q7UMW4aSm/YS/Sqt8KzqOG59XvLtADDlaS+8+KDV0K9Jwq1WXBbqXd
gXlUjLdIh+UAgF0zv5N8MGoS9BxvBNr932XkUV5VC26JgU3tPqiiiSXfPParBSJt
wt/PSpQDqkcWE9VsRmCe5pAgmv3AQlv+jSLlB8aDdCP8/+/AoI7St4n7STl8QtPl
XXWmO8EJwqEXFpaitcpNyzuol6/7H4mQV6XeNjezjmTWeedvxWcZXi1Pxp/FfOEK
iJxrPNMxlZZA26WvTEhc0vi9hxYxTsZKWuenZoGvgR2/sy2tqbEV3/4JhowQ6K56
MvdOj/vvArK/BIwPJnCYv4kCAwEAAQ==
-----END PUBLIC KEY-----
)PEM";

// Platform key used to look up this platform's package in the manifest.
#if defined(_WIN32) && defined(_M_ARM64)
Expand All @@ -61,16 +82,78 @@ constexpr const char* kRegistrationName = "Foundry.WebGPU";
/// Parsed manifest entry for a single platform.
struct ManifestPackageInfo {
std::string url;
std::string sha256; // expected SHA256 hash of kWebGpuProviderLib
std::vector<std::pair<std::string, std::string>> sha256; // filename -> expected SHA256 hash
};

/// Fetch the manifest JSON from CDN and extract the package info for this platform.
/// Fetch the manifest zip (atomically containing manifest.json and manifest.json.sig) from CDN,
/// extract it, verify the signature, and extract the package info for this platform.
ManifestPackageInfo FetchManifest(fl::ILogger& logger) {
logger.Log(fl::LogLevel::Debug, fmt::format("WebGPU EP: fetching manifest from {}", kManifestUrl));
logger.Log(fl::LogLevel::Debug, fmt::format("WebGPU EP: fetching manifest zip from {}", kManifestZipUrl));

// Download manifest zip atomically (contains both manifest.json and manifest.json.sig)
auto zip_path = std::filesystem::temp_directory_path() / "webgpu_manifest_temp.zip";

if (!HttpDownloadFile(kManifestZipUrl, zip_path, kUserAgent, nullptr, nullptr, logger)) {
throw std::runtime_error("WebGPU EP: failed to download manifest zip");
}

// Extract to temporary directory
auto extract_dir = std::filesystem::temp_directory_path() / kStagingDirName;
if (std::filesystem::exists(extract_dir)) {
std::filesystem::remove_all(extract_dir);
}
std::filesystem::create_directories(extract_dir);

if (!ExtractZip(zip_path, extract_dir, logger)) {
std::filesystem::remove_all(extract_dir);
std::filesystem::remove(zip_path);
throw std::runtime_error("WebGPU EP: failed to extract manifest zip");
}

// Read manifest and signature from extracted files
auto manifest_file = extract_dir / "manifest.json";
auto sig_file = extract_dir / "manifest.json.sig";

if (!std::filesystem::exists(manifest_file)) {
std::filesystem::remove_all(extract_dir);
std::filesystem::remove(zip_path);
throw std::runtime_error("WebGPU EP: manifest.json not found in manifest zip");
}

if (!std::filesystem::exists(sig_file)) {
std::filesystem::remove_all(extract_dir);
std::filesystem::remove(zip_path);
throw std::runtime_error("WebGPU EP: manifest.json.sig not found in manifest zip");
}

// Read both files as strings
std::ifstream manifest_stream(manifest_file, std::ios::binary);
std::string body((std::istreambuf_iterator<char>(manifest_stream)), std::istreambuf_iterator<char>());
manifest_stream.close();

std::ifstream sig_stream(sig_file, std::ios::binary);
std::string sig((std::istreambuf_iterator<char>(sig_stream)), std::istreambuf_iterator<char>());
sig_stream.close();

// Trim any trailing whitespace (CDN may append \r\n).
while (!sig.empty() && (sig.back() == '\n' || sig.back() == '\r' || sig.back() == ' ')) {
sig.pop_back();
}

// Verify signature
if (!fl::VerifyRsaSha256Signature(body, sig, kManifestSigningPublicKey, logger)) {
std::filesystem::remove_all(extract_dir);
std::filesystem::remove(zip_path);
throw std::runtime_error(
"WebGPU EP: manifest signature verification failed — refusing to use manifest");
}

logger.Log(fl::LogLevel::Debug, "WebGPU EP: manifest signature verified");

// Clean up temporary files before parsing
std::filesystem::remove_all(extract_dir);
std::filesystem::remove(zip_path);

fl::http::HttpRequestOptions options;
options.user_agent = kUserAgent;
auto body = fl::http::HttpGetWithRetry(kManifestUrl, logger, options);
auto manifest = nlohmann::json::parse(body);

if (!manifest.contains("packages") || !manifest["packages"].is_object()) {
Expand All @@ -96,7 +179,9 @@ ManifestPackageInfo FetchManifest(fl::ILogger& logger) {
const auto& pkg = packages[kPlatformKey];
ManifestPackageInfo info;
info.url = pkg.at("url").get<std::string>();
info.sha256 = pkg.at("sha256").at(kWebGpuProviderLib).get<std::string>();
for (const auto& [filename, hash] : pkg.at("sha256").items()) {
info.sha256.push_back({filename, hash.get<std::string>()});
}

logger.Log(fl::LogLevel::Information,
fmt::format("WebGPU EP: manifest fetched for platform '{}'", kPlatformKey));
Expand Down Expand Up @@ -142,8 +227,22 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force,
// Fetch manifest before acquiring lock (avoid holding lock during network I/O)
auto manifest = FetchManifest(logger);

// Build a verifier for all binaries listed in the manifest (EP binary + Windows DX dlls).
auto verify_package = [&](const std::filesystem::path& dir) -> bool {
// Verify each file individually (VerifyEpPackage takes initializer_list for compile-time constants)
for (const auto& [filename, expected_hash] : manifest.sha256) {
bool verified = VerifyEpPackage(dir, {{filename, expected_hash}}, "WebGPU EP", logger);
logger.Log(LogLevel::Debug,
fmt::format("WebGPU EP: verifying SHA256 of '{}': {}", filename, verified));
if (!verified) {
return false;
}
}
return true;
};

// Check if package already exists and is valid
if (!force && VerifyEpPackage(ep_dir, {{kWebGpuProviderLib, manifest.sha256}}, "WebGPU EP", logger)) {
if (!force && verify_package(ep_dir)) {
logger.Log(LogLevel::Debug, "WebGPU EP: local binaries match manifest, skipping download");
} else {
// Ensure parent directory exists for the lock file
Expand All @@ -154,7 +253,7 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force,
FileLock lock(lock_path);

// Re-check after acquiring lock (another process may have completed the update)
if (!force && VerifyEpPackage(ep_dir, {{kWebGpuProviderLib, manifest.sha256}}, "WebGPU EP", logger)) {
if (!force && verify_package(ep_dir)) {
logger.Log(LogLevel::Debug, "WebGPU EP: another process already completed the update");
} else {
// Download and extract to staging directory for atomic swap
Expand Down Expand Up @@ -202,7 +301,7 @@ bool WebGpuEpBootstrapper::DownloadAndRegister(bool force,
std::filesystem::remove(zip_path);

// Verify staging
if (!VerifyEpPackage(staging_dir, {{kWebGpuProviderLib, manifest.sha256}}, "WebGPU EP", logger)) {
if (!verify_package(staging_dir)) {
logger.Log(LogLevel::Warning,
fmt::format("WebGPU EP: verification failed after extraction (attempt {})",
attempts_));
Expand Down
1 change: 1 addition & 0 deletions sdk_v2/cpp/vcpkg.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"azure-storage-blobs-cpp",
"ms-gsl",
"nlohmann-json",
"openssl",
"spdlog"
],
"overrides": [
Expand Down