diff --git a/sigstore-gradle/README.md b/sigstore-gradle/README.md index 4f00aca5..49489b49 100644 --- a/sigstore-gradle/README.md +++ b/sigstore-gradle/README.md @@ -135,6 +135,11 @@ Automatically signs all Maven publications in Sigstore. Provides `SigstoreSignFilesTask` task for signing files in Sigstore. The plugin adds no tasks by default. +Properties: +* `dev.sigstore.sign.remove.sigstore.asc` (since 0.6.0, default: `true`). Removes `.sigstore.asc` files from the publication. + Sonatype OSSRH supports publishing `.sigstore` signatures, and it does not require `.sigstore.asc` files, so + `dev.sigstore.sign` plugin removes them by default. If you need to sign all the files, set this property to `false`. + Extensions: * `sigstoreSign`: `dev.sigstore.sign.SigstoreSignExtension` diff --git a/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/SigstoreSignExtension.kt b/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/SigstoreSignExtension.kt index a10acc4f..dded6c90 100644 --- a/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/SigstoreSignExtension.kt +++ b/sigstore-gradle/sigstore-gradle-sign-base-plugin/src/main/kotlin/dev/sigstore/sign/SigstoreSignExtension.kt @@ -30,6 +30,8 @@ import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.register import org.gradle.kotlin.dsl.the +import org.gradle.kotlin.dsl.withType +import org.gradle.plugins.signing.Sign import kotlin.collections.set abstract class SigstoreSignExtension(private val project: Project) { @@ -78,6 +80,11 @@ abstract class SigstoreSignExtension(private val project: Project) { this.signatureDirectory.set(signatureDirectory) } + val removeSigstoreAsc = + project.findProperty("dev.sigstore.sign.remove.sigstore.asc")?.toString()?.toBoolean() != false + + val publicationName = publication.name + val artifacts = mutableMapOf() publication.allPublishableArtifacts { val publishableArtifact = this @@ -92,6 +99,20 @@ abstract class SigstoreSignExtension(private val project: Project) { publishableArtifact, DefaultDerivedArtifactFile(project.tasks.named(signTask.name), signatureLocation) ).apply { builtBy(signTask) } + // Gradle's signing plugin reacts on adding artifacts, and it might add .asc signature + // So we need to remove .sigstore.asc as it is unwanted in most of the cases + if (removeSigstoreAsc) { + project.tasks.withType() + .matching { it.name.contains(publicationName, ignoreCase = true) } + .configureEach { + // Remove .sigstore.asc signature. + // Unfortunately, it will scan all the signatures every time, + // however, it seems to be the only way to do it since the artifacts can be added + // within afterEvaluate block, so we can't use afterEvaluate + // to "remove all .sigstore.asc" at once + signatures.removeIf { it.name.endsWith(".sigstore.asc") } + } + } } } publication.whenPublishableArtifactRemoved { diff --git a/sigstore-gradle/sigstore-gradle-sign-plugin/src/test/kotlin/dev/sigstore/gradle/RemoveSigstoreAscTest.kt b/sigstore-gradle/sigstore-gradle-sign-plugin/src/test/kotlin/dev/sigstore/gradle/RemoveSigstoreAscTest.kt new file mode 100644 index 00000000..b2a9f55e --- /dev/null +++ b/sigstore-gradle/sigstore-gradle-sign-plugin/src/test/kotlin/dev/sigstore/gradle/RemoveSigstoreAscTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2022 The Sigstore Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package dev.sigstore.gradle + +import dev.sigstore.testkit.BaseGradleTest +import dev.sigstore.testkit.TestedGradle +import dev.sigstore.testkit.TestedSigstoreJava +import dev.sigstore.testkit.annotations.EnabledIfOidcExists +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.SoftAssertions +import org.gradle.util.GradleVersion +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource + +@EnabledIfOidcExists +class RemoveSigstoreAscTest : BaseGradleTest() { + companion object { + @JvmStatic + fun signingSupportedGradleAndSigstoreJavaVersions(): Iterable = + if (isCI) { + gradleAndSigstoreJavaVersions() + } else { + // Find the first version that supports configuration cache for Gradle's signing plugin (8.1+) + listOf( + arguments( + TestedGradle( + // Signing plugin supports configuration cache since 8.1 + gradleVersions().first { it >= GradleVersion.version("8.1") }, + ConfigurationCache.ON + ), + SIGSTORE_JAVA_CURRENT_VERSION + ) + ) + } + + @JvmStatic + fun oneSigningSupportedGradleAndSigstoreJavaVersions(): Iterable = + signingSupportedGradleAndSigstoreJavaVersions().take(1) + } + + @ParameterizedTest + @MethodSource("signingSupportedGradleAndSigstoreJavaVersions") + fun `basic configuration avoids signing sigstore with pgp`(gradle: TestedGradle, sigstoreJava: TestedSigstoreJava) { + prepareBuildScripts(gradle, sigstoreJava) + + prepare(gradle.version, "publishAllPublicationsToTmpRepository", "-s") + .build() + + assertSoftly { + assertSignatures("sigstore-test-1.0.pom") + assertSignatures("sigstore-test-1.0-sources.jar") + assertSignatures("sigstore-test-1.0.module") + assertSignatures("sigstore-test-1.0.pom") + } + + if (gradle.configurationCache == ConfigurationCache.ON) { + val result = prepare(gradle.version, "publishAllPublicationsToTmpRepository", "-s") + .build() + + assertThat(result.output) + .contains( + "Configuration cache entry reused", + "7 actionable tasks: 4 executed, 3 up-to-date", + ) + } + } + + @ParameterizedTest + @MethodSource("oneSigningSupportedGradleAndSigstoreJavaVersions") + fun `crossign sigstore with pgp`(gradle: TestedGradle, sigstoreJava: TestedSigstoreJava) { + prepareBuildScripts(gradle, sigstoreJava) + projectDir.resolve("gradle.properties").toFile().appendText( + """ + + # By default, dev.sigstore.sign asks Gradle to avoid signing .sigstore as .sigstore.asc + # This is an opt-out hatch for those who need .sigstore.asc + dev.sigstore.sign.remove.sigstore.asc=false + """.trimIndent() + ) + prepare(gradle.version, "publishAllPublicationsToTmpRepository", "-s") + .build() + assertSoftly { + assertSignatures("sigstore-test-1.0.pom", expectSigstoreAsc = true) + assertSignatures("sigstore-test-1.0-sources.jar", expectSigstoreAsc = true) + assertSignatures("sigstore-test-1.0.module", expectSigstoreAsc = true) + assertSignatures("sigstore-test-1.0.pom", expectSigstoreAsc = true) + } + } + + private fun prepareBuildScripts(gradle: TestedGradle, sigstoreJava: TestedSigstoreJava) { + writeBuildGradle( + """ + plugins { + id("java") + id("signing") + id("maven-publish") + id("dev.sigstore.sign") + } + ${declareRepositoryAndDependency(sigstoreJava)} + + group = "dev.sigstore.test" + java { + withSourcesJar() + } + publishing { + publications { + maven(MavenPublication) { + groupId = 'dev.sigstore.test' + artifactId = 'sigstore-test' + version = '1.0' + from components.java + } + } + repositories { + maven { + name = "tmp" + url = layout.buildDirectory.dir("tmp-repo") + } + } + } + signing { + useInMemoryPgpKeys( + '''$testOnlySigningKey''', + "testforsigstorejava" + ) + sign(publishing.publications.withType(MavenPublication)) + } + """.trimIndent() + ) + writeSettingsGradle( + """ + rootProject.name = 'sigstore-test' + """.trimIndent() + ) + if (gradle.version >= GradleVersion.version("8.1")) { + enableConfigurationCache(gradle) + } + } + + private fun SoftAssertions.assertSignatures(name: String, expectSigstoreAsc: Boolean = false) { + assertThat(projectDir.resolve("build/tmp-repo/dev/sigstore/test/sigstore-test/1.0/$name.sigstore")) + .describedAs("$name should be signed with Sigstore") + .content() + .basicSigstoreStructure() + assertThat(projectDir.resolve("build/tmp-repo/dev/sigstore/test/sigstore-test/1.0/$name.asc")) + .describedAs("$name should be signed with PGP") + .isNotEmptyFile() + assertThat(projectDir.resolve("build/tmp-repo/dev/sigstore/test/sigstore-test/1.0/$name.asc.sigstore")) + .describedAs("$name.asc should NOT be signed with Sigstore") + .doesNotExist() + assertThat(projectDir.resolve("build/tmp-repo/dev/sigstore/test/sigstore-test/1.0/$name.sigstore.asc")) + .apply { + if (expectSigstoreAsc) { + describedAs("$name.sigstore should be signed with PGP") + exists() + } else { + // We don't want to sign .sigstore files with PGP + describedAs("$name.sigstore should NOT be signed with PGP") + doesNotExist() + } + } + } + + private val testOnlySigningKey = """ + -----BEGIN PGP PRIVATE KEY BLOCK----- + + lIYEZaDRyxYJKwYBBAHaRw8BAQdAjMi3g07livoPo+se6/+wF7LRv2DDJ6UKVBrp + 9rugpwj+BwMCDZlNm7zWHTP6ny1jqI5sdTFaEkHRjFhm63Il9qeF7QcSibgAnBO5 + YK0E4vp8MUQxSAwoOV80mO46a2Ci9hA281lXH6fFTP3qyERXl2/ilrQvVGVzdCBL + ZXkgZm9yIFNpZ3N0b3JlIEphdmEgPHNpZ3N0b3JlQGdpdGh1Yi5pbz6IkwQTFgoA + OxYhBNejX8GGaAn2Jspav54UgcovliH1BQJloNHLAhsDBQsJCAcCAiICBhUKCQgL + AgQWAgMBAh4HAheAAAoJEJ4UgcovliH1YDUBAPE1yBo7i4YgHuHKIGLqkOJqEKE5 + Jbw8ffyZO6tqud2qAP49liajq/HkdEXgUdA6DySpzLYFtd+F6UlpTQE0TeaLAA== + =6fgq + -----END PGP PRIVATE KEY BLOCK----- + """.trimIndent() +} diff --git a/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/BaseGradleTest.kt b/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/BaseGradleTest.kt index 1ae7971f..14ef9286 100644 --- a/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/BaseGradleTest.kt +++ b/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/BaseGradleTest.kt @@ -17,6 +17,7 @@ package dev.sigstore.testkit import org.assertj.core.api.AbstractCharSequenceAssert +import org.assertj.core.api.SoftAssertions import org.gradle.testkit.runner.GradleRunner import org.gradle.testkit.runner.internal.DefaultGradleRunner import org.gradle.util.GradleVersion @@ -46,23 +47,33 @@ open class BaseGradleTest { System.getProperty("sigstore.test.current.version") ) + @JvmStatic + fun gradleVersions() = listOf( + // Gradle 7.2 fails with "No service of type ObjectFactory available in default services" + // So we require Gradle 7.3+ + "7.3", + "7.5.1", + "8.1", + "8.5", + ).map { GradleVersion.version(it) } + @JvmStatic fun gradleVersionAndSettings(): Iterable { if (!isCI) { - // Make the test faster, and skip extra tests with the configuration cache to reduce OIDC flows - // Gradle 7.2 fails with "No service of type ObjectFactory available in default services" - return listOf(arguments(TestedGradle("7.3", ConfigurationCache.ON))) + // Execute a single combination only when running locally + return listOf(arguments(TestedGradle(gradleVersions().first(), ConfigurationCache.ON))) } - return mutableListOf().apply { - add(arguments(TestedGradle("7.3", ConfigurationCache.ON))) - add(arguments(TestedGradle("7.5.1", ConfigurationCache.ON))) - add(arguments(TestedGradle("7.5.1", ConfigurationCache.OFF))) + return buildList { + addAll( + gradleVersions().map { arguments(TestedGradle(it, ConfigurationCache.ON)) } + ) + add(arguments(TestedGradle(gradleVersions().first(), ConfigurationCache.OFF))) } } @JvmStatic fun sigstoreJavaVersions(): Iterable { - return mutableListOf().apply { + return buildList { add(arguments(SIGSTORE_JAVA_CURRENT_VERSION)) // For now, we test the plugins only with locally-built sigstore-java version if (isCI && false) { @@ -150,9 +161,9 @@ open class BaseGradleTest { ) } - protected fun prepare(gradleVersion: String, vararg arguments: String) = + protected fun prepare(gradleVersion: GradleVersion, vararg arguments: String) = gradleRunner - .withGradleVersion(gradleVersion) + .withGradleVersion(gradleVersion.version) .withProjectDir(projectDir.toFile()) .apply { this as DefaultGradleRunner @@ -176,7 +187,7 @@ open class BaseGradleTest { if (gradle.configurationCache != ConfigurationCache.ON) { return } - if (GradleVersion.version(gradle.version) < GradleVersion.version("7.0")) { + if (gradle.version < GradleVersion.version("7.0")) { Assertions.fail("Gradle version $gradle does not support configuration cache") } // Gradle 6.5 expects values ON, OFF, WARN, so we add the option for 7.0 only @@ -189,6 +200,9 @@ open class BaseGradleTest { ) } + protected fun assertSoftly(body: SoftAssertions.() -> Unit) = + SoftAssertions.assertSoftly(body) + protected fun , ACTUAL : CharSequence> AbstractCharSequenceAssert.basicSigstoreStructure() = contains( """"mediaType": "application/vnd.dev.sigstore.bundle+json;version\u003d0.2"""", diff --git a/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/TestedGradle.kt b/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/TestedGradle.kt index 21415ef5..b6ee684b 100644 --- a/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/TestedGradle.kt +++ b/sigstore-testkit/src/main/kotlin/dev/sigstore/testkit/TestedGradle.kt @@ -16,10 +16,12 @@ */ package dev.sigstore.testkit +import org.gradle.util.GradleVersion + /** * Lists Gradle versions and its configuration for backward compatibility testing of Sigstore Gradle plugin. */ data class TestedGradle( - val version: String, + val version: GradleVersion, val configurationCache: BaseGradleTest.ConfigurationCache )