Add some code to either transform everything or skip signed jars
diff --git a/tomee-patch-core/pom.xml b/tomee-patch-core/pom.xml index 0045487..e8f7082 100644 --- a/tomee-patch-core/pom.xml +++ b/tomee-patch-core/pom.xml
@@ -92,6 +92,12 @@ <version>4.10</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <version>1.69</version> + <scope>test</scope> + </dependency> </dependencies> </project>
diff --git a/tomee-patch-core/src/main/java/org/apache/tomee/patch/core/Jars.java b/tomee-patch-core/src/main/java/org/apache/tomee/patch/core/Jars.java new file mode 100644 index 0000000..73f31c9 --- /dev/null +++ b/tomee-patch-core/src/main/java/org/apache/tomee/patch/core/Jars.java
@@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomee.patch.core; + +import org.tomitribe.util.IO; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +public class Jars { + + public static boolean isSigned(final File jarFile) throws IOException { + // first approach is to look for signature files + if (!listJarContent(jarFile, "^META-INF/[^.]+.(SF|DSA|RSA)$").isEmpty()) { + return true; + } + + // parse the digest and see if we can find entries with a Digest suffix + if (hasDigest(jarFileContent(jarFile, "META-INF/MANIFEST.MF"))) { + return true; + } + + // we can also look for Digest entries in the manifest as a String if we can't parse it + return IO.slurp(jarFileContent(jarFile, "META-INF/MANIFEST.MF")).contains("-Digest"); + } + + public static List<String> listJarContent(final File jarFile) throws IOException { + return listJarContent(jarFile, null); + } + + public static List<String> listJarContent(final ZipInputStream zipInputStream) throws IOException { + return listJarContent(zipInputStream, null); + } + + public static List<String> listJarContent(final File jarFile, final String matchPattern) throws IOException { + return new ZipFile(jarFile).stream() + .filter(zipEntry -> matchPattern == null || zipEntry.getName().matches(matchPattern)) + .filter(Objects::nonNull) + .map(ZipEntry::getName) + .collect(Collectors.toList()); + } + + public static List<String> listJarContent(final ZipInputStream zipInputStream, final String matchPattern) throws IOException { + final List<String> result = new ArrayList<>(); + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + final String name = entry.getName(); + if (matchPattern != null && !name.matches(matchPattern)) { + continue; + } + result.add(name); + } + return result; + } + + public static InputStream jarFileContent(final ZipInputStream zipInputStream, final String name) throws IOException { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + final String entryName = entry.getName(); + if (entryName.contains(name)) { + return zipInputStream; + } + + } + return null; + } + + public static InputStream jarFileContent(final File jarFile, final String name) throws IOException { + return new ZipFile(jarFile).stream() + .filter(zipEntry -> zipEntry.getName().contains(name)) + .findFirst() + .map(zipEntry -> { + try { + return new ZipFile(jarFile).getInputStream(zipEntry); + + } catch (final IOException e) { + throw new RuntimeException(e); + } + }).orElse(null); + } + + public static boolean hasDigest(final InputStream inputStream) throws IOException { + if (inputStream == null) { + return false; + } + final Manifest man = new Manifest(inputStream); + for(Map.Entry<String, Attributes> entry: man.getEntries().entrySet()) { + for(Object attrkey: entry.getValue().keySet()) { + if (attrkey instanceof Attributes.Name && attrkey.toString().contains("-Digest")) + return true; + } + } + return false; + } +}
diff --git a/tomee-patch-core/src/main/java/org/apache/tomee/patch/core/Transformation.java b/tomee-patch-core/src/main/java/org/apache/tomee/patch/core/Transformation.java index 3cc2a39..ca22a9f 100644 --- a/tomee-patch-core/src/main/java/org/apache/tomee/patch/core/Transformation.java +++ b/tomee-patch-core/src/main/java/org/apache/tomee/patch/core/Transformation.java
@@ -24,11 +24,19 @@ import org.tomitribe.util.Mvn; import org.tomitribe.util.dir.Dir; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.Manifest; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.ZipEntry; @@ -46,6 +54,7 @@ private final Additions additions; private final Boolean skipTransform; private final File patchResources; + private final Boolean skipSigned; public Transformation() { this.log = new NullLog(); @@ -53,11 +62,15 @@ this.skips = new Skips(); this.additions = new Additions(); this.skipTransform = false; + this.skipSigned = false; this.patchResources = new File("does not exist"); } - public Transformation(final List<Clazz> classes, final File patchResources, final Replacements replacements, final Skips skips, final Additions additions, final Log log, final Boolean skipTransform) { + public Transformation(final List<Clazz> classes, final File patchResources, final Replacements replacements, + final Skips skips, final Additions additions, final Log log, final Boolean skipTransform, + final Boolean skipSigned) { + this.classes.addAll(classes); this.log = log; this.replacements = replacements == null ? new Replacements() : replacements; @@ -65,6 +78,7 @@ this.additions = additions == null ? new Additions() : additions; this.patchResources = patchResources; this.skipTransform = skipTransform; + this.skipSigned = skipSigned; } public static File transform(final File jar) throws IOException { @@ -84,6 +98,7 @@ } private void scanJar(final String name, final InputStream inputStream, final OutputStream outputStream) throws IOException { + { final String jar = new File(name).getName(); final String replacement = replacements.getJars().get(jar); @@ -112,7 +127,8 @@ // TODO: the name may be changed in transformation final String path = updatePath(oldEntry.getName()); - if (skip(path)) { + // skip all signature files .RSA, .DSA, .SF + if (!skipSigned && isSigned(path)) { IO.copy(zipInputStream, skipped); continue; } @@ -138,9 +154,9 @@ if (path.endsWith(".class")) { scanClass(zipInputStream, zipOutputStream); } else if (isZip(path)) { - if(isExcludedJar(path)){ + if(isExcludedJar(path)) { IO.copy(zipInputStream, zipOutputStream); - }else{ + } else { scanJar(path, zipInputStream, zipOutputStream); } } else if (copyUnmodified(path)) { @@ -243,7 +259,7 @@ * Skip signed jar public key files. We most definitely * have tampered with the jar. */ - private boolean skip(final String name) { + private boolean isSigned(final String name) { if (name.startsWith("META-INF/")) { if (name.endsWith(".SF")) return true; if (name.endsWith(".DSA")) return true; @@ -429,6 +445,30 @@ .get(); + // we remove signatures (.DSA, .RSA, .SF) but we also need to remove all digest entries + if (!skipSigned && path.endsWith("MANIFEST.MF")) { + final Manifest manifest = new Manifest(inputStream); + final Manifest transformedManifest = new Manifest(); + transformedManifest.getMainAttributes().putAll(manifest.getMainAttributes()); + + for (final Map.Entry<String, Attributes> e : manifest.getEntries().entrySet()) { + final Attributes attributes = e.getValue(); + final Attributes transformedAttributes = new Attributes(); + for (final Map.Entry<Object, Object> entry : attributes.entrySet()) { + if (!String.valueOf(entry.getKey()).contains("-Digest")) { + transformedAttributes.put(entry.getKey(), entry.getValue()); + } + } + if (!transformedAttributes.isEmpty()) { + transformedManifest.getEntries().put(e.getKey(), transformedAttributes); + } + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transformedManifest.write(baos); + inputStream = IO.read(baos.toByteArray()); + } + IO.copy(inputStream, outputStream); }
diff --git a/tomee-patch-core/src/test/java/org/apache/tomee/patch/core/ExcludeJarsTest.java b/tomee-patch-core/src/test/java/org/apache/tomee/patch/core/ExcludeJarsTest.java index 719ba97..7807cb7 100644 --- a/tomee-patch-core/src/test/java/org/apache/tomee/patch/core/ExcludeJarsTest.java +++ b/tomee-patch-core/src/test/java/org/apache/tomee/patch/core/ExcludeJarsTest.java
@@ -3,17 +3,26 @@ import org.junit.Before; import org.junit.Test; import org.tomitribe.util.Archive; +import org.tomitribe.util.IO; +import org.tomitribe.util.Mvn; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.util.*; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import static org.apache.tomee.patch.core.Jars.hasDigest; +import static org.apache.tomee.patch.core.Jars.jarFileContent; +import static org.apache.tomee.patch.core.Jars.listJarContent; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -28,19 +37,133 @@ } @Test + public void checkJarIsSigned() throws IOException { + final String coordinates = "org.bouncycastle:bcprov-jdk15on:jar:1.69"; + final File jarFile = Mvn.mvn(coordinates); + + assertTrue(listJarContent(jarFile).size() > 10); // BC Prov has much more + + assertTrue(listJarContent(jarFile, "^META-INF/[^.]+.(SF|DSA|RSA)$").size() > 1); + + System.out.println(IO.slurp(jarFileContent(jarFile, "META-INF/MANIFEST.MF"))); + + assertTrue(IO.slurp(jarFileContent(jarFile, "META-INF/MANIFEST.MF")).contains("-Digest")); + + // This call will throw a java.lang.SecurityException if someone has tampered + // with the signature of _any_ element of the JAR file. + // Alas, it will proceed without a problem if the JAR file is not signed at all + final JarFile jar = new JarFile(jarFile); + try (final InputStream is = jar.getInputStream(jar.getEntry("META-INF/MANIFEST.MF"))) { + assertTrue(hasDigest(is)); + } + + assertTrue(Jars.isSigned(jarFile)); + } + + @Test + public void isTransformedNotSkippedForSignedJars() throws IOException { + final String coordinates = "org.bouncycastle:bcprov-jdk15on:jar:1.69"; + final String jarName = "bcprov-jdk15on-1.69.jar"; + final File realRepoJar = Mvn.mvn(coordinates); + final File tempFile = Files.createTempFile("bc", "temp").toFile(); + IO.copy(realRepoJar, tempFile); + + final File zipFile = Archive.archive() + .add("README.txt", "hi") + .add(jarName, tempFile).toJar(); + + assertTrue(listJarContent(tempFile).size() > 10); // BC Prov has much more + + assertTrue(listJarContent(tempFile, "^META-INF/[^.]+.(SF|DSA|RSA)$").size() > 1); + + // System.out.println(IO.slurp(jarFileContent(jarFile, "META-INF/MANIFEST.MF"))); + + assertTrue(IO.slurp(jarFileContent(tempFile, "META-INF/MANIFEST.MF")).contains("-Digest")); + + // This call will throw a java.lang.SecurityException if someone has tampered + // with the signature of _any_ element of the JAR file. + // Alas, it will proceed without a problem if the JAR file is not signed at all + final JarFile jar = new JarFile(tempFile); + try (final InputStream is = jar.getInputStream(jar.getEntry("META-INF/MANIFEST.MF"))) { + assertTrue(hasDigest(is)); + } + + assertTrue(Jars.isSigned(tempFile)); + + Transformation transformation = new Transformation(new ArrayList<Clazz>(), new File("does not exist"), + null, null, null, new NullLog(), + false, true); + final File transformedZip = transformation.transformArchive(zipFile); + + // With skipSigned to true, we'll skip signed jars so they remain valid + assertTrue(obtainJarContent(transformedZip).contains("META-INF/BC1024KE.SF")); + assertTrue(obtainJarContent(transformedZip).contains("META-INF/BC1024KE.DSA")); + + assertValidSignedJar(jarName, transformedZip); + } + + private void assertValidSignedJar(final String jarName, final File transformedZip) throws IOException { + final ZipFile zip = new ZipFile(transformedZip); + final InputStream inputStream = zip.getInputStream(new ZipEntry(jarName)); + final File tempTransformedJar = Files.createTempFile("bc", "temp").toFile(); + IO.copy(inputStream, tempTransformedJar); + final JarFile jarFile = new JarFile(tempTransformedJar); + + final Manifest manifest = jarFile.getManifest(); + System.out.println(manifest.toString()); + + // This call will throw a java.lang.SecurityException if someone has tampered + // with the signature of _any_ element of the JAR file. + // Alas, it will proceed without a problem if the JAR file is not signed at all + try (final InputStream is = jarFile.getInputStream(jarFile.getEntry("META-INF/MANIFEST.MF"))) { + System.out.println(IO.slurp(is)); + } + } + + @Test + public void isTransformedSkippedForSignedJars() throws IOException { + final String coordinates = "org.bouncycastle:bcprov-jdk15on:jar:1.69"; + final String jarName = "bcprov-jdk15on-1.69.jar"; + final File realRepoJar = Mvn.mvn(coordinates); + final File tempFile = Files.createTempFile("bc", "temp").toFile(); + IO.copy(realRepoJar, tempFile); + + final File zipFile = Archive.archive() + .add("README.txt", "hi") + .add(jarName, tempFile).toJar(); + + assertTrue(Jars.isSigned(tempFile)); + + Transformation transformation = new Transformation(new ArrayList<Clazz>(), new File("does not exist"), + null, null, null, new NullLog(), + false, false); + final File transformedZip = transformation.transformArchive(zipFile); + + // when skipSigned is false, we'll try to transform them, BUT ... + // we need to remove the signature jars, we need to also filter the manifest to remove Digest entries + assertFalse(obtainJarContent(transformedZip).contains("META-INF/BC1024KE.SF")); + assertFalse(obtainJarContent(transformedZip).contains("META-INF/BC1024KE.DSA")); + + assertValidSignedJar(jarName, transformedZip); + } + + @Test public void transformWithJarExclusions() throws Exception { final String jarSignatureFileName = "META-INF/sigTest.DSA"; final String jarName = "bcprov-jdk15on-1.69.jar"; + final File testJar = Archive.archive() - .add(jarSignatureFileName, "DC143C") - .add("index.txt", "red,green,blue") - .toJar(); + .add(jarSignatureFileName, "DC143C") + .add("index.txt", "red,green,blue") + .toJar(); final File zipFile = Archive.archive() - .add("README.txt", "hi") - .add(jarName, testJar).toJar(); + .add("README.txt", "hi") + .add(jarName, testJar).toJar(); - Transformation transformation = new Transformation(new ArrayList<Clazz>(), new File("does not exist"),null, customSkips, null, new NullLog(), false); + Transformation transformation = new Transformation(new ArrayList<Clazz>(), new File("does not exist"), + null, customSkips, null, new NullLog(), + false, false); File transformedJar = transformation.transformArchive(zipFile); assertTrue(obtainJarContent(transformedJar).contains(jarSignatureFileName)); } @@ -51,15 +174,17 @@ final String jarName = "jdbc.jar"; final File testJar = Archive.archive() - .add(jarSignatureFileName, "DC143C") - .add("index.txt", "red,green,blue") - .toJar(); + .add(jarSignatureFileName, "DC143C") + .add("index.txt", "red,green,blue") + .toJar(); final File zipFile = Archive.archive() - .add("README.txt", "hi") - .add(jarName, testJar).toJar(); + .add("README.txt", "hi") + .add(jarName, testJar).toJar(); - Transformation transformation = new Transformation(new ArrayList<Clazz>(), new File("does not exist"),null, customSkips, null, new NullLog(), false); + Transformation transformation = new Transformation(new ArrayList<Clazz>(), new File("does not exist"), + null, customSkips, null, new NullLog(), + false, false); File transformedJar = transformation.transformArchive(zipFile); assertFalse(obtainJarContent(transformedJar).contains(jarSignatureFileName)); }
diff --git a/tomee-patch-plugin/src/main/java/org/apache/tomee/patch/plugin/PatchMojo.java b/tomee-patch-plugin/src/main/java/org/apache/tomee/patch/plugin/PatchMojo.java index 192e54a..3fcb85a 100644 --- a/tomee-patch-plugin/src/main/java/org/apache/tomee/patch/plugin/PatchMojo.java +++ b/tomee-patch-plugin/src/main/java/org/apache/tomee/patch/plugin/PatchMojo.java
@@ -187,6 +187,9 @@ private Boolean skipTransform; @Parameter(defaultValue = "false") + private Boolean skipSigned; + + @Parameter(defaultValue = "false") private Boolean transformSources; /** @@ -283,7 +286,7 @@ final List<Clazz> clazzes = classes(); - final Transformation transformation = new Transformation(clazzes, patchResourceDirectory, replace, skips, add, new MavenLog(getLog()), skipTransform); + final Transformation transformation = new Transformation(clazzes, patchResourceDirectory, replace, skips, add, new MavenLog(getLog()), skipTransform, skipSigned); for (final Artifact artifact : artifacts) { final File file = artifact.getFile(); getLog().debug("Patching " + file.getAbsolutePath());