Skip to content
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

fix: Use CA certificates from conda-forge::ca-certificates #3765

Merged
merged 16 commits into from
Jan 24, 2025
Merged
2 changes: 2 additions & 0 deletions libmamba/include/mamba/api/configuration.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@ namespace mamba

namespace detail
{
auto get_root_prefix() -> fs::u8path;

template <class T>
bool ConfigurableImpl<T>::cli_configured() const
{
Expand Down
79 changes: 43 additions & 36 deletions libmamba/src/api/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -713,22 +713,52 @@ namespace mamba
return { fs::weakly_canonical(std::move(prefix)) };
}

/**
* In mamba 1.0, only micromamba was using this location.
*/
auto default_root_prefix_v1() -> fs::u8path
auto get_default_root_prefix(fs::u8path& prefix) -> void
{
return fs::u8path(util::user_home_dir()) / "micromamba";
if (util::get_env("MAMBA_DEFAULT_ROOT_PREFIX"))
{
prefix = util::get_env("MAMBA_DEFAULT_ROOT_PREFIX").value();
LOG_WARNING << unindent(R"(
'MAMBA_DEFAULT_ROOT_PREFIX' is meant for testing purpose.
Consider using 'MAMBA_ROOT_PREFIX' instead)");
}
else
{
#ifdef MAMBA_USE_INSTALL_PREFIX_AS_BASE
// mamba case
// set the root prefix as the mamba installation path
get_root_prefix_from_mamba_bin(util::which("mamba"))
.transform([&](fs::u8path&& p) { prefix = std::move(p); })
.or_else([](mamba_error&& error) { throw std::move(error); });
#else
// micromamba case

// In 1.0, only micromamba was using this location.
const fs::u8path default_root_prefix_v1 = fs::u8path(util::user_home_dir())
/ "micromamba";

// In 2.0, we change the default location.
// We unconditionally name the subfolder "mamba" for compatibility between ``mamba``
// and ``micromamba``, as well as consistency with ``MAMBA_`` environment variables.
const fs::u8path default_root_prefix_v2 = fs::u8path(util::user_data_dir()) / "mamba";

validate_existing_root_prefix(default_root_prefix_v1)
.or_else([&default_root_prefix_v2](const auto& /* error */)
{ return validate_root_prefix(default_root_prefix_v2); })
.transform([&](fs::u8path&& p) { prefix = std::move(p); })
.or_else([](mamba_error&& error) { throw std::move(error); });
#endif
}
}

/**
* In mamba 2.0, we change the default location.
* We unconditionally name the subfolder "mamba" for compatibility between ``mamba``
* and ``micromamba``, as well as consistency with ``MAMBA_`` environment variables.
*/
auto default_root_prefix_v2() -> fs::u8path
auto get_root_prefix() -> fs::u8path
{
return fs::u8path(util::user_data_dir()) / "mamba";
fs::u8path root_prefix = util::get_env("MAMBA_ROOT_PREFIX").value_or("");
if (root_prefix.empty())
{
get_default_root_prefix(root_prefix);
}
return root_prefix;
}

void root_prefix_hook(Configuration& config, fs::u8path& prefix)
Expand All @@ -737,30 +767,7 @@ namespace mamba

if (prefix.empty())
{
if (util::get_env("MAMBA_DEFAULT_ROOT_PREFIX"))
{
prefix = util::get_env("MAMBA_DEFAULT_ROOT_PREFIX").value();
LOG_WARNING << unindent(R"(
'MAMBA_DEFAULT_ROOT_PREFIX' is meant for testing purpose.
Consider using 'MAMBA_ROOT_PREFIX' instead)");
}
else
{
#ifdef MAMBA_USE_INSTALL_PREFIX_AS_BASE
// mamba case
// set the root prefix as the mamba installation path
get_root_prefix_from_mamba_bin(util::which("mamba"))
.transform([&](fs::u8path&& p) { prefix = std::move(p); })
.or_else([](mamba_error&& error) { throw std::move(error); });
#else
// micromamba case
validate_existing_root_prefix(default_root_prefix_v1())
.or_else([](const auto& /* error */)
{ return validate_root_prefix(default_root_prefix_v2()); })
.transform([&](fs::u8path&& p) { prefix = std::move(p); })
.or_else([](mamba_error&& error) { throw std::move(error); });
#endif
}
prefix = get_root_prefix();

if (env_name.configured())
{
Expand Down
3 changes: 2 additions & 1 deletion libmamba/src/core/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>

#include "mamba/api/configuration.hpp"
#include "mamba/core/context.hpp"
#include "mamba/core/execution.hpp"
#include "mamba/core/output.hpp"
Expand Down Expand Up @@ -177,7 +178,7 @@ namespace mamba
Context::Context(const ContextOptions& options)
{
on_ci = static_cast<bool>(util::get_env("CI"));
prefix_params.root_prefix = util::get_env("MAMBA_ROOT_PREFIX").value_or("");
prefix_params.root_prefix = detail::get_root_prefix();
Copy link
Member

Choose a reason for hiding this comment

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

That's the only part I'm not sure about, since prefix_params.root_prefix is used to store the root_prefix set by the config (see https://github.com/mamba-org/mamba/blob/main/libmamba/src/api/configuration.cpp#L1237-L1245). I don't know whether some parts of the code do something specific when this variable is empty, nor if there are use cases where it would be valid to have it empty (when using the library out of mamba / micromamba for instance).

prefix_params.conda_prefix = prefix_params.root_prefix;

envs_dirs = { prefix_params.root_prefix / "envs" };
Expand Down
46 changes: 44 additions & 2 deletions libmamba/src/download/downloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//
// The full license is in the file LICENSE, distributed with this software.

#include "mamba/api/configuration.hpp"
#include "mamba/core/invoke.hpp"
#include "mamba/core/thread_utils.hpp"
#include "mamba/core/util.hpp"
Expand All @@ -24,13 +25,18 @@ namespace mamba::download
namespace
{

constexpr std::array<const char*, 6> cert_locations{
constexpr std::array<const char*, 10> cert_locations{
"/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc.
"/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6
"/etc/ssl/ca-bundle.pem", // OpenSUSE
"/etc/pki/tls/cacert.pem", // OpenELEC
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7
"/etc/ssl/cert.pem", // Alpine Linux
// MacOS
"/System/Library/OpenSSL/certs/cert.pem",
"/usr/local/etc/openssl/cert.pem",
"/usr/local/share/certs/ca-root-nss.crt",
"/usr/local/share/certs/ca-root.crt",
};

void init_remote_fetch_params(Context::RemoteFetchParams& remote_fetch_params)
Expand Down Expand Up @@ -74,16 +80,52 @@ namespace mamba::download
LOG_INFO << "Using REQUESTS_CA_BUNDLE " << remote_fetch_params.ssl_verify;
}
}
else if (remote_fetch_params.ssl_verify == "<system>" && util::on_linux)
// TODO: Adapt the semantic of `<system>` to decouple the use of CA certificates
// from `conda-forge::ca-certificates` and the system CA certificates.
else if (remote_fetch_params.ssl_verify == "<system>")
jjerphan marked this conversation as resolved.
Show resolved Hide resolved
{
// Use the CA certificates from `conda-forge::ca-certificates` installed in the
// root prefix or the system CA certificates if the certificate is not present.
fs::u8path libmamba_library_path;

fs::u8path root_prefix = detail::get_root_prefix();
fs::u8path env_prefix_conda_cert = root_prefix / "ssl" / "cacert.pem";

LOG_INFO << "Checking for CA certificates at the root prefix: "
<< env_prefix_conda_cert;

if (fs::exists(env_prefix_conda_cert))
{
LOG_INFO << "Using CA certificates from `conda-forge::ca-certificates` installed in the root prefix "
<< "(i.e " << env_prefix_conda_cert << ")";
remote_fetch_params.ssl_verify = env_prefix_conda_cert;
remote_fetch_params.curl_initialized = true;
return;
}
jjerphan marked this conversation as resolved.
Show resolved Hide resolved

// Fallback on system CA certificates.
bool found = false;

// TODO: find if one needs to specify a CA certificate on Windows or not
// given that the location of system's CA certificates is not clear on Windows.
// For now, just use `libcurl` and the SSL libraries' default.
if (util::on_win)
{
LOG_INFO << "Using libcurl/the SSL library's default CA certification";
remote_fetch_params.ssl_verify = "";
found = true;
remote_fetch_params.curl_initialized = true;
return;
}

for (const auto& loc : cert_locations)
{
if (fs::exists(loc))
{
LOG_INFO << "Using system CA certificates at: " << loc;
remote_fetch_params.ssl_verify = loc;
found = true;
break;
}
}

Expand Down
42 changes: 42 additions & 0 deletions libmamba/tests/src/download/test_downloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <catch2/catch_all.hpp>

#include "mamba/api/configuration.hpp"
#include "mamba/download/downloader.hpp"

#include "mambatests.hpp"
Expand Down Expand Up @@ -54,5 +55,46 @@ namespace mamba
std::runtime_error
);
}

TEST_CASE("Use CA certificate from the root prefix")
{
// Create a context, make a request and check that ssl_verify is set to the correct path
auto& context = mambatests::singletons().context;

// Set the context values to the default ones
context.remote_fetch_params.curl_initialized = false;
context.remote_fetch_params.ssl_verify = "<system>";

download::Request request(
"test",
download::MirrorName(""),
"https://conda.anaconda.org/conda-forge/linux-64/repodata.json",
"test_download_repodata.json"
);
download::MultiRequest dl_request{ std::vector{ std::move(request) } };

// Downloading must initialize curl and set `ssl_verify` to the path of the CA
// certificate
REQUIRE(!context.remote_fetch_params.curl_initialized);
download::MultiResult res = download::download(dl_request, context.mirrors, context);
REQUIRE(context.remote_fetch_params.curl_initialized);

auto certificates = context.remote_fetch_params.ssl_verify;
const fs::u8path root_prefix = detail::get_root_prefix();
const fs::u8path expected_certificates = root_prefix / "ssl" / "cert.pem";

// TODO: is libmamba tested without a root prefix or a base installation?
bool reach_fallback_certificates;
if (util::on_win)
{
// Default certificates from libcurl/libssl are used on Windows
reach_fallback_certificates = certificates == "";
}
else
{
reach_fallback_certificates = (mamba::util::ends_with(certificates, "cert.pem") || mamba::util::ends_with(certificates, "ca-certificates.crt"));
}
REQUIRE((certificates == expected_certificates || reach_fallback_certificates));
}
}
}
30 changes: 30 additions & 0 deletions micromamba/tests/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -1575,3 +1575,33 @@ def test_update_spec_list(tmp_path):
out = helpers.create("-p", env_prefix, "-f", env_spec_file, "--dry-run")

assert update_specs_list in out.replace("\r", "")


def test_ca_certificates(tmp_path):
# Check that CA certificates from conda-forge or that the fall back is used by micromamba.
env_prefix = tmp_path / "env-ca-certificates"

umamba = helpers.get_umamba()
args = [umamba, "create", "-p", env_prefix, "numpy", "--dry-run", "-vvv"]
p = subprocess.run(args, capture_output=True, check=True)
verbose_logs = p.stderr.decode()

root_prefix_ca_certificates_used = (
"Using CA certificates from `conda-forge::ca-certificates` installed in the root prefix"
in verbose_logs
)

system_ca_certificates_used = "Using system CA certificates at" in verbose_logs

default_libcurl_certificates_used = (
"Using libcurl/the SSL library's default CA certification" in verbose_logs
)

# On Windows default
fall_back_certificates_used = (
default_libcurl_certificates_used
if platform.system() == "Windows"
else system_ca_certificates_used
)

assert root_prefix_ca_certificates_used or fall_back_certificates_used
Loading