001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.tools.ant.taskdefs;
020:
021: import java.io.File;
022: import java.io.IOException;
023: import java.util.Iterator;
024:
025: import org.apache.tools.ant.BuildException;
026: import org.apache.tools.ant.Project;
027: import org.apache.tools.ant.taskdefs.condition.IsSigned;
028: import org.apache.tools.ant.types.Path;
029: import org.apache.tools.ant.types.resources.FileResource;
030: import org.apache.tools.ant.util.FileUtils;
031: import org.apache.tools.ant.util.IdentityMapper;
032: import org.apache.tools.ant.util.FileNameMapper;
033:
034: /**
035: * Signs JAR or ZIP files with the javasign command line tool. The tool detailed
036: * dependency checking: files are only signed if they are not signed. The
037: * <tt>signjar</tt> attribute can point to the file to generate; if this file
038: * exists then its modification date is used as a cue as to whether to resign
039: * any JAR file.
040: *
041: * Timestamp driven signing is based on the unstable and inadequately documented
042: * information in the Java1.5 docs
043: * @see <a href="http://java.sun.com/j2se/1.5.0/docs/guide/security/time-of-signing-beta1.html">
044: * beta documentation</a>
045: * @ant.task category="java"
046: * @since Ant 1.1
047: */
048: public class SignJar extends AbstractJarSignerTask {
049: // CheckStyle:VisibilityModifier OFF - bc
050:
051: private static final FileUtils FILE_UTILS = FileUtils
052: .getFileUtils();
053:
054: /**
055: * name to a signature file
056: */
057: protected String sigfile;
058:
059: /**
060: * name of a single jar
061: */
062: protected File signedjar;
063:
064: /**
065: * flag for internal sf signing
066: */
067: protected boolean internalsf;
068:
069: /**
070: * sign sections only?
071: */
072: protected boolean sectionsonly;
073:
074: /**
075: * flag to preserve timestamp on modified files
076: */
077: private boolean preserveLastModified;
078:
079: /**
080: * Whether to assume a jar which has an appropriate .SF file in is already
081: * signed.
082: */
083: protected boolean lazy;
084:
085: /**
086: * the output directory when using paths.
087: */
088: protected File destDir;
089:
090: /**
091: * mapper for todir work
092: */
093: private FileNameMapper mapper;
094:
095: /**
096: * URL for a tsa; null implies no tsa support
097: */
098: protected String tsaurl;
099:
100: /**
101: * alias for the TSA in the keystore
102: */
103: protected String tsacert;
104:
105: /**
106: * error string for unit test verification: {@value}
107: */
108: public static final String ERROR_TODIR_AND_SIGNEDJAR = "'destdir' and 'signedjar' cannot both be set";
109: /**
110: * error string for unit test verification: {@value}
111: */
112: public static final String ERROR_TOO_MANY_MAPPERS = "Too many mappers";
113: /**
114: * error string for unit test verification {@value}
115: */
116: public static final String ERROR_SIGNEDJAR_AND_PATHS = "You cannot specify the signed JAR when using paths or filesets";
117: /**
118: * error string for unit test verification: {@value}
119: */
120: public static final String ERROR_BAD_MAP = "Cannot map source file to anything sensible: ";
121: /**
122: * error string for unit test verification: {@value}
123: */
124: public static final String ERROR_MAPPER_WITHOUT_DEST = "The destDir attribute is required if a mapper is set";
125: /**
126: * error string for unit test verification: {@value}
127: */
128: public static final String ERROR_NO_ALIAS = "alias attribute must be set";
129: /**
130: * error string for unit test verification: {@value}
131: */
132: public static final String ERROR_NO_STOREPASS = "storepass attribute must be set";
133:
134: // CheckStyle:VisibilityModifier ON
135:
136: /**
137: * name of .SF/.DSA file; optional
138: *
139: * @param sigfile the name of the .SF/.DSA file
140: */
141: public void setSigfile(final String sigfile) {
142: this .sigfile = sigfile;
143: }
144:
145: /**
146: * name of signed JAR file; optional
147: *
148: * @param signedjar the name of the signed jar file
149: */
150: public void setSignedjar(final File signedjar) {
151: this .signedjar = signedjar;
152: }
153:
154: /**
155: * Flag to include the .SF file inside the signature; optional; default
156: * false
157: *
158: * @param internalsf if true include the .SF file inside the signature
159: */
160: public void setInternalsf(final boolean internalsf) {
161: this .internalsf = internalsf;
162: }
163:
164: /**
165: * flag to compute hash of entire manifest; optional, default false
166: *
167: * @param sectionsonly flag to compute hash of entire manifest
168: */
169: public void setSectionsonly(final boolean sectionsonly) {
170: this .sectionsonly = sectionsonly;
171: }
172:
173: /**
174: * flag to control whether the presence of a signature file means a JAR is
175: * signed; optional, default false
176: *
177: * @param lazy flag to control whether the presence of a signature
178: */
179: public void setLazy(final boolean lazy) {
180: this .lazy = lazy;
181: }
182:
183: /**
184: * Optionally sets the output directory to be used.
185: *
186: * @param destDir the directory in which to place signed jars
187: * @since Ant 1.7
188: */
189: public void setDestDir(File destDir) {
190: this .destDir = destDir;
191: }
192:
193: /**
194: * add a mapper to determine file naming policy. Only used with toDir
195: * processing.
196: *
197: * @param newMapper the mapper to add.
198: * @since Ant 1.7
199: */
200: public void add(FileNameMapper newMapper) {
201: if (mapper != null) {
202: throw new BuildException(ERROR_TOO_MANY_MAPPERS);
203: }
204: mapper = newMapper;
205: }
206:
207: /**
208: * get the active mapper; may be null
209: * @return mapper or null
210: * @since Ant 1.7
211: */
212: public FileNameMapper getMapper() {
213: return mapper;
214: }
215:
216: /**
217: * get the -tsaurl url
218: * @return url or null
219: * @since Ant 1.7
220: */
221: public String getTsaurl() {
222: return tsaurl;
223: }
224:
225: /**
226: *
227: * @param tsaurl the tsa url.
228: * @since Ant 1.7
229: */
230: public void setTsaurl(String tsaurl) {
231: this .tsaurl = tsaurl;
232: }
233:
234: /**
235: * get the -tsacert option
236: * @since Ant 1.7
237: * @return a certificate alias or null
238: */
239: public String getTsacert() {
240: return tsacert;
241: }
242:
243: /**
244: * set the alias in the keystore of the TSA to use;
245: * @param tsacert the cert alias.
246: */
247: public void setTsacert(String tsacert) {
248: this .tsacert = tsacert;
249: }
250:
251: /**
252: * sign the jar(s)
253: *
254: * @throws BuildException on errors
255: */
256: public void execute() throws BuildException {
257: //validation logic
258: final boolean hasJar = jar != null;
259: final boolean hasSignedJar = signedjar != null;
260: final boolean hasDestDir = destDir != null;
261: final boolean hasMapper = mapper != null;
262:
263: if (!hasJar && !hasResources()) {
264: throw new BuildException(ERROR_NO_SOURCE);
265: }
266: if (null == alias) {
267: throw new BuildException(ERROR_NO_ALIAS);
268: }
269:
270: if (null == storepass) {
271: throw new BuildException(ERROR_NO_STOREPASS);
272: }
273:
274: if (hasDestDir && hasSignedJar) {
275: throw new BuildException(ERROR_TODIR_AND_SIGNEDJAR);
276: }
277:
278: if (hasResources() && hasSignedJar) {
279: throw new BuildException(ERROR_SIGNEDJAR_AND_PATHS);
280: }
281:
282: //this isnt strictly needed, but by being fussy now,
283: //we can change implementation details later
284: if (!hasDestDir && hasMapper) {
285: throw new BuildException(ERROR_MAPPER_WITHOUT_DEST);
286: }
287:
288: beginExecution();
289:
290: try {
291: //special case single jar handling with signedjar attribute set
292: if (hasJar && hasSignedJar) {
293: // single jar processing
294: signOneJar(jar, signedjar);
295: //return here.
296: return;
297: }
298:
299: //the rest of the method treats single jar like
300: //a nested path with one file
301:
302: Path sources = createUnifiedSourcePath();
303: //set up our mapping policy
304: FileNameMapper destMapper;
305: if (hasMapper) {
306: destMapper = mapper;
307: } else {
308: //no mapper? use the identity policy
309: destMapper = new IdentityMapper();
310: }
311:
312: //at this point the paths are set up with lists of files,
313: //and the mapper is ready to map from source dirs to dest files
314: //now we iterate through every JAR giving source and dest names
315: // deal with the paths
316: Iterator iter = sources.iterator();
317: while (iter.hasNext()) {
318: FileResource fr = (FileResource) iter.next();
319:
320: //calculate our destination directory; it is either the destDir
321: //attribute, or the base dir of the fileset (for in situ updates)
322: File toDir = hasDestDir ? destDir : fr.getBaseDir();
323:
324: //determine the destination filename via the mapper
325: String[] destFilenames = destMapper.mapFileName(fr
326: .getName());
327: if (destFilenames == null || destFilenames.length != 1) {
328: //we only like simple mappers.
329: throw new BuildException(ERROR_BAD_MAP
330: + fr.getFile());
331: }
332: File destFile = new File(toDir, destFilenames[0]);
333: signOneJar(fr.getFile(), destFile);
334: }
335: } finally {
336: endExecution();
337: }
338: }
339:
340: /**
341: * Sign one jar.
342: * <p/>
343: * The signing only takes place if {@link #isUpToDate(File, File)} indicates
344: * that it is needed.
345: *
346: * @param jarSource source to sign
347: * @param jarTarget target; may be null
348: * @throws BuildException
349: */
350: private void signOneJar(File jarSource, File jarTarget)
351: throws BuildException {
352:
353: File targetFile = jarTarget;
354: if (targetFile == null) {
355: targetFile = jarSource;
356: }
357: if (isUpToDate(jarSource, targetFile)) {
358: return;
359: }
360:
361: long lastModified = jarSource.lastModified();
362: final ExecTask cmd = createJarSigner();
363:
364: setCommonOptions(cmd);
365:
366: bindToKeystore(cmd);
367: if (null != sigfile) {
368: addValue(cmd, "-sigfile");
369: String value = this .sigfile;
370: addValue(cmd, value);
371: }
372:
373: //DO NOT SET THE -signedjar OPTION if source==dest
374: //unless you like fielding hotspot crash reports
375: if (null != targetFile && !jarSource.equals(targetFile)) {
376: addValue(cmd, "-signedjar");
377: addValue(cmd, targetFile.getPath());
378: }
379:
380: if (internalsf) {
381: addValue(cmd, "-internalsf");
382: }
383:
384: if (sectionsonly) {
385: addValue(cmd, "-sectionsonly");
386: }
387:
388: //add -tsa operations if declared
389: addTimestampAuthorityCommands(cmd);
390:
391: //JAR source is required
392: addValue(cmd, jarSource.getPath());
393:
394: //alias is required for signing
395: addValue(cmd, alias);
396:
397: log("Signing JAR: " + jarSource.getAbsolutePath() + " to "
398: + targetFile.getAbsolutePath() + " as " + alias);
399:
400: cmd.execute();
401:
402: // restore the lastModified attribute
403: if (preserveLastModified) {
404: targetFile.setLastModified(lastModified);
405: }
406: }
407:
408: /**
409: * If the tsa parameters are set, this passes them to the command.
410: * There is no validation of java version, as third party JDKs
411: * may implement this on earlier/later jarsigner implementations.
412: * @param cmd the exec task.
413: */
414: private void addTimestampAuthorityCommands(final ExecTask cmd) {
415: if (tsaurl != null) {
416: addValue(cmd, "-tsa");
417: addValue(cmd, tsaurl);
418: }
419: if (tsacert != null) {
420: addValue(cmd, "-tsacert");
421: addValue(cmd, tsacert);
422: }
423: }
424:
425: /**
426: * Compare a jar file with its corresponding signed jar. The logic for this
427: * is complex, and best explained in the source itself. Essentially if
428: * either file doesnt exist, or the destfile has an out of date timestamp,
429: * then the return value is false.
430: * <p/>
431: * If we are signing ourself, the check {@link #isSigned(File)} is used to
432: * trigger the process.
433: *
434: * @param jarFile the unsigned jar file
435: * @param signedjarFile the result signed jar file
436: * @return true if the signedjarFile is considered up to date
437: */
438: protected boolean isUpToDate(File jarFile, File signedjarFile) {
439: if (null == jarFile || !jarFile.exists()) {
440: //these are pathological cases, but retained in case somebody
441: //subclassed us.
442: return false;
443: }
444:
445: //we normally compare destination with source
446: File destFile = signedjarFile;
447: if (destFile == null) {
448: //but if no dest is specified, compare source to source
449: destFile = jarFile;
450: }
451:
452: //if, by any means, the destfile and source match,
453: if (jarFile.equals(destFile)) {
454: if (lazy) {
455: //we check the presence of signatures on lazy signing
456: return isSigned(jarFile);
457: }
458: //unsigned or non-lazy self signings are always false
459: return false;
460: }
461:
462: //if they are different, the timestamps are used
463: return FILE_UTILS.isUpToDate(jarFile, destFile);
464: }
465:
466: /**
467: * test for a file being signed, by looking for a signature in the META-INF
468: * directory with our alias.
469: *
470: * @param file the file to be checked
471: * @return true if the file is signed
472: * @see IsSigned#isSigned(File, String)
473: */
474: protected boolean isSigned(File file) {
475: try {
476: return IsSigned.isSigned(file, alias);
477: } catch (IOException e) {
478: //just log this
479: log(e.toString(), Project.MSG_VERBOSE);
480: return false;
481: }
482: }
483:
484: /**
485: * true to indicate that the signed jar modification date remains the same
486: * as the original. Defaults to false
487: *
488: * @param preserveLastModified if true preserve the last modified time
489: */
490: public void setPreserveLastModified(boolean preserveLastModified) {
491: this.preserveLastModified = preserveLastModified;
492: }
493: }
|