diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.cpp b/src/AppInstallerCLICore/Commands/DebugCommand.cpp index af596c91c5..7af30965db 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.cpp +++ b/src/AppInstallerCLICore/Commands/DebugCommand.cpp @@ -9,8 +9,11 @@ #include #include #include "AppInstallerDownloader.h" +#include #include "Sixel.h" #include +#include +#include using namespace AppInstaller::CLI::Execution; @@ -68,6 +71,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), }); } @@ -708,6 +712,168 @@ namespace AppInstaller::CLI context.Reporter.Info() << std::endl; } } + +// ── ValidateStorePinningCommand ────────────────────────────────────────────── + + namespace + { + std::string PercentageToString(double value) + { + std::ostringstream stream; + stream << std::fixed << std::setprecision(1) << (value * 100.0) << '%'; + return std::move(stream).str(); + } + + // The state shared with the certificate validation callback, which is invoked on another thread. + struct StorePinningValidationState + { + std::mutex Lock; + bool CertificateSeen = false; + std::string ChainDescription; + bool PinningAccepted = false; + HRESULT Error = S_OK; + }; + } + + std::vector ValidateStorePinningCommand::GetArguments() const + { + return { + Argument{ "url", 'u', Args::Type::SourceArg, Resource::String::SourceListUpdatedNever, ArgumentType::Positional }, + }; + } + + Resource::LocString ValidateStorePinningCommand::ShortDescription() const + { + return Utility::LocIndString("Validate the Store source certificate pinning"sv); + } + + Resource::LocString ValidateStorePinningCommand::LongDescription() const + { + return Utility::LocIndString( + "Connects to the given URL (defaulting to the Microsoft Store source URL) and validates the " + "certificate that it presents against the current static pinning configuration for the Store source. " + "Use this to determine if a rotated certificate will be accepted before it is deployed."sv); + } + + void ValidateStorePinningCommand::ExecuteInternal(Execution::Context& context) const + { + Repository::SourceDetails storeDetails = Repository::GetWellKnownSourceDetails(Repository::WellKnownSource::MicrosoftStore); + + std::string url{ storeDetails.Arg }; + if (context.Args.Contains(Args::Type::SourceArg)) + { + url = context.Args.GetArg(Args::Type::SourceArg); + } + + if (url.find("://") == std::string::npos) + { + url = "https://" + url; + } + + context.Reporter.Info() << "Validating the Microsoft Store source pinning configuration against: " << url << std::endl; + + if (storeDetails.CertificatePinningConfiguration.IsEmpty()) + { + context.Reporter.Warn() << + "The Store source has no pinning configuration; it has likely been disabled by the " + "BypassCertificatePinningForMicrosoftStore admin setting. Every certificate will be accepted." << std::endl; + } + else + { + context.Reporter.Info() << "Current pinning configuration:" << std::endl << + storeDetails.CertificatePinningConfiguration.GetDescription() << std::endl; + context.Reporter.Info() << "Pinned certificate remaining lifetime: " << + PercentageToString(storeDetails.CertificatePinningConfiguration.GetRemainingLifetimePercentage()) << std::endl; + } + + auto state = std::make_shared(); + + // Accept every certificate so that the pinning result can be reported independently of the connection result. + Certificates::PinningConfiguration captureConfiguration{ "Store Pinning Validation" }; + captureConfiguration.AddChain(std::make_shared( + [state, pinningConfiguration = storeDetails.CertificatePinningConfiguration](PCCERT_CONTEXT certContext) + { + std::lock_guard lock{ state->Lock }; + state->CertificateSeen = true; + + try + { + wil::unique_cert_chain_context chainContext; + + try + { + chainContext = Certificates::PinningConfiguration::BuildCertificateChain(certContext); + } + catch (...) + { + // Revocation information may not be reachable for all endpoints; try again without it. + LOG_CAUGHT_EXCEPTION(); + chainContext = Certificates::PinningConfiguration::BuildCertificateChain(certContext, nullptr, nullptr, 0); + } + + state->ChainDescription = Certificates::GetCertificateChainDescription(chainContext.get()); + state->PinningAccepted = pinningConfiguration.Validate(certContext, chainContext.get()); + } + catch (...) + { + state->Error = LOG_CAUGHT_EXCEPTION(); + } + + return true; + })); + + Http::HttpClientHelper client; + client.SetPinningConfiguration(captureConfiguration, context.GetSharedThreadGlobals()); + + HRESULT connectionResult = S_OK; + std::optional statusCode; + + try + { + statusCode = client.Get(Utility::ConvertToUTF16(url)).get().status_code(); + } + catch (...) + { + connectionResult = LOG_CAUGHT_EXCEPTION(); + } + + if (state->CertificateSeen) + { + context.Reporter.Info() << "Server certificate chain (root first):" << std::endl << state->ChainDescription << std::endl; + } + + if (statusCode) + { + context.Reporter.Info() << "Connection completed with HTTP status: " << statusCode.value() << std::endl; + } + else + { + context.Reporter.Warn() << "Connection failed with: 0x" << Logging::SetHRFormat << connectionResult << std::endl; + } + + if (!state->CertificateSeen) + { + context.Reporter.Error() << "No server certificate was received; the endpoint could not be reached." << std::endl; + AICLI_TERMINATE_CONTEXT(FAILED(connectionResult) ? connectionResult : E_UNEXPECTED); + } + + if (FAILED(state->Error)) + { + context.Reporter.Error() << "Failed to evaluate the certificate against the pinning configuration: 0x" << + Logging::SetHRFormat << state->Error << std::endl; + AICLI_TERMINATE_CONTEXT(state->Error); + } + + if (state->PinningAccepted) + { + context.Reporter.Info() << "Pinning validation: PASSED" << std::endl; + } + else + { + context.Reporter.Error() << "Pinning validation: FAILED" << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH); + } + } } #endif diff --git a/src/AppInstallerCLICore/Commands/DebugCommand.h b/src/AppInstallerCLICore/Commands/DebugCommand.h index 907f874a13..e6f80c6757 100644 --- a/src/AppInstallerCLICore/Commands/DebugCommand.h +++ b/src/AppInstallerCLICore/Commands/DebugCommand.h @@ -128,6 +128,20 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; }; + + // Validates the Microsoft Store source certificate pinning configuration against an endpoint. + struct ValidateStorePinningCommand final : public Command + { + ValidateStorePinningCommand(std::string_view parent) : Command("validate-store-pinning", {}, parent) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; } #endif diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h index 4c7dcadc31..ee262698ec 100644 --- a/src/AppInstallerCLICore/pch.h +++ b/src/AppInstallerCLICore/pch.h @@ -58,6 +58,15 @@ #include #pragma warning( pop ) +#ifndef WINGET_DISABLE_FOR_FUZZING +#pragma warning( push ) +#pragma warning ( disable : 26495 26439 ) +#include +#include +#include +#pragma warning( pop ) +#endif + #include #include #include diff --git a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h index f772a8f8a1..a91a1fdb9b 100644 --- a/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h +++ b/src/AppInstallerRepositoryCore/Public/winget/RepositorySource.h @@ -165,6 +165,10 @@ namespace AppInstaller::Repository // Check if a source matches a well known source std::optional CheckForWellKnownSource(const SourceDetails& sourceDetails); + // Gets the details for a well known source, including its certificate pinning configuration. + // The details are not populated with any locally stored metadata. + SourceDetails GetWellKnownSourceDetails(WellKnownSource source); + // Individual source agreement entry. Label will be highlighted in the display as the key of the agreement entry. struct SourceAgreement { diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp index ac273fc0c8..32dfc5d81f 100644 --- a/src/AppInstallerRepositoryCore/RepositorySource.cpp +++ b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -432,6 +432,11 @@ namespace AppInstaller::Repository return CheckForWellKnownSourceMatch(sourceDetails.Name, sourceDetails.Arg, sourceDetails.Type); } + SourceDetails GetWellKnownSourceDetails(WellKnownSource source) + { + return GetWellKnownSourceDetailsInternal(source); + } + Source::Source() {} Source::Source(std::string_view name) diff --git a/src/AppInstallerSharedLib/Certificates.cpp b/src/AppInstallerSharedLib/Certificates.cpp index eca9a866a9..17b4c281af 100644 --- a/src/AppInstallerSharedLib/Certificates.cpp +++ b/src/AppInstallerSharedLib/Certificates.cpp @@ -841,6 +841,34 @@ namespace AppInstaller::Certificates return result; } + std::string PinningConfiguration::GetDescription() const + { + std::ostringstream stream; + stream << "Pinning configuration [" << m_identifier << "]"; + + if (m_configuration.empty()) + { + stream << ": "; + } + else + { + size_t index = 0; + + for (const auto& chain : m_configuration) + { + stream << std::endl << "Chain #" << ++index << ':' << std::endl << chain->GetDescription(); + } + } + + return std::move(stream).str(); + } + + std::string GetCertificateChainDescription(PCCERT_CHAIN_CONTEXT chainContext) + { + THROW_HR_IF(E_INVALIDARG, !chainContext || chainContext->cChain == 0); + return GetDescriptionOfCertChain(chainContext); + } + std::string GetAuthenticodeSubject(const std::filesystem::path& filePath) { const std::wstring& pathStr = filePath.wstring(); diff --git a/src/AppInstallerSharedLib/Public/winget/Certificates.h b/src/AppInstallerSharedLib/Public/winget/Certificates.h index 868098b3fd..a35d5d84bb 100644 --- a/src/AppInstallerSharedLib/Public/winget/Certificates.h +++ b/src/AppInstallerSharedLib/Public/winget/Certificates.h @@ -22,6 +22,9 @@ namespace AppInstaller::Certificates // Returns an empty string if the file is unsigned, untrusted, or on any error. std::string GetAuthenticodeSubject(const std::filesystem::path& filePath); + // Gets a human readable, indented description of the certificates in the given chain, from root to leaf. + std::string GetCertificateChainDescription(PCCERT_CHAIN_CONTEXT chainContext); + // Defines the types of certificate pinning to perform. enum class PinningVerificationType : uint32_t { @@ -254,6 +257,9 @@ namespace AppInstaller::Certificates // Loads the pinning configuration from the given JSON. [[nodiscard]] bool LoadFrom(const Json::Value& configuration); + // Gets a description of the configuration, including all of its chains. + std::string GetDescription() const; + // Determines how far the configuration is through its lifespan (the maximum of all of its chains). double GetRemainingLifetimePercentage() const;