Skip to content

Commit cf184b7

Browse files
authored
In-proc certificate pinning validation override (#6233)
## 📖 Description Adds the ability to override the certificate pinning validation with their own handler for in-proc COM callers. In-proc callers can provide a handler delegate on their `PackageCatalogReference` object(s) before calling `Connect` and will receive a callback when winget would execute a certificate pinning validation. If they accept the server connection, the certificate will be cached as it is with the internal check and automatically approved for any future connections. Note that `Connect` may not actually trigger the callback if the `/information` for the catalog is already cached, but you must set the callback before the `Connect` call for it to be attached to the connected catalog for future use. The callback can only be set for the MS Store catalog when the group policy `BypassCertificatePinningForMicrosoftStore` is not configured.
1 parent dcd3581 commit cf184b7

31 files changed

Lines changed: 907 additions & 61 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0-windows10.0.26100.0</TargetFramework>
6+
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
7+
<Nullable>enable</Nullable>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<Platforms>x64;x86;arm64</Platforms>
10+
<!-- Override these to match the platform/configuration you built the solution with. -->
11+
<WinGetBuildPlatform Condition="'$(WinGetBuildPlatform)' == ''">x64</WinGetBuildPlatform>
12+
<WinGetBuildConfiguration Condition="'$(WinGetBuildConfiguration)' == ''">Debug</WinGetBuildConfiguration>
13+
<!-- Platform-specific build output root. -->
14+
<WinGetSrcBinRoot>$([MSBuild]::NormalizePath('$(MSBuildThisFileDirectory)..\..\src\$(WinGetBuildPlatform)\$(WinGetBuildConfiguration)'))</WinGetSrcBinRoot>
15+
</PropertyGroup>
16+
17+
<!--
18+
Generates a CsWinRT projection from the built winmd so this sample needs no published NuGet package.
19+
Build the solution first (src\AppInstallerCLI.sln), then pass a catalog name as the argument.
20+
Override the defaults if needed: -p:WinGetBuildPlatform=x64 -p:WinGetBuildConfiguration=Release
21+
-->
22+
<ItemGroup>
23+
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
24+
</ItemGroup>
25+
26+
<PropertyGroup>
27+
<CsWinRTWindowsMetadata>10.0.26100.0</CsWinRTWindowsMetadata>
28+
<CsWinRTIncludes>Microsoft.Management.Deployment</CsWinRTIncludes>
29+
</PropertyGroup>
30+
31+
<ItemGroup>
32+
<CsWinRTInputs Include="$(WinGetSrcBinRoot)\Microsoft.Management.Deployment\Microsoft.Management.Deployment.winmd" />
33+
</ItemGroup>
34+
35+
<!-- Copy the native in-proc COM DLL next to the sample so CsWinRT can activate it. -->
36+
<ItemGroup>
37+
<Content Include="$(WinGetSrcBinRoot)\Microsoft.Management.Deployment.InProc\Microsoft.Management.Deployment.dll">
38+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
39+
<Link>Microsoft.Management.Deployment.dll</Link>
40+
</Content>
41+
<Content Include="$(WinGetSrcBinRoot)\WindowsPackageManager\WindowsPackageManager.dll">
42+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
43+
<Link>WindowsPackageManager.dll</Link>
44+
</Content>
45+
<Content Include="$(WinGetSrcBinRoot)\WindowsPackageManager\WindowsPackageManager.pdb">
46+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
47+
<Link>WindowsPackageManager.pdb</Link>
48+
</Content>
49+
</ItemGroup>
50+
51+
</Project>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
// Sample demonstrating the ConnectionValidationHandler on PackageCatalogReference.
5+
//
6+
// This sample retrieves a named package catalog, installs a connection validation
7+
// callback that shows certificate information, prompts the user to accept or reject
8+
// the certificate, then reports the Connect() result.
9+
//
10+
// Usage: ConnectionValidationSample <CatalogName>
11+
// Example: ConnectionValidationSample winget
12+
//
13+
// Requirements:
14+
// - Must run in-process (the ConnectionValidationHandler setter rejects out-of-proc callers).
15+
// - The Microsoft.Management.Deployment COM server must be registered (deploy the dev package
16+
// or install WinGet from the Microsoft Store).
17+
18+
using Microsoft.Management.Deployment;
19+
using Windows.Security.Cryptography.Certificates;
20+
21+
if (args.Length == 0)
22+
{
23+
Console.Error.WriteLine("Usage: ConnectionValidationSample <CatalogName>");
24+
Console.Error.WriteLine("Example: ConnectionValidationSample winget");
25+
return 1;
26+
}
27+
28+
string catalogName = args[0];
29+
30+
// Use in-proc activation so that ConnectionValidationHandler can be set.
31+
// CsWinRT activates PackageManager in-proc via DllGetActivationFactory from
32+
// Microsoft.Management.Deployment.dll placed alongside this executable.
33+
var packageManager = new PackageManager();
34+
35+
var catalogRef = packageManager.GetPackageCatalogByName(catalogName);
36+
if (catalogRef is null)
37+
{
38+
Console.Error.WriteLine($"No catalog named '{catalogName}' found.");
39+
Console.Error.WriteLine("Use 'winget source list' to see available catalogs.");
40+
return 1;
41+
}
42+
43+
Console.WriteLine($"Catalog: {catalogRef.Info.Name} ({catalogRef.Info.Argument})");
44+
Console.WriteLine("Setting connection validation handler...");
45+
46+
catalogRef.ConnectionValidationHandler = (PackageCatalogConnectionValidationEventArgs validationArgs) =>
47+
{
48+
Certificate cert = validationArgs.ServerCertificate;
49+
50+
Console.WriteLine();
51+
Console.WriteLine("Catalog connection validation for: " + catalogRef.Info.Name);
52+
Console.WriteLine(" at: " + catalogRef.Info.Argument);
53+
Console.WriteLine();
54+
Console.WriteLine("=== Server Certificate ===");
55+
Console.WriteLine($" Subject: {cert.Subject}");
56+
Console.WriteLine($" Issuer: {cert.Issuer}");
57+
Console.WriteLine($" Valid from: {cert.ValidFrom}");
58+
Console.WriteLine($" Valid to: {cert.ValidTo}");
59+
Console.WriteLine($" Serial: {cert.SerialNumber}");
60+
Console.WriteLine("==========================");
61+
Console.WriteLine();
62+
Console.Write("Accept this certificate? [Y/N]: ");
63+
64+
while (true)
65+
{
66+
string? input = Console.ReadLine()?.Trim().ToUpperInvariant();
67+
if (input == "Y")
68+
{
69+
Console.WriteLine("Certificate accepted.");
70+
return PackageCatalogConnectionValidationResult.Ok;
71+
}
72+
else if (input == "N")
73+
{
74+
Console.WriteLine("Certificate rejected.");
75+
return PackageCatalogConnectionValidationResult.CertificateRejected;
76+
}
77+
else
78+
{
79+
Console.Write("Please enter Y or N: ");
80+
}
81+
}
82+
};
83+
84+
Console.WriteLine("Connecting...");
85+
ConnectResult result = catalogRef.Connect();
86+
87+
Console.WriteLine();
88+
Console.WriteLine($"Connect result: {result.Status}");
89+
90+
if (result.Status == ConnectResultStatus.Ok)
91+
{
92+
Console.WriteLine($"Successfully connected to '{catalogName}'.");
93+
94+
// Run a simple search to force a live network call, since Connect() may serve a cached response.
95+
Console.WriteLine("Searching to verify live connection...");
96+
var findOptions = new FindPackagesOptions();
97+
findOptions.Selectors.Add(new PackageMatchFilter
98+
{
99+
Field = PackageMatchField.Id,
100+
Option = PackageFieldMatchOption.StartsWithCaseInsensitive,
101+
Value = "Microsoft.",
102+
});
103+
var searchResult = result.PackageCatalog.FindPackages(findOptions);
104+
Console.WriteLine($"Search result: {searchResult.Status} ({searchResult.Matches.Count} match(es))");
105+
106+
return 0;
107+
}
108+
else
109+
{
110+
Console.Error.WriteLine($"Failed to connect to '{catalogName}'.");
111+
if (result.ExtendedErrorCode is not null)
112+
{
113+
Console.Error.WriteLine($" Error code: 0x{result.ExtendedErrorCode.HResult:X8}");
114+
}
115+
return 1;
116+
}

src/AppInstallerCLICore/Commands/RootCommand.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,20 @@ namespace AppInstaller::CLI
102102
if (groupPolicies.GetState(Settings::TogglePolicy::Policy::AdditionalSources) == Settings::PolicyState::Enabled)
103103
{
104104
info << std::endl;
105-
auto sources = groupPolicies.GetValueRef<Settings::ValuePolicy::AdditionalSources>();
106-
if (sources.has_value() && !sources->get().empty())
105+
auto sources = groupPolicies.GetValue<Settings::ValuePolicy::AdditionalSources>();
106+
if (sources.has_value() && !sources->empty())
107107
{
108-
OutputGroupPolicySourceList(context, sources->get(), Resource::String::SourceListAdditionalSource);
108+
OutputGroupPolicySourceList(context, sources.value(), Resource::String::SourceListAdditionalSource);
109109
}
110110
}
111111

112112
if (groupPolicies.GetState(Settings::TogglePolicy::Policy::AllowedSources) == Settings::PolicyState::Enabled)
113113
{
114114
info << std::endl;
115-
auto sources = groupPolicies.GetValueRef<Settings::ValuePolicy::AllowedSources>();
116-
if (sources.has_value() && !sources->get().empty())
115+
auto sources = groupPolicies.GetValue<Settings::ValuePolicy::AllowedSources>();
116+
if (sources.has_value() && !sources->empty())
117117
{
118-
OutputGroupPolicySourceList(context, sources->get(), Resource::String::SourceListAllowedSource);
118+
OutputGroupPolicySourceList(context, sources.value(), Resource::String::SourceListAllowedSource);
119119
}
120120
}
121121
info << std::endl;

src/AppInstallerCLIE2ETests/Constants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ public class Constants
4747
public const string TestSourceUrl = @"https://localhost:5001/TestKit";
4848
public const string TestSourceType = "Microsoft.PreIndexed.Package";
4949
public const string TestSourceIdentifier = @"WingetE2E.Tests_8wekyb3d8bbwe";
50+
public const string RestTestSourceName = @"TestRestSource";
51+
public const string RestTestSourceUrl = @"https://localhost:5001/TestKit/TestData/TestRestSource";
52+
public const string RestTestSourceType = "Microsoft.Rest";
5053

5154
public const string AICLIPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe";
5255
public const string AICLIPackageName = "WinGetDevCLI";

src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace AppInstallerCLIE2ETests
1010
using System.Collections.Generic;
1111
using System.IO;
1212
using System.Linq;
13+
using System.Runtime.InteropServices;
1314
using System.Xml.Linq;
1415
using AppInstallerCLIE2ETests.Helpers;
1516
using Microsoft.Win32;
@@ -133,6 +134,11 @@ private GroupPolicyHelper(string name, string elementId)
133134
/// </summary>
134135
public static GroupPolicyHelper EnableConfigurationProcessorPath { get; private set; } = new GroupPolicyHelper("EnableWindowsPackageManagerConfigurationProcessorPath");
135136

137+
/// <summary>
138+
/// Gets the Bypass certificate pinning for Microsoft Store policy.
139+
/// </summary>
140+
public static GroupPolicyHelper BypassCertificatePinningForMicrosoftStore { get; private set; } = new GroupPolicyHelper("EnableBypassCertificatePinningForMicrosoftStore");
141+
136142
/// <summary>
137143
/// Gets the Enable auto update interval policy.
138144
/// </summary>
@@ -231,6 +237,8 @@ public void Enable()
231237
{
232238
key.SetValue(this.ValueName, enabledValue);
233239
}
240+
241+
ReloadGroupPolicyIfAvailable();
234242
}
235243

