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
+
+
+
+
+
+