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, ¬After));
138
+
139
+ wil::unique_cert_context cert{CertCreateSelfSignCertificate(
140
+ key, &subjectBlob, 0, &keyProvInfo, &signatureAlgorithm, nullptr, ¬After, &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