1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 package org.apache.maven.plugins.gpg; 20 21 import java.io.File; 22 import java.util.List; 23 24 import org.apache.maven.execution.MavenSession; 25 import org.apache.maven.plugin.AbstractMojo; 26 import org.apache.maven.plugin.MojoExecutionException; 27 import org.apache.maven.plugin.MojoFailureException; 28 import org.apache.maven.plugins.annotations.Component; 29 import org.apache.maven.plugins.annotations.Parameter; 30 import org.apache.maven.project.MavenProject; 31 import org.apache.maven.settings.Server; 32 import org.apache.maven.settings.Settings; 33 import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; 34 import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException; 35 36 /** 37 * @author Benjamin Bentmann 38 */ 39 public abstract class AbstractGpgMojo extends AbstractMojo { 40 public static final String DEFAULT_ENV_MAVEN_GPG_KEY = "MAVEN_GPG_KEY"; 41 public static final String DEFAULT_ENV_MAVEN_GPG_FINGERPRINT = "MAVEN_GPG_KEY_FINGERPRINT"; 42 public static final String DEFAULT_ENV_MAVEN_GPG_PASSPHRASE = "MAVEN_GPG_PASSPHRASE"; 43 44 /** 45 * BC Signer only: The comma separate list of Unix Domain Socket paths, to use to communicate with GnuPG agent. 46 * If relative, they are resolved against user home directory. 47 * 48 * @since 3.2.0 49 */ 50 @Parameter(property = "gpg.agentSocketLocations", defaultValue = ".gnupg/S.gpg-agent") 51 private String agentSocketLocations; 52 53 /** 54 * BC Signer only: The path of the exported key in 55 * <a href="https://openpgp.dev/book/private_keys.html#transferable-secret-key-format">TSK format</a>, 56 * and may be passphrase protected. If relative, the file is resolved against user home directory. 57 * <p> 58 * <em>Note: it is not recommended to have sensitive files checked into SCM repository. Key file should reside on 59 * developer workstation, outside of SCM tracked repository. For CI-like use cases you should set the 60 * key material as env variable instead.</em> 61 * 62 * @since 3.2.0 63 */ 64 @Parameter(property = "gpg.keyFilePath", defaultValue = "maven-signing-key.key") 65 private String keyFilePath; 66 67 /** 68 * BC Signer only: The fingerprint of the key to use for signing. If not given, first key in keyring will be used. 69 * 70 * @since 3.2.0 71 */ 72 @Parameter(property = "gpg.keyFingerprint") 73 private String keyFingerprint; 74 75 /** 76 * BC Signer only: The env variable name where the GnuPG key is set. 77 * To use BC Signer you must provide GnuPG key, as it does not use GnuPG home directory to extract/find the 78 * key (while it does use GnuPG Agent to ask for password in interactive mode). The key should be in 79 * <a href="https://openpgp.dev/book/private_keys.html#transferable-secret-key-format">TSK format</a> and may 80 * be passphrase protected. 81 * 82 * @since 3.2.0 83 */ 84 @Parameter(property = "gpg.keyEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_KEY) 85 private String keyEnvName; 86 87 /** 88 * BC Signer only: The env variable name where the GnuPG key fingerprint is set, if the provided keyring contains 89 * multiple keys. 90 * 91 * @since 3.2.0 92 */ 93 @Parameter(property = "gpg.keyFingerprintEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_FINGERPRINT) 94 private String keyFingerprintEnvName; 95 96 /** 97 * The env variable name where the GnuPG passphrase is set. This is the recommended way to pass passphrase 98 * for signing in batch mode execution of Maven. 99 * 100 * @since 3.2.0 101 */ 102 @Parameter(property = "gpg.passphraseEnvName", defaultValue = DEFAULT_ENV_MAVEN_GPG_PASSPHRASE) 103 private String passphraseEnvName; 104 105 /** 106 * GPG Signer only: The directory from which gpg will load keyrings. If not specified, gpg will use the value configured for its 107 * installation, e.g. <code>~/.gnupg</code> or <code>%APPDATA%/gnupg</code>. 108 * 109 * @since 1.0 110 */ 111 @Parameter(property = "gpg.homedir") 112 private File homedir; 113 114 /** 115 * The passphrase to use when signing. If not given, look up the value under Maven 116 * settings using server id at 'passphraseServerKey' configuration. <em>Do not use this parameter, it leaks 117 * sensitive data. Passphrase should be provided only via gpg-agent or via env variable. 118 * If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured.</em> 119 * 120 * @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env 121 * variables instead. 122 **/ 123 @Deprecated 124 @Parameter(property = GPG_PASSPHRASE) 125 private String passphrase; 126 127 /** 128 * Server id to lookup the passphrase under Maven settings. <em>Do not use this parameter, it leaks 129 * sensitive data. Passphrase should be provided only via gpg-agent or via env variable. 130 * If parameter {@link #bestPractices} set to {@code true}, plugin fails when this parameter is configured.</em> 131 * 132 * @since 1.6 133 * @deprecated Do not use this configuration, it may leak sensitive information. Rely on gpg-agent or env 134 * variables instead. 135 **/ 136 @Deprecated 137 @Parameter(property = "gpg.passphraseServerId", defaultValue = GPG_PASSPHRASE) 138 private String passphraseServerId; 139 140 /** 141 * GPG Signer only: The "name" of the key to sign with. Passed to gpg as <code>--local-user</code>. 142 */ 143 @Parameter(property = "gpg.keyname") 144 private String keyname; 145 146 /** 147 * All signers: whether gpg-agent is allowed to be used or not. If enabled, passphrase is optional, as agent may 148 * provide it. Have to be noted, that in "batch" mode, gpg-agent will be prevented to pop up pinentry 149 * dialogue, hence best is to "prime" the agent caches beforehand. 150 * <p> 151 * GPG Signer: Passes <code>--use-agent</code> or <code>--no-use-agent</code> option to gpg if it is version 2.1 152 * or older. Otherwise, will use an agent. In non-interactive mode gpg options are appended with 153 * <code>--pinentry-mode error</code>, preventing gpg agent to pop up pinentry dialogue. Agent will be able to 154 * hand over only cached passwords. 155 * <p> 156 * BC Signer: Allows signer to communicate with gpg agent. In non-interactive mode it uses 157 * <code>--no-ask</code> option with the <code>GET_PASSPHRASE</code> function. Agent will be able to hand over 158 * only cached passwords. 159 */ 160 @Parameter(property = "gpg.useagent", defaultValue = "true") 161 private boolean useAgent; 162 163 /** 164 * GPG Signer only: The path to the GnuPG executable to use for artifact signing. Defaults to either "gpg" or 165 * "gpg.exe" depending on the operating system. 166 * 167 * @since 1.1 168 */ 169 @Parameter(property = "gpg.executable") 170 private String executable; 171 172 /** 173 * GPG Signer only: Whether to add the default keyrings from gpg's home directory to the list of used keyrings. 174 * 175 * @since 1.2 176 */ 177 @Parameter(property = "gpg.defaultKeyring", defaultValue = "true") 178 private boolean defaultKeyring; 179 180 /** 181 * GPG Signer only: The path to a secret keyring to add to the list of keyrings. By default, only the 182 * {@code secring.gpg} from gpg's home directory is considered. Use this option (in combination with 183 * {@link #publicKeyring} and {@link #defaultKeyring} if required) to use a different secret key. 184 * <em>Note:</em> Relative paths are resolved against gpg's home directory, not the project base directory. 185 * <p> 186 * <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All secret keys are stored in the 187 * ‘private-keys-v1.d’ directory below the GnuPG home directory. 188 * 189 * @since 1.2 190 * @deprecated Obsolete option since GnuPG 2.1 version. 191 */ 192 @Deprecated 193 @Parameter(property = "gpg.secretKeyring") 194 private String secretKeyring; 195 196 /** 197 * GPG Signer only: The path to a public keyring to add to the list of keyrings. By default, only the 198 * {@code pubring.gpg} from gpg's home directory is considered. Use this option (and {@link #defaultKeyring} 199 * if required) to use a different public key. <em>Note:</em> Relative paths are resolved against gpg's home 200 * directory, not the project base directory. 201 * <p> 202 * <strong>NOTE: </strong>As of gpg 2.1 this is an obsolete option and ignored. All public keys are stored in the 203 * ‘pubring.kbx’ file below the GnuPG home directory. 204 * 205 * @since 1.2 206 * @deprecated Obsolete option since GnuPG 2.1 version. 207 */ 208 @Deprecated 209 @Parameter(property = "gpg.publicKeyring") 210 private String publicKeyring; 211 212 /** 213 * GPG Signer only: The lock mode to use when invoking gpg. By default no lock mode will be specified. Valid 214 * values are {@code once}, {@code multiple} and {@code never}. The lock mode gets translated into the 215 * corresponding {@code --lock-___} command line argument. Improper usage of this option may lead to data and 216 * key corruption. 217 * 218 * @see <a href="http://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration-Options.html">the 219 * --lock-options</a> 220 * @since 1.5 221 */ 222 @Parameter(property = "gpg.lockMode") 223 private String lockMode; 224 225 /** 226 * Skip doing the gpg signing. 227 */ 228 @Parameter(property = "gpg.skip", defaultValue = "false") 229 private boolean skip; 230 231 /** 232 * GPG Signer only: Sets the arguments to be passed to gpg. Example: 233 * 234 * <pre> 235 * <gpgArguments> 236 * <arg>--no-random-seed-file</arg> 237 * <arg>--no-permission-warning</arg> 238 * </gpgArguments> 239 * </pre> 240 * 241 * @since 1.5 242 */ 243 @Parameter 244 private List<String> gpgArguments; 245 246 /** 247 * The name of the Signer implementation to use. Accepted values are {@code "gpg"} (the default, uses GnuPG 248 * executable) and {@code "bc"} (uses Bouncy Castle pure Java signer). 249 * 250 * @since 3.2.0 251 */ 252 @Parameter(property = "gpg.signer", defaultValue = GpgSigner.NAME) 253 private String signer; 254 255 /** 256 * @since 3.0.0 257 */ 258 @Component 259 protected MavenSession session; 260 261 /** 262 * Switch to improve plugin enforcement of "best practices". If set to {@code false}, plugin retains all the 263 * backward compatibility regarding getting secrets (but will warn). If set to {@code true}, plugin will fail 264 * if any "bad practices" regarding sensitive data handling are detected. By default, plugin remains backward 265 * compatible (this flag is {@code false}). Somewhere in the future, when this parameter enabling transitioning 266 * from older plugin versions is removed, the logic using this flag will be modified like it is set to {@code true}. 267 * It is warmly advised to configure this parameter to {@code true} and migrate project and user environment 268 * regarding how sensitive information is stored. 269 * 270 * @since 3.2.0 271 */ 272 @Parameter(property = "gpg.bestPractices", defaultValue = "false") 273 private boolean bestPractices; 274 275 /** 276 * Current user system settings for use in Maven. 277 * 278 * @since 1.6 279 */ 280 @Parameter(defaultValue = "${settings}", readonly = true, required = true) 281 protected Settings settings; 282 283 /** 284 * Maven Security Dispatcher. 285 * 286 * @since 1.6 287 * @deprecated Provides quasi-encryption, should be avoided. 288 */ 289 @Deprecated 290 @Component 291 private SecDispatcher secDispatcher; 292 293 @Override 294 public final void execute() throws MojoExecutionException, MojoFailureException { 295 if (skip) { 296 // We're skipping the signing stuff 297 return; 298 } 299 if (bestPractices && (isNotBlank(passphrase) || isNotBlank(passphraseServerId))) { 300 // Stop propagating worst practices: passphrase MUST NOT be in any file on disk 301 throw new MojoFailureException( 302 "Do not store passphrase in any file (disk or SCM repository), rely on GnuPG agent or provide passphrase in " 303 + passphraseEnvName + " environment variable."); 304 } 305 306 doExecute(); 307 } 308 309 protected abstract void doExecute() throws MojoExecutionException, MojoFailureException; 310 311 private void logBestPracticeWarning(String source) { 312 getLog().warn(""); 313 getLog().warn("W A R N I N G"); 314 getLog().warn(""); 315 getLog().warn("Do not store passphrase in any file (disk or SCM repository),"); 316 getLog().warn("instead rely on GnuPG agent or provide passphrase in "); 317 getLog().warn(passphraseEnvName + " environment variable for batch mode."); 318 getLog().warn(""); 319 getLog().warn("Sensitive content loaded from " + source); 320 getLog().warn(""); 321 } 322 323 protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFailureException { 324 AbstractGpgSigner signer; 325 if (GpgSigner.NAME.equals(this.signer)) { 326 signer = new GpgSigner(executable); 327 } else if (BcSigner.NAME.equals(this.signer)) { 328 signer = new BcSigner( 329 session.getRepositorySession(), 330 keyEnvName, 331 keyFingerprintEnvName, 332 agentSocketLocations, 333 keyFilePath, 334 keyFingerprint); 335 } else { 336 throw new MojoFailureException("Unknown signer: " + this.signer); 337 } 338 339 signer.setLog(getLog()); 340 signer.setInteractive(settings.isInteractiveMode()); 341 signer.setKeyName(keyname); 342 signer.setUseAgent(useAgent); 343 signer.setHomeDirectory(homedir); 344 signer.setDefaultKeyring(defaultKeyring); 345 signer.setSecretKeyring(secretKeyring); 346 signer.setPublicKeyring(publicKeyring); 347 signer.setLockMode(lockMode); 348 signer.setArgs(gpgArguments); 349 350 // "new way": env prevails 351 String passphrase = 352 (String) session.getRepositorySession().getConfigProperties().get("env." + passphraseEnvName); 353 if (isNotBlank(passphrase)) { 354 signer.setPassPhrase(passphrase); 355 } else if (!bestPractices) { 356 // "old way": mojo config 357 passphrase = this.passphrase; 358 if (isNotBlank(passphrase)) { 359 logBestPracticeWarning("Mojo configuration"); 360 signer.setPassPhrase(passphrase); 361 } else { 362 // "old way": serverId + settings 363 passphrase = loadGpgPassphrase(); 364 if (isNotBlank(passphrase)) { 365 logBestPracticeWarning("settings.xml"); 366 signer.setPassPhrase(passphrase); 367 } else { 368 // "old way": project properties 369 passphrase = getPassphrase(mavenProject); 370 if (isNotBlank(passphrase)) { 371 logBestPracticeWarning("Project properties"); 372 signer.setPassPhrase(passphrase); 373 } 374 } 375 } 376 } 377 signer.prepare(); 378 379 return signer; 380 } 381 382 private boolean isNotBlank(String string) { 383 return string != null && !string.trim().isEmpty(); 384 } 385 386 // Below is attic, to be thrown out 387 388 @Deprecated 389 private static final String GPG_PASSPHRASE = "gpg.passphrase"; 390 391 @Deprecated 392 private String loadGpgPassphrase() throws MojoFailureException { 393 if (isNotBlank(passphraseServerId)) { 394 Server server = settings.getServer(passphraseServerId); 395 if (server != null) { 396 if (isNotBlank(server.getPassphrase())) { 397 try { 398 return secDispatcher.decrypt(server.getPassphrase()); 399 } catch (SecDispatcherException e) { 400 throw new MojoFailureException("Unable to decrypt gpg passphrase", e); 401 } 402 } 403 } 404 } 405 return null; 406 } 407 408 @Deprecated 409 public String getPassphrase(MavenProject project) { 410 String pass = null; 411 if (project != null) { 412 pass = project.getProperties().getProperty(GPG_PASSPHRASE); 413 if (pass == null) { 414 MavenProject prj2 = findReactorProject(project); 415 pass = prj2.getProperties().getProperty(GPG_PASSPHRASE); 416 } 417 } 418 if (project != null && pass != null) { 419 findReactorProject(project).getProperties().setProperty(GPG_PASSPHRASE, pass); 420 } 421 return pass; 422 } 423 424 @Deprecated 425 private MavenProject findReactorProject(MavenProject prj) { 426 if (prj.getParent() != null 427 && prj.getParent().getBasedir() != null 428 && prj.getParent().getBasedir().exists()) { 429 return findReactorProject(prj.getParent()); 430 } 431 return prj; 432 } 433 }