ebourg / jsign

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

NuGet file support (`.nupkg`) #162

Closed HofmeisterAn closed 4 months ago

HofmeisterAn commented 1 year ago

Thank you for developing Jsign! We greatly appreciate the utility it provides in signing MSI files. We have also encountered the need to sign NuGet files (dependencies) and noticed that the current version seams not support it. Being able to sign NuGet files would be a great improvement. Is there a workaround or alternative solution we might have missed?

It looks like NuGetKeyVaultSignTool uses NuGet.Packaging (SigningUtility) to sign the files.

lanwen commented 1 year ago

More related links:

ebourg commented 1 year ago

NuGet packages don't use an Authenticode signature, but I can get a look and see if Jsign could be extended to support this format. Do you have an example of a signed package?

HofmeisterAn commented 1 year ago

That would be great.

Do you have an example of a signed package?

A signed NuGet? Yes, e.g. the latest versions of Testcontainers are signed (the download button is on the right side).

ebourg commented 1 year ago

Thank you, at first glance:

More technical details here: https://github.com/NuGet/Home/wiki/Package-Signatures-Technical-Details

ebourg commented 1 year ago

I think supporting NuGet files with Jsign is possible. I won't have the time to work on this in the near future, but if someone wants to implement it I'll review and merge it.

lanwen commented 1 year ago

@ebourg could you share some kind of a task list to make it easier where to start?

ebourg commented 1 year ago
sstamm commented 5 months ago

I would like to enter the race for the PR: https://github.com/sstamm/jsign/commit/f2d667b74c642fde708e0dbca48f87602172adb9 Added a NugetFile which by itself passes already 'nuget verify'. But I am struggling at the AuthenticodeSigner integration. I have two problems:

  1. I started a CMSSignedDataGenerator and DigestCalculatorProvider but I cant figure out how to pass the signature through the SignerInfoGeneratorBuilder
  2. AuthenticodeSigner.verify() does not like my signature (currently I commented it out)

(AuthenticodeSigner.createSignedDatacreateSignedData() is where I stopped to merge my testing with the existing SignerInfoGeneratorBuilder)

Any advises how to proceed/handle this?

ebourg commented 5 months ago

@sstamm Thank you for contributing! I've pushed the initial refactoring moving the zip classes to a separate package, if you could rebase your changes that would ease the review. Are you sure implementing a CMSSignedDataGenerator is necessary? I had to create one for Authenticode, but I assume the default implementation can be used in this case.

sstamm commented 5 months ago

Updated my branch: https://github.com/sstamm/jsign/commit/3ce57d2df0eda02c85b06bc6354bf5f702895f7b

Extending the generator itself is not needed, At AuthenticodeSigner.createSignedData() I simply use it without adoptions. Most of the initialization is similar, therefor I thought integration it into createSignedDataGenerator() would avoid duplicate logic.

