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 Tony Chemit
42   * @since 1.0
43   */
44  public class JarSignerUtil
45  {
46  
47      private JarSignerUtil()
48      {
49          // static class
50      }
51  
52      /**
53       * Checks whether the specified file is a JAR file. For our purposes, a ZIP file is a ZIP stream with at least one
54       * entry.
55       *
56       * @param file The file to check, must not be <code>null</code>.
57       * @return <code>true</code> if the file looks like a ZIP file, <code>false</code> otherwise.
58       */
59      public static boolean isZipFile( final File file )
60      {
61          boolean result = false;
62  
63          try ( ZipInputStream zis = new ZipInputStream( new FileInputStream( file ) ) )
64          {
65              result = zis.getNextEntry() != null;
66          }
67          catch ( Exception e )
68          {
69              // ignore, will fail below
70          }
71  
72          return result;
73      }
74  
75      /**
76       * Removes any existing signatures from the specified JAR file. We will stream from the input JAR directly to the
77       * output JAR to retain as much metadata from the original JAR as possible.
78       *
79       * @param jarFile The JAR file to unsign, must not be <code>null</code>.
80       * @throws IOException when error occurs during processing the file
81       */
82      public static void unsignArchive( File jarFile )
83          throws IOException
84      {
85  
86          File unsignedFile = new File( jarFile.getAbsolutePath() + ".unsigned" );
87  
88          try ( ZipInputStream zis = new ZipInputStream( new BufferedInputStream( new FileInputStream( jarFile ) ) );
89                          ZipOutputStream zos =
90                              new ZipOutputStream( new BufferedOutputStream( new FileOutputStream( unsignedFile ) ) ) )
91          {
92              for ( ZipEntry ze = zis.getNextEntry(); ze != null; ze = zis.getNextEntry() )
93              {
94                  if ( isSignatureFile( ze.getName() ) )
95                  {
96                      continue;
97                  }
98  
99                  zos.putNextEntry( new ZipEntry( ze.getName() ) );
100 
101                 if ( isManifestFile( ze.getName() ) )
102                 {
103 
104                     // build a new manifest while removing all digest entries
105                     // see https://jira.codehaus.org/browse/MSHARED-314
106                     Manifest oldManifest = new Manifest( zis );
107                     Manifest newManifest = buildUnsignedManifest( oldManifest );
108                     newManifest.write( zos );
109 
110                     continue;
111                 }
112 
113                 IOUtil.copy( zis, zos );
114             }
115 
116         }
117 
118         FileUtils.rename( unsignedFile, jarFile );
119 
120     }
121 
122     /**
123      * Build a new manifest from the given one, removing any signing information inside it.
124      *
125      * This is done by removing any attributes containing some digest information.
126      * If an entry has then no more attributes, then it will not be written in the result manifest.
127      *
128      * @param manifest manifest to clean
129      * @return the build manifest with no digest attributes
130      * @since 1.3
131      */
132     protected static Manifest buildUnsignedManifest( Manifest manifest )
133     {
134         Manifest result = new Manifest( manifest );
135         result.getEntries().clear();
136 
137         for ( Map.Entry<String, Attributes> manifestEntry : manifest.getEntries().entrySet() )
138         {
139             Attributes oldAttributes = manifestEntry.getValue();
140             Attributes newAttributes = new Attributes();
141 
142             for ( Map.Entry<Object, Object> attributesEntry : oldAttributes.entrySet() )
143             {
144                 String attributeKey = String.valueOf( attributesEntry.getKey() );
145                 if ( !attributeKey.endsWith( "-Digest" ) )
146                 {
147                     // can add this attribute
148                     newAttributes.put( attributesEntry.getKey(), attributesEntry.getValue() );
149                 }
150             }
151 
152             if ( !newAttributes.isEmpty() )
153             {
154                 // can add this entry
155                 result.getEntries().put( manifestEntry.getKey(), newAttributes );
156             }
157         }
158 
159         return result;
160     }
161 
162     /**
163      * Scans an archive for existing signatures.
164      *
165      * @param jarFile The archive to scan, must not be <code>null</code>.
166      * @return <code>true</code>, if the archive contains at least one signature file; <code>false</code>, if the
167      *         archive does not contain any signature files.
168      * @throws IOException if scanning <code>jarFile</code> fails.
169      */
170     public static boolean isArchiveSigned( final File jarFile )
171         throws IOException
172     {
173         if ( jarFile == null )
174         {
175             throw new NullPointerException( "jarFile" );
176         }
177 
178         try ( ZipInputStream in = new ZipInputStream( new BufferedInputStream( new FileInputStream( jarFile ) ) ) )
179         {
180             boolean signed = false;
181 
182             for ( ZipEntry ze = in.getNextEntry(); ze != null; ze = in.getNextEntry() )
183             {
184                 if ( isSignatureFile( ze.getName() ) )
185                 {
186                     signed = true;
187                     break;
188                 }
189             }
190 
191             return signed;
192         }
193     }
194 
195     /**
196      * Checks whether the specified JAR file entry denotes a signature-related file, i.e. matches
197      * <code>META-INF/*.SF</code>, <code>META-INF/*.DSA</code>, <code>META-INF/*.RSA</code> or
198      * <code>META-INF/*.EC</code>.
199      *
200      * @param entryName The name of the JAR file entry to check, must not be <code>null</code>.
201      * @return <code>true</code> if the entry is related to a signature, <code>false</code> otherwise.
202      */
203     protected static boolean isSignatureFile( String entryName )
204     {
205         if ( entryName.regionMatches( true, 0, "META-INF", 0, 8 ) )
206         {
207             entryName = entryName.replace( '\\', '/' );
208 
209             if ( entryName.indexOf( '/' ) == 8 && entryName.lastIndexOf( '/' ) == 8 )
210             {
211                 return endsWithIgnoreCase( entryName, ".SF" ) || endsWithIgnoreCase( entryName, ".DSA" )
212                     || endsWithIgnoreCase( entryName, ".RSA" ) || endsWithIgnoreCase( entryName, ".EC" );
213             }
214         }
215         return false;
216     }
217 
218     protected static boolean isManifestFile( String entryName )
219     {
220         if ( entryName.regionMatches( true, 0, "META-INF", 0, 8 ) )
221         {
222             entryName = entryName.replace( '\\', '/' );
223 
224             if ( entryName.indexOf( '/' ) == 8 && entryName.lastIndexOf( '/' ) == 8 )
225             {
226                 return endsWithIgnoreCase( entryName, "/MANIFEST.MF" );
227             }
228         }
229         return false;
230     }
231 
232     private static boolean endsWithIgnoreCase( String str, String searchStr )
233     {
234         return str.regionMatches( true, str.length() - searchStr.length(), searchStr, 0, searchStr.length() );
235     }
236 }