diff --git a/sdk_v2/cpp/CMakeLists.txt b/sdk_v2/cpp/CMakeLists.txt index 4d47eb49..da95966e 100644 --- a/sdk_v2/cpp/CMakeLists.txt +++ b/sdk_v2/cpp/CMakeLists.txt @@ -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) if(FOUNDRY_LOCAL_BUILD_SERVICE) find_package(oatpp CONFIG REQUIRED) @@ -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) diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.cc b/sdk_v2/cpp/src/ep_detection/ep_utils.cc index 7fa6524f..06e36905 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.cc +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.cc @@ -7,9 +7,14 @@ #include +#include +#include +#include + #include #include #include +#include namespace fl { @@ -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(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(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 sig_bytes(base64_sig.size()); + int sig_len = BIO_read(b64, sig_bytes.data(), static_cast(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(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 diff --git a/sdk_v2/cpp/src/ep_detection/ep_utils.h b/sdk_v2/cpp/src/ep_detection/ep_utils.h index 634bb517..12f3e584 100644 --- a/sdk_v2/cpp/src/ep_detection/ep_utils.h +++ b/sdk_v2/cpp/src/ep_detection/ep_utils.h @@ -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 diff --git a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc index 9aafb838..45ad6cbd 100644 --- a/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc +++ b/sdk_v2/cpp/src/ep_detection/webgpu_ep_bootstrapper.cc @@ -17,7 +17,9 @@ #include #include #include +#include #include +#include #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN @@ -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) @@ -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> 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(manifest_stream)), std::istreambuf_iterator()); + manifest_stream.close(); + + std::ifstream sig_stream(sig_file, std::ios::binary); + std::string sig((std::istreambuf_iterator(sig_stream)), std::istreambuf_iterator()); + 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()) { @@ -96,7 +179,9 @@ ManifestPackageInfo FetchManifest(fl::ILogger& logger) { const auto& pkg = packages[kPlatformKey]; ManifestPackageInfo info; info.url = pkg.at("url").get(); - info.sha256 = pkg.at("sha256").at(kWebGpuProviderLib).get(); + for (const auto& [filename, hash] : pkg.at("sha256").items()) { + info.sha256.push_back({filename, hash.get()}); + } logger.Log(fl::LogLevel::Information, fmt::format("WebGPU EP: manifest fetched for platform '{}'", kPlatformKey)); @@ -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 @@ -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 @@ -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_)); diff --git a/sdk_v2/cpp/vcpkg.json b/sdk_v2/cpp/vcpkg.json index 781f3f2f..31b1446b 100644 --- a/sdk_v2/cpp/vcpkg.json +++ b/sdk_v2/cpp/vcpkg.json @@ -7,6 +7,7 @@ "azure-storage-blobs-cpp", "ms-gsl", "nlohmann-json", + "openssl", "spdlog" ], "overrides": [