-
Notifications
You must be signed in to change notification settings - Fork 429
chore(ci): introduce provenance and attestation in release #2746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
leandrodamascena
merged 15 commits into
aws-powertools:develop
from
heitorlessa:refactor/ci-seal
Jul 12, 2023
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
428a577
feat(ci): seal and seal-restore composite actions
heitorlessa ef9a654
feat(ci): upload provenance to release action
heitorlessa 3c3c16c
feat(ci): introduce provenance and attestation to release
heitorlessa 73d1dc1
chore(ci): add slsa generator in allow list
heitorlessa 159eba0
chore(ci): add script to verify provenance at ci in the future
heitorlessa c3427ba
chore(ci): use immutable commit for checksum file
heitorlessa 14f5f03
docs: update CD diagrams with provenance step
heitorlessa 89f5c78
chore: convert to american english
heitorlessa 0ff943f
chore: enforce layer to use sealed source code
heitorlessa 00bc556
docs(security): add instructions to verify build
heitorlessa 702927a
docs(security): re-add ci and cd practices as Simon suggested
heitorlessa f20f3f9
verify_provenance: adding command line argument
leandrodamascena 3c96196
Merge branch 'develop' into refactor/ci-seal
heitorlessa 6a38120
chore: remove test endpoints; last cleanup
heitorlessa b345eea
fix: adjust gh org+repo
heitorlessa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
name: "Restore sealed source code" | ||
description: "Restore sealed source code and confirm integrity hash" | ||
|
||
# PROCESS | ||
# | ||
# 1. Exports artifact name using Prefix + GitHub Run ID (unique for each release trigger) | ||
# 2. Compress entire source code as tarball OR given files | ||
# 3. Create and export integrity hash for tarball | ||
# 4. Upload artifact | ||
# 5. Remove archive | ||
|
||
# USAGE | ||
# | ||
# - name: Seal and upload | ||
# id: seal_source_code | ||
# uses: ./.github/actions/seal | ||
# with: | ||
# artifact_name_prefix: "source" | ||
# | ||
# - name: Restore sealed source code | ||
# uses: ./.github/actions/seal-restore | ||
# with: | ||
# integrity_hash: ${{ needs.seal_source_code.outputs.integrity_hash }} | ||
# artifact_name: ${{ needs.seal_source_code.outputs.artifact_name }} | ||
|
||
# NOTES | ||
# | ||
# To be used together with .github/actions/seal | ||
|
||
inputs: | ||
integrity_hash: | ||
description: "Integrity hash to verify" | ||
required: true | ||
artifact_name: | ||
description: "Sealed artifact name to restore" | ||
required: true | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- id: adjust-path | ||
run: echo "${{ github.action_path }}" >> $GITHUB_PATH | ||
shell: bash | ||
|
||
- name: Download artifacts | ||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 | ||
with: | ||
name: ${{ inputs.artifact_name }} | ||
path: . | ||
|
||
- id: integrity_hash | ||
name: Create integrity hash for downloaded artifact | ||
run: | | ||
HASH=$(sha256sum "${ARTIFACT_NAME}.tar" | awk '{print $1}') | ||
|
||
echo "current_hash=${HASH}" >> "$GITHUB_OUTPUT" | ||
env: | ||
ARTIFACT_NAME: ${{ inputs.artifact_name }} | ||
shell: bash | ||
|
||
- id: verify_hash | ||
name: Verify sealed artifact integrity hash | ||
run: test "${CURRENT_HASH}" = "${PROVIDED_HASH}" || exit 1 | ||
env: | ||
ARTIFACT_NAME: ${{ inputs.artifact_name }} | ||
PROVIDED_HASH: ${{ inputs.integrity_hash }} | ||
CURRENT_HASH: ${{ steps.integrity_hash.outputs.current_hash }} | ||
shell: bash | ||
|
||
# Restore and overwrite tarball in current directory | ||
- id: overwrite | ||
name: Extract tarball | ||
run: tar -xvf "${ARTIFACT_NAME}".tar | ||
env: | ||
ARTIFACT_NAME: ${{ inputs.artifact_name }} | ||
shell: bash | ||
|
||
- name: Remove archive | ||
run: rm -f "${ARTIFACT_NAME}.tar" | ||
env: | ||
ARTIFACT_NAME: ${{ inputs.artifact_name }} | ||
shell: bash |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
name: "Seal and hash source code" | ||
description: "Seal and export source code as a tarball artifact along with its integrity hash" | ||
|
||
# PROCESS | ||
# | ||
# 1. Exports artifact name using Prefix + GitHub Run ID (unique for each release trigger) | ||
# 2. Compress entire source code as tarball OR given files | ||
# 3. Create and export integrity hash for tarball | ||
# 4. Upload artifact | ||
# 5. Remove archive | ||
|
||
# USAGE | ||
# | ||
# - name: Seal and upload | ||
# id: seal_source_code | ||
# uses: ./.github/actions/seal | ||
# with: | ||
# artifact_name_prefix: "source" | ||
|
||
inputs: | ||
files: | ||
description: "Files to seal separated by space" | ||
required: false | ||
artifact_name_prefix: | ||
description: "Prefix to use when exporting artifact" | ||
required: true | ||
|
||
outputs: | ||
integrity_hash: | ||
description: "Source code integrity hash" | ||
value: ${{ steps.integrity_hash.outputs.integrity_hash }} | ||
artifact_name: | ||
description: "Artifact name containTemporary branch created with staged changed" | ||
value: ${{ steps.export_artifact_name.outputs.artifact_name }} | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- id: adjust-path | ||
run: echo "${{ github.action_path }}" >> $GITHUB_PATH | ||
shell: bash | ||
|
||
- id: export_artifact_name | ||
name: Export final artifact name | ||
run: echo "artifact_name=${ARTIFACT_PREFIX}-${GITHUB_RUN_ID}" >> "$GITHUB_OUTPUT" | ||
env: | ||
GITHUB_RUN_ID: ${{ github.run_id }} | ||
ARTIFACT_PREFIX: ${{ inputs.artifact_name_prefix }} | ||
shell: bash | ||
|
||
# By default, create a tarball of the current directory minus .git | ||
# otherwise it breaks GH Actions when restoring it | ||
- id: compress_all | ||
if: ${{ !inputs.files }} | ||
name: Create tarball for entire source | ||
run: tar --exclude-vcs -cvf "${ARTIFACT_NAME}".tar * | ||
env: | ||
ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} | ||
shell: bash | ||
|
||
# If a list of files are given, then create a tarball for those only | ||
- id: compress_selected_files | ||
if: ${{ inputs.files }} | ||
name: Create tarball for selected files | ||
run: tar --exclude-vcs -cvf "${ARTIFACT_NAME}".tar "${FILES}" | ||
env: | ||
FILES: ${{ inputs.files }} | ||
ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} | ||
shell: bash | ||
|
||
- id: integrity_hash | ||
name: Create and export integrity hash for tarball | ||
run: | | ||
HASH=$(sha256sum "${ARTIFACT_NAME}.tar" | awk '{print $1}') | ||
|
||
echo "integrity_hash=${HASH}" >> "$GITHUB_OUTPUT" | ||
env: | ||
ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} | ||
shell: bash | ||
|
||
- name: Upload artifacts | ||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 | ||
with: | ||
if-no-files-found: error | ||
name: ${{ steps.export_artifact_name.outputs.artifact_name }} | ||
path: ${{ steps.export_artifact_name.outputs.artifact_name }}.tar | ||
retention-days: 1 | ||
|
||
- name: Remove archive | ||
run: rm -f "${ARTEFACT_NAME}.tar" | ||
env: | ||
ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} | ||
shell: bash |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
name: "Upload provenance attestation to release" | ||
description: "Download and upload newly generated provenance attestation to latest release." | ||
|
||
# PROCESS | ||
# | ||
# 1. Downloads provenance attestation artifact generated earlier in the release pipeline | ||
# 2. Updates latest GitHub draft release pointing to newly git release tag | ||
# 3. Uploads provenance attestation file to latest GitHub draft release | ||
|
||
# USAGE | ||
# | ||
# - name: Upload provenance | ||
# id: upload-provenance | ||
# uses: ./.github/actions/upload-release-provenance | ||
# with: | ||
# release_version: ${{ needs.seal.outputs.RELEASE_VERSION }} | ||
# provenance_name: ${{needs.provenance.outputs.provenance-name}} | ||
# github_token: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
# NOTES | ||
# | ||
# There are no outputs. | ||
# | ||
|
||
inputs: | ||
provenance_name: | ||
description: "Provenance artifact name to download" | ||
required: true | ||
release_version: | ||
description: "Release version (e.g., 2.20.0)" | ||
required: true | ||
github_token: | ||
description: "GitHub token for GitHub CLI" | ||
required: true | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- id: adjust-path | ||
run: echo "${{ github.action_path }}" >> $GITHUB_PATH | ||
shell: bash | ||
|
||
- id: download-provenance | ||
name: Download newly generated provenance | ||
uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 | ||
with: | ||
name: ${{ inputs.provenance_name }} | ||
|
||
- id: sync-release-tag | ||
name: Update draft release tag to release commit tag | ||
run: | | ||
CURRENT_DRAFT_RELEASE=$(gh release list | awk '{ if ($2 == "Draft") print $1}') | ||
gh release edit "${CURRENT_DRAFT_RELEASE}" --tag v"${RELEASE_VERSION}" | ||
env: | ||
RELEASE_VERSION: ${{ inputs.release_version }} | ||
GH_TOKEN: ${{ inputs.github_token }} | ||
shell: bash | ||
|
||
- id: upload-provenance | ||
name: Upload provenance to release tag | ||
# clobber flag means overwrite release asset if available (eventual consistency, retried failed steps) | ||
run: gh release upload --clobber v"${RELEASE_VERSION}" "${PROVENANCE_FILE}" | ||
env: | ||
RELEASE_VERSION: ${{ inputs.release_version }} | ||
PROVENANCE_FILE: ${{ inputs.provenance_name }} | ||
GH_TOKEN: ${{ inputs.github_token }} | ||
shell: bash |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
#!/bin/bash | ||
set -uo pipefail # prevent accessing unset env vars, prevent masking pipeline errors to the next command | ||
|
||
#docs | ||
#title :verify_provenance.sh | ||
#description :This script will download and verify a signed Powertools for AWS Lambda (Python) release build with SLSA Verifier | ||
#author :@heitorlessa | ||
#date :July 1st 2023 | ||
#version :0.1 | ||
#usage :bash verify_provenance.sh {release version} | ||
#notes :Meant to use in GitHub Actions or locally (MacOS, Linux, WSL). | ||
#os_version :Ubuntu 22.04.2 LTS | ||
#============================================================================== | ||
|
||
# Check if RELEASE_VERSION is provided as a command line argument | ||
if [[ $# -eq 1 ]]; then | ||
export readonly RELEASE_VERSION="$1" | ||
else | ||
echo "ERROR: Please provider Powertools release version as a command line argument." | ||
echo "Example: bash verify_provenance.sh 2.20.0" | ||
exit 1 | ||
fi | ||
|
||
export readonly ARCHITECTURE=$(uname -m | sed 's/x86_64/amd64/g') # arm64, x86_64 ->amd64 | ||
export readonly OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]') # darwin, linux | ||
export readonly SLSA_VERIFIER_VERSION="2.3.0" | ||
export readonly SLSA_VERIFIER_CHECKSUM_FILE="SHA256SUM.md" | ||
export readonly SLSA_VERIFIER_BINARY="./slsa-verifier-${OS_NAME}-${ARCHITECTURE}" | ||
|
||
export readonly RELEASE_BINARY="aws_lambda_powertools-${RELEASE_VERSION}-py3-none-any.whl" | ||
export readonly ORG="aws-powertools" | ||
export readonly REPO="powertools-lambda-python" | ||
export readonly PROVENANCE_FILE="multiple.intoto.jsonl" | ||
|
||
export readonly FILES=("${SLSA_VERIFIER_BINARY}" "${SLSA_VERIFIER_CHECKSUM_FILE}" "${PROVENANCE_FILE}" "${RELEASE_BINARY}") | ||
|
||
function debug() { | ||
TIMESTAMP=$(date -u "+%FT%TZ") # 2023-05-10T07:53:59Z | ||
echo ""${TIMESTAMP}" DEBUG - $1" | ||
} | ||
|
||
function download_slsa_verifier() { | ||
debug "[*] Downloading SLSA Verifier for - Binary: slsa-verifier-${OS_NAME}-${ARCHITECTURE}" | ||
curl --location --silent -O "https://github.com/slsa-framework/slsa-verifier/releases/download/v${SLSA_VERIFIER_VERSION}/slsa-verifier-${OS_NAME}-${ARCHITECTURE}" | ||
|
||
debug "[*] Downloading SLSA Verifier checksums" | ||
curl --location --silent -O "https://raw.githubusercontent.com/slsa-framework/slsa-verifier/f59b55ef2190581d40fc1a5f3b7a51cab2f4a652/${SLSA_VERIFIER_CHECKSUM_FILE}" | ||
|
||
debug "[*] Verifying SLSA Verifier binary integrity" | ||
CURRENT_HASH=$(sha256sum "${SLSA_VERIFIER_BINARY}" | awk '{print $1}') | ||
if [[ $(grep "${CURRENT_HASH}" "${SLSA_VERIFIER_CHECKSUM_FILE}") ]]; then | ||
debug "[*] SLSA Verifier binary integrity confirmed" | ||
chmod +x "${SLSA_VERIFIER_BINARY}" | ||
else | ||
debug "[!] Failed integrity check for SLSA Verifier binary: ${SLSA_VERIFIER_BINARY}" | ||
exit 1 | ||
fi | ||
} | ||
|
||
function download_provenance() { | ||
debug "[*] Downloading attestation for - Release: https://github.com/${ORG}/${REPO}/releases/v${RELEASE_VERSION}" | ||
|
||
curl --location --silent -O "https://github.com/${ORG}/${REPO}/releases/download/v${RELEASE_VERSION}/${PROVENANCE_FILE}" | ||
} | ||
|
||
function download_release_artifact() { | ||
debug "[*] Downloading ${RELEASE_VERSION} release from PyPi" | ||
python -m pip download \ | ||
--only-binary=:all: \ | ||
--no-deps \ | ||
--quiet \ | ||
aws-lambda-powertools=="${RELEASE_VERSION}" | ||
} | ||
|
||
function verify_provenance() { | ||
debug "[*] Verifying attestation with slsa-verifier" | ||
"${SLSA_VERIFIER_BINARY}" verify-artifact \ | ||
--provenance-path "${PROVENANCE_FILE}" \ | ||
--source-uri github.com/${ORG}/${REPO} \ | ||
${RELEASE_BINARY} | ||
} | ||
|
||
function cleanup() { | ||
debug "[*] Cleaning up previously downloaded files" | ||
rm "${SLSA_VERIFIER_BINARY}" | ||
rm "${SLSA_VERIFIER_CHECKSUM_FILE}" | ||
rm "${PROVENANCE_FILE}" | ||
rm "${RELEASE_BINARY}" | ||
echo "${FILES[@]}" | xargs -n1 echo "Removed file: " | ||
} | ||
|
||
function main() { | ||
download_slsa_verifier | ||
download_provenance | ||
download_release_artifact | ||
verify_provenance | ||
cleanup | ||
} | ||
|
||
main | ||
|
||
# Lessons learned | ||
# | ||
# 1. If source doesn't match provenance | ||
# | ||
# FAILED: SLSA verification failed: source used to generate the binary does not match provenance: expected source 'awslabs/aws-lambda-powertools-python', got 'heitorlessa/aws-lambda-powertools-test' | ||
# | ||
# 2. Avoid building deps during download in Test registry endpoints | ||
# | ||
# FAILED: Could not find a version that satisfies the requirement poetry-core>=1.3.2 (from versions: 1.2.0) | ||
# |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.