Skip to content

Conversation

@fedpre
Copy link

@fedpre fedpre commented Dec 11, 2025

Motivation

Android doesn't allow native code to check the system ssl certificate. This PR allows the certificate to be embedded at compile time and avoid the issue on Android.

Test Plan

If you changed any code, please provide us with clear instructions on how you verified your changes work. Bonus points for screenshots and videos!

Documentation

If this PR adds or changes functionality, consider clarifying which docs need to be updated, e.g. on AleoNet/welcome.

Backwards compatibility

Please review backwards compatibility. Does any functionality need to be guarded by an existing or new ConsensusVersion?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses SSL certificate verification issues on Android and iOS by embedding Mozilla's CA certificate bundle at compile time. The solution downloads the CA bundle during the build process for mobile platforms and configures curl to use it directly from memory, avoiding filesystem access issues that Android's security model restricts.

Key Changes

  • Adds a build script that downloads Mozilla's CA certificate bundle for Android/iOS builds
  • Modifies curl configuration to use embedded certificates via ssl_cainfo_blob API
  • Introduces reqwest 0.11 as a build-time dependency for downloading the CA bundle

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 9 comments.

File Description
parameters/build.rs New build script that downloads CA certificate bundle for mobile platforms and writes it to OUT_DIR
parameters/src/macros.rs Adds platform-specific SSL configuration to use embedded CA bundle on Android/iOS with fallback logic
parameters/Cargo.toml Adds reqwest 0.11 as build dependency with blocking and rustls-tls features
Cargo.lock Adds reqwest 0.11.27 and its transitive dependencies to the lock file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

fs::write(&ca_bundle_path, &contents).expect("Failed to write CA certificate bundle");

println!("cargo:warning=CA certificate bundle downloaded successfully");
println!("cargo:rerun-if-changed=build.rs");
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cargo:rerun-if-changed directive is placed inside the success branch of the match statement (line 37), which means it only gets executed when the CA bundle download succeeds. This directive should be outside the match statement to ensure proper rebuild behavior.

When placed inside the success branch, the build script won't properly track when build.rs itself changes if the download ever fails. This could lead to stale build artifacts.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +43
panic!(
"Failed to download CA certificate bundle: {e}\n\
You may need to download it manually from {ca_bundle_url} and place it at {ca_bundle_path:?}"
);
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message suggests manual download and placement, but doesn't provide clear instructions on where exactly the file should be placed. The path shown is in the OUT_DIR which varies between builds and isn't suitable for manual file placement.

