Why does Maven Central require PGP/GPG signatures on published artifacts, and how do you wire the Gradle signing plugin to satisfy it (including on CI)?
answer
- signature = integrity + authenticity
- sign(publications[...]) -> .asc per file
- public key on keyserver
- CI: useInMemoryPgpKeys(key, pass)
- key/pass from secrets, guard for local
basics
~20 sSignatures let anyone verify an artifact came from the real publisher and wasn't tampered with. In Gradle you apply the signing plugin and sign(publication); the public key must be on a keyserver. On CI you provide the key/passphrase via the in-memory signing keys.
solid answer
~40 sCentral requires a detached **PGP signature** (`.asc`) for every published file because the repository is public and immutable — signatures provide **integrity** (the bytes weren't altered) and **authenticity** (they came from the key owner). Consumers and Sonatype's validation verify each `.asc` against the publisher's **public key**, which must be discoverable on a public keyserver. In Gradle you apply the `signing` plugin and call `sign(publishing.publications["maven"])`, which produces `.asc` files for every artifact and the POM during publish. Locally the plugin can use your GPG agent; on **CI**, where there's no interactive keyring, you supply an ASCII-armored private key and passphrase and use `signing.useInMemoryPgpKeys(key, passphrase)` so the build signs without a local keystore. The key and passphrase come from secrets/env vars, never committed.
code
kotlin · 6 linessigning {
val key = System.getenv("SIGNING_KEY") // gpg --armor --export-secret-keys
val pass = System.getenv("SIGNING_PASSWORD")
if (key != null) useInMemoryPgpKeys(key, pass)
sign(publishing.publications["maven"])
}go deeper
Know that artifacts must be PGP-signed and that the signing plugin + sign(publication) does it.
Explain integrity vs authenticity, the keyserver requirement, and basic sign(...) wiring.
Handle CI signing with useInMemoryPgpKeys, conditional signing, and secret management.
Define org key management: signing subkeys, offline master, rotation/revocation, and centralizing signing config in a convention plugin.
## The trust problem signatures solve Maven Central artifacts are downloaded by millions of builds and are immutable. Without signatures, a compromised mirror or man-in-the-middle could serve altered bytes and consumers couldn't tell. A **PGP signature** is a cryptographic proof, made with the publisher's **private key**, that a specific file is exactly as the publisher produced it. Anyone with the publisher's **public key** can verify it. This gives two guarantees: - **Integrity** — the file matches what was signed (no tampering/corruption). - **Authenticity** — it was signed by the holder of that private key (provenance). That's why Central requires a `.asc` for the JAR, sources JAR, javadoc JAR, and the POM, and why the **public key must be published to a keyserver** so validation and consumers can fetch it. ## Wiring the signing plugin ```kotlin plugins { `maven-publish` signing } signing { sign(publishing.publications["maven"]) } ``` `sign(...)` registers signing tasks that run as part of `publish`, generating `.asc` detached signatures alongside each artifact. Locally, the plugin defaults to the **gpg-agent / keyring** on your machine (or `~/.gradle/gradle.properties` `signing.keyId/signing.password/signing.secretKeyRingFile`). ## Signing on CI (the part interviewers probe) CI runners have no interactive GPG agent and no keyring file, so the file-based config breaks. The robust approach is **in-memory keys**: ```kotlin val signingKey: String? = System.getenv("SIGNING_KEY") // ASCII-armored private key val signingPassword: String? = System.getenv("SIGNING_PASSWORD") signing { if (signingKey != null) { useInMemoryPgpKeys(signingKey, signingPassword) } sign(publishing.publications["maven"]) } ``` `useInMemoryPgpKeys` takes the armored private key (and passphrase) directly, so no `secretKeyRingFile` is needed. You inject `SIGNING_KEY`/`SIGNING_PASSWORD` as CI secrets. Export the armored key once with `gpg --armor --export-secret-keys <KEYID>`. ## Operational hygiene - Publish the **public** key: `gpg --keyserver keyserver.ubuntu.com --send-keys <KEYID>` so validation/consumers can verify. - Never commit the private key, passphrase, or `secretKeyRingFile`; treat them as secrets. - Use a **subkey** for signing and keep the master key offline where possible; rotate/revoke if leaked. - Make signing **conditional** so local dev / SNAPSHOT builds don't fail when no key is present (the `if (signingKey != null)` guard above). ## Validation outcome If a signature is missing or doesn't verify (e.g., the public key isn't on a keyserver), Sonatype validation fails the staging deployment and the release never goes public — so signing is not optional for Central.
- Why does file-based signing config (signing.secretKeyRingFile) fail on CI?CI runners have no keyring file or gpg-agent, so the plugin can't find the secret key. useInMemoryPgpKeys passes the armored private key and passphrase directly via secrets, removing the keyring dependency.
- Validation says the signature can't be verified even though the .asc exists. What's a common cause?The corresponding public key isn't published to a keyserver, so Sonatype/consumers can't fetch it to verify. Send the public key with gpg --send-keys.
- How do you keep local non-release builds from failing when no signing key is configured?Guard sign(...)/useInMemoryPgpKeys behind a null check on the key env var, or use signing.setRequired { ... } / required false for snapshot builds, so signing only runs when a key is present.
saying these in an interview costs you the question
- Saying signatures are optional or only for security-conscious projects.
- Committing the private key or passphrase into the repo or gradle.properties in VCS.
- Using only file-based keyring config and expecting it to work on a CI runner.