236244
/// <summary>
@@ -249,6 +257,8 @@ public void Disable()
249257
{
250258
key.SetValue(this.ValueName, disabledValue);
251259
}
260+
261+
ReloadGroupPolicyIfAvailable();
252262
}
253263

254264
/// <summary>
@@ -283,6 +293,8 @@ public void SetNotConfigured()
283293
}
284294
}
285295
}
296+
297+
ReloadGroupPolicyIfAvailable();
286298
}
287299

288300
/// <summary>
@@ -339,6 +351,25 @@ public void SetEnabledList(IEnumerable<GroupPolicySource> values)
339351
this.SetEnabledList(values.Select(source => JsonConvert.SerializeObject(source)));
340352
}
341353

354+
[DllImport("WindowsPackageManager.dll", CallingConvention = CallingConvention.StdCall)]
355+
private static extern int WindowsPackageManagerTestHook_ReloadGroupPolicy();
356+
357+
/// <summary>
358+
/// Calls the in-process test hook to reload the GroupPolicy singleton from the current registry state.
359+
/// Silently ignored if the DLL is not loaded in this process (e.g., out-of-process test scenarios).
360+
/// </summary>
361+
private static void ReloadGroupPolicyIfAvailable()
362+
{
363+
try
364+
{
365+
WindowsPackageManagerTestHook_ReloadGroupPolicy();
366+
}
367+
catch (Exception)
368+
{
369+
// The DLL is not loaded in this process (out-of-process scenario); nothing to do.
370+
}
371+
}
372+
342373
/// <summary>
343374
/// Gets the value from a "decimal" child element.
344375
/// </summary>

0 commit comments

Comments
 (0)