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 // === Deprecated stuff 262 263 /** 264 * Switch to improve plugin enforcement of "best practices". If set to {@code false}, plugin retains all the 265 * backward compatibility regarding getting secrets (but will warn). If set to {@code true}, plugin will fail 266 * if any "bad practices" regarding sensitive data handling are detected. By default, plugin remains backward 267 * compatible (this flag is {@code false}). Somewhere in the future, when this parameter enabling transitioning 268 * from older plugin versions is removed, the logic using this flag will be modified like it is set to {@code true}. 269 * It is warmly advised to configure this parameter to {@code true} and migrate project and user environment 270 * regarding how sensitive information is stored. 271 * 272 * @since 3.2.0 273 */ 274 @Parameter(property = "gpg.bestPractices", defaultValue = "false") 275 private boolean bestPractices; 276 277 /** 278 * Current user system settings for use in Maven. 279 * 280 * @since 1.6 281 */ 282 @Parameter(defaultValue = "${settings}", readonly = true, required = true) 283 private Settings settings; 284 285 /** 286 * Maven Security Dispatcher. 287 * 288 * @since 1.6 289 * @deprecated Provides quasi-encryption, should be avoided. 290 */ 291 @Deprecated 292 @Component 293 private SecDispatcher secDispatcher; 294 295 @Override 296 public final void execute() throws MojoExecutionException, MojoFailureException { 297 if (skip) { 298 // We're skipping the signing stuff 299 return; 300 } 301 if (bestPractices && (isNotBlank(passphrase) || isNotBlank(passphraseServerId))) { 302 // Stop propagating worst practices: passphrase MUST NOT be in any file on disk 303 throw new MojoFailureException( 304 "Do not store passphrase in any file (disk or SCM repository), rely on GnuPG agent or provide passphrase in " 305 + passphraseEnvName + " environment variable."); 306 } 307 308 doExecute(); 309 } 310 311 protected abstract void doExecute() throws MojoExecutionException, MojoFailureException; 312 313 private void logBestPracticeWarning(String source) { 314 getLog().warn(""); 315 getLog().warn("W A R N I N G"); 316 getLog().warn(""); 317 getLog().warn("Do not store passphrase in any file (disk or SCM repository),"); 318 getLog().warn("instead rely on GnuPG agent or provide passphrase in "); 319 getLog().warn(passphraseEnvName + " environment variable for batch mode."); 320 getLog().warn(""); 321 getLog().warn("Sensitive content loaded from " + source); 322 getLog().warn(""); 323 } 324 325 protected AbstractGpgSigner newSigner(MavenProject mavenProject) throws MojoFailureException { 326 AbstractGpgSigner signer; 327 if (GpgSigner.NAME.equals(this.signer)) { 328 signer = new GpgSigner(executable); 329 } else if (BcSigner.NAME.equals(this.signer)) { 330 signer = new BcSigner( 331 session.getRepositorySession(), 332 keyEnvName, 333 keyFingerprintEnvName, 334 agentSocketLocations, 335 keyFilePath, 336 keyFingerprint); 337 } else { 338 throw new MojoFailureException("Unknown signer: " + this.signer); 339 } 340 341 signer.setLog(getLog()); 342 signer.setInteractive(settings.isInteractiveMode()); 343 signer.setKeyName(keyname); 344 signer.setUseAgent(useAgent); 345 signer.setHomeDirectory(homedir); 346 signer.setDefaultKeyring(defaultKeyring); 347 signer.setSecretKeyring(secretKeyring); 348 signer.setPublicKeyring(publicKeyring); 349 signer.setLockMode(lockMode); 350 signer.setArgs(gpgArguments); 351 352 // "new way": env prevails 353 String passphrase = 354 (String) session.getRepositorySession().getConfigProperties().get("env." + passphraseEnvName); 355 if (isNotBlank(passphrase)) { 356 signer.setPassPhrase(passphrase); 357 } else if (!bestPractices) { 358 // "old way": mojo config 359 passphrase = this.passphrase; 360 if (isNotBlank(passphrase)) { 361 logBestPracticeWarning("Mojo configuration"); 362 signer.setPassPhrase(passphrase); 363 } else { 364 // "old way": serverId + settings 365 passphrase = loadGpgPassphrase(); 366 if (isNotBlank(passphrase)) { 367 logBestPracticeWarning("settings.xml"); 368 signer.setPassPhrase(passphrase); 369 } else { 370 // "old way": project properties 371 passphrase = getPassphrase(mavenProject); 372 if (isNotBlank(passphrase)) { 373 logBestPracticeWarning("Project properties"); 374 signer.setPassPhrase(passphrase); 375 } 376 } 377 } 378 } 379 signer.prepare(); 380 381 return signer; 382 } 383 384 private boolean isNotBlank(String string) { 385 return string != null && !string.trim().isEmpty(); 386 } 387 388 // Below is attic, to be thrown out 389 390 @Deprecated 391 private static final String GPG_PASSPHRASE = "gpg.passphrase"; 392 393 @Deprecated 394 private String loadGpgPassphrase() throws MojoFailureException { 395 if (isNotBlank(passphraseServerId)) { 396 Server server = settings.getServer(passphraseServerId); 397 if (server != null) { 398 if (isNotBlank(server.getPassphrase())) { 399 try { 400 return secDispatcher.decrypt(server.getPassphrase()); 401 } catch (SecDispatcherException e) { 402 throw new MojoFailureException("Unable to decrypt gpg passphrase", e); 403 } 404 } 405 } 406 } 407 return null; 408 } 409 410 @Deprecated 411 public String getPassphrase(MavenProject project) { 412 String pass = null; 413 if (project != null) { 414 pass = project.getProperties().getProperty(GPG_PASSPHRASE); 415 if (pass == null) { 416 MavenProject prj2 = findReactorProject(project); 417 pass = prj2.getProperties().getProperty(GPG_PASSPHRASE); 418 } 419 } 420 if (project != null && pass != null) { 421 findReactorProject(project).getProperties().setProperty(GPG_PASSPHRASE, pass); 422 } 423 return pass; 424 } 425 426 @Deprecated 427 private MavenProject findReactorProject(MavenProject prj) { 428 if (prj.getParent() != null 429 && prj.getParent().getBasedir() != null 430 && prj.getParent().getBasedir().exists()) { 431 return findReactorProject(prj.getParent()); 432 } 433 return prj; 434 } 435 }