For verify() looks like I need some kind of NugetDigestCalculatorProvider (currently it's just overwritten) but there I struggle with the output of the default implementation.

ebourg commented 5 months ago

Besides the issue with the verify() method, is the signature valid when checked with nuget verify?

sstamm commented 5 months ago

Beside some warnings related to the certificate: yes. Next week I can valide with an Digicert certifiacte because I'm off for the rest of the week and have no access to the HSM containing it.

nuget verify -All testcontainers_test.3.3.0.nupk -Verbosity detailed
NuGet Version: 6.8.0.131

Verifying Testcontainers.3.3.0
R:\testcontainers_test.3.3.0.nupk

Signature Hash Algorithm: SHA256
Verifying the primary signature with certificate:

  Subject Name: CN=Jsign Code Signing Test Certificate 2022 (RSA)
  SHA1 hash: 6FD90E92283AF2B39C26DDCEA31B14958F9A1BCF
  SHA256 hash: 5899B2E3B030EFE7FA6E7AE968D417C6D0600A07173AD2C45F63BA83BEBA1D06
  Issued by: CN=Jsign Code Signing CA 2022
  Valid from: 15.11.2022 19:15:33 to 10.11.2042 19:15:33
      Subject Name: CN=Jsign Code Signing CA 2022
      SHA1 hash: 4CD3C8E0D0B6A61B0815B6E4431B2FE178954571
      SHA256 hash: 01B2F4AD9B10E46329F2E5D86B7EFCE68131C9BB8ECCA1A286C74BB1D9833F49
      Issued by: CN=Jsign Root Certificate Authority 2022
      Valid from: 15.11.2022 19:15:33 to 10.11.2042 19:15:33
            Subject Name: CN=Jsign Root Certificate Authority 2022
            SHA1 hash: 8787B4C3C45D2D543B5FF16F164499B011B59242
            SHA256 hash: A9A06D06D6B9156893508FDDC85F47FA0BE9C0B6281271529842AD1CCF686DBF
            Issued by: CN=Jsign Root Certificate Authority 2022
            Valid from: 15.11.2022 19:15:33 to 10.11.2042 19:15:33

The primary signature's certificate chain validation failed with error(s): UntrustedRoot, RevocationStatusUnknown, OfflineRevocation

Finished with 1 errors and 3 warnings.
NU3018: The primary signature's signing certificate is not trusted by the trust provider.
WARNING: NU3018: The primary signature found a chain building issue: The revocation function was unable to check revocation because the revocation server could not be reached. For more information, visit https://aka.ms/certificateRevocationMode.
WARNING: NU3018: The primary signature found a chain building issue: RevocationStatusUnknown: The revocation function was unable to check revocation for the certificate.
WARNING: NU3027: The signature should be timestamped to enable long-term signature validity after the certificate has expired.

Package signature validation failed. 

Maybe I broke minor things during my last changes, but with a test-class instead of AuthenticodeSigner verification was successful.

Current error is:

java.security.SignatureException: Signature verification failed, the private key doesn't match the certificate
    at net.jsign.AuthenticodeSigner.verify(AuthenticodeSigner.java:580)
    at net.jsign.AuthenticodeSigner.createSignedData(AuthenticodeSigner.java:435)
    at net.jsign.AuthenticodeSigner.sign(AuthenticodeSigner.java:369)
    at net.jsign.nuget.NugetFileTest.testRemoveSignature(NugetFileTest.java:50)
...
Caused by: org.bouncycastle.cms.CMSSignerDigestMismatchException: message-digest attribute value does not match calculated value
    at org.bouncycastle.cms.SignerInformation.verifyMessageDigestAttribute(Unknown Source)
    at org.bouncycastle.cms.SignerInformation.doVerify(Unknown Source)
    at org.bouncycastle.cms.SignerInformation.verify(Unknown Source)
    at org.bouncycastle.cms.CMSSignedData.verifySignatures(Unknown Source)
    at net.jsign.AuthenticodeSigner.verify(AuthenticodeSigner.java:571)
    ... 29 more
ebourg commented 4 months ago

I've reworked AuthenticodeSigner a bit to ease the implementation:

sstamm commented 4 months ago

Updated my fork: https://github.com/sstamm/jsign/commit/fcc0bab7d596b2a8c873c9ba87c7a072444eb2ff

  1. Which formatter config you are using? Mine broke the format of AuthenticodeSigner again.
  2. Looks like the problem was at ContentInfo (for the PKCS7ProcessableObject), because the content there is a ASN1Encodable. If we e.g. use there a DEROctetString it automatically adds BERTags.OCTET_STRING (0x4) to the content. For now I replaced the PKCS7ProcessableObject for NugetFiles with an CMSProcessableByteArrayCMSProcessableByteArray. (not sure if there is a better way?)
  3. Saw your comment regarding computeDiges(): not sure if it's similar to the AppxFiles, because for nuget we need an hash the complete file-content and for Appx it looks like only the central-directory.

Currently timestmaping does not work and removing the signature, have to take a deeper look but want to report an update.

Without timestmaping it looks already good:

λ nuget verify -All -Verbosity detailed testcontainers.3.3.0.nupkg
NuGet Version: 6.8.0.131

Verifying Testcontainers.3.3.0
R:\testcontainers.3.3.0.nupkg

Signature Hash Algorithm: SHA256
Verifying the primary signature with certificate:

  Subject Name: CN="......"
  SHA1 hash: F9B6F3EAFD7ECBE7A951E4BE41A34D87E2BB1CEB
  SHA256 hash: 2C1EDBB6A258AC6226A084F98F108C8124B33D57DFDCB5E36E3DD722F1F3CFC7
  Issued by: CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1, O="DigiCert, Inc.", C=US
  Valid from: 11.04.2022 02:00:00 to 11.04.2025 01:59:59
      Subject Name: CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1, O="DigiCert, Inc.", C=US
      SHA1 hash: 7B0F360B775F76C94A12CA48445AA2D2A875701C
      SHA256 hash: 46011EDE1C147EB2BC731A539B7C047B7EE93E48B9D3C3BA710CE132BBDFAC6B
      Issued by: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
      Valid from: 29.04.2021 02:00:00 to 29.04.2036 01:59:59
            Subject Name: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
            SHA1 hash: DDFB16CD4931C973A2037D3FC83A4D7D775D05E4
            SHA256 hash: 552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988
            Issued by: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
            Valid from: 01.08.2013 14:00:00 to 15.01.2038 13:00:00

Finished with 0 errors and 1 warnings.
WARNING: NU3027: The signature should be timestamped to enable long-term signature validity after the certificate has expired.

Successfully verified package 'Testcontainers.3.3.0'.
ebourg commented 4 months ago

Updated my fork

Thanks! I'll get a look

Which formatter config you are using? Mine broke the format of AuthenticodeSigner again.

Just 4 spaces, no tabs

Saw your comment regarding computeDiges(): not sure if it's similar to the AppxFiles, because for nuget we need an hash the complete file-content and for Appx it looks like only the central-directory

That's similar, for APPX the whole file has to be hashed as if it wasn't signed, so when checking the signature the unsigned central directory is recreated in a temporary file.

Looks like the problem was at ContentInfo (for the PKCS7ProcessableObject), because the content there is a ASN1Encodable. If we e.g. use there a DEROctetString it automatically adds BERTags.OCTET_STRING (0x4) to the content. For now I replaced the PKCS7ProcessableObject for NugetFiles with an CMSProcessableByteArrayCMSProcessableByteArray. (not sure if there is a better way?)

My latest refactoring has introduced a regression, the signature is now invalid for some file types (msix), I'm investigating why but you've probably pinned the reason.

ebourg commented 4 months ago

The regression is now fixed, that was indeed an issue with the ContentInfo encoding.

For NuGet files I wonder if AuthenticodeSignedDataGenerator can be replaced with CMSSignedDataGenerator. If it works we can leave AuthenticodeSignedDataGenerator unchanged.

sstamm commented 4 months ago

That's similar, for APPX the whole file has to be hashed as if it wasn't signed, so when checking the signature the unsigned central directory is recreated in a temporary file. Indeed, updated the function:

https://github.com/sstamm/jsign/commit/088eefbc345d2936113af674da5cffa7c4a495c3 Not sure if it can happen, that the signature-file is not the last one in the directory, if so, this case is currently not handled. But otherwise it works.

For NuGet files I wonder if AuthenticodeSignedDataGenerator can be replaced with CMSSignedDataGenerator. If it works we can leave AuthenticodeSignedDataGenerator unchanged.

AuthenticodeSignedDataGenerator is also used at timestmaping, therefore I added this hack, otherwise we have to differentiate at both functions (signing and timestmaping). But yes, in general the CMSSignedDataGenerator is sufficent.

For timestamping I'am a little bit confused: At first I thought I can use the RFC3161Timestamper, which does not work. Comparing it with one I use for signing JARs, I am using a different identifier: /** PKCS#9: 1.2.840.113549.1.9.16.2.14 - <a href="https://tools.ietf.org/html/rfc3126">RFC 3126</a> */ PKCSObjectIdentifiers.id_aa_signatureTimeStampToken Further on CMSSignedDataGenerator.generate(CMSTypedData content, boolean encapsulate) I use encapsulate=true Is this may wrong in the RFC3161Timestamper?

With this changes it looks like it is working: https://github.com/sstamm/jsign/commit/f5331bcf76b840c12eaf9bae7e2eaa4a0ad02421

 nuget verify -All -Verbosity detailed testcontainers.3.3.0.nupkg
NuGet Version: 6.8.0.131

Verifying Testcontainers.3.3.0
R:\testcontainers.3.3.0.nupkg

Signature Hash Algorithm: SHA256
Verifying the primary signature with certificate:

  Subject Name: CN="...."
  SHA1 hash: F9B6F3EAFD7ECBE7A951E4BE41A34D87E2BB1CEB
  SHA256 hash: 2C1EDBB6A258AC6226A084F98F108C8124B33D57DFDCB5E36E3DD722F1F3CFC7
  Issued by: CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1, O="DigiCert, Inc.", C=US
  Valid from: 11.04.2022 02:00:00 to 11.04.2025 01:59:59
      Subject Name: CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1, O="DigiCert, Inc.", C=US
      SHA1 hash: 7B0F360B775F76C94A12CA48445AA2D2A875701C
      SHA256 hash: 46011EDE1C147EB2BC731A539B7C047B7EE93E48B9D3C3BA710CE132BBDFAC6B
      Issued by: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
      Valid from: 29.04.2021 02:00:00 to 29.04.2036 01:59:59
            Subject Name: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
            SHA1 hash: DDFB16CD4931C973A2037D3FC83A4D7D775D05E4
            SHA256 hash: 552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988
            Issued by: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
            Valid from: 01.08.2013 14:00:00 to 15.01.2038 13:00:00

Timestamp: 06.02.2024 12:56:49

Verifying primary signature's timestamp with timestamping service certificate:
  Subject Name: CN=DigiCert Timestamp 2023, O="DigiCert, Inc.", C=US
  SHA1 hash: 66F02B32C2C2C90F825DCEAA8AC9C64F199CCF40
  SHA256 hash: D2F6E46DED7422CCD1D440576841366F828ADA559AAE3316AF4D1A9AD40C7828
  Issued by: CN=DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA, O="DigiCert, Inc.", C=US
  Valid from: 14.07.2023 02:00:00 to 14.10.2034 01:59:59

      Subject Name: CN=DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA, O="DigiCert, Inc.", C=US
      SHA1 hash: B6C8AF834D4E53B673C76872AA8C950C7C54DF5F
      SHA256 hash: 281734D4592D1291D27190709CB510B07E22C405D5E0D6119B70E73589F98ACF
      Issued by: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
      Valid from: 23.03.2022 01:00:00 to 23.03.2037 00:59:59
            Subject Name: CN=DigiCert Trusted Root G4, OU=www.digicert.com, O=DigiCert Inc, C=US
            SHA1 hash: A99D5B79E9F1CDA59CDAB6373169D5353F5874C6
            SHA256 hash: 33846B545A49C9BE4903C60E01713C1BD4E4EF31EA65CD95D69E62794F30B941
            Issued by: CN=DigiCert Assured ID Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US
            Valid from: 01.08.2022 02:00:00 to 10.11.2031 00:59:59
                  Subject Name: CN=DigiCert Assured ID Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US
                  SHA1 hash: 0563B8630D62D75ABBC8AB1E4BDFB5A899B24D43
                  SHA256 hash: 3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C
                  Issued by: CN=DigiCert Assured ID Root CA, OU=www.digicert.com, O=DigiCert Inc, C=US
                  Valid from: 10.11.2006 01:00:00 to 10.11.2031 01:00:00

Successfully verified package 'Testcontainers.3.3.0'.
ebourg commented 4 months ago

Further on CMSSignedDataGenerator.generate(CMSTypedData content, boolean encapsulate) I use encapsulate=true Is this may wrong in the RFC3161Timestamper?

AuthenticodeSignedDataGenerator ignores the encapsulate parameter, the signature generated always encapsulates the message. So you can safely assume encapsulate is always true.

ebourg commented 4 months ago

For timestamping I'am a little bit confused: At first I thought I can use the RFC3161Timestamper, which does not work. Comparing it with one I use for signing JARs, I am using a different identifier: /* PKCS#9: 1.2.840.113549.1.9.16.2.14 - RFC 3126 / PKCSObjectIdentifiers.id_aa_signatureTimeStampToken

I've modified RFC3161Timestamper to automatically use the right attribute depending on the type of the signed message.

sstamm commented 4 months ago

Fixed the timestmaping related things and will adopt computeDigest() for the case that there is something after an existing signature, anything else or should I create a PR?

ebourg commented 4 months ago

I've pushed a test file to replace testcontainers.nupkg.

Could you squash your changes into one commit and rebase it on top of the current master branch? I'll take care of the cosmetic changes, expand the test coverage and update the documentation.

sstamm commented 4 months ago

Et voilà! Only thing in my mind is, that the ZipFile only can delete the last entry. I guess this should not be the case, that the signature is not the last one. In general it could be handy if you are able to delete or even replace all entries, but I guess this would be a different story.