skip to content

You need to sign artifacts in CI using useInMemoryPgpKeys but the ASCII-armored key has newlines that your CI secret store mangles. How do you reliably get the key and passphrase into the Gradle build?

level: seniorimportance: should knowfreq 45%

answer

  1. armored block has significant newlines
  2. base64 -w0 → single-line secret
  3. decode in build before useInMemoryPgpKeys
  4. ORG_GRADLE_PROJECT_ env → project property
  5. guard block + never log secrets

basics

~10 s

Base64-encode the armored key so it's a single-line value, store it (and the passphrase) as a CI secret, then decode it in the build and pass both to useInMemoryPgpKeys(decodedKey, password).

solid answer

~40 s

The ASCII-armored secret key is a multi-line block, and many secret stores or env-var mechanisms either strip or corrupt newlines. Two robust patterns: 1. **Base64-encode the armored key** into one line: `gpg --armor --export-secret-keys KEYID | base64`. Store that and the passphrase as CI secrets. In the build, read the env var and `String(Base64.getDecoder().decode(...))` (or have CI decode into a Gradle property) before passing to `useInMemoryPgpKeys`. 2. Use a CI system that supports genuine **multiline secrets** (GitHub Actions secrets preserve newlines) and inject directly. Feed the values via env vars (`ORG_GRADLE_PROJECT_signingKey`) or `-P` properties — never commit them. Then: ```kotlin useInMemoryPgpKeys(signingKey, signingPassword) ``` Guard the whole signing block so local builds without the secret don't fail.

code

bash · 7 lines
bash
# Export and base64-encode the armored secret key for a single-line CI secret
gpg --armor --export-secret-keys 0xABCD1234 | base64 -w0 > signing_key.b64

# In CI, decode back into an env var Gradle can read:
export ORG_GRADLE_PROJECT_signingKey="$(echo "$SIGNING_KEY_B64" | base64 -d)"
export ORG_GRADLE_PROJECT_signingPassword="$SIGNING_PASSWORD"
./gradlew publish

go deeper

for a junior

Know that the key is a multi-line block and goes in as a secret, not committed to the repo.

for a middle

Explain base64-encoding the armored key to dodge newline issues and reading it from an env var into useInMemoryPgpKeys.

for a senior

Cover ORG_GRADLE_PROJECT_ mapping, env-vs--P leakage, guarding the block, and decode-in-build wiring end to end.

for a principal

Address secret governance: scoping signing secrets to release pipelines, key rotation, dedicated release subkeys, and standardizing this across repositories.

## The core problem `useInMemoryPgpKeys(secretKey, password)` expects the **ASCII-armored** secret key — a multi-line text block delimited by `-----BEGIN/END PGP PRIVATE KEY BLOCK-----`. Newlines are *significant*: they delimit the armor header, the base64 payload lines, and the checksum. If your transport (an env var, a CI UI field, a `.properties` line) collapses or escapes those newlines, the armored block becomes unparseable and signing fails with cryptic errors. ## Pattern A — base64 the armored key Make the multi-line block a single safe token: ```bash gpg --armor --export-secret-keys ABCD1234 | base64 -w0 # one long line ``` Store that single line as a CI secret (e.g. `SIGNING_KEY_B64`) and the passphrase as another. In the build, decode it back to the armored text: ```kotlin val armored = System.getenv("SIGNING_KEY_B64") ?.let { String(java.util.Base64.getDecoder().decode(it)) } ``` This sidesteps every newline-handling quirk because base64 is line-free (with `-w0`). ## Pattern B — rely on multiline-secret support GitHub Actions, GitLab CI, etc. preserve newlines in secrets. You can store the armored block verbatim and inject it. This avoids the encode/decode step but couples you to the CI provider's behavior; base64 is the portable fallback. ## Getting values into Gradle: the `ORG_GRADLE_PROJECT_` convention Gradle maps any env var named `ORG_GRADLE_PROJECT_<name>` to a project property `<name>`. So exporting `ORG_GRADLE_PROJECT_signingKey` and `ORG_GRADLE_PROJECT_signingPassword` makes `findProperty("signingKey")` work with no extra wiring — clean for CI. Alternatively pass `-PsigningKey=...` (avoid: leaks into process listings) or read `System.getenv` directly. ## Putting it together safely ```kotlin signing { val key = System.getenv("SIGNING_KEY") ?: findProperty("signingKey") as String? val pass = System.getenv("SIGNING_PASSWORD") ?: findProperty("signingPassword") as String? if (key != null && pass != null) { useInMemoryPgpKeys(key, pass) sign(publishing.publications) } } ``` Guarding the block means a contributor building locally without the secret doesn't hit a hard failure on every `build`. ## Security hygiene - Treat the armored secret key and passphrase as **secrets**: never echo them, never `println`, never commit to `gradle.properties` in VCS. - Prefer env vars over `-P` flags (which can appear in process listings / build scans). - Limit which jobs/branches can read the signing secret (e.g. only tag/release pipelines). - Consider a dedicated, rotatable release subkey rather than your primary master key.

  • Why prefer env vars over passing the key with -PsigningKey on the command line?
    -P values can show up in process listings and build scans, leaking the secret. Env vars (especially ORG_GRADLE_PROJECT_*) keep it out of the visible command line.
  • Why guard the signing block with a null check on the key?
    So local builds without the CI secret don't fail outright; signing only activates when both key and passphrase are present. (Required signing only on release is a related, separate concern.)

saying these in an interview costs you the question

  • Committing the armored key or passphrase into gradle.properties in version control.
  • Logging or println-ing the key/passphrase during the build.
  • Assuming raw multi-line keys survive every env-var/secret mechanism unchanged.

context