diff --git a/.github/workflows/publish_maven.yml b/.github/workflows/publish_maven.yml deleted file mode 100644 index cac8f2f10c..0000000000 --- a/.github/workflows/publish_maven.yml +++ /dev/null @@ -1,124 +0,0 @@ -# Copyright 2023 SLSA Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -permissions: - contents: read - -on: - workflow_call: - inputs: - provenance-download-name: - description: "The artifact name for the package provenance." - required: true - type: string - provenance-download-sha256: - description: "The sha256 of the package provenance artifact." - required: false - type: string - target-download-sha256: - description: "The sha256 of the target directory." - required: true - type: string - secrets: - maven-username: - description: "Maven username" - required: false - maven-password: - description: "Maven password" - required: false - gpg-key-pass: - description: "gpg-key-pass" - required: false - gpg-private-key: - description: "gpg-key-pass" - required: false - -jobs: - setup-java: - runs-on: ubuntu-latest - steps: - - name: Checkout the project repository - uses: slsa-framework/slsa-github-generator/.github/actions/secure-project-checkout@main - - name: Set up Java for publishing to Maven Central Repository - uses: actions/setup-java@cd89f46ac9d01407894225f350157564c9c7cee2 # v3.12.0 - env: - MAVEN_USERNAME: ${{ secrets.maven-username }} - MAVEN_PASSWORD: ${{ secrets.maven-password }} - GPG_KEY_PASS: ${{ secrets.gpg-key-pass }} - with: - java-version: '11' - distribution: 'temurin' - server-id: ossrh - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD - gpg-private-key: ${{ secrets.gpg-private-key }} - gpg-passphrase: GPG_KEY_PASS - - - name: Download the slsa attestation - uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-folder@main - with: - name: "${{ inputs.provenance-download-name }}" - path: slsa-attestations - sha256: "${{ inputs.provenance-download-sha256 }}" - - - name: Download the target dir - uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-folder@main - with: - name: target - path: ./ - sha256: "${{ inputs.target-download-sha256 }}" - - - name: Publish to the Maven Central Repository - shell: bash - env: - MAVEN_USERNAME: "${{ secrets.maven-username }}" - MAVEN_PASSWORD: "${{ secrets.maven-password }}" - GPG_KEY_PASS: "${{ secrets.gpg-key-pass }}" - SLSA_DIR: "${{ inputs.provenance-download-name }}" - PROVENANCE_FILES: "${{ inputs.provenance-download-name }}" - run: | - # Build and run custom plugin - cd plugin && mvn clean install && cd .. - # Re-indexing the secondary jar files for deploy - mvn javadoc:jar source:jar - # Retrieve project version - VERSION=$(mvn org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version -q -DforceStdout) - ARTIFACTID=$(mvn org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.artifactId -q -DforceStdout) - # Reset the environment variables add in the base provenance - FILES="slsa-attestations/${PROVENANCE_FILES}/${ARTIFACTID}-${VERSION}.jar.intoto.build.slsa" - TYPES=slsa - CLASSIFIERS=jar.intoto.build - # Find all necessary built jar files and attach them to the environment variable deploy - # shellcheck disable=SC2044 # We don't spawn a new sub shell since we are appending to global env vars - for name in $(find ./ -name "$ARTIFACTID-$VERSION-*.jar") - do - # shellcheck disable=SC1001 # shellcheck complains over \- but the line does what it should. - TARGET=$(echo "${name}" | rev | cut -d\- -f1 | rev) - FILES=$FILES,$name - TYPES=$TYPES,${TARGET##*.} - CLASSIFIERS=$CLASSIFIERS,${TARGET%.*} - done - - # Find all generated provenance files and attach them the the environment variable for deploy - # shellcheck disable=SC2044 # We don't spawn a new sub shell since we are appending to global env vars - for name in $(find ./ -name "$ARTIFACTID-$VERSION-*.jar.intoto.build.slsa") - do - # shellcheck disable=SC1001 # shellcheck complains over \- but the line does what it should. - TARGET=$(echo "${name}" | rev | cut -d\- -f1 | rev) - FILES=$FILES,$name - TYPES=$TYPES",slsa" - CLASSIFIERS=$CLASSIFIERS,${TARGET::-9} - done - # Sign and deploy the files to the ossrh remote repository - mvn validate jar:jar -Dfile=target/"${ARTIFACTID}"-"${VERSION}".jar -Durl=https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ -DrepositoryId=ossrh -Dfiles="${FILES}" -Dtypes="${TYPES}" -Dclassifiers="${CLASSIFIERS}" -DpomFile=pom.xml gpg:sign-and-deploy-file diff --git a/actions/maven/publish/README.md b/actions/maven/publish/README.md new file mode 100644 index 0000000000..42a35a23b7 --- /dev/null +++ b/actions/maven/publish/README.md @@ -0,0 +1,69 @@ +# Publishing SLSA3+ provenance to Maven Central + +This document explains how to publish SLSA3+ artifacts and provenance to Maven central. + +The publish Action is in its early stages and is likely to develop over time. Future breaking changes may occur. + +To get started with publishing artifacts to Maven Central Repository, see [this guide](https://maven.apache.org/repository/guide-central-repository-upload.html). + +Before you use this publish Action, you will need to configure your Github project with the correct secrets. See [this guide](https://docs.github.com/en/actions/publishing-packages/publishing-java-packages-with-maven) for more. + +## Using the Maven Publish action + +To use the Maven action you need to add the step in your release workflow that invokes it. + +Before using the Maven publish action, you should have a workflow that invokes the [Maven builder](https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/maven/README.md). It will look something like this: + +```yaml +name: Release Maven project +on: + - workflow_dispatch + +permissions: read-all + +jobs: + build: + permissions: + id-token: write + contents: read + actions: read + uses: slsa-framework/slsa-github-generator/.github/workflows/builder_maven_slsa3.yml@v1.7.0 + with: + rekor-log-public: true +``` + +To use the Publish action, you need to add another job: + +```yaml +publish: + runs-on: ubuntu-latest + needs: build + permissions: + id-token: write + contents: read + actions: read + steps: + - name: publish + id: publish + uses: slsa-framework/slsa-github-generator/actions/maven/publish@v1.7.0 + with: + provenance-download-name: "${{ needs.build.outputs.provenance-download-name }}" + provenance-download-sha256: "${{ needs.build.outputs.provenance-download-sha256 }}" + target-download-sha256: "${{ needs.build.outputs.target-download-sha256 }}" + maven-username: ${{ secrets.OSSRH_USERNAME }} + maven-password: ${{ secrets.OSSRH_PASSWORD }} + gpg-key-pass: ${{ secrets.GPG_PASSPHRASE }} + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} +``` + +Set the values of "maven-username", "maven-password", "gpg-key-pass" and " gpg-private-key" for your account. The parameters to `provenance-download-name`, `provenance-download-sha256` and `target-download-sha256` should not be changed. + +Once you trigger this workflow, your artifacts and provenance files will be added to a staging repository in Maven Central. You need to close the staging repository and then release: + +Closing the staging repository: + +![closing the staging repository](/actions/gradle/publish/images/gradle-publisher-staging-repository.png) + +Releasing: + +![releasing the Gradle artefacts](/actions/gradle/publish/images/gradle-publisher-release-closed-repository.png) diff --git a/actions/maven/publish/action.yml b/actions/maven/publish/action.yml new file mode 100644 index 0000000000..529be1bad3 --- /dev/null +++ b/actions/maven/publish/action.yml @@ -0,0 +1,115 @@ +# Copyright 2023 SLSA Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +inputs: + provenance-download-name: + description: "The artifact name for the package provenance." + required: true + type: string + provenance-download-sha256: + description: "The sha256 of the package provenance artifact." + required: true + type: string + target-download-sha256: + description: "The sha256 of the target directory." + required: true + type: string + maven-username: + description: "Maven username" + required: true + maven-password: + description: "Maven password" + required: true + gpg-key-pass: + description: "gpg-key-pass" + required: true + gpg-private-key: + description: "gpg-key-pass" + required: true +runs: + using: "composite" + steps: + - name: Checkout the project repository + uses: slsa-framework/slsa-github-generator/.github/actions/secure-project-checkout@main # needed because we run javadoc and sources. + - name: Set up Java for publishing to Maven Central Repository + uses: actions/setup-java@5ffc13f4174014e2d4d4572b3d74c3fa61aeb2c2 # v3 + env: + MAVEN_USERNAME: ${{ inputs.maven-username }} + MAVEN_PASSWORD: ${{ inputs.maven-password }} + GPG_KEY_PASS: ${{ inputs.gpg-key-pass }} + with: + java-version: '11' + distribution: 'temurin' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ inputs.gpg-private-key }} + gpg-passphrase: GPG_KEY_PASS + + - name: Download the slsa attestation + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-folder@main + with: + name: "${{ inputs.provenance-download-name }}" + path: slsa-attestations + sha256: "${{ inputs.provenance-download-sha256 }}" + + - name: Download the target dir + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-folder@main + with: + name: target + path: ./ + sha256: "${{ inputs.target-download-sha256 }}" + + - name: Checkout the framework repository + uses: slsa-framework/slsa-github-generator/.github/actions/secure-builder-checkout@main + with: + repository: slsa-framework/slsa-github-generator + ref: v1.8.0 + path: __BUILDER_CHECKOUT_DIR__ + + - name: Publish to the Maven Central Repository + shell: bash + env: + MAVEN_USERNAME: "${{ inputs.maven-username }}" + MAVEN_PASSWORD: "${{ inputs.maven-password }}" + GPG_KEY_PASS: "${{ inputs.gpg-key-pass }}" + SLSA_DIR: "${{ inputs.provenance-download-name }}" + PROVENANCE_FILES: "${{ inputs.provenance-download-name }}" + run: | + cd __BUILDER_CHECKOUT_DIR__/actions/maven/publish/slsa-hashing-plugin && mvn clean install && cd - + mvn javadoc:jar source:jar + # Retrieve project version + export version=$(mvn org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version -q -DforceStdout) + export artifactid=$(mvn org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.artifactId -q -DforceStdout) + # Reset the environment variables add in the base provenance + export files="slsa-attestations/${PROVENANCE_FILES}/${artifactid}-${version}.jar.build.slsa" + export types=slsa + export classifiers=jar.build + # Find all necessary built jar files and attach them to the environment variable deploy + while read -r name; do + target=$(echo "${name}" | rev | cut -d- -f1 | rev) + files=$files,$name + types=$types,${target##*.} + classifiers=$classifiers,${target%.*} + done <<<"$(find ./ -name "$artifactid-$version-*.jar")" + # Find all generated provenance files and attach them the the environment variable for deploy + while read -r name; do + target=$(echo "${name}" | rev | cut -d- -f1 | rev) + files=$files,$name + types=$types",slsa" + classifiers=$classifiers,${target::-9} + done <<<"$(find ./ -name "$artifactid-$version-*.jar.build.slsa")" + # Sign and deploy the files to the ossrh remote repository + mvn validate jar:jar -Dfile=target/"${artifactid}"-"${version}".jar -Durl=https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ -DrepositoryId=ossrh -Dfiles="${files}" -Dtypes="${types}" -Dclassifiers="${classifiers}" -DpomFile=pom.xml gpg:sign-and-deploy-file diff --git a/actions/maven/publish/slsa-hashing-plugin/pom.xml b/actions/maven/publish/slsa-hashing-plugin/pom.xml new file mode 100644 index 0000000000..dcf8b39851 --- /dev/null +++ b/actions/maven/publish/slsa-hashing-plugin/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + io.github.slsa-framework.slsa-github-generator + hash-maven-plugin + maven-plugin + 0.0.1 + + Jarfile Hashing Maven Mojo + http://maven.apache.org + + + 1.8 + 1.8 + + + + + org.apache.maven + maven-plugin-api + 3.6.3 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.6.0 + provided + + + org.apache.maven + maven-project + 2.2.1 + + + org.json + json + 20230227 + + + diff --git a/actions/maven/publish/slsa-hashing-plugin/src/main/java/io/github/slsa-framework/JarfileHashMojo.java b/actions/maven/publish/slsa-hashing-plugin/src/main/java/io/github/slsa-framework/JarfileHashMojo.java new file mode 100644 index 0000000000..036004a849 --- /dev/null +++ b/actions/maven/publish/slsa-hashing-plugin/src/main/java/io/github/slsa-framework/JarfileHashMojo.java @@ -0,0 +1,108 @@ +// Copyright 2023 SLSA Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package io.github.slsaframework; + +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.project.MavenProject; + +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.LinkedList; + +@Mojo(name = "hash-jarfile", defaultPhase = LifecyclePhase.PACKAGE) +public class JarfileHashMojo extends AbstractMojo { + private final String jsonBase = "{\"version\": 1, \"attestations\":[%ATTESTATIONS%]}"; + private final String attestationTemplate = "{\"name\": \"%NAME%\",\"subjects\":[{\"name\": \"%NAME%\",\"digest\":{\"sha256\":\"%HASH%\"}}]}"; + + @Parameter(defaultValue = "${project}", required = true, readonly = true) + private MavenProject project; + + @Parameter(property = "hash-jarfile.outputJsonPath", defaultValue = "") + private String outputJsonPath; + + @Parameter(property = "run.hash.jarfile", defaultValue = "false") + private Boolean runHashJarfile; + + + public void execute() throws MojoExecutionException, MojoFailureException { + if (!runHashJarfile) { + getLog().info("SLSA Hash Jarfile plugin is skipped."); + return; + } + + getLog().info("Start running SLSA Hash Jarfile plugin."); + + try { + StringBuilder attestations = new StringBuilder(); + + File targetDir = new File(project.getBasedir(), "target"); + File outputJson = this.getOutputJsonFile(targetDir.getAbsolutePath()); + for (File file : targetDir.listFiles()) { + String filePath = file.getAbsolutePath(); + if (!filePath.endsWith("original") && (filePath.endsWith(".pom") || filePath.endsWith(".jar"))) { + byte[] data = Files.readAllBytes(file.toPath()); + byte[] hash = MessageDigest.getInstance("SHA-256").digest(data); + String checksum = new BigInteger(1, hash).toString(16); + + String attestation = attestationTemplate.replaceAll("%NAME%", file.getName()); + attestation = attestation.replaceAll("%HASH%", checksum); + if (attestations.length() > 0) { + attestations.append(","); + } + attestations.append(attestation); + } + } + String json = jsonBase.replaceAll("%ATTESTATIONS%", attestations.toString()); + + Files.write(outputJson.toPath(), new JSONObject(json).toString(4).getBytes()); + } catch (IOException | NoSuchAlgorithmException e) { + throw new MojoFailureException("Fail to generate hash for the jar files", e); + } + + } + + private File getOutputJsonFile(String targetDir) { + try { + if (this.outputJsonPath != null && this.outputJsonPath.length() > 0) { + File outputJson = new File(outputJsonPath); + if (!outputJson.exists() || !outputJson.isFile()) { + outputJson.getParentFile().mkdirs(); + Files.createFile(outputJson.toPath()); + } + + if (Files.isWritable(outputJson.toPath())) { + return outputJson; + } + } + getLog().error("Could not generate the output json file."); + return new File(targetDir, "hash.json"); + } catch (IOException e) { + getLog().error("Could not generate the output json file."); + return new File(targetDir, "hash.json"); + } + } +} diff --git a/internal/builders/maven/README.md b/internal/builders/maven/README.md index d6606194a9..6c92522f60 100644 --- a/internal/builders/maven/README.md +++ b/internal/builders/maven/README.md @@ -19,6 +19,8 @@ workflow the "Maven builder" from now on. - [Limitations](#limitations) - [Generating Provenance](#generating-provenance) - [Getting Started](#getting-started) + - [Releasing to Maven Central](#releasing-to-maven-central) + - [Action requirements](#action-requirements) - [Private Repositories](#private-repositories) - [Verification](#verification) @@ -86,25 +88,32 @@ jobs: Now, when you invoke this workflow, the Maven builder will build both your artifacts and the provenance files for them. -You can also release artifacts to Maven Central by adding the following step to your workflow: - -```yaml - publish: - needs: build - uses: slsa-framework/slsa-github-generator/.github/workflows/publish_maven.yml@v1.7.0 - with: - provenance-download-name: "${{ needs.build.outputs.provenance-download-name }}" - provenance-download-sha256: "${{ needs.build.outputs.provenance-download-sha256 }}" - target-download-sha256: "${{ needs.build.outputs.target-download-sha256 }}" - secrets: - maven-username: ${{ secrets.OSSRH_USERNAME }} - maven-password: ${{ secrets.OSSRH_PASSWORD }} - gpg-key-pass: ${{ secrets.GPG_PASSPHRASE }} - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} +### Releasing to Maven Central + +You can also release artifacts to Maven Central with [the slsa-github-generator Maven publish action](https://github.com/slsa-framework/slsa-github-generator/blob/main/actions/maven/publish/README.md). + +#### Action requirements + +Besides adding the above workflow to your CI pipeline, you also need to add the following plugin to your `pom.xml`: + +```xml + + io.github.slsa-framework.slsa-github-generator + hash-maven-plugin + 0.0.1 + + + + hash-jarfile + + + + + ${SLSA_OUTPUTS_ARTIFACTS_FILE} + + ``` -Now your workflow will build your artifacts and publish them to a staging repository in Maven Central. - ### Private Repositories The builder records all provenance signatures in the [Rekor](https://github.com/sigstore/rekor) public transparency log. This record includes the repository name. To acknowledge you're aware that your repository name will be public, set the flag `rekor-log-public: true` when calling the builder: diff --git a/internal/builders/maven/action.yml b/internal/builders/maven/action.yml index 0341de7353..70b814ebc4 100644 --- a/internal/builders/maven/action.yml +++ b/internal/builders/maven/action.yml @@ -58,11 +58,22 @@ runs: with: distribution: temurin java-version: ${{ fromJson(inputs.slsa-workflow-inputs).jdk-version }} + - name: Checkout the tool repository + uses: slsa-framework/slsa-github-generator/.github/actions/secure-builder-checkout@main + with: + repository: slsa-framework/slsa-github-generator + ref: main + path: __BUILDER_CHECKOUT_DIR__ - name: Run mvn package shell: bash env: SLSA_OUTPUTS_ARTIFACTS_FILE: ${{ inputs.slsa-layout-file }} - run: cd plugin && mvn clean install && cd .. && mvn package + run: | + mv ./__BUILDER_CHECKOUT_DIR__ ../__BUILDER_CHECKOUT_DIR__ \ + && cd ../__BUILDER_CHECKOUT_DIR__/actions/maven/publish/slsa-hashing-plugin \ + && mvn clean install \ + && cd - \ + && mvn package -Drun.hash.jarfile=true - name: Upload target id: upload-target uses: slsa-framework/slsa-github-generator/.github/actions/secure-upload-folder@main