winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 888b4ed8f4f7d25cb05a47210e083fe29348163b
parent 3d7f35cc274fa4a744360018de5c28e84760dfbf
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Wed, 27 Jul 2022 18:03:45 -0700

Server certificate pinning for Store source (#2347)

This change adds a generic certificate chain verification infrastructure for pinning certificate chains. It is specifically used to pin the Microsoft Store source by default. More sources may be pinned later, but currently the packaged index is less in need of it because it is already signed.

The pinning configuration consists of 1 or more chains, only one of which needs to successfully validate the incoming certificate. This allows for rolling to a new certificate when needed. Each chain consists of a fixed set of certificates, which can each be configured to validate any or all of the following properties:

- Public Key
- Subject
- Issuer

If the certificate is configured to validate none of the values, it will allow any certificate through.

An admin setting is added to disable pinning, both as an emergency measure in the event that there is a bug or rolled certificate that was not communicated, but also because there are test scenarios where the user actively wants to disable it (HTTPS redirection via something like Fiddler).

The configuration can be loaded from JSON for future dynamic configuration, but it is currently only as a test hook to enable configuration via Group Policy.

In order to better secure the source by default, reconfiguring (remove then add) the Store source manually will convert it back to the built-in values. This includes the pinning configuration.

It was necessary to modify the cpprestsdk subtree to add a new callback. This enables the request handle to be passed back to our code when the server certificate is first available. We can then check the server certificate against the configured pinning chain, making a decision to terminate the request before it is sent.
Diffstat:
M.github/actions/spelling/allow.txt | 7++++++-
M.github/actions/spelling/expect.txt | 10++++++++++
Msrc/AppInstallerCLI.sln | 8++++++++
Msrc/AppInstallerCLIE2ETests/BaseCommand.cs | 6++++--
Msrc/AppInstallerCLIE2ETests/Constants.cs | 11+++++++++++
Msrc/AppInstallerCLIE2ETests/GroupPolicyHelper.cs | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/SearchCommand.cs | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/AppInstallerCLIE2ETests/SetUpFixture.cs | 2+-
Msrc/AppInstallerCLIE2ETests/SourceCommand.cs | 4++--
Msrc/AppInstallerCLIE2ETests/TestCommon.cs | 56+++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/AppInstallerCLIE2ETests/TestIndexSetup.cs | 6+++++-
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 2++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Asrc/AppInstallerCLITests/Certificates.cpp | 185+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/Command.cpp | 2+-
Msrc/AppInstallerCLITests/Completion.cpp | 48++++++++++++++++++++++++------------------------
Msrc/AppInstallerCLITests/GroupPolicy.cpp | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerCLITests/HttpClientHelper.cpp | 23+++++++++++++++++++++++
Msrc/AppInstallerCLITests/Sources.cpp | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/Strings.cpp | 9+++++++++
Msrc/AppInstallerCommonCore/AdminSettings.cpp | 146++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 4++++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 9+++++++++
Msrc/AppInstallerCommonCore/AppInstallerStrings.cpp | 39+++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCommonCore/Certificates.cpp | 549+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Errors.cpp | 2++
Msrc/AppInstallerCommonCore/GroupPolicy.cpp | 13+++++++++++++
Msrc/AppInstallerCommonCore/JsonSchemaValidation.cpp | 36++++--------------------------------
Msrc/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp | 3++-
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerCommonCore/Public/AppInstallerStrings.h | 6++++++
Msrc/AppInstallerCommonCore/Public/winget/AdminSettings.h | 1+
Asrc/AppInstallerCommonCore/Public/winget/Certificates.h | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Public/winget/GroupPolicy.h | 11++++++++---
Msrc/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h | 5+----
Msrc/AppInstallerCommonCore/Public/winget/Resources.h | 68++++++++++++++++++++++++++++++++++++++++++--------------------------
Asrc/AppInstallerCommonCore/Resources.cpp | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/SHA256.cpp | 33++-------------------------------
Msrc/AppInstallerCommonCore/pch.h | 1+
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 4+++-
Msrc/AppInstallerRepositoryCore/Public/winget/RepositorySource.h | 8++++++--
Msrc/AppInstallerRepositoryCore/RepositorySource.cpp | 24++++++++++++++++++++----
Msrc/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp | 6++++--
Msrc/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp | 26+++++++++++++++++++++++---
Msrc/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h | 10+++++++---
Msrc/AppInstallerRepositoryCore/SourceList.cpp | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/SourceList.h | 1+
Msrc/AppInstallerRepositoryCore/pch.h | 1+
Asrc/CertificateResources/CertificateResources.h | 9+++++++++
Asrc/CertificateResources/CertificateResources.rc | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/CertificateResources/CertificateResources.vcxitems | 29+++++++++++++++++++++++++++++
Asrc/CertificateResources/CertificateResources.vcxitems.filters | 27+++++++++++++++++++++++++++
Asrc/CertificateResources/StoreIntermediate1.cer | 0
Asrc/CertificateResources/StoreLeaf1.cer | 0
Asrc/CertificateResources/StoreRoot1.cer | 0
Asrc/CertificateResources/resource.h | 14++++++++++++++
Msrc/LocalhostWebServer/Program.cs | 14+++++++++++---
Msrc/LocalhostWebServer/Run-LocalhostWebServer.ps1 | 2+-
Msrc/LocalhostWebServer/Startup.cs | 2++
Msrc/PowerShell/Microsoft.WinGet.Client/Properties/Resources.Designer.cs | 2+-
Msrc/WindowsPackageManager/WindowsPackageManager.vcxproj | 1+
Msrc/cpprestsdk/cpprestsdk/Release/include/cpprest/http_client.h | 27+++++++++++++++++++++++++++
Msrc/cpprestsdk/cpprestsdk/Release/src/http/client/http_client_winhttp.cpp | 12++++++++++++
63 files changed, 1854 insertions(+), 227 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -93,6 +93,8 @@ cref csproj CStr csv +ctl +ctls CURSORPOSITON CUSTOMHEADER cwctype @@ -360,7 +362,7 @@ nuspec OAuth ODR ofstream -Oid +oid opencode opensource openxmlformats @@ -432,6 +434,7 @@ refactor refactored refactoring REFCLSID +REFCOUNT regex regexp removemanifest @@ -646,8 +649,10 @@ WHOLECHAIN wil wildcards WINAPI +wincrypt WINEVENT winget +winhttp wininet winmd winmeta diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -220,6 +220,7 @@ kayone Keivan KF KNOWNFOLDERID +kp ktf langs LATN @@ -275,7 +276,9 @@ myinstalldir mylog mysilent mysilentwithprogress +mytool nameof +nativehandle NESTEDINSTALLER NETFX netlify @@ -305,6 +308,7 @@ PACL pathparts Patil pb +PCCERT PCs pcwsz pdp @@ -316,6 +320,7 @@ pidl pidlist pkgmgr pkindex +pkix PMS positionals powershellgallery @@ -329,6 +334,7 @@ pseudocode psm psobject ptstr +publickey pvk pvm pwabuilder @@ -351,6 +357,8 @@ REGSAM reparse restsource rgex +rgp +rgpsz rhs riid roblox @@ -367,6 +375,8 @@ sddl semver seof serializer +servercert +servercertificate setmetadatabymanifestid SETTINGMAPPING SHCONTF diff --git a/src/AppInstallerCLI.sln b/src/AppInstallerCLI.sln @@ -123,6 +123,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "COMServer", "COMServer\COMS EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Management.Deployment.InProc", "Microsoft.Management.Deployment.InProc\Microsoft.Management.Deployment.InProc.vcxproj", "{9AC3C6A4-1875-4D3E-BF9C-C31E81EFF6B4}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CertificateResources", "CertificateResources\CertificateResources.vcxitems", "{B0BBBD92-943B-408F-B2B2-DBBAB4A22D23}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerShell", "PowerShell", "{7C218A3E-9BC8-48FF-B91B-BCACD828C0C9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.WinGet.Client", "PowerShell\Microsoft.WinGet.Client\Microsoft.WinGet.Client.csproj", "{463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C}" @@ -1038,6 +1040,7 @@ Global {866C3F06-636F-4BE8-BC24-5F86ECC606A1} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {1A47951F-5C7A-4D6D-BB5F-D77484437940} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {409CD681-22A4-469D-88AE-CB5E4836E07A} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} + {B0BBBD92-943B-408F-B2B2-DBBAB4A22D23} = {8D53D749-D51C-46F8-A162-9371AAA6C2E7} {463C0EF3-DF38-4C3D-8E7E-D4901E0CDC6C} = {7C218A3E-9BC8-48FF-B91B-BCACD828C0C9} {31ED69A8-5310-45A9-953F-56C351D2C3E1} = {60618CAC-2995-4DF9-9914-45C6FC02C995} {787EC629-C0FB-4BA9-9746-4A82CD06B73E} = {60618CAC-2995-4DF9-9914-45C6FC02C995} @@ -1051,22 +1054,27 @@ Global WinGetSchemas\WinGetSchemas.vcxitems*{1c6e0108-2860-4b17-9f7e-fa5c6c1f3d3d}*SharedItemsImports = 4 COMServer\COMServer.vcxitems*{1cc41a9a-ae66-459d-9210-1e572dd7be69}*SharedItemsImports = 4 binver\binver.vcxitems*{2046b5af-666d-4ce8-8d3e-c32c57908a56}*SharedItemsImports = 4 + CertificateResources\CertificateResources.vcxitems*{2046b5af-666d-4ce8-8d3e-c32c57908a56}*SharedItemsImports = 4 COMServer\COMServer.vcxitems*{2046b5af-666d-4ce8-8d3e-c32c57908a56}*SharedItemsImports = 4 ManifestSchema\ManifestSchema.vcxitems*{2046b5af-666d-4ce8-8d3e-c32c57908a56}*SharedItemsImports = 4 WinGetSchemas\WinGetSchemas.vcxitems*{2046b5af-666d-4ce8-8d3e-c32c57908a56}*SharedItemsImports = 4 Valijson\Valijson.vcxitems*{358bc478-0624-4ad1-a933-0422b5292af8}*SharedItemsImports = 9 COMServer\COMServer.vcxitems*{409cd681-22a4-469d-88ae-cb5e4836e07a}*SharedItemsImports = 9 catch2\catch2.vcxitems*{5295e21e-9868-4de2-a177-fbb97b36579b}*SharedItemsImports = 9 + CertificateResources\CertificateResources.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 COMServer\COMServer.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 ManifestSchema\ManifestSchema.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 Valijson\Valijson.vcxitems*{5890d6ed-7c3b-40f3-b436-b54f640d9e65}*SharedItemsImports = 4 binver\binver.vcxitems*{5b6f90df-fd19-4bae-83d9-24dad128e777}*SharedItemsImports = 4 + CertificateResources\CertificateResources.vcxitems*{5eb88068-5fb9-4e69-89b2-72dbc5e068f9}*SharedItemsImports = 4 binver\binver.vcxitems*{6e36ddd7-1602-474e-b1d7-d0a7e1d5ad86}*SharedItemsImports = 9 ManifestSchema\ManifestSchema.vcxitems*{7d05f64d-ce5a-42aa-a2c1-e91458f061cf}*SharedItemsImports = 9 catch2\catch2.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 + CertificateResources\CertificateResources.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 ManifestSchema\ManifestSchema.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 WinGetSchemas\WinGetSchemas.vcxitems*{89b1aab4-2bbc-4b65-9ed7-a01d5cf88230}*SharedItemsImports = 4 WinGetSchemas\WinGetSchemas.vcxitems*{952b513f-8a00-4d74-9271-925afb3c6252}*SharedItemsImports = 9 + CertificateResources\CertificateResources.vcxitems*{b0bbbd92-943b-408f-b2b2-dbbab4a22d23}*SharedItemsImports = 9 binver\binver.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 ManifestSchema\ManifestSchema.vcxitems*{fb313532-38b0-4676-9303-ab200aa13576}*SharedItemsImports = 4 EndGlobalSection diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs @@ -22,9 +22,11 @@ namespace AppInstallerCLIE2ETests TestCommon.TearDownTestSource(); } - public void ResetTestSource() + // TODO: If/when cert pinning is implemented on the packaged index source, useGroupPolicyForTestSource should be set to default true + // to enable testing it by default. Until then, leaving this here... + public void ResetTestSource(bool useGroupPolicyForTestSource = false) { - TestCommon.SetupTestSource(); + TestCommon.SetupTestSource(useGroupPolicyForTestSource); } public void ConfigureFeature(string featureName, bool status) diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -25,9 +25,14 @@ namespace AppInstallerCLIE2ETests public const string DefaultWingetSourceUrl = @"https://winget.azureedge.net/cache"; public const string DefaultMSStoreSourceName = @"msstore"; public const string DefaultMSStoreSourceUrl = @"https://storeedgefd.dsx.mp.microsoft.com/v9.0"; + public const string DefaultMSStoreSourceType = "Microsoft.Rest"; + public const string DefaultMSStoreSourceIdentifier = "StoreEdgeFD"; public const string TestSourceName = @"TestSource"; + public const string TestAlternateSourceName = @"TestSource2"; public const string TestSourceUrl = @"https://localhost:5001/TestKit"; + public const string TestSourceType = "Microsoft.PreIndexed.Package"; public const string TestSourceIdentifier = @"WingetE2E.Tests_8wekyb3d8bbwe"; + public const string TestSourceServerCertificateFileName = "servercert.cer"; public const string AICLIPackageFamilyName = "WinGetDevCLI_8wekyb3d8bbwe"; public const string AICLIPackageName = "WinGetDevCLI"; @@ -178,6 +183,12 @@ namespace AppInstallerCLIE2ETests public const int ERROR_INSTALLER_PROHIBITS_ELEVATION = unchecked((int)0x8A150056); public const int ERROR_PORTABLE_UNINSTALL_FAILED = unchecked((int)0x8A150057); public const int ERROR_ARP_VERSION_VALIDATION_FAILED = unchecked((int)0x8A150058); + public const int ERROR_UNSUPPORTED_ARGUMENT = unchecked((int)0x8A150059); + public const int ERROR_BIND_WITH_EMBEDDED_NULL = unchecked((int)0x8A15005A); + public const int ERROR_NESTEDINSTALLER_NOT_FOUND = unchecked((int)0x8A15005B); + public const int ERROR_EXTRACT_ARCHIVE_FAILED = unchecked((int)0x8A15005C); + public const int ERROR_NESTEDINSTALLER_INVALID_PATH = unchecked((int)0x8A15005D); + public const int ERROR_PINNED_CERTIFICATE_MISMATCH = unchecked((int)0x8A15005E); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); diff --git a/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs b/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs @@ -7,6 +7,8 @@ using System.IO; using System.Linq; using System.Xml.Linq; using Microsoft.Win32; +using Newtonsoft.Json; +using NUnit.Framework; namespace AppInstallerCLIE2ETests { @@ -269,6 +271,7 @@ namespace AppInstallerCLIE2ETests int index = 0; foreach (string value in values) { + TestContext.Out.WriteLine($"Setting {name} list value: {value}"); listKey.SetValue(index++.ToString(), value); } @@ -276,6 +279,17 @@ namespace AppInstallerCLIE2ETests } /// <summary> + /// Sets the list value of the policy when enabled. + /// This sets from the "elements" and also sets the enabled value as lists are also gated by a toggle. + /// This will fail if the value of the policy is not a list. + /// </summary> + /// <param name="values">Values to set in the list.</param> + public void SetEnabledList(IEnumerable<GroupPolicySource> values) + { + SetEnabledList(values.Select(source => JsonConvert.SerializeObject(source))); + } + + /// <summary> /// Gets the value from a "decimal" child element. /// </summary> /// <param name="element">Element containing the decimal.</param> @@ -299,4 +313,34 @@ namespace AppInstallerCLIE2ETests return Registry.LocalMachine.CreateSubKey(this.KeyPath); } } + + /// <summary> + /// A group policy source object as used by AdditionalSources and AllowedSources. + /// </summary> + public class GroupPolicySource + { + public string Name { get; set; } + public string Arg { get; set; } + public string Type { get; set; } + public string Data { get; set; } + public string Identifier { get; set; } + public GroupPolicyCertificatePinning CertificatePinning { get; set; } + } + + public class GroupPolicyCertificatePinning + { + public GroupPolicyCertificatePinningChain[] Chains { get; set; } + } + + public class GroupPolicyCertificatePinningChain + { + public GroupPolicyCertificatePinningDetails[] Chain { get; set; } + } + + public class GroupPolicyCertificatePinningDetails + { + public string[] Validation { get; set; } + public string EmbeddedCertificate { get; set; } + } + } diff --git a/src/AppInstallerCLIE2ETests/SearchCommand.cs b/src/AppInstallerCLIE2ETests/SearchCommand.cs @@ -91,11 +91,72 @@ namespace AppInstallerCLIE2ETests { TestCommon.RunAICLICommand("source add", "failSearch \"{ \"\"OpenHR\"\": \"\"0x80070002\"\" }\" Microsoft.Test.Configurable --header \"{}\""); - var result = TestCommon.RunAICLICommand("search", "--exact AppInstallerTest.TestExampleInstaller"); - Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(result.StdOut.Contains("Failed when searching source; results will not be included: failSearch")); - Assert.True(result.StdOut.Contains("TestExampleInstaller")); - Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + try + { + var result = TestCommon.RunAICLICommand("search", "--exact AppInstallerTest.TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Failed when searching source; results will not be included: failSearch")); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + finally + { + ResetTestSource(); + } + } + + [Test] + public void SearchStoreWithBadPin() + { + // Configure as close as possible to the real chain but use the test cert for everything + // This will at least force the public key to be checked rather than simply failing based on chain length + GroupPolicyHelper.EnableAdditionalSources.SetEnabledList(new GroupPolicySource[] + { + new GroupPolicySource + { + Name = Constants.TestAlternateSourceName, + Arg = Constants.DefaultMSStoreSourceUrl, + Type = Constants.DefaultMSStoreSourceType, + Data = "", + Identifier = Constants.DefaultMSStoreSourceIdentifier, + CertificatePinning = new GroupPolicyCertificatePinning + { + Chains = new GroupPolicyCertificatePinningChain[] { + new GroupPolicyCertificatePinningChain + { + Chain = new GroupPolicyCertificatePinningDetails[] + { + new GroupPolicyCertificatePinningDetails + { + Validation = new string[] { "publickey" }, + EmbeddedCertificate = TestCommon.GetTestServerCertificateHexString() + }, + new GroupPolicyCertificatePinningDetails + { + Validation = new string[] { "subject", "issuer" }, + EmbeddedCertificate = TestCommon.GetTestServerCertificateHexString() + }, + new GroupPolicyCertificatePinningDetails + { + Validation = new string[] { "subject", "issuer" }, + EmbeddedCertificate = TestCommon.GetTestServerCertificateHexString() + } + } + } + } + } + } + }); + + try + { + var result = TestCommon.RunAICLICommand("search", $"-s {Constants.TestAlternateSourceName} foo --verbose-logs"); + Assert.AreEqual(Constants.ErrorCode.ERROR_PINNED_CERTIFICATE_MISMATCH, result.ExitCode); + } + finally + { + ResetTestSource(); + } } } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLIE2ETests/SetUpFixture.cs b/src/AppInstallerCLIE2ETests/SetUpFixture.cs @@ -215,7 +215,7 @@ namespace AppInstallerCLIE2ETests debugging = new { enableSelfInitiatedMinidump = true - } + }, }; // Run winget one time to initialize settings directory diff --git a/src/AppInstallerCLIE2ETests/SourceCommand.cs b/src/AppInstallerCLIE2ETests/SourceCommand.cs @@ -10,7 +10,7 @@ namespace AppInstallerCLIE2ETests [SetUp] public void Setup() { - ResetTestSource(); + ResetTestSource(false); } [Test] @@ -100,7 +100,7 @@ namespace AppInstallerCLIE2ETests var result = TestCommon.RunAICLICommand("source remove", $"-n {Constants.TestSourceName}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Done")); - ResetTestSource(); + ResetTestSource(false); } [Test] diff --git a/src/AppInstallerCLIE2ETests/TestCommon.cs b/src/AppInstallerCLIE2ETests/TestCommon.cs @@ -354,6 +354,14 @@ namespace AppInstallerCLIE2ETests } } + /// <summary> + /// Gets the server certificate as a hex string. + /// </summary> + public static string GetTestServerCertificateHexString() + { + return Convert.ToHexString(File.ReadAllBytes(Path.Combine(StaticFileRootPath, Constants.TestSourceServerCertificateFileName))); + } + public static bool VerifyTestExeInstalledAndCleanup(string installDir, string expectedContent = null) { if (!File.Exists(Path.Combine(installDir, Constants.TestExeInstalledFileName))) @@ -418,12 +426,50 @@ namespace AppInstallerCLIE2ETests } } - public static void SetupTestSource() + public static void SetupTestSource(bool useGroupPolicyForTestSource = false) { - RunAICLICommand("source reset", "--force"); - RunAICLICommand("source remove", Constants.DefaultWingetSourceName); - RunAICLICommand("source remove", Constants.DefaultMSStoreSourceName); - RunAICLICommand("source add", $"{Constants.TestSourceName} {Constants.TestSourceUrl}"); + TestCommon.RunAICLICommand("source reset", "--force"); + TestCommon.RunAICLICommand("source remove", Constants.DefaultWingetSourceName); + TestCommon.RunAICLICommand("source remove", Constants.DefaultMSStoreSourceName); + + // TODO: If/when cert pinning is implemented on the packaged index source, useGroupPolicyForTestSource should be set to default true + // to enable testing it by default. Until then, leaving this here... + if (useGroupPolicyForTestSource) + { + GroupPolicyHelper.EnableAdditionalSources.SetEnabledList(new GroupPolicySource[] + { + new GroupPolicySource + { + Name = Constants.TestSourceName, + Arg = Constants.TestSourceUrl, + Type = Constants.TestSourceType, + Data = Constants.TestSourceIdentifier, + Identifier = Constants.TestSourceIdentifier, + CertificatePinning = new GroupPolicyCertificatePinning + { + Chains = new GroupPolicyCertificatePinningChain[] { + new GroupPolicyCertificatePinningChain + { + Chain = new GroupPolicyCertificatePinningDetails[] + { + new GroupPolicyCertificatePinningDetails + { + Validation = new string[] { "publickey" }, + EmbeddedCertificate = TestCommon.GetTestServerCertificateHexString() + } + } + } + } + } + } + }); + } + else + { + GroupPolicyHelper.EnableAdditionalSources.SetNotConfigured(); + TestCommon.RunAICLICommand("source add", $"{Constants.TestSourceName} {Constants.TestSourceUrl}"); + } + Thread.Sleep(2000); } diff --git a/src/AppInstallerCLIE2ETests/TestIndexSetup.cs b/src/AppInstallerCLIE2ETests/TestIndexSetup.cs @@ -191,7 +191,11 @@ namespace AppInstallerCLIE2ETests { foreach (FileInfo file in directory.GetFiles()) { - file.Delete(); + // Leave the server certificate file if present + if (file.Name.ToLower() != Constants.TestSourceServerCertificateFileName) + { + file.Delete(); + } } foreach (DirectoryInfo dir in directory.GetDirectories()) diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -57,6 +57,7 @@ <Import Project="..\catch2\catch2.vcxitems" Label="Shared" /> <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> <Import Project="..\WinGetSchemas\WinGetSchemas.vcxitems" Label="Shared" /> + <Import Project="..\CertificateResources\CertificateResources.vcxitems" Label="Shared" /> </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> @@ -188,6 +189,7 @@ <ItemGroup> <ClCompile Include="Archive.cpp" /> <ClCompile Include="ARPChanges.cpp" /> + <ClCompile Include="Certificates.cpp" /> <ClCompile Include="Command.cpp" /> <ClCompile Include="Completion.cpp" /> <ClCompile Include="CompositeSource.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -209,6 +209,9 @@ <ClCompile Include="YamlManifest.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="Certificates.cpp"> + <Filter>Source Files\Common</Filter> + </ClCompile> <ClCompile Include="Runtime.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> diff --git a/src/AppInstallerCLITests/Certificates.cpp b/src/AppInstallerCLITests/Certificates.cpp @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <winget/Certificates.h> +#include <CertificateResources.h> + +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::Certificates; + + +TEST_CASE("Certificates_NoPinningSucceeds", "[certificates]") +{ + PinningDetails expected; + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::None); + + PinningDetails actual; + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(expected.Validate(actual.GetCertificate())); +} + +TEST_CASE("Certificates_PublicKeyMismatch", "[certificates]") +{ + PinningDetails expected; + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + + PinningDetails actual; + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(!expected.Validate(actual.GetCertificate())); +} + +TEST_CASE("Certificates_PublicKeyMatch", "[certificates]") +{ + PinningDetails expected; + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + + PinningDetails actual; + actual.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1); + + REQUIRE(expected.Validate(actual.GetCertificate())); +} + +TEST_CASE("Certificates_SubjectMismatch", "[certificates]") +{ + PinningDetails expected; + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::Subject); + + PinningDetails actual; + actual.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1); + + REQUIRE(!expected.Validate(actual.GetCertificate())); +} + +TEST_CASE("Certificates_SubjectMatch", "[certificates]") +{ + PinningDetails expected; + expected.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject); + + PinningDetails actual; + actual.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1); + + REQUIRE(expected.Validate(actual.GetCertificate())); +} + +TEST_CASE("Certificates_IssuerMismatch", "[certificates]") +{ + PinningDetails expected; + expected.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Issuer); + + PinningDetails actual; + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(!expected.Validate(actual.GetCertificate())); +} + +TEST_CASE("Certificates_IssuerMatch", "[certificates]") +{ + PinningDetails expected; + expected.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Issuer); + + PinningDetails actual; + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(expected.Validate(actual.GetCertificate())); +} + +TEST_CASE("Certificates_ChainLengthDiffers", "[certificates]") +{ + PinningChain chain; + auto chainElement = chain.Root(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + + PinningConfiguration config; + config.AddChain(chain); + + PinningDetails details; + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(!config.Validate(details.GetCertificate())); +} + +TEST_CASE("Certificates_EmptyChainRejects", "[certificates]") +{ + PinningChain chain; + + PinningConfiguration config; + config.AddChain(chain); + + PinningDetails details; + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(!config.Validate(details.GetCertificate())); +} + +TEST_CASE("Certificates_ChainOrderDiffers", "[certificates]") +{ + PinningChain chain; + auto chainElement = chain.Root(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + + PinningConfiguration config; + config.AddChain(chain); + + PinningDetails details; + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(!config.Validate(details.GetCertificate())); +} + +TEST_CASE("Certificates_StoreChain_BuiltInTest", "[certificates]") +{ + PinningChain chain; + auto chainElement = chain.Root(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + + PinningConfiguration config; + config.AddChain(chain); + + PinningDetails details; + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(config.Validate(details.GetCertificate())); +} + +TEST_CASE("Certificates_MultipleChains_Success", "[certificates]") +{ + PinningChain chainOutOfOrder; + auto chainElement = chainOutOfOrder.Root(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + + PinningConfiguration config; + config.AddChain(chainOutOfOrder); + + PinningChain chain; + chainElement = chain.Root(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + + config.AddChain(chain); + + PinningDetails details; + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + REQUIRE(config.Validate(details.GetCertificate())); +} diff --git a/src/AppInstallerCLITests/Command.cpp b/src/AppInstallerCLITests/Command.cpp @@ -222,7 +222,7 @@ void RequireValueParsedToArg(const std::string& value, const Argument& arg, cons } // Description used for tests; doesn't need to be anything in particular. -static constexpr Resource::StringId DefaultDesc{ L""sv }; +static constexpr CLI::Resource::StringId DefaultDesc{ L""sv }; TEST_CASE("ParseArguments_MultiplePositional", "[command]") { diff --git a/src/AppInstallerCLITests/Completion.cpp b/src/AppInstallerCLITests/Completion.cpp @@ -266,7 +266,7 @@ TEST_CASE("CommandComplete_Simple", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "test1", "test2" }; - command.Arguments = { Argument{ "arg1", 'a', Args::Type::Query, Resource::String::Done, ArgumentType::Standard } }; + command.Arguments = { Argument{ "arg1", 'a', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard } }; command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; command.Complete(ctc.context); @@ -318,8 +318,8 @@ TEST_CASE("CommandComplete_Routing1", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, }; Args::Type argType = static_cast<Args::Type>(-1); command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; @@ -339,8 +339,8 @@ TEST_CASE("CommandComplete_Routing2", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, }; Args::Type argType = static_cast<Args::Type>(-1); command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; @@ -360,8 +360,8 @@ TEST_CASE("CommandComplete_PositionalRouting", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Positional }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, }; Args::Type argType = static_cast<Args::Type>(-1); command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; @@ -383,8 +383,8 @@ TEST_CASE("CommandComplete_PositionalRoutingAfterArgs", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Positional }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, }; Args::Type argType = static_cast<Args::Type>(-1); command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; @@ -405,8 +405,8 @@ TEST_CASE("CommandComplete_PositionalRoutingAfterDoubleDash", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Positional }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, }; Args::Type argType = static_cast<Args::Type>(-1); command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; @@ -426,8 +426,8 @@ TEST_CASE("CommandComplete_ArgNamesAfterDash", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Positional }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, }; command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; command.Complete(ctc.context); @@ -446,8 +446,8 @@ TEST_CASE("CommandComplete_AliasNames", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Positional }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, }; command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; command.Complete(ctc.context); @@ -466,9 +466,9 @@ TEST_CASE("CommandComplete_ArgNamesFilter", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Positional }, - Argument{ "foo1", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Positional }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, + Argument{ "foo1", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Positional }, }; command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; command.Complete(ctc.context); @@ -487,8 +487,8 @@ TEST_CASE("CommandComplete_IgnoreBadArgs", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, }; Args::Type argType = static_cast<Args::Type>(-1); command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; @@ -508,8 +508,8 @@ TEST_CASE("CommandComplete_OtherArgsParsed", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, }; command.ArgumentValueCallback = [&](Context&, Execution::Args::Type) { FAIL("No argument value should be requested"); }; command.Complete(ctc.context); @@ -532,8 +532,8 @@ TEST_CASE("CommandComplete_Complex", "[complete]") CompletionTestCommand command; command.SubCommandNames = { "car", "cart", "cartesian", "carpet" }; command.Arguments = { - Argument{ "arg1", '1', Args::Type::Query, Resource::String::Done, ArgumentType::Standard }, - Argument{ "arg2", '2', Args::Type::Channel, Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg1", '1', Args::Type::Query, CLI::Resource::String::Done, ArgumentType::Standard }, + Argument{ "arg2", '2', Args::Type::Channel, CLI::Resource::String::Done, ArgumentType::Standard }, }; Args::Type argType = static_cast<Args::Type>(-1); command.ArgumentValueCallback = [&](Context&, Execution::Args::Type type) { argType = type; }; diff --git a/src/AppInstallerCLITests/GroupPolicy.cpp b/src/AppInstallerCLITests/GroupPolicy.cpp @@ -4,6 +4,8 @@ #include "TestCommon.h" #include "TestSettings.h" #include "winget/GroupPolicy.h" +#include <AppInstallerStrings.h> +#include <CertificateResources.h> using namespace TestCommon; using namespace AppInstaller::Settings; @@ -11,10 +13,15 @@ using namespace std::string_view_literals; namespace { - std::wstring GetSourceJson(std::wstring_view name, std::wstring_view arg, std::wstring_view type, std::wstring_view data, std::wstring_view identifier) + std::wstring GetSourceJson(std::wstring_view name, std::wstring_view arg, std::wstring_view type, std::wstring_view data, std::wstring_view identifier, std::wstring_view pinningConfig = {}) { std::wstringstream json; - json << L"{ \"Name\":\"" << name << L"\", \"Arg\":\"" << arg << L"\", \"Type\":\"" << type << L"\", \"Data\":\"" << data << L"\", \"Identifier\":\"" << identifier << L"\" }"; + json << L"{ \"Name\":\"" << name << L"\", \"Arg\":\"" << arg << L"\", \"Type\":\"" << type << L"\", \"Data\":\"" << data << L"\", \"Identifier\":\"" << identifier << L"\""; + if (!pinningConfig.empty()) + { + json << L", \"CertificatePinning\":" << pinningConfig; + } + json << " }"; return json.str(); } } @@ -266,6 +273,55 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") REQUIRE(policy.value()[0].Data == source.Data); REQUIRE(policy.value()[0].Identifier == source.Identifier); } + SECTION("Source with PinningConfiguration") + { + using namespace AppInstaller::Certificates; + + auto additionalSourcesKey = RegCreateVolatileSubKey(policiesKey.get(), AdditionalSourcesPolicyKeyName); + + PinningDetails rootCert; + rootCert.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1); + PinningDetails intermediateCert; + intermediateCert.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1); + PinningDetails leafCert; + leafCert.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1); + + auto getBytesString = [](const PinningDetails& details) + { + std::vector<BYTE> bytes; + bytes.assign(details.GetCertificate()->pbCertEncoded, details.GetCertificate()->pbCertEncoded + details.GetCertificate()->cbCertEncoded); + return AppInstaller::Utility::ConvertToUTF16(AppInstaller::Utility::ConvertToHexString(bytes)); + }; + + std::wostringstream pinningConfig; + pinningConfig << +LR"({ + "Chains": [{ + "Chain":[ + { "Validation": ["publickey"], "EmbeddedCertificate": ")" << getBytesString(rootCert) << LR"(" }, + { "Validation": ["subject","issuer"], "EmbeddedCertificate": ")" << getBytesString(intermediateCert) << LR"(" }, + { "Validation": ["subject","issuer"], "EmbeddedCertificate": ")" << getBytesString(leafCert) << LR"(" } + ] + }] +})"; + + SetRegistryValue(additionalSourcesKey.get(), L"0", GetSourceJson(L"source-name", L"source-arg", L"source-type", L"source-data", L"source-identifier", pinningConfig.str()), REG_SZ); + GroupPolicy groupPolicy{ policiesKey.get() }; + + auto policy = groupPolicy.GetValue<ValuePolicy::AdditionalSources>(); + REQUIRE(policy.has_value()); + REQUIRE(policy->size() == 1); + const auto& sourceInfo = policy.value()[0]; + REQUIRE(sourceInfo.Name == "source-name"); + REQUIRE(sourceInfo.Arg == "source-arg"); + REQUIRE(sourceInfo.Type == "source-type"); + REQUIRE(sourceInfo.Data == "source-data"); + REQUIRE(sourceInfo.Identifier == "source-identifier"); + + // Use loaded pinning config and validate against leaf certificate + REQUIRE(!sourceInfo.PinningConfiguration.IsEmpty()); + REQUIRE(sourceInfo.PinningConfiguration.Validate(leafCert.GetCertificate())); + } } TEST_CASE("GroupPolicy_Toggle", "[groupPolicy]") diff --git a/src/AppInstallerCLITests/HttpClientHelper.cpp b/src/AppInstallerCLITests/HttpClientHelper.cpp @@ -7,10 +7,13 @@ #include <Rest/Schema/HttpClientHelper.h> #include <AppInstallerRuntime.h> #include <AppInstallerStrings.h> +#include <winget/Certificates.h> +#include <CertificateResources.h> using namespace AppInstaller::Repository::Rest::Schema; using namespace AppInstaller::Runtime; using namespace AppInstaller::Utility; +using namespace AppInstaller::Certificates; TEST_CASE("ExtractJsonResponse_UnsupportedMimeType", "[RestSource][RestSearch]") { @@ -56,3 +59,23 @@ TEST_CASE("EnsureDefaultUserAgent", "[RestSource]") REQUIRE_NOTHROW(helper.HandlePost(L"https://testUri", {})); } } + +TEST_CASE("HttpClientHelper_PinningConfiguration", "[RestSource]") +{ + // Create the Store chain config + PinningChain chain; + auto chainElement = chain.Root(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + + PinningConfiguration config; + config.AddChain(chain); + + HttpClientHelper helper; + helper.SetPinningConfiguration(config); + + REQUIRE_THROWS_HR(helper.HandleGet(L"https://microsoft.com"), APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH); +} diff --git a/src/AppInstallerCLITests/Sources.cpp b/src/AppInstallerCLITests/Sources.cpp @@ -1224,3 +1224,44 @@ TEST_CASE("RepoSources_UpdateSettingsDuringAction_MetadataUpdate", "[sources]") REQUIRE(sources.size() == c_DefaultSourceCount); } } + +TEST_CASE("RepoSources_RestoringWellKnownSource", "[sources]") +{ + TestHook_ClearSourceFactoryOverrides(); + RemoveSetting(Stream::UserSources); + + Source storeSource{ WellKnownSource::MicrosoftStore }; + SourceDetails details = storeSource.GetDetails(); + REQUIRE(!details.CertificatePinningConfiguration.IsEmpty()); + + TestSourceFactory factory{ SourcesTestSource::Create }; + TestHook_SetSourceFactoryOverride(details.Type, factory); + + ProgressCallback progress; + + REQUIRE(storeSource.Remove(progress)); + + Source storeAfterRemove{ details.Name }; + REQUIRE(!storeAfterRemove); + + SECTION("with well known name") + { + Source addStoreBack{ details.Name, details.Arg, details.Type }; + REQUIRE(addStoreBack.Add(progress)); + + Source storeAfterAdd{ details.Name }; + REQUIRE(storeAfterAdd); + REQUIRE(!storeAfterAdd.GetDetails().CertificatePinningConfiguration.IsEmpty()); + } + + SECTION("with different name") + { + std::string newName = details.Name + "_new"; + Source addStoreBack{ newName, details.Arg, details.Type }; + REQUIRE(addStoreBack.Add(progress)); + + Source storeAfterAdd{ newName }; + REQUIRE(storeAfterAdd); + REQUIRE(storeAfterAdd.GetDetails().CertificatePinningConfiguration.IsEmpty()); + } +} diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp @@ -216,3 +216,12 @@ TEST_CASE("ReplaceEmbeddedNullCharacters", "[strings]") ReplaceEmbeddedNullCharacters(test); REQUIRE(test == "Test Parts"); } + +TEST_CASE("HexStrings", "[strings]") +{ + std::vector<uint8_t> buffer{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + std::string value = "000102030405060708090a0b0c0d0e0f"; + + REQUIRE(value == ConvertToHexString(buffer)); + REQUIRE(std::equal(buffer.begin(), buffer.end(), ParseFromHexString(value).begin())); +} diff --git a/src/AppInstallerCommonCore/AdminSettings.cpp b/src/AppInstallerCommonCore/AdminSettings.cpp @@ -1,59 +1,61 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "AppInstallerLogging.h" -#include "AppInstallerStrings.h" -#include "winget/Settings.h" -#include "winget/AdminSettings.h" -#include "winget/GroupPolicy.h" -#include "winget/Yaml.h" - -namespace AppInstaller::Settings -{ - using namespace std::string_view_literals; - using namespace Utility; - - namespace - { - constexpr std::string_view s_AdminSettingsYaml_LocalManifestFiles = "LocalManifestFiles"sv; - - // Attempts to read a single scalar value from the node. - template<typename Value> - bool TryReadScalar(const YAML::Node& rootNode, std::string_view name, Value& value) - { - YAML::Node valueNode = rootNode[std::string{ name }]; - - if (!valueNode || !valueNode.IsScalar()) - { - AICLI_LOG(Core, Verbose, << "Admin setting '" << name << "' was not found or did not contain the expected format"); - return false; - } - - value = valueNode.as<Value>(); - return true; - } - - struct AdminSettingValues - { - bool LocalManifestFiles = false; - }; - - struct AdminSettingsInternal - { - AdminSettingsInternal(); - - void SetAdminSetting(AdminSetting setting, bool enabled); - - bool GetAdminSettingBoolValue(AdminSetting setting) const; - - private: - void LoadAdminSettings(); - [[nodiscard]] bool SaveAdminSettings(); - - Stream m_settingStream; - AdminSettingValues m_settingValues; - }; - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AppInstallerLogging.h" +#include "AppInstallerStrings.h" +#include "winget/Settings.h" +#include "winget/AdminSettings.h" +#include "winget/GroupPolicy.h" +#include "winget/Yaml.h" + +namespace AppInstaller::Settings +{ + using namespace std::string_view_literals; + using namespace Utility; + + namespace + { + constexpr std::string_view s_AdminSettingsYaml_LocalManifestFiles = "LocalManifestFiles"sv; + constexpr std::string_view s_AdminSettingsYaml_BypassCertificatePinningForMicrosoftStore = "BypassCertificatePinningForMicrosoftStore"sv; + + // Attempts to read a single scalar value from the node. + template<typename Value> + bool TryReadScalar(const YAML::Node& rootNode, std::string_view name, Value& value) + { + YAML::Node valueNode = rootNode[std::string{ name }]; + + if (!valueNode || !valueNode.IsScalar()) + { + AICLI_LOG(Core, Verbose, << "Admin setting '" << name << "' was not found or did not contain the expected format"); + return false; + } + + value = valueNode.as<Value>(); + return true; + } + + struct AdminSettingValues + { + bool LocalManifestFiles = false; + bool BypassCertificatePinningForMicrosoftStore = false; + }; + + struct AdminSettingsInternal + { + AdminSettingsInternal(); + + void SetAdminSetting(AdminSetting setting, bool enabled); + + bool GetAdminSettingBoolValue(AdminSetting setting) const; + + private: + void LoadAdminSettings(); + [[nodiscard]] bool SaveAdminSettings(); + + Stream m_settingStream; + AdminSettingValues m_settingValues; + }; + AdminSettingsInternal::AdminSettingsInternal() : m_settingStream(Stream::AdminSettings) { LoadAdminSettings(); @@ -68,6 +70,9 @@ namespace AppInstaller::Settings case AdminSetting::LocalManifestFiles: m_settingValues.LocalManifestFiles = enabled; break; + case AdminSetting::BypassCertificatePinningForMicrosoftStore: + m_settingValues.BypassCertificatePinningForMicrosoftStore = enabled; + break; default: return; } @@ -90,6 +95,8 @@ namespace AppInstaller::Settings { case AdminSetting::LocalManifestFiles: return m_settingValues.LocalManifestFiles; + case AdminSetting::BypassCertificatePinningForMicrosoftStore: + return m_settingValues.BypassCertificatePinningForMicrosoftStore; default: return false; } @@ -130,6 +137,7 @@ namespace AppInstaller::Settings } TryReadScalar<bool>(document, s_AdminSettingsYaml_LocalManifestFiles, m_settingValues.LocalManifestFiles); + TryReadScalar<bool>(document, s_AdminSettingsYaml_BypassCertificatePinningForMicrosoftStore, m_settingValues.BypassCertificatePinningForMicrosoftStore); } bool AdminSettingsInternal::SaveAdminSettings() @@ -137,12 +145,13 @@ namespace AppInstaller::Settings YAML::Emitter out; out << YAML::BeginMap; out << YAML::Key << s_AdminSettingsYaml_LocalManifestFiles << YAML::Value << m_settingValues.LocalManifestFiles; + out << YAML::Key << s_AdminSettingsYaml_BypassCertificatePinningForMicrosoftStore << YAML::Value << m_settingValues.BypassCertificatePinningForMicrosoftStore; out << YAML::EndMap; return m_settingStream.Set(out.str()); } - } - + } + AdminSetting StringToAdminSetting(std::string_view in) { AdminSetting result = AdminSetting::Unknown; @@ -151,6 +160,10 @@ namespace AppInstaller::Settings { result = AdminSetting::LocalManifestFiles; } + else if (Utility::CaseInsensitiveEquals(s_AdminSettingsYaml_BypassCertificatePinningForMicrosoftStore, in)) + { + result = AdminSetting::BypassCertificatePinningForMicrosoftStore; + } return result; } @@ -161,6 +174,8 @@ namespace AppInstaller::Settings { case AdminSetting::LocalManifestFiles: return s_AdminSettingsYaml_LocalManifestFiles; + case AdminSetting::BypassCertificatePinningForMicrosoftStore: + return s_AdminSettingsYaml_BypassCertificatePinningForMicrosoftStore; default: return "Unknown"sv; } @@ -180,14 +195,17 @@ namespace AppInstaller::Settings bool IsAdminSettingEnabled(AdminSetting setting) { - // For some admin settings, even if it's disabled, if the corresponding policy is enabled then override it. - if (setting == AdminSetting::LocalManifestFiles && - GroupPolicies().GetState(TogglePolicy::Policy::LocalManifestFiles) == PolicyState::Enabled) - { - return true; + // Check for a policy that overrides this setting. + if (setting == AdminSetting::LocalManifestFiles) + { + PolicyState localManifestFilesPolicy = GroupPolicies().GetState(TogglePolicy::Policy::LocalManifestFiles); + if (localManifestFilesPolicy != PolicyState::NotConfigured) + { + return localManifestFilesPolicy == PolicyState::Enabled; + } } AdminSettingsInternal adminSettingsInternal; return adminSettingsInternal.GetAdminSettingBoolValue(setting); } -} +} diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -95,6 +95,7 @@ <Import Project="..\Valijson\Valijson.vcxitems" Label="Shared" /> <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> <Import Project="..\COMServer\COMServer.vcxitems" Label="Shared" /> + <Import Project="..\CertificateResources\CertificateResources.vcxitems" Label="Shared" /> </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> @@ -278,6 +279,7 @@ </ItemDefinitionGroup> <ItemGroup> <ClInclude Include="DODownloader.h" /> + <ClInclude Include="Public\winget\Certificates.h" /> <ClInclude Include="Public\winget\FolderFileWatcher.h" /> <ClInclude Include="Public\winget\MsixManifest.h" /> <ClInclude Include="Public\winget\AdminSettings.h" /> @@ -339,6 +341,7 @@ </ItemGroup> <ItemGroup> <ClCompile Include="AdminSettings.cpp" /> + <ClCompile Include="Certificates.cpp" /> <ClCompile Include="Debugging.cpp" /> <ClCompile Include="DependenciesGraph.cpp" /> <ClCompile Include="DODownloader.cpp" /> @@ -387,6 +390,7 @@ <ClCompile Include="NameNormalization.cpp" /> <ClCompile Include="Regex.cpp" /> <ClCompile Include="Registry.cpp" /> + <ClCompile Include="Resources.cpp" /> <ClCompile Include="Runtime.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -201,6 +201,9 @@ <ClInclude Include="Public\winget\MsixManifest.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Public\winget\Certificates.h"> + <Filter>Public\winget</Filter> + </ClInclude> <ClInclude Include="Public\winget\FolderFileWatcher.h"> <Filter>Public\winget</Filter> </ClInclude> @@ -356,6 +359,12 @@ <ClCompile Include="MsixManifest.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Certificates.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Resources.cpp"> + <Filter>Source Files</Filter> + </ClCompile> <ClCompile Include="FolderFileWatcher.cpp"> <Filter>Source Files</Filter> </ClCompile> diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -668,4 +668,43 @@ namespace AppInstaller::Utility return result; } + + std::string ConvertToHexString(const std::vector<uint8_t>& buffer, size_t byteCount) + { + if (byteCount && buffer.size() != byteCount) + { + THROW_HR_MSG(E_INVALIDARG, "ConvertToHexString: Invalid buffer size"); + } + + std::string result(2 * buffer.size(), '\0'); + static constexpr std::array<char, 16> hexChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + for (size_t i = 0; i < buffer.size(); ++i) + { + result[2 * i] = hexChars[(buffer[i] >> 4) & 0xF]; + result[2 * i + 1] = hexChars[buffer[i] & 0xF]; + } + + return result; + } + + std::vector<uint8_t> ParseFromHexString(const std::string& value, size_t byteCount) + { + if ((byteCount && value.size() != (2 * byteCount)) || + (value.size() % 2)) + { + THROW_HR_MSG(E_INVALIDARG, "ParseFromHexString: Invalid value size"); + } + + const char* valuePtr = value.c_str(); + std::vector<uint8_t> result; + result.resize(value.size() / 2); + + for (size_t i = 0; i < result.size(); i++) + { + sscanf_s(valuePtr + 2 * i, "%02hhx", &result[i]); + } + + return result; + } } diff --git a/src/AppInstallerCommonCore/Certificates.cpp b/src/AppInstallerCommonCore/Certificates.cpp @@ -0,0 +1,549 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/Certificates.h" +#include "AppInstallerLogging.h" +#include "AppInstallerStrings.h" +#include "winget/JsonUtil.h" +#include "winget/Resources.h" + +#include <CertificateResources.h> + + +namespace AppInstaller::Certificates +{ + namespace + { + std::string GetSimpleDisplayName(PCCERT_CONTEXT certContext) + { + DWORD characterCount = CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, nullptr, 0); + std::wstring result(characterCount, L'\0'); + characterCount = CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, &result[0], characterCount); + + if (static_cast<size_t>(characterCount) == result.size()) + { + return Utility::ConvertToUTF8(static_cast<std::wstring_view>(result).substr(0, result.size() - 1)); + } + else + { + return "<unknown>"; + } + } + + std::string GetDescriptionOfCertChain(PCCERT_CHAIN_CONTEXT chainContext) + { + PCCERT_SIMPLE_CHAIN chain = chainContext->rgpChain[0]; + std::ostringstream stream; + std::string indent; + + for (DWORD i = 0; i < chain->cElement; ++i) + { + PCCERT_CHAIN_ELEMENT element = chain->rgpElement[(chain->cElement - 1) - i]; + + if (!indent.empty()) + { + stream << std::endl; + } + + stream << indent; + + stream << GetSimpleDisplayName(element->pCertContext); + + indent.append(" "); + } + + return std::move(stream).str(); + } + + std::optional<PinningVerificationType> GetTypeFromString(std::string_view value) + { + std::string lowerValue = Utility::ToLower(value); + + if (lowerValue == "none") + { + return PinningVerificationType::None; + } + else if (lowerValue == "publickey") + { + return PinningVerificationType::PublicKey; + } + else if (lowerValue == "subject") + { + return PinningVerificationType::Subject; + } + else if (lowerValue == "issuer") + { + return PinningVerificationType::Issuer; + } + + return {}; + } + } + + PinningDetails& PinningDetails::LoadCertificate(int resource) + { + return LoadCertificate(Resource::GetResourceAsBytes(resource, CERTIFICATE_RESOURCE_TYPE)); + } + + PinningDetails& PinningDetails::LoadCertificate(const std::vector<BYTE>& certificateBytes) + { + return LoadCertificate(std::make_pair(&certificateBytes[0], certificateBytes.size())); + } + + PinningDetails& PinningDetails::LoadCertificate(const std::pair<const BYTE*, size_t> certificateBytes) + { + m_certificateContext.reset(CertCreateCertificateContext(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, certificateBytes.first, static_cast<DWORD>(certificateBytes.second))); + THROW_LAST_ERROR_IF(!m_certificateContext); + return *this; + } + + PinningDetails& PinningDetails::SetPinning(PinningVerificationType type) + { + m_pinning = type; + return *this; + } + + // The JSON is expected to look like: + // { + // "Validation":["publickey"], + // "EmbeddedCertificate":"<Hexadecimal string data for certificate>" + // } + bool PinningDetails::LoadFrom(const Json::Value& configuration) + { + const std::string validationName = "Validation"; + + if (!configuration.isMember(validationName)) + { + AICLI_LOG(Core, Warning, << "Details JSON item has no member " << validationName); + return false; + } + + auto validationValue = JSON::GetValue<std::vector<std::string>>(configuration[validationName]); + if (!validationValue) + { + AICLI_LOG(Core, Warning, << "Details JSON item member " << validationName << " was not an array of strings"); + return false; + } + + for (const std::string& singleValidation : validationValue.value()) + { + auto validationType = GetTypeFromString(singleValidation); + + if (!validationType) + { + AICLI_LOG(Core, Warning, << "Details JSON validation is unknown: " << singleValidation); + return false; + } + + m_pinning |= validationType.value(); + } + + if (m_pinning == PinningVerificationType::None) + { + // No need to load a certificate if not doing any pinning + return true; + } + + const std::string embeddedCertificateName = "EmbeddedCertificate"; + + if (!configuration.isMember(embeddedCertificateName)) + { + AICLI_LOG(Core, Warning, << "Details JSON item has no member " << embeddedCertificateName); + return false; + } + + auto embeddedCertificateValue = JSON::GetValue<std::string>(configuration[embeddedCertificateName]); + if (!validationValue) + { + AICLI_LOG(Core, Warning, << "Details JSON item member " << embeddedCertificateName << " was not a string"); + return false; + } + + auto embeddedCertificateBytes = Utility::ParseFromHexString(embeddedCertificateValue.value()); + LoadCertificate(embeddedCertificateBytes); + + return true; + } + + bool PinningDetails::Validate(PCCERT_CONTEXT certContext) const + { + if (WI_IsFlagSet(m_pinning, PinningVerificationType::PublicKey)) + { + if (!CertComparePublicKeyInfo( + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + &m_certificateContext.get()->pCertInfo->SubjectPublicKeyInfo, + &certContext->pCertInfo->SubjectPublicKeyInfo)) + { + AICLI_LOG(Core, Verbose, << "Public key mismatch: Expected certificate [" << GetSimpleDisplayName(m_certificateContext.get()) << "], Actual certificate [" << GetSimpleDisplayName(certContext) << "]"); + return false; + } + } + + if (WI_IsFlagSet(m_pinning, PinningVerificationType::Subject)) + { + if (!CertCompareCertificateName( + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + &m_certificateContext.get()->pCertInfo->Subject, + &certContext->pCertInfo->Subject)) + { + AICLI_LOG(Core, Verbose, << "Subject mismatch: Expected certificate [" << GetSimpleDisplayName(m_certificateContext.get()) << "], Actual certificate [" << GetSimpleDisplayName(certContext) << "]"); + return false; + } + } + + if (WI_IsFlagSet(m_pinning, PinningVerificationType::Issuer)) + { + if (!CertCompareCertificateName( + X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, + &m_certificateContext.get()->pCertInfo->Issuer, + &certContext->pCertInfo->Issuer)) + { + AICLI_LOG(Core, Verbose, << "Issuer mismatch: Expected certificate [" << GetSimpleDisplayName(m_certificateContext.get()) << "], Actual certificate [" << GetSimpleDisplayName(certContext) << "]"); + return false; + } + } + + return true; + } + + PinningChain::Node PinningChain::Node::Next() + { + if (!HasNext()) + { + m_chain.get().emplace_back(); + } + + return { m_chain, m_index + 1 }; + } + + const PinningChain::Node PinningChain::Node::Next() const + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !HasNext()); + return { m_chain, m_index + 1 }; + } + + void PinningChain::Node::RemoveNext() + { + m_chain.get().erase(m_chain.get().begin() + m_index + 1, m_chain.get().end()); + } + + bool PinningChain::Node::HasNext() const + { + return (m_index + 1 < m_chain.get().size()); + } + + PinningChain::Node::Node(std::vector<PinningDetails>& chain, size_t index) : + m_chain(chain), m_index(index) {} + + PinningChain::Node PinningChain::Root() + { + if (m_chain.empty()) + { + m_chain.emplace_back(); + } + + return { m_chain, 0 }; + } + + const PinningChain::Node PinningChain::Root() const + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), m_chain.empty()); + return { const_cast<std::vector<PinningDetails>&>(m_chain), 0 }; + } + + bool PinningChain::Validate(PCCERT_CHAIN_CONTEXT chainContext) const + { + if (m_chain.empty()) + { + // An empty chain rejects all inputs. + AICLI_LOG(Core, Warning, << "Empty pinning chain blindly rejecting chain context"); + return false; + } + + THROW_HR_IF(E_INVALIDARG, chainContext->cChain == 0); + + // Currently don't support chains bridged with CTLs; there must be only one simple chain that terminates in a trusted root. + if (chainContext->cChain > 1) + { + AICLI_LOG(Core, Verbose, << "Rejecting chain context with multiple chains"); + return false; + } + + PCCERT_SIMPLE_CHAIN chain = chainContext->rgpChain[0]; + + if (chain->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) + { + AICLI_LOG(Core, Verbose, << "Rejecting simple chain context with bad TrustStatus: " << chain->TrustStatus.dwErrorStatus << " [" << chain->TrustStatus.dwInfoStatus << "]"); + return false; + } + + if (chain->pTrustListInfo) + { + // This should not happen as the only reason for pTrustListInfo to be set is when `chainContext->cChain > 1`, which is rejected above + AICLI_LOG(Core, Verbose, << "Rejecting simple chain context with CTL info"); + return false; + } + + if (static_cast<size_t>(chain->cElement) != m_chain.size()) + { + AICLI_LOG(Core, Verbose, << "Rejecting simple chain context based on size: expected " << m_chain.size() << ", got " << chain->cElement); + return false; + } + + for (DWORD i = 0; i < chain->cElement; ++i) + { + PCCERT_CHAIN_ELEMENT element = chain->rgpElement[(chain->cElement - 1) - i]; + + if (element->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) + { + AICLI_LOG(Core, Verbose, << "Rejecting chain element with bad TrustStatus: " << element->TrustStatus.dwErrorStatus << " [" << element->TrustStatus.dwInfoStatus << "]"); + return false; + } + + if (!m_chain[i].Validate(element->pCertContext)) + { + return false; + } + } + + return true; + } + + std::string PinningChain::GetDescription() const + { + if (m_chain.empty()) + { + return "<empty>"; + } + + std::ostringstream stream; + std::string indent; + + for (const PinningDetails& details : m_chain) + { + if (!indent.empty()) + { + stream << std::endl; + } + + stream << indent; + + if (details.GetCertificate()) + { + stream << GetSimpleDisplayName(details.GetCertificate()) << " : "; + } + + if (details.GetPinning() == PinningVerificationType::None) + { + stream << "<No verification>"; + } + + bool prepend = false; + + if (WI_IsFlagSet(details.GetPinning(), PinningVerificationType::PublicKey)) + { + stream << "PublicKey"; + prepend = true; + } + + if (WI_IsFlagSet(details.GetPinning(), PinningVerificationType::Subject)) + { + if (prepend) + { + stream << " | "; + } + stream << "Subject"; + prepend = true; + } + + if (WI_IsFlagSet(details.GetPinning(), PinningVerificationType::Issuer)) + { + if (prepend) + { + stream << " | "; + } + stream << "Issuer"; + prepend = true; + } + + indent.append(" "); + } + + return std::move(stream).str(); + } + + // The JSON is expected to look like: + // { + // "Chain":[ + // { <See PinningDetails::LoadFrom> + // "Validation":["publickey"], + // "EmbeddedCertificate":"<Hexadecimal string data for certificate>" + // }, + // { + // "Validation":["subject","issuer"], + // "EmbeddedCertificate":"<Hexadecimal string data for certificate>" + // }, + // ... + // ] + // } + bool PinningChain::LoadFrom(const Json::Value& configuration) + { + const std::string chainName = "Chain"; + if (!configuration.isMember(chainName)) + { + AICLI_LOG(Core, Warning, << "Chains JSON item has no member " << chainName); + return false; + } + + const auto& chain = configuration[chainName]; + if (!chain.isArray()) + { + AICLI_LOG(Core, Warning, << "Chain JSON input is not an array"); + return false; + } + + for (const auto& configItem : chain) + { + PinningDetails details; + if (!details.LoadFrom(configItem)) + { + return false; + } + + m_chain.emplace_back(std::move(details)); + } + + return true; + } + + PinningConfiguration::PinningConfiguration(std::string identifier) : m_identifier(identifier) + { + if (m_identifier.empty()) + { + GUID guid; + LOG_IF_FAILED(CoCreateGuid(&guid)); + wchar_t identifierBuffer[256] = {}; + (void)StringFromGUID2(guid, identifierBuffer, ARRAYSIZE(identifierBuffer)); + m_identifier = Utility::ConvertToUTF8(identifierBuffer); + } + } + + void PinningConfiguration::AddChain(PinningChain chain) + { + AICLI_LOG(Core, Verbose, << "Adding chain to pinning configuration [" << m_identifier << "]:\n" << chain.GetDescription()); + m_configuration.emplace_back(std::move(chain)); + } + + bool PinningConfiguration::Validate(PCCERT_CONTEXT certContext) const + { + if (m_configuration.empty()) + { + // No pinning configured + return true; + } + + const BYTE* encodedBegin = certContext->pbCertEncoded; + const BYTE* encodedEnd = encodedBegin + certContext->cbCertEncoded; + if (certContext->cbCertEncoded == m_cachedCertificate.size() && + std::equal(encodedBegin, encodedEnd, m_cachedCertificate.begin())) + { + // We have seen this certificate and deemed it valid already. + return true; + } + + // Get the chain for the given leaf certificate + wil::unique_cert_chain_context chainContext; + + char oidPkixKpServerAuth[] = szOID_PKIX_KP_SERVER_AUTH; + std::array<char*, 1> chainUses = { + oidPkixKpServerAuth, + }; + + CERT_CHAIN_PARA chainParameters = {}; + chainParameters.cbSize = sizeof(chainParameters); + chainParameters.RequestedUsage.dwType = USAGE_MATCH_TYPE_OR; + chainParameters.RequestedUsage.Usage.cUsageIdentifier = static_cast<DWORD>(chainUses.size()); + chainParameters.RequestedUsage.Usage.rgpszUsageIdentifier = chainUses.data(); + + THROW_IF_WIN32_BOOL_FALSE(CertGetCertificateChain(nullptr, certContext, nullptr, certContext->hCertStore, &chainParameters, CERT_CHAIN_REVOCATION_CHECK_CHAIN, nullptr, &chainContext)); + + bool result = false; + + for (const auto& chain : m_configuration) + { + if (chain.Validate(chainContext.get())) + { + result = true; + break; + } + } + + if (result) + { + // Only cache a successful validation + m_cachedCertificate.assign(encodedBegin, encodedEnd); + } + else + { + AICLI_LOG(Core, Error, << "Rejecting certificate [" << GetSimpleDisplayName(certContext) << "] as it did not match anything in pinning configuration [" << m_identifier << "]:\n" << GetDescriptionOfCertChain(chainContext.get())); + } + + return result; + } + + // The JSON is expected to look like: + // { + // "Chains":[ + // { <See PinningChain::LoadFrom> + // "Chain":[ + // { <See PinningDetails::LoadFrom> + // "Validation":["publickey"], + // "EmbeddedCertificate":"<Hexadecimal string data for certificate>" + // }, + // { + // "Validation":["subject","issuer"], + // "EmbeddedCertificate":"<Hexadecimal string data for certificate>" + // }, + // ... + // ] + // } + // ] + // } + bool PinningConfiguration::LoadFrom(const Json::Value& configuration) + { + const std::string chainsName = "Chains"; + if (!configuration.isMember(chainsName)) + { + AICLI_LOG(Core, Warning, << "PinningConfiguration JSON item has no member " << chainsName); + return false; + } + const auto& chains = configuration[chainsName]; + + if (!chains.isArray()) + { + AICLI_LOG(Core, Warning, << "PinningConfiguration.Chains is not an array"); + return false; + } + + std::vector<PinningChain> resultCache; + + for (const auto& configItem : chains) + { + PinningChain chain; + if (!chain.LoadFrom(configItem)) + { + return false; + } + + resultCache.emplace_back(std::move(chain)); + } + + // Move all chains into the config now that we have succeeded + for (auto& result : resultCache) + { + AddChain(std::move(result)); + } + + return true; + } +} diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -222,6 +222,8 @@ namespace AppInstaller return "Failed to install package dependencies."; case APPINSTALLER_CLI_ERROR_BIND_WITH_EMBEDDED_NULL: return "Embedded null characters are disallowed for SQLite"; + case APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH: + return "The server certificate did not match any of the expected values."; case APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND: return "Failed to find the nested installer in the archive."; case APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH: diff --git a/src/AppInstallerCommonCore/GroupPolicy.cpp b/src/AppInstallerCommonCore/GroupPolicy.cpp @@ -196,6 +196,19 @@ namespace AppInstaller::Settings return std::nullopt; } +#ifndef AICLI_DISABLE_TEST_HOOKS + // Enable certificate pinning configuration through GP sources for testing + const std::string pinningConfigurationName = "CertificatePinning"; + if (sourceJson.isMember(pinningConfigurationName)) + { + source.PinningConfiguration = Certificates::PinningConfiguration(source.Name); + if (!source.PinningConfiguration.LoadFrom(sourceJson[pinningConfigurationName])) + { + return std::nullopt; + } + } +#endif + return source; } } diff --git a/src/AppInstallerCommonCore/JsonSchemaValidation.cpp b/src/AppInstallerCommonCore/JsonSchemaValidation.cpp @@ -2,46 +2,18 @@ // Licensed under the MIT License. #include "pch.h" #include "winget/JsonSchemaValidation.h" +#include "winget/Resources.h" namespace AppInstaller::JsonSchema { - std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType) - { - HMODULE resourceModule = NULL; - GetModuleHandleEx( - GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, - (PCWSTR)LoadResourceAsString, - &resourceModule); - THROW_LAST_ERROR_IF_NULL(resourceModule); - - HRSRC resourceInfoHandle = FindResource(resourceModule, resourceName, resourceType); - THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); - - HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); - THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); - - ULONG resourceSize = 0; - char* resourceContent = NULL; - resourceSize = SizeofResource(resourceModule, resourceInfoHandle); - THROW_LAST_ERROR_IF(resourceSize == 0); - - resourceContent = reinterpret_cast<char*>(LockResource(resourceMemoryHandle)); - THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); - - std::string resourceStr; - resourceStr.assign(resourceContent, resourceSize); - - return resourceStr; - } - - Json::Value LoadSchemaDoc(const std::string& schemaStr) + Json::Value LoadSchemaDoc(std::string_view schemaStr) { Json::Value schemaJson; int schemaLength = static_cast<int>(schemaStr.length()); Json::CharReaderBuilder charReaderBuilder; const std::unique_ptr<Json::CharReader> jsonReader(charReaderBuilder.newCharReader()); std::string errorMsg; - if (!jsonReader->parse(schemaStr.c_str(), schemaStr.c_str() + schemaLength, &schemaJson, &errorMsg)) + if (!jsonReader->parse(schemaStr.data(), schemaStr.data() + schemaLength, &schemaJson, &errorMsg)) { THROW_HR_MSG(E_UNEXPECTED, "Jsoncpp parser failed to parse the schema doc. Reason: %hs", errorMsg.c_str()); } @@ -51,7 +23,7 @@ namespace AppInstaller::JsonSchema Json::Value LoadResourceAsSchemaDoc(PCWSTR resourceName, PCWSTR resourceType) { - return LoadSchemaDoc(LoadResourceAsString(resourceName, resourceType)); + return LoadSchemaDoc(Resource::GetResourceAsString(resourceName, resourceType)); } void PopulateSchema(const Json::Value& schemaJson, valijson::Schema& schema) diff --git a/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestSchemaValidation.cpp @@ -6,6 +6,7 @@ #include "winget/ManifestCommon.h" #include "winget/ManifestSchemaValidation.h" #include "winget/ManifestYamlParser.h" +#include "winget/Resources.h" #include <ManifestSchema.h> @@ -159,7 +160,7 @@ namespace AppInstaller::Manifest::YamlParser THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } - std::string schemaStr = JsonSchema::LoadResourceAsString(MAKEINTRESOURCE(idx), MAKEINTRESOURCE(MANIFESTSCHEMA_RESOURCE_TYPE)); + std::string_view schemaStr = Resource::GetResourceAsString(idx, MANIFESTSCHEMA_RESOURCE_TYPE); return JsonSchema::LoadSchemaDoc(schemaStr); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -106,6 +106,7 @@ #define APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_NOT_FOUND ((HRESULT)0x8A15005B) #define APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED ((HRESULT)0x8A15005C) #define APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH ((HRESULT)0x8A15005D) +#define APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH ((HRESULT)0x8A15005E) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -221,4 +221,10 @@ namespace AppInstaller::Utility return {}; } } + + // Converts the given bytes into a hexadecimal string. + std::string ConvertToHexString(const std::vector<uint8_t>& buffer, size_t byteCount = 0); + + // Converts the given hexadecimal string into bytes. + std::vector<uint8_t> ParseFromHexString(const std::string& value, size_t byteCount = 0); } diff --git a/src/AppInstallerCommonCore/Public/winget/AdminSettings.h b/src/AppInstallerCommonCore/Public/winget/AdminSettings.h @@ -11,6 +11,7 @@ namespace AppInstaller::Settings { Unknown, LocalManifestFiles, + BypassCertificatePinningForMicrosoftStore, }; AdminSetting StringToAdminSetting(std::string_view in); diff --git a/src/AppInstallerCommonCore/Public/winget/Certificates.h b/src/AppInstallerCommonCore/Public/winget/Certificates.h @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <wincrypt.h> + +#include <json/json-forwards.h> +#include <wil/resource.h> + +#include <functional> +#include <vector> + + +namespace AppInstaller::Certificates +{ + // Defines the types of certificate pinning to perform. + enum class PinningVerificationType : uint32_t + { + None = 0x0, + PublicKey = 0x1, + Subject = 0x2, + Issuer = 0x4, + }; + + DEFINE_ENUM_FLAG_OPERATORS(PinningVerificationType); + + // Contains the specific information about a certificate to pin. + struct PinningDetails + { + PinningDetails() = default; + + PinningDetails(const PinningDetails&) = default; + PinningDetails& operator=(const PinningDetails&) = default; + + PinningDetails(PinningDetails&&) = default; + PinningDetails& operator=(PinningDetails&&) = default; + + // Loads the certificate context. + PinningDetails& LoadCertificate(int resource); + PinningDetails& LoadCertificate(const std::vector<BYTE>& certificateBytes); + PinningDetails& LoadCertificate(const std::pair<const BYTE*,size_t> certificateBytes); + PCCERT_CONTEXT GetCertificate() const { return m_certificateContext.get(); } + + PinningDetails& SetPinning(PinningVerificationType type); + PinningVerificationType GetPinning() const { return m_pinning; } + + // Validates the given certificate against the pinning information. + // Returns true to indicate that the certificate meets the pinning configuration criteria. + // Returns false to indicate the it does not. + bool Validate(PCCERT_CONTEXT certContext) const; + + // Loads the pinning details from the given JSON. + [[nodiscard]] bool LoadFrom(const Json::Value& configuration); + + private: + wil::shared_cert_context m_certificateContext; + PinningVerificationType m_pinning = PinningVerificationType::None; + }; + + // Contains the full chain of pinning details. + struct PinningChain + { + PinningChain() = default; + + PinningChain(const PinningChain&) = default; + PinningChain& operator=(const PinningChain&) = default; + + PinningChain(PinningChain&&) = default; + PinningChain& operator=(PinningChain&&) = default; + + // A single entry in the chain. + struct Node + { + friend PinningChain; + + // Access the value + PinningDetails* operator->() { return &m_chain.get()[m_index]; } + + // Create/access the next node in the chain. + Node Next(); + const Node Next() const; + + // Drops the next node (and all subsequent nodes) from the chain. + void RemoveNext(); + + // Indicates if there is already an existing next node. + bool HasNext() const; + + private: + Node(std::vector<PinningDetails>& chain, size_t index); + + std::reference_wrapper<std::vector<PinningDetails>> m_chain; + size_t m_index = 0; + }; + + // Gets the root certificate pinning details in the chain. + // These will correspond to the root of the certificate chain being verified. + Node Root(); + const Node Root() const; + + // Validates the given certificate chain against the configuration. + // Returns true to indicate that the chain meets the pinning configuration criteria. + // Returns false to indicate the it does not. + bool Validate(PCCERT_CHAIN_CONTEXT chainContext) const; + + // Gets a description of the pinning chain. + std::string GetDescription() const; + + // Loads the pinning chain from the given JSON. + [[nodiscard]] bool LoadFrom(const Json::Value& configuration); + + private: + std::vector<PinningDetails> m_chain; + }; + + // Holds the details about how a certificate chain is to be validated (aka "pinned"). + struct PinningConfiguration + { + PinningConfiguration(std::string identifier = {}); + + PinningConfiguration(const PinningConfiguration&) = default; + PinningConfiguration& operator=(const PinningConfiguration&) = default; + + PinningConfiguration(PinningConfiguration&&) = default; + PinningConfiguration& operator=(PinningConfiguration&&) = default; + + // Adds a possible chain to the configuration. + // For a certificate to be valid, it must match only one of the configured chains. + void AddChain(PinningChain chain); + + // Validates the given leaf certificate against the configuration. + // Returns true to indicate that the certificate meets the pinning configuration criteria. + // Returns false to indicate the it does not. + bool Validate(PCCERT_CONTEXT certContext) const; + + // True if no pinning is configured. + bool IsEmpty() const { return m_configuration.empty(); } + + // Loads the pinning configuration from the given JSON. + [[nodiscard]] bool LoadFrom(const Json::Value& configuration); + + private: + // The identifier used when logging. + std::string m_identifier; + + // The configured chains. + std::vector<PinningChain> m_configuration; + + // We store the last certificate that was successfully validated to speed up subsequent checks. + // Only cache a single certificate under the assumption that most of the time there will + // only be a single server certificate in use. + mutable std::vector<BYTE> m_cachedCertificate; + }; +} diff --git a/src/AppInstallerCommonCore/Public/winget/GroupPolicy.h b/src/AppInstallerCommonCore/Public/winget/GroupPolicy.h @@ -1,10 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerLanguageUtilities.h> +#include <winget/Certificates.h> +#include <winget/Registry.h> +#include <winget/Resources.h> -#include "AppInstallerLanguageUtilities.h" -#include "winget/Registry.h" -#include "winget/Resources.h" #include <string_view> using namespace std::string_view_literals; @@ -78,6 +79,10 @@ namespace AppInstaller::Settings std::string Data; std::string Identifier; +#ifndef AICLI_DISABLE_TEST_HOOKS + Certificates::PinningConfiguration PinningConfiguration; +#endif + std::string ToJsonString() const; }; diff --git a/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h b/src/AppInstallerCommonCore/Public/winget/JsonSchemaValidation.h @@ -5,11 +5,8 @@ namespace AppInstaller::JsonSchema { - // Load an embedded resource from binary and return as std::string - std::string LoadResourceAsString(PCWSTR resourceName, PCWSTR resourceType); - // Load schema as parsed json doc - Json::Value LoadSchemaDoc(const std::string& schemaStr); + Json::Value LoadSchemaDoc(std::string_view schemaStr); // Load an embedded resource from binary and return as Json::Value Json::Value LoadResourceAsSchemaDoc(PCWSTR resourceName, PCWSTR resourceType); diff --git a/src/AppInstallerCommonCore/Public/winget/Resources.h b/src/AppInstallerCommonCore/Public/winget/Resources.h @@ -4,40 +4,56 @@ #include <winrt/Windows.ApplicationModel.Resources.h> #include <string> +#include <vector> using namespace std::string_view_literals; -namespace AppInstaller::StringResource +namespace AppInstaller { - + namespace StringResource + { #define WINGET_WIDE_STRINGIFY_HELP(_id_) L ## _id_ #define WINGET_WIDE_STRINGIFY(_id_) WINGET_WIDE_STRINGIFY_HELP(_id_) #define WINGET_DEFINE_RESOURCE_STRINGID(_id_) static constexpr AppInstaller::StringResource::StringId _id_ { WINGET_WIDE_STRINGIFY(#_id_) ## sv } - // A resource identifier - struct StringId : public std::wstring_view - { - explicit constexpr StringId(std::wstring_view id) : std::wstring_view(id) {} - }; + // A resource identifier + struct StringId : public std::wstring_view + { + explicit constexpr StringId(std::wstring_view id) : std::wstring_view(id) {} + }; - // Resource string identifiers. - struct String + // Resource string identifiers. + struct String + { + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWinGet); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWingetSettings); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableExperimentalFeatures); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableLocalManifests); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableHashOverride); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableDefaultSource); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableMSStoreSource); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyAdditionalSources); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyAllowedSources); + WINGET_DEFINE_RESOURCE_STRINGID(PolicySourceAutoUpdateInterval); + + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldFormat); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldValue); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidValueFromPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningLoadedBackupSettings); + WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningParseError); + }; + } + + namespace Resource { - WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWinGet); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWingetSettings); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableExperimentalFeatures); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableLocalManifests); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableHashOverride); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableDefaultSource); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableMSStoreSource); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyAdditionalSources); - WINGET_DEFINE_RESOURCE_STRINGID(PolicyAllowedSources); - WINGET_DEFINE_RESOURCE_STRINGID(PolicySourceAutoUpdateInterval); - - WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldFormat); - WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldValue); - WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidValueFromPolicy); - WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningLoadedBackupSettings); - WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningParseError); - }; + // Get an embedded resource from the binary and return as std::string_view. + // Resource data is valid as long as the binary is loaded. + std::string_view GetResourceAsString(int resourceName, int resourceType); + std::string_view GetResourceAsString(PCWSTR resourceName, PCWSTR resourceType); + + // Get an embedded resource from the binary and return as std::pair<BYTE*, size_t>. + // Resource data is valid as long as the binary is loaded. + std::pair<const BYTE*, size_t> GetResourceAsBytes(int resourceName, int resourceType); + std::pair<const BYTE*, size_t> GetResourceAsBytes(PCWSTR resourceName, PCWSTR resourceType); + } } diff --git a/src/AppInstallerCommonCore/Resources.cpp b/src/AppInstallerCommonCore/Resources.cpp @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/Resources.h" +#include "Public/AppInstallerLogging.h" +#include "Public/AppInstallerStrings.h" + +namespace AppInstaller::Resource +{ + namespace + { + std::pair<void*, size_t> GetResourceData(PCWSTR resourceName, PCWSTR resourceType) + { + HMODULE resourceModule = nullptr; + GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast<PCWSTR>(GetResourceData), + &resourceModule); + THROW_LAST_ERROR_IF_NULL(resourceModule); + + HRSRC resourceInfoHandle = FindResourceW(resourceModule, resourceName, resourceType); + THROW_LAST_ERROR_IF_NULL(resourceInfoHandle); + + HGLOBAL resourceMemoryHandle = LoadResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF_NULL(resourceMemoryHandle); + + DWORD resourceSize = SizeofResource(resourceModule, resourceInfoHandle); + THROW_LAST_ERROR_IF(resourceSize == 0); + + void* resourceContent = LockResource(resourceMemoryHandle); + THROW_HR_IF_NULL(E_UNEXPECTED, resourceContent); + + return std::make_pair(resourceContent, static_cast<size_t>(resourceSize)); + } + } + + std::string_view GetResourceAsString(int resourceName, int resourceType) + { + return GetResourceAsString(MAKEINTRESOURCE(resourceName), MAKEINTRESOURCE(resourceType)); + } + + std::string_view GetResourceAsString(PCWSTR resourceName, PCWSTR resourceType) + { + auto resourceData = GetResourceData(resourceName, resourceType); + return { reinterpret_cast<char*>(resourceData.first), resourceData.second }; + } + + std::pair<const BYTE*, size_t> GetResourceAsBytes(int resourceName, int resourceType) + { + return GetResourceAsBytes(MAKEINTRESOURCE(resourceName), MAKEINTRESOURCE(resourceType)); + } + + std::pair<const BYTE*, size_t> GetResourceAsBytes(PCWSTR resourceName, PCWSTR resourceType) + { + auto resourceData = GetResourceData(resourceName, resourceType); + return std::make_pair(reinterpret_cast<BYTE*>(resourceData.first), resourceData.second); + } +} diff --git a/src/AppInstallerCommonCore/SHA256.cpp b/src/AppInstallerCommonCore/SHA256.cpp @@ -92,21 +92,7 @@ namespace AppInstaller::Utility { std::string SHA256::ConvertToString(const HashBuffer& hashBuffer) { - if (hashBuffer.size() != HashBufferSizeInBytes) - { - THROW_HR_MSG(E_INVALIDARG, "Invalid SHA256 size when SHA256::ConvertToString() is called."); - } - - char resultBuffer[HashStringSizeInChars + 1]; - - for (size_t i = 0; i < HashBufferSizeInBytes; i++) - { - sprintf_s(resultBuffer + i * 2, 3, "%02x", hashBuffer[i]); - } - - resultBuffer[HashStringSizeInChars] = '\0'; - - return std::string(resultBuffer); + return Utility::ConvertToHexString(hashBuffer, HashBufferSizeInBytes); } std::wstring SHA256::ConvertToWideString(const HashBuffer& hashBuffer) @@ -116,22 +102,7 @@ namespace AppInstaller::Utility { SHA256::HashBuffer SHA256::ConvertToBytes(const std::string& hashStr) { - if (hashStr.size() != HashStringSizeInChars) - { - THROW_HR_MSG(E_INVALIDARG, "Invalid SHA256 size when SHA256::ConvertToBytes() is called."); - } - - auto hashCStr = hashStr.c_str(); - SHA256::HashBuffer resultBuffer; - - resultBuffer.resize(HashBufferSizeInBytes); - - for (size_t i = 0; i < HashBufferSizeInBytes; i++) - { - sscanf_s(hashCStr + 2 * i, "%02hhx", &resultBuffer[i]); - } - - return resultBuffer; + return Utility::ParseFromHexString(hashStr, HashBufferSizeInBytes); } SHA256::HashBuffer SHA256::ComputeHash(const std::uint8_t* buffer, std::uint32_t cbBuffer) diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -16,6 +16,7 @@ #include <DbgHelp.h> #include <SoftPub.h> #include <WinTrust.h> +#include <wincrypt.h> #include "TraceLogging.h" diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -81,7 +81,9 @@ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <ImportGroup Label="ExtensionSettings"> </ImportGroup> - <ImportGroup Label="Shared" /> + <ImportGroup Label="Shared"> + <Import Project="..\CertificateResources\CertificateResources.vcxitems" Label="Shared" /> + </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> </ImportGroup> diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include <Public/winget/RepositorySearch.h> -#include <Public/winget/PackageTrackingCatalog.h> +#include <winget/RepositorySearch.h> +#include <winget/PackageTrackingCatalog.h> #include <AppInstallerProgress.h> +#include <winget/Certificates.h> #include <chrono> #include <filesystem> @@ -112,6 +113,9 @@ namespace AppInstaller::Repository // Whether the source supports InstalledSource correlation. bool SupportInstalledSearchCorrelation = true; + + // The configuration of how the server certificate will be validated. + Certificates::PinningConfiguration CertificatePinningConfiguration; }; // Individual source agreement entry. Label will be highlighted in the display as the key of the agreement entry. diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -259,9 +259,20 @@ namespace AppInstaller::Repository { m_isSourceToBeAdded = true; SourceDetails details; - details.Name = name; - details.Arg = arg; - details.Type = type; + + std::optional<WellKnownSource> wellKnownSourceCheck = CheckForWellKnownSourceMatch(name, arg, type); + + if (wellKnownSourceCheck) + { + details = GetWellKnownSourceDetailsInternal(wellKnownSourceCheck.value()); + } + else + { + details.Name = name; + details.Arg = arg; + details.Type = type; + } + m_sourceReferences.emplace_back(CreateSourceFromDetails(details)); } @@ -585,7 +596,12 @@ namespace AppInstaller::Repository } sourceDetails.LastUpdateTime = Utility::ConvertUnixEpochToSystemClock(0); - sourceDetails.Origin = SourceOrigin::User; + + // Allow the origin to stay as Default if the incoming details match a well known value + if (!(sourceDetails.Origin == SourceOrigin::Default && CheckForWellKnownSourceMatch(sourceDetails.Name, sourceDetails.Arg, sourceDetails.Type))) + { + sourceDetails.Origin = SourceOrigin::User; + } bool result = AddSourceFromDetails(sourceDetails, progress); if (result) diff --git a/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp b/src/AppInstallerRepositoryCore/Rest/RestSourceFactory.cpp @@ -40,7 +40,7 @@ namespace AppInstaller::Repository::Rest std::shared_ptr<ISource> Open(IProgressCallback&) override { Initialize(); - RestClient restClient = RestClient::Create(m_details.Arg, m_customHeader); + RestClient restClient = RestClient::Create(m_details.Arg, m_customHeader, m_httpClientHelper); return std::make_shared<RestSource>(m_details, m_information, std::move(restClient)); } @@ -50,7 +50,8 @@ namespace AppInstaller::Repository::Rest std::call_once(m_initializeFlag, [&]() { - RestClient restClient = RestClient::Create(m_details.Arg, m_customHeader); + m_httpClientHelper.SetPinningConfiguration(m_details.CertificatePinningConfiguration); + RestClient restClient = RestClient::Create(m_details.Arg, m_customHeader, m_httpClientHelper); m_details.Identifier = restClient.GetSourceIdentifier(); @@ -69,6 +70,7 @@ namespace AppInstaller::Repository::Rest } SourceDetails m_details; + Schema::HttpClientHelper m_httpClientHelper; SourceInformation m_information; std::optional<std::string> m_customHeader; std::once_flag m_initializeFlag; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.cpp @@ -17,9 +17,21 @@ namespace AppInstaller::Repository::Rest::Schema request.headers().add(web::http::header_names::user_agent, c_defaultUserAgent); } } + + void NativeHandleServerCertificateValidation(web::http::client::native_handle handle, const Certificates::PinningConfiguration& pinningConfiguration) + { + HINTERNET requestHandle = reinterpret_cast<HINTERNET>(handle); + + // Get certificate and pass along to pinning config + wil::unique_cert_context certContext; + DWORD bufferSize = sizeof(&certContext); + THROW_IF_WIN32_BOOL_FALSE(WinHttpQueryOption(requestHandle, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &certContext, &bufferSize)); + + THROW_HR_IF(APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH, !pinningConfiguration.Validate(certContext.get())); + } } - HttpClientHelper::HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> stage) : m_defaultRequestHandlerStage(stage) {} + HttpClientHelper::HttpClientHelper(std::shared_ptr<web::http::http_pipeline_stage> stage) : m_defaultRequestHandlerStage(std::move(stage)) {} pplx::task<web::http::http_response> HttpClientHelper::Post( const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t>& headers) const @@ -86,14 +98,22 @@ namespace AppInstaller::Repository::Rest::Schema return ValidateAndExtractResponse(httpResponse); } + void HttpClientHelper::SetPinningConfiguration(const Certificates::PinningConfiguration& configuration) + { + m_clientConfig.set_nativehandle_servercertificate_validation([pinConfig = configuration](web::http::client::native_handle handle) + { + NativeHandleServerCertificateValidation(handle, pinConfig); + }); + } + web::http::client::http_client HttpClientHelper::GetClient(const utility::string_t& uri) const { - web::http::client::http_client client{ uri }; + web::http::client::http_client client{ uri, m_clientConfig }; // Add default custom handlers if any. if (m_defaultRequestHandlerStage) { - client.add_handler(m_defaultRequestHandlerStage.value()); + client.add_handler(m_defaultRequestHandlerStage); } return client; diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h b/src/AppInstallerRepositoryCore/Rest/Schema/HttpClientHelper.h @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <winget/Certificates.h> + #include <cpprest/http_client.h> #include <cpprest/json.h> @@ -11,7 +13,7 @@ namespace AppInstaller::Repository::Rest::Schema { struct HttpClientHelper { - HttpClientHelper(std::optional<std::shared_ptr<web::http::http_pipeline_stage>> = {}); + HttpClientHelper(std::shared_ptr<web::http::http_pipeline_stage> = {}); pplx::task<web::http::http_response> Post(const utility::string_t& uri, const web::json::value& body, const std::unordered_map<utility::string_t, utility::string_t> &headers = {}) const; @@ -20,7 +22,8 @@ namespace AppInstaller::Repository::Rest::Schema pplx::task<web::http::http_response> Get(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; std::optional<web::json::value> HandleGet(const utility::string_t& uri, const std::unordered_map<utility::string_t, utility::string_t>& headers = {}) const; - + + void SetPinningConfiguration(const Certificates::PinningConfiguration& configuration); protected: std::optional<web::json::value> ValidateAndExtractResponse(const web::http::http_response& response) const; @@ -29,6 +32,7 @@ namespace AppInstaller::Repository::Rest::Schema private: web::http::client::http_client GetClient(const utility::string_t& uri) const; - std::optional<std::shared_ptr<web::http::http_pipeline_stage>> m_defaultRequestHandlerStage; + std::shared_ptr<web::http::http_pipeline_stage> m_defaultRequestHandlerStage; + web::http::client::http_client_config m_clientConfig; }; } diff --git a/src/AppInstallerRepositoryCore/SourceList.cpp b/src/AppInstallerRepositoryCore/SourceList.cpp @@ -6,6 +6,10 @@ #include "Microsoft/PreIndexedPackageSourceFactory.h" #include "Rest/RestSourceFactory.h" +#include <winget/AdminSettings.h> +#include <winget/Certificates.h> +#include <CertificateResources.h> + using namespace AppInstaller::Settings; using namespace std::string_view_literals; @@ -249,6 +253,26 @@ namespace AppInstaller::Repository return {}; } + std::optional<WellKnownSource> CheckForWellKnownSourceMatch(std::string_view name, std::string_view arg, std::string_view type) + { + if (name == s_Source_WingetCommunityDefault_Name && arg == s_Source_WingetCommunityDefault_Arg && type == Microsoft::PreIndexedPackageSourceFactory::Type()) + { + return WellKnownSource::WinGet; + } + + if (name == s_Source_MSStoreDefault_Name && arg == s_Source_MSStoreDefault_Arg && type == Rest::RestSourceFactory::Type()) + { + return WellKnownSource::MicrosoftStore; + } + + if (name == s_Source_DesktopFrameworks_Name && arg == s_Source_DesktopFrameworks_Arg && type == Microsoft::PreIndexedPackageSourceFactory::Type()) + { + return WellKnownSource::DesktopFrameworks; + } + + return {}; + } + SourceDetailsInternal GetWellKnownSourceDetailsInternal(WellKnownSource source) { switch (source) @@ -275,6 +299,23 @@ namespace AppInstaller::Repository details.Identifier = s_Source_MSStoreDefault_Identifier; details.TrustLevel = SourceTrustLevel::Trusted; details.SupportInstalledSearchCorrelation = false; + + if (!Settings::IsAdminSettingEnabled(Settings::AdminSetting::BypassCertificatePinningForMicrosoftStore)) + { + using namespace AppInstaller::Certificates; + + PinningChain chain; + auto chainElement = chain.Root(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement = chainElement.Next(); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + + details.CertificatePinningConfiguration = PinningConfiguration("Microsoft Store Source"); + details.CertificatePinningConfiguration.AddChain(std::move(chain)); + } + return details; } case WellKnownSource::DesktopFrameworks: @@ -588,12 +629,14 @@ namespace AppInstaller::Repository { if (GroupPolicies().GetState(TogglePolicy::Policy::AdditionalSources) == PolicyState::Enabled) { + AICLI_LOG(Repo, Verbose, << "Additional sources GP is enabled..."); auto additionalSourcesOpt = GroupPolicies().GetValueRef<ValuePolicy::AdditionalSources>(); if (additionalSourcesOpt.has_value()) { const auto& additionalSources = additionalSourcesOpt->get(); for (const auto& additionalSource : additionalSources) { + AICLI_LOG(Repo, Verbose, << "... with configured source " << additionalSource.Name); SourceDetailsInternal details; details.Name = additionalSource.Name; details.Type = additionalSource.Type; @@ -601,9 +644,20 @@ namespace AppInstaller::Repository details.Data = additionalSource.Data; details.Identifier = additionalSource.Identifier; details.Origin = SourceOrigin::GroupPolicy; +#ifndef AICLI_DISABLE_TEST_HOOKS + details.CertificatePinningConfiguration = additionalSource.PinningConfiguration; +#endif result.emplace_back(std::move(details)); } } + else + { + AICLI_LOG(Repo, Verbose, << "... but has no values."); + } + } + else + { + AICLI_LOG(Repo, Verbose, << "Additional sources GP is not enabled."); } } break; diff --git a/src/AppInstallerRepositoryCore/SourceList.h b/src/AppInstallerRepositoryCore/SourceList.h @@ -11,6 +11,7 @@ namespace AppInstaller::Repository std::string_view GetWellKnownSourceName(WellKnownSource source); std::string_view GetWellKnownSourceArg(WellKnownSource source); std::string_view GetWellKnownSourceIdentifier(WellKnownSource source); + std::optional<WellKnownSource> CheckForWellKnownSourceMatch(std::string_view name, std::string_view arg, std::string_view type); // SourceDetails with additional data used internally. struct SourceDetailsInternal : public SourceDetails diff --git a/src/AppInstallerRepositoryCore/pch.h b/src/AppInstallerRepositoryCore/pch.h @@ -6,6 +6,7 @@ #include <windows.h> #include <urlmon.h> #include <appmodel.h> +#include <winhttp.h> #pragma warning( push ) #pragma warning ( disable : 6001 6340 6388 ) diff --git a/src/CertificateResources/CertificateResources.h b/src/CertificateResources/CertificateResources.h @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +#define CERTIFICATE_RESOURCE_TYPE 400 + +#define IDX_CERTIFICATE_STORE_ROOT_1 401 +#define IDX_CERTIFICATE_STORE_INTERMEDIATE_1 402 +#define IDX_CERTIFICATE_STORE_LEAF_1 403 diff --git a/src/CertificateResources/CertificateResources.rc b/src/CertificateResources/CertificateResources.rc @@ -0,0 +1,69 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" +#include "CertificateResources.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE 9, 1 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + +///////////////////////////////////////////////////////////////////////////// +// +// Packages schema +// + +IDX_CERTIFICATE_STORE_ROOT_1 CERTIFICATE_RESOURCE_TYPE "StoreRoot1.cer" +IDX_CERTIFICATE_STORE_INTERMEDIATE_1 CERTIFICATE_RESOURCE_TYPE "StoreIntermediate1.cer" +IDX_CERTIFICATE_STORE_LEAF_1 CERTIFICATE_RESOURCE_TYPE "StoreLeaf1.cer" diff --git a/src/CertificateResources/CertificateResources.vcxitems b/src/CertificateResources/CertificateResources.vcxitems @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup Label="Globals"> + <MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> + <HasSharedItems>true</HasSharedItems> + <ItemsProjectGuid>{b0bbbd92-943b-408f-b2b2-dbbab4a22d23}</ItemsProjectGuid> + </PropertyGroup> + <ItemDefinitionGroup> + <ClCompile> + <AdditionalIncludeDirectories>%(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory)</AdditionalIncludeDirectories> + </ClCompile> + </ItemDefinitionGroup> + <ItemGroup> + <ProjectCapability Include="SourceItemsFromImports" /> + </ItemGroup> + <ItemGroup> + <None Include="$(MSBuildThisFileDirectory)StoreRoot1.cer" /> + <None Include="$(MSBuildThisFileDirectory)StoreIntermediate1.cer" /> + <None Include="$(MSBuildThisFileDirectory)StoreLeaf1.cer" /> + </ItemGroup> + <ItemGroup> + <ClInclude Include="$(MSBuildThisFileDirectory)CertificateResources.h" /> + <ClInclude Include="$(MSBuildThisFileDirectory)resource.h" /> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="$(MSBuildThisFileDirectory)CertificateResources.rc" /> + </ItemGroup> +</Project>+ \ No newline at end of file diff --git a/src/CertificateResources/CertificateResources.vcxitems.filters b/src/CertificateResources/CertificateResources.vcxitems.filters @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <Filter Include="Certificates"> + <UniqueIdentifier>{ad622b6a-16bb-4e32-921c-f1d6505fc0ed}</UniqueIdentifier> + </Filter> + </ItemGroup> + <ItemGroup> + <None Include="$(MSBuildThisFileDirectory)StoreRoot1.cer"> + <Filter>Certificates</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)StoreIntermediate1.cer"> + <Filter>Certificates</Filter> + </None> + <None Include="$(MSBuildThisFileDirectory)StoreLeaf1.cer"> + <Filter>Certificates</Filter> + </None> + </ItemGroup> + <ItemGroup> + <ClInclude Include="$(MSBuildThisFileDirectory)resource.h" /> + <ClInclude Include="$(MSBuildThisFileDirectory)CertificateResources.h" /> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="$(MSBuildThisFileDirectory)CertificateResources.rc" /> + </ItemGroup> +</Project>+ \ No newline at end of file diff --git a/src/CertificateResources/StoreIntermediate1.cer b/src/CertificateResources/StoreIntermediate1.cer Binary files differ. diff --git a/src/CertificateResources/StoreLeaf1.cer b/src/CertificateResources/StoreLeaf1.cer Binary files differ. diff --git a/src/CertificateResources/StoreRoot1.cer b/src/CertificateResources/StoreRoot1.cer Binary files differ. diff --git a/src/CertificateResources/resource.h b/src/CertificateResources/resource.h @@ -0,0 +1,14 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by CertificateResources.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/LocalhostWebServer/Program.cs b/src/LocalhostWebServer/Program.cs @@ -9,6 +9,7 @@ namespace LocalhostWebServer using System; using System.IO; using Microsoft.Extensions.Configuration; + using System.Security.Cryptography.X509Certificates; public class Program { @@ -21,18 +22,25 @@ namespace LocalhostWebServer Startup.StaticFileRoot = config.GetValue<string>("StaticFileRoot"); Startup.CertPath = config.GetValue<string>("CertPath"); Startup.CertPassword = config.GetValue<string>("CertPassword"); + Startup.PutCertInRoot = config.GetValue<bool>("PutCertInRoot", false); Startup.Port = config.GetValue<Int32>("Port", 5001); if (string.IsNullOrEmpty(Startup.StaticFileRoot) || - string.IsNullOrEmpty(Startup.CertPath) || - string.IsNullOrEmpty(Startup.CertPassword)) + string.IsNullOrEmpty(Startup.CertPath)) { Console.WriteLine("Usage: LocalhostWebServer.exe StaticFileRoot=<Path to Serve Static Root Directory> " + - "CertPath=<Path to HTTPS Developer Certificate> CertPassword=<Certificate Password> <Port=Port Number>"); + "CertPath=<Path to HTTPS Developer Certificate> CertPassword=<Certificate Password> <Port=Port Number> <PutCertInRoot=Boolean>"); return; } Directory.CreateDirectory(Startup.StaticFileRoot); + + if (Startup.PutCertInRoot) + { + X509Certificate2 serverCertificate = new X509Certificate2(Startup.CertPath, Startup.CertPassword, X509KeyStorageFlags.EphemeralKeySet); + File.WriteAllBytes(Path.Combine(Startup.StaticFileRoot, "servercert.cer"), serverCertificate.Export(X509ContentType.Cert)); + } + CreateHostBuilder(args).Build().Run(); } diff --git a/src/LocalhostWebServer/Run-LocalhostWebServer.ps1 b/src/LocalhostWebServer/Run-LocalhostWebServer.ps1 @@ -27,5 +27,5 @@ param( cd $BuildRoot -Start-Process -FilePath "LocalhostWebServer.exe" -ArgumentList "StaticFileRoot=$StaticFileRoot CertPath=$CertPath CertPassword=$CertPassword" +Start-Process -FilePath "LocalhostWebServer.exe" -ArgumentList "StaticFileRoot=$StaticFileRoot CertPath=$CertPath CertPassword=$CertPassword PutCertInRoot=True" diff --git a/src/LocalhostWebServer/Startup.cs b/src/LocalhostWebServer/Startup.cs @@ -21,6 +21,8 @@ namespace LocalhostWebServer public static string CertPassword { get; set; } + public static bool PutCertInRoot { get; set; } + public static int Port { get; set; } public Startup(IConfiguration configuration) diff --git a/src/PowerShell/Microsoft.WinGet.Client/Properties/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Client/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.WinGet.Client.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/src/WindowsPackageManager/WindowsPackageManager.vcxproj b/src/WindowsPackageManager/WindowsPackageManager.vcxproj @@ -89,6 +89,7 @@ <Import Project="..\ManifestSchema\ManifestSchema.vcxitems" Label="Shared" /> <Import Project="..\WinGetSchemas\WinGetSchemas.vcxitems" Label="Shared" /> <Import Project="..\COMServer\COMServer.vcxitems" Label="Shared" /> + <Import Project="..\CertificateResources\CertificateResources.vcxitems" Label="Shared" /> </ImportGroup> <ImportGroup Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> diff --git a/src/cpprestsdk/cpprestsdk/Release/include/cpprest/http_client.h b/src/cpprestsdk/cpprestsdk/Release/include/cpprest/http_client.h @@ -362,6 +362,32 @@ public: if (m_set_user_nativehandle_options) m_set_user_nativehandle_options(handle); } + /// <summary> + /// Sets a callback to enable custom handling when the server certificate is available. + /// </summary> + /// <remarks> + /// The native_handle is the following type depending on the underlying platform: + /// Windows Desktop, WinHTTP - HINTERNET !!! Is only implemented to call in here !!! + /// Windows Runtime, WinRT - IXMLHTTPRequest2 * + /// All other platforms, Boost.Asio: + /// https - boost::asio::ssl::stream<boost::asio::ip::tcp::socket &> * + /// http - boost::asio::ip::tcp::socket * + /// </remarks> + /// <param name="callback">A user callback allowing for validation of the server certificate.</param> + void set_nativehandle_servercertificate_validation(const std::function<void(native_handle)>& callback) + { + m_nativehandle_servercertificate_validation = callback; + } + + /// <summary> + /// Invokes a user's callback to validate the server certificate. + /// </summary> + /// <param name="handle">A internal implementation handle.</param> + void invoke_nativehandle_servercertificate_validation(native_handle handle) const + { + if (m_nativehandle_servercertificate_validation) m_nativehandle_servercertificate_validation(handle); + } + #if !defined(_WIN32) && !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) /// <summary> /// Sets a callback to enable custom setting of the ssl context, at construction time. @@ -418,6 +444,7 @@ private: std::function<void(native_handle)> m_set_user_nativehandle_options; std::function<void(native_handle)> m_set_user_nativesessionhandle_options; + std::function<void(native_handle)> m_nativehandle_servercertificate_validation; #if !defined(_WIN32) && !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) std::function<void(boost::asio::ssl::context&)> m_ssl_context_callback; diff --git a/src/cpprestsdk/cpprestsdk/Release/src/http/client/http_client_winhttp.cpp b/src/cpprestsdk/cpprestsdk/Release/src/http/client/http_client_winhttp.cpp @@ -2039,6 +2039,18 @@ private: case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST: { p_request_context->on_send_request_validate_cn(); + + try + { + p_request_context->m_http_client->client_config().invoke_nativehandle_servercertificate_validation(hRequestHandle); + } + catch (...) + { + p_request_context->report_exception(std::current_exception()); + p_request_context->cleanup(); + return; + } + return; } case WINHTTP_CALLBACK_STATUS_SECURE_FAILURE: