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}