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