diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a7dabd68..5afeb405b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,13 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin") set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/lib") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/lib") +## Require that the target is little endian +include(TestBigEndian) +TEST_BIG_ENDIAN(IS_BIG_ENDIAN) +if(IS_BIG_ENDIAN) + message(FATAL_ERROR "Big endian targets are not supported at this time") +endif() + ## Git (and its revision) find_package(Git) # QUIET) # if we don't find git or FindGit.cmake is not on the system we ignore it. diff --git a/README.md b/README.md index 18b7e8b42..863509531 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,10 @@ The following native platforms are in development now: **Release x64 With Pack**|[![Build Status](https://dev.azure.com/ms/msix-packaging/_apis/build/status/msix-packaging%20Windows%20CI?branchName=master&jobName=Windows&configuration=Windows%20release_64_pack)](https://dev.azure.com/ms/msix-packaging/_build/latest?definitionId=64&branchName=master)| **Release x32 Xerces With Pack**|[![Build Status](https://dev.azure.com/ms/msix-packaging/_apis/build/status/msix-packaging%20Windows%20CI?branchName=master&jobName=Windows&configuration=Windows%20release_32_xerces)](https://dev.azure.com/ms/msix-packaging/_build/latest?definitionId=64&branchName=master)| **Release x64 Xerces With Pack**|[![Build Status](https://dev.azure.com/ms/msix-packaging/_apis/build/status/msix-packaging%20Windows%20CI?branchName=master&jobName=Windows&configuration=Windows%20release_64_xerces)](https://dev.azure.com/ms/msix-packaging/_build/latest?definitionId=64&branchName=master)| +**Debug x32 With Pack and OpenSSL**|[![Build Status](https://dev.azure.com/ms/msix-packaging/_apis/build/status/msix-packaging%20Windows%20CI?branchName=master&jobName=Windows&configuration=Windows%20debug_32_sign)](https://dev.azure.com/ms/msix-packaging/_build/latest?definitionId=64&branchName=master)| +**Debug x64 With Pack and OpenSSL**|[![Build Status](https://dev.azure.com/ms/msix-packaging/_apis/build/status/msix-packaging%20Windows%20CI?branchName=master&jobName=Windows&configuration=Windows%20debug_64_sign)](https://dev.azure.com/ms/msix-packaging/_build/latest?definitionId=64&branchName=master)| +**Release x32 With Pack and OpenSSL**|[![Build Status](https://dev.azure.com/ms/msix-packaging/_apis/build/status/msix-packaging%20Windows%20CI?branchName=master&jobName=Windows&configuration=Windows%20release_32_sign)](https://dev.azure.com/ms/msix-packaging/_build/latest?definitionId=64&branchName=master)| +**Release x64 With Pack and OpenSSL**|[![Build Status](https://dev.azure.com/ms/msix-packaging/_apis/build/status/msix-packaging%20Windows%20CI?branchName=master&jobName=Windows&configuration=Windows%20release_64_sign)](https://dev.azure.com/ms/msix-packaging/_build/latest?definitionId=64&branchName=master)| Built in the Azure Pipelines windows-latest pool. See specifications [here](https://github.com/actions/virtual-environments/blob/main/images/win/Windows2019-Readme.md) diff --git a/cmake/crypto.cmake b/cmake/crypto.cmake index 19d5c2a72..60475f695 100644 --- a/cmake/crypto.cmake +++ b/cmake/crypto.cmake @@ -36,8 +36,12 @@ file( COPY ${OpenSSL_SOURCE_PATH}/include/openssl DESTINATION ${OpenSLL_INCLUDE_ if(WIN32) # TODO: Replicate build flags for cl - # Flags taken from OpenSSL Configure file for VC-WIN64A target. More care may be required for other targets. - set(TARGET_COMPILE_FLAGS -W3 -Gs0 -Gy -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -DUNICODE -D_UNICODE) + # Flags taken from OpenSSL Configure file for VC-WIN64A target. More care may be required for other targets. + if ((CMAKE_BUILD_TYPE MATCHES Release) OR (CMAKE_BUILD_TYPE MATCHES MinSizeRel)) + set(TARGET_COMPILE_FLAGS -O1 -W3 -Gs0 -Gy -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -DUNICODE -D_UNICODE) + else() + set(TARGET_COMPILE_FLAGS -Zi -W3 -Gs0 -Gy -nologo -DOPENSSL_SYSNAME_WIN32 -DWIN32_LEAN_AND_MEAN -DL_ENDIAN -DUNICODE -D_UNICODE) + endif() else() set( TARGET_COMPILE_FLAGS -fno-rtti -fno-stack-protector -O1 -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-math-errno -fno-unroll-loops -fmerge-all-constants) @@ -84,6 +88,12 @@ endif() # Begin configure public headers file( READ "${MSIX_PROJECT_ROOT}/cmake/openssl/opensslconf.h.cmake" CONF ) +set(CONDITIONAL_CONF "") + +if(NOT MSIX_PACK) + set(CONDITIONAL_CONF "${CONDITIONAL_CONF} +#define OPENSSL_NO_DES") +endif() set( CONF " #define OPENSSL_NO_GMP #define OPENSSL_NO_JPAKE @@ -100,7 +110,6 @@ set( CONF " #define OPENSSL_NO_BF #define OPENSSL_NO_IDEA #define OPENSSL_NO_ENGINE -#define OPENSSL_NO_DES #define OPENSSL_NO_MDC2 #define OPENSSL_NO_SEED #define OPENSSL_NO_DEPRECATED @@ -124,6 +133,9 @@ set( CONF " #define OPENSSL_NO_CMS #define OPENSSL_NO_SRP #define OPENSSL_NO_SM2 + +${CONDITIONAL_CONF} + ${CONF}" ) file( WRITE "${OpenSLL_INCLUDE_PATH}/openssl/opensslconf.h.cmake" "${CONF}" ) @@ -160,4 +172,4 @@ target_compile_definitions( crypto PRIVATE ${TARGET_DEFINES} ${TARGET_DEFINES_PR target_compile_options ( crypto PRIVATE ${TARGET_COMPILE_FLAGS} ${TARGET_COMPILE_FLAGS_PRIVATE}) target_include_directories( crypto PUBLIC ${TARGET_INCLUDE_DIRS} ${OpenSLL_INCLUDE_PATH} ${OpenSLL_INCLUDE_PATH}/openssl) target_compile_definitions( crypto PUBLIC ${TARGET_DEFINES} ) -target_compile_options ( crypto PUBLIC ${TARGET_COMPILE_FLAGS}) \ No newline at end of file +target_compile_options ( crypto PUBLIC ${TARGET_COMPILE_FLAGS}) diff --git a/cmake/crypto_sources.cmake b/cmake/crypto_sources.cmake index 9c7aedac8..83661eb91 100644 --- a/cmake/crypto_sources.cmake +++ b/cmake/crypto_sources.cmake @@ -77,6 +77,7 @@ list(APPEND XSRC ${CRYPTO}/asn1/nsseq.c ${CRYPTO}/asn1/p5_pbe.c ${CRYPTO}/asn1/p5_pbev2.c + ${CRYPTO}/asn1/p5_scrypt.c ${CRYPTO}/asn1/p8_pkey.c ${CRYPTO}/asn1/t_bitst.c ${CRYPTO}/asn1/t_pkey.c @@ -309,6 +310,7 @@ list(APPEND XSRC ${CRYPTO}/pkcs12/p12_npas.c ${CRYPTO}/pkcs12/p12_p8d.c ${CRYPTO}/pkcs12/p12_p8e.c + ${CRYPTO}/pkcs12/p12_sbag.c ${CRYPTO}/pkcs12/p12_utl.c ${CRYPTO}/pkcs12/pk12err.c @@ -481,6 +483,30 @@ else() ) endif() +if(MSIX_PACK) + # Enable better error reporting in signing scenarios + list(APPEND XSRC + ${CRYPTO}/ocsp/ocsp_err.c + ) + + # Added for DES support + list(APPEND XSRC + ${CRYPTO}/evp/e_des.c + ${CRYPTO}/evp/e_des3.c + ${CRYPTO}/evp/e_xcbc_d.c + ${CRYPTO}/des/cfb_enc.c + ${CRYPTO}/des/ecb_enc.c + ${CRYPTO}/des/cfb64enc.c + ${CRYPTO}/des/cfb64ede.c + ${CRYPTO}/des/ofb64enc.c + ${CRYPTO}/des/ofb64ede.c + ${CRYPTO}/des/ecb3_enc.c + ${CRYPTO}/des/des_enc.c + ${CRYPTO}/des/xcbc_enc.c + ${CRYPTO}/des/set_key.c + ) +endif() + if( WIN32 ) list(APPEND XSRC ${CRYPTO}/async/arch/async_win.c diff --git a/pipelines/templates/build-windows.yml b/pipelines/templates/build-windows.yml index d702e439c..abdbc6ee6 100644 --- a/pipelines/templates/build-windows.yml +++ b/pipelines/templates/build-windows.yml @@ -47,6 +47,18 @@ jobs: debug_64_pack: _arguments: x64 -d --pack _artifact: WIN32-x64chk-pack + release_32_sign: + _arguments: x86 --pack -co + _artifact: WIN32-sign + release_64_sign: + _arguments: x64 --pack -co + _artifact: WIN32-x64-sign + debug_32_sign: + _arguments: x86 -d --pack -co + _artifact: WIN32chk-sign + debug_64_sign: + _arguments: x64 -d --pack -co + _artifact: WIN32-x64chk-sign steps: - task: BatchScript@1 diff --git a/src/inc/internal/AppxPackageObject.hpp b/src/inc/internal/AppxPackageObject.hpp index e06951bf2..09ba5e06b 100644 --- a/src/inc/internal/AppxPackageObject.hpp +++ b/src/inc/internal/AppxPackageObject.hpp @@ -38,6 +38,8 @@ class IPackage : public IUnknown public: virtual void Unpack(MSIX_PACKUNPACK_OPTION options, const MSIX::ComPtr& to) = 0; virtual std::vector& GetFootprintFiles() = 0; + virtual MSIX::ComPtr GetFactory() = 0; + virtual MSIX::ComPtr GetUnderlyingStorageObject() = 0; }; MSIX_INTERFACE(IPackage, 0x51b2c456,0xaaa9,0x46d6,0x8e,0xc9,0x29,0x82,0x20,0x55,0x91,0x89); @@ -108,6 +110,8 @@ namespace MSIX { // internal IPackage methods void Unpack(MSIX_PACKUNPACK_OPTION options, const ComPtr& to) override; std::vector& GetFootprintFiles() override { return m_footprintFiles; } + MSIX::ComPtr GetFactory() override { return m_factory; } + MSIX::ComPtr GetUnderlyingStorageObject() override { return m_container; } // IAppxPackageReader HRESULT STDMETHODCALLTYPE GetBlockMap(IAppxBlockMapReader** blockMapReader) noexcept override; diff --git a/src/inc/internal/AppxPackageWriter.hpp b/src/inc/internal/AppxPackageWriter.hpp index b9c77c8f7..cc45bb7bf 100644 --- a/src/inc/internal/AppxPackageWriter.hpp +++ b/src/inc/internal/AppxPackageWriter.hpp @@ -10,6 +10,8 @@ #include "AppxBlockMapWriter.hpp" #include "ContentTypeWriter.hpp" #include "ZipObjectWriter.hpp" +#include "AppxPackageObject.hpp" +#include "Signing.hpp" #include #include @@ -28,6 +30,12 @@ class IPackageWriter : public IUnknown public: // TODO: add options if needed virtual void PackPayloadFiles(const MSIX::ComPtr& from) = 0; + // Custom Close used to finish out the signing process + virtual void Close( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey) = 0; }; MSIX_INTERFACE(IPackageWriter, 0x32e89da5,0x7cbb,0x4443,0x8c,0xf0,0xb8,0x4e,0xed,0xb5,0x1d,0x0a); @@ -37,10 +45,16 @@ namespace MSIX { { public: AppxPackageWriter(IMsixFactory* factory, const ComPtr& zip, bool enableFileHash); + AppxPackageWriter(IPackage* packageToSign, std::unique_ptr&& accumulator); ~AppxPackageWriter() {}; // IPackageWriter void PackPayloadFiles(const ComPtr& from) override; + void Close( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey) override; // IAppxPackageWriter HRESULT STDMETHODCALLTYPE AddPayloadFile(LPCWSTR fileName, LPCWSTR contentType, @@ -72,15 +86,16 @@ namespace MSIX { APPX_COMPRESSION_OPTION compressionOpt, const char* contentType); void AddFileToPackage(const std::string& name, IStream* stream, bool toCompress, - bool addToBlockMap, const char* contentType, bool forceContentTypeOverride = false); + bool addToBlockMap, const char* contentType, bool forceContentTypeOverride = false, bool forceDataDescriptor = true); void ValidateCompressionOption(APPX_COMPRESSION_OPTION compressionOpt); - WriterState m_state; + WriterState m_state = WriterState::Open; ComPtr m_factory; ComPtr m_zipWriter; BlockMapWriter m_blockMapWriter; ContentTypeWriter m_contentTypeWriter; + std::unique_ptr m_signatureAccumulator; }; } diff --git a/src/inc/internal/AppxSignature.hpp b/src/inc/internal/AppxSignature.hpp index c8e83625a..dbe869395 100644 --- a/src/inc/internal/AppxSignature.hpp +++ b/src/inc/internal/AppxSignature.hpp @@ -32,6 +32,11 @@ namespace MSIX { // APPX-specific header placed in the P7X file, before the actual signature const DWORD P7X_FILE_ID = 0x58434b50; + // APPX-SIP default version + const DWORD APPX_SIP_DEFAULT_VERSION = 0x01010000; + +#define APPX_SIP_GUID_BYTES 0x4B, 0xDF, 0xC5, 0x0A, 0x07, 0xCE, 0xE2, 0x4D, 0xB7, 0x6E, 0x23, 0xC8, 0x39, 0xA0, 0x9F, 0xD1 + enum class DigestName : std::uint32_t { HEAD = 0x58505041, // APPX @@ -60,8 +65,10 @@ namespace MSIX { class AppxSignatureObject final : public ComClass { public: + // Used in signing; we create an empty object to fill with digests. + AppxSignatureObject() = default; - AppxSignatureObject(IMsixFactory* factory, MSIX_VALIDATION_OPTION validationOptions,const ComPtr& stream); + AppxSignatureObject(IMsixFactory* factory, MSIX_VALIDATION_OPTION validationOptions, const ComPtr& stream); // IVerifierObject const std::string& GetPublisher() override { return m_publisher; } diff --git a/src/inc/internal/ContentTypeWriter.hpp b/src/inc/internal/ContentTypeWriter.hpp index 257eaf377..d20e71c4a 100644 --- a/src/inc/internal/ContentTypeWriter.hpp +++ b/src/inc/internal/ContentTypeWriter.hpp @@ -9,6 +9,7 @@ #include "ComHelper.hpp" #include +#include namespace MSIX { @@ -17,6 +18,10 @@ namespace MSIX { public: ContentTypeWriter(); + // Used for editing an existing content type file, but only in the very specific case of signing. + // Creates a copy and sets the cursor to the end of the existing elements stream. + ContentTypeWriter(IStream* stream); + void AddContentType(const std::string& name, const std::string& contentType, bool forceOverride = false); void Close(); ComPtr GetStream() { return m_xmlWriter.GetStream(); } @@ -24,8 +29,14 @@ namespace MSIX { protected: void AddDefault(const std::string& ext, const std::string& contentType); void AddOverride(const std::string& file, const std::string& contentType); - + + static std::string GetPartNameSearchString(const std::string& fileName); + std::map m_defaultExtensions; XmlWriter m_xmlWriter; + + // For the signing scenario, we need to know if the signature files are already present. + bool m_hasSignatureOverride = false; + bool m_hasCIOverride = false; }; } \ No newline at end of file diff --git a/src/inc/internal/FileStream.hpp b/src/inc/internal/FileStream.hpp index 66a4d5338..7d410987f 100644 --- a/src/inc/internal/FileStream.hpp +++ b/src/inc/internal/FileStream.hpp @@ -4,6 +4,13 @@ // #pragma once +// For SetSize file truncation support. +#ifdef WIN32 +#include +#else +#include +#endif + #include #include #include @@ -23,10 +30,10 @@ namespace MSIX { static const char* modes[] = { "rb", "wb", "ab", "r+b", "w+b", "a+b" }; #ifdef WIN32 errno_t err = fopen_s(&m_file, name.c_str(), modes[mode]); - ThrowErrorIfNot(Error::FileOpen, (err==0), std::string("file: " + m_name + " does not exist.").c_str()); + ThrowErrorIfNot(Error::FileOpen, (err == 0), std::string("error opening [" + m_name + "] with mode [" + std::to_string(mode) + "] => " + std::to_string(err)).c_str()); #else m_file = std::fopen(name.c_str(), modes[mode]); - ThrowErrorIfNot(Error::FileOpen, (m_file), std::string("file: " + m_name + " does not exist.").c_str()); + ThrowErrorIfNot(Error::FileOpen, (m_file), std::string("error opening [" + m_name + "] with mode [" + std::to_string(mode) + "] => " + std::to_string(errno)).c_str()); #endif // Get size of the file @@ -43,11 +50,11 @@ namespace MSIX { #ifdef WIN32 static const wchar_t* modes[] = { L"rb", L"wb", L"ab", L"r+b", L"w+b", L"a+b" }; errno_t err = _wfopen_s(&m_file, name.c_str(), modes[mode]); - ThrowErrorIfNot(Error::FileOpen, (err==0), std::string("file: " + m_name + " does not exist.").c_str()); + ThrowErrorIfNot(Error::FileOpen, (err==0), std::string("error opening [" + m_name + "] with mode [" + std::to_string(mode) + "] => " + std::to_string(err)).c_str()); #else static const char* modes[] = { "rb", "wb", "ab", "r+b", "w+b", "a+b" }; m_file = std::fopen(m_name.c_str(), modes[mode]); - ThrowErrorIfNot(Error::FileOpen, (m_file), std::string("file: " + m_name + " does not exist.").c_str()); + ThrowErrorIfNot(Error::FileOpen, (m_file), std::string("error opening [" + m_name + "] with mode [" + std::to_string(mode) + "] => " + std::to_string(errno)).c_str()); #endif // Get size of the file LARGE_INTEGER start = { 0 }; @@ -74,14 +81,8 @@ namespace MSIX { // IStream HRESULT STDMETHODCALLTYPE Seek(LARGE_INTEGER move, DWORD origin, ULARGE_INTEGER* newPosition) noexcept override try { - #ifdef WIN32 - int rc = _fseeki64(m_file, move.QuadPart, origin); - #else - int rc = std::fseek(m_file, static_cast(move.QuadPart), origin); - #endif - ThrowErrorIfNot(Error::FileSeek, (rc == 0), "seek failed"); - m_offset = Ftell(); - if (newPosition) { newPosition->QuadPart = m_offset; } + SeekInternal(move.QuadPart, origin); + if (newPosition) { newPosition->QuadPart = Ftell(); } return static_cast(Error::OK); } CATCH_RETURN(); @@ -90,7 +91,6 @@ namespace MSIX { if (bytesRead) { *bytesRead = 0; } ULONG result = static_cast(std::fread(buffer, sizeof(std::uint8_t), countBytes, m_file)); ThrowErrorIfNot(Error::FileRead, (result == countBytes || Feof()), "read failed"); - m_offset = Ftell(); if (bytesRead) { *bytesRead = result; } return static_cast(Error::OK); } CATCH_RETURN(); @@ -100,11 +100,29 @@ namespace MSIX { if (bytesWritten) { *bytesWritten = 0; } ULONG result = static_cast(std::fwrite(buffer, sizeof(std::uint8_t), countBytes, m_file)); ThrowErrorIfNot(Error::FileWrite, (result == countBytes), "write failed"); - m_offset = Ftell(); if (bytesWritten) { *bytesWritten = result; } return static_cast(Error::OK); } CATCH_RETURN(); + HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER size) noexcept override try + { +#ifdef WIN32 + // Store the current location... + uint64_t resetLocation = Ftell(); + // ... then move to the end of the stream, as this is how SetEndOfFile works. + SeekInternal(size.QuadPart, StreamBase::Reference::START); + + // Get the HANDLE of the file to pass to SetEndOfFile + ThrowHrIfFalse(SetEndOfFile(reinterpret_cast(_get_osfhandle(_fileno(m_file)))), "Failed to set the end of the file"); + + // Return to the previous location + SeekInternal(resetLocation, StreamBase::Reference::START); +#else + ThrowHrIfPOSIXFailed(ftruncate(fileno(m_file), static_cast(size.QuadPart)), "Failed to set the end of the file"); +#endif + return static_cast(Error::OK); + } CATCH_RETURN(); + // IStreamInternal std::string GetName() override { return m_name; } @@ -113,6 +131,16 @@ namespace MSIX { inline bool Feof() { return 0 != std::feof(m_file); } inline void Flush() { std::fflush(m_file); } + inline void SeekInternal(int64_t move, DWORD origin) + { + #ifdef WIN32 + int rc = _fseeki64(m_file, move, origin); + #else + int rc = std::fseek(m_file, static_cast(move), origin); + #endif + ThrowErrorIfNot(Error::FileSeek, (rc == 0), "seek failed"); + } + inline std::uint64_t Ftell() { #ifdef WIN32 @@ -123,7 +151,6 @@ namespace MSIX { return static_cast(result); } - std::uint64_t m_offset = 0; std::uint64_t m_size = 0; std::string m_name; FILE* m_file; diff --git a/src/inc/internal/VectorStream.hpp b/src/inc/internal/MemoryStream.hpp similarity index 85% rename from src/inc/internal/VectorStream.hpp rename to src/inc/internal/MemoryStream.hpp index 881ff0e03..4a9155264 100644 --- a/src/inc/internal/VectorStream.hpp +++ b/src/inc/internal/MemoryStream.hpp @@ -8,20 +8,25 @@ #include "ComHelper.hpp" #include "MsixFeatureSelector.hpp" +#include #include #include namespace MSIX { - class VectorStream final : public StreamBase + class MemoryStream final : public StreamBase { public: - VectorStream(std::vector* data) : m_data(data) {} + // Create a stream that contains its own storage. + MemoryStream() : m_storage(std::make_unique>()), m_data(m_storage.get()) {} + + // Create a stream backed by external storage. + MemoryStream(std::vector* data) : m_data(data) {} HRESULT STDMETHODCALLTYPE Read(void* buffer, ULONG countBytes, ULONG* bytesRead) noexcept override try { ULONG amountToRead = std::min(countBytes, static_cast(m_data->size() - m_offset)); - if (amountToRead > 0) { memcpy(buffer, &(m_data->at(m_offset)), amountToRead); } + if (amountToRead > 0) { memcpy(buffer, &(m_data->at(m_offset)), amountToRead); } m_offset += amountToRead; if (bytesRead) { *bytesRead = amountToRead; } return static_cast(Error::OK); @@ -67,6 +72,7 @@ namespace MSIX { protected: ULONG m_offset = 0; + std::unique_ptr> m_storage; std::vector* m_data; }; } // namespace MSIX \ No newline at end of file diff --git a/src/inc/internal/MsixFeatureSelector.hpp b/src/inc/internal/MsixFeatureSelector.hpp index f3e2f3afa..39a031a6b 100644 --- a/src/inc/internal/MsixFeatureSelector.hpp +++ b/src/inc/internal/MsixFeatureSelector.hpp @@ -10,7 +10,7 @@ #ifdef BUNDLE_SUPPORT #define THROW_IF_BUNDLE_NOT_ENABLED #else -#define THROW_IF_BUNDLE_NOT_ENABLED { MSIX::RaiseException(__LINE__, __FILE__, "Bundle support not enabled", MSIX::Error::NotSupported); } +#define THROW_IF_BUNDLE_NOT_ENABLED { MSIX::RaiseException(__LINE__, __FILE__, "Bundle support not enabled", MSIX::Error::SupportExcludedByBuild); } #endif // BUNDLE_SUPPORT #endif // THROW_IF_BUNDLE_NOT_ENABLED @@ -18,6 +18,6 @@ #ifdef MSIX_PACK #define THROW_IF_PACK_NOT_ENABLED #else -#define THROW_IF_PACK_NOT_ENABLED { MSIX::RaiseException(__LINE__, __FILE__, "Packaging support not enabled", MSIX::Error::NotSupported); } +#define THROW_IF_PACK_NOT_ENABLED { MSIX::RaiseException(__LINE__, __FILE__, "Packaging support not enabled", MSIX::Error::SupportExcludedByBuild ); } #endif // MSIX_PACK #endif // THROW_IF_PACK_NOT_ENABLED diff --git a/src/inc/internal/ObjectBase.hpp b/src/inc/internal/ObjectBase.hpp index e18b4c77e..c50af4bc2 100644 --- a/src/inc/internal/ObjectBase.hpp +++ b/src/inc/internal/ObjectBase.hpp @@ -4,6 +4,8 @@ // #pragma once +#include +#include #include #include #include @@ -19,22 +21,30 @@ namespace MSIX { namespace Meta { // there is exactly one value that this field is allowed to be template static void ExactValueValidation(T value, T spec) { - ThrowErrorIfNot(Error::InvalidParameter, spec == value, "Incorrect value specified at field."); + std::string message = "Value in field doesn't match expectation; expected " + + std::to_string(spec) + ", got " + std::to_string(value); + ThrowErrorIfNot(Error::InvalidParameter, spec == value, message.c_str()); } // there is exactly one value that this field is not allowed to be template static void NotValueValidation(T value, T spec) { - ThrowErrorIf(Error::InvalidParameter, spec == value, "Incorrect value specified at field."); + std::string message = + "Value in field is disallowed match expectation; got disallowed value " + + std::to_string(value); + ThrowErrorIf(Error::InvalidParameter, spec == value, message.c_str()); } // there are exactly two values that this field is allowed to be template static void OnlyEitherValueValidation(T value, T spec1, T spec2) { + std::string message = "Value in field doesn't match expectations; expected either " + + std::to_string(spec1) + " or " + std::to_string(spec2) + ", got " + + std::to_string(value); ThrowErrorIf(Error::InvalidParameter, spec1 != value && spec2 != value, - "Incorrect value specified at field."); + message.c_str()); } ////////////////////////////////////////////////////////////////////////////////////////////// @@ -229,4 +239,4 @@ class StructuredObject : public TypeList } }; -} /* namespace Meta */ } /* namespace MSIX */ \ No newline at end of file +} /* namespace Meta */ } /* namespace MSIX */ diff --git a/src/inc/internal/RangeStream.hpp b/src/inc/internal/RangeStream.hpp index 8fd5ad4b1..6c3ba71d3 100644 --- a/src/inc/internal/RangeStream.hpp +++ b/src/inc/internal/RangeStream.hpp @@ -103,7 +103,7 @@ namespace MSIX { return static_cast(Error::OK); } CATCH_RETURN(); - std::uint64_t Size() { return m_size; } + std::uint64_t GetSize() override { return m_size; } protected: std::uint64_t m_offset; diff --git a/src/inc/internal/SHA256HashStream.hpp b/src/inc/internal/SHA256HashStream.hpp new file mode 100644 index 000000000..fa4697887 --- /dev/null +++ b/src/inc/internal/SHA256HashStream.hpp @@ -0,0 +1,36 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once + +#include "Exceptions.hpp" +#include "StreamBase.hpp" +#include "Crypto.hpp" + +namespace MSIX { + // Hashes the data written to it; out stream only. + class SHA256HashStream final : public StreamBase + { + public: + SHA256HashStream() = default; + + HRESULT STDMETHODCALLTYPE Write(const void* buffer, ULONG countBytes, ULONG* bytesWritten) noexcept override try + { + m_hasher.HashData(reinterpret_cast(buffer), countBytes); + if (bytesWritten) + { + *bytesWritten = countBytes; + } + return static_cast(Error::OK); + } CATCH_RETURN(); + + void FinalizeAndGetHashValue(std::vector& hash) + { + m_hasher.FinalizeAndGetHashValue(hash); + } + + private: + SHA256 m_hasher; + }; +} diff --git a/src/inc/internal/SignatureCreator.hpp b/src/inc/internal/SignatureCreator.hpp new file mode 100644 index 000000000..0d614b7b1 --- /dev/null +++ b/src/inc/internal/SignatureCreator.hpp @@ -0,0 +1,23 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once +#include "ComHelper.hpp" +#include "AppxPackaging.hpp" +#include "AppxSignature.hpp" + +namespace MSIX { + + class SignatureCreator + { + public: + // Creates a signature stream from the digests, signed by the certificate. + static ComPtr Sign( + AppxSignatureObject* digests, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey); + }; +} diff --git a/src/inc/internal/Signing.hpp b/src/inc/internal/Signing.hpp new file mode 100644 index 000000000..6249e0fb5 --- /dev/null +++ b/src/inc/internal/Signing.hpp @@ -0,0 +1,145 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once +#include "AppxPackaging.hpp" +#include "ComHelper.hpp" +#include "AppxSignature.hpp" +#include "Crypto.hpp" +#include "ZipObjectWriter.hpp" + +#include +#include +#include +#include + +namespace MSIX +{ + +static const std::array signingModifiedFiles = +{ + CONTENT_TYPES_XML, + CODEINTEGRITY_CAT, + APPXSIGNATURE_P7X, +}; + +// Given a file name, determine the format of the certificate. +MSIX_CERTIFICATE_FORMAT DetermineCertificateFormat(LPCSTR file); + +// Given a format, is a separate private key file required? +bool DoesCertificateFormatRequirePrivateKey(MSIX_CERTIFICATE_FORMAT format); + +// Signs a package in-place with the given certificate. +void SignPackage( + IAppxPackageReader* package, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey); + +// Allows signature data to be accumulated, either for creation or validation of a signature. +// The design is intended to enable signing both during and after packaging, as well as +// to be used during signature validation. +struct SignatureAccumulator +{ + // TODO: Take in hash algorithm and options for what to accumulate + SignatureAccumulator() = default; + + SignatureAccumulator(const SignatureAccumulator&) = delete; + SignatureAccumulator& operator=(const SignatureAccumulator&) = delete; + + SignatureAccumulator(SignatureAccumulator&&) = default; + SignatureAccumulator& operator=(SignatureAccumulator&&) = default; + + // RAII class to encompass a single file's accumulated data. + struct FileAccumulator + { + friend SignatureAccumulator; + + ~FileAccumulator(); + + FileAccumulator(const FileAccumulator&) = delete; + FileAccumulator& operator=(const FileAccumulator&) = delete; + + FileAccumulator(FileAccumulator&&) = delete; + FileAccumulator& operator=(FileAccumulator&&) = delete; + + inline bool WantsRaw() const { return wantsRaw; } + + bool AccumulateRaw(IStream* stream); + bool AccumulateRaw(const std::vector& data); + + inline bool WantsZip() const { return wantsZip; } + + bool AccumulateZip(IStream* stream); + + private: + FileAccumulator(SignatureAccumulator& accumulator, std::string partName, bool createCICatalog); + + SignatureAccumulator& signatureAccumulator; + std::string name; + bool wantsRaw = true; + bool wantsZip = true; + bool isBlockmap = false; + bool isContentTypes = false; + + SHA256& GetRawHasher() + { + if (!rawHasher) + { + rawHasher = std::make_unique(); + } + return *rawHasher; + } + + SHA256& GetZipHasher() + { + return signatureAccumulator.GetZipHasher(); + } + + std::unique_ptr rawHasher; + }; + + friend FileAccumulator; + + // Gets a file accumulator for the given filename. The caller can then accumulate data + // into it as it comes in. + std::unique_ptr GetFileAccumulator(std::string partName); + + ComPtr GetCodeIntegrityStream( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + IStream* privateKey); + + ComPtr GetSignatureObject(IZipWriter* zipWriter); + +private: + // Not currently thread safe; but the hash operation for zips needs to be in order in any + // case. So if multi-threaded packing/signing is ever implemented, some external code + // will keep us safe anyway. + SHA256& GetZipHasher() + { + if (!zipHasher) + { + zipHasher = std::make_unique(); + } + return *zipHasher; + } + + AppxSignatureObject* GetSignatureObject() + { + if (!digests) + { + digests = ComPtr::Make(); + } + return digests.Get(); + } + + // TODO: Implement CI catalog + bool createCICatalog = false; + std::unique_ptr zipHasher; + ComPtr digests; +}; + +} diff --git a/src/inc/internal/StreamHelper.hpp b/src/inc/internal/StreamHelper.hpp index b735a23cd..26ac1db89 100644 --- a/src/inc/internal/StreamHelper.hpp +++ b/src/inc/internal/StreamHelper.hpp @@ -6,58 +6,71 @@ #include "AppxPackaging.hpp" #include "Exceptions.hpp" +#include "ComHelper.hpp" #include "StreamBase.hpp" +#include #include +#include namespace MSIX { namespace Helper { - inline std::vector CreateBufferFromStream(const ComPtr& stream) - { - // Create buffer from stream - LARGE_INTEGER start = { 0 }; - ULARGE_INTEGER end = { 0 }; - ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::END, &end)); - ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); - - std::uint32_t streamSize = end.u.LowPart; - std::vector buffer(streamSize); - ULONG actualRead = 0; - ThrowHrIfFailed(stream->Read(buffer.data(), streamSize, &actualRead)); - ThrowErrorIf(Error::FileRead, (actualRead != streamSize), "read error"); - - // move the underlying stream back to the beginning. - ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); - return buffer; - } + std::vector CreateBufferFromStream(const ComPtr& stream); - inline std::pair> CreateRawBufferFromStream(const ComPtr& stream) - { - // Create buffer from stream - LARGE_INTEGER start = { 0 }; - ULARGE_INTEGER end = { 0 }; - ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::END, &end)); - ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); - - std::uint32_t streamSize = end.u.LowPart; - std::unique_ptr buffer = std::make_unique(streamSize); - ULONG actualRead = 0; - ThrowHrIfFailed(stream->Read(buffer.get(), streamSize, &actualRead)); - ThrowErrorIf(Error::FileRead, (actualRead != streamSize), "read error"); - - // move the underlying stream back to the beginning. - ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); - return std::make_pair(streamSize, std::move(buffer)); - } + std::string CreateStringFromStream(IStream* stream); + + std::pair> CreateRawBufferFromStream(const ComPtr& stream); inline void WriteStringToStream(const ComPtr& stream, const std::string& toWrite) { - ULONG written; + ULONG written = 0; ThrowHrIfFailed(stream->Write(static_cast(toWrite.data()), static_cast(toWrite.size()), &written)); ThrowErrorIf(Error::FileWrite, (static_cast(toWrite.size()) != written), "write failed"); } + // Helper struct that allows range based for loops to operate on blocks of data from the given stream. + struct StreamProcessor + { + // Forward only iterator; advancing overwrites old data + struct iterator + { + friend StreamProcessor; + + iterator& operator++(); + const std::vector& operator*(); + bool operator!=(const iterator& other); + + private: + iterator(IStream* stream, size_t blockSize); + iterator(); + + void ReadNextBytes(); + + ComPtr m_stream; + size_t m_blockSize; + std::vector m_bytes; + bool isEnd = false; + }; + + StreamProcessor(IStream* stream, size_t blockSize) : + m_stream(stream), m_blockSize(blockSize) {} + + iterator begin() + { + return { m_stream.Get(), m_blockSize }; + } + + iterator end() + { + return {}; + } + + private: + ComPtr m_stream; + size_t m_blockSize; + }; + // Reverts a stream's position on destruction struct StreamPositionReset { diff --git a/src/inc/internal/StringStream.hpp b/src/inc/internal/StringStream.hpp deleted file mode 100644 index 1ed76c176..000000000 --- a/src/inc/internal/StringStream.hpp +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright (C) 2019 Microsoft. All rights reserved. -// See LICENSE file in the project root for full license information. -// -#pragma once - -#include -#include - -#include - -namespace MSIX { - - class StringStream final : public StreamBase - { - public: - HRESULT STDMETHODCALLTYPE Read(void* buffer, ULONG countBytes, ULONG* bytesRead) noexcept override try - { - auto current = m_data.tellp(); - m_data.seekp(0, std::ios_base::end); - auto available = m_data.tellp() - current; - m_data.seekp(current); // rewind - auto buf = m_data.rdbuf(); - ULONG amountToRead = std::min(countBytes, static_cast(available)); - if (amountToRead > 0) - { - buf->sgetn(static_cast(buffer),amountToRead); - m_data.seekp(static_cast(amountToRead), std::ios_base::cur); - } - if (bytesRead) { *bytesRead = amountToRead; } - return static_cast(Error::OK); - } CATCH_RETURN(); - - HRESULT STDMETHODCALLTYPE Seek(LARGE_INTEGER move, DWORD origin, ULARGE_INTEGER *newPosition) noexcept override try - { - std::ios_base::seekdir dir; - switch (origin) - { - case Reference::CURRENT: - dir = std::ios_base::cur; - break; - case Reference::START: - dir = std::ios_base::beg; - break; - case Reference::END: - dir = std::ios_base::end; - break; - } - m_data.seekp(static_cast(move.QuadPart), dir); - ThrowErrorIf(Error::FileWrite, m_data.rdstate() != std::ios_base::goodbit, "StringStream Seek failed"); - if (newPosition) { newPosition->QuadPart = static_cast(m_data.tellp()); } - return static_cast(Error::OK); - } CATCH_RETURN(); - - HRESULT STDMETHODCALLTYPE Write(const void *buffer, ULONG countBytes, ULONG *bytesWritten) noexcept override try - { - if (bytesWritten) { *bytesWritten = 0; } - m_data.write(static_cast(buffer), static_cast(countBytes)); - // std::basic_ostream::write : Characters are inserted into the output sequence until one of the following occurs: - // exactly count characters are inserted or inserting into the output sequence fails (in which case setstate(badbit) is called) - // If the state is std::ios_base::goodbit we know the exact number of bytes were written. - ThrowErrorIf(Error::FileWrite, m_data.rdstate() != std::ios_base::goodbit, "StringStream Write failed"); - if (bytesWritten) { *bytesWritten = countBytes; } - return static_cast(Error::OK); - } CATCH_RETURN(); - - protected: - std::stringstream m_data; - }; -} // namespace MSIX \ No newline at end of file diff --git a/src/inc/internal/XmlWriter.hpp b/src/inc/internal/XmlWriter.hpp index 42a5cc393..e4e059997 100644 --- a/src/inc/internal/XmlWriter.hpp +++ b/src/inc/internal/XmlWriter.hpp @@ -5,7 +5,7 @@ #pragma once #include "ComHelper.hpp" -#include "StringStream.hpp" +#include "MemoryStream.hpp" #include #include @@ -29,11 +29,14 @@ namespace MSIX { } State; - XmlWriter() = delete; // A root must be given + // Used for editing an existing XML stream; copies the given stream + // and moves the cursor back to before the end of the root element. + // Must call Initialize in order to get to the correct state. + XmlWriter() = default; XmlWriter(const std::string& root, bool standalone = false) { - m_stream = ComPtr::Make(); + m_stream = ComPtr::Make(); StartWrite(root, standalone); } @@ -42,6 +45,8 @@ namespace MSIX { StartWrite(root, standalone); } + void Initialize(const std::string& source, const std::string& root); + void StartElement(const std::string& name); void CloseElement(); void AddAttribute(const std::string& name, const std::string& value); diff --git a/src/inc/internal/ZipObject.hpp b/src/inc/internal/ZipObject.hpp index 9868d1ae2..043031cdb 100644 --- a/src/inc/internal/ZipObject.hpp +++ b/src/inc/internal/ZipObject.hpp @@ -4,464 +4,61 @@ // #pragma once -#include "Exceptions.hpp" #include "ComHelper.hpp" #include "StorageObject.hpp" -#include "AppxFactory.hpp" -#include "ObjectBase.hpp" +#include "ZipObjectComponents.hpp" -#include #include -#include - -namespace MSIX { - - enum class ZipVersions : std::uint16_t - { - Zip32DefaultVersion = 20, - Zip64FormatExtension = 45, - }; - - enum class GeneralPurposeBitFlags : std::uint16_t - { - UNSUPPORTED_0 = 0x0001, // Bit 0: If set, indicates that the file is encrypted. - - Deflate_MaxCompress = 0x0002, // Maximum compression (-exx/-ex), otherwise, normal compression (-en) - Deflate_FastCompress = 0x0004, // Fast (-ef), if Max+Fast then SuperFast (-es) compression - - DataDescriptor = 0x0008, // the field's crc-32 compressed and uncompressed sizes = 0 in the local header - // the correct values are put in the data descriptor immediately following the - // compressed data. - EnhancedDeflate = 0x0010, - CompressedPatchedData = 0x0020, - UNSUPPORTED_6 = 0x0040, // Strong encryption. - UnUsed_7 = 0x0080, // currently unused - UnUsed_8 = 0x0100, // currently unused - UnUsed_9 = 0x0200, // currently unused - UnUsed_10 = 0x0400, // currently unused - - EncodingMustUseUTF8 = 0x0800, // Language encoding flag (EFS). File name and comments fields MUST be encoded UTF-8 - - UNSUPPORTED_12 = 0x1000, // Reserved by PKWARE for enhanced compression - UNSUPPORTED_13 = 0x2000, // Set when encrypting the Central Directory - UNSUPPORTED_14 = 0x4000, // Reserved by PKWARE - UNSUPPORTED_15 = 0x8000, // Reserved by PKWARE - }; - - constexpr GeneralPurposeBitFlags operator &(GeneralPurposeBitFlags a, GeneralPurposeBitFlags b) - { - return static_cast(static_cast(a) & static_cast(b)); - } - - constexpr GeneralPurposeBitFlags operator |(GeneralPurposeBitFlags a, GeneralPurposeBitFlags b) - { - return static_cast(static_cast(a) | static_cast(b)); - } - - constexpr GeneralPurposeBitFlags operator ~(GeneralPurposeBitFlags a) - { - return static_cast(~static_cast(a)); - } - - enum class CompressionType : std::uint16_t - { - Store = 0, - Deflate = 8, - }; - - // from ZIP file format specification detailed in AppNote.txt - enum class Signatures : std::uint32_t - { - LocalFileHeader = 0x04034b50, - DataDescriptor = 0x08074b50, - CentralFileHeader = 0x02014b50, - Zip64EndOfCD = 0x06064b50, - Zip64EndOfCDLocator = 0x07064b50, - EndOfCentralDirectory = 0x06054b50, - }; - - constexpr uint64_t MaxSizeToNotUseDataDescriptor = static_cast(std::numeric_limits::max() - 1); - - template - inline bool IsValueInExtendedInfo(T value) noexcept - { - return (value == std::numeric_limits::max()); - } - - template - inline bool IsValueInExtendedInfo(const Meta::FieldBase& field) noexcept - { - return IsValueInExtendedInfo(field.get()); - } - - class Zip64ExtendedInformation final : public Meta::StructuredObject< - Meta::Field2Bytes, // 0 - tag for the "extra" block type 2 bytes(0x0001) - Meta::Field2Bytes, // 1 - size of this "extra" block 2 bytes - Meta::OptionalField8Bytes, // 2 - Original uncompressed file size 8 bytes - Meta::OptionalField8Bytes, // 3 - Compressed file size 8 bytes - Meta::OptionalField8Bytes, // 4 - Offset of local header record 8 bytes - Meta::OptionalField4Bytes // 5 - number of the disk on which the file starts 4 bytes - > - { - public: - Zip64ExtendedInformation(); - - // The incoming values are those from the central directory record. Their value there determines - // whether we attempt to read them here. - void Read(const ComPtr& stream, ULARGE_INTEGER start, uint32_t uncompressedSize, uint32_t compressedSize, uint32_t offset, uint16_t disk); - - std::uint64_t GetUncompressedSize() const { return Field<2>(); } - std::uint64_t GetCompressedSize() const { return Field<3>(); } - std::uint64_t GetRelativeOffsetOfLocalHeader() const { return Field<4>(); } - std::uint32_t GetDiskStartNumber() const { return Field<5>(); } - - void SetUncompressedSize(std::uint64_t value) noexcept { Field<2>() = value; } - void SetCompressedSize(std::uint64_t value) noexcept { Field<3>() = value; } - void SetRelativeOffsetOfLocalHeader(std::uint64_t value) noexcept { Field<4>() = value; } - - bool HasAnySet() const - { - return (Field<2>() || Field<3>() || Field<4>() || Field<5>()); - } - - std::vector GetBytes() - { - SetSize(static_cast(Size() - NonOptionalSize)); - return StructuredObject::GetBytes(); - } - - protected: - constexpr static size_t NonOptionalSize = 4; - - void SetSignature(std::uint16_t value) noexcept { Field<0>() = value; } - void SetSize(std::uint16_t value) noexcept { Field<1>() = value; } - }; - - class CentralDirectoryFileHeader final : public Meta::StructuredObject< - Meta::Field4Bytes, // 0 - central file header signature 4 bytes(0x02014b50) - Meta::Field2Bytes, // 1 - version made by 2 bytes - Meta::Field2Bytes, // 2 - version needed to extract 2 bytes - Meta::Field2Bytes, // 3 - general purpose bit flag 2 bytes - Meta::Field2Bytes, // 4 - compression method 2 bytes - Meta::Field2Bytes, // 5 - last mod file time 2 bytes - Meta::Field2Bytes, // 6 - last mod file date 2 bytes - Meta::Field4Bytes, // 7 - crc - 32 4 bytes - Meta::Field4Bytes, // 8 - compressed size 4 bytes - Meta::Field4Bytes, // 9 - uncompressed size 4 bytes - Meta::Field2Bytes, //10 - file name length 2 bytes - Meta::Field2Bytes, //11 - extra field length 2 bytes - Meta::Field2Bytes, //12 - file comment length 2 bytes - Meta::Field2Bytes, //13 - disk number start 2 bytes - Meta::Field2Bytes, //14 - internal file attributes 2 bytes - Meta::Field4Bytes, //15 - external file attributes 4 bytes - Meta::Field4Bytes, //16 - relative offset of local header 4 bytes - Meta::FieldNBytes, //17 - file name(variable size) - Meta::FieldNBytes, //18 - extra field(variable size) - Meta::FieldNBytes //19 - file comment(variable size) NOT USED - > - { - public: - CentralDirectoryFileHeader(); - - void SetData(const std::string& name, std::uint32_t crc, std::uint64_t compressedSize, - std::uint64_t uncompressedSize, std::uint64_t relativeOffset, std::uint16_t compressionMethod, bool forceDataDescriptor); - - void Read(const ComPtr& stream, bool isZip64); - - GeneralPurposeBitFlags GetGeneralPurposeBitFlags() const noexcept { return static_cast(Field<3>().get()); } - - bool IsGeneralPurposeBitSet() const noexcept - { - return ((GetGeneralPurposeBitFlags() & GeneralPurposeBitFlags::DataDescriptor) == GeneralPurposeBitFlags::DataDescriptor); - } - - CompressionType GetCompressionMethod() const noexcept { return static_cast(Field<4>().get()); } - - std::uint64_t GetCompressedSize() const noexcept - { - if (IsValueInExtendedInfo(Field<8>())) - { - return m_extendedInfo.GetCompressedSize(); - } - return static_cast(Field<8>().get()); - } - - std::uint64_t GetUncompressedSize() const noexcept - { - if (IsValueInExtendedInfo(Field<9>())) - { - return m_extendedInfo.GetUncompressedSize(); - } - return static_cast(Field<9>().get()); - } - - std::uint64_t GetRelativeOffsetOfLocalHeader() const noexcept - { - if (IsValueInExtendedInfo(Field<16>())) - { - return m_extendedInfo.GetRelativeOffsetOfLocalHeader(); - - } - return static_cast(Field<16>().get()); - } - - std::string GetFileName() const - { - auto data = Field<17>().get(); - return std::string(data.begin(), data.end()); - } - - protected: - void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } - void SetVersionMadeBy(std::uint16_t value) noexcept { Field<1>() = value; } - void SetVersionNeededToExtract(std::uint16_t value) noexcept { Field<2>() = value; } - void SetGeneralPurposeBitFlags(std::uint16_t value) noexcept { Field<3>() = value; } - void SetCompressionMethod(std::uint16_t value) noexcept { Field<4>() = value; } - void SetLastModFileTime(std::uint16_t value) noexcept { Field<5>() = value; } - void SetLastModFileDate(std::uint16_t value) noexcept { Field<6>() = value; } - void SetCrc(std::uint32_t value) noexcept { Field<7>() = value; } - void SetFileNameLength(std::uint16_t value) noexcept { Field<10>() = value; } - void SetExtraFieldLength(std::uint16_t value) noexcept { Field<11>() = value; } - void SetFileCommentLength(std::uint16_t value) noexcept { Field<12>() = value; } - void SetDiskNumberStart(std::uint16_t value) noexcept { Field<13>() = value; } - void SetInternalFileAttributes(std::uint16_t value) noexcept { Field<14>() = value; } - void SetExternalFileAttributes(std::uint16_t value) noexcept { Field<15>() = value; } - - // Values that might appear in the extended info (minus disk, which we will never set there) - void SetCompressedSize(std::uint64_t value) noexcept - { - if (value > MaxSizeToNotUseDataDescriptor) - { - m_extendedInfo.SetCompressedSize(value); - Field<8>() = std::numeric_limits::max(); - } - else - { - Field<8>() = static_cast(value); - } - } - - void SetUncompressedSize(std::uint64_t value)noexcept - { - if (value > MaxSizeToNotUseDataDescriptor) - { - m_extendedInfo.SetUncompressedSize(value); - Field<9>() = std::numeric_limits::max(); - } - else - { - Field<9>() = static_cast(value); - } - } - - void SetRelativeOffsetOfLocalHeader(std::uint64_t value) noexcept - { - if (value > MaxSizeToNotUseDataDescriptor) - { - m_extendedInfo.SetRelativeOffsetOfLocalHeader(value); - Field<16>() = std::numeric_limits::max(); - } - else - { - Field<16>() = static_cast(value); - } - } - - void SetFileName(const std::string& name) - { - SetFileNameLength(static_cast(name.size())); - Field<17>().get().resize(name.size(), 0); - std::copy(name.begin(), name.end(), Field<17>().get().begin()); - } - - void UpdateExtraField() - { - if (m_extendedInfo.HasAnySet()) - { - SetVersionNeededToExtract(static_cast(ZipVersions::Zip64FormatExtension)); - SetExtraFieldLength(static_cast(m_extendedInfo.Size())); - Field<18>().get() = m_extendedInfo.GetBytes(); - } - } - - Zip64ExtendedInformation m_extendedInfo; - bool m_isZip64 = true; - }; - - class LocalFileHeader final : public Meta::StructuredObject< - Meta::Field4Bytes, // 0 - local file header signature 4 bytes(0x04034b50) - Meta::Field2Bytes, // 1 - version needed to extract 2 bytes - Meta::Field2Bytes, // 2 - general purpose bit flag 2 bytes - Meta::Field2Bytes, // 3 - compression method 2 bytes - Meta::Field2Bytes, // 4 - last mod file time 2 bytes - Meta::Field2Bytes, // 5 - last mod file date 2 bytes - Meta::Field4Bytes, // 6 - crc - 32 4 bytes - Meta::Field4Bytes, // 7 - compressed size 4 bytes - Meta::Field4Bytes, // 8 - uncompressed size 4 bytes - Meta::Field2Bytes, // 9 - file name length 2 bytes - Meta::Field2Bytes, // 10- extra field length 2 bytes - Meta::FieldNBytes, // 11- file name (variable size) - Meta::FieldNBytes // 12- extra field (variable size) NOT USED - > - { - public: - LocalFileHeader(); - - void SetData(const std::string& name, bool isCompressed); - void SetData(std::uint32_t crc, std::uint64_t compressedSize, std::uint64_t uncompressedSize); - - void Read(const ComPtr& stream, CentralDirectoryFileHeader& directoryEntry); - - GeneralPurposeBitFlags GetGeneralPurposeBitFlags() const noexcept { return static_cast(Field<2>().get()); } - std::uint16_t GetCompressionMethod() const noexcept { return Field<3>(); } - std::uint16_t GetFileNameLength() const noexcept { return Field<9>(); } - std::string GetFileName() const - { - auto data = Field<11>().get(); - return std::string(data.begin(), data.end()); - } - - protected: - bool IsGeneralPurposeBitSet() const noexcept - { - return ((GetGeneralPurposeBitFlags() & GeneralPurposeBitFlags::DataDescriptor) == GeneralPurposeBitFlags::DataDescriptor); - } - - void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } - void SetVersionNeededToExtract(std::uint16_t value) noexcept { Field<1>() = value; } - void SetGeneralPurposeBitFlags(std::uint16_t value) noexcept { Field<2>() = value; } - void SetCompressionMethod(std::uint16_t value) noexcept { Field<3>() = value; } - void SetLastModFileTime(std::uint16_t value) noexcept { Field<4>() = value; } - void SetLastModFileDate(std::uint16_t value) noexcept { Field<5>() = value; } - void SetCrc(std::uint32_t value) noexcept { Field<6>() = value; } - void SetCompressedSize(std::uint32_t value) noexcept { Field<7>() = value; } - void SetUncompressedSize(std::uint32_t value) noexcept { Field<8>() = value; } - void SetFileNameLength(std::uint16_t value) noexcept { Field<9>() = value; } - void SetExtraFieldLength(std::uint16_t value) noexcept { Field<10>() = value; } - void SetFileName(const std::string& name) - { - SetFileNameLength(static_cast(name.size())); - Field<11>().get().resize(name.size(), 0); - std::copy(name.begin(), name.end(), Field<11>().get().begin()); - } - }; - - class Zip64EndOfCentralDirectoryRecord final : public Meta::StructuredObject< - Meta::Field4Bytes, // 0 - zip64 end of central dir signature 4 bytes(0x06064b50) - Meta::Field8Bytes, // 1 - size of zip64 end of central directory record 8 bytes - Meta::Field2Bytes, // 2 - version made by 2 bytes - Meta::Field2Bytes, // 3 - version needed to extract 2 bytes - Meta::Field4Bytes, // 4 - number of this disk 4 bytes - Meta::Field4Bytes, // 5 - number of the disk with the start of the central directory 4 bytes - Meta::Field8Bytes, // 6 - total number of entries in the central directory on this disk 8 bytes - Meta::Field8Bytes, // 7 - total number of entries in the central directory 8 bytes - Meta::Field8Bytes, // 8 - size of the central directory 8 bytes - Meta::Field8Bytes, // 9 - offset of start of central directory with respect to the - // starting disk number 8 bytes - Meta::FieldNBytes //10 - zip64 extensible data sector (variable size) NOT USED - > - { - public: - Zip64EndOfCentralDirectoryRecord(); - - void SetData(std::uint64_t numCentralDirs, std::uint64_t sizeCentralDir, std::uint64_t offsetStartCentralDirectory); - - void Read(const ComPtr& stream); - - std::uint64_t GetTotalNumberOfEntries() const noexcept { return Field<6>(); } - std::uint64_t GetOffsetStartOfCD() const noexcept { return Field<9>(); } - - protected: - void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } - void SetSizeOfZip64CDRecord(std::uint64_t value) noexcept { Field<1>() = value; } - void SetVersionMadeBy(std::uint16_t value) noexcept { Field<2>() = value; } - void SetVersionNeededToExtract(std::uint16_t value) noexcept { Field<3>() = value; } - void SetNumberOfThisDisk(std::uint32_t value) noexcept { Field<4>() = value; } - void SetNumberOfTheDiskWithStartOfCD(std::uint32_t value) noexcept { Field<5>() = value; } - void SetTotalNumberOfEntriesDisk(std::uint64_t value) noexcept - { - Field<6>() = value; - Field<7>() = value; - } - void SetSizeOfCD(std::uint64_t value) noexcept { Field<8>() = value; } - void SetOffsetfStartOfCD(std::uint64_t value) noexcept { Field<9>() = value; } - }; - - class Zip64EndOfCentralDirectoryLocator final : public Meta::StructuredObject< - Meta::Field4Bytes, // 0 - zip64 end of central dir locator signature 4 bytes(0x07064b50) - Meta::Field4Bytes, // 1 - number of the disk with the start of the zip64 - // end of central directory 4 bytes - Meta::Field8Bytes, // 2 - relative offset of the zip64 end of central - // directory record 8 bytes - Meta::Field4Bytes // 3 - total number of disks 4 bytes - > - { - public: - Zip64EndOfCentralDirectoryLocator(); - - void SetData(std::uint64_t zip64EndCdrOffset); - - void Read(const ComPtr& stream); - - std::uint64_t GetRelativeOffset() const noexcept { return Field<2>(); } - - protected: - void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } - void SetNumberOfDisk(std::uint32_t value) noexcept { Field<1>() = value; } - void SetRelativeOffset(std::uint64_t value) noexcept { Field<2>() = value; } - void SetTotalNumberOfDisks(std::uint32_t value) noexcept { Field<3>() = value; } - }; - - class EndCentralDirectoryRecord final : public Meta::StructuredObject< - Meta::Field4Bytes, // 0 - end of central dir signature 4 bytes (0x06054b50) - Meta::Field2Bytes, // 1 - number of this disk 2 bytes - Meta::Field2Bytes, // 2 - number of the disk with the start of the - // central directory 2 bytes - Meta::Field2Bytes, // 3 - total number of entries in the central - // directory on this disk 2 bytes - Meta::Field2Bytes, // 4 - total number of entries in the central - // directory 2 bytes - Meta::Field4Bytes, // 5 - size of the central directory 4 bytes - Meta::Field4Bytes, // 6 - offset of start of central directory with - // respect to the starting disk number 4 bytes - Meta::Field2Bytes, // 7 - .ZIP file comment length 2 bytes - Meta::FieldNBytes // 8 - .ZIP file comment (variable size) - > - { - public: - EndCentralDirectoryRecord(); - - void Read(const ComPtr& stream); - - bool GetIsZip64() const noexcept { return m_isZip64; } - - std::uint64_t GetNumberOfCentralDirectoryEntries() noexcept { return static_cast(Field<3>().get()); } - std::uint64_t GetStartOfCentralDirectory() noexcept { return static_cast(Field<6>().get()); } - - protected: - void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } - void SetNumberOfDisk(std::uint16_t value) noexcept { Field<1>() = value; } - void SetDiskStart(std::uint16_t value) noexcept { Field<2>() = value; } - void SetTotalNumberOfEntries(std::uint16_t value) noexcept { Field<3>() = value; } - void SetTotalEntriesInCentralDirectory(std::uint16_t value) noexcept { Field<4>() = value; } - void SetSizeOfCentralDirectory(std::uint32_t value) noexcept { Field<5>() = value; } - void SetOffsetOfCentralDirectory(std::uint32_t value) noexcept { Field<6>() = value; } - void SetCommentLength(std::uint16_t value) noexcept { Field<7>() = value; } +#include +#include - bool m_isZip64 = true; - }; +// internal interface +// {986355bc-4e9c-413b-8b2b-72c9aa3a594d} +#ifndef WIN32 +interface IZipObject : public IUnknown +#else +#include "Unknwn.h" +#include "Objidl.h" +class IZipObject : public IUnknown +#endif +{ +public: + // TODO: Try to make these more functional rather than publicizing the internals + virtual MSIX::ComPtr GetStream() = 0; + virtual MSIX::EndCentralDirectoryRecord& GetEndCentralDirectoryRecord() = 0; + virtual MSIX::Zip64EndOfCentralDirectoryLocator& GetZip64Locator() = 0; + virtual MSIX::Zip64EndOfCentralDirectoryRecord& GetZip64EndOfCentralDirectory() = 0; + virtual std::vector>& GetCentralDirectories() = 0; + virtual MSIX::ComPtr GetEntireZipFileStream(const std::string& fileName) = 0; +}; +MSIX_INTERFACE(IZipObject, 0x986355bc, 0x4e9c, 0x413b, 0x8b, 0x2b, 0x72, 0xc9, 0xaa, 0x3a, 0x59, 0x4d); - class ZipObject +namespace MSIX { + // This represents a raw stream over a.zip file. + class ZipObject final : public ComClass { public: - ZipObject(const ComPtr& stream) : m_stream(stream) {} - ZipObject(const ComPtr& storageObject); - - protected: + ZipObject(IStream* stream, bool readStream = true); + + // IStorageObject methods + std::vector GetFileNames(FileNameOptions options) override; + ComPtr GetFile(const std::string& fileName) override; + std::string GetFileName() override; + + // IZipObject + ComPtr GetStream() override; + MSIX::EndCentralDirectoryRecord& GetEndCentralDirectoryRecord() override; + MSIX::Zip64EndOfCentralDirectoryLocator& GetZip64Locator() override; + MSIX::Zip64EndOfCentralDirectoryRecord& GetZip64EndOfCentralDirectory() override; + std::vector>& GetCentralDirectories() override; + MSIX::ComPtr GetEntireZipFileStream(const std::string& fileName) override; + + private: + ComPtr m_stream; EndCentralDirectoryRecord m_endCentralDirectoryRecord; Zip64EndOfCentralDirectoryLocator m_zip64Locator; Zip64EndOfCentralDirectoryRecord m_zip64EndOfCentralDirectory; - std::map m_centralDirectories; - ComPtr m_stream; + std::vector> m_centralDirectories; + std::map> m_streams; }; } diff --git a/src/inc/internal/ZipObjectComponents.hpp b/src/inc/internal/ZipObjectComponents.hpp new file mode 100644 index 000000000..ecc51f4db --- /dev/null +++ b/src/inc/internal/ZipObjectComponents.hpp @@ -0,0 +1,472 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once + +#include "Exceptions.hpp" +#include "ComHelper.hpp" +#include "ObjectBase.hpp" + +#include + +namespace MSIX { + + enum class ZipVersions : std::uint16_t + { + Zip32DefaultVersion = 20, + Zip64FormatExtension = 45, + }; + + enum class GeneralPurposeBitFlags : std::uint16_t + { + UNSUPPORTED_0 = 0x0001, // Bit 0: If set, indicates that the file is encrypted. + + Deflate_MaxCompress = 0x0002, // Maximum compression (-exx/-ex), otherwise, normal compression (-en) + Deflate_FastCompress = 0x0004, // Fast (-ef), if Max+Fast then SuperFast (-es) compression + + DataDescriptor = 0x0008, // the field's crc-32 compressed and uncompressed sizes = 0 in the local header + // the correct values are put in the data descriptor immediately following the + // compressed data. + EnhancedDeflate = 0x0010, + CompressedPatchedData = 0x0020, + UNSUPPORTED_6 = 0x0040, // Strong encryption. + UnUsed_7 = 0x0080, // currently unused + UnUsed_8 = 0x0100, // currently unused + UnUsed_9 = 0x0200, // currently unused + UnUsed_10 = 0x0400, // currently unused + + EncodingMustUseUTF8 = 0x0800, // Language encoding flag (EFS). File name and comments fields MUST be encoded UTF-8 + + UNSUPPORTED_12 = 0x1000, // Reserved by PKWARE for enhanced compression + UNSUPPORTED_13 = 0x2000, // Set when encrypting the Central Directory + UNSUPPORTED_14 = 0x4000, // Reserved by PKWARE + UNSUPPORTED_15 = 0x8000, // Reserved by PKWARE + }; + + constexpr GeneralPurposeBitFlags operator &(GeneralPurposeBitFlags a, GeneralPurposeBitFlags b) + { + return static_cast(static_cast(a) & static_cast(b)); + } + + constexpr GeneralPurposeBitFlags operator |(GeneralPurposeBitFlags a, GeneralPurposeBitFlags b) + { + return static_cast(static_cast(a) | static_cast(b)); + } + + constexpr GeneralPurposeBitFlags operator ~(GeneralPurposeBitFlags a) + { + return static_cast(~static_cast(a)); + } + + enum class CompressionType : std::uint16_t + { + Store = 0, + Deflate = 8, + }; + + // from ZIP file format specification detailed in AppNote.txt + enum class Signatures : std::uint32_t + { + LocalFileHeader = 0x04034b50, + DataDescriptor = 0x08074b50, + CentralFileHeader = 0x02014b50, + Zip64EndOfCD = 0x06064b50, + Zip64EndOfCDLocator = 0x07064b50, + EndOfCentralDirectory = 0x06054b50, + }; + + constexpr uint64_t MaxSizeToNotUseDataDescriptor = static_cast(std::numeric_limits::max() - 1); + + template + inline bool IsValueInExtendedInfo(T value) noexcept + { + return (value == std::numeric_limits::max()); + } + + template + inline bool IsValueInExtendedInfo(const Meta::FieldBase& field) noexcept + { + return IsValueInExtendedInfo(field.get()); + } + + class Zip64ExtendedInformation final : public Meta::StructuredObject< + Meta::Field2Bytes, // 0 - tag for the "extra" block type 2 bytes(0x0001) + Meta::Field2Bytes, // 1 - size of this "extra" block 2 bytes + Meta::OptionalField8Bytes, // 2 - Original uncompressed file size 8 bytes + Meta::OptionalField8Bytes, // 3 - Compressed file size 8 bytes + Meta::OptionalField8Bytes, // 4 - Offset of local header record 8 bytes + Meta::OptionalField4Bytes // 5 - number of the disk on which the file starts 4 bytes + > + { + public: + Zip64ExtendedInformation(); + + // The incoming values are those from the central directory record. Their value there determines + // whether we attempt to read them here. + void Read(const ComPtr& stream, ULARGE_INTEGER start, uint32_t uncompressedSize, uint32_t compressedSize, uint32_t offset, uint16_t disk); + + std::uint64_t GetUncompressedSize() const { return Field<2>(); } + std::uint64_t GetCompressedSize() const { return Field<3>(); } + std::uint64_t GetRelativeOffsetOfLocalHeader() const { return Field<4>(); } + std::uint32_t GetDiskStartNumber() const { return Field<5>(); } + + void SetUncompressedSize(std::uint64_t value) noexcept { Field<2>() = value; } + void SetCompressedSize(std::uint64_t value) noexcept { Field<3>() = value; } + void SetRelativeOffsetOfLocalHeader(std::uint64_t value) noexcept { Field<4>() = value; } + + bool HasAnySet() const + { + return (Field<2>() || Field<3>() || Field<4>() || Field<5>()); + } + + std::vector GetBytes() + { + SetSize(static_cast(Size() - NonOptionalSize)); + return StructuredObject::GetBytes(); + } + + protected: + constexpr static size_t NonOptionalSize = 4; + + void SetSignature(std::uint16_t value) noexcept { Field<0>() = value; } + void SetSize(std::uint16_t value) noexcept { Field<1>() = value; } + }; + + class CentralDirectoryFileHeader final : public Meta::StructuredObject< + Meta::Field4Bytes, // 0 - central file header signature 4 bytes(0x02014b50) + Meta::Field2Bytes, // 1 - version made by 2 bytes + Meta::Field2Bytes, // 2 - version needed to extract 2 bytes + Meta::Field2Bytes, // 3 - general purpose bit flag 2 bytes + Meta::Field2Bytes, // 4 - compression method 2 bytes + Meta::Field2Bytes, // 5 - last mod file time 2 bytes + Meta::Field2Bytes, // 6 - last mod file date 2 bytes + Meta::Field4Bytes, // 7 - crc - 32 4 bytes + Meta::Field4Bytes, // 8 - compressed size 4 bytes + Meta::Field4Bytes, // 9 - uncompressed size 4 bytes + Meta::Field2Bytes, //10 - file name length 2 bytes + Meta::Field2Bytes, //11 - extra field length 2 bytes + Meta::Field2Bytes, //12 - file comment length 2 bytes + Meta::Field2Bytes, //13 - disk number start 2 bytes + Meta::Field2Bytes, //14 - internal file attributes 2 bytes + Meta::Field4Bytes, //15 - external file attributes 4 bytes + Meta::Field4Bytes, //16 - relative offset of local header 4 bytes + Meta::FieldNBytes, //17 - file name(variable size) + Meta::FieldNBytes, //18 - extra field(variable size) + Meta::FieldNBytes //19 - file comment(variable size) NOT USED + > + { + public: + CentralDirectoryFileHeader(); + + void SetData(const std::string& name, std::uint32_t crc, std::uint64_t compressedSize, + std::uint64_t uncompressedSize, std::uint64_t relativeOffset, std::uint16_t compressionMethod, bool forceDataDescriptor); + + void Read(const ComPtr& stream, bool isZip64); + + GeneralPurposeBitFlags GetGeneralPurposeBitFlags() const noexcept { return static_cast(Field<3>().get()); } + + bool IsDataDescriptorBitSet() const noexcept + { + return ((GetGeneralPurposeBitFlags() & GeneralPurposeBitFlags::DataDescriptor) == GeneralPurposeBitFlags::DataDescriptor); + } + + CompressionType GetCompressionMethod() const noexcept { return static_cast(Field<4>().get()); } + + std::uint64_t GetCompressedSize() const noexcept + { + if (IsValueInExtendedInfo(Field<8>())) + { + return m_extendedInfo.GetCompressedSize(); + } + return static_cast(Field<8>().get()); + } + + std::uint64_t GetUncompressedSize() const noexcept + { + if (IsValueInExtendedInfo(Field<9>())) + { + return m_extendedInfo.GetUncompressedSize(); + } + return static_cast(Field<9>().get()); + } + + std::uint64_t GetRelativeOffsetOfLocalHeader() const noexcept + { + if (IsValueInExtendedInfo(Field<16>())) + { + return m_extendedInfo.GetRelativeOffsetOfLocalHeader(); + + } + return static_cast(Field<16>().get()); + } + + std::string GetFileName() const + { + auto data = Field<17>().get(); + return std::string(data.begin(), data.end()); + } + + protected: + void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } + void SetVersionMadeBy(std::uint16_t value) noexcept { Field<1>() = value; } + void SetVersionNeededToExtract(std::uint16_t value) noexcept { Field<2>() = value; } + void SetGeneralPurposeBitFlags(std::uint16_t value) noexcept { Field<3>() = value; } + void SetCompressionMethod(std::uint16_t value) noexcept { Field<4>() = value; } + void SetLastModFileTime(std::uint16_t value) noexcept { Field<5>() = value; } + void SetLastModFileDate(std::uint16_t value) noexcept { Field<6>() = value; } + void SetCrc(std::uint32_t value) noexcept { Field<7>() = value; } + void SetFileNameLength(std::uint16_t value) noexcept { Field<10>() = value; } + void SetExtraFieldLength(std::uint16_t value) noexcept { Field<11>() = value; } + void SetFileCommentLength(std::uint16_t value) noexcept { Field<12>() = value; } + void SetDiskNumberStart(std::uint16_t value) noexcept { Field<13>() = value; } + void SetInternalFileAttributes(std::uint16_t value) noexcept { Field<14>() = value; } + void SetExternalFileAttributes(std::uint16_t value) noexcept { Field<15>() = value; } + + // Values that might appear in the extended info (minus disk, which we will never set there) + void SetCompressedSize(std::uint64_t value) noexcept + { + if (value > MaxSizeToNotUseDataDescriptor) + { + m_extendedInfo.SetCompressedSize(value); + Field<8>() = std::numeric_limits::max(); + } + else + { + Field<8>() = static_cast(value); + } + } + + void SetUncompressedSize(std::uint64_t value)noexcept + { + if (value > MaxSizeToNotUseDataDescriptor) + { + m_extendedInfo.SetUncompressedSize(value); + Field<9>() = std::numeric_limits::max(); + } + else + { + Field<9>() = static_cast(value); + } + } + + void SetRelativeOffsetOfLocalHeader(std::uint64_t value) noexcept + { + if (value > MaxSizeToNotUseDataDescriptor) + { + m_extendedInfo.SetRelativeOffsetOfLocalHeader(value); + Field<16>() = std::numeric_limits::max(); + } + else + { + Field<16>() = static_cast(value); + } + } + + void SetFileName(const std::string& name) + { + SetFileNameLength(static_cast(name.size())); + Field<17>().get().resize(name.size(), 0); + std::copy(name.begin(), name.end(), Field<17>().get().begin()); + } + + void UpdateExtraField() + { + if (m_extendedInfo.HasAnySet()) + { + SetVersionNeededToExtract(static_cast(ZipVersions::Zip64FormatExtension)); + SetExtraFieldLength(static_cast(m_extendedInfo.Size())); + Field<18>().get() = m_extendedInfo.GetBytes(); + } + } + + Zip64ExtendedInformation m_extendedInfo; + bool m_isZip64 = true; + }; + + class LocalFileHeader final : public Meta::StructuredObject< + Meta::Field4Bytes, // 0 - local file header signature 4 bytes(0x04034b50) + Meta::Field2Bytes, // 1 - version needed to extract 2 bytes + Meta::Field2Bytes, // 2 - general purpose bit flag 2 bytes + Meta::Field2Bytes, // 3 - compression method 2 bytes + Meta::Field2Bytes, // 4 - last mod file time 2 bytes + Meta::Field2Bytes, // 5 - last mod file date 2 bytes + Meta::Field4Bytes, // 6 - crc - 32 4 bytes + Meta::Field4Bytes, // 7 - compressed size 4 bytes + Meta::Field4Bytes, // 8 - uncompressed size 4 bytes + Meta::Field2Bytes, // 9 - file name length 2 bytes + Meta::Field2Bytes, // 10- extra field length 2 bytes + Meta::FieldNBytes, // 11- file name (variable size) + Meta::FieldNBytes // 12- extra field (variable size) NOT USED + > + { + public: + LocalFileHeader(); + + void SetData(const std::string& name, bool isCompressed); + void SetData(std::uint32_t crc, std::uint64_t compressedSize, std::uint64_t uncompressedSize); + + void Read(const ComPtr& stream, CentralDirectoryFileHeader& directoryEntry); + + GeneralPurposeBitFlags GetGeneralPurposeBitFlags() const noexcept { return static_cast(Field<2>().get()); } + bool IsDataDescriptorBitSet() const noexcept + { + return ((GetGeneralPurposeBitFlags() & GeneralPurposeBitFlags::DataDescriptor) == GeneralPurposeBitFlags::DataDescriptor); + } + + std::uint16_t GetCompressionMethod() const noexcept { return Field<3>(); } + std::uint16_t GetFileNameLength() const noexcept { return Field<9>(); } + std::string GetFileName() const + { + auto data = Field<11>().get(); + return std::string(data.begin(), data.end()); + } + + protected: + void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } + void SetVersionNeededToExtract(std::uint16_t value) noexcept { Field<1>() = value; } + void SetGeneralPurposeBitFlags(std::uint16_t value) noexcept { Field<2>() = value; } + void SetCompressionMethod(std::uint16_t value) noexcept { Field<3>() = value; } + void SetLastModFileTime(std::uint16_t value) noexcept { Field<4>() = value; } + void SetLastModFileDate(std::uint16_t value) noexcept { Field<5>() = value; } + void SetCrc(std::uint32_t value) noexcept { Field<6>() = value; } + void SetCompressedSize(std::uint32_t value) noexcept { Field<7>() = value; } + void SetUncompressedSize(std::uint32_t value) noexcept { Field<8>() = value; } + void SetFileNameLength(std::uint16_t value) noexcept { Field<9>() = value; } + void SetExtraFieldLength(std::uint16_t value) noexcept { Field<10>() = value; } + void SetFileName(const std::string& name) + { + SetFileNameLength(static_cast(name.size())); + Field<11>().get().resize(name.size(), 0); + std::copy(name.begin(), name.end(), Field<11>().get().begin()); + } + }; + + // Data descriptor field the comes after the file stream when DataDescriptor bit is set. + class DataDescriptor final : public Meta::StructuredObject< + Meta::Field4Bytes, // 0 - data descriptor header signature 4 bytes(0x08074b50) + Meta::Field4Bytes, // 1 - crc -32 4 bytes + Meta::Field8Bytes, // 2 - compressed size 8 bytes(zip64) + Meta::Field8Bytes // 3 - uncompressed size 8 bytes(zip64) + > + { + public: + // Used only to get the Size + DataDescriptor() + { + } + + DataDescriptor(std::uint32_t crc, std::uint64_t compressSize, std::uint64_t uncompressSize) + { + Field<0>() = static_cast(Signatures::DataDescriptor); + Field<1>() = crc; + Field<2>() = compressSize; + Field<3>() = uncompressSize; + } + }; + + class Zip64EndOfCentralDirectoryRecord final : public Meta::StructuredObject< + Meta::Field4Bytes, // 0 - zip64 end of central dir signature 4 bytes(0x06064b50) + Meta::Field8Bytes, // 1 - size of zip64 end of central directory record 8 bytes + Meta::Field2Bytes, // 2 - version made by 2 bytes + Meta::Field2Bytes, // 3 - version needed to extract 2 bytes + Meta::Field4Bytes, // 4 - number of this disk 4 bytes + Meta::Field4Bytes, // 5 - number of the disk with the start of the central directory 4 bytes + Meta::Field8Bytes, // 6 - total number of entries in the central directory on this disk 8 bytes + Meta::Field8Bytes, // 7 - total number of entries in the central directory 8 bytes + Meta::Field8Bytes, // 8 - size of the central directory 8 bytes + Meta::Field8Bytes, // 9 - offset of start of central directory with respect to the + // starting disk number 8 bytes + Meta::FieldNBytes //10 - zip64 extensible data sector (variable size) NOT USED + > + { + public: + Zip64EndOfCentralDirectoryRecord(); + + void SetData(std::uint64_t numCentralDirs, std::uint64_t sizeCentralDir, std::uint64_t offsetStartCentralDirectory); + + void Read(const ComPtr& stream); + + std::uint64_t GetTotalNumberOfEntries() const noexcept { return Field<6>(); } + std::uint64_t GetOffsetStartOfCD() const noexcept { return Field<9>(); } + + protected: + void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } + void SetSizeOfZip64CDRecord(std::uint64_t value) noexcept { Field<1>() = value; } + void SetVersionMadeBy(std::uint16_t value) noexcept { Field<2>() = value; } + void SetVersionNeededToExtract(std::uint16_t value) noexcept { Field<3>() = value; } + void SetNumberOfThisDisk(std::uint32_t value) noexcept { Field<4>() = value; } + void SetNumberOfTheDiskWithStartOfCD(std::uint32_t value) noexcept { Field<5>() = value; } + void SetTotalNumberOfEntriesDisk(std::uint64_t value) noexcept + { + Field<6>() = value; + Field<7>() = value; + } + void SetSizeOfCD(std::uint64_t value) noexcept { Field<8>() = value; } + void SetOffsetfStartOfCD(std::uint64_t value) noexcept { Field<9>() = value; } + }; + + class Zip64EndOfCentralDirectoryLocator final : public Meta::StructuredObject< + Meta::Field4Bytes, // 0 - zip64 end of central dir locator signature 4 bytes(0x07064b50) + Meta::Field4Bytes, // 1 - number of the disk with the start of the zip64 + // end of central directory 4 bytes + Meta::Field8Bytes, // 2 - relative offset of the zip64 end of central + // directory record 8 bytes + Meta::Field4Bytes // 3 - total number of disks 4 bytes + > + { + public: + Zip64EndOfCentralDirectoryLocator(); + + void SetData(std::uint64_t zip64EndCdrOffset); + + void Read(const ComPtr& stream); + + std::uint64_t GetRelativeOffset() const noexcept { return Field<2>(); } + + protected: + void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } + void SetNumberOfDisk(std::uint32_t value) noexcept { Field<1>() = value; } + void SetRelativeOffset(std::uint64_t value) noexcept { Field<2>() = value; } + void SetTotalNumberOfDisks(std::uint32_t value) noexcept { Field<3>() = value; } + }; + + class EndCentralDirectoryRecord final : public Meta::StructuredObject< + Meta::Field4Bytes, // 0 - end of central dir signature 4 bytes (0x06054b50) + Meta::Field2Bytes, // 1 - number of this disk 2 bytes + Meta::Field2Bytes, // 2 - number of the disk with the start of the + // central directory 2 bytes + Meta::Field2Bytes, // 3 - total number of entries in the central + // directory on this disk 2 bytes + Meta::Field2Bytes, // 4 - total number of entries in the central + // directory 2 bytes + Meta::Field4Bytes, // 5 - size of the central directory 4 bytes + Meta::Field4Bytes, // 6 - offset of start of central directory with + // respect to the starting disk number 4 bytes + Meta::Field2Bytes, // 7 - .ZIP file comment length 2 bytes + Meta::FieldNBytes // 8 - .ZIP file comment (variable size) + > + { + public: + EndCentralDirectoryRecord(); + + void Read(const ComPtr& stream); + + bool GetIsZip64() const noexcept { return m_isZip64; } + + std::uint64_t GetNumberOfCentralDirectoryEntries() noexcept { return static_cast(Field<3>().get()); } + std::uint64_t GetStartOfCentralDirectory() noexcept { return static_cast(Field<6>().get()); } + + protected: + void SetSignature(std::uint32_t value) noexcept { Field<0>() = value; } + void SetNumberOfDisk(std::uint16_t value) noexcept { Field<1>() = value; } + void SetDiskStart(std::uint16_t value) noexcept { Field<2>() = value; } + void SetTotalNumberOfEntries(std::uint16_t value) noexcept { Field<3>() = value; } + void SetTotalEntriesInCentralDirectory(std::uint16_t value) noexcept { Field<4>() = value; } + void SetSizeOfCentralDirectory(std::uint32_t value) noexcept { Field<5>() = value; } + void SetOffsetOfCentralDirectory(std::uint32_t value) noexcept { Field<6>() = value; } + void SetCommentLength(std::uint16_t value) noexcept { Field<7>() = value; } + + bool m_isZip64 = true; + }; +} diff --git a/src/inc/internal/ZipObjectReader.hpp b/src/inc/internal/ZipObjectReader.hpp deleted file mode 100644 index abc833651..000000000 --- a/src/inc/internal/ZipObjectReader.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright (C) 2019 Microsoft. All rights reserved. -// See LICENSE file in the project root for full license information. -// -#pragma once - -#include "Exceptions.hpp" -#include "ComHelper.hpp" -#include "ZipObject.hpp" - -#include -#include -#include - -namespace MSIX { - // This represents a raw stream over a.zip file. - class ZipObjectReader final : public ComClass, ZipObject - { - public: - ZipObjectReader(const ComPtr& stream); - - // IStorageObject methods - std::vector GetFileNames(FileNameOptions options) override; - ComPtr GetFile(const std::string& fileName) override; - std::string GetFileName() override; - - protected: - std::map> m_streams; - }; -} diff --git a/src/inc/internal/ZipObjectWriter.hpp b/src/inc/internal/ZipObjectWriter.hpp index 33b35417b..33eba5066 100644 --- a/src/inc/internal/ZipObjectWriter.hpp +++ b/src/inc/internal/ZipObjectWriter.hpp @@ -9,10 +9,11 @@ #include "ComHelper.hpp" #include "ZipObject.hpp" -#include #include #include #include +#include +#include #include @@ -33,6 +34,11 @@ class IZipWriter : public IUnknown // to the central directories map virtual void EndFile(std::uint32_t crc, std::uint64_t compressedSize, std::uint64_t uncompressedSize, bool forceDataDescriptor) = 0; + virtual void RemoveFiles(const std::vector& files) = 0; + + // Writes the central directory to the given stream, as if it were closing. + virtual void WriteCentralDirectoryToStream(IStream* stream) = 0; + // Ends zip file by writing the central directory records, zip64 locator, // zip64 end of central directory and the end of central directories. virtual void Close() = 0; @@ -41,24 +47,29 @@ MSIX_INTERFACE(IZipWriter, 0x350dd671,0x0c40,0x4cd7,0x9a,0x5b,0x27,0x45,0x6d,0x6 namespace MSIX { - class ZipObjectWriter final : public ComClass, ZipObject + class ZipObjectWriter final : public ComClass { public: - ZipObjectWriter(const ComPtr& stream); + ZipObjectWriter(IStream* stream); - ZipObjectWriter(const ComPtr& storageObject); + ZipObjectWriter(IZipObject* zipObject); - // IStorage methods - std::vector GetFileNames(FileNameOptions options) override; - ComPtr GetFile(const std::string& fileName) override; - std::string GetFileName() override { NOTIMPLEMENTED }; + // IZipObject + ComPtr GetStream() override; + MSIX::EndCentralDirectoryRecord& GetEndCentralDirectoryRecord() override; + MSIX::Zip64EndOfCentralDirectoryLocator& GetZip64Locator() override; + MSIX::Zip64EndOfCentralDirectoryRecord& GetZip64EndOfCentralDirectory() override; + std::vector>& GetCentralDirectories() override; + MSIX::ComPtr GetEntireZipFileStream(const std::string& fileName) override; // IZipWriter std::pair> PrepareToAddFile(const std::string& name, bool isCompressed) override; void EndFile(std::uint32_t crc, std::uint64_t compressedSize, std::uint64_t uncompressedSize, bool forceDataDescriptor) override; + void RemoveFiles(const std::vector& files) override; + void WriteCentralDirectoryToStream(IStream* stream) override; void Close() override; - protected: + private: enum class State { ReadyForLfhOrClose, @@ -66,6 +77,7 @@ namespace MSIX { Closed, }; + ComPtr m_zipObject; State m_state = State::ReadyForLfhOrClose; std::pair m_lastLFH; std::vector m_fileNameSequence; diff --git a/src/inc/public/AppxPackaging.hpp b/src/inc/public/AppxPackaging.hpp index e3fa5257b..8a35a24dc 100644 --- a/src/inc/public/AppxPackaging.hpp +++ b/src/inc/public/AppxPackaging.hpp @@ -1703,6 +1703,10 @@ enum MSIX_APPLICABILITY_OPTIONS MSIX_APPLICABILITY_OPTION_SKIPLANGUAGE = 0x2, } MSIX_APPLICABILITY_OPTIONS; +#define MSIX_VALIDATION_NONE static_cast( \ + MSIX_VALIDATION_OPTION_SKIPSIGNATURE \ + ) + typedef /* [v1_enum] */ enum MSIX_BUNDLE_OPTIONS { @@ -1736,39 +1740,52 @@ enum MSIX_FACTORY_OPTIONS #define MSIX_APPLICABILITY_NONE MSIX_APPLICABILITY_OPTION_SKIPPLATFORM | \ MSIX_APPLICABILITY_OPTION_SKIPLANGUAGE \ +typedef /* [v1_enum] */ +enum MSIX_SIGNING_OPTIONS + { + MSIX_SIGNING_OPTIONS_NONE = 0x0, + } MSIX_SIGNING_OPTIONS; + +typedef /* [v1_enum] */ +enum MSIX_CERTIFICATE_FORMAT + { + MSIX_CERTIFICATE_FORMAT_UNKNOWN = 0x0, + MSIX_CERTIFICATE_FORMAT_PFX = 0x1, + } MSIX_CERTIFICATE_FORMAT; + // Unpack MSIX_API HRESULT STDMETHODCALLTYPE UnpackPackage( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, - char* utf8SourcePackage, - char* utf8Destination + LPCSTR utf8SourcePackage, + LPCSTR utf8Destination ) noexcept; MSIX_API HRESULT STDMETHODCALLTYPE UnpackPackageFromPackageReader( MSIX_PACKUNPACK_OPTION packUnpackOptions, IAppxPackageReader* packageReader, - char* utf8Destination + LPCSTR utf8Destination ) noexcept; MSIX_API HRESULT STDMETHODCALLTYPE UnpackPackageFromStream( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, IStream* stream, - char* utf8Destination + LPCSTR utf8Destination ) noexcept; MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundle( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, MSIX_APPLICABILITY_OPTIONS applicabilityOptions, - char* utf8SourcePackage, - char* utf8Destination + LPCSTR utf8SourcePackage, + LPCSTR utf8Destination ) noexcept; MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundleFromBundleReader( MSIX_PACKUNPACK_OPTION packUnpackOptions, IAppxBundleReader* bundleReader, - char* utf8Destination + LPCSTR utf8Destination ) noexcept; MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundleFromStream( @@ -1776,7 +1793,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundleFromStream( MSIX_VALIDATION_OPTION validationOption, MSIX_APPLICABILITY_OPTIONS applicabilityOptions, IStream* stream, - char* utf8Destination + LPCSTR utf8Destination ) noexcept; #ifdef MSIX_PACK @@ -1784,8 +1801,17 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundleFromStream( MSIX_API HRESULT STDMETHODCALLTYPE PackPackage( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, - char* directoryPath, - char* outputPackage + LPCSTR directoryPath, + LPCSTR outputPackage +) noexcept; + +MSIX_API HRESULT STDMETHODCALLTYPE SignPackage( + MSIX_SIGNING_OPTIONS signingOptions, + LPCSTR package, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + LPCSTR signingCertificate, + LPCSTR pass, + LPCSTR privateKey ) noexcept; MSIX_API HRESULT STDMETHODCALLTYPE PackBundle( @@ -1852,7 +1878,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE CoCreateAppxBundleFactoryWithHeap( // provided as a helper for platforms that do not have an implementation of SHCreateStreamOnFileEx MSIX_API HRESULT STDMETHODCALLTYPE CreateStreamOnFile( - char* utf8File, + LPCSTR utf8File, bool forRead, IStream** stream) noexcept; @@ -1863,4 +1889,4 @@ MSIX_API HRESULT STDMETHODCALLTYPE CreateStreamOnFileUTF16( } // extern "C++" -#endif //__appxpackaging_hpp__ \ No newline at end of file +#endif //__appxpackaging_hpp__ diff --git a/src/inc/public/MsixErrors.hpp b/src/inc/public/MsixErrors.hpp index 19502eb58..7775f60aa 100644 --- a/src/inc/public/MsixErrors.hpp +++ b/src/inc/public/MsixErrors.hpp @@ -87,6 +87,9 @@ namespace MSIX { DeflateWrite = ERROR_FACILITY + 0x0082, DeflateRead = ERROR_FACILITY + 0x0083, + // Generic errors + SupportExcludedByBuild = ERROR_FACILITY + 0x0091, + // XML parsing errors XmlWarning = XML_FACILITY + 0x0001, XmlError = XML_FACILITY + 0x0002, diff --git a/src/inc/shared/ComHelper.hpp b/src/inc/shared/ComHelper.hpp index 9f1c35aad..17220da8f 100644 --- a/src/inc/shared/ComHelper.hpp +++ b/src/inc/shared/ComHelper.hpp @@ -154,10 +154,16 @@ namespace MSIX { template ComPtr As() const { - ComPtr out; - ThrowHrIfFailed(m_ptr->QueryInterface(UuidOfImpl::iid, reinterpret_cast(&out))); + return ComPtr::From(m_ptr); + } + + static ComPtr From(IUnknown* iunk) + { + ComPtr out; + ThrowHrIfFailed(iunk->QueryInterface(UuidOfImpl::iid, reinterpret_cast(&out))); return out; } + protected: T* m_ptr = nullptr; diff --git a/src/inc/shared/Exceptions.hpp b/src/inc/shared/Exceptions.hpp index fc045d02c..98131a6cb 100644 --- a/src/inc/shared/Exceptions.hpp +++ b/src/inc/shared/Exceptions.hpp @@ -26,6 +26,12 @@ namespace MSIX { } #endif +#ifdef WIN32 +#define MSIX_NOINLINE(rt) __declspec(noinline) rt +#else +#define MSIX_NOINLINE(rt) rt __attribute__((noinline)) +#endif + namespace MSIX { // Defines a common exception type to throw in exceptional cases. DO NOT USE FOR FLOW CONTROL! @@ -33,16 +39,16 @@ namespace MSIX { class Exception : public std::exception { public: - Exception(std::string& message, Error error) : + Exception(std::string message, Error error) : m_code(static_cast(error)), - m_message(message) + m_message(std::move(message)) { Global::Log::Append(Message()); } - Exception(std::string& message, HRESULT error) : + Exception(std::string message, HRESULT error) : m_code(error), - m_message(message) + m_message(std::move(message)) { Global::Log::Append(Message()); } @@ -58,9 +64,16 @@ namespace MSIX { class Win32Exception final : public Exception { public: - Win32Exception(std::string& message, DWORD error) : Exception(message, 0x80070000 + error) + Win32Exception(std::string message, DWORD error) : Exception(std::move(message), 0x80070000 + error) + { + } + }; + + class POSIXException final : public Exception + { + public: + POSIXException(std::string message, int error) : Exception(std::move(message), 0xA0070000 + error) { - Global::Log::Append(Message()); } }; @@ -112,14 +125,7 @@ namespace MSIX { throw E(message, c); } - #ifdef WIN32 - __declspec(noinline) - #endif - void - #ifndef WIN32 - __attribute__(( noinline)) - #endif - RaiseExceptionIfFailed(HRESULT hr, const int line, const char* const file); + MSIX_NOINLINE(void) RaiseExceptionIfFailed(HRESULT hr, const int line, const char* const file); } // Helper to make code more terse and more readable at the same time. @@ -127,6 +133,7 @@ namespace MSIX { #define UNEXPECTED { MSIX::RaiseException(__LINE__, __FILE__, nullptr, Error::Unexpected); } #define NOTSUPPORTED { MSIX::RaiseException(__LINE__, __FILE__, nullptr, Error::NotSupported); } #define NOTIMPLEMENTED { MSIX::RaiseException(__LINE__, __FILE__, nullptr, Error::NotImplemented); } +#define BUILDEXCLUDED { MSIX::RaiseException(__LINE__, __FILE__, nullptr, Error::SupportExcludedByBuild); } #define ThrowErrorIfNot(c, a, m) if (!(a)) { MSIX::RaiseException(__LINE__, __FILE__, m, c); } #define ThrowWin32ErrorIfNot(c, a, m) if (!(a)) { MSIX::RaiseException(__LINE__, __FILE__, m, c); } @@ -140,11 +147,20 @@ namespace MSIX { #endif #ifdef WIN32 - #define ThrowHrIfFalse(a, m) \ - { BOOL _result = a; \ - if (!_result) \ - { MSIX::RaiseException (__LINE__, __FILE__, m, HRESULT_FROM_WIN32(GetLastError())); \ - } \ + #define ThrowHrIfFalse(a, m) \ + { BOOL _result = a; \ + if (!_result) \ + { MSIX::RaiseException(__LINE__, __FILE__, m, HRESULT_FROM_WIN32(GetLastError())); \ + } \ } #define ThrowLastErrorIf(a, m) { if (a) { MSIX::RaiseException (__LINE__, __FILE__, m, HRESULT_FROM_WIN32(GetLastError())); }} +#else + #define ThrowHrIfPOSIXFailed(a, m) \ + { \ + int _result = a; \ + if (_result < 0) \ + { \ + MSIX::RaiseException(__LINE__, __FILE__, m, errno); \ + } \ + } #endif diff --git a/src/inc/shared/StreamBase.hpp b/src/inc/shared/StreamBase.hpp index 80057130a..df4d3a781 100644 --- a/src/inc/shared/StreamBase.hpp +++ b/src/inc/shared/StreamBase.hpp @@ -58,7 +58,7 @@ namespace MSIX { if (bytesWritten) { bytesWritten->QuadPart = 0; } ThrowErrorIf(Error::InvalidParameter, (nullptr == stream), "invalid parameter."); - static const ULONGLONG size = 1024; + static const ULONGLONG size = 1 << 20; std::vector bytes(size); std::int64_t read = 0; std::int64_t written = 0; @@ -105,7 +105,7 @@ namespace MSIX { virtual HRESULT STDMETHODCALLTYPE Seek(LARGE_INTEGER, DWORD, ULARGE_INTEGER*) noexcept override { return static_cast(Error::NotImplemented); } // Changes the size of the stream object. - virtual HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER) noexcept override { return static_cast(Error::NotSupported); } + virtual HRESULT STDMETHODCALLTYPE SetSize(ULARGE_INTEGER) noexcept override { return static_cast(Error::NotImplemented); } // Retrieves the STATSTG structure for this stream. virtual HRESULT STDMETHODCALLTYPE Stat(STATSTG* statStg, DWORD /*grfStatFlag*/) noexcept override try diff --git a/src/makemsix/main.cpp b/src/makemsix/main.cpp index 4cdf1652a..c01f9a340 100644 --- a/src/makemsix/main.cpp +++ b/src/makemsix/main.cpp @@ -212,6 +212,20 @@ struct Invocation return opt->params[0]; } + const std::string* TryGetOptionValue(const std::string& name) const + { + const InvokedOption* opt = GetInvokedOption(name); + if (!opt) + { + return nullptr; + } + if (opt->option.ParameterCount != 1) + { + throw std::runtime_error("Given option does not take exactly one parameter"); + } + return &(opt->params[0]); + } + private: mutable std::string error; std::string toolName; @@ -418,6 +432,26 @@ MSIX_APPLICABILITY_OPTIONS GetApplicabilityOption(const Invocation& invocation) return applicability; } +#ifdef MSIX_PACK +MSIX_CERTIFICATE_FORMAT GetCertificateFormat(const Invocation& invocation) +{ + MSIX_CERTIFICATE_FORMAT format = MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN; + + const std::string* formatStringPtr = invocation.TryGetOptionValue("-cf"); + if (formatStringPtr) + { + const std::string& formatStr = *formatStringPtr; + + if (formatStr == "pfx") + { + format = MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_PFX; + } + } + + return format; +} +#endif + MSIX_BUNDLE_OPTIONS GetBundleOptions(const Invocation& invocation) { MSIX_BUNDLE_OPTIONS bundleOptions = MSIX_BUNDLE_OPTIONS::MSIX_OPTION_NONE; @@ -594,6 +628,52 @@ Command CreatePackCommand() return result; } +Command CreateSignCommand() +{ + Command result{ "sign", "Signs an existing package in-place", + { + Option{ "-p", "Package file path.", true, 1, "package" }, + Option{ "-c", "Certificate file path.", true, 1, "cert" }, + Option{ "-cf", "Certificate format.", false, 1, "format" }, + Option{ "-pass", "Certificate password.", false, 1, "password" }, + // TODO: Potentially support other types of certificate files, along with separate private keys. + // TODO: Support passing in the certificate chain separately. + // TODO: Windows signing allows choosing whether to validate the block hashes, could add here. + // TODO: Potentially allow non-inplace by making an output file param. + // TODO: Full package content hash is optional + // TODO: Flag to control CI catalog generation + Option{ TOOL_HELP_COMMAND_STRING, "Displays this help text." }, + } + }; + result.SetDescription({ + "Signs the given file, modifying it to either include or replace a", + "signature. The file contains the signing certificate.", + "The following certificate formats are supported, and the format will be", + "inferred from the file name if not provided:", + " pfx: [*.pfx] A PKCS12 containing both public and private keys", + "", + "WARNING: This feature is not yet complete. It has only had manual testing", + " and does not yet allow for verification of custom certificates", + " used during signing. It also required building with openssl." + }); + + result.SetInvocationFunc([](const Invocation& invocation) + { + std::cout << "WARNING: The signing feature is not complete, see the help for this command for more information." << std::endl; + std::cout << std::endl; + + return SignPackage( + MSIX_SIGNING_OPTIONS::MSIX_SIGNING_OPTIONS_NONE, + const_cast(invocation.GetOptionValue("-p").c_str()), + GetCertificateFormat(invocation), + const_cast(invocation.GetOptionValue("-c").c_str()), + const_cast(invocation.GetOptionValue("-pass").c_str()), + nullptr); + }); + + return result; +} + Command CreateBundleCommand() { Command result{ "bundle", "Create a new app bundle from files on disk", @@ -666,6 +746,7 @@ int main(int argc, char* argv[]) CreateUnbundleCommand(), #ifdef MSIX_PACK CreatePackCommand(), + CreateSignCommand(), CreateBundleCommand(), #endif }; diff --git a/src/msix/CMakeLists.txt b/src/msix/CMakeLists.txt index 6ed53241a..4039c5ffd 100644 --- a/src/msix/CMakeLists.txt +++ b/src/msix/CMakeLists.txt @@ -19,6 +19,7 @@ list(APPEND MSIX_UNPACK_EXPORTS if(MSIX_PACK) list(APPEND MSIX_PACK_EXPORTS "PackPackage" + "SignPackage" "PackBundle" ) endif() @@ -111,6 +112,7 @@ list(APPEND MsixSrc common/FileNameValidation.cpp common/AppxManifestValidation.cpp common/IXml.cpp + common/StreamHelper.cpp common/TimeHelpers.cpp ) @@ -120,7 +122,6 @@ list(APPEND MsixSrc unpack/AppxPackageObject.cpp unpack/AppxSignature.cpp unpack/InflateStream.cpp - unpack/ZipObjectReader.cpp ) # Pack @@ -134,6 +135,7 @@ if(MSIX_PACK) pack/ContentType.cpp pack/DeflateStream.cpp pack/ZipObjectWriter.cpp + pack/Signing.cpp pack/BundleManifestWriter.cpp pack/BundleWriterHelper.cpp pack/AppxBundleWriter.cpp @@ -202,14 +204,26 @@ endif() if(CRYPTO_LIB MATCHES crypt32) list(APPEND MsixSrc PAL/Crypto/Win32/Crypto.cpp - PAL/Signature/Win32/SignatureValidator.cpp + PAL/Crypto/Win32/SignatureValidator.cpp ) + if(MSIX_PACK) + list(APPEND MsixSrc + PAL/Crypto/Win32/SignatureCreator.cpp + ) + endif() elseif(CRYPTO_LIB MATCHES openssl) if(OpenSSL_FOUND) list(APPEND MsixSrc + PAL/Crypto/OpenSSL/SharedOpenSSL.cpp PAL/Crypto/OpenSSL/Crypto.cpp - PAL/Signature/OpenSSL/SignatureValidator.cpp + PAL/Crypto/OpenSSL/SignatureValidator.cpp ) + if(MSIX_PACK) + list(APPEND MsixSrc + PAL/Crypto/OpenSSL/OpenSSLWriting.cpp + PAL/Crypto/OpenSSL/SignatureCreator.cpp + ) + endif() else() # ... and were done here... :/ message(FATAL_ERROR "OpenSSL NOT FOUND!") diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp new file mode 100644 index 000000000..ac7d32529 --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp @@ -0,0 +1,169 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// + +#include "OpenSSLWriting.hpp" +#include "SharedOpenSSL.hpp" + +#include +#include + +namespace MSIX +{ + namespace + { + struct CustomObjectDef + { + CustomOpenSSLObjectName name; + const char* oid; + const char* shortName; + const char* longName; + }; + +#define MSIX_MAKE_CUSTOM_OBJECT_DEF(_name_,_oid_) { CustomOpenSSLObjectName:: _name_, _oid_, #_name_, #_name_ } + + CustomObjectDef customObjects[] + { + MSIX_MAKE_CUSTOM_OBJECT_DEF(spcIndirectDataContext, "1.3.6.1.4.1.311.2.1.4"), + MSIX_MAKE_CUSTOM_OBJECT_DEF(spcSipInfoObjID, "1.3.6.1.4.1.311.2.1.30"), + MSIX_MAKE_CUSTOM_OBJECT_DEF(spcSpOpusInfo, "1.3.6.1.4.1.311.2.1.12"), + MSIX_MAKE_CUSTOM_OBJECT_DEF(spcStatementType, "1.3.6.1.4.1.311.2.1.11"), + MSIX_MAKE_CUSTOM_OBJECT_DEF(individualCodeSigning, "1.3.6.1.4.1.311.2.1.21"), + }; + } + + CustomOpenSSLObjects::CustomOpenSSLObjects() + { + for (const auto& obj : customObjects) + { + int nid = OBJ_txt2nid(obj.oid); + if (nid == NID_undef) + { + nid = OBJ_create(obj.oid, obj.shortName, obj.longName); + if (nid == NID_undef) + { + ThrowOpenSSLError("Failed to create custom OpenSSL object"); + } + } + objects.emplace_back(obj.name, nid); + } + } + + CustomOpenSSLObjects::~CustomOpenSSLObjects() + { + OBJ_cleanup(); + } + + const CustomOpenSSLObject& CustomOpenSSLObjects::Get(CustomOpenSSLObjectName name) const + { + for (const auto& obj : objects) + { + if (obj.GetName() == name) + { + return obj; + } + } + + UNEXPECTED; + } + + // Custom ASN1 types +namespace ASN1 { + +#define LENGTH_LONG_FORM_BIT 0x80 + + size_t CountBytesNeededForInteger(uint64_t val) + { + for (size_t i = 0; i < sizeof(val); ++i) + { + size_t potentialByteCount = sizeof(val) - i; + + uint64_t mask = 0xFF; + mask = mask << ((potentialByteCount - 1) * 8); + + if (mask & val) + { + return potentialByteCount; + } + } + + // Will always need 1 byte for 0 + return 1; + } + + void AppendCountBytesOfInteger(Container::BytesType& bytes, size_t count, uint64_t val) + { + for (size_t i = 0; i < count; ++i) + { + size_t bitShift = (count - i - 1) * 8; + + uint64_t mask = 0xFF; + mask = mask << bitShift; + + bytes.push_back(static_cast((mask & val) >> bitShift)); + } + } + + void AppendLength(Container::BytesType& bytes, size_t length) + { + if (length > 127) + { + // long form, first count the bytes needed + size_t byteCount = CountBytesNeededForInteger(length); + bytes.push_back(static_cast(byteCount | LENGTH_LONG_FORM_BIT)); + + // Add the bytes + AppendCountBytesOfInteger(bytes, byteCount, length); + } + else + { + // short form + bytes.push_back(static_cast(length)); + } + } + + void Sequence::AppendTo(Container::BytesType& bytes) const + { + bytes.push_back(V_ASN1_SEQUENCE | V_ASN1_CONSTRUCTED); + AppendLength(bytes, m_bytes.size()); + bytes.insert(bytes.end(), m_bytes.begin(), m_bytes.end()); + } + + void ObjectIdentifier::AppendTo(Container::BytesType& bytes) const + { + int byteCount = i2d_ASN1_OBJECT(m_object, nullptr); + size_t currentSize = bytes.size(); + bytes.resize(currentSize + static_cast(byteCount)); + + uint8_t* currentEnd = &bytes[currentSize]; + ThrowOpenSSLErrIfFailed(i2d_ASN1_OBJECT(m_object, reinterpret_cast(¤tEnd))); + } + + void Integer::AppendTo(Container::BytesType& bytes) const + { + bytes.push_back(V_ASN1_INTEGER); + + // Count the number of bytes to use + size_t byteCount = CountBytesNeededForInteger(m_val); + AppendLength(bytes, byteCount); + + // Add the bytes + AppendCountBytesOfInteger(bytes, byteCount, m_val); + } + + void OctetString::AppendTo(Container::BytesType& bytes) const + { + bytes.push_back(V_ASN1_OCTET_STRING); + AppendLength(bytes, m_bytes.size()); + bytes.insert(bytes.end(), m_bytes.begin(), m_bytes.end()); + } + + void Null::AppendTo(Container::BytesType& bytes) const + { + bytes.push_back(V_ASN1_NULL); + AppendLength(bytes, 0); + } +} // namespace ASN1 + +} // namespace MSIX diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp new file mode 100644 index 000000000..d2e48557d --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp @@ -0,0 +1,148 @@ +// +// Copyright (C) 2017 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once + +#include +#include +#include + +#include + +namespace MSIX +{ + // Support for our custom OIDs + + enum class CustomOpenSSLObjectName + { + spcIndirectDataContext, + spcSipInfoObjID, + spcSpOpusInfo, + spcStatementType, + individualCodeSigning, + }; + + struct CustomOpenSSLObject + { + CustomOpenSSLObject(CustomOpenSSLObjectName name_, int nid_) : + name(name_), nid(nid_) {} + + CustomOpenSSLObject(const CustomOpenSSLObject&) = default; + CustomOpenSSLObject& operator=(const CustomOpenSSLObject&) = default; + + CustomOpenSSLObject(CustomOpenSSLObject&&) = default; + CustomOpenSSLObject& operator=(CustomOpenSSLObject&&) = default; + + CustomOpenSSLObjectName GetName() const { return name; } + + int GetNID() const { return nid; } + + ASN1_OBJECT* GetObj() const { return OBJ_nid2obj(nid); } + + private: + CustomOpenSSLObjectName name; + int nid = NID_undef; + }; + + // This helper class can only support a single use at a time because OBJ_cleanup will destroy + // any other simultaneous use. A shared_ptr singleton model would be best if needed. + struct CustomOpenSSLObjects + { + CustomOpenSSLObjects(); + ~CustomOpenSSLObjects(); + + CustomOpenSSLObjects(const CustomOpenSSLObjects&) = delete; + CustomOpenSSLObjects& operator=(const CustomOpenSSLObjects&) = delete; + + CustomOpenSSLObjects(CustomOpenSSLObjects&&) = delete; + CustomOpenSSLObjects& operator=(CustomOpenSSLObjects&&) = delete; + + const CustomOpenSSLObject& Get(CustomOpenSSLObjectName name) const; + + private: + // Not enough to bother with more complex search + std::vector objects; + }; + + // Custom ASN1 writing. + // All of these types are ephemeral; used merely to tag the data when serializing. + namespace ASN1 + { + // Empty type to allow for template filtering + struct Item {}; + + // Base type for items that hold other items + struct Container : public Item + { + using BytesType = std::vector; + + Container() = default; + Container(BytesType&& contents) : m_bytes(std::move(contents)) {} + + BytesType& GetBytes() { return m_bytes; } + + protected: + BytesType m_bytes; + }; + + struct Sequence : public Container + { + Sequence() = default; + Sequence(Container::BytesType&& contents) : Container(std::move(contents)) {} + void AppendTo(Container::BytesType& bytes) const; + }; + + struct ObjectIdentifier : public Item + { + ObjectIdentifier(ASN1_OBJECT* obj) : m_object(obj) {} + void AppendTo(Container::BytesType& bytes) const; + + private: + ASN1_OBJECT* m_object; + }; + + struct Integer : public Item + { + Integer(uint32_t val) : m_val(val) {} + void AppendTo(Container::BytesType& bytes) const; + + private: + uint64_t m_val; + }; + + struct OctetString : public Item + { + OctetString(const Container::BytesType& bytes) : m_bytes(bytes) {} + OctetString(Container::BytesType&& bytes) : m_bytes(std::move(bytes)) {} + void AppendTo(Container::BytesType& bytes) const; + + private: + Container::BytesType m_bytes; + }; + + struct Null : public Item + { + void AppendTo(Container::BytesType& bytes) const; + }; + + // Any item can write itself to a vector + template + inline std::vector& operator<<(std::vector& output, const ItemType& item) + { + static_assert(std::is_convertible::value, "Output type must be an Item"); + item.AppendTo(output); + return output; + } + + // Only items can be written to containers + template + inline ContainerType& operator<<(ContainerType&& container, const ItemType& item) + { + static_assert(std::is_convertible*, Container*>::value, "Receiving type must be a Container"); + static_assert(std::is_convertible::value, "Output type must be an Item"); + container.GetBytes() << item; + return container; + } + } // namespace ASN1 +} // namespace MSIX diff --git a/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp new file mode 100644 index 000000000..70f9ddb1b --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp @@ -0,0 +1,69 @@ +// +// Copyright (C) 2017 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#include "SharedOpenSSL.hpp" + +#include +#include +#include + +namespace MSIX +{ + inline std::string GetOpenSSLErrString(const char* message) + { + ERR_load_crypto_strings(); + + std::ostringstream strstr; + + if (message) + { + strstr << message << std::endl; + } + + strstr << "OpenSSL Error Data:" << std::endl; + + unsigned long err = 0; + do + { + const char* file{}; + int line{}; + const char* data{}; + int flags{}; + + err = ERR_get_error_line_data(&file, &line, &data, &flags); + + if (err) + { + strstr << " at " << file << '[' << line << ']'; + if (flags & ERR_TXT_STRING) + { + strstr << " : " << data; + } + strstr << std::endl; + + strstr << " " << ERR_error_string(err, nullptr) << std::endl; + } + } while (err); + + return strstr.str(); + } + + MSIX_NOINLINE(void) RaiseOpenSSLException(const char* message, const int line, const char* const file, DWORD error) + { + const char* messageToPass = nullptr; + std::string messageStr; + + if (error == static_cast(Error::OutOfMemory)) + { + messageToPass = message; + } + else + { + messageStr = GetOpenSSLErrString(message); + messageToPass = messageStr.c_str(); + } + + MSIX::RaiseException(line, file, messageToPass, error); + } +} // namespace MSIX diff --git a/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp new file mode 100644 index 000000000..7ae7a52f0 --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp @@ -0,0 +1,143 @@ +// +// Copyright (C) 2017 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once +#include "Exceptions.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MSIX +{ + struct unique_BIO_deleter { + void operator()(BIO* b) const { if (b) BIO_free(b); }; + }; + + struct unique_PKCS7_deleter { + void operator()(PKCS7* p) const { if (p) PKCS7_free(p); }; + }; + + struct unique_PKCS12_deleter { + void operator()(PKCS12* p) const { if (p) PKCS12_free(p); }; + }; + + struct unique_X509_deleter { + void operator()(X509* x) const { if (x) X509_free(x); }; + }; + + struct unique_X509_STORE_deleter { + void operator()(X509_STORE* xs) const { if (xs) X509_STORE_free(xs); }; + }; + + struct unique_X509_STORE_CTX_deleter { + void operator()(X509_STORE_CTX* xsc) const { if (xsc) { X509_STORE_CTX_cleanup(xsc); X509_STORE_CTX_free(xsc); } }; + }; + + struct unique_OPENSSL_string_deleter { + void operator()(char* os) const { if (os) OPENSSL_free(os); }; + }; + + struct unique_STACK_X509_deleter { + void operator()(STACK_OF(X509)* sx) const { if (sx) sk_X509_free(sx); }; + }; + + struct unique_EVP_PKEY_deleter { + void operator()(EVP_PKEY* pkey) const { if (pkey) EVP_PKEY_free(pkey); } + }; + + struct unique_ASN1_STRING_deleter { + void operator()(ASN1_STRING* pStr) const { if (pStr) ASN1_STRING_free(pStr); } + }; + + struct shared_BIO_deleter { + void operator()(BIO* b) const { if (b) BIO_free(b); }; + }; + + using unique_BIO = std::unique_ptr; + using unique_PKCS7 = std::unique_ptr; + using unique_PKCS12 = std::unique_ptr; + using unique_X509 = std::unique_ptr; + using unique_X509_STORE = std::unique_ptr; + using unique_X509_STORE_CTX = std::unique_ptr; + using unique_OPENSSL_string = std::unique_ptr; + using unique_STACK_X509 = std::unique_ptr; + using unique_EVP_PKEY = std::unique_ptr; + using unique_ASN1_STRING = std::unique_ptr; + + typedef struct Asn1Sequence + { + std::uint8_t tag; + std::uint8_t encoding; + union + { + struct { + std::uint8_t length; + std::uint8_t content; + } rle8; + struct { + std::uint8_t lengthHigh; + std::uint8_t lengthLow; + std::uint8_t content; + } rle16; + std::uint8_t content; + }; + } Asn1Sequence; + + // A common exception class to be used by all OpenSSL errors. + // OpenSSL does not return detailed error codes, so we use a singular HResult. + class OpenSSLException final : public Exception + { + public: + OpenSSLException(std::string& message, DWORD error) : Exception(message, error) + { + } + }; + + MSIX_NOINLINE(void) RaiseOpenSSLException(const char* message, const int line, const char* const file, DWORD error = 0x80FA11ED); + + // Use only to verify the result of *_new functions from OpenSSL + template + inline void CheckOpenSSLAlloc(const T& t, const int line, const char* const file) + { + if (!t) + { + RaiseOpenSSLException("OpenSSL allocation failed", line, file, static_cast(Error::OutOfMemory)); + } + } + + // Use to check the result of functions that actually do something beyond allocation. + // The overloads allow for other return types, such as pointers. + inline void CheckOpenSSLErr(int err, const int line, const char* const file, const char* message = nullptr) + { + if (err <= 0) + { + RaiseOpenSSLException(message, line, file); + } + } + + template + inline void CheckOpenSSLErr(T* returnVal, const int line, const char* const file, const char* message = nullptr) + { + if (!returnVal) + { + RaiseOpenSSLException(message, line, file); + } + } +} // namespace MSIX + +#define ThrowOpenSSLError(m) MSIX::RaiseOpenSSLException(m, __LINE__, __FILE__) +#define ThrowOpenSSLErrIfAllocFailed(x) MSIX::CheckOpenSSLAlloc(x, __LINE__, __FILE__) +#define ThrowOpenSSLErrIfFailed(x) MSIX::CheckOpenSSLErr(x, __LINE__, __FILE__) +#define ThrowOpenSSLErrIfFailedMsg(x,m) MSIX::CheckOpenSSLErr(x, __LINE__, __FILE__, m) diff --git a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp new file mode 100644 index 000000000..13669060f --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp @@ -0,0 +1,270 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#include "AppxSignature.hpp" +#include "Exceptions.hpp" +#include "SignatureCreator.hpp" +#include "MSIXResource.hpp" +#include "StreamHelper.hpp" +#include "MemoryStream.hpp" + +#include "SharedOpenSSL.hpp" +#include "OpenSSLWriting.hpp" + +#include +#include + +namespace MSIX +{ + namespace + { + struct SigningInfo + { + SigningInfo( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey) + { + switch (signingCertificateFormat) + { + case MSIX_CERTIFICATE_FORMAT_PFX: + { + auto certBytes = Helper::CreateBufferFromStream(signingCertificate); + + unique_BIO certBIO{ BIO_new_mem_buf(reinterpret_cast(certBytes.data()), static_cast(certBytes.size())) }; + unique_PKCS12 cert{ d2i_PKCS12_bio(certBIO.get(), nullptr) }; + + ParsePKCS12(cert.get(), pass, "Unable to open PFX file"); + } + break; + + default: + NOTSUPPORTED + } + } + + void ParsePKCS12(PKCS12* p12, const char* pass, const char* failureMessage = nullptr) + { + EVP_PKEY* pkey_ = nullptr; + X509* cert_ = nullptr; + STACK_OF(X509)* ca_ = chain.get(); + + ThrowOpenSSLErrIfFailedMsg(PKCS12_parse(p12, pass, &pkey_, &cert_, &ca_), failureMessage); + + // If ca existed, the certs will have been added to it and it is the same pointer. + // If it is not set, a new stack will have been created and we need to set it out. + if (!static_cast(chain)) + { + chain.reset(ca_); + } + certificate.reset(cert_); + privateKey.reset(pkey_); + } + + unique_X509 certificate; + unique_STACK_X509 chain; + unique_EVP_PKEY privateKey; + }; + + // Create the indirect data object within the given message. + // Copied and modified from the internal OpenSSL function that creates the content based on the type of the outer message. + // Required to attach our custom type (spcIndirectDataContext) to it. + void PKCS7_indirect_data_content_new(PKCS7* p7, const CustomOpenSSLObjects& customObjects) + { + unique_PKCS7 content{ PKCS7_new() }; + ThrowOpenSSLErrIfAllocFailed(content); + + content->type = customObjects.Get(CustomOpenSSLObjectName::spcIndirectDataContext).GetObj(); + + content->d.other = ASN1_TYPE_new(); + ThrowOpenSSLErrIfAllocFailed(content->d.other); + + ThrowOpenSSLErrIfFailed(ASN1_TYPE_set_octetstring(content->d.other, nullptr, 0)); + ThrowOpenSSLErrIfFailed(PKCS7_set_content(p7, content.get())); + + // The parent PKCS7 now owns this + content.release(); + } + + // Create a signed PKCS7 message from the given data. + // Copied and modified from PKCS7_sign. + // Required because we need to attach our message contents and various attributes. + unique_PKCS7 PKCS7_sign_indirect_data(X509* signcert, EVP_PKEY* pkey, STACK_OF(X509)* certs, + BIO* data, int flags, const CustomOpenSSLObjects& customObjects) + { + unique_PKCS7 p7{ PKCS7_new() }; + ThrowOpenSSLErrIfAllocFailed(p7); + + ThrowOpenSSLErrIfFailed(PKCS7_set_type(p7.get(), NID_pkcs7_signed)); + + // Standard PKCS7_sign only supports NID_pkcs7_data, but we want SPC_INDIRECT_DATA_OBJID + PKCS7_indirect_data_content_new(p7.get(), customObjects); + + // Force SHA256 for now + PKCS7_SIGNER_INFO* signerInfo = PKCS7_sign_add_signer(p7.get(), signcert, pkey, EVP_sha256(), flags); + ThrowOpenSSLErrIfFailed(signerInfo); + + // Add our authenticated attributes + ThrowOpenSSLErrIfFailed(PKCS7_add_attrib_content_type(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcIndirectDataContext).GetObj())); + + // Add individual code signing statement type to the authenticated attributes. + std::vector statementTypeSequence; + statementTypeSequence << ( ASN1::Sequence{} << ASN1::ObjectIdentifier{ customObjects.Get(CustomOpenSSLObjectName::individualCodeSigning).GetObj() } ); + + unique_ASN1_STRING statementString{ ASN1_STRING_type_new(V_ASN1_SEQUENCE) }; + ThrowOpenSSLErrIfAllocFailed(statementString); + + // ASN1_STRING_set copies the given data + ThrowOpenSSLErrIfFailed(ASN1_STRING_set(statementString.get(), static_cast(statementTypeSequence.data()), static_cast(statementTypeSequence.size()))); + + ThrowOpenSSLErrIfFailed(PKCS7_add_signed_attribute(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcStatementType).GetNID(), V_ASN1_SEQUENCE, statementString.get())); + statementString.release(); + + // Add empty opusType + std::vector opusTypeSequence; + opusTypeSequence << ASN1::Sequence{}; + + unique_ASN1_STRING opusString{ ASN1_STRING_type_new(V_ASN1_SEQUENCE) }; + ThrowOpenSSLErrIfAllocFailed(opusString); + + ThrowOpenSSLErrIfFailed(ASN1_STRING_set(opusString.get(), static_cast(opusTypeSequence.data()), static_cast(opusTypeSequence.size()))); + + ThrowOpenSSLErrIfFailed(PKCS7_add_signed_attribute(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcSpOpusInfo).GetNID(), V_ASN1_SEQUENCE, opusString.get())); + opusString.release(); + + // Always include the chain certs + for (int i = 0; i < sk_X509_num(certs); i++) { + ThrowOpenSSLErrIfFailed(PKCS7_add_certificate(p7.get(), sk_X509_value(certs, i))); + } + + ThrowOpenSSLErrIfFailed(PKCS7_final(p7.get(), data, flags)); + + return p7; + } + + void AppendDigestName(std::vector& target, DigestName name) + { + uint32_t nameVal = static_cast(name); + for (size_t i = 0; i < 4; ++i) + { + size_t bitShift = i * 8; + uint32_t mask = 0xFF << bitShift; + target.push_back(static_cast((nameVal & mask) >> bitShift)); + } + } + + void AppendDigest(std::vector& target, const std::vector& digest) + { + target.insert(target.end(), digest.begin(), digest.end()); + } + + // Create the custom digest blob that contains the hashes to be signed. + std::vector CreateDigestBlob(AppxSignatureObject* digests) + { + std::vector result; + + AppendDigestName(result, DigestName::HEAD); + AppendDigestName(result, DigestName::AXPC); + AppendDigest(result, digests->GetFileRecordsDigest()); + AppendDigestName(result, DigestName::AXCD); + AppendDigest(result, digests->GetCentralDirectoryDigest()); + AppendDigestName(result, DigestName::AXCT); + AppendDigest(result, digests->GetContentTypesDigest()); + AppendDigestName(result, DigestName::AXBM); + AppendDigest(result, digests->GetAppxBlockMapDigest()); + if (!digests->GetCodeIntegrityDigest().empty()) + { + AppendDigestName(result, DigestName::AXCI); + AppendDigest(result, digests->GetCodeIntegrityDigest()); + } + + return result; + } + + // Encapsulate the digest blob within the ASN1 wrapper. + // All of this data is signed. + std::vector CreateDataToBeSigned(AppxSignatureObject* digests, const CustomOpenSSLObjects& customObjects) + { + std::vector digestBlob = CreateDigestBlob(digests); + + std::vector result; + + result + << ( ASN1::Sequence{} + << ASN1::ObjectIdentifier{ customObjects.Get(CustomOpenSSLObjectName::spcSipInfoObjID).GetObj() } + << ( ASN1::Sequence{} + << ASN1::Integer{ APPX_SIP_DEFAULT_VERSION } + << ASN1::OctetString{ std::vector{ APPX_SIP_GUID_BYTES } } + << ASN1::Integer{ 0 } + << ASN1::Integer{ 0 } + << ASN1::Integer{ 0 } + << ASN1::Integer{ 0 } + << ASN1::Integer{ 0 } + ) + ) + << ( ASN1::Sequence{} + << ( ASN1::Sequence{} + << ASN1::ObjectIdentifier{ OBJ_nid2obj(NID_sha256) } + << ASN1::Null{} + ) + << ASN1::OctetString{ digestBlob } + ); + + return result; + } + } + + // Given a set of digest hashes from a package and the signing info, create a p7x signature stream. + ComPtr SignatureCreator::Sign( + AppxSignatureObject* digests, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey) + { + OpenSSL_add_all_algorithms(); + CustomOpenSSLObjects customObjects{}; + + // Read in the signing info based on format, etc. + SigningInfo signingInfo{ signingCertificateFormat, signingCertificate, pass, privateKey }; + + // Create the blob to be signed + std::vector signedData = CreateDataToBeSigned(digests, customObjects); + unique_BIO dataBIO{ BIO_new_mem_buf(signedData.data(), static_cast(signedData.size())) }; + ThrowOpenSSLErrIfAllocFailed(dataBIO); + + // Sign it + unique_PKCS7 p7{ PKCS7_sign_indirect_data(signingInfo.certificate.get(), signingInfo.privateKey.get(), signingInfo.chain.get(), dataBIO.get(), PKCS7_BINARY | PKCS7_NOATTR, customObjects) }; + + // Overwrite the signed contents with the complete one including the additional sequence at the beginning. + // It is unclear why things work this way, but this is necessary. + std::vector completeBlob; + completeBlob << ASN1::Sequence{ std::move(signedData) }; + + unique_ASN1_STRING sequenceString{ ASN1_STRING_type_new(V_ASN1_SEQUENCE) }; + ThrowOpenSSLErrIfAllocFailed(sequenceString); + ThrowOpenSSLErrIfFailed(ASN1_STRING_set(sequenceString.get(), static_cast(completeBlob.data()), static_cast(completeBlob.size()))); + + ASN1_TYPE_set(p7->d.sign->contents->d.other, V_ASN1_SEQUENCE, sequenceString.get()); + sequenceString.release(); + + // Serialize the PKCS7 + unique_BIO outBIO{ BIO_new(BIO_s_mem()) }; + ThrowOpenSSLErrIfAllocFailed(outBIO); + ThrowOpenSSLErrIfFailed(i2d_PKCS7_bio(outBIO.get(), p7.get())); + + // Write the signature out, including the extra bytes that tag it as a package signature. + ComPtr p7xOutStream = ComPtr::Make(); + + char* out = nullptr; + long cOut = BIO_get_mem_data(outBIO.get(), &out); + + uint32_t prefix = P7X_FILE_ID; + p7xOutStream->Write(&prefix, sizeof(prefix), nullptr); + p7xOutStream->Write(out, cOut, nullptr); + + return p7xOutStream; + } +} // namespace MSIX diff --git a/src/msix/PAL/Signature/OpenSSL/SignatureValidator.cpp b/src/msix/PAL/Crypto/OpenSSL/SignatureValidator.cpp similarity index 98% rename from src/msix/PAL/Signature/OpenSSL/SignatureValidator.cpp rename to src/msix/PAL/Crypto/OpenSSL/SignatureValidator.cpp index 031b58fb7..a2b8b2f98 100644 --- a/src/msix/PAL/Signature/OpenSSL/SignatureValidator.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/SignatureValidator.cpp @@ -239,7 +239,7 @@ namespace MSIX if ((asn1Sequence->encoding & 0x80) == 0) { spcIndirectDataContent = &asn1Sequence->content; - spcIndirectDataContentSize = (asn1Sequence->encoding & 0x7F); + spcIndirectDataContentSize = (asn1Sequence->encoding & 0x7F); } else if ((asn1Sequence->encoding & 0x81) == 0x81) @@ -264,6 +264,7 @@ namespace MSIX unique_BIO bioMem(BIO_new_mem_buf(spcIndirectDataContent, spcIndirectDataContentSize)); signatureDigest.swap(bioMem); + // TODO: We can potentially do better to decode the ASN1 and ensure that it's the correct SIP GUID and version // Scan through the spcIndirectData for the APPX header bool found = false; while (spcIndirectDataContent < spcIndirectDataContentEnd && !found) @@ -285,8 +286,8 @@ namespace MSIX (spcIndirectDataContentSize - sizeof(DigestName)) / sizeof(DigestHash), (spcIndirectDataContentSize - sizeof(DigestName)) % sizeof(DigestHash) ); - } - + } + // This callback will be invoked during certificate verification int VerifyCallback(int ok, X509_STORE_CTX *ctx) { @@ -384,6 +385,7 @@ namespace MSIX return false; } + // TODO: Needs a pass to ensure proper behavior and correctness bool SignatureValidator::Validate( IMsixFactory* factory, MSIX_VALIDATION_OPTION option, @@ -408,11 +410,11 @@ namespace MSIX std::uint32_t p7sSize = end.u.LowPart - sizeof(fileID); std::vector p7s(p7sSize); ULONG actualRead = 0; - ThrowHrIfFailed(stream->Read(p7s.data(), p7s.size(), &actualRead)); + ThrowHrIfFailed(stream->Read(p7s.data(), static_cast(p7s.size()), &actualRead)); ThrowErrorIf(Error::SignatureInvalid, (actualRead != p7s.size()), "read error"); // Load the p7s into a BIO buffer - unique_BIO bmem(BIO_new_mem_buf(p7s.data(), p7s.size())); + unique_BIO bmem(BIO_new_mem_buf(p7s.data(), static_cast(p7s.size()))); // Initialize the PKCS7 object from the BIO buffer unique_PKCS7 p7(d2i_PKCS7_bio(bmem.get(), nullptr)); @@ -445,7 +447,7 @@ namespace MSIX { auto certBuffer = Helper::CreateBufferFromStream(appxCert.second); // Load the cert into memory - unique_BIO bcert(BIO_new_mem_buf(certBuffer.data(), certBuffer.size())); + unique_BIO bcert(BIO_new_mem_buf(certBuffer.data(), static_cast(certBuffer.size()))); // Create a cert from the memory buffer unique_X509 cert(PEM_read_bio_X509(bcert.get(), nullptr, nullptr, nullptr)); diff --git a/src/msix/PAL/Crypto/Win32/Crypto.cpp b/src/msix/PAL/Crypto/Win32/Crypto.cpp index 9f1b7ca55..508196d6b 100644 --- a/src/msix/PAL/Crypto/Win32/Crypto.cpp +++ b/src/msix/PAL/Crypto/Win32/Crypto.cpp @@ -70,7 +70,7 @@ namespace MSIX { BCRYPT_ALG_HANDLE algHandleT; // Open an algorithm handle - // This code passes BCRYPT_HASH_REUSABLE_FLAG with BCryptAlgorithmProvider(...) to load a provider which supports reusable hash + BCRYPT_ALG_HANDLE algHandleT{}; ThrowStatusIfFailed(BCryptOpenAlgorithmProvider( &algHandleT, // Alg Handle pointer BCRYPT_SHA256_ALGORITHM, // Cryptographic Algorithm name (null terminated unicode string) @@ -81,7 +81,7 @@ namespace MSIX { // Create a hash handle ThrowStatusIfFailed(BCryptCreateHash( - algHandle.get(), // Handle to an algorithm provider + context->algHandle.get(), // Handle to an algorithm provider &hashHandleT, // A pointer to a hash handle - can be a hash or hmac object nullptr, // Pointer to the buffer that receives the hash/hmac object 0, // Size of the buffer in bytes @@ -132,6 +132,11 @@ namespace MSIX { return true; } + void SHA256::SHA256ContextDeleter::operator()(SHA256Context* context) + { + delete context; + } + std::string Base64::ComputeBase64(const std::vector& buffer) { std::wstring result; diff --git a/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp b/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp new file mode 100644 index 000000000..e270f1582 --- /dev/null +++ b/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp @@ -0,0 +1,17 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#include "SignatureCreator.hpp" + +namespace MSIX +{ + ComPtr SignatureCreator::Sign( + AppxSignatureObject* digests, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + IStream* privateKey) + { + ThrowErrorAndLog(Error::SupportExcludedByBuild, "Signing requires building with openssl"); + } +} // namespace MSIX diff --git a/src/msix/PAL/Signature/Win32/SignatureValidator.cpp b/src/msix/PAL/Crypto/Win32/SignatureValidator.cpp similarity index 100% rename from src/msix/PAL/Signature/Win32/SignatureValidator.cpp rename to src/msix/PAL/Crypto/Win32/SignatureValidator.cpp diff --git a/src/msix/common/AppxFactory.cpp b/src/msix/common/AppxFactory.cpp index ad000dd08..da4aaebbd 100644 --- a/src/msix/common/AppxFactory.cpp +++ b/src/msix/common/AppxFactory.cpp @@ -5,10 +5,9 @@ #include "AppxFactory.hpp" #include "UnicodeConversion.hpp" #include "Exceptions.hpp" -#include "ZipObjectReader.hpp" #include "AppxPackageObject.hpp" #include "MSIXResource.hpp" -#include "VectorStream.hpp" +#include "MemoryStream.hpp" #include "MsixFeatureSelector.hpp" #include "AppxPackageWriter.hpp" #include "AppxBundleWriter.hpp" @@ -29,12 +28,10 @@ namespace MSIX { ThrowErrorIf(Error::InvalidParameter, (outputStream == nullptr || packageWriter == nullptr || *packageWriter != nullptr), "Invalid parameter"); // We should never be here is packing if disabled, but the compiler // is not smart enough to remove it and the linker will fail. - #ifdef MSIX_PACK - ComPtr self; - ThrowHrIfFailed(QueryInterface(UuidOfImpl::iid, reinterpret_cast(&self))); + #ifdef MSIX_PACK auto zip = ComPtr::Make(outputStream); bool enableFileHash = m_factoryOptions & MSIX_FACTORY_OPTION_WRITER_ENABLE_FILE_HASH; - auto result = ComPtr::Make(self.Get(), zip, enableFileHash); + auto result = ComPtr::Make(this, zip, enableFileHash); *packageWriter = result.Detach(); #endif return static_cast(Error::OK); @@ -45,8 +42,7 @@ namespace MSIX { IAppxPackageReader** packageReader) noexcept try { ThrowErrorIf(Error::InvalidParameter, (packageReader == nullptr || *packageReader != nullptr), "Invalid parameter"); - ComPtr input(inputStream); - auto zip = ComPtr::Make(input); + auto zip = ComPtr::Make(inputStream); auto result = ComPtr::Make(this, m_validationOptions, m_applicabilityFlags, zip); *packageReader = result.Detach(); return static_cast(Error::OK); @@ -184,8 +180,8 @@ namespace MSIX { { // Get stream of the resource zip file generated at CMake processing. m_resourcesVector = std::vector(Resource::resourceByte, Resource::resourceByte + Resource::resourceLength); - auto resourceStream = ComPtr::Make(&m_resourcesVector); - m_resourcezip = ComPtr::Make(resourceStream.Get()); + auto resourceStream = ComPtr::Make(&m_resourcesVector); + m_resourcezip = ComPtr::Make(resourceStream.Get()); } auto file = m_resourcezip->GetFile(resource); ThrowErrorIfNot(Error::FileNotFound, file, resource.c_str()); diff --git a/src/msix/common/Exceptions.cpp b/src/msix/common/Exceptions.cpp index 3fafd459d..14ec6c8c1 100644 --- a/src/msix/common/Exceptions.cpp +++ b/src/msix/common/Exceptions.cpp @@ -28,14 +28,8 @@ const PfnDliHook __pfnDliFailureHook2 = MsixDelayLoadFailureHandler; #endif namespace MSIX { -#ifdef WIN32 -__declspec(noinline) -#endif -void -#ifndef WIN32 -__attribute__(( noinline)) -#endif -RaiseExceptionIfFailed(HRESULT hr, const int line, const char* const file) + +MSIX_NOINLINE(void) RaiseExceptionIfFailed(HRESULT hr, const int line, const char* const file) { if (FAILED(hr)) { MSIX::RaiseException (line, file, nullptr, hr); diff --git a/src/msix/common/StreamHelper.cpp b/src/msix/common/StreamHelper.cpp new file mode 100644 index 000000000..c1d0b796c --- /dev/null +++ b/src/msix/common/StreamHelper.cpp @@ -0,0 +1,125 @@ +// +// Copyright (C) 2017 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#include "StreamHelper.hpp" +#include "StreamBase.hpp" + +namespace MSIX { + namespace Helper { + + std::vector CreateBufferFromStream(const ComPtr& stream) + { + // Create buffer from stream + LARGE_INTEGER start = { 0 }; + ULARGE_INTEGER end = { 0 }; + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::END, &end)); + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); + + std::uint32_t streamSize = end.u.LowPart; + std::vector buffer(streamSize); + ULONG actualRead = 0; + ThrowHrIfFailed(stream->Read(buffer.data(), streamSize, &actualRead)); + ThrowErrorIf(Error::FileRead, (actualRead != streamSize), "read error"); + + // move the underlying stream back to the beginning. + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); + return buffer; + } + + std::string CreateStringFromStream(IStream* stream) + { + // Create buffer from stream + LARGE_INTEGER start = { 0 }; + ULARGE_INTEGER end = { 0 }; + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::END, &end)); + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); + + std::uint32_t streamSize = end.u.LowPart; + std::string buffer(streamSize, ' '); + ULONG actualRead = 0; + ThrowHrIfFailed(stream->Read(&buffer[0], streamSize, &actualRead)); + ThrowErrorIf(Error::FileRead, (actualRead != streamSize), "read error"); + + // move the underlying stream back to the beginning. + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); + return buffer; + } + + std::pair> CreateRawBufferFromStream(const ComPtr& stream) + { + // Create buffer from stream + LARGE_INTEGER start = { 0 }; + ULARGE_INTEGER end = { 0 }; + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::END, &end)); + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); + + std::uint32_t streamSize = end.u.LowPart; + std::unique_ptr buffer = std::make_unique(streamSize); + ULONG actualRead = 0; + ThrowHrIfFailed(stream->Read(buffer.get(), streamSize, &actualRead)); + ThrowErrorIf(Error::FileRead, (actualRead != streamSize), "read error"); + + // move the underlying stream back to the beginning. + ThrowHrIfFailed(stream->Seek(start, StreamBase::Reference::START, nullptr)); + return std::make_pair(streamSize, std::move(buffer)); + } + + StreamProcessor::iterator& StreamProcessor::iterator::operator++() + { + ReadNextBytes(); + return *this; + } + + const std::vector& StreamProcessor::iterator::operator*() + { + return m_bytes; + } + + bool StreamProcessor::iterator::operator!=(const iterator& other) + { + // The only equality is when both are end + return !(isEnd && other.isEnd); + } + + StreamProcessor::iterator::iterator(IStream* stream, size_t blockSize) : + m_stream(stream), m_blockSize(blockSize) + { + m_bytes.resize(m_blockSize); + ReadNextBytes(); + } + + StreamProcessor::iterator::iterator() : + isEnd(true) {} + + void StreamProcessor::iterator::ReadNextBytes() + { + const ULONG blockSize = static_cast(m_blockSize); + + ULONG bytesRead = 0; + m_stream->Read(static_cast(m_bytes.data()), blockSize, &bytesRead); + + if (bytesRead == 0) + { + isEnd = true; + return; + } + + if (bytesRead != blockSize) + { + // Try to read more data, to ensure that there is indeed no more. + // Intentionally ignore any errors that might occur. + ULONG moreBytesRead = 0; + do + { + bytesRead += moreBytesRead; + moreBytesRead = 0; + m_stream->Read(static_cast(&(m_bytes[bytesRead])), blockSize - bytesRead, &moreBytesRead); + } while (moreBytesRead && bytesRead < blockSize); + + m_bytes.resize(bytesRead); + } + } + + } +} \ No newline at end of file diff --git a/src/msix/common/ZipObject.cpp b/src/msix/common/ZipObject.cpp index 246c20b34..7828b5090 100644 --- a/src/msix/common/ZipObject.cpp +++ b/src/msix/common/ZipObject.cpp @@ -7,8 +7,11 @@ #include "ObjectBase.hpp" #include "ComHelper.hpp" #include "ZipObject.hpp" -#include "VectorStream.hpp" +#include "MemoryStream.hpp" #include "MsixFeatureSelector.hpp" +#include "ZipFileStream.hpp" +#include "InflateStream.hpp" +#include "RangeStream.hpp" #include "TimeHelpers.hpp" #include @@ -230,7 +233,7 @@ void CentralDirectoryFileHeader::Read(const ComPtr& stream, bool isZip6 { LARGE_INTEGER zero = {0}; ThrowHrIfFailed(stream->Seek(zero, StreamBase::Reference::CURRENT, &pos)); - auto vectorStream = ComPtr::Make(&Field<18>()); + auto vectorStream = ComPtr::Make(&Field<18>()); m_extendedInfo.Read(vectorStream.Get(), pos, Field<9>(), Field<8>(), Field<16>(), Field<13>()); } @@ -288,7 +291,7 @@ void LocalFileHeader::Read(const ComPtr &stream, CentralDirectoryFileHe StreamBase::Read(stream, &Field<2>()); ThrowErrorIfNot(Error::ZipLocalFileHeader, ((Field<2>().get() & static_cast(UnsupportedFlagsMask)) == 0), "unsupported flag(s) specified"); - ThrowErrorIfNot(Error::ZipLocalFileHeader, (IsGeneralPurposeBitSet() == directoryEntry.IsGeneralPurposeBitSet()), "inconsistent general purpose bits specified"); + ThrowErrorIfNot(Error::ZipLocalFileHeader, (IsDataDescriptorBitSet() == directoryEntry.IsDataDescriptorBitSet()), "inconsistent general purpose bits specified"); StreamBase::Read(stream, &Field<3>()); Meta::OnlyEitherValueValidation(Field<3>(), static_cast(CompressionType::Deflate), @@ -297,10 +300,10 @@ void LocalFileHeader::Read(const ComPtr &stream, CentralDirectoryFileHe StreamBase::Read(stream, &Field<4>()); StreamBase::Read(stream, &Field<5>()); StreamBase::Read(stream, &Field<6>()); - ThrowErrorIfNot(Error::ZipLocalFileHeader, (!IsGeneralPurposeBitSet() || (Field<6>().get() == 0)), "Invalid Zip CRC"); + ThrowErrorIfNot(Error::ZipLocalFileHeader, (!IsDataDescriptorBitSet() || (Field<6>().get() == 0)), "Invalid Zip CRC"); StreamBase::Read(stream, &Field<7>()); - ThrowErrorIfNot(Error::ZipLocalFileHeader, (!IsGeneralPurposeBitSet() || (Field<7>().get() == 0)), "Invalid Zip compressed size"); + ThrowErrorIfNot(Error::ZipLocalFileHeader, (!IsDataDescriptorBitSet() || (Field<7>().get() == 0)), "Invalid Zip compressed size"); StreamBase::Read(stream, &Field<8>()); @@ -481,15 +484,179 @@ void EndCentralDirectoryRecord::Read(const ComPtr& stream) } } -// Use for editing a package -ZipObject::ZipObject(const ComPtr& storageObject) +////////////////////////////////////////////////////////////////////////////////////////////// +// ZipObject // +////////////////////////////////////////////////////////////////////////////////////////////// +ZipObject::ZipObject(IStream* stream, bool readStream) : m_stream(stream) +{ + // Used by the writer to create an empty zip object + if (!readStream) + { + return; + } + + LARGE_INTEGER pos = { 0 }; + pos.QuadPart = m_endCentralDirectoryRecord.Size(); + pos.QuadPart *= -1; + ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::END, nullptr)); + m_endCentralDirectoryRecord.Read(m_stream.Get()); + + // find where the zip central directory exists. + std::uint64_t offsetStartOfCD = 0; + std::uint64_t totalNumberOfEntries = 0; + if (!m_endCentralDirectoryRecord.GetIsZip64()) + { + offsetStartOfCD = m_endCentralDirectoryRecord.GetStartOfCentralDirectory(); + totalNumberOfEntries = m_endCentralDirectoryRecord.GetNumberOfCentralDirectoryEntries(); + } + else + { // Make sure that we have a zip64 end of central directory locator + pos.QuadPart = m_endCentralDirectoryRecord.Size() + m_zip64Locator.Size(); + pos.QuadPart *= -1; + ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::END, nullptr)); + m_zip64Locator.Read(m_stream.Get()); + + // now read the end of zip central directory record + pos.QuadPart = m_zip64Locator.GetRelativeOffset(); + ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::START, nullptr)); + m_zip64EndOfCentralDirectory.Read(m_stream.Get()); + offsetStartOfCD = m_zip64EndOfCentralDirectory.GetOffsetStartOfCD(); + totalNumberOfEntries = m_zip64EndOfCentralDirectory.GetTotalNumberOfEntries(); + } + + // read the zip central directory + pos.QuadPart = offsetStartOfCD; + ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::START, nullptr)); + for (std::uint32_t index = 0; index < totalNumberOfEntries; index++) + { + auto centralFileHeader = CentralDirectoryFileHeader(); + centralFileHeader.Read(m_stream.Get(), m_endCentralDirectoryRecord.GetIsZip64()); + // TODO: ensure that there are no collisions on name! + m_centralDirectories.emplace_back(std::make_pair(centralFileHeader.GetFileName(), std::move(centralFileHeader))); + } + + if (m_endCentralDirectoryRecord.GetIsZip64()) + { // We should have no data between the end of the last central directory header and the start of the EoCD + ULARGE_INTEGER uPos = { 0 }; + ThrowHrIfFailed(m_stream->Seek({ 0 }, StreamBase::Reference::CURRENT, &uPos)); + ThrowErrorIfNot(Error::ZipHiddenData, (uPos.QuadPart == m_zip64Locator.GetRelativeOffset()), "hidden data unsupported"); + } +} + +// IStoreageObject +std::vector ZipObject::GetFileNames(FileNameOptions) +{ + std::vector result; + for (const auto& cd : m_centralDirectories) + { + result.push_back(cd.first); + } + return result; +} + +// ZipObject::GetFile has cache semantics. If not found on m_streams, get the file from the central directories. +// Not finding a file is non-fatal +ComPtr ZipObject::GetFile(const std::string& fileName) +{ + auto result = m_streams.find(fileName); + if (result == m_streams.end()) + { + // Find the central directory item in question + CentralDirectoryFileHeader* targetCDptr = nullptr; + for (auto& cd : m_centralDirectories) + { + if (cd.first == fileName) + { + targetCDptr = &cd.second; + } + } + if (!targetCDptr) + { + return {}; + } + CentralDirectoryFileHeader& targetCD = *targetCDptr; + + LARGE_INTEGER pos = { 0 }; + pos.QuadPart = targetCD.GetRelativeOffsetOfLocalHeader(); + ThrowHrIfFailed(m_stream->Seek(pos, MSIX::StreamBase::Reference::START, nullptr)); + LocalFileHeader lfh = LocalFileHeader(); + lfh.Read(m_stream.Get(), targetCD); + + auto fileStream = ComPtr::Make( + fileName, + targetCD.GetCompressionMethod() == CompressionType::Deflate, + targetCD.GetRelativeOffsetOfLocalHeader() + lfh.Size(), + targetCD.GetCompressedSize(), + m_stream.Get() + ); + + if (targetCD.GetCompressionMethod() == CompressionType::Deflate) + { + fileStream = ComPtr::Make(std::move(fileStream), targetCD.GetUncompressedSize()); + } + ComPtr result(fileStream); + m_streams.insert(std::make_pair(fileName, std::move(fileStream))); + return result; + } + return result->second; +} + +std::string ZipObject::GetFileName() +{ + return m_stream.As()->GetName(); +} + +ComPtr ZipObject::GetStream() +{ + return m_stream; +} + +MSIX::EndCentralDirectoryRecord& ZipObject::GetEndCentralDirectoryRecord() +{ + return m_endCentralDirectoryRecord; +} + +MSIX::Zip64EndOfCentralDirectoryLocator& ZipObject::GetZip64Locator() +{ + return m_zip64Locator; +} + +MSIX::Zip64EndOfCentralDirectoryRecord& ZipObject::GetZip64EndOfCentralDirectory() { - auto other = reinterpret_cast(storageObject.Get()); - m_endCentralDirectoryRecord = other->m_endCentralDirectoryRecord; - m_zip64Locator = other->m_zip64Locator; - m_zip64EndOfCentralDirectory = other->m_zip64EndOfCentralDirectory; - m_centralDirectories = std::move(other->m_centralDirectories); - m_stream = std::move(m_stream); + return m_zip64EndOfCentralDirectory; +} + +std::vector>& ZipObject::GetCentralDirectories() +{ + return m_centralDirectories; +} + +MSIX::ComPtr ZipObject::GetEntireZipFileStream(const std::string& fileName) +{ + // Find the file in the CD + CentralDirectoryFileHeader* targetCDptr = nullptr; + for (auto& cd : m_centralDirectories) + { + if (cd.first == fileName) + { + targetCDptr = &cd.second; + } + } + if (!targetCDptr) + { + return {}; + } + CentralDirectoryFileHeader& targetCD = *targetCDptr; + + LARGE_INTEGER pos = { 0 }; + pos.QuadPart = targetCD.GetRelativeOffsetOfLocalHeader(); + ThrowHrIfFailed(m_stream->Seek(pos, MSIX::StreamBase::Reference::START, nullptr)); + LocalFileHeader lfh = LocalFileHeader(); + lfh.Read(m_stream.Get(), targetCD); + + uint64_t streamSize = lfh.Size() + targetCD.GetCompressedSize() + (lfh.IsDataDescriptorBitSet() ? DataDescriptor{}.Size() : 0); + + return ComPtr::Make(targetCD.GetRelativeOffsetOfLocalHeader(), streamSize, m_stream.Get()); } } // namespace MSIX diff --git a/src/msix/msix.cpp b/src/msix/msix.cpp index f5de1b728..7dc2d4879 100644 --- a/src/msix/msix.cpp +++ b/src/msix/msix.cpp @@ -20,10 +20,11 @@ #include "AppxPackageWriter.hpp" #include "AppxBundleWriter.hpp" #include "ScopeExit.hpp" +#include "Signing.hpp" #include "VersionHelpers.hpp" #include "MappingFileParser.hpp" #include "FileStream.hpp" -#include "VectorStream.hpp" +#include "MemoryStream.hpp" #ifndef WIN32 // on non-win32 platforms, compile with -fvisibility=hidden @@ -62,7 +63,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE MsixGetLogTextUTF8(COTASKMEMALLOC* memalloc, } CATCH_RETURN(); MSIX_API HRESULT STDMETHODCALLTYPE CreateStreamOnFile( - char* utf8File, + LPCSTR utf8File, bool forRead, IStream** stream) noexcept try { @@ -154,8 +155,8 @@ MSIX_API HRESULT STDMETHODCALLTYPE CoCreateAppxBundleFactory( MSIX_API HRESULT STDMETHODCALLTYPE UnpackPackage( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, - char* utf8SourcePackage, - char* utf8Destination) noexcept try + LPCSTR utf8SourcePackage, + LPCSTR utf8Destination) noexcept try { ThrowErrorIfNot(MSIX::Error::InvalidParameter, (utf8SourcePackage != nullptr && utf8Destination != nullptr), @@ -172,7 +173,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackPackage( MSIX_API HRESULT STDMETHODCALLTYPE UnpackPackageFromPackageReader( MSIX_PACKUNPACK_OPTION packUnpackOptions, IAppxPackageReader* packageReader, - char* utf8Destination) noexcept try + LPCSTR utf8Destination) noexcept try { ThrowErrorIfNot(MSIX::Error::InvalidParameter, (packageReader != nullptr && utf8Destination != nullptr), @@ -192,7 +193,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackPackageFromStream( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, IStream* stream, - char* utf8Destination) noexcept try + LPCSTR utf8Destination) noexcept try { ThrowErrorIfNot(MSIX::Error::InvalidParameter, (stream != nullptr && utf8Destination != nullptr), @@ -216,8 +217,8 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundle( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, MSIX_APPLICABILITY_OPTIONS applicabilityOptions, - char* utf8SourcePackage, - char* utf8Destination) noexcept try + LPCSTR utf8SourcePackage, + LPCSTR utf8Destination) noexcept try { THROW_IF_BUNDLE_NOT_ENABLED ThrowErrorIfNot(MSIX::Error::InvalidParameter, @@ -234,7 +235,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundle( MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundleFromBundleReader( MSIX_PACKUNPACK_OPTION packUnpackOptions, IAppxBundleReader* bundleReader, - char* utf8Destination) noexcept try + LPCSTR utf8Destination) noexcept try { THROW_IF_BUNDLE_NOT_ENABLED ThrowErrorIfNot(MSIX::Error::InvalidParameter, @@ -255,7 +256,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundleFromStream( MSIX_VALIDATION_OPTION validationOption, MSIX_APPLICABILITY_OPTIONS applicabilityOptions, IStream* stream, - char* utf8Destination) noexcept try + LPCSTR utf8Destination) noexcept try { THROW_IF_BUNDLE_NOT_ENABLED ThrowErrorIfNot(MSIX::Error::InvalidParameter, @@ -281,8 +282,8 @@ MSIX_API HRESULT STDMETHODCALLTYPE UnpackBundleFromStream( MSIX_API HRESULT STDMETHODCALLTYPE PackPackage( MSIX_PACKUNPACK_OPTION packUnpackOptions, MSIX_VALIDATION_OPTION validationOption, - char* directoryPath, - char* outputPackage + LPCSTR directoryPath, + LPCSTR outputPackage ) noexcept try { ThrowErrorIfNot(MSIX::Error::InvalidParameter, @@ -312,6 +313,55 @@ MSIX_API HRESULT STDMETHODCALLTYPE PackPackage( return static_cast(MSIX::Error::OK); } CATCH_RETURN(); +MSIX_API HRESULT STDMETHODCALLTYPE SignPackage( + MSIX_SIGNING_OPTIONS signingOptions, + LPCSTR package, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + LPCSTR signingCertificate, + LPCSTR pass, + LPCSTR privateKey +) noexcept try +{ + ThrowErrorIf(MSIX::Error::InvalidParameter, + (package == nullptr || signingCertificate == nullptr), + "Invalid parameters"); + + if (signingCertificateFormat == MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN) + { + signingCertificateFormat = MSIX::DetermineCertificateFormat(signingCertificate); + + ThrowErrorIf(MSIX::Error::InvalidParameter, + signingCertificateFormat == MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN, + "Certificate format could not be determined"); + + ThrowErrorIf(MSIX::Error::InvalidParameter, + (MSIX::DoesCertificateFormatRequirePrivateKey(signingCertificateFormat) && privateKey == nullptr), + "Certificate format requires separate private key"); + } + + MSIX::ComPtr packageStream = + MSIX::ComPtr::Make(MSIX::utf8_to_wstring(package).c_str(), MSIX::FileStream::Mode::READ_UPDATE); + + MSIX::ComPtr certificateStream; + ThrowHrIfFailed(CreateStreamOnFile(signingCertificate, true, &certificateStream)); + + MSIX::ComPtr privateKeyStream; + if (MSIX::DoesCertificateFormatRequirePrivateKey(signingCertificateFormat)) + { + ThrowHrIfFailed(CreateStreamOnFile(privateKey, true, &privateKeyStream)); + } + + MSIX::ComPtr factory; + ThrowHrIfFailed(CoCreateAppxFactoryWithHeap(InternalAllocate, InternalFree, MSIX_VALIDATION_NONE, &factory)); + + MSIX::ComPtr reader; + ThrowHrIfFailed(factory->CreatePackageReader(packageStream.Get(), &reader)); + + MSIX::SignPackage(reader.Get(), signingCertificateFormat, certificateStream.Get(), pass, privateKeyStream.Get()); + + return static_cast(MSIX::Error::OK); +} CATCH_RETURN(); + MSIX_API HRESULT STDMETHODCALLTYPE PackBundle( MSIX_BUNDLE_OPTIONS bundleOptions, char* directoryPath, @@ -396,7 +446,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE PackBundle( if(manifestOnly) { - stream = MSIX::ComPtr::Make(&streamVector); + stream = MSIX::ComPtr::Make(&streamVector); } else { @@ -433,7 +483,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE PackBundle( std::string outputPath = fileListIterator->first; std::vector tempPackageVector; - auto tempPackageStream = MSIX::ComPtr::Make(&tempPackageVector); + auto tempPackageStream = MSIX::ComPtr::Make(&tempPackageVector); auto manifestStream = MSIX::ComPtr::Make(inputPath, MSIX::FileStream::Mode::READ); diff --git a/src/msix/pack/AppxBundleWriter.cpp b/src/msix/pack/AppxBundleWriter.cpp index 050d53b44..933b57686 100644 --- a/src/msix/pack/AppxBundleWriter.cpp +++ b/src/msix/pack/AppxBundleWriter.cpp @@ -5,6 +5,7 @@ #include "AppxPackaging.hpp" #include "AppxBundleWriter.hpp" +#include "AppxFactory.hpp" #include "MsixErrors.hpp" #include "Exceptions.hpp" #include "ContentType.hpp" @@ -14,7 +15,6 @@ #include "ScopeExit.hpp" #include "FileNameValidation.hpp" #include "StringHelper.hpp" -#include "VectorStream.hpp" #include #include diff --git a/src/msix/pack/AppxPackageWriter.cpp b/src/msix/pack/AppxPackageWriter.cpp index fc47d4cb1..dfa5364b7 100644 --- a/src/msix/pack/AppxPackageWriter.cpp +++ b/src/msix/pack/AppxPackageWriter.cpp @@ -14,6 +14,8 @@ #include "ScopeExit.hpp" #include "FileNameValidation.hpp" #include "StringHelper.hpp" +#include "SignatureCreator.hpp" +#include "StreamHelper.hpp" #include #include @@ -32,6 +34,16 @@ namespace MSIX { m_state = WriterState::Open; } + AppxPackageWriter::AppxPackageWriter(IPackage* packageToSign, std::unique_ptr&& accumulator) : + m_signatureAccumulator(std::move(accumulator)), m_contentTypeWriter(packageToSign->GetUnderlyingStorageObject()->GetFile(CONTENT_TYPES_XML).Get()) + { + m_factory = packageToSign->GetFactory(); + m_zipWriter = ComPtr::Make(packageToSign->GetUnderlyingStorageObject().As().Get()); + + // Remove the files that are modified by signing + m_zipWriter->RemoveFiles({ CONTENT_TYPES_XML, CODEINTEGRITY_CAT, APPXSIGNATURE_P7X }); + } + // IPackageWriter void AppxPackageWriter::PackPayloadFiles(const ComPtr& from) { @@ -57,6 +69,65 @@ namespace MSIX { failState.release(); } + void AppxPackageWriter::Close( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey) + { + bool signing = static_cast(m_signatureAccumulator); + ThrowErrorIf(Error::InvalidParameter, signing && signingCertificate == nullptr, "Writer opened for signing needs a certificate"); + + auto failState = MSIX::scope_exit([this] + { + this->m_state = WriterState::Failed; + }); + + ComPtr catalogStream; + + if (signing) + { + // Add content type for signature + m_contentTypeWriter.AddContentType(APPXSIGNATURE_P7X, ContentType::GetPayloadFileContentType(APPX_FOOTPRINT_FILE_TYPE_SIGNATURE), true); + + // Add content type for the catalog file if it exists + catalogStream = m_signatureAccumulator->GetCodeIntegrityStream(signingCertificateFormat, signingCertificate, privateKey); + if (catalogStream) + { + m_contentTypeWriter.AddContentType(CODEINTEGRITY_CAT, ContentType::GetPayloadFileContentType(APPX_FOOTPRINT_FILE_TYPE_CODEINTEGRITY), true); + } + } + + // Close content types and add it to package + m_contentTypeWriter.Close(); + auto contentTypeStream = m_contentTypeWriter.GetStream(); + AddFileToPackage(CONTENT_TYPES_XML, contentTypeStream.Get(), true, false, nullptr, false, false); + + if (signing) + { + // Add the catalog after the content types to preserve historical ordering + if (catalogStream) + { + AddFileToPackage(CODEINTEGRITY_CAT, catalogStream.Get(), true, false, nullptr, false, false); + } + + auto digestData = m_signatureAccumulator->GetSignatureObject(m_zipWriter.Get()); + auto signatureStream = SignatureCreator::Sign(digestData.Get(), signingCertificateFormat, signingCertificate, pass, privateKey); + AddFileToPackage(APPXSIGNATURE_P7X, signatureStream.Get(), true, false, nullptr, false, false); + } + + m_zipWriter->Close(); + + // Ensure that the stream does not have any additional data hanging off the end + ComPtr zipStream = m_zipWriter.As()->GetStream(); + ULARGE_INTEGER fileSize = { 0 }; + ThrowHrIfFailed(zipStream->Seek({ 0 }, StreamBase::Reference::CURRENT, &fileSize)); + ThrowHrIfFailed(zipStream->SetSize(fileSize)); + + failState.release(); + m_state = WriterState::Closed; + } + // IAppxPackageWriter HRESULT STDMETHODCALLTYPE AppxPackageWriter::AddPayloadFile(LPCWSTR fileName, LPCWSTR contentType, APPX_COMPRESSION_OPTION compressionOption, IStream *inputStream) noexcept try @@ -87,14 +158,11 @@ namespace MSIX { auto blockMapContentType = ContentType::GetPayloadFileContentType(APPX_FOOTPRINT_FILE_TYPE_BLOCKMAP); AddFileToPackage(APPXBLOCKMAP_XML, blockMapStream.Get(), true, false, blockMapContentType.c_str()); - // Close content types and add it to package - m_contentTypeWriter.Close(); - auto contentTypeStream = m_contentTypeWriter.GetStream(); - AddFileToPackage(CONTENT_TYPES_XML, contentTypeStream.Get(), true, false, nullptr); - - m_zipWriter->Close(); failState.release(); - m_state = WriterState::Closed; + + // Merge with standalone signing path, with no signing information. + Close(MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN, nullptr, nullptr, nullptr); + return static_cast(Error::OK); } CATCH_RETURN(); @@ -107,8 +175,7 @@ namespace MSIX { { this->m_state = WriterState::Failed; }); - ComPtr stream(inputStream); - ValidateAndAddPayloadFile(fileName, stream.Get(), compressionOption, contentType); + ValidateAndAddPayloadFile(fileName, inputStream, compressionOption, contentType); failState.release(); return static_cast(Error::OK); } CATCH_RETURN(); @@ -126,9 +193,8 @@ namespace MSIX { for(UINT32 i = 0; i < fileCount; i++) { std::string fileName = wstring_to_utf8(payloadFiles[i].fileName); - ComPtr stream(payloadFiles[i].inputStream); std::string contentType = wstring_to_utf8(payloadFiles[i].contentType); - ValidateAndAddPayloadFile(fileName, stream.Get(), payloadFiles[i].compressionOption, contentType.c_str()); + ValidateAndAddPayloadFile(fileName, payloadFiles[i].inputStream, payloadFiles[i].compressionOption, contentType.c_str()); } failState.release(); return static_cast(Error::OK); @@ -146,8 +212,7 @@ namespace MSIX { // TODO: use memoryLimit for how many files are going to be added for(UINT32 i = 0; i < fileCount; i++) { - ComPtr stream(payloadFiles[i].inputStream); - ValidateAndAddPayloadFile(payloadFiles[i].fileName, stream.Get(), payloadFiles[i].compressionOption, payloadFiles[i].contentType); + ValidateAndAddPayloadFile(payloadFiles[i].fileName, payloadFiles[i].inputStream, payloadFiles[i].compressionOption, payloadFiles[i].contentType); } failState.release(); return static_cast(Error::OK); @@ -164,7 +229,7 @@ namespace MSIX { } void AppxPackageWriter::AddFileToPackage(const std::string& name, IStream* stream, bool toCompress, - bool addToBlockMap, const char* contentType, bool forceContentTypeOverride) + bool addToBlockMap, const char* contentType, bool forceContentTypeOverride, bool forceDataDescriptor) { std::string opcFileName; // Don't encode [Content Type].xml @@ -199,6 +264,12 @@ namespace MSIX { auto& zipFileStream = fileInfo.second; + std::unique_ptr fileAccumulator; + if (m_signatureAccumulator) + { + fileAccumulator = m_signatureAccumulator->GetFileAccumulator(name); + } + std::uint64_t bytesToRead = uncompressedSize; std::uint32_t crc = 0; while (bytesToRead > 0) @@ -219,12 +290,17 @@ namespace MSIX { ULONG bytesWritten = 0; ThrowHrIfFailed(zipFileStream->Write(block.data(), static_cast(block.size()), &bytesWritten)); + // Send data to file accumulator for signature creation + if (fileAccumulator) + { + fileAccumulator->AccumulateRaw(block); + } + // Add block to blockmap if (addToBlockMap) { m_blockMapWriter.AddBlock(block, bytesWritten, toCompress); } - } if (toCompress) @@ -243,7 +319,16 @@ namespace MSIX { // This could be the compressed or uncompressed size auto streamSize = zipFileStream.As()->GetSize(); - m_zipWriter->EndFile(crc, streamSize, uncompressedSize, true); + m_zipWriter->EndFile(crc, streamSize, uncompressedSize, forceDataDescriptor); + + // Send entire zip stream to accumulator + if (fileAccumulator) + { + // We have to ensure that we reset the output stream position + ComPtr zipObj = m_zipWriter.As(); + Helper::StreamPositionReset positionReset{ zipObj->GetStream().Get() }; + fileAccumulator->AccumulateZip(zipObj->GetEntireZipFileStream(opcFileName).Get()); + } } void AppxPackageWriter::ValidateCompressionOption(APPX_COMPRESSION_OPTION compressionOpt) diff --git a/src/msix/pack/ContentType.cpp b/src/msix/pack/ContentType.cpp index eaac6c8ec..105d97613 100644 --- a/src/msix/pack/ContentType.cpp +++ b/src/msix/pack/ContentType.cpp @@ -120,19 +120,20 @@ namespace MSIX { const std::string ContentType::GetPayloadFileContentType(APPX_FOOTPRINT_FILE_TYPE footprintFile) { - if (footprintFile == APPX_FOOTPRINT_FILE_TYPE_MANIFEST) + switch (footprintFile) { + case APPX_FOOTPRINT_FILE_TYPE_MANIFEST: return "application/vnd.ms-appx.manifest+xml"; - } - if (footprintFile == APPX_FOOTPRINT_FILE_TYPE_BLOCKMAP) - { + case APPX_FOOTPRINT_FILE_TYPE_BLOCKMAP: return "application/vnd.ms-appx.blockmap+xml"; - } - if (footprintFile == APPX_FOOTPRINT_FILE_TYPE_SIGNATURE) - { + case APPX_FOOTPRINT_FILE_TYPE_SIGNATURE: return "application/vnd.ms-appx.signature"; + case APPX_FOOTPRINT_FILE_TYPE_CODEINTEGRITY: + return "application/vnd.ms-pkiseccat"; + case APPX_FOOTPRINT_FILE_TYPE_CONTENTGROUPMAP: + return "application/vnd.ms-appx.streammap+xml"; } - // TODO: add other ones if needed, otherwise throw + ThrowErrorAndLog(Error::NotSupported, "Payload file content type not found"); } diff --git a/src/msix/pack/ContentTypeWriter.cpp b/src/msix/pack/ContentTypeWriter.cpp index 67d62b321..619498c1b 100644 --- a/src/msix/pack/ContentTypeWriter.cpp +++ b/src/msix/pack/ContentTypeWriter.cpp @@ -6,6 +6,8 @@ #include "XmlWriter.hpp" #include "ContentTypeWriter.hpp" #include "Encoding.hpp" +#include "StreamHelper.hpp" +#include "AppxFactory.hpp" #include #include @@ -32,16 +34,37 @@ namespace MSIX { static const char* partNameAttribute = "PartName"; // - ContentTypeWriter::ContentTypeWriter() : m_xmlWriter(XmlWriter(typesElement, true)) + ContentTypeWriter::ContentTypeWriter() : m_xmlWriter(typesElement, true) { m_xmlWriter.AddAttribute(xmlnsAttribute, typesNamespace); } + ContentTypeWriter::ContentTypeWriter(IStream* stream) + { + // Check to see if we already have signing content types + std::string sourceXml = Helper::CreateStringFromStream(stream); + + // Determine if the signature file overrides are already present + std::string signaturePartNameSearch = GetPartNameSearchString(APPXSIGNATURE_P7X); + std::string ciPartNameSearch = GetPartNameSearchString(CODEINTEGRITY_CAT); + m_hasSignatureOverride = (sourceXml.rfind(signaturePartNameSearch) != std::string::npos); + m_hasCIOverride = (sourceXml.rfind(ciPartNameSearch) != std::string::npos); + + m_xmlWriter.Initialize(sourceXml, typesElement); + } + // File extension to MIME value map that are added as default elements // If the extension is already in the map and its content type is different or // if the file doesn't have an extensions AddOverride is called. void ContentTypeWriter::AddContentType(const std::string& name, const std::string& contentType, bool forceOverride) { + // Skip the signature files if they are already present + if ((name == APPXSIGNATURE_P7X && m_hasSignatureOverride) || + (name == CODEINTEGRITY_CAT && m_hasCIOverride)) + { + return; + } + auto percentageEncodedName = Encoding::EncodeFileName(name); auto filename = percentageEncodedName; @@ -109,4 +132,10 @@ namespace MSIX { m_xmlWriter.AddAttribute(partNameAttribute, partName); m_xmlWriter.CloseElement(); } + + // Gets the search string from a file name; AppxSignature.p7x => "/AppxSignature.p7x" + std::string ContentTypeWriter::GetPartNameSearchString(const std::string& fileName) + { + return "\"/" + fileName + '"'; + } } \ No newline at end of file diff --git a/src/msix/pack/Signing.cpp b/src/msix/pack/Signing.cpp new file mode 100644 index 000000000..6363a1b21 --- /dev/null +++ b/src/msix/pack/Signing.cpp @@ -0,0 +1,284 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#include "Signing.hpp" +#include "Exceptions.hpp" +#include "FileStream.hpp" +#include "AppxPackageObject.hpp" +#include "StorageObject.hpp" +#include "ContentTypeWriter.hpp" +#include "AppxPackageWriter.hpp" +#include "StreamHelper.hpp" +#include "SHA256HashStream.hpp" + +#include +#include + +// Enable this to output debug data for the package contents hash. +#define MSIX_DEBUG_PACKAGE_CONTENT_HASH 0 +#if MSIX_DEBUG_PACKAGE_CONTENT_HASH +#define MSIX_DEBUG_PACKAGE_CONTENT_HASH_OUTPUT_DIR ".\\" +#endif + +namespace MSIX +{ + +// Given a file name, determine the format of the certificate. +MSIX_CERTIFICATE_FORMAT DetermineCertificateFormat(LPCSTR file) +{ + std::string fileStr{ file }; + + // Since we only have one supported format currently, just go directly for it. + if (fileStr.length() > 4) + { + std::string ext = fileStr.substr(fileStr.length() - 4); + std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); + + if (ext == ".pfx") + { + return MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_PFX; + } + } + + return MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN; +} + +// Given a format, is a separate private key file required? +bool DoesCertificateFormatRequirePrivateKey(MSIX_CERTIFICATE_FORMAT format) +{ + switch (format) + { + case MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_PFX: + return false; + case MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN: + ThrowErrorAndLog(Error::InvalidState, "Internal error; format should be known by now"); + } + + UNEXPECTED +} + +// Signs a package in-place with the given certificate. +// The package writer itself has the ability to sign the package as it is being created, +// but this is an after the fact signing. To converge the flows, we create the +// SignatureAccumulator here, and catch it up with all of the files already present in the +// package. Creating the package writer and closing it handles the rest. +void SignPackage( + IAppxPackageReader* package, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + const char* pass, + IStream* privateKey) +{ + std::unique_ptr signatureAccumulator = std::make_unique(); + + // Get the publisher from the manifest for verifying later + // TODO: Figure out that flow from both paths... + + // Send all of the files to the accumulator, skipping the files modified by signing + auto packageAsIPackage = ComPtr::From(package); + auto packageAsIStorageObject = ComPtr::From(package); + auto underlyingStorage = packageAsIPackage->GetUnderlyingStorageObject(); + auto underlyingZipObject = underlyingStorage.As(); + auto filenames = underlyingStorage->GetFileNames(FileNameOptions::All); + + for (const auto& filename : filenames) + { + // These are the files created by signing; they are not to be included from the original package. + if (std::find(signingModifiedFiles.cbegin(), signingModifiedFiles.cend(), filename) != signingModifiedFiles.cend()) + { + continue; + } + + auto fileAccumulator = signatureAccumulator->GetFileAccumulator(filename); + + if (fileAccumulator->WantsRaw()) + { + auto validatedStream = packageAsIStorageObject->GetFile(filename); + fileAccumulator->AccumulateRaw(validatedStream.Get()); + } + + if (fileAccumulator->WantsZip()) + { + auto entireZipStream = underlyingZipObject->GetEntireZipFileStream(filename); + fileAccumulator->AccumulateZip(entireZipStream.Get()); + } + } + + // Create a package writer from the reader, giving it our objects. With this, the new package writer will + // be at the same point as it would be if we were doing signing while creating the package. Then we just + // continue that process with Close. + auto packageWriter = ComPtr::Make(packageAsIPackage.Get(), std::move(signatureAccumulator)); + + packageWriter->Close(signingCertificateFormat, signingCertificate, pass, privateKey); +} + +// SignatureAccumulator + +std::unique_ptr SignatureAccumulator::GetFileAccumulator(std::string partName) +{ + return std::unique_ptr{ new FileAccumulator{ *this, std::move(partName), createCICatalog } }; +} + +ComPtr SignatureAccumulator::GetCodeIntegrityStream( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + IStream* privateKey) +{ + if (!createCICatalog) + { + return {}; + } + + NOTIMPLEMENTED +} + +ComPtr SignatureAccumulator::GetSignatureObject(IZipWriter* zipWriter) +{ + ComPtr result = GetSignatureObject(); + + // Blockmap and Content Types hashes get set directly by the FileAccumulator closing out. + // The CI catalog hash similarly gets set when the code integrity stream is created. + // This leaves only the full package hash and the central directory hash. + + // Simply copy the zip hash over + GetZipHasher().FinalizeAndGetHashValue(result->GetFileRecordsDigest()); + + // Create the central directory as it currently exists and hash it + ComPtr cdHash = ComPtr::Make(); + zipWriter->WriteCentralDirectoryToStream(cdHash.Get()); + cdHash->FinalizeAndGetHashValue(result->GetCentralDirectoryDigest()); + + return result; +} + +// FileAccumulator + +SignatureAccumulator::FileAccumulator::FileAccumulator(SignatureAccumulator& accumulator, std::string partName, bool createCICatalog) : + signatureAccumulator(accumulator), name(std::move(partName)) +{ + // We always need the blockmap and content types raw data, and we only need to see the other + // files if we are creating the CI catalog. + if (name == APPXBLOCKMAP_XML) + { + isBlockmap = true; + } + else if (name == CONTENT_TYPES_XML) + { + isContentTypes = true; + } + else if (name == APPXSIGNATURE_P7X) + { + // Ignore the signature file when it comes in + wantsRaw = false; + wantsZip = false; + } + else if (!createCICatalog) + { + wantsRaw = false; + } +} + +SignatureAccumulator::FileAccumulator::~FileAccumulator() +{ + if (isBlockmap) + { + GetRawHasher().FinalizeAndGetHashValue(signatureAccumulator.GetSignatureObject()->GetAppxBlockMapDigest()); + } + else if (isContentTypes) + { + GetRawHasher().FinalizeAndGetHashValue(signatureAccumulator.GetSignatureObject()->GetContentTypesDigest()); + } + else + { + // TODO: Implement CI catalog + } +} + +bool SignatureAccumulator::FileAccumulator::AccumulateRaw(IStream* stream) +{ + if (wantsRaw) + { + // These just need their entire contents hashed + if (isBlockmap || isContentTypes) + { + auto& hasher = GetRawHasher(); + + for (const auto& bytes : Helper::StreamProcessor{ stream, 1 << 20 }) + { + hasher.HashData(bytes.data(), bytes.size()); + } + } + else + { + // Only other reason to want raw is to inspect this for a PE header + // TODO: But CI catalog not yet implemented + NOTIMPLEMENTED + } + } + + return wantsRaw; +} + +bool SignatureAccumulator::FileAccumulator::AccumulateRaw(const std::vector& data) +{ + if (wantsRaw) + { + // These just need their entire contents hashed + if (isBlockmap || isContentTypes) + { + GetRawHasher().HashData(data.data(), data.size()); + } + else + { + // Only other reason to want raw is to inspect this for a PE header + // TODO: But CI catalog not yet implemented + NOTIMPLEMENTED + } + } + + return wantsRaw; +} + +#if MSIX_DEBUG_PACKAGE_CONTENT_HASH + +ComPtr MsixDebugPackageContentHashCreateFile(LPCSTR fileName) +{ + ComPtr result; + ThrowHrIfFailed(CreateStreamOnFile(fileName, false, &result)); + return result; +} + +#endif + +bool SignatureAccumulator::FileAccumulator::AccumulateZip(IStream* stream) +{ + if (wantsZip) + { +#if MSIX_DEBUG_PACKAGE_CONTENT_HASH + static ComPtr contentsOutput = MsixDebugPackageContentHashCreateFile(MSIX_DEBUG_PACKAGE_CONTENT_HASH_OUTPUT_DIR "contents.bin"); + static ComPtr detailsOutput = MsixDebugPackageContentHashCreateFile(MSIX_DEBUG_PACKAGE_CONTENT_HASH_OUTPUT_DIR "contents.csv"); + + ComPtr streamInternal = ComPtr::From(stream); + + std::ostringstream strstr; + strstr << name << ',' << streamInternal->GetSize() << std::endl; + Helper::WriteStringToStream(detailsOutput, strstr.str()); +#endif + + auto& hasher = GetZipHasher(); + + for (const auto& bytes : Helper::StreamProcessor{ stream, 1 << 20 }) + { + hasher.HashData(bytes.data(), bytes.size()); + +#if MSIX_DEBUG_PACKAGE_CONTENT_HASH + ThrowHrIfFailed(contentsOutput->Write(static_cast(bytes.data()), static_cast(bytes.size()), nullptr)); +#endif + } + } + + return wantsZip; +} + +} diff --git a/src/msix/pack/XmlWriter.cpp b/src/msix/pack/XmlWriter.cpp index cbaa3635b..d0f023649 100644 --- a/src/msix/pack/XmlWriter.cpp +++ b/src/msix/pack/XmlWriter.cpp @@ -3,7 +3,7 @@ // See LICENSE file in the project root for full license information. // #include "XmlWriter.hpp" -#include "StringStream.hpp" +#include "MemoryStream.hpp" #include "ComHelper.hpp" #include "Exceptions.hpp" #include "MsixErrors.hpp" @@ -14,123 +14,148 @@ namespace MSIX { - const static char* xmlStart = "<"; + void XmlWriter::Initialize(const std::string& source, const std::string& root) + { + // Verify that the string actually ends with the proper end element + std::string endElementString; + endElementString += ""; - // Adds xml header declaration plus the name of the root element - void XmlWriter::StartWrite(const std::string& root, bool standalone) - { - m_elements.emplace(root); - Write(xmlStart); - if (standalone) - { - Write("yes"); - } - else - { - Write("no"); - } - Write(xmlStartEnd); - Write(root); - m_state = State::OpenElement; - } + ThrowErrorIf(Error::InvalidParameter, source.length() < endElementString.length(), "not enough bytes in string"); + + std::string endElementCandidate = source.substr(source.length() - endElementString.length()); + ThrowErrorIf(Error::InvalidParameter, endElementCandidate != endElementString, "stream did not end with end element"); + + // Write out everything but the end element + m_stream = ComPtr::Make(); + + ULONG toWrite = static_cast(source.length() - endElementString.length()); + ULONG written = 0; + ThrowHrIfFailed(m_stream->Write(static_cast(source.data()), toWrite, &written)); + ThrowErrorIf(Error::FileWrite, (toWrite != written), "write failed"); - void XmlWriter::StartElement(const std::string& name) + // Set us up to close things out later + m_elements.emplace(root); + m_state = State::ClosedElement; + } + + const static char* xmlStart = "<"; + + // Adds xml header declaration plus the name of the root element + void XmlWriter::StartWrite(const std::string& root, bool standalone) + { + m_elements.emplace(root); + Write(xmlStart); + if (standalone) { - ThrowErrorIf(Error::XmlError, m_state == State::Finish, "Invalid call, xml already finished"); - m_elements.emplace(name); - // If the state is open, then we are adding a child element to the previous one. We need to close that element's - // tag and add the new one. If the state is closed, there is no need to close the tag as it had already been closed. - if (m_state == State::OpenElement) - { - Write(">"); // close parent element - } - Write("<"); - Write(name); - m_state = State::OpenElement; + Write("yes"); } - - void XmlWriter::CloseElement() + else { - ThrowErrorIf(Error::XmlError, m_state == State::Finish, "Invalid call, xml already finished"); - // If the state is open and we are closing an element, it means that it doesn't have any child, so we can - // just close it with "/>". If we are closing an element and a closing just happened, it means that we are - // closing an element that has child elements, so it must be closed with - if (m_state == State::OpenElement) - { - Write("/>"); - } - else // State::ClosedElement - { - // - Write(""); - } - m_state = State::ClosedElement; - m_elements.pop(); - if (m_elements.size() == 0) - { - m_state = State::Finish; - } + Write("no"); } + Write(xmlStartEnd); + Write(root); + m_state = State::OpenElement; + } - void XmlWriter::AddAttribute(const std::string& name, const std::string& value) + void XmlWriter::StartElement(const std::string& name) + { + ThrowErrorIf(Error::XmlError, m_state == State::Finish, "Invalid call, xml already finished"); + m_elements.emplace(name); + // If the state is open, then we are adding a child element to the previous one. We need to close that element's + // tag and add the new one. If the state is closed, there is no need to close the tag as it had already been closed. + if (m_state == State::OpenElement) { - ThrowErrorIf(Error::XmlError, (m_state == State::Finish) || (m_state == State::ClosedElement), "Invalid call to AddAttribute"); - Write(" "); // always write a space. We just wrote either an element or an attribute - Write(name); // name="value" - Write("=\""); - WriteTextValue(value); - Write("\""); + Write(">"); // close parent element } + Write("<"); + Write(name); + m_state = State::OpenElement; + } - ComPtr XmlWriter::GetStream() + void XmlWriter::CloseElement() + { + ThrowErrorIf(Error::XmlError, m_state == State::Finish, "Invalid call, xml already finished"); + // If the state is open and we are closing an element, it means that it doesn't have any child, so we can + // just close it with "/>". If we are closing an element and a closing just happened, it means that we are + // closing an element that has child elements, so it must be closed with + if (m_state == State::OpenElement) { - ThrowErrorIf(Error::XmlError, m_state != State::Finish, "Invalid call, the stream can only be accessed when the writer is done"); - return m_stream; + Write("/>"); } - - // Following msxml6 rule for text values - // all ampersands (&) are replaced by & - // all open angle brackets (<) are replaced by < - // all closing angle brackets (>) are replaced by > - // and all #xD characters are replaced by - void XmlWriter::WriteTextValue(const std::string& value) + else // State::ClosedElement { - for(int i = 0; i < value.size(); i++) - { - if (value[i] == '&') - { - Write("&"); - } - else if (value[i] == '<') - { - Write("<"); - } - else if (value[i] == '>') - { - Write(">"); - } - else if (value[i] == 0xd) - { - Write(" "); - } - else - { - Write(value[i]); - } - } + // + Write(""); } - - void XmlWriter::Write(const std::string& toWrite) + m_state = State::ClosedElement; + m_elements.pop(); + if (m_elements.size() == 0) { - Helper::WriteStringToStream(m_stream, toWrite); + m_state = State::Finish; } + } - void XmlWriter::Write(const char toWrite) + void XmlWriter::AddAttribute(const std::string& name, const std::string& value) + { + ThrowErrorIf(Error::XmlError, (m_state == State::Finish) || (m_state == State::ClosedElement), "Invalid call to AddAttribute"); + Write(" "); // always write a space. We just wrote either an element or an attribute + Write(name); // name="value" + Write("=\""); + WriteTextValue(value); + Write("\""); + } + + ComPtr XmlWriter::GetStream() + { + ThrowErrorIf(Error::XmlError, m_state != State::Finish, "Invalid call, the stream can only be accessed when the writer is done"); + return m_stream; + } + +// Following msxml6 rule for text values +// all ampersands (&) are replaced by & +// all open angle brackets (<) are replaced by < +// all closing angle brackets (>) are replaced by > +// and all #xD characters are replaced by + void XmlWriter::WriteTextValue(const std::string& value) + { + for (size_t i = 0; i < value.size(); i++) { - Helper::WriteStringToStream(m_stream, std::string(1, toWrite)); + if (value[i] == '&') + { + Write("&"); + } + else if (value[i] == '<') + { + Write("<"); + } + else if (value[i] == '>') + { + Write(">"); + } + else if (value[i] == 0xd) + { + Write(" "); + } + else + { + Write(value[i]); + } } + } + + void XmlWriter::Write(const std::string& toWrite) + { + Helper::WriteStringToStream(m_stream, toWrite); + } + void XmlWriter::Write(const char toWrite) + { + Helper::WriteStringToStream(m_stream, std::string(1, toWrite)); + } } diff --git a/src/msix/pack/ZipObjectWriter.cpp b/src/msix/pack/ZipObjectWriter.cpp index 68e4f6a51..b81b70bea 100644 --- a/src/msix/pack/ZipObjectWriter.cpp +++ b/src/msix/pack/ZipObjectWriter.cpp @@ -12,73 +12,79 @@ #include "StreamHelper.hpp" #include "Encoding.hpp" -namespace MSIX { +#include - // We only use this for writting. If we ever decide to validate it, it needs to move to - // ZipObject and ZipObjectReader must validate it - class DataDescriptor final : public Meta::StructuredObject< - Meta::Field4Bytes, // 0 - data descriptor header signature 4 bytes(0x08074b50) - Meta::Field4Bytes, // 1 - crc -32 4 bytes - Meta::Field8Bytes, // 2 - compressed size 8 bytes(zip64) - Meta::Field8Bytes // 3 - uncompressed size 8 bytes(zip64) - > - { - public: - DataDescriptor(std::uint32_t crc, std::uint64_t compressSize, std::uint64_t uncompressSize) - { - Field<0>() = static_cast(Signatures::DataDescriptor); - Field<1>() = crc; - Field<2>() = compressSize; - Field<3>() = uncompressSize; - } - }; +namespace MSIX { - ZipObjectWriter::ZipObjectWriter(const ComPtr& stream) : ZipObject(stream) + ZipObjectWriter::ZipObjectWriter(IStream* stream) { + m_zipObject = ComPtr::Make(stream, false); } // This is used for editing a package (aka signing) - ZipObjectWriter::ZipObjectWriter(const ComPtr& storageObject) : ZipObject(storageObject) + ZipObjectWriter::ZipObjectWriter(IZipObject* zipObject) : m_zipObject(zipObject) { // The storage object provided should had already initialize all the data. - ThrowErrorIfNot(Error::Zip64EOCDRecord, m_endCentralDirectoryRecord.GetIsZip64(), + ThrowErrorIfNot(Error::Zip64EOCDRecord, m_zipObject->GetEndCentralDirectoryRecord().GetIsZip64(), "Editing non zip64 packages not supported"); // Move the stream at the start of central directory record so we can start overwritting. - // Central directory data in already in m_centralDirectories. + // Central directory data is already in m_centralDirectories. LARGE_INTEGER pos = {0}; - pos.QuadPart = m_zip64EndOfCentralDirectory.GetOffsetStartOfCD(); - ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::START, nullptr)); + pos.QuadPart = m_zipObject->GetZip64EndOfCentralDirectory().GetOffsetStartOfCD(); + ThrowHrIfFailed(m_zipObject->GetStream()->Seek(pos, StreamBase::Reference::START, nullptr)); + } + + // IZipObject + ComPtr ZipObjectWriter::GetStream() + { + return m_zipObject->GetStream(); + } + + MSIX::EndCentralDirectoryRecord& ZipObjectWriter::GetEndCentralDirectoryRecord() + { + return m_zipObject->GetEndCentralDirectoryRecord(); } - // IStorage - std::vector ZipObjectWriter::GetFileNames(FileNameOptions options) + MSIX::Zip64EndOfCentralDirectoryLocator& ZipObjectWriter::GetZip64Locator() { - // TODO: implement - NOTIMPLEMENTED; + return m_zipObject->GetZip64Locator(); } - ComPtr ZipObjectWriter::GetFile(const std::string& fileName) + MSIX::Zip64EndOfCentralDirectoryRecord& ZipObjectWriter::GetZip64EndOfCentralDirectory() { - // TODO: implement - NOTIMPLEMENTED; + return m_zipObject->GetZip64EndOfCentralDirectory(); + } + + std::vector>& ZipObjectWriter::GetCentralDirectories() + { + return m_zipObject->GetCentralDirectories(); + } + + MSIX::ComPtr ZipObjectWriter::GetEntireZipFileStream(const std::string& fileName) + { + return m_zipObject->GetEntireZipFileStream(fileName); } // IZipWriter std::pair> ZipObjectWriter::PrepareToAddFile(const std::string& name, bool isCompressed) { + auto zipStream = m_zipObject->GetStream(); + ThrowErrorIf(Error::InvalidState, m_state != ZipObjectWriter::State::ReadyForLfhOrClose, "Invalid zip writer state"); - auto result = m_centralDirectories.find(name); - if (result != m_centralDirectories.end()) + for (const auto& cd : m_zipObject->GetCentralDirectories()) { - auto message = "Adding duplicated file " + Encoding::DecodeFileName(name) + "to package"; - ThrowErrorAndLog(Error::DuplicateFile, message.c_str()); + if (cd.first == name) + { + auto message = "Adding duplicated file " + Encoding::DecodeFileName(name) + "to package"; + ThrowErrorAndLog(Error::DuplicateFile, message.c_str()); + } } // Get position were the lfh is going to be written ULARGE_INTEGER pos = {0}; - ThrowHrIfFailed(m_stream->Seek({0}, StreamBase::Reference::CURRENT, &pos)); + ThrowHrIfFailed(zipStream->Seek({0}, StreamBase::Reference::CURRENT, &pos)); // track the sequence of file names to sort the central directory upon Close m_fileNameSequence.push_back(name); @@ -86,22 +92,24 @@ namespace MSIX { // Write lfh LocalFileHeader lfh; lfh.SetData(name, isCompressed); - lfh.WriteTo(m_stream); + lfh.WriteTo(zipStream); m_lastLFH = std::make_pair(static_cast(pos.QuadPart), std::move(lfh)); m_state = ZipObjectWriter::State::ReadyForFile; - ComPtr zipStream = ComPtr::Make(name, isCompressed, m_stream.Get()); + ComPtr newZipStream = ComPtr::Make(name, isCompressed, zipStream.Get()); if (isCompressed) { - zipStream = ComPtr::Make(zipStream); + newZipStream = ComPtr::Make(newZipStream); } - return std::make_pair(static_cast(m_lastLFH.second.Size()), std::move(zipStream)); + return std::make_pair(static_cast(m_lastLFH.second.Size()), std::move(newZipStream)); } void ZipObjectWriter::EndFile(std::uint32_t crc, std::uint64_t compressedSize, std::uint64_t uncompressedSize, bool forceDataDescriptor) { + auto zipStream = m_zipObject->GetStream(); + ThrowErrorIf(Error::InvalidState, m_state != ZipObjectWriter::State::ReadyForFile, "Invalid zip writer state"); if (forceDataDescriptor || @@ -110,66 +118,112 @@ namespace MSIX { { // Create and write data descriptor DataDescriptor descriptor = DataDescriptor(crc, compressedSize, uncompressedSize); - descriptor.WriteTo(m_stream); + descriptor.WriteTo(zipStream); } else { // The sizes can fit in the LFH, rewrite it with the new data - Helper::StreamPositionReset resetAfterLFHWrite{ m_stream.Get() }; + Helper::StreamPositionReset resetAfterLFHWrite{ zipStream.Get() }; LARGE_INTEGER lfhLocation; lfhLocation.QuadPart = static_cast(m_lastLFH.first); - ThrowHrIfFailed(m_stream->Seek(lfhLocation, StreamBase::Reference::START, nullptr)); + ThrowHrIfFailed(zipStream->Seek(lfhLocation, StreamBase::Reference::START, nullptr)); // We cannot change the size of the LFH, ensure that we don't accidentally size_t currentSize = m_lastLFH.second.Size(); m_lastLFH.second.SetData(crc, compressedSize, uncompressedSize); ThrowErrorIf(Error::Unexpected, currentSize != m_lastLFH.second.Size(), "Cannot change the LFH size when updating it"); - m_lastLFH.second.WriteTo(m_stream); + m_lastLFH.second.WriteTo(zipStream); } // Create and add cdh to map CentralDirectoryFileHeader cdh; cdh.SetData(m_lastLFH.second.GetFileName(), crc, compressedSize, uncompressedSize, m_lastLFH.first, m_lastLFH.second.GetCompressionMethod(), forceDataDescriptor); - m_centralDirectories.insert(std::make_pair(m_lastLFH.second.GetFileName(), std::move(cdh))); + m_zipObject->GetCentralDirectories().emplace_back(std::make_pair(m_lastLFH.second.GetFileName(), std::move(cdh))); m_state = ZipObjectWriter::State::ReadyForLfhOrClose; } - void ZipObjectWriter::Close() + void ZipObjectWriter::RemoveFiles(const std::vector& files) + { + std::vector centralDirectoryIndexes; + + // Search from the back to find all of the files, as they are most likely there + for (size_t i = m_zipObject->GetCentralDirectories().size(); i > 0; --i) + { + const auto& cd = m_zipObject->GetCentralDirectories()[i - 1]; + auto itr = std::find(files.begin(), files.end(), cd.first); + if (itr != files.end()) + { + centralDirectoryIndexes.emplace_back(i - 1); + + // Early out if we find all of the files + if (centralDirectoryIndexes.size() == files.size()) + { + break; + } + } + } + + // None of the given files were found + if (centralDirectoryIndexes.empty()) + { + return; + } + + // Ensure that all of the files are at the end of the stream, + // at least until we want to support more adventurous editing. + size_t minimumIndex = m_zipObject->GetCentralDirectories().size() - centralDirectoryIndexes.size(); + for (size_t i : centralDirectoryIndexes) + { + ThrowErrorIf(Error::NotSupported, i < minimumIndex, "Removing files from the middle of the archive is not supported"); + } + + // Now that we know we have a contiguous block of files at the end, we can safely remove them from the stream. + // Do that by simply moving the stream to point to the start of the LFH of the file. + CentralDirectoryFileHeader& cdToRemove = m_zipObject->GetCentralDirectories()[minimumIndex].second; + LARGE_INTEGER pos = { 0 }; + pos.QuadPart = cdToRemove.GetRelativeOffsetOfLocalHeader(); + ThrowHrIfFailed(m_zipObject->GetStream()->Seek(pos, MSIX::StreamBase::Reference::START, nullptr)); + + // Remove the CD entries at the end + m_zipObject->GetCentralDirectories().resize(minimumIndex); + } + + void ZipObjectWriter::WriteCentralDirectoryToStream(IStream* stream) { ThrowErrorIf(Error::InvalidState, m_state != ZipObjectWriter::State::ReadyForLfhOrClose, "Invalid zip writer state"); + // Write central directories - ULARGE_INTEGER startOfCdh = {0}; - ThrowHrIfFailed(m_stream->Seek({0}, StreamBase::Reference::CURRENT, &startOfCdh)); + ULARGE_INTEGER offsetToStartOfCD = { 0 }; + ThrowHrIfFailed(m_zipObject->GetStream()->Seek({ 0 }, StreamBase::Reference::CURRENT, &offsetToStartOfCD)); + std::size_t cdhsSize = 0; - for (const auto& fileName : m_fileNameSequence) + for (auto& cdh : m_zipObject->GetCentralDirectories()) { - auto it = m_centralDirectories.find(fileName); - if (it != m_centralDirectories.end()) - { - auto& cdh = it->second; - cdhsSize += cdh.Size(); - cdh.WriteTo(m_stream); - } + cdhsSize += cdh.second.Size(); + cdh.second.WriteTo(stream); } - m_fileNameSequence.clear(); // Write zip64 end of cds - ULARGE_INTEGER startOfZip64EndOfCds = {0}; - ThrowHrIfFailed(m_stream->Seek({0}, StreamBase::Reference::CURRENT, &startOfZip64EndOfCds)); - m_zip64EndOfCentralDirectory.SetData(m_centralDirectories.size(), static_cast(cdhsSize), - static_cast(startOfCdh.QuadPart)); - m_zip64EndOfCentralDirectory.WriteTo(m_stream); + ULARGE_INTEGER startOfZip64EndOfCds{}; + startOfZip64EndOfCds.QuadPart = offsetToStartOfCD.QuadPart + cdhsSize; + m_zipObject->GetZip64EndOfCentralDirectory().SetData(m_zipObject->GetCentralDirectories().size(), static_cast(cdhsSize), + static_cast(offsetToStartOfCD.QuadPart)); + m_zipObject->GetZip64EndOfCentralDirectory().WriteTo(stream); // Write zip64 locator - m_zip64Locator.SetData(static_cast(startOfZip64EndOfCds.QuadPart)); - m_zip64Locator.WriteTo(m_stream); + m_zipObject->GetZip64Locator().SetData(static_cast(startOfZip64EndOfCds.QuadPart)); + m_zipObject->GetZip64Locator().WriteTo(stream); // Because we only use zip64, EndCentralDirectoryRecord never changes - m_endCentralDirectoryRecord.WriteTo(m_stream); + m_zipObject->GetEndCentralDirectoryRecord().WriteTo(stream); + } + void ZipObjectWriter::Close() + { + WriteCentralDirectoryToStream(m_zipObject->GetStream().Get()); m_state = ZipObjectWriter::State::Closed; } -} \ No newline at end of file +} diff --git a/src/msix/unpack/AppxSignature.cpp b/src/msix/unpack/AppxSignature.cpp index b78db7cde..564fd41fc 100644 --- a/src/msix/unpack/AppxSignature.cpp +++ b/src/msix/unpack/AppxSignature.cpp @@ -78,19 +78,19 @@ AppxSignatureObject::AppxSignatureObject(IMsixFactory* factory, MSIX_VALIDATION_ } } -ComPtr AppxSignatureObject::GetValidationStream(const std::string& part, const ComPtr& stream) +ComPtr AppxSignatureObject::GetValidationStream(const std::string& part, const ComPtr& stream) { if (m_hasDigests) { - if (part == std::string("AppxBlockMap.xml")) + if (part == std::string(APPXBLOCKMAP_XML)) { // This stream implementation will throw if the underlying stream does not match the digest return ComPtr::Make(stream, this->GetAppxBlockMapDigest()); } - else if (part == std::string("[Content_Types].xml")) - { // This stream implementation will throw if the underlying stream does not match the digest' + else if (part == std::string(CONTENT_TYPES_XML)) + { // This stream implementation will throw if the underlying stream does not match the digest return ComPtr::Make(stream, this->GetContentTypesDigest()); } - else if (part == std::string("AppxMetadata/CodeIntegrity.cat")) + else if (part == std::string(CODEINTEGRITY_CAT)) { // This stream implementation will throw if the underlying stream does not match the digest return ComPtr::Make(stream, this->GetCodeIntegrityDigest()); } diff --git a/src/msix/unpack/ZipObjectReader.cpp b/src/msix/unpack/ZipObjectReader.cpp deleted file mode 100644 index c4f3c9332..000000000 --- a/src/msix/unpack/ZipObjectReader.cpp +++ /dev/null @@ -1,117 +0,0 @@ -// -// Copyright (C) 2019 Microsoft. All rights reserved. -// See LICENSE file in the project root for full license information. -// - -#include "ZipObjectReader.hpp" -#include "ComHelper.hpp" -#include "ZipFileStream.hpp" -#include "InflateStream.hpp" - -#include - -namespace MSIX { - - ZipObjectReader::ZipObjectReader(const ComPtr& stream) : ZipObject(stream) - { - LARGE_INTEGER pos = {0}; - pos.QuadPart = m_endCentralDirectoryRecord.Size(); - pos.QuadPart *= -1; - ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::END, nullptr)); - m_endCentralDirectoryRecord.Read(m_stream.Get()); - - // find where the zip central directory exists. - std::uint64_t offsetStartOfCD = 0; - std::uint64_t totalNumberOfEntries = 0; - if (!m_endCentralDirectoryRecord.GetIsZip64()) - { - offsetStartOfCD = m_endCentralDirectoryRecord.GetStartOfCentralDirectory(); - totalNumberOfEntries = m_endCentralDirectoryRecord.GetNumberOfCentralDirectoryEntries(); - } - else - { // Make sure that we have a zip64 end of central directory locator - pos.QuadPart = m_endCentralDirectoryRecord.Size() + m_zip64Locator.Size(); - pos.QuadPart *= -1; - ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::END, nullptr)); - m_zip64Locator.Read(m_stream.Get()); - - // now read the end of zip central directory record - pos.QuadPart = m_zip64Locator.GetRelativeOffset(); - ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::START, nullptr)); - m_zip64EndOfCentralDirectory.Read(m_stream.Get()); - offsetStartOfCD = m_zip64EndOfCentralDirectory.GetOffsetStartOfCD(); - totalNumberOfEntries = m_zip64EndOfCentralDirectory.GetTotalNumberOfEntries(); - } - - // read the zip central directory - pos.QuadPart = offsetStartOfCD; - ThrowHrIfFailed(m_stream->Seek(pos, StreamBase::Reference::START, nullptr)); - for (std::uint32_t index = 0; index < totalNumberOfEntries; index++) - { - auto centralFileHeader = CentralDirectoryFileHeader(); - centralFileHeader.Read(m_stream.Get(), m_endCentralDirectoryRecord.GetIsZip64()); - // TODO: ensure that there are no collisions on name! - m_centralDirectories.insert(std::make_pair(centralFileHeader.GetFileName(), std::move(centralFileHeader))); - } - - if (m_endCentralDirectoryRecord.GetIsZip64()) - { // We should have no data between the end of the last central directory header and the start of the EoCD - ULARGE_INTEGER uPos = {0}; - ThrowHrIfFailed(m_stream->Seek({0}, StreamBase::Reference::CURRENT, &uPos)); - ThrowErrorIfNot(Error::ZipHiddenData, (uPos.QuadPart == m_zip64Locator.GetRelativeOffset()), "hidden data unsupported"); - } - } - - // IStoreageObject - std::vector ZipObjectReader::GetFileNames(FileNameOptions) - { - std::vector result; - std::for_each(m_centralDirectories.begin(), m_centralDirectories.end(), [&result](auto it) - { - result.push_back(it.first); - }); - return result; - } - - // ZipObjectReader::GetFile has cache semantics. If not found on m_streams, get the file from the central directories. - // Not finding a file is non-fatal - ComPtr ZipObjectReader::GetFile(const std::string& fileName) - { - auto result = m_streams.find(fileName); - if (result == m_streams.end()) - { - auto centralFileHeader = m_centralDirectories.find(fileName); - if(centralFileHeader == m_centralDirectories.end()) - { - return ComPtr(); - } - LARGE_INTEGER pos = {0}; - pos.QuadPart = centralFileHeader->second.GetRelativeOffsetOfLocalHeader(); - ThrowHrIfFailed(m_stream->Seek(pos, MSIX::StreamBase::Reference::START, nullptr)); - LocalFileHeader lfh = LocalFileHeader(); - lfh.Read(m_stream.Get(), centralFileHeader->second); - - auto fileStream = ComPtr::Make( - centralFileHeader->first, - centralFileHeader->second.GetCompressionMethod() == CompressionType::Deflate, - centralFileHeader->second.GetRelativeOffsetOfLocalHeader() + lfh.Size(), - centralFileHeader->second.GetCompressedSize(), - m_stream.Get() - ); - - if (centralFileHeader->second.GetCompressionMethod() == CompressionType::Deflate) - { - fileStream = ComPtr::Make(std::move(fileStream), centralFileHeader->second.GetUncompressedSize()); - } - ComPtr result(fileStream); - m_streams.insert(std::make_pair(centralFileHeader->first, std::move(fileStream))); - return result; - } - return result->second; - } - - std::string ZipObjectReader::GetFileName() - { - return m_stream.As()->GetName(); - } -} diff --git a/src/test/msixtest/CMakeLists.txt b/src/test/msixtest/CMakeLists.txt index 8cd0cd5d8..b1c9e0af9 100644 --- a/src/test/msixtest/CMakeLists.txt +++ b/src/test/msixtest/CMakeLists.txt @@ -47,6 +47,7 @@ if(MSIX_PACK) add_definitions(-DMSIX_PACK=1) list(APPEND MsixTestFiles pack.cpp + sign.cpp api_packagewriter.cpp testData/PackTestData.cpp ) diff --git a/src/test/msixtest/PAL/Pack/NonWindows/PackValidation.cpp b/src/test/msixtest/PAL/Pack/NonWindows/PackValidation.cpp index c0aba08e5..e1edce43a 100644 --- a/src/test/msixtest/PAL/Pack/NonWindows/PackValidation.cpp +++ b/src/test/msixtest/PAL/Pack/NonWindows/PackValidation.cpp @@ -11,19 +11,22 @@ namespace MsixTest { namespace Pack { // For non-windows, just use the MSIX SDK to validate the package. - void ValidatePackageStream(const std::string& packageName) + void ValidatePackageStream(const std::string& packageName, bool isSigned) { // verify output package exists auto outputStream = MsixTest::StreamFile(packageName, true, true); // Verify new package can be unpacked auto outputDir = MsixTest::TestPath::GetInstance()->GetPath(MsixTest::TestPath::Directory::Output); + auto validationOption = isSigned ? + MSIX_VALIDATION_OPTION::MSIX_VALIDATION_OPTION_ALLOWSIGNATUREORIGINUNKNOWN + : MSIX_VALIDATION_OPTION::MSIX_VALIDATION_OPTION_SKIPSIGNATURE; REQUIRE_SUCCEEDED(UnpackPackageFromStream(MSIX_PACKUNPACK_OPTION::MSIX_PACKUNPACK_OPTION_NONE, - MSIX_VALIDATION_OPTION::MSIX_VALIDATION_OPTION_SKIPSIGNATURE, + validationOption, outputStream.Get(), const_cast(outputDir.c_str()))); - auto files = MsixTest::Pack::GetExpectedFiles(); + auto files = MsixTest::Pack::GetExpectedFiles(isSigned); CHECK(MsixTest::Directory::CompareDirectory(outputDir, files)); // Clean directory diff --git a/src/test/msixtest/PAL/Pack/Windows/PackValidation.cpp b/src/test/msixtest/PAL/Pack/Windows/PackValidation.cpp index 0e0f5c387..a8a592ac2 100644 --- a/src/test/msixtest/PAL/Pack/Windows/PackValidation.cpp +++ b/src/test/msixtest/PAL/Pack/Windows/PackValidation.cpp @@ -13,19 +13,22 @@ namespace MsixTest { namespace Pack { // For windows, just use the MSIX SDK and Windows AppxPackaging APIs to validate the package. - void ValidatePackageStream(const std::string& packageName) + void ValidatePackageStream(const std::string& packageName, bool isSigned) { // verify output package exists auto packageStream = MsixTest::StreamFile(packageName, true, true); // Verify new package can be unpacked via MSIX SDK auto outputDir = MsixTest::TestPath::GetInstance()->GetPath(MsixTest::TestPath::Directory::Output); + auto validationOption = isSigned ? + MSIX_VALIDATION_OPTION::MSIX_VALIDATION_OPTION_ALLOWSIGNATUREORIGINUNKNOWN + : MSIX_VALIDATION_OPTION::MSIX_VALIDATION_OPTION_SKIPSIGNATURE; REQUIRE_SUCCEEDED(UnpackPackageFromStream(MSIX_PACKUNPACK_OPTION::MSIX_PACKUNPACK_OPTION_NONE, - MSIX_VALIDATION_OPTION::MSIX_VALIDATION_OPTION_SKIPSIGNATURE, + validationOption, packageStream.Get(), const_cast(outputDir.c_str()))); - auto files = MsixTest::Pack::GetExpectedFiles(); + auto files = MsixTest::Pack::GetExpectedFiles(isSigned); CHECK(MsixTest::Directory::CompareDirectory(outputDir, files)); // Clean directory diff --git a/src/test/msixtest/inc/PackTestData.hpp b/src/test/msixtest/inc/PackTestData.hpp index 2cab3c914..71b34c180 100644 --- a/src/test/msixtest/inc/PackTestData.hpp +++ b/src/test/msixtest/inc/PackTestData.hpp @@ -15,7 +15,7 @@ namespace MsixTest { namespace Pack { void MakeManifestStream(IStream** manifestStream); // Get files unpacked after packing testData/pack/input - const std::map& GetExpectedFiles(); + const std::map& GetExpectedFiles(bool isSigned = false); namespace TestConstants { diff --git a/src/test/msixtest/inc/PackValidation.hpp b/src/test/msixtest/inc/PackValidation.hpp index a14583585..4626bb83b 100644 --- a/src/test/msixtest/inc/PackValidation.hpp +++ b/src/test/msixtest/inc/PackValidation.hpp @@ -8,6 +8,6 @@ namespace MsixTest { namespace Pack { - void ValidatePackageStream(const std::string& packageName); + void ValidatePackageStream(const std::string& packageName, bool isSigned = false); } } diff --git a/src/test/msixtest/inc/msixtest_int.hpp b/src/test/msixtest/inc/msixtest_int.hpp index b11e900a3..db778f0dd 100644 --- a/src/test/msixtest/inc/msixtest_int.hpp +++ b/src/test/msixtest/inc/msixtest_int.hpp @@ -28,6 +28,7 @@ namespace MsixTest { Flat, BadFlat, Pack, + Sign, Manifest, } Directory; diff --git a/src/test/msixtest/msixtest.cpp b/src/test/msixtest/msixtest.cpp index 767965ec3..51aa2ae67 100644 --- a/src/test/msixtest/msixtest.cpp +++ b/src/test/msixtest/msixtest.cpp @@ -56,6 +56,8 @@ namespace MsixTest { return m_root + "testData/unpack/badFlat"; case Pack: return m_root + "testData/pack"; + case Sign: + return m_root + "testData/sign"; case Manifest: return m_root + "testData/manifest"; } diff --git a/src/test/msixtest/sign.cpp b/src/test/msixtest/sign.cpp new file mode 100644 index 000000000..43353f9ba --- /dev/null +++ b/src/test/msixtest/sign.cpp @@ -0,0 +1,77 @@ +// +// Copyright (C) 2019 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +// End-to-end sign tests +#include "catch.hpp" +#include "msixtest_int.hpp" +#include "macros.hpp" +#include "FileHelpers.hpp" +#include "PackTestData.hpp" +#include "PackValidation.hpp" + +#include + +static std::string outputPackage = "package-signed.msix"; + +void RunSignTest( + HRESULT expected, + const std::string& directory, + const std::string& pfx, + const LPCSTR pass = nullptr + ) +{ + std::cout << "\tPacking test directory: " << directory << std::endl; + + auto testData = MsixTest::TestPath::GetInstance(); + + auto directoryPath = testData->GetPath(MsixTest::TestPath::Directory::Pack) + "/" + directory; + directoryPath = MsixTest::Directory::PathAsCurrentPlatform(directoryPath); + + auto outputDir = testData->GetPath(MsixTest::TestPath::Directory::Output); + + HRESULT actual = PackPackage(MSIX_PACKUNPACK_OPTION::MSIX_PACKUNPACK_OPTION_NONE, + MSIX_VALIDATION_OPTION::MSIX_VALIDATION_OPTION_SKIPSIGNATURE, + const_cast(directoryPath.c_str()), + const_cast(outputPackage.c_str())); + + CHECK(S_OK == actual); + + auto signPath = testData->GetPath(MsixTest::TestPath::Directory::Sign) + "/" + pfx; + + std::cout << "\tSigning generated package: " << outputPackage << std::endl; + + actual = SignPackage( + MSIX_SIGNING_OPTIONS::MSIX_SIGNING_OPTIONS_NONE, + outputPackage.c_str(), + MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_PFX, + signPath.c_str(), + pass, + nullptr); + + CHECK(expected == actual); + + MsixTest::Log::PrintMsixLog(expected, actual); +} + +TEST_CASE("Sign_Good_NoPass", "[sign]") +{ + HRESULT expected = S_OK; + std::string directory = "input"; + + RunSignTest(expected, directory, "test-self-signed-pfx-no-pass.pfx"); + + // Verify output package + MsixTest::Pack::ValidatePackageStream(outputPackage, true); +} + +TEST_CASE("Sign_Good_SimplePass", "[sign]") +{ + HRESULT expected = S_OK; + std::string directory = "input"; + + RunSignTest(expected, directory, "test-self-signed-pfx-simple-pass.pfx", "testPass"); + + // Verify output package + MsixTest::Pack::ValidatePackageStream(outputPackage, true); +} diff --git a/src/test/msixtest/testData/PackTestData.cpp b/src/test/msixtest/testData/PackTestData.cpp index 77b680eaf..2e79c3501 100644 --- a/src/test/msixtest/testData/PackTestData.cpp +++ b/src/test/msixtest/testData/PackTestData.cpp @@ -78,7 +78,7 @@ namespace MsixTest { namespace Pack { *manifestStream = stream.Detach(); } - const std::map& GetExpectedFiles() + const std::map& GetExpectedFiles(bool isSigned) { static const std::map files = { @@ -95,6 +95,14 @@ namespace MsixTest { namespace Pack { { "Assets/StoreLogo.png", 1451 }, { "Assets/Wide310x150Logo.scale-200.png", 3204 } }; + if (isSigned) { + static const std::map files_signed = [&](){ + std::map temp = files; + temp["AppxSignature.p7x"] = 2671; + return temp; + }(); + return files_signed; + } return files; } } } diff --git a/src/test/testData/sign/README.md b/src/test/testData/sign/README.md new file mode 100644 index 000000000..94b409e1a --- /dev/null +++ b/src/test/testData/sign/README.md @@ -0,0 +1,14 @@ +The pfx files here are arbitary ones generated just for these tests; should you +get an alert related to them, feel free to ignore it. + +These were generated as follows: + +test-self-signed-pfx-no-pass.pfx: +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 36500 -noenc -subj "/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Corporation" --addext "basicConstraints=critical,CA:FALSE" --addext "keyUsage=digitalSignature" --addext "extendedKeyUsage=codeSigning" +openssl pkcs12 -export -out test-self-signed-pfx-no-pass.pfx -inkey key.pem -in cert.pem -passout "pass:" + +test-self-signed-pfx-simple-pass.pfx: +openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 36500 -noenc -subj "/C=US/ST=Washington/L=Redmond/O=Microsoft Corporation/CN=Microsoft Corporation" --addext "basicConstraints=critical,CA:FALSE" --addext "keyUsage=digitalSignature" --addext "extendedKeyUsage=codeSigning" +openssl pkcs12 -export -out test-self-signed-pfx-simple-pass.pfx -inkey key.pem -in cert.pem -passout "pass:testPass" + + diff --git a/src/test/testData/sign/test-self-signed-pfx-no-pass.pfx b/src/test/testData/sign/test-self-signed-pfx-no-pass.pfx new file mode 100644 index 000000000..331687cbe Binary files /dev/null and b/src/test/testData/sign/test-self-signed-pfx-no-pass.pfx differ diff --git a/src/test/testData/sign/test-self-signed-pfx-simple-pass.pfx b/src/test/testData/sign/test-self-signed-pfx-simple-pass.pfx new file mode 100644 index 000000000..563921518 Binary files /dev/null and b/src/test/testData/sign/test-self-signed-pfx-simple-pass.pfx differ