-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Load index from validated msix for unpackaged context #2139
Conversation
@@ -214,4 +215,74 @@ namespace TestCommon | |||
{ | |||
AppInstaller::Settings::SetUserSettingsOverride(nullptr); | |||
} | |||
|
|||
bool InstallCertFromSignedPackage(const std::filesystem::path& package) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a fan of requiring the tests to run as admin.
Also not a fan of the tests changing the security profile of the machine they are run on.
Could we possibly test with a package signed by something already trusted? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made the tests to skip if not running with admin. But I cannot find a way to "mark" the tests as skipped. So I just output an info message.
ComPtr<IStream> otherStream; | ||
THROW_IF_FAILED(SHCreateStreamOnFileEx(otherManifest.c_str(), | ||
STGM_READ | STGM_SHARE_DENY_WRITE | STGM_FAILIFTHERE, 0, FALSE, nullptr, &otherStream)); | ||
MsixInfo other{ otherPackage.u8string() }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just have a MsixInfo(const std::filesystem::path&)
constructor? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just tried and since the other constructor is std::string_view, compiler gives ambiguous overloaded function call unless we update all the callers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I asked about this and found out that this is a reasonable way to do it:
template<typename T, std::enable_if_t<std::is_same_v<T, std::filesystem::path>, int> = 0>
MsixInfo(const T& path) { ConstructFromPath(path); }
Where ConstructFromPath
can then be implemented in the .cpp
.
MsixInfo msixInfo{ msixPath.u8string() }; | ||
auto signature = msixInfo.GetSignature(); | ||
THROW_HR_IF(E_UNEXPECTED, signature.size() <= P7xFileIdSize); | ||
signature.erase(signature.begin(), signature.begin() + P7xFileIdSize); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GetSignature(true)
to skip the first 4 bytes if they match the file id? erase
here is expensive and isn't actually verifying that the bytes match the id. #Resolved
|
||
bool ValidateMsixTrustInfo(const std::filesystem::path& msixPath, bool verifyMicrosoftOrigin) | ||
{ | ||
bool result = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bool result = true; | |
bool result = false; |
Bugs should not result in returning a 👍 verification. #Resolved
WINTRUST_DATA trustData = { 0 }; | ||
trustData.cbStruct = sizeof(WINTRUST_DATA); | ||
trustData.dwUIChoice = WTD_UI_NONE; | ||
trustData.fdwRevocationChecks = WTD_REVOKE_NONE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we be doing revocation checks? #Resolved
|
||
if (!std::filesystem::exists(packageLocation)) | ||
{ | ||
AICLI_LOG(Repo, Info, << "Data not found at " << packageLocation); | ||
THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING); | ||
} | ||
|
||
SQLiteIndex index = SQLiteIndex::Open(packageLocation.u8string(), SQLiteIndex::OpenDisposition::Read); | ||
THROW_HR_IF(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE, !Msix::ValidateMsixTrustInfo(packageLocation, WI_IsFlagSet(m_details.TrustLevel, SourceTrustLevel::StoreOrigin))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This appears to be vulnerable to a time of check vs time of use attack; nothing is preventing the package from being replaced between the check here and opening the file later.
You need the TempSQLiteIndexFile
(or another helper) that can lock the file from being modified, then do the validation and extraction. #Resolved
{ | ||
namespace | ||
{ | ||
static constexpr std::string_view s_PreIndexedPackageSourceFactory_IndexFilePath = "Public\\index.db"sv; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be defined in a second location. #Resolved
@@ -57,7 +58,7 @@ namespace AppInstaller::Repository::Microsoft | |||
}; | |||
|
|||
// Opens an existing index database. | |||
static SQLiteIndex Open(const std::string& filePath, OpenDisposition disposition); | |||
static SQLiteIndex Open(const std::string& filePath, OpenDisposition disposition, TempSQLiteIndexFile&& tempIndexFileHandle = {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love this being passed in, but I understand the need. Can we change the type to WriteLockedFile
or something more generic? That could be used for both the package and index files (with a create
bool that controls both open and remove on destruction functionality).
Then also just move the "extract the index file" part out of the type as it isn't really necessary to be included. #Resolved
@check-spelling-bot ReportUnrecognized words, please review:
Previously acknowledged words that are now absentactivatable amd Archs dsc enr FWW Globals hackathon lww mytool OSVERSION Packagedx parametermap symlink Uninitialize WDAG whatif wsbTo accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands... in a clone of the [email protected]:yao-msft/winget-cli.git repository
|
{ | ||
if (!Runtime::IsRunningAsAdmin()) | ||
{ | ||
INFO("Test requires admin privilege. Skipped."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
INFO("Test requires admin privilege. Skipped."); | |
WARN("Test requires admin privilege. Skipped."); |
Not sure how this behaves instead, maybe it is more obvious? #Resolved
ManagedFile(ManagedFile&&) = default; | ||
ManagedFile& operator=(ManagedFile&&) = default; | ||
|
||
HANDLE GetFileHandle() { return m_fileHandle.get(); } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HANDLE GetFileHandle() { return m_fileHandle.get(); } | |
HANDLE GetFileHandle() const { return m_fileHandle.get(); } | |
``` #Resolved |
ManagedFile& operator=(ManagedFile&&) = default; | ||
|
||
HANDLE GetFileHandle() { return m_fileHandle.get(); } | ||
const std::filesystem::path& GetFilePath() { return m_filePath; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const std::filesystem::path& GetFilePath() { return m_filePath; } | |
const std::filesystem::path& GetFilePath() const { return m_filePath; } | |
``` #Resolved |
// If we got bytes, just accept them and keep going. | ||
LOG_IF_FAILED(hr); | ||
|
||
THROW_LAST_ERROR_IF(INVALID_SET_FILE_POINTER == SetFilePointer(target, 0, nullptr, FILE_END)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to set this every time through the loop? #Resolved
else | ||
{ | ||
// If given a size, and we have read it all, quit | ||
if (expectedSize && totalBytesRead == expectedSize) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't appear to be doing anything to prevent writing more than expectedSize
. Is that intentional? #Resolved
@@ -335,6 +556,18 @@ namespace AppInstaller::Msix | |||
THROW_IF_FAILED(signatureStream->Stat(&stat, STATFLAG_NONAME)); | |||
THROW_HR_IF(E_UNEXPECTED, stat.cbSize.HighPart != 0); // Signature size should be small | |||
signatureSize = stat.cbSize.LowPart; | |||
THROW_HR_IF(E_UNEXPECTED, signatureSize <= P7xFileIdSize); | |||
|
|||
if (getRawSignature) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getRawSignature
being true, but the code then modifying the signature, doesn't make sense to me. Maybe this was supposed to be if (!getRawSignature)
? Or should the name be changed to skipFileID
? #Resolved
THROW_IF_FAILED(CoCreateGuid(&guid)); | ||
WCHAR tempFileName[256]; | ||
THROW_HR_IF(E_UNEXPECTED, StringFromGUID2(guid, tempFileName, ARRAYSIZE(tempFileName)) == 0); | ||
auto tempIndexFilePath = Runtime::GetPathTo(Runtime::PathName::Temp); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would either create a new function GetNewTempFilePath
or GetPathTo(PathName::NewTempFile)
, but both would return the same type of path you are constructing here. This is definitely reusable. #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by looking at GetPathTo() code, it's more of returning a directory caller can work on. I'll create GetNewTempFilePath() then
// If we already have a manifest, use it to determine if we need to update or not. | ||
if (!packageInfo.IsNewerThan(manifestPath)) | ||
// If we already have a trusted index package, use it to determine if we need to update or not. | ||
if (Msix::ValidateMsixTrustInfo(packagePath, WI_IsFlagSet(details.TrustLevel, SourceTrustLevel::StoreOrigin)) && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need another
auto indexPackageLock = Utility::ManagedFile::OpenWriteLockedFile(packagePath, 0);
to prevent a TOCTOU here as well (between validation and the IsNewerThan
).
It might even be worth making a helper type like:
struct PersistedIndexPackage
{
PersistedIndexPackage(path);
bool ValidateMsixTrustInfo(bool checkMSOrigin);
private:
ManagedFile m_fileLock;
}
Then remove the free function ValidateMsixTrustInfo
. That way you must lock the file to validate it, and hopefully that translates into the appropriate lifetime of the lock in the calling code. #Resolved
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
|
||
bool ValidateMsixTrustInfo(bool checkMicrosoftOrigin) const | ||
{ | ||
return Msix::ValidateMsixTrustInfo(m_file.GetFilePath(), checkMicrosoftOrigin); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason to move to this model was that the only way to call ValidateMsixTrustInfo
would be to create a PersistedIndexPackage
. If you leave the free function ValidateMsixTrustInfo
around, someone can still use it without the file lock. #Resolved
Validation: Added new tests and fixed existing tests. Also did manual verification that source opening works as expected.
Microsoft Reviewers: Open in CodeFlow