View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.shared.jarsigner;
20  
21  import java.io.BufferedInputStream;
22  import java.io.BufferedOutputStream;
23  import java.io.File;
24  import java.io.IOException;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.util.Map;
28  import java.util.jar.Attributes;
29  import java.util.jar.Manifest;
30  import java.util.zip.ZipEntry;
31  import java.util.zip.ZipInputStream;
32  import java.util.zip.ZipOutputStream;
33  
34  import org.apache.commons.io.IOUtils;
35  
36  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
37  
38  /**
39   * Useful methods.
40   *
41   * @author Tony Chemit
42   * @since 1.0
43   */
44  public class JarSignerUtil {
45  
46      private JarSignerUtil() {
47          // static class
48      }
49  
50      /**
51       * Checks whether the specified file is a JAR file. For our purposes, a ZIP file is a ZIP stream with at least one
52       * entry.
53       *
54       * @param file The file to check, must not be <code>null</code>.
55       * @return <code>true</code> if the file looks like a ZIP file, <code>false</code> otherwise.
56       */
57      public static boolean isZipFile(final File file) {
58          boolean result = false;
59  
60          try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file.toPath()))) {
61              result = zis.getNextEntry() != null;
62          } catch (Exception e) {
63              // ignore, will fail below
64          }
65  
66          return result;
67      }
68  
69      /**
70       * Removes any existing signatures from the specified JAR file. We will stream from the input JAR directly to the
71       * output JAR to retain as much metadata from the original JAR as possible.
72       *
73       * @param jarFile The JAR file to unsign, must not be <code>null</code>.
74       * @throws IOException when error occurs during processing the file
75       */
76      public static void unsignArchive(File jarFile) throws IOException {
77  
78          Path unsignedPath = new File(jarFile.getAbsolutePath() + ".unsigned").toPath();
79  
80          try (ZipInputStream zis = new ZipInputStream(new BufferedInputStream(Files.newInputStream(jarFile.toPath())));
81                  ZipOutputStream zos =
82                          new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(unsignedPath)))) {
83              for (ZipEntry ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry()) {
84                  if (isSignatureFile(ze.getName())) {
85                      continue;
86                  }
87  
88                  zos.putNextEntry(new ZipEntry(ze.getName()));
89  
90                  if (isManifestFile(ze.getName())) {
91  
92                      // build a new manifest while removing all digest entries
93                      // see https://jira.codehaus.org/browse/MSHARED-314
94                      Manifest oldManifest = new Manifest(zis);
95                      Manifest newManifest = buildUnsignedManifest(oldManifest);
96                      newManifest.write(zos);
97  
98                      continue;
99                  }
100 
101                 IOUtils.copy(zis, zos);
102             }
103         }
104         Files.move(unsignedPath, jarFile.toPath(), REPLACE_EXISTING);
105     }
106 
107     /**
108      * Build a new manifest from the given one, removing any signing information inside it.
109      *
110      * This is done by removing any attributes containing some digest information.
111      * If an entry has then no more attributes, then it will not be written in the result manifest.
112      *
113      * @param manifest manifest to clean
114      * @return the build manifest with no digest attributes
115      * @since 1.3
116      */
117     protected static Manifest buildUnsignedManifest(Manifest manifest) {
118         Manifest result = new Manifest(manifest);
119         result.getEntries().clear();
120 
121         for (Map.Entry<String, Attributes> manifestEntry : manifest.getEntries().entrySet()) {
122             Attributes oldAttributes = manifestEntry.getValue();
123             Attributes newAttributes = new Attributes();
124 
125             for (Map.Entry<Object, Object> attributesEntry : oldAttributes.entrySet()) {
126                 String attributeKey = String.valueOf(attributesEntry.getKey());
127                 if (!attributeKey.endsWith("-Digest")) {
128                     // can add this attribute
129                     newAttributes.put(attributesEntry.getKey(), attributesEntry.getValue());
130                 }
131             }
132 
133             if (!newAttributes.isEmpty()) {
134                 // can add this entry
135                 result.getEntries().put(manifestEntry.getKey(), newAttributes);
136             }
137         }
138 
139         return result;
140     }
141 
142     /**
143      * Scans an archive for existing signatures.
144      *
145      * @param jarFile The archive to scan, must not be <code>null</code>.
146      * @return <code>true</code>, if the archive contains at least one signature file; <code>false</code>, if the
147      *         archive does not contain any signature files.
148      * @throws IOException if scanning <code>jarFile</code> fails.
149      */
150     public static boolean isArchiveSigned(final File jarFile) throws IOException {
151         if (jarFile == null) {
152             throw new NullPointerException("jarFile");
153         }
154 
155         try (ZipInputStream in = new ZipInputStream(new BufferedInputStream(Files.newInputStream(jarFile.toPath())))) {
156             boolean signed = false;
157 
158             for (ZipEntry ze = in.getNextEntry(); ze != null; ze = in.getNextEntry()) {
159                 if (isSignatureFile(ze.getName())) {
160                     signed = true;
161                     break;
162                 }
163             }
164 
165             return signed;
166         }
167     }
168 
169     /**
170      * Checks whether the specified JAR file entry denotes a signature-related file, i.e. matches
171      * <code>META-INF/*.SF</code>, <code>META-INF/*.DSA</code>, <code>META-INF/*.RSA</code> or
172      * <code>META-INF/*.EC</code>.
173      *
174      * @param entryName The name of the JAR file entry to check, must not be <code>null</code>.
175      * @return <code>true</code> if the entry is related to a signature, <code>false</code> otherwise.
176      */
177     protected static boolean isSignatureFile(String entryName) {
178         if (entryName.regionMatches(true, 0, "META-INF", 0, 8)) {
179             entryName = entryName.replace('\\', '/');
180 
181             if (entryName.indexOf('/') == 8 && entryName.lastIndexOf('/') == 8) {
182                 return endsWithIgnoreCase(entryName, ".SF")
183                         || endsWithIgnoreCase(entryName, ".DSA")
184                         || endsWithIgnoreCase(entryName, ".RSA")
185                         || endsWithIgnoreCase(entryName, ".EC");
186             }
187         }
188         return false;
189     }
190 
191     protected static boolean isManifestFile(String entryName) {
192         if (entryName.regionMatches(true, 0, "META-INF", 0, 8)) {
193             entryName = entryName.replace('\\', '/');
194 
195             if (entryName.indexOf('/') == 8 && entryName.lastIndexOf('/') == 8) {
196                 return endsWithIgnoreCase(entryName, "/MANIFEST.MF");
197             }
198         }
199         return false;
200     }
201 
202     private static boolean endsWithIgnoreCase(String str, String searchStr) {
203         return str.regionMatches(true, str.length() - searchStr.length(), searchStr, 0, searchStr.length());
204     }
205 }