diff --git a/libmamba/include/mamba/api/configuration.hpp b/libmamba/include/mamba/api/configuration.hpp index 6d1d9ccf52..7d37ee7406 100644 --- a/libmamba/include/mamba/api/configuration.hpp +++ b/libmamba/include/mamba/api/configuration.hpp @@ -483,6 +483,8 @@ namespace mamba namespace detail { + auto get_root_prefix() -> fs::u8path; + template bool ConfigurableImpl::cli_configured() const { diff --git a/libmamba/src/api/configuration.cpp b/libmamba/src/api/configuration.cpp index 435433bb88..e7fd2e5400 100644 --- a/libmamba/src/api/configuration.cpp +++ b/libmamba/src/api/configuration.cpp @@ -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) @@ -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()) { diff --git a/libmamba/src/core/context.cpp b/libmamba/src/core/context.cpp index e58ad01b27..e436c59f97 100644 --- a/libmamba/src/core/context.cpp +++ b/libmamba/src/core/context.cpp @@ -12,6 +12,7 @@ #include #include +#include "mamba/api/configuration.hpp" #include "mamba/core/context.hpp" #include "mamba/core/execution.hpp" #include "mamba/core/output.hpp" @@ -177,7 +178,7 @@ namespace mamba Context::Context(const ContextOptions& options) { on_ci = static_cast(util::get_env("CI")); - prefix_params.root_prefix = util::get_env("MAMBA_ROOT_PREFIX").value_or(""); + prefix_params.root_prefix = detail::get_root_prefix(); prefix_params.conda_prefix = prefix_params.root_prefix; envs_dirs = { prefix_params.root_prefix / "envs" }; diff --git a/libmamba/src/download/downloader.cpp b/libmamba/src/download/downloader.cpp index d695183b65..6f905fe68e 100644 --- a/libmamba/src/download/downloader.cpp +++ b/libmamba/src/download/downloader.cpp @@ -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" @@ -24,13 +25,18 @@ namespace mamba::download namespace { - constexpr std::array cert_locations{ + constexpr std::array 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) @@ -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 == "" && util::on_linux) + // TODO: Adapt the semantic of `` to decouple the use of CA certificates + // from `conda-forge::ca-certificates` and the system CA certificates. + else if (remote_fetch_params.ssl_verify == "") { + // 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; + } + + // 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; } } diff --git a/libmamba/tests/src/download/test_downloader.cpp b/libmamba/tests/src/download/test_downloader.cpp index 014ac827bf..612a1732f5 100644 --- a/libmamba/tests/src/download/test_downloader.cpp +++ b/libmamba/tests/src/download/test_downloader.cpp @@ -6,6 +6,7 @@ #include +#include "mamba/api/configuration.hpp" #include "mamba/download/downloader.hpp" #include "mambatests.hpp" @@ -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 = ""; + + 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)); + } } } diff --git a/micromamba/tests/test_create.py b/micromamba/tests/test_create.py index 6da8a61917..65a424d06f 100644 --- a/micromamba/tests/test_create.py +++ b/micromamba/tests/test_create.py @@ -1608,3 +1608,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