ebourg / jsign

Java implementation of Microsoft Authenticode for signing Windows executables, installers & scripts
https://ebourg.github.io/jsign
Apache License 2.0
259 stars 108 forks source link

APPX/MSIX file support #81

Closed g88704 closed 1 year ago

g88704 commented 4 years ago

Sorry beginner question...

I want to sign Microsoft MSIX-packages under Linux. I tried your "jsign" tool but with MSIX-Files I get a "jsign: Unsupported file" error.

Thanks in advance and Kind Regards

Michael

ebourg commented 4 years ago

Hi Michael, thank you for bringing this to my attention. Jsign doesn't support MSIX files, this could be an improvement for a future version.

ebourg commented 4 years ago

Some resources about the MSIX format:

Sample files:

Quick overview:

ebourg commented 4 years ago

APPX files used for UWP applications use the same format. The spcSipInfoObjID version and UUID are identical.

Sample files: see Windows Subsystem for Linux distro packages

g88704 commented 3 years ago

Hello, is there any update on this APPX/MSIX issue? Thanks and Kind Regards Michael

ebourg commented 3 years ago

@g88704 Nobody is working on this yet.

PibePlayer commented 2 years ago

Bump, I need this :)

ebourg commented 2 years ago

@PibePlayer code contributions are welcome :)

ebourg commented 2 years ago

Known open source implementations:

mat1e commented 1 year ago

Hi @ebourg, I'm very interrested by this feature and I'm able to contribute. Do you know the process to follow to sign msix and appx packages? It will help me a lot if you can give me somes entries.

ebourg commented 1 year ago

@mat1e Thank you for offering you help. I don't know much about msix signing, besides the info I posted above. I don't think a specification was published, but with several implementations available it should be easy to replicate the hashing mechanism.

ebourg commented 1 year ago

More technical details, taken from https://learn.microsoft.com/en-us/windows/msix/package/signing-known-issues :

ebourg commented 1 year ago

When a MSIX package is signed, the [Content_Types].xml file is modified and an extra entry is added for the AppxSignature.p7x file:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="dat" ContentType="appv/vfs-file" />
  <Default Extension="png" ContentType="appv/vfs-file" />
  <Default Extension="pri" ContentType="appv/vfs-file" />
  <Default Extension="xml" ContentType="application/vnd.ms-appx.manifest+xml" />
  <Override PartName="/AppxBlockMap.xml" ContentType="application/vnd.ms-appx.blockmap+xml" />
  <Override PartName="/AppxSignature.p7x" ContentType="application/vnd.ms-appx.signature" />
</Types>

It would be worth checking if this is mandatory.

ebourg commented 1 year ago

Another implementation: https://github.com/microsoft/msix-packaging/blob/johnmcpms/signing/src/inc/internal/AppxSignature.hpp https://github.com/microsoft/msix-packaging/blob/johnmcpms/signing/src/msix/unpack/AppxSignature.cpp

And a useful comment in the related issue discussing this branch: https://github.com/microsoft/msix-packaging/issues/340#issuecomment-940160085

The sticking point is the hash calculation. The MSIX format has an interesting design in which the zip file effectively contains its own hash. Because you can't hash something that contains its own hash, the writing process has to be done in the following stages:

  1. Write out the first part of the zip, adding all the files. When you write out the manifest, blockmap and content types file, hash their raw (uncompressed contents) and record it.
  2. Hash everything written so far, make a note of that.
  3. Create a fake central directory header, hash that.
  4. Glue together these hashes in a little custom format prefixed by type codes.
  5. se this small serialized data structure as the digest field in an Authenticate P7X signature file.
  6. Write out the resulting file as another zip file entry.
  7. Write out a new central directory structure containing the signature file as well.

The high level approach is clear, but the devil is in the details. The problem is that without being able to see the Windows code it's extremely unclear how this is validated. Presumably Windows is editing the central directory or reformatting it to reproduce the original "fake" central directory table, so it can hash that and compare it to the signed digest. But ... how?

ebourg commented 1 year ago

