Closed g88704 closed 1 year 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.
Some resources about the MSIX format:
Sample files:
Quick overview:
.msix
or .msixbundle
AppxSignature.p7x
, it consists in a PKCS#7 signedData ASN.1 structure prefixed by PKCX
.4BDFC50A-07CE-E24D-B76E-23C839A09FD1
for a msix file, and B3585F0F-DEAA-9A4B-A434-95742D92ECEB
for a bundle.APPX
and is followed by an array of names (4 bytes) + hash (32 bytes) (see the DigestName enum).AppxBlockMap.xml
entry (as an URI in the HashMethod
attribute of the root BlockMap
element, see MSIX package signing).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
Hello, is there any update on this APPX/MSIX issue? Thanks and Kind Regards Michael
@g88704 Nobody is working on this yet.
Bump, I need this :)
@PibePlayer code contributions are welcome :)
Known open source implementations:
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.
@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.
More technical details, taken from https://learn.microsoft.com/en-us/windows/msix/package/signing-known-issues :
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.
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:
- 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.
- Hash everything written so far, make a note of that.
- Create a fake central directory header, hash that.
- Glue together these hashes in a little custom format prefixed by type codes.
- se this small serialized data structure as the digest field in an Authenticate P7X signature file.
- Write out the resulting file as another zip file entry.
- 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?
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) :
AXPC
: digest of the file records, i.e. from offset 0 to 0x247A, right before the first central directory entry. When verifying a signed file, the AppxSignature.p7x
entry must be skipped.AXCT
: digest of the uncompressed content of [ContentTypes].xml
AXBM
: digest of the uncompressed content of AppxBlockMap.xml
AXCD
: digest of the central directory. The expected value is 233E5C593B6BE0F4D6115AFA5F9F4C38D6577C76785BEBF21D0743B3E6AF08F7
, but I'm unable to reproduce it. My understanding is that the central directory should be hashed completely from the first entry to the end of the file before the signature is added to the file, even if it's altered after adding the signature (which means that the signature verification process involves reverting the changes to get back of the original central directory). Unfortunately, hashing the unsigned file from offset 0x247B to the end of the file yields a different value. I tried several variations, by excluding the end of central directory records, but the hash still doesn't match.I'm a bit lost at this point, if someone has a clue please ping me.
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
@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.
@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.
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.
I have started a wiki page summarizing the process of signing MSIX files: https://github.com/ebourg/jsign/wiki/MSIX-APPX-signing
@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?
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.
@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
Well done! The Jsign implementation is near too.
Well done! The Jsign implementation is near too.
@ebourg you are doing the implementation on Jsign ?
@mat1e Yes I'm working on it
@panekmaciej I'm struggling with the 0x80096010 error too. Could you tell me the APPX hash you get for this file please?
I've noticed that signtool strips the unnecessary zip64 extra info fields, I wonder if that normalization step is required.
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
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:
Hashes are OK now. File is not timestamped, but according to the MS documentation, it should still verify.
Victory! 🎉
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.
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?
Actually, I got the content hash wrong when signing the second time, ignore that.
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
Error found, I confirm multiple signatures are possible.
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':
BTW, do you support signing bundles (including signing every appx in the bundle)?
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?
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.
@g88704 @PibePlayer APPX/MSIX signing is now implemented, could you give it a try please?
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.
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.
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
.
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