Consider updating the error message to provide more actionable guidance, such as suggesting an environment variable that could be used to specify a pre-downloaded CA bundle path, or documenting a location in the source tree where users could place the file.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +48
} else {
// For non-mobile targets, write an empty file
fs::write(&ca_bundle_path, b"").expect("Failed to write empty CA bundle");
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Writing an empty file for non-mobile targets is unnecessary and could cause issues. The include_bytes! macro on line 81 of macros.rs is only compiled for mobile targets (android/ios), so non-mobile builds never include this file. Writing an empty file creates unnecessary filesystem operations during the build process.

Consider wrapping the entire cacert.pem file creation in the mobile platform check, or adding a comment explaining why the empty file is needed for non-mobile targets if there's a specific reason.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +34
let ca_bundle_url = "https://curl.se/ca/cacert.pem";
match download_ca_bundle(ca_bundle_url) {
Ok(contents) => {
// Write the CA bundle as a binary file that can be included with include_bytes!
// This is simpler and more efficient than generating a Rust array
fs::write(&ca_bundle_path, &contents).expect("Failed to write CA certificate bundle");
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CA bundle is downloaded from an external source (https://curl.se/ca/cacert.pem) without any integrity verification (e.g., checksum validation). This creates a security risk where the build process could be compromised if the downloaded bundle is tampered with or if there's a man-in-the-middle attack during the build.

Consider adding checksum verification for the downloaded CA bundle to ensure integrity. You could:

  1. Store the expected SHA-256 checksum of the bundle in the code or a configuration file
  2. Verify the checksum after download
  3. Update the checksum when intentionally updating to a newer bundle version

This is particularly important since this code is handling SSL/TLS certificates that form the foundation of secure communications.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +122
} else {
// Fallback: try to write to a file if blob method doesn't work or bundle is empty
// This should not happen for mobile builds, but provides a fallback
let mut ca_bundle_path = {
let aleo_dir = aleo_std::aleo_dir();
if aleo_dir.to_string_lossy().contains(".cargo/registry")
|| aleo_dir.to_string_lossy().contains("target/")
|| std::fs::create_dir_all(&aleo_dir).is_err()
{
std::env::temp_dir()
} else {
aleo_dir
}
};

std::fs::create_dir_all(&ca_bundle_path).map_err(|e| {
$crate::errors::ParameterError::Crate(
"std::fs",
format!("Failed to create directory at {:?}: {}", ca_bundle_path, e),
)
})?;

ca_bundle_path.push("snarkvm_cacert.pem");
std::fs::write(&ca_bundle_path, CA_BUNDLE).map_err(|e| {
$crate::errors::ParameterError::Crate("std::fs", format!("Failed to write CA bundle: {}", e))
})?;

let ca_bundle_str = ca_bundle_path.to_str().ok_or_else(|| {
$crate::errors::ParameterError::Crate("std::path", "CA bundle path is not valid UTF-8".to_string())
})?;
easy.cainfo(ca_bundle_str)
.map_err(|e| $crate::errors::ParameterError::Crate("curl", format!("Failed to set CA bundle: {}", e)))?;
}
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback logic when CA_BUNDLE is empty is unnecessarily complex and unlikely to execute in normal builds. Since this code block only runs on mobile platforms (Android/iOS) where the CA bundle should be downloaded at build time, an empty bundle indicates a serious build configuration issue that should fail fast rather than attempting fallback recovery.

The fallback writing to the filesystem also contradicts the PR's stated goal of avoiding file I/O and permission issues on Android. If the empty case does need to be handled, it should probably error out with a clear message rather than attempt a workaround.

Consider simplifying this by either:

  1. Removing the fallback entirely and returning an error if CA_BUNDLE is empty, or
  2. If keeping it, add a clear comment explaining when this edge case would occur and why the fallback is appropriate

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +45
// Download CA bundle for mobile platforms (Android and iOS)
let target = env::var("TARGET").unwrap_or_default();
if target.contains("android") || target.contains("ios") {
let platform = if target.contains("android") { "Android" } else { "iOS" };
// Download Mozilla's CA certificate bundle
println!("cargo:warning=Downloading CA certificate bundle for {platform}...");

let ca_bundle_url = "https://curl.se/ca/cacert.pem";
match download_ca_bundle(ca_bundle_url) {
Ok(contents) => {
// Write the CA bundle as a binary file that can be included with include_bytes!
// This is simpler and more efficient than generating a Rust array
fs::write(&ca_bundle_path, &contents).expect("Failed to write CA certificate bundle");

println!("cargo:warning=CA certificate bundle downloaded successfully");
println!("cargo:rerun-if-changed=build.rs");
}
Err(e) => {
panic!(
"Failed to download CA certificate bundle: {e}\n\
You may need to download it manually from {ca_bundle_url} and place it at {ca_bundle_path:?}"
);
}
}
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build script downloads the CA bundle during every build for mobile platforms. This is inefficient and creates several issues:

  1. Build reliability: Builds will fail if https://curl.se is unavailable or experiencing network issues
  2. Build performance: Every clean build downloads ~200KB unnecessarily
  3. Reproducibility: Different builds could get different versions of the CA bundle if it's updated on the server

Consider adding a mechanism to cache the downloaded bundle and only re-download when necessary (e.g., based on a version number or when explicitly requested). Alternatively, the CA bundle could be vendored in the repository with a separate script to update it when needed, ensuring deterministic builds.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +92
// Fallback: try to write to a file if blob method doesn't work or bundle is empty
// This should not happen for mobile builds, but provides a fallback
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states this fallback "should not happen for mobile builds" but the code doesn't explain what scenario would actually trigger this path. This creates confusion about whether this is handling a legitimate edge case or defensive programming against impossible conditions.

Additionally, the fallback attempts complex filesystem operations (checking for .cargo/registry, target/, creating directories) that seem inconsistent with the primary path's goal of avoiding filesystem I/O. If the CA bundle is empty on a mobile build, it indicates a build system failure that should probably fail fast rather than attempt recovery.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +121
ca_bundle_path.push("snarkvm_cacert.pem");
std::fs::write(&ca_bundle_path, CA_BUNDLE).map_err(|e| {
$crate::errors::ParameterError::Crate("std::fs", format!("Failed to write CA bundle: {}", e))
})?;

let ca_bundle_str = ca_bundle_path.to_str().ok_or_else(|| {
$crate::errors::ParameterError::Crate("std::path", "CA bundle path is not valid UTF-8".to_string())
})?;
easy.cainfo(ca_bundle_str)
.map_err(|e| $crate::errors::ParameterError::Crate("curl", format!("Failed to set CA bundle: {}", e)))?;
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback path writes the CA bundle to a file at runtime and then configures curl to use it. This creates a potential race condition issue:

  1. Multiple concurrent curl requests could all try to write to the same "snarkvm_cacert.pem" file
  2. The file could be deleted or modified between writing and curl using it
  3. In multi-threaded scenarios, one thread could overwrite while another is reading

Additionally, this reintroduces the exact filesystem permission and I/O issues that the primary code path (using ssl_cainfo_blob) was designed to avoid.

Consider either removing this fallback entirely (since it contradicts the PR's goal) or at minimum adding proper synchronization and error handling for concurrent access scenarios.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +86
// Only use this if CA_BUNDLE is not empty (mobile builds should have it)
if !CA_BUNDLE.is_empty() {
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Only use this if CA_BUNDLE is not empty (mobile builds should have it)" but this is misleading. The condition if !CA_BUNDLE.is_empty() means the code ONLY uses ssl_cainfo_blob when the bundle is NOT empty, which is correct. However, the phrasing "Only use this if..." makes it sound like there's a choice being made, when in reality this is the expected/normal case for mobile builds.

Consider rephrasing to be clearer, for example: "For mobile builds, the CA bundle should always be non-empty. Use ssl_cainfo_blob to set it directly from memory."

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant