diff --git a/README.md b/README.md index 7d4e7c76..399d73b5 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ See https://ebourg.github.io/jsign for more information. #### Version 6.1 (in development) * Signing of NuGet packages has been implemented (contributed by Sebastian Stamm) +* Jsign now checks if the certificate subject matches the app manifest publisher before signing APPX/MSIX packages * The APPX/MSIX bundles are now signed with the correct Authenticode UUID * The error message displayed when the password of a PKCS#12 keystore is missing has been fixed diff --git a/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java b/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java index d4494fa9..c181c0ca 100644 --- a/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java +++ b/jsign-core/src/main/java/net/jsign/AuthenticodeSigner.java @@ -353,6 +353,8 @@ public AuthenticodeSigner withSignatureProvider(Provider signatureProvider) { * @throws Exception if signing fails */ public void sign(Signable file) throws Exception { + file.validate(chain[0]); + if (file instanceof PEFile) { PEFile pefile = (PEFile) file; diff --git a/jsign-core/src/main/java/net/jsign/Signable.java b/jsign-core/src/main/java/net/jsign/Signable.java index 622b575b..59b0fe66 100644 --- a/jsign-core/src/main/java/net/jsign/Signable.java +++ b/jsign-core/src/main/java/net/jsign/Signable.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.security.MessageDigest; +import java.security.cert.Certificate; import java.util.List; import java.util.ServiceLoader; @@ -98,6 +99,17 @@ default byte[] computeDigest(DigestAlgorithm digestAlgorithm) throws IOException */ ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOException; + /** + * Checks if the specified certificate is suitable for signing the file. + * + * @param certificate the certificate to validate + * @throws IOException if an I/O error occurs + * @throws IllegalArgumentException if the certificate doesn't match the publisher identity + * @since 6.1 + */ + default void validate(Certificate certificate) throws IOException, IllegalArgumentException { + } + /** * Returns the Authenticode signatures on the file. * diff --git a/jsign-core/src/main/java/net/jsign/appx/APPXFile.java b/jsign-core/src/main/java/net/jsign/appx/APPXFile.java index c99ec4a7..696902e0 100644 --- a/jsign-core/src/main/java/net/jsign/appx/APPXFile.java +++ b/jsign-core/src/main/java/net/jsign/appx/APPXFile.java @@ -23,8 +23,12 @@ import java.nio.channels.SeekableByteChannel; import java.security.DigestOutputStream; import java.security.MessageDigest; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.commons.io.output.NullOutputStream; import org.apache.poi.util.IOUtils; @@ -163,6 +167,15 @@ public ASN1Object createIndirectData(DigestAlgorithm digestAlgorithm) throws IOE return new SpcIndirectDataContent(data, digestInfo); } + @Override + public void validate(Certificate certificate) throws IOException, IllegalArgumentException { + String name = ((X509Certificate) certificate).getSubjectX500Principal().getName(); + String publisher = getPublisher(); + if (!name.equals(publisher)) { + throw new IllegalArgumentException("The app manifest publisher name (" + publisher + ") must match the subject name of the signing certificate (" + name + ")"); + } + } + /** * Tells if the package is a bundle. */ @@ -170,6 +183,18 @@ boolean isBundle() { return centralDirectory.entries.containsKey("AppxMetadata/AppxBundleManifest.xml"); } + /** + * Returns the publisher of the package. + */ + String getPublisher() throws IOException { + InputStream in = getInputStream(isBundle() ? "AppxMetadata/AppxBundleManifest.xml" : "AppxManifest.xml", 10 * 1024 * 1024 /* 10MB */); + String manifest = new String(IOUtils.toByteArray(in), UTF_8); + + Pattern pattern = Pattern.compile("Publisher\\s*=\\s*\"([^\"]+)", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(manifest); + return matcher.find() ? matcher.group(1) : null; + } + @Override public List getSignatures() throws IOException { List signatures = new ArrayList<>(); diff --git a/jsign-core/src/test/java/net/jsign/APPXSignerTest.java b/jsign-core/src/test/java/net/jsign/APPXSignerTest.java index 66ce2292..fc41393e 100644 --- a/jsign-core/src/test/java/net/jsign/APPXSignerTest.java +++ b/jsign-core/src/test/java/net/jsign/APPXSignerTest.java @@ -21,11 +21,14 @@ import org.apache.commons.compress.utils.SeekableInMemoryByteChannel; import org.apache.commons.io.FileUtils; +import org.hamcrest.MatcherAssert; import org.junit.Test; import net.jsign.appx.APPXFile; import static net.jsign.DigestAlgorithm.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; public class APPXSignerTest { @@ -172,4 +175,27 @@ public void testSignInMemory() throws Exception { SignatureAssert.assertSigned(file, SHA256); } } + + @Test + public void testSignWithMismatchedCertificate() throws Exception { + File sourceFile = new File("target/test-classes/minimal.msix"); + File targetFile = new File("target/test-classes/minimal-signed-with-mismatched-certificate.msix"); + + FileUtils.copyFile(sourceFile, targetFile); + + KeyStore keystore = new KeyStoreBuilder().storetype("NONE") + .keyfile("target/test-classes/keystores/privatekey.pkcs8.pem") + .certfile("target/test-classes/keystores/jsign-test-certificate-partial-chain.pem") + .build(); + AuthenticodeSigner signer = new AuthenticodeSigner(keystore, "jsign", "").withTimestamping(false); + + try (Signable file = Signable.of(targetFile)) { + try { + signer.sign(file); + fail("Exception not thrown"); + } catch (Exception e) { + MatcherAssert.assertThat(e.getMessage(), matchesPattern("The app manifest publisher name (.*) must match the subject name of the signing certificate (.*)")); + } + } + } } diff --git a/jsign-core/src/test/java/net/jsign/appx/APPXFileTest.java b/jsign-core/src/test/java/net/jsign/appx/APPXFileTest.java index cf1a2957..4401a711 100644 --- a/jsign-core/src/test/java/net/jsign/appx/APPXFileTest.java +++ b/jsign-core/src/test/java/net/jsign/appx/APPXFileTest.java @@ -96,4 +96,18 @@ public void testIsBundle() throws Exception { assertTrue("minimal.appxbundle is not a bundle", file.isBundle()); } } + + @Test + public void testGetPackagePublisher() throws Exception { + try (APPXFile file = new APPXFile(new File("target/test-classes/minimal.msix"))) { + assertEquals("Publisher", "CN=Jsign Code Signing Test Certificate 2022 (RSA)", file.getPublisher()); + } + } + + @Test + public void testGetBundlePublisher() throws Exception { + try (APPXFile file = new APPXFile(new File("target/test-classes/minimal.appxbundle"))) { + assertEquals("Publisher", "CN=Jsign Code Signing Test Certificate 2022 (RSA)", file.getPublisher()); + } + } }