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 }