001/** 002 * Copyright 2017 Emmanuel Bourg 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package net.jsign; 018 019import java.io.File; 020 021import org.apache.maven.plugin.AbstractMojo; 022import org.apache.maven.plugin.MojoExecutionException; 023import org.apache.maven.plugin.MojoFailureException; 024import org.apache.maven.plugins.annotations.Component; 025import org.apache.maven.plugins.annotations.LifecyclePhase; 026import org.apache.maven.plugins.annotations.Mojo; 027import org.apache.maven.plugins.annotations.Parameter; 028import org.apache.maven.project.MavenProject; 029import org.apache.maven.settings.Proxy; 030import org.apache.maven.settings.Server; 031import org.apache.maven.settings.Settings; 032import org.apache.maven.shared.model.fileset.FileSet; 033import org.apache.maven.shared.model.fileset.util.FileSetManager; 034import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; 035import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException; 036 037/** 038 * Maven plugin for signing files with Authenticode. 039 * 040 * @author Emmanuel Bourg 041 * @since 2.0 042 */ 043@Mojo(name = "sign", defaultPhase = LifecyclePhase.PACKAGE) 044public class JsignMojo extends AbstractMojo { 045 046 /** The file to be signed. Use {@link #fileset} to sign multiple files using the same certificate. */ 047 @Parameter 048 private File file; 049 050 /** The set of files to be signed. */ 051 @Parameter 052 private FileSet fileset; 053 054 /** The program name embedded in the signature. */ 055 @Parameter( property = "jsign.name" ) 056 private String name; 057 058 /** The program URL embedded in the signature. */ 059 @Parameter( property = "jsign.url" ) 060 private String url; 061 062 /** The digest algorithm to use for the signature (SHA-1, SHA-256, SHA-384 or SHA-512). */ 063 @Parameter( property = "jsign.algorithm", defaultValue = "SHA-256" ) 064 private String algorithm; 065 066 /** 067 * The keystore file, the SunPKCS11 configuration file, the cloud keystore name, or the smart card or hardware 068 * token name. For file based keystores this parameter must be specified unless the keyfile and certfile parameters 069 * are already specified. For smart cards and hardware tokens, this parameter may be specified to distinguish 070 * between multiple connected devices. 071 */ 072 @Parameter( property = "jsign.keystore" ) 073 private String keystore; 074 075 /** 076 * The password to open the keystore. The password can be loaded from a file by using the <code>file:</code> prefix 077 * followed by the path of the file, from an environment variable by using the <code>env:</code> prefix followed 078 * by the name of the variable, or from the Maven settings file by using the <code>mvn:</code> prefix followed by 079 * the server id. 080 */ 081 @Parameter( property = "jsign.storepass" ) 082 private String storepass; 083 084 /** 085 * The type of the keystore (JKS, JCEKS, PKCS12, PKCS11, ETOKEN, NITROKEY, OPENPGP, OPENSC, PIV, YUBIKEY, AWS, 086 * AZUREKEYVAULT, DIGICERTONE, ESIGNER, GOOGLECLOUD or HASHICORPVAULT). 087 */ 088 @Parameter( property = "jsign.storetype" ) 089 private String storetype; 090 091 /** 092 * The alias of the certificate used for signing in the keystore. This parameter is mandatory if the keystore 093 * parameter is specified and if the keystore contains more than one alias. 094 */ 095 @Parameter( property = "jsign.alias" ) 096 private String alias; 097 098 /** 099 * The file containing the PKCS#7 certificate chain (.p7b or .spc files). 100 * This parameter is used in combination with the keyfile parameter. 101 */ 102 @Parameter( property = "jsign.certfile" ) 103 private File certfile; 104 105 /** 106 * The file containing the private key (PEM or PVK format). 107 * This parameter is used in combination with the certfile parameter. 108 */ 109 @Parameter( property = "jsign.keyfile" ) 110 private File keyfile; 111 112 /** 113 * The password of the private key. When using a keystore, this parameter can be omitted if the keystore shares 114 * the same password. The password can be loaded from a file by using the <code>file:</code> prefix followed by 115 * the path of the file, from an environment variable by using the <code>env:</code> prefix followed by the name 116 * of the variable, or from the Maven settings file by using the <code>mvn:</code> prefix followed by the server id. 117 */ 118 @Parameter( property = "jsign.keypass" ) 119 private String keypass; 120 121 /** 122 * The URL of the timestamping authority. 123 * Several URLs separated by a comma can be specified to fallback on alternative servers. 124 */ 125 @Parameter( property = "jsign.tsaurl" ) 126 private String tsaurl; 127 128 /** The protocol used for the timestamping (RFC3161 or Authenticode) */ 129 @Parameter( property = "jsign.tsmode", defaultValue = "Authenticode" ) 130 private String tsmode; 131 132 /** The number of retries for timestamping */ 133 @Parameter( property = "jsign.tsretries", defaultValue = "3") 134 private int tsretries = -1; 135 136 /** The number of seconds to wait between timestamping retries */ 137 @Parameter( property = "jsign.tsretrywait", defaultValue = "10") 138 private int tsretrywait = -1; 139 140 /** Tells if previous signatures should be replaced */ 141 @Parameter( property = "jsign.replace", defaultValue = "false") 142 private boolean replace; 143 144 /** The encoding of the script to be signed (UTF-8 by default). */ 145 @Parameter( property = "jsign.encoding", defaultValue = "UTF-8") 146 private String encoding = "UTF-8"; 147 148 /** 149 * Tells if a detached signature should be generated or reused. The detached signature is a file in the same 150 * directory using the name of the file signed with the <code>.sig</code> suffix added 151 * (for example <code>application.exe.sig</code>). 152 * 153 * <ul> 154 * <li>If the signature doesn't exist, the file is signed as usual and the detached signature is generated.</li> 155 * <li>If the signature exists it is attached to the file, replacing any existing signature (in this case 156 * the private key isn't used for signing and no timestamping is performed)</li> 157 * </ul> 158 */ 159 @Parameter( property = "jsign.detached", defaultValue = "false") 160 private boolean detached; 161 162 @Parameter(defaultValue = "${project}", required = true, readonly = true) 163 private MavenProject project; 164 165 @Parameter(defaultValue = "${settings}", readonly = true) 166 private Settings settings; 167 168 @Parameter( property = "jsign.proxyId" ) 169 private String proxyId; 170 171 /** Specifies whether the signing should be skipped. */ 172 @Parameter( property = "jsign.skip", defaultValue = "false" ) 173 protected boolean skip; 174 175 @Component(hint = "mng-4384") 176 private SecDispatcher securityDispatcher; 177 178 @Override 179 public void execute() throws MojoExecutionException, MojoFailureException { 180 if (skip) { 181 getLog().info("Skipping signing"); 182 return; 183 } 184 185 if (file == null && fileset == null) { 186 throw new MojoExecutionException("file of fileset must be set"); 187 } 188 189 SignerHelper helper = new SignerHelper(new MavenConsole(getLog()), "element"); 190 helper.setBaseDir(project.getBasedir()); 191 192 helper.name(name); 193 helper.url(url); 194 helper.alg(algorithm); 195 helper.keystore(keystore); 196 helper.storepass(decrypt(storepass)); 197 helper.storetype(storetype); 198 helper.alias(alias); 199 helper.certfile(certfile); 200 helper.keyfile(keyfile); 201 helper.keypass(decrypt(keypass)); 202 helper.tsaurl(tsaurl); 203 helper.tsmode(tsmode); 204 helper.tsretries(tsretries); 205 helper.tsretrywait(tsretrywait); 206 helper.replace(replace); 207 helper.encoding(encoding); 208 helper.detached(detached); 209 210 Proxy proxy = getProxyFromSettings(); 211 if (proxy != null) { 212 helper.proxyUrl(proxy.getProtocol() + "://" + proxy.getHost() + ":" + proxy.getPort()); 213 helper.proxyUser(proxy.getUsername()); 214 helper.proxyPass(proxy.getPassword()); 215 } 216 217 try { 218 if (file != null) { 219 helper.sign(file); 220 } 221 222 if (fileset != null) { 223 for (String filename : new FileSetManager().getIncludedFiles(fileset)) { 224 File file = new File(fileset.getDirectory(), filename); 225 helper.sign(file); 226 } 227 } 228 } catch (SignerException e) { 229 throw new MojoFailureException(e.getMessage(), e); 230 } 231 } 232 233 private Proxy getProxyFromSettings() throws MojoExecutionException { 234 if (settings == null) { 235 return null; 236 } 237 238 if (proxyId != null) { 239 for (Proxy proxy : settings.getProxies()) { 240 if (proxyId.equals(proxy.getId())) { 241 return proxy; 242 } 243 } 244 throw new MojoExecutionException("Configured proxy with id=" + proxyId + " not found"); 245 } 246 247 // Get active http/https proxy 248 for (Proxy proxy : settings.getProxies()) { 249 if (proxy.isActive() && ("http".equalsIgnoreCase(proxy.getProtocol()) || "https".equalsIgnoreCase(proxy.getProtocol()))) { 250 return proxy; 251 } 252 } 253 254 return null; 255 } 256 257 /** 258 * Decrypts a password using the Maven settings. The password specified can be either: 259 * <ul> 260 * <li>unencrypted</li> 261 * <li>encrypted with the Maven master password, Base64 encoded and enclosed in curly brackets (for example <code>{COQLCE6DU6GtcS5P=}</code>)</li> 262 * <li>a reference to a server in the settings.xml file prefixed with <code>mvn:</code> (for example <code>mvn:keystore</code>)</li> 263 * </ul 264 * 265 * @param encoded the password to be decrypted 266 * @return The decrypted password 267 */ 268 private String decrypt(String encoded) throws MojoExecutionException { 269 if (encoded == null) { 270 return null; 271 } 272 273 if (encoded.startsWith("mvn:")) { 274 String serverId = encoded.substring(4); 275 Server server = this.settings.getServer(serverId); 276 if (server == null) { 277 throw new MojoExecutionException("Server '" + serverId + "' not found in settings.xml"); 278 } 279 if (server.getPassword() != null) { 280 encoded = server.getPassword(); 281 } else if (server.getPassphrase() != null) { 282 encoded = server.getPassphrase(); 283 } else { 284 throw new MojoExecutionException("No password or passphrase found for server '" + serverId + "' in settings.xml"); 285 } 286 } 287 288 try { 289 return securityDispatcher.decrypt(encoded); 290 } catch (SecDispatcherException e) { 291 getLog().error("error using security dispatcher: " + e.getMessage(), e); 292 throw new MojoExecutionException("error using security dispatcher: " + e.getMessage(), e); 293 } 294 } 295}