From f0b7ab122c32f8ad40e08adf72933a2b4c44403c Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 8 Jul 2019 15:32:30 -0700 Subject: [PATCH 01/22] Everything is in place, but some changes are needed in packing to make things happy. Also need lots of polish. --- cmake/crypto.cmake | 20 +- cmake/crypto_sources.cmake | 24 + src/inc/internal/AppxPackageObject.hpp | 4 + src/inc/internal/AppxPackageWriter.hpp | 17 +- src/inc/internal/AppxSignature.hpp | 9 +- src/inc/internal/ContentTypeWriter.hpp | 16 +- src/inc/internal/Crypto.hpp | 68 ++- src/inc/internal/FileStream.hpp | 57 +- src/inc/internal/SHA256HashStream.hpp | 36 ++ src/inc/internal/SignatureCreator.hpp | 22 + src/inc/internal/Signing.hpp | 144 +++++ src/inc/internal/StreamHelper.hpp | 87 ++-- src/inc/internal/XmlWriter.hpp | 7 +- src/inc/internal/ZipObject.hpp | 493 ++---------------- src/inc/internal/ZipObjectComponents.hpp | 449 ++++++++++++++++ src/inc/internal/ZipObjectReader.hpp | 30 -- src/inc/internal/ZipObjectWriter.hpp | 30 +- src/inc/public/AppxPackaging.hpp | 49 +- src/inc/shared/ComHelper.hpp | 10 +- src/inc/shared/Exceptions.hpp | 28 +- src/inc/shared/StreamBase.hpp | 4 +- src/makemsix/main.cpp | 79 +++ src/msix/CMakeLists.txt | 19 +- .../PAL/Crypto/OpenSSL/OpenSSLWriting.cpp | 165 ++++++ .../PAL/Crypto/OpenSSL/OpenSSLWriting.hpp | 146 ++++++ src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp | 146 ++++++ .../PAL/Crypto/OpenSSL/SignatureCreator.cpp | 290 +++++++++++ .../OpenSSL/SignatureValidator.cpp | 14 +- src/msix/PAL/Crypto/Win32/Crypto.cpp | 9 +- .../PAL/Crypto/Win32/SignatureCreator.cpp | 17 + .../Win32/SignatureValidator.cpp | 0 src/msix/common/AppxFactory.cpp | 12 +- src/msix/common/StreamHelper.cpp | 127 +++++ src/msix/common/ZipObject.cpp | 181 ++++++- src/msix/msix.cpp | 71 ++- src/msix/pack/AppxPackageWriter.cpp | 116 ++++- src/msix/pack/ContentType.cpp | 14 +- src/msix/pack/ContentTypeWriter.cpp | 37 +- src/msix/pack/Signing.cpp | 268 ++++++++++ src/msix/pack/XmlWriter.cpp | 223 ++++---- src/msix/pack/ZipObjectWriter.cpp | 188 ++++--- src/msix/unpack/ZipObjectReader.cpp | 117 ----- 42 files changed, 2899 insertions(+), 944 deletions(-) create mode 100644 src/inc/internal/SHA256HashStream.hpp create mode 100644 src/inc/internal/SignatureCreator.hpp create mode 100644 src/inc/internal/Signing.hpp create mode 100644 src/inc/internal/ZipObjectComponents.hpp delete mode 100644 src/inc/internal/ZipObjectReader.hpp create mode 100644 src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp create mode 100644 src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp create mode 100644 src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp create mode 100644 src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp rename src/msix/PAL/{Signature => Crypto}/OpenSSL/SignatureValidator.cpp (98%) create mode 100644 src/msix/PAL/Crypto/Win32/SignatureCreator.cpp rename src/msix/PAL/{Signature => Crypto}/Win32/SignatureValidator.cpp (100%) create mode 100644 src/msix/common/StreamHelper.cpp create mode 100644 src/msix/pack/Signing.cpp delete mode 100644 src/msix/unpack/ZipObjectReader.cpp 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..3e0ec919b 100644 --- a/cmake/crypto_sources.cmake +++ b/cmake/crypto_sources.cmake @@ -481,6 +481,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/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..5067c43b3 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,11 @@ 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, + IStream* privateKey) = 0; }; MSIX_INTERFACE(IPackageWriter, 0x32e89da5,0x7cbb,0x4443,0x8c,0xf0,0xb8,0x4e,0xed,0xb5,0x1d,0x0a); @@ -37,10 +44,15 @@ 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, + IStream* privateKey) override; // IAppxPackageWriter HRESULT STDMETHODCALLTYPE AddPayloadFile(LPCWSTR fileName, LPCWSTR contentType, @@ -72,15 +84,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..528685aa5 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,17 @@ 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); + + // 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, + // AddOverride is called. 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/Crypto.hpp b/src/inc/internal/Crypto.hpp index 20ab28da3..761bb2a96 100644 --- a/src/inc/internal/Crypto.hpp +++ b/src/inc/internal/Crypto.hpp @@ -3,7 +3,10 @@ // See LICENSE file in the project root for full license information. // #pragma once +#include "Exceptions.hpp" +#include +#include #include #ifndef SHA256_DIGEST_LENGTH @@ -12,39 +15,50 @@ namespace MSIX { + // Forward declaration of type defined within PAL + struct SHA256Context; + + // Class used to compute SHA256 hashes over various sets of data. + // Create one and Add data to it if the data is not all available, + // or simply call ComputeHash if the data is all in memory. class SHA256 { public: - static bool ComputeHash(const std::uint8_t *buffer, std::uint32_t cbBuffer, std::vector& hash); + using HashBuffer = std::vector; - /// - /// Construct and initialize the hash engine so it can be used to compute hash of input data. - /// SHA256(); - ~SHA256(); - - /// - /// Reset the internal state of the hash engine so it can be used again to hash data. - /// - void Reset(); - - /// - /// Hash data. Can be called repeatedly to hash a stream of data. Call FinalizeAndGetHashValue to finalize the hash engine - /// and get the hash value of all the input data. - /// - /// Buffer containing data - /// Size of the data in bytes - void HashData(const std::uint8_t* buffer, std::uint32_t cbBuffer); - - /// - /// Finalize the hash engine and get the computed hash value of all the input data from HashData calls. - /// After this call, the hash engine cannot be used again until Reset is called to reset its state. - /// - /// Output bufer to receive the computed hash value. - void FinalizeAndGetHashValue(std::vector& hash); + + // Adds the next chunk of data to the hash. + void Add(const uint8_t* buffer, size_t cbBuffer); + + inline void Add(const std::vector& buffer) + { + Add(buffer.data(), buffer.size()); + } + + // Gets the hash of the data. This is a destructive action; the accumulated hash + // value will be returned and the object can no longer be used. + void Get(HashBuffer& hash); + + inline HashBuffer Get() + { + HashBuffer result{}; + Get(result); + return result; + } + + // Computes the hash of the given buffer immediately. + static bool ComputeHash(uint8_t *buffer, std::uint32_t cbBuffer, HashBuffer& hash); private: - void* m_hashContext = nullptr; + void EnsureNotFinished() const { ThrowErrorIfNot(Error::InvalidState, context, "The hash is already finished"); } + + struct SHA256ContextDeleter + { + void operator()(SHA256Context* context); + }; + + std::unique_ptr context; }; class Base64 @@ -52,4 +66,4 @@ namespace MSIX { public: static std::string ComputeBase64(const std::vector& buffer); }; -} \ 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/SHA256HashStream.hpp b/src/inc/internal/SHA256HashStream.hpp new file mode 100644 index 000000000..f11f121bc --- /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.Add(reinterpret_cast(buffer), countBytes); + if (bytesWritten) + { + *bytesWritten = countBytes; + } + return static_cast(Error::OK); + } CATCH_RETURN(); + + SHA256::HashBuffer GetHash() + { + return m_hasher.Get(); + } + + private: + SHA256 m_hasher; + }; +} diff --git a/src/inc/internal/SignatureCreator.hpp b/src/inc/internal/SignatureCreator.hpp new file mode 100644 index 000000000..83ebddc0c --- /dev/null +++ b/src/inc/internal/SignatureCreator.hpp @@ -0,0 +1,22 @@ +// +// 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, + IStream* privateKey); + }; +} diff --git a/src/inc/internal/Signing.hpp b/src/inc/internal/Signing.hpp new file mode 100644 index 000000000..f477f2d2e --- /dev/null +++ b/src/inc/internal/Signing.hpp @@ -0,0 +1,144 @@ +// +// 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, + 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&) = default; + SignatureAccumulator& operator=(const SignatureAccumulator&) = default; + + 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/XmlWriter.hpp b/src/inc/internal/XmlWriter.hpp index 42a5cc393..4e6bca94e 100644 --- a/src/inc/internal/XmlWriter.hpp +++ b/src/inc/internal/XmlWriter.hpp @@ -29,7 +29,10 @@ 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) { @@ -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..6882d9cc4 --- /dev/null +++ b/src/inc/internal/ZipObjectComponents.hpp @@ -0,0 +1,449 @@ +// +// 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 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; } + + 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..0f8b87220 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,16 @@ 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 privateKey ) noexcept; MSIX_API HRESULT STDMETHODCALLTYPE PackBundle( @@ -1852,7 +1877,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 +1888,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/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..8faaa86ea 100644 --- a/src/inc/shared/Exceptions.hpp +++ b/src/inc/shared/Exceptions.hpp @@ -64,6 +64,15 @@ namespace MSIX { } }; + class POSIXException final : public Exception + { + public: + POSIXException(std::string& message, int error) : Exception(message, 0xA0070000 + error) + { + Global::Log::Append(Message()); + } + }; + // Provides an ABI exception boundary with parameter validation #define CATCH_RETURN() \ catch (MSIX::Exception& e) \ @@ -140,11 +149,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..ee80cb783 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,50 @@ 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" }, + // 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." + }); + + 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()), + nullptr); + }); + + return result; +} + Command CreateBundleCommand() { Command result{ "bundle", "Create a new app bundle from files on disk", @@ -666,6 +744,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..98259c096 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,25 @@ 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/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..d93bfdbfc --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp @@ -0,0 +1,165 @@ +// +// 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_create(obj.oid, obj.shortName, obj.longName); + if (nid == NID_undef) + { + ThrowOpenSSLError(); + } + 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 \ No newline at end of file diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp new file mode 100644 index 000000000..bb4d8e9e4 --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp @@ -0,0 +1,146 @@ +// +// Copyright (C) 2017 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once + +#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(std::uint32_t val) : m_val(val) {} + void AppendTo(Container::BytesType& bytes) const; + + private: + uint64_t m_val; + }; + + struct OctetString : public Item + { + OctetString(Container::BytesType& bytes) : m_bytes(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::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.hpp b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp new file mode 100644 index 000000000..d13b3f1d6 --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp @@ -0,0 +1,146 @@ +// +// 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 + +#define ThrowOpenSSLError() PrintOpenSSLErr() +// TODO: More like this +//#define ThrowOpenSSLErrIfFailed(_x_) MSIX::RaiseOpenSSLExceptionIfFailed(a, __LINE__, __FILE__); +#define ThrowOpenSSLErrIfFailed(_x_) CheckOpenSSLErr(_x_) +#define ThrowOpenSSLErrIfAllocFailed(_x_) CheckOpenSSLAlloc(_x_) + +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 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; + + 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; + + // TODO: Make this output to the text log and throw an exception + inline void PrintOpenSSLErr() + { + ERR_load_crypto_strings(); + + std::cout << "OpenSSL Error:" << 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) + { + std::cout << " at " << file << '[' << line << ']'; + if (flags & ERR_TXT_STRING) + { + std::cout << " : " << data; + } + std::cout << std::endl; + + std::cout << " " << ERR_error_string(err, nullptr) << std::endl; + } + } while (err); + } + + template + inline void CheckOpenSSLAlloc(const T& t) + { + if (!t) + { + PrintOpenSSLErr(); + } + } + + inline void CheckOpenSSLErr(int err) + { + if (err <= 0) + { + PrintOpenSSLErr(); + } + } +} // namespace MSIX diff --git a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp new file mode 100644 index 000000000..b1b877414 --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp @@ -0,0 +1,290 @@ +// +// 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 "FileStream.hpp" +#include "SignatureCreator.hpp" +#include "MSIXResource.hpp" +#include "StreamHelper.hpp" + +#include "SharedOpenSSL.hpp" +#include "OpenSSLWriting.hpp" + +#include +#include + +namespace MSIX +{ + namespace + { + struct SigningInfo + { + SigningInfo( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + 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(), nullptr); + } + break; + + default: + NOTSUPPORTED + } + } + + void ParsePKCS12(PKCS12* p12, const char* pass) + { + EVP_PKEY* pkey_ = nullptr; + X509* cert_ = nullptr; + STACK_OF(X509)* ca_ = chain.get(); + + ThrowOpenSSLErrIfFailed(PKCS12_parse(p12, pass, &pkey_, &cert_, &ca_)); + + // 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; + }; + + int PKCS7_indirect_data_content_new(PKCS7* p7, const CustomOpenSSLObjects& customObjects) + { + PKCS7* ret = NULL; + + if ((ret = PKCS7_new()) == NULL) + goto err; + + ret->type = customObjects.Get(CustomOpenSSLObjectName::spcIndirectDataContext).GetObj(); + + ret->d.other = ASN1_TYPE_new(); + if (!ret->d.other) + goto err; + + if (!ASN1_TYPE_set_octetstring(ret->d.other, nullptr, 0)) + goto err; + + if (!PKCS7_set_content(p7, ret)) + goto err; + + return (1); + err: + if (ret != NULL) + PKCS7_free(ret); + return (0); + } + + PKCS7* PKCS7_sign_indirect_data(X509* signcert, EVP_PKEY* pkey, STACK_OF(X509)* certs, + BIO* data, int flags, const CustomOpenSSLObjects& customObjects) + { + PKCS7* p7; + int i; + + if (!(p7 = PKCS7_new())) { + PKCS7err(PKCS7_F_PKCS7_SIGN, ERR_R_MALLOC_FAILURE); + return NULL; + } + + if (!PKCS7_set_type(p7, NID_pkcs7_signed)) + goto err; + + // Standard PKCS7_sign only supports NID_pkcs7_data, but we want SPC_INDIRECT_DATA_OBJID + if (!PKCS7_indirect_data_content_new(p7, customObjects)) + goto err; + + // Force SHA256 for now + PKCS7_SIGNER_INFO* signerInfo = PKCS7_sign_add_signer(p7, signcert, pkey, EVP_sha256(), flags); + if (!signerInfo) { + PKCS7err(PKCS7_F_PKCS7_SIGN, PKCS7_R_PKCS7_ADD_SIGNER_ERROR); + goto err; + } + + // Add our authenticated attributes + PKCS7_add_attrib_content_type(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcIndirectDataContext).GetObj()); + + // TODO: Make a cleaner way to generate this sequence for the statement type. + const uint8_t statementType[]{ 0x30, 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x15 }; + + // TODO: smart pointer + ASN1_STRING* statementString = ASN1_STRING_type_new(V_ASN1_SEQUENCE); + ASN1_STRING_set(statementString, const_cast(statementType), sizeof(statementType)); + + PKCS7_add_signed_attribute(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcStatementType).GetNID(), V_ASN1_SEQUENCE, statementString); + + // TODO: Make a cleaner way to generate this too + const uint8_t opusType[]{ 0x30, 0x00 }; + + // TODO: smart pointer + ASN1_STRING* opusString = ASN1_STRING_type_new(V_ASN1_SEQUENCE); + ASN1_STRING_set(opusString, const_cast(opusType), sizeof(opusType)); + + PKCS7_add_signed_attribute(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcSpOpusInfo).GetNID(), V_ASN1_SEQUENCE, opusString); + + // Always include the chain certs + for (i = 0; i < sk_X509_num(certs); i++) { + if (!PKCS7_add_certificate(p7, sk_X509_value(certs, i))) + goto err; + } + + if (!PKCS7_final(p7, data, flags)) + { + goto err; + } + + return p7; + + err: + PKCS7_free(p7); + return NULL; + } + + 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()); + } + + 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; + } + + 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; + } + } + + // [X] 1. Get self signed PKCS7_sign working + // [X] 2. Try to hack the OID of the contents to not be 'data' + // [ ] 3. If that makes a sufficiently nice looking output, create indirect data blob in ASN + // [ ] 4. Else? + ComPtr SignatureCreator::Sign( + AppxSignatureObject* digests, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + IStream* privateKey) + { + OpenSSL_add_all_algorithms(); + CustomOpenSSLObjects customObjects{}; + + // Read in the signing info based on format, etc. + SigningInfo signingInfo{ signingCertificateFormat, signingCertificate, 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())) }; + + // 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) }; + ThrowOpenSSLErrIfAllocFailed(p7); + + // 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) }; + + // TODO: Smart poitners + ASN1_STRING* sequenceString = ASN1_STRING_type_new(V_ASN1_SEQUENCE); + ASN1_STRING_set(sequenceString, reinterpret_cast(completeBlob.data()), static_cast(completeBlob.size())); + + ASN1_TYPE_set(p7->d.sign->contents->d.other, V_ASN1_SEQUENCE, sequenceString); + + unique_BIO outBIO{ BIO_new(BIO_s_mem()) }; + i2d_PKCS7_bio(outBIO.get(), p7.get()); + + MSIX::ComPtr outStream; + ThrowHrIfFailed(CreateStreamOnFile(R"(C:\Temp\evernotesup\openssltest.p7s)", false, &outStream)); + + char* out = nullptr; + long cOut = BIO_get_mem_data(outBIO.get(), &out); + + outStream->Write(out, cOut, nullptr); + + { + MSIX::ComPtr p7xOutStream; + ThrowHrIfFailed(CreateStreamOnFile(R"(C:\Temp\evernotesup\openssltest.p7x)", false, &p7xOutStream)); + + uint32_t prefix = P7X_FILE_ID; + p7xOutStream->Write(&prefix, sizeof(prefix), nullptr); + p7xOutStream->Write(out, cOut, nullptr); + } + + // For funsies + MSIX::ComPtr result; + ThrowHrIfFailed(CreateStreamOnFile(R"(C:\Temp\evernotesup\openssltest.p7x)", true, &result)); + + return result; + } +} // 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..d14ddda41 --- /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 Sign( + AppxSignatureObject* digests, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + IStream* privateKey) + { + NOTIMPLEMENTED + } +} // 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..e9f5b593a 100644 --- a/src/msix/common/AppxFactory.cpp +++ b/src/msix/common/AppxFactory.cpp @@ -5,7 +5,6 @@ #include "AppxFactory.hpp" #include "UnicodeConversion.hpp" #include "Exceptions.hpp" -#include "ZipObjectReader.hpp" #include "AppxPackageObject.hpp" #include "MSIXResource.hpp" #include "VectorStream.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); @@ -185,7 +181,7 @@ 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()); + 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/StreamHelper.cpp b/src/msix/common/StreamHelper.cpp new file mode 100644 index 000000000..a01a91dd8 --- /dev/null +++ b/src/msix/common/StreamHelper.cpp @@ -0,0 +1,127 @@ +// +// Copyright (C) 2017 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once + +#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..44b8ea0ab 100644 --- a/src/msix/common/ZipObject.cpp +++ b/src/msix/common/ZipObject.cpp @@ -9,6 +9,9 @@ #include "ZipObject.hpp" #include "VectorStream.hpp" #include "MsixFeatureSelector.hpp" +#include "ZipFileStream.hpp" +#include "InflateStream.hpp" +#include "RangeStream.hpp" #include "TimeHelpers.hpp" #include @@ -481,15 +484,177 @@ 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) { - 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); + 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() +{ + 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); + + return ComPtr::Make(targetCD.GetRelativeOffsetOfLocalHeader(), lfh.Size() + targetCD.GetCompressedSize(), m_stream.Get()); } } // namespace MSIX diff --git a/src/msix/msix.cpp b/src/msix/msix.cpp index f5de1b728..1ef656d8e 100644 --- a/src/msix/msix.cpp +++ b/src/msix/msix.cpp @@ -20,6 +20,7 @@ #include "AppxPackageWriter.hpp" #include "AppxBundleWriter.hpp" #include "ScopeExit.hpp" +#include "Signing.hpp" #include "VersionHelpers.hpp" #include "MappingFileParser.hpp" #include "FileStream.hpp" @@ -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,54 @@ 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 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(), privateKeyStream.Get()); + + return static_cast(MSIX::Error::OK); +} CATCH_RETURN(); + MSIX_API HRESULT STDMETHODCALLTYPE PackBundle( MSIX_BUNDLE_OPTIONS bundleOptions, char* directoryPath, diff --git a/src/msix/pack/AppxPackageWriter.cpp b/src/msix/pack/AppxPackageWriter.cpp index fc47d4cb1..5bd3ac01c 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,64 @@ namespace MSIX { failState.release(); } + void AppxPackageWriter::Close( + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + 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, 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 +157,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); + return static_cast(Error::OK); } CATCH_RETURN(); @@ -107,8 +174,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 +192,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 +211,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 +228,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 +263,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 +289,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 +318,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..fb6af8ea2 100644 --- a/src/msix/pack/ContentType.cpp +++ b/src/msix/pack/ContentType.cpp @@ -120,18 +120,18 @@ 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"; } + // 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..450a74a60 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); } - // 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. + 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..a4242475c --- /dev/null +++ b/src/msix/pack/Signing.cpp @@ -0,0 +1,268 @@ +// +// 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 + +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; + } + + UNEXPECTED +} + +// Signs a package in-place with the given certificate. +void SignPackage( + IAppxPackageReader* package, + MSIX_CERTIFICATE_FORMAT signingCertificateFormat, + IStream* signingCertificate, + IStream* privateKey) +{ + // TODO: Dissimenate or delete comment + // Steps: + // Create digest accumulator (AppxSipCreateIndirectData) + // Hashes are for [in order]: + // Package Content [hash of everything but the central directory, except signature file] + // Central Directory [Containing everything but the signature file] + // Content Types [final file stream, must include signature and catalog types] + // Block Map [full stream] + // CI Catalog [full stream] [optional] + // Extract Content Types for editing + // Remove content types, any existing catalog and signature + // Spin through all files, passing them along to accumulator + // Create catalog if needed (also adds its own digest) (CreateSignedPEFilesCatalog) + // [MERGE WITH CONCURRENT PACKAGE WRITE PATH HERE] + // Collect last few digests (non-file based ones) + // Create signature stream + // Append content types, catalog, and signature + // Write zip central directory back + + 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()); + } + } + + // Extract content types for editing, then create a new writer that will be ready to append. + auto contentTypesStream = underlyingStorage->GetFile(CONTENT_TYPES_XML); + ContentTypeWriter contentTypeWriter{ contentTypesStream.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), std::move(contentTypeWriter)); + + packageWriter->Close(signingCertificateFormat, signingCertificate, 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 + result->GetFileRecordsDigest() = GetZipHasher().Get(); + + // Create the central directory as it currently exists and hash it + ComPtr cdHash = ComPtr::Make(); + zipWriter->WriteCentralDirectoryToStream(cdHash.Get()); + result->GetCentralDirectoryDigest() = cdHash->GetHash(); + + 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) + { + signatureAccumulator.GetSignatureObject()->GetAppxBlockMapDigest() = GetRawHasher().Get(); + } + else if (isContentTypes) + { + signatureAccumulator.GetSignatureObject()->GetContentTypesDigest() = GetRawHasher().Get(); + } + 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.Add(bytes); + } + } + 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().Add(data); + } + 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::AccumulateZip(IStream* stream) +{ + if (wantsZip) + { + auto& hasher = GetZipHasher(); + + for (const auto& bytes : Helper::StreamProcessor{ stream, 1 << 20 }) + { + hasher.Add(bytes); + } + } + + return wantsZip; +} + +} diff --git a/src/msix/pack/XmlWriter.cpp b/src/msix/pack/XmlWriter.cpp index cbaa3635b..5db87e630 100644 --- a/src/msix/pack/XmlWriter.cpp +++ b/src/msix/pack/XmlWriter.cpp @@ -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 (int 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/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(); - } -} From 9e0b1be26018c8dc419e6fdf587d72a214727c33 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Mon, 5 Aug 2019 17:09:49 -0700 Subject: [PATCH 02/22] Signing successfully creates a package verified by AppxSip --- src/inc/internal/RangeStream.hpp | 2 +- src/inc/internal/ZipObjectComponents.hpp | 35 +++++++++++++++---- .../PAL/Crypto/OpenSSL/SignatureCreator.cpp | 6 ++-- src/msix/common/ZipObject.cpp | 10 +++--- src/msix/pack/Signing.cpp | 32 +++++++++++++++++ 5 files changed, 71 insertions(+), 14 deletions(-) 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/ZipObjectComponents.hpp b/src/inc/internal/ZipObjectComponents.hpp index 6882d9cc4..ecc51f4db 100644 --- a/src/inc/internal/ZipObjectComponents.hpp +++ b/src/inc/internal/ZipObjectComponents.hpp @@ -166,7 +166,7 @@ namespace MSIX { GeneralPurposeBitFlags GetGeneralPurposeBitFlags() const noexcept { return static_cast(Field<3>().get()); } - bool IsGeneralPurposeBitSet() const noexcept + bool IsDataDescriptorBitSet() const noexcept { return ((GetGeneralPurposeBitFlags() & GeneralPurposeBitFlags::DataDescriptor) == GeneralPurposeBitFlags::DataDescriptor); } @@ -309,6 +309,11 @@ namespace MSIX { 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 @@ -318,11 +323,6 @@ namespace MSIX { } 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; } @@ -342,6 +342,29 @@ namespace MSIX { } }; + // 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 diff --git a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp index b1b877414..f246c5437 100644 --- a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp @@ -265,7 +265,7 @@ namespace MSIX i2d_PKCS7_bio(outBIO.get(), p7.get()); MSIX::ComPtr outStream; - ThrowHrIfFailed(CreateStreamOnFile(R"(C:\Temp\evernotesup\openssltest.p7s)", false, &outStream)); + ThrowHrIfFailed(CreateStreamOnFile(R"(D:\Temp\evernotesup\openssltest.p7s)", false, &outStream)); char* out = nullptr; long cOut = BIO_get_mem_data(outBIO.get(), &out); @@ -274,7 +274,7 @@ namespace MSIX { MSIX::ComPtr p7xOutStream; - ThrowHrIfFailed(CreateStreamOnFile(R"(C:\Temp\evernotesup\openssltest.p7x)", false, &p7xOutStream)); + ThrowHrIfFailed(CreateStreamOnFile(R"(D:\Temp\evernotesup\openssltest.p7x)", false, &p7xOutStream)); uint32_t prefix = P7X_FILE_ID; p7xOutStream->Write(&prefix, sizeof(prefix), nullptr); @@ -283,7 +283,7 @@ namespace MSIX // For funsies MSIX::ComPtr result; - ThrowHrIfFailed(CreateStreamOnFile(R"(C:\Temp\evernotesup\openssltest.p7x)", true, &result)); + ThrowHrIfFailed(CreateStreamOnFile(R"(D:\Temp\evernotesup\openssltest.p7x)", true, &result)); return result; } diff --git a/src/msix/common/ZipObject.cpp b/src/msix/common/ZipObject.cpp index 44b8ea0ab..a9f541f20 100644 --- a/src/msix/common/ZipObject.cpp +++ b/src/msix/common/ZipObject.cpp @@ -291,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), @@ -300,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>()); @@ -654,7 +654,9 @@ MSIX::ComPtr ZipObject::GetEntireZipFileStream(const std::string& fileN LocalFileHeader lfh = LocalFileHeader(); lfh.Read(m_stream.Get(), targetCD); - return ComPtr::Make(targetCD.GetRelativeOffsetOfLocalHeader(), lfh.Size() + targetCD.GetCompressedSize(), m_stream.Get()); + 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/pack/Signing.cpp b/src/msix/pack/Signing.cpp index a4242475c..81314e020 100644 --- a/src/msix/pack/Signing.cpp +++ b/src/msix/pack/Signing.cpp @@ -15,6 +15,12 @@ #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 { @@ -250,15 +256,41 @@ bool SignatureAccumulator::FileAccumulator::AccumulateRaw(const std::vector 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.Add(bytes); + +#if MSIX_DEBUG_PACKAGE_CONTENT_HASH + ThrowHrIfFailed(contentsOutput->Write(static_cast(bytes.data()), static_cast(bytes.size()), nullptr)); +#endif } } From 2794d361afd4bed468c38fb16932c7cb3da8e7d8 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 7 Aug 2019 12:31:08 -0700 Subject: [PATCH 03/22] Clean up SignatureCreator to production quality --- CMakeLists.txt | 7 + .../{VectorStream.hpp => MemoryStream.hpp} | 12 +- src/inc/internal/StringStream.hpp | 70 -------- src/inc/internal/XmlWriter.hpp | 4 +- src/inc/shared/Exceptions.hpp | 29 ++-- src/msix/CMakeLists.txt | 1 + .../PAL/Crypto/OpenSSL/OpenSSLWriting.cpp | 2 +- src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp | 70 ++++++++ src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp | 79 +++++---- .../PAL/Crypto/OpenSSL/SignatureCreator.cpp | 160 ++++++++---------- src/msix/common/AppxFactory.cpp | 4 +- src/msix/common/Exceptions.cpp | 10 +- src/msix/common/ZipObject.cpp | 4 +- src/msix/msix.cpp | 6 +- src/msix/pack/Signing.cpp | 29 +--- src/msix/pack/XmlWriter.cpp | 4 +- src/msix/unpack/AppxSignature.cpp | 10 +- 17 files changed, 231 insertions(+), 270 deletions(-) rename src/inc/internal/{VectorStream.hpp => MemoryStream.hpp} (85%) delete mode 100644 src/inc/internal/StringStream.hpp create mode 100644 src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp 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/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/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 4e6bca94e..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 @@ -36,7 +36,7 @@ namespace MSIX { XmlWriter(const std::string& root, bool standalone = false) { - m_stream = ComPtr::Make(); + m_stream = ComPtr::Make(); StartWrite(root, standalone); } diff --git a/src/inc/shared/Exceptions.hpp b/src/inc/shared/Exceptions.hpp index 8faaa86ea..bcf595cd1 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,18 +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) { - Global::Log::Append(Message()); } }; class POSIXException final : public Exception { public: - POSIXException(std::string& message, int error) : Exception(message, 0xA0070000 + error) + POSIXException(std::string message, int error) : Exception(std::move(message), 0xA0070000 + error) { - Global::Log::Append(Message()); } }; @@ -121,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. diff --git a/src/msix/CMakeLists.txt b/src/msix/CMakeLists.txt index 98259c096..4039c5ffd 100644 --- a/src/msix/CMakeLists.txt +++ b/src/msix/CMakeLists.txt @@ -214,6 +214,7 @@ if(CRYPTO_LIB MATCHES crypt32) elseif(CRYPTO_LIB MATCHES openssl) if(OpenSSL_FOUND) list(APPEND MsixSrc + PAL/Crypto/OpenSSL/SharedOpenSSL.cpp PAL/Crypto/OpenSSL/Crypto.cpp PAL/Crypto/OpenSSL/SignatureValidator.cpp ) diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp index d93bfdbfc..263457358 100644 --- a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp @@ -40,7 +40,7 @@ namespace MSIX int nid = OBJ_create(obj.oid, obj.shortName, obj.longName); if (nid == NID_undef) { - ThrowOpenSSLError(); + ThrowOpenSSLError("Failed to create custom OpenSSL object"); } objects.emplace_back(obj.name, nid); } diff --git a/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp new file mode 100644 index 000000000..2b2179666 --- /dev/null +++ b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp @@ -0,0 +1,70 @@ +// +// Copyright (C) 2017 Microsoft. All rights reserved. +// See LICENSE file in the project root for full license information. +// +#pragma once +#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 index d13b3f1d6..7ae7a52f0 100644 --- a/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp +++ b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.hpp @@ -6,7 +6,6 @@ #include "Exceptions.hpp" #include -#include #include #include @@ -18,12 +17,7 @@ #include #include #include - -#define ThrowOpenSSLError() PrintOpenSSLErr() -// TODO: More like this -//#define ThrowOpenSSLErrIfFailed(_x_) MSIX::RaiseOpenSSLExceptionIfFailed(a, __LINE__, __FILE__); -#define ThrowOpenSSLErrIfFailed(_x_) CheckOpenSSLErr(_x_) -#define ThrowOpenSSLErrIfAllocFailed(_x_) CheckOpenSSLAlloc(_x_) +#include namespace MSIX { @@ -63,6 +57,10 @@ namespace MSIX 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); }; }; @@ -76,6 +74,7 @@ namespace MSIX 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 { @@ -95,52 +94,50 @@ namespace MSIX 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); - // TODO: Make this output to the text log and throw an exception - inline void PrintOpenSSLErr() + // 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) { - ERR_load_crypto_strings(); - - std::cout << "OpenSSL Error:" << std::endl; - - unsigned long err = 0; - do + if (!t) { - const char* file{}; - int line{}; - const char* data{}; - int flags{}; - - err = ERR_get_error_line_data(&file, &line, &data, &flags); - - if (err) - { - std::cout << " at " << file << '[' << line << ']'; - if (flags & ERR_TXT_STRING) - { - std::cout << " : " << data; - } - std::cout << std::endl; - - std::cout << " " << ERR_error_string(err, nullptr) << std::endl; - } - } while (err); + RaiseOpenSSLException("OpenSSL allocation failed", line, file, static_cast(Error::OutOfMemory)); + } } - template - inline void CheckOpenSSLAlloc(const T& t) + // 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 (!t) + if (err <= 0) { - PrintOpenSSLErr(); + RaiseOpenSSLException(message, line, file); } } - inline void CheckOpenSSLErr(int err) + template + inline void CheckOpenSSLErr(T* returnVal, const int line, const char* const file, const char* message = nullptr) { - if (err <= 0) + if (!returnVal) { - PrintOpenSSLErr(); + 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 index f246c5437..27542f218 100644 --- a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp @@ -4,10 +4,10 @@ // #include "AppxSignature.hpp" #include "Exceptions.hpp" -#include "FileStream.hpp" #include "SignatureCreator.hpp" #include "MSIXResource.hpp" #include "StreamHelper.hpp" +#include "MemoryStream.hpp" #include "SharedOpenSSL.hpp" #include "OpenSSLWriting.hpp" @@ -35,7 +35,7 @@ namespace MSIX 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(), nullptr); + ParsePKCS12(cert.get(), nullptr, "Unable to open PFX file"); } break; @@ -44,13 +44,13 @@ namespace MSIX } } - void ParsePKCS12(PKCS12* p12, const char* pass) + void ParsePKCS12(PKCS12* p12, const char* pass, const char* failureMessage = nullptr) { EVP_PKEY* pkey_ = nullptr; X509* cert_ = nullptr; STACK_OF(X509)* ca_ = chain.get(); - ThrowOpenSSLErrIfFailed(PKCS12_parse(p12, pass, &pkey_, &cert_, &ca_)); + 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. @@ -67,94 +67,80 @@ namespace MSIX unique_EVP_PKEY privateKey; }; - int PKCS7_indirect_data_content_new(PKCS7* p7, const CustomOpenSSLObjects& customObjects) + // 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) { - PKCS7* ret = NULL; + unique_PKCS7 content{ PKCS7_new() }; + ThrowOpenSSLErrIfAllocFailed(content); - if ((ret = PKCS7_new()) == NULL) - goto err; + content->type = customObjects.Get(CustomOpenSSLObjectName::spcIndirectDataContext).GetObj(); - ret->type = customObjects.Get(CustomOpenSSLObjectName::spcIndirectDataContext).GetObj(); + content->d.other = ASN1_TYPE_new(); + ThrowOpenSSLErrIfAllocFailed(content->d.other); - ret->d.other = ASN1_TYPE_new(); - if (!ret->d.other) - goto err; + ThrowOpenSSLErrIfFailed(ASN1_TYPE_set_octetstring(content->d.other, nullptr, 0)); + ThrowOpenSSLErrIfFailed(PKCS7_set_content(p7, content.get())); - if (!ASN1_TYPE_set_octetstring(ret->d.other, nullptr, 0)) - goto err; - - if (!PKCS7_set_content(p7, ret)) - goto err; - - return (1); - err: - if (ret != NULL) - PKCS7_free(ret); - return (0); + // The parent PKCS7 now owns this + content.release(); } - PKCS7* PKCS7_sign_indirect_data(X509* signcert, EVP_PKEY* pkey, STACK_OF(X509)* certs, + // 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) { - PKCS7* p7; - int i; - - if (!(p7 = PKCS7_new())) { - PKCS7err(PKCS7_F_PKCS7_SIGN, ERR_R_MALLOC_FAILURE); - return NULL; - } + unique_PKCS7 p7{ PKCS7_new() }; + ThrowOpenSSLErrIfAllocFailed(p7); - if (!PKCS7_set_type(p7, NID_pkcs7_signed)) - goto err; + ThrowOpenSSLErrIfFailed(PKCS7_set_type(p7.get(), NID_pkcs7_signed)); // Standard PKCS7_sign only supports NID_pkcs7_data, but we want SPC_INDIRECT_DATA_OBJID - if (!PKCS7_indirect_data_content_new(p7, customObjects)) - goto err; + PKCS7_indirect_data_content_new(p7.get(), customObjects); // Force SHA256 for now - PKCS7_SIGNER_INFO* signerInfo = PKCS7_sign_add_signer(p7, signcert, pkey, EVP_sha256(), flags); - if (!signerInfo) { - PKCS7err(PKCS7_F_PKCS7_SIGN, PKCS7_R_PKCS7_ADD_SIGNER_ERROR); - goto err; - } + PKCS7_SIGNER_INFO* signerInfo = PKCS7_sign_add_signer(p7.get(), signcert, pkey, EVP_sha256(), flags); + ThrowOpenSSLErrIfFailed(signerInfo); // Add our authenticated attributes - PKCS7_add_attrib_content_type(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcIndirectDataContext).GetObj()); + 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() } ); - // TODO: Make a cleaner way to generate this sequence for the statement type. - const uint8_t statementType[]{ 0x30, 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x01, 0x15 }; + unique_ASN1_STRING statementString{ ASN1_STRING_type_new(V_ASN1_SEQUENCE) }; + ThrowOpenSSLErrIfAllocFailed(statementString); - // TODO: smart pointer - ASN1_STRING* statementString = ASN1_STRING_type_new(V_ASN1_SEQUENCE); - ASN1_STRING_set(statementString, const_cast(statementType), sizeof(statementType)); + // ASN1_STRING_set copies the given data + ThrowOpenSSLErrIfFailed(ASN1_STRING_set(statementString.get(), static_cast(statementTypeSequence.data()), static_cast(statementTypeSequence.size()))); - PKCS7_add_signed_attribute(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcStatementType).GetNID(), V_ASN1_SEQUENCE, statementString); + ThrowOpenSSLErrIfFailed(PKCS7_add_signed_attribute(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcStatementType).GetNID(), V_ASN1_SEQUENCE, statementString.get())); + statementString.release(); - // TODO: Make a cleaner way to generate this too - const uint8_t opusType[]{ 0x30, 0x00 }; + // Add empty opusType + std::vector opusTypeSequence; + opusTypeSequence << ASN1::Sequence{}; - // TODO: smart pointer - ASN1_STRING* opusString = ASN1_STRING_type_new(V_ASN1_SEQUENCE); - ASN1_STRING_set(opusString, const_cast(opusType), sizeof(opusType)); + unique_ASN1_STRING opusString{ ASN1_STRING_type_new(V_ASN1_SEQUENCE) }; + ThrowOpenSSLErrIfAllocFailed(opusString); - PKCS7_add_signed_attribute(signerInfo, customObjects.Get(CustomOpenSSLObjectName::spcSpOpusInfo).GetNID(), V_ASN1_SEQUENCE, 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 (i = 0; i < sk_X509_num(certs); i++) { - if (!PKCS7_add_certificate(p7, sk_X509_value(certs, i))) - goto err; + for (int i = 0; i < sk_X509_num(certs); i++) { + ThrowOpenSSLErrIfFailed(PKCS7_add_certificate(p7.get(), sk_X509_value(certs, i))); } - if (!PKCS7_final(p7, data, flags)) - { - goto err; - } + ThrowOpenSSLErrIfFailed(PKCS7_final(p7.get(), data, flags)); return p7; - - err: - PKCS7_free(p7); - return NULL; } void AppendDigestName(std::vector& target, DigestName name) @@ -173,6 +159,7 @@ namespace MSIX 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; @@ -195,6 +182,8 @@ namespace MSIX 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); @@ -226,10 +215,7 @@ namespace MSIX } } - // [X] 1. Get self signed PKCS7_sign working - // [X] 2. Try to hack the OID of the contents to not be 'data' - // [ ] 3. If that makes a sufficiently nice looking output, create indirect data blob in ASN - // [ ] 4. Else? + // 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, @@ -245,46 +231,38 @@ namespace MSIX // 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) }; - ThrowOpenSSLErrIfAllocFailed(p7); // 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) }; - // TODO: Smart poitners - ASN1_STRING* sequenceString = ASN1_STRING_type_new(V_ASN1_SEQUENCE); - ASN1_STRING_set(sequenceString, reinterpret_cast(completeBlob.data()), static_cast(completeBlob.size())); + 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); + 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()) }; - i2d_PKCS7_bio(outBIO.get(), p7.get()); + ThrowOpenSSLErrIfAllocFailed(outBIO); + ThrowOpenSSLErrIfFailed(i2d_PKCS7_bio(outBIO.get(), p7.get())); - MSIX::ComPtr outStream; - ThrowHrIfFailed(CreateStreamOnFile(R"(D:\Temp\evernotesup\openssltest.p7s)", false, &outStream)); + // 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); - outStream->Write(out, cOut, nullptr); - - { - MSIX::ComPtr p7xOutStream; - ThrowHrIfFailed(CreateStreamOnFile(R"(D:\Temp\evernotesup\openssltest.p7x)", false, &p7xOutStream)); - - uint32_t prefix = P7X_FILE_ID; - p7xOutStream->Write(&prefix, sizeof(prefix), nullptr); - p7xOutStream->Write(out, cOut, nullptr); - } - - // For funsies - MSIX::ComPtr result; - ThrowHrIfFailed(CreateStreamOnFile(R"(D:\Temp\evernotesup\openssltest.p7x)", true, &result)); + uint32_t prefix = P7X_FILE_ID; + p7xOutStream->Write(&prefix, sizeof(prefix), nullptr); + p7xOutStream->Write(out, cOut, nullptr); - return result; + return p7xOutStream; } } // namespace MSIX diff --git a/src/msix/common/AppxFactory.cpp b/src/msix/common/AppxFactory.cpp index e9f5b593a..da4aaebbd 100644 --- a/src/msix/common/AppxFactory.cpp +++ b/src/msix/common/AppxFactory.cpp @@ -7,7 +7,7 @@ #include "Exceptions.hpp" #include "AppxPackageObject.hpp" #include "MSIXResource.hpp" -#include "VectorStream.hpp" +#include "MemoryStream.hpp" #include "MsixFeatureSelector.hpp" #include "AppxPackageWriter.hpp" #include "AppxBundleWriter.hpp" @@ -180,7 +180,7 @@ 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); + auto resourceStream = ComPtr::Make(&m_resourcesVector); m_resourcezip = ComPtr::Make(resourceStream.Get()); } auto file = m_resourcezip->GetFile(resource); 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/ZipObject.cpp b/src/msix/common/ZipObject.cpp index a9f541f20..7828b5090 100644 --- a/src/msix/common/ZipObject.cpp +++ b/src/msix/common/ZipObject.cpp @@ -7,7 +7,7 @@ #include "ObjectBase.hpp" #include "ComHelper.hpp" #include "ZipObject.hpp" -#include "VectorStream.hpp" +#include "MemoryStream.hpp" #include "MsixFeatureSelector.hpp" #include "ZipFileStream.hpp" #include "InflateStream.hpp" @@ -233,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>()); } diff --git a/src/msix/msix.cpp b/src/msix/msix.cpp index 1ef656d8e..85192b504 100644 --- a/src/msix/msix.cpp +++ b/src/msix/msix.cpp @@ -24,7 +24,7 @@ #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 @@ -445,7 +445,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE PackBundle( if(manifestOnly) { - stream = MSIX::ComPtr::Make(&streamVector); + stream = MSIX::ComPtr::Make(&streamVector); } else { @@ -482,7 +482,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/Signing.cpp b/src/msix/pack/Signing.cpp index 81314e020..8fc60055d 100644 --- a/src/msix/pack/Signing.cpp +++ b/src/msix/pack/Signing.cpp @@ -57,31 +57,16 @@ bool DoesCertificateFormatRequirePrivateKey(MSIX_CERTIFICATE_FORMAT format) } // 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, IStream* privateKey) { - // TODO: Dissimenate or delete comment - // Steps: - // Create digest accumulator (AppxSipCreateIndirectData) - // Hashes are for [in order]: - // Package Content [hash of everything but the central directory, except signature file] - // Central Directory [Containing everything but the signature file] - // Content Types [final file stream, must include signature and catalog types] - // Block Map [full stream] - // CI Catalog [full stream] [optional] - // Extract Content Types for editing - // Remove content types, any existing catalog and signature - // Spin through all files, passing them along to accumulator - // Create catalog if needed (also adds its own digest) (CreateSignedPEFilesCatalog) - // [MERGE WITH CONCURRENT PACKAGE WRITE PATH HERE] - // Collect last few digests (non-file based ones) - // Create signature stream - // Append content types, catalog, and signature - // Write zip central directory back - std::unique_ptr signatureAccumulator = std::make_unique(); // Get the publisher from the manifest for verifying later @@ -117,14 +102,10 @@ void SignPackage( } } - // Extract content types for editing, then create a new writer that will be ready to append. - auto contentTypesStream = underlyingStorage->GetFile(CONTENT_TYPES_XML); - ContentTypeWriter contentTypeWriter{ contentTypesStream.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), std::move(contentTypeWriter)); + auto packageWriter = ComPtr::Make(packageAsIPackage.Get(), std::move(signatureAccumulator)); packageWriter->Close(signingCertificateFormat, signingCertificate, privateKey); } diff --git a/src/msix/pack/XmlWriter.cpp b/src/msix/pack/XmlWriter.cpp index 5db87e630..dedf6a63c 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" @@ -28,7 +28,7 @@ namespace MSIX { ThrowErrorIf(Error::InvalidParameter, endElementCandidate != endElementString, "stream did not end with end element"); // Write out everything but the end element - m_stream = ComPtr::Make(); + m_stream = ComPtr::Make(); ULONG toWrite = static_cast(source.length() - endElementString.length()); ULONG written = 0; 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()); } From d7a2eb4a8f28d202fdae29ea69c37f944373b097 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 7 Aug 2019 15:29:10 -0700 Subject: [PATCH 04/22] Fix a few more build breaks and include new builds in readme --- README.md | 4 ++++ src/msix/PAL/Crypto/Win32/SignatureCreator.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp b/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp index d14ddda41..b4e1964c5 100644 --- a/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp +++ b/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp @@ -6,7 +6,7 @@ namespace MSIX { - ComPtr Sign( + ComPtr SignatureCreator::Sign( AppxSignatureObject* digests, MSIX_CERTIFICATE_FORMAT signingCertificateFormat, IStream* signingCertificate, From 1aad253bfa06140002bc315e54e8f2d80711a627 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 7 Aug 2019 15:44:33 -0700 Subject: [PATCH 05/22] Pre-PR feedback and various other platform build breaks --- src/inc/internal/ContentTypeWriter.hpp | 5 +---- src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp | 2 +- src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp | 1 - src/msix/common/StreamHelper.cpp | 2 -- src/msix/pack/ContentTypeWriter.cpp | 6 +++--- 5 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/inc/internal/ContentTypeWriter.hpp b/src/inc/internal/ContentTypeWriter.hpp index 528685aa5..d20e71c4a 100644 --- a/src/inc/internal/ContentTypeWriter.hpp +++ b/src/inc/internal/ContentTypeWriter.hpp @@ -30,11 +30,8 @@ namespace MSIX { 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); + static std::string GetPartNameSearchString(const std::string& fileName); - // 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, - // AddOverride is called. std::map m_defaultExtensions; XmlWriter m_xmlWriter; diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp index 263457358..595de1ee8 100644 --- a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp @@ -21,7 +21,7 @@ namespace MSIX const char* longName; }; -#define MSIX_MAKE_CUSTOM_OBJECT_DEF(_name_,_oid_) { CustomOpenSSLObjectName:: ## _name_, _oid_, #_name_, #_name_ } +#define MSIX_MAKE_CUSTOM_OBJECT_DEF(_name_,_oid_) { CustomOpenSSLObjectName:: _name_, _oid_, #_name_, #_name_ } CustomObjectDef customObjects[] { diff --git a/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp index 2b2179666..70f9ddb1b 100644 --- a/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/SharedOpenSSL.cpp @@ -2,7 +2,6 @@ // Copyright (C) 2017 Microsoft. All rights reserved. // See LICENSE file in the project root for full license information. // -#pragma once #include "SharedOpenSSL.hpp" #include diff --git a/src/msix/common/StreamHelper.cpp b/src/msix/common/StreamHelper.cpp index a01a91dd8..c1d0b796c 100644 --- a/src/msix/common/StreamHelper.cpp +++ b/src/msix/common/StreamHelper.cpp @@ -2,8 +2,6 @@ // Copyright (C) 2017 Microsoft. All rights reserved. // See LICENSE file in the project root for full license information. // -#pragma once - #include "StreamHelper.hpp" #include "StreamBase.hpp" diff --git a/src/msix/pack/ContentTypeWriter.cpp b/src/msix/pack/ContentTypeWriter.cpp index 450a74a60..619498c1b 100644 --- a/src/msix/pack/ContentTypeWriter.cpp +++ b/src/msix/pack/ContentTypeWriter.cpp @@ -53,9 +53,9 @@ namespace MSIX { 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. + // 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 From a092e8416132bcf187f82ec2119a7ed86df451c4 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 7 Aug 2019 16:16:09 -0700 Subject: [PATCH 06/22] Fix Linux and mac build breaks --- src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp index bb4d8e9e4..b0ed4ba25 100644 --- a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp @@ -4,6 +4,7 @@ // #pragma once +#include #include #include @@ -103,7 +104,7 @@ namespace MSIX struct Integer : public Item { - Integer(std::uint32_t val) : m_val(val) {} + Integer(uint32_t val) : m_val(val) {} void AppendTo(Container::BytesType& bytes) const; private: @@ -135,9 +136,9 @@ namespace MSIX // Only items can be written to containers template - inline ContainerType& operator<<(ContainerType& container, const ItemType& item) + inline ContainerType& operator<<(ContainerType&& container, const ItemType& item) { - static_assert(std::is_convertible::value, "Receiving type must be a Container"); + 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; From c8ce38d7e33a8716948467974b09d88e013c9f8e Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 7 Aug 2019 16:28:23 -0700 Subject: [PATCH 07/22] Another fix for l vs r value pedantry --- src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp index b0ed4ba25..c0fb9d36c 100644 --- a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp @@ -113,7 +113,8 @@ namespace MSIX struct OctetString : public Item { - OctetString(Container::BytesType& bytes) : m_bytes(bytes) {} + OctetString(const Container::BytesType& bytes) : m_bytes(bytes) {} + OctetString(Container::BytesType&& bytes) : m_bytes(std::move(bytes)) {} void AppendTo(Container::BytesType& bytes) const; private: From fe5f6b8b9e9f31e112e5abb5beaee8218de71125 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 8 Aug 2019 10:16:51 -0700 Subject: [PATCH 08/22] More build fixes --- src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp index c0fb9d36c..d2e48557d 100644 --- a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.hpp @@ -118,7 +118,7 @@ namespace MSIX void AppendTo(Container::BytesType& bytes) const; private: - Container::BytesType& m_bytes; + Container::BytesType m_bytes; }; struct Null : public Item From 479237f8d977f17b15b0f5623d6cd23281542747 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 8 Aug 2019 11:24:30 -0700 Subject: [PATCH 09/22] Handle all switch cases --- src/msix/pack/ContentType.cpp | 3 ++- src/msix/pack/Signing.cpp | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/msix/pack/ContentType.cpp b/src/msix/pack/ContentType.cpp index fb6af8ea2..105d97613 100644 --- a/src/msix/pack/ContentType.cpp +++ b/src/msix/pack/ContentType.cpp @@ -130,9 +130,10 @@ namespace MSIX { 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/Signing.cpp b/src/msix/pack/Signing.cpp index 8fc60055d..cc9414f13 100644 --- a/src/msix/pack/Signing.cpp +++ b/src/msix/pack/Signing.cpp @@ -51,6 +51,8 @@ bool DoesCertificateFormatRequirePrivateKey(MSIX_CERTIFICATE_FORMAT 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 From c50bf36e7e2ce812bd69e4f16a7f8de9ca92fab8 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Wed, 21 Aug 2019 11:36:02 -0700 Subject: [PATCH 10/22] A bit more verbosity in the unsupported case --- src/inc/internal/MsixFeatureSelector.hpp | 4 ++-- src/inc/public/MsixErrors.hpp | 3 +++ src/inc/shared/Exceptions.hpp | 1 + src/makemsix/main.cpp | 2 +- src/msix/PAL/Crypto/Win32/SignatureCreator.cpp | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) 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/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/Exceptions.hpp b/src/inc/shared/Exceptions.hpp index bcf595cd1..98131a6cb 100644 --- a/src/inc/shared/Exceptions.hpp +++ b/src/inc/shared/Exceptions.hpp @@ -133,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); } diff --git a/src/makemsix/main.cpp b/src/makemsix/main.cpp index ee80cb783..b6bcf1d1a 100644 --- a/src/makemsix/main.cpp +++ b/src/makemsix/main.cpp @@ -653,7 +653,7 @@ Command CreateSignCommand() "", "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." + " used during signing. It also required building with openssl." }); result.SetInvocationFunc([](const Invocation& invocation) diff --git a/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp b/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp index b4e1964c5..e270f1582 100644 --- a/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp +++ b/src/msix/PAL/Crypto/Win32/SignatureCreator.cpp @@ -12,6 +12,6 @@ namespace MSIX IStream* signingCertificate, IStream* privateKey) { - NOTIMPLEMENTED + ThrowErrorAndLog(Error::SupportExcludedByBuild, "Signing requires building with openssl"); } } // namespace MSIX From 63a1bd30991448af6939c8ed83b53fb7cf648aa8 Mon Sep 17 00:00:00 2001 From: John McPherson Date: Thu, 25 Jun 2020 16:54:14 -0700 Subject: [PATCH 11/22] Fix a few minor issues after merging --- src/msix/pack/XmlWriter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msix/pack/XmlWriter.cpp b/src/msix/pack/XmlWriter.cpp index dedf6a63c..d0f023649 100644 --- a/src/msix/pack/XmlWriter.cpp +++ b/src/msix/pack/XmlWriter.cpp @@ -124,7 +124,7 @@ namespace MSIX { // and all #xD characters are replaced by void XmlWriter::WriteTextValue(const std::string& value) { - for (int i = 0; i < value.size(); i++) + for (size_t i = 0; i < value.size(); i++) { if (value[i] == '&') { From 4589e8e145e3c7615a0ce8f94cd750fa8d280ba7 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Thu, 19 Sep 2024 15:00:54 -0700 Subject: [PATCH 12/22] build fixes --- cmake/crypto_sources.cmake | 2 + src/inc/internal/Crypto.hpp | 68 +++++++++++---------------- src/inc/internal/SHA256HashStream.hpp | 6 +-- src/msix/pack/AppxBundleWriter.cpp | 2 +- src/msix/pack/Signing.cpp | 14 +++--- 5 files changed, 40 insertions(+), 52 deletions(-) diff --git a/cmake/crypto_sources.cmake b/cmake/crypto_sources.cmake index 3e0ec919b..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 diff --git a/src/inc/internal/Crypto.hpp b/src/inc/internal/Crypto.hpp index 761bb2a96..20ab28da3 100644 --- a/src/inc/internal/Crypto.hpp +++ b/src/inc/internal/Crypto.hpp @@ -3,10 +3,7 @@ // See LICENSE file in the project root for full license information. // #pragma once -#include "Exceptions.hpp" -#include -#include #include #ifndef SHA256_DIGEST_LENGTH @@ -15,50 +12,39 @@ namespace MSIX { - // Forward declaration of type defined within PAL - struct SHA256Context; - - // Class used to compute SHA256 hashes over various sets of data. - // Create one and Add data to it if the data is not all available, - // or simply call ComputeHash if the data is all in memory. class SHA256 { public: - using HashBuffer = std::vector; + static bool ComputeHash(const std::uint8_t *buffer, std::uint32_t cbBuffer, std::vector& hash); + /// + /// Construct and initialize the hash engine so it can be used to compute hash of input data. + /// SHA256(); - - // Adds the next chunk of data to the hash. - void Add(const uint8_t* buffer, size_t cbBuffer); - - inline void Add(const std::vector& buffer) - { - Add(buffer.data(), buffer.size()); - } - - // Gets the hash of the data. This is a destructive action; the accumulated hash - // value will be returned and the object can no longer be used. - void Get(HashBuffer& hash); - - inline HashBuffer Get() - { - HashBuffer result{}; - Get(result); - return result; - } - - // Computes the hash of the given buffer immediately. - static bool ComputeHash(uint8_t *buffer, std::uint32_t cbBuffer, HashBuffer& hash); + ~SHA256(); + + /// + /// Reset the internal state of the hash engine so it can be used again to hash data. + /// + void Reset(); + + /// + /// Hash data. Can be called repeatedly to hash a stream of data. Call FinalizeAndGetHashValue to finalize the hash engine + /// and get the hash value of all the input data. + /// + /// Buffer containing data + /// Size of the data in bytes + void HashData(const std::uint8_t* buffer, std::uint32_t cbBuffer); + + /// + /// Finalize the hash engine and get the computed hash value of all the input data from HashData calls. + /// After this call, the hash engine cannot be used again until Reset is called to reset its state. + /// + /// Output bufer to receive the computed hash value. + void FinalizeAndGetHashValue(std::vector& hash); private: - void EnsureNotFinished() const { ThrowErrorIfNot(Error::InvalidState, context, "The hash is already finished"); } - - struct SHA256ContextDeleter - { - void operator()(SHA256Context* context); - }; - - std::unique_ptr context; + void* m_hashContext = nullptr; }; class Base64 @@ -66,4 +52,4 @@ namespace MSIX { public: static std::string ComputeBase64(const std::vector& buffer); }; -} +} \ No newline at end of file diff --git a/src/inc/internal/SHA256HashStream.hpp b/src/inc/internal/SHA256HashStream.hpp index f11f121bc..fa4697887 100644 --- a/src/inc/internal/SHA256HashStream.hpp +++ b/src/inc/internal/SHA256HashStream.hpp @@ -17,7 +17,7 @@ namespace MSIX { HRESULT STDMETHODCALLTYPE Write(const void* buffer, ULONG countBytes, ULONG* bytesWritten) noexcept override try { - m_hasher.Add(reinterpret_cast(buffer), countBytes); + m_hasher.HashData(reinterpret_cast(buffer), countBytes); if (bytesWritten) { *bytesWritten = countBytes; @@ -25,9 +25,9 @@ namespace MSIX { return static_cast(Error::OK); } CATCH_RETURN(); - SHA256::HashBuffer GetHash() + void FinalizeAndGetHashValue(std::vector& hash) { - return m_hasher.Get(); + m_hasher.FinalizeAndGetHashValue(hash); } private: 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/Signing.cpp b/src/msix/pack/Signing.cpp index cc9414f13..fdbaba912 100644 --- a/src/msix/pack/Signing.cpp +++ b/src/msix/pack/Signing.cpp @@ -141,12 +141,12 @@ ComPtr SignatureAccumulator::GetSignatureObject(IZipWriter* // This leaves only the full package hash and the central directory hash. // Simply copy the zip hash over - result->GetFileRecordsDigest() = GetZipHasher().Get(); + GetZipHasher().FinalizeAndGetHashValue(result->GetFileRecordsDigest()); // Create the central directory as it currently exists and hash it ComPtr cdHash = ComPtr::Make(); zipWriter->WriteCentralDirectoryToStream(cdHash.Get()); - result->GetCentralDirectoryDigest() = cdHash->GetHash(); + cdHash->FinalizeAndGetHashValue(result->GetCentralDirectoryDigest()); return result; } @@ -182,11 +182,11 @@ SignatureAccumulator::FileAccumulator::~FileAccumulator() { if (isBlockmap) { - signatureAccumulator.GetSignatureObject()->GetAppxBlockMapDigest() = GetRawHasher().Get(); + GetRawHasher().FinalizeAndGetHashValue(signatureAccumulator.GetSignatureObject()->GetAppxBlockMapDigest()); } else if (isContentTypes) { - signatureAccumulator.GetSignatureObject()->GetContentTypesDigest() = GetRawHasher().Get(); + GetRawHasher().FinalizeAndGetHashValue(signatureAccumulator.GetSignatureObject()->GetContentTypesDigest()); } else { @@ -205,7 +205,7 @@ bool SignatureAccumulator::FileAccumulator::AccumulateRaw(IStream* stream) for (const auto& bytes : Helper::StreamProcessor{ stream, 1 << 20 }) { - hasher.Add(bytes); + hasher.HashData(bytes.data(), bytes.size()); } } else @@ -226,7 +226,7 @@ bool SignatureAccumulator::FileAccumulator::AccumulateRaw(const std::vectorWrite(static_cast(bytes.data()), static_cast(bytes.size()), nullptr)); From e1db6416029c873ef6eeaaf4db709b7f84773076 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Thu, 19 Sep 2024 15:01:08 -0700 Subject: [PATCH 13/22] improve debug message --- src/inc/internal/ObjectBase.hpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/inc/internal/ObjectBase.hpp b/src/inc/internal/ObjectBase.hpp index e18b4c77e..5d74ebf85 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,7 +21,10 @@ 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."); + if (spec != value) { + std::cout << "Value in field doesn't match expection; expected " << spec << ", got " << value << std::endl; + ThrowErrorIfNot(Error::InvalidParameter, spec == value, "Incorrect value specified at field."); + } } // there is exactly one value that this field is not allowed to be @@ -229,4 +234,4 @@ class StructuredObject : public TypeList } }; -} /* namespace Meta */ } /* namespace MSIX */ \ No newline at end of file +} /* namespace Meta */ } /* namespace MSIX */ From 8d0334e7c8b1bac54646a783ddc989e4198a54f3 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Thu, 19 Sep 2024 17:35:30 -0700 Subject: [PATCH 14/22] Fix bug with oids that now exist in openssl's db --- src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp index 595de1ee8..ac7d32529 100644 --- a/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/OpenSSLWriting.cpp @@ -37,10 +37,14 @@ namespace MSIX { for (const auto& obj : customObjects) { - int nid = OBJ_create(obj.oid, obj.shortName, obj.longName); + int nid = OBJ_txt2nid(obj.oid); if (nid == NID_undef) { - ThrowOpenSSLError("Failed to create custom OpenSSL object"); + 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); } @@ -162,4 +166,4 @@ namespace ASN1 { } } // namespace ASN1 -} // namespace MSIX \ No newline at end of file +} // namespace MSIX From 73a3daea55f5e1edb2704de85429b447c7222f9b Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Thu, 19 Sep 2024 17:35:40 -0700 Subject: [PATCH 15/22] Add password parameter for pfxes --- src/inc/internal/AppxPackageWriter.hpp | 2 ++ src/inc/internal/SignatureCreator.hpp | 1 + src/inc/internal/Signing.hpp | 1 + src/inc/public/AppxPackaging.hpp | 1 + src/makemsix/main.cpp | 2 ++ src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp | 6 ++++-- src/msix/msix.cpp | 3 ++- src/msix/pack/AppxPackageWriter.cpp | 5 +++-- src/msix/pack/Signing.cpp | 3 ++- 9 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/inc/internal/AppxPackageWriter.hpp b/src/inc/internal/AppxPackageWriter.hpp index 5067c43b3..cc45bb7bf 100644 --- a/src/inc/internal/AppxPackageWriter.hpp +++ b/src/inc/internal/AppxPackageWriter.hpp @@ -34,6 +34,7 @@ class IPackageWriter : public IUnknown 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); @@ -52,6 +53,7 @@ namespace MSIX { void Close( MSIX_CERTIFICATE_FORMAT signingCertificateFormat, IStream* signingCertificate, + const char* pass, IStream* privateKey) override; // IAppxPackageWriter diff --git a/src/inc/internal/SignatureCreator.hpp b/src/inc/internal/SignatureCreator.hpp index 83ebddc0c..0d614b7b1 100644 --- a/src/inc/internal/SignatureCreator.hpp +++ b/src/inc/internal/SignatureCreator.hpp @@ -17,6 +17,7 @@ namespace MSIX { 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 index f477f2d2e..6ff92a698 100644 --- a/src/inc/internal/Signing.hpp +++ b/src/inc/internal/Signing.hpp @@ -35,6 +35,7 @@ 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. diff --git a/src/inc/public/AppxPackaging.hpp b/src/inc/public/AppxPackaging.hpp index 0f8b87220..8a35a24dc 100644 --- a/src/inc/public/AppxPackaging.hpp +++ b/src/inc/public/AppxPackaging.hpp @@ -1810,6 +1810,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE SignPackage( LPCSTR package, MSIX_CERTIFICATE_FORMAT signingCertificateFormat, LPCSTR signingCertificate, + LPCSTR pass, LPCSTR privateKey ) noexcept; diff --git a/src/makemsix/main.cpp b/src/makemsix/main.cpp index b6bcf1d1a..d5e2a3775 100644 --- a/src/makemsix/main.cpp +++ b/src/makemsix/main.cpp @@ -635,6 +635,7 @@ Command CreateSignCommand() 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", "Certificat epassword.", 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. @@ -666,6 +667,7 @@ Command CreateSignCommand() const_cast(invocation.GetOptionValue("-p").c_str()), GetCertificateFormat(invocation), const_cast(invocation.GetOptionValue("-c").c_str()), + const_cast(invocation.GetOptionValue("-pass").c_str()), nullptr); }); diff --git a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp index 27542f218..01a6c854c 100644 --- a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp @@ -24,6 +24,7 @@ namespace MSIX SigningInfo( MSIX_CERTIFICATE_FORMAT signingCertificateFormat, IStream* signingCertificate, + const char* pass, IStream* privateKey) { switch (signingCertificateFormat) @@ -35,7 +36,7 @@ namespace MSIX 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(), nullptr, "Unable to open PFX file"); + ParsePKCS12(cert.get(), pass, "Unable to open PFX file - incorrect password?"); } break; @@ -220,13 +221,14 @@ namespace MSIX 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, privateKey }; + SigningInfo signingInfo{ signingCertificateFormat, signingCertificate, pass, privateKey }; // Create the blob to be signed std::vector signedData = CreateDataToBeSigned(digests, customObjects); diff --git a/src/msix/msix.cpp b/src/msix/msix.cpp index 85192b504..7dc2d4879 100644 --- a/src/msix/msix.cpp +++ b/src/msix/msix.cpp @@ -318,6 +318,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE SignPackage( LPCSTR package, MSIX_CERTIFICATE_FORMAT signingCertificateFormat, LPCSTR signingCertificate, + LPCSTR pass, LPCSTR privateKey ) noexcept try { @@ -356,7 +357,7 @@ MSIX_API HRESULT STDMETHODCALLTYPE SignPackage( MSIX::ComPtr reader; ThrowHrIfFailed(factory->CreatePackageReader(packageStream.Get(), &reader)); - MSIX::SignPackage(reader.Get(), signingCertificateFormat, certificateStream.Get(), privateKeyStream.Get()); + MSIX::SignPackage(reader.Get(), signingCertificateFormat, certificateStream.Get(), pass, privateKeyStream.Get()); return static_cast(MSIX::Error::OK); } CATCH_RETURN(); diff --git a/src/msix/pack/AppxPackageWriter.cpp b/src/msix/pack/AppxPackageWriter.cpp index 5bd3ac01c..dfa5364b7 100644 --- a/src/msix/pack/AppxPackageWriter.cpp +++ b/src/msix/pack/AppxPackageWriter.cpp @@ -72,6 +72,7 @@ namespace MSIX { void AppxPackageWriter::Close( MSIX_CERTIFICATE_FORMAT signingCertificateFormat, IStream* signingCertificate, + const char* pass, IStream* privateKey) { bool signing = static_cast(m_signatureAccumulator); @@ -111,7 +112,7 @@ namespace MSIX { } auto digestData = m_signatureAccumulator->GetSignatureObject(m_zipWriter.Get()); - auto signatureStream = SignatureCreator::Sign(digestData.Get(), signingCertificateFormat, signingCertificate, privateKey); + auto signatureStream = SignatureCreator::Sign(digestData.Get(), signingCertificateFormat, signingCertificate, pass, privateKey); AddFileToPackage(APPXSIGNATURE_P7X, signatureStream.Get(), true, false, nullptr, false, false); } @@ -160,7 +161,7 @@ namespace MSIX { failState.release(); // Merge with standalone signing path, with no signing information. - Close(MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN, nullptr, nullptr); + Close(MSIX_CERTIFICATE_FORMAT::MSIX_CERTIFICATE_FORMAT_UNKNOWN, nullptr, nullptr, nullptr); return static_cast(Error::OK); } CATCH_RETURN(); diff --git a/src/msix/pack/Signing.cpp b/src/msix/pack/Signing.cpp index fdbaba912..6363a1b21 100644 --- a/src/msix/pack/Signing.cpp +++ b/src/msix/pack/Signing.cpp @@ -67,6 +67,7 @@ void SignPackage( IAppxPackageReader* package, MSIX_CERTIFICATE_FORMAT signingCertificateFormat, IStream* signingCertificate, + const char* pass, IStream* privateKey) { std::unique_ptr signatureAccumulator = std::make_unique(); @@ -109,7 +110,7 @@ void SignPackage( // continue that process with Close. auto packageWriter = ComPtr::Make(packageAsIPackage.Get(), std::move(signatureAccumulator)); - packageWriter->Close(signingCertificateFormat, signingCertificate, privateKey); + packageWriter->Close(signingCertificateFormat, signingCertificate, pass, privateKey); } // SignatureAccumulator From 89f20103a52ad6f1af28a489ba3ad3f229e4dc86 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Mon, 21 Oct 2024 15:04:30 -0700 Subject: [PATCH 16/22] fix mismatch in windows pipelines --- pipelines/templates/build-windows.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 From 63475521b01054931242ebde7bbe012fc34102c4 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 22 Oct 2024 14:11:43 -0700 Subject: [PATCH 17/22] Clean up construction and presentation of errors --- src/inc/internal/ObjectBase.hpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/inc/internal/ObjectBase.hpp b/src/inc/internal/ObjectBase.hpp index 5d74ebf85..c50af4bc2 100644 --- a/src/inc/internal/ObjectBase.hpp +++ b/src/inc/internal/ObjectBase.hpp @@ -21,25 +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) { - if (spec != value) { - std::cout << "Value in field doesn't match expection; expected " << spec << ", got " << value << std::endl; - 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()); } ////////////////////////////////////////////////////////////////////////////////////////////// From a9c4c5719b41ba4a0098e554300555510c1c9c3e Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 22 Oct 2024 14:19:09 -0700 Subject: [PATCH 18/22] fix typo --- src/makemsix/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/makemsix/main.cpp b/src/makemsix/main.cpp index d5e2a3775..c01f9a340 100644 --- a/src/makemsix/main.cpp +++ b/src/makemsix/main.cpp @@ -635,7 +635,7 @@ Command CreateSignCommand() 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", "Certificat epassword.", false, 1, "password" }, + 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. From 77f31928e91d56f643c02d16701faccfe18c61dc Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 22 Oct 2024 15:02:08 -0700 Subject: [PATCH 19/22] Allow OpenSSL to provide error message --- src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp index 01a6c854c..13669060f 100644 --- a/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp +++ b/src/msix/PAL/Crypto/OpenSSL/SignatureCreator.cpp @@ -36,7 +36,7 @@ namespace MSIX 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 - incorrect password?"); + ParsePKCS12(cert.get(), pass, "Unable to open PFX file"); } break; From 8cabbccf2889e64ac305d8d88d0c3b9bdc7a3326 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 22 Oct 2024 15:02:29 -0700 Subject: [PATCH 20/22] Reduce build warnings from implicitly deleted explicitly defaulted copy constructor --- src/inc/internal/Signing.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inc/internal/Signing.hpp b/src/inc/internal/Signing.hpp index 6ff92a698..6249e0fb5 100644 --- a/src/inc/internal/Signing.hpp +++ b/src/inc/internal/Signing.hpp @@ -46,8 +46,8 @@ struct SignatureAccumulator // TODO: Take in hash algorithm and options for what to accumulate SignatureAccumulator() = default; - SignatureAccumulator(const SignatureAccumulator&) = default; - SignatureAccumulator& operator=(const SignatureAccumulator&) = default; + SignatureAccumulator(const SignatureAccumulator&) = delete; + SignatureAccumulator& operator=(const SignatureAccumulator&) = delete; SignatureAccumulator(SignatureAccumulator&&) = default; SignatureAccumulator& operator=(SignatureAccumulator&&) = default; From b45b7a9dd060024ddd22644486377dd27b1e2050 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 22 Oct 2024 20:40:10 -0700 Subject: [PATCH 21/22] Add tests --- src/test/msixtest/CMakeLists.txt | 1 + .../PAL/Pack/NonWindows/PackValidation.cpp | 9 ++- .../PAL/Pack/Windows/PackValidation.cpp | 9 ++- src/test/msixtest/inc/PackTestData.hpp | 2 +- src/test/msixtest/inc/PackValidation.hpp | 2 +- src/test/msixtest/inc/msixtest_int.hpp | 1 + src/test/msixtest/msixtest.cpp | 2 + src/test/msixtest/sign.cpp | 77 +++++++++++++++++++ src/test/msixtest/testData/PackTestData.cpp | 10 ++- src/test/testData/sign/README.md | 14 ++++ 10 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/test/msixtest/sign.cpp create mode 100644 src/test/testData/sign/README.md 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" + + From ba8ccf99f67d6e0e5a1a181b4c924ec7e881a595 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Fri, 25 Oct 2024 10:54:31 -0700 Subject: [PATCH 22/22] Add generated self-signed untrusted pfx files --- .../sign/test-self-signed-pfx-no-pass.pfx | Bin 0 -> 4435 bytes .../sign/test-self-signed-pfx-simple-pass.pfx | Bin 0 -> 4435 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/test/testData/sign/test-self-signed-pfx-no-pass.pfx create mode 100644 src/test/testData/sign/test-self-signed-pfx-simple-pass.pfx 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 0000000000000000000000000000000000000000..331687cbe2ee97f51d500b320dd74a8401eb4fa1 GIT binary patch literal 4435 zcmai&Wl$6j^T#>vI9l%j>28iXT1rZ!Tj`R{Bcws%r~?Fq6X}rdPLY%&q#NWYMKM6S z>+zZYe}2E0&x@U%-S3Ot*_oZs&ccyU9b5nqj)dZY31Km+F=wOzd_VybatcC14&g}1 zA2<^H`CkcKfCP{JO%34!aQ?d9e-!}S>Yo6Q6s`{!`Zpwjvx6W~?}C{vCwFQCDrCfa z9m{xeaS3qXBw&L7tBOmAhXZE-6I#cp0_=ex01O0S|TYuLu}RNHKx=V$YS~iptnI zUFC0y=Aa3aPwgqLTMJy?Q+~hT{u{r>OhxgEj&)9I24M7cuWzX38)xU2RA8WEy^;1 zT~GG&S5zb6e>BnaNwHq;`k@xKskzIz)-jl|5LNjA_aT#LeT?WU6-5p@szHnIOCS!y zQs>9YN6D(Vs8bGx%$BqSm~gn}7w&s_)a=}`0(t2HbPQc}n`~}-KrM3T*(ObhS;?W5 z6AC3l+ufq~T&mzqy}WNR%(6HD(dOlQqio8@Uvdv7ImAUYzX1!q*OiK;HA7}woh z7G3H^&V-#)_ap)ZLf3lV&*H@4vGuIRxQ&BtZ;gUJP{OUFm@)r$=v?g)gKUj<@TnW{H-krZs zd3ZT*HR6{kksTl!(WJCD@XUmY-b??b*D}2?2QMy%X7h)64q(iB&gdPvjC1cP5F1FB zdd=jqJzX6RK{@)v@NrEnxr-MX0nYtxBcW1enn^wWGJ;H|!FFOX3GZH&H2u=W)^zvG z=#j;0JVandM2V#`8@!XWqF%Q8TnWuYkzj*#Qgv$$&8_b$xNDBxZB7vyav2Qsgvipy zuL7TS&L2%L;8V_B%Tw4GX)doBumBD5Gvg9TwCj`<4c@xOsWa|~ZfBXbzg6eAkju)v zTIHbDHwxG53aLMC5`Akw&SI?@QUTFdkeAJ|GAEWRN#Ybzm>xE`K#W)OQ3ck&Bqm~1>)3I7+H-r6q&i8HwW*TgtR;_ZLul~F z$1l`!c!sqcp0`g-w9C1KtnY5&+YE}?e6DL|86-sXntqrg03%2YRs_1Ceyo4ji;1P_ zZ8~@!hmqVv753-SE2^)KK}Lu2m;c($e0hojSGdHR)x0TqeU8-ZxAVf*hVk|1JYG{O>_&R}+}Mq%Svnml2KFkx zt$$o8oiDj>WG8UDvkAUUJQ7~RFH*Vc+p&j+yiM>l)B^T^MBKSJe&R%8d^jfs0-VrH zpwTHVzY{)LHE0W*h4Mj#ur)yEkx+38Yem>ideD)^<(^$uS4GKr${kmvgcQ4^ig1dk z-Bsoa-tFp~&%vQFt>B8-Zl{N!*o29EtzGAi&MV@%6_uf@9}*%^NE&%C#d~F3#3V8XbyWOc#T(p zrLKXf1&mM7!ZU}m89nx;tn-~5Gk-ya-Ju?CM!Cly00nHl-`!%Q$B4~6YT;2&WqRYq zH0l~4Uf|f8|9J~193bFdC-zOM+@HHDW+)P83c9OwGv4~pjj^7{;aP6i*cK|1GYjN@w)FCgq>79R>mqSTY2&pPgivC%~boT93_&0hY|Ea$7H{b21QDW zZPQX;J?(EmxK+#h8At69h!xp**OeYZs$UQ0{g(G;EU*qZs-xzHxh!+VTDG33V4LWi z(n;V*B9ebN^>q)He%fKZwI=+We@mN9^_sOxNO#ynu7NCe6!|Hrtej4 z($DNV;a=qOLoyun0`qL8XMbMC_;AKBD3Iy`V%^Qu4u?I!>>FNZK-xb3Z<{!)Y|7RO zL~mp-2KRUsm+XacM5nV^Hh=nYtIW|;WhR%`2Q-!6~r`175_au zERThz$%mWvM?!NE7LWP{^EX~AgAQ(8Kw=Dip|c5|c8fH*Rpk9L=l(|gWZPzyHT8NK z>_N0=Dc`U(Lt?7Y=4iVa7Y9Ht2_4Bv?4XOMBz;|j0#TaTE6*8+`$cAnBv15Z9*%d= z=@C1wS~~r_&+Ei5hFx_u1kV1%(+$o0T65g0;edBxYrDK-Ax1~j+c>!w!*4ln9D}x6 z(twkxfZ;X4 z^zydGdkxA(+2C+`jtC4{q>_5_{0@i3_6)g^jJ)z_n1_{?Opxy7#0G(;<#`IJ^DB(I z2g24cqA$qXMCFX{C57D+)Qb}QINhhXpWmx=4X)w;=(}cR?q$)jk4Ce46zBKl{aQ;) z$)G!U(GppU&W<};$_vg_ZX_Wi_1Ji_5A8ANp(=kyh$ z6AM9F2g9C*%mlxCBy03*uV~OI0<(!+(gp-(^?Y7%lLPN<0U(yLz{WXLbvA8dSLJg0 z!{x_HO#|DaC$PzWJ_@pJ4KNcZrMOHu9jYBbmWvq-=qEL|u6>chlbh^9_E22EuTy45 zsMf{Bvbtvx*4EWMchDrIx-IF>Wu~$h?e=NUALK0DQZ-V0y|1R@!AHsCZRhzg&wCDH z$FEE3&3ik;L=}>fOh1z8sAaEWww7^pq1etP@SUUlhul4^s_Ww|iD<8zp)tC}$O%!H z8e1!*CVYvTTRmyZ1+6UTJue*TbD}gj;UP`rDf?9klE;X^3#Ku(4XLNS(`)H!*7|bl z`@Srsz{6gPt0#7M0fA~nX8C3pCD80>+KhR%M_CV=(|q4QqD^d}o}KF4UaGY+-GWO6 zgz^(l=mw+Wu6HbfuH1mljI`E}IB-|W4{I&?QE^CmCHCmeoOON^{M(@+6*CG0-nr8{ zv^zk5RA~HOe5War_Smxm&ZV4nKa;HEuJ0P)sMmUD*jiosW-YkQ#b=|`QIEPZMZ zhSbz}x8lOt5RCSOB^Z_xnpT3swJA)`_ql~rAyZ>o0SakUI zyU<|&G5$%fnXWg#zeA<{mz4KaLT9~*Sb@W1ntI2=pK2i;1RNiFaZT>o%UvfXFN8_u zJ&Cn-l(4_`#(lB9axC87GjzvSlOzG@F=MV8S;f|5p25&ROO?LGTBXOur{7ffiBvw? zW`||~?ZN|b#Mr=S=|uA@P21uV4=m^KZa^kc^|{4Kod=4f3}Y-m4%3^#7F*7Y|19mS z`C*;Jl>WP_p?~$EhFrmYn9vThadPn6*K4#N8kHO+lUNv>UJsF3%GUq53aBO-cpoCF!)gAUT|b7#50-`3B5rEB3j z8v9DwPaFlyB=&)aX~jxRUG-G+3Nj{6%}bqrc`vVpdhR}O_$)b99OHRw z;)|dNroYmhi`M+%FY*Zv|HXXjJa#Hi(D~@|(cEiigt^)C>=yU~SCUXbL^ehR4NP-# zoxzlpUvQ2)wL}yF%3J&f?aUQdGX;V|ZF>e5D=Z?>7?$$s{P#J3n($L7KQP4l#+Mcr zw|jGCNo)zT>cG7nC+w$!Nz!je+B&?8^ab{0cX;ZcbN;zZf1Co;-U~v1sC%892$(h~ zRLl+G^R*&kRzhff-JQU$4?^MLO=Ni za=i$qRH&;aEofa8E#M4|#%n!TXcv6Ic&n^4y75ALrnmmZzQ0UQWvx(A7w;!MRR}}L zsoVhc+$N=R&?oa{Ze`5k?_;lF9iD+Ndfy&d``v!JpQC{LSj5(CvhQ>`IclSoBhvE7 ztMf;NB4$uCeiVbA_iM3#m|CxW53X-DI>%T^pT1|gIB81SiO5K1zfz^0AT-Ty9C?D+ z=41g@%%#Y4E)lD{`O?br+gVl^_XQBA)BAjRI*k%mlj}V1z2Cgk8jvZ%b#F_d`vEdu z0n#$l)!^e(HRtV6U?lF2$dN`{+IEs5G5oG;!3J8?<|+3X2z{iJV3n@%M}rY znI@0c!lC7Dgn`d}FPgqbutpTx4LJqzHzn*a^M>5FhB69N2<}(-Od&{SOF^7?XAce* zeDIcw@eKP_S7nvcB3Y2V9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5639215189597e849ad4537d7bc5692db60db379 GIT binary patch literal 4435 zcmai&Ra6uV)5lp@dI{++NvWknI;EHH?xh<91Xfx~fu*GcsU-xZ1Vp+!mJkVH0cq)a zeb4uu=ec|@=FFM@|6*pYescy4d!mC5zyQOZU}E7ui+vG$NdUkG6v2oufH2}yFpT&R z48vOb*TO1-VU7Hi2G9X$f77>r9RS$+p8}HrtOpkOw*-M%fyANQc1j4uPCw8gf;{h_ z`z1O$4jLGQh4X)1(Qz@+!1P$SHnA@Nju=3|Ga#{S@8H~XfE6Y~gQ>pZ-gHMK3=0w! zTrFkq!xpjZdZF>X_pzhuoqp34gP&kq|@oSC=G`hRQ^$c2dDw!e!Ni#*55La;*e$tQL zQ(tbM=8Xhg#b(mR)S6_YR3~b4hVbA6Mo3M*((8$RpL7jY{uT@$NZ-iQ%qhF$kq$*G z_%o}1d3g`hZzs_3UXX)wdK+RWoSa%SJM5KTQpDn{UM-f!IC+knWweV)Bn zidS=438H7tdaRx8dbARFtSeHfJ6c1Q<|LT?^xE!$bbSp-*G$30ikaY$jYOHzi= z?;=l$&tiPXL1^`%p}S+bA8}8{9SV=2=>b8$^X{IU6D2j1acZ~%DDG?5X^Us%_o{Q$ zB=vfdt!bxBF~Gwu)t!DX^^eCWy9%@=;&Rn-6(a@{I*G*rvpcf69lSP+%Ke zx@>>*WSBox%T^I3Pu6gv?sZqQsv8W6`Rw2gi!tQE;%PJK>!tXh!n#z=G!4?4FPiNk^+bUwqxiUj)qJUa$mw~xlF(~`dq&=?a zOJwO+cqk2HINfy$RkA_l6&F@jP6#ce!U9<#i{+oNYMh{-Rq*y6`3jI;Uvg)%@*J4n zHzU_BM(-?MK^i3!aJBuQw`@+#xLZE>VpC{lc+2Ues%>K??aCz9)A!`tHNv zPm@k1XYXP9d0%96D7B_R8(zd=(z(^)&fACGzd5~bXh__d0GV;6VuGJz(7{X&XF26E zJUkTL)FNfUZ^&R8>C*K}#=b?^W!ov;7&&3WEs#Sl!(%UlrUc>Dbt}US*+OacYQpZ3 z419Xw>Kw#dU+%la=)NK{N;^63f)yI60~NPTCScBSdG^K$#yp=4W1w30D@@neCZ z8Cq)8{Eh8w(zps*2hyl|N3cWek^+{Cgkjp~g<^CM-h&8X-5BmKI&mtKw2I#gMQR$XVwuBRsIg^i3 z@b##dGZrqE;r5Umm>D`9Us)ckXA)kCSIf**7;p#j0J5ZA@R<#J+8Wg$=a{x~;C5A0SYYYX{4{KR>Se%L{Ymk>ch)YBfp4>Efj zc>rhbBeN^TpSCgk%GtC=MZxRNXh{&y9lNx88tTeKoHd4)ZZI~m5mY>LyM9~Y_(!h*AUZxnqQ({}cS!3o_p}e9bsNOz?zVWSw_4d}wQ^Qv_Rxs> z{N?w}?fj3SuP=JZsBBf4M0s$8f-XzU`QZm1v%f47f{epVX4kGSNay$+lz~mS!deC1 zAvVEgKl>Xf$%Zs0{K^+n@Aj8~`ujgJ7;MC6iMDE+>l$zT^%xVU5gvZ??sJ$JTMfBn z4o522w&H|CpW1BxgqZVScgbpx-aqZTT=D1ps8&zwX*zKDrpkY-Ky`XOBd#r`tYLvm zf5TOZ1P-d!Q@1w*I$w0>8(kTtE4wEei~P}pUm#i#86FMiNnG0KqyQHF+Vhbg-D6bg zuPK2wU>F|gA5Q%je7N^O816L~hI{@uPO$L*pNSwm0Qz6E^{@E(e}Rs5o$nKARiw3~ z$&K-L<3#EI1RbwKMx(1?JiG|UI)QpP1^9OY$SKtG_|I@tuoL$M0~1?pwbmqr3-?;? zqp!mYRCYjW5C6;DJJN#lrD2j*9_AOY!2AeMGkKBUpIa_~hXQ+VEaD~gbsdAzp7x@x zFCDu;3;yIT%(J?~T4(&^)J%Ql-NUDt0xqSY`^FfT&{ z;jf6iwrz{G zc#m;E6?1t_<>BQ3ssl=VPQ2ho^TD}`Uwf?cqUGrOnkXXzqWn>t@Rr<#BSX{0Cow$A zw?igITmh`Z(oiV|sF2(RCQdIp6#`3$I_=kV0x{#>13o9DdtwT|YhUCvK7{hSR^FlP zPN1FTyZgI@;p*eZbz9k*{Mmy_r@2Mi&UKliLR@aLVIHOTeLstrunCexk4%N#Fbvfvkpln!x`Cc3AC4Xzb#L! zxy)JZnYxU6_Q1Qr>}UQlAV6ZCoKEjRq|Wzc~~vUM3WXO<H?JTcXOMGXfbabxnjGAZ3t2Dbes#z5co zeK$q`ziw=C7f)(m%6b>_2SZxwPQikRYPUGms}dn`GQ`(81LW`f@8$M=2!T`1$>o5kTz zA$ca<`Z%3ZCXc#8JKEAt_cGE_Rvywag1u32lTs_h7Q-HY=(lWc+#s{lYE`504|IQA zFUJ)J7#c1~|5ZLF!UpHSekcw#Dv&i%Pbo7{EU8BlZSrwLI?TYTxWZT}S@6nwL>fE7lq4@jaRp&8 zocB;%=5=v#wGM&@A`25L96cTZEq^hZ0-QaYc5^yssN+xoRip(jam%si)^#RYEpET=5BE z_a3;k9PR|~x)@J=lfKdlk2Xi;sae~$I4gvKD>2O{ocxsh$NY*q|9DXZx*ws(jNG_CXJv@4^h=a5kT~JAKvE_wPqUvJ|E%yBFQa= zB9_`-oQP;2ZDFpRD%Z`|3bkHO%_whF`(t+4nF}OxhTkNo8q4SN3qUnllBZ_jv!tve z{j%?*u@jz4?d0)-b+9*W<^c8q{44n znzNFfwCx;v69VIlt{tcE{Nm)ASt(q=8%r?>v>x<*$MQ}G7Jr`O&&_JL>Bdft$6KpV zrv$3e)p6MsBv?oSGbYzHDD7d$wAZV5M|->qy+OCBf zO`2v>_E(*aFC6NCuJjIu7`VJ1Y>#|6Z|Vamw5dYAhLy(~ z4rxB{=&mcO{?LCpI5#XM7g-TGD%|j$p*<6Modt8Vmo^9+D{&n0nW!P!Eg8F*3c^OY zVzN;Nv1EywAGfd!9EEe_NVI-zlThZPcTIM3&_K!TPiuRprj*0dBmxcMO>s%(-9-aX z`8^0MPyh4c7PyUD`i(CusgETgz`jF#0fSv@rEz{6oYH2RE##c$9TTUTesWo7QMm3$ z7qr+u7lux~X9#U+5D}g1C+HX-$^T)i+K2DlLzo?)u)HaX7Cza$v_1bT6}2rQE}CG* zpy?iUkgL+26Nvw>H07bpAmubrIwh8s?m5*s24