| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| |
| package org.apache.tools.ant.taskdefs; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Iterator; |
| |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.taskdefs.condition.IsSigned; |
| import org.apache.tools.ant.types.Path; |
| import org.apache.tools.ant.types.resources.FileResource; |
| import org.apache.tools.ant.util.FileUtils; |
| import org.apache.tools.ant.util.IdentityMapper; |
| import org.apache.tools.ant.util.FileNameMapper; |
| |
| /** |
| * Signs JAR or ZIP files with the javasign command line tool. The tool detailed |
| * dependency checking: files are only signed if they are not signed. The |
| * <tt>signjar</tt> attribute can point to the file to generate; if this file |
| * exists then its modification date is used as a cue as to whether to resign |
| * any JAR file. |
| * |
| * Timestamp driven signing is based on the unstable and inadequately documented |
| * information in the Java1.5 docs |
| * @see <a href="http://java.sun.com/j2se/1.5.0/docs/guide/security/time-of-signing-beta1.html"> |
| * beta documentation</a> |
| * @ant.task category="java" |
| * @since Ant 1.1 |
| */ |
| public class SignJar extends AbstractJarSignerTask { |
| // CheckStyle:VisibilityModifier OFF - bc |
| |
| private static final FileUtils FILE_UTILS = FileUtils.getFileUtils(); |
| |
| /** |
| * name to a signature file |
| */ |
| protected String sigfile; |
| |
| /** |
| * name of a single jar |
| */ |
| protected File signedjar; |
| |
| /** |
| * flag for internal sf signing |
| */ |
| protected boolean internalsf; |
| |
| /** |
| * sign sections only? |
| */ |
| protected boolean sectionsonly; |
| |
| /** |
| * flag to preserve timestamp on modified files |
| */ |
| private boolean preserveLastModified; |
| |
| /** |
| * Whether to assume a jar which has an appropriate .SF file in is already |
| * signed. |
| */ |
| protected boolean lazy; |
| |
| /** |
| * the output directory when using paths. |
| */ |
| protected File destDir; |
| |
| /** |
| * mapper for todir work |
| */ |
| private FileNameMapper mapper; |
| |
| /** |
| * URL for a tsa; null implies no tsa support |
| */ |
| protected String tsaurl; |
| |
| /** |
| * alias for the TSA in the keystore |
| */ |
| protected String tsacert; |
| |
| /** |
| * error string for unit test verification: {@value} |
| */ |
| public static final String ERROR_TODIR_AND_SIGNEDJAR |
| = "'destdir' and 'signedjar' cannot both be set"; |
| /** |
| * error string for unit test verification: {@value} |
| */ |
| public static final String ERROR_TOO_MANY_MAPPERS = "Too many mappers"; |
| /** |
| * error string for unit test verification {@value} |
| */ |
| public static final String ERROR_SIGNEDJAR_AND_PATHS |
| = "You cannot specify the signed JAR when using paths or filesets"; |
| /** |
| * error string for unit test verification: {@value} |
| */ |
| public static final String ERROR_BAD_MAP = "Cannot map source file to anything sensible: "; |
| /** |
| * error string for unit test verification: {@value} |
| */ |
| public static final String ERROR_MAPPER_WITHOUT_DEST |
| = "The destDir attribute is required if a mapper is set"; |
| /** |
| * error string for unit test verification: {@value} |
| */ |
| public static final String ERROR_NO_ALIAS = "alias attribute must be set"; |
| /** |
| * error string for unit test verification: {@value} |
| */ |
| public static final String ERROR_NO_STOREPASS = "storepass attribute must be set"; |
| // CheckStyle:VisibilityModifier ON |
| |
| /** |
| * name of .SF/.DSA file; optional |
| * |
| * @param sigfile the name of the .SF/.DSA file |
| */ |
| public void setSigfile(final String sigfile) { |
| this.sigfile = sigfile; |
| } |
| |
| /** |
| * name of signed JAR file; optional |
| * |
| * @param signedjar the name of the signed jar file |
| */ |
| public void setSignedjar(final File signedjar) { |
| this.signedjar = signedjar; |
| } |
| |
| /** |
| * Flag to include the .SF file inside the signature; optional; default |
| * false |
| * |
| * @param internalsf if true include the .SF file inside the signature |
| */ |
| public void setInternalsf(final boolean internalsf) { |
| this.internalsf = internalsf; |
| } |
| |
| /** |
| * flag to compute hash of entire manifest; optional, default false |
| * |
| * @param sectionsonly flag to compute hash of entire manifest |
| */ |
| public void setSectionsonly(final boolean sectionsonly) { |
| this.sectionsonly = sectionsonly; |
| } |
| |
| /** |
| * flag to control whether the presence of a signature file means a JAR is |
| * signed; optional, default false |
| * |
| * @param lazy flag to control whether the presence of a signature |
| */ |
| public void setLazy(final boolean lazy) { |
| this.lazy = lazy; |
| } |
| |
| /** |
| * Optionally sets the output directory to be used. |
| * |
| * @param destDir the directory in which to place signed jars |
| * @since Ant 1.7 |
| */ |
| public void setDestDir(File destDir) { |
| this.destDir = destDir; |
| } |
| |
| |
| /** |
| * add a mapper to determine file naming policy. Only used with toDir |
| * processing. |
| * |
| * @param newMapper the mapper to add. |
| * @since Ant 1.7 |
| */ |
| public void add(FileNameMapper newMapper) { |
| if (mapper != null) { |
| throw new BuildException(ERROR_TOO_MANY_MAPPERS); |
| } |
| mapper = newMapper; |
| } |
| |
| /** |
| * get the active mapper; may be null |
| * @return mapper or null |
| * @since Ant 1.7 |
| */ |
| public FileNameMapper getMapper() { |
| return mapper; |
| } |
| |
| /** |
| * get the -tsaurl url |
| * @return url or null |
| * @since Ant 1.7 |
| */ |
| public String getTsaurl() { |
| return tsaurl; |
| } |
| |
| /** |
| * |
| * @param tsaurl the tsa url. |
| * @since Ant 1.7 |
| */ |
| public void setTsaurl(String tsaurl) { |
| this.tsaurl = tsaurl; |
| } |
| |
| /** |
| * get the -tsacert option |
| * @since Ant 1.7 |
| * @return a certificate alias or null |
| */ |
| public String getTsacert() { |
| return tsacert; |
| } |
| |
| /** |
| * set the alias in the keystore of the TSA to use; |
| * @param tsacert the cert alias. |
| */ |
| public void setTsacert(String tsacert) { |
| this.tsacert = tsacert; |
| } |
| |
| /** |
| * sign the jar(s) |
| * |
| * @throws BuildException on errors |
| */ |
| public void execute() throws BuildException { |
| //validation logic |
| final boolean hasJar = jar != null; |
| final boolean hasSignedJar = signedjar != null; |
| final boolean hasDestDir = destDir != null; |
| final boolean hasMapper = mapper != null; |
| |
| if (!hasJar && !hasResources()) { |
| throw new BuildException(ERROR_NO_SOURCE); |
| } |
| if (null == alias) { |
| throw new BuildException(ERROR_NO_ALIAS); |
| } |
| |
| if (null == storepass) { |
| throw new BuildException(ERROR_NO_STOREPASS); |
| } |
| |
| if (hasDestDir && hasSignedJar) { |
| throw new BuildException(ERROR_TODIR_AND_SIGNEDJAR); |
| } |
| |
| |
| if (hasResources() && hasSignedJar) { |
| throw new BuildException(ERROR_SIGNEDJAR_AND_PATHS); |
| } |
| |
| //this isnt strictly needed, but by being fussy now, |
| //we can change implementation details later |
| if (!hasDestDir && hasMapper) { |
| throw new BuildException(ERROR_MAPPER_WITHOUT_DEST); |
| } |
| |
| beginExecution(); |
| |
| |
| try { |
| //special case single jar handling with signedjar attribute set |
| if (hasJar && hasSignedJar) { |
| // single jar processing |
| signOneJar(jar, signedjar); |
| //return here. |
| return; |
| } |
| |
| //the rest of the method treats single jar like |
| //a nested path with one file |
| |
| Path sources = createUnifiedSourcePath(); |
| //set up our mapping policy |
| FileNameMapper destMapper; |
| if (hasMapper) { |
| destMapper = mapper; |
| } else { |
| //no mapper? use the identity policy |
| destMapper = new IdentityMapper(); |
| } |
| |
| |
| //at this point the paths are set up with lists of files, |
| //and the mapper is ready to map from source dirs to dest files |
| //now we iterate through every JAR giving source and dest names |
| // deal with the paths |
| Iterator iter = sources.iterator(); |
| while (iter.hasNext()) { |
| FileResource fr = (FileResource) iter.next(); |
| |
| //calculate our destination directory; it is either the destDir |
| //attribute, or the base dir of the fileset (for in situ updates) |
| File toDir = hasDestDir ? destDir : fr.getBaseDir(); |
| |
| //determine the destination filename via the mapper |
| String[] destFilenames = destMapper.mapFileName(fr.getName()); |
| if (destFilenames == null || destFilenames.length != 1) { |
| //we only like simple mappers. |
| throw new BuildException(ERROR_BAD_MAP + fr.getFile()); |
| } |
| File destFile = new File(toDir, destFilenames[0]); |
| signOneJar(fr.getFile(), destFile); |
| } |
| } finally { |
| endExecution(); |
| } |
| } |
| |
| /** |
| * Sign one jar. |
| * <p/> |
| * The signing only takes place if {@link #isUpToDate(File, File)} indicates |
| * that it is needed. |
| * |
| * @param jarSource source to sign |
| * @param jarTarget target; may be null |
| * @throws BuildException |
| */ |
| private void signOneJar(File jarSource, File jarTarget) |
| throws BuildException { |
| |
| |
| File targetFile = jarTarget; |
| if (targetFile == null) { |
| targetFile = jarSource; |
| } |
| if (isUpToDate(jarSource, targetFile)) { |
| return; |
| } |
| |
| long lastModified = jarSource.lastModified(); |
| final ExecTask cmd = createJarSigner(); |
| |
| setCommonOptions(cmd); |
| |
| bindToKeystore(cmd); |
| if (null != sigfile) { |
| addValue(cmd, "-sigfile"); |
| String value = this.sigfile; |
| addValue(cmd, value); |
| } |
| |
| //DO NOT SET THE -signedjar OPTION if source==dest |
| //unless you like fielding hotspot crash reports |
| if (!jarSource.equals(targetFile)) { |
| addValue(cmd, "-signedjar"); |
| addValue(cmd, targetFile.getPath()); |
| } |
| |
| if (internalsf) { |
| addValue(cmd, "-internalsf"); |
| } |
| |
| if (sectionsonly) { |
| addValue(cmd, "-sectionsonly"); |
| } |
| |
| //add -tsa operations if declared |
| addTimestampAuthorityCommands(cmd); |
| |
| //JAR source is required |
| addValue(cmd, jarSource.getPath()); |
| |
| //alias is required for signing |
| addValue(cmd, alias); |
| |
| log("Signing JAR: " |
| + jarSource.getAbsolutePath() |
| + " to " |
| + targetFile.getAbsolutePath() |
| + " as " + alias); |
| |
| cmd.execute(); |
| |
| // restore the lastModified attribute |
| if (preserveLastModified) { |
| targetFile.setLastModified(lastModified); |
| } |
| } |
| |
| /** |
| * If the tsa parameters are set, this passes them to the command. |
| * There is no validation of java version, as third party JDKs |
| * may implement this on earlier/later jarsigner implementations. |
| * @param cmd the exec task. |
| */ |
| private void addTimestampAuthorityCommands(final ExecTask cmd) { |
| if (tsaurl != null) { |
| addValue(cmd, "-tsa"); |
| addValue(cmd, tsaurl); |
| } |
| if (tsacert != null) { |
| addValue(cmd, "-tsacert"); |
| addValue(cmd, tsacert); |
| } |
| } |
| |
| /** |
| * Compare a jar file with its corresponding signed jar. The logic for this |
| * is complex, and best explained in the source itself. Essentially if |
| * either file doesnt exist, or the destfile has an out of date timestamp, |
| * then the return value is false. |
| * <p/> |
| * If we are signing ourself, the check {@link #isSigned(File)} is used to |
| * trigger the process. |
| * |
| * @param jarFile the unsigned jar file |
| * @param signedjarFile the result signed jar file |
| * @return true if the signedjarFile is considered up to date |
| */ |
| protected boolean isUpToDate(File jarFile, File signedjarFile) { |
| if (null == jarFile || !jarFile.exists()) { |
| //these are pathological cases, but retained in case somebody |
| //subclassed us. |
| return false; |
| } |
| |
| //we normally compare destination with source |
| File destFile = signedjarFile; |
| if (destFile == null) { |
| //but if no dest is specified, compare source to source |
| destFile = jarFile; |
| } |
| |
| //if, by any means, the destfile and source match, |
| if (jarFile.equals(destFile)) { |
| if (lazy) { |
| //we check the presence of signatures on lazy signing |
| return isSigned(jarFile); |
| } |
| //unsigned or non-lazy self signings are always false |
| return false; |
| } |
| |
| //if they are different, the timestamps are used |
| return FILE_UTILS.isUpToDate(jarFile, destFile); |
| } |
| |
| /** |
| * test for a file being signed, by looking for a signature in the META-INF |
| * directory with our alias. |
| * |
| * @param file the file to be checked |
| * @return true if the file is signed |
| * @see IsSigned#isSigned(File, String) |
| */ |
| protected boolean isSigned(File file) { |
| try { |
| return IsSigned.isSigned(file, alias); |
| } catch (IOException e) { |
| //just log this |
| log(e.toString(), Project.MSG_VERBOSE); |
| return false; |
| } |
| } |
| |
| /** |
| * true to indicate that the signed jar modification date remains the same |
| * as the original. Defaults to false |
| * |
| * @param preserveLastModified if true preserve the last modified time |
| */ |
| public void setPreserveLastModified(boolean preserveLastModified) { |
| this.preserveLastModified = preserveLastModified; |
| } |
| } |