diff --git a/schemas/JSON/manifests/v1.4.0/manifest.installer.1.4.0.json b/schemas/JSON/manifests/v1.4.0/manifest.installer.1.4.0.json index 1c09ee9ae8..83f22299bd 100644 --- a/schemas/JSON/manifests/v1.4.0/manifest.installer.1.4.0.json +++ b/schemas/JSON/manifests/v1.4.0/manifest.installer.1.4.0.json @@ -96,6 +96,11 @@ "maxLength": 512, "description": "The relative path to the nested installer file" }, + "FileSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Optional Sha256 of the nested installer file." + }, "PortableCommandAlias": { "type": [ "string", "null" ], "minLength": 1, diff --git a/schemas/JSON/manifests/v1.4.0/manifest.singleton.1.4.0.json b/schemas/JSON/manifests/v1.4.0/manifest.singleton.1.4.0.json index cf0e9cc9b7..c010a7e8a1 100644 --- a/schemas/JSON/manifests/v1.4.0/manifest.singleton.1.4.0.json +++ b/schemas/JSON/manifests/v1.4.0/manifest.singleton.1.4.0.json @@ -138,6 +138,11 @@ "maxLength": 512, "description": "The relative path to the nested installer file" }, + "FileSha256": { + "type": [ "string", "null" ], + "pattern": "^[A-Fa-f0-9]{64}$", + "description": "Optional Sha256 of the nested installer file." + }, "PortableCommandAlias": { "type": [ "string", "null" ], "minLength": 1, diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index cc4ca9ff1e..61c6adbfac 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -208,6 +208,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(MultipleNonPortableNestedInstallersSpecified); WINGET_DEFINE_RESOURCE_STRINGID(MultiplePackagesFound); WINGET_DEFINE_RESOURCE_STRINGID(NameArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerHashMismatchAdminBlock); + WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerHashMismatchError); + WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerHashMismatchOverridden); + WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerHashMismatchOverrideRequired); + WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerHashVerified);; WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerNotFound); WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerNotSpecified); WINGET_DEFINE_RESOURCE_STRINGID(NestedInstallerNotSupported); diff --git a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp index 2c56cec013..47954b9a37 100644 --- a/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ArchiveFlow.cpp @@ -75,11 +75,27 @@ namespace AppInstaller::CLI::Workflow AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_MANIFEST); } + // Since multiple files might be checked, we need a way to understand whether any files had a hash mismatch. + int hashMismatchCount = 0; + // If none of the installers have a FileSha256, we don't need to print that the hashses were verified + bool hashMismatchChecked = false; + + // Check if any of the Nested Installer Files have a FileSha256 specified + if (std::any_of(installer.NestedInstallerFiles.begin(), installer.NestedInstallerFiles.end(), [](auto& v) { + return !v.FileSha256.empty(); + })) + { + // Since the flag of the installer hash was set when downloading the archive file, it should be cleared here + context.ClearFlags(Execution::ContextFlag::InstallerHashMatched); + hashMismatchChecked = true; + } + std::filesystem::path targetInstallerPath = context.Get().parent_path() / s_Extracted; for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { const std::filesystem::path& nestedInstallerPath = targetInstallerPath / ConvertToUTF16(nestedInstallerFile.RelativeFilePath); + const Utility::SHA256::HashBuffer& nestedInstallerSha256 = nestedInstallerFile.FileSha256; if (Filesystem::PathEscapesBaseDirectory(nestedInstallerPath, targetInstallerPath)) { @@ -99,8 +115,56 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG(CLI, Info, << "Setting installerPath to: " << nestedInstallerPath); targetInstallerPath = nestedInstallerPath; } + + if (!nestedInstallerSha256.empty()) { + const auto& fileSha256 = Utility::SHA256::ComputeHashFromFile(nestedInstallerPath); + if (!std::equal( + nestedInstallerSha256.begin(), + nestedInstallerSha256.end(), + fileSha256.begin())) + { + hashMismatchCount++; + AICLI_LOG(CLI, Warning, << "Nested installer file hash does not match." + << " Expected: " << Utility::SHA256::ConvertToString(nestedInstallerSha256) + << " Found: " << Utility::SHA256::ConvertToString(fileSha256) + << " at " << nestedInstallerPath + ); + } + } } + bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); + + if (hashMismatchCount == 0) + { + AICLI_LOG(CLI, Info, << "Nested installer file hashes verified"); + context.Reporter.Info() << Resource::String::NestedInstallerHashVerified << std::endl; + + context.SetFlags(Execution::ContextFlag::InstallerHashMatched); + } + else if (overrideHashMismatch && !Runtime::IsRunningAsAdmin()) + { + AICLI_LOG(CLI, Warning, << "Nested installer files contain hash mismatches. Proceeding due to --ignore-security-hash."); + context.Reporter.Warn() << Resource::String::NestedInstallerHashMismatchOverridden << std::endl; + } + else + { + // If running as admin, do not allow the user to override the hash failure. + if (Runtime::IsRunningAsAdmin()) + { + context.Reporter.Error() << Resource::String::NestedInstallerHashMismatchAdminBlock << std::endl; + } + else if (Settings::GroupPolicies().IsEnabled(Settings::TogglePolicy::Policy::HashOverride)) + { + context.Reporter.Error() << Resource::String::NestedInstallerHashMismatchOverrideRequired << std::endl; + } + else + { + context.Reporter.Error() << Resource::String::NestedInstallerHashMismatchError << std::endl; + } + AICLI_LOG(CLI, Error, << "Nested installer files contain hash mismatches."); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NESTED_INSTALLER_HASH_MISMATCH); + } context.Add(targetInstallerPath); } diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index a86aaeade1..f48649603e 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -392,6 +392,23 @@ They can be configured through the settings file 'winget settings'. Filter results by name + + Hash of extracted files does not match; this cannot be overridden when running as admin + + + Hash of extracted files does not match. + + + Hash of extracted files does not match; proceeding due to --ignore-security-hash + {Locked="--ignore-security-hash"} + + + Hash of extracted files does not match; to override this check use --ignore-security-hash + {Locked="--ignore-security-hash"} + + + Successfully verified hash of extracted files + No applicable installer found; see logs for more details. diff --git a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp index f27088b566..2fd0ac939a 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp @@ -32,6 +32,7 @@ namespace AppInstaller::Manifest { AppInstaller::Manifest::ManifestError::InstallerTypeDoesNotWriteAppsAndFeaturesEntry, "The specified installer type does not write to Apps and Features entry."sv }, { AppInstaller::Manifest::ManifestError::IncompleteMultiFileManifest, "The multi file manifest is incomplete.A multi file manifest must contain at least version, installer and defaultLocale manifest."sv }, { AppInstaller::Manifest::ManifestError::InconsistentMultiFileManifestFieldValue, "The multi file manifest has inconsistent field values."sv }, + { AppInstaller::Manifest::ManifestError::DuplicateFileSha256, "Duplicate file hash found."sv }, { AppInstaller::Manifest::ManifestError::DuplicatePortableCommandAlias, "Duplicate portable command alias found."sv }, { AppInstaller::Manifest::ManifestError::DuplicateRelativeFilePath, "Duplicate relative file path found."sv }, { AppInstaller::Manifest::ManifestError::DuplicateMultiFileManifestType, "The multi file manifest should contain only one file with the particular ManifestType."sv }, @@ -264,6 +265,7 @@ namespace AppInstaller::Manifest std::set commandAliasSet; std::set relativeFilePathSet; + std::set fileSha256Set; for (const auto& nestedInstallerFile : installer.NestedInstallerFiles) { @@ -287,6 +289,12 @@ namespace AppInstaller::Manifest resultErrors.emplace_back(ManifestError::DuplicateRelativeFilePath, "RelativeFilePath"); } + // Check for duplicate file hash values. + if (!relativeFilePathSet.insert(Utility::ToLower(Utility::SHA256::ConvertToString(nestedInstallerFile.FileSha256))).second) + { + resultErrors.emplace_back(ManifestError::DuplicateFileSha256, "FileSha256"); + } + // Check for duplicate portable command alias values. const auto& alias = Utility::ToLower(nestedInstallerFile.PortableCommandAlias); if (!alias.empty() && !commandAliasSet.insert(alias).second) diff --git a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp index aacb3b7f41..acdc638e0c 100644 --- a/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp +++ b/src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp @@ -580,6 +580,7 @@ namespace AppInstaller::Manifest result = { { "RelativeFilePath", [this](const YAML::Node& value)->ValidationErrors { m_p_nestedInstallerFile->RelativeFilePath = Utility::Trim(value.as()); return {}; } }, + { "FileSha256", [this](const YAML::Node& value)->ValidationErrors { m_p_nestedInstallerFile->FileSha256 = Utility::SHA256::ConvertToBytes(value.as()); return {}; } }, { "PortableCommandAlias", [this](const YAML::Node& value)->ValidationErrors { m_p_nestedInstallerFile->PortableCommandAlias = Utility::Trim(value.as()); return {}; } }, }; } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h index 130a556b0b..1cdb0a91eb 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -110,6 +110,7 @@ #define APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED ((HRESULT)0x8A15005F) #define APPINSTALLER_CLI_ERROR_ARCHIVE_SCAN_FAILED ((HRESULT)0x8A150060) #define APPINSTALLER_CLI_ERROR_PACKAGE_ALREADY_INSTALLED ((HRESULT)0x8A150061) +#define APPINSTALLER_CLI_ERROR_NESTED_INSTALLER_HASH_MISMATCH ((HRESULT)0x8A150062) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h index e69bf4e2de..6aa8800316 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestCommon.h @@ -264,6 +264,7 @@ namespace AppInstaller::Manifest struct NestedInstallerFile { string_t RelativeFilePath; + std::vector FileSha256; string_t PortableCommandAlias; }; diff --git a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h index 52aa910680..4e8cfb5940 100644 --- a/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h +++ b/src/AppInstallerCommonCore/Public/winget/ManifestValidation.h @@ -23,6 +23,7 @@ namespace AppInstaller::Manifest WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionOverlapWithIndex); WINGET_DEFINE_RESOURCE_STRINGID(ArpVersionValidationInternalError); WINGET_DEFINE_RESOURCE_STRINGID(BothAllowedAndExcludedMarketsDefined); + WINGET_DEFINE_RESOURCE_STRINGID(DuplicateFileSha256); WINGET_DEFINE_RESOURCE_STRINGID(DuplicatePortableCommandAlias); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateRelativeFilePath); WINGET_DEFINE_RESOURCE_STRINGID(DuplicateMultiFileManifestLocale); diff --git a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer_1_4.cpp b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer_1_4.cpp index 05b606882a..987cc1195f 100644 --- a/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer_1_4.cpp +++ b/src/AppInstallerRepositoryCore/Rest/Schema/1_4/Json/ManifestDeserializer_1_4.cpp @@ -166,6 +166,13 @@ namespace AppInstaller::Repository::Rest::Schema::V1_4::Json Manifest::NestedInstallerFile nestedInstallerFile; nestedInstallerFile.RelativeFilePath = std::move(*relativeFilePath); + + std::optional sha256 = JSON::GetRawStringValueFromJsonNode(nestedInstallerFileNode, JSON::GetUtilityString(FileSha256)); + if (JSON::IsValidNonEmptyStringValue(sha256)) + { + nestedInstallerFile.FileSha256 = Utility::SHA256::ConvertToBytes(*sha256); + } + nestedInstallerFile.PortableCommandAlias = JSON::GetRawStringValueFromJsonNode(nestedInstallerFileNode, JSON::GetUtilityString(PortableCommandAlias)).value_or(""); installer.NestedInstallerFiles.emplace_back(std::move(nestedInstallerFile));