Skip to content
Draft
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
34 changes: 26 additions & 8 deletions sdk_v2/cpp/docs/AndroidBuildPlan.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,27 @@ When `--android_run_emulator --test` is specified, `build.py`:

### Problem

Statically-linked OpenSSL (built by vcpkg) **ignores the `SSL_CERT_DIR` environment variable** at
runtime. The `--openssldir` path is baked at build time to a non-existent location on Android.
`SSL_CERT_FILE` with a single concatenated PEM bundle *is* honored.
Statically-linked OpenSSL/libcurl (built by vcpkg) **ignores both the `SSL_CERT_DIR` and the
`SSL_CERT_FILE` environment variables** at request time. The default CA location (`--openssldir`) is
baked at build time to a path that does not exist on Android, and this libcurl build was not
compiled with the fallback that would otherwise consult `SSL_CERT_FILE`. As a result, setting either
environment variable alone has **no effect** — curl uses its (non-existent) compiled-in default and
verification fails with a misleading "self-signed certificate in certificate chain".

### Solution for Testing
> This was confirmed on-device: passing the device's own trust store via `SSL_CERT_FILE` still
> failed, while the *exact same* bundle passed explicitly as a CA file (`openssl s_client -CAfile`,
> equivalent to `CURLOPT_CAINFO`) verified `ai.azure.com` with `Verify return code: 0 (ok)`. Only the
> delivery mechanism differed.

### Solution

The Core reads the `SSL_CERT_FILE` environment variable and forwards it explicitly to libcurl via
`CurlTransportOptions.CAInfo` (`CURLOPT_CAINFO`), which libcurl **always** honors. See
`src/http/http_client.cc` and `src/http/http_download.cc`. Callers therefore still set
`SSL_CERT_FILE`, but its effect now comes from the Core forwarding it to `CAInfo` — not from OpenSSL
auto-reading the environment.

### Building the bundle for testing

`tools/android.py` builds a CA bundle at test time from the device's system certificates:

Expand All @@ -207,7 +223,7 @@ runtime. The `--openssldir` path is baked at build time to a non-existent locati
```
cat /system/etc/security/cacerts/*.0 > /data/local/tmp/foundry_tests/cacert.pem
```
3. Sets `SSL_CERT_FILE` pointing to the bundle
3. Sets `SSL_CERT_FILE` pointing to the bundle (the Core forwards it to `CAInfo`)

This approach uses the device's actual trust store, so it works with any API level and includes
all root CAs that the device trusts (including DigiCert Global Root G2 needed for Azure endpoints).
Expand All @@ -220,9 +236,11 @@ the same pattern FL Core used.

### Key Insight

`SSL_CERT_DIR` does **not** work with statically-linked OpenSSL on Android. Always use `SSL_CERT_FILE`
with a concatenated PEM bundle. The error message when this fails is misleading: "self-signed
certificate in certificate chain" — it actually means OpenSSL cannot find **any** CA store.
Neither `SSL_CERT_DIR` nor `SSL_CERT_FILE` is auto-consulted by this statically-linked
OpenSSL/libcurl on Android. The CA bundle **must** be passed explicitly via `CURLOPT_CAINFO`; the
Core does this by forwarding `SSL_CERT_FILE`. The error message when the store is missing is
misleading: "self-signed certificate in certificate chain" — it actually means OpenSSL cannot find
**any** CA store.

---

Expand Down
32 changes: 30 additions & 2 deletions sdk_v2/cpp/src/download/blob_downloader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "download/blob_download_state.h"
#include "download/file_writer.h"
#include "exception.h"
#include "http/http_client.h"
#include "logger.h"
#include "util/path_safety.h"
#include "util/string_utils.h"
Expand All @@ -18,11 +19,19 @@
#include <memory>
#include <mutex>
#include <string>
#include <utility>
#include <vector>

#include <azure/core/context.hpp>
#include <azure/storage/blobs.hpp>

// The Azure Storage SDK builds its own libcurl transport, which — like our other curl transports —
// does not honor SSL_CERT_FILE. On non-Windows builds we install a CurlTransport preconfigured with
// CAInfo (see MakeBlobClientOptions below). Desktop Windows uses the default WinHTTP transport.
#if !defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT)
#include <azure/core/http/curl_transport.hpp>
#endif

