-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(parameters): fix android ssl certificate issue #3061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Conversation
…es, including base64, h2, http, hyper, and reqwest with version updates
…ing to a file instead of generating a Rust module
…e for curl compatibility
…irectory and using curl's cainfo() method
…g fallback to temp directory
…for direct memory access and adding fallback to file writing
…ndroid for mobile platforms
There was a problem hiding this 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_blobAPI - 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"); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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.
| 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:?}" | ||
| ); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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.
| } else { | ||
| // For non-mobile targets, write an empty file | ||
| fs::write(&ca_bundle_path, b"").expect("Failed to write empty CA bundle"); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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.
| 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"); |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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:
- Store the expected SHA-256 checksum of the bundle in the code or a configuration file
- Verify the checksum after download
- 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.
| } 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)))?; | ||
| } |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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:
- Removing the fallback entirely and returning an error if CA_BUNDLE is empty, or
- If keeping it, add a clear comment explaining when this edge case would occur and why the fallback is appropriate
| // 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:?}" | ||
| ); | ||
| } | ||
| } |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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:
- Build reliability: Builds will fail if https://curl.se is unavailable or experiencing network issues
- Build performance: Every clean build downloads ~200KB unnecessarily
- 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.
| // 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 |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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.
| 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)))?; |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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:
- Multiple concurrent curl requests could all try to write to the same "snarkvm_cacert.pem" file
- The file could be deleted or modified between writing and curl using it
- 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.
| // Only use this if CA_BUNDLE is not empty (mobile builds should have it) | ||
| if !CA_BUNDLE.is_empty() { |
Copilot
AI
Dec 17, 2025
There was a problem hiding this comment.
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."
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?