wslc trust Windows host Trusted Store certs (#40785)

yao-msft committed Jun 15, 2026 at 12:53 UTC f227b1c9b977cf8c9c4149606b2ab97c617e9059
10 files changed +526 -12
localization/strings/en-US/Resources.resw
+12
@@ -3284,4 +3284,16 @@ On first run, creates the file with all settings commented out at their defaults
3284 <value>Failed to release host resources for volume '{}'</value>
3285 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3286 </data>
3287 + <data name="MessageWslcImportCertsSkippedComputer" xml:space="preserve">
3288 + <value>Skipped importing {} certificates from Local Machine Trusted Root store due to certificate encoding failures</value>
3289 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3290 + </data>
3291 + <data name="MessageWslcImportCertsSkippedUser" xml:space="preserve">
3292 + <value>Skipped importing {} certificates from Current User Trusted Root store due to certificate encoding failures</value>
3293 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3294 + </data>
3295 + <data name="MessageWslcInstallCertsFailed" xml:space="preserve">
3296 + <value>Failed to install host trusted root certificates into the VM: {}</value>
3297 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3298 + </data>
3299 </root>
src/windows/wslcsession/CMakeLists.txt
+2
@@ -28,6 +28,7 @@ set(SOURCES
28 IORelay.cpp
29 OptionParser.cpp
30 ServiceProcessLauncher.cpp
31 + WindowsCertStore.cpp
32 )
33
34 set(HEADERS
@@ -38,6 +39,7 @@ set(HEADERS
39 ServiceProcessLauncher.h
40 WSLCContainer.h
41 WSLCContainerMetadata.h
42 + WindowsCertStore.h
43 WSLCProcess.h
44 WSLCProcessControl.h
45 WSLCProcessIO.h
src/windows/wslcsession/WSLCSession.cpp
+44
@@ -19,11 +19,14 @@ Abstract:
19 #include "WSLCNetworkMetadata.h"
20 #include "ContainerNameGenerator.h"
21 #include "ServiceProcessLauncher.h"
22 +#include "WindowsCertStore.h"
23 #include "WslCoreFilesystem.h"
24 #include "wslpolicies.h"
25
26 using namespace wsl::windows::common;
27 using io::MultiHandleWait;
28 +using io::OverlappedIOHandle;
29 +using io::WriteHandle;
30 using wsl::shared::Localization;
31 using wsl::windows::service::wslc::UserCOMCallback;
32 using wsl::windows::service::wslc::UserHandle;
@@ -348,6 +351,9 @@ try
351 // Configure storage.
352 ConfigureStorage(*Settings, tokenInfo->User.Sid);
353
354 + // Mirror the host's trusted root CAs into the VM before dockerd starts.
355 + InstallTrustedRootCertificates();
356 +
357 // Launch containerd first
358 StartContainerd();
359
@@ -595,6 +601,44 @@ void WSLCSession::StartDockerd()
601 WSL_LOG("DockerdStarted");
602 }
603
604 +void WSLCSession::InstallTrustedRootCertificates()
605 +try
606 +{
607 + const auto pem = CollectTrustedRootCertificatesPem();
608 + if (pem.empty())
609 + {
610 + WSL_LOG("InstallTrustedRootCertificatesSkipped");
611 + return;
612 + }
613 +
614 + // dockerd and containerd read the certificates found in /etc/ssl/certs into
615 + // their default system certificate pool.
616 + constexpr auto c_certPath = "/etc/ssl/certs/wsl-windows-roots.pem";
617 + const auto script = std::format("cat > '{}'", c_certPath);
618 +
619 + ServiceProcessLauncher launcher("/bin/sh", {"/bin/sh", "--norc", "-c", script}, {}, WSLCProcessFlagsStdin);
620 + auto process = launcher.Launch(*m_virtualMachine);
621 +
622 + std::unique_ptr<OverlappedIOHandle> writeStdin(
623 + new WriteHandle(process.GetStdHandle(WSLCFDStdin), std::vector<char>{pem.begin(), pem.end()}));
624 + std::vector<std::unique_ptr<OverlappedIOHandle>> extraHandles;
625 + extraHandles.emplace_back(std::move(writeStdin));
626 +
627 + const auto result = process.WaitAndCaptureOutput(60000UL, std::move(extraHandles));
628 + THROW_HR_IF_MSG(E_FAIL, result.Code != 0, "%hs", launcher.FormatResult(result).c_str());
629 +
630 + WSL_LOG(
631 + "InstalledTrustedRootCertificates",
632 + TraceLoggingValue(c_certPath, "Path"),
633 + TraceLoggingValue(static_cast<uint64_t>(pem.size()), "BundleBytes"));
634 +}
635 +catch (...)
636 +{
637 + // Best-effort: failing to install the host's trusted roots must not prevent the session from starting.
638 + LOG_CAUGHT_EXCEPTION_MSG("Failed to install trusted root certificates into the VM");
639 + EMIT_USER_WARNING(Localization::MessageWslcInstallCertsFailed(wslutil::GetErrorString(wil::ResultFromCaughtException())));
640 +}
641 +
642 void WSLCSession::StreamImageOperation(DockerHTTPClient::HTTPRequestContext& requestContext, LPCSTR Image, LPCSTR OperationName, IProgressCallback* ProgressCallback)
643 {
644 auto io = CreateIOContext();
src/windows/wslcsession/WSLCSession.h
+1
@@ -247,6 +247,7 @@ private:
247 void OnVmExited();
248 ServiceRunningProcess StartProcess(
249 const std::string& Executable, const std::vector<std::string>& Args, PCSTR LogSource, std::function<void()>&& ExitCallback);
250 + void InstallTrustedRootCertificates();
251 void StartContainerd();
252 void StartDockerd();
253 int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs);
src/windows/wslcsession/WindowsCertStore.cpp new
+123
@@ -0,0 +1,123 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WindowsCertStore.cpp
8 +
9 +Abstract:
10 +
11 + Implementation of helpers for reading the Windows certificate stores.
12 +
13 +--*/
14 +
15 +#include "precomp.h"
16 +#include "WindowsCertStore.h"
17 +#include <optional>
18 +#include <set>
19 +#include <wincrypt.h>
20 +
21 +namespace {
22 +
23 +// Converts a single DER-encoded certificate into a PEM block.
24 +// Returns nullopt if the certificate cannot be encoded.
25 +std::optional<std::string> TryEncodeCertificateAsPem(const CERT_CONTEXT& Cert)
26 +{
27 + DWORD pemSize = 0;
28 + if (!CryptBinaryToStringA(Cert.pbCertEncoded, Cert.cbCertEncoded, CRYPT_STRING_BASE64HEADER, nullptr, &pemSize))
29 + {
30 + LOG_LAST_ERROR_MSG("CryptBinaryToStringA (size query) failed for a root certificate; skipping it");
31 + return std::nullopt;
32 + }
33 +
34 + std::string pem(pemSize, '\0');
35 + if (!CryptBinaryToStringA(Cert.pbCertEncoded, Cert.cbCertEncoded, CRYPT_STRING_BASE64HEADER, pem.data(), &pemSize))
36 + {
37 + LOG_LAST_ERROR_MSG("CryptBinaryToStringA (encode) failed for a root certificate; skipping it");
38 + return std::nullopt;
39 + }
40 +
41 + // The pemSize after the actual write call does not include terminating null,
42 + // and is 1 less than the value returned by the earlier query call.
43 + pem.resize(pemSize);
44 +
45 + return pem;
46 +}
47 +
48 +// Enumerates every certificate in the given "ROOT" system store and appends each
49 +// (deduplicated by cert thumbprint) to the output PEM bundle. Returns number of skipped certs.
50 +int AppendRootStore(DWORD StoreFlags, std::set<std::string>& Seen, std::string& Pem)
51 +{
52 + const wil::unique_hcertstore store{CertOpenStore(
53 + CERT_STORE_PROV_SYSTEM_W, 0, NULL, StoreFlags | CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG, L"ROOT")};
54 + if (!store)
55 + {
56 + LOG_LAST_ERROR_MSG("CertOpenStore failed for ROOT store (flags 0x%x)", StoreFlags);
57 + return 0;
58 + }
59 +
60 + // N.B. CertEnumCertificatesInStore frees the context passed to it and returns the next one,
61 + // so the loop must not free the context itself.
62 + int skippedCount = 0;
63 + PCCERT_CONTEXT cert = nullptr;
64 + while ((cert = CertEnumCertificatesInStore(store.get(), cert)) != nullptr)
65 + {
66 + if (cert->cbCertEncoded == 0)
67 + {
68 + continue;
69 + }
70 +
71 + // Use cert Thumbprint for dedupe.
72 + BYTE hash[20];
73 + DWORD hashSize = sizeof(hash);
74 + if (!CertGetCertificateContextProperty(cert, CERT_SHA1_HASH_PROP_ID, hash, &hashSize))
75 + {
76 + LOG_LAST_ERROR_MSG("CertGetCertificateContextProperty(CERT_SHA1_HASH_PROP_ID) failed; skipping a root certificate");
77 + skippedCount++;
78 + continue;
79 + }
80 +
81 + if (!Seen.insert(std::string{reinterpret_cast<const char*>(hash), hashSize}).second)
82 + {
83 + continue;
84 + }
85 +
86 + if (auto pem = TryEncodeCertificateAsPem(*cert))
87 + {
88 + Pem += *pem;
89 + }
90 + else
91 + {
92 + skippedCount++;
93 + }
94 + }
95 +
96 + return skippedCount;
97 +}
98 +
99 +} // namespace
100 +
101 +namespace wsl::windows::service::wslc {
102 +
103 +std::string CollectTrustedRootCertificatesPem()
104 +{
105 + std::set<std::string> seen;
106 + std::string pem;
107 +
108 + auto skippedCount = AppendRootStore(CERT_SYSTEM_STORE_LOCAL_MACHINE, seen, pem);
109 + if (skippedCount > 0)
110 + {
111 + EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcImportCertsSkippedComputer(std::to_wstring(skippedCount)));
112 + }
113 +
114 + skippedCount = AppendRootStore(CERT_SYSTEM_STORE_CURRENT_USER, seen, pem);
115 + if (skippedCount > 0)
116 + {
117 + EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcImportCertsSkippedUser(std::to_wstring(skippedCount)));
118 + }
119 +
120 + return pem;
121 +}
122 +
123 +} // namespace wsl::windows::service::wslc
src/windows/wslcsession/WindowsCertStore.h new
+30
@@ -0,0 +1,30 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WindowsCertStore.h
8 +
9 +Abstract:
10 +
11 + Helpers for reading the Windows certificate stores so that the trusted root
12 + certificate authorities configured on the host can be mirrored into the
13 + container VM.
14 +
15 +--*/
16 +
17 +#pragma once
18 +
19 +#include <string>
20 +
21 +namespace wsl::windows::service::wslc {
22 +
23 +// Collects the certificates from the host's "Trusted Root Certification
24 +// Authorities" stores (both the local machine and current user stores) and
25 +// returns them serialized as a single PEM bundle.
26 +// Duplicate certificates that appear in more than one store are emitted once.
27 +// Returns an empty string if no certificates were found.
28 +std::string CollectTrustedRootCertificatesPem();
29 +
30 +} // namespace wsl::windows::service::wslc
test/windows/CMakeLists.txt
+2 -1
@@ -42,7 +42,8 @@ target_link_libraries(wsltests
42 Wer.lib
43 Dbghelp.lib
44 sfc.lib
45 - Crypt32.lib)
45 + Crypt32.lib
46 + Ncrypt.lib)
47
48 add_dependencies(wsltests wslserviceidl wslclib wslc wslcsdk wslcsdkwinrtidl)
49 add_subdirectory(testplugin)
test/windows/wslc/e2e/WSLCE2EHelpers.cpp
+38 -8
@@ -493,7 +493,8 @@ wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession()
493 return std::move(session);
494 }
495
496 -std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(IWSLCSession& session, const std::string& username, const std::string& password, USHORT port)
496 +std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(
497 + IWSLCSession& session, const std::string& username, const std::string& password, USHORT port, const std::wstring& tlsCertDir)
498 {
499 // Check if the registry image is already loaded on this session.
500 wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
@@ -507,6 +508,8 @@ std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(IWSLCSession& se
508 LoadTestImage(session, "wslc-registry:latest");
509 }
510
511 + const bool useTls = !tlsCertDir.empty();
512 +
513 std::vector<std::string> env = {std::format("REGISTRY_HTTP_ADDR=0.0.0.0:{}", port)};
514
515 if (!username.empty())
@@ -515,9 +518,25 @@ std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(IWSLCSession& se
518 env.push_back(std::format("PASSWORD={}", password));
519 }
520
518 - WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env);
521 + if (useTls)
522 + {
523 + env.push_back("REGISTRY_HTTP_TLS_CERTIFICATE=/certs/server.crt");
524 + env.push_back("REGISTRY_HTTP_TLS_KEY=/certs/server.key");
525 + }
526 +
527 + // TLS needs a non-loopback address for real verification, so use bridge networking and reach the
528 + // container by its bridge IP. Plain HTTP uses host networking with a published loopback port.
529 + WSLCContainerLauncher launcher("wslc-registry:latest", {}, {}, env, useTls ? "bridge" : "host");
530 launcher.SetEntrypoint({"/entrypoint.sh"});
520 - launcher.AddPort(port, port, AF_INET);
531 +
532 + if (useTls)
533 + {
534 + launcher.AddVolume(tlsCertDir, "/certs", true);
535 + }
536 + else
537 + {
538 + launcher.AddPort(port, port, AF_INET);
539 + }
540
541 auto container = launcher.Launch(session);
542
@@ -525,13 +544,24 @@ std::pair<RunningWSLCContainer, std::string> StartLocalRegistry(IWSLCSession& se
544 auto initProcess = container.GetInitProcess();
545 WaitForOutput(initProcess.GetStdHandle(2), std::format("listening on [::]:{}", port));
546
528 - auto address = std::format("127.0.0.1:{}", port);
529 - auto url = std::format(L"http://{}/v2/", wsl::shared::string::MultiByteToWide(address));
547 + if (useTls)
548 + {
549 + auto inspect = container.Inspect();
550 + THROW_HR_IF(E_UNEXPECTED, inspect.NetworkSettings.Networks.empty());
551 + auto address = std::format("{}:{}", inspect.NetworkSettings.Networks.begin()->second.IPAddress, port);
552
531 - int expectedCode = username.empty() ? 200 : 401;
532 - ExpectHttpResponse(url.c_str(), expectedCode, true);
553 + return {std::move(container), std::move(address)};
554 + }
555 + else
556 + {
557 + auto address = std::format("127.0.0.1:{}", port);
558 + auto url = std::format(L"http://{}/v2/", wsl::shared::string::MultiByteToWide(address));
559 +
560 + int expectedCode = username.empty() ? 200 : 401;
561 + ExpectHttpResponse(url.c_str(), expectedCode, true);
562
534 - return {std::move(container), std::move(address)};
563 + return {std::move(container), std::move(address)};
564 + }
565 }
566
567 std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress)
test/windows/wslc/e2e/WSLCE2EHelpers.h
+12 -3
@@ -97,6 +97,11 @@ struct TestSession
97 return m_storagePath;
98 }
99
100 + IWSLCSession& Session() const
101 + {
102 + return *m_session;
103 + }
104 +
105 private:
106 std::wstring m_name;
107 std::filesystem::path m_storagePath;
@@ -180,10 +185,14 @@ wil::com_ptr<IWSLCSession> OpenDefaultElevatedSession();
185
186 void VerifyPseudoConsoleTtySize(WSLCInteractiveSession& session, SHORT columns, SHORT rows);
187
183 -// Starts a local registry container with host networking using the COM API.
184 -// Returns the running container (holds it alive) and the registry address (e.g. "127.0.0.1:PORT").
188 +// Starts a local registry container using the COM API and returns the running container (holds it
189 +// alive) plus the registry address. Host network for plain http, bridge network for tls enabled.
190 std::pair<wsl::windows::common::RunningWSLCContainer, std::string> StartLocalRegistry(
186 - IWSLCSession& session, const std::string& username = "", const std::string& password = "", USHORT port = 5000);
191 + IWSLCSession& session,
192 + const std::string& username = "",
193 + const std::string& password = "",
194 + USHORT port = 5000,
195 + const std::wstring& tlsCertDir = L"");
196
197 // Tags an image for a registry and returns the full registry image reference (e.g. "127.0.0.1:PORT/debian:latest").
198 std::wstring TagImageForRegistry(const std::wstring& imageName, const std::wstring& registryAddress);
test/windows/wslc/e2e/WSLCE2ETlsRegistryTests.cpp new
+262
@@ -0,0 +1,262 @@
1 +/*++
2 +
3 +Copyright (c) Microsoft. All rights reserved.
4 +
5 +Module Name:
6 +
7 + WSLCE2ETlsRegistryTests.cpp
8 +
9 +Abstract:
10 +
11 + End-to-end test for private-registry SSL trust. Stands up a registry that serves TLS with a
12 + private CA, verifies that a push fails with an "unknown authority" error while the CA is not
13 + trusted, then adds the CA to the machine's Trusted Root store and verifies that a push from a
14 + freshly created session succeeds.
15 +
16 +--*/
17 +
18 +#include "precomp.h"
19 +#include "windows/Common.h"
20 +#include "WSLCExecutor.h"
21 +#include "WSLCE2EHelpers.h"
22 +#include <wslutil.h>
23 +#include <ncrypt.h>
24 +
25 +namespace WSLCE2ETests {
26 +using namespace wsl::shared;
27 +using namespace wsl::windows::common;
28 +
29 +namespace {
30 + // The bridge IP assigned to the first container started in a fresh session. The registry is always
31 + // the first container we start in this test's session, so this is deterministic.
32 + constexpr auto c_registryIp = "172.17.0.2";
33 + constexpr USHORT c_registryPort = 5000;
34 +
35 + // DER-encodes a certificate-extension value structure (e.g. CERT_ALT_NAME_INFO for the SAN OID).
36 + std::vector<BYTE> EncodeCertExtensionValue(LPCSTR Oid, const void* StructInfo)
37 + {
38 + DWORD size = 0;
39 + THROW_IF_WIN32_BOOL_FALSE(CryptEncodeObjectEx(X509_ASN_ENCODING, Oid, StructInfo, 0, nullptr, nullptr, &size));
40 + std::vector<BYTE> encoded(size);
41 + THROW_IF_WIN32_BOOL_FALSE(CryptEncodeObjectEx(X509_ASN_ENCODING, Oid, StructInfo, 0, nullptr, encoded.data(), &size));
42 + encoded.resize(size);
43 + return encoded;
44 + }
45 +
46 + // Wraps DER bytes in a PEM block with the given label (e.g. "CERTIFICATE", "PRIVATE KEY").
47 + std::string ToPem(const BYTE* Data, DWORD Size, const std::string& Label)
48 + {
49 + DWORD base64Size = 0;
50 + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA(Data, Size, CRYPT_STRING_BASE64, nullptr, &base64Size));
51 + std::string base64(base64Size, '\0');
52 + THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA(Data, Size, CRYPT_STRING_BASE64, base64.data(), &base64Size));
53 + base64.resize(base64Size);
54 + return std::format("-----BEGIN {}-----\n{}-----END {}-----\n", Label, base64, Label);
55 + }
56 +
57 + // Generates a self-signed certificate usable as both a TLS server cert and a CA,
58 + // writes server.crt and server.key (PEM) into OutDir for the registry to serve,
59 + // and returns the certificate context so the test can add it to a trust store later.
60 + wil::unique_cert_context GenerateRegistryTlsCertificate(const std::string& IpAddress, const std::filesystem::path& OutDir)
61 + {
62 + // Create an exportable RSA key
63 + NCRYPT_PROV_HANDLE prov{};
64 + THROW_IF_FAILED(NCryptOpenStorageProvider(&prov, MS_KEY_STORAGE_PROVIDER, 0));
65 + auto freeProv = wil::scope_exit([&] { NCryptFreeObject(prov); });
66 +
67 + GUID guid{};
68 + THROW_IF_FAILED(CoCreateGuid(&guid));
69 + const auto keyName =
70 + L"WslcE2eTlsKey-" + wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::None);
71 +
72 + NCRYPT_KEY_HANDLE key{};
73 + THROW_IF_FAILED(NCryptCreatePersistedKey(prov, &key, BCRYPT_RSA_ALGORITHM, keyName.c_str(), 0, 0));
74 + auto deleteKey = wil::scope_exit([&] { NCryptDeleteKey(key, 0); }); // also frees the handle
75 +
76 + DWORD keyLength = 2048;
77 + THROW_IF_FAILED(NCryptSetProperty(key, NCRYPT_LENGTH_PROPERTY, reinterpret_cast<PBYTE>(&keyLength), sizeof(keyLength), 0));
78 + DWORD exportPolicy = NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
79 + THROW_IF_FAILED(NCryptSetProperty(key, NCRYPT_EXPORT_POLICY_PROPERTY, reinterpret_cast<PBYTE>(&exportPolicy), sizeof(exportPolicy), 0));
80 + THROW_IF_FAILED(NCryptFinalizeKey(key, 0));
81 +
82 + // Build the extensions (SAN, basic constraints, key usage, extended key usage)
83 + in_addr ipv4{};
84 + wslutil::ParseIpv4Address(IpAddress.c_str(), ipv4);
85 + CERT_ALT_NAME_ENTRY altEntry{};
86 + altEntry.dwAltNameChoice = CERT_ALT_NAME_IP_ADDRESS;
87 + altEntry.IPAddress.cbData = sizeof(ipv4);
88 + altEntry.IPAddress.pbData = reinterpret_cast<BYTE*>(&ipv4);
89 + CERT_ALT_NAME_INFO altInfo{1, &altEntry};
90 + auto sanEncoded = EncodeCertExtensionValue(szOID_SUBJECT_ALT_NAME2, &altInfo);
91 +
92 + CERT_BASIC_CONSTRAINTS2_INFO basicConstraints{};
93 + basicConstraints.fCA = TRUE;
94 + auto bcEncoded = EncodeCertExtensionValue(szOID_BASIC_CONSTRAINTS2, &basicConstraints);
95 +
96 + BYTE keyUsageBits = CERT_DIGITAL_SIGNATURE_KEY_USAGE | CERT_KEY_ENCIPHERMENT_KEY_USAGE | CERT_KEY_CERT_SIGN_KEY_USAGE;
97 + CRYPT_BIT_BLOB keyUsage{};
98 + keyUsage.cbData = 1;
99 + keyUsage.pbData = &keyUsageBits;
100 + auto kuEncoded = EncodeCertExtensionValue(szOID_KEY_USAGE, &keyUsage);
101 +
102 + LPSTR serverAuthOid = const_cast<LPSTR>(szOID_PKIX_KP_SERVER_AUTH);
103 + CERT_ENHKEY_USAGE enhKeyUsage{1, &serverAuthOid};
104 + auto ekuEncoded = EncodeCertExtensionValue(szOID_ENHANCED_KEY_USAGE, &enhKeyUsage);
105 +
106 + CERT_EXTENSION extensions[4]{};
107 + extensions[0] = {const_cast<LPSTR>(szOID_SUBJECT_ALT_NAME2), FALSE, {static_cast<DWORD>(sanEncoded.size()), sanEncoded.data()}};
108 + extensions[1] = {const_cast<LPSTR>(szOID_BASIC_CONSTRAINTS2), TRUE, {static_cast<DWORD>(bcEncoded.size()), bcEncoded.data()}};
109 + extensions[2] = {const_cast<LPSTR>(szOID_KEY_USAGE), TRUE, {static_cast<DWORD>(kuEncoded.size()), kuEncoded.data()}};
110 + extensions[3] = {const_cast<LPSTR>(szOID_ENHANCED_KEY_USAGE), FALSE, {static_cast<DWORD>(ekuEncoded.size()), ekuEncoded.data()}};
111 + CERT_EXTENSIONS certExtensions{ARRAYSIZE(extensions), extensions};
112 +
113 + // Subject / issuer name.
114 + const auto subject = L"CN=" + wsl::shared::string::MultiByteToWide(IpAddress);
115 + DWORD nameSize = 0;
116 + THROW_IF_WIN32_BOOL_FALSE(CertStrToNameW(X509_ASN_ENCODING, subject.c_str(), CERT_X500_NAME_STR, nullptr, nullptr, &nameSize, nullptr));
117 + std::vector<BYTE> nameBlob(nameSize);
118 + THROW_IF_WIN32_BOOL_FALSE(
119 + CertStrToNameW(X509_ASN_ENCODING, subject.c_str(), CERT_X500_NAME_STR, nullptr, nameBlob.data(), &nameSize, nullptr));
120 + CERT_NAME_BLOB subjectBlob{nameSize, nameBlob.data()};
121 +
122 + CRYPT_KEY_PROV_INFO keyProvInfo{};
123 + keyProvInfo.pwszContainerName = const_cast<LPWSTR>(keyName.c_str());
124 + keyProvInfo.pwszProvName = const_cast<LPWSTR>(MS_KEY_STORAGE_PROVIDER);
125 +
126 + CRYPT_ALGORITHM_IDENTIFIER signatureAlgorithm{};
127 + signatureAlgorithm.pszObjId = const_cast<LPSTR>(szOID_RSA_SHA256RSA);
128 +
129 + FILETIME nowFt{};
130 + GetSystemTimeAsFileTime(&nowFt);
131 + ULARGE_INTEGER ticks{};
132 + ticks.LowPart = nowFt.dwLowDateTime;
133 + ticks.HighPart = nowFt.dwHighDateTime;
134 + ticks.QuadPart += 100ULL * 24 * 60 * 60 * 10'000'000; // 100 days, in 100-ns intervals
135 + FILETIME endFt{ticks.LowPart, ticks.HighPart};
136 + SYSTEMTIME notAfter{};
137 + THROW_IF_WIN32_BOOL_FALSE(FileTimeToSystemTime(&endFt, &notAfter));
138 +
139 + wil::unique_cert_context cert{CertCreateSelfSignCertificate(
140 + key, &subjectBlob, 0, &keyProvInfo, &signatureAlgorithm, nullptr, &notAfter, &certExtensions)};
141 + THROW_LAST_ERROR_IF_NULL(cert);
142 +
143 + // Export server.crt and server.key
144 + const auto certPem = ToPem(cert.get()->pbCertEncoded, cert.get()->cbCertEncoded, "CERTIFICATE");
145 +
146 + DWORD keyBlobSize = 0;
147 + THROW_IF_FAILED(NCryptExportKey(key, 0, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, nullptr, nullptr, 0, &keyBlobSize, 0));
148 + std::vector<BYTE> keyBlob(keyBlobSize);
149 + THROW_IF_FAILED(NCryptExportKey(key, 0, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, nullptr, keyBlob.data(), keyBlobSize, &keyBlobSize, 0));
150 + const auto keyPem = ToPem(keyBlob.data(), keyBlobSize, "PRIVATE KEY");
151 +
152 + const auto writeFile = [](const std::filesystem::path& path, const std::string& contents) {
153 + std::ofstream stream(path, std::ios::binary | std::ios::trunc);
154 + THROW_HR_IF_MSG(E_FAIL, !stream.is_open(), "Failed to open %ls for writing", path.c_str());
155 + stream.write(contents.data(), contents.size());
156 + };
157 + writeFile(OutDir / "server.crt", certPem);
158 + writeFile(OutDir / "server.key", keyPem);
159 +
160 + return cert;
161 + }
162 +
163 + // Helper that adds a certificate to the machine's Trusted Root store and removes it on destruction.
164 + class TrustedRootCertificate
165 + {
166 + public:
167 + NON_COPYABLE(TrustedRootCertificate);
168 + NON_MOVABLE(TrustedRootCertificate);
169 +
170 + explicit TrustedRootCertificate(const CERT_CONTEXT& Cert)
171 + {
172 + m_store.reset(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, NULL, CERT_SYSTEM_STORE_LOCAL_MACHINE, L"ROOT"));
173 + THROW_LAST_ERROR_IF_NULL(m_store);
174 +
175 + PCCERT_CONTEXT added{};
176 + THROW_IF_WIN32_BOOL_FALSE(CertAddEncodedCertificateToStore(
177 + m_store.get(), X509_ASN_ENCODING, Cert.pbCertEncoded, Cert.cbCertEncoded, CERT_STORE_ADD_REPLACE_EXISTING, &added));
178 + m_added.reset(added);
179 + }
180 +
181 + ~TrustedRootCertificate()
182 + {
183 + if (m_added)
184 + {
185 + // CertDeleteCertificateFromStore frees the context; the store is still open here.
186 + LOG_IF_WIN32_BOOL_FALSE(CertDeleteCertificateFromStore(m_added.release()));
187 + }
188 + }
189 +
190 + private:
191 + wil::unique_hcertstore m_store;
192 + wil::unique_cert_context m_added;
193 + };
194 +
195 +} // namespace
196 +
197 +class WSLCE2ETlsRegistryTests
198 +{
199 + WSLC_TEST_CLASS(WSLCE2ETlsRegistryTests)
200 +
201 + // Verifies registry SSL trust end to end: a push to a registry serving a private-CA cert fails
202 + // with "unknown authority" and succeeds after the CA is added to the host Trusted Root store.
203 + WSLC_TEST_METHOD(WSLCE2E_Registry_PrivateCa_SslTrust)
204 + {
205 + const auto& image = AlpineTestImage();
206 + const auto registryImage =
207 + std::format(L"{}:{}/{}", wsl::shared::string::MultiByteToWide(c_registryIp), c_registryPort, image.NameAndTag());
208 +
209 + // Generate the registry's certs.
210 + const auto certDir = std::filesystem::temp_directory_path() / std::format(L"wslc-tls-registry-{}", GetCurrentProcessId());
211 + std::filesystem::create_directories(certDir);
212 + auto removeCertDir = wil::scope_exit([&] {
213 + std::error_code ec;
214 + std::filesystem::remove_all(certDir, ec);
215 + });
216 +
217 + auto caCert = GenerateRegistryTlsCertificate(c_registryIp, certDir);
218 +
219 + // CA NOT trusted: the push fails with unknown authority.
220 + {
221 + auto session = TestSession::Create(L"wslc-tls-untrusted");
222 +
223 + auto cleanup = wil::scope_exit([&] { EnsureSessionIsTerminated(L"wslc-tls-untrusted"); });
224 +
225 + EnsureImageIsLoaded(image, session.Name());
226 +
227 + auto [registry, address] = StartLocalRegistry(session.Session(), "", "", c_registryPort, certDir.wstring());
228 + VERIFY_ARE_EQUAL(std::format("{}:{}", c_registryIp, c_registryPort), address);
229 +
230 + RunWslcAndVerify(std::format(L"image tag {} {} --session {}", image.NameAndTag(), registryImage, session.Name()), {.ExitCode = 0});
231 +
232 + auto result = RunWslc(std::format(L"push {} --session {}", registryImage, session.Name()));
233 + VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0), L"Push should fail while the CA is not trusted");
234 + VERIFY_IS_TRUE(result.Stderr.has_value());
235 + VERIFY_IS_TRUE(
236 + result.Stderr->find(L"certificate signed by unknown authority") != std::wstring::npos,
237 + L"Expected an untrusted-certificate error");
238 + }
239 +
240 + // CA trusted: push succeeds.
241 + {
242 + // Trust the CA on the host.
243 + TrustedRootCertificate trustedCa{*caCert.get()};
244 +
245 + auto session = TestSession::Create(L"wslc-tls-trusted");
246 +
247 + auto cleanup = wil::scope_exit([&] { EnsureSessionIsTerminated(L"wslc-tls-trusted"); });
248 +
249 + EnsureImageIsLoaded(image, session.Name());
250 +
251 + auto [registry, address] = StartLocalRegistry(session.Session(), "", "", c_registryPort, certDir.wstring());
252 + VERIFY_ARE_EQUAL(std::format("{}:{}", c_registryIp, c_registryPort), address);
253 +
254 + RunWslcAndVerify(std::format(L"image tag {} {} --session {}", image.NameAndTag(), registryImage, session.Name()), {.ExitCode = 0});
255 +
256 + auto result = RunWslc(std::format(L"push {} --session {}", registryImage, session.Name()));
257 + VERIFY_ARE_EQUAL(0u, result.ExitCode.value_or(1), L"Push should succeed once the CA is trusted");
258 + }
259 + }
260 +};
261 +
262 +} // namespace WSLCE2ETests