From bcc68dece783fb47daa26cf8e2ff585f68c8d3f1 Mon Sep 17 00:00:00 2001 From: Maciej Gajek <61919032+MaciejG604@users.noreply.github.com> Date: Wed, 5 Apr 2023 13:37:44 +0200 Subject: [PATCH] Properly handle pgp keychains generated by Scala CLI (#1987) * Make config --create-pgp-key require password or explicit 'none' or 'random' * Merge password and password-value options * Update documentation regarding PGP * NIT: change option description * Remove warning about missing password in config, prevent secret values leaking to publish-conf.scala --- .../build/internal/util/WarningMessages.scala | 10 ++ .../scala/cli/commands/config/Config.scala | 40 +++++-- .../cli/commands/config/ConfigOptions.scala | 22 ++-- .../publish/checks/PgpSecretKeyCheck.scala | 51 +++++--- .../scala/cli/integration/ConfigTests.scala | 113 +++++++++++++----- .../cli/integration/PublishSetupTests.scala | 71 +++++++---- .../integration/PublishTestDefinitions.scala | 8 +- website/docs/commands/config.md | 11 +- website/docs/commands/misc/pgp.md | 2 + .../docs/commands/publishing/publish-setup.md | 6 +- website/docs/commands/publishing/publish.md | 21 ++-- website/docs/reference/cli-options.md | 16 ++- 12 files changed, 259 insertions(+), 112 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala b/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala index 7894e37ef0..2ecc332a7a 100644 --- a/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala +++ b/modules/build/src/main/scala/scala/build/internal/util/WarningMessages.scala @@ -19,4 +19,14 @@ object WarningMessages { def experimentalConfigKeyUsed(name: String): String = experimentalFeatureUsed(s"The '$name' configuration key") + + def rawValueNotWrittenToPublishFile( + rawValue: String, + valueName: String, + directiveName: String + ): String = + s"""The value of $valueName ${Console.BOLD}will not${Console.RESET} be written to a potentially public file! + |Provide it as an option to the publish subcommand with: + | $directiveName value:$rawValue + |""".stripMargin } diff --git a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala index 7571cf6e56..51a52ccbd6 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/config/Config.scala @@ -51,10 +51,15 @@ object Config extends ScalaCommand[ConfigOptions] { args.all match { case Seq() => if (options.createPgpKey) { - val coursierCache = options.coursier.coursierCache(logger.coursierLogger("")) - val secKeyEntry = Keys.pgpSecretKey - val secKeyPasswordEntry = Keys.pgpSecretKeyPassword - val pubKeyEntry = Keys.pgpPublicKey + if (options.pgpPassword.isEmpty) { + logger.error( + s"--pgp-password not specified, use 'none' to create an unprotected keychain or 'random' to generate a password" + ) + sys.exit(1) + } + val coursierCache = options.coursier.coursierCache(logger.coursierLogger("")) + val secKeyEntry = Keys.pgpSecretKey + val pubKeyEntry = Keys.pgpPublicKey val mail = options.email .filter(_.trim.nonEmpty) @@ -64,17 +69,23 @@ object Config extends ScalaCommand[ConfigOptions] { .orExit(logger) } .getOrElse { - System.err.println( - s"Error: --email ... not specified, and ${Keys.userEmail.fullName} not set (either is required to generate a PGP key)" + logger.error( + s"--email ... not specified, and ${Keys.userEmail.fullName} not set (either is required to generate a PGP key)" ) sys.exit(1) } - val password = ThrowawayPgpSecret.pgpPassPhrase() + val passwordOpt = if (options.pgpPassword.contains("none")) + None + else if (options.pgpPassword.contains("random")) + Some(ThrowawayPgpSecret.pgpPassPhrase()) + else + options.pgpPassword.map(scala.cli.signing.shared.Secret.apply) + val (pgpPublic, pgpSecret0) = ThrowawayPgpSecret.pgpSecret( mail, - Some(password), + passwordOpt, logger, coursierCache, () => @@ -88,11 +99,20 @@ object Config extends ScalaCommand[ConfigOptions] { val pgpSecretBase64 = pgpSecret0.map(Base64.getEncoder.encodeToString) db.set(secKeyEntry, PasswordOption.Value(pgpSecretBase64.toConfig)) - db.set(secKeyPasswordEntry, PasswordOption.Value(password.toConfig)) db.set(pubKeyEntry, PasswordOption.Value(pgpPublic.toConfig)) db.save(directories.dbPath.toNIO) .wrapConfigException .orExit(logger) + + logger.message("PGP keychains written to config") + if (options.pgpPassword.contains("random")) + passwordOpt.foreach { password => + println( + s"""Password: ${password.value} + |Don't lose it! + |""".stripMargin + ) + } } else { System.err.println("No argument passed") @@ -122,7 +142,7 @@ object Config extends ScalaCommand[ConfigOptions] { valueOpt match { case Some(value) => for (v <- value) - if (options.password && entry.isPasswordOption) + if (options.passwordValue && entry.isPasswordOption) PasswordOption.parse(v) match { case Left(err) => System.err.println(err) diff --git a/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala index b4a98c4de7..c35b51f1a0 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/config/ConfigOptions.scala @@ -27,20 +27,26 @@ final case class ConfigOptions( @Tag(tags.inShortHelp) dump: Boolean = false, @Group(HelpGroup.Config.toString) - @HelpMessage("Create PGP key in config") + @HelpMessage("Create PGP keychain in config") + @Tag(tags.inShortHelp) @Tag(tags.experimental) createPgpKey: Boolean = false, @Group(HelpGroup.Config.toString) - @HelpMessage("Email to use to create PGP key in config") + @HelpMessage("A password used to encode the private PGP keychain") @Tag(tags.experimental) - email: Option[String] = None, + @ValueDescription("YOUR_PASSWORD|random|none") + @ExtraName("passphrase") + pgpPassword: Option[String] = None, @Group(HelpGroup.Config.toString) - @HelpMessage("If the entry is a password, print the password value rather than how to get the password") - @Tag(tags.restricted) - @Tag(tags.inShortHelp) - password: Boolean = false, + @HelpMessage("Email used to create the PGP keychains in config") + @Tag(tags.experimental) + email: Option[String] = None, @Group(HelpGroup.Config.toString) - @HelpMessage("If the entry is a password, save the password value rather than how to get the password") + @HelpMessage( + """When accessing config's content print the password value rather than how to get the password + |When saving an entry in config save the password value rather than how to get the password + |e.g. print/save the value of environment variable ENV_VAR rather than "env:ENV_VAR" + |""".stripMargin) @Tag(tags.restricted) @Tag(tags.inShortHelp) passwordValue: Boolean = false, diff --git a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala index 82e016dff3..3ec456f69f 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/publish/checks/PgpSecretKeyCheck.scala @@ -11,6 +11,7 @@ import scala.build.EitherCps.{either, value} import scala.build.Logger import scala.build.Ops.* import scala.build.errors.{BuildException, CompositeBuildException, MalformedCliInputError} +import scala.build.internal.util.WarningMessages import scala.build.options.publish.ConfigPasswordOption import scala.build.options.publish.ConfigPasswordOption.* import scala.build.options.PublishOptions as BPublishOptions @@ -321,8 +322,6 @@ final case class PgpSecretKeyCheck( if (keysFromConfig.publicKeyOpt.isEmpty) logger.message(" warning: no PGP public key found in config") - if (keysFromConfig.secretKeyPasswordOpt.isEmpty) - logger.message(" warning: no PGP secret key password found in config") (keysFromConfig, true) } @@ -392,34 +391,50 @@ final case class PgpSecretKeyCheck( scala.util.Try(path.relativeTo(os.pwd)) .map(p => s"file:${p.toString}") .getOrElse(optionValue) - } else optionValue case ConfigOption(fullName) => s"config:$fullName" } - val passwordDirectives = getDirectiveValue(setupKeys.secretKeyPasswordOpt).map { - "publish.secretKeyPassword" -> _ - }.toSeq - - val secretKeyDirValue = getDirectiveValue(setupKeys.secretKeyOpt) + val rawValueRegex = "^value:(.*)".r + + // Prevent potential leakage of a secret value + val passwordDirectives = getDirectiveValue(setupKeys.secretKeyPasswordOpt) + .flatMap { + case rawValueRegex(rawValue) => + logger.diagnostic( + WarningMessages.rawValueNotWrittenToPublishFile( + rawValue, + "PGP password", + "--secret-key-password" + ) + ) + None + case secretOption => Some("publish.secretKeyPassword" -> secretOption) + } + .toSeq + + // Prevent potential leakage of a secret value + val secretKeyDirValue = getDirectiveValue(setupKeys.secretKeyOpt).flatMap { + case rawValueRegex(rawValue) => + logger.diagnostic( + WarningMessages.rawValueNotWrittenToPublishFile( + rawValue, + "PGP secret key", + "--secret-key" + ) + ) + None + case secretOption => Some(secretOption) + } + // This is safe to be publicly available val publicKeyDirective = getDirectiveValue(setupKeys.publicKeyOpt).map { "publish.publicKey" -> _ }.toSeq val extraDirectives = passwordDirectives ++ publicKeyDirective - if (passwordDirectives.exists(_._2.startsWith("value:"))) - logger.diagnostic( - "The secret value of PGP private key password will be written to a potentially public file!" - ) - - if (secretKeyDirValue.exists(_.startsWith("value:"))) - logger.diagnostic( - "The secret value of PGP private key will be written to a potentially public file!" - ) - OptionCheck.DefaultValue( () => uploadKey(publicKeyOpt).map(_ => secretKeyDirValue), extraDirectives, diff --git a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala index e825542770..86c81a5d6e 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ConfigTests.scala @@ -67,7 +67,7 @@ class ConfigTests extends ScalaCliSuite { res.out.trim() } def readDecoded(env: Map[String, String] = Map.empty): String = { - val res = os.proc(TestUtil.cli, "--power", "config", key, "--password") + val res = os.proc(TestUtil.cli, "--power", "config", key, "--password-value") .call(cwd = root, env = configEnv ++ env) res.out.trim() } @@ -166,10 +166,12 @@ class ConfigTests extends ScalaCliSuite { } if (!TestUtil.isCI || !Properties.isWin) - test("Create a default PGP key") { - createDefaultPgpKeyTest() - } - def createDefaultPgpKeyTest(): Unit = { + for (pgpPasswordOption <- List("none", "random", "MY_CHOSEN_PASSWORD")) + test(s"Create a default PGP key, password: $pgpPasswordOption") { + createDefaultPgpKeyTest(pgpPasswordOption) + } + + def createDefaultPgpKeyTest(pgpPasswordOption: String): Unit = { TestInputs().fromRoot { root => val configFile = { val dir = root / "config" @@ -179,22 +181,57 @@ class ConfigTests extends ScalaCliSuite { val extraEnv = Map( "SCALA_CLI_CONFIG" -> configFile.toString ) - val checkRes = os.proc(TestUtil.cli, "--power", "config", "--create-pgp-key") + val checkPassword = os.proc(TestUtil.cli, "--power", "config", "--create-pgp-key") .call(cwd = root, env = extraEnv, check = false, mergeErrIntoOut = true) - expect(checkRes.exitCode != 0) - expect(checkRes.out.text().contains("--email")) - os.proc(TestUtil.cli, "--power", "config", "--create-pgp-key", "--email", "alex@alex.me") - .call(cwd = root, env = extraEnv, stdin = os.Inherit, stdout = os.Inherit) + expect(checkPassword.exitCode != 0) + expect(checkPassword.out.text().contains("--pgp-password")) + + val checkEmail = os.proc( + TestUtil.cli, + "--power", + "config", + "--create-pgp-key", + "--pgp-password", + pgpPasswordOption + ) + .call(cwd = root, env = extraEnv, check = false, mergeErrIntoOut = true) + expect(checkEmail.exitCode != 0) + expect(checkEmail.out.text().contains("--email")) + + val pgpCreated = os.proc( + TestUtil.cli, + "--power", + "config", + "--create-pgp-key", + "--email", + "alex@alex.me", + "--pgp-password", + pgpPasswordOption + ) + .call(cwd = root, env = extraEnv, mergeErrIntoOut = true) + + val pgpPasswordOpt: Option[String] = pgpCreated.out.text() + .linesIterator + .toSeq + .find(_.startsWith("Password")) + .map(_.stripPrefix("Password:").trim()) + + if (pgpPasswordOption != "random") + expect(pgpPasswordOpt.isEmpty) + else + expect(pgpPasswordOpt.isDefined) + + val passwordInConfig = os.proc(TestUtil.cli, "--power", "config", "pgp.secret-key-password") + .call(cwd = root, env = extraEnv, stderr = os.Pipe) + expect(passwordInConfig.out.text().isEmpty()) - val password = os.proc(TestUtil.cli, "--power", "config", "pgp.secret-key-password") - .call(cwd = root, env = extraEnv) - .out.trim() val secretKey = os.proc(TestUtil.cli, "--power", "config", "pgp.secret-key") - .call(cwd = root, env = extraEnv) - .out.trim() - val rawPublicKey = os.proc(TestUtil.cli, "--power", "config", "pgp.public-key", "--password") - .call(cwd = root, env = extraEnv) + .call(cwd = root, env = extraEnv, stderr = os.Pipe) .out.trim() + val rawPublicKey = + os.proc(TestUtil.cli, "--power", "config", "pgp.public-key", "--password-value") + .call(cwd = root, env = extraEnv, stderr = os.Pipe) + .out.trim() val tmpFile = root / "test-file" val tmpFileAsc = root / "test-file.asc" @@ -204,23 +241,37 @@ class ConfigTests extends ScalaCliSuite { def maybeEscape(arg: String): String = if (Properties.isWin) q + arg + q else arg - os.proc( - TestUtil.cli, - "--power", - "pgp", - "sign", - "--password", - maybeEscape(password), - "--secret-key", - maybeEscape(secretKey), - tmpFile - ) - .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = extraEnv) + val signProcess = if (pgpPasswordOption != "none") + os.proc( + TestUtil.cli, + "--power", + "pgp", + "sign", + "--password", + s"value:${maybeEscape(pgpPasswordOpt.getOrElse("MY_CHOSEN_PASSWORD"))}", + "--secret-key", + maybeEscape(secretKey), + tmpFile + ) + else + os.proc( + TestUtil.cli, + "--power", + "pgp", + "sign", + "--secret-key", + maybeEscape(secretKey), + tmpFile + ) + signProcess.call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = extraEnv) val pubKeyFile = root / "key.pub" os.write(pubKeyFile, rawPublicKey) - os.proc(TestUtil.cli, "--power", "pgp", "verify", "--key", pubKeyFile, tmpFileAsc) - .call(cwd = root, stdin = os.Inherit, stdout = os.Inherit, env = extraEnv) + val verifyResult = + os.proc(TestUtil.cli, "--power", "pgp", "verify", "--key", pubKeyFile, tmpFileAsc) + .call(cwd = root, env = extraEnv, mergeErrIntoOut = true) + + expect(verifyResult.out.text().contains("valid signature")) } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala b/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala index b52cb1f783..45c4840ebb 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PublishSetupTests.scala @@ -23,11 +23,11 @@ class PublishSetupTests extends ScalaCliSuite { private def configSetup(configFile: os.Path, root: os.Path): Unit = { val envs = Map("SCALA_CLI_CONFIG" -> configFile.toString) os.proc(TestUtil.cli, "--power", "config", "publish.user.name", devName) - .call(cwd = root, stdout = os.Inherit, env = envs) + .call(cwd = root, stdout = os.Inherit, env = envs, stderr = os.Pipe) os.proc(TestUtil.cli, "--power", "config", "publish.user.email", devMail) - .call(cwd = root, stdout = os.Inherit, env = envs) + .call(cwd = root, stdout = os.Inherit, env = envs, stderr = os.Pipe) os.proc(TestUtil.cli, "--power", "config", "publish.user.url", devUrl) - .call(cwd = root, stdout = os.Inherit, env = envs) + .call(cwd = root, stdout = os.Inherit, env = envs, stderr = os.Pipe) os.proc( TestUtil.cli, "--power", @@ -37,9 +37,9 @@ class PublishSetupTests extends ScalaCliSuite { "value:uSeR", "value:1234" ) - .call(cwd = root, stdout = os.Inherit, env = envs) - os.proc(TestUtil.cli, "--power", "config", "--create-pgp-key") - .call(cwd = root, stdout = os.Inherit, env = envs) + .call(cwd = root, stdout = os.Inherit, env = envs, stderr = os.Pipe) + os.proc(TestUtil.cli, "--power", "config", "--create-pgp-key", "--pgp-password", "random") + .call(cwd = root, stdout = os.Inherit, env = envs, stderr = os.Pipe) } private val projDir = os.rel / projName @@ -123,27 +123,25 @@ class PublishSetupTests extends ScalaCliSuite { test("CI") { val expectedDirectives = Map( - "publish.versionControl" -> List(s"github:$ghUserName/tests"), - "publish.organization" -> List(s"io.github.$ghUserName"), - "publish.developer" -> List(s"$devName|$devMail|$devUrl"), - "publish.name" -> List(projName), - "publish.license" -> List("Apache-2.0"), - "publish.url" -> List(s"https://github.com/$ghUserName/tests"), - "publish.ci.secretKey" -> List("env:PUBLISH_SECRET_KEY"), - "publish.ci.user" -> List("env:PUBLISH_USER"), - "publish.ci.password" -> List("env:PUBLISH_PASSWORD"), - "publish.ci.secretKeyPassword" -> List("env:PUBLISH_SECRET_KEY_PASSWORD"), - "publish.ci.publicKey" -> List("env:PUBLISH_PUBLIC_KEY"), - "publish.ci.repository" -> List("central-s01"), - "publish.ci.computeVersion" -> List("git:tag") + "publish.versionControl" -> List(s"github:$ghUserName/tests"), + "publish.organization" -> List(s"io.github.$ghUserName"), + "publish.developer" -> List(s"$devName|$devMail|$devUrl"), + "publish.name" -> List(projName), + "publish.license" -> List("Apache-2.0"), + "publish.url" -> List(s"https://github.com/$ghUserName/tests"), + "publish.ci.secretKey" -> List("env:PUBLISH_SECRET_KEY"), + "publish.ci.user" -> List("env:PUBLISH_USER"), + "publish.ci.password" -> List("env:PUBLISH_PASSWORD"), + "publish.ci.publicKey" -> List("env:PUBLISH_PUBLIC_KEY"), + "publish.ci.repository" -> List("central-s01"), + "publish.ci.computeVersion" -> List("git:tag") ) val expectedGhSecrets = Set( "PUBLISH_USER", "PUBLISH_PASSWORD", "PUBLISH_SECRET_KEY", - "PUBLISH_PUBLIC_KEY", - "PUBLISH_SECRET_KEY_PASSWORD" + "PUBLISH_PUBLIC_KEY" ) testInputs.fromRoot { root => configSetup(root / configFile, root) @@ -441,7 +439,19 @@ class PublishSetupTests extends ScalaCliSuite { } } - test("local secret value written") { + test("local secret value NOT written") { + val expectedDirectives = Map( + "publish.versionControl" -> Seq(s"github:$ghUserName/tests"), + "publish.organization" -> Seq(s"io.github.$ghUserName"), + "publish.developer" -> Seq(s"$devName|$devMail|$devUrl"), + "publish.repository" -> Seq("central-s01"), + "publish.url" -> Seq(s"https://github.com/$ghUserName/tests"), + "publish.name" -> Seq(projName), + "publish.computeVersion" -> Seq("git:tag"), + "publish.license" -> Seq("Apache-2.0") + // SHOULD NOT BE HERE "publish.secretKey" -> Seq("value:whatever") + ) + testInputs.fromRoot { root => configSetup(root / configFile, root) gitInit(root / projDir) @@ -459,11 +469,22 @@ class PublishSetupTests extends ScalaCliSuite { cwd = root, mergeErrIntoOut = true, env = envs - ) + ).out.text() - expect(res.out.text().contains( - "The secret value of PGP private key will be written to a potentially public file!" + expect(res.contains( + s"The value of PGP secret key ${Console.BOLD}will not${Console.RESET} be written to a potentially public file!" )) + + val ghSecrets = res + .linesIterator + .filter(_.startsWith("Would have set GitHub secret ")) + .map(_.stripPrefix("Would have set GitHub secret ")) + .toSet + val directives0 = directives(os.read(root / projDir / "publish-conf.scala")) + expect(directives0 == expectedDirectives) + expect(ghSecrets.isEmpty) + expect(!res.contains("found keys in config")) + expect(res.contains("Warning: no public key passed, not checking")) } } diff --git a/modules/integration/src/test/scala/scala/cli/integration/PublishTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/PublishTestDefinitions.scala index cf391e351e..093ade5978 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/PublishTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/PublishTestDefinitions.scala @@ -381,7 +381,9 @@ abstract class PublishTestDefinitions(val scalaVersionOpt: Option[String]) "config", "--create-pgp-key", "--email", - "some_email" + "some_email", + "--pgp-password", + "none" ).call(cwd = root, env = extraEnv) os.proc( @@ -481,7 +483,9 @@ abstract class PublishTestDefinitions(val scalaVersionOpt: Option[String]) "config", "--create-pgp-key", "--email", - "some_email" + "some_email", + "--pgp-password", + "random" ).call(cwd = root, env = extraEnv) os.proc( diff --git a/website/docs/commands/config.md b/website/docs/commands/config.md index 1e33d77436..92b6b43357 100644 --- a/website/docs/commands/config.md +++ b/website/docs/commands/config.md @@ -73,7 +73,7 @@ scala-cli config --dump | jq . -Use `--password` to get the value of a password entry: +Use `--password-value` to get the value of a password entry: @@ -89,7 +89,7 @@ env:MY_GITHUB_TOKEN ```bash export MY_GITHUB_TOKEN=1234 -scala-cli --power config --password github.token +scala-cli --power config --password-value github.token ``` ```text @@ -102,10 +102,13 @@ Use `--create-pgp-key` to create a PGP key pair, protected by a randomly-generat be used by the `publish setup` sub-command: ```sh -scala-cli --power config --create-pgp-key --email "some_email" +scala-cli --power config --create-pgp-key --pgp-password MY_CHOSEN_PASSWORD --email "some_email" ``` -The `--email` option or `publish.user.email` has to be specified for this subcommand to work properly. +It's not mandatory, although recomended, to use a password to encrypt your keychains. +To store the private keychain in an unencrypted form use `--pgp-password none`. +To randomly generate a pasword, use `--pgp-password random` instead. +Also, the `--email` option or `publish.user.email` has to be specified for this subcommand to work properly. Configuration values are stored in a directory under your home directory, with restricted permissions: diff --git a/website/docs/commands/misc/pgp.md b/website/docs/commands/misc/pgp.md index 9e3776f203..734cfbbf69 100644 --- a/website/docs/commands/misc/pgp.md +++ b/website/docs/commands/misc/pgp.md @@ -27,6 +27,8 @@ to be handled using `gpg`. ## Create key pairs +It's not mandatory, although recomended, to use a password to encrypt your keychains. + ```text $ scala-cli pgp create --email alex@alex.me --password env:MY_PASSWORD Wrote public key e259e7e8a23475b3 to key.pub diff --git a/website/docs/commands/publishing/publish-setup.md b/website/docs/commands/publishing/publish-setup.md index 0328a4e6f8..756f141922 100644 --- a/website/docs/commands/publishing/publish-setup.md +++ b/website/docs/commands/publishing/publish-setup.md @@ -75,9 +75,13 @@ scala-cli --power config publish.user.email "" Generate a PGP key pair for publishing with ```bash -scala-cli --power config --create-pgp-key +scala-cli --power config --create-pgp-key --pgp-password MY_CHOSEN_PASSWORD ``` +It's not mandatory, although recomended, to use a password to encrypt your keychains. +To store the private keychain in an unencrypted form use `--pgp-password none`. +To randomly generate a pasword, use `--pgp-password random` instead. + This sets 3 entries in the Scala CLI configuration, that you can print with ```bash scala-cli --power config pgp.public-key diff --git a/website/docs/commands/publishing/publish.md b/website/docs/commands/publishing/publish.md index dd5ef5f26a..1621d897dc 100644 --- a/website/docs/commands/publishing/publish.md +++ b/website/docs/commands/publishing/publish.md @@ -128,28 +128,32 @@ handled by either A signing mechanism will be chosen based on options and directives specified, it can also be overriden with `--signer` with one of the values: - `bc` - Bouncy Castle library will be used for signing, PGP secret key is required -- `gpg` - a local `gpg` binary will be used for singing, GPG key ID is required -- `none` - NO singing will take place +- `gpg` - a local `gpg` binary will be used for signing, GPG key ID is required +- `none` - NO signing will take place #### Bouncy Castle -Bouncy Castle library is the recommended way of singing artifacts with Scala CLI. +Bouncy Castle library is the quickest way of signing artifacts with Scala CLI. A benefit of using it is that it has no external dependencies, Scala CLI is able to sign things with Bouncy Castle without further setup on your side. +However, it does not provide a complex PGP handling functionality as e.g. GPG does. When the `--signer` option is not specified Bouncy Castle library will be used for signing if one of these conditions occur: - the `--secret-key` option has been passed - target repository requires signing (e.g. `central`) -To succesfully use PGP signing with Bouncy Castle a secret key, possibly protected by a password is required. +To succesfully use PGP signing with Bouncy Castle a PGP key pair is required. Scala CLI can generate and keep PGP keys for you by using: ```bash ignore -scala-cli --power config --create-pgp-key +scala-cli --power config --create-pgp-key --pgp-password MY_CHOSEN_PASSWORD ``` -This generates a public key and password protected private key, all values are kept in config -and will be used by default unless specified otherwise: +It's not mandatory, although recomended, to use a password to encrypt your keychains. +To store the private keychain in an unencrypted form use `--pgp-password none`. +To randomly generate a pasword, use `--pgp-password random` instead. + +The generated values are kept in the `config` and will be used by default unless specified otherwise: - with directives: ```scala //> using publish.secretKey env:PGP_SECRET @@ -172,6 +176,9 @@ Using GPG to sign artifacts requires the `gpg` binary to be installed on your sy A benefit of using `gpg` to sign artifacts over Bouncy Castle is: you can use keys from your GPG key ring, or from external devices that GPG may support. +To get started, consult the [documentation on the library's website](https://gnupg.org/documentation/guides.html) and be sure to read about +[Protecting code integrity with PGP guide from the Linux Foundation](https://github.com/lfit/itpol/blob/master/protecting-code-integrity.md#target-audience). + To enable signing with GPG, pass `--gpg-key *key_id*` on the command line or specify it with a `using` directive: `//>using publish.gpgKey "key_id"`. If needed, you can specify arguments meant to be passed to `gpg`, diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index e20c8d84c6..d499b1429c 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -167,19 +167,23 @@ Dump config DB as JSON ### `--create-pgp-key` -Create PGP key in config +Create PGP keychain in config -### `--email` +### `--pgp-password` -Email to use to create PGP key in config +Aliases: `--passphrase` -### `--password` +A password used to encode the private PGP keychain + +### `--email` -If the entry is a password, print the password value rather than how to get the password +Email used to create the PGP keychains in config ### `--password-value` -If the entry is a password, save the password value rather than how to get the password +When accessing config's content print the password value rather than how to get the password +When saving an entry in config save the password value rather than how to get the password +e.g. print/save the value of environment variable ENV_VAR rather than "env:ENV_VAR" ### `--unset`