I've pushed a msix branch with a test file, unsigned and signed by signtool: https://github.com/ebourg/jsign/blob/msix/jsign-core/src/test/resources/minimal.msix https://github.com/ebourg/jsign/blob/msix/jsign-core/src/test/resources/minimal-signed-by-signtool.msix

For this file the digest in the spcIndirectDataContext structure looks like this:

00000000 41 50 50 58 41 58 50 43 FC AB FD 6D E4 B9 CA 86 APPXAXPCü«ýmä¹Ê.
00000010 3B 92 61 66 B1 91 A2 01 F9 5C 39 97 0B 51 6F D6 ;.af±.¢.ù\9..QoÖ
00000020 03 37 7A AF F5 10 9D 36 41 58 43 44 23 3E 5C 59 .7z¯õ..6AXCD#>\Y
00000030 3B 6B E0 F4 D6 11 5A FA 5F 9F 4C 38 D6 57 7C 76 ;kàôÖ.Zú_.L8ÖW|v
00000040 78 5B EB F2 1D 07 43 B3 E6 AF 08 F7 41 58 43 54 x[ëò..C³æ¯.÷AXCT
00000050 C9 86 D8 E1 3E F8 0D 82 BD 75 42 7B 9C 44 42 23 É.Øá>ø..½uB{.DB#
00000060 74 6D 1B B6 EC A8 55 6F 35 08 B8 AE 39 4B C1 19 tm.¶ì¨Uo5.¸®9KÁ.
00000070 41 58 42 4D 2B E5 5D EF 3E 00 08 EE 70 1E AB 04 AXBM+å]ï>..îp.«.
00000080 C2 14 17 10 C6 DE 65 EE E8 26 CD 74 EC 16 13 32 Â...ÆÞeîè&Ítì..2
00000090 75 F2 CF 3A                                     uòÏ:

There is the APPX header, and then 4 names of 4 bytes followed by a 32 byte hash (always SHA-256) :

I'm a bit lost at this point, if someone has a clue please ping me.

panekmaciej commented 1 year ago

This is because you need to hash the central directory together with the EOCDR/zip64 locator records, pretending that AppxSignatrue.7x file was never in the zip (but is it was already added to the XML files). Adding the AppxSignature.px7 modifies the central directory, EOCDR & the locator.

I have pushed a WIP branch of osslsigncode that is able to verify the appx files properly. I will try to finish the signing this week. https://github.com/panekmaciej/osslsigncode/blob/appx/appx.c

Basically: [ContentTypes].xml is hashed after 'AppSignature.7x' record is added to the XML AXPC, meaning the file data is hashed with the Zip local headers, all of the files without AppSignature.7x AXCD is hashed as the central directory without 'AppSignature.7x' entry and with Zip64 EOCDR/Zip64 EOCD locator/ZIP EOCDR, where the EOCDR records are modified such as there was no 'AppSignature.7x' file inside in the zip.

What it means (if you take the original zip EOCDR as the input): -Size of the central directory in EOCDR has to be modified (exclude 'AppSignature.7x" entry in the cd) -Offset of the central directory in EOCDR has to be modified (subtract size of 'AppSignature.7x' data + local header size) -No of disk entries has to be modified (subtract 1 entry) -No of total entries has to be modified (subtract 1 entry) -ZIP64 EOCDR offset in the ZIP64 EOCD Locator has to be modified (subtract size of 'AppSignature.7x' data + local header size + size of 'AppSignature.7x" entry in the central directory).

After you do that, you will get the correct hash.

You can take a look at uint8_t *appx_eocdr_to_buffer(zipFile_t *zip, bool removeSignatureFile, uint64_t *size in my code.

Checking Block Map hashes:
Message digest algorithm  : SHA256
Current message digest    : F864B3E8533CB611C9B7B674CF61E895B77F2FA5CF87DAE74CAB8B4B952DBBB3
Calculated message digest : F864B3E8533CB611C9B7B674CF61E895B77F2FA5CF87DAE74CAB8B4B952DBBB3

Checking Content Types hashes:
Message digest algorithm  : SHA256
Current message digest    : 8AE94B77A2A3A7C0123A95DA7311FFF3C947B0DF40E003FF5EA69492E7B8EEAA
Calculated message digest : 8AE94B77A2A3A7C0123A95DA7311FFF3C947B0DF40E003FF5EA69492E7B8EEAA

Checking Central Directory hashes:
Message digest algorithm  : SHA256
Current message digest    : 64EF5EEF94099B81B14D4CF93F2686F147286A3FA6A8E182133BE8F5297432B8
Calculated message digest : 64EF5EEF94099B81B14D4CF93F2686F147286A3FA6A8E182133BE8F5297432B8

Checking Data hashes:
Message digest algorithm  : SHA256
Current message digest    : FCF69065033BE4485F7B56DB76C7C6B45EF4B7E148F6B5FCD7C4F5DF25E49B5C
Calculated message digest : FCF69065033BE4485F7B56DB76C7C6B45EF4B7E148F6B5FCD7C4F5DF25E49B5C
ebourg commented 1 year ago

@panekmaciej Many thanks for the info! Did you try not to change [ContentTypes].xml when signing? I wonder if this step is optional, that would greatly simplify the process.

panekmaciej commented 1 year ago

@ebourg Unfortunately, you have to add the [ContentTypes].xml entry, otherwise Windows will not see the certificate at all. I am currently stuck at 0x80096010 -> although all my content hashes calculate same as with SignTool signed files, something else fails. I will try to debug using msix-packaging, there is some sort of signature verification there, so maybe I will be able to find out what fails.

ebourg commented 1 year ago

I have been able to reproduce the right checksum for the central directory, that's indeed the SHA-256 from the start of the central directory to the end of the file after [ContentTypes].xml is modified but before AppSignature.7x is added.

ebourg commented 1 year ago

I have started a wiki page summarizing the process of signing MSIX files: https://github.com/ebourg/jsign/wiki/MSIX-APPX-signing

ebourg commented 1 year ago

@panekmaciej According to https://learn.microsoft.com/en-us/windows/win32/com/com-error-codes-4 the 0x80096010 error code means TRUST_E_BAD_DIGEST: The digital signature of the object did not verify.

Did you compare the full spcIndirectDataContext structure with the one produced by signtool? Maybe the APPX header is missing or the order of the AX* digests is different?

ebourg commented 1 year ago

Unfortunately, you have to add the [ContentTypes].xml entry, otherwise Windows will not see the certificate at all

I can confirm that, I've copied the AppSignature.7x entry from the signed file to the unsigned one, and the signature isn't detected by Windows. Copying the modified [ContentTypes].xml entry as well makes the signature visible in the file properties.

panekmaciej commented 1 year ago

@ebourg I have got it working, I have pushed a commit to my branch. I will need to do some cleanup and will issue a PR to the maintainer of osslsigncode.

The problem was, that the Windows verification code is very touchy to the headers of the zip files - it failed when creator version was 0x14 instead of 0x2D. msix-packacking was not helpful, there is no full verification there. Windows error codes were not helpful neither - the hashes were ok, only zip header was not what it expected. I ended up comparing the files with hex editor and making changes one by one until Windows accepted the file.

Anyway, if you have any questions let me know. I will test in on Linux today, and hope to be done with it soon.

Number of files successfully Verified: 1
Number of warnings: 0
Number of errors: 0
ebourg commented 1 year ago

Well done! The Jsign implementation is near too.

mat1e commented 1 year ago

Well done! The Jsign implementation is near too.

@ebourg you are doing the implementation on Jsign ?

ebourg commented 1 year ago

@mat1e Yes I'm working on it

ebourg commented 1 year ago

@panekmaciej I'm struggling with the 0x80096010 error too. Could you tell me the APPX hash you get for this file please?

minimal-signed.msix.zip

I've noticed that signtool strips the unnecessary zip64 extra info fields, I wonder if that normalization step is required.

panekmaciej commented 1 year ago

You have some issue with data hash, it should be all the contents of zip, excluding everything from the beginning of the central directory & excluding whole AppSignature.p7x (local header + compressed data + data descriptor (if exist)).

Checking Block Map hashes:
Message digest algorithm  : SHA256
Current message digest    : 2BE55DEF3E0008EE701EAB04C2141710C6DE65EEE826CD74EC16133275F2CF3A
Calculated message digest : 2BE55DEF3E0008EE701EAB04C2141710C6DE65EEE826CD74EC16133275F2CF3A

Checking Content Types hashes:
Message digest algorithm  : SHA256
Current message digest    : C986D8E13EF80D82BD75427B9C444223746D1BB6ECA8556F3508B8AE394BC119
Calculated message digest : C986D8E13EF80D82BD75427B9C444223746D1BB6ECA8556F3508B8AE394BC119

Checking Data hashes:
Message digest algorithm  : SHA256
Current message digest    : A740C1BABE0653053A2275F41BEF278D574030BCC5189CE090B607130AC09EF1
Calculated message digest : 2ED11C898EC59D63033863AF829D0A6DA8B1A0C48315DE7C986AACBA423E734F     MISMATCH!!!

Checking Central Directory hashes:
Message digest algorithm  : SHA256
Current message digest    : 5518B2826FBAF6920FF6956FA2B29B3B0F93CDEBA9C48025F97A70230F0D5279
Calculated message digest : 5518B2826FBAF6920FF6956FA2B29B3B0F93CDEBA9C48025F97A70230F0D5279

Signatue hash verification failed
Number of verified signatures: 1
Failed
ebourg commented 1 year ago

Got it, thank you, the content hash was calculated before modifying the content types. But there is still an issue, I guess some headers aren't as expected yet. Here is the current signed file:

minimal-signed.msix.zip

panekmaciej commented 1 year ago

Hashes are OK now. File is not timestamped, but according to the MS documentation, it should still verify.

ebourg commented 1 year ago

Victory! 🎉 image

The issue was the two disk number fields in the End of Central Directory Record, they were set to -1 (0xFFFF) by the MSIX packaging tool, but they must be set to 0 for the signature to be valid.

Also, the AppxSignature.p7x entry in the central directory must not have a zip64 extra field (probably unless that's absolutely required, I'll have to check with a package > 4GB). The other entries can retain their zip64 extra field, this has no impact on the validity.

Regarding the versions in the central directory entries, I confirm that for AppxSignature.p7x the "version made by" field must be set to 45 (0x2D) and the "version needed to extract" field must be set to 20 (0x14). Any other value makes the signature invalid.

ebourg commented 1 year ago

I think I've spotted another restriction, it looks like APPX files can only have one signature. signtool often refuses to append a secondary signature, for example when signing a PowerShell script. However if another signing tool adds more signatures, it works. For APPX packages that seems different, once a secondary signature is added, both signature become invalid.

@panekmaciej could you try signing a file twice to confirm?

ebourg commented 1 year ago

Actually, I got the content hash wrong when signing the second time, ignore that.

ebourg commented 1 year ago

With the right hashes the signatures are still invalid :(

I tried with osslsigncode, but the second signature triggers a segmentation fault:

ebourg@icare:~/osslsigncode $ ./osslsigncode sign -pkcs12 keystore.p12 -pass password -in minimal.msix -out minimal-signed.msix
Central directory entry count: 7
Name: Registry.dat Compressed: 2715 Uncompressed: 16384 Offset: 0
Name: User.dat Compressed: 1284 Uncompressed: 12288 Offset: 2781
Name: Assets/StoreLogo.png Compressed: 3110 Uncompressed: 4173 Offset: 4127
Name: Resources.pri Compressed: 397 Uncompressed: 872 Offset: 7311
Name: AppxManifest.xml Compressed: 589 Uncompressed: 1224 Offset: 7775
Name: AppxBlockMap.xml Compressed: 541 Uncompressed: 928 Offset: 8434
Name: [Content_Types].xml Compressed: 221 Uncompressed: 457 Offset: 9045
Succeeded

ebourg@icare:~/osslsigncode $ ./osslsigncode sign -pkcs12 keystore.p12 -pass password -in minimal-signed.msix -out minimal-signed-twice.msix
Central directory entry count: 8
Name: Registry.dat Compressed: 2715 Uncompressed: 16384 Offset: 0
Name: User.dat Compressed: 1284 Uncompressed: 12288 Offset: 2781
Name: Assets/StoreLogo.png Compressed: 3110 Uncompressed: 4173 Offset: 4127
Name: Resources.pri Compressed: 397 Uncompressed: 872 Offset: 7311
Name: AppxManifest.xml Compressed: 589 Uncompressed: 1224 Offset: 7775
Name: AppxBlockMap.xml Compressed: 541 Uncompressed: 928 Offset: 8434
Name: [Content_Types].xml Compressed: 237 Uncompressed: 546 Offset: 9045
Name: AppxSignature.p7x Compressed: 3466 Uncompressed: 4083 Offset: 9355
Segmentation fault
ebourg commented 1 year ago

Error found, I confirm multiple signatures are possible.

panekmaciej commented 1 year ago

Hmm, I did not even know that you would want to sign it twice. Why is that? Excuse me my ignorance, I have only started learning the whole business of code signing last Friday.

I will fix the osslsigncode, segfault is not nice, but I did not expect that someone would sign already signed code.

The only thing I do is I use a timestamp server, so after signing it shows, it as 'Countersigned':

panekmaciej commented 1 year ago

BTW, do you support signing bundles (including signing every appx in the bundle)?

ebourg commented 1 year ago

I did not even know that you would want to sign it twice. Why is that?

At some point signing an exectuable twice with different algorithms was interesting to support older and newer versions of Windows. That's probably useless for APPX/MSIX packages since only one hash algorithm is possible. Another use case would be to sign the file with different trust roots, but since the CN of the signing certificate must match the publisher name, I doubt that will happen in practice.

do you support signing bundles (including signing every appx in the bundle)?

I haven't tested with bundles yet. I assume the embedded appx are to be signed before being packaged together. Does signtool also sign the bundled packages?

panekmaciej commented 1 year ago

Does signtool also sign the bundled packages?

Yes it does. If the packages in the bundle are not signed it will pass the signtool verification, but the package will fail to install with error:

This app package’s publisher certificate could not be verified. Contact your system administrator or the app developer to obtain a new app package with verified certificates. The root certificate and all immediate certificates of the signature in the app package must be verified (0x800B010A)

For my own purposes, I will just sign packages before bundling them.

ebourg commented 1 year ago

@g88704 @PibePlayer APPX/MSIX signing is now implemented, could you give it a try please?

mat1e commented 1 year ago

Hello, firstly many thanks for this feature! I started to study a little bit the .appx format to help to this feature, but you was lot of faster than me.

I'm testing the signature process by downloading files from Microsoft store (using this tutorial from Windows OS Hub). I had an issue because store.rg-adguard.net put uppercases on extension... Once it is fixed, it works. This is due to the case sensitive of Signable.of but anyway, It should not be good to put uppercase on extension name.

I tested with .appx, .msix and .msixbundle and it works. I can see the signature added in the properties of the file. For now I just tested with already signed files but I will continu my tests and let you know if I find something.

ebourg commented 1 year ago

This is due to the case sensitive of Signable.of

This is a bug in the Signable interface, the check should be case insensitive. If you want to open a PR fixing this I'll merge it.

I tested with .appx, .msix and .msixbundle and it works

Great! Thank you for the feedback.

If ever you find an appx/msix file with a SHA-384 or SHA-512 signature I'd be very interested in looking into it. I'd like to verify that the hashes in the spcIndirectDataContext structure always use SHA-256, even if the block hash in AppxBlockMap.xml use a different algorithm.

ebourg commented 1 year ago

I'd like to verify that the hashes in the spcIndirectDataContext structure always use SHA-256, even if the block hash in AppxBlockMap.xml use a different algorithm.

After playing with makeappx.exe I can now say that my assumption was wrong, the spcIndirectDataContext structure does use the hash algorithm defined in AppxBlockMap.xml.

Does signtool also sign the bundled packages?

I've also played with bundles and I can confirm that. Bundles have an AppxBundleManifest.xml file listing the packages bundled. This file also references the offset and the length of the packages in the bundle. So when signing the embedded packages, AppxBundleManifest.xml has to be updated with the new offsets and lengths. And then AppxBlockMap.xml must also be updated with the new size and hash of AppxBundleManifest.xml.