HH-462: harden release attestation semantics (#94)
* HH-462: bind release attestations to source and digest * HH-462: exercise release attestation production paths --------- Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Executable
+204
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
readonly statement_type=https://in-toto.io/Statement/v0.1
|
||||
readonly provenance_type=https://slsa.dev/provenance/v1
|
||||
readonly spdx_type=https://spdx.dev/Document
|
||||
|
||||
image_subject() {
|
||||
local image_ref=$1
|
||||
[[ $image_ref =~ ^(.+)@sha256:([0-9a-f]{64})$ ]] || {
|
||||
echo "Expected immutable sha256 image reference: $image_ref" >&2
|
||||
return 1
|
||||
}
|
||||
printf '%s\n%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}"
|
||||
}
|
||||
|
||||
create_provenance() {
|
||||
local output=$1 source_uri=$2 source_ref=$3 commit=$4 builder=$5 invocation=$6
|
||||
jq -n \
|
||||
--arg source_uri "$source_uri" \
|
||||
--arg source_ref "$source_ref" \
|
||||
--arg commit "$commit" \
|
||||
--arg builder "$builder" \
|
||||
--arg invocation "$invocation" \
|
||||
'{
|
||||
buildDefinition: {
|
||||
buildType: "https://github.com/Attestations/GitHubActionsWorkflow@v1",
|
||||
externalParameters: {source: {uri: $source_uri, ref: $source_ref}},
|
||||
internalParameters: {},
|
||||
resolvedDependencies: [{uri: $source_uri, digest: {gitCommit: $commit}}]
|
||||
},
|
||||
runDetails: {
|
||||
builder: {id: $builder},
|
||||
metadata: {invocationId: $invocation}
|
||||
}
|
||||
}' > "$output"
|
||||
}
|
||||
|
||||
extract_sbom() (
|
||||
local output=$1 image_ref=$2 wrapper
|
||||
wrapper=$(mktemp)
|
||||
trap 'rm -f "$wrapper"' EXIT
|
||||
docker buildx imagetools inspect "$image_ref" --format '{{ json .SBOM }}' > "$wrapper"
|
||||
jq -e '
|
||||
.SPDX
|
||||
| .SPDXID == "SPDXRef-DOCUMENT"
|
||||
and (.spdxVersion | startswith("SPDX-"))
|
||||
and (.packages | type == "array")
|
||||
' "$wrapper" > /dev/null
|
||||
jq '.SPDX' "$wrapper" > "$output"
|
||||
)
|
||||
|
||||
verify_subject() {
|
||||
local statement=$1 subject_ref=$2 expected_name expected_digest
|
||||
mapfile -t subject < <(image_subject "$subject_ref")
|
||||
expected_name=${subject[0]}
|
||||
expected_digest=${subject[1]}
|
||||
jq -e \
|
||||
--arg statement_type "$statement_type" \
|
||||
--arg expected_name "$expected_name" \
|
||||
--arg expected_digest "$expected_digest" '
|
||||
._type == $statement_type
|
||||
and (.subject | length == 1)
|
||||
and .subject[0].name == $expected_name
|
||||
and .subject[0].digest.sha256 == $expected_digest
|
||||
' "$statement" > /dev/null
|
||||
}
|
||||
|
||||
verify_statement() {
|
||||
local kind=$1 statement=$2 subject_ref=$3
|
||||
verify_subject "$statement" "$subject_ref"
|
||||
shift 3
|
||||
|
||||
case $kind in
|
||||
provenance)
|
||||
local source_uri=$1 source_ref=$2 commit=$3 builder=$4 invocation=$5
|
||||
jq -e \
|
||||
--arg predicate_type "$provenance_type" \
|
||||
--arg source_uri "$source_uri" \
|
||||
--arg source_ref "$source_ref" \
|
||||
--arg commit "$commit" \
|
||||
--arg builder "$builder" \
|
||||
--arg invocation "$invocation" '
|
||||
.predicateType == $predicate_type
|
||||
and .predicate.buildDefinition.buildType == "https://github.com/Attestations/GitHubActionsWorkflow@v1"
|
||||
and .predicate.buildDefinition.externalParameters.source.uri == $source_uri
|
||||
and .predicate.buildDefinition.externalParameters.source.ref == $source_ref
|
||||
and (.predicate.buildDefinition.resolvedDependencies | length == 1)
|
||||
and .predicate.buildDefinition.resolvedDependencies[0].uri == $source_uri
|
||||
and .predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit == $commit
|
||||
and .predicate.runDetails.builder.id == $builder
|
||||
and .predicate.runDetails.metadata.invocationId == $invocation
|
||||
' "$statement" > /dev/null
|
||||
;;
|
||||
sbom)
|
||||
jq -e \
|
||||
--arg predicate_type "$spdx_type" '
|
||||
.predicateType == $predicate_type
|
||||
and .predicate.SPDXID == "SPDXRef-DOCUMENT"
|
||||
and (.predicate.spdxVersion | startswith("SPDX-"))
|
||||
and (.predicate.packages | type == "array")
|
||||
' "$statement" > /dev/null
|
||||
;;
|
||||
*)
|
||||
echo "Unknown attestation kind: $kind" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
verify_attestation() (
|
||||
local kind=$1 lookup_ref=$2 subject_ref=$3 identity=$4 predicate_alias envelope statement payload
|
||||
shift 4
|
||||
case $kind in
|
||||
provenance) predicate_alias=slsaprovenance1 ;;
|
||||
sbom) predicate_alias=spdxjson ;;
|
||||
*) echo "Unknown attestation kind: $kind" >&2; return 1 ;;
|
||||
esac
|
||||
|
||||
envelope=$(mktemp)
|
||||
statement=$(mktemp)
|
||||
trap 'rm -f "$envelope" "$statement"' EXIT
|
||||
cosign verify-attestation \
|
||||
--type "$predicate_alias" \
|
||||
--certificate-identity "$identity" \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
"$lookup_ref" > "$envelope"
|
||||
payload=$(jq -sr -e 'map(select(.payload?))[0].payload' "$envelope")
|
||||
printf '%s' "$payload" | base64 --decode > "$statement"
|
||||
verify_statement "$kind" "$statement" "$subject_ref" "$@"
|
||||
)
|
||||
|
||||
attest_images() (
|
||||
local source_uri=$1 source_ref=$2 commit=$3 builder=$4 invocation=$5
|
||||
local image_ref sbom provenance temp_dir
|
||||
shift 5
|
||||
(($# > 0))
|
||||
temp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$temp_dir"' EXIT
|
||||
|
||||
for image_ref in "$@"; do
|
||||
sbom="$temp_dir/sbom.json"
|
||||
provenance="$temp_dir/provenance.json"
|
||||
extract_sbom "$sbom" "$image_ref"
|
||||
create_provenance "$provenance" "$source_uri" "$source_ref" "$commit" "$builder" "$invocation"
|
||||
cosign attest --yes --type spdxjson --predicate "$sbom" "$image_ref"
|
||||
cosign attest --yes --type slsaprovenance1 --predicate "$provenance" "$image_ref"
|
||||
done
|
||||
)
|
||||
|
||||
verify_image() {
|
||||
local lookup_ref=$1 subject_ref=$2 identity=$3 source_uri=$4 source_ref=$5 commit=$6 builder=$7 invocation=$8
|
||||
cosign verify \
|
||||
--certificate-identity "$identity" \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
"$lookup_ref"
|
||||
verify_attestation sbom "$lookup_ref" "$subject_ref" "$identity"
|
||||
verify_attestation provenance "$lookup_ref" "$subject_ref" "$identity" \
|
||||
"$source_uri" "$source_ref" "$commit" "$builder" "$invocation"
|
||||
}
|
||||
|
||||
verify_images() {
|
||||
local identity=$1 source_uri=$2 source_ref=$3 commit=$4 builder=$5 invocation=$6 image_ref
|
||||
shift 6
|
||||
(($# > 0))
|
||||
for image_ref in "$@"; do
|
||||
verify_image "$image_ref" "$image_ref" "$identity" \
|
||||
"$source_uri" "$source_ref" "$commit" "$builder" "$invocation"
|
||||
done
|
||||
}
|
||||
|
||||
verify_promoted() {
|
||||
local identity=$1 source_uri=$2 source_ref=$3 commit=$4 builder=$5 invocation=$6
|
||||
local entry subject_ref target expected actual
|
||||
shift 6
|
||||
(($# > 0))
|
||||
for entry in "$@"; do
|
||||
subject_ref=${entry%%|*}
|
||||
target=${entry#*|}
|
||||
expected=${subject_ref##*@}
|
||||
actual=$(crane digest "$target")
|
||||
[[ $actual == "$expected" ]] || {
|
||||
echo "Digest mismatch: $target resolved to $actual, expected $expected" >&2
|
||||
return 1
|
||||
}
|
||||
echo "$target -> $actual"
|
||||
verify_image "$target" "$subject_ref" "$identity" \
|
||||
"$source_uri" "$source_ref" "$commit" "$builder" "$invocation"
|
||||
done
|
||||
}
|
||||
|
||||
case ${1:-} in
|
||||
create-provenance) shift; create_provenance "$@" ;;
|
||||
extract-sbom) shift; extract_sbom "$@" ;;
|
||||
attest-images) shift; attest_images "$@" ;;
|
||||
verify-statement) shift; verify_statement "$@" ;;
|
||||
verify) shift; verify_attestation "$@" ;;
|
||||
verify-images) shift; verify_images "$@" ;;
|
||||
verify-promoted) shift; verify_promoted "$@" ;;
|
||||
*)
|
||||
echo "Usage: $0 {create-provenance|extract-sbom|attest-images|verify-statement|verify|verify-images|verify-promoted} ..." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user