From 1074cbcd20d5e31d7e096675ff04b5f693802905 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 30 Mar 2026 16:47:37 +0200 Subject: [PATCH 01/51] Add `build-attestation` target This PR was moved from apache/commons-build-plugin#417 It adds a goal to generate a [SLSA](https://slsa.dev/) build attestation and attaches it to the build as a file with the `.intoto.json` extension. The attestation records the following information about the build environment: - The Java version used (vendor, version string) - The Maven version used - The `gitTree` hash of the unpacked Java distribution - The `gitTree` hash of the unpacked Maven distribution --- checkstyle.xml | 2 +- fb-excludes.xml | 5 + pom.xml | 65 +++ .../plugin/internal/ArtifactUtils.java | 118 +++++ .../plugin/internal/BuildToolDescriptors.java | 89 ++++ .../release/plugin/internal/GitUtils.java | 117 +++++ .../release/plugin/internal/package-info.java | 23 + .../plugin/mojos/BuildAttestationMojo.java | 454 ++++++++++++++++++ .../plugin/slsa/v1_2/BuildDefinition.java | 173 +++++++ .../plugin/slsa/v1_2/BuildMetadata.java | 140 ++++++ .../release/plugin/slsa/v1_2/Builder.java | 125 +++++ .../release/plugin/slsa/v1_2/Provenance.java | 120 +++++ .../plugin/slsa/v1_2/ResourceDescriptor.java | 227 +++++++++ .../release/plugin/slsa/v1_2/RunDetails.java | 137 ++++++ .../release/plugin/slsa/v1_2/Statement.java | 122 +++++ .../plugin/slsa/v1_2/package-info.java | 34 ++ .../release/plugin/internal/MojoUtils.java | 71 +++ .../mojos/BuildAttestationMojoTest.java | 129 +++++ src/test/resources/artifacts/artifact-jar.txt | 2 + src/test/resources/artifacts/artifact-pom.txt | 2 + 20 files changed, 2154 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java create mode 100644 src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java create mode 100644 src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java create mode 100644 src/main/java/org/apache/commons/release/plugin/internal/package-info.java create mode 100644 src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/package-info.java create mode 100644 src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java create mode 100644 src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java create mode 100644 src/test/resources/artifacts/artifact-jar.txt create mode 100644 src/test/resources/artifacts/artifact-pom.txt diff --git a/checkstyle.xml b/checkstyle.xml index 8f329d35b..0f5a18551 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -185,7 +185,7 @@ - + diff --git a/fb-excludes.xml b/fb-excludes.xml index 2cba28121..0a0a38438 100644 --- a/fb-excludes.xml +++ b/fb-excludes.xml @@ -18,6 +18,11 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd"> + + + + + diff --git a/pom.xml b/pom.xml index faa5b1ae0..e0c6767e7 100644 --- a/pom.xml +++ b/pom.xml @@ -113,7 +113,22 @@ true true + + 2.21.1 + 2.21 + 2.0.17 + + + + org.slf4j + slf4j-bom + ${commons.slf4j.version} + pom + import + + + org.apache.commons @@ -151,6 +166,18 @@ maven-scm-api ${maven-scm.version} + + org.apache.maven.scm + maven-scm-manager-plexus + ${maven-scm.version} + compile + + + org.apache.maven.scm + maven-scm-provider-gitexe + ${maven-scm.version} + runtime + org.apache.maven.scm maven-scm-provider-svnexe @@ -171,6 +198,22 @@ commons-compress 1.28.0 + + com.fasterxml.jackson.core + jackson-databind + ${commons.jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${commons.jackson.annotations.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${commons.jackson.version} + runtime + org.apache.maven.plugin-testing maven-plugin-testing-harness @@ -188,11 +231,28 @@ junit-jupiter test + + net.javacrumbs.json-unit + json-unit-assertj + 2.40.1 + test + + + org.junit.jupiter + junit-jupiter-api + test + org.junit.vintage junit-vintage-engine test + + org.mockito + mockito-core + 4.11.0 + test + org.apache.maven @@ -223,6 +283,11 @@ + + org.slf4j + slf4j-simple + test + clean verify apache-rat:check checkstyle:check spotbugs:check javadoc:javadoc site diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java new file mode 100644 index 000000000..7f07244e2 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; + +/** + * Utilities to convert {@link Artifact} from and to other types. + */ +public final class ArtifactUtils { + + /** No instances. */ + private ArtifactUtils() { + // prevent instantiation + } + + /** + * Returns the conventional filename for the given artifact. + * + * @param artifact A Maven artifact. + * @return A filename. + */ + public static String getFileName(Artifact artifact) { + return getFileName(artifact, artifact.getArtifactHandler().getExtension()); + } + + /** + * Returns the filename for the given artifact with a changed extension. + * + * @param artifact A Maven artifact. + * @param extension The file name extension. + * @return A filename. + */ + public static String getFileName(Artifact artifact, String extension) { + StringBuilder fileName = new StringBuilder(); + fileName.append(artifact.getArtifactId()).append("-").append(artifact.getVersion()); + if (artifact.getClassifier() != null) { + fileName.append("-").append(artifact.getClassifier()); + } + fileName.append(".").append(extension); + return fileName.toString(); + } + + /** + * Returns the Package URL corresponding to this artifact. + * + * @param artifact A maven artifact. + * @return A PURL for the given artifact. + */ + public static String getPackageUrl(Artifact artifact) { + StringBuilder sb = new StringBuilder(); + sb.append("pkg:maven/").append(artifact.getGroupId()).append("/").append(artifact.getArtifactId()).append("@").append(artifact.getVersion()) + .append("?"); + String classifier = artifact.getClassifier(); + if (classifier != null) { + sb.append("classifier=").append(classifier).append("&"); + } + sb.append("type=").append(artifact.getType()); + return sb.toString(); + } + + /** + * Returns a map of checksum algorithm names to hex-encoded digest values for the given artifact file. + * + * @param artifact A Maven artifact. + * @return A map of checksum algorithm names to hex-encoded digest values. + * @throws IOException If an I/O error occurs reading the artifact file. + */ + private static Map getChecksums(Artifact artifact) throws IOException { + Map checksums = new HashMap<>(); + DigestUtils digest = new DigestUtils(DigestUtils.getSha256Digest()); + String sha256sum = digest.digestAsHex(artifact.getFile()); + checksums.put("sha256", sha256sum); + return checksums; + } + + /** + * Converts a Maven artifact to a SLSA {@link ResourceDescriptor}. + * + * @param artifact A Maven artifact. + * @return A SLSA resource descriptor. + * @throws MojoExecutionException If an I/O error occurs retrieving the artifact. + */ + public static ResourceDescriptor toResourceDescriptor(Artifact artifact) throws MojoExecutionException { + ResourceDescriptor descriptor = new ResourceDescriptor(); + descriptor.setName(getFileName(artifact)); + descriptor.setUri(getPackageUrl(artifact)); + if (artifact.getFile() != null) { + try { + descriptor.setDigest(getChecksums(artifact)); + } catch (IOException e) { + throw new MojoExecutionException("Unable to compute hash for artifact file: " + artifact.getFile(), e); + } + } + return descriptor; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java new file mode 100644 index 000000000..15be8d73e --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; + +/** + * Factory methods for {@link ResourceDescriptor} instances representing build-tool dependencies. + */ +public final class BuildToolDescriptors { + + /** No instances. */ + private BuildToolDescriptors() { + // no instantiation + } + + /** + * Creates a {@link ResourceDescriptor} for the JDK used during the build. + * + * @param javaHome path to the JDK home directory (value of the {@code java.home} system property) + * @return a descriptor with digest and annotations populated from system properties + * @throws IOException if hashing the JDK directory fails + */ + public static ResourceDescriptor jvm(Path javaHome) throws IOException { + ResourceDescriptor descriptor = new ResourceDescriptor(); + descriptor.setName("JDK"); + Map digest = new HashMap<>(); + digest.put("gitTree", GitUtils.gitTree(javaHome)); + descriptor.setDigest(digest); + String[] propertyNames = {"java.version", "java.vendor", "java.vendor.version", "java.vm.name", "java.vm.version", "java.vm.vendor", + "java.runtime.name", "java.runtime.version", "java.specification.version"}; + Map annotations = new HashMap<>(); + for (String prop : propertyNames) { + annotations.put(prop.substring("java.".length()), System.getProperty(prop)); + } + descriptor.setAnnotations(annotations); + return descriptor; + } + + /** + * Creates a {@link ResourceDescriptor} for the Maven installation used during the build. + * + * @param version Maven version string + * @param mavenHome path to the Maven home directory + * @return a descriptor for the Maven installation + * @throws IOException if hashing the Maven home directory fails + */ + public static ResourceDescriptor maven(String version, Path mavenHome) throws IOException { + ResourceDescriptor descriptor = new ResourceDescriptor(); + descriptor.setName("Maven"); + descriptor.setUri("pkg:maven/org.apache.maven/apache-maven@" + version); + Map digest = new HashMap<>(); + digest.put("gitTree", GitUtils.gitTree(mavenHome)); + descriptor.setDigest(digest); + Properties buildProps = new Properties(); + try (InputStream in = BuildToolDescriptors.class.getResourceAsStream("/org/apache/maven/messages/build.properties")) { + if (in != null) { + buildProps.load(in); + } + } + if (!buildProps.isEmpty()) { + Map annotations = new HashMap<>(); + buildProps.forEach((key, value) -> annotations.put((String) key, value)); + descriptor.setAnnotations(annotations); + } + return descriptor; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java new file mode 100644 index 000000000..246027c49 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +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.security.MessageDigest; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.digest.DigestUtils; + +/** + * Utilities for Git operations. + */ +public final class GitUtils { + + /** The SCM URI prefix for Git repositories. */ + private static final String SCM_GIT_PREFIX = "scm:git:"; + + /** + * Returns the Git tree hash for the given directory. + * + * @param path A directory path. + * @return A hex-encoded SHA-1 tree hash. + * @throws IOException If the path is not a directory or an I/O error occurs. + */ + public static String gitTree(Path path) throws IOException { + if (!Files.isDirectory(path)) { + throw new IOException("Path is not a directory: " + path); + } + MessageDigest digest = DigestUtils.getSha1Digest(); + return Hex.encodeHexString(DigestUtils.gitTree(digest, path)); + } + + /** + * Converts an SCM URI to a download URI suffixed with the current branch name. + * + * @param scmUri A Maven SCM URI starting with {@code scm:git}. + * @param repositoryPath A path inside the Git repository. + * @return A download URI of the form {@code git+@}. + * @throws IOException If the current branch cannot be determined. + */ + public static String scmToDownloadUri(String scmUri, Path repositoryPath) throws IOException { + if (!scmUri.startsWith(SCM_GIT_PREFIX)) { + throw new IllegalArgumentException("Invalid scmUri: " + scmUri); + } + String currentBranch = getCurrentBranch(repositoryPath); + return "git+" + scmUri.substring(SCM_GIT_PREFIX.length()) + "@" + currentBranch; + } + + /** + * Returns the current branch name for the given repository path. + * + *

Returns the commit SHA if the repository is in a detached HEAD state. + * + * @param repositoryPath A path inside the Git repository. + * @return The current branch name, or the commit SHA for a detached HEAD. + * @throws IOException If the {@code .git} directory cannot be found or read. + */ + public static String getCurrentBranch(Path repositoryPath) throws IOException { + Path gitDir = findGitDir(repositoryPath); + String head = new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim(); + if (head.startsWith("ref: refs/heads/")) { + return head.substring("ref: refs/heads/".length()); + } + // detached HEAD — return the commit SHA + return head; + } + + /** + * Walks up the directory tree from {@code path} to find the {@code .git} directory. + * + * @param path A path inside the Git repository. + * @return The path to the {@code .git} directory (or file for worktrees). + * @throws IOException If no {@code .git} directory is found. + */ + private static Path findGitDir(Path path) throws IOException { + Path current = path.toAbsolutePath(); + while (current != null) { + Path candidate = current.resolve(".git"); + if (Files.isDirectory(candidate)) { + return candidate; + } + if (Files.isRegularFile(candidate)) { + // git worktree: .git is a file containing "gitdir: /path/to/real/.git" + String content = new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim(); + if (content.startsWith("gitdir: ")) { + return Paths.get(content.substring("gitdir: ".length())); + } + } + current = current.getParent(); + } + throw new IOException("No .git directory found above: " + path); + } + + /** No instances. */ + private GitUtils() { + // no instantiation + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/internal/package-info.java b/src/main/java/org/apache/commons/release/plugin/internal/package-info.java new file mode 100644 index 000000000..9218ebff4 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/package-info.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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. + */ + +/** + * Internal utilities for the commons-release-plugin. + * + *

Should not be referenced by external artifacts.

+ */ +package org.apache.commons.release.plugin.internal; diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java new file mode 100644 index 000000000..0260e22e1 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -0,0 +1,454 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.mojos; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.inject.Inject; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.apache.commons.release.plugin.internal.ArtifactUtils; +import org.apache.commons.release.plugin.internal.BuildToolDescriptors; +import org.apache.commons.release.plugin.internal.GitUtils; +import org.apache.commons.release.plugin.slsa.v1_2.BuildDefinition; +import org.apache.commons.release.plugin.slsa.v1_2.BuildMetadata; +import org.apache.commons.release.plugin.slsa.v1_2.Builder; +import org.apache.commons.release.plugin.slsa.v1_2.Provenance; +import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; +import org.apache.commons.release.plugin.slsa.v1_2.RunDetails; +import org.apache.commons.release.plugin.slsa.v1_2.Statement; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +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.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.rtinfo.RuntimeInformation; +import org.apache.maven.scm.CommandParameters; +import org.apache.maven.scm.ScmException; +import org.apache.maven.scm.ScmFileSet; +import org.apache.maven.scm.command.info.InfoItem; +import org.apache.maven.scm.command.info.InfoScmResult; +import org.apache.maven.scm.manager.ScmManager; +import org.apache.maven.scm.repository.ScmRepository; + +/** + * This plugin generates an in-toto attestation for all the artifacts. + */ +@Mojo(name = "build-attestation", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) +public class BuildAttestationMojo extends AbstractMojo { + + /** The file extension for in-toto attestation files. */ + private static final String ATTESTATION_EXTENSION = "intoto.json"; + + /** Shared Jackson object mapper for serializing attestation statements. */ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + static { + OBJECT_MAPPER.findAndRegisterModules(); + OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + + /** The SCM connection URL for the current project. */ + @Parameter(defaultValue = "${project.scm.connection}", readonly = true) + private String scmConnectionUrl; + + /** The Maven home directory. */ + @Parameter(defaultValue = "${maven.home}", readonly = true) + private File mavenHome; + + /** + * Issue SCM actions at this local directory. + */ + @Parameter(property = "commons.release.scmDirectory", defaultValue = "${basedir}") + private File scmDirectory; + + /** The output directory for the attestation file. */ + @Parameter(property = "commons.release.outputDirectory", defaultValue = "${project.build.directory}") + private File outputDirectory; + + /** Whether to skip attaching the attestation artifact to the project. */ + @Parameter(property = "commons.release.skipAttach") + private boolean skipAttach; + + /** + * The current Maven project. + */ + private final MavenProject project; + + /** + * SCM manager to detect the Git revision. + */ + private final ScmManager scmManager; + + /** + * Runtime information. + */ + private final RuntimeInformation runtimeInformation; + + /** + * The current Maven session, used to resolve plugin dependencies. + */ + private final MavenSession session; + + /** + * Helper to attach artifacts to the project. + */ + private final MavenProjectHelper mavenProjectHelper; + + /** + * Creates a new instance with the given dependencies. + * + * @param project A Maven project. + * @param scmManager A SCM manager. + * @param runtimeInformation Maven runtime information. + * @param session A Maven session. + * @param mavenProjectHelper A helper to attach artifacts to the project. + */ + @Inject + public BuildAttestationMojo(MavenProject project, ScmManager scmManager, RuntimeInformation runtimeInformation, MavenSession session, + MavenProjectHelper mavenProjectHelper) { + this.project = project; + this.scmManager = scmManager; + this.runtimeInformation = runtimeInformation; + this.session = session; + this.mavenProjectHelper = mavenProjectHelper; + } + + /** + * Sets the output directory for the attestation file. + * + * @param outputDirectory The output directory. + */ + void setOutputDirectory(final File outputDirectory) { + this.outputDirectory = outputDirectory; + } + + /** + * Returns the SCM directory. + * + * @return The SCM directory. + */ + public File getScmDirectory() { + return scmDirectory; + } + + /** + * Sets the SCM directory. + * + * @param scmDirectory The SCM directory. + */ + public void setScmDirectory(File scmDirectory) { + this.scmDirectory = scmDirectory; + } + + /** + * Sets the public SCM connection URL. + * + * @param scmConnectionUrl The SCM connection URL. + */ + void setScmConnectionUrl(final String scmConnectionUrl) { + this.scmConnectionUrl = scmConnectionUrl; + } + + /** + * Sets the Maven home directory. + * + * @param mavenHome The Maven home directory. + */ + void setMavenHome(final File mavenHome) { + this.mavenHome = mavenHome; + } + + @Override + public void execute() throws MojoFailureException, MojoExecutionException { + // Build definition + BuildDefinition buildDefinition = new BuildDefinition(); + buildDefinition.setExternalParameters(getExternalParameters()); + buildDefinition.setResolvedDependencies(getBuildDependencies()); + // Builder + Builder builder = new Builder(); + // RunDetails + RunDetails runDetails = new RunDetails(); + runDetails.setBuilder(builder); + runDetails.setMetadata(getBuildMetadata()); + // Provenance + Provenance provenance = new Provenance(); + provenance.setBuildDefinition(buildDefinition); + provenance.setRunDetails(runDetails); + // Statement + Statement statement = new Statement(); + statement.setSubject(getSubjects()); + statement.setPredicate(provenance); + + writeStatement(statement); + } + + /** + * Serializes the attestation statement to a file and optionally attaches it to the project. + * + * @param statement The attestation statement to write. + * @throws MojoExecutionException If the output directory cannot be created or the file cannot be written. + */ + private void writeStatement(final Statement statement) throws MojoExecutionException { + final Path outputPath = outputDirectory.toPath(); + try { + if (!Files.exists(outputPath)) { + Files.createDirectories(outputPath); + } + } catch (IOException e) { + throw new MojoExecutionException("Could not create output directory.", e); + } + final Artifact mainArtifact = project.getArtifact(); + final Path artifactPath = outputPath.resolve(ArtifactUtils.getFileName(mainArtifact, ATTESTATION_EXTENSION)); + getLog().info("Writing attestation statement to: " + artifactPath); + try (OutputStream os = Files.newOutputStream(artifactPath)) { + OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(os, statement); + } catch (IOException e) { + throw new MojoExecutionException("Could not write attestation statement to: " + artifactPath, e); + } + if (!skipAttach) { + getLog().info(String.format("Attaching attestation statement as %s-%s.%s", mainArtifact.getArtifactId(), mainArtifact.getVersion(), + ATTESTATION_EXTENSION)); + mavenProjectHelper.attachArtifact(project, ATTESTATION_EXTENSION, null, artifactPath.toFile()); + } + } + + /** + * Get the artifacts generated by the build. + * + * @return A list of resource descriptors for the build artifacts. + * @throws MojoExecutionException If artifact hashing fails. + */ + private List getSubjects() throws MojoExecutionException { + List subjects = new ArrayList<>(); + subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact())); + for (Artifact artifact : project.getAttachedArtifacts()) { + subjects.add(ArtifactUtils.toResourceDescriptor(artifact)); + } + return subjects; + } + + /** + * Gets map of external build parameters captured from the current JVM and Maven session. + * + * @return A map of parameter names to values. + */ + private Map getExternalParameters() { + Map params = new HashMap<>(); + params.put("jvm.args", ManagementFactory.getRuntimeMXBean().getInputArguments()); + MavenExecutionRequest request = session.getRequest(); + params.put("maven.goals", request.getGoals()); + params.put("maven.profiles", request.getActiveProfiles()); + params.put("maven.user.properties", request.getUserProperties()); + params.put("maven.cmdline", getCommandLine(request)); + Map env = new HashMap<>(); + params.put("env", env); + for (Map.Entry entry : System.getenv().entrySet()) { + String key = entry.getKey(); + if ("TZ".equals(key) || "LANG".equals(key) || key.startsWith("LC_")) { + env.put(key, entry.getValue()); + } + } + return params; + } + + /** + * Reconstructs the Maven command line string from the given execution request. + * + * @param request The Maven execution request. + * @return A string representation of the Maven command line. + */ + private String getCommandLine(final MavenExecutionRequest request) { + StringBuilder sb = new StringBuilder(); + for (String goal : request.getGoals()) { + sb.append(goal); + sb.append(" "); + } + List activeProfiles = request.getActiveProfiles(); + if (activeProfiles != null && !activeProfiles.isEmpty()) { + sb.append("-P"); + for (String profile : activeProfiles) { + sb.append(profile); + sb.append(","); + } + removeLast(sb); + sb.append(" "); + } + Properties userProperties = request.getUserProperties(); + for (String propertyName : userProperties.stringPropertyNames()) { + sb.append("-D"); + sb.append(propertyName); + sb.append("="); + sb.append(userProperties.get(propertyName)); + sb.append(" "); + } + removeLast(sb); + return sb.toString(); + } + + /** + * Removes the last character from the given {@link StringBuilder} if it is non-empty. + * + * @param sb The string builder to trim. + */ + private static void removeLast(final StringBuilder sb) { + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + } + + /** + * Returns resource descriptors for the JVM, Maven installation, SCM source, and project dependencies. + * + * @return A list of resolved build dependencies. + * @throws MojoExecutionException If any dependency cannot be resolved or hashed. + */ + private List getBuildDependencies() throws MojoExecutionException { + List dependencies = new ArrayList<>(); + try { + dependencies.add(BuildToolDescriptors.jvm(Paths.get(System.getProperty("java.home")))); + dependencies.add(BuildToolDescriptors.maven(runtimeInformation.getMavenVersion(), mavenHome.toPath())); + dependencies.add(getScmDescriptor()); + } catch (IOException e) { + throw new MojoExecutionException(e); + } + dependencies.addAll(getProjectDependencies()); + return dependencies; + } + + /** + * Returns resource descriptors for all resolved project dependencies. + * + * @return A list of resource descriptors for the project's resolved artifacts. + * @throws MojoExecutionException If a dependency artifact cannot be described. + */ + private List getProjectDependencies() throws MojoExecutionException { + List dependencies = new ArrayList<>(); + for (Artifact artifact : project.getArtifacts()) { + dependencies.add(ArtifactUtils.toResourceDescriptor(artifact)); + } + return dependencies; + } + + /** + * Returns a resource descriptor for the current SCM source, including the URI and Git commit digest. + * + * @return A resource descriptor for the SCM source. + * @throws IOException If the current branch cannot be determined. + * @throws MojoExecutionException If the SCM revision cannot be retrieved. + */ + private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionException { + ResourceDescriptor scmDescriptor = new ResourceDescriptor(); + String scmUri = GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath()); + scmDescriptor.setUri(scmUri); + // Compute the revision + Map digest = new HashMap<>(); + digest.put("gitCommit", getScmRevision()); + scmDescriptor.setDigest(digest); + return scmDescriptor; + } + + /** + * Creates and returns an SCM repository from the configured connection URL. + * + * @return The SCM repository. + * @throws MojoExecutionException If the SCM repository cannot be created. + */ + private ScmRepository getScmRepository() throws MojoExecutionException { + try { + return scmManager.makeScmRepository(scmConnectionUrl); + } catch (ScmException e) { + throw new MojoExecutionException("Failed to create SCM repository", e); + } + } + + /** + * Returns the current SCM revision (commit hash) for the configured SCM directory. + * + * @return The current SCM revision string. + * @throws MojoExecutionException If the revision cannot be retrieved from SCM. + */ + private String getScmRevision() throws MojoExecutionException { + ScmRepository scmRepository = getScmRepository(); + CommandParameters commandParameters = new CommandParameters(); + try { + InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), + new ScmFileSet(scmDirectory), commandParameters); + + return getScmRevision(result); + } catch (ScmException e) { + throw new MojoExecutionException("Failed to retrieve SCM revision", e); + } + } + + /** + * Extracts the revision string from an SCM info result. + * + * @param result The SCM info result. + * @return The revision string. + * @throws MojoExecutionException If the result is unsuccessful or contains no revision. + */ + private String getScmRevision(final InfoScmResult result) throws MojoExecutionException { + if (!result.isSuccess()) { + throw new MojoExecutionException("Failed to retrieve SCM revision: " + result.getProviderMessage()); + } + + if (result.getInfoItems() == null || result.getInfoItems().isEmpty()) { + throw new MojoExecutionException("No SCM revision information found for " + scmDirectory); + } + + InfoItem item = result.getInfoItems().get(0); + + String revision = item.getRevision(); + if (revision == null) { + throw new MojoExecutionException("Empty SCM revision returned for " + scmDirectory); + } + return revision; + } + + /** + * Returns build metadata derived from the current Maven session, including start and finish timestamps. + * + * @return The build metadata. + */ + private BuildMetadata getBuildMetadata() { + OffsetDateTime startedOn = session.getStartTime().toInstant().atOffset(ZoneOffset.UTC); + OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC); + return new BuildMetadata(session.getRequest().getBuilderId(), startedOn, finishedOn); + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java new file mode 100644 index 000000000..843bc0e17 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Inputs that define the build: the build type, external and internal parameters, and resolved dependencies. + * + *

Specifies everything that influenced the build output. Together with {@link RunDetails}, it forms the complete + * {@link Provenance} record.

+ * + * @see SLSA v1.2 Specification + */ +public class BuildDefinition { + + /** URI indicating what type of build was performed. */ + @JsonProperty("buildType") + private String buildType = "https://commons.apache.org/builds/0.1.0"; + + /** Inputs passed to the build. */ + @JsonProperty("externalParameters") + private Map externalParameters = new HashMap<>(); + + /** Parameters set by the build platform. */ + @JsonProperty("internalParameters") + private Map internalParameters = new HashMap<>(); + + /** Artifacts the build depends on, specified by URI and digest. */ + @JsonProperty("resolvedDependencies") + private List resolvedDependencies; + + /** Creates a new BuildDefinition instance with the default build type. */ + public BuildDefinition() { + } + + /** + * Creates a new BuildDefinition with the given build type and external parameters. + * + * @param buildType URI indicating what type of build was performed + * @param externalParameters inputs passed to the build + */ + public BuildDefinition(String buildType, Map externalParameters) { + this.buildType = buildType; + this.externalParameters = externalParameters; + } + + /** + * Returns the URI indicating what type of build was performed. + * + *

Determines the meaning of {@code externalParameters} and {@code internalParameters}.

+ * + * @return the build type URI + */ + public String getBuildType() { + return buildType; + } + + /** + * Sets the URI indicating what type of build was performed. + * + * @param buildType the build type URI + */ + public void setBuildType(String buildType) { + this.buildType = buildType; + } + + /** + * Returns the inputs passed to the build, such as command-line arguments or environment variables. + * + * @return the external parameters map, or {@code null} if not set + */ + public Map getExternalParameters() { + return externalParameters; + } + + /** + * Sets the inputs passed to the build. + * + * @param externalParameters the external parameters map + */ + public void setExternalParameters(Map externalParameters) { + this.externalParameters = externalParameters; + } + + /** + * Returns the artifacts the build depends on, such as sources, dependencies, build tools, and base images, + * specified by URI and digest. + * + * @return the internal parameters map, or {@code null} if not set + */ + public Map getInternalParameters() { + return internalParameters; + } + + /** + * Sets the artifacts the build depends on. + * + * @param internalParameters the internal parameters map + */ + public void setInternalParameters(Map internalParameters) { + this.internalParameters = internalParameters; + } + + /** + * Returns the materials that influenced the build. + * + *

Considered incomplete unless resolved materials are present.

+ * + * @return the list of resolved dependencies, or {@code null} if not set + */ + public List getResolvedDependencies() { + return resolvedDependencies; + } + + /** + * Sets the materials that influenced the build. + * + * @param resolvedDependencies the list of resolved dependencies + */ + public void setResolvedDependencies(List resolvedDependencies) { + this.resolvedDependencies = resolvedDependencies; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BuildDefinition that = (BuildDefinition) o; + return Objects.equals(buildType, that.buildType) + && Objects.equals(externalParameters, that.externalParameters) + && Objects.equals(internalParameters, that.internalParameters) + && Objects.equals(resolvedDependencies, that.resolvedDependencies); + } + + @Override + public int hashCode() { + return Objects.hash(buildType, externalParameters, internalParameters, resolvedDependencies); + } + + @Override + public String toString() { + return "BuildDefinition{" + + "buildType='" + buildType + '\'' + + ", externalParameters=" + externalParameters + + ", internalParameters=" + internalParameters + + ", resolvedDependencies=" + resolvedDependencies + + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java new file mode 100644 index 000000000..345eb91ee --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Metadata about a build invocation: its identifier and start and finish timestamps. + * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BuildMetadata { + + /** Identifier for this build invocation. */ + @JsonProperty("invocationId") + private String invocationId; + + /** Timestamp when the build started. */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + @JsonProperty("startedOn") + private OffsetDateTime startedOn; + + /** Timestamp when the build completed. */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + @JsonProperty("finishedOn") + private OffsetDateTime finishedOn; + + /** Creates a new BuildMetadata instance. */ + public BuildMetadata() { + } + + /** + * Creates a new BuildMetadata instance with all fields set. + * + * @param invocationId identifier for this build invocation + * @param startedOn timestamp when the build started + * @param finishedOn timestamp when the build completed + */ + public BuildMetadata(String invocationId, OffsetDateTime startedOn, OffsetDateTime finishedOn) { + this.invocationId = invocationId; + this.startedOn = startedOn; + this.finishedOn = finishedOn; + } + + /** + * Returns the identifier for this build invocation. + * + *

Useful for finding associated logs or other ad-hoc analysis. The exact meaning and format is defined by the + * builder and is treated as opaque and case-sensitive. The value SHOULD be globally unique.

+ * + * @return the invocation identifier, or {@code null} if not set + */ + public String getInvocationId() { + return invocationId; + } + + /** + * Sets the identifier for this build invocation. + * + * @param invocationId the invocation identifier + */ + public void setInvocationId(String invocationId) { + this.invocationId = invocationId; + } + + /** + * Returns the timestamp of when the build started, serialized as RFC 3339 in UTC ({@code "Z"} suffix). + * + * @return the start timestamp, or {@code null} if not set + */ + public OffsetDateTime getStartedOn() { + return startedOn; + } + + /** + * Sets the timestamp of when the build started. + * + * @param startedOn the start timestamp + */ + public void setStartedOn(OffsetDateTime startedOn) { + this.startedOn = startedOn; + } + + /** + * Returns the timestamp of when the build completed, serialized as RFC 3339 in UTC ({@code "Z"} suffix). + * + * @return the completion timestamp, or {@code null} if not set + */ + public OffsetDateTime getFinishedOn() { + return finishedOn; + } + + /** + * Sets the timestamp of when the build completed. + * + * @param finishedOn the completion timestamp + */ + public void setFinishedOn(OffsetDateTime finishedOn) { + this.finishedOn = finishedOn; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BuildMetadata)) { + return false; + } + BuildMetadata that = (BuildMetadata) o; + return Objects.equals(invocationId, that.invocationId) && Objects.equals(startedOn, that.startedOn) && Objects.equals(finishedOn, that.finishedOn); + } + + @Override + public int hashCode() { + return Objects.hash(invocationId, startedOn, finishedOn); + } + + @Override + public String toString() { + return "BuildMetadata{invocationId='" + invocationId + "', startedOn=" + startedOn + ", finishedOn=" + finishedOn + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java new file mode 100644 index 000000000..36e0f1a89 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Entity that executed the build and is trusted to have correctly performed the operation and populated the provenance. + * + * @see SLSA v1.2 Specification + */ +public class Builder { + + /** Identifier URI of the builder. */ + @JsonProperty("id") + private String id = "https://commons.apache.org/builds/0.1.0"; + + /** Orchestrator dependencies that may affect provenance generation. */ + @JsonProperty("builderDependencies") + private List builderDependencies = new ArrayList<>(); + + /** Map of build platform component names to their versions. */ + @JsonProperty("version") + private Map version = new HashMap<>(); + + /** Creates a new Builder instance. */ + public Builder() { + } + + /** + * Returns the identifier of the builder. + * + * @return the builder identifier URI + */ + public String getId() { + return id; + } + + /** + * Sets the identifier of the builder. + * + * @param id the builder identifier URI + */ + public void setId(String id) { + this.id = id; + } + + /** + * Returns orchestrator dependencies that do not run within the build workload and do not affect the build output, + * but may affect provenance generation or security guarantees. + * + * @return the list of builder dependencies, or {@code null} if not set + */ + public List getBuilderDependencies() { + return builderDependencies; + } + + /** + * Sets the orchestrator dependencies that may affect provenance generation or security guarantees. + * + * @param builderDependencies the list of builder dependencies + */ + public void setBuilderDependencies(List builderDependencies) { + this.builderDependencies = builderDependencies; + } + + /** + * Returns a map of build platform component names to their versions. + * + * @return the version map, or {@code null} if not set + */ + public Map getVersion() { + return version; + } + + /** + * Sets the map of build platform component names to their versions. + * + * @param version the version map + */ + public void setVersion(Map version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Builder)) { + return false; + } + Builder that = (Builder) o; + return Objects.equals(id, that.id) + && Objects.equals(builderDependencies, that.builderDependencies) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, builderDependencies, version); + } + + @Override + public String toString() { + return "Builder{id='" + id + "', builderDependencies=" + builderDependencies + ", version=" + version + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java new file mode 100644 index 000000000..884246006 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Root predicate of an SLSA v1.2 provenance attestation, describing what was built and how. + * + *

Combines a {@link BuildDefinition} (the inputs) with {@link RunDetails} (the execution context). Intended to be + * used as the {@code predicate} field of an in-toto {@link Statement}.

+ * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Provenance { + + /** Predicate type URI used in the in-toto {@link Statement} wrapping this provenance. */ + public static final String PREDICATE_TYPE = "https://slsa.dev/provenance/v1"; + + /** Inputs that defined the build. */ + @JsonProperty("buildDefinition") + private BuildDefinition buildDefinition; + + /** Details about the build invocation. */ + @JsonProperty("runDetails") + private RunDetails runDetails; + + /** Creates a new Provenance instance. */ + public Provenance() { + } + + /** + * Creates a new Provenance with the given build definition and run details. + * + * @param buildDefinition inputs that defined the build + * @param runDetails details about the build invocation + */ + public Provenance(BuildDefinition buildDefinition, RunDetails runDetails) { + this.buildDefinition = buildDefinition; + this.runDetails = runDetails; + } + + /** + * Returns the build definition describing all inputs that produced the build output. + * + *

Includes source code, dependencies, build tools, base images, and other materials.

+ * + * @return the build definition, or {@code null} if not set + */ + public BuildDefinition getBuildDefinition() { + return buildDefinition; + } + + /** + * Sets the build definition describing all inputs that produced the build output. + * + * @param buildDefinition the build definition + */ + public void setBuildDefinition(BuildDefinition buildDefinition) { + this.buildDefinition = buildDefinition; + } + + /** + * Returns the details about the invocation of the build tool and the environment in which it was run. + * + * @return the run details, or {@code null} if not set + */ + public RunDetails getRunDetails() { + return runDetails; + } + + /** + * Sets the details about the invocation of the build tool and the environment in which it was run. + * + * @param runDetails the run details + */ + public void setRunDetails(RunDetails runDetails) { + this.runDetails = runDetails; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Provenance that = (Provenance) o; + return Objects.equals(buildDefinition, that.buildDefinition) && Objects.equals(runDetails, that.runDetails); + } + + @Override + public int hashCode() { + return Objects.hash(buildDefinition, runDetails); + } + + @Override + public String toString() { + return "Provenance{buildDefinition=" + buildDefinition + ", runDetails=" + runDetails + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java new file mode 100644 index 000000000..55333f220 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Description of an artifact or resource referenced in the build, identified by URI and cryptographic digest. + * + *

Used to represent inputs to, outputs from, or byproducts of the build process.

+ * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ResourceDescriptor { + + /** Human-readable name of the resource. */ + @JsonProperty("name") + private String name; + + /** URI identifying the resource. */ + @JsonProperty("uri") + private String uri; + + /** Map of digest algorithm names to hex-encoded values. */ + @JsonProperty("digest") + private Map digest; + + /** Raw contents of the resource, base64-encoded in JSON. */ + @JsonProperty("content") + private byte[] content; + + /** Download URI for the resource, if different from {@link #uri}. */ + @JsonProperty("downloadLocation") + private String downloadLocation; + + /** Media type of the resource. */ + @JsonProperty("mediaType") + private String mediaType; + + /** Additional key-value metadata about the resource. */ + @JsonProperty("annotations") + private Map annotations; + + /** Creates a new ResourceDescriptor instance. */ + public ResourceDescriptor() { + } + + /** + * Creates a new ResourceDescriptor with the given URI and digest. + * + * @param uri URI identifying the resource + * @param digest map of digest algorithm names to their hex-encoded values + */ + public ResourceDescriptor(String uri, Map digest) { + this.uri = uri; + this.digest = digest; + } + + /** + * Returns the name of the resource. + * + * @return the resource name, or {@code null} if not set + */ + public String getName() { + return name; + } + + /** + * Sets the name of the resource. + * + * @param name the resource name + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the URI identifying the resource. + * + * @return the resource URI, or {@code null} if not set + */ + public String getUri() { + return uri; + } + + /** + * Sets the URI identifying the resource. + * + * @param uri the resource URI + */ + public void setUri(String uri) { + this.uri = uri; + } + + /** + * Returns the map of cryptographic digest algorithms to their corresponding hex-encoded values for this resource. + * + *

Common keys include {@code "sha256"} and {@code "sha512"}.

+ * + * @return the digest map, or {@code null} if not set + */ + public Map getDigest() { + return digest; + } + + /** + * Sets the map of cryptographic digest algorithms to their hex-encoded values. + * + * @param digest the digest map + */ + public void setDigest(Map digest) { + this.digest = digest; + } + + /** + * Returns the raw contents of the resource, base64-encoded when serialized to JSON. + * + * @return the resource content, or {@code null} if not set + */ + public byte[] getContent() { + return content; + } + + /** + * Sets the raw contents of the resource. + * + * @param content the resource content + */ + public void setContent(byte[] content) { + this.content = content; + } + + /** + * Returns the download URI for the resource, if different from {@link #getUri()}. + * + * @return the download location URI, or {@code null} if not set + */ + public String getDownloadLocation() { + return downloadLocation; + } + + /** + * Sets the download URI for the resource. + * + * @param downloadLocation the download location URI + */ + public void setDownloadLocation(String downloadLocation) { + this.downloadLocation = downloadLocation; + } + + /** + * Returns the media type of the resource (e.g., {@code "application/octet-stream"}). + * + * @return the media type, or {@code null} if not set + */ + public String getMediaType() { + return mediaType; + } + + /** + * Sets the media type of the resource. + * + * @param mediaType the media type + */ + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + /** + * Returns additional key-value metadata about the resource, such as filename, size, or builder-specific attributes. + * + * @return the annotations map, or {@code null} if not set + */ + public Map getAnnotations() { + return annotations; + } + + /** + * Sets additional key-value metadata about the resource. + * + * @param annotations the annotations map + */ + public void setAnnotations(Map annotations) { + this.annotations = annotations; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourceDescriptor that = (ResourceDescriptor) o; + return Objects.equals(uri, that.uri) && Objects.equals(digest, that.digest); + } + + @Override + public int hashCode() { + return Objects.hash(uri, digest); + } + + @Override + public String toString() { + return "ResourceDescriptor{uri='" + uri + '\'' + ", digest=" + digest + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java new file mode 100644 index 000000000..ffb118677 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Details about the build invocation: the builder identity, execution metadata, and any byproduct artifacts. + * + * @see SLSA v1.2 Specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RunDetails { + + /** Entity that executed the build. */ + @JsonProperty("builder") + private Builder builder; + + /** Metadata about the build invocation. */ + @JsonProperty("metadata") + private BuildMetadata metadata; + + /** Artifacts produced as a side effect of the build. */ + @JsonProperty("byproducts") + private List byproducts; + + /** Creates a new RunDetails instance. */ + public RunDetails() { + } + + /** + * Creates a new RunDetails with the given builder and metadata. + * + * @param builder entity that executed the build + * @param metadata metadata about the build invocation + */ + public RunDetails(Builder builder, BuildMetadata metadata) { + this.builder = builder; + this.metadata = metadata; + } + + /** + * Returns the builder that executed the invocation. + * + *

Trusted to have correctly performed the operation and populated this provenance.

+ * + * @return the builder, or {@code null} if not set + */ + public Builder getBuilder() { + return builder; + } + + /** + * Sets the builder that executed the invocation. + * + * @param builder the builder + */ + public void setBuilder(Builder builder) { + this.builder = builder; + } + + /** + * Returns the metadata about the build invocation, including its identifier and timing. + * + * @return the build metadata, or {@code null} if not set + */ + public BuildMetadata getMetadata() { + return metadata; + } + + /** + * Sets the metadata about the build invocation. + * + * @param metadata the build metadata + */ + public void setMetadata(BuildMetadata metadata) { + this.metadata = metadata; + } + + /** + * Returns artifacts produced as a side effect of the build that are not the primary output. + * + * @return the list of byproduct artifacts, or {@code null} if not set + */ + public List getByproducts() { + return byproducts; + } + + /** + * Sets the artifacts produced as a side effect of the build that are not the primary output. + * + * @param byproducts the list of byproduct artifacts + */ + public void setByproducts(List byproducts) { + this.byproducts = byproducts; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RunDetails that = (RunDetails) o; + return Objects.equals(builder, that.builder) && Objects.equals(metadata, that.metadata) && Objects.equals(byproducts, that.byproducts); + } + + @Override + public int hashCode() { + return Objects.hash(builder, metadata, byproducts); + } + + @Override + public String toString() { + return "RunDetails{builder=" + builder + ", metadata=" + metadata + ", byproducts=" + byproducts + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java new file mode 100644 index 000000000..88aeb8ae8 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java @@ -0,0 +1,122 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * In-toto v1 attestation envelope that binds a set of subject artifacts to an SLSA provenance predicate. + * + * @see in-toto Statement v1 + */ +public class Statement { + + /** The in-toto statement schema URI. */ + @JsonProperty("_type") + public static final String TYPE = "https://in-toto.io/Statement/v1"; + + /** Software artifacts that the attestation applies to. */ + @JsonProperty("subject") + private List subject; + + /** URI identifying the type of the predicate. */ + @JsonProperty("predicateType") + private String predicateType; + + /** The provenance predicate. */ + @JsonProperty("predicate") + private Provenance predicate; + + /** Creates a new Statement instance. */ + public Statement() { + } + + /** + * Returns the set of software artifacts that the attestation applies to. + * + *

Each element represents a single artifact. Artifacts are matched purely by digest, regardless of content + * type.

+ * + * @return the list of subject artifacts, or {@code null} if not set + */ + public List getSubject() { + return subject; + } + + /** + * Sets the set of software artifacts that the attestation applies to. + * + * @param subject the list of subject artifacts + */ + public void setSubject(List subject) { + this.subject = subject; + } + + /** + * Returns the URI identifying the type of the predicate. + * + * @return the predicate type URI, or {@code null} if no predicate has been set + */ + public String getPredicateType() { + return predicateType; + } + + /** + * Returns the provenance predicate. + * + *

Unset is treated the same as set-but-empty. May be omitted if {@code predicateType} fully describes the + * predicate.

+ * + * @return the provenance predicate, or {@code null} if not set + */ + public Provenance getPredicate() { + return predicate; + } + + /** + * Sets the provenance predicate and automatically assigns {@code predicateType} to the SLSA provenance v1 URI. + * + * @param predicate the provenance predicate + */ + public void setPredicate(Provenance predicate) { + this.predicate = predicate; + this.predicateType = Provenance.PREDICATE_TYPE; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Statement)) { + return false; + } + Statement statement = (Statement) o; + return Objects.equals(subject, statement.subject) && Objects.equals(predicateType, statement.predicateType) && Objects.equals(predicate, + statement.predicate); + } + + @Override + public int hashCode() { + return Objects.hash(subject, predicateType, predicate); + } + + @Override + public String toString() { + return "Statement{_type='" + TYPE + "', subject=" + subject + ", predicateType='" + predicateType + "', predicate=" + predicate + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/package-info.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/package-info.java new file mode 100644 index 000000000..69a5ce287 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/package-info.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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. + */ + +/** + * SLSA 1.2 Build Attestation Models. + * + *

This package provides Jackson-annotated model classes that implement the Supply-chain Levels for Software Artifacts + * (SLSA) v1.2 specification.

+ * + *

Overview

+ * + *

SLSA is a framework for evaluating and improving the security posture of build systems. SLSA v1.2 defines a standard for recording build provenance: + * information about how software artifacts were produced.

+ * + * @see SLSA v1.2 Specification + * @see In-toto Attestation Framework + * @see Jackson JSON processor + */ +package org.apache.commons.release.plugin.slsa.v1_2; + diff --git a/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java new file mode 100644 index 000000000..6a6c3f5bf --- /dev/null +++ b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +import java.nio.file.Path; + +import org.codehaus.plexus.ContainerConfiguration; +import org.codehaus.plexus.DefaultContainerConfiguration; +import org.codehaus.plexus.DefaultPlexusContainer; +import org.codehaus.plexus.PlexusConstants; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.PlexusContainerException; +import org.codehaus.plexus.classworlds.ClassWorld; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.LocalRepositoryManager; +import org.eclipse.aether.repository.RepositoryPolicy; +import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; + +/** + * Utilities to instantiate Mojos in a test environment. + */ +public final class MojoUtils { + + private static ContainerConfiguration setupContainerConfiguration() { + ClassWorld classWorld = + new ClassWorld("plexus.core", Thread.currentThread().getContextClassLoader()); + return new DefaultContainerConfiguration() + .setClassWorld(classWorld) + .setClassPathScanning(PlexusConstants.SCANNING_INDEX) + .setAutoWiring(true) + .setName("maven"); + } + + public static PlexusContainer setupContainer() throws PlexusContainerException { + return new DefaultPlexusContainer(setupContainerConfiguration()); + } + + public static RepositorySystemSession createRepositorySystemSession( + PlexusContainer container, Path localRepositoryPath) throws ComponentLookupException, RepositoryException { + LocalRepositoryManagerFactory factory = container.lookup(LocalRepositoryManagerFactory.class, "simple"); + DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + LocalRepositoryManager manager = + factory.newInstance(repoSession, new LocalRepository(localRepositoryPath.toFile())); + repoSession.setLocalRepositoryManager(manager); + // Default policies + repoSession.setUpdatePolicy(RepositoryPolicy.UPDATE_POLICY_DAILY); + repoSession.setChecksumPolicy(RepositoryPolicy.CHECKSUM_POLICY_WARN); + return repoSession; + } + + private MojoUtils() { + } +} diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java new file mode 100644 index 000000000..5f7cb6a3f --- /dev/null +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.mojos; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; + +import org.apache.commons.release.plugin.internal.MojoUtils; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.bridge.MavenRepositorySystem; +import org.apache.maven.execution.DefaultMavenExecutionRequest; +import org.apache.maven.execution.DefaultMavenExecutionResult; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenExecutionResult; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Model; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.rtinfo.RuntimeInformation; +import org.apache.maven.scm.manager.ScmManager; +import org.codehaus.plexus.PlexusContainer; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.RepositorySystemSession; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +public class BuildAttestationMojoTest { + + @TempDir + private static Path localRepositoryPath; + + private static PlexusContainer container; + private static RepositorySystemSession repoSession; + + @BeforeAll + static void setup() throws Exception { + container = MojoUtils.setupContainer(); + repoSession = MojoUtils.createRepositorySystemSession(container, localRepositoryPath); + } + + private static MavenExecutionRequest createMavenExecutionRequest() { + DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setStartTime(new Date()); + return request; + } + + @SuppressWarnings("deprecation") + private static MavenSession createMavenSession(MavenExecutionRequest request, MavenExecutionResult result) { + return new MavenSession(container, repoSession, request, result); + } + + private static BuildAttestationMojo createBuildAttestationMojo(MavenProject project, MavenProjectHelper projectHelper) throws ComponentLookupException { + ScmManager scmManager = container.lookup(ScmManager.class); + RuntimeInformation runtimeInfo = container.lookup(RuntimeInformation.class); + return new BuildAttestationMojo(project, scmManager, runtimeInfo, + createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); + } + + private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) throws ComponentLookupException { + MavenProject project = new MavenProject(new Model()); + Artifact artifact = repoSystem.createArtifact("groupId", "artifactId", "1.2.3", null, "jar"); + project.setArtifact(artifact); + project.setGroupId("groupId"); + project.setArtifactId("artifactId"); + project.setVersion("1.2.3"); + // Attach a couple of artifacts + projectHelper.attachArtifact(project, "pom", null, new File("src/test/resources/artifacts/artifact-pom.txt")); + artifact.setFile(new File("src/test/resources/artifacts/artifact-jar.txt")); + return project; + } + + @Test + void attestationTest() throws Exception { + MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); + MavenRepositorySystem repoSystem = container.lookup(MavenRepositorySystem.class); + MavenProject project = createMavenProject(projectHelper, repoSystem); + + BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); + mojo.setOutputDirectory(new File("target/attestations")); + mojo.setScmDirectory(new File(".")); + mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-lang.git"); + mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); + mojo.execute(); + + Artifact attestation = project.getAttachedArtifacts().stream() + .filter(a -> "intoto.json".equals(a.getType())) + .findFirst() + .orElseThrow(() -> new AssertionError("No intoto.json artifact attached to project")); + String json = new String(Files.readAllBytes(attestation.getFile().toPath()), StandardCharsets.UTF_8); + + String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; + String javaVersion = System.getProperty("java.version"); + + assertThatJson(json) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> { + assertThatJson(dep).node("name").isEqualTo("JDK"); + assertThatJson(dep).node("annotations.version").isEqualTo(javaVersion); + }); + + assertThatJson(json) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> assertThatJson(dep).node("name").isEqualTo("Maven")); + + assertThatJson(json) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> assertThatJson(dep).node("uri").isString().startsWith("git+https://github.com/apache/commons-lang.git")); + } +} diff --git a/src/test/resources/artifacts/artifact-jar.txt b/src/test/resources/artifacts/artifact-jar.txt new file mode 100644 index 000000000..103a21e64 --- /dev/null +++ b/src/test/resources/artifacts/artifact-jar.txt @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: Apache-2.0 +A mock-up of a JAR file \ No newline at end of file diff --git a/src/test/resources/artifacts/artifact-pom.txt b/src/test/resources/artifacts/artifact-pom.txt new file mode 100644 index 000000000..48d1bbc32 --- /dev/null +++ b/src/test/resources/artifacts/artifact-pom.txt @@ -0,0 +1,2 @@ +// SPDX-License-Identifier: Apache-2.0 +A mock-up of a POM file \ No newline at end of file From 54e34b72f3cde12acadccbb94ac7973dfe65a79e Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 30 Mar 2026 20:35:09 +0200 Subject: [PATCH 02/51] Add documentation of `buildType` --- .../plugin/internal/BuildToolDescriptors.java | 24 +++- .../plugin/mojos/BuildAttestationMojo.java | 3 +- src/site/markdown/slsa/v0.1.0.md | 131 ++++++++++++++++++ 3 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 src/site/markdown/slsa/v0.1.0.md diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java index 15be8d73e..5dac9a192 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java @@ -48,8 +48,15 @@ public static ResourceDescriptor jvm(Path javaHome) throws IOException { Map digest = new HashMap<>(); digest.put("gitTree", GitUtils.gitTree(javaHome)); descriptor.setDigest(digest); - String[] propertyNames = {"java.version", "java.vendor", "java.vendor.version", "java.vm.name", "java.vm.version", "java.vm.vendor", - "java.runtime.name", "java.runtime.version", "java.specification.version"}; + String[] propertyNames = { + "java.version", "java.version.date", + "java.vendor", "java.vendor.url", "java.vendor.version", + "java.home", + "java.vm.specification.version", "java.vm.specification.vendor", "java.vm.specification.name", + "java.vm.version", "java.vm.vendor", "java.vm.name", + "java.specification.version", "java.specification.maintenance.version", + "java.specification.vendor", "java.specification.name", + }; Map annotations = new HashMap<>(); for (String prop : propertyNames) { annotations.put(prop.substring("java.".length()), System.getProperty(prop)); @@ -61,12 +68,17 @@ public static ResourceDescriptor jvm(Path javaHome) throws IOException { /** * Creates a {@link ResourceDescriptor} for the Maven installation used during the build. * - * @param version Maven version string - * @param mavenHome path to the Maven home directory + *

{@code build.properties} resides in a JAR inside {@code ${maven.home}/lib/}, which is loaded by Maven's Core Classloader. + * Plugin code runs in an isolated Plugin Classloader, which does see that resources. Therefore, we need to pass the classloader from a class from + * Maven Core, such as {@link org.apache.maven.rtinfo.RuntimeInformation}.

+ * + * @param version Maven version string + * @param mavenHome path to the Maven home directory + * @param coreClassLoader a classloader from Maven's Core Classloader realm, used to load core resources * @return a descriptor for the Maven installation * @throws IOException if hashing the Maven home directory fails */ - public static ResourceDescriptor maven(String version, Path mavenHome) throws IOException { + public static ResourceDescriptor maven(String version, Path mavenHome, ClassLoader coreClassLoader) throws IOException { ResourceDescriptor descriptor = new ResourceDescriptor(); descriptor.setName("Maven"); descriptor.setUri("pkg:maven/org.apache.maven/apache-maven@" + version); @@ -74,7 +86,7 @@ public static ResourceDescriptor maven(String version, Path mavenHome) throws IO digest.put("gitTree", GitUtils.gitTree(mavenHome)); descriptor.setDigest(digest); Properties buildProps = new Properties(); - try (InputStream in = BuildToolDescriptors.class.getResourceAsStream("/org/apache/maven/messages/build.properties")) { + try (InputStream in = coreClassLoader.getResourceAsStream("org/apache/maven/messages/build.properties")) { if (in != null) { buildProps.load(in); } diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 0260e22e1..6841193db 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -342,7 +342,8 @@ private List getBuildDependencies() throws MojoExecutionExce List dependencies = new ArrayList<>(); try { dependencies.add(BuildToolDescriptors.jvm(Paths.get(System.getProperty("java.home")))); - dependencies.add(BuildToolDescriptors.maven(runtimeInformation.getMavenVersion(), mavenHome.toPath())); + dependencies.add(BuildToolDescriptors.maven(runtimeInformation.getMavenVersion(), mavenHome.toPath(), + runtimeInformation.getClass().getClassLoader())); dependencies.add(getScmDescriptor()); } catch (IOException e) { throw new MojoExecutionException(e); diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md new file mode 100644 index 000000000..b9a569ef2 --- /dev/null +++ b/src/site/markdown/slsa/v0.1.0.md @@ -0,0 +1,131 @@ + + +# Build Type: Apache Commons Maven Release + +```jsonc +"buildType": "https://commons.apache.org/proper/commons-release-plugin/slsa/v0.1.0" +``` + +This is a [SLSA Build Provenance](https://slsa.dev/spec/v1.2/build-provenance) build type +that describes releases produced by Apache Commons PMC release managers running Maven on their own equipment. + +## Build definition + +Artifacts are generated by a single Maven execution, typically of the form: + +```shell +mvn -Prelease deploy +``` + +The provenance is recorded by the `build-attestation` goal of the +`commons-release-plugin`, which runs in the `verify` phase. + +### External parameters + +External parameters capture everything supplied by the release manager at invocation time. +All parameters are captured from the running Maven session. + +| Parameter | Type | Description | +|-------------------------|----------|-------------------------------------------------------------------------| +| `maven.goals` | string[] | The list of Maven goals passed on the command line (e.g. `["deploy"]`). | +| `maven.profiles` | string[] | The list of active profiles passed via `-P` (e.g. `["release"]`). | +| `maven.user.properties` | object | User-defined properties passed via `-D` flags. | +| `maven.cmdline` | string | The reconstructed Maven command line. | +| `jvm.args` | string[] | JVM input arguments. | +| `env` | object | A filtered subset of environment variables: `TZ` and locale variables. | + +### Internal parameters + +No internal parameters are recorded for this build type. + +### Resolved dependencies + +The `resolvedDependencies` list captures all inputs that contributed to the build output. +It always contains the following entries, in order: + +#### JDK + +Represents the Java Development Kit used to run Maven (`"name": "JDK"`). +To allow verification of the JDK's integrity, a `gitTree` digest is computed over the `java.home` directory. + +The following annotations are recorded from [ +`System.getProperties()`](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/System.html#getProperties()): + +| Annotation key | System property | Description | +|-------------------------------------|------------------------------------------|--------------------------------------------------------------------------| +| `version` | `java.version` | Java Runtime Environment version. | +| `version.date` | `java.version.date` | Java Runtime Environment version date, in ISO-8601 YYYY-MM-DD format. | +| `vendor` | `java.vendor` | Java Runtime Environment vendor. | +| `vendor.url` | `java.vendor.url` | Java vendor URL. | +| `vendor.version` | `java.vendor.version` | Java vendor version _(optional)_. | +| `home` | `java.home` | Java installation directory. | +| `vm.specification.version` | `java.vm.specification.version` | Java Virtual Machine specification version. | +| `vm.specification.vendor` | `java.vm.specification.vendor` | Java Virtual Machine specification vendor. | +| `vm.specification.name` | `java.vm.specification.name` | Java Virtual Machine specification name. | +| `vm.version` | `java.vm.version` | Java Virtual Machine implementation version. | +| `vm.vendor` | `java.vm.vendor` | Java Virtual Machine implementation vendor. | +| `vm.name` | `java.vm.name` | Java Virtual Machine implementation name. | +| `specification.version` | `java.specification.version` | Java Runtime Environment specification version. | +| `specification.maintenance.version` | `java.specification.maintenance.version` | Java Runtime Environment specification maintenance version _(optional)_. | +| `specification.vendor` | `java.specification.vendor` | Java Runtime Environment specification vendor. | +| `specification.name` | `java.specification.name` | Java Runtime Environment specification name. | + +#### Maven + +Represents the Maven installation used to run the build (`"name": "Maven"`). +To allow verification of the installation's integrity, a `gitTree` hash is computed over the `maven.home` directory. + +The `uri` key contains the Package URL of the Maven distribution, as published to Maven Central. + +The following annotations are sourced from Maven's `build.properties`, bundled inside the Maven distribution. +They are only present if the resource is accessible from Maven's Core Classloader at runtime. + +| Annotation key | Description | +|-------------------------|--------------------------------------------------------------| +| `distributionId` | The ID of the Maven distribution. | +| `distributionName` | The full name of the Maven distribution. | +| `distributionShortName` | The short name of the Mavendistribution. | +| `buildNumber` | The Git commit hash from which this Maven release was built. | +| `version` | The Maven version string. | + +#### Source repository + +Represents the source code being built. +The URI follows +the [SPDX Download Location](https://spdx.github.io/spdx-spec/v2.3/package-information/#77-package-download-location-field) +format. + +#### Project dependencies + +One entry per resolved Maven dependency (compile + runtime scope), as declared in the project's POM. +These are appended after the build tool entries above. + +| Field | Value | +|-----------------|-----------------------------------------------------| +| `name` | Artifact filename, e.g. `commons-lang3-3.14.0.jar`. | +| `uri` | Package URL. | +| `digest.sha256` | SHA-256 hex digest of the artifact file on disk. | + +## Run details + +### Builder + +The `builder.id` is always `https://commons.apache.org/builds/0.1.0`. +It represents the commons-release-plugin acting as the build platform. + +## Subjects + +The attestation covers all artifacts attached to the Maven project at the time the `verify` phase runs: +the primary artifact (e.g. the JAR) and any attached artifacts (e.g. sources JAR, javadoc JAR, POM). + +| Field | Value | +|-----------------|------------------------------------------| +| `name` | Artifact filename. | +| `uri` | Package URL. | +| `digest.sha256` | SHA-256 hex digest of the artifact file. | + +## Version history + +### v0.1.0 + +Initial version. \ No newline at end of file From f005ea1de32e9142adebff215490b87f601d655c Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 2 Apr 2026 15:07:49 +0200 Subject: [PATCH 03/51] Temporarily change version to publish snapshot --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e0c6767e7..d07c6218a 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,8 @@ commons-release-plugin maven-plugin - 1.9.3-SNAPSHOT + + 1.9.3.slsa-SNAPSHOT Apache Commons Release Plugin Apache Maven Mojo for Apache Commons Release tasks. From 5167d7397f88b5755a6c2d071ee2439456f408bb Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 3 Apr 2026 19:10:18 +0200 Subject: [PATCH 04/51] fix: output attestation in JSON Line format --- .../commons/release/plugin/mojos/BuildAttestationMojo.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 6841193db..030f6587d 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -73,7 +73,7 @@ public class BuildAttestationMojo extends AbstractMojo { /** The file extension for in-toto attestation files. */ - private static final String ATTESTATION_EXTENSION = "intoto.json"; + private static final String ATTESTATION_EXTENSION = "intoto.jsonl"; /** Shared Jackson object mapper for serializing attestation statements. */ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -237,7 +237,8 @@ private void writeStatement(final Statement statement) throws MojoExecutionExcep final Path artifactPath = outputPath.resolve(ArtifactUtils.getFileName(mainArtifact, ATTESTATION_EXTENSION)); getLog().info("Writing attestation statement to: " + artifactPath); try (OutputStream os = Files.newOutputStream(artifactPath)) { - OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValue(os, statement); + OBJECT_MAPPER.writeValue(os, statement); + os.write('\n'); } catch (IOException e) { throw new MojoExecutionException("Could not write attestation statement to: " + artifactPath, e); } From 1ace65c7f76b447bd63f9c39d3244d86c940de82 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 3 Apr 2026 19:21:22 +0200 Subject: [PATCH 05/51] fix: disable Jackson auto-close option --- .../commons/release/plugin/mojos/BuildAttestationMojo.java | 2 ++ .../release/plugin/mojos/BuildAttestationMojoTest.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 030f6587d..254779981 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -33,6 +33,7 @@ import javax.inject.Inject; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.apache.commons.release.plugin.internal.ArtifactUtils; @@ -81,6 +82,7 @@ public class BuildAttestationMojo extends AbstractMojo { static { OBJECT_MAPPER.findAndRegisterModules(); OBJECT_MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + OBJECT_MAPPER.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } /** The SCM connection URL for the current project. */ diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 5f7cb6a3f..dae20c43c 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -103,9 +103,9 @@ void attestationTest() throws Exception { mojo.execute(); Artifact attestation = project.getAttachedArtifacts().stream() - .filter(a -> "intoto.json".equals(a.getType())) + .filter(a -> "intoto.jsonl".equals(a.getType())) .findFirst() - .orElseThrow(() -> new AssertionError("No intoto.json artifact attached to project")); + .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); String json = new String(Files.readAllBytes(attestation.getFile().toPath()), StandardCharsets.UTF_8); String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; From 02ad5af3c39fa77be621f52275462a0f2936a080 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sun, 12 Apr 2026 21:07:53 +0200 Subject: [PATCH 06/51] fix: adapt to Commons Code API change --- .../org/apache/commons/release/plugin/internal/GitUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java index 246027c49..c4d4aecec 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -25,6 +25,7 @@ import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.GitIdentifiers; /** * Utilities for Git operations. @@ -46,7 +47,7 @@ public static String gitTree(Path path) throws IOException { throw new IOException("Path is not a directory: " + path); } MessageDigest digest = DigestUtils.getSha1Digest(); - return Hex.encodeHexString(DigestUtils.gitTree(digest, path)); + return Hex.encodeHexString(GitIdentifiers.treeId(digest, path)); } /** From 16f776f54baa725884d8af5b338dc42d5569bf39 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Wed, 15 Apr 2026 14:18:59 +0200 Subject: [PATCH 07/51] feat: add support for DSSE signing Artifacts are signed using the Maven GPG Plugin and the results are wrapped in the DSSE envelope. --- pom.xml | 5 + .../release/plugin/internal/DsseUtils.java | 190 ++++++++++++++++++ .../plugin/mojos/BuildAttestationMojo.java | 164 +++++++++++++-- .../plugin/slsa/v1_2/DsseEnvelope.java | 133 ++++++++++++ .../release/plugin/slsa/v1_2/Signature.java | 108 ++++++++++ 5 files changed, 579 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java create mode 100644 src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java diff --git a/pom.xml b/pom.xml index d07c6218a..26a71467f 100644 --- a/pom.xml +++ b/pom.xml @@ -199,6 +199,11 @@ commons-compress 1.28.0 + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java new file mode 100644 index 000000000..e4e362f01 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -0,0 +1,190 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.IOUtils; +import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope; +import org.apache.commons.release.plugin.slsa.v1_2.Statement; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.gpg.AbstractGpgSigner; +import org.apache.maven.plugins.gpg.GpgSigner; +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; + +/** + * Utility methods for creating DSSE (Dead Simple Signing Envelope) envelopes signed with a PGP key. + */ +public final class DsseUtils { + + /** + * Not instantiable. + */ + private DsseUtils() { + } + + /** + * Creates and prepares a {@link GpgSigner} from the given configuration. + * + *

The returned signer has {@link AbstractGpgSigner#prepare()} already called and is ready + * for use with {@link #signPaeFile(AbstractGpgSigner, Path)}.

+ * + * @param executable path to the GPG executable, or {@code null} to use {@code gpg} from {@code PATH} + * @param defaultKeyring whether to include the default GPG keyring + * @param lockMode GPG lock mode ({@code "once"}, {@code "multiple"}, or {@code "never"}), + * or {@code null} for no explicit lock flag + * @param keyname name or fingerprint of the signing key, or {@code null} for the default key + * @param useAgent whether to use gpg-agent for passphrase management + * @param log Maven logger to attach to the signer + * @return a prepared {@link AbstractGpgSigner} + * @throws MojoFailureException if {@link AbstractGpgSigner#prepare()} fails + */ + public static AbstractGpgSigner createGpgSigner(final String executable, final boolean defaultKeyring, final String lockMode, final String keyname, + final boolean useAgent, final Log log) throws MojoFailureException { + final GpgSigner signer = new GpgSigner(executable); + signer.setDefaultKeyring(defaultKeyring); + signer.setLockMode(lockMode); + signer.setKeyName(keyname); + signer.setUseAgent(useAgent); + signer.setLog(log); + signer.prepare(); + return signer; + } + + /** + * Serializes {@code statement} to JSON, computes the DSSE Pre-Authentication Encoding (PAE), and writes + * the result to {@code buildDirectory/statement.pae}. + * + *

The PAE format is: + * {@code "DSSEv1" SP LEN(payloadType) SP payloadType SP LEN(payload) SP payload}, + * where {@code LEN} is the ASCII decimal byte-length of the operand.

+ * + * @param statement the attestation statement to encode + * @param objectMapper the Jackson mapper used to serialize {@code statement} + * @param buildDirectory directory in which the PAE file is created + * @return path to the written PAE file + * @throws MojoExecutionException if serialization or I/O fails + */ + public static Path writePaeFile(final Statement statement, final ObjectMapper objectMapper, final Path buildDirectory) throws MojoExecutionException { + try { + return writePaeFile(objectMapper.writeValueAsBytes(statement), buildDirectory); + } catch (final JsonProcessingException e) { + throw new MojoExecutionException("Failed to serialize attestation statement", e); + } + } + + /** + * Computes the DSSE Pre-Authentication Encoding (PAE) for {@code statementBytes} and writes it to + * {@code buildDirectory/statement.pae}. + * + *

Use this overload when the statement has already been serialized to bytes, so the same byte array + * can be reused as the {@link DsseEnvelope#setPayload(byte[]) envelope payload} without a second + * serialization pass.

+ * + * @param statementBytes the already-serialized JSON statement bytes to encode + * @param buildDirectory directory in which the PAE file is created + * @return path to the written PAE file + * @throws MojoExecutionException if I/O fails + */ + public static Path writePaeFile(final byte[] statementBytes, final Path buildDirectory) throws MojoExecutionException { + try { + final byte[] payloadTypeBytes = DsseEnvelope.PAYLOAD_TYPE.getBytes(StandardCharsets.UTF_8); + + final ByteArrayOutputStream pae = new ByteArrayOutputStream(); + pae.write(("DSSEv1 " + payloadTypeBytes.length + " ").getBytes(StandardCharsets.UTF_8)); + pae.write(payloadTypeBytes); + pae.write((" " + statementBytes.length + " ").getBytes(StandardCharsets.UTF_8)); + pae.write(statementBytes); + + final Path paeFile = buildDirectory.resolve("statement.pae"); + Files.write(paeFile, pae.toByteArray()); + return paeFile; + } catch (final IOException e) { + throw new MojoExecutionException("Failed to write PAE file", e); + } + } + + /** + * Signs {@code paeFile} using {@link AbstractGpgSigner#generateSignatureForArtifact(File)}, + * then decodes the resulting ASCII-armored {@code .asc} file with BouncyCastle and returns the raw + * binary PGP signature bytes. + * + *

The signer must already have {@link AbstractGpgSigner#prepare()} called before this method is + * invoked. The {@code .asc} file produced by the signer is not deleted; callers may remove it once + * the raw bytes have been consumed.

+ * + * @param signer the configured, prepared signer + * @param paeFile path to the PAE-encoded file to sign + * @return raw binary PGP signature bytes (suitable for storing in {@link org.apache.commons.release.plugin.slsa.v1_2.Signature#setSig}) + * @throws MojoExecutionException if signing or signature decoding fails + */ + public static byte[] signPaeFile(final AbstractGpgSigner signer, final Path paeFile) throws MojoExecutionException { + final File signatureFile = signer.generateSignatureForArtifact(paeFile.toFile()); + try (InputStream in = Files.newInputStream(signatureFile.toPath()); ArmoredInputStream armoredIn = new ArmoredInputStream(in)) { + return IOUtils.toByteArray(armoredIn); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to read signature file: " + signatureFile, e); + } + } + + /** + * Extracts the key identifier from a binary OpenPGP Signature Packet. + * + *

Inspects the hashed subpackets for an {@code IssuerFingerprint} subpacket (type 33), + * which carries the full public-key fingerprint and is present in all signatures produced by + * GPG 2.1+. Falls back to the 8-byte {@code IssuerKeyID} from the unhashed subpackets + * when no fingerprint subpacket is found.

+ * + * @param sigBytes raw binary OpenPGP Signature Packet bytes, as returned by + * {@link #signPaeFile(AbstractGpgSigner, Path)} + * @return uppercase hex-encoded fingerprint or key ID string + * @throws MojoExecutionException if {@code sigBytes} cannot be parsed as an OpenPGP signature + */ + public static String getKeyId(final byte[] sigBytes) throws MojoExecutionException { + try { + final PGPSignatureList sigList = (PGPSignatureList) new BcPGPObjectFactory(sigBytes).nextObject(); + final PGPSignature sig = sigList.get(0); + final PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); + if (hashed != null) { + final IssuerFingerprint fp = hashed.getIssuerFingerprint(); + if (fp != null) { + return Hex.encodeHexString(fp.getFingerprint()); + } + } + return Long.toHexString(sig.getKeyID()).toUpperCase(Locale.ROOT); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to extract key ID from signature", e); + } + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 254779981..7bcabda8d 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -26,6 +26,7 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -34,17 +35,21 @@ import javax.inject.Inject; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.apache.commons.release.plugin.internal.ArtifactUtils; import org.apache.commons.release.plugin.internal.BuildToolDescriptors; +import org.apache.commons.release.plugin.internal.DsseUtils; import org.apache.commons.release.plugin.internal.GitUtils; import org.apache.commons.release.plugin.slsa.v1_2.BuildDefinition; import org.apache.commons.release.plugin.slsa.v1_2.BuildMetadata; import org.apache.commons.release.plugin.slsa.v1_2.Builder; +import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope; import org.apache.commons.release.plugin.slsa.v1_2.Provenance; import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; import org.apache.commons.release.plugin.slsa.v1_2.RunDetails; +import org.apache.commons.release.plugin.slsa.v1_2.Signature; import org.apache.commons.release.plugin.slsa.v1_2.Statement; import org.apache.maven.artifact.Artifact; import org.apache.maven.execution.MavenExecutionRequest; @@ -56,6 +61,7 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.plugins.gpg.AbstractGpgSigner; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.apache.maven.rtinfo.RuntimeInformation; @@ -73,10 +79,14 @@ @Mojo(name = "build-attestation", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) public class BuildAttestationMojo extends AbstractMojo { - /** The file extension for in-toto attestation files. */ + /** + * The file extension for in-toto attestation files. + */ private static final String ATTESTATION_EXTENSION = "intoto.jsonl"; - /** Shared Jackson object mapper for serializing attestation statements. */ + /** + * Shared Jackson object mapper for serializing attestation statements. + */ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); static { @@ -85,11 +95,15 @@ public class BuildAttestationMojo extends AbstractMojo { OBJECT_MAPPER.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } - /** The SCM connection URL for the current project. */ + /** + * The SCM connection URL for the current project. + */ @Parameter(defaultValue = "${project.scm.connection}", readonly = true) private String scmConnectionUrl; - /** The Maven home directory. */ + /** + * The Maven home directory. + */ @Parameter(defaultValue = "${maven.home}", readonly = true) private File mavenHome; @@ -99,14 +113,62 @@ public class BuildAttestationMojo extends AbstractMojo { @Parameter(property = "commons.release.scmDirectory", defaultValue = "${basedir}") private File scmDirectory; - /** The output directory for the attestation file. */ + /** + * The output directory for the attestation file. + */ @Parameter(property = "commons.release.outputDirectory", defaultValue = "${project.build.directory}") private File outputDirectory; - /** Whether to skip attaching the attestation artifact to the project. */ + /** + * Whether to skip attaching the attestation artifact to the project. + */ @Parameter(property = "commons.release.skipAttach") private boolean skipAttach; + /** + * Whether to sign the attestation envelope with GPG. + */ + @Parameter(property = "commons.release.signAttestation", defaultValue = "true") + private boolean signAttestation; + + /** + * Path to the GPG executable; if not set, {@code gpg} is resolved from {@code PATH}. + */ + @Parameter(property = "gpg.executable") + private String executable; + + /** + * Whether to include the default GPG keyring. + * + *

When {@code false}, passes {@code --no-default-keyring} to the GPG command.

+ */ + @Parameter(property = "gpg.defaultKeyring", defaultValue = "true") + private boolean defaultKeyring; + + /** + * GPG database lock mode passed via {@code --lock-once}, {@code --lock-multiple}, or + * {@code --lock-never}; no lock flag is added when not set. + */ + @Parameter(property = "gpg.lockMode") + private String lockMode; + + /** + * Name or fingerprint of the GPG key to use for signing. + * + *

Passed as {@code --local-user} to the GPG command; uses the default key when not set.

+ */ + @Parameter(property = "gpg.keyname") + private String keyname; + + /** + * Whether to use gpg-agent for passphrase management. + * + *

For GPG versions before 2.1, passes {@code --use-agent} or {@code --no-use-agent} + * accordingly; ignored for GPG 2.1 and later where the agent is always used.

+ */ + @Parameter(property = "gpg.useagent", defaultValue = "true") + private boolean useAgent; + /** * The current Maven project. */ @@ -135,10 +197,10 @@ public class BuildAttestationMojo extends AbstractMojo { /** * Creates a new instance with the given dependencies. * - * @param project A Maven project. - * @param scmManager A SCM manager. + * @param project A Maven project. + * @param scmManager A SCM manager. * @param runtimeInformation Maven runtime information. - * @param session A Maven session. + * @param session A Maven session. * @param mavenProjectHelper A helper to attach artifacts to the project. */ @Inject @@ -217,16 +279,22 @@ public void execute() throws MojoFailureException, MojoExecutionException { statement.setSubject(getSubjects()); statement.setPredicate(provenance); - writeStatement(statement); + final Path outputPath = ensureOutputDirectory(); + final Path artifactPath = outputPath.resolve(ArtifactUtils.getFileName(project.getArtifact(), ATTESTATION_EXTENSION)); + if (signAttestation) { + signAndWriteStatement(statement, outputPath, artifactPath); + } else { + writeStatement(statement, artifactPath); + } } /** - * Serializes the attestation statement to a file and optionally attaches it to the project. + * Creates the output directory if it does not already exist and returns its path. * - * @param statement The attestation statement to write. - * @throws MojoExecutionException If the output directory cannot be created or the file cannot be written. + * @return the output directory path + * @throws MojoExecutionException if the directory cannot be created */ - private void writeStatement(final Statement statement) throws MojoExecutionException { + private Path ensureOutputDirectory() throws MojoExecutionException { final Path outputPath = outputDirectory.toPath(); try { if (!Files.exists(outputPath)) { @@ -235,18 +303,72 @@ private void writeStatement(final Statement statement) throws MojoExecutionExcep } catch (IOException e) { throw new MojoExecutionException("Could not create output directory.", e); } - final Artifact mainArtifact = project.getArtifact(); - final Path artifactPath = outputPath.resolve(ArtifactUtils.getFileName(mainArtifact, ATTESTATION_EXTENSION)); + return outputPath; + } + + /** + * Serializes the attestation statement as a bare JSON line and writes it to {@code artifactPath}. + * + * @param statement the attestation statement to write + * @param artifactPath the destination file path + * @throws MojoExecutionException if the file cannot be written + */ + private void writeStatement(final Statement statement, final Path artifactPath) throws MojoExecutionException { getLog().info("Writing attestation statement to: " + artifactPath); + writeAndAttach(statement, artifactPath); + } + + /** + * Signs the attestation statement with GPG, wraps it in a DSSE envelope, and writes it to + * {@code artifactPath}. + * + * @param statement the attestation statement to sign and write + * @param outputPath directory used for intermediate PAE and signature files + * @param artifactPath the destination file path for the envelope + * @throws MojoExecutionException if serialization, signing, or file I/O fails + * @throws MojoFailureException if the GPG signer cannot be prepared + */ + private void signAndWriteStatement(final Statement statement, final Path outputPath, + final Path artifactPath) throws MojoExecutionException, MojoFailureException { + final byte[] statementBytes; + try { + statementBytes = OBJECT_MAPPER.writeValueAsBytes(statement); + } catch (JsonProcessingException e) { + throw new MojoExecutionException("Failed to serialize attestation statement", e); + } + final AbstractGpgSigner signer = DsseUtils.createGpgSigner(executable, defaultKeyring, lockMode, keyname, useAgent, getLog()); + final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); + final byte[] sigBytes = DsseUtils.signPaeFile(signer, paeFile); + + final Signature sig = new Signature(); + sig.setKeyid(DsseUtils.getKeyId(sigBytes)); + sig.setSig(sigBytes); + + final DsseEnvelope envelope = new DsseEnvelope(); + envelope.setPayload(statementBytes); + envelope.setSignatures(Collections.singletonList(sig)); + + getLog().info("Writing signed attestation envelope to: " + artifactPath); + writeAndAttach(envelope, artifactPath); + } + + /** + * Writes {@code value} as a JSON line to {@code artifactPath} and optionally attaches it to the project. + * + * @param value the object to serialize + * @param artifactPath the destination file path + * @throws MojoExecutionException if the file cannot be written + */ + private void writeAndAttach(final Object value, final Path artifactPath) throws MojoExecutionException { try (OutputStream os = Files.newOutputStream(artifactPath)) { - OBJECT_MAPPER.writeValue(os, statement); + OBJECT_MAPPER.writeValue(os, value); os.write('\n'); } catch (IOException e) { - throw new MojoExecutionException("Could not write attestation statement to: " + artifactPath, e); + throw new MojoExecutionException("Could not write attestation to: " + artifactPath, e); } if (!skipAttach) { - getLog().info(String.format("Attaching attestation statement as %s-%s.%s", mainArtifact.getArtifactId(), mainArtifact.getVersion(), - ATTESTATION_EXTENSION)); + final Artifact mainArtifact = project.getArtifact(); + getLog().info(String.format("Attaching attestation as %s-%s.%s", mainArtifact.getArtifactId(), mainArtifact.getVersion(), ATTESTATION_EXTENSION)); mavenProjectHelper.attachArtifact(project, ATTESTATION_EXTENSION, null, artifactPath.toFile()); } } @@ -373,7 +495,7 @@ private List getProjectDependencies() throws MojoExecutionEx * Returns a resource descriptor for the current SCM source, including the URI and Git commit digest. * * @return A resource descriptor for the SCM source. - * @throws IOException If the current branch cannot be determined. + * @throws IOException If the current branch cannot be determined. * @throws MojoExecutionException If the SCM revision cannot be retrieved. */ private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionException { diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java new file mode 100644 index 000000000..604286a28 --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java @@ -0,0 +1,133 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * DSSE (Dead Simple Signing Envelope) that wraps a signed in-toto statement payload. + * + *

The {@code payload} field holds the serialized {@link Statement} bytes; Jackson serializes them as Base64. The + * {@code payloadType} identifies the content type of the payload. The {@code signatures} list contains one or more + * cryptographic signatures over the PAE-encoded payload.

+ * + *

All three fields are REQUIRED and MUST be set, even if empty.

+ * + * @see DSSE Envelope specification + */ +public class DsseEnvelope { + + /** The payload type URI for in-toto attestation statements. */ + public static final String PAYLOAD_TYPE = "application/vnd.in-toto+json"; + + /** Content type identifying the format of {@link #payload}. */ + @JsonProperty("payloadType") + private String payloadType = PAYLOAD_TYPE; + + /** Serialized statement bytes, Base64-encoded in JSON. */ + @JsonProperty("payload") + private byte[] payload; + + /** One or more signatures over the PAE-encoded payload. */ + @JsonProperty("signatures") + private List signatures; + + /** Creates a new DsseEnvelope instance with {@code payloadType} pre-set to {@link #PAYLOAD_TYPE}. */ + public DsseEnvelope() { + } + + /** + * Returns the payload type URI. + * + * @return the payload type, never {@code null} in a valid envelope + */ + public String getPayloadType() { + return payloadType; + } + + /** + * Sets the payload type URI. + * + * @param payloadType the payload type URI + */ + public void setPayloadType(String payloadType) { + this.payloadType = payloadType; + } + + /** + * Returns the serialized payload bytes. + * + *

When serialized to JSON the bytes are Base64-encoded.

+ * + * @return the payload bytes, or {@code null} if not set + */ + public byte[] getPayload() { + return payload; + } + + /** + * Sets the serialized payload bytes. + * + * @param payload the payload bytes + */ + public void setPayload(byte[] payload) { + this.payload = payload; + } + + /** + * Returns the list of signatures over the PAE-encoded payload. + * + * @return the signatures, or {@code null} if not set + */ + public List getSignatures() { + return signatures; + } + + /** + * Sets the list of signatures over the PAE-encoded payload. + * + * @param signatures the signatures + */ + public void setSignatures(List signatures) { + this.signatures = signatures; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof DsseEnvelope)) { + return false; + } + DsseEnvelope envelope = (DsseEnvelope) o; + return Objects.equals(payloadType, envelope.payloadType) && Arrays.equals(payload, envelope.payload) + && Objects.equals(signatures, envelope.signatures); + } + + @Override + public int hashCode() { + return Objects.hash(payloadType, Arrays.hashCode(payload), signatures); + } + + @Override + public String toString() { + return "DsseEnvelope{payloadType='" + payloadType + "', payload=<" + (payload != null ? payload.length : 0) + + " bytes>, signatures=" + signatures + '}'; + } +} diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java new file mode 100644 index 000000000..1a3d381bf --- /dev/null +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.slsa.v1_2; + +import java.util.Arrays; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * A single cryptographic signature within a DSSE envelope. + * + *

The {@code sig} field holds the raw signature bytes; Jackson serializes them as Base64. The optional + * {@code keyid} field identifies which key produced the signature.

+ * + * @see DSSE Envelope specification + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Signature { + + /** + * Hint for which key was used to sign; unset is treated as empty. + * + *

Consumers MUST NOT require this field to be set, and MUST NOT use it for security decisions.

+ */ + @JsonProperty("keyid") + private String keyid; + + /** Raw signature bytes over the PAE-encoded payload, Base64-encoded in JSON. */ + @JsonProperty("sig") + private byte[] sig; + + /** Creates a new Signature instance. */ + public Signature() { + } + + /** + * Returns the key identifier hint, or {@code null} if not set. + * + * @return the key identifier, or {@code null} + */ + public String getKeyid() { + return keyid; + } + + /** + * Sets the key identifier hint. + * + * @param keyid the key identifier, or {@code null} to leave unset + */ + public void setKeyid(String keyid) { + this.keyid = keyid; + } + + /** + * Returns the raw signature bytes. + * + *

When serialized to JSON the bytes are Base64-encoded.

+ * + * @return the signature bytes, or {@code null} if not set + */ + public byte[] getSig() { + return sig; + } + + /** + * Sets the raw signature bytes. + * + * @param sig the signature bytes + */ + public void setSig(byte[] sig) { + this.sig = sig; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Signature)) { + return false; + } + Signature signature = (Signature) o; + return Objects.equals(keyid, signature.keyid) && Arrays.equals(sig, signature.sig); + } + + @Override + public int hashCode() { + return Objects.hash(keyid, Arrays.hashCode(sig)); + } + + @Override + public String toString() { + return "Signature{keyid='" + keyid + "', sig=<" + (sig != null ? sig.length : 0) + " bytes>}"; + } +} From c1644fa32581b429d364ebe36eba7e93c25122f7 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 16 Apr 2026 18:58:23 +0200 Subject: [PATCH 08/51] fix: Javadoc of the model --- .../commons/release/plugin/internal/DsseUtils.java | 6 ++---- .../release/plugin/slsa/v1_2/BuildDefinition.java | 8 ++++---- .../release/plugin/slsa/v1_2/BuildMetadata.java | 9 +++------ .../commons/release/plugin/slsa/v1_2/Builder.java | 6 +++--- .../release/plugin/slsa/v1_2/DsseEnvelope.java | 12 +++--------- .../release/plugin/slsa/v1_2/Provenance.java | 4 ++-- .../plugin/slsa/v1_2/ResourceDescriptor.java | 14 +++++++------- .../release/plugin/slsa/v1_2/RunDetails.java | 6 +++--- .../release/plugin/slsa/v1_2/Signature.java | 11 +++-------- .../release/plugin/slsa/v1_2/Statement.java | 9 ++++----- 10 files changed, 34 insertions(+), 51 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java index e4e362f01..99283d80d 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -57,13 +57,11 @@ private DsseUtils() { /** * Creates and prepares a {@link GpgSigner} from the given configuration. * - *

The returned signer has {@link AbstractGpgSigner#prepare()} already called and is ready - * for use with {@link #signPaeFile(AbstractGpgSigner, Path)}.

+ *

The returned signer has {@link AbstractGpgSigner#prepare()} already called and is ready for use with {@link #signPaeFile(AbstractGpgSigner, Path)}.

* * @param executable path to the GPG executable, or {@code null} to use {@code gpg} from {@code PATH} * @param defaultKeyring whether to include the default GPG keyring - * @param lockMode GPG lock mode ({@code "once"}, {@code "multiple"}, or {@code "never"}), - * or {@code null} for no explicit lock flag + * @param lockMode GPG lock mode ({@code "once"}, {@code "multiple"}, or {@code "never"}), or {@code null} for no explicit lock flag * @param keyname name or fingerprint of the signing key, or {@code null} for the default key * @param useAgent whether to use gpg-agent for passphrase management * @param log Maven logger to attach to the signer diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java index 843bc0e17..661d0ccd9 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java @@ -65,7 +65,7 @@ public BuildDefinition(String buildType, Map externalParameters) } /** - * Returns the URI indicating what type of build was performed. + * Gets the URI indicating what type of build was performed. * *

Determines the meaning of {@code externalParameters} and {@code internalParameters}.

* @@ -85,7 +85,7 @@ public void setBuildType(String buildType) { } /** - * Returns the inputs passed to the build, such as command-line arguments or environment variables. + * Gets the inputs passed to the build, such as command-line arguments or environment variables. * * @return the external parameters map, or {@code null} if not set */ @@ -103,7 +103,7 @@ public void setExternalParameters(Map externalParameters) { } /** - * Returns the artifacts the build depends on, such as sources, dependencies, build tools, and base images, + * Gets the artifacts the build depends on, such as sources, dependencies, build tools, and base images, * specified by URI and digest. * * @return the internal parameters map, or {@code null} if not set @@ -122,7 +122,7 @@ public void setInternalParameters(Map internalParameters) { } /** - * Returns the materials that influenced the build. + * Gets the materials that influenced the build. * *

Considered incomplete unless resolved materials are present.

* diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java index 345eb91ee..35d04e412 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java @@ -63,10 +63,7 @@ public BuildMetadata(String invocationId, OffsetDateTime startedOn, OffsetDateTi } /** - * Returns the identifier for this build invocation. - * - *

Useful for finding associated logs or other ad-hoc analysis. The exact meaning and format is defined by the - * builder and is treated as opaque and case-sensitive. The value SHOULD be globally unique.

+ * Gets the identifier for this build invocation. * * @return the invocation identifier, or {@code null} if not set */ @@ -84,7 +81,7 @@ public void setInvocationId(String invocationId) { } /** - * Returns the timestamp of when the build started, serialized as RFC 3339 in UTC ({@code "Z"} suffix). + * Gets the timestamp of when the build started, serialized as RFC 3339 in UTC ({@code "Z"} suffix). * * @return the start timestamp, or {@code null} if not set */ @@ -102,7 +99,7 @@ public void setStartedOn(OffsetDateTime startedOn) { } /** - * Returns the timestamp of when the build completed, serialized as RFC 3339 in UTC ({@code "Z"} suffix). + * Gets the timestamp of when the build completed, serialized as RFC 3339 in UTC ({@code "Z"} suffix). * * @return the completion timestamp, or {@code null} if not set */ diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java index 36e0f1a89..635e75cfb 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java @@ -48,7 +48,7 @@ public Builder() { } /** - * Returns the identifier of the builder. + * Gets the identifier of the builder. * * @return the builder identifier URI */ @@ -66,7 +66,7 @@ public void setId(String id) { } /** - * Returns orchestrator dependencies that do not run within the build workload and do not affect the build output, + * Gets orchestrator dependencies that do not run within the build workload and do not affect the build output, * but may affect provenance generation or security guarantees. * * @return the list of builder dependencies, or {@code null} if not set @@ -85,7 +85,7 @@ public void setBuilderDependencies(List builderDependencies) } /** - * Returns a map of build platform component names to their versions. + * Gets a map of build platform component names to their versions. * * @return the version map, or {@code null} if not set */ diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java index 604286a28..fdb2353f3 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java @@ -25,12 +25,6 @@ /** * DSSE (Dead Simple Signing Envelope) that wraps a signed in-toto statement payload. * - *

The {@code payload} field holds the serialized {@link Statement} bytes; Jackson serializes them as Base64. The - * {@code payloadType} identifies the content type of the payload. The {@code signatures} list contains one or more - * cryptographic signatures over the PAE-encoded payload.

- * - *

All three fields are REQUIRED and MUST be set, even if empty.

- * * @see DSSE Envelope specification */ public class DsseEnvelope { @@ -55,7 +49,7 @@ public DsseEnvelope() { } /** - * Returns the payload type URI. + * Gets the payload type URI. * * @return the payload type, never {@code null} in a valid envelope */ @@ -73,7 +67,7 @@ public void setPayloadType(String payloadType) { } /** - * Returns the serialized payload bytes. + * Gets the serialized payload bytes. * *

When serialized to JSON the bytes are Base64-encoded.

* @@ -93,7 +87,7 @@ public void setPayload(byte[] payload) { } /** - * Returns the list of signatures over the PAE-encoded payload. + * Gets the list of signatures over the PAE-encoded payload. * * @return the signatures, or {@code null} if not set */ diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java index 884246006..c9dfd2e28 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java @@ -59,7 +59,7 @@ public Provenance(BuildDefinition buildDefinition, RunDetails runDetails) { } /** - * Returns the build definition describing all inputs that produced the build output. + * Gets the build definition describing all inputs that produced the build output. * *

Includes source code, dependencies, build tools, base images, and other materials.

* @@ -79,7 +79,7 @@ public void setBuildDefinition(BuildDefinition buildDefinition) { } /** - * Returns the details about the invocation of the build tool and the environment in which it was run. + * Gets the details about the invocation of the build tool and the environment in which it was run. * * @return the run details, or {@code null} if not set */ diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java index 55333f220..2ce42ce25 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java @@ -76,7 +76,7 @@ public ResourceDescriptor(String uri, Map digest) { } /** - * Returns the name of the resource. + * Gets the name of the resource. * * @return the resource name, or {@code null} if not set */ @@ -94,7 +94,7 @@ public void setName(String name) { } /** - * Returns the URI identifying the resource. + * Gets the URI identifying the resource. * * @return the resource URI, or {@code null} if not set */ @@ -112,7 +112,7 @@ public void setUri(String uri) { } /** - * Returns the map of cryptographic digest algorithms to their corresponding hex-encoded values for this resource. + * Gets the map of cryptographic digest algorithms to their corresponding hex-encoded values for this resource. * *

Common keys include {@code "sha256"} and {@code "sha512"}.

* @@ -132,7 +132,7 @@ public void setDigest(Map digest) { } /** - * Returns the raw contents of the resource, base64-encoded when serialized to JSON. + * Gets the raw contents of the resource, base64-encoded when serialized to JSON. * * @return the resource content, or {@code null} if not set */ @@ -150,7 +150,7 @@ public void setContent(byte[] content) { } /** - * Returns the download URI for the resource, if different from {@link #getUri()}. + * Gets the download URI for the resource, if different from {@link #getUri()}. * * @return the download location URI, or {@code null} if not set */ @@ -168,7 +168,7 @@ public void setDownloadLocation(String downloadLocation) { } /** - * Returns the media type of the resource (e.g., {@code "application/octet-stream"}). + * Gets the media type of the resource (e.g., {@code "application/octet-stream"}). * * @return the media type, or {@code null} if not set */ @@ -186,7 +186,7 @@ public void setMediaType(String mediaType) { } /** - * Returns additional key-value metadata about the resource, such as filename, size, or builder-specific attributes. + * Gets additional key-value metadata about the resource, such as filename, size, or builder-specific attributes. * * @return the annotations map, or {@code null} if not set */ diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java index ffb118677..7b20b5a1d 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java @@ -58,7 +58,7 @@ public RunDetails(Builder builder, BuildMetadata metadata) { } /** - * Returns the builder that executed the invocation. + * Gets the builder that executed the invocation. * *

Trusted to have correctly performed the operation and populated this provenance.

* @@ -78,7 +78,7 @@ public void setBuilder(Builder builder) { } /** - * Returns the metadata about the build invocation, including its identifier and timing. + * Gets the metadata about the build invocation, including its identifier and timing. * * @return the build metadata, or {@code null} if not set */ @@ -96,7 +96,7 @@ public void setMetadata(BuildMetadata metadata) { } /** - * Returns artifacts produced as a side effect of the build that are not the primary output. + * Gets artifacts produced as a side effect of the build that are not the primary output. * * @return the list of byproduct artifacts, or {@code null} if not set */ diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java index 1a3d381bf..c2caf8000 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java @@ -25,9 +25,6 @@ /** * A single cryptographic signature within a DSSE envelope. * - *

The {@code sig} field holds the raw signature bytes; Jackson serializes them as Base64. The optional - * {@code keyid} field identifies which key produced the signature.

- * * @see DSSE Envelope specification */ @JsonInclude(JsonInclude.Include.NON_NULL) @@ -41,7 +38,7 @@ public class Signature { @JsonProperty("keyid") private String keyid; - /** Raw signature bytes over the PAE-encoded payload, Base64-encoded in JSON. */ + /** Raw signature bytes of the PAE-encoded payload. */ @JsonProperty("sig") private byte[] sig; @@ -50,7 +47,7 @@ public Signature() { } /** - * Returns the key identifier hint, or {@code null} if not set. + * Gets the key identifier hint, or {@code null} if not set. * * @return the key identifier, or {@code null} */ @@ -68,9 +65,7 @@ public void setKeyid(String keyid) { } /** - * Returns the raw signature bytes. - * - *

When serialized to JSON the bytes are Base64-encoded.

+ * Gets the raw signature bytes. * * @return the signature bytes, or {@code null} if not set */ diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java index 88aeb8ae8..4f3506e0b 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java @@ -49,10 +49,9 @@ public Statement() { } /** - * Returns the set of software artifacts that the attestation applies to. + * Gets the set of software artifacts that the attestation applies to. * - *

Each element represents a single artifact. Artifacts are matched purely by digest, regardless of content - * type.

+ *

Each element represents a single artifact. Artifacts are matched purely by digest, regardless of content type.

* * @return the list of subject artifacts, or {@code null} if not set */ @@ -70,7 +69,7 @@ public void setSubject(List subject) { } /** - * Returns the URI identifying the type of the predicate. + * Gets the URI identifying the type of the predicate. * * @return the predicate type URI, or {@code null} if no predicate has been set */ @@ -79,7 +78,7 @@ public String getPredicateType() { } /** - * Returns the provenance predicate. + * Gets the provenance predicate. * *

Unset is treated the same as set-but-empty. May be omitted if {@code predicateType} fully describes the * predicate.

From 722edc03d3c552966c162a15007ffb33bfc45315 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 16 Apr 2026 20:32:33 +0200 Subject: [PATCH 09/51] fix: Javadoc of `DsseUtils` --- .../release/plugin/internal/DsseUtils.java | 54 ++++++++----------- .../plugin/mojos/BuildAttestationMojo.java | 2 +- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java index 99283d80d..23cac277c 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -17,7 +17,6 @@ package org.apache.commons.release.plugin.internal; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -57,7 +56,7 @@ private DsseUtils() { /** * Creates and prepares a {@link GpgSigner} from the given configuration. * - *

The returned signer has {@link AbstractGpgSigner#prepare()} already called and is ready for use with {@link #signPaeFile(AbstractGpgSigner, Path)}.

+ *

The returned signer has {@link AbstractGpgSigner#prepare()} already called and is ready for use with {@link #signFile(AbstractGpgSigner, Path)}.

* * @param executable path to the GPG executable, or {@code null} to use {@code gpg} from {@code PATH} * @param defaultKeyring whether to include the default GPG keyring @@ -81,12 +80,9 @@ public static AbstractGpgSigner createGpgSigner(final String executable, final b } /** - * Serializes {@code statement} to JSON, computes the DSSE Pre-Authentication Encoding (PAE), and writes - * the result to {@code buildDirectory/statement.pae}. + * Serializes {@code statement} to JSON using the DSSE Pre-Authentication Encoding (PAE) * - *

The PAE format is: - * {@code "DSSEv1" SP LEN(payloadType) SP payloadType SP LEN(payload) SP payload}, - * where {@code LEN} is the ASCII decimal byte-length of the operand.

+ *
PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
* * @param statement the attestation statement to encode * @param objectMapper the Jackson mapper used to serialize {@code statement} @@ -103,12 +99,9 @@ public static Path writePaeFile(final Statement statement, final ObjectMapper ob } /** - * Computes the DSSE Pre-Authentication Encoding (PAE) for {@code statementBytes} and writes it to - * {@code buildDirectory/statement.pae}. + * Writes serialized JSON to a file using the DSSE Pre-Authentication Encoding (PAE) * - *

Use this overload when the statement has already been serialized to bytes, so the same byte array - * can be reused as the {@link DsseEnvelope#setPayload(byte[]) envelope payload} without a second - * serialization pass.

+ *
PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
* * @param statementBytes the already-serialized JSON statement bytes to encode * @param buildDirectory directory in which the PAE file is created @@ -134,38 +127,35 @@ public static Path writePaeFile(final byte[] statementBytes, final Path buildDir } /** - * Signs {@code paeFile} using {@link AbstractGpgSigner#generateSignatureForArtifact(File)}, - * then decodes the resulting ASCII-armored {@code .asc} file with BouncyCastle and returns the raw - * binary PGP signature bytes. + * Signs {@code paeFile} and returns the raw OpenPGP signature bytes. * - *

The signer must already have {@link AbstractGpgSigner#prepare()} called before this method is - * invoked. The {@code .asc} file produced by the signer is not deleted; callers may remove it once - * the raw bytes have been consumed.

+ *

The signer must already have {@link AbstractGpgSigner#prepare()} called before this method is invoked.

* * @param signer the configured, prepared signer - * @param paeFile path to the PAE-encoded file to sign - * @return raw binary PGP signature bytes (suitable for storing in {@link org.apache.commons.release.plugin.slsa.v1_2.Signature#setSig}) + * @param path path to the file to sign + * @return raw binary PGP signature bytes * @throws MojoExecutionException if signing or signature decoding fails */ - public static byte[] signPaeFile(final AbstractGpgSigner signer, final Path paeFile) throws MojoExecutionException { - final File signatureFile = signer.generateSignatureForArtifact(paeFile.toFile()); - try (InputStream in = Files.newInputStream(signatureFile.toPath()); ArmoredInputStream armoredIn = new ArmoredInputStream(in)) { - return IOUtils.toByteArray(armoredIn); + public static byte[] signFile(final AbstractGpgSigner signer, final Path path) throws MojoExecutionException { + final Path signaturePath = signer.generateSignatureForArtifact(path.toFile()).toPath(); + final byte[] signatureBytes; + try (InputStream in = Files.newInputStream(signaturePath); ArmoredInputStream armoredIn = new ArmoredInputStream(in)) { + signatureBytes = IOUtils.toByteArray(armoredIn); } catch (final IOException e) { - throw new MojoExecutionException("Failed to read signature file: " + signatureFile, e); + throw new MojoExecutionException("Failed to read signature file: " + signaturePath, e); } + try { + Files.delete(signaturePath); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to delete signature file: " + signaturePath, e); + } + return signatureBytes; } /** * Extracts the key identifier from a binary OpenPGP Signature Packet. * - *

Inspects the hashed subpackets for an {@code IssuerFingerprint} subpacket (type 33), - * which carries the full public-key fingerprint and is present in all signatures produced by - * GPG 2.1+. Falls back to the 8-byte {@code IssuerKeyID} from the unhashed subpackets - * when no fingerprint subpacket is found.

- * - * @param sigBytes raw binary OpenPGP Signature Packet bytes, as returned by - * {@link #signPaeFile(AbstractGpgSigner, Path)} + * @param sigBytes raw binary OpenPGP Signature Packet bytes * @return uppercase hex-encoded fingerprint or key ID string * @throws MojoExecutionException if {@code sigBytes} cannot be parsed as an OpenPGP signature */ diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 7bcabda8d..a72853075 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -338,7 +338,7 @@ private void signAndWriteStatement(final Statement statement, final Path outputP } final AbstractGpgSigner signer = DsseUtils.createGpgSigner(executable, defaultKeyring, lockMode, keyname, useAgent, getLog()); final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); - final byte[] sigBytes = DsseUtils.signPaeFile(signer, paeFile); + final byte[] sigBytes = DsseUtils.signFile(signer, paeFile); final Signature sig = new Signature(); sig.setKeyid(DsseUtils.getKeyId(sigBytes)); From ec90db88aa6035d05df3890a72b5f2d5990cd73a Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 16 Apr 2026 20:36:03 +0200 Subject: [PATCH 10/51] fix: Javadoc of `BuildAttestationMojo` --- .../commons/release/plugin/mojos/BuildAttestationMojo.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index a72853075..06326f0c1 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -319,8 +319,7 @@ private void writeStatement(final Statement statement, final Path artifactPath) } /** - * Signs the attestation statement with GPG, wraps it in a DSSE envelope, and writes it to - * {@code artifactPath}. + * Signs the attestation statement with GPG and writes it to {@code artifactPath}. * * @param statement the attestation statement to sign and write * @param outputPath directory used for intermediate PAE and signature files @@ -533,8 +532,8 @@ private String getScmRevision() throws MojoExecutionException { ScmRepository scmRepository = getScmRepository(); CommandParameters commandParameters = new CommandParameters(); try { - InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), - new ScmFileSet(scmDirectory), commandParameters); + InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), new ScmFileSet(scmDirectory) + , commandParameters); return getScmRevision(result); } catch (ScmException e) { From 2ad0751624a3e23fc2357c42760d413a5d14d373 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 16 Apr 2026 21:10:22 +0200 Subject: [PATCH 11/51] fix: add sign test --- .../plugin/mojos/BuildAttestationMojo.java | 40 ++++++- .../mojos/BuildAttestationMojoTest.java | 107 ++++++++++++++---- .../commons-release-plugin-1.9.2.jar.asc | 7 ++ 3 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 06326f0c1..75346ab82 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -169,6 +169,11 @@ public class BuildAttestationMojo extends AbstractMojo { @Parameter(property = "gpg.useagent", defaultValue = "true") private boolean useAgent; + /** + * GPG signer used for signing; lazily initialized from plugin parameters when {@code null}. + */ + private AbstractGpgSigner signer; + /** * The current Maven project. */ @@ -258,6 +263,37 @@ void setMavenHome(final File mavenHome) { this.mavenHome = mavenHome; } + /** + * Sets whether to sign the attestation envelope. + * + * @param signAttestation {@code true} to sign, {@code false} to skip signing + */ + void setSignAttestation(final boolean signAttestation) { + this.signAttestation = signAttestation; + } + + /** + * Overrides the GPG signer used for signing. Intended for testing. + * + * @param signer the signer to use + */ + void setSigner(final AbstractGpgSigner signer) { + this.signer = signer; + } + + /** + * Returns the GPG signer, creating and preparing it from plugin parameters if not already set. + * + * @return the prepared signer + * @throws MojoFailureException if signer preparation fails + */ + private AbstractGpgSigner getSigner() throws MojoFailureException { + if (signer == null) { + signer = DsseUtils.createGpgSigner(executable, defaultKeyring, lockMode, keyname, useAgent, getLog()); + } + return signer; + } + @Override public void execute() throws MojoFailureException, MojoExecutionException { // Build definition @@ -335,7 +371,7 @@ private void signAndWriteStatement(final Statement statement, final Path outputP } catch (JsonProcessingException e) { throw new MojoExecutionException("Failed to serialize attestation statement", e); } - final AbstractGpgSigner signer = DsseUtils.createGpgSigner(executable, defaultKeyring, lockMode, keyname, useAgent, getLog()); + final AbstractGpgSigner signer = getSigner(); final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); final byte[] sigBytes = DsseUtils.signFile(signer, paeFile); @@ -417,7 +453,7 @@ private Map getExternalParameters() { * @param request The Maven execution request. * @return A string representation of the Maven command line. */ - private String getCommandLine(final MavenExecutionRequest request) { + private static String getCommandLine(final MavenExecutionRequest request) { StringBuilder sb = new StringBuilder(); for (String goal : request.getGoals()) { sb.append(goal); diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index dae20c43c..c52de3993 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -19,12 +19,17 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; 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.nio.file.StandardCopyOption; import java.util.Date; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.release.plugin.internal.MojoUtils; +import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope; import org.apache.maven.artifact.Artifact; import org.apache.maven.bridge.MavenRepositorySystem; import org.apache.maven.execution.DefaultMavenExecutionRequest; @@ -33,8 +38,10 @@ import org.apache.maven.execution.MavenExecutionResult; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Model; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.plugins.gpg.AbstractGpgSigner; import org.apache.maven.rtinfo.RuntimeInformation; import org.apache.maven.scm.manager.ScmManager; import org.codehaus.plexus.PlexusContainer; @@ -46,6 +53,8 @@ public class BuildAttestationMojoTest { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + @TempDir private static Path localRepositoryPath; @@ -89,6 +98,50 @@ private static MavenProject createMavenProject(MavenProjectHelper projectHelper, return project; } + private static AbstractGpgSigner createMockSigner() { + return new AbstractGpgSigner() { + @Override + public String signerName() { + return "mock"; + } + + @Override + public String getKeyInfo() { + return "mock-key"; + } + + @Override + protected void generateSignatureForFile(final File file, final File signature) throws MojoExecutionException { + try { + Files.copy(Paths.get("src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc"), + signature.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to copy mock signature", e); + } + } + }; + } + + private static void assertResolvedDependencies(final String statementJson) { + String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; + String javaVersion = System.getProperty("java.version"); + + assertThatJson(statementJson) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> { + assertThatJson(dep).node("name").isEqualTo("JDK"); + assertThatJson(dep).node("annotations.version").isEqualTo(javaVersion); + }); + + assertThatJson(statementJson) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> assertThatJson(dep).node("name").isEqualTo("Maven")); + + assertThatJson(statementJson) + .node(resolvedDeps).isArray() + .anySatisfy(dep -> assertThatJson(dep).node("uri").isString().startsWith("git+https://github.com/apache/commons-lang.git")); + } + @Test void attestationTest() throws Exception { MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); @@ -102,28 +155,44 @@ void attestationTest() throws Exception { mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); mojo.execute(); - Artifact attestation = project.getAttachedArtifacts().stream() - .filter(a -> "intoto.jsonl".equals(a.getType())) - .findFirst() - .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); + Artifact attestation = getAttestation(project); String json = new String(Files.readAllBytes(attestation.getFile().toPath()), StandardCharsets.UTF_8); - String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; - String javaVersion = System.getProperty("java.version"); + assertResolvedDependencies(json); + } - assertThatJson(json) - .node(resolvedDeps).isArray() - .anySatisfy(dep -> { - assertThatJson(dep).node("name").isEqualTo("JDK"); - assertThatJson(dep).node("annotations.version").isEqualTo(javaVersion); - }); + @Test + void signingTest() throws Exception { + MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); + MavenRepositorySystem repoSystem = container.lookup(MavenRepositorySystem.class); + MavenProject project = createMavenProject(projectHelper, repoSystem); - assertThatJson(json) - .node(resolvedDeps).isArray() - .anySatisfy(dep -> assertThatJson(dep).node("name").isEqualTo("Maven")); + BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); + mojo.setOutputDirectory(new File("target/attestations")); + mojo.setScmDirectory(new File(".")); + mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-lang.git"); + mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); + mojo.setSignAttestation(true); + mojo.setSigner(createMockSigner()); + mojo.execute(); - assertThatJson(json) - .node(resolvedDeps).isArray() - .anySatisfy(dep -> assertThatJson(dep).node("uri").isString().startsWith("git+https://github.com/apache/commons-lang.git")); + Artifact attestation = getAttestation(project); + String envelopeJson = new String(Files.readAllBytes(attestation.getFile().toPath()), StandardCharsets.UTF_8); + + assertThatJson(envelopeJson).node("payloadType").isEqualTo(DsseEnvelope.PAYLOAD_TYPE); + assertThatJson(envelopeJson).node("signatures").isArray().hasSize(1); + assertThatJson(envelopeJson).node("signatures[0].sig").isString().isNotEmpty(); + + DsseEnvelope envelope = OBJECT_MAPPER.readValue(envelopeJson.trim(), DsseEnvelope.class); + String statementJson = new String(envelope.getPayload(), StandardCharsets.UTF_8); + assertResolvedDependencies(statementJson); + } + + private static Artifact getAttestation(MavenProject project) { + return project.getAttachedArtifacts() + .stream() + .filter(a -> "intoto.jsonl".equals(a.getType())) + .findFirst() + .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); } -} +} \ No newline at end of file diff --git a/src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc b/src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc new file mode 100644 index 000000000..c8e60938a --- /dev/null +++ b/src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc @@ -0,0 +1,7 @@ +-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQT03VnJAUi9xSvrkKRTCqXyXCUBHwUCaXYyIwAKCRBTCqXyXCUB +H9E7AQDHPmR05PZJGDiGzz1qJnqTuu/Jo3mS3A9AoHWSJbT6HwD+InboQcE6tCGT +5MpDNFzs2aNqyb9klElvKE2c6o8cJw4= +=Nf0G +-----END PGP SIGNATURE----- From f8876c412112e22a5555e7d142766bb5e968d543 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 16 Apr 2026 21:18:53 +0200 Subject: [PATCH 12/51] fix: build errors --- .../apache/commons/release/plugin/internal/DsseUtils.java | 4 ++-- .../commons/release/plugin/mojos/BuildAttestationMojo.java | 4 ++-- .../release/plugin/mojos/BuildAttestationMojoTest.java | 6 +++--- .../signatures/commons-release-plugin-1.9.2.jar.asc | 7 ------- 4 files changed, 7 insertions(+), 14 deletions(-) delete mode 100644 src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java index 23cac277c..20267a497 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -80,7 +80,7 @@ public static AbstractGpgSigner createGpgSigner(final String executable, final b } /** - * Serializes {@code statement} to JSON using the DSSE Pre-Authentication Encoding (PAE) + * Serializes {@code statement} to JSON using the DSSE Pre-Authentication Encoding (PAE). * *
PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
* @@ -99,7 +99,7 @@ public static Path writePaeFile(final Statement statement, final ObjectMapper ob } /** - * Writes serialized JSON to a file using the DSSE Pre-Authentication Encoding (PAE) + * Writes serialized JSON to a file using the DSSE Pre-Authentication Encoding (PAE). * *
PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
* diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 75346ab82..e08cbe11e 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -568,8 +568,8 @@ private String getScmRevision() throws MojoExecutionException { ScmRepository scmRepository = getScmRepository(); CommandParameters commandParameters = new CommandParameters(); try { - InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), new ScmFileSet(scmDirectory) - , commandParameters); + InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), + new ScmFileSet(scmDirectory), commandParameters); return getScmRevision(result); } catch (ScmException e) { diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index c52de3993..ee90bd8d2 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -39,9 +39,9 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Model; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.gpg.AbstractGpgSigner; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; -import org.apache.maven.plugins.gpg.AbstractGpgSigner; import org.apache.maven.rtinfo.RuntimeInformation; import org.apache.maven.scm.manager.ScmManager; import org.codehaus.plexus.PlexusContainer; @@ -113,7 +113,7 @@ public String getKeyInfo() { @Override protected void generateSignatureForFile(final File file, final File signature) throws MojoExecutionException { try { - Files.copy(Paths.get("src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc"), + Files.copy(Paths.get("src/test/resources/mojos/detach-distributions/target/commons-text-1.4.jar.asc"), signature.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (final IOException e) { throw new MojoExecutionException("Failed to copy mock signature", e); @@ -195,4 +195,4 @@ private static Artifact getAttestation(MavenProject project) { .findFirst() .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); } -} \ No newline at end of file +} diff --git a/src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc b/src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc deleted file mode 100644 index c8e60938a..000000000 --- a/src/test/resources/signatures/commons-release-plugin-1.9.2.jar.asc +++ /dev/null @@ -1,7 +0,0 @@ ------BEGIN PGP SIGNATURE----- - -iHUEABYKAB0WIQT03VnJAUi9xSvrkKRTCqXyXCUBHwUCaXYyIwAKCRBTCqXyXCUB -H9E7AQDHPmR05PZJGDiGzz1qJnqTuu/Jo3mS3A9AoHWSJbT6HwD+InboQcE6tCGT -5MpDNFzs2aNqyb9klElvKE2c6o8cJw4= -=Nf0G ------END PGP SIGNATURE----- From affb0a77124beeb896e30bf6175ae498c9ce668a Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 16 Apr 2026 21:36:16 +0200 Subject: [PATCH 13/51] fix: base tests on already existing test resources --- .../mojos/BuildAttestationMojoTest.java | 97 +++++++++++++------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index ee90bd8d2..210b27f67 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -53,6 +53,8 @@ public class BuildAttestationMojoTest { + private static final String ARTIFACTS_DIR = "src/test/resources/mojos/detach-distributions/target/"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @TempDir @@ -85,16 +87,23 @@ private static BuildAttestationMojo createBuildAttestationMojo(MavenProject proj createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); } - private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) throws ComponentLookupException { + private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) { MavenProject project = new MavenProject(new Model()); - Artifact artifact = repoSystem.createArtifact("groupId", "artifactId", "1.2.3", null, "jar"); + Artifact artifact = repoSystem.createArtifact("org.apache.commons", "commons-text", "1.4", null, "jar"); + artifact.setFile(new File(ARTIFACTS_DIR + "commons-text-1.4.jar")); project.setArtifact(artifact); - project.setGroupId("groupId"); - project.setArtifactId("artifactId"); - project.setVersion("1.2.3"); - // Attach a couple of artifacts - projectHelper.attachArtifact(project, "pom", null, new File("src/test/resources/artifacts/artifact-pom.txt")); - artifact.setFile(new File("src/test/resources/artifacts/artifact-jar.txt")); + project.setGroupId("org.apache.commons"); + project.setArtifactId("commons-text"); + project.setVersion("1.4"); + projectHelper.attachArtifact(project, "pom", null, new File(ARTIFACTS_DIR + "commons-text-1.4.pom")); + projectHelper.attachArtifact(project, "jar", "sources", new File(ARTIFACTS_DIR + "commons-text-1.4-sources.jar")); + projectHelper.attachArtifact(project, "jar", "javadoc", new File(ARTIFACTS_DIR + "commons-text-1.4-javadoc.jar")); + projectHelper.attachArtifact(project, "jar", "tests", new File(ARTIFACTS_DIR + "commons-text-1.4-tests.jar")); + projectHelper.attachArtifact(project, "jar", "test-sources", new File(ARTIFACTS_DIR + "commons-text-1.4-test-sources.jar")); + projectHelper.attachArtifact(project, "tar.gz", "bin", new File(ARTIFACTS_DIR + "commons-text-1.4-bin.tar.gz")); + projectHelper.attachArtifact(project, "zip", "bin", new File(ARTIFACTS_DIR + "commons-text-1.4-bin.zip")); + projectHelper.attachArtifact(project, "tar.gz", "src", new File(ARTIFACTS_DIR + "commons-text-1.4-src.tar.gz")); + projectHelper.attachArtifact(project, "zip", "src", new File(ARTIFACTS_DIR + "commons-text-1.4-src.zip")); return project; } @@ -113,8 +122,7 @@ public String getKeyInfo() { @Override protected void generateSignatureForFile(final File file, final File signature) throws MojoExecutionException { try { - Files.copy(Paths.get("src/test/resources/mojos/detach-distributions/target/commons-text-1.4.jar.asc"), - signature.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy(Paths.get(ARTIFACTS_DIR + "commons-text-1.4.jar.asc"), signature.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (final IOException e) { throw new MojoExecutionException("Failed to copy mock signature", e); } @@ -122,7 +130,44 @@ protected void generateSignatureForFile(final File file, final File signature) t }; } - private static void assertResolvedDependencies(final String statementJson) { + private static Artifact getAttestation(final MavenProject project) { + return project.getAttachedArtifacts().stream() + .filter(a -> "intoto.jsonl".equals(a.getType())) + .findFirst() + .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); + } + + private static void assertSubject(final String statementJson, final String name, final String sha256) { + assertThatJson(statementJson) + .node("subject").isArray() + .anySatisfy(s -> { + assertThatJson(s).node("name").isEqualTo(name); + assertThatJson(s).node("digest.sha256").isEqualTo(sha256); + }); + } + + private static void assertStatementContent(final String statementJson) { + assertSubject(statementJson, "commons-text-1.4.jar", + "ad2d2eacf15ab740c115294afc1192603d8342004a6d7d0ad35446f7dda8a134"); + assertSubject(statementJson, "commons-text-1.4.pom", + "4d6277b1e0720bb054c640620679a9da120f753029342150e714095f48934d76"); + assertSubject(statementJson, "commons-text-1.4-sources.jar", + "58a95591fe7fc94db94a0a9e64b4a5bcc1c49edf17f2b24d7c0747357d855761"); + assertSubject(statementJson, "commons-text-1.4-javadoc.jar", + "42f5b341d0fbeaa30b06aed90612840bc513fb39792c3d39446510670216e8b1"); + assertSubject(statementJson, "commons-text-1.4-tests.jar", + "e4e365d08d601a4bda44be2a31f748b96762504d301742d4a0f7f5953d4c793a"); + assertSubject(statementJson, "commons-text-1.4-test-sources.jar", + "9200a2a41b35f2d6d30c1c698308591cf577547ec39514657dff0e2f7dff18ca"); + assertSubject(statementJson, "commons-text-1.4-bin.tar.gz", + "8b9393f7ddc2efb69d8c2b6f4d85d8711dddfe77009799cf21619fc9b8411897"); + assertSubject(statementJson, "commons-text-1.4-bin.zip", + "ad3732dcb38e510b1dbb1544115d0eb797fab61afe0008fdb187cd4ef1706cd7"); + assertSubject(statementJson, "commons-text-1.4-src.tar.gz", + "1cb8536c375c3cff66757fd40c2bf878998254ba0a247866a6536bd48ba2e88a"); + assertSubject(statementJson, "commons-text-1.4-src.zip", + "e4a6c992153faae4f7faff689b899073000364e376736b9746a5d0acb9d8b980"); + String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; String javaVersion = System.getProperty("java.version"); @@ -132,14 +177,13 @@ private static void assertResolvedDependencies(final String statementJson) { assertThatJson(dep).node("name").isEqualTo("JDK"); assertThatJson(dep).node("annotations.version").isEqualTo(javaVersion); }); - assertThatJson(statementJson) .node(resolvedDeps).isArray() .anySatisfy(dep -> assertThatJson(dep).node("name").isEqualTo("Maven")); - assertThatJson(statementJson) .node(resolvedDeps).isArray() - .anySatisfy(dep -> assertThatJson(dep).node("uri").isString().startsWith("git+https://github.com/apache/commons-lang.git")); + .anySatisfy(dep -> assertThatJson(dep).node("uri").isString() + .startsWith("git+https://github.com/apache/commons-text.git")); } @Test @@ -151,14 +195,12 @@ void attestationTest() throws Exception { BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); mojo.setOutputDirectory(new File("target/attestations")); mojo.setScmDirectory(new File(".")); - mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-lang.git"); + mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); mojo.execute(); - Artifact attestation = getAttestation(project); - String json = new String(Files.readAllBytes(attestation.getFile().toPath()), StandardCharsets.UTF_8); - - assertResolvedDependencies(json); + String json = new String(Files.readAllBytes(getAttestation(project).getFile().toPath()), StandardCharsets.UTF_8); + assertStatementContent(json); } @Test @@ -170,14 +212,13 @@ void signingTest() throws Exception { BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); mojo.setOutputDirectory(new File("target/attestations")); mojo.setScmDirectory(new File(".")); - mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-lang.git"); + mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); mojo.setSignAttestation(true); mojo.setSigner(createMockSigner()); mojo.execute(); - Artifact attestation = getAttestation(project); - String envelopeJson = new String(Files.readAllBytes(attestation.getFile().toPath()), StandardCharsets.UTF_8); + String envelopeJson = new String(Files.readAllBytes(getAttestation(project).getFile().toPath()), StandardCharsets.UTF_8); assertThatJson(envelopeJson).node("payloadType").isEqualTo(DsseEnvelope.PAYLOAD_TYPE); assertThatJson(envelopeJson).node("signatures").isArray().hasSize(1); @@ -185,14 +226,6 @@ void signingTest() throws Exception { DsseEnvelope envelope = OBJECT_MAPPER.readValue(envelopeJson.trim(), DsseEnvelope.class); String statementJson = new String(envelope.getPayload(), StandardCharsets.UTF_8); - assertResolvedDependencies(statementJson); - } - - private static Artifact getAttestation(MavenProject project) { - return project.getAttachedArtifacts() - .stream() - .filter(a -> "intoto.jsonl".equals(a.getType())) - .findFirst() - .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); + assertStatementContent(statementJson); } -} +} \ No newline at end of file From 3e4deda6991d7786f54c9330c6ea2a062c8d1253 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 16 Apr 2026 21:59:37 +0200 Subject: [PATCH 14/51] fix: refactor external parameters from BuildAttestationMojo --- ...Descriptors.java => BuildDefinitions.java} | 85 +++++++++++++++++-- .../plugin/mojos/BuildAttestationMojo.java | 80 +---------------- .../plugin/internal/BuildDefinitionsTest.java | 65 ++++++++++++++ .../mojos/BuildAttestationMojoTest.java | 2 +- 4 files changed, 147 insertions(+), 85 deletions(-) rename src/main/java/org/apache/commons/release/plugin/internal/{BuildToolDescriptors.java => BuildDefinitions.java} (56%) create mode 100644 src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java similarity index 56% rename from src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java rename to src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index 5dac9a192..dc51d0730 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildToolDescriptors.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -18,21 +18,26 @@ import java.io.IOException; import java.io.InputStream; +import java.lang.management.ManagementFactory; import java.nio.file.Path; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; +import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.execution.MavenSession; /** - * Factory methods for {@link ResourceDescriptor} instances representing build-tool dependencies. + * Factory methods for the SLSA {@code BuildDefinition} fields: JVM, Maven descriptors and external build parameters. */ -public final class BuildToolDescriptors { +public final class BuildDefinitions { - /** No instances. */ - private BuildToolDescriptors() { - // no instantiation + /** + * No instances. + */ + private BuildDefinitions() { } /** @@ -72,9 +77,9 @@ public static ResourceDescriptor jvm(Path javaHome) throws IOException { * Plugin code runs in an isolated Plugin Classloader, which does see that resources. Therefore, we need to pass the classloader from a class from * Maven Core, such as {@link org.apache.maven.rtinfo.RuntimeInformation}.

* - * @param version Maven version string - * @param mavenHome path to the Maven home directory - * @param coreClassLoader a classloader from Maven's Core Classloader realm, used to load core resources + * @param version Maven version string + * @param mavenHome path to the Maven home directory + * @param coreClassLoader a classloader from Maven's Core Classloader realm, used to load core resources * @return a descriptor for the Maven installation * @throws IOException if hashing the Maven home directory fails */ @@ -98,4 +103,68 @@ public static ResourceDescriptor maven(String version, Path mavenHome, ClassLoad } return descriptor; } + + /** + * Returns a map of external build parameters captured from the current JVM and Maven session. + * + * @param session the current Maven session + * @return a map of parameter names to values + */ + public static Map externalParameters(final MavenSession session) { + Map params = new HashMap<>(); + params.put("jvm.args", ManagementFactory.getRuntimeMXBean().getInputArguments()); + MavenExecutionRequest request = session.getRequest(); + params.put("maven.goals", request.getGoals()); + params.put("maven.profiles", request.getActiveProfiles()); + params.put("maven.user.properties", request.getUserProperties()); + params.put("maven.cmdline", commandLine(request)); + Map env = new HashMap<>(); + params.put("env", env); + for (Map.Entry entry : System.getenv().entrySet()) { + String key = entry.getKey(); + if ("TZ".equals(key) || "LANG".equals(key) || key.startsWith("LC_")) { + env.put(key, entry.getValue()); + } + } + return params; + } + + /** + * Reconstructs the Maven command line string from the given execution request. + * + * @param request the Maven execution request + * @return a string representation of the Maven command line + */ + static String commandLine(final MavenExecutionRequest request) { + StringBuilder sb = new StringBuilder(); + for (String goal : request.getGoals()) { + sb.append(goal).append(" "); + } + List activeProfiles = request.getActiveProfiles(); + if (activeProfiles != null && !activeProfiles.isEmpty()) { + sb.append("-P"); + for (String profile : activeProfiles) { + sb.append(profile).append(","); + } + removeLast(sb); + sb.append(" "); + } + Properties userProperties = request.getUserProperties(); + for (String propertyName : userProperties.stringPropertyNames()) { + sb.append("-D").append(propertyName).append("=").append(userProperties.get(propertyName)).append(" "); + } + removeLast(sb); + return sb.toString(); + } + + /** + * Removes last character from a {@link StringBuilder}. + * + * @param sb The {@link StringBuilder} to use + */ + private static void removeLast(final StringBuilder sb) { + if (sb.length() > 0) { + sb.setLength(sb.length() - 1); + } + } } diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index e08cbe11e..32fe3b341 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -19,7 +19,6 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.lang.management.ManagementFactory; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -30,7 +29,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import javax.inject.Inject; @@ -39,7 +37,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import org.apache.commons.release.plugin.internal.ArtifactUtils; -import org.apache.commons.release.plugin.internal.BuildToolDescriptors; +import org.apache.commons.release.plugin.internal.BuildDefinitions; import org.apache.commons.release.plugin.internal.DsseUtils; import org.apache.commons.release.plugin.internal.GitUtils; import org.apache.commons.release.plugin.slsa.v1_2.BuildDefinition; @@ -52,7 +50,6 @@ import org.apache.commons.release.plugin.slsa.v1_2.Signature; import org.apache.commons.release.plugin.slsa.v1_2.Statement; import org.apache.maven.artifact.Artifact; -import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; @@ -298,7 +295,7 @@ private AbstractGpgSigner getSigner() throws MojoFailureException { public void execute() throws MojoFailureException, MojoExecutionException { // Build definition BuildDefinition buildDefinition = new BuildDefinition(); - buildDefinition.setExternalParameters(getExternalParameters()); + buildDefinition.setExternalParameters(BuildDefinitions.externalParameters(session)); buildDefinition.setResolvedDependencies(getBuildDependencies()); // Builder Builder builder = new Builder(); @@ -423,75 +420,6 @@ private List getSubjects() throws MojoExecutionException { return subjects; } - /** - * Gets map of external build parameters captured from the current JVM and Maven session. - * - * @return A map of parameter names to values. - */ - private Map getExternalParameters() { - Map params = new HashMap<>(); - params.put("jvm.args", ManagementFactory.getRuntimeMXBean().getInputArguments()); - MavenExecutionRequest request = session.getRequest(); - params.put("maven.goals", request.getGoals()); - params.put("maven.profiles", request.getActiveProfiles()); - params.put("maven.user.properties", request.getUserProperties()); - params.put("maven.cmdline", getCommandLine(request)); - Map env = new HashMap<>(); - params.put("env", env); - for (Map.Entry entry : System.getenv().entrySet()) { - String key = entry.getKey(); - if ("TZ".equals(key) || "LANG".equals(key) || key.startsWith("LC_")) { - env.put(key, entry.getValue()); - } - } - return params; - } - - /** - * Reconstructs the Maven command line string from the given execution request. - * - * @param request The Maven execution request. - * @return A string representation of the Maven command line. - */ - private static String getCommandLine(final MavenExecutionRequest request) { - StringBuilder sb = new StringBuilder(); - for (String goal : request.getGoals()) { - sb.append(goal); - sb.append(" "); - } - List activeProfiles = request.getActiveProfiles(); - if (activeProfiles != null && !activeProfiles.isEmpty()) { - sb.append("-P"); - for (String profile : activeProfiles) { - sb.append(profile); - sb.append(","); - } - removeLast(sb); - sb.append(" "); - } - Properties userProperties = request.getUserProperties(); - for (String propertyName : userProperties.stringPropertyNames()) { - sb.append("-D"); - sb.append(propertyName); - sb.append("="); - sb.append(userProperties.get(propertyName)); - sb.append(" "); - } - removeLast(sb); - return sb.toString(); - } - - /** - * Removes the last character from the given {@link StringBuilder} if it is non-empty. - * - * @param sb The string builder to trim. - */ - private static void removeLast(final StringBuilder sb) { - if (sb.length() > 0) { - sb.setLength(sb.length() - 1); - } - } - /** * Returns resource descriptors for the JVM, Maven installation, SCM source, and project dependencies. * @@ -501,8 +429,8 @@ private static void removeLast(final StringBuilder sb) { private List getBuildDependencies() throws MojoExecutionException { List dependencies = new ArrayList<>(); try { - dependencies.add(BuildToolDescriptors.jvm(Paths.get(System.getProperty("java.home")))); - dependencies.add(BuildToolDescriptors.maven(runtimeInformation.getMavenVersion(), mavenHome.toPath(), + dependencies.add(BuildDefinitions.jvm(Paths.get(System.getProperty("java.home")))); + dependencies.add(BuildDefinitions.maven(runtimeInformation.getMavenVersion(), mavenHome.toPath(), runtimeInformation.getClass().getClassLoader())); dependencies.add(getScmDescriptor()); } catch (IOException e) { diff --git a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java new file mode 100644 index 000000000..aa9081d71 --- /dev/null +++ b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Properties; +import java.util.stream.Stream; + +import org.apache.maven.execution.DefaultMavenExecutionRequest; +import org.apache.maven.execution.MavenExecutionRequest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class BuildDefinitionsTest { + + static Stream commandLineArguments() { + return Stream.of( + Arguments.of("empty", emptyList(), emptyList(), new Properties(), ""), + Arguments.of("single goal", singletonList("verify"), emptyList(), new Properties(), "verify"), + Arguments.of("multiple goals", asList("clean", "verify"), emptyList(), new Properties(), "clean verify"), + Arguments.of("single profile", singletonList("verify"), singletonList("release"), new Properties(), "verify -Prelease"), + Arguments.of("multiple profiles", singletonList("verify"), asList("release", "sign"), new Properties(), "verify -Prelease,sign"), + Arguments.of("user property", singletonList("verify"), emptyList(), props("foo", "bar"), "verify -Dfoo=bar"), + Arguments.of("goals, profile and property", singletonList("verify"), singletonList("release"), props("foo", "bar"), + "verify -Prelease -Dfoo=bar") + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("commandLineArguments") + void commandLineTest(final String description, final List goals, final List profiles, + final Properties userProperties, final String expected) { + MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setGoals(goals); + request.setActiveProfiles(profiles); + request.setUserProperties(userProperties); + assertThat(BuildDefinitions.commandLine(request)).isEqualTo(expected); + } + + private static Properties props(final String key, final String value) { + Properties p = new Properties(); + p.setProperty(key, value); + return p; + } +} diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 210b27f67..b5d527294 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -228,4 +228,4 @@ void signingTest() throws Exception { String statementJson = new String(envelope.getPayload(), StandardCharsets.UTF_8); assertStatementContent(statementJson); } -} \ No newline at end of file +} From f519b3670795da3fb4f43b6af1f727eadf8e6800 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 11:20:38 +0200 Subject: [PATCH 15/51] fix: remove usage of AssertJ (and Hamcrest) --- pom.xml | 2 +- .../plugin/internal/BuildDefinitionsTest.java | 4 +- .../mojos/BuildAttestationMojoTest.java | 55 +++++++++++-------- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 26a71467f..c64183e45 100644 --- a/pom.xml +++ b/pom.xml @@ -239,7 +239,7 @@
net.javacrumbs.json-unit - json-unit-assertj + json-unit 2.40.1 test diff --git a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java index aa9081d71..d826d7c29 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java @@ -19,7 +19,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List; import java.util.Properties; @@ -54,7 +54,7 @@ void commandLineTest(final String description, final List goals, final L request.setGoals(goals); request.setActiveProfiles(profiles); request.setUserProperties(userProperties); - assertThat(BuildDefinitions.commandLine(request)).isEqualTo(expected); + assertEquals(expected, BuildDefinitions.commandLine(request)); } private static Properties props(final String key, final String value) { diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index b5d527294..8d5da2465 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -16,7 +16,10 @@ */ package org.apache.commons.release.plugin.mojos; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static net.javacrumbs.jsonunit.JsonAssert.assertJsonNodeAbsent; +import static net.javacrumbs.jsonunit.JsonAssert.assertJsonNodePresent; +import static net.javacrumbs.jsonunit.JsonAssert.assertJsonPartEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; @@ -27,7 +30,11 @@ import java.nio.file.StandardCopyOption; import java.util.Date; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import net.javacrumbs.jsonunit.JsonAssert; +import net.javacrumbs.jsonunit.core.Configuration; +import net.javacrumbs.jsonunit.core.Option; import org.apache.commons.release.plugin.internal.MojoUtils; import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope; import org.apache.maven.artifact.Artifact; @@ -54,6 +61,8 @@ public class BuildAttestationMojoTest { private static final String ARTIFACTS_DIR = "src/test/resources/mojos/detach-distributions/target/"; + public static final Configuration IGNORING_CONFIGURATION = JsonAssert.when( + Option.IGNORING_ARRAY_ORDER, Option.IGNORING_EXTRA_ARRAY_ITEMS, Option.IGNORING_EXTRA_FIELDS); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -138,15 +147,12 @@ private static Artifact getAttestation(final MavenProject project) { } private static void assertSubject(final String statementJson, final String name, final String sha256) { - assertThatJson(statementJson) - .node("subject").isArray() - .anySatisfy(s -> { - assertThatJson(s).node("name").isEqualTo(name); - assertThatJson(s).node("digest.sha256").isEqualTo(sha256); - }); + assertJsonPartEquals( + String.format("[{\"name\":\"%s\",\"digest\":{\"sha256\":\"%s\"}}]", name, sha256), + statementJson, "subject", IGNORING_CONFIGURATION); } - private static void assertStatementContent(final String statementJson) { + private static void assertStatementContent(final String statementJson) throws IOException { assertSubject(statementJson, "commons-text-1.4.jar", "ad2d2eacf15ab740c115294afc1192603d8342004a6d7d0ad35446f7dda8a134"); assertSubject(statementJson, "commons-text-1.4.pom", @@ -171,19 +177,19 @@ private static void assertStatementContent(final String statementJson) { String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; String javaVersion = System.getProperty("java.version"); - assertThatJson(statementJson) - .node(resolvedDeps).isArray() - .anySatisfy(dep -> { - assertThatJson(dep).node("name").isEqualTo("JDK"); - assertThatJson(dep).node("annotations.version").isEqualTo(javaVersion); - }); - assertThatJson(statementJson) - .node(resolvedDeps).isArray() - .anySatisfy(dep -> assertThatJson(dep).node("name").isEqualTo("Maven")); - assertThatJson(statementJson) - .node(resolvedDeps).isArray() - .anySatisfy(dep -> assertThatJson(dep).node("uri").isString() - .startsWith("git+https://github.com/apache/commons-text.git")); + assertJsonPartEquals( + "[{\"name\":\"JDK\",\"annotations\":{\"version\":\"" + javaVersion + "\"}}]", + statementJson, resolvedDeps, IGNORING_CONFIGURATION); + assertJsonPartEquals("[{\"name\":\"Maven\"}]", statementJson, resolvedDeps, IGNORING_CONFIGURATION); + String gitUriPrefix = "git+https://github.com/apache/commons-text.git"; + boolean hasGitUri = false; + for (JsonNode dep : OBJECT_MAPPER.readTree(statementJson).at("/predicate/buildDefinition/resolvedDependencies")) { + if (dep.path("uri").asText().startsWith(gitUriPrefix)) { + hasGitUri = true; + break; + } + } + assertTrue(hasGitUri, "No resolved dependency with URI starting with " + gitUriPrefix); } @Test @@ -220,9 +226,10 @@ void signingTest() throws Exception { String envelopeJson = new String(Files.readAllBytes(getAttestation(project).getFile().toPath()), StandardCharsets.UTF_8); - assertThatJson(envelopeJson).node("payloadType").isEqualTo(DsseEnvelope.PAYLOAD_TYPE); - assertThatJson(envelopeJson).node("signatures").isArray().hasSize(1); - assertThatJson(envelopeJson).node("signatures[0].sig").isString().isNotEmpty(); + assertJsonPartEquals(DsseEnvelope.PAYLOAD_TYPE, envelopeJson, "payloadType"); + assertJsonNodePresent(envelopeJson, "signatures[0]"); + assertJsonNodeAbsent(envelopeJson, "signatures[1]"); + assertJsonPartEquals("${json-unit.regex}.+", envelopeJson, "signatures[0].sig"); DsseEnvelope envelope = OBJECT_MAPPER.readValue(envelopeJson.trim(), DsseEnvelope.class); String statementJson = new String(envelope.getPayload(), StandardCharsets.UTF_8); From c82977f43181ee13820c8a8f9aa901de608585fb Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 12:09:45 +0200 Subject: [PATCH 16/51] fix: add attestation example and use in tests --- .../plugin/mojos/BuildAttestationMojo.java | 2 +- .../mojos/BuildAttestationMojoTest.java | 102 +++++------- .../attestations/commons-text-1.4.intoto.json | 152 ++++++++++++++++++ 3 files changed, 195 insertions(+), 61 deletions(-) create mode 100644 src/test/resources/attestations/commons-text-1.4.intoto.json diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 32fe3b341..a2f586995 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -538,6 +538,6 @@ private String getScmRevision(final InfoScmResult result) throws MojoExecutionEx private BuildMetadata getBuildMetadata() { OffsetDateTime startedOn = session.getStartTime().toInstant().atOffset(ZoneOffset.UTC); OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC); - return new BuildMetadata(session.getRequest().getBuilderId(), startedOn, finishedOn); + return new BuildMetadata(null, startedOn, finishedOn); } } diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 8d5da2465..434058e45 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -16,24 +16,26 @@ */ package org.apache.commons.release.plugin.mojos; +import static net.javacrumbs.jsonunit.JsonAssert.assertJsonEquals; import static net.javacrumbs.jsonunit.JsonAssert.assertJsonNodeAbsent; import static net.javacrumbs.jsonunit.JsonAssert.assertJsonNodePresent; import static net.javacrumbs.jsonunit.JsonAssert.assertJsonPartEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static net.javacrumbs.jsonunit.JsonAssert.whenIgnoringPaths; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.time.Instant; import java.util.Date; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import net.javacrumbs.jsonunit.JsonAssert; -import net.javacrumbs.jsonunit.core.Configuration; import net.javacrumbs.jsonunit.core.Option; import org.apache.commons.release.plugin.internal.MojoUtils; import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope; @@ -45,6 +47,7 @@ import org.apache.maven.execution.MavenExecutionResult; import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.gpg.AbstractGpgSigner; import org.apache.maven.project.MavenProject; @@ -61,8 +64,6 @@ public class BuildAttestationMojoTest { private static final String ARTIFACTS_DIR = "src/test/resources/mojos/detach-distributions/target/"; - public static final Configuration IGNORING_CONFIGURATION = JsonAssert.when( - Option.IGNORING_ARRAY_ORDER, Option.IGNORING_EXTRA_ARRAY_ITEMS, Option.IGNORING_EXTRA_FIELDS); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -71,16 +72,20 @@ public class BuildAttestationMojoTest { private static PlexusContainer container; private static RepositorySystemSession repoSession; + private static JsonNode expectedStatement; @BeforeAll static void setup() throws Exception { container = MojoUtils.setupContainer(); repoSession = MojoUtils.createRepositorySystemSession(container, localRepositoryPath); + try (InputStream in = BuildAttestationMojoTest.class.getResourceAsStream("/attestations/commons-text-1.4.intoto.json")) { + expectedStatement = OBJECT_MAPPER.readTree(in); + } } private static MavenExecutionRequest createMavenExecutionRequest() { DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); - request.setStartTime(new Date()); + request.setStartTime(Date.from(Instant.parse("2026-04-20T09:28:44Z"))); return request; } @@ -96,15 +101,19 @@ private static BuildAttestationMojo createBuildAttestationMojo(MavenProject proj createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); } - private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) { - MavenProject project = new MavenProject(new Model()); - Artifact artifact = repoSystem.createArtifact("org.apache.commons", "commons-text", "1.4", null, "jar"); + private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) throws Exception { + File pomFile = new File(ARTIFACTS_DIR + "commons-text-1.4.pom"); + Model model; + try (InputStream in = Files.newInputStream(pomFile.toPath())) { + model = new MavenXpp3Reader().read(in); + } + // Group id is inherited from the missing parent, so we override it + model.setGroupId("org.apache.commons"); + MavenProject project = new MavenProject(model); + Artifact artifact = repoSystem.createArtifact(model.getArtifactId(), model.getArtifactId(), model.getVersion(), null, "jar"); artifact.setFile(new File(ARTIFACTS_DIR + "commons-text-1.4.jar")); project.setArtifact(artifact); - project.setGroupId("org.apache.commons"); - project.setArtifactId("commons-text"); - project.setVersion("1.4"); - projectHelper.attachArtifact(project, "pom", null, new File(ARTIFACTS_DIR + "commons-text-1.4.pom")); + projectHelper.attachArtifact(project, "pom", null, pomFile); projectHelper.attachArtifact(project, "jar", "sources", new File(ARTIFACTS_DIR + "commons-text-1.4-sources.jar")); projectHelper.attachArtifact(project, "jar", "javadoc", new File(ARTIFACTS_DIR + "commons-text-1.4-javadoc.jar")); projectHelper.attachArtifact(project, "jar", "tests", new File(ARTIFACTS_DIR + "commons-text-1.4-tests.jar")); @@ -146,50 +155,23 @@ private static Artifact getAttestation(final MavenProject project) { .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); } - private static void assertSubject(final String statementJson, final String name, final String sha256) { - assertJsonPartEquals( - String.format("[{\"name\":\"%s\",\"digest\":{\"sha256\":\"%s\"}}]", name, sha256), - statementJson, "subject", IGNORING_CONFIGURATION); - } - - private static void assertStatementContent(final String statementJson) throws IOException { - assertSubject(statementJson, "commons-text-1.4.jar", - "ad2d2eacf15ab740c115294afc1192603d8342004a6d7d0ad35446f7dda8a134"); - assertSubject(statementJson, "commons-text-1.4.pom", - "4d6277b1e0720bb054c640620679a9da120f753029342150e714095f48934d76"); - assertSubject(statementJson, "commons-text-1.4-sources.jar", - "58a95591fe7fc94db94a0a9e64b4a5bcc1c49edf17f2b24d7c0747357d855761"); - assertSubject(statementJson, "commons-text-1.4-javadoc.jar", - "42f5b341d0fbeaa30b06aed90612840bc513fb39792c3d39446510670216e8b1"); - assertSubject(statementJson, "commons-text-1.4-tests.jar", - "e4e365d08d601a4bda44be2a31f748b96762504d301742d4a0f7f5953d4c793a"); - assertSubject(statementJson, "commons-text-1.4-test-sources.jar", - "9200a2a41b35f2d6d30c1c698308591cf577547ec39514657dff0e2f7dff18ca"); - assertSubject(statementJson, "commons-text-1.4-bin.tar.gz", - "8b9393f7ddc2efb69d8c2b6f4d85d8711dddfe77009799cf21619fc9b8411897"); - assertSubject(statementJson, "commons-text-1.4-bin.zip", - "ad3732dcb38e510b1dbb1544115d0eb797fab61afe0008fdb187cd4ef1706cd7"); - assertSubject(statementJson, "commons-text-1.4-src.tar.gz", - "1cb8536c375c3cff66757fd40c2bf878998254ba0a247866a6536bd48ba2e88a"); - assertSubject(statementJson, "commons-text-1.4-src.zip", - "e4a6c992153faae4f7faff689b899073000364e376736b9746a5d0acb9d8b980"); - - String resolvedDeps = "predicate.buildDefinition.resolvedDependencies"; - String javaVersion = System.getProperty("java.version"); - - assertJsonPartEquals( - "[{\"name\":\"JDK\",\"annotations\":{\"version\":\"" + javaVersion + "\"}}]", - statementJson, resolvedDeps, IGNORING_CONFIGURATION); - assertJsonPartEquals("[{\"name\":\"Maven\"}]", statementJson, resolvedDeps, IGNORING_CONFIGURATION); - String gitUriPrefix = "git+https://github.com/apache/commons-text.git"; - boolean hasGitUri = false; - for (JsonNode dep : OBJECT_MAPPER.readTree(statementJson).at("/predicate/buildDefinition/resolvedDependencies")) { - if (dep.path("uri").asText().startsWith(gitUriPrefix)) { - hasGitUri = true; - break; - } - } - assertTrue(hasGitUri, "No resolved dependency with URI starting with " + gitUriPrefix); + private static void assertStatementContent(final JsonNode statement) { + assertJsonEquals(expectedStatement.get("subject"), statement.get("subject"), + JsonAssert.when(Option.IGNORING_ARRAY_ORDER)); + assertJsonEquals(expectedStatement.get("predicateType"), statement.get("predicateType")); + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/buildType"), + statement.at("/predicate/buildDefinition/buildType")); + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/externalParameters"), + statement.at("/predicate/buildDefinition/externalParameters"), + JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("jvm.args", "env")); + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/internalParameters"), + statement.at("/predicate/buildDefinition/internalParameters")); + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/resolvedDependencies"), + statement.at("/predicate/buildDefinition/resolvedDependencies"), + JsonAssert.when(Option.IGNORING_VALUES)); + assertJsonEquals(expectedStatement.at("/predicate/runDetails"), + statement.at("/predicate/runDetails"), + whenIgnoringPaths("metadata.finishedOn")); } @Test @@ -205,8 +187,8 @@ void attestationTest() throws Exception { mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); mojo.execute(); - String json = new String(Files.readAllBytes(getAttestation(project).getFile().toPath()), StandardCharsets.UTF_8); - assertStatementContent(json); + JsonNode statement = OBJECT_MAPPER.readTree(getAttestation(project).getFile()); + assertStatementContent(statement); } @Test @@ -232,7 +214,7 @@ void signingTest() throws Exception { assertJsonPartEquals("${json-unit.regex}.+", envelopeJson, "signatures[0].sig"); DsseEnvelope envelope = OBJECT_MAPPER.readValue(envelopeJson.trim(), DsseEnvelope.class); - String statementJson = new String(envelope.getPayload(), StandardCharsets.UTF_8); - assertStatementContent(statementJson); + JsonNode statement = OBJECT_MAPPER.readTree(envelope.getPayload()); + assertStatementContent(statement); } } diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json new file mode 100644 index 000000000..d03957dbb --- /dev/null +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -0,0 +1,152 @@ +{ + "subject": [ + { + "name": "commons-text-1.4.jar", + "uri": "pkg:maven/commons-text/commons-text@1.4?type=jar", + "digest": { + "sha256": "ad2d2eacf15ab740c115294afc1192603d8342004a6d7d0ad35446f7dda8a134" + } + }, + { + "name": "commons-text-1.4.pom", + "uri": "pkg:maven/commons-text/commons-text@1.4?type=pom", + "digest": { + "sha256": "4d6277b1e0720bb054c640620679a9da120f753029342150e714095f48934d76" + } + }, + { + "name": "commons-text-1.4-sources.jar", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=sources&type=jar", + "digest": { + "sha256": "58a95591fe7fc94db94a0a9e64b4a5bcc1c49edf17f2b24d7c0747357d855761" + } + }, + { + "name": "commons-text-1.4-javadoc.jar", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=javadoc&type=jar", + "digest": { + "sha256": "42f5b341d0fbeaa30b06aed90612840bc513fb39792c3d39446510670216e8b1" + } + }, + { + "name": "commons-text-1.4-tests.jar", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=tests&type=jar", + "digest": { + "sha256": "e4e365d08d601a4bda44be2a31f748b96762504d301742d4a0f7f5953d4c793a" + } + }, + { + "name": "commons-text-1.4-test-sources.jar", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=test-sources&type=jar", + "digest": { + "sha256": "9200a2a41b35f2d6d30c1c698308591cf577547ec39514657dff0e2f7dff18ca" + } + }, + { + "name": "commons-text-1.4-bin.tar.gz", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=bin&type=tar.gz", + "digest": { + "sha256": "8b9393f7ddc2efb69d8c2b6f4d85d8711dddfe77009799cf21619fc9b8411897" + } + }, + { + "name": "commons-text-1.4-bin.zip", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=bin&type=zip", + "digest": { + "sha256": "ad3732dcb38e510b1dbb1544115d0eb797fab61afe0008fdb187cd4ef1706cd7" + } + }, + { + "name": "commons-text-1.4-src.tar.gz", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=src&type=tar.gz", + "digest": { + "sha256": "1cb8536c375c3cff66757fd40c2bf878998254ba0a247866a6536bd48ba2e88a" + } + }, + { + "name": "commons-text-1.4-src.zip", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=src&type=zip", + "digest": { + "sha256": "e4a6c992153faae4f7faff689b899073000364e376736b9746a5d0acb9d8b980" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://commons.apache.org/builds/0.1.0", + "externalParameters": { + "maven.profiles": [], + "maven.cmdline": "", + "jvm.args": [ + "-Dfile.encoding=UTF-8", + "-Dsun.stdout.encoding=UTF-8", + "-Dsun.stderr.encoding=UTF-8" + ], + "maven.user.properties": {}, + "maven.goals": [], + "env": { + "LANG": "pl_PL.UTF-8" + } + }, + "internalParameters": {}, + "resolvedDependencies": [ + { + "name": "JDK", + "digest": { + "gitTree": "bdb67e47c1b7df9c35ae045f29a348bb5bd32dc3" + }, + "annotations": { + "vendor.version": "Temurin-25.0.2+10", + "specification.name": "Java Platform API Specification", + "specification.vendor": "Oracle Corporation", + "vm.version": "25.0.2+10-LTS", + "version": "25.0.2", + "version.date": "2026-01-20", + "vm.specification.vendor": "Oracle Corporation", + "home": "/usr/lib/jvm/temurin-25-jdk-amd64", + "vendor": "Eclipse Adoptium", + "vm.vendor": "Eclipse Adoptium", + "vendor.url": "https://adoptium.net/", + "specification.maintenance.version": null, + "vm.specification.version": "25", + "vm.name": "OpenJDK 64-Bit Server VM", + "vm.specification.name": "Java Virtual Machine Specification", + "specification.version": "25" + } + }, + { + "name": "Maven", + "uri": "pkg:maven/org.apache.maven/apache-maven@3.9.12", + "digest": { + "gitTree": "3cdb4a67690dc18373f70ead98dc86567cc5ad67" + }, + "annotations": { + "distributionId": "apache-maven", + "distributionName": "Apache Maven", + "distributionShortName": "Maven", + "buildNumber": "848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1", + "version": "3.9.12" + } + }, + { + "uri": "git+https://github.com/apache/commons-text.git@feat/slsa", + "digest": { + "gitCommit": "f519b3670795da3fb4f43b6af1f727eadf8e6800" + } + } + ] + }, + "runDetails": { + "builder": { + "id": "https://commons.apache.org/builds/0.1.0", + "builderDependencies": [], + "version": {} + }, + "metadata": { + "startedOn": "2026-04-20T09:28:44Z", + "finishedOn": "2026-04-20T09:28:45Z" + } + } + } +} From 595fbdcc848d870d09d8cd7d58c46323dd14f5a3 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 12:18:01 +0200 Subject: [PATCH 17/51] fix: add Maven invocation parameters --- .../plugin/mojos/BuildAttestationMojoTest.java | 7 +++++++ .../attestations/commons-text-1.4.intoto.json | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 434058e45..7219ee5fd 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -31,7 +31,9 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.Instant; +import java.util.Collections; import java.util.Date; +import java.util.Properties; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -86,6 +88,11 @@ static void setup() throws Exception { private static MavenExecutionRequest createMavenExecutionRequest() { DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); request.setStartTime(Date.from(Instant.parse("2026-04-20T09:28:44Z"))); + request.setActiveProfiles(Collections.singletonList("release")); + request.setGoals(Collections.singletonList("deploy")); + Properties userProperties = new Properties(); + userProperties.setProperty("gpg.keyname", "3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9"); + request.setUserProperties(userProperties); return request; } diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json index d03957dbb..ac58c8b6d 100644 --- a/src/test/resources/attestations/commons-text-1.4.intoto.json +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -76,15 +76,21 @@ "buildDefinition": { "buildType": "https://commons.apache.org/builds/0.1.0", "externalParameters": { - "maven.profiles": [], - "maven.cmdline": "", + "maven.profiles": [ + "release" + ], + "maven.cmdline": "deploy -Prelease -Dgpg.keyname=3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9", "jvm.args": [ "-Dfile.encoding=UTF-8", "-Dsun.stdout.encoding=UTF-8", "-Dsun.stderr.encoding=UTF-8" ], - "maven.user.properties": {}, - "maven.goals": [], + "maven.user.properties": { + "gpg.keyname": "3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9" + }, + "maven.goals": [ + "deploy" + ], "env": { "LANG": "pl_PL.UTF-8" } @@ -108,7 +114,7 @@ "vendor": "Eclipse Adoptium", "vm.vendor": "Eclipse Adoptium", "vendor.url": "https://adoptium.net/", - "specification.maintenance.version": null, + "specification.maintenance.version": "", "vm.specification.version": "25", "vm.name": "OpenJDK 64-Bit Server VM", "vm.specification.name": "Java Virtual Machine Specification", @@ -145,7 +151,7 @@ }, "metadata": { "startedOn": "2026-04-20T09:28:44Z", - "finishedOn": "2026-04-20T09:28:45Z" + "finishedOn": "2026-04-20T09:38:12Z" } } } From 2d3146d16b0aefde766ba2797978c11a3b2e578d Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 12:33:11 +0200 Subject: [PATCH 18/51] fix: failures due to different set of JVM properties --- .../mojos/BuildAttestationMojoTest.java | 23 ++++++++++++++++++- .../attestations/commons-text-1.4.intoto.json | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 7219ee5fd..51565d368 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -21,6 +21,7 @@ import static net.javacrumbs.jsonunit.JsonAssert.assertJsonNodePresent; import static net.javacrumbs.jsonunit.JsonAssert.assertJsonPartEquals; import static net.javacrumbs.jsonunit.JsonAssert.whenIgnoringPaths; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; import java.io.IOException; @@ -33,7 +34,10 @@ import java.time.Instant; import java.util.Collections; import java.util.Date; +import java.util.Iterator; import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -173,14 +177,31 @@ private static void assertStatementContent(final JsonNode statement) { JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("jvm.args", "env")); assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/internalParameters"), statement.at("/predicate/buildDefinition/internalParameters")); + // `[0].annotations` holds JVM system properties; + // Not all properties are available on all JDKs, so they are either null or strings, which json-unit treats as a structural mismatch. + // We will check them below assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/resolvedDependencies"), statement.at("/predicate/buildDefinition/resolvedDependencies"), - JsonAssert.when(Option.IGNORING_VALUES)); + JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("[0].annotations")); + Set expectedJdkFields = fieldNames( + expectedStatement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); + Set actualJdkFields = fieldNames( + statement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); + assertEquals(expectedJdkFields, actualJdkFields); assertJsonEquals(expectedStatement.at("/predicate/runDetails"), statement.at("/predicate/runDetails"), whenIgnoringPaths("metadata.finishedOn")); } + private static Set fieldNames(final JsonNode node) { + Set names = new TreeSet<>(); + Iterator it = node.fieldNames(); + while (it.hasNext()) { + names.add(it.next()); + } + return names; + } + @Test void attestationTest() throws Exception { MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json index ac58c8b6d..5c33198d0 100644 --- a/src/test/resources/attestations/commons-text-1.4.intoto.json +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -114,7 +114,7 @@ "vendor": "Eclipse Adoptium", "vm.vendor": "Eclipse Adoptium", "vendor.url": "https://adoptium.net/", - "specification.maintenance.version": "", + "specification.maintenance.version": null, "vm.specification.version": "25", "vm.name": "OpenJDK 64-Bit Server VM", "vm.specification.name": "Java Virtual Machine Specification", From e633bc1d9a4c7d61a4e70395a4fe90b41da50133 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 12:46:12 +0200 Subject: [PATCH 19/51] fix: `props` to `singletonProperties` --- .../release/plugin/internal/BuildDefinitionsTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java index d826d7c29..605657ebe 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java @@ -40,8 +40,8 @@ static Stream commandLineArguments() { Arguments.of("multiple goals", asList("clean", "verify"), emptyList(), new Properties(), "clean verify"), Arguments.of("single profile", singletonList("verify"), singletonList("release"), new Properties(), "verify -Prelease"), Arguments.of("multiple profiles", singletonList("verify"), asList("release", "sign"), new Properties(), "verify -Prelease,sign"), - Arguments.of("user property", singletonList("verify"), emptyList(), props("foo", "bar"), "verify -Dfoo=bar"), - Arguments.of("goals, profile and property", singletonList("verify"), singletonList("release"), props("foo", "bar"), + Arguments.of("user property", singletonList("verify"), emptyList(), singletonProperties("foo", "bar"), "verify -Dfoo=bar"), + Arguments.of("goals, profile and property", singletonList("verify"), singletonList("release"), singletonProperties("foo", "bar"), "verify -Prelease -Dfoo=bar") ); } @@ -57,7 +57,7 @@ void commandLineTest(final String description, final List goals, final L assertEquals(expected, BuildDefinitions.commandLine(request)); } - private static Properties props(final String key, final String value) { + private static Properties singletonProperties(final String key, final String value) { Properties p = new Properties(); p.setProperty(key, value); return p; From d0ca53497b1586e12a043fa742e0235eff767ff5 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 12:54:57 +0200 Subject: [PATCH 20/51] fix: getter gets --- .../release/plugin/internal/ArtifactUtils.java | 8 ++++---- .../release/plugin/internal/GitUtils.java | 2 +- .../plugin/mojos/BuildAttestationMojo.java | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java index 7f07244e2..b831edfa9 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -36,7 +36,7 @@ private ArtifactUtils() { } /** - * Returns the conventional filename for the given artifact. + * Gets the conventional filename for the given artifact. * * @param artifact A Maven artifact. * @return A filename. @@ -46,7 +46,7 @@ public static String getFileName(Artifact artifact) { } /** - * Returns the filename for the given artifact with a changed extension. + * Gets the filename for the given artifact with a changed extension. * * @param artifact A Maven artifact. * @param extension The file name extension. @@ -63,7 +63,7 @@ public static String getFileName(Artifact artifact, String extension) { } /** - * Returns the Package URL corresponding to this artifact. + * Gets the Package URL corresponding to this artifact. * * @param artifact A maven artifact. * @return A PURL for the given artifact. @@ -81,7 +81,7 @@ public static String getPackageUrl(Artifact artifact) { } /** - * Returns a map of checksum algorithm names to hex-encoded digest values for the given artifact file. + * Gets a map of checksum algorithm names to hex-encoded digest values for the given artifact file. * * @param artifact A Maven artifact. * @return A map of checksum algorithm names to hex-encoded digest values. diff --git a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java index c4d4aecec..dc539bbd2 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -67,7 +67,7 @@ public static String scmToDownloadUri(String scmUri, Path repositoryPath) throws } /** - * Returns the current branch name for the given repository path. + * Gets the current branch name for the given repository path. * *

Returns the commit SHA if the repository is in a detached HEAD state. * diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index a2f586995..d9e0ee65b 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -225,7 +225,7 @@ void setOutputDirectory(final File outputDirectory) { } /** - * Returns the SCM directory. + * Gets the SCM directory. * * @return The SCM directory. */ @@ -270,7 +270,7 @@ void setSignAttestation(final boolean signAttestation) { } /** - * Overrides the GPG signer used for signing. Intended for testing. + * Sets the GPG signer used for signing. Intended for testing. * * @param signer the signer to use */ @@ -279,7 +279,7 @@ void setSigner(final AbstractGpgSigner signer) { } /** - * Returns the GPG signer, creating and preparing it from plugin parameters if not already set. + * Gets the GPG signer, creating and preparing it from plugin parameters if not already set. * * @return the prepared signer * @throws MojoFailureException if signer preparation fails @@ -421,7 +421,7 @@ private List getSubjects() throws MojoExecutionException { } /** - * Returns resource descriptors for the JVM, Maven installation, SCM source, and project dependencies. + * Gets resource descriptors for the JVM, Maven installation, SCM source, and project dependencies. * * @return A list of resolved build dependencies. * @throws MojoExecutionException If any dependency cannot be resolved or hashed. @@ -441,7 +441,7 @@ private List getBuildDependencies() throws MojoExecutionExce } /** - * Returns resource descriptors for all resolved project dependencies. + * Gets resource descriptors for all resolved project dependencies. * * @return A list of resource descriptors for the project's resolved artifacts. * @throws MojoExecutionException If a dependency artifact cannot be described. @@ -455,7 +455,7 @@ private List getProjectDependencies() throws MojoExecutionEx } /** - * Returns a resource descriptor for the current SCM source, including the URI and Git commit digest. + * Gets a resource descriptor for the current SCM source, including the URI and Git commit digest. * * @return A resource descriptor for the SCM source. * @throws IOException If the current branch cannot be determined. @@ -473,7 +473,7 @@ private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionE } /** - * Creates and returns an SCM repository from the configured connection URL. + * Gets an SCM repository from the configured connection URL. * * @return The SCM repository. * @throws MojoExecutionException If the SCM repository cannot be created. @@ -487,7 +487,7 @@ private ScmRepository getScmRepository() throws MojoExecutionException { } /** - * Returns the current SCM revision (commit hash) for the configured SCM directory. + * Gets the current SCM revision (commit hash) for the configured SCM directory. * * @return The current SCM revision string. * @throws MojoExecutionException If the revision cannot be retrieved from SCM. @@ -531,7 +531,7 @@ private String getScmRevision(final InfoScmResult result) throws MojoExecutionEx } /** - * Returns build metadata derived from the current Maven session, including start and finish timestamps. + * Gets build metadata derived from the current Maven session, including start and finish timestamps. * * @return The build metadata. */ From c8855e36dedc68ec2000d9aeebd99be06cc58297 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 13:04:48 +0200 Subject: [PATCH 21/51] fix: fix `getFileName` Javadoc --- .../apache/commons/release/plugin/internal/ArtifactUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java index b831edfa9..54575f53e 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -36,7 +36,7 @@ private ArtifactUtils() { } /** - * Gets the conventional filename for the given artifact. + * Gets the filename of an artifact in the default Maven repository layout. * * @param artifact A Maven artifact. * @return A filename. @@ -46,7 +46,7 @@ public static String getFileName(Artifact artifact) { } /** - * Gets the filename for the given artifact with a changed extension. + * Gets the filename of an artifact in the default Maven repository layout, using the specified extension. * * @param artifact A Maven artifact. * @param extension The file name extension. From 28f0b57811318f6c05ff15cfc45eee4bc211ffaa Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 13:51:01 +0200 Subject: [PATCH 22/51] fix: add support for multiple checksum algorithms --- pom.xml | 5 ++ .../plugin/internal/ArtifactUtils.java | 52 ++++++++++++++++--- .../plugin/mojos/BuildAttestationMojo.java | 21 ++++++-- .../mojos/BuildAttestationMojoTest.java | 22 ++++---- .../attestations/commons-text-1.4.intoto.json | 50 ++++++++++++++---- 5 files changed, 121 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index a740bb530..a5001cd27 100644 --- a/pom.xml +++ b/pom.xml @@ -199,6 +199,11 @@ commons-compress 1.28.0 + + org.apache.commons + commons-lang3 + 3.20.0 + org.apache.maven.plugins maven-gpg-plugin diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java index 54575f53e..729df6fb3 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -17,10 +17,12 @@ package org.apache.commons.release.plugin.internal; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; import org.apache.maven.artifact.Artifact; import org.apache.maven.plugin.MojoExecutionException; @@ -30,6 +32,35 @@ */ public final class ArtifactUtils { + /** + * Maps standard JDK {@link java.security.MessageDigest} algorithm names to the in-toto digest names used in SLSA {@link ResourceDescriptor} digest sets. + * + *

JDK algorithms that have no in-toto equivalent (such as {@code MD2}) are omitted.

+ * + * @see + * JDK standard {@code MessageDigest} algorithm names + * @see + * in-toto digest set specification + */ + private static final Map IN_TOTO_DIGEST_NAMES; + + static { + final Map m = new HashMap<>(); + m.put("MD5", "md5"); + m.put("SHA-1", "sha1"); + m.put("SHA-224", "sha224"); + m.put("SHA-256", "sha256"); + m.put("SHA-384", "sha384"); + m.put("SHA-512", "sha512"); + m.put("SHA-512/224", "sha512_224"); + m.put("SHA-512/256", "sha512_256"); + m.put("SHA3-224", "sha3_224"); + m.put("SHA3-256", "sha3_256"); + m.put("SHA3-384", "sha3_384"); + m.put("SHA3-512", "sha3_512"); + IN_TOTO_DIGEST_NAMES = Collections.unmodifiableMap(m); + } + /** No instances. */ private ArtifactUtils() { // prevent instantiation @@ -84,14 +115,22 @@ public static String getPackageUrl(Artifact artifact) { * Gets a map of checksum algorithm names to hex-encoded digest values for the given artifact file. * * @param artifact A Maven artifact. + * @param algorithms JSSE names of algorithms to use * @return A map of checksum algorithm names to hex-encoded digest values. * @throws IOException If an I/O error occurs reading the artifact file. + * @throws IllegalArgumentException If any of the algorithms is not supported. */ - private static Map getChecksums(Artifact artifact) throws IOException { + private static Map getChecksums(Artifact artifact, String... algorithms) throws IOException { Map checksums = new HashMap<>(); - DigestUtils digest = new DigestUtils(DigestUtils.getSha256Digest()); - String sha256sum = digest.digestAsHex(artifact.getFile()); - checksums.put("sha256", sha256sum); + for (String algorithm : algorithms) { + String key = IN_TOTO_DIGEST_NAMES.get(algorithm); + if (key == null) { + throw new IllegalArgumentException("Invalid algorithm name for in-toto attestation: " + algorithm); + } + DigestUtils digest = new DigestUtils(DigestUtils.getDigest(algorithm)); + String checksum = digest.digestAsHex(artifact.getFile()); + checksums.put(key, checksum); + } return checksums; } @@ -99,16 +138,17 @@ private static Map getChecksums(Artifact artifact) throws IOExce * Converts a Maven artifact to a SLSA {@link ResourceDescriptor}. * * @param artifact A Maven artifact. + * @param algorithms A comma-separated list of checksum algorithms to use. * @return A SLSA resource descriptor. * @throws MojoExecutionException If an I/O error occurs retrieving the artifact. */ - public static ResourceDescriptor toResourceDescriptor(Artifact artifact) throws MojoExecutionException { + public static ResourceDescriptor toResourceDescriptor(Artifact artifact, String algorithms) throws MojoExecutionException { ResourceDescriptor descriptor = new ResourceDescriptor(); descriptor.setName(getFileName(artifact)); descriptor.setUri(getPackageUrl(artifact)); if (artifact.getFile() != null) { try { - descriptor.setDigest(getChecksums(artifact)); + descriptor.setDigest(getChecksums(artifact, StringUtils.split(algorithms, ","))); } catch (IOException e) { throw new MojoExecutionException("Unable to compute hash for artifact file: " + artifact.getFile(), e); } diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index d9e0ee65b..9ba424459 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -128,6 +128,12 @@ public class BuildAttestationMojo extends AbstractMojo { @Parameter(property = "commons.release.signAttestation", defaultValue = "true") private boolean signAttestation; + /** + * Checksum algorithms used in the generated attestation. + */ + @Parameter(property = "commons.release.checksums.algorithms", defaultValue = "SHA-512,SHA-256,SHA-1,MD5") + private String algorithmNames; + /** * Path to the GPG executable; if not set, {@code gpg} is resolved from {@code PATH}. */ @@ -278,6 +284,15 @@ void setSigner(final AbstractGpgSigner signer) { this.signer = signer; } + /** + * Sets the list of checksum algorithms to use. + * + * @param algorithmNames A comma-separated list of {@link java.security.MessageDigest} algorithm names to use. + */ + void setAlgorithmNames(String algorithmNames) { + this.algorithmNames = algorithmNames; + } + /** * Gets the GPG signer, creating and preparing it from plugin parameters if not already set. * @@ -413,9 +428,9 @@ private void writeAndAttach(final Object value, final Path artifactPath) throws */ private List getSubjects() throws MojoExecutionException { List subjects = new ArrayList<>(); - subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact())); + subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact(), algorithmNames)); for (Artifact artifact : project.getAttachedArtifacts()) { - subjects.add(ArtifactUtils.toResourceDescriptor(artifact)); + subjects.add(ArtifactUtils.toResourceDescriptor(artifact, algorithmNames)); } return subjects; } @@ -449,7 +464,7 @@ private List getBuildDependencies() throws MojoExecutionExce private List getProjectDependencies() throws MojoExecutionException { List dependencies = new ArrayList<>(); for (Artifact artifact : project.getArtifacts()) { - dependencies.add(ArtifactUtils.toResourceDescriptor(artifact)); + dependencies.add(ArtifactUtils.toResourceDescriptor(artifact, algorithmNames)); } return dependencies; } diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 51565d368..c9aa84ef5 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -112,6 +112,16 @@ private static BuildAttestationMojo createBuildAttestationMojo(MavenProject proj createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); } + private static void configureBuildAttestationMojo(BuildAttestationMojo mojo, boolean signAttestation) { + mojo.setOutputDirectory(new File("target/attestations")); + mojo.setScmDirectory(new File(".")); + mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); + mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); + mojo.setAlgorithmNames("SHA-512,SHA-256,SHA-1,MD5"); + mojo.setSignAttestation(signAttestation); + mojo.setSigner(createMockSigner()); + } + private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) throws Exception { File pomFile = new File(ARTIFACTS_DIR + "commons-text-1.4.pom"); Model model; @@ -209,10 +219,7 @@ void attestationTest() throws Exception { MavenProject project = createMavenProject(projectHelper, repoSystem); BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); - mojo.setOutputDirectory(new File("target/attestations")); - mojo.setScmDirectory(new File(".")); - mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); - mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); + configureBuildAttestationMojo(mojo, false); mojo.execute(); JsonNode statement = OBJECT_MAPPER.readTree(getAttestation(project).getFile()); @@ -226,12 +233,7 @@ void signingTest() throws Exception { MavenProject project = createMavenProject(projectHelper, repoSystem); BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); - mojo.setOutputDirectory(new File("target/attestations")); - mojo.setScmDirectory(new File(".")); - mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); - mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); - mojo.setSignAttestation(true); - mojo.setSigner(createMockSigner()); + configureBuildAttestationMojo(mojo, true); mojo.execute(); String envelopeJson = new String(Files.readAllBytes(getAttestation(project).getFile().toPath()), StandardCharsets.UTF_8); diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json index 5c33198d0..37007233b 100644 --- a/src/test/resources/attestations/commons-text-1.4.intoto.json +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -4,70 +4,100 @@ "name": "commons-text-1.4.jar", "uri": "pkg:maven/commons-text/commons-text@1.4?type=jar", "digest": { - "sha256": "ad2d2eacf15ab740c115294afc1192603d8342004a6d7d0ad35446f7dda8a134" + "md5": "9cbe22bb0ce86c70779213dfb7f3eb5a", + "sha1": "c81f089b3542485d4d09b02aae822906e5d2f209", + "sha256": "ad2d2eacf15ab740c115294afc1192603d8342004a6d7d0ad35446f7dda8a134", + "sha512": "126302c5f6865733774eb41fecc10ba8d0bb5ba11d14b9562047429abeb13bf8cdcdbfdf5e7d7708e2a40f67f4265cbbce609164f57abcd676067a840aa48e6a" } }, { "name": "commons-text-1.4.pom", "uri": "pkg:maven/commons-text/commons-text@1.4?type=pom", "digest": { - "sha256": "4d6277b1e0720bb054c640620679a9da120f753029342150e714095f48934d76" + "md5": "00045f652e3dc8970442ce819806db34", + "sha1": "26fa30e496321e74c77ad66781ba53448e6e3a68", + "sha256": "4d6277b1e0720bb054c640620679a9da120f753029342150e714095f48934d76", + "sha512": "db8934f3062ea9c965ea27cfe4517a25513fb7cebe35ed02bedc1d8287b01c7ba64c93a8a261325fe12ab6957cbd80dbc8c06ec34c9a23c5a5c89ef6bace88fe" } }, { "name": "commons-text-1.4-sources.jar", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=sources&type=jar", "digest": { - "sha256": "58a95591fe7fc94db94a0a9e64b4a5bcc1c49edf17f2b24d7c0747357d855761" + "md5": "21af10902cea10cf54bc9acf956863d4", + "sha1": "cadbe9d3980a21e6eaec3aad629bbcdb7714aa3f", + "sha256": "58a95591fe7fc94db94a0a9e64b4a5bcc1c49edf17f2b24d7c0747357d855761", + "sha512": "c91ed209fa97c5e69e21d3a29d1f2ea90f2f77451762b3c387a8cb94dea167b4d3f04ea1a8635232476d804f40935698b4f8884fd43520bcc79b3a0f9a757716" } }, { "name": "commons-text-1.4-javadoc.jar", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=javadoc&type=jar", "digest": { - "sha256": "42f5b341d0fbeaa30b06aed90612840bc513fb39792c3d39446510670216e8b1" + "md5": "2ad93e1d99c0b80cbf6872d1762d7297", + "sha1": "9f06cdf753e1bb512e7640ec5fcce83f5a19ba2c", + "sha256": "42f5b341d0fbeaa30b06aed90612840bc513fb39792c3d39446510670216e8b1", + "sha512": "052ab4e9facfc64f265a567607d40d37620b616908ce71405cae9cd30ad6ac9f2558663029933f90df67c1e748ac81451a32e28975dc773c2c7476c489146c30" } }, { "name": "commons-text-1.4-tests.jar", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=tests&type=jar", "digest": { - "sha256": "e4e365d08d601a4bda44be2a31f748b96762504d301742d4a0f7f5953d4c793a" + "md5": "9820547734aff2c17c4a696bbd7d8d5e", + "sha1": "dbfb945a12375e9fe06558840b43f35374c391ff", + "sha256": "e4e365d08d601a4bda44be2a31f748b96762504d301742d4a0f7f5953d4c793a", + "sha512": "a8e373cf10a9dc2d3c1cfdd43b23910c9ca9d83dea4e566772dd1ebe5e28419777e0d83b002e8cf950f7bde3218b714e0cc3718464ee34ecda77f603e260ab20" } }, { "name": "commons-text-1.4-test-sources.jar", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=test-sources&type=jar", "digest": { - "sha256": "9200a2a41b35f2d6d30c1c698308591cf577547ec39514657dff0e2f7dff18ca" + "md5": "340231a697e2862c8d820de419a91d3e", + "sha1": "5e107fc877c67992948620a8a2d9bb94cab21aa0", + "sha256": "9200a2a41b35f2d6d30c1c698308591cf577547ec39514657dff0e2f7dff18ca", + "sha512": "02b405eb9aff57959a3e066030745be47b76c71af71f86e06fd5004602a399cdf86b1149554e92e9922d9da2fd2239dcf6e8c783462167320838dd7662405b30" } }, { "name": "commons-text-1.4-bin.tar.gz", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=bin&type=tar.gz", "digest": { - "sha256": "8b9393f7ddc2efb69d8c2b6f4d85d8711dddfe77009799cf21619fc9b8411897" + "md5": "9fe25162590be6fa684a9d9cdc0b505d", + "sha1": "6f46fd82d5ccf9644a37bcec6f2158a52ebdbab8", + "sha256": "8b9393f7ddc2efb69d8c2b6f4d85d8711dddfe77009799cf21619fc9b8411897", + "sha512": "c76f4e5814c0533030fecf0ef7639ce68df54b76ebc1320d9c4e3b8dff0a90c95ce0a425b8df6a0d742f6e9fca8dce6f08632d576c6a99357fdd54b9435fed6c" } }, { "name": "commons-text-1.4-bin.zip", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=bin&type=zip", "digest": { - "sha256": "ad3732dcb38e510b1dbb1544115d0eb797fab61afe0008fdb187cd4ef1706cd7" + "md5": "e29e5290b7420ba1908c418fc5bc7f8a", + "sha1": "447a051d6c8292c7e4d7641ca6586b335ef13bd6", + "sha256": "ad3732dcb38e510b1dbb1544115d0eb797fab61afe0008fdb187cd4ef1706cd7", + "sha512": "040634b27146e2008bf953f1bb63f8ccbb63cdaaaf24beb17acc6782d31452214e755539c2df737e46e7ede5d4e376cb3de5bbf72ceba5409b43f25612c2bf40" } }, { "name": "commons-text-1.4-src.tar.gz", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=src&type=tar.gz", "digest": { - "sha256": "1cb8536c375c3cff66757fd40c2bf878998254ba0a247866a6536bd48ba2e88a" + "md5": "be130c23d3b6630824e2a08530bd4581", + "sha1": "0dcee421c4e03d6bc098a61a5cdcc90656856611", + "sha256": "1cb8536c375c3cff66757fd40c2bf878998254ba0a247866a6536bd48ba2e88a", + "sha512": "8279eb7f45009f11658c256b07cfb06c5a45ef89949e78a67e5d64520d957707f90a9614c95e8aac350a07a54f9160f0802dbd1efc0769a5b1391e52ee4cd51b" } }, { "name": "commons-text-1.4-src.zip", "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=src&type=zip", "digest": { - "sha256": "e4a6c992153faae4f7faff689b899073000364e376736b9746a5d0acb9d8b980" + "md5": "fd65603e930f2b0805c809aa2deb1498", + "sha1": "ca1cc6fbb4e46b44f8bb09b70c9e3a2ae3c5fce8", + "sha256": "e4a6c992153faae4f7faff689b899073000364e376736b9746a5d0acb9d8b980", + "sha512": "79ca61ff7b287407428bbb6ae13c6d372dcd0665114c55cd5bc57978a6fa760305e32feabef62cfeb0c4181220a59406239f6cccaa9a25c68773eef0250cb3a9" } } ], From 86a440155f163f6d0460a5bf59a6707468595d9b Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 14:06:51 +0200 Subject: [PATCH 23/51] fix: improve `BuildDefinitions.commandLine` --- .../plugin/internal/BuildDefinitions.java | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index dc51d0730..b6b175a71 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -20,6 +20,7 @@ import java.io.InputStream; import java.lang.management.ManagementFactory; import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -136,35 +137,12 @@ public static Map externalParameters(final MavenSession session) * @return a string representation of the Maven command line */ static String commandLine(final MavenExecutionRequest request) { - StringBuilder sb = new StringBuilder(); - for (String goal : request.getGoals()) { - sb.append(goal).append(" "); - } - List activeProfiles = request.getActiveProfiles(); - if (activeProfiles != null && !activeProfiles.isEmpty()) { - sb.append("-P"); - for (String profile : activeProfiles) { - sb.append(profile).append(","); - } - removeLast(sb); - sb.append(" "); - } - Properties userProperties = request.getUserProperties(); - for (String propertyName : userProperties.stringPropertyNames()) { - sb.append("-D").append(propertyName).append("=").append(userProperties.get(propertyName)).append(" "); - } - removeLast(sb); - return sb.toString(); - } - - /** - * Removes last character from a {@link StringBuilder}. - * - * @param sb The {@link StringBuilder} to use - */ - private static void removeLast(final StringBuilder sb) { - if (sb.length() > 0) { - sb.setLength(sb.length() - 1); + List args = new ArrayList<>(request.getGoals()); + String profiles = String.join(",", request.getActiveProfiles()); + if (!profiles.isEmpty()) { + args.add("-P" + profiles); } + request.getUserProperties().forEach((key, value) -> args.add("-D" + key + "=" + value)); + return String.join(" ", args); } } From c3cff4d616c2dab5640954530b9ea8f77fcba941 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 14:14:50 +0200 Subject: [PATCH 24/51] fix: add final everywhere --- .../plugin/internal/ArtifactUtils.java | 30 +++++----- .../plugin/internal/BuildDefinitions.java | 36 +++++------ .../release/plugin/internal/GitUtils.java | 20 +++---- .../plugin/mojos/BuildAttestationMojo.java | 60 +++++++++---------- .../plugin/internal/BuildDefinitionsTest.java | 4 +- .../release/plugin/internal/MojoUtils.java | 10 ++-- .../mojos/BuildAttestationMojoTest.java | 57 +++++++++--------- 7 files changed, 109 insertions(+), 108 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java index 729df6fb3..e2e2aa75e 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -72,7 +72,7 @@ private ArtifactUtils() { * @param artifact A Maven artifact. * @return A filename. */ - public static String getFileName(Artifact artifact) { + public static String getFileName(final Artifact artifact) { return getFileName(artifact, artifact.getArtifactHandler().getExtension()); } @@ -83,8 +83,8 @@ public static String getFileName(Artifact artifact) { * @param extension The file name extension. * @return A filename. */ - public static String getFileName(Artifact artifact, String extension) { - StringBuilder fileName = new StringBuilder(); + public static String getFileName(final Artifact artifact, final String extension) { + final StringBuilder fileName = new StringBuilder(); fileName.append(artifact.getArtifactId()).append("-").append(artifact.getVersion()); if (artifact.getClassifier() != null) { fileName.append("-").append(artifact.getClassifier()); @@ -99,11 +99,11 @@ public static String getFileName(Artifact artifact, String extension) { * @param artifact A maven artifact. * @return A PURL for the given artifact. */ - public static String getPackageUrl(Artifact artifact) { - StringBuilder sb = new StringBuilder(); + public static String getPackageUrl(final Artifact artifact) { + final StringBuilder sb = new StringBuilder(); sb.append("pkg:maven/").append(artifact.getGroupId()).append("/").append(artifact.getArtifactId()).append("@").append(artifact.getVersion()) .append("?"); - String classifier = artifact.getClassifier(); + final String classifier = artifact.getClassifier(); if (classifier != null) { sb.append("classifier=").append(classifier).append("&"); } @@ -120,15 +120,15 @@ public static String getPackageUrl(Artifact artifact) { * @throws IOException If an I/O error occurs reading the artifact file. * @throws IllegalArgumentException If any of the algorithms is not supported. */ - private static Map getChecksums(Artifact artifact, String... algorithms) throws IOException { - Map checksums = new HashMap<>(); - for (String algorithm : algorithms) { - String key = IN_TOTO_DIGEST_NAMES.get(algorithm); + private static Map getChecksums(final Artifact artifact, final String... algorithms) throws IOException { + final Map checksums = new HashMap<>(); + for (final String algorithm : algorithms) { + final String key = IN_TOTO_DIGEST_NAMES.get(algorithm); if (key == null) { throw new IllegalArgumentException("Invalid algorithm name for in-toto attestation: " + algorithm); } - DigestUtils digest = new DigestUtils(DigestUtils.getDigest(algorithm)); - String checksum = digest.digestAsHex(artifact.getFile()); + final DigestUtils digest = new DigestUtils(DigestUtils.getDigest(algorithm)); + final String checksum = digest.digestAsHex(artifact.getFile()); checksums.put(key, checksum); } return checksums; @@ -142,14 +142,14 @@ private static Map getChecksums(Artifact artifact, String... alg * @return A SLSA resource descriptor. * @throws MojoExecutionException If an I/O error occurs retrieving the artifact. */ - public static ResourceDescriptor toResourceDescriptor(Artifact artifact, String algorithms) throws MojoExecutionException { - ResourceDescriptor descriptor = new ResourceDescriptor(); + public static ResourceDescriptor toResourceDescriptor(final Artifact artifact, final String algorithms) throws MojoExecutionException { + final ResourceDescriptor descriptor = new ResourceDescriptor(); descriptor.setName(getFileName(artifact)); descriptor.setUri(getPackageUrl(artifact)); if (artifact.getFile() != null) { try { descriptor.setDigest(getChecksums(artifact, StringUtils.split(algorithms, ","))); - } catch (IOException e) { + } catch (final IOException e) { throw new MojoExecutionException("Unable to compute hash for artifact file: " + artifact.getFile(), e); } } diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index b6b175a71..67b8ea813 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -48,13 +48,13 @@ private BuildDefinitions() { * @return a descriptor with digest and annotations populated from system properties * @throws IOException if hashing the JDK directory fails */ - public static ResourceDescriptor jvm(Path javaHome) throws IOException { - ResourceDescriptor descriptor = new ResourceDescriptor(); + public static ResourceDescriptor jvm(final Path javaHome) throws IOException { + final ResourceDescriptor descriptor = new ResourceDescriptor(); descriptor.setName("JDK"); - Map digest = new HashMap<>(); + final Map digest = new HashMap<>(); digest.put("gitTree", GitUtils.gitTree(javaHome)); descriptor.setDigest(digest); - String[] propertyNames = { + final String[] propertyNames = { "java.version", "java.version.date", "java.vendor", "java.vendor.url", "java.vendor.version", "java.home", @@ -63,8 +63,8 @@ public static ResourceDescriptor jvm(Path javaHome) throws IOException { "java.specification.version", "java.specification.maintenance.version", "java.specification.vendor", "java.specification.name", }; - Map annotations = new HashMap<>(); - for (String prop : propertyNames) { + final Map annotations = new HashMap<>(); + for (final String prop : propertyNames) { annotations.put(prop.substring("java.".length()), System.getProperty(prop)); } descriptor.setAnnotations(annotations); @@ -84,21 +84,21 @@ public static ResourceDescriptor jvm(Path javaHome) throws IOException { * @return a descriptor for the Maven installation * @throws IOException if hashing the Maven home directory fails */ - public static ResourceDescriptor maven(String version, Path mavenHome, ClassLoader coreClassLoader) throws IOException { - ResourceDescriptor descriptor = new ResourceDescriptor(); + public static ResourceDescriptor maven(final String version, final Path mavenHome, final ClassLoader coreClassLoader) throws IOException { + final ResourceDescriptor descriptor = new ResourceDescriptor(); descriptor.setName("Maven"); descriptor.setUri("pkg:maven/org.apache.maven/apache-maven@" + version); - Map digest = new HashMap<>(); + final Map digest = new HashMap<>(); digest.put("gitTree", GitUtils.gitTree(mavenHome)); descriptor.setDigest(digest); - Properties buildProps = new Properties(); + final Properties buildProps = new Properties(); try (InputStream in = coreClassLoader.getResourceAsStream("org/apache/maven/messages/build.properties")) { if (in != null) { buildProps.load(in); } } if (!buildProps.isEmpty()) { - Map annotations = new HashMap<>(); + final Map annotations = new HashMap<>(); buildProps.forEach((key, value) -> annotations.put((String) key, value)); descriptor.setAnnotations(annotations); } @@ -112,17 +112,17 @@ public static ResourceDescriptor maven(String version, Path mavenHome, ClassLoad * @return a map of parameter names to values */ public static Map externalParameters(final MavenSession session) { - Map params = new HashMap<>(); + final Map params = new HashMap<>(); params.put("jvm.args", ManagementFactory.getRuntimeMXBean().getInputArguments()); - MavenExecutionRequest request = session.getRequest(); + final MavenExecutionRequest request = session.getRequest(); params.put("maven.goals", request.getGoals()); params.put("maven.profiles", request.getActiveProfiles()); params.put("maven.user.properties", request.getUserProperties()); params.put("maven.cmdline", commandLine(request)); - Map env = new HashMap<>(); + final Map env = new HashMap<>(); params.put("env", env); - for (Map.Entry entry : System.getenv().entrySet()) { - String key = entry.getKey(); + for (final Map.Entry entry : System.getenv().entrySet()) { + final String key = entry.getKey(); if ("TZ".equals(key) || "LANG".equals(key) || key.startsWith("LC_")) { env.put(key, entry.getValue()); } @@ -137,8 +137,8 @@ public static Map externalParameters(final MavenSession session) * @return a string representation of the Maven command line */ static String commandLine(final MavenExecutionRequest request) { - List args = new ArrayList<>(request.getGoals()); - String profiles = String.join(",", request.getActiveProfiles()); + final List args = new ArrayList<>(request.getGoals()); + final String profiles = String.join(",", request.getActiveProfiles()); if (!profiles.isEmpty()) { args.add("-P" + profiles); } diff --git a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java index dc539bbd2..acbd7bed1 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -42,11 +42,11 @@ public final class GitUtils { * @return A hex-encoded SHA-1 tree hash. * @throws IOException If the path is not a directory or an I/O error occurs. */ - public static String gitTree(Path path) throws IOException { + public static String gitTree(final Path path) throws IOException { if (!Files.isDirectory(path)) { throw new IOException("Path is not a directory: " + path); } - MessageDigest digest = DigestUtils.getSha1Digest(); + final MessageDigest digest = DigestUtils.getSha1Digest(); return Hex.encodeHexString(GitIdentifiers.treeId(digest, path)); } @@ -58,11 +58,11 @@ public static String gitTree(Path path) throws IOException { * @return A download URI of the form {@code git+@}. * @throws IOException If the current branch cannot be determined. */ - public static String scmToDownloadUri(String scmUri, Path repositoryPath) throws IOException { + public static String scmToDownloadUri(final String scmUri, final Path repositoryPath) throws IOException { if (!scmUri.startsWith(SCM_GIT_PREFIX)) { throw new IllegalArgumentException("Invalid scmUri: " + scmUri); } - String currentBranch = getCurrentBranch(repositoryPath); + final String currentBranch = getCurrentBranch(repositoryPath); return "git+" + scmUri.substring(SCM_GIT_PREFIX.length()) + "@" + currentBranch; } @@ -75,9 +75,9 @@ public static String scmToDownloadUri(String scmUri, Path repositoryPath) throws * @return The current branch name, or the commit SHA for a detached HEAD. * @throws IOException If the {@code .git} directory cannot be found or read. */ - public static String getCurrentBranch(Path repositoryPath) throws IOException { - Path gitDir = findGitDir(repositoryPath); - String head = new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim(); + public static String getCurrentBranch(final Path repositoryPath) throws IOException { + final Path gitDir = findGitDir(repositoryPath); + final String head = new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim(); if (head.startsWith("ref: refs/heads/")) { return head.substring("ref: refs/heads/".length()); } @@ -92,16 +92,16 @@ public static String getCurrentBranch(Path repositoryPath) throws IOException { * @return The path to the {@code .git} directory (or file for worktrees). * @throws IOException If no {@code .git} directory is found. */ - private static Path findGitDir(Path path) throws IOException { + private static Path findGitDir(final Path path) throws IOException { Path current = path.toAbsolutePath(); while (current != null) { - Path candidate = current.resolve(".git"); + final Path candidate = current.resolve(".git"); if (Files.isDirectory(candidate)) { return candidate; } if (Files.isRegularFile(candidate)) { // git worktree: .git is a file containing "gitdir: /path/to/real/.git" - String content = new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim(); + final String content = new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim(); if (content.startsWith("gitdir: ")) { return Paths.get(content.substring("gitdir: ".length())); } diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 9ba424459..84ec69cfd 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -212,8 +212,8 @@ public class BuildAttestationMojo extends AbstractMojo { * @param mavenProjectHelper A helper to attach artifacts to the project. */ @Inject - public BuildAttestationMojo(MavenProject project, ScmManager scmManager, RuntimeInformation runtimeInformation, MavenSession session, - MavenProjectHelper mavenProjectHelper) { + public BuildAttestationMojo(final MavenProject project, final ScmManager scmManager, final RuntimeInformation runtimeInformation, + final MavenSession session, final MavenProjectHelper mavenProjectHelper) { this.project = project; this.scmManager = scmManager; this.runtimeInformation = runtimeInformation; @@ -244,7 +244,7 @@ public File getScmDirectory() { * * @param scmDirectory The SCM directory. */ - public void setScmDirectory(File scmDirectory) { + public void setScmDirectory(final File scmDirectory) { this.scmDirectory = scmDirectory; } @@ -289,7 +289,7 @@ void setSigner(final AbstractGpgSigner signer) { * * @param algorithmNames A comma-separated list of {@link java.security.MessageDigest} algorithm names to use. */ - void setAlgorithmNames(String algorithmNames) { + void setAlgorithmNames(final String algorithmNames) { this.algorithmNames = algorithmNames; } @@ -309,21 +309,21 @@ private AbstractGpgSigner getSigner() throws MojoFailureException { @Override public void execute() throws MojoFailureException, MojoExecutionException { // Build definition - BuildDefinition buildDefinition = new BuildDefinition(); + final BuildDefinition buildDefinition = new BuildDefinition(); buildDefinition.setExternalParameters(BuildDefinitions.externalParameters(session)); buildDefinition.setResolvedDependencies(getBuildDependencies()); // Builder - Builder builder = new Builder(); + final Builder builder = new Builder(); // RunDetails - RunDetails runDetails = new RunDetails(); + final RunDetails runDetails = new RunDetails(); runDetails.setBuilder(builder); runDetails.setMetadata(getBuildMetadata()); // Provenance - Provenance provenance = new Provenance(); + final Provenance provenance = new Provenance(); provenance.setBuildDefinition(buildDefinition); provenance.setRunDetails(runDetails); // Statement - Statement statement = new Statement(); + final Statement statement = new Statement(); statement.setSubject(getSubjects()); statement.setPredicate(provenance); @@ -348,7 +348,7 @@ private Path ensureOutputDirectory() throws MojoExecutionException { if (!Files.exists(outputPath)) { Files.createDirectories(outputPath); } - } catch (IOException e) { + } catch (final IOException e) { throw new MojoExecutionException("Could not create output directory.", e); } return outputPath; @@ -380,7 +380,7 @@ private void signAndWriteStatement(final Statement statement, final Path outputP final byte[] statementBytes; try { statementBytes = OBJECT_MAPPER.writeValueAsBytes(statement); - } catch (JsonProcessingException e) { + } catch (final JsonProcessingException e) { throw new MojoExecutionException("Failed to serialize attestation statement", e); } final AbstractGpgSigner signer = getSigner(); @@ -410,7 +410,7 @@ private void writeAndAttach(final Object value, final Path artifactPath) throws try (OutputStream os = Files.newOutputStream(artifactPath)) { OBJECT_MAPPER.writeValue(os, value); os.write('\n'); - } catch (IOException e) { + } catch (final IOException e) { throw new MojoExecutionException("Could not write attestation to: " + artifactPath, e); } if (!skipAttach) { @@ -427,9 +427,9 @@ private void writeAndAttach(final Object value, final Path artifactPath) throws * @throws MojoExecutionException If artifact hashing fails. */ private List getSubjects() throws MojoExecutionException { - List subjects = new ArrayList<>(); + final List subjects = new ArrayList<>(); subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact(), algorithmNames)); - for (Artifact artifact : project.getAttachedArtifacts()) { + for (final Artifact artifact : project.getAttachedArtifacts()) { subjects.add(ArtifactUtils.toResourceDescriptor(artifact, algorithmNames)); } return subjects; @@ -442,13 +442,13 @@ private List getSubjects() throws MojoExecutionException { * @throws MojoExecutionException If any dependency cannot be resolved or hashed. */ private List getBuildDependencies() throws MojoExecutionException { - List dependencies = new ArrayList<>(); + final List dependencies = new ArrayList<>(); try { dependencies.add(BuildDefinitions.jvm(Paths.get(System.getProperty("java.home")))); dependencies.add(BuildDefinitions.maven(runtimeInformation.getMavenVersion(), mavenHome.toPath(), runtimeInformation.getClass().getClassLoader())); dependencies.add(getScmDescriptor()); - } catch (IOException e) { + } catch (final IOException e) { throw new MojoExecutionException(e); } dependencies.addAll(getProjectDependencies()); @@ -462,8 +462,8 @@ private List getBuildDependencies() throws MojoExecutionExce * @throws MojoExecutionException If a dependency artifact cannot be described. */ private List getProjectDependencies() throws MojoExecutionException { - List dependencies = new ArrayList<>(); - for (Artifact artifact : project.getArtifacts()) { + final List dependencies = new ArrayList<>(); + for (final Artifact artifact : project.getArtifacts()) { dependencies.add(ArtifactUtils.toResourceDescriptor(artifact, algorithmNames)); } return dependencies; @@ -477,11 +477,11 @@ private List getProjectDependencies() throws MojoExecutionEx * @throws MojoExecutionException If the SCM revision cannot be retrieved. */ private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionException { - ResourceDescriptor scmDescriptor = new ResourceDescriptor(); - String scmUri = GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath()); + final ResourceDescriptor scmDescriptor = new ResourceDescriptor(); + final String scmUri = GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath()); scmDescriptor.setUri(scmUri); // Compute the revision - Map digest = new HashMap<>(); + final Map digest = new HashMap<>(); digest.put("gitCommit", getScmRevision()); scmDescriptor.setDigest(digest); return scmDescriptor; @@ -496,7 +496,7 @@ private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionE private ScmRepository getScmRepository() throws MojoExecutionException { try { return scmManager.makeScmRepository(scmConnectionUrl); - } catch (ScmException e) { + } catch (final ScmException e) { throw new MojoExecutionException("Failed to create SCM repository", e); } } @@ -508,14 +508,14 @@ private ScmRepository getScmRepository() throws MojoExecutionException { * @throws MojoExecutionException If the revision cannot be retrieved from SCM. */ private String getScmRevision() throws MojoExecutionException { - ScmRepository scmRepository = getScmRepository(); - CommandParameters commandParameters = new CommandParameters(); + final ScmRepository scmRepository = getScmRepository(); + final CommandParameters commandParameters = new CommandParameters(); try { - InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), + final InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), new ScmFileSet(scmDirectory), commandParameters); return getScmRevision(result); - } catch (ScmException e) { + } catch (final ScmException e) { throw new MojoExecutionException("Failed to retrieve SCM revision", e); } } @@ -536,9 +536,9 @@ private String getScmRevision(final InfoScmResult result) throws MojoExecutionEx throw new MojoExecutionException("No SCM revision information found for " + scmDirectory); } - InfoItem item = result.getInfoItems().get(0); + final InfoItem item = result.getInfoItems().get(0); - String revision = item.getRevision(); + final String revision = item.getRevision(); if (revision == null) { throw new MojoExecutionException("Empty SCM revision returned for " + scmDirectory); } @@ -551,8 +551,8 @@ private String getScmRevision(final InfoScmResult result) throws MojoExecutionEx * @return The build metadata. */ private BuildMetadata getBuildMetadata() { - OffsetDateTime startedOn = session.getStartTime().toInstant().atOffset(ZoneOffset.UTC); - OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC); + final OffsetDateTime startedOn = session.getStartTime().toInstant().atOffset(ZoneOffset.UTC); + final OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC); return new BuildMetadata(null, startedOn, finishedOn); } } diff --git a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java index 605657ebe..fe58a540e 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java @@ -50,7 +50,7 @@ static Stream commandLineArguments() { @MethodSource("commandLineArguments") void commandLineTest(final String description, final List goals, final List profiles, final Properties userProperties, final String expected) { - MavenExecutionRequest request = new DefaultMavenExecutionRequest(); + final MavenExecutionRequest request = new DefaultMavenExecutionRequest(); request.setGoals(goals); request.setActiveProfiles(profiles); request.setUserProperties(userProperties); @@ -58,7 +58,7 @@ void commandLineTest(final String description, final List goals, final L } private static Properties singletonProperties(final String key, final String value) { - Properties p = new Properties(); + final Properties p = new Properties(); p.setProperty(key, value); return p; } diff --git a/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java index 6a6c3f5bf..3cdef92fc 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java @@ -40,7 +40,7 @@ public final class MojoUtils { private static ContainerConfiguration setupContainerConfiguration() { - ClassWorld classWorld = + final ClassWorld classWorld = new ClassWorld("plexus.core", Thread.currentThread().getContextClassLoader()); return new DefaultContainerConfiguration() .setClassWorld(classWorld) @@ -54,10 +54,10 @@ public static PlexusContainer setupContainer() throws PlexusContainerException { } public static RepositorySystemSession createRepositorySystemSession( - PlexusContainer container, Path localRepositoryPath) throws ComponentLookupException, RepositoryException { - LocalRepositoryManagerFactory factory = container.lookup(LocalRepositoryManagerFactory.class, "simple"); - DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); - LocalRepositoryManager manager = + final PlexusContainer container, final Path localRepositoryPath) throws ComponentLookupException, RepositoryException { + final LocalRepositoryManagerFactory factory = container.lookup(LocalRepositoryManagerFactory.class, "simple"); + final DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + final LocalRepositoryManager manager = factory.newInstance(repoSession, new LocalRepository(localRepositoryPath.toFile())); repoSession.setLocalRepositoryManager(manager); // Default policies diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index c9aa84ef5..309b98fe9 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -90,29 +90,30 @@ static void setup() throws Exception { } private static MavenExecutionRequest createMavenExecutionRequest() { - DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); + final DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); request.setStartTime(Date.from(Instant.parse("2026-04-20T09:28:44Z"))); request.setActiveProfiles(Collections.singletonList("release")); request.setGoals(Collections.singletonList("deploy")); - Properties userProperties = new Properties(); + final Properties userProperties = new Properties(); userProperties.setProperty("gpg.keyname", "3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9"); request.setUserProperties(userProperties); return request; } @SuppressWarnings("deprecation") - private static MavenSession createMavenSession(MavenExecutionRequest request, MavenExecutionResult result) { + private static MavenSession createMavenSession(final MavenExecutionRequest request, final MavenExecutionResult result) { return new MavenSession(container, repoSession, request, result); } - private static BuildAttestationMojo createBuildAttestationMojo(MavenProject project, MavenProjectHelper projectHelper) throws ComponentLookupException { - ScmManager scmManager = container.lookup(ScmManager.class); - RuntimeInformation runtimeInfo = container.lookup(RuntimeInformation.class); + private static BuildAttestationMojo createBuildAttestationMojo(final MavenProject project, final MavenProjectHelper projectHelper) + throws ComponentLookupException { + final ScmManager scmManager = container.lookup(ScmManager.class); + final RuntimeInformation runtimeInfo = container.lookup(RuntimeInformation.class); return new BuildAttestationMojo(project, scmManager, runtimeInfo, createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); } - private static void configureBuildAttestationMojo(BuildAttestationMojo mojo, boolean signAttestation) { + private static void configureBuildAttestationMojo(final BuildAttestationMojo mojo, final boolean signAttestation) { mojo.setOutputDirectory(new File("target/attestations")); mojo.setScmDirectory(new File(".")); mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); @@ -122,16 +123,16 @@ private static void configureBuildAttestationMojo(BuildAttestationMojo mojo, boo mojo.setSigner(createMockSigner()); } - private static MavenProject createMavenProject(MavenProjectHelper projectHelper, MavenRepositorySystem repoSystem) throws Exception { - File pomFile = new File(ARTIFACTS_DIR + "commons-text-1.4.pom"); - Model model; + private static MavenProject createMavenProject(final MavenProjectHelper projectHelper, final MavenRepositorySystem repoSystem) throws Exception { + final File pomFile = new File(ARTIFACTS_DIR + "commons-text-1.4.pom"); + final Model model; try (InputStream in = Files.newInputStream(pomFile.toPath())) { model = new MavenXpp3Reader().read(in); } // Group id is inherited from the missing parent, so we override it model.setGroupId("org.apache.commons"); - MavenProject project = new MavenProject(model); - Artifact artifact = repoSystem.createArtifact(model.getArtifactId(), model.getArtifactId(), model.getVersion(), null, "jar"); + final MavenProject project = new MavenProject(model); + final Artifact artifact = repoSystem.createArtifact(model.getArtifactId(), model.getArtifactId(), model.getVersion(), null, "jar"); artifact.setFile(new File(ARTIFACTS_DIR + "commons-text-1.4.jar")); project.setArtifact(artifact); projectHelper.attachArtifact(project, "pom", null, pomFile); @@ -193,9 +194,9 @@ private static void assertStatementContent(final JsonNode statement) { assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/resolvedDependencies"), statement.at("/predicate/buildDefinition/resolvedDependencies"), JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("[0].annotations")); - Set expectedJdkFields = fieldNames( + final Set expectedJdkFields = fieldNames( expectedStatement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); - Set actualJdkFields = fieldNames( + final Set actualJdkFields = fieldNames( statement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); assertEquals(expectedJdkFields, actualJdkFields); assertJsonEquals(expectedStatement.at("/predicate/runDetails"), @@ -204,8 +205,8 @@ private static void assertStatementContent(final JsonNode statement) { } private static Set fieldNames(final JsonNode node) { - Set names = new TreeSet<>(); - Iterator it = node.fieldNames(); + final Set names = new TreeSet<>(); + final Iterator it = node.fieldNames(); while (it.hasNext()) { names.add(it.next()); } @@ -214,37 +215,37 @@ private static Set fieldNames(final JsonNode node) { @Test void attestationTest() throws Exception { - MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); - MavenRepositorySystem repoSystem = container.lookup(MavenRepositorySystem.class); - MavenProject project = createMavenProject(projectHelper, repoSystem); + final MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); + final MavenRepositorySystem repoSystem = container.lookup(MavenRepositorySystem.class); + final MavenProject project = createMavenProject(projectHelper, repoSystem); - BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); + final BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); configureBuildAttestationMojo(mojo, false); mojo.execute(); - JsonNode statement = OBJECT_MAPPER.readTree(getAttestation(project).getFile()); + final JsonNode statement = OBJECT_MAPPER.readTree(getAttestation(project).getFile()); assertStatementContent(statement); } @Test void signingTest() throws Exception { - MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); - MavenRepositorySystem repoSystem = container.lookup(MavenRepositorySystem.class); - MavenProject project = createMavenProject(projectHelper, repoSystem); + final MavenProjectHelper projectHelper = container.lookup(MavenProjectHelper.class); + final MavenRepositorySystem repoSystem = container.lookup(MavenRepositorySystem.class); + final MavenProject project = createMavenProject(projectHelper, repoSystem); - BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); + final BuildAttestationMojo mojo = createBuildAttestationMojo(project, projectHelper); configureBuildAttestationMojo(mojo, true); mojo.execute(); - String envelopeJson = new String(Files.readAllBytes(getAttestation(project).getFile().toPath()), StandardCharsets.UTF_8); + final String envelopeJson = new String(Files.readAllBytes(getAttestation(project).getFile().toPath()), StandardCharsets.UTF_8); assertJsonPartEquals(DsseEnvelope.PAYLOAD_TYPE, envelopeJson, "payloadType"); assertJsonNodePresent(envelopeJson, "signatures[0]"); assertJsonNodeAbsent(envelopeJson, "signatures[1]"); assertJsonPartEquals("${json-unit.regex}.+", envelopeJson, "signatures[0].sig"); - DsseEnvelope envelope = OBJECT_MAPPER.readValue(envelopeJson.trim(), DsseEnvelope.class); - JsonNode statement = OBJECT_MAPPER.readTree(envelope.getPayload()); + final DsseEnvelope envelope = OBJECT_MAPPER.readValue(envelopeJson.trim(), DsseEnvelope.class); + final JsonNode statement = OBJECT_MAPPER.readTree(envelope.getPayload()); assertStatementContent(statement); } } From db99b3c9904ee9127d8469d7d3c0994606fa8ca0 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 14:18:22 +0200 Subject: [PATCH 25/51] fix: indentation --- .../release/plugin/slsa/v1_2/RunDetails.java | 212 +++++++++--------- 1 file changed, 110 insertions(+), 102 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java index 7b20b5a1d..85d8f6b0b 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java @@ -30,108 +30,116 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class RunDetails { - /** Entity that executed the build. */ - @JsonProperty("builder") - private Builder builder; - - /** Metadata about the build invocation. */ - @JsonProperty("metadata") - private BuildMetadata metadata; - - /** Artifacts produced as a side effect of the build. */ - @JsonProperty("byproducts") - private List byproducts; - - /** Creates a new RunDetails instance. */ - public RunDetails() { - } - - /** - * Creates a new RunDetails with the given builder and metadata. - * - * @param builder entity that executed the build - * @param metadata metadata about the build invocation - */ - public RunDetails(Builder builder, BuildMetadata metadata) { - this.builder = builder; - this.metadata = metadata; - } - - /** - * Gets the builder that executed the invocation. - * - *

Trusted to have correctly performed the operation and populated this provenance.

- * - * @return the builder, or {@code null} if not set - */ - public Builder getBuilder() { - return builder; - } - - /** - * Sets the builder that executed the invocation. - * - * @param builder the builder - */ - public void setBuilder(Builder builder) { - this.builder = builder; - } - - /** - * Gets the metadata about the build invocation, including its identifier and timing. - * - * @return the build metadata, or {@code null} if not set - */ - public BuildMetadata getMetadata() { - return metadata; - } - - /** - * Sets the metadata about the build invocation. - * - * @param metadata the build metadata - */ - public void setMetadata(BuildMetadata metadata) { - this.metadata = metadata; - } - - /** - * Gets artifacts produced as a side effect of the build that are not the primary output. - * - * @return the list of byproduct artifacts, or {@code null} if not set - */ - public List getByproducts() { - return byproducts; - } - - /** - * Sets the artifacts produced as a side effect of the build that are not the primary output. - * - * @param byproducts the list of byproduct artifacts - */ - public void setByproducts(List byproducts) { - this.byproducts = byproducts; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; + /** + * Entity that executed the build. + */ + @JsonProperty("builder") + private Builder builder; + + /** + * Metadata about the build invocation. + */ + @JsonProperty("metadata") + private BuildMetadata metadata; + + /** + * Artifacts produced as a side effect of the build. + */ + @JsonProperty("byproducts") + private List byproducts; + + /** + * Creates a new RunDetails instance. + */ + public RunDetails() { } - if (o == null || getClass() != o.getClass()) { - return false; + + /** + * Creates a new RunDetails with the given builder and metadata. + * + * @param builder entity that executed the build + * @param metadata metadata about the build invocation + */ + public RunDetails(Builder builder, BuildMetadata metadata) { + this.builder = builder; + this.metadata = metadata; + } + + /** + * Gets the builder that executed the invocation. + * + *

Trusted to have correctly performed the operation and populated this provenance.

+ * + * @return the builder, or {@code null} if not set + */ + public Builder getBuilder() { + return builder; + } + + /** + * Sets the builder that executed the invocation. + * + * @param builder the builder + */ + public void setBuilder(Builder builder) { + this.builder = builder; + } + + /** + * Gets the metadata about the build invocation, including its identifier and timing. + * + * @return the build metadata, or {@code null} if not set + */ + public BuildMetadata getMetadata() { + return metadata; + } + + /** + * Sets the metadata about the build invocation. + * + * @param metadata the build metadata + */ + public void setMetadata(BuildMetadata metadata) { + this.metadata = metadata; + } + + /** + * Gets artifacts produced as a side effect of the build that are not the primary output. + * + * @return the list of byproduct artifacts, or {@code null} if not set + */ + public List getByproducts() { + return byproducts; + } + + /** + * Sets the artifacts produced as a side effect of the build that are not the primary output. + * + * @param byproducts the list of byproduct artifacts + */ + public void setByproducts(List byproducts) { + this.byproducts = byproducts; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RunDetails that = (RunDetails) o; + return Objects.equals(builder, that.builder) && Objects.equals(metadata, that.metadata) && Objects.equals(byproducts, that.byproducts); + } + + @Override + public int hashCode() { + return Objects.hash(builder, metadata, byproducts); + } + + @Override + public String toString() { + return "RunDetails{builder=" + builder + ", metadata=" + metadata + ", byproducts=" + byproducts + '}'; } - RunDetails that = (RunDetails) o; - return Objects.equals(builder, that.builder) && Objects.equals(metadata, that.metadata) && Objects.equals(byproducts, that.byproducts); - } - - @Override - public int hashCode() { - return Objects.hash(builder, metadata, byproducts); - } - - @Override - public String toString() { - return "RunDetails{builder=" + builder + ", metadata=" + metadata + ", byproducts=" + byproducts + '}'; - } } From 717bc2cf93585834136cc4b29e3ea52053e8a656 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 14:22:37 +0200 Subject: [PATCH 26/51] fix: sort members --- .../plugin/internal/ArtifactUtils.java | 64 +-- .../plugin/internal/BuildDefinitions.java | 78 +-- .../release/plugin/internal/DsseUtils.java | 104 ++-- .../release/plugin/internal/GitUtils.java | 90 ++-- .../plugin/mojos/BuildAttestationMojo.java | 491 +++++++++--------- .../plugin/slsa/v1_2/BuildDefinition.java | 94 ++-- .../plugin/slsa/v1_2/BuildMetadata.java | 78 ++- .../release/plugin/slsa/v1_2/Builder.java | 72 ++- .../plugin/slsa/v1_2/DsseEnvelope.java | 73 ++- .../release/plugin/slsa/v1_2/Provenance.java | 50 +- .../plugin/slsa/v1_2/ResourceDescriptor.java | 164 +++--- .../release/plugin/slsa/v1_2/RunDetails.java | 72 ++- .../release/plugin/slsa/v1_2/Signature.java | 44 +- .../release/plugin/slsa/v1_2/Statement.java | 79 ++- .../plugin/internal/BuildDefinitionsTest.java | 12 +- .../release/plugin/internal/MojoUtils.java | 28 +- .../mojos/BuildAttestationMojoTest.java | 146 +++--- 17 files changed, 851 insertions(+), 888 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java index e2e2aa75e..17a3e8728 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -61,19 +61,27 @@ public final class ArtifactUtils { IN_TOTO_DIGEST_NAMES = Collections.unmodifiableMap(m); } - /** No instances. */ - private ArtifactUtils() { - // prevent instantiation - } - /** - * Gets the filename of an artifact in the default Maven repository layout. + * Gets a map of checksum algorithm names to hex-encoded digest values for the given artifact file. * * @param artifact A Maven artifact. - * @return A filename. + * @param algorithms JSSE names of algorithms to use + * @return A map of checksum algorithm names to hex-encoded digest values. + * @throws IOException If an I/O error occurs reading the artifact file. + * @throws IllegalArgumentException If any of the algorithms is not supported. */ - public static String getFileName(final Artifact artifact) { - return getFileName(artifact, artifact.getArtifactHandler().getExtension()); + private static Map getChecksums(final Artifact artifact, final String... algorithms) throws IOException { + final Map checksums = new HashMap<>(); + for (final String algorithm : algorithms) { + final String key = IN_TOTO_DIGEST_NAMES.get(algorithm); + if (key == null) { + throw new IllegalArgumentException("Invalid algorithm name for in-toto attestation: " + algorithm); + } + final DigestUtils digest = new DigestUtils(DigestUtils.getDigest(algorithm)); + final String checksum = digest.digestAsHex(artifact.getFile()); + checksums.put(key, checksum); + } + return checksums; } /** @@ -93,6 +101,16 @@ public static String getFileName(final Artifact artifact, final String extension return fileName.toString(); } + /** + * Gets the filename of an artifact in the default Maven repository layout. + * + * @param artifact A Maven artifact. + * @return A filename. + */ + public static String getFileName(final Artifact artifact) { + return getFileName(artifact, artifact.getArtifactHandler().getExtension()); + } + /** * Gets the Package URL corresponding to this artifact. * @@ -111,29 +129,6 @@ public static String getPackageUrl(final Artifact artifact) { return sb.toString(); } - /** - * Gets a map of checksum algorithm names to hex-encoded digest values for the given artifact file. - * - * @param artifact A Maven artifact. - * @param algorithms JSSE names of algorithms to use - * @return A map of checksum algorithm names to hex-encoded digest values. - * @throws IOException If an I/O error occurs reading the artifact file. - * @throws IllegalArgumentException If any of the algorithms is not supported. - */ - private static Map getChecksums(final Artifact artifact, final String... algorithms) throws IOException { - final Map checksums = new HashMap<>(); - for (final String algorithm : algorithms) { - final String key = IN_TOTO_DIGEST_NAMES.get(algorithm); - if (key == null) { - throw new IllegalArgumentException("Invalid algorithm name for in-toto attestation: " + algorithm); - } - final DigestUtils digest = new DigestUtils(DigestUtils.getDigest(algorithm)); - final String checksum = digest.digestAsHex(artifact.getFile()); - checksums.put(key, checksum); - } - return checksums; - } - /** * Converts a Maven artifact to a SLSA {@link ResourceDescriptor}. * @@ -155,4 +150,9 @@ public static ResourceDescriptor toResourceDescriptor(final Artifact artifact, f } return descriptor; } + + /** No instances. */ + private ArtifactUtils() { + // prevent instantiation + } } diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index 67b8ea813..996e57ffe 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -36,9 +36,44 @@ public final class BuildDefinitions { /** - * No instances. + * Reconstructs the Maven command line string from the given execution request. + * + * @param request the Maven execution request + * @return a string representation of the Maven command line */ - private BuildDefinitions() { + static String commandLine(final MavenExecutionRequest request) { + final List args = new ArrayList<>(request.getGoals()); + final String profiles = String.join(",", request.getActiveProfiles()); + if (!profiles.isEmpty()) { + args.add("-P" + profiles); + } + request.getUserProperties().forEach((key, value) -> args.add("-D" + key + "=" + value)); + return String.join(" ", args); + } + + /** + * Returns a map of external build parameters captured from the current JVM and Maven session. + * + * @param session the current Maven session + * @return a map of parameter names to values + */ + public static Map externalParameters(final MavenSession session) { + final Map params = new HashMap<>(); + params.put("jvm.args", ManagementFactory.getRuntimeMXBean().getInputArguments()); + final MavenExecutionRequest request = session.getRequest(); + params.put("maven.goals", request.getGoals()); + params.put("maven.profiles", request.getActiveProfiles()); + params.put("maven.user.properties", request.getUserProperties()); + params.put("maven.cmdline", commandLine(request)); + final Map env = new HashMap<>(); + params.put("env", env); + for (final Map.Entry entry : System.getenv().entrySet()) { + final String key = entry.getKey(); + if ("TZ".equals(key) || "LANG".equals(key) || key.startsWith("LC_")) { + env.put(key, entry.getValue()); + } + } + return params; } /** @@ -106,43 +141,8 @@ public static ResourceDescriptor maven(final String version, final Path mavenHom } /** - * Returns a map of external build parameters captured from the current JVM and Maven session. - * - * @param session the current Maven session - * @return a map of parameter names to values - */ - public static Map externalParameters(final MavenSession session) { - final Map params = new HashMap<>(); - params.put("jvm.args", ManagementFactory.getRuntimeMXBean().getInputArguments()); - final MavenExecutionRequest request = session.getRequest(); - params.put("maven.goals", request.getGoals()); - params.put("maven.profiles", request.getActiveProfiles()); - params.put("maven.user.properties", request.getUserProperties()); - params.put("maven.cmdline", commandLine(request)); - final Map env = new HashMap<>(); - params.put("env", env); - for (final Map.Entry entry : System.getenv().entrySet()) { - final String key = entry.getKey(); - if ("TZ".equals(key) || "LANG".equals(key) || key.startsWith("LC_")) { - env.put(key, entry.getValue()); - } - } - return params; - } - - /** - * Reconstructs the Maven command line string from the given execution request. - * - * @param request the Maven execution request - * @return a string representation of the Maven command line + * No instances. */ - static String commandLine(final MavenExecutionRequest request) { - final List args = new ArrayList<>(request.getGoals()); - final String profiles = String.join(",", request.getActiveProfiles()); - if (!profiles.isEmpty()) { - args.add("-P" + profiles); - } - request.getUserProperties().forEach((key, value) -> args.add("-D" + key + "=" + value)); - return String.join(" ", args); + private BuildDefinitions() { } } diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java index 20267a497..3ad334ff2 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -47,12 +47,6 @@ */ public final class DsseUtils { - /** - * Not instantiable. - */ - private DsseUtils() { - } - /** * Creates and prepares a {@link GpgSigner} from the given configuration. * @@ -79,6 +73,56 @@ public static AbstractGpgSigner createGpgSigner(final String executable, final b return signer; } + /** + * Extracts the key identifier from a binary OpenPGP Signature Packet. + * + * @param sigBytes raw binary OpenPGP Signature Packet bytes + * @return uppercase hex-encoded fingerprint or key ID string + * @throws MojoExecutionException if {@code sigBytes} cannot be parsed as an OpenPGP signature + */ + public static String getKeyId(final byte[] sigBytes) throws MojoExecutionException { + try { + final PGPSignatureList sigList = (PGPSignatureList) new BcPGPObjectFactory(sigBytes).nextObject(); + final PGPSignature sig = sigList.get(0); + final PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); + if (hashed != null) { + final IssuerFingerprint fp = hashed.getIssuerFingerprint(); + if (fp != null) { + return Hex.encodeHexString(fp.getFingerprint()); + } + } + return Long.toHexString(sig.getKeyID()).toUpperCase(Locale.ROOT); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to extract key ID from signature", e); + } + } + + /** + * Signs {@code paeFile} and returns the raw OpenPGP signature bytes. + * + *

The signer must already have {@link AbstractGpgSigner#prepare()} called before this method is invoked.

+ * + * @param signer the configured, prepared signer + * @param path path to the file to sign + * @return raw binary PGP signature bytes + * @throws MojoExecutionException if signing or signature decoding fails + */ + public static byte[] signFile(final AbstractGpgSigner signer, final Path path) throws MojoExecutionException { + final Path signaturePath = signer.generateSignatureForArtifact(path.toFile()).toPath(); + final byte[] signatureBytes; + try (InputStream in = Files.newInputStream(signaturePath); ArmoredInputStream armoredIn = new ArmoredInputStream(in)) { + signatureBytes = IOUtils.toByteArray(armoredIn); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to read signature file: " + signaturePath, e); + } + try { + Files.delete(signaturePath); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to delete signature file: " + signaturePath, e); + } + return signatureBytes; + } + /** * Serializes {@code statement} to JSON using the DSSE Pre-Authentication Encoding (PAE). * @@ -127,52 +171,8 @@ public static Path writePaeFile(final byte[] statementBytes, final Path buildDir } /** - * Signs {@code paeFile} and returns the raw OpenPGP signature bytes. - * - *

The signer must already have {@link AbstractGpgSigner#prepare()} called before this method is invoked.

- * - * @param signer the configured, prepared signer - * @param path path to the file to sign - * @return raw binary PGP signature bytes - * @throws MojoExecutionException if signing or signature decoding fails - */ - public static byte[] signFile(final AbstractGpgSigner signer, final Path path) throws MojoExecutionException { - final Path signaturePath = signer.generateSignatureForArtifact(path.toFile()).toPath(); - final byte[] signatureBytes; - try (InputStream in = Files.newInputStream(signaturePath); ArmoredInputStream armoredIn = new ArmoredInputStream(in)) { - signatureBytes = IOUtils.toByteArray(armoredIn); - } catch (final IOException e) { - throw new MojoExecutionException("Failed to read signature file: " + signaturePath, e); - } - try { - Files.delete(signaturePath); - } catch (final IOException e) { - throw new MojoExecutionException("Failed to delete signature file: " + signaturePath, e); - } - return signatureBytes; - } - - /** - * Extracts the key identifier from a binary OpenPGP Signature Packet. - * - * @param sigBytes raw binary OpenPGP Signature Packet bytes - * @return uppercase hex-encoded fingerprint or key ID string - * @throws MojoExecutionException if {@code sigBytes} cannot be parsed as an OpenPGP signature + * Not instantiable. */ - public static String getKeyId(final byte[] sigBytes) throws MojoExecutionException { - try { - final PGPSignatureList sigList = (PGPSignatureList) new BcPGPObjectFactory(sigBytes).nextObject(); - final PGPSignature sig = sigList.get(0); - final PGPSignatureSubpacketVector hashed = sig.getHashedSubPackets(); - if (hashed != null) { - final IssuerFingerprint fp = hashed.getIssuerFingerprint(); - if (fp != null) { - return Hex.encodeHexString(fp.getFingerprint()); - } - } - return Long.toHexString(sig.getKeyID()).toUpperCase(Locale.ROOT); - } catch (final IOException e) { - throw new MojoExecutionException("Failed to extract key ID from signature", e); - } + private DsseUtils() { } } diff --git a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java index acbd7bed1..ecaa19f35 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -36,34 +36,29 @@ public final class GitUtils { private static final String SCM_GIT_PREFIX = "scm:git:"; /** - * Returns the Git tree hash for the given directory. - * - * @param path A directory path. - * @return A hex-encoded SHA-1 tree hash. - * @throws IOException If the path is not a directory or an I/O error occurs. - */ - public static String gitTree(final Path path) throws IOException { - if (!Files.isDirectory(path)) { - throw new IOException("Path is not a directory: " + path); - } - final MessageDigest digest = DigestUtils.getSha1Digest(); - return Hex.encodeHexString(GitIdentifiers.treeId(digest, path)); - } - - /** - * Converts an SCM URI to a download URI suffixed with the current branch name. + * Walks up the directory tree from {@code path} to find the {@code .git} directory. * - * @param scmUri A Maven SCM URI starting with {@code scm:git}. - * @param repositoryPath A path inside the Git repository. - * @return A download URI of the form {@code git+@}. - * @throws IOException If the current branch cannot be determined. + * @param path A path inside the Git repository. + * @return The path to the {@code .git} directory (or file for worktrees). + * @throws IOException If no {@code .git} directory is found. */ - public static String scmToDownloadUri(final String scmUri, final Path repositoryPath) throws IOException { - if (!scmUri.startsWith(SCM_GIT_PREFIX)) { - throw new IllegalArgumentException("Invalid scmUri: " + scmUri); + private static Path findGitDir(final Path path) throws IOException { + Path current = path.toAbsolutePath(); + while (current != null) { + final Path candidate = current.resolve(".git"); + if (Files.isDirectory(candidate)) { + return candidate; + } + if (Files.isRegularFile(candidate)) { + // git worktree: .git is a file containing "gitdir: /path/to/real/.git" + final String content = new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim(); + if (content.startsWith("gitdir: ")) { + return Paths.get(content.substring("gitdir: ".length())); + } + } + current = current.getParent(); } - final String currentBranch = getCurrentBranch(repositoryPath); - return "git+" + scmUri.substring(SCM_GIT_PREFIX.length()) + "@" + currentBranch; + throw new IOException("No .git directory found above: " + path); } /** @@ -86,29 +81,34 @@ public static String getCurrentBranch(final Path repositoryPath) throws IOExcept } /** - * Walks up the directory tree from {@code path} to find the {@code .git} directory. + * Returns the Git tree hash for the given directory. * - * @param path A path inside the Git repository. - * @return The path to the {@code .git} directory (or file for worktrees). - * @throws IOException If no {@code .git} directory is found. + * @param path A directory path. + * @return A hex-encoded SHA-1 tree hash. + * @throws IOException If the path is not a directory or an I/O error occurs. */ - private static Path findGitDir(final Path path) throws IOException { - Path current = path.toAbsolutePath(); - while (current != null) { - final Path candidate = current.resolve(".git"); - if (Files.isDirectory(candidate)) { - return candidate; - } - if (Files.isRegularFile(candidate)) { - // git worktree: .git is a file containing "gitdir: /path/to/real/.git" - final String content = new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim(); - if (content.startsWith("gitdir: ")) { - return Paths.get(content.substring("gitdir: ".length())); - } - } - current = current.getParent(); + public static String gitTree(final Path path) throws IOException { + if (!Files.isDirectory(path)) { + throw new IOException("Path is not a directory: " + path); } - throw new IOException("No .git directory found above: " + path); + final MessageDigest digest = DigestUtils.getSha1Digest(); + return Hex.encodeHexString(GitIdentifiers.treeId(digest, path)); + } + + /** + * Converts an SCM URI to a download URI suffixed with the current branch name. + * + * @param scmUri A Maven SCM URI starting with {@code scm:git}. + * @param repositoryPath A path inside the Git repository. + * @return A download URI of the form {@code git+@}. + * @throws IOException If the current branch cannot be determined. + */ + public static String scmToDownloadUri(final String scmUri, final Path repositoryPath) throws IOException { + if (!scmUri.startsWith(SCM_GIT_PREFIX)) { + throw new IllegalArgumentException("Invalid scmUri: " + scmUri); + } + final String currentBranch = getCurrentBranch(repositoryPath); + return "git+" + scmUri.substring(SCM_GIT_PREFIX.length()) + "@" + currentBranch; } /** No instances. */ diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 84ec69cfd..18b6dd20e 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -92,54 +92,11 @@ public class BuildAttestationMojo extends AbstractMojo { OBJECT_MAPPER.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } - /** - * The SCM connection URL for the current project. - */ - @Parameter(defaultValue = "${project.scm.connection}", readonly = true) - private String scmConnectionUrl; - - /** - * The Maven home directory. - */ - @Parameter(defaultValue = "${maven.home}", readonly = true) - private File mavenHome; - - /** - * Issue SCM actions at this local directory. - */ - @Parameter(property = "commons.release.scmDirectory", defaultValue = "${basedir}") - private File scmDirectory; - - /** - * The output directory for the attestation file. - */ - @Parameter(property = "commons.release.outputDirectory", defaultValue = "${project.build.directory}") - private File outputDirectory; - - /** - * Whether to skip attaching the attestation artifact to the project. - */ - @Parameter(property = "commons.release.skipAttach") - private boolean skipAttach; - - /** - * Whether to sign the attestation envelope with GPG. - */ - @Parameter(property = "commons.release.signAttestation", defaultValue = "true") - private boolean signAttestation; - /** * Checksum algorithms used in the generated attestation. */ @Parameter(property = "commons.release.checksums.algorithms", defaultValue = "SHA-512,SHA-256,SHA-1,MD5") private String algorithmNames; - - /** - * Path to the GPG executable; if not set, {@code gpg} is resolved from {@code PATH}. - */ - @Parameter(property = "gpg.executable") - private String executable; - /** * Whether to include the default GPG keyring. * @@ -147,14 +104,11 @@ public class BuildAttestationMojo extends AbstractMojo { */ @Parameter(property = "gpg.defaultKeyring", defaultValue = "true") private boolean defaultKeyring; - /** - * GPG database lock mode passed via {@code --lock-once}, {@code --lock-multiple}, or - * {@code --lock-never}; no lock flag is added when not set. + * Path to the GPG executable; if not set, {@code gpg} is resolved from {@code PATH}. */ - @Parameter(property = "gpg.lockMode") - private String lockMode; - + @Parameter(property = "gpg.executable") + private String executable; /** * Name or fingerprint of the GPG key to use for signing. * @@ -162,45 +116,74 @@ public class BuildAttestationMojo extends AbstractMojo { */ @Parameter(property = "gpg.keyname") private String keyname; - /** - * Whether to use gpg-agent for passphrase management. - * - *

For GPG versions before 2.1, passes {@code --use-agent} or {@code --no-use-agent} - * accordingly; ignored for GPG 2.1 and later where the agent is always used.

+ * GPG database lock mode passed via {@code --lock-once}, {@code --lock-multiple}, or + * {@code --lock-never}; no lock flag is added when not set. */ - @Parameter(property = "gpg.useagent", defaultValue = "true") - private boolean useAgent; - + @Parameter(property = "gpg.lockMode") + private String lockMode; /** - * GPG signer used for signing; lazily initialized from plugin parameters when {@code null}. + * The Maven home directory. */ - private AbstractGpgSigner signer; - + @Parameter(defaultValue = "${maven.home}", readonly = true) + private File mavenHome; /** - * The current Maven project. + * Helper to attach artifacts to the project. */ - private final MavenProject project; - + private final MavenProjectHelper mavenProjectHelper; /** - * SCM manager to detect the Git revision. + * The output directory for the attestation file. */ - private final ScmManager scmManager; - + @Parameter(property = "commons.release.outputDirectory", defaultValue = "${project.build.directory}") + private File outputDirectory; + /** + * The current Maven project. + */ + private final MavenProject project; /** * Runtime information. */ private final RuntimeInformation runtimeInformation; - + /** + * The SCM connection URL for the current project. + */ + @Parameter(defaultValue = "${project.scm.connection}", readonly = true) + private String scmConnectionUrl; + /** + * Issue SCM actions at this local directory. + */ + @Parameter(property = "commons.release.scmDirectory", defaultValue = "${basedir}") + private File scmDirectory; + /** + * SCM manager to detect the Git revision. + */ + private final ScmManager scmManager; /** * The current Maven session, used to resolve plugin dependencies. */ private final MavenSession session; - /** - * Helper to attach artifacts to the project. + * Whether to sign the attestation envelope with GPG. */ - private final MavenProjectHelper mavenProjectHelper; + @Parameter(property = "commons.release.signAttestation", defaultValue = "true") + private boolean signAttestation; + /** + * GPG signer used for signing; lazily initialized from plugin parameters when {@code null}. + */ + private AbstractGpgSigner signer; + /** + * Whether to skip attaching the attestation artifact to the project. + */ + @Parameter(property = "commons.release.skipAttach") + private boolean skipAttach; + /** + * Whether to use gpg-agent for passphrase management. + * + *

For GPG versions before 2.1, passes {@code --use-agent} or {@code --no-use-agent} + * accordingly; ignored for GPG 2.1 and later where the agent is always used.

+ */ + @Parameter(property = "gpg.useagent", defaultValue = "true") + private boolean useAgent; /** * Creates a new instance with the given dependencies. @@ -222,88 +205,21 @@ public BuildAttestationMojo(final MavenProject project, final ScmManager scmMana } /** - * Sets the output directory for the attestation file. - * - * @param outputDirectory The output directory. - */ - void setOutputDirectory(final File outputDirectory) { - this.outputDirectory = outputDirectory; - } - - /** - * Gets the SCM directory. - * - * @return The SCM directory. - */ - public File getScmDirectory() { - return scmDirectory; - } - - /** - * Sets the SCM directory. - * - * @param scmDirectory The SCM directory. - */ - public void setScmDirectory(final File scmDirectory) { - this.scmDirectory = scmDirectory; - } - - /** - * Sets the public SCM connection URL. - * - * @param scmConnectionUrl The SCM connection URL. - */ - void setScmConnectionUrl(final String scmConnectionUrl) { - this.scmConnectionUrl = scmConnectionUrl; - } - - /** - * Sets the Maven home directory. - * - * @param mavenHome The Maven home directory. - */ - void setMavenHome(final File mavenHome) { - this.mavenHome = mavenHome; - } - - /** - * Sets whether to sign the attestation envelope. - * - * @param signAttestation {@code true} to sign, {@code false} to skip signing - */ - void setSignAttestation(final boolean signAttestation) { - this.signAttestation = signAttestation; - } - - /** - * Sets the GPG signer used for signing. Intended for testing. - * - * @param signer the signer to use - */ - void setSigner(final AbstractGpgSigner signer) { - this.signer = signer; - } - - /** - * Sets the list of checksum algorithms to use. - * - * @param algorithmNames A comma-separated list of {@link java.security.MessageDigest} algorithm names to use. - */ - void setAlgorithmNames(final String algorithmNames) { - this.algorithmNames = algorithmNames; - } - - /** - * Gets the GPG signer, creating and preparing it from plugin parameters if not already set. + * Creates the output directory if it does not already exist and returns its path. * - * @return the prepared signer - * @throws MojoFailureException if signer preparation fails + * @return the output directory path + * @throws MojoExecutionException if the directory cannot be created */ - private AbstractGpgSigner getSigner() throws MojoFailureException { - if (signer == null) { - signer = DsseUtils.createGpgSigner(executable, defaultKeyring, lockMode, keyname, useAgent, getLog()); + private Path ensureOutputDirectory() throws MojoExecutionException { + final Path outputPath = outputDirectory.toPath(); + try { + if (!Files.exists(outputPath)) { + Files.createDirectories(outputPath); + } + } catch (final IOException e) { + throw new MojoExecutionException("Could not create output directory.", e); } - return signer; + return outputPath; } @Override @@ -336,105 +252,6 @@ public void execute() throws MojoFailureException, MojoExecutionException { } } - /** - * Creates the output directory if it does not already exist and returns its path. - * - * @return the output directory path - * @throws MojoExecutionException if the directory cannot be created - */ - private Path ensureOutputDirectory() throws MojoExecutionException { - final Path outputPath = outputDirectory.toPath(); - try { - if (!Files.exists(outputPath)) { - Files.createDirectories(outputPath); - } - } catch (final IOException e) { - throw new MojoExecutionException("Could not create output directory.", e); - } - return outputPath; - } - - /** - * Serializes the attestation statement as a bare JSON line and writes it to {@code artifactPath}. - * - * @param statement the attestation statement to write - * @param artifactPath the destination file path - * @throws MojoExecutionException if the file cannot be written - */ - private void writeStatement(final Statement statement, final Path artifactPath) throws MojoExecutionException { - getLog().info("Writing attestation statement to: " + artifactPath); - writeAndAttach(statement, artifactPath); - } - - /** - * Signs the attestation statement with GPG and writes it to {@code artifactPath}. - * - * @param statement the attestation statement to sign and write - * @param outputPath directory used for intermediate PAE and signature files - * @param artifactPath the destination file path for the envelope - * @throws MojoExecutionException if serialization, signing, or file I/O fails - * @throws MojoFailureException if the GPG signer cannot be prepared - */ - private void signAndWriteStatement(final Statement statement, final Path outputPath, - final Path artifactPath) throws MojoExecutionException, MojoFailureException { - final byte[] statementBytes; - try { - statementBytes = OBJECT_MAPPER.writeValueAsBytes(statement); - } catch (final JsonProcessingException e) { - throw new MojoExecutionException("Failed to serialize attestation statement", e); - } - final AbstractGpgSigner signer = getSigner(); - final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); - final byte[] sigBytes = DsseUtils.signFile(signer, paeFile); - - final Signature sig = new Signature(); - sig.setKeyid(DsseUtils.getKeyId(sigBytes)); - sig.setSig(sigBytes); - - final DsseEnvelope envelope = new DsseEnvelope(); - envelope.setPayload(statementBytes); - envelope.setSignatures(Collections.singletonList(sig)); - - getLog().info("Writing signed attestation envelope to: " + artifactPath); - writeAndAttach(envelope, artifactPath); - } - - /** - * Writes {@code value} as a JSON line to {@code artifactPath} and optionally attaches it to the project. - * - * @param value the object to serialize - * @param artifactPath the destination file path - * @throws MojoExecutionException if the file cannot be written - */ - private void writeAndAttach(final Object value, final Path artifactPath) throws MojoExecutionException { - try (OutputStream os = Files.newOutputStream(artifactPath)) { - OBJECT_MAPPER.writeValue(os, value); - os.write('\n'); - } catch (final IOException e) { - throw new MojoExecutionException("Could not write attestation to: " + artifactPath, e); - } - if (!skipAttach) { - final Artifact mainArtifact = project.getArtifact(); - getLog().info(String.format("Attaching attestation as %s-%s.%s", mainArtifact.getArtifactId(), mainArtifact.getVersion(), ATTESTATION_EXTENSION)); - mavenProjectHelper.attachArtifact(project, ATTESTATION_EXTENSION, null, artifactPath.toFile()); - } - } - - /** - * Get the artifacts generated by the build. - * - * @return A list of resource descriptors for the build artifacts. - * @throws MojoExecutionException If artifact hashing fails. - */ - private List getSubjects() throws MojoExecutionException { - final List subjects = new ArrayList<>(); - subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact(), algorithmNames)); - for (final Artifact artifact : project.getAttachedArtifacts()) { - subjects.add(ArtifactUtils.toResourceDescriptor(artifact, algorithmNames)); - } - return subjects; - } - /** * Gets resource descriptors for the JVM, Maven installation, SCM source, and project dependencies. * @@ -455,6 +272,17 @@ private List getBuildDependencies() throws MojoExecutionExce return dependencies; } + /** + * Gets build metadata derived from the current Maven session, including start and finish timestamps. + * + * @return The build metadata. + */ + private BuildMetadata getBuildMetadata() { + final OffsetDateTime startedOn = session.getStartTime().toInstant().atOffset(ZoneOffset.UTC); + final OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC); + return new BuildMetadata(null, startedOn, finishedOn); + } + /** * Gets resource descriptors for all resolved project dependencies. * @@ -487,6 +315,15 @@ private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionE return scmDescriptor; } + /** + * Gets the SCM directory. + * + * @return The SCM directory. + */ + public File getScmDirectory() { + return scmDirectory; + } + /** * Gets an SCM repository from the configured connection URL. * @@ -546,13 +383,159 @@ private String getScmRevision(final InfoScmResult result) throws MojoExecutionEx } /** - * Gets build metadata derived from the current Maven session, including start and finish timestamps. + * Gets the GPG signer, creating and preparing it from plugin parameters if not already set. * - * @return The build metadata. + * @return the prepared signer + * @throws MojoFailureException if signer preparation fails */ - private BuildMetadata getBuildMetadata() { - final OffsetDateTime startedOn = session.getStartTime().toInstant().atOffset(ZoneOffset.UTC); - final OffsetDateTime finishedOn = OffsetDateTime.now(ZoneOffset.UTC); - return new BuildMetadata(null, startedOn, finishedOn); + private AbstractGpgSigner getSigner() throws MojoFailureException { + if (signer == null) { + signer = DsseUtils.createGpgSigner(executable, defaultKeyring, lockMode, keyname, useAgent, getLog()); + } + return signer; + } + + /** + * Get the artifacts generated by the build. + * + * @return A list of resource descriptors for the build artifacts. + * @throws MojoExecutionException If artifact hashing fails. + */ + private List getSubjects() throws MojoExecutionException { + final List subjects = new ArrayList<>(); + subjects.add(ArtifactUtils.toResourceDescriptor(project.getArtifact(), algorithmNames)); + for (final Artifact artifact : project.getAttachedArtifacts()) { + subjects.add(ArtifactUtils.toResourceDescriptor(artifact, algorithmNames)); + } + return subjects; + } + + /** + * Sets the list of checksum algorithms to use. + * + * @param algorithmNames A comma-separated list of {@link java.security.MessageDigest} algorithm names to use. + */ + void setAlgorithmNames(final String algorithmNames) { + this.algorithmNames = algorithmNames; + } + + /** + * Sets the Maven home directory. + * + * @param mavenHome The Maven home directory. + */ + void setMavenHome(final File mavenHome) { + this.mavenHome = mavenHome; + } + + /** + * Sets the output directory for the attestation file. + * + * @param outputDirectory The output directory. + */ + void setOutputDirectory(final File outputDirectory) { + this.outputDirectory = outputDirectory; + } + + /** + * Sets the public SCM connection URL. + * + * @param scmConnectionUrl The SCM connection URL. + */ + void setScmConnectionUrl(final String scmConnectionUrl) { + this.scmConnectionUrl = scmConnectionUrl; + } + + /** + * Sets the SCM directory. + * + * @param scmDirectory The SCM directory. + */ + public void setScmDirectory(final File scmDirectory) { + this.scmDirectory = scmDirectory; + } + + /** + * Sets whether to sign the attestation envelope. + * + * @param signAttestation {@code true} to sign, {@code false} to skip signing + */ + void setSignAttestation(final boolean signAttestation) { + this.signAttestation = signAttestation; + } + + /** + * Sets the GPG signer used for signing. Intended for testing. + * + * @param signer the signer to use + */ + void setSigner(final AbstractGpgSigner signer) { + this.signer = signer; + } + + /** + * Signs the attestation statement with GPG and writes it to {@code artifactPath}. + * + * @param statement the attestation statement to sign and write + * @param outputPath directory used for intermediate PAE and signature files + * @param artifactPath the destination file path for the envelope + * @throws MojoExecutionException if serialization, signing, or file I/O fails + * @throws MojoFailureException if the GPG signer cannot be prepared + */ + private void signAndWriteStatement(final Statement statement, final Path outputPath, + final Path artifactPath) throws MojoExecutionException, MojoFailureException { + final byte[] statementBytes; + try { + statementBytes = OBJECT_MAPPER.writeValueAsBytes(statement); + } catch (final JsonProcessingException e) { + throw new MojoExecutionException("Failed to serialize attestation statement", e); + } + final AbstractGpgSigner signer = getSigner(); + final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); + final byte[] sigBytes = DsseUtils.signFile(signer, paeFile); + + final Signature sig = new Signature(); + sig.setKeyid(DsseUtils.getKeyId(sigBytes)); + sig.setSig(sigBytes); + + final DsseEnvelope envelope = new DsseEnvelope(); + envelope.setPayload(statementBytes); + envelope.setSignatures(Collections.singletonList(sig)); + + getLog().info("Writing signed attestation envelope to: " + artifactPath); + writeAndAttach(envelope, artifactPath); + } + + /** + * Writes {@code value} as a JSON line to {@code artifactPath} and optionally attaches it to the project. + * + * @param value the object to serialize + * @param artifactPath the destination file path + * @throws MojoExecutionException if the file cannot be written + */ + private void writeAndAttach(final Object value, final Path artifactPath) throws MojoExecutionException { + try (OutputStream os = Files.newOutputStream(artifactPath)) { + OBJECT_MAPPER.writeValue(os, value); + os.write('\n'); + } catch (final IOException e) { + throw new MojoExecutionException("Could not write attestation to: " + artifactPath, e); + } + if (!skipAttach) { + final Artifact mainArtifact = project.getArtifact(); + getLog().info(String.format("Attaching attestation as %s-%s.%s", mainArtifact.getArtifactId(), mainArtifact.getVersion(), ATTESTATION_EXTENSION)); + mavenProjectHelper.attachArtifact(project, ATTESTATION_EXTENSION, null, artifactPath.toFile()); + } + } + + /** + * Serializes the attestation statement as a bare JSON line and writes it to {@code artifactPath}. + * + * @param statement the attestation statement to write + * @param artifactPath the destination file path + * @throws MojoExecutionException if the file cannot be written + */ + private void writeStatement(final Statement statement, final Path artifactPath) throws MojoExecutionException { + getLog().info("Writing attestation statement to: " + artifactPath); + writeAndAttach(statement, artifactPath); } } diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java index 661d0ccd9..a856e2c9e 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java @@ -64,6 +64,21 @@ public BuildDefinition(String buildType, Map externalParameters) this.externalParameters = externalParameters; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BuildDefinition that = (BuildDefinition) o; + return Objects.equals(buildType, that.buildType) + && Objects.equals(externalParameters, that.externalParameters) + && Objects.equals(internalParameters, that.internalParameters) + && Objects.equals(resolvedDependencies, that.resolvedDependencies); + } + /** * Gets the URI indicating what type of build was performed. * @@ -75,15 +90,6 @@ public String getBuildType() { return buildType; } - /** - * Sets the URI indicating what type of build was performed. - * - * @param buildType the build type URI - */ - public void setBuildType(String buildType) { - this.buildType = buildType; - } - /** * Gets the inputs passed to the build, such as command-line arguments or environment variables. * @@ -93,15 +99,6 @@ public Map getExternalParameters() { return externalParameters; } - /** - * Sets the inputs passed to the build. - * - * @param externalParameters the external parameters map - */ - public void setExternalParameters(Map externalParameters) { - this.externalParameters = externalParameters; - } - /** * Gets the artifacts the build depends on, such as sources, dependencies, build tools, and base images, * specified by URI and digest. @@ -112,15 +109,6 @@ public Map getInternalParameters() { return internalParameters; } - /** - * Sets the artifacts the build depends on. - * - * @param internalParameters the internal parameters map - */ - public void setInternalParameters(Map internalParameters) { - this.internalParameters = internalParameters; - } - /** * Gets the materials that influenced the build. * @@ -132,33 +120,45 @@ public List getResolvedDependencies() { return resolvedDependencies; } + @Override + public int hashCode() { + return Objects.hash(buildType, externalParameters, internalParameters, resolvedDependencies); + } + /** - * Sets the materials that influenced the build. + * Sets the URI indicating what type of build was performed. * - * @param resolvedDependencies the list of resolved dependencies + * @param buildType the build type URI */ - public void setResolvedDependencies(List resolvedDependencies) { - this.resolvedDependencies = resolvedDependencies; + public void setBuildType(String buildType) { + this.buildType = buildType; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - BuildDefinition that = (BuildDefinition) o; - return Objects.equals(buildType, that.buildType) - && Objects.equals(externalParameters, that.externalParameters) - && Objects.equals(internalParameters, that.internalParameters) - && Objects.equals(resolvedDependencies, that.resolvedDependencies); + /** + * Sets the inputs passed to the build. + * + * @param externalParameters the external parameters map + */ + public void setExternalParameters(Map externalParameters) { + this.externalParameters = externalParameters; } - @Override - public int hashCode() { - return Objects.hash(buildType, externalParameters, internalParameters, resolvedDependencies); + /** + * Sets the artifacts the build depends on. + * + * @param internalParameters the internal parameters map + */ + public void setInternalParameters(Map internalParameters) { + this.internalParameters = internalParameters; + } + + /** + * Sets the materials that influenced the build. + * + * @param resolvedDependencies the list of resolved dependencies + */ + public void setResolvedDependencies(List resolvedDependencies) { + this.resolvedDependencies = resolvedDependencies; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java index 35d04e412..6f6316961 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java @@ -31,20 +31,18 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class BuildMetadata { + /** Timestamp when the build completed. */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") + @JsonProperty("finishedOn") + private OffsetDateTime finishedOn; /** Identifier for this build invocation. */ @JsonProperty("invocationId") private String invocationId; - /** Timestamp when the build started. */ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") @JsonProperty("startedOn") private OffsetDateTime startedOn; - /** Timestamp when the build completed. */ - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'") - @JsonProperty("finishedOn") - private OffsetDateTime finishedOn; - /** Creates a new BuildMetadata instance. */ public BuildMetadata() { } @@ -62,22 +60,31 @@ public BuildMetadata(String invocationId, OffsetDateTime startedOn, OffsetDateTi this.finishedOn = finishedOn; } + @Override + public boolean equals(Object o) { + if (!(o instanceof BuildMetadata)) { + return false; + } + BuildMetadata that = (BuildMetadata) o; + return Objects.equals(invocationId, that.invocationId) && Objects.equals(startedOn, that.startedOn) && Objects.equals(finishedOn, that.finishedOn); + } + /** - * Gets the identifier for this build invocation. + * Gets the timestamp of when the build completed, serialized as RFC 3339 in UTC ({@code "Z"} suffix). * - * @return the invocation identifier, or {@code null} if not set + * @return the completion timestamp, or {@code null} if not set */ - public String getInvocationId() { - return invocationId; + public OffsetDateTime getFinishedOn() { + return finishedOn; } /** - * Sets the identifier for this build invocation. + * Gets the identifier for this build invocation. * - * @param invocationId the invocation identifier + * @return the invocation identifier, or {@code null} if not set */ - public void setInvocationId(String invocationId) { - this.invocationId = invocationId; + public String getInvocationId() { + return invocationId; } /** @@ -89,22 +96,9 @@ public OffsetDateTime getStartedOn() { return startedOn; } - /** - * Sets the timestamp of when the build started. - * - * @param startedOn the start timestamp - */ - public void setStartedOn(OffsetDateTime startedOn) { - this.startedOn = startedOn; - } - - /** - * Gets the timestamp of when the build completed, serialized as RFC 3339 in UTC ({@code "Z"} suffix). - * - * @return the completion timestamp, or {@code null} if not set - */ - public OffsetDateTime getFinishedOn() { - return finishedOn; + @Override + public int hashCode() { + return Objects.hash(invocationId, startedOn, finishedOn); } /** @@ -116,18 +110,22 @@ public void setFinishedOn(OffsetDateTime finishedOn) { this.finishedOn = finishedOn; } - @Override - public boolean equals(Object o) { - if (!(o instanceof BuildMetadata)) { - return false; - } - BuildMetadata that = (BuildMetadata) o; - return Objects.equals(invocationId, that.invocationId) && Objects.equals(startedOn, that.startedOn) && Objects.equals(finishedOn, that.finishedOn); + /** + * Sets the identifier for this build invocation. + * + * @param invocationId the invocation identifier + */ + public void setInvocationId(String invocationId) { + this.invocationId = invocationId; } - @Override - public int hashCode() { - return Objects.hash(invocationId, startedOn, finishedOn); + /** + * Sets the timestamp of when the build started. + * + * @param startedOn the start timestamp + */ + public void setStartedOn(OffsetDateTime startedOn) { + this.startedOn = startedOn; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java index 635e75cfb..31102295a 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java @@ -31,14 +31,12 @@ */ public class Builder { - /** Identifier URI of the builder. */ - @JsonProperty("id") - private String id = "https://commons.apache.org/builds/0.1.0"; - /** Orchestrator dependencies that may affect provenance generation. */ @JsonProperty("builderDependencies") private List builderDependencies = new ArrayList<>(); - + /** Identifier URI of the builder. */ + @JsonProperty("id") + private String id = "https://commons.apache.org/builds/0.1.0"; /** Map of build platform component names to their versions. */ @JsonProperty("version") private Map version = new HashMap<>(); @@ -47,6 +45,27 @@ public class Builder { public Builder() { } + @Override + public boolean equals(Object o) { + if (!(o instanceof Builder)) { + return false; + } + Builder that = (Builder) o; + return Objects.equals(id, that.id) + && Objects.equals(builderDependencies, that.builderDependencies) + && Objects.equals(version, that.version); + } + + /** + * Gets orchestrator dependencies that do not run within the build workload and do not affect the build output, + * but may affect provenance generation or security guarantees. + * + * @return the list of builder dependencies, or {@code null} if not set + */ + public List getBuilderDependencies() { + return builderDependencies; + } + /** * Gets the identifier of the builder. * @@ -57,22 +76,17 @@ public String getId() { } /** - * Sets the identifier of the builder. + * Gets a map of build platform component names to their versions. * - * @param id the builder identifier URI + * @return the version map, or {@code null} if not set */ - public void setId(String id) { - this.id = id; + public Map getVersion() { + return version; } - /** - * Gets orchestrator dependencies that do not run within the build workload and do not affect the build output, - * but may affect provenance generation or security guarantees. - * - * @return the list of builder dependencies, or {@code null} if not set - */ - public List getBuilderDependencies() { - return builderDependencies; + @Override + public int hashCode() { + return Objects.hash(id, builderDependencies, version); } /** @@ -85,12 +99,12 @@ public void setBuilderDependencies(List builderDependencies) } /** - * Gets a map of build platform component names to their versions. + * Sets the identifier of the builder. * - * @return the version map, or {@code null} if not set + * @param id the builder identifier URI */ - public Map getVersion() { - return version; + public void setId(String id) { + this.id = id; } /** @@ -102,22 +116,6 @@ public void setVersion(Map version) { this.version = version; } - @Override - public boolean equals(Object o) { - if (!(o instanceof Builder)) { - return false; - } - Builder that = (Builder) o; - return Objects.equals(id, that.id) - && Objects.equals(builderDependencies, that.builderDependencies) - && Objects.equals(version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(id, builderDependencies, version); - } - @Override public String toString() { return "Builder{id='" + id + "', builderDependencies=" + builderDependencies + ", version=" + version + '}'; diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java index fdb2353f3..2c9bc601a 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java @@ -31,15 +31,12 @@ public class DsseEnvelope { /** The payload type URI for in-toto attestation statements. */ public static final String PAYLOAD_TYPE = "application/vnd.in-toto+json"; - - /** Content type identifying the format of {@link #payload}. */ - @JsonProperty("payloadType") - private String payloadType = PAYLOAD_TYPE; - /** Serialized statement bytes, Base64-encoded in JSON. */ @JsonProperty("payload") private byte[] payload; - + /** Content type identifying the format of {@link #payload}. */ + @JsonProperty("payloadType") + private String payloadType = PAYLOAD_TYPE; /** One or more signatures over the PAE-encoded payload. */ @JsonProperty("signatures") private List signatures; @@ -48,6 +45,27 @@ public class DsseEnvelope { public DsseEnvelope() { } + @Override + public boolean equals(Object o) { + if (!(o instanceof DsseEnvelope)) { + return false; + } + DsseEnvelope envelope = (DsseEnvelope) o; + return Objects.equals(payloadType, envelope.payloadType) && Arrays.equals(payload, envelope.payload) + && Objects.equals(signatures, envelope.signatures); + } + + /** + * Gets the serialized payload bytes. + * + *

When serialized to JSON the bytes are Base64-encoded.

+ * + * @return the payload bytes, or {@code null} if not set + */ + public byte[] getPayload() { + return payload; + } + /** * Gets the payload type URI. * @@ -58,23 +76,17 @@ public String getPayloadType() { } /** - * Sets the payload type URI. + * Gets the list of signatures over the PAE-encoded payload. * - * @param payloadType the payload type URI + * @return the signatures, or {@code null} if not set */ - public void setPayloadType(String payloadType) { - this.payloadType = payloadType; + public List getSignatures() { + return signatures; } - /** - * Gets the serialized payload bytes. - * - *

When serialized to JSON the bytes are Base64-encoded.

- * - * @return the payload bytes, or {@code null} if not set - */ - public byte[] getPayload() { - return payload; + @Override + public int hashCode() { + return Objects.hash(payloadType, Arrays.hashCode(payload), signatures); } /** @@ -87,12 +99,12 @@ public void setPayload(byte[] payload) { } /** - * Gets the list of signatures over the PAE-encoded payload. + * Sets the payload type URI. * - * @return the signatures, or {@code null} if not set + * @param payloadType the payload type URI */ - public List getSignatures() { - return signatures; + public void setPayloadType(String payloadType) { + this.payloadType = payloadType; } /** @@ -104,21 +116,6 @@ public void setSignatures(List signatures) { this.signatures = signatures; } - @Override - public boolean equals(Object o) { - if (!(o instanceof DsseEnvelope)) { - return false; - } - DsseEnvelope envelope = (DsseEnvelope) o; - return Objects.equals(payloadType, envelope.payloadType) && Arrays.equals(payload, envelope.payload) - && Objects.equals(signatures, envelope.signatures); - } - - @Override - public int hashCode() { - return Objects.hash(payloadType, Arrays.hashCode(payload), signatures); - } - @Override public String toString() { return "DsseEnvelope{payloadType='" + payloadType + "', payload=<" + (payload != null ? payload.length : 0) diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java index c9dfd2e28..1b7361e7d 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java @@ -58,6 +58,18 @@ public Provenance(BuildDefinition buildDefinition, RunDetails runDetails) { this.runDetails = runDetails; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Provenance that = (Provenance) o; + return Objects.equals(buildDefinition, that.buildDefinition) && Objects.equals(runDetails, that.runDetails); + } + /** * Gets the build definition describing all inputs that produced the build output. * @@ -70,21 +82,26 @@ public BuildDefinition getBuildDefinition() { } /** - * Sets the build definition describing all inputs that produced the build output. + * Gets the details about the invocation of the build tool and the environment in which it was run. * - * @param buildDefinition the build definition + * @return the run details, or {@code null} if not set */ - public void setBuildDefinition(BuildDefinition buildDefinition) { - this.buildDefinition = buildDefinition; + public RunDetails getRunDetails() { + return runDetails; + } + + @Override + public int hashCode() { + return Objects.hash(buildDefinition, runDetails); } /** - * Gets the details about the invocation of the build tool and the environment in which it was run. + * Sets the build definition describing all inputs that produced the build output. * - * @return the run details, or {@code null} if not set + * @param buildDefinition the build definition */ - public RunDetails getRunDetails() { - return runDetails; + public void setBuildDefinition(BuildDefinition buildDefinition) { + this.buildDefinition = buildDefinition; } /** @@ -96,23 +113,6 @@ public void setRunDetails(RunDetails runDetails) { this.runDetails = runDetails; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Provenance that = (Provenance) o; - return Objects.equals(buildDefinition, that.buildDefinition) && Objects.equals(runDetails, that.runDetails); - } - - @Override - public int hashCode() { - return Objects.hash(buildDefinition, runDetails); - } - @Override public String toString() { return "Provenance{buildDefinition=" + buildDefinition + ", runDetails=" + runDetails + '}'; diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java index 2ce42ce25..79210d33d 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java @@ -32,33 +32,27 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class ResourceDescriptor { - /** Human-readable name of the resource. */ - @JsonProperty("name") - private String name; - - /** URI identifying the resource. */ - @JsonProperty("uri") - private String uri; - - /** Map of digest algorithm names to hex-encoded values. */ - @JsonProperty("digest") - private Map digest; - + /** Additional key-value metadata about the resource. */ + @JsonProperty("annotations") + private Map annotations; /** Raw contents of the resource, base64-encoded in JSON. */ @JsonProperty("content") private byte[] content; - + /** Map of digest algorithm names to hex-encoded values. */ + @JsonProperty("digest") + private Map digest; /** Download URI for the resource, if different from {@link #uri}. */ @JsonProperty("downloadLocation") private String downloadLocation; - /** Media type of the resource. */ @JsonProperty("mediaType") private String mediaType; - - /** Additional key-value metadata about the resource. */ - @JsonProperty("annotations") - private Map annotations; + /** Human-readable name of the resource. */ + @JsonProperty("name") + private String name; + /** URI identifying the resource. */ + @JsonProperty("uri") + private String uri; /** Creates a new ResourceDescriptor instance. */ public ResourceDescriptor() { @@ -75,69 +69,95 @@ public ResourceDescriptor(String uri, Map digest) { this.digest = digest; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourceDescriptor that = (ResourceDescriptor) o; + return Objects.equals(uri, that.uri) && Objects.equals(digest, that.digest); + } + /** - * Gets the name of the resource. + * Gets additional key-value metadata about the resource, such as filename, size, or builder-specific attributes. * - * @return the resource name, or {@code null} if not set + * @return the annotations map, or {@code null} if not set */ - public String getName() { - return name; + public Map getAnnotations() { + return annotations; } /** - * Sets the name of the resource. + * Gets the raw contents of the resource, base64-encoded when serialized to JSON. * - * @param name the resource name + * @return the resource content, or {@code null} if not set */ - public void setName(String name) { - this.name = name; + public byte[] getContent() { + return content; } /** - * Gets the URI identifying the resource. + * Gets the map of cryptographic digest algorithms to their corresponding hex-encoded values for this resource. * - * @return the resource URI, or {@code null} if not set + *

Common keys include {@code "sha256"} and {@code "sha512"}.

+ * + * @return the digest map, or {@code null} if not set */ - public String getUri() { - return uri; + public Map getDigest() { + return digest; } /** - * Sets the URI identifying the resource. + * Gets the download URI for the resource, if different from {@link #getUri()}. * - * @param uri the resource URI + * @return the download location URI, or {@code null} if not set */ - public void setUri(String uri) { - this.uri = uri; + public String getDownloadLocation() { + return downloadLocation; } /** - * Gets the map of cryptographic digest algorithms to their corresponding hex-encoded values for this resource. + * Gets the media type of the resource (e.g., {@code "application/octet-stream"}). * - *

Common keys include {@code "sha256"} and {@code "sha512"}.

+ * @return the media type, or {@code null} if not set + */ + public String getMediaType() { + return mediaType; + } + + /** + * Gets the name of the resource. * - * @return the digest map, or {@code null} if not set + * @return the resource name, or {@code null} if not set */ - public Map getDigest() { - return digest; + public String getName() { + return name; } /** - * Sets the map of cryptographic digest algorithms to their hex-encoded values. + * Gets the URI identifying the resource. * - * @param digest the digest map + * @return the resource URI, or {@code null} if not set */ - public void setDigest(Map digest) { - this.digest = digest; + public String getUri() { + return uri; + } + + @Override + public int hashCode() { + return Objects.hash(uri, digest); } /** - * Gets the raw contents of the resource, base64-encoded when serialized to JSON. + * Sets additional key-value metadata about the resource. * - * @return the resource content, or {@code null} if not set + * @param annotations the annotations map */ - public byte[] getContent() { - return content; + public void setAnnotations(Map annotations) { + this.annotations = annotations; } /** @@ -150,12 +170,12 @@ public void setContent(byte[] content) { } /** - * Gets the download URI for the resource, if different from {@link #getUri()}. + * Sets the map of cryptographic digest algorithms to their hex-encoded values. * - * @return the download location URI, or {@code null} if not set + * @param digest the digest map */ - public String getDownloadLocation() { - return downloadLocation; + public void setDigest(Map digest) { + this.digest = digest; } /** @@ -167,15 +187,6 @@ public void setDownloadLocation(String downloadLocation) { this.downloadLocation = downloadLocation; } - /** - * Gets the media type of the resource (e.g., {@code "application/octet-stream"}). - * - * @return the media type, or {@code null} if not set - */ - public String getMediaType() { - return mediaType; - } - /** * Sets the media type of the resource. * @@ -186,38 +197,21 @@ public void setMediaType(String mediaType) { } /** - * Gets additional key-value metadata about the resource, such as filename, size, or builder-specific attributes. + * Sets the name of the resource. * - * @return the annotations map, or {@code null} if not set + * @param name the resource name */ - public Map getAnnotations() { - return annotations; + public void setName(String name) { + this.name = name; } /** - * Sets additional key-value metadata about the resource. + * Sets the URI identifying the resource. * - * @param annotations the annotations map + * @param uri the resource URI */ - public void setAnnotations(Map annotations) { - this.annotations = annotations; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ResourceDescriptor that = (ResourceDescriptor) o; - return Objects.equals(uri, that.uri) && Objects.equals(digest, that.digest); - } - - @Override - public int hashCode() { - return Objects.hash(uri, digest); + public void setUri(String uri) { + this.uri = uri; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java index 85d8f6b0b..90aae318c 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java @@ -35,18 +35,16 @@ public class RunDetails { */ @JsonProperty("builder") private Builder builder; - - /** - * Metadata about the build invocation. - */ - @JsonProperty("metadata") - private BuildMetadata metadata; - /** * Artifacts produced as a side effect of the build. */ @JsonProperty("byproducts") private List byproducts; + /** + * Metadata about the build invocation. + */ + @JsonProperty("metadata") + private BuildMetadata metadata; /** * Creates a new RunDetails instance. @@ -65,6 +63,18 @@ public RunDetails(Builder builder, BuildMetadata metadata) { this.metadata = metadata; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RunDetails that = (RunDetails) o; + return Objects.equals(builder, that.builder) && Objects.equals(metadata, that.metadata) && Objects.equals(byproducts, that.byproducts); + } + /** * Gets the builder that executed the invocation. * @@ -77,12 +87,12 @@ public Builder getBuilder() { } /** - * Sets the builder that executed the invocation. + * Gets artifacts produced as a side effect of the build that are not the primary output. * - * @param builder the builder + * @return the list of byproduct artifacts, or {@code null} if not set */ - public void setBuilder(Builder builder) { - this.builder = builder; + public List getByproducts() { + return byproducts; } /** @@ -94,22 +104,18 @@ public BuildMetadata getMetadata() { return metadata; } - /** - * Sets the metadata about the build invocation. - * - * @param metadata the build metadata - */ - public void setMetadata(BuildMetadata metadata) { - this.metadata = metadata; + @Override + public int hashCode() { + return Objects.hash(builder, metadata, byproducts); } /** - * Gets artifacts produced as a side effect of the build that are not the primary output. + * Sets the builder that executed the invocation. * - * @return the list of byproduct artifacts, or {@code null} if not set + * @param builder the builder */ - public List getByproducts() { - return byproducts; + public void setBuilder(Builder builder) { + this.builder = builder; } /** @@ -121,21 +127,13 @@ public void setByproducts(List byproducts) { this.byproducts = byproducts; } - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RunDetails that = (RunDetails) o; - return Objects.equals(builder, that.builder) && Objects.equals(metadata, that.metadata) && Objects.equals(byproducts, that.byproducts); - } - - @Override - public int hashCode() { - return Objects.hash(builder, metadata, byproducts); + /** + * Sets the metadata about the build invocation. + * + * @param metadata the build metadata + */ + public void setMetadata(BuildMetadata metadata) { + this.metadata = metadata; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java index c2caf8000..d514608e4 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java @@ -46,6 +46,15 @@ public class Signature { public Signature() { } + @Override + public boolean equals(Object o) { + if (!(o instanceof Signature)) { + return false; + } + Signature signature = (Signature) o; + return Objects.equals(keyid, signature.keyid) && Arrays.equals(sig, signature.sig); + } + /** * Gets the key identifier hint, or {@code null} if not set. * @@ -56,21 +65,26 @@ public String getKeyid() { } /** - * Sets the key identifier hint. + * Gets the raw signature bytes. * - * @param keyid the key identifier, or {@code null} to leave unset + * @return the signature bytes, or {@code null} if not set */ - public void setKeyid(String keyid) { - this.keyid = keyid; + public byte[] getSig() { + return sig; + } + + @Override + public int hashCode() { + return Objects.hash(keyid, Arrays.hashCode(sig)); } /** - * Gets the raw signature bytes. + * Sets the key identifier hint. * - * @return the signature bytes, or {@code null} if not set + * @param keyid the key identifier, or {@code null} to leave unset */ - public byte[] getSig() { - return sig; + public void setKeyid(String keyid) { + this.keyid = keyid; } /** @@ -82,20 +96,6 @@ public void setSig(byte[] sig) { this.sig = sig; } - @Override - public boolean equals(Object o) { - if (!(o instanceof Signature)) { - return false; - } - Signature signature = (Signature) o; - return Objects.equals(keyid, signature.keyid) && Arrays.equals(sig, signature.sig); - } - - @Override - public int hashCode() { - return Objects.hash(keyid, Arrays.hashCode(sig)); - } - @Override public String toString() { return "Signature{keyid='" + keyid + "', sig=<" + (sig != null ? sig.length : 0) + " bytes>}"; diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java index 4f3506e0b..eefb90dc7 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java @@ -31,41 +31,40 @@ public class Statement { /** The in-toto statement schema URI. */ @JsonProperty("_type") public static final String TYPE = "https://in-toto.io/Statement/v1"; - - /** Software artifacts that the attestation applies to. */ - @JsonProperty("subject") - private List subject; - - /** URI identifying the type of the predicate. */ - @JsonProperty("predicateType") - private String predicateType; - /** The provenance predicate. */ @JsonProperty("predicate") private Provenance predicate; + /** URI identifying the type of the predicate. */ + @JsonProperty("predicateType") + private String predicateType; + /** Software artifacts that the attestation applies to. */ + @JsonProperty("subject") + private List subject; /** Creates a new Statement instance. */ public Statement() { } - /** - * Gets the set of software artifacts that the attestation applies to. - * - *

Each element represents a single artifact. Artifacts are matched purely by digest, regardless of content type.

- * - * @return the list of subject artifacts, or {@code null} if not set - */ - public List getSubject() { - return subject; + @Override + public boolean equals(Object o) { + if (!(o instanceof Statement)) { + return false; + } + Statement statement = (Statement) o; + return Objects.equals(subject, statement.subject) && Objects.equals(predicateType, statement.predicateType) && Objects.equals(predicate, + statement.predicate); } /** - * Sets the set of software artifacts that the attestation applies to. + * Gets the provenance predicate. * - * @param subject the list of subject artifacts + *

Unset is treated the same as set-but-empty. May be omitted if {@code predicateType} fully describes the + * predicate.

+ * + * @return the provenance predicate, or {@code null} if not set */ - public void setSubject(List subject) { - this.subject = subject; + public Provenance getPredicate() { + return predicate; } /** @@ -78,15 +77,19 @@ public String getPredicateType() { } /** - * Gets the provenance predicate. + * Gets the set of software artifacts that the attestation applies to. * - *

Unset is treated the same as set-but-empty. May be omitted if {@code predicateType} fully describes the - * predicate.

+ *

Each element represents a single artifact. Artifacts are matched purely by digest, regardless of content type.

* - * @return the provenance predicate, or {@code null} if not set + * @return the list of subject artifacts, or {@code null} if not set */ - public Provenance getPredicate() { - return predicate; + public List getSubject() { + return subject; + } + + @Override + public int hashCode() { + return Objects.hash(subject, predicateType, predicate); } /** @@ -99,19 +102,13 @@ public void setPredicate(Provenance predicate) { this.predicateType = Provenance.PREDICATE_TYPE; } - @Override - public boolean equals(Object o) { - if (!(o instanceof Statement)) { - return false; - } - Statement statement = (Statement) o; - return Objects.equals(subject, statement.subject) && Objects.equals(predicateType, statement.predicateType) && Objects.equals(predicate, - statement.predicate); - } - - @Override - public int hashCode() { - return Objects.hash(subject, predicateType, predicate); + /** + * Sets the set of software artifacts that the attestation applies to. + * + * @param subject the list of subject artifacts + */ + public void setSubject(List subject) { + this.subject = subject; } @Override diff --git a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java index fe58a540e..6c7a4d7a0 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java @@ -46,6 +46,12 @@ static Stream commandLineArguments() { ); } + private static Properties singletonProperties(final String key, final String value) { + final Properties p = new Properties(); + p.setProperty(key, value); + return p; + } + @ParameterizedTest(name = "{0}") @MethodSource("commandLineArguments") void commandLineTest(final String description, final List goals, final List profiles, @@ -56,10 +62,4 @@ void commandLineTest(final String description, final List goals, final L request.setUserProperties(userProperties); assertEquals(expected, BuildDefinitions.commandLine(request)); } - - private static Properties singletonProperties(final String key, final String value) { - final Properties p = new Properties(); - p.setProperty(key, value); - return p; - } } diff --git a/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java index 3cdef92fc..f751b114b 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/MojoUtils.java @@ -39,20 +39,6 @@ */ public final class MojoUtils { - private static ContainerConfiguration setupContainerConfiguration() { - final ClassWorld classWorld = - new ClassWorld("plexus.core", Thread.currentThread().getContextClassLoader()); - return new DefaultContainerConfiguration() - .setClassWorld(classWorld) - .setClassPathScanning(PlexusConstants.SCANNING_INDEX) - .setAutoWiring(true) - .setName("maven"); - } - - public static PlexusContainer setupContainer() throws PlexusContainerException { - return new DefaultPlexusContainer(setupContainerConfiguration()); - } - public static RepositorySystemSession createRepositorySystemSession( final PlexusContainer container, final Path localRepositoryPath) throws ComponentLookupException, RepositoryException { final LocalRepositoryManagerFactory factory = container.lookup(LocalRepositoryManagerFactory.class, "simple"); @@ -66,6 +52,20 @@ public static RepositorySystemSession createRepositorySystemSession( return repoSession; } + public static PlexusContainer setupContainer() throws PlexusContainerException { + return new DefaultPlexusContainer(setupContainerConfiguration()); + } + + private static ContainerConfiguration setupContainerConfiguration() { + final ClassWorld classWorld = + new ClassWorld("plexus.core", Thread.currentThread().getContextClassLoader()); + return new DefaultContainerConfiguration() + .setClassWorld(classWorld) + .setClassPathScanning(PlexusConstants.SCANNING_INDEX) + .setAutoWiring(true) + .setName("maven"); + } + private MojoUtils() { } } diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 309b98fe9..89df57072 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -72,37 +72,47 @@ public class BuildAttestationMojoTest { private static final String ARTIFACTS_DIR = "src/test/resources/mojos/detach-distributions/target/"; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - + private static PlexusContainer container; + private static JsonNode expectedStatement; @TempDir private static Path localRepositoryPath; - - private static PlexusContainer container; private static RepositorySystemSession repoSession; - private static JsonNode expectedStatement; - @BeforeAll - static void setup() throws Exception { - container = MojoUtils.setupContainer(); - repoSession = MojoUtils.createRepositorySystemSession(container, localRepositoryPath); - try (InputStream in = BuildAttestationMojoTest.class.getResourceAsStream("/attestations/commons-text-1.4.intoto.json")) { - expectedStatement = OBJECT_MAPPER.readTree(in); - } - } - - private static MavenExecutionRequest createMavenExecutionRequest() { - final DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); - request.setStartTime(Date.from(Instant.parse("2026-04-20T09:28:44Z"))); - request.setActiveProfiles(Collections.singletonList("release")); - request.setGoals(Collections.singletonList("deploy")); - final Properties userProperties = new Properties(); - userProperties.setProperty("gpg.keyname", "3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9"); - request.setUserProperties(userProperties); - return request; + private static void assertStatementContent(final JsonNode statement) { + assertJsonEquals(expectedStatement.get("subject"), statement.get("subject"), + JsonAssert.when(Option.IGNORING_ARRAY_ORDER)); + assertJsonEquals(expectedStatement.get("predicateType"), statement.get("predicateType")); + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/buildType"), + statement.at("/predicate/buildDefinition/buildType")); + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/externalParameters"), + statement.at("/predicate/buildDefinition/externalParameters"), + JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("jvm.args", "env")); + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/internalParameters"), + statement.at("/predicate/buildDefinition/internalParameters")); + // `[0].annotations` holds JVM system properties; + // Not all properties are available on all JDKs, so they are either null or strings, which json-unit treats as a structural mismatch. + // We will check them below + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/resolvedDependencies"), + statement.at("/predicate/buildDefinition/resolvedDependencies"), + JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("[0].annotations")); + final Set expectedJdkFields = fieldNames( + expectedStatement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); + final Set actualJdkFields = fieldNames( + statement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); + assertEquals(expectedJdkFields, actualJdkFields); + assertJsonEquals(expectedStatement.at("/predicate/runDetails"), + statement.at("/predicate/runDetails"), + whenIgnoringPaths("metadata.finishedOn")); } - @SuppressWarnings("deprecation") - private static MavenSession createMavenSession(final MavenExecutionRequest request, final MavenExecutionResult result) { - return new MavenSession(container, repoSession, request, result); + private static void configureBuildAttestationMojo(final BuildAttestationMojo mojo, final boolean signAttestation) { + mojo.setOutputDirectory(new File("target/attestations")); + mojo.setScmDirectory(new File(".")); + mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); + mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); + mojo.setAlgorithmNames("SHA-512,SHA-256,SHA-1,MD5"); + mojo.setSignAttestation(signAttestation); + mojo.setSigner(createMockSigner()); } private static BuildAttestationMojo createBuildAttestationMojo(final MavenProject project, final MavenProjectHelper projectHelper) @@ -113,14 +123,15 @@ private static BuildAttestationMojo createBuildAttestationMojo(final MavenProjec createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); } - private static void configureBuildAttestationMojo(final BuildAttestationMojo mojo, final boolean signAttestation) { - mojo.setOutputDirectory(new File("target/attestations")); - mojo.setScmDirectory(new File(".")); - mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); - mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); - mojo.setAlgorithmNames("SHA-512,SHA-256,SHA-1,MD5"); - mojo.setSignAttestation(signAttestation); - mojo.setSigner(createMockSigner()); + private static MavenExecutionRequest createMavenExecutionRequest() { + final DefaultMavenExecutionRequest request = new DefaultMavenExecutionRequest(); + request.setStartTime(Date.from(Instant.parse("2026-04-20T09:28:44Z"))); + request.setActiveProfiles(Collections.singletonList("release")); + request.setGoals(Collections.singletonList("deploy")); + final Properties userProperties = new Properties(); + userProperties.setProperty("gpg.keyname", "3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9"); + request.setUserProperties(userProperties); + return request; } private static MavenProject createMavenProject(final MavenProjectHelper projectHelper, final MavenRepositorySystem repoSystem) throws Exception { @@ -147,11 +158,20 @@ private static MavenProject createMavenProject(final MavenProjectHelper projectH return project; } + @SuppressWarnings("deprecation") + private static MavenSession createMavenSession(final MavenExecutionRequest request, final MavenExecutionResult result) { + return new MavenSession(container, repoSession, request, result); + } + private static AbstractGpgSigner createMockSigner() { return new AbstractGpgSigner() { @Override - public String signerName() { - return "mock"; + protected void generateSignatureForFile(final File file, final File signature) throws MojoExecutionException { + try { + Files.copy(Paths.get(ARTIFACTS_DIR + "commons-text-1.4.jar.asc"), signature.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to copy mock signature", e); + } } @Override @@ -160,16 +180,21 @@ public String getKeyInfo() { } @Override - protected void generateSignatureForFile(final File file, final File signature) throws MojoExecutionException { - try { - Files.copy(Paths.get(ARTIFACTS_DIR + "commons-text-1.4.jar.asc"), signature.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (final IOException e) { - throw new MojoExecutionException("Failed to copy mock signature", e); - } + public String signerName() { + return "mock"; } }; } + private static Set fieldNames(final JsonNode node) { + final Set names = new TreeSet<>(); + final Iterator it = node.fieldNames(); + while (it.hasNext()) { + names.add(it.next()); + } + return names; + } + private static Artifact getAttestation(final MavenProject project) { return project.getAttachedArtifacts().stream() .filter(a -> "intoto.jsonl".equals(a.getType())) @@ -177,40 +202,13 @@ private static Artifact getAttestation(final MavenProject project) { .orElseThrow(() -> new AssertionError("No intoto.jsonl artifact attached to project")); } - private static void assertStatementContent(final JsonNode statement) { - assertJsonEquals(expectedStatement.get("subject"), statement.get("subject"), - JsonAssert.when(Option.IGNORING_ARRAY_ORDER)); - assertJsonEquals(expectedStatement.get("predicateType"), statement.get("predicateType")); - assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/buildType"), - statement.at("/predicate/buildDefinition/buildType")); - assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/externalParameters"), - statement.at("/predicate/buildDefinition/externalParameters"), - JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("jvm.args", "env")); - assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/internalParameters"), - statement.at("/predicate/buildDefinition/internalParameters")); - // `[0].annotations` holds JVM system properties; - // Not all properties are available on all JDKs, so they are either null or strings, which json-unit treats as a structural mismatch. - // We will check them below - assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/resolvedDependencies"), - statement.at("/predicate/buildDefinition/resolvedDependencies"), - JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("[0].annotations")); - final Set expectedJdkFields = fieldNames( - expectedStatement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); - final Set actualJdkFields = fieldNames( - statement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); - assertEquals(expectedJdkFields, actualJdkFields); - assertJsonEquals(expectedStatement.at("/predicate/runDetails"), - statement.at("/predicate/runDetails"), - whenIgnoringPaths("metadata.finishedOn")); - } - - private static Set fieldNames(final JsonNode node) { - final Set names = new TreeSet<>(); - final Iterator it = node.fieldNames(); - while (it.hasNext()) { - names.add(it.next()); + @BeforeAll + static void setup() throws Exception { + container = MojoUtils.setupContainer(); + repoSession = MojoUtils.createRepositorySystemSession(container, localRepositoryPath); + try (InputStream in = BuildAttestationMojoTest.class.getResourceAsStream("/attestations/commons-text-1.4.intoto.json")) { + expectedStatement = OBJECT_MAPPER.readTree(in); } - return names; } @Test From b457710c7689987b165b58dd3a100af7e4a05186 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 14:33:30 +0200 Subject: [PATCH 27/51] fix: allow for setter chaining --- .../plugin/internal/ArtifactUtils.java | 6 +- .../plugin/internal/BuildDefinitions.java | 22 +- .../plugin/mojos/BuildAttestationMojo.java | 56 ++-- .../plugin/slsa/v1_2/BuildDefinition.java | 284 +++++++++--------- .../plugin/slsa/v1_2/BuildMetadata.java | 12 +- .../release/plugin/slsa/v1_2/Builder.java | 12 +- .../plugin/slsa/v1_2/DsseEnvelope.java | 12 +- .../release/plugin/slsa/v1_2/Provenance.java | 8 +- .../plugin/slsa/v1_2/ResourceDescriptor.java | 28 +- .../release/plugin/slsa/v1_2/RunDetails.java | 12 +- .../release/plugin/slsa/v1_2/Signature.java | 8 +- .../release/plugin/slsa/v1_2/Statement.java | 8 +- 12 files changed, 257 insertions(+), 211 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java index 17a3e8728..20cee902d 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/ArtifactUtils.java @@ -138,9 +138,9 @@ public static String getPackageUrl(final Artifact artifact) { * @throws MojoExecutionException If an I/O error occurs retrieving the artifact. */ public static ResourceDescriptor toResourceDescriptor(final Artifact artifact, final String algorithms) throws MojoExecutionException { - final ResourceDescriptor descriptor = new ResourceDescriptor(); - descriptor.setName(getFileName(artifact)); - descriptor.setUri(getPackageUrl(artifact)); + final ResourceDescriptor descriptor = new ResourceDescriptor() + .setName(getFileName(artifact)) + .setUri(getPackageUrl(artifact)); if (artifact.getFile() != null) { try { descriptor.setDigest(getChecksums(artifact, StringUtils.split(algorithms, ","))); diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index 996e57ffe..f74549a63 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -21,6 +21,7 @@ import java.lang.management.ManagementFactory; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -84,11 +85,6 @@ public static Map externalParameters(final MavenSession session) * @throws IOException if hashing the JDK directory fails */ public static ResourceDescriptor jvm(final Path javaHome) throws IOException { - final ResourceDescriptor descriptor = new ResourceDescriptor(); - descriptor.setName("JDK"); - final Map digest = new HashMap<>(); - digest.put("gitTree", GitUtils.gitTree(javaHome)); - descriptor.setDigest(digest); final String[] propertyNames = { "java.version", "java.version.date", "java.vendor", "java.vendor.url", "java.vendor.version", @@ -102,8 +98,10 @@ public static ResourceDescriptor jvm(final Path javaHome) throws IOException { for (final String prop : propertyNames) { annotations.put(prop.substring("java.".length()), System.getProperty(prop)); } - descriptor.setAnnotations(annotations); - return descriptor; + return new ResourceDescriptor() + .setName("JDK") + .setDigest(Collections.singletonMap("gitTree", GitUtils.gitTree(javaHome))) + .setAnnotations(annotations); } /** @@ -120,12 +118,10 @@ public static ResourceDescriptor jvm(final Path javaHome) throws IOException { * @throws IOException if hashing the Maven home directory fails */ public static ResourceDescriptor maven(final String version, final Path mavenHome, final ClassLoader coreClassLoader) throws IOException { - final ResourceDescriptor descriptor = new ResourceDescriptor(); - descriptor.setName("Maven"); - descriptor.setUri("pkg:maven/org.apache.maven/apache-maven@" + version); - final Map digest = new HashMap<>(); - digest.put("gitTree", GitUtils.gitTree(mavenHome)); - descriptor.setDigest(digest); + final ResourceDescriptor descriptor = new ResourceDescriptor() + .setName("Maven") + .setUri("pkg:maven/org.apache.maven/apache-maven@" + version) + .setDigest(Collections.singletonMap("gitTree", GitUtils.gitTree(mavenHome))); final Properties buildProps = new Properties(); try (InputStream in = coreClassLoader.getResourceAsStream("org/apache/maven/messages/build.properties")) { if (in != null) { diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 18b6dd20e..15fde7811 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -26,9 +26,7 @@ import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import javax.inject.Inject; @@ -224,24 +222,18 @@ private Path ensureOutputDirectory() throws MojoExecutionException { @Override public void execute() throws MojoFailureException, MojoExecutionException { - // Build definition - final BuildDefinition buildDefinition = new BuildDefinition(); - buildDefinition.setExternalParameters(BuildDefinitions.externalParameters(session)); - buildDefinition.setResolvedDependencies(getBuildDependencies()); - // Builder - final Builder builder = new Builder(); - // RunDetails - final RunDetails runDetails = new RunDetails(); - runDetails.setBuilder(builder); - runDetails.setMetadata(getBuildMetadata()); - // Provenance - final Provenance provenance = new Provenance(); - provenance.setBuildDefinition(buildDefinition); - provenance.setRunDetails(runDetails); - // Statement - final Statement statement = new Statement(); - statement.setSubject(getSubjects()); - statement.setPredicate(provenance); + final BuildDefinition buildDefinition = new BuildDefinition() + .setExternalParameters(BuildDefinitions.externalParameters(session)) + .setResolvedDependencies(getBuildDependencies()); + final RunDetails runDetails = new RunDetails() + .setBuilder(new Builder()) + .setMetadata(getBuildMetadata()); + final Provenance provenance = new Provenance() + .setBuildDefinition(buildDefinition) + .setRunDetails(runDetails); + final Statement statement = new Statement() + .setSubject(getSubjects()) + .setPredicate(provenance); final Path outputPath = ensureOutputDirectory(); final Path artifactPath = outputPath.resolve(ArtifactUtils.getFileName(project.getArtifact(), ATTESTATION_EXTENSION)); @@ -305,14 +297,9 @@ private List getProjectDependencies() throws MojoExecutionEx * @throws MojoExecutionException If the SCM revision cannot be retrieved. */ private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionException { - final ResourceDescriptor scmDescriptor = new ResourceDescriptor(); - final String scmUri = GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath()); - scmDescriptor.setUri(scmUri); - // Compute the revision - final Map digest = new HashMap<>(); - digest.put("gitCommit", getScmRevision()); - scmDescriptor.setDigest(digest); - return scmDescriptor; + return new ResourceDescriptor() + .setUri(GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath())) + .setDigest(Collections.singletonMap("gitCommit", getScmRevision())); } /** @@ -494,13 +481,12 @@ private void signAndWriteStatement(final Statement statement, final Path outputP final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); final byte[] sigBytes = DsseUtils.signFile(signer, paeFile); - final Signature sig = new Signature(); - sig.setKeyid(DsseUtils.getKeyId(sigBytes)); - sig.setSig(sigBytes); - - final DsseEnvelope envelope = new DsseEnvelope(); - envelope.setPayload(statementBytes); - envelope.setSignatures(Collections.singletonList(sig)); + final Signature sig = new Signature() + .setKeyid(DsseUtils.getKeyId(sigBytes)) + .setSig(sigBytes); + final DsseEnvelope envelope = new DsseEnvelope() + .setPayload(statementBytes) + .setSignatures(Collections.singletonList(sig)); getLog().info("Writing signed attestation envelope to: " + artifactPath); writeAndAttach(envelope, artifactPath); diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java index a856e2c9e..b24f60cbc 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java @@ -33,141 +33,155 @@ */ public class BuildDefinition { - /** URI indicating what type of build was performed. */ - @JsonProperty("buildType") - private String buildType = "https://commons.apache.org/builds/0.1.0"; - - /** Inputs passed to the build. */ - @JsonProperty("externalParameters") - private Map externalParameters = new HashMap<>(); - - /** Parameters set by the build platform. */ - @JsonProperty("internalParameters") - private Map internalParameters = new HashMap<>(); - - /** Artifacts the build depends on, specified by URI and digest. */ - @JsonProperty("resolvedDependencies") - private List resolvedDependencies; - - /** Creates a new BuildDefinition instance with the default build type. */ - public BuildDefinition() { - } - - /** - * Creates a new BuildDefinition with the given build type and external parameters. - * - * @param buildType URI indicating what type of build was performed - * @param externalParameters inputs passed to the build - */ - public BuildDefinition(String buildType, Map externalParameters) { - this.buildType = buildType; - this.externalParameters = externalParameters; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; + /** + * URI indicating what type of build was performed. + */ + @JsonProperty("buildType") + private String buildType = "https://commons.apache.org/builds/0.1.0"; + + /** + * Inputs passed to the build. + */ + @JsonProperty("externalParameters") + private Map externalParameters = new HashMap<>(); + + /** + * Parameters set by the build platform. + */ + @JsonProperty("internalParameters") + private Map internalParameters = new HashMap<>(); + + /** + * Artifacts the build depends on, specified by URI and digest. + */ + @JsonProperty("resolvedDependencies") + private List resolvedDependencies; + + /** + * Creates a new BuildDefinition instance with the default build type. + */ + public BuildDefinition() { } - if (o == null || getClass() != o.getClass()) { - return false; + + /** + * Creates a new BuildDefinition with the given build type and external parameters. + * + * @param buildType URI indicating what type of build was performed + * @param externalParameters inputs passed to the build + */ + public BuildDefinition(String buildType, Map externalParameters) { + this.buildType = buildType; + this.externalParameters = externalParameters; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BuildDefinition that = (BuildDefinition) o; + return Objects.equals(buildType, that.buildType) && Objects.equals(externalParameters, that.externalParameters) && Objects.equals(internalParameters, + that.internalParameters) && Objects.equals(resolvedDependencies, that.resolvedDependencies); + } + + /** + * Gets the URI indicating what type of build was performed. + * + *

Determines the meaning of {@code externalParameters} and {@code internalParameters}.

+ * + * @return the build type URI + */ + public String getBuildType() { + return buildType; + } + + /** + * Gets the inputs passed to the build, such as command-line arguments or environment variables. + * + * @return the external parameters map, or {@code null} if not set + */ + public Map getExternalParameters() { + return externalParameters; + } + + /** + * Gets the artifacts the build depends on, such as sources, dependencies, build tools, and base images, + * specified by URI and digest. + * + * @return the internal parameters map, or {@code null} if not set + */ + public Map getInternalParameters() { + return internalParameters; + } + + /** + * Gets the materials that influenced the build. + * + *

Considered incomplete unless resolved materials are present.

+ * + * @return the list of resolved dependencies, or {@code null} if not set + */ + public List getResolvedDependencies() { + return resolvedDependencies; + } + + @Override + public int hashCode() { + return Objects.hash(buildType, externalParameters, internalParameters, resolvedDependencies); + } + + /** + * Sets the URI indicating what type of build was performed. + * + * @param buildType the build type URI + * @return this for chaining + */ + public BuildDefinition setBuildType(String buildType) { + this.buildType = buildType; + return this; + } + + /** + * Sets the inputs passed to the build. + * + * @param externalParameters the external parameters map + * @return this for chaining + */ + public BuildDefinition setExternalParameters(Map externalParameters) { + this.externalParameters = externalParameters; + return this; + } + + /** + * Sets the artifacts the build depends on. + * + * @param internalParameters the internal parameters map + * @return this for chaining + */ + public BuildDefinition setInternalParameters(Map internalParameters) { + this.internalParameters = internalParameters; + return this; + } + + /** + * Sets the materials that influenced the build. + * + * @param resolvedDependencies the list of resolved dependencies + * @return this for chaining + */ + public BuildDefinition setResolvedDependencies(List resolvedDependencies) { + this.resolvedDependencies = resolvedDependencies; + return this; + } + + @Override + public String toString() { + return "BuildDefinition{buildType='" + buildType + '\'' + + ", externalParameters=" + externalParameters + + ", internalParameters=" + internalParameters + + ", resolvedDependencies=" + resolvedDependencies + '}'; } - BuildDefinition that = (BuildDefinition) o; - return Objects.equals(buildType, that.buildType) - && Objects.equals(externalParameters, that.externalParameters) - && Objects.equals(internalParameters, that.internalParameters) - && Objects.equals(resolvedDependencies, that.resolvedDependencies); - } - - /** - * Gets the URI indicating what type of build was performed. - * - *

Determines the meaning of {@code externalParameters} and {@code internalParameters}.

- * - * @return the build type URI - */ - public String getBuildType() { - return buildType; - } - - /** - * Gets the inputs passed to the build, such as command-line arguments or environment variables. - * - * @return the external parameters map, or {@code null} if not set - */ - public Map getExternalParameters() { - return externalParameters; - } - - /** - * Gets the artifacts the build depends on, such as sources, dependencies, build tools, and base images, - * specified by URI and digest. - * - * @return the internal parameters map, or {@code null} if not set - */ - public Map getInternalParameters() { - return internalParameters; - } - - /** - * Gets the materials that influenced the build. - * - *

Considered incomplete unless resolved materials are present.

- * - * @return the list of resolved dependencies, or {@code null} if not set - */ - public List getResolvedDependencies() { - return resolvedDependencies; - } - - @Override - public int hashCode() { - return Objects.hash(buildType, externalParameters, internalParameters, resolvedDependencies); - } - - /** - * Sets the URI indicating what type of build was performed. - * - * @param buildType the build type URI - */ - public void setBuildType(String buildType) { - this.buildType = buildType; - } - - /** - * Sets the inputs passed to the build. - * - * @param externalParameters the external parameters map - */ - public void setExternalParameters(Map externalParameters) { - this.externalParameters = externalParameters; - } - - /** - * Sets the artifacts the build depends on. - * - * @param internalParameters the internal parameters map - */ - public void setInternalParameters(Map internalParameters) { - this.internalParameters = internalParameters; - } - - /** - * Sets the materials that influenced the build. - * - * @param resolvedDependencies the list of resolved dependencies - */ - public void setResolvedDependencies(List resolvedDependencies) { - this.resolvedDependencies = resolvedDependencies; - } - - @Override - public String toString() { - return "BuildDefinition{" - + "buildType='" + buildType + '\'' - + ", externalParameters=" + externalParameters - + ", internalParameters=" + internalParameters - + ", resolvedDependencies=" + resolvedDependencies - + '}'; - } } diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java index 6f6316961..595e0f714 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildMetadata.java @@ -105,27 +105,33 @@ public int hashCode() { * Sets the timestamp of when the build completed. * * @param finishedOn the completion timestamp + * @return this for chaining */ - public void setFinishedOn(OffsetDateTime finishedOn) { + public BuildMetadata setFinishedOn(OffsetDateTime finishedOn) { this.finishedOn = finishedOn; + return this; } /** * Sets the identifier for this build invocation. * * @param invocationId the invocation identifier + * @return this for chaining */ - public void setInvocationId(String invocationId) { + public BuildMetadata setInvocationId(String invocationId) { this.invocationId = invocationId; + return this; } /** * Sets the timestamp of when the build started. * * @param startedOn the start timestamp + * @return this for chaining */ - public void setStartedOn(OffsetDateTime startedOn) { + public BuildMetadata setStartedOn(OffsetDateTime startedOn) { this.startedOn = startedOn; + return this; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java index 31102295a..135f9c999 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java @@ -93,27 +93,33 @@ public int hashCode() { * Sets the orchestrator dependencies that may affect provenance generation or security guarantees. * * @param builderDependencies the list of builder dependencies + * @return this for chaining */ - public void setBuilderDependencies(List builderDependencies) { + public Builder setBuilderDependencies(List builderDependencies) { this.builderDependencies = builderDependencies; + return this; } /** * Sets the identifier of the builder. * * @param id the builder identifier URI + * @return this for chaining */ - public void setId(String id) { + public Builder setId(String id) { this.id = id; + return this; } /** * Sets the map of build platform component names to their versions. * * @param version the version map + * @return this for chaining */ - public void setVersion(Map version) { + public Builder setVersion(Map version) { this.version = version; + return this; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java index 2c9bc601a..e68e5757c 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/DsseEnvelope.java @@ -93,27 +93,33 @@ public int hashCode() { * Sets the serialized payload bytes. * * @param payload the payload bytes + * @return this for chaining */ - public void setPayload(byte[] payload) { + public DsseEnvelope setPayload(byte[] payload) { this.payload = payload; + return this; } /** * Sets the payload type URI. * * @param payloadType the payload type URI + * @return this for chaining */ - public void setPayloadType(String payloadType) { + public DsseEnvelope setPayloadType(String payloadType) { this.payloadType = payloadType; + return this; } /** * Sets the list of signatures over the PAE-encoded payload. * * @param signatures the signatures + * @return this for chaining */ - public void setSignatures(List signatures) { + public DsseEnvelope setSignatures(List signatures) { this.signatures = signatures; + return this; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java index 1b7361e7d..6002dce7e 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Provenance.java @@ -99,18 +99,22 @@ public int hashCode() { * Sets the build definition describing all inputs that produced the build output. * * @param buildDefinition the build definition + * @return this for chaining */ - public void setBuildDefinition(BuildDefinition buildDefinition) { + public Provenance setBuildDefinition(BuildDefinition buildDefinition) { this.buildDefinition = buildDefinition; + return this; } /** * Sets the details about the invocation of the build tool and the environment in which it was run. * * @param runDetails the run details + * @return this for chaining */ - public void setRunDetails(RunDetails runDetails) { + public Provenance setRunDetails(RunDetails runDetails) { this.runDetails = runDetails; + return this; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java index 79210d33d..cb3510ed1 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/ResourceDescriptor.java @@ -155,63 +155,77 @@ public int hashCode() { * Sets additional key-value metadata about the resource. * * @param annotations the annotations map + * @return this for chaining */ - public void setAnnotations(Map annotations) { + public ResourceDescriptor setAnnotations(Map annotations) { this.annotations = annotations; + return this; } /** * Sets the raw contents of the resource. * * @param content the resource content + * @return this for chaining */ - public void setContent(byte[] content) { + public ResourceDescriptor setContent(byte[] content) { this.content = content; + return this; } /** * Sets the map of cryptographic digest algorithms to their hex-encoded values. * * @param digest the digest map + * @return this for chaining */ - public void setDigest(Map digest) { + public ResourceDescriptor setDigest(Map digest) { this.digest = digest; + return this; } /** * Sets the download URI for the resource. * * @param downloadLocation the download location URI + * @return this for chaining */ - public void setDownloadLocation(String downloadLocation) { + public ResourceDescriptor setDownloadLocation(String downloadLocation) { this.downloadLocation = downloadLocation; + return this; } /** * Sets the media type of the resource. * * @param mediaType the media type + * @return this for chaining */ - public void setMediaType(String mediaType) { + public ResourceDescriptor setMediaType(String mediaType) { this.mediaType = mediaType; + return this; } /** * Sets the name of the resource. * * @param name the resource name + * @return this for chaining */ - public void setName(String name) { + public ResourceDescriptor setName(String name) { this.name = name; + return this; } /** * Sets the URI identifying the resource. * * @param uri the resource URI + * @return this for chaining */ - public void setUri(String uri) { + public ResourceDescriptor setUri(String uri) { this.uri = uri; + return this; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java index 90aae318c..da14aefc4 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/RunDetails.java @@ -113,27 +113,33 @@ public int hashCode() { * Sets the builder that executed the invocation. * * @param builder the builder + * @return this for chaining */ - public void setBuilder(Builder builder) { + public RunDetails setBuilder(Builder builder) { this.builder = builder; + return this; } /** * Sets the artifacts produced as a side effect of the build that are not the primary output. * * @param byproducts the list of byproduct artifacts + * @return this for chaining */ - public void setByproducts(List byproducts) { + public RunDetails setByproducts(List byproducts) { this.byproducts = byproducts; + return this; } /** * Sets the metadata about the build invocation. * * @param metadata the build metadata + * @return this for chaining */ - public void setMetadata(BuildMetadata metadata) { + public RunDetails setMetadata(BuildMetadata metadata) { this.metadata = metadata; + return this; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java index d514608e4..77e769805 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Signature.java @@ -82,18 +82,22 @@ public int hashCode() { * Sets the key identifier hint. * * @param keyid the key identifier, or {@code null} to leave unset + * @return this for chaining */ - public void setKeyid(String keyid) { + public Signature setKeyid(String keyid) { this.keyid = keyid; + return this; } /** * Sets the raw signature bytes. * * @param sig the signature bytes + * @return this for chaining */ - public void setSig(byte[] sig) { + public Signature setSig(byte[] sig) { this.sig = sig; + return this; } @Override diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java index eefb90dc7..b44dfc03c 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java @@ -96,19 +96,23 @@ public int hashCode() { * Sets the provenance predicate and automatically assigns {@code predicateType} to the SLSA provenance v1 URI. * * @param predicate the provenance predicate + * @return this for chaining */ - public void setPredicate(Provenance predicate) { + public Statement setPredicate(Provenance predicate) { this.predicate = predicate; this.predicateType = Provenance.PREDICATE_TYPE; + return this; } /** * Sets the set of software artifacts that the attestation applies to. * * @param subject the list of subject artifacts + * @return this for chaining */ - public void setSubject(List subject) { + public Statement setSubject(List subject) { this.subject = subject; + return this; } @Override From ad63bc775e915471004b73dca60a97c91e09acb5 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 14:37:39 +0200 Subject: [PATCH 28/51] fix: use `jackson-bom` --- pom.xml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index a5001cd27..ba61b018b 100644 --- a/pom.xml +++ b/pom.xml @@ -115,8 +115,7 @@ true true - 2.21.1 - 2.21 + 2.21.2 2.0.17 @@ -128,6 +127,14 @@ pom import + + + com.fasterxml.jackson + jackson-bom + ${commons.jackson.version} + pom + import + @@ -212,17 +219,14 @@ com.fasterxml.jackson.core jackson-databind - ${commons.jackson.version} com.fasterxml.jackson.core jackson-annotations - ${commons.jackson.annotations.version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 - ${commons.jackson.version} runtime From 89d61d277a202380377f306d746bb0b80e26d7f4 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 14:40:51 +0200 Subject: [PATCH 29/51] fix: `internal` Javadoc --- .../apache/commons/release/plugin/internal/package-info.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/package-info.java b/src/main/java/org/apache/commons/release/plugin/internal/package-info.java index 9218ebff4..44c988052 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/package-info.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/package-info.java @@ -16,8 +16,8 @@ */ /** - * Internal utilities for the commons-release-plugin. + * Internal utilities * - *

Should not be referenced by external artifacts.

+ *

Should not be referenced by external artifacts. Their API can change at any moment

*/ package org.apache.commons.release.plugin.internal; From d64965bf39428e762e1ed13caa257a2ab45052ad Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 15:16:25 +0200 Subject: [PATCH 30/51] fix: improve documentation --- src/site/markdown/slsa/v0.1.0.md | 155 ++++++++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 11 deletions(-) diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index b9a569ef2..a5287f72d 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -6,8 +6,32 @@ "buildType": "https://commons.apache.org/proper/commons-release-plugin/slsa/v0.1.0" ``` -This is a [SLSA Build Provenance](https://slsa.dev/spec/v1.2/build-provenance) build type -that describes releases produced by Apache Commons PMC release managers running Maven on their own equipment. +This document defines a [SLSA v1.2 Build Provenance](https://slsa.dev/spec/v1.2/build-provenance) **build type** for +releases of Apache Commons components. + +Apache Commons releases are cut on a PMC release manager's workstation by invoking Maven against a checkout of the +project's Git repository. The `commons-release-plugin` captures the build inputs and emits the result as an in-toto +attestation covering every artifact attached to the project. + +Because the build runs on the release manager's own hardware rather than on a hosted build service, the provenance +corresponds to [SLSA Build Level 1](https://slsa.dev/spec/v1.2/levels): it is generated by the same process that +produces the artifacts and is signed with the release manager's OpenPGP key, but the build platform itself is not +separately attested. + +The OpenPGP keys used to sign past and present artifacts are available at: https://downloads.apache.org/commons/KEYS + +Attestations are published to Maven Central under the released artifact's coordinates, distinguished by an +`intoto.jsonl` type: + +```xml + + + org.apache.commons + ${artifactId} + intoto.jsonl + ${version} + +``` ## Build definition @@ -84,7 +108,7 @@ They are only present if the resource is accessible from Maven's Core Classloade |-------------------------|--------------------------------------------------------------| | `distributionId` | The ID of the Maven distribution. | | `distributionName` | The full name of the Maven distribution. | -| `distributionShortName` | The short name of the Mavendistribution. | +| `distributionShortName` | The short name of the Maven distribution. | | `buildNumber` | The Git commit hash from which this Maven release was built. | | `version` | The Maven version string. | @@ -115,14 +139,123 @@ It represents the commons-release-plugin acting as the build platform. ## Subjects -The attestation covers all artifacts attached to the Maven project at the time the `verify` phase runs: -the primary artifact (e.g. the JAR) and any attached artifacts (e.g. sources JAR, javadoc JAR, POM). - -| Field | Value | -|-----------------|------------------------------------------| -| `name` | Artifact filename. | -| `uri` | Package URL. | -| `digest.sha256` | SHA-256 hex digest of the artifact file. | +The [`subject`](https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md#fields) array +lists every artifact produces by the build. It has the following properties + +| Field | Value | +|----------|-------------------------------------------------------------------------------------------------------------------------------------| +| `name` | Artifact filename in the default Maven repository layout, e.g. `commons-text-1.4-sources.jar`. | +| `uri` | [Package URL](https://github.com/package-url/purl-spec) identifying the artifact in the `maven` namespace. | +| `digest` | Map of [in-toto digest names](https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md) to hex-encoded digest values. | + +By default, every subject carries `md5`, `sha1`, `sha256` and `sha512` digests. + +## Example + +The following is the bare attestation statement produced for the `commons-text` 1.4 release +(abridged: most subjects are elided, and the JDK annotations trimmed). The full fixture lives at +[`src/test/resources/attestations/commons-text-1.4.intoto.json`](https://github.com/apache/commons-release-plugin/blob/main/src/test/resources/attestations/commons-text-1.4.intoto.json) +in the plugin source tree. + +The statement shown below is wrapped in a [DSSE envelope](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md) +signed with the release manager's OpenPGP key, and the `.intoto.jsonl` file deployed to Maven Central +contains that envelope. + +```json5 +{ + "subject": [ + { + "name": "commons-text-1.4.jar", + "uri": "pkg:maven/commons-text/commons-text@1.4?type=jar", + "digest": { + "md5": "9cbe22bb0ce86c70779213dfb7f3eb5a", + "sha1": "c81f089b3542485d4d09b02aae822906e5d2f209", + "sha256": "ad2d2eacf15ab740c115294afc1192603d8342004a6d7d0ad35446f7dda8a134", + "sha512": "126302c5f6865733774eb41fecc10ba8d0bb5ba11d14b9562047429abeb13bf8cdcdbfdf5e7d7708e2a40f67f4265cbbce609164f57abcd676067a840aa48e6a" + } + }, + // … one entry per attached artifact (POM, sources, javadoc, tests, and distribution archives) … + { + "name": "commons-text-1.4-src.zip", + "uri": "pkg:maven/commons-text/commons-text@1.4?classifier=src&type=zip", + "digest": { + "md5": "fd65603e930f2b0805c809aa2deb1498", + "sha1": "ca1cc6fbb4e46b44f8bb09b70c9e3a2ae3c5fce8", + "sha256": "e4a6c992153faae4f7faff689b899073000364e376736b9746a5d0acb9d8b980", + "sha512": "79ca61ff7b287407428bbb6ae13c6d372dcd0665114c55cd5bc57978a6fa760305e32feabef62cfeb0c4181220a59406239f6cccaa9a25c68773eef0250cb3a9" + } + } + ], + "predicateType": "https://slsa.dev/provenance/v1", + "predicate": { + "buildDefinition": { + "buildType": "https://commons.apache.org/builds/0.1.0", + "externalParameters": { + "maven.goals": ["deploy"], + "maven.profiles": ["release"], + "maven.user.properties": { + "gpg.keyname": "3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9" + }, + "maven.cmdline": "deploy -Prelease -Dgpg.keyname=3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9", + "jvm.args": [ + "-Dfile.encoding=UTF-8", + "-Dsun.stdout.encoding=UTF-8", + "-Dsun.stderr.encoding=UTF-8" + ], + "env": { + "LANG": "pl_PL.UTF-8", + "TZ": "UTC" + } + }, + "internalParameters": {}, + "resolvedDependencies": [ + // JDK that ran the build + { + "name": "JDK", + "digest": { "gitTree": "bdb67e47c1b7df9c35ae045f29a348bb5bd32dc3" }, + "annotations": { + "vendor": "Eclipse Adoptium", + "vendor.version": "Temurin-25.0.2+10", + "version": "25.0.2", + "vm.name": "OpenJDK 64-Bit Server VM", + "vm.version": "25.0.2+10-LTS" + // … remaining java.* system properties elided … + } + }, + // Maven installation + { + "name": "Maven", + "uri": "pkg:maven/org.apache.maven/apache-maven@3.9.12", + "digest": { "gitTree": "3cdb4a67690dc18373f70ead98dc86567cc5ad67" }, + "annotations": { + "distributionId": "apache-maven", + "distributionName": "Apache Maven", + "distributionShortName": "Maven", + "buildNumber": "848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1", + "version": "3.9.12" + } + }, + // Source revision (branch or tag at release time) + { + "uri": "git+https://github.com/apache/commons-text.git@rel/commons-text-1.4", + "digest": { "gitCommit": "f519b3670795da3fb4f43b6af1f727eadf8e6800" } + } + ] + }, + "runDetails": { + "builder": { + "id": "https://commons.apache.org/builds/0.1.0", + "builderDependencies": [], + "version": {} + }, + "metadata": { + "startedOn": "2026-04-20T09:28:44Z", + "finishedOn": "2026-04-20T09:38:12Z" + } + } + } +} +``` ## Version history From 2cac4bd5ed23921f5915dc27387d4fdcab55b7ab Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 15:22:31 +0200 Subject: [PATCH 31/51] fix: sort Java properties --- .../plugin/internal/BuildDefinitions.java | 13 ++--- src/site/markdown/slsa/v0.1.0.md | 58 +++++++++++-------- .../attestations/commons-text-1.4.intoto.json | 20 +++---- 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index f74549a63..537e3ad4d 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.TreeMap; import org.apache.commons.release.plugin.slsa.v1_2.ResourceDescriptor; import org.apache.maven.execution.MavenExecutionRequest; @@ -86,15 +87,11 @@ public static Map externalParameters(final MavenSession session) */ public static ResourceDescriptor jvm(final Path javaHome) throws IOException { final String[] propertyNames = { - "java.version", "java.version.date", - "java.vendor", "java.vendor.url", "java.vendor.version", - "java.home", - "java.vm.specification.version", "java.vm.specification.vendor", "java.vm.specification.name", - "java.vm.version", "java.vm.vendor", "java.vm.name", - "java.specification.version", "java.specification.maintenance.version", - "java.specification.vendor", "java.specification.name", + "java.home", "java.specification.maintenance.version", "java.specification.name", "java.specification.vendor", "java.specification.version", + "java.vendor", "java.vendor.url", "java.vendor.version", "java.version", "java.version.date", "java.vm.name", "java.vm.specification.name", + "java.vm.specification.vendor", "java.vm.specification.version", "java.vm.vendor", "java.vm.version" }; - final Map annotations = new HashMap<>(); + final Map annotations = new TreeMap<>(); for (final String prop : propertyNames) { annotations.put(prop.substring("java.".length()), System.getProperty(prop)); } diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index a5287f72d..8dbee91d8 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -77,22 +77,22 @@ The following annotations are recorded from [ | Annotation key | System property | Description | |-------------------------------------|------------------------------------------|--------------------------------------------------------------------------| -| `version` | `java.version` | Java Runtime Environment version. | -| `version.date` | `java.version.date` | Java Runtime Environment version date, in ISO-8601 YYYY-MM-DD format. | +| `home` | `java.home` | Java installation directory. | +| `specification.maintenance.version` | `java.specification.maintenance.version` | Java Runtime Environment specification maintenance version _(optional)_. | +| `specification.name` | `java.specification.name` | Java Runtime Environment specification name. | +| `specification.vendor` | `java.specification.vendor` | Java Runtime Environment specification vendor. | +| `specification.version` | `java.specification.version` | Java Runtime Environment specification version. | | `vendor` | `java.vendor` | Java Runtime Environment vendor. | | `vendor.url` | `java.vendor.url` | Java vendor URL. | | `vendor.version` | `java.vendor.version` | Java vendor version _(optional)_. | -| `home` | `java.home` | Java installation directory. | -| `vm.specification.version` | `java.vm.specification.version` | Java Virtual Machine specification version. | -| `vm.specification.vendor` | `java.vm.specification.vendor` | Java Virtual Machine specification vendor. | +| `version` | `java.version` | Java Runtime Environment version. | +| `version.date` | `java.version.date` | Java Runtime Environment version date, in ISO-8601 YYYY-MM-DD format. | +| `vm.name` | `java.vm.name` | Java Virtual Machine implementation name. | | `vm.specification.name` | `java.vm.specification.name` | Java Virtual Machine specification name. | -| `vm.version` | `java.vm.version` | Java Virtual Machine implementation version. | +| `vm.specification.vendor` | `java.vm.specification.vendor` | Java Virtual Machine specification vendor. | +| `vm.specification.version` | `java.vm.specification.version` | Java Virtual Machine specification version. | | `vm.vendor` | `java.vm.vendor` | Java Virtual Machine implementation vendor. | -| `vm.name` | `java.vm.name` | Java Virtual Machine implementation name. | -| `specification.version` | `java.specification.version` | Java Runtime Environment specification version. | -| `specification.maintenance.version` | `java.specification.maintenance.version` | Java Runtime Environment specification maintenance version _(optional)_. | -| `specification.vendor` | `java.specification.vendor` | Java Runtime Environment specification vendor. | -| `specification.name` | `java.specification.name` | Java Runtime Environment specification name. | +| `vm.version` | `java.vm.version` | Java Virtual Machine implementation version. | #### Maven @@ -154,10 +154,12 @@ By default, every subject carries `md5`, `sha1`, `sha256` and `sha512` digests. The following is the bare attestation statement produced for the `commons-text` 1.4 release (abridged: most subjects are elided, and the JDK annotations trimmed). The full fixture lives at -[`src/test/resources/attestations/commons-text-1.4.intoto.json`](https://github.com/apache/commons-release-plugin/blob/main/src/test/resources/attestations/commons-text-1.4.intoto.json) +[ +`src/test/resources/attestations/commons-text-1.4.intoto.json`](https://github.com/apache/commons-release-plugin/blob/main/src/test/resources/attestations/commons-text-1.4.intoto.json) in the plugin source tree. -The statement shown below is wrapped in a [DSSE envelope](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md) +The statement shown below is wrapped in +a [DSSE envelope](https://github.com/secure-systems-lab/dsse/blob/master/envelope.md) signed with the release manager's OpenPGP key, and the `.intoto.jsonl` file deployed to Maven Central contains that envelope. @@ -191,8 +193,12 @@ contains that envelope. "buildDefinition": { "buildType": "https://commons.apache.org/builds/0.1.0", "externalParameters": { - "maven.goals": ["deploy"], - "maven.profiles": ["release"], + "maven.goals": [ + "deploy" + ], + "maven.profiles": [ + "release" + ], "maven.user.properties": { "gpg.keyname": "3C8D57E0A2B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9" }, @@ -212,13 +218,15 @@ contains that envelope. // JDK that ran the build { "name": "JDK", - "digest": { "gitTree": "bdb67e47c1b7df9c35ae045f29a348bb5bd32dc3" }, + "digest": { + "gitTree": "bdb67e47c1b7df9c35ae045f29a348bb5bd32dc3" + }, "annotations": { - "vendor": "Eclipse Adoptium", - "vendor.version": "Temurin-25.0.2+10", - "version": "25.0.2", - "vm.name": "OpenJDK 64-Bit Server VM", - "vm.version": "25.0.2+10-LTS" + "home": "/usr/lib/jvm/temurin-25-jdk-amd64", + "specification.maintenance.version": null, + "specification.name": "Java Platform API Specification", + "specification.vendor": "Oracle Corporation", + "specification.version": "25", // … remaining java.* system properties elided … } }, @@ -226,7 +234,9 @@ contains that envelope. { "name": "Maven", "uri": "pkg:maven/org.apache.maven/apache-maven@3.9.12", - "digest": { "gitTree": "3cdb4a67690dc18373f70ead98dc86567cc5ad67" }, + "digest": { + "gitTree": "3cdb4a67690dc18373f70ead98dc86567cc5ad67" + }, "annotations": { "distributionId": "apache-maven", "distributionName": "Apache Maven", @@ -238,7 +248,9 @@ contains that envelope. // Source revision (branch or tag at release time) { "uri": "git+https://github.com/apache/commons-text.git@rel/commons-text-1.4", - "digest": { "gitCommit": "f519b3670795da3fb4f43b6af1f727eadf8e6800" } + "digest": { + "gitCommit": "f519b3670795da3fb4f43b6af1f727eadf8e6800" + } } ] }, diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json index 37007233b..314f033fe 100644 --- a/src/test/resources/attestations/commons-text-1.4.intoto.json +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -133,22 +133,22 @@ "gitTree": "bdb67e47c1b7df9c35ae045f29a348bb5bd32dc3" }, "annotations": { - "vendor.version": "Temurin-25.0.2+10", + "home": "/usr/lib/jvm/temurin-25-jdk-amd64", + "specification.maintenance.version": null, "specification.name": "Java Platform API Specification", "specification.vendor": "Oracle Corporation", - "vm.version": "25.0.2+10-LTS", - "version": "25.0.2", - "version.date": "2026-01-20", - "vm.specification.vendor": "Oracle Corporation", - "home": "/usr/lib/jvm/temurin-25-jdk-amd64", + "specification.version": "25", "vendor": "Eclipse Adoptium", - "vm.vendor": "Eclipse Adoptium", "vendor.url": "https://adoptium.net/", - "specification.maintenance.version": null, - "vm.specification.version": "25", + "vendor.version": "Temurin-25.0.2+10", + "version": "25.0.2", + "version.date": "2026-01-20", "vm.name": "OpenJDK 64-Bit Server VM", "vm.specification.name": "Java Virtual Machine Specification", - "specification.version": "25" + "vm.specification.vendor": "Oracle Corporation", + "vm.specification.version": "25", + "vm.vendor": "Eclipse Adoptium", + "vm.version": "25.0.2+10-LTS" } }, { From 92c9d69636495e83e504b66cf31d433141afb1f8 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 15:31:26 +0200 Subject: [PATCH 32/51] fix: buildType URL --- .../commons/release/plugin/slsa/v1_2/BuildDefinition.java | 2 +- src/site/markdown/slsa/v0.1.0.md | 2 +- src/test/resources/attestations/commons-text-1.4.intoto.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java index b24f60cbc..43bcb94d5 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/BuildDefinition.java @@ -37,7 +37,7 @@ public class BuildDefinition { * URI indicating what type of build was performed. */ @JsonProperty("buildType") - private String buildType = "https://commons.apache.org/builds/0.1.0"; + private String buildType = "https://commons.apache.org/proper/commons-release-plugin/slsa/v0.1.0"; /** * Inputs passed to the build. diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index 8dbee91d8..a52924296 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -191,7 +191,7 @@ contains that envelope. "predicateType": "https://slsa.dev/provenance/v1", "predicate": { "buildDefinition": { - "buildType": "https://commons.apache.org/builds/0.1.0", + "buildType": "https://commons.apache.org/proper/commons-release-plugin/slsa/v0.1.0", "externalParameters": { "maven.goals": [ "deploy" diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json index 314f033fe..9063f5338 100644 --- a/src/test/resources/attestations/commons-text-1.4.intoto.json +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -104,7 +104,7 @@ "predicateType": "https://slsa.dev/provenance/v1", "predicate": { "buildDefinition": { - "buildType": "https://commons.apache.org/builds/0.1.0", + "buildType": "https://commons.apache.org/proper/commons-release-plugin/slsa/v0.1.0", "externalParameters": { "maven.profiles": [ "release" From 2cf85f176a99c0c7665d2cb4eed42f28a370a8b6 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 15:41:15 +0200 Subject: [PATCH 33/51] fix: builder.id resolves to commons-release-plugin version --- pom.xml | 19 ++++++++++++++++++ .../plugin/mojos/BuildAttestationMojo.java | 20 ++++++++++++++++++- .../release/plugin/slsa/v1_2/Builder.java | 2 +- src/site/markdown/slsa/v0.1.0.md | 9 ++++++--- .../mojos/BuildAttestationMojoTest.java | 11 ++++++++++ .../attestations/commons-text-1.4.intoto.json | 2 +- src/test/resources/plugin.properties | 18 +++++++++++++++++ 7 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 src/test/resources/plugin.properties diff --git a/pom.xml b/pom.xml index ba61b018b..5b914750a 100644 --- a/pom.xml +++ b/pom.xml @@ -320,6 +320,25 @@ + + + + src/test/resources + true + + attestations/** + plugin.properties + + + + src/test/resources + false + + attestations/** + plugin.properties + + + diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 15fde7811..54f20682c 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -52,6 +52,7 @@ import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; @@ -165,6 +166,12 @@ public class BuildAttestationMojo extends AbstractMojo { */ @Parameter(property = "commons.release.signAttestation", defaultValue = "true") private boolean signAttestation; + /** + * Descriptor of this plugin; used to fill in {@code builder.id} with the plugin's own + * Package URL so that consumers can resolve the exact code that produced the provenance. + */ + @Parameter(defaultValue = "${plugin}", readonly = true) + private PluginDescriptor pluginDescriptor; /** * GPG signer used for signing; lazily initialized from plugin parameters when {@code null}. */ @@ -225,8 +232,10 @@ public void execute() throws MojoFailureException, MojoExecutionException { final BuildDefinition buildDefinition = new BuildDefinition() .setExternalParameters(BuildDefinitions.externalParameters(session)) .setResolvedDependencies(getBuildDependencies()); + final String builderId = String.format("pkg:maven/%s/%s@%s", + pluginDescriptor.getGroupId(), pluginDescriptor.getArtifactId(), pluginDescriptor.getVersion()); final RunDetails runDetails = new RunDetails() - .setBuilder(new Builder()) + .setBuilder(new Builder().setId(builderId)) .setMetadata(getBuildMetadata()); final Provenance provenance = new Provenance() .setBuildDefinition(buildDefinition) @@ -451,6 +460,15 @@ void setSignAttestation(final boolean signAttestation) { this.signAttestation = signAttestation; } + /** + * Sets the plugin descriptor. Intended for testing. + * + * @param pluginDescriptor the plugin descriptor + */ + void setPluginDescriptor(final PluginDescriptor pluginDescriptor) { + this.pluginDescriptor = pluginDescriptor; + } + /** * Sets the GPG signer used for signing. Intended for testing. * diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java index 135f9c999..508d622a8 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Builder.java @@ -36,7 +36,7 @@ public class Builder { private List builderDependencies = new ArrayList<>(); /** Identifier URI of the builder. */ @JsonProperty("id") - private String id = "https://commons.apache.org/builds/0.1.0"; + private String id; /** Map of build platform component names to their versions. */ @JsonProperty("version") private Map version = new HashMap<>(); diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index a52924296..cff196464 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -134,8 +134,11 @@ These are appended after the build tool entries above. ### Builder -The `builder.id` is always `https://commons.apache.org/builds/0.1.0`. -It represents the commons-release-plugin acting as the build platform. +The `builder.id` is the [Package URL](https://github.com/package-url/purl-spec) of the +`commons-release-plugin` release that produced the attestation, e.g. +`pkg:maven/org.apache.commons/commons-release-plugin@1.9.3`. It identifies the trust boundary of +the "build platform": the exact plugin code that emitted the provenance. Verifiers can resolve the +PURL to the signed artifact on Maven Central to inspect the builder. ## Subjects @@ -256,7 +259,7 @@ contains that envelope. }, "runDetails": { "builder": { - "id": "https://commons.apache.org/builds/0.1.0", + "id": "pkg:maven/org.apache.commons/commons-release-plugin@1.9.3", "builderDependencies": [], "version": {} }, diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 89df57072..a079a85a4 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -55,6 +55,7 @@ import org.apache.maven.model.Model; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugins.gpg.AbstractGpgSigner; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; @@ -76,6 +77,7 @@ public class BuildAttestationMojoTest { private static JsonNode expectedStatement; @TempDir private static Path localRepositoryPath; + private static PluginDescriptor pluginDescriptor; private static RepositorySystemSession repoSession; private static void assertStatementContent(final JsonNode statement) { @@ -111,6 +113,7 @@ private static void configureBuildAttestationMojo(final BuildAttestationMojo moj mojo.setScmConnectionUrl("scm:git:https://github.com/apache/commons-text.git"); mojo.setMavenHome(new File(System.getProperty("maven.home", "."))); mojo.setAlgorithmNames("SHA-512,SHA-256,SHA-1,MD5"); + mojo.setPluginDescriptor(pluginDescriptor); mojo.setSignAttestation(signAttestation); mojo.setSigner(createMockSigner()); } @@ -209,6 +212,14 @@ static void setup() throws Exception { try (InputStream in = BuildAttestationMojoTest.class.getResourceAsStream("/attestations/commons-text-1.4.intoto.json")) { expectedStatement = OBJECT_MAPPER.readTree(in); } + final Properties pluginProps = new Properties(); + try (InputStream in = BuildAttestationMojoTest.class.getResourceAsStream("/plugin.properties")) { + pluginProps.load(in); + } + pluginDescriptor = new PluginDescriptor(); + pluginDescriptor.setGroupId(pluginProps.getProperty("plugin.groupId")); + pluginDescriptor.setArtifactId(pluginProps.getProperty("plugin.artifactId")); + pluginDescriptor.setVersion(pluginProps.getProperty("plugin.version")); } @Test diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json index 9063f5338..ca76d35a2 100644 --- a/src/test/resources/attestations/commons-text-1.4.intoto.json +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -175,7 +175,7 @@ }, "runDetails": { "builder": { - "id": "https://commons.apache.org/builds/0.1.0", + "id": "pkg:maven/${project.groupId}/${project.artifactId}@${project.version}", "builderDependencies": [], "version": {} }, diff --git a/src/test/resources/plugin.properties b/src/test/resources/plugin.properties new file mode 100644 index 000000000..7e61707a5 --- /dev/null +++ b/src/test/resources/plugin.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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 +# +# https://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. + +plugin.groupId=${project.groupId} +plugin.artifactId=${project.artifactId} +plugin.version=${project.version} From 9d8dc45cf01fcfdb0660de89725d3b7df310ad75 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 15:43:57 +0200 Subject: [PATCH 34/51] fix: latin abbreviations --- src/site/markdown/slsa/v0.1.0.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index cff196464..2914b3141 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -49,14 +49,14 @@ The provenance is recorded by the `build-attestation` goal of the External parameters capture everything supplied by the release manager at invocation time. All parameters are captured from the running Maven session. -| Parameter | Type | Description | -|-------------------------|----------|-------------------------------------------------------------------------| -| `maven.goals` | string[] | The list of Maven goals passed on the command line (e.g. `["deploy"]`). | -| `maven.profiles` | string[] | The list of active profiles passed via `-P` (e.g. `["release"]`). | -| `maven.user.properties` | object | User-defined properties passed via `-D` flags. | -| `maven.cmdline` | string | The reconstructed Maven command line. | -| `jvm.args` | string[] | JVM input arguments. | -| `env` | object | A filtered subset of environment variables: `TZ` and locale variables. | +| Parameter | Type | Description | +|-------------------------|----------|--------------------------------------------------------------------------------| +| `maven.goals` | string[] | The list of Maven goals passed on the command line (for example `["deploy"]`). | +| `maven.profiles` | string[] | The list of active profiles passed via `-P` (for example `["release"]`). | +| `maven.user.properties` | object | User-defined properties passed via `-D` flags. | +| `maven.cmdline` | string | The reconstructed Maven command line. | +| `jvm.args` | string[] | JVM input arguments. | +| `env` | object | A filtered subset of environment variables: `TZ` and locale variables. | ### Internal parameters @@ -124,18 +124,18 @@ format. One entry per resolved Maven dependency (compile + runtime scope), as declared in the project's POM. These are appended after the build tool entries above. -| Field | Value | -|-----------------|-----------------------------------------------------| -| `name` | Artifact filename, e.g. `commons-lang3-3.14.0.jar`. | -| `uri` | Package URL. | -| `digest.sha256` | SHA-256 hex digest of the artifact file on disk. | +| Field | Value | +|-----------------|------------------------------------------------------------| +| `name` | Artifact filename, for example `commons-lang3-3.14.0.jar`. | +| `uri` | Package URL. | +| `digest.sha256` | SHA-256 hex digest of the artifact file on disk. | ## Run details ### Builder The `builder.id` is the [Package URL](https://github.com/package-url/purl-spec) of the -`commons-release-plugin` release that produced the attestation, e.g. +`commons-release-plugin` release that produced the attestation, for example `pkg:maven/org.apache.commons/commons-release-plugin@1.9.3`. It identifies the trust boundary of the "build platform": the exact plugin code that emitted the provenance. Verifiers can resolve the PURL to the signed artifact on Maven Central to inspect the builder. @@ -147,7 +147,7 @@ lists every artifact produces by the build. It has the following properties | Field | Value | |----------|-------------------------------------------------------------------------------------------------------------------------------------| -| `name` | Artifact filename in the default Maven repository layout, e.g. `commons-text-1.4-sources.jar`. | +| `name` | Artifact filename in the default Maven repository layout, for example `commons-text-1.4-sources.jar`. | | `uri` | [Package URL](https://github.com/package-url/purl-spec) identifying the artifact in the `maven` namespace. | | `digest` | Map of [in-toto digest names](https://github.com/in-toto/attestation/blob/main/spec/v1/digest_set.md) to hex-encoded digest values. | From 8118ebbaa2a11f9d98319bba8a469a2c11b61819 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 20 Apr 2026 15:50:38 +0200 Subject: [PATCH 35/51] fix: clarify usage of JSON --- .../commons/release/plugin/mojos/BuildAttestationMojo.java | 7 ++++++- src/site/markdown/slsa/v0.1.0.md | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 54f20682c..03b7c9687 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -81,7 +81,12 @@ public class BuildAttestationMojo extends AbstractMojo { private static final String ATTESTATION_EXTENSION = "intoto.jsonl"; /** - * Shared Jackson object mapper for serializing attestation statements. + * Shared Jackson object mapper used to serialize SLSA statements and DSSE envelopes to JSON. + * + *

Each attestation is written as a single JSON value followed by a line separator, matching + * the JSON Lines format used by {@code .intoto.jsonl} + * files. The mapper is configured not to auto-close the output stream so the caller can append + * the trailing newline, and to emit ISO-8601 timestamps rather than numeric ones.

*/ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index 2914b3141..abc0f9f24 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -20,8 +20,9 @@ separately attested. The OpenPGP keys used to sign past and present artifacts are available at: https://downloads.apache.org/commons/KEYS -Attestations are published to Maven Central under the released artifact's coordinates, distinguished by an -`intoto.jsonl` type: +Attestations are serialized in the [JSON Lines](https://jsonlines.org/) format used across the +in-toto ecosystem, one JSON value per line, and published to Maven Central under the released +artifact's coordinates with an `intoto.jsonl` type: ```xml From 33b1c2e4217a8e6f5d0a63bdf9e43bd1848b36e7 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 16:00:44 +0200 Subject: [PATCH 36/51] fix: remove unused method --- .../release/plugin/internal/DsseUtils.java | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java index 3ad334ff2..76ad5314b 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -24,12 +24,9 @@ import java.nio.file.Path; import java.util.Locale; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.codec.binary.Hex; import org.apache.commons.io.IOUtils; import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope; -import org.apache.commons.release.plugin.slsa.v1_2.Statement; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; @@ -123,25 +120,6 @@ public static byte[] signFile(final AbstractGpgSigner signer, final Path path) t return signatureBytes; } - /** - * Serializes {@code statement} to JSON using the DSSE Pre-Authentication Encoding (PAE). - * - *
PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
- * - * @param statement the attestation statement to encode - * @param objectMapper the Jackson mapper used to serialize {@code statement} - * @param buildDirectory directory in which the PAE file is created - * @return path to the written PAE file - * @throws MojoExecutionException if serialization or I/O fails - */ - public static Path writePaeFile(final Statement statement, final ObjectMapper objectMapper, final Path buildDirectory) throws MojoExecutionException { - try { - return writePaeFile(objectMapper.writeValueAsBytes(statement), buildDirectory); - } catch (final JsonProcessingException e) { - throw new MojoExecutionException("Failed to serialize attestation statement", e); - } - } - /** * Writes serialized JSON to a file using the DSSE Pre-Authentication Encoding (PAE). * From 286e0218ba79740bf0307f15cf14e10d391cc783 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 16:01:00 +0200 Subject: [PATCH 37/51] fix: clean-up after signature --- .../release/plugin/mojos/BuildAttestationMojo.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 03b7c9687..6029b7230 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -501,8 +501,14 @@ private void signAndWriteStatement(final Statement statement, final Path outputP throw new MojoExecutionException("Failed to serialize attestation statement", e); } final AbstractGpgSigner signer = getSigner(); - final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); - final byte[] sigBytes = DsseUtils.signFile(signer, paeFile); + final byte[] sigBytes; + try { + final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); + sigBytes = DsseUtils.signFile(signer, paeFile); + Files.deleteIfExists(paeFile); + } catch (final IOException e) { + throw new MojoExecutionException("Failed to sign attestation statement", e); + } final Signature sig = new Signature() .setKeyid(DsseUtils.getKeyId(sigBytes)) From 9b008bc1777e6aad68bfdc662682c70cd1f7dd32 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 18:51:32 +0200 Subject: [PATCH 38/51] fix: add tests for `GitUtils` --- pom.xml | 22 +++- .../release/plugin/internal/GitUtils.java | 21 +++- .../release/plugin/internal/GitFixture.java | 99 ++++++++++++++++ .../release/plugin/internal/GitUtilsTest.java | 106 ++++++++++++++++++ 4 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/test/java/org/apache/commons/release/plugin/internal/GitFixture.java create mode 100644 src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java diff --git a/pom.xml b/pom.xml index 5b914750a..d4bf52085 100644 --- a/pom.xml +++ b/pom.xml @@ -268,6 +268,12 @@ 4.11.0 test
+ + org.apache.commons + commons-exec + 1.6.0 + test + org.apache.maven @@ -518,6 +524,20 @@ + + + + org.apache.maven.plugins + maven-resources-plugin + + + default-testResources + + false + + + +
@@ -631,7 +651,7 @@ com.github.spotbugs spotbugs-maven-plugin - + diff --git a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java index ecaa19f35..0b529af40 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -20,7 +20,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.security.MessageDigest; import org.apache.commons.codec.binary.Hex; @@ -32,7 +31,15 @@ */ public final class GitUtils { - /** The SCM URI prefix for Git repositories. */ + /** + * Prefix used in a {@code gitfile} to point to the Git directory. + * + *

See gitrepository-layout.

+ */ + private static final String GITDIR_PREFIX = "gitdir: "; + /** + * The SCM URI prefix for Git repositories. + */ private static final String SCM_GIT_PREFIX = "scm:git:"; /** @@ -52,8 +59,8 @@ private static Path findGitDir(final Path path) throws IOException { if (Files.isRegularFile(candidate)) { // git worktree: .git is a file containing "gitdir: /path/to/real/.git" final String content = new String(Files.readAllBytes(candidate), StandardCharsets.UTF_8).trim(); - if (content.startsWith("gitdir: ")) { - return Paths.get(content.substring("gitdir: ".length())); + if (content.startsWith(GITDIR_PREFIX)) { + return current.resolve(content.substring(GITDIR_PREFIX.length())); } } current = current.getParent(); @@ -98,7 +105,7 @@ public static String gitTree(final Path path) throws IOException { /** * Converts an SCM URI to a download URI suffixed with the current branch name. * - * @param scmUri A Maven SCM URI starting with {@code scm:git}. + * @param scmUri A Maven SCM URI starting with {@code scm:git}. * @param repositoryPath A path inside the Git repository. * @return A download URI of the form {@code git+@}. * @throws IOException If the current branch cannot be determined. @@ -111,7 +118,9 @@ public static String scmToDownloadUri(final String scmUri, final Path repository return "git+" + scmUri.substring(SCM_GIT_PREFIX.length()) + "@" + currentBranch; } - /** No instances. */ + /** + * No instances. + */ private GitUtils() { // no instantiation } diff --git a/src/test/java/org/apache/commons/release/plugin/internal/GitFixture.java b/src/test/java/org/apache/commons/release/plugin/internal/GitFixture.java new file mode 100644 index 000000000..8e534a8b0 --- /dev/null +++ b/src/test/java/org/apache/commons/release/plugin/internal/GitFixture.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.Executor; +import org.apache.commons.exec.PumpStreamHandler; +import org.apache.commons.exec.environment.EnvironmentUtils; +import org.apache.commons.io.output.NullOutputStream; + +/** + * Builds real Git fixtures on disk by invoking the {@code git} CLI via Commons Exec + */ +final class GitFixture { + + static final String REPO_BRANCH = "foo"; + static final String WORKTREE_BRANCH = "bar"; + static final String SUBDIR = "subdir"; + /** + * SHA-1 of the single commit produced by {@link #createRepoAndWorktree}; deterministic thanks to {@link #ENV}. + */ + static final String INITIAL_COMMIT_SHA = "a2782b3461d2ed2a81193da1139f65bf9d2befc2"; + + /** + * Process environment with fixed author/committer dates so commit SHAs are stable across runs. + */ + private static final Map ENV; + + static { + try { + final Map env = EnvironmentUtils.getProcEnvironment(); + env.put("GIT_AUTHOR_DATE", "2026-01-01T00:00:00Z"); + env.put("GIT_COMMITTER_DATE", "2026-01-01T00:00:00Z"); + ENV = env; + } catch (final IOException e) { + throw new ExceptionInInitializerError(e); + } + } + + /** + * Creates a Git repo for testing. + * + * @param repo Path to the repository to create + * @param worktree Path to a separate worktree to create + */ + static void createRepoAndWorktree(final Path repo, final Path worktree) throws IOException { + final Path subdir = repo.resolve(SUBDIR); + Files.createDirectories(subdir); + git(repo, "init", "-q", "."); + // Put HEAD on 'foo' before the first commit (portable to older git without --initial-branch). + git(repo, "symbolic-ref", "HEAD", "refs/heads/" + REPO_BRANCH); + git(repo, "config", "user.email", "test@example.invalid"); + git(repo, "config", "user.name", "Test"); + git(repo, "config", "commit.gpgsign", "false"); + final Path readme = subdir.resolve("README"); + Files.write(readme, "hi\n".getBytes(StandardCharsets.UTF_8)); + git(repo, "add", repo.relativize(readme).toString()); + git(repo, "commit", "-q", "-m", "init"); + git(repo, "branch", WORKTREE_BRANCH); + git(repo, "worktree", "add", "-q", repo.relativize(worktree).toString(), "bar"); + } + + /** + * Runs {@code git} with the given args; stdout is discarded, stderr is forwarded to {@link System#err}. + */ + static void git(final Path workingDir, final String... args) throws IOException { + final CommandLine cmd = new CommandLine("git"); + for (final String a : args) { + cmd.addArgument(a, false); + } + final Executor exec = DefaultExecutor.builder().setWorkingDirectory(workingDir.toFile()).get(); + exec.setStreamHandler(new PumpStreamHandler(NullOutputStream.INSTANCE, System.err)); + exec.execute(cmd, ENV); + } + + private GitFixture() { + } +} diff --git a/src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java new file mode 100644 index 000000000..caed45c4c --- /dev/null +++ b/src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * https://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 org.apache.commons.release.plugin.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +class GitUtilsTest { + + private static Path repo; + @TempDir + static Path tempDir; + private static Path worktree; + + @BeforeAll + static void setUp() throws IOException { + repo = tempDir.resolve("repo"); + worktree = tempDir.resolve("worktree"); + GitFixture.createRepoAndWorktree(repo, worktree); + } + + static Stream testGetCurrentBranch() { + return Stream.of(Arguments.of(repo, GitFixture.REPO_BRANCH), Arguments.of(repo.resolve(GitFixture.SUBDIR), GitFixture.REPO_BRANCH), + Arguments.of(worktree, GitFixture.WORKTREE_BRANCH), Arguments.of(worktree.resolve(GitFixture.SUBDIR), GitFixture.WORKTREE_BRANCH)); + } + + static Stream testScmToDownloadUri() { + return Stream.of( + Arguments.of("scm:git:https://gitbox.apache.org/repos/asf/commons-release-plugin.git", + repo, + "git+https://gitbox.apache.org/repos/asf/commons-release-plugin.git@" + GitFixture.REPO_BRANCH), + Arguments.of("scm:git:git@github.com:apache/commons-release-plugin.git", + repo, + "git+git@github.com:apache/commons-release-plugin.git@" + GitFixture.REPO_BRANCH), + Arguments.of("scm:git:ssh://git@github.com/apache/commons-release-plugin.git", + worktree, + "git+ssh://git@github.com/apache/commons-release-plugin.git@" + GitFixture.WORKTREE_BRANCH)); + } + + @ParameterizedTest + @MethodSource + void testGetCurrentBranch(final Path repo, final String expectedBranchName) throws Exception { + assertEquals(expectedBranchName, GitUtils.getCurrentBranch(repo)); + } + + @Test + void testGetCurrentBranchDetachedHead() throws IOException { + // Build a fresh repo so we don't mutate HEAD shared with the parameterized tests. + final Path detachedRepo = tempDir.resolve("detached-repo"); + final Path detachedWorktree = tempDir.resolve("detached-worktree"); + GitFixture.createRepoAndWorktree(detachedRepo, detachedWorktree); + GitFixture.git(detachedRepo, "checkout", "-q", "--detach", "HEAD"); + assertEquals(GitFixture.INITIAL_COMMIT_SHA, GitUtils.getCurrentBranch(detachedRepo)); + } + + @ParameterizedTest + @MethodSource + void testScmToDownloadUri(final String scmUri, final Path repositoryPath, final String expectedDownloadUri) throws IOException { + assertEquals(expectedDownloadUri, GitUtils.scmToDownloadUri(scmUri, repositoryPath)); + } + + @ParameterizedTest + @ValueSource(strings = { + "scm:svn:https://svn.apache.org/repos/asf/commons-release-plugin", + "scm:hg:https://example.com/repo", + "https://github.com/apache/commons-release-plugin.git", + "git:https://github.com/apache/commons-release-plugin.git", + "" + }) + void testScmToDownloadUriRejectsNonGit(final String scmUri) { + assertThrows(IllegalArgumentException.class, () -> GitUtils.scmToDownloadUri(scmUri, repo)); + } + + @Test + void throwsWhenNoGitDirectoryFound() throws IOException { + final Path plain = Files.createDirectories(tempDir.resolve("plain")); + assertThrows(IOException.class, () -> GitUtils.getCurrentBranch(plain)); + } +} From a654a983d7dad51401b518af4e314f6455503434 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 19:24:22 +0200 Subject: [PATCH 39/51] fix: filter possibly sensitive use properties --- .../plugin/internal/BuildDefinitions.java | 49 ++++++++++++++++++- .../plugin/internal/BuildDefinitionsTest.java | 18 +++++-- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index 537e3ad4d..ca46ec52d 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -21,9 +21,11 @@ import java.lang.management.ManagementFactory; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.TreeMap; @@ -37,8 +39,18 @@ */ public final class BuildDefinitions { + /** + * User-property names containing any of these substrings (case-insensitive) are omitted from attestations. + * + *

The Maven GPG plugin discourages passing credentials on the command line, but a stray {@code -Dgpg.passphrase=...} must not be captured in the + * attestation if someone does it anyway.

+ */ + private static final List SENSITIVE_KEYWORDS = + Arrays.asList("secret", "password", "passphrase", "token", "credential"); + /** * Reconstructs the Maven command line string from the given execution request. + * User properties whose name matches {@link #SENSITIVE_KEYWORDS} are omitted. * * @param request the Maven execution request * @return a string representation of the Maven command line @@ -49,10 +61,26 @@ static String commandLine(final MavenExecutionRequest request) { if (!profiles.isEmpty()) { args.add("-P" + profiles); } - request.getUserProperties().forEach((key, value) -> args.add("-D" + key + "=" + value)); + request.getUserProperties().forEach((key, value) -> { + final String k = key.toString(); + if (isNotSensitive(k)) { + args.add("-D" + k + "=" + value); + } + }); return String.join(" ", args); } + /** + * Checks if a property key is not sensitive. + * + * @param property A property key + * @return {@code true} if the property is not considered sensitive + */ + private static boolean isNotSensitive(final String property) { + final String lower = property.toLowerCase(Locale.ROOT); + return SENSITIVE_KEYWORDS.stream().noneMatch(lower::contains); + } + /** * Returns a map of external build parameters captured from the current JVM and Maven session. * @@ -65,7 +93,7 @@ public static Map externalParameters(final MavenSession session) final MavenExecutionRequest request = session.getRequest(); params.put("maven.goals", request.getGoals()); params.put("maven.profiles", request.getActiveProfiles()); - params.put("maven.user.properties", request.getUserProperties()); + params.put("maven.user.properties", getUserProperties(request)); params.put("maven.cmdline", commandLine(request)); final Map env = new HashMap<>(); params.put("env", env); @@ -78,6 +106,23 @@ public static Map externalParameters(final MavenSession session) return params; } + /** + * Returns a filtered map of user properties. + * + * @param request A Maven request + * @return A map of user properties. + */ + private static TreeMap getUserProperties(final MavenExecutionRequest request) { + final TreeMap properties = new TreeMap<>(); + request.getUserProperties().forEach((k, value) -> { + final String key = k.toString(); + if (isNotSensitive(key)) { + properties.put(key, value.toString()); + } + }); + return properties; + } + /** * Creates a {@link ResourceDescriptor} for the JDK used during the build. * diff --git a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java index 6c7a4d7a0..dac28a51c 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/BuildDefinitionsTest.java @@ -40,15 +40,23 @@ static Stream commandLineArguments() { Arguments.of("multiple goals", asList("clean", "verify"), emptyList(), new Properties(), "clean verify"), Arguments.of("single profile", singletonList("verify"), singletonList("release"), new Properties(), "verify -Prelease"), Arguments.of("multiple profiles", singletonList("verify"), asList("release", "sign"), new Properties(), "verify -Prelease,sign"), - Arguments.of("user property", singletonList("verify"), emptyList(), singletonProperties("foo", "bar"), "verify -Dfoo=bar"), - Arguments.of("goals, profile and property", singletonList("verify"), singletonList("release"), singletonProperties("foo", "bar"), - "verify -Prelease -Dfoo=bar") + Arguments.of("user property", singletonList("verify"), emptyList(), toProperties("foo", "bar"), "verify -Dfoo=bar"), + Arguments.of("goals, profile and property", singletonList("verify"), singletonList("release"), toProperties("foo", "bar"), + "verify -Prelease -Dfoo=bar"), + Arguments.of("redacts gpg.passphrase", singletonList("verify"), emptyList(), toProperties("gpg.passphrase", "s3cr3t"), "verify"), + Arguments.of("redacts passphrase case-insensitively", singletonList("verify"), emptyList(), toProperties("GPG_PASSPHRASE", "s3cr3t"), "verify"), + Arguments.of("redacts any *password*", singletonList("verify"), emptyList(), toProperties("my.db.password", "hunter2"), "verify"), + Arguments.of("redacts *token*", singletonList("verify"), emptyList(), toProperties("github.token", "ghp_xxx"), "verify"), + Arguments.of("keeps safe property, drops sensitive one", singletonList("verify"), emptyList(), + toProperties("foo", "bar", "gpg.passphrase", "s3cr3t"), "verify -Dfoo=bar") ); } - private static Properties singletonProperties(final String key, final String value) { + private static Properties toProperties(final String... keysAndValues) { final Properties p = new Properties(); - p.setProperty(key, value); + for (int i = 0; i < keysAndValues.length; i += 2) { + p.setProperty(keysAndValues[i], keysAndValues[i + 1]); + } return p; } From 21ad6733e7554ccc953686a6dbf5673f7e8b98d9 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 19:28:14 +0200 Subject: [PATCH 40/51] fix: lower-case key id --- .../org/apache/commons/release/plugin/internal/DsseUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java index 76ad5314b..dd7063112 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -88,7 +88,7 @@ public static String getKeyId(final byte[] sigBytes) throws MojoExecutionExcepti return Hex.encodeHexString(fp.getFingerprint()); } } - return Long.toHexString(sig.getKeyID()).toUpperCase(Locale.ROOT); + return Long.toHexString(sig.getKeyID()).toLowerCase(Locale.ROOT); } catch (final IOException e) { throw new MojoExecutionException("Failed to extract key ID from signature", e); } From 476ac4f510f4802d795d65403873b398fa00bed7 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 19:46:02 +0200 Subject: [PATCH 41/51] fix: `_type` property of attestation --- .../release/plugin/slsa/v1_2/Statement.java | 11 +++++++- src/site/markdown/slsa/v0.1.0.md | 1 + .../mojos/BuildAttestationMojoTest.java | 26 +++++++++---------- .../attestations/commons-text-1.4.intoto.json | 1 + 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java index b44dfc03c..1d779c63d 100644 --- a/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java +++ b/src/main/java/org/apache/commons/release/plugin/slsa/v1_2/Statement.java @@ -29,7 +29,6 @@ public class Statement { /** The in-toto statement schema URI. */ - @JsonProperty("_type") public static final String TYPE = "https://in-toto.io/Statement/v1"; /** The provenance predicate. */ @JsonProperty("predicate") @@ -55,6 +54,16 @@ public boolean equals(Object o) { statement.predicate); } + /** + * Type of JSON object. + * + * @return Always {@value TYPE} + */ + @JsonProperty("_type") + public String getType() { + return TYPE; + } + /** * Gets the provenance predicate. * diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index abc0f9f24..5e37371c7 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -169,6 +169,7 @@ contains that envelope. ```json5 { + "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "commons-text-1.4.jar", diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index a079a85a4..d266e67b5 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -20,7 +20,6 @@ import static net.javacrumbs.jsonunit.JsonAssert.assertJsonNodeAbsent; import static net.javacrumbs.jsonunit.JsonAssert.assertJsonNodePresent; import static net.javacrumbs.jsonunit.JsonAssert.assertJsonPartEquals; -import static net.javacrumbs.jsonunit.JsonAssert.whenIgnoringPaths; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; @@ -81,30 +80,29 @@ public class BuildAttestationMojoTest { private static RepositorySystemSession repoSession; private static void assertStatementContent(final JsonNode statement) { - assertJsonEquals(expectedStatement.get("subject"), statement.get("subject"), - JsonAssert.when(Option.IGNORING_ARRAY_ORDER)); - assertJsonEquals(expectedStatement.get("predicateType"), statement.get("predicateType")); - assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/buildType"), - statement.at("/predicate/buildDefinition/buildType")); - assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/externalParameters"), - statement.at("/predicate/buildDefinition/externalParameters"), - JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("jvm.args", "env")); - assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/internalParameters"), - statement.at("/predicate/buildDefinition/internalParameters")); + // Check all fields except `predicate` + assertJsonEquals(expectedStatement, statement, + JsonAssert.whenIgnoringPaths("predicate")); + + // Build definition except: + // - some external parameters we don't control + // - the resolved dependencies for which we check the structure, but ignore the values + assertJsonEquals(expectedStatement.at("/predicate/buildDefinition"), statement.at("/predicate/buildDefinition"), + JsonAssert.whenIgnoringPaths("externalParameters.jvm.args", "externalParameters.env", "resolvedDependencies", + "runDetails.metadata.finishedOn")); + // `[0].annotations` holds JVM system properties; // Not all properties are available on all JDKs, so they are either null or strings, which json-unit treats as a structural mismatch. // We will check them below assertJsonEquals(expectedStatement.at("/predicate/buildDefinition/resolvedDependencies"), statement.at("/predicate/buildDefinition/resolvedDependencies"), JsonAssert.when(Option.IGNORING_VALUES).whenIgnoringPaths("[0].annotations")); + final Set expectedJdkFields = fieldNames( expectedStatement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); final Set actualJdkFields = fieldNames( statement.at("/predicate/buildDefinition/resolvedDependencies/0/annotations")); assertEquals(expectedJdkFields, actualJdkFields); - assertJsonEquals(expectedStatement.at("/predicate/runDetails"), - statement.at("/predicate/runDetails"), - whenIgnoringPaths("metadata.finishedOn")); } private static void configureBuildAttestationMojo(final BuildAttestationMojo mojo, final boolean signAttestation) { diff --git a/src/test/resources/attestations/commons-text-1.4.intoto.json b/src/test/resources/attestations/commons-text-1.4.intoto.json index ca76d35a2..1ddc4f2ed 100644 --- a/src/test/resources/attestations/commons-text-1.4.intoto.json +++ b/src/test/resources/attestations/commons-text-1.4.intoto.json @@ -1,4 +1,5 @@ { + "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "commons-text-1.4.jar", From b7fbbf6a4c0e4c564c0441fd6ddd917cc7824aff Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 19:52:03 +0200 Subject: [PATCH 42/51] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/site/markdown/slsa/v0.1.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/site/markdown/slsa/v0.1.0.md b/src/site/markdown/slsa/v0.1.0.md index 5e37371c7..adb6f0668 100644 --- a/src/site/markdown/slsa/v0.1.0.md +++ b/src/site/markdown/slsa/v0.1.0.md @@ -144,7 +144,7 @@ PURL to the signed artifact on Maven Central to inspect the builder. ## Subjects The [`subject`](https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md#fields) array -lists every artifact produces by the build. It has the following properties +lists every artifact produced by the build. It has the following properties | Field | Value | |----------|-------------------------------------------------------------------------------------------------------------------------------------| From 1f9bb3016d4a68f4dcdedcf7e63ec6222aaff8ac Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 19:52:38 +0200 Subject: [PATCH 43/51] Apply suggestions from code review (2) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../commons/release/plugin/internal/BuildDefinitions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java index ca46ec52d..5935492bb 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/BuildDefinitions.java @@ -150,7 +150,7 @@ public static ResourceDescriptor jvm(final Path javaHome) throws IOException { * Creates a {@link ResourceDescriptor} for the Maven installation used during the build. * *

{@code build.properties} resides in a JAR inside {@code ${maven.home}/lib/}, which is loaded by Maven's Core Classloader. - * Plugin code runs in an isolated Plugin Classloader, which does see that resources. Therefore, we need to pass the classloader from a class from + * Plugin code runs in an isolated Plugin Classloader, which does not see those resources. Therefore, we need to pass the classloader from a class from * Maven Core, such as {@link org.apache.maven.rtinfo.RuntimeInformation}.

* * @param version Maven version string From cfd3b97268da4c00b91b6dd002ea67fdd9295d7a Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 20:36:01 +0200 Subject: [PATCH 44/51] fix: document attestation examples --- src/test/resources/attestations/README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/test/resources/attestations/README.md diff --git a/src/test/resources/attestations/README.md b/src/test/resources/attestations/README.md new file mode 100644 index 000000000..abfedaff1 --- /dev/null +++ b/src/test/resources/attestations/README.md @@ -0,0 +1,23 @@ + +# Attestation examples + +Golden files used as expected values by the tests. + +Real build attestations are single-line JSON Lines (`.intoto.jsonl`). The files here are pretty-printed JSON (`.intoto.json`) for readability. + +The tests compare structurally via `json-unit`, ignoring environment-dependent fields (JVM arguments, timestamps, `resolvedDependencies` digests), so formatting differences are not significant. \ No newline at end of file From 095d93fd4c060c9189d80bb1c860f04dbbddd207 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 21:24:01 +0200 Subject: [PATCH 45/51] fix: add documentation of the `build-attestation` goal --- .../plugin/mojos/BuildAttestationMojo.java | 20 +++- src/site/markdown/build-attestation.md | 105 ++++++++++++++++++ src/site/xdoc/index.xml | 4 + 3 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/site/markdown/build-attestation.md diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 6029b7230..4ebf0a921 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -70,9 +70,25 @@ import org.apache.maven.scm.repository.ScmRepository; /** - * This plugin generates an in-toto attestation for all the artifacts. + * Generates a SLSA v1.2 in-toto attestation covering all artifacts attached to the project. + * + *

The goal binds to the {@code post-integration-test} phase so the following constraints are + * satisfied:

+ *
    + *
  • it runs after {@code package}, so every build artifact is already attached to the project;
  • + *
  • it runs before {@code maven-gpg-plugin}'s {@code sign} goal (bound to {@code verify}), so + * the attestation file itself receives the detached {@code .asc} signature required by + * Maven Central;
  • + *
  • it runs before {@code detach-distributions} (also bound to {@code verify}), so the + * distribution archives ({@code tar.gz}, {@code zip}) are covered by the attestation before + * they are removed from the list of artifacts to deploy.
  • + *
+ * + *

Binding to an earlier lifecycle phase than {@code verify} is needed because Maven 3 cannot + * order executions of different plugins within the same phase in a way that satisfies all three + * constraints at once.

*/ -@Mojo(name = "build-attestation", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) +@Mojo(name = "build-attestation", defaultPhase = LifecyclePhase.POST_INTEGRATION_TEST, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) public class BuildAttestationMojo extends AbstractMojo { /** diff --git a/src/site/markdown/build-attestation.md b/src/site/markdown/build-attestation.md new file mode 100644 index 000000000..49b96a68c --- /dev/null +++ b/src/site/markdown/build-attestation.md @@ -0,0 +1,105 @@ + +# commons-release:build-attestation + +## Overview + +The `commons-release:build-attestation` goal produces a [SLSA](https://slsa.dev/) v1.2 provenance +statement in the [in-toto](https://in-toto.io/) format. The attestation lists every artifact +attached to the project as a subject, records the JDK, Maven installation, SCM source and +resolved dependencies used during the build, and writes the result to +`target/-.intoto.jsonl`. The envelope is signed with GPG by default and +attached to the project so that it is deployed alongside the other artifacts. + +The structure of the `predicate.buildDefinition.buildType` field is documented at +[SLSA build type v0.1.0](slsa/v0.1.0.html). + +## Phase ordering + +A Commons release relies on three goals running in a fixed order: + +1. `commons-release:build-attestation`, bound to `post-integration-test`. At this point every + build artifact, including the distribution archives, is already attached to the project. +2. `maven-gpg-plugin:sign`, bound to `verify`. It signs every attached artifact with a detached + `.asc`, including the `.intoto.jsonl` produced in step 1. Maven Central requires this for + every uploaded file. +3. `commons-release:detach-distributions`, bound to `verify`. It removes the `.tar.gz` and + `.zip` archives from the set of artifacts that will be uploaded to Nexus. + +Binding `build-attestation` to `post-integration-test` (rather than `verify`) puts it in an +earlier lifecycle phase than the other two goals, so Maven 3 is guaranteed to run it first, +regardless of the order in which plugins are declared in the POM. Within the `verify` phase, +`sign` must run before `detach-distributions`; this is controlled by declaring +`maven-gpg-plugin` before `commons-release-plugin` in the POM, since Maven executes plugins +within a single phase in the order they appear. + +If the distribution archives should not be covered by the attestation, override the default +phase binding and bind `build-attestation` to `verify` after `detach-distributions`. + +## Example configuration + +The snippet below wires the three goals in the recommended order. + +```xml + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + org.apache.commons + commons-release-plugin + + + + build-attestation + + build-attestation + + + + detach-distributions + verify + + detach-distributions + + + + + + +``` + +See the [goal parameters](build-attestation-mojo.html) for the full list of configurable +properties. diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml index ed9ce8dbc..36792674f 100644 --- a/src/site/xdoc/index.xml +++ b/src/site/xdoc/index.xml @@ -52,6 +52,10 @@ code readability):

    +
  • + commons-release:build-attestation generates a signed + SLSA v1.2 in-toto attestation covering all build artifacts and attaches it to the project. +
  • commons-release:detach-distributions - Remove tar.gz, tar.gz.asc, zip, and zip.asc From 9f69d2e762efba20a24dd3057c7b174aecfb3241 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 21:52:30 +0200 Subject: [PATCH 46/51] fix: remove dependency on ScmManager --- pom.xml | 6 - .../release/plugin/internal/GitUtils.java | 105 +++++++++++++++++- .../plugin/mojos/BuildAttestationMojo.java | 83 +------------- .../release/plugin/internal/GitUtilsTest.java | 33 ++++++ .../mojos/BuildAttestationMojoTest.java | 4 +- 5 files changed, 143 insertions(+), 88 deletions(-) diff --git a/pom.xml b/pom.xml index d4bf52085..368c9cfe0 100644 --- a/pom.xml +++ b/pom.xml @@ -180,12 +180,6 @@ ${maven-scm.version} compile - - org.apache.maven.scm - maven-scm-provider-gitexe - ${maven-scm.version} - runtime - org.apache.maven.scm maven-scm-provider-svnexe diff --git a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java index 0b529af40..3b7c7be91 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/GitUtils.java @@ -16,6 +16,7 @@ */ package org.apache.commons.release.plugin.internal; +import java.io.BufferedReader; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -37,6 +38,14 @@ public final class GitUtils { *

    See gitrepository-layout.

    */ private static final String GITDIR_PREFIX = "gitdir: "; + /** + * Maximum number of symbolic-ref hops before we give up (to avoid cycles). + */ + private static final int MAX_REF_DEPTH = 5; + /** + * Prefix used in {@code HEAD} and ref files to indicate a symbolic reference. + */ + private static final String REF_PREFIX = "ref: "; /** * The SCM URI prefix for Git repositories. */ @@ -79,14 +88,37 @@ private static Path findGitDir(final Path path) throws IOException { */ public static String getCurrentBranch(final Path repositoryPath) throws IOException { final Path gitDir = findGitDir(repositoryPath); - final String head = new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim(); + final String head = readHead(gitDir); if (head.startsWith("ref: refs/heads/")) { return head.substring("ref: refs/heads/".length()); } - // detached HEAD — return the commit SHA + // Detached HEAD: the file contains the commit SHA. return head; } + /** + * Gets the commit SHA pointed to by {@code HEAD}. + * + *

    Handles loose refs under {@code /refs/...}, packed refs in {@code /packed-refs}, + * symbolic indirection (a ref file that itself contains {@code ref: ...}), and detached HEAD.

    + * + * @param repositoryPath A path inside the Git repository. + * @return The hex-encoded commit SHA. + * @throws IOException If the {@code .git} directory cannot be found, the ref cannot be resolved, + * or the symbolic chain is deeper than {@value #MAX_REF_DEPTH}. + */ + public static String getHeadCommit(final Path repositoryPath) throws IOException { + final Path gitDir = findGitDir(repositoryPath); + String value = readHead(gitDir); + for (int i = 0; i < MAX_REF_DEPTH; i++) { + if (!value.startsWith(REF_PREFIX)) { + return value; + } + value = resolveRef(gitDir, value.substring(REF_PREFIX.length())); + } + throw new IOException("Symbolic ref chain exceeds " + MAX_REF_DEPTH + " hops in: " + gitDir); + } + /** * Returns the Git tree hash for the given directory. * @@ -102,6 +134,75 @@ public static String gitTree(final Path path) throws IOException { return Hex.encodeHexString(GitIdentifiers.treeId(digest, path)); } + /** + * Reads and trims the {@code HEAD} file of the given Git directory. + * + * @param gitDir The {@code .git} directory. + * @return The trimmed contents of {@code /HEAD}. + * @throws IOException If the file cannot be read. + */ + private static String readHead(final Path gitDir) throws IOException { + return new String(Files.readAllBytes(gitDir.resolve("HEAD")), StandardCharsets.UTF_8).trim(); + } + + /** + * Returns the directory that holds shared repository state (loose refs, {@code packed-refs}). + * In a linked worktree this is read from {@code /commondir}; otherwise it is + * {@code gitDir} itself. + * + * @param gitDir The {@code .git} directory. + * @return The shared-state directory. + * @throws IOException If {@code commondir} exists but cannot be read. + */ + private static Path resolveCommonDir(final Path gitDir) throws IOException { + final Path commonDir = gitDir.resolve("commondir"); + if (Files.isRegularFile(commonDir)) { + final String value = new String(Files.readAllBytes(commonDir), StandardCharsets.UTF_8).trim(); + return gitDir.resolve(value).normalize(); + } + return gitDir; + } + + /** + * Resolves a single ref (e.g. {@code refs/heads/foo}) to its stored value. + * + *

    The return value is either a commit SHA or another {@code ref: ...} line, which the caller continues to resolve.

    + * + *

    In a linked worktree, loose and packed refs are stored in the "common dir" (usually the + * main repository's {@code .git}), which is pointed to by {@code /commondir}.

    + * + * @param gitDir The {@code .git} directory. + * @param refPath The ref path relative to the common dir (e.g. {@code refs/heads/main}). + * @return Either a commit SHA or another {@code ref: ...} line to be resolved by the caller. + * @throws IOException If the ref is not found as a loose file or in {@code packed-refs}. + */ + private static String resolveRef(final Path gitDir, final String refPath) throws IOException { + final Path refsDir = resolveCommonDir(gitDir); + final Path refFile = refsDir.resolve(refPath); + if (Files.isRegularFile(refFile)) { + return new String(Files.readAllBytes(refFile), StandardCharsets.UTF_8).trim(); + } + final Path packed = refsDir.resolve("packed-refs"); + if (Files.isRegularFile(packed)) { + try (BufferedReader reader = Files.newBufferedReader(packed, StandardCharsets.UTF_8)) { + // packed-refs format: one ref per line as " ", with '#' header lines, + // blank lines, and "^" peeled-tag continuation lines that we skip. + // See https://git-scm.com/docs/gitrepository-layout + String line; + while ((line = reader.readLine()) != null) { + if (line.isEmpty() || line.charAt(0) == '#' || line.charAt(0) == '^') { + continue; + } + final int space = line.indexOf(' '); + if (space > 0 && refPath.equals(line.substring(space + 1))) { + return line.substring(0, space); + } + } + } + } + throw new IOException("Cannot resolve ref: " + refPath); + } + /** * Converts an SCM URI to a download URI suffixed with the current branch name. * diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 4ebf0a921..7e5ef10fb 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -61,13 +61,6 @@ import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.apache.maven.rtinfo.RuntimeInformation; -import org.apache.maven.scm.CommandParameters; -import org.apache.maven.scm.ScmException; -import org.apache.maven.scm.ScmFileSet; -import org.apache.maven.scm.command.info.InfoItem; -import org.apache.maven.scm.command.info.InfoScmResult; -import org.apache.maven.scm.manager.ScmManager; -import org.apache.maven.scm.repository.ScmRepository; /** * Generates a SLSA v1.2 in-toto attestation covering all artifacts attached to the project. @@ -174,10 +167,6 @@ public class BuildAttestationMojo extends AbstractMojo { */ @Parameter(property = "commons.release.scmDirectory", defaultValue = "${basedir}") private File scmDirectory; - /** - * SCM manager to detect the Git revision. - */ - private final ScmManager scmManager; /** * The current Maven session, used to resolve plugin dependencies. */ @@ -215,16 +204,14 @@ public class BuildAttestationMojo extends AbstractMojo { * Creates a new instance with the given dependencies. * * @param project A Maven project. - * @param scmManager A SCM manager. * @param runtimeInformation Maven runtime information. * @param session A Maven session. * @param mavenProjectHelper A helper to attach artifacts to the project. */ @Inject - public BuildAttestationMojo(final MavenProject project, final ScmManager scmManager, final RuntimeInformation runtimeInformation, + public BuildAttestationMojo(final MavenProject project, final RuntimeInformation runtimeInformation, final MavenSession session, final MavenProjectHelper mavenProjectHelper) { this.project = project; - this.scmManager = scmManager; this.runtimeInformation = runtimeInformation; this.session = session; this.mavenProjectHelper = mavenProjectHelper; @@ -323,13 +310,13 @@ private List getProjectDependencies() throws MojoExecutionEx * Gets a resource descriptor for the current SCM source, including the URI and Git commit digest. * * @return A resource descriptor for the SCM source. - * @throws IOException If the current branch cannot be determined. - * @throws MojoExecutionException If the SCM revision cannot be retrieved. + * @throws IOException If the current branch or the HEAD commit cannot be determined. */ - private ResourceDescriptor getScmDescriptor() throws IOException, MojoExecutionException { + private ResourceDescriptor getScmDescriptor() throws IOException { + final Path scmPath = scmDirectory.toPath(); return new ResourceDescriptor() - .setUri(GitUtils.scmToDownloadUri(scmConnectionUrl, scmDirectory.toPath())) - .setDigest(Collections.singletonMap("gitCommit", getScmRevision())); + .setUri(GitUtils.scmToDownloadUri(scmConnectionUrl, scmPath)) + .setDigest(Collections.singletonMap("gitCommit", GitUtils.getHeadCommit(scmPath))); } /** @@ -341,64 +328,6 @@ public File getScmDirectory() { return scmDirectory; } - /** - * Gets an SCM repository from the configured connection URL. - * - * @return The SCM repository. - * @throws MojoExecutionException If the SCM repository cannot be created. - */ - private ScmRepository getScmRepository() throws MojoExecutionException { - try { - return scmManager.makeScmRepository(scmConnectionUrl); - } catch (final ScmException e) { - throw new MojoExecutionException("Failed to create SCM repository", e); - } - } - - /** - * Gets the current SCM revision (commit hash) for the configured SCM directory. - * - * @return The current SCM revision string. - * @throws MojoExecutionException If the revision cannot be retrieved from SCM. - */ - private String getScmRevision() throws MojoExecutionException { - final ScmRepository scmRepository = getScmRepository(); - final CommandParameters commandParameters = new CommandParameters(); - try { - final InfoScmResult result = scmManager.getProviderByRepository(scmRepository).info(scmRepository.getProviderRepository(), - new ScmFileSet(scmDirectory), commandParameters); - - return getScmRevision(result); - } catch (final ScmException e) { - throw new MojoExecutionException("Failed to retrieve SCM revision", e); - } - } - - /** - * Extracts the revision string from an SCM info result. - * - * @param result The SCM info result. - * @return The revision string. - * @throws MojoExecutionException If the result is unsuccessful or contains no revision. - */ - private String getScmRevision(final InfoScmResult result) throws MojoExecutionException { - if (!result.isSuccess()) { - throw new MojoExecutionException("Failed to retrieve SCM revision: " + result.getProviderMessage()); - } - - if (result.getInfoItems() == null || result.getInfoItems().isEmpty()) { - throw new MojoExecutionException("No SCM revision information found for " + scmDirectory); - } - - final InfoItem item = result.getInfoItems().get(0); - - final String revision = item.getRevision(); - if (revision == null) { - throw new MojoExecutionException("Empty SCM revision returned for " + scmDirectory); - } - return revision; - } - /** * Gets the GPG signer, creating and preparing it from plugin parameters if not already set. * diff --git a/src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java b/src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java index caed45c4c..b6e35ae9f 100644 --- a/src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java +++ b/src/test/java/org/apache/commons/release/plugin/internal/GitUtilsTest.java @@ -51,6 +51,14 @@ static Stream testGetCurrentBranch() { Arguments.of(worktree, GitFixture.WORKTREE_BRANCH), Arguments.of(worktree.resolve(GitFixture.SUBDIR), GitFixture.WORKTREE_BRANCH)); } + static Stream testGetHeadCommit() { + return Stream.of( + Arguments.of(repo, GitFixture.INITIAL_COMMIT_SHA), + Arguments.of(repo.resolve(GitFixture.SUBDIR), GitFixture.INITIAL_COMMIT_SHA), + Arguments.of(worktree, GitFixture.INITIAL_COMMIT_SHA), + Arguments.of(worktree.resolve(GitFixture.SUBDIR), GitFixture.INITIAL_COMMIT_SHA)); + } + static Stream testScmToDownloadUri() { return Stream.of( Arguments.of("scm:git:https://gitbox.apache.org/repos/asf/commons-release-plugin.git", @@ -80,6 +88,31 @@ void testGetCurrentBranchDetachedHead() throws IOException { assertEquals(GitFixture.INITIAL_COMMIT_SHA, GitUtils.getCurrentBranch(detachedRepo)); } + @ParameterizedTest + @MethodSource + void testGetHeadCommit(final Path repositoryPath, final String expectedSha) throws IOException { + assertEquals(expectedSha, GitUtils.getHeadCommit(repositoryPath)); + } + + @Test + void testGetHeadCommitDetachedHead() throws IOException { + final Path detachedRepo = tempDir.resolve("detached-head-commit-repo"); + final Path detachedWorktree = tempDir.resolve("detached-head-commit-worktree"); + GitFixture.createRepoAndWorktree(detachedRepo, detachedWorktree); + GitFixture.git(detachedRepo, "checkout", "-q", "--detach", "HEAD"); + assertEquals(GitFixture.INITIAL_COMMIT_SHA, GitUtils.getHeadCommit(detachedRepo)); + } + + @Test + void testGetHeadCommitPackedRefs() throws IOException { + final Path packedRepo = tempDir.resolve("packed-repo"); + final Path packedWorktree = tempDir.resolve("packed-worktree"); + GitFixture.createRepoAndWorktree(packedRepo, packedWorktree); + // Move all loose refs (branches, tags) into .git/packed-refs and delete the loose files. + GitFixture.git(packedRepo, "pack-refs", "--all", "--prune"); + assertEquals(GitFixture.INITIAL_COMMIT_SHA, GitUtils.getHeadCommit(packedRepo)); + } + @ParameterizedTest @MethodSource void testScmToDownloadUri(final String scmUri, final Path repositoryPath, final String expectedDownloadUri) throws IOException { diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index d266e67b5..19f40be4d 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -59,7 +59,6 @@ import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.apache.maven.rtinfo.RuntimeInformation; -import org.apache.maven.scm.manager.ScmManager; import org.codehaus.plexus.PlexusContainer; import org.codehaus.plexus.component.repository.exception.ComponentLookupException; import org.eclipse.aether.RepositorySystemSession; @@ -118,9 +117,8 @@ private static void configureBuildAttestationMojo(final BuildAttestationMojo moj private static BuildAttestationMojo createBuildAttestationMojo(final MavenProject project, final MavenProjectHelper projectHelper) throws ComponentLookupException { - final ScmManager scmManager = container.lookup(ScmManager.class); final RuntimeInformation runtimeInfo = container.lookup(RuntimeInformation.class); - return new BuildAttestationMojo(project, scmManager, runtimeInfo, + return new BuildAttestationMojo(project, runtimeInfo, createMavenSession(createMavenExecutionRequest(), new DefaultMavenExecutionResult()), projectHelper); } From d74eb3eb58e0cd7bf08ab234f0173686679111b0 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 22:16:53 +0200 Subject: [PATCH 47/51] fix: use temporary files for signing --- .../release/plugin/internal/DsseUtils.java | 75 +++++++++++-------- .../plugin/mojos/BuildAttestationMojo.java | 10 +-- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java index dd7063112..22e49249d 100644 --- a/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java +++ b/src/main/java/org/apache/commons/release/plugin/internal/DsseUtils.java @@ -17,6 +17,7 @@ package org.apache.commons.release.plugin.internal; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -25,6 +26,7 @@ import java.util.Locale; import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.release.plugin.slsa.v1_2.DsseEnvelope; import org.apache.maven.plugin.MojoExecutionException; @@ -47,7 +49,7 @@ public final class DsseUtils { /** * Creates and prepares a {@link GpgSigner} from the given configuration. * - *

    The returned signer has {@link AbstractGpgSigner#prepare()} already called and is ready for use with {@link #signFile(AbstractGpgSigner, Path)}.

    + *

    The returned signer has {@link AbstractGpgSigner#prepare()} already called and is ready for use with {@link #signStatement}.

    * * @param executable path to the GPG executable, or {@code null} to use {@code gpg} from {@code PATH} * @param defaultKeyring whether to include the default GPG keyring @@ -95,57 +97,66 @@ public static String getKeyId(final byte[] sigBytes) throws MojoExecutionExcepti } /** - * Signs {@code paeFile} and returns the raw OpenPGP signature bytes. + * Signs a serialized DSSE payload and returns the raw OpenPGP signature bytes. * - *

    The signer must already have {@link AbstractGpgSigner#prepare()} called before this method is invoked.

    + *

    Creates a unique temporary {@code .pae} file inside {@code workDir}, containing the payload + * wrapped in the DSSE Pre-Authentication Encoding:

    * - * @param signer the configured, prepared signer - * @param path path to the file to sign + *
    PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
    + * + *

    then invokes {@code signer} on that file, reads back the un-armored PGP signature, and + * deletes both temporary files in a {@code finally} block. Cleanup is best-effort; a delete + * failure at the end of a successful sign only leaves a stray temporary file in {@code workDir} + * and does not fail the build.

    + * + *

    The signer must already have {@link AbstractGpgSigner#prepare()} called before this method + * is invoked.

    + * + * @param signer the configured, prepared signer + * @param statementBytes the already-serialized JSON statement bytes to sign + * @param workDir directory in which to create the intermediate PAE and signature files * @return raw binary PGP signature bytes - * @throws MojoExecutionException if signing or signature decoding fails + * @throws MojoExecutionException if encoding, signing, or signature decoding fails */ - public static byte[] signFile(final AbstractGpgSigner signer, final Path path) throws MojoExecutionException { - final Path signaturePath = signer.generateSignatureForArtifact(path.toFile()).toPath(); - final byte[] signatureBytes; - try (InputStream in = Files.newInputStream(signaturePath); ArmoredInputStream armoredIn = new ArmoredInputStream(in)) { - signatureBytes = IOUtils.toByteArray(armoredIn); - } catch (final IOException e) { - throw new MojoExecutionException("Failed to read signature file: " + signaturePath, e); - } + public static byte[] signStatement(final AbstractGpgSigner signer, final byte[] statementBytes, final Path workDir) + throws MojoExecutionException { + File paeFile = null; + File ascFile = null; try { - Files.delete(signaturePath); + paeFile = File.createTempFile("statement-", ".pae", workDir.toFile()); + FileUtils.writeByteArrayToFile(paeFile, paeEncode(statementBytes)); + ascFile = signer.generateSignatureForArtifact(paeFile); + try (InputStream in = Files.newInputStream(ascFile.toPath()); + ArmoredInputStream armoredIn = new ArmoredInputStream(in)) { + return IOUtils.toByteArray(armoredIn); + } } catch (final IOException e) { - throw new MojoExecutionException("Failed to delete signature file: " + signaturePath, e); + throw new MojoExecutionException("Failed to sign attestation statement", e); + } finally { + FileUtils.deleteQuietly(paeFile); + FileUtils.deleteQuietly(ascFile); } - return signatureBytes; } /** - * Writes serialized JSON to a file using the DSSE Pre-Authentication Encoding (PAE). - * - *
    PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body
    + * Encodes {@code statementBytes} using the DSSEv1 Pre-Authentication Encoding. * * @param statementBytes the already-serialized JSON statement bytes to encode - * @param buildDirectory directory in which the PAE file is created - * @return path to the written PAE file - * @throws MojoExecutionException if I/O fails + * @return the PAE-encoded bytes */ - public static Path writePaeFile(final byte[] statementBytes, final Path buildDirectory) throws MojoExecutionException { + private static byte[] paeEncode(final byte[] statementBytes) { + final byte[] payloadTypeBytes = DsseEnvelope.PAYLOAD_TYPE.getBytes(StandardCharsets.UTF_8); + final ByteArrayOutputStream pae = new ByteArrayOutputStream(); try { - final byte[] payloadTypeBytes = DsseEnvelope.PAYLOAD_TYPE.getBytes(StandardCharsets.UTF_8); - - final ByteArrayOutputStream pae = new ByteArrayOutputStream(); pae.write(("DSSEv1 " + payloadTypeBytes.length + " ").getBytes(StandardCharsets.UTF_8)); pae.write(payloadTypeBytes); pae.write((" " + statementBytes.length + " ").getBytes(StandardCharsets.UTF_8)); pae.write(statementBytes); - - final Path paeFile = buildDirectory.resolve("statement.pae"); - Files.write(paeFile, pae.toByteArray()); - return paeFile; } catch (final IOException e) { - throw new MojoExecutionException("Failed to write PAE file", e); + // ByteArrayOutputStream#write(byte[]) never throws; this branch is unreachable. + throw new IllegalStateException(e); } + return pae.toByteArray(); } /** diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 7e5ef10fb..04cd317b7 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -445,15 +445,7 @@ private void signAndWriteStatement(final Statement statement, final Path outputP } catch (final JsonProcessingException e) { throw new MojoExecutionException("Failed to serialize attestation statement", e); } - final AbstractGpgSigner signer = getSigner(); - final byte[] sigBytes; - try { - final Path paeFile = DsseUtils.writePaeFile(statementBytes, outputPath); - sigBytes = DsseUtils.signFile(signer, paeFile); - Files.deleteIfExists(paeFile); - } catch (final IOException e) { - throw new MojoExecutionException("Failed to sign attestation statement", e); - } + final byte[] sigBytes = DsseUtils.signStatement(getSigner(), statementBytes, outputPath); final Signature sig = new Signature() .setKeyid(DsseUtils.getKeyId(sigBytes)) From 8102f343dd25569d063ce33d2d9fa2ceb89ed3b5 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 22:22:17 +0200 Subject: [PATCH 48/51] fix: remove unnecessary `pom.xml` changes --- pom.xml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/pom.xml b/pom.xml index 368c9cfe0..404d3c42f 100644 --- a/pom.xml +++ b/pom.xml @@ -174,12 +174,6 @@ maven-scm-api ${maven-scm.version}
    - - org.apache.maven.scm - maven-scm-manager-plexus - ${maven-scm.version} - compile - org.apache.maven.scm maven-scm-provider-svnexe @@ -518,20 +512,6 @@ - - - - org.apache.maven.plugins - maven-resources-plugin - - - default-testResources - - false - - - - From 2dec90f2a051ca44496c765b9fca1c01f140c7b4 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 22:27:33 +0200 Subject: [PATCH 49/51] fix: clean-up public API --- .../release/plugin/mojos/BuildAttestationMojo.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index 04cd317b7..dbf5f1b6a 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -319,15 +319,6 @@ private ResourceDescriptor getScmDescriptor() throws IOException { .setDigest(Collections.singletonMap("gitCommit", GitUtils.getHeadCommit(scmPath))); } - /** - * Gets the SCM directory. - * - * @return The SCM directory. - */ - public File getScmDirectory() { - return scmDirectory; - } - /** * Gets the GPG signer, creating and preparing it from plugin parameters if not already set. * @@ -397,7 +388,7 @@ void setScmConnectionUrl(final String scmConnectionUrl) { * * @param scmDirectory The SCM directory. */ - public void setScmDirectory(final File scmDirectory) { + void setScmDirectory(final File scmDirectory) { this.scmDirectory = scmDirectory; } From 02477241d62d676e2b4bfa766d3cfb87e5e138bd Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 22:44:24 +0200 Subject: [PATCH 50/51] fix: document checksum choice --- .../release/plugin/mojos/BuildAttestationMojo.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java index dbf5f1b6a..39c398ff7 100644 --- a/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java +++ b/src/main/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojo.java @@ -107,6 +107,12 @@ public class BuildAttestationMojo extends AbstractMojo { /** * Checksum algorithms used in the generated attestation. + * + *

    The default list is:

    + *
      + *
    • {@code SHA-1} and {@code MD5} are easily available from Maven Central without downloading the artifact;
    • + *
    • {@code SHA-512} and {@code SHA-256} provide more security to the signed attestation, if the artifact is downloaded.
    • + *
    */ @Parameter(property = "commons.release.checksums.algorithms", defaultValue = "SHA-512,SHA-256,SHA-1,MD5") private String algorithmNames; @@ -189,7 +195,7 @@ public class BuildAttestationMojo extends AbstractMojo { /** * Whether to skip attaching the attestation artifact to the project. */ - @Parameter(property = "commons.release.skipAttach") + @Parameter(property = "commons.release.skipAttach", defaultValue = "false") private boolean skipAttach; /** * Whether to use gpg-agent for passphrase management. From d92845a9ab0fe6a8f5a43a6f5b6df1b2e9a96830 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Tue, 21 Apr 2026 22:48:34 +0200 Subject: [PATCH 51/51] fix: strengthen signature verification in test --- .../commons/release/plugin/mojos/BuildAttestationMojoTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java index 19f40be4d..fc507375a 100644 --- a/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java +++ b/src/test/java/org/apache/commons/release/plugin/mojos/BuildAttestationMojoTest.java @@ -248,6 +248,8 @@ void signingTest() throws Exception { assertJsonNodePresent(envelopeJson, "signatures[0]"); assertJsonNodeAbsent(envelopeJson, "signatures[1]"); assertJsonPartEquals("${json-unit.regex}.+", envelopeJson, "signatures[0].sig"); + // Issuer fingerprint extracted from the canned commons-text-1.4.jar.asc. + assertJsonPartEquals("b6e73d84ea4fcc47166087253faad2cd5ecbb314", envelopeJson, "signatures[0].keyid"); final DsseEnvelope envelope = OBJECT_MAPPER.readValue(envelopeJson.trim(), DsseEnvelope.class); final JsonNode statement = OBJECT_MAPPER.readTree(envelope.getPayload());