|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Exercise the actual pre-publication steps; only the issuer request is synthetic. |
| 3 | +set -euo pipefail |
| 4 | +repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) |
| 5 | +publisher=$repo_root/.github/workflows/publish-age.yaml |
| 6 | +scratch=$(mktemp -d) |
| 7 | +trap 'rm -rf "$scratch"' EXIT |
| 8 | +mkdir "$scratch/bin" |
| 9 | +yq -r '.jobs.publish.steps[0].run' "$publisher" >"$scratch/source.sh" |
| 10 | +yq -r '.jobs.publish.steps[1].run' "$publisher" >"$scratch/identity.sh" |
| 11 | +cat >"$scratch/bin/curl" <<'ISSUER' |
| 12 | +#!/usr/bin/env bash |
| 13 | +set -euo pipefail |
| 14 | +touch "$FIXTURE_REQUEST" |
| 15 | +[[ $FIXTURE_MODE != unavailable ]] || exit 7 |
| 16 | +cat "$FIXTURE_RESPONSE" |
| 17 | +if [[ $FIXTURE_MODE == partial ]]; then exit 7; fi |
| 18 | +if [[ $FIXTURE_MODE == multi ]]; then cat "$FIXTURE_RESPONSE"; fi |
| 19 | +ISSUER |
| 20 | +chmod +x "$scratch/bin/curl" |
| 21 | +pin=0123456789abcdef0123456789abcdef01234567 |
| 22 | + |
| 23 | +# claims creates public synthetic issuer data, never a real credential. |
| 24 | +claims() { |
| 25 | + jq -cn --arg repository "$1" --arg workflow "$1/.github/workflows/publish-age.yaml@$pin" \ |
| 26 | + '{repository:$repository,repository_id:"1350892700",job_workflow_ref:$workflow}' |
| 27 | +} |
| 28 | + |
| 29 | +# run_case catches missing identity checks through observable failure and output boundaries. |
| 30 | +run_case() { |
| 31 | + local wanted=$1 label=$2 repository=$3 id=$4 body=$5 mode=${6:-success} |
| 32 | + local event=${7:-push} ref_type=${8:-tag} tag=${9:-v5.0.0} result=0 encoded expected |
| 33 | + encoded=$(printf '%s' "$body" | base64 | tr -d '\r\n=' | tr '/+' '_-') |
| 34 | + jq -cn --arg value "fixture.$encoded.fixture" '{value:$value}' >"$scratch/response" |
| 35 | + : >"$scratch/output" |
| 36 | + rm -f "$scratch/request" |
| 37 | + # The nested shell expands its own positional arguments, not this shell's. |
| 38 | + # shellcheck disable=SC2016 |
| 39 | + env -i PATH="$scratch/bin:$PATH" REPOSITORY="$repository" REPOSITORY_ID="$id" \ |
| 40 | + EVENT="$event" REF_TYPE="$ref_type" RELEASE_TAG="$tag" GITHUB_OUTPUT="$scratch/output" \ |
| 41 | + ACTIONS_ID_TOKEN_REQUEST_URL='https://issuer.example.test/token?fixture=1' \ |
| 42 | + ACTIONS_ID_TOKEN_REQUEST_TOKEN=synthetic FIXTURE_REQUEST="$scratch/request" \ |
| 43 | + FIXTURE_RESPONSE="$scratch/response" FIXTURE_MODE="$mode" \ |
| 44 | + /bin/bash -c '/bin/bash "$1" && /bin/bash "$2"' fixture \ |
| 45 | + "$scratch/source.sh" "$scratch/identity.sh" >"$scratch/stdout" 2>"$scratch/stderr" || result=$? |
| 46 | + if [[ $wanted == accept ]]; then |
| 47 | + expected="workflow=$repository/.github/workflows/publish-age.yaml@$pin" |
| 48 | + [[ $result == 0 && $(cat "$scratch/output") == "$expected" && -f $scratch/request ]] || { |
| 49 | + cat "$scratch/stderr" >&2 |
| 50 | + echo "AGE identity positive control failed: $label" >&2 |
| 51 | + exit 1 |
| 52 | + } |
| 53 | + else |
| 54 | + [[ $result != 0 && $result != 124 && $result != 127 && ! -s $scratch/output ]] || { |
| 55 | + echo "AGE identity accepted or published an invalid identity: $label (exit $result)" >&2 |
| 56 | + exit 1 |
| 57 | + } |
| 58 | + if [[ $wanted == source ]]; then |
| 59 | + [[ ! -e $scratch/request ]] || { |
| 60 | + echo "AGE identity contacted issuer before rejecting caller: $label" >&2 |
| 61 | + exit 1 |
| 62 | + } |
| 63 | + fi |
| 64 | + fi |
| 65 | +} |
| 66 | + |
| 67 | +original=devantler-tech/data-product-controller |
| 68 | +canonical=devantler-tech/khoist |
| 69 | +original_claims=$(claims "$original") |
| 70 | +# This must fail against the original name-only admission, after a real positive control. |
| 71 | +run_case accept current "$original" 1350892700 "$original_claims" |
| 72 | +run_case source reused-original-name "$original" 1 "$original_claims" |
| 73 | +run_case source missing-context-id "$original" '' "$original_claims" |
| 74 | +run_case source fork example/data-product-controller 1 "$original_claims" |
| 75 | +run_case source non-release "$original" 1350892700 "$original_claims" success workflow_dispatch |
| 76 | +run_case source branch "$original" 1350892700 "$original_claims" success push branch |
| 77 | +run_case source invalid-tag "$original" 1350892700 "$original_claims" success push tag main |
| 78 | +for repository in "$original" "$canonical"; do |
| 79 | + valid=$(claims "$repository") |
| 80 | + run_case accept "$repository" "$repository" 1350892700 "$valid" |
| 81 | + run_case accept proof-tag "$repository" 1350892700 "$valid" success push tag v0.0.0-age-proof.fixture |
| 82 | + run_case source reused-name "$repository" 1 "$valid" |
| 83 | + for change in \ |
| 84 | + '.repository_id="1"' 'del(.repository_id)' '.repository_id=1350892700' \ |
| 85 | + '.repository_id=[]' '.repository="example/fork"' \ |
| 86 | + '.job_workflow_ref="example/fork/.github/workflows/publish-age.yaml@0123456789abcdef0123456789abcdef01234567"' \ |
| 87 | + '.job_workflow_ref |= sub("@.*$"; "@refs/heads/main")' \ |
| 88 | + '.job_workflow_ref |= sub("@.*$"; "@v5.0.0")' \ |
| 89 | + '.job_workflow_ref |= sub("@.*$"; "@0123456")' \ |
| 90 | + 'del(.job_workflow_ref)'; do |
| 91 | + changed=$(printf '%s' "$valid" | jq -c "$change") |
| 92 | + run_case closed "$change" "$repository" 1350892700 "$changed" |
| 93 | + done |
| 94 | + other=$original |
| 95 | + if [[ $repository == "$original" ]]; then other=$canonical; fi |
| 96 | + changed=$(printf '%s' "$valid" | jq -c --arg other "$other" '.repository=$other') |
| 97 | + run_case closed cross-name-caller "$repository" 1350892700 "$changed" |
| 98 | + changed=$(printf '%s' "$valid" | jq -c --arg other "$other" \ |
| 99 | + '.job_workflow_ref=$other+"/.github/workflows/publish-age.yaml@0123456789abcdef0123456789abcdef01234567"') |
| 100 | + run_case closed cross-name-callee "$repository" 1350892700 "$changed" |
| 101 | + for mode in unavailable partial multi; do |
| 102 | + run_case closed "$mode-issuer-response" "$repository" 1350892700 "$valid" "$mode" |
| 103 | + done |
| 104 | + run_case closed malformed-claims "$repository" 1350892700 '{' |
| 105 | + run_case closed non-object-claims "$repository" 1350892700 '[]' |
| 106 | +done |
| 107 | + |
| 108 | +# Guard execution is useful only when GitHub supplies its trusted context at both boundaries. |
| 109 | +for step in 0 1; do |
| 110 | + # GitHub expressions must remain literal when inspecting workflow wiring. |
| 111 | + # shellcheck disable=SC2016 |
| 112 | + STEP=$step yq -e '.jobs.publish.steps[env(STEP)].env.REPOSITORY == "${{ github.repository }}" and |
| 113 | + .jobs.publish.steps[env(STEP)].env.REPOSITORY_ID == "${{ github.repository_id }}"' "$publisher" >/dev/null |
| 114 | +done |
| 115 | + |
| 116 | +# Run the documented independent command through rejecting signer/registry boundaries. |
| 117 | +awk '/^```bash$/{copy=1; next} copy && /^```$/{exit} copy{print}' \ |
| 118 | + "$repo_root/docs/postgresql-age-image.md" >"$scratch/verify.sh" |
| 119 | +cat >"$scratch/bin/cosign" <<'VERIFIER' |
| 120 | +#!/usr/bin/env bash |
| 121 | +set -euo pipefail |
| 122 | +touch "$FIXTURE_SIGNER" |
| 123 | +printf '%s\n' "$@" | cmp - "$FIXTURE_EXPECTED" |
| 124 | +touch "$FIXTURE_VERIFIED" |
| 125 | +VERIFIER |
| 126 | +cat >"$scratch/bin/docker" <<'REGISTRY' |
| 127 | +#!/usr/bin/env bash |
| 128 | +set -euo pipefail |
| 129 | +touch "$FIXTURE_REGISTRY" |
| 130 | +if [[ $1 == pull && $2 == "$AGE_IMAGE" && $# == 2 ]]; then exit 0; fi |
| 131 | +if [[ $1 == image && $2 == inspect && $3 == "$AGE_IMAGE" && $# == 5 ]]; then |
| 132 | + printf '%s\n' "$SOURCE_SHA" |
| 133 | + exit 0 |
| 134 | +fi |
| 135 | +exit 1 |
| 136 | +REGISTRY |
| 137 | +chmod +x "$scratch/bin/cosign" "$scratch/bin/docker" |
| 138 | + |
| 139 | +# verify_example binds the exact documented filters; mixed identities must stop before a pull. |
| 140 | +verify_example() { |
| 141 | + local wanted=$1 repository=$2 expected_repository=$3 result=0 |
| 142 | + local image |
| 143 | + image="ghcr.io/devantler-tech/data-product-controller-postgresql-age@sha256:$(printf '%064d' 0)" |
| 144 | + printf '%s\n' verify \ |
| 145 | + --certificate-identity "https://github.com/$expected_repository/.github/workflows/publish-age.yaml@$pin" \ |
| 146 | + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ |
| 147 | + --certificate-github-workflow-repository "$expected_repository" \ |
| 148 | + --certificate-github-workflow-ref refs/tags/v5.0.0 \ |
| 149 | + --certificate-github-workflow-sha "$pin" "$image" >"$scratch/expected" |
| 150 | + rm -f "$scratch/signer" "$scratch/registry" "$scratch/verified" |
| 151 | + env -i PATH="$scratch/bin:$PATH" RELEASE_REPOSITORY="$repository" AGE_IMAGE="$image" \ |
| 152 | + RELEASE_TAG=v5.0.0 SOURCE_SHA="$pin" PUBLISHER_SHA="$pin" \ |
| 153 | + FIXTURE_SIGNER="$scratch/signer" FIXTURE_REGISTRY="$scratch/registry" \ |
| 154 | + FIXTURE_VERIFIED="$scratch/verified" \ |
| 155 | + FIXTURE_EXPECTED="$scratch/expected" /bin/bash "$scratch/verify.sh" \ |
| 156 | + >"$scratch/stdout" 2>"$scratch/stderr" || result=$? |
| 157 | + if [[ $wanted == accept ]]; then |
| 158 | + [[ $result == 0 && -f $scratch/verified && -f $scratch/registry ]] || { |
| 159 | + echo "AGE documented verification rejected permitted repository: $repository" >&2 |
| 160 | + exit 1 |
| 161 | + } |
| 162 | + else |
| 163 | + [[ $result != 0 && ! -e $scratch/registry ]] || { |
| 164 | + echo "AGE documented verification continued after identity rejection: $repository" >&2 |
| 165 | + exit 1 |
| 166 | + } |
| 167 | + if [[ $wanted == source && -e $scratch/signer ]]; then |
| 168 | + echo "AGE documented verification contacted signer for unsupported repository" >&2 |
| 169 | + exit 1 |
| 170 | + fi |
| 171 | + fi |
| 172 | +} |
| 173 | +verify_example accept "$original" "$original" |
| 174 | +verify_example accept "$canonical" "$canonical" |
| 175 | +verify_example reject "$original" "$canonical" |
| 176 | +verify_example reject "$canonical" "$original" |
| 177 | +verify_example source example/fork "$original" |
| 178 | +verify_example source '' "$original" |
| 179 | +printf '%s\n' 'PASS: actual AGE publisher steps reject foreign callers and mismatched or incomplete issuer evidence without identity output' |
0 commit comments