blob: 4a30b6d584756e57e048c3236f1a9225306aea66 [file] [log] [blame]
/*
* 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;
}
}