Skip to content
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

chore(ci): introduce provenance and attestation in release #2746

Merged
merged 15 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/actions/seal-restore/action.yml
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
93 changes: 93 additions & 0 deletions .github/actions/seal/action.yml
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
67 changes: 67 additions & 0 deletions .github/actions/upload-release-provenance/action.yml
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
111 changes: 111 additions & 0 deletions .github/actions/verify-provenance/verify_provenance.sh
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 \
heitorlessa marked this conversation as resolved.
Show resolved Hide resolved
--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)
#
Loading