diff --git a/.github/workflows/generator_generic_slsa3_alt.yml b/.github/workflows/generator_generic_slsa3_alt.yml new file mode 100644 index 0000000000..1d17545522 --- /dev/null +++ b/.github/workflows/generator_generic_slsa3_alt.yml @@ -0,0 +1,319 @@ +# Copyright 2022 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. + +name: SLSA provenance generator alt + +permissions: + contents: read + +env: + SLSA_LAYOUT_FILENAME: "slsa-layout.json" + SUBJECTS_FILENAME: "subjects.sha256sum.base64" + SLSA_PREDICATE_FILE: "predicate.json" + ATTESTATIONS_FOLDER: "." + +defaults: + run: + shell: bash + +on: + workflow_call: + inputs: + base64-subjects: + description: "Artifacts for which to generate provenance, formatted the same as the output of sha256sum (SHA256 NAME\\n[...]) and base64 encoded." + required: false + type: string + base64-subjects-as-file: + description: > + The file 'handle' representing the filename containing the artifacts for which to generate provenance, formatted the same as the output of sha256sum (SHA256 NAME\\n[...]) and base64 encoded. 'actions/generator/generic/create-base64-subjects-from-file'. + The handle must be created using Action 'slsa-framework/slsa-github-generator/actions/generator/generic/create-base64-subjects-from-file'. + required: false + type: string + upload-assets: + description: > + If true, provenance is uploaded to a GitHub release for new tags. + When the workflow does not run on a new tag, such as on a workflow_dispatch, + the `upload-tag-name` argument must be provided as well. + required: false + type: boolean + default: false + upload-tag-name: + description: > + If non-empty and `upload-assets` is set to true, the provenance is uploaded to the GitHub + release identified by the tag name. If a workflow is run on a new tag and `upload-tag-name` + is non-empty, the new tag is ignored and the value of `upload-tag-name` is used instead to upload + the assets. + type: string + default: "" + provenance-name: + description: The artifact name of the signed provenance. The file must have the build.slsa extension. Defaults to attestations.build.slsa. + required: false + type: string + compile-generator: + description: "Build the generator from source. This increases build time by ~2m." + required: false + type: boolean + default: false + private-repository: + description: "If true, private repositories can post to the public transparency log." + required: false + type: boolean + default: false + continue-on-error: + description: "Prevents a workflow run from failing when a job fails. Set to 'true' to allow a workflow run to pass when a job fails." + required: false + type: boolean + default: false + draft-release: + description: > + Boolean identifying the release as a draft. If 'true' then the + created release is marked as a draft. If other non-empty value + then it is not marked as a draft. + + The default is to not modify the draft setting for existing + releases, and false for new releases. + required: false + type: string + default: "" + outputs: + release-id: + description: > + The name of the release where provenance was uploaded. + + Note: This value is non-empty only when a release asset is uploaded, according to + the values of `upload-assets` and `upload-tag-name`. + value: ${{ jobs.upload-assets.outputs.release-id }} + provenance-name: + description: "The artifact name of the signed provenance. (A file with the intoto.jsonl extension)." + value: ${{ jobs.generator.outputs.provenance-name }} + # Note: we use this output because there is no buildt-in `outcome` and `result` is always `success` + # if `continue-on-error` is set to `true`. + outcome: + description: > + The outcome status of the run ('success' or 'failure'). + + Note: this is only set when `continue-on-error` is `true`. + value: ${{ jobs.final.outputs.outcome }} + +jobs: + # detect-env detects the reusable workflow's repository and ref for use later + # in the workflow. + detect-env: + outputs: + outcome: ${{ steps.final.outputs.outcome }} + repository: ${{ steps.detect.outputs.repository }} + ref: ${{ steps.detect.outputs.ref }} + runs-on: ubuntu-latest + permissions: + id-token: write # Needed to detect the current reusable repository and ref. + steps: + - name: Detect the generator ref + id: detect + continue-on-error: true + uses: slsa-framework/slsa-github-generator/.github/actions/detect-workflow-js@main + + - name: Final outcome + id: final + env: + SUCCESS: ${{ steps.detect.outcome != 'failure' }} + run: | + set -euo pipefail + echo "outcome=$([ "$SUCCESS" == "true" ] && echo "success" || echo "failure")" >> "$GITHUB_OUTPUT" + + # generator builds the generator binary and runs it to generate SLSA + # provenance. + # + # If `compile-generator` is true then the generator is compiled + # from source at the ref detected by `detect-env`. + # + # If `compile-generator` is false, then the generator binary is downloaded + # with the release at the ref detected by `detect-env`. This must be a tag + # reference. + generator: + outputs: + outcome: ${{ steps.final.outputs.outcome }} + provenance-sha256: ${{ steps.upload-prov.outputs.sha256 }} + provenance-name: ${{ inputs.provenance-name }} + subject-artifact-name: ${{ steps.metadata.outputs.artifact_name }} + runs-on: ubuntu-latest + needs: [detect-env] + permissions: + id-token: write # Needed to create an OIDC token for keyless signing. + contents: read + actions: read # Needed to read workflow info. + steps: + # - name: Generate builder + # id: generate-builder + # continue-on-error: true + # uses: slsa-framework/slsa-github-generator/.github/actions/generate-builder@main + # with: + # repository: "${{ needs.detect-env.outputs.repository }}" + # ref: "${{ needs.detect-env.outputs.ref }}" + # go-version: "1.21" + # binary: "${{ env.BUILDER_BINARY }}" + # compile-builder: "${{ inputs.compile-generator }}" + # directory: "${{ env.BUILDER_DIR }}" + # allow-private-repository: ${{ inputs.private-repository }} + + - name: Extract subjects file metadata + id: metadata + continue-on-error: true + if: inputs.base64-subjects-as-file != '' + env: + UNTRUSTED_SUBJECTS_AS_FILE: "${{ inputs.base64-subjects-as-file }}" + run: | + set -euo pipefail + obj=$(echo "${UNTRUSTED_SUBJECTS_AS_FILE}" | base64 -d | jq) + echo "UNTRUSTED_SUBJECTS_AS_FILE: ${obj}" + artifact_name=$(echo "${obj}" | jq -r '.artifact_name') + filename=$(echo "${obj}" | jq -r '.filename') + sha256=$(echo "${obj}" | jq -r '.sha256') + + # shellcheck disable=SC2129 + echo "artifact_name=${artifact_name}" >> "$GITHUB_OUTPUT" + echo "filename=${filename}" >> "$GITHUB_OUTPUT" + echo "sha256=${sha256}" >> "$GITHUB_OUTPUT" + + - name: Download subjects file + id: download-file + continue-on-error: true + if: inputs.base64-subjects-as-file != '' + uses: slsa-framework/slsa-github-generator/.github/actions/secure-download-artifact@main + with: + name: "${{ steps.metadata.outputs.artifact_name }}" + path: "${{ steps.metadata.outputs.filename }}" + sha256: "${{ steps.metadata.outputs.sha256 }}" + + - name: Create subject file + id: create-file + continue-on-error: true + env: + UNTRUSTED_SUBJECTS: "${{ inputs.base64-subjects }}" + UNTRUSTED_SUBJECTS_FILENAME: "${{ steps.metadata.outputs.filename }}" + run: | + set -euo pipefail + # NOTE: SUBJECTS_FILE is trusted and declared at the top of the file. + if [[ -n "${UNTRUSTED_SUBJECTS_FILENAME}" ]]; then + mv "${UNTRUSTED_SUBJECTS_FILENAME}" "${SUBJECTS_FILENAME}" + else + echo "${UNTRUSTED_SUBJECTS}" > "${SUBJECTS_FILENAME}" + fi + + - name: Generate slsa layout file + id: slsa-layout + uses: ./internal/builders/slsa-layout + with: + provenance-name: "${{ inputs.provenance-name }}" + base64-subjects-file: "${{ env.SUBJECTS_FILENAME }}" + slsa-layout-file: "${{ env.SLSA_LAYOUT_FILENAME }}" + + - name: Generate attestations + id: attestations + uses: slsa-framework/slsa-github-generator/.github/actions/generate-attestations@main + with: + slsa-layout-file: ${{ env.SLSA_LAYOUT_FILENAME }} + predicate-file: ${{ env.SLSA_PREDICATE_FILE }} + output-folder: ${{ env.ATTESTATIONS_FOLDER }} + + - name: Sign attestations + id: sign-prov + uses: slsa-framework/slsa-github-generator/.github/actions/sign-attestations@main + with: + attestations: ${{ env.ATTESTATIONS_FOLDER }} + output-folder: ${{ env.ATTESTATIONS_FOLDER }} + + - name: Upload provenance + id: upload-prov + uses: slsa-framework/slsa-github-generator/.github/actions/secure-upload-artifact@main + with: + name: "${{ inputs.provenance-name }}" + path: "${{ env.ATTESTATIONS_FOLDER }}/${{ inputs.provenance-name }}" + + - name: Final outcome + id: final + env: + SUCCESS: ${{ steps.metadata.outcome != 'failure' && steps.download-file.outcome != 'failure' && steps.create-file.outcome != 'failure' && steps.sign-prov.outcome != 'failure' && steps.upload-prov.outcome != 'failure' }} + run: | + set -euo pipefail + echo "outcome=$([ "$SUCCESS" == "true" ] && echo "success" || echo "failure")" >> "$GITHUB_OUTPUT" + + # upload-assets uploads provenance to the release + # if github.ref is a tag and `upload-assets` is true. + upload-assets: + outputs: + outcome: ${{ steps.final.outputs.outcome }} + release-id: ${{ steps.release.outputs.id }} + runs-on: ubuntu-latest + needs: [detect-env, generator] + permissions: + contents: write # Needed to write artifacts to a release. + if: inputs.upload-assets && (startsWith(github.ref, 'refs/tags/') || inputs.upload-tag-name != '') + steps: + - name: Checkout builder repository + id: checkout-builder + continue-on-error: true + uses: slsa-framework/slsa-github-generator/.github/actions/secure-builder-checkout@main + with: + repository: "${{ needs.detect-env.outputs.repository }}" + ref: "${{ needs.detect-env.outputs.ref }}" + path: __BUILDER_CHECKOUT_DIR__ + + - name: Download the provenance + id: download-prov + continue-on-error: true + uses: ./__BUILDER_CHECKOUT_DIR__/.github/actions/secure-download-artifact + with: + name: "${{ needs.generator.outputs.provenance-name }}" + path: "${{ needs.generator.outputs.provenance-name }}" + sha256: "${{ needs.generator.outputs.provenance-sha256 }}" + + - name: Upload provenance + uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2.0.6 + id: release + with: + draft: ${{ inputs.draft-release }} + tag_name: ${{ inputs.upload-tag-name }} + files: | + ${{ needs.generator.outputs.provenance-name }} + + - name: Final outcome + id: final + env: + SUCCESS: ${{ steps.checkout-builder.outcome != 'failure' && steps.download-prov.outcome != 'failure' && steps.release.outcome != 'failure' }} + run: | + set -euo pipefail + echo "outcome=$([ "$SUCCESS" == "true" ] && echo "success" || echo "failure")" >> "$GITHUB_OUTPUT" + + # final fails or succeeds based on the value of `inputs.continue-on-error` + # and the outcome of previous jobs. + final: + outputs: + outcome: ${{ steps.final.outputs.outcome }} + runs-on: ubuntu-latest + needs: [detect-env, generator, upload-assets] + # Note: always run even if needed jobs are skipped. + if: always() + steps: + - name: Final outcome + id: final + env: + SUCCESS: ${{ needs.detect-env.outputs.outcome != 'failure' && needs.generator.outputs.outcome != 'failure' && needs.upload-assets.outputs.outcome != 'failure' }} + CONTINUE: ${{ inputs.continue-on-error }} + run: | + set -euo pipefail + echo "outcome=$([ "$SUCCESS" == "true" ] && echo "success" || echo "failure")" >> "$GITHUB_OUTPUT" + [ "$CONTINUE" == "true" ] || [ "$SUCCESS" == "true" ] || exit 27 + + # cleanup deletes internal artifacts used by the generator workflow + # TODO(#2382): Delete artifacts ${{ needs.generator.outputs.subject-artifact-name }} diff --git a/internal/builders/slsa-layout/action.yml b/internal/builders/slsa-layout/action.yml new file mode 100644 index 0000000000..d1c6cf79a9 --- /dev/null +++ b/internal/builders/slsa-layout/action.yml @@ -0,0 +1,53 @@ +# 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. + +name: SLSA3 Generic Generator internal wrapper +description: SLSA3 Generic Generator internal wrapper + +inputs: + provenance-name: + description: The artifact name of the signed provenance. The file must have the build.slsa extension. + required: true + base64-subjects-file: + description: "location of the file containing the base64 encoded subjects" + required: true + slsa-layout-file: + description: "Location to store the layout content" + required: true + +outputs: + attestation-name: + description: "The name of the attestation in the generated layout file." + value: ${{ steps.generate-layout.outputs.attestation-name }} + +runs: + using: "composite" + steps: + - id: generate-layout + shell: bash + env: + BASE64_SUBJECTS_FILE: ${{ inputs.base64-subjects-file }} + PROVENANCE_NAME: ${{ inputs.provenance-name }} + OUTPUT_FILE: ${{ inputs.slsa-layout-file }} + run: | + DIR="$( pwd )" + ( + cd ./../__TOOL_ACTION_DIR__/ && \ + ls -lahR && \ + go run . \ + --base64-subjects-file $DIR/$BASE64_SUBJECTS_FILE \ + --provenance-name $PROVENANCE_NAME \ + --output-file $DIR/$OUTPUT_FILE + ) + echo "attestation-name=${PROVENANCE_NAME}" >>"${GITHUB_OUTPUT}" diff --git a/internal/builders/slsa-layout/go.mod b/internal/builders/slsa-layout/go.mod new file mode 100644 index 0000000000..455dc1df39 --- /dev/null +++ b/internal/builders/slsa-layout/go.mod @@ -0,0 +1,3 @@ +module github.com/slsa-framework/slsa-github-generator/slsa-layout + +go 1.22 diff --git a/internal/builders/slsa-layout/main.go b/internal/builders/slsa-layout/main.go new file mode 100644 index 0000000000..ee38c85d6c --- /dev/null +++ b/internal/builders/slsa-layout/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "encoding/base64" + "encoding/json" + "flag" + "log" + "os" + "strings" +) + +type SLSALayout struct { + Version int `json:"version"` + Attestations []*Attestation `json:"attestations"` +} +type Attestation struct { + Name string `json:"name"` + Subjects []*Subject `json:"subjects"` +} + +type Subject struct { + Name string `json:"name"` + Digest map[string]string `json:"digest"` +} + +func main() { + base64Subjects := flag.String("base64-subjects", "", "a base64-encoded list of subjects") + base64SubjectsFile := flag.String("base64-subjects-file", "", "file with a base64-encoded list of subjects") + provenanceName := flag.String("provenance-name", "", "name of the provenance, including the .build.slsa suffix") + outputFile := flag.String("output-file", "", "outfile to write the SLSA layout to") + flag.Parse() + + if !strings.HasSuffix(*provenanceName, ".build.slsa") { + log.Fatalf("provenance name must have the .build.slsa suffix: %s", *provenanceName) + } + + var base64Content string + if *base64Subjects != "" { + base64Content = *base64Subjects + } else if *base64SubjectsFile != "" { + base64ContentBytes, err := os.ReadFile(*base64SubjectsFile) + if err != nil { + log.Fatalf("failed to read file %s: %v", *base64SubjectsFile, err) + } + base64Content = string(base64ContentBytes) + } else { + log.Fatal("either --base64-subjects or --base64-subjects-file must be set") + } + + decodedContent, err := base64.StdEncoding.DecodeString(base64Content) + if err != nil { + log.Fatalf("failed to decode base64 the content. Did you base64-encode?: %v", err) + } + + attestation := Attestation{ + Name: strings.TrimSuffix(*provenanceName, ".build.slsa"), + } + layout := SLSALayout{ + Version: 1, + Attestations: []*Attestation{&attestation}, + } + + lines := strings.Split(string(decodedContent), "\n") + for _, line := range lines { + if len(line) == 0 { + continue + } + parts := strings.SplitN(line, " ", 2) + if len(parts) != 2 { + log.Fatalf("invalid line, should be ` `: %s", line) + } + artifactDigest := parts[0] + artifactName := parts[1] + + subject := &Subject{ + Name: artifactName, + Digest: map[string]string{ + "sha256": artifactDigest, + }, + } + + attestation.Subjects = append(attestation.Subjects, subject) + } + if len(attestation.Subjects) == 0 { + log.Fatal("no subjects found") + } + + layoutBytes, err := json.Marshal(layout) + if err != nil { + log.Fatalf("failed to marshal layout: %v", err) + } + + f, err := os.Create(*outputFile) + if err != nil { + log.Fatalf("failed to create file %s: %v", *outputFile, err) + } + defer f.Close() + + _, err = f.Write(layoutBytes) + if err != nil { + log.Fatalf("failed to write layout to file: %v", err) + } + return +}