namespace fl {

namespace {
Expand All @@ -32,6 +41,24 @@ namespace {
/// memory at this many bytes regardless of chunk size.
constexpr size_t kStreamingBufferBytes = 64 * 1024;

/// Builds BlobClientOptions with the CA bundle wired into the transport. The Azure Storage SDK
/// constructs its own libcurl transport internally, which does not consult SSL_CERT_FILE, so blob
/// downloads would otherwise fail TLS verification on Android with "unable to get local issuer
/// certificate". On non-Windows builds we install a CurlTransport preconfigured with CAInfo so
/// verification uses the caller-provided trust store; on desktop Windows the default WinHTTP
/// transport uses the OS store and is left untouched.
Azure::Storage::Blobs::BlobClientOptions MakeBlobClientOptions() {
Azure::Storage::Blobs::BlobClientOptions options;
#if !defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT)
if (std::string ca_bundle = fl::http::CaBundleFile(); !ca_bundle.empty()) {
Azure::Core::Http::CurlTransportOptions curl_opts;
curl_opts.CAInfo = std::move(ca_bundle);
options.Transport.Transport = std::make_shared<Azure::Core::Http::CurlTransport>(curl_opts);
}
#endif
return options;
}

} // namespace

// ========================================================================
Expand All @@ -55,7 +82,7 @@ AzureBlobDownloader::AzureBlobDownloader(ILogger& logger) : logger_(logger) {}

std::vector<BlobItemInfo> AzureBlobDownloader::ListBlobs(const std::string& sas_uri) {
try {
auto container_client = Azure::Storage::Blobs::BlobContainerClient(sas_uri);
auto container_client = Azure::Storage::Blobs::BlobContainerClient(sas_uri, MakeBlobClientOptions());
std::vector<BlobItemInfo> items;

for (auto page = container_client.ListBlobs(); page.HasPage(); page.MoveToNextPage()) {
Expand Down Expand Up @@ -137,7 +164,8 @@ void AzureBlobDownloader::DownloadBlob(const std::string& sas_uri,
try {
// Configure retry at the SDK level instead of a manual retry loop.
// Exponential backoff: 2s initial delay, 30s cap, generous retry count.
Azure::Storage::Blobs::BlobClientOptions client_options;
// MakeBlobClientOptions wires the CA bundle into the transport (see its comment).
auto client_options = MakeBlobClientOptions();
client_options.Retry.MaxRetries = 10;
client_options.Retry.RetryDelay = std::chrono::milliseconds{2000};
client_options.Retry.MaxRetryDelay = std::chrono::milliseconds{30000};
Expand Down
19 changes: 18 additions & 1 deletion sdk_v2/cpp/src/http/http_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,24 @@
#endif

#include <chrono>
#include <cstdlib>
#include <random>
#include <string>
#include <thread>
#include <utility>
#include <vector>

namespace fl {
namespace http {

std::string CaBundleFile() {
const char* cert_file = std::getenv("SSL_CERT_FILE");
if (cert_file != nullptr && cert_file[0] != '\0') {
return cert_file;
}
return {};
}

namespace {

struct HttpRawResult {
Expand All @@ -48,7 +58,14 @@ HttpRawResult HttpRequestRaw(const Azure::Core::Http::HttpMethod& method,
#if defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT)
WinHttpTransport transport;
#else
CurlTransport transport;
// The bundled libcurl does not honor the SSL_CERT_FILE environment variable (it was built with a
// compiled-in default CA path that does not exist on platforms like Android). Explicitly pass the
// CA bundle via CAInfo so the caller-provided trust store is actually used for TLS verification.
CurlTransportOptions curl_opts;
if (std::string ca_bundle = CaBundleFile(); !ca_bundle.empty()) {
curl_opts.CAInfo = std::move(ca_bundle);
}
CurlTransport transport(curl_opts);
#endif

// Build the request. For methods with a body (POST), attach a MemoryBodyStream.
Expand Down
8 changes: 8 additions & 0 deletions sdk_v2/cpp/src/http/http_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ struct HttpRequestOptions {
bool close_connection = false;
};

/// Returns the CA bundle file path from the `SSL_CERT_FILE` environment variable, or an empty
/// string when it is unset/empty. The bundled libcurl does not consult `SSL_CERT_FILE`
/// automatically (it was built with a compiled-in default CA path that does not exist on platforms
/// like Android), so every libcurl-based transport we construct — direct requests, file downloads,
/// and the Azure Storage blob client — must pass this explicitly as `CAInfo`. On desktop Windows the
/// WinHTTP transport uses the OS trust store and ignores this.
std::string CaBundleFile();

/// Perform an HTTP POST and return status, headers, and body without throwing on non-2xx responses.
/// Transport failures are returned as `status == 0` with the error message in `body`.
HttpResponse HttpPostWithResponse(const std::string& url,
Expand Down
12 changes: 11 additions & 1 deletion sdk_v2/cpp/src/http/http_download.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.
#include "http/http_download.h"

#include "http/http_client.h"
#include "logger.h"
#include "util/string_utils.h"

Expand All @@ -17,8 +18,10 @@
#include <azure/core/http/curl_transport.hpp>
#endif

#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <utility>

namespace fl {

Expand All @@ -40,7 +43,14 @@ bool HttpDownloadFile(const std::string& url,
#if defined(FOUNDRY_LOCAL_USE_WINHTTP_TRANSPORT)
WinHttpTransport transport;
#else
CurlTransport transport;
// The bundled libcurl does not honor the SSL_CERT_FILE environment variable (it was built with a
// compiled-in default CA path that does not exist on platforms like Android). Explicitly pass the
// CA bundle via CAInfo so the caller-provided trust store is actually used for TLS verification.
CurlTransportOptions curl_opts;
if (std::string ca_bundle = http::CaBundleFile(); !ca_bundle.empty()) {
curl_opts.CAInfo = std::move(ca_bundle);
}
CurlTransport transport(curl_opts);
#endif
Request request(HttpMethod::Get, Url(url));
request.SetHeader("User-Agent", user_agent);
Expand Down
Loading