master
cpp 266 lines 12.4 KB
Raw
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 "TestImageRegistry.h"
23 #include <wslutil.h>
24 #include <ncrypt.h>
25
26 namespace WSLCE2ETests {
27 using namespace wsl::shared;
28
29 namespace wslutil = wsl::windows::common::wslutil;
30
31 namespace {
32 // The bridge IP assigned to the first container started in a fresh session. The registry is always
33 // the first container we start in this test's session, so this is deterministic.
34 constexpr auto c_registryIp = "172.17.0.2";
35 constexpr USHORT c_registryPort = 5000;
36
37 // DER-encodes a certificate-extension value structure (e.g. CERT_ALT_NAME_INFO for the SAN OID).
38 std::vector<BYTE> EncodeCertExtensionValue(LPCSTR Oid, const void* StructInfo)
39 {
40 DWORD size = 0;
41 THROW_IF_WIN32_BOOL_FALSE(CryptEncodeObjectEx(X509_ASN_ENCODING, Oid, StructInfo, 0, nullptr, nullptr, &size));
42 std::vector<BYTE> encoded(size);
43 THROW_IF_WIN32_BOOL_FALSE(CryptEncodeObjectEx(X509_ASN_ENCODING, Oid, StructInfo, 0, nullptr, encoded.data(), &size));
44 encoded.resize(size);
45 return encoded;
46 }
47
48 // Wraps DER bytes in a PEM block with the given label (e.g. "CERTIFICATE", "PRIVATE KEY").
49 std::string ToPem(const BYTE* Data, DWORD Size, const std::string& Label)
50 {
51 DWORD base64Size = 0;
52 THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA(Data, Size, CRYPT_STRING_BASE64, nullptr, &base64Size));
53 std::string base64(base64Size, '\0');
54 THROW_IF_WIN32_BOOL_FALSE(CryptBinaryToStringA(Data, Size, CRYPT_STRING_BASE64, base64.data(), &base64Size));
55 base64.resize(base64Size);
56 return std::format("-----BEGIN {}-----\n{}-----END {}-----\n", Label, base64, Label);
57 }
58
59 // Generates a self-signed certificate usable as both a TLS server cert and a CA,
60 // writes server.crt and server.key (PEM) into OutDir for the registry to serve,
61 // and returns the certificate context so the test can add it to a trust store later.
62 wil::unique_cert_context GenerateRegistryTlsCertificate(const std::string& IpAddress, const std::filesystem::path& OutDir)
63 {
64 // Create an exportable RSA key
65 NCRYPT_PROV_HANDLE prov{};
66 THROW_IF_FAILED(NCryptOpenStorageProvider(&prov, MS_KEY_STORAGE_PROVIDER, 0));
67 auto freeProv = wil::scope_exit([&] { NCryptFreeObject(prov); });
68
69 GUID guid{};
70 THROW_IF_FAILED(CoCreateGuid(&guid));
71 const auto keyName =
72 L"WslcE2eTlsKey-" + wsl::shared::string::GuidToString<wchar_t>(guid, wsl::shared::string::GuidToStringFlags::None);
73
74 NCRYPT_KEY_HANDLE key{};
75 THROW_IF_FAILED(NCryptCreatePersistedKey(prov, &key, BCRYPT_RSA_ALGORITHM, keyName.c_str(), 0, 0));
76 auto deleteKey = wil::scope_exit([&] { NCryptDeleteKey(key, 0); }); // also frees the handle
77
78 DWORD keyLength = 2048;
79 THROW_IF_FAILED(NCryptSetProperty(key, NCRYPT_LENGTH_PROPERTY, reinterpret_cast<PBYTE>(&keyLength), sizeof(keyLength), 0));
80 DWORD exportPolicy = NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
81 THROW_IF_FAILED(NCryptSetProperty(key, NCRYPT_EXPORT_POLICY_PROPERTY, reinterpret_cast<PBYTE>(&exportPolicy), sizeof(exportPolicy), 0));
82 THROW_IF_FAILED(NCryptFinalizeKey(key, 0));
83
84 // Build the extensions (SAN, basic constraints, key usage, extended key usage)
85 in_addr ipv4{};
86 wslutil::ParseIpv4Address(IpAddress.c_str(), ipv4);
87 CERT_ALT_NAME_ENTRY altEntry{};
88 altEntry.dwAltNameChoice = CERT_ALT_NAME_IP_ADDRESS;
89 altEntry.IPAddress.cbData = sizeof(ipv4);
90 altEntry.IPAddress.pbData = reinterpret_cast<BYTE*>(&ipv4);
91 CERT_ALT_NAME_INFO altInfo{1, &altEntry};
92 auto sanEncoded = EncodeCertExtensionValue(szOID_SUBJECT_ALT_NAME2, &altInfo);
93
94 CERT_BASIC_CONSTRAINTS2_INFO basicConstraints{};
95 basicConstraints.fCA = TRUE;
96 auto bcEncoded = EncodeCertExtensionValue(szOID_BASIC_CONSTRAINTS2, &basicConstraints);
97
98 BYTE keyUsageBits = CERT_DIGITAL_SIGNATURE_KEY_USAGE | CERT_KEY_ENCIPHERMENT_KEY_USAGE | CERT_KEY_CERT_SIGN_KEY_USAGE;
99 CRYPT_BIT_BLOB keyUsage{};
100 keyUsage.cbData = 1;
101 keyUsage.pbData = &keyUsageBits;
102 auto kuEncoded = EncodeCertExtensionValue(szOID_KEY_USAGE, &keyUsage);
103
104 LPSTR serverAuthOid = const_cast<LPSTR>(szOID_PKIX_KP_SERVER_AUTH);
105 CERT_ENHKEY_USAGE enhKeyUsage{1, &serverAuthOid};
106 auto ekuEncoded = EncodeCertExtensionValue(szOID_ENHANCED_KEY_USAGE, &enhKeyUsage);
107
108 CERT_EXTENSION extensions[4]{};
109 extensions[0] = {const_cast<LPSTR>(szOID_SUBJECT_ALT_NAME2), FALSE, {static_cast<DWORD>(sanEncoded.size()), sanEncoded.data()}};
110 extensions[1] = {const_cast<LPSTR>(szOID_BASIC_CONSTRAINTS2), TRUE, {static_cast<DWORD>(bcEncoded.size()), bcEncoded.data()}};
111 extensions[2] = {const_cast<LPSTR>(szOID_KEY_USAGE), TRUE, {static_cast<DWORD>(kuEncoded.size()), kuEncoded.data()}};
112 extensions[3] = {const_cast<LPSTR>(szOID_ENHANCED_KEY_USAGE), FALSE, {static_cast<DWORD>(ekuEncoded.size()), ekuEncoded.data()}};
113 CERT_EXTENSIONS certExtensions{ARRAYSIZE(extensions), extensions};
114
115 // Subject / issuer name.
116 const auto subject = L"CN=" + wsl::shared::string::MultiByteToWide(IpAddress);
117 DWORD nameSize = 0;
118 THROW_IF_WIN32_BOOL_FALSE(CertStrToNameW(X509_ASN_ENCODING, subject.c_str(), CERT_X500_NAME_STR, nullptr, nullptr, &nameSize, nullptr));
119 std::vector<BYTE> nameBlob(nameSize);
120 THROW_IF_WIN32_BOOL_FALSE(
121 CertStrToNameW(X509_ASN_ENCODING, subject.c_str(), CERT_X500_NAME_STR, nullptr, nameBlob.data(), &nameSize, nullptr));
122 CERT_NAME_BLOB subjectBlob{nameSize, nameBlob.data()};
123
124 CRYPT_KEY_PROV_INFO keyProvInfo{};
125 keyProvInfo.pwszContainerName = const_cast<LPWSTR>(keyName.c_str());
126 keyProvInfo.pwszProvName = const_cast<LPWSTR>(MS_KEY_STORAGE_PROVIDER);
127
128 CRYPT_ALGORITHM_IDENTIFIER signatureAlgorithm{};
129 signatureAlgorithm.pszObjId = const_cast<LPSTR>(szOID_RSA_SHA256RSA);
130
131 FILETIME nowFt{};
132 GetSystemTimeAsFileTime(&nowFt);
133 ULARGE_INTEGER ticks{};
134 ticks.LowPart = nowFt.dwLowDateTime;
135 ticks.HighPart = nowFt.dwHighDateTime;
136 ticks.QuadPart += 100ULL * 24 * 60 * 60 * 10'000'000; // 100 days, in 100-ns intervals
137 FILETIME endFt{ticks.LowPart, ticks.HighPart};
138 SYSTEMTIME notAfter{};
139 THROW_IF_WIN32_BOOL_FALSE(FileTimeToSystemTime(&endFt, &notAfter));
140
141 wil::unique_cert_context cert{CertCreateSelfSignCertificate(
142 key, &subjectBlob, 0, &keyProvInfo, &signatureAlgorithm, nullptr, &notAfter, &certExtensions)};
143 THROW_LAST_ERROR_IF_NULL(cert);
144
145 // Export server.crt and server.key
146 const auto certPem = ToPem(cert.get()->pbCertEncoded, cert.get()->cbCertEncoded, "CERTIFICATE");
147
148 DWORD keyBlobSize = 0;
149 THROW_IF_FAILED(NCryptExportKey(key, 0, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, nullptr, nullptr, 0, &keyBlobSize, 0));
150 std::vector<BYTE> keyBlob(keyBlobSize);
151 THROW_IF_FAILED(NCryptExportKey(key, 0, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, nullptr, keyBlob.data(), keyBlobSize, &keyBlobSize, 0));
152 const auto keyPem = ToPem(keyBlob.data(), keyBlobSize, "PRIVATE KEY");
153
154 const auto writeFile = [](const std::filesystem::path& path, const std::string& contents) {
155 std::ofstream stream(path, std::ios::binary | std::ios::trunc);
156 THROW_HR_IF_MSG(E_FAIL, !stream.is_open(), "Failed to open %ls for writing", path.c_str());
157 stream.write(contents.data(), contents.size());
158 };
159 writeFile(OutDir / "server.crt", certPem);
160 writeFile(OutDir / "server.key", keyPem);
161
162 return cert;
163 }
164
165 // Helper that adds a certificate to the machine's Trusted Root store and removes it on destruction.
166 class TrustedRootCertificate
167 {
168 public:
169 NON_COPYABLE(TrustedRootCertificate);
170 NON_MOVABLE(TrustedRootCertificate);
171
172 explicit TrustedRootCertificate(const CERT_CONTEXT& Cert)
173 {
174 m_store.reset(CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, NULL, CERT_SYSTEM_STORE_LOCAL_MACHINE, L"ROOT"));
175 THROW_LAST_ERROR_IF_NULL(m_store);
176
177 PCCERT_CONTEXT added{};
178 THROW_IF_WIN32_BOOL_FALSE(CertAddEncodedCertificateToStore(
179 m_store.get(), X509_ASN_ENCODING, Cert.pbCertEncoded, Cert.cbCertEncoded, CERT_STORE_ADD_REPLACE_EXISTING, &added));
180 m_added.reset(added);
181 }
182
183 ~TrustedRootCertificate()
184 {
185 if (m_added)
186 {
187 // CertDeleteCertificateFromStore frees the context; the store is still open here.
188 LOG_IF_WIN32_BOOL_FALSE(CertDeleteCertificateFromStore(m_added.release()));
189 }
190 }
191
192 private:
193 wil::unique_hcertstore m_store;
194 wil::unique_cert_context m_added;
195 };
196
197 } // namespace
198
199 class WSLCE2ETlsRegistryTests
200 {
201 WSLC_TEST_CLASS(WSLCE2ETlsRegistryTests)
202
203 // Verifies registry SSL trust end to end: a push to a registry serving a private-CA cert fails
204 // with "unknown authority" and succeeds after the CA is added to the host Trusted Root store.
205 WSLC_TEST_METHOD(WSLCE2E_Registry_PrivateCa_SslTrust)
206 {
207 const auto& image = AlpineTestImage();
208 const auto registryImage =
209 std::format(L"{}:{}/{}", wsl::shared::string::MultiByteToWide(c_registryIp), c_registryPort, image.NameAndTag());
210
211 // Generate the registry's certs.
212 const auto certDir = std::filesystem::temp_directory_path() / std::format(L"wslc-tls-registry-{}", GetCurrentProcessId());
213 std::filesystem::create_directories(certDir);
214 auto removeCertDir = wil::scope_exit([&] {
215 std::error_code ec;
216 std::filesystem::remove_all(certDir, ec);
217 });
218
219 auto caCert = GenerateRegistryTlsCertificate(c_registryIp, certDir);
220
221 // CA NOT trusted: the push fails with unknown authority.
222 {
223 auto session = TestSession::Create(L"wslc-tls-untrusted");
224
225 auto cleanup = wil::scope_exit([&] { EnsureSessionIsTerminated(L"wslc-tls-untrusted"); });
226
227 TestImageRegistry::Instance().EnsureLoaded(image, session.Name());
228
229 auto [registry, address] = StartLocalRegistry(session.Session(), "", "", c_registryPort, certDir.wstring());
230 VERIFY_ARE_EQUAL(std::format("{}:{}", c_registryIp, c_registryPort), address);
231
232 RunWslcAndVerify(
233 std::format(L"--session \"{}\" image tag {} {}", session.Name(), image.NameAndTag(), registryImage), {.ExitCode = 0});
234
235 auto result = RunWslc(std::format(L"--session \"{}\" push {}", session.Name(), registryImage));
236 VERIFY_ARE_EQUAL(1u, result.ExitCode.value_or(0), L"Push should fail while the CA is not trusted");
237 VERIFY_IS_TRUE(result.Stderr.has_value());
238 VERIFY_IS_TRUE(
239 result.Stderr->find(L"certificate signed by unknown authority") != std::wstring::npos,
240 L"Expected an untrusted-certificate error");
241 }
242
243 // CA trusted: push succeeds.
244 {
245 // Trust the CA on the host.
246 TrustedRootCertificate trustedCa{*caCert.get()};
247
248 auto session = TestSession::Create(L"wslc-tls-trusted");
249
250 auto cleanup = wil::scope_exit([&] { EnsureSessionIsTerminated(L"wslc-tls-trusted"); });
251
252 TestImageRegistry::Instance().EnsureLoaded(image, session.Name());
253
254 auto [registry, address] = StartLocalRegistry(session.Session(), "", "", c_registryPort, certDir.wstring());
255 VERIFY_ARE_EQUAL(std::format("{}:{}", c_registryIp, c_registryPort), address);
256
257 RunWslcAndVerify(
258 std::format(L"--session \"{}\" image tag {} {}", session.Name(), image.NameAndTag(), registryImage), {.ExitCode = 0});
259
260 auto result = RunWslc(std::format(L"--session \"{}\" push {}", session.Name(), registryImage));
261 VERIFY_ARE_EQUAL(0u, result.ExitCode.value_or(1), L"Push should succeed once the CA is trusted");
262 }
263 }
264 };
265
266 } // namespace WSLCE2ETests