diff --git a/build-logic/publishing/src/main/kotlin/build-logic.depends-on-local-sigstore-java-repo.gradle.kts b/build-logic/publishing/src/main/kotlin/build-logic.depends-on-local-sigstore-java-repo.gradle.kts new file mode 100644 index 00000000..d4bbfeb5 --- /dev/null +++ b/build-logic/publishing/src/main/kotlin/build-logic.depends-on-local-sigstore-java-repo.gradle.kts @@ -0,0 +1,44 @@ +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.kotlin.dsl.* +import java.io.File + +plugins { + java +} + +val sigstoreJavaRuntime by configurations.creating { + description = "declares dependencies that will be useful for testing purposes" + isCanBeConsumed = false + isCanBeResolved = false +} + +val sigstoreJavaTestClasspath by configurations.creating { + description = "sigstore-java in local repository for testing purposes" + isCanBeConsumed = false + isCanBeResolved = true + extendsFrom(sigstoreJavaRuntime) + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named("maven-repository")) + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + } +} + +tasks.test { + dependsOn(sigstoreJavaTestClasspath) + systemProperty("sigstore.test.current.version", version) + val projectDir = layout.projectDirectory.asFile + // This adds paths to the local repositories that contain currently-built sigstore-java + // It enables testing both "sigstore-java from Central" and "sigstore-java build locally" in the plugin tests + jvmArgumentProviders.add( + // Gradle does not support Provider for systemProperties yet, see https://github.com/gradle/gradle/issues/12247 + CommandLineArgumentProvider { + listOf( + "-Dsigstore.test.local.maven.repo=" + + sigstoreJavaTestClasspath.joinToString(File.pathSeparator) { + it.toRelativeString(projectDir) + }, + ) + } + ) +} diff --git a/build-logic/publishing/src/main/kotlin/build-logic.depends-on-local-sigstore-maven-plugin-repo.gradle.kts b/build-logic/publishing/src/main/kotlin/build-logic.depends-on-local-sigstore-maven-plugin-repo.gradle.kts new file mode 100644 index 00000000..54c2df23 --- /dev/null +++ b/build-logic/publishing/src/main/kotlin/build-logic.depends-on-local-sigstore-maven-plugin-repo.gradle.kts @@ -0,0 +1,43 @@ +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.kotlin.dsl.* +import java.io.File + +plugins { + java +} + +val sigstoreMavenPluginRuntime by configurations.creating { + description = "declares dependencies that will be useful for testing purposes" + isCanBeConsumed = false + isCanBeResolved = false +} + +val sigstoreMavenPluginTestClasspath by configurations.creating { + description = "sigstore-maven-plugin in local repository for testing purposes" + isCanBeConsumed = false + isCanBeResolved = true + extendsFrom(sigstoreMavenPluginRuntime) + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named("maven-repository")) + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + } +} + +tasks.test { + dependsOn(sigstoreMavenPluginTestClasspath) + systemProperty("sigstore.test.current.maven.plugin.version", version) + val projectDir = layout.projectDirectory.asFile + // This adds paths to the local repositories that contain currently-built sigstore-maven-plugin + jvmArgumentProviders.add( + // Gradle does not support Provider for systemProperties yet, see https://github.com/gradle/gradle/issues/12247 + CommandLineArgumentProvider { + listOf( + "-Dsigstore.test.local.maven.plugin.repo=" + + sigstoreMavenPluginTestClasspath.joinToString(File.pathSeparator) { + it.toRelativeString(projectDir) + }, + ) + } + ) +} diff --git a/build-logic/publishing/src/main/kotlin/build-logic.kotlin-dsl-published-gradle-plugin.gradle.kts b/build-logic/publishing/src/main/kotlin/build-logic.kotlin-dsl-published-gradle-plugin.gradle.kts index 87ad8e12..07ea5e02 100644 --- a/build-logic/publishing/src/main/kotlin/build-logic.kotlin-dsl-published-gradle-plugin.gradle.kts +++ b/build-logic/publishing/src/main/kotlin/build-logic.kotlin-dsl-published-gradle-plugin.gradle.kts @@ -6,40 +6,5 @@ plugins { id("build-logic.reproducible-builds") id("build-logic.dokka-javadoc") id("build-logic.publish-to-central") -} - -val sigstoreJavaRuntime by configurations.creating { - description = "declares dependencies that will be useful for testing purposes" - isCanBeConsumed = false - isCanBeResolved = false -} - -val sigstoreJavaTestClasspath by configurations.creating { - description = "sigstore-java in local repository for testing purposes" - isCanBeConsumed = false - isCanBeResolved = true - extendsFrom(sigstoreJavaRuntime) - attributes { - attribute(Category.CATEGORY_ATTRIBUTE, objects.named("maven-repository")) - attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) - } -} - -tasks.test { - dependsOn(sigstoreJavaTestClasspath) - systemProperty("sigstore.test.current.version", version) - val projectDir = layout.projectDirectory.asFile - // This adds paths to the local repositories that contain currently-built sigstore-java - // It enables testing both "sigstore-java from Central" and "sigstore-java build locally" in the plugin tests - jvmArgumentProviders.add( - // Gradle does not support Provider for systemProperties yet, see https://github.com/gradle/gradle/issues/12247 - CommandLineArgumentProvider { - listOf( - "-Dsigstore.test.local.maven.repo=" + - sigstoreJavaTestClasspath.joinToString(File.pathSeparator) { - it.toRelativeString(projectDir) - } - ) - } - ) + id("build-logic.depends-on-local-sigstore-java-repo") } diff --git a/build-logic/publishing/src/main/kotlin/build-logic.publish-to-tmp-maven-repo.gradle.kts b/build-logic/publishing/src/main/kotlin/build-logic.publish-to-tmp-maven-repo.gradle.kts index 67377084..7efbee85 100644 --- a/build-logic/publishing/src/main/kotlin/build-logic.publish-to-tmp-maven-repo.gradle.kts +++ b/build-logic/publishing/src/main/kotlin/build-logic.publish-to-tmp-maven-repo.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.kotlin.dsl.registering + plugins { id("java-library") id("maven-publish") diff --git a/settings.gradle.kts b/settings.gradle.kts index 1417d04b..628f2228 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,5 +8,6 @@ include("sigstore-gradle:sigstore-gradle-sign-base-plugin") include("sigstore-gradle:sigstore-gradle-sign-plugin") include("sigstore-testkit") include("sigstore-cli") +include("sigstore-maven-plugin") include("fuzzing") diff --git a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java index 4fe2b56d..0227bc3d 100644 --- a/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java +++ b/sigstore-java/src/main/java/dev/sigstore/KeylessSigner.java @@ -64,7 +64,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.checkerframework.checker.nullness.qual.Nullable; +import javax.annotation.Nullable; /** * A full sigstore keyless signing flow. @@ -93,14 +93,16 @@ public class KeylessSigner implements AutoCloseable { /** The code signing certificate from Fulcio. */ @GuardedBy("lock") - private @Nullable CertPath signingCert; + @Nullable + private CertPath signingCert; /** * Representation {@link #signingCert} in PEM bytes format. This is used to avoid serializing the * certificate for each use. */ @GuardedBy("lock") - private byte @Nullable [] signingCertPemBytes; + @Nullable + private byte[] signingCertPemBytes; private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); diff --git a/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java b/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java index e7891d06..4fc163c7 100644 --- a/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java +++ b/sigstore-java/src/main/java/dev/sigstore/encryption/certificates/Certificates.java @@ -22,6 +22,7 @@ import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.security.cert.*; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -208,4 +209,9 @@ public static boolean isSelfSigned(CertPath certPath) { public static X509Certificate getLeaf(CertPath certPath) { return (X509Certificate) certPath.getCertificates().get(0); } + + public static long validity(X509Certificate certificate, ChronoUnit unit) { + return unit.between( + certificate.getNotAfter().toInstant(), certificate.getNotBefore().toInstant()); + } } diff --git a/sigstore-maven-plugin/.gitignore b/sigstore-maven-plugin/.gitignore new file mode 100644 index 00000000..605bac49 --- /dev/null +++ b/sigstore-maven-plugin/.gitignore @@ -0,0 +1,24 @@ +.vscode +.factorypath +.project +.classpath +.settings/ +*.iml +*.ipr +.idea +*.class +*.jar +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +pom.xml.bak +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.apt_generated/ +.apt_generated_tests/ +bin/ diff --git a/sigstore-maven-plugin/README.md b/sigstore-maven-plugin/README.md new file mode 100644 index 00000000..5cbed47f --- /dev/null +++ b/sigstore-maven-plugin/README.md @@ -0,0 +1,36 @@ +sigstore-maven-plugin +===================== + +[![Maven Central](https://img.shields.io/maven-central/v/dev.sigstore/sigstore-maven-plugin.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/dev.sigstore/sigstore-maven-plugin) + +This is a Maven plugin that can be used to use the "keyless" signing paradigm supported by Sigstore. +This plugin is still in early phases, then has known limitations described below. + +sign +---- + +```xml + + dev.sigstore + sigstore-maven-plugin + 0.4.0 + + + sign + + sign + + + + +``` + +Notes: + +- GPG: Maven Central publication rules require GPG signing each files: to avoid GPG signing of `.sigstore.json` files, just use version 3.1.0 minimum of [maven-gpg-plugin](https://maven.apache.org/plugins/maven-gpg-plugin/). +- `.md5`/`.sha1`: to avoid unneeded checksum files for `.sigstore.java` files, use Maven 3.9.2 minimum or create `.mvn/maven.config` file containing `-Daether.checksums.omitChecksumsForExtensions=.asc,.sigstore.java` + +Known limitations: + +- Maven multi-module build: each module will require an OIDC authentication, +- 10 minutes signing session: if a build takes more than 10 minutes, a new OIDC authentication will be required each 10 minutes. diff --git a/sigstore-maven-plugin/build.gradle.kts b/sigstore-maven-plugin/build.gradle.kts new file mode 100644 index 00000000..2bcb4d6e --- /dev/null +++ b/sigstore-maven-plugin/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("build-logic.java-published-library") + id("build-logic.test-junit5") + id("build-logic.depends-on-local-sigstore-java-repo") + id("build-logic.depends-on-local-sigstore-maven-plugin-repo") + id("de.benediktritter.maven-plugin-development") version "0.4.3" +} + +dependencies { + compileOnly("org.apache.maven:maven-plugin-api:3.9.8") + compileOnly("org.apache.maven:maven-core:3.9.8") + compileOnly("org.apache.maven:maven-core:3.9.8") + compileOnly("org.apache.maven.plugin-tools:maven-plugin-annotations:3.13.1") + + implementation(project(":sigstore-java")) + implementation("org.bouncycastle:bcutil-jdk18on:1.78.1") + implementation("org.apache.maven.plugins:maven-gpg-plugin:3.1.0") + + testImplementation("org.apache.maven.shared:maven-verifier:1.8.0") + + testImplementation(project(":sigstore-testkit")) + + sigstoreJavaRuntime(project(":sigstore-java")) { + because("Test code needs access locally-built sigstore-java as a Maven repository") + } + sigstoreMavenPluginRuntime(project(":sigstore-maven-plugin")) { + because("Test code needs access locally-built sigstore-java as a Maven repository") + } +} diff --git a/sigstore-maven-plugin/src/main/java/dev/sigstore/plugin/FulcioOidHelper.java b/sigstore-maven-plugin/src/main/java/dev/sigstore/plugin/FulcioOidHelper.java new file mode 100644 index 00000000..a59a684d --- /dev/null +++ b/sigstore-maven-plugin/src/main/java/dev/sigstore/plugin/FulcioOidHelper.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023 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.plugin; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1String; +import org.bouncycastle.asn1.DEROctetString; + +/** + * Helper to decode Fulcio OID data, see Sigstore OID + * information. + */ +public class FulcioOidHelper { + private static final String SIGSTORE_OID_ROOT = "1.3.6.1.4.1.57264"; + private static final String FULCIO_OID_ROOT = SIGSTORE_OID_ROOT + ".1"; + + @Deprecated private static final String FULCIO_ISSUER_OID = FULCIO_OID_ROOT + ".1"; + + private static final String FULCIO_ISSUER_V2_OID = FULCIO_OID_ROOT + ".8"; + + public static String getIssuer(X509Certificate cert) { + String issuerV2 = getIssuerV2(cert); + if (issuerV2 == null) { + return getIssuerV1(cert); + } + return issuerV2; + } + + @Deprecated + public static String getIssuerV1(X509Certificate cert) { + return getExtensionValue(cert, FULCIO_ISSUER_OID, true); + } + + public static String getIssuerV2(X509Certificate cert) { + return getExtensionValue(cert, FULCIO_ISSUER_V2_OID, false); + } + + /* Extracts the octets from an extension value and converts to utf-8 directly, it does NOT + * account for any ASN1 encoded value. If the extension value is an ASN1 object (like an + * ASN1 encoded string), you need to write a new extraction helper. */ + private static String getExtensionValue(X509Certificate cert, String oid, boolean rawUtf8) { + byte[] extensionValue = cert.getExtensionValue(oid); + + if (extensionValue == null) { + return null; + } + try { + ASN1Primitive derObject = ASN1Sequence.fromByteArray(cert.getExtensionValue(oid)); + if (derObject instanceof DEROctetString) { + DEROctetString derOctetString = (DEROctetString) derObject; + if (rawUtf8) { + // this is unusual, but the octet is a raw utf8 string in fulcio land (no prefix of type) + // and not an ASN1 object. + return new String(derOctetString.getOctets(), StandardCharsets.UTF_8); + } + + derObject = ASN1Sequence.fromByteArray(derOctetString.getOctets()); + if (derObject instanceof ASN1String) { + ASN1String s = (ASN1String) derObject; + return s.getString(); + } + } + throw new RuntimeException( + "Could not parse extension " + + oid + + " in certificate because it was not an octet string"); + } catch (IOException ioe) { + throw new RuntimeException("Could not parse extension " + oid + " in certificate", ioe); + } + } +} diff --git a/sigstore-maven-plugin/src/main/java/dev/sigstore/plugin/SigstoreSignAttachedMojo.java b/sigstore-maven-plugin/src/main/java/dev/sigstore/plugin/SigstoreSignAttachedMojo.java new file mode 100644 index 00000000..9daf6a8a --- /dev/null +++ b/sigstore-maven-plugin/src/main/java/dev/sigstore/plugin/SigstoreSignAttachedMojo.java @@ -0,0 +1,147 @@ +/* + * Copyright 2024 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.plugin; + +import dev.sigstore.KeylessSigner; +import dev.sigstore.bundle.Bundle; +import dev.sigstore.encryption.certificates.Certificates; +import java.io.File; +import java.security.cert.X509Certificate; +import java.time.temporal.ChronoUnit; +import java.util.List; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Component; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.gpg.FilesCollector; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.codehaus.plexus.util.FileUtils; + +/** Sign project artifact, the POM, and attached artifacts with sigstore for deployment. */ +@Mojo(name = "sign", defaultPhase = LifecyclePhase.VERIFY, threadSafe = true) +public class SigstoreSignAttachedMojo extends AbstractMojo { + + private static final String BUNDLE_EXTENSION = ".sigstore.json"; + + // TODO: this can potentially be derived from mvn-gpg-plugin:FilesCollector.java, + // but that requires a change in that plugin before it makes sense here. + private static final String DEFAULT_EXCLUDES[] = + new String[] { + "**/*.md5", "**/*.sha1", "**/*.sha256", "**/*.sha512", "**/*.asc", "**/*.sigstore.json" + }; + + /** Skip doing the sigstore signing. */ + @Parameter(property = "sigstore.skip", defaultValue = "false") + private boolean skip; + + /** + * A list of files to exclude from being signed. Can contain Ant-style wildcards and double + * wildcards. The default excludes are + * **/*.md5 **/*.sha1 **/*.sha256 **/*.sha512 **/*.asc **/*.sigstore.json + * . + */ + @Parameter private String[] excludes; + + /** Use public staging {@code sigstage.dev} instead of public default {@code sigstore.dev}. */ + @Parameter(defaultValue = "false", property = "public-staging") + private boolean publicStaging; + + /** The Maven project. */ + @Parameter(defaultValue = "${project}", readonly = true) + private MavenProject project; + + /** Maven ProjectHelper */ + @Component private MavenProjectHelper projectHelper; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + if (skip) { + // We're skipping the signing stuff + return; + } + + // ---------------------------------------------------------------------------- + // Collect files to sign + // ---------------------------------------------------------------------------- + + FilesCollector collector = + new FilesCollector(project, (excludes == null) ? DEFAULT_EXCLUDES : excludes, getLog()); + List items = collector.collect(); + + // ---------------------------------------------------------------------------- + // Sign the filesToSign and attach all the signatures + // ---------------------------------------------------------------------------- + + getLog().info("Signing " + items.size() + " file" + ((items.size() > 1) ? "s" : "") + "."); + + try { + KeylessSigner signer; + + if (publicStaging) { + signer = KeylessSigner.builder().sigstoreStagingDefaults().build(); + } else { + signer = KeylessSigner.builder().sigstorePublicDefaults().build(); + } + + X509Certificate prevCert = null; + for (FilesCollector.Item item : items) { + File fileToSign = item.getFile(); + + getLog().info("Signing " + fileToSign); + long start = System.currentTimeMillis(); + Bundle bundle = signer.signFile(fileToSign.toPath()); + + X509Certificate cert = (X509Certificate) bundle.getCertPath().getCertificates().get(0); + if (!cert.equals(prevCert)) { + prevCert = cert; + long durationMinutes = Certificates.validity(cert, ChronoUnit.MINUTES); + + getLog() + .info( + " Fulcio certificate (valid for " + + durationMinutes + + " m) obtained for " + + cert.getSubjectAlternativeNames().iterator().next().get(1) + + " (by " + + FulcioOidHelper.getIssuerV2(cert) + + " IdP)"); + } + + File bundleFile = new File(fileToSign + BUNDLE_EXTENSION); + FileUtils.fileWrite(bundleFile, "UTF-8", bundle.toJson()); + + long duration = System.currentTimeMillis() - start; + getLog() + .info( + " > Rekor entry " + + bundle.getEntries().get(0).getLogIndex() + + " obtained in " + + duration + + " ms, saved to " + + bundleFile.getName()); + + projectHelper.attachArtifact( + project, item.getExtension() + BUNDLE_EXTENSION, item.getClassifier(), bundleFile); + } + } catch (Exception e) { + throw new MojoExecutionException("Error while signing with sigstore", e); + } + } +} diff --git a/sigstore-maven-plugin/src/test/java/dev/sigstore/plugin/SigningTest.java b/sigstore-maven-plugin/src/test/java/dev/sigstore/plugin/SigningTest.java new file mode 100644 index 00000000..c83bb503 --- /dev/null +++ b/sigstore-maven-plugin/src/test/java/dev/sigstore/plugin/SigningTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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.plugin; + +import dev.sigstore.plugin.test.MavenTestProject; +import dev.sigstore.testkit.annotations.EnabledIfOidcExists; +import dev.sigstore.testkit.annotations.OidcProviderType; +import java.io.IOException; +import java.nio.file.Path; +import org.apache.maven.it.VerificationException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class SigningTest { + + @TempDir public static Path testRoot; + + @Test + @EnabledIfOidcExists(provider = OidcProviderType.ANY) + public void test_simpleProject() throws IOException, VerificationException { + var testProject = new MavenTestProject(testRoot, "simple"); + var verifier = testProject.newVerifier(); + + verifier.executeGoal("package"); + verifier.verifyErrorFreeLog(); + verifier.verifyFilePresent("target/simple-it-1.0-SNAPSHOT.jar.sigstore.json"); + verifier.verifyFilePresent("target/simple-it-1.0-SNAPSHOT.pom.sigstore.json"); + } +} diff --git a/sigstore-maven-plugin/src/test/java/dev/sigstore/plugin/test/MavenTestProject.java b/sigstore-maven-plugin/src/test/java/dev/sigstore/plugin/test/MavenTestProject.java new file mode 100644 index 00000000..30391519 --- /dev/null +++ b/sigstore-maven-plugin/src/test/java/dev/sigstore/plugin/test/MavenTestProject.java @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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.plugin.test; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.stream.Collectors; +import org.apache.maven.it.VerificationException; +import org.apache.maven.it.Verifier; +import org.apache.maven.it.util.ResourceExtractor; + +/** + * Initialize a test project verifier. You should use this to inject the right local repository into + * settings.xml and the proejct version into pom.xml. Works with the test Maven projects in the + * {@code resources/maven/projects} directory. + */ +public class MavenTestProject { + + private static final String PROJECTS_PATH_IN_RESOURCES = "/maven/projects/"; + private static final String SETTINGS_XML = "/maven/settings.xml"; + + private final Path testDir; + private final String testProjectName; + + public MavenTestProject(Path testDir, String testProjectName) { + this.testDir = testDir; + this.testProjectName = testProjectName; + } + + public Verifier newVerifier() throws IOException, VerificationException { + ResourceExtractor.extractResourcePath( + MavenTestProject.class, + PROJECTS_PATH_IN_RESOURCES + testProjectName, + testDir.toFile(), + true); + + File settingsXml = + ResourceExtractor.extractResourceToDestination( + MavenTestProject.class, SETTINGS_XML, testDir.resolve("settings.xml").toFile(), true); + + // properties are injected into the test task by the build (see + // build-logic.depends-on-local-sigstore-java-repo.gradle.kts, + // build-logic.depends-on-local-sigstore-maven-plugin-repo.gradle.kts) + String pluginVersion = System.getProperty("sigstore.test.current.maven.plugin.version"); + + try (var walker = Files.walk(testDir)) { + var pomXmls = + walker + .filter(p -> p.getFileName().toString().equals("pom.xml")) + .collect(Collectors.toList()); + for (var pomXml : pomXmls) { + Files.write( + pomXml, + Files.readString(pomXml) + .replace("@PluginVersion@", pluginVersion) + .getBytes(StandardCharsets.UTF_8)); + } + } + + var localMavenRepoProp = System.getProperty("sigstore.test.local.maven.repo"); + if (localMavenRepoProp == null) { + throw new RuntimeException("no local repo configured for maven test"); + } + var localMavenRepo = "file:///" + Paths.get(localMavenRepoProp).toRealPath(); + var localMavenRepoPluginProp = System.getProperty("sigstore.test.local.maven.plugin.repo"); + if (localMavenRepoPluginProp == null) { + throw new RuntimeException("no local plugin repo configured for maven test"); + } + var localMavenPluginRepo = "file:///" + Paths.get(localMavenRepoPluginProp).toRealPath(); + Files.write( + settingsXml.toPath(), + Files.readString(settingsXml.toPath()) + .replace("@localRepositoryUrl@", localMavenRepo) + .replace("@localPluginRepositoryUrl@", localMavenPluginRepo) + .getBytes(StandardCharsets.UTF_8)); + + Path projectRoot = Paths.get(testDir.toString(), PROJECTS_PATH_IN_RESOURCES, testProjectName); + var verifier = new Verifier(projectRoot.toAbsolutePath().toString()); + verifier.addCliOption("--settings=" + settingsXml.getCanonicalPath()); + return verifier; + } +} diff --git a/sigstore-maven-plugin/src/test/resources/maven/projects/simple/pom.xml b/sigstore-maven-plugin/src/test/resources/maven/projects/simple/pom.xml new file mode 100644 index 00000000..8199acd7 --- /dev/null +++ b/sigstore-maven-plugin/src/test/resources/maven/projects/simple/pom.xml @@ -0,0 +1,50 @@ + + + + + 4.0.0 + + sigstore.plugin.it + simple-it + 1.0-SNAPSHOT + + A simple IT verifying the basic use case. + + + UTF-8 + + + + + + dev.sigstore + sigstore-maven-plugin + @PluginVersion@ + + + sign + package + + sign + + + + + + + diff --git a/sigstore-maven-plugin/src/test/resources/maven/settings.xml b/sigstore-maven-plugin/src/test/resources/maven/settings.xml new file mode 100644 index 00000000..ad7f5220 --- /dev/null +++ b/sigstore-maven-plugin/src/test/resources/maven/settings.xml @@ -0,0 +1,49 @@ + + + + + + + it-repo + + true + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + local.plugin.central + @localPluginRepositoryUrl@ + + true + + + true + + + + + +