winget-cli

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

commit f7c1a4499af38608f378a30f0a3e2417ec595529
parent 721c81ff47c12c454c4e8d973701b7899f2f22ef
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Tue,  5 Sep 2023 16:26:37 -0700

Make config not experimental and add GP to control it (#3585)

Remove the configuration experimental feature and add a group policy for it instead.  This required moving the GP code and dependencies into the shared library.
Diffstat:
Mdoc/admx/DesktopAppInstaller.admx | 10++++++++++
Mdoc/admx/en-US/DesktopAppInstaller.adml | 6++++++
Msrc/AppInstallerCLICore/Commands/ConfigureCommand.cpp | 2+-
Msrc/AppInstallerCLIE2ETests/ConfigureCommand.cs | 1-
Msrc/AppInstallerCLIE2ETests/ConfigureShowCommand.cs | 9---------
Msrc/AppInstallerCLIE2ETests/ConfigureTestCommand.cs | 1-
Msrc/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs | 9---------
Msrc/AppInstallerCLIE2ETests/GroupPolicy.cs | 14++++++++++++++
Msrc/AppInstallerCLIE2ETests/GroupPolicyHelper.cs | 6++++++
Msrc/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs | 1-
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 3+++
Msrc/AppInstallerCLITests/Certificates.cpp | 66+++++++++++++++++++++++++++++++++---------------------------------
Msrc/AppInstallerCLITests/GroupPolicy.cpp | 7++++---
Msrc/AppInstallerCLITests/HttpClientHelper.cpp | 6+++---
Msrc/AppInstallerCLITests/TestSettings.h | 1+
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 10----------
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 24------------------------
Dsrc/AppInstallerCommonCore/Certificates.cpp | 549-------------------------------------------------------------------------------
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4----
Dsrc/AppInstallerCommonCore/GroupPolicy.cpp | 380-------------------------------------------------------------------------------
Dsrc/AppInstallerCommonCore/Public/winget/Certificates.h | 153-------------------------------------------------------------------------------
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 1-
Dsrc/AppInstallerCommonCore/Public/winget/GroupPolicy.h | 245-------------------------------------------------------------------------------
Msrc/AppInstallerCommonCore/Public/winget/PortableARPEntry.h | 2+-
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2--
Msrc/AppInstallerCommonCore/UserSettings.cpp | 1-
Msrc/AppInstallerRepositoryCore/SourceList.cpp | 12++++++------
Msrc/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj | 11+++++++++++
Msrc/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters | 24++++++++++++++++++++++++
Asrc/AppInstallerSharedLib/Certificates.cpp | 547+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerSharedLib/GroupPolicy.cpp | 382+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/AppInstallerCommonCore/JsonUtil.cpp -> src/AppInstallerSharedLib/JsonUtil.cpp | 0
Asrc/AppInstallerSharedLib/Public/winget/Certificates.h | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerSharedLib/Public/winget/GroupPolicy.h | 246+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/AppInstallerCommonCore/Public/winget/JsonUtil.h -> src/AppInstallerSharedLib/Public/winget/JsonUtil.h | 0
Rsrc/AppInstallerCommonCore/Public/winget/Registry.h -> src/AppInstallerSharedLib/Public/winget/Registry.h | 0
Msrc/AppInstallerSharedLib/Public/winget/Resources.h | 1+
Rsrc/AppInstallerCommonCore/Registry.cpp -> src/AppInstallerSharedLib/Registry.cpp | 0
Msrc/Microsoft.Management.Configuration/ConfigurationProcessor.cpp | 4++++
Msrc/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj | 4++--
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs | 5+++++
Msrc/PowerShell/Microsoft.WinGet.SharedLib/Extensions/EnumPolicyExtension.cs | 2++
Msrc/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/Enums.cs | 5+++++
Msrc/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/TogglePolicy.cs | 2++
Msrc/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.Designer.cs | 9+++++++++
Msrc/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.resx | 3+++
Msrc/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 | 15+++++++++++++++
Msrc/WindowsPackageManager/ConfigurationStaticFunctions.cpp | 2+-
48 files changed, 1500 insertions(+), 1440 deletions(-)

diff --git a/doc/admx/DesktopAppInstaller.admx b/doc/admx/DesktopAppInstaller.admx @@ -156,5 +156,15 @@ <decimal value="0" /> </disabledValue> </policy> + <policy name="EnableWindowsPackageManagerConfiguration" class="Machine" displayName="$(string.EnableWindowsPackageManagerConfiguration)" explainText="$(string.EnableWindowsPackageManagerConfigurationExplanation)" key="Software\Policies\Microsoft\Windows\AppInstaller" valueName="EnableWindowsPackageManagerConfiguration"> + <parentCategory ref="AppInstaller" /> + <supportedOn ref="windows:SUPPORTED_Windows_10_0_RS5" /> + <enabledValue> + <decimal value="1" /> + </enabledValue> + <disabledValue> + <decimal value="0" /> + </disabledValue> + </policy> </policies> </policyDefinitions> diff --git a/doc/admx/en-US/DesktopAppInstaller.adml b/doc/admx/en-US/DesktopAppInstaller.adml @@ -103,6 +103,12 @@ If you disable or do not configure this setting, users will not be able to insta If you enable, or do not configuring this policy, users will be able to execute the Windows Package Manager CLI commands, and PowerShell cmdlets. (Provided “Enable App Installer” policy is not disabled). This policy does not override the “Enable App Installer” policy.</string> + <string id="EnableWindowsPackageManagerConfiguration">Enable Windows Package Manager Configuration</string> + <string id="EnableWindowsPackageManagerConfigurationExplanation">This policy controls whether the Windows Package Manager configuration feature can be used by users. + +If you enable or do not configure this setting, users will be able to use the Windows Package Manager configuration feature. + +If you disable this setting, users will not be able to use the Windows Package Manager configuration feature.</string> </stringTable> <presentationTable> <presentation id="SourceAutoUpdateInterval"> diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -14,7 +14,7 @@ using namespace AppInstaller::CLI::Workflow; namespace AppInstaller::CLI { ConfigureCommand::ConfigureCommand(std::string_view parent) : - Command("configure", { "configuration" }, parent, Settings::ExperimentalFeature::Feature::Configuration) + Command("configure", { "configuration" }, parent, Settings::TogglePolicy::Policy::Configuration) { SelectCurrentCommandIfUnrecognizedSubcommandFound(true); } diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -23,7 +23,6 @@ namespace AppInstallerCLIE2ETests [OneTimeSetUp] public void OneTimeSetup() { - WinGetSettingsHelper.ConfigureFeature("configuration", true); this.DeleteTxtFiles(); } diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -15,15 +15,6 @@ namespace AppInstallerCLIE2ETests public class ConfigureShowCommand { /// <summary> - /// Setup done once before all the tests here. - /// </summary> - [OneTimeSetUp] - public void OneTimeSetup() - { - WinGetSettingsHelper.ConfigureFeature("configuration", true); - } - - /// <summary> /// Simple test to confirm that a resource without a module specified can be discovered in the PSGallery. /// </summary> [Test] diff --git a/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs @@ -24,7 +24,6 @@ namespace AppInstallerCLIE2ETests [OneTimeSetUp] public void OneTimeSetup() { - WinGetSettingsHelper.ConfigureFeature("configuration", true); this.DeleteTxtFiles(); } diff --git a/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureValidateCommand.cs @@ -17,15 +17,6 @@ namespace AppInstallerCLIE2ETests private const string Command = "configure validate"; /// <summary> - /// Setup done once before all the tests here. - /// </summary> - [OneTimeSetUp] - public void OneTimeSetup() - { - WinGetSettingsHelper.ConfigureFeature("configuration", true); - } - - /// <summary> /// The configuration file is empty. /// </summary> [Test] diff --git a/src/AppInstallerCLIE2ETests/GroupPolicy.cs b/src/AppInstallerCLIE2ETests/GroupPolicy.cs @@ -223,5 +223,19 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.IsTrue(result.StdOut.Contains("Source Auto Update Interval In Minutes 123")); } + + /// <summary> + /// Test configuration is disabled by policy. + /// </summary> + [Test] + public void EnableConfiguration() + { + GroupPolicyHelper.EnableConfiguration.Disable(); + var result = TestCommon.RunAICLICommand("configure", TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo.yml")); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); + + result = TestCommon.RunAICLICommand("configure show", TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo.yml")); + Assert.AreEqual(Constants.ErrorCode.ERROR_BLOCKED_BY_POLICY, result.ExitCode); + } } } diff --git a/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs b/src/AppInstallerCLIE2ETests/GroupPolicyHelper.cs @@ -114,6 +114,11 @@ namespace AppInstallerCLIE2ETests public static GroupPolicyHelper EnableAllowedSources { get; private set; } = new GroupPolicyHelper("EnableAllowedSources", "AllowedSources"); /// <summary> + /// Gets the Enable Windows Package Manager Configuration Interfaces policy. + /// </summary> + public static GroupPolicyHelper EnableConfiguration { get; private set; } = new GroupPolicyHelper("EnableWindowsPackageManagerConfiguration"); + + /// <summary> /// Gets the Enable auto update interval policy. /// </summary> public static GroupPolicyHelper SourceAutoUpdateInterval { get; private set; } = new GroupPolicyHelper("SourceAutoUpdateInterval", "SourceAutoUpdateInterval"); @@ -132,6 +137,7 @@ namespace AppInstallerCLIE2ETests EnableAllowedSources, SourceAutoUpdateInterval, EnableWinGetCommandLineInterfaces, + EnableConfiguration, }; /// <summary> diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -207,7 +207,6 @@ namespace AppInstallerCLIE2ETests.Helpers ConfigureFeature("experimentalCmd", status); ConfigureFeature("directMSI", status); ConfigureFeature("pinning", status); - ConfigureFeature("configuration", status); } private static JObject GetJsonSettingsObject(string objectName) diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2077,4 +2077,7 @@ Please specify one of them using the --source option to proceed.</value> <value>`--module-path` value must be `currentuser`, `allusers`, `default` or an absolute path.</value> <comment>{Locked="{--module-path}, {currentuser}, {allusers}, {default}}</comment> </data> + <data name="PolicyEnableWinGetConfiguration" xml:space="preserve"> + <value>Enable Windows Package Manager Configuration</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/Certificates.cpp b/src/AppInstallerCLITests/Certificates.cpp @@ -13,10 +13,10 @@ using namespace AppInstaller::Certificates; TEST_CASE("Certificates_NoPinningSucceeds", "[certificates]") { PinningDetails expected; - expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::None); + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::None); PinningDetails actual; - actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(expected.Validate(actual.GetCertificate())); } @@ -24,10 +24,10 @@ TEST_CASE("Certificates_NoPinningSucceeds", "[certificates]") TEST_CASE("Certificates_PublicKeyMismatch", "[certificates]") { PinningDetails expected; - expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); PinningDetails actual; - actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(!expected.Validate(actual.GetCertificate())); } @@ -35,10 +35,10 @@ TEST_CASE("Certificates_PublicKeyMismatch", "[certificates]") TEST_CASE("Certificates_PublicKeyMatch", "[certificates]") { PinningDetails expected; - expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); PinningDetails actual; - actual.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2); + actual.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(expected.Validate(actual.GetCertificate())); } @@ -46,10 +46,10 @@ TEST_CASE("Certificates_PublicKeyMatch", "[certificates]") TEST_CASE("Certificates_SubjectMismatch", "[certificates]") { PinningDetails expected; - expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::Subject); + expected.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject); PinningDetails actual; - actual.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2); + actual.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(!expected.Validate(actual.GetCertificate())); } @@ -57,10 +57,10 @@ TEST_CASE("Certificates_SubjectMismatch", "[certificates]") TEST_CASE("Certificates_SubjectMatch", "[certificates]") { PinningDetails expected; - expected.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Subject); + expected.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject); PinningDetails actual; - actual.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2); + actual.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(expected.Validate(actual.GetCertificate())); } @@ -68,10 +68,10 @@ TEST_CASE("Certificates_SubjectMatch", "[certificates]") TEST_CASE("Certificates_IssuerMismatch", "[certificates]") { PinningDetails expected; - expected.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Issuer); + expected.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Issuer); PinningDetails actual; - actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(!expected.Validate(actual.GetCertificate())); } @@ -79,10 +79,10 @@ TEST_CASE("Certificates_IssuerMismatch", "[certificates]") TEST_CASE("Certificates_IssuerMatch", "[certificates]") { PinningDetails expected; - expected.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Issuer); + expected.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Issuer); PinningDetails actual; - actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + actual.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(expected.Validate(actual.GetCertificate())); } @@ -91,15 +91,15 @@ TEST_CASE("Certificates_ChainLengthDiffers", "[certificates]") { PinningChain chain; auto chainElement = chain.Root(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); PinningConfiguration config; config.AddChain(chain); PinningDetails details; - details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(!config.Validate(details.GetCertificate())); } @@ -112,7 +112,7 @@ TEST_CASE("Certificates_EmptyChainRejects", "[certificates]") config.AddChain(chain); PinningDetails details; - details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(!config.Validate(details.GetCertificate())); } @@ -121,17 +121,17 @@ TEST_CASE("Certificates_ChainOrderDiffers", "[certificates]") { PinningChain chain; auto chainElement = chain.Root(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); PinningConfiguration config; config.AddChain(chain); PinningDetails details; - details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(!config.Validate(details.GetCertificate())); } @@ -140,17 +140,17 @@ TEST_CASE("Certificates_StoreChain_BuiltInTest", "[certificates]") { PinningChain chain; auto chainElement = chain.Root(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); PinningConfiguration config; config.AddChain(chain); PinningDetails details; - details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(config.Validate(details.GetCertificate())); } @@ -159,27 +159,27 @@ TEST_CASE("Certificates_MultipleChains_Success", "[certificates]") { PinningChain chainOutOfOrder; auto chainElement = chainOutOfOrder.Root(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); PinningConfiguration config; config.AddChain(chainOutOfOrder); PinningChain chain; chainElement = chain.Root(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); config.AddChain(chain); PinningDetails details; - details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + details.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); REQUIRE(config.Validate(details.GetCertificate())); } diff --git a/src/AppInstallerCLITests/GroupPolicy.cpp b/src/AppInstallerCLITests/GroupPolicy.cpp @@ -280,11 +280,11 @@ TEST_CASE("GroupPolicy_Sources", "[groupPolicy]") auto additionalSourcesKey = RegCreateVolatileSubKey(policiesKey.get(), AdditionalSourcesPolicyKeyName); PinningDetails rootCert; - rootCert.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2); + rootCert.LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE); PinningDetails intermediateCert; - intermediateCert.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2); + intermediateCert.LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE); PinningDetails leafCert; - leafCert.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2); + leafCert.LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE); auto getBytesString = [](const PinningDetails& details) { @@ -375,6 +375,7 @@ TEST_CASE("GroupPolicy_AllEnabled", "[groupPolicy]") SetRegistryValue(policiesKey.get(), AllowedSourcesPolicyValueName, 1); SetRegistryValue(policiesKey.get(), BypassCertificatePinningForMicrosoftStoreValueName, 1); SetRegistryValue(policiesKey.get(), EnableWindowsPackageManagerCommandLineInterfaces, 1); + SetRegistryValue(policiesKey.get(), ConfigurationPolicyValueName, 1); GroupPolicy groupPolicy{ policiesKey.get() }; for (const auto& policy : TogglePolicy::GetAllPolicies()) diff --git a/src/AppInstallerCLITests/HttpClientHelper.cpp b/src/AppInstallerCLITests/HttpClientHelper.cpp @@ -65,11 +65,11 @@ TEST_CASE("HttpClientHelper_PinningConfiguration", "[RestSource]") // Create the Store chain config PinningChain chain; auto chainElement = chain.Root(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); PinningConfiguration config; config.AddChain(chain); diff --git a/src/AppInstallerCLITests/TestSettings.h b/src/AppInstallerCLITests/TestSettings.h @@ -21,6 +21,7 @@ namespace TestCommon const std::wstring AllowedSourcesPolicyValueName = L"EnableAllowedSources"; const std::wstring BypassCertificatePinningForMicrosoftStoreValueName = L"EnableBypassCertificatePinningForMicrosoftStore"; const std::wstring EnableWindowsPackageManagerCommandLineInterfaces = L"EnableWindowsPackageManagerCommandLineInterfaces"; + const std::wstring ConfigurationPolicyValueName = L"EnableWindowsPackageManagerConfiguration"; const std::wstring SourceUpdateIntervalPolicyValueName = L"SourceAutoUpdateInterval"; const std::wstring SourceUpdateIntervalPolicyOldValueName = L"SourceAutoUpdateIntervalInMinutes"; diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -390,13 +390,11 @@ </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" /> <ClInclude Include="Public\winget\Debugging.h" /> <ClInclude Include="Public\winget\DependenciesGraph.h" /> - <ClInclude Include="Public\winget\GroupPolicy.h" /> <ClInclude Include="HttpStream\HttpClientWrapper.h" /> <ClInclude Include="HttpStream\HttpLocalCache.h" /> <ClInclude Include="HttpStream\HttpRandomAccessStream.h" /> @@ -413,7 +411,6 @@ <ClInclude Include="Public\winget\Archive.h" /> <ClInclude Include="Public\winget\ExperimentalFeature.h" /> <ClInclude Include="Public\winget\ExtensionCatalog.h" /> - <ClInclude Include="Public\winget\JsonUtil.h" /> <ClInclude Include="Public\winget\Locale.h" /> <ClInclude Include="Public\winget\ManagedFile.h" /> <ClInclude Include="Public\winget\Manifest.h" /> @@ -432,7 +429,6 @@ <ClInclude Include="Public\winget\PackageDependenciesValidationUtil.h" /> <ClInclude Include="Public\winget\Pin.h" /> <ClInclude Include="Public\winget\Regex.h" /> - <ClInclude Include="Public\winget\Registry.h" /> <ClInclude Include="Public\winget\PathVariable.h" /> <ClInclude Include="Public\winget\PortableARPEntry.h" /> <ClInclude Include="Public\winget\PortableFileEntry.h" /> @@ -447,15 +443,11 @@ </ItemGroup> <ItemGroup> <ClCompile Include="AdminSettings.cpp" /> - <ClCompile Include="Certificates.cpp" /> <ClCompile Include="Debugging.cpp" /> <ClCompile Include="DependenciesGraph.cpp" /> <ClCompile Include="DODownloader.cpp" /> <ClCompile Include="Filesystem.cpp" /> <ClCompile Include="FolderFileWatcher.cpp" /> - <ClCompile Include="GroupPolicy.cpp"> - <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> - </ClCompile> <ClCompile Include="Deployment.cpp"> <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> </ClCompile> @@ -474,7 +466,6 @@ <ClCompile Include="HttpStream\HttpRandomAccessStream.cpp"> <ExcludedFromBuild Condition="'$(Configuration)'=='Fuzzing'">true</ExcludedFromBuild> </ClCompile> - <ClCompile Include="JsonUtil.cpp" /> <ClCompile Include="Locale.cpp" /> <ClCompile Include="ManagedFile.cpp" /> <ClCompile Include="Manifest\Manifest.cpp" /> @@ -496,7 +487,6 @@ <ClCompile Include="Pin.cpp" /> <ClCompile Include="Progress.cpp" /> <ClCompile Include="Regex.cpp" /> - <ClCompile Include="Registry.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 @@ -99,9 +99,6 @@ <ClInclude Include="Public\winget\ManifestYamlWriter.h"> <Filter>Public\winget</Filter> </ClInclude> - <ClInclude Include="Public\winget\Registry.h"> - <Filter>Public\winget</Filter> - </ClInclude> <ClInclude Include="Public\winget\NameNormalization.h"> <Filter>Public\winget</Filter> </ClInclude> @@ -120,9 +117,6 @@ <ClInclude Include="Public\winget\ManifestCommon.h"> <Filter>Public\winget</Filter> </ClInclude> - <ClInclude Include="Public\winget\GroupPolicy.h"> - <Filter>Public\winget</Filter> - </ClInclude> <ClInclude Include="Public\winget\Locale.h"> <Filter>Public\winget</Filter> </ClInclude> @@ -156,18 +150,12 @@ <ClInclude Include="Public\winget\PortableFileEntry.h"> <Filter>Public\winget</Filter> </ClInclude> - <ClInclude Include="Public\winget\JsonUtil.h"> - <Filter>Public\winget</Filter> - </ClInclude> <ClInclude Include="Public\winget\ManagedFile.h"> <Filter>Public\winget</Filter> </ClInclude> <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> @@ -239,9 +227,6 @@ <ClCompile Include="ExtensionCatalog.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="JsonUtil.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="Settings.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -260,9 +245,6 @@ <ClCompile Include="Manifest\YamlWriter.cpp"> <Filter>Manifest</Filter> </ClCompile> - <ClCompile Include="Registry.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="NameNormalization.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -281,9 +263,6 @@ <ClCompile Include="Manifest\Manifest.cpp"> <Filter>Manifest</Filter> </ClCompile> - <ClCompile Include="GroupPolicy.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="Locale.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -320,9 +299,6 @@ <ClCompile Include="MsixManifest.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="Certificates.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="FolderFileWatcher.cpp"> <Filter>Source Files</Filter> </ClCompile> diff --git a/src/AppInstallerCommonCore/Certificates.cpp b/src/AppInstallerCommonCore/Certificates.cpp @@ -1,549 +0,0 @@ -// 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/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -40,8 +40,6 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFExperimentalArg>(); case ExperimentalFeature::Feature::DirectMSI: return userSettings.Get<Setting::EFDirectMSI>(); - case ExperimentalFeature::Feature::Configuration: - return userSettings.Get<Setting::EFConfiguration>(); default: THROW_HR(E_UNEXPECTED); } @@ -71,8 +69,6 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Argument Sample", "experimentalArg", "https://aka.ms/winget-settings", Feature::ExperimentalArg }; case Feature::DirectMSI: return ExperimentalFeature{ "Direct MSI Installation", "directMSI", "https://aka.ms/winget-settings", Feature::DirectMSI }; - case Feature::Configuration: - return ExperimentalFeature{ "Configuration", "configuration", "https://aka.ms/winget-settings#configuration", Feature::Configuration }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/GroupPolicy.cpp b/src/AppInstallerCommonCore/GroupPolicy.cpp @@ -1,379 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "winget/GroupPolicy.h" -#include "AppInstallerLogging.h" - -using namespace AppInstaller::StringResource; - -namespace AppInstaller::Settings -{ - namespace - { - const GroupPolicy& InstanceInternal(std::optional<GroupPolicy*> overridePolicy = {}) - { - const static GroupPolicy s_groupPolicy{ Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, "Software\\Policies\\Microsoft\\Windows\\AppInstaller") }; - static GroupPolicy* s_override = nullptr; - - if (overridePolicy.has_value()) - { - s_override = overridePolicy.value(); - } - - return (s_override ? *s_override : s_groupPolicy); - } - - std::optional<Registry::Value> GetRegistryValueObject(const Registry::Key& key, const std::string_view valueName) - { - if (!key) - { - // Key does not exist; there's nothing to return - return std::nullopt; - } - - return key[valueName]; - } - - template<Registry::Value::Type T> - std::optional<decltype(std::declval<Registry::Value>().GetValue<T>())> GetRegistryValueData(const Registry::Value& regValue, const std::string_view valueName) - { - auto value = regValue.TryGetValue<T>(); - if (!value.has_value()) - { - AICLI_LOG(Core, Warning, << "Value for policy '" << valueName << "' does not have expected type"); - return std::nullopt; - } - - return std::move(value.value()); - } - - template<Registry::Value::Type T> - std::optional<decltype(std::declval<Registry::Value>().GetValue<T>())> GetRegistryValueData(const Registry::Key& key, const std::string_view valueName) - { - auto regValue = GetRegistryValueObject(key, valueName); - if (!regValue.has_value()) - { - // Value does not exist; there's nothing to return - return std::nullopt; - } - - return GetRegistryValueData<T>(regValue.value(), valueName); - } - - std::optional<bool> RegistryValueIsTrue(const Registry::Key& key, std::string_view valueName) - { - auto intValue = GetRegistryValueData<Registry::Value::Type::DWord>(key, valueName); - if (!intValue.has_value()) - { - return std::nullopt; - } - - AICLI_LOG(Core, Verbose, << "Found policy '" << valueName << "', Value: " << *intValue); - return (bool)*intValue; - } - - PolicyState GetStateInternal(const Registry::Key& key, TogglePolicy::Policy policy) - { - // Default to not configured if there is no policy for this - if (policy == TogglePolicy::Policy::None) - { - return PolicyState::NotConfigured; - } - - auto togglePolicy = TogglePolicy::GetPolicy(policy); - - // Policies are not configured if there is no registry value. - auto setting = RegistryValueIsTrue(key, togglePolicy.RegValueName()); - if (!setting.has_value()) - { - return PolicyState::NotConfigured; - } - - // Return flag as-is or invert depending on the policy - return *setting ? PolicyState::Enabled : PolicyState::Disabled; - } - - template <ValuePolicy P> - void Validate(const Registry::Key& policiesKey, GroupPolicy::ValuePoliciesMap& policies) - { - auto value = details::ValuePolicyMapping<P>::ReadAndValidate(policiesKey); - if (value.has_value()) - { - policies.Add<P>(std::move(*value)); - } - } - - template <> - void Validate<ValuePolicy::None>(const Registry::Key&, GroupPolicy::ValuePoliciesMap&) {}; - - template <size_t... P> - void ValidateAllValuePolicies( - const Registry::Key& policiesKey, - GroupPolicy::ValuePoliciesMap& policies, - std::index_sequence<P...>) - { - // Use folding to call each policy validate function. - (FoldHelper{}, ..., Validate<static_cast<ValuePolicy>(P)>(policiesKey, policies)); - } - - // Reads a list from a Group Policy. - // The list is stored in a sub-key of the policies key, and each value in that key is a list item. - // Cases not considered by this function because we don't use them: - // - When the list is in an arbitrary key, not a sub key. - // - When the list values are mixed with other values and are identified by a prefix in their names. - // - When the value names are relevant. - template<ValuePolicy P> - std::optional<typename details::ValuePolicyMapping<P>::value_t> ReadList(const Registry::Key& policiesKey) - { - using Mapping = details::ValuePolicyMapping<P>; - - auto listKey = policiesKey.SubKey(Mapping::KeyName); - if (!listKey.has_value()) - { - return std::nullopt; - } - - typename Mapping::value_t items; - for (const auto& value : listKey->Values()) - { - auto item = Mapping::ReadAndValidateItem(value); - if (item.has_value()) - { - items.emplace_back(std::move(item.value())); - } - else - { - AICLI_LOG(Core, Warning, << "Failed to read Group Policy list value. Policy [" << Mapping::KeyName << "], Value [" << value.Name() << ']'); - } - } - - return items; - } - - std::optional<SourceFromPolicy> ReadSourceFromRegistryValue(const Registry::Value& item) - { - auto jsonString = item.TryGetValue<Registry::Value::Type::String>(); - if (!jsonString.has_value()) - { - AICLI_LOG(Core, Warning, << "Registry value is not a string"); - return std::nullopt; - } - - int stringLength = static_cast<int>(jsonString->length()); - Json::Value sourceJson; - Json::CharReaderBuilder charReaderBuilder; - const std::unique_ptr<Json::CharReader> jsonReader(charReaderBuilder.newCharReader()); - Json::String jsonErrors; - if (!jsonReader->parse(jsonString->c_str(), jsonString->c_str() + stringLength, &sourceJson, &jsonErrors)) - { - AICLI_LOG(Core, Warning, << "Registry value does not contain a valid JSON: " << jsonErrors); - return std::nullopt; - } - - SourceFromPolicy source; - - auto readSourceAttribute = [&](const std::string& name, std::string SourceFromPolicy::* member) - { - if (sourceJson.isMember(name) && sourceJson[name].isString()) - { - source.*member = sourceJson[name].asString(); - return true; - } - else - { - AICLI_LOG(Core, Warning, << "Source JSON does not contain a string value for " << name); - return false; - } - }; - - bool allRead = readSourceAttribute("Name", &SourceFromPolicy::Name) - && readSourceAttribute("Arg", &SourceFromPolicy::Arg) - && readSourceAttribute("Type", &SourceFromPolicy::Type) - && readSourceAttribute("Data", &SourceFromPolicy::Data) - && readSourceAttribute("Identifier", &SourceFromPolicy::Identifier); - if (!allRead) - { - 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; - } - } - - namespace details - { -#define POLICY_MAPPING_DEFAULT_LIST_READ(_policy_) \ - std::optional<typename ValuePolicyMapping<_policy_>::value_t> ValuePolicyMapping<_policy_>::ReadAndValidate(const Registry::Key& policiesKey) \ - { \ - return ReadList<_policy_>(policiesKey); \ - } - - POLICY_MAPPING_DEFAULT_LIST_READ(ValuePolicy::AdditionalSources); - POLICY_MAPPING_DEFAULT_LIST_READ(ValuePolicy::AllowedSources); - - std::nullopt_t ValuePolicyMapping<ValuePolicy::None>::ReadAndValidate(const Registry::Key&) - { - return std::nullopt; - } - - std::optional<uint32_t> ValuePolicyMapping<ValuePolicy::SourceAutoUpdateIntervalInMinutes>::ReadAndValidate(const Registry::Key& policiesKey) - { - // This policy used to have another name in the registry. - // Try to read first with the current name, and if it's not present - // check if the old name is present. - using Mapping = ValuePolicyMapping<ValuePolicy::SourceAutoUpdateIntervalInMinutes>; - - auto regValueWithCurrentName = GetRegistryValueObject(policiesKey, Mapping::ValueName); - if (regValueWithCurrentName.has_value()) - { - // We use the current name even if it doesn't have valid data. - return GetRegistryValueData<Mapping::ValueType>(regValueWithCurrentName.value(), Mapping::ValueName); - } - else - { - return GetRegistryValueData<Mapping::ValueType>(policiesKey, "SourceAutoUpdateIntervalInMinutes"sv); - } - } - - std::optional<SourceFromPolicy> ValuePolicyMapping<ValuePolicy::AdditionalSources>::ReadAndValidateItem(const Registry::Value& item) - { - return ReadSourceFromRegistryValue(item); - } - - std::optional<SourceFromPolicy> ValuePolicyMapping<ValuePolicy::AllowedSources>::ReadAndValidateItem(const Registry::Value& item) - { - return ReadSourceFromRegistryValue(item); - } - } - - TogglePolicy TogglePolicy::GetPolicy(TogglePolicy::Policy policy) - { - switch (policy) - { - case TogglePolicy::Policy::WinGet: - return TogglePolicy(policy, "EnableAppInstaller"sv, String::PolicyEnableWinGet); - case TogglePolicy::Policy::Settings: - return TogglePolicy(policy, "EnableSettings"sv, String::PolicyEnableWingetSettings); - case TogglePolicy::Policy::ExperimentalFeatures: - return TogglePolicy(policy, "EnableExperimentalFeatures"sv, String::PolicyEnableExperimentalFeatures); - case TogglePolicy::Policy::LocalManifestFiles: - return TogglePolicy(policy, "EnableLocalManifestFiles"sv, String::PolicyEnableLocalManifests); - case TogglePolicy::Policy::HashOverride: - return TogglePolicy(policy, "EnableHashOverride"sv, String::PolicyEnableHashOverride); - case TogglePolicy::Policy::LocalArchiveMalwareScanOverride: - return TogglePolicy(policy, "EnableLocalArchiveMalwareScanOverride"sv, String::PolicyEnableLocalArchiveMalwareScanOverride); - case TogglePolicy::Policy::DefaultSource: - return TogglePolicy(policy, "EnableDefaultSource"sv, String::PolicyEnableDefaultSource); - case TogglePolicy::Policy::MSStoreSource: - return TogglePolicy(policy, "EnableMicrosoftStoreSource"sv, String::PolicyEnableMSStoreSource); - case TogglePolicy::Policy::AdditionalSources: - return TogglePolicy(policy, "EnableAdditionalSources"sv, String::PolicyAdditionalSources); - case TogglePolicy::Policy::AllowedSources: - return TogglePolicy(policy, "EnableAllowedSources"sv, String::PolicyAllowedSources); - case TogglePolicy::Policy::BypassCertificatePinningForMicrosoftStore: - return TogglePolicy(policy, "EnableBypassCertificatePinningForMicrosoftStore"sv, String::PolicyEnableBypassCertificatePinningForMicrosoftStore); - case TogglePolicy::Policy::WinGetCommandLineInterfaces: - return TogglePolicy(policy, "EnableWindowsPackageManagerCommandLineInterfaces"sv, String::PolicyEnableWindowsPackageManagerCommandLineInterfaces); - default: - THROW_HR(E_UNEXPECTED); - } - } - - std::vector<TogglePolicy> TogglePolicy::GetAllPolicies() - { - using Toggle_t = std::underlying_type_t<TogglePolicy::Policy>; - - std::vector<TogglePolicy> result; - - // Skip "None" - for (Toggle_t i = 1 + static_cast<Toggle_t>(TogglePolicy::Policy::None); i < static_cast<Toggle_t>(TogglePolicy::Policy::Max); ++i) - { - result.emplace_back(GetPolicy(static_cast<Policy>(i))); - } - - return result; - } - - std::string SourceFromPolicy::ToJsonString() const - { - Json::Value json{ Json::ValueType::objectValue }; - json["Name"] = Name; - json["Type"] = Type; - json["Arg"] = Arg; - json["Data"] = Data; - json["Identifier"] = Identifier; - - Json::StreamWriterBuilder writerBuilder; - writerBuilder.settings_["indentation"] = ""; - return Json::writeString(writerBuilder, json); - } - - GroupPolicy::GroupPolicy(const Registry::Key& key) - { - ValidateAllValuePolicies(key, m_values, std::make_index_sequence<static_cast<size_t>(ValuePolicy::Max)>()); - - using Toggle_t = std::underlying_type_t<TogglePolicy::Policy>; - for (Toggle_t i = static_cast<Toggle_t>(TogglePolicy::Policy::None); i < static_cast<Toggle_t>(TogglePolicy::Policy::Max); ++i) - { - auto policy = static_cast<TogglePolicy::Policy>(i); - m_toggles[policy] = GetStateInternal(key, policy); - } - } - - PolicyState GroupPolicy::GetState(TogglePolicy::Policy policy) const - { - auto itr = m_toggles.find(policy); - if (itr == m_toggles.end()) - { - return PolicyState::NotConfigured; - } - - return itr->second; - } - - bool GroupPolicy::IsEnabled(TogglePolicy::Policy policy) const - { - if (policy == TogglePolicy::Policy::None) - { - return true; - } - - PolicyState state = GetState(policy); - if (state == PolicyState::NotConfigured) - { - return TogglePolicy::GetPolicy(policy).DefaultIsEnabled(); - } - - return state == PolicyState::Enabled; - } - - GroupPolicy const& GroupPolicy::Instance() - { - return InstanceInternal(); - } - -#ifndef AICLI_DISABLE_TEST_HOOKS - void GroupPolicy::OverrideInstance(GroupPolicy* overridePolicy) - { - InstanceInternal(overridePolicy); - } - - void GroupPolicy::ResetInstance() - { - InstanceInternal(nullptr); - } -#endif -}- \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/Certificates.h b/src/AppInstallerCommonCore/Public/winget/Certificates.h @@ -1,153 +0,0 @@ -// 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/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -23,7 +23,6 @@ namespace AppInstaller::Settings None = 0x0, // Before making DirectMSI non-experimental, it should be part of manifest validation. DirectMSI = 0x1, - Configuration = 0x2, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/GroupPolicy.h b/src/AppInstallerCommonCore/Public/winget/GroupPolicy.h @@ -1,245 +0,0 @@ -// 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 <string_view> - -using namespace std::string_view_literals; - -namespace AppInstaller::Settings -{ - - // A policy that sets a value for some setting. - // The value of the policy is a value in the registry key, or is - // made up of sub-keys for settings that are lists. - enum class ValuePolicy - { - None, - SourceAutoUpdateIntervalInMinutes, - AdditionalSources, - AllowedSources, - Max, - }; - - // A policy that acts as a toggle to enable or disable a feature. - // They are backed by a DWORD value with values 0 and 1. - struct TogglePolicy - { - enum class Policy - { - None = 0, - WinGet, - Settings, - ExperimentalFeatures, - LocalManifestFiles, - HashOverride, - LocalArchiveMalwareScanOverride, - DefaultSource, - MSStoreSource, - AdditionalSources, - AllowedSources, - BypassCertificatePinningForMicrosoftStore, - WinGetCommandLineInterfaces, - Max, - }; - - TogglePolicy(Policy policy, std::string_view regValueName, StringResource::StringId policyName, bool defaultIsEnabled = true) : - m_policy(policy), m_regValueName(regValueName), m_policyName(policyName), m_defaultIsEnabled(defaultIsEnabled) {} - - static TogglePolicy GetPolicy(Policy policy); - static std::vector<TogglePolicy> GetAllPolicies(); - - Policy GetPolicy() const { return m_policy; } - std::string_view RegValueName() const { return m_regValueName; } - StringResource::StringId PolicyName() const { return m_policyName; } - bool DefaultIsEnabled() const { return m_defaultIsEnabled; } - - private: - Policy m_policy; - std::string_view m_regValueName; - StringResource::StringId m_policyName; - bool m_defaultIsEnabled; - }; - - // Possible configuration states for a policy. - enum class PolicyState - { - NotConfigured, - Disabled, - Enabled, - }; - - // A source defined by Group Policy to be added or allowed - struct SourceFromPolicy - { - std::string Name; - std::string Arg; - std::string Type; - std::string Data; - std::string Identifier; - -#ifndef AICLI_DISABLE_TEST_HOOKS - Certificates::PinningConfiguration PinningConfiguration; -#endif - - std::string ToJsonString() const; - }; - - - namespace details - { - - template <ValuePolicy P> - struct ValuePolicyMapping - { - // value_t - type of the policy - // ReadAndValidate() - Function that reads the value and does semantic validation. - - // For simple values: - // ValueName - Name of the registry value - // ValueType - Type of the registry value - // reg_value_t - Type returned by the registry when reading the value - - // For lists: - // item_t - Type of each item - // KeyName -- Name of the sub-key containing the list - // ReadAndValidateItem() - Function that reads a single item from a subkey - }; - - template<> - struct ValuePolicyMapping<ValuePolicy::None> - { - using value_t = std::monostate; - using opt_value_t = std::nullopt_t; - using opt_ref_value_t = std::nullopt_t; - static std::nullopt_t ReadAndValidate(const Registry::Key& policiesKey); - }; - -#define POLICY_MAPPING_SPECIALIZATION(_policy_, _type_, _extra_) \ - template <> \ - struct ValuePolicyMapping<_policy_> \ - { \ - using value_t = _type_; \ - using opt_value_t = std::optional<value_t>; \ - using opt_ref_value_t = std::optional<std::reference_wrapper<const value_t>>; \ - static std::optional<value_t> ReadAndValidate(const Registry::Key& policiesKey); \ - _extra_ \ - } - -#define POLICY_MAPPING_VALUE_SPECIALIZATION(_policy_, _type_, _valueName_, _valueType_) \ - POLICY_MAPPING_SPECIALIZATION(_policy_, _type_, \ - static constexpr std::string_view ValueName = _valueName_; \ - static constexpr Registry::Value::Type ValueType = _valueType_; \ - using reg_value_t = decltype(std::declval<Registry::Value>().GetValue<ValueType>()); \ - ) - -#define POLICY_MAPPING_LIST_SPECIALIZATION(_policy_, _type_, _keyName_) \ - POLICY_MAPPING_SPECIALIZATION(_policy_, std::vector<_type_>, \ - static constexpr std::string_view KeyName = _keyName_; \ - using item_t = _type_; \ - static std::optional<item_t> ReadAndValidateItem(const Registry::Value& item); \ - ) - - POLICY_MAPPING_VALUE_SPECIALIZATION(ValuePolicy::SourceAutoUpdateIntervalInMinutes, uint32_t, "SourceAutoUpdateInterval"sv, Registry::Value::Type::DWord); - - POLICY_MAPPING_LIST_SPECIALIZATION(ValuePolicy::AdditionalSources, SourceFromPolicy, "AdditionalSources"sv); - POLICY_MAPPING_LIST_SPECIALIZATION(ValuePolicy::AllowedSources, SourceFromPolicy, "AllowedSources"sv); - } - - // Representation of the policies read from the registry. - struct GroupPolicy - { - using ValuePoliciesMap = EnumBasedVariantMap<ValuePolicy, details::ValuePolicyMapping>; - - static GroupPolicy const& Instance(); - - GroupPolicy(const Registry::Key& key); - ~GroupPolicy() = default; - - GroupPolicy() = delete; - - GroupPolicy(const GroupPolicy&) = delete; - GroupPolicy& operator=(const GroupPolicy&) = delete; - - GroupPolicy(GroupPolicy&&) = delete; - GroupPolicy& operator=(GroupPolicy&&) = delete; - - template<ValuePolicy P> - using ValueType = typename details::ValuePolicyMapping<P>::value_t; - - // Gets the policy value if it is present - template<ValuePolicy P> - typename details::ValuePolicyMapping<P>::opt_value_t GetValue() const - { - if (m_values.Contains(P)) - { - return m_values.Get<P>(); - } - else - { - return std::nullopt; - } - } - - template<ValuePolicy P> - typename details::ValuePolicyMapping<P>::opt_ref_value_t GetValueRef() const - { - if (m_values.Contains(P)) - { - return std::cref(m_values.Get<P>()); - } - else - { - return std::nullopt; - } - } - - template<> - std::nullopt_t GetValue<ValuePolicy::None>() const - { - return std::nullopt; - } - - template<> - std::nullopt_t GetValueRef<ValuePolicy::None>() const - { - return std::nullopt; - } - - PolicyState GetState(TogglePolicy::Policy policy) const; - - // Checks whether a policy is enabled, using an appropriate default when not configured. - // Should not be used when not configured means something different than enabled/disabled. - bool IsEnabled(TogglePolicy::Policy policy) const; - -#ifndef AICLI_DISABLE_TEST_HOOKS - protected: - static void OverrideInstance(GroupPolicy* gp); - static void ResetInstance(); -#else - private: -#endif - std::map<TogglePolicy::Policy, PolicyState> m_toggles; - ValuePoliciesMap m_values; - }; - - inline const GroupPolicy& GroupPolicies() - { - return GroupPolicy::Instance(); - } - - struct GroupPolicyException - { - GroupPolicyException(TogglePolicy::Policy policy) : m_policy(policy) {} - - const TogglePolicy::Policy& Policy() const { return m_policy; } - - private: - TogglePolicy::Policy m_policy; - }; - -} diff --git a/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h b/src/AppInstallerCommonCore/Public/winget/PortableARPEntry.h @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include "Registry.h" +#include <winget/Registry.h> #include "Manifest.h" namespace AppInstaller::Registry::Portable diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -70,7 +70,6 @@ namespace AppInstaller::Settings EFExperimentalCmd, EFExperimentalArg, EFDirectMSI, - EFConfiguration, // Telemetry TelemetryDisable, // Install behavior @@ -146,7 +145,6 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration, bool, bool, false, ".experimentalFeatures.configuration"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -259,7 +259,6 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFExperimentalCmd) WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) - WINGET_VALIDATE_PASS_THROUGH(EFConfiguration) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerRepositoryCore/SourceList.cpp b/src/AppInstallerRepositoryCore/SourceList.cpp @@ -312,19 +312,19 @@ namespace AppInstaller::Repository PinningChain chain; auto chainElement = chain.Root(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1).SetPinning(PinningVerificationType::PublicKey); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_1, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_1, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); chainElement = chainElement.Next(); - chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_1, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); PinningChain chain2; auto chainElement2 = chain2.Root(); - chainElement2->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2).SetPinning(PinningVerificationType::PublicKey); + chainElement2->LoadCertificate(IDX_CERTIFICATE_STORE_ROOT_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::PublicKey); chainElement2 = chainElement2.Next(); - chainElement2->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement2->LoadCertificate(IDX_CERTIFICATE_STORE_INTERMEDIATE_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); chainElement2 = chainElement2.Next(); - chainElement2->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); + chainElement2->LoadCertificate(IDX_CERTIFICATE_STORE_LEAF_2, CERTIFICATE_RESOURCE_TYPE).SetPinning(PinningVerificationType::Subject | PinningVerificationType::Issuer); details.CertificatePinningConfiguration = PinningConfiguration("Microsoft Store Source"); details.CertificatePinningConfiguration.AddChain(std::move(chain)); diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj @@ -399,10 +399,14 @@ <ClInclude Include="Public\Telemetry\MicrosoftTelemetry.h" /> <ClInclude Include="Public\Telemetry\WinEventLogLevels.h" /> <ClInclude Include="Public\winget\AsyncTokens.h" /> + <ClInclude Include="Public\winget\Certificates.h" /> <ClInclude Include="Public\winget\ConfigurationSetProcessorHandlers.h" /> + <ClInclude Include="Public\winget\GroupPolicy.h" /> <ClInclude Include="Public\winget\ILifetimeWatcher.h" /> <ClInclude Include="Public\winget\JsonSchemaValidation.h" /> + <ClInclude Include="Public\winget\JsonUtil.h" /> <ClInclude Include="Public\winget\LocIndependent.h" /> + <ClInclude Include="Public\winget\Registry.h" /> <ClInclude Include="Public\winget\Resources.h" /> <ClInclude Include="Public\winget\Runtime.h" /> <ClInclude Include="Public\winget\Security.h" /> @@ -413,9 +417,13 @@ <ItemGroup> <ClCompile Include="AppInstallerLogging.cpp" /> <ClCompile Include="AppInstallerStrings.cpp" /> + <ClCompile Include="Certificates.cpp" /> <ClCompile Include="DateTime.cpp" /> <ClCompile Include="Errors.cpp" /> + <ClCompile Include="GroupPolicy.cpp" /> <ClCompile Include="JsonSchemaValidation.cpp" /> + <ClCompile Include="JsonUtil.cpp" /> + <ClCompile Include="Registry.cpp" /> <ClCompile Include="Resources.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> @@ -433,6 +441,9 @@ <None Include="PropertySheet.props" /> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\cpprestsdk\cpprestsdk.vcxproj"> + <Project>{866c3f06-636f-4be8-bc24-5f86ecc606a1}</Project> + </ProjectReference> <ProjectReference Include="..\JsonCppLib\JsonCppLib.vcxproj"> <Project>{82b39fda-e86b-4713-a873-9d56de00247a}</Project> </ProjectReference> diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -83,6 +83,18 @@ <ClInclude Include="Public\winget\Security.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\Certificates.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\GroupPolicy.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\Registry.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\JsonUtil.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -127,6 +139,18 @@ <ClCompile Include="Security.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Certificates.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="GroupPolicy.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Registry.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="JsonUtil.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerSharedLib/Certificates.cpp b/src/AppInstallerSharedLib/Certificates.cpp @@ -0,0 +1,547 @@ +// 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" + + +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, int resourceType) + { + return LoadCertificate(Resource::GetResourceAsBytes(resource, resourceType)); + } + + 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/AppInstallerSharedLib/GroupPolicy.cpp b/src/AppInstallerSharedLib/GroupPolicy.cpp @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "winget/GroupPolicy.h" +#include "AppInstallerLogging.h" + +using namespace AppInstaller::StringResource; + +namespace AppInstaller::Settings +{ + namespace + { + const GroupPolicy& InstanceInternal(std::optional<GroupPolicy*> overridePolicy = {}) + { + const static GroupPolicy s_groupPolicy{ Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, "Software\\Policies\\Microsoft\\Windows\\AppInstaller") }; + static GroupPolicy* s_override = nullptr; + + if (overridePolicy.has_value()) + { + s_override = overridePolicy.value(); + } + + return (s_override ? *s_override : s_groupPolicy); + } + + std::optional<Registry::Value> GetRegistryValueObject(const Registry::Key& key, const std::string_view valueName) + { + if (!key) + { + // Key does not exist; there's nothing to return + return std::nullopt; + } + + return key[valueName]; + } + + template<Registry::Value::Type T> + std::optional<decltype(std::declval<Registry::Value>().GetValue<T>())> GetRegistryValueData(const Registry::Value& regValue, const std::string_view valueName) + { + auto value = regValue.TryGetValue<T>(); + if (!value.has_value()) + { + AICLI_LOG(Core, Warning, << "Value for policy '" << valueName << "' does not have expected type"); + return std::nullopt; + } + + return std::move(value.value()); + } + + template<Registry::Value::Type T> + std::optional<decltype(std::declval<Registry::Value>().GetValue<T>())> GetRegistryValueData(const Registry::Key& key, const std::string_view valueName) + { + auto regValue = GetRegistryValueObject(key, valueName); + if (!regValue.has_value()) + { + // Value does not exist; there's nothing to return + return std::nullopt; + } + + return GetRegistryValueData<T>(regValue.value(), valueName); + } + + std::optional<bool> RegistryValueIsTrue(const Registry::Key& key, std::string_view valueName) + { + auto intValue = GetRegistryValueData<Registry::Value::Type::DWord>(key, valueName); + if (!intValue.has_value()) + { + return std::nullopt; + } + + AICLI_LOG(Core, Verbose, << "Found policy '" << valueName << "', Value: " << *intValue); + return (bool)*intValue; + } + + PolicyState GetStateInternal(const Registry::Key& key, TogglePolicy::Policy policy) + { + // Default to not configured if there is no policy for this + if (policy == TogglePolicy::Policy::None) + { + return PolicyState::NotConfigured; + } + + auto togglePolicy = TogglePolicy::GetPolicy(policy); + + // Policies are not configured if there is no registry value. + auto setting = RegistryValueIsTrue(key, togglePolicy.RegValueName()); + if (!setting.has_value()) + { + return PolicyState::NotConfigured; + } + + // Return flag as-is or invert depending on the policy + return *setting ? PolicyState::Enabled : PolicyState::Disabled; + } + + template <ValuePolicy P> + void Validate(const Registry::Key& policiesKey, GroupPolicy::ValuePoliciesMap& policies) + { + auto value = details::ValuePolicyMapping<P>::ReadAndValidate(policiesKey); + if (value.has_value()) + { + policies.Add<P>(std::move(*value)); + } + } + + template <> + void Validate<ValuePolicy::None>(const Registry::Key&, GroupPolicy::ValuePoliciesMap&) {}; + + template <size_t... P> + void ValidateAllValuePolicies( + const Registry::Key& policiesKey, + GroupPolicy::ValuePoliciesMap& policies, + std::index_sequence<P...>) + { + // Use folding to call each policy validate function. + (FoldHelper{}, ..., Validate<static_cast<ValuePolicy>(P)>(policiesKey, policies)); + } + + // Reads a list from a Group Policy. + // The list is stored in a sub-key of the policies key, and each value in that key is a list item. + // Cases not considered by this function because we don't use them: + // - When the list is in an arbitrary key, not a sub key. + // - When the list values are mixed with other values and are identified by a prefix in their names. + // - When the value names are relevant. + template<ValuePolicy P> + std::optional<typename details::ValuePolicyMapping<P>::value_t> ReadList(const Registry::Key& policiesKey) + { + using Mapping = details::ValuePolicyMapping<P>; + + auto listKey = policiesKey.SubKey(Mapping::KeyName); + if (!listKey.has_value()) + { + return std::nullopt; + } + + typename Mapping::value_t items; + for (const auto& value : listKey->Values()) + { + auto item = Mapping::ReadAndValidateItem(value); + if (item.has_value()) + { + items.emplace_back(std::move(item.value())); + } + else + { + AICLI_LOG(Core, Warning, << "Failed to read Group Policy list value. Policy [" << Mapping::KeyName << "], Value [" << value.Name() << ']'); + } + } + + return items; + } + + std::optional<SourceFromPolicy> ReadSourceFromRegistryValue(const Registry::Value& item) + { + auto jsonString = item.TryGetValue<Registry::Value::Type::String>(); + if (!jsonString.has_value()) + { + AICLI_LOG(Core, Warning, << "Registry value is not a string"); + return std::nullopt; + } + + int stringLength = static_cast<int>(jsonString->length()); + Json::Value sourceJson; + Json::CharReaderBuilder charReaderBuilder; + const std::unique_ptr<Json::CharReader> jsonReader(charReaderBuilder.newCharReader()); + Json::String jsonErrors; + if (!jsonReader->parse(jsonString->c_str(), jsonString->c_str() + stringLength, &sourceJson, &jsonErrors)) + { + AICLI_LOG(Core, Warning, << "Registry value does not contain a valid JSON: " << jsonErrors); + return std::nullopt; + } + + SourceFromPolicy source; + + auto readSourceAttribute = [&](const std::string& name, std::string SourceFromPolicy::* member) + { + if (sourceJson.isMember(name) && sourceJson[name].isString()) + { + source.*member = sourceJson[name].asString(); + return true; + } + else + { + AICLI_LOG(Core, Warning, << "Source JSON does not contain a string value for " << name); + return false; + } + }; + + bool allRead = readSourceAttribute("Name", &SourceFromPolicy::Name) + && readSourceAttribute("Arg", &SourceFromPolicy::Arg) + && readSourceAttribute("Type", &SourceFromPolicy::Type) + && readSourceAttribute("Data", &SourceFromPolicy::Data) + && readSourceAttribute("Identifier", &SourceFromPolicy::Identifier); + if (!allRead) + { + 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; + } + } + + namespace details + { +#define POLICY_MAPPING_DEFAULT_LIST_READ(_policy_) \ + std::optional<typename ValuePolicyMapping<_policy_>::value_t> ValuePolicyMapping<_policy_>::ReadAndValidate(const Registry::Key& policiesKey) \ + { \ + return ReadList<_policy_>(policiesKey); \ + } + + POLICY_MAPPING_DEFAULT_LIST_READ(ValuePolicy::AdditionalSources); + POLICY_MAPPING_DEFAULT_LIST_READ(ValuePolicy::AllowedSources); + + std::nullopt_t ValuePolicyMapping<ValuePolicy::None>::ReadAndValidate(const Registry::Key&) + { + return std::nullopt; + } + + std::optional<uint32_t> ValuePolicyMapping<ValuePolicy::SourceAutoUpdateIntervalInMinutes>::ReadAndValidate(const Registry::Key& policiesKey) + { + // This policy used to have another name in the registry. + // Try to read first with the current name, and if it's not present + // check if the old name is present. + using Mapping = ValuePolicyMapping<ValuePolicy::SourceAutoUpdateIntervalInMinutes>; + + auto regValueWithCurrentName = GetRegistryValueObject(policiesKey, Mapping::ValueName); + if (regValueWithCurrentName.has_value()) + { + // We use the current name even if it doesn't have valid data. + return GetRegistryValueData<Mapping::ValueType>(regValueWithCurrentName.value(), Mapping::ValueName); + } + else + { + return GetRegistryValueData<Mapping::ValueType>(policiesKey, "SourceAutoUpdateIntervalInMinutes"sv); + } + } + + std::optional<SourceFromPolicy> ValuePolicyMapping<ValuePolicy::AdditionalSources>::ReadAndValidateItem(const Registry::Value& item) + { + return ReadSourceFromRegistryValue(item); + } + + std::optional<SourceFromPolicy> ValuePolicyMapping<ValuePolicy::AllowedSources>::ReadAndValidateItem(const Registry::Value& item) + { + return ReadSourceFromRegistryValue(item); + } + } + + TogglePolicy TogglePolicy::GetPolicy(TogglePolicy::Policy policy) + { + switch (policy) + { + case TogglePolicy::Policy::WinGet: + return TogglePolicy(policy, "EnableAppInstaller"sv, String::PolicyEnableWinGet); + case TogglePolicy::Policy::Settings: + return TogglePolicy(policy, "EnableSettings"sv, String::PolicyEnableWingetSettings); + case TogglePolicy::Policy::ExperimentalFeatures: + return TogglePolicy(policy, "EnableExperimentalFeatures"sv, String::PolicyEnableExperimentalFeatures); + case TogglePolicy::Policy::LocalManifestFiles: + return TogglePolicy(policy, "EnableLocalManifestFiles"sv, String::PolicyEnableLocalManifests); + case TogglePolicy::Policy::HashOverride: + return TogglePolicy(policy, "EnableHashOverride"sv, String::PolicyEnableHashOverride); + case TogglePolicy::Policy::LocalArchiveMalwareScanOverride: + return TogglePolicy(policy, "EnableLocalArchiveMalwareScanOverride"sv, String::PolicyEnableLocalArchiveMalwareScanOverride); + case TogglePolicy::Policy::DefaultSource: + return TogglePolicy(policy, "EnableDefaultSource"sv, String::PolicyEnableDefaultSource); + case TogglePolicy::Policy::MSStoreSource: + return TogglePolicy(policy, "EnableMicrosoftStoreSource"sv, String::PolicyEnableMSStoreSource); + case TogglePolicy::Policy::AdditionalSources: + return TogglePolicy(policy, "EnableAdditionalSources"sv, String::PolicyAdditionalSources); + case TogglePolicy::Policy::AllowedSources: + return TogglePolicy(policy, "EnableAllowedSources"sv, String::PolicyAllowedSources); + case TogglePolicy::Policy::BypassCertificatePinningForMicrosoftStore: + return TogglePolicy(policy, "EnableBypassCertificatePinningForMicrosoftStore"sv, String::PolicyEnableBypassCertificatePinningForMicrosoftStore); + case TogglePolicy::Policy::WinGetCommandLineInterfaces: + return TogglePolicy(policy, "EnableWindowsPackageManagerCommandLineInterfaces"sv, String::PolicyEnableWindowsPackageManagerCommandLineInterfaces); + case TogglePolicy::Policy::Configuration: + return TogglePolicy(policy, "EnableWindowsPackageManagerConfiguration"sv, String::PolicyEnableWinGetConfiguration); + default: + THROW_HR(E_UNEXPECTED); + } + } + + std::vector<TogglePolicy> TogglePolicy::GetAllPolicies() + { + using Toggle_t = std::underlying_type_t<TogglePolicy::Policy>; + + std::vector<TogglePolicy> result; + + // Skip "None" + for (Toggle_t i = 1 + static_cast<Toggle_t>(TogglePolicy::Policy::None); i < static_cast<Toggle_t>(TogglePolicy::Policy::Max); ++i) + { + result.emplace_back(GetPolicy(static_cast<Policy>(i))); + } + + return result; + } + + std::string SourceFromPolicy::ToJsonString() const + { + Json::Value json{ Json::ValueType::objectValue }; + json["Name"] = Name; + json["Type"] = Type; + json["Arg"] = Arg; + json["Data"] = Data; + json["Identifier"] = Identifier; + + Json::StreamWriterBuilder writerBuilder; + writerBuilder.settings_["indentation"] = ""; + return Json::writeString(writerBuilder, json); + } + + GroupPolicy::GroupPolicy(const Registry::Key& key) + { + ValidateAllValuePolicies(key, m_values, std::make_index_sequence<static_cast<size_t>(ValuePolicy::Max)>()); + + using Toggle_t = std::underlying_type_t<TogglePolicy::Policy>; + for (Toggle_t i = static_cast<Toggle_t>(TogglePolicy::Policy::None); i < static_cast<Toggle_t>(TogglePolicy::Policy::Max); ++i) + { + auto policy = static_cast<TogglePolicy::Policy>(i); + m_toggles[policy] = GetStateInternal(key, policy); + } + } + + PolicyState GroupPolicy::GetState(TogglePolicy::Policy policy) const + { + auto itr = m_toggles.find(policy); + if (itr == m_toggles.end()) + { + return PolicyState::NotConfigured; + } + + return itr->second; + } + + bool GroupPolicy::IsEnabled(TogglePolicy::Policy policy) const + { + if (policy == TogglePolicy::Policy::None) + { + return true; + } + + PolicyState state = GetState(policy); + if (state == PolicyState::NotConfigured) + { + return TogglePolicy::GetPolicy(policy).DefaultIsEnabled(); + } + + return state == PolicyState::Enabled; + } + + GroupPolicy const& GroupPolicy::Instance() + { + return InstanceInternal(); + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + void GroupPolicy::OverrideInstance(GroupPolicy* overridePolicy) + { + InstanceInternal(overridePolicy); + } + + void GroupPolicy::ResetInstance() + { + InstanceInternal(nullptr); + } +#endif +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/JsonUtil.cpp b/src/AppInstallerSharedLib/JsonUtil.cpp diff --git a/src/AppInstallerSharedLib/Public/winget/Certificates.h b/src/AppInstallerSharedLib/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, int resourceType); + 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/AppInstallerSharedLib/Public/winget/GroupPolicy.h b/src/AppInstallerSharedLib/Public/winget/GroupPolicy.h @@ -0,0 +1,246 @@ +// 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 <string_view> + +using namespace std::string_view_literals; + +namespace AppInstaller::Settings +{ + + // A policy that sets a value for some setting. + // The value of the policy is a value in the registry key, or is + // made up of sub-keys for settings that are lists. + enum class ValuePolicy + { + None, + SourceAutoUpdateIntervalInMinutes, + AdditionalSources, + AllowedSources, + Max, + }; + + // A policy that acts as a toggle to enable or disable a feature. + // They are backed by a DWORD value with values 0 and 1. + struct TogglePolicy + { + enum class Policy + { + None = 0, + WinGet, + Settings, + ExperimentalFeatures, + LocalManifestFiles, + HashOverride, + LocalArchiveMalwareScanOverride, + DefaultSource, + MSStoreSource, + AdditionalSources, + AllowedSources, + BypassCertificatePinningForMicrosoftStore, + WinGetCommandLineInterfaces, + Configuration, + Max, + }; + + TogglePolicy(Policy policy, std::string_view regValueName, StringResource::StringId policyName, bool defaultIsEnabled = true) : + m_policy(policy), m_regValueName(regValueName), m_policyName(policyName), m_defaultIsEnabled(defaultIsEnabled) {} + + static TogglePolicy GetPolicy(Policy policy); + static std::vector<TogglePolicy> GetAllPolicies(); + + Policy GetPolicy() const { return m_policy; } + std::string_view RegValueName() const { return m_regValueName; } + StringResource::StringId PolicyName() const { return m_policyName; } + bool DefaultIsEnabled() const { return m_defaultIsEnabled; } + + private: + Policy m_policy; + std::string_view m_regValueName; + StringResource::StringId m_policyName; + bool m_defaultIsEnabled; + }; + + // Possible configuration states for a policy. + enum class PolicyState + { + NotConfigured, + Disabled, + Enabled, + }; + + // A source defined by Group Policy to be added or allowed + struct SourceFromPolicy + { + std::string Name; + std::string Arg; + std::string Type; + std::string Data; + std::string Identifier; + +#ifndef AICLI_DISABLE_TEST_HOOKS + Certificates::PinningConfiguration PinningConfiguration; +#endif + + std::string ToJsonString() const; + }; + + + namespace details + { + + template <ValuePolicy P> + struct ValuePolicyMapping + { + // value_t - type of the policy + // ReadAndValidate() - Function that reads the value and does semantic validation. + + // For simple values: + // ValueName - Name of the registry value + // ValueType - Type of the registry value + // reg_value_t - Type returned by the registry when reading the value + + // For lists: + // item_t - Type of each item + // KeyName -- Name of the sub-key containing the list + // ReadAndValidateItem() - Function that reads a single item from a subkey + }; + + template<> + struct ValuePolicyMapping<ValuePolicy::None> + { + using value_t = std::monostate; + using opt_value_t = std::nullopt_t; + using opt_ref_value_t = std::nullopt_t; + static std::nullopt_t ReadAndValidate(const Registry::Key& policiesKey); + }; + +#define POLICY_MAPPING_SPECIALIZATION(_policy_, _type_, _extra_) \ + template <> \ + struct ValuePolicyMapping<_policy_> \ + { \ + using value_t = _type_; \ + using opt_value_t = std::optional<value_t>; \ + using opt_ref_value_t = std::optional<std::reference_wrapper<const value_t>>; \ + static std::optional<value_t> ReadAndValidate(const Registry::Key& policiesKey); \ + _extra_ \ + } + +#define POLICY_MAPPING_VALUE_SPECIALIZATION(_policy_, _type_, _valueName_, _valueType_) \ + POLICY_MAPPING_SPECIALIZATION(_policy_, _type_, \ + static constexpr std::string_view ValueName = _valueName_; \ + static constexpr Registry::Value::Type ValueType = _valueType_; \ + using reg_value_t = decltype(std::declval<Registry::Value>().GetValue<ValueType>()); \ + ) + +#define POLICY_MAPPING_LIST_SPECIALIZATION(_policy_, _type_, _keyName_) \ + POLICY_MAPPING_SPECIALIZATION(_policy_, std::vector<_type_>, \ + static constexpr std::string_view KeyName = _keyName_; \ + using item_t = _type_; \ + static std::optional<item_t> ReadAndValidateItem(const Registry::Value& item); \ + ) + + POLICY_MAPPING_VALUE_SPECIALIZATION(ValuePolicy::SourceAutoUpdateIntervalInMinutes, uint32_t, "SourceAutoUpdateInterval"sv, Registry::Value::Type::DWord); + + POLICY_MAPPING_LIST_SPECIALIZATION(ValuePolicy::AdditionalSources, SourceFromPolicy, "AdditionalSources"sv); + POLICY_MAPPING_LIST_SPECIALIZATION(ValuePolicy::AllowedSources, SourceFromPolicy, "AllowedSources"sv); + } + + // Representation of the policies read from the registry. + struct GroupPolicy + { + using ValuePoliciesMap = EnumBasedVariantMap<ValuePolicy, details::ValuePolicyMapping>; + + static GroupPolicy const& Instance(); + + GroupPolicy(const Registry::Key& key); + ~GroupPolicy() = default; + + GroupPolicy() = delete; + + GroupPolicy(const GroupPolicy&) = delete; + GroupPolicy& operator=(const GroupPolicy&) = delete; + + GroupPolicy(GroupPolicy&&) = delete; + GroupPolicy& operator=(GroupPolicy&&) = delete; + + template<ValuePolicy P> + using ValueType = typename details::ValuePolicyMapping<P>::value_t; + + // Gets the policy value if it is present + template<ValuePolicy P> + typename details::ValuePolicyMapping<P>::opt_value_t GetValue() const + { + if (m_values.Contains(P)) + { + return m_values.Get<P>(); + } + else + { + return std::nullopt; + } + } + + template<ValuePolicy P> + typename details::ValuePolicyMapping<P>::opt_ref_value_t GetValueRef() const + { + if (m_values.Contains(P)) + { + return std::cref(m_values.Get<P>()); + } + else + { + return std::nullopt; + } + } + + template<> + std::nullopt_t GetValue<ValuePolicy::None>() const + { + return std::nullopt; + } + + template<> + std::nullopt_t GetValueRef<ValuePolicy::None>() const + { + return std::nullopt; + } + + PolicyState GetState(TogglePolicy::Policy policy) const; + + // Checks whether a policy is enabled, using an appropriate default when not configured. + // Should not be used when not configured means something different than enabled/disabled. + bool IsEnabled(TogglePolicy::Policy policy) const; + +#ifndef AICLI_DISABLE_TEST_HOOKS + protected: + static void OverrideInstance(GroupPolicy* gp); + static void ResetInstance(); +#else + private: +#endif + std::map<TogglePolicy::Policy, PolicyState> m_toggles; + ValuePoliciesMap m_values; + }; + + inline const GroupPolicy& GroupPolicies() + { + return GroupPolicy::Instance(); + } + + struct GroupPolicyException + { + GroupPolicyException(TogglePolicy::Policy policy) : m_policy(policy) {} + + const TogglePolicy::Policy& Policy() const { return m_policy; } + + private: + TogglePolicy::Policy m_policy; + }; + +} diff --git a/src/AppInstallerCommonCore/Public/winget/JsonUtil.h b/src/AppInstallerSharedLib/Public/winget/JsonUtil.h diff --git a/src/AppInstallerCommonCore/Public/winget/Registry.h b/src/AppInstallerSharedLib/Public/winget/Registry.h diff --git a/src/AppInstallerSharedLib/Public/winget/Resources.h b/src/AppInstallerSharedLib/Public/winget/Resources.h @@ -57,6 +57,7 @@ namespace AppInstaller WINGET_DEFINE_RESOURCE_STRINGID(PolicySourceAutoUpdateInterval); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableBypassCertificatePinningForMicrosoftStore); WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWindowsPackageManagerCommandLineInterfaces); + WINGET_DEFINE_RESOURCE_STRINGID(PolicyEnableWinGetConfiguration); WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldFormat); WINGET_DEFINE_RESOURCE_STRINGID(SettingsWarningInvalidFieldValue); diff --git a/src/AppInstallerCommonCore/Registry.cpp b/src/AppInstallerSharedLib/Registry.cpp diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -20,6 +20,7 @@ #include <AppInstallerErrors.h> #include <AppInstallerStrings.h> +#include <winget/GroupPolicy.h> using namespace std::chrono_literals; @@ -115,6 +116,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation ConfigurationProcessor::ConfigurationProcessor() { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !::AppInstaller::Settings::GroupPolicies().IsEnabled(::AppInstaller::Settings::TogglePolicy::Policy::WinGet)); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !::AppInstaller::Settings::GroupPolicies().IsEnabled(::AppInstaller::Settings::TogglePolicy::Policy::Configuration)); + AppInstaller::Logging::DiagnosticLogger& logger = m_threadGlobals.GetDiagnosticLogger(); logger.EnableChannel(AppInstaller::Logging::Channel::All); logger.SetLevel(AppInstaller::Logging::Level::Verbose); diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj @@ -124,14 +124,14 @@ <SDLCheck>true</SDLCheck> <PreprocessorDefinitions>_WINRT_DLL;WIN32_LEAN_AND_MEAN;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions> <AdditionalUsingDirectories>$(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories)</AdditionalUsingDirectories> - <AdditionalIncludeDirectories>$(ProjectDir);$(ProjectDir)..\AppInstallerSharedLib\Public;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories>$(ProjectDir);$(ProjectDir)..\AppInstallerSharedLib\Public;$(ProjectDir)..\JsonCppLib;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> </ClCompile> <Link> <SubSystem>Console</SubSystem> <GenerateWindowsMetadata>false</GenerateWindowsMetadata> <ModuleDefinitionFile>Microsoft_Management_Configuration.def</ModuleDefinitionFile> <WindowsMetadataFile>$(OutDir)$(ProjectName).winmd</WindowsMetadataFile> - <AdditionalDependencies>Advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies>Advapi32.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'"> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/AsyncCommand.cs @@ -61,6 +61,11 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands throw new GroupPolicyException(Policy.WinGet, GroupPolicyFailureType.BlockedByPolicy); } + if (!groupPolicy.IsEnabled(Policy.Configuration)) + { + throw new GroupPolicyException(Policy.Configuration, GroupPolicyFailureType.BlockedByPolicy); + } + if (!groupPolicy.IsEnabled(Policy.WinGetCommandLineInterfaces)) { throw new GroupPolicyException(Policy.WinGetCommandLineInterfaces, GroupPolicyFailureType.BlockedByPolicy); diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/Extensions/EnumPolicyExtension.cs b/src/PowerShell/Microsoft.WinGet.SharedLib/Extensions/EnumPolicyExtension.cs @@ -48,6 +48,8 @@ namespace Microsoft.WinGet.SharedLib.Extensions return GroupPolicyResource.PolicyEnableBypassCertificatePinningForMicrosoftStore; case Policy.WinGetCommandLineInterfaces: return GroupPolicyResource.PolicyEnableWindowsPackageManagerCommandLineInterfaces; + case Policy.Configuration: + return GroupPolicyResource.PolicyEnableWinGetConfiguration; default: return string.Empty; } diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/Enums.cs b/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/Enums.cs @@ -91,6 +91,11 @@ namespace Microsoft.WinGet.SharedLib.PolicySettings /// Enabled Command line Interfaces. /// </summary> WinGetCommandLineInterfaces, + + /// <summary> + /// Enabled configuration. + /// </summary> + Configuration, } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/TogglePolicy.cs b/src/PowerShell/Microsoft.WinGet.SharedLib/PolicySettings/TogglePolicy.cs @@ -123,6 +123,8 @@ namespace Microsoft.WinGet.SharedLib.PolicySettings return new TogglePolicy(policy, "EnableBypassCertificatePinningForMicrosoftStore", GroupPolicyResource.PolicyEnableBypassCertificatePinningForMicrosoftStore); case Policy.WinGetCommandLineInterfaces: return new TogglePolicy(policy, "EnableWindowsPackageManagerCommandLineInterfaces", GroupPolicyResource.PolicyEnableWindowsPackageManagerCommandLineInterfaces); + case Policy.Configuration: + return new TogglePolicy(policy, "EnableWindowsPackageManagerConfiguration", GroupPolicyResource.PolicyEnableWinGetConfiguration); default: throw new ArgumentException(null, nameof(policy)); } diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.Designer.cs b/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.Designer.cs @@ -169,6 +169,15 @@ namespace Microsoft.WinGet.SharedLib.Resources { } /// <summary> + /// Looks up a localized string similar to Enable Windows Package Manager Configuration. + /// </summary> + internal static string PolicyEnableWinGetConfiguration { + get { + return ResourceManager.GetString("PolicyEnableWinGetConfiguration", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Enable Windows Package Manager Settings. /// </summary> internal static string PolicyEnableWinGetSettings { diff --git a/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.resx b/src/PowerShell/Microsoft.WinGet.SharedLib/Resources/GroupPolicyResource.resx @@ -153,6 +153,9 @@ <data name="PolicyEnableWinGet" xml:space="preserve"> <value>Enable Windows Package Manager</value> </data> + <data name="PolicyEnableWinGetConfiguration" xml:space="preserve"> + <value>Enable Windows Package Manager Configuration</value> + </data> <data name="PolicyEnableWinGetSettings" xml:space="preserve"> <value>Enable Windows Package Manager Settings</value> </data> diff --git a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 @@ -82,6 +82,21 @@ Describe 'Test-GroupPolicies' { CleanupGroupPolicies } + It "Disable EnableWindowsPackageManagerConfiguration Policy and run Get-WinGetConfiguration" { + $policyKeyValueName = "EnableWindowsPackageManagerConfiguration" + + Set-ItemProperty -Path $wingetGroupPolicyRegistryRoot -Name $policyKeyValueName -Value 0 + $registryKey = Get-ItemProperty -Path $wingetGroupPolicyRegistryRoot -Name $policyKeyValueName + $registryKey | Should -Not -BeNullOrEmpty + $registryKey.EnableWindowsPackageManagerConfiguration | Should -Be 0 + + # [NOTE:] We don't need a valid yml file path to test Group Policy blocking scenario as it is the earliest check, + # so just using some random file path for this test. + { Get-WinGetConfiguration -File "Z:\NonExisting_SettingsFile.yml" } | Should -Throw "This operation is disabled by Group Policy : Enable Windows Package Manager Configuration" + + CleanupGroupPolicies + } + AfterAll { CleanupGroupPolicies CleanupGroupPolicyKeyIfExists diff --git a/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp b/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp @@ -216,8 +216,8 @@ namespace ConfigurationShim IFACEMETHODIMP CreateInstance(_In_opt_::IUnknown* unknownOuter, REFIID riid, _COM_Outptr_ void** object) noexcept try { *object = nullptr; - // TODO: Review of policies for configuration RETURN_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !::AppInstaller::Settings::GroupPolicies().IsEnabled(::AppInstaller::Settings::TogglePolicy::Policy::WinGet)); + RETURN_HR_IF(APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY, !::AppInstaller::Settings::GroupPolicies().IsEnabled(::AppInstaller::Settings::TogglePolicy::Policy::Configuration)); // TODO: Review of security for configuration OOP RETURN_HR_IF(E_ACCESSDENIED, !::AppInstaller::Security::IsCOMCallerSameUserAndIntegrityLevel());